Per user request, we exercised #19's new stash-before-commit logic as a real test instead of a synthetic dry run: recreated the exact #18 incident shape (dirty git-guard.sh on a HEAD 3 commits behind origin/main) directly on docker-2's checkout.
Result: the new logic from #19 worked flawlessly end-to-end — detected dirty+stale, stashed safely (git-guard-safety-stash-20260909T205701Z), called resync_with_origin() which correctly identified behind-only and fast-forwarded to d72a8eb, then git stash pop reapplied cleanly and dropped the stash. Every new code path from #19 behaved exactly as designed.
But then it hit a pre-existing, previously-latent bug on the final exec "$0" "$@" self-re-invocation:
deploy/git-guard.sh is tracked in git at mode 100644 (confirmed via git ls-tree HEAD) — not executable. stack-deploy.sh correctly invokes it as bash "$DIR/deploy/git-guard.sh", which sidesteps the exec bit for the first call (its own comment even says "invoked via bash explicitly so the tracked file's exec bit doesn't matter"). But the script's own internal exec "$0" "$@" calls — used to cleanly re-run after a successful push or (now, post-#19) a successful stash-pop — rely on the kernel executing the file directly via $0, which does require +x. Since every fresh checkout/fast-forward preserves the tracked 644 mode, this fails.
This bug pre-dates #19. The old dirty-commit-then-push-success path had the identical exec "$0" "$@" pattern and would have hit the same failure if it were ever exercised on a non-executable checkout (i.e., any normal checkout, since the file has never been tracked with the exec bit). #19 just added a second trigger point (the stash-pop-success path), and running the live test against it is what surfaced this.
Fix
Change both exec "$0" "$@" occurrences to exec bash "$0" "$@" — explicitly invokes through the interpreter instead of relying on the tracked file's execute bit. Strictly more robust; no behavior change on systems where the bit happens to be set.
Verification
Live-reproduced the failure on docker-2 first (see above), confirmed root cause via git ls-tree HEAD deploy/git-guard.sh showing mode 100644.
After this fix, re-ran the identical dirty+stale scenario on docker-2 — full re-verification results follow in the chat / next comment.
## Found via live testing of #19 on docker-2
Per user request, we exercised #19's new stash-before-commit logic as a real test instead of a synthetic dry run: recreated the exact #18 incident shape (dirty `git-guard.sh` on a HEAD 3 commits behind `origin/main`) directly on docker-2's checkout.
**Result: the new logic from #19 worked flawlessly end-to-end** — detected dirty+stale, stashed safely (`git-guard-safety-stash-20260909T205701Z`), called `resync_with_origin()` which correctly identified behind-only and fast-forwarded to `d72a8eb`, then `git stash pop` reapplied cleanly and dropped the stash. Every new code path from #19 behaved exactly as designed.
**But then it hit a pre-existing, previously-latent bug** on the final `exec "$0" "$@"` self-re-invocation:
```
==> Stash reapplied cleanly. Re-checking sync state...
/volume1/docker/compose-files/deploy/git-guard.sh: line 221: /volume1/docker/compose-files/deploy/git-guard.sh: Permission denied
EXIT_CODE=126
```
## Root cause
`deploy/git-guard.sh` is tracked in git at mode `100644` (confirmed via `git ls-tree HEAD`) — not executable. `stack-deploy.sh` correctly invokes it as `bash "$DIR/deploy/git-guard.sh"`, which sidesteps the exec bit for the *first* call (its own comment even says "invoked via bash explicitly so the tracked file's exec bit doesn't matter"). But the script's own internal `exec "$0" "$@"` calls — used to cleanly re-run after a successful push or (now, post-#19) a successful stash-pop — rely on the kernel executing the file directly via `$0`, which does require `+x`. Since every fresh checkout/fast-forward preserves the tracked `644` mode, this fails.
**This bug pre-dates #19.** The old dirty-commit-then-push-success path had the identical `exec "$0" "$@"` pattern and would have hit the same failure if it were ever exercised on a non-executable checkout (i.e., any normal checkout, since the file has never been tracked with the exec bit). #19 just added a second trigger point (the stash-pop-success path), and running the live test against it is what surfaced this.
## Fix
Change both `exec "$0" "$@"` occurrences to `exec bash "$0" "$@"` — explicitly invokes through the interpreter instead of relying on the tracked file's execute bit. Strictly more robust; no behavior change on systems where the bit happens to be set.
## Verification
- Live-reproduced the failure on docker-2 first (see above), confirmed root cause via `git ls-tree HEAD deploy/git-guard.sh` showing mode `100644`.
- After this fix, re-ran the identical dirty+stale scenario on docker-2 — full re-verification results follow in the chat / next comment.
Found via live testing on docker-2 after merging #19: git-guard.sh is
tracked in git at mode 100644 (not executable). stack-deploy.sh correctly
invokes it as `bash deploy/git-guard.sh`, sidestepping the exec bit for the
first call -- but the script's own internal `exec "$0" "$@"` calls (used to
cleanly re-run after a successful push or stash-pop) rely on the kernel
executing the file directly, which requires +x. Since every fresh checkout
or fast-forward preserves the tracked 644 mode, this failed with
"Permission denied" (exit 126) the moment either self-re-invocation path
was actually exercised.
Confirmed via a live dry run: simulated the exact #18 incident shape (dirty
git-guard.sh on a HEAD 3 commits behind origin/main) on docker-2. The new
stash-first logic from #19 worked perfectly end-to-end -- detected
dirty+stale, stashed safely, fast-forwarded via resync_with_origin(), and
popped the stash cleanly -- but then hit this pre-existing bug on the
final `exec "$0" "$@"` re-invocation. This bug pre-dates #19 (the old
dirty-commit-then-push-success path had the identical pattern); #19 just
added a second trigger point that happened to surface it during testing.
Fix: `exec bash "$0" "$@"` explicitly invokes through the interpreter
instead of relying on the file's own execute bit -- correct regardless of
what git tracks the file's mode as.
AVB
merged commit d5db4c1a4e into main2026-09-09 19:36:31 -07:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Found via live testing of #19 on docker-2
Per user request, we exercised #19's new stash-before-commit logic as a real test instead of a synthetic dry run: recreated the exact #18 incident shape (dirty
git-guard.shon a HEAD 3 commits behindorigin/main) directly on docker-2's checkout.Result: the new logic from #19 worked flawlessly end-to-end — detected dirty+stale, stashed safely (
git-guard-safety-stash-20260909T205701Z), calledresync_with_origin()which correctly identified behind-only and fast-forwarded tod72a8eb, thengit stash popreapplied cleanly and dropped the stash. Every new code path from #19 behaved exactly as designed.But then it hit a pre-existing, previously-latent bug on the final
exec "$0" "$@"self-re-invocation:Root cause
deploy/git-guard.shis tracked in git at mode100644(confirmed viagit ls-tree HEAD) — not executable.stack-deploy.shcorrectly invokes it asbash "$DIR/deploy/git-guard.sh", which sidesteps the exec bit for the first call (its own comment even says "invoked via bash explicitly so the tracked file's exec bit doesn't matter"). But the script's own internalexec "$0" "$@"calls — used to cleanly re-run after a successful push or (now, post-#19) a successful stash-pop — rely on the kernel executing the file directly via$0, which does require+x. Since every fresh checkout/fast-forward preserves the tracked644mode, this fails.This bug pre-dates #19. The old dirty-commit-then-push-success path had the identical
exec "$0" "$@"pattern and would have hit the same failure if it were ever exercised on a non-executable checkout (i.e., any normal checkout, since the file has never been tracked with the exec bit). #19 just added a second trigger point (the stash-pop-success path), and running the live test against it is what surfaced this.Fix
Change both
exec "$0" "$@"occurrences toexec bash "$0" "$@"— explicitly invokes through the interpreter instead of relying on the tracked file's execute bit. Strictly more robust; no behavior change on systems where the bit happens to be set.Verification
git ls-tree HEAD deploy/git-guard.shshowing mode100644.