2016-11-04 01:16:01 +03:00
// Copyright 2015 The Gogs Authors. All rights reserved.
2017-05-30 12:32:01 +03:00
// Copyright 2017 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"
2020-12-02 21:36:06 +03:00
"context"
2019-05-05 19:25:25 +03:00
"fmt"
2016-11-04 01:16:01 +03:00
"os"
"path"
2019-05-05 19:25:25 +03:00
"strconv"
2017-04-08 05:23:39 +03:00
"strings"
2016-11-04 01:16:01 +03:00
"time"
)
2019-10-16 16:42:42 +03:00
// GPGSettings represents the default GPG settings for this repository
type GPGSettings struct {
Sign bool
KeyID string
Email string
Name string
PublicKeyContent string
2016-11-04 01:16:01 +03:00
}
2016-12-22 12:30:52 +03:00
const prettyLogFormat = ` --pretty=format:%H `
2016-11-04 01:16:01 +03:00
2019-11-07 21:09:51 +03:00
// GetAllCommitsCount returns count of all commits in repository
func ( repo * Repository ) GetAllCommitsCount ( ) ( int64 , error ) {
2020-11-08 20:21:54 +03:00
return AllCommitsCount ( repo . Path , false )
2019-11-07 21:09:51 +03:00
}
2016-11-04 01:16:01 +03:00
func ( repo * Repository ) parsePrettyFormatLogToList ( logs [ ] byte ) ( * list . List , error ) {
l := list . New ( )
if len ( logs ) == 0 {
return l , nil
}
parts := bytes . Split ( logs , [ ] byte { '\n' } )
2016-12-22 12:30:52 +03:00
for _ , commitID := range parts {
commit , err := repo . GetCommit ( string ( commitID ) )
2016-11-04 01:16:01 +03:00
if err != nil {
return nil , err
}
l . PushBack ( commit )
}
return l , nil
}
// IsRepoURLAccessible checks if given repository URL is accessible.
func IsRepoURLAccessible ( url string ) bool {
_ , err := NewCommand ( "ls-remote" , "-q" , "-h" , url , "HEAD" ) . Run ( )
2019-06-12 22:41:28 +03:00
return err == nil
2016-11-04 01:16:01 +03:00
}
// InitRepository initializes a new Git repository.
func InitRepository ( repoPath string , bare bool ) error {
2019-06-12 22:41:28 +03:00
err := os . MkdirAll ( repoPath , os . ModePerm )
if err != nil {
return err
}
2016-11-04 01:16:01 +03:00
cmd := NewCommand ( "init" )
if bare {
cmd . AddArguments ( "--bare" )
}
2019-06-12 22:41:28 +03:00
_ , err = cmd . RunInDir ( repoPath )
2016-11-04 01:16:01 +03:00
return err
}
2019-06-26 21:15:26 +03:00
// IsEmpty Check if repository is empty.
func ( repo * Repository ) IsEmpty ( ) ( bool , error ) {
var errbuf strings . Builder
if err := NewCommand ( "log" , "-1" ) . RunInDirPipeline ( repo . Path , nil , & errbuf ) ; err != nil {
if strings . Contains ( errbuf . String ( ) , "fatal: bad default revision 'HEAD'" ) ||
strings . Contains ( errbuf . String ( ) , "fatal: your current branch 'master' does not have any commits yet" ) {
return true , nil
}
return true , fmt . Errorf ( "check empty: %v - %s" , err , errbuf . String ( ) )
}
return false , nil
}
2016-12-22 12:30:52 +03:00
// CloneRepoOptions options when clone a repository
2016-11-04 01:16:01 +03:00
type CloneRepoOptions struct {
2019-05-11 18:29:17 +03:00
Timeout time . Duration
Mirror bool
Bare bool
Quiet bool
Branch string
Shared bool
NoCheckout bool
2019-11-30 09:54:47 +03:00
Depth int
2016-11-04 01:16:01 +03:00
}
// Clone clones original repository to target path.
func Clone ( from , to string , opts CloneRepoOptions ) ( err error ) {
2020-12-02 21:36:06 +03:00
return CloneWithContext ( DefaultContext , from , to , opts )
}
// CloneWithContext clones original repository to target path.
func CloneWithContext ( ctx context . Context , from , to string , opts CloneRepoOptions ) ( err error ) {
2019-11-27 03:35:52 +03:00
cargs := make ( [ ] string , len ( GlobalCommandArgs ) )
copy ( cargs , GlobalCommandArgs )
2020-12-02 21:36:06 +03:00
return CloneWithArgs ( ctx , from , to , cargs , opts )
2019-11-27 03:35:52 +03:00
}
// CloneWithArgs original repository to target path.
2020-12-02 21:36:06 +03:00
func CloneWithArgs ( ctx context . Context , from , to string , args [ ] string , opts CloneRepoOptions ) ( err error ) {
2016-11-04 01:16:01 +03:00
toDir := path . Dir ( to )
if err = os . MkdirAll ( toDir , os . ModePerm ) ; err != nil {
return err
}
2020-12-02 21:36:06 +03:00
cmd := NewCommandContextNoGlobals ( ctx , args ... ) . AddArguments ( "clone" )
2016-11-04 01:16:01 +03:00
if opts . Mirror {
cmd . AddArguments ( "--mirror" )
}
if opts . Bare {
cmd . AddArguments ( "--bare" )
}
if opts . Quiet {
cmd . AddArguments ( "--quiet" )
}
2019-05-11 18:29:17 +03:00
if opts . Shared {
cmd . AddArguments ( "-s" )
}
if opts . NoCheckout {
cmd . AddArguments ( "--no-checkout" )
}
2019-11-30 09:54:47 +03:00
if opts . Depth > 0 {
cmd . AddArguments ( "--depth" , strconv . Itoa ( opts . Depth ) )
}
2019-05-11 18:29:17 +03:00
2016-11-04 01:16:01 +03:00
if len ( opts . Branch ) > 0 {
cmd . AddArguments ( "-b" , opts . Branch )
}
2019-05-11 18:29:17 +03:00
cmd . AddArguments ( "--" , from , to )
2016-11-04 01:16:01 +03:00
if opts . Timeout <= 0 {
opts . Timeout = - 1
}
_ , err = cmd . RunTimeout ( opts . Timeout )
return err
}
2016-12-22 12:30:52 +03:00
// PullRemoteOptions options when pull from remote
2016-11-04 01:16:01 +03:00
type PullRemoteOptions struct {
Timeout time . Duration
All bool
2016-11-12 14:09:25 +03:00
Rebase bool
2016-11-04 01:16:01 +03:00
Remote string
Branch string
}
// Pull pulls changes from remotes.
func Pull ( repoPath string , opts PullRemoteOptions ) error {
cmd := NewCommand ( "pull" )
2016-11-12 14:09:25 +03:00
if opts . Rebase {
cmd . AddArguments ( "--rebase" )
}
2016-11-04 01:16:01 +03:00
if opts . All {
cmd . AddArguments ( "--all" )
} else {
2019-08-05 23:39:39 +03:00
cmd . AddArguments ( "--" , opts . Remote , opts . Branch )
2016-11-04 01:16:01 +03:00
}
if opts . Timeout <= 0 {
opts . Timeout = - 1
}
_ , err := cmd . RunInDirTimeout ( opts . Timeout , repoPath )
return err
}
2017-05-30 12:32:01 +03:00
// PushOptions options when push to remote
type PushOptions struct {
Remote string
Branch string
Force bool
2019-05-11 18:29:17 +03:00
Env [ ] string
2017-05-30 12:32:01 +03:00
}
2016-11-04 01:16:01 +03:00
// Push pushs local commits to given remote branch.
2017-05-30 12:32:01 +03:00
func Push ( repoPath string , opts PushOptions ) error {
cmd := NewCommand ( "push" )
if opts . Force {
cmd . AddArguments ( "-f" )
}
2019-08-05 23:39:39 +03:00
cmd . AddArguments ( "--" , opts . Remote , opts . Branch )
2020-03-28 07:13:18 +03:00
var outbuf , errbuf strings . Builder
err := cmd . RunInDirTimeoutEnvPipeline ( opts . Env , - 1 , repoPath , & outbuf , & errbuf )
if err != nil {
if strings . Contains ( errbuf . String ( ) , "non-fast-forward" ) {
return & ErrPushOutOfDate {
StdOut : outbuf . String ( ) ,
StdErr : errbuf . String ( ) ,
Err : err ,
}
} else if strings . Contains ( errbuf . String ( ) , "! [remote rejected]" ) {
err := & ErrPushRejected {
StdOut : outbuf . String ( ) ,
StdErr : errbuf . String ( ) ,
Err : err ,
}
err . GenerateMessage ( )
return err
}
}
if errbuf . Len ( ) > 0 && err != nil {
return fmt . Errorf ( "%v - %s" , err , errbuf . String ( ) )
}
2016-11-04 01:16:01 +03:00
return err
}
2016-12-22 12:30:52 +03:00
// CheckoutOptions options when heck out some branch
2016-11-04 01:16:01 +03:00
type CheckoutOptions struct {
Timeout time . Duration
Branch string
OldBranch string
}
// Checkout checkouts a branch
func Checkout ( repoPath string , opts CheckoutOptions ) error {
cmd := NewCommand ( "checkout" )
if len ( opts . OldBranch ) > 0 {
cmd . AddArguments ( "-b" )
}
if opts . Timeout <= 0 {
opts . Timeout = - 1
}
cmd . AddArguments ( opts . Branch )
if len ( opts . OldBranch ) > 0 {
cmd . AddArguments ( opts . OldBranch )
}
_ , err := cmd . RunInDirTimeout ( opts . Timeout , repoPath )
return err
}
// ResetHEAD resets HEAD to given revision or head of branch.
func ResetHEAD ( repoPath string , hard bool , revision string ) error {
cmd := NewCommand ( "reset" )
if hard {
cmd . AddArguments ( "--hard" )
}
_ , err := cmd . AddArguments ( revision ) . RunInDir ( repoPath )
return err
}
// MoveFile moves a file to another file or directory.
func MoveFile ( repoPath , oldTreeName , newTreeName string ) error {
_ , err := NewCommand ( "mv" ) . AddArguments ( oldTreeName , newTreeName ) . RunInDir ( repoPath )
return err
}
2017-04-08 05:23:39 +03:00
// CountObject represents repository count objects report
type CountObject struct {
Count int64
Size int64
InPack int64
Packs int64
SizePack int64
PrunePack int64
Garbage int64
SizeGarbage int64
}
const (
statCount = "count: "
statSize = "size: "
statInpack = "in-pack: "
statPacks = "packs: "
statSizePack = "size-pack: "
statPrunePackage = "prune-package: "
statGarbage = "garbage: "
statSizeGarbage = "size-garbage: "
)
2019-11-11 00:33:47 +03:00
// CountObjects returns the results of git count-objects on the repoPath
func CountObjects ( repoPath string ) ( * CountObject , error ) {
2017-04-08 05:23:39 +03:00
cmd := NewCommand ( "count-objects" , "-v" )
stdout , err := cmd . RunInDir ( repoPath )
if err != nil {
return nil , err
}
return parseSize ( stdout ) , nil
}
// parseSize parses the output from count-objects and return a CountObject
func parseSize ( objects string ) * CountObject {
repoSize := new ( CountObject )
for _ , line := range strings . Split ( objects , "\n" ) {
switch {
case strings . HasPrefix ( line , statCount ) :
2020-12-25 12:59:32 +03:00
repoSize . Count , _ = strconv . ParseInt ( line [ 7 : ] , 10 , 64 )
2017-04-08 05:23:39 +03:00
case strings . HasPrefix ( line , statSize ) :
2020-12-25 12:59:32 +03:00
repoSize . Size , _ = strconv . ParseInt ( line [ 6 : ] , 10 , 64 )
repoSize . Size *= 1024
2017-04-08 05:23:39 +03:00
case strings . HasPrefix ( line , statInpack ) :
2020-12-25 12:59:32 +03:00
repoSize . InPack , _ = strconv . ParseInt ( line [ 9 : ] , 10 , 64 )
2017-04-08 05:23:39 +03:00
case strings . HasPrefix ( line , statPacks ) :
2020-12-25 12:59:32 +03:00
repoSize . Packs , _ = strconv . ParseInt ( line [ 7 : ] , 10 , 64 )
2017-04-08 05:23:39 +03:00
case strings . HasPrefix ( line , statSizePack ) :
2020-12-25 12:59:32 +03:00
repoSize . Count , _ = strconv . ParseInt ( line [ 11 : ] , 10 , 64 )
repoSize . Count *= 1024
2017-04-08 05:23:39 +03:00
case strings . HasPrefix ( line , statPrunePackage ) :
2020-12-25 12:59:32 +03:00
repoSize . PrunePack , _ = strconv . ParseInt ( line [ 16 : ] , 10 , 64 )
2017-04-08 05:23:39 +03:00
case strings . HasPrefix ( line , statGarbage ) :
2020-12-25 12:59:32 +03:00
repoSize . Garbage , _ = strconv . ParseInt ( line [ 9 : ] , 10 , 64 )
2017-04-08 05:23:39 +03:00
case strings . HasPrefix ( line , statSizeGarbage ) :
2020-12-25 12:59:32 +03:00
repoSize . SizeGarbage , _ = strconv . ParseInt ( line [ 14 : ] , 10 , 64 )
repoSize . SizeGarbage *= 1024
2017-04-08 05:23:39 +03:00
}
}
return repoSize
}
2017-05-30 12:32:01 +03:00
// GetLatestCommitTime returns time for latest commit in repository (across all branches)
func GetLatestCommitTime ( repoPath string ) ( time . Time , error ) {
cmd := NewCommand ( "for-each-ref" , "--sort=-committerdate" , "refs/heads/" , "--count" , "1" , "--format=%(committerdate)" )
stdout , err := cmd . RunInDir ( repoPath )
if err != nil {
return time . Time { } , err
}
commitTime := strings . TrimSpace ( stdout )
2017-12-11 05:23:34 +03:00
return time . Parse ( GitTimeLayout , commitTime )
2017-05-30 12:32:01 +03:00
}
2019-05-05 19:25:25 +03:00
// DivergeObject represents commit count diverging commits
type DivergeObject struct {
Ahead int
Behind int
}
func checkDivergence ( repoPath string , baseBranch string , targetBranch string ) ( int , error ) {
branches := fmt . Sprintf ( "%s..%s" , baseBranch , targetBranch )
cmd := NewCommand ( "rev-list" , "--count" , branches )
stdout , err := cmd . RunInDir ( repoPath )
if err != nil {
return - 1 , err
}
outInteger , errInteger := strconv . Atoi ( strings . Trim ( stdout , "\n" ) )
if errInteger != nil {
return - 1 , errInteger
}
return outInteger , nil
}
// GetDivergingCommits returns the number of commits a targetBranch is ahead or behind a baseBranch
func GetDivergingCommits ( repoPath string , baseBranch string , targetBranch string ) ( DivergeObject , error ) {
// $(git rev-list --count master..feature) commits ahead of master
ahead , errorAhead := checkDivergence ( repoPath , baseBranch , targetBranch )
if errorAhead != nil {
return DivergeObject { } , errorAhead
}
// $(git rev-list --count feature..master) commits behind master
behind , errorBehind := checkDivergence ( repoPath , targetBranch , baseBranch )
if errorBehind != nil {
return DivergeObject { } , errorBehind
}
return DivergeObject { ahead , behind } , nil
}