mirror of
https://relay.ngit.dev/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git
synced 2026-10-05 15:08:24 +00:00
test(grasp-audit): route git subprocesses through hermetic git_command
Fixture behaviour and the hard-coded deterministic commit hashes were only stable because the host's git configuration happened to be benign. Empirically, a failing pre-commit hook delivered via core.hooksPath or init.templateDir (including from the XDG config location) breaks every fixture commit, and init.defaultBranch renames branches fixtures rely on; per-site "-c" flags and GIT_TERMINAL_PROMPT covered only fragments of that surface. Add grasp_audit::git_command(), which masks the global and system config files via GIT_CONFIG_GLOBAL/GIT_CONFIG_SYSTEM=/dev/null (requires git >= 2.32; the flake pins 2.54), disables terminal prompts, and clears the GIT_CONFIG_COUNT/GIT_CONFIG_PARAMETERS injection vectors plus inherited GIT_DIR/GIT_WORK_TREE/GIT_INDEX_FILE. Every git invocation in fixtures and specs now goes through it, so an invocation sees only configuration it supplies itself (repo-local config, -c flags, or env vars at the call site). Redundant per-site GIT_TERMINAL_PROMPT pins are dropped. A regression test first proves a deliberately hostile global config (defaultBranch=main, failing hooks via hooksPath and templateDir, gpgsign, autocrlf) breaks a non-hermetic commit, then proves the hermetic recipe still yields DETERMINISTIC_COMMIT_HASH with that config exposed through the process environment. The test waits only on process exit, no sleeps. Excluded scope: ngit-grasp's tests/ adopt the helper separately, and swallowed git exit statuses are fixed separately; behaviour of the relay itself is unchanged. Validated: cargo clippy -p grasp-audit --all-targets -D warnings; cargo test -p grasp-audit --lib (37 passed).
This commit is contained in:
+32
-35
@@ -1314,7 +1314,7 @@ impl<'a> TestContext<'a> {
|
|||||||
|
|
||||||
// -B creates or resets main at HEAD, tolerating a pre-existing local
|
// -B creates or resets main at HEAD, tolerating a pre-existing local
|
||||||
// main (e.g. when the host sets init.defaultBranch=main)
|
// main (e.g. when the host sets init.defaultBranch=main)
|
||||||
let _ = Command::new("git")
|
let _ = git_command()
|
||||||
.args(["checkout", "-B", "main"])
|
.args(["checkout", "-B", "main"])
|
||||||
.current_dir(&clone_path)
|
.current_dir(&clone_path)
|
||||||
.output();
|
.output();
|
||||||
@@ -1416,7 +1416,7 @@ impl<'a> TestContext<'a> {
|
|||||||
// Create or reset main at our deterministic commit and check it out.
|
// Create or reset main at our deterministic commit and check it out.
|
||||||
// -B tolerates a pre-existing local main (e.g. when the host sets
|
// -B tolerates a pre-existing local main (e.g. when the host sets
|
||||||
// init.defaultBranch=main)
|
// init.defaultBranch=main)
|
||||||
let checkout_output = Command::new("git")
|
let checkout_output = git_command()
|
||||||
.args(["checkout", "-B", "main"])
|
.args(["checkout", "-B", "main"])
|
||||||
.current_dir(&clone_path)
|
.current_dir(&clone_path)
|
||||||
.output();
|
.output();
|
||||||
@@ -1553,13 +1553,13 @@ impl<'a> TestContext<'a> {
|
|||||||
|
|
||||||
// Reset to orphan state and create deterministic root commit
|
// Reset to orphan state and create deterministic root commit
|
||||||
// Step 1: Create orphan branch (removes all history)
|
// Step 1: Create orphan branch (removes all history)
|
||||||
let _ = Command::new("git")
|
let _ = git_command()
|
||||||
.args(["checkout", "--orphan", "main-new"])
|
.args(["checkout", "--orphan", "main-new"])
|
||||||
.current_dir(&clone_path)
|
.current_dir(&clone_path)
|
||||||
.output();
|
.output();
|
||||||
|
|
||||||
// Step 2: Clear staged files (orphan keeps files staged from previous branch)
|
// Step 2: Clear staged files (orphan keeps files staged from previous branch)
|
||||||
let _ = Command::new("git")
|
let _ = git_command()
|
||||||
.args(["rm", "-rf", "--cached", "."])
|
.args(["rm", "-rf", "--cached", "."])
|
||||||
.current_dir(&clone_path)
|
.current_dir(&clone_path)
|
||||||
.output();
|
.output();
|
||||||
@@ -1577,12 +1577,12 @@ impl<'a> TestContext<'a> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Step 4: Replace main branch with our new orphan branch
|
// Step 4: Replace main branch with our new orphan branch
|
||||||
let _ = Command::new("git")
|
let _ = git_command()
|
||||||
.args(["branch", "-D", "main"])
|
.args(["branch", "-D", "main"])
|
||||||
.current_dir(&clone_path)
|
.current_dir(&clone_path)
|
||||||
.output();
|
.output();
|
||||||
|
|
||||||
let _ = Command::new("git")
|
let _ = git_command()
|
||||||
.args(["branch", "-m", "main"])
|
.args(["branch", "-m", "main"])
|
||||||
.current_dir(&clone_path)
|
.current_dir(&clone_path)
|
||||||
.output();
|
.output();
|
||||||
@@ -1730,13 +1730,13 @@ impl<'a> TestContext<'a> {
|
|||||||
|
|
||||||
// Reset to orphan state and create deterministic root commit
|
// Reset to orphan state and create deterministic root commit
|
||||||
// Step 1: Create orphan branch (removes all history)
|
// Step 1: Create orphan branch (removes all history)
|
||||||
let _ = Command::new("git")
|
let _ = git_command()
|
||||||
.args(["checkout", "--orphan", "main-new"])
|
.args(["checkout", "--orphan", "main-new"])
|
||||||
.current_dir(&clone_path)
|
.current_dir(&clone_path)
|
||||||
.output();
|
.output();
|
||||||
|
|
||||||
// Step 2: Clear staged files (orphan keeps files staged from previous branch)
|
// Step 2: Clear staged files (orphan keeps files staged from previous branch)
|
||||||
let _ = Command::new("git")
|
let _ = git_command()
|
||||||
.args(["rm", "-rf", "--cached", "."])
|
.args(["rm", "-rf", "--cached", "."])
|
||||||
.current_dir(&clone_path)
|
.current_dir(&clone_path)
|
||||||
.output();
|
.output();
|
||||||
@@ -1757,12 +1757,12 @@ impl<'a> TestContext<'a> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Step 4: Replace main branch with our new orphan branch
|
// Step 4: Replace main branch with our new orphan branch
|
||||||
let _ = Command::new("git")
|
let _ = git_command()
|
||||||
.args(["branch", "-D", "main"])
|
.args(["branch", "-D", "main"])
|
||||||
.current_dir(&clone_path)
|
.current_dir(&clone_path)
|
||||||
.output();
|
.output();
|
||||||
|
|
||||||
let _ = Command::new("git")
|
let _ = git_command()
|
||||||
.args(["branch", "-m", "main"])
|
.args(["branch", "-m", "main"])
|
||||||
.current_dir(&clone_path)
|
.current_dir(&clone_path)
|
||||||
.output();
|
.output();
|
||||||
@@ -1937,12 +1937,12 @@ impl<'a> TestContext<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create master branch if needed and push to refs/nostr/<pr-event-id>
|
// Create master branch if needed and push to refs/nostr/<pr-event-id>
|
||||||
let _ = Command::new("git")
|
let _ = git_command()
|
||||||
.args(["branch", "-M", "master"])
|
.args(["branch", "-M", "master"])
|
||||||
.current_dir(&clone_path)
|
.current_dir(&clone_path)
|
||||||
.output();
|
.output();
|
||||||
|
|
||||||
let push_output = Command::new("git")
|
let push_output = git_command()
|
||||||
.args([
|
.args([
|
||||||
"push",
|
"push",
|
||||||
"origin",
|
"origin",
|
||||||
@@ -2070,13 +2070,13 @@ impl<'a> TestContext<'a> {
|
|||||||
|
|
||||||
// Reset to orphan state and create deterministic root commit
|
// Reset to orphan state and create deterministic root commit
|
||||||
// Step 1: Create orphan branch (removes all history)
|
// Step 1: Create orphan branch (removes all history)
|
||||||
let _ = Command::new("git")
|
let _ = git_command()
|
||||||
.args(["checkout", "--orphan", "pr-branch"])
|
.args(["checkout", "--orphan", "pr-branch"])
|
||||||
.current_dir(&clone_path)
|
.current_dir(&clone_path)
|
||||||
.output();
|
.output();
|
||||||
|
|
||||||
// Step 2: Clear staged files (orphan keeps files staged from previous branch)
|
// Step 2: Clear staged files (orphan keeps files staged from previous branch)
|
||||||
let _ = Command::new("git")
|
let _ = git_command()
|
||||||
.args(["rm", "-rf", "--cached", "."])
|
.args(["rm", "-rf", "--cached", "."])
|
||||||
.current_dir(&clone_path)
|
.current_dir(&clone_path)
|
||||||
.output();
|
.output();
|
||||||
@@ -2112,7 +2112,7 @@ impl<'a> TestContext<'a> {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let push_output = Command::new("git")
|
let push_output = git_command()
|
||||||
.args([
|
.args([
|
||||||
"push",
|
"push",
|
||||||
"origin",
|
"origin",
|
||||||
@@ -2335,9 +2335,9 @@ pub async fn send_and_verify_rejected(
|
|||||||
// Git Operation Helpers
|
// Git Operation Helpers
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
use crate::git_command;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
/// Clone a repository from the relay and return the path
|
/// Clone a repository from the relay and return the path
|
||||||
@@ -2368,9 +2368,8 @@ pub fn clone_repo(relay_domain: &str, npub: &str, repo_id: &str) -> Result<PathB
|
|||||||
let _ = fs::remove_dir_all(&clone_path);
|
let _ = fs::remove_dir_all(&clone_path);
|
||||||
|
|
||||||
let clone_url = format!("http://{}/{}/{}.git", relay_domain, npub, repo_id);
|
let clone_url = format!("http://{}/{}/{}.git", relay_domain, npub, repo_id);
|
||||||
let output = Command::new("git")
|
let output = git_command()
|
||||||
.args(["clone", &clone_url, clone_path.to_str().unwrap()])
|
.args(["clone", &clone_url, clone_path.to_str().unwrap()])
|
||||||
.env("GIT_TERMINAL_PROMPT", "0")
|
|
||||||
.output()
|
.output()
|
||||||
.map_err(|e| format!("Failed to execute git clone: {}", e))?;
|
.map_err(|e| format!("Failed to execute git clone: {}", e))?;
|
||||||
|
|
||||||
@@ -2380,11 +2379,11 @@ pub fn clone_repo(relay_domain: &str, npub: &str, repo_id: &str) -> Result<PathB
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Configure git user
|
// Configure git user
|
||||||
let _ = Command::new("git")
|
let _ = git_command()
|
||||||
.args(["config", "user.email", "test@grasp-audit.local"])
|
.args(["config", "user.email", "test@grasp-audit.local"])
|
||||||
.current_dir(&clone_path)
|
.current_dir(&clone_path)
|
||||||
.output();
|
.output();
|
||||||
let _ = Command::new("git")
|
let _ = git_command()
|
||||||
.args(["config", "user.name", "GRASP Audit Test"])
|
.args(["config", "user.name", "GRASP Audit Test"])
|
||||||
.current_dir(&clone_path)
|
.current_dir(&clone_path)
|
||||||
.output();
|
.output();
|
||||||
@@ -2417,7 +2416,7 @@ pub fn create_commit(clone_path: &Path, message: &str) -> Result<String, String>
|
|||||||
fs::write(&test_file, message).map_err(|e| format!("Failed to write file: {}", e))?;
|
fs::write(&test_file, message).map_err(|e| format!("Failed to write file: {}", e))?;
|
||||||
|
|
||||||
let filename = test_file.file_name().unwrap().to_str().unwrap();
|
let filename = test_file.file_name().unwrap().to_str().unwrap();
|
||||||
let output = Command::new("git")
|
let output = git_command()
|
||||||
.args(["add", filename])
|
.args(["add", filename])
|
||||||
.current_dir(clone_path)
|
.current_dir(clone_path)
|
||||||
.output()
|
.output()
|
||||||
@@ -2427,7 +2426,7 @@ pub fn create_commit(clone_path: &Path, message: &str) -> Result<String, String>
|
|||||||
return Err("Git add failed".to_string());
|
return Err("Git add failed".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
let output = Command::new("git")
|
let output = git_command()
|
||||||
.args(["-c", "commit.gpgsign=false", "commit", "-m", message])
|
.args(["-c", "commit.gpgsign=false", "commit", "-m", message])
|
||||||
.current_dir(clone_path)
|
.current_dir(clone_path)
|
||||||
.output()
|
.output()
|
||||||
@@ -2437,7 +2436,7 @@ pub fn create_commit(clone_path: &Path, message: &str) -> Result<String, String>
|
|||||||
return Err("Git commit failed".to_string());
|
return Err("Git commit failed".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
let output = Command::new("git")
|
let output = git_command()
|
||||||
.args(["rev-parse", "HEAD"])
|
.args(["rev-parse", "HEAD"])
|
||||||
.current_dir(clone_path)
|
.current_dir(clone_path)
|
||||||
.output()
|
.output()
|
||||||
@@ -2514,7 +2513,7 @@ pub fn create_deterministic_commit_with_variant(
|
|||||||
|
|
||||||
fs::write(&test_file, content).map_err(|e| format!("Failed to write file: {}", e))?;
|
fs::write(&test_file, content).map_err(|e| format!("Failed to write file: {}", e))?;
|
||||||
|
|
||||||
let output = Command::new("git")
|
let output = git_command()
|
||||||
.args(["add", "test.txt"])
|
.args(["add", "test.txt"])
|
||||||
.current_dir(clone_path)
|
.current_dir(clone_path)
|
||||||
.output()
|
.output()
|
||||||
@@ -2525,7 +2524,7 @@ pub fn create_deterministic_commit_with_variant(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create deterministic commit with fixed dates and GPG disabled
|
// Create deterministic commit with fixed dates and GPG disabled
|
||||||
let output = Command::new("git")
|
let output = git_command()
|
||||||
.args(["-c", "commit.gpgsign=false", "commit", "-m", message])
|
.args(["-c", "commit.gpgsign=false", "commit", "-m", message])
|
||||||
.env("GIT_AUTHOR_DATE", "2024-01-01T00:00:00Z")
|
.env("GIT_AUTHOR_DATE", "2024-01-01T00:00:00Z")
|
||||||
.env("GIT_COMMITTER_DATE", "2024-01-01T00:00:00Z")
|
.env("GIT_COMMITTER_DATE", "2024-01-01T00:00:00Z")
|
||||||
@@ -2538,7 +2537,7 @@ pub fn create_deterministic_commit_with_variant(
|
|||||||
return Err(format!("Git commit failed: {}", stderr));
|
return Err(format!("Git commit failed: {}", stderr));
|
||||||
}
|
}
|
||||||
|
|
||||||
let output = Command::new("git")
|
let output = git_command()
|
||||||
.args(["rev-parse", "HEAD"])
|
.args(["rev-parse", "HEAD"])
|
||||||
.current_dir(clone_path)
|
.current_dir(clone_path)
|
||||||
.output()
|
.output()
|
||||||
@@ -2594,10 +2593,9 @@ pub fn create_deterministic_commit(clone_path: &Path, _message: &str) -> Result<
|
|||||||
/// # }
|
/// # }
|
||||||
/// ```
|
/// ```
|
||||||
pub fn try_push(clone_path: &Path) -> Result<bool, String> {
|
pub fn try_push(clone_path: &Path) -> Result<bool, String> {
|
||||||
let output = Command::new("git")
|
let output = git_command()
|
||||||
.args(["push", "origin", "main", "--force"])
|
.args(["push", "origin", "main", "--force"])
|
||||||
.current_dir(clone_path)
|
.current_dir(clone_path)
|
||||||
.env("GIT_TERMINAL_PROMPT", "0")
|
|
||||||
.output()
|
.output()
|
||||||
.map_err(|e| format!("Failed to execute git push: {}", e))?;
|
.map_err(|e| format!("Failed to execute git push: {}", e))?;
|
||||||
|
|
||||||
@@ -2632,10 +2630,9 @@ pub fn try_push(clone_path: &Path) -> Result<bool, String> {
|
|||||||
/// # }
|
/// # }
|
||||||
/// ```
|
/// ```
|
||||||
pub fn try_push_to_ref(clone_path: &Path, ref_name: &str) -> Result<bool, String> {
|
pub fn try_push_to_ref(clone_path: &Path, ref_name: &str) -> Result<bool, String> {
|
||||||
let output = Command::new("git")
|
let output = git_command()
|
||||||
.args(["push", "origin", &format!("HEAD:{}", ref_name)])
|
.args(["push", "origin", &format!("HEAD:{}", ref_name)])
|
||||||
.current_dir(clone_path)
|
.current_dir(clone_path)
|
||||||
.env("GIT_TERMINAL_PROMPT", "0")
|
|
||||||
.output()
|
.output()
|
||||||
.map_err(|e| format!("Failed to execute git push: {}", e))?;
|
.map_err(|e| format!("Failed to execute git push: {}", e))?;
|
||||||
|
|
||||||
@@ -2653,7 +2650,7 @@ pub fn try_push_to_ref(clone_path: &Path, ref_name: &str) -> Result<bool, String
|
|||||||
/// * `remote_url` - The remote URL to add as `origin`
|
/// * `remote_url` - The remote URL to add as `origin`
|
||||||
pub fn init_local_repo(path: &Path, remote_url: &str) -> Result<(), String> {
|
pub fn init_local_repo(path: &Path, remote_url: &str) -> Result<(), String> {
|
||||||
// Step 1: git init <path>
|
// Step 1: git init <path>
|
||||||
let output = Command::new("git")
|
let output = git_command()
|
||||||
.args(["init", path.to_str().unwrap_or(".")])
|
.args(["init", path.to_str().unwrap_or(".")])
|
||||||
.output()
|
.output()
|
||||||
.map_err(|e| format!("Failed to execute git init: {}", e))?;
|
.map_err(|e| format!("Failed to execute git init: {}", e))?;
|
||||||
@@ -2664,7 +2661,7 @@ pub fn init_local_repo(path: &Path, remote_url: &str) -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: git config user.email
|
// Step 2: git config user.email
|
||||||
let output = Command::new("git")
|
let output = git_command()
|
||||||
.args(["config", "user.email", "probe@grasp-audit.local"])
|
.args(["config", "user.email", "probe@grasp-audit.local"])
|
||||||
.current_dir(path)
|
.current_dir(path)
|
||||||
.output()
|
.output()
|
||||||
@@ -2676,7 +2673,7 @@ pub fn init_local_repo(path: &Path, remote_url: &str) -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Step 3: git config user.name
|
// Step 3: git config user.name
|
||||||
let output = Command::new("git")
|
let output = git_command()
|
||||||
.args(["config", "user.name", "GRASP Probe"])
|
.args(["config", "user.name", "GRASP Probe"])
|
||||||
.current_dir(path)
|
.current_dir(path)
|
||||||
.output()
|
.output()
|
||||||
@@ -2688,7 +2685,7 @@ pub fn init_local_repo(path: &Path, remote_url: &str) -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Step 4: git symbolic-ref HEAD refs/heads/main (sets default branch to main)
|
// Step 4: git symbolic-ref HEAD refs/heads/main (sets default branch to main)
|
||||||
let output = Command::new("git")
|
let output = git_command()
|
||||||
.args(["symbolic-ref", "HEAD", "refs/heads/main"])
|
.args(["symbolic-ref", "HEAD", "refs/heads/main"])
|
||||||
.current_dir(path)
|
.current_dir(path)
|
||||||
.output()
|
.output()
|
||||||
@@ -2700,7 +2697,7 @@ pub fn init_local_repo(path: &Path, remote_url: &str) -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Step 5: git remote add origin <remote_url>
|
// Step 5: git remote add origin <remote_url>
|
||||||
let output = Command::new("git")
|
let output = git_command()
|
||||||
.args(["remote", "add", "origin", remote_url])
|
.args(["remote", "add", "origin", remote_url])
|
||||||
.current_dir(path)
|
.current_dir(path)
|
||||||
.output()
|
.output()
|
||||||
|
|||||||
@@ -0,0 +1,204 @@
|
|||||||
|
//! Hermetic git subprocess construction.
|
||||||
|
//!
|
||||||
|
//! Fixtures and tests spawn many `git` subprocesses and several of them feed
|
||||||
|
//! hard-coded deterministic commit hashes. Those hashes (and the surrounding
|
||||||
|
//! control flow) are only meaningful if every input git reads is supplied by
|
||||||
|
//! the fixture itself rather than inherited from whatever the host happens to
|
||||||
|
//! have configured.
|
||||||
|
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
/// Build a `git` [`Command`] that is hermetic with respect to ambient git
|
||||||
|
/// configuration.
|
||||||
|
///
|
||||||
|
/// Ambient configuration — the system `/etc/gitconfig`, the user's
|
||||||
|
/// `~/.gitconfig` or `$XDG_CONFIG_HOME/git/config`, and config injected via
|
||||||
|
/// the `GIT_CONFIG_COUNT` / `GIT_CONFIG_PARAMETERS` environment — can change
|
||||||
|
/// fixture behaviour: a failing hook reached through `core.hooksPath` or
|
||||||
|
/// `init.templateDir` breaks every commit, `init.defaultBranch` renames
|
||||||
|
/// branches fixtures rely on, and credential helpers can stall network
|
||||||
|
/// operations waiting for input.
|
||||||
|
///
|
||||||
|
/// Every git subprocess spawned by fixtures or tests must be built through
|
||||||
|
/// this helper. The invocation then sees only configuration it supplies
|
||||||
|
/// itself: repo-local `git config`, `-c` flags, or `GIT_*` environment
|
||||||
|
/// variables set at the call site.
|
||||||
|
///
|
||||||
|
/// Requires git >= 2.32 (June 2021) for `GIT_CONFIG_GLOBAL` /
|
||||||
|
/// `GIT_CONFIG_SYSTEM`; the flake pins a far newer git.
|
||||||
|
pub fn git_command() -> Command {
|
||||||
|
let mut cmd = Command::new("git");
|
||||||
|
// Mask the system config and the user-global config in both of its
|
||||||
|
// locations (~/.gitconfig and $XDG_CONFIG_HOME/git/config).
|
||||||
|
cmd.env("GIT_CONFIG_GLOBAL", "/dev/null");
|
||||||
|
cmd.env("GIT_CONFIG_SYSTEM", "/dev/null");
|
||||||
|
// Fail fast instead of stalling the suite on a credential prompt.
|
||||||
|
cmd.env("GIT_TERMINAL_PROMPT", "0");
|
||||||
|
// Config can also be injected through the environment. GIT_CONFIG_KEY_n /
|
||||||
|
// GIT_CONFIG_VALUE_n are only honoured while GIT_CONFIG_COUNT is set, so
|
||||||
|
// removing the count neutralises the whole family.
|
||||||
|
cmd.env_remove("GIT_CONFIG_COUNT");
|
||||||
|
cmd.env_remove("GIT_CONFIG_PARAMETERS");
|
||||||
|
// Repository discovery must come from `current_dir`, not from a parent
|
||||||
|
// process that happens to run inside a git hook, alias, or rebase.
|
||||||
|
cmd.env_remove("GIT_DIR");
|
||||||
|
cmd.env_remove("GIT_WORK_TREE");
|
||||||
|
cmd.env_remove("GIT_INDEX_FILE");
|
||||||
|
cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::fixtures::{
|
||||||
|
create_deterministic_commit_with_variant, CommitVariant, DETERMINISTIC_COMMIT_HASH,
|
||||||
|
};
|
||||||
|
use std::fs;
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
/// Write a global gitconfig whose settings demonstrably break the fixture
|
||||||
|
/// recipe when they leak in: `core.hooksPath` and `init.templateDir` both
|
||||||
|
/// deliver a pre-commit hook that always fails, `init.defaultBranch`
|
||||||
|
/// renames the initial branch, and gpgsign/autocrlf cover the remaining
|
||||||
|
/// commit inputs.
|
||||||
|
fn write_hostile_gitconfig(dir: &Path) -> PathBuf {
|
||||||
|
let hooks = dir.join("hooks");
|
||||||
|
fs::create_dir_all(&hooks).expect("create hooks dir");
|
||||||
|
let hook = hooks.join("pre-commit");
|
||||||
|
fs::write(
|
||||||
|
&hook,
|
||||||
|
"#!/bin/sh\necho 'hostile ambient git config leaked into a fixture' >&2\nexit 1\n",
|
||||||
|
)
|
||||||
|
.expect("write pre-commit hook");
|
||||||
|
fs::set_permissions(&hook, fs::Permissions::from_mode(0o755)).expect("chmod hook");
|
||||||
|
|
||||||
|
let template_hooks = dir.join("template").join("hooks");
|
||||||
|
fs::create_dir_all(&template_hooks).expect("create template hooks dir");
|
||||||
|
fs::copy(&hook, template_hooks.join("pre-commit")).expect("copy hook into template");
|
||||||
|
|
||||||
|
let config = dir.join("gitconfig");
|
||||||
|
fs::write(
|
||||||
|
&config,
|
||||||
|
format!(
|
||||||
|
"[init]\n\
|
||||||
|
\tdefaultBranch = main\n\
|
||||||
|
\ttemplateDir = {template}\n\
|
||||||
|
[core]\n\
|
||||||
|
\thooksPath = {hooks}\n\
|
||||||
|
\tautocrlf = true\n\
|
||||||
|
[commit]\n\
|
||||||
|
\tgpgsign = true\n",
|
||||||
|
template = dir.join("template").display(),
|
||||||
|
hooks = hooks.display(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.expect("write hostile gitconfig");
|
||||||
|
config
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Restores the mutated process environment even if the test panics.
|
||||||
|
struct EnvGuard {
|
||||||
|
saved: Vec<(&'static str, Option<std::ffi::OsString>)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EnvGuard {
|
||||||
|
fn set(vars: &[(&'static str, &Path)]) -> Self {
|
||||||
|
let saved = vars
|
||||||
|
.iter()
|
||||||
|
.map(|(key, value)| {
|
||||||
|
let previous = std::env::var_os(key);
|
||||||
|
std::env::set_var(key, value);
|
||||||
|
(*key, previous)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Self { saved }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for EnvGuard {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
for (key, previous) in self.saved.drain(..) {
|
||||||
|
match previous {
|
||||||
|
Some(value) => std::env::set_var(key, value),
|
||||||
|
None => std::env::remove_var(key),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_repo_with_identity(git: impl Fn() -> Command, repo: &Path) {
|
||||||
|
let init = git()
|
||||||
|
.args(["init", repo.to_str().expect("utf-8 repo path")])
|
||||||
|
.output()
|
||||||
|
.expect("spawn git init");
|
||||||
|
assert!(
|
||||||
|
init.status.success(),
|
||||||
|
"git init failed: {}",
|
||||||
|
String::from_utf8_lossy(&init.stderr)
|
||||||
|
);
|
||||||
|
for (key, value) in [
|
||||||
|
("user.email", "test@grasp-audit.local"),
|
||||||
|
("user.name", "GRASP Audit Test"),
|
||||||
|
] {
|
||||||
|
let config = git()
|
||||||
|
.args(["config", key, value])
|
||||||
|
.current_dir(repo)
|
||||||
|
.output()
|
||||||
|
.expect("spawn git config");
|
||||||
|
assert!(
|
||||||
|
config.status.success(),
|
||||||
|
"git config {key} failed: {}",
|
||||||
|
String::from_utf8_lossy(&config.stderr)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Regression test for hermeticity: the deterministic commit fixture must
|
||||||
|
/// produce its pinned hash even when the surrounding process advertises a
|
||||||
|
/// hostile git configuration.
|
||||||
|
///
|
||||||
|
/// The test first proves the hostile configuration is potent (a plain git
|
||||||
|
/// invocation honouring it cannot commit at all), then proves
|
||||||
|
/// [`git_command`]-based fixtures neutralise it.
|
||||||
|
#[test]
|
||||||
|
fn deterministic_commit_survives_hostile_ambient_git_config() {
|
||||||
|
let dir = tempfile::tempdir().expect("create temp dir");
|
||||||
|
let hostile_config = write_hostile_gitconfig(dir.path());
|
||||||
|
|
||||||
|
// Potency check: a non-hermetic git honouring the hostile config must
|
||||||
|
// fail to commit (the pre-commit hook fires). If this stops failing,
|
||||||
|
// the hostile config has rotted and no longer guards anything.
|
||||||
|
let raw_repo = dir.path().join("raw");
|
||||||
|
let raw_git = || {
|
||||||
|
let mut cmd = Command::new("git");
|
||||||
|
cmd.env("GIT_CONFIG_GLOBAL", &hostile_config);
|
||||||
|
cmd.env("GIT_CONFIG_SYSTEM", "/dev/null");
|
||||||
|
cmd
|
||||||
|
};
|
||||||
|
init_repo_with_identity(raw_git, &raw_repo);
|
||||||
|
let err = create_deterministic_commit_with_variant(&raw_repo, CommitVariant::Owner)
|
||||||
|
.expect_err("hostile config should break a commit that inherits it");
|
||||||
|
assert!(
|
||||||
|
err.contains("hostile ambient git config leaked"),
|
||||||
|
"expected the hostile hook to fire, got: {err}"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Hermetic check: with the hostile config exposed the way a real host
|
||||||
|
// exposes it (global/system config paths in the environment), the
|
||||||
|
// fixture recipe must still produce the pinned deterministic hash.
|
||||||
|
let _guard = EnvGuard::set(&[
|
||||||
|
("HOME", dir.path()),
|
||||||
|
("GIT_CONFIG_GLOBAL", &hostile_config),
|
||||||
|
("GIT_CONFIG_SYSTEM", &hostile_config),
|
||||||
|
]);
|
||||||
|
let hermetic_repo = dir.path().join("hermetic");
|
||||||
|
init_repo_with_identity(git_command, &hermetic_repo);
|
||||||
|
let hash = create_deterministic_commit_with_variant(&hermetic_repo, CommitVariant::Owner)
|
||||||
|
.expect("hermetic fixture commit should succeed under hostile ambient config");
|
||||||
|
assert_eq!(
|
||||||
|
hash, DETERMINISTIC_COMMIT_HASH,
|
||||||
|
"deterministic hash must not depend on ambient git configuration"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,6 +31,7 @@
|
|||||||
pub mod audit;
|
pub mod audit;
|
||||||
pub mod client;
|
pub mod client;
|
||||||
pub mod fixtures;
|
pub mod fixtures;
|
||||||
|
pub mod git;
|
||||||
pub mod isolation;
|
pub mod isolation;
|
||||||
pub mod probe;
|
pub mod probe;
|
||||||
pub mod result;
|
pub mod result;
|
||||||
@@ -62,6 +63,7 @@ pub use fixtures::{
|
|||||||
PR_TEST_COMMIT_HASH,
|
PR_TEST_COMMIT_HASH,
|
||||||
RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH,
|
RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH,
|
||||||
};
|
};
|
||||||
|
pub use git::git_command;
|
||||||
pub use probe::{run_probe, ProbeCheck, ProbeReport};
|
pub use probe::{run_probe, ProbeCheck, ProbeReport};
|
||||||
pub use result::{AuditResult, TestResult};
|
pub use result::{AuditResult, TestResult};
|
||||||
|
|
||||||
|
|||||||
@@ -15,11 +15,11 @@
|
|||||||
//! cd grasp-audit && nix develop -c bash test-ngit-relay.sh --mode test
|
//! cd grasp-audit && nix develop -c bash test-ngit-relay.sh --mode test
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
|
use crate::git_command;
|
||||||
use crate::specs::grasp01::SpecRef;
|
use crate::specs::grasp01::SpecRef;
|
||||||
use crate::{AuditClient, FixtureKind, TestContext, TestResult};
|
use crate::{AuditClient, FixtureKind, TestContext, TestResult};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::process::Command;
|
|
||||||
|
|
||||||
/// Test suite for Git clone operations
|
/// Test suite for Git clone operations
|
||||||
pub struct GitCloneTests;
|
pub struct GitCloneTests;
|
||||||
@@ -106,7 +106,7 @@ impl GitCloneTests {
|
|||||||
let clone_url = format!("http://{}/{}/{}.git", relay_domain, npub, repo_id);
|
let clone_url = format!("http://{}/{}/{}.git", relay_domain, npub, repo_id);
|
||||||
|
|
||||||
// Attempt to clone the repository
|
// Attempt to clone the repository
|
||||||
let output = Command::new("git")
|
let output = git_command()
|
||||||
.args(["clone", &clone_url, clone_path.to_str().unwrap()])
|
.args(["clone", &clone_url, clone_path.to_str().unwrap()])
|
||||||
.env("GIT_TERMINAL_PROMPT", "0") // Disable password prompts
|
.env("GIT_TERMINAL_PROMPT", "0") // Disable password prompts
|
||||||
.output();
|
.output();
|
||||||
@@ -229,9 +229,8 @@ impl GitCloneTests {
|
|||||||
|
|
||||||
let invalid_url = format!("http://{}/invalid/path", relay_domain);
|
let invalid_url = format!("http://{}/invalid/path", relay_domain);
|
||||||
|
|
||||||
let output = Command::new("git")
|
let output = git_command()
|
||||||
.args(["clone", &invalid_url, clone_path.to_str().unwrap()])
|
.args(["clone", &invalid_url, clone_path.to_str().unwrap()])
|
||||||
.env("GIT_TERMINAL_PROMPT", "0")
|
|
||||||
.output()
|
.output()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
|||||||
@@ -22,11 +22,11 @@
|
|||||||
//! cd grasp-audit && nix develop -c bash test-ngit-relay.sh --mode test
|
//! cd grasp-audit && nix develop -c bash test-ngit-relay.sh --mode test
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
|
use crate::git_command;
|
||||||
use crate::specs::grasp01::SpecRef;
|
use crate::specs::grasp01::SpecRef;
|
||||||
use crate::{AuditClient, FixtureKind, TestContext, TestResult};
|
use crate::{AuditClient, FixtureKind, TestContext, TestResult};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::process::Command;
|
|
||||||
|
|
||||||
/// Test suite for Git filter capability operations
|
/// Test suite for Git filter capability operations
|
||||||
pub struct GitFilterTests;
|
pub struct GitFilterTests;
|
||||||
@@ -223,14 +223,13 @@ impl GitFilterTests {
|
|||||||
let clone_url = format!("http://{}/{}/{}.git", relay_domain, npub, repo_id);
|
let clone_url = format!("http://{}/{}/{}.git", relay_domain, npub, repo_id);
|
||||||
|
|
||||||
// Attempt filtered clone with blob:none
|
// Attempt filtered clone with blob:none
|
||||||
let output = Command::new("git")
|
let output = git_command()
|
||||||
.args([
|
.args([
|
||||||
"clone",
|
"clone",
|
||||||
"--filter=blob:none",
|
"--filter=blob:none",
|
||||||
&clone_url,
|
&clone_url,
|
||||||
clone_path.to_str().unwrap(),
|
clone_path.to_str().unwrap(),
|
||||||
])
|
])
|
||||||
.env("GIT_TERMINAL_PROMPT", "0")
|
|
||||||
.output();
|
.output();
|
||||||
|
|
||||||
// Clean up
|
// Clean up
|
||||||
@@ -334,14 +333,13 @@ impl GitFilterTests {
|
|||||||
let clone_url = format!("http://{}/{}/{}.git", relay_domain, npub, repo_id);
|
let clone_url = format!("http://{}/{}/{}.git", relay_domain, npub, repo_id);
|
||||||
|
|
||||||
// First do a shallow clone to have a repository to fetch into
|
// First do a shallow clone to have a repository to fetch into
|
||||||
let clone_output = Command::new("git")
|
let clone_output = git_command()
|
||||||
.args([
|
.args([
|
||||||
"clone",
|
"clone",
|
||||||
"--depth=1",
|
"--depth=1",
|
||||||
&clone_url,
|
&clone_url,
|
||||||
clone_path.to_str().unwrap(),
|
clone_path.to_str().unwrap(),
|
||||||
])
|
])
|
||||||
.env("GIT_TERMINAL_PROMPT", "0")
|
|
||||||
.output();
|
.output();
|
||||||
|
|
||||||
// Clean up
|
// Clean up
|
||||||
@@ -360,10 +358,9 @@ impl GitFilterTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Now attempt a filtered fetch
|
// Now attempt a filtered fetch
|
||||||
let output = Command::new("git")
|
let output = git_command()
|
||||||
.args(["fetch", "--filter=tree:0", "origin"])
|
.args(["fetch", "--filter=tree:0", "origin"])
|
||||||
.current_dir(&clone_path)
|
.current_dir(&clone_path)
|
||||||
.env("GIT_TERMINAL_PROMPT", "0")
|
|
||||||
.output();
|
.output();
|
||||||
|
|
||||||
let output = match output {
|
let output = match output {
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
const PR_TEST_COMMIT_HASH: &str = "5a51b30e4615b572dcd5b9e487861b58605a5c21";
|
const PR_TEST_COMMIT_HASH: &str = "5a51b30e4615b572dcd5b9e487861b58605a5c21";
|
||||||
|
|
||||||
|
use crate::git_command;
|
||||||
use crate::specs::grasp01::SpecRef;
|
use crate::specs::grasp01::SpecRef;
|
||||||
use crate::{
|
use crate::{
|
||||||
clone_repo, create_commit, create_deterministic_commit_with_variant, try_push, try_push_to_ref,
|
clone_repo, create_commit, create_deterministic_commit_with_variant, try_push, try_push_to_ref,
|
||||||
@@ -40,7 +41,6 @@ use crate::{
|
|||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -66,13 +66,13 @@ use std::time::Duration;
|
|||||||
fn create_pr_test_commit(clone_path: &Path) -> Result<String, String> {
|
fn create_pr_test_commit(clone_path: &Path) -> Result<String, String> {
|
||||||
// Step 1: Clean up any tracked files in the working directory
|
// Step 1: Clean up any tracked files in the working directory
|
||||||
// This ensures we start with a clean slate
|
// This ensures we start with a clean slate
|
||||||
let _ = Command::new("git")
|
let _ = git_command()
|
||||||
.args(["clean", "-fd"])
|
.args(["clean", "-fd"])
|
||||||
.current_dir(clone_path)
|
.current_dir(clone_path)
|
||||||
.output();
|
.output();
|
||||||
|
|
||||||
// Step 2: Create orphan branch (removes all history)
|
// Step 2: Create orphan branch (removes all history)
|
||||||
let output = Command::new("git")
|
let output = git_command()
|
||||||
.args(["checkout", "--orphan", "pr-test-branch"])
|
.args(["checkout", "--orphan", "pr-test-branch"])
|
||||||
.current_dir(clone_path)
|
.current_dir(clone_path)
|
||||||
.output()
|
.output()
|
||||||
@@ -86,7 +86,7 @@ fn create_pr_test_commit(clone_path: &Path) -> Result<String, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Step 3: Remove ALL files from the index (staging area)
|
// Step 3: Remove ALL files from the index (staging area)
|
||||||
let output = Command::new("git")
|
let output = git_command()
|
||||||
.args(["rm", "-rf", "--cached", "."])
|
.args(["rm", "-rf", "--cached", "."])
|
||||||
.current_dir(clone_path)
|
.current_dir(clone_path)
|
||||||
.output()
|
.output()
|
||||||
@@ -123,7 +123,7 @@ fn create_pr_test_commit(clone_path: &Path) -> Result<String, String> {
|
|||||||
create_deterministic_commit_with_variant(clone_path, CommitVariant::PRTestCommit)?;
|
create_deterministic_commit_with_variant(clone_path, CommitVariant::PRTestCommit)?;
|
||||||
|
|
||||||
// Step 6: Verify this is actually a root commit (no parent)
|
// Step 6: Verify this is actually a root commit (no parent)
|
||||||
let output = Command::new("git")
|
let output = git_command()
|
||||||
.args(["rev-list", "--max-parents=0", "HEAD"])
|
.args(["rev-list", "--max-parents=0", "HEAD"])
|
||||||
.current_dir(clone_path)
|
.current_dir(clone_path)
|
||||||
.output()
|
.output()
|
||||||
@@ -139,12 +139,12 @@ fn create_pr_test_commit(clone_path: &Path) -> Result<String, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Step 7: Replace main branch with our new orphan branch
|
// Step 7: Replace main branch with our new orphan branch
|
||||||
let _ = Command::new("git")
|
let _ = git_command()
|
||||||
.args(["branch", "-D", "main"])
|
.args(["branch", "-D", "main"])
|
||||||
.current_dir(clone_path)
|
.current_dir(clone_path)
|
||||||
.output();
|
.output();
|
||||||
|
|
||||||
let output = Command::new("git")
|
let output = git_command()
|
||||||
.args(["branch", "-m", "main"])
|
.args(["branch", "-m", "main"])
|
||||||
.current_dir(clone_path)
|
.current_dir(clone_path)
|
||||||
.output()
|
.output()
|
||||||
@@ -160,7 +160,7 @@ fn create_pr_test_commit(clone_path: &Path) -> Result<String, String> {
|
|||||||
// Step 8: Verify commit hash matches expected
|
// Step 8: Verify commit hash matches expected
|
||||||
if commit_hash != PR_TEST_COMMIT_HASH {
|
if commit_hash != PR_TEST_COMMIT_HASH {
|
||||||
// Debug: Show what's in the commit
|
// Debug: Show what's in the commit
|
||||||
let tree_output = Command::new("git")
|
let tree_output = git_command()
|
||||||
.args(["ls-tree", "-r", "HEAD"])
|
.args(["ls-tree", "-r", "HEAD"])
|
||||||
.current_dir(clone_path)
|
.current_dir(clone_path)
|
||||||
.output();
|
.output();
|
||||||
@@ -168,7 +168,7 @@ fn create_pr_test_commit(clone_path: &Path) -> Result<String, String> {
|
|||||||
.map(|o| String::from_utf8_lossy(&o.stdout).to_string())
|
.map(|o| String::from_utf8_lossy(&o.stdout).to_string())
|
||||||
.unwrap_or_else(|_| "Failed to get tree".to_string());
|
.unwrap_or_else(|_| "Failed to get tree".to_string());
|
||||||
|
|
||||||
let cat_output = Command::new("git")
|
let cat_output = git_command()
|
||||||
.args(["cat-file", "-p", "HEAD"])
|
.args(["cat-file", "-p", "HEAD"])
|
||||||
.current_dir(clone_path)
|
.current_dir(clone_path)
|
||||||
.output();
|
.output();
|
||||||
@@ -254,7 +254,7 @@ fn reset_to_correct_pr_commit(clone_path: &Path) -> Result<String, String> {
|
|||||||
/// Returns Ok(true) if push succeeded, Ok(false) if rejected, Err on git error.
|
/// Returns Ok(true) if push succeeded, Ok(false) if rejected, Err on git error.
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
fn push_to_pr_ref(clone_path: &Path, pr_event_id: &str) -> Result<bool, String> {
|
fn push_to_pr_ref(clone_path: &Path, pr_event_id: &str) -> Result<bool, String> {
|
||||||
let push_output = Command::new("git")
|
let push_output = git_command()
|
||||||
.args([
|
.args([
|
||||||
"push",
|
"push",
|
||||||
"--force",
|
"--force",
|
||||||
@@ -332,7 +332,7 @@ async fn get_default_branch_from_info_refs(
|
|||||||
/// Checks if a ref exists on the remote.
|
/// Checks if a ref exists on the remote.
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
fn ref_exists_on_remote(clone_path: &Path, ref_name: &str) -> Result<bool, String> {
|
fn ref_exists_on_remote(clone_path: &Path, ref_name: &str) -> Result<bool, String> {
|
||||||
let output = Command::new("git")
|
let output = git_command()
|
||||||
.args(["ls-remote", "origin", ref_name])
|
.args(["ls-remote", "origin", ref_name])
|
||||||
.current_dir(clone_path)
|
.current_dir(clone_path)
|
||||||
.output()
|
.output()
|
||||||
@@ -541,8 +541,6 @@ impl PushAuthorizationTests {
|
|||||||
client: &AuditClient,
|
client: &AuditClient,
|
||||||
relay_domain: &str,
|
relay_domain: &str,
|
||||||
) -> TestResult {
|
) -> TestResult {
|
||||||
use std::process::Command;
|
|
||||||
|
|
||||||
let test_name = "test_push_rejected_wrong_commit";
|
let test_name = "test_push_rejected_wrong_commit";
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -617,7 +615,7 @@ impl PushAuthorizationTests {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Create/checkout main branch
|
// Create/checkout main branch
|
||||||
let branch_output = Command::new("git")
|
let branch_output = git_command()
|
||||||
.args(["checkout", "-B", "main"])
|
.args(["checkout", "-B", "main"])
|
||||||
.current_dir(&clone_path)
|
.current_dir(&clone_path)
|
||||||
.output();
|
.output();
|
||||||
@@ -1568,7 +1566,7 @@ impl PushAuthorizationTests {
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
// Step 4: Create and checkout develop1 branch, then create unique commit
|
// Step 4: Create and checkout develop1 branch, then create unique commit
|
||||||
// ============================================================
|
// ============================================================
|
||||||
let output = Command::new("git")
|
let output = git_command()
|
||||||
.args(["checkout", "-b", "develop1"])
|
.args(["checkout", "-b", "develop1"])
|
||||||
.current_dir(&clone_path)
|
.current_dir(&clone_path)
|
||||||
.output();
|
.output();
|
||||||
@@ -1683,14 +1681,13 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_pr_test_commit_hash_discovery() {
|
fn test_pr_test_commit_hash_discovery() {
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::process::Command;
|
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|
||||||
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
||||||
let path = temp_dir.path();
|
let path = temp_dir.path();
|
||||||
|
|
||||||
// Initialize git repo
|
// Initialize git repo
|
||||||
let output = Command::new("git")
|
let output = git_command()
|
||||||
.args(["init"])
|
.args(["init"])
|
||||||
.current_dir(path)
|
.current_dir(path)
|
||||||
.output()
|
.output()
|
||||||
@@ -1702,14 +1699,14 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Configure git user - use same identity as clone_repo in fixtures.rs
|
// Configure git user - use same identity as clone_repo in fixtures.rs
|
||||||
let output = Command::new("git")
|
let output = git_command()
|
||||||
.args(["config", "user.email", "test@grasp-audit.local"])
|
.args(["config", "user.email", "test@grasp-audit.local"])
|
||||||
.current_dir(path)
|
.current_dir(path)
|
||||||
.output()
|
.output()
|
||||||
.expect("git config email failed");
|
.expect("git config email failed");
|
||||||
assert!(output.status.success(), "git config email failed");
|
assert!(output.status.success(), "git config email failed");
|
||||||
|
|
||||||
let output = Command::new("git")
|
let output = git_command()
|
||||||
.args(["config", "user.name", "GRASP Audit Test"])
|
.args(["config", "user.name", "GRASP Audit Test"])
|
||||||
.current_dir(path)
|
.current_dir(path)
|
||||||
.output()
|
.output()
|
||||||
@@ -1721,7 +1718,7 @@ mod tests {
|
|||||||
fs::write(&test_file, "PR test deterministic commit\n").expect("Failed to write test file");
|
fs::write(&test_file, "PR test deterministic commit\n").expect("Failed to write test file");
|
||||||
|
|
||||||
// Add the file
|
// Add the file
|
||||||
let output = Command::new("git")
|
let output = git_command()
|
||||||
.args(["add", "test.txt"])
|
.args(["add", "test.txt"])
|
||||||
.current_dir(path)
|
.current_dir(path)
|
||||||
.output()
|
.output()
|
||||||
@@ -1733,7 +1730,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Create deterministic commit with fixed dates and GPG disabled
|
// Create deterministic commit with fixed dates and GPG disabled
|
||||||
let output = Command::new("git")
|
let output = git_command()
|
||||||
.args([
|
.args([
|
||||||
"-c",
|
"-c",
|
||||||
"commit.gpgsign=false",
|
"commit.gpgsign=false",
|
||||||
@@ -1753,7 +1750,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Get the commit hash
|
// Get the commit hash
|
||||||
let output = Command::new("git")
|
let output = git_command()
|
||||||
.args(["rev-parse", "HEAD"])
|
.args(["rev-parse", "HEAD"])
|
||||||
.current_dir(path)
|
.current_dir(path)
|
||||||
.output()
|
.output()
|
||||||
|
|||||||
@@ -48,6 +48,7 @@
|
|||||||
//! sleep before the ref-mirror probe — keeps the total wait close to
|
//! sleep before the ref-mirror probe — keeps the total wait close to
|
||||||
//! the original 2 s while making the two failure modes attributable.
|
//! the original 2 s while making the two failure modes attributable.
|
||||||
|
|
||||||
|
use crate::git_command;
|
||||||
use crate::specs::grasp06::SpecRef;
|
use crate::specs::grasp06::SpecRef;
|
||||||
use crate::{
|
use crate::{
|
||||||
create_commit, init_local_repo, try_push_to_ref, AuditClient, FixtureKind, TestContext,
|
create_commit, init_local_repo, try_push_to_ref, AuditClient, FixtureKind, TestContext,
|
||||||
@@ -56,7 +57,6 @@ use crate::{
|
|||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
pub struct MirroringTests;
|
pub struct MirroringTests;
|
||||||
@@ -619,9 +619,8 @@ impl MirroringTests {
|
|||||||
// had ample time to run. A negative result here is a real
|
// had ample time to run. A negative result here is a real
|
||||||
// "didn't happen, won't happen" signal.
|
// "didn't happen, won't happen" signal.
|
||||||
let refname = format!("refs/nostr/{}", pr_event_id);
|
let refname = format!("refs/nostr/{}", pr_event_id);
|
||||||
let out = Command::new("git")
|
let out = git_command()
|
||||||
.args(["ls-remote", &prs_url, &refname])
|
.args(["ls-remote", &prs_url, &refname])
|
||||||
.env("GIT_TERMINAL_PROMPT", "0")
|
|
||||||
.output()
|
.output()
|
||||||
.map_err(|e| format!("Failed to execute git ls-remote {}: {}", prs_url, e))?;
|
.map_err(|e| format!("Failed to execute git ls-remote {}: {}", prs_url, e))?;
|
||||||
|
|
||||||
@@ -707,9 +706,5 @@ impl Drop for TempPath {
|
|||||||
/// Run a `git` command in `cwd` and return the [`std::process::Output`]
|
/// Run a `git` command in `cwd` and return the [`std::process::Output`]
|
||||||
/// unchanged. The caller decides whether non-zero exit is failure.
|
/// unchanged. The caller decides whether non-zero exit is failure.
|
||||||
fn run_git(cwd: &Path, args: &[&str]) -> std::io::Result<std::process::Output> {
|
fn run_git(cwd: &Path, args: &[&str]) -> std::io::Result<std::process::Output> {
|
||||||
Command::new("git")
|
git_command().args(args).current_dir(cwd).output()
|
||||||
.args(args)
|
|
||||||
.current_dir(cwd)
|
|
||||||
.env("GIT_TERMINAL_PROMPT", "0")
|
|
||||||
.output()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
//! Each test here maps 1:1 to a MUST in the spec, or to an audit-derived
|
//! Each test here maps 1:1 to a MUST in the spec, or to an audit-derived
|
||||||
//! invariant that follows directly from NIP-11 discovery semantics.
|
//! invariant that follows directly from NIP-11 discovery semantics.
|
||||||
|
|
||||||
|
use crate::git_command;
|
||||||
use crate::specs::grasp06::fixtures::advertises_grasp;
|
use crate::specs::grasp06::fixtures::advertises_grasp;
|
||||||
use crate::specs::grasp06::SpecRef;
|
use crate::specs::grasp06::SpecRef;
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -13,7 +14,6 @@ use crate::{
|
|||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
pub struct PrsEndpointTests;
|
pub struct PrsEndpointTests;
|
||||||
@@ -164,9 +164,8 @@ impl PrsEndpointTests {
|
|||||||
let _ = fs::remove_dir_all(&clone_path);
|
let _ = fs::remove_dir_all(&clone_path);
|
||||||
};
|
};
|
||||||
|
|
||||||
let clone_output = Command::new("git")
|
let clone_output = git_command()
|
||||||
.args(["clone", &clone_url, clone_path.to_str().unwrap()])
|
.args(["clone", &clone_url, clone_path.to_str().unwrap()])
|
||||||
.env("GIT_TERMINAL_PROMPT", "0")
|
|
||||||
.output();
|
.output();
|
||||||
|
|
||||||
let clone_output = match clone_output {
|
let clone_output = match clone_output {
|
||||||
@@ -186,7 +185,7 @@ impl PrsEndpointTests {
|
|||||||
// 4. Verify the cloned repo has zero refs. `for-each-ref` lists
|
// 4. Verify the cloned repo has zero refs. `for-each-ref` lists
|
||||||
// every ref one-per-line; empty stdout means no refs at all,
|
// every ref one-per-line; empty stdout means no refs at all,
|
||||||
// which is the spec's "empty bare repository" invariant.
|
// which is the spec's "empty bare repository" invariant.
|
||||||
let refs_output = Command::new("git")
|
let refs_output = git_command()
|
||||||
.args(["-C", clone_path.to_str().unwrap(), "for-each-ref"])
|
.args(["-C", clone_path.to_str().unwrap(), "for-each-ref"])
|
||||||
.output();
|
.output();
|
||||||
|
|
||||||
@@ -520,9 +519,8 @@ impl PushValidationTests {
|
|||||||
// empty stdout means the ref was deleted (or never written).
|
// empty stdout means the ref was deleted (or never written).
|
||||||
// A failed ls-remote (404 / connection error) is a setup
|
// A failed ls-remote (404 / connection error) is a setup
|
||||||
// failure — the /prs/ endpoint must be reachable.
|
// failure — the /prs/ endpoint must be reachable.
|
||||||
let ls_out = Command::new("git")
|
let ls_out = git_command()
|
||||||
.args(["ls-remote", &prs_url, &refname])
|
.args(["ls-remote", &prs_url, &refname])
|
||||||
.env("GIT_TERMINAL_PROMPT", "0")
|
|
||||||
.output()
|
.output()
|
||||||
.map_err(|e| format!("Failed to execute git ls-remote {}: {}", prs_url, e))?;
|
.map_err(|e| format!("Failed to execute git ls-remote {}: {}", prs_url, e))?;
|
||||||
|
|
||||||
@@ -670,10 +668,9 @@ impl Drop for LocalWorkspace2 {
|
|||||||
/// Run a `git` command in `cwd` and require success. Returns the stderr text
|
/// Run a `git` command in `cwd` and require success. Returns the stderr text
|
||||||
/// on failure so the caller's error message contains the actual git output.
|
/// on failure so the caller's error message contains the actual git output.
|
||||||
fn run_git(cwd: &Path, args: &[&str]) -> Result<(), String> {
|
fn run_git(cwd: &Path, args: &[&str]) -> Result<(), String> {
|
||||||
let output = Command::new("git")
|
let output = git_command()
|
||||||
.args(args)
|
.args(args)
|
||||||
.current_dir(cwd)
|
.current_dir(cwd)
|
||||||
.env("GIT_TERMINAL_PROMPT", "0")
|
|
||||||
.output()
|
.output()
|
||||||
.map_err(|e| format!("Failed to execute git {:?}: {}", args, e))?;
|
.map_err(|e| format!("Failed to execute git {:?}: {}", args, e))?;
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
@@ -687,10 +684,9 @@ fn run_git(cwd: &Path, args: &[&str]) -> Result<(), String> {
|
|||||||
/// about the exit code — the caller decides whether success or failure is
|
/// about the exit code — the caller decides whether success or failure is
|
||||||
/// the spec-correct outcome.
|
/// the spec-correct outcome.
|
||||||
fn git_push(cwd: &Path, url: &str, refname: &str) -> std::io::Result<std::process::Output> {
|
fn git_push(cwd: &Path, url: &str, refname: &str) -> std::io::Result<std::process::Output> {
|
||||||
Command::new("git")
|
git_command()
|
||||||
.args(["push", url, &format!("HEAD:{}", refname)])
|
.args(["push", url, &format!("HEAD:{}", refname)])
|
||||||
.current_dir(cwd)
|
.current_dir(cwd)
|
||||||
.env("GIT_TERMINAL_PROMPT", "0")
|
|
||||||
.output()
|
.output()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user