cascade_cli/cli/commands/
entry.rs1use 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 ratatui::{
12 backend::CrosstermBackend,
13 layout::{Alignment, Constraint, Direction, Layout},
14 style::{Color, Modifier, Style},
15 text::{Line, Span},
16 widgets::{Block, Borders, List, ListItem, ListState, Paragraph},
17 Terminal,
18};
19use std::env;
20use std::io;
21use tracing::{info, warn};
22
23#[derive(Debug, Subcommand)]
24pub enum EntryAction {
25 Checkout {
27 entry: Option<usize>,
29 #[arg(long)]
31 direct: bool,
32 #[arg(long, short)]
34 yes: bool,
35 },
36 Status {
38 #[arg(long)]
40 quiet: bool,
41 },
42 List {
44 #[arg(long, short)]
46 verbose: bool,
47 },
48}
49
50pub async fn run(action: EntryAction) -> Result<()> {
51 let _current_dir = env::current_dir()
52 .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
53
54 match action {
55 EntryAction::Checkout { entry, direct, yes } => checkout_entry(entry, direct, yes).await,
56 EntryAction::Status { quiet } => show_edit_status(quiet).await,
57 EntryAction::List { verbose } => list_entries(verbose).await,
58 }
59}
60
61async fn checkout_entry(
63 entry_num: Option<usize>,
64 direct: bool,
65 skip_confirmation: bool,
66) -> Result<()> {
67 let current_dir = env::current_dir()
68 .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
69
70 let repo_root = find_repository_root(¤t_dir)
71 .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
72
73 let mut manager = StackManager::new(&repo_root)?;
74
75 let active_stack = manager.get_active_stack().ok_or_else(|| {
77 CascadeError::config("No active stack. Create a stack first with 'ca stack create'")
78 })?;
79
80 if active_stack.entries.is_empty() {
81 return Err(CascadeError::config(
82 "Stack is empty. Push some commits first with 'ca stack push'",
83 ));
84 }
85
86 let target_entry_num = if let Some(num) = entry_num {
88 if num == 0 || num > active_stack.entries.len() {
89 return Err(CascadeError::config(format!(
90 "Invalid entry number: {}. Stack has {} entries",
91 num,
92 active_stack.entries.len()
93 )));
94 }
95 num
96 } else if direct {
97 return Err(CascadeError::config(
98 "Entry number required when using --direct flag",
99 ));
100 } else {
101 show_entry_picker(active_stack).await?
103 };
104
105 let target_entry = &active_stack.entries[target_entry_num - 1]; let stack_id = active_stack.id;
109 let entry_id = target_entry.id;
110 let entry_commit_hash = target_entry.commit_hash.clone();
111 let entry_branch = target_entry.branch.clone();
112 let entry_short_hash = target_entry.short_hash();
113 let entry_short_message = target_entry.short_message(50);
114 let entry_pr_id = target_entry.pull_request_id.clone();
115
116 if manager.is_in_edit_mode() {
118 let edit_info = manager.get_edit_mode_info().unwrap();
119 warn!("Already in edit mode for entry in stack");
120
121 if !skip_confirmation {
122 Output::warning("Already in edit mode!");
123 Output::sub_item(format!(
124 "Current target: {} (TODO: get commit message)",
125 &edit_info.original_commit_hash[..8]
126 ));
127 Output::info("Do you want to exit current edit mode and start a new one? [y/N]");
128
129 return Err(CascadeError::config("Exit current edit mode first with 'ca entry status' and handle any pending changes"));
132 }
133 }
134
135 if !skip_confirmation {
137 Output::section("Checking out entry for editing");
138 Output::sub_item(format!(
139 "Entry #{target_entry_num}: {entry_short_hash} ({entry_short_message})"
140 ));
141 Output::sub_item(format!("Branch: {entry_branch}"));
142 if let Some(pr_id) = &entry_pr_id {
143 Output::sub_item(format!("PR: #{pr_id}"));
144 }
145 Output::warning("This will checkout the commit and enter edit mode.");
146 Output::info("Any changes you make can be amended to this commit or create new entries.");
147 Output::info("\nContinue? [y/N]");
148
149 info!("Skipping confirmation for now - will implement interactive prompt in next step");
152 }
153
154 manager.enter_edit_mode(stack_id, entry_id)?;
156
157 let current_dir = env::current_dir()
159 .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
160
161 let repo_root = find_repository_root(¤t_dir)
162 .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
163 let repo = crate::git::GitRepository::open(&repo_root)?;
164
165 info!("Checking out commit: {}", entry_commit_hash);
166 repo.checkout_commit(&entry_commit_hash)?;
167
168 Output::success(format!("Entered edit mode for entry #{target_entry_num}"));
169 Output::sub_item(format!(
170 "You are now on commit: {entry_short_hash} ({entry_short_message})"
171 ));
172 Output::sub_item(format!("Branch: {entry_branch}"));
173
174 Output::section("Make your changes and commit normally");
175 Output::bullet("Use 'ca entry status' to see edit mode info");
176 Output::bullet("Use 'git commit --amend' to modify this entry");
177 Output::bullet("Use 'git commit' to create a new entry on top");
178 Output::bullet("Run 'ca sync' after committing to update PRs");
179
180 let hooks_dir = repo_root.join(".git/hooks");
182 let hook_path = hooks_dir.join("prepare-commit-msg");
183 if !hook_path.exists() {
184 Output::tip("Install the prepare-commit-msg hook for better guidance:");
185 Output::sub_item("ca hooks add prepare-commit-msg");
186 }
187
188 Ok(())
189}
190
191async fn show_entry_picker(stack: &crate::stack::Stack) -> Result<usize> {
193 enable_raw_mode()?;
195 let mut stdout = io::stdout();
196 execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
197 let backend = CrosstermBackend::new(stdout);
198 let mut terminal = Terminal::new(backend)?;
199
200 let mut list_state = ListState::default();
201 list_state.select(Some(0));
202
203 let result = loop {
204 terminal.draw(|f| {
205 let size = f.size();
206
207 let chunks = Layout::default()
209 .direction(Direction::Vertical)
210 .margin(2)
211 .constraints(
212 [
213 Constraint::Length(3), Constraint::Min(5), Constraint::Length(3), ]
217 .as_ref(),
218 )
219 .split(size);
220
221 let title = Paragraph::new(format!("📚 Select Entry from Stack: {}", stack.name))
223 .style(
224 Style::default()
225 .fg(Color::Cyan)
226 .add_modifier(Modifier::BOLD),
227 )
228 .alignment(Alignment::Center)
229 .block(Block::default().borders(Borders::ALL));
230 f.render_widget(title, chunks[0]);
231
232 let items: Vec<ListItem> = stack
234 .entries
235 .iter()
236 .enumerate()
237 .map(|(i, entry)| {
238 let status_icon = if entry.is_submitted {
239 if entry.pull_request_id.is_some() {
240 "📤"
241 } else {
242 "📝"
243 }
244 } else {
245 "🔄"
246 };
247
248 let pr_text = if let Some(pr_id) = &entry.pull_request_id {
249 format!(" PR: #{pr_id}")
250 } else {
251 "".to_string()
252 };
253
254 let line = Line::from(vec![
255 Span::raw(format!(" {}. ", i + 1)),
256 Span::raw(status_icon),
257 Span::raw(" "),
258 Span::styled(entry.short_message(40), Style::default().fg(Color::White)),
259 Span::raw(" "),
260 Span::styled(
261 format!("({})", entry.short_hash()),
262 Style::default().fg(Color::Yellow),
263 ),
264 Span::styled(pr_text, Style::default().fg(Color::Green)),
265 ]);
266
267 ListItem::new(line)
268 })
269 .collect();
270
271 let list = List::new(items)
272 .block(Block::default().borders(Borders::ALL).title("Entries"))
273 .highlight_style(Style::default().fg(Color::Black).bg(Color::Cyan))
274 .highlight_symbol("→ ");
275
276 f.render_stateful_widget(list, chunks[1], &mut list_state);
277
278 let help = Paragraph::new("↑/↓: Navigate • Enter: Select • q: Quit • r: Refresh")
280 .style(Style::default().fg(Color::DarkGray))
281 .alignment(Alignment::Center)
282 .block(Block::default().borders(Borders::ALL));
283 f.render_widget(help, chunks[2]);
284 })?;
285
286 if let Event::Key(key) = event::read()? {
288 if key.kind == KeyEventKind::Press {
289 match key.code {
290 KeyCode::Char('q') => {
291 break Err(CascadeError::config("Entry selection cancelled"));
292 }
293 KeyCode::Up => {
294 let selected = list_state.selected().unwrap_or(0);
295 if selected > 0 {
296 list_state.select(Some(selected - 1));
297 } else {
298 list_state.select(Some(stack.entries.len() - 1));
299 }
300 }
301 KeyCode::Down => {
302 let selected = list_state.selected().unwrap_or(0);
303 if selected < stack.entries.len() - 1 {
304 list_state.select(Some(selected + 1));
305 } else {
306 list_state.select(Some(0));
307 }
308 }
309 KeyCode::Enter => {
310 let selected = list_state.selected().unwrap_or(0);
311 break Ok(selected + 1); }
313 KeyCode::Char('r') => {
314 continue;
316 }
317 _ => {}
318 }
319 }
320 }
321 };
322
323 disable_raw_mode()?;
325 execute!(
326 terminal.backend_mut(),
327 LeaveAlternateScreen,
328 DisableMouseCapture
329 )?;
330 terminal.show_cursor()?;
331
332 result
333}
334
335async fn show_edit_status(quiet: bool) -> Result<()> {
337 let current_dir = env::current_dir()
338 .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
339
340 let repo_root = find_repository_root(¤t_dir)
341 .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
342 let manager = StackManager::new(&repo_root)?;
343
344 if !manager.is_in_edit_mode() {
345 if quiet {
346 println!("inactive");
347 } else {
348 Output::info("Not in edit mode");
349 Output::sub_item("Use 'ca entry checkout' to start editing a stack entry");
350 }
351 return Ok(());
352 }
353
354 let edit_info = manager.get_edit_mode_info().unwrap();
355
356 if quiet {
357 println!("active:{:?}", edit_info.target_entry_id);
358 return Ok(());
359 }
360
361 Output::section("Currently in edit mode");
362 Output::sub_item(format!("Target entry: {:?}", edit_info.target_entry_id));
363 Output::sub_item(format!(
364 "Original commit: {}",
365 &edit_info.original_commit_hash[..8]
366 ));
367 Output::sub_item(format!(
368 "Started: {}",
369 edit_info.started_at.format("%Y-%m-%d %H:%M:%S")
370 ));
371
372 Output::section("Current state");
374
375 Output::sub_item("Use 'git status' for detailed working directory status");
381 Output::sub_item("Use 'ca entry list' to see all entries");
382
383 Ok(())
384}
385
386async fn list_entries(verbose: bool) -> Result<()> {
388 let current_dir = env::current_dir()
389 .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
390
391 let repo_root = find_repository_root(¤t_dir)
392 .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
393 let manager = StackManager::new(&repo_root)?;
394
395 let active_stack = manager.get_active_stack().ok_or_else(|| {
396 CascadeError::config(
397 "No active stack. Create a stack first with 'ca stack create'".to_string(),
398 )
399 })?;
400
401 if active_stack.entries.is_empty() {
402 Output::info(format!(
403 "Active stack '{}' has no entries yet",
404 active_stack.name
405 ));
406 Output::sub_item("Add some commits to the stack with 'ca stack push'");
407 return Ok(());
408 }
409
410 Output::section(format!(
411 "Stack: {} ({} entries)",
412 active_stack.name,
413 active_stack.entries.len()
414 ));
415
416 let edit_mode_info = manager.get_edit_mode_info();
417
418 for (i, entry) in active_stack.entries.iter().enumerate() {
419 let entry_num = i + 1;
420
421 let status_icon = if entry.is_submitted {
423 if entry.pull_request_id.is_some() {
424 "📤"
425 } else {
426 "📝"
427 }
428 } else {
429 "🔄"
430 };
431
432 let edit_indicator = if edit_mode_info.is_some()
434 && edit_mode_info.unwrap().target_entry_id == Some(entry.id)
435 {
436 " 🎯"
437 } else {
438 ""
439 };
440
441 print!(
443 " {}. {} {} ({})",
444 entry_num,
445 status_icon,
446 entry.short_message(50),
447 entry.short_hash()
448 );
449
450 if let Some(pr_id) = &entry.pull_request_id {
452 print!(" PR: #{pr_id}");
453 }
454
455 print!("{edit_indicator}");
456 println!();
457
458 if verbose {
460 println!(" Branch: {}", entry.branch);
461 println!(
462 " Created: {}",
463 entry.created_at.format("%Y-%m-%d %H:%M:%S")
464 );
465 if entry.is_submitted {
466 println!(" Status: Submitted");
467 } else {
468 println!(" Status: Draft");
469 }
470 println!();
471 }
472 }
473
474 if let Some(_edit_info) = edit_mode_info {
475 println!();
476 Output::info("Edit mode active - use 'ca entry status' for details");
477 } else {
478 println!();
479 Output::tip("Use 'ca entry checkout' to start editing an entry");
480 }
481
482 Ok(())
483}