cascade_cli/cli/commands/
entry.rs

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