mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 10:22:13 +00:00 
			
		
		
		
	enable mirror, usestdlibbars and perfsprint part of: https://github.com/go-gitea/gitea/issues/34083 --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
		
			
				
	
	
		
			150 lines
		
	
	
		
			3.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			150 lines
		
	
	
		
			3.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright 2024 The Gitea Authors. All rights reserved.
 | |
| // SPDX-License-Identifier: MIT
 | |
| 
 | |
| package git
 | |
| 
 | |
| import (
 | |
| 	"bufio"
 | |
| 	"bytes"
 | |
| 	"context"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"os"
 | |
| 	"slices"
 | |
| 	"strconv"
 | |
| 	"strings"
 | |
| 
 | |
| 	"code.gitea.io/gitea/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
 | |
| }
 | |
| 
 | |
| 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("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)
 | |
| 	stderr := bytes.Buffer{}
 | |
| 	err = cmd.Run(ctx, &RunOpts{
 | |
| 		Dir:    repo.Path,
 | |
| 		Stdout: stdoutWriter,
 | |
| 		Stderr: &stderr,
 | |
| 		PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error {
 | |
| 			_ = stdoutWriter.Close()
 | |
| 			defer stdoutReader.Close()
 | |
| 
 | |
| 			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 {
 | |
| 						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 nil
 | |
| 		},
 | |
| 	})
 | |
| 	// git grep exits by cancel (killed), usually it is caused by the limit of results
 | |
| 	if IsErrorExitCode(err, -1) && stderr.Len() == 0 {
 | |
| 		return results, nil
 | |
| 	}
 | |
| 	// 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
 | |
| }
 |