Repository: lindell/multi-gitter
Pull request #128 (2021-06-05 19:16:41)

feat: added fork mode
- [x] Support for forking to custom owner
- [x] Support forking on GitHub
- [x] Support forking on GitLab
- [x] Support forking on Gitea
- [x] Make branch already exist error message work for forks

lindell: @dorner I would consider the changes to this PR to be mostly done. I saw that you had some experience with Go. Do you mind taking a look at the code and doing a review on it? :) 
dorner: Sure! I'm *far* from an expert (I've only done two small projects so far) but I'll give it a whirl.
lindell: Thanks :) Supporting forking is something that's been on my mind for some time. Getting confirmation that it was actually needed gave me the motivation to make it happened!

Patch:
diff --git a/.golangci.yml b/.golangci.yml
index bf8cdc6..2ec8a79 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -66,6 +66,7 @@ issues:
         - errcheck
         - dupl
         - gosec
+        - unparam
     - path: cmd/ # The cmd package contains a lot of descriptions that must be on a single long line
       linters:
         - lll
diff --git a/cmd/root.go b/cmd/root.go
index 2a34b66..4e98def 100755
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -227,6 +227,7 @@ func createGithubClient(flag *flag.FlagSet, verifyFlags bool) (multigitter.Versi
 	orgs, _ := flag.GetStringSlice("org")
 	users, _ := flag.GetStringSlice("user")
 	repos, _ := flag.GetStringSlice("repo")
+	forkMode, _ := flag.GetBool("fork")
 
 	if verifyFlags && len(orgs) == 0 && len(users) == 0 && len(repos) == 0 {
 		return nil, errors.New("no organization, user or repo set")
@@ -254,7 +255,7 @@ func createGithubClient(flag *flag.FlagSet, verifyFlags bool) (multigitter.Versi
 		Organizations: orgs,
 		Users:         users,
 		Repositories:  repoRefs,
-	}, mergeTypes)
+	}, mergeTypes, forkMode)
 	if err != nil {
 		return nil, err
 	}
diff --git a/cmd/run.go b/cmd/run.go
index 747535b..cc692e0 100755
--- a/cmd/run.go
+++ b/cmd/run.go
@@ -45,6 +45,8 @@ func RunCmd() *cobra.Command {
 	cmd.Flags().IntP("fetch-depth", "f", 1, "Limit fetching to the specified number of commits. Set to 0 for no limit")
 	cmd.Flags().BoolP("skip-pr", "", false, "Skip pull request and directly push to the branch")
 	cmd.Flags().BoolP("dry-run", "d", false, "Run without pushing changes or creating pull requests")
+	cmd.Flags().BoolP("fork", "", false, "Fork the repository instead of creating a new branch on the same owner")
+	cmd.Flags().StringP("fork-owner", "", "", "If set, make the fork to defined one. Default behavior is for the fork to be on the logged in user.")
 	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.")
 	configurePlatform(cmd)
@@ -68,6 +70,8 @@ func run(cmd *cobra.Command, args []string) error {
 	concurrent, _ := flag.GetInt("concurrent")
 	skipPullRequest, _ := flag.GetBool("skip-pr")
 	dryRun, _ := flag.GetBool("dry-run")
+	forkMode, _ := flag.GetBool("fork")
+	forkOwner, _ := flag.GetString("fork-owner")
 	authorName, _ := flag.GetString("author-name")
 	authorEmail, _ := flag.GetString("author-email")
 	strOutput, _ := flag.GetString("output")
@@ -104,6 +108,10 @@ func run(cmd *cobra.Command, args []string) error {
 		}
 	}
 
+	if skipPullRequest && forkMode {
+		return errors.New("--fork and --skip-pr can't be used at the same time")
+	}
+
 	// Parse commit author data
 	var commitAuthor *domain.CommitAuthor
 	if authorName != "" || authorEmail != "" {
@@ -170,6 +178,8 @@ func run(cmd *cobra.Command, args []string) error {
 		Reviewers:        reviewers,
 		MaxReviewers:     maxReviewers,
 		DryRun:           dryRun,
+		Fork:             forkMode,
+		ForkOwner:        forkOwner,
 		SkipPullRequest:  skipPullRequest,
 		CommitAuthor:     commitAuthor,
 		BaseBranch:       baseBranchName,
diff --git a/internal/git/git.go b/internal/git/git.go
index 7c568e8..50520e0 100755
--- a/internal/git/git.go
+++ b/internal/git/git.go
@@ -2,7 +2,6 @@ package git
 
 import (
 	"bytes"
-	"fmt"
 	"time"
 
 	"github.com/go-git/go-git/v5/config"
@@ -39,20 +38,6 @@ func (g *Git) Clone(baseName, headName string) error {
 		return errors.Wrap(err, "could not clone from the remote")
 	}
 
-	if headName != "" {
-		err = r.Fetch(&git.FetchOptions{
-			RefSpecs: []config.RefSpec{
-				config.RefSpec(fmt.Sprintf("refs/heads/%s:refs/remotes/origin/%s", headName, headName)),
-			},
-			Depth: g.FetchDepth,
-		})
-		if err != nil {
-			if _, ok := err.(git.NoMatchingRefSpecError); !ok {
-				return err
-			}
-		}
-	}
-
 	g.repo = r
 
 	return nil
@@ -184,17 +169,36 @@ func (g *Git) logDiff(aHash, bHash plumbing.Hash) error {
 }
 
 // BranchExist checks if the new branch exists
-func (g *Git) BranchExist(branchName string) (bool, error) {
-	_, err := g.repo.Reference(plumbing.ReferenceName(fmt.Sprintf("refs/remotes/origin/%s", branchName)), false)
-	if err == plumbing.ErrReferenceNotFound {
-		return false, nil
-	} else if err != nil {
+func (g *Git) BranchExist(remoteName, branchName string) (bool, error) {
+	remote, err := g.repo.Remote(remoteName)
+	if err != nil {
+		return false, err
+	}
+
+	refs, err := remote.List(&git.ListOptions{})
+	if err != nil {
 		return false, err
 	}
-	return true, nil
+	for _, r := range refs {
+		if r.Name().Short() == branchName {
+			return true, nil
+		}
+	}
+	return false, nil
 }
 
 // Push the committed changes to the remote
-func (g *Git) Push() error {
-	return g.repo.Push(&git.PushOptions{})
+func (g *Git) Push(remoteName string) error {
+	return g.repo.Push(&git.PushOptions{
+		RemoteName: remoteName,
+	})
+}
+
+// AddRemote adds a new remote
+func (g *Git) AddRemote(name, url string) error {
+	_, err := g.repo.CreateRemote(&config.RemoteConfig{
+		Name: name,
+		URLs: []string{url},
+	})
+	return err
 }
diff --git a/internal/multigitter/run.go b/internal/multigitter/run.go
index 4264086..8b5e8ea 100755
--- a/internal/multigitter/run.go
+++ b/internal/multigitter/run.go
@@ -22,10 +22,11 @@ import (
 // VersionController fetches repositories
 type VersionController interface {
 	GetRepositories(ctx context.Context) ([]domain.Repository, error)
-	CreatePullRequest(ctx context.Context, repo domain.Repository, newPR domain.NewPullRequest) (domain.PullRequest, error)
+	CreatePullRequest(ctx context.Context, repo domain.Repository, prRepo domain.Repository, newPR domain.NewPullRequest) (domain.PullRequest, error)
 	GetPullRequests(ctx context.Context, branchName string) ([]domain.PullRequest, error)
 	MergePullRequest(ctx context.Context, pr domain.PullRequest) error
 	ClosePullRequest(ctx context.Context, pr domain.PullRequest) error
+	ForkRepository(ctx context.Context, repo domain.Repository, newOwner string) (domain.Repository, error)
 }
 
 // Runner contains fields to be able to do the run
@@ -51,6 +52,9 @@ type Runner struct {
 	FetchDepth      int // Limit fetching to the specified number of commits. Set to 0 for no limit
 	Concurrent      int
 	SkipPullRequest bool // If set, the script will run directly on the base-branch without creating any PR
+
+	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
 }
 
 var errAborted = errors.New("run was never started because of aborted execution")
@@ -89,6 +93,7 @@ func (r Runner) Run(ctx context.Context) error {
 
 		defer func() {
 			if r := recover(); r != nil {
+				log.Error(r)
 				rc.AddError(errors.New("run paniced"), repos[i])
 			}
 		}()
@@ -169,13 +174,6 @@ func (r Runner) runSingleRepo(ctx context.Context, repo domain.Repository) (doma
 
 	// Change the branch to the feature branch
 	if !r.SkipPullRequest {
-		featureBranchExist, err := sourceController.BranchExist(r.FeatureBranch)
-		if err != nil {
-			return nil, errors.Wrap(err, "could not verify if branch already exist")
-		} else if featureBranchExist {
-			return nil, domain.BranchExistError
-		}
-
 		err = sourceController.ChangeBranch(r.FeatureBranch)
 		if err != nil {
 			return nil, err
@@ -218,7 +216,33 @@ func (r Runner) runSingleRepo(ctx context.Context, repo domain.Repository) (doma
 		}, nil
 	}
 
-	err = sourceController.Push()
+	remoteName := "origin"
+	var prRepo domain.Repository = repo
+	if r.Fork {
+		log.Info("Forking repository")
+
+		prRepo, err = r.VersionController.ForkRepository(ctx, repo, r.ForkOwner)
+		if err != nil {
+			return nil, errors.Wrap(err, "could not fork repository")
+		}
+
+		err = sourceController.AddRemote("fork", prRepo.URL(r.Token))
+		if err != nil {
+			return nil, err
+		}
+		remoteName = "fork"
+	}
+
+	if !r.SkipPullRequest {
+		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 {
+			return nil, domain.BranchExistError
+		}
+	}
+
+	err = sourceController.Push(remoteName)
 	if err != nil {
 		return nil, errors.Wrap(err, "could not push changes")
 	}
@@ -228,7 +252,7 @@ func (r Runner) runSingleRepo(ctx context.Context, repo domain.Repository) (doma
 	}
 
 	log.Info("Change done, creating pull request")
-	pr, err := r.VersionController.CreatePullRequest(ctx, repo, domain.NewPullRequest{
+	pr, err := r.VersionController.CreatePullRequest(ctx, repo, prRepo, domain.NewPullRequest{
 		Title:     r.PullRequestTitle,
 		Body:      r.PullRequestBody,
 		Head:      r.FeatureBranch,
diff --git a/internal/scm/README.md b/internal/scm/README.md
index fe01eb9..472de06 100644
--- a/internal/scm/README.md
+++ b/internal/scm/README.md
@@ -7,13 +7,15 @@ type VersionController interface {
 	// Should get repositories based on the scm configuration
 	GetRepositories(ctx context.Context) ([]domain.Repository, error)
 	// Creates a pull request. The repo parameter will always originate from the same package
-	CreatePullRequest(ctx context.Context, repo domain.Repository, newPR domain.NewPullRequest) (domain.PullRequest, error)
+	CreatePullRequest(ctx context.Context, repo domain.Repository, prRepo domain.Repository, newPR domain.NewPullRequest) (domain.PullRequest, error)
 	// Gets the latest pull requests from repositories based on the scm configuration
 	GetPullRequests(ctx context.Context, branchName string) ([]domain.PullRequest, error)
 	// Merges a pull request, the pr parameter will always originate from the same package
 	MergePullRequest(ctx context.Context, pr domain.PullRequest) error
 	// Close a pull request, the pr parameter will always originate from the same package
 	ClosePullRequest(ctx context.Context, pr domain.PullRequest) error
+	// ForkRepository forks a repository. If newOwner is set, use it, otherwise fork to the current user
+	ForkRepository(ctx context.Context, repo domain.Repository, newOwner string) (domain.Repository, error)
 }
 ```
 
diff --git a/internal/scm/gitea/gitea.go b/internal/scm/gitea/gitea.go
index 6cebc46..8e07548 100644
--- a/internal/scm/gitea/gitea.go
+++ b/internal/scm/gitea/gitea.go
@@ -56,6 +56,8 @@ type Gitea struct {
 	baseURL string
 	token   string
 
+	currentUser *gitea.User
+
 	MergeTypes []domain.MergeType
 }
 
@@ -106,12 +108,14 @@ func (r repository) FullName() string {
 }
 
 type pullRequest struct {
-	ownerName  string
-	repoName   string
-	branchName string
-	index      int64 // The id of the PR
-	webURL     string
-	status     domain.PullRequestStatus
+	ownerName   string
+	repoName    string
+	branchName  string
+	prOwnerName string
+	prRepoName  string
+	index       int64 // The id of the PR
+	webURL      string
+	status      domain.PullRequestStatus
 }
 
 func (pr pullRequest) String() string {
@@ -135,17 +139,11 @@ func (g *Gitea) GetRepositories(ctx context.Context) ([]domain.Repository, error
 
 	repos := make([]domain.Repository, 0, len(allRepos))
 	for _, repo := range allRepos {
-		u, err := url.Parse(repo.CloneURL)
+		convertedRepo, err := convertRepository(repo)
 		if err != nil {
-			return nil, err // TODO: better error
+			return nil, err
 		}
-
-		repos = append(repos, repository{
-			url:           *u,
-			name:          repo.Name,
-			ownerName:     repo.Owner.UserName,
-			defaultBranch: repo.DefaultBranch,
-		})
+		repos = append(repos, convertedRepo)
 	}
 
 	return repos, nil
@@ -247,11 +245,14 @@ func (g *Gitea) getUserRepositories(ctx context.Context, username string) ([]*gi
 }
 
 // CreatePullRequest creates a pull request
-func (g *Gitea) CreatePullRequest(ctx context.Context, repo domain.Repository, newPR domain.NewPullRequest) (domain.PullRequest, error) {
+func (g *Gitea) CreatePullRequest(ctx context.Context, repo domain.Repository, prRepo domain.Repository, newPR domain.NewPullRequest) (domain.PullRequest, error) {
 	r := repo.(repository)
+	prR := prRepo.(repository)
+
+	head := fmt.Sprintf("%s:%s", prR.ownerName, newPR.Head)
 
 	pr, _, err := g.giteaClient(ctx).CreatePullRequest(r.ownerName, r.name, gitea.CreatePullRequestOption{
-		Head:  newPR.Head,
+		Head:  head,
 		Base:  newPR.Base,
 		Title: newPR.Title,
 		Body:  newPR.Body,
@@ -268,11 +269,13 @@ func (g *Gitea) CreatePullRequest(ctx context.Context, repo domain.Repository, n
 	}
 
 	return pullRequest{
-		repoName:   r.name,
-		ownerName:  r.ownerName,
-		branchName: newPR.Head,
-		index:      pr.Index,
-		webURL:     pr.HTMLURL,
+		repoName:    r.name,
+		ownerName:   r.ownerName,
+		branchName:  newPR.Head,
+		prOwnerName: pr.Head.Repository.Owner.UserName,
+		prRepoName:  pr.Head.Repository.Name,
+		index:       pr.Index,
+		webURL:      pr.HTMLURL,
 	}, nil
 }
 
@@ -299,12 +302,14 @@ func (g *Gitea) GetPullRequests(ctx context.Context, branchName string) ([]domai
 		}
 
 		prs = append(prs, pullRequest{
-			repoName:   repo.Name,
-			ownerName:  repo.Owner.UserName,
-			branchName: branchName,
-			status:     status,
-			index:      pr.Index,
-			webURL:     pr.HTMLURL,
+			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,
 		})
 	}
 
@@ -385,7 +390,7 @@ func (g *Gitea) MergePullRequest(ctx context.Context, pullReq domain.PullRequest
 		return errors.Errorf("could not merge %s/%s#%d", pr.ownerName, pr.repoName, pr.index)
 	}
 
-	deleted, _, err := g.giteaClient(ctx).DeleteRepoBranch(pr.ownerName, pr.repoName, pr.branchName)
+	deleted, _, err := g.giteaClient(ctx).DeleteRepoBranch(pr.prOwnerName, pr.prRepoName, pr.branchName)
 	if err != nil {
 		return errors.Wrapf(err, "could not delete branch after merging %s/%s", pr.ownerName, pr.repoName)
 	}
@@ -409,7 +414,7 @@ func (g *Gitea) ClosePullRequest(ctx context.Context, pullReq domain.PullRequest
 		return errors.Wrapf(err, "could not close %s/%s#%d", pr.ownerName, pr.repoName, pr.index)
 	}
 
-	deleted, _, err := g.giteaClient(ctx).DeleteRepoBranch(pr.ownerName, pr.repoName, pr.branchName)
+	deleted, _, err := g.giteaClient(ctx).DeleteRepoBranch(pr.prOwnerName, pr.prRepoName, pr.branchName)
 	if err != nil {
 		return errors.Wrapf(err, "could not delete branch after merging %s/%s", pr.ownerName, pr.repoName)
 	}
@@ -420,3 +425,62 @@ func (g *Gitea) ClosePullRequest(ctx context.Context, pullReq domain.PullRequest
 
 	return nil
 }
+
+// ForkRepository forks a repository. If newOwner is empty, fork on the logged in user
+func (g *Gitea) ForkRepository(ctx context.Context, repo domain.Repository, newOwner string) (domain.Repository, error) {
+	r := repo.(repository)
+
+	forkTo := newOwner
+	if forkTo == "" {
+		user, err := g.getUser(ctx)
+		if err != nil {
+			return nil, err
+		}
+		forkTo = user.UserName
+	}
+
+	existingRepo, _, err := g.giteaClient(ctx).GetRepo(forkTo, r.name)
+	if err == nil { // NB!
+		return convertRepository(existingRepo)
+	}
+
+	forkOptions := gitea.CreateForkOption{}
+	if newOwner != "" {
+		forkOptions.Organization = &newOwner
+	}
+
+	createdRepo, _, err := g.giteaClient(ctx).CreateFork(r.ownerName, r.name, forkOptions)
+	if err != nil {
+		return nil, err
+	}
+
+	return convertRepository(createdRepo)
+}
+
+func (g *Gitea) getUser(ctx context.Context) (*gitea.User, error) {
+	if g.currentUser != nil {
+		return g.currentUser, nil
+	}
+
+	user, _, err := g.giteaClient(ctx).GetMyUserInfo()
+	if err != nil {
+		return nil, err
+	}
+
+	g.currentUser = user
+	return user, nil
+}
+
+func convertRepository(repo *gitea.Repository) (repository, error) {
+	u, err := url.Parse(repo.CloneURL)
+	if err != nil {
+		return repository{}, err
+	}
+
+	return repository{
+		url:           *u,
+		name:          repo.Name,
+		ownerName:     repo.Owner.UserName,
+		defaultBranch: repo.DefaultBranch,
+	}, nil
+}
diff --git a/internal/scm/github/github.go b/internal/scm/github/github.go
index 71f4564..e030fd9 100755
--- a/internal/scm/github/github.go
+++ b/internal/scm/github/github.go
@@ -2,14 +2,15 @@ package github
 
 import (
 	"context"
-	"errors"
 	"fmt"
 	"net/http"
 	"net/url"
 	"sort"
 	"strings"
+	"time"
 
 	"github.com/google/go-github/v35/github"
+	"github.com/pkg/errors"
 	log "github.com/sirupsen/logrus"
 	"golang.org/x/oauth2"
 
@@ -23,6 +24,7 @@ func New(
 	transportMiddleware func(http.RoundTripper) http.RoundTripper,
 	repoListing RepositoryListing,
 	mergeTypes []domain.MergeType,
+	forkMode bool,
 ) (*Github, error) {
 	ctx := context.Background()
 	ts := oauth2.StaticTokenSource(
@@ -45,6 +47,7 @@ func New(
 	return &Github{
 		RepositoryListing: repoListing,
 		MergeTypes:        mergeTypes,
+		Fork:              forkMode,
 		ghClient:          client,
 	}, nil
 }
@@ -53,7 +56,12 @@ func New(
 type Github struct {
 	RepositoryListing
 	MergeTypes []domain.MergeType
-	ghClient   *github.Client
+
+	// This determines if forks will be used when creating a prs.
+	// In this package, it mainly determines which repos are possible to make changes on
+	Fork bool
+
+	ghClient *github.Client
 }
 
 // RepositoryListing contains information about which repositories that should be fetched
@@ -91,12 +99,14 @@ func (r repository) FullName() string {
 }
 
 type pullRequest struct {
-	ownerName  string
-	repoName   string
-	branchName string
-	number     int
-	guiURL     string
-	status     domain.PullRequestStatus
+	ownerName   string
+	repoName    string
+	branchName  string
+	prOwnerName string
+	prRepoName  string
+	number      int
+	guiURL      string
+	status      domain.PullRequestStatus
 }
 
 func (pr pullRequest) String() string {
@@ -133,19 +143,21 @@ func (g Github) GetRepositories(ctx context.Context) ([]domain.Repository, error
 	repos := make([]domain.Repository, 0, len(allRepos))
 	for _, r := range allRepos {
 		permissions := r.GetPermissions()
-		if !r.GetArchived() && !r.GetDisabled() && permissions["pull"] && permissions["push"] {
-			u, err := url.Parse(r.GetCloneURL())
-			if err != nil {
-				return nil, err // TODO: better error
-			}
 
-			repos = append(repos, repository{
-				url:           *u,
-				name:          r.GetName(),
-				ownerName:     r.GetOwner().GetLogin(),
-				defaultBranch: r.GetDefaultBranch(),
-			})
+		if r.GetArchived() || r.GetDisabled() || !permissions["pull"] {
+			continue
+		}
+		// The user needs push permissions or have defined that the pr should be on a fork
+		if !g.Fork && !permissions["push"] {
+			continue
+		}
+
+		newRepo, err := convertRepo(r)
+		if err != nil {
+			return nil, err
 		}
+
+		repos = append(repos, newRepo)
 	}
 
 	return repos, nil
@@ -252,10 +264,11 @@ func (g Github) getRepository(ctx context.Context, repoRef RepositoryReference)
 }
 
 // CreatePullRequest creates a pull request
-func (g Github) CreatePullRequest(ctx context.Context, repo domain.Repository, newPR domain.NewPullRequest) (domain.PullRequest, error) {
+func (g Github) CreatePullRequest(ctx context.Context, repo domain.Repository, prRepo domain.Repository, newPR domain.NewPullRequest) (domain.PullRequest, error) {
 	r := repo.(repository)
+	prR := prRepo.(repository)
 
-	pr, err := g.createPullRequest(ctx, r, newPR)
+	pr, err := g.createPullRequest(ctx, r, prR, newPR)
 	if err != nil {
 		return nil, err
 	}
@@ -264,20 +277,15 @@ func (g Github) CreatePullRequest(ctx context.Context, repo domain.Repository, n
 		return nil, err
 	}
 
-	return pullRequest{
-		ownerName:  pr.GetBase().GetUser().GetLogin(),
-		repoName:   pr.GetBase().GetRepo().GetName(),
-		branchName: pr.GetHead().GetRef(),
-		number:     pr.GetNumber(),
-		guiURL:     pr.GetHTMLURL(),
-	}, nil
+	return convertPullRequest(pr), nil
 }
 
-func (g Github) createPullRequest(ctx context.Context, repo repository, newPR domain.NewPullRequest) (*github.PullRequest, error) {
+func (g Github) createPullRequest(ctx context.Context, repo repository, prRepo repository, newPR domain.NewPullRequest) (*github.PullRequest, error) {
+	head := fmt.Sprintf("%s:%s", prRepo.ownerName, newPR.Head)
 	pr, _, err := g.ghClient.PullRequests.Create(ctx, repo.ownerName, repo.name, &github.NewPullRequest{
 		Title: &newPR.Title,
 		Body:  &newPR.Body,
-		Head:  &newPR.Head,
+		Head:  &head,
 		Base:  &newPR.Base,
 	})
 	if err != nil {
@@ -313,7 +321,7 @@ func (g Github) GetPullRequests(ctx context.Context, branchName string) ([]domai
 		log := log.WithField("repo", fmt.Sprintf("%s/%s", repoOwner, repoName))
 		log.Debug("Fetching latest pull request")
 		prs, _, err := g.ghClient.PullRequests.List(ctx, repoOwner, repoName, &github.PullRequestListOptions{
-			Head:      fmt.Sprintf("%s:%s", repoOwner, branchName),
+			Head:      branchName,
 			State:     "all",
 			Direction: "desc",
 			ListOptions: github.ListOptions{
@@ -328,41 +336,14 @@ func (g Github) GetPullRequests(ctx context.Context, branchName string) ([]domai
 		}
 		pr := prs[0]
 
-		// Determine the status of the pr
-		var status domain.PullRequestStatus
-		if pr.MergedAt != nil {
-			status = domain.PullRequestStatusMerged
-		} else if pr.ClosedAt != nil {
-			status = domain.PullRequestStatusClosed
-		} else {
-			log.Debug("Fetching the combined status of the pull request")
-			combinedStatus, _, err := g.ghClient.Repositories.GetCombinedStatus(ctx, repoOwner, repoName, pr.GetHead().GetSHA(), nil)
-			if err != nil {
-				return nil, err
-			}
-
-			if combinedStatus.GetTotalCount() == 0 {
-				status = domain.PullRequestStatusSuccess
-			} else {
-				switch combinedStatus.GetState() {
-				case "pending":
-					status = domain.PullRequestStatusPending
-				case "success":
-					status = domain.PullRequestStatusSuccess
-				case "failure", "error":
-					status = domain.PullRequestStatusError
-				}
-			}
+		status, err := g.getPrStatus(ctx, pr)
+		if err != nil {
+			return nil, err
 		}
 
-		prStatuses = append(prStatuses, pullRequest{
-			ownerName:  repoOwner,
-			repoName:   repoName,
-			branchName: pr.GetHead().GetRef(),
-			number:     pr.GetNumber(),
-			guiURL:     pr.GetHTMLURL(),
-			status:     status,
-		})
+		localPR := convertPullRequest(pr)
+		localPR.status = status
+		prStatuses = append(prStatuses, localPR)
 	}
 
 	return prStatuses, nil
@@ -391,7 +372,7 @@ func (g Github) MergePullRequest(ctx context.Context, pullReq domain.PullRequest
 		return err
 	}
 
-	_, err = g.ghClient.Git.DeleteRef(ctx, pr.ownerName, pr.repoName, fmt.Sprintf("heads/%s", pr.branchName))
+	_, err = g.ghClient.Git.DeleteRef(ctx, pr.prOwnerName, pr.prRepoName, fmt.Sprintf("heads/%s", pr.branchName))
 	return err
 }
 
@@ -406,10 +387,41 @@ func (g Github) ClosePullRequest(ctx context.Context, pullReq domain.PullRequest
 		return err
 	}
 
-	_, err = g.ghClient.Git.DeleteRef(ctx, pr.ownerName, pr.repoName, fmt.Sprintf("heads/%s", pr.branchName))
+	_, err = g.ghClient.Git.DeleteRef(ctx, pr.prOwnerName, pr.prRepoName, fmt.Sprintf("heads/%s", pr.branchName))
 	return err
 }
 
+// ForkRepository forks a repository. If newOwner is empty, fork on the logged in user
+func (g Github) ForkRepository(ctx context.Context, repo domain.Repository, newOwner string) (domain.Repository, error) {
+	r := repo.(repository)
+
+	createdRepo, _, err := g.ghClient.Repositories.CreateFork(ctx, r.ownerName, r.name, &github.RepositoryCreateForkOptions{
+		Organization: newOwner,
+	})
+	if err != nil {
+		if _, isAccepted := err.(*github.AcceptedError); !isAccepted {
+			return nil, err
+		}
+
+		// Request to fork was accepted, but the repo was not created yet. Poll for the repo to be created
+		var err error
+		var repo *github.Repository
+		for i := 0; i < 10; i++ {
+			repo, _, err = g.ghClient.Repositories.Get(ctx, createdRepo.GetOwner().GetLogin(), createdRepo.GetName())
+			if err != nil {
+				time.Sleep(time.Second * 3)
+				continue
+			}
+			// The fork does now exist
+			return convertRepo(repo)
+		}
+
+		return nil, errors.New("time waiting for fork to complete was exceeded")
+	}
+
+	return convertRepo(createdRepo)
+}
+
 // GetAutocompleteOrganizations gets organizations for autocompletion
 func (g Github) GetAutocompleteOrganizations(ctx context.Context, _ string) ([]string, error) {
 	orgs, _, err := g.ghClient.Organizations.List(ctx, "", nil)
@@ -467,3 +479,60 @@ func (g Github) GetAutocompleteRepositories(ctx context.Context, str string) ([]
 
 	return ret, nil
 }
+
+func convertRepo(r *github.Repository) (repository, error) {
+	u, err := url.Parse(r.GetCloneURL())
+	if err != nil {
+		return repository{}, errors.Wrap(err, "could not parse github clone error")
+	}
+
+	return repository{
+		url:           *u,
+		name:          r.GetName(),
+		ownerName:     r.GetOwner().GetLogin(),
+		defaultBranch: r.GetDefaultBranch(),
+	}, nil
+}
+
+func convertPullRequest(pr *github.PullRequest) pullRequest {
+	return pullRequest{
+		ownerName:   pr.GetBase().GetUser().GetLogin(),
+		repoName:    pr.GetBase().GetRepo().GetName(),
+		branchName:  pr.GetHead().GetRef(),
+		prOwnerName: pr.GetHead().GetUser().GetLogin(),
+		prRepoName:  pr.GetHead().GetRepo().GetName(),
+		number:      pr.GetNumber(),
+		guiURL:      pr.GetHTMLURL(),
+	}
+}
+
+func (g Github) getPrStatus(ctx context.Context, pr *github.PullRequest) (domain.PullRequestStatus, error) {
+	// Determine the status of the pr
+	var status domain.PullRequestStatus
+	if pr.MergedAt != nil {
+		status = domain.PullRequestStatusMerged
+	} else if pr.ClosedAt != nil {
+		status = domain.PullRequestStatusClosed
+	} else {
+		log.Debug("Fetching the combined status of the pull request")
+		combinedStatus, _, err := g.ghClient.Repositories.GetCombinedStatus(ctx, pr.GetBase().GetUser().GetLogin(), pr.GetBase().GetRepo().GetName(), pr.GetHead().GetSHA(), nil)
+		if err != nil {
+			return domain.PullRequestStatusUnknown, err
+		}
+
+		if combinedStatus.GetTotalCount() == 0 {
+			status = domain.PullRequestStatusSuccess
+		} else {
+			switch combinedStatus.GetState() {
+			case "pending":
+				status = domain.PullRequestStatusPending
+			case "success":
+				status = domain.PullRequestStatusSuccess
+			case "failure", "error":
+				status = domain.PullRequestStatusError
+			}
+		}
+	}
+
+	return status, nil
+}
diff --git a/internal/scm/gitlab/gitlab.go b/internal/scm/gitlab/gitlab.go
index c53ff46..307d746 100644
--- a/internal/scm/gitlab/gitlab.go
+++ b/internal/scm/gitlab/gitlab.go
@@ -2,11 +2,13 @@ package gitlab
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"net/http"
 	"net/url"
 	"sort"
 	"strings"
+	"time"
 
 	"github.com/xanzy/go-gitlab"
 
@@ -42,6 +44,9 @@ type Gitlab struct {
 	RepositoryListing
 	Config   Config
 	glClient *gitlab.Client
+
+	// Cached current user
+	currentUser *gitlab.User
 }
 
 // RepositoryListing contains information about which repositories that should be fetched
@@ -99,7 +104,8 @@ func (r repository) FullName() string {
 type pullRequest struct {
 	ownerName  string
 	repoName   string
-	pid        int
+	targetPID  int
+	sourcePID  int
 	branchName string
 	iid        int
 	webURL     string
@@ -127,18 +133,13 @@ func (g *Gitlab) GetRepositories(ctx context.Context) ([]domain.Repository, erro
 
 	repos := make([]domain.Repository, 0, len(allProjects))
 	for _, project := range allProjects {
-		u, err := url.Parse(project.HTTPURLToRepo)
+
+		p, err := convertProject(project)
 		if err != nil {
-			return nil, err // TODO: better error
+			return nil, err
 		}
 
-		repos = append(repos, repository{
-			url:           *u,
-			pid:           project.ID,
-			name:          project.Path,
-			ownerName:     project.Namespace.Path,
-			defaultBranch: project.DefaultBranch,
-		})
+		repos = append(repos, p)
 	}
 
 	return repos, nil
@@ -245,8 +246,9 @@ func (g *Gitlab) getUserProjects(ctx context.Context, username string) ([]*gitla
 }
 
 // CreatePullRequest creates a pull request
-func (g *Gitlab) CreatePullRequest(ctx context.Context, repo domain.Repository, newPR domain.NewPullRequest) (domain.PullRequest, error) {
+func (g *Gitlab) CreatePullRequest(ctx context.Context, repo domain.Repository, prRepo domain.Repository, newPR domain.NewPullRequest) (domain.PullRequest, error) {
 	r := repo.(repository)
+	prR := prRepo.(repository)
 
 	// Convert from usernames to user ids
 	var assigneeIDs []int
@@ -259,11 +261,12 @@ func (g *Gitlab) CreatePullRequest(ctx context.Context, repo domain.Repository,
 	}
 
 	removeSourceBranch := true
-	mr, _, err := g.glClient.MergeRequests.CreateMergeRequest(r.pid, &gitlab.CreateMergeRequestOptions{
+	mr, _, err := g.glClient.MergeRequests.CreateMergeRequest(prR.pid, &gitlab.CreateMergeRequestOptions{
 		Title:              &newPR.Title,
 		Description:        &newPR.Body,
 		SourceBranch:       &newPR.Head,
 		TargetBranch:       &newPR.Base,
+		TargetProjectID:    &r.pid,
 		AssigneeIDs:        assigneeIDs,
 		RemoveSourceBranch: &removeSourceBranch,
 	})
@@ -274,7 +277,8 @@ func (g *Gitlab) CreatePullRequest(ctx context.Context, repo domain.Repository,
 	return pullRequest{
 		repoName:   r.name,
 		ownerName:  r.ownerName,
-		pid:        r.pid,
+		targetPID:  mr.TargetProjectID,
+		sourcePID:  mr.SourceProjectID,
 		branchName: newPR.Head,
 		iid:        mr.IID,
 		webURL:     mr.WebURL,
@@ -318,7 +322,8 @@ func (g *Gitlab) GetPullRequests(ctx context.Context, branchName string) ([]doma
 		prs = append(prs, pullRequest{
 			repoName:   project.Path,
 			ownerName:  project.Namespace.Path,
-			pid:        project.ID,
+			targetPID:  mr.TargetProjectID,
+			sourcePID:  mr.SourceProjectID,
 			branchName: branchName,
 			status:     pullRequestStatus(mr),
 			iid:        mr.IID,
@@ -371,7 +376,7 @@ func (g *Gitlab) MergePullRequest(ctx context.Context, pullReq domain.PullReques
 	pr := pullReq.(pullRequest)
 
 	shouldRemoveSourceBranch := true
-	_, _, err := g.glClient.MergeRequests.AcceptMergeRequest(pr.pid, pr.iid, &gitlab.AcceptMergeRequestOptions{
+	_, _, err := g.glClient.MergeRequests.AcceptMergeRequest(pr.targetPID, pr.iid, &gitlab.AcceptMergeRequestOptions{
 		ShouldRemoveSourceBranch: &shouldRemoveSourceBranch,
 	}, gitlab.WithContext(ctx))
 	if err != nil {
@@ -385,15 +390,94 @@ func (g *Gitlab) MergePullRequest(ctx context.Context, pullReq domain.PullReques
 func (g *Gitlab) ClosePullRequest(ctx context.Context, pullReq domain.PullRequest) error {
 	pr := pullReq.(pullRequest)
 
-	_, err := g.glClient.MergeRequests.DeleteMergeRequest(pr.pid, pr.iid, gitlab.WithContext(ctx))
+	_, err := g.glClient.MergeRequests.DeleteMergeRequest(pr.targetPID, pr.iid, gitlab.WithContext(ctx))
 	if err != nil {
 		return err
 	}
 
-	_, err = g.glClient.Branches.DeleteBranch(pr.pid, pr.branchName, gitlab.WithContext(ctx))
+	_, err = g.glClient.Branches.DeleteBranch(pr.sourcePID, pr.branchName, gitlab.WithContext(ctx))
 	if err != nil {
 		return err
 	}
 
 	return nil
 }
+
+// ForkRepository forks a project
+func (g *Gitlab) ForkRepository(ctx context.Context, repo domain.Repository, newOwner string) (domain.Repository, error) {
+	r := repo.(repository)
+
+	// Get the username of the fork (logged in user if none is set)
+	ownerUsername := newOwner
+	if newOwner == "" {
+		currentUser, err := g.getCurrentUser(ctx)
+		if err != nil {
+			return nil, err
+		}
+		ownerUsername = currentUser.Username
+	}
+
+	// Check if the project already exist
+	project, resp, err := g.glClient.Projects.GetProject(
+		fmt.Sprintf("%s/%s", ownerUsername, r.name),
+		nil,
+		gitlab.WithContext(ctx),
+	)
+	if err == nil { // Already forked, just return it
+		return convertProject(project)
+	} else if resp.StatusCode != http.StatusNotFound { // If the error was that the project does not exist, continue to fork it
+		return nil, err
+	}
+
+	newRepo, _, err := g.glClient.Projects.ForkProject(r.pid, &gitlab.ForkProjectOptions{
+		Namespace: &newOwner,
+	}, gitlab.WithContext(ctx))
+	if err != nil {
+		return nil, err
+	}
+
+	for i := 0; i < 10; i++ {
+		repo, _, err := g.glClient.Projects.GetProject(newRepo.ID, nil, gitlab.WithContext(ctx))
+		if err != nil {
+			return nil, err
+		}
+
+		if repo.ImportStatus == "finished" {
+			return convertProject(newRepo)
+		}
+
+		time.Sleep(time.Second * 3)
+	}
+
+	return nil, errors.New("time waiting for fork to complete was exceeded")
+}
+
+func (g *Gitlab) getCurrentUser(ctx context.Context) (*gitlab.User, error) {
+	if g.currentUser != nil {
+		return g.currentUser, nil
+	}
+
+	user, _, err := g.glClient.Users.CurrentUser(gitlab.WithContext(ctx))
+	if err != nil {
+		return nil, err
+	}
+
+	g.currentUser = user
+
+	return user, nil
+}
+
+func convertProject(project *gitlab.Project) (repository, error) {
+	u, err := url.Parse(project.HTTPURLToRepo)
+	if err != nil {
+		return repository{}, err
+	}
+
+	return repository{
+		url:           *u,
+		pid:           project.ID,
+		name:          project.Path,
+		ownerName:     project.Namespace.Path,
+		defaultBranch: project.DefaultBranch,
+	}, nil
+}


Test patch:
diff --git a/internal/scm/github/github_test.go b/internal/scm/github/github_test.go
index 6629686..8b26aba 100644
--- a/internal/scm/github/github_test.go
+++ b/internal/scm/github/github_test.go
@@ -117,7 +117,7 @@ func Test_GetRepositories(t *testing.T) {
 	{
 		gh, err := github.New("", "", transport.Wrapper, github.RepositoryListing{
 			Organizations: []string{"test-org"},
-		}, []domain.MergeType{domain.MergeTypeMerge})
+		}, []domain.MergeType{domain.MergeTypeMerge}, false)
 		require.NoError(t, err)
 
 		repos, err := gh.GetRepositories(context.Background())
@@ -137,7 +137,7 @@ func Test_GetRepositories(t *testing.T) {
 					Name:      "test1",
 				},
 			},
-		}, []domain.MergeType{domain.MergeTypeMerge})
+		}, []domain.MergeType{domain.MergeTypeMerge}, false)
 		require.NoError(t, err)
 
 		repos, err := gh.GetRepositories(context.Background())
@@ -152,7 +152,7 @@ func Test_GetRepositories(t *testing.T) {
 	{
 		gh, err := github.New("", "", transport.Wrapper, github.RepositoryListing{
 			Users: []string{"test-user"},
-		}, []domain.MergeType{domain.MergeTypeMerge})
+		}, []domain.MergeType{domain.MergeTypeMerge}, false)
 		require.NoError(t, err)
 
 		repos, err := gh.GetRepositories(context.Background())
@@ -174,7 +174,7 @@ func Test_GetRepositories(t *testing.T) {
 					Name:      "test1",
 				},
 			},
-		}, []domain.MergeType{domain.MergeTypeMerge})
+		}, []domain.MergeType{domain.MergeTypeMerge}, false)
 		require.NoError(t, err)
 
 		repos, err := gh.GetRepositories(context.Background())
diff --git a/tests/print_test.go b/tests/print_test.go
index 6b104a9..5b5bdba 100644
--- a/tests/print_test.go
+++ b/tests/print_test.go
@@ -23,9 +23,9 @@ func TestPrint(t *testing.T) {
 	workingDir, err := os.Getwd()
 	assert.NoError(t, err)
 
-	changeRepo := createRepo(t, "test-1", "i like apples")
-	changeRepo2 := createRepo(t, "test-2", "i like my apple")
-	noChangeRepo := createRepo(t, "test-3", "i like oranges")
+	changeRepo := createRepo(t, "owner", "test-1", "i like apples")
+	changeRepo2 := createRepo(t, "owner", "test-2", "i like my apple")
+	noChangeRepo := createRepo(t, "owner", "test-3", "i like oranges")
 	vcMock.AddRepository(changeRepo)
 	vcMock.AddRepository(changeRepo2)
 	vcMock.AddRepository(noChangeRepo)
diff --git a/tests/repo_helper_test.go b/tests/repo_helper_test.go
index 40d697f..96dfadc 100644
--- a/tests/repo_helper_test.go
+++ b/tests/repo_helper_test.go
@@ -18,13 +18,14 @@ import (
 
 const fileName = "test.txt"
 
-func createRepo(t *testing.T, name, dataInFile string) vcmock.Repository {
+func createRepo(t *testing.T, ownerName string, repoName string, dataInFile string) vcmock.Repository {
 	tmpDir, err := createDummyRepo(dataInFile)
 	require.NoError(t, err)
 
 	return vcmock.Repository{
-		Name: name,
-		Path: tmpDir,
+		OwnerName: ownerName,
+		RepoName:  repoName,
+		Path:      tmpDir,
 	}
 }
 
diff --git a/tests/story_test.go b/tests/story_test.go
index da44289..f53a564 100644
--- a/tests/story_test.go
+++ b/tests/story_test.go
@@ -28,9 +28,9 @@ func TestStory(t *testing.T) {
 
 	changerBinaryPath := filepath.ToSlash(filepath.Join(workingDir, changerBinaryPath))
 
-	changeRepo := createRepo(t, "should-change", "i like apples")
-	changeRepo2 := createRepo(t, "should-change-2", "i like my apple")
-	noChangeRepo := createRepo(t, "should-not-change", "i like oranges")
+	changeRepo := createRepo(t, "owner", "should-change", "i like apples")
+	changeRepo2 := createRepo(t, "owner", "should-change-2", "i like my apple")
+	noChangeRepo := createRepo(t, "owner", "should-not-change", "i like oranges")
 	vcMock.AddRepository(changeRepo)
 	vcMock.AddRepository(changeRepo2)
 	vcMock.AddRepository(noChangeRepo)
@@ -64,10 +64,10 @@ func TestStory(t *testing.T) {
 	runOutData, err := ioutil.ReadFile(runOutFile)
 	require.NoError(t, err)
 	assert.Equal(t, `No data was changed:
-  should-not-change
+  owner/should-not-change
 Repositories with a successful run:
-  should-change #1
-  should-change-2 #2
+  owner/should-change #1
+  owner/should-change-2 #2
 `, string(runOutData))
 
 	//
@@ -86,7 +86,7 @@ Repositories with a successful run:
 	// Verify that the output was correct
 	statusOutData, err := ioutil.ReadFile(statusOutFile)
 	require.NoError(t, err)
-	assert.Equal(t, "should-change #1: Pending\nshould-change-2 #2: Pending\n", string(statusOutData))
+	assert.Equal(t, "owner/should-change #1: Pending\nowner/should-change-2 #2: Pending\n", string(statusOutData))
 
 	// One of the created PRs is set to succeeded
 	vcMock.SetPRStatus("should-change", "custom-branch-name", domain.PullRequestStatusSuccess)
@@ -108,7 +108,7 @@ Repositories with a successful run:
 	mergeLogData, err := ioutil.ReadFile(mergeLogFile)
 	require.NoError(t, err)
 	assert.Contains(t, string(mergeLogData), "Merging 1 pull requests")
-	assert.Contains(t, string(mergeLogData), "Merging pr=\"should-change #1\"")
+	assert.Contains(t, string(mergeLogData), "Merging pr=\"owner/should-change #1\"")
 
 	//
 	// After Merge Status
@@ -126,7 +126,7 @@ Repositories with a successful run:
 	// Verify that the output was correct
 	afterMergeStatusOutData, err := ioutil.ReadFile(afterMergeStatusOutFile)
 	require.NoError(t, err)
-	assert.Equal(t, "should-change #1: Merged\nshould-change-2 #2: Pending\n", string(afterMergeStatusOutData))
+	assert.Equal(t, "owner/should-change #1: Merged\nowner/should-change-2 #2: Pending\n", string(afterMergeStatusOutData))
 
 	//
 	// Close
@@ -145,7 +145,7 @@ Repositories with a successful run:
 	closeLogData, err := ioutil.ReadFile(closeLogFile)
 	require.NoError(t, err)
 	assert.Contains(t, string(closeLogData), "Closing 1 pull request")
-	assert.Contains(t, string(closeLogData), "Closing pr=\"should-change-2 #2\"")
+	assert.Contains(t, string(closeLogData), "Closing pr=\"owner/should-change-2 #2\"")
 
 	//
 	// After Close Status
@@ -163,5 +163,5 @@ Repositories with a successful run:
 	// Verify that the output was correct
 	afterCloseStatusOutData, err := ioutil.ReadFile(afterCloseStatusOutFile)
 	require.NoError(t, err)
-	assert.Equal(t, "should-change #1: Merged\nshould-change-2 #2: Closed\n", string(afterCloseStatusOutData))
+	assert.Equal(t, "owner/should-change #1: Merged\nowner/should-change-2 #2: Closed\n", string(afterCloseStatusOutData))
 }
diff --git a/tests/table_test.go b/tests/table_test.go
index 78c4215..9237c7b 100644
--- a/tests/table_test.go
+++ b/tests/table_test.go
@@ -42,7 +42,7 @@ func TestTable(t *testing.T) {
 			name: "simple",
 			vc: &vcmock.VersionController{
 				Repositories: []vcmock.Repository{
-					createRepo(t, "should-change", "i like apples"),
+					createRepo(t, "owner", "should-change", "i like apples"),
 				},
 			},
 			args: []string{
@@ -64,7 +64,7 @@ func TestTable(t *testing.T) {
 				assert.Contains(t, runData.logOut, "Change done, creating pull request")
 
 				assert.Equal(t, `Repositories with a successful run:
-  should-change #1
+  owner/should-change #1
 `, runData.out)
 			},
 		},
@@ -73,7 +73,7 @@ func TestTable(t *testing.T) {
 			name: "with go run",
 			vc: &vcmock.VersionController{
 				Repositories: []vcmock.Repository{
-					createRepo(t, "should-change", "i like apples"),
+					createRepo(t, "owner", "should-change", "i like apples"),
 				},
 			},
 			args: []string{
@@ -94,7 +94,7 @@ func TestTable(t *testing.T) {
 				assert.Contains(t, runData.logOut, "Change done, creating pull request")
 
 				assert.Equal(t, `Repositories with a successful run:
-  should-change #1
+  owner/should-change #1
 `, runData.out)
 			},
 		},
@@ -103,7 +103,7 @@ func TestTable(t *testing.T) {
 			name: "failing base-branch",
 			vc: &vcmock.VersionController{
 				Repositories: []vcmock.Repository{
-					createRepo(t, "should-change", "i like apples"),
+					createRepo(t, "owner", "should-change", "i like apples"),
 				},
 			},
 			args: []string{
@@ -124,7 +124,7 @@ func TestTable(t *testing.T) {
 		{
 			name: "success base-branch",
 			vcCreate: func(t *testing.T) *vcmock.VersionController {
-				repo := createRepo(t, "should-change", "i like apples")
+				repo := createRepo(t, "owner", "should-change", "i like apples")
 				changeBranch(t, repo.Path, "custom-base-branch", true)
 				changeTestFile(t, repo.Path, "i like apple", "test change")
 				changeBranch(t, repo.Path, "master", false)
@@ -158,7 +158,7 @@ func TestTable(t *testing.T) {
 			name: "reviewers",
 			vc: &vcmock.VersionController{
 				Repositories: []vcmock.Repository{
-					createRepo(t, "should-change", "i like apples"),
+					createRepo(t, "owner", "should-change", "i like apples"),
 				},
 			},
 			args: []string{
@@ -181,7 +181,7 @@ func TestTable(t *testing.T) {
 			name: "random reviewers",
 			vc: &vcmock.VersionController{
 				Repositories: []vcmock.Repository{
-					createRepo(t, "should-change", "i like apples"),
+					createRepo(t, "owner", "should-change", "i like apples"),
 				},
 			},
 			args: []string{
@@ -203,7 +203,7 @@ func TestTable(t *testing.T) {
 			name: "dry run",
 			vc: &vcmock.VersionController{
 				Repositories: []vcmock.Repository{
-					createRepo(t, "should-change", "i like apples"),
+					createRepo(t, "owner", "should-change", "i like apples"),
 				},
 			},
 			args: []string{
@@ -226,16 +226,16 @@ func TestTable(t *testing.T) {
 			name: "parallel",
 			vc: &vcmock.VersionController{
 				Repositories: []vcmock.Repository{
-					createRepo(t, "should-change-1", "i like apples"),
-					createRepo(t, "should-change-2", "i like apples"),
-					createRepo(t, "should-change-3", "i like apples"),
-					createRepo(t, "should-change-4", "i like apples"),
-					createRepo(t, "should-change-5", "i like apples"),
-					createRepo(t, "should-change-6", "i like apples"),
-					createRepo(t, "should-change-7", "i like apples"),
-					createRepo(t, "should-change-8", "i like apples"),
-					createRepo(t, "should-change-9", "i like apples"),
-					createRepo(t, "should-change-10", "i like apples"),
+					createRepo(t, "owner", "should-change-1", "i like apples"),
+					createRepo(t, "owner", "should-change-2", "i like apples"),
+					createRepo(t, "owner", "should-change-3", "i like apples"),
+					createRepo(t, "owner", "should-change-4", "i like apples"),
+					createRepo(t, "owner", "should-change-5", "i like apples"),
+					createRepo(t, "owner", "should-change-6", "i like apples"),
+					createRepo(t, "owner", "should-change-7", "i like apples"),
+					createRepo(t, "owner", "should-change-8", "i like apples"),
+					createRepo(t, "owner", "should-change-9", "i like apples"),
+					createRepo(t, "owner", "should-change-10", "i like apples"),
 				},
 			},
 			args: []string{
@@ -256,14 +256,14 @@ func TestTable(t *testing.T) {
 		{
 			name: "existing head branch",
 			vcCreate: func(t *testing.T) *vcmock.VersionController {
-				repo := createRepo(t, "already-existing-branch", "i like apples")
+				repo := createRepo(t, "owner", "already-existing-branch", "i like apples")
 				changeBranch(t, repo.Path, "custom-branch-name", true)
 				changeTestFile(t, repo.Path, "i like apple", "test change")
 				changeBranch(t, repo.Path, "master", false)
 				return &vcmock.VersionController{
 					Repositories: []vcmock.Repository{
 						repo,
-						createRepo(t, "should-change", "i like apples"),
+						createRepo(t, "owner", "should-change", "i like apples"),
 					},
 				}
 			},
@@ -281,9 +281,9 @@ func TestTable(t *testing.T) {
 				assert.Contains(t, runData.logOut, "Cloning and running script")
 
 				assert.Equal(t, `The new branch does already exist:
-  already-existing-branch
+  owner/already-existing-branch
 Repositories with a successful run:
-  should-change #1
+  owner/should-change #1
 `, runData.out)
 			},
 		},
@@ -291,7 +291,7 @@ Repositories with a successful run:
 		{
 			name: "skip-pr",
 			vcCreate: func(t *testing.T) *vcmock.VersionController {
-				repo := createRepo(t, "should-change", "i like apples")
+				repo := createRepo(t, "owner", "should-change", "i like apples")
 
 				// Change branch so that it's not the one we are expected to push to.
 				// If this can be avoided, it would be good.
@@ -321,7 +321,7 @@ Repositories with a successful run:
 				assert.Contains(t, runData.logOut, "Cloning and running script")
 
 				assert.Equal(t, `Repositories with a successful run:
-  should-change
+  owner/should-change
 `, runData.out)
 
 				changeBranch(t, vcMock.Repositories[0].Path, "master", false)
@@ -372,7 +372,7 @@ Repositories with a successful run:
 			name: "debug log",
 			vc: &vcmock.VersionController{
 				Repositories: []vcmock.Repository{
-					createRepo(t, "should-change", "i like apples"),
+					createRepo(t, "owner", "should-change", "i like apples"),
 				},
 			},
 			args: []string{
@@ -395,7 +395,7 @@ Repositories with a successful run:
 				assert.Contains(t, runData.logOut, "Change done, creating pull request")
 
 				assert.Equal(t, `Repositories with a successful run:
-  should-change #1
+  owner/should-change #1
 `, runData.out)
 				assert.Contains(t, runData.logOut, `--- a/test.txt\n+++ b/test.txt\n@@ -1 +1 @@\n-i like apples\n\\ No newline at end of file\n+i like bananas\n\\ No newline at end of file\n`)
 			},
@@ -404,7 +404,7 @@ Repositories with a successful run:
 		{
 			name: "gitignore",
 			vcCreate: func(t *testing.T) *vcmock.VersionController {
-				repo := createRepo(t, "should-change", "i like apples")
+				repo := createRepo(t, "owner", "should-change", "i like apples")
 				addFile(t, repo.Path, ".gitignore", "node_modules", "added .gitignore")
 				return &vcmock.VersionController{
 					Repositories: []vcmock.Repository{
@@ -432,7 +432,7 @@ Repositories with a successful run:
 		{
 			name: "no gitignore",
 			vcCreate: func(t *testing.T) *vcmock.VersionController {
-				repo := createRepo(t, "should-change", "i like apples")
+				repo := createRepo(t, "owner", "should-change", "i like apples")
 				return &vcmock.VersionController{
 					Repositories: []vcmock.Repository{
 						repo,
@@ -455,6 +455,81 @@ Repositories with a successful run:
 				assert.True(t, fileExist(t, vcMock.Repositories[0].Path, "node_modules/react/README.md"))
 			},
 		},
+
+		{
+			name: "fork mode",
+			vc: &vcmock.VersionController{
+				Repositories: []vcmock.Repository{
+					createRepo(t, "owner", "should-change", "i like apples"),
+				},
+			},
+			args: []string{
+				"run",
+				"--author-name", "Test Author",
+				"--author-email", "test@example.com",
+				"-B", "custom-branch-name",
+				"-m", "custom message",
+				"--fork",
+				changerBinaryPath,
+			},
+			verify: func(t *testing.T, vcMock *vcmock.VersionController, runData runData) {
+				require.Len(t, vcMock.PullRequests, 1)
+				assert.Equal(t, "custom-branch-name", vcMock.PullRequests[0].Head)
+				assert.Equal(t, "master", vcMock.PullRequests[0].Base)
+				assert.Equal(t, "custom message", vcMock.PullRequests[0].Title)
+
+				assert.Contains(t, runData.logOut, "Running on 1 repositories")
+				assert.Contains(t, runData.logOut, "Cloning and running script")
+				assert.Contains(t, runData.logOut, "Forking repository")
+				assert.Contains(t, runData.logOut, "Change done, creating pull request")
+
+				assert.Equal(t, `Repositories with a successful run:
+  owner/should-change #1
+`, runData.out)
+
+				assert.False(t, branchExist(t, vcMock.Repositories[0].Path, "custom-branch-name"))
+				changeBranch(t, vcMock.Repositories[0].Path+"-forked-default-owner", "custom-branch-name", false)
+				assert.Equal(t, "i like bananas", readTestFile(t, vcMock.Repositories[0].Path+"-forked-default-owner"))
+			},
+		},
+
+		{
+			name: "fork mode with specified owner",
+			vc: &vcmock.VersionController{
+				Repositories: []vcmock.Repository{
+					createRepo(t, "owner", "should-change", "i like apples"),
+				},
+			},
+			args: []string{
+				"run",
+				"--author-name", "Test Author",
+				"--author-email", "test@example.com",
+				"-B", "custom-branch-name",
+				"-m", "custom message",
+				"--fork",
+				"--fork-owner", "custom-org",
+				changerBinaryPath,
+			},
+			verify: func(t *testing.T, vcMock *vcmock.VersionController, runData runData) {
+				require.Len(t, vcMock.PullRequests, 1)
+				assert.Equal(t, "custom-branch-name", vcMock.PullRequests[0].Head)
+				assert.Equal(t, "master", vcMock.PullRequests[0].Base)
+				assert.Equal(t, "custom message", vcMock.PullRequests[0].Title)
+
+				assert.Contains(t, runData.logOut, "Running on 1 repositories")
+				assert.Contains(t, runData.logOut, "Cloning and running script")
+				assert.Contains(t, runData.logOut, "Forking repository")
+				assert.Contains(t, runData.logOut, "Change done, creating pull request")
+
+				assert.Equal(t, `Repositories with a successful run:
+  owner/should-change #1
+`, runData.out)
+
+				assert.False(t, branchExist(t, vcMock.Repositories[0].Path, "custom-branch-name"))
+				changeBranch(t, vcMock.Repositories[0].Path+"-forked-custom-org", "custom-branch-name", false)
+				assert.Equal(t, "i like bananas", readTestFile(t, vcMock.Repositories[0].Path+"-forked-custom-org"))
+			},
+		},
 	}
 
 	for _, test := range tests {
diff --git a/tests/vcmock/vcmock.go b/tests/vcmock/vcmock.go
index 510dd5b..c048233 100644
--- a/tests/vcmock/vcmock.go
+++ b/tests/vcmock/vcmock.go
@@ -8,6 +8,7 @@ import (
 	"fmt"
 	"path/filepath"
 
+	git "github.com/go-git/go-git/v5"
 	"github.com/lindell/multi-gitter/internal/domain"
 )
 
@@ -28,7 +29,7 @@ func (vc *VersionController) GetRepositories(ctx context.Context) ([]domain.Repo
 }
 
 // CreatePullRequest stores a mock pull request
-func (vc *VersionController) CreatePullRequest(ctx context.Context, repo domain.Repository, newPR domain.NewPullRequest) (domain.PullRequest, error) {
+func (vc *VersionController) CreatePullRequest(ctx context.Context, repo domain.Repository, prRepo domain.Repository, newPR domain.NewPullRequest) (domain.PullRequest, error) {
 	repository := repo.(Repository)
 
 	vc.PRNumber++
@@ -58,7 +59,7 @@ func (vc *VersionController) GetPullRequests(ctx context.Context, branchName str
 func (vc *VersionController) MergePullRequest(ctx context.Context, pr domain.PullRequest) error {
 	pullRequest := pr.(PullRequest)
 	for i := range vc.PullRequests {
-		if vc.PullRequests[i].Repository.Name == pullRequest.Repository.Name {
+		if vc.PullRequests[i].Repository.FullName() == pullRequest.Repository.FullName() {
 			vc.PullRequests[i].PRStatus = domain.PullRequestStatusMerged
 			return nil
 		}
@@ -70,7 +71,7 @@ func (vc *VersionController) MergePullRequest(ctx context.Context, pr domain.Pul
 func (vc *VersionController) ClosePullRequest(ctx context.Context, pr domain.PullRequest) error {
 	pullRequest := pr.(PullRequest)
 	for i := range vc.PullRequests {
-		if vc.PullRequests[i].Repository.Name == pullRequest.Repository.Name {
+		if vc.PullRequests[i].Repository.FullName() == pullRequest.Repository.FullName() {
 			vc.PullRequests[i].PRStatus = domain.PullRequestStatusClosed
 			return nil
 		}
@@ -86,7 +87,7 @@ func (vc *VersionController) AddRepository(repo ...Repository) {
 // SetPRStatus sets the status of a pull request
 func (vc *VersionController) SetPRStatus(repoName string, branchName string, newStatus domain.PullRequestStatus) {
 	for i := range vc.PullRequests {
-		if vc.PullRequests[i].Repository.Name == repoName && vc.PullRequests[i].Head == branchName {
+		if vc.PullRequests[i].Repository.RepoName == repoName && vc.PullRequests[i].Head == branchName {
 			vc.PullRequests[i].PRStatus = newStatus
 		}
 	}
@@ -107,6 +108,30 @@ func (vc *VersionController) GetAutocompleteRepositories(ctx context.Context, st
 	return []string{"static-repo", str}, nil
 }
 
+// ForkRepository forks a repository
+func (vc *VersionController) ForkRepository(ctx context.Context, repo domain.Repository, newOwner string) (domain.Repository, error) {
+	r := repo.(Repository)
+
+	if newOwner == "" {
+		newOwner = "default-owner"
+	}
+
+	newPath := fmt.Sprintf("%s-forked-%s", r.Path, newOwner)
+
+	_, err := git.PlainCloneContext(ctx, newPath, false, &git.CloneOptions{
+		URL: fmt.Sprintf(`file://"%s"`, filepath.ToSlash(r.Path)),
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	return Repository{
+		OwnerName: newOwner,
+		RepoName:  r.RepoName,
+		Path:      newPath,
+	}, nil
+}
+
 // PullRequest is a mock pr
 type PullRequest struct {
 	PRStatus domain.PullRequestStatus
@@ -124,13 +149,14 @@ func (pr PullRequest) Status() domain.PullRequestStatus {
 
 // String return a description of the pr
 func (pr PullRequest) String() string {
-	return fmt.Sprintf("%s #%d", pr.Repository.Name, pr.PRNumber)
+	return fmt.Sprintf("%s #%d", pr.Repository.FullName(), pr.PRNumber)
 }
 
 // Repository is a mock repository
 type Repository struct {
-	Name string
-	Path string
+	OwnerName string
+	RepoName  string
+	Path      string
 }
 
 // URL return the URL (filepath) of the repository on disk
@@ -145,5 +171,10 @@ func (r Repository) DefaultBranch() string {
 
 // FullName returns the name of the mock repo
 func (r Repository) FullName() string {
-	return r.Name
+	return fmt.Sprintf("%s/%s", r.OwnerName, r.RepoName)
+}
+
+// Owner returns the owner of a repo
+func (r Repository) Owner() string {
+	return r.OwnerName
 }