You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

blame.go 4.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. // Copyright 2019 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package git
  4. import (
  5. "bufio"
  6. "bytes"
  7. "context"
  8. "fmt"
  9. "io"
  10. "os"
  11. "regexp"
  12. "strings"
  13. "code.gitea.io/gitea/modules/log"
  14. "code.gitea.io/gitea/modules/util"
  15. )
  16. // BlamePart represents block of blame - continuous lines with one sha
  17. type BlamePart struct {
  18. Sha string
  19. Lines []string
  20. PreviousSha string
  21. PreviousPath string
  22. }
  23. // BlameReader returns part of file blame one by one
  24. type BlameReader struct {
  25. output io.WriteCloser
  26. reader io.ReadCloser
  27. bufferedReader *bufio.Reader
  28. done chan error
  29. lastSha *string
  30. ignoreRevsFile *string
  31. }
  32. func (r *BlameReader) UsesIgnoreRevs() bool {
  33. return r.ignoreRevsFile != nil
  34. }
  35. var shaLineRegex = regexp.MustCompile("^([a-z0-9]{40})")
  36. // NextPart returns next part of blame (sequential code lines with the same commit)
  37. func (r *BlameReader) NextPart() (*BlamePart, error) {
  38. var blamePart *BlamePart
  39. if r.lastSha != nil {
  40. blamePart = &BlamePart{
  41. Sha: *r.lastSha,
  42. Lines: make([]string, 0),
  43. }
  44. }
  45. var lineBytes []byte
  46. var isPrefix bool
  47. var err error
  48. for err != io.EOF {
  49. lineBytes, isPrefix, err = r.bufferedReader.ReadLine()
  50. if err != nil && err != io.EOF {
  51. return blamePart, err
  52. }
  53. if len(lineBytes) == 0 {
  54. // isPrefix will be false
  55. continue
  56. }
  57. line := string(lineBytes)
  58. lines := shaLineRegex.FindStringSubmatch(line)
  59. if lines != nil {
  60. sha1 := lines[1]
  61. if blamePart == nil {
  62. blamePart = &BlamePart{
  63. Sha: sha1,
  64. Lines: make([]string, 0),
  65. }
  66. }
  67. if blamePart.Sha != sha1 {
  68. r.lastSha = &sha1
  69. // need to munch to end of line...
  70. for isPrefix {
  71. _, isPrefix, err = r.bufferedReader.ReadLine()
  72. if err != nil && err != io.EOF {
  73. return blamePart, err
  74. }
  75. }
  76. return blamePart, nil
  77. }
  78. } else if line[0] == '\t' {
  79. blamePart.Lines = append(blamePart.Lines, line[1:])
  80. } else if strings.HasPrefix(line, "previous ") {
  81. parts := strings.SplitN(line[len("previous "):], " ", 2)
  82. blamePart.PreviousSha = parts[0]
  83. blamePart.PreviousPath = parts[1]
  84. }
  85. // need to munch to end of line...
  86. for isPrefix {
  87. _, isPrefix, err = r.bufferedReader.ReadLine()
  88. if err != nil && err != io.EOF {
  89. return blamePart, err
  90. }
  91. }
  92. }
  93. r.lastSha = nil
  94. return blamePart, nil
  95. }
  96. // Close BlameReader - don't run NextPart after invoking that
  97. func (r *BlameReader) Close() error {
  98. err := <-r.done
  99. r.bufferedReader = nil
  100. _ = r.reader.Close()
  101. _ = r.output.Close()
  102. if r.ignoreRevsFile != nil {
  103. _ = util.Remove(*r.ignoreRevsFile)
  104. }
  105. return err
  106. }
  107. // CreateBlameReader creates reader for given repository, commit and file
  108. func CreateBlameReader(ctx context.Context, repoPath string, commit *Commit, file string, bypassBlameIgnore bool) (*BlameReader, error) {
  109. var ignoreRevsFile *string
  110. if CheckGitVersionAtLeast("2.23") == nil && !bypassBlameIgnore {
  111. ignoreRevsFile = tryCreateBlameIgnoreRevsFile(commit)
  112. }
  113. cmd := NewCommandContextNoGlobals(ctx, "blame", "--porcelain")
  114. if ignoreRevsFile != nil {
  115. // Possible improvement: use --ignore-revs-file /dev/stdin on unix
  116. // There is no equivalent on Windows. May be implemented if Gitea uses an external git backend.
  117. cmd.AddOptionValues("--ignore-revs-file", *ignoreRevsFile)
  118. }
  119. cmd.AddDynamicArguments(commit.ID.String()).
  120. AddDashesAndList(file).
  121. SetDescription(fmt.Sprintf("GetBlame [repo_path: %s]", repoPath))
  122. reader, stdout, err := os.Pipe()
  123. if err != nil {
  124. if ignoreRevsFile != nil {
  125. _ = util.Remove(*ignoreRevsFile)
  126. }
  127. return nil, err
  128. }
  129. done := make(chan error, 1)
  130. go func() {
  131. stderr := bytes.Buffer{}
  132. // TODO: it doesn't work for directories (the directories shouldn't be "blamed"), and the "err" should be returned by "Read" but not by "Close"
  133. err := cmd.Run(&RunOpts{
  134. UseContextTimeout: true,
  135. Dir: repoPath,
  136. Stdout: stdout,
  137. Stderr: &stderr,
  138. })
  139. done <- err
  140. _ = stdout.Close()
  141. if err != nil {
  142. log.Error("Error running git blame (dir: %v): %v, stderr: %v", repoPath, err, stderr.String())
  143. }
  144. }()
  145. bufferedReader := bufio.NewReader(reader)
  146. return &BlameReader{
  147. output: stdout,
  148. reader: reader,
  149. bufferedReader: bufferedReader,
  150. done: done,
  151. ignoreRevsFile: ignoreRevsFile,
  152. }, nil
  153. }
  154. func tryCreateBlameIgnoreRevsFile(commit *Commit) *string {
  155. entry, err := commit.GetTreeEntryByPath(".git-blame-ignore-revs")
  156. if err != nil {
  157. return nil
  158. }
  159. r, err := entry.Blob().DataAsync()
  160. if err != nil {
  161. return nil
  162. }
  163. defer r.Close()
  164. f, err := os.CreateTemp("", "gitea_git-blame-ignore-revs")
  165. if err != nil {
  166. return nil
  167. }
  168. _, err = io.Copy(f, r)
  169. _ = f.Close()
  170. if err != nil {
  171. _ = util.Remove(f.Name())
  172. return nil
  173. }
  174. return util.ToPointer(f.Name())
  175. }