#![deny(clippy::cast_possible_truncation)]
use std::{
collections::HashMap,
time::{SystemTime, UNIX_EPOCH},
};
use objects::object::{ChangeId, State, Status};
use serde::{Deserialize, Serialize};
use sley::{ObjectId, Repository};
use super::git_core::{GitBridgeError, GitResult, git_err};
pub const NOTES_REF: &str = "refs/notes/heddle";
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct HeddleNote {
pub change_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent: Option<NoteAgent>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub confidence: Option<f32>,
pub status: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub omitted_annotations_breakdown: Option<OmittedBreakdown>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signal_counts: Option<SignalCounts>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub attribution: Option<NoteAttribution>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct NoteAgent {
pub provider: String,
pub model: String,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct OmittedBreakdown {
#[serde(default)]
pub internal: u32,
#[serde(default)]
pub team: u32,
#[serde(default)]
pub restricted: u32,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct SignalCounts {
#[serde(default)]
pub novelty: u32,
#[serde(default)]
pub test_reachability: u32,
#[serde(default)]
pub pattern_deviation: u32,
#[serde(default)]
pub invariant_adjacency: u32,
#[serde(default)]
pub self_flagged_uncertainty: u32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct NoteAttribution {
pub principal_name: String,
pub principal_email: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent: Option<NoteAgent>,
}
impl HeddleNote {
pub fn from_state(state: &State) -> Self {
let status = match state.status {
Status::Draft => "draft".to_string(),
Status::Published => "published".to_string(),
};
let agent = state.attribution.agent.as_ref().map(|a| NoteAgent {
provider: a.provider.clone(),
model: a.model.clone(),
});
Self {
change_id: state.change_id.to_string_full(),
agent,
confidence: state.confidence,
status,
omitted_annotations_breakdown: None,
signal_counts: None,
attribution: None,
}
}
pub fn with_omitted_breakdown(mut self, breakdown: OmittedBreakdown) -> Self {
self.omitted_annotations_breakdown = Some(breakdown);
self
}
pub fn with_signal_counts(mut self, counts: SignalCounts) -> Self {
self.signal_counts = Some(counts);
self
}
pub fn with_attribution(mut self, attribution: NoteAttribution) -> Self {
self.attribution = Some(attribution);
self
}
pub fn to_json_bytes(&self) -> GitResult<Vec<u8>> {
serde_json::to_vec_pretty(self)
.map_err(|e| GitBridgeError::Git(format!("note serialize: {e}")))
}
pub fn from_json_bytes(bytes: &[u8]) -> GitResult<Self> {
serde_json::from_slice(bytes).map_err(|e| GitBridgeError::Git(format!("note parse: {e}")))
}
}
fn notes_ref() -> sley::notes::NotesRef {
sley::notes::NotesRef::expand(NOTES_REF)
}
pub fn write_note(repo: &Repository, commit_oid: ObjectId, note: &HeddleNote) -> GitResult<()> {
let json = note.to_json_bytes()?;
let notes_ref = notes_ref();
let refs = repo.references();
sley::notes::upsert_note_bytes_for(
repo.git_dir(),
repo.object_format(),
&refs,
¬es_ref,
&commit_oid,
&json,
"heddle: state metadata",
&bridge_notes_identity(),
sley::notes::notes_ref_expected(&refs, ¬es_ref).map_err(git_err)?,
)
.map_err(git_err)?;
Ok(())
}
pub fn remove_notes(
repo: &Repository,
commit_oids: &std::collections::HashSet<ObjectId>,
) -> GitResult<()> {
if commit_oids.is_empty() {
return Ok(());
}
let notes_ref = notes_ref();
let refs = repo.references();
let annotated: Vec<ObjectId> = commit_oids.iter().copied().collect();
sley::notes::remove_notes_for(
repo.git_dir(),
repo.object_format(),
&refs,
¬es_ref,
&annotated,
"heddle: retract state metadata",
&bridge_notes_identity(),
sley::notes::notes_ref_expected(&refs, ¬es_ref).map_err(git_err)?,
)
.map_err(git_err)?;
Ok(())
}
pub fn read_note(repo: &Repository, commit_oid: ObjectId) -> GitResult<Option<HeddleNote>> {
let Some(bytes) = repo
.read_note_bytes(¬es_ref(), &commit_oid)
.map_err(git_err)?
else {
return Ok(None);
};
HeddleNote::from_json_bytes(&bytes).map(Some)
}
pub(crate) fn read_identity_mappings(repo: &Repository) -> GitResult<Vec<(ChangeId, ObjectId)>> {
read_all_notes(repo)?
.into_iter()
.map(|(oid, note)| Ok((ChangeId::parse(¬e.change_id)?, oid)))
.collect()
}
pub(crate) fn read_all_notes(repo: &Repository) -> GitResult<HashMap<ObjectId, HeddleNote>> {
let mut out = HashMap::new();
for note_entry in repo.list_notes(¬es_ref()).map_err(git_err)? {
let object = repo.read_object(¬e_entry.blob).map_err(git_err)?;
if object.object_type != sley::GitObjectType::Blob {
continue;
}
if let Ok(note) = HeddleNote::from_json_bytes(&object.body) {
out.insert(note_entry.annotated, note);
}
}
Ok(out)
}
fn bridge_notes_identity() -> sley::notes::NotesCommitIdentity {
let seconds = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
let ident = format!("Heddle <heddle@local> {seconds} +0000").into_bytes();
sley::notes::NotesCommitIdentity {
author: ident.clone(),
committer: ident,
}
}