Skip to main content

guts_migrate/
progress.rs

1//! Progress tracking for migration operations.
2
3use std::sync::atomic::{AtomicU64, Ordering};
4use std::sync::Arc;
5
6/// Callback type for progress updates.
7pub type ProgressCallback = Box<dyn Fn(ProgressUpdate) + Send + Sync>;
8
9/// Progress update information.
10#[derive(Debug, Clone)]
11pub struct ProgressUpdate {
12    /// Current phase of migration.
13    pub phase: MigrationPhase,
14
15    /// Current item being processed.
16    pub current_item: Option<String>,
17
18    /// Items completed in current phase.
19    pub completed: u64,
20
21    /// Total items in current phase.
22    pub total: u64,
23
24    /// Optional message.
25    pub message: Option<String>,
26}
27
28/// Phases of the migration process.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum MigrationPhase {
31    /// Initializing migration.
32    Initializing,
33    /// Creating repository on Guts.
34    CreatingRepository,
35    /// Cloning source repository.
36    CloningRepository,
37    /// Pushing to Guts.
38    PushingRepository,
39    /// Migrating labels.
40    MigratingLabels,
41    /// Migrating milestones.
42    MigratingMilestones,
43    /// Migrating issues.
44    MigratingIssues,
45    /// Migrating pull requests.
46    MigratingPullRequests,
47    /// Migrating releases.
48    MigratingReleases,
49    /// Migrating wiki.
50    MigratingWiki,
51    /// Verifying migration.
52    Verifying,
53    /// Migration complete.
54    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
76/// Progress tracker for migration operations.
77pub struct MigrationProgress {
78    phase: std::sync::atomic::AtomicU8,
79    completed: AtomicU64,
80    total: AtomicU64,
81    callback: Option<Arc<ProgressCallback>>,
82}
83
84impl MigrationProgress {
85    /// Create a new progress tracker.
86    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    /// Create a progress tracker with a callback.
96    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    /// Set the current phase.
106    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    /// Increment progress.
114    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    /// Set a message.
120    pub fn message(&self, msg: &str) {
121        self.notify(None, Some(msg.to_string()));
122    }
123
124    /// Get current progress percentage.
125    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    /// Get current phase.
135    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
172/// Console progress reporter using indicatif.
173pub struct ConsoleProgressReporter {
174    progress_bar: indicatif::ProgressBar,
175    multi_progress: indicatif::MultiProgress,
176}
177
178impl ConsoleProgressReporter {
179    /// Create a new console progress reporter.
180    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    /// Create a progress callback for use with migration.
198    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    /// Finish the progress bar.
216    pub fn finish(&self, message: &str) {
217        self.progress_bar.finish_with_message(message.to_string());
218    }
219
220    /// Get the multi-progress handle.
221    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}