Repository: lindell/multi-gitter (go)

Issue:
Add run option to tell multi-gitter to create PR in draft mode
It would be nice if `multi-gitter` run had a `--draft` option to create GH PRs in draft mode. This is useful in case the run script doesn't support some edge case in one of the repos, and we can spot check PRs before marking them ready-for-review.

Pull request:
feat: add ability to create PR as draft
# What does this change
Adds ability to create Pull request as draft for GitHub, GitLab and Gitea.

Config:

![image](https://user-images.githubusercontent.com/17970041/152756206-38cab9f7-af59-4241-a2b4-157d561cc6d8.png)

Result:

![image](https://user-images.githubusercontent.com/17970041/152756261-0d44f539-8736-474c-ac2d-424bca133a9a.png)


# What issue does it fix
Closes #225 

# Notes for the reviewer
Had a question in #225 about modifying pull requests with draft status, but I think those can be addressed in a separate issue and MR.

Are you able to test this on GitHub? I'm not able to now, since my Multi-Gitter is setup for GitLab.

Also is there a way to write tests for this?

# Checklist
- [x] Made sure the PR follows the [CONTRIBUTING.md](CONTRIBUTING.md) guidelines
- [x] Tests if something new is added


Patch:
diff --git a/cmd/cmd-run.go b/cmd/cmd-run.go
index 9bb845e..275917d 100755
--- a/cmd/cmd-run.go
+++ b/cmd/cmd-run.go
@@ -50,6 +50,7 @@ Available values:
   skip: Skip making any changes to the existing branch and do not create a new pull request.
   replace: Replace the existing content of the branch by force pushing any new changes, then reuse any existing pull request, or create a new one if none exist.
 `)
+	cmd.Flags().BoolP("draft", "", false, "Create pull request(s) as draft.")
 	_ = cmd.RegisterFlagCompletionFunc("conflict-strategy", func(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
 		return []string{"skip", "replace"}, cobra.ShellCompDirectiveNoFileComp
 	})
@@ -87,6 +88,7 @@ func run(cmd *cobra.Command, args []string) error {
 	authorEmail, _ := flag.GetString("author-email")
 	strOutput, _ := flag.GetString("output")
 	assignees, _ := flag.GetStringSlice("assignees")
+	draft, _ := flag.GetBool("draft")
 
 	if concurrent < 1 {
 		return errors.New("concurrent runs can't be less than one")
@@ -189,6 +191,7 @@ func run(cmd *cobra.Command, args []string) error {
 		BaseBranch:       baseBranchName,
 		Assignees:        assignees,
 		ConflictStrategy: conflictStrategy,
+		Draft:            draft,
 
 		Concurrent: concurrent,
 
diff --git a/internal/multigitter/run.go b/internal/multigitter/run.go
index 090bbab..0c658f4 100755
--- a/internal/multigitter/run.go
+++ b/internal/multigitter/run.go
@@ -62,6 +62,8 @@ type Runner struct {
 
 	ConflictStrategy ConflictStrategy // Defines what will happen if a branch does already exist
 
+	Draft bool // If set, creates Pull Requests as draft
+
 	Interactive bool // If set, interactive mode is activated and the user will be asked to verify every change
 
 	CreateGit func(dir string) Git
@@ -328,6 +330,7 @@ func (r *Runner) runSingleRepo(ctx context.Context, repo scm.Repository) (scm.Pu
 			Base:      baseBranch,
 			Reviewers: getReviewers(r.Reviewers, r.MaxReviewers),
 			Assignees: r.Assignees,
+			Draft:     r.Draft,
 		})
 		if err != nil {
 			return nil, err
diff --git a/internal/scm/gitea/gitea.go b/internal/scm/gitea/gitea.go
index 5ee7aec..488287f 100644
--- a/internal/scm/gitea/gitea.go
+++ b/internal/scm/gitea/gitea.go
@@ -211,10 +211,15 @@ func (g *Gitea) CreatePullRequest(ctx context.Context, repo scm.Repository, prRe
 		head = fmt.Sprintf("%s:%s", prR.ownerName, newPR.Head)
 	}
 
+	prTitle := newPR.Title
+	if newPR.Draft {
+		prTitle = "WIP: " + prTitle // See https://docs.gitea.io/en-us/pull-request/
+	}
+
 	pr, _, err := g.giteaClient(ctx).CreatePullRequest(r.ownerName, r.name, gitea.CreatePullRequestOption{
 		Head:      head,
 		Base:      newPR.Base,
-		Title:     newPR.Title,
+		Title:     prTitle,
 		Body:      newPR.Body,
 		Assignees: newPR.Assignees,
 	})
diff --git a/internal/scm/github/github.go b/internal/scm/github/github.go
index 959e621..514ad50 100755
--- a/internal/scm/github/github.go
+++ b/internal/scm/github/github.go
@@ -284,6 +284,7 @@ func (g *Github) createPullRequest(ctx context.Context, repo repository, prRepo
 		Body:  &newPR.Body,
 		Head:  &head,
 		Base:  &newPR.Base,
+		Draft: &newPR.Draft,
 	})
 	if err != nil {
 		return nil, err
diff --git a/internal/scm/gitlab/gitlab.go b/internal/scm/gitlab/gitlab.go
index c6f63cf..d1dbbf7 100644
--- a/internal/scm/gitlab/gitlab.go
+++ b/internal/scm/gitlab/gitlab.go
@@ -217,9 +217,14 @@ func (g *Gitlab) CreatePullRequest(ctx context.Context, repo scm.Repository, prR
 		return nil, err
 	}
 
+	prTitle := newPR.Title
+	if newPR.Draft {
+		prTitle = "Draft: " + prTitle // See https://docs.gitlab.com/ee/user/project/merge_requests/drafts.html#mark-merge-requests-as-drafts
+	}
+
 	removeSourceBranch := true
 	mr, _, err := g.glClient.MergeRequests.CreateMergeRequest(prR.pid, &gitlab.CreateMergeRequestOptions{
-		Title:              &newPR.Title,
+		Title:              &prTitle,
 		Description:        &newPR.Body,
 		SourceBranch:       &newPR.Head,
 		TargetBranch:       &newPR.Base,
diff --git a/internal/scm/pullrequest.go b/internal/scm/pullrequest.go
index e29f602..285bf4b 100755
--- a/internal/scm/pullrequest.go
+++ b/internal/scm/pullrequest.go
@@ -14,6 +14,7 @@ type NewPullRequest struct {
 
 	Reviewers []string // The username of all reviewers
 	Assignees []string
+	Draft     bool
 }
 
 // PullRequestStatus is the status of a pull request, including statuses of the last commit


Test patch:
diff --git a/tests/table_test.go b/tests/table_test.go
index fb3e861..a52815c 100644
--- a/tests/table_test.go
+++ b/tests/table_test.go
@@ -834,7 +834,7 @@ Repositories with a successful run:
 		},
 
 		{
-			name: "multi line message body",
+			name: "no repositories found",
 			vcCreate: func(t *testing.T) *vcmock.VersionController {
 				return &vcmock.VersionController{
 					Repositories: []vcmock.Repository{},
@@ -854,6 +854,30 @@ Repositories with a successful run:
 				assert.Contains(t, runData.logOut, "No repositories found")
 			},
 		},
+
+		{
+			name: "PRs as draft",
+			vcCreate: func(t *testing.T) *vcmock.VersionController {
+				return &vcmock.VersionController{
+					Repositories: []vcmock.Repository{
+						createRepo(t, "owner", "should-change", "i like apples as draft"),
+					},
+				}
+			},
+			args: []string{
+				"run",
+				"--author-name", "Test Author",
+				"--author-email", "test@example.com",
+				"-B", "custom-branch-name",
+				"-m", "custom message",
+				"--draft",
+				changerBinaryPath,
+			},
+			verify: func(t *testing.T, vcMock *vcmock.VersionController, runData runData) {
+				require.Len(t, vcMock.PullRequests, 1)
+				assert.True(t, vcMock.PullRequests[0].Draft)
+			},
+		},
 	}
 
 	for _, gitBackend := range gitBackends {