penguin/flatgit

flatgit showcase

commit f69874987a5dea73355979b0fbe38644d85eb0d0

author斟酌 鵬兄 <tgckpg@gmail.com>
date2026-06-18T22:39:51Z
subjectRemoved Slug() and use RepoBase() instead
commit f69874987a5dea73355979b0fbe38644d85eb0d0
Author: 斟酌 鵬兄 <tgckpg@gmail.com>
Date:   2026-06-18T22:39:51Z

    Removed Slug() and use RepoBase() instead
---
 .gitignore                          |  1 +
 cmd/flatgit/main.go                 | 37 +++++++++++++++--
 internal/buildinfo/buildinfo_gen.go |  4 +-
 internal/config/config.go           | 59 ++++++++++++++++----------
 internal/config/config_test.go      |  4 +-
 internal/render/manifest.go         |  2 +-
 internal/webhook/webhook.go         | 82 +++++++++++++++++++++++++++++++++++--
 7 files changed, 157 insertions(+), 32 deletions(-)

diff --git a/.gitignore b/.gitignore
index 8b87ab2..5342bc1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@
 out/
 bin/
 build.env.work
+values.work.yaml
 *.test
 *.out
 *.swp
diff --git a/cmd/flatgit/main.go b/cmd/flatgit/main.go
index 98eb00c..d0b141d 100644
--- a/cmd/flatgit/main.go
+++ b/cmd/flatgit/main.go
@@ -145,25 +145,56 @@ func daemonCmd(log *slog.Logger, args []string) error {
 	})
 }
 
+func repoMatches(repo config.Repo, name string) bool {
+	name = strings.TrimSpace(name)
+	name = strings.Trim(name, "/")
+
+	if name == "" {
+		return true
+	}
+
+	fullName := strings.Trim(repo.FullName(), "/")
+	repoBase := strings.Trim(repo.RepoBase(), "/")
+
+	return strings.EqualFold(repo.Name, name) ||
+		strings.EqualFold(fullName, name) ||
+		strings.EqualFold(repoBase, name)
+}
+
 func renderConfigured(ctx context.Context, log *slog.Logger, cfg *config.Config, repoName string, fetch bool) error {
 	git := gitcmd.New(cfg.Git.Command)
 	r := render.New(git, cfg.PublicURL, cfg.Render.MaxCommits)
+
+	matched := false
+
 	for i := range cfg.Repos {
 		repo := cfg.Repos[i]
-		if repoName != "" && !strings.EqualFold(repo.Name, repoName) && !strings.EqualFold(repo.Slug(), repoName) && !strings.EqualFold(repo.FullName(), repoName) {
+
+		if repoName != "" && !repoMatches(repo, repoName) {
 			continue
 		}
-		log.Info("rendering repo", "repo", repo.FullName())
+
+		matched = true
+
+		log.Info("rendering repo", "repo", repo.FullName(), "base", repo.RepoBase())
+
 		if fetch {
 			if err := git.EnsureMirror(ctx, repo, cfg.CloneTimeout(), cfg.FetchTimeout()); err != nil {
 				return err
 			}
 		}
+
 		if err := r.RenderRepo(ctx, repo); err != nil {
 			return err
 		}
-		log.Info("rendered repo", "repo", repo.FullName(), "output", repo.OutputDir)
+
+		log.Info("rendered repo", "repo", repo.FullName(), "base", repo.RepoBase(), "output", repo.OutputDir)
+	}
+
+	if repoName != "" && !matched {
+		return fmt.Errorf("repo not configured: %s", repoName)
 	}
+
 	return nil
 }
 
diff --git a/internal/buildinfo/buildinfo_gen.go b/internal/buildinfo/buildinfo_gen.go
index 0326bbb..89801d4 100644
--- a/internal/buildinfo/buildinfo_gen.go
+++ b/internal/buildinfo/buildinfo_gen.go
@@ -2,6 +2,6 @@ package buildinfo
 
 const (
     Version     = "dev"
-    GitRevision = "bdd09021663dcb4b6d0267105cd47964ff810c51"
-    Timestamp   = "20260618.214049"
+    GitRevision = "ae2ca700755df37c186d45857ad98648ad940781"
+    Timestamp   = "20260618.222654"
 )
diff --git a/internal/config/config.go b/internal/config/config.go
index d0eaa96..cbd894d 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -4,6 +4,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"net/url"
 	"os"
 	"path/filepath"
 	"strings"
@@ -43,6 +44,9 @@ type Repo struct {
 	MirrorDir     string `json:"mirror_dir"`
 	OutputDir     string `json:"output_dir"`
 	Description   string `json:"description"`
+
+	fullName string
+	repoBase string
 }
 
 func Load(path string) (*Config, error) {
@@ -103,10 +107,10 @@ func (c *Config) ApplyDefaults() error {
 			r.DefaultBranch = "main"
 		}
 		if r.MirrorDir == "" {
-			r.MirrorDir = filepath.Join(c.DataDir, "repos", r.Slug()+".git")
+			r.MirrorDir = filepath.Join(c.DataDir, "repos", r.RepoBase()+".git")
 		}
 		if r.OutputDir == "" {
-			r.OutputDir = filepath.Join(c.DataDir, "www", r.Slug())
+			r.OutputDir = filepath.Join(c.DataDir, "www", r.RepoBase())
 		}
 	}
 	return nil
@@ -124,41 +128,54 @@ func (c *Config) WebRoot() string {
 	return filepath.Join(c.DataDir, "www")
 }
 
-func (c *Config) RepoByName(name string) (*Repo, bool) {
-	name = cleanName(name)
-	for i := range c.Repos {
-		if c.Repos[i].Name == name || c.Repos[i].Slug() == name || c.Repos[i].FullName() == name {
-			return &c.Repos[i], true
+func (c Config) RepoByName(name string) (*Repo, bool) {
+	for _, repo := range c.Repos {
+		if repo.FullName() == name || repo.Name == name {
+			return &repo, true
 		}
 	}
-	return nil, false
+	return &Repo{}, false
 }
 
-func (c *Config) RepoByWebhookName(fullName string) (*Repo, bool) {
-	fullName = strings.TrimSpace(fullName)
-	for i := range c.Repos {
-		r := &c.Repos[i]
-		if fullName == r.FullName() || fullName == r.Name || fullName == r.Slug() {
-			return r, true
+func (c Config) RepoByWebhookName(name string) (Repo, bool) {
+	for _, repo := range c.Repos {
+		if repo.FullName() == name {
+			return repo, true
 		}
-	}
-	return nil, false
-}
 
-func (r Repo) Slug() string {
-	if r.Owner == "" {
-		return cleanName(r.Name)
+		// Fallback for single-owner or old/simple configs.
+		if repo.Name == name {
+			return repo, true
+		}
 	}
-	return cleanName(r.Owner + "_" + r.Name)
+
+	return Repo{}, false
 }
 
 func (r Repo) FullName() string {
+	if r.fullName != "" {
+		return r.fullName
+	}
+
 	if r.Owner == "" {
 		return r.Name
 	}
+
 	return r.Owner + "/" + r.Name
 }
 
+func (r Repo) RepoBase() string {
+	if r.repoBase != "" {
+		return r.repoBase
+	}
+
+	if r.Owner == "" {
+		return "/" + url.PathEscape(r.Name) + "/"
+	}
+
+	return "/" + url.PathEscape(r.Owner) + "/" + url.PathEscape(r.Name) + "/"
+}
+
 func parseDuration(s string, fallback time.Duration) time.Duration {
 	d, err := time.ParseDuration(s)
 	if err != nil {
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index 73b1e97..6927f95 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -10,8 +10,8 @@ func TestDefaults(t *testing.T) {
 	if cfg.Addr != ":8080" {
 		t.Fatalf("addr = %q", cfg.Addr)
 	}
-	if cfg.Repos[0].Slug() != "penguin_test-repo" {
-		t.Fatalf("slug = %q", cfg.Repos[0].Slug())
+	if cfg.Repos[0].RepoBase() != "/penguin/test-repo/" {
+		t.Fatalf("slug = %q", cfg.Repos[0].RepoBase())
 	}
 	if cfg.Repos[0].MirrorDir == "" || cfg.Repos[0].OutputDir == "" {
 		t.Fatalf("paths were not defaulted")
diff --git a/internal/render/manifest.go b/internal/render/manifest.go
index da6f7df..faf0f72 100644
--- a/internal/render/manifest.go
+++ b/internal/render/manifest.go
@@ -74,7 +74,7 @@ func NewManifest(repo config.Repo, defaultBranch string, defaultCommit string) M
 			DefaultBranch:  defaultBranch,
 			DefaultCommit:  defaultCommit,
 			DefaultRefSlug: refSlug(repo.DefaultBranch),
-			SitePath:       "/" + repo.Slug() + "/",
+			SitePath:       repo.RepoBase(),
 		},
 		Human: ManifestHumanRoutes{
 			Index: "./index.html",
diff --git a/internal/webhook/webhook.go b/internal/webhook/webhook.go
index 1d73e40..0c6d468 100644
--- a/internal/webhook/webhook.go
+++ b/internal/webhook/webhook.go
@@ -41,42 +41,118 @@ func (h *Handler) Register(mux *http.ServeMux) {
 }
 
 func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	log := h.Logger
+
+	if log != nil {
+		log.Info("webhook request",
+			"method", r.Method,
+			"path", r.URL.Path,
+			"remote", r.RemoteAddr,
+			"event", r.Header.Get("X-Gitea-Event"),
+			"content_type", r.Header.Get("Content-Type"),
+			"delivery", r.Header.Get("X-Gitea-Delivery"),
+			"signature", r.Header.Get("X-Gitea-Signature") != "",
+			"signature_256", r.Header.Get("X-Gitea-Signature-256") != "",
+		)
+	}
+
 	if r.Method != http.MethodPost {
+		if log != nil {
+			log.Warn("webhook rejected", "reason", "method not allowed", "method", r.Method)
+		}
 		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
 		return
 	}
+
 	if h.MaxBody <= 0 {
 		h.MaxBody = 1 << 20
 	}
+
 	body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, h.MaxBody))
 	if err != nil {
+		if log != nil {
+			log.Warn("webhook rejected", "reason", "bad body", "error", err)
+		}
 		http.Error(w, "bad body", http.StatusBadRequest)
 		return
 	}
+
+	if log != nil {
+		log.Info("webhook body read", "bytes", len(body))
+	}
+
 	if !verifySignature(h.Config.Webhook.Secret, body, r) {
+		if log != nil {
+			log.Warn("webhook rejected",
+				"reason", "bad signature",
+				"secret_configured", h.Config.Webhook.Secret != "",
+				"signature", r.Header.Get("X-Gitea-Signature"),
+				"signature_256", r.Header.Get("X-Gitea-Signature-256"),
+			)
+		}
 		http.Error(w, "bad signature", http.StatusUnauthorized)
 		return
 	}
 
 	var p payload
 	if err := json.Unmarshal(body, &p); err != nil {
+		if log != nil {
+			log.Warn("webhook rejected", "reason", "bad json", "error", err)
+		}
 		http.Error(w, "bad json", http.StatusBadRequest)
 		return
 	}
+
 	fullName := firstNonEmpty(p.Repository.FullName, p.Repository.Name)
+
+	if log != nil {
+		log.Info("webhook payload",
+			"ref", p.Ref,
+			"repo_name", p.Repository.Name,
+			"repo_full_name", p.Repository.FullName,
+			"selected_name", fullName,
+		)
+	}
+
 	if fullName == "" {
+		if log != nil {
+			log.Warn("webhook rejected", "reason", "missing repository name")
+		}
 		http.Error(w, "missing repository name", http.StatusBadRequest)
 		return
 	}
+
 	repo, ok := h.Config.RepoByWebhookName(fullName)
 	if !ok {
+		if log != nil {
+			var configured []string
+			for _, r := range h.Config.Repos {
+				configured = append(configured, r.FullName())
+			}
+
+			log.Warn("webhook rejected",
+				"reason", "repo not configured",
+				"payload_repo", fullName,
+				"configured_repos", configured,
+			)
+		}
+
 		http.Error(w, fmt.Sprintf("repo not configured: %s", fullName), http.StatusNotFound)
 		return
 	}
-	queued := h.Queue.Enqueue(repo.Name)
-	if h.Logger != nil {
-		h.Logger.Info("webhook received", "repo", repo.FullName(), "ref", p.Ref, "queued", queued)
+
+	queued := h.Queue.Enqueue(repo.FullName())
+
+	if log != nil {
+		h.Logger.Info("webhook accepted",
+			"payload_repo", fullName,
+			"repo", repo.FullName(),
+			"queue_name", repo.Name,
+			"ref", p.Ref,
+			"queued", queued,
+		)
 	}
+
 	w.Header().Set("Content-Type", "application/json")
 	w.WriteHeader(http.StatusAccepted)
 	_, _ = fmt.Fprintf(w, `{"ok":true,"queued":%t,"repo":%q}`+"\n", queued, repo.FullName())