use std::collections::BTreeSet;
use std::path::PathBuf;
use anyhow::Result;
use super::Context;
use crate::logs::command_record::{CommandRecord, StageTime};
use crate::logs::reading::Command;
use crate::logs::session::original;
use crate::otlp_jsonl::SpanId;
pub(in crate::logs) struct Node<'a> {
pub command: &'a Command,
pub context: Context,
pub parent: Option<SpanId>,
pub fact: Fact<'a>,
}
pub(in crate::logs) enum Fact<'a> {
Command,
Run,
Stage(StageTime<'a>),
Session(Attachment<'a>),
}
pub(in crate::logs) struct Attachment<'a> {
pub path: PathBuf,
pub original: PathBuf,
pub stage: Option<StageTime<'a>>,
}
impl Node<'_> {
pub fn id(&self) -> Option<SpanId> {
match &self.fact {
Fact::Command => Some(self.context.command),
Fact::Run => Some(self.context.run),
Fact::Stage(time) => Some(self.context.stage_id(time.stage)),
Fact::Session(_) => None,
}
}
}
pub(in crate::logs) fn of<'a>(selected: &'a Command, owner: &Command) -> Result<Vec<Node<'a>>> {
let context = Context::of_session(&owner.command_log);
let mut walk = Walk {
claimed: BTreeSet::new(),
nodes: vec![Node {
command: selected,
context: context.clone(),
parent: None,
fact: Fact::Command,
}],
};
walk.record(selected, &context, context.command)?;
Ok(walk.nodes)
}
struct Walk<'a> {
claimed: BTreeSet<PathBuf>,
nodes: Vec<Node<'a>>,
}
impl<'a> Walk<'a> {
fn record(&mut self, command: &'a Command, outer: &Context, parent: SpanId) -> Result<()> {
let context = outer.for_record(&command.record);
let node = |parent, fact| Node {
command,
context: context.clone(),
parent: Some(parent),
fact,
};
self.nodes.push(node(parent, Fact::Run));
for time in command.record.stage_times(None) {
self.nodes.push(node(context.run, Fact::Stage(time)));
}
for child in command.dispatched.iter().chain(&command.children) {
let parent = dispatch_parent(&context, &command.record, &child.record);
self.record(child, outer, parent)?;
}
for path in command.sessions()? {
let identity = original(&path);
if !self.claimed.insert(identity.clone()) {
continue;
}
let stage = command.record.stage_times(None).find(|time| {
time.stage
.session_log
.as_deref()
.is_some_and(|reference| original(reference) == identity)
});
let parent = stage
.as_ref()
.map_or(context.run, |time| context.stage_id(time.stage));
self.nodes.push(node(
parent,
Fact::Session(Attachment {
path,
original: identity,
stage,
}),
));
}
Ok(())
}
}
fn dispatch_parent(context: &Context, record: &CommandRecord, child: &CommandRecord) -> SpanId {
let started = chrono::DateTime::parse_from_rfc3339(&child.started_at).ok();
record
.stages
.iter()
.rev()
.find(|stage| {
chrono::DateTime::parse_from_rfc3339(&stage.began_at)
.ok()
.zip(started)
.is_some_and(|(began, started)| began <= started)
})
.map_or(context.run, |stage| context.stage_id(stage))
}
#[cfg(test)]
mod tests;