use std::path::Path;
use anyhow::Result;
use serde::Serialize;
use crate::blocking::check_blocked_with_archive;
use crate::index::{ArchiveIndex, Index, IndexEntry};
use crate::unit::{Status, UnitType};
use crate::util::natural_cmp;
#[derive(Debug, Serialize)]
pub struct StatusSummary {
pub epics: Vec<IndexEntry>,
pub features: Vec<IndexEntry>,
pub claimed: Vec<IndexEntry>,
pub ready: Vec<IndexEntry>,
pub goals: Vec<IndexEntry>,
pub blocked: Vec<BlockedEntry>,
}
#[derive(Debug, Serialize)]
pub struct BlockedEntry {
#[serde(flatten)]
pub entry: IndexEntry,
pub block_reason: String,
}
pub fn status(mana_dir: &Path) -> Result<StatusSummary> {
let index = Index::load_or_rebuild(mana_dir)?;
let archive = ArchiveIndex::load_or_rebuild(mana_dir)
.unwrap_or_else(|_| ArchiveIndex { units: Vec::new() });
let mut epics: Vec<IndexEntry> = Vec::new();
let mut features: Vec<IndexEntry> = Vec::new();
let mut claimed: Vec<IndexEntry> = Vec::new();
let mut ready: Vec<IndexEntry> = Vec::new();
let mut goals: Vec<IndexEntry> = Vec::new();
let mut blocked: Vec<BlockedEntry> = Vec::new();
for entry in &index.units {
if entry.feature {
features.push(entry.clone());
continue;
}
if entry.kind == UnitType::Epic {
epics.push(entry.clone());
continue;
}
match entry.status {
Status::InProgress | Status::AwaitingVerify => {
claimed.push(entry.clone());
}
Status::Open => {
if let Some(reason) = check_blocked_with_archive(entry, &index, Some(&archive)) {
blocked.push(BlockedEntry {
entry: entry.clone(),
block_reason: reason.to_string(),
});
} else if entry.kind == UnitType::Task && entry.has_verify {
ready.push(entry.clone());
} else {
goals.push(entry.clone());
}
}
Status::Closed => {}
}
}
sort_entries(&mut epics);
sort_entries(&mut features);
sort_entries(&mut claimed);
sort_entries(&mut ready);
sort_entries(&mut goals);
blocked.sort_by(|a, b| match a.entry.priority.cmp(&b.entry.priority) {
std::cmp::Ordering::Equal => natural_cmp(&a.entry.id, &b.entry.id),
other => other,
});
Ok(StatusSummary {
epics,
features,
claimed,
ready,
goals,
blocked,
})
}
fn sort_entries(entries: &mut [IndexEntry]) {
entries.sort_by(|a, b| match a.priority.cmp(&b.priority) {
std::cmp::Ordering::Equal => natural_cmp(&a.id, &b.id),
other => other,
});
}
#[cfg(test)]
mod tests {
use super::*;
use crate::unit::Unit;
use crate::util::title_to_slug;
use std::fs;
use tempfile::TempDir;
fn setup() -> (TempDir, std::path::PathBuf) {
let dir = TempDir::new().unwrap();
let mana_dir = dir.path().join(".mana");
fs::create_dir(&mana_dir).unwrap();
(dir, mana_dir)
}
fn write_unit(mana_dir: &Path, unit: &Unit) {
let slug = title_to_slug(&unit.title);
let path = mana_dir.join(format!("{}-{}.md", unit.id, slug));
unit.to_file(path).unwrap();
}
#[test]
fn status_groups_by_kind() {
let (_dir, mana_dir) = setup();
let mut epic = Unit::new("1", "Epic");
epic.kind = UnitType::Epic;
write_unit(&mana_dir, &epic);
let mut task = Unit::new("2", "Task");
task.kind = UnitType::Task;
task.verify = Some("cargo test task".to_string());
write_unit(&mana_dir, &task);
let mut feature = Unit::new("3", "Feature");
feature.kind = UnitType::Epic;
feature.feature = true;
write_unit(&mana_dir, &feature);
let result = status(&mana_dir).unwrap();
assert_eq!(result.epics.len(), 1);
assert_eq!(result.epics[0].id, "1");
assert_eq!(result.ready.len(), 1);
assert_eq!(result.ready[0].id, "2");
assert_eq!(result.features.len(), 1);
assert_eq!(result.features[0].id, "3");
}
#[test]
fn status_categorizes_units() {
let (_dir, mana_dir) = setup();
let mut ready_unit = Unit::new("1", "Ready task");
ready_unit.verify = Some("cargo test unit::check".to_string());
write_unit(&mana_dir, &ready_unit);
let goal_unit = Unit::new("2", "Goal task");
write_unit(&mana_dir, &goal_unit);
let mut claimed_unit = Unit::new("3", "Claimed task");
claimed_unit.status = Status::InProgress;
write_unit(&mana_dir, &claimed_unit);
let result = status(&mana_dir).unwrap();
assert_eq!(result.ready.len(), 1);
assert_eq!(result.ready[0].id, "1");
assert_eq!(result.goals.len(), 1);
assert_eq!(result.goals[0].id, "2");
assert_eq!(result.claimed.len(), 1);
assert_eq!(result.claimed[0].id, "3");
assert!(result.blocked.is_empty());
}
#[test]
fn status_detects_blocked() {
let (_dir, mana_dir) = setup();
let mut dep = Unit::new("1", "Dependency");
dep.verify = Some("true".to_string());
write_unit(&mana_dir, &dep);
let mut blocked_unit = Unit::new("2", "Blocked task");
blocked_unit.verify = Some("true".to_string());
blocked_unit.dependencies = vec!["1".to_string()];
write_unit(&mana_dir, &blocked_unit);
let result = status(&mana_dir).unwrap();
assert_eq!(result.blocked.len(), 1);
assert_eq!(result.blocked[0].entry.id, "2");
}
#[test]
fn status_empty_project() {
let (_dir, mana_dir) = setup();
let result = status(&mana_dir).unwrap();
assert!(result.features.is_empty());
assert!(result.claimed.is_empty());
assert!(result.ready.is_empty());
assert!(result.goals.is_empty());
assert!(result.blocked.is_empty());
}
#[test]
fn status_skips_closed() {
let (_dir, mana_dir) = setup();
let mut unit = Unit::new("1", "Closed task");
unit.status = Status::Closed;
write_unit(&mana_dir, &unit);
let result = status(&mana_dir).unwrap();
assert!(result.claimed.is_empty());
assert!(result.ready.is_empty());
assert!(result.goals.is_empty());
assert!(result.blocked.is_empty());
}
#[test]
fn awaiting_verify_appears_in_claimed() {
let (_dir, mana_dir) = setup();
let mut unit = Unit::new("1", "Awaiting verify task");
unit.verify = Some("cargo test unit::check".to_string());
unit.status = Status::AwaitingVerify;
write_unit(&mana_dir, &unit);
let result = status(&mana_dir).unwrap();
assert_eq!(result.claimed.len(), 1);
assert_eq!(result.claimed[0].id, "1");
assert!(result.ready.is_empty());
assert!(result.goals.is_empty());
}
#[test]
fn status_archived_dep_not_blocking() {
let (_dir, mana_dir) = setup();
let archive_dir = mana_dir.join("archive");
fs::create_dir(&archive_dir).unwrap();
let mut archived_dep = Unit::new("1", "Archived dep");
archived_dep.status = Status::Closed;
archived_dep
.to_file(archive_dir.join("1-archived-dep.md"))
.unwrap();
let mut unit = Unit::new("2", "Dependent task");
unit.verify = Some("true".to_string());
unit.dependencies = vec!["1".to_string()];
write_unit(&mana_dir, &unit);
let result = status(&mana_dir).unwrap();
assert!(
result.blocked.is_empty(),
"expected no blocked units, got: {:?}",
result
.blocked
.iter()
.map(|b| &b.entry.id)
.collect::<Vec<_>>()
);
assert_eq!(result.ready.len(), 1);
assert_eq!(result.ready[0].id, "2");
}
}