Skip to content

Commit 529ea68

Browse files
committed
Support multiple source IDs (ranges and lists)
Allow commands to accept multiple source identifiers by parsing ranges (start-end), comma-separated lists, or single IDs. Previously the ids() function enforced exactly one source and returned a single CliId; now it returns a Vec<CliId> for sources and a single target. This change adds parse_sources, parse_range, and parse_list helpers, uses status/committed lists to resolve ranges, and improves error messages for not-found or ambiguous items.
1 parent bd86818 commit 529ea68

File tree

1 file changed

+120
-24
lines changed

1 file changed

+120
-24
lines changed

crates/but/src/rub/mod.rs

Lines changed: 120 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -134,29 +134,8 @@ fn makes_no_sense_error(source: &CliId, target: &CliId) -> String {
134134
)
135135
}
136136

137-
fn ids(ctx: &mut CommandContext, source: &str, target: &str) -> anyhow::Result<(CliId, CliId)> {
138-
let source_result = crate::id::CliId::from_str(ctx, source)?;
139-
if source_result.len() != 1 {
140-
if source_result.is_empty() {
141-
return Err(anyhow::anyhow!(
142-
"Source '{}' not found. If you just performed a Git operation (squash, rebase, etc.), try running 'but status' to refresh the current state.",
143-
source
144-
));
145-
} else {
146-
let matches: Vec<String> = source_result.iter().map(|id| {
147-
match id {
148-
CliId::Commit { oid } => format!("{} (commit {})", id.to_string(), &oid.to_string()[..7]),
149-
CliId::Branch { name } => format!("{} (branch '{}')", id.to_string(), name),
150-
_ => format!("{} ({})", id.to_string(), id.kind())
151-
}
152-
}).collect();
153-
return Err(anyhow::anyhow!(
154-
"Source '{}' is ambiguous. Matches: {}. Try using more characters, a longer SHA, or the full branch name to disambiguate.",
155-
source,
156-
matches.join(", ")
157-
));
158-
}
159-
}
137+
fn ids(ctx: &mut CommandContext, source: &str, target: &str) -> anyhow::Result<(Vec<CliId>, CliId)> {
138+
let sources = parse_sources(ctx, source)?;
160139
let target_result = crate::id::CliId::from_str(ctx, target)?;
161140
if target_result.len() != 1 {
162141
if target_result.is_empty() {
@@ -179,7 +158,124 @@ fn ids(ctx: &mut CommandContext, source: &str, target: &str) -> anyhow::Result<(
179158
));
180159
}
181160
}
182-
Ok((source_result[0].clone(), target_result[0].clone()))
161+
Ok((sources, target_result[0].clone()))
162+
}
163+
164+
fn parse_sources(ctx: &mut CommandContext, source: &str) -> anyhow::Result<Vec<CliId>> {
165+
// Check if it's a range (contains '-')
166+
if source.contains('-') {
167+
parse_range(ctx, source)
168+
}
169+
// Check if it's a list (contains ',')
170+
else if source.contains(',') {
171+
parse_list(ctx, source)
172+
}
173+
// Single source
174+
else {
175+
let source_result = crate::id::CliId::from_str(ctx, source)?;
176+
if source_result.len() != 1 {
177+
if source_result.is_empty() {
178+
return Err(anyhow::anyhow!(
179+
"Source '{}' not found. If you just performed a Git operation (squash, rebase, etc.), try running 'but status' to refresh the current state.",
180+
source
181+
));
182+
} else {
183+
let matches: Vec<String> = source_result.iter().map(|id| {
184+
match id {
185+
CliId::Commit { oid } => format!("{} (commit {})", id.to_string(), &oid.to_string()[..7]),
186+
CliId::Branch { name } => format!("{} (branch '{}')", id.to_string(), name),
187+
_ => format!("{} ({})", id.to_string(), id.kind())
188+
}
189+
}).collect();
190+
return Err(anyhow::anyhow!(
191+
"Source '{}' is ambiguous. Matches: {}. Try using more characters, a longer SHA, or the full branch name to disambiguate.",
192+
source,
193+
matches.join(", ")
194+
));
195+
}
196+
}
197+
Ok(vec![source_result[0].clone()])
198+
}
199+
}
200+
201+
fn parse_range(ctx: &mut CommandContext, source: &str) -> anyhow::Result<Vec<CliId>> {
202+
let parts: Vec<&str> = source.split('-').collect();
203+
if parts.len() != 2 {
204+
return Err(anyhow::anyhow!("Range format should be 'start-end', got '{}'", source));
205+
}
206+
207+
let start_str = parts[0];
208+
let end_str = parts[1];
209+
210+
// Get the start and end IDs
211+
let start_matches = crate::id::CliId::from_str(ctx, start_str)?;
212+
let end_matches = crate::id::CliId::from_str(ctx, end_str)?;
213+
214+
if start_matches.len() != 1 {
215+
return Err(anyhow::anyhow!("Start of range '{}' must match exactly one item", start_str));
216+
}
217+
if end_matches.len() != 1 {
218+
return Err(anyhow::anyhow!("End of range '{}' must match exactly one item", end_str));
219+
}
220+
221+
let start_id = &start_matches[0];
222+
let end_id = &end_matches[0];
223+
224+
// Get all files from status to find the range
225+
let all_files = crate::status::all_files(ctx)?;
226+
227+
// Find the positions of start and end in the file list
228+
let start_pos = all_files.iter().position(|id| id == start_id);
229+
let end_pos = all_files.iter().position(|id| id == end_id);
230+
231+
if let (Some(start_idx), Some(end_idx)) = (start_pos, end_pos) {
232+
if start_idx <= end_idx {
233+
return Ok(all_files[start_idx..=end_idx].to_vec());
234+
} else {
235+
return Ok(all_files[end_idx..=start_idx].to_vec());
236+
}
237+
}
238+
239+
// If not found in files, try committed files
240+
let all_committed_files = crate::status::all_committed_files(ctx)?;
241+
let start_pos = all_committed_files.iter().position(|id| id == start_id);
242+
let end_pos = all_committed_files.iter().position(|id| id == end_id);
243+
244+
if let (Some(start_idx), Some(end_idx)) = (start_pos, end_pos) {
245+
if start_idx <= end_idx {
246+
return Ok(all_committed_files[start_idx..=end_idx].to_vec());
247+
} else {
248+
return Ok(all_committed_files[end_idx..=start_idx].to_vec());
249+
}
250+
}
251+
252+
Err(anyhow::anyhow!("Could not find range from '{}' to '{}' in the same file list", start_str, end_str))
253+
}
254+
255+
fn parse_list(ctx: &mut CommandContext, source: &str) -> anyhow::Result<Vec<CliId>> {
256+
let parts: Vec<&str> = source.split(',').collect();
257+
let mut result = Vec::new();
258+
259+
for part in parts {
260+
let part = part.trim();
261+
let matches = crate::id::CliId::from_str(ctx, part)?;
262+
if matches.len() != 1 {
263+
if matches.is_empty() {
264+
return Err(anyhow::anyhow!(
265+
"Item '{}' in list not found. If you just performed a Git operation (squash, rebase, etc.), try running 'but status' to refresh the current state.",
266+
part
267+
));
268+
} else {
269+
return Err(anyhow::anyhow!(
270+
"Item '{}' in list is ambiguous. Try using more characters to disambiguate.",
271+
part
272+
));
273+
}
274+
}
275+
result.push(matches[0].clone());
276+
}
277+
278+
Ok(result)
183279
}
184280

185281
fn create_snapshot(ctx: &mut CommandContext, project: &Project, operation: OperationKind) {

0 commit comments

Comments
 (0)