342 lines · 9.9 KB · Go
RawDownload
// Copyright 2015 The Gogs Authors. All rights reserved.// Copyright 2017 The Gitea Authors. All rights reserved.// Copyright 2024 The Forgejo Authors. All rights reserved.// SPDX-License-Identifier: MIT package git import (	"bytes"	"context"	"fmt"	"io"	"net/url"	"os"	"path"	"path/filepath"	"strconv"	"strings"	"time" 	"forgejo.org/modules/proxy"	"forgejo.org/modules/setting"	"forgejo.org/modules/util") // GPGSettings represents the default GPG settings for this repositorytype GPGSettings struct {	Sign             bool	KeyID            string	Email            string	Name             string	PublicKeyContent string} const prettyLogFormat = `--pretty=format:%H` // GetAllCommitsCount returns count of all commits in repositoryfunc (repo *Repository) GetAllCommitsCount() (int64, error) {	return AllCommitsCount(repo.Ctx, repo.Path, false)} func (repo *Repository) parsePrettyFormatLogToList(logs []byte) ([]*Commit, error) {	var commits []*Commit	if len(logs) == 0 {		return commits, nil	} 	parts := bytes.Split(logs, []byte{'\n'}) 	for _, commitID := range parts {		commit, err := repo.GetCommit(string(commitID))		if err != nil {			return nil, err		}		commits = append(commits, commit)	} 	return commits, nil} // IsRepoURLAccessible checks if given repository URL is accessible.func IsRepoURLAccessible(ctx context.Context, url string) bool {	_, _, err := NewCommand(ctx, "ls-remote", "-q", "-h").AddDynamicArguments(url, "HEAD").RunStdString(nil)	return err == nil} // InitRepository initializes a new Git repository.func InitRepository(ctx context.Context, repoPath string, bare bool, objectFormatName string) error {	err := os.MkdirAll(repoPath, os.ModePerm)	if err != nil {		return err	} 	cmd := NewCommand(ctx, "init") 	if !IsValidObjectFormat(objectFormatName) {		return fmt.Errorf("invalid object format: %s", objectFormatName)	}	if SupportHashSha256 {		cmd.AddOptionValues("--object-format", objectFormatName)	} 	if bare {		cmd.AddArguments("--bare")	}	_, _, err = cmd.RunStdString(&RunOpts{Dir: repoPath})	return err} // IsEmpty Check if repository is empty.func (repo *Repository) IsEmpty() (bool, error) {	var errbuf, output strings.Builder	if err := NewCommand(repo.Ctx).AddOptionFormat("--git-dir=%s", repo.Path).AddArguments("rev-list", "-n", "1", "--all").		Run(&RunOpts{			Dir:    repo.Path,			Stdout: &output,			Stderr: &errbuf,		}); err != nil {		if (err.Error() == "exit status 1" && strings.TrimSpace(errbuf.String()) == "") || err.Error() == "exit status 129" {			// git 2.11 exits with 129 if the repo is empty			return true, nil		}		return true, fmt.Errorf("check empty: %w - %s", err, errbuf.String())	} 	return strings.TrimSpace(output.String()) == "", nil} // CloneRepoOptions options when clone a repositorytype CloneRepoOptions struct {	Timeout       time.Duration	Mirror        bool	Bare          bool	Quiet         bool	Branch        string	Shared        bool	NoCheckout    bool	Depth         int	Filter        string	SkipTLSVerify bool} // Clone clones original repository to target path.func Clone(ctx context.Context, from, to string, opts CloneRepoOptions) error {	return CloneWithArgs(ctx, globalCommandArgs, from, to, opts)} // CloneWithArgs original repository to target path.func CloneWithArgs(ctx context.Context, args TrustedCmdArgs, from, to string, opts CloneRepoOptions) (err error) {	toDir := path.Dir(to)	if err = os.MkdirAll(toDir, os.ModePerm); err != nil {		return err	} 	cmd := NewCommandContextNoGlobals(ctx, args...).AddArguments("clone")	if opts.SkipTLSVerify {		cmd.AddArguments("-c", "http.sslVerify=false")	}	if opts.Mirror {		cmd.AddArguments("--mirror")	}	if opts.Bare {		cmd.AddArguments("--bare")	}	if opts.Quiet {		cmd.AddArguments("--quiet")	}	if opts.Shared {		cmd.AddArguments("-s")	}	if opts.NoCheckout {		cmd.AddArguments("--no-checkout")	}	if opts.Depth > 0 {		cmd.AddArguments("--depth").AddDynamicArguments(strconv.Itoa(opts.Depth))	}	if opts.Filter != "" {		cmd.AddArguments("--filter").AddDynamicArguments(opts.Filter)	}	if len(opts.Branch) > 0 {		cmd.AddArguments("-b").AddDynamicArguments(opts.Branch)	}	cmd.AddDashesAndList(from, to) 	if strings.Contains(from, "://") && strings.Contains(from, "@") {		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))	} 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))	} 	if opts.Timeout <= 0 {		opts.Timeout = -1	} 	envs := os.Environ()	u, err := url.Parse(from)	if err == nil {		envs = proxy.EnvWithProxy(u)	} 	stderr := new(bytes.Buffer)	if err = cmd.Run(&RunOpts{		Timeout: opts.Timeout,		Env:     envs,		Stdout:  io.Discard,		Stderr:  stderr,	}); err != nil {		return ConcatenateError(err, stderr.String())	}	return nil} // PushOptions options when push to remotetype PushOptions struct {	Remote         string	Branch         string	Force          bool	Mirror         bool	Env            []string	Timeout        time.Duration	PrivateKeyPath string} // Push pushs local commits to given remote branch.func Push(ctx context.Context, repoPath string, opts PushOptions) error {	cmd := NewCommand(ctx, "push") 	if opts.PrivateKeyPath != "" {		// Preserve the behavior that existing environments are used if no		// environments are passed.		if len(opts.Env) == 0 {			opts.Env = os.Environ()		} 		// Use environment because it takes precedence over using -c core.sshcommand		// and it's possible that a system might have an existing GIT_SSH_COMMAND		// environment set.		opts.Env = append(opts.Env, "GIT_SSH_COMMAND=ssh"+			fmt.Sprintf(` -i %s`, opts.PrivateKeyPath)+			" -o IdentitiesOnly=yes"+			// This will store new SSH host keys and verify connections to existing			// host keys, but it doesn't allow replacement of existing host keys. This			// means TOFU is used for Git over SSH pushes.			" -o StrictHostKeyChecking=accept-new"+			" -o UserKnownHostsFile="+filepath.Join(setting.SSH.RootPath, "known_hosts"))	} 	if opts.Force {		cmd.AddArguments("-f")	}	if opts.Mirror {		cmd.AddArguments("--mirror")	}	remoteBranchArgs := []string{opts.Remote}	if len(opts.Branch) > 0 {		remoteBranchArgs = append(remoteBranchArgs, opts.Branch)	}	cmd.AddDashesAndList(remoteBranchArgs...) 	if strings.Contains(opts.Remote, "://") && strings.Contains(opts.Remote, "@") {		cmd.SetDescription(fmt.Sprintf("push branch %s to %s (force: %t, mirror: %t)", opts.Branch, util.SanitizeCredentialURLs(opts.Remote), opts.Force, opts.Mirror))	} else {		cmd.SetDescription(fmt.Sprintf("push branch %s to %s (force: %t, mirror: %t)", opts.Branch, opts.Remote, opts.Force, opts.Mirror))	} 	stdout, stderr, err := cmd.RunStdString(&RunOpts{Env: opts.Env, Timeout: opts.Timeout, Dir: repoPath})	if err != nil {		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}			err.GenerateMessage()			return err		} else if strings.Contains(stderr, "matches more than one") {			return &ErrMoreThanOne{StdOut: stdout, StdErr: stderr, Err: err}		}		return fmt.Errorf("push failed: %w - %s\n%s", err, stderr, stdout)	} 	return nil} // GetLatestCommitTime returns time for latest commit in repository (across all branches)func GetLatestCommitTime(ctx context.Context, repoPath string) (time.Time, error) {	cmd := NewCommand(ctx, "for-each-ref", "--sort=-committerdate", BranchPrefix, "--count", "1", "--format=%(committerdate)")	stdout, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath})	if err != nil {		return time.Time{}, err	}	commitTime := strings.TrimSpace(stdout)	return time.Parse("Mon Jan _2 15:04:05 2006 -0700", commitTime)} // DivergeObject represents commit count diverging commitstype DivergeObject struct {	Ahead  int	Behind int} // GetDivergingCommits returns the number of commits a targetBranch is ahead or behind a baseBranchfunc GetDivergingCommits(ctx context.Context, repoPath, baseBranch, targetBranch string) (do DivergeObject, err error) {	cmd := NewCommand(ctx, "rev-list", "--count", "--left-right").		AddDynamicArguments(baseBranch + "..." + targetBranch).AddArguments("--")	stdout, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath})	if err != nil {		return do, err	}	left, right, found := strings.Cut(strings.Trim(stdout, "\n"), "\t")	if !found {		return do, fmt.Errorf("git rev-list output is missing a tab: %q", stdout)	} 	do.Behind, err = strconv.Atoi(left)	if err != nil {		return do, err	}	do.Ahead, err = strconv.Atoi(right)	if err != nil {		return do, err	}	return do, nil} // CreateBundle create bundle content to the target pathfunc (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) 	env := append(os.Environ(), "GIT_OBJECT_DIRECTORY="+filepath.Join(repo.Path, "objects"))	_, _, err = NewCommand(ctx, "init", "--bare").RunStdString(&RunOpts{Dir: tmp, Env: env})	if err != nil {		return err	} 	_, _, err = NewCommand(ctx, "reset", "--soft").AddDynamicArguments(commit).RunStdString(&RunOpts{Dir: tmp, Env: env})	if err != nil {		return err	} 	_, _, err = NewCommand(ctx, "branch", "-m", "bundle").RunStdString(&RunOpts{Dir: tmp, Env: env})	if err != nil {		return err	} 	tmpFile := filepath.Join(tmp, "bundle")	_, _, err = NewCommand(ctx, "bundle", "create").AddDynamicArguments(tmpFile, "bundle", "HEAD").RunStdString(&RunOpts{Dir: tmp, Env: env})	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}