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 3.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. // Copyright 2019 The Gitea Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. package git
  5. import (
  6. "bufio"
  7. "context"
  8. "fmt"
  9. "io"
  10. "os"
  11. "os/exec"
  12. "regexp"
  13. "code.gitea.io/gitea/modules/process"
  14. )
  15. // BlamePart represents block of blame - continuous lines with one sha
  16. type BlamePart struct {
  17. Sha string
  18. Lines []string
  19. }
  20. // BlameReader returns part of file blame one by one
  21. type BlameReader struct {
  22. cmd *exec.Cmd
  23. pid int64
  24. output io.ReadCloser
  25. reader *bufio.Reader
  26. lastSha *string
  27. cancel context.CancelFunc
  28. }
  29. var shaLineRegex = regexp.MustCompile("^([a-z0-9]{40})")
  30. // NextPart returns next part of blame (sequencial code lines with the same commit)
  31. func (r *BlameReader) NextPart() (*BlamePart, error) {
  32. var blamePart *BlamePart
  33. reader := r.reader
  34. if r.lastSha != nil {
  35. blamePart = &BlamePart{*r.lastSha, make([]string, 0)}
  36. }
  37. var line []byte
  38. var isPrefix bool
  39. var err error
  40. for err != io.EOF {
  41. line, isPrefix, err = reader.ReadLine()
  42. if err != nil && err != io.EOF {
  43. return blamePart, err
  44. }
  45. if len(line) == 0 {
  46. // isPrefix will be false
  47. continue
  48. }
  49. lines := shaLineRegex.FindSubmatch(line)
  50. if lines != nil {
  51. sha1 := string(lines[1])
  52. if blamePart == nil {
  53. blamePart = &BlamePart{sha1, make([]string, 0)}
  54. }
  55. if blamePart.Sha != sha1 {
  56. r.lastSha = &sha1
  57. // need to munch to end of line...
  58. for isPrefix {
  59. _, isPrefix, err = reader.ReadLine()
  60. if err != nil && err != io.EOF {
  61. return blamePart, err
  62. }
  63. }
  64. return blamePart, nil
  65. }
  66. } else if line[0] == '\t' {
  67. code := line[1:]
  68. blamePart.Lines = append(blamePart.Lines, string(code))
  69. }
  70. // need to munch to end of line...
  71. for isPrefix {
  72. _, isPrefix, err = reader.ReadLine()
  73. if err != nil && err != io.EOF {
  74. return blamePart, err
  75. }
  76. }
  77. }
  78. r.lastSha = nil
  79. return blamePart, nil
  80. }
  81. // Close BlameReader - don't run NextPart after invoking that
  82. func (r *BlameReader) Close() error {
  83. defer process.GetManager().Remove(r.pid)
  84. r.cancel()
  85. _ = r.output.Close()
  86. if err := r.cmd.Wait(); err != nil {
  87. return fmt.Errorf("Wait: %v", err)
  88. }
  89. return nil
  90. }
  91. // CreateBlameReader creates reader for given repository, commit and file
  92. func CreateBlameReader(ctx context.Context, repoPath, commitID, file string) (*BlameReader, error) {
  93. gitRepo, err := OpenRepository(repoPath)
  94. if err != nil {
  95. return nil, err
  96. }
  97. gitRepo.Close()
  98. return createBlameReader(ctx, repoPath, GitExecutable, "blame", commitID, "--porcelain", "--", file)
  99. }
  100. func createBlameReader(ctx context.Context, dir string, command ...string) (*BlameReader, error) {
  101. // Here we use the provided context - this should be tied to the request performing the blame so that it does not hang around.
  102. ctx, cancel := context.WithCancel(ctx)
  103. cmd := exec.CommandContext(ctx, command[0], command[1:]...)
  104. cmd.Dir = dir
  105. cmd.Stderr = os.Stderr
  106. stdout, err := cmd.StdoutPipe()
  107. if err != nil {
  108. defer cancel()
  109. return nil, fmt.Errorf("StdoutPipe: %v", err)
  110. }
  111. if err = cmd.Start(); err != nil {
  112. defer cancel()
  113. return nil, fmt.Errorf("Start: %v", err)
  114. }
  115. pid := process.GetManager().Add(fmt.Sprintf("GetBlame [repo_path: %s]", dir), cancel)
  116. reader := bufio.NewReader(stdout)
  117. return &BlameReader{
  118. cmd,
  119. pid,
  120. stdout,
  121. reader,
  122. nil,
  123. cancel,
  124. }, nil
  125. }