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