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
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
582    for (i, entry) in active_stack.entries.iter().enumerate() {
583        let entry_num = i + 1;
584
585        // Status icon
586        let status_icon = if entry.is_submitted {
587            if entry.pull_request_id.is_some() {
588                "📤"
589            } else {
590                "📝"
591            }
592        } else {
593            "🔄"
594        };
595
596        // Edit mode indicator
597        let edit_indicator = if edit_mode_info.is_some()
598            && edit_mode_info.unwrap().target_entry_id == Some(entry.id)
599        {
600            " 🎯"
601        } else {
602            ""
603        };
604
605        // Basic entry line
606        print!(
607            "   {}. {} {} ({})",
608            entry_num,
609            status_icon,
610            entry.short_message(50),
611            entry.short_hash()
612        );
613
614        // PR information
615        if let Some(pr_id) = &entry.pull_request_id {
616            print!(" PR: #{pr_id}");
617        }
618
619        print!("{edit_indicator}");
620        println!(); // Line break for entry
621
622        // Verbose information
623        if verbose {
624            Output::sub_item(format!("Branch: {}", entry.branch));
625            Output::sub_item(format!("Commit: {}", entry.commit_hash));
626            Output::sub_item(format!(
627                "Created: {}",
628                entry.created_at.format("%Y-%m-%d %H:%M:%S")
629            ));
630            if entry.is_submitted {
631                Output::sub_item("Status: Submitted");
632            } else {
633                Output::sub_item("Status: Draft");
634            }
635
636            // Display full commit message
637            Output::sub_item("Message:");
638            let lines: Vec<&str> = entry.message.lines().collect();
639            for line in lines {
640                Output::sub_item(format!("  {line}"));
641            }
642
643            // Add Git status info for entry in edit mode
644            if edit_mode_info.is_some() && edit_mode_info.unwrap().target_entry_id == Some(entry.id)
645            {
646                if let Ok(repo_root) = find_repository_root(&env::current_dir().unwrap_or_default())
647                {
648                    if let Ok(repo) = crate::git::GitRepository::open(&repo_root) {
649                        match repo.get_status_summary() {
650                            Ok(status) => {
651                                if !status.is_clean() {
652                                    Output::sub_item("Git Status:");
653                                    if status.has_staged_changes() {
654                                        Output::sub_item(format!(
655                                            "  Staged: {} files",
656                                            status.staged_count()
657                                        ));
658                                    }
659                                    if status.has_unstaged_changes() {
660                                        Output::sub_item(format!(
661                                            "  Unstaged: {} files",
662                                            status.unstaged_count()
663                                        ));
664                                    }
665                                    if status.has_untracked_files() {
666                                        Output::sub_item(format!(
667                                            "  Untracked: {} files",
668                                            status.untracked_count()
669                                        ));
670                                    }
671                                } else {
672                                    Output::sub_item("Git Status: clean");
673                                }
674                            }
675                            Err(_) => {
676                                Output::sub_item("Git Status: unavailable");
677                            }
678                        }
679                    }
680                }
681            }
682            // Add spacing between entries
683        }
684    }
685
686    if let Some(_edit_info) = edit_mode_info {
687        Output::spacing();
688        Output::info("Edit mode active - use 'ca entry status' for details");
689    } else {
690        Output::spacing();
691        Output::tip("Use 'ca entry checkout' to start editing an entry");
692    }
693
694    Ok(())
695}
696
697/// Clear/exit edit mode (useful for recovering from corrupted state)
698async fn clear_edit_mode(skip_confirmation: bool) -> Result<()> {
699    let current_dir = env::current_dir()
700        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
701
702    let repo_root = find_repository_root(&current_dir)
703        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
704
705    let mut manager = StackManager::new(&repo_root)?;
706
707    if !manager.is_in_edit_mode() {
708        Output::info("Not currently in edit mode");
709        return Ok(());
710    }
711
712    // Show current edit mode info
713    if let Some(edit_info) = manager.get_edit_mode_info() {
714        Output::section("Current edit mode state");
715
716        if let Some(target_entry_id) = &edit_info.target_entry_id {
717            Output::sub_item(format!("Target entry: {}", target_entry_id));
718
719            // Try to find the entry
720            if let Some(active_stack) = manager.get_active_stack() {
721                if let Some(entry) = active_stack
722                    .entries
723                    .iter()
724                    .find(|e| e.id == *target_entry_id)
725                {
726                    Output::sub_item(format!("Entry: {}", entry.short_message(50)));
727                } else {
728                    Output::warning("Target entry not found in stack (corrupted state)");
729                }
730            }
731        }
732
733        Output::sub_item(format!(
734            "Original commit: {}",
735            &edit_info.original_commit_hash[..8]
736        ));
737        Output::sub_item(format!(
738            "Started: {}",
739            edit_info.started_at.format("%Y-%m-%d %H:%M:%S")
740        ));
741    }
742
743    // Confirm before clearing
744    if !skip_confirmation {
745        println!();
746        let confirmed = Confirm::with_theme(&ColorfulTheme::default())
747            .with_prompt("Clear edit mode state?")
748            .default(true)
749            .interact()
750            .map_err(|e| CascadeError::config(format!("Failed to get user confirmation: {e}")))?;
751
752        if !confirmed {
753            return Err(CascadeError::config("Operation cancelled."));
754        }
755    }
756
757    // Clear edit mode
758    manager.exit_edit_mode()?;
759
760    Output::success("Edit mode cleared");
761    Output::tip("Use 'ca entry checkout' to start a new edit session");
762
763    Ok(())
764}
765
766/// Amend the current stack entry commit and update working branch
767async fn amend_entry(message: Option<String>, all: bool, push: bool, restack: bool) -> Result<()> {
768    let current_dir = env::current_dir()
769        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
770
771    let repo_root = find_repository_root(&current_dir)
772        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
773
774    let mut manager = StackManager::new(&repo_root)?;
775    let repo = crate::git::GitRepository::open(&repo_root)?;
776
777    let current_branch = repo.get_current_branch()?;
778
779    // Get active stack info we need (clone to avoid borrow issues)
780    let (stack_id, entry_index, entry_id, entry_branch, working_branch, has_dependents, has_pr) = {
781        let active_stack = manager.get_active_stack().ok_or_else(|| {
782            CascadeError::config("No active stack. Create a stack first with 'ca stack create'")
783        })?;
784
785        // Find which entry we're amending (must be on a stack branch)
786        let mut found_entry = None;
787
788        for (idx, entry) in active_stack.entries.iter().enumerate() {
789            if entry.branch == current_branch {
790                found_entry = Some((
791                    idx,
792                    entry.id,
793                    entry.branch.clone(),
794                    entry.pull_request_id.clone(),
795                ));
796                break;
797            }
798        }
799
800        match found_entry {
801            Some((idx, id, branch, pr_id)) => {
802                let has_dependents = idx + 1 < active_stack.entries.len();
803                (
804                    active_stack.id,
805                    idx,
806                    id,
807                    branch,
808                    active_stack.working_branch.clone(),
809                    has_dependents,
810                    pr_id.is_some(),
811                )
812            }
813            None => {
814                return Err(CascadeError::config(format!(
815                    "Current branch '{}' is not a stack entry branch.\n\
816                     Use 'ca entry checkout <N>' to checkout a stack entry first.",
817                    current_branch
818                )));
819            }
820        }
821    };
822
823    Output::section(format!("Amending stack entry #{}", entry_index + 1));
824
825    // 1. Perform the git commit --amend
826    let mut amend_args = vec!["commit", "--amend"];
827
828    if all {
829        amend_args.insert(1, "-a");
830    }
831
832    if let Some(ref msg) = message {
833        amend_args.push("-m");
834        amend_args.push(msg);
835    } else {
836        // Use git editor for interactive message editing
837        amend_args.push("--no-edit");
838    }
839
840    debug!("Running git {}", amend_args.join(" "));
841
842    let output = std::process::Command::new("git")
843        .args(&amend_args)
844        .current_dir(&repo_root)
845        .output()
846        .map_err(CascadeError::Io)?;
847
848    if !output.status.success() {
849        let stderr = String::from_utf8_lossy(&output.stderr);
850        return Err(CascadeError::branch(format!(
851            "Failed to amend commit: {}",
852            stderr.trim()
853        )));
854    }
855
856    Output::success("Commit amended");
857
858    // 2. Get the new commit hash
859    let new_commit_hash = repo.get_head_commit()?.id().to_string();
860    debug!("New commit hash after amend: {}", new_commit_hash);
861
862    // 3. Update stack metadata with new commit hash
863    {
864        let stack = manager
865            .get_stack_mut(&stack_id)
866            .ok_or_else(|| CascadeError::config("Stack not found"))?;
867
868        if let Some(entry) = stack.entries.iter_mut().find(|e| e.id == entry_id) {
869            let old_hash = entry.commit_hash.clone();
870            entry.commit_hash = new_commit_hash.clone();
871            debug!(
872                "Updated entry commit hash: {} -> {}",
873                &old_hash[..8],
874                &new_commit_hash[..8]
875            );
876            Output::sub_item(format!(
877                "Updated metadata: {} → {}",
878                &old_hash[..8],
879                &new_commit_hash[..8]
880            ));
881        }
882    }
883
884    manager.save_to_disk()?;
885
886    // 4. Update working branch to keep safety net in sync
887    if let Some(ref working_branch_name) = working_branch {
888        Output::sub_item(format!("Updating working branch: {}", working_branch_name));
889
890        // Force update the working branch to point to the amended commit
891        repo.update_branch_to_commit(working_branch_name, &new_commit_hash)?;
892
893        Output::success(format!("Working branch '{}' updated", working_branch_name));
894    } else {
895        Output::warning("No working branch found - create one with 'ca stack create' for safety");
896    }
897
898    // 5. Auto-restack dependent entries if requested
899    if restack && has_dependents {
900        println!();
901        Output::section("Auto-restacking dependent entries");
902
903        // Create fresh instances for rebase manager
904        let rebase_manager_stack = StackManager::new(&repo_root)?;
905        let rebase_manager_repo = crate::git::GitRepository::open(&repo_root)?;
906
907        // Use the sync_stack mechanism to rebase dependent entries
908        let mut rebase_manager = crate::stack::RebaseManager::new(
909            rebase_manager_stack,
910            rebase_manager_repo,
911            crate::stack::RebaseOptions {
912                strategy: crate::stack::RebaseStrategy::ForcePush,
913                target_base: Some(entry_branch.clone()),
914                skip_pull: Some(true), // Don't pull, we're rebasing on local changes
915                ..Default::default()
916            },
917        );
918
919        match rebase_manager.rebase_stack(&stack_id) {
920            Ok(_) => {
921                Output::success("Dependent entries restacked");
922            }
923            Err(e) => {
924                Output::warning(format!("Could not auto-restack: {}", e));
925                Output::tip("Run 'ca sync' manually to restack dependent entries");
926            }
927        }
928    }
929
930    // 6. Auto-push if requested and entry has a PR
931    if push {
932        println!();
933
934        if has_pr {
935            Output::section("Force-pushing to remote");
936
937            // Set env var to skip force-push confirmation
938            std::env::set_var("FORCE_PUSH_NO_CONFIRM", "1");
939
940            repo.force_push_branch(&current_branch, &current_branch)?;
941            Output::success(format!("Force-pushed '{}' to remote", current_branch));
942            Output::sub_item("PR will be automatically updated");
943        } else {
944            Output::warning("No PR found for this entry - skipping push");
945            Output::tip("Use 'ca submit' to create a PR");
946        }
947    }
948
949    // Summary
950    println!();
951    Output::section("Summary");
952    Output::bullet(format!(
953        "Amended entry #{} on branch '{}'",
954        entry_index + 1,
955        entry_branch
956    ));
957    if working_branch.is_some() {
958        Output::bullet("Working branch updated");
959    }
960    if restack {
961        Output::bullet("Dependent entries restacked");
962    }
963    if push {
964        Output::bullet("Changes force-pushed to remote");
965    } else {
966        Output::tip("Use --push to automatically force-push after amending");
967    }
968
969    if !restack && has_dependents {
970        Output::tip("Use --restack to automatically update dependent entries");
971    }
972
973    Ok(())
974}