mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 02:46:04 +01:00 
			
		
		
		
	Each "indexer" should provide the "search modes" they support by themselves. And we need to remove the "fuzzy" search for code.
		
			
				
	
	
		
			149 lines
		
	
	
		
			3.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			149 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", fmt.Sprint(opts.ContextLineNumber))
 | |
| 	if opts.GrepMode == GrepModeExact {
 | |
| 		cmd.AddArguments("--fixed-strings")
 | |
| 		cmd.AddOptionValues("-e", strings.TrimLeft(search, "-"))
 | |
| 	} else if opts.GrepMode == GrepModeRegexp {
 | |
| 		cmd.AddArguments("--perl-regexp")
 | |
| 		cmd.AddOptionValues("-e", strings.TrimLeft(search, "-"))
 | |
| 	} else /* 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
 | |
| }
 |