Skip to main content

guts_migrate/
verify.rs

1//! Migration verification utilities.
2
3use crate::client::GutsClient;
4use crate::error::{MigrationError, Result};
5use crate::types::MigrationReport;
6
7use std::process::Command;
8use tempfile::TempDir;
9use tracing::info;
10
11/// Verification results for a migration.
12#[derive(Debug, Clone, Default)]
13pub struct VerificationResult {
14    /// Git data verification passed.
15    pub git_verified: bool,
16
17    /// Number of commits verified.
18    pub commits_verified: usize,
19
20    /// Number of branches verified.
21    pub branches_verified: usize,
22
23    /// Number of tags verified.
24    pub tags_verified: usize,
25
26    /// Issues count matches.
27    pub issues_verified: bool,
28
29    /// Pull requests count matches.
30    pub prs_verified: bool,
31
32    /// Releases count matches.
33    pub releases_verified: bool,
34
35    /// Verification errors.
36    pub errors: Vec<String>,
37
38    /// Verification warnings.
39    pub warnings: Vec<String>,
40}
41
42impl VerificationResult {
43    /// Check if verification passed.
44    pub fn is_success(&self) -> bool {
45        self.git_verified && self.errors.is_empty()
46    }
47
48    /// Print verification summary.
49    pub fn print_summary(&self) {
50        println!("\n=== Verification Summary ===\n");
51        println!(
52            "Git data:       {}",
53            if self.git_verified { "✓" } else { "✗" }
54        );
55        println!("  Commits:      {}", self.commits_verified);
56        println!("  Branches:     {}", self.branches_verified);
57        println!("  Tags:         {}", self.tags_verified);
58        println!(
59            "Issues:         {}",
60            if self.issues_verified { "✓" } else { "✗" }
61        );
62        println!(
63            "Pull Requests:  {}",
64            if self.prs_verified { "✓" } else { "✗" }
65        );
66        println!(
67            "Releases:       {}",
68            if self.releases_verified { "✓" } else { "✗" }
69        );
70
71        if !self.errors.is_empty() {
72            println!("\nErrors:");
73            for error in &self.errors {
74                println!("  - {error}");
75            }
76        }
77
78        if !self.warnings.is_empty() {
79            println!("\nWarnings:");
80            for warning in &self.warnings {
81                println!("  - {warning}");
82            }
83        }
84
85        println!(
86            "\nVerification: {}",
87            if self.is_success() {
88                "PASSED"
89            } else {
90                "FAILED"
91            }
92        );
93    }
94}
95
96/// Verifier for post-migration validation.
97pub struct MigrationVerifier {
98    #[allow(dead_code)]
99    guts_client: GutsClient,
100}
101
102impl MigrationVerifier {
103    /// Create a new verifier.
104    pub fn new(guts_url: &str, guts_token: Option<String>) -> Result<Self> {
105        let guts_client = GutsClient::new(guts_url, guts_token)?;
106        Ok(Self { guts_client })
107    }
108
109    /// Verify a migration between source and target.
110    pub async fn verify(
111        &self,
112        source_url: &str,
113        target_owner: &str,
114        target_repo: &str,
115        report: &MigrationReport,
116    ) -> Result<VerificationResult> {
117        let mut result = VerificationResult::default();
118
119        info!("Starting verification...");
120
121        // Verify Git data
122        if report.git_mirrored {
123            match self.verify_git(source_url, target_owner, target_repo).await {
124                Ok((commits, branches, tags)) => {
125                    result.git_verified = true;
126                    result.commits_verified = commits;
127                    result.branches_verified = branches;
128                    result.tags_verified = tags;
129                    info!("Git verification passed: {commits} commits, {branches} branches, {tags} tags");
130                }
131                Err(e) => {
132                    result.git_verified = false;
133                    result.errors.push(format!("Git verification failed: {e}"));
134                }
135            }
136        }
137
138        // Verify issues count
139        if report.issues_migrated > 0 {
140            match self.verify_issues(target_owner, target_repo).await {
141                Ok(count) => {
142                    if count >= report.issues_migrated {
143                        result.issues_verified = true;
144                        info!("Issues verification passed: {count} issues found");
145                    } else {
146                        result.warnings.push(format!(
147                            "Issue count mismatch: expected {}, found {}",
148                            report.issues_migrated, count
149                        ));
150                    }
151                }
152                Err(e) => {
153                    result
154                        .errors
155                        .push(format!("Issues verification failed: {e}"));
156                }
157            }
158        } else {
159            result.issues_verified = true; // No issues to verify
160        }
161
162        // Verify PRs count
163        if report.prs_migrated > 0 {
164            match self.verify_prs(target_owner, target_repo).await {
165                Ok(count) => {
166                    if count >= report.prs_migrated {
167                        result.prs_verified = true;
168                        info!("PRs verification passed: {count} PRs found");
169                    } else {
170                        result.warnings.push(format!(
171                            "PR count mismatch: expected {}, found {}",
172                            report.prs_migrated, count
173                        ));
174                    }
175                }
176                Err(e) => {
177                    result.errors.push(format!("PRs verification failed: {e}"));
178                }
179            }
180        } else {
181            result.prs_verified = true; // No PRs to verify
182        }
183
184        // Verify releases count
185        if report.releases_migrated > 0 {
186            match self.verify_releases(target_owner, target_repo).await {
187                Ok(count) => {
188                    if count >= report.releases_migrated {
189                        result.releases_verified = true;
190                        info!("Releases verification passed: {count} releases found");
191                    } else {
192                        result.warnings.push(format!(
193                            "Release count mismatch: expected {}, found {}",
194                            report.releases_migrated, count
195                        ));
196                    }
197                }
198                Err(e) => {
199                    result
200                        .errors
201                        .push(format!("Releases verification failed: {e}"));
202                }
203            }
204        } else {
205            result.releases_verified = true; // No releases to verify
206        }
207
208        Ok(result)
209    }
210
211    async fn verify_git(
212        &self,
213        source_url: &str,
214        target_owner: &str,
215        target_repo: &str,
216    ) -> Result<(usize, usize, usize)> {
217        let temp_dir = TempDir::new()?;
218        let source_path = temp_dir.path().join("source");
219        let target_path = temp_dir.path().join("target");
220
221        // Clone source
222        let output = Command::new("git")
223            .args(["clone", "--mirror", source_url])
224            .arg(&source_path)
225            .output()?;
226
227        if !output.status.success() {
228            return Err(MigrationError::VerificationFailed(format!(
229                "Failed to clone source: {}",
230                String::from_utf8_lossy(&output.stderr)
231            )));
232        }
233
234        // Clone target from Guts
235        // Note: This assumes the Guts git URL format
236        let guts_url = format!("http://localhost:8080/git/{target_owner}/{target_repo}.git");
237        let output = Command::new("git")
238            .args(["clone", "--mirror", &guts_url])
239            .arg(&target_path)
240            .output()?;
241
242        if !output.status.success() {
243            return Err(MigrationError::VerificationFailed(format!(
244                "Failed to clone target: {}",
245                String::from_utf8_lossy(&output.stderr)
246            )));
247        }
248
249        // Compare commit counts
250        let source_commits = count_commits(&source_path)?;
251        let target_commits = count_commits(&target_path)?;
252
253        if source_commits != target_commits {
254            return Err(MigrationError::VerificationFailed(format!(
255                "Commit count mismatch: source={source_commits}, target={target_commits}"
256            )));
257        }
258
259        // Count branches and tags
260        let branches = count_branches(&target_path)?;
261        let tags = count_tags(&target_path)?;
262
263        Ok((target_commits, branches, tags))
264    }
265
266    async fn verify_issues(&self, _owner: &str, _repo: &str) -> Result<usize> {
267        // TODO: Implement API call to count issues
268        Ok(0)
269    }
270
271    async fn verify_prs(&self, _owner: &str, _repo: &str) -> Result<usize> {
272        // TODO: Implement API call to count PRs
273        Ok(0)
274    }
275
276    async fn verify_releases(&self, _owner: &str, _repo: &str) -> Result<usize> {
277        // TODO: Implement API call to count releases
278        Ok(0)
279    }
280}
281
282fn count_commits(repo_path: &std::path::Path) -> Result<usize> {
283    let output = Command::new("git")
284        .current_dir(repo_path)
285        .args(["rev-list", "--all", "--count"])
286        .output()?;
287
288    if !output.status.success() {
289        return Ok(0);
290    }
291
292    let count_str = String::from_utf8_lossy(&output.stdout);
293    count_str.trim().parse().map_err(|e| {
294        MigrationError::VerificationFailed(format!("Failed to parse commit count: {e}"))
295    })
296}
297
298fn count_branches(repo_path: &std::path::Path) -> Result<usize> {
299    let output = Command::new("git")
300        .current_dir(repo_path)
301        .args(["branch", "-r"])
302        .output()?;
303
304    Ok(String::from_utf8_lossy(&output.stdout)
305        .lines()
306        .filter(|l| !l.is_empty())
307        .count())
308}
309
310fn count_tags(repo_path: &std::path::Path) -> Result<usize> {
311    let output = Command::new("git")
312        .current_dir(repo_path)
313        .args(["tag"])
314        .output()?;
315
316    Ok(String::from_utf8_lossy(&output.stdout)
317        .lines()
318        .filter(|l| !l.is_empty())
319        .count())
320}
321
322#[cfg(test)]
323mod tests {
324    use super::*;
325
326    #[test]
327    fn test_verification_result() {
328        let result = VerificationResult {
329            git_verified: true,
330            commits_verified: 100,
331            branches_verified: 5,
332            tags_verified: 3,
333            issues_verified: true,
334            prs_verified: true,
335            releases_verified: true,
336            ..Default::default()
337        };
338
339        assert!(result.is_success());
340    }
341
342    #[test]
343    fn test_verification_failure() {
344        let mut result = VerificationResult {
345            git_verified: false,
346            ..Default::default()
347        };
348        result.errors.push("Git mismatch".to_string());
349
350        assert!(!result.is_success());
351    }
352}