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 7.0KB

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