From 7dbe9c31776e6b0ab76fba55bb12e65b9179615d Mon Sep 17 00:00:00 2001 From: Sunny Date: Sat, 9 Dec 2017 18:04:24 +0530 Subject: [PATCH 1/4] git: worktree, add Grep() method for git grep Signed-off-by: Sunny --- options.go | 39 ++++++++++++++++++ worktree.go | 114 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 153 insertions(+) diff --git a/options.go b/options.go index e5745ea04..adea62353 100644 --- a/options.go +++ b/options.go @@ -365,3 +365,42 @@ type ListOptions struct { type CleanOptions struct { Dir bool } + +// GrepOptions describes how a grep should be performed. +type GrepOptions struct { + // Pattern is the match pattern. + Pattern string + // IgnoreCase enables case insensitive match. + IgnoreCase bool + // 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 the pathspec to use for matching. + PathSpec string +} + +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 e87f5676a..33ec4a700 100644 --- a/worktree.go +++ b/worktree.go @@ -8,6 +8,8 @@ import ( stdioutil "io/ioutil" "os" "path/filepath" + "regexp" + "strings" "gopkg.in/src-d/go-git.v4/config" "gopkg.in/src-d/go-git.v4/plumbing" @@ -711,6 +713,118 @@ 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() + } + + var results []GrepResult + + // 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() + + // Create a Regexp object using the provided Pattern. + reg, err := regexp.Compile(opts.Pattern) + if err != nil { + return nil, err + } + + // Create a case insensitive Regexp object if IgnoreCase is enabled. + if opts.IgnoreCase { + reg, err = regexp.Compile("(?i)" + opts.Pattern) + if err != nil { + return nil, err + } + } + + // Create a Regexp object using the PathSpec. + regPathSpec, err := regexp.Compile(opts.PathSpec) + if err != nil { + return nil, err + } + + // 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 !regPathSpec.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 reg.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 From 3ae86a622731e0c63cf0f4b070033c651c233e0b Mon Sep 17 00:00:00 2001 From: Sunny Date: Sat, 9 Dec 2017 19:29:08 +0530 Subject: [PATCH 2/4] test: worktree.Grep() Signed-off-by: Sunny --- worktree_test.go | 191 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) diff --git a/worktree_test.go b/worktree_test.go index 4f24cff22..320c82cff 100644 --- a/worktree_test.go +++ b/worktree_test.go @@ -1317,3 +1317,194 @@ 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: "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: "ignore case match", + options: GrepOptions{ + Pattern: "IMport", + IgnoreCase: true, + }, + 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: "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: "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: "import", + PathSpec: "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: "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: "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) + } + } + } +} From e4735d3235a1db0a85809926d6bc9d171c19d05d Mon Sep 17 00:00:00 2001 From: Sunny Date: Mon, 11 Dec 2017 22:30:29 +0530 Subject: [PATCH 3/4] git: worktree.Grep(), accept regexp in options Signed-off-by: Sunny --- options.go | 11 +++++------ worktree.go | 34 ++++++++++------------------------ worktree_test.go | 38 ++++++++++++++++---------------------- 3 files changed, 31 insertions(+), 52 deletions(-) diff --git a/options.go b/options.go index adea62353..d0898db44 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" @@ -368,18 +369,16 @@ type CleanOptions struct { // GrepOptions describes how a grep should be performed. type GrepOptions struct { - // Pattern is the match pattern. - Pattern string - // IgnoreCase enables case insensitive match. - IgnoreCase bool + // 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 the pathspec to use for matching. - PathSpec string + // PathSpec is a compiled Regexp object of pathspec to use in the matching. + PathSpec *regexp.Regexp } var ( diff --git a/worktree.go b/worktree.go index 33ec4a700..2c35ffb5b 100644 --- a/worktree.go +++ b/worktree.go @@ -8,7 +8,6 @@ import ( stdioutil "io/ioutil" "os" "path/filepath" - "regexp" "strings" "gopkg.in/src-d/go-git.v4/config" @@ -753,8 +752,6 @@ func (w *Worktree) Grep(opts *GrepOptions) ([]GrepResult, error) { treeName = opts.CommitHash.String() } - var results []GrepResult - // Obtain a tree from the commit hash and get a tracked files iterator from // the tree. tree, err := w.getTreeFromCommitHash(commitHash) @@ -763,30 +760,19 @@ func (w *Worktree) Grep(opts *GrepOptions) ([]GrepResult, error) { } fileiter := tree.Files() - // Create a Regexp object using the provided Pattern. - reg, err := regexp.Compile(opts.Pattern) - if err != nil { - return nil, err - } - - // Create a case insensitive Regexp object if IgnoreCase is enabled. - if opts.IgnoreCase { - reg, err = regexp.Compile("(?i)" + opts.Pattern) - if err != nil { - return nil, err - } - } + return findMatchInFiles(fileiter, treeName, opts) +} - // Create a Regexp object using the PathSpec. - regPathSpec, err := regexp.Compile(opts.PathSpec) - if err != nil { - return nil, err - } +// 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 { + err := fileiter.ForEach(func(file *object.File) error { // Check if the file name matches with the pathspec. - if !regPathSpec.MatchString(file.Name) { + if opts.PathSpec != nil && !opts.PathSpec.MatchString(file.Name) { return nil } @@ -800,7 +786,7 @@ func (w *Worktree) Grep(opts *GrepOptions) ([]GrepResult, error) { for lineNum, cnt := range contentByLine { addToResult := false // Match the pattern and content. - if reg.MatchString(cnt) { + if opts.Pattern != nil && opts.Pattern.MatchString(cnt) { // Add to result only if invert match is not enabled. if !opts.InvertMatch { addToResult = true diff --git a/worktree_test.go b/worktree_test.go index 320c82cff..36e3a08d3 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" @@ -1329,7 +1330,7 @@ func (s *WorktreeSuite) TestGrep(c *C) { { name: "basic word match", options: GrepOptions{ - Pattern: "import", + Pattern: regexp.MustCompile("import"), }, wantResult: []GrepResult{ { @@ -1345,12 +1346,10 @@ func (s *WorktreeSuite) TestGrep(c *C) { TreeName: "6ecf0ef2c2dffb796033e5a02219af86ec6584e5", }, }, - }, - { - name: "ignore case match", + }, { + name: "case insensitive match", options: GrepOptions{ - Pattern: "IMport", - IgnoreCase: true, + Pattern: regexp.MustCompile(`(?i)IMport`), }, wantResult: []GrepResult{ { @@ -1366,11 +1365,10 @@ func (s *WorktreeSuite) TestGrep(c *C) { TreeName: "6ecf0ef2c2dffb796033e5a02219af86ec6584e5", }, }, - }, - { + }, { name: "invert match", options: GrepOptions{ - Pattern: "import", + Pattern: regexp.MustCompile("import"), InvertMatch: true, }, dontWantResult: []GrepResult{ @@ -1387,11 +1385,10 @@ func (s *WorktreeSuite) TestGrep(c *C) { TreeName: "6ecf0ef2c2dffb796033e5a02219af86ec6584e5", }, }, - }, - { + }, { name: "match at a given commit hash", options: GrepOptions{ - Pattern: "The MIT License", + Pattern: regexp.MustCompile("The MIT License"), CommitHash: plumbing.NewHash("b029517f6300c2da0f4b651b8642506cd6aaf45d"), }, wantResult: []GrepResult{ @@ -1410,12 +1407,11 @@ func (s *WorktreeSuite) TestGrep(c *C) { TreeName: "6ecf0ef2c2dffb796033e5a02219af86ec6584e5", }, }, - }, - { + }, { name: "match for a given pathspec", options: GrepOptions{ - Pattern: "import", - PathSpec: "go/", + Pattern: regexp.MustCompile("import"), + PathSpec: regexp.MustCompile("go/"), }, wantResult: []GrepResult{ { @@ -1433,11 +1429,10 @@ func (s *WorktreeSuite) TestGrep(c *C) { TreeName: "6ecf0ef2c2dffb796033e5a02219af86ec6584e5", }, }, - }, - { + }, { name: "match at a given reference name", options: GrepOptions{ - Pattern: "import", + Pattern: regexp.MustCompile("import"), ReferenceName: "refs/heads/master", }, wantResult: []GrepResult{ @@ -1448,11 +1443,10 @@ func (s *WorktreeSuite) TestGrep(c *C) { TreeName: "refs/heads/master", }, }, - }, - { + }, { name: "ambiguous options", options: GrepOptions{ - Pattern: "import", + Pattern: regexp.MustCompile("import"), CommitHash: plumbing.NewHash("2d55a722f3c3ecc36da919dfd8b6de38352f3507"), ReferenceName: "somereferencename", }, From fb4bbee23ed8d031566ef0d05c9671382a77206a Mon Sep 17 00:00:00 2001 From: Sunny Date: Mon, 11 Dec 2017 22:31:35 +0530 Subject: [PATCH 4/4] doc: update compatibility for grep Signed-off-by: Sunny --- COMPATIBILITY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 5a7f0f058..e07e79915 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -45,7 +45,7 @@ is supported by go-git. | **debugging** | | bisect | ✖ | | blame | ✔ | -| grep | ✖ | +| grep | ✔ | | **email** || | am | ✖ | | apply | ✖ |