diff --git a/src/commands/interactive/command.rs b/src/commands/interactive/command.rs index 05ebef6..292a266 100644 --- a/src/commands/interactive/command.rs +++ b/src/commands/interactive/command.rs @@ -14,7 +14,7 @@ use ratatui::{ }; use super::{ - Action, EventSource, Focus, StatusMessage, WorktreeEntry, + Action, EventSource, Focus, Selection, StatusMessage, WorktreeEntry, dialog::{CreateDialog, CreateDialogFocus, Dialog}, view::{DetailData, DialogView, Snapshot}, }; @@ -72,7 +72,7 @@ where } } - pub fn run(mut self, mut on_remove: F, mut on_create: G) -> Result> + pub fn run(mut self, mut on_remove: F, mut on_create: G) -> Result> where F: FnMut(&str) -> Result<()>, G: FnMut(&str, Option<&str>) -> Result<()>, @@ -90,7 +90,11 @@ where result } - fn event_loop(&mut self, on_remove: &mut F, on_create: &mut G) -> Result> + fn event_loop( + &mut self, + on_remove: &mut F, + on_create: &mut G, + ) -> Result> where F: FnMut(&str) -> Result<()>, G: FnMut(&str, Option<&str>) -> Result<()>, @@ -203,7 +207,9 @@ where Focus::Worktrees => { if let Some(index) = self.selected { return Ok(LoopControl::Exit( - self.worktrees.get(index).map(|entry| entry.name.clone()), + self.worktrees + .get(index) + .map(|entry| Selection::Worktree(entry.name.clone())), )); } } @@ -212,7 +218,9 @@ where match action { Action::Open => { if let Some(entry) = self.current_entry() { - return Ok(LoopControl::Exit(Some(entry.name.clone()))); + return Ok(LoopControl::Exit(Some(Selection::Worktree( + entry.name.clone(), + )))); } self.status = Some(StatusMessage::info("No worktree selected.")); } @@ -224,6 +232,14 @@ where Some(StatusMessage::info("No worktree selected to remove.")); } } + Action::PrGithub => { + if let Some(entry) = self.current_entry() { + return Ok(LoopControl::Exit(Some(Selection::PrGithub( + entry.name.clone(), + )))); + } + self.status = Some(StatusMessage::info("No worktree selected.")); + } } } Focus::GlobalActions => match self.global_action_selected { @@ -233,9 +249,7 @@ where self.dialog = Some(Dialog::Create(dialog)); } 1 => { - return Ok(LoopControl::Exit(Some( - super::REPO_ROOT_SELECTION.to_string(), - ))); + return Ok(LoopControl::Exit(Some(Selection::RepoRoot))); } _ => {} }, @@ -641,7 +655,7 @@ where enum LoopControl { Continue, - Exit(Option), + Exit(Option), } fn build_detail_data(entry: &WorktreeEntry) -> DetailData { diff --git a/src/commands/interactive/mod.rs b/src/commands/interactive/mod.rs index 33d017b..da73197 100644 --- a/src/commands/interactive/mod.rs +++ b/src/commands/interactive/mod.rs @@ -55,26 +55,34 @@ impl Focus { } pub(crate) const GLOBAL_ACTIONS: [&str; 2] = ["Create worktree", "Cd to root dir"]; -pub(crate) const REPO_ROOT_SELECTION: &str = "__RSWORKTREE_REPO_ROOT__"; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) enum Selection { + Worktree(String), + PrGithub(String), + RepoRoot, +} #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) enum Action { Open, Remove, + PrGithub, } impl Action { - pub(crate) const ALL: [Action; 2] = [Action::Open, Action::Remove]; + pub(crate) const ALL: [Action; 3] = [Action::Open, Action::Remove, Action::PrGithub]; pub(crate) fn label(self) -> &'static str { match self { Action::Open => "Open", Action::Remove => "Remove", + Action::PrGithub => "PR (GitHub)", } } pub(crate) fn requires_selection(self) -> bool { - matches!(self, Action::Open | Action::Remove) + matches!(self, Action::Open | Action::Remove | Action::PrGithub) } pub(crate) fn from_index(index: usize) -> Self { diff --git a/src/commands/interactive/runtime.rs b/src/commands/interactive/runtime.rs index dc6b144..f22e22f 100644 --- a/src/commands/interactive/runtime.rs +++ b/src/commands/interactive/runtime.rs @@ -14,11 +14,12 @@ use crate::{ cd::{CdCommand, shell_command}, create::{CreateCommand, CreateOutcome}, list::{find_worktrees, format_worktree}, + pr_github::{PrGithubCommand, PrGithubOptions}, rm::RemoveCommand, }, }; -use super::{EventSource, REPO_ROOT_SELECTION, WorktreeEntry, command::InteractiveCommand}; +use super::{EventSource, Selection, WorktreeEntry, command::InteractiveCommand}; pub struct CrosstermEvents; @@ -91,12 +92,29 @@ pub fn run(repo: &Repo) -> Result<()> { } }; - if let Some(name) = selection { - if name == REPO_ROOT_SELECTION { - cd_repo_root(repo)?; - } else { - let command = CdCommand::new(name, false); - command.execute(repo)?; + if let Some(selection) = selection { + match selection { + Selection::RepoRoot => { + cd_repo_root(repo)?; + } + Selection::Worktree(name) => { + let command = CdCommand::new(name, false); + command.execute(repo)?; + } + Selection::PrGithub(name) => { + let options = PrGithubOptions { + name, + push: true, + draft: false, + fill: false, + web: false, + remote: String::from("origin"), + reviewers: Vec::new(), + extra_args: Vec::new(), + }; + let mut command = PrGithubCommand::new(options); + command.execute(repo)?; + } } } diff --git a/src/commands/interactive/tests.rs b/src/commands/interactive/tests.rs index d7b89d3..e2a6c94 100644 --- a/src/commands/interactive/tests.rs +++ b/src/commands/interactive/tests.rs @@ -58,7 +58,7 @@ fn returns_first_worktree_when_enter_pressed_immediately() -> Result<()> { let selection = command .run(|_| Ok(()), |_, _| panic!("create should not be called"))? .expect("expected selection"); - assert_eq!(selection, "alpha"); + assert_eq!(selection, Selection::Worktree(String::from("alpha"))); Ok(()) } @@ -81,7 +81,42 @@ fn navigates_down_before_selecting() -> Result<()> { let selection = command .run(|_| Ok(()), |_, _| panic!("create should not be called"))? .expect("expected selection"); - assert_eq!(selection, "beta"); + assert_eq!(selection, Selection::Worktree(String::from("beta"))); + + Ok(()) +} + +#[test] +fn selecting_pr_github_action_exits_with_pr_variant() -> Result<()> { + let backend = TestBackend::new(40, 12); + let terminal = Terminal::new(backend)?; + let events = StubEvents::new(vec![ + key(KeyCode::Tab), + key(KeyCode::Down), + key(KeyCode::Down), + key(KeyCode::Enter), + ]); + let worktrees = entries(&["alpha", "beta"]); + let command = InteractiveCommand::new( + terminal, + events, + PathBuf::from("/tmp/worktrees"), + worktrees, + vec![String::from("main")], + Some(String::from("main")), + ); + + let mut removed = Vec::new(); + let result = command.run( + |name| { + removed.push(name.to_owned()); + Ok(()) + }, + |_, _| panic!("create should not be called"), + )?; + + assert!(removed.is_empty(), "remove should not be triggered"); + assert_eq!(result, Some(Selection::PrGithub(String::from("alpha")))); Ok(()) } @@ -199,7 +234,7 @@ fn create_action_adds_new_worktree() -> Result<()> { }, )?; - assert_eq!(result, Some(String::from("new"))); + assert_eq!(result, Some(Selection::Worktree(String::from("new")))); assert_eq!( created, vec![(String::from("new"), Some(String::from("main")))] @@ -260,7 +295,7 @@ fn cd_to_root_global_action_exits() -> Result<()> { let result = command.run(|_| Ok(()), |_, _| Ok(()))?; - assert_eq!(result, Some(String::from(super::REPO_ROOT_SELECTION))); + assert_eq!(result, Some(Selection::RepoRoot)); Ok(()) } @@ -287,7 +322,7 @@ fn up_from_top_moves_to_global_actions() -> Result<()> { let result = command.run(|_| Ok(()), |_, _| Ok(()))?; - assert_eq!(result, Some(String::from(super::REPO_ROOT_SELECTION))); + assert_eq!(result, Some(Selection::RepoRoot)); Ok(()) }