mirror of
https://github.com/go-gitea/gitea
synced 2025-09-10 10:48:28 +00:00
Move git config/remote to gitrepo package and add global lock to resolve possible conflict when updating repository git config file (#35151)
Partially fix #32018 `git config` and `git remote` write operations create a temporary file named `config.lock`. Since these operations are not atomic, they must not be run in parallel. If two requests attempt to modify the same repository concurrently—such as during a compare operation—one may fail due to the presence of an existing `config.lock` file. In cases where `config.lock` is left behind due to an unexpected program exit, a global lock mechanism could allow us to safely remove the stale lock file when a related error is detected. While this behavior is not yet implemented in this PR, it is planned for a future enhancement. --------- Signed-off-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
@@ -9,7 +9,6 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
giturl "code.gitea.io/gitea/modules/git/url"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
@@ -33,15 +32,6 @@ func GetRemoteAddress(ctx context.Context, repoPath, remoteName string) (string,
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetRemoteURL returns the url of a specific remote of the repository.
|
||||
func GetRemoteURL(ctx context.Context, repoPath, remoteName string) (*giturl.GitURL, error) {
|
||||
addr, err := GetRemoteAddress(ctx, repoPath, remoteName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return giturl.ParseGitURL(addr)
|
||||
}
|
||||
|
||||
// ErrInvalidCloneAddr represents a "InvalidCloneAddr" kind of error.
|
||||
type ErrInvalidCloneAddr struct {
|
||||
Host string
|
||||
|
@@ -38,6 +38,17 @@ func (repo *Repository) GetAllCommitsCount() (int64, error) {
|
||||
return AllCommitsCount(repo.Ctx, repo.Path, false)
|
||||
}
|
||||
|
||||
func (repo *Repository) ShowPrettyFormatLogToList(ctx context.Context, revisionRange string) ([]*Commit, error) {
|
||||
// avoid: ambiguous argument 'refs/a...refs/b': unknown revision or path not in the working tree. Use '--': 'git <command> [<revision>...] -- [<file>...]'
|
||||
logs, _, err := NewCommand("log").AddArguments(prettyLogFormat).
|
||||
AddDynamicArguments(revisionRange).AddArguments("--").
|
||||
RunStdBytes(ctx, &RunOpts{Dir: repo.Path})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return repo.parsePrettyFormatLogToList(logs)
|
||||
}
|
||||
|
||||
func (repo *Repository) parsePrettyFormatLogToList(logs []byte) ([]*Commit, error) {
|
||||
var commits []*Commit
|
||||
if len(logs) == 0 {
|
||||
|
@@ -79,12 +79,6 @@ func (repo *Repository) AddRemote(name, url string, fetch bool) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// RemoveRemote removes a remote from repository.
|
||||
func (repo *Repository) RemoveRemote(name string) error {
|
||||
_, _, err := NewCommand("remote", "rm").AddDynamicArguments(name).RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path})
|
||||
return err
|
||||
}
|
||||
|
||||
// RenameBranch rename a branch
|
||||
func (repo *Repository) RenameBranch(from, to string) error {
|
||||
_, _, err := NewCommand("branch", "-m").AddDynamicArguments(from, to).RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path})
|
||||
|
@@ -16,20 +16,8 @@ import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
logger "code.gitea.io/gitea/modules/log"
|
||||
)
|
||||
|
||||
// CompareInfo represents needed information for comparing references.
|
||||
type CompareInfo struct {
|
||||
MergeBase string
|
||||
BaseCommitID string
|
||||
HeadCommitID string
|
||||
Commits []*Commit
|
||||
NumFiles int
|
||||
}
|
||||
|
||||
// GetMergeBase checks and returns merge base of two branches and the reference used as base.
|
||||
func (repo *Repository) GetMergeBase(tmpRemote, base, head string) (string, string, error) {
|
||||
if tmpRemote == "" {
|
||||
@@ -49,83 +37,6 @@ func (repo *Repository) GetMergeBase(tmpRemote, base, head string) (string, stri
|
||||
return strings.TrimSpace(stdout), base, err
|
||||
}
|
||||
|
||||
// GetCompareInfo generates and returns compare information between base and head branches of repositories.
|
||||
func (repo *Repository) GetCompareInfo(basePath, baseBranch, headBranch string, directComparison, fileOnly bool) (_ *CompareInfo, err error) {
|
||||
var (
|
||||
remoteBranch string
|
||||
tmpRemote string
|
||||
)
|
||||
|
||||
// We don't need a temporary remote for same repository.
|
||||
if repo.Path != basePath {
|
||||
// Add a temporary remote
|
||||
tmpRemote = strconv.FormatInt(time.Now().UnixNano(), 10)
|
||||
if err = repo.AddRemote(tmpRemote, basePath, false); err != nil {
|
||||
return nil, fmt.Errorf("AddRemote: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := repo.RemoveRemote(tmpRemote); err != nil {
|
||||
logger.Error("GetPullRequestInfo: RemoveRemote: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
compareInfo := new(CompareInfo)
|
||||
|
||||
compareInfo.HeadCommitID, err = GetFullCommitID(repo.Ctx, repo.Path, headBranch)
|
||||
if err != nil {
|
||||
compareInfo.HeadCommitID = headBranch
|
||||
}
|
||||
|
||||
compareInfo.MergeBase, remoteBranch, err = repo.GetMergeBase(tmpRemote, baseBranch, headBranch)
|
||||
if err == nil {
|
||||
compareInfo.BaseCommitID, err = GetFullCommitID(repo.Ctx, repo.Path, remoteBranch)
|
||||
if err != nil {
|
||||
compareInfo.BaseCommitID = remoteBranch
|
||||
}
|
||||
separator := "..."
|
||||
baseCommitID := compareInfo.MergeBase
|
||||
if directComparison {
|
||||
separator = ".."
|
||||
baseCommitID = compareInfo.BaseCommitID
|
||||
}
|
||||
|
||||
// We have a common base - therefore we know that ... should work
|
||||
if !fileOnly {
|
||||
// avoid: ambiguous argument 'refs/a...refs/b': unknown revision or path not in the working tree. Use '--': 'git <command> [<revision>...] -- [<file>...]'
|
||||
var logs []byte
|
||||
logs, _, err = NewCommand("log").AddArguments(prettyLogFormat).
|
||||
AddDynamicArguments(baseCommitID+separator+headBranch).AddArguments("--").
|
||||
RunStdBytes(repo.Ctx, &RunOpts{Dir: repo.Path})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
compareInfo.Commits, err = repo.parsePrettyFormatLogToList(logs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsePrettyFormatLogToList: %w", err)
|
||||
}
|
||||
} else {
|
||||
compareInfo.Commits = []*Commit{}
|
||||
}
|
||||
} else {
|
||||
compareInfo.Commits = []*Commit{}
|
||||
compareInfo.MergeBase, err = GetFullCommitID(repo.Ctx, repo.Path, remoteBranch)
|
||||
if err != nil {
|
||||
compareInfo.MergeBase = remoteBranch
|
||||
}
|
||||
compareInfo.BaseCommitID = compareInfo.MergeBase
|
||||
}
|
||||
|
||||
// Count number of changed files.
|
||||
// This probably should be removed as we need to use shortstat elsewhere
|
||||
// Now there is git diff --shortstat but this appears to be slower than simply iterating with --nameonly
|
||||
compareInfo.NumFiles, err = repo.GetDiffNumChangedFiles(remoteBranch, headBranch, directComparison)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return compareInfo, nil
|
||||
}
|
||||
|
||||
type lineCountWriter struct {
|
||||
numLines int
|
||||
}
|
||||
|
48
modules/gitrepo/config.go
Normal file
48
modules/gitrepo/config.go
Normal file
@@ -0,0 +1,48 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package gitrepo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/globallock"
|
||||
)
|
||||
|
||||
func GitConfigGet(ctx context.Context, repo Repository, key string) (string, error) {
|
||||
result, _, err := git.NewCommand("config", "--get").
|
||||
AddDynamicArguments(key).
|
||||
RunStdString(ctx, &git.RunOpts{Dir: repoPath(repo)})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(result), nil
|
||||
}
|
||||
|
||||
func getRepoConfigLockKey(repoStoragePath string) string {
|
||||
return "repo-config:" + repoStoragePath
|
||||
}
|
||||
|
||||
// GitConfigAdd add a git configuration key to a specific value for the given repository.
|
||||
func GitConfigAdd(ctx context.Context, repo Repository, key, value string) error {
|
||||
return globallock.LockAndDo(ctx, getRepoConfigLockKey(repo.RelativePath()), func(ctx context.Context) error {
|
||||
_, _, err := git.NewCommand("config", "--add").
|
||||
AddDynamicArguments(key, value).
|
||||
RunStdString(ctx, &git.RunOpts{Dir: repoPath(repo)})
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// GitConfigSet updates a git configuration key to a specific value for the given repository.
|
||||
// If the key does not exist, it will be created.
|
||||
// If the key exists, it will be updated to the new value.
|
||||
func GitConfigSet(ctx context.Context, repo Repository, key, value string) error {
|
||||
return globallock.LockAndDo(ctx, getRepoConfigLockKey(repo.RelativePath()), func(ctx context.Context) error {
|
||||
_, _, err := git.NewCommand("config").
|
||||
AddDynamicArguments(key, value).
|
||||
RunStdString(ctx, &git.RunOpts{Dir: repoPath(repo)})
|
||||
return err
|
||||
})
|
||||
}
|
85
modules/gitrepo/remote.go
Normal file
85
modules/gitrepo/remote.go
Normal file
@@ -0,0 +1,85 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package gitrepo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
giturl "code.gitea.io/gitea/modules/git/url"
|
||||
"code.gitea.io/gitea/modules/globallock"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
type RemoteOption string
|
||||
|
||||
const (
|
||||
RemoteOptionMirrorPush RemoteOption = "--mirror=push"
|
||||
RemoteOptionMirrorFetch RemoteOption = "--mirror=fetch"
|
||||
)
|
||||
|
||||
func GitRemoteAdd(ctx context.Context, repo Repository, remoteName, remoteURL string, options ...RemoteOption) error {
|
||||
return globallock.LockAndDo(ctx, getRepoConfigLockKey(repo.RelativePath()), func(ctx context.Context) error {
|
||||
cmd := git.NewCommand("remote", "add")
|
||||
if len(options) > 0 {
|
||||
switch options[0] {
|
||||
case RemoteOptionMirrorPush:
|
||||
cmd.AddArguments("--mirror=push")
|
||||
case RemoteOptionMirrorFetch:
|
||||
cmd.AddArguments("--mirror=fetch")
|
||||
default:
|
||||
return errors.New("unknown remote option: " + string(options[0]))
|
||||
}
|
||||
}
|
||||
_, _, err := cmd.
|
||||
AddDynamicArguments(remoteName, remoteURL).
|
||||
RunStdString(ctx, &git.RunOpts{Dir: repoPath(repo)})
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func GitRemoteRemove(ctx context.Context, repo Repository, remoteName string) error {
|
||||
return globallock.LockAndDo(ctx, getRepoConfigLockKey(repo.RelativePath()), func(ctx context.Context) error {
|
||||
cmd := git.NewCommand("remote", "rm").AddDynamicArguments(remoteName)
|
||||
_, _, err := cmd.RunStdString(ctx, &git.RunOpts{Dir: repoPath(repo)})
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// GitRemoteGetURL returns the url of a specific remote of the repository.
|
||||
func GitRemoteGetURL(ctx context.Context, repo Repository, remoteName string) (*giturl.GitURL, error) {
|
||||
addr, err := git.GetRemoteAddress(ctx, repoPath(repo), remoteName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if addr == "" {
|
||||
return nil, util.NewNotExistErrorf("remote '%s' does not exist", remoteName)
|
||||
}
|
||||
return giturl.ParseGitURL(addr)
|
||||
}
|
||||
|
||||
// GitRemotePrune prunes the remote branches that no longer exist in the remote repository.
|
||||
func GitRemotePrune(ctx context.Context, repo Repository, remoteName string, timeout time.Duration, stdout, stderr io.Writer) error {
|
||||
return git.NewCommand("remote", "prune").AddDynamicArguments(remoteName).
|
||||
Run(ctx, &git.RunOpts{
|
||||
Timeout: timeout,
|
||||
Dir: repoPath(repo),
|
||||
Stdout: stdout,
|
||||
Stderr: stderr,
|
||||
})
|
||||
}
|
||||
|
||||
// GitRemoteUpdatePrune updates the remote branches and prunes the ones that no longer exist in the remote repository.
|
||||
func GitRemoteUpdatePrune(ctx context.Context, repo Repository, remoteName string, timeout time.Duration, stdout, stderr io.Writer) error {
|
||||
return git.NewCommand("remote", "update", "--prune").AddDynamicArguments(remoteName).
|
||||
Run(ctx, &git.RunOpts{
|
||||
Timeout: timeout,
|
||||
Dir: repoPath(repo),
|
||||
Stdout: stdout,
|
||||
Stderr: stderr,
|
||||
})
|
||||
}
|
@@ -14,8 +14,7 @@ import (
|
||||
|
||||
activities_model "code.gitea.io/gitea/models/activities"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
giturl "code.gitea.io/gitea/modules/git/url"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/repository"
|
||||
@@ -145,18 +144,12 @@ type remoteAddress struct {
|
||||
|
||||
func mirrorRemoteAddress(ctx context.Context, m *repo_model.Repository, remoteName string) remoteAddress {
|
||||
ret := remoteAddress{}
|
||||
remoteURL, err := git.GetRemoteAddress(ctx, m.RepoPath(), remoteName)
|
||||
u, err := gitrepo.GitRemoteGetURL(ctx, m, remoteName)
|
||||
if err != nil {
|
||||
log.Error("GetRemoteURL %v", err)
|
||||
return ret
|
||||
}
|
||||
|
||||
u, err := giturl.ParseGitURL(remoteURL)
|
||||
if err != nil {
|
||||
log.Error("giturl.Parse %v", err)
|
||||
return ret
|
||||
}
|
||||
|
||||
if u.Scheme != "ssh" && u.Scheme != "file" {
|
||||
if u.User != nil {
|
||||
ret.Username = u.User.Username()
|
||||
|
Reference in New Issue
Block a user