diff --git a/cmd/cmd-run.go b/cmd/cmd-run.go index 031eb335..d4ed2478 100755 --- a/cmd/cmd-run.go +++ b/cmd/cmd-run.go @@ -2,16 +2,17 @@ package cmd import ( "context" - "errors" "fmt" "os" "os/signal" + "regexp" "strings" "syscall" "github.com/lindell/multi-gitter/internal/git" "github.com/lindell/multi-gitter/internal/multigitter" + "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -62,6 +63,8 @@ Available values: cmd.Flags().StringSliceP("labels", "", nil, "Labels to be added to any created pull request.") cmd.Flags().StringP("author-name", "", "", "Name of the committer. If not set, the global git config setting will be used.") cmd.Flags().StringP("author-email", "", "", "Email of the committer. If not set, the global git config setting will be used.") + cmd.Flags().StringP("repo-include", "", "", "Include repositories that match with a given Regular Expression") + cmd.Flags().StringP("repo-exclude", "", "", "Exclude repositories that match with a given Regular Expression") configureGit(cmd) configurePlatform(cmd) configureRunPlatform(cmd, true) @@ -98,6 +101,8 @@ func run(cmd *cobra.Command, _ []string) error { assignees, _ := flag.GetStringSlice("assignees") draft, _ := flag.GetBool("draft") labels, _ := flag.GetStringSlice("labels") + repoInclude, _ := flag.GetString("repo-include") + repoExclude, _ := flag.GetString("repo-exclude") if concurrent < 1 { return errors.New("concurrent runs can't be less than one") @@ -144,6 +149,23 @@ func run(cmd *cobra.Command, _ []string) error { } } + var regExIncludeRepository *regexp.Regexp + var regExExcludeRepository *regexp.Regexp + if repoInclude != "" { + repoIncludeFilterCompile, err := regexp.Compile(repoInclude) + if err != nil { + return errors.WithMessage(err, "could not parse repo-include") + } + regExIncludeRepository = repoIncludeFilterCompile + } + if repoExclude != "" { + repoExcludeFilterCompile, err := regexp.Compile(repoExclude) + if err != nil { + return errors.WithMessage(err, "could not parse repo-exclude") + } + regExExcludeRepository = repoExcludeFilterCompile + } + vc, err := getVersionController(flag, true, false) if err != nil { return err @@ -185,25 +207,27 @@ func run(cmd *cobra.Command, _ []string) error { VersionController: vc, - CommitMessage: commitMessage, - PullRequestTitle: prTitle, - PullRequestBody: prBody, - Reviewers: reviewers, - TeamReviewers: teamReviewers, - MaxReviewers: maxReviewers, - MaxTeamReviewers: maxTeamReviewers, - Interactive: interactive, - DryRun: dryRun, - Fork: forkMode, - ForkOwner: forkOwner, - SkipPullRequest: skipPullRequest, - SkipRepository: skipRepository, - CommitAuthor: commitAuthor, - BaseBranch: baseBranchName, - Assignees: assignees, - ConflictStrategy: conflictStrategy, - Draft: draft, - Labels: labels, + CommitMessage: commitMessage, + PullRequestTitle: prTitle, + PullRequestBody: prBody, + Reviewers: reviewers, + TeamReviewers: teamReviewers, + MaxReviewers: maxReviewers, + MaxTeamReviewers: maxTeamReviewers, + Interactive: interactive, + DryRun: dryRun, + RegExIncludeRepository: regExIncludeRepository, + RegExExcludeRepository: regExExcludeRepository, + Fork: forkMode, + ForkOwner: forkOwner, + SkipPullRequest: skipPullRequest, + SkipRepository: skipRepository, + CommitAuthor: commitAuthor, + BaseBranch: baseBranchName, + Assignees: assignees, + ConflictStrategy: conflictStrategy, + Draft: draft, + Labels: labels, Concurrent: concurrent, diff --git a/internal/multigitter/run.go b/internal/multigitter/run.go index 5fc82106..1d84dfb3 100755 --- a/internal/multigitter/run.go +++ b/internal/multigitter/run.go @@ -7,6 +7,7 @@ import ( "math/rand" "os" "os/exec" + "regexp" "sync" "syscall" @@ -54,9 +55,11 @@ type Runner struct { BaseBranch string // The base branch of the PR, use default branch if not set Assignees []string - Concurrent int - SkipPullRequest bool // If set, the script will run directly on the base-branch without creating any PR - SkipRepository []string // A list of repositories that run will skip + Concurrent int + SkipPullRequest bool // If set, the script will run directly on the base-branch without creating any PR + SkipRepository []string // A list of repositories that run will skip + RegExIncludeRepository *regexp.Regexp + RegExExcludeRepository *regexp.Regexp Fork bool // If set, create a fork and make the pull request from it ForkOwner string // The owner of the new fork. If empty, the fork should happen on the logged in user @@ -98,7 +101,7 @@ func (r *Runner) Run(ctx context.Context) error { return errors.Wrap(err, "could not fetch repositories") } - repos = filterRepositories(repos, r.SkipRepository) + repos = filterRepositories(repos, r.SkipRepository, r.RegExIncludeRepository, r.RegExExcludeRepository) if len(repos) == 0 { log.Infof("No repositories found. Please make sure the user of the token has the correct access to the repos you want to change.") @@ -152,7 +155,24 @@ func (r *Runner) Run(ctx context.Context) error { return nil } -func filterRepositories(repos []scm.Repository, skipRepositoryNames []string) []scm.Repository { +// Determines if Repository should be excluded based on provided Regular Expression +func excludeRepositoryFilter(repoName string, regExp *regexp.Regexp) bool { + if regExp == nil { + return false + } + return regExp.MatchString(repoName) +} + +// Determines if Repository should be included based on provided Regular Expression +func matchesRepositoryFilter(repoName string, regExp *regexp.Regexp) bool { + if regExp == nil { + return true + } + return regExp.MatchString(repoName) +} + +func filterRepositories(repos []scm.Repository, skipRepositoryNames []string, regExIncludeRepository *regexp.Regexp, + regExExcludeRepository *regexp.Regexp) []scm.Repository { skipReposMap := map[string]struct{}{} for _, skipRepo := range skipRepositoryNames { skipReposMap[skipRepo] = struct{}{} @@ -160,10 +180,14 @@ func filterRepositories(repos []scm.Repository, skipRepositoryNames []string) [] filteredRepos := make([]scm.Repository, 0, len(repos)) for _, r := range repos { - if _, shouldSkip := skipReposMap[r.FullName()]; !shouldSkip { - filteredRepos = append(filteredRepos, r) + if _, shouldSkip := skipReposMap[r.FullName()]; shouldSkip { + log.Infof("Skipping %s since it is in exclusion list", r.FullName()) + } else if !matchesRepositoryFilter(r.FullName(), regExIncludeRepository) { + log.Infof("Skipping %s since it does not match the inclusion regexp", r.FullName()) + } else if excludeRepositoryFilter(r.FullName(), regExExcludeRepository) { + log.Infof("Skipping %s since it match the exclusion regexp", r.FullName()) } else { - log.Infof("Skipping %s", r.FullName()) + filteredRepos = append(filteredRepos, r) } } return filteredRepos diff --git a/tests/table_test.go b/tests/table_test.go index a6199e36..d6462911 100644 --- a/tests/table_test.go +++ b/tests/table_test.go @@ -336,6 +336,108 @@ func TestTable(t *testing.T) { assert.False(t, branchExist(t, vcMock.Repositories[0].Path, "custom-branch-name")) }, }, + { + name: "repo-include regex repository filtering", + vcCreate: func(t *testing.T) *vcmock.VersionController { + return &vcmock.VersionController{ + Repositories: []vcmock.Repository{ + createRepo(t, "owner", "repo1", "i like apples"), + createRepo(t, "owner", "repo-2", "i like oranges"), + createRepo(t, "owner", "repo-change", "i like carrots"), + createRepo(t, "owner", "repo-3", "i like carrots"), + }, + } + }, + args: []string{ + "run", + "--repo-search", "repo", + "--repo-include", "^owner/repo-", + "--commit-message", "chore: foo", + "--dry-run", + changerBinaryPath, + }, + verify: func(t *testing.T, vcMock *vcmock.VersionController, runData runData) { + require.Len(t, vcMock.PullRequests, 0) + assert.Contains(t, runData.logOut, "Running on 3 repositories") + }, + }, + { + name: "repo-exclude regex repository filtering", + vcCreate: func(t *testing.T) *vcmock.VersionController { + return &vcmock.VersionController{ + Repositories: []vcmock.Repository{ + createRepo(t, "owner", "repo1", "i like apples"), + createRepo(t, "owner", "repo-2", "i like oranges"), + createRepo(t, "owner", "repo-change", "i like carrots"), + createRepo(t, "owner", "repo-3", "i like carrots"), + }, + } + }, + args: []string{ + "run", + "--repo-search", "repo", + "--repo-exclude", "\\d$", + "--commit-message", "chore: foo", + "--dry-run", + changerBinaryPath, + }, + verify: func(t *testing.T, vcMock *vcmock.VersionController, runData runData) { + require.Len(t, vcMock.PullRequests, 0) + assert.Contains(t, runData.logOut, "Running on 1 repositories") + }, + }, + { + name: "invalid repo-include regex repository filtering", + vcCreate: func(t *testing.T) *vcmock.VersionController { + return &vcmock.VersionController{ + Repositories: []vcmock.Repository{ + createRepo(t, "owner", "repo1", "i like apples"), + createRepo(t, "owner", "repo-2", "i like oranges"), + createRepo(t, "owner", "repo-change", "i like carrots"), + createRepo(t, "owner", "repo-3", "i like carrots"), + }, + } + }, + args: []string{ + "run", + "--repo-search", "repo", + "--repo-include", "(abc[def$", + "--commit-message", "chore: foo", + "--dry-run", + changerBinaryPath, + }, + verify: func(t *testing.T, vcMock *vcmock.VersionController, runData runData) { + require.Len(t, vcMock.PullRequests, 0) + assert.Contains(t, runData.cmdOut, "could not parse repo-include") + }, + expectErr: true, + }, + { + name: "invalid repo-exclude regex repository filtering", + vcCreate: func(t *testing.T) *vcmock.VersionController { + return &vcmock.VersionController{ + Repositories: []vcmock.Repository{ + createRepo(t, "owner", "repo1", "i like apples"), + createRepo(t, "owner", "repo-2", "i like oranges"), + createRepo(t, "owner", "repo-change", "i like carrots"), + createRepo(t, "owner", "repo-3", "i like carrots"), + }, + } + }, + args: []string{ + "run", + "--repo-search", "repo", + "--repo-exclude", "(abc[def$", + "--commit-message", "chore: foo", + "--dry-run", + changerBinaryPath, + }, + verify: func(t *testing.T, vcMock *vcmock.VersionController, runData runData) { + require.Len(t, vcMock.PullRequests, 0) + assert.Contains(t, runData.cmdOut, "could not parse repo-exclude") + }, + expectErr: true, + }, { name: "parallel",