Skip to content

Commit

Permalink
git: worktree, add Grep() method for git grep (src-d#686)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
darkowlzz authored and mcuadros committed Dec 12, 2017
1 parent afdd28d commit 757a260
Show file tree
Hide file tree
Showing 4 changed files with 324 additions and 1 deletion.
2 changes: 1 addition & 1 deletion COMPATIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ is supported by go-git.
| **debugging** |
| bisect ||
| blame ||
| grep | |
| grep | |
| **email** ||
| am ||
| apply ||
Expand Down
38 changes: 38 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package git

import (
"errors"
"regexp"

"gopkg.in/src-d/go-git.v4/config"
"gopkg.in/src-d/go-git.v4/plumbing"
Expand Down Expand Up @@ -365,3 +366,40 @@ type ListOptions struct {
type CleanOptions struct {
Dir bool
}

// GrepOptions describes how a grep should be performed.
type GrepOptions struct {
// Pattern is a compiled Regexp object to be matched.
Pattern *regexp.Regexp
// InvertMatch selects non-matching lines.
InvertMatch bool
// CommitHash is the hash of the commit from which worktree should be derived.
CommitHash plumbing.Hash
// ReferenceName is the branch or tag name from which worktree should be derived.
ReferenceName plumbing.ReferenceName
// PathSpec is a compiled Regexp object of pathspec to use in the matching.
PathSpec *regexp.Regexp
}

var (
ErrHashOrReference = errors.New("ambiguous options, only one of CommitHash or ReferenceName can be passed")
)

// Validate validates the fields and sets the default values.
func (o *GrepOptions) Validate(w *Worktree) error {
if !o.CommitHash.IsZero() && o.ReferenceName != "" {
return ErrHashOrReference
}

// If none of CommitHash and ReferenceName are provided, set commit hash of
// the repository's head.
if o.CommitHash.IsZero() && o.ReferenceName == "" {
ref, err := w.r.Head()
if err != nil {
return err
}
o.CommitHash = ref.Hash()
}

return nil
}
100 changes: 100 additions & 0 deletions worktree.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
stdioutil "io/ioutil"
"os"
"path/filepath"
"strings"

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

// GrepResult is structure of a grep result.
type GrepResult struct {
// FileName is the name of file which contains match.
FileName string
// LineNumber is the line number of a file at which a match was found.
LineNumber int
// Content is the content of the file at the matching line.
Content string
// TreeName is the name of the tree (reference name/commit hash) at
// which the match was performed.
TreeName string
}

func (gr GrepResult) String() string {
return fmt.Sprintf("%s:%s:%d:%s", gr.TreeName, gr.FileName, gr.LineNumber, gr.Content)
}

// Grep performs grep on a worktree.
func (w *Worktree) Grep(opts *GrepOptions) ([]GrepResult, error) {
if err := opts.Validate(w); err != nil {
return nil, err
}

// Obtain commit hash from options (CommitHash or ReferenceName).
var commitHash plumbing.Hash
// treeName contains the value of TreeName in GrepResult.
var treeName string

if opts.ReferenceName != "" {
ref, err := w.r.Reference(opts.ReferenceName, true)
if err != nil {
return nil, err
}
commitHash = ref.Hash()
treeName = opts.ReferenceName.String()
} else if !opts.CommitHash.IsZero() {
commitHash = opts.CommitHash
treeName = opts.CommitHash.String()
}

// Obtain a tree from the commit hash and get a tracked files iterator from
// the tree.
tree, err := w.getTreeFromCommitHash(commitHash)
if err != nil {
return nil, err
}
fileiter := tree.Files()

return findMatchInFiles(fileiter, treeName, opts)
}

// findMatchInFiles takes a FileIter, worktree name and GrepOptions, and
// returns a slice of GrepResult containing the result of regex pattern matching
// in the file content.
func findMatchInFiles(fileiter *object.FileIter, treeName string, opts *GrepOptions) ([]GrepResult, error) {
var results []GrepResult

// Iterate through the files and look for any matches.
err := fileiter.ForEach(func(file *object.File) error {
// Check if the file name matches with the pathspec.
if opts.PathSpec != nil && !opts.PathSpec.MatchString(file.Name) {
return nil
}

content, err := file.Contents()
if err != nil {
return err
}

// Split the content and make parseable line-by-line.
contentByLine := strings.Split(content, "\n")
for lineNum, cnt := range contentByLine {
addToResult := false
// Match the pattern and content.
if opts.Pattern != nil && opts.Pattern.MatchString(cnt) {
// Add to result only if invert match is not enabled.
if !opts.InvertMatch {
addToResult = true
}
} else if opts.InvertMatch {
// If matching fails, and invert match is enabled, add to results.
addToResult = true
}

if addToResult {
results = append(results, GrepResult{
FileName: file.Name,
LineNumber: lineNum + 1,
Content: cnt,
TreeName: treeName,
})
}
}
return nil
})

return results, err
}

func rmFileAndDirIfEmpty(fs billy.Filesystem, name string) error {
if err := util.RemoveAll(fs, name); err != nil {
return err
Expand Down
185 changes: 185 additions & 0 deletions worktree_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"io/ioutil"
"os"
"path/filepath"
"regexp"
"runtime"

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

c.Assert(commit1.String(), Equals, commit2.String())
}

func (s *WorktreeSuite) TestGrep(c *C) {
cases := []struct {
name string
options GrepOptions
wantResult []GrepResult
dontWantResult []GrepResult
wantError error
}{
{
name: "basic word match",
options: GrepOptions{
Pattern: regexp.MustCompile("import"),
},
wantResult: []GrepResult{
{
FileName: "go/example.go",
LineNumber: 3,
Content: "import (",
TreeName: "6ecf0ef2c2dffb796033e5a02219af86ec6584e5",
},
{
FileName: "vendor/foo.go",
LineNumber: 3,
Content: "import \"fmt\"",
TreeName: "6ecf0ef2c2dffb796033e5a02219af86ec6584e5",
},
},
}, {
name: "case insensitive match",
options: GrepOptions{
Pattern: regexp.MustCompile(`(?i)IMport`),
},
wantResult: []GrepResult{
{
FileName: "go/example.go",
LineNumber: 3,
Content: "import (",
TreeName: "6ecf0ef2c2dffb796033e5a02219af86ec6584e5",
},
{
FileName: "vendor/foo.go",
LineNumber: 3,
Content: "import \"fmt\"",
TreeName: "6ecf0ef2c2dffb796033e5a02219af86ec6584e5",
},
},
}, {
name: "invert match",
options: GrepOptions{
Pattern: regexp.MustCompile("import"),
InvertMatch: true,
},
dontWantResult: []GrepResult{
{
FileName: "go/example.go",
LineNumber: 3,
Content: "import (",
TreeName: "6ecf0ef2c2dffb796033e5a02219af86ec6584e5",
},
{
FileName: "vendor/foo.go",
LineNumber: 3,
Content: "import \"fmt\"",
TreeName: "6ecf0ef2c2dffb796033e5a02219af86ec6584e5",
},
},
}, {
name: "match at a given commit hash",
options: GrepOptions{
Pattern: regexp.MustCompile("The MIT License"),
CommitHash: plumbing.NewHash("b029517f6300c2da0f4b651b8642506cd6aaf45d"),
},
wantResult: []GrepResult{
{
FileName: "LICENSE",
LineNumber: 1,
Content: "The MIT License (MIT)",
TreeName: "b029517f6300c2da0f4b651b8642506cd6aaf45d",
},
},
dontWantResult: []GrepResult{
{
FileName: "go/example.go",
LineNumber: 3,
Content: "import (",
TreeName: "6ecf0ef2c2dffb796033e5a02219af86ec6584e5",
},
},
}, {
name: "match for a given pathspec",
options: GrepOptions{
Pattern: regexp.MustCompile("import"),
PathSpec: regexp.MustCompile("go/"),
},
wantResult: []GrepResult{
{
FileName: "go/example.go",
LineNumber: 3,
Content: "import (",
TreeName: "6ecf0ef2c2dffb796033e5a02219af86ec6584e5",
},
},
dontWantResult: []GrepResult{
{
FileName: "vendor/foo.go",
LineNumber: 3,
Content: "import \"fmt\"",
TreeName: "6ecf0ef2c2dffb796033e5a02219af86ec6584e5",
},
},
}, {
name: "match at a given reference name",
options: GrepOptions{
Pattern: regexp.MustCompile("import"),
ReferenceName: "refs/heads/master",
},
wantResult: []GrepResult{
{
FileName: "go/example.go",
LineNumber: 3,
Content: "import (",
TreeName: "refs/heads/master",
},
},
}, {
name: "ambiguous options",
options: GrepOptions{
Pattern: regexp.MustCompile("import"),
CommitHash: plumbing.NewHash("2d55a722f3c3ecc36da919dfd8b6de38352f3507"),
ReferenceName: "somereferencename",
},
wantError: ErrHashOrReference,
},
}

path := fixtures.Basic().ByTag("worktree").One().Worktree().Root()
server, err := PlainClone(c.MkDir(), false, &CloneOptions{
URL: path,
})
c.Assert(err, IsNil)

w, err := server.Worktree()
c.Assert(err, IsNil)

for _, tc := range cases {
gr, err := w.Grep(&tc.options)
if tc.wantError != nil {
c.Assert(err, Equals, tc.wantError)
} else {
c.Assert(err, IsNil)
}

// Iterate through the results and check if the wanted result is present
// in the got result.
for _, wantResult := range tc.wantResult {
found := false
for _, gotResult := range gr {
if wantResult == gotResult {
found = true
break
}
}
if found != true {
c.Errorf("unexpected grep results for %q, expected result to contain: %v", tc.name, wantResult)
}
}

// Iterate through the results and check if the not wanted result is
// present in the got result.
for _, dontWantResult := range tc.dontWantResult {
found := false
for _, gotResult := range gr {
if dontWantResult == gotResult {
found = true
break
}
}
if found != false {
c.Errorf("unexpected grep results for %q, expected result to NOT contain: %v", tc.name, dontWantResult)
}
}
}
}

0 comments on commit 757a260

Please sign in to comment.