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.

Add repository code search

aef992843b95fd107327b103c12d135cd708c11e · Keenan Nemetz · 15d ago · parent 5cab7b18

Add repository code search

The Code tab now searches file names and plain-text contents at the selected branch. Contents go through git grep with fixed strings, case-insensitive matching, binary skipping, and a capped result set; there is no second index to drift. Results group by file and link to line anchors in the blob view. Also highlights a targeted line and adds the remaining polish notes to the UI plan.

server/web/code_search.go +118

@@ -0,0 +1,118 @@
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
68 g := m.Git()
69 needle := strings.ToLower(query)
70 if entries, err := g.LsTreeR(commit); err == nil {
71 for _, e := range entries {
72 if e.Type != "blob" || !strings.Contains(strings.ToLower(e.Name), needle) {
73 continue
74 }
75 view.FileHits = append(view.FileHits, e.Name)
76 if len(view.FileHits) == codeSearchFiles {
77 view.Truncated = true
78 break
79 }
80 }
81 }
82
83 raw, cut, err := g.Grep(commit, query, codeSearchBytes)
84 if err != nil {
85 w.serverError(rw, r, "code search failed: "+err.Error())
86 return
87 }
88 view.Truncated = view.Truncated || cut
89 byPath := map[string]int{}
90 for _, line := range strings.Split(string(raw), "\n") {
91 // git grep prefixes every record with the tree-ish it searched; strip
92 // that when present, and accept the short form too.
93 parts := strings.SplitN(line, ":", 4)
94 if len(parts) == 4 && parts[0] == commit {
95 parts = parts[1:]
96 }
97 if len(parts) != 3 {
98 continue
99 }
100 n, err := strconv.Atoi(parts[1])
101 if err != nil {
102 continue
103 }
104 group, ok := byPath[parts[0]]
105 if !ok {
106 group = len(view.Groups)
107 byPath[parts[0]] = group
108 view.Groups = append(view.Groups, codeHitGroup{Path: parts[0]})
109 }
110 view.Groups[group].Lines = append(view.Groups[group].Lines, codeHitLine{Line: n, Text: parts[2]})
111 view.Hits++
112 if view.Hits == codeSearchHits {
113 view.Truncated = true
114 break
115 }
116 }
117 w.render(rw, "code_search", view, 0)
118 }