1mod repository;
7
8pub use repository::{
9 ChangeLifecycleFilter, ChangeRepository, ChangeTargetResolution, ResolveTargetOptions,
10};
11
12use chrono::{DateTime, Utc};
13use std::path::PathBuf;
14
15use crate::tasks::{ProgressInfo, TasksParseResult};
16
17#[derive(Debug, Clone)]
19pub struct Spec {
20 pub name: String,
22 pub content: String,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum ChangeStatus {
29 NoTasks,
31 InProgress,
33 Complete,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum ChangeWorkStatus {
50 Draft,
52 Ready,
54 InProgress,
56 Paused,
60 Complete,
64}
65
66impl std::fmt::Display for ChangeWorkStatus {
67 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68 match self {
69 ChangeWorkStatus::Draft => write!(f, "draft"),
70 ChangeWorkStatus::Ready => write!(f, "ready"),
71 ChangeWorkStatus::InProgress => write!(f, "in-progress"),
72 ChangeWorkStatus::Paused => write!(f, "paused"),
73 ChangeWorkStatus::Complete => write!(f, "complete"),
74 }
75 }
76}
77
78impl std::fmt::Display for ChangeStatus {
79 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80 match self {
81 ChangeStatus::NoTasks => write!(f, "no-tasks"),
82 ChangeStatus::InProgress => write!(f, "in-progress"),
83 ChangeStatus::Complete => write!(f, "complete"),
84 }
85 }
86}
87
88#[derive(Debug, Clone)]
90pub struct Change {
91 pub id: String,
93 pub module_id: Option<String>,
95 pub sub_module_id: Option<String>,
99 pub path: PathBuf,
101 pub proposal: Option<String>,
103 pub design: Option<String>,
105 pub specs: Vec<Spec>,
107 pub tasks: TasksParseResult,
109 pub last_modified: DateTime<Utc>,
111}
112
113impl Change {
114 pub fn status(&self) -> ChangeStatus {
116 let progress = &self.tasks.progress;
117 if progress.total == 0 {
118 ChangeStatus::NoTasks
119 } else if progress.complete >= progress.total {
120 ChangeStatus::Complete
121 } else {
122 ChangeStatus::InProgress
123 }
124 }
125
126 pub fn work_status(&self) -> ChangeWorkStatus {
128 let ProgressInfo {
129 total,
130 complete,
131 shelved,
132 in_progress,
133 pending,
134 remaining: _,
135 } = self.tasks.progress;
136
137 let has_planning_artifacts = self.proposal.is_some() && !self.specs.is_empty() && total > 0;
139 if !has_planning_artifacts {
140 return ChangeWorkStatus::Draft;
141 }
142
143 if complete == total {
144 return ChangeWorkStatus::Complete;
145 }
146 if in_progress > 0 {
147 return ChangeWorkStatus::InProgress;
148 }
149
150 let done_or_shelved = complete + shelved;
151 if pending == 0 && shelved > 0 && done_or_shelved == total {
152 return ChangeWorkStatus::Paused;
153 }
154
155 ChangeWorkStatus::Ready
156 }
157
158 pub fn artifacts_complete(&self) -> bool {
160 self.proposal.is_some()
161 && self.design.is_some()
162 && !self.specs.is_empty()
163 && self.tasks.progress.total > 0
164 }
165
166 pub fn task_progress(&self) -> (u32, u32) {
168 (
169 self.tasks.progress.complete as u32,
170 self.tasks.progress.total as u32,
171 )
172 }
173
174 pub fn progress(&self) -> &ProgressInfo {
176 &self.tasks.progress
177 }
178}
179
180#[derive(Debug, Clone)]
182pub struct ChangeSummary {
183 pub id: String,
185 pub module_id: Option<String>,
187 pub sub_module_id: Option<String>,
191 pub completed_tasks: u32,
193 pub shelved_tasks: u32,
195 pub in_progress_tasks: u32,
197 pub pending_tasks: u32,
199 pub total_tasks: u32,
201 pub last_modified: DateTime<Utc>,
203 pub has_proposal: bool,
205 pub has_design: bool,
207 pub has_specs: bool,
209 pub has_tasks: bool,
211}
212
213impl ChangeSummary {
214 pub fn status(&self) -> ChangeStatus {
216 if self.total_tasks == 0 {
217 ChangeStatus::NoTasks
218 } else if self.completed_tasks >= self.total_tasks {
219 ChangeStatus::Complete
220 } else {
221 ChangeStatus::InProgress
222 }
223 }
224
225 pub fn work_status(&self) -> ChangeWorkStatus {
227 let has_planning_artifacts = self.has_proposal && self.has_specs && self.has_tasks;
228 if !has_planning_artifacts {
229 return ChangeWorkStatus::Draft;
230 }
231
232 if self.total_tasks > 0 && self.completed_tasks == self.total_tasks {
233 return ChangeWorkStatus::Complete;
234 }
235 if self.in_progress_tasks > 0 {
236 return ChangeWorkStatus::InProgress;
237 }
238
239 let done_or_shelved = self.completed_tasks + self.shelved_tasks;
240 if self.pending_tasks == 0 && self.shelved_tasks > 0 && done_or_shelved == self.total_tasks
241 {
242 return ChangeWorkStatus::Paused;
243 }
244
245 ChangeWorkStatus::Ready
246 }
247
248 pub fn is_ready(&self) -> bool {
253 self.work_status() == ChangeWorkStatus::Ready
254 }
255}
256
257pub fn extract_module_id(change_id: &str) -> Option<String> {
267 let parts: Vec<&str> = change_id.split('-').collect();
268 if parts.len() >= 2 {
269 let module_part = parts[0].split('.').next().unwrap_or(parts[0]);
271 Some(normalize_id(module_part, 3))
272 } else {
273 None
274 }
275}
276
277pub fn extract_sub_module_id(change_id: &str) -> Option<String> {
285 let prefix = change_id.split('-').next()?;
287 if !prefix.contains('.') {
288 return None;
289 }
290 ito_common::id::parse_sub_module_id(prefix)
292 .map(|p| p.sub_module_id.as_str().to_string())
293 .ok()
294}
295
296pub fn normalize_id(id: &str, width: usize) -> String {
302 let num: u32 = id.parse().unwrap_or(0);
304 format!("{:0>width$}", num, width = width)
305}
306
307pub fn parse_change_id(input: &str) -> Option<(String, String)> {
316 let id_part = input.split('_').next().unwrap_or(input);
318
319 let parts: Vec<&str> = id_part.split('-').collect();
320 if parts.len() >= 2 {
321 let module_part = parts[0].split('.').next().unwrap_or(parts[0]);
323 let module_id = normalize_id(module_part, 3);
324 let change_num = normalize_id(parts[1], 2);
325 Some((module_id, change_num))
326 } else {
327 None
328 }
329}
330
331pub fn parse_module_id(input: &str) -> String {
339 let id_part = input.split('_').next().unwrap_or(input);
341 normalize_id(id_part, 3)
342}
343
344#[cfg(test)]
345mod tests {
346 use super::*;
347
348 #[test]
349 fn test_normalize_id() {
350 assert_eq!(normalize_id("5", 3), "005");
351 assert_eq!(normalize_id("05", 3), "005");
352 assert_eq!(normalize_id("005", 3), "005");
353 assert_eq!(normalize_id("0005", 3), "005");
354 assert_eq!(normalize_id("1", 2), "01");
355 assert_eq!(normalize_id("01", 2), "01");
356 assert_eq!(normalize_id("001", 2), "01");
357 }
358
359 #[test]
360 fn test_parse_change_id() {
361 assert_eq!(
362 parse_change_id("005-01_my-change"),
363 Some(("005".to_string(), "01".to_string()))
364 );
365 assert_eq!(
366 parse_change_id("5-1_whatever"),
367 Some(("005".to_string(), "01".to_string()))
368 );
369 assert_eq!(
370 parse_change_id("1-2"),
371 Some(("001".to_string(), "02".to_string()))
372 );
373 assert_eq!(
374 parse_change_id("001-000002_foo"),
375 Some(("001".to_string(), "02".to_string()))
376 );
377 assert_eq!(parse_change_id("invalid"), None);
378 }
379
380 #[test]
381 fn test_parse_module_id() {
382 assert_eq!(parse_module_id("005"), "005");
383 assert_eq!(parse_module_id("5"), "005");
384 assert_eq!(parse_module_id("005_dev-tooling"), "005");
385 assert_eq!(parse_module_id("5_dev-tooling"), "005");
386 }
387
388 #[test]
389 fn test_extract_module_id() {
390 assert_eq!(
391 extract_module_id("005-01_my-change"),
392 Some("005".to_string())
393 );
394 assert_eq!(extract_module_id("013-18_cleanup"), Some("013".to_string()));
395 assert_eq!(extract_module_id("5-1_foo"), Some("005".to_string()));
396 assert_eq!(extract_module_id("invalid"), None);
397 assert_eq!(extract_module_id("024.01-03_foo"), Some("024".to_string()));
399 assert_eq!(extract_module_id("5.1-2_bar"), Some("005".to_string()));
400 }
401
402 #[test]
403 fn test_extract_sub_module_id() {
404 assert_eq!(
405 extract_sub_module_id("024.01-03_foo"),
406 Some("024.01".to_string())
407 );
408 assert_eq!(
409 extract_sub_module_id("5.1-2_bar"),
410 Some("005.01".to_string())
411 );
412 assert_eq!(extract_sub_module_id("005-01_my-change"), None);
413 assert_eq!(extract_sub_module_id("invalid"), None);
414 }
415
416 #[test]
417 fn test_parse_change_id_sub_module_format() {
418 assert_eq!(
419 parse_change_id("024.01-03_foo"),
420 Some(("024".to_string(), "03".to_string()))
421 );
422 assert_eq!(
423 parse_change_id("5.1-2_bar"),
424 Some(("005".to_string(), "02".to_string()))
425 );
426 }
427
428 #[test]
429 fn test_change_sub_module_id_field() {
430 let summary = ChangeSummary {
431 id: "005.01-03_my-change".to_string(),
432 module_id: Some("005".to_string()),
433 sub_module_id: Some("005.01".to_string()),
434 completed_tasks: 0,
435 shelved_tasks: 0,
436 in_progress_tasks: 0,
437 pending_tasks: 0,
438 total_tasks: 0,
439 last_modified: Utc::now(),
440 has_proposal: false,
441 has_design: false,
442 has_specs: false,
443 has_tasks: false,
444 };
445
446 assert_eq!(summary.sub_module_id.as_deref(), Some("005.01"));
447 }
448
449 #[test]
450 fn test_change_status_display() {
451 assert_eq!(ChangeStatus::NoTasks.to_string(), "no-tasks");
452 assert_eq!(ChangeStatus::InProgress.to_string(), "in-progress");
453 assert_eq!(ChangeStatus::Complete.to_string(), "complete");
454 }
455
456 #[test]
457 fn test_change_summary_status() {
458 let mut summary = ChangeSummary {
459 id: "test".to_string(),
460 module_id: None,
461 sub_module_id: None,
462 completed_tasks: 0,
463 shelved_tasks: 0,
464 in_progress_tasks: 0,
465 pending_tasks: 0,
466 total_tasks: 0,
467 last_modified: Utc::now(),
468 has_proposal: false,
469 has_design: false,
470 has_specs: false,
471 has_tasks: false,
472 };
473
474 assert_eq!(summary.status(), ChangeStatus::NoTasks);
475
476 summary.total_tasks = 5;
477 summary.completed_tasks = 3;
478 assert_eq!(summary.status(), ChangeStatus::InProgress);
479
480 summary.completed_tasks = 5;
481 assert_eq!(summary.status(), ChangeStatus::Complete);
482 }
483
484 #[test]
485 fn test_change_work_status() {
486 let mut summary = ChangeSummary {
487 id: "test".to_string(),
488 module_id: None,
489 sub_module_id: None,
490 completed_tasks: 0,
491 shelved_tasks: 0,
492 in_progress_tasks: 0,
493 pending_tasks: 0,
494 total_tasks: 0,
495 last_modified: Utc::now(),
496 has_proposal: false,
497 has_design: false,
498 has_specs: false,
499 has_tasks: false,
500 };
501
502 assert_eq!(summary.work_status(), ChangeWorkStatus::Draft);
503
504 summary.has_proposal = true;
505 summary.has_specs = true;
506 summary.has_tasks = true;
507 summary.total_tasks = 3;
508 summary.pending_tasks = 3;
509
510 assert_eq!(summary.work_status(), ChangeWorkStatus::Ready);
511
512 summary.in_progress_tasks = 1;
513 summary.pending_tasks = 2;
514 assert_eq!(summary.work_status(), ChangeWorkStatus::InProgress);
515
516 summary.in_progress_tasks = 0;
517 summary.pending_tasks = 0;
518 summary.shelved_tasks = 1;
519 summary.completed_tasks = 2;
520 assert_eq!(summary.work_status(), ChangeWorkStatus::Paused);
521
522 summary.shelved_tasks = 0;
523 summary.completed_tasks = 3;
524 assert_eq!(summary.work_status(), ChangeWorkStatus::Complete);
525 }
526}