1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7#[derive(Debug, Clone)]
9pub struct MigrationConfig {
10 pub source_repo: String,
12
13 pub guts_url: String,
15
16 pub guts_token: Option<String>,
18
19 pub target_name: Option<String>,
21
22 pub target_owner: Option<String>,
24}
25
26impl MigrationConfig {
27 pub fn new(source_repo: impl Into<String>, guts_url: impl Into<String>) -> Self {
29 Self {
30 source_repo: source_repo.into(),
31 guts_url: guts_url.into(),
32 guts_token: None,
33 target_name: None,
34 target_owner: None,
35 }
36 }
37
38 pub fn with_token(mut self, token: impl Into<String>) -> Self {
40 self.guts_token = Some(token.into());
41 self
42 }
43
44 pub fn with_target_name(mut self, name: impl Into<String>) -> Self {
46 self.target_name = Some(name.into());
47 self
48 }
49
50 pub fn with_target_owner(mut self, owner: impl Into<String>) -> Self {
52 self.target_owner = Some(owner.into());
53 self
54 }
55}
56
57#[derive(Debug, Clone)]
59pub struct MigrationOptions {
60 pub migrate_issues: bool,
62
63 pub migrate_pull_requests: bool,
65
66 pub migrate_releases: bool,
68
69 pub migrate_wiki: bool,
71
72 pub migrate_labels: bool,
74
75 pub migrate_milestones: bool,
77
78 pub include_closed: bool,
80
81 pub rewrite_links: bool,
83
84 pub user_mapping: HashMap<String, String>,
86}
87
88impl Default for MigrationOptions {
89 fn default() -> Self {
90 Self {
91 migrate_issues: true,
92 migrate_pull_requests: true,
93 migrate_releases: true,
94 migrate_wiki: true,
95 migrate_labels: true,
96 migrate_milestones: true,
97 include_closed: true,
98 rewrite_links: true,
99 user_mapping: HashMap::new(),
100 }
101 }
102}
103
104impl MigrationOptions {
105 pub fn with_issues(mut self, migrate: bool) -> Self {
107 self.migrate_issues = migrate;
108 self
109 }
110
111 pub fn with_pull_requests(mut self, migrate: bool) -> Self {
113 self.migrate_pull_requests = migrate;
114 self
115 }
116
117 pub fn with_releases(mut self, migrate: bool) -> Self {
119 self.migrate_releases = migrate;
120 self
121 }
122
123 pub fn with_wiki(mut self, migrate: bool) -> Self {
125 self.migrate_wiki = migrate;
126 self
127 }
128
129 pub fn with_labels(mut self, migrate: bool) -> Self {
131 self.migrate_labels = migrate;
132 self
133 }
134
135 pub fn with_milestones(mut self, migrate: bool) -> Self {
137 self.migrate_milestones = migrate;
138 self
139 }
140
141 pub fn with_closed(mut self, include: bool) -> Self {
143 self.include_closed = include;
144 self
145 }
146
147 pub fn with_link_rewriting(mut self, rewrite: bool) -> Self {
149 self.rewrite_links = rewrite;
150 self
151 }
152
153 pub fn with_user_mapping(
155 mut self,
156 source_user: impl Into<String>,
157 guts_user: impl Into<String>,
158 ) -> Self {
159 self.user_mapping
160 .insert(source_user.into(), guts_user.into());
161 self
162 }
163}
164
165#[derive(Debug, Clone, Default, Serialize, Deserialize)]
167pub struct MigrationReport {
168 pub repo_created: bool,
170
171 pub git_mirrored: bool,
173
174 pub branches_migrated: usize,
176
177 pub tags_migrated: usize,
179
180 pub issues_migrated: usize,
182
183 pub prs_migrated: usize,
185
186 pub releases_migrated: usize,
188
189 pub assets_migrated: usize,
191
192 pub wiki_migrated: bool,
194
195 pub labels_migrated: usize,
197
198 pub milestones_migrated: usize,
200
201 pub errors: Vec<MigrationErrorInfo>,
203
204 pub warnings: Vec<String>,
206
207 pub started_at: Option<DateTime<Utc>>,
209
210 pub completed_at: Option<DateTime<Utc>>,
212
213 pub guts_repo_url: Option<String>,
215}
216
217impl MigrationReport {
218 pub fn new() -> Self {
220 Self {
221 started_at: Some(Utc::now()),
222 ..Default::default()
223 }
224 }
225
226 pub fn complete(&mut self) {
228 self.completed_at = Some(Utc::now());
229 }
230
231 pub fn is_successful(&self) -> bool {
233 self.repo_created && self.git_mirrored && self.errors.iter().all(|e| !e.is_critical)
234 }
235
236 pub fn total_items_migrated(&self) -> usize {
238 self.issues_migrated
239 + self.prs_migrated
240 + self.releases_migrated
241 + self.labels_migrated
242 + self.milestones_migrated
243 }
244
245 pub fn add_error(&mut self, category: &str, message: &str, is_critical: bool) {
247 self.errors.push(MigrationErrorInfo {
248 category: category.to_string(),
249 message: message.to_string(),
250 is_critical,
251 });
252 }
253
254 pub fn add_warning(&mut self, message: impl Into<String>) {
256 self.warnings.push(message.into());
257 }
258
259 pub fn duration(&self) -> Option<chrono::Duration> {
261 match (self.started_at, self.completed_at) {
262 (Some(start), Some(end)) => Some(end - start),
263 _ => None,
264 }
265 }
266
267 pub fn print_summary(&self) {
269 println!("\n=== Migration Summary ===\n");
270 println!(
271 "Repository created: {}",
272 if self.repo_created { "✓" } else { "✗" }
273 );
274 println!(
275 "Git data mirrored: {}",
276 if self.git_mirrored { "✓" } else { "✗" }
277 );
278
279 if self.branches_migrated > 0 || self.tags_migrated > 0 {
280 println!("Branches migrated: {}", self.branches_migrated);
281 println!("Tags migrated: {}", self.tags_migrated);
282 }
283
284 println!("Issues migrated: {}", self.issues_migrated);
285 println!("PRs migrated: {}", self.prs_migrated);
286 println!("Releases migrated: {}", self.releases_migrated);
287 println!("Assets migrated: {}", self.assets_migrated);
288 println!(
289 "Wiki migrated: {}",
290 if self.wiki_migrated { "✓" } else { "N/A" }
291 );
292 println!("Labels migrated: {}", self.labels_migrated);
293 println!("Milestones: {}", self.milestones_migrated);
294
295 if let Some(url) = &self.guts_repo_url {
296 println!("\nRepository URL: {url}");
297 }
298
299 if let Some(duration) = self.duration() {
300 println!("\nCompleted in {} seconds", duration.num_seconds());
301 }
302
303 if !self.errors.is_empty() {
304 println!("\nErrors ({}):", self.errors.len());
305 for error in &self.errors {
306 let severity = if error.is_critical {
307 "CRITICAL"
308 } else {
309 "WARNING"
310 };
311 println!(" [{severity}] {}: {}", error.category, error.message);
312 }
313 }
314
315 if !self.warnings.is_empty() {
316 println!("\nWarnings ({}):", self.warnings.len());
317 for warning in &self.warnings {
318 println!(" - {warning}");
319 }
320 }
321
322 let status = if self.is_successful() {
323 "SUCCESS"
324 } else {
325 "FAILED"
326 };
327 println!("\nOverall Status: {status}");
328 }
329}
330
331#[derive(Debug, Clone, Serialize, Deserialize)]
333pub struct MigrationErrorInfo {
334 pub category: String,
336
337 pub message: String,
339
340 pub is_critical: bool,
342}
343
344#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
346#[serde(rename_all = "lowercase")]
347pub enum SourcePlatform {
348 GitHub,
350 GitLab,
352 Bitbucket,
354}
355
356impl std::fmt::Display for SourcePlatform {
357 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
358 match self {
359 Self::GitHub => write!(f, "GitHub"),
360 Self::GitLab => write!(f, "GitLab"),
361 Self::Bitbucket => write!(f, "Bitbucket"),
362 }
363 }
364}
365
366#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
368#[serde(rename_all = "lowercase")]
369pub enum IssueState {
370 Open,
371 Closed,
372}
373
374#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
376#[serde(rename_all = "lowercase")]
377pub enum PullRequestState {
378 Open,
379 Closed,
380 Merged,
381}
382
383#[derive(Debug, Clone, Serialize, Deserialize)]
385pub struct MigratedIssue {
386 pub source_number: u64,
387 pub guts_number: u64,
388 pub title: String,
389 pub state: IssueState,
390}
391
392#[derive(Debug, Clone, Serialize, Deserialize)]
394pub struct MigratedPullRequest {
395 pub source_number: u64,
396 pub guts_number: u64,
397 pub title: String,
398 pub state: PullRequestState,
399}
400
401#[derive(Debug, Clone, Serialize, Deserialize)]
403pub struct MigratedRelease {
404 pub tag_name: String,
405 pub name: String,
406 pub assets_count: usize,
407}