Skip to main content

tokmd_git/
command.rs

1//! Git command construction with process-environment isolation.
2//!
3//! This module owns the subprocess boundary for git invocations. Callers should
4//! build git commands here so ambient repository and helper environment
5//! variables cannot bypass an explicit `-C` path or launch unexpected helpers.
6
7use std::process::Command;
8
9const GIT_REPO_SHAPING_ENV: &[&str] = &[
10    // Repository and object-store overrides.
11    "GIT_DIR",
12    "GIT_WORK_TREE",
13    "GIT_INDEX_FILE",
14    "GIT_OBJECT_DIRECTORY",
15    "GIT_ALTERNATE_OBJECT_DIRECTORIES",
16    "GIT_COMMON_DIR",
17    "GIT_CEILING_DIRECTORIES",
18    // Git hooks that can execute helper programs from ambient environment.
19    "GIT_SSH",
20    "GIT_SSH_COMMAND",
21    "GIT_ASKPASS",
22    "GIT_PAGER",
23    "GIT_EDITOR",
24    "GIT_PROXY_COMMAND",
25    "GIT_EXTERNAL_DIFF",
26];
27
28/// Create a `Command` for git with process-environment isolation.
29///
30/// Strips repo, worktree, index, object-store, discovery, and execution helper
31/// overrides so inherited environment variables cannot bypass the explicit
32/// `-C` path used by functions in this crate or launch ambient helper programs.
33pub fn git_cmd() -> Command {
34    let mut cmd = Command::new("git");
35    for name in GIT_REPO_SHAPING_ENV {
36        cmd.env_remove(name);
37    }
38    cmd
39}
40
41#[cfg(test)]
42mod tests {
43    use super::*;
44    use std::collections::BTreeSet;
45
46    #[test]
47    fn git_cmd_removes_repo_shaping_env_overrides() {
48        let removed: BTreeSet<_> = git_cmd()
49            .get_envs()
50            .filter(|(_, value)| value.is_none())
51            .map(|(name, _)| name.to_string_lossy().into_owned())
52            .collect();
53
54        for name in GIT_REPO_SHAPING_ENV {
55            assert!(removed.contains(*name), "missing env_remove for {name}");
56        }
57    }
58
59    #[test]
60    fn git_cmd_removes_execution_helper_env_overrides() {
61        let removed: BTreeSet<_> = git_cmd()
62            .get_envs()
63            .filter(|(_, value)| value.is_none())
64            .map(|(name, _)| name.to_string_lossy().into_owned())
65            .collect();
66
67        for name in [
68            "GIT_SSH",
69            "GIT_SSH_COMMAND",
70            "GIT_ASKPASS",
71            "GIT_PAGER",
72            "GIT_EDITOR",
73            "GIT_PROXY_COMMAND",
74            "GIT_EXTERNAL_DIFF",
75        ] {
76            assert!(
77                removed.contains(name),
78                "missing execution env_remove for {name}"
79            );
80        }
81    }
82}