diff --git a/src/ops/branch.rs b/src/ops/branch.rs index 825234982d..7f2143a7d8 100644 --- a/src/ops/branch.rs +++ b/src/ops/branch.rs @@ -9,8 +9,10 @@ use crate::{ }, item_data::{ItemData, RefKind}, menu::arg::Arg, + picker::PickerState, term::Term, }; + use std::{process::Command, rc::Rc}; pub(crate) fn init_args() -> Vec { @@ -21,17 +23,16 @@ pub(crate) struct Checkout; impl OpTrait for Checkout { fn get_action(&self, _target: &ItemData) -> Option { Some(Rc::new(move |app: &mut App, term: &mut Term| { - let rev = app.prompt( - term, - &PromptParams { - prompt: "Checkout", - create_default_value: Box::new(selected_rev), - ..Default::default() - }, - )?; - - checkout(app, term, &rev)?; - Ok(()) + let picker = PickerState::for_branches("Checkout", &app.state.repo, selected_rev(app))?; + match app.picker(term, picker)? { + Some(data) => checkout(app, term, data.display()), + None => { + // TODO: necessary to make sure parent menu closes, shouldn't this be + // handled by .picker, like .prompt does? + app.close_menu(); + Ok(()) + } + } })) } @@ -53,15 +54,28 @@ pub(crate) struct CheckoutNewBranch; impl OpTrait for CheckoutNewBranch { fn get_action(&self, _target: &ItemData) -> Option { Some(Rc::new(|app: &mut App, term: &mut Term| { + let start_point_picker = PickerState::for_branches( + "Create branch starting at", + &app.state.repo, + Some(get_current_branch_name(&app.state.repo)?), + )?; + + let Some(starting_point) = app.picker(term, start_point_picker)? else { + // TODO: necessary to make sure parent menu closes, shouldn't this be + // handled by .picker, like .prompt does? + app.close_menu(); + return Ok(()); + }; + let branch_name = app.prompt( term, &PromptParams { - prompt: "Create and checkout branch:", + prompt: "Create and checkout branch", ..Default::default() }, )?; - checkout_new_branch_prompt_update(app, term, &branch_name)?; + checkout_new_branch_prompt_update(app, term, &branch_name, starting_point.display())?; Ok(()) })) } @@ -71,9 +85,14 @@ impl OpTrait for CheckoutNewBranch { } } -fn checkout_new_branch_prompt_update(app: &mut App, term: &mut Term, branch_name: &str) -> Res<()> { +fn checkout_new_branch_prompt_update( + app: &mut App, + term: &mut Term, + branch_name: &str, + starting_point: &str, +) -> Res<()> { let mut cmd = Command::new("git"); - cmd.args(["checkout", "-b", branch_name]); + cmd.args(["checkout", "-b", branch_name, starting_point]); app.close_menu(); app.run_cmd(term, &[], cmd)?; diff --git a/src/picker.rs b/src/picker.rs index ec70302ffa..991a5ad73b 100644 --- a/src/picker.rs +++ b/src/picker.rs @@ -1,5 +1,8 @@ +use crate::error::Error; use fuzzy_matcher::FuzzyMatcher; use fuzzy_matcher::skim::SkimMatcherV2; +use git2::Repository; +use itertools::Itertools; use std::borrow::Cow; use tui_prompts::State as _; use tui_prompts::TextState; @@ -106,6 +109,57 @@ impl PickerState { state } + // Creates a new picker displaying the given branches sorted from local to remote. + // + // The picker also allows to pick a custom revision, i.e. a commit hash or tag. + // + // If a default revision is provided, it is at the top and selected by default. + // + // The current branch and any invalid branches are excluded. + pub fn for_branches( + prompt: impl Into>, + repo: &Repository, + default_revision: Option, + ) -> Result { + // Collect and sort all branches (local and remote) excluding the current branch & + // default_value, if there's any. + let mut branches: Vec = repo + .branches(None) + .map_err(Error::ListGitReferences)? + .filter_map(Result::ok) + .filter_map(|(branch, _)| { + if branch.is_head() { + return None; + } + + let name = branch.name().ok()??; + + // The default revision will be added to the top below, + // so filter it out. + if let Some(ref rev) = default_revision + && rev == name + { + return None; + } + + // Remote is only used for sorting + Some((branch.get().is_remote(), name.to_string())) + }) + .sorted() + .map(|(_remote, branch_name)| { + PickerItem::new(branch_name.clone(), PickerData::Revision(branch_name)) + }) + .collect(); + + // Add the default revision to the top, so it's selected by default and + // can be accepted by without any extra steps. + if let Some(rev) = default_revision { + branches.insert(0, PickerItem::new(rev.clone(), PickerData::Revision(rev))); + } + + Ok(Self::new(prompt, branches, true)) + } + /// Get current input pattern pub fn pattern(&self) -> &str { self.input_state.value() diff --git a/src/tests/branch.rs b/src/tests/branch.rs index ce189cf4d1..cc52b7b1f4 100644 --- a/src/tests/branch.rs +++ b/src/tests/branch.rs @@ -23,9 +23,34 @@ fn switch_branch_input() { snapshot!(setup(setup_clone!()), "Ybbmerged"); } +#[test] +fn switch_branch_picker() { + snapshot!(setup(setup_clone!()), "bb"); +} + +#[test] +fn switch_branch_selected_revision_picker() { + snapshot!(setup(setup_clone!()), "Yjjbb"); +} + +#[test] +fn checkout_new_branch_starting_point_picker() { + snapshot!(setup(setup_clone!()), "bc"); +} + +#[test] +fn checkout_new_branch_starting_point_picker_from_selected_rev() { + snapshot!(setup(setup_clone!()), "Yjjbc"); +} + +#[test] +fn checkout_new_branch_name_prompt() { + snapshot!(setup(setup_clone!()), "bc"); +} + #[test] fn checkout_new_branch() { - snapshot!(setup(setup_clone!()), "bcnew"); + snapshot!(setup(setup_clone!()), "bcnew"); } #[test] diff --git a/src/tests/snapshots/gitu__tests__branch__checkout_new_branch.snap b/src/tests/snapshots/gitu__tests__branch__checkout_new_branch.snap index 3b0cc6f1ef..1322791533 100644 --- a/src/tests/snapshots/gitu__tests__branch__checkout_new_branch.snap +++ b/src/tests/snapshots/gitu__tests__branch__checkout_new_branch.snap @@ -20,6 +20,6 @@ expression: ctx.redact_buffer() | | ────────────────────────────────────────────────────────────────────────────────| -$ git checkout -b new | +$ git checkout -b new main | Switched to a new branch 'new' | -styles_hash: 2afc72138214b087 +styles_hash: 51d00da50a627344 diff --git a/src/tests/snapshots/gitu__tests__branch__checkout_new_branch_name_prompt.snap b/src/tests/snapshots/gitu__tests__branch__checkout_new_branch_name_prompt.snap new file mode 100644 index 0000000000..335fb34ee4 --- /dev/null +++ b/src/tests/snapshots/gitu__tests__branch__checkout_new_branch_name_prompt.snap @@ -0,0 +1,25 @@ +--- +source: src/tests/branch.rs +expression: ctx.redact_buffer() +--- +▌On branch main | +▌Your branch is up to date with 'origin/main'. | + | + Recent commits | + b66a0bf main merged origin/main add initial-file | + | + | + | + | + | + | + | + | + | + | + | + | + | +────────────────────────────────────────────────────────────────────────────────| +? Create and checkout branch: › █ | +styles_hash: 5475e2883a3b1774 diff --git a/src/tests/snapshots/gitu__tests__branch__checkout_new_branch_starting_point_picker.snap b/src/tests/snapshots/gitu__tests__branch__checkout_new_branch_starting_point_picker.snap new file mode 100644 index 0000000000..8a9cd88b8c --- /dev/null +++ b/src/tests/snapshots/gitu__tests__branch__checkout_new_branch_starting_point_picker.snap @@ -0,0 +1,25 @@ +--- +source: src/tests/branch.rs +expression: ctx.redact_buffer() +--- + On branch main | + Your branch is up to date with 'origin/main'. | + | + Recent commits | + b66a0bf main merged origin/main add initial-file | + | + | + | +────────────────────────────────────────────────────────────────────────────────| + 5/5 Create branch starting at › █ | +▌main | + merged | + unmerged | + origin/HEAD | + origin/main | + | + | + | + | + | +styles_hash: 106946388e06558f diff --git a/src/tests/snapshots/gitu__tests__branch__checkout_new_branch_starting_point_picker_from_selected_rev.snap b/src/tests/snapshots/gitu__tests__branch__checkout_new_branch_starting_point_picker_from_selected_rev.snap new file mode 100644 index 0000000000..1608419f41 --- /dev/null +++ b/src/tests/snapshots/gitu__tests__branch__checkout_new_branch_starting_point_picker_from_selected_rev.snap @@ -0,0 +1,25 @@ +--- +source: src/tests/branch.rs +expression: ctx.redact_buffer() +--- + Branches | + * main | + merged | + unmerged | + | + Remote origin | + origin/HEAD | + origin/main | +────────────────────────────────────────────────────────────────────────────────| + 5/5 Create branch starting at › █ | +▌main | + merged | + unmerged | + origin/HEAD | + origin/main | + | + | + | + | + | +styles_hash: e48ad585ed1f40a5 diff --git a/src/tests/snapshots/gitu__tests__branch__switch_branch_picker.snap b/src/tests/snapshots/gitu__tests__branch__switch_branch_picker.snap new file mode 100644 index 0000000000..0e7ba2891d --- /dev/null +++ b/src/tests/snapshots/gitu__tests__branch__switch_branch_picker.snap @@ -0,0 +1,25 @@ +--- +source: src/tests/branch.rs +expression: ctx.redact_buffer() +--- + On branch main | + Your branch is up to date with 'origin/main'. | + | + Recent commits | + b66a0bf main merged origin/main add initial-file | + | + | + | +────────────────────────────────────────────────────────────────────────────────| + 4/4 Checkout › █ | +▌merged | + unmerged | + origin/HEAD | + origin/main | + | + | + | + | + | + | +styles_hash: a64f5b0f5c87b3e5 diff --git a/src/tests/snapshots/gitu__tests__branch__switch_branch_selected_revision_picker.snap b/src/tests/snapshots/gitu__tests__branch__switch_branch_selected_revision_picker.snap new file mode 100644 index 0000000000..307126511c --- /dev/null +++ b/src/tests/snapshots/gitu__tests__branch__switch_branch_selected_revision_picker.snap @@ -0,0 +1,25 @@ +--- +source: src/tests/branch.rs +expression: ctx.redact_buffer() +--- + Branches | + * main | + merged | + unmerged | + | + Remote origin | + origin/HEAD | + origin/main | +────────────────────────────────────────────────────────────────────────────────| + 4/4 Checkout › █ | +▌merged | + unmerged | + origin/HEAD | + origin/main | + | + | + | + | + | + | +styles_hash: 2cac6f3d25b0271c diff --git a/src/tests/snapshots/gitu__tests__editor__re_enter_prompt_from_menu.snap b/src/tests/snapshots/gitu__tests__editor__re_enter_prompt_from_menu.snap index 225503bda1..eea969d927 100644 --- a/src/tests/snapshots/gitu__tests__editor__re_enter_prompt_from_menu.snap +++ b/src/tests/snapshots/gitu__tests__editor__re_enter_prompt_from_menu.snap @@ -2,14 +2,18 @@ source: src/tests/editor.rs expression: ctx.redact_buffer() --- -▌On branch main | -▌Your branch is up to date with 'origin/main'. | + On branch main | + Your branch is up to date with 'origin/main'. | | Recent commits | b66a0bf main origin/main add initial-file | | | | +────────────────────────────────────────────────────────────────────────────────| + 2/2 Checkout › █ | +▌origin/HEAD | + origin/main | | | | @@ -18,8 +22,4 @@ expression: ctx.redact_buffer() | | | - | - | -────────────────────────────────────────────────────────────────────────────────| -? Checkout: › █ | -styles_hash: 43da189f53b3be3d +styles_hash: 49e995be810c0237