diff --git a/modules/gitrepo/fetch.go b/modules/gitrepo/fetch.go index 5c86f70cac..0474d6111e 100644 --- a/modules/gitrepo/fetch.go +++ b/modules/gitrepo/fetch.go @@ -7,6 +7,7 @@ import ( "context" "code.gitea.io/gitea/modules/git/gitcmd" + "code.gitea.io/gitea/modules/globallock" ) // FetchRemoteCommit fetches a specific commit and its related objects from a remote @@ -19,7 +20,9 @@ import ( // This behavior is sufficient for temporary operations, such as determining the // merge base between commits. func FetchRemoteCommit(ctx context.Context, repo, remoteRepo Repository, commitID string) error { - return RunCmd(ctx, repo, gitcmd.NewCommand("fetch", "--no-tags"). - AddDynamicArguments(repoPath(remoteRepo)). - AddDynamicArguments(commitID)) + return globallock.LockAndDo(ctx, getRepoWriteLockKey(repo.RelativePath()), func(ctx context.Context) error { + return RunCmd(ctx, repo, gitcmd.NewCommand("fetch", "--no-tags"). + AddDynamicArguments(repoPath(remoteRepo)). + AddDynamicArguments(commitID)) + }) } diff --git a/modules/gitrepo/push.go b/modules/gitrepo/push.go index 920c317f79..a8945b5392 100644 --- a/modules/gitrepo/push.go +++ b/modules/gitrepo/push.go @@ -7,6 +7,7 @@ import ( "context" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/globallock" ) // PushToExternal pushes a managed repository to an external remote. @@ -16,12 +17,16 @@ func PushToExternal(ctx context.Context, repo Repository, opts git.PushOptions) // Push pushes from one managed repository to another managed repository. func Push(ctx context.Context, fromRepo, toRepo Repository, opts git.PushOptions) error { - opts.Remote = repoPath(toRepo) - return git.Push(ctx, repoPath(fromRepo), opts) + return globallock.LockAndDo(ctx, getRepoWriteLockKey(toRepo.RelativePath()), func(ctx context.Context) error { + opts.Remote = repoPath(toRepo) + return git.Push(ctx, repoPath(fromRepo), opts) + }) } // PushFromLocal pushes from a local path to a managed repository. func PushFromLocal(ctx context.Context, fromLocalPath string, toRepo Repository, opts git.PushOptions) error { - opts.Remote = repoPath(toRepo) - return git.Push(ctx, fromLocalPath, opts) + return globallock.LockAndDo(ctx, getRepoWriteLockKey(toRepo.RelativePath()), func(ctx context.Context) error { + opts.Remote = repoPath(toRepo) + return git.Push(ctx, fromLocalPath, opts) + }) } diff --git a/modules/gitrepo/repo_lock.go b/modules/gitrepo/repo_lock.go new file mode 100644 index 0000000000..2eb89ce807 --- /dev/null +++ b/modules/gitrepo/repo_lock.go @@ -0,0 +1,10 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitrepo + +// getRepoWriteLockKey returns the global lock key for write operations on the repository. +// Parallel write operations on the same git repository should be avoided to prevent data corruption. +func getRepoWriteLockKey(repoStoragePath string) string { + return "repo-write:" + repoStoragePath +} diff --git a/services/git/compare.go b/services/git/compare.go index 48c60688e3..9e4645c5b1 100644 --- a/services/git/compare.go +++ b/services/git/compare.go @@ -65,8 +65,11 @@ func GetCompareInfo(ctx context.Context, baseRepo, headRepo *repo_model.Reposito // if they are not the same repository, then we need to fetch the base commit into the head repository // because we will use headGitRepo in the following code if baseRepo.ID != headRepo.ID { - if err := gitrepo.FetchRemoteCommit(ctx, headRepo, baseRepo, compareInfo.BaseCommitID); err != nil { - return nil, fmt.Errorf("FetchRemoteCommit: %w", err) + exist := headGitRepo.IsCommitExist(compareInfo.BaseCommitID) + if !exist { + if err := gitrepo.FetchRemoteCommit(ctx, headRepo, baseRepo, compareInfo.BaseCommitID); err != nil { + return nil, fmt.Errorf("FetchRemoteCommit: %w", err) + } } }