Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Blame · server/web/code_search.go

Line attribution on main · 124 line(s)

aef99284
Keenan Nemetz
15d ago
1 package web
2
3 // Code search at one ref. Content matching is delegated to git grep with fixed
4 // strings: there is no second index to go stale, and a user's query is data
5 // rather than a regular expression.
6
7 import (
8 "net/http"
9 "strconv"
10 "strings"
11 "unicode/utf8"
12 )
13
14 const (
15 codeSearchBytes = 256 << 10
16 codeSearchHits = 200
17 codeSearchFiles = 50
18 )
19
20 type codeHitLine struct {
21 Line int
22 Text string
23 }
24
25 type codeHitGroup struct {
26 Path string
27 Lines []codeHitLine
28 }
29
30 type codeSearchView struct {
31 Page
32 Name string
33 Ref string
34 Commit string
35 Query string
36 FileHits []string
37 Groups []codeHitGroup
38 Hits int
39 Truncated bool
40 Error string
41 }
42
43 func (w *Web) CodeSearch(rw http.ResponseWriter, r *http.Request) {
44 m, ok := w.open(rw, r)
45 if !ok {
46 return
47 }
48 ref := strings.TrimSpace(r.URL.Query().Get("ref"))
49 if ref == "" {
50 ref = m.DefaultBranch
51 }
52 commit, _ := m.Git().RevParse(ref)
53 query := strings.TrimSpace(r.URL.Query().Get("q"))
54 view := codeSearchView{
55 Page: w.page(r, m.Name+" · code search"), Name: m.Name,
56 Ref: ref, Commit: commit, Query: query,
57 }
58 if query == "" || commit == "" {
59 w.render(rw, "code_search", view, 0)
60 return
61 }
62 if utf8.RuneCountInString(query) > 200 {
63 view.Error = "Search is limited to 200 characters."
64 w.render(rw, "code_search", view, http.StatusBadRequest)
65 return
66 }
67
701232cf
Keenan Nemetz
12d ago
68 release, ok := w.gitSlot(rw, r)
69 if !ok {
70 return
71 }
72 defer release()
73 g, cancel := gitRepo(r, m)
74 defer cancel()
aef99284
Keenan Nemetz
15d ago
75 needle := strings.ToLower(query)
76 if entries, err := g.LsTreeR(commit); err == nil {
77 for _, e := range entries {
78 if e.Type != "blob" || !strings.Contains(strings.ToLower(e.Name), needle) {
79 continue
80 }
81 view.FileHits = append(view.FileHits, e.Name)
82 if len(view.FileHits) == codeSearchFiles {
83 view.Truncated = true
84 break
85 }
86 }
87 }
88
89 raw, cut, err := g.Grep(commit, query, codeSearchBytes)
90 if err != nil {
91 w.serverError(rw, r, "code search failed: "+err.Error())
92 return
93 }
94 view.Truncated = view.Truncated || cut
95 byPath := map[string]int{}
96 for _, line := range strings.Split(string(raw), "\n") {
97 // git grep prefixes every record with the tree-ish it searched; strip
98 // that when present, and accept the short form too.
99 parts := strings.SplitN(line, ":", 4)
100 if len(parts) == 4 && parts[0] == commit {
101 parts = parts[1:]
102 }
103 if len(parts) != 3 {
104 continue
105 }
106 n, err := strconv.Atoi(parts[1])
107 if err != nil {
108 continue
109 }
110 group, ok := byPath[parts[0]]
111 if !ok {
112 group = len(view.Groups)
113 byPath[parts[0]] = group
114 view.Groups = append(view.Groups, codeHitGroup{Path: parts[0]})
115 }
116 view.Groups[group].Lines = append(view.Groups[group].Lines, codeHitLine{Line: n, Text: parts[2]})
117 view.Hits++
118 if view.Hits == codeSearchHits {
119 view.Truncated = true
120 break
121 }
122 }
123 w.render(rw, "code_search", view, 0)
124 }