penguin/flatgit

flatgit showcase

internal/render/rtree.go

raw ยท 3638 bytes

package render

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"html/template"
	"io"
	"io/fs"
	"os"
	"path/filepath"
	"unicode/utf8"

	"github.com/tgckpg/flatgit/internal/config"
)

func fileExists(path string) bool {
	_, err := os.Stat(path)
	return err == nil
}

func linkOrCopy(src, dst string, mode fs.FileMode) error {
	if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
		return err
	}

	// If this is a re-render into a fresh `next` dir this usually won't exist,
	// but removing makes the helper safe for reuse.
	if err := os.Remove(dst); err != nil && !errors.Is(err, os.ErrNotExist) {
		return err
	}

	if err := os.Link(src, dst); err == nil {
		return nil
	}

	in, err := os.Open(src)
	if err != nil {
		return err
	}
	defer in.Close()

	out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode)
	if err != nil {
		return err
	}

	_, copyErr := io.Copy(out, in)
	closeErr := out.Close()

	if copyErr != nil {
		return copyErr
	}
	return closeErr
}

func (r *Renderer) renderCommitFiles(
	ctx context.Context,
	repo config.Repo,
	next string,
	commit string,
	revSlug string,
	refLabel string,
	page basePage,
) error {
	tree, err := r.tree(ctx, repo, commit)
	if err != nil {
		return err
	}

	if err := writeJSON(filepath.Join(next, "tree", revSlug, "tree.json"), tree); err != nil {
		return err
	}

	if err := renderTemplate(filepath.Join(next, "tree", revSlug, "index.html"), treeTemplate, struct {
		basePage
		Ref   string
		Files []TreeEntry
	}{page, refLabel, tree}); err != nil {
		return err
	}

	for _, e := range tree {
		if e.Type != "blob" {
			continue
		}

		if err := r.renderBlobEntry(ctx, repo, next, commit, revSlug, page, e); err != nil {
			return err
		}
	}

	return nil
}

func (r *Renderer) renderBlobEntry(
	ctx context.Context,
	repo config.Repo,
	next string,
	commit string,
	revSlug string,
	page basePage,
	e TreeEntry,
) error {
	if e.Hash == "" {
		return fmt.Errorf("tree entry %q has empty blob hash", e.Path)
	}

	blobID := e.Hash

	rawObjectPath := filepath.Join(
		next,
		"_objects",
		"raw",
		blobID[:2],
		blobID,
	)

	if !fileExists(rawObjectPath) {
		content, err := r.Git.Output(
			ctx,
			repo.MirrorDir,
			"cat-file",
			"-p",
			blobID,
		)
		if err != nil {
			return fmt.Errorf("read blob %s for %s at %s: %w", blobID, e.Path, commit, err)
		}

		if err := writeFile(rawObjectPath, content, 0o644); err != nil {
			return fmt.Errorf("write raw object %s: %w", rawObjectPath, err)
		}
	}

	rawPath := filepath.Join(
		next,
		"raw",
		revSlug,
		filepath.FromSlash(e.Path),
	)

	if err := linkOrCopy(rawObjectPath, rawPath, 0o644); err != nil {
		return fmt.Errorf("link/copy raw %s -> %s: %w", rawObjectPath, rawPath, err)
	}

	content, err := os.ReadFile(rawObjectPath)
	if err != nil {
		return fmt.Errorf("read raw object %s: %w", rawObjectPath, err)
	}

	blobPath := filepath.Join(
		next,
		"blob",
		revSlug,
		filepath.FromSlash(e.Path)+".html",
	)

	blob := blobView{
		Path:    e.Path,
		Size:    int64(len(content)),
		RawHref: relPath(filepath.Dir(blobPath), rawPath),
	}

	if len(content) > maxBlobHTMLBytes || bytes.IndexByte(content, 0) >= 0 || !utf8.Valid(content) {
		blob.Binary = true
	} else {
		blob.Text = string(content)
	}

	if !blob.Binary && isMarkdownPath(blob.Path) {
		htmlBytes, err := renderMarkdownSafe(content)
		if err != nil {
			return err
		}

		blob.HTML = template.HTML(htmlBytes)
		blob.RenderedMarkdown = true
	}

	if err := renderTemplate(blobPath, blobTemplate, struct {
		basePage
		Blob blobView
	}{page, blob}); err != nil {
		return fmt.Errorf("render blob page %s: %w", blobPath, err)
	}

	return nil
}