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        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            Output::warning("Already in edit mode!");
123            Output::sub_item(format!(
124                "Current target: {} (TODO: get commit message)",
125                &edit_info.original_commit_hash[..8]
126            ));
127            Output::info("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        Output::section("Checking out entry for editing");
138        Output::sub_item(format!(
139            "Entry #{target_entry_num}: {entry_short_hash} ({entry_short_message})"
140        ));
141        Output::sub_item(format!("Branch: {entry_branch}"));
142        if let Some(pr_id) = &entry_pr_id {
143            Output::sub_item(format!("PR: #{pr_id}"));
144        }
145        Output::warning("This will checkout the commit and enter edit mode.");
146        Output::info("Any changes you make can be amended to this commit or create new entries.");
147        Output::info("\nContinue? [y/N]");
148
149        // TODO: Implement interactive confirmation with dialoguer
150        // For now, just proceed
151        info!("Skipping confirmation for now - will implement interactive prompt in next step");
152    }
153
154    // Enter edit mode
155    manager.enter_edit_mode(stack_id, entry_id)?;
156
157    // Checkout the commit
158    let current_dir = env::current_dir()
159        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
160
161    let repo_root = find_repository_root(&current_dir)
162        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
163    let repo = crate::git::GitRepository::open(&repo_root)?;
164
165    info!("Checking out commit: {}", entry_commit_hash);
166    repo.checkout_commit(&entry_commit_hash)?;
167
168    Output::success(format!("Entered edit mode for entry #{target_entry_num}"));
169    Output::sub_item(format!(
170        "You are now on commit: {entry_short_hash} ({entry_short_message})"
171    ));
172    Output::sub_item(format!("Branch: {entry_branch}"));
173
174    Output::section("Make your changes and commit normally");
175    Output::bullet("Use 'ca entry status' to see edit mode info");
176    Output::bullet("Use 'git commit --amend' to modify this entry");
177    Output::bullet("Use 'git commit' to create a new entry on top");
178    Output::bullet("Run 'ca sync' after committing to update PRs");
179
180    // Check if prepare-commit-msg hook is installed
181    let hooks_dir = repo_root.join(".git/hooks");
182    let hook_path = hooks_dir.join("prepare-commit-msg");
183    if !hook_path.exists() {
184        Output::tip("Install the prepare-commit-msg hook for better guidance:");
185        Output::sub_item("ca hooks add prepare-commit-msg");
186    }
187
188    Ok(())
189}
190
191/// Interactive entry picker using TUI
192async fn show_entry_picker(stack: &crate::stack::Stack) -> Result<usize> {
193    // Setup terminal
194    enable_raw_mode()?;
195    let mut stdout = io::stdout();
196    execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
197    let backend = CrosstermBackend::new(stdout);
198    let mut terminal = Terminal::new(backend)?;
199
200    let mut list_state = ListState::default();
201    list_state.select(Some(0));
202
203    let result = loop {
204        terminal.draw(|f| {
205            let size = f.size();
206
207            // Create layout
208            let chunks = Layout::default()
209                .direction(Direction::Vertical)
210                .margin(2)
211                .constraints(
212                    [
213                        Constraint::Length(3), // Title
214                        Constraint::Min(5),    // List
215                        Constraint::Length(3), // Help
216                    ]
217                    .as_ref(),
218                )
219                .split(size);
220
221            // Title
222            let title = Paragraph::new(format!("📚 Select Entry from Stack: {}", stack.name))
223                .style(
224                    Style::default()
225                        .fg(Color::Cyan)
226                        .add_modifier(Modifier::BOLD),
227                )
228                .alignment(Alignment::Center)
229                .block(Block::default().borders(Borders::ALL));
230            f.render_widget(title, chunks[0]);
231
232            // Entry list
233            let items: Vec<ListItem> = stack
234                .entries
235                .iter()
236                .enumerate()
237                .map(|(i, entry)| {
238                    let status_icon = if entry.is_submitted {
239                        if entry.pull_request_id.is_some() {
240                            "📤"
241                        } else {
242                            "📝"
243                        }
244                    } else {
245                        "🔄"
246                    };
247
248                    let pr_text = if let Some(pr_id) = &entry.pull_request_id {
249                        format!(" PR: #{pr_id}")
250                    } else {
251                        "".to_string()
252                    };
253
254                    let line = Line::from(vec![
255                        Span::raw(format!("  {}. ", i + 1)),
256                        Span::raw(status_icon),
257                        Span::raw(" "),
258                        Span::styled(entry.short_message(40), Style::default().fg(Color::White)),
259                        Span::raw(" "),
260                        Span::styled(
261                            format!("({})", entry.short_hash()),
262                            Style::default().fg(Color::Yellow),
263                        ),
264                        Span::styled(pr_text, Style::default().fg(Color::Green)),
265                    ]);
266
267                    ListItem::new(line)
268                })
269                .collect();
270
271            let list = List::new(items)
272                .block(Block::default().borders(Borders::ALL).title("Entries"))
273                .highlight_style(Style::default().fg(Color::Black).bg(Color::Cyan))
274                .highlight_symbol("→ ");
275
276            f.render_stateful_widget(list, chunks[1], &mut list_state);
277
278            // Help text
279            let help = Paragraph::new("↑/↓: Navigate • Enter: Select • q: Quit • r: Refresh")
280                .style(Style::default().fg(Color::DarkGray))
281                .alignment(Alignment::Center)
282                .block(Block::default().borders(Borders::ALL));
283            f.render_widget(help, chunks[2]);
284        })?;
285
286        // Handle input
287        if let Event::Key(key) = event::read()? {
288            if key.kind == KeyEventKind::Press {
289                match key.code {
290                    KeyCode::Char('q') => {
291                        break Err(CascadeError::config("Entry selection cancelled"));
292                    }
293                    KeyCode::Up => {
294                        let selected = list_state.selected().unwrap_or(0);
295                        if selected > 0 {
296                            list_state.select(Some(selected - 1));
297                        } else {
298                            list_state.select(Some(stack.entries.len() - 1));
299                        }
300                    }
301                    KeyCode::Down => {
302                        let selected = list_state.selected().unwrap_or(0);
303                        if selected < stack.entries.len() - 1 {
304                            list_state.select(Some(selected + 1));
305                        } else {
306                            list_state.select(Some(0));
307                        }
308                    }
309                    KeyCode::Enter => {
310                        let selected = list_state.selected().unwrap_or(0);
311                        break Ok(selected + 1); // Convert to 1-based index
312                    }
313                    KeyCode::Char('r') => {
314                        // Refresh - for now just continue the loop
315                        continue;
316                    }
317                    _ => {}
318                }
319            }
320        }
321    };
322
323    // Restore terminal
324    disable_raw_mode()?;
325    execute!(
326        terminal.backend_mut(),
327        LeaveAlternateScreen,
328        DisableMouseCapture
329    )?;
330    terminal.show_cursor()?;
331
332    result
333}
334
335/// Show current edit mode status
336async fn show_edit_status(quiet: bool) -> Result<()> {
337    let current_dir = env::current_dir()
338        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
339
340    let repo_root = find_repository_root(&current_dir)
341        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
342    let manager = StackManager::new(&repo_root)?;
343
344    if !manager.is_in_edit_mode() {
345        if quiet {
346            println!("inactive");
347        } else {
348            Output::info("Not in edit mode");
349            Output::sub_item("Use 'ca entry checkout' to start editing a stack entry");
350        }
351        return Ok(());
352    }
353
354    let edit_info = manager.get_edit_mode_info().unwrap();
355
356    if quiet {
357        println!("active:{:?}", edit_info.target_entry_id);
358        return Ok(());
359    }
360
361    Output::section("Currently in edit mode");
362    Output::sub_item(format!("Target entry: {:?}", edit_info.target_entry_id));
363    Output::sub_item(format!(
364        "Original commit: {}",
365        &edit_info.original_commit_hash[..8]
366    ));
367    Output::sub_item(format!(
368        "Started: {}",
369        edit_info.started_at.format("%Y-%m-%d %H:%M:%S")
370    ));
371
372    // Show current Git status
373    Output::section("Current state");
374
375    // TODO: Add Git status information
376    // - Current HEAD vs original commit
377    // - Working directory status
378    // - Staged changes
379
380    Output::sub_item("Use 'git status' for detailed working directory status");
381    Output::sub_item("Use 'ca entry list' to see all entries");
382
383    Ok(())
384}
385
386/// List all entries in the stack with edit status
387async fn list_entries(verbose: bool) -> Result<()> {
388    let current_dir = env::current_dir()
389        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
390
391    let repo_root = find_repository_root(&current_dir)
392        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
393    let manager = StackManager::new(&repo_root)?;
394
395    let active_stack = manager.get_active_stack().ok_or_else(|| {
396        CascadeError::config(
397            "No active stack. Create a stack first with 'ca stack create'".to_string(),
398        )
399    })?;
400
401    if active_stack.entries.is_empty() {
402        Output::info(format!(
403            "Active stack '{}' has no entries yet",
404            active_stack.name
405        ));
406        Output::sub_item("Add some commits to the stack with 'ca stack push'");
407        return Ok(());
408    }
409
410    Output::section(format!(
411        "Stack: {} ({} entries)",
412        active_stack.name,
413        active_stack.entries.len()
414    ));
415
416    let edit_mode_info = manager.get_edit_mode_info();
417
418    for (i, entry) in active_stack.entries.iter().enumerate() {
419        let entry_num = i + 1;
420
421        // Status icon
422        let status_icon = if entry.is_submitted {
423            if entry.pull_request_id.is_some() {
424                "📤"
425            } else {
426                "📝"
427            }
428        } else {
429            "🔄"
430        };
431
432        // Edit mode indicator
433        let edit_indicator = if edit_mode_info.is_some()
434            && edit_mode_info.unwrap().target_entry_id == Some(entry.id)
435        {
436            " 🎯"
437        } else {
438            ""
439        };
440
441        // Basic entry line
442        print!(
443            "   {}. {} {} ({})",
444            entry_num,
445            status_icon,
446            entry.short_message(50),
447            entry.short_hash()
448        );
449
450        // PR information
451        if let Some(pr_id) = &entry.pull_request_id {
452            print!(" PR: #{pr_id}");
453        }
454
455        print!("{edit_indicator}");
456        println!();
457
458        // Verbose information
459        if verbose {
460            println!("      Branch: {}", entry.branch);
461            println!(
462                "      Created: {}",
463                entry.created_at.format("%Y-%m-%d %H:%M:%S")
464            );
465            if entry.is_submitted {
466                println!("      Status: Submitted");
467            } else {
468                println!("      Status: Draft");
469            }
470            println!();
471        }
472    }
473
474    if let Some(_edit_info) = edit_mode_info {
475        println!();
476        Output::info("Edit mode active - use 'ca entry status' for details");
477    } else {
478        println!();
479        Output::tip("Use 'ca entry checkout' to start editing an entry");
480    }
481
482    Ok(())
483}