Repository: lindell/multi-gitter
Pull request #178 (2021-09-02 21:44:49)

feat(bitbucketserver): added support for bitbucket server
Tested against local instance of Bitbucket and worked on 200 plus repos.

# What does this change
Provides support for Bitbucket Server and Data Center (On-Prem).

# What issue does it fix
Partially Closes https://github.com/lindell/multi-gitter/issues/99. Only supports on-prem and not cloud.

# Notes for the reviewer
- I can't propagate `ctx` when initializing the Bitbucket client. The client requires context at init.
- Renamed the package `domain` based on Go best practice. Which is name packages after what they provide not what they contain.
- Had to modify the `Repository` interface to allow for username + token auth.

# Checklist
- [ x] Made sure the PR follows the [CONTRIBUTING.md](/CONTRIBUTING.md) guidelines
- [ ] Tests if something new is added


lindell: Thanks for the contribution 💚 I will take a look at the PR during the weekend 🙂 
ryancurrah: I see the tests and lints are failing. I will take a look.

Patch:
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 0e60b37..21163c6 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -12,5 +12,5 @@ _If there is not an existing issue, please make sure we have context on why this
 _Put any questions or notes for the reviewer here._
 
 # Checklist
-- [ ] Made sure the PR follows the [CONTRIBUTING.md](/CONTRIBUTING.md) guidelines
+- [ ] Made sure the PR follows the [CONTRIBUTING.md](CONTRIBUTING.md) guidelines
 - [ ] Tests if something new is added
diff --git a/.gitignore b/.gitignore
index c3e94d2..beba0f3 100755
--- a/.gitignore
+++ b/.gitignore
@@ -23,3 +23,7 @@ dist/
 coverage/
 
 completions/
+
+config.yml
+
+.idea/
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index a44d0a8..85d7a90 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -25,6 +25,12 @@ The only dependency needed for multi-gitter development is `go`. By forking and
 
 All tests can be run with `go test ./...`. These tests will also run on multiple platforms once you push it.
 
+### Linting
+
+This project is linted with golangci-lint, and the [configuration file exist in the root of this project](/.golangci.yml). Linting is never perfect, and if something is not linting correctly, please discuss it in the PR.
+
+Some rules might seem superfluous, like comments on everything that is exported, but to encourage the addition of as many useful comments as possible, this repository imposes the rule that everything that is exported should have comments.
+
 ### Docs
 
 If you want to make changes to the README.md file. Please take a look in `./docs/README.template.md` since the README.md file is generated based on it. If you want to change something like the description of a flag, that change can be made directly in the code and docs will be updated automatically.
diff --git a/README.md b/README.md
index 85769c3..56f3296 100755
--- a/README.md
+++ b/README.md
@@ -84,7 +84,7 @@ To use multi-gitter, a token that is allowed to list repositories and create pul
 
 ### Gitea
 
-In Gitea, access tokens can be generated under Settings -> Applications -> Manage Access Tokens 
+In Gitea, access tokens can be generated under Settings -> Applications -> Manage Access Tokens
 
 ## Config file
 
diff --git a/cmd/cmd-print.go b/cmd/cmd-print.go
index 615e9d7..896bdeb 100755
--- a/cmd/cmd-print.go
+++ b/cmd/cmd-print.go
@@ -48,11 +48,6 @@ func print(cmd *cobra.Command, args []string) error {
 	strOutput, _ := flag.GetString("output")
 	strErrOutput, _ := flag.GetString("error-output")
 
-	token, err := getToken(flag)
-	if err != nil {
-		return err
-	}
-
 	if concurrent < 1 {
 		return errors.New("concurrent runs can't be less than one")
 	}
@@ -97,7 +92,6 @@ func print(cmd *cobra.Command, args []string) error {
 	printer := multigitter.Printer{
 		ScriptPath: executablePath,
 		Arguments:  arguments,
-		Token:      token,
 
 		VersionController: vc,
 
diff --git a/cmd/cmd-run.go b/cmd/cmd-run.go
index e76569e..20d2c6b 100755
--- a/cmd/cmd-run.go
+++ b/cmd/cmd-run.go
@@ -9,7 +9,7 @@ import (
 	"strings"
 	"syscall"
 
-	"github.com/lindell/multi-gitter/internal/domain"
+	"github.com/lindell/multi-gitter/internal/git"
 
 	"github.com/lindell/multi-gitter/internal/multigitter"
 	"github.com/spf13/cobra"
@@ -76,11 +76,6 @@ func run(cmd *cobra.Command, args []string) error {
 	authorEmail, _ := flag.GetString("author-email")
 	strOutput, _ := flag.GetString("output")
 
-	token, err := getToken(flag)
-	if err != nil {
-		return err
-	}
-
 	if concurrent < 1 {
 		return errors.New("concurrent runs can't be less than one")
 	}
@@ -115,12 +110,12 @@ func run(cmd *cobra.Command, args []string) error {
 	}
 
 	// Parse commit author data
-	var commitAuthor *domain.CommitAuthor
+	var commitAuthor *git.CommitAuthor
 	if authorName != "" || authorEmail != "" {
 		if authorName == "" || authorEmail == "" {
 			return errors.New("both author-name and author-email has to be set if the other is set")
 		}
-		commitAuthor = &domain.CommitAuthor{
+		commitAuthor = &git.CommitAuthor{
 			Name:  authorName,
 			Email: authorEmail,
 		}
@@ -157,7 +152,6 @@ func run(cmd *cobra.Command, args []string) error {
 		ScriptPath:    executablePath,
 		Arguments:     arguments,
 		FeatureBranch: branchName,
-		Token:         token,
 
 		Output: output,
 
diff --git a/cmd/other.go b/cmd/other.go
index 4373233..6d435b2 100644
--- a/cmd/other.go
+++ b/cmd/other.go
@@ -4,10 +4,9 @@ import (
 	"io"
 	"os"
 
+	"github.com/lindell/multi-gitter/internal/git"
 	"github.com/pkg/errors"
 	flag "github.com/spf13/pflag"
-
-	"github.com/lindell/multi-gitter/internal/domain"
 )
 
 func outputFlag() *flag.FlagSet {
@@ -32,24 +31,26 @@ func getToken(flag *flag.FlagSet) (string, error) {
 			token = ght
 		} else if ght := os.Getenv("GITEA_TOKEN"); ght != "" {
 			token = ght
+		} else if ght := os.Getenv("BITBUCKET_SERVER_TOKEN"); ght != "" {
+			token = ght
 		}
 	}
 
 	if token == "" {
-		return "", errors.New("either the --token flag or the GITHUB_TOKEN environment variable has to be set")
+		return "", errors.New("either the --token flag or the GITHUB_TOKEN/GITLAB_TOKEN/GITEA_TOKEN/BITBUCKET_SERVER_TOKEN environment variable has to be set")
 	}
 
 	return token, nil
 }
 
-func getMergeTypes(flag *flag.FlagSet) ([]domain.MergeType, error) {
+func getMergeTypes(flag *flag.FlagSet) ([]git.MergeType, error) {
 	mergeTypeStrs, _ := flag.GetStringSlice("merge-type") // Only used for the merge command
 
 	// Convert all defined merge types (if any)
 	var err error
-	mergeTypes := make([]domain.MergeType, len(mergeTypeStrs))
+	mergeTypes := make([]git.MergeType, len(mergeTypeStrs))
 	for i, mt := range mergeTypeStrs {
-		mergeTypes[i], err = domain.ParseMergeType(mt)
+		mergeTypes[i], err = git.ParseMergeType(mt)
 		if err != nil {
 			return nil, err
 		}
diff --git a/cmd/platform.go b/cmd/platform.go
index 50a6a1b..ab6f044 100644
--- a/cmd/platform.go
+++ b/cmd/platform.go
@@ -6,6 +6,7 @@ import (
 
 	"github.com/lindell/multi-gitter/internal/http"
 	"github.com/lindell/multi-gitter/internal/multigitter"
+	"github.com/lindell/multi-gitter/internal/scm/bitbucketserver"
 	"github.com/lindell/multi-gitter/internal/scm/gitea"
 	"github.com/lindell/multi-gitter/internal/scm/github"
 	"github.com/lindell/multi-gitter/internal/scm/gitlab"
@@ -18,7 +19,9 @@ func configurePlatform(cmd *cobra.Command) {
 	flags := cmd.Flags()
 
 	flags.StringP("base-url", "g", "", "Base URL of the (v3) GitHub API, needs to be changed if GitHub enterprise is used. Or the url to a self-hosted GitLab instance.")
-	flags.StringP("token", "T", "", "The GitHub/GitLab personal access token. Can also be set using the GITHUB_TOKEN/GITLAB_TOKEN environment variable.")
+	flags.BoolP("insecure", "", false, "Insecure controls whether a client verifies the server certificate chain and host name. Used only for Bitbucket server.")
+	flags.StringP("username", "u", "", "The Bitbucket server username.")
+	flags.StringP("token", "T", "", "The GitHub/GitLab personal access token. Can also be set using the GITHUB_TOKEN/GITLAB_TOKEN/GITEA_TOKEN/BITBUCKET_SERVER_TOKEN environment variable.")
 
 	flags.StringSliceP("org", "O", nil, "The name of a GitHub organization. All repositories in that organization will be used.")
 	flags.StringSliceP("group", "G", nil, "The name of a GitLab organization. All repositories in that group will be used.")
@@ -27,9 +30,9 @@ func configurePlatform(cmd *cobra.Command) {
 	flags.StringSliceP("project", "P", nil, "The name, including owner of a GitLab project in the format \"ownerName/repoName\".")
 	flags.BoolP("include-subgroups", "", false, "Include GitLab subgroups when using the --group flag.")
 
-	flags.StringP("platform", "p", "github", "The platform that is used. Available values: github, gitlab, gitea.")
+	flags.StringP("platform", "p", "github", "The platform that is used. Available values: github, gitlab, gitea, bitbucket_server.")
 	_ = cmd.RegisterFlagCompletionFunc("platform", func(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
-		return []string{"github", "gitlab", "gitea"}, cobra.ShellCompDirectiveDefault
+		return []string{"github", "gitlab", "gitea", "bitbucket_server"}, cobra.ShellCompDirectiveDefault
 	})
 
 	// Autocompletion for organizations
@@ -118,14 +121,16 @@ func getVersionController(flag *flag.FlagSet, verifyFlags bool) (multigitter.Ver
 
 	platform, _ := flag.GetString("platform")
 	switch platform {
-	default:
-		return nil, fmt.Errorf("unknown platform: %s", platform)
 	case "github":
 		return createGithubClient(flag, verifyFlags)
 	case "gitlab":
 		return createGitlabClient(flag, verifyFlags)
 	case "gitea":
 		return createGiteaClient(flag, verifyFlags)
+	case "bitbucket_server":
+		return createBitbucketServerClient(flag, verifyFlags)
+	default:
+		return nil, fmt.Errorf("unknown platform: %s", platform)
 	}
 }
 
@@ -251,3 +256,44 @@ func createGiteaClient(flag *flag.FlagSet, verifyFlags bool) (multigitter.Versio
 
 	return vc, nil
 }
+
+func createBitbucketServerClient(flag *flag.FlagSet, verifyFlags bool) (multigitter.VersionController, error) {
+	bitbucketServerBaseURL, _ := flag.GetString("base-url")
+	projects, _ := flag.GetStringSlice("org")
+	users, _ := flag.GetStringSlice("user")
+	repos, _ := flag.GetStringSlice("repo")
+	username, _ := flag.GetString("username")
+	insecure, _ := flag.GetBool("insecure")
+
+	if verifyFlags && len(projects) == 0 && len(users) == 0 && len(repos) == 0 {
+		return nil, errors.New("no organization, user or repository set")
+	}
+
+	if bitbucketServerBaseURL == "" {
+		return nil, errors.New("no base-url set for bitbucket server")
+	}
+
+	token, err := getToken(flag)
+	if err != nil {
+		return nil, err
+	}
+
+	repoRefs := make([]bitbucketserver.RepositoryReference, len(repos))
+	for i := range repos {
+		repoRefs[i], err = bitbucketserver.ParseRepositoryReference(repos[i])
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	vc, err := bitbucketserver.New(username, token, bitbucketServerBaseURL, insecure, http.NewLoggingRoundTripper, bitbucketserver.RepositoryListing{
+		Projects:     projects,
+		Users:        users,
+		Repositories: repoRefs,
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	return vc, nil
+}
diff --git a/go.mod b/go.mod
index 6643831..f0f73bc 100644
--- a/go.mod
+++ b/go.mod
@@ -5,8 +5,10 @@ go 1.16
 require (
 	code.gitea.io/sdk/gitea v0.14.0
 	github.com/eiannone/keyboard v0.0.0-20200508000154-caf4b762e807
+	github.com/gfleury/go-bitbucket-v1 v0.0.0-20210119103841-412cc3323e5e
 	github.com/go-git/go-git/v5 v5.4.2
 	github.com/google/go-github/v38 v38.1.0
+	github.com/mitchellh/mapstructure v1.4.1
 	github.com/pkg/errors v0.9.1
 	github.com/sirupsen/logrus v1.8.1
 	github.com/spf13/cobra v1.2.1
@@ -14,5 +16,5 @@ require (
 	github.com/spf13/viper v1.8.1
 	github.com/stretchr/testify v1.7.0
 	github.com/xanzy/go-gitlab v0.50.1
-	golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f
+	golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f // indirect
 )
diff --git a/go.sum b/go.sum
index 560ccfe..57518b8 100644
--- a/go.sum
+++ b/go.sum
@@ -89,6 +89,8 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv
 github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
 github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/gfleury/go-bitbucket-v1 v0.0.0-20210119103841-412cc3323e5e h1:wl/vkmB2Qws0xYZPSgyO4m8MkxXXI/XopMJFvmQe08k=
+github.com/gfleury/go-bitbucket-v1 v0.0.0-20210119103841-412cc3323e5e/go.mod h1:LB3osS9X2JMYmTzcCArHHLrndBAfcVLQAvUddfs+ONs=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
 github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
diff --git a/internal/domain/repo.go b/internal/domain/repo.go
deleted file mode 100755
index b50e230..0000000
--- a/internal/domain/repo.go
+++ /dev/null
@@ -1,9 +0,0 @@
-package domain
-
-// Repository contains all information about a git repository
-type Repository interface {
-	URL(token string) string
-	DefaultBranch() string
-	// Returns the full id of the repository, usually ownerName/repoName
-	FullName() string
-}
diff --git a/internal/git/cmdgit/git.go b/internal/git/cmdgit/git.go
index 1241dcb..68e843d 100644
--- a/internal/git/cmdgit/git.go
+++ b/internal/git/cmdgit/git.go
@@ -7,10 +7,9 @@ import (
 	"regexp"
 	"strings"
 
+	"github.com/lindell/multi-gitter/internal/git"
 	"github.com/pkg/errors"
 	log "github.com/sirupsen/logrus"
-
-	"github.com/lindell/multi-gitter/internal/domain"
 )
 
 // Git is an implementation of git that executes git as commands
@@ -73,7 +72,7 @@ func (g *Git) Changes() (bool, error) {
 }
 
 // Commit and push all changes
-func (g *Git) Commit(commitAuthor *domain.CommitAuthor, commitMessage string) error {
+func (g *Git) Commit(commitAuthor *git.CommitAuthor, commitMessage string) error {
 	cmd := exec.Command("git", "add", ".")
 	_, err := g.run(cmd)
 	if err != nil {
diff --git a/internal/domain/commit.go b/internal/git/commit.go
similarity index 89%
rename from internal/domain/commit.go
rename to internal/git/commit.go
index e406d01..86df793 100644
--- a/internal/domain/commit.go
+++ b/internal/git/commit.go
@@ -1,4 +1,4 @@
-package domain
+package git
 
 // CommitAuthor is the data (name and email) used when a commit is made
 type CommitAuthor struct {
diff --git a/internal/domain/git.go b/internal/git/git.go
similarity index 62%
rename from internal/domain/git.go
rename to internal/git/git.go
index 8af0b5e..3ad0c9e 100644
--- a/internal/domain/git.go
+++ b/internal/git/git.go
@@ -1,7 +1,7 @@
-package domain
+package git
 
-// GitConfig is configration for any git implementation
-type GitConfig struct {
+// Config is configuration for any git implementation
+type Config struct {
 	// Absolute path to the directory
 	Directory string
 	// The fetch depth used when cloning, if set to 0, the entire history will be used
diff --git a/internal/git/gogit/git.go b/internal/git/gogit/git.go
index 58c4f11..a350581 100755
--- a/internal/git/gogit/git.go
+++ b/internal/git/gogit/git.go
@@ -7,7 +7,7 @@ import (
 	"github.com/go-git/go-git/v5/config"
 	"github.com/go-git/go-git/v5/plumbing/format/gitignore"
 	"github.com/go-git/go-git/v5/plumbing/object"
-	"github.com/lindell/multi-gitter/internal/domain"
+	internalgit "github.com/lindell/multi-gitter/internal/git"
 	"github.com/pkg/errors"
 
 	git "github.com/go-git/go-git/v5"
@@ -75,7 +75,7 @@ func (g *Git) Changes() (bool, error) {
 }
 
 // Commit and push all changes
-func (g *Git) Commit(commitAuthor *domain.CommitAuthor, commitMessage string) error {
+func (g *Git) Commit(commitAuthor *internalgit.CommitAuthor, commitMessage string) error {
 	w, err := g.repo.Worktree()
 	if err != nil {
 		return err
diff --git a/internal/domain/pr.go b/internal/git/pullrequest.go
similarity index 99%
rename from internal/domain/pr.go
rename to internal/git/pullrequest.go
index d685c81..c8b88c2 100755
--- a/internal/domain/pr.go
+++ b/internal/git/pullrequest.go
@@ -1,4 +1,4 @@
-package domain
+package git
 
 import (
 	"fmt"
diff --git a/internal/git/repository.go b/internal/git/repository.go
new file mode 100755
index 0000000..6e81020
--- /dev/null
+++ b/internal/git/repository.go
@@ -0,0 +1,11 @@
+package git
+
+// Repository provides all the information needed about a git repository
+type Repository interface {
+	// CloneURL returns the clone address of the repository
+	CloneURL() string
+	// DefaultBranch returns the name of the default branch of the repository
+	DefaultBranch() string
+	// FullName returns the full id of the repository, usually ownerName/repoName
+	FullName() string
+}
diff --git a/internal/domain/errors.go b/internal/gittererrors/gittererrors.go
similarity index 93%
rename from internal/domain/errors.go
rename to internal/gittererrors/gittererrors.go
index 0c44925..985a308 100755
--- a/internal/domain/errors.go
+++ b/internal/gittererrors/gittererrors.go
@@ -1,4 +1,4 @@
-package domain
+package gittererrors
 
 // Error exist to create constant errors
 type Error string
diff --git a/internal/multigitter/close.go b/internal/multigitter/close.go
index d5a8a5d..e19598e 100644
--- a/internal/multigitter/close.go
+++ b/internal/multigitter/close.go
@@ -3,7 +3,7 @@ package multigitter
 import (
 	"context"
 
-	"github.com/lindell/multi-gitter/internal/domain"
+	"github.com/lindell/multi-gitter/internal/git"
 	log "github.com/sirupsen/logrus"
 )
 
@@ -21,9 +21,9 @@ func (s Closer) Close(ctx context.Context) error {
 		return err
 	}
 
-	openPRs := make([]domain.PullRequest, 0, len(prs))
+	openPRs := make([]git.PullRequest, 0, len(prs))
 	for _, pr := range prs {
-		if pr.Status() != domain.PullRequestStatusClosed && pr.Status() != domain.PullRequestStatusMerged {
+		if pr.Status() != git.PullRequestStatusClosed && pr.Status() != git.PullRequestStatusMerged {
 			openPRs = append(openPRs, pr)
 		}
 	}
diff --git a/internal/multigitter/merge.go b/internal/multigitter/merge.go
index 90092bc..f3d50c9 100644
--- a/internal/multigitter/merge.go
+++ b/internal/multigitter/merge.go
@@ -3,9 +3,8 @@ package multigitter
 import (
 	"context"
 
+	"github.com/lindell/multi-gitter/internal/git"
 	log "github.com/sirupsen/logrus"
-
-	"github.com/lindell/multi-gitter/internal/domain"
 )
 
 // Merger merges pull requests in an organization
@@ -22,9 +21,9 @@ func (s Merger) Merge(ctx context.Context) error {
 		return err
 	}
 
-	successPrs := make([]domain.PullRequest, 0, len(prs))
+	successPrs := make([]git.PullRequest, 0, len(prs))
 	for _, pr := range prs {
-		if pr.Status() == domain.PullRequestStatusSuccess {
+		if pr.Status() == git.PullRequestStatusSuccess {
 			successPrs = append(successPrs, pr)
 		}
 	}
diff --git a/internal/multigitter/print.go b/internal/multigitter/print.go
index f4abc73..0472323 100755
--- a/internal/multigitter/print.go
+++ b/internal/multigitter/print.go
@@ -8,9 +8,9 @@ import (
 	"os"
 	"os/exec"
 
+	"github.com/lindell/multi-gitter/internal/git"
 	log "github.com/sirupsen/logrus"
 
-	"github.com/lindell/multi-gitter/internal/domain"
 	"github.com/lindell/multi-gitter/internal/multigitter/repocounter"
 )
 
@@ -20,7 +20,6 @@ type Printer struct {
 
 	ScriptPath string // Must be absolute path
 	Arguments  []string
-	Token      string
 
 	Stdout io.Writer
 	Stderr io.Writer
@@ -63,7 +62,7 @@ func (r Printer) Print(ctx context.Context) error {
 	return nil
 }
 
-func (r Printer) runSingleRepo(ctx context.Context, repo domain.Repository) error {
+func (r Printer) runSingleRepo(ctx context.Context, repo git.Repository) error {
 	if ctx.Err() != nil {
 		return errAborted
 	}
@@ -79,7 +78,7 @@ func (r Printer) runSingleRepo(ctx context.Context, repo domain.Repository) erro
 
 	sourceController := r.CreateGit(tmpDir)
 
-	err = sourceController.Clone(repo.URL(r.Token), repo.DefaultBranch())
+	err = sourceController.Clone(repo.CloneURL(), repo.DefaultBranch())
 	if err != nil {
 		return err
 	}
diff --git a/internal/multigitter/repocounter/counter.go b/internal/multigitter/repocounter/counter.go
index 7255d03..17ac081 100644
--- a/internal/multigitter/repocounter/counter.go
+++ b/internal/multigitter/repocounter/counter.go
@@ -5,27 +5,27 @@ import (
 	"strings"
 	"sync"
 
-	"github.com/lindell/multi-gitter/internal/domain"
+	"github.com/lindell/multi-gitter/internal/git"
 	"github.com/lindell/multi-gitter/internal/multigitter/terminal"
 )
 
 // Counter keeps track of succeeded and failed repositories
 type Counter struct {
-	successPullRequests []domain.PullRequest
-	successRepositories []domain.Repository
-	errorRepositories   map[string][]domain.Repository
+	successPullRequests []git.PullRequest
+	successRepositories []git.Repository
+	errorRepositories   map[string][]git.Repository
 	lock                sync.RWMutex
 }
 
 // NewCounter create a new repo counter
 func NewCounter() *Counter {
 	return &Counter{
-		errorRepositories: map[string][]domain.Repository{},
+		errorRepositories: map[string][]git.Repository{},
 	}
 }
 
 // AddError add a failing repository together with the error that caused it
-func (r *Counter) AddError(err error, repo domain.Repository) {
+func (r *Counter) AddError(err error, repo git.Repository) {
 	defer r.lock.Unlock()
 	r.lock.Lock()
 
@@ -34,7 +34,7 @@ func (r *Counter) AddError(err error, repo domain.Repository) {
 }
 
 // AddSuccessRepositories adds a repository that succeeded
-func (r *Counter) AddSuccessRepositories(repo domain.Repository) {
+func (r *Counter) AddSuccessRepositories(repo git.Repository) {
 	defer r.lock.Unlock()
 	r.lock.Lock()
 
@@ -42,7 +42,7 @@ func (r *Counter) AddSuccessRepositories(repo domain.Repository) {
 }
 
 // AddSuccessPullRequest adds a pullrequest that succeeded
-func (r *Counter) AddSuccessPullRequest(repo domain.PullRequest) {
+func (r *Counter) AddSuccessPullRequest(repo git.PullRequest) {
 	defer r.lock.Unlock()
 	r.lock.Lock()
 
diff --git a/internal/multigitter/run.go b/internal/multigitter/run.go
index fb61184..71904b3 100755
--- a/internal/multigitter/run.go
+++ b/internal/multigitter/run.go
@@ -12,10 +12,11 @@ import (
 	"syscall"
 
 	"github.com/eiannone/keyboard"
+	"github.com/lindell/multi-gitter/internal/git"
+	"github.com/lindell/multi-gitter/internal/gittererrors"
 	"github.com/pkg/errors"
 	log "github.com/sirupsen/logrus"
 
-	"github.com/lindell/multi-gitter/internal/domain"
 	"github.com/lindell/multi-gitter/internal/multigitter/logger"
 	"github.com/lindell/multi-gitter/internal/multigitter/repocounter"
 	"github.com/lindell/multi-gitter/internal/multigitter/terminal"
@@ -23,12 +24,12 @@ import (
 
 // VersionController fetches repositories
 type VersionController interface {
-	GetRepositories(ctx context.Context) ([]domain.Repository, 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)
+	GetRepositories(ctx context.Context) ([]git.Repository, error)
+	CreatePullRequest(ctx context.Context, repo git.Repository, prRepo git.Repository, newPR git.NewPullRequest) (git.PullRequest, error)
+	GetPullRequests(ctx context.Context, branchName string) ([]git.PullRequest, error)
+	MergePullRequest(ctx context.Context, pr git.PullRequest) error
+	ClosePullRequest(ctx context.Context, pr git.PullRequest) error
+	ForkRepository(ctx context.Context, repo git.Repository, newOwner string) (git.Repository, error)
 }
 
 // Runner contains fields to be able to do the run
@@ -38,7 +39,6 @@ type Runner struct {
 	ScriptPath    string // Must be absolute path
 	Arguments     []string
 	FeatureBranch string
-	Token         string
 
 	Output io.Writer
 
@@ -48,7 +48,7 @@ type Runner struct {
 	Reviewers        []string
 	MaxReviewers     int // If set to zero, all reviewers will be used
 	DryRun           bool
-	CommitAuthor     *domain.CommitAuthor
+	CommitAuthor     *git.CommitAuthor
 	BaseBranch       string // The base branch of the PR, use default branch if not set
 
 	Concurrent      int
@@ -66,11 +66,11 @@ var errAborted = errors.New("run was never started because of aborted execution"
 var errRejected = errors.New("changes were not included since they were manually rejected")
 
 type dryRunPullRequest struct {
-	status     domain.PullRequestStatus
-	Repository domain.Repository
+	status     git.PullRequestStatus
+	Repository git.Repository
 }
 
-func (pr dryRunPullRequest) Status() domain.PullRequestStatus {
+func (pr dryRunPullRequest) Status() git.PullRequestStatus {
 	return pr.status
 }
 
@@ -157,7 +157,7 @@ func getReviewers(reviewers []string, maxReviewers int) []string {
 	return reviewers[0:maxReviewers]
 }
 
-func (r *Runner) runSingleRepo(ctx context.Context, repo domain.Repository) (domain.PullRequest, error) {
+func (r *Runner) runSingleRepo(ctx context.Context, repo git.Repository) (git.PullRequest, error) {
 	if ctx.Err() != nil {
 		return nil, errAborted
 	}
@@ -178,7 +178,7 @@ func (r *Runner) runSingleRepo(ctx context.Context, repo domain.Repository) (dom
 		baseBranch = repo.DefaultBranch()
 	}
 
-	err = sourceController.Clone(repo.URL(r.Token), baseBranch)
+	err = sourceController.Clone(repo.CloneURL(), baseBranch)
 	if err != nil {
 		return nil, err
 	}
@@ -213,7 +213,7 @@ func (r *Runner) runSingleRepo(ctx context.Context, repo domain.Repository) (dom
 	if changed, err := sourceController.Changes(); err != nil {
 		return nil, err
 	} else if !changed {
-		return nil, domain.NoChangeError
+		return nil, gittererrors.NoChangeError
 	}
 
 	err = sourceController.Commit(r.CommitAuthor, r.CommitMessage)
@@ -236,7 +236,7 @@ func (r *Runner) runSingleRepo(ctx context.Context, repo domain.Repository) (dom
 	}
 
 	remoteName := "origin"
-	var prRepo domain.Repository = repo
+	var prRepo = repo
 	if r.Fork {
 		log.Info("Forking repository")
 
@@ -245,7 +245,7 @@ func (r *Runner) runSingleRepo(ctx context.Context, repo domain.Repository) (dom
 			return nil, errors.Wrap(err, "could not fork repository")
 		}
 
-		err = sourceController.AddRemote("fork", prRepo.URL(r.Token))
+		err = sourceController.AddRemote("fork", prRepo.CloneURL())
 		if err != nil {
 			return nil, err
 		}
@@ -257,7 +257,7 @@ func (r *Runner) runSingleRepo(ctx context.Context, repo domain.Repository) (dom
 		if err != nil {
 			return nil, errors.Wrap(err, "could not verify if branch already exist")
 		} else if featureBranchExist {
-			return nil, domain.BranchExistError
+			return nil, gittererrors.BranchExistError
 		}
 	}
 
@@ -272,7 +272,7 @@ func (r *Runner) runSingleRepo(ctx context.Context, repo domain.Repository) (dom
 	}
 
 	log.Info("Creating pull request")
-	pr, err := r.VersionController.CreatePullRequest(ctx, repo, prRepo, domain.NewPullRequest{
+	pr, err := r.VersionController.CreatePullRequest(ctx, repo, prRepo, git.NewPullRequest{
 		Title:     r.PullRequestTitle,
 		Body:      r.PullRequestBody,
 		Head:      r.FeatureBranch,
@@ -288,7 +288,7 @@ func (r *Runner) runSingleRepo(ctx context.Context, repo domain.Repository) (dom
 
 var interactiveInfo = `(V)iew changes. (A)ccept or (R)eject`
 
-func (r *Runner) interactive(dir string, repo domain.Repository) error {
+func (r *Runner) interactive(dir string, repo git.Repository) error {
 	fmt.Printf("Changes were made to %s\n", terminal.Bold(repo.FullName()))
 	fmt.Println(interactiveInfo)
 	for {
diff --git a/internal/multigitter/shared.go b/internal/multigitter/shared.go
index 30b1c05..8add55c 100644
--- a/internal/multigitter/shared.go
+++ b/internal/multigitter/shared.go
@@ -4,7 +4,7 @@ import (
 	"fmt"
 	"syscall"
 
-	"github.com/lindell/multi-gitter/internal/domain"
+	"github.com/lindell/multi-gitter/internal/git"
 	"github.com/pkg/errors"
 )
 
@@ -27,7 +27,7 @@ type Git interface {
 	Clone(url string, baseName string) error
 	ChangeBranch(branchName string) error
 	Changes() (bool, error)
-	Commit(commitAuthor *domain.CommitAuthor, commitMessage string) error
+	Commit(commitAuthor *git.CommitAuthor, commitMessage string) error
 	BranchExist(remoteName, branchName string) (bool, error)
 	Push(remoteName string) error
 	AddRemote(name, url string) error
diff --git a/internal/scm/bitbucketserver/bitbucket_server.go b/internal/scm/bitbucketserver/bitbucket_server.go
new file mode 100644
index 0000000..fae5f9d
--- /dev/null
+++ b/internal/scm/bitbucketserver/bitbucket_server.go
@@ -0,0 +1,521 @@
+package bitbucketserver
+
+import (
+	"bytes"
+	"context"
+	"crypto/tls"
+	"encoding/base64"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+	"net/url"
+	"path"
+	"sort"
+	"strings"
+
+	bitbucketv1 "github.com/gfleury/go-bitbucket-v1"
+	"github.com/lindell/multi-gitter/internal/git"
+	"github.com/mitchellh/mapstructure"
+	"github.com/pkg/errors"
+)
+
+const (
+	cloneType     = "http"
+	stateMerged   = "MERGED"
+	stateDeclined = "DECLINED"
+)
+
+// New create a new BitbucketServer client
+func New(username, token, baseURL string, insecure bool, transportMiddleware func(http.RoundTripper) http.RoundTripper, repoListing RepositoryListing) (*BitbucketServer, error) {
+	if strings.TrimSpace(token) == "" {
+		return nil, errors.New("token is empty")
+	}
+
+	if strings.TrimSpace(baseURL) == "" {
+		return nil, errors.New("base url is empty")
+	}
+
+	bitbucketBaseURL, err := url.Parse(baseURL)
+	if err != nil {
+		return nil, err
+	}
+
+	if !strings.HasSuffix(bitbucketBaseURL.Path, "/rest") {
+		bitbucketBaseURL.Path = path.Join(bitbucketBaseURL.Path, "/rest")
+	}
+
+	bitbucketServer := &BitbucketServer{}
+	bitbucketServer.RepositoryListing = repoListing
+	bitbucketServer.baseURL = bitbucketBaseURL
+	bitbucketServer.username = username
+	bitbucketServer.token = token
+	bitbucketServer.httpClient = &http.Client{
+		Transport: transportMiddleware(&http.Transport{
+			TLSClientConfig: &tls.Config{InsecureSkipVerify: insecure}, // nolint: gosec
+		}),
+	}
+	bitbucketServer.config = bitbucketv1.NewConfiguration(bitbucketBaseURL.String(), func(config *bitbucketv1.Configuration) {
+		config.AddDefaultHeader("Authorization", fmt.Sprintf("Bearer %s", token))
+		config.HTTPClient = &http.Client{
+			Transport: transportMiddleware(&http.Transport{
+				TLSClientConfig: &tls.Config{InsecureSkipVerify: insecure}, // nolint: gosec
+			}),
+		}
+	})
+
+	return bitbucketServer, nil
+}
+
+func newClient(ctx context.Context, config *bitbucketv1.Configuration) *bitbucketv1.APIClient {
+	return bitbucketv1.NewAPIClient(
+		ctx,
+		config,
+	)
+}
+
+// BitbucketServer is a SCM instance of Bitbucket Server the on-prem version of Bitbucket
+type BitbucketServer struct {
+	RepositoryListing
+	baseURL         *url.URL
+	username, token string
+	config          *bitbucketv1.Configuration
+	httpClient      *http.Client
+}
+
+// RepositoryListing contains information about which repositories that should be fetched
+type RepositoryListing struct {
+	Projects     []string
+	Users        []string
+	Repositories []RepositoryReference
+}
+
+// RepositoryReference contains information to be able to reference a repository
+type RepositoryReference struct {
+	ProjectKey string
+	Name       string
+}
+
+// ParseRepositoryReference parses a GiteaRepository reference from the format "projectKey/repoName"
+func ParseRepositoryReference(val string) (RepositoryReference, error) {
+	split := strings.Split(val, "/")
+	if len(split) != 2 {
+		return RepositoryReference{}, fmt.Errorf("could not parse repository reference: %s", val)
+	}
+	return RepositoryReference{
+		ProjectKey: split[0],
+		Name:       split[1],
+	}, nil
+}
+
+// String returns the string representation of a repo reference
+func (rr RepositoryReference) String() string {
+	return fmt.Sprintf("%s/%s", rr.ProjectKey, rr.Name)
+}
+
+// GetRepositories Should get repositories based on the scm configuration
+func (b *BitbucketServer) GetRepositories(ctx context.Context) ([]git.Repository, error) {
+	client := newClient(ctx, b.config)
+
+	bitbucketRepositories, err := b.getRepositories(client)
+	if err != nil {
+		return nil, err
+	}
+
+	repositories := make([]git.Repository, 0, len(bitbucketRepositories))
+
+	// Get default branches and create repo interfaces
+	for _, bitbucketRepository := range bitbucketRepositories {
+		response, getDefaultBranchErr := client.DefaultApi.GetDefaultBranch(bitbucketRepository.Project.Key, bitbucketRepository.Slug)
+		if getDefaultBranchErr != nil {
+			return nil, getDefaultBranchErr
+		}
+
+		var defaultBranch bitbucketv1.Branch
+		err = mapstructure.Decode(response.Values, &defaultBranch)
+		if err != nil {
+			return nil, err
+		}
+
+		repo, repoErr := convertRepository(bitbucketRepository, defaultBranch, b.username, b.token)
+		if repoErr != nil {
+			return nil, repoErr
+		}
+
+		repositories = append(repositories, *repo)
+	}
+
+	return repositories, nil
+}
+
+func (b *BitbucketServer) getRepositories(client *bitbucketv1.APIClient) ([]*bitbucketv1.Repository, error) {
+	var bitbucketRepositories []*bitbucketv1.Repository
+
+	for _, project := range b.Projects {
+		repos, err := b.getProjectRepositories(client, project)
+		if err != nil {
+			return nil, err
+		}
+
+		bitbucketRepositories = append(bitbucketRepositories, repos...)
+	}
+
+	for _, user := range b.Users {
+		repos, err := b.getProjectRepositories(client, user)
+		if err != nil {
+			return nil, err
+		}
+
+		bitbucketRepositories = append(bitbucketRepositories, repos...)
+	}
+
+	for _, repositoryRef := range b.Repositories {
+		repo, err := b.getRepository(client, repositoryRef.ProjectKey, repositoryRef.Name)
+		if err != nil {
+			return nil, err
+		}
+
+		bitbucketRepositories = append(bitbucketRepositories, repo)
+	}
+
+	// Remove duplicate repos
+	repositoryMap := make(map[int]*bitbucketv1.Repository, len(bitbucketRepositories))
+	for _, bitbucketRepository := range bitbucketRepositories {
+		repositoryMap[bitbucketRepository.ID] = bitbucketRepository
+	}
+	bitbucketRepositories = make([]*bitbucketv1.Repository, 0, len(repositoryMap))
+	for _, repo := range repositoryMap {
+		bitbucketRepositories = append(bitbucketRepositories, repo)
+	}
+	sort.Slice(bitbucketRepositories, func(i, j int) bool {
+		return bitbucketRepositories[i].ID < bitbucketRepositories[j].ID
+	})
+
+	return bitbucketRepositories, nil
+}
+
+func (b *BitbucketServer) getRepository(client *bitbucketv1.APIClient, projectKey, repositorySlug string) (*bitbucketv1.Repository, error) {
+	response, err := client.DefaultApi.GetRepository(projectKey, repositorySlug)
+	if err != nil {
+		return nil, err
+	}
+
+	var bitbucketRepository bitbucketv1.Repository
+	err = mapstructure.Decode(response.Values, &bitbucketRepository)
+	if err != nil {
+		return nil, err
+	}
+
+	return &bitbucketRepository, nil
+}
+
+func (b *BitbucketServer) getProjectRepositories(client *bitbucketv1.APIClient, projectKey string) ([]*bitbucketv1.Repository, error) {
+	params := map[string]interface{}{"start": 0, "limit": 25}
+
+	var repositories []*bitbucketv1.Repository
+	for {
+		response, err := client.DefaultApi.GetRepositoriesWithOptions(projectKey, params)
+		if err != nil {
+			return nil, err
+		}
+
+		var pager bitbucketRepositoryPager
+		err = mapstructure.Decode(response.Values, &pager)
+		if err != nil {
+			return nil, err
+		}
+
+		for _, repo := range pager.Values {
+			r := repo
+			repositories = append(repositories, &r)
+		}
+
+		if pager.IsLastPage {
+			break
+		}
+
+		params["start"] = pager.NextPageStart
+	}
+
+	return repositories, nil
+}
+
+// CreatePullRequest Creates a pull request. The repo parameter will always originate from the same package
+func (b *BitbucketServer) CreatePullRequest(ctx context.Context, repo git.Repository, prRepo git.Repository, newPR git.NewPullRequest) (git.PullRequest, error) {
+	r := repo.(repository)
+	prR := prRepo.(repository)
+
+	client := newClient(ctx, b.config)
+
+	var usersWithMetadata []bitbucketv1.UserWithMetadata
+	for _, reviewer := range newPR.Reviewers {
+		response, err := client.DefaultApi.GetUser(reviewer)
+		if err != nil {
+			return nil, err
+		}
+
+		var userWithLinks bitbucketv1.UserWithLinks
+		err = mapstructure.Decode(response.Values, &userWithLinks)
+		if err != nil {
+			return nil, err
+		}
+
+		usersWithMetadata = append(usersWithMetadata, bitbucketv1.UserWithMetadata{User: userWithLinks})
+	}
+
+	response, err := client.DefaultApi.CreatePullRequest(r.project, r.name, bitbucketv1.PullRequest{
+		Title:       newPR.Title,
+		Description: newPR.Body,
+		Reviewers:   usersWithMetadata,
+		FromRef: bitbucketv1.PullRequestRef{
+			ID: fmt.Sprintf("refs/heads/%s", newPR.Head),
+			Repository: bitbucketv1.Repository{
+				Slug: prR.name,
+				Project: &bitbucketv1.Project{
+					Key: prR.project,
+				},
+			},
+		},
+		ToRef: bitbucketv1.PullRequestRef{
+			ID: fmt.Sprintf("refs/heads/%s", newPR.Base),
+			Repository: bitbucketv1.Repository{
+				Slug: r.name,
+				Project: &bitbucketv1.Project{
+					Key: r.project,
+				},
+			},
+		},
+	})
+	if err != nil {
+		return nil, fmt.Errorf("unable to create pull request for repository %s: %s", r.name, err)
+	}
+
+	pullRequestResp, err := bitbucketv1.GetPullRequestResponse(response)
+	if err != nil {
+		return nil, fmt.Errorf("unable to create pull request for repository %s: %s", r.name, err)
+	}
+
+	return newPullRequest(pullRequestResp), nil
+}
+
+// GetPullRequests Gets the latest pull requests from repositories based on the scm configuration
+func (b *BitbucketServer) GetPullRequests(ctx context.Context, branchName string) ([]git.PullRequest, error) {
+	client := newClient(ctx, b.config)
+
+	repositories, err := b.getRepositories(client)
+	if err != nil {
+		return nil, err
+	}
+
+	var prs []git.PullRequest
+	for _, repo := range repositories {
+		pr, getPullRequestErr := b.getPullRequest(client, branchName, repo)
+		if getPullRequestErr != nil {
+			return nil, getPullRequestErr
+		}
+		if pr == nil {
+			continue
+		}
+
+		status, pullRequestStatusErr := b.pullRequestStatus(client, repo, pr)
+		if pullRequestStatusErr != nil {
+			return nil, pullRequestStatusErr
+		}
+
+		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,
+		})
+	}
+
+	return prs, nil
+}
+
+func (b *BitbucketServer) pullRequestStatus(client *bitbucketv1.APIClient, repo *bitbucketv1.Repository, pr *bitbucketv1.PullRequest) (git.PullRequestStatus, error) {
+	switch pr.State {
+	case stateMerged:
+		return git.PullRequestStatusMerged, nil
+	case stateDeclined:
+		return git.PullRequestStatusClosed, nil
+	}
+
+	response, err := client.DefaultApi.CanMerge(repo.Project.Key, repo.Slug, int64(pr.ID))
+	if err != nil {
+		return git.PullRequestStatusUnknown, err
+	}
+
+	var merge bitbucketv1.MergeGetResponse
+	err = mapstructure.Decode(response.Values, &merge)
+	if err != nil {
+		return git.PullRequestStatusUnknown, err
+	}
+
+	if !merge.CanMerge {
+		return git.PullRequestStatusPending, nil
+	}
+
+	if merge.Conflicted {
+		return git.PullRequestStatusError, nil
+	}
+
+	return git.PullRequestStatusSuccess, nil
+}
+
+func (b *BitbucketServer) getPullRequest(client *bitbucketv1.APIClient, branchName string, repo *bitbucketv1.Repository) (*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)
+		if err != nil {
+			return nil, err
+		}
+
+		var pager bitbucketPullRequestPager
+		err = mapstructure.Decode(response.Values, &pager)
+		if err != nil {
+			return nil, err
+		}
+
+		pullRequests = append(pullRequests, pager.Values...)
+
+		if pager.IsLastPage {
+			break
+		}
+
+		params["start"] = pager.NextPageStart
+	}
+
+	for _, pr := range pullRequests {
+		if pr.FromRef.DisplayID == branchName {
+			return &pr, nil
+		}
+	}
+
+	return nil, nil
+}
+
+// MergePullRequest Merges a pull request, the pr parameter will always originate from the same package
+func (b *BitbucketServer) MergePullRequest(ctx context.Context, pr git.PullRequest) error {
+	bitbucketPR := pr.(pullRequest)
+
+	client := newClient(ctx, b.config)
+
+	response, err := client.DefaultApi.GetPullRequest(bitbucketPR.project, bitbucketPR.repoName, bitbucketPR.number)
+	if err != nil {
+		if strings.Contains(err.Error(), "com.atlassian.bitbucket.pull.NoSuchPullRequestException") {
+			return nil
+		}
+		return err
+	}
+
+	pullRequestResponse, err := bitbucketv1.GetPullRequestResponse(response)
+	if err != nil {
+		return err
+	}
+
+	if !pullRequestResponse.Open {
+		return nil
+	}
+
+	mergeMap := make(map[string]interface{})
+	mergeMap["version"] = pullRequestResponse.Version
+
+	_, err = client.DefaultApi.Merge(bitbucketPR.project, bitbucketPR.repoName, bitbucketPR.number, mergeMap, nil, []string{"application/json"})
+	if err != nil {
+		return err
+	}
+
+	return b.deleteBranch(ctx, bitbucketPR)
+}
+
+// ClosePullRequest Close a pull request, the pr parameter will always originate from the same package
+func (b *BitbucketServer) ClosePullRequest(ctx context.Context, pr git.PullRequest) error {
+	bitbucketPR := pr.(pullRequest)
+
+	client := newClient(ctx, b.config)
+
+	_, err := client.DefaultApi.DeleteWithVersion(bitbucketPR.project, bitbucketPR.repoName, int64(bitbucketPR.number), int64(bitbucketPR.version))
+	if err != nil {
+		return err
+	}
+
+	return b.deleteBranch(ctx, bitbucketPR)
+}
+
+func (b *BitbucketServer) deleteBranch(ctx context.Context, pr pullRequest) error {
+	urlPath := *b.baseURL
+	urlPath.Path = path.Join(urlPath.Path, "branch-utils/1.0/projects", pr.project, "repos", pr.repoName, "branches")
+
+	body := bitbucketDeleteBranch{Name: path.Join("refs", "heads", pr.branchName), DryRun: false}
+	bodyBytes, err := json.Marshal(&body)
+	if err != nil {
+		return err
+	}
+
+	request, err := http.NewRequestWithContext(ctx, "DELETE", urlPath.String(), bytes.NewReader(bodyBytes))
+	if err != nil {
+		return err
+	}
+
+	request.Header.Add("Content-Type", "application/json")
+	request.Header.Add(
+		"Authorization",
+		"Basic "+base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", b.username, b.token))),
+	)
+
+	response, err := b.httpClient.Do(request) //nolint:bodyclose
+	if err != nil {
+		return err
+	}
+	defer func(Body io.ReadCloser) {
+		_ = Body.Close()
+	}(response.Body)
+
+	if response.StatusCode >= 400 {
+		buf := new(bytes.Buffer)
+		_, readFromErr := buf.ReadFrom(response.Body)
+		if readFromErr != nil {
+			return readFromErr
+		}
+
+		return errors.Errorf("unable to delete branch: status code %d: %s", response.StatusCode, buf.String())
+	}
+
+	return nil
+}
+
+// ForkRepository forks a repository. If newOwner is set, use it, otherwise fork to the current user
+func (b *BitbucketServer) ForkRepository(_ context.Context, _ git.Repository, _ string) (git.Repository, error) {
+	return nil, errors.New("forking not implemented for bitbucket server")
+}
+
+type bitbucketRepositoryPager struct {
+	Size          int                      `json:"size"`
+	Limit         int                      `json:"limit"`
+	Start         int                      `json:"start"`
+	NextPageStart int                      `json:"nextPageStart"`
+	IsLastPage    bool                     `json:"isLastPage"`
+	Values        []bitbucketv1.Repository `json:"values"`
+}
+
+type bitbucketPullRequestPager struct {
+	Size          int                       `json:"size"`
+	Limit         int                       `json:"limit"`
+	Start         int                       `json:"start"`
+	NextPageStart int                       `json:"nextPageStart"`
+	IsLastPage    bool                      `json:"isLastPage"`
+	Values        []bitbucketv1.PullRequest `json:"values"`
+}
+
+type bitbucketDeleteBranch struct {
+	Name   string `json:"name"`
+	DryRun bool   `json:"dryRun"`
+}
diff --git a/internal/scm/bitbucketserver/pullrequest.go b/internal/scm/bitbucketserver/pullrequest.go
new file mode 100644
index 0000000..773e667
--- /dev/null
+++ b/internal/scm/bitbucketserver/pullrequest.go
@@ -0,0 +1,44 @@
+package bitbucketserver
+
+import (
+	"fmt"
+
+	bitbucketv1 "github.com/gfleury/go-bitbucket-v1"
+	"github.com/lindell/multi-gitter/internal/git"
+)
+
+func newPullRequest(pr bitbucketv1.PullRequest) pullRequest {
+	return pullRequest{
+		project:    pr.ToRef.Repository.Project.Key,
+		repoName:   pr.ToRef.Repository.Slug,
+		branchName: pr.FromRef.DisplayID,
+		prProject:  pr.FromRef.Repository.Project.Key,
+		prRepoName: pr.FromRef.Repository.Slug,
+		number:     pr.ID,
+		guiURL:     pr.Links.Self[0].Href,
+	}
+}
+
+type pullRequest struct {
+	project    string
+	repoName   string
+	branchName string
+	prProject  string
+	prRepoName string
+	number     int
+	version    int32
+	guiURL     string
+	status     git.PullRequestStatus
+}
+
+func (pr pullRequest) String() string {
+	return fmt.Sprintf("%s/%s #%d", pr.project, pr.repoName, pr.number)
+}
+
+func (pr pullRequest) Status() git.PullRequestStatus {
+	return pr.status
+}
+
+func (pr pullRequest) URL() string {
+	return pr.guiURL
+}
diff --git a/internal/scm/bitbucketserver/repository.go b/internal/scm/bitbucketserver/repository.go
new file mode 100644
index 0000000..029f925
--- /dev/null
+++ b/internal/scm/bitbucketserver/repository.go
@@ -0,0 +1,67 @@
+package bitbucketserver
+
+import (
+	"net/url"
+	"strings"
+
+	bitbucketv1 "github.com/gfleury/go-bitbucket-v1"
+	"github.com/pkg/errors"
+)
+
+func convertRepository(bitbucketRepository *bitbucketv1.Repository, defaultBranch bitbucketv1.Branch, username, token string) (*repository, error) {
+	var cloneURL *url.URL
+	var err error
+	for _, clone := range bitbucketRepository.Links.Clone {
+		if strings.EqualFold(clone.Name, cloneType) {
+			cloneURL, err = url.Parse(clone.Href)
+			if err != nil {
+				return nil, err
+			}
+
+			break
+		}
+	}
+
+	if cloneURL == nil {
+		return nil, errors.Errorf("unable to find clone url for repostory %s using clone type %s", bitbucketRepository.Name, cloneType)
+	}
+
+	repo := repository{
+		name:          bitbucketRepository.Slug,
+		project:       bitbucketRepository.Project.Key,
+		defaultBranch: defaultBranch.DisplayID,
+		username:      username,
+		token:         token,
+		cloneURL:      cloneURL,
+	}
+
+	return &repo, nil
+}
+
+// repository contains information about a bitbucket repository
+type repository struct {
+	name          string
+	project       string
+	defaultBranch string
+	username      string
+	token         string
+	cloneURL      *url.URL
+}
+
+func (r repository) CloneURL() string {
+	cloneURL := r.cloneURL
+
+	if r.username != "" && r.token != "" {
+		cloneURL.User = url.UserPassword(r.username, r.token)
+	}
+
+	return cloneURL.String()
+}
+
+func (r repository) DefaultBranch() string {
+	return r.defaultBranch
+}
+
+func (r repository) FullName() string {
+	return r.project + "/" + r.name
+}
diff --git a/internal/scm/gitea/gitea.go b/internal/scm/gitea/gitea.go
index 8e07548..ffd933f 100644
--- a/internal/scm/gitea/gitea.go
+++ b/internal/scm/gitea/gitea.go
@@ -4,19 +4,18 @@ import (
 	"context"
 	"fmt"
 	"net/http"
-	"net/url"
 	"sort"
 	"strings"
 
 	"code.gitea.io/sdk/gitea"
+	"github.com/lindell/multi-gitter/internal/git"
 	"github.com/pkg/errors"
 
-	"github.com/lindell/multi-gitter/internal/domain"
 	internalHTTP "github.com/lindell/multi-gitter/internal/http"
 )
 
 // New create a new Gitea client
-func New(token, baseURL string, repoListing RepositoryListing, mergeTypes []domain.MergeType) (*Gitea, error) {
+func New(token, baseURL string, repoListing RepositoryListing, mergeTypes []git.MergeType) (*Gitea, error) {
 	gitea := &Gitea{
 		RepositoryListing: repoListing,
 
@@ -58,7 +57,7 @@ type Gitea struct {
 
 	currentUser *gitea.User
 
-	MergeTypes []domain.MergeType
+	MergeTypes []git.MergeType
 }
 
 // RepositoryListing contains information about which repositories that should be fetched
@@ -86,60 +85,16 @@ func ParseRepositoryReference(val string) (RepositoryReference, error) {
 	}, nil
 }
 
-type repository struct {
-	url           url.URL
-	name          string
-	ownerName     string
-	defaultBranch string
-}
-
-func (r repository) URL(token string) string {
-	// Set the token as https://oauth2:TOKEN@url
-	r.url.User = url.UserPassword("oauth2", token)
-	return r.url.String()
-}
-
-func (r repository) DefaultBranch() string {
-	return r.defaultBranch
-}
-
-func (r repository) FullName() string {
-	return fmt.Sprintf("%s/%s", r.ownerName, r.name)
-}
-
-type pullRequest struct {
-	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 {
-	return fmt.Sprintf("%s/%s #%d", pr.ownerName, pr.repoName, pr.index)
-}
-
-func (pr pullRequest) Status() domain.PullRequestStatus {
-	return pr.status
-}
-
-func (pr pullRequest) URL() string {
-	return pr.webURL
-}
-
 // GetRepositories fetches repositories from all sources (groups/user/specific repo)
-func (g *Gitea) GetRepositories(ctx context.Context) ([]domain.Repository, error) {
+func (g *Gitea) GetRepositories(ctx context.Context) ([]git.Repository, error) {
 	allRepos, err := g.getRepositories(ctx)
 	if err != nil {
 		return nil, err
 	}
 
-	repos := make([]domain.Repository, 0, len(allRepos))
+	repos := make([]git.Repository, 0, len(allRepos))
 	for _, repo := range allRepos {
-		convertedRepo, err := convertRepository(repo)
+		convertedRepo, err := convertRepository(repo, g.token)
 		if err != nil {
 			return nil, err
 		}
@@ -245,7 +200,7 @@ 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, prRepo domain.Repository, newPR domain.NewPullRequest) (domain.PullRequest, error) {
+func (g *Gitea) CreatePullRequest(ctx context.Context, repo git.Repository, prRepo git.Repository, newPR git.NewPullRequest) (git.PullRequest, error) {
 	r := repo.(repository)
 	prR := prRepo.(repository)
 
@@ -280,13 +235,13 @@ func (g *Gitea) CreatePullRequest(ctx context.Context, repo domain.Repository, p
 }
 
 // GetPullRequests gets all pull requests of with a specific branch
-func (g *Gitea) GetPullRequests(ctx context.Context, branchName string) ([]domain.PullRequest, error) {
+func (g *Gitea) GetPullRequests(ctx context.Context, branchName string) ([]git.PullRequest, error) {
 	repos, err := g.getRepositories(ctx)
 	if err != nil {
 		return nil, err
 	}
 
-	prs := []domain.PullRequest{}
+	prs := []git.PullRequest{}
 	for _, repo := range repos {
 		pr, err := g.getPullRequest(ctx, branchName, repo)
 		if err != nil {
@@ -334,38 +289,38 @@ 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) (domain.PullRequestStatus, error) {
+func (g *Gitea) pullRequestStatus(ctx context.Context, repo *gitea.Repository, pr *gitea.PullRequest) (git.PullRequestStatus, error) {
 	if pr.Merged != nil {
-		return domain.PullRequestStatusMerged, nil
+		return git.PullRequestStatusMerged, nil
 	}
 
 	if pr.State == gitea.StateClosed {
-		return domain.PullRequestStatusClosed, nil
+		return git.PullRequestStatusClosed, nil
 	}
 
 	status, _, err := g.giteaClient(ctx).GetCombinedStatus(repo.Owner.UserName, repo.Name, pr.Head.Sha)
 	if err != nil {
-		return domain.PullRequestStatusUnknown, err
+		return git.PullRequestStatusUnknown, err
 	}
 
 	if len(status.Statuses) == 0 {
-		return domain.PullRequestStatusSuccess, nil
+		return git.PullRequestStatusSuccess, nil
 	}
 
 	switch status.State {
 	case gitea.StatusPending:
-		return domain.PullRequestStatusPending, nil
+		return git.PullRequestStatusPending, nil
 	case gitea.StatusSuccess:
-		return domain.PullRequestStatusSuccess, nil
+		return git.PullRequestStatusSuccess, nil
 	case gitea.StatusError, gitea.StatusFailure:
-		return domain.PullRequestStatusError, nil
+		return git.PullRequestStatusError, nil
 	}
 
-	return domain.PullRequestStatusUnknown, nil
+	return git.PullRequestStatusUnknown, nil
 }
 
 // MergePullRequest merges a pull request
-func (g *Gitea) MergePullRequest(ctx context.Context, pullReq domain.PullRequest) error {
+func (g *Gitea) MergePullRequest(ctx context.Context, pullReq git.PullRequest) error {
 	pr := pullReq.(pullRequest)
 
 	repo, _, err := g.giteaClient(ctx).GetRepo(pr.ownerName, pr.repoName)
@@ -374,7 +329,7 @@ func (g *Gitea) MergePullRequest(ctx context.Context, pullReq domain.PullRequest
 	}
 
 	// Filter out all merge types to only the allowed ones, but keep the order of the ones left
-	mergeTypes := domain.MergeTypeIntersection(g.MergeTypes, repoMergeTypes(repo))
+	mergeTypes := git.MergeTypeIntersection(g.MergeTypes, repoMergeTypes(repo))
 	if len(mergeTypes) == 0 {
 		return errors.New("none of the configured merge types was permitted")
 	}
@@ -403,7 +358,7 @@ func (g *Gitea) MergePullRequest(ctx context.Context, pullReq domain.PullRequest
 }
 
 // ClosePullRequest closes a pull request
-func (g *Gitea) ClosePullRequest(ctx context.Context, pullReq domain.PullRequest) error {
+func (g *Gitea) ClosePullRequest(ctx context.Context, pullReq git.PullRequest) error {
 	pr := pullReq.(pullRequest)
 
 	state := gitea.StateClosed
@@ -426,8 +381,8 @@ 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) {
+// ForkRepository forks a GiteaRepository. If newOwner is empty, fork on the logged in user
+func (g *Gitea) ForkRepository(ctx context.Context, repo git.Repository, newOwner string) (git.Repository, error) {
 	r := repo.(repository)
 
 	forkTo := newOwner
@@ -441,7 +396,7 @@ func (g *Gitea) ForkRepository(ctx context.Context, repo domain.Repository, newO
 
 	existingRepo, _, err := g.giteaClient(ctx).GetRepo(forkTo, r.name)
 	if err == nil { // NB!
-		return convertRepository(existingRepo)
+		return convertRepository(existingRepo, g.token)
 	}
 
 	forkOptions := gitea.CreateForkOption{}
@@ -454,7 +409,7 @@ func (g *Gitea) ForkRepository(ctx context.Context, repo domain.Repository, newO
 		return nil, err
 	}
 
-	return convertRepository(createdRepo)
+	return convertRepository(createdRepo, g.token)
 }
 
 func (g *Gitea) getUser(ctx context.Context) (*gitea.User, error) {
@@ -470,17 +425,3 @@ func (g *Gitea) getUser(ctx context.Context) (*gitea.User, error) {
 	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/gitea/pullrequest.go b/internal/scm/gitea/pullrequest.go
new file mode 100644
index 0000000..7d99ab8
--- /dev/null
+++ b/internal/scm/gitea/pullrequest.go
@@ -0,0 +1,30 @@
+package gitea
+
+import (
+	"fmt"
+
+	"github.com/lindell/multi-gitter/internal/git"
+)
+
+type pullRequest struct {
+	ownerName   string
+	repoName    string
+	branchName  string
+	prOwnerName string
+	prRepoName  string
+	index       int64 // The id of the PR
+	webURL      string
+	status      git.PullRequestStatus
+}
+
+func (pr pullRequest) String() string {
+	return fmt.Sprintf("%s/%s #%d", pr.ownerName, pr.repoName, pr.index)
+}
+
+func (pr pullRequest) Status() git.PullRequestStatus {
+	return pr.status
+}
+
+func (pr pullRequest) URL() string {
+	return pr.webURL
+}
diff --git a/internal/scm/gitea/repository.go b/internal/scm/gitea/repository.go
new file mode 100644
index 0000000..23f596b
--- /dev/null
+++ b/internal/scm/gitea/repository.go
@@ -0,0 +1,45 @@
+package gitea
+
+import (
+	"fmt"
+	"net/url"
+
+	"code.gitea.io/sdk/gitea"
+)
+
+func convertRepository(repo *gitea.Repository, token string) (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,
+		token:         token,
+	}, nil
+}
+
+type repository struct {
+	url           url.URL
+	name          string
+	ownerName     string
+	defaultBranch string
+	token         string
+}
+
+func (r repository) CloneURL() string {
+	// Set the token as https://oauth2:TOKEN@url
+	r.url.User = url.UserPassword("oauth2", r.token)
+	return r.url.String()
+}
+
+func (r repository) DefaultBranch() string {
+	return r.defaultBranch
+}
+
+func (r repository) FullName() string {
+	return fmt.Sprintf("%s/%s", r.ownerName, r.name)
+}
diff --git a/internal/scm/gitea/util.go b/internal/scm/gitea/util.go
index dd82a52..ca66fdb 100644
--- a/internal/scm/gitea/util.go
+++ b/internal/scm/gitea/util.go
@@ -2,27 +2,27 @@ package gitea
 
 import (
 	"code.gitea.io/sdk/gitea"
-	"github.com/lindell/multi-gitter/internal/domain"
+	"github.com/lindell/multi-gitter/internal/git"
 )
 
 // maps merge types to what they are called in the gitea api
-var mergeTypeGiteaName = map[domain.MergeType]gitea.MergeStyle{
-	domain.MergeTypeMerge:  gitea.MergeStyleMerge,
-	domain.MergeTypeRebase: gitea.MergeStyleRebase,
-	domain.MergeTypeSquash: gitea.MergeStyleSquash,
+var mergeTypeGiteaName = map[git.MergeType]gitea.MergeStyle{
+	git.MergeTypeMerge:  gitea.MergeStyleMerge,
+	git.MergeTypeRebase: gitea.MergeStyleRebase,
+	git.MergeTypeSquash: gitea.MergeStyleSquash,
 }
 
 // repoMergeTypes returns a list of all allowed merge types
-func repoMergeTypes(repo *gitea.Repository) []domain.MergeType {
-	ret := []domain.MergeType{}
+func repoMergeTypes(repo *gitea.Repository) []git.MergeType {
+	ret := []git.MergeType{}
 	if repo.AllowMerge {
-		ret = append(ret, domain.MergeTypeMerge)
+		ret = append(ret, git.MergeTypeMerge)
 	}
 	if repo.AllowMerge {
-		ret = append(ret, domain.MergeTypeRebase)
+		ret = append(ret, git.MergeTypeRebase)
 	}
 	if repo.AllowSquash {
-		ret = append(ret, domain.MergeTypeSquash)
+		ret = append(ret, git.MergeTypeSquash)
 	}
 	return ret
 }
diff --git a/internal/scm/github/github.go b/internal/scm/github/github.go
index e48150f..de026e7 100755
--- a/internal/scm/github/github.go
+++ b/internal/scm/github/github.go
@@ -4,17 +4,15 @@ import (
 	"context"
 	"fmt"
 	"net/http"
-	"net/url"
 	"sort"
 	"strings"
 	"time"
 
 	"github.com/google/go-github/v38/github"
+	"github.com/lindell/multi-gitter/internal/git"
 	"github.com/pkg/errors"
 	log "github.com/sirupsen/logrus"
 	"golang.org/x/oauth2"
-
-	"github.com/lindell/multi-gitter/internal/domain"
 )
 
 // New create a new Github client
@@ -23,7 +21,7 @@ func New(
 	baseURL string,
 	transportMiddleware func(http.RoundTripper) http.RoundTripper,
 	repoListing RepositoryListing,
-	mergeTypes []domain.MergeType,
+	mergeTypes []git.MergeType,
 	forkMode bool,
 ) (*Github, error) {
 	ctx := context.Background()
@@ -47,6 +45,7 @@ func New(
 	return &Github{
 		RepositoryListing: repoListing,
 		MergeTypes:        mergeTypes,
+		token:             token,
 		Fork:              forkMode,
 		ghClient:          client,
 	}, nil
@@ -55,7 +54,8 @@ func New(
 // Github contain github configuration
 type Github struct {
 	RepositoryListing
-	MergeTypes []domain.MergeType
+	MergeTypes []git.MergeType
+	token      string
 
 	// 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
@@ -82,50 +82,6 @@ func (rr RepositoryReference) String() string {
 	return fmt.Sprintf("%s/%s", rr.OwnerName, rr.Name)
 }
 
-type repository struct {
-	url           url.URL
-	name          string
-	ownerName     string
-	defaultBranch string
-}
-
-func (r repository) URL(token string) string {
-	// Set the token as https://TOKEN@url
-	r.url.User = url.User(token)
-	return r.url.String()
-}
-
-func (r repository) DefaultBranch() string {
-	return r.defaultBranch
-}
-
-func (r repository) FullName() string {
-	return fmt.Sprintf("%s/%s", r.ownerName, r.name)
-}
-
-type pullRequest struct {
-	ownerName   string
-	repoName    string
-	branchName  string
-	prOwnerName string
-	prRepoName  string
-	number      int
-	guiURL      string
-	status      domain.PullRequestStatus
-}
-
-func (pr pullRequest) String() string {
-	return fmt.Sprintf("%s/%s #%d", pr.ownerName, pr.repoName, pr.number)
-}
-
-func (pr pullRequest) Status() domain.PullRequestStatus {
-	return pr.status
-}
-
-func (pr pullRequest) URL() string {
-	return pr.guiURL
-}
-
 // ParseRepositoryReference parses a repository reference from the format "ownerName/repoName"
 func ParseRepositoryReference(val string) (RepositoryReference, error) {
 	split := strings.Split(val, "/")
@@ -139,13 +95,13 @@ func ParseRepositoryReference(val string) (RepositoryReference, error) {
 }
 
 // GetRepositories fetches repositories from all sources (orgs/user/specific repo)
-func (g Github) GetRepositories(ctx context.Context) ([]domain.Repository, error) {
+func (g Github) GetRepositories(ctx context.Context) ([]git.Repository, error) {
 	allRepos, err := g.getRepositories(ctx)
 	if err != nil {
 		return nil, err
 	}
 
-	repos := make([]domain.Repository, 0, len(allRepos))
+	repos := make([]git.Repository, 0, len(allRepos))
 	for _, r := range allRepos {
 		permissions := r.GetPermissions()
 
@@ -157,7 +113,7 @@ func (g Github) GetRepositories(ctx context.Context) ([]domain.Repository, error
 			continue
 		}
 
-		newRepo, err := convertRepo(r)
+		newRepo, err := convertRepo(r, g.token)
 		if err != nil {
 			return nil, err
 		}
@@ -269,7 +225,7 @@ func (g Github) getRepository(ctx context.Context, repoRef RepositoryReference)
 }
 
 // CreatePullRequest creates a pull request
-func (g Github) CreatePullRequest(ctx context.Context, repo domain.Repository, prRepo domain.Repository, newPR domain.NewPullRequest) (domain.PullRequest, error) {
+func (g Github) CreatePullRequest(ctx context.Context, repo git.Repository, prRepo git.Repository, newPR git.NewPullRequest) (git.PullRequest, error) {
 	r := repo.(repository)
 	prR := prRepo.(repository)
 
@@ -285,7 +241,7 @@ func (g Github) CreatePullRequest(ctx context.Context, repo domain.Repository, p
 	return convertPullRequest(pr), nil
 }
 
-func (g Github) createPullRequest(ctx context.Context, repo repository, prRepo repository, newPR domain.NewPullRequest) (*github.PullRequest, error) {
+func (g Github) createPullRequest(ctx context.Context, repo repository, prRepo repository, newPR git.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,
@@ -300,7 +256,7 @@ func (g Github) createPullRequest(ctx context.Context, repo repository, prRepo r
 	return pr, nil
 }
 
-func (g Github) addReviewers(ctx context.Context, repo repository, newPR domain.NewPullRequest, createdPR *github.PullRequest) error {
+func (g Github) addReviewers(ctx context.Context, repo repository, newPR git.NewPullRequest, createdPR *github.PullRequest) error {
 	if len(newPR.Reviewers) == 0 {
 		return nil
 	}
@@ -311,7 +267,7 @@ func (g Github) addReviewers(ctx context.Context, repo repository, newPR domain.
 }
 
 // GetPullRequests gets all pull requests of with a specific branch
-func (g Github) GetPullRequests(ctx context.Context, branchName string) ([]domain.PullRequest, error) {
+func (g Github) GetPullRequests(ctx context.Context, branchName string) ([]git.PullRequest, error) {
 	// TODO: If this is implemented with the GitHub v4 graphql api, it would be much faster
 
 	repos, err := g.getRepositories(ctx)
@@ -319,7 +275,7 @@ func (g Github) GetPullRequests(ctx context.Context, branchName string) ([]domai
 		return nil, err
 	}
 
-	prStatuses := []domain.PullRequest{}
+	prStatuses := []git.PullRequest{}
 	for _, r := range repos {
 		repoOwner := r.GetOwner().GetLogin()
 		repoName := r.GetName()
@@ -355,7 +311,7 @@ func (g Github) GetPullRequests(ctx context.Context, branchName string) ([]domai
 }
 
 // MergePullRequest merges a pull request
-func (g Github) MergePullRequest(ctx context.Context, pullReq domain.PullRequest) error {
+func (g Github) MergePullRequest(ctx context.Context, pullReq git.PullRequest) error {
 	pr := pullReq.(pullRequest)
 
 	// We need to fetch the repo again since no AllowXMerge is present in listings of repositories
@@ -365,7 +321,7 @@ func (g Github) MergePullRequest(ctx context.Context, pullReq domain.PullRequest
 	}
 
 	// Filter out all merge types to only the allowed ones, but keep the order of the ones left
-	mergeTypes := domain.MergeTypeIntersection(g.MergeTypes, repoMergeTypes(repo))
+	mergeTypes := git.MergeTypeIntersection(g.MergeTypes, repoMergeTypes(repo))
 	if len(mergeTypes) == 0 {
 		return errors.New("none of the configured merge types was permitted")
 	}
@@ -382,7 +338,7 @@ func (g Github) MergePullRequest(ctx context.Context, pullReq domain.PullRequest
 }
 
 // ClosePullRequest closes a pull request
-func (g Github) ClosePullRequest(ctx context.Context, pullReq domain.PullRequest) error {
+func (g Github) ClosePullRequest(ctx context.Context, pullReq git.PullRequest) error {
 	pr := pullReq.(pullRequest)
 
 	_, _, err := g.ghClient.PullRequests.Edit(ctx, pr.ownerName, pr.repoName, pr.number, &github.PullRequest{
@@ -397,7 +353,7 @@ func (g Github) ClosePullRequest(ctx context.Context, pullReq domain.PullRequest
 }
 
 // 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) {
+func (g Github) ForkRepository(ctx context.Context, repo git.Repository, newOwner string) (git.Repository, error) {
 	r := repo.(repository)
 
 	createdRepo, _, err := g.ghClient.Repositories.CreateFork(ctx, r.ownerName, r.name, &github.RepositoryCreateForkOptions{
@@ -418,13 +374,13 @@ func (g Github) ForkRepository(ctx context.Context, repo domain.Repository, newO
 				continue
 			}
 			// The fork does now exist
-			return convertRepo(repo)
+			return convertRepo(repo, g.token)
 		}
 
 		return nil, errors.New("time waiting for fork to complete was exceeded")
 	}
 
-	return convertRepo(createdRepo)
+	return convertRepo(createdRepo, g.token)
 }
 
 // GetAutocompleteOrganizations gets organizations for autocompletion
@@ -485,56 +441,30 @@ 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) {
+func (g Github) getPrStatus(ctx context.Context, pr *github.PullRequest) (git.PullRequestStatus, error) {
 	// Determine the status of the pr
-	var status domain.PullRequestStatus
+	var status git.PullRequestStatus
 	if pr.MergedAt != nil {
-		status = domain.PullRequestStatusMerged
+		status = git.PullRequestStatusMerged
 	} else if pr.ClosedAt != nil {
-		status = domain.PullRequestStatusClosed
+		status = git.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
+			return git.PullRequestStatusUnknown, err
 		}
 
 		if combinedStatus.GetTotalCount() == 0 {
-			status = domain.PullRequestStatusSuccess
+			status = git.PullRequestStatusSuccess
 		} else {
 			switch combinedStatus.GetState() {
 			case "pending":
-				status = domain.PullRequestStatusPending
+				status = git.PullRequestStatusPending
 			case "success":
-				status = domain.PullRequestStatusSuccess
+				status = git.PullRequestStatusSuccess
 			case "failure", "error":
-				status = domain.PullRequestStatusError
+				status = git.PullRequestStatusError
 			}
 		}
 	}
diff --git a/internal/scm/github/pullrequest.go b/internal/scm/github/pullrequest.go
new file mode 100644
index 0000000..0e1831f
--- /dev/null
+++ b/internal/scm/github/pullrequest.go
@@ -0,0 +1,43 @@
+package github
+
+import (
+	"fmt"
+
+	"github.com/google/go-github/v38/github"
+	"github.com/lindell/multi-gitter/internal/git"
+)
+
+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(),
+	}
+}
+
+type pullRequest struct {
+	ownerName   string
+	repoName    string
+	branchName  string
+	prOwnerName string
+	prRepoName  string
+	number      int
+	guiURL      string
+	status      git.PullRequestStatus
+}
+
+func (pr pullRequest) String() string {
+	return fmt.Sprintf("%s/%s #%d", pr.ownerName, pr.repoName, pr.number)
+}
+
+func (pr pullRequest) Status() git.PullRequestStatus {
+	return pr.status
+}
+
+func (pr pullRequest) URL() string {
+	return pr.guiURL
+}
diff --git a/internal/scm/github/repository.go b/internal/scm/github/repository.go
new file mode 100644
index 0000000..9362b00
--- /dev/null
+++ b/internal/scm/github/repository.go
@@ -0,0 +1,46 @@
+package github
+
+import (
+	"fmt"
+	"net/url"
+
+	"github.com/google/go-github/v38/github"
+	"github.com/pkg/errors"
+)
+
+func convertRepo(r *github.Repository, token string) (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(),
+		token:         token,
+	}, nil
+}
+
+type repository struct {
+	url           url.URL
+	name          string
+	ownerName     string
+	defaultBranch string
+	token         string
+}
+
+func (r repository) CloneURL() string {
+	// Set the token as https://TOKEN@url
+	r.url.User = url.User(r.token)
+	return r.url.String()
+}
+
+func (r repository) DefaultBranch() string {
+	return r.defaultBranch
+}
+
+func (r repository) FullName() string {
+	return fmt.Sprintf("%s/%s", r.ownerName, r.name)
+}
diff --git a/internal/scm/github/util.go b/internal/scm/github/util.go
index 07d031d..72dc9e1 100644
--- a/internal/scm/github/util.go
+++ b/internal/scm/github/util.go
@@ -2,27 +2,27 @@ package github
 
 import (
 	"github.com/google/go-github/v38/github"
-	"github.com/lindell/multi-gitter/internal/domain"
+	"github.com/lindell/multi-gitter/internal/git"
 )
 
 // maps merge types to what they are called in the github api
-var mergeTypeGhName = map[domain.MergeType]string{
-	domain.MergeTypeMerge:  "merge",
-	domain.MergeTypeRebase: "rebase",
-	domain.MergeTypeSquash: "squash",
+var mergeTypeGhName = map[git.MergeType]string{
+	git.MergeTypeMerge:  "merge",
+	git.MergeTypeRebase: "rebase",
+	git.MergeTypeSquash: "squash",
 }
 
 // repoMergeTypes returns a list of all allowed merge types
-func repoMergeTypes(repo *github.Repository) []domain.MergeType {
-	ret := []domain.MergeType{}
+func repoMergeTypes(repo *github.Repository) []git.MergeType {
+	ret := []git.MergeType{}
 	if repo.GetAllowMergeCommit() {
-		ret = append(ret, domain.MergeTypeMerge)
+		ret = append(ret, git.MergeTypeMerge)
 	}
 	if repo.GetAllowRebaseMerge() {
-		ret = append(ret, domain.MergeTypeRebase)
+		ret = append(ret, git.MergeTypeRebase)
 	}
 	if repo.GetAllowSquashMerge() {
-		ret = append(ret, domain.MergeTypeSquash)
+		ret = append(ret, git.MergeTypeSquash)
 	}
 	return ret
 }
diff --git a/internal/scm/gitlab/gitlab.go b/internal/scm/gitlab/gitlab.go
index 3ec6314..38d4f50 100644
--- a/internal/scm/gitlab/gitlab.go
+++ b/internal/scm/gitlab/gitlab.go
@@ -5,15 +5,13 @@ import (
 	"errors"
 	"fmt"
 	"net/http"
-	"net/url"
 	"sort"
 	"strings"
 	"time"
 
-	"github.com/xanzy/go-gitlab"
-
-	"github.com/lindell/multi-gitter/internal/domain"
+	"github.com/lindell/multi-gitter/internal/git"
 	internalHTTP "github.com/lindell/multi-gitter/internal/http"
+	"github.com/xanzy/go-gitlab"
 )
 
 // New create a new Gitlab client
@@ -36,6 +34,7 @@ func New(token, baseURL string, repoListing RepositoryListing, config Config) (*
 		RepositoryListing: repoListing,
 		Config:            config,
 		glClient:          client,
+		token:             token,
 	}, nil
 }
 
@@ -43,6 +42,7 @@ func New(token, baseURL string, repoListing RepositoryListing, config Config) (*
 type Gitlab struct {
 	RepositoryListing
 	Config   Config
+	token    string
 	glClient *gitlab.Client
 
 	// Cached current user
@@ -79,61 +79,16 @@ func ParseProjectReference(val string) (ProjectReference, error) {
 	}, nil
 }
 
-type repository struct {
-	url           url.URL
-	pid           int
-	name          string
-	ownerName     string
-	defaultBranch string
-}
-
-func (r repository) URL(token string) string {
-	// Set the token as https://oauth2:TOKEN@url
-	r.url.User = url.UserPassword("oauth2", token)
-	return r.url.String()
-}
-
-func (r repository) DefaultBranch() string {
-	return r.defaultBranch
-}
-
-func (r repository) FullName() string {
-	return fmt.Sprintf("%s/%s", r.ownerName, r.name)
-}
-
-type pullRequest struct {
-	ownerName  string
-	repoName   string
-	targetPID  int
-	sourcePID  int
-	branchName string
-	iid        int
-	webURL     string
-	status     domain.PullRequestStatus
-}
-
-func (pr pullRequest) String() string {
-	return fmt.Sprintf("%s/%s #%d", pr.ownerName, pr.repoName, pr.iid)
-}
-
-func (pr pullRequest) Status() domain.PullRequestStatus {
-	return pr.status
-}
-
-func (pr pullRequest) URL() string {
-	return pr.webURL
-}
-
 // GetRepositories fetches repositories from all sources (groups/user/specific project)
-func (g *Gitlab) GetRepositories(ctx context.Context) ([]domain.Repository, error) {
+func (g *Gitlab) GetRepositories(ctx context.Context) ([]git.Repository, error) {
 	allProjects, err := g.getProjects(ctx)
 	if err != nil {
 		return nil, err
 	}
 
-	repos := make([]domain.Repository, 0, len(allProjects))
+	repos := make([]git.Repository, 0, len(allProjects))
 	for _, project := range allProjects {
-		p, err := convertProject(project)
+		p, err := convertProject(project, g.token)
 		if err != nil {
 			return nil, err
 		}
@@ -245,7 +200,7 @@ 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, prRepo domain.Repository, newPR domain.NewPullRequest) (domain.PullRequest, error) {
+func (g *Gitlab) CreatePullRequest(ctx context.Context, repo git.Repository, prRepo git.Repository, newPR git.NewPullRequest) (git.PullRequest, error) {
 	r := repo.(repository)
 	prR := prRepo.(repository)
 
@@ -302,13 +257,13 @@ func (g *Gitlab) getUserIDs(ctx context.Context, usernames []string) ([]int, err
 }
 
 // GetPullRequests gets all pull requests of with a specific branch
-func (g *Gitlab) GetPullRequests(ctx context.Context, branchName string) ([]domain.PullRequest, error) {
+func (g *Gitlab) GetPullRequests(ctx context.Context, branchName string) ([]git.PullRequest, error) {
 	projects, err := g.getProjects(ctx)
 	if err != nil {
 		return nil, err
 	}
 
-	prs := []domain.PullRequest{}
+	prs := []git.PullRequest{}
 	for _, project := range projects {
 		mr, err := g.getPullRequest(ctx, branchName, project)
 		if err != nil {
@@ -355,23 +310,23 @@ func (g *Gitlab) getPullRequest(ctx context.Context, branchName string, project
 	return mr, nil
 }
 
-func pullRequestStatus(mr *gitlab.MergeRequest) domain.PullRequestStatus {
+func pullRequestStatus(mr *gitlab.MergeRequest) git.PullRequestStatus {
 	switch {
 	case mr.MergedAt != nil:
-		return domain.PullRequestStatusMerged
+		return git.PullRequestStatusMerged
 	case mr.ClosedAt != nil:
-		return domain.PullRequestStatusClosed
+		return git.PullRequestStatusClosed
 	case mr.Pipeline == nil, mr.Pipeline.Status == "success":
-		return domain.PullRequestStatusSuccess
+		return git.PullRequestStatusSuccess
 	case mr.Pipeline.Status == "failed":
-		return domain.PullRequestStatusError
+		return git.PullRequestStatusError
 	default:
-		return domain.PullRequestStatusPending
+		return git.PullRequestStatusPending
 	}
 }
 
 // MergePullRequest merges a pull request
-func (g *Gitlab) MergePullRequest(ctx context.Context, pullReq domain.PullRequest) error {
+func (g *Gitlab) MergePullRequest(ctx context.Context, pullReq git.PullRequest) error {
 	pr := pullReq.(pullRequest)
 
 	shouldRemoveSourceBranch := true
@@ -386,7 +341,7 @@ func (g *Gitlab) MergePullRequest(ctx context.Context, pullReq domain.PullReques
 }
 
 // ClosePullRequest closes a pull request
-func (g *Gitlab) ClosePullRequest(ctx context.Context, pullReq domain.PullRequest) error {
+func (g *Gitlab) ClosePullRequest(ctx context.Context, pullReq git.PullRequest) error {
 	pr := pullReq.(pullRequest)
 
 	_, err := g.glClient.MergeRequests.DeleteMergeRequest(pr.targetPID, pr.iid, gitlab.WithContext(ctx))
@@ -403,7 +358,7 @@ func (g *Gitlab) ClosePullRequest(ctx context.Context, pullReq domain.PullReques
 }
 
 // ForkRepository forks a project
-func (g *Gitlab) ForkRepository(ctx context.Context, repo domain.Repository, newOwner string) (domain.Repository, error) {
+func (g *Gitlab) ForkRepository(ctx context.Context, repo git.Repository, newOwner string) (git.Repository, error) {
 	r := repo.(repository)
 
 	// Get the username of the fork (logged in user if none is set)
@@ -423,7 +378,7 @@ func (g *Gitlab) ForkRepository(ctx context.Context, repo domain.Repository, new
 		gitlab.WithContext(ctx),
 	)
 	if err == nil { // Already forked, just return it
-		return convertProject(project)
+		return convertProject(project, g.token)
 	} else if resp.StatusCode != http.StatusNotFound { // If the error was that the project does not exist, continue to fork it
 		return nil, err
 	}
@@ -442,7 +397,7 @@ func (g *Gitlab) ForkRepository(ctx context.Context, repo domain.Repository, new
 		}
 
 		if repo.ImportStatus == "finished" {
-			return convertProject(newRepo)
+			return convertProject(newRepo, g.token)
 		}
 
 		time.Sleep(time.Second * 3)
@@ -465,18 +420,3 @@ func (g *Gitlab) getCurrentUser(ctx context.Context) (*gitlab.User, error) {
 
 	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
-}
diff --git a/internal/scm/gitlab/pullrequest.go b/internal/scm/gitlab/pullrequest.go
new file mode 100644
index 0000000..d4a1e6b
--- /dev/null
+++ b/internal/scm/gitlab/pullrequest.go
@@ -0,0 +1,30 @@
+package gitlab
+
+import (
+	"fmt"
+
+	"github.com/lindell/multi-gitter/internal/git"
+)
+
+type pullRequest struct {
+	ownerName  string
+	repoName   string
+	targetPID  int
+	sourcePID  int
+	branchName string
+	iid        int
+	webURL     string
+	status     git.PullRequestStatus
+}
+
+func (pr pullRequest) String() string {
+	return fmt.Sprintf("%s/%s #%d", pr.ownerName, pr.repoName, pr.iid)
+}
+
+func (pr pullRequest) Status() git.PullRequestStatus {
+	return pr.status
+}
+
+func (pr pullRequest) URL() string {
+	return pr.webURL
+}
diff --git a/internal/scm/gitlab/repository.go b/internal/scm/gitlab/repository.go
new file mode 100644
index 0000000..0e0e8ea
--- /dev/null
+++ b/internal/scm/gitlab/repository.go
@@ -0,0 +1,47 @@
+package gitlab
+
+import (
+	"fmt"
+	"net/url"
+
+	"github.com/xanzy/go-gitlab"
+)
+
+func convertProject(project *gitlab.Project, token string) (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,
+		token:         token,
+	}, nil
+}
+
+type repository struct {
+	url           url.URL
+	pid           int
+	name          string
+	ownerName     string
+	defaultBranch string
+	token         string
+}
+
+func (r repository) CloneURL() string {
+	// Set the token as https://oauth2:TOKEN@url
+	r.url.User = url.UserPassword("oauth2", r.token)
+	return r.url.String()
+}
+
+func (r repository) DefaultBranch() string {
+	return r.defaultBranch
+}
+
+func (r repository) FullName() string {
+	return fmt.Sprintf("%s/%s", r.ownerName, r.name)
+}


Test patch:
diff --git a/internal/scm/github/github_test.go b/internal/scm/github/github_test.go
index 8b26aba..fcdb3b0 100644
--- a/internal/scm/github/github_test.go
+++ b/internal/scm/github/github_test.go
@@ -8,7 +8,7 @@ import (
 	"strings"
 	"testing"
 
-	"github.com/lindell/multi-gitter/internal/domain"
+	"github.com/lindell/multi-gitter/internal/git"
 	"github.com/lindell/multi-gitter/internal/scm/github"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -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}, false)
+		}, []git.MergeType{git.MergeTypeMerge}, false)
 		require.NoError(t, err)
 
 		repos, err := gh.GetRepositories(context.Background())
@@ -128,7 +128,7 @@ func Test_GetRepositories(t *testing.T) {
 		}
 	}
 
-	// Repository
+	// repository
 	{
 		gh, err := github.New("", "", transport.Wrapper, github.RepositoryListing{
 			Repositories: []github.RepositoryReference{
@@ -137,7 +137,7 @@ func Test_GetRepositories(t *testing.T) {
 					Name:      "test1",
 				},
 			},
-		}, []domain.MergeType{domain.MergeTypeMerge}, false)
+		}, []git.MergeType{git.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}, false)
+		}, []git.MergeType{git.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}, false)
+		}, []git.MergeType{git.MergeTypeMerge}, false)
 		require.NoError(t, err)
 
 		repos, err := gh.GetRepositories(context.Background())
diff --git a/tests/story_test.go b/tests/story_test.go
index 6f76961..6e66f92 100644
--- a/tests/story_test.go
+++ b/tests/story_test.go
@@ -6,9 +6,8 @@ import (
 	"path/filepath"
 	"testing"
 
-	"github.com/lindell/multi-gitter/internal/domain"
-
 	"github.com/lindell/multi-gitter/cmd"
+	"github.com/lindell/multi-gitter/internal/git"
 	"github.com/lindell/multi-gitter/tests/vcmock"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -72,7 +71,7 @@ Repositories with a successful run:
 `, string(runOutData))
 
 	//
-	// Status
+	// PullRequestStatus
 	//
 	statusOutFile := filepath.Join(tmpDir, "status-log.txt")
 
@@ -90,7 +89,7 @@ Repositories with a successful run:
 	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)
+	vcMock.SetPRStatus("should-change", "custom-branch-name", git.PullRequestStatusSuccess)
 
 	//
 	// Merge
@@ -112,7 +111,7 @@ Repositories with a successful run:
 	assert.Contains(t, string(mergeLogData), "Merging pr=\"owner/should-change #1\"")
 
 	//
-	// After Merge Status
+	// After Merge PullRequestStatus
 	//
 	afterMergeStatusOutFile := filepath.Join(tmpDir, "after-merge-status-log.txt")
 
@@ -149,7 +148,7 @@ Repositories with a successful run:
 	assert.Contains(t, string(closeLogData), "Closing pr=\"owner/should-change-2 #2\"")
 
 	//
-	// After Close Status
+	// After Close PullRequestStatus
 	//
 	afterCloseStatusOutFile := filepath.Join(tmpDir, "after-close-status-log.txt")
 
diff --git a/tests/vcmock/vcmock.go b/tests/vcmock/vcmock.go
index 0133883..8aea761 100644
--- a/tests/vcmock/vcmock.go
+++ b/tests/vcmock/vcmock.go
@@ -10,7 +10,7 @@ import (
 	"path/filepath"
 
 	git "github.com/go-git/go-git/v5"
-	"github.com/lindell/multi-gitter/internal/domain"
+	internalgit "github.com/lindell/multi-gitter/internal/git"
 )
 
 // VersionController is a mock of an version controller (Github/Gitlab/etc.)
@@ -21,8 +21,8 @@ type VersionController struct {
 }
 
 // GetRepositories returns mock repositories
-func (vc *VersionController) GetRepositories(ctx context.Context) ([]domain.Repository, error) {
-	ret := make([]domain.Repository, len(vc.Repositories))
+func (vc *VersionController) GetRepositories(ctx context.Context) ([]internalgit.Repository, error) {
+	ret := make([]internalgit.Repository, len(vc.Repositories))
 	for i := range vc.Repositories {
 		ret[i] = vc.Repositories[i]
 	}
@@ -30,12 +30,12 @@ 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, prRepo domain.Repository, newPR domain.NewPullRequest) (domain.PullRequest, error) {
+func (vc *VersionController) CreatePullRequest(ctx context.Context, repo internalgit.Repository, prRepo internalgit.Repository, newPR internalgit.NewPullRequest) (internalgit.PullRequest, error) {
 	repository := repo.(Repository)
 
 	vc.PRNumber++
 	pr := PullRequest{
-		PRStatus:       domain.PullRequestStatusPending,
+		PRStatus:       internalgit.PullRequestStatusPending,
 		PRNumber:       vc.PRNumber,
 		Repository:     repository,
 		NewPullRequest: newPR,
@@ -46,8 +46,8 @@ func (vc *VersionController) CreatePullRequest(ctx context.Context, repo domain.
 }
 
 // GetPullRequests gets mock pull request statuses
-func (vc *VersionController) GetPullRequests(ctx context.Context, branchName string) ([]domain.PullRequest, error) {
-	ret := make([]domain.PullRequest, 0, len(vc.PullRequests))
+func (vc *VersionController) GetPullRequests(ctx context.Context, branchName string) ([]internalgit.PullRequest, error) {
+	ret := make([]internalgit.PullRequest, 0, len(vc.PullRequests))
 	for _, pr := range vc.PullRequests {
 		if pr.NewPullRequest.Head == branchName {
 			ret = append(ret, pr)
@@ -57,11 +57,11 @@ func (vc *VersionController) GetPullRequests(ctx context.Context, branchName str
 }
 
 // MergePullRequest sets the status of a mock pull requests to merged
-func (vc *VersionController) MergePullRequest(ctx context.Context, pr domain.PullRequest) error {
+func (vc *VersionController) MergePullRequest(ctx context.Context, pr internalgit.PullRequest) error {
 	pullRequest := pr.(PullRequest)
 	for i := range vc.PullRequests {
 		if vc.PullRequests[i].Repository.FullName() == pullRequest.Repository.FullName() {
-			vc.PullRequests[i].PRStatus = domain.PullRequestStatusMerged
+			vc.PullRequests[i].PRStatus = internalgit.PullRequestStatusMerged
 			return nil
 		}
 	}
@@ -69,11 +69,11 @@ func (vc *VersionController) MergePullRequest(ctx context.Context, pr domain.Pul
 }
 
 // ClosePullRequest sets the status of a mock pull requests to closed
-func (vc *VersionController) ClosePullRequest(ctx context.Context, pr domain.PullRequest) error {
+func (vc *VersionController) ClosePullRequest(ctx context.Context, pr internalgit.PullRequest) error {
 	pullRequest := pr.(PullRequest)
 	for i := range vc.PullRequests {
 		if vc.PullRequests[i].Repository.FullName() == pullRequest.Repository.FullName() {
-			vc.PullRequests[i].PRStatus = domain.PullRequestStatusClosed
+			vc.PullRequests[i].PRStatus = internalgit.PullRequestStatusClosed
 			return nil
 		}
 	}
@@ -86,7 +86,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) {
+func (vc *VersionController) SetPRStatus(repoName string, branchName string, newStatus internalgit.PullRequestStatus) {
 	for i := range vc.PullRequests {
 		if vc.PullRequests[i].Repository.RepoName == repoName && vc.PullRequests[i].Head == branchName {
 			vc.PullRequests[i].PRStatus = newStatus
@@ -110,7 +110,7 @@ func (vc *VersionController) GetAutocompleteRepositories(ctx context.Context, st
 }
 
 // ForkRepository forks a repository
-func (vc *VersionController) ForkRepository(ctx context.Context, repo domain.Repository, newOwner string) (domain.Repository, error) {
+func (vc *VersionController) ForkRepository(ctx context.Context, repo internalgit.Repository, newOwner string) (internalgit.Repository, error) {
 	r := repo.(Repository)
 
 	if newOwner == "" {
@@ -142,16 +142,16 @@ func (vc *VersionController) Clean() {
 
 // PullRequest is a mock pr
 type PullRequest struct {
-	PRStatus domain.PullRequestStatus
+	PRStatus internalgit.PullRequestStatus
 	PRNumber int
 	Merged   bool
 
 	Repository
-	domain.NewPullRequest
+	internalgit.NewPullRequest
 }
 
 // Status returns the pr status
-func (pr PullRequest) Status() domain.PullRequestStatus {
+func (pr PullRequest) Status() internalgit.PullRequestStatus {
 	return pr.PRStatus
 }
 
@@ -167,8 +167,8 @@ type Repository struct {
 	Path      string
 }
 
-// URL return the URL (filepath) of the repository on disk
-func (r Repository) URL(token string) string {
+// CloneURL return the URL (filepath) of the repository on disk
+func (r Repository) CloneURL() string {
 	return fmt.Sprintf(`file://%s`, filepath.ToSlash(r.Path))
 }