mirror of
https://github.com/go-gitea/gitea.git
synced 2026-07-22 06:15:18 +02:00
before: gitrepo vs git packages after: git package fully handle all git operations by the way, use `WithRepo(repo)` instead of `WithDir(repo.Path)` to hide path details. benefits: 1. remove all unnecessary wrappers, developers no need to struggle with "which package should be used" 2. simplify code, RepositoryFacade can (will) be used everywhere, all "path" details are (will be) hidden
141 lines
3.8 KiB
Go
141 lines
3.8 KiB
Go
// Copyright 2024 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package git
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"gitea.dev/modules/git/gitcmd"
|
|
"gitea.dev/modules/util"
|
|
)
|
|
|
|
type GrepResult struct {
|
|
Filename string
|
|
LineNumbers []int
|
|
LineCodes []string
|
|
}
|
|
|
|
type GrepModeType string
|
|
|
|
const (
|
|
GrepModeExact GrepModeType = "exact"
|
|
GrepModeWords GrepModeType = "words"
|
|
GrepModeRegexp GrepModeType = "regexp"
|
|
)
|
|
|
|
type GrepOptions struct {
|
|
RefName string
|
|
MaxResultLimit int
|
|
ContextLineNumber int
|
|
GrepMode GrepModeType
|
|
MaxLineLength int // the maximum length of a line to parse, exceeding chars will be truncated
|
|
PathspecList []string
|
|
}
|
|
|
|
// grepSearchTimeout is the timeout for git grep search, it should be long enough to get results
|
|
// but not too long to cause performance issues
|
|
const grepSearchTimeout = 30 * time.Second
|
|
|
|
func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepOptions) ([]*GrepResult, error) {
|
|
/*
|
|
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 := gitcmd.NewCommand("grep", "--null", "--break", "--heading", "--line-number", "--full-name")
|
|
cmd.AddOptionValues("--context", strconv.Itoa(opts.ContextLineNumber))
|
|
switch opts.GrepMode {
|
|
case GrepModeExact:
|
|
cmd.AddArguments("--fixed-strings")
|
|
cmd.AddOptionValues("-e", strings.TrimLeft(search, "-"))
|
|
case GrepModeRegexp:
|
|
cmd.AddArguments("--perl-regexp")
|
|
cmd.AddOptionValues("-e", strings.TrimLeft(search, "-"))
|
|
default: /* words */
|
|
words := strings.Fields(search)
|
|
cmd.AddArguments("--fixed-strings", "--ignore-case")
|
|
for i, word := range words {
|
|
cmd.AddOptionValues("-e", strings.TrimLeft(word, "-"))
|
|
if i < len(words)-1 {
|
|
cmd.AddOptionValues("--and")
|
|
}
|
|
}
|
|
}
|
|
cmd.AddDynamicArguments(util.IfZero(opts.RefName, "HEAD"))
|
|
cmd.AddDashesAndList(opts.PathspecList...)
|
|
opts.MaxResultLimit = util.IfZero(opts.MaxResultLimit, 50)
|
|
|
|
stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe()
|
|
defer stdoutReaderClose()
|
|
err := cmd.WithRepo(repo).
|
|
WithTimeout(grepSearchTimeout).
|
|
WithPipelineFunc(func(ctx gitcmd.Context) error {
|
|
isInBlock := false
|
|
rd := bufio.NewReaderSize(stdoutReader, util.IfZero(opts.MaxLineLength, 16*1024))
|
|
var res *GrepResult
|
|
for {
|
|
lineBytes, isPrefix, err := rd.ReadLine()
|
|
if isPrefix {
|
|
lineBytes = slices.Clone(lineBytes)
|
|
for isPrefix && err == nil {
|
|
_, isPrefix, err = rd.ReadLine()
|
|
}
|
|
}
|
|
if len(lineBytes) == 0 && err != nil {
|
|
break
|
|
}
|
|
line := string(lineBytes) // the memory of lineBytes is mutable
|
|
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) >= opts.MaxResultLimit {
|
|
return ctx.CancelPipeline(nil)
|
|
}
|
|
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 nil
|
|
}).
|
|
RunWithStderr(ctx)
|
|
// git grep exits by cancel (killed), usually it is caused by the limit of results
|
|
if gitcmd.IsErrorExitCode(err, -1) && err.Stderr() == "" {
|
|
return results, nil
|
|
}
|
|
// git grep exits with 1 if no results are found
|
|
if gitcmd.IsErrorExitCode(err, 1) && err.Stderr() == "" {
|
|
return nil, nil
|
|
}
|
|
if err != nil && !errors.Is(err, context.Canceled) {
|
|
return nil, fmt.Errorf("unable to run git grep: %w", err)
|
|
}
|
|
return results, nil
|
|
}
|