]> source.dussan.org Git - gitea.git/commitdiff
Retarget depending pulls when the parent branch is deleted (#28686)
authorViktor Kuzmin <kvaster@gmail.com>
Wed, 17 Jan 2024 00:44:56 +0000 (03:44 +0300)
committerGitHub <noreply@github.com>
Wed, 17 Jan 2024 00:44:56 +0000 (01:44 +0100)
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 #27062
Fixes #18408

---------

Co-authored-by: Denys Konovalov <kontakt@denyskon.de>
Co-authored-by: delvh <dev.lh@web.de>
custom/conf/app.example.ini
docs/content/administration/config-cheat-sheet.en-us.md
modules/setting/repository.go
routers/api/v1/repo/pull.go
routers/web/repo/pull.go
services/pull/pull.go
tests/integration/pull_create_test.go
tests/integration/pull_merge_test.go
tests/integration/repo_activity_test.go

index 301e88b14e645f964e62514e68d5768fbb91f02e..b0875123b7b8b038e26410d2f19056a8ae095004 100644 (file)
@@ -1067,6 +1067,9 @@ LEVEL = Info
 ;;
 ;; In addition to testing patches using the three-way merge method, re-test conflicting patches with git apply
 ;TEST_CONFLICTING_PATCHES_WITH_GIT_APPLY = false
+;;
+;; 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.
+;RETARGET_CHILDREN_ON_MERGE = true
 
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
index c9e99ea54fcef423817318821fe1670478021309..eb9b8d1ae9a433fbc756058eb1f71abd5d229312 100644 (file)
@@ -135,6 +135,7 @@ In addition, there is _`StaticRootPath`_ which can be set as a built-in at build
 - `POPULATE_SQUASH_COMMENT_WITH_COMMIT_MESSAGES`: **false**: In default squash-merge messages include the commit message of all commits comprising the pull request.
 - `ADD_CO_COMMITTER_TRAILERS`: **true**: Add co-authored-by and co-committed-by trailers to merge commit messages if committer does not match author.
 - `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.
+- `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.
 
 ### Repository - Issue (`repository.issue`)
 
index 9697a851d3ba98fe862fb7f53df848d626856729..a6f0ed8833589adf12b80e4c6a7f1f65a69d8d19 100644 (file)
@@ -85,6 +85,7 @@ var (
                        PopulateSquashCommentWithCommitMessages  bool
                        AddCoCommitterTrailers                   bool
                        TestConflictingPatchesWithGitApply       bool
+                       RetargetChildrenOnMerge                  bool
                } `ini:"repository.pull-request"`
 
                // Issue Setting
@@ -209,6 +210,7 @@ var (
                        PopulateSquashCommentWithCommitMessages  bool
                        AddCoCommitterTrailers                   bool
                        TestConflictingPatchesWithGitApply       bool
+                       RetargetChildrenOnMerge                  bool
                }{
                        WorkInProgressPrefixes: []string{"WIP:", "[WIP]"},
                        // Same as GitHub. See
@@ -223,6 +225,7 @@ var (
                        DefaultMergeMessageOfficialApproversOnly: true,
                        PopulateSquashCommentWithCommitMessages:  false,
                        AddCoCommitterTrailers:                   true,
+                       RetargetChildrenOnMerge:                  true,
                },
 
                // Issue settings
index 34129ad5958c811af0906d511f550052f9e333c5..b1cb7011f1d4b3c4206473d90744977798dbb64d 100644 (file)
@@ -913,6 +913,10 @@ func MergePullRequest(ctx *context.APIContext) {
                        }
                        defer headRepo.Close()
                }
+               if err := pull_service.RetargetChildrenOnMerge(ctx, ctx.Doer, pr); err != nil {
+                       ctx.Error(http.StatusInternalServerError, "RetargetChildrenOnMerge", err)
+                       return
+               }
                if err := repo_service.DeleteBranch(ctx, ctx.Doer, pr.HeadRepo, headRepo, pr.HeadBranch); err != nil {
                        switch {
                        case git.IsErrBranchNotExist(err):
index 32d82b2a643cbfd942ac2dd764bcc8c96b111895..e36d7092af286ac03e844a3b1174f9c61aa4b7a3 100644 (file)
@@ -1587,6 +1587,12 @@ func CleanUpPullRequest(ctx *context.Context) {
 
 func deleteBranch(ctx *context.Context, pr *issues_model.PullRequest, gitRepo *git.Repository) {
        fullBranchName := pr.HeadRepo.FullName() + ":" + pr.HeadBranch
+
+       if err := pull_service.RetargetChildrenOnMerge(ctx, ctx.Doer, pr); err != nil {
+               ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
+               return
+       }
+
        if err := repo_service.DeleteBranch(ctx, ctx.Doer, pr.HeadRepo, gitRepo, pr.HeadBranch); err != nil {
                switch {
                case git.IsErrBranchNotExist(err):
index d1630f3792c07fc27a1590ecd870cdd28af18c69..930954bdfdfa1b9caf7e91e32673d39e3cc9ec14 100644 (file)
@@ -546,6 +546,43 @@ func (errs errlist) Error() string {
        return ""
 }
 
+// RetargetChildrenOnMerge retarget children pull requests on merge if possible
+func RetargetChildrenOnMerge(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) error {
+       if setting.Repository.PullRequest.RetargetChildrenOnMerge && pr.BaseRepoID == pr.HeadRepoID {
+               return RetargetBranchPulls(ctx, doer, pr.HeadRepoID, pr.HeadBranch, pr.BaseBranch)
+       }
+       return nil
+}
+
+// RetargetBranchPulls change target branch for all pull requests whose base branch is the branch
+// Both branch and targetBranch must be in the same repo (for security reasons)
+func RetargetBranchPulls(ctx context.Context, doer *user_model.User, repoID int64, branch, targetBranch string) error {
+       prs, err := issues_model.GetUnmergedPullRequestsByBaseInfo(ctx, repoID, branch)
+       if err != nil {
+               return err
+       }
+
+       if err := issues_model.PullRequestList(prs).LoadAttributes(ctx); err != nil {
+               return err
+       }
+
+       var errs errlist
+       for _, pr := range prs {
+               if err = pr.Issue.LoadRepo(ctx); err != nil {
+                       errs = append(errs, err)
+               } else if err = ChangeTargetBranch(ctx, pr, doer, targetBranch); err != nil &&
+                       !issues_model.IsErrIssueIsClosed(err) && !models.IsErrPullRequestHasMerged(err) &&
+                       !issues_model.IsErrPullRequestAlreadyExists(err) {
+                       errs = append(errs, err)
+               }
+       }
+
+       if len(errs) > 0 {
+               return errs
+       }
+       return nil
+}
+
 // CloseBranchPulls close all the pull requests who's head branch is the branch
 func CloseBranchPulls(ctx context.Context, doer *user_model.User, repoID int64, branch string) error {
        prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(ctx, repoID, branch)
index a6ee0d9dfa1eff842d24373a2572f8bf34555586..029ea65d7190fe265d62ba41507d1aad8bdb2c3f 100644 (file)
@@ -17,7 +17,7 @@ import (
        "github.com/stretchr/testify/assert"
 )
 
-func testPullCreate(t *testing.T, session *TestSession, user, repo, branch, title string) *httptest.ResponseRecorder {
+func testPullCreate(t *testing.T, session *TestSession, user, repo string, toSelf bool, targetBranch, sourceBranch, title string) *httptest.ResponseRecorder {
        req := NewRequest(t, "GET", path.Join(user, repo))
        resp := session.MakeRequest(t, req, http.StatusOK)
 
@@ -25,8 +25,21 @@ func testPullCreate(t *testing.T, session *TestSession, user, repo, branch, titl
        htmlDoc := NewHTMLParser(t, resp.Body)
        link, exists := htmlDoc.doc.Find("#new-pull-request").Attr("href")
        assert.True(t, exists, "The template has changed")
-       if branch != "master" {
-               link = strings.Replace(link, ":master", ":"+branch, 1)
+
+       targetUser := strings.Split(link, "/")[1]
+       if toSelf && targetUser != user {
+               link = strings.Replace(link, targetUser, user, 1)
+       }
+
+       if targetBranch != "master" {
+               link = strings.Replace(link, "master...", targetBranch+"...", 1)
+       }
+       if sourceBranch != "master" {
+               if targetUser == user {
+                       link = strings.Replace(link, "...master", "..."+sourceBranch, 1)
+               } else {
+                       link = strings.Replace(link, ":master", ":"+sourceBranch, 1)
+               }
        }
 
        req = NewRequest(t, "GET", link)
@@ -49,7 +62,7 @@ func TestPullCreate(t *testing.T) {
                session := loginUser(t, "user1")
                testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
                testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
-               resp := testPullCreate(t, session, "user1", "repo1", "master", "This is a pull title")
+               resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "This is a pull title")
 
                // check the redirected URL
                url := test.RedirectURL(resp)
@@ -77,7 +90,7 @@ func TestPullCreate_TitleEscape(t *testing.T) {
                session := loginUser(t, "user1")
                testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
                testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
-               resp := testPullCreate(t, session, "user1", "repo1", "master", "<i>XSS PR</i>")
+               resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "<i>XSS PR</i>")
 
                // check the redirected URL
                url := test.RedirectURL(resp)
@@ -142,7 +155,7 @@ func TestPullBranchDelete(t *testing.T) {
                testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
                testCreateBranch(t, session, "user1", "repo1", "branch/master", "master1", http.StatusSeeOther)
                testEditFile(t, session, "user1", "repo1", "master1", "README.md", "Hello, World (Edited)\n")
-               resp := testPullCreate(t, session, "user1", "repo1", "master1", "This is a pull title")
+               resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master1", "This is a pull title")
 
                // check the redirected URL
                url := test.RedirectURL(resp)
index 35af47f802fd62aa9ec770106e536828a2292542..2aa6742a56ac0501ee923871ef9179b87818b91b 100644 (file)
@@ -35,16 +35,23 @@ import (
        "github.com/stretchr/testify/assert"
 )
 
-func testPullMerge(t *testing.T, session *TestSession, user, repo, pullnum string, mergeStyle repo_model.MergeStyle) *httptest.ResponseRecorder {
+func testPullMerge(t *testing.T, session *TestSession, user, repo, pullnum string, mergeStyle repo_model.MergeStyle, deleteBranch bool) *httptest.ResponseRecorder {
        req := NewRequest(t, "GET", path.Join(user, repo, "pulls", pullnum))
        resp := session.MakeRequest(t, req, http.StatusOK)
 
        htmlDoc := NewHTMLParser(t, resp.Body)
        link := path.Join(user, repo, "pulls", pullnum, "merge")
-       req = NewRequestWithValues(t, "POST", link, map[string]string{
+
+       options := map[string]string{
                "_csrf": htmlDoc.GetCSRF(),
                "do":    string(mergeStyle),
-       })
+       }
+
+       if deleteBranch {
+               options["delete_branch_after_merge"] = "on"
+       }
+
+       req = NewRequestWithValues(t, "POST", link, options)
        resp = session.MakeRequest(t, req, http.StatusOK)
 
        respJSON := struct {
@@ -83,11 +90,11 @@ func TestPullMerge(t *testing.T) {
                testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
                testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
 
-               resp := testPullCreate(t, session, "user1", "repo1", "master", "This is a pull title")
+               resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "This is a pull title")
 
                elem := strings.Split(test.RedirectURL(resp), "/")
                assert.EqualValues(t, "pulls", elem[3])
-               testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleMerge)
+               testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleMerge, false)
 
                hookTasks, err = webhook.HookTasks(db.DefaultContext, 1, 1)
                assert.NoError(t, err)
@@ -105,11 +112,11 @@ func TestPullRebase(t *testing.T) {
                testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
                testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
 
-               resp := testPullCreate(t, session, "user1", "repo1", "master", "This is a pull title")
+               resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "This is a pull title")
 
                elem := strings.Split(test.RedirectURL(resp), "/")
                assert.EqualValues(t, "pulls", elem[3])
-               testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleRebase)
+               testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleRebase, false)
 
                hookTasks, err = webhook.HookTasks(db.DefaultContext, 1, 1)
                assert.NoError(t, err)
@@ -127,11 +134,11 @@ func TestPullRebaseMerge(t *testing.T) {
                testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
                testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
 
-               resp := testPullCreate(t, session, "user1", "repo1", "master", "This is a pull title")
+               resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "This is a pull title")
 
                elem := strings.Split(test.RedirectURL(resp), "/")
                assert.EqualValues(t, "pulls", elem[3])
-               testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleRebaseMerge)
+               testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleRebaseMerge, false)
 
                hookTasks, err = webhook.HookTasks(db.DefaultContext, 1, 1)
                assert.NoError(t, err)
@@ -150,11 +157,11 @@ func TestPullSquash(t *testing.T) {
                testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
                testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited!)\n")
 
-               resp := testPullCreate(t, session, "user1", "repo1", "master", "This is a pull title")
+               resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "This is a pull title")
 
                elem := strings.Split(test.RedirectURL(resp), "/")
                assert.EqualValues(t, "pulls", elem[3])
-               testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleSquash)
+               testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleSquash, false)
 
                hookTasks, err = webhook.HookTasks(db.DefaultContext, 1, 1)
                assert.NoError(t, err)
@@ -168,11 +175,11 @@ func TestPullCleanUpAfterMerge(t *testing.T) {
                testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
                testEditFileToNewBranch(t, session, "user1", "repo1", "master", "feature/test", "README.md", "Hello, World (Edited - TestPullCleanUpAfterMerge)\n")
 
-               resp := testPullCreate(t, session, "user1", "repo1", "feature/test", "This is a pull title")
+               resp := testPullCreate(t, session, "user1", "repo1", false, "master", "feature/test", "This is a pull title")
 
                elem := strings.Split(test.RedirectURL(resp), "/")
                assert.EqualValues(t, "pulls", elem[3])
-               testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleMerge)
+               testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleMerge, false)
 
                // Check PR branch deletion
                resp = testPullCleanUp(t, session, elem[1], elem[2], elem[4])
@@ -203,7 +210,7 @@ func TestCantMergeWorkInProgress(t *testing.T) {
                testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
                testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
 
-               resp := testPullCreate(t, session, "user1", "repo1", "master", "[wip] This is a pull title")
+               resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "[wip] This is a pull title")
 
                req := NewRequest(t, "GET", test.RedirectURL(resp))
                resp = session.MakeRequest(t, req, http.StatusOK)
@@ -435,3 +442,63 @@ func TestConflictChecking(t *testing.T) {
                assert.False(t, conflictingPR.Mergeable(db.DefaultContext))
        })
 }
+
+func TestPullRetargetChildOnBranchDelete(t *testing.T) {
+       onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
+               session := loginUser(t, "user1")
+               testEditFileToNewBranch(t, session, "user2", "repo1", "master", "base-pr", "README.md", "Hello, World\n(Edited - TestPullRetargetOnCleanup - base PR)\n")
+               testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+               testEditFileToNewBranch(t, session, "user1", "repo1", "base-pr", "child-pr", "README.md", "Hello, World\n(Edited - TestPullRetargetOnCleanup - base PR)\n(Edited - TestPullRetargetOnCleanup - child PR)")
+
+               respBasePR := testPullCreate(t, session, "user2", "repo1", true, "master", "base-pr", "Base Pull Request")
+               elemBasePR := strings.Split(test.RedirectURL(respBasePR), "/")
+               assert.EqualValues(t, "pulls", elemBasePR[3])
+
+               respChildPR := testPullCreate(t, session, "user1", "repo1", false, "base-pr", "child-pr", "Child Pull Request")
+               elemChildPR := strings.Split(test.RedirectURL(respChildPR), "/")
+               assert.EqualValues(t, "pulls", elemChildPR[3])
+
+               testPullMerge(t, session, elemBasePR[1], elemBasePR[2], elemBasePR[4], repo_model.MergeStyleMerge, true)
+
+               // Check child PR
+               req := NewRequest(t, "GET", test.RedirectURL(respChildPR))
+               resp := session.MakeRequest(t, req, http.StatusOK)
+
+               htmlDoc := NewHTMLParser(t, resp.Body)
+               targetBranch := htmlDoc.doc.Find("#branch_target>a").Text()
+               prStatus := strings.TrimSpace(htmlDoc.doc.Find(".issue-title-meta>.issue-state-label").Text())
+
+               assert.EqualValues(t, "master", targetBranch)
+               assert.EqualValues(t, "Open", prStatus)
+       })
+}
+
+func TestPullDontRetargetChildOnWrongRepo(t *testing.T) {
+       onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
+               session := loginUser(t, "user1")
+               testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+               testEditFileToNewBranch(t, session, "user1", "repo1", "master", "base-pr", "README.md", "Hello, World\n(Edited - TestPullDontRetargetChildOnWrongRepo - base PR)\n")
+               testEditFileToNewBranch(t, session, "user1", "repo1", "base-pr", "child-pr", "README.md", "Hello, World\n(Edited - TestPullDontRetargetChildOnWrongRepo - base PR)\n(Edited - TestPullDontRetargetChildOnWrongRepo - child PR)")
+
+               respBasePR := testPullCreate(t, session, "user1", "repo1", false, "master", "base-pr", "Base Pull Request")
+               elemBasePR := strings.Split(test.RedirectURL(respBasePR), "/")
+               assert.EqualValues(t, "pulls", elemBasePR[3])
+
+               respChildPR := testPullCreate(t, session, "user1", "repo1", true, "base-pr", "child-pr", "Child Pull Request")
+               elemChildPR := strings.Split(test.RedirectURL(respChildPR), "/")
+               assert.EqualValues(t, "pulls", elemChildPR[3])
+
+               testPullMerge(t, session, elemBasePR[1], elemBasePR[2], elemBasePR[4], repo_model.MergeStyleMerge, true)
+
+               // Check child PR
+               req := NewRequest(t, "GET", test.RedirectURL(respChildPR))
+               resp := session.MakeRequest(t, req, http.StatusOK)
+
+               htmlDoc := NewHTMLParser(t, resp.Body)
+               targetBranch := htmlDoc.doc.Find("#branch_target>a").Text()
+               prStatus := strings.TrimSpace(htmlDoc.doc.Find(".issue-title-meta>.issue-state-label").Text())
+
+               assert.EqualValues(t, "base-pr", targetBranch)
+               assert.EqualValues(t, "Closed", prStatus)
+       })
+}
index c8d0c46d6455e8f6c5ed51f98e7253c29677ce64..792554db4bc93fae81e0f153b637610197c5f055 100644 (file)
@@ -22,16 +22,16 @@ func TestRepoActivity(t *testing.T) {
                // Create PRs (1 merged & 2 proposed)
                testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
                testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
-               resp := testPullCreate(t, session, "user1", "repo1", "master", "This is a pull title")
+               resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "This is a pull title")
                elem := strings.Split(test.RedirectURL(resp), "/")
                assert.EqualValues(t, "pulls", elem[3])
-               testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleMerge)
+               testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleMerge, false)
 
                testEditFileToNewBranch(t, session, "user1", "repo1", "master", "feat/better_readme", "README.md", "Hello, World (Edited Again)\n")
-               testPullCreate(t, session, "user1", "repo1", "feat/better_readme", "This is a pull title")
+               testPullCreate(t, session, "user1", "repo1", false, "master", "feat/better_readme", "This is a pull title")
 
                testEditFileToNewBranch(t, session, "user1", "repo1", "master", "feat/much_better_readme", "README.md", "Hello, World (Edited More)\n")
-               testPullCreate(t, session, "user1", "repo1", "feat/much_better_readme", "This is a pull title")
+               testPullCreate(t, session, "user1", "repo1", false, "master", "feat/much_better_readme", "This is a pull title")
 
                // Create issues (3 new issues)
                testNewIssue(t, session, "user2", "repo1", "Issue 1", "Description 1")