use super::*;
use std::path::Path;
use std::process::Command;
fn git_cmd(dir: &Path) -> Command {
let mut cmd = Command::new("git");
cmd.current_dir(dir)
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.env("GIT_CONFIG_SYSTEM", "/dev/null")
.env("GIT_CONFIG_NOSYSTEM", "1")
.env("GIT_AUTHOR_NAME", "Tester")
.env("GIT_AUTHOR_EMAIL", "t@example.com")
.env("GIT_COMMITTER_NAME", "Tester")
.env("GIT_COMMITTER_EMAIL", "t@example.com")
.env("GIT_TERMINAL_PROMPT", "0")
.env("GIT_PAGER", "cat")
.env_remove("EDITOR")
.env_remove("GIT_EDITOR")
.env_remove("VISUAL")
.env_remove("GIT_ASKPASS")
.env_remove("SSH_ASKPASS");
for var in [
"GIT_DIR",
"GIT_WORK_TREE",
"GIT_INDEX_FILE",
"GIT_OBJECT_DIRECTORY",
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
"GIT_COMMON_DIR",
"GIT_NAMESPACE",
"GIT_CEILING_DIRECTORIES",
"GIT_PREFIX",
] {
cmd.env_remove(var);
}
cmd
}
fn git(dir: &Path, args: &[&str]) {
let ok = git_cmd(dir)
.args(args)
.status()
.expect("git runs")
.success();
assert!(ok, "git {args:?} failed");
}
fn repo_with_commit() -> tempfile::TempDir {
let dir = tempfile::tempdir().unwrap();
let p = dir.path();
git(p, &["init", "-q", "-b", "main"]);
std::fs::write(p.join("a.txt"), "hello\n").unwrap();
git(p, &["add", "a.txt"]);
git(p, &["commit", "-q", "-m", "first commit"]);
dir
}
fn repo_with_commit_on_task_branch() -> tempfile::TempDir {
let dir = repo_with_commit();
git(dir.path(), &["checkout", "-q", "-b", "task"]);
dir
}
fn rev_parse(dir: &Path, rev: &str) -> String {
String::from_utf8(
git_cmd(dir)
.args(["rev-parse", rev])
.output()
.unwrap()
.stdout,
)
.unwrap()
.trim()
.to_string()
}
fn repo_with_history() -> (tempfile::TempDir, Vec<String>) {
let dir = tempfile::tempdir().unwrap();
let p = dir.path();
let mut ids = Vec::new();
git(p, &["init", "-q", "-b", "main"]);
std::fs::write(p.join("a.txt"), "hello\n").unwrap();
git(p, &["add", "a.txt"]);
git(p, &["commit", "-q", "-m", "c1: add a.txt"]);
ids.push(rev_parse(p, "HEAD"));
std::fs::write(p.join("b.txt"), "world\n").unwrap();
git(p, &["add", "b.txt"]);
git(p, &["commit", "-q", "-m", "c2: add b.txt"]);
ids.push(rev_parse(p, "HEAD"));
std::fs::write(p.join("a.txt"), "hello again\n").unwrap();
git(p, &["add", "a.txt"]);
git(p, &["commit", "-q", "-m", "c3: modify a.txt"]);
ids.push(rev_parse(p, "HEAD"));
(dir, ids)
}
use newt_core::agentic::GitTool as _;
fn tool(dir: &Path) -> LocalGitTool {
LocalGitTool {
root: dir.to_path_buf(),
author: Author {
name: "newt-agent[bot]".into(),
email: "bot@example.com".into(),
},
attribution: Some(newt_core::attribution::CommitAttribution::from_runtime(
"qwen3:30b",
None,
"noreply@newt-agent.com",
)),
commit_succeeded: std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)),
contributors_consumed: std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)),
signer: None,
}
}
fn commit_count(dir: &Path) -> usize {
let out = git_cmd(dir)
.args(["rev-list", "--count", "HEAD"])
.output()
.unwrap();
String::from_utf8_lossy(&out.stdout).trim().parse().unwrap()
}
fn head_message(dir: &Path) -> String {
let out = git_cmd(dir)
.args(["log", "-1", "--pretty=%B"])
.output()
.unwrap();
String::from_utf8_lossy(&out.stdout).to_string()
}
fn git_status_porcelain(dir: &Path) -> String {
let out = git_cmd(dir)
.args(["status", "--porcelain"])
.output()
.unwrap();
String::from_utf8_lossy(&out.stdout).to_string()
}
#[cfg(test)]
mod ambient_environment {
use super::*;
#[test]
fn git_cmd_defeats_a_poisoned_global_config() {
let dir = repo_with_commit();
let p = dir.path();
let poison = p.join("poisoned.gitconfig");
std::fs::write(&poison, "[tag]\n\tgpgsign = true\n").unwrap();
let seen = Command::new("git")
.current_dir(p)
.env("GIT_CONFIG_GLOBAL", &poison)
.args(["config", "--get", "tag.gpgsign"])
.output()
.expect("git runs");
assert_eq!(
String::from_utf8_lossy(&seen.stdout).trim(),
"true",
"fixture no longer reproduces #2221; this test would be vacuous"
);
let unscrubbed = Command::new("git")
.current_dir(p)
.env("GIT_CONFIG_GLOBAL", &poison)
.env_remove("EDITOR")
.env_remove("GIT_EDITOR")
.env_remove("VISUAL")
.args(["tag", "poisoned"])
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.expect("git runs");
if unscrubbed.success() {
assert_eq!(
object_type(p, "refs/tags/poisoned"),
"tag",
"poisoned config should have produced an annotated tag"
);
}
git(p, &["tag", "clean"]);
assert_eq!(
object_type(p, "refs/tags/clean"),
"commit",
"ambient config leaked into the fixture helper"
);
}
#[test]
fn git_cmd_removes_the_git_dir_family() {
let dir = repo_with_commit();
let envs: std::collections::HashMap<_, _> = git_cmd(dir.path())
.get_envs()
.map(|(k, v)| {
(
k.to_string_lossy().into_owned(),
v.map(|v| v.to_string_lossy().into_owned()),
)
})
.collect();
for var in [
"GIT_DIR",
"GIT_WORK_TREE",
"GIT_INDEX_FILE",
"GIT_COMMON_DIR",
] {
assert_eq!(
envs.get(var),
Some(&None),
"{var} must be removed, not merely left inherited"
);
}
assert_eq!(
envs.get("GIT_CONFIG_GLOBAL").and_then(|v| v.as_deref()),
Some("/dev/null"),
"global config must be pinned away from the developer's ~/.gitconfig"
);
}
fn object_type(dir: &Path, refname: &str) -> String {
let out = git_cmd(dir)
.args(["cat-file", "-t", refname])
.output()
.expect("git runs");
String::from_utf8_lossy(&out.stdout).trim().to_string()
}
}
#[cfg(test)]
#[path = "attribution.rs"]
mod attribution;
#[cfg(test)]
#[path = "branch_list.rs"]
mod branch_list;
#[cfg(test)]
#[path = "checkout_branch.rs"]
mod checkout_branch;
#[cfg(test)]
#[path = "engine_read.rs"]
mod engine_read;
#[cfg(test)]
#[path = "signing.rs"]
mod signing;
#[path = "engine_write.rs"]
mod engine_write;
#[cfg(test)]
#[path = "git_scope.rs"]
mod git_scope;
#[cfg(test)]
#[path = "log_diff_operands.rs"]
mod log_diff_operands;
#[cfg(test)]
#[path = "rebase.rs"]
mod rebase;
#[cfg(test)]
#[path = "scoped_legacy.rs"]
mod scoped_legacy;
#[cfg(test)]
#[path = "stash_and_unknown.rs"]
mod stash_and_unknown;
#[cfg(test)]
#[path = "tool_dispatch.rs"]
mod tool_dispatch;
#[cfg(test)]
#[path = "tool_surface.rs"]
mod tool_surface;
#[path = "worktree_cwd.rs"]
mod worktree_cwd;