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