//! The git-commit-shaped image a snapshot exports to.
use core::fmt;
use jac_core::{BlobId, RepoPath, SnapshotId};
/// Trailer keys jacquard writes onto exported commits.
///
/// The vocabulary a knowledge layer greps for: because these are ordinary
/// git trailers on ordinary commits, anything that already crawls the repo —
/// blame tooling, enterprise search, an AI reviewer citing prior decisions —
/// indexes jacquard's provenance for free.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum TrailerKey {
/// `Jacquard-Provenance`: human | agent | mixed.
Provenance,
/// `Jacquard-Author`: `human:hum-…` or `agent:agt-…`.
Author,
/// `Jacquard-Timestamp`: milliseconds since the epoch.
Timestamp,
/// `Jacquard-Decision`: a decision id this change was made under.
Decision,
}
impl TrailerKey {
/// The literal trailer key.
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Provenance => "Jacquard-Provenance",
Self::Author => "Jacquard-Author",
Self::Timestamp => "Jacquard-Timestamp",
Self::Decision => "Jacquard-Decision",
}
}
}
impl fmt::Display for TrailerKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
/// What a snapshot looks like as a git commit.
///
/// The manifest flattens the tree to `(path, blob)` pairs — how a real
/// adapter would drive `git fast-import` — and the trailers carry everything
/// git's own commit format has no field for. If a datum is not in this
/// struct, the bridge does not preserve it; that is the point of modelling
/// the image explicitly.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GitCommitImage {
/// Every file in the snapshot, as full path plus blob id, sorted.
pub manifest: Vec<(RepoPath, BlobId)>,
/// Parent snapshots.
pub parents: Vec<SnapshotId>,
/// The commit message, without trailers.
pub message: String,
/// The jacquard trailers.
pub trailers: Vec<(TrailerKey, String)>,
}
impl GitCommitImage {
/// The first value for a trailer key, if present.
#[must_use]
pub fn trailer(&self, key: TrailerKey) -> Option<&str> {
self.trailers
.iter()
.find(|(k, _)| *k == key)
.map(|(_, v)| v.as_str())
}
/// Renders the full commit message with trailers appended, as a real
/// adapter would write it.
#[must_use]
pub fn full_message(&self) -> String {
use core::fmt::Write as _;
let mut out = self.message.clone();
if !self.trailers.is_empty() {
out.push_str("\n\n");
for (key, value) in &self.trailers {
let _ = writeln!(out, "{key}: {value}");
}
}
out
}
}