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"
"errors"
2019-05-05 19:25:25 +03:00
"fmt"
2016-11-04 01:16:01 +03:00
"os"
"path"
"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"
2017-04-08 05:23:39 +03:00
2019-11-13 10:01:19 +03:00
gitealog "code.gitea.io/gitea/modules/log"
2020-03-17 19:19:58 +03:00
"github.com/go-git/go-billy/v5/osfs"
gogit "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/cache"
"github.com/go-git/go-git/v5/storage/filesystem"
2019-08-23 19:40:30 +03:00
"github.com/unknwon/com"
2016-11-04 01:16:01 +03:00
)
// Repository represents a Git repository.
type Repository struct {
Path string
2019-04-19 15:17:27 +03:00
tagCache * ObjectCache
gogitRepo * gogit . Repository
gogitStorage * filesystem . Storage
2019-10-16 16:42:42 +03:00
gpgSettings * GPGSettings
}
// 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 ) {
return AllCommitsCount ( repo . Path )
}
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
}
// OpenRepository opens the repository at the given path.
func OpenRepository ( repoPath string ) ( * Repository , error ) {
repoPath , err := filepath . Abs ( repoPath )
if err != nil {
return nil , err
} else if ! isDir ( repoPath ) {
return nil , errors . New ( "no such file or directory" )
}
2019-04-19 15:17:27 +03:00
fs := osfs . New ( repoPath )
_ , err = fs . Stat ( ".git" )
if err == nil {
fs , err = fs . Chroot ( ".git" )
if err != nil {
return nil , err
}
}
storage := filesystem . NewStorageWithOptions ( fs , cache . NewObjectLRUDefault ( ) , filesystem . Options { KeepDescriptors : true } )
gogitRepo , err := gogit . Open ( storage , fs )
if err != nil {
return nil , err
}
2016-11-04 01:16:01 +03:00
return & Repository {
2019-04-19 15:17:27 +03:00
Path : repoPath ,
gogitRepo : gogitRepo ,
gogitStorage : storage ,
tagCache : newObjectCache ( ) ,
2016-11-04 01:16:01 +03:00
} , nil
}
2019-11-13 10:01:19 +03:00
// Close this repository, in particular close the underlying gogitStorage if this is not nil
func ( repo * Repository ) Close ( ) {
if repo == nil || repo . gogitStorage == nil {
return
}
if err := repo . gogitStorage . Close ( ) ; err != nil {
gitealog . Error ( "Error closing storage: %v" , err )
}
}
2019-10-28 21:31:55 +03:00
// GoGitRepo gets the go-git repo representation
func ( repo * Repository ) GoGitRepo ( ) * gogit . Repository {
return repo . gogitRepo
}
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 ) {
2019-11-27 03:35:52 +03:00
cargs := make ( [ ] string , len ( GlobalCommandArgs ) )
copy ( cargs , GlobalCommandArgs )
return CloneWithArgs ( from , to , cargs , opts )
}
// CloneWithArgs original repository to target path.
func CloneWithArgs ( 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
}
2019-11-27 03:35:52 +03:00
cmd := NewCommandNoGlobals ( 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 )
2019-05-11 18:29:17 +03:00
_ , err := cmd . RunInDirWithEnv ( repoPath , opts . Env )
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 ) :
repoSize . Count = com . StrTo ( line [ 7 : ] ) . MustInt64 ( )
case strings . HasPrefix ( line , statSize ) :
repoSize . Size = com . StrTo ( line [ 6 : ] ) . MustInt64 ( ) * 1024
case strings . HasPrefix ( line , statInpack ) :
repoSize . InPack = com . StrTo ( line [ 9 : ] ) . MustInt64 ( )
case strings . HasPrefix ( line , statPacks ) :
repoSize . Packs = com . StrTo ( line [ 7 : ] ) . MustInt64 ( )
case strings . HasPrefix ( line , statSizePack ) :
repoSize . SizePack = com . StrTo ( line [ 11 : ] ) . MustInt64 ( ) * 1024
case strings . HasPrefix ( line , statPrunePackage ) :
repoSize . PrunePack = com . StrTo ( line [ 16 : ] ) . MustInt64 ( )
case strings . HasPrefix ( line , statGarbage ) :
repoSize . Garbage = com . StrTo ( line [ 9 : ] ) . MustInt64 ( )
case strings . HasPrefix ( line , statSizeGarbage ) :
repoSize . SizeGarbage = com . StrTo ( line [ 14 : ] ) . MustInt64 ( ) * 1024
}
}
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
}