use api::heddle::api::v1alpha1::{
ConfidenceBand as ProtoConfidenceBand, IntegrationPolicyStatus as ProtoIntegrationPolicyStatus,
ThreadFreshness as ProtoThreadFreshness, ThreadMode as ProtoThreadMode, ThreadSummary,
thread_state::Kind as ProtoThreadState,
};
use objects::{object::StateId, store::ObjectStore};
use repo::{Repository, SyncedThreadMetadata};
use wire::ProtocolError;
pub(super) fn from_summary(
repo: &Repository,
remote_thread: &str,
pulled_state: StateId,
summary: ThreadSummary,
) -> Result<SyncedThreadMetadata, ProtocolError> {
if summary.name != remote_thread {
return Err(invalid_metadata(
remote_thread,
format!("was named '{}'", summary.name),
));
}
let base_state = super::helpers::parse_proto_state_id(summary.base_state)?
.ok_or_else(|| invalid_metadata(remote_thread, "is missing its managed base state"))?;
let advertised_current = super::helpers::parse_proto_state_id(summary.current_state)?;
if advertised_current != Some(pulled_state) {
return Err(invalid_metadata(
remote_thread,
format!("does not match pulled state {pulled_state}"),
));
}
let base_root = repo
.store()
.get_state(&base_state)?
.ok_or_else(|| {
invalid_metadata(
remote_thread,
format!("names base state {base_state}, which was not transferred"),
)
})?
.tree
.short();
let state = thread_state(remote_thread, summary.thread_state)?;
let now = chrono::Utc::now();
Ok(SyncedThreadMetadata {
id: remote_thread.to_string(),
thread: remote_thread.to_string(),
target_thread: summary.target_thread,
parent_thread: summary.parent_thread,
mode: thread_mode(remote_thread, summary.thread_mode)?,
state: state.clone(),
base_state: base_state.short(),
base_root,
current_state: Some(pulled_state.short()),
merged_state: (state == repo::ThreadState::Merged).then(|| pulled_state.short()),
task: summary.task,
changed_paths: summary.changed_paths,
impact_categories: impact_categories(remote_thread, summary.impact_categories)?,
heavy_impact_paths: summary.heavy_impact_paths,
promotion_suggested: summary.promotion_suggested,
freshness: freshness(summary.freshness),
verification_summary: verification_summary(summary.verification_summary),
confidence_summary: confidence_summary(summary.confidence_summary),
integration_policy_result: integration_policy(summary.integration_policy_result),
created_at: now,
updated_at: now,
ephemeral: None,
auto: false,
shared_target_dir: None,
})
}
fn invalid_metadata(remote_thread: &str, detail: impl std::fmt::Display) -> ProtocolError {
ProtocolError::InvalidState(format!("hosted thread '{remote_thread}' metadata {detail}"))
}
fn thread_state(remote_thread: &str, value: i32) -> Result<repo::ThreadState, ProtocolError> {
match ProtoThreadState::try_from(value).ok() {
Some(ProtoThreadState::ThreadStateDraft) => Ok(repo::ThreadState::Draft),
Some(ProtoThreadState::ThreadStateActive) => Ok(repo::ThreadState::Active),
Some(ProtoThreadState::ThreadStateReady) => Ok(repo::ThreadState::Ready),
Some(ProtoThreadState::ThreadStateBlocked) => Ok(repo::ThreadState::Blocked),
Some(ProtoThreadState::ThreadStateMerged) => Ok(repo::ThreadState::Merged),
Some(ProtoThreadState::ThreadStateAbandoned) => Ok(repo::ThreadState::Abandoned),
Some(ProtoThreadState::ThreadStatePromoted) => Ok(repo::ThreadState::Promoted),
_ => Err(invalid_metadata(
remote_thread,
"has no managed lifecycle state",
)),
}
}
fn thread_mode(remote_thread: &str, value: i32) -> Result<repo::ThreadMode, ProtocolError> {
match ProtoThreadMode::try_from(value).ok() {
Some(ProtoThreadMode::Materialized) => Ok(repo::ThreadMode::Materialized),
Some(ProtoThreadMode::Virtualized) => Ok(repo::ThreadMode::Virtualized),
Some(ProtoThreadMode::Solid) => Ok(repo::ThreadMode::Solid),
_ => Err(invalid_metadata(
remote_thread,
"has no managed workspace mode",
)),
}
}
fn freshness(value: i32) -> repo::ThreadFreshness {
match ProtoThreadFreshness::try_from(value).ok() {
Some(ProtoThreadFreshness::Current) => repo::ThreadFreshness::Current,
Some(ProtoThreadFreshness::Stale) => repo::ThreadFreshness::Stale,
_ => repo::ThreadFreshness::Unknown,
}
}
fn verification_summary(
value: Option<api::heddle::api::v1alpha1::ThreadVerificationSummary>,
) -> repo::ThreadVerificationSummary {
value.map_or_else(repo::ThreadVerificationSummary::default, |summary| {
repo::ThreadVerificationSummary {
tests_passed: summary.tests_passed,
tests_failed: Some(summary.tests_failed),
coverage_pct: summary.coverage_pct,
lint_warnings: summary.lint_warnings,
}
})
}
fn confidence_summary(
value: Option<api::heddle::api::v1alpha1::ThreadConfidenceSummary>,
) -> repo::ThreadConfidenceSummary {
value.map_or_else(repo::ThreadConfidenceSummary::default, |summary| {
repo::ThreadConfidenceSummary {
value: summary.value,
band: match ProtoConfidenceBand::try_from(summary.band).ok() {
Some(ProtoConfidenceBand::Low) => Some(repo::ConfidenceBand::Low),
Some(ProtoConfidenceBand::Medium) => Some(repo::ConfidenceBand::Medium),
Some(ProtoConfidenceBand::High) => Some(repo::ConfidenceBand::High),
_ => None,
},
}
})
}
fn integration_policy(
value: Option<api::heddle::api::v1alpha1::ThreadIntegrationPolicy>,
) -> repo::ThreadIntegrationPolicy {
value.map_or_else(repo::ThreadIntegrationPolicy::default, |policy| {
repo::ThreadIntegrationPolicy {
status: match ProtoIntegrationPolicyStatus::try_from(policy.status).ok() {
Some(ProtoIntegrationPolicyStatus::Previewed) => Some("previewed".to_string()),
Some(ProtoIntegrationPolicyStatus::Current) => Some("current".to_string()),
Some(ProtoIntegrationPolicyStatus::Blocked) => Some("blocked".to_string()),
Some(ProtoIntegrationPolicyStatus::ManualResolved) => {
Some("manual_resolved".to_string())
}
Some(ProtoIntegrationPolicyStatus::AutoIntegrated) => {
Some("auto_integrated".to_string())
}
_ => None,
},
reason: (!policy.reason.is_empty()).then_some(policy.reason),
manual_resolution_state: None,
conflicts_resolved_manually: false,
}
})
}
fn impact_categories(
remote_thread: &str,
values: Vec<String>,
) -> Result<Vec<repo::ThreadImpactCategory>, ProtocolError> {
values
.into_iter()
.map(|category| match category.as_str() {
"dependency_graph" => Ok(repo::ThreadImpactCategory::DependencyGraph),
"build_runtime_config" => Ok(repo::ThreadImpactCategory::BuildRuntimeConfig),
"generated_outputs" => Ok(repo::ThreadImpactCategory::GeneratedOutputs),
"repo_wide_refactor" => Ok(repo::ThreadImpactCategory::RepoWideRefactor),
"public_api_surface" => Ok(repo::ThreadImpactCategory::PublicApiSurface),
_ => Err(invalid_metadata(
remote_thread,
format!("has unknown impact category '{category}'"),
)),
})
.collect()
}
#[cfg(test)]
mod tests {
use api::heddle::api::v1alpha1::{
ThreadFreshness as ProtoThreadFreshness, ThreadMode as ProtoThreadMode, ThreadSummary,
thread_state::Kind as ProtoThreadState,
};
use repo::Repository;
use tempfile::TempDir;
use super::from_summary;
#[test]
fn hosted_thread_summary_rehydrates_managed_metadata_at_pulled_tip() {
let temp = TempDir::new().unwrap();
let repo = Repository::init_default(temp.path()).unwrap();
std::fs::write(temp.path().join("base.txt"), "base\n").unwrap();
let base = repo.snapshot(Some("base".to_string()), None).unwrap();
std::fs::write(temp.path().join("runner.txt"), "runner\n").unwrap();
let tip = repo.snapshot(Some("runner".to_string()), None).unwrap();
let summary = ThreadSummary {
name: "shuttle/runner".to_string(),
base_state: super::super::helpers::proto_state_id(base.state_id),
current_state: super::super::helpers::proto_state_id(tip.state_id),
target_thread: Some("main".to_string()),
task: Some("fixture runner".to_string()),
thread_mode: ProtoThreadMode::Solid as i32,
freshness: ProtoThreadFreshness::Current as i32,
thread_state: ProtoThreadState::ThreadStateReady as i32,
changed_paths: vec!["runner.txt".to_string()],
..ThreadSummary::default()
};
let metadata = from_summary(&repo, "shuttle/runner", tip.state_id, summary).unwrap();
assert_eq!(metadata.thread, "shuttle/runner");
assert_eq!(metadata.target_thread.as_deref(), Some("main"));
assert_eq!(metadata.state, repo::ThreadState::Ready);
assert_eq!(metadata.current_state, Some(tip.state_id.short()));
assert_eq!(metadata.changed_paths, vec!["runner.txt"]);
}
}