all repos

mugit @ a0a9d4ac6fd9080169581577fc9ba0d2da6a5807

🐮 git server that your cow will love
7 files changed, 108 insertions(+), 126 deletions(-)
some refactoring
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed at: 2026-01-23 00:22:58 +0200
Change ID: ttwlklypxyvnwqwxssuqqnzstuyspspk
Parent: 10fc8ef
M internal/git/gitservice/gitservice.go

@@ -64,12 +64,12 @@ Stdout: out,

}) } -func ReceivePack(dir string, in io.Reader, out, stderr io.Writer) error { +func ReceivePack(dir string, in io.Reader, out, errout io.Writer) error { return gitCmd("receive-pack", config{ Dir: dir, Stdin: in, Stdout: out, - Stderr: stderr, + Stderr: errout, }) }
M internal/git/repo.go

@@ -151,7 +151,6 @@ }

func (g *Repo) Description() (string, error) { // TODO: ??? Support both mugit.description and /description file - path := filepath.Join(g.path, "description") if _, err := os.Stat(path); err != nil { return "", fmt.Errorf("no description file found")
M internal/handlers/git.go

@@ -2,13 +2,11 @@ package handlers

import ( "compress/gzip" - "fmt" "io" "log/slog" "net/http" "path/filepath" - "olexsmir.xyz/mugit/internal/git" "olexsmir.xyz/mugit/internal/git/gitservice" )

@@ -17,7 +15,7 @@ // passes it to git smart http, otherwise renders templates

func (h *handlers) multiplex(w http.ResponseWriter, r *http.Request) { if r.URL.RawQuery == "service=git-receive-pack" { w.WriteHeader(http.StatusBadRequest) - w.Write([]byte("http pushing isn't supported")) + w.Write([]byte("http pushing is not supported")) return }

@@ -42,10 +40,8 @@

w.Header().Set("content-type", "application/x-git-upload-pack-advertisement") w.WriteHeader(http.StatusOK) - if err := gitservice.InfoRefs( - filepath.Join(h.c.Repo.Dir, name), // FIXME: use securejoin - w, - ); err != nil { + path := filepath.Join(h.c.Repo.Dir, filepath.Clean(name)) + if err := gitservice.InfoRefs(path, w); err != nil { slog.Error("git: info/refs", "err", err) return }

@@ -68,6 +64,7 @@ reader := io.Reader(r.Body)

if r.Header.Get("Content-Encoding") == "gzip" { gr, err := gzip.NewReader(r.Body) if err != nil { + w.WriteHeader(http.StatusInternalServerError) slog.Error("git: gzip reader", "err", err) return }

@@ -75,34 +72,11 @@ defer gr.Close()

reader = gr } - if err := gitservice.UploadPack( - filepath.Join(h.c.Repo.Dir, name), - true, - reader, - newFlushWriter(w), - ); err != nil { + path := filepath.Join(h.c.Repo.Dir, filepath.Clean(name)) + if err := gitservice.UploadPack(path, true, reader, newFlushWriter(w)); err != nil { slog.Error("git: upload-pack", "err", err) return } -} - -func (h *handlers) openPublicRepo(name, ref string) (*git.Repo, error) { - n := filepath.Clean(name) - repo, err := git.Open(filepath.Join(h.c.Repo.Dir, n), ref) - if err != nil { - return nil, err - } - - isPrivate, err := repo.IsPrivate() - if err != nil { - return nil, err - } - - if isPrivate { - return nil, fmt.Errorf("repo is private") - } - - return repo, nil } type flushWriter struct {
M internal/handlers/handlers.go

@@ -25,20 +25,19 @@ ParseFS(web.TemplatesFS, "*"))

h := handlers{cfg, tmpls} mux := http.NewServeMux() - mux.HandleFunc("GET /", h.index) + mux.HandleFunc("GET /", h.indexHandler) mux.HandleFunc("GET /static/{file}", h.serveStatic) mux.HandleFunc("GET /{name}", h.multiplex) mux.HandleFunc("POST /{name}", h.multiplex) mux.HandleFunc("GET /{name}/{rest...}", h.multiplex) mux.HandleFunc("POST /{name}/{rest...}", h.multiplex) - mux.HandleFunc("GET /{name}/tree/{ref}/{rest...}", h.repoTree) - mux.HandleFunc("GET /{name}/blob/{ref}/{rest...}", h.fileContents) - mux.HandleFunc("GET /{name}/log/{ref}", h.log) - mux.HandleFunc("GET /{name}/commit/{ref}", h.commit) - mux.HandleFunc("GET /{name}/refs/{$}", h.refs) + mux.HandleFunc("GET /{name}/tree/{ref}/{rest...}", h.repoTreeHandler) + 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) return mux } - func (h *handlers) serveStatic(w http.ResponseWriter, r *http.Request) { f := filepath.Clean(r.PathValue("file"))
M internal/handlers/repo.go

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

import ( "bytes" + "errors" "fmt" "html/template" "io"

@@ -17,61 +18,19 @@

"github.com/yuin/goldmark" "github.com/yuin/goldmark/extension" "github.com/yuin/goldmark/renderer/html" - "olexsmir.xyz/mugit/internal/humanize" + "olexsmir.xyz/mugit/internal/git" ) -func (h *handlers) index(w http.ResponseWriter, r *http.Request) { - dirs, err := os.ReadDir(h.c.Repo.Dir) +func (h *handlers) indexHandler(w http.ResponseWriter, r *http.Request) { + repos, err := h.listPublicRepos() if err != nil { h.write500(w, err) return } - type repoInfo struct { - Name, Desc, Idle string - t time.Time - } - - repoInfos := []repoInfo{} - for _, dir := range dirs { - if !dir.IsDir() { - continue - } - - name := dir.Name() - repo, err := h.openPublicRepo(name, "") - if err != nil { - slog.Error("", "name", name, "err", err) - continue - } - - desc, err := repo.Description() - if err != nil { - slog.Error("", "err", err) - continue - } - - lastComit, err := repo.LastCommit() - if err != nil { - slog.Error("", "err", err) - continue - } - - repoInfos = append(repoInfos, repoInfo{ - Name: name, - Desc: desc, - Idle: humanize.Time(lastComit.Author.When), - t: lastComit.Author.When, - }) - } - - sort.Slice(repoInfos, func(i, j int) bool { - return repoInfos[j].t.Before(repoInfos[i].t) - }) - data := make(map[string]any) data["meta"] = h.c.Meta - data["repos"] = repoInfos + data["repos"] = repos h.templ(w, "index", data) }

@@ -145,7 +104,7 @@

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

@@ -156,12 +115,6 @@ h.write404(w, err)

return } - isPrivate, err := repo.IsPrivate() - if isPrivate || err != nil { - h.write404(w, err) - return - } - desc, err := repo.Description() if err != nil { h.write500(w, err)

@@ -186,7 +139,7 @@

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

@@ -214,18 +167,18 @@ h.write500(w, err)

return } - data := make(map[string]any) - data["name"] = name - data["ref"] = ref - data["desc"] = desc - data["path"] = treePath - if raw { w.WriteHeader(http.StatusOK) w.Header().Set("Content-Type", "text/plain") w.Write([]byte(contents)) return } + + data := make(map[string]any) + data["name"] = name + data["ref"] = ref + data["desc"] = desc + data["path"] = treePath lc, err := countLines(strings.NewReader(contents)) if err != nil {

@@ -243,10 +196,10 @@ data["linecount"] = lines

data["content"] = contents data["meta"] = h.c.Meta - h.templ(w, "file", data) + h.templ(w, "repo_file", data) } -func (h *handlers) log(w http.ResponseWriter, r *http.Request) { +func (h *handlers) logHandler(w http.ResponseWriter, r *http.Request) { name := r.PathValue("name") ref := r.PathValue("ref")

@@ -256,12 +209,6 @@ h.write404(w, err)

return } - isPrivate, err := repo.IsPrivate() - if isPrivate || err != nil { - h.write404(w, err) - return - } - commits, err := repo.Commits() if err != nil { h.write500(w, err)

@@ -284,7 +231,7 @@ data["commits"] = commits

h.templ(w, "repo_log", data) } -func (h *handlers) commit(w http.ResponseWriter, r *http.Request) { +func (h *handlers) commitHandler(w http.ResponseWriter, r *http.Request) { name := r.PathValue("name") ref := r.PathValue("ref") repo, err := h.openPublicRepo(name, ref)

@@ -293,12 +240,6 @@ h.write404(w, err)

return } - isPrivate, err := repo.IsPrivate() - if isPrivate || err != nil { - h.write404(w, err) - return - } - diff, err := repo.Diff() if err != nil { h.write500(w, err)

@@ -321,16 +262,10 @@ data["desc"] = desc

h.templ(w, "commit", data) } -func (h *handlers) refs(w http.ResponseWriter, r *http.Request) { +func (h *handlers) refsHandler(w http.ResponseWriter, r *http.Request) { name := r.PathValue("name") repo, err := h.openPublicRepo(name, "") if err != nil { - h.write404(w, err) - return - } - - isPrivate, err := repo.IsPrivate() - if isPrivate || err != nil { h.write404(w, err) return }

@@ -387,3 +322,78 @@ return 0, err

} } } + +var errPrivateRepo = errors.New("privat err") + +func (h *handlers) openPublicRepo(name, ref string) (*git.Repo, error) { + n := filepath.Clean(name) + repo, err := git.Open(filepath.Join(h.c.Repo.Dir, n), ref) + if err != nil { + return nil, err + } + + isPrivate, err := repo.IsPrivate() + if err != nil { + return nil, err + } + if isPrivate { + return nil, errPrivateRepo + } + + return repo, nil +} + +type repoList struct { + Name string + Desc string + LastCommit time.Time +} + +func (h *handlers) listPublicRepos() ([]repoList, error) { + dirs, err := os.ReadDir(h.c.Repo.Dir) + if err != nil { + return nil, err + } + + var repos []repoList + var errs []error + for _, dir := range dirs { + if !dir.IsDir() { + continue + } + + name := dir.Name() + repo, err := h.openPublicRepo(name, "") + if err != nil { + if errors.Is(err, errPrivateRepo) { + continue + } + errs = append(errs, err) + continue + } + + desc, err := repo.Description() + if err != nil { + errs = append(errs, err) + continue + } + + lastComit, err := repo.LastCommit() + if err != nil { + errs = append(errs, err) + continue + } + + repos = append(repos, repoList{ + Name: name, + Desc: desc, + LastCommit: lastComit.Author.When, + }) + } + + sort.Slice(repos, func(i, j int) bool { + return repos[j].LastCommit.Before(repos[i].LastCommit) + }) + + return repos, errors.Join(errs...) +}
M web/templates/repo_file.htmlweb/templates/repo_file.html

@@ -1,4 +1,4 @@

-{{ define "file" }} +{{ define "repo_file" }} <html> <head> {{ template "head" . }}
M web/templates/index.html

@@ -24,7 +24,7 @@ {{ range .repos }}

<tr> <td class="url"><a href="/{{ .Name }}">{{ .Name }}</a></td> <td class="desc">{{ .Desc }}</td> - <td class="idle">{{ .Idle }}</td> + <td class="idle">{{ humanTime .LastCommit }}</td> </tr> {{ end}} </tbody>