#!/usr/bin/env bash set -euo pipefail repo_root="$(git rev-parse --show-toplevel)" module="$repo_root/nushell/modules/git.nu" tmp="$(mktemp -d /tmp/dotfiles-wt-test-XXXXXX)" trap 'rm -rf "$tmp"' EXIT fail() { echo "FAIL: $*" >&2; exit 1; } assert_dir() { [ -d "$1" ] || fail "missing dir: $1"; } assert_file() { [ -f "$1" ] || fail "missing file: $1"; } assert_no_path() { [ ! -e "$1" ] || fail "unexpected path: $1"; } run_nu() { local cwd="$1" shift DOTFILES_GIT_TEST_PHASE=nu nu --commands "use '$module' *; cd '$cwd'; $*" } expect_fail() { local cwd="$1" shift if run_nu "$cwd" "$*" >/tmp/dotfiles-wt-unexpected-success.log 2>&1; then cat /tmp/dotfiles-wt-unexpected-success.log >&2 fail "expected failure: $*" fi } expected_exports='co wt wt add wt done wt go wt list wt lock wt ls wt migrate wt move wt new wt path wt prune wt remove wt repair wt repo wt rm wt root wt unlock' actual_exports="$(perl -ne 'if (/^export def(?: --env)? (?:"([^"]+)"|([^\s\[]+))/) { print (($1 // $2) . "\n") }' "$module" | sort)" [ "$actual_exports" = "$expected_exports" ] || fail "exported command list mismatch expected: $expected_exports actual: $actual_exports" mkdir -p "$tmp/src" "$tmp/ws" git init --bare "$tmp/origin.git" >/dev/null git clone "$tmp/origin.git" "$tmp/src" >/dev/null 2>&1 git -C "$tmp/src" config user.email test@example.invalid git -C "$tmp/src" config user.name test echo root > "$tmp/src/root.txt" git -C "$tmp/src" add root.txt git -C "$tmp/src" commit -m init >/dev/null git -C "$tmp/src" branch -M trunk git -C "$tmp/src" push -u origin trunk >/dev/null 2>&1 git --git-dir="$tmp/origin.git" symbolic-ref HEAD refs/heads/trunk run_nu "$tmp/ws" "co '$tmp/origin.git' default-demo --dir '$tmp/ws' | ignore" assert_dir "$tmp/ws/default-demo/.bare" assert_dir "$tmp/ws/default-demo/trunk" git -C "$tmp/ws/default-demo/trunk" status --short --branch >/dev/null run_nu "$tmp/ws" "co '$tmp/origin.git' demo --branch trunk --dir '$tmp/ws' | ignore" assert_dir "$tmp/ws/demo/.bare" assert_dir "$tmp/ws/demo/trunk" assert_dir "$tmp/ws/demo/wt" assert_file "$tmp/ws/demo/trunk/.git" assert_no_path "$tmp/ws/demo/.git" assert_no_path "$tmp/ws/demo/demo.trunk" gitfile="$(cat "$tmp/ws/demo/trunk/.git")" [[ "$gitfile" == "gitdir: $tmp/ws/demo/.bare/worktrees/"* ]] || fail "gitfile not absolute .bare path: $gitfile" [ "$(git --git-dir="$tmp/ws/demo/.bare" config --bool core.bare)" = true ] || fail ".bare not bare" [ -z "$(git --git-dir="$tmp/ws/demo/.bare" config --get extensions.relativeWorktrees || true)" ] || fail "relativeWorktrees set" [ "$(git --git-dir="$tmp/ws/demo/.bare" config --bool worktree.useRelativePaths)" = false ] || fail "useRelativePaths not false" git -C "$tmp/ws/demo/trunk" status --short --branch >/dev/null [ "$(run_nu "$tmp/ws/demo/trunk" "wt root")" = "$tmp/ws/demo" ] || fail "wt root mismatch" [ "$(run_nu "$tmp/ws/demo/trunk" "wt repo")" = "demo" ] || fail "wt repo mismatch" [ "$(run_nu "$tmp/ws/demo/trunk" "wt path trunk")" = "$tmp/ws/demo/trunk" ] || fail "wt path trunk mismatch" run_nu "$tmp/ws/demo/trunk" "wt | ignore" run_nu "$tmp/ws/demo/trunk" "wt ls | ignore" run_nu "$tmp/ws/demo/trunk" "wt list | ignore" run_nu "$tmp/ws/demo/trunk" "wt repair" run_nu "$tmp/ws/demo/trunk" "wt migrate | ignore" expect_fail "$tmp/ws" "wt ls | ignore" expect_fail "$tmp/ws/demo/trunk" "co . | ignore" expect_fail "$tmp/ws/demo/trunk" "wt new trunk | ignore" run_nu "$tmp/ws/demo/trunk" "wt new switch/me --switch | ignore; if \$env.PWD != '$tmp/ws/demo/wt/switch-me' { error make {msg: 'wt new --switch did not cd'} }" assert_dir "$tmp/ws/demo/wt/switch-me" run_nu "$tmp/ws/demo/wt/switch-me" "wt remove switch/me --force | ignore" assert_no_path "$tmp/ws/demo/wt/switch-me" run_nu "$tmp/ws/demo/trunk" "wt go go/create --create | ignore" assert_dir "$tmp/ws/demo/wt/go-create" run_nu "$tmp/ws/demo/wt/go-create" "wt remove go/create --force | ignore" assert_no_path "$tmp/ws/demo/wt/go-create" run_nu "$tmp/ws/demo/trunk" "wt new feature/x | ignore" assert_dir "$tmp/ws/demo/wt/feature-x" git -C "$tmp/ws/demo/wt/feature-x" status --short --branch >/dev/null run_nu "$tmp/ws/demo/trunk" "wt lock feature/x" run_nu "$tmp/ws/demo/trunk" "wt unlock feature/x" run_nu "$tmp/ws/demo/trunk" "wt lock feature/x testing" run_nu "$tmp/ws/demo/trunk" "wt unlock feature/x" run_nu "$tmp/ws/demo/trunk" "wt go feature/x | ignore" run_nu "$tmp/ws/demo/wt/feature-x" "wt rm feature/x --force | ignore" assert_no_path "$tmp/ws/demo/wt/feature-x" run_nu "$tmp/ws/demo/trunk" "wt new clean/remove | ignore" assert_dir "$tmp/ws/demo/wt/clean-remove" run_nu "$tmp/ws/demo/trunk" "wt remove clean/remove --branch | ignore" assert_no_path "$tmp/ws/demo/wt/clean-remove" ! git --git-dir="$tmp/ws/demo/.bare" show-ref --verify --quiet refs/heads/clean/remove || fail "wt remove --branch kept branch" run_nu "$tmp/ws/demo/trunk" "wt add move/me | ignore" assert_dir "$tmp/ws/demo/wt/move-me" run_nu "$tmp/ws/demo/trunk" "wt move move/me '$tmp/ws/demo/wt/moved-me' | ignore" assert_no_path "$tmp/ws/demo/wt/move-me" assert_dir "$tmp/ws/demo/wt/moved-me" git -C "$tmp/ws/demo/wt/moved-me" status --short --branch >/dev/null run_nu "$tmp/ws/demo/trunk" "wt done move/me --force | ignore" assert_no_path "$tmp/ws/demo/wt/moved-me" ! git --git-dir="$tmp/ws/demo/.bare" show-ref --verify --quiet refs/heads/move/me || fail "wt done kept branch" run_nu "$tmp/ws/demo/trunk" "wt prune" # normal checkout migration: dirty/staged/untracked/ignored/stash preserved, original kept as backup. git clone "$tmp/origin.git" "$tmp/normal" >/dev/null 2>&1 git -C "$tmp/normal" checkout -b local >/dev/null 2>&1 git -C "$tmp/normal" config user.email test@example.invalid git -C "$tmp/normal" config user.name test echo stash > "$tmp/normal/stash.txt" git -C "$tmp/normal" add stash.txt git -C "$tmp/normal" stash push -m stash-test >/dev/null echo '*.log' > "$tmp/normal/.gitignore" git -C "$tmp/normal" add .gitignore echo dirty >> "$tmp/normal/root.txt" echo staged > "$tmp/normal/staged.txt" git -C "$tmp/normal" add staged.txt echo untracked > "$tmp/normal/untracked.txt" echo ignored > "$tmp/normal/ignored.log" normal_status="$(git -C "$tmp/normal" status --porcelain=v1)" run_nu "$tmp/normal" "wt migrate --dry | ignore" run_nu "$tmp/normal" "wt migrate | ignore" assert_dir "$tmp/normal/.bare" assert_dir "$tmp/normal/local" assert_dir "$tmp/normal/wt" assert_file "$tmp/normal/local/.git" assert_file "$tmp/normal/local/ignored.log" [ "$(git -C "$tmp/normal/local" status --porcelain=v1)" = "$normal_status" ] || fail "normal migrate status mismatch" git --git-dir="$tmp/normal/.bare" show-ref --verify --quiet refs/stash || fail "normal migrate lost stash" compgen -G "$tmp/.normal.pre-wt-migrate-*" >/dev/null || fail "normal migrate missing backup" # detached migration must fail without replacing checkout. git clone "$tmp/origin.git" "$tmp/detached" >/dev/null 2>&1 git -C "$tmp/detached" checkout --detach >/dev/null 2>&1 expect_fail "$tmp/detached" "wt migrate | ignore" assert_dir "$tmp/detached/.git" # legacy bare-container migration: .git admin repo becomes .bare; worktrees move to trunk + wt/. mkdir -p "$tmp/legacy" git clone --bare "$tmp/origin.git" "$tmp/legacy/.git" >/dev/null 2>&1 git --git-dir="$tmp/legacy/.git" config wt.repo-name legacy git --git-dir="$tmp/legacy/.git" config wt.main-branch trunk git --git-dir="$tmp/legacy/.git" -c worktree.useRelativePaths=false worktree add "$tmp/legacy/legacy.trunk" trunk >/dev/null git --git-dir="$tmp/legacy/.git" -c worktree.useRelativePaths=false worktree add -b feature/x "$tmp/legacy/legacy.feature-x" trunk >/dev/null run_nu "$tmp/legacy/legacy.trunk" "wt migrate | ignore" assert_dir "$tmp/legacy/.bare" assert_no_path "$tmp/legacy/.git" assert_dir "$tmp/legacy/trunk" assert_dir "$tmp/legacy/wt/feature-x" git -C "$tmp/legacy/trunk" status --short --branch >/dev/null git -C "$tmp/legacy/wt/feature-x" status --short --branch >/dev/null legacy_gitfile="$(cat "$tmp/legacy/trunk/.git")" [[ "$legacy_gitfile" == "gitdir: $tmp/legacy/.bare/worktrees/"* ]] || fail "legacy gitfile not absolute .bare path: $legacy_gitfile" [ -z "$(git --git-dir="$tmp/legacy/.bare" config --get extensions.relativeWorktrees || true)" ] || fail "legacy relativeWorktrees set" [ "$(git --git-dir="$tmp/legacy/.bare" config --bool worktree.useRelativePaths)" = false ] || fail "legacy useRelativePaths not false" printf 'ok git-worktree-layout\n'