use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};
use anyhow::{bail, Context, Result};
use serde::{Deserialize, Serialize};
pub const RECEIPTS_DIR: &str = "e2e-attestations";
const LEGACY_ATTESTATION: &str = "e2e-attestation.json";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Attestation {
pub command: String,
pub ran_at: u64,
pub exit_code: i32,
pub commit: String,
#[serde(default)]
pub branch: String,
}
pub fn branch_slug(branch: &str) -> String {
let mut slug = String::new();
for c in branch.to_lowercase().chars() {
let mapped = if c.is_ascii_lowercase() || c.is_ascii_digit() || c == '.' || c == '_' {
c
} else {
'-'
};
if mapped == '-' && slug.ends_with('-') {
continue;
}
slug.push(mapped);
}
let slug: String = slug.chars().take(80).collect();
let slug = slug.trim_matches(|c| c == '-' || c == '.');
if slug.is_empty() {
"branch".to_string()
} else {
slug.to_string()
}
}
pub(crate) fn current_branch(repo: &Path) -> Result<String> {
git_capture(repo, &["symbolic-ref", "--short", "-q", "HEAD"]).context(
"resolving the current branch — the receipt is keyed by branch, so this \
must run on a checked-out branch (a detached HEAD has none): `git switch <branch>`",
)
}
pub fn attest(repo: &Path, command: &str) -> Result<Attestation> {
let commit = git_capture(repo, &["rev-parse", "HEAD"])
.context("resolving HEAD — `e2e attest` must run inside a git repo with a commit")?;
let branch = current_branch(repo)?;
let status = run_shell(repo, command)?;
let exit_code = status.code().unwrap_or(-1);
let ran_at = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let attestation = Attestation {
command: command.to_string(),
ran_at,
exit_code,
commit,
branch: branch.clone(),
};
if exit_code != 0 {
return Ok(attestation);
}
let dir = repo.join(RECEIPTS_DIR);
std::fs::create_dir_all(&dir).with_context(|| format!("creating {}", dir.display()))?;
let path = dir.join(format!("{}.json", branch_slug(&branch)));
let json = serde_json::to_string_pretty(&attestation).context("serializing the receipt")?;
std::fs::write(&path, format!("{json}\n"))
.with_context(|| format!("writing {}", path.display()))?;
git_run(repo, &["add", "-A", "--", RECEIPTS_DIR])?;
let message = format!("e2e attestation for {branch}");
git_run(repo, &["commit", "-q", "-m", message.as_str()])?;
Ok(attestation)
}
fn run_shell(repo: &Path, command: &str) -> Result<std::process::ExitStatus> {
Command::new("sh")
.arg("-c")
.arg(command)
.current_dir(repo)
.status()
.with_context(|| format!("running e2e command `{command}`"))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Verification {
Fresh,
Missing,
}
pub fn verify(repo: &Path) -> Result<Verification> {
verify_scoped(repo, repo)
}
pub fn verify_scoped(repo: &Path, scope: &Path) -> Result<Verification> {
verify_since(repo, scope, None)
}
pub fn verify_since(repo: &Path, scope: &Path, base: Option<&str>) -> Result<Verification> {
verify_extra_scoped(repo, scope, base, &[], &[], None)
}
pub fn verify_extra_scoped(
repo: &Path,
scope: &Path,
base: Option<&str>,
extra_scopes: &[PathBuf],
excludes: &[PathBuf],
branch: Option<&str>,
) -> Result<Verification> {
let Some(base) = base else {
return Ok(if has_receipts(repo) {
Verification::Fresh
} else {
Verification::Missing
});
};
validate_scopes(repo, scope, extra_scopes)?;
let args = scoped_source_args(repo, scope, base, extra_scopes, excludes);
let arg_refs: Vec<&str> = args.iter().map(String::as_str).collect();
if !git_diff_changed(repo, &arg_refs)? {
return Ok(Verification::Fresh);
}
receipt_freshness(repo, base, branch)
}
fn receipt_freshness(repo: &Path, base: &str, branch: Option<&str>) -> Result<Verification> {
let range = format!("{base}...HEAD");
let acting_branch = branch
.map(str::to_string)
.or_else(|| current_branch(repo).ok());
let pathspec = receipt_pathspec(acting_branch.as_deref());
let receipt_diff = [
"diff",
"--name-only",
"--diff-filter=ACMRT",
&range,
"--",
&pathspec,
];
let out = git_capture(repo, &receipt_diff)?;
Ok(if out.is_empty() {
Verification::Missing
} else {
Verification::Fresh
})
}
fn scoped_source_args(
repo: &Path,
scope: &Path,
base: &str,
extra_scopes: &[PathBuf],
excludes: &[PathBuf],
) -> Vec<String> {
let mut args: Vec<String> = vec![
"diff".into(),
"--quiet".into(),
format!("{base}...HEAD"),
"--".into(),
relative_pathspec(repo, scope),
];
for extra in extra_scopes {
args.push(format!(":(top){}", extra.display()));
}
args.push(format!(":(exclude){RECEIPTS_DIR}"));
args.push(format!(":(exclude){LEGACY_ATTESTATION}"));
args.push(format!(":(top,exclude,glob)**/{RECEIPTS_DIR}/**"));
args.push(format!(":(top,exclude,glob)**/{LEGACY_ATTESTATION}"));
for exclude in excludes {
args.push(format!(":(top,exclude){}", exclude.display()));
}
args
}
fn receipt_pathspec(acting_branch: Option<&str>) -> String {
match acting_branch {
Some(branch) => format!("{RECEIPTS_DIR}/{}.json", branch_slug(branch)),
None => RECEIPTS_DIR.to_string(),
}
}
fn has_receipts(repo: &Path) -> bool {
let Ok(entries) = std::fs::read_dir(repo.join(RECEIPTS_DIR)) else {
return false;
};
entries
.flatten()
.any(|e| e.path().extension().is_some_and(|ext| ext == "json") && e.path().is_file())
}
fn relative_pathspec(repo: &Path, scope: &Path) -> String {
if scope == repo {
return ".".to_string();
}
match scope.strip_prefix(repo) {
Ok(rel) if !rel.as_os_str().is_empty() => rel.to_string_lossy().into_owned(),
_ => scope.to_string_lossy().into_owned(),
}
}
fn validate_scopes(repo: &Path, scope: &Path, extra_scopes: &[PathBuf]) -> Result<()> {
let scope_spec = relative_pathspec(repo, scope);
if !pathspec_matches_tracked(repo, &scope_spec)? {
bail!("{}", untracked_scope(repo, scope));
}
for extra in extra_scopes {
let extra_spec = format!(":(top){}", extra.display());
if !pathspec_matches_tracked(repo, &extra_spec)? {
bail!("{}", untracked_extra_scope(extra));
}
}
Ok(())
}
fn untracked_scope(repo: &Path, scope: &Path) -> String {
format!(
"e2e verify: --scope `{}` matches no tracked path under `{}` — \
--scope must name `{}` or a directory beneath it that git tracks",
scope.display(),
repo.display(),
repo.display(),
)
}
fn untracked_extra_scope(extra: &Path) -> String {
format!(
"e2e verify: --extra-scope `{}` matches no tracked path — \
--extra-scope must name a repo-root-relative directory that git tracks",
extra.display(),
)
}
fn pathspec_matches_tracked(repo: &Path, pathspec: &str) -> Result<bool> {
let out = Command::new("git")
.args(["ls-files", "--", pathspec])
.current_dir(repo)
.output()
.with_context(|| format!("running `git ls-files -- {pathspec}`"))?;
Ok(out.status.success() && !out.stdout.is_empty())
}
fn git_diff_changed(repo: &Path, args: &[&str]) -> Result<bool> {
let out = Command::new("git")
.args(args)
.current_dir(repo)
.output()
.with_context(|| format!("running `git {}`", args.join(" ")))?;
match out.status.code() {
Some(0) => Ok(false),
Some(1) => Ok(true),
_ => bail!(
"`git {}` failed: {}",
args.join(" "),
String::from_utf8_lossy(&out.stderr).trim()
),
}
}
fn git_capture(repo: &Path, args: &[&str]) -> Result<String> {
let out = Command::new("git")
.args(args)
.current_dir(repo)
.output()
.with_context(|| format!("running `git {}`", args.join(" ")))?;
if !out.status.success() {
bail!(
"`git {}` failed: {}",
args.join(" "),
String::from_utf8_lossy(&out.stderr).trim()
);
}
Ok(String::from_utf8(out.stdout)?.trim().to_string())
}
fn git_run(repo: &Path, args: &[&str]) -> Result<()> {
let status = Command::new("git")
.args(args)
.current_dir(repo)
.status()
.with_context(|| format!("running `git {}`", args.join(" ")))?;
if !status.success() {
bail!("`git {}` failed", args.join(" "));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::{
branch_slug, git_capture, git_diff_changed, git_run, pathspec_matches_tracked,
receipt_pathspec, run_shell, scoped_source_args, untracked_extra_scope, untracked_scope,
validate_scopes, LEGACY_ATTESTATION, RECEIPTS_DIR,
};
use std::path::{Path, PathBuf};
fn repo_tracking_src(slug: &str) -> PathBuf {
let repo = std::env::temp_dir().join(format!("tc-e2e-{slug}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&repo);
std::fs::create_dir_all(repo.join("src")).unwrap();
std::fs::write(repo.join("src").join("a.rs"), "pub fn f() {}\n").unwrap();
git_run(&repo, &["init", "-q"]).unwrap();
git_run(&repo, &["add", "-A", "--", "src"]).unwrap();
repo
}
#[test]
fn scope_validation_passes_a_tracked_scope_and_rejects_either_untracked_one() {
let repo = repo_tracking_src("validate-scopes");
let tracked = repo.join("src");
validate_scopes(&repo, &tracked, &[PathBuf::from("src")])
.expect("a tracked scope and a tracked extra scope both pass");
let err = format!(
"{:#}",
validate_scopes(&repo, &repo.join("nope"), &[]).unwrap_err()
);
assert!(err.contains("--scope `"), "got: {err}");
assert!(err.contains("matches no tracked path under"), "got: {err}");
let err = format!(
"{:#}",
validate_scopes(&repo, &tracked, &[PathBuf::from("nope")]).unwrap_err()
);
assert!(err.contains("--extra-scope `nope`"), "got: {err}");
let _ = std::fs::remove_dir_all(&repo);
}
#[test]
fn an_untracked_scope_names_the_scope_the_repository_and_what_scope_must_be() {
let message = untracked_scope(Path::new("/repo"), Path::new("/repo/packages/typo"));
assert_eq!(
message,
"e2e verify: --scope `/repo/packages/typo` matches no tracked path under `/repo` — \
--scope must name `/repo` or a directory beneath it that git tracks"
);
}
#[test]
fn an_untracked_extra_scope_names_the_scope_and_that_it_is_repo_root_relative() {
let message = untracked_extra_scope(Path::new("internals/typo"));
assert_eq!(
message,
"e2e verify: --extra-scope `internals/typo` matches no tracked path — \
--extra-scope must name a repo-root-relative directory that git tracks"
);
}
#[test]
fn a_named_branch_scopes_question_two_to_its_own_receipt() {
assert_eq!(
receipt_pathspec(Some("chore/750-ratchet-rust-step4")),
format!("{RECEIPTS_DIR}/chore-750-ratchet-rust-step4.json")
);
}
#[test]
fn with_no_branch_to_name_every_receipt_counts() {
assert_eq!(receipt_pathspec(None), RECEIPTS_DIR);
}
#[test]
fn the_scoped_source_argv_excludes_every_receipt_path_in_the_tree() {
let args = scoped_source_args(
Path::new("/repo"),
Path::new("/repo/packages/rust"),
"main",
&[],
&[],
);
assert!(args.starts_with(&[
"diff".to_string(),
"--quiet".to_string(),
"main...HEAD".to_string(),
"--".to_string(),
"packages/rust".to_string(),
]));
assert!(args.contains(&format!(":(exclude){RECEIPTS_DIR}")));
assert!(args.contains(&format!(":(top,exclude,glob)**/{RECEIPTS_DIR}/**")));
assert!(args.contains(&format!(":(exclude){LEGACY_ATTESTATION}")));
assert!(args.contains(&format!(":(top,exclude,glob)**/{LEGACY_ATTESTATION}")));
}
#[test]
fn extra_scopes_join_the_argv_and_excludes_subtract_from_it() {
let args = scoped_source_args(
Path::new("/repo"),
Path::new("/repo/packages/rust"),
"main",
&[PathBuf::from("packages/node")],
&[PathBuf::from("packages/rust/docs")],
);
assert!(args.contains(&":(top)packages/node".to_string()));
assert!(args.contains(&":(top,exclude)packages/rust/docs".to_string()));
}
const NOWHERE: &str = "/nonexistent-tc-e2e";
#[test]
fn run_shell_reports_a_spawn_failure_with_the_command() {
let err = run_shell(Path::new(NOWHERE), "true").unwrap_err();
assert!(format!("{err:#}").contains("running e2e command `true`"));
}
#[test]
fn pathspec_check_reports_a_spawn_failure() {
let err = pathspec_matches_tracked(Path::new(NOWHERE), "src").unwrap_err();
assert!(format!("{err:#}").contains("git ls-files -- src"));
}
#[test]
fn diff_check_reports_a_spawn_failure() {
let err = git_diff_changed(Path::new(NOWHERE), &["diff", "--quiet"]).unwrap_err();
assert!(format!("{err:#}").contains("running `git diff --quiet`"));
}
#[test]
fn capture_reports_a_spawn_failure() {
let err = git_capture(Path::new(NOWHERE), &["rev-parse", "HEAD"]).unwrap_err();
assert!(format!("{err:#}").contains("running `git rev-parse HEAD`"));
}
#[test]
fn run_reports_a_spawn_failure() {
let err = git_run(Path::new(NOWHERE), &["add", "-A"]).unwrap_err();
assert!(format!("{err:#}").contains("running `git add -A`"));
}
#[test]
fn slug_lowercases_and_maps_separators() {
assert_eq!(branch_slug("feature/one"), "feature-one");
assert_eq!(branch_slug("Feature/One"), "feature-one");
assert_eq!(
branch_slug("claude/e2e-attestation-conflicts-mrkc1b"),
"claude-e2e-attestation-conflicts-mrkc1b"
);
}
#[test]
fn slug_keeps_dots_and_underscores() {
assert_eq!(branch_slug("v1.2_rc"), "v1.2_rc");
}
#[test]
fn slug_collapses_runs_and_trims_edges() {
assert_eq!(branch_slug("wip//Émil's"), "wip-mil-s");
assert_eq!(branch_slug("--dashes--"), "dashes");
assert_eq!(branch_slug(".hidden."), "hidden");
}
#[test]
fn slug_truncates_to_80() {
let long = "x".repeat(300);
assert_eq!(branch_slug(&long).len(), 80);
}
#[test]
fn slug_never_returns_empty() {
assert_eq!(branch_slug(""), "branch");
assert_eq!(branch_slug("É"), "branch");
}
}