aboutsummaryrefslogtreecommitdiffstats
path: root/modules
diff options
context:
space:
mode:
authorwxiaoguang <wxiaoguang@gmail.com>2024-03-25 00:05:00 +0800
committerGitHub <noreply@github.com>2024-03-24 17:05:00 +0100
commit4734d43e1422da04f9ff79ea0212f7e9472b55a1 (patch)
treed6ba422cc7b7f942d817a83836da426edd007ec4 /modules
parent90a4f9a49eecc4b672df0c29f5034be25244191c (diff)
downloadgitea-4734d43e1422da04f9ff79ea0212f7e9472b55a1.tar.gz
gitea-4734d43e1422da04f9ff79ea0212f7e9472b55a1.zip
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>
Diffstat (limited to 'modules')
-rw-r--r--modules/git/command.go5
-rw-r--r--modules/git/git.go8
-rw-r--r--modules/git/grep.go112
-rw-r--r--modules/git/grep_test.go41
-rw-r--r--modules/indexer/code/search.go35
5 files changed, 177 insertions, 24 deletions
diff --git a/modules/git/command.go b/modules/git/command.go
index 371109730a..22cb275ab2 100644
--- a/modules/git/command.go
+++ b/modules/git/command.go
@@ -367,7 +367,6 @@ type RunStdError interface {
error
Unwrap() error
Stderr() string
- IsExitCode(code int) bool
}
type runStdError struct {
@@ -392,9 +391,9 @@ func (r *runStdError) Stderr() string {
return r.stderr
}
-func (r *runStdError) IsExitCode(code int) bool {
+func IsErrorExitCode(err error, code int) bool {
var exitError *exec.ExitError
- if errors.As(r.err, &exitError) {
+ if errors.As(err, &exitError) {
return exitError.ExitCode() == code
}
return false
diff --git a/modules/git/git.go b/modules/git/git.go
index f688ea7488..e411269f7c 100644
--- a/modules/git/git.go
+++ b/modules/git/git.go
@@ -341,7 +341,7 @@ func checkGitVersionCompatibility(gitVer *version.Version) error {
func configSet(key, value string) error {
stdout, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key).RunStdString(nil)
- if err != nil && !err.IsExitCode(1) {
+ if err != nil && !IsErrorExitCode(err, 1) {
return fmt.Errorf("failed to get git config %s, err: %w", key, err)
}
@@ -364,7 +364,7 @@ func configSetNonExist(key, value string) error {
// already exist
return nil
}
- if err.IsExitCode(1) {
+ if IsErrorExitCode(err, 1) {
// not exist, set new config
_, _, err = NewCommand(DefaultContext, "config", "--global").AddDynamicArguments(key, value).RunStdString(nil)
if err != nil {
@@ -382,7 +382,7 @@ func configAddNonExist(key, value string) error {
// already exist
return nil
}
- if err.IsExitCode(1) {
+ if IsErrorExitCode(err, 1) {
// not exist, add new config
_, _, err = NewCommand(DefaultContext, "config", "--global", "--add").AddDynamicArguments(key, value).RunStdString(nil)
if err != nil {
@@ -403,7 +403,7 @@ func configUnsetAll(key, value string) error {
}
return nil
}
- if err.IsExitCode(1) {
+ if IsErrorExitCode(err, 1) {
// not exist
return nil
}
diff --git a/modules/git/grep.go b/modules/git/grep.go
new file mode 100644
index 0000000000..e533995984
--- /dev/null
+++ b/modules/git/grep.go
@@ -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
+}
diff --git a/modules/git/grep_test.go b/modules/git/grep_test.go
new file mode 100644
index 0000000000..3993fa7ffc
--- /dev/null
+++ b/modules/git/grep_test.go
@@ -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)
+}
diff --git a/modules/indexer/code/search.go b/modules/indexer/code/search.go
index 51c7595cf8..5f35e8073b 100644
--- a/modules/indexer/code/search.go
+++ b/modules/indexer/code/search.go
@@ -70,13 +70,27 @@ func writeStrings(buf *bytes.Buffer, strs ...string) error {
return nil
}
+func HighlightSearchResultCode(filename string, lineNums []int, code string) []ResultLine {
+ // we should highlight the whole code block first, otherwise it doesn't work well with multiple line highlighting
+ hl, _ := highlight.Code(filename, "", code)
+ highlightedLines := strings.Split(string(hl), "\n")
+
+ // The lineNums outputted by highlight.Code might not match the original lineNums, because "highlight" removes the last `\n`
+ lines := make([]ResultLine, min(len(highlightedLines), len(lineNums)))
+ for i := 0; i < len(lines); i++ {
+ lines[i].Num = lineNums[i]
+ lines[i].FormattedContent = template.HTML(highlightedLines[i])
+ }
+ return lines
+}
+
func searchResult(result *internal.SearchResult, startIndex, endIndex int) (*Result, error) {
startLineNum := 1 + strings.Count(result.Content[:startIndex], "\n")
var formattedLinesBuffer bytes.Buffer
contentLines := strings.SplitAfter(result.Content[startIndex:endIndex], "\n")
- lines := make([]ResultLine, 0, len(contentLines))
+ lineNums := make([]int, 0, len(contentLines))
index := startIndex
for i, line := range contentLines {
var err error
@@ -91,29 +105,16 @@ func searchResult(result *internal.SearchResult, startIndex, endIndex int) (*Res
line[closeActiveIndex:],
)
} else {
- err = writeStrings(&formattedLinesBuffer,
- line,
- )
+ err = writeStrings(&formattedLinesBuffer, line)
}
if err != nil {
return nil, err
}
- lines = append(lines, ResultLine{Num: startLineNum + i})
+ lineNums = append(lineNums, startLineNum+i)
index += len(line)
}
- // we should highlight the whole code block first, otherwise it doesn't work well with multiple line highlighting
- hl, _ := highlight.Code(result.Filename, "", formattedLinesBuffer.String())
- highlightedLines := strings.Split(string(hl), "\n")
-
- // The lines outputted by highlight.Code might not match the original lines, because "highlight" removes the last `\n`
- lines = lines[:min(len(highlightedLines), len(lines))]
- highlightedLines = highlightedLines[:len(lines)]
- for i := 0; i < len(lines); i++ {
- lines[i].FormattedContent = template.HTML(highlightedLines[i])
- }
-
return &Result{
RepoID: result.RepoID,
Filename: result.Filename,
@@ -121,7 +122,7 @@ func searchResult(result *internal.SearchResult, startIndex, endIndex int) (*Res
UpdatedUnix: result.UpdatedUnix,
Language: result.Language,
Color: result.Color,
- Lines: lines,
+ Lines: HighlightSearchResultCode(result.Filename, lineNums, formattedLinesBuffer.String()),
}, nil
}