Skip to content

fix(init): detect Git Bash on Windows instead of assuming PowerShell - #113

Merged
timvw merged 1 commit into
mainfrom
fix/git-bash-shell-detection
Aug 10, 2026
Merged

fix(init): detect Git Bash on Windows instead of assuming PowerShell#113
timvw merged 1 commit into
mainfrom
fix/git-bash-shell-detection

Conversation

@timvw

@timvw timvw commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Fixes #112.

Problem

wt co printed wt navigating to: ... but the shell stayed put, and wt init from Git Bash installed a PowerShell script.

Two places decided the target shell from runtime.GOOS alone, so Windows always resolved to PowerShell:

  • detectShell (cmd/init.go) — short-circuited to powershell before the $SHELL checks, so wt init under Git Bash wrote to Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1. The bash session never loads that, so the wt wrapper function was never defined and there was nothing to perform the cd.
  • shellenvTargetShell (cmd/shellenv.go) — had the identical short-circuit. This is why passing the shell explicitly did not help: the installed bash block was eval "$(wt shellenv)", and with no argument shellenv re-ran the same GOOS-only detection at every shell startup and emitted PowerShell into bash.

Changes

Detection — a shared isPOSIXShellEnv() gates the Windows→PowerShell default. Git Bash and MSYS2 export MSYSTEM; Cygwin does not, but all of them set $SHELL to a Unix shell path, which neither PowerShell nor cmd does. $SHELL is matched on its basename against a known set (sh, bash, zsh, fish, dash, ash, ksh, with an optional .exe) — a substring match would have read powershell as a POSIX shell, since it ends in shell. Explicit arguments still win, so wt init powershell from Git Bash works.

Config blockgetShellConfigContent now writes the resolved shell into the block: eval "$(wt shellenv bash)", Invoke-Expression (& wt shellenv powershell). The installed line no longer depends on startup-time detection. Existing blocks are rewritten in place on the next wt init (marker handling is unchanged, and removal of old-format blocks still works).

script(1) fallback — Git for Windows does not ship script, so the bash wrapper would fail there even with detection fixed. Added a command -v script guard; without it, stdout is redirected to the log file and replayed with cat. Deliberately not tee: a pipeline makes $? tee's status, and PIPESTATUS/pipestatus are clobbered by the very next command — including the test needed to choose between bash's and zsh's spelling of it. I hit exactly that while writing this (bash returned 1 on success, zsh returned 0 on failure); the redirect form has no such trap. stderr is left alone so errors still stream live. Interactive promptui menus need a TTY and won't render in this mode — documented, and wt co <branch> works fine.

Path translationcygpath -u is applied to the navigation target when available, so native C:\... paths become the POSIX form cd understands. No-op everywhere else, since cygpath only exists on MSYS/Cygwin.

Also refactored detectShell to take an injected goos, matching the pattern shellenvTargetShell already used, so the Windows branches are testable off-Windows.

Testing

  • New table cases in TestDetectShell and TestShellenvTargetShell cover Git Bash via MSYSTEM, via Unix $SHELL, the .exe suffix, explicit-argument override, and the powershell/bash-named-directory false-positive guards. Env vars are set unconditionally via t.Setenv so an ambient MSYSTEM on a Windows runner cannot leak in.
  • Verified the generated script parses under both bash -n and zsh -n.
  • Exercised the no-script(1) branch against a stub binary on a stripped PATH in both bash and zsh: auto-cd works and exit codes propagate (confirming the PIPESTATUS fix).
  • go test ./... and all 292 e2e scenarios pass. Two TestBashTabCompletion* tests fail on this machine, but they fail identically on unmodified main (macOS bash 3.2) — pre-existing and unrelated.

Not verified

I have no Windows machine, so the Git Bash path is reasoned from the code and tested by proxy (stubbed PATH, injected goos) rather than run for real. The three assumptions worth a second pair of eyes: that Git for Windows genuinely lacks script(1), that cygpath is on PATH in a Git Bash session, and that $SHELL/MSYSTEM are exported to the wt.exe child process. @celluj34 — if you're able to try this branch, that would confirm it.

Both `wt init` and `wt shellenv` decided the target shell from GOOS alone,
so on Windows they always chose PowerShell. Under Git Bash / MSYS2 that
means `wt init` writes the integration into the PowerShell $PROFILE, which
the bash session never loads — leaving `wt co` with no wrapper function, so
it prints "wt navigating to: ..." and the shell never changes directory.

Naming the shell explicitly did not help either: the installed bash block
was `eval "$(wt shellenv)"`, and shellenv re-ran the same GOOS-only
detection at every startup, evaluating PowerShell code inside bash.

- Detect POSIX shell environments on Windows via MSYSTEM and $SHELL, and
  only fall back to PowerShell when neither indicates one. $SHELL is matched
  on its basename so "powershell" is not read as a POSIX shell.
- Write the resolved shell into the config block (`wt shellenv bash`), so
  the installed line no longer depends on startup-time detection.
- Fall back to a non-PTY mode when script(1) is absent, as it is in Git
  Bash. stdout is redirected and replayed rather than piped through tee,
  since a pipeline would report tee's exit status and PIPESTATUS is
  clobbered by the next command run.
- Translate native Windows paths through cygpath before cd, when available.

Fixes #112
@codecov

codecov Bot commented Aug 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 42.50000% with 23 lines in your changes missing coverage. Please review.
✅ Project coverage is 32.32%. Comparing base (22d83db) to head (cc218f5).

Files with missing lines Patch % Lines
cmd/shellenv.go 4.34% 22 Missing ⚠️
cmd/init.go 94.11% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #113      +/-   ##
==========================================
+ Coverage   32.11%   32.32%   +0.20%     
==========================================
  Files          27       27              
  Lines        3036     3066      +30     
==========================================
+ Hits          975      991      +16     
- Misses       1975     1989      +14     
  Partials       86       86              
Files with missing lines Coverage Δ
cmd/examples.go 67.74% <ø> (ø)
cmd/init.go 49.55% <94.11%> (+3.54%) ⬆️
cmd/shellenv.go 31.69% <4.34%> (-1.97%) ⬇️

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@timvw
timvw merged commit 90726cb into main Aug 10, 2026
14 checks passed
@timvw
timvw deleted the fix/git-bash-shell-detection branch August 10, 2026 09:33
timvw added a commit that referenced this pull request Aug 10, 2026
wt spawns hook commands itself, so the shell the user is sitting in never
had a say in what ran them: on Windows every hook went through `cmd /c`.
That breaks essentially every hook we document, since `cmd` expands
neither `$WT_PATH` nor provides `test`, `cp` or `&&`. Post-hooks warned to
stderr and reported success, so a Git Bash user's .env was silently not
copied; pre-hooks aborted the operation naming a command that looks
perfectly valid.

Reuse isPOSIXShellEnv() from #113: on Windows, use `sh -c` only when we
can see a POSIX environment and find an sh to run it with, otherwise keep
`cmd /c`. PowerShell and cmd users who wrote %WT_PATH% hooks are
unaffected.

Choosing sh is not enough on its own — the hook vars hold native paths,
and `cd $WT_PATH` in bash eats the backslashes as escapes. WT_PATH and
WT_MAIN are therefore converted to mixed form (C:\a\b -> C:/a/b) when the
hook shell is POSIX. Mixed form over /c/a/b so it also works under Cygwin
(which mounts drives at /cygdrive) and survives being passed to a native
tool the hook invokes, e.g. `code $WT_PATH`.

The five hooks.yaml scenarios skipped for this exact cause are enabled,
with their assertions moved to $WORKTREE_ROOT_POSIX so they read paths in
the harness shell's own form.

Fixes #118
timvw added a commit that referenced this pull request Aug 10, 2026
…#120)

wt spawns hook commands itself, so the shell the user is sitting in never
had a say in what ran them: on Windows every hook went through `cmd /c`.
That breaks essentially every hook we document, since `cmd` expands
neither `$WT_PATH` nor provides `test`, `cp` or `&&`. Post-hooks warned to
stderr and reported success, so a Git Bash user's .env was silently not
copied; pre-hooks aborted the operation naming a command that looks
perfectly valid.

Reuse isPOSIXShellEnv() from #113: on Windows, use `sh -c` only when we
can see a POSIX environment and find an sh to run it with, otherwise keep
`cmd /c`. PowerShell and cmd users who wrote %WT_PATH% hooks are
unaffected.

Choosing sh is not enough on its own — the hook vars hold native paths,
and `cd $WT_PATH` in bash eats the backslashes as escapes. WT_PATH and
WT_MAIN are therefore converted to mixed form (C:\a\b -> C:/a/b) when the
hook shell is POSIX. Mixed form over /c/a/b so it also works under Cygwin
(which mounts drives at /cygdrive) and survives being passed to a native
tool the hook invokes, e.g. `code $WT_PATH`.

The five hooks.yaml scenarios skipped for this exact cause are enabled,
with their assertions moved to $WORKTREE_ROOT_POSIX so they read paths in
the harness shell's own form.

Fixes #118
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

wt co not actually changing directory

1 participant