Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -654,7 +654,6 @@ impl App {
/// }
/// }
/// ```
#[allow(dead_code)]
pub fn picker(
&mut self,
term: &mut Term,
Expand Down
140 changes: 120 additions & 20 deletions src/ops/branch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::{
},
item_data::{ItemData, RefKind},
menu::arg::Arg,
picker::{PickerData, PickerItem, PickerState},
term::Term,
};
use std::{process::Command, rc::Rc};
Expand All @@ -21,16 +22,15 @@ pub(crate) struct Checkout;
impl OpTrait for Checkout {
fn get_action(&self, _target: &ItemData) -> Option<Action> {
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()
},
)?;
let picker = create_branch_picker(app, "Checkout", true)?;
app.close_menu();
let result = app.picker(term, picker)?;

if let Some(data) = result {
let rev = data.display();
checkout(app, term, rev)?;
}

checkout(app, term, &rev)?;
Ok(())
}))
}
Expand All @@ -44,7 +44,6 @@ fn checkout(app: &mut App, term: &mut Term, rev: &str) -> Res<()> {
let mut cmd = Command::new("git");
cmd.args(["checkout", rev]);

app.close_menu();
app.run_cmd(term, &[], cmd)?;
Ok(())
}
Expand Down Expand Up @@ -93,17 +92,15 @@ impl OpTrait for Delete {

Some(Rc::new(move |app: &mut App, term: &mut Term| {
let default = default.clone();
let picker = create_branch_picker_with_default(app, "Delete", true, default)?;
app.close_menu();
let result = app.picker(term, picker)?;

let branch_name = app.prompt(
term,
&PromptParams {
prompt: "Delete",
create_default_value: Box::new(move |_| default.clone()),
..Default::default()
},
)?;
if let Some(data) = result {
let branch_name = data.display();
delete(app, term, branch_name)?;
}

delete(app, term, &branch_name)?;
Ok(())
}))
}
Expand Down Expand Up @@ -132,7 +129,6 @@ pub fn delete(app: &mut App, term: &mut Term, branch_name: &str) -> Res<()> {

cmd.arg(branch_name);

app.close_menu();
app.run_cmd(term, &[], cmd)?;
Ok(())
}
Expand Down Expand Up @@ -226,3 +222,107 @@ impl OpTrait for Spinoff {
"Spinoff branch".into()
}
}

fn create_branch_picker(
app: &App,
prompt: &'static str,
exclude_current: bool,
) -> Result<PickerState, Error> {
create_branch_picker_with_default(app, prompt, exclude_current, selected_rev(app))
}

fn create_branch_picker_with_default(
app: &App,
prompt: &'static str,
exclude_current: bool,
default_rev: Option<String>,
) -> Result<PickerState, Error> {
let mut items = Vec::new();
let mut seen = std::collections::HashSet::new();

// Get current branch name if we need to exclude it
let current_branch = if exclude_current {
app.state
.repo
.head()
.ok()
.and_then(|head| head.shorthand().map(|s| s.to_string()))
} else {
None
};

// Add default value first if it exists and is not current branch
if let Some(ref default) = default_rev
&& Some(default.as_str()) != current_branch.as_deref()
{
items.push(PickerItem::new(
default.clone(),
PickerData::Revision(default.clone()),
));
seen.insert(default.clone());
}

// Get all branches (exclude current if needed)
let branches = app
.state
.repo
.branches(None)
.map_err(Error::ListGitReferences)?;
for branch in branches {
let (branch, _) = branch.map_err(Error::ListGitReferences)?;
if let Some(name) = branch.name().map_err(Error::ListGitReferences)? {
let name = name.to_string();
// Skip current branch and already seen names
if Some(&name) != current_branch.as_ref() && !seen.contains(&name) {
items.push(PickerItem::new(
name.clone(),
PickerData::Revision(name.clone()),
));
seen.insert(name);
}
}
}

// Get all tags
let tag_names = app
.state
.repo
.tag_names(None)
.map_err(Error::ListGitReferences)?;
for tag_name in tag_names.iter().flatten() {
let tag_name = tag_name.to_string();
if !seen.contains(&tag_name) {
items.push(PickerItem::new(
tag_name.clone(),
PickerData::Revision(tag_name.clone()),
));
seen.insert(tag_name);
}
}

// Get all remote branches
let references = app
.state
.repo
.references()
.map_err(Error::ListGitReferences)?;
for reference in references {
let reference = reference.map_err(Error::ListGitReferences)?;
if reference.is_remote()
&& let Some(name) = reference.shorthand()
{
let name = name.to_string();
if !seen.contains(&name) {
items.push(PickerItem::new(
name.clone(),
PickerData::Revision(name.clone()),
));
seen.insert(name);
}
}
}

// Allow custom input to support commit hashes, relative refs (e.g., HEAD~3),
// and other git revisions not in the predefined list
Ok(PickerState::new(prompt, items, true))
}
87 changes: 65 additions & 22 deletions src/tests/branch.rs
Original file line number Diff line number Diff line change
@@ -1,57 +1,93 @@
use super::*;

fn setup(ctx: TestContext) -> TestContext {
run(&ctx.dir, &["git", "checkout", "-b", "merged"]);
run(&ctx.dir, &["git", "checkout", "-b", "unmerged"]);
commit(&ctx.dir, "first commit", "");
fn setup_picker(ctx: TestContext) -> TestContext {
// Create multiple branches and tags for comprehensive testing
run(&ctx.dir, &["git", "checkout", "-b", "feature-a"]);
commit(&ctx.dir, "feature-a commit", "");
run(&ctx.dir, &["git", "checkout", "main"]);

run(&ctx.dir, &["git", "checkout", "-b", "feature-b"]);
commit(&ctx.dir, "feature-b commit", "");
run(&ctx.dir, &["git", "checkout", "main"]);

run(&ctx.dir, &["git", "checkout", "-b", "bugfix-123"]);
commit(&ctx.dir, "bugfix commit", "");
run(&ctx.dir, &["git", "checkout", "main"]);

// Create some tags
run(&ctx.dir, &["git", "tag", "v1.0.0"]);
run(&ctx.dir, &["git", "tag", "v2.0.0"]);

ctx
}

// ==================== Checkout Tests ====================

#[test]
fn branch_menu() {
snapshot!(setup(setup_clone!()), "Yjb");
fn checkout_picker() {
snapshot!(setup_picker(setup_clone!()), "bb");
}

#[test]
fn switch_branch_selected() {
snapshot!(setup(setup_clone!()), "Yjjbb<enter>");
fn checkout_picker_cancel() {
snapshot!(setup_picker(setup_clone!()), "bb<esc>");
}

#[test]
fn switch_branch_input() {
snapshot!(setup(setup_clone!()), "Ybbmerged<enter>");
fn checkout_select_from_list() {
snapshot!(setup_picker(setup_clone!()), "bbfeature-a<enter>");
}

#[test]
fn checkout_new_branch() {
snapshot!(setup(setup_clone!()), "bcnew<enter>");
fn checkout_use_custom_input() {
let ctx = setup_picker(setup_clone!());
// Get the commit hash of the first commit
let output = run(&ctx.dir, &["git", "rev-parse", "HEAD"]);
let commit_hash = output.trim();

snapshot!(ctx, &format!("bb{}<enter>", commit_hash));
}

// ==================== Delete Tests ====================

#[test]
fn delete_picker() {
snapshot!(setup_picker(setup_clone!()), "bK");
}

#[test]
fn delete_branch_selected() {
snapshot!(setup(setup_clone!()), "YjjbK<enter>");
fn delete_picker_cancel() {
snapshot!(setup_picker(setup_clone!()), "bK<esc>");
}

#[test]
fn delete_branch_input() {
snapshot!(setup(setup_clone!()), "bKmerged<enter>");
fn delete_select_from_list() {
snapshot!(setup_picker(setup_clone!()), "bKfeature-a<enter>");
}

#[test]
fn delete_branch_empty() {
snapshot!(setup(setup_clone!()), "bK<enter>");
fn delete_use_custom_input() {
snapshot!(setup_picker(setup_clone!()), "bKfeature-b<enter>");
}

#[test]
fn delete_unmerged_branch() {
let ctx = setup(setup_clone!());
snapshot!(ctx, "bKunmerged<enter>nbKunmerged<enter>y");
let ctx = setup_picker(setup_clone!());
snapshot!(ctx, "bKbugfix-123<enter>nbKbugfix-123<enter>y");
}

// ==================== CheckoutNewBranch Tests ====================

#[test]
fn checkout_new_branch() {
snapshot!(setup_clone!(), "bcnew<enter>");
}

// ==================== Spinoff Tests ====================

#[test]
fn spinoff_branch() {
snapshot!(setup(setup_clone!()), "bsnew<enter>");
snapshot!(setup_picker(setup_clone!()), "bsnew<enter>");
}

#[test]
Expand All @@ -64,5 +100,12 @@ fn spinoff_branch_with_unmerged_commits() {

#[test]
fn spinoff_existing_branch() {
snapshot!(setup(setup_clone!()), "bsunmerged<enter>");
snapshot!(setup_picker(setup_clone!()), "bsfeature-a<enter>");
}

// ==================== Branch Menu Test ====================

#[test]
fn branch_menu() {
snapshot!(setup_picker(setup_clone!()), "b");
}
4 changes: 2 additions & 2 deletions src/tests/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,11 @@ fn move_next_then_parent_section() {
}

#[test]
fn exit_from_prompt_exits_menu() {
fn exit_from_picker_exits_menu() {
snapshot!(setup_clone!(), "bb<esc>");
}

#[test]
fn re_enter_prompt_from_menu() {
fn re_enter_picker_from_menu() {
snapshot!(setup_clone!(), "bb<esc>bb");
}
41 changes: 0 additions & 41 deletions src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,47 +218,6 @@ fn merge_conflict() {
insta::assert_snapshot!(ctx.redact_buffer());
}

#[test]
fn revert_conflict() {
let mut ctx = setup_clone!();
commit(&ctx.dir, "new-file", "hey");
commit(&ctx.dir, "new-file", "hi");

run_ignore_status(&ctx.dir, &["git", "revert", "HEAD~1"]);

ctx.init_app();
insta::assert_snapshot!(ctx.redact_buffer());
}

#[test]
fn revert_abort() {
let ctx = setup_clone!();
commit(&ctx.dir, "new-file", "hey");
commit(&ctx.dir, "new-file", "hi");

run_ignore_status(&ctx.dir, &["git", "revert", "HEAD~1"]);

snapshot!(ctx, "Va");
}

#[test]
fn revert_menu() {
let ctx = setup_clone!();
snapshot!(ctx, "llV");
}

#[test]
fn revert_commit_prompt() {
let ctx = setup_clone!();
snapshot!(ctx, "llVV");
}

#[test]
fn revert_commit() {
let ctx = setup_clone!();
snapshot!(ctx, "llV-EV<enter>");
}

#[test]
fn moved_file() {
let mut ctx = setup_clone!();
Expand Down
Loading