code.gitea.io/gitea@v1.19.3/modules/git/repo.go (about)

     1  // Copyright 2015 The Gogs Authors. All rights reserved.
     2  // Copyright 2017 The Gitea Authors. All rights reserved.
     3  // SPDX-License-Identifier: MIT
     4  
     5  package git
     6  
     7  import (
     8  	"bytes"
     9  	"context"
    10  	"fmt"
    11  	"io"
    12  	"net/url"
    13  	"os"
    14  	"path"
    15  	"path/filepath"
    16  	"strconv"
    17  	"strings"
    18  	"time"
    19  
    20  	"code.gitea.io/gitea/modules/proxy"
    21  	"code.gitea.io/gitea/modules/util"
    22  )
    23  
    24  // GPGSettings represents the default GPG settings for this repository
    25  type GPGSettings struct {
    26  	Sign             bool
    27  	KeyID            string
    28  	Email            string
    29  	Name             string
    30  	PublicKeyContent string
    31  }
    32  
    33  const prettyLogFormat = `--pretty=format:%H`
    34  
    35  // GetAllCommitsCount returns count of all commits in repository
    36  func (repo *Repository) GetAllCommitsCount() (int64, error) {
    37  	return AllCommitsCount(repo.Ctx, repo.Path, false)
    38  }
    39  
    40  func (repo *Repository) parsePrettyFormatLogToList(logs []byte) ([]*Commit, error) {
    41  	var commits []*Commit
    42  	if len(logs) == 0 {
    43  		return commits, nil
    44  	}
    45  
    46  	parts := bytes.Split(logs, []byte{'\n'})
    47  
    48  	for _, commitID := range parts {
    49  		commit, err := repo.GetCommit(string(commitID))
    50  		if err != nil {
    51  			return nil, err
    52  		}
    53  		commits = append(commits, commit)
    54  	}
    55  
    56  	return commits, nil
    57  }
    58  
    59  // IsRepoURLAccessible checks if given repository URL is accessible.
    60  func IsRepoURLAccessible(ctx context.Context, url string) bool {
    61  	_, _, err := NewCommand(ctx, "ls-remote", "-q", "-h").AddDynamicArguments(url, "HEAD").RunStdString(nil)
    62  	return err == nil
    63  }
    64  
    65  // InitRepository initializes a new Git repository.
    66  func InitRepository(ctx context.Context, repoPath string, bare bool) error {
    67  	err := os.MkdirAll(repoPath, os.ModePerm)
    68  	if err != nil {
    69  		return err
    70  	}
    71  
    72  	cmd := NewCommand(ctx, "init")
    73  	if bare {
    74  		cmd.AddArguments("--bare")
    75  	}
    76  	_, _, err = cmd.RunStdString(&RunOpts{Dir: repoPath})
    77  	return err
    78  }
    79  
    80  // IsEmpty Check if repository is empty.
    81  func (repo *Repository) IsEmpty() (bool, error) {
    82  	var errbuf, output strings.Builder
    83  	if err := NewCommand(repo.Ctx, "show-ref", "--head", "^HEAD$").
    84  		Run(&RunOpts{
    85  			Dir:    repo.Path,
    86  			Stdout: &output,
    87  			Stderr: &errbuf,
    88  		}); err != nil {
    89  		if err.Error() == "exit status 1" && errbuf.String() == "" {
    90  			return true, nil
    91  		}
    92  		return true, fmt.Errorf("check empty: %w - %s", err, errbuf.String())
    93  	}
    94  
    95  	return strings.TrimSpace(output.String()) == "", nil
    96  }
    97  
    98  // CloneRepoOptions options when clone a repository
    99  type CloneRepoOptions struct {
   100  	Timeout       time.Duration
   101  	Mirror        bool
   102  	Bare          bool
   103  	Quiet         bool
   104  	Branch        string
   105  	Shared        bool
   106  	NoCheckout    bool
   107  	Depth         int
   108  	Filter        string
   109  	SkipTLSVerify bool
   110  }
   111  
   112  // Clone clones original repository to target path.
   113  func Clone(ctx context.Context, from, to string, opts CloneRepoOptions) error {
   114  	return CloneWithArgs(ctx, globalCommandArgs, from, to, opts)
   115  }
   116  
   117  // CloneWithArgs original repository to target path.
   118  func CloneWithArgs(ctx context.Context, args TrustedCmdArgs, from, to string, opts CloneRepoOptions) (err error) {
   119  	toDir := path.Dir(to)
   120  	if err = os.MkdirAll(toDir, os.ModePerm); err != nil {
   121  		return err
   122  	}
   123  
   124  	cmd := NewCommandContextNoGlobals(ctx, args...).AddArguments("clone")
   125  	if opts.SkipTLSVerify {
   126  		cmd.AddArguments("-c", "http.sslVerify=false")
   127  	}
   128  	if opts.Mirror {
   129  		cmd.AddArguments("--mirror")
   130  	}
   131  	if opts.Bare {
   132  		cmd.AddArguments("--bare")
   133  	}
   134  	if opts.Quiet {
   135  		cmd.AddArguments("--quiet")
   136  	}
   137  	if opts.Shared {
   138  		cmd.AddArguments("-s")
   139  	}
   140  	if opts.NoCheckout {
   141  		cmd.AddArguments("--no-checkout")
   142  	}
   143  	if opts.Depth > 0 {
   144  		cmd.AddArguments("--depth").AddDynamicArguments(strconv.Itoa(opts.Depth))
   145  	}
   146  	if opts.Filter != "" {
   147  		cmd.AddArguments("--filter").AddDynamicArguments(opts.Filter)
   148  	}
   149  	if len(opts.Branch) > 0 {
   150  		cmd.AddArguments("-b").AddDynamicArguments(opts.Branch)
   151  	}
   152  	cmd.AddDashesAndList(from, to)
   153  
   154  	if strings.Contains(from, "://") && strings.Contains(from, "@") {
   155  		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))
   156  	} else {
   157  		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))
   158  	}
   159  
   160  	if opts.Timeout <= 0 {
   161  		opts.Timeout = -1
   162  	}
   163  
   164  	envs := os.Environ()
   165  	u, err := url.Parse(from)
   166  	if err == nil {
   167  		envs = proxy.EnvWithProxy(u)
   168  	}
   169  
   170  	stderr := new(bytes.Buffer)
   171  	if err = cmd.Run(&RunOpts{
   172  		Timeout: opts.Timeout,
   173  		Env:     envs,
   174  		Stdout:  io.Discard,
   175  		Stderr:  stderr,
   176  	}); err != nil {
   177  		return ConcatenateError(err, stderr.String())
   178  	}
   179  	return nil
   180  }
   181  
   182  // PushOptions options when push to remote
   183  type PushOptions struct {
   184  	Remote  string
   185  	Branch  string
   186  	Force   bool
   187  	Mirror  bool
   188  	Env     []string
   189  	Timeout time.Duration
   190  }
   191  
   192  // Push pushs local commits to given remote branch.
   193  func Push(ctx context.Context, repoPath string, opts PushOptions) error {
   194  	cmd := NewCommand(ctx, "push")
   195  	if opts.Force {
   196  		cmd.AddArguments("-f")
   197  	}
   198  	if opts.Mirror {
   199  		cmd.AddArguments("--mirror")
   200  	}
   201  	remoteBranchArgs := []string{opts.Remote}
   202  	if len(opts.Branch) > 0 {
   203  		remoteBranchArgs = append(remoteBranchArgs, opts.Branch)
   204  	}
   205  	cmd.AddDashesAndList(remoteBranchArgs...)
   206  
   207  	if strings.Contains(opts.Remote, "://") && strings.Contains(opts.Remote, "@") {
   208  		cmd.SetDescription(fmt.Sprintf("push branch %s to %s (force: %t, mirror: %t)", opts.Branch, util.SanitizeCredentialURLs(opts.Remote), opts.Force, opts.Mirror))
   209  	} else {
   210  		cmd.SetDescription(fmt.Sprintf("push branch %s to %s (force: %t, mirror: %t)", opts.Branch, opts.Remote, opts.Force, opts.Mirror))
   211  	}
   212  	var outbuf, errbuf strings.Builder
   213  
   214  	if opts.Timeout == 0 {
   215  		opts.Timeout = -1
   216  	}
   217  
   218  	err := cmd.Run(&RunOpts{
   219  		Env:     opts.Env,
   220  		Timeout: opts.Timeout,
   221  		Dir:     repoPath,
   222  		Stdout:  &outbuf,
   223  		Stderr:  &errbuf,
   224  	})
   225  	if err != nil {
   226  		if strings.Contains(errbuf.String(), "non-fast-forward") {
   227  			return &ErrPushOutOfDate{
   228  				StdOut: outbuf.String(),
   229  				StdErr: errbuf.String(),
   230  				Err:    err,
   231  			}
   232  		} else if strings.Contains(errbuf.String(), "! [remote rejected]") {
   233  			err := &ErrPushRejected{
   234  				StdOut: outbuf.String(),
   235  				StdErr: errbuf.String(),
   236  				Err:    err,
   237  			}
   238  			err.GenerateMessage()
   239  			return err
   240  		} else if strings.Contains(errbuf.String(), "matches more than one") {
   241  			err := &ErrMoreThanOne{
   242  				StdOut: outbuf.String(),
   243  				StdErr: errbuf.String(),
   244  				Err:    err,
   245  			}
   246  			return err
   247  		}
   248  	}
   249  
   250  	if errbuf.Len() > 0 && err != nil {
   251  		return fmt.Errorf("%w - %s", err, errbuf.String())
   252  	}
   253  
   254  	return err
   255  }
   256  
   257  // GetLatestCommitTime returns time for latest commit in repository (across all branches)
   258  func GetLatestCommitTime(ctx context.Context, repoPath string) (time.Time, error) {
   259  	cmd := NewCommand(ctx, "for-each-ref", "--sort=-committerdate", BranchPrefix, "--count", "1", "--format=%(committerdate)")
   260  	stdout, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath})
   261  	if err != nil {
   262  		return time.Time{}, err
   263  	}
   264  	commitTime := strings.TrimSpace(stdout)
   265  	return time.Parse(GitTimeLayout, commitTime)
   266  }
   267  
   268  // DivergeObject represents commit count diverging commits
   269  type DivergeObject struct {
   270  	Ahead  int
   271  	Behind int
   272  }
   273  
   274  func checkDivergence(ctx context.Context, repoPath, baseBranch, targetBranch string) (int, error) {
   275  	branches := fmt.Sprintf("%s..%s", baseBranch, targetBranch)
   276  	cmd := NewCommand(ctx, "rev-list", "--count").AddDynamicArguments(branches)
   277  	stdout, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath})
   278  	if err != nil {
   279  		return -1, err
   280  	}
   281  	outInteger, errInteger := strconv.Atoi(strings.Trim(stdout, "\n"))
   282  	if errInteger != nil {
   283  		return -1, errInteger
   284  	}
   285  	return outInteger, nil
   286  }
   287  
   288  // GetDivergingCommits returns the number of commits a targetBranch is ahead or behind a baseBranch
   289  func GetDivergingCommits(ctx context.Context, repoPath, baseBranch, targetBranch string) (DivergeObject, error) {
   290  	// $(git rev-list --count master..feature) commits ahead of master
   291  	ahead, errorAhead := checkDivergence(ctx, repoPath, baseBranch, targetBranch)
   292  	if errorAhead != nil {
   293  		return DivergeObject{}, errorAhead
   294  	}
   295  
   296  	// $(git rev-list --count feature..master) commits behind master
   297  	behind, errorBehind := checkDivergence(ctx, repoPath, targetBranch, baseBranch)
   298  	if errorBehind != nil {
   299  		return DivergeObject{}, errorBehind
   300  	}
   301  
   302  	return DivergeObject{ahead, behind}, nil
   303  }
   304  
   305  // CreateBundle create bundle content to the target path
   306  func (repo *Repository) CreateBundle(ctx context.Context, commit string, out io.Writer) error {
   307  	tmp, err := os.MkdirTemp(os.TempDir(), "gitea-bundle")
   308  	if err != nil {
   309  		return err
   310  	}
   311  	defer os.RemoveAll(tmp)
   312  
   313  	env := append(os.Environ(), "GIT_OBJECT_DIRECTORY="+filepath.Join(repo.Path, "objects"))
   314  	_, _, err = NewCommand(ctx, "init", "--bare").RunStdString(&RunOpts{Dir: tmp, Env: env})
   315  	if err != nil {
   316  		return err
   317  	}
   318  
   319  	_, _, err = NewCommand(ctx, "reset", "--soft").AddDynamicArguments(commit).RunStdString(&RunOpts{Dir: tmp, Env: env})
   320  	if err != nil {
   321  		return err
   322  	}
   323  
   324  	_, _, err = NewCommand(ctx, "branch", "-m", "bundle").RunStdString(&RunOpts{Dir: tmp, Env: env})
   325  	if err != nil {
   326  		return err
   327  	}
   328  
   329  	tmpFile := filepath.Join(tmp, "bundle")
   330  	_, _, err = NewCommand(ctx, "bundle", "create").AddDynamicArguments(tmpFile, "bundle", "HEAD").RunStdString(&RunOpts{Dir: tmp, Env: env})
   331  	if err != nil {
   332  		return err
   333  	}
   334  
   335  	fi, err := os.Open(tmpFile)
   336  	if err != nil {
   337  		return err
   338  	}
   339  	defer fi.Close()
   340  
   341  	_, err = io.Copy(out, fi)
   342  	return err
   343  }