Commit d0f0ad6
fix(export): scrub git hook env and skip cross-worktree git-add (GH#3311) (gastownhall#3347)
When export.git-add=true (the v1.0.1+ default), the pre-commit hook in
a worktree stages a spurious issues.jsonl at the repo root instead of
(or in addition to) .beads/issues.jsonl. The ghost file lands in the
commit's diff but not on disk — easy to miss and painful to clean up.
Root cause (not what the issue body guessed): the absolute path passed
to `git add` was always correct. The bug is env inheritance. Git
invokes the pre-commit hook with GIT_DIR/GIT_WORK_TREE/GIT_INDEX_FILE/
GIT_COMMON_DIR/GIT_PREFIX set in the environment, and bd's git-add
subprocesses inherit them.
Both call sites set `cmd.Dir = filepath.Dir(fullPath)` (= `.beads/`).
With GIT_DIR set in the environment and GIT_WORK_TREE unset, git
defaults the work-tree to cwd. So the subprocess's git sees `.beads/`
as the work-tree root and records `.beads/issues.jsonl` at the root of
that broken view — which ends up in the worktree's actual index as a
bare `issues.jsonl`. Verified empirically: with GIT_DIR set and
cwd=`.beads/`, `git rev-parse --show-toplevel` returns the `.beads/`
directory itself.
Fix, in two parts:
1. scrubGitHookEnv() strips the vars that poison repo/worktree
auto-discovery, index routing, object routing, and config
injection:
- discovery: GIT_DIR, GIT_WORK_TREE, GIT_COMMON_DIR, GIT_PREFIX,
GIT_CEILING_DIRECTORIES, GIT_DISCOVERY_ACROSS_FILESYSTEM
- index: GIT_INDEX_FILE
- objects: GIT_OBJECT_DIRECTORY, GIT_ALTERNATE_OBJECT_DIRECTORIES
- config: the whole GIT_CONFIG* family (covers the case where the
parent invoked `git -c core.worktree=… commit`, which sets
GIT_CONFIG_PARAMETERS / GIT_CONFIG_COUNT in the child env)
With these cleared, git rediscovers the repo/worktree from cwd.
2. hookWorkTreeRoot() reads the inherited GIT_DIR to determine the
worktree whose hook is currently firing. If the target path is
outside that worktree, gitAddFile returns without staging. This
prevents a narrower but real regression that would otherwise land
after part 1 alone: in the .beads/redirect configuration, fullPath
points into the main repo (not the worktree), and post-scrub git
would silently stage the file into main's index from a worktree
hook. The existing preCommitHookBody template (init_git_hooks.go)
documents the same intent: "For worktrees: .beads is in the main
repo's working tree... we skip it."
The guard uses pathInsideDir which handles the macOS symlinked-parent
case: on /tmp → /private/tmp, a fresh-file target expressed as
/tmp/.../new.txt compared against a worktree root resolved to
/private/tmp/... would otherwise misclassify as outside the worktree.
pathInsideDir resolves the parent of each side via EvalSymlinks and
reattaches the basename — sufficient for the real caller shapes, since
gitAddFile's parent directory (beadsDir or the export output dir)
always exists at call time.
Also fixes the variant reported in a follow-up comment where the
.beads/redirect workaround did not resolve the root ghost: the env
pollution broke git-add inside the `bd export -o` subprocess spawned
from exportJSONLForCommit (that subprocess has BD_GIT_HOOK stripped,
but git env vars preserved, so its PostRun auto-export hits the same
bug via the shared gitAddFile).
Deduplicates the parallel git-add invocation in exportJSONLForCommit
by delegating to gitAddFile, so both call sites share the env scrub
and the cross-worktree guard.
Adds:
- TestGitAddFile_InWorktreeHook_StagesCorrectPath — end-to-end
worktree regression test; fails on main, passes with the fix.
- TestGitAddFile_RedirectCase_DoesNotStageInMainRepo — verifies
the cross-worktree skip does not pollute main's index when a
worktree has .beads/redirect pointing into main.
- TestGitAddFile_NonHookContext_GuardDoesNotFire — regression guard
confirming the cross-worktree skip is a no-op outside git hooks.
- TestScrubGitHookEnv — unit test for the env scrub helper, covering
the discovery/index/object vars and the GIT_CONFIG* family, and
verifying non-discovery vars (author/committer/editor/pager) pass
through untouched.
- TestPathInsideDir — covers structural cases plus the macOS
fresh-file + symlinked-parent regression.
- TestHookWorkTreeRoot — covers linked-worktree gitdir file,
plain-repo .git, bare/unrecognized, and not-a-hook contexts.
Workaround for users on older releases remains `BD_EXPORT_GIT_ADD=false`.
Closes GH#3311
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent 0dce51a commit d0f0ad6
3 files changed
Lines changed: 616 additions & 5 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
272 | 272 | | |
273 | 273 | | |
274 | 274 | | |
275 | | - | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
276 | 281 | | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
277 | 287 | | |
278 | 288 | | |
| 289 | + | |
279 | 290 | | |
280 | 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 | + | |
0 commit comments