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