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 Output::sub_item(" → Press Enter (or 'A') to amend this entry");
265 Output::sub_item(" → Type 'n' to create new entry on top");
266 Output::bullet("Run 'ca sync' after committing to update PRs");
267
268 let hooks_dir = repo_root.join(".git/hooks");
270 let hook_path = hooks_dir.join("prepare-commit-msg");
271 if !hook_path.exists() {
272 Output::tip("Install the prepare-commit-msg hook for better guidance:");
273 Output::sub_item("ca hooks add prepare-commit-msg");
274 }
275
276 Ok(())
277}
278
279async fn show_entry_picker(stack: &crate::stack::Stack) -> Result<usize> {
281 enable_raw_mode()?;
283 let mut stdout = io::stdout();
284 execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
285 let backend = CrosstermBackend::new(stdout);
286 let mut terminal = Terminal::new(backend)?;
287
288 let mut list_state = ListState::default();
289 list_state.select(Some(0));
290
291 let result = loop {
292 terminal.draw(|f| {
293 let size = f.area();
294
295 let chunks = Layout::default()
297 .direction(Direction::Vertical)
298 .margin(2)
299 .constraints(
300 [
301 Constraint::Length(3), Constraint::Min(5), Constraint::Length(3), ]
305 .as_ref(),
306 )
307 .split(size);
308
309 let title = Paragraph::new(format!("📚 Select Entry from Stack: {}", stack.name))
311 .style(
312 Style::default()
313 .fg(Color::Cyan)
314 .add_modifier(Modifier::BOLD),
315 )
316 .alignment(Alignment::Center)
317 .block(Block::default().borders(Borders::ALL));
318 f.render_widget(title, chunks[0]);
319
320 let items: Vec<ListItem> = stack
322 .entries
323 .iter()
324 .enumerate()
325 .map(|(i, entry)| {
326 let status_icon = if entry.is_submitted {
327 if entry.pull_request_id.is_some() {
328 "📤"
329 } else {
330 "📝"
331 }
332 } else {
333 "🔄"
334 };
335
336 let pr_text = if let Some(pr_id) = &entry.pull_request_id {
337 format!(" PR: #{pr_id}")
338 } else {
339 "".to_string()
340 };
341
342 let line = Line::from(vec![
343 Span::raw(format!(" {}. ", i + 1)),
344 Span::raw(status_icon),
345 Span::raw(" "),
346 Span::styled(entry.short_message(40), Style::default().fg(Color::White)),
347 Span::raw(" "),
348 Span::styled(
349 format!("({})", entry.short_hash()),
350 Style::default().fg(Color::Yellow),
351 ),
352 Span::styled(pr_text, Style::default().fg(Color::Green)),
353 ]);
354
355 ListItem::new(line)
356 })
357 .collect();
358
359 let list = List::new(items)
360 .block(Block::default().borders(Borders::ALL).title("Entries"))
361 .highlight_style(Style::default().fg(Color::Black).bg(Color::Cyan))
362 .highlight_symbol("→ ");
363
364 f.render_stateful_widget(list, chunks[1], &mut list_state);
365
366 let help = Paragraph::new("↑/↓: Navigate • Enter: Select • q: Quit • r: Refresh")
368 .style(Style::default().fg(Color::DarkGray))
369 .alignment(Alignment::Center)
370 .block(Block::default().borders(Borders::ALL));
371 f.render_widget(help, chunks[2]);
372 })?;
373
374 if let Event::Key(key) = event::read()? {
376 if key.kind == KeyEventKind::Press {
377 match key.code {
378 KeyCode::Char('q') => {
379 break Err(CascadeError::config("Entry selection cancelled"));
380 }
381 KeyCode::Up => {
382 let selected = list_state.selected().unwrap_or(0);
383 if selected > 0 {
384 list_state.select(Some(selected - 1));
385 } else {
386 list_state.select(Some(stack.entries.len() - 1));
387 }
388 }
389 KeyCode::Down => {
390 let selected = list_state.selected().unwrap_or(0);
391 if selected < stack.entries.len() - 1 {
392 list_state.select(Some(selected + 1));
393 } else {
394 list_state.select(Some(0));
395 }
396 }
397 KeyCode::Enter => {
398 let selected = list_state.selected().unwrap_or(0);
399 break Ok(selected + 1); }
401 KeyCode::Char('r') => {
402 continue;
404 }
405 _ => {}
406 }
407 }
408 }
409 };
410
411 disable_raw_mode()?;
413 execute!(
414 terminal.backend_mut(),
415 LeaveAlternateScreen,
416 DisableMouseCapture
417 )?;
418 terminal.show_cursor()?;
419
420 result
421}
422
423async fn show_edit_status(quiet: bool) -> Result<()> {
425 let current_dir = env::current_dir()
426 .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
427
428 let repo_root = find_repository_root(¤t_dir)
429 .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
430 let manager = StackManager::new(&repo_root)?;
431
432 if !manager.is_in_edit_mode() {
433 if quiet {
434 println!("inactive");
435 } else {
436 Output::info("Not in edit mode");
437 Output::sub_item("Use 'ca entry checkout' to start editing a stack entry");
438 }
439 return Ok(());
440 }
441
442 let edit_info = manager.get_edit_mode_info().unwrap();
443
444 if quiet {
445 println!("active:{:?}", edit_info.target_entry_id);
446 return Ok(());
447 }
448
449 Output::section("Currently in edit mode");
450
451 if let Some(active_stack) = manager.get_active_stack() {
453 if let Some(target_entry_id) = edit_info.target_entry_id {
454 if let Some(entry) = active_stack
455 .entries
456 .iter()
457 .find(|e| e.id == target_entry_id)
458 {
459 Output::sub_item(format!(
460 "Target entry: {} ({})",
461 entry.short_hash(),
462 entry.short_message(50)
463 ));
464 Output::sub_item(format!("Branch: {}", entry.branch));
465
466 Output::sub_item("Commit Message:");
468 let lines: Vec<&str> = entry.message.lines().collect();
469 for line in lines {
470 Output::sub_item(format!(" {line}"));
471 }
472 } else {
473 Output::sub_item(format!("Target entry: {target_entry_id:?} (not found)"));
474 }
475 } else {
476 Output::sub_item("Target entry: Unknown");
477 }
478 } else {
479 Output::sub_item(format!("Target entry: {:?}", edit_info.target_entry_id));
480 }
481
482 Output::sub_item(format!(
483 "Original commit: {}",
484 &edit_info.original_commit_hash[..8]
485 ));
486 Output::sub_item(format!(
487 "Started: {}",
488 edit_info.started_at.format("%Y-%m-%d %H:%M:%S")
489 ));
490
491 Output::section("Current state");
493
494 let current_dir = env::current_dir()
496 .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
497 let repo_root = find_repository_root(¤t_dir)
498 .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
499 let repo = crate::git::GitRepository::open(&repo_root)?;
500
501 let current_head = repo.get_current_commit_hash()?;
503 if current_head != edit_info.original_commit_hash {
504 let current_short = ¤t_head[..8];
505 let original_short = &edit_info.original_commit_hash[..8];
506 Output::sub_item(format!("HEAD moved: {original_short} → {current_short}"));
507
508 match repo.get_commit_count_between(&edit_info.original_commit_hash, ¤t_head) {
510 Ok(count) if count > 0 => {
511 Output::sub_item(format!(" {count} new commit(s) created"));
512 }
513 _ => {}
514 }
515 } else {
516 Output::sub_item(format!("HEAD: {} (unchanged)", ¤t_head[..8]));
517 }
518
519 match repo.get_status_summary() {
521 Ok(status) => {
522 if status.is_clean() {
523 Output::sub_item("Working directory: clean");
524 } else {
525 if status.has_staged_changes() {
526 Output::sub_item(format!("Staged changes: {} files", status.staged_count()));
527 }
528 if status.has_unstaged_changes() {
529 Output::sub_item(format!(
530 "Unstaged changes: {} files",
531 status.unstaged_count()
532 ));
533 }
534 if status.has_untracked_files() {
535 Output::sub_item(format!(
536 "Untracked files: {} files",
537 status.untracked_count()
538 ));
539 }
540 }
541 }
542 Err(_) => {
543 Output::sub_item("Working directory: status unavailable");
544 }
545 }
546
547 Output::tip("Use 'git status' for detailed file-level status");
548 Output::sub_item("Use 'ca entry list' to see all entries");
549
550 Ok(())
551}
552
553async fn list_entries(verbose: bool) -> Result<()> {
555 let current_dir = env::current_dir()
556 .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
557
558 let repo_root = find_repository_root(¤t_dir)
559 .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
560 let manager = StackManager::new(&repo_root)?;
561
562 let active_stack = manager.get_active_stack().ok_or_else(|| {
563 CascadeError::config(
564 "No active stack. Create a stack first with 'ca stack create'".to_string(),
565 )
566 })?;
567
568 if active_stack.entries.is_empty() {
569 Output::info(format!(
570 "Active stack '{}' has no entries yet",
571 active_stack.name
572 ));
573 Output::sub_item("Add some commits to the stack with 'ca stack push'");
574 return Ok(());
575 }
576
577 Output::section(format!(
578 "Stack: {} ({} entries)",
579 active_stack.name,
580 active_stack.entries.len()
581 ));
582
583 let edit_mode_info = manager.get_edit_mode_info();
584
585 for (i, entry) in active_stack.entries.iter().enumerate() {
586 let entry_num = i + 1;
587
588 let status_icon = if entry.is_submitted {
590 if entry.pull_request_id.is_some() {
591 "📤"
592 } else {
593 "📝"
594 }
595 } else {
596 "🔄"
597 };
598
599 let edit_indicator = if edit_mode_info.is_some()
601 && edit_mode_info.unwrap().target_entry_id == Some(entry.id)
602 {
603 " 🎯"
604 } else {
605 ""
606 };
607
608 print!(
610 " {}. {} {} ({})",
611 entry_num,
612 status_icon,
613 entry.short_message(50),
614 entry.short_hash()
615 );
616
617 if let Some(pr_id) = &entry.pull_request_id {
619 print!(" PR: #{pr_id}");
620 }
621
622 print!("{edit_indicator}");
623 println!(); if verbose {
627 Output::sub_item(format!("Branch: {}", entry.branch));
628 Output::sub_item(format!("Commit: {}", entry.commit_hash));
629 Output::sub_item(format!(
630 "Created: {}",
631 entry.created_at.format("%Y-%m-%d %H:%M:%S")
632 ));
633 if entry.is_submitted {
634 Output::sub_item("Status: Submitted");
635 } else {
636 Output::sub_item("Status: Draft");
637 }
638
639 Output::sub_item("Message:");
641 let lines: Vec<&str> = entry.message.lines().collect();
642 for line in lines {
643 Output::sub_item(format!(" {line}"));
644 }
645
646 if edit_mode_info.is_some() && edit_mode_info.unwrap().target_entry_id == Some(entry.id)
648 {
649 if let Ok(repo_root) = find_repository_root(&env::current_dir().unwrap_or_default())
650 {
651 if let Ok(repo) = crate::git::GitRepository::open(&repo_root) {
652 match repo.get_status_summary() {
653 Ok(status) => {
654 if !status.is_clean() {
655 Output::sub_item("Git Status:");
656 if status.has_staged_changes() {
657 Output::sub_item(format!(
658 " Staged: {} files",
659 status.staged_count()
660 ));
661 }
662 if status.has_unstaged_changes() {
663 Output::sub_item(format!(
664 " Unstaged: {} files",
665 status.unstaged_count()
666 ));
667 }
668 if status.has_untracked_files() {
669 Output::sub_item(format!(
670 " Untracked: {} files",
671 status.untracked_count()
672 ));
673 }
674 } else {
675 Output::sub_item("Git Status: clean");
676 }
677 }
678 Err(_) => {
679 Output::sub_item("Git Status: unavailable");
680 }
681 }
682 }
683 }
684 }
685 }
687 }
688
689 if let Some(_edit_info) = edit_mode_info {
690 Output::spacing();
691 Output::info("Edit mode active - use 'ca entry status' for details");
692 } else {
693 Output::spacing();
694 Output::tip("Use 'ca entry checkout' to start editing an entry");
695 }
696
697 Ok(())
698}
699
700async fn clear_edit_mode(skip_confirmation: bool) -> Result<()> {
702 let current_dir = env::current_dir()
703 .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
704
705 let repo_root = find_repository_root(¤t_dir)
706 .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
707
708 let mut manager = StackManager::new(&repo_root)?;
709
710 if !manager.is_in_edit_mode() {
711 Output::info("Not currently in edit mode");
712 return Ok(());
713 }
714
715 if let Some(edit_info) = manager.get_edit_mode_info() {
717 Output::section("Current edit mode state");
718
719 if let Some(target_entry_id) = &edit_info.target_entry_id {
720 Output::sub_item(format!("Target entry: {}", target_entry_id));
721
722 if let Some(active_stack) = manager.get_active_stack() {
724 if let Some(entry) = active_stack
725 .entries
726 .iter()
727 .find(|e| e.id == *target_entry_id)
728 {
729 Output::sub_item(format!("Entry: {}", entry.short_message(50)));
730 } else {
731 Output::warning("Target entry not found in stack (corrupted state)");
732 }
733 }
734 }
735
736 Output::sub_item(format!(
737 "Original commit: {}",
738 &edit_info.original_commit_hash[..8]
739 ));
740 Output::sub_item(format!(
741 "Started: {}",
742 edit_info.started_at.format("%Y-%m-%d %H:%M:%S")
743 ));
744 }
745
746 if !skip_confirmation {
748 println!();
749 let confirmed = Confirm::with_theme(&ColorfulTheme::default())
750 .with_prompt("Clear edit mode state?")
751 .default(true)
752 .interact()
753 .map_err(|e| CascadeError::config(format!("Failed to get user confirmation: {e}")))?;
754
755 if !confirmed {
756 return Err(CascadeError::config("Operation cancelled."));
757 }
758 }
759
760 manager.exit_edit_mode()?;
762
763 Output::success("Edit mode cleared");
764 Output::tip("Use 'ca entry checkout' to start a new edit session");
765
766 Ok(())
767}
768
769async fn amend_entry(message: Option<String>, all: bool, push: bool, restack: bool) -> Result<()> {
771 let current_dir = env::current_dir()
772 .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
773
774 let repo_root = find_repository_root(¤t_dir)
775 .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
776
777 let mut manager = StackManager::new(&repo_root)?;
778 let repo = crate::git::GitRepository::open(&repo_root)?;
779
780 let current_branch = repo.get_current_branch()?;
781
782 let (stack_id, entry_index, entry_id, entry_branch, working_branch, has_dependents, has_pr) = {
784 let active_stack = manager.get_active_stack().ok_or_else(|| {
785 CascadeError::config("No active stack. Create a stack first with 'ca stack create'")
786 })?;
787
788 let mut found_entry = None;
790
791 for (idx, entry) in active_stack.entries.iter().enumerate() {
792 if entry.branch == current_branch {
793 found_entry = Some((
794 idx,
795 entry.id,
796 entry.branch.clone(),
797 entry.pull_request_id.clone(),
798 ));
799 break;
800 }
801 }
802
803 match found_entry {
804 Some((idx, id, branch, pr_id)) => {
805 let has_dependents = idx + 1 < active_stack.entries.len();
806 (
807 active_stack.id,
808 idx,
809 id,
810 branch,
811 active_stack.working_branch.clone(),
812 has_dependents,
813 pr_id.is_some(),
814 )
815 }
816 None => {
817 return Err(CascadeError::config(format!(
818 "Current branch '{}' is not a stack entry branch.\n\
819 Use 'ca entry checkout <N>' to checkout a stack entry first.",
820 current_branch
821 )));
822 }
823 }
824 };
825
826 Output::section(format!("Amending stack entry #{}", entry_index + 1));
827
828 let mut amend_args = vec!["commit", "--amend"];
830
831 if all {
832 amend_args.insert(1, "-a");
833 }
834
835 if let Some(ref msg) = message {
836 amend_args.push("-m");
837 amend_args.push(msg);
838 } else {
839 amend_args.push("--no-edit");
841 }
842
843 debug!("Running git {}", amend_args.join(" "));
844
845 let output = std::process::Command::new("git")
846 .args(&amend_args)
847 .current_dir(&repo_root)
848 .output()
849 .map_err(CascadeError::Io)?;
850
851 if !output.status.success() {
852 let stderr = String::from_utf8_lossy(&output.stderr);
853 return Err(CascadeError::branch(format!(
854 "Failed to amend commit: {}",
855 stderr.trim()
856 )));
857 }
858
859 Output::success("Commit amended");
860
861 let new_commit_hash = repo.get_head_commit()?.id().to_string();
863 debug!("New commit hash after amend: {}", new_commit_hash);
864
865 {
867 let stack = manager
868 .get_stack_mut(&stack_id)
869 .ok_or_else(|| CascadeError::config("Stack not found"))?;
870
871 if let Some(entry) = stack.entries.iter_mut().find(|e| e.id == entry_id) {
872 let old_hash = entry.commit_hash.clone();
873 entry.commit_hash = new_commit_hash.clone();
874 debug!(
875 "Updated entry commit hash: {} -> {}",
876 &old_hash[..8],
877 &new_commit_hash[..8]
878 );
879 Output::sub_item(format!(
880 "Updated metadata: {} → {}",
881 &old_hash[..8],
882 &new_commit_hash[..8]
883 ));
884 }
885 }
886
887 manager.save_to_disk()?;
888
889 if let Some(ref working_branch_name) = working_branch {
891 Output::sub_item(format!("Updating working branch: {}", working_branch_name));
892
893 repo.update_branch_to_commit(working_branch_name, &new_commit_hash)?;
895
896 Output::success(format!(
897 "Working branch '{}' updated (safety net preserved)",
898 working_branch_name
899 ));
900 } else {
901 Output::warning("No working branch found - create one with 'ca stack create' for safety");
902 }
903
904 if restack && has_dependents {
906 println!();
907 Output::section("Auto-restacking dependent entries");
908
909 let rebase_manager_stack = StackManager::new(&repo_root)?;
911 let rebase_manager_repo = crate::git::GitRepository::open(&repo_root)?;
912
913 let mut rebase_manager = crate::stack::RebaseManager::new(
915 rebase_manager_stack,
916 rebase_manager_repo,
917 crate::stack::RebaseOptions {
918 strategy: crate::stack::RebaseStrategy::ForcePush,
919 target_base: Some(entry_branch.clone()),
920 skip_pull: Some(true), ..Default::default()
922 },
923 );
924
925 match rebase_manager.rebase_stack(&stack_id) {
926 Ok(_) => {
927 Output::success("Dependent entries restacked");
928 }
929 Err(e) => {
930 Output::warning(format!("Could not auto-restack: {}", e));
931 Output::tip("Run 'ca sync' manually to restack dependent entries");
932 }
933 }
934 }
935
936 if push {
938 println!();
939
940 if has_pr {
941 Output::section("Force-pushing to remote");
942
943 std::env::set_var("FORCE_PUSH_NO_CONFIRM", "1");
945
946 repo.force_push_branch(¤t_branch, ¤t_branch)?;
947 Output::success(format!("Force-pushed '{}' to remote", current_branch));
948 Output::sub_item("PR will be automatically updated");
949 } else {
950 Output::warning("No PR found for this entry - skipping push");
951 Output::tip("Use 'ca submit' to create a PR");
952 }
953 }
954
955 println!();
957 Output::section("Summary");
958 Output::bullet(format!(
959 "Amended entry #{} on branch '{}'",
960 entry_index + 1,
961 entry_branch
962 ));
963 if working_branch.is_some() {
964 Output::bullet("Working branch updated (safety net preserved)");
965 }
966 if restack {
967 Output::bullet("Dependent entries restacked");
968 }
969 if push {
970 Output::bullet("Changes force-pushed to remote");
971 } else {
972 Output::tip("Use --push to automatically force-push after amending");
973 }
974
975 if !restack && has_dependents {
976 Output::tip("Use --restack to automatically update dependent entries");
977 }
978
979 Ok(())
980}