4 files changed,
70 insertions(+),
0 deletions(-)
Author:
Oleksandr Smirnov
olexsmir@gmail.com
Committed at:
2026-02-13 01:58:03 +0200
Authored at:
2026-02-13 01:24:10 +0200
Change ID:
lzunywnrunzvkwotpklqpwzsstwqnvnm
Parent:
d0b4683
A
internal/git/gitx/archive.go
路路路 1 +package gitx 2 + 3 +import ( 4 + "context" 5 + "fmt" 6 + "io" 7 + "os/exec" 8 + "regexp" 9 + "strings" 10 +) 11 + 12 +// ArchiveTar generates a tarball of a git ref. 13 +func ArchiveTar(ctx context.Context, repoDir, ref string, out io.Writer) error { 14 + if !isValidRef(ref) { 15 + return fmt.Errorf("invalid ref: %s", ref) 16 + } 17 + 18 + cmd := exec.CommandContext(ctx, "git", "archive", "--format=tar.gz", ref) 19 + cmd.Dir = repoDir 20 + cmd.Env = gitEnv 21 + cmd.Stdout = out 22 + cmd.Stderr = io.Discard 23 + 24 + if err := cmd.Run(); err != nil { 25 + return fmt.Errorf("git archive %s: %w", ref, err) 26 + } 27 + 28 + return nil 29 +} 30 + 31 +func isValidRef(ref string) bool { 32 + if ref == "" || strings.Contains(ref, "..") { 33 + return false 34 + } 35 + matched, _ := regexp.MatchString(`^[a-zA-Z0-9._/-]+$`, ref) 36 + return matched 37 +}
M
internal/handlers/git.go
路路路 2 2 3 3 import ( 4 4 "compress/gzip" 5 + "fmt" 5 6 "io" 6 7 "log/slog" 7 8 "net/http" 路路路 90 91 if err := gitx.UploadPack(r.Context(), path, true, reader, newFlushWriter(w)); err != nil { 91 92 // Don't call w.WriteHeader here - connection already started! 92 93 slog.Error("git: upload-pack", "err", err) 94 + return 95 + } 96 +} 97 + 98 +func (h *handlers) archiveHandler(w http.ResponseWriter, r *http.Request) { 99 + name := getNormalizedName(r.PathValue("name")) 100 + ref := r.PathValue("ref") 101 + 102 + path, err := securejoin.SecureJoin(h.c.Repo.Dir, repoNameToPath(name)) 103 + if err != nil { 104 + http.Error(w, "invalid path", http.StatusBadRequest) 105 + slog.Error("git: upload-pack path", "err", err) 106 + return 107 + } 108 + 109 + _, err = h.openPublicRepo(name, ref) 110 + if err != nil { 111 + h.write404(w, err) 112 + return 113 + } 114 + 115 + filename := fmt.Sprintf("%s-%s.tar.gz", name, ref) 116 + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filename)) 117 + w.Header().Set("Content-Type", "application/gzip") 118 + w.WriteHeader(http.StatusOK) 119 + 120 + if err := gitx.ArchiveTar(r.Context(), path, ref, w); err != nil { 121 + slog.Error("git: archive", "ref", ref, "err", err) 93 122 return 94 123 } 95 124 }
M
internal/handlers/handlers.go
路路路 35 35 mux.HandleFunc("GET /{name}/log/{ref}", h.logHandler) 36 36 mux.HandleFunc("GET /{name}/commit/{ref}", h.commitHandler) 37 37 mux.HandleFunc("GET /{name}/refs/{$}", h.refsHandler) 38 + mux.HandleFunc("GET /{name}/archive/{ref}", h.archiveHandler) 39 + 38 40 39 41 handler := h.recoverMiddleware(mux) 40 42 return h.loggingMiddleware(handler)
M
web/templates/repo_refs.html
路路路 15 15 <strong>{{ .Name }}</strong> 16 16 <a href="/{{ $name }}/tree/{{ .Name }}/">browse</a> 17 17 <a href="/{{ $name }}/log/{{ .Name }}">log</a> 18 + <a href="/{{ $name }}/archive/{{ .Name }}">tar.gz</a> 18 19 </div> 19 20 {{ end }} 20 21 </div> 路路路 26 27 <strong>{{ .Name }}</strong> 27 28 <a href="/{{ $name }}/tree/{{ .Name }}/">browse</a> 28 29 <a href="/{{ $name }}/log/{{ .Name }}">log</a> 30 + <a href="/{{ $name }}/archive/{{ .Name }}">tar.gz</a> 29 31 {{ if .Message }} 30 32 <pre>{{ .Message }}</pre> 31 33 </div>