2016-11-04 01:16:01 +03:00
// Copyright 2015 The Gogs Authors. All rights reserved.
2019-04-19 15:17:27 +03:00
// Copyright 2019 The Gitea Authors. All rights reserved.
2016-11-04 01:16:01 +03:00
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package git
import (
"bytes"
"container/list"
2021-03-04 03:48:19 +03:00
"io"
"io/ioutil"
2016-11-04 01:16:01 +03:00
"strconv"
"strings"
)
// GetBranchCommitID returns last commit ID string of given branch.
func ( repo * Repository ) GetBranchCommitID ( name string ) ( string , error ) {
2018-01-19 09:18:51 +03:00
return repo . GetRefCommitID ( BranchPrefix + name )
2016-11-04 01:16:01 +03:00
}
// GetTagCommitID returns last commit ID string of given tag.
func ( repo * Repository ) GetTagCommitID ( name string ) ( string , error ) {
2021-04-14 17:22:37 +03:00
return repo . GetRefCommitID ( TagPrefix + name )
2016-11-04 01:16:01 +03:00
}
2019-08-05 23:39:39 +03:00
// GetCommit returns commit object of by ID string.
func ( repo * Repository ) GetCommit ( commitID string ) ( * Commit , error ) {
id , err := repo . ConvertToSHA1 ( commitID )
2016-11-04 01:16:01 +03:00
if err != nil {
return nil , err
}
return repo . getCommit ( id )
}
// GetBranchCommit returns the last commit of given branch.
func ( repo * Repository ) GetBranchCommit ( name string ) ( * Commit , error ) {
commitID , err := repo . GetBranchCommitID ( name )
if err != nil {
return nil , err
}
return repo . GetCommit ( commitID )
}
2016-12-22 12:30:52 +03:00
// GetTagCommit get the commit of the specific tag via name
2016-11-04 01:16:01 +03:00
func ( repo * Repository ) GetTagCommit ( name string ) ( * Commit , error ) {
commitID , err := repo . GetTagCommitID ( name )
if err != nil {
return nil , err
}
return repo . GetCommit ( commitID )
}
2016-12-22 12:30:52 +03:00
func ( repo * Repository ) getCommitByPathWithID ( id SHA1 , relpath string ) ( * Commit , error ) {
2016-11-04 01:16:01 +03:00
// File name starts with ':' must be escaped.
if relpath [ 0 ] == ':' {
relpath = ` \ ` + relpath
}
2016-12-22 12:30:52 +03:00
stdout , err := NewCommand ( "log" , "-1" , prettyLogFormat , id . String ( ) , "--" , relpath ) . RunInDir ( repo . Path )
2016-11-04 01:16:01 +03:00
if err != nil {
return nil , err
}
id , err = NewIDFromString ( stdout )
if err != nil {
return nil , err
}
return repo . getCommit ( id )
}
// GetCommitByPath returns the last commit of relative path.
func ( repo * Repository ) GetCommitByPath ( relpath string ) ( * Commit , error ) {
2016-12-22 12:30:52 +03:00
stdout , err := NewCommand ( "log" , "-1" , prettyLogFormat , "--" , relpath ) . RunInDirBytes ( repo . Path )
2016-11-04 01:16:01 +03:00
if err != nil {
return nil , err
}
commits , err := repo . parsePrettyFormatLogToList ( stdout )
if err != nil {
return nil , err
}
return commits . Front ( ) . Value . ( * Commit ) , nil
}
2016-12-22 12:30:52 +03:00
// CommitsRangeSize the default commits range size
2016-11-04 01:16:01 +03:00
var CommitsRangeSize = 50
2021-01-19 07:07:38 +03:00
// BranchesRangeSize the default branches range size
var BranchesRangeSize = 20
2020-01-24 22:00:29 +03:00
func ( repo * Repository ) commitsByRange ( id SHA1 , page , pageSize int ) ( * list . List , error ) {
stdout , err := NewCommand ( "log" , id . String ( ) , "--skip=" + strconv . Itoa ( ( page - 1 ) * pageSize ) ,
"--max-count=" + strconv . Itoa ( pageSize ) , prettyLogFormat ) . RunInDirBytes ( repo . Path )
2016-11-04 01:16:01 +03:00
if err != nil {
return nil , err
}
return repo . parsePrettyFormatLogToList ( stdout )
}
2019-04-12 05:28:44 +03:00
func ( repo * Repository ) searchCommits ( id SHA1 , opts SearchCommitsOptions ) ( * list . List , error ) {
2020-06-12 00:44:39 +03:00
// create new git log command with limit of 100 commis
2019-09-03 02:38:04 +03:00
cmd := NewCommand ( "log" , id . String ( ) , "-100" , prettyLogFormat )
2020-06-12 00:44:39 +03:00
// ignore case
2019-09-03 02:38:04 +03:00
args := [ ] string { "-i" }
2020-06-12 00:44:39 +03:00
// add authors if present in search query
2019-04-12 05:28:44 +03:00
if len ( opts . Authors ) > 0 {
for _ , v := range opts . Authors {
2019-09-03 02:38:04 +03:00
args = append ( args , "--author=" + v )
2019-04-12 05:28:44 +03:00
}
}
2020-06-12 00:44:39 +03:00
// add commiters if present in search query
2019-04-12 05:28:44 +03:00
if len ( opts . Committers ) > 0 {
for _ , v := range opts . Committers {
2019-09-03 02:38:04 +03:00
args = append ( args , "--committer=" + v )
2019-04-12 05:28:44 +03:00
}
}
2020-06-12 00:44:39 +03:00
// add time constraints if present in search query
2019-04-12 05:28:44 +03:00
if len ( opts . After ) > 0 {
2019-09-03 02:38:04 +03:00
args = append ( args , "--after=" + opts . After )
2019-04-12 05:28:44 +03:00
}
if len ( opts . Before ) > 0 {
2019-09-03 02:38:04 +03:00
args = append ( args , "--before=" + opts . Before )
2019-04-12 05:28:44 +03:00
}
2020-06-12 00:44:39 +03:00
// pretend that all refs along with HEAD were listed on command line as <commis>
// https://git-scm.com/docs/git-log#Documentation/git-log.txt---all
// note this is done only for command created above
2019-04-12 05:28:44 +03:00
if opts . All {
2020-06-12 00:44:39 +03:00
cmd . AddArguments ( "--all" )
2019-09-03 02:38:04 +03:00
}
2020-06-12 00:44:39 +03:00
// add remaining keywords from search string
// note this is done only for command created above
2019-09-03 02:38:04 +03:00
if len ( opts . Keywords ) > 0 {
for _ , v := range opts . Keywords {
cmd . AddArguments ( "--grep=" + v )
}
2017-02-05 17:43:28 +03:00
}
2020-06-12 00:44:39 +03:00
// search for commits matching given constraints and keywords in commit msg
2019-09-03 02:38:04 +03:00
cmd . AddArguments ( args ... )
2017-02-05 17:43:28 +03:00
stdout , err := cmd . RunInDirBytes ( repo . Path )
2016-11-04 01:16:01 +03:00
if err != nil {
return nil , err
}
2019-09-03 02:38:04 +03:00
if len ( stdout ) != 0 {
stdout = append ( stdout , '\n' )
}
2020-06-12 00:44:39 +03:00
// if there are any keywords (ie not commiter:, author:, time:)
// then let's iterate over them
2019-09-03 02:38:04 +03:00
if len ( opts . Keywords ) > 0 {
for _ , v := range opts . Keywords {
2020-06-12 00:44:39 +03:00
// ignore anything below 4 characters as too unspecific
2019-09-03 02:38:04 +03:00
if len ( v ) >= 4 {
2020-06-12 00:44:39 +03:00
// create new git log command with 1 commit limit
2019-09-03 02:38:04 +03:00
hashCmd := NewCommand ( "log" , "-1" , prettyLogFormat )
2020-06-12 00:44:39 +03:00
// add previous arguments except for --grep and --all
2019-09-03 02:38:04 +03:00
hashCmd . AddArguments ( args ... )
2020-06-12 00:44:39 +03:00
// add keyword as <commit>
2019-09-03 02:38:04 +03:00
hashCmd . AddArguments ( v )
2020-06-12 00:44:39 +03:00
// search with given constraints for commit matching sha hash of v
2019-09-03 02:38:04 +03:00
hashMatching , err := hashCmd . RunInDirBytes ( repo . Path )
if err != nil || bytes . Contains ( stdout , hashMatching ) {
continue
}
stdout = append ( stdout , hashMatching ... )
stdout = append ( stdout , '\n' )
}
}
}
return repo . parsePrettyFormatLogToList ( bytes . TrimSuffix ( stdout , [ ] byte { '\n' } ) )
2016-11-04 01:16:01 +03:00
}
2019-04-17 19:06:35 +03:00
func ( repo * Repository ) getFilesChanged ( id1 , id2 string ) ( [ ] string , error ) {
2016-11-04 01:16:01 +03:00
stdout , err := NewCommand ( "diff" , "--name-only" , id1 , id2 ) . RunInDirBytes ( repo . Path )
if err != nil {
return nil , err
}
return strings . Split ( string ( stdout ) , "\n" ) , nil
}
2019-04-17 19:06:35 +03:00
// FileChangedBetweenCommits Returns true if the file changed between commit IDs id1 and id2
2019-08-05 23:39:39 +03:00
// You must ensure that id1 and id2 are valid commit ids.
2019-04-17 19:06:35 +03:00
func ( repo * Repository ) FileChangedBetweenCommits ( filename , id1 , id2 string ) ( bool , error ) {
stdout , err := NewCommand ( "diff" , "--name-only" , "-z" , id1 , id2 , "--" , filename ) . RunInDirBytes ( repo . Path )
if err != nil {
return false , err
}
return len ( strings . TrimSpace ( string ( stdout ) ) ) > 0 , nil
}
2016-12-22 12:30:52 +03:00
// FileCommitsCount return the number of files at a revison
2016-11-04 01:16:01 +03:00
func ( repo * Repository ) FileCommitsCount ( revision , file string ) ( int64 , error ) {
2020-11-08 20:21:54 +03:00
return CommitsCountFiles ( repo . Path , [ ] string { revision } , [ ] string { file } )
2016-11-04 01:16:01 +03:00
}
2019-03-27 12:33:00 +03:00
// CommitsByFileAndRange return the commits according revison file and the page
2016-11-04 01:16:01 +03:00
func ( repo * Repository ) CommitsByFileAndRange ( revision , file string , page int ) ( * list . List , error ) {
2021-03-04 03:48:19 +03:00
skip := ( page - 1 ) * CommitsRangeSize
stdoutReader , stdoutWriter := io . Pipe ( )
defer func ( ) {
_ = stdoutReader . Close ( )
_ = stdoutWriter . Close ( )
} ( )
go func ( ) {
stderr := strings . Builder { }
err := NewCommand ( "log" , revision , "--follow" ,
"--max-count=" + strconv . Itoa ( CommitsRangeSize * page ) ,
prettyLogFormat , "--" , file ) .
RunInDirPipeline ( repo . Path , stdoutWriter , & stderr )
if err != nil {
_ = stdoutWriter . CloseWithError ( ConcatenateError ( err , ( & stderr ) . String ( ) ) )
} else {
_ = stdoutWriter . Close ( )
}
} ( )
if skip > 0 {
_ , err := io . CopyN ( ioutil . Discard , stdoutReader , int64 ( skip * 41 ) )
if err != nil {
if err == io . EOF {
return list . New ( ) , nil
}
_ = stdoutReader . CloseWithError ( err )
return nil , err
}
}
stdout , err := ioutil . ReadAll ( stdoutReader )
2016-11-04 01:16:01 +03:00
if err != nil {
return nil , err
}
return repo . parsePrettyFormatLogToList ( stdout )
}
2019-07-11 17:45:10 +03:00
// CommitsByFileAndRangeNoFollow return the commits according revison file and the page
func ( repo * Repository ) CommitsByFileAndRangeNoFollow ( revision , file string , page int ) ( * list . List , error ) {
stdout , err := NewCommand ( "log" , revision , "--skip=" + strconv . Itoa ( ( page - 1 ) * 50 ) ,
"--max-count=" + strconv . Itoa ( CommitsRangeSize ) , prettyLogFormat , "--" , file ) . RunInDirBytes ( repo . Path )
if err != nil {
return nil , err
}
return repo . parsePrettyFormatLogToList ( stdout )
}
2016-12-22 12:30:52 +03:00
// FilesCountBetween return the number of files changed between two commits
2016-11-04 01:16:01 +03:00
func ( repo * Repository ) FilesCountBetween ( startCommitID , endCommitID string ) ( int , error ) {
stdout , err := NewCommand ( "diff" , "--name-only" , startCommitID + "..." + endCommitID ) . RunInDir ( repo . Path )
2020-07-29 20:53:04 +03:00
if err != nil && strings . Contains ( err . Error ( ) , "no merge base" ) {
// git >= 2.28 now returns an error if startCommitID and endCommitID have become unrelated.
// previously it would return the results of git diff --name-only startCommitID endCommitID so let's try that...
stdout , err = NewCommand ( "diff" , "--name-only" , startCommitID , endCommitID ) . RunInDir ( repo . Path )
}
2016-11-04 01:16:01 +03:00
if err != nil {
return 0 , err
}
return len ( strings . Split ( stdout , "\n" ) ) - 1 , nil
}
// CommitsBetween returns a list that contains commits between [last, before).
func ( repo * Repository ) CommitsBetween ( last * Commit , before * Commit ) ( * list . List , error ) {
2019-12-31 02:34:11 +03:00
var stdout [ ] byte
var err error
if before == nil {
2020-02-11 02:04:43 +03:00
stdout , err = NewCommand ( "rev-list" , last . ID . String ( ) ) . RunInDirBytes ( repo . Path )
2019-12-31 02:34:11 +03:00
} else {
stdout , err = NewCommand ( "rev-list" , before . ID . String ( ) + "..." + last . ID . String ( ) ) . RunInDirBytes ( repo . Path )
2020-07-29 20:53:04 +03:00
if err != nil && strings . Contains ( err . Error ( ) , "no merge base" ) {
// future versions of git >= 2.28 are likely to return an error if before and last have become unrelated.
// previously it would return the results of git rev-list before last so let's try that...
stdout , err = NewCommand ( "rev-list" , before . ID . String ( ) , last . ID . String ( ) ) . RunInDirBytes ( repo . Path )
}
2019-12-31 02:34:11 +03:00
}
if err != nil {
return nil , err
}
return repo . parsePrettyFormatLogToList ( bytes . TrimSpace ( stdout ) )
}
// CommitsBetweenLimit returns a list that contains at most limit commits skipping the first skip commits between [last, before)
func ( repo * Repository ) CommitsBetweenLimit ( last * Commit , before * Commit , limit , skip int ) ( * list . List , error ) {
var stdout [ ] byte
var err error
if before == nil {
stdout , err = NewCommand ( "rev-list" , "--max-count" , strconv . Itoa ( limit ) , "--skip" , strconv . Itoa ( skip ) , last . ID . String ( ) ) . RunInDirBytes ( repo . Path )
} else {
stdout , err = NewCommand ( "rev-list" , "--max-count" , strconv . Itoa ( limit ) , "--skip" , strconv . Itoa ( skip ) , before . ID . String ( ) + "..." + last . ID . String ( ) ) . RunInDirBytes ( repo . Path )
2020-07-29 20:53:04 +03:00
if err != nil && strings . Contains ( err . Error ( ) , "no merge base" ) {
// future versions of git >= 2.28 are likely to return an error if before and last have become unrelated.
// previously it would return the results of git rev-list --max-count n before last so let's try that...
stdout , err = NewCommand ( "rev-list" , "--max-count" , strconv . Itoa ( limit ) , "--skip" , strconv . Itoa ( skip ) , before . ID . String ( ) , last . ID . String ( ) ) . RunInDirBytes ( repo . Path )
}
2019-12-31 02:34:11 +03:00
}
2017-10-23 16:36:14 +03:00
if err != nil {
return nil , err
2016-11-04 01:16:01 +03:00
}
2017-10-23 16:36:14 +03:00
return repo . parsePrettyFormatLogToList ( bytes . TrimSpace ( stdout ) )
2016-11-04 01:16:01 +03:00
}
2016-12-22 12:30:52 +03:00
// CommitsBetweenIDs return commits between twoe commits
2016-11-04 01:16:01 +03:00
func ( repo * Repository ) CommitsBetweenIDs ( last , before string ) ( * list . List , error ) {
lastCommit , err := repo . GetCommit ( last )
if err != nil {
return nil , err
}
2019-12-31 02:34:11 +03:00
if before == "" {
return repo . CommitsBetween ( lastCommit , nil )
}
2016-11-04 01:16:01 +03:00
beforeCommit , err := repo . GetCommit ( before )
if err != nil {
return nil , err
}
return repo . CommitsBetween ( lastCommit , beforeCommit )
}
2016-12-22 12:30:52 +03:00
// CommitsCountBetween return numbers of commits between two commits
2016-11-04 01:16:01 +03:00
func ( repo * Repository ) CommitsCountBetween ( start , end string ) ( int64 , error ) {
2020-11-08 20:21:54 +03:00
count , err := CommitsCountFiles ( repo . Path , [ ] string { start + "..." + end } , [ ] string { } )
2020-07-29 20:53:04 +03:00
if err != nil && strings . Contains ( err . Error ( ) , "no merge base" ) {
// future versions of git >= 2.28 are likely to return an error if before and last have become unrelated.
// previously it would return the results of git rev-list before last so let's try that...
2020-11-08 20:21:54 +03:00
return CommitsCountFiles ( repo . Path , [ ] string { start , end } , [ ] string { } )
2020-07-29 20:53:04 +03:00
}
return count , err
2016-11-04 01:16:01 +03:00
}
2016-12-22 12:30:52 +03:00
// commitsBefore the limit is depth, not total number of returned commits.
2017-12-11 05:23:34 +03:00
func ( repo * Repository ) commitsBefore ( id SHA1 , limit int ) ( * list . List , error ) {
cmd := NewCommand ( "log" )
if limit > 0 {
2018-05-27 21:47:34 +03:00
cmd . AddArguments ( "-" + strconv . Itoa ( limit ) , prettyLogFormat , id . String ( ) )
2017-12-11 05:23:34 +03:00
} else {
cmd . AddArguments ( prettyLogFormat , id . String ( ) )
2016-11-04 01:16:01 +03:00
}
2017-12-11 05:23:34 +03:00
stdout , err := cmd . RunInDirBytes ( repo . Path )
2016-11-04 01:16:01 +03:00
if err != nil {
2017-12-11 05:23:34 +03:00
return nil , err
2016-11-04 01:16:01 +03:00
}
2017-12-11 05:23:34 +03:00
formattedLog , err := repo . parsePrettyFormatLogToList ( bytes . TrimSpace ( stdout ) )
if err != nil {
return nil , err
2016-11-04 01:16:01 +03:00
}
2017-12-11 05:23:34 +03:00
commits := list . New ( )
for logEntry := formattedLog . Front ( ) ; logEntry != nil ; logEntry = logEntry . Next ( ) {
commit := logEntry . Value . ( * Commit )
branches , err := repo . getBranches ( commit , 2 )
2016-11-04 01:16:01 +03:00
if err != nil {
2017-12-11 05:23:34 +03:00
return nil , err
2016-11-04 01:16:01 +03:00
}
2017-12-11 05:23:34 +03:00
if len ( branches ) > 1 {
break
2016-11-04 01:16:01 +03:00
}
2017-12-11 05:23:34 +03:00
commits . PushBack ( commit )
2016-11-04 01:16:01 +03:00
}
2017-12-11 05:23:34 +03:00
return commits , nil
2016-11-04 01:16:01 +03:00
}
2016-12-22 12:30:52 +03:00
func ( repo * Repository ) getCommitsBefore ( id SHA1 ) ( * list . List , error ) {
2017-12-11 05:23:34 +03:00
return repo . commitsBefore ( id , 0 )
2016-11-04 01:16:01 +03:00
}
2016-12-22 12:30:52 +03:00
func ( repo * Repository ) getCommitsBeforeLimit ( id SHA1 , num int ) ( * list . List , error ) {
2017-12-11 05:23:34 +03:00
return repo . commitsBefore ( id , num )
}
func ( repo * Repository ) getBranches ( commit * Commit , limit int ) ( [ ] string , error ) {
2020-10-21 18:42:08 +03:00
if CheckGitVersionAtLeast ( "2.7.0" ) == nil {
2018-05-27 21:47:34 +03:00
stdout , err := NewCommand ( "for-each-ref" , "--count=" + strconv . Itoa ( limit ) , "--format=%(refname:strip=2)" , "--contains" , commit . ID . String ( ) , BranchPrefix ) . RunInDir ( repo . Path )
if err != nil {
return nil , err
}
branches := strings . Fields ( stdout )
return branches , nil
}
stdout , err := NewCommand ( "branch" , "--contains" , commit . ID . String ( ) ) . RunInDir ( repo . Path )
2017-12-11 05:23:34 +03:00
if err != nil {
return nil , err
}
refs := strings . Split ( stdout , "\n" )
2018-05-27 21:47:34 +03:00
var max int
if len ( refs ) > limit {
max = limit
} else {
max = len ( refs ) - 1
}
branches := make ( [ ] string , max )
for i , ref := range refs [ : max ] {
parts := strings . Fields ( ref )
branches [ i ] = parts [ len ( parts ) - 1 ]
2017-12-11 05:23:34 +03:00
}
return branches , nil
2016-11-04 01:16:01 +03:00
}
2020-05-20 15:47:24 +03:00
// GetCommitsFromIDs get commits from commit IDs
func ( repo * Repository ) GetCommitsFromIDs ( commitIDs [ ] string ) ( commits * list . List ) {
commits = list . New ( )
for _ , commitID := range commitIDs {
commit , err := repo . GetCommit ( commitID )
if err == nil && commit != nil {
commits . PushBack ( commit )
}
}
return commits
}
2021-03-04 06:41:23 +03:00
// IsCommitInBranch check if the commit is on the branch
func ( repo * Repository ) IsCommitInBranch ( commitID , branch string ) ( r bool , err error ) {
stdout , err := NewCommand ( "branch" , "--contains" , commitID , branch ) . RunInDir ( repo . Path )
if err != nil {
return false , err
}
return len ( stdout ) > 0 , err
}