From 8eac762c2ab7633bfa72b7c5d083594c6722dd07 Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Sat, 11 Feb 2023 09:10:48 -0500 Subject: [PATCH] feat(grep): search a repository (#86) Co-authored-by: Joe Chen --- git_test.go | 3 +- repo_diff.go | 4 +- repo_grep.go | 120 ++++++++++++++++++++++++++++++++++++++ repo_grep_test.go | 144 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 267 insertions(+), 4 deletions(-) create mode 100644 repo_grep.go create mode 100644 repo_grep_test.go diff --git a/git_test.go b/git_test.go index f6fcb3650..13adc2943 100644 --- a/git_test.go +++ b/git_test.go @@ -22,10 +22,9 @@ const repoPath = "testdata/testrepo.git" var testrepo *Repository func TestMain(m *testing.M) { - verbose := flag.Bool("verbose", false, "") flag.Parse() - if *verbose { + if testing.Verbose() { SetOutput(os.Stdout) } diff --git a/repo_diff.go b/repo_diff.go index a7550b938..430bf94a4 100644 --- a/repo_diff.go +++ b/repo_diff.go @@ -126,7 +126,7 @@ func (r *Repository) RawDiff(rev string, diffType RawDiffFormat, w io.Writer, op if commit.ParentsCount() == 0 { cmd = cmd.AddArgs("format-patch"). AddOptions(opt.CommandOptions). - AddArgs("--full-index", "--no-signature", "--stdout", "--root", rev) + AddArgs("--full-index", "--no-signoff", "--no-signature", "--stdout", "--root", rev) } else { c, err := commit.Parent(0) if err != nil { @@ -134,7 +134,7 @@ func (r *Repository) RawDiff(rev string, diffType RawDiffFormat, w io.Writer, op } cmd = cmd.AddArgs("format-patch"). AddOptions(opt.CommandOptions). - AddArgs("--full-index", "--no-signature", "--stdout", rev+"..."+c.ID.String()) + AddArgs("--full-index", "--no-signoff", "--no-signature", "--stdout", rev+"..."+c.ID.String()) } default: return fmt.Errorf("invalid diffType: %s", diffType) diff --git a/repo_grep.go b/repo_grep.go new file mode 100644 index 000000000..e09191495 --- /dev/null +++ b/repo_grep.go @@ -0,0 +1,120 @@ +// Copyright 2022 The Gogs Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package git + +import ( + "fmt" + "strconv" + "strings" + "time" +) + +// GrepOptions contains optional arguments for grep search over repository files. +// +// Docs: https://git-scm.com/docs/git-grep +type GrepOptions struct { + // The tree to run the search. Defaults to "HEAD". + Tree string + // Limits the search to files in the specified pathspec. + Pathspec string + // Whether to do case insensitive search. + IgnoreCase bool + // Whether to match the pattern only at word boundaries. + WordRegexp bool + // Whether use extended regular expressions. + ExtendedRegexp bool + // The timeout duration before giving up for each shell command execution. The + // default timeout duration will be used when not supplied. + Timeout time.Duration + // The additional options to be passed to the underlying git. + CommandOptions +} + +// GrepResult represents a single result from a grep search. +type GrepResult struct { + // The tree of the file that matched, e.g. "HEAD". + Tree string + // The path of the file that matched. + Path string + // The line number of the match. + Line int + // The 1-indexed column number of the match. + Column int + // The text of the line that matched. + Text string +} + +func parseGrepLine(line string) (*GrepResult, error) { + r := &GrepResult{} + sp := strings.SplitN(line, ":", 5) + var n int + switch len(sp) { + case 4: + // HEAD + r.Tree = "HEAD" + case 5: + // Tree included + r.Tree = sp[0] + n++ + default: + return nil, fmt.Errorf("invalid grep line: %s", line) + } + r.Path = sp[n] + n++ + r.Line, _ = strconv.Atoi(sp[n]) + n++ + r.Column, _ = strconv.Atoi(sp[n]) + n++ + r.Text = sp[n] + return r, nil +} + +// Grep returns the results of a grep search in the repository. +func (r *Repository) Grep(pattern string, opts ...GrepOptions) []*GrepResult { + var opt GrepOptions + if len(opts) > 0 { + opt = opts[0] + } + if opt.Tree == "" { + opt.Tree = "HEAD" + } + + cmd := NewCommand("grep"). + AddOptions(opt.CommandOptions). + // Display full-name, line number and column number + AddArgs("--full-name", "--line-number", "--column") + if opt.IgnoreCase { + cmd.AddArgs("--ignore-case") + } + if opt.WordRegexp { + cmd.AddArgs("--word-regexp") + } + if opt.ExtendedRegexp { + cmd.AddArgs("--extended-regexp") + } + cmd.AddArgs(pattern, opt.Tree) + if opt.Pathspec != "" { + cmd.AddArgs("--", opt.Pathspec) + } + + stdout, err := cmd.RunInDirWithTimeout(opt.Timeout, r.path) + if err != nil { + return nil + } + + var results []*GrepResult + // Normalize line endings + lines := strings.Split(strings.ReplaceAll(string(stdout), "\r", ""), "\n") + for _, line := range lines { + if len(line) == 0 { + continue + } + r, err := parseGrepLine(line) + if err == nil { + results = append(results, r) + } + } + return results +} diff --git a/repo_grep_test.go b/repo_grep_test.go new file mode 100644 index 000000000..2c0be3906 --- /dev/null +++ b/repo_grep_test.go @@ -0,0 +1,144 @@ +// Copyright 2022 The Gogs Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package git + +import ( + "runtime" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRepository_Grep_Simple(t *testing.T) { + want := []*GrepResult{ + { + Tree: "HEAD", + Path: "src/Main.groovy", + Line: 7, + Column: 5, + Text: "int programmingPoints = 10", + }, { + Tree: "HEAD", + Path: "src/Main.groovy", + Line: 10, + Column: 33, + Text: `println "${name} has at least ${programmingPoints} programming points."`, + }, { + Tree: "HEAD", + Path: "src/Main.groovy", + Line: 11, + Column: 12, + Text: `println "${programmingPoints} squared is ${square(programmingPoints)}"`, + }, { + Tree: "HEAD", + Path: "src/Main.groovy", + Line: 12, + Column: 12, + Text: `println "${programmingPoints} divided by 2 bonus points is ${divide(programmingPoints, 2)}"`, + }, { + Tree: "HEAD", + Path: "src/Main.groovy", + Line: 13, + Column: 12, + Text: `println "${programmingPoints} minus 7 bonus points is ${subtract(programmingPoints, 7)}"`, + }, { + Tree: "HEAD", + Path: "src/Main.groovy", + Line: 14, + Column: 12, + Text: `println "${programmingPoints} plus 3 bonus points is ${sum(programmingPoints, 3)}"`, + }, + } + got := testrepo.Grep("programmingPoints") + assert.Equal(t, want, got) +} + +func TestRepository_Grep_IgnoreCase(t *testing.T) { + want := []*GrepResult{ + { + Tree: "HEAD", + Path: "README.txt", + Line: 9, + Column: 36, + Text: "* git@github.com:matthewmccullough/hellogitworld.git", + }, { + Tree: "HEAD", + Path: "README.txt", + Line: 10, + Column: 38, + Text: "* git://github.com/matthewmccullough/hellogitworld.git", + }, { + Tree: "HEAD", + Path: "README.txt", + Line: 11, + Column: 58, + Text: "* https://matthewmccullough@github.com/matthewmccullough/hellogitworld.git", + }, { + Tree: "HEAD", + Path: "src/Main.groovy", + Line: 9, + Column: 10, + Text: `println "Hello ${name}"`, + }, { + Tree: "HEAD", + Path: "src/main/java/com/github/App.java", + Line: 4, + Column: 4, + Text: " * Hello again", + }, { + Tree: "HEAD", + Path: "src/main/java/com/github/App.java", + Line: 5, + Column: 4, + Text: " * Hello world!", + }, { + Tree: "HEAD", + Path: "src/main/java/com/github/App.java", + Line: 6, + Column: 4, + Text: " * Hello", + }, { + Tree: "HEAD", + Path: "src/main/java/com/github/App.java", + Line: 13, + Column: 30, + Text: ` System.out.println( "Hello World!" );`, + }, + } + got := testrepo.Grep("Hello", GrepOptions{IgnoreCase: true}) + assert.Equal(t, want, got) +} + +func TestRepository_Grep_ExtendedRegexp(t *testing.T) { + if runtime.GOOS == "darwin" { + t.Skip("Skipping testing on macOS") + return + } + want := []*GrepResult{ + { + Tree: "HEAD", + Path: "src/main/java/com/github/App.java", + Line: 13, + Column: 30, + Text: ` System.out.println( "Hello World!" );`, + }, + } + got := testrepo.Grep(`Hello\sW\w+`, GrepOptions{ExtendedRegexp: true}) + assert.Equal(t, want, got) +} + +func TestRepository_Grep_WordRegexp(t *testing.T) { + want := []*GrepResult{ + { + Tree: "HEAD", + Path: "src/main/java/com/github/App.java", + Line: 5, + Column: 10, + Text: ` * Hello world!`, + }, + } + got := testrepo.Grep("world", GrepOptions{WordRegexp: true}) + assert.Equal(t, want, got) +}