Skip to content

fix(git): resolve absolute bare repo dir in post-receive hook#304

Closed
cjunxiang wants to merge 2 commits into
kunchenguid:mainfrom
cjunxiang:fix/gate-absolute-git-dir-269
Closed

fix(git): resolve absolute bare repo dir in post-receive hook#304
cjunxiang wants to merge 2 commits into
kunchenguid:mainfrom
cjunxiang:fix/gate-absolute-git-dir-269

Conversation

@cjunxiang

@cjunxiang cjunxiang commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

What Changed

  • The shipped post-receive hook resolves the bare repo directory via git rev-parse --absolute-git-dir (falling back to pwd only if git is unavailable) instead of relying on pwd, so it yields the true bare-repo path regardless of the cwd/PWD state git invoked the hook from.
  • Both the notify-push --gate argument and the notify-push.log path now use the resolved GATE_DIR rather than $(pwd).
  • Add hook tests covering cwd-divergent and PWD=.-poisoned invocations where the old pwd-based path collapsed to an invalid --gate ..

Risk Assessment

✅ Low: Well-bounded one-commit fix (a single shell snippet plus a focused, deterministic regression test) that correctly addresses the diagnosed root cause; the daemon matches repos by basename only, so the new absolute/symlink-resolved path cannot regress downstream path matching, and the change includes a sensible defensive fallback.

Testing

Verified the #269 fix end-to-end through the actual product surfaces (real hook script, real git push, real daemon over real IPC), not by re-running unit tests. Proof chain: (1) The shipped hook (PostReceiveHookScript, rendered to 01-real-shipped-hook.sh) now computes GATE_DIR=$(git rev-parse --absolute-git-dir 2>/dev/null || pwd). (2) Driving BOTH the NEW and OLD hook script under the three #269 conditions (02-gate-resolution-transcript.txt) shows the NEW hook resolves the absolute bare dir every time (A/B/C) and on a genuine git push (D), while the OLD hook produces the bug values: '.' under PWD=. poison and a non-.git sibling dir under cwd-divergence. (3) Most decisive — feeding each candidate value to the REAL daemon (no-mistakes daemon in an isolated NM_HOME, reached via real unix-socket IPC with daemon notify-push) and reading the server's own slog verdict (03-daemon-gate-validation.txt): the daemon rejects '.' with invalid gate path: . and rejects the sibling dir with invalid gate path: <dir> — exactly the #269 fatal error that left pipelines never starting — but it ACCEPTS the NEW hook's absolute <id>.git value at the gate layer (unknown repo for gate …, i.e. repoIDFromGatePath accepted the path and the failure is the unrelated 'repo not registered' check). That closes the loop: the value the OLD hook computes is fatal at the layer that broke; the value the NEW hook computes passes it. Method note: client-side daemon notify-push returns empty output on error because the root cobra command sets SilenceErrors=true, so the authoritative verdict is the daemon's server-side slog line "ipc request failed method=push_received error=…" — read from the daemon log.

Evidence: 01 — Real shipped post-receive hook (contains the fix)

Rendered PostReceiveHookScript(). Line 17: GATE_DIR=$(git rev-parse --absolute-git-dir 2>/dev/null || pwd) — the #269 fix. Old '$(pwd)' gate path is gone. Includes the rationale comment explaining why pwd collapses to '.' and citing Git 2.13+.

#!/bin/sh
# no-mistakes post-receive hook
# Notifies the daemon of the push. Non-blocking: post-receive exit code is
# ignored by git, so we never reject the push here. Instead, failures are
# surfaced on stderr (so the pushing client sees them) and appended to
# notify-push.log inside the bare repo for later inspection.
NM_BIN='/var/folders/z4/lqr0grkx5szdkkqjhjyprljw0000gn/T/go-build434623105/b001/git.test'
if [ ! -f "$NM_BIN" ]; then
  NM_BIN="$(command -v no-mistakes 2>/dev/null || echo no-mistakes)"
fi
# Resolve the bare repo dir explicitly. Git can invoke this hook from a cwd
# whose pwd collapses to "." (issue #269), which would pass "--gate ." and be
# rejected by the daemon ("invalid gate path: ."), so the pipeline never
# starts. git rev-parse --absolute-git-dir queries git directly and always
# yields the true path regardless of cwd/PWD state (Git 2.13+, May 2017); fall
# back to pwd only if git itself is somehow unavailable.
GATE_DIR=$(git rev-parse --absolute-git-dir 2>/dev/null || pwd)
LOG="$GATE_DIR/notify-push.log"
nm_ts() { date '+%Y-%m-%dT%H:%M:%S' 2>/dev/null || echo unknown; }
notify_failed=0
while read oldrev newrev refname; do
	  set -- --gate "$GATE_DIR" \
	    --ref "$refname" \
	    --old "$oldrev" \
	    --new "$newrev"
	  i=0
	  while [ "$i" -lt "${GIT_PUSH_OPTION_COUNT:-0}" ]; do
	    opt=$(printenv "GIT_PUSH_OPTION_$i" 2>/dev/null || :)
	    set -- "$@" --push-option "$opt"
	    i=$((i + 1))
	  done
	  out=$(NM_HOOK_HELPER=1 "$NM_BIN" daemon notify-push "$@" 2>&1)
  status=$?
  if [ $status -ne 0 ]; then
    notify_failed=1
    {
      printf '[%s] notify-push failed for %s (exit %d)\n' "$(nm_ts)" "$refname" "$status"
      printf '%s\n\n' "$out"
    } >> "$LOG"
    {
      printf 'no-mistakes: notify-push failed for %s (exit %d):\n' "$refname" "$status"
      printf '%s\n' "$out"
      printf 'See %s for full history.\n' "$LOG"
    } >&2
  fi
done

if [ "$notify_failed" -eq 0 ]; then
  cat >&2 <<'BANNER'
_  _ ____    _  _ _ ____ ___ ____ _  _ ____ ____
|\ | |  |    |\/| | [__   |  |__| |_/  |___ [__
| \| |__|    |  | | ___]  |  |  | | \_ |___ ___]

  * Pipeline started

  Run no-mistakes to review.

BANNER
fi
exit 0
Evidence: 02 — NEW vs OLD hook gate resolution under all three #269 conditions + genuine git push

A) normal cwd — both correct. B) cwd diverges to sibling, GIT_DIR=bare — NEW resolves absolute bare dir; OLD yields non-.git sibling dir (daemon-rejectable). C) PWD=. poisoned — NEW resolves absolute bare dir; OLD yields '.' (the reported bug). D) genuine git push with real NEW hook installed — hook fires, records absolute push.git gate, push succeeds.

==============================================================
ISSUE #269 — post-receive --gate resolution: NEW(fixed) vs OLD(pre-fix)
recorder installed as 'no-mistakes' on PATH; hook reaches it via the
[ ! -f "$NM_BIN" ] fallback (the embedded path is the test binary, now gone).
bare-new = /var/folders/z4/lqr0grkx5szdkkqjhjyprljw0000gn/T/tmp.MK8hAWK7Fb/gate-new.git
bare-old = /var/folders/z4/lqr0grkx5szdkkqjhjyprljw0000gn/T/tmp.nti44GY2Oj/gate-old.git
==============================================================

A) NORMAL  — cwd == bare dir, no stress (even OLD is correct here)
   NEW --gate = /private/var/folders/z4/lqr0grkx5szdkkqjhjyprljw0000gn/T/tmp.MK8hAWK7Fb/gate-new.git
   OLD --gate = /var/folders/z4/lqr0grkx5szdkkqjhjyprljw0000gn/T/tmp.nti44GY2Oj/gate-old.git

B) STRESS #1 — cwd diverges from repo (cwd=sibling, GIT_DIR=bare)
   (the #269 'pwd no longer names the bare repo' failure class)
   NEW --gate = /private/var/folders/z4/lqr0grkx5szdkkqjhjyprljw0000gn/T/tmp.MK8hAWK7Fb/gate-new.git   (expect: bare dir  -> CORRECT)
   OLD --gate = /var/folders/z4/lqr0grkx5szdkkqjhjyprljw0000gn/T/tmp.nti44GY2Oj   (expect: sibling dir -> WRONG, not a .git path -> daemon rejects)

C) STRESS #2 — PWD=. poisoned, cwd=bare (author's deterministic repro from hook_test.go)
   NEW --gate = /private/var/folders/z4/lqr0grkx5szdkkqjhjyprljw0000gn/T/tmp.MK8hAWK7Fb/gate-new.git   (expect: bare dir  -> CORRECT)
   OLD --gate = .   (expect: '.' or wrong -> the reported bug)

--------------------------------------------------------------
RAW RESULTS  (val == basename of bare dir 'gate-new.git' => correct)
   NEW :  A=/private/var/folders/z4/lqr0grkx5szdkkqjhjyprljw0000gn/T/tmp.MK8hAWK7Fb/gate-new.git | B=/private/var/folders/z4/lqr0grkx5szdkkqjhjyprljw0000gn/T/tmp.MK8hAWK7Fb/gate-new.git | C=/private/var/folders/z4/lqr0grkx5szdkkqjhjyprljw0000gn/T/tmp.MK8hAWK7Fb/gate-new.git
   OLD :  A=/var/folders/z4/lqr0grkx5szdkkqjhjyprljw0000gn/T/tmp.nti44GY2Oj/gate-old.git | B=/var/folders/z4/lqr0grkx5szdkkqjhjyprljw0000gn/T/tmp.nti44GY2Oj | C=.
--------------------------------------------------------------

D) GENUINE 'git push' to a bare repo with the real shipped NEW hook installed
   hook fired on real push; recorded --gate = /private/var/folders/z4/lqr0grkx5szdkkqjhjyprljw0000gn/T/tmp.A2qf2zCDV2/push.git
   push succeeded and hook ran: YES
Evidence: 03 — Real daemon verdicts (decisive before/after proof)

Real daemon server slog log. --gate '.' -> 'invalid gate path: .' (the #269 fatal). --gate sibling dir -> 'invalid gate path: <dir>'. --gate absolute '<id>.git' (NEW hook value) -> 'unknown repo for gate …' (ACCEPTED at gate layer; fails only for unrelated unregistered-repo reason).

Driving the REAL daemon (isolated NM_HOME) with each candidate --gate value.
The daemon (no-mistakes daemon) logs the server verdict via slog; we read its log.

  -> --gate '.'   [OLD hook #269 bug: pwd collapsed to '.']
  -> --gate '/var/folders/z4/lqr0grkx5szdkkqjhjyprljw0000gn/T'   [OLD hook wrong-dir: sibling dir, no .git suffix]
  -> --gate '/var/folders/z4/lqr0grkx5szdkkqjhjyprljw0000gn/T/tmp.URNsKol0UL/repos/example.git'   [NEW hook resolved: absolute <id>.git]

=== Real daemon server log (relevant lines) ===
time=2026-06-27T13:19:12.141+08:00 level=INFO msg="push received" ref=refs/heads/main old=0000000000000000000000000000000000000000 new=deadbeefdeadbeefdeadbeefdeadbeefdeadbeef gate=.
time=2026-06-27T13:19:12.141+08:00 level=INFO msg="ipc request failed" method=push_received error="invalid gate path: ."
time=2026-06-27T13:19:12.158+08:00 level=INFO msg="push received" ref=refs/heads/main old=0000000000000000000000000000000000000000 new=deadbeefdeadbeefdeadbeefdeadbeefdeadbeef gate=/var/folders/z4/lqr0grkx5szdkkqjhjyprljw0000gn/T
time=2026-06-27T13:19:12.158+08:00 level=INFO msg="ipc request failed" method=push_received error="invalid gate path: /var/folders/z4/lqr0grkx5szdkkqjhjyprljw0000gn/T"
time=2026-06-27T13:19:12.171+08:00 level=INFO msg="push received" ref=refs/heads/main old=0000000000000000000000000000000000000000 new=deadbeefdeadbeefdeadbeefdeadbeefdeadbeef gate=/var/folders/z4/lqr0grkx5szdkkqjhjyprljw0000gn/T/tmp.URNsKol0UL/repos/example.git
time=2026-06-27T13:19:12.172+08:00 level=INFO msg="ipc request failed" method=push_received error="unknown repo for gate /var/folders/z4/lqr0grkx5szdkkqjhjyprljw0000gn/T/tmp.URNsKol0UL/repos/example.git"

=== Verdict map ===
  server verdicts for the three pushes: invalid gate path: . invalid gate path: /var/folders/z4/lqr0grkx5szdkkqjhjyprljw0000gn/T|unknown repo for gate /var/folders/z4/lqr0grkx5szdkkqjhjyprljw0000gn/T/tmp.URNsKol0UL/repos/example.git

Pipeline

Updates from git push no-mistakes

⏭️ **intent** - skipped

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - passed

✅ No issues found.

✅ **Test** - passed

✅ No issues found.

  • Shipped hook content: rendered PostReceiveHookScript() and confirmed line 'GATE_DIR=$(git rev-parse --absolute-git-dir 2>/dev/null || pwd)' plus the #269 rationale comment are present; old 'pwd' gate path is gone (01-real-shipped-hook.sh).
  • NEW hook gate resolution — case A (normal, cwd==bare dir): resolves absolute bare dir. CORRECT.
  • NEW hook gate resolution — case B (cwd diverges to a sibling dir while GIT_DIR points at the bare repo, the #269 'pwd no longer names the bare repo' class): resolves absolute bare dir. CORRECT (OLD hook yields the sibling dir, which is not a .git path).
  • NEW hook gate resolution — case C (PWD=. poisoned with cwd==bare, the author's deterministic repro from hook_test.go): resolves absolute bare dir. CORRECT (OLD hook yields exactly '.').
  • NEW hook gate resolution — case D (genuine git push to a bare repo with the real shipped NEW hook installed): hook fires on real push and records the absolute push.git gate; push succeeds. CORRECT.
  • OLD (pre-fix) hook reproduction: confirmed it computes '.' under PWD=. poison and a non-.git sibling dir under cwd-divergence — reproducing the exact reported bug.
  • Real daemon gate validation (isolated NM_HOME, real daemon process, real unix-socket IPC via daemon notify-push): --gate '.' -> 'invalid gate path: .' (fatal); --gate sibling dir -> 'invalid gate path: <dir>' (fatal); --gate absolute '<id>.git' (NEW hook value) -> 'unknown repo for gate …' (ACCEPTED at the gate layer; fails only for unrelated unregistered-repo reason).
  • Worktree hygiene: removed throwaway zz_dump_hook_test.go and ./bin build output; verified git status clean and HEAD at 5369df6.
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

@cjunxiang

cjunxiang commented Jun 21, 2026

Copy link
Copy Markdown
Contributor Author

Hey sorry folks, issue is bugging me for awhile, decided to push ahead with a straight forward fix after seeing similar reported issues.

Do close this if it's too intrusive and pushy.

@kunchenguid

Copy link
Copy Markdown
Owner

Thanks for the contribution, @cjunxiang! One process note on this repo: PRs need to be raised through no-mistakes (git push no-mistakes), which runs the review/test/lint/CI pipeline and stamps the required signature into the PR body. This PR is currently failing the "PR must be raised via no-mistakes" check because it wasn't raised that way.

If working from a fork was the blocker, that's fixed as of v1.30.0 (#306). Per CONTRIBUTING.md: point origin at this parent repo, run no-mistakes init --fork-url <your-fork-url>, then git push no-mistakes.

I won't be merging PRs that bypass the gate going forward, but I'd genuinely love to land your work once it's re-raised. Thanks for understanding! 🙏

…enguid#269)

The post-receive hook passed --gate "$(pwd)", but git can invoke the hook
from a cwd whose pwd collapses to ".". The daemon then rejected the push
with "invalid gate path: ." and the pipeline never started; git printed
"Everything up-to-date" with no hint, and `no-mistakes runs` showed none.

Resolve the bare repo dir explicitly via `git rev-parse --absolute-git-dir`
(queries git directly; always returns the true path regardless of cwd/PWD
state) and reuse it for both --gate and notify-push.log. Falls back to pwd
only if git itself is unavailable.

Regression test poisons PWD="." to reproduce the reported failure
deterministically (the POSIX pwd builtin returns "." exactly as the reporter
saw), then asserts the recorded --gate is the absolute bare dir.

Closes kunchenguid#269.
@cjunxiang cjunxiang force-pushed the fix/gate-absolute-git-dir-269 branch from a36c186 to 5369df6 Compare June 27, 2026 05:30
@cjunxiang cjunxiang changed the title fix(gate): resolve absolute bare repo dir in post-receive hook (#269) fix(git): resolve absolute bare repo dir in post-receive hook Jun 27, 2026
@cjunxiang

Copy link
Copy Markdown
Contributor Author

pushed with no mistake. 👍🏼 Thanks @kunchenguid appreciate the work

@kunchenguid

Copy link
Copy Markdown
Owner

thanks for this. quick note on process - i require PRs to come through no-mistakes, which is why the "PR must be raised via no-mistakes" check is red here (this was opened directly).

the reason i ask for it: no-mistakes runs review, tests, lint, and docs on your branch before the PR is opened, so contributions arrive already validated. that keeps the quality bar high and keeps me from having to hand-review every change - i maintain this solo, so it's the only way i keep up.

can you re-raise it through no-mistakes? running the pipeline on your branch opens/updates the PR through the gate and turns that check green. the change looks useful, it just needs to come through that path. ping me if the setup gives you trouble.

@cjunxiang

Copy link
Copy Markdown
Contributor Author

thanks for this. quick note on process - i require PRs to come through no-mistakes, which is why the "PR must be raised via no-mistakes" check is red here (this was opened directly).

the reason i ask for it: no-mistakes runs review, tests, lint, and docs on your branch before the PR is opened, so contributions arrive already validated. that keeps the quality bar high and keeps me from having to hand-review every change - i maintain this solo, so it's the only way i keep up.

can you re-raise it through no-mistakes? running the pipeline on your branch opens/updates the PR through the gate and turns that check green. the change looks useful, it just needs to come through that path. ping me if the setup gives you trouble.

I've moved to #358 as advised, and closed this MR.

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.

2 participants