mod mutations;
mod repository;
pub use mutations::{
ChangeArtifactKind, ChangeArtifactMutationError, ChangeArtifactMutationResult,
ChangeArtifactMutationService, ChangeArtifactMutationServiceResult, ChangeArtifactRef,
};
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 changes_tests;