1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package web
4
5import (
6 "context"
7 "net/http"
8
9 "github.com/a-h/templ"
10
11 "bnema.dev/ermite/internal/domain"
12)
13
14// TreeRow is one entry of a directory.
15type TreeRow struct {
16 Glyph string // ▸ directory, @ symbolic link, + submodule, · file
17 Name string
18 Href string // "" for a submodule
19 // Expand is the URL of the rows of a directory (Href?rows); "" for any
20 // other entry.
21 Expand string
22 Size string
23 Subject string
24 CHref string
25 Author string
26 When string
27 WhenAt string
28 Cursor bool
29}
30
31// TreePage is a directory.
32type TreePage struct {
33 Rows []TreeRow
34 Readme *Readme
35 // Notes say what the listing leaves out.
36 Notes []string
37 // More, for the rows of a directory expanded in place, is the page of the
38 // directory when it has more entries than are listed; "" otherwise.
39 More string
40}
41
42// tree lists a directory of a ref, with the last commit of each entry, and
43// renders the README of the directory below.
44//
45//hexcheck:ignore suspicious-business-logic-in-adapter builds the directory page from the use case results; no rule is decided here
46func (s *Server) tree(ctx context.Context, r *http.Request) (templ.Component, int, error) {
47 d, err := s.loadRepo(ctx, r.PathValue("repo"))
48 if err != nil {
49 return nil, 0, err
50 }
51 t, ok := d.locate(r.PathValue("rest"))
52 if !ok {
53 return nil, 0, errNotFound
54 }
55 res, err := s.getTree(ctx, d.repo.Name, t.rev, t.path, true)
56 if err != nil {
57 return nil, 0, err
58 }
59 entries := res.Entries
60 now := s.cfg.Now()
61
62 rowsOnly := r.URL.Query().Has("rows")
63 p := TreePage{}
64 for i, e := range entries {
65 row := TreeRow{Glyph: "·", Name: visibleText(e.Name), Size: humanSize(e.Size), Cursor: i == 0 && !rowsOnly}
66 switch e.Kind {
67 case domain.EntryDirectory:
68 row.Glyph, row.Name, row.Size = "▸", row.Name+"/", "-"
69 row.Href = repoHref(d.name(), "tree", t.name, e.Path)
70 row.Expand = row.Href + "?rows"
71 case domain.EntrySymlink:
72 row.Glyph = "@"
73 row.Href = repoHref(d.name(), "blob", t.name, e.Path)
74 case domain.EntrySubmodule:
75 row.Glyph, row.Size = "+", "-"
76 default:
77 row.Href = repoHref(d.name(), "blob", t.name, e.Path)
78 }
79 if c := e.LastCommit; 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 rowsOnly {
86 // The rows alone, for a directory opened in place (files.js).
87 if res.Truncated {
88 p.More = repoHref(d.name(), "tree", t.name, t.path)
89 }
90 return TreeRows(p), http.StatusOK, nil
91 }
92 if res.Truncated {
93 p.Notes = append(p.Notes, "this directory has more entries than are listed here: the first "+itoa(len(entries))+" are shown.")
94 }
95 if res.LastCommitsOmitted {
96 p.Notes = append(p.Notes, "the last commit is shown for the first entries only.")
97 }
98 rd, err := s.readmeIn(ctx, d.repo.Name, res.CommitID.String(), t.name, t.path, entries)
99 if err != nil {
100 return nil, 0, err
101 }
102 if rd != nil {
103 p.Readme = rd.readme
104 }
105
106 l := s.repoLayout(ctx, d, "tree")
107 // The finder searches the commit this page lists, whichever way the URL
108 // spelled it (an abbreviated id included): the listing knows it.
109 t.commit = res.CommitID.String()
110 pinRef(&l, t)
111 l.Crumbs = pathCrumbs(d.name(), "tree", t)
112 if t.path == "" {
113 l.Title = "tree · " + d.name() + " · " + s.instance
114 } else {
115 l.Title = visibleText(t.path) + " · " + d.name() + " · " + s.instance
116 }
117 return Tree(l, p), http.StatusOK, nil
118}