cascade_cli/cli/commands/
entry.rs

1use 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    /// Interactively checkout a stack entry for editing
25    Checkout {
26        /// Stack entry number (optional, shows picker if not provided)
27        #[arg(short, long)]
28        entry: Option<usize>,
29        /// Skip interactive picker and use entry number directly
30        #[arg(long)]
31        direct: bool,
32        /// Skip confirmation prompts
33        #[arg(long, short)]
34        yes: bool,
35    },
36    /// Show current edit mode status
37    Status {
38        /// Show brief status only
39        #[arg(long)]
40        quiet: bool,
41    },
42    /// List all entries with their edit status
43    List {
44        /// Show detailed information
45        #[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
61/// Checkout a specific stack entry for editing
62async 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(&current_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    // Get active stack
76    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    // Determine which entry to checkout
87    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 interactive picker
102        show_entry_picker(active_stack).await?
103    };
104
105    let target_entry = &active_stack.entries[target_entry_num - 1]; // Convert to 0-based index
106
107    // Clone the values we need before borrowing manager mutably
108    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    // Check if already in edit mode
117    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            // TODO: Implement interactive confirmation
130            // For now, just warn and exit
131            return Err(CascadeError::config("Exit current edit mode first with 'ca entry status' and handle any pending changes"));
132        }
133    }
134
135    // Confirmation prompt
136    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        // TODO: Implement interactive confirmation with dialoguer
148        // For now, just proceed
149        info!("Skipping confirmation for now - will implement interactive prompt in next step");
150    }
151
152    // Enter edit mode
153    manager.enter_edit_mode(stack_id, entry_id)?;
154
155    // Checkout the commit
156    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(&current_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
177/// Interactive entry picker using TUI
178async fn show_entry_picker(stack: &crate::stack::Stack) -> Result<usize> {
179    // Setup terminal
180    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            // Create layout
194            let chunks = Layout::default()
195                .direction(Direction::Vertical)
196                .margin(2)
197                .constraints(
198                    [
199                        Constraint::Length(3), // Title
200                        Constraint::Min(5),    // List
201                        Constraint::Length(3), // Help
202                    ]
203                    .as_ref(),
204                )
205                .split(size);
206
207            // Title
208            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            // Entry list
219            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            // Help text
265            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        // Handle input
273        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); // Convert to 1-based index
298                    }
299                    KeyCode::Char('r') => {
300                        // Refresh - for now just continue the loop
301                        continue;
302                    }
303                    _ => {}
304                }
305            }
306        }
307    };
308
309    // Restore terminal
310    disable_raw_mode()?;
311    execute!(
312        terminal.backend_mut(),
313        LeaveAlternateScreen,
314        DisableMouseCapture
315    )?;
316    terminal.show_cursor()?;
317
318    result
319}
320
321/// Show current edit mode status
322async 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(&current_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    // Show current Git status
359    println!("\nšŸ“‹ Current state:");
360
361    // TODO: Add Git status information
362    // - Current HEAD vs original commit
363    // - Working directory status
364    // - Staged changes
365
366    println!("   Use 'git status' for detailed working directory status");
367    println!("   Use 'ca entry list' to see all entries");
368
369    Ok(())
370}
371
372/// List all entries in the stack with edit status
373async 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(&current_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        // Status icon
405        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        // Edit mode indicator
416        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        // Basic entry line
425        print!(
426            "   {}. {} {} ({})",
427            entry_num,
428            status_icon,
429            entry.short_message(50),
430            entry.short_hash()
431        );
432
433        // PR information
434        if let Some(pr_id) = &entry.pull_request_id {
435            print!(" PR: #{pr_id}");
436        }
437
438        print!("{edit_indicator}");
439        println!();
440
441        // Verbose information
442        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}