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.

commit_info.go 6.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. // Copyright 2017 The Gitea Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. package git
  5. import (
  6. "github.com/emirpasic/gods/trees/binaryheap"
  7. "gopkg.in/src-d/go-git.v4/plumbing"
  8. "gopkg.in/src-d/go-git.v4/plumbing/object"
  9. )
  10. // GetCommitsInfo gets information of all commits that are corresponding to these entries
  11. func (tes Entries) GetCommitsInfo(commit *Commit, treePath string, cache LastCommitCache) ([][]interface{}, *Commit, error) {
  12. entryPaths := make([]string, len(tes)+1)
  13. // Get the commit for the treePath itself
  14. entryPaths[0] = ""
  15. for i, entry := range tes {
  16. entryPaths[i+1] = entry.Name()
  17. }
  18. c, err := commit.repo.gogitRepo.CommitObject(plumbing.Hash(commit.ID))
  19. if err != nil {
  20. return nil, nil, err
  21. }
  22. revs, err := getLastCommitForPaths(c, treePath, entryPaths)
  23. if err != nil {
  24. return nil, nil, err
  25. }
  26. commit.repo.gogitStorage.Close()
  27. commitsInfo := make([][]interface{}, len(tes))
  28. for i, entry := range tes {
  29. if rev, ok := revs[entry.Name()]; ok {
  30. entryCommit := convertCommit(rev)
  31. if entry.IsSubModule() {
  32. subModuleURL := ""
  33. if subModule, err := commit.GetSubModule(entry.Name()); err != nil {
  34. return nil, nil, err
  35. } else if subModule != nil {
  36. subModuleURL = subModule.URL
  37. }
  38. subModuleFile := NewSubModuleFile(entryCommit, subModuleURL, entry.ID.String())
  39. commitsInfo[i] = []interface{}{entry, subModuleFile}
  40. } else {
  41. commitsInfo[i] = []interface{}{entry, entryCommit}
  42. }
  43. } else {
  44. commitsInfo[i] = []interface{}{entry, nil}
  45. }
  46. }
  47. // Retrieve the commit for the treePath itself (see above). We basically
  48. // get it for free during the tree traversal and it's used for listing
  49. // pages to display information about newest commit for a given path.
  50. var treeCommit *Commit
  51. if rev, ok := revs[""]; ok {
  52. treeCommit = convertCommit(rev)
  53. }
  54. return commitsInfo, treeCommit, nil
  55. }
  56. type commitAndPaths struct {
  57. commit *object.Commit
  58. // Paths that are still on the branch represented by commit
  59. paths []string
  60. // Set of hashes for the paths
  61. hashes map[string]plumbing.Hash
  62. }
  63. func getCommitTree(c *object.Commit, treePath string) (*object.Tree, error) {
  64. tree, err := c.Tree()
  65. if err != nil {
  66. return nil, err
  67. }
  68. // Optimize deep traversals by focusing only on the specific tree
  69. if treePath != "" {
  70. tree, err = tree.Tree(treePath)
  71. if err != nil {
  72. return nil, err
  73. }
  74. }
  75. return tree, nil
  76. }
  77. func getFileHashes(c *object.Commit, treePath string, paths []string) (map[string]plumbing.Hash, error) {
  78. tree, err := getCommitTree(c, treePath)
  79. if err == object.ErrDirectoryNotFound {
  80. // The whole tree didn't exist, so return empty map
  81. return make(map[string]plumbing.Hash), nil
  82. }
  83. if err != nil {
  84. return nil, err
  85. }
  86. hashes := make(map[string]plumbing.Hash)
  87. for _, path := range paths {
  88. if path != "" {
  89. entry, err := tree.FindEntry(path)
  90. if err == nil {
  91. hashes[path] = entry.Hash
  92. }
  93. } else {
  94. hashes[path] = tree.Hash
  95. }
  96. }
  97. return hashes, nil
  98. }
  99. func getLastCommitForPaths(c *object.Commit, treePath string, paths []string) (map[string]*object.Commit, error) {
  100. // We do a tree traversal with nodes sorted by commit time
  101. heap := binaryheap.NewWith(func(a, b interface{}) int {
  102. if a.(*commitAndPaths).commit.Committer.When.Before(b.(*commitAndPaths).commit.Committer.When) {
  103. return 1
  104. }
  105. return -1
  106. })
  107. result := make(map[string]*object.Commit)
  108. initialHashes, err := getFileHashes(c, treePath, paths)
  109. if err != nil {
  110. return nil, err
  111. }
  112. // Start search from the root commit and with full set of paths
  113. heap.Push(&commitAndPaths{c, paths, initialHashes})
  114. for {
  115. cIn, ok := heap.Pop()
  116. if !ok {
  117. break
  118. }
  119. current := cIn.(*commitAndPaths)
  120. // Load the parent commits for the one we are currently examining
  121. numParents := current.commit.NumParents()
  122. var parents []*object.Commit
  123. for i := 0; i < numParents; i++ {
  124. parent, err := current.commit.Parent(i)
  125. if err != nil {
  126. break
  127. }
  128. parents = append(parents, parent)
  129. }
  130. // Examine the current commit and set of interesting paths
  131. pathUnchanged := make([]bool, len(current.paths))
  132. parentHashes := make([]map[string]plumbing.Hash, len(parents))
  133. for j, parent := range parents {
  134. parentHashes[j], err = getFileHashes(parent, treePath, current.paths)
  135. if err != nil {
  136. break
  137. }
  138. for i, path := range current.paths {
  139. if parentHashes[j][path] == current.hashes[path] {
  140. pathUnchanged[i] = true
  141. }
  142. }
  143. }
  144. var remainingPaths []string
  145. for i, path := range current.paths {
  146. // The results could already contain some newer change for the same path,
  147. // so don't override that and bail out on the file early.
  148. if result[path] == nil {
  149. if pathUnchanged[i] {
  150. // The path existed with the same hash in at least one parent so it could
  151. // not have been changed in this commit directly.
  152. remainingPaths = append(remainingPaths, path)
  153. } else {
  154. // There are few possible cases how can we get here:
  155. // - The path didn't exist in any parent, so it must have been created by
  156. // this commit.
  157. // - The path did exist in the parent commit, but the hash of the file has
  158. // changed.
  159. // - We are looking at a merge commit and the hash of the file doesn't
  160. // match any of the hashes being merged. This is more common for directories,
  161. // but it can also happen if a file is changed through conflict resolution.
  162. result[path] = current.commit
  163. }
  164. }
  165. }
  166. if len(remainingPaths) > 0 {
  167. // Add the parent nodes along with remaining paths to the heap for further
  168. // processing.
  169. for j, parent := range parents {
  170. // Combine remainingPath with paths available on the parent branch
  171. // and make union of them
  172. remainingPathsForParent := make([]string, 0, len(remainingPaths))
  173. newRemainingPaths := make([]string, 0, len(remainingPaths))
  174. for _, path := range remainingPaths {
  175. if parentHashes[j][path] == current.hashes[path] {
  176. remainingPathsForParent = append(remainingPathsForParent, path)
  177. } else {
  178. newRemainingPaths = append(newRemainingPaths, path)
  179. }
  180. }
  181. if remainingPathsForParent != nil {
  182. heap.Push(&commitAndPaths{parent, remainingPathsForParent, parentHashes[j]})
  183. }
  184. if len(newRemainingPaths) == 0 {
  185. break
  186. } else {
  187. remainingPaths = newRemainingPaths
  188. }
  189. }
  190. }
  191. }
  192. return result, nil
  193. }