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  }