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.

diff.go 9.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. // Copyright 2020 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. "strconv"
  13. "strings"
  14. "code.gitea.io/gitea/modules/log"
  15. )
  16. // RawDiffType type of a raw diff.
  17. type RawDiffType string
  18. // RawDiffType possible values.
  19. const (
  20. RawDiffNormal RawDiffType = "diff"
  21. RawDiffPatch RawDiffType = "patch"
  22. )
  23. // GetRawDiff dumps diff results of repository in given commit ID to io.Writer.
  24. func GetRawDiff(repo *Repository, commitID string, diffType RawDiffType, writer io.Writer) error {
  25. return GetRepoRawDiffForFile(repo, "", commitID, diffType, "", writer)
  26. }
  27. // GetReverseRawDiff dumps the reverse diff results of repository in given commit ID to io.Writer.
  28. func GetReverseRawDiff(ctx context.Context, repoPath, commitID string, writer io.Writer) error {
  29. stderr := new(bytes.Buffer)
  30. cmd := NewCommand(ctx, "show", "--pretty=format:revert %H%n", "-R").AddDynamicArguments(commitID)
  31. if err := cmd.Run(&RunOpts{
  32. Dir: repoPath,
  33. Stdout: writer,
  34. Stderr: stderr,
  35. }); err != nil {
  36. return fmt.Errorf("Run: %w - %s", err, stderr)
  37. }
  38. return nil
  39. }
  40. // GetRepoRawDiffForFile dumps diff results of file in given commit ID to io.Writer according given repository
  41. func GetRepoRawDiffForFile(repo *Repository, startCommit, endCommit string, diffType RawDiffType, file string, writer io.Writer) error {
  42. commit, err := repo.GetCommit(endCommit)
  43. if err != nil {
  44. return err
  45. }
  46. var files []string
  47. if len(file) > 0 {
  48. files = append(files, file)
  49. }
  50. cmd := NewCommand(repo.Ctx)
  51. switch diffType {
  52. case RawDiffNormal:
  53. if len(startCommit) != 0 {
  54. cmd.AddArguments("diff", "-M").AddDynamicArguments(startCommit, endCommit).AddDashesAndList(files...)
  55. } else if commit.ParentCount() == 0 {
  56. cmd.AddArguments("show").AddDynamicArguments(endCommit).AddDashesAndList(files...)
  57. } else {
  58. c, _ := commit.Parent(0)
  59. cmd.AddArguments("diff", "-M").AddDynamicArguments(c.ID.String(), endCommit).AddDashesAndList(files...)
  60. }
  61. case RawDiffPatch:
  62. if len(startCommit) != 0 {
  63. query := fmt.Sprintf("%s...%s", endCommit, startCommit)
  64. cmd.AddArguments("format-patch", "--no-signature", "--stdout", "--root").AddDynamicArguments(query).AddDashesAndList(files...)
  65. } else if commit.ParentCount() == 0 {
  66. cmd.AddArguments("format-patch", "--no-signature", "--stdout", "--root").AddDynamicArguments(endCommit).AddDashesAndList(files...)
  67. } else {
  68. c, _ := commit.Parent(0)
  69. query := fmt.Sprintf("%s...%s", endCommit, c.ID.String())
  70. cmd.AddArguments("format-patch", "--no-signature", "--stdout").AddDynamicArguments(query).AddDashesAndList(files...)
  71. }
  72. default:
  73. return fmt.Errorf("invalid diffType: %s", diffType)
  74. }
  75. stderr := new(bytes.Buffer)
  76. if err = cmd.Run(&RunOpts{
  77. Dir: repo.Path,
  78. Stdout: writer,
  79. Stderr: stderr,
  80. }); err != nil {
  81. return fmt.Errorf("Run: %w - %s", err, stderr)
  82. }
  83. return nil
  84. }
  85. // ParseDiffHunkString parse the diffhunk content and return
  86. func ParseDiffHunkString(diffhunk string) (leftLine, leftHunk, rightLine, righHunk int) {
  87. ss := strings.Split(diffhunk, "@@")
  88. ranges := strings.Split(ss[1][1:], " ")
  89. leftRange := strings.Split(ranges[0], ",")
  90. leftLine, _ = strconv.Atoi(leftRange[0][1:])
  91. if len(leftRange) > 1 {
  92. leftHunk, _ = strconv.Atoi(leftRange[1])
  93. }
  94. if len(ranges) > 1 {
  95. rightRange := strings.Split(ranges[1], ",")
  96. rightLine, _ = strconv.Atoi(rightRange[0])
  97. if len(rightRange) > 1 {
  98. righHunk, _ = strconv.Atoi(rightRange[1])
  99. }
  100. } else {
  101. log.Debug("Parse line number failed: %v", diffhunk)
  102. rightLine = leftLine
  103. righHunk = leftHunk
  104. }
  105. return leftLine, leftHunk, rightLine, righHunk
  106. }
  107. // Example: @@ -1,8 +1,9 @@ => [..., 1, 8, 1, 9]
  108. var hunkRegex = regexp.MustCompile(`^@@ -(?P<beginOld>[0-9]+)(,(?P<endOld>[0-9]+))? \+(?P<beginNew>[0-9]+)(,(?P<endNew>[0-9]+))? @@`)
  109. const cmdDiffHead = "diff --git "
  110. func isHeader(lof string, inHunk bool) bool {
  111. return strings.HasPrefix(lof, cmdDiffHead) || (!inHunk && (strings.HasPrefix(lof, "---") || strings.HasPrefix(lof, "+++")))
  112. }
  113. // CutDiffAroundLine cuts a diff of a file in way that only the given line + numberOfLine above it will be shown
  114. // it also recalculates hunks and adds the appropriate headers to the new diff.
  115. // Warning: Only one-file diffs are allowed.
  116. func CutDiffAroundLine(originalDiff io.Reader, line int64, old bool, numbersOfLine int) (string, error) {
  117. if line == 0 || numbersOfLine == 0 {
  118. // no line or num of lines => no diff
  119. return "", nil
  120. }
  121. scanner := bufio.NewScanner(originalDiff)
  122. hunk := make([]string, 0)
  123. // begin is the start of the hunk containing searched line
  124. // end is the end of the hunk ...
  125. // currentLine is the line number on the side of the searched line (differentiated by old)
  126. // otherLine is the line number on the opposite side of the searched line (differentiated by old)
  127. var begin, end, currentLine, otherLine int64
  128. var headerLines int
  129. inHunk := false
  130. for scanner.Scan() {
  131. lof := scanner.Text()
  132. // Add header to enable parsing
  133. if isHeader(lof, inHunk) {
  134. if strings.HasPrefix(lof, cmdDiffHead) {
  135. inHunk = false
  136. }
  137. hunk = append(hunk, lof)
  138. headerLines++
  139. }
  140. if currentLine > line {
  141. break
  142. }
  143. // Detect "hunk" with contains commented lof
  144. if strings.HasPrefix(lof, "@@") {
  145. inHunk = true
  146. // Already got our hunk. End of hunk detected!
  147. if len(hunk) > headerLines {
  148. break
  149. }
  150. // A map with named groups of our regex to recognize them later more easily
  151. submatches := hunkRegex.FindStringSubmatch(lof)
  152. groups := make(map[string]string)
  153. for i, name := range hunkRegex.SubexpNames() {
  154. if i != 0 && name != "" {
  155. groups[name] = submatches[i]
  156. }
  157. }
  158. if old {
  159. begin, _ = strconv.ParseInt(groups["beginOld"], 10, 64)
  160. end, _ = strconv.ParseInt(groups["endOld"], 10, 64)
  161. // init otherLine with begin of opposite side
  162. otherLine, _ = strconv.ParseInt(groups["beginNew"], 10, 64)
  163. } else {
  164. begin, _ = strconv.ParseInt(groups["beginNew"], 10, 64)
  165. if groups["endNew"] != "" {
  166. end, _ = strconv.ParseInt(groups["endNew"], 10, 64)
  167. } else {
  168. end = 0
  169. }
  170. // init otherLine with begin of opposite side
  171. otherLine, _ = strconv.ParseInt(groups["beginOld"], 10, 64)
  172. }
  173. end += begin // end is for real only the number of lines in hunk
  174. // lof is between begin and end
  175. if begin <= line && end >= line {
  176. hunk = append(hunk, lof)
  177. currentLine = begin
  178. continue
  179. }
  180. } else if len(hunk) > headerLines {
  181. hunk = append(hunk, lof)
  182. // Count lines in context
  183. switch lof[0] {
  184. case '+':
  185. if !old {
  186. currentLine++
  187. } else {
  188. otherLine++
  189. }
  190. case '-':
  191. if old {
  192. currentLine++
  193. } else {
  194. otherLine++
  195. }
  196. case '\\':
  197. // FIXME: handle `\ No newline at end of file`
  198. default:
  199. currentLine++
  200. otherLine++
  201. }
  202. }
  203. }
  204. if err := scanner.Err(); err != nil {
  205. return "", err
  206. }
  207. // No hunk found
  208. if currentLine == 0 {
  209. return "", nil
  210. }
  211. // headerLines + hunkLine (1) = totalNonCodeLines
  212. if len(hunk)-headerLines-1 <= numbersOfLine {
  213. // No need to cut the hunk => return existing hunk
  214. return strings.Join(hunk, "\n"), nil
  215. }
  216. var oldBegin, oldNumOfLines, newBegin, newNumOfLines int64
  217. if old {
  218. oldBegin = currentLine
  219. newBegin = otherLine
  220. } else {
  221. oldBegin = otherLine
  222. newBegin = currentLine
  223. }
  224. // headers + hunk header
  225. newHunk := make([]string, headerLines)
  226. // transfer existing headers
  227. copy(newHunk, hunk[:headerLines])
  228. // transfer last n lines
  229. newHunk = append(newHunk, hunk[len(hunk)-numbersOfLine-1:]...)
  230. // calculate newBegin, ... by counting lines
  231. for i := len(hunk) - 1; i >= len(hunk)-numbersOfLine; i-- {
  232. switch hunk[i][0] {
  233. case '+':
  234. newBegin--
  235. newNumOfLines++
  236. case '-':
  237. oldBegin--
  238. oldNumOfLines++
  239. default:
  240. oldBegin--
  241. newBegin--
  242. newNumOfLines++
  243. oldNumOfLines++
  244. }
  245. }
  246. // construct the new hunk header
  247. newHunk[headerLines] = fmt.Sprintf("@@ -%d,%d +%d,%d @@",
  248. oldBegin, oldNumOfLines, newBegin, newNumOfLines)
  249. return strings.Join(newHunk, "\n"), nil
  250. }
  251. // GetAffectedFiles returns the affected files between two commits
  252. func GetAffectedFiles(repo *Repository, oldCommitID, newCommitID string, env []string) ([]string, error) {
  253. stdoutReader, stdoutWriter, err := os.Pipe()
  254. if err != nil {
  255. log.Error("Unable to create os.Pipe for %s", repo.Path)
  256. return nil, err
  257. }
  258. defer func() {
  259. _ = stdoutReader.Close()
  260. _ = stdoutWriter.Close()
  261. }()
  262. affectedFiles := make([]string, 0, 32)
  263. // Run `git diff --name-only` to get the names of the changed files
  264. err = NewCommand(repo.Ctx, "diff", "--name-only").AddDynamicArguments(oldCommitID, newCommitID).
  265. Run(&RunOpts{
  266. Env: env,
  267. Dir: repo.Path,
  268. Stdout: stdoutWriter,
  269. PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error {
  270. // Close the writer end of the pipe to begin processing
  271. _ = stdoutWriter.Close()
  272. defer func() {
  273. // Close the reader on return to terminate the git command if necessary
  274. _ = stdoutReader.Close()
  275. }()
  276. // Now scan the output from the command
  277. scanner := bufio.NewScanner(stdoutReader)
  278. for scanner.Scan() {
  279. path := strings.TrimSpace(scanner.Text())
  280. if len(path) == 0 {
  281. continue
  282. }
  283. affectedFiles = append(affectedFiles, path)
  284. }
  285. return scanner.Err()
  286. },
  287. })
  288. if err != nil {
  289. log.Error("Unable to get affected files for commits from %s to %s in %s: %v", oldCommitID, newCommitID, repo.Path, err)
  290. }
  291. return affectedFiles, err
  292. }