all repos — legit @ 6857937283c6dac2ce6d07c9b0a0794b9a75a33d

web frontend for git

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
commit

6857937283c6dac2ce6d07c9b0a0794b9a75a33d

parent

4a53314108486ca25e4ef6e4dc250d2d407dd237

5 files changed, 127 insertions(+), 78 deletions(-)

jump to
M routes/handler.goroutes/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.goroutes/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.goroutes/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.cssstatic/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.htmltemplates/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>