From a9c2806d1cb25bd4ff3ddc32837b09f0c7062ecf Mon Sep 17 00:00:00 2001 From: Mateusz Hawrus <48822818+nieomylnieja@users.noreply.github.com> Date: Thu, 7 Dec 2023 22:25:52 +0100 Subject: [PATCH] fix: use latest version returned by goproxy instead of manually finding latest (#17) --- command.go | 84 +++++++++++++++++++++----------------- command_test.go | 19 +++++++-- internal/go_list.go | 7 ++-- internal/go_proxy.go | 2 +- internal/module.go | 14 +++---- test/inputs/responses.json | 7 +++- 6 files changed, 80 insertions(+), 53 deletions(-) diff --git a/command.go b/command.go index 23a00e6..f15f2a1 100644 --- a/command.go +++ b/command.go @@ -2,6 +2,7 @@ package libyear import ( "context" + "errors" "log" "os" "slices" @@ -57,7 +58,7 @@ func (c Command) Run(ctx context.Context) error { mainModule.Time = time.Now() if !c.optionIsSet(OptionIncludeIndirect) { // Filter out indirect. - modules = slices.DeleteFunc(modules, func(module *internal.Module) bool { return module.IsIndirect }) + modules = slices.DeleteFunc(modules, func(module *internal.Module) bool { return module.Indirect }) } group, _ := c.newErrGroup(ctx) @@ -70,7 +71,7 @@ func (c Command) Run(ctx context.Context) error { } // Remove skipped modules. if c.optionIsSet(OptionSkipFresh) { - modules = slices.DeleteFunc(modules, func(module *internal.Module) bool { return module.IsFresh }) + modules = slices.DeleteFunc(modules, func(module *internal.Module) bool { return module.Skipped }) } // Aggregate results for main module. @@ -93,7 +94,21 @@ const secondsInYear = float64(365 * 24 * 60 * 60) func (c Command) runForModule(module *internal.Module) error { // We skip this module, unless we get to the end and manage to calculate libyear. - module.IsFresh = true + module.Skipped = true + + // Fetch latest. + latest, err := c.repo.GetLatestInfo(module.Path) + if err != nil { + return err + } + // It returns -1 (smaller), 0 (larger), or 1 (greater) when compared. + if module.Version.Compare(latest.Version) != -1 { + module.Latest = module + module.Time = latest.Time + return nil + } + module.Latest = latest + // Since we're parsing the go.mod file directly, we might need to fetch the Module.Time. if module.Time.IsZero() { fetchedModule, err := c.repo.GetInfo(module.Path, module.Version) @@ -102,55 +117,50 @@ func (c Command) runForModule(module *internal.Module) error { } module.Time = fetchedModule.Time } - // Fetch all versions. + + // The following calculations are based on https://ericbouwers.github.io/papers/icse15.pdf. + module.Libyear = calculateLibyear(module, latest) + if c.optionIsSet(OptionShowReleases) { + versions, err := c.getVersions(module) + if err == errNoVersions { + log.Printf("WARN: module '%s' does not have any versions", module.Path) + return nil + } + module.ReleasesDiff = calculateReleases(module, latest, versions) + } + if c.optionIsSet(OptionShowVersions) { + module.VersionsDiff = calculateVersions(module, latest) + } + + module.Skipped = false + return nil +} + +var errNoVersions = errors.New("no versions found") + +func (c Command) getVersions(module *internal.Module) ([]*semver.Version, error) { versions, err := c.repo.GetVersions(module.Path) if err != nil { - return err + return nil, err } if len(versions) == 0 { if module.Version.Prerelease() == "" { - log.Printf("WARN: module '%s' does not have any versions", module.Path) - return nil + return nil, errNoVersions } // Try fetching the versions from deps.dev. // Go list does not list prerelease versions, which is fine, // unless we're dealing with a prerelease version ourselves. versions, err = c.fallbackVersions.GetVersions(module.Path) if err != nil { - return err + return nil, err } // Check again. if len(versions) == 0 { - log.Printf("WARN: module '%s' does not have any versions", module.Path) - return nil + return nil, errNoVersions } } sort.Sort(semver.Collection(versions)) - - latestVersion := versions[len(versions)-1] - // It returns -1 (smaller), 0 (larger), or 1 (greater) when compared. - if module.Version.Compare(latestVersion) != -1 { - module.Latest = module - return nil - } - // Fetch latest. - latest, err := c.repo.GetInfo(module.Path, latestVersion) - if err != nil { - return err - } - module.Latest = latest - - // The following calculations are based on https://ericbouwers.github.io/papers/icse15.pdf. - module.Libyear = calculateLibyear(module, latest) - if c.optionIsSet(OptionShowReleases) { - module.ReleasesDiff = calculateReleases(module, versions) - } - if c.optionIsSet(OptionShowVersions) { - module.VersionsDiff = calculateVersions(module, latest) - } - - module.IsFresh = false - return nil + return versions, nil } func calculateLibyear(module, latest *internal.Module) float64 { @@ -158,9 +168,9 @@ func calculateLibyear(module, latest *internal.Module) float64 { return diff.Seconds() / secondsInYear } -func calculateReleases(module *internal.Module, versions []*semver.Version) int { +func calculateReleases(module, latest *internal.Module, versions []*semver.Version) int { currentIndex := slices.IndexFunc(versions, func(v *semver.Version) bool { return module.Version.Equal(v) }) - latestIndex := len(versions) - 1 + latestIndex := slices.IndexFunc(versions, func(v *semver.Version) bool { return latest.Version.Equal(v) }) // Example: // v: v1 | v2 | v3 | v4 // i: 0 1 2 3 > len == 4 diff --git a/command_test.go b/command_test.go index df8de4c..d0301f3 100644 --- a/command_test.go +++ b/command_test.go @@ -69,22 +69,27 @@ func TestCommand_calculateLibyear(t *testing.T) { func TestCommand_calculateReleases(t *testing.T) { tests := []struct { CurrentVersion string + LatestVersion string Versions []string Expected int }{ { CurrentVersion: "v0.9.0", + LatestVersion: "v1.0.0", Versions: []string{ "v0.9.0", "v0.9.1", "v0.9.2", "v0.10.0", "v1.0.0", + "v2.0.0-incompatible1", + "v2.0.0-incompatible2", }, Expected: 4, }, { CurrentVersion: "v0.10.0", + LatestVersion: "v1.0.0", Versions: []string{ "v0.9.1", "v0.9.2", @@ -95,6 +100,7 @@ func TestCommand_calculateReleases(t *testing.T) { }, { CurrentVersion: "v1.0.0", + LatestVersion: "v1.0.0", Versions: []string{ "v0.9.2", "v0.10.0", @@ -104,6 +110,7 @@ func TestCommand_calculateReleases(t *testing.T) { }, { CurrentVersion: "v1.0.0", + LatestVersion: "v1.0.0", Versions: []string{ "v1.0.0", }, @@ -118,6 +125,7 @@ func TestCommand_calculateReleases(t *testing.T) { } actual := calculateReleases( &internal.Module{Version: semver.MustParse(test.CurrentVersion)}, + &internal.Module{Version: semver.MustParse(test.LatestVersion)}, versions) assert.Equal(t, test.Expected, actual) }) @@ -193,10 +201,11 @@ func TestCommand_Fallback(t *testing.T) { semver.MustParse("v1.0.0"), semver.MustParse("v2.0.0"), }, - getInfoResponse: &internal.Module{}, + getInfoResponse: &internal.Module{}, + getLatestResponse: &internal.Module{Version: semver.MustParse("v2.0.0")}, } versionsGetter := &mockVersionsGetter{} - cmd := Command{repo: modulesRepo, fallbackVersions: versionsGetter} + cmd := Command{repo: modulesRepo, fallbackVersions: versionsGetter, opts: OptionShowReleases} err := cmd.runForModule(&internal.Module{Version: semver.MustParse("v1.0.0")}) @@ -208,9 +217,10 @@ func TestCommand_Fallback(t *testing.T) { modulesRepo := &mockModulesRepo{ getVersionsResponse: []*semver.Version{}, getInfoResponse: &internal.Module{}, + getLatestResponse: &internal.Module{Version: semver.MustParse("v2.0.0")}, } versionsGetter := &mockVersionsGetter{} - cmd := Command{repo: modulesRepo, fallbackVersions: versionsGetter} + cmd := Command{repo: modulesRepo, fallbackVersions: versionsGetter, opts: OptionShowReleases} // Don't call fallback if a version does not contain a prerelease. // We only expect GOPROXY to lack versions list when no semver version was released by a module. @@ -232,6 +242,7 @@ type mockModulesRepo struct { getVersionsCalledTimes int getVersionsResponse []*semver.Version getInfoResponse *internal.Module + getLatestResponse *internal.Module } func (m *mockModulesRepo) GetVersions(string) ([]*semver.Version, error) { @@ -248,7 +259,7 @@ func (m *mockModulesRepo) GetInfo(string, *semver.Version) (*internal.Module, er } func (m *mockModulesRepo) GetLatestInfo(string) (*internal.Module, error) { - panic("implement me") + return m.getLatestResponse, nil } type mockVersionsGetter struct { diff --git a/internal/go_list.go b/internal/go_list.go index 81be388..14b9f50 100644 --- a/internal/go_list.go +++ b/internal/go_list.go @@ -49,22 +49,23 @@ func (e *GoListExecutor) GetLatestInfo(path string) (*Module, error) { return e.getInfo(path, nil, true) } +// Fetch module details. func (e *GoListExecutor) getInfo(path string, version *semver.Version, latest bool) (*Module, error) { var versionStr string if latest { versionStr = "latest" } else { - versionStr = version.String() + versionStr = "v" + version.String() } // Try loading from cache. - if e.cache != nil { + if version != nil && e.cache != nil { m, loaded := e.cache.Load(path, version) if loaded { return m, nil } } // Fetch module details. - out, err := e.exec(path + "@v" + versionStr) + out, err := e.exec(path + "@" + versionStr) if err != nil { return nil, err } diff --git a/internal/go_proxy.go b/internal/go_proxy.go index abba351..07f6db7 100644 --- a/internal/go_proxy.go +++ b/internal/go_proxy.go @@ -64,7 +64,7 @@ func (c *GoProxyClient) GetLatestInfo(path string) (*Module, error) { func (c *GoProxyClient) getInfo(path string, version *semver.Version, latest bool) (*Module, error) { // Try loading from cache. - if c.cache != nil { + if version != nil && c.cache != nil { m, loaded := c.cache.Load(path, version) if loaded { return m, nil diff --git a/internal/module.go b/internal/module.go index 56125c9..c8b57b3 100644 --- a/internal/module.go +++ b/internal/module.go @@ -19,10 +19,10 @@ type Module struct { Version *semver.Version `json:"Version"` Time time.Time `json:"Time"` - IsIndirect bool `json:"-"` - IsFresh bool `json:"-"` - Latest *Module `json:"-"` - Libyear float64 `json:"-"` + Indirect bool `json:"-"` + Skipped bool `json:"-"` + Latest *Module `json:"-"` + Libyear float64 `json:"-"` // ReleasesDiff is the number of release versions between latest and current. ReleasesDiff int `json:"-"` // VersionsDiff is an array of 3 elements: major, minor and patch versions. @@ -64,9 +64,9 @@ func ReadGoMod(content []byte) (mainModule *Module, modules []*Module, err error return nil, nil, err } modules = append(modules, &Module{ - Path: require.Mod.Path, - Version: version, - IsIndirect: require.Indirect, + Path: require.Mod.Path, + Version: version, + Indirect: require.Indirect, }) } if modFile.Module == nil { diff --git a/test/inputs/responses.json b/test/inputs/responses.json index d77d369..458f221 100644 --- a/test/inputs/responses.json +++ b/test/inputs/responses.json @@ -218,12 +218,17 @@ "Version": "v1.3.2", "Time": "2023-06-08T06:14:45Z" }, + "github.com/!burnt!sushi/toml": { + "Path": "github.com/BurntSushi/toml", + "Version": "v2.0.0-experimental", + "Time": "2021-08-05T05:14:45Z" + }, "github.com/!burnt!sushi/toml/@latest": { "Path": "github.com/BurntSushi/toml", "Version": "v1.3.2", "Time": "2023-06-08T06:14:45Z" }, - "github.com/!burnt!sushi/toml/@v/list": "v0.1.0\nv0.2.0\nv0.3.0\nv0.3.1\nv0.4.0\nv0.4.1\nv1.0.0\nv1.1.0\nv1.2.0\nv1.2.1\nv1.3.0\nv1.3.1\nv1.3.2", + "github.com/!burnt!sushi/toml/@v/list": "v0.1.0\nv0.2.0\nv0.3.0\nv0.3.1\nv0.4.0\nv0.4.1\nv1.0.0\nv1.1.0\nv1.2.0\nv1.2.1\nv1.3.0\nv1.3.1\nv1.3.2\nv2.0.0-experimental", "github.com/test/test/@latest": "v1.0.0", "github.com/test/test/@v/v1.0.0.mod": "./test/inputs/go.mod" }