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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ Commands are auto-documented with a # @doc comment on the same line as the comma
| `gsl` | Git stash list | `config/.bash_aliases` |
| `gsp` | Git stash pop | `config/.bash_aliases` |
| `gss` | Git stash save | `config/.bash_aliases` |
| `gwt` | git-worktree: gwt <add|list|rm|cd> [branch] [options] | `bin/gwt.sh` |
| `gwt` | git-worktree: gwt <add|list|rm|cd|bootstrap> [branch] [options] | `bin/gwt.sh` |
| `htmlq` | jq for HTML | htmlq <selector> < page.html | `bin/stubs.sh` |
| `httpie` | Friendly HTTP client (httpie) | http GET <url> | `bin/stubs.sh` |
| `hyperfine` | Benchmark commands head-to-head | hyperfine 'cmd1' 'cmd2' | `bin/stubs.sh` |
Expand Down
39 changes: 32 additions & 7 deletions bin/gwt.sh
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
## Git Worktree Helpers

gwt() { # @doc git-worktree: gwt <add|list|rm|cd> [branch] [options]
local usage="Usage: gwt <add|list|rm|cd> [branch] [options]"
gwt() { # @doc git-worktree: gwt <add|list|rm|cd|bootstrap> [branch] [options]
local usage="Usage: gwt <add|list|rm|cd|bootstrap> [branch] [options]"

case "${1:-}" in
add) shift; _gwt_add "$@" ;;
bootstrap)
local wt_path
wt_path="$(git rev-parse --show-toplevel 2>/dev/null)" || {
echo "Error: not in a git repository" >&2
return 1
}
_gwt_bootstrap "${wt_path}"
;;
list | ls) _gwt_list ;;
rm) shift; _gwt_rm "$@" ;;
cd) shift; _gwt_cd "$@" ;;
Expand Down Expand Up @@ -60,8 +68,20 @@ _gwt_pick_base_ref() {
}

_gwt_add() {
local branch="${1:-}"
local base_ref="${2:-}"
local add_usage="Usage: gwt add <branch> [base-ref] [-b|--bootstrap]"
local bootstrap=false
local positional=()
while (($#)); do
case "$1" in
-b | --bootstrap) bootstrap=true ;;
-h | --help) echo "${add_usage}"; return 0 ;;
-*) echo "Unknown option: $1" >&2; echo "${add_usage}" >&2; return 1 ;;
*) positional+=("$1") ;;
esac
shift
done
local branch="${positional[0]:-}"
local base_ref="${positional[1]:-}"

if [[ -z "${branch}" ]]; then
if [[ -t 0 ]]; then
Expand All @@ -71,7 +91,7 @@ _gwt_add() {
return 1
fi
else
echo "Usage: gwt add <branch> [base-ref]" >&2
echo "${add_usage}" >&2
echo "Error: branch is required when not running in a TTY." >&2
return 1
fi
Expand Down Expand Up @@ -107,7 +127,11 @@ _gwt_add() {
fi

echo ""
_gwt_bootstrap "${worktree_path}"
if [[ "${bootstrap}" == true ]]; then
_gwt_bootstrap "${worktree_path}"
else
echo "Skipped dependency bootstrap (run 'gwt bootstrap' inside the worktree if needed)."
fi

echo ""
echo "Ready: cd ${worktree_path}"
Expand All @@ -117,7 +141,8 @@ _gwt_add() {
_gwt_bootstrap() {
local wt_path="${1}"
local git_root
git_root="$(git rev-parse --show-toplevel 2>/dev/null)"
git_root="$(git -C "${wt_path}" rev-parse --path-format=absolute --git-common-dir 2>/dev/null)"
git_root="${git_root%/.git}"

echo "Bootstrapping dependencies..."

Expand Down
2 changes: 1 addition & 1 deletion config/.bash_aliases
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ vsi() { # @doc Fuzzy find files and open in Neovim (git-aware)
[[ ${#files[@]} -gt 0 ]] && nvim "${files[@]}"
}

alias praf='pre-commit run --all-files'
alias praf='prek run --all-files'

alias lg='lazygit' # @doc Open lazygit TUI
alias gitdoctor='~/projects/dotfiles/scripts/git_auth_doctor.sh' # @doc Diagnose why a GitHub push is refused (HTTPS chain or SSH): gitdoctor [repo-dir]
Expand Down
115 changes: 115 additions & 0 deletions scripts/test_gwt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
"""Tests for the gwt worktree helper in bin/gwt.sh.

`npm` is stubbed on PATH so bootstrap runs are observable and instant.
"""

import os
import subprocess
from pathlib import Path

import pytest

REPO = Path(__file__).parent.parent
SCRIPT = REPO / 'bin' / 'gwt.sh'
NPM_STUB = '#!/usr/bin/env bash\necho "npm $*" >> "${NPM_CALLS_FILE}"\n'


@pytest.fixture
def env(tmp_path: Path) -> dict[str, str]:
stub_dir = tmp_path / 'stubs'
stub_dir.mkdir()
npm = stub_dir / 'npm'
npm.write_text(NPM_STUB)
npm.chmod(0o755)
return {
**os.environ,
'PATH': f'{stub_dir}:{os.environ["PATH"]}',
'NPM_CALLS_FILE': str(tmp_path / 'npm_calls'),
'GIT_CONFIG_GLOBAL': '/dev/null',
'GIT_AUTHOR_NAME': 't',
'GIT_AUTHOR_EMAIL': 't@t',
'GIT_COMMITTER_NAME': 't',
'GIT_COMMITTER_EMAIL': 't@t',
}


@pytest.fixture
def repo(tmp_path: Path, env: dict[str, str]) -> Path:
path = tmp_path / 'proj'
path.mkdir()
(path / 'package.json').write_text('{}')
for cmd in (
['git', 'init', '-q', '-b', 'main'],
['git', 'add', '.'],
['git', 'commit', '-qm', 'init'],
):
subprocess.run(cmd, cwd=path, env=env, check=True) # noqa: S603
return path


def run_gwt(
repo: Path,
env: dict[str, str],
*args: str,
) -> subprocess.CompletedProcess[str]:
quoted = ' '.join(f"'{a}'" for a in args)
return subprocess.run( # noqa: S603
['bash', '-c', f'source "{SCRIPT}" && gwt {quoted} && pwd'], # noqa: S607
cwd=repo,
env=env,
capture_output=True,
text=True,
stdin=subprocess.DEVNULL,
check=False,
)


@pytest.mark.parametrize(
('args', 'expect_bootstrap'),
[
(('add', 'feat', 'HEAD'), False),
(('add', 'feat', 'HEAD', '--bootstrap'), True),
(('add', '-b', 'feat', 'HEAD'), True),
(('add', 'feat', '--bootstrap', 'HEAD'), True),
],
)
def test_add_bootstrap_is_opt_in(
repo: Path,
env: dict[str, str],
args: tuple[str, ...],
expect_bootstrap: bool, # noqa: FBT001
) -> None:
result = run_gwt(repo, env, *args)

worktree = repo.parent / 'proj-worktrees' / 'feat'
assert result.returncode == 0, result.stderr
assert worktree.is_dir()
assert result.stdout.strip().endswith(str(worktree))
assert Path(env['NPM_CALLS_FILE']).exists() is expect_bootstrap


def test_add_without_bootstrap_hints_at_command(
repo: Path,
env: dict[str, str],
) -> None:
result = run_gwt(repo, env, 'add', 'feat', 'HEAD')

assert 'gwt bootstrap' in result.stdout


def test_bootstrap_subcommand_runs_in_current_worktree(
repo: Path,
env: dict[str, str],
) -> None:
result = run_gwt(repo, env, 'bootstrap')

assert result.returncode == 0, result.stderr
assert Path(env['NPM_CALLS_FILE']).read_text().startswith('npm install')


def test_add_rejects_unknown_flag(repo: Path, env: dict[str, str]) -> None:
result = run_gwt(repo, env, 'add', 'feat', 'HEAD', '--nope')

assert result.returncode != 0
assert 'Unknown option' in result.stderr
assert not (repo.parent / 'proj-worktrees' / 'feat').exists()
Loading