gitea源码

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