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// TreeRow is one entry of a directory.
16type TreeRow struct {
17 Glyph string // ▸ directory, @ symbolic link, + submodule, · file
18 Name string
19 Href string // "" for a submodule
20 // Expand is the URL of the rows of a directory (Href?rows); "" for any
21 // other entry.
22 Expand string
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 // More, for the rows of a directory expanded in place, is the page of the
39 // directory when it has more entries than are listed; "" otherwise.
40 More string
41}
42
43// tree lists a directory of a ref, with the last commit of each entry, and
44// renders the README of the directory below.
45func (s *Server) tree(ctx context.Context, r *http.Request) (templ.Component, int, error) {
46 d, err := s.loadRepo(ctx, r.PathValue("repo"))
47 if err != nil {
48 return nil, 0, err
49 }
50 t, ok := d.locate(r.PathValue("rest"))
51 if !ok {
52 return nil, 0, errNotFound
53 }
54 res, err := s.cfg.Git.GetTree(ctx, connect.NewRequest(&pb.GetTreeRequest{
55 Repo: d.name(), Revision: t.rev, Path: t.path, IncludeLastCommit: true,
56 }))
57 if err != nil {
58 return nil, 0, err
59 }
60 entries := res.Msg.GetEntries()
61 now := s.cfg.Now()
62
63 rowsOnly := r.URL.Query().Has("rows")
64 p := TreePage{}
65 for i, e := range entries {
66 row := TreeRow{Glyph: "·", Name: visibleText(e.GetName()), Size: humanSize(e.GetSize()), Cursor: i == 0 && !rowsOnly}
67 switch e.GetKind() {
68 case pb.EntryKind_ENTRY_KIND_DIRECTORY:
69 row.Glyph, row.Name, row.Size = "▸", row.Name+"/", "-"
70 row.Href = repoHref(d.name(), "tree", t.name, e.GetPath())
71 row.Expand = row.Href + "?rows"
72 case pb.EntryKind_ENTRY_KIND_SYMLINK:
73 row.Glyph = "@"
74 row.Href = repoHref(d.name(), "blob", t.name, e.GetPath())
75 case pb.EntryKind_ENTRY_KIND_SUBMODULE:
76 row.Glyph, row.Size = "+", "-"
77 default:
78 row.Href = repoHref(d.name(), "blob", t.name, e.GetPath())
79 }
80 if c := e.GetLastCommit(); c != nil {
81 last := commitRow(d.name(), c, now)
82 row.Subject, row.Author, row.CHref, row.When, row.WhenAt = last.Subject, last.Author, last.CHref, last.When, last.WhenAt
83 }
84 p.Rows = append(p.Rows, row)
85 }
86 if rowsOnly {
87 // The rows alone, for a directory opened in place (files.js).
88 if res.Msg.GetTruncated() {
89 p.More = repoHref(d.name(), "tree", t.name, t.path)
90 }
91 return TreeRows(p), http.StatusOK, nil
92 }
93 if res.Msg.GetTruncated() {
94 p.Notes = append(p.Notes, "this directory has more entries than are listed here: the first "+itoa(len(entries))+" are shown.")
95 }
96 if res.Msg.GetLastCommitsTruncated() {
97 p.Notes = append(p.Notes, "the last commit is shown for the first entries only.")
98 }
99 rd, err := s.readmeIn(ctx, d.name(), res.Msg.GetCommitId(), t.name, t.path, entries)
100 if err != nil {
101 return nil, 0, err
102 }
103 if rd != nil {
104 p.Readme = rd.readme
105 }
106
107 l := s.repoLayout(ctx, d, "tree")
108 // The finder searches the commit this page lists, whichever way the URL
109 // spelled it (an abbreviated id included): the listing knows it.
110 t.commit = res.Msg.GetCommitId()
111 pinRef(&l, t)
112 l.Crumbs = pathCrumbs(d.name(), "tree", t)
113 if t.path == "" {
114 l.Title = "tree · " + d.name() + " · " + s.instance
115 } else {
116 l.Title = visibleText(t.path) + " · " + d.name() + " · " + s.instance
117 }
118 return Tree(l, p), http.StatusOK, nil
119}