all repos — legit @ a3a5a64520f61b69f9e7f37610b9e7f30fd81763

Unnamed repository; edit this file 'description' to name the repository.

chore(release): v0.2.6

### Added
- Reference links displayed in commit log page (#54)
- Symlink resolution when scanning for git repositories (#59)

### Fixed
- Malformed HTML in templates (#61)
- Correct reader variable used in git operations (#58)

### Changed
- Default scan path updated to /var/git (#60)
- Dependency bumps: go-git v5.13.2, golang.org/x/crypto v0.31.0
- Path joining hardened with securejoin across all routes
- Code quality: all golangci-lint issues resolved
Maxwell Jensen 85795372+maxwelljens@users.noreply.github.com
Tue, 12 May 2026 12:19:22 +0200
commit

a3a5a64520f61b69f9e7f37610b9e7f30fd81763

parent

d6fa0d66c9f6a6c41030f28872132f4290671c48

A .golangci.yml

@@ -0,0 +1,16 @@

+version: "2" + +linters: + enable: + - errcheck # checking for unchecked errors, these unchecked errors can be critical bugs in some cases + - govet # reports suspicious constructs, such as Printf calls whose arguments do not align with the format string + - usetesting # reports uses of functions with replacement inside the testing package + - perfsprint # checks that fmt.Sprintf can be replaced with a faster alternative + - unused # checks for unused constants, variables, functions and types + - gocritic # provides diagnostics that check for bugs, performance and style issues + - staticcheck # is a go vet on steroids, applying a ton of static analysis checks + - godoclint # checks Golang's documentation practice + - errorlint # finds code that will cause problems with the error wrapping scheme introduced in Go 1.13 + - recvcheck # checks for receiver type consistency + - unparam # reports unused function parameters + - usestdlibvars # detects the possibility to use variables/constants from the Go standard library
M CHANGELOG.mdCHANGELOG.md

@@ -6,7 +6,27 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),

and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.2.5] — 2025-01-26 +## [0.2.6] - 2026-05-12 + +### Added + +- Reference links displayed in commit log page (#54) +- Symlink resolution when scanning for git repositories (#59) + +### Fixed + +- Malformed HTML in templates (#61) +- Correct reader variable used in git operations (#58) + +### Changed + +- Default scan path updated to `/var/git` (#60) +- Dependency bumps: go-git v5.13.2, golang.org/x/crypto v0.31.0 +- Path joining hardened with securejoin across all routes +- Code quality: all golangci-lint issues resolved (errcheck, errorlint, +gocritic, godoclint, perfsprint, staticcheck, unused, usestdlibvars) + +## [0.2.5] - 2025-01-26 ### Changed

@@ -14,7 +34,7 @@ - CSS: font stack updated to system fonts, custom font features removed

- CSS: colours, dark mode refinements - Routes: README content sanitised for non-Markdown files (uses <pre>) -## [0.2.4] — 2024-10-06 +## [0.2.4] - 2024-10-06 ### Added

@@ -30,7 +50,7 @@

- Dependencies bumped - README reworded with Docker image references -## [0.2.3] — 2024-07-13 +## [0.2.3] - 2024-07-13 ### Added

@@ -50,7 +70,3 @@ ### Fixed

- Raw file view accidentally removed code re-added - `getDisplayName` now works correctly for repos with `.git` suffix - -[0.2.5]: https://git.icyphox.sh/legit/compare/v0.2.4...v0.2.5 -[0.2.4]: https://git.icyphox.sh/legit/compare/v0.2.3...v0.2.4 -[0.2.3]: https://git.icyphox.sh/legit/compare/v0.2.2...v0.2.3
M git/diff.gogit/diff.go

@@ -25,7 +25,7 @@ IsNew bool

IsDelete bool } -// A nicer git diff representation. +// NiceDiff is a nicer git diff representation. type NiceDiff struct { Commit struct { Message string
M git/git.gogit/git.go

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

import ( "archive/tar" + "errors" "fmt" "io" "io/fs"

@@ -48,23 +49,23 @@ modTime time.Time

isDir bool } -func (self *TagList) Len() int { - return len(self.refs) +func (t *TagList) Len() int { + return len(t.refs) } -func (self *TagList) Swap(i, j int) { - self.refs[i], self.refs[j] = self.refs[j], self.refs[i] +func (t *TagList) Swap(i, j int) { + t.refs[i], t.refs[j] = t.refs[j], t.refs[i] } -// sorting tags in reverse chronological order -func (self *TagList) Less(i, j int) bool { +// Less sorts tags in reverse chronological order. +func (t *TagList) Less(i, j int) bool { var dateI time.Time var dateJ time.Time - if self.refs[i].tag != nil { - dateI = self.refs[i].tag.Tagger.When + if t.refs[i].tag != nil { + dateI = t.refs[i].tag.Tagger.When } else { - c, err := self.r.CommitObject(self.refs[i].ref.Hash()) + c, err := t.r.CommitObject(t.refs[i].ref.Hash()) if err != nil { dateI = time.Now() } else {

@@ -72,10 +73,10 @@ dateI = c.Committer.When

} } - if self.refs[j].tag != nil { - dateJ = self.refs[j].tag.Tagger.When + if t.refs[j].tag != nil { + dateJ = t.refs[j].tag.Tagger.When } else { - c, err := self.r.CommitObject(self.refs[j].ref.Hash()) + c, err := t.r.CommitObject(t.refs[j].ref.Hash()) if err != nil { dateJ = time.Now() } else {

@@ -117,10 +118,12 @@ return nil, fmt.Errorf("commits from ref: %w", err)

} commitRefs := []*CommitReference{} - ci.ForEach(func(c *object.Commit) error { + if err := ci.ForEach(func(c *object.Commit) error { commitRefs = append(commitRefs, &CommitReference{commit: c}) return nil - }) + }); err != nil { + return nil, err + } // new we fetch for possible tags for each commit iter, err := g.r.References()

@@ -131,12 +134,12 @@

if err := iter.ForEach(func(ref *plumbing.Reference) error { for _, c := range commitRefs { obj, err := g.r.TagObject(ref.Hash()) - switch err { - case nil: + switch { + case err == nil: if obj.Target == c.commit.Hash { c.AddReference(ref) } - case plumbing.ErrObjectNotFound: + case errors.Is(err, plumbing.ErrObjectNotFound): if c.commit.Hash == ref.Hash() { c.AddReference(ref) }

@@ -166,12 +169,12 @@

commitRef := &CommitReference{commit: c} if err := iter.ForEach(func(ref *plumbing.Reference) error { obj, err := g.r.TagObject(ref.Hash()) - switch err { - case nil: + switch { + case err == nil: if obj.Target == commitRef.commit.Hash { commitRef.AddReference(ref) } - case plumbing.ErrObjectNotFound: + case errors.Is(err, plumbing.ErrObjectNotFound): if commitRef.commit.Hash == ref.Hash() { commitRef.AddReference(ref) }

@@ -222,13 +225,13 @@ tags := make([]*TagReference, 0)

if err := iter.ForEach(func(ref *plumbing.Reference) error { obj, err := g.r.TagObject(ref.Hash()) - switch err { - case nil: + switch { + case err == nil: tags = append(tags, &TagReference{ ref: ref, tag: obj, }) - case plumbing.ErrObjectNotFound: + case errors.Is(err, plumbing.ErrObjectNotFound): tags = append(tags, &TagReference{ ref: ref, })

@@ -268,14 +271,14 @@ if err == nil {

return b, nil } } - return "", fmt.Errorf("unable to find main branch") + return "", errors.New("unable to find main branch") } // WriteTar writes itself from a tree into a binary tar file format. // prefix is root folder to be appended. func (g *GitRepo) WriteTar(w io.Writer, prefix string) error { tw := tar.NewWriter(w) - defer tw.Close() + defer func() { _ = tw.Close() }() c, err := g.r.CommitObject(g.h) if err != nil {

@@ -313,17 +316,17 @@ if err != nil {

return err } - reader, err := file.Blob.Reader() + reader, err := file.Reader() if err != nil { return err } _, err = io.Copy(tw, reader) if err != nil { - reader.Close() + _ = reader.Close() return err } - reader.Close() + _ = reader.Close() } }
M git/service/service.gogit/service/service.go

@@ -79,13 +79,13 @@ cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}

stdoutPipe, _ := cmd.StdoutPipe() cmd.Stderr = cmd.Stdout - defer stdoutPipe.Close() + defer func() { _ = stdoutPipe.Close() }() stdinPipe, err := cmd.StdinPipe() if err != nil { return err } - defer stdinPipe.Close() + defer func() { _ = stdinPipe.Close() }() if err := cmd.Start(); err != nil { log.Printf("git: failed to start git-upload-pack: %s", err)

@@ -96,7 +96,7 @@ if _, err := io.Copy(stdinPipe, c.Stdin); err != nil {

log.Printf("git: failed to copy stdin: %s", err) return err } - stdinPipe.Close() + _ = stdinPipe.Close() if _, err := io.Copy(newWriteFlusher(c.Stdout), stdoutPipe); err != nil { log.Printf("git: failed to copy stdout: %s", err)
M git/tree.gogit/tree.go

@@ -39,7 +39,7 @@

return files, nil } -// A nicer git tree representation. +// NiceTree is a nicer git tree representation. type NiceTree struct { Name string Mode string
M routes/git.goroutes/git.go

@@ -31,7 +31,7 @@ Stdout: w,

} if err := cmd.InfoRefs(); err != nil { - http.Error(w, err.Error(), 500) + http.Error(w, err.Error(), http.StatusInternalServerError) log.Printf("git: failed to execute git-upload-pack (info/refs) %s", err) return }

@@ -64,16 +64,16 @@

if r.Header.Get("Content-Encoding") == "gzip" { reader, err = gzip.NewReader(r.Body) if err != nil { - http.Error(w, err.Error(), 500) + http.Error(w, err.Error(), http.StatusInternalServerError) log.Printf("git: failed to create gzip reader: %s", err) return } - defer reader.Close() + defer func() { _ = reader.Close() }() } cmd.Stdin = reader if err := cmd.UploadPack(); err != nil { - http.Error(w, err.Error(), 500) + http.Error(w, err.Error(), http.StatusInternalServerError) log.Printf("git: failed to execute git-upload-pack %s", err) return }
M routes/handler.goroutes/handler.go

@@ -13,17 +13,18 @@ path := r.PathValue("rest")

if r.URL.RawQuery == "service=git-receive-pack" { w.WriteHeader(http.StatusBadRequest) - w.Write([]byte("no pushing allowed!")) + _, _ = w.Write([]byte("no pushing allowed!")) return } - if path == "info/refs" && + switch { + case path == "info/refs" && r.URL.RawQuery == "service=git-upload-pack" && - r.Method == "GET" { + r.Method == http.MethodGet: d.InfoRefs(w, r) - } else if path == "git-upload-pack" && r.Method == "POST" { + case path == "git-upload-pack" && r.Method == http.MethodPost: d.UploadPack(w, r) - } else if r.Method == "GET" { + case r.Method == http.MethodGet: d.RepoIndex(w, r) } }
M routes/routes.goroutes/routes.go

@@ -175,8 +175,6 @@ if err := t.ExecuteTemplate(w, "repo", data); err != nil {

log.Println(err) return } - - return } func (d *deps) RepoTree(w http.ResponseWriter, r *http.Request) {

@@ -217,7 +215,6 @@ data["desc"] = getDescription(path)

data["dotdot"] = filepath.Dir(treePath) d.listFiles(files, data, w) - return } func (d *deps) FileContent(w http.ResponseWriter, r *http.Request) {

@@ -308,7 +305,7 @@ return

} gw := gzip.NewWriter(w) - defer gw.Close() + defer func() { _ = gw.Close() }() prefix := fmt.Sprintf("%s-%s", name, ref) err = gr.WriteTar(gw, prefix)
M routes/template.goroutes/template.go

@@ -18,7 +18,7 @@

func (d *deps) Write404(w http.ResponseWriter) { tpath := filepath.Join(d.c.Dirs.Templates, "*") t := template.Must(template.ParseGlob(tpath)) - w.WriteHeader(404) + w.WriteHeader(http.StatusNotFound) if err := t.ExecuteTemplate(w, "404", nil); err != nil { log.Printf("404 template: %s", err) }

@@ -27,7 +27,7 @@

func (d *deps) Write500(w http.ResponseWriter) { tpath := filepath.Join(d.c.Dirs.Templates, "*") t := template.Must(template.ParseGlob(tpath)) - w.WriteHeader(500) + w.WriteHeader(http.StatusInternalServerError) if err := t.ExecuteTemplate(w, "500", nil); err != nil { log.Printf("500 template: %s", err) }

@@ -145,6 +145,5 @@

func (d *deps) showRaw(content string, w http.ResponseWriter) { w.WriteHeader(http.StatusOK) w.Header().Set("Content-Type", "text/plain") - w.Write([]byte(content)) - return + _, _ = w.Write([]byte(content)) }
M routes/util.goroutes/util.go

@@ -1,8 +1,6 @@

package routes import ( - "io/fs" - "log" "net/http" "os" "path/filepath"

@@ -48,91 +46,6 @@ }

} return false -} - -type repoInfo struct { - Git *git.GitRepo - Path string - Category string -} - -func (d *deps) getAllRepos() ([]repoInfo, error) { - repos := []repoInfo{} - seen := map[string]bool{} - max := strings.Count(d.c.Repo.ScanPath, string(os.PathSeparator)) + 2 - - err := filepath.WalkDir(d.c.Repo.ScanPath, func(path string, de fs.DirEntry, err error) error { - if err != nil { - return err - } - - if de.IsDir() { - // Check if we've exceeded our recursion depth - if strings.Count(path, string(os.PathSeparator)) > max { - return fs.SkipDir - } - - if d.isIgnored(path) { - return fs.SkipDir - } - - // A bare repo should always have at least a HEAD file, if it - // doesn't we can continue recursing - if _, err := os.Lstat(filepath.Join(path, "HEAD")); err == nil { - repo, err := git.Open(path, "") - if err != nil { - log.Println(err) - } else { - relpath, _ := filepath.Rel(d.c.Repo.ScanPath, path) - repos = append(repos, repoInfo{ - Git: repo, - Path: relpath, - Category: d.category(path), - }) - // Since we found a Git repo, we don't want to recurse - // further - return fs.SkipDir - } - } - } else if de.Type()&fs.ModeSymlink != 0 { - if d.isIgnored(path) { - return nil - } - - target, err := filepath.EvalSymlinks(path) - if err != nil { - log.Printf("failed to resolve symlink %s: %v", path, err) - return nil - } - - // Avoid duplicates if multiple symlinks point to the same target - if seen[target] { - return nil - } - seen[target] = true - - if _, err := os.Stat(filepath.Join(target, "HEAD")); err == nil { - repo, err := git.Open(target, "") - if err != nil { - log.Println(err) - } else { - relpath, _ := filepath.Rel(d.c.Repo.ScanPath, path) - repos = append(repos, repoInfo{ - Git: repo, - Path: relpath, - Category: d.category(path), - }) - } - } - } - return nil - }) - - return repos, err -} - -func (d *deps) category(path string) string { - return strings.TrimPrefix(filepath.Dir(strings.TrimPrefix(path, d.c.Repo.ScanPath)), string(os.PathSeparator)) } func setContentDisposition(w http.ResponseWriter, name string) {