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