Skip to content

Commit ea45dfc

Browse files
kvasterdenyskondelvh
authored andcommitted
Retarget depending pulls when the parent branch is deleted (go-gitea#28686)
Sometimes you need to work on a feature which depends on another (unmerged) feature. In this case, you may create a PR based on that feature instead of the main branch. Currently, such PRs will be closed without the possibility to reopen in case the parent feature is merged and its branch is deleted. Automatic target branch change make life a lot easier in such cases. Github and Bitbucket behave in such way. Example: $PR_1$: main <- feature1 $PR_2$: feature1 <- feature2 Currently, merging $PR_1$ and deleting its branch leads to $PR_2$ being closed without the possibility to reopen. This is both annoying and loses the review history when you open a new PR. With this change, $PR_2$ will change its target branch to main ($PR_2$: main <- feature2) after $PR_1$ has been merged and its branch has been deleted. This behavior is enabled by default but can be disabled. For security reasons, this target branch change will not be executed when merging PRs targeting another repo. Fixes go-gitea#27062 Fixes go-gitea#18408 --------- Co-authored-by: Denys Konovalov <[email protected]> Co-authored-by: delvh <[email protected]>
1 parent e7449e7 commit ea45dfc

File tree

9 files changed

+158
-24
lines changed

9 files changed

+158
-24
lines changed

custom/conf/app.example.ini

+3
Original file line numberDiff line numberDiff line change
@@ -1067,6 +1067,9 @@ LEVEL = Info
10671067
;;
10681068
;; In addition to testing patches using the three-way merge method, re-test conflicting patches with git apply
10691069
;TEST_CONFLICTING_PATCHES_WITH_GIT_APPLY = false
1070+
;;
1071+
;; Retarget child pull requests to the parent pull request branch target on merge of parent pull request. It only works on merged PRs where the head and base branch target the same repo.
1072+
;RETARGET_CHILDREN_ON_MERGE = true
10701073

10711074
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
10721075
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

docs/content/administration/config-cheat-sheet.en-us.md

+1
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ In addition, there is _`StaticRootPath`_ which can be set as a built-in at build
135135
- `POPULATE_SQUASH_COMMENT_WITH_COMMIT_MESSAGES`: **false**: In default squash-merge messages include the commit message of all commits comprising the pull request.
136136
- `ADD_CO_COMMITTER_TRAILERS`: **true**: Add co-authored-by and co-committed-by trailers to merge commit messages if committer does not match author.
137137
- `TEST_CONFLICTING_PATCHES_WITH_GIT_APPLY`: **false**: PR patches are tested using a three-way merge method to discover if there are conflicts. If this setting is set to **true**, conflicting patches will be retested using `git apply` - This was the previous behaviour in 1.18 (and earlier) but is somewhat inefficient. Please report if you find that this setting is required.
138+
- `RETARGET_CHILDREN_ON_MERGE`: **true**: Retarget child pull requests to the parent pull request branch target on merge of parent pull request. It only works on merged PRs where the head and base branch target the same repo.
138139

139140
### Repository - Issue (`repository.issue`)
140141

modules/setting/repository.go

+3
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ var (
8585
PopulateSquashCommentWithCommitMessages bool
8686
AddCoCommitterTrailers bool
8787
TestConflictingPatchesWithGitApply bool
88+
RetargetChildrenOnMerge bool
8889
} `ini:"repository.pull-request"`
8990

9091
// Issue Setting
@@ -209,6 +210,7 @@ var (
209210
PopulateSquashCommentWithCommitMessages bool
210211
AddCoCommitterTrailers bool
211212
TestConflictingPatchesWithGitApply bool
213+
RetargetChildrenOnMerge bool
212214
}{
213215
WorkInProgressPrefixes: []string{"WIP:", "[WIP]"},
214216
// Same as GitHub. See
@@ -223,6 +225,7 @@ var (
223225
DefaultMergeMessageOfficialApproversOnly: true,
224226
PopulateSquashCommentWithCommitMessages: false,
225227
AddCoCommitterTrailers: true,
228+
RetargetChildrenOnMerge: true,
226229
},
227230

228231
// Issue settings

routers/api/v1/repo/pull.go

+4
Original file line numberDiff line numberDiff line change
@@ -913,6 +913,10 @@ func MergePullRequest(ctx *context.APIContext) {
913913
}
914914
defer headRepo.Close()
915915
}
916+
if err := pull_service.RetargetChildrenOnMerge(ctx, ctx.Doer, pr); err != nil {
917+
ctx.Error(http.StatusInternalServerError, "RetargetChildrenOnMerge", err)
918+
return
919+
}
916920
if err := repo_service.DeleteBranch(ctx, ctx.Doer, pr.HeadRepo, headRepo, pr.HeadBranch); err != nil {
917921
switch {
918922
case git.IsErrBranchNotExist(err):

routers/web/repo/pull.go

+6
Original file line numberDiff line numberDiff line change
@@ -1587,6 +1587,12 @@ func CleanUpPullRequest(ctx *context.Context) {
15871587

15881588
func deleteBranch(ctx *context.Context, pr *issues_model.PullRequest, gitRepo *git.Repository) {
15891589
fullBranchName := pr.HeadRepo.FullName() + ":" + pr.HeadBranch
1590+
1591+
if err := pull_service.RetargetChildrenOnMerge(ctx, ctx.Doer, pr); err != nil {
1592+
ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
1593+
return
1594+
}
1595+
15901596
if err := repo_service.DeleteBranch(ctx, ctx.Doer, pr.HeadRepo, gitRepo, pr.HeadBranch); err != nil {
15911597
switch {
15921598
case git.IsErrBranchNotExist(err):

services/pull/pull.go

+37
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,43 @@ func (errs errlist) Error() string {
546546
return ""
547547
}
548548

549+
// RetargetChildrenOnMerge retarget children pull requests on merge if possible
550+
func RetargetChildrenOnMerge(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) error {
551+
if setting.Repository.PullRequest.RetargetChildrenOnMerge && pr.BaseRepoID == pr.HeadRepoID {
552+
return RetargetBranchPulls(ctx, doer, pr.HeadRepoID, pr.HeadBranch, pr.BaseBranch)
553+
}
554+
return nil
555+
}
556+
557+
// RetargetBranchPulls change target branch for all pull requests whose base branch is the branch
558+
// Both branch and targetBranch must be in the same repo (for security reasons)
559+
func RetargetBranchPulls(ctx context.Context, doer *user_model.User, repoID int64, branch, targetBranch string) error {
560+
prs, err := issues_model.GetUnmergedPullRequestsByBaseInfo(ctx, repoID, branch)
561+
if err != nil {
562+
return err
563+
}
564+
565+
if err := issues_model.PullRequestList(prs).LoadAttributes(ctx); err != nil {
566+
return err
567+
}
568+
569+
var errs errlist
570+
for _, pr := range prs {
571+
if err = pr.Issue.LoadRepo(ctx); err != nil {
572+
errs = append(errs, err)
573+
} else if err = ChangeTargetBranch(ctx, pr, doer, targetBranch); err != nil &&
574+
!issues_model.IsErrIssueIsClosed(err) && !models.IsErrPullRequestHasMerged(err) &&
575+
!issues_model.IsErrPullRequestAlreadyExists(err) {
576+
errs = append(errs, err)
577+
}
578+
}
579+
580+
if len(errs) > 0 {
581+
return errs
582+
}
583+
return nil
584+
}
585+
549586
// CloseBranchPulls close all the pull requests who's head branch is the branch
550587
func CloseBranchPulls(ctx context.Context, doer *user_model.User, repoID int64, branch string) error {
551588
prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(ctx, repoID, branch)

tests/integration/pull_create_test.go

+19-6
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,29 @@ import (
1717
"github.com/stretchr/testify/assert"
1818
)
1919

20-
func testPullCreate(t *testing.T, session *TestSession, user, repo, branch, title string) *httptest.ResponseRecorder {
20+
func testPullCreate(t *testing.T, session *TestSession, user, repo string, toSelf bool, targetBranch, sourceBranch, title string) *httptest.ResponseRecorder {
2121
req := NewRequest(t, "GET", path.Join(user, repo))
2222
resp := session.MakeRequest(t, req, http.StatusOK)
2323

2424
// Click the PR button to create a pull
2525
htmlDoc := NewHTMLParser(t, resp.Body)
2626
link, exists := htmlDoc.doc.Find("#new-pull-request").Attr("href")
2727
assert.True(t, exists, "The template has changed")
28-
if branch != "master" {
29-
link = strings.Replace(link, ":master", ":"+branch, 1)
28+
29+
targetUser := strings.Split(link, "/")[1]
30+
if toSelf && targetUser != user {
31+
link = strings.Replace(link, targetUser, user, 1)
32+
}
33+
34+
if targetBranch != "master" {
35+
link = strings.Replace(link, "master...", targetBranch+"...", 1)
36+
}
37+
if sourceBranch != "master" {
38+
if targetUser == user {
39+
link = strings.Replace(link, "...master", "..."+sourceBranch, 1)
40+
} else {
41+
link = strings.Replace(link, ":master", ":"+sourceBranch, 1)
42+
}
3043
}
3144

3245
req = NewRequest(t, "GET", link)
@@ -49,7 +62,7 @@ func TestPullCreate(t *testing.T) {
4962
session := loginUser(t, "user1")
5063
testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
5164
testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
52-
resp := testPullCreate(t, session, "user1", "repo1", "master", "This is a pull title")
65+
resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "This is a pull title")
5366

5467
// check the redirected URL
5568
url := test.RedirectURL(resp)
@@ -77,7 +90,7 @@ func TestPullCreate_TitleEscape(t *testing.T) {
7790
session := loginUser(t, "user1")
7891
testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
7992
testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
80-
resp := testPullCreate(t, session, "user1", "repo1", "master", "<i>XSS PR</i>")
93+
resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "<i>XSS PR</i>")
8194

8295
// check the redirected URL
8396
url := test.RedirectURL(resp)
@@ -142,7 +155,7 @@ func TestPullBranchDelete(t *testing.T) {
142155
testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
143156
testCreateBranch(t, session, "user1", "repo1", "branch/master", "master1", http.StatusSeeOther)
144157
testEditFile(t, session, "user1", "repo1", "master1", "README.md", "Hello, World (Edited)\n")
145-
resp := testPullCreate(t, session, "user1", "repo1", "master1", "This is a pull title")
158+
resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master1", "This is a pull title")
146159

147160
// check the redirected URL
148161
url := test.RedirectURL(resp)

tests/integration/pull_merge_test.go

+81-14
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,23 @@ import (
3535
"github.com/stretchr/testify/assert"
3636
)
3737

38-
func testPullMerge(t *testing.T, session *TestSession, user, repo, pullnum string, mergeStyle repo_model.MergeStyle) *httptest.ResponseRecorder {
38+
func testPullMerge(t *testing.T, session *TestSession, user, repo, pullnum string, mergeStyle repo_model.MergeStyle, deleteBranch bool) *httptest.ResponseRecorder {
3939
req := NewRequest(t, "GET", path.Join(user, repo, "pulls", pullnum))
4040
resp := session.MakeRequest(t, req, http.StatusOK)
4141

4242
htmlDoc := NewHTMLParser(t, resp.Body)
4343
link := path.Join(user, repo, "pulls", pullnum, "merge")
44-
req = NewRequestWithValues(t, "POST", link, map[string]string{
44+
45+
options := map[string]string{
4546
"_csrf": htmlDoc.GetCSRF(),
4647
"do": string(mergeStyle),
47-
})
48+
}
49+
50+
if deleteBranch {
51+
options["delete_branch_after_merge"] = "on"
52+
}
53+
54+
req = NewRequestWithValues(t, "POST", link, options)
4855
resp = session.MakeRequest(t, req, http.StatusOK)
4956

5057
respJSON := struct {
@@ -83,11 +90,11 @@ func TestPullMerge(t *testing.T) {
8390
testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
8491
testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
8592

86-
resp := testPullCreate(t, session, "user1", "repo1", "master", "This is a pull title")
93+
resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "This is a pull title")
8794

8895
elem := strings.Split(test.RedirectURL(resp), "/")
8996
assert.EqualValues(t, "pulls", elem[3])
90-
testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleMerge)
97+
testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleMerge, false)
9198

9299
hookTasks, err = webhook.HookTasks(db.DefaultContext, 1, 1)
93100
assert.NoError(t, err)
@@ -105,11 +112,11 @@ func TestPullRebase(t *testing.T) {
105112
testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
106113
testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
107114

108-
resp := testPullCreate(t, session, "user1", "repo1", "master", "This is a pull title")
115+
resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "This is a pull title")
109116

110117
elem := strings.Split(test.RedirectURL(resp), "/")
111118
assert.EqualValues(t, "pulls", elem[3])
112-
testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleRebase)
119+
testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleRebase, false)
113120

114121
hookTasks, err = webhook.HookTasks(db.DefaultContext, 1, 1)
115122
assert.NoError(t, err)
@@ -127,11 +134,11 @@ func TestPullRebaseMerge(t *testing.T) {
127134
testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
128135
testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
129136

130-
resp := testPullCreate(t, session, "user1", "repo1", "master", "This is a pull title")
137+
resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "This is a pull title")
131138

132139
elem := strings.Split(test.RedirectURL(resp), "/")
133140
assert.EqualValues(t, "pulls", elem[3])
134-
testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleRebaseMerge)
141+
testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleRebaseMerge, false)
135142

136143
hookTasks, err = webhook.HookTasks(db.DefaultContext, 1, 1)
137144
assert.NoError(t, err)
@@ -150,11 +157,11 @@ func TestPullSquash(t *testing.T) {
150157
testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
151158
testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited!)\n")
152159

153-
resp := testPullCreate(t, session, "user1", "repo1", "master", "This is a pull title")
160+
resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "This is a pull title")
154161

155162
elem := strings.Split(test.RedirectURL(resp), "/")
156163
assert.EqualValues(t, "pulls", elem[3])
157-
testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleSquash)
164+
testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleSquash, false)
158165

159166
hookTasks, err = webhook.HookTasks(db.DefaultContext, 1, 1)
160167
assert.NoError(t, err)
@@ -168,11 +175,11 @@ func TestPullCleanUpAfterMerge(t *testing.T) {
168175
testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
169176
testEditFileToNewBranch(t, session, "user1", "repo1", "master", "feature/test", "README.md", "Hello, World (Edited - TestPullCleanUpAfterMerge)\n")
170177

171-
resp := testPullCreate(t, session, "user1", "repo1", "feature/test", "This is a pull title")
178+
resp := testPullCreate(t, session, "user1", "repo1", false, "master", "feature/test", "This is a pull title")
172179

173180
elem := strings.Split(test.RedirectURL(resp), "/")
174181
assert.EqualValues(t, "pulls", elem[3])
175-
testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleMerge)
182+
testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleMerge, false)
176183

177184
// Check PR branch deletion
178185
resp = testPullCleanUp(t, session, elem[1], elem[2], elem[4])
@@ -203,7 +210,7 @@ func TestCantMergeWorkInProgress(t *testing.T) {
203210
testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
204211
testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
205212

206-
resp := testPullCreate(t, session, "user1", "repo1", "master", "[wip] This is a pull title")
213+
resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "[wip] This is a pull title")
207214

208215
req := NewRequest(t, "GET", test.RedirectURL(resp))
209216
resp = session.MakeRequest(t, req, http.StatusOK)
@@ -435,3 +442,63 @@ func TestConflictChecking(t *testing.T) {
435442
assert.False(t, conflictingPR.Mergeable(db.DefaultContext))
436443
})
437444
}
445+
446+
func TestPullRetargetChildOnBranchDelete(t *testing.T) {
447+
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
448+
session := loginUser(t, "user1")
449+
testEditFileToNewBranch(t, session, "user2", "repo1", "master", "base-pr", "README.md", "Hello, World\n(Edited - TestPullRetargetOnCleanup - base PR)\n")
450+
testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
451+
testEditFileToNewBranch(t, session, "user1", "repo1", "base-pr", "child-pr", "README.md", "Hello, World\n(Edited - TestPullRetargetOnCleanup - base PR)\n(Edited - TestPullRetargetOnCleanup - child PR)")
452+
453+
respBasePR := testPullCreate(t, session, "user2", "repo1", true, "master", "base-pr", "Base Pull Request")
454+
elemBasePR := strings.Split(test.RedirectURL(respBasePR), "/")
455+
assert.EqualValues(t, "pulls", elemBasePR[3])
456+
457+
respChildPR := testPullCreate(t, session, "user1", "repo1", false, "base-pr", "child-pr", "Child Pull Request")
458+
elemChildPR := strings.Split(test.RedirectURL(respChildPR), "/")
459+
assert.EqualValues(t, "pulls", elemChildPR[3])
460+
461+
testPullMerge(t, session, elemBasePR[1], elemBasePR[2], elemBasePR[4], repo_model.MergeStyleMerge, true)
462+
463+
// Check child PR
464+
req := NewRequest(t, "GET", test.RedirectURL(respChildPR))
465+
resp := session.MakeRequest(t, req, http.StatusOK)
466+
467+
htmlDoc := NewHTMLParser(t, resp.Body)
468+
targetBranch := htmlDoc.doc.Find("#branch_target>a").Text()
469+
prStatus := strings.TrimSpace(htmlDoc.doc.Find(".issue-title-meta>.issue-state-label").Text())
470+
471+
assert.EqualValues(t, "master", targetBranch)
472+
assert.EqualValues(t, "Open", prStatus)
473+
})
474+
}
475+
476+
func TestPullDontRetargetChildOnWrongRepo(t *testing.T) {
477+
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
478+
session := loginUser(t, "user1")
479+
testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
480+
testEditFileToNewBranch(t, session, "user1", "repo1", "master", "base-pr", "README.md", "Hello, World\n(Edited - TestPullDontRetargetChildOnWrongRepo - base PR)\n")
481+
testEditFileToNewBranch(t, session, "user1", "repo1", "base-pr", "child-pr", "README.md", "Hello, World\n(Edited - TestPullDontRetargetChildOnWrongRepo - base PR)\n(Edited - TestPullDontRetargetChildOnWrongRepo - child PR)")
482+
483+
respBasePR := testPullCreate(t, session, "user1", "repo1", false, "master", "base-pr", "Base Pull Request")
484+
elemBasePR := strings.Split(test.RedirectURL(respBasePR), "/")
485+
assert.EqualValues(t, "pulls", elemBasePR[3])
486+
487+
respChildPR := testPullCreate(t, session, "user1", "repo1", true, "base-pr", "child-pr", "Child Pull Request")
488+
elemChildPR := strings.Split(test.RedirectURL(respChildPR), "/")
489+
assert.EqualValues(t, "pulls", elemChildPR[3])
490+
491+
testPullMerge(t, session, elemBasePR[1], elemBasePR[2], elemBasePR[4], repo_model.MergeStyleMerge, true)
492+
493+
// Check child PR
494+
req := NewRequest(t, "GET", test.RedirectURL(respChildPR))
495+
resp := session.MakeRequest(t, req, http.StatusOK)
496+
497+
htmlDoc := NewHTMLParser(t, resp.Body)
498+
targetBranch := htmlDoc.doc.Find("#branch_target>a").Text()
499+
prStatus := strings.TrimSpace(htmlDoc.doc.Find(".issue-title-meta>.issue-state-label").Text())
500+
501+
assert.EqualValues(t, "base-pr", targetBranch)
502+
assert.EqualValues(t, "Closed", prStatus)
503+
})
504+
}

tests/integration/repo_activity_test.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,16 @@ func TestRepoActivity(t *testing.T) {
2222
// Create PRs (1 merged & 2 proposed)
2323
testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
2424
testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
25-
resp := testPullCreate(t, session, "user1", "repo1", "master", "This is a pull title")
25+
resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "This is a pull title")
2626
elem := strings.Split(test.RedirectURL(resp), "/")
2727
assert.EqualValues(t, "pulls", elem[3])
28-
testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleMerge)
28+
testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleMerge, false)
2929

3030
testEditFileToNewBranch(t, session, "user1", "repo1", "master", "feat/better_readme", "README.md", "Hello, World (Edited Again)\n")
31-
testPullCreate(t, session, "user1", "repo1", "feat/better_readme", "This is a pull title")
31+
testPullCreate(t, session, "user1", "repo1", false, "master", "feat/better_readme", "This is a pull title")
3232

3333
testEditFileToNewBranch(t, session, "user1", "repo1", "master", "feat/much_better_readme", "README.md", "Hello, World (Edited More)\n")
34-
testPullCreate(t, session, "user1", "repo1", "feat/much_better_readme", "This is a pull title")
34+
testPullCreate(t, session, "user1", "repo1", false, "master", "feat/much_better_readme", "This is a pull title")
3535

3636
// Create issues (3 new issues)
3737
testNewIssue(t, session, "user2", "repo1", "Issue 1", "Description 1")

0 commit comments

Comments
 (0)