internal/control/symbols.go

e6cd75b5f28bacf51620bb531320c30fd4e66bfd
gitbay/internal/control/symbols.go history · blame · raw

206 lines · 7078 bytes

10 symbols in this file
  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}