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"
2020-12-02 21:36:06 +03:00
"context"
2019-05-05 19:25:25 +03:00
"fmt"
2021-08-18 16:10:39 +03:00
"io"
"net/url"
2016-11-04 01:16:01 +03:00
"os"
"path"
2021-08-24 19:47:09 +03:00
"path/filepath"
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"
2021-08-18 16:10:39 +03:00
"code.gitea.io/gitea/modules/proxy"
2016-11-04 01:16:01 +03:00
)
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 ) {
2022-01-20 02:26:57 +03:00
return AllCommitsCount ( repo . Ctx , repo . Path , false )
2019-11-07 21:09:51 +03:00
}
2021-08-09 21:08:51 +03:00
func ( repo * Repository ) parsePrettyFormatLogToList ( logs [ ] byte ) ( [ ] * Commit , error ) {
var commits [ ] * Commit
2016-11-04 01:16:01 +03:00
if len ( logs ) == 0 {
2021-08-09 21:08:51 +03:00
return commits , nil
2016-11-04 01:16:01 +03:00
}
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
}
2021-08-09 21:08:51 +03:00
commits = append ( commits , commit )
2016-11-04 01:16:01 +03:00
}
2021-08-09 21:08:51 +03:00
return commits , nil
2016-11-04 01:16:01 +03:00
}
// IsRepoURLAccessible checks if given repository URL is accessible.
2022-01-20 02:26:57 +03:00
func IsRepoURLAccessible ( ctx context . Context , url string ) bool {
2022-02-06 22:01:47 +03:00
_ , err := NewCommand ( ctx , "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.
2022-01-20 02:26:57 +03:00
func InitRepository ( ctx context . Context , 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
2022-02-06 22:01:47 +03:00
cmd := NewCommand ( ctx , "init" )
2016-11-04 01:16:01 +03:00
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 ) {
2022-01-28 05:51:16 +03:00
var errbuf , output strings . Builder
2022-02-06 22:01:47 +03:00
if err := NewCommand ( repo . Ctx , "rev-list" , "--all" , "--count" , "--max-count=1" ) .
2022-01-28 05:51:16 +03:00
RunWithContext ( & RunContext {
Timeout : - 1 ,
Dir : repo . Path ,
Stdout : & output ,
Stderr : & errbuf ,
} ) ; err != nil {
2019-06-26 21:15:26 +03:00
return true , fmt . Errorf ( "check empty: %v - %s" , err , errbuf . String ( ) )
}
2022-01-28 05:51:16 +03:00
c , err := strconv . Atoi ( strings . TrimSpace ( output . String ( ) ) )
if err != nil {
return true , fmt . Errorf ( "check empty: convert %s to count failed: %v" , output . String ( ) , err )
}
return c == 0 , nil
2019-06-26 21:15:26 +03:00
}
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
2022-01-24 00:19:32 +03:00
Filter string
2016-11-04 01:16:01 +03:00
}
// Clone clones original repository to target path.
2022-01-20 02:26:57 +03:00
func Clone ( ctx context . Context , from , to string , opts CloneRepoOptions ) error {
2022-01-25 21:15:58 +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 ) )
}
2022-01-24 00:19:32 +03:00
if opts . Filter != "" {
cmd . AddArguments ( "--filter" , opts . Filter )
}
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
}
2022-01-20 20:46:10 +03:00
envs := os . Environ ( )
2021-08-18 16:10:39 +03:00
u , err := url . Parse ( from )
if err == nil && ( strings . EqualFold ( u . Scheme , "http" ) || strings . EqualFold ( u . Scheme , "https" ) ) {
if proxy . Match ( u . Host ) {
envs = append ( envs , fmt . Sprintf ( "https_proxy=%s" , proxy . GetProxyURL ( ) ) )
}
}
2022-01-20 20:46:10 +03:00
stderr := new ( bytes . Buffer )
2021-08-18 16:10:39 +03:00
if err = cmd . RunWithContext ( & RunContext {
Timeout : opts . Timeout ,
Env : envs ,
Stdout : io . Discard ,
Stderr : stderr ,
} ) ; err != nil {
return ConcatenateError ( err , stderr . String ( ) )
}
return nil
2016-11-04 01:16:01 +03:00
}
2017-05-30 12:32:01 +03:00
// PushOptions options when push to remote
type PushOptions struct {
2021-06-14 20:20:43 +03:00
Remote string
Branch string
Force bool
Mirror bool
Env [ ] string
Timeout time . Duration
2017-05-30 12:32:01 +03:00
}
2016-11-04 01:16:01 +03:00
// Push pushs local commits to given remote branch.
2021-11-30 23:06:32 +03:00
func Push ( ctx context . Context , repoPath string , opts PushOptions ) error {
2022-02-06 22:01:47 +03:00
cmd := NewCommand ( ctx , "push" )
2017-05-30 12:32:01 +03:00
if opts . Force {
cmd . AddArguments ( "-f" )
}
2021-06-14 20:20:43 +03:00
if opts . Mirror {
cmd . AddArguments ( "--mirror" )
}
cmd . AddArguments ( "--" , opts . Remote )
if len ( opts . Branch ) > 0 {
cmd . AddArguments ( opts . Branch )
}
2020-03-28 07:13:18 +03:00
var outbuf , errbuf strings . Builder
2021-06-14 20:20:43 +03:00
if opts . Timeout == 0 {
opts . Timeout = - 1
}
err := cmd . RunInDirTimeoutEnvPipeline ( opts . Env , opts . Timeout , repoPath , & outbuf , & errbuf )
2020-03-28 07:13:18 +03:00
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
2021-06-24 00:08:26 +03:00
} else if strings . Contains ( errbuf . String ( ) , "matches more than one" ) {
err := & ErrMoreThanOne {
StdOut : outbuf . String ( ) ,
StdErr : errbuf . String ( ) ,
Err : err ,
}
return err
2020-03-28 07:13:18 +03:00
}
}
if errbuf . Len ( ) > 0 && err != nil {
return fmt . Errorf ( "%v - %s" , err , errbuf . String ( ) )
}
2016-11-04 01:16:01 +03:00
return err
}
2017-05-30 12:32:01 +03:00
// GetLatestCommitTime returns time for latest commit in repository (across all branches)
2022-01-20 02:26:57 +03:00
func GetLatestCommitTime ( ctx context . Context , repoPath string ) ( time . Time , error ) {
2022-02-06 22:01:47 +03:00
cmd := NewCommand ( ctx , "for-each-ref" , "--sort=-committerdate" , BranchPrefix , "--count" , "1" , "--format=%(committerdate)" )
2017-05-30 12:32:01 +03:00
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
}
2022-01-20 02:26:57 +03:00
func checkDivergence ( ctx context . Context , repoPath , baseBranch , targetBranch string ) ( int , error ) {
2019-05-05 19:25:25 +03:00
branches := fmt . Sprintf ( "%s..%s" , baseBranch , targetBranch )
2022-02-06 22:01:47 +03:00
cmd := NewCommand ( ctx , "rev-list" , "--count" , branches )
2019-05-05 19:25:25 +03:00
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
2022-01-20 02:26:57 +03:00
func GetDivergingCommits ( ctx context . Context , repoPath , baseBranch , targetBranch string ) ( DivergeObject , error ) {
2019-05-05 19:25:25 +03:00
// $(git rev-list --count master..feature) commits ahead of master
2022-01-20 02:26:57 +03:00
ahead , errorAhead := checkDivergence ( ctx , repoPath , baseBranch , targetBranch )
2019-05-05 19:25:25 +03:00
if errorAhead != nil {
return DivergeObject { } , errorAhead
}
// $(git rev-list --count feature..master) commits behind master
2022-01-20 02:26:57 +03:00
behind , errorBehind := checkDivergence ( ctx , repoPath , targetBranch , baseBranch )
2019-05-05 19:25:25 +03:00
if errorBehind != nil {
return DivergeObject { } , errorBehind
}
return DivergeObject { ahead , behind } , nil
}
2021-08-24 19:47:09 +03:00
// CreateBundle create bundle content to the target path
func ( repo * Repository ) CreateBundle ( ctx context . Context , commit string , out io . Writer ) error {
tmp , err := os . MkdirTemp ( os . TempDir ( ) , "gitea-bundle" )
if err != nil {
return err
}
defer os . RemoveAll ( tmp )
2021-09-26 00:29:25 +03:00
env := append ( os . Environ ( ) , "GIT_OBJECT_DIRECTORY=" + filepath . Join ( repo . Path , "objects" ) )
2022-02-06 22:01:47 +03:00
_ , err = NewCommand ( ctx , "init" , "--bare" ) . RunInDirWithEnv ( tmp , env )
2021-09-26 00:29:25 +03:00
if err != nil {
return err
}
2022-02-06 22:01:47 +03:00
_ , err = NewCommand ( ctx , "reset" , "--soft" , commit ) . RunInDirWithEnv ( tmp , env )
2021-09-26 00:29:25 +03:00
if err != nil {
return err
2021-08-24 19:47:09 +03:00
}
2021-09-26 00:29:25 +03:00
2022-02-06 22:01:47 +03:00
_ , err = NewCommand ( ctx , "branch" , "-m" , "bundle" ) . RunInDirWithEnv ( tmp , env )
2021-09-26 00:29:25 +03:00
if err != nil {
return err
}
tmpFile := filepath . Join ( tmp , "bundle" )
2022-02-06 22:01:47 +03:00
_ , err = NewCommand ( ctx , "bundle" , "create" , tmpFile , "bundle" , "HEAD" ) . RunInDirWithEnv ( tmp , env )
2021-08-24 19:47:09 +03:00
if err != nil {
return err
}
fi , err := os . Open ( tmpFile )
if err != nil {
return err
}
defer fi . Close ( )
_ , err = io . Copy ( out , fi )
return err
}