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 path: PathBuf,
97 pub proposal: Option<String>,
99 pub design: Option<String>,
101 pub specs: Vec<Spec>,
103 pub tasks: TasksParseResult,
105 pub last_modified: DateTime<Utc>,
107}
108
109impl Change {
110 pub fn status(&self) -> ChangeStatus {
112 let progress = &self.tasks.progress;
113 if progress.total == 0 {
114 ChangeStatus::NoTasks
115 } else if progress.complete >= progress.total {
116 ChangeStatus::Complete
117 } else {
118 ChangeStatus::InProgress
119 }
120 }
121
122 pub fn work_status(&self) -> ChangeWorkStatus {
124 let ProgressInfo {
125 total,
126 complete,
127 shelved,
128 in_progress,
129 pending,
130 remaining: _,
131 } = self.tasks.progress;
132
133 let has_planning_artifacts = self.proposal.is_some() && !self.specs.is_empty() && total > 0;
135 if !has_planning_artifacts {
136 return ChangeWorkStatus::Draft;
137 }
138
139 if complete == total {
140 return ChangeWorkStatus::Complete;
141 }
142 if in_progress > 0 {
143 return ChangeWorkStatus::InProgress;
144 }
145
146 let done_or_shelved = complete + shelved;
147 if pending == 0 && shelved > 0 && done_or_shelved == total {
148 return ChangeWorkStatus::Paused;
149 }
150
151 ChangeWorkStatus::Ready
152 }
153
154 pub fn artifacts_complete(&self) -> bool {
156 self.proposal.is_some()
157 && self.design.is_some()
158 && !self.specs.is_empty()
159 && self.tasks.progress.total > 0
160 }
161
162 pub fn task_progress(&self) -> (u32, u32) {
164 (
165 self.tasks.progress.complete as u32,
166 self.tasks.progress.total as u32,
167 )
168 }
169
170 pub fn progress(&self) -> &ProgressInfo {
172 &self.tasks.progress
173 }
174}
175
176#[derive(Debug, Clone)]
178pub struct ChangeSummary {
179 pub id: String,
181 pub module_id: Option<String>,
183 pub completed_tasks: u32,
185 pub shelved_tasks: u32,
187 pub in_progress_tasks: u32,
189 pub pending_tasks: u32,
191 pub total_tasks: u32,
193 pub last_modified: DateTime<Utc>,
195 pub has_proposal: bool,
197 pub has_design: bool,
199 pub has_specs: bool,
201 pub has_tasks: bool,
203}
204
205impl ChangeSummary {
206 pub fn status(&self) -> ChangeStatus {
208 if self.total_tasks == 0 {
209 ChangeStatus::NoTasks
210 } else if self.completed_tasks >= self.total_tasks {
211 ChangeStatus::Complete
212 } else {
213 ChangeStatus::InProgress
214 }
215 }
216
217 pub fn work_status(&self) -> ChangeWorkStatus {
219 let has_planning_artifacts = self.has_proposal && self.has_specs && self.has_tasks;
220 if !has_planning_artifacts {
221 return ChangeWorkStatus::Draft;
222 }
223
224 if self.total_tasks > 0 && self.completed_tasks == self.total_tasks {
225 return ChangeWorkStatus::Complete;
226 }
227 if self.in_progress_tasks > 0 {
228 return ChangeWorkStatus::InProgress;
229 }
230
231 let done_or_shelved = self.completed_tasks + self.shelved_tasks;
232 if self.pending_tasks == 0 && self.shelved_tasks > 0 && done_or_shelved == self.total_tasks
233 {
234 return ChangeWorkStatus::Paused;
235 }
236
237 ChangeWorkStatus::Ready
238 }
239
240 pub fn is_ready(&self) -> bool {
245 self.work_status() == ChangeWorkStatus::Ready
246 }
247}
248
249pub fn extract_module_id(change_id: &str) -> Option<String> {
257 let parts: Vec<&str> = change_id.split('-').collect();
258 if parts.len() >= 2 {
259 Some(normalize_id(parts[0], 3))
260 } else {
261 None
262 }
263}
264
265pub fn normalize_id(id: &str, width: usize) -> String {
271 let num: u32 = id.parse().unwrap_or(0);
273 format!("{:0>width$}", num, width = width)
274}
275
276pub fn parse_change_id(input: &str) -> Option<(String, String)> {
284 let id_part = input.split('_').next().unwrap_or(input);
286
287 let parts: Vec<&str> = id_part.split('-').collect();
288 if parts.len() >= 2 {
289 let module_id = normalize_id(parts[0], 3);
290 let change_num = normalize_id(parts[1], 2);
291 Some((module_id, change_num))
292 } else {
293 None
294 }
295}
296
297pub fn parse_module_id(input: &str) -> String {
305 let id_part = input.split('_').next().unwrap_or(input);
307 normalize_id(id_part, 3)
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313
314 #[test]
315 fn test_normalize_id() {
316 assert_eq!(normalize_id("5", 3), "005");
317 assert_eq!(normalize_id("05", 3), "005");
318 assert_eq!(normalize_id("005", 3), "005");
319 assert_eq!(normalize_id("0005", 3), "005");
320 assert_eq!(normalize_id("1", 2), "01");
321 assert_eq!(normalize_id("01", 2), "01");
322 assert_eq!(normalize_id("001", 2), "01");
323 }
324
325 #[test]
326 fn test_parse_change_id() {
327 assert_eq!(
328 parse_change_id("005-01_my-change"),
329 Some(("005".to_string(), "01".to_string()))
330 );
331 assert_eq!(
332 parse_change_id("5-1_whatever"),
333 Some(("005".to_string(), "01".to_string()))
334 );
335 assert_eq!(
336 parse_change_id("1-2"),
337 Some(("001".to_string(), "02".to_string()))
338 );
339 assert_eq!(
340 parse_change_id("001-000002_foo"),
341 Some(("001".to_string(), "02".to_string()))
342 );
343 assert_eq!(parse_change_id("invalid"), None);
344 }
345
346 #[test]
347 fn test_parse_module_id() {
348 assert_eq!(parse_module_id("005"), "005");
349 assert_eq!(parse_module_id("5"), "005");
350 assert_eq!(parse_module_id("005_dev-tooling"), "005");
351 assert_eq!(parse_module_id("5_dev-tooling"), "005");
352 }
353
354 #[test]
355 fn test_extract_module_id() {
356 assert_eq!(
357 extract_module_id("005-01_my-change"),
358 Some("005".to_string())
359 );
360 assert_eq!(extract_module_id("013-18_cleanup"), Some("013".to_string()));
361 assert_eq!(extract_module_id("5-1_foo"), Some("005".to_string()));
362 assert_eq!(extract_module_id("invalid"), None);
363 }
364
365 #[test]
366 fn test_change_status_display() {
367 assert_eq!(ChangeStatus::NoTasks.to_string(), "no-tasks");
368 assert_eq!(ChangeStatus::InProgress.to_string(), "in-progress");
369 assert_eq!(ChangeStatus::Complete.to_string(), "complete");
370 }
371
372 #[test]
373 fn test_change_summary_status() {
374 let mut summary = ChangeSummary {
375 id: "test".to_string(),
376 module_id: None,
377 completed_tasks: 0,
378 shelved_tasks: 0,
379 in_progress_tasks: 0,
380 pending_tasks: 0,
381 total_tasks: 0,
382 last_modified: Utc::now(),
383 has_proposal: false,
384 has_design: false,
385 has_specs: false,
386 has_tasks: false,
387 };
388
389 assert_eq!(summary.status(), ChangeStatus::NoTasks);
390
391 summary.total_tasks = 5;
392 summary.completed_tasks = 3;
393 assert_eq!(summary.status(), ChangeStatus::InProgress);
394
395 summary.completed_tasks = 5;
396 assert_eq!(summary.status(), ChangeStatus::Complete);
397 }
398
399 #[test]
400 fn test_change_work_status() {
401 let mut summary = ChangeSummary {
402 id: "test".to_string(),
403 module_id: None,
404 completed_tasks: 0,
405 shelved_tasks: 0,
406 in_progress_tasks: 0,
407 pending_tasks: 0,
408 total_tasks: 0,
409 last_modified: Utc::now(),
410 has_proposal: false,
411 has_design: false,
412 has_specs: false,
413 has_tasks: false,
414 };
415
416 assert_eq!(summary.work_status(), ChangeWorkStatus::Draft);
417
418 summary.has_proposal = true;
419 summary.has_specs = true;
420 summary.has_tasks = true;
421 summary.total_tasks = 3;
422 summary.pending_tasks = 3;
423
424 assert_eq!(summary.work_status(), ChangeWorkStatus::Ready);
425
426 summary.in_progress_tasks = 1;
427 summary.pending_tasks = 2;
428 assert_eq!(summary.work_status(), ChangeWorkStatus::InProgress);
429
430 summary.in_progress_tasks = 0;
431 summary.pending_tasks = 0;
432 summary.shelved_tasks = 1;
433 summary.completed_tasks = 2;
434 assert_eq!(summary.work_status(), ChangeWorkStatus::Paused);
435
436 summary.shelved_tasks = 0;
437 summary.completed_tasks = 3;
438 assert_eq!(summary.work_status(), ChangeWorkStatus::Complete);
439 }
440}