use std::collections::BTreeMap;
use std::sync::RwLock;
use serde::Serialize;
use super::plan::{FileKind, Plan, Status};
use super::service::{ApplyReport, MigrationService};
use crate::platform::error::PlatformError;
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct Notice {
pub owner: String,
pub project: String,
pub items: usize,
pub plan_href: String,
pub line: String,
}
#[derive(Debug, Default)]
pub struct MigrationNotices {
by_project: RwLock<BTreeMap<(String, String), Notice>>,
}
impl MigrationNotices {
pub fn get(&self, owner: &str, project: &str) -> Option<Notice> {
self.by_project.read().ok()?.get(&(owner.to_string(), project.to_string())).cloned()
}
pub fn set(&self, notice: Notice) {
if let Ok(mut map) = self.by_project.write() {
map.insert((notice.owner.clone(), notice.project.clone()), notice);
}
}
pub fn clear(&self, owner: &str, project: &str) {
if let Ok(mut map) = self.by_project.write() {
map.remove(&(owner.to_string(), project.to_string()));
}
}
}
#[derive(Debug, Clone)]
pub enum StartupOutcome {
Nothing,
Applied(ApplyReport),
Named(Notice),
}
pub fn has_work(plan: &Plan) -> bool {
plan.files.iter().any(|f| f.status == Status::Rewrite || (f.kind == FileKind::Pipeline && f.needs_activation))
}
fn plan_href(owner: &str, project: &str) -> String {
format!("/api/projects/{owner}/{project}/migration/0.11/plan?format=markdown")
}
fn blocked_notice(plan: &Plan) -> Notice {
let c = &plan.counts;
let mut blocks = Vec::new();
if c.unresolved > 0 {
blocks.push(format!("{} unresolved item(s)", c.unresolved));
}
if c.refusals > 0 {
blocks.push(format!("{} refused check(s)", c.refusals));
}
if c.review > 0 {
blocks.push(format!("{} review item(s)", c.review));
}
let href = plan_href(&plan.owner, &plan.project);
Notice {
owner: plan.owner.clone(),
project: plan.project.clone(),
items: c.unresolved + c.refusals + c.review,
line: format!(
"0.11 migration NOT applied: {}/{} — {} pipeline(s) and {} page(s) still 0.10; blocked by {}; read the plan at {href}",
plan.owner,
plan.project,
c.pipelines_to_rewrite,
c.pages_to_rewrite,
blocks.join(", ")
),
plan_href: href,
}
}
impl MigrationService {
pub async fn migrate_at_start(&self, owner: &str, project: &str) -> Result<StartupOutcome, PlatformError> {
let notices = &self.platform().migration_notices;
let plan = self.plan_project(owner, project)?;
if !has_work(&plan) {
notices.clear(owner, project);
return Ok(StartupOutcome::Nothing);
}
if !plan.ready || plan.counts.review > 0 {
let notice = blocked_notice(&plan);
notices.set(notice.clone());
return Ok(StartupOutcome::Named(notice));
}
let report = self.apply_plan(owner, project, &plan.fingerprint).await?;
if report.ok {
return Ok(StartupOutcome::Applied(report));
}
let href = plan_href(owner, project);
let notice = Notice {
owner: owner.to_string(),
project: project.to_string(),
items: report.failed.len(),
line: format!(
"0.11 migration applied with {} failure(s): {owner}/{project} — {}; the record is archive/0.10/MIGRATION.md",
report.failed.len(),
report.failed.join("; ")
),
plan_href: href,
};
notices.set(notice.clone());
Ok(StartupOutcome::Named(notice))
}
pub fn refresh_notice(&self, owner: &str, project: &str) -> Option<Notice> {
let notices = &self.platform().migration_notices;
notices.get(owner, project)?;
match self.plan_project(owner, project) {
Ok(plan) if !has_work(&plan) => {
notices.clear(owner, project);
None
}
Ok(plan) if plan.ready && plan.counts.review == 0 => notices.get(owner, project),
Ok(plan) => {
let notice = blocked_notice(&plan);
notices.set(notice.clone());
Some(notice)
}
Err(_) => notices.get(owner, project),
}
}
}