Skip to content

fix(git): clear git's repository-local env for cbm's own git children (#2003) - #2341

Merged
DeusData merged 1 commit into
mainfrom
fix/git-spawn-local-env
Sep 30, 2026
Merged

DeusData merged 1 commit into
mainfrom
fix/git-spawn-local-env

Conversation

@DeusData

Copy link
Copy Markdown
Owner

Git exports repository-local variables (GIT_DIR, GIT_WORK_TREE,
GIT_INDEX_FILE, GIT_COMMON_DIR, ...) into hooks, and some editors export
them into their terminals. They take precedence over git -C <dir>, and
every git child cbm spawned inherited them. A cbm started from such an
environment therefore resolved every project against the CALLER's
repository: git_context reported a plain directory as is_git=true with
the caller's worktree root, branch and HEAD, the history pass attributed
the caller's commits to it, and the watcher / detect_changes /
artifact git calls read (and config merge.ours.driver could write) the
wrong repository.

Fix: one production helper, src/foundation/git_env.{c,h}, holds the
15-variable list git rev-parse --local-env-vars prints and builds the
child environment with exactly those removed -- a POSIX envp array or a
Windows CREATE_UNICODE_ENVIRONMENT block (case-insensitive names). It is
applied per spawn; cbm's own environment is never modified (no unsetenv
in the parent, which would race other threads):

  • cbm_popen_git(cmd): read-mode popen for git command lines. POSIX spawns
    /bin/sh -c via posix_spawn with the scrubbed envp (stdin /dev/null,
    pipe ends close-on-exec, reaped by cbm_pclose); Windows passes the block
    to the existing handle-isolated CreateProcessW spawn. Fails closed.
  • cbm_proc_opts_t.strip_git_repo_env: cbm_subprocess_spawn passes the
    scrubbed envp to posix_spawnp / the fork+exec child, or the block to
    CreateProcessW. Fails closed.
  • Every git spawn uses them: git_context, pass_githistory, artifact
    (4 sites), the watcher's git runner, and detect_changes' git commands.
    search_code's grep scan keeps the inherited environment.

Tests (RED before, green after, RED on revert of the helper and of the
call sites):

  • git_context::git_context_ignores_inherited_repo_env -- GIT_DIR and the
    hook set pointing at a decoy repo: a plain dir is is_git=false, a
    fixture repo reports its own HEAD and branch, the decoy is untouched.
  • git_context::githistory_ignores_inherited_repo_env -- the history pass
    on a plain dir sees 0 commits (was the decoy's 4).
  • subprocess::subprocess_strip_git_repo_env_is_per_child and
    popen_git_strips_repo_env_and_reports_exit_status -- all 15 variables
    are gone in the child, unrelated variables pass through, the parent
    environment is unchanged, and cbm_pclose keeps pclose() status
    semantics.

Proof (macOS arm64, GIT_DIR/GIT_WORK_TREE/GIT_INDEX_FILE/GIT_PREFIX
exported at a decoy clone, cli index_status verbose=true): a plain dir
went from is_git=true, worktree_root=, branch=<decoy's> (v0.11.0)
to is_git=false; a fixture repo went from the decoy's root/branch/HEAD
to its own root and branch.

Refs #2003


Production half of #2003 (the test-suite half is the fix/issue-2003 PR). Windows branches are reviewed but only exercised on the Windows CI leg.

…#2003)

Git exports repository-local variables (GIT_DIR, GIT_WORK_TREE,
GIT_INDEX_FILE, GIT_COMMON_DIR, ...) into hooks, and some editors export
them into their terminals. They take precedence over `git -C <dir>`, and
every git child cbm spawned inherited them. A cbm started from such an
environment therefore resolved every project against the CALLER's
repository: git_context reported a plain directory as is_git=true with
the caller's worktree root, branch and HEAD, the history pass attributed
the caller's commits to it, and the watcher / detect_changes /
artifact git calls read (and `config merge.ours.driver` could write) the
wrong repository.

Fix: one production helper, src/foundation/git_env.{c,h}, holds the
15-variable list `git rev-parse --local-env-vars` prints and builds the
child environment with exactly those removed -- a POSIX envp array or a
Windows CREATE_UNICODE_ENVIRONMENT block (case-insensitive names). It is
applied per spawn; cbm's own environment is never modified (no unsetenv
in the parent, which would race other threads):

- cbm_popen_git(cmd): read-mode popen for git command lines. POSIX spawns
  /bin/sh -c via posix_spawn with the scrubbed envp (stdin /dev/null,
  pipe ends close-on-exec, reaped by cbm_pclose); Windows passes the block
  to the existing handle-isolated CreateProcessW spawn. Fails closed.
- cbm_proc_opts_t.strip_git_repo_env: cbm_subprocess_spawn passes the
  scrubbed envp to posix_spawnp / the fork+exec child, or the block to
  CreateProcessW. Fails closed.
- Every git spawn uses them: git_context, pass_githistory, artifact
  (4 sites), the watcher's git runner, and detect_changes' git commands.
  search_code's grep scan keeps the inherited environment.

Tests (RED before, green after, RED on revert of the helper and of the
call sites):
- git_context::git_context_ignores_inherited_repo_env -- GIT_DIR and the
  hook set pointing at a decoy repo: a plain dir is is_git=false, a
  fixture repo reports its own HEAD and branch, the decoy is untouched.
- git_context::githistory_ignores_inherited_repo_env -- the history pass
  on a plain dir sees 0 commits (was the decoy's 4).
- subprocess::subprocess_strip_git_repo_env_is_per_child and
  popen_git_strips_repo_env_and_reports_exit_status -- all 15 variables
  are gone in the child, unrelated variables pass through, the parent
  environment is unchanged, and cbm_pclose keeps pclose() status
  semantics.

Proof (macOS arm64, GIT_DIR/GIT_WORK_TREE/GIT_INDEX_FILE/GIT_PREFIX
exported at a decoy clone, `cli index_status verbose=true`): a plain dir
went from is_git=true, worktree_root=<decoy>, branch=<decoy's> (v0.11.0)
to is_git=false; a fixture repo went from the decoy's root/branch/HEAD
to its own root and branch.

Refs #2003

Signed-off-by: Martin Vogel <[email protected]>
@DeusData
DeusData merged commit 94097f0 into main Sep 30, 2026
41 checks passed
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.

1 participant