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_gogit.go 8.2KB

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