Skip to content

Commit 2b74d9d

Browse files
sei40krclaude
andauthored
feat: use new interactive picker when merging (#496)
* feat: convert merge operation to interactive picker - Convert Merge operation to use interactive picker with fuzzy search - Add comprehensive test coverage with 5 test cases - Exclude current branch from merge target list - Support custom input for commit hashes and relative refs - All tests verify operations execute correctly 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * feat(merge): handle duplicate ref names and exclude current ref from picker - Exclude current ref from merge picker whether it's a branch, tag, or remote - Add RefKind::to_full_refname() and shorthand() helper methods - Handle duplicate branch/tag names with refs/ prefixes - Use full refnames for git merge when duplicates exist - Add comprehensive tests for edge cases with duplicate names * refactor: extract RefKind::from_reference() to eliminate duplicate logic Consolidate the logic for converting git2::Reference to RefKind by adding a from_reference() method. This removes duplicate code across merge.rs and show_refs.rs. * refactor: remove unnecessary comment * refactor: extract PickerState::with_refs to simplify merge operation Add PickerState::with_refs method to encapsulate the complex logic of building a picker from git references. This method automatically handles duplicate detection, sorting (default -> branches -> tags -> remotes), and filtering of excluded references. Simplify merge.rs by delegating the picker construction logic to the new with_refs method, reducing code complexity from ~110 lines to ~50 lines. --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 6d16d79 commit 2b74d9d

12 files changed

+631
-31
lines changed

src/item_data.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,37 @@ pub(crate) enum RefKind {
7474
Remote(String),
7575
}
7676

77+
impl RefKind {
78+
/// Convert to fully qualified refname (e.g., "refs/heads/main", "refs/tags/v1.0.0")
79+
pub(crate) fn to_full_refname(&self) -> String {
80+
match self {
81+
RefKind::Branch(name) => format!("refs/heads/{}", name),
82+
RefKind::Tag(name) => format!("refs/tags/{}", name),
83+
RefKind::Remote(name) => format!("refs/remotes/{}", name),
84+
}
85+
}
86+
87+
/// Get the shorthand name without refs/ prefix
88+
pub(crate) fn shorthand(&self) -> &str {
89+
match self {
90+
RefKind::Branch(name) | RefKind::Tag(name) | RefKind::Remote(name) => name,
91+
}
92+
}
93+
94+
/// Convert a git2::Reference to RefKind, returning None if the reference has no shorthand
95+
pub(crate) fn from_reference(reference: &git2::Reference<'_>) -> Option<Self> {
96+
let shorthand = reference.shorthand()?.to_string();
97+
98+
Some(if reference.is_branch() {
99+
RefKind::Branch(shorthand)
100+
} else if reference.is_tag() {
101+
RefKind::Tag(shorthand)
102+
} else {
103+
RefKind::Remote(shorthand)
104+
})
105+
}
106+
}
107+
77108
#[derive(Clone, Debug)]
78109
pub(crate) enum SectionHeader {
79110
Remote(String),

src/ops/merge.rs

Lines changed: 56 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1-
use super::{Action, OpTrait, selected_rev};
1+
use super::{Action, OpTrait};
2+
use crate::item_data::RefKind;
23
use crate::{
34
Res,
4-
app::{App, PromptParams, State},
5+
app::{App, State},
6+
error::Error,
57
item_data::ItemData,
68
menu::arg::Arg,
9+
picker::PickerState,
710
term::Term,
811
};
12+
913
use std::{process::Command, rc::Rc};
1014

1115
pub(crate) fn init_args() -> Vec<Arg> {
@@ -64,18 +68,56 @@ fn merge(app: &mut App, term: &mut Term, rev: &str) -> Res<()> {
6468

6569
pub(crate) struct Merge;
6670
impl OpTrait for Merge {
67-
fn get_action(&self, _target: &ItemData) -> Option<Action> {
68-
Some(Rc::new(|app: &mut App, term: &mut Term| {
69-
let rev = app.prompt(
70-
term,
71-
&PromptParams {
72-
prompt: "Merge",
73-
create_default_value: Box::new(selected_rev),
74-
..Default::default()
75-
},
76-
)?;
77-
78-
merge(app, term, &rev)?;
71+
fn get_action(&self, target: &ItemData) -> Option<Action> {
72+
// Extract default ref from target if it's a Reference
73+
let default_ref = if let ItemData::Reference { kind, .. } = target {
74+
Some(kind.clone())
75+
} else {
76+
None
77+
};
78+
79+
Some(Rc::new(move |app: &mut App, term: &mut Term| {
80+
// Get current HEAD reference to exclude it from picker
81+
let exclude_ref = {
82+
let head = app.state.repo.head().map_err(Error::GetHead)?;
83+
RefKind::from_reference(&head)
84+
};
85+
86+
// Collect all branches (local and remote)
87+
let branches = app
88+
.state
89+
.repo
90+
.branches(None)
91+
.map_err(Error::ListGitReferences)?
92+
.filter_map(|branch| {
93+
let (branch, _) = branch.ok()?;
94+
RefKind::from_reference(branch.get())
95+
});
96+
97+
// Collect all tags
98+
let tags: Vec<RefKind> = app
99+
.state
100+
.repo
101+
.tag_names(None)
102+
.map_err(Error::ListGitReferences)?
103+
.into_iter()
104+
.flatten()
105+
.map(|tag_name| RefKind::Tag(tag_name.to_string()))
106+
.collect();
107+
108+
let all_refs: Vec<RefKind> = branches.chain(tags).collect();
109+
110+
// Allow custom input to support commit hashes, relative refs (e.g., HEAD~3),
111+
// and other git revisions not in the predefined list
112+
let picker =
113+
PickerState::with_refs("Merge", all_refs, exclude_ref, default_ref.clone(), true);
114+
let result = app.picker(term, picker)?;
115+
116+
if let Some(data) = result {
117+
let rev = data.display();
118+
merge(app, term, rev)?;
119+
}
120+
79121
Ok(())
80122
}))
81123
}

0 commit comments

Comments
 (0)