mirror of https://github.com/go-gitea/gitea.git
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