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.

repo_compare.go 6.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. // Copyright 2015 The Gogs Authors. All rights reserved.
  2. // Copyright 2019 The Gitea Authors. All rights reserved.
  3. // Use of this source code is governed by a MIT-style
  4. // license that can be found in the LICENSE file.
  5. package git
  6. import (
  7. "bytes"
  8. "container/list"
  9. "fmt"
  10. "io"
  11. "regexp"
  12. "strconv"
  13. "strings"
  14. "time"
  15. logger "code.gitea.io/gitea/modules/log"
  16. )
  17. // CompareInfo represents needed information for comparing references.
  18. type CompareInfo struct {
  19. MergeBase string
  20. Commits *list.List
  21. NumFiles int
  22. }
  23. // GetMergeBase checks and returns merge base of two branches and the reference used as base.
  24. func (repo *Repository) GetMergeBase(tmpRemote string, base, head string) (string, string, error) {
  25. if tmpRemote == "" {
  26. tmpRemote = "origin"
  27. }
  28. if tmpRemote != "origin" {
  29. tmpBaseName := "refs/remotes/" + tmpRemote + "/tmp_" + base
  30. // Fetch commit into a temporary branch in order to be able to handle commits and tags
  31. _, err := NewCommand("fetch", tmpRemote, base+":"+tmpBaseName).RunInDir(repo.Path)
  32. if err == nil {
  33. base = tmpBaseName
  34. }
  35. }
  36. stdout, err := NewCommand("merge-base", "--", base, head).RunInDir(repo.Path)
  37. return strings.TrimSpace(stdout), base, err
  38. }
  39. // GetCompareInfo generates and returns compare information between base and head branches of repositories.
  40. func (repo *Repository) GetCompareInfo(basePath, baseBranch, headBranch string) (_ *CompareInfo, err error) {
  41. var (
  42. remoteBranch string
  43. tmpRemote string
  44. )
  45. // We don't need a temporary remote for same repository.
  46. if repo.Path != basePath {
  47. // Add a temporary remote
  48. tmpRemote = strconv.FormatInt(time.Now().UnixNano(), 10)
  49. if err = repo.AddRemote(tmpRemote, basePath, false); err != nil {
  50. return nil, fmt.Errorf("AddRemote: %v", err)
  51. }
  52. defer func() {
  53. if err := repo.RemoveRemote(tmpRemote); err != nil {
  54. logger.Error("GetPullRequestInfo: RemoveRemote: %v", err)
  55. }
  56. }()
  57. }
  58. compareInfo := new(CompareInfo)
  59. compareInfo.MergeBase, remoteBranch, err = repo.GetMergeBase(tmpRemote, baseBranch, headBranch)
  60. if err == nil {
  61. // We have a common base
  62. logs, err := NewCommand("log", compareInfo.MergeBase+"..."+headBranch, prettyLogFormat).RunInDirBytes(repo.Path)
  63. if err != nil {
  64. return nil, err
  65. }
  66. compareInfo.Commits, err = repo.parsePrettyFormatLogToList(logs)
  67. if err != nil {
  68. return nil, fmt.Errorf("parsePrettyFormatLogToList: %v", err)
  69. }
  70. } else {
  71. compareInfo.Commits = list.New()
  72. compareInfo.MergeBase, err = GetFullCommitID(repo.Path, remoteBranch)
  73. if err != nil {
  74. compareInfo.MergeBase = remoteBranch
  75. }
  76. }
  77. // Count number of changed files.
  78. // This probably should be removed as we need to use shortstat elsewhere
  79. // Now there is git diff --shortstat but this appears to be slower than simply iterating with --nameonly
  80. compareInfo.NumFiles, err = repo.GetDiffNumChangedFiles(remoteBranch, headBranch)
  81. if err != nil {
  82. return nil, err
  83. }
  84. return compareInfo, nil
  85. }
  86. type lineCountWriter struct {
  87. numLines int
  88. }
  89. // Write counts the number of newlines in the provided bytestream
  90. func (l *lineCountWriter) Write(p []byte) (n int, err error) {
  91. n = len(p)
  92. l.numLines += bytes.Count(p, []byte{'\000'})
  93. return
  94. }
  95. // GetDiffNumChangedFiles counts the number of changed files
  96. // This is substantially quicker than shortstat but...
  97. func (repo *Repository) GetDiffNumChangedFiles(base, head string) (int, error) {
  98. // Now there is git diff --shortstat but this appears to be slower than simply iterating with --nameonly
  99. w := &lineCountWriter{}
  100. stderr := new(bytes.Buffer)
  101. if err := NewCommand("diff", "-z", "--name-only", base+"..."+head).
  102. RunInDirPipeline(repo.Path, w, stderr); err != nil {
  103. return 0, fmt.Errorf("%v: Stderr: %s", err, stderr)
  104. }
  105. return w.numLines, nil
  106. }
  107. // GetDiffShortStat counts number of changed files, number of additions and deletions
  108. func (repo *Repository) GetDiffShortStat(base, head string) (numFiles, totalAdditions, totalDeletions int, err error) {
  109. return GetDiffShortStat(repo.Path, base+"..."+head)
  110. }
  111. // GetDiffShortStat counts number of changed files, number of additions and deletions
  112. func GetDiffShortStat(repoPath string, args ...string) (numFiles, totalAdditions, totalDeletions int, err error) {
  113. // Now if we call:
  114. // $ git diff --shortstat 1ebb35b98889ff77299f24d82da426b434b0cca0...788b8b1440462d477f45b0088875
  115. // we get:
  116. // " 9902 files changed, 2034198 insertions(+), 298800 deletions(-)\n"
  117. args = append([]string{
  118. "diff",
  119. "--shortstat",
  120. }, args...)
  121. stdout, err := NewCommand(args...).RunInDir(repoPath)
  122. if err != nil {
  123. return 0, 0, 0, err
  124. }
  125. return parseDiffStat(stdout)
  126. }
  127. var shortStatFormat = regexp.MustCompile(
  128. `\s*(\d+) files? changed(?:, (\d+) insertions?\(\+\))?(?:, (\d+) deletions?\(-\))?`)
  129. func parseDiffStat(stdout string) (numFiles, totalAdditions, totalDeletions int, err error) {
  130. if len(stdout) == 0 || stdout == "\n" {
  131. return 0, 0, 0, nil
  132. }
  133. groups := shortStatFormat.FindStringSubmatch(stdout)
  134. if len(groups) != 4 {
  135. return 0, 0, 0, fmt.Errorf("unable to parse shortstat: %s groups: %s", stdout, groups)
  136. }
  137. numFiles, err = strconv.Atoi(groups[1])
  138. if err != nil {
  139. return 0, 0, 0, fmt.Errorf("unable to parse shortstat: %s. Error parsing NumFiles %v", stdout, err)
  140. }
  141. if len(groups[2]) != 0 {
  142. totalAdditions, err = strconv.Atoi(groups[2])
  143. if err != nil {
  144. return 0, 0, 0, fmt.Errorf("unable to parse shortstat: %s. Error parsing NumAdditions %v", stdout, err)
  145. }
  146. }
  147. if len(groups[3]) != 0 {
  148. totalDeletions, err = strconv.Atoi(groups[3])
  149. if err != nil {
  150. return 0, 0, 0, fmt.Errorf("unable to parse shortstat: %s. Error parsing NumDeletions %v", stdout, err)
  151. }
  152. }
  153. return
  154. }
  155. // GetDiffOrPatch generates either diff or formatted patch data between given revisions
  156. func (repo *Repository) GetDiffOrPatch(base, head string, w io.Writer, formatted bool) error {
  157. if formatted {
  158. return repo.GetPatch(base, head, w)
  159. }
  160. return repo.GetDiff(base, head, w)
  161. }
  162. // GetDiff generates and returns patch data between given revisions.
  163. func (repo *Repository) GetDiff(base, head string, w io.Writer) error {
  164. return NewCommand("diff", "-p", "--binary", base, head).
  165. RunInDirPipeline(repo.Path, w, nil)
  166. }
  167. // GetPatch generates and returns format-patch data between given revisions.
  168. func (repo *Repository) GetPatch(base, head string, w io.Writer) error {
  169. return NewCommand("format-patch", "--binary", "--stdout", base+"..."+head).
  170. RunInDirPipeline(repo.Path, w, nil)
  171. }
  172. // GetDiffFromMergeBase generates and return patch data from merge base to head
  173. func (repo *Repository) GetDiffFromMergeBase(base, head string, w io.Writer) error {
  174. return NewCommand("diff", "-p", "--binary", base+"..."+head).
  175. RunInDirPipeline(repo.Path, w, nil)
  176. }