cascade_cli/cli/commands/
entry.rs

1use 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    /// Interactively checkout a stack entry for editing
24    Checkout {
25        /// Stack entry number (optional, shows picker if not provided)
26        #[arg(short, long)]
27        entry: Option<usize>,
28        /// Skip interactive picker and use entry number directly
29        #[arg(long)]
30        direct: bool,
31        /// Skip confirmation prompts
32        #[arg(long, short)]
33        yes: bool,
34    },
35    /// Show current edit mode status
36    Status {
37        /// Show brief status only
38        #[arg(long)]
39        quiet: bool,
40    },
41    /// List all entries with their edit status
42    List {
43        /// Show detailed information
44        #[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
60/// Checkout a specific stack entry for editing
61async 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(&current_dir)?;
70
71    // Get active stack
72    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    // Determine which entry to checkout
83    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 interactive picker
98        show_entry_picker(active_stack).await?
99    };
100
101    let target_entry = &active_stack.entries[target_entry_num - 1]; // Convert to 0-based index
102
103    // Clone the values we need before borrowing manager mutably
104    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    // Check if already in edit mode
113    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            // TODO: Implement interactive confirmation
126            // For now, just warn and exit
127            return Err(CascadeError::config("Exit current edit mode first with 'cc entry status' and handle any pending changes"));
128        }
129    }
130
131    // Confirmation prompt
132    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        // TODO: Implement interactive confirmation with dialoguer
144        // For now, just proceed
145        info!("Skipping confirmation for now - will implement interactive prompt in next step");
146    }
147
148    // Enter edit mode
149    manager.enter_edit_mode(stack_id, entry_id)?;
150
151    // Checkout the commit
152    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(&current_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
170/// Interactive entry picker using TUI
171async fn show_entry_picker(stack: &crate::stack::Stack) -> Result<usize> {
172    // Setup terminal
173    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            // Create layout
187            let chunks = Layout::default()
188                .direction(Direction::Vertical)
189                .margin(2)
190                .constraints(
191                    [
192                        Constraint::Length(3), // Title
193                        Constraint::Min(5),    // List
194                        Constraint::Length(3), // Help
195                    ]
196                    .as_ref(),
197                )
198                .split(size);
199
200            // Title
201            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            // Entry list
212            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            // Help text
258            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        // Handle input
266        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); // Convert to 1-based index
291                    }
292                    KeyCode::Char('r') => {
293                        // Refresh - for now just continue the loop
294                        continue;
295                    }
296                    _ => {}
297                }
298            }
299        }
300    };
301
302    // Restore terminal
303    disable_raw_mode()?;
304    execute!(
305        terminal.backend_mut(),
306        LeaveAlternateScreen,
307        DisableMouseCapture
308    )?;
309    terminal.show_cursor()?;
310
311    result
312}
313
314/// Show current edit mode status
315async 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(&current_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    // Show current Git status
349    println!("\nšŸ“‹ Current state:");
350
351    // TODO: Add Git status information
352    // - Current HEAD vs original commit
353    // - Working directory status
354    // - Staged changes
355
356    println!("   Use 'git status' for detailed working directory status");
357    println!("   Use 'cc entry list' to see all entries");
358
359    Ok(())
360}
361
362/// List all entries in the stack with edit status
363async 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(&current_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        // Status icon
392        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        // Edit mode indicator
403        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        // Basic entry line
412        print!(
413            "   {}. {} {} ({})",
414            entry_num,
415            status_icon,
416            entry.short_message(50),
417            entry.short_hash()
418        );
419
420        // PR information
421        if let Some(pr_id) = &entry.pull_request_id {
422            print!(" PR: #{pr_id}");
423        }
424
425        print!("{edit_indicator}");
426        println!();
427
428        // Verbose information
429        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}