speccoder_runtime/attest/
notes.rs1use std::path::Path;
5
6use crate::git::Git;
7
8pub const REF: &str = "refs/notes/speccoder";
10
11const IDENTITY: [&str; 6] = [
12 "-c",
13 "user.name=speccoder",
14 "-c",
15 "user.email=speccoder@users.noreply.invalid",
16 "-c",
17 "commit.gpgsign=false",
18];
19
20static WRITING: std::sync::Mutex<()> = std::sync::Mutex::new(());
22
23struct Held(Option<std::path::PathBuf>);
29
30impl Drop for Held {
31 fn drop(&mut self) {
32 if let Some(p) = self.0.take() {
33 let _ = std::fs::remove_file(p);
34 }
35 }
36}
37
38fn hold(repo: &Path) -> Held {
39 let dir = Git::at(repo)
40 .line(&["rev-parse", "--path-format=absolute", "--git-common-dir"])
41 .ok()
42 .map(std::path::PathBuf::from);
43 let Some(file) = dir.map(|d| d.join("speccoder-notes.lock")) else {
44 return Held(None);
45 };
46 for _ in 0..200 {
47 match std::fs::OpenOptions::new()
48 .write(true)
49 .create_new(true)
50 .open(&file)
51 {
52 Ok(_) => return Held(Some(file)),
53 Err(_) => {
54 let stale = std::fs::metadata(&file)
55 .and_then(|m| m.modified())
56 .ok()
57 .and_then(|t| t.elapsed().ok())
58 .is_some_and(|age| age > std::time::Duration::from_secs(60));
59 if stale {
60 let _ = std::fs::remove_file(&file);
61 } else {
62 std::thread::sleep(std::time::Duration::from_millis(25));
63 }
64 }
65 }
66 }
67 Held(None)
69}
70
71pub fn write(repo: &Path, object: &str, bundle: &str) -> Result<(), String> {
73 let _one = WRITING
74 .lock()
75 .unwrap_or_else(std::sync::PoisonError::into_inner);
76 let _held = hold(repo);
77 let mut args = IDENTITY.to_vec();
78 args.extend(["notes", "--ref=speccoder", "add", "-f", "-F", "-", object]);
79 Git::at(repo)
80 .run_with_stdin(&args, bundle.as_bytes())
81 .map(|_| ())
82 .map_err(|e| e.0)
83}
84
85pub fn remove(repo: &Path, object: &str) {
87 let _one = WRITING
88 .lock()
89 .unwrap_or_else(std::sync::PoisonError::into_inner);
90 let _held = hold(repo);
91 let mut args = IDENTITY.to_vec();
92 args.extend([
93 "notes",
94 "--ref=speccoder",
95 "remove",
96 "--ignore-missing",
97 object,
98 ]);
99 let _ = Git::at(repo).run(&args);
100}
101
102#[must_use]
104pub fn read(repo: &Path, object: &str) -> Option<String> {
105 Git::at(repo)
106 .run(&["notes", "--ref=speccoder", "show", object])
107 .ok()
108 .map(|b| String::from_utf8_lossy(&b).into_owned())
109}
110
111#[must_use]
113pub fn noted(repo: &Path) -> std::collections::BTreeSet<String> {
114 Git::at(repo)
115 .run(&["notes", "--ref=speccoder", "list"])
116 .map(|b| {
117 String::from_utf8_lossy(&b)
118 .lines()
119 .filter_map(|l| l.split_whitespace().nth(1).map(str::to_owned))
120 .collect()
121 })
122 .unwrap_or_default()
123}