use super::{ChildCard, ChildInput, Notch};
use crate::git_tree::StepCommit;
const TAIL_CHARS: usize = 140;
const TAIL_LINES: usize = 2;
pub(super) fn card(
parent_name: &str,
parent_commits: &[StepCommit],
notches: &[Notch],
child: &ChildInput,
) -> Option<ChildCard> {
notches.first()?;
let shared = shared_prefix(parent_commits, &child.commits);
let context = shared
.checked_sub(1)
.and_then(|fork_pos| notch_at_or_before(notches, parent_commits, fork_pos));
let birth = child.commits.get(shared).or_else(|| child.commits.last())?;
let provenance_notch = notch_by_time(notches, parent_commits, birth.timestamp_unix);
Some(ChildCard {
agent_id: child.agent_id.clone(),
name: child.name.clone(),
fork: fork_label(parent_name, context.as_ref(), provenance_notch, child),
state: child.state,
tokens: child.tokens,
tail: child.streaming_text.as_deref().and_then(tail_of),
provenance_notch,
})
}
fn shared_prefix(parent: &[StepCommit], child: &[StepCommit]) -> usize {
parent
.iter()
.zip(child)
.take_while(|(a, b)| a.oid == b.oid)
.count()
}
fn position(parent_commits: &[StepCommit], notch: &Notch) -> Option<usize> {
let oid = notch.commit.as_deref()?;
parent_commits.iter().position(|c| c.oid == oid)
}
fn notch_at_or_before(
notches: &[Notch],
parent_commits: &[StepCommit],
pos: usize,
) -> Option<(usize, String)> {
notches
.iter()
.enumerate()
.filter(|(_, notch)| position(parent_commits, notch).is_some_and(|at| at <= pos))
.map(|(index, notch)| (index, notch.short()))
.next_back()
}
fn notch_by_time(notches: &[Notch], parent_commits: &[StepCommit], birth: i64) -> usize {
notches
.iter()
.enumerate()
.filter(|(_, notch)| {
position(parent_commits, notch)
.and_then(|at| parent_commits.get(at))
.is_some_and(|commit| commit.timestamp_unix <= birth)
})
.map(|(index, _)| index)
.next_back()
.unwrap_or(0)
}
fn fork_label(
parent_name: &str,
context: Option<&(usize, String)>,
provenance_notch: usize,
child: &ChildInput,
) -> String {
match context {
Some(&(index, _)) if index == provenance_notch => "from here".to_owned(),
Some((_, short)) => format!("from {parent_name}@{short}"),
None => child.config_label.as_ref().map_or_else(
|| "from a config commit".to_owned(),
|name| format!("from config/{name}"),
),
}
}
fn tail_of(text: &str) -> Option<String> {
let mut lines: Vec<&str> = text
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
.collect();
let keep = lines.len().saturating_sub(TAIL_LINES);
lines.drain(..keep);
let joined = lines.join(" ");
if joined.is_empty() {
return None;
}
Some(joined.chars().take(TAIL_CHARS).collect())
}