Skip to content

Commit

Permalink
feat(grep): search a repository (#86)
Browse files Browse the repository at this point in the history
Co-authored-by: Joe Chen <[email protected]>
  • Loading branch information
aymanbagabas and unknwon authored Feb 11, 2023
1 parent 03d62cb commit 8eac762
Show file tree
Hide file tree
Showing 4 changed files with 267 additions and 4 deletions.
3 changes: 1 addition & 2 deletions git_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
4 changes: 2 additions & 2 deletions repo_diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,15 +126,15 @@ 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 {
return err
}
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)
Expand Down
120 changes: 120 additions & 0 deletions repo_grep.go
Original file line number Diff line number Diff line change
@@ -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
}
144 changes: 144 additions & 0 deletions repo_grep_test.go
Original file line number Diff line number Diff line change
@@ -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: "* [email protected]: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://[email protected]/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)
}

0 comments on commit 8eac762

Please sign in to comment.