Skip to content

Commit 79f4cd7

Browse files
authored
Fix bugs when comparing and creating pull request (#36144)
Backport #36166
1 parent 522cc25 commit 79f4cd7

File tree

7 files changed

+189
-16
lines changed

7 files changed

+189
-16
lines changed

models/user/user.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1444,3 +1444,15 @@ func DisabledFeaturesWithLoginType(user *User) *container.Set[string] {
14441444
}
14451445
return &setting.Admin.UserDisabledFeatures
14461446
}
1447+
1448+
// GetUserOrOrgByName returns the user or org by name
1449+
func GetUserOrOrgByName(ctx context.Context, name string) (*User, error) {
1450+
var u User
1451+
has, err := db.GetEngine(ctx).Where("lower_name = ?", strings.ToLower(name)).Get(&u)
1452+
if err != nil {
1453+
return nil, err
1454+
} else if !has {
1455+
return nil, ErrUserNotExist{Name: name}
1456+
}
1457+
return &u, nil
1458+
}

options/locale/locale_en-US.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1858,6 +1858,7 @@ pulls.desc = Enable pull requests and code reviews.
18581858
pulls.new = New Pull Request
18591859
pulls.new.blocked_user = Cannot create pull request because you are blocked by the repository owner.
18601860
pulls.new.must_collaborator = You must be a collaborator to create pull request.
1861+
pulls.new.already_existed = A pull request between these branches already exists
18611862
pulls.edit.already_changed = Unable to save changes to the pull request. It appears the content has already been changed by another user. Please refresh the page and try editing again to avoid overwriting their changes.
18621863
pulls.view = View Pull Request
18631864
pulls.compare_changes = New Pull Request

routers/api/v1/repo/pull.go

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"code.gitea.io/gitea/modules/timeutil"
3030
"code.gitea.io/gitea/modules/web"
3131
"code.gitea.io/gitea/routers/api/v1/utils"
32+
"code.gitea.io/gitea/routers/common"
3233
asymkey_service "code.gitea.io/gitea/services/asymkey"
3334
"code.gitea.io/gitea/services/automerge"
3435
"code.gitea.io/gitea/services/context"
@@ -1076,7 +1077,7 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
10761077
} else if len(headInfos) == 2 {
10771078
// There is a head repository (the head repository could also be the same base repo)
10781079
headRefToGuess = headInfos[1]
1079-
headUser, err = user_model.GetUserByName(ctx, headInfos[0])
1080+
headUser, err = user_model.GetUserOrOrgByName(ctx, headInfos[0])
10801081
if err != nil {
10811082
if user_model.IsErrUserNotExist(err) {
10821083
ctx.APIErrorNotFound("GetUserByName")
@@ -1092,28 +1093,23 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
10921093

10931094
isSameRepo := ctx.Repo.Owner.ID == headUser.ID
10941095

1095-
// Check if current user has fork of repository or in the same repository.
1096-
headRepo := repo_model.GetForkedRepo(ctx, headUser.ID, baseRepo.ID)
1097-
if headRepo == nil && !isSameRepo {
1098-
err = baseRepo.GetBaseRepo(ctx)
1096+
var headRepo *repo_model.Repository
1097+
if isSameRepo {
1098+
headRepo = baseRepo
1099+
} else {
1100+
headRepo, err = common.FindHeadRepo(ctx, baseRepo, headUser.ID)
10991101
if err != nil {
11001102
ctx.APIErrorInternal(err)
11011103
return nil, nil
11021104
}
1103-
1104-
// Check if baseRepo's base repository is the same as headUser's repository.
1105-
if baseRepo.BaseRepo == nil || baseRepo.BaseRepo.OwnerID != headUser.ID {
1106-
log.Trace("parseCompareInfo[%d]: does not have fork or in same repository", baseRepo.ID)
1107-
ctx.APIErrorNotFound("GetBaseRepo")
1105+
if headRepo == nil {
1106+
ctx.APIErrorNotFound("head repository not found")
11081107
return nil, nil
11091108
}
1110-
// Assign headRepo so it can be used below.
1111-
headRepo = baseRepo.BaseRepo
11121109
}
11131110

11141111
var headGitRepo *git.Repository
11151112
if isSameRepo {
1116-
headRepo = ctx.Repo.Repository
11171113
headGitRepo = ctx.Repo.GitRepo
11181114
closer = func() {} // no need to close the head repo because it shares the base repo
11191115
} else {
@@ -1137,7 +1133,7 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
11371133
return nil, nil
11381134
}
11391135

1140-
if !permBase.CanReadIssuesOrPulls(true) || !permBase.CanRead(unit.TypeCode) {
1136+
if !permBase.CanRead(unit.TypeCode) {
11411137
log.Trace("Permission Denied: User %-v cannot create/read pull requests or cannot read code in Repo %-v\nUser in baseRepo has Permissions: %-+v", ctx.Doer, baseRepo, permBase)
11421138
ctx.APIErrorNotFound("Can't read pulls or can't read UnitTypeCode")
11431139
return nil, nil

routers/common/compare.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
package common
55

66
import (
7+
"context"
8+
79
repo_model "code.gitea.io/gitea/models/repo"
810
user_model "code.gitea.io/gitea/models/user"
911
"code.gitea.io/gitea/modules/git"
@@ -20,3 +22,54 @@ type CompareInfo struct {
2022
HeadBranch string
2123
DirectComparison bool
2224
}
25+
26+
// maxForkTraverseLevel defines the maximum levels to traverse when searching for the head repository.
27+
const maxForkTraverseLevel = 10
28+
29+
// FindHeadRepo tries to find the head repository based on the base repository and head user ID.
30+
func FindHeadRepo(ctx context.Context, baseRepo *repo_model.Repository, headUserID int64) (*repo_model.Repository, error) {
31+
if baseRepo.IsFork {
32+
curRepo := baseRepo
33+
for curRepo.OwnerID != headUserID { // We assume the fork deepth is not too deep.
34+
if err := curRepo.GetBaseRepo(ctx); err != nil {
35+
return nil, err
36+
}
37+
if curRepo.BaseRepo == nil {
38+
return findHeadRepoFromRootBase(ctx, curRepo, headUserID, maxForkTraverseLevel)
39+
}
40+
curRepo = curRepo.BaseRepo
41+
}
42+
return curRepo, nil
43+
}
44+
45+
return findHeadRepoFromRootBase(ctx, baseRepo, headUserID, maxForkTraverseLevel)
46+
}
47+
48+
func findHeadRepoFromRootBase(ctx context.Context, baseRepo *repo_model.Repository, headUserID int64, traverseLevel int) (*repo_model.Repository, error) {
49+
if traverseLevel == 0 {
50+
return nil, nil
51+
}
52+
// test if we are lucky
53+
repo, err := repo_model.GetUserFork(ctx, baseRepo.ID, headUserID)
54+
if err != nil {
55+
return nil, err
56+
}
57+
if repo != nil {
58+
return repo, nil
59+
}
60+
61+
firstLevelForkedRepos, err := repo_model.GetRepositoriesByForkID(ctx, baseRepo.ID)
62+
if err != nil {
63+
return nil, err
64+
}
65+
for _, repo := range firstLevelForkedRepos {
66+
forked, err := findHeadRepoFromRootBase(ctx, repo, headUserID, traverseLevel-1)
67+
if err != nil {
68+
return nil, err
69+
}
70+
if forked != nil {
71+
return forked, nil
72+
}
73+
}
74+
return nil, nil
75+
}

routers/web/repo/compare.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
258258
} else if len(headInfos) == 2 {
259259
headInfosSplit := strings.Split(headInfos[0], "/")
260260
if len(headInfosSplit) == 1 {
261-
ci.HeadUser, err = user_model.GetUserByName(ctx, headInfos[0])
261+
ci.HeadUser, err = user_model.GetUserOrOrgByName(ctx, headInfos[0])
262262
if err != nil {
263263
if user_model.IsErrUserNotExist(err) {
264264
ctx.NotFound(nil)

routers/web/repo/pull.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1305,6 +1305,17 @@ func CompareAndPullRequestPost(ctx *context.Context) {
13051305
return
13061306
}
13071307

1308+
// Check if a pull request already exists with the same head and base branch.
1309+
pr, err := issues_model.GetUnmergedPullRequest(ctx, ci.HeadRepo.ID, repo.ID, ci.HeadBranch, ci.BaseBranch, issues_model.PullRequestFlowGithub)
1310+
if err != nil && !issues_model.IsErrPullRequestNotExist(err) {
1311+
ctx.ServerError("GetUnmergedPullRequest", err)
1312+
return
1313+
}
1314+
if pr != nil {
1315+
ctx.JSONError(ctx.Tr("repo.pulls.new.already_existed"))
1316+
return
1317+
}
1318+
13081319
content := form.Content
13091320
if filename := ctx.Req.Form.Get("template-file"); filename != "" {
13101321
if template, err := issue_template.UnmarshalFromRepo(ctx.Repo.GitRepo, ctx.Repo.Repository.DefaultBranch, filename); err == nil {

tests/integration/pull_create_test.go

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package integration
55

66
import (
7+
"encoding/base64"
78
"fmt"
89
"net/http"
910
"net/http/httptest"
@@ -17,7 +18,9 @@ import (
1718
repo_model "code.gitea.io/gitea/models/repo"
1819
"code.gitea.io/gitea/models/unittest"
1920
"code.gitea.io/gitea/modules/git/gitcmd"
21+
api "code.gitea.io/gitea/modules/structs"
2022
"code.gitea.io/gitea/modules/test"
23+
"code.gitea.io/gitea/modules/util"
2124
"code.gitea.io/gitea/tests"
2225

2326
"github.com/stretchr/testify/assert"
@@ -153,8 +156,16 @@ func TestPullCreate(t *testing.T) {
153156
url := test.RedirectURL(resp)
154157
assert.Regexp(t, "^/user2/repo1/pulls/[0-9]*$", url)
155158

159+
// test create the pull request again and it should fail now
160+
link := "/user2/repo1/compare/master...user1/repo1:master"
161+
req := NewRequestWithValues(t, "POST", link, map[string]string{
162+
"_csrf": GetUserCSRFToken(t, session),
163+
"title": "This is a pull title",
164+
})
165+
session.MakeRequest(t, req, http.StatusBadRequest)
166+
156167
// check .diff can be accessed and matches performed change
157-
req := NewRequest(t, "GET", url+".diff")
168+
req = NewRequest(t, "GET", url+".diff")
158169
resp = session.MakeRequest(t, req, http.StatusOK)
159170
assert.Regexp(t, `\+Hello, World \(Edited\)`, resp.Body)
160171
assert.Regexp(t, "^diff", resp.Body)
@@ -295,6 +306,95 @@ func TestPullCreatePrFromBaseToFork(t *testing.T) {
295306
})
296307
}
297308

309+
func TestCreatePullRequestFromNestedOrgForks(t *testing.T) {
310+
onGiteaRun(t, func(t *testing.T, _ *url.URL) {
311+
session := loginUser(t, "user1")
312+
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteOrganization)
313+
314+
const (
315+
baseOrg = "test-fork-org1"
316+
midForkOrg = "test-fork-org2"
317+
leafForkOrg = "test-fork-org3"
318+
repoName = "test-fork-repo"
319+
patchBranch = "teabot-patch-1"
320+
)
321+
322+
createOrg := func(name string) {
323+
req := NewRequestWithJSON(t, "POST", "/api/v1/orgs", &api.CreateOrgOption{
324+
UserName: name,
325+
Visibility: "public",
326+
}).AddTokenAuth(token)
327+
MakeRequest(t, req, http.StatusCreated)
328+
}
329+
330+
createOrg(baseOrg)
331+
createOrg(midForkOrg)
332+
createOrg(leafForkOrg)
333+
334+
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/orgs/%s/repos", baseOrg), &api.CreateRepoOption{
335+
Name: repoName,
336+
AutoInit: true,
337+
DefaultBranch: "main",
338+
Private: false,
339+
Readme: "Default",
340+
}).AddTokenAuth(token)
341+
resp := MakeRequest(t, req, http.StatusCreated)
342+
var baseRepo api.Repository
343+
DecodeJSON(t, resp, &baseRepo)
344+
assert.Equal(t, "main", baseRepo.DefaultBranch)
345+
346+
forkIntoOrg := func(srcOrg, dstOrg string) api.Repository {
347+
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/forks", srcOrg, repoName), &api.CreateForkOption{
348+
Organization: util.ToPointer(dstOrg),
349+
}).AddTokenAuth(token)
350+
resp := MakeRequest(t, req, http.StatusAccepted)
351+
var forkRepo api.Repository
352+
DecodeJSON(t, resp, &forkRepo)
353+
assert.NotNil(t, forkRepo.Owner)
354+
if forkRepo.Owner != nil {
355+
assert.Equal(t, dstOrg, forkRepo.Owner.UserName)
356+
}
357+
return forkRepo
358+
}
359+
360+
forkIntoOrg(baseOrg, midForkOrg)
361+
forkIntoOrg(midForkOrg, leafForkOrg)
362+
363+
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", leafForkOrg, repoName, "patch-from-org3.txt"), &api.CreateFileOptions{
364+
FileOptions: api.FileOptions{
365+
BranchName: "main",
366+
NewBranchName: patchBranch,
367+
Message: "create patch from org3",
368+
},
369+
ContentBase64: base64.StdEncoding.EncodeToString([]byte("patch content")),
370+
}).AddTokenAuth(token)
371+
MakeRequest(t, req, http.StatusCreated)
372+
373+
prPayload := map[string]string{
374+
"head": fmt.Sprintf("%s:%s", leafForkOrg, patchBranch),
375+
"base": "main",
376+
"title": "test creating pull from test-fork-org3 to test-fork-org1",
377+
}
378+
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/pulls", baseOrg, repoName), prPayload).AddTokenAuth(token)
379+
resp = MakeRequest(t, req, http.StatusCreated)
380+
var pr api.PullRequest
381+
DecodeJSON(t, resp, &pr)
382+
assert.Equal(t, prPayload["title"], pr.Title)
383+
if assert.NotNil(t, pr.Head) {
384+
assert.Equal(t, patchBranch, pr.Head.Ref)
385+
if assert.NotNil(t, pr.Head.Repository) {
386+
assert.Equal(t, fmt.Sprintf("%s/%s", leafForkOrg, repoName), pr.Head.Repository.FullName)
387+
}
388+
}
389+
if assert.NotNil(t, pr.Base) {
390+
assert.Equal(t, "main", pr.Base.Ref)
391+
if assert.NotNil(t, pr.Base.Repository) {
392+
assert.Equal(t, fmt.Sprintf("%s/%s", baseOrg, repoName), pr.Base.Repository.FullName)
393+
}
394+
}
395+
})
396+
}
397+
298398
func TestPullCreateParallel(t *testing.T) {
299399
onGiteaRun(t, func(t *testing.T, u *url.URL) {
300400
sessionFork := loginUser(t, "user1")

0 commit comments

Comments
 (0)