mirror of https://github.com/go-gitea/gitea
Support repo code search without setting up an indexer (#29998)
By using git's ability, end users (especially small instance users) do not need to enable the indexer, they could also benefit from the code searching feature. Fix #29996 ![image](https://github.com/go-gitea/gitea/assets/2114189/11b7e458-88a4-480d-b4d7-72ee59406dd1) ![image](https://github.com/go-gitea/gitea/assets/2114189/0fe777d5-c95c-4288-a818-0427680805b6) --------- Co-authored-by: silverwind <me@silverwind.io>pull/30043/head^2
parent
90a4f9a49e
commit
4734d43e14
@ -0,0 +1,112 @@ |
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package git |
||||
|
||||
import ( |
||||
"bufio" |
||||
"bytes" |
||||
"context" |
||||
"errors" |
||||
"fmt" |
||||
"os" |
||||
"strconv" |
||||
"strings" |
||||
|
||||
"code.gitea.io/gitea/modules/util" |
||||
) |
||||
|
||||
type GrepResult struct { |
||||
Filename string |
||||
LineNumbers []int |
||||
LineCodes []string |
||||
} |
||||
|
||||
type GrepOptions struct { |
||||
RefName string |
||||
ContextLineNumber int |
||||
IsFuzzy bool |
||||
} |
||||
|
||||
func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepOptions) ([]*GrepResult, error) { |
||||
stdoutReader, stdoutWriter, err := os.Pipe() |
||||
if err != nil { |
||||
return nil, fmt.Errorf("unable to create os pipe to grep: %w", err) |
||||
} |
||||
defer func() { |
||||
_ = stdoutReader.Close() |
||||
_ = stdoutWriter.Close() |
||||
}() |
||||
|
||||
/* |
||||
The output is like this ( "^@" means \x00): |
||||
|
||||
HEAD:.air.toml |
||||
6^@bin = "gitea" |
||||
|
||||
HEAD:.changelog.yml |
||||
2^@repo: go-gitea/gitea |
||||
*/ |
||||
var results []*GrepResult |
||||
cmd := NewCommand(ctx, "grep", "--null", "--break", "--heading", "--fixed-strings", "--line-number", "--ignore-case", "--full-name") |
||||
cmd.AddOptionValues("--context", fmt.Sprint(opts.ContextLineNumber)) |
||||
if opts.IsFuzzy { |
||||
words := strings.Fields(search) |
||||
for _, word := range words { |
||||
cmd.AddOptionValues("-e", strings.TrimLeft(word, "-")) |
||||
} |
||||
} else { |
||||
cmd.AddOptionValues("-e", strings.TrimLeft(search, "-")) |
||||
} |
||||
cmd.AddDynamicArguments(util.IfZero(opts.RefName, "HEAD")) |
||||
stderr := bytes.Buffer{} |
||||
err = cmd.Run(&RunOpts{ |
||||
Dir: repo.Path, |
||||
Stdout: stdoutWriter, |
||||
Stderr: &stderr, |
||||
PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error { |
||||
_ = stdoutWriter.Close() |
||||
defer stdoutReader.Close() |
||||
|
||||
isInBlock := false |
||||
scanner := bufio.NewScanner(stdoutReader) |
||||
var res *GrepResult |
||||
for scanner.Scan() { |
||||
line := scanner.Text() |
||||
if !isInBlock { |
||||
if _ /* ref */, filename, ok := strings.Cut(line, ":"); ok { |
||||
isInBlock = true |
||||
res = &GrepResult{Filename: filename} |
||||
results = append(results, res) |
||||
} |
||||
continue |
||||
} |
||||
if line == "" { |
||||
if len(results) >= 50 { |
||||
cancel() |
||||
break |
||||
} |
||||
isInBlock = false |
||||
continue |
||||
} |
||||
if line == "--" { |
||||
continue |
||||
} |
||||
if lineNum, lineCode, ok := strings.Cut(line, "\x00"); ok { |
||||
lineNumInt, _ := strconv.Atoi(lineNum) |
||||
res.LineNumbers = append(res.LineNumbers, lineNumInt) |
||||
res.LineCodes = append(res.LineCodes, lineCode) |
||||
} |
||||
} |
||||
return scanner.Err() |
||||
}, |
||||
}) |
||||
// git grep exits with 1 if no results are found
|
||||
if IsErrorExitCode(err, 1) && stderr.Len() == 0 { |
||||
return nil, nil |
||||
} |
||||
if err != nil && !errors.Is(err, context.Canceled) { |
||||
return nil, fmt.Errorf("unable to run git grep: %w, stderr: %s", err, stderr.String()) |
||||
} |
||||
return results, nil |
||||
} |
@ -0,0 +1,41 @@ |
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package git |
||||
|
||||
import ( |
||||
"context" |
||||
"path/filepath" |
||||
"testing" |
||||
|
||||
"github.com/stretchr/testify/assert" |
||||
) |
||||
|
||||
func TestGrepSearch(t *testing.T) { |
||||
repo, err := openRepositoryWithDefaultContext(filepath.Join(testReposDir, "language_stats_repo")) |
||||
assert.NoError(t, err) |
||||
defer repo.Close() |
||||
|
||||
res, err := GrepSearch(context.Background(), repo, "void", GrepOptions{}) |
||||
assert.NoError(t, err) |
||||
assert.Equal(t, []*GrepResult{ |
||||
{ |
||||
Filename: "java-hello/main.java", |
||||
LineNumbers: []int{3}, |
||||
LineCodes: []string{" public static void main(String[] args)"}, |
||||
}, |
||||
{ |
||||
Filename: "main.vendor.java", |
||||
LineNumbers: []int{3}, |
||||
LineCodes: []string{" public static void main(String[] args)"}, |
||||
}, |
||||
}, res) |
||||
|
||||
res, err = GrepSearch(context.Background(), repo, "no-such-content", GrepOptions{}) |
||||
assert.NoError(t, err) |
||||
assert.Len(t, res, 0) |
||||
|
||||
res, err = GrepSearch(context.Background(), &Repository{Path: "no-such-git-repo"}, "no-such-content", GrepOptions{}) |
||||
assert.Error(t, err) |
||||
assert.Len(t, res, 0) |
||||
} |
Loading…
Reference in new issue