Skip to content
This repository was archived by the owner on Sep 11, 2020. It is now read-only.

Commit 757a260

Browse files
darkowlzzmcuadros
authored andcommitted
git: worktree, add Grep() method for git grep (#686)
This change implemented grep on worktree with options to invert match and specify pathspec. Also, a commit hash or reference can be used to specify the worktree to search.
1 parent afdd28d commit 757a260

File tree

4 files changed

+324
-1
lines changed

4 files changed

+324
-1
lines changed

COMPATIBILITY.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ is supported by go-git.
4545
| **debugging** |
4646
| bisect ||
4747
| blame ||
48-
| grep | |
48+
| grep | |
4949
| **email** ||
5050
| am ||
5151
| apply ||

options.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package git
22

33
import (
44
"errors"
5+
"regexp"
56

67
"gopkg.in/src-d/go-git.v4/config"
78
"gopkg.in/src-d/go-git.v4/plumbing"
@@ -365,3 +366,40 @@ type ListOptions struct {
365366
type CleanOptions struct {
366367
Dir bool
367368
}
369+
370+
// GrepOptions describes how a grep should be performed.
371+
type GrepOptions struct {
372+
// Pattern is a compiled Regexp object to be matched.
373+
Pattern *regexp.Regexp
374+
// InvertMatch selects non-matching lines.
375+
InvertMatch bool
376+
// CommitHash is the hash of the commit from which worktree should be derived.
377+
CommitHash plumbing.Hash
378+
// ReferenceName is the branch or tag name from which worktree should be derived.
379+
ReferenceName plumbing.ReferenceName
380+
// PathSpec is a compiled Regexp object of pathspec to use in the matching.
381+
PathSpec *regexp.Regexp
382+
}
383+
384+
var (
385+
ErrHashOrReference = errors.New("ambiguous options, only one of CommitHash or ReferenceName can be passed")
386+
)
387+
388+
// Validate validates the fields and sets the default values.
389+
func (o *GrepOptions) Validate(w *Worktree) error {
390+
if !o.CommitHash.IsZero() && o.ReferenceName != "" {
391+
return ErrHashOrReference
392+
}
393+
394+
// If none of CommitHash and ReferenceName are provided, set commit hash of
395+
// the repository's head.
396+
if o.CommitHash.IsZero() && o.ReferenceName == "" {
397+
ref, err := w.r.Head()
398+
if err != nil {
399+
return err
400+
}
401+
o.CommitHash = ref.Hash()
402+
}
403+
404+
return nil
405+
}

worktree.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
stdioutil "io/ioutil"
99
"os"
1010
"path/filepath"
11+
"strings"
1112

1213
"gopkg.in/src-d/go-git.v4/config"
1314
"gopkg.in/src-d/go-git.v4/plumbing"
@@ -711,6 +712,105 @@ func (w *Worktree) Clean(opts *CleanOptions) error {
711712
return nil
712713
}
713714

715+
// GrepResult is structure of a grep result.
716+
type GrepResult struct {
717+
// FileName is the name of file which contains match.
718+
FileName string
719+
// LineNumber is the line number of a file at which a match was found.
720+
LineNumber int
721+
// Content is the content of the file at the matching line.
722+
Content string
723+
// TreeName is the name of the tree (reference name/commit hash) at
724+
// which the match was performed.
725+
TreeName string
726+
}
727+
728+
func (gr GrepResult) String() string {
729+
return fmt.Sprintf("%s:%s:%d:%s", gr.TreeName, gr.FileName, gr.LineNumber, gr.Content)
730+
}
731+
732+
// Grep performs grep on a worktree.
733+
func (w *Worktree) Grep(opts *GrepOptions) ([]GrepResult, error) {
734+
if err := opts.Validate(w); err != nil {
735+
return nil, err
736+
}
737+
738+
// Obtain commit hash from options (CommitHash or ReferenceName).
739+
var commitHash plumbing.Hash
740+
// treeName contains the value of TreeName in GrepResult.
741+
var treeName string
742+
743+
if opts.ReferenceName != "" {
744+
ref, err := w.r.Reference(opts.ReferenceName, true)
745+
if err != nil {
746+
return nil, err
747+
}
748+
commitHash = ref.Hash()
749+
treeName = opts.ReferenceName.String()
750+
} else if !opts.CommitHash.IsZero() {
751+
commitHash = opts.CommitHash
752+
treeName = opts.CommitHash.String()
753+
}
754+
755+
// Obtain a tree from the commit hash and get a tracked files iterator from
756+
// the tree.
757+
tree, err := w.getTreeFromCommitHash(commitHash)
758+
if err != nil {
759+
return nil, err
760+
}
761+
fileiter := tree.Files()
762+
763+
return findMatchInFiles(fileiter, treeName, opts)
764+
}
765+
766+
// findMatchInFiles takes a FileIter, worktree name and GrepOptions, and
767+
// returns a slice of GrepResult containing the result of regex pattern matching
768+
// in the file content.
769+
func findMatchInFiles(fileiter *object.FileIter, treeName string, opts *GrepOptions) ([]GrepResult, error) {
770+
var results []GrepResult
771+
772+
// Iterate through the files and look for any matches.
773+
err := fileiter.ForEach(func(file *object.File) error {
774+
// Check if the file name matches with the pathspec.
775+
if opts.PathSpec != nil && !opts.PathSpec.MatchString(file.Name) {
776+
return nil
777+
}
778+
779+
content, err := file.Contents()
780+
if err != nil {
781+
return err
782+
}
783+
784+
// Split the content and make parseable line-by-line.
785+
contentByLine := strings.Split(content, "\n")
786+
for lineNum, cnt := range contentByLine {
787+
addToResult := false
788+
// Match the pattern and content.
789+
if opts.Pattern != nil && opts.Pattern.MatchString(cnt) {
790+
// Add to result only if invert match is not enabled.
791+
if !opts.InvertMatch {
792+
addToResult = true
793+
}
794+
} else if opts.InvertMatch {
795+
// If matching fails, and invert match is enabled, add to results.
796+
addToResult = true
797+
}
798+
799+
if addToResult {
800+
results = append(results, GrepResult{
801+
FileName: file.Name,
802+
LineNumber: lineNum + 1,
803+
Content: cnt,
804+
TreeName: treeName,
805+
})
806+
}
807+
}
808+
return nil
809+
})
810+
811+
return results, err
812+
}
813+
714814
func rmFileAndDirIfEmpty(fs billy.Filesystem, name string) error {
715815
if err := util.RemoveAll(fs, name); err != nil {
716816
return err

worktree_test.go

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"io/ioutil"
77
"os"
88
"path/filepath"
9+
"regexp"
910
"runtime"
1011

1112
"gopkg.in/src-d/go-git.v4/config"
@@ -1317,3 +1318,187 @@ func (s *WorktreeSuite) TestAlternatesRepo(c *C) {
13171318

13181319
c.Assert(commit1.String(), Equals, commit2.String())
13191320
}
1321+
1322+
func (s *WorktreeSuite) TestGrep(c *C) {
1323+
cases := []struct {
1324+
name string
1325+
options GrepOptions
1326+
wantResult []GrepResult
1327+
dontWantResult []GrepResult
1328+
wantError error
1329+
}{
1330+
{
1331+
name: "basic word match",
1332+
options: GrepOptions{
1333+
Pattern: regexp.MustCompile("import"),
1334+
},
1335+
wantResult: []GrepResult{
1336+
{
1337+
FileName: "go/example.go",
1338+
LineNumber: 3,
1339+
Content: "import (",
1340+
TreeName: "6ecf0ef2c2dffb796033e5a02219af86ec6584e5",
1341+
},
1342+
{
1343+
FileName: "vendor/foo.go",
1344+
LineNumber: 3,
1345+
Content: "import \"fmt\"",
1346+
TreeName: "6ecf0ef2c2dffb796033e5a02219af86ec6584e5",
1347+
},
1348+
},
1349+
}, {
1350+
name: "case insensitive match",
1351+
options: GrepOptions{
1352+
Pattern: regexp.MustCompile(`(?i)IMport`),
1353+
},
1354+
wantResult: []GrepResult{
1355+
{
1356+
FileName: "go/example.go",
1357+
LineNumber: 3,
1358+
Content: "import (",
1359+
TreeName: "6ecf0ef2c2dffb796033e5a02219af86ec6584e5",
1360+
},
1361+
{
1362+
FileName: "vendor/foo.go",
1363+
LineNumber: 3,
1364+
Content: "import \"fmt\"",
1365+
TreeName: "6ecf0ef2c2dffb796033e5a02219af86ec6584e5",
1366+
},
1367+
},
1368+
}, {
1369+
name: "invert match",
1370+
options: GrepOptions{
1371+
Pattern: regexp.MustCompile("import"),
1372+
InvertMatch: true,
1373+
},
1374+
dontWantResult: []GrepResult{
1375+
{
1376+
FileName: "go/example.go",
1377+
LineNumber: 3,
1378+
Content: "import (",
1379+
TreeName: "6ecf0ef2c2dffb796033e5a02219af86ec6584e5",
1380+
},
1381+
{
1382+
FileName: "vendor/foo.go",
1383+
LineNumber: 3,
1384+
Content: "import \"fmt\"",
1385+
TreeName: "6ecf0ef2c2dffb796033e5a02219af86ec6584e5",
1386+
},
1387+
},
1388+
}, {
1389+
name: "match at a given commit hash",
1390+
options: GrepOptions{
1391+
Pattern: regexp.MustCompile("The MIT License"),
1392+
CommitHash: plumbing.NewHash("b029517f6300c2da0f4b651b8642506cd6aaf45d"),
1393+
},
1394+
wantResult: []GrepResult{
1395+
{
1396+
FileName: "LICENSE",
1397+
LineNumber: 1,
1398+
Content: "The MIT License (MIT)",
1399+
TreeName: "b029517f6300c2da0f4b651b8642506cd6aaf45d",
1400+
},
1401+
},
1402+
dontWantResult: []GrepResult{
1403+
{
1404+
FileName: "go/example.go",
1405+
LineNumber: 3,
1406+
Content: "import (",
1407+
TreeName: "6ecf0ef2c2dffb796033e5a02219af86ec6584e5",
1408+
},
1409+
},
1410+
}, {
1411+
name: "match for a given pathspec",
1412+
options: GrepOptions{
1413+
Pattern: regexp.MustCompile("import"),
1414+
PathSpec: regexp.MustCompile("go/"),
1415+
},
1416+
wantResult: []GrepResult{
1417+
{
1418+
FileName: "go/example.go",
1419+
LineNumber: 3,
1420+
Content: "import (",
1421+
TreeName: "6ecf0ef2c2dffb796033e5a02219af86ec6584e5",
1422+
},
1423+
},
1424+
dontWantResult: []GrepResult{
1425+
{
1426+
FileName: "vendor/foo.go",
1427+
LineNumber: 3,
1428+
Content: "import \"fmt\"",
1429+
TreeName: "6ecf0ef2c2dffb796033e5a02219af86ec6584e5",
1430+
},
1431+
},
1432+
}, {
1433+
name: "match at a given reference name",
1434+
options: GrepOptions{
1435+
Pattern: regexp.MustCompile("import"),
1436+
ReferenceName: "refs/heads/master",
1437+
},
1438+
wantResult: []GrepResult{
1439+
{
1440+
FileName: "go/example.go",
1441+
LineNumber: 3,
1442+
Content: "import (",
1443+
TreeName: "refs/heads/master",
1444+
},
1445+
},
1446+
}, {
1447+
name: "ambiguous options",
1448+
options: GrepOptions{
1449+
Pattern: regexp.MustCompile("import"),
1450+
CommitHash: plumbing.NewHash("2d55a722f3c3ecc36da919dfd8b6de38352f3507"),
1451+
ReferenceName: "somereferencename",
1452+
},
1453+
wantError: ErrHashOrReference,
1454+
},
1455+
}
1456+
1457+
path := fixtures.Basic().ByTag("worktree").One().Worktree().Root()
1458+
server, err := PlainClone(c.MkDir(), false, &CloneOptions{
1459+
URL: path,
1460+
})
1461+
c.Assert(err, IsNil)
1462+
1463+
w, err := server.Worktree()
1464+
c.Assert(err, IsNil)
1465+
1466+
for _, tc := range cases {
1467+
gr, err := w.Grep(&tc.options)
1468+
if tc.wantError != nil {
1469+
c.Assert(err, Equals, tc.wantError)
1470+
} else {
1471+
c.Assert(err, IsNil)
1472+
}
1473+
1474+
// Iterate through the results and check if the wanted result is present
1475+
// in the got result.
1476+
for _, wantResult := range tc.wantResult {
1477+
found := false
1478+
for _, gotResult := range gr {
1479+
if wantResult == gotResult {
1480+
found = true
1481+
break
1482+
}
1483+
}
1484+
if found != true {
1485+
c.Errorf("unexpected grep results for %q, expected result to contain: %v", tc.name, wantResult)
1486+
}
1487+
}
1488+
1489+
// Iterate through the results and check if the not wanted result is
1490+
// present in the got result.
1491+
for _, dontWantResult := range tc.dontWantResult {
1492+
found := false
1493+
for _, gotResult := range gr {
1494+
if dontWantResult == gotResult {
1495+
found = true
1496+
break
1497+
}
1498+
}
1499+
if found != false {
1500+
c.Errorf("unexpected grep results for %q, expected result to NOT contain: %v", tc.name, dontWantResult)
1501+
}
1502+
}
1503+
}
1504+
}

0 commit comments

Comments
 (0)