Skip to content

Commit 8eac762

Browse files
feat(grep): search a repository (#86)
Co-authored-by: Joe Chen <[email protected]>
1 parent 03d62cb commit 8eac762

File tree

4 files changed

+267
-4
lines changed

4 files changed

+267
-4
lines changed

git_test.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,9 @@ const repoPath = "testdata/testrepo.git"
2222
var testrepo *Repository
2323

2424
func TestMain(m *testing.M) {
25-
verbose := flag.Bool("verbose", false, "")
2625
flag.Parse()
2726

28-
if *verbose {
27+
if testing.Verbose() {
2928
SetOutput(os.Stdout)
3029
}
3130

repo_diff.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -126,15 +126,15 @@ func (r *Repository) RawDiff(rev string, diffType RawDiffFormat, w io.Writer, op
126126
if commit.ParentsCount() == 0 {
127127
cmd = cmd.AddArgs("format-patch").
128128
AddOptions(opt.CommandOptions).
129-
AddArgs("--full-index", "--no-signature", "--stdout", "--root", rev)
129+
AddArgs("--full-index", "--no-signoff", "--no-signature", "--stdout", "--root", rev)
130130
} else {
131131
c, err := commit.Parent(0)
132132
if err != nil {
133133
return err
134134
}
135135
cmd = cmd.AddArgs("format-patch").
136136
AddOptions(opt.CommandOptions).
137-
AddArgs("--full-index", "--no-signature", "--stdout", rev+"..."+c.ID.String())
137+
AddArgs("--full-index", "--no-signoff", "--no-signature", "--stdout", rev+"..."+c.ID.String())
138138
}
139139
default:
140140
return fmt.Errorf("invalid diffType: %s", diffType)

repo_grep.go

+120
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// Copyright 2022 The Gogs Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package git
6+
7+
import (
8+
"fmt"
9+
"strconv"
10+
"strings"
11+
"time"
12+
)
13+
14+
// GrepOptions contains optional arguments for grep search over repository files.
15+
//
16+
// Docs: https://git-scm.com/docs/git-grep
17+
type GrepOptions struct {
18+
// The tree to run the search. Defaults to "HEAD".
19+
Tree string
20+
// Limits the search to files in the specified pathspec.
21+
Pathspec string
22+
// Whether to do case insensitive search.
23+
IgnoreCase bool
24+
// Whether to match the pattern only at word boundaries.
25+
WordRegexp bool
26+
// Whether use extended regular expressions.
27+
ExtendedRegexp bool
28+
// The timeout duration before giving up for each shell command execution. The
29+
// default timeout duration will be used when not supplied.
30+
Timeout time.Duration
31+
// The additional options to be passed to the underlying git.
32+
CommandOptions
33+
}
34+
35+
// GrepResult represents a single result from a grep search.
36+
type GrepResult struct {
37+
// The tree of the file that matched, e.g. "HEAD".
38+
Tree string
39+
// The path of the file that matched.
40+
Path string
41+
// The line number of the match.
42+
Line int
43+
// The 1-indexed column number of the match.
44+
Column int
45+
// The text of the line that matched.
46+
Text string
47+
}
48+
49+
func parseGrepLine(line string) (*GrepResult, error) {
50+
r := &GrepResult{}
51+
sp := strings.SplitN(line, ":", 5)
52+
var n int
53+
switch len(sp) {
54+
case 4:
55+
// HEAD
56+
r.Tree = "HEAD"
57+
case 5:
58+
// Tree included
59+
r.Tree = sp[0]
60+
n++
61+
default:
62+
return nil, fmt.Errorf("invalid grep line: %s", line)
63+
}
64+
r.Path = sp[n]
65+
n++
66+
r.Line, _ = strconv.Atoi(sp[n])
67+
n++
68+
r.Column, _ = strconv.Atoi(sp[n])
69+
n++
70+
r.Text = sp[n]
71+
return r, nil
72+
}
73+
74+
// Grep returns the results of a grep search in the repository.
75+
func (r *Repository) Grep(pattern string, opts ...GrepOptions) []*GrepResult {
76+
var opt GrepOptions
77+
if len(opts) > 0 {
78+
opt = opts[0]
79+
}
80+
if opt.Tree == "" {
81+
opt.Tree = "HEAD"
82+
}
83+
84+
cmd := NewCommand("grep").
85+
AddOptions(opt.CommandOptions).
86+
// Display full-name, line number and column number
87+
AddArgs("--full-name", "--line-number", "--column")
88+
if opt.IgnoreCase {
89+
cmd.AddArgs("--ignore-case")
90+
}
91+
if opt.WordRegexp {
92+
cmd.AddArgs("--word-regexp")
93+
}
94+
if opt.ExtendedRegexp {
95+
cmd.AddArgs("--extended-regexp")
96+
}
97+
cmd.AddArgs(pattern, opt.Tree)
98+
if opt.Pathspec != "" {
99+
cmd.AddArgs("--", opt.Pathspec)
100+
}
101+
102+
stdout, err := cmd.RunInDirWithTimeout(opt.Timeout, r.path)
103+
if err != nil {
104+
return nil
105+
}
106+
107+
var results []*GrepResult
108+
// Normalize line endings
109+
lines := strings.Split(strings.ReplaceAll(string(stdout), "\r", ""), "\n")
110+
for _, line := range lines {
111+
if len(line) == 0 {
112+
continue
113+
}
114+
r, err := parseGrepLine(line)
115+
if err == nil {
116+
results = append(results, r)
117+
}
118+
}
119+
return results
120+
}

repo_grep_test.go

+144
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
// Copyright 2022 The Gogs Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package git
6+
7+
import (
8+
"runtime"
9+
"testing"
10+
11+
"github.com/stretchr/testify/assert"
12+
)
13+
14+
func TestRepository_Grep_Simple(t *testing.T) {
15+
want := []*GrepResult{
16+
{
17+
Tree: "HEAD",
18+
Path: "src/Main.groovy",
19+
Line: 7,
20+
Column: 5,
21+
Text: "int programmingPoints = 10",
22+
}, {
23+
Tree: "HEAD",
24+
Path: "src/Main.groovy",
25+
Line: 10,
26+
Column: 33,
27+
Text: `println "${name} has at least ${programmingPoints} programming points."`,
28+
}, {
29+
Tree: "HEAD",
30+
Path: "src/Main.groovy",
31+
Line: 11,
32+
Column: 12,
33+
Text: `println "${programmingPoints} squared is ${square(programmingPoints)}"`,
34+
}, {
35+
Tree: "HEAD",
36+
Path: "src/Main.groovy",
37+
Line: 12,
38+
Column: 12,
39+
Text: `println "${programmingPoints} divided by 2 bonus points is ${divide(programmingPoints, 2)}"`,
40+
}, {
41+
Tree: "HEAD",
42+
Path: "src/Main.groovy",
43+
Line: 13,
44+
Column: 12,
45+
Text: `println "${programmingPoints} minus 7 bonus points is ${subtract(programmingPoints, 7)}"`,
46+
}, {
47+
Tree: "HEAD",
48+
Path: "src/Main.groovy",
49+
Line: 14,
50+
Column: 12,
51+
Text: `println "${programmingPoints} plus 3 bonus points is ${sum(programmingPoints, 3)}"`,
52+
},
53+
}
54+
got := testrepo.Grep("programmingPoints")
55+
assert.Equal(t, want, got)
56+
}
57+
58+
func TestRepository_Grep_IgnoreCase(t *testing.T) {
59+
want := []*GrepResult{
60+
{
61+
Tree: "HEAD",
62+
Path: "README.txt",
63+
Line: 9,
64+
Column: 36,
65+
Text: "* [email protected]:matthewmccullough/hellogitworld.git",
66+
}, {
67+
Tree: "HEAD",
68+
Path: "README.txt",
69+
Line: 10,
70+
Column: 38,
71+
Text: "* git://github.com/matthewmccullough/hellogitworld.git",
72+
}, {
73+
Tree: "HEAD",
74+
Path: "README.txt",
75+
Line: 11,
76+
Column: 58,
77+
Text: "* https://[email protected]/matthewmccullough/hellogitworld.git",
78+
}, {
79+
Tree: "HEAD",
80+
Path: "src/Main.groovy",
81+
Line: 9,
82+
Column: 10,
83+
Text: `println "Hello ${name}"`,
84+
}, {
85+
Tree: "HEAD",
86+
Path: "src/main/java/com/github/App.java",
87+
Line: 4,
88+
Column: 4,
89+
Text: " * Hello again",
90+
}, {
91+
Tree: "HEAD",
92+
Path: "src/main/java/com/github/App.java",
93+
Line: 5,
94+
Column: 4,
95+
Text: " * Hello world!",
96+
}, {
97+
Tree: "HEAD",
98+
Path: "src/main/java/com/github/App.java",
99+
Line: 6,
100+
Column: 4,
101+
Text: " * Hello",
102+
}, {
103+
Tree: "HEAD",
104+
Path: "src/main/java/com/github/App.java",
105+
Line: 13,
106+
Column: 30,
107+
Text: ` System.out.println( "Hello World!" );`,
108+
},
109+
}
110+
got := testrepo.Grep("Hello", GrepOptions{IgnoreCase: true})
111+
assert.Equal(t, want, got)
112+
}
113+
114+
func TestRepository_Grep_ExtendedRegexp(t *testing.T) {
115+
if runtime.GOOS == "darwin" {
116+
t.Skip("Skipping testing on macOS")
117+
return
118+
}
119+
want := []*GrepResult{
120+
{
121+
Tree: "HEAD",
122+
Path: "src/main/java/com/github/App.java",
123+
Line: 13,
124+
Column: 30,
125+
Text: ` System.out.println( "Hello World!" );`,
126+
},
127+
}
128+
got := testrepo.Grep(`Hello\sW\w+`, GrepOptions{ExtendedRegexp: true})
129+
assert.Equal(t, want, got)
130+
}
131+
132+
func TestRepository_Grep_WordRegexp(t *testing.T) {
133+
want := []*GrepResult{
134+
{
135+
Tree: "HEAD",
136+
Path: "src/main/java/com/github/App.java",
137+
Line: 5,
138+
Column: 10,
139+
Text: ` * Hello world!`,
140+
},
141+
}
142+
got := testrepo.Grep("world", GrepOptions{WordRegexp: true})
143+
assert.Equal(t, want, got)
144+
}

0 commit comments

Comments
 (0)