2016-11-04 01:16:01 +03:00
// Copyright 2015 The Gogs Authors. All rights reserved.
2019-02-03 06:35:17 +03:00
// Copyright 2018 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 (
"bufio"
2017-03-22 13:43:54 +03:00
"bytes"
2022-01-20 02:26:57 +03:00
"context"
2021-02-10 10:00:57 +03:00
"errors"
2016-11-04 01:16:01 +03:00
"fmt"
2019-02-03 06:35:17 +03:00
"io"
2021-02-10 10:00:57 +03:00
"os/exec"
2016-11-04 01:16:01 +03:00
"strconv"
"strings"
2021-07-02 22:23:37 +03:00
"code.gitea.io/gitea/modules/log"
2016-11-04 01:16:01 +03:00
)
// Commit represents a git commit.
type Commit struct {
2019-02-06 00:47:01 +03:00
Branch string // Branch this commit belongs to
2016-11-04 01:16:01 +03:00
Tree
2016-12-22 12:30:52 +03:00
ID SHA1 // The ID of this commit object
2016-11-04 01:16:01 +03:00
Author * Signature
Committer * Signature
CommitMessage string
2017-03-22 13:43:54 +03:00
Signature * CommitGPGSignature
2016-11-04 01:16:01 +03:00
2020-01-15 11:32:57 +03:00
Parents [ ] SHA1 // SHA1 strings
2016-12-22 12:30:52 +03:00
submoduleCache * ObjectCache
2016-11-04 01:16:01 +03:00
}
2017-03-22 13:43:54 +03:00
// CommitGPGSignature represents a git commit signature part.
type CommitGPGSignature struct {
Signature string
2022-01-20 20:46:10 +03:00
Payload string // TODO check if can be reconstruct from the rest of commit information to not have duplicate data
2017-03-22 13:43:54 +03:00
}
2016-11-04 01:16:01 +03:00
// Message returns the commit message. Same as retrieving CommitMessage directly.
func ( c * Commit ) Message ( ) string {
return c . CommitMessage
}
// Summary returns first line of commit message.
func ( c * Commit ) Summary ( ) string {
2017-07-01 18:05:01 +03:00
return strings . Split ( strings . TrimSpace ( c . CommitMessage ) , "\n" ) [ 0 ]
2016-11-04 01:16:01 +03:00
}
// ParentID returns oid of n-th parent (0-based index).
// It returns nil if no such parent exists.
2016-12-22 12:30:52 +03:00
func ( c * Commit ) ParentID ( n int ) ( SHA1 , error ) {
2020-01-15 11:32:57 +03:00
if n >= len ( c . Parents ) {
2016-12-22 12:30:52 +03:00
return SHA1 { } , ErrNotExist { "" , "" }
2016-11-04 01:16:01 +03:00
}
2020-01-15 11:32:57 +03:00
return c . Parents [ n ] , nil
2016-11-04 01:16:01 +03:00
}
// Parent returns n-th parent (0-based index) of the commit.
func ( c * Commit ) Parent ( n int ) ( * Commit , error ) {
id , err := c . ParentID ( n )
if err != nil {
return nil , err
}
parent , err := c . repo . getCommit ( id )
if err != nil {
return nil , err
}
return parent , nil
}
// ParentCount returns number of parents of the commit.
// 0 if this is the root commit, otherwise 1,2, etc.
func ( c * Commit ) ParentCount ( ) int {
2020-01-15 11:32:57 +03:00
return len ( c . Parents )
2016-11-04 01:16:01 +03:00
}
// GetCommitByPath return the commit of relative path object.
func ( c * Commit ) GetCommitByPath ( relpath string ) ( * Commit , error ) {
return c . repo . getCommitByPathWithID ( c . ID , relpath )
}
2016-12-22 12:30:52 +03:00
// AddChanges marks local changes to be ready for commit.
2016-11-04 01:16:01 +03:00
func AddChanges ( repoPath string , all bool , files ... string ) error {
2022-01-25 21:15:58 +03:00
return AddChangesWithArgs ( repoPath , globalCommandArgs , all , files ... )
2019-11-27 03:35:52 +03:00
}
// AddChangesWithArgs marks local changes to be ready for commit.
2022-01-10 18:46:26 +03:00
func AddChangesWithArgs ( repoPath string , globalArgs [ ] string , all bool , files ... string ) error {
cmd := NewCommandNoGlobals ( append ( globalArgs , "add" ) ... )
2016-11-04 01:16:01 +03:00
if all {
cmd . AddArguments ( "--all" )
}
2019-08-05 23:39:39 +03:00
cmd . AddArguments ( "--" )
2016-11-04 01:16:01 +03:00
_ , err := cmd . AddArguments ( files ... ) . RunInDir ( repoPath )
return err
}
2016-12-22 12:30:52 +03:00
// CommitChangesOptions the options when a commit created
2016-11-04 01:16:01 +03:00
type CommitChangesOptions struct {
Committer * Signature
Author * Signature
Message string
}
// CommitChanges commits local changes with given committer, author and message.
// If author is nil, it will be the same as committer.
func CommitChanges ( repoPath string , opts CommitChangesOptions ) error {
2022-01-25 21:15:58 +03:00
cargs := make ( [ ] string , len ( globalCommandArgs ) )
copy ( cargs , globalCommandArgs )
2019-11-27 03:35:52 +03:00
return CommitChangesWithArgs ( repoPath , cargs , opts )
}
// CommitChangesWithArgs commits local changes with given committer, author and message.
// If author is nil, it will be the same as committer.
func CommitChangesWithArgs ( repoPath string , args [ ] string , opts CommitChangesOptions ) error {
cmd := NewCommandNoGlobals ( args ... )
2016-11-04 01:16:01 +03:00
if opts . Committer != nil {
cmd . AddArguments ( "-c" , "user.name=" + opts . Committer . Name , "-c" , "user.email=" + opts . Committer . Email )
}
cmd . AddArguments ( "commit" )
if opts . Author == nil {
opts . Author = opts . Committer
}
if opts . Author != nil {
cmd . AddArguments ( fmt . Sprintf ( "--author='%s <%s>'" , opts . Author . Name , opts . Author . Email ) )
}
cmd . AddArguments ( "-m" , opts . Message )
_ , err := cmd . RunInDir ( repoPath )
// No stderr but exit status 1 means nothing to commit.
if err != nil && err . Error ( ) == "exit status 1" {
return nil
}
return err
}
2019-11-07 21:09:51 +03:00
// AllCommitsCount returns count of all commits in repository
2022-01-20 02:26:57 +03:00
func AllCommitsCount ( ctx context . Context , repoPath string , hidePRRefs bool , files ... string ) ( int64 , error ) {
2020-11-08 20:21:54 +03:00
args := [ ] string { "--all" , "--count" }
if hidePRRefs {
2021-12-02 10:28:08 +03:00
args = append ( [ ] string { "--exclude=" + PullPrefix + "*" } , args ... )
2020-11-08 20:21:54 +03:00
}
2022-02-06 22:01:47 +03:00
cmd := NewCommand ( ctx , "rev-list" )
2020-11-08 20:21:54 +03:00
cmd . AddArguments ( args ... )
if len ( files ) > 0 {
cmd . AddArguments ( "--" )
cmd . AddArguments ( files ... )
}
stdout , err := cmd . RunInDir ( repoPath )
2019-11-07 21:09:51 +03:00
if err != nil {
return 0 , err
}
return strconv . ParseInt ( strings . TrimSpace ( stdout ) , 10 , 64 )
}
2020-11-08 20:21:54 +03:00
// CommitsCountFiles returns number of total commits of until given revision.
2022-01-20 02:26:57 +03:00
func CommitsCountFiles ( ctx context . Context , repoPath string , revision , relpath [ ] string ) ( int64 , error ) {
2022-02-06 22:01:47 +03:00
cmd := NewCommand ( ctx , "rev-list" , "--count" )
2020-07-29 20:53:04 +03:00
cmd . AddArguments ( revision ... )
2016-11-04 01:16:01 +03:00
if len ( relpath ) > 0 {
2020-07-29 20:53:04 +03:00
cmd . AddArguments ( "--" )
cmd . AddArguments ( relpath ... )
2016-11-04 01:16:01 +03:00
}
stdout , err := cmd . RunInDir ( repoPath )
if err != nil {
return 0 , err
}
return strconv . ParseInt ( strings . TrimSpace ( stdout ) , 10 , 64 )
}
// CommitsCount returns number of total commits of until given revision.
2022-01-20 02:26:57 +03:00
func CommitsCount ( ctx context . Context , repoPath string , revision ... string ) ( int64 , error ) {
return CommitsCountFiles ( ctx , repoPath , revision , [ ] string { } )
2016-11-04 01:16:01 +03:00
}
2016-12-22 12:30:52 +03:00
// CommitsCount returns number of total commits of until current revision.
2016-11-04 01:16:01 +03:00
func ( c * Commit ) CommitsCount ( ) ( int64 , error ) {
2022-01-20 02:26:57 +03:00
return CommitsCount ( c . repo . Ctx , c . repo . Path , c . ID . String ( ) )
2016-11-04 01:16:01 +03:00
}
2016-12-22 12:30:52 +03:00
// CommitsByRange returns the specific page commits before current revision, every page's number default by CommitsRangeSize
2021-08-09 21:08:51 +03:00
func ( c * Commit ) CommitsByRange ( page , pageSize int ) ( [ ] * Commit , error ) {
2020-01-24 22:00:29 +03:00
return c . repo . commitsByRange ( c . ID , page , pageSize )
2016-11-04 01:16:01 +03:00
}
2016-12-22 12:30:52 +03:00
// CommitsBefore returns all the commits before current revision
2021-08-09 21:08:51 +03:00
func ( c * Commit ) CommitsBefore ( ) ( [ ] * Commit , error ) {
2016-11-04 01:16:01 +03:00
return c . repo . getCommitsBefore ( c . ID )
}
2019-12-16 09:20:25 +03:00
// HasPreviousCommit returns true if a given commitHash is contained in commit's parents
func ( c * Commit ) HasPreviousCommit ( commitHash SHA1 ) ( bool , error ) {
2021-02-10 10:00:57 +03:00
this := c . ID . String ( )
that := commitHash . String ( )
if this == that {
return false , nil
}
if err := CheckGitVersionAtLeast ( "1.8" ) ; err == nil {
2022-02-06 22:01:47 +03:00
_ , err := NewCommand ( c . repo . Ctx , "merge-base" , "--is-ancestor" , that , this ) . RunInDir ( c . repo . Path )
2021-02-10 10:00:57 +03:00
if err == nil {
2019-12-16 09:20:25 +03:00
return true , nil
}
2021-02-10 10:00:57 +03:00
var exitError * exec . ExitError
if errors . As ( err , & exitError ) {
if exitError . ProcessState . ExitCode ( ) == 1 && len ( exitError . Stderr ) == 0 {
return false , nil
}
2019-12-16 09:20:25 +03:00
}
2021-02-10 10:00:57 +03:00
return false , err
2019-12-16 09:20:25 +03:00
}
2021-02-10 10:00:57 +03:00
2022-02-06 22:01:47 +03:00
result , err := NewCommand ( c . repo . Ctx , "rev-list" , "--ancestry-path" , "-n1" , that + ".." + this , "--" ) . RunInDir ( c . repo . Path )
2021-02-10 10:00:57 +03:00
if err != nil {
return false , err
}
return len ( strings . TrimSpace ( result ) ) > 0 , nil
2019-12-16 09:20:25 +03:00
}
2016-12-22 12:30:52 +03:00
// CommitsBeforeLimit returns num commits before current revision
2021-08-09 21:08:51 +03:00
func ( c * Commit ) CommitsBeforeLimit ( num int ) ( [ ] * Commit , error ) {
2016-11-04 01:16:01 +03:00
return c . repo . getCommitsBeforeLimit ( c . ID , num )
}
2016-12-22 12:30:52 +03:00
// CommitsBeforeUntil returns the commits between commitID to current revision
2021-08-09 21:08:51 +03:00
func ( c * Commit ) CommitsBeforeUntil ( commitID string ) ( [ ] * Commit , error ) {
2016-11-04 01:16:01 +03:00
endCommit , err := c . repo . GetCommit ( commitID )
if err != nil {
return nil , err
}
return c . repo . CommitsBetween ( c , endCommit )
}
2019-04-12 05:28:44 +03:00
// SearchCommitsOptions specify the parameters for SearchCommits
type SearchCommitsOptions struct {
Keywords [ ] string
Authors , Committers [ ] string
After , Before string
All bool
}
2019-06-12 22:41:28 +03:00
// NewSearchCommitsOptions construct a SearchCommitsOption from a space-delimited search string
2019-04-12 05:28:44 +03:00
func NewSearchCommitsOptions ( searchString string , forAllRefs bool ) SearchCommitsOptions {
var keywords , authors , committers [ ] string
var after , before string
fields := strings . Fields ( searchString )
for _ , k := range fields {
switch {
case strings . HasPrefix ( k , "author:" ) :
authors = append ( authors , strings . TrimPrefix ( k , "author:" ) )
case strings . HasPrefix ( k , "committer:" ) :
committers = append ( committers , strings . TrimPrefix ( k , "committer:" ) )
case strings . HasPrefix ( k , "after:" ) :
after = strings . TrimPrefix ( k , "after:" )
case strings . HasPrefix ( k , "before:" ) :
before = strings . TrimPrefix ( k , "before:" )
default :
keywords = append ( keywords , k )
}
}
return SearchCommitsOptions {
Keywords : keywords ,
Authors : authors ,
Committers : committers ,
After : after ,
Before : before ,
All : forAllRefs ,
}
}
2016-12-22 12:30:52 +03:00
// SearchCommits returns the commits match the keyword before current revision
2021-08-09 21:08:51 +03:00
func ( c * Commit ) SearchCommits ( opts SearchCommitsOptions ) ( [ ] * Commit , error ) {
2019-04-12 05:28:44 +03:00
return c . repo . searchCommits ( c . ID , opts )
2016-11-04 01:16:01 +03:00
}
2016-12-22 12:30:52 +03:00
// GetFilesChangedSinceCommit get all changed file names between pastCommit to current revision
2016-11-04 01:16:01 +03:00
func ( c * Commit ) GetFilesChangedSinceCommit ( pastCommit string ) ( [ ] string , error ) {
return c . repo . getFilesChanged ( pastCommit , c . ID . String ( ) )
}
2019-04-17 19:06:35 +03:00
// FileChangedSinceCommit Returns true if the file given has changed since the the past commit
2019-08-05 23:39:39 +03:00
// YOU MUST ENSURE THAT pastCommit is a valid commit ID.
2019-04-17 19:06:35 +03:00
func ( c * Commit ) FileChangedSinceCommit ( filename , pastCommit string ) ( bool , error ) {
return c . repo . FileChangedBetweenCommits ( filename , pastCommit , c . ID . String ( ) )
}
2019-09-16 12:03:22 +03:00
// HasFile returns true if the file given exists on this commit
// This does only mean it's there - it does not mean the file was changed during the commit.
func ( c * Commit ) HasFile ( filename string ) ( bool , error ) {
2019-10-04 22:58:54 +03:00
_ , err := c . GetBlobByPath ( filename )
if err != nil {
return false , err
}
return true , nil
2019-09-16 12:03:22 +03:00
}
2016-12-22 12:30:52 +03:00
// GetSubModules get all the sub modules of current revision git tree
func ( c * Commit ) GetSubModules ( ) ( * ObjectCache , error ) {
2016-11-04 01:16:01 +03:00
if c . submoduleCache != nil {
return c . submoduleCache , nil
}
entry , err := c . GetTreeEntryByPath ( ".gitmodules" )
if err != nil {
2017-06-23 03:06:43 +03:00
if _ , ok := err . ( ErrNotExist ) ; ok {
return nil , nil
}
2016-11-04 01:16:01 +03:00
return nil , err
}
2019-04-19 15:17:27 +03:00
rd , err := entry . Blob ( ) . DataAsync ( )
2016-11-04 01:16:01 +03:00
if err != nil {
return nil , err
}
2019-04-19 15:17:27 +03:00
defer rd . Close ( )
2016-11-04 01:16:01 +03:00
scanner := bufio . NewScanner ( rd )
c . submoduleCache = newObjectCache ( )
var ismodule bool
var path string
for scanner . Scan ( ) {
if strings . HasPrefix ( scanner . Text ( ) , "[submodule" ) {
ismodule = true
continue
}
if ismodule {
fields := strings . Split ( scanner . Text ( ) , "=" )
k := strings . TrimSpace ( fields [ 0 ] )
if k == "path" {
path = strings . TrimSpace ( fields [ 1 ] )
} else if k == "url" {
c . submoduleCache . Set ( path , & SubModule { path , strings . TrimSpace ( fields [ 1 ] ) } )
ismodule = false
}
}
}
return c . submoduleCache , nil
}
2016-12-22 12:30:52 +03:00
// GetSubModule get the sub module according entryname
2016-11-04 01:16:01 +03:00
func ( c * Commit ) GetSubModule ( entryname string ) ( * SubModule , error ) {
modules , err := c . GetSubModules ( )
if err != nil {
return nil , err
}
2017-06-23 03:06:43 +03:00
if modules != nil {
module , has := modules . Get ( entryname )
if has {
return module . ( * SubModule ) , nil
}
2016-11-04 01:16:01 +03:00
}
return nil , nil
}
2018-09-07 05:06:09 +03:00
2020-06-11 22:42:55 +03:00
// GetBranchName gets the closest branch name (as returned by 'git name-rev --name-only')
2019-04-19 15:17:27 +03:00
func ( c * Commit ) GetBranchName ( ) ( string , error ) {
2020-09-05 19:42:58 +03:00
err := LoadGitVersion ( )
2020-07-28 17:11:05 +03:00
if err != nil {
return "" , fmt . Errorf ( "Git version missing: %v" , err )
}
args := [ ] string {
"name-rev" ,
}
2020-10-21 18:42:08 +03:00
if CheckGitVersionAtLeast ( "2.13.0" ) == nil {
2020-07-28 17:11:05 +03:00
args = append ( args , "--exclude" , "refs/tags/*" )
}
args = append ( args , "--name-only" , "--no-undefined" , c . ID . String ( ) )
2022-02-06 22:01:47 +03:00
data , err := NewCommand ( c . repo . Ctx , args ... ) . RunInDir ( c . repo . Path )
2019-04-19 15:17:27 +03:00
if err != nil {
2020-05-23 22:49:48 +03:00
// handle special case where git can not describe commit
if strings . Contains ( err . Error ( ) , "cannot describe" ) {
return "" , nil
}
2019-04-19 15:17:27 +03:00
return "" , err
}
2020-05-20 15:47:24 +03:00
// name-rev commitID output will be "master" or "master~12"
return strings . SplitN ( strings . TrimSpace ( data ) , "~" , 2 ) [ 0 ] , nil
2019-04-19 15:17:27 +03:00
}
2020-06-24 22:40:52 +03:00
// LoadBranchName load branch name for commit
func ( c * Commit ) LoadBranchName ( ) ( err error ) {
if len ( c . Branch ) != 0 {
return
}
c . Branch , err = c . GetBranchName ( )
return
}
2020-06-11 22:42:55 +03:00
// GetTagName gets the current tag name for given commit
func ( c * Commit ) GetTagName ( ) ( string , error ) {
2022-02-06 22:01:47 +03:00
data , err := NewCommand ( c . repo . Ctx , "describe" , "--exact-match" , "--tags" , "--always" , c . ID . String ( ) ) . RunInDir ( c . repo . Path )
2020-06-11 22:42:55 +03:00
if err != nil {
// handle special case where there is no tag for this commit
if strings . Contains ( err . Error ( ) , "no tag exactly matches" ) {
return "" , nil
}
return "" , err
}
return strings . TrimSpace ( data ) , nil
}
2019-02-03 06:35:17 +03:00
// CommitFileStatus represents status of files in a commit.
type CommitFileStatus struct {
Added [ ] string
Removed [ ] string
Modified [ ] string
}
// NewCommitFileStatus creates a CommitFileStatus
func NewCommitFileStatus ( ) * CommitFileStatus {
return & CommitFileStatus {
[ ] string { } , [ ] string { } , [ ] string { } ,
}
}
2021-07-02 22:23:37 +03:00
func parseCommitFileStatus ( fileStatus * CommitFileStatus , stdout io . Reader ) {
rd := bufio . NewReader ( stdout )
peek , err := rd . Peek ( 1 )
if err != nil {
if err != io . EOF {
log . Error ( "Unexpected error whilst reading from git log --name-status. Error: %v" , err )
}
return
}
if peek [ 0 ] == '\n' || peek [ 0 ] == '\x00' {
_ , _ = rd . Discard ( 1 )
}
for {
modifier , err := rd . ReadSlice ( '\x00' )
if err != nil {
if err != io . EOF {
log . Error ( "Unexpected error whilst reading from git log --name-status. Error: %v" , err )
}
return
}
file , err := rd . ReadString ( '\x00' )
if err != nil {
if err != io . EOF {
log . Error ( "Unexpected error whilst reading from git log --name-status. Error: %v" , err )
}
return
}
file = file [ : len ( file ) - 1 ]
switch modifier [ 0 ] {
case 'A' :
fileStatus . Added = append ( fileStatus . Added , file )
case 'D' :
fileStatus . Removed = append ( fileStatus . Removed , file )
case 'M' :
fileStatus . Modified = append ( fileStatus . Modified , file )
}
}
}
2019-02-03 06:35:17 +03:00
// GetCommitFileStatus returns file status of commit in given repository.
2022-01-20 02:26:57 +03:00
func GetCommitFileStatus ( ctx context . Context , repoPath , commitID string ) ( * CommitFileStatus , error ) {
2019-02-03 06:35:17 +03:00
stdout , w := io . Pipe ( )
done := make ( chan struct { } )
fileStatus := NewCommitFileStatus ( )
go func ( ) {
2021-07-02 22:23:37 +03:00
parseCommitFileStatus ( fileStatus , stdout )
close ( done )
2019-02-03 06:35:17 +03:00
} ( )
stderr := new ( bytes . Buffer )
2021-07-02 22:23:37 +03:00
args := [ ] string { "log" , "--name-status" , "-c" , "--pretty=format:" , "--parents" , "--no-renames" , "-z" , "-1" , commitID }
2022-02-11 15:47:22 +03:00
err := NewCommand ( ctx , args ... ) . RunWithContext ( & RunContext {
Timeout : - 1 ,
Dir : repoPath ,
Stdout : w ,
Stderr : stderr ,
} )
2019-02-03 06:35:17 +03:00
w . Close ( ) // Close writer to exit parsing goroutine
if err != nil {
2020-12-17 17:00:47 +03:00
return nil , ConcatenateError ( err , stderr . String ( ) )
2019-02-03 06:35:17 +03:00
}
<- done
return fileStatus , nil
}
2018-09-07 05:06:09 +03:00
// GetFullCommitID returns full length (40) of commit ID by given short SHA in a repository.
2022-01-20 02:26:57 +03:00
func GetFullCommitID ( ctx context . Context , repoPath , shortID string ) ( string , error ) {
2022-02-06 22:01:47 +03:00
commitID , err := NewCommand ( ctx , "rev-parse" , shortID ) . RunInDir ( repoPath )
2018-09-07 05:06:09 +03:00
if err != nil {
if strings . Contains ( err . Error ( ) , "exit status 128" ) {
return "" , ErrNotExist { shortID , "" }
}
return "" , err
}
return strings . TrimSpace ( commitID ) , nil
}
2019-10-16 16:42:42 +03:00
// GetRepositoryDefaultPublicGPGKey returns the default public key for this commit
func ( c * Commit ) GetRepositoryDefaultPublicGPGKey ( forceUpdate bool ) ( * GPGSettings , error ) {
if c . repo == nil {
return nil , nil
}
return c . repo . GetDefaultPublicGPGKey ( forceUpdate )
}