From 818bd64c66e52ad86b5f89311f23f20ec41c09fb Mon Sep 17 00:00:00 2001 From: RW Date: Tue, 26 Aug 2025 22:35:34 +0200 Subject: [PATCH 1/2] test: add include/exclude tests --- cmd/internal/migrations/filter_test.go | 55 ++++++++++++++++++++++++++ cmd/internal/migrations/lists.go | 43 ++++++++++++++++++-- cmd/internal/migrations/lists_test.go | 8 ++-- cmd/migrate.go | 10 ++++- docs/guide/migrate.md | 2 + 5 files changed, 110 insertions(+), 8 deletions(-) create mode 100644 cmd/internal/migrations/filter_test.go diff --git a/cmd/internal/migrations/filter_test.go b/cmd/internal/migrations/filter_test.go new file mode 100644 index 0000000..f885251 --- /dev/null +++ b/cmd/internal/migrations/filter_test.go @@ -0,0 +1,55 @@ +package migrations_test + +import ( + "bytes" + "testing" + + semver "github.com/Masterminds/semver/v3" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/gofiber/cli/cmd/internal/migrations" +) + +func fnFoo(cmd *cobra.Command, cwd string, curr, target *semver.Version) error { + cmd.Println("foo") + return nil +} + +func fnBar(cmd *cobra.Command, cwd string, curr, target *semver.Version) error { + cmd.Println("bar") + return nil +} + +func Test_DoMigration_IncludeExclude(t *testing.T) { + orig := migrations.Migrations + migrations.Migrations = []migrations.Migration{ + {From: ">=0.0.0", To: ">=0.0.0", Functions: []migrations.MigrationFn{fnFoo, fnBar}}, + } + t.Cleanup(func() { migrations.Migrations = orig }) + + dir := t.TempDir() + curr := semver.MustParse("0.0.0") + target := semver.MustParse("1.0.0") + + t.Run("include glob", func(t *testing.T) { + var buf bytes.Buffer + cmd := &cobra.Command{} + cmd.SetOut(&buf) + require.NoError(t, migrations.DoMigration(cmd, dir, curr, target, true, false, []string{"fnF*"}, nil)) + out := buf.String() + assert.Contains(t, out, "foo") + assert.NotContains(t, out, "bar") + }) + + t.Run("exclude regex", func(t *testing.T) { + var buf bytes.Buffer + cmd := &cobra.Command{} + cmd.SetOut(&buf) + require.NoError(t, migrations.DoMigration(cmd, dir, curr, target, true, false, nil, []string{"^fnB.*"})) + out := buf.String() + assert.Contains(t, out, "foo") + assert.NotContains(t, out, "bar") + }) +} diff --git a/cmd/internal/migrations/lists.go b/cmd/internal/migrations/lists.go index a2c43fd..2b2e3e2 100644 --- a/cmd/internal/migrations/lists.go +++ b/cmd/internal/migrations/lists.go @@ -5,7 +5,9 @@ import ( "errors" "fmt" "io" + "path/filepath" "reflect" + "regexp" "runtime" "strings" @@ -90,9 +92,37 @@ func migrationName(fn MigrationFn) string { return f } +func matchPatternList(name string, patterns []string) bool { + for _, p := range patterns { + if matchPattern(name, p) { + return true + } + } + return false +} + +func matchPattern(name, pattern string) bool { + if ok, err := filepath.Match(pattern, name); err == nil { + if ok { + return true + } + if !isRegexPattern(pattern) { + return false + } + } + if re, err := regexp.Compile(pattern); err == nil { + return re.MatchString(name) + } + return name == pattern +} + +func isRegexPattern(p string) bool { + return strings.ContainsAny(p, "^$[]()|+?\\") +} + // DoMigration runs all migrations // It will run all migrations that match the current and target version -func DoMigration(cmd *cobra.Command, cwd string, curr, target *semver.Version, skipGoMod, verbose bool) error { +func DoMigration(cmd *cobra.Command, cwd string, curr, target *semver.Version, skipGoMod, verbose bool, include, exclude []string) error { var errs []error var origDeps map[string]map[string]*semver.Version if !skipGoMod { @@ -114,13 +144,20 @@ func DoMigration(cmd *cobra.Command, cwd string, curr, target *semver.Version, s if fromC.Check(curr) && toC.Check(target) { for _, fn := range m.Functions { + name := migrationName(fn) + if len(include) > 0 && !matchPatternList(name, include) { + continue + } + if len(exclude) > 0 && matchPatternList(name, exclude) { + continue + } + if verbose { var buf bytes.Buffer origOut := cmd.OutOrStdout() cmd.SetOut(io.MultiWriter(origOut, &buf)) err := fn(cmd, cwd, curr, target) cmd.SetOut(origOut) - name := migrationName(fn) if buf.Len() == 0 { cmd.Printf("%s: no changes\n", name) } else { @@ -131,7 +168,7 @@ func DoMigration(cmd *cobra.Command, cwd string, curr, target *semver.Version, s } } else { if err := fn(cmd, cwd, curr, target); err != nil { - errs = append(errs, fmt.Errorf("%s: %w", migrationName(fn), err)) + errs = append(errs, fmt.Errorf("%s: %w", name, err)) } } } diff --git a/cmd/internal/migrations/lists_test.go b/cmd/internal/migrations/lists_test.go index 07a8f58..40e48a4 100644 --- a/cmd/internal/migrations/lists_test.go +++ b/cmd/internal/migrations/lists_test.go @@ -26,7 +26,7 @@ func Test_DoMigration_Verbose(t *testing.T) { var buf bytes.Buffer cmd := &cobra.Command{} cmd.SetOut(&buf) - require.NoError(t, migrations.DoMigration(cmd, dir, curr, target, true, false)) + require.NoError(t, migrations.DoMigration(cmd, dir, curr, target, true, false, nil, nil)) assert.Equal(t, "", buf.String()) }) @@ -36,7 +36,7 @@ func Test_DoMigration_Verbose(t *testing.T) { var buf bytes.Buffer cmd := &cobra.Command{} cmd.SetOut(&buf) - require.NoError(t, migrations.DoMigration(cmd, dir, curr, target, true, true)) + require.NoError(t, migrations.DoMigration(cmd, dir, curr, target, true, true, nil, nil)) out := buf.String() assert.Contains(t, out, "Skipping migration from >=1.0.0-0 to >=0.0.0-0") assert.Contains(t, out, "Skipping migration from >=2.0.0-0 to <4.0.0-0") @@ -64,7 +64,7 @@ require github.com/valyala/fasthttp v1.0.0` var buf bytes.Buffer cmd := &cobra.Command{} cmd.SetOut(&buf) - require.NoError(t, migrations.DoMigration(cmd, dir, curr, target, true, true)) + require.NoError(t, migrations.DoMigration(cmd, dir, curr, target, true, true, nil, nil)) out := buf.String() assert.Contains(t, out, "MigrateGoPkgs: no changes") assert.Contains(t, out, "Skipping migration from >=2.0.0-0 to <4.0.0-0") @@ -88,7 +88,7 @@ require github.com/valyala/fasthttp v1.0.0` var buf bytes.Buffer cmd := &cobra.Command{} cmd.SetOut(&buf) - require.NoError(t, migrations.DoMigration(cmd, dir, curr, target, true, true)) + require.NoError(t, migrations.DoMigration(cmd, dir, curr, target, true, true, nil, nil)) out := buf.String() assert.Contains(t, out, "Migrating Go packages") assert.Contains(t, out, "MigrateGoPkgs: changed") diff --git a/cmd/migrate.go b/cmd/migrate.go index bfa33a3..fdbbde8 100644 --- a/cmd/migrate.go +++ b/cmd/migrate.go @@ -27,6 +27,8 @@ func newMigrateCmd() *cobra.Command { var skipGoMod bool var verbose bool var thirdParty []string + var include []string + var exclude []string cmd := &cobra.Command{ Use: "migrate", @@ -39,6 +41,8 @@ func newMigrateCmd() *cobra.Command { cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose output") cmd.Flags().StringVar(&targetHash, "hash", "", "Commit hash for Fiber version") cmd.Flags().StringSliceVar(&thirdParty, "third-party", nil, "Refresh third-party modules (contrib,storage,template). Use a comma-separated list like --third-party=contrib,storage and append @ to pin a commit") + cmd.Flags().StringSliceVar(&include, "include", nil, "Comma-separated list of migrations to include. Supports glob and regex patterns") + cmd.Flags().StringSliceVar(&exclude, "exclude", nil, "Comma-separated list of migrations to exclude. Supports glob and regex patterns") cmd.RunE = func(cmd *cobra.Command, _ []string) error { tps := make([]ThirdPartyParam, 0, len(thirdParty)) @@ -61,6 +65,8 @@ func newMigrateCmd() *cobra.Command { SkipGoMod: skipGoMod, Verbose: verbose, ThirdParty: tps, + Include: include, + Exclude: exclude, }) } @@ -74,6 +80,8 @@ type MigrateOptions struct { TargetVersionS string TargetHash string ThirdParty []ThirdPartyParam + Include []string + Exclude []string Force bool SkipGoMod bool Verbose bool @@ -138,7 +146,7 @@ func migrateRunE(cmd *cobra.Command, opts MigrateOptions) error { migrateFromS = migrateFrom.String() } - err = migrations.DoMigration(cmd, wd, migrateFrom, targetVersion, opts.SkipGoMod, opts.Verbose) + err = migrations.DoMigration(cmd, wd, migrateFrom, targetVersion, opts.SkipGoMod, opts.Verbose, opts.Include, opts.Exclude) if err != nil { return fmt.Errorf("migration failed %w", err) } diff --git a/docs/guide/migrate.md b/docs/guide/migrate.md index 4fc1b5c..4fb95ee 100644 --- a/docs/guide/migrate.md +++ b/docs/guide/migrate.md @@ -23,6 +23,8 @@ fiber migrate --to 3.0.0 - `-f`, `--force` – Force migration even if already on the target version - `-s`, `--skip_go_mod` – Skip running `go mod tidy`, `go mod download`, and `go mod vendor` - `-v`, `--verbose` – Enable verbose output during migration +- `--include` – Comma-separated list of migrations to run. Supports glob and regex patterns +- `--exclude` – Comma-separated list of migrations to skip. Supports glob and regex patterns ## Examples From e4ddfa3658267dd9a2f6ddf4a86ba49cb5466974 Mon Sep 17 00:00:00 2001 From: RW Date: Wed, 27 Aug 2025 13:18:20 +0200 Subject: [PATCH 2/2] feat: filter migrated files --- cmd/internal/helpers.go | 59 +++++++++++++++++ cmd/internal/migrations/file_filter_test.go | 70 +++++++++++++++++++++ cmd/internal/migrations/filter_test.go | 55 ---------------- cmd/internal/migrations/lists.go | 41 +----------- cmd/migrate.go | 18 +++--- docs/guide/migrate.md | 10 ++- 6 files changed, 149 insertions(+), 104 deletions(-) create mode 100644 cmd/internal/migrations/file_filter_test.go delete mode 100644 cmd/internal/migrations/filter_test.go diff --git a/cmd/internal/helpers.go b/cmd/internal/helpers.go index 1935f63..f80a944 100644 --- a/cmd/internal/helpers.go +++ b/cmd/internal/helpers.go @@ -4,7 +4,9 @@ import ( "fmt" "os" "path/filepath" + "regexp" "strings" + "sync" "github.com/containerd/console" "github.com/muesli/termenv" @@ -32,6 +34,20 @@ func checkConsole() (size console.WinSize, err error) { // FileProcessor processes the file content and returns the modified content. type FileProcessor func(content string) string +var ( + fileIncludePatterns []string + fileExcludePatterns []string + fileFilterMu sync.RWMutex +) + +// SetFileFilters sets the include and exclude patterns used by ChangeFileContent. +func SetFileFilters(include, exclude []string) { + fileFilterMu.Lock() + fileIncludePatterns = include + fileExcludePatterns = exclude + fileFilterMu.Unlock() +} + // ChangeFileContent walks through cwd and applies the processorFn to every Go // file found. Files in a vendor directory are skipped. It returns true if any // file content was modified. @@ -51,6 +67,21 @@ func ChangeFileContent(cwd string, processorFn FileProcessor) (bool, error) { if info.IsDir() || !strings.HasSuffix(info.Name(), ".go") { return nil } + + rel, err := filepath.Rel(cwd, path) + if err != nil { + rel = path + } + fileFilterMu.RLock() + include := fileIncludePatterns + exclude := fileExcludePatterns + fileFilterMu.RUnlock() + if len(include) > 0 && !matchPatternList(rel, include) { + return nil + } + if len(exclude) > 0 && matchPatternList(rel, exclude) { + return nil + } fileContent, err := os.ReadFile(path) // #nosec G304 if err != nil { return fmt.Errorf("read file %s: %w", path, err) @@ -77,3 +108,31 @@ func ChangeFileContent(cwd string, processorFn FileProcessor) (bool, error) { return changed, nil } + +func matchPatternList(name string, patterns []string) bool { + for _, p := range patterns { + if matchPattern(name, p) { + return true + } + } + return false +} + +func matchPattern(name, pattern string) bool { + if ok, err := filepath.Match(pattern, name); err == nil { + if ok { + return true + } + if !isRegexPattern(pattern) { + return false + } + } + if re, err := regexp.Compile(pattern); err == nil { + return re.MatchString(name) + } + return name == pattern +} + +func isRegexPattern(p string) bool { + return strings.ContainsAny(p, "^$[]()|+?\\") +} diff --git a/cmd/internal/migrations/file_filter_test.go b/cmd/internal/migrations/file_filter_test.go new file mode 100644 index 0000000..355b35a --- /dev/null +++ b/cmd/internal/migrations/file_filter_test.go @@ -0,0 +1,70 @@ +package migrations_test + +import ( + "bytes" + "os" + "path/filepath" + "strings" + "testing" + + semver "github.com/Masterminds/semver/v3" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/gofiber/cli/cmd/internal" + "github.com/gofiber/cli/cmd/internal/migrations" +) + +func replaceFoo(cmd *cobra.Command, cwd string, curr, target *semver.Version) error { + changed, err := internal.ChangeFileContent(cwd, func(content string) string { + return strings.ReplaceAll(content, "foo", "bar") + }) + if changed { + cmd.Println("replaceFoo") + } + return err //nolint:wrapcheck // returning raw error is fine for tests +} + +func Test_DoMigration_FileIncludeExclude(t *testing.T) { + orig := migrations.Migrations + migrations.Migrations = []migrations.Migration{ + {From: ">=0.0.0", To: ">=0.0.0", Functions: []migrations.MigrationFn{replaceFoo}}, + } + t.Cleanup(func() { migrations.Migrations = orig }) + + curr := semver.MustParse("0.0.0") + target := semver.MustParse("1.0.0") + + t.Run("include glob", func(t *testing.T) { + dir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(dir, "foo.go"), []byte("package main\nvar foo = 1\n"), 0o600)) + require.NoError(t, os.WriteFile(filepath.Join(dir, "bar.go"), []byte("package main\nvar foo = 1\n"), 0o600)) + var buf bytes.Buffer + cmd := &cobra.Command{} + cmd.SetOut(&buf) + require.NoError(t, migrations.DoMigration(cmd, dir, curr, target, true, false, []string{"*foo.go"}, nil)) + b, err := os.ReadFile(filepath.Join(dir, "foo.go")) // #nosec G304 + require.NoError(t, err) + assert.Contains(t, string(b), "var bar = 1") + b, err = os.ReadFile(filepath.Join(dir, "bar.go")) // #nosec G304 + require.NoError(t, err) + assert.Contains(t, string(b), "var foo = 1") + }) + + t.Run("exclude regex", func(t *testing.T) { + dir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(dir, "foo.go"), []byte("package main\nvar foo = 1\n"), 0o600)) + require.NoError(t, os.WriteFile(filepath.Join(dir, "bar.go"), []byte("package main\nvar foo = 1\n"), 0o600)) + var buf bytes.Buffer + cmd := &cobra.Command{} + cmd.SetOut(&buf) + require.NoError(t, migrations.DoMigration(cmd, dir, curr, target, true, false, nil, []string{"^bar\\.go$"})) + b, err := os.ReadFile(filepath.Join(dir, "foo.go")) // #nosec G304 + require.NoError(t, err) + assert.Contains(t, string(b), "var bar = 1") + b, err = os.ReadFile(filepath.Join(dir, "bar.go")) // #nosec G304 + require.NoError(t, err) + assert.Contains(t, string(b), "var foo = 1") + }) +} diff --git a/cmd/internal/migrations/filter_test.go b/cmd/internal/migrations/filter_test.go deleted file mode 100644 index f885251..0000000 --- a/cmd/internal/migrations/filter_test.go +++ /dev/null @@ -1,55 +0,0 @@ -package migrations_test - -import ( - "bytes" - "testing" - - semver "github.com/Masterminds/semver/v3" - "github.com/spf13/cobra" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/gofiber/cli/cmd/internal/migrations" -) - -func fnFoo(cmd *cobra.Command, cwd string, curr, target *semver.Version) error { - cmd.Println("foo") - return nil -} - -func fnBar(cmd *cobra.Command, cwd string, curr, target *semver.Version) error { - cmd.Println("bar") - return nil -} - -func Test_DoMigration_IncludeExclude(t *testing.T) { - orig := migrations.Migrations - migrations.Migrations = []migrations.Migration{ - {From: ">=0.0.0", To: ">=0.0.0", Functions: []migrations.MigrationFn{fnFoo, fnBar}}, - } - t.Cleanup(func() { migrations.Migrations = orig }) - - dir := t.TempDir() - curr := semver.MustParse("0.0.0") - target := semver.MustParse("1.0.0") - - t.Run("include glob", func(t *testing.T) { - var buf bytes.Buffer - cmd := &cobra.Command{} - cmd.SetOut(&buf) - require.NoError(t, migrations.DoMigration(cmd, dir, curr, target, true, false, []string{"fnF*"}, nil)) - out := buf.String() - assert.Contains(t, out, "foo") - assert.NotContains(t, out, "bar") - }) - - t.Run("exclude regex", func(t *testing.T) { - var buf bytes.Buffer - cmd := &cobra.Command{} - cmd.SetOut(&buf) - require.NoError(t, migrations.DoMigration(cmd, dir, curr, target, true, false, nil, []string{"^fnB.*"})) - out := buf.String() - assert.Contains(t, out, "foo") - assert.NotContains(t, out, "bar") - }) -} diff --git a/cmd/internal/migrations/lists.go b/cmd/internal/migrations/lists.go index 2b2e3e2..c0f6149 100644 --- a/cmd/internal/migrations/lists.go +++ b/cmd/internal/migrations/lists.go @@ -5,9 +5,7 @@ import ( "errors" "fmt" "io" - "path/filepath" "reflect" - "regexp" "runtime" "strings" @@ -92,37 +90,11 @@ func migrationName(fn MigrationFn) string { return f } -func matchPatternList(name string, patterns []string) bool { - for _, p := range patterns { - if matchPattern(name, p) { - return true - } - } - return false -} - -func matchPattern(name, pattern string) bool { - if ok, err := filepath.Match(pattern, name); err == nil { - if ok { - return true - } - if !isRegexPattern(pattern) { - return false - } - } - if re, err := regexp.Compile(pattern); err == nil { - return re.MatchString(name) - } - return name == pattern -} - -func isRegexPattern(p string) bool { - return strings.ContainsAny(p, "^$[]()|+?\\") -} - // DoMigration runs all migrations // It will run all migrations that match the current and target version -func DoMigration(cmd *cobra.Command, cwd string, curr, target *semver.Version, skipGoMod, verbose bool, include, exclude []string) error { +func DoMigration(cmd *cobra.Command, cwd string, curr, target *semver.Version, skipGoMod, verbose bool, includeFiles, excludeFiles []string) error { + internal.SetFileFilters(includeFiles, excludeFiles) + defer internal.SetFileFilters(nil, nil) var errs []error var origDeps map[string]map[string]*semver.Version if !skipGoMod { @@ -145,13 +117,6 @@ func DoMigration(cmd *cobra.Command, cwd string, curr, target *semver.Version, s if fromC.Check(curr) && toC.Check(target) { for _, fn := range m.Functions { name := migrationName(fn) - if len(include) > 0 && !matchPatternList(name, include) { - continue - } - if len(exclude) > 0 && matchPatternList(name, exclude) { - continue - } - if verbose { var buf bytes.Buffer origOut := cmd.OutOrStdout() diff --git a/cmd/migrate.go b/cmd/migrate.go index fdbbde8..2ee2472 100644 --- a/cmd/migrate.go +++ b/cmd/migrate.go @@ -27,8 +27,8 @@ func newMigrateCmd() *cobra.Command { var skipGoMod bool var verbose bool var thirdParty []string - var include []string - var exclude []string + var includeFiles []string + var excludeFiles []string cmd := &cobra.Command{ Use: "migrate", @@ -41,8 +41,8 @@ func newMigrateCmd() *cobra.Command { cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose output") cmd.Flags().StringVar(&targetHash, "hash", "", "Commit hash for Fiber version") cmd.Flags().StringSliceVar(&thirdParty, "third-party", nil, "Refresh third-party modules (contrib,storage,template). Use a comma-separated list like --third-party=contrib,storage and append @ to pin a commit") - cmd.Flags().StringSliceVar(&include, "include", nil, "Comma-separated list of migrations to include. Supports glob and regex patterns") - cmd.Flags().StringSliceVar(&exclude, "exclude", nil, "Comma-separated list of migrations to exclude. Supports glob and regex patterns") + cmd.Flags().StringSliceVar(&includeFiles, "include", nil, "Comma-separated list of files to include. Supports glob and regex patterns") + cmd.Flags().StringSliceVar(&excludeFiles, "exclude", nil, "Comma-separated list of files to exclude. Supports glob and regex patterns") cmd.RunE = func(cmd *cobra.Command, _ []string) error { tps := make([]ThirdPartyParam, 0, len(thirdParty)) @@ -65,8 +65,8 @@ func newMigrateCmd() *cobra.Command { SkipGoMod: skipGoMod, Verbose: verbose, ThirdParty: tps, - Include: include, - Exclude: exclude, + IncludeFiles: includeFiles, + ExcludeFiles: excludeFiles, }) } @@ -80,8 +80,8 @@ type MigrateOptions struct { TargetVersionS string TargetHash string ThirdParty []ThirdPartyParam - Include []string - Exclude []string + IncludeFiles []string + ExcludeFiles []string Force bool SkipGoMod bool Verbose bool @@ -146,7 +146,7 @@ func migrateRunE(cmd *cobra.Command, opts MigrateOptions) error { migrateFromS = migrateFrom.String() } - err = migrations.DoMigration(cmd, wd, migrateFrom, targetVersion, opts.SkipGoMod, opts.Verbose, opts.Include, opts.Exclude) + err = migrations.DoMigration(cmd, wd, migrateFrom, targetVersion, opts.SkipGoMod, opts.Verbose, opts.IncludeFiles, opts.ExcludeFiles) if err != nil { return fmt.Errorf("migration failed %w", err) } diff --git a/docs/guide/migrate.md b/docs/guide/migrate.md index 4fb95ee..43d1923 100644 --- a/docs/guide/migrate.md +++ b/docs/guide/migrate.md @@ -23,8 +23,8 @@ fiber migrate --to 3.0.0 - `-f`, `--force` – Force migration even if already on the target version - `-s`, `--skip_go_mod` – Skip running `go mod tidy`, `go mod download`, and `go mod vendor` - `-v`, `--verbose` – Enable verbose output during migration -- `--include` – Comma-separated list of migrations to run. Supports glob and regex patterns -- `--exclude` – Comma-separated list of migrations to skip. Supports glob and regex patterns +- `--include` – Comma-separated list of files to include in the migration. Supports glob and regex patterns +- `--exclude` – Comma-separated list of files to exclude from the migration. Supports glob and regex patterns ## Examples @@ -81,3 +81,9 @@ Verbose mode prints detailed progress and executed steps: ```bash fiber migrate --verbose ``` + +Limit the migration to specific files: + +```bash +fiber migrate --include=internal/** --exclude="*_test.go" +```