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  }