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.

patch_unmerged.go 5.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. // Copyright 2021 The Gitea Authors.
  2. // All rights reserved.
  3. // SPDX-License-Identifier: MIT
  4. package pull
  5. import (
  6. "bufio"
  7. "context"
  8. "fmt"
  9. "io"
  10. "os"
  11. "strconv"
  12. "strings"
  13. "code.gitea.io/gitea/modules/git"
  14. "code.gitea.io/gitea/modules/log"
  15. )
  16. // lsFileLine is a Quadruplet struct (+error) representing a partially parsed line from ls-files
  17. type lsFileLine struct {
  18. mode string
  19. sha string
  20. stage int
  21. path string
  22. err error
  23. }
  24. // SameAs checks if two lsFileLines are referring to the same path, sha and mode (ignoring stage)
  25. func (line *lsFileLine) SameAs(other *lsFileLine) bool {
  26. if line == nil || other == nil {
  27. return false
  28. }
  29. if line.err != nil || other.err != nil {
  30. return false
  31. }
  32. return line.mode == other.mode &&
  33. line.sha == other.sha &&
  34. line.path == other.path
  35. }
  36. // String provides a string representation for logging
  37. func (line *lsFileLine) String() string {
  38. if line == nil {
  39. return "<nil>"
  40. }
  41. if line.err != nil {
  42. return fmt.Sprintf("%d %s %s %s %v", line.stage, line.mode, line.path, line.sha, line.err)
  43. }
  44. return fmt.Sprintf("%d %s %s %s", line.stage, line.mode, line.path, line.sha)
  45. }
  46. // readUnmergedLsFileLines calls git ls-files -u -z and parses the lines into mode-sha-stage-path quadruplets
  47. // it will push these to the provided channel closing it at the end
  48. func readUnmergedLsFileLines(ctx context.Context, tmpBasePath string, outputChan chan *lsFileLine) {
  49. defer func() {
  50. // Always close the outputChan at the end of this function
  51. close(outputChan)
  52. }()
  53. lsFilesReader, lsFilesWriter, err := os.Pipe()
  54. if err != nil {
  55. log.Error("Unable to open stderr pipe: %v", err)
  56. outputChan <- &lsFileLine{err: fmt.Errorf("unable to open stderr pipe: %w", err)}
  57. return
  58. }
  59. defer func() {
  60. _ = lsFilesWriter.Close()
  61. _ = lsFilesReader.Close()
  62. }()
  63. stderr := &strings.Builder{}
  64. err = git.NewCommand(ctx, "ls-files", "-u", "-z").
  65. Run(&git.RunOpts{
  66. Dir: tmpBasePath,
  67. Stdout: lsFilesWriter,
  68. Stderr: stderr,
  69. PipelineFunc: func(_ context.Context, _ context.CancelFunc) error {
  70. _ = lsFilesWriter.Close()
  71. defer func() {
  72. _ = lsFilesReader.Close()
  73. }()
  74. bufferedReader := bufio.NewReader(lsFilesReader)
  75. for {
  76. line, err := bufferedReader.ReadString('\000')
  77. if err != nil {
  78. if err == io.EOF {
  79. return nil
  80. }
  81. return err
  82. }
  83. toemit := &lsFileLine{}
  84. split := strings.SplitN(line, " ", 3)
  85. if len(split) < 3 {
  86. return fmt.Errorf("malformed line: %s", line)
  87. }
  88. toemit.mode = split[0]
  89. toemit.sha = split[1]
  90. if len(split[2]) < 4 {
  91. return fmt.Errorf("malformed line: %s", line)
  92. }
  93. toemit.stage, err = strconv.Atoi(split[2][0:1])
  94. if err != nil {
  95. return fmt.Errorf("malformed line: %s", line)
  96. }
  97. toemit.path = split[2][2 : len(split[2])-1]
  98. outputChan <- toemit
  99. }
  100. },
  101. })
  102. if err != nil {
  103. outputChan <- &lsFileLine{err: fmt.Errorf("git ls-files -u -z: %w", git.ConcatenateError(err, stderr.String()))}
  104. }
  105. }
  106. // unmergedFile is triple (+error) of lsFileLines split into stages 1,2 & 3.
  107. type unmergedFile struct {
  108. stage1 *lsFileLine
  109. stage2 *lsFileLine
  110. stage3 *lsFileLine
  111. err error
  112. }
  113. // String provides a string representation of the an unmerged file for logging
  114. func (u *unmergedFile) String() string {
  115. if u == nil {
  116. return "<nil>"
  117. }
  118. if u.err != nil {
  119. return fmt.Sprintf("error: %v\n%v\n%v\n%v", u.err, u.stage1, u.stage2, u.stage3)
  120. }
  121. return fmt.Sprintf("%v\n%v\n%v", u.stage1, u.stage2, u.stage3)
  122. }
  123. // unmergedFiles will collate the output from readUnstagedLsFileLines in to file triplets and send them
  124. // to the provided channel, closing at the end.
  125. func unmergedFiles(ctx context.Context, tmpBasePath string, unmerged chan *unmergedFile) {
  126. defer func() {
  127. // Always close the channel
  128. close(unmerged)
  129. }()
  130. ctx, cancel := context.WithCancel(ctx)
  131. lsFileLineChan := make(chan *lsFileLine, 10) // give lsFileLineChan a buffer
  132. go readUnmergedLsFileLines(ctx, tmpBasePath, lsFileLineChan)
  133. defer func() {
  134. cancel()
  135. for range lsFileLineChan {
  136. // empty channel
  137. }
  138. }()
  139. next := &unmergedFile{}
  140. for line := range lsFileLineChan {
  141. log.Trace("Got line: %v Current State:\n%v", line, next)
  142. if line.err != nil {
  143. log.Error("Unable to run ls-files -u -z! Error: %v", line.err)
  144. unmerged <- &unmergedFile{err: fmt.Errorf("unable to run ls-files -u -z! Error: %w", line.err)}
  145. return
  146. }
  147. // stages are always emitted 1,2,3 but sometimes 1, 2 or 3 are dropped
  148. switch line.stage {
  149. case 0:
  150. // Should not happen as this represents successfully merged file - we will tolerate and ignore though
  151. case 1:
  152. if next.stage1 != nil || next.stage2 != nil || next.stage3 != nil {
  153. // We need to handle the unstaged file stage1,stage2,stage3
  154. unmerged <- next
  155. }
  156. next = &unmergedFile{stage1: line}
  157. case 2:
  158. if next.stage3 != nil || next.stage2 != nil || (next.stage1 != nil && next.stage1.path != line.path) {
  159. // We need to handle the unstaged file stage1,stage2,stage3
  160. unmerged <- next
  161. next = &unmergedFile{}
  162. }
  163. next.stage2 = line
  164. case 3:
  165. if next.stage3 != nil || (next.stage1 != nil && next.stage1.path != line.path) || (next.stage2 != nil && next.stage2.path != line.path) {
  166. // We need to handle the unstaged file stage1,stage2,stage3
  167. unmerged <- next
  168. next = &unmergedFile{}
  169. }
  170. next.stage3 = line
  171. default:
  172. log.Error("Unexpected stage %d for path %s in run ls-files -u -z!", line.stage, line.path)
  173. unmerged <- &unmergedFile{err: fmt.Errorf("unexpected stage %d for path %s in git ls-files -u -z", line.stage, line.path)}
  174. return
  175. }
  176. }
  177. // We need to handle the unstaged file stage1,stage2,stage3
  178. if next.stage1 != nil || next.stage2 != nil || next.stage3 != nil {
  179. unmerged <- next
  180. }
  181. }