選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

blame.go 5.1KB

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