cascade_cli/cli/commands/
entry.rs

1use crate::cli::output::Output;
2use crate::errors::{CascadeError, Result};
3use crate::git::find_repository_root;
4use crate::stack::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 tracing::{debug, warn};
23
24#[derive(Debug, Subcommand)]
25pub enum EntryAction {
26    /// Interactively checkout a stack entry for editing
27    Checkout {
28        /// Stack entry number (optional, shows picker if not provided)
29        entry: Option<usize>,
30        /// Skip interactive picker and use entry number directly
31        #[arg(long)]
32        direct: bool,
33        /// Skip confirmation prompts
34        #[arg(long, short)]
35        yes: bool,
36    },
37    /// Show current edit mode status
38    Status {
39        /// Show brief status only
40        #[arg(long)]
41        quiet: bool,
42    },
43    /// List all entries with their edit status
44    List {
45        /// Show detailed information
46        #[arg(long, short)]
47        verbose: bool,
48    },
49    /// Clear/exit edit mode (useful for recovering from corrupted state)
50    Clear {
51        /// Skip confirmation prompt
52        #[arg(long, short)]
53        yes: bool,
54    },
55    /// Amend the current stack entry commit and update working branch
56    Amend {
57        /// New commit message (optional, uses git editor if not provided)
58        #[arg(long, short)]
59        message: Option<String>,
60        /// Include all changes (staged + unstaged) like 'git commit -a --amend'
61        #[arg(long, short)]
62        all: bool,
63        /// Automatically force-push after amending (if PR exists)
64        #[arg(long)]
65        push: bool,
66        /// Auto-restack dependent entries after amending
67        #[arg(long)]
68        restack: bool,
69    },
70}
71
72pub async fn run(action: EntryAction) -> Result<()> {
73    let _current_dir = env::current_dir()
74        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
75
76    match action {
77        EntryAction::Checkout { entry, direct, yes } => checkout_entry(entry, direct, yes).await,
78        EntryAction::Status { quiet } => show_edit_status(quiet).await,
79        EntryAction::List { verbose } => list_entries(verbose).await,
80        EntryAction::Clear { yes } => clear_edit_mode(yes).await,
81        EntryAction::Amend {
82            message,
83            all,
84            push,
85            restack,
86        } => amend_entry(message, all, push, restack).await,
87    }
88}
89
90/// Checkout a specific stack entry for editing
91async fn checkout_entry(
92    entry_num: Option<usize>,
93    direct: bool,
94    skip_confirmation: bool,
95) -> Result<()> {
96    let current_dir = env::current_dir()
97        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
98
99    let repo_root = find_repository_root(&current_dir)
100        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
101
102    let mut manager = StackManager::new(&repo_root)?;
103
104    // Get active stack
105    let active_stack = manager.get_active_stack().ok_or_else(|| {
106        CascadeError::config("No active stack. Create a stack first with 'ca stack create'")
107    })?;
108
109    if active_stack.entries.is_empty() {
110        return Err(CascadeError::config(
111            "Stack is empty. Push some commits first with 'ca stack push'",
112        ));
113    }
114
115    // Determine which entry to checkout
116    let target_entry_num = if let Some(num) = entry_num {
117        if num == 0 || num > active_stack.entries.len() {
118            return Err(CascadeError::config(format!(
119                "Invalid entry number: {}. Stack has {} entries",
120                num,
121                active_stack.entries.len()
122            )));
123        }
124        num
125    } else if direct {
126        return Err(CascadeError::config(
127            "Entry number required when using --direct flag",
128        ));
129    } else {
130        // Show interactive picker
131        show_entry_picker(active_stack).await?
132    };
133
134    let target_entry = &active_stack.entries[target_entry_num - 1]; // Convert to 0-based index
135
136    // Clone the values we need before borrowing manager mutably
137    let stack_id = active_stack.id;
138    let entry_id = target_entry.id;
139    let entry_branch = target_entry.branch.clone();
140    let entry_short_hash = target_entry.short_hash();
141    let entry_short_message = target_entry.short_message(50);
142    let entry_pr_id = target_entry.pull_request_id.clone();
143    let entry_message = target_entry.message.clone();
144
145    // Check if already in edit mode and get info before confirmation
146    let already_in_edit_mode = manager.is_in_edit_mode();
147    let edit_mode_display = if already_in_edit_mode {
148        let edit_info = manager.get_edit_mode_info().unwrap();
149
150        // Get the commit message for the current edit target
151        let commit_message = if let Some(target_entry_id) = &edit_info.target_entry_id {
152            if let Some(entry) = active_stack
153                .entries
154                .iter()
155                .find(|e| e.id == *target_entry_id)
156            {
157                entry.short_message(50)
158            } else {
159                "Unknown entry".to_string()
160            }
161        } else {
162            "Unknown target".to_string()
163        };
164
165        Some((edit_info.original_commit_hash.clone(), commit_message))
166    } else {
167        None
168    };
169
170    // Let the active_stack reference go out of scope before we potentially mutably borrow manager
171    let _ = active_stack;
172
173    // Handle edit mode exit if needed
174    if let Some((commit_hash, commit_message)) = edit_mode_display {
175        warn!("Already in edit mode for entry in stack");
176
177        if !skip_confirmation {
178            Output::warning("Already in edit mode!");
179            Output::sub_item(format!(
180                "Current target: {} ({})",
181                &commit_hash[..8],
182                commit_message
183            ));
184
185            // Interactive confirmation to exit current edit mode
186            let should_exit_edit_mode = Confirm::with_theme(&ColorfulTheme::default())
187                .with_prompt("Exit current edit mode and start a new one?")
188                .default(false)
189                .interact()
190                .map_err(|e| {
191                    CascadeError::config(format!("Failed to get user confirmation: {e}"))
192                })?;
193
194            if !should_exit_edit_mode {
195                return Err(CascadeError::config(
196                    "Operation cancelled. Use 'ca entry status' to see current edit mode details.",
197                ));
198            }
199
200            // Exit current edit mode before starting a new one
201            Output::info("Exiting current edit mode...");
202            manager.exit_edit_mode()?;
203            Output::success("✓ Exited previous edit mode");
204        }
205    }
206
207    // Confirmation prompt
208    if !skip_confirmation {
209        Output::section("Checking out entry for editing");
210        Output::sub_item(format!(
211            "Entry #{target_entry_num}: {entry_short_hash} ({entry_short_message})"
212        ));
213        Output::sub_item(format!("Branch: {entry_branch}"));
214        if let Some(pr_id) = &entry_pr_id {
215            Output::sub_item(format!("PR: #{pr_id}"));
216        }
217
218        // Display full commit message
219        Output::sub_item("Commit Message:");
220        let lines: Vec<&str> = entry_message.lines().collect();
221        for line in lines {
222            Output::sub_item(format!("  {line}"));
223        }
224
225        Output::warning("This will checkout the commit and enter edit mode.");
226        Output::info("Any changes you make can be amended to this commit or create new entries.");
227
228        // Interactive confirmation to proceed with checkout
229        let should_continue = Confirm::with_theme(&ColorfulTheme::default())
230            .with_prompt("Continue with checkout?")
231            .default(false)
232            .interact()
233            .map_err(|e| CascadeError::config(format!("Failed to get user confirmation: {e}")))?;
234
235        if !should_continue {
236            return Err(CascadeError::config("Entry checkout cancelled"));
237        }
238    }
239
240    // Enter edit mode
241    manager.enter_edit_mode(stack_id, entry_id)?;
242
243    // Checkout the branch (not the commit - we want to stay on the branch)
244    let current_dir = env::current_dir()
245        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
246
247    let repo_root = find_repository_root(&current_dir)
248        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
249    let repo = crate::git::GitRepository::open(&repo_root)?;
250
251    debug!("Checking out branch: {}", entry_branch);
252    repo.checkout_branch(&entry_branch)?;
253
254    Output::success(format!("Entered edit mode for entry #{target_entry_num}"));
255    Output::sub_item(format!(
256        "You are now on commit: {} ({})",
257        entry_short_hash, entry_short_message
258    ));
259    Output::sub_item(format!("Branch: {entry_branch}"));
260
261    Output::section("Make your changes and commit normally");
262    Output::bullet("Use 'ca entry status' to see edit mode info");
263    Output::bullet("When you commit, the pre-commit hook will guide you:");
264    Output::sub_item("  → Press Enter (or 'A') to amend this entry");
265    Output::sub_item("  → Type 'n' to create new entry on top");
266    Output::bullet("Run 'ca sync' after committing to update PRs");
267
268    // Check if prepare-commit-msg hook is installed
269    let hooks_dir = repo_root.join(".git/hooks");
270    let hook_path = hooks_dir.join("prepare-commit-msg");
271    if !hook_path.exists() {
272        Output::tip("Install the prepare-commit-msg hook for better guidance:");
273        Output::sub_item("ca hooks add prepare-commit-msg");
274    }
275
276    Ok(())
277}
278
279/// Interactive entry picker using TUI
280async fn show_entry_picker(stack: &crate::stack::Stack) -> Result<usize> {
281    // Setup terminal
282    enable_raw_mode()?;
283    let mut stdout = io::stdout();
284    execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
285    let backend = CrosstermBackend::new(stdout);
286    let mut terminal = Terminal::new(backend)?;
287
288    let mut list_state = ListState::default();
289    list_state.select(Some(0));
290
291    let result = loop {
292        terminal.draw(|f| {
293            let size = f.area();
294
295            // Create layout
296            let chunks = Layout::default()
297                .direction(Direction::Vertical)
298                .margin(2)
299                .constraints(
300                    [
301                        Constraint::Length(3), // Title
302                        Constraint::Min(5),    // List
303                        Constraint::Length(3), // Help
304                    ]
305                    .as_ref(),
306                )
307                .split(size);
308
309            // Title
310            let title = Paragraph::new(format!("📚 Select Entry from Stack: {}", stack.name))
311                .style(
312                    Style::default()
313                        .fg(Color::Cyan)
314                        .add_modifier(Modifier::BOLD),
315                )
316                .alignment(Alignment::Center)
317                .block(Block::default().borders(Borders::ALL));
318            f.render_widget(title, chunks[0]);
319
320            // Entry list
321            let items: Vec<ListItem> = stack
322                .entries
323                .iter()
324                .enumerate()
325                .map(|(i, entry)| {
326                    let status_icon = if entry.is_submitted {
327                        if entry.pull_request_id.is_some() {
328                            "📤"
329                        } else {
330                            "📝"
331                        }
332                    } else {
333                        "🔄"
334                    };
335
336                    let pr_text = if let Some(pr_id) = &entry.pull_request_id {
337                        format!(" PR: #{pr_id}")
338                    } else {
339                        "".to_string()
340                    };
341
342                    let line = Line::from(vec![
343                        Span::raw(format!("  {}. ", i + 1)),
344                        Span::raw(status_icon),
345                        Span::raw(" "),
346                        Span::styled(entry.short_message(40), Style::default().fg(Color::White)),
347                        Span::raw(" "),
348                        Span::styled(
349                            format!("({})", entry.short_hash()),
350                            Style::default().fg(Color::Yellow),
351                        ),
352                        Span::styled(pr_text, Style::default().fg(Color::Green)),
353                    ]);
354
355                    ListItem::new(line)
356                })
357                .collect();
358
359            let list = List::new(items)
360                .block(Block::default().borders(Borders::ALL).title("Entries"))
361                .highlight_style(Style::default().fg(Color::Black).bg(Color::Cyan))
362                .highlight_symbol("→ ");
363
364            f.render_stateful_widget(list, chunks[1], &mut list_state);
365
366            // Help text
367            let help = Paragraph::new("↑/↓: Navigate • Enter: Select • q: Quit • r: Refresh")
368                .style(Style::default().fg(Color::DarkGray))
369                .alignment(Alignment::Center)
370                .block(Block::default().borders(Borders::ALL));
371            f.render_widget(help, chunks[2]);
372        })?;
373
374        // Handle input
375        if let Event::Key(key) = event::read()? {
376            if key.kind == KeyEventKind::Press {
377                match key.code {
378                    KeyCode::Char('q') => {
379                        break Err(CascadeError::config("Entry selection cancelled"));
380                    }
381                    KeyCode::Up => {
382                        let selected = list_state.selected().unwrap_or(0);
383                        if selected > 0 {
384                            list_state.select(Some(selected - 1));
385                        } else {
386                            list_state.select(Some(stack.entries.len() - 1));
387                        }
388                    }
389                    KeyCode::Down => {
390                        let selected = list_state.selected().unwrap_or(0);
391                        if selected < stack.entries.len() - 1 {
392                            list_state.select(Some(selected + 1));
393                        } else {
394                            list_state.select(Some(0));
395                        }
396                    }
397                    KeyCode::Enter => {
398                        let selected = list_state.selected().unwrap_or(0);
399                        break Ok(selected + 1); // Convert to 1-based index
400                    }
401                    KeyCode::Char('r') => {
402                        // Refresh - for now just continue the loop
403                        continue;
404                    }
405                    _ => {}
406                }
407            }
408        }
409    };
410
411    // Restore terminal
412    disable_raw_mode()?;
413    execute!(
414        terminal.backend_mut(),
415        LeaveAlternateScreen,
416        DisableMouseCapture
417    )?;
418    terminal.show_cursor()?;
419
420    result
421}
422
423/// Show current edit mode status
424async fn show_edit_status(quiet: bool) -> Result<()> {
425    let current_dir = env::current_dir()
426        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
427
428    let repo_root = find_repository_root(&current_dir)
429        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
430    let manager = StackManager::new(&repo_root)?;
431
432    if !manager.is_in_edit_mode() {
433        if quiet {
434            println!("inactive");
435        } else {
436            Output::info("Not in edit mode");
437            Output::sub_item("Use 'ca entry checkout' to start editing a stack entry");
438        }
439        return Ok(());
440    }
441
442    let edit_info = manager.get_edit_mode_info().unwrap();
443
444    if quiet {
445        println!("active:{:?}", edit_info.target_entry_id);
446        return Ok(());
447    }
448
449    Output::section("Currently in edit mode");
450
451    // Try to get the entry information
452    if let Some(active_stack) = manager.get_active_stack() {
453        if let Some(target_entry_id) = edit_info.target_entry_id {
454            if let Some(entry) = active_stack
455                .entries
456                .iter()
457                .find(|e| e.id == target_entry_id)
458            {
459                Output::sub_item(format!(
460                    "Target entry: {} ({})",
461                    entry.short_hash(),
462                    entry.short_message(50)
463                ));
464                Output::sub_item(format!("Branch: {}", entry.branch));
465
466                // Display full commit message
467                Output::sub_item("Commit Message:");
468                let lines: Vec<&str> = entry.message.lines().collect();
469                for line in lines {
470                    Output::sub_item(format!("  {line}"));
471                }
472            } else {
473                Output::sub_item(format!("Target entry: {target_entry_id:?} (not found)"));
474            }
475        } else {
476            Output::sub_item("Target entry: Unknown");
477        }
478    } else {
479        Output::sub_item(format!("Target entry: {:?}", edit_info.target_entry_id));
480    }
481
482    Output::sub_item(format!(
483        "Original commit: {}",
484        &edit_info.original_commit_hash[..8]
485    ));
486    Output::sub_item(format!(
487        "Started: {}",
488        edit_info.started_at.format("%Y-%m-%d %H:%M:%S")
489    ));
490
491    // Show current Git status
492    Output::section("Current state");
493
494    // Get current repository state
495    let current_dir = env::current_dir()
496        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
497    let repo_root = find_repository_root(&current_dir)
498        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
499    let repo = crate::git::GitRepository::open(&repo_root)?;
500
501    // Current HEAD vs original commit
502    let current_head = repo.get_current_commit_hash()?;
503    if current_head != edit_info.original_commit_hash {
504        let current_short = &current_head[..8];
505        let original_short = &edit_info.original_commit_hash[..8];
506        Output::sub_item(format!("HEAD moved: {original_short} → {current_short}"));
507
508        // Show if there are new commits
509        match repo.get_commit_count_between(&edit_info.original_commit_hash, &current_head) {
510            Ok(count) if count > 0 => {
511                Output::sub_item(format!("  {count} new commit(s) created"));
512            }
513            _ => {}
514        }
515    } else {
516        Output::sub_item(format!("HEAD: {} (unchanged)", &current_head[..8]));
517    }
518
519    // Working directory and staging status
520    match repo.get_status_summary() {
521        Ok(status) => {
522            if status.is_clean() {
523                Output::sub_item("Working directory: clean");
524            } else {
525                if status.has_staged_changes() {
526                    Output::sub_item(format!("Staged changes: {} files", status.staged_count()));
527                }
528                if status.has_unstaged_changes() {
529                    Output::sub_item(format!(
530                        "Unstaged changes: {} files",
531                        status.unstaged_count()
532                    ));
533                }
534                if status.has_untracked_files() {
535                    Output::sub_item(format!(
536                        "Untracked files: {} files",
537                        status.untracked_count()
538                    ));
539                }
540            }
541        }
542        Err(_) => {
543            Output::sub_item("Working directory: status unavailable");
544        }
545    }
546
547    Output::tip("Use 'git status' for detailed file-level status");
548    Output::sub_item("Use 'ca entry list' to see all entries");
549
550    Ok(())
551}
552
553/// List all entries in the stack with edit status
554async fn list_entries(verbose: bool) -> Result<()> {
555    let current_dir = env::current_dir()
556        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
557
558    let repo_root = find_repository_root(&current_dir)
559        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
560    let manager = StackManager::new(&repo_root)?;
561
562    let active_stack = manager.get_active_stack().ok_or_else(|| {
563        CascadeError::config(
564            "No active stack. Create a stack first with 'ca stack create'".to_string(),
565        )
566    })?;
567
568    if active_stack.entries.is_empty() {
569        Output::info(format!(
570            "Active stack '{}' has no entries yet",
571            active_stack.name
572        ));
573        Output::sub_item("Add some commits to the stack with 'ca stack push'");
574        return Ok(());
575    }
576
577    Output::section(format!(
578        "Stack: {} ({} entries)",
579        active_stack.name,
580        active_stack.entries.len()
581    ));
582
583    let edit_mode_info = manager.get_edit_mode_info();
584
585    for (i, entry) in active_stack.entries.iter().enumerate() {
586        let entry_num = i + 1;
587
588        // Status icon
589        let status_icon = if entry.is_submitted {
590            if entry.pull_request_id.is_some() {
591                "📤"
592            } else {
593                "📝"
594            }
595        } else {
596            "🔄"
597        };
598
599        // Edit mode indicator
600        let edit_indicator = if edit_mode_info.is_some()
601            && edit_mode_info.unwrap().target_entry_id == Some(entry.id)
602        {
603            " 🎯"
604        } else {
605            ""
606        };
607
608        // Basic entry line
609        print!(
610            "   {}. {} {} ({})",
611            entry_num,
612            status_icon,
613            entry.short_message(50),
614            entry.short_hash()
615        );
616
617        // PR information
618        if let Some(pr_id) = &entry.pull_request_id {
619            print!(" PR: #{pr_id}");
620        }
621
622        print!("{edit_indicator}");
623        println!(); // Line break for entry
624
625        // Verbose information
626        if verbose {
627            Output::sub_item(format!("Branch: {}", entry.branch));
628            Output::sub_item(format!("Commit: {}", entry.commit_hash));
629            Output::sub_item(format!(
630                "Created: {}",
631                entry.created_at.format("%Y-%m-%d %H:%M:%S")
632            ));
633            if entry.is_submitted {
634                Output::sub_item("Status: Submitted");
635            } else {
636                Output::sub_item("Status: Draft");
637            }
638
639            // Display full commit message
640            Output::sub_item("Message:");
641            let lines: Vec<&str> = entry.message.lines().collect();
642            for line in lines {
643                Output::sub_item(format!("  {line}"));
644            }
645
646            // Add Git status info for entry in edit mode
647            if edit_mode_info.is_some() && edit_mode_info.unwrap().target_entry_id == Some(entry.id)
648            {
649                if let Ok(repo_root) = find_repository_root(&env::current_dir().unwrap_or_default())
650                {
651                    if let Ok(repo) = crate::git::GitRepository::open(&repo_root) {
652                        match repo.get_status_summary() {
653                            Ok(status) => {
654                                if !status.is_clean() {
655                                    Output::sub_item("Git Status:");
656                                    if status.has_staged_changes() {
657                                        Output::sub_item(format!(
658                                            "  Staged: {} files",
659                                            status.staged_count()
660                                        ));
661                                    }
662                                    if status.has_unstaged_changes() {
663                                        Output::sub_item(format!(
664                                            "  Unstaged: {} files",
665                                            status.unstaged_count()
666                                        ));
667                                    }
668                                    if status.has_untracked_files() {
669                                        Output::sub_item(format!(
670                                            "  Untracked: {} files",
671                                            status.untracked_count()
672                                        ));
673                                    }
674                                } else {
675                                    Output::sub_item("Git Status: clean");
676                                }
677                            }
678                            Err(_) => {
679                                Output::sub_item("Git Status: unavailable");
680                            }
681                        }
682                    }
683                }
684            }
685            // Add spacing between entries
686        }
687    }
688
689    if let Some(_edit_info) = edit_mode_info {
690        Output::spacing();
691        Output::info("Edit mode active - use 'ca entry status' for details");
692    } else {
693        Output::spacing();
694        Output::tip("Use 'ca entry checkout' to start editing an entry");
695    }
696
697    Ok(())
698}
699
700/// Clear/exit edit mode (useful for recovering from corrupted state)
701async fn clear_edit_mode(skip_confirmation: bool) -> Result<()> {
702    let current_dir = env::current_dir()
703        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
704
705    let repo_root = find_repository_root(&current_dir)
706        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
707
708    let mut manager = StackManager::new(&repo_root)?;
709
710    if !manager.is_in_edit_mode() {
711        Output::info("Not currently in edit mode");
712        return Ok(());
713    }
714
715    // Show current edit mode info
716    if let Some(edit_info) = manager.get_edit_mode_info() {
717        Output::section("Current edit mode state");
718
719        if let Some(target_entry_id) = &edit_info.target_entry_id {
720            Output::sub_item(format!("Target entry: {}", target_entry_id));
721
722            // Try to find the entry
723            if let Some(active_stack) = manager.get_active_stack() {
724                if let Some(entry) = active_stack
725                    .entries
726                    .iter()
727                    .find(|e| e.id == *target_entry_id)
728                {
729                    Output::sub_item(format!("Entry: {}", entry.short_message(50)));
730                } else {
731                    Output::warning("Target entry not found in stack (corrupted state)");
732                }
733            }
734        }
735
736        Output::sub_item(format!(
737            "Original commit: {}",
738            &edit_info.original_commit_hash[..8]
739        ));
740        Output::sub_item(format!(
741            "Started: {}",
742            edit_info.started_at.format("%Y-%m-%d %H:%M:%S")
743        ));
744    }
745
746    // Confirm before clearing
747    if !skip_confirmation {
748        println!();
749        let confirmed = Confirm::with_theme(&ColorfulTheme::default())
750            .with_prompt("Clear edit mode state?")
751            .default(true)
752            .interact()
753            .map_err(|e| CascadeError::config(format!("Failed to get user confirmation: {e}")))?;
754
755        if !confirmed {
756            return Err(CascadeError::config("Operation cancelled."));
757        }
758    }
759
760    // Clear edit mode
761    manager.exit_edit_mode()?;
762
763    Output::success("Edit mode cleared");
764    Output::tip("Use 'ca entry checkout' to start a new edit session");
765
766    Ok(())
767}
768
769/// Amend the current stack entry commit and update working branch
770async fn amend_entry(message: Option<String>, all: bool, push: bool, restack: bool) -> Result<()> {
771    let current_dir = env::current_dir()
772        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
773
774    let repo_root = find_repository_root(&current_dir)
775        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
776
777    let mut manager = StackManager::new(&repo_root)?;
778    let repo = crate::git::GitRepository::open(&repo_root)?;
779
780    let current_branch = repo.get_current_branch()?;
781
782    // Get active stack info we need (clone to avoid borrow issues)
783    let (stack_id, entry_index, entry_id, entry_branch, working_branch, has_dependents, has_pr) = {
784        let active_stack = manager.get_active_stack().ok_or_else(|| {
785            CascadeError::config("No active stack. Create a stack first with 'ca stack create'")
786        })?;
787
788        // Find which entry we're amending (must be on a stack branch)
789        let mut found_entry = None;
790
791        for (idx, entry) in active_stack.entries.iter().enumerate() {
792            if entry.branch == current_branch {
793                found_entry = Some((
794                    idx,
795                    entry.id,
796                    entry.branch.clone(),
797                    entry.pull_request_id.clone(),
798                ));
799                break;
800            }
801        }
802
803        match found_entry {
804            Some((idx, id, branch, pr_id)) => {
805                let has_dependents = idx + 1 < active_stack.entries.len();
806                (
807                    active_stack.id,
808                    idx,
809                    id,
810                    branch,
811                    active_stack.working_branch.clone(),
812                    has_dependents,
813                    pr_id.is_some(),
814                )
815            }
816            None => {
817                return Err(CascadeError::config(format!(
818                    "Current branch '{}' is not a stack entry branch.\n\
819                     Use 'ca entry checkout <N>' to checkout a stack entry first.",
820                    current_branch
821                )));
822            }
823        }
824    };
825
826    Output::section(format!("Amending stack entry #{}", entry_index + 1));
827
828    // 1. Perform the git commit --amend
829    let mut amend_args = vec!["commit", "--amend"];
830
831    if all {
832        amend_args.insert(1, "-a");
833    }
834
835    if let Some(ref msg) = message {
836        amend_args.push("-m");
837        amend_args.push(msg);
838    } else {
839        // Use git editor for interactive message editing
840        amend_args.push("--no-edit");
841    }
842
843    debug!("Running git {}", amend_args.join(" "));
844
845    let output = std::process::Command::new("git")
846        .args(&amend_args)
847        .current_dir(&repo_root)
848        .output()
849        .map_err(CascadeError::Io)?;
850
851    if !output.status.success() {
852        let stderr = String::from_utf8_lossy(&output.stderr);
853        return Err(CascadeError::branch(format!(
854            "Failed to amend commit: {}",
855            stderr.trim()
856        )));
857    }
858
859    Output::success("Commit amended");
860
861    // 2. Get the new commit hash
862    let new_commit_hash = repo.get_head_commit()?.id().to_string();
863    debug!("New commit hash after amend: {}", new_commit_hash);
864
865    // 3. Update stack metadata with new commit hash
866    {
867        let stack = manager
868            .get_stack_mut(&stack_id)
869            .ok_or_else(|| CascadeError::config("Stack not found"))?;
870
871        if let Some(entry) = stack.entries.iter_mut().find(|e| e.id == entry_id) {
872            let old_hash = entry.commit_hash.clone();
873            entry.commit_hash = new_commit_hash.clone();
874            debug!(
875                "Updated entry commit hash: {} -> {}",
876                &old_hash[..8],
877                &new_commit_hash[..8]
878            );
879            Output::sub_item(format!(
880                "Updated metadata: {} → {}",
881                &old_hash[..8],
882                &new_commit_hash[..8]
883            ));
884        }
885    }
886
887    manager.save_to_disk()?;
888
889    // 4. Update working branch to keep safety net in sync
890    if let Some(ref working_branch_name) = working_branch {
891        Output::sub_item(format!("Updating working branch: {}", working_branch_name));
892
893        // Force update the working branch to point to the amended commit
894        repo.update_branch_to_commit(working_branch_name, &new_commit_hash)?;
895
896        Output::success(format!("Working branch '{}' updated", working_branch_name));
897    } else {
898        Output::warning("No working branch found - create one with 'ca stack create' for safety");
899    }
900
901    // 5. Auto-restack dependent entries if requested
902    if restack && has_dependents {
903        println!();
904        Output::section("Auto-restacking dependent entries");
905
906        // Create fresh instances for rebase manager
907        let rebase_manager_stack = StackManager::new(&repo_root)?;
908        let rebase_manager_repo = crate::git::GitRepository::open(&repo_root)?;
909
910        // Use the sync_stack mechanism to rebase dependent entries
911        let mut rebase_manager = crate::stack::RebaseManager::new(
912            rebase_manager_stack,
913            rebase_manager_repo,
914            crate::stack::RebaseOptions {
915                strategy: crate::stack::RebaseStrategy::ForcePush,
916                target_base: Some(entry_branch.clone()),
917                skip_pull: Some(true), // Don't pull, we're rebasing on local changes
918                ..Default::default()
919            },
920        );
921
922        match rebase_manager.rebase_stack(&stack_id) {
923            Ok(_) => {
924                Output::success("Dependent entries restacked");
925            }
926            Err(e) => {
927                Output::warning(format!("Could not auto-restack: {}", e));
928                Output::tip("Run 'ca sync' manually to restack dependent entries");
929            }
930        }
931    }
932
933    // 6. Auto-push if requested and entry has a PR
934    if push {
935        println!();
936
937        if has_pr {
938            Output::section("Force-pushing to remote");
939
940            // Set env var to skip force-push confirmation
941            std::env::set_var("FORCE_PUSH_NO_CONFIRM", "1");
942
943            repo.force_push_branch(&current_branch, &current_branch)?;
944            Output::success(format!("Force-pushed '{}' to remote", current_branch));
945            Output::sub_item("PR will be automatically updated");
946        } else {
947            Output::warning("No PR found for this entry - skipping push");
948            Output::tip("Use 'ca submit' to create a PR");
949        }
950    }
951
952    // Summary
953    println!();
954    Output::section("Summary");
955    Output::bullet(format!(
956        "Amended entry #{} on branch '{}'",
957        entry_index + 1,
958        entry_branch
959    ));
960    if working_branch.is_some() {
961        Output::bullet("Working branch updated");
962    }
963    if restack {
964        Output::bullet("Dependent entries restacked");
965    }
966    if push {
967        Output::bullet("Changes force-pushed to remote");
968    } else {
969        Output::tip("Use --push to automatically force-push after amending");
970    }
971
972    if !restack && has_dependents {
973        Output::tip("Use --restack to automatically update dependent entries");
974    }
975
976    Ok(())
977}