git_editor/rewrite/
rewrite_range.rs

1use crate::utils::types::CommitInfo;
2use crate::utils::types::Result;
3use crate::{args::Args, utils::commit_history::get_commit_history};
4use chrono::NaiveDateTime;
5use colored::Colorize;
6use crossterm::{
7    cursor,
8    event::{self, Event, KeyCode, KeyEvent, KeyEventKind},
9    terminal::{self, Clear, ClearType},
10    ExecutableCommand,
11};
12use git2::{Repository, Signature, Sort, Time};
13use std::collections::HashMap;
14use std::io::{self, Write};
15
16#[derive(Debug, Clone)]
17struct CommitEdit {
18    index: usize,
19    original: CommitInfo,
20    author_name: String,
21    author_email: String,
22    timestamp: NaiveDateTime,
23    message: String,
24    is_modified: bool,
25    modifications: ModificationFlags,
26}
27
28#[derive(Debug, Clone, Default)]
29struct ModificationFlags {
30    author_name_changed: bool,
31    author_email_changed: bool,
32    timestamp_changed: bool,
33    message_changed: bool,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq)]
37enum TableColumn {
38    Index = 0,
39    Hash = 1,
40    AuthorName = 2,
41    AuthorEmail = 3,
42    Timestamp = 4,
43    Message = 5,
44}
45
46struct InteractiveTable {
47    commits: Vec<CommitEdit>,
48    current_row: usize,
49    current_col: TableColumn,
50    editing: bool,
51    edit_buffer: String,
52    editable_fields: (bool, bool, bool, bool), // (author_name, author_email, timestamp, message)
53}
54
55impl InteractiveTable {
56    fn new(
57        commits: Vec<CommitInfo>,
58        start_idx: usize,
59        end_idx: usize,
60        editable_fields: (bool, bool, bool, bool),
61    ) -> Self {
62        let mut commit_edits = Vec::new();
63
64        for (i, commit) in commits[start_idx..=end_idx].iter().enumerate() {
65            commit_edits.push(CommitEdit {
66                index: start_idx + i,
67                original: commit.clone(),
68                author_name: commit.author_name.clone(),
69                author_email: commit.author_email.clone(),
70                timestamp: commit.timestamp,
71                message: commit.message.clone(), // Keep full message, truncate only for display
72                is_modified: false,
73                modifications: ModificationFlags::default(),
74            });
75        }
76
77        // Find the first editable column as starting position
78        let starting_col = if editable_fields.0 {
79            // author_name
80            TableColumn::AuthorName
81        } else if editable_fields.1 {
82            // author_email
83            TableColumn::AuthorEmail
84        } else if editable_fields.2 {
85            // timestamp
86            TableColumn::Timestamp
87        } else if editable_fields.3 {
88            // message
89            TableColumn::Message
90        } else {
91            TableColumn::AuthorName // fallback
92        };
93
94        Self {
95            commits: commit_edits,
96            current_row: 0,
97            current_col: starting_col,
98            editing: false,
99            edit_buffer: String::new(),
100            editable_fields,
101        }
102    }
103
104    fn draw_table(&self) {
105        // Clear screen using crossterm
106        let _ = io::stdout().execute(Clear(ClearType::All));
107        let _ = io::stdout().execute(cursor::MoveTo(0, 0));
108
109        println!(
110            "{}",
111            "Interactive Commit Editor - Range Mode".bold().green()
112        );
113
114        // Show which fields are editable
115        let editable_info = if self.editable_fields == (true, true, true, true) {
116            "All fields editable".to_string()
117        } else {
118            let mut editable = Vec::new();
119            if self.editable_fields.0 || self.editable_fields.1 {
120                editable.push("Author");
121            }
122            if self.editable_fields.2 {
123                editable.push("Time");
124            }
125            if self.editable_fields.3 {
126                editable.push("Message");
127            }
128            format!("Editable: {}", editable.join(", "))
129        };
130        println!("{}", editable_info.cyan());
131        println!(
132            "{}",
133            "Use Arrow Keys to navigate, Enter to edit, Esc to save & exit, Ctrl+C to cancel"
134                .yellow()
135        );
136        println!();
137
138        // Print header
139        println!(
140            "{:<4} {:<8} {:<15} {:<20} {:<19} {}",
141            "#".bold().white(),
142            "HASH".bold().white(),
143            "AUTHOR NAME".bold().white(),
144            "AUTHOR EMAIL".bold().white(),
145            "TIMESTAMP".bold().white(),
146            "MESSAGE".bold().white()
147        );
148
149        // Draw rows
150        for (row_idx, commit) in self.commits.iter().enumerate() {
151            let is_current_row = row_idx == self.current_row;
152
153            // Prepare content
154            let index_str = format!("{}", commit.index + 1);
155            let hash_str = self.truncate_text(&commit.original.short_hash, 8);
156            let author_name_str = self.truncate_text(&commit.author_name, 15);
157            let author_email_str = self.truncate_text(&commit.author_email, 20);
158            let timestamp_str = commit.timestamp.format("%Y-%m-%d %H:%M:%S").to_string();
159            let first_line_message = commit.message.lines().next().unwrap_or("");
160            let message_str = self.truncate_text(first_line_message, 40);
161
162            // Add modification indicators and current cell brackets
163            let is_current_cell_index =
164                is_current_row && matches!(self.current_col, TableColumn::Index);
165            let is_current_cell_hash =
166                is_current_row && matches!(self.current_col, TableColumn::Hash);
167            let is_current_cell_author_name =
168                is_current_row && matches!(self.current_col, TableColumn::AuthorName);
169            let is_current_cell_author_email =
170                is_current_row && matches!(self.current_col, TableColumn::AuthorEmail);
171            let is_current_cell_timestamp =
172                is_current_row && matches!(self.current_col, TableColumn::Timestamp);
173            let is_current_cell_message =
174                is_current_row && matches!(self.current_col, TableColumn::Message);
175
176            let index_final = index_str; // Index is never editable, so no brackets
177            let hash_final = hash_str; // Hash is never editable, so no brackets
178
179            let author_name_with_mod = if commit.modifications.author_name_changed {
180                format!("*{author_name_str}")
181            } else {
182                author_name_str
183            };
184            let author_name_final = author_name_with_mod;
185
186            let author_email_with_mod = if commit.modifications.author_email_changed {
187                format!("*{author_email_str}")
188            } else {
189                author_email_str
190            };
191            let author_email_final = author_email_with_mod;
192
193            let timestamp_with_mod = if commit.modifications.timestamp_changed {
194                format!("*{timestamp_str}")
195            } else {
196                timestamp_str
197            };
198            let timestamp_final = timestamp_with_mod;
199
200            let message_with_mod = if commit.modifications.message_changed {
201                format!("*{message_str}")
202            } else {
203                message_str
204            };
205            let message_final = message_with_mod;
206
207            // Apply formatting and colors
208            if is_current_row {
209                if self.editing {
210                    println!(
211                        "{:<4} {:<8} {:<15} {:<20} {:<19} {}",
212                        index_final.black().on_yellow(),
213                        hash_final.black().on_yellow(),
214                        author_name_final.black().on_yellow(),
215                        author_email_final.black().on_yellow(),
216                        timestamp_final.black().on_yellow(),
217                        message_final.black().on_yellow()
218                    );
219                } else {
220                    // Current row, not editing - highlight current cell with special background
221                    let index_styled = if is_current_cell_index {
222                        index_final.white().on_blue()
223                    } else {
224                        index_final.white().on_bright_black()
225                    };
226                    let hash_styled = if is_current_cell_hash {
227                        hash_final.white().on_blue()
228                    } else {
229                        hash_final.yellow().on_bright_black()
230                    };
231                    let author_name_styled =
232                        if is_current_cell_author_name && self.editable_fields.0 {
233                            author_name_final.white().on_blue()
234                        } else {
235                            author_name_final.cyan().on_bright_black()
236                        };
237                    let author_email_styled =
238                        if is_current_cell_author_email && self.editable_fields.1 {
239                            author_email_final.white().on_blue()
240                        } else {
241                            author_email_final.blue().on_bright_black()
242                        };
243                    let timestamp_styled = if is_current_cell_timestamp && self.editable_fields.2 {
244                        timestamp_final.white().on_blue()
245                    } else {
246                        timestamp_final.magenta().on_bright_black()
247                    };
248                    let message_styled = if is_current_cell_message && self.editable_fields.3 {
249                        message_final.white().on_blue()
250                    } else {
251                        message_final.green().on_bright_black()
252                    };
253
254                    println!(
255                        "{index_styled:<4} {hash_styled:<8} {author_name_styled:<15} {author_email_styled:<20} {timestamp_styled:<19} {message_styled}"
256                    );
257                }
258            } else {
259                println!(
260                    "{:<4} {:<8} {:<15} {:<20} {:<19} {}",
261                    index_final.white(),
262                    hash_final.yellow(),
263                    author_name_final.cyan(),
264                    author_email_final.blue(),
265                    timestamp_final.magenta(),
266                    message_final.green()
267                );
268            }
269        }
270
271        println!();
272
273        if self.editing {
274            println!("{}: {}", "Editing".bold().yellow(), self.edit_buffer);
275            println!("{}", "Press Enter to save, Esc to cancel edit".italic());
276        } else {
277            println!(
278                "{}",
279                "Navigation: ←→↑↓  Edit: Enter  Save & Exit: Esc  Cancel: Ctrl+C".italic()
280            );
281            println!(
282                "{}",
283                "Tip: Use '*' when selecting range to edit ALL commits at once".dimmed()
284            );
285        }
286    }
287
288    fn truncate_text(&self, text: &str, max_width: usize) -> String {
289        if text.len() > max_width {
290            format!("{}…", &text[..max_width.saturating_sub(1)])
291        } else {
292            text.to_string()
293        }
294    }
295
296    fn handle_navigation_key_input(&mut self, key: KeyCode) -> Result<bool> {
297        match key {
298            KeyCode::Up => {
299                if self.current_row > 0 {
300                    self.current_row -= 1;
301                }
302            }
303            KeyCode::Down => {
304                if self.current_row < self.commits.len() - 1 {
305                    self.current_row += 1;
306                }
307            }
308            KeyCode::Left => {
309                self.move_to_prev_editable_column();
310            }
311            KeyCode::Right => {
312                self.move_to_next_editable_column();
313            }
314            KeyCode::Char('h') => {
315                // Left (vim-style)
316                self.move_to_prev_editable_column();
317            }
318            KeyCode::Char('l') => {
319                // Right (vim-style)
320                self.move_to_next_editable_column();
321            }
322            KeyCode::Char('k') => {
323                // Up (vim-style)
324                if self.current_row > 0 {
325                    self.current_row -= 1;
326                }
327            }
328            KeyCode::Char('j') => {
329                // Down (vim-style)
330                if self.current_row < self.commits.len() - 1 {
331                    self.current_row += 1;
332                }
333            }
334            KeyCode::Enter => {
335                self.start_editing();
336                return Ok(true);
337            }
338            KeyCode::Esc => {
339                return Ok(false); // Exit and save
340            }
341            _ => {}
342        }
343        Ok(true)
344    }
345
346    fn is_column_editable(&self, col: &TableColumn) -> bool {
347        match col {
348            TableColumn::Index | TableColumn::Hash => false,
349            TableColumn::AuthorName => self.editable_fields.0,
350            TableColumn::AuthorEmail => self.editable_fields.1,
351            TableColumn::Timestamp => self.editable_fields.2,
352            TableColumn::Message => self.editable_fields.3,
353        }
354    }
355
356    fn move_to_next_editable_column(&mut self) {
357        let columns = [
358            TableColumn::Index,
359            TableColumn::Hash,
360            TableColumn::AuthorName,
361            TableColumn::AuthorEmail,
362            TableColumn::Timestamp,
363            TableColumn::Message,
364        ];
365
366        let current_index = columns
367            .iter()
368            .position(|c| std::mem::discriminant(c) == std::mem::discriminant(&self.current_col))
369            .unwrap_or(0);
370
371        for i in 1..columns.len() {
372            let next_index = (current_index + i) % columns.len();
373            let next_col = &columns[next_index];
374            if self.is_column_editable(next_col) {
375                self.current_col = *next_col;
376                return;
377            }
378        }
379    }
380
381    fn move_to_prev_editable_column(&mut self) {
382        let columns = [
383            TableColumn::Index,
384            TableColumn::Hash,
385            TableColumn::AuthorName,
386            TableColumn::AuthorEmail,
387            TableColumn::Timestamp,
388            TableColumn::Message,
389        ];
390
391        let current_index = columns
392            .iter()
393            .position(|c| std::mem::discriminant(c) == std::mem::discriminant(&self.current_col))
394            .unwrap_or(0);
395
396        for i in 1..columns.len() {
397            let prev_index = if current_index >= i {
398                current_index - i
399            } else {
400                columns.len() - (i - current_index)
401            };
402            let prev_col = &columns[prev_index];
403            if self.is_column_editable(prev_col) {
404                self.current_col = *prev_col;
405                return;
406            }
407        }
408    }
409
410    fn start_editing(&mut self) {
411        if !self.is_column_editable(&self.current_col) {
412            return; // This column is not editable
413        }
414
415        self.editing = true;
416
417        // Initialize edit buffer with current value
418        self.edit_buffer = match self.current_col {
419            TableColumn::AuthorName => self.commits[self.current_row].author_name.clone(),
420            TableColumn::AuthorEmail => self.commits[self.current_row].author_email.clone(),
421            TableColumn::Timestamp => self.commits[self.current_row]
422                .timestamp
423                .format("%Y-%m-%d %H:%M:%S")
424                .to_string(),
425            TableColumn::Message => {
426                // Use the full original message when editing, not the truncated display version
427                if self.commits[self.current_row].modifications.message_changed {
428                    self.commits[self.current_row].message.clone()
429                } else {
430                    // Get the full original message from the first line or full message
431                    self.commits[self.current_row].original.message.clone()
432                }
433            }
434            _ => String::new(),
435        };
436    }
437
438    fn handle_edit_key_input(&mut self, key: KeyCode) -> Result<bool> {
439        match key {
440            KeyCode::Esc => {
441                // Esc - cancel edit
442                self.editing = false;
443                self.edit_buffer.clear();
444            }
445            KeyCode::Enter => {
446                // Enter - save edit
447                if let Err(e) = self.save_current_edit() {
448                    // On error, show message and stay in edit mode
449                    self.edit_buffer = format!("Error: {e} (Press Esc to cancel)");
450                    return Ok(true);
451                }
452                self.editing = false;
453                self.edit_buffer.clear();
454            }
455            KeyCode::Backspace => {
456                self.edit_buffer.pop();
457            }
458            KeyCode::Char(c) => {
459                // Handle printable characters
460                self.edit_buffer.push(c);
461            }
462            _ => {}
463        }
464        Ok(true)
465    }
466
467    fn save_current_edit(&mut self) -> Result<()> {
468        let commit = &mut self.commits[self.current_row];
469
470        match self.current_col {
471            TableColumn::AuthorName => {
472                if self.edit_buffer.trim().is_empty() {
473                    return Err("Author name cannot be empty".into());
474                }
475                if commit.author_name != self.edit_buffer {
476                    commit.author_name = self.edit_buffer.clone();
477                    commit.modifications.author_name_changed =
478                        commit.original.author_name != commit.author_name;
479                    commit.is_modified = true;
480                }
481            }
482            TableColumn::AuthorEmail => {
483                if self.edit_buffer.trim().is_empty() {
484                    return Err("Author email cannot be empty".into());
485                }
486                if !self.edit_buffer.contains('@') {
487                    return Err("Invalid email format".into());
488                }
489                if commit.author_email != self.edit_buffer {
490                    commit.author_email = self.edit_buffer.clone();
491                    commit.modifications.author_email_changed =
492                        commit.original.author_email != commit.author_email;
493                    commit.is_modified = true;
494                }
495            }
496            TableColumn::Timestamp => {
497                let new_timestamp =
498                    NaiveDateTime::parse_from_str(&self.edit_buffer, "%Y-%m-%d %H:%M:%S")
499                        .map_err(|_| "Invalid timestamp format (use YYYY-MM-DD HH:MM:SS)")?;
500
501                if commit.timestamp != new_timestamp {
502                    commit.timestamp = new_timestamp;
503                    commit.modifications.timestamp_changed =
504                        commit.original.timestamp != commit.timestamp;
505                    commit.is_modified = true;
506                }
507            }
508            TableColumn::Message => {
509                if self.edit_buffer.trim().is_empty() {
510                    return Err("Commit message cannot be empty".into());
511                }
512                if commit.message != self.edit_buffer {
513                    commit.message = self.edit_buffer.clone();
514                    commit.modifications.message_changed =
515                        commit.original.message != commit.message;
516                    commit.is_modified = true;
517                }
518            }
519            _ => {}
520        }
521        Ok(())
522    }
523
524    fn run(&mut self) -> Result<bool> {
525        let result = loop {
526            // Disable raw mode for drawing the table
527            let _ = terminal::disable_raw_mode();
528            self.draw_table();
529
530            // Enable raw mode only for reading input
531            terminal::enable_raw_mode()?;
532
533            if let Event::Key(KeyEvent {
534                code,
535                kind: KeyEventKind::Press,
536                ..
537            }) = event::read()?
538            {
539                let should_continue = if self.editing {
540                    match self.handle_edit_key_input(code) {
541                        Ok(cont) => cont,
542                        Err(_) => break Ok(false),
543                    }
544                } else {
545                    match self.handle_navigation_key_input(code) {
546                        Ok(cont) => cont,
547                        Err(_) => break Ok(false),
548                    }
549                };
550
551                if !should_continue {
552                    break Ok(true); // User wants to save
553                }
554            }
555        };
556
557        self.restore_terminal();
558        result
559    }
560
561    fn restore_terminal(&self) {
562        let _ = terminal::disable_raw_mode();
563        let _ = io::stdout().execute(Clear(ClearType::All));
564        let _ = io::stdout().execute(cursor::MoveTo(0, 0));
565    }
566
567    fn get_modified_commits(&self) -> Vec<&CommitEdit> {
568        self.commits.iter().filter(|c| c.is_modified).collect()
569    }
570}
571
572pub fn parse_range_input(input: &str, total_commits: usize) -> Result<(usize, usize)> {
573    let trimmed_input = input.trim();
574
575    // Check if user entered '*' to select all commits
576    if trimmed_input == "*" {
577        if total_commits == 0 {
578            return Err("No commits available to select".into());
579        }
580        return Ok((1, total_commits)); // Return 1-based indexing for all commits
581    }
582
583    let parts: Vec<&str> = trimmed_input.split('-').collect();
584
585    if parts.len() != 2 {
586        return Err("Invalid range format. Use format like '5-11' or '*' for all commits".into());
587    }
588
589    let start = parts[0]
590        .trim()
591        .parse::<usize>()
592        .map_err(|_| "Invalid start number in range")?;
593    let end = parts[1]
594        .trim()
595        .parse::<usize>()
596        .map_err(|_| "Invalid end number in range")?;
597
598    if start < 1 {
599        return Err("Start position must be 1 or greater".into());
600    }
601
602    if end < start {
603        return Err("End position must be greater than or equal to start position".into());
604    }
605
606    Ok((start, end))
607}
608
609pub fn select_commit_range(commits: &[CommitInfo]) -> Result<(usize, usize)> {
610    println!("\n{}", "Commit History:".bold().green());
611    println!("{}", "-".repeat(80).cyan());
612
613    for (i, commit) in commits.iter().enumerate() {
614        println!(
615            "{:3}. {} {} {} {}",
616            i + 1,
617            commit.short_hash.yellow().bold(),
618            commit
619                .timestamp
620                .format("%Y-%m-%d %H:%M:%S")
621                .to_string()
622                .blue(),
623            commit.author_name.magenta(),
624            commit.message.lines().next().unwrap_or("").white()
625        );
626    }
627
628    println!("{}", "-".repeat(80).cyan());
629    println!(
630        "\n{}",
631        "Enter range in format 'start-end' (e.g., '5-11') or '*' for all commits:"
632            .bold()
633            .green()
634    );
635    print!("{} ", "Range:".bold());
636    io::stdout().flush()?;
637
638    let mut input = String::new();
639    io::stdin().read_line(&mut input)?;
640
641    let (start, end) = parse_range_input(&input, commits.len())?;
642
643    if start > commits.len() || end > commits.len() {
644        return Err(format!(
645            "Range out of bounds. Available commits: 1-{}",
646            commits.len()
647        )
648        .into());
649    }
650
651    Ok((start - 1, end - 1)) // Convert to 0-based indexing
652}
653
654pub fn show_range_details(commits: &[CommitInfo], start_idx: usize, end_idx: usize) -> Result<()> {
655    let total_selected = end_idx - start_idx + 1;
656    let is_all_commits = total_selected == commits.len();
657
658    if is_all_commits {
659        println!("\n{}", "Selected All Commits for Editing:".bold().green());
660    } else {
661        println!("\n{}", "Selected Commit Range:".bold().green());
662    }
663    println!("{}", "=".repeat(80).cyan());
664
665    for (idx, commit) in commits[start_idx..=end_idx].iter().enumerate() {
666        println!(
667            "\n{}: {} ({})",
668            format!("Commit {}", start_idx + idx + 1).bold(),
669            commit.short_hash.yellow(),
670            &commit.oid.to_string()[..8]
671        );
672        println!(
673            "{}: {}",
674            "Author".bold(),
675            format!("{} <{}>", commit.author_name, commit.author_email).magenta()
676        );
677        println!(
678            "{}: {}",
679            "Date".bold(),
680            commit
681                .timestamp
682                .format("%Y-%m-%d %H:%M:%S")
683                .to_string()
684                .blue()
685        );
686        println!(
687            "{}: {}",
688            "Message".bold(),
689            commit.message.lines().next().unwrap_or("").white()
690        );
691    }
692
693    println!("\n{}", "=".repeat(80).cyan());
694    if is_all_commits {
695        println!(
696            "{} {} commits selected for editing {}",
697            "Total:".bold(),
698            total_selected.to_string().green(),
699            "(ALL COMMITS)".bold().yellow()
700        );
701    } else {
702        println!(
703            "{} {} commits selected for editing",
704            "Total:".bold(),
705            total_selected.to_string().green()
706        );
707    }
708
709    Ok(())
710}
711
712pub fn get_range_edit_info(args: &Args) -> Result<(String, String, NaiveDateTime, NaiveDateTime)> {
713    println!("\n{}", "Range Edit Configuration:".bold().green());
714
715    // Get author name
716    let author_name = if let Some(name) = &args.name {
717        name.clone()
718    } else {
719        print!("{} ", "New author name:".bold());
720        io::stdout().flush()?;
721        let mut input = String::new();
722        io::stdin().read_line(&mut input)?;
723        input.trim().to_string()
724    };
725
726    // Get author email
727    let author_email = if let Some(email) = &args.email {
728        email.clone()
729    } else {
730        print!("{} ", "New author email:".bold());
731        io::stdout().flush()?;
732        let mut input = String::new();
733        io::stdin().read_line(&mut input)?;
734        input.trim().to_string()
735    };
736
737    // Get start timestamp
738    let start_timestamp = if let Some(start) = &args.start {
739        NaiveDateTime::parse_from_str(start, "%Y-%m-%d %H:%M:%S")
740            .map_err(|_| "Invalid start timestamp format")?
741    } else {
742        print!("{} ", "Start timestamp (YYYY-MM-DD HH:MM:SS):".bold());
743        io::stdout().flush()?;
744        let mut input = String::new();
745        io::stdin().read_line(&mut input)?;
746        NaiveDateTime::parse_from_str(input.trim(), "%Y-%m-%d %H:%M:%S")
747            .map_err(|_| "Invalid start timestamp format")?
748    };
749
750    // Get end timestamp
751    let end_timestamp = if let Some(end) = &args.end {
752        NaiveDateTime::parse_from_str(end, "%Y-%m-%d %H:%M:%S")
753            .map_err(|_| "Invalid end timestamp format")?
754    } else {
755        print!("{} ", "End timestamp (YYYY-MM-DD HH:MM:SS):".bold());
756        io::stdout().flush()?;
757        let mut input = String::new();
758        io::stdin().read_line(&mut input)?;
759        NaiveDateTime::parse_from_str(input.trim(), "%Y-%m-%d %H:%M:%S")
760            .map_err(|_| "Invalid end timestamp format")?
761    };
762
763    if end_timestamp <= start_timestamp {
764        return Err("End timestamp must be after start timestamp".into());
765    }
766
767    Ok((author_name, author_email, start_timestamp, end_timestamp))
768}
769
770pub fn generate_range_timestamps(
771    start_time: NaiveDateTime,
772    end_time: NaiveDateTime,
773    count: usize,
774) -> Vec<NaiveDateTime> {
775    if count == 0 {
776        return vec![];
777    }
778
779    if count == 1 {
780        return vec![start_time];
781    }
782
783    let total_duration = end_time.signed_duration_since(start_time);
784    let step_duration = total_duration / (count - 1) as i32;
785
786    (0..count)
787        .map(|i| start_time + step_duration * i as i32)
788        .collect()
789}
790
791pub fn rewrite_range_commits(args: &Args) -> Result<()> {
792    let commits = get_commit_history(args, false)?;
793
794    if commits.is_empty() {
795        println!("{}", "No commits found!".red());
796        return Ok(());
797    }
798
799    let (start_idx, end_idx) = select_commit_range(&commits)?;
800
801    // Show range details for user feedback
802    show_range_details(&commits, start_idx, end_idx)?;
803
804    // Get editable fields based on command line flags
805    let editable_fields = args.get_editable_fields();
806
807    // Launch interactive table editor
808    let mut table = InteractiveTable::new(commits.clone(), start_idx, end_idx, editable_fields);
809    let should_save = table.run()?;
810
811    if !should_save {
812        println!("{}", "Operation cancelled.".yellow());
813        return Ok(());
814    }
815
816    let modified_commits = table.get_modified_commits();
817
818    if modified_commits.is_empty() {
819        println!("{}", "No changes made.".yellow());
820        return Ok(());
821    }
822
823    // Show summary of changes
824    println!("\n{}", "Summary of Changes:".bold().green());
825    println!("{}", "=".repeat(80).cyan());
826
827    for commit_edit in &modified_commits {
828        println!(
829            "\n{}: {} ({})",
830            format!("Commit {}", commit_edit.index + 1).bold(),
831            commit_edit.original.short_hash.yellow(),
832            &commit_edit.original.oid.to_string()[..8]
833        );
834
835        if commit_edit.modifications.author_name_changed {
836            println!(
837                "  {}: {} -> {}",
838                "Author Name".bold(),
839                commit_edit.original.author_name.red(),
840                commit_edit.author_name.green()
841            );
842        }
843
844        if commit_edit.modifications.author_email_changed {
845            println!(
846                "  {}: {} -> {}",
847                "Author Email".bold(),
848                commit_edit.original.author_email.red(),
849                commit_edit.author_email.green()
850            );
851        }
852
853        if commit_edit.modifications.timestamp_changed {
854            println!(
855                "  {}: {} -> {}",
856                "Timestamp".bold(),
857                commit_edit
858                    .original
859                    .timestamp
860                    .format("%Y-%m-%d %H:%M:%S")
861                    .to_string()
862                    .red(),
863                commit_edit
864                    .timestamp
865                    .format("%Y-%m-%d %H:%M:%S")
866                    .to_string()
867                    .green()
868            );
869        }
870
871        if commit_edit.modifications.message_changed {
872            let original_first_line = commit_edit.original.message.lines().next().unwrap_or("");
873            let new_first_line = commit_edit.message.lines().next().unwrap_or("");
874            println!(
875                "  {}: {} -> {}",
876                "Message".bold(),
877                original_first_line.red(),
878                new_first_line.green()
879            );
880        }
881    }
882
883    print!("\n{} (y/n): ", "Apply these changes?".bold());
884    io::stdout().flush()?;
885
886    let mut confirm = String::new();
887    io::stdin().read_line(&mut confirm)?;
888
889    if confirm.trim().to_lowercase() != "y" {
890        println!("{}", "Operation cancelled.".yellow());
891        return Ok(());
892    }
893
894    // Apply changes
895    apply_interactive_range_changes(args, &commits, &table.commits)?;
896
897    println!("\n{}", "✓ Commit range successfully edited!".green().bold());
898
899    if args.show_history {
900        get_commit_history(args, true)?;
901    }
902
903    Ok(())
904}
905
906fn apply_interactive_range_changes(
907    args: &Args,
908    _original_commits: &[CommitInfo],
909    edited_commits: &[CommitEdit],
910) -> Result<()> {
911    let repo = Repository::open(args.repo_path.as_ref().unwrap())?;
912    let head_ref = repo.head()?;
913    let branch_name = head_ref
914        .shorthand()
915        .ok_or("Detached HEAD or invalid branch")?;
916    let full_ref = format!("refs/heads/{branch_name}");
917
918    let mut revwalk = repo.revwalk()?;
919    revwalk.push_head()?;
920    revwalk.set_sorting(Sort::TOPOLOGICAL | Sort::TIME)?;
921    let mut orig_oids: Vec<_> = revwalk.filter_map(|id| id.ok()).collect();
922    orig_oids.reverse();
923
924    // Create a map for quick lookup of edited commits
925    let mut edit_map: HashMap<usize, &CommitEdit> = HashMap::new();
926    for commit_edit in edited_commits {
927        if commit_edit.is_modified {
928            edit_map.insert(commit_edit.index, commit_edit);
929        }
930    }
931
932    let mut new_map: HashMap<git2::Oid, git2::Oid> = HashMap::new();
933    let mut last_new_oid = None;
934
935    for (commit_idx, &oid) in orig_oids.iter().enumerate() {
936        let orig = repo.find_commit(oid)?;
937        let tree = orig.tree()?;
938
939        let new_parents: Result<Vec<_>> = orig
940            .parent_ids()
941            .map(|pid| {
942                let new_pid = *new_map.get(&pid).unwrap_or(&pid);
943                repo.find_commit(new_pid).map_err(|e| e.into())
944            })
945            .collect();
946
947        let new_oid = if let Some(commit_edit) = edit_map.get(&commit_idx) {
948            // This commit has been edited - apply changes
949            let author_sig = Signature::new(
950                &commit_edit.author_name,
951                &commit_edit.author_email,
952                &Time::new(commit_edit.timestamp.and_utc().timestamp(), 0),
953            )?;
954
955            let committer_sig = Signature::new(
956                &commit_edit.author_name,
957                &commit_edit.author_email,
958                &Time::new(commit_edit.timestamp.and_utc().timestamp(), 0),
959            )?;
960
961            // Use the edited message or keep the original if not changed
962            let message = if commit_edit.modifications.message_changed {
963                &commit_edit.message
964            } else {
965                orig.message().unwrap_or_default()
966            };
967
968            repo.commit(
969                None,
970                &author_sig,
971                &committer_sig,
972                message,
973                &tree,
974                &new_parents?.iter().collect::<Vec<_>>(),
975            )?
976        } else {
977            // Keep other commits as-is but update parent references
978            let author = orig.author();
979            let committer = orig.committer();
980
981            repo.commit(
982                None,
983                &author,
984                &committer,
985                orig.message().unwrap_or_default(),
986                &tree,
987                &new_parents?.iter().collect::<Vec<_>>(),
988            )?
989        };
990
991        new_map.insert(oid, new_oid);
992        last_new_oid = Some(new_oid);
993    }
994
995    if let Some(new_head) = last_new_oid {
996        repo.reference(
997            &full_ref,
998            new_head,
999            true,
1000            "edited commit range interactively",
1001        )?;
1002        println!(
1003            "{} '{}' -> {}",
1004            "Updated branch".green(),
1005            branch_name.cyan(),
1006            new_head.to_string()[..8].to_string().cyan()
1007        );
1008    }
1009
1010    Ok(())
1011}
1012
1013#[cfg(test)]
1014mod tests {
1015    use super::*;
1016    use std::fs;
1017    use tempfile::TempDir;
1018
1019    fn create_test_repo_with_commits() -> (TempDir, String) {
1020        let temp_dir = TempDir::new().unwrap();
1021        let repo_path = temp_dir.path().to_str().unwrap().to_string();
1022
1023        // Initialize git repo
1024        let repo = git2::Repository::init(&repo_path).unwrap();
1025
1026        // Create multiple commits
1027        for i in 1..=5 {
1028            let file_path = temp_dir.path().join(format!("test{i}.txt"));
1029            fs::write(&file_path, format!("test content {i}")).unwrap();
1030
1031            let mut index = repo.index().unwrap();
1032            index
1033                .add_path(std::path::Path::new(&format!("test{i}.txt")))
1034                .unwrap();
1035            index.write().unwrap();
1036
1037            let tree_id = index.write_tree().unwrap();
1038            let tree = repo.find_tree(tree_id).unwrap();
1039
1040            let sig = git2::Signature::new(
1041                "Test User",
1042                "test@example.com",
1043                &git2::Time::new(1234567890 + i as i64 * 3600, 0),
1044            )
1045            .unwrap();
1046
1047            let parents = if i == 1 {
1048                vec![]
1049            } else {
1050                let head = repo.head().unwrap();
1051                let parent_commit = head.peel_to_commit().unwrap();
1052                vec![parent_commit]
1053            };
1054
1055            repo.commit(
1056                Some("HEAD"),
1057                &sig,
1058                &sig,
1059                &format!("Commit {i}"),
1060                &tree,
1061                &parents.iter().collect::<Vec<_>>(),
1062            )
1063            .unwrap();
1064        }
1065
1066        (temp_dir, repo_path)
1067    }
1068
1069    #[test]
1070    fn test_parse_range_input_valid() {
1071        let result = parse_range_input("5-11", 20);
1072        assert!(result.is_ok());
1073        let (start, end) = result.unwrap();
1074        assert_eq!(start, 5);
1075        assert_eq!(end, 11);
1076    }
1077
1078    #[test]
1079    fn test_parse_range_input_with_spaces() {
1080        let result = parse_range_input(" 3 - 8 ", 20);
1081        assert!(result.is_ok());
1082        let (start, end) = result.unwrap();
1083        assert_eq!(start, 3);
1084        assert_eq!(end, 8);
1085    }
1086
1087    #[test]
1088    fn test_parse_range_input_asterisk() {
1089        let result = parse_range_input("*", 10);
1090        assert!(result.is_ok());
1091        let (start, end) = result.unwrap();
1092        assert_eq!(start, 1);
1093        assert_eq!(end, 10);
1094
1095        // Test with empty repository
1096        let result = parse_range_input("*", 0);
1097        assert!(result.is_err());
1098    }
1099
1100    #[test]
1101    fn test_parse_range_input_invalid_format() {
1102        let result = parse_range_input("5", 20);
1103        assert!(result.is_err());
1104
1105        let result = parse_range_input("5-11-15", 20);
1106        assert!(result.is_err());
1107
1108        let result = parse_range_input("abc-def", 20);
1109        assert!(result.is_err());
1110    }
1111
1112    #[test]
1113    fn test_parse_range_input_invalid_range() {
1114        let result = parse_range_input("11-5", 20);
1115        assert!(result.is_err());
1116
1117        let result = parse_range_input("0-5", 20);
1118        assert!(result.is_err());
1119    }
1120
1121    #[test]
1122    fn test_generate_range_timestamps() {
1123        let start =
1124            NaiveDateTime::parse_from_str("2023-01-01 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
1125        let end =
1126            NaiveDateTime::parse_from_str("2023-01-01 10:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
1127
1128        let timestamps = generate_range_timestamps(start, end, 5);
1129
1130        assert_eq!(timestamps.len(), 5);
1131        assert_eq!(timestamps[0], start);
1132        assert_eq!(timestamps[4], end);
1133
1134        // Check that timestamps are evenly distributed
1135        for i in 1..timestamps.len() {
1136            assert!(timestamps[i] >= timestamps[i - 1]);
1137        }
1138    }
1139
1140    #[test]
1141    fn test_generate_range_timestamps_edge_cases() {
1142        let start =
1143            NaiveDateTime::parse_from_str("2023-01-01 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
1144        let end =
1145            NaiveDateTime::parse_from_str("2023-01-01 10:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
1146
1147        // Zero count
1148        let timestamps = generate_range_timestamps(start, end, 0);
1149        assert_eq!(timestamps.len(), 0);
1150
1151        // Single timestamp
1152        let timestamps = generate_range_timestamps(start, end, 1);
1153        assert_eq!(timestamps.len(), 1);
1154        assert_eq!(timestamps[0], start);
1155    }
1156
1157    #[test]
1158    fn test_rewrite_range_commits_with_repo() {
1159        let (_temp_dir, repo_path) = create_test_repo_with_commits();
1160        let args = Args {
1161            repo_path: Some(repo_path),
1162            email: Some("new@example.com".to_string()),
1163            name: Some("New User".to_string()),
1164            start: Some("2023-01-01 00:00:00".to_string()),
1165            end: Some("2023-01-01 10:00:00".to_string()),
1166            show_history: false,
1167            pick_specific_commits: false,
1168            range: false,
1169            simulate: false,
1170            show_diff: false,
1171            edit_message: false,
1172            edit_author: false,
1173            edit_time: false,
1174        };
1175
1176        // Test that get_commit_history returns commits for this repo
1177        let commits = get_commit_history(&args, false).unwrap();
1178        assert_eq!(commits.len(), 5);
1179
1180        // Test range validation
1181        let (start, end) = (0, 2); // 0-based indexing
1182        assert!(start <= end);
1183        assert!(end < commits.len());
1184
1185        // Test timestamp generation
1186        let start_time =
1187            NaiveDateTime::parse_from_str("2023-01-01 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
1188        let end_time =
1189            NaiveDateTime::parse_from_str("2023-01-01 10:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
1190        let timestamps = generate_range_timestamps(start_time, end_time, 3);
1191        assert_eq!(timestamps.len(), 3);
1192    }
1193}