pull/35469/merge
Lunny Xiao 2025-11-14 06:40:46 -07:00 committed by GitHub
commit 1c33249c75
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 94 additions and 99 deletions

View File

@ -256,6 +256,9 @@ func (pr *PullRequest) LoadHeadRepo(ctx context.Context) (err error) {
return fmt.Errorf("pr[%d].LoadHeadRepo[%d]: %w", pr.ID, pr.HeadRepoID, err)
}
pr.isHeadRepoLoaded = true
if pr.IsSameRepo() && pr.BaseRepo == nil {
pr.BaseRepo = pr.HeadRepo
}
}
return nil
}
@ -322,6 +325,9 @@ func (pr *PullRequest) LoadBaseRepo(ctx context.Context) (err error) {
if err != nil {
return fmt.Errorf("pr[%d].LoadBaseRepo[%d]: %w", pr.ID, pr.BaseRepoID, err)
}
if pr.IsSameRepo() && pr.HeadRepo == nil {
pr.HeadRepo = pr.BaseRepo
}
return nil
}

29
modules/gitrepo/fetch.go Normal file
View File

@ -0,0 +1,29 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package gitrepo
import (
"context"
"fmt"
"code.gitea.io/gitea/modules/git/gitcmd"
)
// FetchRemoteBranch fetches a remote branch into a local branch
func FetchRemoteBranch(ctx context.Context, repo Repository, localBranch string, remoteRepo Repository, remoteBranch string, args ...string) error {
_, _, err := gitcmd.NewCommand("fetch", "--no-tags", "--refmap=").
AddDynamicArguments(repoPath(remoteRepo)).
// + means force fetch
AddDynamicArguments(fmt.Sprintf("+refs/heads/%s:%s", remoteBranch, localBranch)).
RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath(repo)})
return err
}
func FetchRemoteCommit(ctx context.Context, repo, remoteRepo Repository, commitID string) error {
_, _, err := gitcmd.NewCommand("fetch", "--no-tags").
AddDynamicArguments(repoPath(remoteRepo)).
AddDynamicArguments(commitID).
RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath(repo)})
return err
}

View File

@ -246,35 +246,30 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.
}
}
// Store old commit ID for review staleness checking
oldHeadCommitID := pr.HeadCommitID
pr.HeadCommitID = opts.NewCommitIDs[i]
if err = pull_service.UpdateRef(ctx, pr); err != nil {
if err = pull_service.UpdatePullRequestHeadRef(ctx, pr); err != nil {
return nil, fmt.Errorf("failed to update pull ref. Error: %w", err)
}
// Mark existing reviews as stale when PR content changes (same as regular GitHub flow)
if oldHeadCommitID != opts.NewCommitIDs[i] {
if err := issues_model.MarkReviewsAsStale(ctx, pr.IssueID); err != nil {
log.Error("MarkReviewsAsStale: %v", err)
}
if err := issues_model.MarkReviewsAsStale(ctx, pr.IssueID); err != nil {
log.Error("MarkReviewsAsStale: %v", err)
}
// Dismiss all approval reviews if protected branch rule item enabled
pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
if err != nil {
log.Error("GetFirstMatchProtectedBranchRule: %v", err)
}
if pb != nil && pb.DismissStaleApprovals {
if err := pull_service.DismissApprovalReviews(ctx, pusher, pr); err != nil {
log.Error("DismissApprovalReviews: %v", err)
}
// Dismiss all approval reviews if protected branch rule item enabled
pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
if err != nil {
log.Error("GetFirstMatchProtectedBranchRule: %v", err)
}
if pb != nil && pb.DismissStaleApprovals {
if err := pull_service.DismissApprovalReviews(ctx, pusher, pr); err != nil {
log.Error("DismissApprovalReviews: %v", err)
}
}
// Mark reviews for the new commit as not stale
if err := issues_model.MarkReviewsAsNotStale(ctx, pr.IssueID, opts.NewCommitIDs[i]); err != nil {
log.Error("MarkReviewsAsNotStale: %v", err)
}
// Mark reviews for the new commit as not stale
if err := issues_model.MarkReviewsAsNotStale(ctx, pr.IssueID, opts.NewCommitIDs[i]); err != nil {
log.Error("MarkReviewsAsNotStale: %v", err)
}
pull_service.StartPullRequestCheckImmediately(ctx, pr)

View File

@ -5,6 +5,7 @@ package pull
import (
"context"
"fmt"
issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
@ -52,6 +53,10 @@ func CreatePushPullComment(ctx context.Context, pusher *user_model.User, pr *iss
return nil, nil
}
if err := pr.LoadIssue(ctx); err != nil {
return nil, fmt.Errorf("unable to load issue for PR[%d]: %w", pr.ID, err)
}
opts := &issues_model.CreateCommentOptions{
Type: issues_model.CommentTypePullRequestPush,
Doer: pusher,

View File

@ -54,6 +54,10 @@ type NewPullRequestOptions struct {
// NewPullRequest creates new pull request with labels for repository.
func NewPullRequest(ctx context.Context, opts *NewPullRequestOptions) error {
if opts.PullRequest.Flow == issues_model.PullRequestFlowAGit && opts.PullRequest.HeadCommitID == "" {
return errors.New("head commit ID cannot be empty for agit flow")
}
repo, issue, labelIDs, uuids, pr, assigneeIDs := opts.Repo, opts.Issue, opts.LabelIDs, opts.AttachmentUUIDs, opts.PullRequest, opts.AssigneeIDs
if err := issue.LoadPoster(ctx); err != nil {
return err
@ -118,12 +122,8 @@ func NewPullRequest(ctx context.Context, opts *NewPullRequestOptions) error {
pr.Issue = issue
issue.PullRequest = pr
if pr.Flow == issues_model.PullRequestFlowGithub {
err = PushToBaseRepo(ctx, pr)
} else {
err = UpdateRef(ctx, pr)
}
if err != nil {
// update head commit id into git repository
if err = UpdatePullRequestHeadRef(ctx, pr); err != nil {
return err
}
@ -396,7 +396,7 @@ func AddTestPullRequestTask(opts TestPullRequestOptions) {
log.Trace("Updating PR[%d]: composing new test task", pr.ID)
pr.HeadRepo = repo // avoid loading again
if pr.Flow == issues_model.PullRequestFlowGithub {
if err := PushToBaseRepo(ctx, pr); err != nil {
if err := UpdatePullRequestHeadRef(ctx, pr); err != nil {
log.Error("PushToBaseRepo: %v", err)
continue
}
@ -549,68 +549,6 @@ func checkIfPRContentChanged(ctx context.Context, pr *issues_model.PullRequest,
return false, nil
}
// PushToBaseRepo pushes commits from branches of head repository to
// corresponding branches of base repository.
// FIXME: Only push branches that are actually updates?
func PushToBaseRepo(ctx context.Context, pr *issues_model.PullRequest) (err error) {
return pushToBaseRepoHelper(ctx, pr, "")
}
func pushToBaseRepoHelper(ctx context.Context, pr *issues_model.PullRequest, prefixHeadBranch string) (err error) {
log.Trace("PushToBaseRepo[%d]: pushing commits to base repo '%s'", pr.BaseRepoID, pr.GetGitHeadRefName())
if err := pr.LoadHeadRepo(ctx); err != nil {
log.Error("Unable to load head repository for PR[%d] Error: %v", pr.ID, err)
return err
}
headRepoPath := pr.HeadRepo.RepoPath()
if err := pr.LoadBaseRepo(ctx); err != nil {
log.Error("Unable to load base repository for PR[%d] Error: %v", pr.ID, err)
return err
}
baseRepoPath := pr.BaseRepo.RepoPath()
if err = pr.LoadIssue(ctx); err != nil {
return fmt.Errorf("unable to load issue %d for pr %d: %w", pr.IssueID, pr.ID, err)
}
if err = pr.Issue.LoadPoster(ctx); err != nil {
return fmt.Errorf("unable to load poster %d for pr %d: %w", pr.Issue.PosterID, pr.ID, err)
}
gitRefName := pr.GetGitHeadRefName()
if err := git.Push(ctx, headRepoPath, git.PushOptions{
Remote: baseRepoPath,
Branch: prefixHeadBranch + pr.HeadBranch + ":" + gitRefName,
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 {
if git.IsErrPushOutOfDate(err) {
// This should not happen as we're using force!
log.Error("Unable to push PR head for %s#%d (%-v:%s) due to ErrPushOfDate: %v", pr.BaseRepo.FullName(), pr.Index, pr.BaseRepo, gitRefName, err)
return err
} else if git.IsErrPushRejected(err) {
rejectErr := err.(*git.ErrPushRejected)
log.Info("Unable to push PR head for %s#%d (%-v:%s) due to rejection:\nStdout: %s\nStderr: %s\nError: %v", pr.BaseRepo.FullName(), pr.Index, pr.BaseRepo, gitRefName, rejectErr.StdOut, rejectErr.StdErr, rejectErr.Err)
return err
} else if git.IsErrMoreThanOne(err) {
if prefixHeadBranch != "" {
log.Info("Can't push with %s%s", prefixHeadBranch, pr.HeadBranch)
return err
}
log.Info("Retrying to push with %s%s", git.BranchPrefix, pr.HeadBranch)
err = pushToBaseRepoHelper(ctx, pr, git.BranchPrefix)
return err
}
log.Error("Unable to push PR head for %s#%d (%-v:%s) due to Error: %v", pr.BaseRepo.FullName(), pr.Index, pr.BaseRepo, gitRefName, err)
return fmt.Errorf("Push: %s:%s %s:%s %w", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), gitRefName, err)
}
return nil
}
// UpdatePullsRefs update all the PRs head file pointers like /refs/pull/1/head so that it will be dependent by other operations
func UpdatePullsRefs(ctx context.Context, repo *repo_model.Repository, update *repo_module.PushUpdateOptions) {
branch := update.RefFullName.BranchName()
@ -622,27 +560,38 @@ func UpdatePullsRefs(ctx context.Context, repo *repo_model.Repository, update *r
for _, pr := range prs {
log.Trace("Updating PR[%d]: composing new test task", pr.ID)
if pr.Flow == issues_model.PullRequestFlowGithub {
if err := PushToBaseRepo(ctx, pr); err != nil {
log.Error("PushToBaseRepo: %v", err)
if err := UpdatePullRequestHeadRef(ctx, pr); err != nil {
log.Error("UpdatePullRequestHead: %v", err)
}
}
}
}
}
// UpdateRef update refs/pull/id/head directly for agit flow pull request
func UpdateRef(ctx context.Context, pr *issues_model.PullRequest) (err error) {
log.Trace("UpdateRef[%d]: upgate pull request ref in base repo '%s'", pr.ID, pr.GetGitHeadRefName())
// UpdatePullRequestHeadRef updates the head reference of a pull request
func UpdatePullRequestHeadRef(ctx context.Context, pr *issues_model.PullRequest) error {
log.Trace("UpdatePullRequestHeadRef[%d]: update pull request ref in base repo '%s'", pr.ID, pr.GetGitHeadRefName())
if err := pr.LoadBaseRepo(ctx); err != nil {
log.Error("Unable to load base repository for PR[%d] Error: %v", pr.ID, err)
return err
}
if err := gitrepo.UpdateRef(ctx, pr.BaseRepo, pr.GetGitHeadRefName(), pr.HeadCommitID); err != nil {
log.Error("Unable to update ref in base repository for PR[%d] Error: %v", pr.ID, err)
if pr.Flow == issues_model.PullRequestFlowAGit {
if pr.HeadCommitID == "" {
return errors.New("head commit ID cannot be empty for agit flow")
}
return gitrepo.UpdateRef(ctx, pr.BaseRepo, pr.GetGitHeadRefName(), pr.HeadCommitID)
}
return err
if !pr.IsSameRepo() { // for cross repository pull request
if err := pr.LoadHeadRepo(ctx); err != nil {
return err
}
return gitrepo.FetchRemoteBranch(ctx, pr.BaseRepo, pr.GetGitHeadRefName(), pr.HeadRepo, pr.HeadBranch)
}
return gitrepo.UpdateRef(ctx, pr.BaseRepo, pr.GetGitHeadRefName(), pr.HeadBranch)
}
// retargetBranchPulls change target branch for all pull requests whose base branch is the branch

View File

@ -710,6 +710,14 @@ func GetBranchDivergingInfo(ctx reqctx.RequestContext, baseRepo *repo_model.Repo
return info, nil
}
// we need fetch the necessary commits from the head repo first if it's not the same repository
if baseRepo.ID != headRepo.ID {
// git default will gc the commit in 2 weeks, so it's safe to do the compare
if err := gitrepo.FetchRemoteCommit(ctx, baseRepo, headRepo, headGitBranch.CommitID); err != nil {
return nil, err
}
}
// if the fork repo has new commits, this call will fail because they are not in the base repo
// exit status 128 - fatal: Invalid symmetric difference expression aaaaaaaaaaaa...bbbbbbbbbbbb
// so at the moment, we first check the update time, then check whether the fork branch has base's head

View File

@ -79,6 +79,9 @@ func TestAPIPullUpdateByRebase(t *testing.T) {
AddTokenAuth(token)
session.MakeRequest(t, req, http.StatusOK)
// reload pr
pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
// Test GetDiverging after update
diffCount, err = gitrepo.GetDivergingCommits(t.Context(), pr.BaseRepo, pr.BaseBranch, pr.GetGitHeadRefName())
assert.NoError(t, err)