internal/control/symbols.go
206 lines · 7078 bytes
1package control
2
3import (
4 "errors"
5 "fmt"
6 "io"
7 "strconv"
8 "strings"
9
10 "gitbay.org/gitbay/internal/gitutil"
11 "gitbay.org/gitbay/internal/policy"
12 "gitbay.org/gitbay/internal/protocol"
13 "gitbay.org/gitbay/internal/store"
14 "gitbay.org/gitbay/internal/symbols"
15)
16
17func init() {
18 register(Command{Path: []string{"repo", "symbols"},
19 Summary: "find where a name is defined, from the default branch's symbol index",
20 Usage: "repo symbols <owner/name> [--ref <ref>] [--kind <kind>] [--limit <n>] [--cursor <c>] <query>",
21 Flags: []Flag{
22 {"--ref", "<ref>", "a ref whose tree is the indexed one; only the default branch is indexed", "the default branch"},
23 {"--kind", "<kind>", "only this kind: " + strings.Join(symbols.Kinds, ", "), ""},
24 {"--limit", "<n>", "rows per page", ""},
25 {"--cursor", "<c>", "continue from the previous page", ""},
26 },
27 Examples: []string{
28 "repo symbols krz/gitbay Dispatch",
29 "repo symbols krz/gitbay --kind method Ctx.",
30 },
31 ReadOnly: true, Run: runRepoSymbols})
32 register(Command{Path: []string{"admin", "symbols", "reindex"},
33 Summary: "rebuild a repository's symbol index, even when its tree is already indexed (instance admins)",
34 Usage: "admin symbols reindex <owner/name>",
35 Examples: []string{"admin symbols reindex krz/gitbay"},
36 Run: runAdminSymbolsReindex})
37}
38
39// symbolsUnpaged caps a listing given without --limit or --cursor.
40const symbolsUnpaged = maxPageLimit
41
42type symbolOut struct {
43 Name string `json:"name"`
44 Kind string `json:"kind"`
45 Path string `json:"path"`
46 Line int `json:"line"`
47}
48
49func runRepoSymbols(c *Ctx, args []string) int {
50 args, p, code := parsePageFlags(c, args, "symbol", false)
51 if code >= 0 {
52 return code
53 }
54 f, err := c.parseArgs(args, flagSpec{Values: []string{"--ref", "--kind"}, MaxPos: 2, Usage: c.Cmd.Usage})
55 if err != nil {
56 return c.fail(protocol.ExitUsage, "%v", err)
57 }
58 path, query, ref, kind := f.pos(0), f.pos(1), f.Value("--ref"), f.Value("--kind")
59 if path == "" || query == "" {
60 return c.usage()
61 }
62 if len(query) < MinSymbolQuery || len(query) > maxQueryLen {
63 return c.fail(protocol.ExitUsage, "query must be %d to %d characters", MinSymbolQuery, maxQueryLen)
64 }
65 if kind != "" && !symbols.ValidKind(kind) {
66 return c.fail(protocol.ExitUsage, "--kind must be one of %s", strings.Join(symbols.Kinds, ", "))
67 }
68 repo, code := resolveRepo(c, path, policy.CanRead)
69 if code >= 0 {
70 return code
71 }
72 idx, err := c.Store.SymbolIndexFor(repo.ID)
73 if errors.Is(err, store.ErrNotFound) {
74 if f, ferr := c.Store.SymbolFailureFor(repo.ID); ferr == nil {
75 return c.fail(protocol.ExitFailure, "the symbol index of %s could not be built: %s", repo.Path(), f.Note)
76 }
77 return c.fail(protocol.ExitNotFound, "%s has no symbol index yet; one is built after a push to %s", repo.Path(), repo.DefaultBranch)
78 } else if err != nil {
79 return c.fail(protocol.ExitFailure, "%v", err)
80 }
81 if ref != "" && ref != repo.DefaultBranch {
82 dir := RepoDir(c.Cfg.Server.Root, repo.OwnerName, repo.Name)
83 sha, err := gitutil.ResolveRef(dir, ref)
84 if err != nil || !IndexedTree(dir, sha, idx) {
85 return c.fail(protocol.ExitNotFound, "only the default branch, %s, is indexed; %s is not at the indexed tree", repo.DefaultBranch, ref)
86 }
87 }
88 var after int64
89 if p.key != "" {
90 cursorIdx, id, ok := ParseSymbolCursor(p.key)
91 if !ok {
92 return c.fail(protocol.ExitUsage, "bad cursor")
93 }
94 if cursorIdx != idx.ID {
95 return c.fail(protocol.ExitUsage, "%s", StaleSymbolCursor)
96 }
97 after = id
98 }
99 limit := p.queryLimit()
100 if !p.active {
101 limit = symbolsUnpaged + 1
102 }
103 rows, err := c.Store.SearchSymbols(idx.ID, query, kind, limit, after)
104 if err != nil {
105 return c.fail(protocol.ExitFailure, "%v", err)
106 }
107 capped := !p.active && len(rows) > symbolsUnpaged
108 if capped {
109 rows = rows[:symbolsUnpaged]
110 }
111 rows, next := trimPage(p, rows, "symbol", func(r store.SymbolRow) string {
112 return SymbolCursor(idx.ID, r.ID)
113 })
114 var ds []symbolOut
115 for _, r := range rows {
116 ds = append(ds, symbolOut{r.Name, r.Kind, r.Path, r.Line})
117 }
118 return c.emitPageView(p, ds, next, func(w io.Writer) {
119 tb := c.table(w, "NAME", "KIND", "LOCATION")
120 for _, d := range ds {
121 tb.row(cRef(d.Name), cText(d.Kind), cFlex(fmt.Sprintf("%s:%d", d.Path, d.Line)))
122 }
123 tb.flush()
124 if capped {
125 fmt.Fprintf(c.Stderr, "first %d matches; page with --limit and --cursor\n", symbolsUnpaged)
126 }
127 if idx.State == "partial" {
128 fmt.Fprintf(c.Stderr, "the index is partial: %s\n", idx.Note)
129 }
130 }, func() screen {
131 rows := make([]row, len(ds))
132 for i, d := range ds {
133 rows[i] = rowOf(cRef(d.Name), cText(d.Kind), cFlex(fmt.Sprintf("%s:%d", d.Path, d.Line)))
134 }
135 if capped {
136 fmt.Fprintf(c.Stderr, "first %d matches; page with --limit and --cursor\n", symbolsUnpaged)
137 }
138 if idx.State == "partial" {
139 fmt.Fprintf(c.Stderr, "the index is partial: %s\n", idx.Note)
140 }
141 s := listScreen(fmt.Sprintf("Symbols matching %q", query), rows)
142 if len(ds) > 0 {
143 cat := []string{"repo", "cat", repo.Path(), ds[0].Path}
144 if ref != "" {
145 cat = append(cat, "--ref", ref)
146 }
147 s.actions = []action{{"Read", cat}}
148 }
149 return s
150 })
151}
152
153// MinSymbolQuery is the shortest query repo symbols and the results page
154// take: one character matches too much of an index to be worth a page.
155const MinSymbolQuery = 2
156
157// StaleSymbolCursor is the refusal for a cursor taken from an index that
158// has since been replaced.
159const StaleSymbolCursor = "the symbol index was rebuilt since that cursor; start again without it"
160
161// SymbolCursor is the paging key after row id of index indexID, the form
162// ParseSymbolCursor reads.
163func SymbolCursor(indexID, id int64) string {
164 return strconv.FormatInt(indexID, 10) + "." + strconv.FormatInt(id, 10)
165}
166
167// ParseSymbolCursor reads "<index id>.<row id>". The index id makes a
168// cursor from before a rebuild fail rather than page through the new
169// index from an unrelated row.
170func ParseSymbolCursor(key string) (int64, int64, bool) {
171 a, b, ok := strings.Cut(key, ".")
172 if !ok {
173 return 0, 0, false
174 }
175 idx, err1 := strconv.ParseInt(a, 10, 64)
176 id, err2 := strconv.ParseInt(b, 10, 64)
177 return idx, id, err1 == nil && err2 == nil && id > 0
178}
179
180// IndexedTree reports whether commit sha has the tree idx was built from:
181// the default branch's head when the index is current, or any other
182// commit with the same content. The blob view uses it to decide whether
183// its names can link into the index.
184func IndexedTree(dir, sha string, idx store.SymbolIndex) bool {
185 if sha == idx.Commit {
186 return true
187 }
188 tree, err := gitutil.ResolveTree(dir, sha)
189 return err == nil && tree == idx.Tree
190}
191
192func runAdminSymbolsReindex(c *Ctx, args []string) int {
193 if len(args) != 1 {
194 return c.usage()
195 }
196 repo, code := adminRepo(c, args[0])
197 if code >= 0 {
198 return code
199 }
200 if err := c.Store.RequestSymbolIndex(repo.ID, true); err != nil {
201 return c.fail(protocol.ExitFailure, "%v", err)
202 }
203 return c.emit(map[string]string{"repo": repo.Path(), "state": "queued"}, func(w io.Writer) {
204 fmt.Fprintf(w, "queued a rebuild of the symbol index of %s\n", repo.Path())
205 })
206}