diff --git a/cmd/internal/migrations/common_test.go b/cmd/internal/migrations/common_test.go index 2e160ce..593e797 100644 --- a/cmd/internal/migrations/common_test.go +++ b/cmd/internal/migrations/common_test.go @@ -49,3 +49,28 @@ require github.com/gofiber/fiber/v2 v2.0.0` assert.Contains(t, mod, "github.com/gofiber/fiber/v3 v3.0.0") assert.Contains(t, buf.String(), "Migrating Go packages") } + +func Test_MigrateGoPkgs_WithHash(t *testing.T) { + dir, err := os.MkdirTemp("", "mgpkgs_hash") + require.NoError(t, err) + defer func() { require.NoError(t, os.RemoveAll(dir)) }() + + file := filepath.Join(dir, "main.go") + require.NoError(t, os.WriteFile(file, []byte("package main"), 0o600)) + + modContent := `module example + +go 1.22 + +require github.com/gofiber/fiber/v2 v2.0.0` + require.NoError(t, os.WriteFile(filepath.Join(dir, "go.mod"), []byte(modContent), 0o600)) + + var buf bytes.Buffer + cmd := newCmd(&buf) + target := semver.MustParse("3.0.1-0.20200102030405-abcdef123456") + require.NoError(t, migrations.MigrateGoPkgs(cmd, dir, nil, target)) + + mod := readFile(t, filepath.Join(dir, "go.mod")) + assert.Contains(t, mod, "github.com/gofiber/fiber/v3 v3.0.1-0.20200102030405-abcdef123456") + assert.Contains(t, buf.String(), "Migrating Go packages") +} diff --git a/cmd/migrate.go b/cmd/migrate.go index 3a96e56..ba3a532 100644 --- a/cmd/migrate.go +++ b/cmd/migrate.go @@ -1,19 +1,26 @@ package cmd import ( + "context" + "encoding/json" "fmt" + "net/http" "os" + "strconv" "strings" + "time" "github.com/Masterminds/semver/v3" "github.com/muesli/termenv" "github.com/spf13/cobra" + "golang.org/x/mod/module" "github.com/gofiber/cli/cmd/internal/migrations" ) func newMigrateCmd() *cobra.Command { var targetVersionS string + var targetHash string var force bool var skipGoMod bool var verbose bool @@ -27,11 +34,13 @@ func newMigrateCmd() *cobra.Command { cmd.Flags().BoolVarP(&force, "force", "f", false, "Force migration even if already on version") 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.RunE = func(cmd *cobra.Command, _ []string) error { return migrateRunE(cmd, MigrateOptions{ CurrentVersionFile: currentVersionFile, TargetVersionS: targetVersionS, + TargetHash: targetHash, Force: force, SkipGoMod: skipGoMod, Verbose: verbose, @@ -46,6 +55,7 @@ var migrateCmd = newMigrateCmd() type MigrateOptions struct { CurrentVersionFile string TargetVersionS string + TargetHash string Force bool SkipGoMod bool Verbose bool @@ -66,11 +76,24 @@ func migrateRunE(cmd *cobra.Command, opts MigrateOptions) error { } } opts.TargetVersionS = strings.TrimPrefix(opts.TargetVersionS, "v") - targetVersion, err := semver.NewVersion(opts.TargetVersionS) + baseVersion, err := semver.NewVersion(opts.TargetVersionS) if err != nil { return fmt.Errorf("invalid version for \"%s\": %w", opts.TargetVersionS, err) } + targetVersion := baseVersion + if opts.TargetHash != "" { + pv, err := pseudoVersionFromHash(baseVersion, opts.TargetHash) + if err != nil { + return fmt.Errorf("pseudo version: %w", err) + } + opts.TargetVersionS = pv + targetVersion, err = semver.NewVersion(pv) + if err != nil { + return fmt.Errorf("invalid pseudo version for \"%s\": %w", pv, err) + } + } + if !targetVersion.GreaterThan(currentVersion) && !(opts.Force && targetVersion.Equal(currentVersion)) { return fmt.Errorf("target version v%s is not greater than current version v%s", opts.TargetVersionS, currentVersionS) } @@ -102,3 +125,42 @@ 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 + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return "", fmt.Errorf("create http request: %w", err) + } + client := http.Client{} + res, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("http request failed: %w", err) + } + defer func() { + if err := res.Body.Close(); err != nil { + fmt.Fprintf(os.Stderr, "failed to close response body: %v\n", err) + } + }() + + var data struct { + Commit struct { + Committer struct { + Date time.Time `json:"date"` + } `json:"committer"` + } `json:"commit"` + } + if err := json.NewDecoder(res.Body).Decode(&data); err != nil { + return "", fmt.Errorf("decode response: %w", err) + } + + short := hash + if len(short) > 12 { + short = short[:12] + } + pv := module.PseudoVersion("v"+strconv.FormatUint(base.Major(), 10), "v"+base.String(), data.Commit.Committer.Date, short) + return strings.TrimPrefix(pv, "v"), nil +} diff --git a/cmd/migrate_test.go b/cmd/migrate_test.go index d58f8f2..20509b6 100644 --- a/cmd/migrate_test.go +++ b/cmd/migrate_test.go @@ -21,18 +21,19 @@ func readFileTB(tb testing.TB, path string) string { return string(b) } -func Test_Migrate_V2_to_V3(t *testing.T) { - dir, err := os.MkdirTemp("", "migrate_v2_v3") - require.NoError(t, err) - defer func() { require.NoError(t, os.RemoveAll(dir)) }() - - gomod := `module example.com/demo +const goModV2 = `module example.com/demo go 1.20 require github.com/gofiber/fiber/v2 v2.0.6 ` - require.NoError(t, os.WriteFile(filepath.Join(dir, "go.mod"), []byte(gomod), 0o600)) + +func Test_Migrate_V2_to_V3(t *testing.T) { + dir, err := os.MkdirTemp("", "migrate_v2_v3") + require.NoError(t, err) + defer func() { require.NoError(t, os.RemoveAll(dir)) }() + + require.NoError(t, os.WriteFile(filepath.Join(dir, "go.mod"), []byte(goModV2), 0o600)) main := `package main import ( @@ -118,13 +119,7 @@ func Test_Migrate_DefaultTarget(t *testing.T) { require.NoError(t, err) defer func() { require.NoError(t, os.RemoveAll(dir)) }() - gomod := `module example.com/demo - -go 1.20 - -require github.com/gofiber/fiber/v2 v2.0.6 -` - require.NoError(t, os.WriteFile(filepath.Join(dir, "go.mod"), []byte(gomod), 0o600)) + require.NoError(t, os.WriteFile(filepath.Join(dir, "go.mod"), []byte(goModV2), 0o600)) main := `package main import "github.com/gofiber/fiber/v2" @@ -303,3 +298,32 @@ func main() {}` content := readFileTB(t, filepath.Join(dir, "main.go")) assert.NotContains(t, content, "github.com/gofiber/fiber/v2") } + +func Test_Migrate_WithHash(t *testing.T) { + dir, err := os.MkdirTemp("", "migrate_hash") + require.NoError(t, err) + defer func() { require.NoError(t, os.RemoveAll(dir)) }() + + require.NoError(t, os.WriteFile(filepath.Join(dir, "go.mod"), []byte(goModV2), 0o600)) + require.NoError(t, os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main"), 0o600)) + + cwd, err := os.Getwd() + require.NoError(t, err) + require.NoError(t, os.Chdir(dir)) + defer func() { require.NoError(t, os.Chdir(cwd)) }() + + hash := "abcdef1234567890abcdef1234567890abcdef12" + httpmock.Activate() + defer httpmock.DeactivateAndReset() + commitURL := "https://api.github.com/repos/gofiber/fiber/commits/" + hash + httpmock.RegisterResponder(http.MethodGet, commitURL, httpmock.NewBytesResponder(200, []byte(`{"commit":{"committer":{"date":"2020-01-02T03:04:05Z"}}}`))) + + cmd := newMigrateCmd() + setupCmd() + defer teardownCmd() + _, err = runCobraCmd(cmd, "-t=3.0.0", "--hash="+hash) + require.NoError(t, err) + + gm := readFileTB(t, filepath.Join(dir, "go.mod")) + assert.Contains(t, gm, "github.com/gofiber/fiber/v3 v3.0.1-0.20200102030405-abcdef123456") +}