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

git: worktree, add Grep() method for git grep #686

Merged
merged 4 commits into from
Dec 12, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}
}
}
}