use std::collections::HashMap;
use objects::object::{State, Status};
use super::git_core::GitBridge;
impl<'a> GitBridge<'a> {
pub(crate) fn parse_trailers(message: &str) -> HashMap<String, String> {
let mut trailers = HashMap::new();
for line in message.lines().rev() {
if line.is_empty() {
break;
}
if let Some(pos) = line.find(':') {
let key = &line[..pos];
let value = line[pos + 1..].trim();
if key.starts_with("Heddle-") {
trailers.insert(key.to_string(), value.to_string());
}
} else if !line.trim().is_empty() {
break;
}
}
trailers
}
pub(crate) fn extract_intent(message: &str) -> Option<String> {
let lines: Vec<&str> = message.lines().collect();
for line in &lines {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if trimmed.starts_with("Heddle-") && trimmed.contains(':') {
break;
}
return Some(trimmed.to_string());
}
None
}
pub(crate) fn state_to_signature(state: &State) -> gix::actor::Signature {
gix::actor::Signature {
name: state.attribution.principal.name.as_str().into(),
email: state.attribution.principal.email.as_str().into(),
time: gix::date::Time {
seconds: state.created_at.timestamp(),
offset: 0,
},
}
}
pub(crate) fn build_commit_message(state: &State) -> String {
let _ = Status::Draft;
state
.intent
.clone()
.unwrap_or_else(|| "No intent specified".to_string())
}
pub(crate) fn build_commit_message_with_footer(
state: &State,
hosted_url: Option<&str>,
annotations_omitted: u32,
) -> String {
let body = Self::build_commit_message(state);
let mut out = body;
if !out.ends_with('\n') {
out.push('\n');
}
out.push('\n');
out.push_str(&format!(
"Heddle-State: {}\n",
state.change_id.to_string_full()
));
if let Some(url) = hosted_url
&& !url.is_empty()
{
let trimmed = url.trim_end_matches('/');
out.push_str(&format!(
"Heddle-URL: {trimmed}/state/{}\n",
state.change_id.to_string_full()
));
}
out.push_str(&format!(
"Heddle-Annotations-Omitted: {annotations_omitted}\n"
));
out
}
}
#[derive(Debug, Default)]
pub struct ExportStats {
pub states_exported: usize,
pub threads_synced: usize,
pub markers_synced: usize,
}
#[derive(Debug, Default)]
pub struct ImportStats {
pub commits_imported: usize,
pub states_created: usize,
pub branches_synced: usize,
pub tags_synced: usize,
pub skipped_non_commit_refs: Vec<SkippedRef>,
pub partial_mirror_refs: Vec<PartialMirrorRef>,
}
#[derive(Debug, Clone)]
pub struct SkippedRef {
pub name: String,
pub peeled_oid: String,
pub peeled_kind: String,
}
#[derive(Debug, Clone)]
pub struct PartialMirrorRef {
pub name: String,
pub error: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_trailers() {
let message = r#"Add feature X
This is the body.
Heddle-Change-Id: hd-abc123
Heddle-Agent: anthropic/claude
Heddle-Confidence: 0.95
"#;
let trailers = GitBridge::parse_trailers(message);
assert_eq!(
trailers.get("Heddle-Change-Id"),
Some(&"hd-abc123".to_string())
);
assert_eq!(
trailers.get("Heddle-Agent"),
Some(&"anthropic/claude".to_string())
);
assert_eq!(trailers.get("Heddle-Confidence"), Some(&"0.95".to_string()));
}
#[test]
fn test_extract_intent() {
let message = "Add feature X\n\nBody here\n\nHeddle-Change-Id: hd-abc123";
assert_eq!(
GitBridge::extract_intent(message),
Some("Add feature X".to_string())
);
let message2 = "Heddle-Change-Id: hd-abc123";
assert_eq!(GitBridge::extract_intent(message2), None);
}
use objects::object::{Attribution, ChangeId, ContentHash, Principal};
fn sample_state() -> State {
State::new_snapshot(
ContentHash::compute(b"tree"),
vec![],
Attribution::human(Principal::new("Alice", "alice@example.com")),
)
.with_intent("ship the auth rewrite")
}
#[test]
fn footer_emits_state_id_and_zero_omitted_when_no_url() {
let state = sample_state();
let msg = GitBridge::build_commit_message_with_footer(&state, None, 0);
assert!(msg.contains(&format!(
"Heddle-State: {}",
state.change_id.to_string_full()
)));
assert!(msg.contains("Heddle-Annotations-Omitted: 0"));
assert!(!msg.contains("Heddle-URL:"));
}
#[test]
fn footer_emits_url_when_hosted_configured() {
let state = sample_state();
let msg =
GitBridge::build_commit_message_with_footer(&state, Some("https://heddle.test/"), 3);
assert!(msg.contains(&format!(
"Heddle-URL: https://heddle.test/state/{}",
state.change_id.to_string_full()
)));
assert!(msg.contains("Heddle-Annotations-Omitted: 3"));
}
#[test]
fn change_id_round_trips_through_footer() {
let state = sample_state();
let id_str = state.change_id.to_string_full();
let _: ChangeId = ChangeId::parse(&id_str).expect("round-trip parse");
}
}