fix(pull): fetch PR head refs instead of pushing them (#39603)

Creating a PR fetches the head commit into the base repo, then pushes
the same objects into `refs/pull/N/head`. Since git 2.54, background
repacks race that push:

1. The push can be rejected with "unable to migrate objects to permanent
storage", see
https://github.com/go-gitea/gitea/actions/runs/37171442185/job/111345034829.
2. For heads with 100+ new objects, the repack can delete the reused
pack, leaving the PR ref pointing at missing objects.

Fetching the head commit with `FetchRemoteTempCommit` and setting the PR
ref with `UpdateRef` avoids both, as the fetch transfers nothing when
the objects exist. Fork PR refs are now updated like AGit PR refs
already are, without going through receive hooks.

Also syncs the PR ref when a PR is reopened again.
https://github.com/go-gitea/gitea/pull/37077 inverted that condition, so
a reopened PR kept a stale ref.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: bircni <bircni@icloud.com>
This commit is contained in:
authored and GitHub committed 2026-10-05 20:31:21 +02:00
1 parent d16b20b972
commit 4e2c3e43f2
2 files changed
+23 -70

No files matched your search

+11 -18
View File
@@ -556,11 +556,10 @@ func checkIfPRContentChanged(ctx context.Context, pr *issues_model.PullRequest,
return false, mergeBase, nil
}
// PushToBaseRepo pushes commits from branches of head repository to
// corresponding branches of base repository.
// FIXME: Only push branches that are actually updates?
// PushToBaseRepo fetches the head branch commit into the base repository and points the PR head ref at it.
// FIXME: Only update refs that actually changed?
func PushToBaseRepo(ctx context.Context, pr *issues_model.PullRequest) error {
log.Trace("PushToBaseRepo[%d]: pushing commits to base repo '%s'", pr.BaseRepoID, pr.GetGitHeadRefName())
log.Trace("PushToBaseRepo[%d]: updating base repo ref '%s'", pr.BaseRepoID, pr.GetGitHeadRefName())
if err := pr.LoadHeadRepo(ctx); err != nil {
return err
@@ -571,23 +570,17 @@ func PushToBaseRepo(ctx context.Context, pr *issues_model.PullRequest) error {
if err := pr.LoadIssue(ctx); err != nil {
return err
}
if err := pr.Issue.LoadPoster(ctx); err != nil {
headCommitID, err := git.GetFullCommitID(ctx, pr.HeadRepo, git.BranchPrefix+pr.HeadBranch)
if err != nil {
return err
}
baseRepoHeadRefName := pr.GetGitHeadRefName()
if err := git.PushManaged(ctx, pr.HeadRepo, pr.BaseRepo, git.PushOptions{
Branch: git.BranchPrefix + pr.HeadBranch + ":" + baseRepoHeadRefName,
Force: true,
// Use InternalPushingEnvironment here because we know that pre-receive and post-receive do not run on a refs/pulls/...
Env: repo_module.InternalPushingEnvironment(pr.Issue.Poster, pr.BaseRepo),
}); err != nil {
// Since we use internal force-push, there should be no git error.
// If any error happens, it must be an internal error (e.g.: broken git hooks) but not user error.
return fmt.Errorf("unable to push from head branch %s:%s to base repo %s:%s, err: %w",
pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), baseRepoHeadRefName, err)
// fetch, not push: pushing objects FetchRemoteTempCommit already fetched races background repacks
if err := git.FetchRemoteTempCommit(ctx, pr.BaseRepo, pr.HeadRepo, headCommitID); err != nil {
return fmt.Errorf("unable to fetch head branch %s:%s into base repo %s, err: %w",
pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), err)
}
return nil
return git.UpdateRef(ctx, pr.BaseRepo, pr.GetGitHeadRefName(), headCommitID)
}
// UpdatePullsRefs update all the PRs head file pointers like /refs/pull/1/head so that it will be dependent by other operations