Repository: lindell/multi-gitter (go)

Issue:
Add flag to update a pull request thats already opened
After I've run multi-gitter and opened my prs, it would be great to have the ability to update my pr in the scenario where maybe I've missed something, or make a mistake and need to update.

Something like a 'multi-gitter run --update-pr`

Pull request:
feat: added --conflict-strategy
This branchs adds `--conflict-strategy` and implements `skip` (previous default behaviour) and `replace`, where the changes to existing branches are replaced by a force push. The `append` strategy is intended to be implemented, but requires more additions and will be done separately. 

| Type | Description |
|-|-|
| Skip| The current behavior and the behavior that should most likely always be the default. Simply skip making any changes to the branch |
| ~Append~ | ~The changes are made on top of any already existing commits in the branch.~ |
| Replace | If a branch already exist and a change was made, replace it the new changes, and force push the changes. |

Closes #205


Patch:
diff --git a/cmd/cmd-run.go b/cmd/cmd-run.go
index f16380e..d2685b5 100755
--- a/cmd/cmd-run.go
+++ b/cmd/cmd-run.go
@@ -45,6 +45,14 @@ func RunCmd() *cobra.Command {
 	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.")
+	cmd.Flags().StringP("conflict-strategy", "", "skip", `What should happen if the branch already exist.
+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.RegisterFlagCompletionFunc("conflict-strategy", func(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
+		return []string{"skip", "replace"}, cobra.ShellCompDirectiveNoFileComp
+	})
 	cmd.Flags().StringP("author-name", "", "", "Name of the committer. If not set, the global git config setting will be used.")
 	cmd.Flags().StringP("author-email", "", "", "Email of the committer. If not set, the global git config setting will be used.")
 	configureGit(cmd)
@@ -74,6 +82,7 @@ func run(cmd *cobra.Command, args []string) error {
 	dryRun, _ := flag.GetBool("dry-run")
 	forkMode, _ := flag.GetBool("fork")
 	forkOwner, _ := flag.GetString("fork-owner")
+	conflictStrategyStr, _ := flag.GetString("conflict-strategy")
 	authorName, _ := flag.GetString("author-name")
 	authorEmail, _ := flag.GetString("author-email")
 	strOutput, _ := flag.GetString("output")
@@ -139,6 +148,11 @@ func run(cmd *cobra.Command, args []string) error {
 		return err
 	}
 
+	conflictStrategy, err := multigitter.ParseConflictStrategy(conflictStrategyStr)
+	if err != nil {
+		return err
+	}
+
 	// Set up signal listening to cancel the context and let started runs finish gracefully
 	ctx, cancel := context.WithCancel(context.Background())
 	c := make(chan os.Signal, 1)
@@ -174,6 +188,7 @@ func run(cmd *cobra.Command, args []string) error {
 		CommitAuthor:     commitAuthor,
 		BaseBranch:       baseBranchName,
 		Assignees:        assignees,
+		ConflictStrategy: conflictStrategy,
 
 		Concurrent: concurrent,
 
diff --git a/cmd/platform.go b/cmd/platform.go
index 754e7f1..b96d107 100644
--- a/cmd/platform.go
+++ b/cmd/platform.go
@@ -253,6 +253,10 @@ func createBitbucketServerClient(flag *flag.FlagSet, verifyFlags bool) (multigit
 		return nil, errors.New("no base-url set for bitbucket server")
 	}
 
+	if username == "" {
+		return nil, errors.New("no username set")
+	}
+
 	token, err := getToken(flag)
 	if err != nil {
 		return nil, err
diff --git a/go.mod b/go.mod
index 6bb27e9..c9d58f2 100644
--- a/go.mod
+++ b/go.mod
@@ -3,11 +3,11 @@ module github.com/lindell/multi-gitter
 go 1.16
 
 require (
-	code.gitea.io/sdk/gitea v0.14.0
+	code.gitea.io/sdk/gitea v0.15.0
 	github.com/eiannone/keyboard v0.0.0-20200508000154-caf4b762e807
 	github.com/gfleury/go-bitbucket-v1 v0.0.0-20210826163055-dff2223adeac
 	github.com/go-git/go-git/v5 v5.4.2
-	github.com/google/go-github/v39 v39.1.0
+	github.com/google/go-github/v39 v39.2.0
 	github.com/mitchellh/mapstructure v1.4.2
 	github.com/pkg/errors v0.9.1
 	github.com/sirupsen/logrus v1.8.1
@@ -15,6 +15,6 @@ require (
 	github.com/spf13/pflag v1.0.5
 	github.com/spf13/viper v1.9.0
 	github.com/stretchr/testify v1.7.0
-	github.com/xanzy/go-gitlab v0.50.1
+	github.com/xanzy/go-gitlab v0.51.1
 	golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f
 )
diff --git a/go.sum b/go.sum
index fc11a48..5ba4c16 100644
--- a/go.sum
+++ b/go.sum
@@ -42,8 +42,9 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
 cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
 cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
 cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
-code.gitea.io/sdk/gitea v0.14.0 h1:m4J352I3p9+bmJUfS+g0odeQzBY/5OXP91Gv6D4fnJ0=
-code.gitea.io/sdk/gitea v0.14.0/go.mod h1:89WiyOX1KEcvjP66sRHdu0RafojGo60bT9UqW17VbWs=
+code.gitea.io/gitea-vet v0.2.1/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE=
+code.gitea.io/sdk/gitea v0.15.0 h1:tsNhxDM/2N1Ohv1Xq5UWrht/esg0WmtRj4wsHVHriTg=
+code.gitea.io/sdk/gitea v0.15.0/go.mod h1:klY2LVI3s3NChzIk/MzMn7G1FHrfU7qd63iSMVoHRBA=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
@@ -169,8 +170,8 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
 github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-github/v39 v39.1.0 h1:1vf4gM0D1e+Df2HMxaYC3+o9+Huj3ywGTtWc3VVYaDA=
-github.com/google/go-github/v39 v39.1.0/go.mod h1:C1s8C5aCC9L+JXIYpJM5GYytdX52vC1bLvHEF1IhBrE=
+github.com/google/go-github/v39 v39.2.0 h1:rNNM311XtPOz5rDdsJXAp2o8F67X9FnROXTvto3aSnQ=
+github.com/google/go-github/v39 v39.2.0/go.mod h1:C1s8C5aCC9L+JXIYpJM5GYytdX52vC1bLvHEF1IhBrE=
 github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
 github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
 github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
@@ -351,8 +352,8 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
 github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
-github.com/xanzy/go-gitlab v0.50.1 h1:eH1G0/ZV1j81rhGrtbcePjbM5Ern7mPA4Xjt+yE+2PQ=
-github.com/xanzy/go-gitlab v0.50.1/go.mod h1:Q+hQhV508bDPoBijv7YjK/Lvlb4PhVhJdKqXVQrUoAE=
+github.com/xanzy/go-gitlab v0.51.1 h1:wWKLalwx4omxFoHh3PLs9zDgAD4GXDP/uoxwMRCSiWM=
+github.com/xanzy/go-gitlab v0.51.1/go.mod h1:Q+hQhV508bDPoBijv7YjK/Lvlb4PhVhJdKqXVQrUoAE=
 github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI=
 github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0=
 github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -602,6 +603,7 @@ golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapK
 golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
 golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
+golang.org/x/tools v0.0.0-20200325010219-a49f79bcc224/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
 golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
 golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
 golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
diff --git a/internal/git/cmdgit/git.go b/internal/git/cmdgit/git.go
index 68e843d..2655d0a 100644
--- a/internal/git/cmdgit/git.go
+++ b/internal/git/cmdgit/git.go
@@ -129,8 +129,14 @@ func (g *Git) BranchExist(remoteName, branchName string) (bool, error) {
 }
 
 // Push the committed changes to the remote
-func (g *Git) Push(remoteName string) error {
-	cmd := exec.Command("git", "push", "--no-verify", remoteName, "HEAD")
+func (g *Git) Push(remoteName string, force bool) error {
+	args := []string{"push", "--no-verify", remoteName}
+	if force {
+		args = append(args, "--force")
+	}
+	args = append(args, "HEAD")
+
+	cmd := exec.Command("git", args...)
 	_, err := g.run(cmd)
 	return err
 }
diff --git a/internal/git/gogit/git.go b/internal/git/gogit/git.go
index a350581..4314c41 100755
--- a/internal/git/gogit/git.go
+++ b/internal/git/gogit/git.go
@@ -186,9 +186,10 @@ func (g *Git) BranchExist(remoteName, branchName string) (bool, error) {
 }
 
 // Push the committed changes to the remote
-func (g *Git) Push(remoteName string) error {
+func (g *Git) Push(remoteName string, force bool) error {
 	return g.repo.Push(&git.PushOptions{
 		RemoteName: remoteName,
+		Force:      force,
 	})
 }
 
diff --git a/internal/multigitter/run.go b/internal/multigitter/run.go
index fb41559..b9759be 100755
--- a/internal/multigitter/run.go
+++ b/internal/multigitter/run.go
@@ -27,6 +27,7 @@ type VersionController interface {
 	GetRepositories(ctx context.Context) ([]scm.Repository, error)
 	CreatePullRequest(ctx context.Context, repo scm.Repository, prRepo scm.Repository, newPR scm.NewPullRequest) (scm.PullRequest, error)
 	GetPullRequests(ctx context.Context, branchName string) ([]scm.PullRequest, error)
+	GetOpenPullRequest(ctx context.Context, repo scm.Repository, branchName string) (scm.PullRequest, error)
 	MergePullRequest(ctx context.Context, pr scm.PullRequest) error
 	ClosePullRequest(ctx context.Context, pr scm.PullRequest) error
 	ForkRepository(ctx context.Context, repo scm.Repository, newOwner string) (scm.Repository, error)
@@ -59,6 +60,8 @@ type Runner struct {
 	Fork      bool   // If set, create a fork and make the pull request from it
 	ForkOwner string // The owner of the new fork. If empty, the fork should happen on the logged in user
 
+	ConflictStrategy ConflictStrategy // Defines what will happen if a branch does already exist
+
 	Interactive bool // If set, interactive mode is activated and the user will be asked to verify every change
 
 	CreateGit func(dir string) Git
@@ -275,17 +278,20 @@ func (r *Runner) runSingleRepo(ctx context.Context, repo scm.Repository) (scm.Pu
 		remoteName = "fork"
 	}
 
+	// Determine if a branch already exist and (depending on the conflict strategy) skip making changes
+	featureBranchExist := false
 	if !r.SkipPullRequest {
-		featureBranchExist, err := sourceController.BranchExist(remoteName, r.FeatureBranch)
+		featureBranchExist, err = sourceController.BranchExist(remoteName, r.FeatureBranch)
 		if err != nil {
 			return nil, errors.Wrap(err, "could not verify if branch already exist")
-		} else if featureBranchExist {
+		} else if featureBranchExist && r.ConflictStrategy == ConflictStrategySkip {
 			return nil, errBranchExist
 		}
 	}
 
 	log.Info("Pushing changes to remote")
-	err = sourceController.Push(remoteName)
+	forcePush := featureBranchExist && r.ConflictStrategy == ConflictStrategyReplace
+	err = sourceController.Push(remoteName, forcePush)
 	if err != nil {
 		return nil, errors.Wrap(err, "could not push changes")
 	}
@@ -294,17 +300,33 @@ func (r *Runner) runSingleRepo(ctx context.Context, repo scm.Repository) (scm.Pu
 		return nil, nil
 	}
 
-	log.Info("Creating pull request")
-	pr, err := r.VersionController.CreatePullRequest(ctx, repo, prRepo, scm.NewPullRequest{
-		Title:     r.PullRequestTitle,
-		Body:      r.PullRequestBody,
-		Head:      r.FeatureBranch,
-		Base:      baseBranch,
-		Reviewers: getReviewers(r.Reviewers, r.MaxReviewers),
-		Assignees: r.Assignees,
-	})
-	if err != nil {
-		return nil, err
+	// Fetching any potentially existing pull request
+	var existingPullRequest scm.PullRequest = nil
+	if featureBranchExist {
+		pr, err := r.VersionController.GetOpenPullRequest(ctx, repo, r.FeatureBranch)
+		if err != nil {
+			return nil, err
+		}
+		existingPullRequest = pr
+	}
+
+	var pr scm.PullRequest
+	if existingPullRequest != nil {
+		log.Info("Skip creating pull requests since one is already open")
+		pr = existingPullRequest
+	} else {
+		log.Info("Creating pull request")
+		pr, err = r.VersionController.CreatePullRequest(ctx, repo, prRepo, scm.NewPullRequest{
+			Title:     r.PullRequestTitle,
+			Body:      r.PullRequestBody,
+			Head:      r.FeatureBranch,
+			Base:      baseBranch,
+			Reviewers: getReviewers(r.Reviewers, r.MaxReviewers),
+			Assignees: r.Assignees,
+		})
+		if err != nil {
+			return nil, err
+		}
 	}
 
 	return pr, nil
diff --git a/internal/multigitter/shared.go b/internal/multigitter/shared.go
index 8add55c..cd030fa 100644
--- a/internal/multigitter/shared.go
+++ b/internal/multigitter/shared.go
@@ -29,7 +29,7 @@ type Git interface {
 	Changes() (bool, error)
 	Commit(commitAuthor *git.CommitAuthor, commitMessage string) error
 	BranchExist(remoteName, branchName string) (bool, error)
-	Push(remoteName string) error
+	Push(remoteName string, force bool) error
 	AddRemote(name, url string) error
 }
 
@@ -47,3 +47,25 @@ func getStackTrace(err error) string {
 	}
 	return ""
 }
+
+// ConflictStrategy define how a conflict of an already existing branch should be handled
+type ConflictStrategy int
+
+const (
+	// ConflictStrategySkip will skip the run for if the branch does already exist
+	ConflictStrategySkip ConflictStrategy = iota + 1
+	// ConflictStrategyReplace will ignore any existing branch and replace it with new changes
+	ConflictStrategyReplace
+)
+
+// ParseConflictStrategy parses a conflict strategy from a string
+func ParseConflictStrategy(str string) (ConflictStrategy, error) {
+	switch str {
+	default:
+		return ConflictStrategy(0), fmt.Errorf("could not parse \"%s\" as conflict strategy", str)
+	case "skip":
+		return ConflictStrategySkip, nil
+	case "replace":
+		return ConflictStrategyReplace, nil
+	}
+}
diff --git a/internal/scm/bitbucketserver/bitbucket_server.go b/internal/scm/bitbucketserver/bitbucket_server.go
index 736a92f..6cf1046 100644
--- a/internal/scm/bitbucketserver/bitbucket_server.go
+++ b/internal/scm/bitbucketserver/bitbucket_server.go
@@ -318,7 +318,7 @@ func (b *BitbucketServer) GetPullRequests(ctx context.Context, branchName string
 
 	var prs []scm.PullRequest
 	for _, repo := range repositories {
-		pr, getPullRequestErr := b.getPullRequest(client, branchName, repo)
+		pr, getPullRequestErr := b.getPullRequest(client, branchName, repo.Project.Key, repo.Slug)
 		if getPullRequestErr != nil {
 			return nil, getPullRequestErr
 		}
@@ -326,28 +326,37 @@ func (b *BitbucketServer) GetPullRequests(ctx context.Context, branchName string
 			continue
 		}
 
-		status, pullRequestStatusErr := b.pullRequestStatus(client, repo, pr)
-		if pullRequestStatusErr != nil {
-			return nil, pullRequestStatusErr
+		convertedPR, err := b.convertPullRequest(client, repo.Project.Key, repo.Slug, branchName, pr)
+		if err != nil {
+			return nil, err
 		}
 
-		prs = append(prs, pullRequest{
-			repoName:   repo.Slug,
-			project:    repo.Project.Key,
-			branchName: branchName,
-			prProject:  pr.FromRef.Repository.Project.Key,
-			prRepoName: pr.FromRef.Repository.Slug,
-			number:     pr.ID,
-			version:    pr.Version,
-			guiURL:     pr.Links.Self[0].Href,
-			status:     status,
-		})
+		prs = append(prs, convertedPR)
 	}
 
 	return prs, nil
 }
 
-func (b *BitbucketServer) pullRequestStatus(client *bitbucketv1.APIClient, repo *bitbucketv1.Repository, pr *bitbucketv1.PullRequest) (scm.PullRequestStatus, error) {
+func (b *BitbucketServer) convertPullRequest(client *bitbucketv1.APIClient, project, repoName, branchName string, pr *bitbucketv1.PullRequest) (pullRequest, error) {
+	status, err := b.pullRequestStatus(client, project, repoName, pr)
+	if err != nil {
+		return pullRequest{}, err
+	}
+
+	return pullRequest{
+		repoName:   repoName,
+		project:    project,
+		branchName: branchName,
+		prProject:  pr.FromRef.Repository.Project.Key,
+		prRepoName: pr.FromRef.Repository.Slug,
+		number:     pr.ID,
+		version:    pr.Version,
+		guiURL:     pr.Links.Self[0].Href,
+		status:     status,
+	}, nil
+}
+
+func (b *BitbucketServer) pullRequestStatus(client *bitbucketv1.APIClient, project, repoName string, pr *bitbucketv1.PullRequest) (scm.PullRequestStatus, error) {
 	switch pr.State {
 	case stateMerged:
 		return scm.PullRequestStatusMerged, nil
@@ -355,7 +364,7 @@ func (b *BitbucketServer) pullRequestStatus(client *bitbucketv1.APIClient, repo
 		return scm.PullRequestStatusClosed, nil
 	}
 
-	response, err := client.DefaultApi.CanMerge(repo.Project.Key, repo.Slug, int64(pr.ID))
+	response, err := client.DefaultApi.CanMerge(project, repoName, int64(pr.ID))
 	if err != nil {
 		return scm.PullRequestStatusUnknown, err
 	}
@@ -377,12 +386,12 @@ func (b *BitbucketServer) pullRequestStatus(client *bitbucketv1.APIClient, repo
 	return scm.PullRequestStatusSuccess, nil
 }
 
-func (b *BitbucketServer) getPullRequest(client *bitbucketv1.APIClient, branchName string, repo *bitbucketv1.Repository) (*bitbucketv1.PullRequest, error) {
+func (b *BitbucketServer) getPullRequest(client *bitbucketv1.APIClient, branchName, project, repoName string) (*bitbucketv1.PullRequest, error) {
 	params := map[string]interface{}{"start": 0, "limit": 25}
 
 	var pullRequests []bitbucketv1.PullRequest
 	for {
-		response, err := client.DefaultApi.GetPullRequestsPage(repo.Project.Key, repo.Slug, params)
+		response, err := client.DefaultApi.GetPullRequestsPage(project, repoName, params)
 		if err != nil {
 			return nil, err
 		}
@@ -411,6 +420,29 @@ func (b *BitbucketServer) getPullRequest(client *bitbucketv1.APIClient, branchNa
 	return nil, nil
 }
 
+// GetOpenPullRequest gets a pull request for one specific repository
+func (b *BitbucketServer) GetOpenPullRequest(ctx context.Context, repo scm.Repository, branchName string) (scm.PullRequest, error) {
+	r := repo.(repository)
+
+	client := newClient(ctx, b.config)
+
+	pr, err := b.getPullRequest(client, branchName, r.project, r.name)
+	if err != nil {
+		return nil, err
+	}
+
+	if pr == nil {
+		return nil, nil
+	}
+
+	convertedPR, err := b.convertPullRequest(client, r.project, r.name, branchName, pr)
+	if err != nil {
+		return nil, err
+	}
+
+	return convertedPR, nil
+}
+
 // MergePullRequest Merges a pull request, the pr parameter will always originate from the same package
 func (b *BitbucketServer) MergePullRequest(ctx context.Context, pr scm.PullRequest) error {
 	bitbucketPR := pr.(pullRequest)
diff --git a/internal/scm/gitea/gitea.go b/internal/scm/gitea/gitea.go
index dacfd40..0c7775a 100644
--- a/internal/scm/gitea/gitea.go
+++ b/internal/scm/gitea/gitea.go
@@ -204,7 +204,10 @@ func (g *Gitea) CreatePullRequest(ctx context.Context, repo scm.Repository, prRe
 	r := repo.(repository)
 	prR := prRepo.(repository)
 
-	head := fmt.Sprintf("%s:%s", prR.ownerName, newPR.Head)
+	head := newPR.Head
+	if r.ownerName != prR.ownerName {
+		head = fmt.Sprintf("%s:%s", prR.ownerName, newPR.Head)
+	}
 
 	pr, _, err := g.giteaClient(ctx).CreatePullRequest(r.ownerName, r.name, gitea.CreatePullRequestOption{
 		Head:      head,
@@ -244,7 +247,7 @@ func (g *Gitea) GetPullRequests(ctx context.Context, branchName string) ([]scm.P
 
 	prs := []scm.PullRequest{}
 	for _, repo := range repos {
-		pr, err := g.getPullRequest(ctx, branchName, repo)
+		pr, err := g.getPullRequest(ctx, branchName, repo.Owner.UserName, repo.Name, gitea.StateAll)
 		if err != nil {
 			return nil, err
 		}
@@ -252,30 +255,39 @@ func (g *Gitea) GetPullRequests(ctx context.Context, branchName string) ([]scm.P
 			continue
 		}
 
-		status, err := g.pullRequestStatus(ctx, repo, pr)
+		convertedPR, err := g.convertPullRequest(ctx, pr)
 		if err != nil {
 			return nil, err
 		}
 
-		prs = append(prs, pullRequest{
-			repoName:    repo.Name,
-			ownerName:   repo.Owner.UserName,
-			branchName:  branchName,
-			prOwnerName: pr.Head.Repository.Owner.UserName,
-			prRepoName:  pr.Head.Repository.Name,
-			status:      status,
-			index:       pr.Index,
-			webURL:      pr.HTMLURL,
-		})
+		prs = append(prs, convertedPR)
 	}
 
 	return prs, nil
 }
 
-func (g *Gitea) getPullRequest(ctx context.Context, branchName string, repo *gitea.Repository) (*gitea.PullRequest, error) {
+func (g *Gitea) convertPullRequest(ctx context.Context, pr *gitea.PullRequest) (pullRequest, error) {
+	status, err := g.pullRequestStatus(ctx, pr)
+	if err != nil {
+		return pullRequest{}, err
+	}
+
+	return pullRequest{
+		repoName:    pr.Base.Repository.Name,
+		ownerName:   pr.Base.Repository.Owner.UserName,
+		branchName:  pr.Head.Name,
+		prOwnerName: pr.Head.Repository.Owner.UserName,
+		prRepoName:  pr.Head.Repository.Name,
+		status:      status,
+		index:       pr.Index,
+		webURL:      pr.HTMLURL,
+	}, nil
+}
+
+func (g *Gitea) getPullRequest(ctx context.Context, branchName string, owner, repoName string, state gitea.StateType) (*gitea.PullRequest, error) {
 	// We would like to be able to search for a pr with a specific head here, but current (2021-04-24), that option does not exist in the API
-	prs, _, err := g.giteaClient(ctx).ListRepoPullRequests(repo.Owner.UserName, repo.Name, gitea.ListPullRequestsOptions{
-		State: "all",
+	prs, _, err := g.giteaClient(ctx).ListRepoPullRequests(owner, repoName, gitea.ListPullRequestsOptions{
+		State: state,
 		Sort:  "recentupdate",
 	})
 	if err != nil {
@@ -290,7 +302,7 @@ func (g *Gitea) getPullRequest(ctx context.Context, branchName string, repo *git
 	return nil, nil
 }
 
-func (g *Gitea) pullRequestStatus(ctx context.Context, repo *gitea.Repository, pr *gitea.PullRequest) (scm.PullRequestStatus, error) {
+func (g *Gitea) pullRequestStatus(ctx context.Context, pr *gitea.PullRequest) (scm.PullRequestStatus, error) {
 	if pr.Merged != nil {
 		return scm.PullRequestStatusMerged, nil
 	}
@@ -299,7 +311,7 @@ func (g *Gitea) pullRequestStatus(ctx context.Context, repo *gitea.Repository, p
 		return scm.PullRequestStatusClosed, nil
 	}
 
-	status, _, err := g.giteaClient(ctx).GetCombinedStatus(repo.Owner.UserName, repo.Name, pr.Head.Sha)
+	status, _, err := g.giteaClient(ctx).GetCombinedStatus(pr.Base.Repository.Owner.UserName, pr.Base.Repository.Name, pr.Head.Sha)
 	if err != nil {
 		return scm.PullRequestStatusUnknown, err
 	}
@@ -320,6 +332,21 @@ func (g *Gitea) pullRequestStatus(ctx context.Context, repo *gitea.Repository, p
 	return scm.PullRequestStatusUnknown, nil
 }
 
+// GetOpenPullRequest gets a pull request for one specific repository
+func (g *Gitea) GetOpenPullRequest(ctx context.Context, repo scm.Repository, branchName string) (scm.PullRequest, error) {
+	r := repo.(repository)
+
+	pr, err := g.getPullRequest(ctx, branchName, r.ownerName, r.name, gitea.StateOpen)
+	if err != nil {
+		return nil, err
+	}
+	if pr == nil {
+		return nil, nil
+	}
+
+	return g.convertPullRequest(ctx, pr)
+}
+
 // MergePullRequest merges a pull request
 func (g *Gitea) MergePullRequest(ctx context.Context, pullReq scm.PullRequest) error {
 	pr := pullReq.(pullRequest)
diff --git a/internal/scm/github/github.go b/internal/scm/github/github.go
index 75d93f4..34a4760 100755
--- a/internal/scm/github/github.go
+++ b/internal/scm/github/github.go
@@ -70,7 +70,8 @@ type Github struct {
 	ghClient *github.Client
 
 	// Caching of the logged in user
-	user string
+	user      string
+	userMutex sync.Mutex
 
 	// Used to make sure request that modifies state does not happen to often
 	modMutex       sync.Mutex
@@ -309,16 +310,9 @@ func (g *Github) GetPullRequests(ctx context.Context, branchName string) ([]scm.
 		repoName := r.GetName()
 		log := log.WithField("repo", fmt.Sprintf("%s/%s", repoOwner, repoName))
 
-		headOwner := repoOwner
-		if g.Fork {
-			if g.ForkOwner != "" {
-				headOwner = g.ForkOwner
-			} else {
-				headOwner, err = g.loggedInUser(ctx)
-				if err != nil {
-					return nil, err
-				}
-			}
+		headOwner, err := g.headOwner(ctx, repoOwner)
+		if err != nil {
+			return nil, err
 		}
 
 		log.Debug("Fetching latest pull request")
@@ -352,6 +346,9 @@ func (g *Github) GetPullRequests(ctx context.Context, branchName string) ([]scm.
 }
 
 func (g *Github) loggedInUser(ctx context.Context) (string, error) {
+	g.userMutex.Lock()
+	defer g.userMutex.Unlock()
+
 	if g.user != "" {
 		return g.user, nil
 	}
@@ -366,6 +363,46 @@ func (g *Github) loggedInUser(ctx context.Context) (string, error) {
 	return g.user, nil
 }
 
+// headOwner returns the owner of the repository from which any pullrequest will be made
+// This is normally the owner of the original repository, but if a fork has been made
+// it will be a different owner
+func (g *Github) headOwner(ctx context.Context, repoOwner string) (string, error) {
+	if !g.Fork {
+		return repoOwner, nil
+	}
+
+	if g.ForkOwner != "" {
+		return g.ForkOwner, nil
+	}
+
+	return g.loggedInUser(ctx)
+}
+
+// GetOpenPullRequest gets a pull request for one specific repository
+func (g *Github) GetOpenPullRequest(ctx context.Context, repo scm.Repository, branchName string) (scm.PullRequest, error) {
+	r := repo.(repository)
+
+	headOwner, err := g.headOwner(ctx, r.ownerName)
+	if err != nil {
+		return nil, err
+	}
+
+	prs, _, err := g.ghClient.PullRequests.List(ctx, headOwner, r.name, &github.PullRequestListOptions{
+		Head:  fmt.Sprintf("%s:%s", headOwner, branchName),
+		State: "open",
+		ListOptions: github.ListOptions{
+			PerPage: 1,
+		},
+	})
+	if err != nil {
+		return nil, err
+	}
+	if len(prs) == 0 {
+		return nil, nil
+	}
+	return convertPullRequest(prs[0]), nil
+}
+
 // MergePullRequest merges a pull request
 func (g *Github) MergePullRequest(ctx context.Context, pullReq scm.PullRequest) error {
 	pr := pullReq.(pullRequest)
diff --git a/internal/scm/gitlab/gitlab.go b/internal/scm/gitlab/gitlab.go
index 6884c20..2781122 100644
--- a/internal/scm/gitlab/gitlab.go
+++ b/internal/scm/gitlab/gitlab.go
@@ -291,21 +291,25 @@ func (g *Gitlab) GetPullRequests(ctx context.Context, branchName string) ([]scm.
 			continue
 		}
 
-		prs = append(prs, pullRequest{
-			repoName:   project.Path,
-			ownerName:  project.Namespace.Path,
-			targetPID:  mr.TargetProjectID,
-			sourcePID:  mr.SourceProjectID,
-			branchName: branchName,
-			status:     pullRequestStatus(mr),
-			iid:        mr.IID,
-			webURL:     mr.WebURL,
-		})
+		prs = append(prs, convertMergeRequest(mr, project.Path, project.Namespace.Path))
 	}
 
 	return prs, nil
 }
 
+func convertMergeRequest(mr *gitlab.MergeRequest, repoName, ownerName string) pullRequest {
+	return pullRequest{
+		repoName:   repoName,
+		ownerName:  ownerName,
+		targetPID:  mr.TargetProjectID,
+		sourcePID:  mr.SourceProjectID,
+		branchName: mr.SourceBranch,
+		status:     pullRequestStatus(mr),
+		iid:        mr.IID,
+		webURL:     mr.WebURL,
+	}
+}
+
 func (g *Gitlab) getPullRequest(ctx context.Context, branchName string, project *gitlab.Project) (*gitlab.MergeRequest, error) {
 	mrs, _, err := g.glClient.MergeRequests.ListProjectMergeRequests(project.ID, &gitlab.ListProjectMergeRequestsOptions{
 		ListOptions: gitlab.ListOptions{
@@ -343,6 +347,29 @@ func pullRequestStatus(mr *gitlab.MergeRequest) scm.PullRequestStatus {
 	}
 }
 
+// GetOpenPullRequest gets a pull request for one specific repository
+func (g *Gitlab) GetOpenPullRequest(ctx context.Context, repo scm.Repository, branchName string) (scm.PullRequest, error) {
+	project := repo.(repository)
+
+	state := "opened"
+	mrs, _, err := g.glClient.MergeRequests.ListProjectMergeRequests(project.pid, &gitlab.ListProjectMergeRequestsOptions{
+		ListOptions: gitlab.ListOptions{
+			PerPage: 1,
+		},
+		SourceBranch: &branchName,
+		State:        &state,
+	}, gitlab.WithContext(ctx))
+	if err != nil {
+		return nil, err
+	}
+
+	if len(mrs) == 0 {
+		return nil, nil
+	}
+
+	return convertMergeRequest(mrs[0], project.name, project.ownerName), nil
+}
+
 // MergePullRequest merges a pull request
 func (g *Gitlab) MergePullRequest(ctx context.Context, pullReq scm.PullRequest) error {
 	pr := pullReq.(pullRequest)


Test patch:
diff --git a/tests/table_test.go b/tests/table_test.go
index 675ef56..cb9af5f 100644
--- a/tests/table_test.go
+++ b/tests/table_test.go
@@ -11,6 +11,7 @@ import (
 	"time"
 
 	"github.com/lindell/multi-gitter/cmd"
+	"github.com/lindell/multi-gitter/internal/scm"
 	"github.com/lindell/multi-gitter/tests/vcmock"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -717,6 +718,94 @@ Repositories with a successful run:
 				assert.Contains(t, vcMock.PullRequests[0].Assignees, "assignee2")
 			},
 		},
+
+		{
+			name: "replace conflict strategy",
+			vcCreate: func(t *testing.T) *vcmock.VersionController {
+				// Create and run mg with these different repositories:
+				// 1. No branch exist when running, change is expected
+				// 2. A branch exist, but no PR
+				// 3. A branch and pull request exist
+				// 4. No branch exist when running, no change should be made
+				// 5. A branch exist, no change compared to the base branch should be made
+				repo1 := createRepo(t, "owner", "no-existing-branch", "i like apples")
+				repo2 := createRepo(t, "owner", "existing-branch", "i like apples")
+				changeBranch(t, repo2.Path, "custom-branch-name", true)
+				changeTestFile(t, repo2.Path, "i like apple", "test change")
+				changeBranch(t, repo2.Path, "master", false)
+				repo3 := createRepo(t, "owner", "existing-pr", "i like apples")
+				changeBranch(t, repo3.Path, "custom-branch-name", true)
+				changeTestFile(t, repo3.Path, "i like apple", "test change")
+				changeBranch(t, repo3.Path, "master", false)
+				repo4 := createRepo(t, "owner", "no-change-1", "i like oranges")
+				repo5 := createRepo(t, "owner", "no-change-2", "i like oranges")
+				changeBranch(t, repo5.Path, "custom-branch-name", true)
+				changeTestFile(t, repo5.Path, "i like apples", "test change")
+				changeBranch(t, repo5.Path, "master", false)
+
+				return &vcmock.VersionController{
+					Repositories: []vcmock.Repository{
+						repo1,
+						repo2,
+						repo3,
+						repo4,
+						repo5,
+					},
+					PullRequests: []vcmock.PullRequest{
+						{
+							PRStatus:   scm.PullRequestStatusSuccess,
+							PRNumber:   42,
+							Repository: repo3,
+							NewPullRequest: scm.NewPullRequest{
+								Head: "custom-branch-name",
+							},
+						},
+					},
+				}
+			},
+			args: []string{
+				"run",
+				"--author-name", "Test Author",
+				"--author-email", "test@example.com",
+				"-B", "custom-branch-name",
+				"-m", "custom message",
+				"--conflict-strategy", "replace",
+				changerBinaryPath,
+			},
+			verify: func(t *testing.T, vcMock *vcmock.VersionController, runData runData) {
+				fmt.Println(runData.logOut)
+
+				require.Len(t, vcMock.PullRequests, 3)
+				assert.Equal(t, "custom-branch-name", vcMock.PullRequests[1].Head)
+				assert.Equal(t, "master", vcMock.PullRequests[1].Base)
+				assert.Equal(t, "custom message", vcMock.PullRequests[1].Title)
+				assert.Equal(t, "custom-branch-name", vcMock.PullRequests[2].Head)
+				assert.Equal(t, "master", vcMock.PullRequests[2].Base)
+				assert.Equal(t, "custom message", vcMock.PullRequests[2].Title)
+
+				assert.Contains(t, runData.logOut, "Running on 5 repositories")
+				assert.Equal(t, 5, strings.Count(runData.logOut, "Cloning and running script"))
+				assert.Equal(t, 3, strings.Count(runData.logOut, "Pushing changes to remote"))
+				assert.Equal(t, 2, strings.Count(runData.logOut, "Creating pull request"))
+				assert.Equal(t, 1, strings.Count(runData.logOut, "Skip creating pull requests since one is already open"))
+
+				assert.Equal(t, `No data was changed:
+  owner/no-change-1
+  owner/no-change-2
+Repositories with a successful run:
+  owner/no-existing-branch #1
+  owner/existing-branch #2
+  owner/existing-pr #42
+`, runData.out)
+
+				changeBranch(t, vcMock.Repositories[0].Path, "custom-branch-name", false)
+				assert.Equal(t, "i like bananas", readTestFile(t, vcMock.Repositories[0].Path))
+				changeBranch(t, vcMock.Repositories[1].Path, "custom-branch-name", false)
+				assert.Equal(t, "i like bananas", readTestFile(t, vcMock.Repositories[1].Path))
+				changeBranch(t, vcMock.Repositories[2].Path, "custom-branch-name", false)
+				assert.Equal(t, "i like bananas", readTestFile(t, vcMock.Repositories[2].Path))
+			},
+		},
 	}
 
 	for _, gitBackend := range gitBackends {
diff --git a/tests/vcmock/vcmock.go b/tests/vcmock/vcmock.go
index 71cdf5e..7ca317d 100644
--- a/tests/vcmock/vcmock.go
+++ b/tests/vcmock/vcmock.go
@@ -65,6 +65,25 @@ func (vc *VersionController) GetPullRequests(ctx context.Context, branchName str
 	return ret, nil
 }
 
+// GetOpenPullRequest gets mock open pull request
+func (vc *VersionController) GetOpenPullRequest(ctx context.Context, repo scm.Repository, branchName string) (scm.PullRequest, error) {
+	vc.prLock.RLock()
+	defer vc.prLock.RUnlock()
+
+	r := repo.(Repository)
+
+	for _, pr := range vc.PullRequests {
+		if r.OwnerName == pr.OwnerName && r.RepoName == pr.RepoName && pr.NewPullRequest.Head == branchName && openPullRequest(pr) {
+			return pr, nil
+		}
+	}
+	return nil, nil
+}
+
+func openPullRequest(pr PullRequest) bool {
+	return pr.PRStatus == scm.PullRequestStatusSuccess || pr.PRStatus == scm.PullRequestStatusPending
+}
+
 // MergePullRequest sets the status of a mock pull requests to merged
 func (vc *VersionController) MergePullRequest(ctx context.Context, pr scm.PullRequest) error {
 	vc.prLock.Lock()