aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSunny <me@darkowlzz.space>2017-12-12 13:56:08 +0530
committerMáximo Cuadros <mcuadros@gmail.com>2017-12-12 09:26:08 +0100
commit757a26038e5404f94523ba07d017d1b38bcbf6dd (patch)
tree7dae086e094b3af83e608155b9304c85941af740
parentafdd28d3cfe11c280723c4f5f33845fd415350d6 (diff)
downloadgo-git-757a26038e5404f94523ba07d017d1b38bcbf6dd.tar.gz
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.
-rw-r--r--COMPATIBILITY.md2
-rw-r--r--options.go38
-rw-r--r--worktree.go100
-rw-r--r--worktree_test.go185
4 files changed, 324 insertions, 1 deletions
diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md
index 5a7f0f0..e07e799 100644
--- a/COMPATIBILITY.md
+++ b/COMPATIBILITY.md
@@ -45,7 +45,7 @@ is supported by go-git.
| **debugging** |
| bisect | ✖ |
| blame | ✔ |
-| grep | ✖ |
+| grep | ✔ |
| **email** ||
| am | ✖ |
| apply | ✖ |
diff --git a/options.go b/options.go
index e5745ea..d0898db 100644
--- a/options.go
+++ b/options.go
@@ -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"
@@ -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
+}
diff --git a/worktree.go b/worktree.go
index e87f567..2c35ffb 100644
--- a/worktree.go
+++ b/worktree.go
@@ -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"
@@ -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
diff --git a/worktree_test.go b/worktree_test.go
index 4f24cff..36e3a08 100644
--- a/worktree_test.go
+++ b/worktree_test.go
@@ -6,6 +6,7 @@ import (
"io/ioutil"
"os"
"path/filepath"
+ "regexp"
"runtime"
"gopkg.in/src-d/go-git.v4/config"
@@ -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)
+ }
+ }
+ }
+}