routes: Implement support for categories This changes the code so that in the routes we now build up the repository path from the category and the name. If no category is provided you simply end up with the name. This path is now used everywhere a path to a repository is constructed which takes care of 80% of making categories work. Handlers on the mux are added for the category matching and reordered to ensure we hit the right entry first. The new repository struct contains all info we need for a repository and we default to allowing some fields to be empty if we can't retrieve the information whereas previously we might've returned an error. The entries is a single level tree of sorts that lifts the category, if available, to be its name and otherwise uses the repo name. We can use this to sort all entries alphabetically and now loop over them in our template.
Daniele Sluijters daenney@users.noreply.github.com
Tue, 03 Jan 2023 14:48:59 +0100
5 files changed,
127 insertions(+),
78 deletions(-)
M
routes/handler.go
→
routes/handler.go
@@ -39,12 +39,23 @@ })
mux.HandleFunc("/", d.Index, "GET") mux.HandleFunc("/static/:file", d.ServeStatic, "GET") - mux.HandleFunc("/:name", d.Multiplex, "GET", "POST") + + mux.HandleFunc("/:category/:name/tree/:ref/...", d.RepoTree, "GET") + mux.HandleFunc("/:category/:name/blob/:ref/...", d.FileContent, "GET") + mux.HandleFunc("/:category/:name/log/:ref", d.Log, "GET") + mux.HandleFunc("/:category/:name/commit/:ref", d.Diff, "GET") + mux.HandleFunc("/:category/:name/refs", d.Refs, "GET") + mux.HandleFunc("/:name/tree/:ref/...", d.RepoTree, "GET") mux.HandleFunc("/:name/blob/:ref/...", d.FileContent, "GET") mux.HandleFunc("/:name/log/:ref", d.Log, "GET") mux.HandleFunc("/:name/commit/:ref", d.Diff, "GET") mux.HandleFunc("/:name/refs", d.Refs, "GET") + + mux.HandleFunc("/:category/:name", d.Multiplex, "GET", "POST") + mux.HandleFunc("/:name", d.Multiplex, "GET", "POST") + + mux.HandleFunc("/:category/:name/...", d.Multiplex, "GET", "POST") mux.HandleFunc("/:name/...", d.Multiplex, "GET", "POST") return mux
M
routes/routes.go
→
routes/routes.go
@@ -5,15 +5,11 @@ "fmt"
"html/template" "log" "net/http" - "os" "path/filepath" - "sort" - "time" "git.icyphox.sh/legit/config" "git.icyphox.sh/legit/git" "github.com/alexedwards/flow" - "github.com/dustin/go-humanize" "github.com/microcosm-cc/bluemonday" "github.com/russross/blackfriday/v2" )@@ -23,58 +19,19 @@ c *config.Config
} func (d *deps) Index(w http.ResponseWriter, r *http.Request) { - dirs, err := os.ReadDir(d.c.Repo.ScanPath) + repos, err := d.getAllRepos() if err != nil { d.Write500(w) log.Printf("reading scan path: %s", err) return } - type info struct { - Name, Desc, Idle string - d time.Time - } - - infos := []info{} - - for _, dir := range dirs { - if d.isIgnored(dir.Name()) { - continue - } - - path := filepath.Join(d.c.Repo.ScanPath, dir.Name()) - gr, err := git.Open(path, "") - if err != nil { - continue - } - - c, err := gr.LastCommit() - if err != nil { - d.Write500(w) - log.Println(err) - return - } - - desc := getDescription(path) - - infos = append(infos, info{ - Name: dir.Name(), - Desc: desc, - Idle: humanize.Time(c.Author.When), - d: c.Author.When, - }) - } - - sort.Slice(infos, func(i, j int) bool { - return infos[j].d.Before(infos[i].d) - }) - tpath := filepath.Join(d.c.Dirs.Templates, "*") t := template.Must(template.ParseGlob(tpath)) data := make(map[string]interface{}) data["meta"] = d.c.Meta - data["info"] = infos + data["info"] = repos.Children if err := t.ExecuteTemplate(w, "index", data); err != nil { log.Println(err)@@ -83,12 +40,12 @@ }
} func (d *deps) RepoIndex(w http.ResponseWriter, r *http.Request) { - name := flow.Param(r.Context(), "name") + name := repoPath(r.Context()) if d.isIgnored(name) { d.Write404(w) return } - name = filepath.Clean(name) + path := filepath.Join(d.c.Repo.ScanPath, name) gr, err := git.Open(path, "")@@ -157,20 +114,18 @@ if err := t.ExecuteTemplate(w, "repo", data); err != nil {
log.Println(err) return } - - return } func (d *deps) RepoTree(w http.ResponseWriter, r *http.Request) { - name := flow.Param(r.Context(), "name") + name := repoPath(r.Context()) if d.isIgnored(name) { d.Write404(w) return } + treePath := flow.Param(r.Context(), "...") ref := flow.Param(r.Context(), "ref") - name = filepath.Clean(name) path := filepath.Join(d.c.Repo.ScanPath, name) gr, err := git.Open(path, ref) if err != nil {@@ -192,11 +147,10 @@ data["parent"] = treePath
data["desc"] = getDescription(path) d.listFiles(files, data, w) - return } func (d *deps) FileContent(w http.ResponseWriter, r *http.Request) { - name := flow.Param(r.Context(), "name") + name := repoPath(r.Context()) if d.isIgnored(name) { d.Write404(w) return@@ -204,7 +158,6 @@ }
treePath := flow.Param(r.Context(), "...") ref := flow.Param(r.Context(), "ref") - name = filepath.Clean(name) path := filepath.Join(d.c.Repo.ScanPath, name) gr, err := git.Open(path, ref) if err != nil {@@ -213,6 +166,11 @@ return
} contents, err := gr.FileContent(treePath) + if err != nil { + d.Write500(w) + log.Println(err) + return + } data := make(map[string]any) data["name"] = name data["ref"] = ref@@ -220,11 +178,10 @@ data["desc"] = getDescription(path)
data["path"] = treePath d.showFile(contents, data, w) - return } func (d *deps) Log(w http.ResponseWriter, r *http.Request) { - name := flow.Param(r.Context(), "name") + name := repoPath(r.Context()) if d.isIgnored(name) { d.Write404(w) return@@ -262,7 +219,7 @@ }
} func (d *deps) Diff(w http.ResponseWriter, r *http.Request) { - name := flow.Param(r.Context(), "name") + name := repoPath(r.Context()) if d.isIgnored(name) { d.Write404(w) return@@ -303,7 +260,7 @@ }
} func (d *deps) Refs(w http.ResponseWriter, r *http.Request) { - name := flow.Param(r.Context(), "name") + name := repoPath(r.Context()) if d.isIgnored(name) { d.Write404(w) return@@ -324,8 +281,8 @@ }
branches, err := gr.Branches() if err != nil { - log.Println(err) d.Write500(w) + log.Println(err) return }
M
routes/util.go
→
routes/util.go
@@ -1,13 +1,17 @@
package routes import ( + "context" "io/fs" "log" "os" "path/filepath" + "sort" "strings" "git.icyphox.sh/legit/git" + "github.com/alexedwards/flow" + "github.com/dustin/go-humanize" ) func isGoModule(gr *git.GitRepo) bool {@@ -35,14 +39,51 @@
return false } -type repoInfo struct { - Git *git.GitRepo - Path string - Category string +type repository struct { + Name string + Category string + Path string + Slug string + Description string + LastCommit string +} + +type entry struct { + Name string + Repositories []*repository +} + +type entries struct { + Children []*entry + c map[string]*entry +} + +func (ent *entries) Add(r repository) { + if r.Category == "" { + ent.Children = append(ent.Children, &entry{ + Name: r.Name, + Repositories: []*repository{&r}, + }) + return + } + t, ok := ent.c[r.Category] + if !ok { + t := &entry{ + Name: r.Category, + Repositories: []*repository{&r}, + } + ent.c[r.Category] = t + ent.Children = append(ent.Children, t) + return + } + t.Repositories = append(t.Repositories, &r) } -func (d *deps) getAllRepos() ([]repoInfo, error) { - repos := []repoInfo{} +func (d *deps) getAllRepos() (*entries, error) { + entries := &entries{ + Children: []*entry{}, + c: map[string]*entry{}, + } 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 {@@ -68,11 +109,18 @@ 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), - }) + category := strings.Split(relpath, string(os.PathSeparator))[0] + r := repository{ + Name: filepath.Base(path), + Category: category, + Path: path, + Slug: relpath, + Description: getDescription(path), + } + if c, err := repo.LastCommit(); err == nil { + r.LastCommit = humanize.Time(c.Author.When) + } + entries.Add(r) // Since we found a Git repo, we don't want to recurse // further return fs.SkipDir@@ -81,10 +129,15 @@ }
} return nil }) - - return repos, err + sort.Slice(entries.Children, func(i, j int) bool { + return entries.Children[i].Name < entries.Children[j].Name + }) + return entries, err } -func (d *deps) category(path string) string { - return strings.TrimPrefix(filepath.Dir(strings.TrimPrefix(path, d.c.Repo.ScanPath)), string(os.PathSeparator)) +func repoPath(ctx context.Context) string { + return filepath.Join( + filepath.Clean(flow.Param(ctx, "category")), + filepath.Clean(flow.Param(ctx, "name")), + ) }
M
static/style.css
→
static/style.css
@@ -108,6 +108,15 @@ grid-row-gap: 0.5em;
min-width: 0; } +.index-category { + background-color: var(--medium-gray); + font-weight: bold; +} + +.index-category-name { + padding-left: 0.7em; +} + .clone-url { padding-top: 2rem; }@@ -289,6 +298,14 @@ }
.index-name:not(:first-child) { padding-top: 1.5rem; + } + + .index-category { + margin-top: 1.5rem; + } + + .index-category-name { + padding-top: 0.7rem; } .commit-info:not(:last-child) {
M
templates/index.html
→
templates/index.html
@@ -14,9 +14,20 @@ <body>
<main> <div class="index"> {{ range .info }} - <div class="index-name"><a href="/{{ .Name }}">{{ .Name }}</a></div> - <div class="desc">{{ .Desc }}</div> - <div>{{ .Idle }}</div> + {{ if eq (len .Repositories) 1 }} + {{ $repo := (index .Repositories 0) }} + <div class="index-name"><a href="/{{ $repo.Slug }}">{{ $repo.Name }}</a></div> + <div class="desc">{{ $repo.Description }}</div> + <div>{{ $repo.LastCommit }}</div> + {{ end }} + {{ if gt (len .Repositories) 1 }} + <div class="index-category">{{ .Name }}/</div><div></div><div></div> + {{ range .Repositories }} + <div class="index-category-name"><a href="/{{ .Slug }}">{{ .Name }}</a></div> + <div class="desc">{{ .Description }}</div> + <div>{{ .LastCommit }}</div> + {{ end }} + {{ end }} {{ end }} </div> </main>