fallow_core/git_env.rs
1//! Helpers for invoking `git` from fallow without inheriting ambient repo
2//! state from the parent process.
3//!
4//! When fallow is invoked from a git hook (`pre-commit`, `pre-push`,
5//! `commit-msg`, ...) or a tool that wraps git (lint-staged, husky, lefthook,
6//! pre-commit framework, IDE git integrations, some CI runners), git exports a
7//! handful of environment variables describing the *enclosing* operation:
8//! `GIT_INDEX_FILE`, `GIT_DIR`, `GIT_WORK_TREE`, `GIT_OBJECT_DIRECTORY`,
9//! `GIT_COMMON_DIR`, `GIT_PREFIX`. Several of these are written as paths
10//! relative to the parent's working directory (e.g. `GIT_INDEX_FILE=.git/index`
11//! during `git commit`). When fallow then spawns its own `git` subprocess from
12//! a different working directory (notably `git worktree add` against a
13//! temporary path), the inherited relative paths no longer resolve and the
14//! call fails.
15//!
16//! Fallow always operates against the repository at `--root` (or the cwd) and
17//! never wants to share index / object / work-tree state with an enclosing
18//! `git` operation, so the safe default is to strip these vars before every
19//! `git` invocation.
20//!
21//! Vars that are *not* stripped: `GIT_AUTHOR_*`, `GIT_COMMITTER_*`, `GIT_EDITOR`,
22//! `GIT_EXEC_PATH`. Those are either harmless to fallow's read-only git
23//! invocations or required for fallow's tests that depend on the parent shell's
24//! git config.
25
26use std::process::Command;
27
28/// Environment variables that describe an enclosing git operation's
29/// repository state, in the order they appear in `git`'s own environment
30/// documentation. Hook subprocesses inherit some or all of these, often as
31/// paths relative to the parent's cwd, and they break fallow's git invocations
32/// when fallow runs from a different cwd.
33pub const AMBIENT_GIT_ENV_VARS: &[&str] = &[
34 "GIT_DIR",
35 "GIT_WORK_TREE",
36 "GIT_INDEX_FILE",
37 "GIT_OBJECT_DIRECTORY",
38 "GIT_COMMON_DIR",
39 "GIT_PREFIX",
40];
41
42/// Strip ambient git repository-state environment variables from a `Command`.
43///
44/// Apply to every `git` subprocess fallow spawns from production code. The
45/// strip is unconditional and idempotent: `Command::env_remove` is a no-op
46/// when the variable is not present in the inherited environment.
47///
48/// Returns the `Command` for fluent chaining alongside `.args()`,
49/// `.current_dir()`, and so on.
50pub fn clear_ambient_git_env(cmd: &mut Command) -> &mut Command {
51 for var in AMBIENT_GIT_ENV_VARS {
52 cmd.env_remove(var);
53 }
54 cmd
55}
56
57#[cfg(test)]
58mod tests {
59 use super::{AMBIENT_GIT_ENV_VARS, clear_ambient_git_env};
60 use std::process::Command;
61
62 #[test]
63 fn clear_ambient_git_env_removes_every_listed_variable() {
64 let mut cmd = Command::new("git");
65 clear_ambient_git_env(&mut cmd);
66 let envs: Vec<_> = cmd.get_envs().collect();
67 for var in AMBIENT_GIT_ENV_VARS {
68 assert!(
69 envs.iter()
70 .any(|(key, value)| key.to_str() == Some(*var) && value.is_none()),
71 "{var} should be cleared from the command env",
72 );
73 }
74 }
75
76 #[test]
77 fn clear_ambient_git_env_is_idempotent() {
78 let mut cmd = Command::new("git");
79 clear_ambient_git_env(&mut cmd);
80 clear_ambient_git_env(&mut cmd);
81 let cleared = cmd.get_envs().filter(|(_, value)| value.is_none()).count();
82 assert_eq!(
83 cleared,
84 AMBIENT_GIT_ENV_VARS.len(),
85 "double-applying the helper should not duplicate env entries",
86 );
87 }
88}