1use anyhow::Result;
2use colored::Colorize;
3use std::collections::HashMap;
4use std::path::PathBuf;
5
6use crate::models::task::{Task, TaskStatus};
7use crate::storage::Storage;
8
9pub fn run(project_root: Option<PathBuf>, dry_run: bool) -> Result<()> {
11 let storage = Storage::new(project_root);
12
13 let tasks_file = storage.tasks_file();
15 if !tasks_file.exists() {
16 println!("{}", "No tasks file found. Nothing to migrate.".yellow());
17 return Ok(());
18 }
19
20 let mut all_tasks = storage.load_tasks()?;
21 let mut changes: Vec<String> = Vec::new();
22 let mut parent_fixes = 0;
23 let mut subtask_links = 0;
24
25 for (epic_tag, epic) in all_tasks.iter_mut() {
26 let mut id_map: HashMap<String, String> = HashMap::new();
27
28 for task in &epic.tasks {
30 if !task.id.contains(':') {
31 let new_id = Task::make_id(epic_tag, &task.id);
32 id_map.insert(task.id.clone(), new_id.clone());
33 changes.push(format!("{}: {} -> {}", epic_tag, task.id, new_id));
34 }
35 }
36
37 for task in &mut epic.tasks {
39 if let Some(new_id) = id_map.get(&task.id) {
41 task.id = new_id.clone();
42 }
43
44 task.dependencies = task
46 .dependencies
47 .iter()
48 .map(|dep| {
49 id_map.get(dep).cloned().unwrap_or_else(|| {
50 if dep.contains(':') {
51 dep.clone()
52 } else {
53 Task::make_id(epic_tag, dep)
54 }
55 })
56 })
57 .collect();
58
59 if let Some(ref parent) = task.parent_id {
61 task.parent_id = Some(
62 id_map
63 .get(parent)
64 .cloned()
65 .unwrap_or_else(|| Task::make_id(epic_tag, parent)),
66 );
67 }
68
69 task.subtasks = task
71 .subtasks
72 .iter()
73 .map(|sub| {
74 id_map
75 .get(sub)
76 .cloned()
77 .unwrap_or_else(|| Task::make_id(epic_tag, sub))
78 })
79 .collect();
80
81 if task.title.starts_with("[PARENT]") {
83 task.title = task.title.trim_start_matches("[PARENT]").trim().to_string();
84 task.status = TaskStatus::Expanded;
85 parent_fixes += 1;
86 }
87 }
88
89 let task_ids: Vec<String> = epic.tasks.iter().map(|t| t.id.clone()).collect();
92 let mut parent_child_links: Vec<(String, String)> = Vec::new(); for task in &epic.tasks {
95 let local_id = task.local_id().to_string();
97 if local_id.contains('.') && task.parent_id.is_none() {
98 if let Some(parent_local) = local_id.rsplit_once('.').map(|(p, _)| p.to_string()) {
100 let parent_id = Task::make_id(epic_tag, &parent_local);
101 if task_ids.contains(&parent_id) {
102 parent_child_links.push((task.id.clone(), parent_id));
103 }
104 }
105 }
106 }
107
108 for (child_id, parent_id) in parent_child_links {
110 if let Some(child) = epic.tasks.iter_mut().find(|t| t.id == child_id) {
112 child.parent_id = Some(parent_id.clone());
113 subtask_links += 1;
114 }
115 if let Some(parent) = epic.tasks.iter_mut().find(|t| t.id == parent_id) {
117 if !parent.subtasks.contains(&child_id) {
118 parent.subtasks.push(child_id);
119 }
120 }
121 }
122 }
123
124 for (_, epic) in all_tasks.iter_mut() {
126 let subtask_ids: Vec<String> = epic
127 .tasks
128 .iter()
129 .filter(|t| t.parent_id.is_some())
130 .filter_map(|t| t.parent_id.clone())
131 .collect();
132
133 for task in &mut epic.tasks {
134 if subtask_ids.contains(&task.id)
135 && task.status != TaskStatus::Expanded
136 && (task.status == TaskStatus::Pending || task.status == TaskStatus::InProgress)
137 {
138 task.status = TaskStatus::Expanded;
139 parent_fixes += 1;
140 }
141 }
142 }
143
144 if dry_run {
145 println!("{}", "Dry run - no changes made".yellow());
146 println!();
147
148 if changes.is_empty() && parent_fixes == 0 && subtask_links == 0 {
149 println!("{}", "No migrations needed. Data is up to date!".green());
150 return Ok(());
151 }
152
153 if !changes.is_empty() {
154 println!("{}", "ID changes:".blue().bold());
155 for change in &changes {
156 println!(" {}", change);
157 }
158 println!();
159 }
160
161 println!("{}", "Summary:".blue().bold());
162 println!(" {} ID namespacing changes", changes.len());
163 println!(" {} [PARENT] prefix fixes", parent_fixes);
164 println!(" {} subtask relationships inferred", subtask_links);
165 } else {
166 if changes.is_empty() && parent_fixes == 0 && subtask_links == 0 {
167 println!("{}", "No migrations needed. Data is up to date!".green());
168 return Ok(());
169 }
170
171 storage.save_tasks(&all_tasks)?;
172
173 println!("{}", "Migration complete!".green().bold());
174 println!();
175 println!(" {} task IDs namespaced", changes.len());
176 println!(
177 " {} [PARENT] prefixes converted to Expanded status",
178 parent_fixes
179 );
180 println!(" {} subtask relationships established", subtask_links);
181 println!();
182 println!(
183 "{}",
184 "Tip: Run 'scud list' to verify the migration.".dimmed()
185 );
186 }
187
188 Ok(())
189}