diff --git a/README.md b/README.md index fccf2e8..64fc17e 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ - Support for listing installed versions of Go - Support for installing multiple versions of Go locally - Support for uninstalling installed versions of Go +- Support for removing superseded versions, keeping the newest of each minor series - Support for freely switching between installed versions of Go - Support for clearing package file cache - Support for self-updating software (>= 1.5.0) @@ -159,6 +160,17 @@ $ g uninstall 1.19.10 Uninstalled go1.19.10 ``` +To remove superseded installed versions, keeping only the newest version of each minor series (the version currently in use is never removed). Use `--dry-run` to preview without removing: + +```shell +$ g prune --dry-run +Would remove 1.19.10 +Would remove 1.20.4 +$ g prune +Removed 1.19.10 +Removed 1.20.4 +``` + To clear the package file cache for Go installations: ```shell diff --git a/README_CN.md b/README_CN.md index 999e14d..3304438 100644 --- a/README_CN.md +++ b/README_CN.md @@ -20,6 +20,7 @@ - 支持列出已安装的 go 版本号 - 支持在本地安装多个 go 版本 - 支持卸载已安装的 go 版本 +- 支持清理已被取代的 go 版本(每个次要版本系列保留最新版) - 支持在已安装的 go 版本之间自由切换 - 支持清空安装包文件缓存 - 支持软件自我更新(>= 1.5.0) @@ -159,6 +160,17 @@ $ g uninstall 1.19.10 Uninstalled go1.19.10 ``` +清理已被取代的 go 版本,即每个次要版本系列只保留最新版(当前使用中的版本不会被删除)。可使用`--dry-run`参数预览而不实际删除: + +```shell +$ g prune --dry-run +Would remove 1.19.10 +Would remove 1.20.4 +$ g prune +Removed 1.19.10 +Removed 1.20.4 +``` + 清空 go 安装包文件缓存 ```shell diff --git a/cli/commands.go b/cli/commands.go index 5cc7390..019e6ba 100644 --- a/cli/commands.go +++ b/cli/commands.go @@ -91,6 +91,18 @@ var ( UsageText: "g uninstall ", Action: uninstall, }, + { + Name: "prune", + Usage: "Remove superseded versions, keeping the newest of each minor series in use", + UsageText: "g prune", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "dry-run", + Usage: "Show what would be removed without removing", + }, + }, + Action: prune, + }, { Name: "update", Usage: "Download and install updates to g", diff --git a/cli/prune.go b/cli/prune.go new file mode 100644 index 0000000..1d8852d --- /dev/null +++ b/cli/prune.go @@ -0,0 +1,81 @@ +// Copyright (c) 2019 voidint +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package cli + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/urfave/cli/v2" + "github.com/voidint/g/version" +) + +func prune(ctx *cli.Context) error { + items, err := listLocalVersions(versionsDir) + if err != nil { + return cli.Exit(errstring(err), 1) + } + if len(items) == 0 { + fmt.Println("No version installed yet") + return nil + } + + doomed := superseded(items, inuse(goroot)) + if len(doomed) == 0 { + fmt.Println("Nothing to remove") + return nil + } + + dryRun := ctx.Bool("dry-run") + for _, v := range doomed { + if dryRun { + fmt.Printf("Would remove %s\n", v.Name()) + continue + } + if err := os.RemoveAll(filepath.Join(versionsDir, v.Name())); err != nil { + return cli.Exit(wrapstring(fmt.Sprintf("Failed to remove %q: %s", v.Name(), err.Error())), 1) + } + fmt.Printf("Removed %s\n", v.Name()) + } + return nil +} + +// superseded returns the versions that have a newer version in the same minor +// series, excluding the version currently in use. It assumes items are sorted +// in ascending order (see listLocalVersions). +func superseded(items []*version.Version, inuse string) (doomed []*version.Version) { + for i, v := range items { + if v.Name() == inuse { + continue // never remove the version in use + } + // Same-series versions are adjacent in ascending order, so a version + // is superseded when its successor belongs to the same minor series. + if i+1 < len(items) && sameMinorSeries(v, items[i+1]) { + doomed = append(doomed, v) + } + } + return doomed +} + +// sameMinorSeries reports whether two versions belong to the same major.minor series. +func sameMinorSeries(a, b *version.Version) bool { + return a.Major() == b.Major() && a.Minor() == b.Minor() +} diff --git a/cli/prune_test.go b/cli/prune_test.go new file mode 100644 index 0000000..8d2c8da --- /dev/null +++ b/cli/prune_test.go @@ -0,0 +1,107 @@ +// Copyright (c) 2019 voidint +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package cli + +import ( + "sort" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/voidint/g/version" +) + +func TestSuperseded(t *testing.T) { + tests := []struct { + name string + install []string + inuse string + want []string + }{ + { + name: "no version installed", + install: nil, + inuse: "", + want: nil, + }, + { + name: "single version", + install: []string{"1.24.6"}, + inuse: "1.24.6", + want: nil, + }, + { + name: "one version per minor series", + install: []string{"1.22.12", "1.23.8", "1.24.6"}, + inuse: "1.24.6", + want: nil, + }, + { + name: "keeps the newest of each minor series", + install: []string{"1.19.10", "1.19.13", "1.20.4", "1.20.5", "1.24.5", "1.24.6"}, + inuse: "1.24.6", + want: []string{"1.19.10", "1.20.4", "1.24.5"}, + }, + { + name: "keeps the version in use even when superseded", + install: []string{"1.24.3", "1.24.5", "1.24.6"}, + inuse: "1.24.3", + want: []string{"1.24.5"}, + }, + { + name: "prerelease is superseded by the final release", + install: []string{"1.25rc1", "1.25.0"}, + inuse: "1.25.0", + want: []string{"1.25rc1"}, + }, + { + name: "keeps the newest prerelease within an unreleased series", + install: []string{"1.26beta1", "1.26rc1"}, + inuse: "1.26rc1", + want: []string{"1.26beta1"}, + }, + { + name: "different major versions are independent series", + install: []string{"1.24.1", "2.0.0", "2.0.1"}, + inuse: "1.24.1", + want: []string{"2.0.0"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + items := make([]*version.Version, 0, len(tt.install)) + for _, vname := range tt.install { + items = append(items, version.MustNew(vname)) + } + sort.Sort(version.Collection(items)) + + doomed := superseded(items, tt.inuse) + got := make([]string, 0, len(doomed)) + for _, v := range doomed { + got = append(got, v.Name()) + } + if tt.want == nil { + assert.Empty(t, got) + return + } + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/version/version.go b/version/version.go index 4b2fd73..fe16318 100644 --- a/version/version.go +++ b/version/version.go @@ -102,6 +102,16 @@ func (v *Version) Name() string { return v.name } +// Major returns the major number of the semantic version. +func (v *Version) Major() uint64 { + return v.sv.Major() +} + +// Minor returns the minor number of the semantic version. +func (v *Version) Minor() uint64 { + return v.sv.Minor() +} + // Packages returns all distribution packages for different OS/ARCH combinations. func (v *Version) Packages() []Package { items := make([]Package, 0, len(v.pkgs))