cascade_cli/cli/commands/
entry.rs

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