Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 36 additions & 3 deletions cmd/clone.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ import (
)

var (
topics []string
topics []string
excludeTopics []string
)

func init() {
RootCmd.AddCommand(cloneCmd)

cloneCmd.Flags().StringSliceVarP(&topics, "topics", "t", []string{}, "clone only repos with matching topics")
cloneCmd.Flags().StringSliceVarP(&excludeTopics, "exclude-topics", "e", []string{}, "skip repos that have any of the specified topics")
}

var cloneCmd = &cobra.Command{
Expand All @@ -27,8 +29,9 @@ var cloneCmd = &cobra.Command{
Long: `Clone all active (non-archived) repositories from a GitHub org or user into the target directory.
The optional directory argument specifies where to clone the repos (defaults to the current directory).

When --topics is provided multiple times, only repos that have ALL specified topics are cloned (AND logic).
To clone repos matching any one topic, run separate clone invocations per topic.`,
When --topics is provided, only repos that have ALL specified topics are cloned (AND logic).
When --exclude-topics is provided, repos that have ANY of the specified topics are skipped.
Both flags may be combined: clone repos with topic A but not topic B.`,
Args: cobra.MaximumNArgs(2),
ValidArgsFunction: createCmdValidArgsFunc,
PersistentPreRun: setupClient,
Expand Down Expand Up @@ -61,6 +64,7 @@ func cloneFunc(cmd *cobra.Command, args []string) error {
}

repos = filterByTopics(repos, topics)
repos = excludeByTopics(repos, excludeTopics)

ctx = ctxhelper.WithRepos(ctx, repos)

Expand Down Expand Up @@ -112,3 +116,32 @@ func filterByTopics(repos []*github.Repository, topics []string) []*github.Repos

return filtered
}

func excludeByTopics(repos []*github.Repository, excludeTopics []string) []*github.Repository {
if len(excludeTopics) == 0 {
return repos
}

excludeSet := make(map[string]struct{}, len(excludeTopics))
for _, t := range excludeTopics {
excludeSet[t] = struct{}{}
}

var filtered []*github.Repository

for _, r := range repos {
excluded := false
for _, t := range r.Topics {
if _, ok := excludeSet[t]; ok {
excluded = true
break
}
}

if !excluded {
filtered = append(filtered, r)
}
}

return filtered
}
58 changes: 58 additions & 0 deletions cmd/clone_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,64 @@ func TestClone(t *testing.T) {
})
}

func TestExcludeByTopics(t *testing.T) {
repoWithTopics := func(topics ...string) *github.Repository {
return &github.Repository{Topics: topics}
}

t.Run("returns all repos when no exclude topics", func(t *testing.T) {
t.Parallel()

repos := []*github.Repository{
repoWithTopics("go", "cli"),
repoWithTopics("rust"),
}

result := excludeByTopics(repos, []string{})
assert.Equal(t, repos, result)
})

t.Run("excludes repos with matching topic", func(t *testing.T) {
t.Parallel()

keep := repoWithTopics("go", "cli")
skip := repoWithTopics("rust")

result := excludeByTopics([]*github.Repository{keep, skip}, []string{"rust"})
assert.Equal(t, []*github.Repository{keep}, result)
})

t.Run("excludes repos matching any excluded topic", func(t *testing.T) {
t.Parallel()

keep := repoWithTopics("go")
skipA := repoWithTopics("rust")
skipB := repoWithTopics("python")

result := excludeByTopics([]*github.Repository{keep, skipA, skipB}, []string{"rust", "python"})
assert.Equal(t, []*github.Repository{keep}, result)
})

t.Run("returns nil when all repos excluded", func(t *testing.T) {
t.Parallel()

repos := []*github.Repository{
repoWithTopics("rust"),
}

result := excludeByTopics(repos, []string{"rust"})
assert.Nil(t, result)
})
}

func TestFilterByTopics(t *testing.T) {
repoWithTopics := func(topics ...string) *github.Repository {
return &github.Repository{Topics: topics}
}

t.Run("returns all repos when no topics filter", func(t *testing.T) {
t.Parallel()

repos := []*github.Repository{
repoWithTopics("go", "cli"),
repoWithTopics("rust"),
Expand All @@ -73,6 +125,8 @@ func TestFilterByTopics(t *testing.T) {
})

t.Run("filters repos by single topic", func(t *testing.T) {
t.Parallel()

match := repoWithTopics("go", "cli")
noMatch := repoWithTopics("rust")

Expand All @@ -81,6 +135,8 @@ func TestFilterByTopics(t *testing.T) {
})

t.Run("filters repos requiring all topics present", func(t *testing.T) {
t.Parallel()

both := repoWithTopics("go", "cli")
onlyOne := repoWithTopics("go")

Expand All @@ -89,6 +145,8 @@ func TestFilterByTopics(t *testing.T) {
})

t.Run("returns nil when no repos match", func(t *testing.T) {
t.Parallel()

repos := []*github.Repository{
repoWithTopics("rust"),
}
Expand Down
Loading