commit e7920416df1fd5a5ec6ec046a931cef5232b08af
| author | 斟酌 鵬兄 <tgckpg@gmail.com> |
| date | 2026-06-24T05:23:48Z |
| subject | Generatee commit tree files |
commit e7920416df1fd5a5ec6ec046a931cef5232b08af
Author: 斟酌 鵬兄 <tgckpg@gmail.com>
Date: 2026-06-24T05:23:48Z
Generatee commit tree files
---
Makefile | 2 +-
internal/buildinfo/buildinfo_gen.go | 4 +-
internal/render/manifest.go | 39 ++++----
internal/render/render.go | 64 ++++++-------
internal/render/rtree.go | 179 ++++++++++++++++++++++++++++++++++++
internal/render/templates.go | 4 +-
internal/server/server.go | 41 ++++++++-
internal/webhook/webhook.go | 6 ++
8 files changed, 281 insertions(+), 58 deletions(-)
diff --git a/Makefile b/Makefile
index ebcb615..213aaf3 100644
--- a/Makefile
+++ b/Makefile
@@ -60,7 +60,7 @@ fmt:
gofmt -w ./cmd ./internal
run: build
- $(BIN_DIR)/flatgit daemon -c examples/tinyproxy.json --v=2
+ $(BIN_DIR)/flatgit daemon -c examples/tinyproxy.json
vet:
go vet ./...
diff --git a/internal/buildinfo/buildinfo_gen.go b/internal/buildinfo/buildinfo_gen.go
index 5bf6f03..e3b58e6 100644
--- a/internal/buildinfo/buildinfo_gen.go
+++ b/internal/buildinfo/buildinfo_gen.go
@@ -2,6 +2,6 @@ package buildinfo
const (
Version = "dev"
- GitRevision = "748df2db506f433b5f56a298d27f616085da27aa"
- Timestamp = "20260622.100339"
+ GitRevision = "c61d98835bd35fc99be5c16051c45b2b87056a5e"
+ Timestamp = "20260624.051331"
)
diff --git a/internal/render/manifest.go b/internal/render/manifest.go
index c702e56..9c5b39e 100644
--- a/internal/render/manifest.go
+++ b/internal/render/manifest.go
@@ -2,7 +2,6 @@ package render
import (
"path"
- "strings"
"time"
"github.com/tgckpg/flatgit/internal/config"
@@ -36,19 +35,22 @@ type ManifestRepository struct {
}
type ManifestHumanRoutes struct {
- Index string `json:"index"`
- Log string `json:"log"`
- Refs string `json:"refs"`
+ Index string `json:"index"`
+ Log string `json:"log"`
+ Refs string `json:"refs"`
+ Tree string `json:"tree"`
+ Blob string `json:"blob"`
+ Commit string `json:"commit"`
}
type ManifestAPIRoutes struct {
- Self string `json:"self"`
- Refs string `json:"refs"`
- Commits string `json:"commits"`
- Tree string `json:"tree"`
- Commit string `json:"commit"`
- Blob string `json:"blob"`
- Raw string `json:"raw"`
+ Self string `json:"self"`
+ Refs string `json:"refs"`
+ Commits string `json:"commits"`
+ Tree string `json:"tree"`
+ Commit string `json:"commit"`
+ Blob *string `json:"blob,omitempty"`
+ Raw string `json:"raw"`
}
type ManifestCapabilities struct {
@@ -77,20 +79,23 @@ func NewManifest(repo config.Repo, defaultBranch string, defaultCommit string) M
DefaultCommit: defaultCommit,
DefaultRefSlug: refSlug(repo.DefaultBranch),
SitePath: repo.RepoBase(),
- OwnerSitePath: path.Dir(strings.TrimSuffix(repo.RepoBase(), "/")),
+ OwnerSitePath: path.Dir(repo.RepoBase()),
},
Human: ManifestHumanRoutes{
- Index: "./index.html",
- Log: "./log.html",
- Refs: "./refs.html",
+ Index: "./index.html",
+ Log: "./log.html",
+ Refs: "./refs.html",
+ Tree: "./tree/{ref}/",
+ Blob: "./blob/{ref}/{path}.html",
+ Commit: "./commit/{sha}.html",
},
Machine: ManifestAPIRoutes{
Self: "./manifest.json",
Refs: "./refs.json",
Commits: "./commits.json",
- Tree: "./tree/{ref}.json",
+ Tree: "./tree/{ref}/tree.json",
Commit: "./commit/{sha}.json",
- Blob: "./blob/{ref}/{path}.json",
+ Blob: nil,
Raw: "./raw/{ref}/{path}",
},
Capabilities: ManifestCapabilities{
diff --git a/internal/render/render.go b/internal/render/render.go
index da48675..c634bf3 100644
--- a/internal/render/render.go
+++ b/internal/render/render.go
@@ -13,7 +13,6 @@ import (
"strconv"
"strings"
"time"
- "unicode/utf8"
"github.com/tgckpg/flatgit/internal/config"
"github.com/tgckpg/flatgit/internal/gitcmd"
@@ -107,7 +106,14 @@ func (r *Renderer) RenderRepo(ctx context.Context, repo config.Repo) error {
return err
}
- page := basePage{RepoManifest: manifest, GeneratedAt: manifest.GeneratedAt.Format(time.RFC3339)}
+ branchSlug := refSlug(repo.DefaultBranch)
+
+ page := basePage{
+ RepoManifest: manifest,
+ Ref: branchSlug,
+ GeneratedAt: manifest.GeneratedAt.Format(time.RFC3339),
+ }
+
if err := renderTemplate(filepath.Join(next, "index.html"), indexTemplate, struct {
basePage
Refs []RefInfo
@@ -129,56 +135,43 @@ func (r *Renderer) RenderRepo(ctx context.Context, repo config.Repo) error {
return err
}
- branchSlug := refSlug(repo.DefaultBranch)
- if err := renderTemplate(filepath.Join(next, "tree", branchSlug, "index.html"), treeTemplate, struct {
- basePage
- Ref string
- Files []TreeEntry
- }{page, repo.DefaultBranch, tree}); err != nil {
+ if err := r.renderCommitFiles(ctx, repo, next, commit, branchSlug, repo.DefaultBranch, page); err != nil {
return err
}
for _, c := range commits {
- show, err := r.Git.Text(ctx, repo.MirrorDir, "show", "--date=iso-strict-local", "--stat", "--patch", "--find-renames", "--no-ext-diff", "--no-color", c.Hash)
+ show, err := r.Git.Text(
+ ctx,
+ repo.MirrorDir,
+ "show",
+ "--date=iso-strict-local",
+ "--stat",
+ "--patch",
+ "--find-renames",
+ "--no-ext-diff",
+ "--no-color",
+ c.Hash,
+ )
if err != nil {
return err
}
+
+ commitPage := page
+ commitPage.Ref = c.Hash
+
if err := renderTemplate(filepath.Join(next, "commit", c.Hash+".html"), commitTemplate, struct {
basePage
Commit CommitInfo
Show string
- }{page, c, show}); err != nil {
- return err
- }
- if err := writeJSON(filepath.Join(next, "commit", c.Hash+".json"), c); err != nil {
+ }{commitPage, c, show}); err != nil {
return err
}
- }
- for _, e := range tree {
- if e.Type != "blob" {
- continue
- }
- content, err := r.Git.Output(ctx, repo.MirrorDir, "show", "--date=iso-strict-local", commit+":"+e.Path)
- if err != nil {
- return err
- }
- rawPath := filepath.Join(next, "raw", branchSlug, filepath.FromSlash(e.Path))
- if err := writeFile(rawPath, content, 0o644); err != nil {
+ if err := writeJSON(filepath.Join(next, "commit", c.Hash+".json"), c); err != nil {
return err
}
- blobPath := filepath.Join(next, "blob", branchSlug, 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 {
+ if err := r.renderCommitFiles(ctx, repo, next, c.Hash, c.Hash, c.Hash, page); err != nil {
return err
}
}
@@ -285,6 +278,7 @@ func (r *Renderer) tree(ctx context.Context, repo config.Repo, ref string) ([]Tr
type basePage struct {
RepoManifest Manifest
+ Ref string
GeneratedAt string
}
diff --git a/internal/render/rtree.go b/internal/render/rtree.go
new file mode 100644
index 0000000..c53206b
--- /dev/null
+++ b/internal/render/rtree.go
@@ -0,0 +1,179 @@
+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
+}
diff --git a/internal/render/templates.go b/internal/render/templates.go
index 1c8a414..3e7a6d9 100644
--- a/internal/render/templates.go
+++ b/internal/render/templates.go
@@ -21,7 +21,7 @@ const layoutTemplate = `{{define "layout"}}<!doctype html>
<a href="{{.RepoManifest.Repository.SitePath}}">summary</a>
<a href="{{.RepoManifest.Repository.SitePath}}log.html">log</a>
<a href="{{.RepoManifest.Repository.SitePath}}refs.html">refs</a>
-<a href="{{.RepoManifest.Repository.SitePath}}tree/{{.RepoManifest.Repository.DefaultRefSlug}}/index.html">files</a>
+<a href="{{.RepoManifest.Repository.SitePath}}tree/{{.Ref}}/index.html">files</a>
<a href="{{.RepoManifest.Repository.SitePath}}manifest.json" title="express view for bots and agents">json</a>
</nav>
</header>
@@ -94,7 +94,7 @@ const treeTemplate = `{{define "pageTitle"}}{{.RepoManifest.Repository.FullName}
<table>
<tr><th>mode</th><th>type</th><th>path</th><th>size</th></tr>
{{range .Files}}
-<tr><td class="mono">{{.Mode}}</td><td>{{.Type}}</td><td class="mono"><a href="{{$.RepoManifest.Repository.SitePath}}blob/{{$.RepoManifest.Repository.DefaultRefSlug}}/{{.Path}}.html">{{.Path}}</a></td><td>{{.Size}}</td></tr>
+<tr><td class="mono">{{.Mode}}</td><td>{{.Type}}</td><td class="mono"><a href="{{$.RepoManifest.Repository.SitePath}}blob/{{$.Ref}}/{{.Path}}.html">{{.Path}}</a></td><td>{{.Size}}</td></tr>
{{end}}
</table>
{{end}}
diff --git a/internal/server/server.go b/internal/server/server.go
index 16f9692..4aa8471 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -87,14 +87,53 @@ func ListenAndServe(ctx context.Context, opts Options) error {
}
}
+func clientIP(r *http.Request) string {
+ if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
+ parts := strings.Split(xff, ",")
+ if len(parts) > 0 {
+ if ip := strings.TrimSpace(parts[0]); ip != "" {
+ return ip
+ }
+ }
+ }
+
+ if xrip := strings.TrimSpace(r.Header.Get("X-Real-IP")); xrip != "" {
+ return xrip
+ }
+
+ return ""
+}
+
func logRequests(log *slog.Logger, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, "/healthz") {
next.ServeHTTP(w, r)
return
}
+
start := time.Now()
next.ServeHTTP(w, r)
- log.Info("http request", "method", r.Method, "path", r.URL.Path, "remote", r.RemoteAddr, "dur", time.Since(start))
+
+ args := []any{
+ "method", r.Method,
+ "path", r.URL.Path,
+ "remote", r.RemoteAddr,
+ }
+
+ if ip := clientIP(r); ip != "" {
+ args = append(args, "client_ip", ip)
+ }
+
+ if xff := strings.TrimSpace(r.Header.Get("X-Forwarded-For")); xff != "" {
+ args = append(args, "x_forwarded_for", xff)
+ }
+
+ if xrip := strings.TrimSpace(r.Header.Get("X-Real-IP")); xrip != "" {
+ args = append(args, "x_real_ip", xrip)
+ }
+
+ args = append(args, "dur", time.Since(start))
+
+ log.Info("http request", args...)
})
}
diff --git a/internal/webhook/webhook.go b/internal/webhook/webhook.go
index 0c6d468..a9a3687 100644
--- a/internal/webhook/webhook.go
+++ b/internal/webhook/webhook.go
@@ -44,9 +44,15 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
log := h.Logger
if log != nil {
+
+ xff := r.Header.Get("X-Forwarded-For")
+ xrealip := r.Header.Get("X-Real-IP")
+
log.Info("webhook request",
"method", r.Method,
"path", r.URL.Path,
+ "x_forwarded_for", xff,
+ "x_real_ip", xrealip,
"remote", r.RemoteAddr,
"event", r.Header.Get("X-Gitea-Event"),
"content_type", r.Header.Get("Content-Type"),