Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions models/fixtures/branch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -213,3 +213,15 @@
is_deleted: false
deleted_by_id: 0
deleted_unix: 0

-
id: 26
repo_id: 10
name: 'feature/1'
commit_id: '65f1bf27bc3bf70f64657658635e66094edbcb4d'
commit_message: 'Initial commit'
commit_time: 1489950479
pusher_id: 2
is_deleted: false
deleted_by_id: 0
deleted_unix: 0
13 changes: 8 additions & 5 deletions routers/web/repo/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -1583,7 +1583,10 @@ func UpdatePullRequestTarget(ctx *context.Context) {
}

if err := pull_service.ChangeTargetBranch(ctx, pr, ctx.Doer, targetBranch); err != nil {
if issues_model.IsErrPullRequestAlreadyExists(err) {
switch {
case git_model.IsErrBranchNotExist(err):
ctx.HTTPError(http.StatusBadRequest)
case issues_model.IsErrPullRequestAlreadyExists(err):
err := err.(issues_model.ErrPullRequestAlreadyExists)

RepoRelPath := ctx.Repo.Owner.Name + "/" + ctx.Repo.Repository.Name
Expand All @@ -1594,31 +1597,31 @@ func UpdatePullRequestTarget(ctx *context.Context) {
"error": err.Error(),
"user_error": errorMessage,
})
} else if issues_model.IsErrIssueIsClosed(err) {
case issues_model.IsErrIssueIsClosed(err):
errorMessage := ctx.Tr("repo.pulls.is_closed")

ctx.Flash.Error(errorMessage)
ctx.JSON(http.StatusConflict, map[string]any{
"error": err.Error(),
"user_error": errorMessage,
})
} else if pull_service.IsErrPullRequestHasMerged(err) {
case pull_service.IsErrPullRequestHasMerged(err):
errorMessage := ctx.Tr("repo.pulls.has_merged")

ctx.Flash.Error(errorMessage)
ctx.JSON(http.StatusConflict, map[string]any{
"error": err.Error(),
"user_error": errorMessage,
})
} else if git_model.IsErrBranchesEqual(err) {
case git_model.IsErrBranchesEqual(err):
errorMessage := ctx.Tr("repo.pulls.nothing_to_compare")

ctx.Flash.Error(errorMessage)
ctx.JSON(http.StatusBadRequest, map[string]any{
"error": err.Error(),
"user_error": errorMessage,
})
} else {
default:
ctx.ServerError("UpdatePullRequestTarget", err)
}
return
Expand Down
1 change: 1 addition & 0 deletions services/pull/comment.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ func CreatePushPullComment(ctx context.Context, pusher *user_model.User, pr *iss
var data issues_model.PushActionContent
if opts.IsForcePush {
data.CommitIDs = []string{oldCommitID, newCommitID}
data.IsForcePush = true
} else {
data.CommitIDs, err = getCommitIDsFromRepo(ctx, pr.BaseRepo, oldCommitID, newCommitID, pr.BaseBranch)
if err != nil {
Expand Down
11 changes: 11 additions & 0 deletions services/pull/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,17 @@ func ChangeTargetBranch(ctx context.Context, pr *issues_model.PullRequest, doer
}
}

exist, err := git_model.IsBranchExist(ctx, pr.BaseRepoID, targetBranch)
if err != nil {
return err
}
if !exist {
return git_model.ErrBranchNotExist{
RepoID: pr.BaseRepoID,
BranchName: targetBranch,
}
}

// Check if branches are equal
branchesEqual, err := IsHeadEqualWithBranch(ctx, pr, targetBranch)
if err != nil {
Expand Down
15 changes: 15 additions & 0 deletions tests/integration/git_helper_for_declarative_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,3 +205,18 @@ func doGitPull(dstPath string, args ...string) func(*testing.T) {
assert.NoError(t, err)
}
}

func doGitCommit(dstPath, commitMessage string) func(*testing.T) {
return func(t *testing.T) {
signature := git.Signature{
Email: "[email protected]",
Name: "test",
When: time.Now(),
}
assert.NoError(t, git.CommitChanges(t.Context(), dstPath, git.CommitChangesOptions{
Committer: &signature,
Author: &signature,
Message: commitMessage,
}))
}
}
120 changes: 120 additions & 0 deletions tests/integration/pull_comment_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package integration

import (
"fmt"
"net/http"
"net/url"
"os"
"testing"
"time"

issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/git/gitcmd"
issues_service "code.gitea.io/gitea/services/issue"

"github.com/stretchr/testify/assert"
)

func TestPull_RebaseComment(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
session := loginUser(t, "user1")
testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
testPullCreate(t, session, "user1", "repo1", false, "master", "master", "This is a pull title")

// create a conflict line on user2/repo1:master README.md
testEditFile(t, session, "user2", "repo1", "master", "README.md", "Hello, World (Edited Conflicted)\n")

// Now the pull request status should be conflicted
prIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: "This is a pull title"})
assert.NoError(t, prIssue.LoadPullRequest(t.Context()))
assert.Equal(t, issues_model.PullRequestStatusConflict, prIssue.PullRequest.Status)
assert.NoError(t, prIssue.PullRequest.LoadBaseRepo(t.Context()))
assert.Equal(t, "user2/repo1", prIssue.PullRequest.BaseRepo.FullName())
assert.NoError(t, prIssue.PullRequest.LoadHeadRepo(t.Context()))
assert.Equal(t, "user1/repo1", prIssue.PullRequest.HeadRepo.FullName())

dstPath := t.TempDir()
u.Path = "/user2/repo1.git"
doGitClone(dstPath, u)(t)
doGitCreateBranch(dstPath, "dev")(t)
content, err := os.ReadFile(dstPath + "/README.md")
assert.NoError(t, err)
assert.Equal(t, "Hello, World (Edited Conflicted)\n", string(content))

err = os.WriteFile(dstPath+"/README.md", []byte("Hello, World (Edited Conflict Resolved)\n"), 0o644)
assert.NoError(t, err)
_, _, err = gitcmd.NewCommand().AddArguments("add", "--all").RunStdString(t.Context(), &gitcmd.RunOpts{Dir: dstPath})
assert.NoError(t, err)
doGitCommit(dstPath, "Resolve conflict")(t)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is already doGitAddSomeCommits?

Copy link
Member Author

@lunny lunny Oct 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That function can not be used here because we need to make a conflict line.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not see why it should duplicate the code by copy&paste

2bea40d


// do force push
u.Path = "/user1/repo1.git"
u.User = url.UserPassword("user1", userPassword)
doGitAddRemote(dstPath, "fork", u)(t)
// non force push will fail
_, _, err = gitcmd.NewCommand().AddArguments("push", "fork", "dev:master").RunStdString(t.Context(), &gitcmd.RunOpts{Dir: dstPath})
assert.Error(t, err)
_, _, err = gitcmd.NewCommand().AddArguments("push", "--force", "fork", "dev:master").RunStdString(t.Context(), &gitcmd.RunOpts{Dir: dstPath})
assert.NoError(t, err)

time.Sleep(time.Second) // wait for pull request conflict checking

// reload the pr
prIssue = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: "This is a pull title"})
assert.NoError(t, prIssue.LoadPullRequest(t.Context()))
assert.Equal(t, issues_model.PullRequestStatusMergeable, prIssue.PullRequest.Status)
comments, err := issues_model.FindComments(t.Context(), &issues_model.FindCommentsOptions{
IssueID: prIssue.ID,
Type: issues_model.CommentTypeUndefined, // get all comments type
})
assert.NoError(t, err)
lastComment := comments[len(comments)-1]
err = issues_service.LoadCommentPushCommits(t.Context(), lastComment)
assert.NoError(t, err)
assert.True(t, lastComment.IsForcePush)
})
}

func TestPull_RetargetComment(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
session := loginUser(t, "user1")
testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
testPullCreate(t, session, "user1", "repo1", false, "master", "master", "This is a pull title")

session2 := loginUser(t, "user2")
// create a non-conflict branch dev from master
testCreateBranch(t, session2, "user2", "repo1", "branch/master", "dev", http.StatusSeeOther)

// create a conflict line on user2/repo1:master README.md
testEditFile(t, session2, "user2", "repo1", "master", "README.md", "Hello, World (Edited Conflicted)\n")

// Now the pull request status should be conflicted
prIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: "This is a pull title"})
assert.NoError(t, prIssue.LoadPullRequest(t.Context()))
assert.Equal(t, issues_model.PullRequestStatusConflict, prIssue.PullRequest.Status)
assert.NoError(t, prIssue.PullRequest.LoadBaseRepo(t.Context()))
assert.Equal(t, "user2/repo1", prIssue.PullRequest.BaseRepo.FullName())
assert.NoError(t, prIssue.PullRequest.LoadHeadRepo(t.Context()))
assert.Equal(t, "user1/repo1", prIssue.PullRequest.HeadRepo.FullName())

// do retarget
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/user2/repo1/pull/%d/target_branch", prIssue.PullRequest.Index), map[string]string{
"_csrf": GetUserCSRFToken(t, session2),
"target_branch": "dev",
})
session2.MakeRequest(t, req, http.StatusOK)

time.Sleep(time.Second) // wait for pull request conflict checking

// reload the pr
prIssue = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: "This is a pull title"})
assert.NoError(t, prIssue.LoadPullRequest(t.Context()))
assert.Equal(t, issues_model.PullRequestStatusMergeable, prIssue.PullRequest.Status)
})
}