use std::path::PathBuf;
use std::process::{Command, Stdio};
use crate::pushrefs::PushRef;
pub const FORMAT: &str = "amont-attest-v2";
pub const NOTES_REF: &str = "amont-attest";
pub const NOTES_FULL_REF: &str = "refs/notes/amont-attest";
pub const INPUTS_REF: &str = "amont-attest-inputs";
pub const INPUTS_FULL_REF: &str = "refs/notes/amont-attest-inputs";
const SPEC_PATHS: [&str; 2] = [".forgejo/attest-inputs", ".github/attest-inputs"];
pub const NAMESPACE: &str = "amont-attest";
pub const PUSH_GUARD: &str = "AMONT_ATTEST_PUSH";
const TOGGLE: &str = "amont.attest";
const KEY_CONFIG: &str = "amont.attestKey";
const KEY_DEFAULT: &str = ".ssh/amont-attest";
pub fn push_guard_active() -> bool {
std::env::var_os(PUSH_GUARD).is_some()
}
pub fn enabled(settings: &crate::config::Settings) -> bool {
crate::config::boolean_or(settings, TOGGLE, false)
}
fn key_path() -> Option<PathBuf> {
if let Some(k) = crate::git::stdout(&["config", "--get", KEY_CONFIG]) {
if !k.is_empty() {
return Some(PathBuf::from(k));
}
}
let home = std::env::var_os("HOME").or_else(|| std::env::var_os("USERPROFILE"))?;
Some(PathBuf::from(home).join(KEY_DEFAULT))
}
pub fn platform() -> String {
format!("{}-{}", std::env::consts::ARCH, std::env::consts::OS)
}
pub fn payload(tree: &str, gates: &[String], inputs: &[(String, String)]) -> String {
let mut p = format!(
"{FORMAT}\ntree {tree}\ngates {}\nplatform {}\n",
gates.join(" "),
platform()
);
for (gate, fp) in inputs {
p.push_str(&format!("input {gate} {fp}\n"));
}
p.push_str(&format!("amont {}\n", env!("CARGO_PKG_VERSION")));
p
}
const MAX_SPEC_BYTES: usize = 65536;
const MAX_SPEC_GATES: usize = 64;
const MAX_SPEC_PATHS: usize = 64;
fn valid_gate(name: &str) -> bool {
let mut chars = name.chars();
match chars.next() {
Some(c) if c.is_ascii_alphanumeric() => {}
_ => return false,
}
name.len() <= 64 && chars.all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '_' || c == '-')
}
#[derive(Debug, Clone, PartialEq)]
struct PathTok {
path: String,
optional: bool,
}
fn bad_path(tok: &str) -> bool {
let tok = match tok.strip_prefix('?') {
Some("") => return true,
Some(rest) => rest,
None => tok,
};
tok.starts_with(':')
|| tok.starts_with('/')
|| tok.starts_with("./")
|| tok.starts_with("../")
|| tok.ends_with('/')
|| tok
.chars()
.any(|c| matches!(c, '*' | '?' | '[' | ']' | '\\'))
|| tok
.split('/')
.any(|c| c.is_empty() || c == "." || c == "..")
}
fn parse_spec(bytes: &[u8]) -> Option<Vec<(String, Vec<PathTok>)>> {
if bytes.len() > MAX_SPEC_BYTES
|| bytes
.iter()
.any(|&b| !(b == b' ' || b == b'\t' || b == b'\n' || (0x21..=0x7e).contains(&b)))
{
return None;
}
let text = std::str::from_utf8(bytes).ok()?;
let mut gates: Vec<(String, Vec<PathTok>)> = Vec::new();
for line in text.split('\n') {
let mut toks = line.split([' ', '\t']).filter(|t| !t.is_empty());
let Some(gate) = toks.next() else { continue };
if gate.starts_with('#') {
continue;
}
if !valid_gate(gate) || gates.iter().any(|(g, _)| g == gate) {
return None;
}
let paths: Vec<String> = toks.map(String::from).collect();
if paths.is_empty() || paths.len() > MAX_SPEC_PATHS || paths.iter().any(|p| bad_path(p)) {
return None;
}
let paths = paths
.into_iter()
.map(|p| match p.strip_prefix('?') {
Some(rest) => PathTok {
path: rest.to_string(),
optional: true,
},
None => PathTok {
path: p,
optional: false,
},
})
.collect();
gates.push((gate.to_string(), paths));
if gates.len() > MAX_SPEC_GATES {
return None;
}
}
Some(gates)
}
fn spec_at(tree: &str) -> Option<Vec<(String, Vec<PathTok>)>> {
let present: Vec<&str> = SPEC_PATHS
.iter()
.copied()
.filter(|p| crate::git::succeeds(&["cat-file", "-e", &format!("{tree}:{p}")]))
.collect();
let [path] = present.as_slice() else {
return None;
};
let bytes = crate::git::stdout_raw(&["cat-file", "blob", &format!("{tree}:{path}")])?;
parse_spec(&bytes)
}
fn implicit_inputs(tokens: &[String]) -> Vec<String> {
let mut attrs: Vec<String> = vec![".gitattributes".to_string()];
for t in tokens {
let comps: Vec<&str> = t.split('/').collect();
let mut prefix = String::new();
for c in &comps[..comps.len().saturating_sub(1)] {
if !prefix.is_empty() {
prefix.push('/');
}
prefix.push_str(c);
attrs.push(format!("{prefix}/.gitattributes"));
}
}
attrs.sort();
attrs.dedup();
let mut out: Vec<String> = SPEC_PATHS.iter().map(|s| s.to_string()).collect();
out.push(".gitmodules".to_string());
out.extend(attrs);
out
}
fn is_oid(s: &str) -> bool {
(s.len() == 40 || s.len() == 64)
&& s.bytes()
.all(|b| b.is_ascii_hexdigit() && !b.is_ascii_uppercase())
}
fn fingerprint(tree: &str, toks: &[PathTok]) -> Option<String> {
let required: Vec<&str> = toks
.iter()
.filter(|t| !t.optional)
.map(|t| t.path.as_str())
.collect();
if !required.is_empty() {
let names: String = required.iter().map(|p| format!("{tree}:{p}\n")).collect();
let answers = crate::git::stdout_piped(&["cat-file", "--batch-check"], &names)?;
if answers.lines().count() != required.len()
|| answers.lines().any(|l| l.ends_with(" missing"))
{
return None;
}
}
let paths: Vec<String> = toks.iter().map(|t| t.path.clone()).collect();
let mut args: Vec<String> = vec![
"ls-tree".into(),
"-r".into(),
"-z".into(),
"--full-tree".into(),
tree.to_string(),
"--".into(),
];
args.extend(implicit_inputs(&paths));
args.extend(paths.iter().cloned());
let argv: Vec<&str> = args.iter().map(String::as_str).collect();
let listing = crate::git::stdout_raw(&argv)?;
if listing.is_empty() {
return None;
}
crate::git::stdout_piped_in(
std::path::Path::new("."),
&["hash-object", "--stdin"],
&listing,
)
.filter(|s| is_oid(s))
}
fn inputs_for(tree: &str, gates: &[String]) -> Vec<(String, String)> {
let Some(spec) = spec_at(tree) else {
return Vec::new();
};
spec.iter()
.filter(|(g, _)| gates.iter().any(|x| x == g))
.filter_map(|(g, paths)| fingerprint(tree, paths).map(|fp| (g.clone(), fp)))
.collect()
}
fn input_key(gate: &str, fp: &str) -> Option<String> {
let pre = format!("amont-attest-input {gate} {fp}\n");
crate::git::stdout_piped_in(
std::path::Path::new("."),
&["hash-object", "--stdin"],
pre.as_bytes(),
)
.filter(|s| is_oid(s))
}
fn sign(payload: &str, key: &std::path::Path) -> Option<String> {
use std::io::Write;
let mut child = Command::new("ssh-keygen")
.args(["-Y", "sign", "-n", NAMESPACE, "-f"])
.arg(key)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.ok()?;
child.stdin.take()?.write_all(payload.as_bytes()).ok()?;
let out = child.wait_with_output().ok()?;
if !out.status.success() {
return None;
}
let sig = String::from_utf8_lossy(&out.stdout).trim().to_string();
sig.starts_with("-----BEGIN SSH SIGNATURE-----")
.then_some(sig)
}
fn create_exclusive(path: &std::path::Path) -> Option<std::fs::File> {
std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(path)
.ok()
}
pub fn verify(
payload: &str,
sig: &str,
allowed_signers: &std::path::Path,
principal: &str,
) -> bool {
use std::io::Write;
let nonce = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.subsec_nanos())
.unwrap_or(0);
let sig_file = std::env::temp_dir().join(format!(
"amont-attest-verify-{}-{:p}-{nonce}.sig",
std::process::id(),
&sig
));
let Some(mut f) = create_exclusive(&sig_file) else {
return false;
};
if f.write_all(format!("{sig}\n").as_bytes()).is_err() {
let _ = std::fs::remove_file(&sig_file);
return false;
}
drop(f);
let ok = (|| {
let mut child = Command::new("ssh-keygen")
.args(["-Y", "verify", "-n", NAMESPACE, "-I", principal, "-f"])
.arg(allowed_signers)
.arg("-s")
.arg(&sig_file)
.stdin(Stdio::piped())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.ok()?;
child.stdin.take()?.write_all(payload.as_bytes()).ok()?;
child.wait().ok().map(|s| s.success())
})()
.unwrap_or(false);
let _ = std::fs::remove_file(&sig_file);
ok
}
pub fn attest_push(
settings: &crate::config::Settings,
remote: &str,
refs: &[PushRef],
gates: &[String],
) {
if gates.is_empty() || remote.is_empty() || !enabled(settings) {
return;
}
let Some(key) = key_path() else { return };
if !key.exists() {
crate::config::complain(
TOGGLE,
&format!("signing key {} does not exist", key.display()),
"no attestation (CI will run the tests)",
);
return;
}
let mut blocks: Vec<(String, String)> = Vec::new();
let mut input_blocks: Vec<(String, String)> = Vec::new();
for r in refs {
if is_zero(&r.local_oid) {
continue; }
let spec = format!("{}^{{tree}}", r.local_oid);
let Some(tree) = crate::git::stdout(&["rev-parse", &spec]) else {
continue;
};
let inputs = inputs_for(&tree, gates);
let p = payload(&tree, gates, &inputs);
let body = match sign(&p, &key) {
Some(sig) => format!("{p}\n{sig}"),
None => continue,
};
for (gate, fp) in &inputs {
if let Some(k) = input_key(gate, fp) {
input_blocks.push((k, body.clone()));
}
}
blocks.push((tree, body.clone()));
blocks.push((r.local_oid.clone(), body));
}
if blocks.is_empty() {
return;
}
let main_ok = publish(remote, NOTES_FULL_REF, &blocks);
let inputs_ok = input_blocks.is_empty() || publish(remote, INPUTS_FULL_REF, &input_blocks);
if main_ok {
crate::say!(
"{} attested {} for CI ({}{})",
crate::ui::valid_sign(),
crate::ui::highlight(&gates.join(" ")),
NOTES_REF,
if input_blocks.is_empty() {
String::new()
} else if inputs_ok {
format!(", {} input fingerprints", input_blocks.len())
} else {
", input fingerprints not published".to_string()
},
);
}
}
const PUSH_ATTEMPTS: u32 = 4;
fn publish(remote: &str, notes_ref: &str, blocks: &[(String, String)]) -> bool {
let short = notes_ref.trim_start_matches("refs/notes/");
let tmp = format!("amont-attest-push-{}-{short}", std::process::id());
let tmp_full = format!("refs/notes/{tmp}");
let fetch_spec = format!("+{notes_ref}:{tmp_full}");
let push_spec = format!("{tmp_full}:{notes_ref}");
let mut published = false;
for _ in 0..PUSH_ATTEMPTS {
let _ = crate::git::succeeds(&["update-ref", "-d", &tmp_full]);
let _ = crate::git::succeeds(&["fetch", "--quiet", remote, &fetch_spec]);
let mut appended = false;
for (object, body) in blocks {
let existing =
crate::git::stdout(&["notes", "--ref", &tmp, "show", object]).unwrap_or_default();
if existing.contains(body.trim_end()) {
continue;
}
if crate::git::succeeds(&["notes", "--ref", &tmp, "append", "-m", body, object]) {
appended = true;
}
}
if !appended {
published = true;
break;
}
match push_notes(remote, &push_spec) {
Push::Done => {
published = true;
break;
}
Push::Raced => continue,
Push::Refused => break,
}
}
if published {
let _ = crate::git::succeeds(&["update-ref", notes_ref, &tmp_full]);
}
let _ = crate::git::succeeds(&["update-ref", "-d", &tmp_full]);
published
}
enum Push {
Done,
Raced,
Refused,
}
pub fn covered(
signers: &std::path::Path,
principal: &str,
require_platform: Option<&str>,
) -> Option<String> {
covered_within(signers, principal, require_platform, REMOTE_BUDGET_SECS)
}
const REMOTE_BUDGET_SECS: u64 = 15;
#[derive(Debug, PartialEq)]
enum Sync {
Judge,
Skip,
}
fn sync_mirror(budget: u64) -> Sync {
if !crate::git::succeeds(&["remote", "get-url", "origin"]) {
return Sync::Judge;
}
sweep_sync_refs();
let old = crate::git::stdout(&["rev-parse", "--verify", "--quiet", NOTES_FULL_REF]);
let tmp = format!("{SYNC_NAMESPACE}{}/amont-attest", std::process::id());
let fetched = crate::git::probe_remote(
&[
"fetch",
"--quiet",
"origin",
&format!("+{NOTES_FULL_REF}:{tmp}"),
],
budget,
);
let verdict = match fetched {
crate::git::Probe::Exit(0) => {
let new = crate::git::stdout(&["rev-parse", "--verify", "--quiet", &tmp]);
let expected = old.clone().unwrap_or_default();
match new {
None => {
say_skip(&format!(
"the fetch of {NOTES_FULL_REF} left nothing to read; local mirror not judged"
));
Sync::Skip
}
Some(new)
if crate::git::succeeds(&["update-ref", NOTES_FULL_REF, &new, &expected]) =>
{
Sync::Judge
}
Some(new) => {
let now =
crate::git::stdout(&["rev-parse", "--verify", "--quiet", NOTES_FULL_REF]);
if now.as_deref() == Some(new.as_str()) {
Sync::Judge
} else {
say_skip(&format!(
"cannot update {NOTES_FULL_REF} to origin's copy; local mirror not judged"
));
Sync::Skip
}
}
}
}
crate::git::Probe::TimedOut(secs) => {
say_skip(&format!(
"origin did not answer within {secs} s for {NOTES_FULL_REF}; local mirror not judged, running everything"
));
Sync::Skip
}
_ => match crate::git::probe_remote(
&["ls-remote", "--exit-code", "origin", NOTES_FULL_REF],
budget,
) {
crate::git::Probe::Exit(2) => drop_mirror(old.as_deref()),
crate::git::Probe::Exit(0) => {
say_skip(&format!(
"origin has {NOTES_FULL_REF} but fetching it failed; local mirror not judged, running everything"
));
Sync::Skip
}
crate::git::Probe::Exit(code) => {
say_skip(&format!(
"cannot fetch {NOTES_FULL_REF} from origin (exit {code}; no credentials? persist-credentials: false?); local mirror not judged, running everything"
));
Sync::Skip
}
crate::git::Probe::TimedOut(secs) => {
say_skip(&format!(
"origin did not answer within {secs} s for {NOTES_FULL_REF}; local mirror not judged, running everything"
));
Sync::Skip
}
crate::git::Probe::Failed => {
say_skip(&format!(
"cannot fetch {NOTES_FULL_REF} from origin (git did not run); local mirror not judged, running everything"
));
Sync::Skip
}
},
};
let _ = crate::git::succeeds(&["update-ref", "-d", &tmp]);
report_stale_lock();
verdict
}
fn drop_mirror(old: Option<&str>) -> Sync {
let Some(oid) = old else {
return Sync::Skip; };
if crate::git::succeeds(&["update-ref", "-d", NOTES_FULL_REF, oid]) {
say(format!(
"amont: origin has no {NOTES_FULL_REF}; deleted the local mirror (was {oid})"
));
say(format!(
"amont: undo locally: git update-ref {NOTES_FULL_REF} {oid} (undo the revocation: git push origin {oid}:{NOTES_FULL_REF})"
));
} else {
say_skip(&format!(
"origin has no {NOTES_FULL_REF} but the local mirror could not be deleted (read-only .git?); local mirror not judged"
));
}
Sync::Skip
}
const SYNC_NAMESPACE: &str = "refs/amont-tmp/";
fn sweep_sync_refs() {
let Some(refs) = crate::git::stdout(&["for-each-ref", "--format=%(refname)", SYNC_NAMESPACE])
else {
return;
};
for r in refs.lines() {
let Some(pid) = r
.strip_prefix(SYNC_NAMESPACE)
.and_then(|rest| rest.split('/').next())
.filter(|p| !p.is_empty() && p.bytes().all(|b| b.is_ascii_digit()))
else {
continue;
};
if pid == std::process::id().to_string() || process_alive(pid) != Some(false) {
continue;
}
let _ = crate::git::succeeds(&["update-ref", "-d", r]);
}
}
fn process_alive(pid: &str) -> Option<bool> {
if !cfg!(unix) {
return None;
}
let out = Command::new("kill")
.args(["-0", pid])
.env("LC_ALL", "C")
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::piped())
.output()
.ok()?;
if out.status.success() {
return Some(true);
}
let err = String::from_utf8_lossy(&out.stderr).to_lowercase();
Some(!err.contains("no such process"))
}
fn report_stale_lock() {
let lock = format!("{NOTES_FULL_REF}.lock");
if let Some(path) = crate::git::stdout(&["rev-parse", "--git-path", &lock]) {
if std::path::Path::new(&path).exists() {
say_skip(&format!(
"a git that was killed left a lock on {NOTES_FULL_REF}; if no git is running: rm {path}"
));
}
}
}
fn say_skip(why: &str) {
say(format!("amont: {why}"));
}
thread_local! {
static CAPTURE: std::cell::RefCell<Option<Vec<String>>> = const { std::cell::RefCell::new(None) };
}
fn say(line: String) {
CAPTURE.with(|c| {
if let Some(v) = c.borrow_mut().as_mut() {
v.push(line.clone());
}
});
eprintln!("{line}");
}
fn covered_within(
signers: &std::path::Path,
principal: &str,
require_platform: Option<&str>,
budget: u64,
) -> Option<String> {
if sync_mirror(budget) == Sync::Skip {
return None;
}
let head_tree = crate::git::stdout(&["rev-parse", "HEAD^{tree}"])?;
for candidate in [head_tree.as_str(), "HEAD", "HEAD^2"] {
let Some(object) = crate::git::stdout(&["rev-parse", "--verify", candidate]) else {
continue;
};
let Some(body) = crate::git::stdout(&["notes", "--ref", NOTES_REF, "show", &object]) else {
continue;
};
let Some((payload, sig)) = split_note(&body) else {
continue;
};
let mut lines = payload.lines();
if lines.next() != Some(FORMAT) {
continue;
}
let field = |name: &str| {
payload
.lines()
.find_map(|l| l.strip_prefix(name).and_then(|r| r.strip_prefix(' ')))
.map(str::trim)
};
let (Some(tree), Some(gates), Some(ran_on)) =
(field("tree"), field("gates"), field("platform"))
else {
continue;
};
if tree != head_tree || gates.is_empty() {
continue; }
if require_platform.is_some_and(|want| want != ran_on) {
continue;
}
if verify(&payload, &sig, signers, principal) {
return Some(gates.to_string());
}
}
None
}
pub fn default_signers() -> Option<PathBuf> {
let root = crate::git::stdout(&["rev-parse", "--show-toplevel"]).map(PathBuf::from);
[".forgejo/allowed_signers", ".github/allowed_signers"]
.into_iter()
.map(|rel| match &root {
Some(root) => root.join(rel),
None => PathBuf::from(rel),
})
.find(|p| p.exists())
}
pub fn first_principal(signers: &std::path::Path) -> Option<String> {
let body = std::fs::read_to_string(signers).ok()?;
body.lines()
.map(str::trim)
.find(|l| !l.is_empty() && !l.starts_with('#'))
.and_then(|l| l.split_whitespace().next())
.map(str::to_string)
}
fn split_note(body: &str) -> Option<(String, String)> {
let (payload, sig) = body.split_once("\n\n")?;
if !sig.starts_with("-----BEGIN SSH SIGNATURE-----") {
return None;
}
Some((format!("{payload}\n"), sig.to_string()))
}
fn push_notes(remote: &str, refspec: &str) -> Push {
let out = Command::new("git")
.args(["push", "--quiet", remote, refspec])
.env(PUSH_GUARD, "1")
.stdin(Stdio::null())
.stdout(Stdio::null())
.output();
let Ok(out) = out else { return Push::Refused };
if out.status.success() {
return Push::Done;
}
let err = String::from_utf8_lossy(&out.stderr);
if [
"non-fast-forward",
"fetch first",
"stale info",
"cannot lock ref",
"failed to lock",
]
.iter()
.any(|m| err.contains(m))
{
Push::Raced
} else {
Push::Refused
}
}
fn is_zero(oid: &str) -> bool {
!oid.is_empty() && oid.bytes().all(|b| b == b'0')
}
pub fn forget() -> bool {
crate::git::succeeds(&["update-ref", "-d", NOTES_FULL_REF])
}
pub fn forget_in(repo: &std::path::Path) -> bool {
crate::git::succeeds_in(repo, &["update-ref", "-d", NOTES_FULL_REF])
}
#[cfg(test)]
mod tests {
use super::*;
fn test_settings() -> crate::config::Settings {
crate::config::Settings::default()
}
use std::path::Path;
fn dir(name: &str) -> PathBuf {
let d = std::env::temp_dir().join(format!("attest-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(&d).unwrap();
d
}
fn git(dir: &Path, args: &[&str]) -> String {
let out = std::process::Command::new("git")
.arg("-C")
.arg(dir)
.args(args)
.output()
.expect("git");
assert!(
out.status.success(),
"fixture: git {args:?} in {} exited {:?}: {}",
dir.display(),
out.status.code(),
String::from_utf8_lossy(&out.stderr).trim()
);
String::from_utf8_lossy(&out.stdout).trim().to_string()
}
fn repo(name: &str) -> PathBuf {
let d = dir(name);
git(&d, &["init", "-q", "--template=", "."]);
git(&d, &["config", "user.email", "t@t.test"]);
git(&d, &["config", "user.name", "t"]);
d
}
fn keypair(d: &Path) -> (PathBuf, PathBuf) {
let key = d.join("attest_key");
let ok = std::process::Command::new("ssh-keygen")
.args(["-q", "-t", "ed25519", "-N", "", "-C", "test", "-f"])
.arg(&key)
.status()
.expect("ssh-keygen must exist for these tests")
.success();
assert!(ok, "keygen failed");
let pubkey = std::fs::read_to_string(key.with_extension("pub")).unwrap();
let signers = d.join("allowed_signers");
std::fs::write(
&signers,
format!("t@t.test namespaces=\"{NAMESPACE}\" {pubkey}"),
)
.unwrap();
(key, signers)
}
fn in_repo<T>(dir: &Path, f: impl FnOnce() -> T) -> T {
let _guard = crate::TEST_CWD.lock().unwrap_or_else(|p| p.into_inner());
let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(dir).unwrap();
let r = f();
std::env::set_current_dir(prev).unwrap();
r
}
#[test]
fn the_signature_file_refuses_to_follow_what_is_already_there() {
let dir = std::env::temp_dir().join(format!("amont-excl-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("temp dir");
let taken = dir.join("taken");
std::fs::write(&taken, "original").expect("seed");
assert!(create_exclusive(&taken).is_none());
assert_eq!(std::fs::read_to_string(&taken).unwrap(), "original");
#[cfg(unix)]
{
let victim = dir.join("victim");
std::fs::write(&victim, "precious").expect("seed");
let link = dir.join("link");
std::os::unix::fs::symlink(&victim, &link).expect("symlink");
assert!(
create_exclusive(&link).is_none(),
"a symlink must be refused, not followed"
);
assert_eq!(
std::fs::read_to_string(&victim).unwrap(),
"precious",
"the link target was written through"
);
}
let fresh = dir.join("fresh");
assert!(create_exclusive(&fresh).is_some());
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn the_payload_is_the_documented_contract() {
let p = payload(
"abc123",
&["pre-push-pytest".into(), "pre-push-cargo-test".into()],
&[],
);
let lines: Vec<&str> = p.lines().collect();
assert_eq!(lines[0], FORMAT);
assert_eq!(lines[1], "tree abc123");
assert_eq!(lines[2], "gates pre-push-pytest pre-push-cargo-test");
assert_eq!(lines[3], format!("platform {}", platform()));
assert_eq!(lines[4], format!("amont {}", env!("CARGO_PKG_VERSION")));
assert!(
p.ends_with('\n'),
"CI reconstructs these bytes; the trailing newline is part of them"
);
}
#[test]
fn sign_verify_roundtrip_and_tamper_rejection() {
let d = dir("roundtrip");
let (key, signers) = keypair(&d);
let p = payload("deadbeef", &["pre-push-pytest".into()], &[]);
let sig = sign(&p, &key).expect("signing with a real key succeeds");
assert!(verify(&p, &sig, &signers, "t@t.test"));
let tampered = payload("deadbeee", &["pre-push-pytest".into()], &[]);
assert!(!verify(&tampered, &sig, &signers, "t@t.test"));
assert!(!verify(&p, &sig, &signers, "someone@else.test"));
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn a_missing_key_signs_nothing() {
assert!(sign("anything", Path::new("/nonexistent/key")).is_none());
}
#[test]
fn an_enabled_push_leaves_a_verifiable_note_on_the_remote() {
let d = dir("e2e");
let (key, signers) = keypair(&d);
let remote = d.join("remote.git");
std::fs::create_dir_all(&remote).unwrap();
git(&remote, &["init", "-q", "--bare", "--template=", "."]);
let work = repo("e2e-work");
git(
&work,
&["remote", "add", "origin", remote.to_str().unwrap()],
);
git(&work, &["config", "amont.attest", "true"]);
git(&work, &["config", "amont.attestKey", key.to_str().unwrap()]);
std::fs::write(work.join("a.ts"), "x").unwrap();
git(&work, &["add", "a.ts"]);
git(&work, &["commit", "-qm", "chore: a"]);
let head = git(&work, &["rev-parse", "HEAD"]);
let tree = git(&work, &["rev-parse", "HEAD^{tree}"]);
let push_ref = PushRef {
local_ref: "refs/heads/main".into(),
local_oid: head.clone(),
remote_ref: "refs/heads/main".into(),
remote_oid: "0".repeat(40),
};
in_repo(&work, || {
attest_push(
&test_settings(),
"origin",
&[push_ref],
&["pre-push-run-tests-js".into()],
);
});
let body = git(&remote, &["notes", "--ref", NOTES_REF, "show", &head]);
assert!(!body.is_empty(), "no note reached the remote");
let (p, sig) = body
.split_once("\n\n")
.expect("payload, blank line, signature");
let p = format!("{p}\n"); assert!(p.starts_with(FORMAT));
assert!(
p.contains(&format!("tree {tree}")),
"attests the pushed tree"
);
assert!(
verify(&p, sig, &signers, "t@t.test"),
"the remote copy verifies"
);
let _ = std::fs::remove_dir_all(&d);
let _ = std::fs::remove_dir_all(&work);
}
fn remote_and_work(name: &str) -> (PathBuf, PathBuf, PathBuf, PathBuf) {
let d = dir(name);
let (key, signers) = keypair(&d);
let remote = d.join("remote.git");
std::fs::create_dir_all(&remote).unwrap();
git(&remote, &["init", "-q", "--bare", "--template=", "."]);
let work = repo(&format!("{name}-work"));
git(
&work,
&["remote", "add", "origin", remote.to_str().unwrap()],
);
git(&work, &["config", "amont.attest", "true"]);
git(&work, &["config", "amont.attestKey", key.to_str().unwrap()]);
std::fs::write(work.join("a.ts"), "x").unwrap();
git(&work, &["add", "a.ts"]);
git(&work, &["commit", "-qm", "chore: a"]);
(d, work, remote, signers)
}
fn push_ref_for(work: &Path) -> PushRef {
PushRef {
local_ref: "refs/heads/main".into(),
local_oid: git(work, &["rev-parse", "HEAD"]),
remote_ref: "refs/heads/main".into(),
remote_oid: "0".repeat(40),
}
}
fn blocks_in(body: &str) -> usize {
body.matches("-----BEGIN SSH SIGNATURE-----").count()
}
#[test]
fn a_block_already_on_the_remote_survives_and_ours_is_appended() {
let (d, work, remote, _) = remote_and_work("append");
let tree = git(&work, &["rev-parse", "HEAD^{tree}"]);
let seed = repo("append-seed");
git(
&seed,
&["remote", "add", "origin", remote.to_str().unwrap()],
);
git(&seed, &["fetch", "-q", "origin"]);
std::fs::write(seed.join("a.ts"), "x").unwrap();
git(&seed, &["add", "a.ts"]);
git(&seed, &["commit", "-qm", "chore: a"]);
let foreign = "amont-attest-v2\ntree x\ngates ci-fmt\nplatform s390x-aix\namont other\n\n-----BEGIN SSH SIGNATURE-----\nnope\n-----END SSH SIGNATURE-----";
git(
&seed,
&["notes", "--ref", NOTES_REF, "add", "-m", foreign, &tree],
);
git(
&seed,
&[
"push",
"-q",
"origin",
&format!("{NOTES_FULL_REF}:{NOTES_FULL_REF}"),
],
);
in_repo(&work, || {
attest_push(
&test_settings(),
"origin",
&[push_ref_for(&work)],
&["pre-push-run-tests-js".into()],
);
});
let body = git(&remote, &["notes", "--ref", NOTES_REF, "show", &tree]);
assert_eq!(blocks_in(&body), 2, "both blocks on the remote:\n{body}");
assert!(body.contains("gates ci-fmt"), "the foreign block survives");
assert!(
body.contains("gates pre-push-run-tests-js"),
"ours was appended"
);
let _ = std::fs::remove_dir_all(&d);
let _ = std::fs::remove_dir_all(&work);
let _ = std::fs::remove_dir_all(&seed);
}
#[test]
fn a_stale_local_notes_ref_no_longer_loses_the_push() {
let (d, work, remote, _) = remote_and_work("stale");
let head = git(&work, &["rev-parse", "HEAD"]);
git(
&work,
&[
"notes",
"--ref",
NOTES_REF,
"add",
"-m",
"stale local",
&head,
],
);
let stale = git(&work, &["rev-parse", NOTES_FULL_REF]);
let seed = repo("stale-seed");
std::fs::write(seed.join("b.ts"), "y").unwrap();
git(&seed, &["add", "b.ts"]);
git(&seed, &["commit", "-qm", "chore: b"]);
git(
&seed,
&["remote", "add", "origin", remote.to_str().unwrap()],
);
git(
&seed,
&[
"notes",
"--ref",
NOTES_REF,
"add",
"-m",
"remote first",
"HEAD",
],
);
git(&seed, &["push", "-q", "origin", "HEAD:refs/heads/other"]);
git(
&seed,
&[
"push",
"-q",
"origin",
&format!("{NOTES_FULL_REF}:{NOTES_FULL_REF}"),
],
);
in_repo(&work, || {
attest_push(
&test_settings(),
"origin",
&[push_ref_for(&work)],
&["pre-push-run-tests-js".into()],
);
});
let body = git(&remote, &["notes", "--ref", NOTES_REF, "show", &head]);
assert!(
body.contains("gates pre-push-run-tests-js"),
"the push landed:\n{body}"
);
assert_ne!(
git(&work, &["rev-parse", NOTES_FULL_REF]),
stale,
"the local ref followed the published state"
);
assert!(
git(&work, &["for-each-ref", "refs/notes/amont-attest-push-*"]).is_empty(),
"no temporary ref left behind"
);
let _ = std::fs::remove_dir_all(&d);
let _ = std::fs::remove_dir_all(&work);
let _ = std::fs::remove_dir_all(&seed);
}
#[test]
fn a_second_push_of_the_same_tree_appends_nothing() {
let (d, work, remote, _) = remote_and_work("twice");
let tree = git(&work, &["rev-parse", "HEAD^{tree}"]);
for _ in 0..2 {
in_repo(&work, || {
attest_push(
&test_settings(),
"origin",
&[push_ref_for(&work)],
&["pre-push-run-tests-js".into()],
);
});
}
let body = git(&remote, &["notes", "--ref", NOTES_REF, "show", &tree]);
assert_eq!(blocks_in(&body), 1, "one block, not two:\n{body}");
let _ = std::fs::remove_dir_all(&d);
let _ = std::fs::remove_dir_all(&work);
}
#[test]
fn a_spec_yields_input_lines_and_fingerprint_keyed_notes() {
let (d, work, remote, _) = remote_and_work("inputs");
std::fs::create_dir_all(work.join(".github")).unwrap();
std::fs::write(
work.join(".github/attest-inputs"),
"# what each gate reads\npre-push-run-tests-js a.ts\nother nope\n",
)
.unwrap();
git(&work, &["add", ".github/attest-inputs"]);
git(&work, &["commit", "-qm", "chore: spec"]);
let tree = git(&work, &["rev-parse", "HEAD^{tree}"]);
let listing = std::process::Command::new("git")
.args([
"-C",
work.to_str().unwrap(),
"ls-tree",
"-r",
"-z",
"--full-tree",
&tree,
"--",
".forgejo/attest-inputs",
".github/attest-inputs",
".gitmodules",
".gitattributes",
"a.ts",
])
.output()
.unwrap()
.stdout;
let expected =
crate::git::stdout_piped_in(&work, &["hash-object", "--stdin"], &listing).unwrap();
in_repo(&work, || {
attest_push(
&test_settings(),
"origin",
&[push_ref_for(&work)],
&["pre-push-run-tests-js".into(), "other".into()],
);
});
let body = git(&remote, &["notes", "--ref", NOTES_REF, "show", &tree]);
assert!(
body.contains(&format!("input pre-push-run-tests-js {expected}\n")),
"the input line carries the reference fingerprint:\n{body}"
);
assert!(
!body.contains("input other "),
"a gate whose path does not exist gets no line"
);
let key = crate::git::stdout_piped_in(
&work,
&["hash-object", "--stdin"],
format!("amont-attest-input pre-push-run-tests-js {expected}\n").as_bytes(),
)
.unwrap();
let under_key = git(&remote, &["notes", "--ref", INPUTS_REF, "show", &key]);
assert_eq!(
under_key, body,
"the same block is filed under the fingerprint key"
);
in_repo(&work, || {
attest_push(
&test_settings(),
"origin",
&[push_ref_for(&work)],
&["pre-push-run-tests-js".into(), "other".into()],
);
});
assert_eq!(
git(&remote, &["notes", "--ref", INPUTS_REF, "show", &key])
.matches("BEGIN SSH SIGNATURE")
.count(),
1
);
let _ = std::fs::remove_dir_all(&d);
let _ = std::fs::remove_dir_all(&work);
}
#[test]
fn without_a_valid_spec_the_block_is_tree_keyed_only() {
let (d, work, remote, _) = remote_and_work("nospec");
std::fs::create_dir_all(work.join(".github")).unwrap();
std::fs::write(
work.join(".github/attest-inputs"),
"pre-push-run-tests-js src/*.ts\n",
)
.unwrap();
git(&work, &["add", ".github/attest-inputs"]);
git(&work, &["commit", "-qm", "chore: bad spec"]);
let tree = git(&work, &["rev-parse", "HEAD^{tree}"]);
in_repo(&work, || {
attest_push(
&test_settings(),
"origin",
&[push_ref_for(&work)],
&["pre-push-run-tests-js".into()],
);
});
let body = git(&remote, &["notes", "--ref", NOTES_REF, "show", &tree]);
assert!(!body.contains("\ninput "), "no input line:\n{body}");
assert!(
!crate::git::succeeds_in(
&remote,
&["rev-parse", "--verify", "--quiet", INPUTS_FULL_REF]
),
"no inputs ref was created"
);
let _ = std::fs::remove_dir_all(&d);
let _ = std::fs::remove_dir_all(&work);
}
#[test]
fn the_spec_grammar_is_attests() {
assert!(parse_spec(b"# c\ng src Cargo.toml\n").is_some());
for bad in [
b"g src/*.rs\n".as_slice(),
b"g :!x\n",
b"g\n",
b"g src\ng x\n",
b"g src\r\n",
b"g sr\xc3\xa9\n",
b"-g src\n",
b"g ./src\n",
b"g src/\n",
b"g a/../b\n",
] {
assert!(parse_spec(bad).is_none(), "{bad:?}");
}
assert_eq!(
implicit_inputs(&["crates/foo/src".into()]),
[
".forgejo/attest-inputs",
".github/attest-inputs",
".gitmodules",
".gitattributes",
"crates/.gitattributes",
"crates/foo/.gitattributes"
]
);
}
#[test]
fn no_opt_in_means_no_note() {
let d = dir("optout");
let (key, _) = keypair(&d);
let remote = d.join("remote.git");
std::fs::create_dir_all(&remote).unwrap();
git(&remote, &["init", "-q", "--bare", "--template=", "."]);
let work = repo("optout-work");
git(
&work,
&["remote", "add", "origin", remote.to_str().unwrap()],
);
git(&work, &["config", "amont.attestKey", key.to_str().unwrap()]);
std::fs::write(work.join("a.ts"), "x").unwrap();
git(&work, &["add", "a.ts"]);
git(&work, &["commit", "-qm", "chore: a"]);
let head = git(&work, &["rev-parse", "HEAD"]);
let push_ref = PushRef {
local_ref: "refs/heads/main".into(),
local_oid: head.clone(),
remote_ref: "refs/heads/main".into(),
remote_oid: "0".repeat(40),
};
in_repo(&work, || {
attest_push(
&test_settings(),
"origin",
&[push_ref],
&["pre-push-run-tests-js".into()],
);
});
assert!(
git(&remote, &["notes", "--ref", NOTES_REF, "list"]).is_empty(),
"an un-opted-in repo attested something"
);
let _ = std::fs::remove_dir_all(&d);
let _ = std::fs::remove_dir_all(&work);
}
#[test]
fn deletions_and_empty_gates_attest_nothing() {
let d = dir("nothing");
let (key, _) = keypair(&d);
let remote = d.join("remote.git");
std::fs::create_dir_all(&remote).unwrap();
git(&remote, &["init", "-q", "--bare", "--template=", "."]);
let work = repo("nothing-work");
git(
&work,
&["remote", "add", "origin", remote.to_str().unwrap()],
);
git(&work, &["config", "amont.attest", "true"]);
git(&work, &["config", "amont.attestKey", key.to_str().unwrap()]);
std::fs::write(work.join("a.ts"), "x").unwrap();
git(&work, &["add", "a.ts"]);
git(&work, &["commit", "-qm", "chore: a"]);
let head = git(&work, &["rev-parse", "HEAD"]);
let deletion = PushRef {
local_ref: "(delete)".into(),
local_oid: "0".repeat(40),
remote_ref: "refs/heads/gone".into(),
remote_oid: head.clone(),
};
let real = PushRef {
local_ref: "refs/heads/main".into(),
local_oid: head,
remote_ref: "refs/heads/main".into(),
remote_oid: "0".repeat(40),
};
in_repo(&work, || {
attest_push(
&test_settings(),
"origin",
&[deletion],
&["pre-push-pytest".into()],
);
attest_push(&test_settings(), "origin", &[real], &[]);
});
assert!(git(&remote, &["notes", "--ref", NOTES_REF, "list"]).is_empty());
let _ = std::fs::remove_dir_all(&d);
let _ = std::fs::remove_dir_all(&work);
}
#[test]
fn covered_answers_in_a_fresh_clone_and_rejects_drift_and_forgery() {
let d = dir("covered");
let (key, signers) = keypair(&d);
let remote = d.join("remote.git");
std::fs::create_dir_all(&remote).unwrap();
git(&remote, &["init", "-q", "--bare", "--template=", "."]);
git(&remote, &["symbolic-ref", "HEAD", "refs/heads/main"]);
let work = repo("covered-work");
git(
&work,
&["remote", "add", "origin", remote.to_str().unwrap()],
);
git(&work, &["config", "amont.attest", "true"]);
git(&work, &["config", "amont.attestKey", key.to_str().unwrap()]);
std::fs::write(work.join("a.ts"), "x").unwrap();
git(&work, &["add", "a.ts"]);
git(&work, &["commit", "-qm", "chore: a"]);
git(&work, &["push", "-q", "origin", "HEAD:main"]);
let head = git(&work, &["rev-parse", "HEAD"]);
let push_ref = PushRef {
local_ref: "refs/heads/main".into(),
local_oid: head.clone(),
remote_ref: "refs/heads/main".into(),
remote_oid: "0".repeat(40),
};
in_repo(&work, || {
attest_push(
&test_settings(),
"origin",
&[push_ref],
&["pre-push-pytest".into()],
);
});
let clone = d.join("ci-checkout");
git(
&d,
&[
"clone",
"-q",
"--template=",
remote.to_str().unwrap(),
clone.to_str().unwrap(),
],
);
in_repo(&clone, || {
assert_eq!(
covered(&signers, "t@t.test", Some(&platform())).as_deref(),
Some("pre-push-pytest"),
"a fresh clone verifies the attestation and reads the gates"
);
assert_eq!(
covered(&signers, "t@t.test", None).as_deref(),
Some("pre-push-pytest"),
"`any` covers a platform-independent suite"
);
assert_eq!(
covered(&signers, "t@t.test", Some("s390x-aix")),
None,
"a pass on one platform is not evidence about another"
);
assert_eq!(
covered(&signers, "someone@else.test", None),
None,
"an unlisted principal covers nothing"
);
});
std::fs::write(clone.join("b.ts"), "y").unwrap();
git(&clone, &["config", "user.email", "t@t.test"]);
git(&clone, &["config", "user.name", "t"]);
git(&clone, &["add", "b.ts"]);
git(&clone, &["commit", "-qm", "chore: b"]);
in_repo(&clone, || {
assert_eq!(covered(&signers, "t@t.test", None), None, "drifted tree");
});
let head_tree = git(&work, &["rev-parse", "HEAD^{tree}"]);
for object in [&head, &head_tree] {
git(
&work,
&[
"notes", "--ref", NOTES_REF, "add", "-f", "-m", "garbage", object,
],
);
}
git(
&work,
&[
"push",
"-q",
"origin",
&format!("+{NOTES_FULL_REF}:{NOTES_FULL_REF}"),
],
);
git(&clone, &["reset", "-q", "--hard", &head]);
in_repo(&clone, || {
assert_eq!(
covered(&signers, "t@t.test", None),
None,
"a foreign note is not a stamp"
);
});
let _ = std::fs::remove_dir_all(&d);
let _ = std::fs::remove_dir_all(&work);
}
#[test]
fn default_signers_is_found_from_a_subdirectory() {
let work = repo("signers-subdir");
std::fs::create_dir_all(work.join(".forgejo")).unwrap();
std::fs::write(work.join(".forgejo/allowed_signers"), "t@t.test x\n").unwrap();
let sub = work.join("packages").join("thing");
std::fs::create_dir_all(&sub).unwrap();
in_repo(&sub, || {
let found = default_signers().expect("resolved from the repo root, not the cwd");
assert!(found.ends_with(".forgejo/allowed_signers"));
assert!(found.exists(), "the path it returns must be usable as-is");
assert_eq!(
first_principal(&found).as_deref(),
Some("t@t.test"),
"and readable from there"
);
});
let _ = std::fs::remove_dir_all(&work);
}
#[test]
fn zero_oids_of_any_width_are_zero() {
assert!(is_zero(&"0".repeat(40)));
assert!(is_zero(&"0".repeat(64)));
assert!(!is_zero("0a0000"));
assert!(!is_zero(""));
}
#[test]
fn forget_removes_the_local_ref() {
let work = repo("forget");
std::fs::write(work.join("a.ts"), "x").unwrap();
git(&work, &["add", "a.ts"]);
git(&work, &["commit", "-qm", "chore: a"]);
git(
&work,
&["notes", "--ref", NOTES_REF, "add", "-m", "x", "HEAD"],
);
in_repo(&work, forget);
assert!(git(&work, &["notes", "--ref", NOTES_REF, "list"]).is_empty());
let _ = std::fs::remove_dir_all(&work);
}
#[test]
fn an_attestation_survives_a_rewrite_that_keeps_the_tree() {
let d = dir("rewritten");
let (key, signers) = keypair(&d);
let remote = d.join("remote.git");
std::fs::create_dir_all(&remote).unwrap();
git(&remote, &["init", "-q", "--bare", "--template=", "."]);
git(&remote, &["symbolic-ref", "HEAD", "refs/heads/main"]);
let work = repo("rewritten-work");
git(
&work,
&["remote", "add", "origin", remote.to_str().unwrap()],
);
git(&work, &["config", "amont.attest", "true"]);
git(&work, &["config", "amont.attestKey", key.to_str().unwrap()]);
std::fs::write(work.join("a.ts"), "x").unwrap();
git(&work, &["add", "a.ts"]);
git(&work, &["commit", "-qm", "feat: on a branch"]);
git(&work, &["push", "-q", "origin", "HEAD:main"]);
let branch_tip = git(&work, &["rev-parse", "HEAD"]);
let push_ref = PushRef {
local_ref: "refs/heads/main".into(),
local_oid: branch_tip.clone(),
remote_ref: "refs/heads/main".into(),
remote_oid: "0".repeat(40),
};
in_repo(&work, || {
attest_push(
&test_settings(),
"origin",
&[push_ref],
&["pre-push-pytest".into()],
);
});
git(
&work,
&[
"commit",
"-q",
"--amend",
"-m",
"feat: squashed by the forge",
],
);
let rewritten = git(&work, &["rev-parse", "HEAD"]);
assert_ne!(rewritten, branch_tip, "the fixture must rewrite the commit");
assert_eq!(
git(&work, &["rev-parse", "HEAD^{tree}"]),
git(&work, &["rev-parse", &format!("{branch_tip}^{{tree}}")]),
"…while preserving the tree, which is the premise"
);
git(&work, &["push", "-q", "-f", "origin", "HEAD:main"]);
let clone = d.join("ci-checkout");
git(
&d,
&[
"clone",
"-q",
"--template=",
remote.to_str().unwrap(),
clone.to_str().unwrap(),
],
);
in_repo(&clone, || {
assert_eq!(
covered(&signers, "t@t.test", None).as_deref(),
Some("pre-push-pytest"),
"the attestation must be found by the tree it signed"
);
});
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn optional_paths_are_marked_once_and_stripped() {
let spec = parse_spec(b"g src ?build.rs ?a/b/c\n").expect("valid");
assert_eq!(
spec[0].1,
[
PathTok {
path: "src".into(),
optional: false
},
PathTok {
path: "build.rs".into(),
optional: true
},
PathTok {
path: "a/b/c".into(),
optional: true
},
]
);
assert!(
parse_spec(b"g ?nope\n").is_some(),
"an all-optional gate is a gate"
);
for bad in [
"?", "??x", "?/x", "?./x", "?x/", "?:x", "x?", "?a//b", "?a/../b",
] {
assert!(
parse_spec(format!("g {bad}\n").as_bytes()).is_none(),
"{bad} should be invalid"
);
}
}
#[test]
fn an_absent_optional_path_is_bound_by_its_absence() {
let work = repo("optional-fp");
std::fs::create_dir_all(work.join("src")).unwrap();
std::fs::create_dir_all(work.join(".github")).unwrap();
std::fs::write(work.join("src/main.rs"), "fn main() {}\n").unwrap();
std::fs::write(work.join(".github/attest-inputs"), "g src ?nope\n").unwrap();
git(&work, &["add", "-A"]);
git(&work, &["commit", "-qm", "chore: spec"]);
let tree = git(&work, &["rev-parse", "HEAD^{tree}"]);
let spec_oid = git(
&work,
&["rev-parse", &format!("{tree}:.github/attest-inputs")],
);
let main_oid = git(&work, &["rev-parse", &format!("{tree}:src/main.rs")]);
let listing = format!(
"100644 blob {spec_oid}\t.github/attest-inputs\x00100644 blob {main_oid}\tsrc/main.rs\x00"
);
let hand =
crate::git::stdout_piped_in(&work, &["hash-object", "--stdin"], listing.as_bytes())
.unwrap();
let before = in_repo(&work, || {
let spec = spec_at(&tree).expect("the spec parses");
fingerprint(&tree, &spec[0].1)
});
assert_eq!(before.as_deref(), Some(hand.as_str()));
std::fs::write(work.join("nope"), "now it exists\n").unwrap();
git(&work, &["add", "nope"]);
git(&work, &["commit", "-qm", "chore: nope"]);
let tree2 = git(&work, &["rev-parse", "HEAD^{tree}"]);
let after = in_repo(&work, || {
let spec = spec_at(&tree2).expect("the spec parses");
fingerprint(&tree2, &spec[0].1)
});
assert!(
after.is_some() && after != before,
"its appearance changes the fingerprint"
);
let _ = std::fs::remove_dir_all(&work);
}
fn attested_clone(name: &str) -> (PathBuf, PathBuf, PathBuf, PathBuf) {
let (d, work, remote, signers) = remote_and_work(name);
git(&remote, &["symbolic-ref", "HEAD", "refs/heads/main"]);
git(&work, &["push", "-q", "origin", "HEAD:main"]);
in_repo(&work, || {
attest_push(
&test_settings(),
"origin",
&[push_ref_for(&work)],
&["pre-push-pytest".into()],
);
});
let clone = d.join("ci-checkout");
git(
&d,
&[
"clone",
"-q",
"--template=",
remote.to_str().unwrap(),
clone.to_str().unwrap(),
],
);
(d, remote, clone, signers)
}
fn covers(clone: &Path, signers: &Path, budget: u64) -> Option<String> {
covers_logged(clone, signers, budget).0
}
fn covers_logged(clone: &Path, signers: &Path, budget: u64) -> (Option<String>, Vec<String>) {
in_repo(clone, || {
CAPTURE.with(|c| *c.borrow_mut() = Some(Vec::new()));
let got = covered_within(signers, "t@t.test", None, budget);
let lines = CAPTURE.with(|c| c.borrow_mut().take()).unwrap_or_default();
(got, lines)
})
}
#[test]
fn a_ref_revoked_on_origin_stops_covering_and_the_mirror_goes() {
let (d, remote, clone, signers) = attested_clone("revoked");
assert_eq!(
covers(&clone, &signers, 15).as_deref(),
Some("pre-push-pytest")
);
let oid = git(&clone, &["rev-parse", NOTES_FULL_REF]);
git(&remote, &["update-ref", "-d", NOTES_FULL_REF]);
let (got, lines) = covers_logged(&clone, &signers, 15);
assert_eq!(got, None, "revoked on origin");
assert_eq!(
lines[0],
format!("amont: origin has no {NOTES_FULL_REF}; deleted the local mirror (was {oid})")
);
assert!(
in_repo(&clone, || crate::git::stdout(&[
"rev-parse",
"--verify",
"--quiet",
NOTES_FULL_REF
]))
.is_none(),
"the local mirror was deleted"
);
let undo = lines[1]
.split("undo locally: git ")
.nth(1)
.and_then(|r| r.split(" (").next())
.expect("an undo command");
git(&clone, &undo.split(' ').collect::<Vec<_>>());
assert_eq!(git(&clone, &["rev-parse", NOTES_FULL_REF]), oid);
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn an_unreachable_origin_covers_nothing_and_keeps_the_mirror() {
let (d, _remote, clone, signers) = attested_clone("unreachable");
assert_eq!(
covers(&clone, &signers, 15).as_deref(),
Some("pre-push-pytest")
);
git(
&clone,
&[
"remote",
"set-url",
"origin",
"file:///nonexistent/amont-origin.git",
],
);
let (got, lines) = covers_logged(&clone, &signers, 15);
assert_eq!(got, None, "a stale mirror is not judged");
assert!(
lines.iter().any(|l| l.starts_with(&format!(
"amont: cannot fetch {NOTES_FULL_REF} from origin (exit "
))),
"the reason is on stderr: {lines:?}"
);
assert!(
!git(&clone, &["rev-parse", NOTES_FULL_REF]).is_empty(),
"and it is kept"
);
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn without_an_origin_the_local_ref_is_judged() {
let (d, _remote, clone, signers) = attested_clone("no-origin");
assert_eq!(
covers(&clone, &signers, 15).as_deref(),
Some("pre-push-pytest")
);
git(&clone, &["remote", "remove", "origin"]);
assert_eq!(
covers(&clone, &signers, 15).as_deref(),
Some("pre-push-pytest")
);
let _ = std::fs::remove_dir_all(&d);
}
#[cfg(unix)]
#[test]
fn a_silent_origin_is_cut_off_and_leaves_nothing_behind() {
use std::os::unix::fs::PermissionsExt;
let (d, remote, clone, signers) = attested_clone("silent");
let hang = d.join("hang-ssh");
std::fs::write(&hang, "#!/bin/sh\nexec sleep 60\n").unwrap();
std::fs::set_permissions(&hang, std::fs::Permissions::from_mode(0o755)).unwrap();
git(
&clone,
&["config", "core.sshCommand", hang.to_str().unwrap()],
);
git(
&clone,
&["remote", "set-url", "origin", "ssh://example.invalid/x.git"],
);
let (got, took) = in_repo(&clone, || {
let t0 = std::time::Instant::now();
let got = covered_within(&signers, "t@t.test", None, 2);
(got, t0.elapsed())
});
assert_eq!(got, None);
assert!(took.as_secs() < 6, "took {took:?}");
let lock = in_repo(&clone, || {
crate::git::stdout(&["rev-parse", "--git-path", &format!("{NOTES_FULL_REF}.lock")])
})
.unwrap();
assert!(
!clone.join(&lock).exists() && !Path::new(&lock).exists(),
"no lock left"
);
git(&clone, &["config", "--unset", "core.sshCommand"]);
git(
&clone,
&["remote", "set-url", "origin", remote.to_str().unwrap()],
);
assert_eq!(
covers(&clone, &signers, 15).as_deref(),
Some("pre-push-pytest")
);
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn a_stale_lock_on_the_mirror_means_nothing_is_judged() {
let (d, remote, clone, signers) = attested_clone("stale-lock");
assert_eq!(
covers(&clone, &signers, 15).as_deref(),
Some("pre-push-pytest")
);
let lock = in_repo(&clone, || {
crate::git::stdout(&["rev-parse", "--git-path", &format!("{NOTES_FULL_REF}.lock")])
})
.unwrap();
let lock = if Path::new(&lock).is_absolute() {
PathBuf::from(lock)
} else {
clone.join(lock)
};
std::fs::write(&lock, "").unwrap();
assert_eq!(
covers(&clone, &signers, 15).as_deref(),
Some("pre-push-pytest"),
"an unchanged origin: the copy is origin's"
);
let main = git(&remote, &["rev-parse", "main"]);
git(
&remote,
&[
"-c",
"user.email=t@t.test",
"-c",
"user.name=t",
"notes",
"--ref",
NOTES_REF,
"add",
"-f",
"-m",
"rewritten",
&main,
],
);
let (got, lines) = covers_logged(&clone, &signers, 15);
assert_eq!(got, None, "a pinned stale copy is never judged");
assert!(
lines.iter().any(|l| l.contains(&format!(
"left a lock on {NOTES_FULL_REF}; if no git is running: rm "
))),
"the lock is reported with its rm: {lines:?}"
);
std::fs::remove_file(&lock).unwrap();
assert_eq!(
covers(&clone, &signers, 15).as_deref(),
Some("pre-push-pytest")
);
let _ = std::fs::remove_dir_all(&d);
}
#[cfg(unix)]
#[test]
fn leftover_sync_refs_are_swept() {
let (d, _remote, clone, signers) = attested_clone("sweep");
let head = git(&clone, &["rev-parse", "HEAD"]);
git(
&clone,
&["update-ref", "refs/amont-tmp/4194305/amont-attest", &head],
);
assert_eq!(
covers(&clone, &signers, 15).as_deref(),
Some("pre-push-pytest")
);
assert_eq!(git(&clone, &["for-each-ref", "refs/amont-tmp/"]), "");
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn the_remote_environment_never_prompts() {
let (args, env) = crate::git::remote_env(crate::git::Ssh::Batch);
assert_eq!(
args,
[
"-c",
"http.lowSpeedLimit=1",
"-c",
"http.lowSpeedTime=10",
"-c",
"credential.interactive=never"
]
);
assert!(env.contains(&("GIT_TERMINAL_PROMPT", "0")));
assert!(
env.contains(&("GIT_ASKPASS", "")),
"present and EMPTY disables every askpass"
);
assert!(env.contains(&("GCM_INTERACTIVE", "never")));
assert!(env.contains(&(
"GIT_SSH_COMMAND",
"ssh -o BatchMode=yes -o ConnectTimeout=10"
)));
let (_, env) = crate::git::remote_env(crate::git::Ssh::User);
assert!(
!env.iter().any(|(k, _)| *k == "GIT_SSH_COMMAND"),
"the user's own ssh command is left alone"
);
}
#[cfg(unix)]
#[test]
fn no_askpass_runs_against_an_origin_that_wants_credentials() {
use std::io::{Read, Write};
use std::os::unix::fs::PermissionsExt;
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
std::thread::spawn(move || {
for stream in listener.incoming().flatten() {
let mut s = stream;
let mut buf = [0u8; 4096];
let _ = s.read(&mut buf);
let _ = s.write_all(
b"HTTP/1.1 401 Unauthorized\r\nWWW-Authenticate: Basic realm=\"x\"\r\nContent-Length: 0\r\nConnection: close\r\n\r\n",
);
}
});
let work = repo("askpass");
let marker = work.join("asked");
let askpass = work.join("askpass.sh");
std::fs::write(
&askpass,
format!("#!/bin/sh\ntouch '{}'\necho x\n", marker.display()),
)
.unwrap();
std::fs::set_permissions(&askpass, std::fs::Permissions::from_mode(0o755)).unwrap();
git(
&work,
&["config", "core.askPass", askpass.to_str().unwrap()],
);
git(&work, &["config", "credential.helper", ""]);
let url = format!("http://127.0.0.1:{port}/x.git");
in_repo(&work, || {
let _ = crate::git::probe_remote(&["ls-remote", &url], 10);
});
assert!(
!marker.exists(),
"an askpass ran through the remote environment"
);
let _ = std::process::Command::new("git")
.current_dir(&work)
.args(["ls-remote", &url])
.env("GIT_TERMINAL_PROMPT", "0")
.env_remove("GIT_ASKPASS")
.env_remove("SSH_ASKPASS")
.stdin(std::process::Stdio::null())
.output();
assert!(
marker.exists(),
"negative control: without it the askpass does run"
);
let _ = std::fs::remove_dir_all(&work);
}
}