From 2db73a0a1a254ff512c6d1fac7ebc86a9ba41e06 Mon Sep 17 00:00:00 2001 From: thelovekesh Date: Tue, 26 May 2026 13:22:42 +0530 Subject: [PATCH 01/11] Remove github.com/pkg/errors --- go.mod | 1 - go.sum | 2 -- 2 files changed, 3 deletions(-) diff --git a/go.mod b/go.mod index 72240fd..3bcf432 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,6 @@ require ( github.com/moby/sys/sequential v0.6.0 github.com/moby/term v0.5.2 github.com/morikuni/aec v1.1.0 - github.com/pkg/errors v0.9.1 github.com/sirupsen/logrus v1.9.4 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 diff --git a/go.sum b/go.sum index 1ed8595..48c30cb 100644 --- a/go.sum +++ b/go.sum @@ -46,8 +46,6 @@ github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ= github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= From 4b88c1fe0a064c61df577b1b0dddaaa2cd020fc5 Mon Sep 17 00:00:00 2001 From: thelovekesh Date: Tue, 26 May 2026 13:34:55 +0530 Subject: [PATCH 02/11] MIgrate to std errors package --- cli/cobra.go | 3 +- cli/command/auth/login.go | 4 +- cli/command/auth/logout.go | 4 +- cli/command/init/init.go | 30 ++++++------- cli/command/install/install.go | 14 +++--- cli/command/install/run.go | 18 ++++---- cli/command/ls/ls.go | 6 +-- cli/command/outdated/outdated.go | 10 ++--- cli/command/publish/publish.go | 14 +++--- cli/command/uninstall/uninstall.go | 4 +- cli/command/whoami/whoami.go | 2 +- cli/command/why/why.go | 8 ++-- cli/required.go | 15 ++++--- cmd/wpm/wpm.go | 10 ++--- pkg/archive/time_windows.go | 2 +- pkg/config/config.go | 8 ++-- pkg/config/configfile/file.go | 7 +-- pkg/pm/installer/installer.go | 71 ++++++++++++++---------------- pkg/pm/resolution/resolver.go | 6 +-- pkg/pm/signatures/signatures.go | 5 +-- pkg/pm/workspace/lock.go | 10 ++--- pkg/pm/wpmjson/wpmjson.go | 10 ++--- pkg/pm/wpmlock/lockfile.go | 14 +++--- 23 files changed, 131 insertions(+), 144 deletions(-) diff --git a/cli/cobra.go b/cli/cobra.go index 1b99358..1da3b5a 100644 --- a/cli/cobra.go +++ b/cli/cobra.go @@ -9,7 +9,6 @@ import ( "github.com/fvbommel/sortorder" "github.com/moby/term" "github.com/morikuni/aec" - "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -101,7 +100,7 @@ var helpCommand = &cobra.Command{ RunE: func(c *cobra.Command, args []string) error { cmd, args, e := c.Root().Find(args) if cmd == nil || e != nil || len(args) > 0 { - return errors.Errorf("unknown help topic: %v", strings.Join(args, " ")) + return fmt.Errorf("unknown help topic: %v", strings.Join(args, " ")) } helpFunc := cmd.HelpFunc() helpFunc(cmd, args) diff --git a/cli/command/auth/login.go b/cli/command/auth/login.go index 4776c4d..28297f2 100644 --- a/cli/command/auth/login.go +++ b/cli/command/auth/login.go @@ -2,10 +2,10 @@ package auth import ( "context" + "errors" "fmt" "github.com/morikuni/aec" - "github.com/pkg/errors" "github.com/spf13/cobra" "go.wpm.so/cli/cli" @@ -53,7 +53,7 @@ func tokenStdinPrompt(ctx context.Context, wpmCli command.Cli, opts *loginOption wpmCli.Err().WriteString("\n") if token == "" { - return errors.Errorf("token cannot be empty") + return errors.New("token cannot be empty") } opts.token = token diff --git a/cli/command/auth/logout.go b/cli/command/auth/logout.go index 0a5af34..1466d15 100644 --- a/cli/command/auth/logout.go +++ b/cli/command/auth/logout.go @@ -1,9 +1,9 @@ package auth import ( + "errors" "fmt" - "github.com/pkg/errors" "github.com/spf13/cobra" "go.wpm.so/cli/cli" @@ -25,7 +25,7 @@ func runLogout(wpmCli command.Cli) error { cfg := wpmCli.ConfigFile() if cfg.AuthToken == "" { - return errors.Errorf("user must be logged in to perform this action") + return errors.New("user must be logged in to perform this action") } cfg.AuthToken = "" diff --git a/cli/command/init/init.go b/cli/command/init/init.go index 7cedaf1..71a6e1b 100644 --- a/cli/command/init/init.go +++ b/cli/command/init/init.go @@ -2,6 +2,7 @@ package init import ( "context" + "errors" "fmt" "os" "path/filepath" @@ -12,7 +13,6 @@ import ( "github.com/Masterminds/semver/v3" "github.com/morikuni/aec" - "github.com/pkg/errors" "github.com/spf13/cobra" "go.wpm.so/cli/cli/command" @@ -91,14 +91,14 @@ func NewInitCommand(wpmCli command.Cli) *cobra.Command { func runNewInit(ctx context.Context, wpmCli command.Cli, opts *initOptions) error { cwd, err := os.Getwd() if err != nil { - return errors.Wrap(err, "failed to get current working directory") + return fmt.Errorf("failed to get current working directory: %w", err) } wpmConfigFilePath := filepath.Join(cwd, wpmjson.ConfigFile) if _, err := os.Stat(wpmConfigFilePath); err == nil { - return errors.Errorf("%s already exists in %s", wpmjson.ConfigFile, cwd) + return fmt.Errorf("%s already exists in %s", wpmjson.ConfigFile, cwd) } else if !os.IsNotExist(err) { - return errors.Wrapf(err, "failed to check for existing %s", wpmjson.ConfigFile) + return fmt.Errorf("failed to check for existing %s: %w", wpmjson.ConfigFile, err) } wpmCfg := wpmjson.New() @@ -125,7 +125,7 @@ func runNewInit(ctx context.Context, wpmCli command.Cli, opts *initOptions) erro } if err := wpmCfg.Write(cwd); err != nil { - return errors.Wrap(err, "failed to write wpm.json") + return fmt.Errorf("failed to write wpm.json: %w", err) } _, _ = fmt.Fprintf(wpmCli.Out(), "config created at %s\n", wpmConfigFilePath) @@ -143,17 +143,17 @@ func extractPackageHeaders(wpmCli command.Cli, cwd string, opts *initOptions) (m mainFilePath := filepath.Join(cwd, "style.css") if _, err := os.Stat(mainFilePath); err != nil { if os.IsNotExist(err) && opts.version == "" { - return nil, "", errors.Errorf("style.css not found in %s", cwd) + return nil, "", fmt.Errorf("style.css not found in %s", cwd) } if !os.IsNotExist(err) { - return nil, "", errors.Wrapf(err, "failed to stat style.css") + return nil, "", fmt.Errorf("failed to stat style.css: %w", err) } return nil, "", nil } headers, hErr := parser.GetThemeHeaders(mainFilePath) if hErr != nil { if opts.version == "" { - return nil, "", errors.Wrapf(hErr, "failed to parse theme headers from style.css") + return nil, "", fmt.Errorf("failed to parse theme headers from style.css: %w", hErr) } return nil, "", nil } @@ -162,13 +162,13 @@ func extractPackageHeaders(wpmCli command.Cli, cwd string, opts *initOptions) (m case "plugin": dirEntries, dErr := os.ReadDir(cwd) if dErr != nil { - return nil, "", errors.Wrap(dErr, "failed to read current directory for plugin files") + return nil, "", fmt.Errorf("failed to read current directory for plugin files: %w", dErr) } foundPath, headers, fErr := findMainPluginFile(cwd, dirEntries) if fErr != nil { if opts.version == "" { - return nil, "", errors.Wrap(fErr, "failed to identify main plugin file") + return nil, "", fmt.Errorf("failed to identify main plugin file: %w", fErr) } return nil, "", nil } @@ -176,7 +176,7 @@ func extractPackageHeaders(wpmCli command.Cli, cwd string, opts *initOptions) (m return headers, headers.Version, nil default: - return nil, "", errors.Errorf("unsupported package type for existing project init: %s", opts.packageType) + return nil, "", fmt.Errorf("unsupported package type for existing project init: %s", opts.packageType) } } @@ -213,7 +213,7 @@ func resolveConfigVersion(wpmCli command.Cli, wpmCfg *wpmjson.Config, opts *init func runExistingInit(wpmCli command.Cli, opts *initOptions) error { cwd, err := os.Getwd() if err != nil { - return errors.Wrap(err, "failed to get current working directory") + return fmt.Errorf("failed to get current working directory: %w", err) } baseFiles, err := findExistingProjectFiles(cwd) @@ -236,7 +236,7 @@ func runExistingInit(wpmCli command.Cli, opts *initOptions) error { if baseFiles.readmeTxt != "" { readmeTxtContent, err := os.ReadFile(baseFiles.readmeTxt) if err != nil { - return errors.Wrap(err, "failed to read readme.txt") + return fmt.Errorf("failed to read readme.txt: %w", err) } readmeParser = parser.NewReadmeParser() readmeParser.Parse(string(readmeTxtContent)) @@ -399,7 +399,7 @@ func promptForConfig(ctx context.Context, wpmCli command.Cli, config *wpmjson.Co for { val, err := command.PromptForInput(ctx, wpmCli.In(), wpmCli.Out(), fmt.Sprintf("%s (%s): ", pf.Prompt.Msg, pf.Prompt.Default)) if err != nil { - return errors.Wrap(err, "failed to get prompt input") + return fmt.Errorf("failed to get prompt input: %w", err) } if err := pf.Prompt.Validate(val); err != nil { @@ -416,7 +416,7 @@ func findExistingProjectFiles(cwd string) (existingProjectFiles, error) { var paths existingProjectFiles files, err := os.ReadDir(cwd) if err != nil { - return paths, errors.Wrap(err, "failed to read current directory") + return paths, fmt.Errorf("failed to read current directory: %w", err) } for _, file := range files { diff --git a/cli/command/install/install.go b/cli/command/install/install.go index 26f936e..b1f967f 100644 --- a/cli/command/install/install.go +++ b/cli/command/install/install.go @@ -2,6 +2,7 @@ package install import ( "context" + "errors" "fmt" "os" "path/filepath" @@ -9,7 +10,6 @@ import ( "sync" "github.com/morikuni/aec" - "github.com/pkg/errors" "github.com/spf13/cobra" "golang.org/x/sync/errgroup" @@ -31,7 +31,7 @@ type installOptions struct { networkConcurrency int } -var runHelp = errors.New("RUN_HELP") +var errRunHelp = errors.New("RUN_HELP") func NewInstallCommand(wpmCli command.Cli) *cobra.Command { var opts installOptions @@ -48,7 +48,7 @@ func NewInstallCommand(wpmCli command.Cli) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { err := runInstall(cmd.Context(), wpmCli, opts, args) if err != nil { - if errors.Is(err, runHelp) { + if errors.Is(err, errRunHelp) { return cmd.Help() } @@ -81,7 +81,7 @@ func NewInstallCommand(wpmCli command.Cli) *cobra.Command { func runInstall(ctx context.Context, wpmCli command.Cli, opts installOptions, packages []string) error { cwd, err := os.Getwd() if err != nil { - return errors.Wrap(err, "failed to get current working directory") + return fmt.Errorf("failed to get current working directory: %w", err) } wpmCli.Output().Prettyln(output.Text{ @@ -101,7 +101,7 @@ func runInstall(ctx context.Context, wpmCli command.Cli, opts installOptions, pa }) }) if err != nil { - return errors.Wrap(err, "failed to acquire workspace lock") + return fmt.Errorf("failed to acquire workspace lock: %w", err) } defer func() { _ = lock.Release() @@ -114,7 +114,7 @@ func runInstall(ctx context.Context, wpmCli command.Cli, opts installOptions, pa if cfg == nil { if len(packages) == 0 { - return runHelp + return errRunHelp } cfg = wpmjson.New() @@ -201,7 +201,7 @@ func addPackages(ctx context.Context, config *wpmjson.Config, wpmCli command.Cli g.Go(func() error { manifest, err := client.GetPackageManifest(ctx, name, versionOrTag, true) if err != nil { - return errors.Wrapf(err, "failed to fetch package %s@%s", name, versionOrTag) + return fmt.Errorf("failed to fetch package %s@%s: %w", name, versionOrTag, err) } mu.Lock() diff --git a/cli/command/install/run.go b/cli/command/install/run.go index d15b48e..e16e37e 100644 --- a/cli/command/install/run.go +++ b/cli/command/install/run.go @@ -2,6 +2,7 @@ package install import ( "context" + "errors" "fmt" "maps" "path/filepath" @@ -9,7 +10,6 @@ import ( "strconv" "github.com/morikuni/aec" - "github.com/pkg/errors" "go.wpm.so/cli/cli/command" "go.wpm.so/cli/pkg/output" @@ -67,12 +67,12 @@ func Run(ctx context.Context, cwd string, wpmCli command.Cli, opts RunOptions) e return errors.New("wpm.json config is required") } if err := wpmCfg.ValidateDependencyNames(); err != nil { - return errors.Wrap(err, "invalid dependency name in wpm.json") + return fmt.Errorf("invalid dependency name in wpm.json: %w", err) } lock, err := wpmlock.Read(cwd) if err != nil { - return errors.Wrap(err, "failed to read lockfile") + return fmt.Errorf("failed to read lockfile: %w", err) } if lock == nil { lock = wpmlock.New() @@ -83,7 +83,7 @@ func Run(ctx context.Context, cwd string, wpmCli command.Cli, opts RunOptions) e client, err := wpmCli.RegistryClient() if err != nil { - return errors.Wrap(err, "failed to create registry client") + return fmt.Errorf("failed to create registry client: %w", err) } resolver := resolution.New(wpmCfg, lock, client) @@ -102,7 +102,7 @@ func Run(ctx context.Context, cwd string, wpmCli command.Cli, opts RunOptions) e if len(plan) == 0 { if opts.SaveConfig { if err := wpmCfg.Write(cwd); err != nil { - return errors.Wrap(err, "failed to save wpm.json") + return fmt.Errorf("failed to save wpm.json: %w", err) } } @@ -120,12 +120,12 @@ func Run(ctx context.Context, cwd string, wpmCli command.Cli, opts RunOptions) e wpmCli.Output().ErrorWrite(fmt.Sprintf(format+"\n", args...)) }) if err != nil { - return errors.Wrap(err, "failed to initialize installer") + return fmt.Errorf("failed to initialize installer: %w", err) } defer func() { _ = inst.Close() }() if err := inst.InstallAll(ctx, plan, installerProgress(wpmCli.Output())); err != nil { - return errors.Wrap(err, "installation failed") + return fmt.Errorf("installation failed: %w", err) } // @todo: binary linking @@ -134,14 +134,14 @@ func Run(ctx context.Context, cwd string, wpmCli command.Cli, opts RunOptions) e updateLockPackages(lock, resolved) if err := lock.Write(cwd); err != nil { - return errors.Wrap(err, "failed to save lockfile") + return fmt.Errorf("failed to save lockfile: %w", err) } // @todo: run root lifecycle scripts if opts.SaveConfig { if err := wpmCfg.Write(cwd); err != nil { - return errors.Wrap(err, "failed to save wpm.json") + return fmt.Errorf("failed to save wpm.json: %w", err) } } diff --git a/cli/command/ls/ls.go b/cli/command/ls/ls.go index 9afebce..723f9a0 100644 --- a/cli/command/ls/ls.go +++ b/cli/command/ls/ls.go @@ -1,6 +1,7 @@ package ls import ( + "errors" "fmt" "io" "maps" @@ -9,7 +10,6 @@ import ( "sort" "github.com/morikuni/aec" - "github.com/pkg/errors" "github.com/spf13/cobra" "go.wpm.so/cli/cli" @@ -42,7 +42,7 @@ func NewLsCommand(wpmCli command.Cli) *cobra.Command { func runLs(wpmCli command.Cli, opts lsOptions) error { cwd, err := os.Getwd() if err != nil { - return errors.Wrap(err, "failed to get current working directory") + return fmt.Errorf("failed to get current working directory: %w", err) } config, err := wpmjson.Read(cwd) @@ -55,7 +55,7 @@ func runLs(wpmCli command.Cli, opts lsOptions) error { lock, err := wpmlock.Read(cwd) if err != nil { - return errors.Wrap(err, "failed to read lockfile") + return fmt.Errorf("failed to read lockfile: %w", err) } if lock == nil { return errors.New("no wpm.lock found, you need to run `wpm install` first") diff --git a/cli/command/outdated/outdated.go b/cli/command/outdated/outdated.go index f3fd2af..77c3793 100644 --- a/cli/command/outdated/outdated.go +++ b/cli/command/outdated/outdated.go @@ -2,6 +2,7 @@ package outdated import ( "context" + "errors" "fmt" "io" "os" @@ -10,7 +11,6 @@ import ( "github.com/Masterminds/semver/v3" "github.com/morikuni/aec" - "github.com/pkg/errors" "github.com/spf13/cobra" "golang.org/x/sync/errgroup" @@ -43,7 +43,7 @@ type depCheck struct { func runOutdated(ctx context.Context, wpmCli command.Cli) error { cwd, err := os.Getwd() if err != nil { - return errors.Wrap(err, "failed to get current working directory") + return fmt.Errorf("failed to get current working directory: %w", err) } config, err := wpmjson.Read(cwd) @@ -57,10 +57,10 @@ func runOutdated(ctx context.Context, wpmCli command.Cli) error { lock, err := wpmlock.Read(cwd) if err != nil { - return errors.Wrap(err, "failed to read lockfile") + return fmt.Errorf("failed to read lockfile: %w", err) } if lock == nil { - return errors.New("no wpm.lock found. Run 'wpm install' first to generate a lockfile.") + return errors.New("no wpm.lock found, run 'wpm install' first to generate a lockfile") } wpmCli.Output().Prettyln(output.Text{ @@ -146,7 +146,7 @@ func findOutdatedPackages(ctx context.Context, wpmCli command.Cli, checks []depC g.Go(func() error { manifest, err := client.GetPackageManifest(ctx, check.name, "latest", true) if err != nil { - return errors.Wrapf(err, "failed to fetch package %s@%s", check.name, "latest") + return fmt.Errorf("failed to fetch package %s@%s: %w", check.name, "latest", err) } currentVer, err1 := semver.NewVersion(check.version) diff --git a/cli/command/publish/publish.go b/cli/command/publish/publish.go index 9598df6..b6ecb53 100644 --- a/cli/command/publish/publish.go +++ b/cli/command/publish/publish.go @@ -4,6 +4,7 @@ import ( "context" "crypto/sha256" "encoding/base64" + "errors" "fmt" "hash" "io" @@ -14,7 +15,6 @@ import ( "github.com/docker/go-units" "github.com/morikuni/aec" - "github.com/pkg/errors" "github.com/spf13/cobra" "go.wpm.so/cli/cli" @@ -147,7 +147,7 @@ func (c *tarballSizeCounter) Write(p []byte) (n int, err error) { func runPublish(ctx context.Context, wpmCli command.Cli, opts publishOptions) error { cwd, err := os.Getwd() if err != nil { - return errors.Wrap(err, "failed to get current working directory") + return fmt.Errorf("failed to get current working directory: %w", err) } visibility := types.PackageVisibility(opts.access) @@ -167,7 +167,7 @@ func runPublish(ctx context.Context, wpmCli command.Cli, opts publishOptions) er tempFile, err := os.CreateTemp("", "wpm-tarball-*.tar.zst") if err != nil { - return errors.Wrap(err, "failed to create temporary tarball") + return fmt.Errorf("failed to create temporary tarball: %w", err) } defer func() { _ = tempFile.Close() @@ -176,7 +176,7 @@ func runPublish(ctx context.Context, wpmCli command.Cli, opts publishOptions) er tarballer, err := pack(cwd, opts, wpmCli.Output()) if err != nil { - return errors.Wrap(err, "failed to pack the package into a tarball") + return fmt.Errorf("failed to pack the package into a tarball: %w", err) } defer func() { _ = tarballer.Close() }() @@ -210,7 +210,7 @@ func runPublish(ctx context.Context, wpmCli command.Cli, opts publishOptions) er readme, err := getReadme(cwd) if err != nil { - return errors.Wrap(err, "failed to read readme file") + return fmt.Errorf("failed to read readme file: %w", err) } pkgManifest := buildManifest(wpmJson, opts, visibility, digest, counter.total, tarballer, readme) @@ -241,7 +241,7 @@ func packIntoTarball(wpmCli command.Cli, opts publishOptions, tarballer *archive packFn := func() error { if _, err := io.Copy(multiWriter, tarballer.Reader()); err != nil { - return errors.Wrap(err, "failed to process tarball") + return fmt.Errorf("failed to process tarball: %w", err) } return nil } @@ -318,7 +318,7 @@ func uploadPackage(ctx context.Context, wpmCli command.Cli, registryClient regis "publishing package", func() error { if _, err := tempFile.Seek(0, io.SeekStart); err != nil { - return errors.Wrap(err, "failed to seek to beginning of tarball") + return fmt.Errorf("failed to seek to beginning of tarball: %w", err) } return registryClient.PutPackage(ctx, pkgManifest, tempFile) }, diff --git a/cli/command/uninstall/uninstall.go b/cli/command/uninstall/uninstall.go index b23dff5..4f86307 100644 --- a/cli/command/uninstall/uninstall.go +++ b/cli/command/uninstall/uninstall.go @@ -2,12 +2,12 @@ package uninstall import ( "context" + "errors" "fmt" "os" "path/filepath" "github.com/morikuni/aec" - "github.com/pkg/errors" "github.com/spf13/cobra" "go.wpm.so/cli/cli" @@ -59,7 +59,7 @@ func runUninstall(ctx context.Context, wpmCli command.Cli, packages []string) er }) }) if err != nil { - return errors.Wrap(err, "failed to acquire workspace lock") + return fmt.Errorf("failed to acquire workspace lock: %w", err) } defer func() { _ = lock.Release() diff --git a/cli/command/whoami/whoami.go b/cli/command/whoami/whoami.go index 0e727c6..ddf88c0 100644 --- a/cli/command/whoami/whoami.go +++ b/cli/command/whoami/whoami.go @@ -2,8 +2,8 @@ package whoami import ( "context" + "errors" - "github.com/pkg/errors" "github.com/spf13/cobra" "go.wpm.so/cli/cli" diff --git a/cli/command/why/why.go b/cli/command/why/why.go index 99a8b45..ed0195e 100644 --- a/cli/command/why/why.go +++ b/cli/command/why/why.go @@ -1,6 +1,7 @@ package why import ( + "errors" "fmt" "os" "path/filepath" @@ -9,7 +10,6 @@ import ( "strings" "github.com/morikuni/aec" - "github.com/pkg/errors" "github.com/spf13/cobra" "go.wpm.so/cli/cli" @@ -41,7 +41,7 @@ func NewWhyCommand(wpmCli command.Cli) *cobra.Command { func runWhy(wpmCli command.Cli, targetPkg string) error { cwd, err := os.Getwd() if err != nil { - return errors.Wrap(err, "failed to get current working directory") + return fmt.Errorf("failed to get current working directory: %w", err) } config, err := wpmjson.Read(cwd) @@ -55,10 +55,10 @@ func runWhy(wpmCli command.Cli, targetPkg string) error { lock, err := wpmlock.Read(cwd) if err != nil { - return errors.Wrap(err, "failed to read lockfile") + return fmt.Errorf("failed to read lockfile: %w", err) } if lock == nil { - return errors.New("no wpm.lock found. Run 'wpm install' first to generate a lockfile.") + return errors.New("no wpm.lock found, run 'wpm install' first to generate a lockfile") } if _, exists := lock.Packages[targetPkg]; !exists { diff --git a/cli/required.go b/cli/required.go index 0f962a4..76829a0 100644 --- a/cli/required.go +++ b/cli/required.go @@ -1,7 +1,8 @@ package cli import ( - "github.com/pkg/errors" + "fmt" + "github.com/spf13/cobra" ) @@ -12,7 +13,7 @@ func NoArgs(cmd *cobra.Command, args []string) error { } if cmd.HasSubCommands() { - return errors.Errorf( + return fmt.Errorf( "%[1]s: unknown command: %[2]s %[3]s\n\nUsage: %[4]s\n\nRun '%[2]s --help' for more information", binName(cmd), cmd.CommandPath(), @@ -21,7 +22,7 @@ func NoArgs(cmd *cobra.Command, args []string) error { ) } - return errors.Errorf( + return fmt.Errorf( "%[1]s: '%[2]s' accepts no arguments\n\nUsage: %[3]s\n\nRun '%[2]s --help' for more information", binName(cmd), cmd.CommandPath(), @@ -35,7 +36,7 @@ func RequiresMinArgs(minArgs int) cobra.PositionalArgs { if len(args) >= minArgs { return nil } - return errors.Errorf( + return fmt.Errorf( "%[1]s: '%[2]s' requires at least %[3]d %[4]s\n\nUsage: %[5]s\n\nSee '%[2]s --help' for more information", binName(cmd), cmd.CommandPath(), @@ -52,7 +53,7 @@ func RequiresMaxArgs(maxArgs int) cobra.PositionalArgs { if len(args) <= maxArgs { return nil } - return errors.Errorf( + return fmt.Errorf( "%[1]s: '%[2]s' requires at most %[3]d %[4]s\n\nUsage: %[5]s\n\nSRun '%[2]s --help' for more information", binName(cmd), cmd.CommandPath(), @@ -69,7 +70,7 @@ func RequiresRangeArgs(minArgs, maxArgs int) cobra.PositionalArgs { if len(args) >= minArgs && len(args) <= maxArgs { return nil } - return errors.Errorf( + return fmt.Errorf( "%[1]s: '%[2]s' requires at least %[3]d and at most %[4]d %[5]s\n\nUsage: %[6]s\n\nRun '%[2]s --help' for more information", binName(cmd), cmd.CommandPath(), @@ -87,7 +88,7 @@ func ExactArgs(number int) cobra.PositionalArgs { if len(args) == number { return nil } - return errors.Errorf( + return fmt.Errorf( "%[1]s: '%[2]s' requires %[3]d %[4]s\n\nUsage: %[5]s\n\nRun '%[2]s --help' for more information", binName(cmd), cmd.CommandPath(), diff --git a/cmd/wpm/wpm.go b/cmd/wpm/wpm.go index e0c042e..95e53c7 100644 --- a/cmd/wpm/wpm.go +++ b/cmd/wpm/wpm.go @@ -2,6 +2,7 @@ package main import ( "context" + "errors" "fmt" "io" "os" @@ -10,7 +11,6 @@ import ( "github.com/containerd/errdefs" "github.com/morikuni/aec" - "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -32,7 +32,7 @@ func (errCtxSignalTerminated) Error() string { func main() { err := wpmMain(context.Background()) - if errors.As(err, &errCtxSignalTerminated{}) { + if _, ok := errors.AsType[errCtxSignalTerminated](err); ok { os.Exit(getExitCode(err)) } @@ -91,8 +91,7 @@ func getExitCode(err error) int { return 0 } - var userTerminatedErr errCtxSignalTerminated - if errors.As(err, &userTerminatedErr) { + if userTerminatedErr, ok := errors.AsType[errCtxSignalTerminated](err); ok { s, ok := userTerminatedErr.signal.(syscall.Signal) if !ok { return 1 @@ -100,8 +99,7 @@ func getExitCode(err error) int { return 128 + int(s) } - var stErr cli.StatusError - if errors.As(err, &stErr) && stErr.StatusCode != 0 { + if stErr, ok := errors.AsType[cli.StatusError](err); ok && stErr.StatusCode != 0 { return stErr.StatusCode } diff --git a/pkg/archive/time_windows.go b/pkg/archive/time_windows.go index 8c7d02e..6076279 100644 --- a/pkg/archive/time_windows.go +++ b/pkg/archive/time_windows.go @@ -7,7 +7,7 @@ import ( "golang.org/x/sys/windows" ) -func chtimes(name string, atime time.Time, mtime time.Time) error { +func chtimes(name string, atime, mtime time.Time) error { if err := os.Chtimes(name, atime, mtime); err != nil { return err } diff --git a/pkg/config/config.go b/pkg/config/config.go index cc60a99..d0be655 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -10,8 +10,6 @@ import ( "strings" "sync" - "github.com/pkg/errors" - "go.wpm.so/cli/pkg/config/configfile" ) @@ -77,7 +75,7 @@ func SetDir(dir string) { func Path(p ...string) (string, error) { path := filepath.Join(append([]string{Dir()}, p...)...) if !strings.HasPrefix(path, Dir()+string(filepath.Separator)) { - return "", errors.Errorf("path %q is outside of root config directory %q", path, Dir()) + return "", fmt.Errorf("path %q is outside of root config directory %q", path, Dir()) } return path, nil } @@ -121,12 +119,12 @@ func load(configDir string) (*configfile.ConfigFile, error) { return configFile, nil } // Any other error happening when failing to read the file must be returned. - return configFile, errors.Wrap(err, "loading config file") + return configFile, fmt.Errorf("loading config file: %w", err) } defer func() { _ = file.Close() }() err = configFile.LoadFromReader(file) if err != nil { - err = errors.Wrapf(err, "parsing config file (%s)", filename) + err = fmt.Errorf("parsing config file (%s): %w", filename, err) } return configFile, err } diff --git a/pkg/config/configfile/file.go b/pkg/config/configfile/file.go index 1980807..7271f36 100644 --- a/pkg/config/configfile/file.go +++ b/pkg/config/configfile/file.go @@ -3,11 +3,12 @@ package configfile import ( "encoding/base64" "encoding/json" + "errors" + "fmt" "io" "os" "path/filepath" - "github.com/pkg/errors" "github.com/sirupsen/logrus" ) @@ -135,7 +136,7 @@ func (configFile *ConfigFile) SaveToWriter(writer io.Writer) error { // Save encodes and writes out all the authorization information func (configFile *ConfigFile) Save() (retErr error) { if configFile.Filename == "" { - return errors.Errorf("Can't save config with empty filename") + return errors.New("can't save config with empty filename") } dir := filepath.Dir(configFile.Filename) @@ -161,7 +162,7 @@ func (configFile *ConfigFile) Save() (retErr error) { } if err := temp.Close(); err != nil { - return errors.Wrap(err, "error closing temp file") + return fmt.Errorf("error closing temp file: %w", err) } // Handle situation where the configfile is a symlink diff --git a/pkg/pm/installer/installer.go b/pkg/pm/installer/installer.go index a562852..3d13e71 100644 --- a/pkg/pm/installer/installer.go +++ b/pkg/pm/installer/installer.go @@ -6,6 +6,7 @@ import ( "crypto/sha256" "encoding/base64" "encoding/hex" + "errors" "fmt" "io" "io/fs" @@ -16,7 +17,6 @@ import ( "syscall" "time" - "github.com/pkg/errors" "golang.org/x/sync/errgroup" "go.wpm.so/cli/pkg/archive" @@ -57,20 +57,20 @@ func New( //nolint:gosec // Dir perms are intentionally permissive here. if err := os.MkdirAll(contentDir, 0o755); err != nil { - return nil, errors.Wrap(err, "failed to create content directory") + return nil, fmt.Errorf("failed to create content directory: %w", err) } tmpDir := filepath.Join(contentDir, ".tmp") //nolint:gosec // Dir perms are intentionally permissive here. if err := os.MkdirAll(tmpDir, 0o755); err != nil { - return nil, errors.Wrap(err, "failed to create tmp directory") + return nil, fmt.Errorf("failed to create tmp directory: %w", err) } sweepStaleRunDirs(tmpDir) runDir, err := os.MkdirTemp(tmpDir, "run-") if err != nil { - return nil, errors.Wrap(err, "failed to create staging directory") + return nil, fmt.Errorf("failed to create staging directory: %w", err) } return &Installer{ @@ -108,7 +108,7 @@ func sweepStaleRunDirs(tmpDir string) { func (i *Installer) InstallAll(ctx context.Context, plan []Action, progressFn func(Action)) error { keys, err := i.client.GetKeysJson(ctx) if err != nil { - return errors.Wrap(err, "failed to fetch public keys for signature verification") + return fmt.Errorf("failed to fetch public keys for signature verification: %w", err) } i.keysJson = keys @@ -140,7 +140,7 @@ func (i *Installer) install(ctx context.Context, action Action) error { switch action.Type { case ActionRemove: if err := i.removeAll(ctx, targetDir); err != nil { - return errors.Wrapf(err, "failed to delete %s", targetDir) + return fmt.Errorf("failed to delete %s: %w", targetDir, err) } return nil case ActionInstall, ActionUpdate: @@ -153,12 +153,12 @@ func (i *Installer) install(ctx context.Context, action Action) error { func (i *Installer) installOrUpdate(ctx context.Context, action Action, targetDir string) error { manifest, err := i.client.GetPackageManifest(ctx, action.Name, action.Version, false) if err != nil { - return errors.Wrapf(err, "failed to fetch manifest for %s@%s", action.Name, action.Version) + return fmt.Errorf("failed to fetch manifest for %s@%s: %w", action.Name, action.Version, err) } sigs := manifest.Dist.Signatures if len(sigs) == 0 { - return errors.Errorf("no signatures found for package %s@%s", action.Name, action.Version) + return fmt.Errorf("no signatures found for package %s@%s", action.Name, action.Version) } err = signatures.Verify( @@ -168,12 +168,12 @@ func (i *Installer) installOrUpdate(ctx context.Context, action Action, targetDi fmt.Appendf(nil, "%s:%s:%s", action.Name, action.Version, action.Digest), ) if err != nil { - return errors.Wrapf(err, "signature verification failed for package %s@%s", action.Name, action.Version) + return fmt.Errorf("signature verification failed for package %s@%s: %w", action.Name, action.Version, err) } resp, err := i.client.DownloadTarball(ctx, action.Resolved) if err != nil { - return errors.Wrapf(err, "failed to download %s", action.Resolved) + return fmt.Errorf("failed to download %s: %w", action.Resolved, err) } defer func() { _ = resp.Close() @@ -194,17 +194,17 @@ func (i *Installer) installOrUpdate(ctx context.Context, action Action, targetDi _ = i.removeAll(context.Background(), tempContainer) }() if err != nil { - return errors.Wrap(err, "failed to unpack package") + return fmt.Errorf("failed to unpack package: %w", err) } if _, err := io.Copy(io.Discard, stream); err != nil { - return errors.Wrap(err, "failed to drain download stream") + return fmt.Errorf("failed to drain download stream: %w", err) } cleanDigest := strings.TrimPrefix(action.Digest, "sha256:") calculated := base64.StdEncoding.EncodeToString(hasher.Sum(nil)) if calculated != cleanDigest { - return errors.Errorf("digest mismatch: expected %s, got %s", cleanDigest, calculated) + return fmt.Errorf("digest mismatch: expected %s, got %s", cleanDigest, calculated) } return i.replaceDir(ctx, extractedPath, targetDir) @@ -213,12 +213,12 @@ func (i *Installer) installOrUpdate(ctx context.Context, action Action, targetDi func (i *Installer) unpackToStaging(r io.Reader) (string, string, error) { rootTemp, err := os.MkdirTemp(i.runDir, "pkg-*") if err != nil { - return "", "", errors.Wrap(err, "failed to create staging directory") + return "", "", fmt.Errorf("failed to create staging directory: %w", err) } opts := &archive.TarOptions{Logger: i.logger} if err := archive.Untar(r, rootTemp, opts); err != nil { - return "", rootTemp, errors.Wrap(err, "failed to extract tarball") + return "", rootTemp, fmt.Errorf("failed to extract tarball: %w", err) } entries, err := os.ReadDir(rootTemp) @@ -236,7 +236,7 @@ func (i *Installer) unpackToStaging(r io.Reader) (string, string, error) { func (i *Installer) replaceDir(ctx context.Context, sourceDir, targetDir string) error { //nolint:gosec // Dir perms are intentionally permissive here. if err := os.MkdirAll(filepath.Dir(targetDir), 0o755); err != nil { - return errors.Wrap(err, "failed to create parent directory") + return fmt.Errorf("failed to create parent directory: %w", err) } if _, err := os.Lstat(targetDir); errors.Is(err, fs.ErrNotExist) { @@ -245,23 +245,22 @@ func (i *Installer) replaceDir(ctx context.Context, sourceDir, targetDir string) var nonce [8]byte if _, err := rand.Read(nonce[:]); err != nil { - return errors.Wrap(err, "failed to generate backup nonce") + return fmt.Errorf("failed to generate backup nonce: %w", err) } backupDir := filepath.Join(i.runDir, filepath.Base(targetDir)+".bak-"+hex.EncodeToString(nonce[:])) if err := i.rename(ctx, targetDir, backupDir); err != nil { - return errors.Wrap(err, "failed to move existing package to backup") + return fmt.Errorf("failed to move existing package to backup: %w", err) } if err := i.rename(ctx, sourceDir, targetDir); err != nil { if rbErr := i.rename(context.Background(), backupDir, targetDir); rbErr != nil { - return errors.Wrapf( - err, - "failed to install new version AND failed to roll back: previous version preserved at %q (rollback error: %v)", - backupDir, rbErr, + return fmt.Errorf( + "failed to install new version AND failed to roll back: previous version preserved at %q (rollback error: %v): %w", + backupDir, rbErr, err, ) } - return errors.Wrap(err, "failed to install new version, rolled back") + return fmt.Errorf("failed to install new version, rolled back: %w", err) } _ = i.removeAll(ctx, backupDir) @@ -272,7 +271,7 @@ func (i *Installer) rename(ctx context.Context, src, dst string) error { return retryFS(ctx, func() error { err := os.Rename(src, dst) if err != nil && isCrossDeviceError(err) { - return errors.Errorf( + return fmt.Errorf( "cannot move %q to %q: source and destination are on different filesystems. "+ "wpm requires the staging area (%s) and the install target (%s) to live on the same volume. "+ "This typically affects Docker setups where individual plugin/theme directories are bind-mounted", @@ -330,10 +329,8 @@ func isRetriableError(err error) bool { return false } - var linkErr *os.LinkError - if errors.As(err, &linkErr) { - var errno syscall.Errno - if errors.As(linkErr.Err, &errno) { + if linkErr, ok := errors.AsType[*os.LinkError](err); ok { + if errno, ok := errors.AsType[syscall.Errno](linkErr.Err); ok { return isRetriableErrno(errno) } } @@ -342,10 +339,8 @@ func isRetriableError(err error) bool { return true } - var pathErr *os.PathError - if errors.As(err, &pathErr) { - var errno syscall.Errno - if errors.As(pathErr.Err, &errno) { + if pathErr, ok := errors.AsType[*os.PathError](err); ok { + if errno, ok := errors.AsType[syscall.Errno](pathErr.Err); ok { return isRetriableErrno(errno) } } @@ -358,10 +353,8 @@ func isRetriableErrno(errno syscall.Errno) bool { } func isCrossDeviceError(err error) bool { - var linkErr *os.LinkError - if errors.As(err, &linkErr) { - var errno syscall.Errno - if errors.As(linkErr.Err, &errno) { + if linkErr, ok := errors.AsType[*os.LinkError](err); ok { + if errno, ok := errors.AsType[syscall.Errno](linkErr.Err); ok { return errno == syscall.EXDEV || errno == errorNotSameDevice } } @@ -370,7 +363,7 @@ func isCrossDeviceError(err error) bool { func (i *Installer) getTargetDir(pkgType types.PackageType, name string) (string, error) { if err := validator.IsValidPackageName(name); err != nil { - return "", errors.Wrapf(err, "refusing to operate on package with invalid name %q", name) + return "", fmt.Errorf("refusing to operate on package with invalid name %q: %w", name, err) } var subDir string @@ -382,7 +375,7 @@ func (i *Installer) getTargetDir(pkgType types.PackageType, name string) (string case types.TypePlugin: subDir = "plugins" default: - return "", errors.Errorf("unknown package type %q for package %q", pkgType, name) + return "", fmt.Errorf("unknown package type %q for package %q", pkgType, name) } target := filepath.Join(i.contentDir, subDir, name) @@ -391,7 +384,7 @@ func (i *Installer) getTargetDir(pkgType types.PackageType, name string) (string // path stays inside contentDir in case of symlinks or other weird filesystem setups. rel, err := filepath.Rel(i.contentDir, target) if err != nil || !filepath.IsLocal(rel) { - return "", errors.Errorf("package %q resolves outside content directory", name) + return "", fmt.Errorf("package %q resolves outside content directory", name) } return target, nil diff --git a/pkg/pm/resolution/resolver.go b/pkg/pm/resolution/resolver.go index 044f1d8..5fdf850 100644 --- a/pkg/pm/resolution/resolver.go +++ b/pkg/pm/resolution/resolver.go @@ -2,12 +2,12 @@ package resolution import ( "context" + "errors" "fmt" "io" "strings" "github.com/Masterminds/semver/v3" - "github.com/pkg/errors" "golang.org/x/sync/errgroup" "go.wpm.so/cli/pkg/pm/registry" @@ -315,12 +315,12 @@ func checkVersionCompatibility(name, required, runtime string) error { constraint, err := semver.NewConstraint(required) if err != nil { - return errors.Wrapf(err, "invalid %s requirement in package", name) + return fmt.Errorf("invalid %s requirement in package: %w", name, err) } version, err := semver.NewVersion(runtime) if err != nil { - return errors.Wrapf(err, "invalid runtime %s version provided", name) + return fmt.Errorf("invalid runtime %s version provided: %w", name, err) } if !constraint.Check(version) { diff --git a/pkg/pm/signatures/signatures.go b/pkg/pm/signatures/signatures.go index d0718a6..b752ec5 100644 --- a/pkg/pm/signatures/signatures.go +++ b/pkg/pm/signatures/signatures.go @@ -6,10 +6,9 @@ import ( "crypto/x509" "encoding/asn1" "encoding/base64" + "errors" "fmt" "math/big" - - "github.com/pkg/errors" ) const signingAlgorithm = "ECDSA_SHA_256" @@ -48,7 +47,7 @@ func Verify(keys KeysJson, keyId, signatureBase64 string, originalMessage []byte keyBytes, err := base64.StdEncoding.DecodeString(rawPublicKeyBase64) if err != nil { - return errors.Wrap(err, "failed to decode base64 public key") + return fmt.Errorf("failed to decode base64 public key: %w", err) } genericPublicKey, err := x509.ParsePKIXPublicKey(keyBytes) diff --git a/pkg/pm/workspace/lock.go b/pkg/pm/workspace/lock.go index d517bb2..e330a62 100644 --- a/pkg/pm/workspace/lock.go +++ b/pkg/pm/workspace/lock.go @@ -2,12 +2,12 @@ package workspace import ( "context" + "fmt" "os" "path/filepath" "time" "github.com/gofrs/flock" - "github.com/pkg/errors" ) type ProjectLock struct { @@ -21,7 +21,7 @@ type ProjectLock struct { func AcquireLock(ctx context.Context, baseDir string, printWaitMsg func()) (*ProjectLock, error) { wpmDir := filepath.Join(baseDir, ".wpm") if err := os.MkdirAll(wpmDir, 0o750); err != nil { - return nil, errors.Wrap(err, "failed to create workspace directory") + return nil, fmt.Errorf("failed to create workspace directory: %w", err) } _ = os.WriteFile(filepath.Join(wpmDir, ".gitignore"), []byte("*\n"), 0o600) @@ -30,7 +30,7 @@ func AcquireLock(ctx context.Context, baseDir string, printWaitMsg func()) (*Pro locked, err := fileLock.TryLock() if err != nil { - return nil, errors.Wrap(err, "failed to acquire workspace lock") + return nil, fmt.Errorf("failed to acquire workspace lock: %w", err) } if locked { return &ProjectLock{fileLock: fileLock}, nil @@ -42,10 +42,10 @@ func AcquireLock(ctx context.Context, baseDir string, printWaitMsg func()) (*Pro locked, err = fileLock.TryLockContext(ctx, 200*time.Millisecond) if err != nil { - return nil, errors.Wrap(err, "failed while waiting for workspace lock") + return nil, fmt.Errorf("failed while waiting for workspace lock: %w", err) } if !locked { - return nil, errors.Wrap(ctx.Err(), "operation cancelled while waiting for workspace lock") + return nil, fmt.Errorf("operation cancelled while waiting for workspace lock: %w", ctx.Err()) } return &ProjectLock{fileLock: fileLock}, nil diff --git a/pkg/pm/wpmjson/wpmjson.go b/pkg/pm/wpmjson/wpmjson.go index 148e90e..cf197d2 100644 --- a/pkg/pm/wpmjson/wpmjson.go +++ b/pkg/pm/wpmjson/wpmjson.go @@ -6,8 +6,6 @@ import ( "os" "path/filepath" - "github.com/pkg/errors" - "go.wpm.so/cli/pkg/pm" "go.wpm.so/cli/pkg/pm/wpmjson/types" "go.wpm.so/cli/pkg/pm/wpmjson/validator" @@ -159,12 +157,12 @@ func Read(cwd string) (*Config, error) { data, err := os.ReadFile(path) //nolint:gosec // path is cwd + ConfigFile constant if err != nil { - return nil, errors.Wrap(err, "failed to read wpm.json") + return nil, fmt.Errorf("failed to read wpm.json: %w", err) } var config Config if err := json.Unmarshal(data, &config); err != nil { - return nil, errors.Wrap(err, "failed to parse wpm.json") + return nil, fmt.Errorf("failed to parse wpm.json: %w", err) } config.Indentation = pm.DetectIndentation(data) @@ -179,12 +177,12 @@ func (c *Config) Write(cwd string) error { data, err := json.MarshalIndent(c, "", c.GetIndentation()) if err != nil { - return errors.Wrap(err, "failed to marshal wpm.json") + return fmt.Errorf("failed to marshal wpm.json: %w", err) } // Write with 0644 permissions (rw-r--r--) if err := os.WriteFile(path, data, 0o644); err != nil { - return errors.Wrap(err, "failed to write wpm.json to disk") + return fmt.Errorf("failed to write wpm.json to disk: %w", err) } return nil diff --git a/pkg/pm/wpmlock/lockfile.go b/pkg/pm/wpmlock/lockfile.go index ef21e21..94a793a 100644 --- a/pkg/pm/wpmlock/lockfile.go +++ b/pkg/pm/wpmlock/lockfile.go @@ -2,11 +2,11 @@ package wpmlock import ( "encoding/json" + "errors" + "fmt" "os" "path/filepath" - "github.com/pkg/errors" - "go.wpm.so/cli/pkg/pm" "go.wpm.so/cli/pkg/pm/wpmjson/types" "go.wpm.so/cli/pkg/pm/wpmjson/validator" @@ -52,12 +52,12 @@ func Read(cwd string) (*Lockfile, error) { data, err := os.ReadFile(path) //nolint:gosec // path is cwd + LockfileName constant if err != nil { - return nil, errors.Wrap(err, "failed to read lockfile") + return nil, fmt.Errorf("failed to read lockfile: %w", err) } var lockfile Lockfile if err := json.Unmarshal(data, &lockfile); err != nil { - return nil, errors.Wrap(err, "failed to parse lockfile") + return nil, fmt.Errorf("failed to parse lockfile: %w", err) } if lockfile.LockfileVersion > CurrentVersion { @@ -70,7 +70,7 @@ func Read(cwd string) (*Lockfile, error) { for name := range lockfile.Packages { if err := validator.IsValidPackageName(name); err != nil { - return nil, errors.Wrapf(err, "invalid package name %q in lockfile", name) + return nil, fmt.Errorf("invalid package name %q in lockfile: %w", name, err) } } @@ -92,12 +92,12 @@ func (l *Lockfile) Write(cwd string) error { data, err := json.MarshalIndent(l, "", l.Indentation) if err != nil { - return errors.Wrap(err, "failed to marshal lockfile") + return fmt.Errorf("failed to marshal lockfile: %w", err) } // Write with 0644 permissions (rw-r--r--) if err := os.WriteFile(path, data, 0o644); err != nil { - return errors.Wrap(err, "failed to write lockfile to disk") + return fmt.Errorf("failed to write lockfile to disk: %w", err) } return nil From 284e3bf2a14c56b96d0fc51652b6e5fff5187c9f Mon Sep 17 00:00:00 2001 From: thelovekesh Date: Tue, 26 May 2026 13:39:38 +0530 Subject: [PATCH 03/11] Remove github.com/containerd/errdefs --- go.mod | 1 - go.sum | 2 -- 2 files changed, 3 deletions(-) diff --git a/go.mod b/go.mod index 3bcf432..70cb0dc 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.26.3 require ( github.com/Masterminds/semver/v3 v3.5.0 github.com/briandowns/spinner v1.23.2 - github.com/containerd/errdefs v1.0.0 github.com/docker/cli-docs-tool v0.11.0 github.com/docker/go-units v0.5.0 github.com/fvbommel/sortorder v1.1.0 diff --git a/go.sum b/go.sum index 48c30cb..715413b 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,6 @@ github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAw github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/briandowns/spinner v1.23.2 h1:Zc6ecUnI+YzLmJniCfDNaMbW0Wid1d5+qcTq4L2FW8w= github.com/briandowns/spinner v1.23.2/go.mod h1:LaZeM4wm2Ywy6vO571mvhQNRcWfRUnXOs0RcKV0wYKM= -github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= -github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= From 2938f9f540b44eefbac578469cdf43d71b7e80ae Mon Sep 17 00:00:00 2001 From: thelovekesh Date: Tue, 26 May 2026 13:40:10 +0530 Subject: [PATCH 04/11] Update context cancellation error check --- cmd/wpm/wpm.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cmd/wpm/wpm.go b/cmd/wpm/wpm.go index 95e53c7..55e2235 100644 --- a/cmd/wpm/wpm.go +++ b/cmd/wpm/wpm.go @@ -9,7 +9,6 @@ import ( "os/signal" "syscall" - "github.com/containerd/errdefs" "github.com/morikuni/aec" "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -36,7 +35,7 @@ func main() { os.Exit(getExitCode(err)) } - if err != nil && !errdefs.IsCanceled(err) { + if err != nil && !errors.Is(err, context.Canceled) { if err.Error() != "" { _, _ = fmt.Fprintln(os.Stderr, err) } From b993b44d46b9828b3417a24aea9afeabf068152e Mon Sep 17 00:00:00 2001 From: thelovekesh Date: Tue, 26 May 2026 14:09:25 +0530 Subject: [PATCH 05/11] Add github.com/rs/zerolog --- go.mod | 6 +++--- go.sum | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/go.mod b/go.mod index 70cb0dc..c11f3f0 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/moby/sys/sequential v0.6.0 github.com/moby/term v0.5.2 github.com/morikuni/aec v1.1.0 - github.com/sirupsen/logrus v1.9.4 + github.com/rs/zerolog v1.35.1 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 github.com/thlib/go-timezone-local v0.0.7 @@ -30,8 +30,8 @@ require ( github.com/fatih/color v1.7.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/kr/text v0.2.0 // indirect - github.com/mattn/go-colorable v0.1.2 // indirect - github.com/mattn/go-isatty v0.0.8 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect diff --git a/go.sum b/go.sum index 715413b..986e7dd 100644 --- a/go.sum +++ b/go.sum @@ -32,10 +32,10 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= -github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U= github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= @@ -48,10 +48,10 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI= +github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= -github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= @@ -65,9 +65,9 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= From 8f54ab78be29c760d25217d6f8b5b1677df12825 Mon Sep 17 00:00:00 2001 From: thelovekesh Date: Tue, 26 May 2026 14:09:47 +0530 Subject: [PATCH 06/11] Migrate from logrus to zerolog --- .golangci.yml | 2 +- CLAUDE.md | 3 ++- cli/debug/debug.go | 6 +++--- cli/flags/options.go | 11 ++++++----- cmd/docgen/docgen.go | 8 ++++++-- cmd/wpm/wpm.go | 10 ++++++++-- pkg/api/client_options.go | 2 +- pkg/api/http_client.go | 4 ++-- pkg/config/configfile/file.go | 4 ++-- pkg/streams/out.go | 4 ++-- 10 files changed, 33 insertions(+), 21 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index f39ff1d..21cda23 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -96,7 +96,7 @@ linters: main: deny: - pkg: 'log' - desc: 'Use logrus for logging instead of the standard log package.' + desc: 'Use github.com/rs/zerolog for logging instead of the standard log package.' - pkg: 'io/ioutil' desc: "The io/ioutil package has been deprecated, use 'os' or 'io' directly." revive: diff --git a/CLAUDE.md b/CLAUDE.md index 9dc0d2c..a7b2722 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -47,7 +47,8 @@ Three-layer separation — do not collapse layers for convenience: `errors.Is` / `errors.As` — never string-match error messages. - **Concurrency:** use `golang.org/x/sync/errgroup`. Always honor `context.Context` cancellation. -- **Logging:** use `logrus`, not stdlib `log` (depguard enforces). Stdlib +- **Logging:** use `github.com/rs/zerolog` (via the package-level `log` + global), not stdlib `log` (depguard enforces). Stdlib `io/ioutil` is denied — use `os` / `io`. - **CLI reference docs** (`docs/cli/*.md`) have an auto-generated marker block (`` / ``). **Never edit inside diff --git a/cli/debug/debug.go b/cli/debug/debug.go index bc3b8f4..b7a512d 100644 --- a/cli/debug/debug.go +++ b/cli/debug/debug.go @@ -3,21 +3,21 @@ package debug import ( "os" - "github.com/sirupsen/logrus" + "github.com/rs/zerolog" ) // Enable sets the WPM_DEBUG env var to true // and makes the logger to log at debug level. func Enable() { _ = os.Setenv("WPM_DEBUG", "1") - logrus.SetLevel(logrus.DebugLevel) + zerolog.SetGlobalLevel(zerolog.DebugLevel) } // Disable sets the WPM_DEBUG env var to false // and makes the logger to log at info level. func Disable() { _ = os.Setenv("WPM_DEBUG", "") - logrus.SetLevel(logrus.InfoLevel) + zerolog.SetGlobalLevel(zerolog.InfoLevel) } // IsEnabled checks whether the debug flag is set or not. diff --git a/cli/flags/options.go b/cli/flags/options.go index f434585..de9ef87 100644 --- a/cli/flags/options.go +++ b/cli/flags/options.go @@ -3,8 +3,9 @@ package flags import ( "fmt" "os" + "strings" - "github.com/sirupsen/logrus" + "github.com/rs/zerolog" "github.com/spf13/pflag" "go.wpm.so/cli/pkg/config" @@ -38,16 +39,16 @@ func (o *ClientOptions) InstallFlags(flags *pflag.FlagSet) { // complete func (*ClientOptions) SetDefaultOptions(_ *pflag.FlagSet) {} -// SetLogLevel sets the logrus logging level +// SetLogLevel sets the global zerolog logging level. func SetLogLevel(logLevel string) { if logLevel != "" { - lvl, err := logrus.ParseLevel(logLevel) + lvl, err := zerolog.ParseLevel(strings.ToLower(logLevel)) if err != nil { fmt.Fprintf(os.Stderr, "Unable to parse logging level: %s\n", logLevel) os.Exit(1) } - logrus.SetLevel(lvl) + zerolog.SetGlobalLevel(lvl) } else { - logrus.SetLevel(logrus.InfoLevel) + zerolog.SetGlobalLevel(zerolog.InfoLevel) } } diff --git a/cmd/docgen/docgen.go b/cmd/docgen/docgen.go index b6fb032..b984943 100644 --- a/cmd/docgen/docgen.go +++ b/cmd/docgen/docgen.go @@ -28,7 +28,8 @@ import ( "os" clidocstool "github.com/docker/cli-docs-tool" - "github.com/sirupsen/logrus" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/spf13/cobra/doc" "github.com/spf13/pflag" @@ -59,7 +60,10 @@ func main() { } func run() error { - logrus.SetFormatter(&logrus.TextFormatter{DisableTimestamp: true}) + log.Logger = zerolog.New(zerolog.ConsoleWriter{ + Out: os.Stderr, + PartsExclude: []string{zerolog.TimestampFieldName}, + }) opts, err := parseArgs(os.Args[1:]) if err != nil { diff --git a/cmd/wpm/wpm.go b/cmd/wpm/wpm.go index 55e2235..7e9aa58 100644 --- a/cmd/wpm/wpm.go +++ b/cmd/wpm/wpm.go @@ -8,9 +8,11 @@ import ( "os" "os/signal" "syscall" + "time" "github.com/morikuni/aec" - "github.com/sirupsen/logrus" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" "github.com/spf13/cobra" "go.wpm.so/cli/cli" @@ -77,7 +79,11 @@ func wpmMain(ctx context.Context) error { if err != nil { return err } - logrus.SetOutput(wpmCli.Err()) + log.Logger = zerolog.New(zerolog.ConsoleWriter{ + Out: wpmCli.Err(), + NoColor: !wpmCli.Err().IsColorEnabled(), + TimeFormat: time.Kitchen, + }).With().Timestamp().Logger() return runWpm(ctx, wpmCli) } diff --git a/pkg/api/client_options.go b/pkg/api/client_options.go index cddfb02..1db25e9 100644 --- a/pkg/api/client_options.go +++ b/pkg/api/client_options.go @@ -29,7 +29,7 @@ type ClientOptions struct { // LogVerboseHTTP enables logging HTTP headers and bodies to Log. // Default is only logging request URLs and response statuses. - // By default fallback to logrus log level. + // By default fallback to zerolog log level. LogVerboseHTTP bool // SkipDefaultHeaders disables setting of the default headers. diff --git a/pkg/api/http_client.go b/pkg/api/http_client.go index 6acf90d..afdc389 100644 --- a/pkg/api/http_client.go +++ b/pkg/api/http_client.go @@ -11,7 +11,7 @@ import ( "github.com/henvic/httpretty" "github.com/klauspost/compress/zstd" - "github.com/sirupsen/logrus" + "github.com/rs/zerolog" "github.com/thlib/go-timezone-local/tzlocal" "golang.org/x/text/transform" @@ -89,7 +89,7 @@ func NewHTTPClient(opts ClientOptions) (*http.Client, error) { rt = newDecompressingRoundTripper(rt) rt = newSanitizerRoundTripper(rt) - if opts.Log != nil && logrus.GetLevel() == logrus.DebugLevel { + if opts.Log != nil && zerolog.GlobalLevel() == zerolog.DebugLevel { opts.LogVerboseHTTP = true logger := &httpretty.Logger{ Time: true, diff --git a/pkg/config/configfile/file.go b/pkg/config/configfile/file.go index 7271f36..1dbad88 100644 --- a/pkg/config/configfile/file.go +++ b/pkg/config/configfile/file.go @@ -9,7 +9,7 @@ import ( "os" "path/filepath" - "github.com/sirupsen/logrus" + "github.com/rs/zerolog/log" ) type UsersAuthConfig struct { @@ -151,7 +151,7 @@ func (configFile *ConfigFile) Save() (retErr error) { _ = temp.Close() if retErr != nil { if err := os.Remove(temp.Name()); err != nil { - logrus.WithError(err).WithField("file", temp.Name()).Debug("Error cleaning up temp file") + log.Debug().Err(err).Str("file", temp.Name()).Msg("Error cleaning up temp file") } } }() diff --git a/pkg/streams/out.go b/pkg/streams/out.go index 6bc9067..211108e 100644 --- a/pkg/streams/out.go +++ b/pkg/streams/out.go @@ -5,7 +5,7 @@ import ( "os" "github.com/moby/term" - "github.com/sirupsen/logrus" + "github.com/rs/zerolog/log" "go.wpm.so/cli/pkg/unsafeconv" ) @@ -86,7 +86,7 @@ func (o *Out) GetTtySize() (height, width uint) { } ws, err := term.GetWinsize(o.fd) if err != nil { - logrus.WithError(err).Debug("Error getting TTY size") + log.Debug().Err(err).Msg("Error getting TTY size") if ws == nil { return 0, 0 } From acadfdd487c36a86e634f1589712f9d468e6eb70 Mon Sep 17 00:00:00 2001 From: thelovekesh Date: Tue, 26 May 2026 14:55:11 +0530 Subject: [PATCH 07/11] Remove github.com/fvbommel/sortorder --- go.mod | 1 - go.sum | 2 -- 2 files changed, 3 deletions(-) diff --git a/go.mod b/go.mod index c11f3f0..e970261 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,6 @@ require ( github.com/briandowns/spinner v1.23.2 github.com/docker/cli-docs-tool v0.11.0 github.com/docker/go-units v0.5.0 - github.com/fvbommel/sortorder v1.1.0 github.com/gofrs/flock v0.13.0 github.com/henvic/httpretty v0.1.4 github.com/klauspost/compress v1.18.6 diff --git a/go.sum b/go.sum index 986e7dd..cb5e552 100644 --- a/go.sum +++ b/go.sum @@ -18,8 +18,6 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4 github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw= -github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= github.com/henvic/httpretty v0.1.4 h1:Jo7uwIRWVFxkqOnErcoYfH90o3ddQyVrSANeS4cxYmU= From da4d97d12fae2d7f97ddc80ecc963abf0686704d Mon Sep 17 00:00:00 2001 From: thelovekesh Date: Tue, 26 May 2026 14:58:15 +0530 Subject: [PATCH 08/11] Port sortorder from github.com/fvbommel/sortorder --- pkg/sortorder/natsort.go | 86 ++++++++++++++++ pkg/sortorder/natsort_test.go | 188 ++++++++++++++++++++++++++++++++++ 2 files changed, 274 insertions(+) create mode 100644 pkg/sortorder/natsort.go create mode 100644 pkg/sortorder/natsort_test.go diff --git a/pkg/sortorder/natsort.go b/pkg/sortorder/natsort.go new file mode 100644 index 0000000..4bbcb39 --- /dev/null +++ b/pkg/sortorder/natsort.go @@ -0,0 +1,86 @@ +package sortorder + +// Natural implements sort.Interface to sort strings in natural order. This +// means that e.g. "abc2" < "abc12". +// +// Non-digit sequences and numbers are compared separately. The former are +// compared bytewise, while digits are compared numerically (except that +// the number of leading zeros is used as a tie-breaker, so e.g. "2" < "02") +// +// Limitation: only ASCII digits (0-9) are considered. +type Natural []string + +func (n Natural) Len() int { return len(n) } +func (n Natural) Swap(i, j int) { n[i], n[j] = n[j], n[i] } +func (n Natural) Less(i, j int) bool { return NaturalLess(n[i], n[j]) } + +func isDigit(b byte) bool { return '0' <= b && b <= '9' } + +// skipZeros advances i past any ASCII '0' bytes in s. +func skipZeros(s string, i int) int { + for i < len(s) && s[i] == '0' { + i++ + } + return i +} + +// skipDigits advances i past any ASCII digit bytes in s. +func skipDigits(s string, i int) int { + for i < len(s) && isDigit(s[i]) { + i++ + } + return i +} + +// NaturalLess compares two strings using natural ordering. This means that e.g. +// "abc2" < "abc12". +// +// Non-digit sequences and numbers are compared separately. The former are +// compared bytewise, while digits are compared numerically (except that +// the number of leading zeros is used as a tie-breaker, so e.g. "2" < "02") +// +// Limitation: only ASCII digits (0-9) are considered. +func NaturalLess(str1, str2 string) bool { + idx1, idx2 := 0, 0 + for idx1 < len(str1) && idx2 < len(str2) { + c1, c2 := str1[idx1], str2[idx2] + dig1, dig2 := isDigit(c1), isDigit(c2) + switch { + case dig1 != dig2: // Digits before other characters. + return dig1 // True if LHS is a digit, false if the RHS is one. + case !dig1: // && !dig2, because dig1 == dig2 + // UTF-8 compares bytewise-lexicographically, no need to decode + // codepoints. + if c1 != c2 { + return c1 < c2 + } + idx1++ + idx2++ + default: // Digits + idx1 = skipZeros(str1, idx1) + idx2 = skipZeros(str2, idx2) + nonZero1, nonZero2 := idx1, idx2 + idx1 = skipDigits(str1, idx1) + idx2 = skipDigits(str2, idx2) + // If lengths of numbers with non-zero prefix differ, the shorter + // one is less. + if len1, len2 := idx1-nonZero1, idx2-nonZero2; len1 != len2 { + return len1 < len2 + } + // If they're equally long, string comparison is correct. + if nr1, nr2 := str1[nonZero1:idx1], str2[nonZero2:idx2]; nr1 != nr2 { + return nr1 < nr2 + } + // Otherwise, the one with less zeros is less. + // Because everything up to the number is equal, comparing the index + // after the zeros is sufficient. + if nonZero1 != nonZero2 { + return nonZero1 < nonZero2 + } + } + // They're identical so far, so continue comparing. + } + // So far they are identical. At least one is ended. If the other continues, + // it sorts last. + return len(str1) < len(str2) +} diff --git a/pkg/sortorder/natsort_test.go b/pkg/sortorder/natsort_test.go new file mode 100644 index 0000000..02a44f7 --- /dev/null +++ b/pkg/sortorder/natsort_test.go @@ -0,0 +1,188 @@ +package sortorder + +import ( + "math/rand" + "reflect" + "sort" + "strconv" + "strings" + "testing" +) + +const benchSeed = 300 + +func TestStringSort(t *testing.T) { + want := []string{ + "ab", "abc1", + "abc01", "abc2", + "abc5", "abc10", + } + got := []string{ + "abc5", "abc1", + "abc01", "ab", + "abc10", "abc2", + } + sort.Sort(Natural(got)) + if !reflect.DeepEqual(want, got) { + t.Errorf("Error: sort failed, expected: %#q, got: %#q", want, got) + } +} + +func TestNaturalLess(t *testing.T) { + testset := []struct { + s1, s2 string + less bool + }{ + {"0", "00", true}, + {"aa", "ab", true}, + {"ab", "abc", true}, + {"abc", "ad", true}, + {"ab1", "ab2", true}, + {"ab1c", "ab1c", false}, + {"ab12", "abc", true}, + {"ab2a", "ab10", true}, + {"a0001", "a0000001", true}, + {"a10", "abcdefgh2", true}, + {"аб2аб", "аб10аб", true}, + {"2аб", "3аб", true}, + // + {"a1b", "a01b", true}, + {"ab01b", "ab010b", true}, + {"a01b001", "a001b01", true}, + {"a1", "a1x", true}, + {"1ax", "1b", true}, + // + {"082", "83", true}, + {"9a", "083a", true}, + } + for _, v := range testset { + if got := NaturalLess(v.s1, v.s2); got != v.less { + t.Errorf("Compared %#q to %#q: expected %v, got %v", + v.s1, v.s2, v.less, got) + } + // If A < B, then B < A must be false. + // The same cannot be said if !(A < B), + // because A might be equal to B + if v.less { + if v.s1 != v.s2 && NaturalLess(v.s2, v.s1) { + t.Errorf("Reverse-compared %#q to %#q: expected false, got true", + v.s2, v.s1) + } + } + } +} + +// Use a regular string sort. +// As this does not perform a natural sort, +// this is not directly comparable with the other sorts. +// It is only here for a sense of scale. +func BenchmarkStdStringSort(b *testing.B) { + set := testSet() + arr := make([]string, len(set[0])) + b.ResetTimer() + for b.Loop() { + for _, list := range set { + b.StopTimer() + copy(arr, list) + b.StartTimer() + + sort.Strings(arr) + } + } +} + +// Natural sort order. +func BenchmarkNaturalStringSort(b *testing.B) { + set := testSet() + arr := make([]string, len(set[0])) + b.ResetTimer() + for b.Loop() { + for _, list := range set { + // Resetting the test set to be unsorted does not count. + b.StopTimer() + copy(arr, list) + b.StartTimer() + + sort.Sort(Natural(arr)) + } + } +} + +func BenchmarkStdStringLess(b *testing.B) { + set := testSet() + b.ResetTimer() + for b.Loop() { + for j := range set[0] { + k := (j + 1) % len(set[0]) + _ = set[0][j] < set[0][k] + } + } +} + +func BenchmarkNaturalLess(b *testing.B) { + set := testSet() + b.ResetTimer() + for b.Loop() { + for j := range set[0] { + k := (j + 1) % len(set[0]) + _ = NaturalLess(set[0][j], set[0][k]) + } + } +} + +// Get 1000 arrays of 10000-string-arrays (less if -short is specified). +func testSet() [][]string { + gen := &generator{ + //nolint:gosec // G404: deterministic PRNG is intentional for reproducible benchmarks. + src: rand.New(rand.NewSource(int64(benchSeed))), + } + n := 1000 + if testing.Short() { + n = 1 + } + set := make([][]string, n) + for i := range set { + entries := make([]string, 10000) + for idx := range entries { + // Generate a random string + entries[idx] = gen.NextString() + } + set[i] = entries + } + return set +} + +type generator struct { + src *rand.Rand +} + +func (g *generator) NextInt(n int) int { + return g.src.Intn(n) +} + +// Gets random random-length alphanumeric string. +func (g *generator) NextString() string { + // Random-length 3-8 chars part + strlen := g.src.Intn(6) + 3 + // Random-length 1-3 num + numlen := g.src.Intn(3) + 1 + // Random position for num in string + numpos := g.src.Intn(strlen + 1) + // Generate the number + var numBuf strings.Builder + for range numlen { + numBuf.WriteString(strconv.Itoa(g.src.Intn(10))) + } + num := numBuf.String() + // Put it all together + var strBuf strings.Builder + for i := range strlen + 1 { + if i == numpos { + strBuf.WriteString(num) + } else { + //nolint:gosec // G115: Intn(16) returns 0..15, no overflow possible. + strBuf.WriteRune('a' + rune(g.src.Intn(16))) + } + } + return strBuf.String() +} From d42214336f319c03a6070366f3c407f8f764b205 Mon Sep 17 00:00:00 2001 From: thelovekesh Date: Tue, 26 May 2026 14:58:25 +0530 Subject: [PATCH 09/11] Update sortorder import --- cli/cobra.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/cobra.go b/cli/cobra.go index 1da3b5a..3a26a09 100644 --- a/cli/cobra.go +++ b/cli/cobra.go @@ -6,7 +6,6 @@ import ( "sort" "strings" - "github.com/fvbommel/sortorder" "github.com/moby/term" "github.com/morikuni/aec" "github.com/spf13/cobra" @@ -15,6 +14,7 @@ import ( "go.wpm.so/cli/cli/command" "go.wpm.so/cli/cli/command/completion" cliflags "go.wpm.so/cli/cli/flags" + "go.wpm.so/cli/pkg/sortorder" ) // setupCommonRootCommand contains the setup common to From 7c44ca85de6549de92487fe4cbd64ae1612b9565 Mon Sep 17 00:00:00 2001 From: thelovekesh Date: Tue, 26 May 2026 15:15:43 +0530 Subject: [PATCH 10/11] Remove github.com/thlib/go-timezone-local --- go.mod | 1 - go.sum | 3 --- 2 files changed, 4 deletions(-) diff --git a/go.mod b/go.mod index e970261..40599f0 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,6 @@ require ( github.com/rs/zerolog v1.35.1 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 - github.com/thlib/go-timezone-local v0.0.7 golang.org/x/sync v0.20.0 golang.org/x/sys v0.45.0 golang.org/x/text v0.37.0 diff --git a/go.sum b/go.sum index cb5e552..b761c3f 100644 --- a/go.sum +++ b/go.sum @@ -57,14 +57,11 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/thlib/go-timezone-local v0.0.7 h1:fX8zd3aJydqLlTs/TrROrIIdztzsdFV23OzOQx31jII= -github.com/thlib/go-timezone-local v0.0.7/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= From 53de5ac10e8e5710f1de8a9a885dc2027023d31b Mon Sep 17 00:00:00 2001 From: thelovekesh Date: Tue, 26 May 2026 15:16:05 +0530 Subject: [PATCH 11/11] Remove Time-Zone header usage --- pkg/api/client_options.go | 2 +- pkg/api/http_client.go | 7 ------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/pkg/api/client_options.go b/pkg/api/client_options.go index 1db25e9..44ee27a 100644 --- a/pkg/api/client_options.go +++ b/pkg/api/client_options.go @@ -13,7 +13,7 @@ type ClientOptions struct { AuthToken string // Headers are the headers that will be sent with every API request. - // Default headers set are Accept, Content-Type, Time-Zone, and User-Agent. + // Default headers set are Accept, Content-Type, and User-Agent. // Default headers will be overridden by keys specified in Headers. Headers map[string]string diff --git a/pkg/api/http_client.go b/pkg/api/http_client.go index afdc389..a1c5bc1 100644 --- a/pkg/api/http_client.go +++ b/pkg/api/http_client.go @@ -12,7 +12,6 @@ import ( "github.com/henvic/httpretty" "github.com/klauspost/compress/zstd" "github.com/rs/zerolog" - "github.com/thlib/go-timezone-local/tzlocal" "golang.org/x/text/transform" "go.wpm.so/cli/pkg/asciisanitizer" @@ -35,7 +34,6 @@ const ( HeaderAccept = "Accept" HeaderAuthorization = "Authorization" HeaderUserAgent = "User-Agent" - HeaderTimeZone = "Time-Zone" // header values CacheHit = "HIT" @@ -135,11 +133,6 @@ func resolveHeaders(headers map[string]string) { if _, ok := headers[HeaderUserAgent]; !ok { headers[HeaderUserAgent] = "wpm-cli" } - if _, ok := headers[HeaderTimeZone]; !ok { - if tz, err := tzlocal.RuntimeTZ(); err == nil && tz != "" { - headers[HeaderTimeZone] = tz - } - } if _, ok := headers[HeaderAccept]; !ok { headers[HeaderAccept] = "application/json" }