commit 249de61fd9ac8f7c418b6a45f54f28ddfb220173
| author | 斟酌 鵬兄 <tgckpg@gmail.com> |
| date | 2026-10-05T04:21:09Z |
| subject | Added git clone support |
commit 249de61fd9ac8f7c418b6a45f54f28ddfb220173
Author: 斟酌 鵬兄 <tgckpg@gmail.com>
Date: 2026-10-05T04:21:09Z
Added git clone support
---
cmd/flatgit/main.go | 2 +
internal/buildinfo/buildinfo_gen.go | 4 +-
internal/server/git_http.go | 189 ++++++++++++++++++++++++++++++++++++
internal/server/server.go | 21 +++-
4 files changed, 213 insertions(+), 3 deletions(-)
diff --git a/cmd/flatgit/main.go b/cmd/flatgit/main.go
index b290789..48838ec 100644
--- a/cmd/flatgit/main.go
+++ b/cmd/flatgit/main.go
@@ -163,6 +163,8 @@ func daemonCmd(log *slog.Logger, level *slog.LevelVar, args []string) error {
WebhookMux: func(mux *http.ServeMux) {
wh.Register(mux)
},
+ GitCommand: cfg.Git.Command,
+ Repos: cfg.Repos,
})
}
diff --git a/internal/buildinfo/buildinfo_gen.go b/internal/buildinfo/buildinfo_gen.go
index c7fcd79..be9bc49 100644
--- a/internal/buildinfo/buildinfo_gen.go
+++ b/internal/buildinfo/buildinfo_gen.go
@@ -2,6 +2,6 @@ package buildinfo
const (
Version = "dev"
- GitRevision = "b4e8406c82460f7b26dbf885ff5c6bf9a0b532f5"
- Timestamp = "20260704.125746"
+ GitRevision = "351aa29cde84023a850bad9d3ede7f44a18b85e3"
+ Timestamp = "20261005.041719"
)
diff --git a/internal/server/git_http.go b/internal/server/git_http.go
new file mode 100644
index 0000000..2900b85
--- /dev/null
+++ b/internal/server/git_http.go
@@ -0,0 +1,189 @@
+package server
+
+import (
+ "bytes"
+ "log/slog"
+ "net/http"
+ "os"
+ "os/exec"
+ "strings"
+
+ "github.com/tgckpg/flatgit/internal/config"
+)
+
+type gitHTTP struct {
+ gitCommand string
+ logger *slog.Logger
+
+ // Exact HTTP repository base path -> bare mirror directory.
+ //
+ // Example:
+ // /penguin/flatgit -> /var/lib/flatgit/repos/...
+ repos map[string]string
+}
+
+func newGitHTTP(
+ gitCommand string,
+ repos []config.Repo,
+ logger *slog.Logger,
+) *gitHTTP {
+ if gitCommand == "" {
+ gitCommand = "git"
+ }
+
+ g := &gitHTTP{
+ gitCommand: gitCommand,
+ logger: logger,
+ repos: make(map[string]string, len(repos)*2),
+ }
+
+ for _, repo := range repos {
+ base := strings.TrimSuffix(repo.RepoBase(), "/")
+
+ g.repos[base] = repo.MirrorDir
+
+ // Supporting the conventional .git form is basically free.
+ g.repos[base+".git"] = repo.MirrorDir
+ }
+
+ return g
+}
+
+// ServeHTTP handles only the two endpoints needed for read-only Smart HTTP.
+//
+// Returns true if the request belonged to the Git transport.
+// Everything else should fall through to Flatgit's static server.
+func (g *gitHTTP) ServeHTTP(w http.ResponseWriter, r *http.Request) bool {
+ path := strings.TrimSuffix(r.URL.Path, "/")
+
+ /*
+ GET /owner/repo/info/refs?service=git-upload-pack
+ */
+ if r.Method == http.MethodGet &&
+ strings.HasSuffix(path, "/info/refs") {
+
+ base := strings.TrimSuffix(path, "/info/refs")
+ mirror, ok := g.repos[base]
+ if !ok {
+ return false
+ }
+
+ if r.URL.Query().Get("service") != "git-upload-pack" {
+ http.Error(w, "unsupported git service", http.StatusForbidden)
+ return true
+ }
+
+ g.infoRefs(w, r, mirror)
+ return true
+ }
+
+ /*
+ POST /owner/repo/git-upload-pack
+ */
+ if r.Method == http.MethodPost &&
+ strings.HasSuffix(path, "/git-upload-pack") {
+
+ base := strings.TrimSuffix(path, "/git-upload-pack")
+ mirror, ok := g.repos[base]
+ if !ok {
+ return false
+ }
+
+ g.uploadPack(w, r, mirror)
+ return true
+ }
+
+ return false
+}
+
+func (g *gitHTTP) command(r *http.Request, args ...string) *exec.Cmd {
+ cmd := exec.CommandContext(r.Context(), g.gitCommand, args...)
+
+ cmd.Env = append(
+ os.Environ(),
+
+ // upload-pack is read-only. Don't let Git perform optional
+ // repository maintenance or locking as a side effect.
+ "GIT_OPTIONAL_LOCKS=0",
+ )
+
+ // Git HTTP protocol negotiation, particularly protocol v2.
+ if protocol := r.Header.Get("Git-Protocol"); protocol != "" {
+ cmd.Env = append(cmd.Env, "GIT_PROTOCOL="+protocol)
+ }
+
+ return cmd
+}
+
+func (g *gitHTTP) infoRefs(
+ w http.ResponseWriter,
+ r *http.Request,
+ mirror string,
+) {
+ cmd := g.command(
+ r,
+ "upload-pack",
+ "--strict",
+ "--http-backend-info-refs",
+ mirror,
+ )
+
+ var stderr bytes.Buffer
+
+ cmd.Stdout = w
+ cmd.Stderr = &stderr
+
+ w.Header().Set(
+ "Content-Type",
+ "application/x-git-upload-pack-advertisement",
+ )
+ w.Header().Set("Cache-Control", "no-cache")
+ w.Header().Set("Pragma", "no-cache")
+ w.Header().Set("Expires", "Fri, 01 Jan 1980 00:00:00 GMT")
+
+ if err := cmd.Run(); err != nil {
+ g.logger.Error(
+ "git upload-pack advertisement failed",
+ "repo", mirror,
+ "error", err,
+ "stderr", strings.TrimSpace(stderr.String()),
+ )
+ }
+}
+
+func (g *gitHTTP) uploadPack(
+ w http.ResponseWriter,
+ r *http.Request,
+ mirror string,
+) {
+ cmd := g.command(
+ r,
+ "upload-pack",
+ "--strict",
+ "--stateless-rpc",
+ mirror,
+ )
+
+ var stderr bytes.Buffer
+
+ cmd.Stdin = r.Body
+ cmd.Stdout = w
+ cmd.Stderr = &stderr
+
+ w.Header().Set(
+ "Content-Type",
+ "application/x-git-upload-pack-result",
+ )
+ w.Header().Set("Cache-Control", "no-cache")
+ w.Header().Set("Pragma", "no-cache")
+ w.Header().Set("Expires", "Fri, 01 Jan 1980 00:00:00 GMT")
+
+ if err := cmd.Run(); err != nil {
+ g.logger.Error(
+ "git upload-pack failed",
+ "repo", mirror,
+ "error", err,
+ "stderr", strings.TrimSpace(stderr.String()),
+ )
+ }
+}
\ No newline at end of file
diff --git a/internal/server/server.go b/internal/server/server.go
index 4aa8471..8ff2371 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -8,6 +8,8 @@ import (
"path"
"strings"
"time"
+
+ "github.com/tgckpg/flatgit/internal/config"
)
type Options struct {
@@ -15,6 +17,9 @@ type Options struct {
Root string
Logger *slog.Logger
WebhookMux func(*http.ServeMux)
+
+ GitCommand string
+ Repos []config.Repo
}
func withCacheHeaders(next http.Handler) http.Handler {
@@ -53,7 +58,21 @@ func ListenAndServe(ctx context.Context, opts Options) error {
opts.WebhookMux(mux)
}
fs := http.FileServer(http.Dir(opts.Root))
- mux.Handle("/", withCacheHeaders(fs))
+ static := withCacheHeaders(fs)
+
+ git := newGitHTTP(
+ opts.GitCommand,
+ opts.Repos,
+ opts.Logger,
+ )
+
+ mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if git.ServeHTTP(w, r) {
+ return
+ }
+
+ static.ServeHTTP(w, r)
+ }))
srv := &http.Server{
Addr: opts.Addr,