mirror of https://github.com/go-gitea/gitea.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
318 lines
8.9 KiB
318 lines
8.9 KiB
// Copyright 2015 The Gogs Authors. All rights reserved. |
|
// Copyright 2017 The Gitea Authors. All rights reserved. |
|
// SPDX-License-Identifier: MIT |
|
|
|
package git |
|
|
|
import ( |
|
"bytes" |
|
"context" |
|
"fmt" |
|
"io" |
|
"net/url" |
|
"os" |
|
"path" |
|
"path/filepath" |
|
"strconv" |
|
"strings" |
|
"time" |
|
|
|
"code.gitea.io/gitea/modules/proxy" |
|
"code.gitea.io/gitea/modules/util" |
|
) |
|
|
|
// GPGSettings represents the default GPG settings for this repository |
|
type GPGSettings struct { |
|
Sign bool |
|
KeyID string |
|
Email string |
|
Name string |
|
PublicKeyContent string |
|
} |
|
|
|
const prettyLogFormat = `--pretty=format:%H` |
|
|
|
// GetAllCommitsCount returns count of all commits in repository |
|
func (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 DefaultFeatures().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 repository |
|
type 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 remote |
|
type PushOptions struct { |
|
Remote string |
|
Branch string |
|
Force bool |
|
Mirror bool |
|
Env []string |
|
Timeout time.Duration |
|
} |
|
|
|
// Push pushs local commits to given remote branch. |
|
func Push(ctx context.Context, repoPath string, opts PushOptions) error { |
|
cmd := NewCommand(ctx, "push") |
|
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 commits |
|
type DivergeObject struct { |
|
Ahead int |
|
Behind int |
|
} |
|
|
|
// GetDivergingCommits returns the number of commits a targetBranch is ahead or behind a baseBranch |
|
func 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 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) |
|
|
|
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 |
|
}
|
|
|