Repository: lindell/multi-gitter (go)

Issue:
Feature request: Add a flag to allow only pushing the Feature Branch without creating a PR, or pushing directly to an existing branch
**Feature Branch only flag**
Add a flag to the `run` command to disable creating a pull request, to only create the feature branch. 

**My use case**
In my case, I have multiple repositories which I want to run scripts against to create feature branches.
However I don't want to create a pull request to the default branch.
Instead I wish to create multiple pull requests from the feature branch to different protected branches in a sequential order.
To do this, I would like to add an option to only create the feature branch, and then create pull requests afterwards

**Implementation**
- [X] I would like to contribute this feature if it's a suitable addition to multi-gitter
- [ ] I have no intention of adding this feature myself.


Pull request:
Feature #454 - Add --push-only flag to allow developers to only push the feature branch
 - Add --push-only flag to only push a feature branch

# What does this change
This feature adds a flag named --push-only to the run command which will allow developers to create feature branches without creating PRs.

# What issue does it fix
Closes #454 

# Notes for the reviewer
_Put any questions or notes for the reviewer here._

# 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 7fde950..6b6c62d 100755
--- a/cmd/cmd-run.go
+++ b/cmd/cmd-run.go
@@ -48,6 +48,7 @@ func RunCmd() *cobra.Command {
 	cmd.Flags().IntP("max-team-reviewers", "", 0, "If this value is set, team reviewers will be randomized")
 	cmd.Flags().IntP("concurrent", "C", 1, "The maximum number of concurrent runs.")
 	cmd.Flags().BoolP("skip-pr", "", false, "Skip pull request and directly push to the branch.")
+	cmd.Flags().BoolP("push-only", "", false, "Skip pull request and only push the feature branch.")
 	cmd.Flags().StringSliceP("skip-repo", "s", nil, "Skip changes on specified repositories, the name is including the owner of repository in the format \"ownerName/repoName\".")
 	cmd.Flags().BoolP("interactive", "i", false, "Take manual decision before committing any change. Requires git to be installed.")
 	cmd.Flags().BoolP("dry-run", "d", false, "Run without pushing changes or creating pull requests.")
@@ -89,6 +90,7 @@ func run(cmd *cobra.Command, _ []string) error {
 	maxTeamReviewers, _ := flag.GetInt("max-team-reviewers")
 	concurrent, _ := flag.GetInt("concurrent")
 	skipPullRequest, _ := flag.GetBool("skip-pr")
+	pushOnly, _ := flag.GetBool("push-only")
 	skipRepository, _ := flag.GetStringSlice("skip-repo")
 	interactive, _ := flag.GetBool("interactive")
 	dryRun, _ := flag.GetBool("dry-run")
@@ -129,6 +131,14 @@ func run(cmd *cobra.Command, _ []string) error {
 		}
 	}
 
+	if pushOnly && forkMode {
+		return errors.New("--push-only and --fork can't be used at the same time")
+	}
+
+	if skipPullRequest && pushOnly {
+		return errors.New("--push-only and --skip-pr can't be used at the same time")
+	}
+
 	if skipPullRequest && forkMode {
 		return errors.New("--fork and --skip-pr can't be used at the same time")
 	}
@@ -228,6 +238,7 @@ func run(cmd *cobra.Command, _ []string) error {
 		Fork:                   forkMode,
 		ForkOwner:              forkOwner,
 		SkipPullRequest:        skipPullRequest,
+		PushOnly:               pushOnly,
 		SkipRepository:         skipRepository,
 		CommitAuthor:           commitAuthor,
 		BaseBranch:             baseBranchName,
diff --git a/internal/multigitter/run.go b/internal/multigitter/run.go
index d76682f..8ce5568 100755
--- a/internal/multigitter/run.go
+++ b/internal/multigitter/run.go
@@ -58,6 +58,7 @@ type Runner struct {
 
 	Concurrent             int
 	SkipPullRequest        bool     // If set, the script will run directly on the base-branch without creating any PR
+	PushOnly               bool     // If set, the script will only publish the feature branch without creating a PR
 	SkipRepository         []string // A list of repositories that run will skip
 	RegExIncludeRepository *regexp.Regexp
 	RegExExcludeRepository *regexp.Regexp
@@ -319,7 +320,7 @@ func (r *Runner) runSingleRepo(ctx context.Context, repo scm.Repository) (scm.Pu
 
 	// Determine if a branch already exists and (depending on the conflict strategy) skip making changes
 	featureBranchExist := false
-	if !r.SkipPullRequest {
+	if !r.SkipPullRequest && !r.PushOnly {
 		featureBranchExist, err = sourceController.BranchExist(remoteName, r.FeatureBranch)
 		if err != nil {
 			return nil, errors.Wrap(err, "could not verify if branch already exists")
@@ -340,6 +341,12 @@ func (r *Runner) runSingleRepo(ctx context.Context, repo scm.Repository) (scm.Pu
 		return nil, errors.Wrap(err, "could not push changes")
 	}
 
+	if r.PushOnly {
+		return dryRunPullRequest{
+			Repository: repo,
+		}, nil
+	}
+
 	return r.ensurePullRequestExists(ctx, log, repo, prRepo, baseBranch, featureBranchExist)
 }
 


Test patch:
diff --git a/tests/table_test.go b/tests/table_test.go
index fe8aabb..7926ee4 100644
--- a/tests/table_test.go
+++ b/tests/table_test.go
@@ -1186,6 +1186,76 @@ Repositories with a successful run:
 				assert.Equal(t, "Both the feature branch and base branch was named master, if you intended to push directly into the base branch, please use the `skip-pr` option:\n  owner/should-not-change\n", runData.out)
 			},
 		},
+
+		{
+			name: "fork conflicts with pushOnly",
+			vcCreate: func(t *testing.T) *vcmock.VersionController {
+				return &vcmock.VersionController{
+					Repositories: []vcmock.Repository{
+						createRepo(t, "owner", "example-repository", "i like apples"),
+					},
+				}
+			},
+			args: []string{
+				"run",
+				"--author-name", "Test Author",
+				"--author-email", "test@example.com",
+				"-B", "custom-branch-name",
+				"-m", "custom message",
+				"--fork", "--push-only",
+				changerBinaryPath,
+			},
+			expectErr: true,
+			verify: func(t *testing.T, vcMock *vcmock.VersionController, runData runData) {
+				assert.Contains(t, runData.cmdOut, "Error: --push-only and --fork can't be used at the same time")
+			},
+		},
+		{
+			name: "skipPr conflicts with pushOnly",
+			vcCreate: func(t *testing.T) *vcmock.VersionController {
+				return &vcmock.VersionController{
+					Repositories: []vcmock.Repository{
+						createRepo(t, "owner", "example-repository", "i like apples"),
+					},
+				}
+			},
+			args: []string{
+				"run",
+				"--author-name", "Test Author",
+				"--author-email", "test@example.com",
+				"-B", "custom-branch-name",
+				"-m", "custom message",
+				"--skip-pr", "--push-only",
+				changerBinaryPath,
+			},
+			expectErr: true,
+			verify: func(t *testing.T, vcMock *vcmock.VersionController, runData runData) {
+				assert.Contains(t, runData.cmdOut, "Error: --push-only and --skip-pr can't be used at the same time")
+			},
+		},
+		{
+			name: "pushOnly success",
+			vcCreate: func(t *testing.T) *vcmock.VersionController {
+				return &vcmock.VersionController{
+					Repositories: []vcmock.Repository{
+						createRepo(t, "owner", "example-repository", "i like apples"),
+					},
+				}
+			},
+			args: []string{
+				"run",
+				"--author-name", "Test Author",
+				"--author-email", "test@example.com",
+				"-B", "custom-branch-name",
+				"-m", "custom message",
+				"--push-only",
+				changerBinaryPath,
+			},
+			verify: func(t *testing.T, vcMock *vcmock.VersionController, runData runData) {
+				assert.Equal(t, runData.cmdOut, "")
+				assert.Equal(t, runData.out, "Repositories with a successful run:\n  owner/example-repository #0\n")
+			},
+		},
 	}
 
 	for _, gitBackend := range gitBackends {