all repos

mugit @ e1d1a9e

🐮 git server that your cow will love
4 files changed, 70 insertions(+), 0 deletions(-)
git: archive
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed at: 2026-02-13 01:58:03 +0200
Change ID: lzunywnrunzvkwotpklqpwzsstwqnvnm
Parent: d0b4683
A internal/git/gitx/archive.go

@@ -0,0 +1,37 @@

+package gitx + +import ( + "context" + "fmt" + "io" + "os/exec" + "regexp" + "strings" +) + +// ArchiveTar generates a tarball of a git ref. +func ArchiveTar(ctx context.Context, repoDir, ref string, out io.Writer) error { + if !isValidRef(ref) { + return fmt.Errorf("invalid ref: %s", ref) + } + + cmd := exec.CommandContext(ctx, "git", "archive", "--format=tar.gz", ref) + cmd.Dir = repoDir + cmd.Env = gitEnv + cmd.Stdout = out + cmd.Stderr = io.Discard + + if err := cmd.Run(); err != nil { + return fmt.Errorf("git archive %s: %w", ref, err) + } + + return nil +} + +func isValidRef(ref string) bool { + if ref == "" || strings.Contains(ref, "..") { + return false + } + matched, _ := regexp.MatchString(`^[a-zA-Z0-9._/-]+$`, ref) + return matched +}
M internal/handlers/git.go

@@ -2,6 +2,7 @@ package handlers

import ( "compress/gzip" + "fmt" "io" "log/slog" "net/http"

@@ -90,6 +91,34 @@

if err := gitx.UploadPack(r.Context(), path, true, reader, newFlushWriter(w)); err != nil { // Don't call w.WriteHeader here - connection already started! slog.Error("git: upload-pack", "err", err) + return + } +} + +func (h *handlers) archiveHandler(w http.ResponseWriter, r *http.Request) { + name := getNormalizedName(r.PathValue("name")) + ref := r.PathValue("ref") + + path, err := securejoin.SecureJoin(h.c.Repo.Dir, repoNameToPath(name)) + if err != nil { + http.Error(w, "invalid path", http.StatusBadRequest) + slog.Error("git: upload-pack path", "err", err) + return + } + + _, err = h.openPublicRepo(name, ref) + if err != nil { + h.write404(w, err) + return + } + + filename := fmt.Sprintf("%s-%s.tar.gz", name, ref) + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filename)) + w.Header().Set("Content-Type", "application/gzip") + w.WriteHeader(http.StatusOK) + + if err := gitx.ArchiveTar(r.Context(), path, ref, w); err != nil { + slog.Error("git: archive", "ref", ref, "err", err) return } }
M internal/handlers/handlers.go

@@ -35,6 +35,8 @@ mux.HandleFunc("GET /{name}/blob/{ref}/{rest...}", h.fileContentsHandler)

mux.HandleFunc("GET /{name}/log/{ref}", h.logHandler) mux.HandleFunc("GET /{name}/commit/{ref}", h.commitHandler) mux.HandleFunc("GET /{name}/refs/{$}", h.refsHandler) + mux.HandleFunc("GET /{name}/archive/{ref}", h.archiveHandler) + handler := h.recoverMiddleware(mux) return h.loggingMiddleware(handler)
M web/templates/repo_refs.html

@@ -15,6 +15,7 @@ <div>

<strong>{{ .Name }}</strong> <a href="/{{ $name }}/tree/{{ .Name }}/">browse</a> <a href="/{{ $name }}/log/{{ .Name }}">log</a> + <a href="/{{ $name }}/archive/{{ .Name }}">tar.gz</a> </div> {{ end }} </div>

@@ -26,6 +27,7 @@ <div>

<strong>{{ .Name }}</strong> <a href="/{{ $name }}/tree/{{ .Name }}/">browse</a> <a href="/{{ $name }}/log/{{ .Name }}">log</a> + <a href="/{{ $name }}/archive/{{ .Name }}">tar.gz</a> {{ if .Message }} <pre>{{ .Message }}</pre> </div>