mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-05-14 15:00:25 +00:00
Fixes #9629. New pull mirrors have credentials stored encrypted in the database, the same as push mirrors, rather than in the repository's `config` file. `git fetch` on the pull mirror is updated to use the credential store. Pull mirrors will have their credentials migrated to the encrypted storage in the database as they're synced or otherwise accessed via the web UI. ## Checklist The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. All work and communication must conform to Forgejo's [AI Agreement](https://codeberg.org/forgejo/governance/src/branch/main/AIAgreement.md). There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org). ### Tests for Go changes - I added test coverage for Go changes... - [ ] in their respective `*_test.go` for unit tests. - [x] in the `tests/integration` directory if it involves interactions with a live Forgejo server. - I ran... - [x] `make pr-go` before pushing ### Documentation - [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change. - [x] I did not document these changes and I do not expect someone else to do it. ### Release notes - [x] This change will be noticed by a Forgejo user or admin (feature, bug fix, performance, etc.). I suggest to include a release note for this change. - [ ] This change is not visible to a Forgejo user or admin (refactor, dependency upgrade, etc.). I think there is no need to add a release note for this change. Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/11909 Reviewed-by: Gusted <gusted@noreply.codeberg.org> Reviewed-by: Andreas Ahlenstorf <aahlenst@noreply.codeberg.org> Co-authored-by: Mathieu Fenniak <mathieu@fenniak.net> Co-committed-by: Mathieu Fenniak <mathieu@fenniak.net>
338 lines
9.3 KiB
Go
338 lines
9.3 KiB
Go
// 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 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.SplitSeq(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...)
|
|
|
|
envs := os.Environ()
|
|
parsedFromURL, err := url.Parse(from)
|
|
if err == nil {
|
|
envs = proxy.EnvWithProxy(parsedFromURL)
|
|
}
|
|
|
|
sanitizedFrom := util.SanitizeCredentialURLs(from)
|
|
|
|
fromURL, cleanup, err := cmd.AddAuthCredentialHelperForRemote(from)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cleanup()
|
|
|
|
cmd.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.SetDescription(fmt.Sprintf("clone branch %s from %s to %s (shared: %t, mirror: %t, depth: %d)", opts.Branch, sanitizedFrom, to, opts.Shared, opts.Mirror, opts.Depth))
|
|
cmd.AddDashesAndList(fromURL, to)
|
|
|
|
if opts.Timeout <= 0 {
|
|
opts.Timeout = -1
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// 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, env []string) (do DivergeObject, err error) {
|
|
cmd := NewCommand(ctx, "rev-list", "--count", "--left-right").
|
|
AddDynamicArguments(baseBranch + "..." + targetBranch).AddArguments("--")
|
|
stdout, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath, Env: env})
|
|
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
|
|
}
|