1use crate::args::Args;
2use crate::utils::types::{CommitInfo, Result};
3use chrono::NaiveDateTime;
4use colored::Colorize;
5
6#[derive(Debug, Clone)]
7pub struct SimulationChange {
8 pub commit_oid: git2::Oid,
9 pub short_hash: String,
10 pub original_author: String,
11 pub original_email: String,
12 pub original_timestamp: NaiveDateTime,
13 pub original_message: String,
14 pub new_author: Option<String>,
15 pub new_email: Option<String>,
16 pub new_timestamp: Option<NaiveDateTime>,
17 pub new_message: Option<String>,
18}
19
20#[derive(Debug)]
21pub struct SimulationStats {
22 pub total_commits: usize,
23 pub commits_to_change: usize,
24 pub authors_changed: usize,
25 pub emails_changed: usize,
26 pub timestamps_changed: usize,
27 pub messages_changed: usize,
28 pub date_range_start: Option<NaiveDateTime>,
29 pub date_range_end: Option<NaiveDateTime>,
30}
31
32#[derive(Debug)]
33pub struct SimulationResult {
34 pub changes: Vec<SimulationChange>,
35 pub stats: SimulationStats,
36 pub operation_mode: String,
37}
38
39impl SimulationChange {
40 pub fn has_changes(&self) -> bool {
41 self.new_author.is_some()
42 || self.new_email.is_some()
43 || self.new_timestamp.is_some()
44 || self.new_message.is_some()
45 }
46
47 pub fn get_change_summary(&self) -> Vec<String> {
48 let mut changes = Vec::new();
49
50 if let Some(ref new_author) = self.new_author {
51 if new_author != &self.original_author {
52 changes.push(format!(
53 "Author: {} → {}",
54 self.original_author.red(),
55 new_author.green()
56 ));
57 }
58 }
59
60 if let Some(ref new_email) = self.new_email {
61 if new_email != &self.original_email {
62 changes.push(format!(
63 "Email: {} → {}",
64 self.original_email.red(),
65 new_email.green()
66 ));
67 }
68 }
69
70 if let Some(ref new_timestamp) = self.new_timestamp {
71 if new_timestamp != &self.original_timestamp {
72 changes.push(format!(
73 "Date: {} → {}",
74 self.original_timestamp
75 .format("%Y-%m-%d %H:%M:%S")
76 .to_string()
77 .red(),
78 new_timestamp
79 .format("%Y-%m-%d %H:%M:%S")
80 .to_string()
81 .green()
82 ));
83 }
84 }
85
86 if let Some(ref new_message) = self.new_message {
87 let original_first_line = self.original_message.lines().next().unwrap_or("");
88 let new_first_line = new_message.lines().next().unwrap_or("");
89 if new_first_line != original_first_line {
90 changes.push(format!(
91 "Message: {} → {}",
92 original_first_line.red(),
93 new_first_line.green()
94 ));
95 }
96 }
97
98 changes
99 }
100}
101
102impl SimulationStats {
103 pub fn new(commits: &[CommitInfo]) -> Self {
104 let total_commits = commits.len();
105 let (date_range_start, date_range_end) = if !commits.is_empty() {
106 let mut timestamps: Vec<_> = commits.iter().map(|c| c.timestamp).collect();
107 timestamps.sort();
108 (timestamps.first().copied(), timestamps.last().copied())
109 } else {
110 (None, None)
111 };
112
113 Self {
114 total_commits,
115 commits_to_change: 0,
116 authors_changed: 0,
117 emails_changed: 0,
118 timestamps_changed: 0,
119 messages_changed: 0,
120 date_range_start,
121 date_range_end,
122 }
123 }
124
125 pub fn update_from_changes(&mut self, changes: &[SimulationChange]) {
126 self.commits_to_change = changes.iter().filter(|c| c.has_changes()).count();
127
128 for change in changes {
129 if change.new_author.is_some() {
130 self.authors_changed += 1;
131 }
132 if change.new_email.is_some() {
133 self.emails_changed += 1;
134 }
135 if change.new_timestamp.is_some() {
136 self.timestamps_changed += 1;
137 }
138 if change.new_message.is_some() {
139 self.messages_changed += 1;
140 }
141 }
142 }
143
144 pub fn print_summary(&self, operation_mode: &str) {
145 println!("\n{}", "📊 SIMULATION SUMMARY".bold().cyan());
146 println!("{}", "=".repeat(50).cyan());
147
148 println!("{}: {}", "Operation Mode".bold(), operation_mode.yellow());
149 println!(
150 "{}: {}",
151 "Total Commits".bold(),
152 self.total_commits.to_string().cyan()
153 );
154 println!(
155 "{}: {}",
156 "Commits to Change".bold(),
157 if self.commits_to_change > 0 {
158 self.commits_to_change.to_string().yellow()
159 } else {
160 self.commits_to_change.to_string().green()
161 }
162 );
163
164 if self.commits_to_change > 0 {
165 println!("\n{}", "Changes Breakdown:".bold());
166 if self.authors_changed > 0 {
167 println!(
168 " • {} commits will have author names changed",
169 self.authors_changed.to_string().yellow()
170 );
171 }
172 if self.emails_changed > 0 {
173 println!(
174 " • {} commits will have author emails changed",
175 self.emails_changed.to_string().yellow()
176 );
177 }
178 if self.timestamps_changed > 0 {
179 println!(
180 " • {} commits will have timestamps changed",
181 self.timestamps_changed.to_string().yellow()
182 );
183 }
184 if self.messages_changed > 0 {
185 println!(
186 " • {} commits will have messages changed",
187 self.messages_changed.to_string().yellow()
188 );
189 }
190 }
191
192 if let (Some(start), Some(end)) = (self.date_range_start, self.date_range_end) {
193 println!("\n{}", "Date Range:".bold());
194 println!(
195 " {} → {}",
196 start.format("%Y-%m-%d %H:%M:%S").to_string().blue(),
197 end.format("%Y-%m-%d %H:%M:%S").to_string().blue()
198 );
199 }
200
201 if self.commits_to_change == 0 {
202 println!(
203 "\n{}",
204 "✅ No changes would be made with current parameters."
205 .green()
206 .bold()
207 );
208 } else {
209 println!(
210 "\n{}",
211 "⚠️ This is a simulation - no actual changes have been made."
212 .yellow()
213 .bold()
214 );
215 println!(
216 "{}",
217 " Run without --simulate to apply these changes.".bright_black()
218 );
219 }
220 }
221}
222
223pub fn create_full_rewrite_simulation(
224 commits: &[CommitInfo],
225 timestamps: &[NaiveDateTime],
226 args: &Args,
227) -> Result<SimulationResult> {
228 let mut changes = Vec::new();
229 let new_author = args.name.as_ref().unwrap();
230 let new_email = args.email.as_ref().unwrap();
231
232 for (i, commit) in commits.iter().enumerate() {
233 let new_timestamp = timestamps.get(i).copied();
234
235 let change = SimulationChange {
236 commit_oid: commit.oid,
237 short_hash: commit.short_hash.clone(),
238 original_author: commit.author_name.clone(),
239 original_email: commit.author_email.clone(),
240 original_timestamp: commit.timestamp,
241 original_message: commit.message.clone(),
242 new_author: Some(new_author.clone()),
243 new_email: Some(new_email.clone()),
244 new_timestamp,
245 new_message: None, };
247
248 changes.push(change);
249 }
250
251 let mut stats = SimulationStats::new(commits);
252 stats.update_from_changes(&changes);
253
254 Ok(SimulationResult {
255 changes,
256 stats,
257 operation_mode: "Full Repository Rewrite".to_string(),
258 })
259}
260
261pub fn create_range_simulation(
262 commits: &[CommitInfo],
263 selected_range: (usize, usize),
264 range_timestamps: &[NaiveDateTime],
265 args: &Args,
266) -> Result<SimulationResult> {
267 let mut changes = Vec::new();
268 let (start_idx, end_idx) = selected_range;
269
270 for (i, commit) in commits.iter().enumerate() {
271 let change = if i >= start_idx && i <= end_idx {
272 let timestamp_idx = i - start_idx;
273 let new_timestamp = range_timestamps.get(timestamp_idx).copied();
274
275 SimulationChange {
276 commit_oid: commit.oid,
277 short_hash: commit.short_hash.clone(),
278 original_author: commit.author_name.clone(),
279 original_email: commit.author_email.clone(),
280 original_timestamp: commit.timestamp,
281 original_message: commit.message.clone(),
282 new_author: args.name.clone(),
283 new_email: args.email.clone(),
284 new_timestamp,
285 new_message: None,
286 }
287 } else {
288 SimulationChange {
290 commit_oid: commit.oid,
291 short_hash: commit.short_hash.clone(),
292 original_author: commit.author_name.clone(),
293 original_email: commit.author_email.clone(),
294 original_timestamp: commit.timestamp,
295 original_message: commit.message.clone(),
296 new_author: None,
297 new_email: None,
298 new_timestamp: None,
299 new_message: None,
300 }
301 };
302
303 changes.push(change);
304 }
305
306 let mut stats = SimulationStats::new(commits);
307 stats.update_from_changes(&changes);
308
309 Ok(SimulationResult {
310 changes,
311 stats,
312 operation_mode: format!("Range Edit (commits {}-{})", start_idx + 1, end_idx + 1),
313 })
314}
315
316pub fn create_specific_commit_simulation(
317 commits: &[CommitInfo],
318 selected_commit_idx: usize,
319 new_author: Option<String>,
320 new_email: Option<String>,
321 new_timestamp: Option<NaiveDateTime>,
322 new_message: Option<String>,
323) -> Result<SimulationResult> {
324 let mut changes = Vec::new();
325
326 for (i, commit) in commits.iter().enumerate() {
327 let change = if i == selected_commit_idx {
328 SimulationChange {
329 commit_oid: commit.oid,
330 short_hash: commit.short_hash.clone(),
331 original_author: commit.author_name.clone(),
332 original_email: commit.author_email.clone(),
333 original_timestamp: commit.timestamp,
334 original_message: commit.message.clone(),
335 new_author: new_author.clone(),
336 new_email: new_email.clone(),
337 new_timestamp,
338 new_message: new_message.clone(),
339 }
340 } else {
341 SimulationChange {
343 commit_oid: commit.oid,
344 short_hash: commit.short_hash.clone(),
345 original_author: commit.author_name.clone(),
346 original_email: commit.author_email.clone(),
347 original_timestamp: commit.timestamp,
348 original_message: commit.message.clone(),
349 new_author: None,
350 new_email: None,
351 new_timestamp: None,
352 new_message: None,
353 }
354 };
355
356 changes.push(change);
357 }
358
359 let mut stats = SimulationStats::new(commits);
360 stats.update_from_changes(&changes);
361
362 Ok(SimulationResult {
363 changes,
364 stats,
365 operation_mode: "Specific Commit Edit".to_string(),
366 })
367}
368
369pub fn print_detailed_diff(result: &SimulationResult) {
370 println!("\n{}", "📋 DETAILED CHANGE PREVIEW".bold().cyan());
371 println!("{}", "=".repeat(70).cyan());
372
373 let changes_to_show: Vec<_> = result.changes.iter().filter(|c| c.has_changes()).collect();
374
375 if changes_to_show.is_empty() {
376 println!("{}", "No changes to display.".green());
377 return;
378 }
379
380 for (i, change) in changes_to_show.iter().enumerate() {
381 println!(
382 "\n{} {} {} ({})",
383 format!("{}.", i + 1).bold(),
384 "Commit".bold(),
385 change.short_hash.yellow().bold(),
386 change.commit_oid.to_string()[..16]
387 .to_string()
388 .bright_black()
389 );
390
391 let change_summary = change.get_change_summary();
392 for summary_line in change_summary {
393 println!(" {summary_line}");
394 }
395
396 if i < changes_to_show.len() - 1 {
397 println!("{}", "─".repeat(50).bright_black());
398 }
399 }
400
401 println!(
402 "\n{}",
403 format!(
404 "Showing {} changes out of {} total commits",
405 changes_to_show.len(),
406 result.changes.len()
407 )
408 .bright_black()
409 );
410}
411
412#[cfg(test)]
413mod tests {
414 use super::*;
415 use chrono::NaiveDateTime;
416
417 fn create_test_commit(
418 oid_str: &str,
419 author: &str,
420 email: &str,
421 timestamp_str: &str,
422 message: &str,
423 ) -> CommitInfo {
424 CommitInfo {
425 oid: git2::Oid::from_str(oid_str).unwrap(),
426 short_hash: oid_str[..8].to_string(),
427 timestamp: NaiveDateTime::parse_from_str(timestamp_str, "%Y-%m-%d %H:%M:%S").unwrap(),
428 author_name: author.to_string(),
429 author_email: email.to_string(),
430 message: message.to_string(),
431 parent_count: 1,
432 }
433 }
434
435 #[test]
436 fn test_simulation_change_has_changes() {
437 let commit = create_test_commit(
438 "1234567890abcdef1234567890abcdef12345678",
439 "Test User",
440 "test@example.com",
441 "2023-01-01 10:00:00",
442 "Test commit",
443 );
444
445 let change = SimulationChange {
446 commit_oid: commit.oid,
447 short_hash: commit.short_hash,
448 original_author: commit.author_name,
449 original_email: commit.author_email,
450 original_timestamp: commit.timestamp,
451 original_message: commit.message,
452 new_author: Some("New Author".to_string()),
453 new_email: None,
454 new_timestamp: None,
455 new_message: None,
456 };
457
458 assert!(change.has_changes());
459 }
460
461 #[test]
462 fn test_simulation_change_no_changes() {
463 let commit = create_test_commit(
464 "1234567890abcdef1234567890abcdef12345678",
465 "Test User",
466 "test@example.com",
467 "2023-01-01 10:00:00",
468 "Test commit",
469 );
470
471 let change = SimulationChange {
472 commit_oid: commit.oid,
473 short_hash: commit.short_hash,
474 original_author: commit.author_name,
475 original_email: commit.author_email,
476 original_timestamp: commit.timestamp,
477 original_message: commit.message,
478 new_author: None,
479 new_email: None,
480 new_timestamp: None,
481 new_message: None,
482 };
483
484 assert!(!change.has_changes());
485 }
486
487 #[test]
488 fn test_simulation_stats_creation() {
489 let commits = vec![
490 create_test_commit(
491 "1234567890abcdef1234567890abcdef12345678",
492 "User1",
493 "user1@example.com",
494 "2023-01-01 10:00:00",
495 "First commit",
496 ),
497 create_test_commit(
498 "abcdef1234567890abcdef1234567890abcdef12",
499 "User2",
500 "user2@example.com",
501 "2023-01-02 15:30:00",
502 "Second commit",
503 ),
504 ];
505
506 let stats = SimulationStats::new(&commits);
507
508 assert_eq!(stats.total_commits, 2);
509 assert_eq!(stats.commits_to_change, 0);
510 assert!(stats.date_range_start.is_some());
511 assert!(stats.date_range_end.is_some());
512 }
513
514 #[test]
515 fn test_create_full_rewrite_simulation() {
516 let commits = vec![create_test_commit(
517 "1234567890abcdef1234567890abcdef12345678",
518 "Old User",
519 "old@example.com",
520 "2023-01-01 10:00:00",
521 "First commit",
522 )];
523
524 let timestamps =
525 vec![
526 NaiveDateTime::parse_from_str("2023-06-01 09:00:00", "%Y-%m-%d %H:%M:%S").unwrap(),
527 ];
528
529 let args = Args {
530 repo_path: Some("./test".to_string()),
531 email: Some("new@example.com".to_string()),
532 name: Some("New User".to_string()),
533 start: Some("2023-06-01 08:00:00".to_string()),
534 end: Some("2023-06-01 18:00:00".to_string()),
535 show_history: false,
536 pick_specific_commits: false,
537 range: false,
538 simulate: true,
539 show_diff: false,
540 };
541
542 let result = create_full_rewrite_simulation(&commits, ×tamps, &args).unwrap();
543
544 assert_eq!(result.changes.len(), 1);
545 assert_eq!(result.stats.total_commits, 1);
546 assert_eq!(result.stats.commits_to_change, 1);
547 assert_eq!(result.operation_mode, "Full Repository Rewrite");
548
549 let change = &result.changes[0];
550 assert!(change.has_changes());
551 assert_eq!(change.new_author.as_ref().unwrap(), "New User");
552 assert_eq!(change.new_email.as_ref().unwrap(), "new@example.com");
553 }
554
555 #[test]
556 fn test_create_specific_commit_simulation() {
557 let commits = vec![
558 create_test_commit(
559 "1234567890abcdef1234567890abcdef12345678",
560 "User1",
561 "user1@example.com",
562 "2023-01-01 10:00:00",
563 "First commit",
564 ),
565 create_test_commit(
566 "abcdef1234567890abcdef1234567890abcdef12",
567 "User2",
568 "user2@example.com",
569 "2023-01-02 15:30:00",
570 "Second commit",
571 ),
572 ];
573
574 let result = create_specific_commit_simulation(
575 &commits,
576 0, Some("New Author".to_string()),
578 Some("new@example.com".to_string()),
579 None,
580 Some("Updated message".to_string()),
581 )
582 .unwrap();
583
584 assert_eq!(result.changes.len(), 2);
585 assert_eq!(result.stats.commits_to_change, 1);
586
587 assert!(result.changes[0].has_changes());
589 assert_eq!(result.changes[0].new_author.as_ref().unwrap(), "New Author");
590
591 assert!(!result.changes[1].has_changes());
593 }
594}