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.4KB

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