From 93c8eef876c481c6fd841756026010fe6fa6e2dc Mon Sep 17 00:00:00 2001 From: Virgil Date: Wed, 1 Apr 2026 09:02:04 +0000 Subject: [PATCH] feat(dev): support glob targets in apply command Co-Authored-By: Virgil --- cmd/dev/cmd_apply.go | 52 +++++++++++++++++++++++---------------- cmd/dev/cmd_apply_test.go | 39 +++++++++++++++++++++++++++++ cmd/setup/cmd_ci_test.go | 2 +- cmd/setup/cmd_wizard.go | 10 ++++---- locales/en.json | 2 +- 5 files changed, 77 insertions(+), 28 deletions(-) create mode 100644 cmd/dev/cmd_apply_test.go diff --git a/cmd/dev/cmd_apply.go b/cmd/dev/cmd_apply.go index 0a712a5..de00f54 100644 --- a/cmd/dev/cmd_apply.go +++ b/cmd/dev/cmd_apply.go @@ -12,14 +12,14 @@ import ( "os" "os/exec" "path/filepath" - "strings" + "sort" - "forge.lthn.ai/core/cli/pkg/cli" - core "dappco.re/go/core/log" - "dappco.re/go/core/scm/git" "dappco.re/go/core/i18n" "dappco.re/go/core/io" + core "dappco.re/go/core/log" + "dappco.re/go/core/scm/git" "dappco.re/go/core/scm/repos" + "forge.lthn.ai/core/cli/pkg/cli" ) // Apply command flags @@ -235,29 +235,39 @@ func getApplyTargetRepos() ([]*repos.Repo, error) { return nil, core.E("dev.apply", "failed to load registry", err) } - // If --repos specified, filter to those - if applyRepos != "" { - repoNames := strings.Split(applyRepos, ",") - nameSet := make(map[string]bool) - for _, name := range repoNames { - nameSet[strings.TrimSpace(name)] = true - } + return filterTargetRepos(registry, applyRepos), nil +} - var matched []*repos.Repo - for _, repo := range registry.Repos { - if nameSet[repo.Name] { +// filterTargetRepos selects repos by exact name/path or glob pattern. +func filterTargetRepos(registry *repos.Registry, selection string) []*repos.Repo { + repoNames := make([]string, 0, len(registry.Repos)) + for name := range registry.Repos { + repoNames = append(repoNames, name) + } + sort.Strings(repoNames) + + if selection == "" { + matched := make([]*repos.Repo, 0, len(repoNames)) + for _, name := range repoNames { + matched = append(matched, registry.Repos[name]) + } + return matched + } + + patterns := splitPatterns(selection) + var matched []*repos.Repo + + for _, name := range repoNames { + repo := registry.Repos[name] + for _, candidate := range patterns { + if matchGlob(repo.Name, candidate) || matchGlob(repo.Path, candidate) { matched = append(matched, repo) + break } } - return matched, nil } - // Return all repos as slice - var all []*repos.Repo - for _, repo := range registry.Repos { - all = append(all, repo) - } - return all, nil + return matched } // runCommandInRepo runs a shell command in a repo directory diff --git a/cmd/dev/cmd_apply_test.go b/cmd/dev/cmd_apply_test.go new file mode 100644 index 0000000..4d3124d --- /dev/null +++ b/cmd/dev/cmd_apply_test.go @@ -0,0 +1,39 @@ +package dev + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "dappco.re/go/core/scm/repos" +) + +func TestFilterTargetRepos_Good(t *testing.T) { + registry := &repos.Registry{ + Repos: map[string]*repos.Repo{ + "core-api": &repos.Repo{Name: "core-api", Path: "packages/core-api"}, + "core-web": &repos.Repo{Name: "core-web", Path: "packages/core-web"}, + "docs-site": &repos.Repo{Name: "docs-site", Path: "sites/docs"}, + }, + } + + t.Run("exact names", func(t *testing.T) { + matched := filterTargetRepos(registry, "core-api,docs-site") + require.Len(t, matched, 2) + require.Equal(t, "core-api", matched[0].Name) + require.Equal(t, "docs-site", matched[1].Name) + }) + + t.Run("glob patterns", func(t *testing.T) { + matched := filterTargetRepos(registry, "core-*,sites/*") + require.Len(t, matched, 3) + require.Equal(t, "core-api", matched[0].Name) + require.Equal(t, "core-web", matched[1].Name) + require.Equal(t, "docs-site", matched[2].Name) + }) + + t.Run("all repos when empty", func(t *testing.T) { + matched := filterTargetRepos(registry, "") + require.Len(t, matched, 3) + }) +} diff --git a/cmd/setup/cmd_ci_test.go b/cmd/setup/cmd_ci_test.go index de75858..f781f35 100644 --- a/cmd/setup/cmd_ci_test.go +++ b/cmd/setup/cmd_ci_test.go @@ -59,6 +59,6 @@ func TestOutputPowershellInstall_Good(t *testing.T) { return outputPowershellInstall(DefaultCIConfig(), "dev") }) require.NoError(t, err) - require.Contains(t, out, `scoop bucket add host-uk https://forge.lthn.ai/core/scoop-bucket.git`) + require.Contains(t, out, `scoop bucket add host-uk $ScoopBucket`) require.NotContains(t, out, `https://https://forge.lthn.ai/core/scoop-bucket.git`) } diff --git a/cmd/setup/cmd_wizard.go b/cmd/setup/cmd_wizard.go index 4174565..250ed8e 100644 --- a/cmd/setup/cmd_wizard.go +++ b/cmd/setup/cmd_wizard.go @@ -94,9 +94,9 @@ func runPackageWizard(reg *repos.Registry, preselectedTypes []string) ([]string, return selected, nil } -func filterReposByTypes(repos []*repos.Repo, allowedTypes []string) []*repos.Repo { +func filterReposByTypes(repoList []*repos.Repo, allowedTypes []string) []*repos.Repo { if len(allowedTypes) == 0 { - return repos + return repoList } allowed := make(map[string]struct{}, len(allowedTypes)) @@ -108,11 +108,11 @@ func filterReposByTypes(repos []*repos.Repo, allowedTypes []string) []*repos.Rep } if len(allowed) == 0 { - return repos + return repoList } - filtered := make([]*repos.Repo, 0, len(repos)) - for _, repo := range repos { + filtered := make([]*repos.Repo, 0, len(repoList)) + for _, repo := range repoList { if _, ok := allowed[repo.Type]; ok { filtered = append(filtered, repo) } diff --git a/locales/en.json b/locales/en.json index 090a768..59d9e39 100644 --- a/locales/en.json +++ b/locales/en.json @@ -151,7 +151,7 @@ "flag": { "command": "Shell command to run in each repo", "script": "Script file to run in each repo", - "repos": "Comma-separated list of repo names to target", + "repos": "Comma-separated list of repo names, paths, or glob patterns to target", "commit": "Commit changes after running", "message": "Commit message (required with --commit)", "push": "Push after committing",