cascade_cli/cli/commands/
entry.rs

1use crate::cli::output::Output;
2use crate::errors::{CascadeError, Result};
3use crate::git::{find_repository_root, GitRepository};
4use crate::stack::{StackEntry, StackManager};
5use clap::Subcommand;
6use crossterm::{
7    event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
8    execute,
9    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
10};
11use dialoguer::{theme::ColorfulTheme, Confirm};
12use ratatui::{
13    backend::CrosstermBackend,
14    layout::{Alignment, Constraint, Direction, Layout},
15    style::{Color, Modifier, Style},
16    text::{Line, Span},
17    widgets::{Block, Borders, List, ListItem, ListState, Paragraph},
18    Terminal,
19};
20use std::env;
21use std::io;
22use std::path::Path;
23use tracing::debug;
24
25#[derive(Debug, Subcommand)]
26pub enum EntryAction {
27    /// Interactively checkout a stack entry for editing
28    Checkout {
29        /// Stack entry number (optional, shows picker if not provided)
30        entry: Option<usize>,
31        /// Skip interactive picker and use entry number directly
32        #[arg(long)]
33        direct: bool,
34        /// Skip confirmation prompts
35        #[arg(long, short)]
36        yes: bool,
37    },
38    /// Show current edit mode status
39    Status {
40        /// Show brief status only
41        #[arg(long)]
42        quiet: bool,
43    },
44    /// List all entries with their edit status
45    List {
46        /// Show detailed information
47        #[arg(long, short)]
48        verbose: bool,
49    },
50    /// Clear/exit edit mode (useful for recovering from corrupted state)
51    Clear {
52        /// Skip confirmation prompt
53        #[arg(long, short)]
54        yes: bool,
55    },
56    /// Amend the current stack entry commit and automatically restack dependent entries
57    ///
58    /// Automatically includes all modified tracked files (like 'git commit -a --amend')
59    /// and rebases all dependent entries onto the amended commit
60    Amend {
61        /// New commit message (optional, uses git editor if not provided)
62        #[arg(long, short)]
63        message: Option<String>,
64        /// (Deprecated: now default behavior) Include all changes
65        #[arg(long, short)]
66        all: bool,
67        /// Automatically force-push after amending (if PR exists)
68        #[arg(long)]
69        push: bool,
70    },
71    /// Continue restacking after resolving cherry-pick conflicts
72    ///
73    /// Use this after manually resolving conflicts during 'ca entry amend'
74    Continue,
75    /// Abort an in-progress restack operation
76    ///
77    /// Safely aborts the cherry-pick and cleans up any partial restack state
78    Abort,
79}
80
81pub async fn run(action: EntryAction) -> Result<()> {
82    let _current_dir = env::current_dir()
83        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
84
85    match action {
86        EntryAction::Checkout { entry, direct, yes } => checkout_entry(entry, direct, yes).await,
87        EntryAction::Status { quiet } => show_edit_status(quiet).await,
88        EntryAction::List { verbose } => list_entries(verbose).await,
89        EntryAction::Clear { yes } => clear_edit_mode(yes).await,
90        EntryAction::Amend { message, all, push } => amend_entry(message, all, push).await,
91        EntryAction::Continue => continue_restack().await,
92        EntryAction::Abort => abort_restack().await,
93    }
94}
95
96/// Checkout a specific stack entry for editing
97async fn checkout_entry(
98    entry_num: Option<usize>,
99    direct: bool,
100    skip_confirmation: bool,
101) -> Result<()> {
102    let current_dir = env::current_dir()
103        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
104
105    let repo_root = find_repository_root(&current_dir)
106        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
107
108    let mut manager = StackManager::new(&repo_root)?;
109
110    // Get active stack
111    let active_stack = manager.get_active_stack().ok_or_else(|| {
112        CascadeError::config("No active stack. Create a stack first with 'ca stack create'")
113    })?;
114
115    if active_stack.entries.is_empty() {
116        return Err(CascadeError::config(
117            "Stack is empty. Push some commits first with 'ca stack push'",
118        ));
119    }
120
121    // Determine which entry to checkout
122    let target_entry_num = if let Some(num) = entry_num {
123        if num == 0 || num > active_stack.entries.len() {
124            return Err(CascadeError::config(format!(
125                "Invalid entry number: {}. Stack has {} entries",
126                num,
127                active_stack.entries.len()
128            )));
129        }
130        num
131    } else if direct {
132        return Err(CascadeError::config(
133            "Entry number required when using --direct flag",
134        ));
135    } else {
136        // Show interactive picker
137        show_entry_picker(active_stack).await?
138    };
139
140    let target_entry = &active_stack.entries[target_entry_num - 1]; // Convert to 0-based index
141
142    // Clone the values we need before borrowing manager mutably
143    let stack_id = active_stack.id;
144    let entry_id = target_entry.id;
145    let entry_branch = target_entry.branch.clone();
146    let entry_short_hash = target_entry.short_hash();
147    let entry_short_message = target_entry.short_message(50);
148    let entry_pr_id = target_entry.pull_request_id.clone();
149    let entry_message = target_entry.message.clone();
150
151    // Check if already in edit mode and get info before confirmation
152    let already_in_edit_mode = manager.is_in_edit_mode();
153    let edit_mode_display = if already_in_edit_mode {
154        let edit_info = manager.get_edit_mode_info().unwrap();
155
156        // Get the commit message for the current edit target
157        let commit_message = if let Some(target_entry_id) = &edit_info.target_entry_id {
158            if let Some(entry) = active_stack
159                .entries
160                .iter()
161                .find(|e| e.id == *target_entry_id)
162            {
163                entry.short_message(50)
164            } else {
165                "Unknown entry".to_string()
166            }
167        } else {
168            "Unknown target".to_string()
169        };
170
171        Some((edit_info.original_commit_hash.clone(), commit_message))
172    } else {
173        None
174    };
175
176    // Let the active_stack reference go out of scope before we potentially mutably borrow manager
177    let _ = active_stack;
178
179    // Handle edit mode exit if needed
180    if let Some((commit_hash, commit_message)) = edit_mode_display {
181        tracing::debug!("Already in edit mode for entry in stack");
182
183        if !skip_confirmation {
184            Output::warning("Already in edit mode!");
185            Output::sub_item(format!(
186                "Current target: {} ({})",
187                &commit_hash[..8],
188                commit_message
189            ));
190
191            // Interactive confirmation to exit current edit mode
192            let should_exit_edit_mode = Confirm::with_theme(&ColorfulTheme::default())
193                .with_prompt("Exit current edit mode and start a new one?")
194                .default(false)
195                .interact()
196                .map_err(|e| {
197                    CascadeError::config(format!("Failed to get user confirmation: {e}"))
198                })?;
199
200            if !should_exit_edit_mode {
201                return Err(CascadeError::config(
202                    "Operation cancelled. Use 'ca entry status' to see current edit mode details.",
203                ));
204            }
205
206            // Exit current edit mode before starting a new one
207            Output::info("Exiting current edit mode...");
208            manager.exit_edit_mode()?;
209            Output::success("✓ Exited previous edit mode");
210        }
211    }
212
213    // Confirmation prompt
214    if !skip_confirmation {
215        Output::section("Checking out entry for editing");
216        Output::sub_item(format!(
217            "Entry #{target_entry_num}: {entry_short_hash} ({entry_short_message})"
218        ));
219        Output::sub_item(format!("Branch: {entry_branch}"));
220        if let Some(pr_id) = &entry_pr_id {
221            Output::sub_item(format!("PR: #{pr_id}"));
222        }
223
224        // Display full commit message
225        Output::sub_item("Commit Message:");
226        let lines: Vec<&str> = entry_message.lines().collect();
227        for line in lines {
228            Output::sub_item(format!("  {line}"));
229        }
230
231        Output::warning("This will checkout the commit and enter edit mode.");
232        Output::info("Any changes you make can be amended to this commit or create new entries.");
233
234        // Interactive confirmation to proceed with checkout
235        let should_continue = Confirm::with_theme(&ColorfulTheme::default())
236            .with_prompt("Continue with checkout?")
237            .default(false)
238            .interact()
239            .map_err(|e| CascadeError::config(format!("Failed to get user confirmation: {e}")))?;
240
241        if !should_continue {
242            return Err(CascadeError::config("Entry checkout cancelled"));
243        }
244    }
245
246    // Enter edit mode
247    manager.enter_edit_mode(stack_id, entry_id)?;
248
249    // Checkout the branch (not the commit - we want to stay on the branch)
250    let current_dir = env::current_dir()
251        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
252
253    let repo_root = find_repository_root(&current_dir)
254        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
255    let repo = crate::git::GitRepository::open(&repo_root)?;
256
257    debug!("Checking out branch: {}", entry_branch);
258    repo.checkout_branch(&entry_branch)?;
259
260    Output::success(format!("Entered edit mode for entry #{target_entry_num}"));
261    Output::sub_item(format!(
262        "You are now on commit: {} ({})",
263        entry_short_hash, entry_short_message
264    ));
265    Output::sub_item(format!("Branch: {entry_branch}"));
266
267    Output::section("Make your changes and commit normally");
268    Output::bullet("Use 'ca entry status' to see edit mode info");
269    Output::bullet("When you commit, the pre-commit hook will guide you");
270
271    // Check if prepare-commit-msg hook is installed
272    let hooks_dir = repo_root.join(".git/hooks");
273    let hook_path = hooks_dir.join("prepare-commit-msg");
274    if !hook_path.exists() {
275        Output::tip("Install the prepare-commit-msg hook for better guidance:");
276        Output::sub_item("ca hooks add prepare-commit-msg");
277    }
278
279    Ok(())
280}
281
282/// Interactive entry picker using TUI
283async fn show_entry_picker(stack: &crate::stack::Stack) -> Result<usize> {
284    // Setup terminal
285    enable_raw_mode()?;
286    let mut stdout = io::stdout();
287    execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
288    let backend = CrosstermBackend::new(stdout);
289    let mut terminal = Terminal::new(backend)?;
290
291    let mut list_state = ListState::default();
292    list_state.select(Some(0));
293
294    let result = loop {
295        terminal.draw(|f| {
296            let size = f.area();
297
298            // Create layout
299            let chunks = Layout::default()
300                .direction(Direction::Vertical)
301                .margin(2)
302                .constraints(
303                    [
304                        Constraint::Length(3), // Title
305                        Constraint::Min(5),    // List
306                        Constraint::Length(3), // Help
307                    ]
308                    .as_ref(),
309                )
310                .split(size);
311
312            // Title
313            let title = Paragraph::new(format!("📚 Select Entry from Stack: {}", stack.name))
314                .style(
315                    Style::default()
316                        .fg(Color::Cyan)
317                        .add_modifier(Modifier::BOLD),
318                )
319                .alignment(Alignment::Center)
320                .block(Block::default().borders(Borders::ALL));
321            f.render_widget(title, chunks[0]);
322
323            // Entry list
324            let items: Vec<ListItem> = stack
325                .entries
326                .iter()
327                .enumerate()
328                .map(|(i, entry)| {
329                    let status_icon = if entry.is_submitted {
330                        if entry.pull_request_id.is_some() {
331                            "📤"
332                        } else {
333                            "📝"
334                        }
335                    } else {
336                        "🔄"
337                    };
338
339                    let pr_text = if let Some(pr_id) = &entry.pull_request_id {
340                        format!(" PR: #{pr_id}")
341                    } else {
342                        "".to_string()
343                    };
344
345                    let line = Line::from(vec![
346                        Span::raw(format!("  {}. ", i + 1)),
347                        Span::raw(status_icon),
348                        Span::raw(" "),
349                        Span::styled(entry.short_message(40), Style::default().fg(Color::White)),
350                        Span::raw(" "),
351                        Span::styled(
352                            format!("({})", entry.short_hash()),
353                            Style::default().fg(Color::Yellow),
354                        ),
355                        Span::styled(pr_text, Style::default().fg(Color::Green)),
356                    ]);
357
358                    ListItem::new(line)
359                })
360                .collect();
361
362            let list = List::new(items)
363                .block(Block::default().borders(Borders::ALL).title("Entries"))
364                .highlight_style(Style::default().fg(Color::Black).bg(Color::Cyan))
365                .highlight_symbol("→ ");
366
367            f.render_stateful_widget(list, chunks[1], &mut list_state);
368
369            // Help text
370            let help = Paragraph::new("↑/↓: Navigate • Enter: Select • q: Quit • r: Refresh")
371                .style(Style::default().fg(Color::DarkGray))
372                .alignment(Alignment::Center)
373                .block(Block::default().borders(Borders::ALL));
374            f.render_widget(help, chunks[2]);
375        })?;
376
377        // Handle input
378        if let Event::Key(key) = event::read()? {
379            if key.kind == KeyEventKind::Press {
380                match key.code {
381                    KeyCode::Char('q') => {
382                        break Err(CascadeError::config("Entry selection cancelled"));
383                    }
384                    KeyCode::Up => {
385                        let selected = list_state.selected().unwrap_or(0);
386                        if selected > 0 {
387                            list_state.select(Some(selected - 1));
388                        } else {
389                            list_state.select(Some(stack.entries.len() - 1));
390                        }
391                    }
392                    KeyCode::Down => {
393                        let selected = list_state.selected().unwrap_or(0);
394                        if selected < stack.entries.len() - 1 {
395                            list_state.select(Some(selected + 1));
396                        } else {
397                            list_state.select(Some(0));
398                        }
399                    }
400                    KeyCode::Enter => {
401                        let selected = list_state.selected().unwrap_or(0);
402                        break Ok(selected + 1); // Convert to 1-based index
403                    }
404                    KeyCode::Char('r') => {
405                        // Refresh - for now just continue the loop
406                        continue;
407                    }
408                    _ => {}
409                }
410            }
411        }
412    };
413
414    // Restore terminal
415    disable_raw_mode()?;
416    execute!(
417        terminal.backend_mut(),
418        LeaveAlternateScreen,
419        DisableMouseCapture
420    )?;
421    terminal.show_cursor()?;
422
423    result
424}
425
426/// Show current edit mode status
427async fn show_edit_status(quiet: bool) -> Result<()> {
428    let current_dir = env::current_dir()
429        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
430
431    let repo_root = find_repository_root(&current_dir)
432        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
433    let manager = StackManager::new(&repo_root)?;
434
435    if !manager.is_in_edit_mode() {
436        if quiet {
437            println!("inactive");
438        } else {
439            Output::info("Not in edit mode");
440            Output::sub_item("Use 'ca entry checkout' to start editing a stack entry");
441        }
442        return Ok(());
443    }
444
445    let edit_info = manager.get_edit_mode_info().unwrap();
446
447    if quiet {
448        println!("active:{:?}", edit_info.target_entry_id);
449        return Ok(());
450    }
451
452    Output::section("Currently in edit mode");
453
454    // Try to get the entry information
455    if let Some(active_stack) = manager.get_active_stack() {
456        if let Some(target_entry_id) = edit_info.target_entry_id {
457            if let Some(entry) = active_stack
458                .entries
459                .iter()
460                .find(|e| e.id == target_entry_id)
461            {
462                Output::sub_item(format!(
463                    "Target entry: {} ({})",
464                    entry.short_hash(),
465                    entry.short_message(50)
466                ));
467                Output::sub_item(format!("Branch: {}", entry.branch));
468
469                // Display full commit message
470                Output::sub_item("Commit Message:");
471                let lines: Vec<&str> = entry.message.lines().collect();
472                for line in lines {
473                    Output::sub_item(format!("  {line}"));
474                }
475            } else {
476                Output::sub_item(format!("Target entry: {target_entry_id:?} (not found)"));
477            }
478        } else {
479            Output::sub_item("Target entry: Unknown");
480        }
481    } else {
482        Output::sub_item(format!("Target entry: {:?}", edit_info.target_entry_id));
483    }
484
485    Output::sub_item(format!(
486        "Original commit: {}",
487        &edit_info.original_commit_hash[..8]
488    ));
489    Output::sub_item(format!(
490        "Started: {}",
491        edit_info.started_at.format("%Y-%m-%d %H:%M:%S")
492    ));
493
494    // Show current Git status
495    Output::section("Current state");
496
497    // Get current repository state
498    let current_dir = env::current_dir()
499        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
500    let repo_root = find_repository_root(&current_dir)
501        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
502    let repo = crate::git::GitRepository::open(&repo_root)?;
503
504    // Current HEAD vs original commit
505    let current_head = repo.get_current_commit_hash()?;
506    if current_head != edit_info.original_commit_hash {
507        let current_short = &current_head[..8];
508        let original_short = &edit_info.original_commit_hash[..8];
509        Output::sub_item(format!("HEAD moved: {original_short} → {current_short}"));
510
511        // Show if there are new commits
512        match repo.get_commit_count_between(&edit_info.original_commit_hash, &current_head) {
513            Ok(count) if count > 0 => {
514                Output::sub_item(format!("  {count} new commit(s) created"));
515            }
516            _ => {}
517        }
518    } else {
519        Output::sub_item(format!("HEAD: {} (unchanged)", &current_head[..8]));
520    }
521
522    // Working directory and staging status
523    match repo.get_status_summary() {
524        Ok(status) => {
525            if status.is_clean() {
526                Output::sub_item("Working directory: clean");
527            } else {
528                if status.has_staged_changes() {
529                    Output::sub_item(format!("Staged changes: {} files", status.staged_count()));
530                }
531                if status.has_unstaged_changes() {
532                    Output::sub_item(format!(
533                        "Unstaged changes: {} files",
534                        status.unstaged_count()
535                    ));
536                }
537                if status.has_untracked_files() {
538                    Output::sub_item(format!(
539                        "Untracked files: {} files",
540                        status.untracked_count()
541                    ));
542                }
543            }
544        }
545        Err(_) => {
546            Output::sub_item("Working directory: status unavailable");
547        }
548    }
549
550    Output::tip("Use 'git status' for detailed file-level status");
551    Output::sub_item("Use 'ca entry list' to see all entries");
552
553    Ok(())
554}
555
556/// List all entries in the stack with edit status
557async fn list_entries(verbose: bool) -> Result<()> {
558    let current_dir = env::current_dir()
559        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
560
561    let repo_root = find_repository_root(&current_dir)
562        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
563    let manager = StackManager::new(&repo_root)?;
564
565    let active_stack = manager.get_active_stack().ok_or_else(|| {
566        CascadeError::config(
567            "No active stack. Create a stack first with 'ca stack create'".to_string(),
568        )
569    })?;
570
571    if active_stack.entries.is_empty() {
572        Output::info(format!(
573            "Active stack '{}' has no entries yet",
574            active_stack.name
575        ));
576        Output::sub_item("Add some commits to the stack with 'ca stack push'");
577        return Ok(());
578    }
579
580    Output::section(format!(
581        "Stack: {} ({} entries)",
582        active_stack.name,
583        active_stack.entries.len()
584    ));
585
586    let edit_mode_info = manager.get_edit_mode_info();
587    let edit_target_entry_id = edit_mode_info
588        .as_ref()
589        .and_then(|info| info.target_entry_id);
590
591    for (i, entry) in active_stack.entries.iter().enumerate() {
592        let entry_num = i + 1;
593        let status_label = Output::entry_status(entry.is_submitted, entry.is_merged);
594        let mut entry_line = format!(
595            "{} {} ({})",
596            status_label,
597            entry.short_message(50),
598            entry.short_hash()
599        );
600
601        if let Some(pr_id) = &entry.pull_request_id {
602            entry_line.push_str(&format!(" PR: #{pr_id}"));
603        }
604
605        if Some(entry.id) == edit_target_entry_id {
606            entry_line.push_str(" [edit target]");
607        }
608
609        Output::numbered_item(entry_num, entry_line);
610
611        if verbose {
612            Output::sub_item(format!("Branch: {}", entry.branch));
613            Output::sub_item(format!("Commit: {}", entry.commit_hash));
614            Output::sub_item(format!(
615                "Created: {}",
616                entry.created_at.format("%Y-%m-%d %H:%M:%S")
617            ));
618
619            if entry.is_merged {
620                Output::sub_item("Status: Merged");
621            } else if entry.is_submitted {
622                Output::sub_item("Status: Submitted");
623            } else {
624                Output::sub_item("Status: Draft");
625            }
626
627            Output::sub_item("Message:");
628            for line in entry.message.lines() {
629                Output::sub_item(format!("  {line}"));
630            }
631
632            if Some(entry.id) == edit_target_entry_id {
633                Output::sub_item("Edit mode target");
634
635                match crate::git::GitRepository::open(&repo_root) {
636                    Ok(repo) => match repo.get_status_summary() {
637                        Ok(status) => {
638                            if !status.is_clean() {
639                                Output::sub_item("Git Status:");
640                                if status.has_staged_changes() {
641                                    Output::sub_item(format!(
642                                        "  Staged: {} files",
643                                        status.staged_count()
644                                    ));
645                                }
646                                if status.has_unstaged_changes() {
647                                    Output::sub_item(format!(
648                                        "  Unstaged: {} files",
649                                        status.unstaged_count()
650                                    ));
651                                }
652                                if status.has_untracked_files() {
653                                    Output::sub_item(format!(
654                                        "  Untracked: {} files",
655                                        status.untracked_count()
656                                    ));
657                                }
658                            } else {
659                                Output::sub_item("Git Status: clean");
660                            }
661                        }
662                        Err(_) => {
663                            Output::sub_item("Git Status: unavailable");
664                        }
665                    },
666                    Err(_) => {
667                        Output::sub_item("Git Status: unavailable");
668                    }
669                }
670            }
671        }
672    }
673
674    if edit_mode_info.is_some() {
675        Output::spacing();
676        Output::info("Edit mode active - use 'ca entry status' for details");
677    } else {
678        Output::spacing();
679        Output::tip("Use 'ca entry checkout' to start editing an entry");
680    }
681
682    Ok(())
683}
684
685/// Clear/exit edit mode (useful for recovering from corrupted state)
686async fn clear_edit_mode(skip_confirmation: bool) -> Result<()> {
687    let current_dir = env::current_dir()
688        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
689
690    let repo_root = find_repository_root(&current_dir)
691        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
692
693    let mut manager = StackManager::new(&repo_root)?;
694
695    if !manager.is_in_edit_mode() {
696        Output::info("Not currently in edit mode");
697        return Ok(());
698    }
699
700    // Show current edit mode info
701    if let Some(edit_info) = manager.get_edit_mode_info() {
702        Output::section("Current edit mode state");
703
704        if let Some(target_entry_id) = &edit_info.target_entry_id {
705            Output::sub_item(format!("Target entry: {}", target_entry_id));
706
707            // Try to find the entry
708            if let Some(active_stack) = manager.get_active_stack() {
709                if let Some(entry) = active_stack
710                    .entries
711                    .iter()
712                    .find(|e| e.id == *target_entry_id)
713                {
714                    Output::sub_item(format!("Entry: {}", entry.short_message(50)));
715                } else {
716                    Output::warning("Target entry not found in stack (corrupted state)");
717                }
718            }
719        }
720
721        Output::sub_item(format!(
722            "Original commit: {}",
723            &edit_info.original_commit_hash[..8]
724        ));
725        Output::sub_item(format!(
726            "Started: {}",
727            edit_info.started_at.format("%Y-%m-%d %H:%M:%S")
728        ));
729    }
730
731    // Confirm before clearing
732    if !skip_confirmation {
733        println!();
734        let confirmed = Confirm::with_theme(&ColorfulTheme::default())
735            .with_prompt("Clear edit mode state?")
736            .default(true)
737            .interact()
738            .map_err(|e| CascadeError::config(format!("Failed to get user confirmation: {e}")))?;
739
740        if !confirmed {
741            return Err(CascadeError::config("Operation cancelled."));
742        }
743    }
744
745    // Clear edit mode
746    manager.exit_edit_mode()?;
747
748    Output::success("Edit mode cleared");
749    Output::tip("Use 'ca entry checkout' to start a new edit session");
750
751    Ok(())
752}
753
754/// Amend the current stack entry commit and update working branch
755async fn amend_entry(message: Option<String>, _all: bool, push: bool) -> Result<()> {
756    let current_dir = env::current_dir()
757        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
758
759    let repo_root = find_repository_root(&current_dir)
760        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
761
762    let mut manager = StackManager::new(&repo_root)?;
763    let repo = crate::git::GitRepository::open(&repo_root)?;
764
765    let current_branch = repo.get_current_branch()?;
766
767    // Get active stack info we need (clone to avoid borrow issues)
768    let (stack_id, entry_index, entry_id, entry_branch, working_branch, has_dependents, has_pr) = {
769        let active_stack = manager.get_active_stack().ok_or_else(|| {
770            CascadeError::config("No active stack. Create a stack first with 'ca stack create'")
771        })?;
772
773        // Find which entry we're amending (must be on a stack branch)
774        let mut found_entry = None;
775
776        for (idx, entry) in active_stack.entries.iter().enumerate() {
777            if entry.branch == current_branch {
778                found_entry = Some((
779                    idx,
780                    entry.id,
781                    entry.branch.clone(),
782                    entry.pull_request_id.clone(),
783                ));
784                break;
785            }
786        }
787
788        match found_entry {
789            Some((idx, id, branch, pr_id)) => {
790                let has_dependents = active_stack
791                    .entries
792                    .iter()
793                    .skip(idx + 1)
794                    .any(|entry| !entry.is_merged);
795                (
796                    active_stack.id,
797                    idx,
798                    id,
799                    branch,
800                    active_stack.working_branch.clone(),
801                    has_dependents,
802                    pr_id.is_some(),
803                )
804            }
805            None => {
806                return Err(CascadeError::config(format!(
807                    "Current branch '{}' is not a stack entry branch.\n\
808                     Use 'ca entry checkout <N>' to checkout a stack entry first.",
809                    current_branch
810                )));
811            }
812        }
813    };
814
815    Output::section(format!("Amending stack entry #{}", entry_index + 1));
816
817    // 1. Perform the git commit --amend
818    // Always auto-stage changes (like 'git commit -a --amend')
819    // This matches user expectations: "amend my changes" should include all working changes
820    let mut amend_args = vec!["commit", "-a", "--amend"];
821
822    if let Some(ref msg) = message {
823        amend_args.push("-m");
824        amend_args.push(msg);
825    } else {
826        // Use git editor for interactive message editing
827        amend_args.push("--no-edit");
828    }
829
830    debug!("Running git {}", amend_args.join(" "));
831
832    // Set environment variable to bypass pre-commit hook (avoid infinite loop)
833    let output = std::process::Command::new("git")
834        .args(&amend_args)
835        .env("CASCADE_SKIP_HOOKS", "1")
836        .current_dir(&repo_root)
837        .stdout(std::process::Stdio::null()) // Suppress Git's output
838        .stderr(std::process::Stdio::piped()) // Capture errors
839        .output()
840        .map_err(CascadeError::Io)?;
841
842    if !output.status.success() {
843        let stderr = String::from_utf8_lossy(&output.stderr);
844        return Err(CascadeError::branch(format!(
845            "Failed to amend commit: {}",
846            stderr.trim()
847        )));
848    }
849
850    Output::success("Commit amended");
851
852    // 2. Get the new commit hash
853    let new_commit_hash = repo.get_head_commit()?.id().to_string();
854    debug!("New commit hash after amend: {}", new_commit_hash);
855
856    // 3. Update stack metadata with new commit hash using safe wrapper
857    {
858        let stack = manager
859            .get_stack_mut(&stack_id)
860            .ok_or_else(|| CascadeError::config("Stack not found"))?;
861
862        let old_hash = stack
863            .entries
864            .iter()
865            .find(|e| e.id == entry_id)
866            .map(|e| e.commit_hash.clone())
867            .ok_or_else(|| CascadeError::config("Entry not found"))?;
868
869        stack
870            .update_entry_commit_hash(&entry_id, new_commit_hash.clone())
871            .map_err(CascadeError::config)?;
872
873        debug!(
874            "Updated entry commit hash: {} -> {}",
875            &old_hash[..8],
876            &new_commit_hash[..8]
877        );
878        Output::sub_item(format!(
879            "Updated metadata: {} → {}",
880            &old_hash[..8],
881            &new_commit_hash[..8]
882        ));
883    }
884
885    manager.save_to_disk()?;
886
887    // 4. Update working branch to keep safety net in sync
888    if let Some(ref working_branch_name) = working_branch {
889        Output::sub_item(format!("Updating working branch: {}", working_branch_name));
890
891        // Force update the working branch to point to the amended commit
892        repo.update_branch_to_commit(working_branch_name, &new_commit_hash)?;
893
894        Output::success(format!("Working branch '{}' updated", working_branch_name));
895    } else {
896        Output::warning("No working branch found - create one with 'ca stack create' for safety");
897    }
898
899    // 5. Auto-push if requested and entry has a PR
900    if push {
901        println!();
902
903        if has_pr {
904            Output::section("Force-pushing to remote");
905
906            // Set env var to skip force-push confirmation
907            std::env::set_var("FORCE_PUSH_NO_CONFIRM", "1");
908
909            repo.force_push_branch(&current_branch, &current_branch)?;
910            Output::success(format!("Force-pushed '{}' to remote", current_branch));
911            Output::sub_item("PR will be automatically updated");
912        } else {
913            Output::warning("No PR found for this entry - skipping push");
914            Output::tip("Use 'ca submit' to create a PR");
915        }
916    }
917
918    // Summary
919    println!();
920    Output::section("Summary");
921    Output::bullet(format!(
922        "Amended entry #{} on branch '{}'",
923        entry_index + 1,
924        entry_branch
925    ));
926    if working_branch.is_some() {
927        Output::bullet("Working branch updated");
928    }
929    if push {
930        Output::bullet("Changes force-pushed to remote");
931    }
932
933    // Automatically restack dependent entries (no flag needed - always required)
934    if has_dependents {
935        println!();
936        let dependent_count = {
937            let stack = manager
938                .get_stack(&stack_id)
939                .ok_or_else(|| CascadeError::config("Stack not found"))?;
940            stack
941                .entries
942                .iter()
943                .skip(entry_index + 1)
944                .filter(|entry| !entry.is_merged)
945                .count()
946        };
947
948        let plural = if dependent_count == 1 {
949            "entry"
950        } else {
951            "entries"
952        };
953
954        Output::section(format!(
955            "Restacking {} dependent {}",
956            dependent_count, plural
957        ));
958
959        // Rebase dependent entries using the same logic as ca sync
960        // This ensures entries #4, #5, etc. are rebased onto the amended entry #3
961        match restack_dependent_entries(&repo_root, &stack_id, entry_index).await {
962            Ok(_) => {
963                Output::success(format!(
964                    "Restacked {} dependent {}",
965                    dependent_count, plural
966                ));
967            }
968            Err(e) => {
969                println!();
970                Output::error(format!("Failed to restack dependent entries: {}", e));
971                println!();
972                Output::section("Recovery Steps");
973                Output::bullet("Resolve any conflicts in your editor");
974                Output::bullet("Stage resolved files: git add <files>");
975                Output::bullet("Continue: ca entry continue");
976                Output::bullet("Or abort: ca entry abort");
977                println!();
978                return Err(CascadeError::validation(
979                    "Restack failed - resolve conflicts and run 'ca entry continue'",
980                ));
981            }
982        }
983    }
984
985    // Tip about --push flag
986    if !push && !has_dependents {
987        println!();
988        Output::tip("Use --push to automatically force-push after amending");
989    }
990
991    Ok(())
992}
993
994/// Restack dependent entries after amending
995/// This ensures entries after the amended one are rebased onto the new commit
996///
997/// CRITICAL CONSTRAINTS:
998/// - User is currently on the amended branch (e.g., entry #3)
999/// - We must NOT touch the amended entry or any entries before it
1000/// - We only rebase entries AFTER the amended one (e.g., #4, #5)
1001/// - Each dependent entry is rebased onto its parent (not develop!)
1002/// - After restacking, update working branch to point to new top of stack
1003async fn restack_dependent_entries(
1004    repo_root: &Path,
1005    stack_id: &uuid::Uuid,
1006    amended_entry_index: usize,
1007) -> Result<()> {
1008    use tracing::debug;
1009
1010    debug!(
1011        "Restacking dependent entries after amending entry #{}",
1012        amended_entry_index + 1
1013    );
1014
1015    // Load fresh stack manager and repo
1016    let mut stack_manager = StackManager::new(repo_root)?;
1017    let git_repo = GitRepository::open(repo_root)?;
1018
1019    // Get the stack (clone to avoid borrow issues)
1020    let stack = stack_manager
1021        .get_stack(stack_id)
1022        .ok_or_else(|| CascadeError::config("Stack not found"))?
1023        .clone();
1024
1025    // Get the amended entry (this is the new "base" for dependents)
1026    let amended_entry = &stack.entries[amended_entry_index];
1027    let amended_branch = &amended_entry.branch;
1028    let amended_commit = &amended_entry.commit_hash;
1029
1030    debug!(
1031        "Amended entry: branch='{}', commit={}",
1032        amended_branch,
1033        &amended_commit[..8]
1034    );
1035
1036    // Collect entries AFTER the amended one
1037    // We need ALL entries (including merged) to correctly advance the base commit
1038    let dependent_entries: Vec<(usize, StackEntry)> = stack
1039        .entries
1040        .iter()
1041        .enumerate()
1042        .skip(amended_entry_index + 1)
1043        .map(|(idx, entry)| (idx, entry.clone()))
1044        .collect();
1045
1046    if dependent_entries.is_empty() {
1047        debug!("No dependent entries after amended entry");
1048        return Ok(());
1049    }
1050
1051    let unmerged_count = dependent_entries
1052        .iter()
1053        .filter(|(_, e)| !e.is_merged)
1054        .count();
1055    debug!(
1056        "Will process {} dependent entries ({} unmerged, {} merged)",
1057        dependent_entries.len(),
1058        unmerged_count,
1059        dependent_entries.len() - unmerged_count
1060    );
1061
1062    // We're currently on the amended branch - save it to restore later
1063    let original_branch = git_repo.get_current_branch()?;
1064    debug!("Currently on branch: {}", original_branch);
1065
1066    // Rebase each dependent entry sequentially
1067    // Entry #4 onto amended entry #3, then entry #5 onto new entry #4, etc.
1068    let mut current_base_commit = amended_commit.clone();
1069
1070    for &(original_index, ref entry) in dependent_entries.iter() {
1071        let entry_num = original_index + 1; // Convert 0-based index to 1-based entry number
1072
1073        // Skip merged entries - they're already in the base branch
1074        // But we still need to advance current_base_commit past them
1075        if entry.is_merged {
1076            debug!(
1077                "Entry #{} ({}) is merged, advancing base to {}",
1078                entry_num,
1079                entry.branch,
1080                &entry.commit_hash[..8]
1081            );
1082            current_base_commit = entry.commit_hash.clone();
1083            continue;
1084        }
1085
1086        debug!(
1087            "Rebasing entry #{} ({}): {} onto {}",
1088            entry_num,
1089            entry.branch,
1090            &entry.commit_hash[..8],
1091            &current_base_commit[..8]
1092        );
1093
1094        // Cherry-pick this entry's commit onto the current base
1095        // This is similar to what rebase_all_entries does, but for one entry at a time
1096        let temp_branch = format!("{}-restack-temp", entry.branch);
1097
1098        // Create temp branch from current base
1099        git_repo.create_branch(&temp_branch, Some(&current_base_commit))?;
1100        git_repo.checkout_branch_silent(&temp_branch)?;
1101
1102        // Cherry-pick the entry's commit
1103        match git_repo.cherry_pick(&entry.commit_hash) {
1104            Ok(new_commit_hash) => {
1105                // Update the entry's branch to point to the new commit
1106                git_repo.update_branch_to_commit(&entry.branch, &new_commit_hash)?;
1107
1108                // Update metadata
1109                {
1110                    let stack_mut = stack_manager
1111                        .get_stack_mut(stack_id)
1112                        .ok_or_else(|| CascadeError::config("Stack not found"))?;
1113
1114                    stack_mut
1115                        .update_entry_commit_hash(&entry.id, new_commit_hash.clone())
1116                        .map_err(CascadeError::config)?;
1117                }
1118                stack_manager.save_to_disk()?;
1119
1120                debug!("  → New commit: {}", &new_commit_hash[..8]);
1121
1122                // This becomes the base for the next entry
1123                current_base_commit = new_commit_hash;
1124            }
1125            Err(e) => {
1126                // Cherry-pick failed - LEAVE EVERYTHING INTACT for recovery
1127                // CRITICAL: DO NOT checkout or delete temp branch!
1128                // The user needs CHERRY_PICK_HEAD and conflict state to resolve/abort
1129
1130                println!();
1131                Output::error(format!(
1132                    "Failed to restack entry #{} ({}): {}",
1133                    entry_num, entry.branch, e
1134                ));
1135                println!();
1136                Output::section("Recovery Options");
1137                println!();
1138                Output::sub_item("To continue after resolving conflicts:");
1139                Output::bullet("1. Check for conflicts: git status");
1140                Output::bullet("2. Resolve conflicts in your editor");
1141                Output::bullet("3. Stage resolved files: git add <files>");
1142                Output::bullet("4. Continue restack: ca entry continue");
1143                println!();
1144                Output::sub_item("To abort and undo the restack:");
1145                Output::bullet("→ Run: ca entry abort");
1146                Output::bullet("→ Then check: ca validate");
1147                println!();
1148                Output::tip("Both commands bypass hooks to avoid edit-mode detection");
1149
1150                return Err(CascadeError::validation(format!(
1151                    "Restack paused at entry #{} - resolve conflicts or abort",
1152                    entry_num
1153                )));
1154            }
1155        }
1156
1157        // Clean up temp branch - checkout away first, then force delete
1158        // CRITICAL: Must checkout away from temp branch before deleting it
1159        git_repo.checkout_branch_unsafe(&original_branch)?;
1160        // Use unsafe delete to avoid interactive prompts for unpushed commits
1161        git_repo.delete_branch_unsafe(&temp_branch)?;
1162    }
1163
1164    // At this point we're already on original_branch from the last loop iteration
1165
1166    // Update working branch to point to the NEW top of stack (last dependent entry)
1167    if let Some(ref working_branch_name) = stack.working_branch {
1168        debug!(
1169            "Updating working branch '{}' to {}",
1170            working_branch_name,
1171            &current_base_commit[..8]
1172        );
1173        git_repo.update_branch_to_commit(working_branch_name, &current_base_commit)?;
1174    }
1175
1176    debug!("Successfully restacked {} entries", dependent_entries.len());
1177    Ok(())
1178}
1179
1180/// Continue restacking after resolving cherry-pick conflicts
1181/// This completes the cherry-pick (skipping hooks) and updates metadata
1182async fn continue_restack() -> Result<()> {
1183    use tracing::debug;
1184
1185    let current_dir = env::current_dir()
1186        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
1187
1188    let repo_root = find_repository_root(&current_dir)?;
1189    let git_repo = GitRepository::open(&repo_root)?;
1190
1191    // Check if there's a cherry-pick in progress
1192    let cherry_pick_head = repo_root.join(".git").join("CHERRY_PICK_HEAD");
1193    if !cherry_pick_head.exists() {
1194        return Err(CascadeError::validation(
1195            "No cherry-pick in progress. Nothing to continue.".to_string(),
1196        ));
1197    }
1198
1199    Output::section("Continuing restack");
1200
1201    // Get current branch (should be *-restack-temp)
1202    let current_branch = git_repo.get_current_branch()?;
1203    if !current_branch.ends_with("-restack-temp") {
1204        return Err(CascadeError::validation(format!(
1205            "Expected to be on a *-restack-temp branch, but on '{}'. Cannot continue safely.",
1206            current_branch
1207        )));
1208    }
1209
1210    // Extract the original entry branch name
1211    let entry_branch = current_branch.trim_end_matches("-restack-temp");
1212
1213    // Auto-stage resolved conflict files (only files that had conflicts)
1214    // This prevents leaking unrelated changes while helping users who forget git add
1215    match git_repo.stage_conflict_resolved_files() {
1216        Ok(_) => {
1217            Output::sub_item("Auto-staged resolved conflict files");
1218        }
1219        Err(e) => {
1220            debug!("Could not auto-stage conflict files: {}", e);
1221            Output::warning("Could not auto-stage files. Make sure you've run 'git add <files>'");
1222        }
1223    }
1224
1225    // Complete the cherry-pick with CASCADE_SKIP_HOOKS to bypass pre-commit hook
1226    let output = std::process::Command::new("git")
1227        .args(["cherry-pick", "--continue"])
1228        .env("CASCADE_SKIP_HOOKS", "1")
1229        .current_dir(&repo_root)
1230        .stdout(std::process::Stdio::null())
1231        .stderr(std::process::Stdio::piped())
1232        .output()
1233        .map_err(CascadeError::Io)?;
1234
1235    if !output.status.success() {
1236        let stderr = String::from_utf8_lossy(&output.stderr);
1237        return Err(CascadeError::validation(format!(
1238            "Failed to continue cherry-pick: {}\n\n\
1239            Make sure all conflicts are resolved and staged:\n\
1240            1. Check status: git status\n\
1241            2. Stage resolved files: git add <files>\n\
1242            3. Try again: ca entry continue",
1243            stderr.trim()
1244        )));
1245    }
1246
1247    Output::success("Cherry-pick completed");
1248
1249    // CRITICAL: Get the new commit hash BEFORE cleaning up temp branch
1250    let new_commit_hash = git_repo.get_head_commit()?.id().to_string();
1251    debug!("New commit hash: {}", &new_commit_hash[..8]);
1252
1253    // CRITICAL: Update the entry branch to point to the new commit
1254    // This must happen BEFORE deleting the temp branch!
1255    Output::sub_item(format!("Updating branch '{}' to new commit", entry_branch));
1256    git_repo.update_branch_to_commit(entry_branch, &new_commit_hash)?;
1257
1258    // CRITICAL: Update metadata with the new commit hash
1259    let mut stack_manager = StackManager::new(&repo_root)?;
1260    let active_stack = stack_manager
1261        .get_active_stack()
1262        .ok_or_else(|| CascadeError::config("No active stack"))?;
1263
1264    // Find the entry by branch name
1265    let entry_id = active_stack
1266        .entries
1267        .iter()
1268        .find(|e| e.branch == entry_branch)
1269        .map(|e| e.id)
1270        .ok_or_else(|| {
1271            CascadeError::config(format!(
1272                "Could not find entry for branch '{}'",
1273                entry_branch
1274            ))
1275        })?;
1276
1277    let stack_id = active_stack.id;
1278
1279    {
1280        let stack_mut = stack_manager
1281            .get_stack_mut(&stack_id)
1282            .ok_or_else(|| CascadeError::config("Stack not found"))?;
1283
1284        stack_mut
1285            .update_entry_commit_hash(&entry_id, new_commit_hash.clone())
1286            .map_err(CascadeError::config)?;
1287    }
1288    stack_manager.save_to_disk()?;
1289
1290    Output::sub_item(format!("Updated metadata: {}", &new_commit_hash[..8]));
1291
1292    // Now safe to clean up temp branch
1293    Output::sub_item(format!("Cleaning up temp branch '{}'", current_branch));
1294
1295    // Checkout to entry branch (which now points to the new commit)
1296    git_repo.checkout_branch_unsafe(entry_branch)?;
1297
1298    // Delete the temp branch
1299    git_repo.delete_branch_unsafe(&current_branch)?;
1300
1301    println!();
1302    Output::warning("Restack is incomplete!");
1303    Output::sub_item("The current entry has been resolved, but:");
1304    Output::sub_item("• Remaining dependent entries still need restacking");
1305    Output::sub_item("• Working branch needs updating");
1306    println!();
1307    Output::section("Next Steps");
1308    Output::bullet("Complete restack: ca sync");
1309    Output::bullet("This will rebase remaining entries and update working branch");
1310    println!();
1311
1312    Ok(())
1313}
1314
1315/// Abort an in-progress restack operation
1316/// Safely aborts the cherry-pick using CASCADE_SKIP_HOOKS to bypass hook issues
1317async fn abort_restack() -> Result<()> {
1318    let current_dir = env::current_dir()
1319        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
1320
1321    let repo_root = find_repository_root(&current_dir)?;
1322
1323    // Check if there's a cherry-pick in progress
1324    let cherry_pick_head = repo_root.join(".git").join("CHERRY_PICK_HEAD");
1325    if !cherry_pick_head.exists() {
1326        return Err(CascadeError::validation(
1327            "No cherry-pick in progress. Nothing to abort.".to_string(),
1328        ));
1329    }
1330
1331    Output::section("Aborting restack");
1332
1333    // Abort the cherry-pick with CASCADE_SKIP_HOOKS to bypass pre-commit hook
1334    let output = std::process::Command::new("git")
1335        .args(["cherry-pick", "--abort"])
1336        .env("CASCADE_SKIP_HOOKS", "1")
1337        .current_dir(&repo_root)
1338        .stdout(std::process::Stdio::null())
1339        .stderr(std::process::Stdio::piped())
1340        .output()
1341        .map_err(CascadeError::Io)?;
1342
1343    if !output.status.success() {
1344        let stderr = String::from_utf8_lossy(&output.stderr);
1345        return Err(CascadeError::validation(format!(
1346            "Failed to abort cherry-pick: {}\n\n\
1347            You may need to manually clean up the Git state:\n\
1348            1. Check status: git status\n\
1349            2. Reset if needed: git reset --hard HEAD",
1350            stderr.trim()
1351        )));
1352    }
1353
1354    Output::success("Cherry-pick aborted");
1355
1356    // Clean up any temp restack branches
1357    let git_repo = GitRepository::open(&repo_root)?;
1358    let current_branch = git_repo.get_current_branch().ok();
1359
1360    // If we're on a *-restack-temp branch, clean it up
1361    if let Some(ref branch) = current_branch {
1362        if branch.ends_with("-restack-temp") {
1363            // Extract the original branch name
1364            let original_branch = branch.trim_end_matches("-restack-temp");
1365
1366            Output::sub_item(format!("Cleaning up temp branch '{}'", branch));
1367
1368            // Checkout to original branch first
1369            if let Err(e) = git_repo.checkout_branch_unsafe(original_branch) {
1370                Output::warning(format!(
1371                    "Could not checkout to '{}': {}. You may need to checkout manually.",
1372                    original_branch, e
1373                ));
1374            } else {
1375                // Delete the temp branch
1376                if let Err(e) = git_repo.delete_branch_unsafe(branch) {
1377                    Output::warning(format!(
1378                        "Could not delete temp branch '{}': {}. You may need to delete it manually.",
1379                        branch, e
1380                    ));
1381                }
1382            }
1383        }
1384    }
1385
1386    println!();
1387    Output::warning("Restack was aborted - stack may be in inconsistent state");
1388    println!();
1389    Output::section("Next Steps");
1390    Output::bullet("Check stack state: ca validate");
1391    Output::bullet("If needed, fix issues with: ca validate (choose 'Incorporate' or 'Reset')");
1392    Output::bullet("Or try restack again: ca sync");
1393    println!();
1394
1395    Ok(())
1396}