2017-12-11 05:23:34 +03:00
// 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 (
2020-02-01 22:11:32 +03:00
"path"
2019-04-19 15:17:27 +03:00
"github.com/emirpasic/gods/trees/binaryheap"
2020-03-17 19:19:58 +03:00
"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"
2017-12-11 05:23:34 +03:00
)
2019-04-19 15:17:27 +03:00
// 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 ( )
}
2017-12-11 05:23:34 +03:00
2019-07-02 05:15:14 +03:00
commitNodeIndex , commitGraphFile := commit . repo . CommitNodeIndex ( )
if commitGraphFile != nil {
defer commitGraphFile . Close ( )
}
2019-07-23 21:50:39 +03:00
c , err := commitNodeIndex . Get ( commit . ID )
2019-04-19 15:17:27 +03:00
if err != nil {
return nil , nil , err
}
2017-12-11 05:23:34 +03:00
2020-02-01 22:11:32 +03:00
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 )
}
2019-04-19 15:17:27 +03:00
if err != nil {
return nil , nil , err
}
2017-12-11 05:23:34 +03:00
2019-04-19 15:17:27 +03:00
commit . repo . gogitStorage . Close ( )
2017-12-11 05:23:34 +03:00
2019-04-19 15:17:27 +03:00
commitsInfo := make ( [ ] [ ] interface { } , len ( tes ) )
for i , entry := range tes {
if rev , ok := revs [ entry . Name ( ) ] ; ok {
entryCommit := convertCommit ( rev )
if entry . IsSubModule ( ) {
subModuleURL := ""
2019-06-21 09:13:54 +03:00
var fullPath string
if len ( treePath ) > 0 {
fullPath = treePath + "/" + entry . Name ( )
} else {
fullPath = entry . Name ( )
}
if subModule , err := commit . GetSubModule ( fullPath ) ; err != nil {
2019-04-19 15:17:27 +03:00
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 }
2017-12-11 05:23:34 +03:00
}
}
2019-04-19 15:17:27 +03:00
// 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
2019-07-22 15:03:15 +03:00
if treePath == "" {
treeCommit = commit
} else if rev , ok := revs [ "" ] ; ok {
2019-04-19 15:17:27 +03:00
treeCommit = convertCommit ( rev )
2019-10-21 01:26:36 +03:00
treeCommit . repo = commit . repo
2017-12-11 05:23:34 +03:00
}
2019-04-19 15:17:27 +03:00
return commitsInfo , treeCommit , nil
2017-12-11 05:23:34 +03:00
}
2019-04-19 15:17:27 +03:00
type commitAndPaths struct {
2019-07-02 05:15:14 +03:00
commit cgobject . CommitNode
2019-04-19 15:17:27 +03:00
// Paths that are still on the branch represented by commit
paths [ ] string
// Set of hashes for the paths
hashes map [ string ] plumbing . Hash
2017-12-11 05:23:34 +03:00
}
2019-07-02 05:15:14 +03:00
func getCommitTree ( c cgobject . CommitNode , treePath string ) ( * object . Tree , error ) {
2019-04-19 15:17:27 +03:00
tree , err := c . Tree ( )
if err != nil {
2017-12-11 05:23:34 +03:00
return nil , err
}
2019-04-19 15:17:27 +03:00
// Optimize deep traversals by focusing only on the specific tree
if treePath != "" {
tree , err = tree . Tree ( treePath )
2017-12-11 05:23:34 +03:00
if err != nil {
2019-04-19 15:17:27 +03:00
return nil , err
2017-12-11 05:23:34 +03:00
}
}
2019-04-19 15:17:27 +03:00
return tree , nil
2017-12-11 05:23:34 +03:00
}
2019-07-02 05:15:14 +03:00
func getFileHashes ( c cgobject . CommitNode , treePath string , paths [ ] string ) ( map [ string ] plumbing . Hash , error ) {
2019-04-19 15:17:27 +03:00
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
2017-12-11 05:23:34 +03:00
}
if err != nil {
2019-04-19 15:17:27 +03:00
return nil , err
2017-12-11 05:23:34 +03:00
}
2019-04-19 15:17:27 +03:00
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
}
2017-12-11 05:23:34 +03:00
}
2019-04-19 15:17:27 +03:00
return hashes , nil
}
2017-12-11 05:23:34 +03:00
2020-02-01 22:11:32 +03:00
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
}
2019-07-02 05:15:14 +03:00
func getLastCommitForPaths ( c cgobject . CommitNode , treePath string , paths [ ] string ) ( map [ string ] * object . Commit , error ) {
2019-04-19 15:17:27 +03:00
// We do a tree traversal with nodes sorted by commit time
heap := binaryheap . NewWith ( func ( a , b interface { } ) int {
2019-07-02 05:15:14 +03:00
if a . ( * commitAndPaths ) . commit . CommitTime ( ) . Before ( b . ( * commitAndPaths ) . commit . CommitTime ( ) ) {
2019-04-19 15:17:27 +03:00
return 1
}
return - 1
} )
2017-12-22 10:00:30 +03:00
2019-07-02 05:15:14 +03:00
resultNodes := make ( map [ string ] cgobject . CommitNode )
2019-04-19 15:17:27 +03:00
initialHashes , err := getFileHashes ( c , treePath , paths )
if err != nil {
return nil , err
2017-12-22 10:00:30 +03:00
}
2019-04-19 15:17:27 +03:00
// 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
2017-12-11 05:23:34 +03:00
}
2019-04-19 15:17:27 +03:00
current := cIn . ( * commitAndPaths )
// Load the parent commits for the one we are currently examining
numParents := current . commit . NumParents ( )
2019-07-02 05:15:14 +03:00
var parents [ ] cgobject . CommitNode
2019-04-19 15:17:27 +03:00
for i := 0 ; i < numParents ; i ++ {
2019-07-02 05:15:14 +03:00
parent , err := current . commit . ParentNode ( i )
2019-04-19 15:17:27 +03:00
if err != nil {
break
2017-12-11 05:23:34 +03:00
}
2019-04-19 15:17:27 +03:00
parents = append ( parents , parent )
}
// Examine the current commit and set of interesting paths
2019-04-21 11:49:06 +03:00
pathUnchanged := make ( [ ] bool , len ( current . paths ) )
2019-04-19 15:17:27 +03:00
parentHashes := make ( [ ] map [ string ] plumbing . Hash , len ( parents ) )
for j , parent := range parents {
parentHashes [ j ] , err = getFileHashes ( parent , treePath , current . paths )
2017-12-11 05:23:34 +03:00
if err != nil {
2019-04-19 15:17:27 +03:00
break
2017-12-11 05:23:34 +03:00
}
2019-04-19 15:17:27 +03:00
for i , path := range current . paths {
2019-04-21 11:49:06 +03:00
if parentHashes [ j ] [ path ] == current . hashes [ path ] {
pathUnchanged [ i ] = true
2017-12-11 05:23:34 +03:00
}
}
}
2019-04-19 15:17:27 +03:00
var remainingPaths [ ] string
for i , path := range current . paths {
2019-04-21 11:49:06 +03:00
// The results could already contain some newer change for the same path,
// so don't override that and bail out on the file early.
2019-07-02 05:15:14 +03:00
if resultNodes [ path ] == nil {
2019-04-21 11:49:06 +03:00
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.
2019-04-19 15:17:27 +03:00
remainingPaths = append ( remainingPaths , path )
2019-04-21 11:49:06 +03:00
} 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.
2019-07-02 05:15:14 +03:00
resultNodes [ path ] = current . commit
2019-04-19 15:17:27 +03:00
}
}
2017-12-11 05:23:34 +03:00
}
2019-04-19 15:17:27 +03:00
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
2019-04-30 16:27:41 +03:00
remainingPathsForParent := make ( [ ] string , 0 , len ( remainingPaths ) )
newRemainingPaths := make ( [ ] string , 0 , len ( remainingPaths ) )
2019-04-19 15:17:27 +03:00
for _ , path := range remainingPaths {
2019-04-21 11:49:06 +03:00
if parentHashes [ j ] [ path ] == current . hashes [ path ] {
2019-04-19 15:17:27 +03:00
remainingPathsForParent = append ( remainingPathsForParent , path )
2019-04-21 11:49:06 +03:00
} else {
newRemainingPaths = append ( newRemainingPaths , path )
2019-04-19 15:17:27 +03:00
}
}
2019-04-21 11:49:06 +03:00
if remainingPathsForParent != nil {
heap . Push ( & commitAndPaths { parent , remainingPathsForParent , parentHashes [ j ] } )
}
if len ( newRemainingPaths ) == 0 {
break
} else {
remainingPaths = newRemainingPaths
}
2019-04-19 15:17:27 +03:00
}
}
2017-12-11 05:23:34 +03:00
}
2019-04-19 15:17:27 +03:00
2019-07-02 05:15:14 +03:00
// 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
}
}
2019-04-19 15:17:27 +03:00
return result , nil
2017-12-11 05:23:34 +03:00
}