mod repository;
pub use repository::{
ChangeLifecycleFilter, ChangeRepository, ChangeTargetResolution, ResolveTargetOptions,
};
use chrono::{DateTime, Utc};
use std::path::PathBuf;
use crate::tasks::{ProgressInfo, TasksParseResult};
#[derive(Debug, Clone)]
pub struct Spec {
pub name: String,
pub content: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChangeStatus {
NoTasks,
InProgress,
Complete,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChangeWorkStatus {
Draft,
Ready,
InProgress,
Paused,
Complete,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ChangeOrchestrateMetadata {
pub depends_on: Vec<String>,
pub preferred_gates: Vec<String>,
}
impl std::fmt::Display for ChangeWorkStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ChangeWorkStatus::Draft => write!(f, "draft"),
ChangeWorkStatus::Ready => write!(f, "ready"),
ChangeWorkStatus::InProgress => write!(f, "in-progress"),
ChangeWorkStatus::Paused => write!(f, "paused"),
ChangeWorkStatus::Complete => write!(f, "complete"),
}
}
}
impl std::fmt::Display for ChangeStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ChangeStatus::NoTasks => write!(f, "no-tasks"),
ChangeStatus::InProgress => write!(f, "in-progress"),
ChangeStatus::Complete => write!(f, "complete"),
}
}
}
#[derive(Debug, Clone)]
pub struct Change {
pub id: String,
pub module_id: Option<String>,
pub sub_module_id: Option<String>,
pub path: PathBuf,
pub proposal: Option<String>,
pub design: Option<String>,
pub specs: Vec<Spec>,
pub tasks: TasksParseResult,
pub orchestrate: ChangeOrchestrateMetadata,
pub last_modified: DateTime<Utc>,
}
impl Change {
pub fn status(&self) -> ChangeStatus {
let progress = &self.tasks.progress;
if progress.total == 0 {
ChangeStatus::NoTasks
} else if progress.complete >= progress.total {
ChangeStatus::Complete
} else {
ChangeStatus::InProgress
}
}
pub fn work_status(&self) -> ChangeWorkStatus {
let ProgressInfo {
total,
complete,
shelved,
in_progress,
pending,
remaining: _,
} = self.tasks.progress;
let has_planning_artifacts = self.proposal.is_some() && !self.specs.is_empty() && total > 0;
if !has_planning_artifacts {
return ChangeWorkStatus::Draft;
}
if complete == total {
return ChangeWorkStatus::Complete;
}
if in_progress > 0 {
return ChangeWorkStatus::InProgress;
}
let done_or_shelved = complete + shelved;
if pending == 0 && shelved > 0 && done_or_shelved == total {
return ChangeWorkStatus::Paused;
}
ChangeWorkStatus::Ready
}
pub fn artifacts_complete(&self) -> bool {
self.proposal.is_some()
&& self.design.is_some()
&& !self.specs.is_empty()
&& self.tasks.progress.total > 0
}
pub fn task_progress(&self) -> (u32, u32) {
(
self.tasks.progress.complete as u32,
self.tasks.progress.total as u32,
)
}
pub fn progress(&self) -> &ProgressInfo {
&self.tasks.progress
}
}
#[derive(Debug, Clone)]
pub struct ChangeSummary {
pub id: String,
pub module_id: Option<String>,
pub sub_module_id: Option<String>,
pub completed_tasks: u32,
pub shelved_tasks: u32,
pub in_progress_tasks: u32,
pub pending_tasks: u32,
pub total_tasks: u32,
pub last_modified: DateTime<Utc>,
pub has_proposal: bool,
pub has_design: bool,
pub has_specs: bool,
pub has_tasks: bool,
pub orchestrate: ChangeOrchestrateMetadata,
}
impl ChangeSummary {
pub fn status(&self) -> ChangeStatus {
if self.total_tasks == 0 {
ChangeStatus::NoTasks
} else if self.completed_tasks >= self.total_tasks {
ChangeStatus::Complete
} else {
ChangeStatus::InProgress
}
}
pub fn work_status(&self) -> ChangeWorkStatus {
let has_planning_artifacts = self.has_proposal && self.has_specs && self.has_tasks;
if !has_planning_artifacts {
return ChangeWorkStatus::Draft;
}
if self.total_tasks > 0 && self.completed_tasks == self.total_tasks {
return ChangeWorkStatus::Complete;
}
if self.in_progress_tasks > 0 {
return ChangeWorkStatus::InProgress;
}
let done_or_shelved = self.completed_tasks + self.shelved_tasks;
if self.pending_tasks == 0 && self.shelved_tasks > 0 && done_or_shelved == self.total_tasks
{
return ChangeWorkStatus::Paused;
}
ChangeWorkStatus::Ready
}
pub fn is_ready(&self) -> bool {
self.work_status() == ChangeWorkStatus::Ready
}
}
pub fn extract_module_id(change_id: &str) -> Option<String> {
let parts: Vec<&str> = change_id.split('-').collect();
if parts.len() >= 2 {
let module_part = parts[0].split('.').next().unwrap_or(parts[0]);
Some(normalize_id(module_part, 3))
} else {
None
}
}
pub fn extract_sub_module_id(change_id: &str) -> Option<String> {
let prefix = change_id.split('-').next()?;
if !prefix.contains('.') {
return None;
}
ito_common::id::parse_sub_module_id(prefix)
.map(|p| p.sub_module_id.as_str().to_string())
.ok()
}
pub fn normalize_id(id: &str, width: usize) -> String {
let num: u32 = id.parse().unwrap_or(0);
format!("{:0>width$}", num, width = width)
}
pub fn parse_change_id(input: &str) -> Option<(String, String)> {
let id_part = input.split('_').next().unwrap_or(input);
let parts: Vec<&str> = id_part.split('-').collect();
if parts.len() >= 2 {
let module_part = parts[0].split('.').next().unwrap_or(parts[0]);
let module_id = normalize_id(module_part, 3);
let change_num = normalize_id(parts[1], 2);
Some((module_id, change_num))
} else {
None
}
}
pub fn parse_module_id(input: &str) -> String {
let id_part = input.split('_').next().unwrap_or(input);
normalize_id(id_part, 3)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_normalize_id() {
assert_eq!(normalize_id("5", 3), "005");
assert_eq!(normalize_id("05", 3), "005");
assert_eq!(normalize_id("005", 3), "005");
assert_eq!(normalize_id("0005", 3), "005");
assert_eq!(normalize_id("1", 2), "01");
assert_eq!(normalize_id("01", 2), "01");
assert_eq!(normalize_id("001", 2), "01");
}
#[test]
fn test_parse_change_id() {
assert_eq!(
parse_change_id("005-01_my-change"),
Some(("005".to_string(), "01".to_string()))
);
assert_eq!(
parse_change_id("5-1_whatever"),
Some(("005".to_string(), "01".to_string()))
);
assert_eq!(
parse_change_id("1-2"),
Some(("001".to_string(), "02".to_string()))
);
assert_eq!(
parse_change_id("001-000002_foo"),
Some(("001".to_string(), "02".to_string()))
);
assert_eq!(parse_change_id("invalid"), None);
}
#[test]
fn test_parse_module_id() {
assert_eq!(parse_module_id("005"), "005");
assert_eq!(parse_module_id("5"), "005");
assert_eq!(parse_module_id("005_dev-tooling"), "005");
assert_eq!(parse_module_id("5_dev-tooling"), "005");
}
#[test]
fn test_extract_module_id() {
assert_eq!(
extract_module_id("005-01_my-change"),
Some("005".to_string())
);
assert_eq!(extract_module_id("013-18_cleanup"), Some("013".to_string()));
assert_eq!(extract_module_id("5-1_foo"), Some("005".to_string()));
assert_eq!(extract_module_id("invalid"), None);
assert_eq!(extract_module_id("024.01-03_foo"), Some("024".to_string()));
assert_eq!(extract_module_id("5.1-2_bar"), Some("005".to_string()));
}
#[test]
fn test_extract_sub_module_id() {
assert_eq!(
extract_sub_module_id("024.01-03_foo"),
Some("024.01".to_string())
);
assert_eq!(
extract_sub_module_id("5.1-2_bar"),
Some("005.01".to_string())
);
assert_eq!(extract_sub_module_id("005-01_my-change"), None);
assert_eq!(extract_sub_module_id("invalid"), None);
}
#[test]
fn test_parse_change_id_sub_module_format() {
assert_eq!(
parse_change_id("024.01-03_foo"),
Some(("024".to_string(), "03".to_string()))
);
assert_eq!(
parse_change_id("5.1-2_bar"),
Some(("005".to_string(), "02".to_string()))
);
}
#[test]
fn test_change_sub_module_id_field() {
let summary = ChangeSummary {
id: "005.01-03_my-change".to_string(),
module_id: Some("005".to_string()),
sub_module_id: Some("005.01".to_string()),
completed_tasks: 0,
shelved_tasks: 0,
in_progress_tasks: 0,
pending_tasks: 0,
total_tasks: 0,
last_modified: Utc::now(),
has_proposal: false,
has_design: false,
has_specs: false,
has_tasks: false,
orchestrate: ChangeOrchestrateMetadata::default(),
};
assert_eq!(summary.sub_module_id.as_deref(), Some("005.01"));
}
#[test]
fn test_change_status_display() {
assert_eq!(ChangeStatus::NoTasks.to_string(), "no-tasks");
assert_eq!(ChangeStatus::InProgress.to_string(), "in-progress");
assert_eq!(ChangeStatus::Complete.to_string(), "complete");
}
#[test]
fn test_change_summary_status() {
let mut summary = ChangeSummary {
id: "test".to_string(),
module_id: None,
sub_module_id: None,
completed_tasks: 0,
shelved_tasks: 0,
in_progress_tasks: 0,
pending_tasks: 0,
total_tasks: 0,
last_modified: Utc::now(),
has_proposal: false,
has_design: false,
has_specs: false,
has_tasks: false,
orchestrate: ChangeOrchestrateMetadata::default(),
};
assert_eq!(summary.status(), ChangeStatus::NoTasks);
summary.total_tasks = 5;
summary.completed_tasks = 3;
assert_eq!(summary.status(), ChangeStatus::InProgress);
summary.completed_tasks = 5;
assert_eq!(summary.status(), ChangeStatus::Complete);
}
#[test]
fn test_change_work_status() {
let mut summary = ChangeSummary {
id: "test".to_string(),
module_id: None,
sub_module_id: None,
completed_tasks: 0,
shelved_tasks: 0,
in_progress_tasks: 0,
pending_tasks: 0,
total_tasks: 0,
last_modified: Utc::now(),
has_proposal: false,
has_design: false,
has_specs: false,
has_tasks: false,
orchestrate: ChangeOrchestrateMetadata::default(),
};
assert_eq!(summary.work_status(), ChangeWorkStatus::Draft);
summary.has_proposal = true;
summary.has_specs = true;
summary.has_tasks = true;
summary.total_tasks = 3;
summary.pending_tasks = 3;
assert_eq!(summary.work_status(), ChangeWorkStatus::Ready);
summary.in_progress_tasks = 1;
summary.pending_tasks = 2;
assert_eq!(summary.work_status(), ChangeWorkStatus::InProgress);
summary.in_progress_tasks = 0;
summary.pending_tasks = 0;
summary.shelved_tasks = 1;
summary.completed_tasks = 2;
assert_eq!(summary.work_status(), ChangeWorkStatus::Paused);
summary.shelved_tasks = 0;
summary.completed_tasks = 3;
assert_eq!(summary.work_status(), ChangeWorkStatus::Complete);
}
}