mirror of
https://github.com/openai/codex.git
synced 2026-10-29 19:08:43 +00:00
Merge local-only Git transport base
This commit is contained in:
@@ -124,7 +124,7 @@ pub fn apply_git_patch(req: &ApplyGitRequest) -> io::Result<ApplyGitResult> {
|
||||
}
|
||||
|
||||
fn resolve_git_root(cwd: &Path) -> io::Result<PathBuf> {
|
||||
let out = std::process::Command::new("git")
|
||||
let out = local_git_command()
|
||||
.arg("rev-parse")
|
||||
.arg("--show-toplevel")
|
||||
.current_dir(cwd)
|
||||
@@ -149,7 +149,7 @@ fn write_temp_patch(diff: &str) -> io::Result<(tempfile::TempDir, PathBuf)> {
|
||||
}
|
||||
|
||||
fn run_git(cwd: &Path, git_cfg: &[String], args: &[String]) -> io::Result<(i32, String, String)> {
|
||||
let mut cmd = std::process::Command::new("git");
|
||||
let mut cmd = local_git_command();
|
||||
for p in git_cfg {
|
||||
cmd.arg(p);
|
||||
}
|
||||
@@ -163,6 +163,12 @@ fn run_git(cwd: &Path, git_cfg: &[String], args: &[String]) -> io::Result<(i32,
|
||||
Ok((code, stdout, stderr))
|
||||
}
|
||||
|
||||
fn local_git_command() -> std::process::Command {
|
||||
let mut command = std::process::Command::new("git");
|
||||
command.envs(crate::local_only_git_env());
|
||||
command
|
||||
}
|
||||
|
||||
fn quote_shell(s: &str) -> String {
|
||||
let simple = s
|
||||
.chars()
|
||||
@@ -329,7 +335,7 @@ pub fn stage_paths(git_root: &Path, diff: &str) -> io::Result<()> {
|
||||
if existing.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let mut cmd = std::process::Command::new("git");
|
||||
let mut cmd = local_git_command();
|
||||
cmd.arg("add");
|
||||
cmd.arg("--");
|
||||
for p in &existing {
|
||||
@@ -592,6 +598,10 @@ fn regex_ci(pat: &str) -> Regex {
|
||||
Regex::new(&format!("(?i){pat}")).unwrap_or_else(|e| panic!("invalid regex: {e}"))
|
||||
}
|
||||
|
||||
#[cfg(all(test, unix))]
|
||||
#[path = "apply_transport_tests.rs"]
|
||||
mod transport_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
use super::ApplyGitRequest;
|
||||
use super::apply_git_patch;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::fs;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use std::process::Output;
|
||||
|
||||
#[test]
|
||||
fn three_way_apply_does_not_lazy_fetch_promisor_objects() {
|
||||
let temp_dir = tempfile::tempdir().expect("create temp dir");
|
||||
let source = temp_dir.path().join("source");
|
||||
let partial = temp_dir.path().join("partial");
|
||||
fs::create_dir(&source).expect("create source repository");
|
||||
run_git(&source, &["init", "-q", "--initial-branch=main"]);
|
||||
run_git(&source, &["config", "uploadpack.allowFilter", "true"]);
|
||||
|
||||
fs::write(source.join("data.txt"), "one\nbase\nthree\n").expect("write base blob");
|
||||
run_git(&source, &["add", "data.txt"]);
|
||||
commit_all(&source, "base");
|
||||
let base_blob = run_git_stdout(&source, &["rev-parse", "HEAD:data.txt"]);
|
||||
|
||||
fs::write(source.join("data.txt"), "one\nbase\npatched\n").expect("write patch blob");
|
||||
let patch = String::from_utf8(run_git(&source, &["diff", "--full-index", "--binary"]).stdout)
|
||||
.expect("patch should be UTF-8");
|
||||
run_git(&source, &["checkout", "-q", "--", "data.txt"]);
|
||||
|
||||
fs::write(source.join("data.txt"), "current\nbase\nthree\n").expect("write head blob");
|
||||
commit_all(&source, "head");
|
||||
|
||||
let complete_result = apply_git_patch(&ApplyGitRequest {
|
||||
cwd: source.clone(),
|
||||
diff: patch.clone(),
|
||||
revert: false,
|
||||
preflight: false,
|
||||
})
|
||||
.expect("apply patch in complete clone");
|
||||
assert_eq!(
|
||||
(
|
||||
complete_result.exit_code,
|
||||
fs::read_to_string(source.join("data.txt")).expect("read complete result"),
|
||||
),
|
||||
(0, "current\nbase\npatched\n".to_string()),
|
||||
"three-way apply should retain normal behavior when every object is local; stderr: {}",
|
||||
complete_result.stderr,
|
||||
);
|
||||
run_git(&source, &["reset", "-q", "--hard", "HEAD"]);
|
||||
|
||||
let source_url = format!("file://{}", source.display());
|
||||
run_git(
|
||||
temp_dir.path(),
|
||||
&[
|
||||
"-c",
|
||||
"protocol.file.allow=always",
|
||||
"clone",
|
||||
"-q",
|
||||
"--no-local",
|
||||
"--filter=blob:none",
|
||||
"--no-checkout",
|
||||
&source_url,
|
||||
partial.to_str().expect("partial path"),
|
||||
],
|
||||
);
|
||||
run_git(&partial, &["checkout", "-q", "main"]);
|
||||
let missing = run_git_stdout(
|
||||
&partial,
|
||||
&["rev-list", "--objects", "--all", "--missing=print"],
|
||||
);
|
||||
assert!(
|
||||
missing.lines().any(|line| line == format!("?{base_blob}")),
|
||||
"expected patch base blob {base_blob} to remain missing:\n{missing}",
|
||||
);
|
||||
|
||||
let helper = temp_dir.path().join("transport-helper.sh");
|
||||
fs::write(&helper, "#!/bin/sh\nprintf ran >\"$0.ran\"\nexit 1\n")
|
||||
.expect("write transport helper");
|
||||
let mut permissions = fs::metadata(&helper)
|
||||
.expect("read transport helper metadata")
|
||||
.permissions();
|
||||
permissions.set_mode(/*mode*/ 0o755);
|
||||
fs::set_permissions(&helper, permissions).expect("make transport helper executable");
|
||||
let helper_url = format!("ext::{}", helper.display());
|
||||
run_git(&partial, &["config", "remote.origin.url", &helper_url]);
|
||||
run_git(&partial, &["config", "protocol.ext.allow", "always"]);
|
||||
|
||||
let patch_path = temp_dir.path().join("change.patch");
|
||||
fs::write(&patch_path, &patch).expect("write attack reproduction patch");
|
||||
let unprotected = Command::new("git")
|
||||
.args(["apply", "--3way", patch_path.to_str().expect("patch path")])
|
||||
.env_remove("GIT_ALLOW_PROTOCOL")
|
||||
.env_remove("GIT_NO_LAZY_FETCH")
|
||||
.current_dir(&partial)
|
||||
.output()
|
||||
.expect("run unprotected Git apply");
|
||||
let helper_marker = helper.with_extension("sh.ran");
|
||||
assert_eq!(
|
||||
(unprotected.status.success(), helper_marker.exists()),
|
||||
(false, true),
|
||||
"unprotected three-way apply should reach the selected promisor transport",
|
||||
);
|
||||
fs::remove_file(&helper_marker).expect("remove reproduction marker");
|
||||
run_git(&partial, &["reset", "-q", "--hard", "HEAD"]);
|
||||
|
||||
let partial_result = apply_git_patch(&ApplyGitRequest {
|
||||
cwd: partial,
|
||||
diff: patch,
|
||||
revert: false,
|
||||
preflight: false,
|
||||
})
|
||||
.expect("run fail-closed apply");
|
||||
|
||||
assert_eq!(
|
||||
(partial_result.exit_code == 0, helper_marker.exists(),),
|
||||
(false, false),
|
||||
"local-only apply must fail without invoking the promisor transport",
|
||||
);
|
||||
}
|
||||
|
||||
fn run_git(cwd: &Path, args: &[&str]) -> Output {
|
||||
let output = Command::new("git")
|
||||
.args(args)
|
||||
.current_dir(cwd)
|
||||
.output()
|
||||
.unwrap_or_else(|error| panic!("run git {args:?}: {error}"));
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"git {args:?} failed\nstdout:\n{}\nstderr:\n{}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr),
|
||||
);
|
||||
output
|
||||
}
|
||||
|
||||
fn run_git_stdout(cwd: &Path, args: &[&str]) -> String {
|
||||
String::from_utf8_lossy(&run_git(cwd, args).stdout)
|
||||
.trim()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn commit_all(cwd: &Path, message: &str) {
|
||||
run_git(
|
||||
cwd,
|
||||
&[
|
||||
"-c",
|
||||
"user.name=Codex Test",
|
||||
"-c",
|
||||
"user.email=codex@example.com",
|
||||
"commit",
|
||||
"-qam",
|
||||
message,
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -424,7 +424,11 @@ impl crate::FsmonitorProbeRunner for LocalFsmonitorProbeRunner<'_> {
|
||||
// Both probes are fast, bounded metadata queries that do not inspect the
|
||||
// worktree or index, so do not reduce the requested command's timeout.
|
||||
let mut command = Command::new(self.git);
|
||||
command.args(args).current_dir(self.cwd).kill_on_drop(true);
|
||||
command
|
||||
.envs(crate::local_only_git_env())
|
||||
.args(args)
|
||||
.current_dir(self.cwd)
|
||||
.kill_on_drop(true);
|
||||
match timeout(GIT_COMMAND_TIMEOUT, command.output()).await {
|
||||
Ok(Ok(output)) if output.status.success() => Some(output.stdout),
|
||||
_ => None,
|
||||
@@ -445,6 +449,7 @@ async fn run_git_command_with_timeout_from(
|
||||
) -> Option<std::process::Output> {
|
||||
let mut command = Command::new(git);
|
||||
command
|
||||
.envs(crate::local_only_git_env())
|
||||
.env("GIT_OPTIONAL_LOCKS", "0")
|
||||
// Keep internal Git commands independent of repository-selected hooks
|
||||
// and fsmonitor helpers while preserving built-in fsmonitor acceleration.
|
||||
@@ -908,6 +913,120 @@ mod tests {
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
#[cfg(unix)]
|
||||
fn run_git(cwd: &Path, args: &[&str]) -> std::process::Output {
|
||||
let output = std::process::Command::new("git")
|
||||
.args(args)
|
||||
.current_dir(cwd)
|
||||
.output()
|
||||
.expect("run Git command");
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"git {args:?} failed\nstdout:\n{}\nstderr:\n{}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
output
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn run_git_stdout(cwd: &Path, args: &[&str]) -> String {
|
||||
String::from_utf8(run_git(cwd, args).stdout)
|
||||
.expect("Git output should be UTF-8")
|
||||
.trim()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn commit_all(cwd: &Path, message: &str) {
|
||||
run_git(
|
||||
cwd,
|
||||
&[
|
||||
"-c",
|
||||
"user.name=Codex Test",
|
||||
"-c",
|
||||
"user.email=codex@example.com",
|
||||
"commit",
|
||||
"-qam",
|
||||
message,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
async fn diff_against_sha_does_not_lazy_fetch_promisor_objects() {
|
||||
let temp_dir = tempfile::tempdir().expect("create temp dir");
|
||||
let source = temp_dir.path().join("source");
|
||||
let clone = temp_dir.path().join("clone");
|
||||
std::fs::create_dir(&source).expect("create source repository");
|
||||
run_git(&source, &["init", "-q", "--initial-branch=main"]);
|
||||
run_git(&source, &["config", "uploadpack.allowFilter", "true"]);
|
||||
|
||||
std::fs::write(source.join("data.txt"), "before\n").expect("write initial blob");
|
||||
run_git(&source, &["add", "data.txt"]);
|
||||
commit_all(&source, "initial");
|
||||
let base_sha = run_git_stdout(&source, &["rev-parse", "HEAD"]);
|
||||
let base_blob = run_git_stdout(&source, &["rev-parse", "HEAD:data.txt"]);
|
||||
|
||||
std::fs::write(source.join("data.txt"), "after\n").expect("write current blob");
|
||||
commit_all(&source, "current");
|
||||
|
||||
let complete_diff = diff_against_sha(&source, &GitSha::new(&base_sha))
|
||||
.await
|
||||
.expect("diff complete repository");
|
||||
assert!(
|
||||
complete_diff.contains("-before") && complete_diff.contains("+after"),
|
||||
"complete-repository diff should remain available:\n{complete_diff}"
|
||||
);
|
||||
|
||||
let source_url = format!("file://{}", source.display());
|
||||
run_git(
|
||||
temp_dir.path(),
|
||||
&[
|
||||
"-c",
|
||||
"protocol.file.allow=always",
|
||||
"clone",
|
||||
"-q",
|
||||
"--no-local",
|
||||
"--filter=blob:none",
|
||||
"--no-checkout",
|
||||
&source_url,
|
||||
clone.to_str().expect("clone path"),
|
||||
],
|
||||
);
|
||||
run_git(&clone, &["checkout", "-q", "main"]);
|
||||
|
||||
let missing = run_git_stdout(
|
||||
&clone,
|
||||
&["rev-list", "--objects", "--all", "--missing=print"],
|
||||
);
|
||||
assert!(
|
||||
missing.lines().any(|line| line == format!("?{base_blob}")),
|
||||
"expected historical blob {base_blob} to remain missing:\n{missing}"
|
||||
);
|
||||
|
||||
let helper = temp_dir.path().join("transport-helper.sh");
|
||||
std::fs::write(&helper, "#!/bin/sh\nprintf ran >\"$0.ran\"\nexit 1\n")
|
||||
.expect("write transport helper");
|
||||
let mut permissions = std::fs::metadata(&helper)
|
||||
.expect("read transport helper metadata")
|
||||
.permissions();
|
||||
permissions.set_mode(/*mode*/ 0o755);
|
||||
std::fs::set_permissions(&helper, permissions).expect("make transport helper executable");
|
||||
let helper_url = format!("ext::{}", helper.display());
|
||||
run_git(&clone, &["config", "remote.origin.url", &helper_url]);
|
||||
run_git(&clone, &["config", "protocol.ext.allow", "always"]);
|
||||
|
||||
let diff = diff_against_sha(&clone, &GitSha::new(&base_sha)).await;
|
||||
|
||||
assert_eq!(
|
||||
(diff, helper.with_extension("sh.ran").exists()),
|
||||
(None, false),
|
||||
"local-only diff must fail without invoking the promisor transport"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonicalize_git_remote_url_normalizes_github_variants() {
|
||||
for remote in [
|
||||
|
||||
@@ -4,6 +4,7 @@ mod branch;
|
||||
mod errors;
|
||||
mod fsmonitor;
|
||||
mod info;
|
||||
mod local_only;
|
||||
mod operations;
|
||||
mod platform;
|
||||
|
||||
@@ -42,4 +43,5 @@ pub use info::git_diff_to_remote;
|
||||
pub use info::local_git_branches;
|
||||
pub use info::recent_commits;
|
||||
pub use info::resolve_root_git_project_for_trust;
|
||||
pub use local_only::local_only_git_env;
|
||||
pub use platform::create_symlink;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
/// Environment overrides for Git commands that must only use local repository data.
|
||||
///
|
||||
/// An empty `GIT_ALLOW_PROTOCOL` list denies every Git transport, including implicit fetches from
|
||||
/// promisor remotes. `GIT_NO_LAZY_FETCH` provides defense in depth on Git versions that support it;
|
||||
/// older versions safely ignore the unknown variable.
|
||||
pub fn local_only_git_env() -> impl Iterator<Item = (&'static str, &'static str)> {
|
||||
[("GIT_ALLOW_PROTOCOL", ""), ("GIT_NO_LAZY_FETCH", "1")].into_iter()
|
||||
}
|
||||
@@ -117,6 +117,7 @@ where
|
||||
command.env(key, value);
|
||||
}
|
||||
}
|
||||
command.envs(crate::local_only_git_env());
|
||||
command.args(&args_vec);
|
||||
let output = command.output()?;
|
||||
if !output.status.success() {
|
||||
|
||||
Reference in New Issue
Block a user