1mod repository;
7
8pub use repository::{ChangeRepository, ChangeTargetResolution, ResolveTargetOptions};
9
10use chrono::{DateTime, Utc};
11use std::path::PathBuf;
12
13use crate::tasks::{ProgressInfo, TasksParseResult};
14
15#[derive(Debug, Clone)]
17pub struct Spec {
18 pub name: String,
20 pub content: String,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum ChangeStatus {
27 NoTasks,
29 InProgress,
31 Complete,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum ChangeWorkStatus {
48 Draft,
50 Ready,
52 InProgress,
54 Paused,
58 Complete,
62}
63
64impl std::fmt::Display for ChangeWorkStatus {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 match self {
67 ChangeWorkStatus::Draft => write!(f, "draft"),
68 ChangeWorkStatus::Ready => write!(f, "ready"),
69 ChangeWorkStatus::InProgress => write!(f, "in-progress"),
70 ChangeWorkStatus::Paused => write!(f, "paused"),
71 ChangeWorkStatus::Complete => write!(f, "complete"),
72 }
73 }
74}
75
76impl std::fmt::Display for ChangeStatus {
77 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78 match self {
79 ChangeStatus::NoTasks => write!(f, "no-tasks"),
80 ChangeStatus::InProgress => write!(f, "in-progress"),
81 ChangeStatus::Complete => write!(f, "complete"),
82 }
83 }
84}
85
86#[derive(Debug, Clone)]
88pub struct Change {
89 pub id: String,
91 pub module_id: Option<String>,
93 pub path: PathBuf,
95 pub proposal: Option<String>,
97 pub design: Option<String>,
99 pub specs: Vec<Spec>,
101 pub tasks: TasksParseResult,
103 pub last_modified: DateTime<Utc>,
105}
106
107impl Change {
108 pub fn status(&self) -> ChangeStatus {
110 let progress = &self.tasks.progress;
111 if progress.total == 0 {
112 ChangeStatus::NoTasks
113 } else if progress.complete >= progress.total {
114 ChangeStatus::Complete
115 } else {
116 ChangeStatus::InProgress
117 }
118 }
119
120 pub fn work_status(&self) -> ChangeWorkStatus {
122 let ProgressInfo {
123 total,
124 complete,
125 shelved,
126 in_progress,
127 pending,
128 remaining: _,
129 } = self.tasks.progress;
130
131 let has_planning_artifacts = self.proposal.is_some() && !self.specs.is_empty() && total > 0;
133 if !has_planning_artifacts {
134 return ChangeWorkStatus::Draft;
135 }
136
137 if complete == total {
138 return ChangeWorkStatus::Complete;
139 }
140 if in_progress > 0 {
141 return ChangeWorkStatus::InProgress;
142 }
143
144 let done_or_shelved = complete + shelved;
145 if pending == 0 && shelved > 0 && done_or_shelved == total {
146 return ChangeWorkStatus::Paused;
147 }
148
149 ChangeWorkStatus::Ready
150 }
151
152 pub fn artifacts_complete(&self) -> bool {
154 self.proposal.is_some()
155 && self.design.is_some()
156 && !self.specs.is_empty()
157 && self.tasks.progress.total > 0
158 }
159
160 pub fn task_progress(&self) -> (u32, u32) {
162 (
163 self.tasks.progress.complete as u32,
164 self.tasks.progress.total as u32,
165 )
166 }
167
168 pub fn progress(&self) -> &ProgressInfo {
170 &self.tasks.progress
171 }
172}
173
174#[derive(Debug, Clone)]
176pub struct ChangeSummary {
177 pub id: String,
179 pub module_id: Option<String>,
181 pub completed_tasks: u32,
183 pub shelved_tasks: u32,
185 pub in_progress_tasks: u32,
187 pub pending_tasks: u32,
189 pub total_tasks: u32,
191 pub last_modified: DateTime<Utc>,
193 pub has_proposal: bool,
195 pub has_design: bool,
197 pub has_specs: bool,
199 pub has_tasks: bool,
201}
202
203impl ChangeSummary {
204 pub fn status(&self) -> ChangeStatus {
206 if self.total_tasks == 0 {
207 ChangeStatus::NoTasks
208 } else if self.completed_tasks >= self.total_tasks {
209 ChangeStatus::Complete
210 } else {
211 ChangeStatus::InProgress
212 }
213 }
214
215 pub fn work_status(&self) -> ChangeWorkStatus {
217 let has_planning_artifacts = self.has_proposal && self.has_specs && self.has_tasks;
218 if !has_planning_artifacts {
219 return ChangeWorkStatus::Draft;
220 }
221
222 if self.total_tasks > 0 && self.completed_tasks == self.total_tasks {
223 return ChangeWorkStatus::Complete;
224 }
225 if self.in_progress_tasks > 0 {
226 return ChangeWorkStatus::InProgress;
227 }
228
229 let done_or_shelved = self.completed_tasks + self.shelved_tasks;
230 if self.pending_tasks == 0 && self.shelved_tasks > 0 && done_or_shelved == self.total_tasks
231 {
232 return ChangeWorkStatus::Paused;
233 }
234
235 ChangeWorkStatus::Ready
236 }
237
238 pub fn is_ready(&self) -> bool {
243 self.work_status() == ChangeWorkStatus::Ready
244 }
245}
246
247pub fn extract_module_id(change_id: &str) -> Option<String> {
255 let parts: Vec<&str> = change_id.split('-').collect();
256 if parts.len() >= 2 {
257 Some(normalize_id(parts[0], 3))
258 } else {
259 None
260 }
261}
262
263pub fn normalize_id(id: &str, width: usize) -> String {
269 let num: u32 = id.parse().unwrap_or(0);
271 format!("{:0>width$}", num, width = width)
272}
273
274pub fn parse_change_id(input: &str) -> Option<(String, String)> {
282 let id_part = input.split('_').next().unwrap_or(input);
284
285 let parts: Vec<&str> = id_part.split('-').collect();
286 if parts.len() >= 2 {
287 let module_id = normalize_id(parts[0], 3);
288 let change_num = normalize_id(parts[1], 2);
289 Some((module_id, change_num))
290 } else {
291 None
292 }
293}
294
295pub fn parse_module_id(input: &str) -> String {
303 let id_part = input.split('_').next().unwrap_or(input);
305 normalize_id(id_part, 3)
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311
312 #[test]
313 fn test_normalize_id() {
314 assert_eq!(normalize_id("5", 3), "005");
315 assert_eq!(normalize_id("05", 3), "005");
316 assert_eq!(normalize_id("005", 3), "005");
317 assert_eq!(normalize_id("0005", 3), "005");
318 assert_eq!(normalize_id("1", 2), "01");
319 assert_eq!(normalize_id("01", 2), "01");
320 assert_eq!(normalize_id("001", 2), "01");
321 }
322
323 #[test]
324 fn test_parse_change_id() {
325 assert_eq!(
326 parse_change_id("005-01_my-change"),
327 Some(("005".to_string(), "01".to_string()))
328 );
329 assert_eq!(
330 parse_change_id("5-1_whatever"),
331 Some(("005".to_string(), "01".to_string()))
332 );
333 assert_eq!(
334 parse_change_id("1-2"),
335 Some(("001".to_string(), "02".to_string()))
336 );
337 assert_eq!(
338 parse_change_id("001-000002_foo"),
339 Some(("001".to_string(), "02".to_string()))
340 );
341 assert_eq!(parse_change_id("invalid"), None);
342 }
343
344 #[test]
345 fn test_parse_module_id() {
346 assert_eq!(parse_module_id("005"), "005");
347 assert_eq!(parse_module_id("5"), "005");
348 assert_eq!(parse_module_id("005_dev-tooling"), "005");
349 assert_eq!(parse_module_id("5_dev-tooling"), "005");
350 }
351
352 #[test]
353 fn test_extract_module_id() {
354 assert_eq!(
355 extract_module_id("005-01_my-change"),
356 Some("005".to_string())
357 );
358 assert_eq!(extract_module_id("013-18_cleanup"), Some("013".to_string()));
359 assert_eq!(extract_module_id("5-1_foo"), Some("005".to_string()));
360 assert_eq!(extract_module_id("invalid"), None);
361 }
362
363 #[test]
364 fn test_change_status_display() {
365 assert_eq!(ChangeStatus::NoTasks.to_string(), "no-tasks");
366 assert_eq!(ChangeStatus::InProgress.to_string(), "in-progress");
367 assert_eq!(ChangeStatus::Complete.to_string(), "complete");
368 }
369
370 #[test]
371 fn test_change_summary_status() {
372 let mut summary = ChangeSummary {
373 id: "test".to_string(),
374 module_id: None,
375 completed_tasks: 0,
376 shelved_tasks: 0,
377 in_progress_tasks: 0,
378 pending_tasks: 0,
379 total_tasks: 0,
380 last_modified: Utc::now(),
381 has_proposal: false,
382 has_design: false,
383 has_specs: false,
384 has_tasks: false,
385 };
386
387 assert_eq!(summary.status(), ChangeStatus::NoTasks);
388
389 summary.total_tasks = 5;
390 summary.completed_tasks = 3;
391 assert_eq!(summary.status(), ChangeStatus::InProgress);
392
393 summary.completed_tasks = 5;
394 assert_eq!(summary.status(), ChangeStatus::Complete);
395 }
396
397 #[test]
398 fn test_change_work_status() {
399 let mut summary = ChangeSummary {
400 id: "test".to_string(),
401 module_id: None,
402 completed_tasks: 0,
403 shelved_tasks: 0,
404 in_progress_tasks: 0,
405 pending_tasks: 0,
406 total_tasks: 0,
407 last_modified: Utc::now(),
408 has_proposal: false,
409 has_design: false,
410 has_specs: false,
411 has_tasks: false,
412 };
413
414 assert_eq!(summary.work_status(), ChangeWorkStatus::Draft);
415
416 summary.has_proposal = true;
417 summary.has_specs = true;
418 summary.has_tasks = true;
419 summary.total_tasks = 3;
420 summary.pending_tasks = 3;
421
422 assert_eq!(summary.work_status(), ChangeWorkStatus::Ready);
423
424 summary.in_progress_tasks = 1;
425 summary.pending_tasks = 2;
426 assert_eq!(summary.work_status(), ChangeWorkStatus::InProgress);
427
428 summary.in_progress_tasks = 0;
429 summary.pending_tasks = 0;
430 summary.shelved_tasks = 1;
431 summary.completed_tasks = 2;
432 assert_eq!(summary.work_status(), ChangeWorkStatus::Paused);
433
434 summary.shelved_tasks = 0;
435 summary.completed_tasks = 3;
436 assert_eq!(summary.work_status(), ChangeWorkStatus::Complete);
437 }
438}