1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package web
4
5import (
6 "context"
7 "net/http"
8
9 "connectrpc.com/connect"
10 "github.com/a-h/templ"
11
12 pb "bnema.dev/ermite-api/gen/ermite"
13)
14
15// maxFileTargets bounds the files of a directory offered in the ":" list.
16const maxFileTargets = 200
17
18// TreeRow is one entry of a directory.
19type TreeRow struct {
20 Glyph string // ▾ directory, @ symbolic link, + submodule, · file
21 Name string
22 Href string // "" for a submodule
23 Size string
24 Subject string
25 CHref string
26 Author string
27 When string
28 WhenAt string
29 Cursor bool
30}
31
32// TreePage is a directory.
33type TreePage struct {
34 Rows []TreeRow
35 Readme *Readme
36 // Notes say what the listing leaves out.
37 Notes []string
38}
39
40// tree lists a directory of a ref, with the last commit of each entry, and
41// renders the README of the directory below.
42func (s *Server) tree(ctx context.Context, r *http.Request) (templ.Component, int, error) {
43 d, err := s.loadRepo(ctx, r.PathValue("repo"))
44 if err != nil {
45 return nil, 0, err
46 }
47 t, ok := d.locate(r.PathValue("rest"))
48 if !ok {
49 return nil, 0, errNotFound
50 }
51 res, err := s.cfg.Git.GetTree(ctx, connect.NewRequest(&pb.GetTreeRequest{
52 Repo: d.name(), Revision: t.rev, Path: t.path, IncludeLastCommit: true,
53 }))
54 if err != nil {
55 return nil, 0, err
56 }
57 entries := res.Msg.GetEntries()
58 now := s.cfg.Now()
59
60 p := TreePage{}
61 l := s.repoLayout(ctx, d, "tree")
62 for i, e := range entries {
63 row := TreeRow{Glyph: "·", Name: visibleText(e.GetName()), Size: humanSize(e.GetSize()), Cursor: i == 0}
64 switch e.GetKind() {
65 case pb.EntryKind_ENTRY_KIND_DIRECTORY:
66 row.Glyph, row.Name, row.Size = "▾", row.Name+"/", "-"
67 row.Href = repoHref(d.name(), "tree", t.name, e.GetPath())
68 case pb.EntryKind_ENTRY_KIND_SYMLINK:
69 row.Glyph = "@"
70 row.Href = repoHref(d.name(), "blob", t.name, e.GetPath())
71 case pb.EntryKind_ENTRY_KIND_SUBMODULE:
72 row.Glyph, row.Size = "+", "-"
73 default:
74 row.Href = repoHref(d.name(), "blob", t.name, e.GetPath())
75 if len(l.Cmd) < maxFileTargets {
76 l.Cmd = append(l.Cmd, CmdItem{Kind: "file", Label: visibleText(e.GetPath()), Href: row.Href})
77 }
78 }
79 if c := e.GetLastCommit(); c != nil {
80 last := commitRow(d.name(), c, now)
81 row.Subject, row.Author, row.CHref, row.When, row.WhenAt = last.Subject, last.Author, last.CHref, last.When, last.WhenAt
82 }
83 p.Rows = append(p.Rows, row)
84 }
85 if res.Msg.GetTruncated() {
86 p.Notes = append(p.Notes, "this directory has more entries than are listed here: the first "+itoa(len(entries))+" are shown.")
87 }
88 if res.Msg.GetLastCommitsTruncated() {
89 p.Notes = append(p.Notes, "the last commit is shown for the first entries only.")
90 }
91 rd, err := s.readmeIn(ctx, d.name(), res.Msg.GetCommitId(), t.name, t.path, entries)
92 if err != nil {
93 return nil, 0, err
94 }
95 if rd != nil {
96 p.Readme = rd.readme
97 }
98
99 pinRef(&l, t)
100 l.Crumbs = pathCrumbs(d.name(), "tree", t)
101 if t.path == "" {
102 l.Title = "tree · " + d.name() + " · " + s.instance
103 } else {
104 l.Title = visibleText(t.path) + " · " + d.name() + " · " + s.instance
105 }
106 l.Status = Status{
107 Mode: "tree", Context: "tree · " + plural(len(entries), "entry", "entries"),
108 Hints: "j/k move · enter open · / filter · b ref · ? help", Cursor: true, Count: len(p.Rows),
109 }
110 return Tree(l, p), http.StatusOK, nil
111}