cascade_cli/cli/commands/
entry.rs1use crate::errors::{CascadeError, Result};
2use crate::git::find_repository_root;
3use crate::stack::StackManager;
4use clap::Subcommand;
5use crossterm::{
6 event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
7 execute,
8 terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
9};
10use ratatui::{
11 backend::CrosstermBackend,
12 layout::{Alignment, Constraint, Direction, Layout},
13 style::{Color, Modifier, Style},
14 text::{Line, Span},
15 widgets::{Block, Borders, List, ListItem, ListState, Paragraph},
16 Terminal,
17};
18use std::env;
19use std::io;
20use tracing::{info, warn};
21
22#[derive(Debug, Subcommand)]
23pub enum EntryAction {
24 Checkout {
26 #[arg(short, long)]
28 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 println!("ā ļø Already in edit mode!");
123 println!(
124 " Current target: {} (TODO: get commit message)",
125 &edit_info.original_commit_hash[..8]
126 );
127 println!(" 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 println!("šÆ Checking out entry for editing:");
138 println!(" Entry #{target_entry_num}: {entry_short_hash} ({entry_short_message})");
139 println!(" Branch: {entry_branch}");
140 if let Some(pr_id) = &entry_pr_id {
141 println!(" PR: #{pr_id}");
142 }
143 println!("\nā ļø This will checkout the commit and enter edit mode.");
144 println!(" Any changes you make can be amended to this commit or create new entries.");
145 println!("\nContinue? [y/N]");
146
147 info!("Skipping confirmation for now - will implement interactive prompt in next step");
150 }
151
152 manager.enter_edit_mode(stack_id, entry_id)?;
154
155 let current_dir = env::current_dir()
157 .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
158
159 let repo_root = find_repository_root(¤t_dir)
160 .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
161 let repo = crate::git::GitRepository::open(&repo_root)?;
162
163 info!("Checking out commit: {}", entry_commit_hash);
164 repo.checkout_commit(&entry_commit_hash)?;
165
166 println!("ā
Entered edit mode for entry #{target_entry_num}");
167 println!(" You are now on commit: {entry_short_hash} ({entry_short_message})");
168 println!(" Branch: {entry_branch}");
169 println!("\nš Make your changes and commit normally.");
170 println!(" ⢠Use 'ca entry status' to see edit mode info");
171 println!(" ⢠Changes will be smartly handled when you commit");
172 println!(" ⢠Use 'ca stack commit-edit' when ready (coming in next step)");
173
174 Ok(())
175}
176
177async fn show_entry_picker(stack: &crate::stack::Stack) -> Result<usize> {
179 enable_raw_mode()?;
181 let mut stdout = io::stdout();
182 execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
183 let backend = CrosstermBackend::new(stdout);
184 let mut terminal = Terminal::new(backend)?;
185
186 let mut list_state = ListState::default();
187 list_state.select(Some(0));
188
189 let result = loop {
190 terminal.draw(|f| {
191 let size = f.size();
192
193 let chunks = Layout::default()
195 .direction(Direction::Vertical)
196 .margin(2)
197 .constraints(
198 [
199 Constraint::Length(3), Constraint::Min(5), Constraint::Length(3), ]
203 .as_ref(),
204 )
205 .split(size);
206
207 let title = Paragraph::new(format!("š Select Entry from Stack: {}", stack.name))
209 .style(
210 Style::default()
211 .fg(Color::Cyan)
212 .add_modifier(Modifier::BOLD),
213 )
214 .alignment(Alignment::Center)
215 .block(Block::default().borders(Borders::ALL));
216 f.render_widget(title, chunks[0]);
217
218 let items: Vec<ListItem> = stack
220 .entries
221 .iter()
222 .enumerate()
223 .map(|(i, entry)| {
224 let status_icon = if entry.is_submitted {
225 if entry.pull_request_id.is_some() {
226 "š¤"
227 } else {
228 "š"
229 }
230 } else {
231 "š"
232 };
233
234 let pr_text = if let Some(pr_id) = &entry.pull_request_id {
235 format!(" PR: #{pr_id}")
236 } else {
237 "".to_string()
238 };
239
240 let line = Line::from(vec![
241 Span::raw(format!(" {}. ", i + 1)),
242 Span::raw(status_icon),
243 Span::raw(" "),
244 Span::styled(entry.short_message(40), Style::default().fg(Color::White)),
245 Span::raw(" "),
246 Span::styled(
247 format!("({})", entry.short_hash()),
248 Style::default().fg(Color::Yellow),
249 ),
250 Span::styled(pr_text, Style::default().fg(Color::Green)),
251 ]);
252
253 ListItem::new(line)
254 })
255 .collect();
256
257 let list = List::new(items)
258 .block(Block::default().borders(Borders::ALL).title("Entries"))
259 .highlight_style(Style::default().fg(Color::Black).bg(Color::Cyan))
260 .highlight_symbol("ā ");
261
262 f.render_stateful_widget(list, chunks[1], &mut list_state);
263
264 let help = Paragraph::new("ā/ā: Navigate ⢠Enter: Select ⢠q: Quit ⢠r: Refresh")
266 .style(Style::default().fg(Color::DarkGray))
267 .alignment(Alignment::Center)
268 .block(Block::default().borders(Borders::ALL));
269 f.render_widget(help, chunks[2]);
270 })?;
271
272 if let Event::Key(key) = event::read()? {
274 if key.kind == KeyEventKind::Press {
275 match key.code {
276 KeyCode::Char('q') => {
277 break Err(CascadeError::config("Entry selection cancelled"));
278 }
279 KeyCode::Up => {
280 let selected = list_state.selected().unwrap_or(0);
281 if selected > 0 {
282 list_state.select(Some(selected - 1));
283 } else {
284 list_state.select(Some(stack.entries.len() - 1));
285 }
286 }
287 KeyCode::Down => {
288 let selected = list_state.selected().unwrap_or(0);
289 if selected < stack.entries.len() - 1 {
290 list_state.select(Some(selected + 1));
291 } else {
292 list_state.select(Some(0));
293 }
294 }
295 KeyCode::Enter => {
296 let selected = list_state.selected().unwrap_or(0);
297 break Ok(selected + 1); }
299 KeyCode::Char('r') => {
300 continue;
302 }
303 _ => {}
304 }
305 }
306 }
307 };
308
309 disable_raw_mode()?;
311 execute!(
312 terminal.backend_mut(),
313 LeaveAlternateScreen,
314 DisableMouseCapture
315 )?;
316 terminal.show_cursor()?;
317
318 result
319}
320
321async fn show_edit_status(quiet: bool) -> Result<()> {
323 let current_dir = env::current_dir()
324 .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
325
326 let repo_root = find_repository_root(¤t_dir)
327 .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
328 let manager = StackManager::new(&repo_root)?;
329
330 if !manager.is_in_edit_mode() {
331 if quiet {
332 println!("inactive");
333 } else {
334 println!("š Not in edit mode");
335 println!(" Use 'ca entry checkout' to start editing a stack entry");
336 }
337 return Ok(());
338 }
339
340 let edit_info = manager.get_edit_mode_info().unwrap();
341
342 if quiet {
343 println!("active:{:?}", edit_info.target_entry_id);
344 return Ok(());
345 }
346
347 println!("šÆ Currently in edit mode");
348 println!(" Target entry: {:?}", edit_info.target_entry_id);
349 println!(
350 " Original commit: {}",
351 &edit_info.original_commit_hash[..8]
352 );
353 println!(
354 " Started: {}",
355 edit_info.started_at.format("%Y-%m-%d %H:%M:%S")
356 );
357
358 println!("\nš Current state:");
360
361 println!(" Use 'git status' for detailed working directory status");
367 println!(" Use 'ca entry list' to see all entries");
368
369 Ok(())
370}
371
372async fn list_entries(verbose: bool) -> Result<()> {
374 let current_dir = env::current_dir()
375 .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
376
377 let repo_root = find_repository_root(¤t_dir)
378 .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
379 let manager = StackManager::new(&repo_root)?;
380
381 let active_stack = manager.get_active_stack().ok_or_else(|| {
382 CascadeError::config(
383 "No active stack. Create a stack first with 'ca stack create'".to_string(),
384 )
385 })?;
386
387 if active_stack.entries.is_empty() {
388 println!("š Active stack '{}' has no entries yet", active_stack.name);
389 println!(" Add some commits to the stack with 'ca stack push'");
390 return Ok(());
391 }
392
393 println!(
394 "š Stack: {} ({} entries)",
395 active_stack.name,
396 active_stack.entries.len()
397 );
398
399 let edit_mode_info = manager.get_edit_mode_info();
400
401 for (i, entry) in active_stack.entries.iter().enumerate() {
402 let entry_num = i + 1;
403
404 let status_icon = if entry.is_submitted {
406 if entry.pull_request_id.is_some() {
407 "š¤"
408 } else {
409 "š"
410 }
411 } else {
412 "š"
413 };
414
415 let edit_indicator = if edit_mode_info.is_some()
417 && edit_mode_info.unwrap().target_entry_id == Some(entry.id)
418 {
419 " šÆ"
420 } else {
421 ""
422 };
423
424 print!(
426 " {}. {} {} ({})",
427 entry_num,
428 status_icon,
429 entry.short_message(50),
430 entry.short_hash()
431 );
432
433 if let Some(pr_id) = &entry.pull_request_id {
435 print!(" PR: #{pr_id}");
436 }
437
438 print!("{edit_indicator}");
439 println!();
440
441 if verbose {
443 println!(" Branch: {}", entry.branch);
444 println!(
445 " Created: {}",
446 entry.created_at.format("%Y-%m-%d %H:%M:%S")
447 );
448 if entry.is_submitted {
449 println!(" Status: Submitted");
450 } else {
451 println!(" Status: Draft");
452 }
453 println!();
454 }
455 }
456
457 if let Some(_edit_info) = edit_mode_info {
458 println!("\nšÆ Edit mode active - use 'ca entry status' for details");
459 } else {
460 println!("\nš” Use 'ca entry checkout' to start editing an entry");
461 }
462
463 Ok(())
464}