1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package web
4
5import (
6 "context"
7 "net/http"
8 "path"
9
10 "github.com/a-h/templ"
11
12 "bnema.dev/ermite/internal/domain"
13)
14
15// Entry kinds of a TreeRow; the stylesheet draws the node of a row by its kind.
16const (
17 kindDir = "dir"
18 kindFile = "file"
19 kindLink = "link"
20 kindModule = "module"
21)
22
23// TreeRow is one entry of a directory.
24type TreeRow struct {
25 Kind string // kindDir, kindFile, kindLink or kindModule
26 Name string
27 Href string // "" for a submodule
28 // Expand is the URL of the rows of a directory (Href?rows); "" for any
29 // other entry.
30 Expand string
31 // Hint says what a directory, a link or a submodule is, in words, for the
32 // screen reader (the node drawn for it is aria-hidden) and the tooltip; ""
33 // for a file.
34 Hint string
35 Size string
36 Subject string
37 CHref string
38 Author string
39 When string
40 WhenAt string
41 Cursor bool
42 // Pos and Set are the aria-posinset and aria-setsize of the row among the
43 // entries of its directory; Set is -1 when the listing is cut, as the
44 // directory has more entries than are listed.
45 Pos, Set int
46}
47
48// TreePage is a directory.
49type TreePage struct {
50 Rows []TreeRow
51 // Path is the directory, "/" for the root.
52 Path string
53 // Up is the page of the parent directory, "" at the root: the h key of the
54 // tree goes there from a top-level entry.
55 Up string
56 // Notes say what the listing leaves out.
57 Notes []string
58 // More, for the rows of a directory expanded in place, is the page of the
59 // directory when it has more entries than are listed; "" otherwise.
60 More string
61 // MorePos is the position of the row that says there are more.
62 MorePos int
63}
64
65// Unit is the noun of the entry count in the head of the tree.
66func (p TreePage) Unit() string {
67 if len(p.Rows) == 1 {
68 return "entry"
69 }
70 return "entries"
71}
72
73// tree lists a directory of a ref, with the last commit of each entry. With
74// ?rows it answers the entries alone, for a directory opened in place.
75//
76//hexcheck:ignore suspicious-business-logic-in-adapter builds the directory page from the use case results; no rule is decided here
77func (s *Server) tree(ctx context.Context, r *http.Request) (templ.Component, int, error) {
78 d, err := s.loadRepo(ctx, r.PathValue("repo"))
79 if err != nil {
80 return nil, 0, err
81 }
82 t, ok := d.locate(r.PathValue("rest"))
83 if !ok {
84 return nil, 0, errNotFound
85 }
86 res, err := s.getTree(ctx, d.repo.Name, t.rev, t.path, true)
87 if err != nil {
88 return nil, 0, err
89 }
90 entries := res.Entries
91 now := s.cfg.Now()
92
93 rowsOnly := r.URL.Query().Has("rows")
94 p := TreePage{Path: "/"}
95 if t.path != "" {
96 p.Path = visibleText(t.path) + "/"
97 }
98 for i, e := range entries {
99 row := TreeRow{Kind: kindFile, Name: visibleText(e.Name), Size: humanSize(e.Size), Cursor: i == 0 && !rowsOnly}
100 switch e.Kind {
101 case domain.EntryDirectory:
102 row.Kind, row.Hint, row.Name, row.Size = kindDir, "directory", row.Name+"/", ""
103 row.Href = repoHref(d.name(), "tree", t.name, e.Path)
104 row.Expand = row.Href + "?rows"
105 case domain.EntrySymlink:
106 row.Kind, row.Hint = kindLink, "symbolic link"
107 row.Href = repoHref(d.name(), "blob", t.name, e.Path)
108 case domain.EntrySubmodule:
109 row.Kind, row.Hint, row.Size = kindModule, "submodule", ""
110 default:
111 row.Href = repoHref(d.name(), "blob", t.name, e.Path)
112 }
113 if c := e.LastCommit; c != nil {
114 last := commitRow(d.name(), *c, now)
115 row.Subject, row.Author, row.CHref, row.When, row.WhenAt = last.Subject, last.Author, last.CHref, last.When, last.WhenAt
116 }
117 p.Rows = append(p.Rows, row)
118 }
119 for i := range p.Rows {
120 p.Rows[i].Pos, p.Rows[i].Set = i+1, len(p.Rows)
121 if res.Truncated {
122 p.Rows[i].Set = -1
123 }
124 }
125 if rowsOnly {
126 // The rows alone, for a directory opened in place (files.js).
127 if res.Truncated {
128 p.More, p.MorePos = repoHref(d.name(), "tree", t.name, t.path), len(p.Rows)+1
129 }
130 return TreeRows(p), http.StatusOK, nil
131 }
132 if res.Truncated {
133 p.Notes = append(p.Notes, "this directory has more entries than are listed here: the first "+itoa(len(entries))+" are shown.")
134 }
135 if res.LastCommitsOmitted {
136 p.Notes = append(p.Notes, "the last commit is shown for the first entries only.")
137 }
138 if t.path != "" {
139 up := path.Dir(t.path)
140 if up == "." {
141 up = ""
142 }
143 p.Up = repoHref(d.name(), "tree", t.name, up)
144 }
145
146 l := s.repoLayout(ctx, d, "tree")
147 // The finder searches the commit this page lists, whichever way the URL
148 // spelled it (an abbreviated id included): the listing knows it.
149 t.commit = res.CommitID.String()
150 pinRef(&l, t)
151 l.Crumbs = pathCrumbs(d.name(), "tree", t)
152 if t.path == "" {
153 l.Title = "tree · " + d.name() + " · " + s.instance
154 } else {
155 l.Title = visibleText(t.path) + " · " + d.name() + " · " + s.instance
156 }
157 return Tree(l, p), http.StatusOK, nil
158}