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.
2022-11-27 21:20:29 +03:00
// SPDX-License-Identifier: MIT
2016-11-04 01:16:01 +03:00
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"
2022-03-27 14:54:09 +03:00
"code.gitea.io/gitea/modules/util"
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-10-23 17:44:45 +03:00
_ , _ , err := NewCommand ( ctx , "ls-remote" , "-q" , "-h" ) . AddDynamicArguments ( url , "HEAD" ) . RunStdString ( nil )
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" )
}
2022-04-01 05:55:30 +03:00
_ , _ , err = cmd . RunStdString ( & RunOpts { Dir : 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
2023-04-19 16:40:42 +03:00
if err := NewCommand ( repo . Ctx ) . AddOptionFormat ( "--git-dir=%s" , repo . Path ) . AddArguments ( "show-ref" , "--head" , "^HEAD$" ) .
2022-04-01 05:55:30 +03:00
Run ( & RunOpts {
Dir : repo . Path ,
Stdout : & output ,
Stderr : & errbuf ,
2022-01-28 05:51:16 +03:00
} ) ; err != nil {
2022-02-13 19:01:23 +03:00
if err . Error ( ) == "exit status 1" && errbuf . String ( ) == "" {
return true , nil
}
2022-10-24 22:29:17 +03:00
return true , fmt . Errorf ( "check empty: %w - %s" , err , errbuf . String ( ) )
2019-06-26 21:15:26 +03:00
}
2022-02-13 19:01:23 +03:00
return strings . TrimSpace ( output . String ( ) ) == "" , 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 {
2022-03-19 17:16:38 +03:00
Timeout time . Duration
Mirror bool
Bare bool
Quiet bool
Branch string
Shared bool
NoCheckout bool
Depth int
Filter string
SkipTLSVerify bool
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-10-23 17:44:45 +03:00
return CloneWithArgs ( ctx , globalCommandArgs , from , to , opts )
2019-11-27 03:35:52 +03:00
}
// CloneWithArgs original repository to target path.
Refactor git command package to improve security and maintainability (#22678)
This PR follows #21535 (and replace #22592)
## Review without space diff
https://github.com/go-gitea/gitea/pull/22678/files?diff=split&w=1
## Purpose of this PR
1. Make git module command completely safe (risky user inputs won't be
passed as argument option anymore)
2. Avoid low-level mistakes like
https://github.com/go-gitea/gitea/pull/22098#discussion_r1045234918
3. Remove deprecated and dirty `CmdArgCheck` function, hide the `CmdArg`
type
4. Simplify code when using git command
## The main idea of this PR
* Move the `git.CmdArg` to the `internal` package, then no other package
except `git` could use it. Then developers could never do
`AddArguments(git.CmdArg(userInput))` any more.
* Introduce `git.ToTrustedCmdArgs`, it's for user-provided and already
trusted arguments. It's only used in a few cases, for example: use git
arguments from config file, help unit test with some arguments.
* Introduce `AddOptionValues` and `AddOptionFormat`, they make code more
clear and simple:
* Before: `AddArguments("-m").AddDynamicArguments(message)`
* After: `AddOptionValues("-m", message)`
* -
* Before: `AddArguments(git.CmdArg(fmt.Sprintf("--author='%s <%s>'",
sig.Name, sig.Email)))`
* After: `AddOptionFormat("--author='%s <%s>'", sig.Name, sig.Email)`
## FAQ
### Why these changes were not done in #21535 ?
#21535 is mainly a search&replace, it did its best to not change too
much logic.
Making the framework better needs a lot of changes, so this separate PR
is needed as the second step.
### The naming of `AddOptionXxx`
According to git's manual, the `--xxx` part is called `option`.
### How can it guarantee that `internal.CmdArg` won't be not misused?
Go's specification guarantees that. Trying to access other package's
internal package causes compilation error.
And, `golangci-lint` also denies the git/internal package. Only the
`git/command.go` can use it carefully.
### There is still a `ToTrustedCmdArgs`, will it still allow developers
to make mistakes and pass untrusted arguments?
Generally speaking, no. Because when using `ToTrustedCmdArgs`, the code
will be very complex (see the changes for examples). Then developers and
reviewers can know that something might be unreasonable.
### Why there was a `CmdArgCheck` and why it's removed?
At the moment of #21535, to reduce unnecessary changes, `CmdArgCheck`
was introduced as a hacky patch. Now, almost all code could be written
as `cmd := NewCommand(); cmd.AddXxx(...)`, then there is no need for
`CmdArgCheck` anymore.
### Why many codes for `signArg == ""` is deleted?
Because in the old code, `signArg` could never be empty string, it's
either `-S[key-id]` or `--no-gpg-sign`. So the `signArg == ""` is just
dead code.
---------
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2023-02-04 05:30:43 +03:00
func CloneWithArgs ( ctx context . Context , args TrustedCmdArgs , from , to 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" )
2022-03-19 17:16:38 +03:00
if opts . SkipTLSVerify {
cmd . AddArguments ( "-c" , "http.sslVerify=false" )
}
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 {
2022-10-23 17:44:45 +03:00
cmd . AddArguments ( "--depth" ) . AddDynamicArguments ( strconv . Itoa ( opts . Depth ) )
2019-11-30 09:54:47 +03:00
}
2022-01-24 00:19:32 +03:00
if opts . Filter != "" {
2022-10-23 17:44:45 +03:00
cmd . AddArguments ( "--filter" ) . AddDynamicArguments ( opts . Filter )
2022-01-24 00:19:32 +03:00
}
2016-11-04 01:16:01 +03:00
if len ( opts . Branch ) > 0 {
2022-10-23 17:44:45 +03:00
cmd . AddArguments ( "-b" ) . AddDynamicArguments ( opts . Branch )
2016-11-04 01:16:01 +03:00
}
2022-10-23 17:44:45 +03:00
cmd . AddDashesAndList ( from , to )
2016-11-04 01:16:01 +03:00
2022-03-27 14:54:09 +03:00
if strings . Contains ( from , "://" ) && strings . Contains ( from , "@" ) {
2022-03-31 05:25:40 +03:00
cmd . SetDescription ( fmt . Sprintf ( "clone branch %s from %s to %s (shared: %t, mirror: %t, depth: %d)" , opts . Branch , util . SanitizeCredentialURLs ( from ) , to , opts . Shared , opts . Mirror , opts . Depth ) )
2022-03-27 14:54:09 +03:00
} else {
cmd . SetDescription ( fmt . Sprintf ( "clone branch %s from %s to %s (shared: %t, mirror: %t, depth: %d)" , opts . Branch , from , to , opts . Shared , opts . Mirror , opts . Depth ) )
}
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 )
2023-02-11 03:39:50 +03:00
if err == nil {
envs = proxy . EnvWithProxy ( u )
2021-08-18 16:10:39 +03:00
}
2022-01-20 20:46:10 +03:00
stderr := new ( bytes . Buffer )
2022-04-01 05:55:30 +03:00
if err = cmd . Run ( & RunOpts {
2021-08-18 16:10:39 +03:00
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" )
}
2022-10-23 17:44:45 +03:00
remoteBranchArgs := [ ] string { opts . Remote }
2021-06-14 20:20:43 +03:00
if len ( opts . Branch ) > 0 {
2022-10-23 17:44:45 +03:00
remoteBranchArgs = append ( remoteBranchArgs , opts . Branch )
2021-06-14 20:20:43 +03:00
}
2022-10-23 17:44:45 +03:00
cmd . AddDashesAndList ( remoteBranchArgs ... )
2022-03-27 14:54:09 +03:00
if strings . Contains ( opts . Remote , "://" ) && strings . Contains ( opts . Remote , "@" ) {
2022-03-31 05:25:40 +03:00
cmd . SetDescription ( fmt . Sprintf ( "push branch %s to %s (force: %t, mirror: %t)" , opts . Branch , util . SanitizeCredentialURLs ( opts . Remote ) , opts . Force , opts . Mirror ) )
2022-03-27 14:54:09 +03:00
} else {
cmd . SetDescription ( fmt . Sprintf ( "push branch %s to %s (force: %t, mirror: %t)" , opts . Branch , opts . Remote , opts . Force , opts . Mirror ) )
}
2020-03-28 07:13:18 +03:00
2023-04-14 02:17:27 +03:00
stdout , stderr , err := cmd . RunStdString ( & RunOpts { Env : opts . Env , Timeout : opts . Timeout , Dir : repoPath } )
2020-03-28 07:13:18 +03:00
if err != nil {
2023-04-14 02:17:27 +03:00
if strings . Contains ( stderr , "non-fast-forward" ) {
return & ErrPushOutOfDate { StdOut : stdout , StdErr : stderr , Err : err }
} else if strings . Contains ( stderr , "! [remote rejected]" ) {
err := & ErrPushRejected { StdOut : stdout , StdErr : stderr , Err : err }
2020-03-28 07:13:18 +03:00
err . GenerateMessage ( )
return err
2023-04-14 02:17:27 +03:00
} else if strings . Contains ( stderr , "matches more than one" ) {
return & ErrMoreThanOne { StdOut : stdout , StdErr : stderr , Err : err }
2020-03-28 07:13:18 +03:00
}
2023-04-14 02:17:27 +03:00
return fmt . Errorf ( "push failed: %w - %s\n%s" , err , stderr , stdout )
2020-03-28 07:13:18 +03:00
}
2023-04-14 02:17:27 +03:00
return nil
2016-11-04 01:16:01 +03:00
}
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)" )
2022-04-01 05:55:30 +03:00
stdout , _ , err := cmd . RunStdString ( & RunOpts { Dir : repoPath } )
2017-05-30 12:32:01 +03:00
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-10-23 17:44:45 +03:00
cmd := NewCommand ( ctx , "rev-list" , "--count" ) . AddDynamicArguments ( branches )
2022-04-01 05:55:30 +03:00
stdout , _ , err := cmd . RunStdString ( & RunOpts { Dir : repoPath } )
2019-05-05 19:25:25 +03:00
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-04-01 05:55:30 +03:00
_ , _ , err = NewCommand ( ctx , "init" , "--bare" ) . RunStdString ( & RunOpts { Dir : tmp , Env : env } )
2021-09-26 00:29:25 +03:00
if err != nil {
return err
}
2022-10-23 17:44:45 +03:00
_ , _ , err = NewCommand ( ctx , "reset" , "--soft" ) . AddDynamicArguments ( commit ) . RunStdString ( & RunOpts { Dir : tmp , Env : 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-04-01 05:55:30 +03:00
_ , _ , err = NewCommand ( ctx , "branch" , "-m" , "bundle" ) . RunStdString ( & RunOpts { Dir : tmp , Env : env } )
2021-09-26 00:29:25 +03:00
if err != nil {
return err
}
tmpFile := filepath . Join ( tmp , "bundle" )
2022-10-23 17:44:45 +03:00
_ , _ , err = NewCommand ( ctx , "bundle" , "create" ) . AddDynamicArguments ( tmpFile , "bundle" , "HEAD" ) . RunStdString ( & RunOpts { Dir : tmp , Env : 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
}