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.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. // Copyright 2019 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package git
  4. import (
  5. "bufio"
  6. "context"
  7. "fmt"
  8. "io"
  9. "os"
  10. "regexp"
  11. )
  12. // BlamePart represents block of blame - continuous lines with one sha
  13. type BlamePart struct {
  14. Sha string
  15. Lines []string
  16. }
  17. // BlameReader returns part of file blame one by one
  18. type BlameReader struct {
  19. cmd *Command
  20. output io.WriteCloser
  21. reader io.ReadCloser
  22. bufferedReader *bufio.Reader
  23. done chan error
  24. lastSha *string
  25. }
  26. var shaLineRegex = regexp.MustCompile("^([a-z0-9]{40})")
  27. // NextPart returns next part of blame (sequential code lines with the same commit)
  28. func (r *BlameReader) NextPart() (*BlamePart, error) {
  29. var blamePart *BlamePart
  30. if r.lastSha != nil {
  31. blamePart = &BlamePart{*r.lastSha, make([]string, 0)}
  32. }
  33. var line []byte
  34. var isPrefix bool
  35. var err error
  36. for err != io.EOF {
  37. line, isPrefix, err = r.bufferedReader.ReadLine()
  38. if err != nil && err != io.EOF {
  39. return blamePart, err
  40. }
  41. if len(line) == 0 {
  42. // isPrefix will be false
  43. continue
  44. }
  45. lines := shaLineRegex.FindSubmatch(line)
  46. if lines != nil {
  47. sha1 := string(lines[1])
  48. if blamePart == nil {
  49. blamePart = &BlamePart{sha1, make([]string, 0)}
  50. }
  51. if blamePart.Sha != sha1 {
  52. r.lastSha = &sha1
  53. // need to munch to end of line...
  54. for isPrefix {
  55. _, isPrefix, err = r.bufferedReader.ReadLine()
  56. if err != nil && err != io.EOF {
  57. return blamePart, err
  58. }
  59. }
  60. return blamePart, nil
  61. }
  62. } else if line[0] == '\t' {
  63. code := line[1:]
  64. blamePart.Lines = append(blamePart.Lines, string(code))
  65. }
  66. // need to munch to end of line...
  67. for isPrefix {
  68. _, isPrefix, err = r.bufferedReader.ReadLine()
  69. if err != nil && err != io.EOF {
  70. return blamePart, err
  71. }
  72. }
  73. }
  74. r.lastSha = nil
  75. return blamePart, nil
  76. }
  77. // Close BlameReader - don't run NextPart after invoking that
  78. func (r *BlameReader) Close() error {
  79. err := <-r.done
  80. r.bufferedReader = nil
  81. _ = r.reader.Close()
  82. _ = r.output.Close()
  83. return err
  84. }
  85. // CreateBlameReader creates reader for given repository, commit and file
  86. func CreateBlameReader(ctx context.Context, repoPath, commitID, file string) (*BlameReader, error) {
  87. cmd := NewCommandContextNoGlobals(ctx, "blame", "--porcelain").
  88. AddDynamicArguments(commitID).
  89. AddDashesAndList(file).
  90. SetDescription(fmt.Sprintf("GetBlame [repo_path: %s]", repoPath))
  91. reader, stdout, err := os.Pipe()
  92. if err != nil {
  93. return nil, err
  94. }
  95. done := make(chan error, 1)
  96. go func(cmd *Command, dir string, stdout io.WriteCloser, done chan error) {
  97. if err := cmd.Run(&RunOpts{
  98. UseContextTimeout: true,
  99. Dir: dir,
  100. Stdout: stdout,
  101. Stderr: os.Stderr,
  102. }); err == nil {
  103. stdout.Close()
  104. }
  105. done <- err
  106. }(cmd, repoPath, stdout, done)
  107. bufferedReader := bufio.NewReader(reader)
  108. return &BlameReader{
  109. cmd: cmd,
  110. output: stdout,
  111. reader: reader,
  112. bufferedReader: bufferedReader,
  113. done: done,
  114. }, nil
  115. }