all repos

mugit @ ac1a0a0

🐮 git server that your cow will love
5 files changed, 39 insertions(+), 18 deletions(-)
support both repo and repo.git as names
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed at: 2026-01-28 19:53:53 +0200
Change ID: wwryywyqkpzwuxmsqnnvotwoqmqzorky
Parent: cd02956
M internal/git/repo.go

@@ -51,7 +51,8 @@ return &g, nil

} func (g *Repo) Name() string { - return filepath.Base(g.path) + name := filepath.Base(g.path) + return strings.TrimSuffix(name, ".git") } func (g *Repo) Commits() ([]*object.Commit, error) {
M internal/handlers/git.go

@@ -30,7 +30,7 @@ }

} func (h *handlers) infoRefs(w http.ResponseWriter, r *http.Request) { - name := r.PathValue("name") + name := getNormalizedName(r.PathValue("name")) _, err := h.openPublicRepo(name, "") if err != nil { h.write404(w, err)

@@ -40,7 +40,8 @@

w.Header().Set("content-type", "application/x-git-upload-pack-advertisement") w.WriteHeader(http.StatusOK) - path, err := securejoin.SecureJoin(h.c.Repo.Dir, name) + repoPath := repoNameToPath(name) + path, err := securejoin.SecureJoin(h.c.Repo.Dir, repoPath) if err != nil { w.WriteHeader(http.StatusBadRequest) slog.Error("git: info/refs", "err", err)

@@ -55,7 +56,7 @@ }

} func (h *handlers) uploadPack(w http.ResponseWriter, r *http.Request) { - name := r.PathValue("name") + name := getNormalizedName(r.PathValue("name")) _, err := h.openPublicRepo(name, "") if err != nil { h.write404(w, err)

@@ -79,7 +80,8 @@ defer gr.Close()

reader = gr } - path, err := securejoin.SecureJoin(h.c.Repo.Dir, name) + repoPath := repoNameToPath(name) + path, err := securejoin.SecureJoin(h.c.Repo.Dir, repoPath) if err != nil { w.WriteHeader(http.StatusBadRequest) slog.Error("git: info/refs", "err", err)
M internal/handlers/handlers.go

@@ -46,6 +46,11 @@ // TODO: check if files exists

http.ServeFileFS(w, r, web.StaticFS, f) } +func repoNameToPath(name string) string { return name + ".git" } +func getNormalizedName(name string) string { + return strings.TrimSuffix(name, ".git") +} + var templateFuncs = template.FuncMap{ "humanizeTime": func(t time.Time) string { return humanize.Time(t) }, "commitSummary": func(s string) string {
M internal/handlers/repo.go

@@ -43,7 +43,7 @@ extension.Linkify,

)) func (h *handlers) repoIndex(w http.ResponseWriter, r *http.Request) { - name := r.PathValue("name") + name := getNormalizedName(r.PathValue("name")) repo, err := h.openPublicRepo(name, "") if err != nil { h.write404(w, err)

@@ -106,7 +106,7 @@ h.templ(w, "repo_index", data)

} func (h *handlers) repoTreeHandler(w http.ResponseWriter, r *http.Request) { - name := r.PathValue("name") + name := getNormalizedName(r.PathValue("name")) ref := r.PathValue("ref") treePath := r.PathValue("rest")

@@ -141,7 +141,7 @@ h.templ(w, "repo_tree", data)

} func (h *handlers) fileContentsHandler(w http.ResponseWriter, r *http.Request) { - name := r.PathValue("name") + name := getNormalizedName(r.PathValue("name")) ref := r.PathValue("ref") treePath := r.PathValue("rest")

@@ -201,7 +201,7 @@ h.templ(w, "repo_file", data)

} func (h *handlers) logHandler(w http.ResponseWriter, r *http.Request) { - name := r.PathValue("name") + name := getNormalizedName(r.PathValue("name")) ref := r.PathValue("ref") repo, err := h.openPublicRepo(name, ref)

@@ -233,7 +233,7 @@ h.templ(w, "repo_log", data)

} func (h *handlers) commitHandler(w http.ResponseWriter, r *http.Request) { - name := r.PathValue("name") + name := getNormalizedName(r.PathValue("name")) ref := r.PathValue("ref") repo, err := h.openPublicRepo(name, ref) if err != nil {

@@ -264,7 +264,7 @@ h.templ(w, "commit", data)

} func (h *handlers) refsHandler(w http.ResponseWriter, r *http.Request) { - name := r.PathValue("name") + name := getNormalizedName(r.PathValue("name")) repo, err := h.openPublicRepo(name, "") if err != nil { h.write404(w, err)

@@ -327,6 +327,9 @@

var errPrivateRepo = errors.New("private err") func (h *handlers) openPublicRepo(name, ref string) (*git.Repo, error) { + // Convert normalized name back to filesystem path with .git suffix + name = repoNameToPath(name) + path, err := securejoin.SecureJoin(h.c.Repo.Dir, name) if err != nil { return nil, err

@@ -368,7 +371,8 @@ continue

} name := dir.Name() - repo, err := h.openPublicRepo(name, "") + normalizedName := getNormalizedName(name) + repo, err := h.openPublicRepo(normalizedName, "") if err != nil { if errors.Is(err, errPrivateRepo) { continue

@@ -390,7 +394,7 @@ continue

} repos = append(repos, repoList{ - Name: name, + Name: normalizedName, Desc: desc, LastCommit: lastComit.Author.When, })
M internal/ssh/server.go

@@ -5,6 +5,7 @@ "fmt"

"log/slog" "slices" "strconv" + "strings" securejoin "github.com/cyphar/filepath-securejoin" "github.com/gliderlabs/ssh"

@@ -76,15 +77,18 @@ return

} gitCmd := cmd[0] - repoPath := cmd[1] - repoPath, err := securejoin.SecureJoin(s.c.Repo.Dir, repoPath) + rawRepoPath := cmd[1] + normalizedRepoName := normalizeRepoName(rawRepoPath) + repoPath := repoNameToPath(normalizedRepoName) + + fullPath, err := securejoin.SecureJoin(s.c.Repo.Dir, repoPath) if err != nil { slog.Error("ssh: invalid path", "err", err) s.repoNotFound(sess) return } - repo, err := git.Open(repoPath, "") + repo, err := git.Open(fullPath, "") if err != nil { slog.Error("ssh: failed to open repo", "err", err) s.repoNotFound(sess)

@@ -104,7 +108,7 @@ s.repoNotFound(sess)

return } - if err := gitservice.UploadPack(repoPath, false, sess, sess); err != nil { + if err := gitservice.UploadPack(fullPath, false, sess, sess); err != nil { s.error(sess, err) return }

@@ -115,7 +119,7 @@ s.unauthorized(sess)

return } - if err := gitservice.ReceivePack(repoPath, sess, sess, sess.Stderr()); err != nil { + if err := gitservice.ReceivePack(fullPath, sess, sess, sess.Stderr()); err != nil { s.error(sess, err) return }

@@ -156,3 +160,8 @@ slog.Error("error on ssh side", "err", err)

gitservice.PackError(sess, "Unexpected server error.") sess.Exit(1) } + +func repoNameToPath(name string) string { return name + ".git" } +func normalizeRepoName(name string) string { + return strings.TrimSuffix(name, ".git") +}