From 757a26038e5404f94523ba07d017d1b38bcbf6dd Mon Sep 17 00:00:00 2001 From: Sunny Date: Tue, 12 Dec 2017 13:56:08 +0530 Subject: 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. --- COMPATIBILITY.md | 2 +- options.go | 38 ++++++++++++ worktree.go | 100 ++++++++++++++++++++++++++++++ worktree_test.go | 185 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 324 insertions(+), 1 deletion(-) 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) + } + } + } +} -- cgit