summaryrefslogtreecommitdiffstats
path: root/modules/git/repo.go
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--modules/git/repo.go342
1 files changed, 342 insertions, 0 deletions
diff --git a/modules/git/repo.go b/modules/git/repo.go
new file mode 100644
index 0000000..84db08d
--- /dev/null
+++ b/modules/git/repo.go
@@ -0,0 +1,342 @@
+// 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"
+
+ "code.gitea.io/gitea/modules/proxy"
+ "code.gitea.io/gitea/modules/setting"
+ "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 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
+ 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 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
+}