gitea源码

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