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;
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        tracing::debug!("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
265    // Check if prepare-commit-msg hook is installed
266    let hooks_dir = repo_root.join(".git/hooks");
267    let hook_path = hooks_dir.join("prepare-commit-msg");
268    if !hook_path.exists() {
269        Output::tip("Install the prepare-commit-msg hook for better guidance:");
270        Output::sub_item("ca hooks add prepare-commit-msg");
271    }
272
273    Ok(())
274}
275
276/// Interactive entry picker using TUI
277async fn show_entry_picker(stack: &crate::stack::Stack) -> Result<usize> {
278    // Setup terminal
279    enable_raw_mode()?;
280    let mut stdout = io::stdout();
281    execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
282    let backend = CrosstermBackend::new(stdout);
283    let mut terminal = Terminal::new(backend)?;
284
285    let mut list_state = ListState::default();
286    list_state.select(Some(0));
287
288    let result = loop {
289        terminal.draw(|f| {
290            let size = f.area();
291
292            // Create layout
293            let chunks = Layout::default()
294                .direction(Direction::Vertical)
295                .margin(2)
296                .constraints(
297                    [
298                        Constraint::Length(3), // Title
299                        Constraint::Min(5),    // List
300                        Constraint::Length(3), // Help
301                    ]
302                    .as_ref(),
303                )
304                .split(size);
305
306            // Title
307            let title = Paragraph::new(format!("📚 Select Entry from Stack: {}", stack.name))
308                .style(
309                    Style::default()
310                        .fg(Color::Cyan)
311                        .add_modifier(Modifier::BOLD),
312                )
313                .alignment(Alignment::Center)
314                .block(Block::default().borders(Borders::ALL));
315            f.render_widget(title, chunks[0]);
316
317            // Entry list
318            let items: Vec<ListItem> = stack
319                .entries
320                .iter()
321                .enumerate()
322                .map(|(i, entry)| {
323                    let status_icon = if entry.is_submitted {
324                        if entry.pull_request_id.is_some() {
325                            "📤"
326                        } else {
327                            "📝"
328                        }
329                    } else {
330                        "🔄"
331                    };
332
333                    let pr_text = if let Some(pr_id) = &entry.pull_request_id {
334                        format!(" PR: #{pr_id}")
335                    } else {
336                        "".to_string()
337                    };
338
339                    let line = Line::from(vec![
340                        Span::raw(format!("  {}. ", i + 1)),
341                        Span::raw(status_icon),
342                        Span::raw(" "),
343                        Span::styled(entry.short_message(40), Style::default().fg(Color::White)),
344                        Span::raw(" "),
345                        Span::styled(
346                            format!("({})", entry.short_hash()),
347                            Style::default().fg(Color::Yellow),
348                        ),
349                        Span::styled(pr_text, Style::default().fg(Color::Green)),
350                    ]);
351
352                    ListItem::new(line)
353                })
354                .collect();
355
356            let list = List::new(items)
357                .block(Block::default().borders(Borders::ALL).title("Entries"))
358                .highlight_style(Style::default().fg(Color::Black).bg(Color::Cyan))
359                .highlight_symbol("→ ");
360
361            f.render_stateful_widget(list, chunks[1], &mut list_state);
362
363            // Help text
364            let help = Paragraph::new("↑/↓: Navigate • Enter: Select • q: Quit • r: Refresh")
365                .style(Style::default().fg(Color::DarkGray))
366                .alignment(Alignment::Center)
367                .block(Block::default().borders(Borders::ALL));
368            f.render_widget(help, chunks[2]);
369        })?;
370
371        // Handle input
372        if let Event::Key(key) = event::read()? {
373            if key.kind == KeyEventKind::Press {
374                match key.code {
375                    KeyCode::Char('q') => {
376                        break Err(CascadeError::config("Entry selection cancelled"));
377                    }
378                    KeyCode::Up => {
379                        let selected = list_state.selected().unwrap_or(0);
380                        if selected > 0 {
381                            list_state.select(Some(selected - 1));
382                        } else {
383                            list_state.select(Some(stack.entries.len() - 1));
384                        }
385                    }
386                    KeyCode::Down => {
387                        let selected = list_state.selected().unwrap_or(0);
388                        if selected < stack.entries.len() - 1 {
389                            list_state.select(Some(selected + 1));
390                        } else {
391                            list_state.select(Some(0));
392                        }
393                    }
394                    KeyCode::Enter => {
395                        let selected = list_state.selected().unwrap_or(0);
396                        break Ok(selected + 1); // Convert to 1-based index
397                    }
398                    KeyCode::Char('r') => {
399                        // Refresh - for now just continue the loop
400                        continue;
401                    }
402                    _ => {}
403                }
404            }
405        }
406    };
407
408    // Restore terminal
409    disable_raw_mode()?;
410    execute!(
411        terminal.backend_mut(),
412        LeaveAlternateScreen,
413        DisableMouseCapture
414    )?;
415    terminal.show_cursor()?;
416
417    result
418}
419
420/// Show current edit mode status
421async fn show_edit_status(quiet: bool) -> Result<()> {
422    let current_dir = env::current_dir()
423        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
424
425    let repo_root = find_repository_root(&current_dir)
426        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
427    let manager = StackManager::new(&repo_root)?;
428
429    if !manager.is_in_edit_mode() {
430        if quiet {
431            println!("inactive");
432        } else {
433            Output::info("Not in edit mode");
434            Output::sub_item("Use 'ca entry checkout' to start editing a stack entry");
435        }
436        return Ok(());
437    }
438
439    let edit_info = manager.get_edit_mode_info().unwrap();
440
441    if quiet {
442        println!("active:{:?}", edit_info.target_entry_id);
443        return Ok(());
444    }
445
446    Output::section("Currently in edit mode");
447
448    // Try to get the entry information
449    if let Some(active_stack) = manager.get_active_stack() {
450        if let Some(target_entry_id) = edit_info.target_entry_id {
451            if let Some(entry) = active_stack
452                .entries
453                .iter()
454                .find(|e| e.id == target_entry_id)
455            {
456                Output::sub_item(format!(
457                    "Target entry: {} ({})",
458                    entry.short_hash(),
459                    entry.short_message(50)
460                ));
461                Output::sub_item(format!("Branch: {}", entry.branch));
462
463                // Display full commit message
464                Output::sub_item("Commit Message:");
465                let lines: Vec<&str> = entry.message.lines().collect();
466                for line in lines {
467                    Output::sub_item(format!("  {line}"));
468                }
469            } else {
470                Output::sub_item(format!("Target entry: {target_entry_id:?} (not found)"));
471            }
472        } else {
473            Output::sub_item("Target entry: Unknown");
474        }
475    } else {
476        Output::sub_item(format!("Target entry: {:?}", edit_info.target_entry_id));
477    }
478
479    Output::sub_item(format!(
480        "Original commit: {}",
481        &edit_info.original_commit_hash[..8]
482    ));
483    Output::sub_item(format!(
484        "Started: {}",
485        edit_info.started_at.format("%Y-%m-%d %H:%M:%S")
486    ));
487
488    // Show current Git status
489    Output::section("Current state");
490
491    // Get current repository state
492    let current_dir = env::current_dir()
493        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
494    let repo_root = find_repository_root(&current_dir)
495        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
496    let repo = crate::git::GitRepository::open(&repo_root)?;
497
498    // Current HEAD vs original commit
499    let current_head = repo.get_current_commit_hash()?;
500    if current_head != edit_info.original_commit_hash {
501        let current_short = &current_head[..8];
502        let original_short = &edit_info.original_commit_hash[..8];
503        Output::sub_item(format!("HEAD moved: {original_short} → {current_short}"));
504
505        // Show if there are new commits
506        match repo.get_commit_count_between(&edit_info.original_commit_hash, &current_head) {
507            Ok(count) if count > 0 => {
508                Output::sub_item(format!("  {count} new commit(s) created"));
509            }
510            _ => {}
511        }
512    } else {
513        Output::sub_item(format!("HEAD: {} (unchanged)", &current_head[..8]));
514    }
515
516    // Working directory and staging status
517    match repo.get_status_summary() {
518        Ok(status) => {
519            if status.is_clean() {
520                Output::sub_item("Working directory: clean");
521            } else {
522                if status.has_staged_changes() {
523                    Output::sub_item(format!("Staged changes: {} files", status.staged_count()));
524                }
525                if status.has_unstaged_changes() {
526                    Output::sub_item(format!(
527                        "Unstaged changes: {} files",
528                        status.unstaged_count()
529                    ));
530                }
531                if status.has_untracked_files() {
532                    Output::sub_item(format!(
533                        "Untracked files: {} files",
534                        status.untracked_count()
535                    ));
536                }
537            }
538        }
539        Err(_) => {
540            Output::sub_item("Working directory: status unavailable");
541        }
542    }
543
544    Output::tip("Use 'git status' for detailed file-level status");
545    Output::sub_item("Use 'ca entry list' to see all entries");
546
547    Ok(())
548}
549
550/// List all entries in the stack with edit status
551async fn list_entries(verbose: bool) -> Result<()> {
552    let current_dir = env::current_dir()
553        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
554
555    let repo_root = find_repository_root(&current_dir)
556        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
557    let manager = StackManager::new(&repo_root)?;
558
559    let active_stack = manager.get_active_stack().ok_or_else(|| {
560        CascadeError::config(
561            "No active stack. Create a stack first with 'ca stack create'".to_string(),
562        )
563    })?;
564
565    if active_stack.entries.is_empty() {
566        Output::info(format!(
567            "Active stack '{}' has no entries yet",
568            active_stack.name
569        ));
570        Output::sub_item("Add some commits to the stack with 'ca stack push'");
571        return Ok(());
572    }
573
574    Output::section(format!(
575        "Stack: {} ({} entries)",
576        active_stack.name,
577        active_stack.entries.len()
578    ));
579
580    let edit_mode_info = manager.get_edit_mode_info();
581    let edit_target_entry_id = edit_mode_info
582        .as_ref()
583        .and_then(|info| info.target_entry_id);
584
585    for (i, entry) in active_stack.entries.iter().enumerate() {
586        let entry_num = i + 1;
587        let status_label = Output::entry_status(entry.is_submitted, entry.is_merged);
588        let mut entry_line = format!(
589            "{} {} ({})",
590            status_label,
591            entry.short_message(50),
592            entry.short_hash()
593        );
594
595        if let Some(pr_id) = &entry.pull_request_id {
596            entry_line.push_str(&format!(" PR: #{pr_id}"));
597        }
598
599        if Some(entry.id) == edit_target_entry_id {
600            entry_line.push_str(" [edit target]");
601        }
602
603        Output::numbered_item(entry_num, entry_line);
604
605        if verbose {
606            Output::sub_item(format!("Branch: {}", entry.branch));
607            Output::sub_item(format!("Commit: {}", entry.commit_hash));
608            Output::sub_item(format!(
609                "Created: {}",
610                entry.created_at.format("%Y-%m-%d %H:%M:%S")
611            ));
612
613            if entry.is_merged {
614                Output::sub_item("Status: Merged");
615            } else if entry.is_submitted {
616                Output::sub_item("Status: Submitted");
617            } else {
618                Output::sub_item("Status: Draft");
619            }
620
621            Output::sub_item("Message:");
622            for line in entry.message.lines() {
623                Output::sub_item(format!("  {line}"));
624            }
625
626            if Some(entry.id) == edit_target_entry_id {
627                Output::sub_item("Edit mode target");
628
629                match crate::git::GitRepository::open(&repo_root) {
630                    Ok(repo) => match repo.get_status_summary() {
631                        Ok(status) => {
632                            if !status.is_clean() {
633                                Output::sub_item("Git Status:");
634                                if status.has_staged_changes() {
635                                    Output::sub_item(format!(
636                                        "  Staged: {} files",
637                                        status.staged_count()
638                                    ));
639                                }
640                                if status.has_unstaged_changes() {
641                                    Output::sub_item(format!(
642                                        "  Unstaged: {} files",
643                                        status.unstaged_count()
644                                    ));
645                                }
646                                if status.has_untracked_files() {
647                                    Output::sub_item(format!(
648                                        "  Untracked: {} files",
649                                        status.untracked_count()
650                                    ));
651                                }
652                            } else {
653                                Output::sub_item("Git Status: clean");
654                            }
655                        }
656                        Err(_) => {
657                            Output::sub_item("Git Status: unavailable");
658                        }
659                    },
660                    Err(_) => {
661                        Output::sub_item("Git Status: unavailable");
662                    }
663                }
664            }
665        }
666    }
667
668    if edit_mode_info.is_some() {
669        Output::spacing();
670        Output::info("Edit mode active - use 'ca entry status' for details");
671    } else {
672        Output::spacing();
673        Output::tip("Use 'ca entry checkout' to start editing an entry");
674    }
675
676    Ok(())
677}
678
679/// Clear/exit edit mode (useful for recovering from corrupted state)
680async fn clear_edit_mode(skip_confirmation: bool) -> Result<()> {
681    let current_dir = env::current_dir()
682        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
683
684    let repo_root = find_repository_root(&current_dir)
685        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
686
687    let mut manager = StackManager::new(&repo_root)?;
688
689    if !manager.is_in_edit_mode() {
690        Output::info("Not currently in edit mode");
691        return Ok(());
692    }
693
694    // Show current edit mode info
695    if let Some(edit_info) = manager.get_edit_mode_info() {
696        Output::section("Current edit mode state");
697
698        if let Some(target_entry_id) = &edit_info.target_entry_id {
699            Output::sub_item(format!("Target entry: {}", target_entry_id));
700
701            // Try to find the entry
702            if let Some(active_stack) = manager.get_active_stack() {
703                if let Some(entry) = active_stack
704                    .entries
705                    .iter()
706                    .find(|e| e.id == *target_entry_id)
707                {
708                    Output::sub_item(format!("Entry: {}", entry.short_message(50)));
709                } else {
710                    Output::warning("Target entry not found in stack (corrupted state)");
711                }
712            }
713        }
714
715        Output::sub_item(format!(
716            "Original commit: {}",
717            &edit_info.original_commit_hash[..8]
718        ));
719        Output::sub_item(format!(
720            "Started: {}",
721            edit_info.started_at.format("%Y-%m-%d %H:%M:%S")
722        ));
723    }
724
725    // Confirm before clearing
726    if !skip_confirmation {
727        println!();
728        let confirmed = Confirm::with_theme(&ColorfulTheme::default())
729            .with_prompt("Clear edit mode state?")
730            .default(true)
731            .interact()
732            .map_err(|e| CascadeError::config(format!("Failed to get user confirmation: {e}")))?;
733
734        if !confirmed {
735            return Err(CascadeError::config("Operation cancelled."));
736        }
737    }
738
739    // Clear edit mode
740    manager.exit_edit_mode()?;
741
742    Output::success("Edit mode cleared");
743    Output::tip("Use 'ca entry checkout' to start a new edit session");
744
745    Ok(())
746}
747
748/// Amend the current stack entry commit and update working branch
749async fn amend_entry(message: Option<String>, all: bool, push: bool, restack: bool) -> Result<()> {
750    let current_dir = env::current_dir()
751        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
752
753    let repo_root = find_repository_root(&current_dir)
754        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
755
756    let mut manager = StackManager::new(&repo_root)?;
757    let repo = crate::git::GitRepository::open(&repo_root)?;
758
759    let current_branch = repo.get_current_branch()?;
760
761    // Get active stack info we need (clone to avoid borrow issues)
762    let (stack_id, entry_index, entry_id, entry_branch, working_branch, has_dependents, has_pr) = {
763        let active_stack = manager.get_active_stack().ok_or_else(|| {
764            CascadeError::config("No active stack. Create a stack first with 'ca stack create'")
765        })?;
766
767        // Find which entry we're amending (must be on a stack branch)
768        let mut found_entry = None;
769
770        for (idx, entry) in active_stack.entries.iter().enumerate() {
771            if entry.branch == current_branch {
772                found_entry = Some((
773                    idx,
774                    entry.id,
775                    entry.branch.clone(),
776                    entry.pull_request_id.clone(),
777                ));
778                break;
779            }
780        }
781
782        match found_entry {
783            Some((idx, id, branch, pr_id)) => {
784                let has_dependents = idx + 1 < active_stack.entries.len();
785                (
786                    active_stack.id,
787                    idx,
788                    id,
789                    branch,
790                    active_stack.working_branch.clone(),
791                    has_dependents,
792                    pr_id.is_some(),
793                )
794            }
795            None => {
796                return Err(CascadeError::config(format!(
797                    "Current branch '{}' is not a stack entry branch.\n\
798                     Use 'ca entry checkout <N>' to checkout a stack entry first.",
799                    current_branch
800                )));
801            }
802        }
803    };
804
805    Output::section(format!("Amending stack entry #{}", entry_index + 1));
806
807    // 1. Perform the git commit --amend
808    let mut amend_args = vec!["commit", "--amend"];
809
810    if all {
811        amend_args.insert(1, "-a");
812    }
813
814    if let Some(ref msg) = message {
815        amend_args.push("-m");
816        amend_args.push(msg);
817    } else {
818        // Use git editor for interactive message editing
819        amend_args.push("--no-edit");
820    }
821
822    debug!("Running git {}", amend_args.join(" "));
823
824    // Set environment variable to bypass pre-commit hook (avoid infinite loop)
825    let output = std::process::Command::new("git")
826        .args(&amend_args)
827        .env("CASCADE_SKIP_HOOKS", "1")
828        .current_dir(&repo_root)
829        .stdout(std::process::Stdio::null()) // Suppress Git's output
830        .stderr(std::process::Stdio::piped()) // Capture errors
831        .output()
832        .map_err(CascadeError::Io)?;
833
834    if !output.status.success() {
835        let stderr = String::from_utf8_lossy(&output.stderr);
836        return Err(CascadeError::branch(format!(
837            "Failed to amend commit: {}",
838            stderr.trim()
839        )));
840    }
841
842    Output::success("Commit amended");
843
844    // 2. Get the new commit hash
845    let new_commit_hash = repo.get_head_commit()?.id().to_string();
846    debug!("New commit hash after amend: {}", new_commit_hash);
847
848    // 3. Update stack metadata with new commit hash using safe wrapper
849    {
850        let stack = manager
851            .get_stack_mut(&stack_id)
852            .ok_or_else(|| CascadeError::config("Stack not found"))?;
853
854        let old_hash = stack
855            .entries
856            .iter()
857            .find(|e| e.id == entry_id)
858            .map(|e| e.commit_hash.clone())
859            .ok_or_else(|| CascadeError::config("Entry not found"))?;
860
861        stack
862            .update_entry_commit_hash(&entry_id, new_commit_hash.clone())
863            .map_err(CascadeError::config)?;
864
865        debug!(
866            "Updated entry commit hash: {} -> {}",
867            &old_hash[..8],
868            &new_commit_hash[..8]
869        );
870        Output::sub_item(format!(
871            "Updated metadata: {} → {}",
872            &old_hash[..8],
873            &new_commit_hash[..8]
874        ));
875    }
876
877    manager.save_to_disk()?;
878
879    // 4. Update working branch to keep safety net in sync
880    if let Some(ref working_branch_name) = working_branch {
881        Output::sub_item(format!("Updating working branch: {}", working_branch_name));
882
883        // Force update the working branch to point to the amended commit
884        repo.update_branch_to_commit(working_branch_name, &new_commit_hash)?;
885
886        Output::success(format!("Working branch '{}' updated", working_branch_name));
887    } else {
888        Output::warning("No working branch found - create one with 'ca stack create' for safety");
889    }
890
891    // 5. Auto-restack dependent entries if requested
892    if restack && has_dependents {
893        println!();
894        Output::info("Auto-restacking dependent entries");
895
896        // Create fresh instances for rebase manager
897        let rebase_manager_stack = StackManager::new(&repo_root)?;
898        let stack_for_count = rebase_manager_stack
899            .get_stack(&stack_id)
900            .ok_or_else(|| CascadeError::config("Stack not found"))?;
901
902        // Count dependent entries (entries after the current one)
903        let entry_index = stack_for_count
904            .entries
905            .iter()
906            .position(|e| e.id == entry_id)
907            .unwrap_or(0);
908        let dependent_count = stack_for_count
909            .entries
910            .len()
911            .saturating_sub(entry_index + 1);
912
913        if dependent_count > 0 {
914            let rebase_manager_repo = crate::git::GitRepository::open(&repo_root)?;
915            let plural = if dependent_count == 1 {
916                "entry"
917            } else {
918                "entries"
919            };
920
921            println!(); // Spacing
922            let rebase_spinner = crate::utils::spinner::Spinner::new_with_output_below(format!(
923                "Restacking {} dependent {}",
924                dependent_count, plural
925            ));
926
927            // Use the sync_stack mechanism to rebase dependent entries
928            let mut rebase_manager = crate::stack::RebaseManager::new(
929                rebase_manager_stack,
930                rebase_manager_repo,
931                crate::stack::RebaseOptions {
932                    strategy: crate::stack::RebaseStrategy::ForcePush,
933                    target_base: Some(entry_branch.clone()),
934                    skip_pull: Some(true), // Don't pull, we're rebasing on local changes
935                    progress_printer: Some(rebase_spinner.printer()),
936                    ..Default::default()
937                },
938            );
939
940            let rebase_result = rebase_manager.rebase_stack(&stack_id);
941
942            rebase_spinner.stop();
943            println!(); // Spacing
944
945            match rebase_result {
946                Ok(_) => {
947                    Output::success("Dependent entries restacked");
948                }
949                Err(e) => {
950                    Output::warning(format!("Could not auto-restack: {}", e));
951                    Output::tip("Run 'ca sync' manually to restack dependent entries");
952                }
953            }
954        }
955    }
956
957    // 6. Auto-push if requested and entry has a PR
958    if push {
959        println!();
960
961        if has_pr {
962            Output::section("Force-pushing to remote");
963
964            // Set env var to skip force-push confirmation
965            std::env::set_var("FORCE_PUSH_NO_CONFIRM", "1");
966
967            repo.force_push_branch(&current_branch, &current_branch)?;
968            Output::success(format!("Force-pushed '{}' to remote", current_branch));
969            Output::sub_item("PR will be automatically updated");
970        } else {
971            Output::warning("No PR found for this entry - skipping push");
972            Output::tip("Use 'ca submit' to create a PR");
973        }
974    }
975
976    // Summary
977    println!();
978    Output::section("Summary");
979    Output::bullet(format!(
980        "Amended entry #{} on branch '{}'",
981        entry_index + 1,
982        entry_branch
983    ));
984    if working_branch.is_some() {
985        Output::bullet("Working branch updated");
986    }
987    if restack {
988        Output::bullet("Dependent entries restacked");
989    }
990    if push {
991        Output::bullet("Changes force-pushed to remote");
992    }
993
994    // Tips section (separated from summary)
995    // Note: restack auto-pushes, so don't show push tip if restack is enabled
996    if (has_dependents || !push) && !restack {
997        println!();
998        if !push && !restack {
999            Output::tip("Use --push to automatically force-push after amending");
1000        }
1001        if !restack && has_dependents {
1002            Output::tip("Use --restack to automatically update dependent entries");
1003        }
1004    }
1005
1006    Ok(())
1007}