Skip to content

Commit 91d432b

Browse files
committed
feat(grep): search a repository
Use `git grep` to search a repository.
1 parent 1041030 commit 91d432b

File tree

2 files changed

+216
-0
lines changed

2 files changed

+216
-0
lines changed

repo_grep.go

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

repo_grep_test.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package git
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func TestRepoGrepSimple(t *testing.T) {
10+
pattern := "programmingPoints"
11+
expect := []GrepResult{
12+
{
13+
TreeID: "HEAD", Path: "src/Main.groovy", Line: 7, Column: 5, Text: "int programmingPoints = 10",
14+
},
15+
{
16+
TreeID: "HEAD", Path: "src/Main.groovy", Line: 10, Column: 33, Text: `println "${name} has at least ${programmingPoints} programming points."`,
17+
},
18+
{
19+
TreeID: "HEAD", Path: "src/Main.groovy", Line: 11, Column: 12, Text: `println "${programmingPoints} squared is ${square(programmingPoints)}"`,
20+
},
21+
{
22+
TreeID: "HEAD", Path: "src/Main.groovy", Line: 12, Column: 12, Text: `println "${programmingPoints} divided by 2 bonus points is ${divide(programmingPoints, 2)}"`,
23+
},
24+
{
25+
TreeID: "HEAD", Path: "src/Main.groovy", Line: 13, Column: 12, Text: `println "${programmingPoints} minus 7 bonus points is ${subtract(programmingPoints, 7)}"`,
26+
},
27+
{
28+
TreeID: "HEAD", Path: "src/Main.groovy", Line: 14, Column: 12, Text: `println "${programmingPoints} plus 3 bonus points is ${sum(programmingPoints, 3)}"`,
29+
},
30+
}
31+
results, err := testrepo.Grep(pattern)
32+
assert.NoError(t, err)
33+
for i, result := range results {
34+
assert.Equal(t, expect[i], *result)
35+
}
36+
}
37+
38+
func TestRepoGrepIgnoreCase(t *testing.T) {
39+
pattern := "Hello"
40+
expect := []GrepResult{
41+
{
42+
TreeID: "HEAD", Path: "README.txt", Line: 9, Column: 36, Text: "* [email protected]:matthewmccullough/hellogitworld.git",
43+
},
44+
{
45+
TreeID: "HEAD", Path: "README.txt", Line: 10, Column: 38, Text: "* git://github.com/matthewmccullough/hellogitworld.git",
46+
},
47+
{
48+
TreeID: "HEAD", Path: "README.txt", Line: 11, Column: 58, Text: "* https://[email protected]/matthewmccullough/hellogitworld.git",
49+
},
50+
{
51+
TreeID: "HEAD", Path: "src/Main.groovy", Line: 9, Column: 10, Text: `println "Hello ${name}"`,
52+
},
53+
{
54+
TreeID: "HEAD", Path: "src/main/java/com/github/App.java", Line: 4, Column: 4, Text: " * Hello again",
55+
},
56+
{
57+
TreeID: "HEAD", Path: "src/main/java/com/github/App.java", Line: 5, Column: 4, Text: " * Hello world!",
58+
},
59+
{
60+
TreeID: "HEAD", Path: "src/main/java/com/github/App.java", Line: 6, Column: 4, Text: " * Hello",
61+
},
62+
{
63+
TreeID: "HEAD", Path: "src/main/java/com/github/App.java", Line: 13, Column: 30, Text: ` System.out.println( "Hello World!" );`,
64+
},
65+
}
66+
results, err := testrepo.Grep(pattern, GrepOptions{IgnoreCase: true})
67+
assert.NoError(t, err)
68+
for i, result := range results {
69+
assert.Equal(t, expect[i], *result)
70+
}
71+
}
72+
73+
func TestRepoGrepRegex(t *testing.T) {
74+
pattern := "Hello\\sW\\w+"
75+
expect := []GrepResult{
76+
{
77+
TreeID: "HEAD", Path: "src/main/java/com/github/App.java", Line: 13, Column: 30, Text: ` System.out.println( "Hello World!" );`,
78+
},
79+
}
80+
results, err := testrepo.Grep(pattern, GrepOptions{ExtendedRegex: true})
81+
assert.NoError(t, err)
82+
for i, result := range results {
83+
assert.Equal(t, expect[i], *result)
84+
}
85+
}
86+
87+
func TestRepoGrepWord(t *testing.T) {
88+
pattern := "Hello\\sW\\w+"
89+
expect := []GrepResult{
90+
{
91+
TreeID: "HEAD", Path: "src/main/java/com/github/App.java", Line: 13, Column: 36, Text: ` System.out.println( "Hello World!" );`,
92+
},
93+
}
94+
results, err := testrepo.Grep(pattern, GrepOptions{WordMatch: true})
95+
assert.NoError(t, err)
96+
for i, result := range results {
97+
assert.Equal(t, expect[i], *result)
98+
}
99+
}

0 commit comments

Comments
 (0)