2016-11-03 23:16:01 +01:00
// Copyright 2015 The Gogs Authors. All rights reserved.
2019-02-03 11:35:17 +08:00
// Copyright 2018 The Gitea Authors. All rights reserved.
2016-11-03 23:16:01 +01: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 11:43:54 +01:00
"bytes"
2016-11-03 23:16:01 +01:00
"container/list"
"fmt"
2019-09-16 11:03:22 +02:00
"image"
"image/color"
_ "image/gif" // for processing gif images
_ "image/jpeg" // for processing jpeg images
_ "image/png" // for processing png images
2019-02-03 11:35:17 +08:00
"io"
2016-11-03 23:16:01 +01:00
"net/http"
"strconv"
"strings"
)
// Commit represents a git commit.
type Commit struct {
2019-02-05 22:47:01 +01:00
Branch string // Branch this commit belongs to
2016-11-03 23:16:01 +01:00
Tree
2016-12-22 17:30:52 +08:00
ID SHA1 // The ID of this commit object
2016-11-03 23:16:01 +01:00
Author * Signature
Committer * Signature
CommitMessage string
2017-03-22 11:43:54 +01:00
Signature * CommitGPGSignature
2016-11-03 23:16:01 +01:00
2020-01-15 08:32:57 +00:00
Parents [ ] SHA1 // SHA1 strings
2016-12-22 17:30:52 +08:00
submoduleCache * ObjectCache
2016-11-03 23:16:01 +01:00
}
2017-03-22 11:43:54 +01:00
// CommitGPGSignature represents a git commit signature part.
type CommitGPGSignature struct {
Signature string
Payload string //TODO check if can be reconstruct from the rest of commit information to not have duplicate data
}
2016-11-03 23:16:01 +01: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 10:05:01 -05:00
return strings . Split ( strings . TrimSpace ( c . CommitMessage ) , "\n" ) [ 0 ]
2016-11-03 23:16:01 +01:00
}
// ParentID returns oid of n-th parent (0-based index).
// It returns nil if no such parent exists.
2016-12-22 17:30:52 +08:00
func ( c * Commit ) ParentID ( n int ) ( SHA1 , error ) {
2020-01-15 08:32:57 +00:00
if n >= len ( c . Parents ) {
2016-12-22 17:30:52 +08:00
return SHA1 { } , ErrNotExist { "" , "" }
2016-11-03 23:16:01 +01:00
}
2020-01-15 08:32:57 +00:00
return c . Parents [ n ] , nil
2016-11-03 23:16:01 +01: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 08:32:57 +00:00
return len ( c . Parents )
2016-11-03 23:16:01 +01:00
}
func isImageFile ( data [ ] byte ) ( string , bool ) {
contentType := http . DetectContentType ( data )
2019-06-12 21:41:28 +02:00
if strings . Contains ( contentType , "image/" ) {
2016-11-03 23:16:01 +01:00
return contentType , true
}
return contentType , false
}
2016-12-22 17:30:52 +08:00
// IsImageFile is a file image type
2016-11-03 23:16:01 +01:00
func ( c * Commit ) IsImageFile ( name string ) bool {
blob , err := c . GetBlobByPath ( name )
if err != nil {
return false
}
2017-11-29 02:50:39 +01:00
dataRc , err := blob . DataAsync ( )
2016-11-03 23:16:01 +01:00
if err != nil {
return false
}
2017-11-29 02:50:39 +01:00
defer dataRc . Close ( )
2016-11-03 23:16:01 +01:00
buf := make ( [ ] byte , 1024 )
n , _ := dataRc . Read ( buf )
buf = buf [ : n ]
_ , isImage := isImageFile ( buf )
return isImage
}
2019-09-16 11:03:22 +02:00
// ImageMetaData represents metadata of an image file
type ImageMetaData struct {
ColorModel color . Model
Width int
Height int
ByteSize int64
}
// ImageInfo returns information about the dimensions of an image
func ( c * Commit ) ImageInfo ( name string ) ( * ImageMetaData , error ) {
if ! c . IsImageFile ( name ) {
return nil , nil
}
blob , err := c . GetBlobByPath ( name )
if err != nil {
return nil , err
}
reader , err := blob . DataAsync ( )
if err != nil {
return nil , err
}
defer reader . Close ( )
config , _ , err := image . DecodeConfig ( reader )
if err != nil {
return nil , err
}
metadata := ImageMetaData {
ColorModel : config . ColorModel ,
Width : config . Width ,
Height : config . Height ,
ByteSize : blob . Size ( ) ,
}
return & metadata , nil
}
2016-11-03 23:16:01 +01: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 17:30:52 +08:00
// AddChanges marks local changes to be ready for commit.
2016-11-03 23:16:01 +01:00
func AddChanges ( repoPath string , all bool , files ... string ) error {
2019-11-27 08:35:52 +08:00
return AddChangesWithArgs ( repoPath , GlobalCommandArgs , all , files ... )
}
// AddChangesWithArgs marks local changes to be ready for commit.
func AddChangesWithArgs ( repoPath string , gloablArgs [ ] string , all bool , files ... string ) error {
cmd := NewCommandNoGlobals ( append ( gloablArgs , "add" ) ... )
2016-11-03 23:16:01 +01:00
if all {
cmd . AddArguments ( "--all" )
}
2019-08-05 21:39:39 +01:00
cmd . AddArguments ( "--" )
2016-11-03 23:16:01 +01:00
_ , err := cmd . AddArguments ( files ... ) . RunInDir ( repoPath )
return err
}
2016-12-22 17:30:52 +08:00
// CommitChangesOptions the options when a commit created
2016-11-03 23:16:01 +01: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 {
2019-11-27 08:35:52 +08:00
cargs := make ( [ ] string , len ( GlobalCommandArgs ) )
copy ( cargs , GlobalCommandArgs )
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-03 23:16:01 +01: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
2020-11-08 17:21:54 +00:00
func AllCommitsCount ( repoPath string , hidePRRefs bool , files ... string ) ( int64 , error ) {
args := [ ] string { "--all" , "--count" }
if hidePRRefs {
args = append ( [ ] string { "--exclude=refs/pull/*" } , args ... )
}
cmd := NewCommand ( "rev-list" )
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 17:21:54 +00:00
// CommitsCountFiles returns number of total commits of until given revision.
func CommitsCountFiles ( repoPath string , revision , relpath [ ] string ) ( int64 , error ) {
2019-06-12 21:41:28 +02:00
cmd := NewCommand ( "rev-list" , "--count" )
2020-07-29 18:53:04 +01:00
cmd . AddArguments ( revision ... )
2016-11-03 23:16:01 +01:00
if len ( relpath ) > 0 {
2020-07-29 18:53:04 +01:00
cmd . AddArguments ( "--" )
cmd . AddArguments ( relpath ... )
2016-11-03 23:16:01 +01: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.
2020-11-08 17:21:54 +00:00
func CommitsCount ( repoPath string , revision ... string ) ( int64 , error ) {
return CommitsCountFiles ( repoPath , revision , [ ] string { } )
2016-11-03 23:16:01 +01:00
}
2016-12-22 17:30:52 +08:00
// CommitsCount returns number of total commits of until current revision.
2016-11-03 23:16:01 +01:00
func ( c * Commit ) CommitsCount ( ) ( int64 , error ) {
return CommitsCount ( c . repo . Path , c . ID . String ( ) )
}
2016-12-22 17:30:52 +08:00
// CommitsByRange returns the specific page commits before current revision, every page's number default by CommitsRangeSize
2020-01-24 19:00:29 +00:00
func ( c * Commit ) CommitsByRange ( page , pageSize int ) ( * list . List , error ) {
return c . repo . commitsByRange ( c . ID , page , pageSize )
2016-11-03 23:16:01 +01:00
}
2016-12-22 17:30:52 +08:00
// CommitsBefore returns all the commits before current revision
2016-11-03 23:16:01 +01:00
func ( c * Commit ) CommitsBefore ( ) ( * list . List , error ) {
return c . repo . getCommitsBefore ( c . ID )
}
2019-12-16 07:20:25 +01:00
// HasPreviousCommit returns true if a given commitHash is contained in commit's parents
func ( c * Commit ) HasPreviousCommit ( commitHash SHA1 ) ( bool , error ) {
for i := 0 ; i < c . ParentCount ( ) ; i ++ {
commit , err := c . Parent ( i )
if err != nil {
return false , err
}
if commit . ID == commitHash {
return true , nil
}
commitInParentCommit , err := commit . HasPreviousCommit ( commitHash )
if err != nil {
return false , err
}
if commitInParentCommit {
return true , nil
}
}
return false , nil
}
2016-12-22 17:30:52 +08:00
// CommitsBeforeLimit returns num commits before current revision
2016-11-03 23:16:01 +01:00
func ( c * Commit ) CommitsBeforeLimit ( num int ) ( * list . List , error ) {
return c . repo . getCommitsBeforeLimit ( c . ID , num )
}
2016-12-22 17:30:52 +08:00
// CommitsBeforeUntil returns the commits between commitID to current revision
2016-11-03 23:16:01 +01:00
func ( c * Commit ) CommitsBeforeUntil ( commitID string ) ( * list . List , error ) {
endCommit , err := c . repo . GetCommit ( commitID )
if err != nil {
return nil , err
}
return c . repo . CommitsBetween ( c , endCommit )
}
2019-04-12 10:28:44 +08:00
// SearchCommitsOptions specify the parameters for SearchCommits
type SearchCommitsOptions struct {
Keywords [ ] string
Authors , Committers [ ] string
After , Before string
All bool
}
2019-06-12 21:41:28 +02:00
// NewSearchCommitsOptions construct a SearchCommitsOption from a space-delimited search string
2019-04-12 10:28:44 +08: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 17:30:52 +08:00
// SearchCommits returns the commits match the keyword before current revision
2019-04-12 10:28:44 +08:00
func ( c * Commit ) SearchCommits ( opts SearchCommitsOptions ) ( * list . List , error ) {
return c . repo . searchCommits ( c . ID , opts )
2016-11-03 23:16:01 +01:00
}
2016-12-22 17:30:52 +08:00
// GetFilesChangedSinceCommit get all changed file names between pastCommit to current revision
2016-11-03 23:16:01 +01:00
func ( c * Commit ) GetFilesChangedSinceCommit ( pastCommit string ) ( [ ] string , error ) {
return c . repo . getFilesChanged ( pastCommit , c . ID . String ( ) )
}
2019-04-17 10:06:35 -06:00
// FileChangedSinceCommit Returns true if the file given has changed since the the past commit
2019-08-05 21:39:39 +01:00
// YOU MUST ENSURE THAT pastCommit is a valid commit ID.
2019-04-17 10:06:35 -06:00
func ( c * Commit ) FileChangedSinceCommit ( filename , pastCommit string ) ( bool , error ) {
return c . repo . FileChangedBetweenCommits ( filename , pastCommit , c . ID . String ( ) )
}
2019-09-16 11:03:22 +02: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 21:58:54 +02:00
_ , err := c . GetBlobByPath ( filename )
if err != nil {
return false , err
}
return true , nil
2019-09-16 11:03:22 +02:00
}
2016-12-22 17:30:52 +08:00
// GetSubModules get all the sub modules of current revision git tree
func ( c * Commit ) GetSubModules ( ) ( * ObjectCache , error ) {
2016-11-03 23:16:01 +01: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-03 23:16:01 +01:00
return nil , err
}
2019-04-19 14:17:27 +02:00
rd , err := entry . Blob ( ) . DataAsync ( )
2016-11-03 23:16:01 +01:00
if err != nil {
return nil , err
}
2019-04-19 14:17:27 +02:00
defer rd . Close ( )
2016-11-03 23:16:01 +01: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 17:30:52 +08:00
// GetSubModule get the sub module according entryname
2016-11-03 23:16:01 +01: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-03 23:16:01 +01:00
}
return nil , nil
}
2018-09-07 05:06:09 +03:00
2020-06-11 21:42:55 +02:00
// GetBranchName gets the closest branch name (as returned by 'git name-rev --name-only')
2019-04-19 14:17:27 +02:00
func ( c * Commit ) GetBranchName ( ) ( string , error ) {
2020-09-05 18:42:58 +02:00
err := LoadGitVersion ( )
2020-07-28 15:11:05 +01:00
if err != nil {
return "" , fmt . Errorf ( "Git version missing: %v" , err )
}
args := [ ] string {
"name-rev" ,
}
2020-10-21 16:42:08 +01:00
if CheckGitVersionAtLeast ( "2.13.0" ) == nil {
2020-07-28 15:11:05 +01:00
args = append ( args , "--exclude" , "refs/tags/*" )
}
args = append ( args , "--name-only" , "--no-undefined" , c . ID . String ( ) )
data , err := NewCommand ( args ... ) . RunInDir ( c . repo . Path )
2019-04-19 14:17:27 +02:00
if err != nil {
2020-05-23 21:49:48 +02:00
// handle special case where git can not describe commit
if strings . Contains ( err . Error ( ) , "cannot describe" ) {
return "" , nil
}
2019-04-19 14:17:27 +02:00
return "" , err
}
2020-05-20 20:47:24 +08:00
// name-rev commitID output will be "master" or "master~12"
return strings . SplitN ( strings . TrimSpace ( data ) , "~" , 2 ) [ 0 ] , nil
2019-04-19 14:17:27 +02:00
}
2020-06-25 03:40:52 +08: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 21:42:55 +02:00
// GetTagName gets the current tag name for given commit
func ( c * Commit ) GetTagName ( ) ( string , error ) {
2020-06-12 14:02:14 -04:00
data , err := NewCommand ( "describe" , "--exact-match" , "--tags" , "--always" , c . ID . String ( ) ) . RunInDir ( c . repo . Path )
2020-06-11 21:42:55 +02: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 11:35:17 +08: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 { } ,
}
}
// GetCommitFileStatus returns file status of commit in given repository.
func GetCommitFileStatus ( repoPath , commitID string ) ( * CommitFileStatus , error ) {
stdout , w := io . Pipe ( )
done := make ( chan struct { } )
fileStatus := NewCommitFileStatus ( )
go func ( ) {
scanner := bufio . NewScanner ( stdout )
for scanner . Scan ( ) {
fields := strings . Fields ( scanner . Text ( ) )
if len ( fields ) < 2 {
continue
}
switch fields [ 0 ] [ 0 ] {
case 'A' :
fileStatus . Added = append ( fileStatus . Added , fields [ 1 ] )
case 'D' :
fileStatus . Removed = append ( fileStatus . Removed , fields [ 1 ] )
case 'M' :
fileStatus . Modified = append ( fileStatus . Modified , fields [ 1 ] )
}
}
done <- struct { } { }
} ( )
stderr := new ( bytes . Buffer )
err := NewCommand ( "show" , "--name-status" , "--pretty=format:''" , commitID ) . RunInDirPipeline ( repoPath , w , stderr )
w . Close ( ) // Close writer to exit parsing goroutine
if err != nil {
2020-12-17 14:00:47 +00:00
return nil , ConcatenateError ( err , stderr . String ( ) )
2019-02-03 11:35:17 +08: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.
func GetFullCommitID ( repoPath , shortID string ) ( string , error ) {
commitID , err := NewCommand ( "rev-parse" , shortID ) . RunInDir ( repoPath )
if err != nil {
if strings . Contains ( err . Error ( ) , "exit status 128" ) {
return "" , ErrNotExist { shortID , "" }
}
return "" , err
}
return strings . TrimSpace ( commitID ) , nil
}
2019-10-16 14:42:42 +01: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 )
}