nbspec/git_env.rs
1//! Environment hygiene for spawning git-aware subprocesses.
2//!
3//! When a process is invoked from inside a Git hook (pre-commit,
4//! pre-push, post-checkout, ...) or from a CI runner, Git exports a
5//! set of repository-routing environment variables into the hook's
6//! environment: `GIT_DIR`, `GIT_INDEX_FILE`, `GIT_COMMON_DIR`,
7//! `GIT_WORK_TREE`, `GIT_OBJECT_DIRECTORY`,
8//! `GIT_ALTERNATE_OBJECT_DIRECTORIES`. Every subprocess the hook
9//! spawns inherits them.
10//!
11//! Any of these variables redirect every Git call inside the
12//! subprocess away from the subprocess's expected repository.
13//! Downstream tools layered over Git — for our purposes, `nb`, which
14//! is a bash script wrapping Git — then act on the wrong repo:
15//! `nb notebooks add` writes a scratch notebook into the parent
16//! repo, the subsequent `nb notebooks` listing reads from a
17//! different root, and the test fails in ways that depend on the
18//! hook environment (CI vs. local vs. the same tests run outside
19//! any hook).
20//!
21//! The fix is mechanical: before invoking any Git-aware subprocess
22//! from inside a context that may be hooked or CI-driven, remove
23//! every `GIT_*` variable from the child's environment. The child
24//! then starts from a clean slate and resolves the repository from
25//! its own `cwd` / its own arguments.
26//!
27//! See `nbspec:issues/4` for the original CI failure analysis and
28//! the local repro (`GIT_DIR=<repo> GIT_INDEX_FILE=<repo>/index
29//! cargo test --test integration`).
30
31use std::process::Command;
32
33/// Returns the names of every environment variable in the current
34/// process whose name starts with `GIT_`. Exposed so other call
35/// sites (e.g., a future tokio-process variant in `git_env`, or
36/// any caller that wants the list without the scrub) share one
37/// enumeration policy.
38///
39/// **Blast vs. selective — deliberate decision.** The `GIT_` prefix
40/// blast also removes intent vars (`GIT_CONFIG_GLOBAL`,
41/// `GIT_SSH_COMMAND`, `GIT_TERMINAL_PROMPT`, ...). Today no nbspec
42/// code path consumes those; the only vars that redirect Git's
43/// view of the repository are `GIT_DIR`, `GIT_INDEX_FILE`,
44/// `GIT_COMMON_DIR`, `GIT_WORK_TREE`, `GIT_OBJECT_DIRECTORY`, and
45/// `GIT_ALTERNATE_OBJECT_DIRECTORIES`. A more selective policy
46/// could enumerate exactly those. The blast is chosen for two
47/// reasons: (1) any future `GIT_*` redirect that lands in this
48/// range gets caught by default rather than requiring a code
49/// change; (2) keeping the predicate to a prefix check is the
50/// minimum surface to audit. Revisit if a container identity
51/// mechanism ever routes through `GIT_CONFIG_GLOBAL` — at that
52/// point a selective enumeration belongs here.
53pub fn leaked_git_names() -> Vec<String> {
54 std::env::vars()
55 .filter_map(|(name, _)| {
56 if name.starts_with("GIT_") {
57 Some(name)
58 } else {
59 None
60 }
61 })
62 .collect()
63}
64
65/// Removes every environment variable whose name starts with `GIT_`
66/// from the given command's environment. The spawned process
67/// inherits every other variable from the parent (PATH, HOME,
68/// LANG, ...), just not the ones that redirect Git's view of the
69/// repository.
70///
71/// Pass the command BEFORE chaining `.args(...)` or `.env(...)` so
72/// later `.env(name, value)` calls are not also removed.
73///
74/// # Example
75///
76/// ```no_run
77/// use std::process::Command;
78/// nbspec::git_env::scrub_git_env(&mut Command::new("nb"));
79/// ```
80pub fn scrub_git_env(command: &mut Command) {
81 for name in leaked_git_names() {
82 command.env_remove(&name);
83 }
84}