Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .DS_Store
Binary file not shown.
17 changes: 17 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/).

See [RELEASING.md](RELEASING.md) for the release process.

## [1.3.0] - 2026-07-23

### Added

- `gvm use` / `gvm install` without a version resolve from the nearest `.go-version` or `go.mod` (`toolchain` / `go` directive), mapping language versions like `1.22` to the latest patch (#10 / moovweb/gvm#523).
- Shared project version helpers; `gvm applymod` uses the same go.mod parsing and patch mapping.

### Fixed

- Directory walk for `.go-version` / `.go-pkgset` / `go.mod` no longer stops at the current directory only, and no longer mutates the caller's working directory.
- Comment tests: write `go.mod` with single-line `printf` so `tf` does not hang on multi-line quotes; silence expected alias cleanup errors.
- `.go-version` stays authoritative (no go.mod fallback after a parsed pin); accept plain `1.x.y` pins; bound remote tag listing via git HTTP low-speed abort.

### Changed

- Document empty-string contract for `_bash_pseudo_hash` (no distinct nil values).

## [1.2.1] - 2026-07-19

### Fixed
Expand Down
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# gvm2

> **Status: stable for daily use (v1.2.0)**
> **Status: stable for daily use (v1.3.0)**
>
> gvm2 is a community reboot of the Go Version Manager. Please file bugs and
> contributions against this repository. Production use is supported for the
Expand All @@ -21,14 +21,25 @@ gvm use go1.22.12 --default
go version
```

In a module directory you can omit the version — gvm2 reads `.go-version` or
`go.mod` (`toolchain` / `go` line) and maps language versions like `go 1.22` to
the latest matching patch:

```bash
gvm install -B # install version from go.mod / .go-version
gvm use # select matching installed version
```

Common commands (nvm-style workflow):

| Goal | Command |
|------|---------|
| List available versions | `gvm listall` |
| Latest stable name only | `gvm listall --latest` |
| Install a version | `gvm install go1.22.12` |
| Install from project files | `gvm install` (or `gvm install -B`) |
| Switch version | `gvm use go1.22.12` |
| Switch from project files | `gvm use` |
| List installed versions | `gvm list` |
| Per-directory auto-switch | commit a `.go-version` (and optional `.go-pkgset`) |

Expand Down Expand Up @@ -63,7 +74,7 @@ triage groups the backlog into these priorities:
| P0 | Shell integration (`cd`, PATH, zsh) | [#527](https://github.com/moovweb/gvm/issues/527), [#528](https://github.com/moovweb/gvm/issues/528), [#515](https://github.com/moovweb/gvm/issues/515) | Fixed in 1.1.0 |
| P1 | Install / bootstrap | [#530](https://github.com/moovweb/gvm/issues/530), [#480](https://github.com/moovweb/gvm/issues/480) | Fixed in 1.1.0 |
| P2 | UX / docs / ergonomics | [#517](https://github.com/moovweb/gvm/issues/517), [#516](https://github.com/moovweb/gvm/issues/516) | Fixed in 1.2.0 ([#4](https://github.com/brianrobt/gvm2/issues/4)) |
| P3 | Features (progress bars, worktrees, auto-detect `go.mod`) | [#514](https://github.com/moovweb/gvm/issues/514), [#523](https://github.com/moovweb/gvm/issues/523) | Backlog |
| P3 | Features (progress bars, worktrees, auto-detect `go.mod`) | [#514](https://github.com/moovweb/gvm/issues/514), [#523](https://github.com/moovweb/gvm/issues/523) | `go.mod` auto-detect in 1.3.0 ([#10](https://github.com/brianrobt/gvm2/issues/10)); progress / worktrees still backlog |

Tracked here: [brianrobt/gvm2/issues](https://github.com/brianrobt/gvm2/issues).

Expand All @@ -72,6 +83,7 @@ Pull requests and other contributions are very much appreciated.
## Features

* Install/Uninstall Go versions with `gvm install [tag]` where tag is "go1.22.12", "go1", "weekly.2011-11-08", or "tip"
* Omit the version on `gvm install` / `gvm use` to resolve from `.go-version` or `go.mod`
* List available versions with `gvm listall` (annotations + `--latest` / `--porcelain`)
* List added/removed files in GOROOT with `gvm diff`
* Manage GOPATHs with `gvm pkgset [create/use/delete] [name]`. Use `--local` as `name` to manage repository under local path (`/path/to/repo/.gvm_local`).
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.2.1
1.3.0
13 changes: 13 additions & 0 deletions scripts/ci-smoke.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,19 @@ if grep -q 'global:$GOPATH' "$GVM_DEST/gvm/environments/go1.22.12@global"; then
echo "FAIL: go1.22.12@global has duplicated global GOPATH prefix" >&2
exit 1
fi
# Auto-detect from go.mod (#10)
moddir="$GVM_DEST/mod-detect"
mkdir -p "$moddir/subdir"
cat > "$moddir/go.mod" <<'EOF'
module example.com/detect

go 1.22
EOF
(
cd "$moddir/subdir"
gvm use
go version | grep -E 'go1\.22\.'
)
spaced="$GVM_DEST/dir with spaces"
mkdir -p "$spaced"
cd "$spaced"
Expand Down
36 changes: 20 additions & 16 deletions scripts/env/applymod
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,33 @@
. "$GVM_ROOT/scripts/functions"

function gvm_applymod(){
local mod_path=""
local hint=""
local version=""

mod_file=go.mod
if [ ! -f $mod_file ]; then
if ! mod_path="$(__gvm_find_file_upwards "go.mod")"; then
return $(display_error "can not find go.mod")
fi

mod_ver=$(cat go.mod | grep -E 'go [[:digit:]]+.[[:digit:]]+' | tr -d ' ' | tr -d '\n')
mod_name=$(cat go.mod | grep 'module ' | tr -d '\n')
display_message "$mod_name use go version: $mod_ver"
hint="$(__gvm_parse_go_mod_version "${mod_path}")" || hint=""
[[ -n "${hint}" ]] || return $(display_error "can not parse go version from go.mod")

if [ $(gvm list | grep $mod_ver | wc -l | tr -d '\n') -lt 1 ]; then
if [ $(gvm listall -a| grep $mod_ver | wc -l | tr -d '\n') -lt 1 ]; then
return $(display_error "can not find a go version match $mod_ver")
fi
# Prefer an already-installed match; otherwise resolve the latest available patch.
version="$(__gvm_map_version_hint_to_tag "${hint}" installed)" || version=""
if [[ -z "${version}" ]]; then
version="$(__gvm_map_version_hint_to_tag "${hint}" available)" || version=""
fi
[[ -n "${version}" ]] || return $(display_error "can not find a go version match ${hint}")

display_message "$mod_ver does not exist, try to install..."
$GVM_ROOT/scripts/install $mod_ver
display_message "go.mod requests ${hint}; using ${version}"

if [ $? -ne 0 ]; then
return $(display_error "gvm install $mod_ver error")
if [[ ! -d "${GVM_ROOT}/gos/${version}" ]]; then
display_message "${version} does not exist, try to install..."
"$GVM_ROOT/scripts/install" "${version}"
if [[ $? -ne 0 ]]; then
return $(display_error "gvm install ${version} error")
fi
fi

gvm_use "$mod_ver"

}
gvm_use "${version}"
}
19 changes: 15 additions & 4 deletions scripts/env/use
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
# <pre>@textblock</pre>
# Usage: gvm use [--version] <value> [--pkgset <value>] [--default]
# gvm use <version-name>@<pkgset-name> [--default]
# gvm use [--pkgset <value>] [--default] # auto-detect from .go-version / go.mod
#
# Options:
# --version value version name
Expand All @@ -22,7 +23,8 @@
# --quiet suppress 'now using' acknowledgements
# -h, --help show this message
#
# When using --pkgset, you must also specify a go version name.
# With no version, resolve from the nearest .go-version (preferred) or go.mod.
# When using --pkgset, you must also specify a go version name (or auto-detect).
# @/textblock</pre>
# @param args Variable list of options and values
# @return Returns success (status 0) if a pkgset was selected successfully or
Expand Down Expand Up @@ -138,6 +140,7 @@ gvm_use() {
then
echo "Usage: gvm use [--version] <value> [--pkgset <value>] [--default]"
echo " gvm use <version-name>@<pkgset-name> [--default]"
echo " gvm use [--pkgset <value>] [--default]"
echo
echo "Options:"
echo " --version value go version name or alias"
Expand All @@ -146,7 +149,9 @@ gvm_use() {
echo " --quiet suppress 'now using' acknowledgements"
echo " -h, --help show this message"
echo
echo "When using --pkgset, you must also specify a go version name."
echo "With no version, resolve from the nearest .go-version or go.mod"
echo "(toolchain / go directive; language versions map to the latest"
echo "installed patch for that minor)."
echo
return 0
else
Expand Down Expand Up @@ -194,8 +199,14 @@ gvm_use() {

if [[ -z "${version}" ]]
then
display_error "Please specify the version. Execute 'gvm use --help' for command help."
return 1
# Bare `gvm use`: resolve from .go-version / go.mod (#10 / moovweb/gvm#523).
version="$(__gvm_resolve_project_go_version installed)" || version=""
if [[ -z "${version}" ]]
then
display_error "Please specify the version (or add .go-version / go.mod). Execute 'gvm use --help' for command help."
return 1
fi
[[ "${GVM_DEBUG}" -eq 1 ]] && echo "Auto-detected version: ${version}"
fi

# Exact match first, then prefix match — avoid fragile multi-grep pipelines under `set -e`.
Expand Down
6 changes: 4 additions & 2 deletions scripts/function/_bash_pseudo_hash
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ setValueForKeyFakeAssocArray() {
fi
# parameter list supports empty arguments!
local target_key="$1"; shift
local new_value="$1"; shift # @todo: need to support setting nil values!
# Empty values are stored as empty strings (no distinct nil). Callers use -n checks.
local new_value="$1"; shift
local target_ary; target_ary=()
local defaultIFS="$IFS"
local IFS="$defaultIFS"
Expand Down Expand Up @@ -137,7 +138,8 @@ valueForKeyFakeAssocArray() {
local _item
for _item in "${target_ary[@]}"; do
if [[ "${_item%%:*}" == "${target_key}" ]]; then
value="$(_decode "${_item#*:}")" # @todo: need to support returning nil values!
# Missing keys and empty values both surface as "" (no distinct nil).
value="$(_decode "${_item#*:}")"
break
fi
done
Expand Down
50 changes: 8 additions & 42 deletions scripts/function/find_path_upwards
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
#!/usr/bin/env bash

. "$GVM_ROOT/scripts/function/_shell_compat" || return 1
# Canonical walker lives here; load eagerly so cd / use share one implementation.
. "$GVM_ROOT/scripts/function/resolve_project_version" || return 1

# __gvm_find_path_upwards()
# /*!
# @abstract Search upwards through the directory tree for a target file or
Expand Down Expand Up @@ -30,52 +34,14 @@ __gvm_find_path_upwards() {
echo "" && return 1
fi

local found_file="$(__gvmp_find_current_path_for_target "${target}")"
echo "${found_file}"

if [[ -z "${found_file}" ]]; then
return 1
fi

return 0
__gvm_find_file_upwards "${target}" "${start_dir}" "${final_dir}"
}

# Compatibility alias for older call sites / tests.
__gvmp_find_path_for_target() {
local target="${1}"
local start_dir="${2}"
local final_dir="${3}"

if [[ ${#target} -eq 0 ]]; then
echo ""; return 1
fi

# resolve tilde for HOME
start_dir="${start_dir/#\~/$HOME}"

# resolve dot for PWD
if [[ "${start_dir}" == "." ]]; then
start_dir="$PWD"
fi

local current_dir="${start_dir}"
local highest_dir="${final_dir}"

builtin cd "${current_dir}"
while [[ "${current_dir}" != "${highest_dir}" && "${current_dir}" != "/" ]]; do
if [[ -f "${current_dir}/${target}" || -d "${current_dir}/${target}" ]]; then
echo "${current_dir}/${target}"; return 0
break
fi

builtin cd ..
current_dir="$(pwd)"
done

echo "";
return 1
__gvm_find_file_upwards "$@"
}


__gvmp_find_current_path_for_target() {
local target="${1}"
local start_dir="$PWD"
Expand All @@ -94,4 +60,4 @@ __gvmp_find_current_path_for_target() {

echo "";
return 1
}
}
Loading
Loading