1
1
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:
Lunny Xiao
2025-09-01 11:47:04 -07:00
committed by GitHub
parent 4e1b8db1fc
commit d2e994db2c
19 changed files with 312 additions and 225 deletions

View File

@@ -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

View File

@@ -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 {

View File

@@ -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})

View File

@@ -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
View 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
View 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,
})
}

View File

@@ -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()