1use crate::cli::output::Output;
2use crate::errors::{CascadeError, Result};
3use crate::git::find_repository_root;
4use crate::stack::StackManager;
5use clap::Subcommand;
6use crossterm::{
7 event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
8 execute,
9 terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
10};
11use dialoguer::{theme::ColorfulTheme, Confirm};
12use ratatui::{
13 backend::CrosstermBackend,
14 layout::{Alignment, Constraint, Direction, Layout},
15 style::{Color, Modifier, Style},
16 text::{Line, Span},
17 widgets::{Block, Borders, List, ListItem, ListState, Paragraph},
18 Terminal,
19};
20use std::env;
21use std::io;
22use tracing::debug;
23
24#[derive(Debug, Subcommand)]
25pub enum EntryAction {
26 Checkout {
28 entry: Option<usize>,
30 #[arg(long)]
32 direct: bool,
33 #[arg(long, short)]
35 yes: bool,
36 },
37 Status {
39 #[arg(long)]
41 quiet: bool,
42 },
43 List {
45 #[arg(long, short)]
47 verbose: bool,
48 },
49 Clear {
51 #[arg(long, short)]
53 yes: bool,
54 },
55 Amend {
57 #[arg(long, short)]
59 message: Option<String>,
60 #[arg(long, short)]
62 all: bool,
63 #[arg(long)]
65 push: bool,
66 #[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
90async 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(¤t_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 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 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_entry_picker(active_stack).await?
132 };
133
134 let target_entry = &active_stack.entries[target_entry_num - 1]; 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 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 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 _ = active_stack;
172
173 if let Some((commit_hash, commit_message)) = edit_mode_display {
175 tracing::debug!("Already in edit mode for entry in stack");
176
177 if !skip_confirmation {
178 Output::warning("Already in edit mode!");
179 Output::sub_item(format!(
180 "Current target: {} ({})",
181 &commit_hash[..8],
182 commit_message
183 ));
184
185 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 Output::info("Exiting current edit mode...");
202 manager.exit_edit_mode()?;
203 Output::success("✓ Exited previous edit mode");
204 }
205 }
206
207 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 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 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 manager.enter_edit_mode(stack_id, entry_id)?;
242
243 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(¤t_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 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
276async fn show_entry_picker(stack: &crate::stack::Stack) -> Result<usize> {
278 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 let chunks = Layout::default()
294 .direction(Direction::Vertical)
295 .margin(2)
296 .constraints(
297 [
298 Constraint::Length(3), Constraint::Min(5), Constraint::Length(3), ]
302 .as_ref(),
303 )
304 .split(size);
305
306 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 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 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 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); }
398 KeyCode::Char('r') => {
399 continue;
401 }
402 _ => {}
403 }
404 }
405 }
406 };
407
408 disable_raw_mode()?;
410 execute!(
411 terminal.backend_mut(),
412 LeaveAlternateScreen,
413 DisableMouseCapture
414 )?;
415 terminal.show_cursor()?;
416
417 result
418}
419
420async 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(¤t_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 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 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 Output::section("Current state");
490
491 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(¤t_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 let current_head = repo.get_current_commit_hash()?;
500 if current_head != edit_info.original_commit_hash {
501 let current_short = ¤t_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 match repo.get_commit_count_between(&edit_info.original_commit_hash, ¤t_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)", ¤t_head[..8]));
514 }
515
516 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
550async 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(¤t_dir)
556 .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
557 let manager = StackManager::new(&repo_root)?;
558
559 let active_stack = manager.get_active_stack().ok_or_else(|| {
560 CascadeError::config(
561 "No active stack. Create a stack first with 'ca stack create'".to_string(),
562 )
563 })?;
564
565 if active_stack.entries.is_empty() {
566 Output::info(format!(
567 "Active stack '{}' has no entries yet",
568 active_stack.name
569 ));
570 Output::sub_item("Add some commits to the stack with 'ca stack push'");
571 return Ok(());
572 }
573
574 Output::section(format!(
575 "Stack: {} ({} entries)",
576 active_stack.name,
577 active_stack.entries.len()
578 ));
579
580 let edit_mode_info = manager.get_edit_mode_info();
581 let edit_target_entry_id = edit_mode_info
582 .as_ref()
583 .and_then(|info| info.target_entry_id);
584
585 for (i, entry) in active_stack.entries.iter().enumerate() {
586 let entry_num = i + 1;
587 let status_label = Output::entry_status(entry.is_submitted, entry.is_merged);
588 let mut entry_line = format!(
589 "{} {} ({})",
590 status_label,
591 entry.short_message(50),
592 entry.short_hash()
593 );
594
595 if let Some(pr_id) = &entry.pull_request_id {
596 entry_line.push_str(&format!(" PR: #{pr_id}"));
597 }
598
599 if Some(entry.id) == edit_target_entry_id {
600 entry_line.push_str(" [edit target]");
601 }
602
603 Output::numbered_item(entry_num, entry_line);
604
605 if verbose {
606 Output::sub_item(format!("Branch: {}", entry.branch));
607 Output::sub_item(format!("Commit: {}", entry.commit_hash));
608 Output::sub_item(format!(
609 "Created: {}",
610 entry.created_at.format("%Y-%m-%d %H:%M:%S")
611 ));
612
613 if entry.is_merged {
614 Output::sub_item("Status: Merged");
615 } else if entry.is_submitted {
616 Output::sub_item("Status: Submitted");
617 } else {
618 Output::sub_item("Status: Draft");
619 }
620
621 Output::sub_item("Message:");
622 for line in entry.message.lines() {
623 Output::sub_item(format!(" {line}"));
624 }
625
626 if Some(entry.id) == edit_target_entry_id {
627 Output::sub_item("Edit mode target");
628
629 match crate::git::GitRepository::open(&repo_root) {
630 Ok(repo) => match repo.get_status_summary() {
631 Ok(status) => {
632 if !status.is_clean() {
633 Output::sub_item("Git Status:");
634 if status.has_staged_changes() {
635 Output::sub_item(format!(
636 " Staged: {} files",
637 status.staged_count()
638 ));
639 }
640 if status.has_unstaged_changes() {
641 Output::sub_item(format!(
642 " Unstaged: {} files",
643 status.unstaged_count()
644 ));
645 }
646 if status.has_untracked_files() {
647 Output::sub_item(format!(
648 " Untracked: {} files",
649 status.untracked_count()
650 ));
651 }
652 } else {
653 Output::sub_item("Git Status: clean");
654 }
655 }
656 Err(_) => {
657 Output::sub_item("Git Status: unavailable");
658 }
659 },
660 Err(_) => {
661 Output::sub_item("Git Status: unavailable");
662 }
663 }
664 }
665 }
666 }
667
668 if edit_mode_info.is_some() {
669 Output::spacing();
670 Output::info("Edit mode active - use 'ca entry status' for details");
671 } else {
672 Output::spacing();
673 Output::tip("Use 'ca entry checkout' to start editing an entry");
674 }
675
676 Ok(())
677}
678
679async fn clear_edit_mode(skip_confirmation: bool) -> Result<()> {
681 let current_dir = env::current_dir()
682 .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
683
684 let repo_root = find_repository_root(¤t_dir)
685 .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
686
687 let mut manager = StackManager::new(&repo_root)?;
688
689 if !manager.is_in_edit_mode() {
690 Output::info("Not currently in edit mode");
691 return Ok(());
692 }
693
694 if let Some(edit_info) = manager.get_edit_mode_info() {
696 Output::section("Current edit mode state");
697
698 if let Some(target_entry_id) = &edit_info.target_entry_id {
699 Output::sub_item(format!("Target entry: {}", target_entry_id));
700
701 if let Some(active_stack) = manager.get_active_stack() {
703 if let Some(entry) = active_stack
704 .entries
705 .iter()
706 .find(|e| e.id == *target_entry_id)
707 {
708 Output::sub_item(format!("Entry: {}", entry.short_message(50)));
709 } else {
710 Output::warning("Target entry not found in stack (corrupted state)");
711 }
712 }
713 }
714
715 Output::sub_item(format!(
716 "Original commit: {}",
717 &edit_info.original_commit_hash[..8]
718 ));
719 Output::sub_item(format!(
720 "Started: {}",
721 edit_info.started_at.format("%Y-%m-%d %H:%M:%S")
722 ));
723 }
724
725 if !skip_confirmation {
727 println!();
728 let confirmed = Confirm::with_theme(&ColorfulTheme::default())
729 .with_prompt("Clear edit mode state?")
730 .default(true)
731 .interact()
732 .map_err(|e| CascadeError::config(format!("Failed to get user confirmation: {e}")))?;
733
734 if !confirmed {
735 return Err(CascadeError::config("Operation cancelled."));
736 }
737 }
738
739 manager.exit_edit_mode()?;
741
742 Output::success("Edit mode cleared");
743 Output::tip("Use 'ca entry checkout' to start a new edit session");
744
745 Ok(())
746}
747
748async fn amend_entry(message: Option<String>, all: bool, push: bool, restack: bool) -> Result<()> {
750 let current_dir = env::current_dir()
751 .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
752
753 let repo_root = find_repository_root(¤t_dir)
754 .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
755
756 let mut manager = StackManager::new(&repo_root)?;
757 let repo = crate::git::GitRepository::open(&repo_root)?;
758
759 let current_branch = repo.get_current_branch()?;
760
761 let (stack_id, entry_index, entry_id, entry_branch, working_branch, has_dependents, has_pr) = {
763 let active_stack = manager.get_active_stack().ok_or_else(|| {
764 CascadeError::config("No active stack. Create a stack first with 'ca stack create'")
765 })?;
766
767 let mut found_entry = None;
769
770 for (idx, entry) in active_stack.entries.iter().enumerate() {
771 if entry.branch == current_branch {
772 found_entry = Some((
773 idx,
774 entry.id,
775 entry.branch.clone(),
776 entry.pull_request_id.clone(),
777 ));
778 break;
779 }
780 }
781
782 match found_entry {
783 Some((idx, id, branch, pr_id)) => {
784 let has_dependents = idx + 1 < active_stack.entries.len();
785 (
786 active_stack.id,
787 idx,
788 id,
789 branch,
790 active_stack.working_branch.clone(),
791 has_dependents,
792 pr_id.is_some(),
793 )
794 }
795 None => {
796 return Err(CascadeError::config(format!(
797 "Current branch '{}' is not a stack entry branch.\n\
798 Use 'ca entry checkout <N>' to checkout a stack entry first.",
799 current_branch
800 )));
801 }
802 }
803 };
804
805 Output::section(format!("Amending stack entry #{}", entry_index + 1));
806
807 let mut amend_args = vec!["commit", "--amend"];
809
810 if all {
811 amend_args.insert(1, "-a");
812 }
813
814 if let Some(ref msg) = message {
815 amend_args.push("-m");
816 amend_args.push(msg);
817 } else {
818 amend_args.push("--no-edit");
820 }
821
822 debug!("Running git {}", amend_args.join(" "));
823
824 let output = std::process::Command::new("git")
826 .args(&amend_args)
827 .env("CASCADE_SKIP_HOOKS", "1")
828 .current_dir(&repo_root)
829 .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::piped()) .output()
832 .map_err(CascadeError::Io)?;
833
834 if !output.status.success() {
835 let stderr = String::from_utf8_lossy(&output.stderr);
836 return Err(CascadeError::branch(format!(
837 "Failed to amend commit: {}",
838 stderr.trim()
839 )));
840 }
841
842 Output::success("Commit amended");
843
844 let new_commit_hash = repo.get_head_commit()?.id().to_string();
846 debug!("New commit hash after amend: {}", new_commit_hash);
847
848 {
850 let stack = manager
851 .get_stack_mut(&stack_id)
852 .ok_or_else(|| CascadeError::config("Stack not found"))?;
853
854 let old_hash = stack
855 .entries
856 .iter()
857 .find(|e| e.id == entry_id)
858 .map(|e| e.commit_hash.clone())
859 .ok_or_else(|| CascadeError::config("Entry not found"))?;
860
861 stack
862 .update_entry_commit_hash(&entry_id, new_commit_hash.clone())
863 .map_err(CascadeError::config)?;
864
865 debug!(
866 "Updated entry commit hash: {} -> {}",
867 &old_hash[..8],
868 &new_commit_hash[..8]
869 );
870 Output::sub_item(format!(
871 "Updated metadata: {} → {}",
872 &old_hash[..8],
873 &new_commit_hash[..8]
874 ));
875 }
876
877 manager.save_to_disk()?;
878
879 if let Some(ref working_branch_name) = working_branch {
881 Output::sub_item(format!("Updating working branch: {}", working_branch_name));
882
883 repo.update_branch_to_commit(working_branch_name, &new_commit_hash)?;
885
886 Output::success(format!("Working branch '{}' updated", working_branch_name));
887 } else {
888 Output::warning("No working branch found - create one with 'ca stack create' for safety");
889 }
890
891 if restack && has_dependents {
893 println!();
894 Output::info("Auto-restacking dependent entries");
895
896 let rebase_manager_stack = StackManager::new(&repo_root)?;
898 let stack_for_count = rebase_manager_stack
899 .get_stack(&stack_id)
900 .ok_or_else(|| CascadeError::config("Stack not found"))?;
901
902 let entry_index = stack_for_count
904 .entries
905 .iter()
906 .position(|e| e.id == entry_id)
907 .unwrap_or(0);
908 let dependent_count = stack_for_count
909 .entries
910 .len()
911 .saturating_sub(entry_index + 1);
912
913 if dependent_count > 0 {
914 let rebase_manager_repo = crate::git::GitRepository::open(&repo_root)?;
915 let plural = if dependent_count == 1 {
916 "entry"
917 } else {
918 "entries"
919 };
920
921 println!(); let rebase_spinner = crate::utils::spinner::Spinner::new_with_output_below(format!(
923 "Restacking {} dependent {}",
924 dependent_count, plural
925 ));
926
927 let mut rebase_manager = crate::stack::RebaseManager::new(
929 rebase_manager_stack,
930 rebase_manager_repo,
931 crate::stack::RebaseOptions {
932 strategy: crate::stack::RebaseStrategy::ForcePush,
933 target_base: Some(entry_branch.clone()),
934 skip_pull: Some(true), progress_printer: Some(rebase_spinner.printer()),
936 ..Default::default()
937 },
938 );
939
940 let rebase_result = rebase_manager.rebase_stack(&stack_id);
941
942 rebase_spinner.stop();
943 println!(); match rebase_result {
946 Ok(_) => {
947 Output::success("Dependent entries restacked");
948 }
949 Err(e) => {
950 Output::warning(format!("Could not auto-restack: {}", e));
951 Output::tip("Run 'ca sync' manually to restack dependent entries");
952 }
953 }
954 }
955 }
956
957 if push {
959 println!();
960
961 if has_pr {
962 Output::section("Force-pushing to remote");
963
964 std::env::set_var("FORCE_PUSH_NO_CONFIRM", "1");
966
967 repo.force_push_branch(¤t_branch, ¤t_branch)?;
968 Output::success(format!("Force-pushed '{}' to remote", current_branch));
969 Output::sub_item("PR will be automatically updated");
970 } else {
971 Output::warning("No PR found for this entry - skipping push");
972 Output::tip("Use 'ca submit' to create a PR");
973 }
974 }
975
976 println!();
978 Output::section("Summary");
979 Output::bullet(format!(
980 "Amended entry #{} on branch '{}'",
981 entry_index + 1,
982 entry_branch
983 ));
984 if working_branch.is_some() {
985 Output::bullet("Working branch updated");
986 }
987 if restack {
988 Output::bullet("Dependent entries restacked");
989 }
990 if push {
991 Output::bullet("Changes force-pushed to remote");
992 }
993
994 if (has_dependents || !push) && !restack {
997 println!();
998 if !push && !restack {
999 Output::tip("Use --push to automatically force-push after amending");
1000 }
1001 if !restack && has_dependents {
1002 Output::tip("Use --restack to automatically update dependent entries");
1003 }
1004 }
1005
1006 Ok(())
1007}