Repository: lindell/multi-gitter
Pull request #341 (2023-04-07 14:02:50)

feat: filter repositories that are forks
# What does this change

Add support for filtering out repositories that are forks using `--skip-forks`.

# What issue does it fix

See https://github.com/lindell/multi-gitter/discussions/340.

# Notes for the reviewer

None.

# Checklist

- [x] Made sure the PR follows the [CONTRIBUTING.md](CONTRIBUTING.md) guidelines
- [x] 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/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/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/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 {


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
 }