internal/store/search.go
79 lines · 2662 bytes
1package store
2
3// Search across repositories, for the `search` command and the web's
4// /search page. Visibility is the same rule everywhere: public plus what
5// the user reaches, so an anonymous caller (user id 0) sees only public
6// rows rather than a different query.
7
8// visibleCond admits public repositories and anything the user reaches.
9const visibleCond = `(r.visibility = 'public' OR ` + reachableCond + `)`
10
11// VisibleRepos returns every repository the user may read, public ones
12// included, ordered by owner then name.
13func (s *Store) VisibleRepos(userID int64) ([]Repo, error) {
14 rows, err := s.DB.Query(repoSelect+" WHERE "+visibleCond+" ORDER BY 4, r.name", userID)
15 if err != nil {
16 return nil, err
17 }
18 defer rows.Close()
19 var out []Repo
20 for rows.Next() {
21 r, err := scanRepo(rows)
22 if err != nil {
23 return nil, err
24 }
25 out = append(out, r)
26 }
27 return out, rows.Err()
28}
29
30// SearchIssues returns issues whose title contains q, newest activity
31// first. Titles only: body search is FTS work that belongs with the
32// per-repository issue search.
33func (s *Store) SearchIssues(userID int64, q string, limit int) ([]DashboardItem, error) {
34 return s.searchQuery("issues", userID, q, limit)
35}
36
37// SearchMRs is SearchIssues for merge requests.
38func (s *Store) SearchMRs(userID int64, q string, limit int) ([]DashboardItem, error) {
39 return s.searchQuery("merge_requests", userID, q, limit)
40}
41
42func (s *Store) searchQuery(table string, userID int64, q string, limit int) ([]DashboardItem, error) {
43 rows, err := s.DB.Query(`
44 SELECT COALESCE(u.username, o.name) || '/' || r.name,
45 x.number, x.title, au.username, x.state, x.updated_at
46 FROM `+table+` x
47 JOIN repos r ON r.id = x.repo_id
48 LEFT JOIN users u ON r.owner_kind = 'user' AND u.id = r.owner_id
49 LEFT JOIN orgs o ON r.owner_kind = 'org' AND o.id = r.owner_id
50 JOIN users au ON au.id = x.author_id
51 WHERE x.title LIKE '%' || ?2 || '%' ESCAPE '\' AND `+visibleCond+`
52 ORDER BY x.updated_at DESC LIMIT ?3`, userID, escapeLike(q), limit)
53 if err != nil {
54 return nil, err
55 }
56 defer rows.Close()
57 var out []DashboardItem
58 for rows.Next() {
59 var d DashboardItem
60 if err := rows.Scan(&d.RepoPath, &d.Number, &d.Title, &d.Author, &d.State, &d.UpdatedAt); err != nil {
61 return nil, err
62 }
63 out = append(out, d)
64 }
65 return out, rows.Err()
66}
67
68// escapeLike neutralises the LIKE wildcards, so a query containing % or _
69// matches those characters rather than everything.
70func escapeLike(q string) string {
71 var b []byte
72 for i := 0; i < len(q); i++ {
73 if c := q[i]; c == '%' || c == '_' || c == '\\' {
74 b = append(b, '\\')
75 }
76 b = append(b, q[i])
77 }
78 return string(b)
79}