internal/render/rtree.go
raw ยท 3414 bytes
package render
import (
"bytes"
"context"
"errors"
"fmt"
"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 err := renderTemplate(blobPath, blobTemplate, struct {
basePage
Blob blobView
}{page, blob}); err != nil {
return fmt.Errorf("render blob page %s: %w", blobPath, err)
}
return nil
}