Skip to content

Commit 46c6b83

Browse files
committed
Allow branch names and partial SHAs for rub source/id
Support identifying branches and commits by branch name (exact or partial) and by partial SHA prefixes in addition to the existing CliId lookup. This change adds helper functions to find branches by name and commits by SHA prefix, extends the CliId.from_str resolution to try branch-name and SHA matching before/alongside existing prefix/exact CliId matching, and de-duplicates results. It also improves ambiguity error messages in rub to include branch names and suggest using longer SHAs or full branch names to disambiguate.
1 parent d90e553 commit 46c6b83

File tree

2 files changed

+91
-45
lines changed

2 files changed

+91
-45
lines changed

crates/but/src/id/mod.rs

Lines changed: 87 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,42 @@ impl CliId {
6161
}
6262
}
6363

64+
fn find_branches_by_name(ctx: &CommandContext, name: &str) -> anyhow::Result<Vec<Self>> {
65+
let stacks = crate::log::stacks(ctx)?;
66+
let mut matches = Vec::new();
67+
68+
for stack in stacks {
69+
for head in &stack.heads {
70+
let branch_name = head.name.to_string();
71+
// Exact match or partial match
72+
if branch_name == name || branch_name.contains(name) {
73+
matches.push(CliId::branch(&branch_name));
74+
}
75+
}
76+
}
77+
78+
Ok(matches)
79+
}
80+
81+
fn find_commits_by_sha(ctx: &CommandContext, sha_prefix: &str) -> anyhow::Result<Vec<Self>> {
82+
let mut matches = Vec::new();
83+
84+
// Only try SHA matching if the input looks like a hex string
85+
if sha_prefix.chars().all(|c| c.is_ascii_hexdigit()) && sha_prefix.len() >= 4 {
86+
let all_commits = crate::log::all_commits(ctx)?;
87+
for commit_id in all_commits {
88+
if let CliId::Commit { oid } = &commit_id {
89+
let sha_string = oid.to_string();
90+
if sha_string.starts_with(sha_prefix) {
91+
matches.push(commit_id);
92+
}
93+
}
94+
}
95+
}
96+
97+
Ok(matches)
98+
}
99+
64100
pub fn matches(&self, s: &str) -> bool {
65101
s == self.to_string()
66102
}
@@ -77,71 +113,79 @@ impl CliId {
77113

78114
pub fn from_str(ctx: &mut CommandContext, s: &str) -> anyhow::Result<Vec<Self>> {
79115
if s.len() < 2 {
80-
return Err(anyhow::anyhow!("Id needs to be 3 characters long: {}", s));
116+
return Err(anyhow::anyhow!("Id needs to be at least 2 characters long: {}", s));
81117
}
82118

83-
// First try with the full input string for prefix matching
119+
let mut matches = Vec::new();
120+
121+
// First, try exact branch name match
122+
if let Ok(branch_matches) = Self::find_branches_by_name(ctx, s) {
123+
matches.extend(branch_matches);
124+
}
125+
126+
// Then try partial SHA matches (for commits)
127+
if let Ok(commit_matches) = Self::find_commits_by_sha(ctx, s) {
128+
matches.extend(commit_matches);
129+
}
130+
131+
// Then try CliId matching (both prefix and exact)
84132
if s.len() > 2 {
85-
let mut everything = Vec::new();
133+
// For longer strings, try prefix matching on CliIds
134+
let mut cli_matches = Vec::new();
86135
crate::status::all_files(ctx)?
87136
.into_iter()
88137
.filter(|id| id.matches_prefix(s))
89-
.for_each(|id| everything.push(id));
138+
.for_each(|id| cli_matches.push(id));
90139
crate::status::all_committed_files(ctx)?
91140
.into_iter()
92141
.filter(|id| id.matches_prefix(s))
93-
.for_each(|id| everything.push(id));
142+
.for_each(|id| cli_matches.push(id));
94143
crate::status::all_branches(ctx)?
95144
.into_iter()
96145
.filter(|id| id.matches_prefix(s))
97-
.for_each(|id| everything.push(id));
146+
.for_each(|id| cli_matches.push(id));
98147
crate::log::all_commits(ctx)?
99148
.into_iter()
100149
.filter(|id| id.matches_prefix(s))
101-
.for_each(|id| everything.push(id));
150+
.for_each(|id| cli_matches.push(id));
102151
if CliId::unassigned().matches_prefix(s) {
103-
everything.push(CliId::unassigned());
152+
cli_matches.push(CliId::unassigned());
104153
}
105-
106-
// If we found exactly one match with the full prefix, return it
107-
if everything.len() == 1 {
108-
return Ok(everything);
109-
}
110-
// If we found multiple matches with the full prefix, return them all (ambiguous)
111-
if everything.len() > 1 {
112-
return Ok(everything);
154+
matches.extend(cli_matches);
155+
} else {
156+
// For 2-character strings, try exact CliId matching
157+
let mut cli_matches = Vec::new();
158+
crate::status::all_files(ctx)?
159+
.into_iter()
160+
.filter(|id| id.matches(s))
161+
.for_each(|id| cli_matches.push(id));
162+
crate::status::all_committed_files(ctx)?
163+
.into_iter()
164+
.filter(|id| id.matches(s))
165+
.for_each(|id| cli_matches.push(id));
166+
crate::status::all_branches(ctx)?
167+
.into_iter()
168+
.filter(|id| id.matches(s))
169+
.for_each(|id| cli_matches.push(id));
170+
crate::log::all_commits(ctx)?
171+
.into_iter()
172+
.filter(|id| id.matches(s))
173+
.for_each(|id| cli_matches.push(id));
174+
if CliId::unassigned().matches(s) {
175+
cli_matches.push(CliId::unassigned());
113176
}
114-
// If no matches with full prefix, fall through to 2-char matching
177+
matches.extend(cli_matches);
115178
}
116179

117-
// Fall back to original 2-character matching behavior
118-
let s = &s[..2];
119-
let mut everything = Vec::new();
120-
crate::status::all_files(ctx)?
121-
.into_iter()
122-
.filter(|id| id.matches(s))
123-
.for_each(|id| everything.push(id));
124-
crate::status::all_committed_files(ctx)?
125-
.into_iter()
126-
.filter(|id| id.matches(s))
127-
.for_each(|id| everything.push(id));
128-
crate::status::all_branches(ctx)?
129-
.into_iter()
130-
.filter(|id| id.matches(s))
131-
.for_each(|id| everything.push(id));
132-
crate::log::all_commits(ctx)?
133-
.into_iter()
134-
.filter(|id| id.matches(s))
135-
.for_each(|id| everything.push(id));
136-
everything.push(CliId::unassigned());
137-
138-
let mut matches = Vec::new();
139-
for id in everything {
140-
if id.matches(s) {
141-
matches.push(id);
180+
// Remove duplicates while preserving order
181+
let mut unique_matches = Vec::new();
182+
for m in matches {
183+
if !unique_matches.contains(&m) {
184+
unique_matches.push(m);
142185
}
143186
}
144-
Ok(matches)
187+
188+
Ok(unique_matches)
145189
}
146190
}
147191

crates/but/src/rub/mod.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,11 +117,12 @@ fn ids(ctx: &mut CommandContext, source: &str, target: &str) -> anyhow::Result<(
117117
let matches: Vec<String> = source_result.iter().map(|id| {
118118
match id {
119119
CliId::Commit { oid } => format!("{} (commit {})", id.to_string(), &oid.to_string()[..7]),
120+
CliId::Branch { name } => format!("{} (branch '{}')", id.to_string(), name),
120121
_ => format!("{} ({})", id.to_string(), id.kind())
121122
}
122123
}).collect();
123124
return Err(anyhow::anyhow!(
124-
"Source '{}' is ambiguous. Matches: {}. Try using more characters to disambiguate.",
125+
"Source '{}' is ambiguous. Matches: {}. Try using more characters, a longer SHA, or the full branch name to disambiguate.",
125126
source,
126127
matches.join(", ")
127128
));
@@ -135,11 +136,12 @@ fn ids(ctx: &mut CommandContext, source: &str, target: &str) -> anyhow::Result<(
135136
let matches: Vec<String> = target_result.iter().map(|id| {
136137
match id {
137138
CliId::Commit { oid } => format!("{} (commit {})", id.to_string(), &oid.to_string()[..7]),
139+
CliId::Branch { name } => format!("{} (branch '{}')", id.to_string(), name),
138140
_ => format!("{} ({})", id.to_string(), id.kind())
139141
}
140142
}).collect();
141143
return Err(anyhow::anyhow!(
142-
"Target '{}' is ambiguous. Matches: {}. Try using more characters to disambiguate.",
144+
"Target '{}' is ambiguous. Matches: {}. Try using more characters, a longer SHA, or the full branch name to disambiguate.",
143145
target,
144146
matches.join(", ")
145147
));

0 commit comments

Comments
 (0)