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

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