Repository: lindell/multi-gitter
Pull request #339 (2023-04-01 23:00:36)

refactor redundant code into a function
# What does this change

As discussed in PR #337 , this is the refactoring to consolidate the code for preparing command execution.

# What issue does it fix
n/a

# Notes for the reviewer
None.

# Checklist
- [/] Made sure the PR follows the [CONTRIBUTING.md](CONTRIBUTING.md) guidelines
- [/] Tests if something new is added



Patch:
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 0000000..351bf73
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,20 @@
+{
+    "name": "multi-gitter development",
+    "customizations": {
+        "codespaces": {
+            "openFiles": [
+                "CONTRIBUTING.md"
+            ]
+        },
+        "vscode": {
+            "extensions": [
+                "golang.go"
+            ],
+            "settings": {
+                "go.lintTool": "golangci-lint"
+            }
+        }
+    },
+    "image": "mcr.microsoft.com/vscode/devcontainers/go:1",
+    "updateContentCommand": "go get"
+}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f60f058..2c031d9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
 # Changelog
 
+## [0.46.0](https://www.github.com/lindell/multi-gitter/compare/v0.45.0...v0.46.0) (2023-04-09)
+
+
+### Features
+
+* option to skip repositories that are forks ([#341](https://www.github.com/lindell/multi-gitter/issues/341)) ([941731b](https://www.github.com/lindell/multi-gitter/commit/941731bfc0a9a89b2abef18286a7a3b06ab5d1db))
+
 ## [0.45.0](https://www.github.com/lindell/multi-gitter/compare/v0.44.2...v0.45.0) (2023-04-01)
 
 
diff --git a/README.md b/README.md
index f7394b7..6f5515c 100755
--- a/README.md
+++ b/README.md
@@ -209,6 +209,9 @@ repo:
 reviewers:
   - example
 
+# Skip repositories which are forks.
+skip-forks: false
+
 # Skip pull request and directly push to the branch.
 skip-pr: false
 
@@ -293,6 +296,9 @@ repo:
   - my-org/js-repo
   - other-org/python-repo
 
+# Skip repositories which are forks.
+skip-forks: false
+
 # Use SSH cloning URL instead of HTTPS + token. This requires that a setup with ssh keys that have access to all repos and that the server is already in known_hosts.
 ssh-auth: false
 
@@ -367,6 +373,9 @@ repo:
   - my-org/js-repo
   - other-org/python-repo
 
+# Skip repositories which are forks.
+skip-forks: false
+
 # Use SSH cloning URL instead of HTTPS + token. This requires that a setup with ssh keys that have access to all repos and that the server is already in known_hosts.
 ssh-auth: false
 
@@ -438,6 +447,9 @@ repo:
   - my-org/js-repo
   - other-org/python-repo
 
+# Skip repositories which are forks.
+skip-forks: false
+
 # Use SSH cloning URL instead of HTTPS + token. This requires that a setup with ssh keys that have access to all repos and that the server is already in known_hosts.
 ssh-auth: false
 
@@ -518,6 +530,9 @@ repo:
   - my-org/js-repo
   - other-org/python-repo
 
+# Skip repositories which are forks.
+skip-forks: false
+
 # Use SSH cloning URL instead of HTTPS + token. This requires that a setup with ssh keys that have access to all repos and that the server is already in known_hosts.
 ssh-auth: false
 
@@ -601,6 +616,7 @@ Flags:
   -P, --project strings            The name, including owner of a GitLab project in the format "ownerName/repoName".
   -R, --repo strings               The name, including owner of a GitHub repository in the format "ownerName/repoName".
   -r, --reviewers strings          The username of the reviewers to be added on the pull request.
+      --skip-forks                 Skip repositories which are forks.
       --skip-pr                    Skip pull request and directly push to the branch.
   -s, --skip-repo strings          Skip changes on specified repositories, the name is including the owner of repository in the format "ownerName/repoName".
       --ssh-auth                   Use SSH cloning URL instead of HTTPS + token. This requires that a setup with ssh keys that have access to all repos and that the server is already in known_hosts.
@@ -634,6 +650,7 @@ Flags:
   -p, --platform string      The platform that is used. Available values: github, gitlab, gitea, bitbucket_server. (default "github")
   -P, --project strings      The name, including owner of a GitLab project in the format "ownerName/repoName".
   -R, --repo strings         The name, including owner of a GitHub repository in the format "ownerName/repoName".
+      --skip-forks           Skip repositories which are forks.
       --ssh-auth             Use SSH cloning URL instead of HTTPS + token. This requires that a setup with ssh keys that have access to all repos and that the server is already in known_hosts.
   -T, --token string         The personal access token for the targeting platform. Can also be set using the GITHUB_TOKEN/GITLAB_TOKEN/GITEA_TOKEN/BITBUCKET_SERVER_TOKEN environment variable.
       --topic strings        The topic of a GitHub/GitLab/Gitea repository. All repositories having at least one matching topic are targeted.
@@ -665,6 +682,7 @@ Flags:
   -p, --platform string     The platform that is used. Available values: github, gitlab, gitea, bitbucket_server. (default "github")
   -P, --project strings     The name, including owner of a GitLab project in the format "ownerName/repoName".
   -R, --repo strings        The name, including owner of a GitHub repository in the format "ownerName/repoName".
+      --skip-forks          Skip repositories which are forks.
       --ssh-auth            Use SSH cloning URL instead of HTTPS + token. This requires that a setup with ssh keys that have access to all repos and that the server is already in known_hosts.
   -T, --token string        The personal access token for the targeting platform. Can also be set using the GITHUB_TOKEN/GITLAB_TOKEN/GITEA_TOKEN/BITBUCKET_SERVER_TOKEN environment variable.
       --topic strings       The topic of a GitHub/GitLab/Gitea repository. All repositories having at least one matching topic are targeted.
@@ -695,6 +713,7 @@ Flags:
   -p, --platform string     The platform that is used. Available values: github, gitlab, gitea, bitbucket_server. (default "github")
   -P, --project strings     The name, including owner of a GitLab project in the format "ownerName/repoName".
   -R, --repo strings        The name, including owner of a GitHub repository in the format "ownerName/repoName".
+      --skip-forks          Skip repositories which are forks.
       --ssh-auth            Use SSH cloning URL instead of HTTPS + token. This requires that a setup with ssh keys that have access to all repos and that the server is already in known_hosts.
   -T, --token string        The personal access token for the targeting platform. Can also be set using the GITHUB_TOKEN/GITLAB_TOKEN/GITEA_TOKEN/BITBUCKET_SERVER_TOKEN environment variable.
       --topic strings       The topic of a GitHub/GitLab/Gitea repository. All repositories having at least one matching topic are targeted.
@@ -736,6 +755,7 @@ Flags:
   -p, --platform string       The platform that is used. Available values: github, gitlab, gitea, bitbucket_server. (default "github")
   -P, --project strings       The name, including owner of a GitLab project in the format "ownerName/repoName".
   -R, --repo strings          The name, including owner of a GitHub repository in the format "ownerName/repoName".
+      --skip-forks            Skip repositories which are forks.
       --ssh-auth              Use SSH cloning URL instead of HTTPS + token. This requires that a setup with ssh keys that have access to all repos and that the server is already in known_hosts.
   -T, --token string          The personal access token for the targeting platform. Can also be set using the GITHUB_TOKEN/GITLAB_TOKEN/GITEA_TOKEN/BITBUCKET_SERVER_TOKEN environment variable.
       --topic strings         The topic of a GitHub/GitLab/Gitea repository. All repositories having at least one matching topic are targeted.
diff --git a/cmd/cmd-close.go b/cmd/cmd-close.go
index a14eaf0..2cd4be6 100644
--- a/cmd/cmd-close.go
+++ b/cmd/cmd-close.go
@@ -15,7 +15,7 @@ func CloseCmd() *cobra.Command {
 		Long:    "Close pull requests with a specified branch name in an organization and with specified conditions.",
 		Args:    cobra.NoArgs,
 		PreRunE: logFlagInit,
-		RunE:    close,
+		RunE:    closeCMD,
 	}
 
 	cmd.Flags().StringP("branch", "B", "multi-gitter-branch", "The name of the branch where changes are committed.")
@@ -27,7 +27,7 @@ func CloseCmd() *cobra.Command {
 	return cmd
 }
 
-func close(cmd *cobra.Command, args []string) error {
+func closeCMD(cmd *cobra.Command, _ []string) error {
 	flag := cmd.Flags()
 
 	branchName, _ := flag.GetString("branch")
diff --git a/cmd/cmd-merge.go b/cmd/cmd-merge.go
index f98ee40..083e70c 100644
--- a/cmd/cmd-merge.go
+++ b/cmd/cmd-merge.go
@@ -19,7 +19,8 @@ func MergeCmd() *cobra.Command {
 	}
 
 	cmd.Flags().StringP("branch", "B", "multi-gitter-branch", "The name of the branch where changes are committed.")
-	cmd.Flags().StringSliceP("merge-type", "", []string{"merge", "squash", "rebase"}, "The type of merge that should be done (GitHub). Multiple types can be used as backup strategies if the first one is not allowed.")
+	cmd.Flags().StringSliceP("merge-type", "", []string{"merge", "squash", "rebase"},
+		"The type of merge that should be done (GitHub). Multiple types can be used as backup strategies if the first one is not allowed.")
 	configurePlatform(cmd)
 	configureRunPlatform(cmd, false)
 	configureLogging(cmd, "-")
@@ -28,7 +29,7 @@ func MergeCmd() *cobra.Command {
 	return cmd
 }
 
-func merge(cmd *cobra.Command, args []string) error {
+func merge(cmd *cobra.Command, _ []string) error {
 	flag := cmd.Flags()
 
 	branchName, _ := flag.GetString("branch")
diff --git a/cmd/cmd-print.go b/cmd/cmd-print.go
index 1cd70f8..1314e2a 100755
--- a/cmd/cmd-print.go
+++ b/cmd/cmd-print.go
@@ -13,6 +13,7 @@ import (
 	"github.com/spf13/cobra"
 )
 
+//nolint:lll
 const printHelp = `
 This command will clone down multiple repositories. For each of those repositories, the script will be run in the context of that repository. The output of each script run in each repo will be printed, by default to stdout and stderr, but it can be configured to files as well.
 
@@ -28,7 +29,7 @@ func PrintCmd() *cobra.Command {
 		Long:    printHelp,
 		Args:    cobra.ExactArgs(1),
 		PreRunE: logFlagInit,
-		RunE:    print,
+		RunE:    printCMD,
 	}
 
 	cmd.Flags().IntP("concurrent", "C", 1, "The maximum number of concurrent runs.")
@@ -42,7 +43,7 @@ func PrintCmd() *cobra.Command {
 	return cmd
 }
 
-func print(cmd *cobra.Command, args []string) error {
+func printCMD(cmd *cobra.Command, _ []string) error {
 	flag := cmd.Flags()
 
 	concurrent, _ := flag.GetInt("concurrent")
diff --git a/cmd/cmd-run.go b/cmd/cmd-run.go
index c52306c..fb80eeb 100755
--- a/cmd/cmd-run.go
+++ b/cmd/cmd-run.go
@@ -15,6 +15,7 @@ import (
 	"github.com/spf13/cobra"
 )
 
+//nolint:lll
 const runHelp = `
 This command will clone down multiple repositories. For each of those repositories, the script will be run in the context of that repository. If the script finished with a zero exit code, and the script resulted in file changes, a pull request will be created with.
 
@@ -69,7 +70,7 @@ Available values:
 	return cmd
 }
 
-func run(cmd *cobra.Command, args []string) error {
+func run(cmd *cobra.Command, _ []string) error {
 	flag := cmd.Flags()
 
 	branchName, _ := flag.GetString("branch")
diff --git a/cmd/cmd-status.go b/cmd/cmd-status.go
index b015734..90d0659 100644
--- a/cmd/cmd-status.go
+++ b/cmd/cmd-status.go
@@ -29,7 +29,7 @@ func StatusCmd() *cobra.Command {
 	return cmd
 }
 
-func status(cmd *cobra.Command, args []string) error {
+func status(cmd *cobra.Command, _ []string) error {
 	flag := cmd.Flags()
 
 	branchName, _ := flag.GetString("branch")
diff --git a/cmd/cmd-version.go b/cmd/cmd-version.go
index 19f23e8..4191f1d 100644
--- a/cmd/cmd-version.go
+++ b/cmd/cmd-version.go
@@ -30,7 +30,7 @@ var BuildDate time.Time
 // Commit is the commit the build was made on (set by main.go)
 var Commit string
 
-func version(cmd *cobra.Command, args []string) {
+func version(_ *cobra.Command, _ []string) {
 	fmt.Printf("multi-gitter version: %s\n", Version)
 	fmt.Printf("Release-Date: %s\n", BuildDate.Format("2006-01-02"))
 	fmt.Printf("Go version: %s\n", runtime.Version())
diff --git a/cmd/config.go b/cmd/config.go
index 68d06fe..8093d7f 100644
--- a/cmd/config.go
+++ b/cmd/config.go
@@ -19,11 +19,7 @@ func initializeConfig(cmd *cobra.Command) error {
 	}
 
 	// Read any config defined in static config files
-	if err := initializeStaticConfig(cmd); err != nil {
-		return err
-	}
-
-	return nil
+	return initializeStaticConfig(cmd)
 }
 
 func initializeDynamicConfig(cmd *cobra.Command) error {
diff --git a/cmd/logging.go b/cmd/logging.go
index 0f3cc15..1c5c337 100644
--- a/cmd/logging.go
+++ b/cmd/logging.go
@@ -27,7 +27,7 @@ func configureLogging(cmd *cobra.Command, logFile string) {
 	flags.StringP("log-file", "", logFile, `The file where all logs should be printed to. "-" means stdout.`)
 }
 
-func logFlagInit(cmd *cobra.Command, args []string) error {
+func logFlagInit(cmd *cobra.Command, _ []string) error {
 	// Parse and set log level
 	strLevel, _ := cmd.Flags().GetString("log-level")
 	logLevel, err := log.ParseLevel(strLevel)
diff --git a/cmd/platform.go b/cmd/platform.go
index 109101a..a8018c4 100644
--- a/cmd/platform.go
+++ b/cmd/platform.go
@@ -32,6 +32,7 @@ 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.BoolP("ssh-auth", "", false, `Use SSH cloning URL instead of HTTPS + token. This requires that a setup with ssh keys that have access to all repos and that the server is already in known_hosts.`)
+	flags.BoolP("skip-forks", "", false, `Skip repositories which are forks.`)
 
 	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) {
@@ -125,6 +126,7 @@ func createGithubClient(flag *flag.FlagSet, verifyFlags bool, readOnly bool) (mu
 	forkMode, _ := flag.GetBool("fork")
 	forkOwner, _ := flag.GetString("fork-owner")
 	sshAuth, _ := flag.GetBool("ssh-auth")
+	skipForks, _ := flag.GetBool("skip-forks")
 
 	if verifyFlags && len(orgs) == 0 && len(users) == 0 && len(repos) == 0 {
 		return nil, errors.New("no organization, user or repo set")
@@ -166,6 +168,7 @@ func createGithubClient(flag *flag.FlagSet, verifyFlags bool, readOnly bool) (mu
 			Users:         users,
 			Repositories:  repoRefs,
 			Topics:        topics,
+			SkipForks:     skipForks,
 		},
 		MergeTypes:       mergeTypes,
 		ForkMode:         forkMode,
@@ -189,6 +192,7 @@ func createGitlabClient(flag *flag.FlagSet, verifyFlags bool) (multigitter.Versi
 	topics, _ := flag.GetStringSlice("topic")
 	includeSubgroups, _ := flag.GetBool("include-subgroups")
 	sshAuth, _ := flag.GetBool("ssh-auth")
+	skipForks, _ := flag.GetBool("skip-forks")
 
 	if verifyFlags && len(groups) == 0 && len(users) == 0 && len(projects) == 0 {
 		return nil, errors.New("no group user or project set")
@@ -208,10 +212,11 @@ func createGitlabClient(flag *flag.FlagSet, verifyFlags bool) (multigitter.Versi
 	}
 
 	vc, err := gitlab.New(token, gitBaseURL, gitlab.RepositoryListing{
-		Groups:   groups,
-		Users:    users,
-		Projects: projRefs,
-		Topics:   topics,
+		Groups:    groups,
+		Users:     users,
+		Projects:  projRefs,
+		Topics:    topics,
+		SkipForks: skipForks,
 	}, gitlab.Config{
 		IncludeSubgroups: includeSubgroups,
 		SSHAuth:          sshAuth,
@@ -230,6 +235,7 @@ func createGiteaClient(flag *flag.FlagSet, verifyFlags bool) (multigitter.Versio
 	repos, _ := flag.GetStringSlice("repo")
 	topics, _ := flag.GetStringSlice("topic")
 	sshAuth, _ := flag.GetBool("ssh-auth")
+	skipForks, _ := flag.GetBool("skip-forks")
 
 	if verifyFlags && len(orgs) == 0 && len(users) == 0 && len(repos) == 0 {
 		return nil, errors.New("no organization, user or repository set")
@@ -262,6 +268,7 @@ func createGiteaClient(flag *flag.FlagSet, verifyFlags bool) (multigitter.Versio
 		Users:         users,
 		Repositories:  repoRefs,
 		Topics:        topics,
+		SkipForks:     skipForks,
 	}, mergeTypes, sshAuth)
 	if err != nil {
 		return nil, err
diff --git a/internal/git/cmdgit/git.go b/internal/git/cmdgit/git.go
index dd92bdc..a4bbd0c 100644
--- a/internal/git/cmdgit/git.go
+++ b/internal/git/cmdgit/git.go
@@ -121,7 +121,7 @@ func (g *Git) logDiff() error {
 
 // BranchExist checks if the new branch exists
 func (g *Git) BranchExist(remoteName, branchName string) (bool, error) {
-	cmd := exec.Command("git", "ls-remote", "-q", "-h")
+	cmd := exec.Command("git", "ls-remote", "-q", "-h", remoteName)
 	stdOut, err := g.run(cmd)
 	if err != nil {
 		return false, err
diff --git a/internal/multigitter/cmd.go b/internal/multigitter/cmd.go
new file mode 100644
index 0000000..aee9dfa
--- /dev/null
+++ b/internal/multigitter/cmd.go
@@ -0,0 +1,26 @@
+package multigitter
+
+import (
+	"context"
+	"fmt"
+	"github.com/lindell/multi-gitter/internal/scm"
+	"os"
+	"os/exec"
+)
+
+func prepareScriptCommand(
+	ctx context.Context,
+	repo scm.Repository,
+	workDir string,
+	scriptPath string,
+	arguments []string,
+) (cmd *exec.Cmd) {
+	// Run the command that might or might not change the content of the repo
+	// If the command return a non-zero exit code, abort.
+	cmd = exec.CommandContext(ctx, scriptPath, arguments...)
+	cmd.Dir = workDir
+	cmd.Env = append(os.Environ(),
+		fmt.Sprintf("REPOSITORY=%s", repo.FullName()),
+	)
+	return cmd
+}
diff --git a/internal/multigitter/merge.go b/internal/multigitter/merge.go
index 4a7b531..36340d9 100644
--- a/internal/multigitter/merge.go
+++ b/internal/multigitter/merge.go
@@ -36,7 +36,7 @@ func (s Merger) Merge(ctx context.Context) error {
 		log.Infof("Merging")
 		err := s.VersionController.MergePullRequest(ctx, pr)
 		if err != nil {
-			log.Errorf("Error occured while merging: %s", err.Error())
+			log.Errorf("Error occurred while merging: %s", err.Error())
 		}
 	}
 
diff --git a/internal/multigitter/print.go b/internal/multigitter/print.go
index 9cb938b..8be890f 100755
--- a/internal/multigitter/print.go
+++ b/internal/multigitter/print.go
@@ -3,13 +3,11 @@ package multigitter
 import (
 	"context"
 	"fmt"
+	"github.com/lindell/multi-gitter/internal/multigitter/repocounter"
+	"github.com/lindell/multi-gitter/internal/scm"
 	log "github.com/sirupsen/logrus"
 	"io"
 	"os"
-	"os/exec"
-
-	"github.com/lindell/multi-gitter/internal/multigitter/repocounter"
-	"github.com/lindell/multi-gitter/internal/scm"
 )
 
 // Printer contains fields to be able to do the print command
@@ -81,13 +79,7 @@ func (r Printer) runSingleRepo(ctx context.Context, repo scm.Repository) error {
 		return err
 	}
 
-	// Run the command that might or might not change the content of the repo
-	// If the command return a non-zero exit code, abort.
-	cmd := exec.CommandContext(ctx, r.ScriptPath, r.Arguments...)
-	cmd.Dir = tmpDir
-	cmd.Env = append(os.Environ(),
-		fmt.Sprintf("REPOSITORY=%s", repo.FullName()),
-	)
+	cmd := prepareScriptCommand(ctx, repo, tmpDir, r.ScriptPath, r.Arguments)
 
 	cmd.Stdout = r.Stdout
 	cmd.Stderr = r.Stderr
diff --git a/internal/multigitter/run.go b/internal/multigitter/run.go
index 53191a2..8f17b1f 100755
--- a/internal/multigitter/run.go
+++ b/internal/multigitter/run.go
@@ -229,13 +229,7 @@ func (r *Runner) runSingleRepo(ctx context.Context, repo scm.Repository) (scm.Pu
 		}
 	}
 
-	// Run the command that might or might not change the content of the repo
-	// If the command return a non-zero exit code, abort.
-	cmd := exec.CommandContext(ctx, r.ScriptPath, r.Arguments...)
-	cmd.Dir = tmpDir
-	cmd.Env = append(os.Environ(),
-		fmt.Sprintf("REPOSITORY=%s", repo.FullName()),
-	)
+	cmd := prepareScriptCommand(ctx, repo, tmpDir, r.ScriptPath, r.Arguments)
 	if r.DryRun {
 		cmd.Env = append(cmd.Env, "DRY_RUN=true")
 	}
diff --git a/internal/scm/gitea/gitea.go b/internal/scm/gitea/gitea.go
index 69739a4..4143555 100644
--- a/internal/scm/gitea/gitea.go
+++ b/internal/scm/gitea/gitea.go
@@ -69,6 +69,7 @@ type RepositoryListing struct {
 	Users         []string
 	Repositories  []RepositoryReference
 	Topics        []string
+	SkipForks     bool
 }
 
 // RepositoryReference contains information to be able to reference a repository
@@ -100,6 +101,11 @@ func (g *Gitea) GetRepositories(ctx context.Context) ([]scm.Repository, error) {
 	for _, repo := range allRepos {
 		log := log.WithField("repo", repo.FullName)
 
+		if g.SkipForks && repo.Fork {
+			log.Debug("Skipping repository since it's a fork")
+			continue
+		}
+
 		if len(g.Topics) != 0 {
 			topics, err := g.getRepoTopics(ctx, repo)
 			if err != nil {
diff --git a/internal/scm/github/github.go b/internal/scm/github/github.go
index f3503ac..1a0ec8e 100755
--- a/internal/scm/github/github.go
+++ b/internal/scm/github/github.go
@@ -109,6 +109,7 @@ type RepositoryListing struct {
 	Users         []string
 	Repositories  []RepositoryReference
 	Topics        []string
+	SkipForks     bool
 }
 
 // RepositoryReference contains information to be able to reference a repository
@@ -158,6 +159,9 @@ func (g *Github) GetRepositories(ctx context.Context) ([]scm.Repository, error)
 		case len(g.Topics) != 0 && !scm.RepoContainsTopic(r.Topics, g.Topics):
 			log.Debug("Skipping repository since it does not match repository topics")
 			continue
+		case g.SkipForks && r.GetFork():
+			log.Debug("Skipping repository since it's a fork")
+			continue
 		}
 
 		if g.checkPermissions {
diff --git a/internal/scm/github/graphql.go b/internal/scm/github/graphql.go
index 5da4ac0..d292310 100644
--- a/internal/scm/github/graphql.go
+++ b/internal/scm/github/graphql.go
@@ -74,7 +74,7 @@ func (g *Github) makeGraphQLRequest(ctx context.Context, query string, data inte
 	}
 
 	if err := json.Unmarshal(resultData.Data, res); err != nil {
-		return err
+		return errors.WithMessage(err, "could not unmarshal graphQL result")
 	}
 
 	return nil
diff --git a/internal/scm/gitlab/gitlab.go b/internal/scm/gitlab/gitlab.go
index a0498b8..ec1800a 100644
--- a/internal/scm/gitlab/gitlab.go
+++ b/internal/scm/gitlab/gitlab.go
@@ -52,10 +52,11 @@ type Gitlab struct {
 
 // RepositoryListing contains information about which repositories that should be fetched
 type RepositoryListing struct {
-	Groups   []string
-	Users    []string
-	Projects []ProjectReference
-	Topics   []string
+	Groups    []string
+	Users     []string
+	Projects  []ProjectReference
+	Topics    []string
+	SkipForks bool
 }
 
 // Config includes extra config parameters for the GitLab client
@@ -96,6 +97,10 @@ func (g *Gitlab) GetRepositories(ctx context.Context) ([]scm.Repository, error)
 			log.Debug("Skipping repository since it does not match repository topics")
 			continue
 		}
+		if g.SkipForks && project.ForkedFromProject != nil {
+			log.Debug("Skipping repository since it's a fork")
+			continue
+		}
 
 		p, err := g.convertProject(project)
 		if err != nil {
diff --git a/version.txt b/version.txt
index bcce5d0..3010923 100644
--- a/version.txt
+++ b/version.txt
@@ -1,1 +1,1 @@
-0.45.0
+0.46.0


Test patch:
diff --git a/internal/scm/github/github_test.go b/internal/scm/github/github_test.go
index 8976547..38f65fa 100644
--- a/internal/scm/github/github_test.go
+++ b/internal/scm/github/github_test.go
@@ -58,6 +58,7 @@ func Test_GetRepositories(t *testing.T) {
 						"site_admin": false
 					},
 					"html_url": "https://github.com/test-org/test1",
+					"fork": false,
 					"archived": false,
 					"disabled": false,
 					"default_branch": "master",
@@ -80,6 +81,7 @@ func Test_GetRepositories(t *testing.T) {
 					"site_admin": false
 				},
 				"html_url": "https://github.com/test-org/test1",
+				"fork": false,
 				"archived": false,
 				"disabled": false,
 				"default_branch": "master",
@@ -106,6 +108,7 @@ func Test_GetRepositories(t *testing.T) {
 						"site_admin": false
 					},
 					"html_url": "https://github.com/lindell/test2",
+					"fork": true,
 					"archived": false,
 					"disabled": false,
 					"default_branch": "main",
@@ -229,4 +232,30 @@ func Test_GetRepositories(t *testing.T) {
 			assert.Equal(t, "lindell/test2", repos[1].FullName())
 		}
 	}
+
+	// Forks
+	{
+		gh, err := github.New(github.Config{
+			TransportMiddleware: transport.Wrapper,
+			RepoListing: github.RepositoryListing{
+				Organizations: []string{"test-org"},
+				Users:         []string{"test-user"},
+				Repositories: []github.RepositoryReference{
+					{
+						OwnerName: "test-org",
+						Name:      "test1",
+					},
+				},
+				SkipForks: true,
+			},
+			MergeTypes: []scm.MergeType{scm.MergeTypeMerge},
+		})
+		require.NoError(t, err)
+
+		repos, err := gh.GetRepositories(context.Background())
+		assert.NoError(t, err)
+		if assert.Len(t, repos, 1) {
+			assert.Equal(t, "test-org/test1", repos[0].FullName())
+		}
+	}
 }
diff --git a/tests/vcmock/vcmock.go b/tests/vcmock/vcmock.go
index ec9db12..93f2682 100644
--- a/tests/vcmock/vcmock.go
+++ b/tests/vcmock/vcmock.go
@@ -24,7 +24,7 @@ type VersionController struct {
 }
 
 // GetRepositories returns mock repositories
-func (vc *VersionController) GetRepositories(ctx context.Context) ([]scm.Repository, error) {
+func (vc *VersionController) GetRepositories(_ context.Context) ([]scm.Repository, error) {
 	ret := make([]scm.Repository, len(vc.Repositories))
 	for i := range vc.Repositories {
 		ret[i] = vc.Repositories[i]
@@ -33,7 +33,7 @@ func (vc *VersionController) GetRepositories(ctx context.Context) ([]scm.Reposit
 }
 
 // CreatePullRequest stores a mock pull request
-func (vc *VersionController) CreatePullRequest(ctx context.Context, repo scm.Repository, prRepo scm.Repository, newPR scm.NewPullRequest) (scm.PullRequest, error) {
+func (vc *VersionController) CreatePullRequest(_ context.Context, repo scm.Repository, _ scm.Repository, newPR scm.NewPullRequest) (scm.PullRequest, error) {
 	repository := repo.(Repository)
 
 	vc.prLock.Lock()
@@ -52,7 +52,7 @@ func (vc *VersionController) CreatePullRequest(ctx context.Context, repo scm.Rep
 }
 
 // GetPullRequests gets mock pull request statuses
-func (vc *VersionController) GetPullRequests(ctx context.Context, branchName string) ([]scm.PullRequest, error) {
+func (vc *VersionController) GetPullRequests(_ context.Context, branchName string) ([]scm.PullRequest, error) {
 	vc.prLock.RLock()
 	defer vc.prLock.RUnlock()
 
@@ -66,7 +66,7 @@ func (vc *VersionController) GetPullRequests(ctx context.Context, branchName str
 }
 
 // GetOpenPullRequest gets mock open pull request
-func (vc *VersionController) GetOpenPullRequest(ctx context.Context, repo scm.Repository, branchName string) (scm.PullRequest, error) {
+func (vc *VersionController) GetOpenPullRequest(_ context.Context, repo scm.Repository, branchName string) (scm.PullRequest, error) {
 	vc.prLock.RLock()
 	defer vc.prLock.RUnlock()
 
@@ -85,7 +85,7 @@ func openPullRequest(pr PullRequest) bool {
 }
 
 // MergePullRequest sets the status of a mock pull requests to merged
-func (vc *VersionController) MergePullRequest(ctx context.Context, pr scm.PullRequest) error {
+func (vc *VersionController) MergePullRequest(_ context.Context, pr scm.PullRequest) error {
 	vc.prLock.Lock()
 	defer vc.prLock.Unlock()
 
@@ -100,7 +100,7 @@ func (vc *VersionController) MergePullRequest(ctx context.Context, pr scm.PullRe
 }
 
 // ClosePullRequest sets the status of a mock pull requests to closed
-func (vc *VersionController) ClosePullRequest(ctx context.Context, pr scm.PullRequest) error {
+func (vc *VersionController) ClosePullRequest(_ context.Context, pr scm.PullRequest) error {
 	vc.prLock.Lock()
 	defer vc.prLock.Unlock()
 
@@ -132,17 +132,17 @@ func (vc *VersionController) SetPRStatus(repoName string, branchName string, new
 }
 
 // GetAutocompleteOrganizations gets organizations for autocompletion
-func (vc *VersionController) GetAutocompleteOrganizations(ctx context.Context, str string) ([]string, error) {
+func (vc *VersionController) GetAutocompleteOrganizations(_ context.Context, str string) ([]string, error) {
 	return []string{"static-org", str}, nil
 }
 
 // GetAutocompleteUsers gets users for autocompletion
-func (vc *VersionController) GetAutocompleteUsers(ctx context.Context, str string) ([]string, error) {
+func (vc *VersionController) GetAutocompleteUsers(_ context.Context, str string) ([]string, error) {
 	return []string{"static-user", str}, nil
 }
 
 // GetAutocompleteRepositories gets repositories for autocompletion
-func (vc *VersionController) GetAutocompleteRepositories(ctx context.Context, str string) ([]string, error) {
+func (vc *VersionController) GetAutocompleteRepositories(_ context.Context, str string) ([]string, error) {
 	return []string{"static-repo", str}, nil
 }