-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest-sync-e2e.sh
More file actions
executable file
·465 lines (389 loc) · 18.2 KB
/
test-sync-e2e.sh
File metadata and controls
executable file
·465 lines (389 loc) · 18.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
#!/usr/bin/env bash
#
# TEMPORARY: This E2E test is included for reviewer validation only.
# It will be removed before merging.
#
# E2E test for `wt sync` — full workflow with 5-PR stack.
#
# Tests the complete stacked-PR workflow including:
# 1. Initial sync + auto stack file creation
# 2. Main advances → rebase all
# 3. Mid-stack commit → rebase downstream
# 4. PR1 squash-merged to main → reparent with --onto
# 5. PR3 squash-merged into PR2 (non-default branch merge) → detected!
# 6. --push with deleted remote branches
#
# Stack: main <- pr1 <- pr2 <- pr3 <- pr4 <- pr5
#
# Requires: gh (authenticated), git, wt (in PATH or via --wt-path)
#
# Usage:
# ./test-sync-e2e.sh # uses `wt` from PATH
# ./test-sync-e2e.sh --wt-path ./target/release/wt
# ./test-sync-e2e.sh --keep # don't prompt for cleanup
# ./test-sync-e2e.sh --auto-cleanup # delete repo without prompting
set -euo pipefail
# ─── Parse arguments ──────────────────────────────────────────────────────
WT_PATH=""
CLEANUP_MODE="prompt" # prompt | keep | auto
while [[ $# -gt 0 ]]; do
case "$1" in
--wt-path)
WT_PATH="$2"
shift 2
;;
--keep)
CLEANUP_MODE="keep"
shift
;;
--auto-cleanup)
CLEANUP_MODE="auto"
shift
;;
*)
echo "Unknown option: $1" >&2
echo "Usage: $0 [--wt-path PATH] [--keep | --auto-cleanup]" >&2
exit 1
;;
esac
done
# Resolve wt binary
if [[ -n "${WT_PATH}" ]]; then
WT_BIN="$(cd "$(dirname "${WT_PATH}")" && pwd)/$(basename "${WT_PATH}")"
if [[ ! -x "${WT_BIN}" ]]; then
echo "Error: wt binary not found or not executable: ${WT_BIN}" >&2
exit 1
fi
else
WT_BIN="$(command -v wt 2>/dev/null || true)"
if [[ -z "${WT_BIN}" ]]; then
echo "Error: wt not found in PATH. Use --wt-path to specify location." >&2
exit 1
fi
fi
echo "Using wt: ${WT_BIN}"
"${WT_BIN}" --version
# ─── Verify prerequisites ────────────────────────────────────────────────
for cmd in gh git; do
if ! command -v "${cmd}" &>/dev/null; then
echo "Error: ${cmd} is required but not found in PATH." >&2
exit 1
fi
done
if ! gh auth status &>/dev/null; then
echo "Error: gh is not authenticated. Run 'gh auth login' first." >&2
exit 1
fi
# ─── Setup ────────────────────────────────────────────────────────────────
GH_USER=$(gh api user -q .login)
REPO_NAME="wt-sync-e2e-$$" # unique per invocation via PID
REPO_FULL="${GH_USER}/${REPO_NAME}"
WORK_DIR=$(mktemp -d)
REPO_DIR="${WORK_DIR}/${REPO_NAME}"
bold='\033[1m'
green='\033[32m'
red='\033[31m'
yellow='\033[33m'
cyan='\033[36m'
reset='\033[0m'
PASS=0
FAIL=0
info() { echo -e "${cyan}${bold}==> ${1}${reset}"; }
ok() { echo -e "${green}${bold} ✓ ${1}${reset}"; PASS=$((PASS + 1)); }
fail() { echo -e "${red}${bold} ✗ ${1}${reset}"; FAIL=$((FAIL + 1)); }
separator() { echo "────────────────────────────────────────"; }
check() {
local description="$1"
local condition="$2"
if eval "$condition"; then
ok "$description"
else
fail "$description"
fi
}
show_branches() {
info "Branch state:"
cd "${REPO_DIR}"
for branch in pr1 pr2 pr3 pr4 pr5; do
if git rev-parse --verify "${branch}" &>/dev/null; then
local count
count=$(git rev-list --count main.."${branch}" 2>/dev/null || echo "?")
echo " ${branch}: ${count} commits ahead of main"
else
echo " ${branch}: (not present)"
fi
done
}
show_stack_file() {
local stack_file="${REPO_DIR}/.git/wt/stack"
if [[ -f "${stack_file}" ]]; then
info "Stack file (.git/wt/stack):"
cat "${stack_file}"
echo ""
else
info "No stack file"
fi
}
# Alias wt to our resolved binary
wt() { "${WT_BIN}" "$@"; }
cleanup() {
echo ""
echo -e "${bold}═══ Cleanup ═══${reset}"
echo "Work directory: ${WORK_DIR}"
echo "GitHub repo: ${REPO_FULL}"
echo ""
case "${CLEANUP_MODE}" in
keep)
echo "Kept (--keep). Clean up manually:"
echo " gh repo delete ${REPO_FULL} --yes"
echo " rm -rf ${WORK_DIR}"
;;
auto)
gh repo delete "${REPO_FULL}" --yes 2>/dev/null && ok "Deleted GitHub repo" || echo " Could not delete repo"
rm -rf "${WORK_DIR}" && ok "Deleted ${WORK_DIR}" || echo " Could not delete work dir"
;;
prompt)
read -rp "Delete the GitHub repo and local files? [y/N] " confirm
if [[ "${confirm}" =~ ^[Yy]$ ]]; then
gh repo delete "${REPO_FULL}" --yes 2>/dev/null && ok "Deleted GitHub repo" || echo " Could not delete repo"
rm -rf "${WORK_DIR}" && ok "Deleted ${WORK_DIR}" || echo " Could not delete work dir"
else
echo "Kept. Clean up manually:"
echo " gh repo delete ${REPO_FULL} --yes"
echo " rm -rf ${WORK_DIR}"
fi
;;
esac
}
# Clean up on exit (including failures)
trap 'cleanup' EXIT
# ─── Create repo ──────────────────────────────────────────────────────────
info "Creating private repo: ${REPO_FULL}"
cd "${WORK_DIR}"
gh repo create "${REPO_NAME}" --private --clone --add-readme -d "wt sync e2e testing"
cd "${REPO_DIR}"
echo "# E2E test" > README.md
echo "module main" > go.mod
git add -A && git commit -m "Initial project setup"
git push
ok "Repo created at ${REPO_FULL}"
# ─── Create 5-PR stack ───────────────────────────────────────────────────
info "Creating stacked worktrees: main <- pr1 <- pr2 <- pr3 <- pr4 <- pr5"
git worktree add -b pr1 "${WORK_DIR}/repo.pr1"
cd "${WORK_DIR}/repo.pr1"
echo "func Auth() {}" > auth.go
git add -A && git commit -m "pr1: add auth"
git push -u origin pr1 --force
cd "${REPO_DIR}"
git worktree add -b pr2 "${WORK_DIR}/repo.pr2" pr1
cd "${WORK_DIR}/repo.pr2"
echo "func Login() {}" > login.go
git add -A && git commit -m "pr2: add login"
git push -u origin pr2 --force
cd "${REPO_DIR}"
git worktree add -b pr3 "${WORK_DIR}/repo.pr3" pr2
cd "${WORK_DIR}/repo.pr3"
echo "func Session() {}" > session.go
git add -A && git commit -m "pr3: add session"
git push -u origin pr3 --force
cd "${REPO_DIR}"
git worktree add -b pr4 "${WORK_DIR}/repo.pr4" pr3
cd "${WORK_DIR}/repo.pr4"
echo "func Middleware() {}" > middleware.go
git add -A && git commit -m "pr4: add middleware"
git push -u origin pr4 --force
cd "${REPO_DIR}"
git worktree add -b pr5 "${WORK_DIR}/repo.pr5" pr4
cd "${WORK_DIR}/repo.pr5"
echo "func Handler() {}" > handler.go
git add -A && git commit -m "pr5: add handler"
git push -u origin pr5 --force
cd "${REPO_DIR}"
ok "Worktrees created"
info "Creating GitHub PRs"
gh pr create --head pr1 --base main \
--title "pr1: auth" --body "Auth." --repo "${REPO_FULL}" 2>/dev/null || true
gh pr create --head pr2 --base pr1 \
--title "pr2: login" --body "Login. Stacked on pr1." --repo "${REPO_FULL}" 2>/dev/null || true
gh pr create --head pr3 --base pr2 \
--title "pr3: session" --body "Session. Stacked on pr2." --repo "${REPO_FULL}" 2>/dev/null || true
gh pr create --head pr4 --base pr3 \
--title "pr4: middleware" --body "Middleware. Stacked on pr3." --repo "${REPO_FULL}" 2>/dev/null || true
gh pr create --head pr5 --base pr4 \
--title "pr5: handler" --body "Handler. Stacked on pr4." --repo "${REPO_FULL}" 2>/dev/null || true
ok "PRs created"
echo ""
info "Setup complete: https://github.com/${REPO_FULL}/pulls"
show_branches
# ═══════════════════════════════════════════════════════════════════════════
# Test 1: Initial sync creates stack file automatically
# ═══════════════════════════════════════════════════════════════════════════
echo ""
echo -e "${bold}═══ Test 1: Initial sync + auto stack file ═══${reset}"
cd "${REPO_DIR}"
info "Running: wt sync --dry-run"
separator
wt sync --dry-run 2>&1
separator
# --dry-run is read-only: it should NOT create the stack file
check "No stack file after dry-run (read-only)" "[[ ! -f ${REPO_DIR}/.git/wt/stack ]]"
# ═══════════════════════════════════════════════════════════════════════════
# Test 2: Main advances → rebase all branches
# ═══════════════════════════════════════════════════════════════════════════
echo ""
echo -e "${bold}═══ Test 2: Main advances → rebase all ═══${reset}"
cd "${REPO_DIR}"
echo 'version = "1.0"' > version.txt
git add -A && git commit -m "chore: add version file"
git push
info "Running: wt sync --push"
separator
wt sync --push 2>&1
separator
check "pr1 has version.txt after rebase" "[[ -f ${WORK_DIR}/repo.pr1/version.txt ]]"
check "pr5 has version.txt after rebase" "[[ -f ${WORK_DIR}/repo.pr5/version.txt ]]"
show_branches
# ═══════════════════════════════════════════════════════════════════════════
# Test 3: Mid-stack commit → downstream rebases
# ═══════════════════════════════════════════════════════════════════════════
echo ""
echo -e "${bold}═══ Test 3: Mid-stack commit → downstream rebases ═══${reset}"
cd "${WORK_DIR}/repo.pr1"
echo "func Logout() {}" >> auth.go
git add -A && git commit -m "pr1: add logout"
git push
cd "${REPO_DIR}"
info "Running: wt sync --push"
separator
wt sync --push 2>&1
separator
check "pr2 has logout after mid-stack rebase" "grep -q Logout ${WORK_DIR}/repo.pr2/auth.go"
check "pr5 has logout after mid-stack rebase" "grep -q Logout ${WORK_DIR}/repo.pr5/auth.go"
show_branches
# ═══════════════════════════════════════════════════════════════════════════
# Test 4: PR1 squash-merged to main → reparent with --onto
# ═══════════════════════════════════════════════════════════════════════════
echo ""
echo -e "${bold}═══ Test 4: PR1 squash-merged to main ═══${reset}"
info "Squash-merging PR1 via GitHub CLI"
gh pr merge pr1 --squash --repo "${REPO_FULL}"
cd "${REPO_DIR}"
info "Running: wt sync --fetch --push"
separator
wt sync --fetch --push 2>&1
separator
show_stack_file
show_branches
check "Stack file does NOT contain pr1" "! grep -q '^pr1$' ${REPO_DIR}/.git/wt/stack"
check "pr2 still has login.go" "[[ -f ${WORK_DIR}/repo.pr2/login.go ]]"
check "pr2 has auth.go (from merged pr1)" "[[ -f ${WORK_DIR}/repo.pr2/auth.go ]]"
# ═══════════════════════════════════════════════════════════════════════════
# Test 5: PR3 squash-merged into PR2 (non-default branch merge!)
# ═══════════════════════════════════════════════════════════════════════════
echo ""
echo -e "${bold}═══ Test 5: PR3 squash-merged into PR2 (non-default branch!) ═══${reset}"
echo -e "${yellow}This is the key test — PR3 merged into its parent PR2 (not main).${reset}"
# Simulate squash-merge of PR3 into PR2 locally.
# Stack before: main ← pr2 ← pr3 ← pr4 ← pr5
# After merge: main ← pr2 (contains pr3's changes) ← pr4 ← pr5
info "Simulating squash-merge of PR3 into PR2 (locally)"
cd "${WORK_DIR}/repo.pr2"
echo "func Session() {}" > session.go
git add -A && git commit -m "squash-merge pr3 into pr2"
git push --force-with-lease
cd "${REPO_DIR}"
info "Running: wt sync"
separator
wt sync 2>&1
separator
show_stack_file
show_branches
check "Stack file does NOT contain pr3" "! grep -q '^[[:space:]]*pr3' ${REPO_DIR}/.git/wt/stack"
check "pr4 has session.go (from pr3 via pr2)" "[[ -f ${WORK_DIR}/repo.pr4/session.go ]]"
check "pr5 has middleware.go" "[[ -f ${WORK_DIR}/repo.pr5/middleware.go ]]"
# ═══════════════════════════════════════════════════════════════════════════
# Test 6: --push with pruned remote branches
# ═══════════════════════════════════════════════════════════════════════════
echo ""
echo -e "${bold}═══ Test 6: --push with pruned remote branches ═══${reset}"
cd "${REPO_DIR}"
info "Deleting remote pr1 and pr3 branches"
git push origin --delete pr1 2>/dev/null || true
git push origin --delete pr3 2>/dev/null || true
git fetch --prune
info "Running: wt sync --push"
separator
wt sync --push 2>&1
separator
check "Sync with --push completes without error" "true"
# ═══════════════════════════════════════════════════════════════════════════
# Test 7: --prune removes integrated worktrees and remote branches
# ═══════════════════════════════════════════════════════════════════════════
echo ""
echo -e "${bold}═══ Test 7: --prune removes integrated worktrees and remote branches ═══${reset}"
# Current stack: main ← pr2 ← pr4 ← pr5
# Simulate squash-merge of PR2 into main locally (same approach as Test 5).
# Using gh pr merge fails due to GitHub merge conflicts from squash history
# rewriting, so we simulate the merge directly.
info "Simulating squash-merge of PR2 into main (locally)"
cd "${REPO_DIR}"
git fetch origin
git checkout main
git pull origin main
git merge --squash pr2 2>/dev/null || {
# If merge --squash conflicts, accept pr2's versions
git checkout --theirs . 2>/dev/null
git add -A
}
git commit -m "squash-merge pr2 into main"
git push
cd "${REPO_DIR}"
info "Running: wt sync --fetch --prune --push"
separator
wt sync --fetch --prune --push 2>&1
separator
show_stack_file
show_branches
check "Stack file does NOT contain pr2" "! grep -q '^[[:space:]]*pr2' ${REPO_DIR}/.git/wt/stack"
check "pr4 reparented onto main" "[[ $(git rev-list --count main..pr4 2>/dev/null) -le 3 ]]"
check "pr4 has login.go (inherited from pr2)" "[[ -f ${WORK_DIR}/repo.pr4/login.go ]]"
# If pr5 had a rebase conflict, resolve it before continuing prune checks.
# This can happen because pr5 carries old commits from the multi-level squash chain.
# Worktrees use a .git file (not directory), so check via git status.
if git -C "${WORK_DIR}/repo.pr5" rev-parse --git-dir &>/dev/null; then
PR5_GIT_DIR=$(git -C "${WORK_DIR}/repo.pr5" rev-parse --git-dir)
if [[ -d "${PR5_GIT_DIR}/rebase-merge" ]]; then
info "pr5 has a rebase conflict (expected with deep stacks) — resolving"
cd "${WORK_DIR}/repo.pr5"
git checkout --theirs . 2>/dev/null
git add -A
GIT_EDITOR=true git rebase --continue 2>/dev/null || true
cd "${REPO_DIR}"
# Re-run sync to complete prune
separator
wt sync --prune --push 2>&1
separator
fi
fi
check "pr2 worktree directory removed" "! [[ -d ${WORK_DIR}/repo.pr2 ]]"
check "pr2 remote branch deleted" "! git ls-remote --exit-code origin pr2 &>/dev/null"
# ═══════════════════════════════════════════════════════════════════════════
# Summary
# ═══════════════════════════════════════════════════════════════════════════
echo ""
echo -e "${bold}═══════════════════════════════════════════${reset}"
echo -e "${bold} Results: ${green}${PASS} passed${reset}, ${red}${FAIL} failed${reset}"
echo -e "${bold}═══════════════════════════════════════════${reset}"
echo ""
echo "Test 1: Auto stack file creation"
echo "Test 2: Main advances → rebase all"
echo "Test 3: Mid-stack commit → rebase downstream"
echo "Test 4: PR1 squash-merged to main → reparent"
echo "Test 5: PR3 merged into PR2 (non-default branch detection)"
echo "Test 6: --push with deleted remote branches"
echo "Test 7: --prune removes integrated worktrees and remote branches"
echo ""
# Exit with failure if any tests failed
if [[ "${FAIL}" -gt 0 ]]; then
exit 1
fi