cli/git_projection_engine/
git_notes.rs1#![deny(clippy::cast_possible_truncation)]
3
4use std::{
16 collections::HashMap,
17 time::{SystemTime, UNIX_EPOCH},
18};
19
20use objects::object::{ChangeId, State, Status};
21use serde::{Deserialize, Serialize};
22use sley::{ObjectId, Repository};
23
24use super::git_core::{GitProjectionError, GitProjectionResult, git_err};
25
26pub const NOTES_REF: &str = "refs/notes/heddle";
29
30#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
32pub struct HeddleNote {
33 pub change_id: String,
35 #[serde(default, skip_serializing_if = "Option::is_none")]
36 pub agent: Option<NoteAgent>,
37 #[serde(default, skip_serializing_if = "Option::is_none")]
38 pub confidence: Option<f32>,
39 pub status: String,
41 #[serde(default, skip_serializing_if = "Option::is_none")]
46 pub omitted_annotations_breakdown: Option<OmittedBreakdown>,
47 #[serde(default, skip_serializing_if = "Option::is_none")]
50 pub signal_counts: Option<SignalCounts>,
51 #[serde(default, skip_serializing_if = "Option::is_none")]
54 pub attribution: Option<NoteAttribution>,
55}
56
57#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
58pub struct NoteAgent {
59 pub provider: String,
60 pub model: String,
61}
62
63#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
65pub struct OmittedBreakdown {
66 #[serde(default)]
67 pub internal: u32,
68 #[serde(default)]
69 pub team: u32,
70 #[serde(default)]
71 pub restricted: u32,
72}
73
74#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
76pub struct SignalCounts {
77 #[serde(default)]
78 pub novelty: u32,
79 #[serde(default)]
80 pub test_reachability: u32,
81 #[serde(default)]
82 pub pattern_deviation: u32,
83 #[serde(default)]
84 pub invariant_adjacency: u32,
85 #[serde(default)]
86 pub self_flagged_uncertainty: u32,
87}
88
89#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
90pub struct NoteAttribution {
91 pub principal_name: String,
92 pub principal_email: String,
93 #[serde(default, skip_serializing_if = "Option::is_none")]
94 pub agent: Option<NoteAgent>,
95}
96
97impl HeddleNote {
98 pub fn from_state(state: &State) -> Self {
100 let status = match state.status {
101 Status::Draft => "draft".to_string(),
102 Status::Published => "published".to_string(),
103 };
104 let agent = state.attribution.agent.as_ref().map(|a| NoteAgent {
105 provider: a.provider.clone(),
106 model: a.model.clone(),
107 });
108 Self {
109 change_id: state.change_id.to_string_full(),
110 agent,
111 confidence: state.confidence,
112 status,
113 omitted_annotations_breakdown: None,
114 signal_counts: None,
115 attribution: None,
116 }
117 }
118
119 pub fn with_omitted_breakdown(mut self, breakdown: OmittedBreakdown) -> Self {
121 self.omitted_annotations_breakdown = Some(breakdown);
122 self
123 }
124
125 pub fn with_signal_counts(mut self, counts: SignalCounts) -> Self {
127 self.signal_counts = Some(counts);
128 self
129 }
130
131 pub fn with_attribution(mut self, attribution: NoteAttribution) -> Self {
133 self.attribution = Some(attribution);
134 self
135 }
136
137 pub fn to_json_bytes(&self) -> GitProjectionResult<Vec<u8>> {
138 serde_json::to_vec_pretty(self)
139 .map_err(|e| GitProjectionError::Git(format!("note serialize: {e}")))
140 }
141
142 pub fn from_json_bytes(bytes: &[u8]) -> GitProjectionResult<Self> {
143 serde_json::from_slice(bytes)
144 .map_err(|e| GitProjectionError::Git(format!("note parse: {e}")))
145 }
146}
147
148fn notes_ref() -> sley::notes::NotesRef {
149 sley::notes::NotesRef::expand(NOTES_REF)
150}
151
152pub fn write_note(
157 repo: &Repository,
158 commit_oid: ObjectId,
159 note: &HeddleNote,
160) -> GitProjectionResult<()> {
161 let json = note.to_json_bytes()?;
162 let notes_ref = notes_ref();
163 let refs = repo.references();
164 sley::notes::upsert_note_bytes_for(
165 repo.git_dir(),
166 repo.object_format(),
167 &refs,
168 ¬es_ref,
169 &commit_oid,
170 &json,
171 "heddle: state metadata",
172 &git_projection_notes_identity(),
173 sley::notes::notes_ref_expected(&refs, ¬es_ref).map_err(git_err)?,
174 )
175 .map_err(git_err)?;
176 Ok(())
177}
178
179pub fn remove_notes(
194 repo: &Repository,
195 commit_oids: &std::collections::HashSet<ObjectId>,
196) -> GitProjectionResult<()> {
197 if commit_oids.is_empty() {
198 return Ok(());
199 }
200 let notes_ref = notes_ref();
201 let refs = repo.references();
202 let annotated: Vec<ObjectId> = commit_oids.iter().copied().collect();
203 sley::notes::remove_notes_for(
204 repo.git_dir(),
205 repo.object_format(),
206 &refs,
207 ¬es_ref,
208 &annotated,
209 "heddle: retract state metadata",
210 &git_projection_notes_identity(),
211 sley::notes::notes_ref_expected(&refs, ¬es_ref).map_err(git_err)?,
212 )
213 .map_err(git_err)?;
214 Ok(())
215}
216
217pub fn read_note(
219 repo: &Repository,
220 commit_oid: ObjectId,
221) -> GitProjectionResult<Option<HeddleNote>> {
222 let Some(bytes) = repo
223 .read_note_bytes(¬es_ref(), &commit_oid)
224 .map_err(git_err)?
225 else {
226 return Ok(None);
227 };
228 HeddleNote::from_json_bytes(&bytes).map(Some)
229}
230
231pub(crate) fn read_identity_mappings(
233 repo: &Repository,
234) -> GitProjectionResult<Vec<(ChangeId, ObjectId)>> {
235 read_all_notes(repo)?
236 .into_iter()
237 .map(|(oid, note)| Ok((ChangeId::parse(¬e.change_id)?, oid)))
238 .collect()
239}
240
241pub(crate) fn read_all_notes(
243 repo: &Repository,
244) -> GitProjectionResult<HashMap<ObjectId, HeddleNote>> {
245 let mut out = HashMap::new();
246 for note_entry in repo.list_notes(¬es_ref()).map_err(git_err)? {
247 let object = repo.read_object(¬e_entry.blob).map_err(git_err)?;
248 if object.object_type != sley::GitObjectType::Blob {
251 continue;
252 }
253 if let Ok(note) = HeddleNote::from_json_bytes(&object.body) {
254 out.insert(note_entry.annotated, note);
255 }
256 }
257 Ok(out)
258}
259
260fn git_projection_notes_identity() -> sley::notes::NotesCommitIdentity {
261 let seconds = SystemTime::now()
262 .duration_since(UNIX_EPOCH)
263 .map(|d| d.as_secs() as i64)
264 .unwrap_or(0);
265 let ident = format!("Heddle <heddle@local> {seconds} +0000").into_bytes();
266 sley::notes::NotesCommitIdentity {
267 author: ident.clone(),
268 committer: ident,
269 }
270}