forgejo/modules/git/repo.go
zeripath 5cb0c9aa0d
Propagate context and ensure git commands run in request context ()
This PR continues the work in  by progressively ensuring that git
commands run within the request context.

This now means that the if there is a git repo already open in the context it will be used instead of reopening it.

Signed-off-by: Andrew Thornton <art27@cantab.net>
2022-01-19 23:26:57 +00:00

317 lines
8.1 KiB
Go

// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2017 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package git
import (
"bytes"
"context"
"fmt"
"io"
"net/url"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"time"
"code.gitea.io/gitea/modules/proxy"
)
// 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 := NewCommandContext(ctx, "ls-remote", "-q", "-h", url, "HEAD").Run()
return err == nil
}
// InitRepository initializes a new Git repository.
func InitRepository(ctx context.Context, repoPath string, bare bool) error {
err := os.MkdirAll(repoPath, os.ModePerm)
if err != nil {
return err
}
cmd := NewCommandContext(ctx, "init")
if bare {
cmd.AddArguments("--bare")
}
_, err = cmd.RunInDir(repoPath)
return err
}
// IsEmpty Check if repository is empty.
func (repo *Repository) IsEmpty() (bool, error) {
var errbuf strings.Builder
if err := NewCommandContext(repo.Ctx, "log", "-1").RunInDirPipeline(repo.Path, nil, &errbuf); err != nil {
if strings.Contains(errbuf.String(), "fatal: bad default revision 'HEAD'") ||
strings.Contains(errbuf.String(), "fatal: your current branch 'master' does not have any commits yet") {
return true, nil
}
return true, fmt.Errorf("check empty: %v - %s", err, errbuf.String())
}
return false, 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
}
// Clone clones original repository to target path.
func Clone(ctx context.Context, from, to string, opts CloneRepoOptions) error {
cargs := make([]string, len(GlobalCommandArgs))
copy(cargs, GlobalCommandArgs)
return CloneWithArgs(ctx, from, to, cargs, opts)
}
// CloneWithArgs original repository to target path.
func CloneWithArgs(ctx context.Context, from, to string, args []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.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", strconv.Itoa(opts.Depth))
}
if len(opts.Branch) > 0 {
cmd.AddArguments("-b", opts.Branch)
}
cmd.AddArguments("--", from, to)
if opts.Timeout <= 0 {
opts.Timeout = -1
}
var envs = os.Environ()
u, err := url.Parse(from)
if err == nil && (strings.EqualFold(u.Scheme, "http") || strings.EqualFold(u.Scheme, "https")) {
if proxy.Match(u.Host) {
envs = append(envs, fmt.Sprintf("https_proxy=%s", proxy.GetProxyURL()))
}
}
var stderr = new(bytes.Buffer)
if err = cmd.RunWithContext(&RunContext{
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 := NewCommandContext(ctx, "push")
if opts.Force {
cmd.AddArguments("-f")
}
if opts.Mirror {
cmd.AddArguments("--mirror")
}
cmd.AddArguments("--", opts.Remote)
if len(opts.Branch) > 0 {
cmd.AddArguments(opts.Branch)
}
var outbuf, errbuf strings.Builder
if opts.Timeout == 0 {
opts.Timeout = -1
}
err := cmd.RunInDirTimeoutEnvPipeline(opts.Env, opts.Timeout, repoPath, &outbuf, &errbuf)
if err != nil {
if strings.Contains(errbuf.String(), "non-fast-forward") {
return &ErrPushOutOfDate{
StdOut: outbuf.String(),
StdErr: errbuf.String(),
Err: err,
}
} else if strings.Contains(errbuf.String(), "! [remote rejected]") {
err := &ErrPushRejected{
StdOut: outbuf.String(),
StdErr: errbuf.String(),
Err: err,
}
err.GenerateMessage()
return err
} else if strings.Contains(errbuf.String(), "matches more than one") {
err := &ErrMoreThanOne{
StdOut: outbuf.String(),
StdErr: errbuf.String(),
Err: err,
}
return err
}
}
if errbuf.Len() > 0 && err != nil {
return fmt.Errorf("%v - %s", err, errbuf.String())
}
return err
}
// GetLatestCommitTime returns time for latest commit in repository (across all branches)
func GetLatestCommitTime(ctx context.Context, repoPath string) (time.Time, error) {
cmd := NewCommandContext(ctx, "for-each-ref", "--sort=-committerdate", BranchPrefix, "--count", "1", "--format=%(committerdate)")
stdout, err := cmd.RunInDir(repoPath)
if err != nil {
return time.Time{}, err
}
commitTime := strings.TrimSpace(stdout)
return time.Parse(GitTimeLayout, commitTime)
}
// DivergeObject represents commit count diverging commits
type DivergeObject struct {
Ahead int
Behind int
}
func checkDivergence(ctx context.Context, repoPath, baseBranch, targetBranch string) (int, error) {
branches := fmt.Sprintf("%s..%s", baseBranch, targetBranch)
cmd := NewCommandContext(ctx, "rev-list", "--count", branches)
stdout, err := cmd.RunInDir(repoPath)
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
func GetDivergingCommits(ctx context.Context, repoPath, baseBranch, targetBranch string) (DivergeObject, error) {
// $(git rev-list --count master..feature) commits ahead of master
ahead, errorAhead := checkDivergence(ctx, repoPath, baseBranch, targetBranch)
if errorAhead != nil {
return DivergeObject{}, errorAhead
}
// $(git rev-list --count feature..master) commits behind master
behind, errorBehind := checkDivergence(ctx, repoPath, targetBranch, baseBranch)
if errorBehind != nil {
return DivergeObject{}, errorBehind
}
return DivergeObject{ahead, behind}, 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 = NewCommandContext(ctx, "init", "--bare").RunInDirWithEnv(tmp, env)
if err != nil {
return err
}
_, err = NewCommandContext(ctx, "reset", "--soft", commit).RunInDirWithEnv(tmp, env)
if err != nil {
return err
}
_, err = NewCommandContext(ctx, "branch", "-m", "bundle").RunInDirWithEnv(tmp, env)
if err != nil {
return err
}
tmpFile := filepath.Join(tmp, "bundle")
_, err = NewCommandContext(ctx, "bundle", "create", tmpFile, "bundle", "HEAD").RunInDirWithEnv(tmp, 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
}