code.gitea.io/gitea@v1.22.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, objectFormatName string) error {
    67  	err := os.MkdirAll(repoPath, os.ModePerm)
    68  	if err != nil {
    69  		return err
    70  	}
    71  
    72  	cmd := NewCommand(ctx, "init")
    73  
    74  	if !IsValidObjectFormat(objectFormatName) {
    75  		return fmt.Errorf("invalid object format: %s", objectFormatName)
    76  	}
    77  	if DefaultFeatures().SupportHashSha256 {
    78  		cmd.AddOptionValues("--object-format", objectFormatName)
    79  	}
    80  
    81  	if bare {
    82  		cmd.AddArguments("--bare")
    83  	}
    84  	_, _, err = cmd.RunStdString(&RunOpts{Dir: repoPath})
    85  	return err
    86  }
    87  
    88  // IsEmpty Check if repository is empty.
    89  func (repo *Repository) IsEmpty() (bool, error) {
    90  	var errbuf, output strings.Builder
    91  	if err := NewCommand(repo.Ctx).AddOptionFormat("--git-dir=%s", repo.Path).AddArguments("rev-list", "-n", "1", "--all").
    92  		Run(&RunOpts{
    93  			Dir:    repo.Path,
    94  			Stdout: &output,
    95  			Stderr: &errbuf,
    96  		}); err != nil {
    97  		if (err.Error() == "exit status 1" && strings.TrimSpace(errbuf.String()) == "") || err.Error() == "exit status 129" {
    98  			// git 2.11 exits with 129 if the repo is empty
    99  			return true, nil
   100  		}
   101  		return true, fmt.Errorf("check empty: %w - %s", err, errbuf.String())
   102  	}
   103  
   104  	return strings.TrimSpace(output.String()) == "", nil
   105  }
   106  
   107  // CloneRepoOptions options when clone a repository
   108  type CloneRepoOptions struct {
   109  	Timeout       time.Duration
   110  	Mirror        bool
   111  	Bare          bool
   112  	Quiet         bool
   113  	Branch        string
   114  	Shared        bool
   115  	NoCheckout    bool
   116  	Depth         int
   117  	Filter        string
   118  	SkipTLSVerify bool
   119  }
   120  
   121  // Clone clones original repository to target path.
   122  func Clone(ctx context.Context, from, to string, opts CloneRepoOptions) error {
   123  	return CloneWithArgs(ctx, globalCommandArgs, from, to, opts)
   124  }
   125  
   126  // CloneWithArgs original repository to target path.
   127  func CloneWithArgs(ctx context.Context, args TrustedCmdArgs, from, to string, opts CloneRepoOptions) (err error) {
   128  	toDir := path.Dir(to)
   129  	if err = os.MkdirAll(toDir, os.ModePerm); err != nil {
   130  		return err
   131  	}
   132  
   133  	cmd := NewCommandContextNoGlobals(ctx, args...).AddArguments("clone")
   134  	if opts.SkipTLSVerify {
   135  		cmd.AddArguments("-c", "http.sslVerify=false")
   136  	}
   137  	if opts.Mirror {
   138  		cmd.AddArguments("--mirror")
   139  	}
   140  	if opts.Bare {
   141  		cmd.AddArguments("--bare")
   142  	}
   143  	if opts.Quiet {
   144  		cmd.AddArguments("--quiet")
   145  	}
   146  	if opts.Shared {
   147  		cmd.AddArguments("-s")
   148  	}
   149  	if opts.NoCheckout {
   150  		cmd.AddArguments("--no-checkout")
   151  	}
   152  	if opts.Depth > 0 {
   153  		cmd.AddArguments("--depth").AddDynamicArguments(strconv.Itoa(opts.Depth))
   154  	}
   155  	if opts.Filter != "" {
   156  		cmd.AddArguments("--filter").AddDynamicArguments(opts.Filter)
   157  	}
   158  	if len(opts.Branch) > 0 {
   159  		cmd.AddArguments("-b").AddDynamicArguments(opts.Branch)
   160  	}
   161  	cmd.AddDashesAndList(from, to)
   162  
   163  	if strings.Contains(from, "://") && strings.Contains(from, "@") {
   164  		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))
   165  	} else {
   166  		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))
   167  	}
   168  
   169  	if opts.Timeout <= 0 {
   170  		opts.Timeout = -1
   171  	}
   172  
   173  	envs := os.Environ()
   174  	u, err := url.Parse(from)
   175  	if err == nil {
   176  		envs = proxy.EnvWithProxy(u)
   177  	}
   178  
   179  	stderr := new(bytes.Buffer)
   180  	if err = cmd.Run(&RunOpts{
   181  		Timeout: opts.Timeout,
   182  		Env:     envs,
   183  		Stdout:  io.Discard,
   184  		Stderr:  stderr,
   185  	}); err != nil {
   186  		return ConcatenateError(err, stderr.String())
   187  	}
   188  	return nil
   189  }
   190  
   191  // PushOptions options when push to remote
   192  type PushOptions struct {
   193  	Remote  string
   194  	Branch  string
   195  	Force   bool
   196  	Mirror  bool
   197  	Env     []string
   198  	Timeout time.Duration
   199  }
   200  
   201  // Push pushs local commits to given remote branch.
   202  func Push(ctx context.Context, repoPath string, opts PushOptions) error {
   203  	cmd := NewCommand(ctx, "push")
   204  	if opts.Force {
   205  		cmd.AddArguments("-f")
   206  	}
   207  	if opts.Mirror {
   208  		cmd.AddArguments("--mirror")
   209  	}
   210  	remoteBranchArgs := []string{opts.Remote}
   211  	if len(opts.Branch) > 0 {
   212  		remoteBranchArgs = append(remoteBranchArgs, opts.Branch)
   213  	}
   214  	cmd.AddDashesAndList(remoteBranchArgs...)
   215  
   216  	if strings.Contains(opts.Remote, "://") && strings.Contains(opts.Remote, "@") {
   217  		cmd.SetDescription(fmt.Sprintf("push branch %s to %s (force: %t, mirror: %t)", opts.Branch, util.SanitizeCredentialURLs(opts.Remote), opts.Force, opts.Mirror))
   218  	} else {
   219  		cmd.SetDescription(fmt.Sprintf("push branch %s to %s (force: %t, mirror: %t)", opts.Branch, opts.Remote, opts.Force, opts.Mirror))
   220  	}
   221  
   222  	stdout, stderr, err := cmd.RunStdString(&RunOpts{Env: opts.Env, Timeout: opts.Timeout, Dir: repoPath})
   223  	if err != nil {
   224  		if strings.Contains(stderr, "non-fast-forward") {
   225  			return &ErrPushOutOfDate{StdOut: stdout, StdErr: stderr, Err: err}
   226  		} else if strings.Contains(stderr, "! [remote rejected]") {
   227  			err := &ErrPushRejected{StdOut: stdout, StdErr: stderr, Err: err}
   228  			err.GenerateMessage()
   229  			return err
   230  		} else if strings.Contains(stderr, "matches more than one") {
   231  			return &ErrMoreThanOne{StdOut: stdout, StdErr: stderr, Err: err}
   232  		}
   233  		return fmt.Errorf("push failed: %w - %s\n%s", err, stderr, stdout)
   234  	}
   235  
   236  	return nil
   237  }
   238  
   239  // GetLatestCommitTime returns time for latest commit in repository (across all branches)
   240  func GetLatestCommitTime(ctx context.Context, repoPath string) (time.Time, error) {
   241  	cmd := NewCommand(ctx, "for-each-ref", "--sort=-committerdate", BranchPrefix, "--count", "1", "--format=%(committerdate)")
   242  	stdout, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath})
   243  	if err != nil {
   244  		return time.Time{}, err
   245  	}
   246  	commitTime := strings.TrimSpace(stdout)
   247  	return time.Parse("Mon Jan _2 15:04:05 2006 -0700", commitTime)
   248  }
   249  
   250  // DivergeObject represents commit count diverging commits
   251  type DivergeObject struct {
   252  	Ahead  int
   253  	Behind int
   254  }
   255  
   256  // GetDivergingCommits returns the number of commits a targetBranch is ahead or behind a baseBranch
   257  func GetDivergingCommits(ctx context.Context, repoPath, baseBranch, targetBranch string) (do DivergeObject, err error) {
   258  	cmd := NewCommand(ctx, "rev-list", "--count", "--left-right").
   259  		AddDynamicArguments(baseBranch + "..." + targetBranch).AddArguments("--")
   260  	stdout, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath})
   261  	if err != nil {
   262  		return do, err
   263  	}
   264  	left, right, found := strings.Cut(strings.Trim(stdout, "\n"), "\t")
   265  	if !found {
   266  		return do, fmt.Errorf("git rev-list output is missing a tab: %q", stdout)
   267  	}
   268  
   269  	do.Behind, err = strconv.Atoi(left)
   270  	if err != nil {
   271  		return do, err
   272  	}
   273  	do.Ahead, err = strconv.Atoi(right)
   274  	if err != nil {
   275  		return do, err
   276  	}
   277  	return do, nil
   278  }
   279  
   280  // CreateBundle create bundle content to the target path
   281  func (repo *Repository) CreateBundle(ctx context.Context, commit string, out io.Writer) error {
   282  	tmp, err := os.MkdirTemp(os.TempDir(), "gitea-bundle")
   283  	if err != nil {
   284  		return err
   285  	}
   286  	defer os.RemoveAll(tmp)
   287  
   288  	env := append(os.Environ(), "GIT_OBJECT_DIRECTORY="+filepath.Join(repo.Path, "objects"))
   289  	_, _, err = NewCommand(ctx, "init", "--bare").RunStdString(&RunOpts{Dir: tmp, Env: env})
   290  	if err != nil {
   291  		return err
   292  	}
   293  
   294  	_, _, err = NewCommand(ctx, "reset", "--soft").AddDynamicArguments(commit).RunStdString(&RunOpts{Dir: tmp, Env: env})
   295  	if err != nil {
   296  		return err
   297  	}
   298  
   299  	_, _, err = NewCommand(ctx, "branch", "-m", "bundle").RunStdString(&RunOpts{Dir: tmp, Env: env})
   300  	if err != nil {
   301  		return err
   302  	}
   303  
   304  	tmpFile := filepath.Join(tmp, "bundle")
   305  	_, _, err = NewCommand(ctx, "bundle", "create").AddDynamicArguments(tmpFile, "bundle", "HEAD").RunStdString(&RunOpts{Dir: tmp, Env: env})
   306  	if err != nil {
   307  		return err
   308  	}
   309  
   310  	fi, err := os.Open(tmpFile)
   311  	if err != nil {
   312  		return err
   313  	}
   314  	defer fi.Close()
   315  
   316  	_, err = io.Copy(out, fi)
   317  	return err
   318  }