use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
pub const FORMAT: &str = "amont-gate-v1";
pub const NOTES_REF: &str = "amont-gate";
pub const NOTES_FULL_REF: &str = "refs/notes/amont-gate";
const MARKER: &str = "amont-gate";
fn marker_path() -> Option<PathBuf> {
let dir = crate::git::stdout(&["rev-parse", "--git-dir"])?;
Some(std::path::Path::new(&dir).join(MARKER))
}
pub fn record(scripts: &[&str]) {
let Some(path) = marker_path() else { return };
if scripts.is_empty() {
let _ = std::fs::remove_file(&path);
return;
}
let Some(tree) = crate::git::stdout(&["write-tree"]) else {
let _ = std::fs::remove_file(&path);
return;
};
let mut body = format!("{FORMAT}\n{tree}\n");
for s in scripts {
body.push_str(s);
body.push('\n');
}
let _ = std::fs::write(&path, body);
}
pub fn bind_to_head() -> Vec<String> {
let Some(path) = marker_path() else {
return Vec::new();
};
let Ok(body) = std::fs::read_to_string(&path) else {
return Vec::new(); };
let _ = std::fs::remove_file(&path);
let mut lines = body.lines();
if lines.next() != Some(FORMAT) {
return Vec::new();
}
let Some(tree) = lines.next() else {
return Vec::new();
};
let scripts: Vec<&str> = lines.filter(|l| !l.trim().is_empty()).collect();
if scripts.is_empty() {
return Vec::new();
}
let Some(head_tree) = crate::git::stdout(&["rev-parse", "HEAD^{tree}"]) else {
return Vec::new();
};
if head_tree != tree {
return Vec::new();
}
let note = format!("{FORMAT} {}", scripts.join(" "));
if !crate::git::succeeds(&[
"notes", "--ref", NOTES_REF, "add", "-f", "-m", ¬e, "HEAD",
]) {
return Vec::new(); }
scripts.iter().map(|s| s.to_string()).collect()
}
pub fn stamps_for(commits: &[String]) -> HashMap<String, Vec<String>> {
let mut out = HashMap::new();
if commits.is_empty() {
return out;
}
let Some(list) = crate::git::stdout(&["notes", "--ref", NOTES_REF, "list"]) else {
return out; };
let noted: HashSet<&str> = list
.lines()
.filter_map(|l| l.split_whitespace().nth(1))
.collect();
for commit in commits {
if !noted.contains(commit.as_str()) {
continue;
}
let Some(body) = crate::git::stdout(&["notes", "--ref", NOTES_REF, "show", commit]) else {
continue;
};
let Some(first) = body.lines().next() else {
continue;
};
let mut tokens = first.split_whitespace();
if tokens.next() != Some(FORMAT) {
continue;
}
out.insert(commit.clone(), tokens.map(str::to_string).collect());
}
out
}
pub fn forget() {
if let Some(path) = marker_path() {
let _ = std::fs::remove_file(&path);
}
let _ = crate::git::succeeds(&["update-ref", "-d", NOTES_FULL_REF]);
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
fn repo(name: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("gate-stamp-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
git(&dir, &["init", "-q", "--template=", "."]);
git(&dir, &["config", "user.email", "t@t.test"]);
git(&dir, &["config", "user.name", "t"]);
dir
}
fn git(dir: &Path, args: &[&str]) -> String {
let out = std::process::Command::new("git")
.arg("-C")
.arg(dir)
.args(args)
.output()
.expect("git");
String::from_utf8_lossy(&out.stdout).trim().to_string()
}
static CWD: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn in_repo<T>(dir: &Path, f: impl FnOnce() -> T) -> T {
let _guard = 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 a_recorded_marker_becomes_a_stamp_on_the_matching_commit() {
let dir = repo("roundtrip");
std::fs::write(dir.join("a.ts"), "x").unwrap();
git(&dir, &["add", "a.ts"]);
in_repo(&dir, || {
record(&["typecheck", "test"]);
git(&dir, &["commit", "-qm", "chore: a"]);
let stamped = bind_to_head();
assert_eq!(
stamped,
vec!["typecheck".to_string(), "test".to_string()],
"bind_to_head reports the scripts it stamped"
);
let head = git(&dir, &["rev-parse", "HEAD"]);
let stamps = stamps_for(std::slice::from_ref(&head));
assert_eq!(
stamps.get(&head).map(Vec::as_slice),
Some(&["typecheck".to_string(), "test".to_string()][..])
);
assert!(!dir.join(".git").join(MARKER).exists());
});
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn a_marker_for_a_different_tree_stamps_nothing() {
let dir = repo("stale");
std::fs::write(dir.join("a.ts"), "x").unwrap();
git(&dir, &["add", "a.ts"]);
in_repo(&dir, || {
record(&["typecheck"]);
std::fs::write(dir.join("a.ts"), "y").unwrap();
git(&dir, &["add", "a.ts"]);
git(&dir, &["commit", "-qm", "chore: different"]);
assert!(
bind_to_head().is_empty(),
"bind_to_head reports nothing when the tree moved"
);
let head = git(&dir, &["rev-parse", "HEAD"]);
assert!(
stamps_for(&[head]).is_empty(),
"a stale marker must not vouch"
);
assert!(
!dir.join(".git").join(MARKER).exists(),
"consumed either way"
);
});
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn an_empty_record_clears_a_previous_marker() {
let dir = repo("clears");
std::fs::write(dir.join("a.ts"), "x").unwrap();
git(&dir, &["add", "a.ts"]);
in_repo(&dir, || {
record(&["typecheck"]);
assert!(dir.join(".git").join(MARKER).exists());
record(&[]);
assert!(!dir.join(".git").join(MARKER).exists());
});
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn a_marker_in_an_unknown_format_stamps_nothing() {
let dir = repo("wrongformat");
std::fs::write(dir.join("a.ts"), "x").unwrap();
git(&dir, &["add", "a.ts"]);
in_repo(&dir, || {
let tree = git(&dir, &["write-tree"]);
let marker = dir.join(".git").join(MARKER);
std::fs::write(&marker, format!("amont-gate-v99\n{tree}\ntypecheck\n")).unwrap();
git(&dir, &["commit", "-qm", "chore: a"]);
bind_to_head();
let head = git(&dir, &["rev-parse", "HEAD"]);
assert!(
stamps_for(std::slice::from_ref(&head)).is_empty(),
"an unknown format was trusted"
);
assert!(!marker.exists(), "consumed either way");
});
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn a_foreign_note_is_not_a_stamp() {
let dir = repo("foreignnote");
std::fs::write(dir.join("a.ts"), "x").unwrap();
git(&dir, &["add", "a.ts"]);
in_repo(&dir, || {
git(&dir, &["commit", "-qm", "chore: a"]);
git(
&dir,
&[
"notes",
"--ref",
NOTES_REF,
"add",
"-m",
"typecheck test",
"HEAD",
],
);
let head = git(&dir, &["rev-parse", "HEAD"]);
assert!(
stamps_for(std::slice::from_ref(&head)).is_empty(),
"a note without the format token was trusted"
);
});
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn forget_removes_the_stamps() {
let dir = repo("forget");
std::fs::write(dir.join("a.ts"), "x").unwrap();
git(&dir, &["add", "a.ts"]);
in_repo(&dir, || {
record(&["typecheck"]);
git(&dir, &["commit", "-qm", "chore: a"]);
bind_to_head();
let head = git(&dir, &["rev-parse", "HEAD"]);
assert!(!stamps_for(std::slice::from_ref(&head)).is_empty());
forget();
assert!(stamps_for(&[head]).is_empty());
});
let _ = std::fs::remove_dir_all(&dir);
}
}