mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in NOTICE, alongside LICENSE. The header lint accepts any copyright line above the SPDX line, so it no longer demands an upstream one. Co-authored-by: Hanzo Dev <[email protected]>
141 lines
3.8 KiB
Go
141 lines
3.8 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package git
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/hanzoai/git/modules/git/gitcmd"
|
|
"github.com/hanzoai/git/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 = "forge"
|
|
|
|
HEAD:.changelog.yml
|
|
2^@repo: hanzoai/forge
|
|
*/
|
|
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.WithDir(repo.Path).
|
|
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
|
|
}
|