Skip to content

Commit efd5dd4

Browse files
authored
Fix bug when creating pull request (#36166)
Extract from #36105 Fix #36116 Fix #35912 Fix #20906
1 parent 1e22bd7 commit efd5dd4

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
@@ -1461,3 +1461,15 @@ func GetUserOrOrgIDByName(ctx context.Context, name string) (int64, error) {
14611461
}
14621462
return id, nil
14631463
}
1464+
1465+
// GetUserOrOrgByName returns the user or org by name
1466+
func GetUserOrOrgByName(ctx context.Context, name string) (*User, error) {
1467+
var u User
1468+
has, err := db.GetEngine(ctx).Where("lower_name = ?", strings.ToLower(name)).Get(&u)
1469+
if err != nil {
1470+
return nil, err
1471+
} else if !has {
1472+
return nil, ErrUserNotExist{Name: name}
1473+
}
1474+
return &u, nil
1475+
}

options/locale/locale_en-US.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1863,6 +1863,7 @@ pulls.desc = Enable pull requests and code reviews.
18631863
pulls.new = New Pull Request
18641864
pulls.new.blocked_user = Cannot create pull request because you are blocked by the repository owner.
18651865
pulls.new.must_collaborator = You must be a collaborator to create pull request.
1866+
pulls.new.already_existed = A pull request between these branches already exists
18661867
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.
18671868
pulls.view = View Pull Request
18681869
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
@@ -30,6 +30,7 @@ import (
3030
"code.gitea.io/gitea/modules/timeutil"
3131
"code.gitea.io/gitea/modules/web"
3232
"code.gitea.io/gitea/routers/api/v1/utils"
33+
"code.gitea.io/gitea/routers/common"
3334
asymkey_service "code.gitea.io/gitea/services/asymkey"
3435
"code.gitea.io/gitea/services/automerge"
3536
"code.gitea.io/gitea/services/context"
@@ -1082,7 +1083,7 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
10821083
} else if len(headInfos) == 2 {
10831084
// There is a head repository (the head repository could also be the same base repo)
10841085
headRefToGuess = headInfos[1]
1085-
headUser, err = user_model.GetUserByName(ctx, headInfos[0])
1086+
headUser, err = user_model.GetUserOrOrgByName(ctx, headInfos[0])
10861087
if err != nil {
10871088
if user_model.IsErrUserNotExist(err) {
10881089
ctx.APIErrorNotFound("GetUserByName")
@@ -1098,28 +1099,23 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
10981099

10991100
isSameRepo := ctx.Repo.Owner.ID == headUser.ID
11001101

1101-
// Check if current user has fork of repository or in the same repository.
1102-
headRepo := repo_model.GetForkedRepo(ctx, headUser.ID, baseRepo.ID)
1103-
if headRepo == nil && !isSameRepo {
1104-
err = baseRepo.GetBaseRepo(ctx)
1102+
var headRepo *repo_model.Repository
1103+
if isSameRepo {
1104+
headRepo = baseRepo
1105+
} else {
1106+
headRepo, err = common.FindHeadRepo(ctx, baseRepo, headUser.ID)
11051107
if err != nil {
11061108
ctx.APIErrorInternal(err)
11071109
return nil, nil
11081110
}
1109-
1110-
// Check if baseRepo's base repository is the same as headUser's repository.
1111-
if baseRepo.BaseRepo == nil || baseRepo.BaseRepo.OwnerID != headUser.ID {
1112-
log.Trace("parseCompareInfo[%d]: does not have fork or in same repository", baseRepo.ID)
1113-
ctx.APIErrorNotFound("GetBaseRepo")
1111+
if headRepo == nil {
1112+
ctx.APIErrorNotFound("head repository not found")
11141113
return nil, nil
11151114
}
1116-
// Assign headRepo so it can be used below.
1117-
headRepo = baseRepo.BaseRepo
11181115
}
11191116

11201117
var headGitRepo *git.Repository
11211118
if isSameRepo {
1122-
headRepo = ctx.Repo.Repository
11231119
headGitRepo = ctx.Repo.GitRepo
11241120
closer = func() {} // no need to close the head repo because it shares the base repo
11251121
} else {
@@ -1143,7 +1139,7 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
11431139
return nil, nil
11441140
}
11451141

1146-
if !permBase.CanReadIssuesOrPulls(true) || !permBase.CanRead(unit.TypeCode) {
1142+
if !permBase.CanRead(unit.TypeCode) {
11471143
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)
11481144
ctx.APIErrorNotFound("Can't read pulls or can't read UnitTypeCode")
11491145
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
@@ -259,7 +259,7 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
259259
} else if len(headInfos) == 2 {
260260
headInfosSplit := strings.Split(headInfos[0], "/")
261261
if len(headInfosSplit) == 1 {
262-
ci.HeadUser, err = user_model.GetUserByName(ctx, headInfos[0])
262+
ci.HeadUser, err = user_model.GetUserOrOrgByName(ctx, headInfos[0])
263263
if err != nil {
264264
if user_model.IsErrUserNotExist(err) {
265265
ctx.NotFound(nil)

routers/web/repo/pull.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1340,6 +1340,17 @@ func CompareAndPullRequestPost(ctx *context.Context) {
13401340
return
13411341
}
13421342

1343+
// Check if a pull request already exists with the same head and base branch.
1344+
pr, err := issues_model.GetUnmergedPullRequest(ctx, ci.HeadRepo.ID, repo.ID, ci.HeadBranch, ci.BaseBranch, issues_model.PullRequestFlowGithub)
1345+
if err != nil && !issues_model.IsErrPullRequestNotExist(err) {
1346+
ctx.ServerError("GetUnmergedPullRequest", err)
1347+
return
1348+
}
1349+
if pr != nil {
1350+
ctx.JSONError(ctx.Tr("repo.pulls.new.already_existed"))
1351+
return
1352+
}
1353+
13431354
content := form.Content
13441355
if filename := ctx.Req.Form.Get("template-file"); filename != "" {
13451356
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)