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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this
### Fixed

- Project resolution skips `.tool-versions` files that only pin other languages, preserving `go.mod` and parent Go pins. Bash/Zsh auto-switching tracks both the unrelated manifest and the selected Go manifest so edits in the same directory invalidate the prompt cache. Incomplete explicit Go entries still fail closed, and the first `go`/`golang` entry decides (a bare entry no longer lets a later line win). `.tool-versions` fields are no longer pathname-expanded, so `golang *` cannot select a version from file names in the working directory.
- Output and error messages that gos captures from file operations (with or without a privilege-escalation retry) keep their trailing newline when replayed, so the next progress or error line is no longer glued onto the command's last line (for example `...: Permission deniedError: ...`).
- Windows uninstall validates ownership, removes only known installed files, and preserves unrelated contents instead of recursively deleting the target directory. Existing installations without a receipt remain supported, with their ambiguous `LICENSE` left in place.
- Windows installs stage replacements on the destination filesystem and restore previous files on publication failure. Failed fresh installs remove partial payloads; failed restoration keeps recovery files and reports their location.
- Windows PATH edits refresh a stale process even when the registry already contains gos, remove process entries on uninstall, and preserve registry value types and unrelated entries. Tests use registry substitutes and a disposable native Windows key.
Expand Down
38 changes: 18 additions & 20 deletions gos.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1178,6 +1178,19 @@ _gos_sudo_for() {
GOS_SUDO_TARGET="$target" _gos_sudo_for_target "$@"
}

# Replay captured stdout/stderr of a command run by _gos_sudo_for_target.
# Command substitution strips trailing newlines, so restore one per non-empty
# stream: otherwise the next progress or error line is glued onto the
# command's last line (e.g. "...: Permission deniedError: ...").
_gos_sudo_replay() {
if [ -n "$1" ]; then
printf '%s\n' "$1"
fi
if [ -n "$2" ]; then
printf '%s\n' "$2" >&2
fi
}

_gos_sudo_for_target() {
local output status err err_file sudo_output sudo_status sudo_err

Expand All @@ -1202,12 +1215,7 @@ _gos_sudo_for_target() {

if [ "$status" -eq 0 ]; then
rm -f "$err_file"
if [ -n "$output" ]; then
printf '%s' "$output"
fi
if [ -n "$err" ]; then
printf '%s' "$err" >&2
fi
_gos_sudo_replay "$output" "$err"
return 0
fi

Expand All @@ -1224,28 +1232,18 @@ _gos_sudo_for_target() {
sudo_err=$(<"$err_file")
rm -f "$err_file"
if [ "$sudo_status" -eq 0 ]; then
if [ -n "$sudo_output" ]; then
printf '%s' "$sudo_output"
fi
if [ -n "$sudo_err" ]; then
printf '%s' "$sudo_err" >&2
fi
_gos_sudo_replay "$sudo_output" "$sudo_err"
return 0
fi
printf '%s' "$err" >&2
if [ -n "$sudo_err" ]; then
printf '%s' "$sudo_err" >&2
fi
_gos_sudo_replay "" "$err"
_gos_sudo_replay "" "$sudo_err"
return "$sudo_status"
;;
esac
fi

rm -f "$err_file"
if [ -n "$output" ]; then
printf '%s' "$output"
fi
printf '%s' "$err" >&2
_gos_sudo_replay "$output" "$err"
return "$status"
}

Expand Down
61 changes: 61 additions & 0 deletions tests/unit-versions.bash
Original file line number Diff line number Diff line change
Expand Up @@ -209,3 +209,64 @@ go_comparison_output="$(
[ "$go_comparison_output" = "go-version-comparison-ok" ] \
|| fail "Go version comparison failed: ${go_comparison_output}"
pass "Go version comparison orders beta, rc, release, and patch versions"

# _gos_sudo_for_target captures a command's stdout/stderr with command
# substitution, which strips trailing newlines. Replaying them must restore
# the line ending, or the next progress/error line is glued onto the
# command's last line (as seen in "...gos-backup.1234Activating go from ...").
sudo_case="${test_root}/sudo-replay"
mkdir -p "${sudo_case}/bin" "${sudo_case}/target"
cat >"${sudo_case}/bin/sudo" <<'FAKE_REPLAY_SUDO'
#!/usr/bin/env bash
GOS_FAKE_UNDER_SUDO=1 exec "$@"
FAKE_REPLAY_SUDO
cat >"${sudo_case}/bin/denied-then-ok" <<'FAKE_DENIED_THEN_OK'
#!/usr/bin/env bash
if [ "${GOS_FAKE_UNDER_SUDO:-}" != 1 ]; then
echo "denied-then-ok: Permission denied" >&2
exit 1
fi
echo "moved"
echo "sudo-warning" >&2
FAKE_DENIED_THEN_OK
cat >"${sudo_case}/bin/direct-ok" <<'FAKE_DIRECT_OK'
#!/usr/bin/env bash
echo "direct-output"
echo "direct-warning" >&2
FAKE_DIRECT_OK
cat >"${sudo_case}/bin/always-denied" <<'FAKE_ALWAYS_DENIED'
#!/usr/bin/env bash
echo "always-denied: Permission denied" >&2
exit 4
FAKE_ALWAYS_DENIED
cat >"${sudo_case}/bin/plain-fail" <<'FAKE_PLAIN_FAIL'
#!/usr/bin/env bash
echo "partial-output"
echo "plain-fail: boom" >&2
exit 3
FAKE_PLAIN_FAIL
chmod +x "${sudo_case}/bin/"*
PATH="${sudo_case}/bin:${fake_bin}:${original_path}" \
GOS_INSTALL_DIR="${sudo_case}/go" \
GOS_CACHE_DIR="${sudo_case}/cache" \
GOS_TEST_REAL_MV="$real_mv" \
bash -c '
set -euo pipefail
. "$1"
GOS_SUDO_TARGET="$2" _gos_sudo_for_target direct-ok
printf "after-direct\n"
printf "after-direct-err\n" >&2
GOS_SUDO_TARGET="$2" _gos_sudo_for_target denied-then-ok
printf "after-retry\n"
printf "after-retry-err\n" >&2
GOS_SUDO_TARGET="$2" _gos_sudo_for_target always-denied || printf "status=%s\n" "$?"
printf "after-denied-err\n" >&2
GOS_SUDO_TARGET="$2" _gos_sudo_for_target plain-fail || printf "status=%s\n" "$?"
printf "after-fail-err\n" >&2
' bash "$sourceable_script" "${sudo_case}/target" \
>"${sudo_case}/stdout" 2>"${sudo_case}/stderr" || fail "sudo replay harness failed: $(cat "${sudo_case}/stderr")"
expected_sudo_stdout="$(printf '%s\n' direct-output after-direct moved after-retry status=4 partial-output status=3)"
expected_sudo_stderr="$(printf '%s\n' direct-warning after-direct-err sudo-warning after-retry-err 'always-denied: Permission denied' 'always-denied: Permission denied' after-denied-err 'plain-fail: boom' after-fail-err)"
[ "$(cat "${sudo_case}/stdout")" = "$expected_sudo_stdout" ] || fail "sudo replay stdout lost line endings: $(cat "${sudo_case}/stdout")"
[ "$(cat "${sudo_case}/stderr")" = "$expected_sudo_stderr" ] || fail "sudo replay stderr lost line endings: $(cat "${sudo_case}/stderr")"
pass "sudo wrapper replays captured output with its line endings on every path"
Loading