internal/control/search.go

a869e55cedfc2d279bbaba2e2a0a02eb1114d9b7
gitbay/internal/control/search.go history · blame · raw

121 lines · 3687 bytes

  1package control
  2
  3import (
  4	"fmt"
  5	"io"
  6	"slices"
  7
  8	"gitbay.org/gitbay/internal/gitutil"
  9	"gitbay.org/gitbay/internal/protocol"
 10	"gitbay.org/gitbay/internal/store"
 11)
 12
 13func init() {
 14	register(Command{Path: []string{"search"},
 15		Summary:  "find repositories, issues and merge requests across the instance",
 16		Usage:    "search <query> [--kind repo|issue|mr]",
 17		ReadOnly: true, Run: runSearch})
 18}
 19
 20// searchLimit caps each kind of result. A query that hits more than this
 21// wants narrowing, not a longer page.
 22const searchLimit = 50
 23
 24// SearchResult is one match, in the shape all three kinds share.
 25type SearchResult struct {
 26	Kind      string `json:"kind"` // repo, issue, or mr
 27	Repo      string `json:"repo"`
 28	Number    int64  `json:"number,omitempty"`
 29	Title     string `json:"title"`
 30	Author    string `json:"author,omitempty"`
 31	State     string `json:"state,omitempty"`
 32	UpdatedAt string `json:"updated_at,omitempty"`
 33}
 34
 35// Search runs a query for a user, or for an anonymous visitor when userID
 36// is 0, in which case only public repositories are reached. kinds names
 37// which of repo, issue and mr to look at; empty means all three. It is
 38// exported because the web's /search page renders it for readers who have
 39// no session to dispatch a command as.
 40func Search(st *store.Store, root string, userID int64, q string, kinds []string) ([]SearchResult, error) {
 41	want := func(kind string) bool { return len(kinds) == 0 || slices.Contains(kinds, kind) }
 42	out := []SearchResult{}
 43	if want("repo") {
 44		repos, err := st.VisibleRepos(userID)
 45		if err != nil {
 46			return nil, err
 47		}
 48		for _, r := range repos {
 49			desc := gitutil.ReadDescription(RepoDir(root, r.OwnerName, r.Name))
 50			topics, _ := st.ListTopics(r.ID)
 51			if !MatchesRepo(q, r.Path(), desc, topics) {
 52				continue
 53			}
 54			out = append(out, SearchResult{Kind: "repo", Repo: r.Path(), Title: desc})
 55			if len(out) == searchLimit {
 56				break
 57			}
 58		}
 59	}
 60	for _, k := range []struct {
 61		kind  string
 62		query func(int64, string, int) ([]store.DashboardItem, error)
 63	}{{"issue", st.SearchIssues}, {"mr", st.SearchMRs}} {
 64		if !want(k.kind) {
 65			continue
 66		}
 67		items, err := k.query(userID, q, searchLimit)
 68		if err != nil {
 69			return nil, err
 70		}
 71		for _, it := range items {
 72			out = append(out, SearchResult{Kind: k.kind, Repo: it.RepoPath, Number: it.Number,
 73				Title: it.Title, Author: it.Author, State: it.State, UpdatedAt: it.UpdatedAt})
 74		}
 75	}
 76	return out, nil
 77}
 78
 79func runSearch(c *Ctx, args []string) int {
 80	const usage = "search <query> [--kind repo|issue|mr]"
 81	f, err := parseFlags(args, flagSpec{Multi: []string{"--kind"}, MaxPos: 1, Usage: usage})
 82	if err != nil {
 83		return c.fail(protocol.ExitUsage, "%v", err)
 84	}
 85	if len(f.Pos) != 1 {
 86		return c.fail(protocol.ExitUsage, "usage: %s", usage)
 87	}
 88	if err := validQuery(f.Pos[0]); err != nil {
 89		return c.failErr(err)
 90	}
 91	kinds := f.List("--kind")
 92	for _, k := range kinds {
 93		if k != "repo" && k != "issue" && k != "mr" {
 94			return c.fail(protocol.ExitUsage, "--kind must be repo, issue or mr")
 95		}
 96	}
 97	results, err := Search(c.Store, c.Cfg.Server.Root, c.User.ID, f.Pos[0], kinds)
 98	if err != nil {
 99		return c.fail(protocol.ExitFailure, "%v", err)
100	}
101	return c.emit(results, func(w io.Writer) {
102		for _, r := range results {
103			switch r.Kind {
104			case "repo":
105				fmt.Fprintf(w, "repo\t%s\t%s\n", r.Repo, r.Title)
106			default:
107				fmt.Fprintf(w, "%s\t%s%s%d\t%s\t%s\n", r.Kind, r.Repo,
108					SearchMarker(r.Kind), r.Number, r.State, r.Title)
109			}
110		}
111	})
112}
113
114// SearchMarker is the sigil a result's number carries, shared with the web
115// so a hit reads the same in both places.
116func SearchMarker(kind string) string {
117	if kind == "mr" {
118		return "!"
119	}
120	return "#"
121}