1use crate::cli::output::Output;
2use crate::errors::{CascadeError, Result};
3use crate::git::find_repository_root;
4use crate::stack::StackManager;
5use clap::Subcommand;
6use crossterm::{
7 event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
8 execute,
9 terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
10};
11use dialoguer::{theme::ColorfulTheme, Confirm};
12use ratatui::{
13 backend::CrosstermBackend,
14 layout::{Alignment, Constraint, Direction, Layout},
15 style::{Color, Modifier, Style},
16 text::{Line, Span},
17 widgets::{Block, Borders, List, ListItem, ListState, Paragraph},
18 Terminal,
19};
20use std::env;
21use std::io;
22use tracing::{debug, warn};
23
24#[derive(Debug, Subcommand)]
25pub enum EntryAction {
26 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 warn!("Already in edit mode for entry in stack");
176
177 if !skip_confirmation {
178 Output::warning("Already in edit mode!");
179 Output::sub_item(format!(
180 "Current target: {} ({})",
181 &commit_hash[..8],
182 commit_message
183 ));
184
185 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
582 for (i, entry) in active_stack.entries.iter().enumerate() {
583 let entry_num = i + 1;
584
585 let status_icon = if entry.is_submitted {
587 if entry.pull_request_id.is_some() {
588 "📤"
589 } else {
590 "📝"
591 }
592 } else {
593 "🔄"
594 };
595
596 let edit_indicator = if edit_mode_info.is_some()
598 && edit_mode_info.unwrap().target_entry_id == Some(entry.id)
599 {
600 " 🎯"
601 } else {
602 ""
603 };
604
605 print!(
607 " {}. {} {} ({})",
608 entry_num,
609 status_icon,
610 entry.short_message(50),
611 entry.short_hash()
612 );
613
614 if let Some(pr_id) = &entry.pull_request_id {
616 print!(" PR: #{pr_id}");
617 }
618
619 print!("{edit_indicator}");
620 println!(); if verbose {
624 Output::sub_item(format!("Branch: {}", entry.branch));
625 Output::sub_item(format!("Commit: {}", entry.commit_hash));
626 Output::sub_item(format!(
627 "Created: {}",
628 entry.created_at.format("%Y-%m-%d %H:%M:%S")
629 ));
630 if entry.is_submitted {
631 Output::sub_item("Status: Submitted");
632 } else {
633 Output::sub_item("Status: Draft");
634 }
635
636 Output::sub_item("Message:");
638 let lines: Vec<&str> = entry.message.lines().collect();
639 for line in lines {
640 Output::sub_item(format!(" {line}"));
641 }
642
643 if edit_mode_info.is_some() && edit_mode_info.unwrap().target_entry_id == Some(entry.id)
645 {
646 if let Ok(repo_root) = find_repository_root(&env::current_dir().unwrap_or_default())
647 {
648 if let Ok(repo) = crate::git::GitRepository::open(&repo_root) {
649 match repo.get_status_summary() {
650 Ok(status) => {
651 if !status.is_clean() {
652 Output::sub_item("Git Status:");
653 if status.has_staged_changes() {
654 Output::sub_item(format!(
655 " Staged: {} files",
656 status.staged_count()
657 ));
658 }
659 if status.has_unstaged_changes() {
660 Output::sub_item(format!(
661 " Unstaged: {} files",
662 status.unstaged_count()
663 ));
664 }
665 if status.has_untracked_files() {
666 Output::sub_item(format!(
667 " Untracked: {} files",
668 status.untracked_count()
669 ));
670 }
671 } else {
672 Output::sub_item("Git Status: clean");
673 }
674 }
675 Err(_) => {
676 Output::sub_item("Git Status: unavailable");
677 }
678 }
679 }
680 }
681 }
682 }
684 }
685
686 if let Some(_edit_info) = edit_mode_info {
687 Output::spacing();
688 Output::info("Edit mode active - use 'ca entry status' for details");
689 } else {
690 Output::spacing();
691 Output::tip("Use 'ca entry checkout' to start editing an entry");
692 }
693
694 Ok(())
695}
696
697async fn clear_edit_mode(skip_confirmation: bool) -> Result<()> {
699 let current_dir = env::current_dir()
700 .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
701
702 let repo_root = find_repository_root(¤t_dir)
703 .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
704
705 let mut manager = StackManager::new(&repo_root)?;
706
707 if !manager.is_in_edit_mode() {
708 Output::info("Not currently in edit mode");
709 return Ok(());
710 }
711
712 if let Some(edit_info) = manager.get_edit_mode_info() {
714 Output::section("Current edit mode state");
715
716 if let Some(target_entry_id) = &edit_info.target_entry_id {
717 Output::sub_item(format!("Target entry: {}", target_entry_id));
718
719 if let Some(active_stack) = manager.get_active_stack() {
721 if let Some(entry) = active_stack
722 .entries
723 .iter()
724 .find(|e| e.id == *target_entry_id)
725 {
726 Output::sub_item(format!("Entry: {}", entry.short_message(50)));
727 } else {
728 Output::warning("Target entry not found in stack (corrupted state)");
729 }
730 }
731 }
732
733 Output::sub_item(format!(
734 "Original commit: {}",
735 &edit_info.original_commit_hash[..8]
736 ));
737 Output::sub_item(format!(
738 "Started: {}",
739 edit_info.started_at.format("%Y-%m-%d %H:%M:%S")
740 ));
741 }
742
743 if !skip_confirmation {
745 println!();
746 let confirmed = Confirm::with_theme(&ColorfulTheme::default())
747 .with_prompt("Clear edit mode state?")
748 .default(true)
749 .interact()
750 .map_err(|e| CascadeError::config(format!("Failed to get user confirmation: {e}")))?;
751
752 if !confirmed {
753 return Err(CascadeError::config("Operation cancelled."));
754 }
755 }
756
757 manager.exit_edit_mode()?;
759
760 Output::success("Edit mode cleared");
761 Output::tip("Use 'ca entry checkout' to start a new edit session");
762
763 Ok(())
764}
765
766async fn amend_entry(message: Option<String>, all: bool, push: bool, restack: bool) -> Result<()> {
768 let current_dir = env::current_dir()
769 .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
770
771 let repo_root = find_repository_root(¤t_dir)
772 .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
773
774 let mut manager = StackManager::new(&repo_root)?;
775 let repo = crate::git::GitRepository::open(&repo_root)?;
776
777 let current_branch = repo.get_current_branch()?;
778
779 let (stack_id, entry_index, entry_id, entry_branch, working_branch, has_dependents, has_pr) = {
781 let active_stack = manager.get_active_stack().ok_or_else(|| {
782 CascadeError::config("No active stack. Create a stack first with 'ca stack create'")
783 })?;
784
785 let mut found_entry = None;
787
788 for (idx, entry) in active_stack.entries.iter().enumerate() {
789 if entry.branch == current_branch {
790 found_entry = Some((
791 idx,
792 entry.id,
793 entry.branch.clone(),
794 entry.pull_request_id.clone(),
795 ));
796 break;
797 }
798 }
799
800 match found_entry {
801 Some((idx, id, branch, pr_id)) => {
802 let has_dependents = idx + 1 < active_stack.entries.len();
803 (
804 active_stack.id,
805 idx,
806 id,
807 branch,
808 active_stack.working_branch.clone(),
809 has_dependents,
810 pr_id.is_some(),
811 )
812 }
813 None => {
814 return Err(CascadeError::config(format!(
815 "Current branch '{}' is not a stack entry branch.\n\
816 Use 'ca entry checkout <N>' to checkout a stack entry first.",
817 current_branch
818 )));
819 }
820 }
821 };
822
823 Output::section(format!("Amending stack entry #{}", entry_index + 1));
824
825 let mut amend_args = vec!["commit", "--amend"];
827
828 if all {
829 amend_args.insert(1, "-a");
830 }
831
832 if let Some(ref msg) = message {
833 amend_args.push("-m");
834 amend_args.push(msg);
835 } else {
836 amend_args.push("--no-edit");
838 }
839
840 debug!("Running git {}", amend_args.join(" "));
841
842 let output = std::process::Command::new("git")
844 .args(&amend_args)
845 .env("CASCADE_SKIP_HOOKS", "1")
846 .current_dir(&repo_root)
847 .output()
848 .map_err(CascadeError::Io)?;
849
850 if !output.status.success() {
851 let stderr = String::from_utf8_lossy(&output.stderr);
852 return Err(CascadeError::branch(format!(
853 "Failed to amend commit: {}",
854 stderr.trim()
855 )));
856 }
857
858 Output::success("Commit amended");
859
860 let new_commit_hash = repo.get_head_commit()?.id().to_string();
862 debug!("New commit hash after amend: {}", new_commit_hash);
863
864 {
866 let stack = manager
867 .get_stack_mut(&stack_id)
868 .ok_or_else(|| CascadeError::config("Stack not found"))?;
869
870 if let Some(entry) = stack.entries.iter_mut().find(|e| e.id == entry_id) {
871 let old_hash = entry.commit_hash.clone();
872 entry.commit_hash = new_commit_hash.clone();
873 debug!(
874 "Updated entry commit hash: {} -> {}",
875 &old_hash[..8],
876 &new_commit_hash[..8]
877 );
878 Output::sub_item(format!(
879 "Updated metadata: {} → {}",
880 &old_hash[..8],
881 &new_commit_hash[..8]
882 ));
883 }
884 }
885
886 manager.save_to_disk()?;
887
888 if let Some(ref working_branch_name) = working_branch {
890 Output::sub_item(format!("Updating working branch: {}", working_branch_name));
891
892 repo.update_branch_to_commit(working_branch_name, &new_commit_hash)?;
894
895 Output::success(format!("Working branch '{}' updated", working_branch_name));
896 } else {
897 Output::warning("No working branch found - create one with 'ca stack create' for safety");
898 }
899
900 if restack && has_dependents {
902 println!();
903 Output::section("Auto-restacking dependent entries");
904
905 let rebase_manager_stack = StackManager::new(&repo_root)?;
907 let rebase_manager_repo = crate::git::GitRepository::open(&repo_root)?;
908
909 let mut rebase_manager = crate::stack::RebaseManager::new(
911 rebase_manager_stack,
912 rebase_manager_repo,
913 crate::stack::RebaseOptions {
914 strategy: crate::stack::RebaseStrategy::ForcePush,
915 target_base: Some(entry_branch.clone()),
916 skip_pull: Some(true), ..Default::default()
918 },
919 );
920
921 match rebase_manager.rebase_stack(&stack_id) {
922 Ok(_) => {
923 Output::success("Dependent entries restacked");
924 }
925 Err(e) => {
926 Output::warning(format!("Could not auto-restack: {}", e));
927 Output::tip("Run 'ca sync' manually to restack dependent entries");
928 }
929 }
930 }
931
932 if push {
934 println!();
935
936 if has_pr {
937 Output::section("Force-pushing to remote");
938
939 std::env::set_var("FORCE_PUSH_NO_CONFIRM", "1");
941
942 repo.force_push_branch(¤t_branch, ¤t_branch)?;
943 Output::success(format!("Force-pushed '{}' to remote", current_branch));
944 Output::sub_item("PR will be automatically updated");
945 } else {
946 Output::warning("No PR found for this entry - skipping push");
947 Output::tip("Use 'ca submit' to create a PR");
948 }
949 }
950
951 println!();
953 Output::section("Summary");
954 Output::bullet(format!(
955 "Amended entry #{} on branch '{}'",
956 entry_index + 1,
957 entry_branch
958 ));
959 if working_branch.is_some() {
960 Output::bullet("Working branch updated");
961 }
962 if restack {
963 Output::bullet("Dependent entries restacked");
964 }
965 if push {
966 Output::bullet("Changes force-pushed to remote");
967 }
968
969 if !push || (!restack && has_dependents) {
971 println!();
972 if !push {
973 Output::tip("Use --push to automatically force-push after amending");
974 }
975 if !restack && has_dependents {
976 Output::tip("Use --restack to automatically update dependent entries");
977 }
978 }
979
980 Ok(())
981}