From 5835adbcee28500e6c36a8542f10e5e3e885aebc Mon Sep 17 00:00:00 2001 From: RW Date: Sun, 24 Aug 2025 19:02:42 +0200 Subject: [PATCH 1/5] feat: update third-party modules --- cmd/migrate.go | 55 ++++- cmd/third_party.go | 445 ++++++++++++++++++++++++++++++++++++++++ cmd/third_party_test.go | 118 +++++++++++ 3 files changed, 615 insertions(+), 3 deletions(-) create mode 100644 cmd/third_party.go create mode 100644 cmd/third_party_test.go diff --git a/cmd/migrate.go b/cmd/migrate.go index 773889d..46e5b46 100644 --- a/cmd/migrate.go +++ b/cmd/migrate.go @@ -16,6 +16,7 @@ import ( "github.com/spf13/cobra" "golang.org/x/mod/module" + "github.com/gofiber/cli/cmd/internal" "github.com/gofiber/cli/cmd/internal/migrations" ) @@ -25,6 +26,7 @@ func newMigrateCmd() *cobra.Command { var force bool var skipGoMod bool var verbose bool + var thirdParty []string cmd := &cobra.Command{ Use: "migrate", @@ -36,8 +38,21 @@ func newMigrateCmd() *cobra.Command { cmd.Flags().BoolVarP(&skipGoMod, "skip_go_mod", "s", false, "Skip running go mod tidy, download and vendor") 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, e.g. --third-party=contrib") cmd.RunE = func(cmd *cobra.Command, _ []string) error { + tps := make([]ThirdPartyParam, 0, len(thirdParty)) + for _, tp := range thirdParty { + parts := strings.SplitN(tp, "@", 2) + p := ThirdPartyParam{Name: parts[0]} + if len(parts) == 2 { + p.Hash = parts[1] + } + if p.Name != "" { + tps = append(tps, p) + } + } + return migrateRunE(cmd, MigrateOptions{ CurrentVersionFile: currentVersionFile, TargetVersionS: targetVersionS, @@ -45,6 +60,7 @@ func newMigrateCmd() *cobra.Command { Force: force, SkipGoMod: skipGoMod, Verbose: verbose, + ThirdParty: tps, }) } @@ -57,11 +73,17 @@ type MigrateOptions struct { CurrentVersionFile string TargetVersionS string TargetHash string + ThirdParty []ThirdPartyParam Force bool SkipGoMod bool Verbose bool } +type ThirdPartyParam struct { + Name string + Hash string +} + func migrateRunE(cmd *cobra.Command, opts MigrateOptions) error { currentVersionS, err := currentVersionFromFile(opts.CurrentVersionFile) if err != nil { @@ -84,7 +106,7 @@ func migrateRunE(cmd *cobra.Command, opts MigrateOptions) error { targetVersion := baseVersion if opts.TargetHash != "" { - pv, err := pseudoVersionFromHash(baseVersion, opts.TargetHash) + pv, err := pseudoVersionFromHash("gofiber/fiber", baseVersion, opts.TargetHash) if err != nil { return fmt.Errorf("pseudo version: %w", err) } @@ -121,6 +143,33 @@ func migrateRunE(cmd *cobra.Command, opts MigrateOptions) error { return fmt.Errorf("migration failed %w", err) } + tpChanged := false + for _, tp := range opts.ThirdParty { + var ( + changed bool + err error + ) + switch tp.Name { + case "contrib": + changed, err = refreshContrib(cmd, wd, tp.Hash) + case "storage": + changed, err = refreshStorage(cmd, wd, tp.Hash) + case "template", "templates": + changed, err = refreshTemplates(cmd, wd, tp.Hash) + } + if err != nil { + return fmt.Errorf("refresh %s packages: %w", tp.Name, err) + } + if changed { + tpChanged = true + } + } + if tpChanged && !opts.SkipGoMod { + if err := internal.RunGoMod(wd); err != nil { + return fmt.Errorf("go mod: %w", err) + } + } + msg := fmt.Sprintf("Migration from Fiber %s to %s", migrateFromS, opts.TargetVersionS) cmd.Println(termenv.String(msg). Foreground(termenv.ANSIBrightBlue)) @@ -128,8 +177,8 @@ func migrateRunE(cmd *cobra.Command, opts MigrateOptions) error { return nil } -func pseudoVersionFromHash(base *semver.Version, hash string) (string, error) { - url := "https://api.github.com/repos/gofiber/fiber/commits/" + hash +func pseudoVersionFromHash(repo string, base *semver.Version, hash string) (string, error) { + url := "https://api.github.com/repos/" + repo + "/commits/" + hash ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() diff --git a/cmd/third_party.go b/cmd/third_party.go new file mode 100644 index 0000000..30686a1 --- /dev/null +++ b/cmd/third_party.go @@ -0,0 +1,445 @@ +package cmd + +import ( + "bufio" + "context" + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" + "regexp" + "sort" + "strings" + "time" + + "github.com/Masterminds/semver/v3" + "github.com/spf13/cobra" + + "github.com/gofiber/cli/cmd/internal" +) + +var ( + latestContribVersionFn = latestContribVersion + latestStorageVersionFn = func(module, major string) string { return latestThirdPartyVersion("storage", module, major) } + latestTemplateVersionFn = func(module, major string) string { return latestThirdPartyVersion("template", module, major) } +) + +const vendorDir = "vendor" + +func refreshContrib(cmd *cobra.Command, cwd, hash string) (bool, error) { + modules, err := findContribModules(cwd) + if err != nil { + return false, fmt.Errorf("find modules: %w", err) + } + if len(modules) == 0 { + return false, nil + } + + versions := make(map[string]string, len(modules)) + if hash == "" { + reader := bufio.NewReader(cmd.InOrStdin()) + for _, m := range modules { + latest := latestContribVersionFn(m) + prompt := fmt.Sprintf("Version for github.com/gofiber/contrib/%s (default %s): ", m, latest) + cmd.Print(prompt) + line, err := reader.ReadString('\n') + if err != nil && err != io.EOF { + return false, fmt.Errorf("read input: %w", err) + } + v := strings.TrimSpace(line) + if v == "" { + v = latest + } + if v != "" { + versions[m] = v + } + } + } else { + for _, m := range modules { + latest := latestContribVersionFn(m) + if latest == "" { + continue + } + base, err := semver.NewVersion(strings.TrimPrefix(latest, "v")) + if err != nil { + return false, fmt.Errorf("parse version: %w", err) + } + pv, err := pseudoVersionFromHash("gofiber/contrib", base, hash) + if err != nil { + return false, fmt.Errorf("pseudo version: %w", err) + } + versions[m] = pv + } + } + if len(versions) == 0 { + return false, nil + } + + re := regexp.MustCompile(`"github\.com/gofiber/contrib(?:/v\d+)?/([a-zA-Z0-9_]+)([^\"]*)"`) + changed, err := internal.ChangeFileContent(cwd, func(content string) string { + return re.ReplaceAllStringFunc(content, func(s string) string { + sub := re.FindStringSubmatch(s) + mod := sub[1] + rest := sub[2] + ver, ok := versions[mod] + if !ok { + return s + } + major := majorFromVersion(ver) + return fmt.Sprintf("\"github.com/gofiber/contrib/v3/%s/%s%s\"", mod, major, rest) + }) + }) + if err != nil { + return false, fmt.Errorf("refresh imports: %w", err) + } + + modFile := filepath.Join(cwd, "go.mod") + b, err := os.ReadFile(modFile) // #nosec G304 + if err != nil && !os.IsNotExist(err) { + return false, fmt.Errorf("read go.mod: %w", err) + } + changedMod := false + if err == nil { + content := string(b) + for mod, ver := range versions { + major := majorFromVersion(ver) + re := regexp.MustCompile(fmt.Sprintf(`(?m)^(\s*(?:require\s+)?)github.com/gofiber/contrib/%s\s+v[\w\.-]+`, regexp.QuoteMeta(mod))) + newLine := fmt.Sprintf(`${1}github.com/gofiber/contrib/v3/%s/%s %s`, mod, major, ver) + replaced := re.ReplaceAllString(content, newLine) + if replaced != content { + content = replaced + changedMod = true + } + } + if changedMod { + if err := os.WriteFile(modFile, []byte(content), 0o600); err != nil { + return false, fmt.Errorf("write go.mod: %w", err) + } + } + } + + if changed || changedMod { + cmd.Println("Refreshing contrib packages") + } + return changed || changedMod, nil +} + +func findContribModules(cwd string) ([]string, error) { + modules := make(map[string]struct{}) + re := regexp.MustCompile(`github\.com/gofiber/contrib/(?:v\d+/)?([a-zA-Z0-9_]+)`) // capture module name + err := filepath.WalkDir(cwd, func(path string, d os.DirEntry, err error) error { + if err != nil { + return fmt.Errorf("walk path: %w", err) + } + if d.IsDir() { + if d.Name() == vendorDir { + return filepath.SkipDir + } + return nil + } + if !strings.HasSuffix(d.Name(), ".go") { + return nil + } + b, err := os.ReadFile(path) // #nosec G304 + if err != nil { + return fmt.Errorf("read %s: %w", path, err) + } + matches := re.FindAllStringSubmatch(string(b), -1) + for _, m := range matches { + modules[m[1]] = struct{}{} + } + return nil + }) + if err != nil { + return nil, fmt.Errorf("walk %s: %w", cwd, err) + } + res := make([]string, 0, len(modules)) + for m := range modules { + res = append(res, m) + } + sort.Strings(res) + return res, nil +} + +func latestContribVersion(module string) string { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + url := fmt.Sprintf("https://proxy.golang.org/github.com/gofiber/contrib/v3/%s/@latest", module) + b, status, err := cachedGET(ctx, url, nil) + if err != nil || status != 200 { + return "" + } + var data struct { + Version string `json:"Version"` //nolint:tagliatelle // field name defined by proxy + } + if err := json.Unmarshal(b, &data); err != nil { + return "" + } + return data.Version +} + +func majorFromVersion(v string) string { + v = strings.TrimPrefix(v, "v") + idx := strings.IndexAny(v, ".-") + if idx >= 0 { + v = v[:idx] + } + return "v" + v +} + +func majorPath(major string) string { + if major == "" || major == "v0" || major == "v1" { + return "" + } + return "/" + major +} + +func refreshStorage(cmd *cobra.Command, cwd, hash string) (bool, error) { + modules, err := findStorageModules(cwd) + if err != nil { + return false, fmt.Errorf("find modules: %w", err) + } + if len(modules) == 0 { + return false, nil + } + + versions := make(map[string]string, len(modules)) + for mod, curMajor := range modules { + latest := latestStorageVersionFn(mod, curMajor) + if latest == "" { + continue + } + ver := latest + if hash != "" { + base, err := semver.NewVersion(strings.TrimPrefix(latest, "v")) + if err != nil { + return false, fmt.Errorf("parse version: %w", err) + } + pv, err := pseudoVersionFromHash("gofiber/storage", base, hash) + if err != nil { + return false, fmt.Errorf("pseudo version: %w", err) + } + ver = pv + } + versions[mod] = ver + } + if len(versions) == 0 { + return false, nil + } + + re := regexp.MustCompile(`"github\.com/gofiber/storage/([a-zA-Z0-9_]+)(?:/v\d+)?([^\"]*)"`) + changed, err := internal.ChangeFileContent(cwd, func(content string) string { + return re.ReplaceAllStringFunc(content, func(s string) string { + sub := re.FindStringSubmatch(s) + mod := sub[1] + rest := sub[2] + ver, ok := versions[mod] + if !ok { + return s + } + major := majorFromVersion(ver) + return fmt.Sprintf("\"github.com/gofiber/storage/%s%s%s\"", mod, majorPath(major), rest) + }) + }) + if err != nil { + return false, fmt.Errorf("refresh imports: %w", err) + } + + modFile := filepath.Join(cwd, "go.mod") + b, err := os.ReadFile(modFile) // #nosec G304 + if err != nil && !os.IsNotExist(err) { + return false, fmt.Errorf("read go.mod: %w", err) + } + changedMod := false + if err == nil { + content := string(b) + for mod, ver := range versions { + major := majorFromVersion(ver) + re := regexp.MustCompile(fmt.Sprintf(`(?m)^(\s*(?:require\s+)?)github.com/gofiber/storage/%s(?:/v\d+)?\s+v[\w\.-]+`, regexp.QuoteMeta(mod))) + newLine := fmt.Sprintf(`${1}github.com/gofiber/storage/%s%s %s`, mod, majorPath(major), ver) + replaced := re.ReplaceAllString(content, newLine) + if replaced != content { + content = replaced + changedMod = true + } + } + if changedMod { + if err := os.WriteFile(modFile, []byte(content), 0o600); err != nil { + return false, fmt.Errorf("write go.mod: %w", err) + } + } + } + + if changed || changedMod { + cmd.Println("Refreshing storage packages") + } + return changed || changedMod, nil +} + +func refreshTemplates(cmd *cobra.Command, cwd, hash string) (bool, error) { + modules, err := findTemplateModules(cwd) + if err != nil { + return false, fmt.Errorf("find modules: %w", err) + } + if len(modules) == 0 { + return false, nil + } + + versions := make(map[string]string, len(modules)) + for mod, curMajor := range modules { + latest := latestTemplateVersionFn(mod, curMajor) + if latest == "" { + continue + } + ver := latest + if hash != "" { + base, err := semver.NewVersion(strings.TrimPrefix(latest, "v")) + if err != nil { + return false, fmt.Errorf("parse version: %w", err) + } + pv, err := pseudoVersionFromHash("gofiber/template", base, hash) + if err != nil { + return false, fmt.Errorf("pseudo version: %w", err) + } + ver = pv + } + versions[mod] = ver + } + if len(versions) == 0 { + return false, nil + } + + re := regexp.MustCompile(`"github\.com/gofiber/template/([a-zA-Z0-9_]+)(?:/v\d+)?([^\"]*)"`) + changed, err := internal.ChangeFileContent(cwd, func(content string) string { + return re.ReplaceAllStringFunc(content, func(s string) string { + sub := re.FindStringSubmatch(s) + mod := sub[1] + rest := sub[2] + ver, ok := versions[mod] + if !ok { + return s + } + major := majorFromVersion(ver) + return fmt.Sprintf("\"github.com/gofiber/template/%s%s%s\"", mod, majorPath(major), rest) + }) + }) + if err != nil { + return false, fmt.Errorf("refresh imports: %w", err) + } + + modFile := filepath.Join(cwd, "go.mod") + b, err := os.ReadFile(modFile) // #nosec G304 + if err != nil && !os.IsNotExist(err) { + return false, fmt.Errorf("read go.mod: %w", err) + } + changedMod := false + if err == nil { + content := string(b) + for mod, ver := range versions { + major := majorFromVersion(ver) + re := regexp.MustCompile(fmt.Sprintf(`(?m)^(\s*(?:require\s+)?)github.com/gofiber/template/%s(?:/v\d+)?\s+v[\w\.-]+`, regexp.QuoteMeta(mod))) + newLine := fmt.Sprintf(`${1}github.com/gofiber/template/%s%s %s`, mod, majorPath(major), ver) + replaced := re.ReplaceAllString(content, newLine) + if replaced != content { + content = replaced + changedMod = true + } + } + if changedMod { + if err := os.WriteFile(modFile, []byte(content), 0o600); err != nil { + return false, fmt.Errorf("write go.mod: %w", err) + } + } + } + + if changed || changedMod { + cmd.Println("Refreshing template packages") + } + return changed || changedMod, nil +} + +func findStorageModules(cwd string) (map[string]string, error) { + modules := make(map[string]string) + re := regexp.MustCompile(`github\.com/gofiber/storage/([a-zA-Z0-9_]+)(?:/(v\d+))?`) + err := filepath.WalkDir(cwd, func(path string, d os.DirEntry, err error) error { + if err != nil { + return fmt.Errorf("walk path: %w", err) + } + if d.IsDir() { + if d.Name() == vendorDir { + return filepath.SkipDir + } + return nil + } + if !strings.HasSuffix(d.Name(), ".go") { + return nil + } + b, err := os.ReadFile(path) // #nosec G304 + if err != nil { + return fmt.Errorf("read %s: %w", path, err) + } + matches := re.FindAllStringSubmatch(string(b), -1) + for _, m := range matches { + modules[m[1]] = m[2] + } + return nil + }) + if err != nil { + return nil, fmt.Errorf("walk %s: %w", cwd, err) + } + return modules, nil +} + +func findTemplateModules(cwd string) (map[string]string, error) { + modules := make(map[string]string) + re := regexp.MustCompile(`github\.com/gofiber/template/([a-zA-Z0-9_]+)(?:/(v\d+))?`) + err := filepath.WalkDir(cwd, func(path string, d os.DirEntry, err error) error { + if err != nil { + return fmt.Errorf("walk path: %w", err) + } + if d.IsDir() { + if d.Name() == vendorDir { + return filepath.SkipDir + } + return nil + } + if !strings.HasSuffix(d.Name(), ".go") { + return nil + } + b, err := os.ReadFile(path) // #nosec G304 + if err != nil { + return fmt.Errorf("read %s: %w", path, err) + } + matches := re.FindAllStringSubmatch(string(b), -1) + for _, m := range matches { + modules[m[1]] = m[2] + } + return nil + }) + if err != nil { + return nil, fmt.Errorf("walk %s: %w", cwd, err) + } + return modules, nil +} + +func latestThirdPartyVersion(repo, module, major string) string { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + url := fmt.Sprintf("https://proxy.golang.org/github.com/gofiber/%s/%s", repo, module) + if major != "" { + url += "/" + major + } + url += "/@latest" + b, status, err := cachedGET(ctx, url, nil) + if err != nil || status != 200 { + return "" + } + var data struct { + Version string `json:"Version"` //nolint:tagliatelle // field name defined by proxy + } + if err := json.Unmarshal(b, &data); err != nil { + return "" + } + return data.Version +} diff --git a/cmd/third_party_test.go b/cmd/third_party_test.go new file mode 100644 index 0000000..3dad231 --- /dev/null +++ b/cmd/third_party_test.go @@ -0,0 +1,118 @@ +package cmd + +import ( + "bytes" + "os" + "path/filepath" + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_refreshContrib(t *testing.T) { + dir := t.TempDir() + mainSrc := `package main + +import _ "github.com/gofiber/contrib/monitor" + +func main(){}` + require.NoError(t, os.WriteFile(filepath.Join(dir, "main.go"), []byte(mainSrc), 0o600)) + modSrc := `module test + +require github.com/gofiber/contrib/monitor v1.0.0 +` + require.NoError(t, os.WriteFile(filepath.Join(dir, "go.mod"), []byte(modSrc), 0o600)) + + old := latestContribVersionFn + latestContribVersionFn = func(string) string { return "v1.2.3" } + defer func() { latestContribVersionFn = old }() + + c := &cobra.Command{} + c.SetIn(bytes.NewBufferString("\n")) + var buf bytes.Buffer + c.SetOut(&buf) + + changed, err := refreshContrib(c, dir, "") + require.NoError(t, err) + assert.True(t, changed) + + content, err := os.ReadFile(filepath.Join(dir, "main.go")) // #nosec G304 + require.NoError(t, err) + assert.Contains(t, string(content), "github.com/gofiber/contrib/v3/monitor/v1") + + gm, err := os.ReadFile(filepath.Join(dir, "go.mod")) // #nosec G304 + require.NoError(t, err) + assert.Contains(t, string(gm), "github.com/gofiber/contrib/v3/monitor/v1 v1.2.3") +} + +func Test_refreshStorage(t *testing.T) { + dir := t.TempDir() + mainSrc := `package main + +import _ "github.com/gofiber/storage/redis" + +func main(){}` + require.NoError(t, os.WriteFile(filepath.Join(dir, "main.go"), []byte(mainSrc), 0o600)) + modSrc := `module test + +require github.com/gofiber/storage/redis v1.0.0 +` + require.NoError(t, os.WriteFile(filepath.Join(dir, "go.mod"), []byte(modSrc), 0o600)) + + old := latestStorageVersionFn + latestStorageVersionFn = func(string, string) string { return "v2.3.4" } + defer func() { latestStorageVersionFn = old }() + + c := &cobra.Command{} + var buf bytes.Buffer + c.SetOut(&buf) + + changed, err := refreshStorage(c, dir, "") + require.NoError(t, err) + assert.True(t, changed) + + content, err := os.ReadFile(filepath.Join(dir, "main.go")) // #nosec G304 + require.NoError(t, err) + assert.Contains(t, string(content), "github.com/gofiber/storage/redis/v2") + + gm, err := os.ReadFile(filepath.Join(dir, "go.mod")) // #nosec G304 + require.NoError(t, err) + assert.Contains(t, string(gm), "github.com/gofiber/storage/redis/v2 v2.3.4") +} + +func Test_refreshTemplates(t *testing.T) { + dir := t.TempDir() + mainSrc := `package main + +import _ "github.com/gofiber/template/html/v2" + +func main(){}` + require.NoError(t, os.WriteFile(filepath.Join(dir, "main.go"), []byte(mainSrc), 0o600)) + modSrc := `module test + +require github.com/gofiber/template/html/v2 v2.0.0 +` + require.NoError(t, os.WriteFile(filepath.Join(dir, "go.mod"), []byte(modSrc), 0o600)) + + old := latestTemplateVersionFn + latestTemplateVersionFn = func(string, string) string { return "v3.2.1" } + defer func() { latestTemplateVersionFn = old }() + + c := &cobra.Command{} + var buf bytes.Buffer + c.SetOut(&buf) + + changed, err := refreshTemplates(c, dir, "") + require.NoError(t, err) + assert.True(t, changed) + + content, err := os.ReadFile(filepath.Join(dir, "main.go")) // #nosec G304 + require.NoError(t, err) + assert.Contains(t, string(content), "github.com/gofiber/template/html/v3") + + gm, err := os.ReadFile(filepath.Join(dir, "go.mod")) // #nosec G304 + require.NoError(t, err) + assert.Contains(t, string(gm), "github.com/gofiber/template/html/v3 v3.2.1") +} From 1f4e8d9b72b6e3cff5d485fc917ea9554aa41769 Mon Sep 17 00:00:00 2001 From: RW Date: Sun, 24 Aug 2025 20:29:34 +0200 Subject: [PATCH 2/5] refactor: fix contrib refresh and unify third-party handling --- cmd/third_party.go | 148 ++++++---------------------------------- cmd/third_party_test.go | 4 +- 2 files changed, 23 insertions(+), 129 deletions(-) diff --git a/cmd/third_party.go b/cmd/third_party.go index 30686a1..ae5ed22 100644 --- a/cmd/third_party.go +++ b/cmd/third_party.go @@ -76,7 +76,7 @@ func refreshContrib(cmd *cobra.Command, cwd, hash string) (bool, error) { return false, nil } - re := regexp.MustCompile(`"github\.com/gofiber/contrib(?:/v\d+)?/([a-zA-Z0-9_]+)([^\"]*)"`) + re := regexp.MustCompile(`"github\.com/gofiber/contrib(?:/v\d+)?/([a-zA-Z0-9_]+)(?:/v\d+)?([^\"]*)"`) changed, err := internal.ChangeFileContent(cwd, func(content string) string { return re.ReplaceAllStringFunc(content, func(s string) string { sub := re.FindStringSubmatch(s) @@ -87,7 +87,7 @@ func refreshContrib(cmd *cobra.Command, cwd, hash string) (bool, error) { return s } major := majorFromVersion(ver) - return fmt.Sprintf("\"github.com/gofiber/contrib/v3/%s/%s%s\"", mod, major, rest) + return fmt.Sprintf("\"github.com/gofiber/contrib/%s%s%s\"", mod, majorPath(major), rest) }) }) if err != nil { @@ -104,8 +104,8 @@ func refreshContrib(cmd *cobra.Command, cwd, hash string) (bool, error) { content := string(b) for mod, ver := range versions { major := majorFromVersion(ver) - re := regexp.MustCompile(fmt.Sprintf(`(?m)^(\s*(?:require\s+)?)github.com/gofiber/contrib/%s\s+v[\w\.-]+`, regexp.QuoteMeta(mod))) - newLine := fmt.Sprintf(`${1}github.com/gofiber/contrib/v3/%s/%s %s`, mod, major, ver) + re := regexp.MustCompile(fmt.Sprintf(`(?m)^(\s*(?:require\s+)?)github.com/gofiber/contrib/(?:v\d+/)?%s(?:/v\d+)?\s+v[\w\.-]+`, regexp.QuoteMeta(mod))) + newLine := fmt.Sprintf(`${1}github.com/gofiber/contrib/%s%s %s`, mod, majorPath(major), ver) replaced := re.ReplaceAllString(content, newLine) if replaced != content { content = replaced @@ -127,7 +127,7 @@ func refreshContrib(cmd *cobra.Command, cwd, hash string) (bool, error) { func findContribModules(cwd string) ([]string, error) { modules := make(map[string]struct{}) - re := regexp.MustCompile(`github\.com/gofiber/contrib/(?:v\d+/)?([a-zA-Z0-9_]+)`) // capture module name + re := regexp.MustCompile(`\bgithub\.com/gofiber/contrib/(?:v\d+/)?([a-zA-Z0-9_]+)`) // capture module name err := filepath.WalkDir(cwd, func(path string, d os.DirEntry, err error) error { if err != nil { return fmt.Errorf("walk path: %w", err) @@ -165,7 +165,7 @@ func findContribModules(cwd string) ([]string, error) { func latestContribVersion(module string) string { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - url := fmt.Sprintf("https://proxy.golang.org/github.com/gofiber/contrib/v3/%s/@latest", module) + url := fmt.Sprintf("https://proxy.golang.org/github.com/gofiber/contrib/%s/@latest", module) b, status, err := cachedGET(ctx, url, nil) if err != nil || status != 200 { return "" @@ -195,8 +195,8 @@ func majorPath(major string) string { return "/" + major } -func refreshStorage(cmd *cobra.Command, cwd, hash string) (bool, error) { - modules, err := findStorageModules(cwd) +func refreshThirdParty(cmd *cobra.Command, cwd, hash, repo, label string, latestFn func(string, string) string) (bool, error) { + modules, err := findThirdPartyModules(cwd, repo) if err != nil { return false, fmt.Errorf("find modules: %w", err) } @@ -206,7 +206,7 @@ func refreshStorage(cmd *cobra.Command, cwd, hash string) (bool, error) { versions := make(map[string]string, len(modules)) for mod, curMajor := range modules { - latest := latestStorageVersionFn(mod, curMajor) + latest := latestFn(mod, curMajor) if latest == "" { continue } @@ -216,7 +216,7 @@ func refreshStorage(cmd *cobra.Command, cwd, hash string) (bool, error) { if err != nil { return false, fmt.Errorf("parse version: %w", err) } - pv, err := pseudoVersionFromHash("gofiber/storage", base, hash) + pv, err := pseudoVersionFromHash("gofiber/"+repo, base, hash) if err != nil { return false, fmt.Errorf("pseudo version: %w", err) } @@ -228,7 +228,7 @@ func refreshStorage(cmd *cobra.Command, cwd, hash string) (bool, error) { return false, nil } - re := regexp.MustCompile(`"github\.com/gofiber/storage/([a-zA-Z0-9_]+)(?:/v\d+)?([^\"]*)"`) + re := regexp.MustCompile(fmt.Sprintf(`"github\.com/gofiber/%s/([a-zA-Z0-9_]+)(?:/v\d+)?([^\"]*)"`, regexp.QuoteMeta(repo))) changed, err := internal.ChangeFileContent(cwd, func(content string) string { return re.ReplaceAllStringFunc(content, func(s string) string { sub := re.FindStringSubmatch(s) @@ -239,7 +239,7 @@ func refreshStorage(cmd *cobra.Command, cwd, hash string) (bool, error) { return s } major := majorFromVersion(ver) - return fmt.Sprintf("\"github.com/gofiber/storage/%s%s%s\"", mod, majorPath(major), rest) + return fmt.Sprintf("\"github.com/gofiber/%s/%s%s%s\"", repo, mod, majorPath(major), rest) }) }) if err != nil { @@ -256,8 +256,8 @@ func refreshStorage(cmd *cobra.Command, cwd, hash string) (bool, error) { content := string(b) for mod, ver := range versions { major := majorFromVersion(ver) - re := regexp.MustCompile(fmt.Sprintf(`(?m)^(\s*(?:require\s+)?)github.com/gofiber/storage/%s(?:/v\d+)?\s+v[\w\.-]+`, regexp.QuoteMeta(mod))) - newLine := fmt.Sprintf(`${1}github.com/gofiber/storage/%s%s %s`, mod, majorPath(major), ver) + re := regexp.MustCompile(fmt.Sprintf(`(?m)^(\s*(?:require\s+)?)github.com/gofiber/%s/%s(?:/v\d+)?\s+v[\w\.-]+`, regexp.QuoteMeta(repo), regexp.QuoteMeta(mod))) + newLine := fmt.Sprintf(`${1}github.com/gofiber/%s/%s%s %s`, repo, mod, majorPath(major), ver) replaced := re.ReplaceAllString(content, newLine) if replaced != content { content = replaced @@ -272,128 +272,22 @@ func refreshStorage(cmd *cobra.Command, cwd, hash string) (bool, error) { } if changed || changedMod { - cmd.Println("Refreshing storage packages") + cmd.Printf("Refreshing %s packages\n", label) } return changed || changedMod, nil } -func refreshTemplates(cmd *cobra.Command, cwd, hash string) (bool, error) { - modules, err := findTemplateModules(cwd) - if err != nil { - return false, fmt.Errorf("find modules: %w", err) - } - if len(modules) == 0 { - return false, nil - } - - versions := make(map[string]string, len(modules)) - for mod, curMajor := range modules { - latest := latestTemplateVersionFn(mod, curMajor) - if latest == "" { - continue - } - ver := latest - if hash != "" { - base, err := semver.NewVersion(strings.TrimPrefix(latest, "v")) - if err != nil { - return false, fmt.Errorf("parse version: %w", err) - } - pv, err := pseudoVersionFromHash("gofiber/template", base, hash) - if err != nil { - return false, fmt.Errorf("pseudo version: %w", err) - } - ver = pv - } - versions[mod] = ver - } - if len(versions) == 0 { - return false, nil - } - - re := regexp.MustCompile(`"github\.com/gofiber/template/([a-zA-Z0-9_]+)(?:/v\d+)?([^\"]*)"`) - changed, err := internal.ChangeFileContent(cwd, func(content string) string { - return re.ReplaceAllStringFunc(content, func(s string) string { - sub := re.FindStringSubmatch(s) - mod := sub[1] - rest := sub[2] - ver, ok := versions[mod] - if !ok { - return s - } - major := majorFromVersion(ver) - return fmt.Sprintf("\"github.com/gofiber/template/%s%s%s\"", mod, majorPath(major), rest) - }) - }) - if err != nil { - return false, fmt.Errorf("refresh imports: %w", err) - } - - modFile := filepath.Join(cwd, "go.mod") - b, err := os.ReadFile(modFile) // #nosec G304 - if err != nil && !os.IsNotExist(err) { - return false, fmt.Errorf("read go.mod: %w", err) - } - changedMod := false - if err == nil { - content := string(b) - for mod, ver := range versions { - major := majorFromVersion(ver) - re := regexp.MustCompile(fmt.Sprintf(`(?m)^(\s*(?:require\s+)?)github.com/gofiber/template/%s(?:/v\d+)?\s+v[\w\.-]+`, regexp.QuoteMeta(mod))) - newLine := fmt.Sprintf(`${1}github.com/gofiber/template/%s%s %s`, mod, majorPath(major), ver) - replaced := re.ReplaceAllString(content, newLine) - if replaced != content { - content = replaced - changedMod = true - } - } - if changedMod { - if err := os.WriteFile(modFile, []byte(content), 0o600); err != nil { - return false, fmt.Errorf("write go.mod: %w", err) - } - } - } - - if changed || changedMod { - cmd.Println("Refreshing template packages") - } - return changed || changedMod, nil +func refreshStorage(cmd *cobra.Command, cwd, hash string) (bool, error) { + return refreshThirdParty(cmd, cwd, hash, "storage", "storage", latestStorageVersionFn) } -func findStorageModules(cwd string) (map[string]string, error) { - modules := make(map[string]string) - re := regexp.MustCompile(`github\.com/gofiber/storage/([a-zA-Z0-9_]+)(?:/(v\d+))?`) - err := filepath.WalkDir(cwd, func(path string, d os.DirEntry, err error) error { - if err != nil { - return fmt.Errorf("walk path: %w", err) - } - if d.IsDir() { - if d.Name() == vendorDir { - return filepath.SkipDir - } - return nil - } - if !strings.HasSuffix(d.Name(), ".go") { - return nil - } - b, err := os.ReadFile(path) // #nosec G304 - if err != nil { - return fmt.Errorf("read %s: %w", path, err) - } - matches := re.FindAllStringSubmatch(string(b), -1) - for _, m := range matches { - modules[m[1]] = m[2] - } - return nil - }) - if err != nil { - return nil, fmt.Errorf("walk %s: %w", cwd, err) - } - return modules, nil +func refreshTemplates(cmd *cobra.Command, cwd, hash string) (bool, error) { + return refreshThirdParty(cmd, cwd, hash, "template", "template", latestTemplateVersionFn) } -func findTemplateModules(cwd string) (map[string]string, error) { +func findThirdPartyModules(cwd, repo string) (map[string]string, error) { modules := make(map[string]string) - re := regexp.MustCompile(`github\.com/gofiber/template/([a-zA-Z0-9_]+)(?:/(v\d+))?`) + re := regexp.MustCompile(fmt.Sprintf(`\bgithub\.com/gofiber/%s/([a-zA-Z0-9_]+)(?:/(v\d+))?`, regexp.QuoteMeta(repo))) err := filepath.WalkDir(cwd, func(path string, d os.DirEntry, err error) error { if err != nil { return fmt.Errorf("walk path: %w", err) diff --git a/cmd/third_party_test.go b/cmd/third_party_test.go index 3dad231..e67a391 100644 --- a/cmd/third_party_test.go +++ b/cmd/third_party_test.go @@ -40,11 +40,11 @@ require github.com/gofiber/contrib/monitor v1.0.0 content, err := os.ReadFile(filepath.Join(dir, "main.go")) // #nosec G304 require.NoError(t, err) - assert.Contains(t, string(content), "github.com/gofiber/contrib/v3/monitor/v1") + assert.Contains(t, string(content), "github.com/gofiber/contrib/monitor") gm, err := os.ReadFile(filepath.Join(dir, "go.mod")) // #nosec G304 require.NoError(t, err) - assert.Contains(t, string(gm), "github.com/gofiber/contrib/v3/monitor/v1 v1.2.3") + assert.Contains(t, string(gm), "github.com/gofiber/contrib/monitor v1.2.3") } func Test_refreshStorage(t *testing.T) { From d8361490ec34c1b9b35080d6c5d4351c2324b623 Mon Sep 17 00:00:00 2001 From: RW Date: Sun, 24 Aug 2025 20:29:39 +0200 Subject: [PATCH 3/5] docs: document third-party refresh flag --- README.md | 1 + docs/guide/migrate.md | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/README.md b/README.md index e2f11cc..96da3eb 100644 --- a/README.md +++ b/README.md @@ -164,6 +164,7 @@ fiber migrate --to 3.0.0 -f, --force Force migration even if already on the version -s, --skip_go_mod Skip running go mod tidy, download and vendor --hash string Commit hash for Fiber version + --third-party strings Refresh third-party modules (e.g. contrib@) -v, --verbose Enable verbose output -h, --help help for migrate ``` diff --git a/docs/guide/migrate.md b/docs/guide/migrate.md index e461272..07596aa 100644 --- a/docs/guide/migrate.md +++ b/docs/guide/migrate.md @@ -19,6 +19,7 @@ fiber migrate --to 3.0.0 - `-t`, `--to` – Target version to migrate to. Defaults to the latest release - `--hash` – Commit hash for the Fiber version when migrating to a pseudo version +- `--third-party` – Refresh third-party modules like `contrib`, `storage`, or `template`. Append `@` to pin to a specific commit - `-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 @@ -43,6 +44,18 @@ Use a commit hash for unreleased versions: fiber migrate --to 3.0.0 --hash abcdef123456 ``` +Refresh contrib packages to their latest versions: + +```bash +fiber migrate --third-party=contrib +``` + +Refresh template packages to a specific commit: + +```bash +fiber migrate --third-party=template@abcdef123456 +``` + Force re-running migrations even if the version matches: ```bash From ce5e321f0c04567ac180dcbd19d96042dadb1180 Mon Sep 17 00:00:00 2001 From: RW Date: Sun, 24 Aug 2025 20:29:44 +0200 Subject: [PATCH 4/5] docs: clarify third-party flag usage --- README.md | 2 +- cmd/migrate.go | 2 +- docs/guide/migrate.md | 8 +++++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 96da3eb..dfc102c 100644 --- a/README.md +++ b/README.md @@ -164,7 +164,7 @@ fiber migrate --to 3.0.0 -f, --force Force migration even if already on the version -s, --skip_go_mod Skip running go mod tidy, download and vendor --hash string Commit hash for Fiber version - --third-party strings Refresh third-party modules (e.g. contrib@) + --third-party strings Refresh third-party modules (contrib,storage,template). Provide a comma-separated list and optionally append @ to pin a commit -v, --verbose Enable verbose output -h, --help help for migrate ``` diff --git a/cmd/migrate.go b/cmd/migrate.go index 46e5b46..bfa33a3 100644 --- a/cmd/migrate.go +++ b/cmd/migrate.go @@ -38,7 +38,7 @@ func newMigrateCmd() *cobra.Command { cmd.Flags().BoolVarP(&skipGoMod, "skip_go_mod", "s", false, "Skip running go mod tidy, download and vendor") 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, e.g. --third-party=contrib") + 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.RunE = func(cmd *cobra.Command, _ []string) error { tps := make([]ThirdPartyParam, 0, len(thirdParty)) diff --git a/docs/guide/migrate.md b/docs/guide/migrate.md index 07596aa..4fc1b5c 100644 --- a/docs/guide/migrate.md +++ b/docs/guide/migrate.md @@ -19,7 +19,7 @@ fiber migrate --to 3.0.0 - `-t`, `--to` – Target version to migrate to. Defaults to the latest release - `--hash` – Commit hash for the Fiber version when migrating to a pseudo version -- `--third-party` – Refresh third-party modules like `contrib`, `storage`, or `template`. Append `@` to pin to a specific commit +- `--third-party` – Refresh third-party modules like `contrib`, `storage`, or `template`. Provide a comma-separated list (e.g. `--third-party=contrib,storage`) and append `@` to pin to a specific commit - `-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 @@ -56,6 +56,12 @@ Refresh template packages to a specific commit: fiber migrate --third-party=template@abcdef123456 ``` +Refresh both contrib and storage packages at once: + +```bash +fiber migrate --third-party=contrib,storage +``` + Force re-running migrations even if the version matches: ```bash From c066ed64e2c87ab6b8dc24c8344486d3235beade Mon Sep 17 00:00:00 2001 From: RW Date: Sun, 24 Aug 2025 20:45:28 +0200 Subject: [PATCH 5/5] fix: anchor third-party module regex --- cmd/third_party.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/third_party.go b/cmd/third_party.go index ae5ed22..8e28a03 100644 --- a/cmd/third_party.go +++ b/cmd/third_party.go @@ -127,7 +127,7 @@ func refreshContrib(cmd *cobra.Command, cwd, hash string) (bool, error) { func findContribModules(cwd string) ([]string, error) { modules := make(map[string]struct{}) - re := regexp.MustCompile(`\bgithub\.com/gofiber/contrib/(?:v\d+/)?([a-zA-Z0-9_]+)`) // capture module name + re := regexp.MustCompile(`(?:^|[^\w])github\.com/gofiber/contrib/(?:v\d+/)?([a-zA-Z0-9_]+)\b`) // capture module name, anchored to avoid partial matches err := filepath.WalkDir(cwd, func(path string, d os.DirEntry, err error) error { if err != nil { return fmt.Errorf("walk path: %w", err) @@ -287,7 +287,7 @@ func refreshTemplates(cmd *cobra.Command, cwd, hash string) (bool, error) { func findThirdPartyModules(cwd, repo string) (map[string]string, error) { modules := make(map[string]string) - re := regexp.MustCompile(fmt.Sprintf(`\bgithub\.com/gofiber/%s/([a-zA-Z0-9_]+)(?:/(v\d+))?`, regexp.QuoteMeta(repo))) + re := regexp.MustCompile(fmt.Sprintf(`(?:^|[^\w])github\.com/gofiber/%s/([a-zA-Z0-9_]+)(?:/(v\d+))?\b`, regexp.QuoteMeta(repo))) err := filepath.WalkDir(cwd, func(path string, d os.DirEntry, err error) error { if err != nil { return fmt.Errorf("walk path: %w", err)