code.gitea.io/gitea@v1.22.3/modules/git/grep.go (about) 1 // Copyright 2024 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package git 5 6 import ( 7 "bufio" 8 "bytes" 9 "context" 10 "errors" 11 "fmt" 12 "os" 13 "slices" 14 "strconv" 15 "strings" 16 17 "code.gitea.io/gitea/modules/util" 18 ) 19 20 type GrepResult struct { 21 Filename string 22 LineNumbers []int 23 LineCodes []string 24 } 25 26 type GrepOptions struct { 27 RefName string 28 MaxResultLimit int 29 ContextLineNumber int 30 IsFuzzy bool 31 MaxLineLength int // the maximum length of a line to parse, exceeding chars will be truncated 32 PathspecList []string 33 } 34 35 func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepOptions) ([]*GrepResult, error) { 36 stdoutReader, stdoutWriter, err := os.Pipe() 37 if err != nil { 38 return nil, fmt.Errorf("unable to create os pipe to grep: %w", err) 39 } 40 defer func() { 41 _ = stdoutReader.Close() 42 _ = stdoutWriter.Close() 43 }() 44 45 /* 46 The output is like this ( "^@" means \x00): 47 48 HEAD:.air.toml 49 6^@bin = "gitea" 50 51 HEAD:.changelog.yml 52 2^@repo: go-gitea/gitea 53 */ 54 var results []*GrepResult 55 cmd := NewCommand(ctx, "grep", "--null", "--break", "--heading", "--fixed-strings", "--line-number", "--ignore-case", "--full-name") 56 cmd.AddOptionValues("--context", fmt.Sprint(opts.ContextLineNumber)) 57 if opts.IsFuzzy { 58 words := strings.Fields(search) 59 for _, word := range words { 60 cmd.AddOptionValues("-e", strings.TrimLeft(word, "-")) 61 } 62 } else { 63 cmd.AddOptionValues("-e", strings.TrimLeft(search, "-")) 64 } 65 cmd.AddDynamicArguments(util.IfZero(opts.RefName, "HEAD")) 66 cmd.AddDashesAndList(opts.PathspecList...) 67 opts.MaxResultLimit = util.IfZero(opts.MaxResultLimit, 50) 68 stderr := bytes.Buffer{} 69 err = cmd.Run(&RunOpts{ 70 Dir: repo.Path, 71 Stdout: stdoutWriter, 72 Stderr: &stderr, 73 PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error { 74 _ = stdoutWriter.Close() 75 defer stdoutReader.Close() 76 77 isInBlock := false 78 rd := bufio.NewReaderSize(stdoutReader, util.IfZero(opts.MaxLineLength, 16*1024)) 79 var res *GrepResult 80 for { 81 lineBytes, isPrefix, err := rd.ReadLine() 82 if isPrefix { 83 lineBytes = slices.Clone(lineBytes) 84 for isPrefix && err == nil { 85 _, isPrefix, err = rd.ReadLine() 86 } 87 } 88 if len(lineBytes) == 0 && err != nil { 89 break 90 } 91 line := string(lineBytes) // the memory of lineBytes is mutable 92 if !isInBlock { 93 if _ /* ref */, filename, ok := strings.Cut(line, ":"); ok { 94 isInBlock = true 95 res = &GrepResult{Filename: filename} 96 results = append(results, res) 97 } 98 continue 99 } 100 if line == "" { 101 if len(results) >= opts.MaxResultLimit { 102 cancel() 103 break 104 } 105 isInBlock = false 106 continue 107 } 108 if line == "--" { 109 continue 110 } 111 if lineNum, lineCode, ok := strings.Cut(line, "\x00"); ok { 112 lineNumInt, _ := strconv.Atoi(lineNum) 113 res.LineNumbers = append(res.LineNumbers, lineNumInt) 114 res.LineCodes = append(res.LineCodes, lineCode) 115 } 116 } 117 return nil 118 }, 119 }) 120 // git grep exits by cancel (killed), usually it is caused by the limit of results 121 if IsErrorExitCode(err, -1) && stderr.Len() == 0 { 122 return results, nil 123 } 124 // git grep exits with 1 if no results are found 125 if IsErrorExitCode(err, 1) && stderr.Len() == 0 { 126 return nil, nil 127 } 128 if err != nil && !errors.Is(err, context.Canceled) { 129 return nil, fmt.Errorf("unable to run git grep: %w, stderr: %s", err, stderr.String()) 130 } 131 return results, nil 132 }