- // Copyright 2017 The Gitea Authors. All rights reserved.
- // Use of this source code is governed by a MIT-style
- // license that can be found in the LICENSE file.
-
- package git
-
- import (
- "path"
-
- "github.com/emirpasic/gods/trees/binaryheap"
- "github.com/go-git/go-git/v5/plumbing"
- "github.com/go-git/go-git/v5/plumbing/object"
- cgobject "github.com/go-git/go-git/v5/plumbing/object/commitgraph"
- )
-
- // GetCommitsInfo gets information of all commits that are corresponding to these entries
- func (tes Entries) GetCommitsInfo(commit *Commit, treePath string, cache LastCommitCache) ([][]interface{}, *Commit, error) {
- entryPaths := make([]string, len(tes)+1)
- // Get the commit for the treePath itself
- entryPaths[0] = ""
- for i, entry := range tes {
- entryPaths[i+1] = entry.Name()
- }
-
- commitNodeIndex, commitGraphFile := commit.repo.CommitNodeIndex()
- if commitGraphFile != nil {
- defer commitGraphFile.Close()
- }
-
- c, err := commitNodeIndex.Get(commit.ID)
- if err != nil {
- return nil, nil, err
- }
-
- var revs map[string]*object.Commit
- if cache != nil {
- var unHitPaths []string
- revs, unHitPaths, err = getLastCommitForPathsByCache(commit.ID.String(), treePath, entryPaths, cache)
- if err != nil {
- return nil, nil, err
- }
- if len(unHitPaths) > 0 {
- revs2, err := getLastCommitForPaths(c, treePath, unHitPaths)
- if err != nil {
- return nil, nil, err
- }
-
- for k, v := range revs2 {
- if err := cache.Put(commit.ID.String(), path.Join(treePath, k), v.ID().String()); err != nil {
- return nil, nil, err
- }
- revs[k] = v
- }
- }
- } else {
- revs, err = getLastCommitForPaths(c, treePath, entryPaths)
- }
- if err != nil {
- return nil, nil, err
- }
-
- commit.repo.gogitStorage.Close()
-
- commitsInfo := make([][]interface{}, len(tes))
- for i, entry := range tes {
- if rev, ok := revs[entry.Name()]; ok {
- entryCommit := convertCommit(rev)
- if entry.IsSubModule() {
- subModuleURL := ""
- var fullPath string
- if len(treePath) > 0 {
- fullPath = treePath + "/" + entry.Name()
- } else {
- fullPath = entry.Name()
- }
- if subModule, err := commit.GetSubModule(fullPath); err != nil {
- return nil, nil, err
- } else if subModule != nil {
- subModuleURL = subModule.URL
- }
- subModuleFile := NewSubModuleFile(entryCommit, subModuleURL, entry.ID.String())
- commitsInfo[i] = []interface{}{entry, subModuleFile}
- } else {
- commitsInfo[i] = []interface{}{entry, entryCommit}
- }
- } else {
- commitsInfo[i] = []interface{}{entry, nil}
- }
- }
-
- // Retrieve the commit for the treePath itself (see above). We basically
- // get it for free during the tree traversal and it's used for listing
- // pages to display information about newest commit for a given path.
- var treeCommit *Commit
- if treePath == "" {
- treeCommit = commit
- } else if rev, ok := revs[""]; ok {
- treeCommit = convertCommit(rev)
- treeCommit.repo = commit.repo
- }
- return commitsInfo, treeCommit, nil
- }
-
- type commitAndPaths struct {
- commit cgobject.CommitNode
- // Paths that are still on the branch represented by commit
- paths []string
- // Set of hashes for the paths
- hashes map[string]plumbing.Hash
- }
-
- func getCommitTree(c cgobject.CommitNode, treePath string) (*object.Tree, error) {
- tree, err := c.Tree()
- if err != nil {
- return nil, err
- }
-
- // Optimize deep traversals by focusing only on the specific tree
- if treePath != "" {
- tree, err = tree.Tree(treePath)
- if err != nil {
- return nil, err
- }
- }
-
- return tree, nil
- }
-
- func getFileHashes(c cgobject.CommitNode, treePath string, paths []string) (map[string]plumbing.Hash, error) {
- tree, err := getCommitTree(c, treePath)
- if err == object.ErrDirectoryNotFound {
- // The whole tree didn't exist, so return empty map
- return make(map[string]plumbing.Hash), nil
- }
- if err != nil {
- return nil, err
- }
-
- hashes := make(map[string]plumbing.Hash)
- for _, path := range paths {
- if path != "" {
- entry, err := tree.FindEntry(path)
- if err == nil {
- hashes[path] = entry.Hash
- }
- } else {
- hashes[path] = tree.Hash
- }
- }
-
- return hashes, nil
- }
-
- func getLastCommitForPathsByCache(commitID, treePath string, paths []string, cache LastCommitCache) (map[string]*object.Commit, []string, error) {
- var unHitEntryPaths []string
- var results = make(map[string]*object.Commit)
- for _, p := range paths {
- lastCommit, err := cache.Get(commitID, path.Join(treePath, p))
- if err != nil {
- return nil, nil, err
- }
- if lastCommit != nil {
- results[p] = lastCommit
- continue
- }
-
- unHitEntryPaths = append(unHitEntryPaths, p)
- }
-
- return results, unHitEntryPaths, nil
- }
-
- func getLastCommitForPaths(c cgobject.CommitNode, treePath string, paths []string) (map[string]*object.Commit, error) {
- // We do a tree traversal with nodes sorted by commit time
- heap := binaryheap.NewWith(func(a, b interface{}) int {
- if a.(*commitAndPaths).commit.CommitTime().Before(b.(*commitAndPaths).commit.CommitTime()) {
- return 1
- }
- return -1
- })
-
- resultNodes := make(map[string]cgobject.CommitNode)
- initialHashes, err := getFileHashes(c, treePath, paths)
- if err != nil {
- return nil, err
- }
-
- // Start search from the root commit and with full set of paths
- heap.Push(&commitAndPaths{c, paths, initialHashes})
-
- for {
- cIn, ok := heap.Pop()
- if !ok {
- break
- }
- current := cIn.(*commitAndPaths)
-
- // Load the parent commits for the one we are currently examining
- numParents := current.commit.NumParents()
- var parents []cgobject.CommitNode
- for i := 0; i < numParents; i++ {
- parent, err := current.commit.ParentNode(i)
- if err != nil {
- break
- }
- parents = append(parents, parent)
- }
-
- // Examine the current commit and set of interesting paths
- pathUnchanged := make([]bool, len(current.paths))
- parentHashes := make([]map[string]plumbing.Hash, len(parents))
- for j, parent := range parents {
- parentHashes[j], err = getFileHashes(parent, treePath, current.paths)
- if err != nil {
- break
- }
-
- for i, path := range current.paths {
- if parentHashes[j][path] == current.hashes[path] {
- pathUnchanged[i] = true
- }
- }
- }
-
- var remainingPaths []string
- for i, path := range current.paths {
- // The results could already contain some newer change for the same path,
- // so don't override that and bail out on the file early.
- if resultNodes[path] == nil {
- if pathUnchanged[i] {
- // The path existed with the same hash in at least one parent so it could
- // not have been changed in this commit directly.
- remainingPaths = append(remainingPaths, path)
- } else {
- // There are few possible cases how can we get here:
- // - The path didn't exist in any parent, so it must have been created by
- // this commit.
- // - The path did exist in the parent commit, but the hash of the file has
- // changed.
- // - We are looking at a merge commit and the hash of the file doesn't
- // match any of the hashes being merged. This is more common for directories,
- // but it can also happen if a file is changed through conflict resolution.
- resultNodes[path] = current.commit
- }
- }
- }
-
- if len(remainingPaths) > 0 {
- // Add the parent nodes along with remaining paths to the heap for further
- // processing.
- for j, parent := range parents {
- // Combine remainingPath with paths available on the parent branch
- // and make union of them
- remainingPathsForParent := make([]string, 0, len(remainingPaths))
- newRemainingPaths := make([]string, 0, len(remainingPaths))
- for _, path := range remainingPaths {
- if parentHashes[j][path] == current.hashes[path] {
- remainingPathsForParent = append(remainingPathsForParent, path)
- } else {
- newRemainingPaths = append(newRemainingPaths, path)
- }
- }
-
- if remainingPathsForParent != nil {
- heap.Push(&commitAndPaths{parent, remainingPathsForParent, parentHashes[j]})
- }
-
- if len(newRemainingPaths) == 0 {
- break
- } else {
- remainingPaths = newRemainingPaths
- }
- }
- }
- }
-
- // Post-processing
- result := make(map[string]*object.Commit)
- for path, commitNode := range resultNodes {
- var err error
- result[path], err = commitNode.Commit()
- if err != nil {
- return nil, err
- }
- }
-
- return result, nil
- }
|