internal/control/symbols.go
186 lines · 6431 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.emitPage(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 })
131}
132
133// MinSymbolQuery is the shortest query repo symbols and the results page
134// take: one character matches too much of an index to be worth a page.
135const MinSymbolQuery = 2
136
137// StaleSymbolCursor is the refusal for a cursor taken from an index that
138// has since been replaced.
139const StaleSymbolCursor = "the symbol index was rebuilt since that cursor; start again without it"
140
141// SymbolCursor is the paging key after row id of index indexID, the form
142// ParseSymbolCursor reads.
143func SymbolCursor(indexID, id int64) string {
144 return strconv.FormatInt(indexID, 10) + "." + strconv.FormatInt(id, 10)
145}
146
147// ParseSymbolCursor reads "<index id>.<row id>". The index id makes a
148// cursor from before a rebuild fail rather than page through the new
149// index from an unrelated row.
150func ParseSymbolCursor(key string) (int64, int64, bool) {
151 a, b, ok := strings.Cut(key, ".")
152 if !ok {
153 return 0, 0, false
154 }
155 idx, err1 := strconv.ParseInt(a, 10, 64)
156 id, err2 := strconv.ParseInt(b, 10, 64)
157 return idx, id, err1 == nil && err2 == nil && id > 0
158}
159
160// IndexedTree reports whether commit sha has the tree idx was built from:
161// the default branch's head when the index is current, or any other
162// commit with the same content. The blob view uses it to decide whether
163// its names can link into the index.
164func IndexedTree(dir, sha string, idx store.SymbolIndex) bool {
165 if sha == idx.Commit {
166 return true
167 }
168 tree, err := gitutil.ResolveTree(dir, sha)
169 return err == nil && tree == idx.Tree
170}
171
172func runAdminSymbolsReindex(c *Ctx, args []string) int {
173 if len(args) != 1 {
174 return c.usage()
175 }
176 repo, code := adminRepo(c, args[0])
177 if code >= 0 {
178 return code
179 }
180 if err := c.Store.RequestSymbolIndex(repo.ID, true); err != nil {
181 return c.fail(protocol.ExitFailure, "%v", err)
182 }
183 return c.emit(map[string]string{"repo": repo.Path(), "state": "queued"}, func(w io.Writer) {
184 fmt.Fprintf(w, "queued a rebuild of the symbol index of %s\n", repo.Path())
185 })
186}