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