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