1use std::sync::atomic::{AtomicU64, Ordering};
4use std::sync::Arc;
5
6pub type ProgressCallback = Box<dyn Fn(ProgressUpdate) + Send + Sync>;
8
9#[derive(Debug, Clone)]
11pub struct ProgressUpdate {
12 pub phase: MigrationPhase,
14
15 pub current_item: Option<String>,
17
18 pub completed: u64,
20
21 pub total: u64,
23
24 pub message: Option<String>,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum MigrationPhase {
31 Initializing,
33 CreatingRepository,
35 CloningRepository,
37 PushingRepository,
39 MigratingLabels,
41 MigratingMilestones,
43 MigratingIssues,
45 MigratingPullRequests,
47 MigratingReleases,
49 MigratingWiki,
51 Verifying,
53 Complete,
55}
56
57impl std::fmt::Display for MigrationPhase {
58 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59 match self {
60 Self::Initializing => write!(f, "Initializing"),
61 Self::CreatingRepository => write!(f, "Creating repository"),
62 Self::CloningRepository => write!(f, "Cloning source repository"),
63 Self::PushingRepository => write!(f, "Pushing to Guts"),
64 Self::MigratingLabels => write!(f, "Migrating labels"),
65 Self::MigratingMilestones => write!(f, "Migrating milestones"),
66 Self::MigratingIssues => write!(f, "Migrating issues"),
67 Self::MigratingPullRequests => write!(f, "Migrating pull requests"),
68 Self::MigratingReleases => write!(f, "Migrating releases"),
69 Self::MigratingWiki => write!(f, "Migrating wiki"),
70 Self::Verifying => write!(f, "Verifying migration"),
71 Self::Complete => write!(f, "Complete"),
72 }
73 }
74}
75
76pub struct MigrationProgress {
78 phase: std::sync::atomic::AtomicU8,
79 completed: AtomicU64,
80 total: AtomicU64,
81 callback: Option<Arc<ProgressCallback>>,
82}
83
84impl MigrationProgress {
85 pub fn new() -> Self {
87 Self {
88 phase: std::sync::atomic::AtomicU8::new(0),
89 completed: AtomicU64::new(0),
90 total: AtomicU64::new(0),
91 callback: None,
92 }
93 }
94
95 pub fn with_callback(callback: ProgressCallback) -> Self {
97 Self {
98 phase: std::sync::atomic::AtomicU8::new(0),
99 completed: AtomicU64::new(0),
100 total: AtomicU64::new(0),
101 callback: Some(Arc::new(callback)),
102 }
103 }
104
105 pub fn set_phase(&self, phase: MigrationPhase, total: u64) {
107 self.phase.store(phase as u8, Ordering::SeqCst);
108 self.completed.store(0, Ordering::SeqCst);
109 self.total.store(total, Ordering::SeqCst);
110 self.notify(None, None);
111 }
112
113 pub fn increment(&self, item: Option<&str>) {
115 self.completed.fetch_add(1, Ordering::SeqCst);
116 self.notify(item.map(|s| s.to_string()), None);
117 }
118
119 pub fn message(&self, msg: &str) {
121 self.notify(None, Some(msg.to_string()));
122 }
123
124 pub fn percentage(&self) -> f64 {
126 let total = self.total.load(Ordering::SeqCst);
127 if total == 0 {
128 return 0.0;
129 }
130 let completed = self.completed.load(Ordering::SeqCst);
131 (completed as f64 / total as f64) * 100.0
132 }
133
134 pub fn current_phase(&self) -> MigrationPhase {
136 match self.phase.load(Ordering::SeqCst) {
137 0 => MigrationPhase::Initializing,
138 1 => MigrationPhase::CreatingRepository,
139 2 => MigrationPhase::CloningRepository,
140 3 => MigrationPhase::PushingRepository,
141 4 => MigrationPhase::MigratingLabels,
142 5 => MigrationPhase::MigratingMilestones,
143 6 => MigrationPhase::MigratingIssues,
144 7 => MigrationPhase::MigratingPullRequests,
145 8 => MigrationPhase::MigratingReleases,
146 9 => MigrationPhase::MigratingWiki,
147 10 => MigrationPhase::Verifying,
148 _ => MigrationPhase::Complete,
149 }
150 }
151
152 fn notify(&self, current_item: Option<String>, message: Option<String>) {
153 if let Some(callback) = &self.callback {
154 let update = ProgressUpdate {
155 phase: self.current_phase(),
156 current_item,
157 completed: self.completed.load(Ordering::SeqCst),
158 total: self.total.load(Ordering::SeqCst),
159 message,
160 };
161 callback(update);
162 }
163 }
164}
165
166impl Default for MigrationProgress {
167 fn default() -> Self {
168 Self::new()
169 }
170}
171
172pub struct ConsoleProgressReporter {
174 progress_bar: indicatif::ProgressBar,
175 multi_progress: indicatif::MultiProgress,
176}
177
178impl ConsoleProgressReporter {
179 pub fn new() -> Self {
181 let multi_progress = indicatif::MultiProgress::new();
182 let progress_bar = multi_progress.add(indicatif::ProgressBar::new(100));
183
184 progress_bar.set_style(
185 indicatif::ProgressStyle::default_bar()
186 .template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({percent}%) {msg}")
187 .unwrap()
188 .progress_chars("#>-"),
189 );
190
191 Self {
192 progress_bar,
193 multi_progress,
194 }
195 }
196
197 pub fn callback(&self) -> ProgressCallback {
199 let pb = self.progress_bar.clone();
200 Box::new(move |update: ProgressUpdate| {
201 pb.set_length(update.total);
202 pb.set_position(update.completed);
203
204 let mut msg = update.phase.to_string();
205 if let Some(item) = &update.current_item {
206 msg = format!("{msg}: {item}");
207 }
208 if let Some(message) = &update.message {
209 msg = format!("{msg} - {message}");
210 }
211 pb.set_message(msg);
212 })
213 }
214
215 pub fn finish(&self, message: &str) {
217 self.progress_bar.finish_with_message(message.to_string());
218 }
219
220 pub fn multi_progress(&self) -> &indicatif::MultiProgress {
222 &self.multi_progress
223 }
224}
225
226impl Default for ConsoleProgressReporter {
227 fn default() -> Self {
228 Self::new()
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235
236 #[test]
237 fn test_progress_tracker() {
238 let progress = MigrationProgress::new();
239
240 progress.set_phase(MigrationPhase::MigratingIssues, 10);
241 assert_eq!(progress.current_phase(), MigrationPhase::MigratingIssues);
242 assert_eq!(progress.percentage(), 0.0);
243
244 progress.increment(Some("Issue #1"));
245 assert!((progress.percentage() - 10.0).abs() < 0.01);
246
247 for _ in 0..9 {
248 progress.increment(None);
249 }
250 assert!((progress.percentage() - 100.0).abs() < 0.01);
251 }
252
253 #[test]
254 fn test_progress_with_callback() {
255 use std::sync::atomic::{AtomicUsize, Ordering};
256 let call_count = Arc::new(AtomicUsize::new(0));
257 let call_count_clone = call_count.clone();
258
259 let progress = MigrationProgress::with_callback(Box::new(move |_| {
260 call_count_clone.fetch_add(1, Ordering::SeqCst);
261 }));
262
263 progress.set_phase(MigrationPhase::MigratingIssues, 5);
264 progress.increment(None);
265 progress.increment(None);
266
267 assert!(call_count.load(Ordering::SeqCst) >= 3);
268 }
269}