1use 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#[derive(Debug, Clone, Default)]
13pub struct VerificationResult {
14 pub git_verified: bool,
16
17 pub commits_verified: usize,
19
20 pub branches_verified: usize,
22
23 pub tags_verified: usize,
25
26 pub issues_verified: bool,
28
29 pub prs_verified: bool,
31
32 pub releases_verified: bool,
34
35 pub errors: Vec<String>,
37
38 pub warnings: Vec<String>,
40}
41
42impl VerificationResult {
43 pub fn is_success(&self) -> bool {
45 self.git_verified && self.errors.is_empty()
46 }
47
48 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
96pub struct MigrationVerifier {
98 #[allow(dead_code)]
99 guts_client: GutsClient,
100}
101
102impl MigrationVerifier {
103 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 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 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 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; }
161
162 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; }
183
184 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; }
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 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 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 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 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 Ok(0)
269 }
270
271 async fn verify_prs(&self, _owner: &str, _repo: &str) -> Result<usize> {
272 Ok(0)
274 }
275
276 async fn verify_releases(&self, _owner: &str, _repo: &str) -> Result<usize> {
277 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}