Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package asdf for openSUSE:Factory checked in at 2026-09-22 15:52:57 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/asdf (Old) and /work/SRC/openSUSE:Factory/.asdf.new.383539 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "asdf" Tue Sep 22 15:52:57 2026 rev:10 rq:1379604 version:0.20.1 Changes: -------- --- /work/SRC/openSUSE:Factory/asdf/asdf.changes 2026-07-07 21:09:34.051499562 +0200 +++ /work/SRC/openSUSE:Factory/.asdf.new.383539/asdf.changes 2026-09-22 15:54:26.755278378 +0200 @@ -1,0 +2,22 @@ +Tue Sep 22 06:31:32 UTC 2026 - Alessio Biancalana <[email protected]> + +- Update to version 0.20.1: + * chore: release 0.20.1 (#2295) + * fix: override urfave/cli built-in help printer for top level asdf -h/--help (#2328) + * fix: never generate a shim named "asdf" during reshim (#2307) + * fix: prevent incomplete installations from being shown as installed (#2303) + * chore: bump @types/node from 26.1.2 to 26.4.0 in /docs in the docs group (#2320) + * docs: fix the ordering of paragraphs in the versions page (#2318) + * docs: explain env vars for dashed tool names (#2317) + * chore: bump github.com/go-git/go-git/v5 from 5.19.1 to 5.19.2 (#2312) + * chore: bump postcss from 8.5.12 to 8.5.25 in /docs (#2310) + * chore: bump the docs group in /docs with 2 updates (#2309) + * chore: bump actions/setup-node from 6 to 7 (#2300) + * chore: bump actions/setup-python from 6 to 7 (#2302) + * chore: bump actions/setup-go from 6 to 7 (#2301) + * fix: Print stderr for list-all / latest-stable callbacks + * fix: always update local Git repsitory in git.Update even if repositories have diverged (#2298) + * chore: bump golang.org/x/crypto from 0.51.0 to 0.52.0 (#2296) + * fix: remove urlfave/cli built-in `-h` flag that is causing confusing behavior (#2294) + +------------------------------------------------------------------- Old: ---- asdf-0.20.0.obscpio New: ---- asdf-0.20.1.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ asdf.spec ++++++ --- /var/tmp/diff_new_pack.0MGjRS/_old 2026-09-22 15:54:27.613313726 +0200 +++ /var/tmp/diff_new_pack.0MGjRS/_new 2026-09-22 15:54:27.614313767 +0200 @@ -17,7 +17,7 @@ Name: asdf -Version: 0.20.0 +Version: 0.20.1 Release: 0 Summary: Extendable version manager License: MIT ++++++ _service ++++++ --- /var/tmp/diff_new_pack.0MGjRS/_old 2026-09-22 15:54:27.659315621 +0200 +++ /var/tmp/diff_new_pack.0MGjRS/_new 2026-09-22 15:54:27.664315827 +0200 @@ -3,7 +3,7 @@ <param name="url">https://github.com/asdf-vm/asdf/</param> <param name="scm">git</param> <param name="exclude">.git</param> - <param name="revision">v0.20.0</param> + <param name="revision">v0.20.1</param> <param name="versionformat">@PARENT_TAG@</param> <param name="changesgenerate">enable</param> <param name="versionrewrite-pattern">v(.*)</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.0MGjRS/_old 2026-09-22 15:54:27.715317928 +0200 +++ /var/tmp/diff_new_pack.0MGjRS/_new 2026-09-22 15:54:27.725318340 +0200 @@ -1,6 +1,6 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/asdf-vm/asdf/</param> - <param name="changesrevision">150aaf051b3b88ac9ad73136d7e629bbdf332bd6</param></service></servicedata> + <param name="changesrevision">8fa9e668c12a11fb078f7e08c2d854e38519a7a4</param></service></servicedata> (No newline at EOF) ++++++ asdf-0.20.0.obscpio -> asdf-0.20.1.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asdf-0.20.0/.release-please-manifest.json new/asdf-0.20.1/.release-please-manifest.json --- old/asdf-0.20.0/.release-please-manifest.json 2026-07-07 16:04:51.000000000 +0200 +++ new/asdf-0.20.1/.release-please-manifest.json 2026-09-22 04:14:48.000000000 +0200 @@ -1,3 +1,3 @@ { - ".": "0.20.0" + ".": "0.20.1" } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asdf-0.20.0/CHANGELOG.md new/asdf-0.20.1/CHANGELOG.md --- old/asdf-0.20.0/CHANGELOG.md 2026-07-07 16:04:51.000000000 +0200 +++ new/asdf-0.20.1/CHANGELOG.md 2026-09-22 04:14:48.000000000 +0200 @@ -1,5 +1,17 @@ # Changelog +## [0.20.1](https://github.com/asdf-vm/asdf/compare/v0.20.0...v0.20.1) (2026-09-22) + + +### Bug Fixes + +* always update local Git repsitory in git.Update even if repositories have diverged ([#2298](https://github.com/asdf-vm/asdf/issues/2298)) ([b684098](https://github.com/asdf-vm/asdf/commit/b684098826642883560f31bd755568d537add933)) +* never generate a shim named "asdf" during reshim ([#2307](https://github.com/asdf-vm/asdf/issues/2307)) ([ca98e44](https://github.com/asdf-vm/asdf/commit/ca98e44ff49cb0a38966b23b42db962203478b59)) +* override urfave/cli built-in help printer for top level asdf -h/--help ([#2328](https://github.com/asdf-vm/asdf/issues/2328)) ([10df6e9](https://github.com/asdf-vm/asdf/commit/10df6e9f5fc7cb4b477bbfa32a5f5a187210f8da)) +* prevent incomplete installations from being shown as installed ([#2303](https://github.com/asdf-vm/asdf/issues/2303)) ([419e2da](https://github.com/asdf-vm/asdf/commit/419e2da7d7785d42d621f1c4fa93b7793cd18168)) +* Print stderr for list-all / latest-stable callbacks ([c1e632e](https://github.com/asdf-vm/asdf/commit/c1e632e937dd0794e9809081fef55f0ab359c21e)) +* remove urlfave/cli built-in `-h` flag that is causing confusing behavior ([#2294](https://github.com/asdf-vm/asdf/issues/2294)) ([017a770](https://github.com/asdf-vm/asdf/commit/017a770b046f0edc687f1d67ea80f211c88d5ab4)) + ## [0.20.0](https://github.com/asdf-vm/asdf/compare/v0.19.0...v0.20.0) (2026-07-07) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asdf-0.20.0/CONTRIBUTING.md new/asdf-0.20.1/CONTRIBUTING.md --- old/asdf-0.20.0/CONTRIBUTING.md 2026-07-07 16:04:51.000000000 +0200 +++ new/asdf-0.20.1/CONTRIBUTING.md 2026-09-22 04:14:48.000000000 +0200 @@ -18,6 +18,10 @@ See [docs/contribute/core.md](docs/contribute/core.md) or on our [Docs Site](https://asdf-vm.com/contribute/core.html). +### Architectural Decision Records + +Architectural Decision Records (ADRs) are stored in `docs/adr/`. These documents capture important architectural decisions made in the project and are useful for understanding the rationale behind past design choices. + ## Documentation Documentation can always be improved! See [docs/contribute/documentation.md](docs/contribute/documentation.md) or on our [Docs Site](https://asdf-vm.com/contribute/documentation.html). diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asdf-0.20.0/SECURITY.md new/asdf-0.20.1/SECURITY.md --- old/asdf-0.20.0/SECURITY.md 2026-07-07 16:04:51.000000000 +0200 +++ new/asdf-0.20.1/SECURITY.md 2026-09-22 04:14:48.000000000 +0200 @@ -9,7 +9,7 @@ <!-- x-release-please-start-version --> ``` -0.20.0 +0.20.1 ``` <!-- x-release-please-end --> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asdf-0.20.0/cmd/asdf/main.go new/asdf-0.20.1/cmd/asdf/main.go --- old/asdf-0.20.0/cmd/asdf/main.go 2026-07-07 16:04:51.000000000 +0200 +++ new/asdf-0.20.1/cmd/asdf/main.go 2026-09-22 04:14:48.000000000 +0200 @@ -10,7 +10,7 @@ ) // Do not touch this next line -var version = "0.20.0" // x-release-please-version +var version = "0.20.1" // x-release-please-version // Placeholder for the real code func main() { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asdf-0.20.0/cmd/asdf/main_test.go new/asdf-0.20.1/cmd/asdf/main_test.go --- old/asdf-0.20.0/cmd/asdf/main_test.go 2026-07-07 16:04:51.000000000 +0200 +++ new/asdf-0.20.1/cmd/asdf/main_test.go 2026-09-22 04:14:48.000000000 +0200 @@ -20,7 +20,10 @@ // Run tests with the asdf binary in the temp directory - // Uncomment these as they are implemented + t.Run("set_command", func(t *testing.T) { + runBatsFile(t, dir, "set_command.bats") + }) + t.Run("current_command", func(t *testing.T) { runBatsFile(t, dir, "current_command.bats") }) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asdf-0.20.0/docs/.vitepress/config.ts new/asdf-0.20.1/docs/.vitepress/config.ts --- old/asdf-0.20.0/docs/.vitepress/config.ts 2026-07-07 16:04:51.000000000 +0200 +++ new/asdf-0.20.1/docs/.vitepress/config.ts 2026-09-22 04:14:48.000000000 +0200 @@ -7,6 +7,7 @@ title: "asdf", description: "Manage multiple runtime versions with a single CLI tool", lastUpdated: true, + srcExclude: ['adr/**'], locales: { root: { label: "English", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asdf-0.20.0/docs/adr/001-incomplete-install-marker.md new/asdf-0.20.1/docs/adr/001-incomplete-install-marker.md --- old/asdf-0.20.0/docs/adr/001-incomplete-install-marker.md 1970-01-01 01:00:00.000000000 +0100 +++ new/asdf-0.20.1/docs/adr/001-incomplete-install-marker.md 2026-09-22 04:14:48.000000000 +0200 @@ -0,0 +1,48 @@ +# ADR 001: Use marker file to signal incomplete installs + +## Status + +Accepted + +## Context + +Due to the initial implementation of asdf all plugin versions must be installed into a subdirectory inside the asdf installs directory. When an installation is terminated by `SIGTERM`, `SIGKILL`, a dropped network connection, or loss of power, the installation directory persists, even though the installation itself was interrupted and is likely in an incomplete or broken state. When a user later runs commands that check for installed versions asdf incorrectly treats these incomplete installations as installed. This happens because asdf considers every directory inside of `$ASDF_DATA_DIR/installs` as a valid installation. This leads to confusing behavior and errors the user may be unable to trace back to the root cause. + +## Decision + +We will implement a mechanism to mark incomplete version installation directories using a marker file stored in a dedicated directory. The mechanism will be robust and prevent any type of failure from resulting in an installation that asdf treats as valid when it is incomplete. + +Here is how the install process will work: + +1. Before beginning an installation asdf will check if the installation directory already exists along with a marker file in the install-locks directory with the same name. If it does, both the installation directory and marker are removed to clean up any stale incomplete installations. +2. asdf creates a marker file in the install-locks directory first (e.g., `$ASDF_DATA_DIR/install-locks/<tool>/<version>`), then creates the installation directory itself (e.g., `$ASDF_DATA_DIR/installs/<tool>/<version>`). Using a separate top-level directory ensures that: + - Plugins which completely replace `$ASDF_INSTALL_PATH` contents do not inadvertently remove the marker + - The install-locks directory is completely separate from the installs directory +3. The plugin's download and install callbacks are invoked, along with any pre-download and pre-install hooks. If the install callback fails the installation directory is removed. +4. When the installation is finished asdf removes the marker file. + +Additionally, signal handlers will be registered for `SIGINT` and `SIGTERM` before installation that will trigger removal of the install directory. + +Commands that list installed versions or check for an installed version do this by reading directories. Now there will be an additional check for the marker file in the install-locks directory for each version directory. + +## Consequences + +### Positive + +* Signal interruption (Ctrl+C) properly cleans up partial installations +* No manual cleanup required for failed installations +* Clear distinction between complete and incomplete installations +* Users no longer encounter confusing errors from partial installations +* Backward compatibility is maintained, all plugins will continue to work +* Marker files survive plugins that replace `$ASDF_INSTALL_PATH` contents (e.g., asdf-nim) +* Simple implementation without temporary directory staging + +### Negative/Neutral + +* `install-locks` directory will be created at the top level of `$ASDF_DATA_DIR` +* Requires creating install-locks directory structure on first incomplete marker creation + +## Related Issues + +* https://github.com/asdf-vm/asdf/issues/2184 +* https://github.com/asdf-vm/asdf/issues/2036 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asdf-0.20.0/docs/guide/getting-started.md new/asdf-0.20.1/docs/guide/getting-started.md --- old/asdf-0.20.0/docs/guide/getting-started.md 2026-07-07 16:04:51.000000000 +0200 +++ new/asdf-0.20.1/docs/guide/getting-started.md 2026-09-22 04:14:48.000000000 +0200 @@ -34,7 +34,7 @@ <!-- x-release-please-start-version --> 1. [Install Go](https://go.dev/doc/install) -2. Run `go install github.com/asdf-vm/asdf/cmd/[email protected]` +2. Run `go install github.com/asdf-vm/asdf/cmd/[email protected]` <!-- x-release-please-end --> :::: @@ -48,7 +48,7 @@ <!-- x-release-please-start-version --> 1. Clone the asdf repository: ```shell - git clone https://github.com/asdf-vm/asdf.git --branch v0.20.0 + git clone https://github.com/asdf-vm/asdf.git --branch v0.20.1 ``` <!-- x-release-please-end --> 2. Run `make` diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asdf-0.20.0/docs/ja-jp/guide/getting-started.md new/asdf-0.20.1/docs/ja-jp/guide/getting-started.md --- old/asdf-0.20.0/docs/ja-jp/guide/getting-started.md 2026-07-07 16:04:51.000000000 +0200 +++ new/asdf-0.20.1/docs/ja-jp/guide/getting-started.md 2026-09-22 04:14:48.000000000 +0200 @@ -33,7 +33,7 @@ <!-- x-release-please-start-version --> 1. [Goをインストールする](https://go.dev/doc/install) -2. コマンドを実行: `go install github.com/asdf-vm/asdf/cmd/[email protected]` +2. コマンドを実行: `go install github.com/asdf-vm/asdf/cmd/[email protected]` <!-- x-release-please-end --> :::: @@ -47,7 +47,7 @@ <!-- x-release-please-start-version --> 1. asdfリポジトリをクローン: ```shell - git clone https://github.com/asdf-vm/asdf.git --branch v0.20.0 + git clone https://github.com/asdf-vm/asdf.git --branch v0.20.1 ``` <!-- x-release-please-end --> 2. `make`を実行。 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asdf-0.20.0/docs/ko-kr/guide/getting-started.md new/asdf-0.20.1/docs/ko-kr/guide/getting-started.md --- old/asdf-0.20.0/docs/ko-kr/guide/getting-started.md 2026-07-07 16:04:51.000000000 +0200 +++ new/asdf-0.20.1/docs/ko-kr/guide/getting-started.md 2026-09-22 04:14:48.000000000 +0200 @@ -34,7 +34,7 @@ <!-- x-release-please-start-version --> 1. [Go 설치](https://go.dev/doc/install) -2. `go install github.com/asdf-vm/asdf/cmd/[email protected]` 실행 +2. `go install github.com/asdf-vm/asdf/cmd/[email protected]` 실행 <!-- x-release-please-end --> :::: @@ -48,7 +48,7 @@ <!-- x-release-please-start-version --> 1. asdf 리포지토리 클론: ```shell - git clone https://github.com/asdf-vm/asdf.git --branch v0.20.0 + git clone https://github.com/asdf-vm/asdf.git --branch v0.20.1 ``` <!-- x-release-please-end --> 2. `make` 실행 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asdf-0.20.0/docs/manage/versions.md new/asdf-0.20.1/docs/manage/versions.md --- old/asdf-0.20.0/docs/manage/versions.md 2026-07-07 16:04:51.000000000 +0200 +++ new/asdf-0.20.1/docs/manage/versions.md 2026-09-22 04:14:48.000000000 +0200 @@ -102,6 +102,10 @@ Will tell asdf to use Elixir `1.18.1` in the current shell session. +For tool names that contain dashes, replace each dash with an underscore when +constructing the environment variable name. For example, the `aws-sam-cli` +tool set `ASDF_AWS_SAM_CLI_VERSION`. + :::warning Because this is an environment variable, it only takes effect where it is set. Any other shell sessions that are running will still use to whatever version is diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asdf-0.20.0/docs/package-lock.json new/asdf-0.20.1/docs/package-lock.json --- old/asdf-0.20.0/docs/package-lock.json 2026-07-07 16:04:51.000000000 +0200 +++ new/asdf-0.20.1/docs/package-lock.json 2026-09-22 04:14:48.000000000 +0200 @@ -5,8 +5,8 @@ "packages": { "": { "devDependencies": { - "@types/node": "^26.1.0", - "prettier": "^3.9.4", + "@types/node": "^26.4.0", + "prettier": "^3.9.6", "vitepress": "^1.6.4" } }, @@ -1255,9 +1255,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "26.1.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.0.tgz", - "integrity": "sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw==", + "version": "26.4.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.4.0.tgz", + "integrity": "sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1983,9 +1983,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "dev": true, "funding": [ { @@ -2028,9 +2028,9 @@ "license": "ISC" }, "node_modules/postcss": { - "version": "8.5.12", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", - "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", + "version": "8.5.25", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz", + "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==", "dev": true, "funding": [ { @@ -2048,7 +2048,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -2068,9 +2068,9 @@ } }, "node_modules/prettier": { - "version": "3.9.4", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.4.tgz", - "integrity": "sha512-yWG/o/4oJfo036EKAfK6ACAoDOfHeRHx4tuxkfBZiauURiaSmYwlpOr5LQqKtIkRD2z1PLteme2WoxEnj4tHTg==", + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", "dev": true, "license": "MIT", "bin": { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asdf-0.20.0/docs/package.json new/asdf-0.20.1/docs/package.json --- old/asdf-0.20.0/docs/package.json 2026-07-07 16:04:51.000000000 +0200 +++ new/asdf-0.20.1/docs/package.json 2026-09-22 04:14:48.000000000 +0200 @@ -7,8 +7,8 @@ "preview": "vitepress preview" }, "devDependencies": { - "@types/node": "^26.1.0", - "prettier": "^3.9.4", + "@types/node": "^26.4.0", + "prettier": "^3.9.6", "vitepress": "^1.6.4" } } \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asdf-0.20.0/docs/pt-br/guide/getting-started.md new/asdf-0.20.1/docs/pt-br/guide/getting-started.md --- old/asdf-0.20.0/docs/pt-br/guide/getting-started.md 2026-07-07 16:04:51.000000000 +0200 +++ new/asdf-0.20.1/docs/pt-br/guide/getting-started.md 2026-09-22 04:14:48.000000000 +0200 @@ -39,7 +39,7 @@ <!-- x-release-please-start-version --> ```shell -git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch v0.20.0 +git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch v0.20.1 ``` <!-- x-release-please-end --> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asdf-0.20.0/docs/zh-hans/guide/getting-started.md new/asdf-0.20.1/docs/zh-hans/guide/getting-started.md --- old/asdf-0.20.0/docs/zh-hans/guide/getting-started.md 2026-07-07 16:04:51.000000000 +0200 +++ new/asdf-0.20.1/docs/zh-hans/guide/getting-started.md 2026-09-22 04:14:48.000000000 +0200 @@ -34,7 +34,7 @@ <!-- x-release-please-start-version --> 1. [安装 Go](https://go.dev/doc/install) -2. 运行 `go install github.com/asdf-vm/asdf/cmd/[email protected]` +2. 运行 `go install github.com/asdf-vm/asdf/cmd/[email protected]` <!-- x-release-please-end --> :::: @@ -48,7 +48,7 @@ <!-- x-release-please-start-version --> 1. 克隆 asdf 仓库: ```shell - git clone https://github.com/asdf-vm/asdf.git --branch v0.20.0 + git clone https://github.com/asdf-vm/asdf.git --branch v0.20.1 ``` <!-- x-release-please-end --> 2. 运行 `make` diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asdf-0.20.0/go.mod new/asdf-0.20.1/go.mod --- old/asdf-0.20.0/go.mod 2026-07-07 16:04:51.000000000 +0200 +++ new/asdf-0.20.1/go.mod 2026-09-22 04:14:48.000000000 +0200 @@ -3,13 +3,13 @@ go 1.26.3 require ( - github.com/go-git/go-git/v5 v5.19.1 + github.com/go-git/go-git/v5 v5.19.2 github.com/mgechev/revive v1.7.0 github.com/otiai10/copy v1.14.0 github.com/rogpeppe/go-internal v1.14.1 - github.com/stretchr/testify v1.11.1 - github.com/urfave/cli/v3 v3.3.3 - golang.org/x/sys v0.45.0 + github.com/stretchr/testify v1.12.1 + github.com/urfave/cli/v3 v3.13.0 + golang.org/x/sys v0.46.0 gopkg.in/ini.v1 v1.67.0 honnef.co/go/tools v0.5.1 mvdan.cc/gofumpt v0.7.0 @@ -23,7 +23,6 @@ github.com/chavacava/garif v0.1.0 // indirect github.com/cloudflare/circl v1.6.3 // indirect github.com/cyphar/filepath-securejoin v0.6.1 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/fatih/color v1.18.0 // indirect github.com/fatih/structtag v1.2.0 // indirect @@ -41,20 +40,19 @@ github.com/mgechev/dots v0.0.0-20210922191527-e955255bf517 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/pjbgf/sha1cd v0.6.0 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/skeema/knownhosts v1.3.1 // indirect github.com/spf13/afero v1.12.0 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect - golang.org/x/crypto v0.51.0 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect + golang.org/x/crypto v0.53.0 // indirect golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 // indirect - golang.org/x/mod v0.35.0 // indirect - golang.org/x/net v0.55.0 // indirect - golang.org/x/sync v0.20.0 // indirect - golang.org/x/text v0.37.0 // indirect - golang.org/x/tools v0.44.0 // indirect + golang.org/x/mod v0.37.0 // indirect + golang.org/x/net v0.56.0 // indirect + golang.org/x/sync v0.21.0 // indirect + golang.org/x/text v0.39.0 // indirect + golang.org/x/tools v0.47.0 // indirect golang.org/x/tools/go/expect v0.1.1-deprecated // indirect gopkg.in/warnings.v0 v0.1.2 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asdf-0.20.0/go.sum new/asdf-0.20.1/go.sum --- old/asdf-0.20.0/go.sum 2026-07-07 16:04:51.000000000 +0200 +++ new/asdf-0.20.1/go.sum 2026-09-22 04:14:48.000000000 +0200 @@ -18,7 +18,6 @@ github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= @@ -36,8 +35,8 @@ github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00= -github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ= +github.com/go-git/go-git/v5 v5.19.2 h1:wkfn7vOlUBu8ivAWKBWisTiwJK4jYHzTF8Ndv1LyGqY= +github.com/go-git/go-git/v5 v5.19.2/go.mod h1:QqCBE1EFN5ddFmrliLQ3/ntRCUjZU3EJuwuB/jWEHjk= github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= @@ -82,7 +81,6 @@ github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= 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/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= @@ -104,26 +102,28 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -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/urfave/cli/v3 v3.3.3 h1:byCBaVdIXuLPIDm5CYZRVG6NvT7tv1ECqdU4YzlEa3I= -github.com/urfave/cli/v3 v3.3.3/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= +github.com/urfave/cli/v3 v3.13.0 h1:Dr6jqMfIyyFsRVn7Nz5mqLsMY+ZMpfh3a0aMs+umPVY= +github.com/urfave/cli/v3 v3.13.0/go.mod h1:vXn6HxPNccJSzQr2QvwVncOKrgYGIHU0HY5h8B2nQj4= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= -golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 h1:1P7xPZEwZMoBoz0Yze5Nx2/4pxj6nw9ZqHWXqP0iRgQ= golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= -golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= -golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= -golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= -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/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -131,17 +131,17 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/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/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= -golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= +golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= +golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= +golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= -golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asdf-0.20.0/internal/cli/cli.go new/asdf-0.20.1/internal/cli/cli.go --- old/asdf-0.20.0/internal/cli/cli.go 2026-07-07 16:04:51.000000000 +0200 +++ new/asdf-0.20.1/internal/cli/cli.go 2026-09-22 04:14:48.000000000 +0200 @@ -54,14 +54,36 @@ log.SetFlags(0) app := &cli.Command{ - Name: "asdf", - Version: version, - Copyright: "(c) 2024 Trevor Brown", + HideHelp: false, + HideHelpCommand: true, + Name: "asdf", + Version: version, + Copyright: "(c) 2024 Trevor Brown", Authors: []any{ - mail.Address{Name: "Trevor Brown", Address: "[email protected]"}, + mail.Address{Name: "Trevor Brown"}, }, Usage: "The multiple runtime version manager", UsageText: usageText, + Action: func(_ context.Context, cmd *cli.Command) error { + // If no args, show help + if cmd.Args().Len() == 0 { + return helpCommand(logger, version, "", "") + } + + // Check if first arg matches any command - if so, let urfave/cli handle it + firstArg := cmd.Args().Get(0) + for _, c := range cmd.Commands { + if c.Name == firstArg { + return nil + } + } + + // No matching command - manually invoke CommandNotFound behavior + logger.Printf("invalid command provided: %s\n\n", firstArg) + helpCommand(logger, version, "", "") + cli.OsExiter(1) + return cli.Exit("", 1) + }, Commands: []*cli.Command{ { Name: "cmd", @@ -339,6 +361,17 @@ }, } + defaultHelpPrinter := cli.HelpPrinter + cli.HelpPrinter = func(w io.Writer, templ string, data interface{}) { + // Only override help output for the root "asdf" command. Subcommand + // help still uses urfave/cli's built-in printer. + if cmd, ok := data.(*cli.Command); ok && cmd == app { + helpCommand(logger, version, "", "") + return + } + defaultHelpPrinter(w, templ, data) + } + err := unsetAsdfReservedEnvVars() if err != nil { cli.OsExiter(1) @@ -1035,7 +1068,7 @@ } else if toolVersion == "latest" || strings.HasPrefix(toolVersion, "latest:") { query := strings.TrimPrefix(toolVersion, "latest") query = strings.TrimPrefix(query, ":") - resolved, err := versions.Latest(plugin, query) + resolved, err := versions.Latest(plugin, query, os.Stderr) if err != nil { failTest(l, "could not get latest version") } @@ -1173,7 +1206,7 @@ } if !all { - err = latestForPlugin(conf, toolName, pattern, false) + err = latestForPlugin(conf, toolName, pattern, false, os.Stderr) if err != nil { cli.OsExiter(1) } @@ -1190,7 +1223,7 @@ var maybeErr error // loop over all plugins and show latest for each one. for _, plugin := range plugins { - maybeErr = latestForPlugin(conf, plugin.Name, "", true) + maybeErr = latestForPlugin(conf, plugin.Name, "", true, os.Stderr) if maybeErr != nil { err = maybeErr } @@ -1558,10 +1591,10 @@ return shims.GenerateForVersion(conf, plugin, version, out, errOut) } -func latestForPlugin(conf config.Config, toolName, pattern string, showStatus bool) error { +func latestForPlugin(conf config.Config, toolName, pattern string, showStatus bool, errOut io.Writer) error { // show single plugin plugin := plugins.New(conf, toolName) - latest, err := versions.Latest(plugin, pattern) + latest, err := versions.Latest(plugin, pattern, errOut) if err != nil && err.Error() != "no latest version found" { fmt.Printf("unable to load latest version: %s\n", err) return err diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asdf-0.20.0/internal/cli/set/set.go new/asdf-0.20.1/internal/cli/set/set.go --- old/asdf-0.20.0/internal/cli/set/set.go 2026-07-07 16:04:51.000000000 +0200 +++ new/asdf-0.20.1/internal/cli/set/set.go 2026-09-22 04:14:48.000000000 +0200 @@ -40,7 +40,7 @@ parsedVersion := toolversions.ParseFromCliArg(version) if parsedVersion.Type == "latest" { plugin := plugins.New(conf, args[0]) - resolvedVersion, err := versions.Latest(plugin, parsedVersion.Value) + resolvedVersion, err := versions.Latest(plugin, parsedVersion.Value, stderr) if err != nil { return fmt.Errorf("unable to resolve latest version for %s", plugin.Name) } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asdf-0.20.0/internal/data/data.go new/asdf-0.20.1/internal/data/data.go --- old/asdf-0.20.0/internal/data/data.go 2026-07-07 16:04:51.000000000 +0200 +++ new/asdf-0.20.1/internal/data/data.go 2026-09-22 04:14:48.000000000 +0200 @@ -9,6 +9,7 @@ const ( dataDirDownloads = "downloads" dataDirInstalls = "installs" + dataDirLocks = "install-locks" dataDirPlugins = "plugins" ) @@ -33,3 +34,8 @@ func PluginDirectory(dataDir, pluginName string) string { return filepath.Join(dataDir, dataDirPlugins, pluginName) } + +// InstallLocksDirectory returns the directory for install marker/lock files +func InstallLocksDirectory(dataDir, pluginName string) string { + return filepath.Join(dataDir, dataDirLocks, pluginName) +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asdf-0.20.0/internal/git/git.go new/asdf-0.20.1/internal/git/git.go --- old/asdf-0.20.0/internal/git/git.go 2026-07-07 16:04:51.000000000 +0200 +++ new/asdf-0.20.1/internal/git/git.go 2026-09-22 04:14:48.000000000 +0200 @@ -113,7 +113,7 @@ commonOpts := []string{"git", "-C", r.Directory} - refSpec := fmt.Sprintf("%s:%s", shortRef, shortRef) + refSpec := fmt.Sprintf("+%s:%s", shortRef, shortRef) cmdStr := append(commonOpts, []string{"fetch", "--prune", "--update-head-ok", remoteName, refSpec}...) _, stderr, err := exec(cmdStr) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asdf-0.20.0/internal/git/git_test.go new/asdf-0.20.1/internal/git/git_test.go --- old/asdf-0.20.0/internal/git/git_test.go 2026-07-07 16:04:51.000000000 +0200 +++ new/asdf-0.20.1/internal/git/git_test.go 2026-09-22 04:14:48.000000000 +0200 @@ -212,6 +212,39 @@ assert.Equal(t, hash, latestHash) assert.Equal(t, newHash, latestHash) }) + + t.Run("updates repo when remote has diverged with non-fast-forward changes", func(t *testing.T) { + remoteDir := generateRepo(t) + localDir := t.TempDir() + localRepo := NewRepo(localDir) + + err := localRepo.Clone(remoteDir, "") + assert.Nil(t, err) + + originalRemoteHash, err := getCurrentCommit(remoteDir) + assert.Nil(t, err) + + originalLocalHash, err := getCurrentCommit(localDir) + assert.Nil(t, err) + assert.Equal(t, originalRemoteHash, originalLocalHash) + + err = amendCommitInRepo(remoteDir) + assert.Nil(t, err) + + newRemoteHash, err := getCurrentCommit(remoteDir) + assert.Nil(t, err) + assert.NotEqual(t, originalRemoteHash, newRemoteHash) + + updatedToRef, oldHash, newHash, err := localRepo.Update("") + assert.Nil(t, err) + assert.Equal(t, "refs/heads/master", updatedToRef) + assert.Equal(t, originalLocalHash, oldHash) + assert.Equal(t, newRemoteHash, newHash) + + currentLocalHash, err := getCurrentCommit(localDir) + assert.Nil(t, err) + assert.Equal(t, newRemoteHash, currentLocalHash) + }) } func getCurrentCommit(path string) (string, error) { @@ -253,6 +286,24 @@ return previousHash.String(), nil } +func amendCommitInRepo(path string) error { + repo, err := git.PlainOpen(path) + if err != nil { + return err + } + + worktree, err := repo.Worktree() + if err != nil { + return err + } + + _, err = worktree.Commit("amended commit", &git.CommitOptions{ + Amend: true, + }) + + return err +} + func generateRepo(t *testing.T) string { t.Helper() tempDir := t.TempDir() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asdf-0.20.0/internal/installs/installs.go new/asdf-0.20.1/internal/installs/installs.go --- old/asdf-0.20.0/internal/installs/installs.go 2026-07-07 16:04:51.000000000 +0200 +++ new/asdf-0.20.1/internal/installs/installs.go 2026-09-22 04:14:48.000000000 +0200 @@ -4,6 +4,8 @@ package installs import ( + "errors" + "fmt" "io/fs" "os" "path/filepath" @@ -14,6 +16,12 @@ "github.com/asdf-vm/asdf/internal/toolversions" ) +// IncompleteMarkerPath returns the path to the incomplete marker file for a version +func IncompleteMarkerPath(conf config.Config, plugin plugins.Plugin, version toolversions.Version) string { + formattedVersion := toolversions.FormatForFS(version) + return filepath.Join(data.InstallLocksDirectory(conf.DataDir, plugin.Name), formattedVersion) +} + // Installed returns a slice of all installed versions for a given plugin func Installed(conf config.Config, plugin plugins.Plugin) (versions []string, err error) { installDirectory := data.InstallDirectory(conf.DataDir, plugin.Name) @@ -31,7 +39,18 @@ continue } - versions = append(versions, toolversions.VersionStringFromFSFormat(file.Name())) + displayVersion := toolversions.VersionStringFromFSFormat(file.Name()) + version := toolversions.Parse(displayVersion) + _, statErr := os.Stat(IncompleteMarkerPath(conf, plugin, version)) + if statErr == nil { + continue + } + if !errors.Is(statErr, fs.ErrNotExist) { + err = fmt.Errorf("checking install marker for version %s: %w", displayVersion, statErr) + continue + } + + versions = append(versions, displayVersion) } return versions, err @@ -59,7 +78,14 @@ func IsInstalled(conf config.Config, plugin plugins.Plugin, version toolversions.Version) bool { installDir := InstallPath(conf, plugin, version) - // Check if version already installed _, err := os.Stat(installDir) - return !os.IsNotExist(err) + if err != nil { + return false + } + + _, err = os.Stat(IncompleteMarkerPath(conf, plugin, version)) + if err == nil { + return false + } + return errors.Is(err, fs.ErrNotExist) } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asdf-0.20.0/internal/installs/installs_test.go new/asdf-0.20.1/internal/installs/installs_test.go --- old/asdf-0.20.0/internal/installs/installs_test.go 2026-07-07 16:04:51.000000000 +0200 +++ new/asdf-0.20.1/internal/installs/installs_test.go 2026-09-22 04:14:48.000000000 +0200 @@ -6,6 +6,7 @@ "testing" "github.com/asdf-vm/asdf/internal/config" + "github.com/asdf-vm/asdf/internal/data" "github.com/asdf-vm/asdf/internal/installtest" "github.com/asdf-vm/asdf/internal/plugins" "github.com/asdf-vm/asdf/internal/repotest" @@ -63,6 +64,68 @@ assert.Nil(t, err) assert.Equal(t, installedVersions, []string{"1.0.0"}) }) + + t.Run("filters out directories with incomplete marker", func(t *testing.T) { + mockInstall(t, conf, plugin, "1.0.0") + mockInstall(t, conf, plugin, "2.0.0") + mockInstall(t, conf, plugin, "3.0.0") + + version2 := toolversions.Version{Type: "version", Value: "2.0.0"} + err := markIncomplete(conf, plugin, version2) + assert.Nil(t, err) + + installedVersions, err := Installed(conf, plugin) + assert.Nil(t, err) + assert.Equal(t, 2, len(installedVersions)) + assert.Contains(t, installedVersions, "1.0.0") + assert.Contains(t, installedVersions, "3.0.0") + assert.NotContains(t, installedVersions, "2.0.0") + }) + + t.Run("continues scanning and reports error when a marker lookup fails", func(t *testing.T) { + if os.Geteuid() == 0 { + t.Skip("permission checks do not apply when running as root") + } + + conf4, plugin4 := generateConfig(t) + mockInstall(t, conf4, plugin4, "1.0.0") + mockInstall(t, conf4, plugin4, "2.0.0") + mockInstall(t, conf4, plugin4, "3.0.0") + + locksDir := data.InstallLocksDirectory(conf4.DataDir, plugin4.Name) + assert.Nil(t, os.MkdirAll(locksDir, 0o755)) + assert.Nil(t, os.Chmod(locksDir, 0o000)) + t.Cleanup(func() { _ = os.Chmod(locksDir, 0o755) }) + + installedVersions, err := Installed(conf4, plugin4) + assert.Empty(t, installedVersions) + assert.NotNil(t, err) + }) + + t.Run("returns all versions when none have incomplete marker", func(t *testing.T) { + conf2, plugin2 := generateConfig(t) + mockInstall(t, conf2, plugin2, "1.0.0") + mockInstall(t, conf2, plugin2, "2.0.0") + + installedVersions, err := Installed(conf2, plugin2) + assert.Nil(t, err) + assert.Equal(t, 2, len(installedVersions)) + }) + + t.Run("filters out ref installs with incomplete marker", func(t *testing.T) { + conf3, plugin3 := generateConfig(t) + refVersion := toolversions.Version{Type: "ref", Value: "foo"} + path := InstallPath(conf3, plugin3, refVersion) + err := os.MkdirAll(path, os.ModePerm) + assert.Nil(t, err) + + err = markIncomplete(conf3, plugin3, refVersion) + assert.Nil(t, err) + + installedVersions, err := Installed(conf3, plugin3) + assert.Nil(t, err) + assert.Empty(t, installedVersions) + }) } func TestIsInstalled(t *testing.T) { @@ -77,6 +140,16 @@ version := toolversions.Version{Type: "version", Value: "1.0.0"} assert.True(t, IsInstalled(conf, plugin, version)) }) + + t.Run("returns false when directory exists but has incomplete marker", func(t *testing.T) { + version := toolversions.Version{Type: "version", Value: "2.0.0"} + mockInstall(t, conf, plugin, "2.0.0") + + err := markIncomplete(conf, plugin, version) + assert.Nil(t, err) + + assert.False(t, IsInstalled(conf, plugin, version)) + }) } // helper functions @@ -106,3 +179,17 @@ err := installtest.InstallOneVersion(conf, plugin, "version", version) assert.Nil(t, err) } + +func markIncomplete(conf config.Config, plugin plugins.Plugin, version toolversions.Version) error { + markerPath := IncompleteMarkerPath(conf, plugin, version) + err := os.MkdirAll(filepath.Dir(markerPath), 0o755) + if err != nil { + return err + } + file, err := os.Create(markerPath) + if err != nil { + return err + } + defer file.Close() + return nil +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asdf-0.20.0/internal/shims/shims.go new/asdf-0.20.1/internal/shims/shims.go --- old/asdf-0.20.0/internal/shims/shims.go 2026-07-07 16:04:51.000000000 +0200 +++ new/asdf-0.20.1/internal/shims/shims.go 2026-09-22 04:14:48.000000000 +0200 @@ -24,6 +24,13 @@ const shimDirName = "shims" +var reservedShimNames = map[string]bool{ + // shims named `asdf` must never be generated, since it would shadow the + // same `asdf` command all asdf shims invoke under the hood. + // See https://github.com/asdf-vm/asdf/issues/2166 + "asdf": true, +} + // UnknownCommandError is an error returned when a shim is not found type UnknownCommandError struct { shim string @@ -309,12 +316,21 @@ // Write generates a shim script and writes it to disk func Write(conf config.Config, plugin plugins.Plugin, version toolversions.Version, executablePath string) error { + shimName := filepath.Base(executablePath) + if reservedShimNames[shimName] { + // Remove any stale shim left over from before this guard existed. + shimPath := Path(conf, shimName) + if err := os.Remove(shimPath); err != nil && !os.IsNotExist(err) { + return err + } + return nil + } + err := ensureShimDirExists(conf) if err != nil { return err } - shimName := filepath.Base(executablePath) shimPath := Path(conf, shimName) versions := []toolversions.ToolVersions{{Name: plugin.Name, Versions: []string{toolversions.Format(version)}}} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asdf-0.20.0/internal/shims/shims_test.go new/asdf-0.20.1/internal/shims/shims_test.go --- old/asdf-0.20.0/internal/shims/shims_test.go 2026-07-07 16:04:51.000000000 +0200 +++ new/asdf-0.20.1/internal/shims/shims_test.go 2026-09-22 04:14:48.000000000 +0200 @@ -388,6 +388,35 @@ assert.Equal(t, want, string(content)) os.Remove(shimPath) }) + + for reservedName := range reservedShimNames { + t.Run(fmt.Sprintf("never creates a shim named %s, no matter what executable is passed", reservedName), func(t *testing.T) { + fakeExecutable := filepath.Join(filepath.Dir(executable), reservedName) + err := Write(conf, plugin, version, fakeExecutable) + assert.Nil(t, err) + + shimPath := Path(conf, reservedName) + _, statErr := os.Stat(shimPath) + assert.True(t, os.IsNotExist(statErr), fmt.Sprintf("expected no shim to be created for the reserved name %q", reservedName)) + }) + } + + for reservedName := range reservedShimNames { + t.Run(fmt.Sprintf("removes a pre-existing shim named %s left over from before this guard existed", reservedName), func(t *testing.T) { + shimPath := Path(conf, reservedName) + err := os.WriteFile(shimPath, []byte(fmt.Sprintf("exec asdf exec %q \"$@\"\n", reservedName)), 0o755) + assert.Nil(t, err) + _, statErr := os.Stat(shimPath) + assert.Nil(t, statErr, "test setup: expected the stale shim to exist before Write runs") + + fakeExecutable := filepath.Join(filepath.Dir(executable), reservedName) + err = Write(conf, plugin, version, fakeExecutable) + assert.Nil(t, err) + + _, statErr = os.Stat(shimPath) + assert.True(t, os.IsNotExist(statErr), fmt.Sprintf("expected the pre-existing shims/%s to be removed", reservedName)) + }) + } } func TestToolExecutables(t *testing.T) { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asdf-0.20.0/internal/versions/versions.go new/asdf-0.20.1/internal/versions/versions.go --- old/asdf-0.20.0/internal/versions/versions.go 2026-07-07 16:04:51.000000000 +0200 +++ new/asdf-0.20.1/internal/versions/versions.go 2026-09-22 04:14:48.000000000 +0200 @@ -6,9 +6,13 @@ "errors" "fmt" "io" + "io/fs" "os" + "os/signal" + "path/filepath" "regexp" "strings" + "syscall" "github.com/asdf-vm/asdf/internal/config" "github.com/asdf-vm/asdf/internal/hook" @@ -128,7 +132,7 @@ resolvedVersion := "" if version.Type == latestVersion { - resolvedVersion, err = Latest(plugin, version.Value) + resolvedVersion, err = Latest(plugin, version.Value, stdErr) if err != nil { return err } @@ -153,13 +157,61 @@ if version.Type == "path" { return UninstallableVersionError{toolName: plugin.Name, versionType: "path"} } - downloadDir := installs.DownloadPath(conf, plugin, version) + installDir := installs.InstallPath(conf, plugin, version) + err = cleanupStaleIncomplete(conf, plugin, version) + if err != nil { + return fmt.Errorf("failed to cleanup stale incomplete install: %w", err) + } if installs.IsInstalled(conf, plugin, version) { return VersionAlreadyInstalledError{version: version, toolName: plugin.Name} } + downloadDir := installs.DownloadPath(conf, plugin, version) + + installParentDir := filepath.Dir(installDir) + err = os.MkdirAll(installParentDir, 0o777) + if err != nil { + return fmt.Errorf("unable to create install parent dir: %w", err) + } + + err = markIncomplete(conf, plugin, version) + if err != nil { + return fmt.Errorf("failed to mark installation as incomplete: %w", err) + } + + err = os.MkdirAll(installDir, 0o777) + if err != nil { + return fmt.Errorf("unable to create install dir: %w", err) + } + + // Set up signal handler to clean up on interrupt + sigChan := make(chan os.Signal, 1) + done := make(chan struct{}) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + + stopCleanup := func() { + signal.Stop(sigChan) + select { + case <-done: + default: + close(done) + } + } + + go func() { + select { + case <-sigChan: + os.RemoveAll(installDir) + os.Exit(1) + case <-done: + return + } + }() + + defer stopCleanup() + concurrency, _ := conf.Concurrency() env := map[string]string{ "ASDF_INSTALL_TYPE": version.Type, @@ -189,19 +241,24 @@ return fmt.Errorf("failed to run pre-install hook: %w", err) } - err = os.MkdirAll(installDir, 0o777) + err = plugin.RunCallback("install", []string{}, env, stdOut, stdErr) if err != nil { - return fmt.Errorf("unable to create install dir: %w", err) + if rmErr := os.RemoveAll(installDir); rmErr != nil { + fmt.Fprintf(stdErr, "failed to clean up '%s' due to %s\n", installDir, rmErr) + } + return fmt.Errorf("failed to run install callback: %w", err) } - err = plugin.RunCallback("install", []string{}, env, stdOut, stdErr) + err = markComplete(conf, plugin, version) if err != nil { if rmErr := os.RemoveAll(installDir); rmErr != nil { fmt.Fprintf(stdErr, "failed to clean up '%s' due to %s\n", installDir, rmErr) } - return fmt.Errorf("failed to run install callback: %w", err) + return fmt.Errorf("failed to mark installation as complete: %w", err) } + stopCleanup() + // Reshim err = shims.GenerateAll(conf, stdOut, stdErr) if err != nil { @@ -235,11 +292,10 @@ // the version it returns. If the callback is missing it invokes the list-all // callback and returns the last version matching the query, if a query is // provided. -func Latest(plugin plugins.Plugin, query string) (version string, err error) { +func Latest(plugin plugins.Plugin, query string, stdErr io.Writer) (version string, err error) { var stdOut strings.Builder - var stdErr strings.Builder - err = plugin.RunCallback("latest-stable", []string{query}, map[string]string{}, &stdOut, &stdErr) + err = plugin.RunCallback("latest-stable", []string{query}, map[string]string{}, &stdOut, stdErr) if err == nil { versions := parseVersions(stdOut.String()) if len(versions) < 1 { @@ -253,7 +309,7 @@ return version, err } - allVersions, err := AllVersions(plugin) + allVersions, err := AllVersions(plugin, stdErr) if err != nil { return version, err } @@ -276,11 +332,10 @@ // AllVersions returns a slice of all available versions for the tool managed by // the given plugin by invoking the plugin's list-all callback -func AllVersions(plugin plugins.Plugin) (versions []string, err error) { +func AllVersions(plugin plugins.Plugin, stdErr io.Writer) (versions []string, err error) { var stdout strings.Builder - var stderr strings.Builder - err = plugin.RunCallback("list-all", []string{}, map[string]string{}, &stdout, &stderr) + err = plugin.RunCallback("list-all", []string{}, map[string]string{}, &stdout, stdErr) if err != nil { return versions, err } @@ -368,3 +423,53 @@ } return versions } + +// markComplete removes the incomplete marker file +func markComplete(conf config.Config, plugin plugins.Plugin, version toolversions.Version) error { + markerPath := installs.IncompleteMarkerPath(conf, plugin, version) + err := os.Remove(markerPath) + if err != nil { + return fmt.Errorf("failed to remove incomplete marker: %w", err) + } + return nil +} + +// markIncomplete creates an incomplete marker file +func markIncomplete(conf config.Config, plugin plugins.Plugin, version toolversions.Version) error { + markerPath := installs.IncompleteMarkerPath(conf, plugin, version) + err := os.MkdirAll(filepath.Dir(markerPath), 0o755) + if err != nil { + return fmt.Errorf("failed to create marker directory: %w", err) + } + file, err := os.Create(markerPath) + if err != nil { + return fmt.Errorf("failed to create incomplete marker: %w", err) + } + defer file.Close() + return nil +} + +// cleanupStaleIncomplete removes an install directory if it has an incomplete marker +func cleanupStaleIncomplete(conf config.Config, plugin plugins.Plugin, version toolversions.Version) error { + markerPath := installs.IncompleteMarkerPath(conf, plugin, version) + _, err := os.Stat(markerPath) + if errors.Is(err, fs.ErrNotExist) { + return nil + } + if err != nil { + return fmt.Errorf("failed to check for incomplete marker: %w", err) + } + + installPath := installs.InstallPath(conf, plugin, version) + err = os.RemoveAll(installPath) + if err != nil { + return fmt.Errorf("failed to remove stale incomplete directory: %w", err) + } + + err = os.Remove(markerPath) + if err != nil { + return fmt.Errorf("failed to remove stale incomplete marker: %w", err) + } + + return nil +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asdf-0.20.0/internal/versions/versions_test.go new/asdf-0.20.1/internal/versions/versions_test.go --- old/asdf-0.20.0/internal/versions/versions_test.go 2026-07-07 16:04:51.000000000 +0200 +++ new/asdf-0.20.1/internal/versions/versions_test.go 2026-09-22 04:14:48.000000000 +0200 @@ -1,13 +1,17 @@ package versions import ( + "errors" "fmt" + "io" + "io/fs" "os" "path/filepath" "strings" "testing" "github.com/asdf-vm/asdf/internal/config" + "github.com/asdf-vm/asdf/internal/installs" "github.com/asdf-vm/asdf/internal/plugins" "github.com/asdf-vm/asdf/internal/repotest" "github.com/asdf-vm/asdf/internal/toolversions" @@ -319,6 +323,29 @@ // no-download install script prints 'install' assert.Equal(t, "install", stdout.String()) }) + + t.Run("cleans up stale incomplete directory from previous failed install", func(t *testing.T) { + conf, plugin := generateConfig(t) + stdout, stderr := buildOutputs() + + version := toolversions.Version{Type: "version", Value: "1.0.0"} + installPath := installs.InstallPath(conf, plugin, version) + err := os.MkdirAll(installPath, 0o777) + assert.Nil(t, err) + + markerPath := installs.IncompleteMarkerPath(conf, plugin, version) + err = os.MkdirAll(filepath.Dir(markerPath), 0o755) + assert.Nil(t, err) + err = os.WriteFile(markerPath, []byte{}, 0o644) + assert.Nil(t, err) + + err = InstallOneVersion(conf, plugin, "1.0.0", false, &stdout, &stderr) + assert.Nil(t, err) + + _, err = os.Stat(markerPath) + assert.True(t, errors.Is(err, fs.ErrNotExist), "incomplete marker should not exist after successful retry") + assertVersionInstalled(t, conf.DataDir, plugin.Name, "1.0.0") + }) } func TestLatest(t *testing.T) { @@ -334,7 +361,7 @@ assert.Nil(t, err) plugin := plugins.New(conf, pluginName) - version, err := Latest(plugin, "") + version, err := Latest(plugin, "", io.Discard) assert.Nil(t, err) assert.Equal(t, "2.0.0", version) }) @@ -350,25 +377,25 @@ err = os.WriteFile(latestScript, []byte("#!/usr/bin/env bash\necho 1.2.3-dev"), 0o777) assert.Nil(t, err) - version, err := Latest(plugin, "") + version, err := Latest(plugin, "", io.Discard) assert.Nil(t, err) assert.Equal(t, "1.2.3-dev", version) }) t.Run("when given query matching no versions return empty slice of versions", func(t *testing.T) { - version, err := Latest(plugin, "impossible-to-satisfy-query") + version, err := Latest(plugin, "impossible-to-satisfy-query", io.Discard) assert.Error(t, err, "no latest version found") assert.Equal(t, version, "") }) t.Run("when given no query returns latest version of plugin", func(t *testing.T) { - version, err := Latest(plugin, "") + version, err := Latest(plugin, "", io.Discard) assert.Nil(t, err) assert.Equal(t, "5.1.0", version) }) t.Run("when given no query returns latest version of plugin", func(t *testing.T) { - version, err := Latest(plugin, "4") + version, err := Latest(plugin, "4", io.Discard) assert.Nil(t, err) assert.Equal(t, "4.0.0", version) }) @@ -387,7 +414,7 @@ err = os.Remove(latestScript) assert.Nil(t, err) - version, err := Latest(plugin, "") + version, err := Latest(plugin, "", io.Discard) assert.Nil(t, err) assert.Equal(t, "3.4.5", version) }) @@ -424,7 +451,7 @@ assert.Nil(t, err) plugin := plugins.New(conf, pluginName) - version, err := Latest(plugin, "") + version, err := Latest(plugin, "", io.Discard) assert.Nil(t, err) assert.Equal(t, tt.expectedOutput, version) } @@ -438,7 +465,7 @@ plugin := plugins.New(conf, pluginName) t.Run("returns slice of available versions from plugin", func(t *testing.T) { - versions, err := AllVersions(plugin) + versions, err := AllVersions(plugin, io.Discard) assert.Nil(t, err) assert.Equal(t, versions, []string{"1.0.0", "1.1.0", "2.0.0"}) }) @@ -449,10 +476,24 @@ assert.Nil(t, err) plugin := plugins.New(conf, pluginName) - versions, err := AllVersions(plugin) + versions, err := AllVersions(plugin, io.Discard) assert.Equal(t, err.(plugins.NoCallbackError).Error(), "Plugin named list-all-fail does not have a callback named list-all") assert.Empty(t, versions) }) + + t.Run("returns error when callback fails", func(t *testing.T) { + pluginName = "list-all-stderr" + _, err := repotest.InstallPlugin("dummy_broken_plugin", conf.DataDir, pluginName) + assert.Nil(t, err) + plugin := plugins.New(conf, pluginName) + + var stdErr strings.Builder + + versions, err := AllVersions(plugin, &stdErr) + assert.Equal(t, "exit status 1", err.Error()) + assert.Empty(t, versions) + assert.Equal(t, "List-all failed!\n", stdErr.String()) + }) } func TestUninstall(t *testing.T) { @@ -577,3 +618,105 @@ err := os.WriteFile(filepath.Join(dir, ".tool-versions"), []byte(contents), 0o666) assert.Nil(t, err) } + +func TestCleanupStaleIncomplete(t *testing.T) { + conf, plugin := generateConfig(t) + + t.Run("removes directory when incomplete marker exists", func(t *testing.T) { + version := toolversions.Version{Type: "version", Value: "5.0.0"} + installPath := installs.InstallPath(conf, plugin, version) + err := os.MkdirAll(installPath, 0o777) + assert.Nil(t, err) + + err = markIncomplete(conf, plugin, version) + assert.Nil(t, err) + + err = cleanupStaleIncomplete(conf, plugin, version) + assert.Nil(t, err) + + _, err = os.Stat(installPath) + assert.True(t, errors.Is(err, fs.ErrNotExist), "directory should be removed") + }) + + t.Run("does nothing when directory does not exist", func(t *testing.T) { + version := toolversions.Version{Type: "version", Value: "6.0.0"} + err := cleanupStaleIncomplete(conf, plugin, version) + assert.Nil(t, err) + }) + + t.Run("does nothing when directory exists without incomplete marker", func(t *testing.T) { + version := toolversions.Version{Type: "version", Value: "7.0.0"} + installPath := installs.InstallPath(conf, plugin, version) + err := os.MkdirAll(installPath, 0o777) + assert.Nil(t, err) + + err = cleanupStaleIncomplete(conf, plugin, version) + assert.Nil(t, err) + + _, err = os.Stat(installPath) + assert.Nil(t, err, "directory should still exist") + }) + + t.Run("removes marker when install directory was never created", func(t *testing.T) { + version := toolversions.Version{Type: "version", Value: "8.0.0"} + + err := markIncomplete(conf, plugin, version) + assert.Nil(t, err) + + err = cleanupStaleIncomplete(conf, plugin, version) + assert.Nil(t, err, "should not error when install directory is missing") + + markerPath := installs.IncompleteMarkerPath(conf, plugin, version) + _, err = os.Stat(markerPath) + assert.True(t, errors.Is(err, fs.ErrNotExist), "marker should be removed") + }) +} + +func TestMarkIncomplete(t *testing.T) { + conf, plugin := generateConfig(t) + + t.Run("creates marker file in install-locks directory", func(t *testing.T) { + version := toolversions.Version{Type: "version", Value: "1.0.0"} + err := markIncomplete(conf, plugin, version) + assert.Nil(t, err) + + markerPath := installs.IncompleteMarkerPath(conf, plugin, version) + _, err = os.Stat(markerPath) + assert.Nil(t, err, "marker file should exist") + }) + + t.Run("creates marker file even when directory does not exist", func(t *testing.T) { + version := toolversions.Version{Type: "version", Value: "2.0.0"} + err := markIncomplete(conf, plugin, version) + assert.Nil(t, err) + + markerPath := installs.IncompleteMarkerPath(conf, plugin, version) + _, err = os.Stat(markerPath) + assert.Nil(t, err, "marker file should exist") + }) +} + +func TestMarkComplete(t *testing.T) { + conf, plugin := generateConfig(t) + + t.Run("removes marker file from install-locks directory", func(t *testing.T) { + version := toolversions.Version{Type: "version", Value: "1.0.0"} + markerPath := installs.IncompleteMarkerPath(conf, plugin, version) + err := os.MkdirAll(filepath.Dir(markerPath), 0o755) + assert.Nil(t, err) + err = os.WriteFile(markerPath, []byte{}, 0o644) + assert.Nil(t, err) + + err = markComplete(conf, plugin, version) + assert.Nil(t, err) + + _, err = os.Stat(markerPath) + assert.True(t, errors.Is(err, fs.ErrNotExist), "marker file should not exist") + }) + + t.Run("returns error when marker file does not exist", func(t *testing.T) { + version := toolversions.Version{Type: "version", Value: "2.0.0"} + err := markComplete(conf, plugin, version) + assert.NotNil(t, err) + }) +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asdf-0.20.0/test/set_command.bats new/asdf-0.20.1/test/set_command.bats --- old/asdf-0.20.0/test/set_command.bats 1970-01-01 01:00:00.000000000 +0100 +++ new/asdf-0.20.1/test/set_command.bats 2026-09-22 04:14:48.000000000 +0200 @@ -0,0 +1,116 @@ +#!/usr/bin/env bats + +load test_helpers + +setup() { + setup_asdf_dir + install_dummy_plugin + install_dummy_version "1.0.0" + install_dummy_version "1.1.0" + + PROJECT_DIR="$HOME/project" + mkdir -p "$PROJECT_DIR" + + cd "$PROJECT_DIR" || exit +} + +teardown() { + clean_asdf_dir +} + +@test "set should emit an error when called with incorrect arity" { + run asdf set + [ "$status" -eq 1 ] + [ "$output" = "tool and version must be provided as arguments" ] +} + +@test "set should emit an error when version is not provided" { + run asdf set "dummy" + [ "$status" -eq 1 ] + [ "$output" = "version must be provided as an argument" ] +} + +@test "set should create .tool-versions file in current directory" { + run asdf set "dummy" "1.0.0" + [ "$status" -eq 0 ] + [ -f "$PROJECT_DIR/.tool-versions" ] + run cat "$PROJECT_DIR/.tool-versions" + [ "$output" = "dummy 1.0.0" ] +} + +@test "set should create .tool-versions file in home directory when --home flag is used" { + run asdf set --home "dummy" "1.0.0" + [ "$status" -eq 0 ] + [ -f "$HOME/.tool-versions" ] + run cat "$HOME/.tool-versions" + [ "$output" = "dummy 1.0.0" ] +} + +@test "set -u should be an alias for --home" { + run asdf set -u "dummy" "1.0.0" + [ "$status" -eq 0 ] + [ -f "$HOME/.tool-versions" ] + run cat "$HOME/.tool-versions" + [ "$output" = "dummy 1.0.0" ] +} + +@test "set should update parent directory .tool-versions when --parent flag is used" { + echo "dummy 1.0.0" >"$PROJECT_DIR/.tool-versions" + + CHILD_DIR="$PROJECT_DIR/child" + mkdir -p "$CHILD_DIR" + cd "$CHILD_DIR" + + run asdf set --parent "dummy" "1.1.0" + [ "$status" -eq 0 ] + [ -f "$PROJECT_DIR/.tool-versions" ] + [ ! -f "$CHILD_DIR/.tool-versions" ] + run cat "$PROJECT_DIR/.tool-versions" + [ "$output" = "dummy 1.1.0" ] +} + +@test "set -p should be an alias for --parent" { + echo "dummy 1.0.0" >"$PROJECT_DIR/.tool-versions" + + CHILD_DIR="$PROJECT_DIR/child" + mkdir -p "$CHILD_DIR" + cd "$CHILD_DIR" + + run asdf set -p "dummy" "1.1.0" + [ "$status" -eq 0 ] + [ -f "$PROJECT_DIR/.tool-versions" ] + [ ! -f "$CHILD_DIR/.tool-versions" ] + run cat "$PROJECT_DIR/.tool-versions" + [ "$output" = "dummy 1.1.0" ] +} + +@test "set should emit an error when both --home and --parent flags are used" { + run asdf set --home --parent "dummy" "1.0.0" + [ "$status" -eq 1 ] + [ "$output" = "home and parent flags cannot both be specified; must be one location or the other" ] +} + +@test "set should emit an error when --parent is used but no .tool-versions file exists in parent directory" { + CHILD_DIR="$PROJECT_DIR/child" + mkdir -p "$CHILD_DIR" + cd "$CHILD_DIR" + + run asdf set --parent "dummy" "1.0.0" + [ "$status" -eq 1 ] + [[ "$output" == *"No .tool-versions version file found in parent directory"* ]] +} + +@test "set with -h flag should show command help" { + run asdf set -h + [ "$status" -eq 0 ] + [[ "$output" == *"USAGE:"* ]] + [[ "$output" == *"asdf set [options]"* ]] +} + +@test "set should support multiple versions" { + run asdf set "dummy" "1.0.0" "1.1.0" + [ "$status" -eq 0 ] + [ -f "$PROJECT_DIR/.tool-versions" ] + run cat "$PROJECT_DIR/.tool-versions" + [ "$output" = "dummy 1.0.0 1.1.0" ] +} ++++++ asdf.obsinfo ++++++ --- /var/tmp/diff_new_pack.0MGjRS/_old 2026-09-22 15:54:28.044331482 +0200 +++ /var/tmp/diff_new_pack.0MGjRS/_new 2026-09-22 15:54:28.048331647 +0200 @@ -1,5 +1,5 @@ name: asdf -version: 0.20.0 -mtime: 1783433091 -commit: 150aaf051b3b88ac9ad73136d7e629bbdf332bd6 +version: 0.20.1 +mtime: 1790043288 +commit: 8fa9e668c12a11fb078f7e08c2d854e38519a7a4 ++++++ vendor.tar.gz ++++++ ++++ 39240 lines of diff (skipped)
