From 09509fa3a288bb5148bf0871ef53424f1b038e6d Mon Sep 17 00:00:00 2001 From: RW Date: Sun, 24 Aug 2025 16:11:32 +0200 Subject: [PATCH] Preserve dependency versions during migration --- cmd/internal/migrations/dependencies.go | 48 +++++++++++++++++--- cmd/internal/migrations/dependencies_test.go | 16 ++++++- cmd/internal/migrations/lists.go | 16 ++++++- cmd/migrate.go | 30 +++++++++++- cmd/migrate_test.go | 2 +- 5 files changed, 99 insertions(+), 13 deletions(-) diff --git a/cmd/internal/migrations/dependencies.go b/cmd/internal/migrations/dependencies.go index 27805ec..7dcceca 100644 --- a/cmd/internal/migrations/dependencies.go +++ b/cmd/internal/migrations/dependencies.go @@ -18,12 +18,12 @@ import ( var ExecCommand = exec.Command // MigrateDependencies ensures that dependencies shared with Fiber are at least -// the versions required by the target Fiber release. +// the versions required by the target Fiber release, and preserves higher +// versions already declared by the project. // -// It updates go.mod files that already require a dependency also required by -// Fiber, bumping the version when it is lower than Fiber's requirement. No -// changes are made if the existing version is equal or higher. -func MigrateDependencies(cmd *cobra.Command, cwd string, _, target *semver.Version) error { +// The current map contains the dependency versions present before any +// migrations ran, keyed by module directory. +func MigrateDependencies(cmd *cobra.Command, cwd string, current map[string]map[string]*semver.Version, target *semver.Version) error { fiberModule := fmt.Sprintf("github.com/gofiber/fiber/v%d@v%s", target.Major(), target.String()) c := ExecCommand("go", "mod", "download", "-json", fiberModule) @@ -80,17 +80,22 @@ func MigrateDependencies(cmd *cobra.Command, cwd string, _, target *semver.Versi } changed := false + orig := current[dir] for _, r := range mf.Require { targetVer, ok := deps[r.Mod.Path] if !ok { continue } + maxVer := targetVer + if v, ok := orig[r.Mod.Path]; ok && v.GreaterThan(maxVer) { + maxVer = v + } currVer, err := semver.NewVersion(strings.TrimPrefix(r.Mod.Version, "v")) if err != nil { return fmt.Errorf("parse %s version in %s: %w", r.Mod.Path, modFile, err) } - if currVer.LessThan(targetVer) { - r.Mod.Version = "v" + targetVer.String() + if currVer.LessThan(maxVer) { + r.Mod.Version = "v" + maxVer.String() changed = true } } @@ -113,3 +118,32 @@ func MigrateDependencies(cmd *cobra.Command, cwd string, _, target *semver.Versi } return nil } + +func dependencyVersions(root string) (map[string]map[string]*semver.Version, error) { + dirs, err := fiberModuleDirs(root) + if err != nil { + return nil, fmt.Errorf("find modules: %w", err) + } + deps := make(map[string]map[string]*semver.Version, len(dirs)) + for _, dir := range dirs { + modFile := filepath.Join(dir, "go.mod") + b, err := os.ReadFile(modFile) // #nosec G304 + if err != nil { + return nil, fmt.Errorf("read %s: %w", modFile, err) + } + mf, err := modfile.Parse(modFile, b, nil) + if err != nil { + return nil, fmt.Errorf("parse %s: %w", modFile, err) + } + m := make(map[string]*semver.Version, len(mf.Require)) + for _, r := range mf.Require { + v, err := semver.NewVersion(strings.TrimPrefix(r.Mod.Version, "v")) + if err != nil { + return nil, fmt.Errorf("parse %s version in %s: %w", r.Mod.Path, modFile, err) + } + m[r.Mod.Path] = v + } + deps[dir] = m + } + return deps, nil +} diff --git a/cmd/internal/migrations/dependencies_test.go b/cmd/internal/migrations/dependencies_test.go index 30fa605..91eaa79 100644 --- a/cmd/internal/migrations/dependencies_test.go +++ b/cmd/internal/migrations/dependencies_test.go @@ -46,7 +46,13 @@ require ( var buf bytes.Buffer cmd := newCmd(&buf) target := semver.MustParse("3.0.0") - require.NoError(t, migrations.MigrateDependencies(cmd, dir, nil, target)) + curr := map[string]map[string]*semver.Version{ + dir: { + "github.com/valyala/fasthttp": semver.MustParse("1.0.0"), + "github.com/andybalholm/brotli": semver.MustParse("1.2.0"), + }, + } + require.NoError(t, migrations.MigrateDependencies(cmd, dir, curr, target)) content := readFile(t, filepath.Join(dir, "go.mod")) assert.Contains(t, content, "github.com/valyala/fasthttp v1.10.0") @@ -87,7 +93,13 @@ require ( var buf bytes.Buffer cmd := newCmd(&buf) target := semver.MustParse("3.0.0") - require.NoError(t, migrations.MigrateDependencies(cmd, dir, nil, target)) + curr := map[string]map[string]*semver.Version{ + dir: { + "github.com/valyala/fasthttp": semver.MustParse("1.10.0"), + "github.com/andybalholm/brotli": semver.MustParse("1.2.0"), + }, + } + require.NoError(t, migrations.MigrateDependencies(cmd, dir, curr, target)) content := readFile(t, filepath.Join(dir, "go.mod")) assert.Contains(t, content, "github.com/valyala/fasthttp v1.10.0") diff --git a/cmd/internal/migrations/lists.go b/cmd/internal/migrations/lists.go index dd8ca56..a2c43fd 100644 --- a/cmd/internal/migrations/lists.go +++ b/cmd/internal/migrations/lists.go @@ -30,7 +30,7 @@ type Migration struct { // Example structure: // {"from": ">=2.0.0", "to": "<=3.*.*", "fn": [MigrateFN, MigrateFN]} var Migrations = []Migration{ - {From: ">=1.0.0-0", To: ">=0.0.0-0", Functions: []MigrationFn{MigrateGoPkgs, MigrateDependencies}}, + {From: ">=1.0.0-0", To: ">=0.0.0-0", Functions: []MigrationFn{MigrateGoPkgs}}, { From: ">=2.0.0-0", To: "<4.0.0-0", @@ -94,6 +94,14 @@ func migrationName(fn MigrationFn) string { // 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 { var errs []error + var origDeps map[string]map[string]*semver.Version + if !skipGoMod { + var err error + origDeps, err = dependencyVersions(cwd) + if err != nil { + return fmt.Errorf("record dependencies: %w", err) + } + } for _, m := range Migrations { toC, err := semver.NewConstraint(m.To) if err != nil { @@ -136,6 +144,12 @@ func DoMigration(cmd *cobra.Command, cwd string, curr, target *semver.Version, s if err := internal.RunGoMod(cwd); err != nil { errs = append(errs, fmt.Errorf("go mod: %w", err)) } + if err := MigrateDependencies(cmd, cwd, origDeps, target); err != nil { + errs = append(errs, fmt.Errorf("migrate dependencies: %w", err)) + } + if err := internal.RunGoMod(cwd); err != nil { + errs = append(errs, fmt.Errorf("go mod: %w", err)) + } } if len(errs) > 0 { diff --git a/cmd/migrate.go b/cmd/migrate.go index 3e58c58..2657335 100644 --- a/cmd/migrate.go +++ b/cmd/migrate.go @@ -3,7 +3,9 @@ package cmd import ( "context" "encoding/json" + "errors" "fmt" + "io" "net/http" "os" "strconv" @@ -136,6 +138,8 @@ func pseudoVersionFromHash(base *semver.Version, hash string) (string, error) { if err != nil { return "", fmt.Errorf("create http request: %w", err) } + req.Header.Set("User-Agent", "fiber-cli") + client := http.Client{} res, err := client.Do(req) if err != nil { @@ -146,12 +150,22 @@ func pseudoVersionFromHash(base *semver.Version, hash string) (string, error) { fmt.Fprintf(os.Stderr, "failed to close response body: %v\n", err) } }() + if res.StatusCode != http.StatusOK { + msg, err := io.ReadAll(res.Body) + if err != nil || len(msg) == 0 { + msg = []byte(res.Status) + } + return "", fmt.Errorf("http request failed: %s", strings.TrimSpace(string(msg))) + } var data struct { Commit struct { Committer struct { - Date time.Time `json:"date"` + Date *time.Time `json:"date"` } `json:"committer"` + Author struct { + Date *time.Time `json:"date"` + } `json:"author"` } `json:"commit"` SHA string `json:"sha"` } @@ -159,6 +173,18 @@ func pseudoVersionFromHash(base *semver.Version, hash string) (string, error) { return "", fmt.Errorf("decode response: %w", err) } + var commitTime time.Time + switch { + case data.Commit.Committer.Date != nil && !data.Commit.Committer.Date.IsZero(): + commitTime = *data.Commit.Committer.Date + case data.Commit.Author.Date != nil && !data.Commit.Author.Date.IsZero(): + commitTime = *data.Commit.Author.Date + default: + return "", errors.New("commit date not found") + } + + commitTime = commitTime.UTC().Truncate(time.Second) + short := data.SHA if short == "" { short = hash @@ -166,6 +192,6 @@ func pseudoVersionFromHash(base *semver.Version, hash string) (string, error) { if len(short) > 12 { short = short[:12] } - pv := module.PseudoVersion("v"+strconv.FormatUint(base.Major(), 10), "v"+base.String(), data.Commit.Committer.Date, short) + pv := module.PseudoVersion("v"+strconv.FormatUint(base.Major(), 10), "v"+base.String(), commitTime, short) return strings.TrimPrefix(pv, "v"), nil } diff --git a/cmd/migrate_test.go b/cmd/migrate_test.go index ba5828f..6e53b21 100644 --- a/cmd/migrate_test.go +++ b/cmd/migrate_test.go @@ -283,7 +283,7 @@ require github.com/gofiber/fiber/v3 v3.0.0 require.NoError(t, err) assert.Contains(t, out, "Migration from Fiber 2.0.0 to 3.0.0") assert.NotContains(t, out, "Migrating Go packages") - assert.Len(t, cmds, 3) + assert.Len(t, cmds, 6) }) t.Run("force skip go mod", func(t *testing.T) {