1use std::collections::HashSet;
16use std::io::BufRead as _;
17use std::path::Path;
18
19use clap::FromArgMatches as _;
20use clap::builder::StyledStr;
21use clap_complete::CompletionCandidate;
22use indoc::indoc;
23use itertools::Itertools as _;
24use jj_lib::config::ConfigNamePathBuf;
25use jj_lib::file_util::normalize_path;
26use jj_lib::file_util::slash_path;
27use jj_lib::settings::UserSettings;
28use jj_lib::workspace::DefaultWorkspaceLoaderFactory;
29use jj_lib::workspace::WorkspaceLoaderFactory as _;
30
31use crate::cli_util::GlobalArgs;
32use crate::cli_util::expand_args;
33use crate::cli_util::find_workspace_dir;
34use crate::cli_util::load_revset_aliases;
35use crate::cli_util::load_template_aliases;
36use crate::command_error::CommandError;
37use crate::command_error::user_error;
38use crate::config::CONFIG_SCHEMA;
39use crate::config::ConfigArgKind;
40use crate::config::ConfigEnv;
41use crate::config::config_from_environment;
42use crate::config::default_config_layers;
43use crate::merge_tools::ExternalMergeTool;
44use crate::merge_tools::configured_merge_tools;
45use crate::merge_tools::get_external_tool_config;
46use crate::ui::Ui;
47
48const BOOKMARK_HELP_TEMPLATE: &str = r#"template-aliases.'bookmark_help()'='''
49" " ++
50coalesce(
51 if(!present, "(deleted bookmark)"),
52 if(!normal_target, "(conflicted bookmark)"),
53 if(!normal_target.description(), "(no description set)"),
54 normal_target.description().first_line(),
55)
56'''"#;
57const TAG_HELP_TEMPLATE: &str = r#"template-aliases.'tag_help()'='''
58" " ++
59coalesce(
60 if(!present, "(deleted tag)"),
61 if(!normal_target, "(conflicted tag)"),
62 if(!normal_target.description(), "(no description set)"),
63 normal_target.description().first_line(),
64)
65'''"#;
66
67fn split_help_text(line: &str) -> (&str, Option<StyledStr>) {
70 match line.split_once(' ') {
71 Some((name, help)) => (name, Some(help.to_string().into())),
72 None => (line, None),
73 }
74}
75
76pub fn local_bookmarks() -> Vec<CompletionCandidate> {
77 with_jj(|jj, _| {
78 let output = jj
79 .build()
80 .arg("bookmark")
81 .arg("list")
82 .arg("--config")
83 .arg(BOOKMARK_HELP_TEMPLATE)
84 .arg("--template")
85 .arg(r#"if(!remote, name ++ bookmark_help()) ++ "\n""#)
86 .output()
87 .map_err(user_error)?;
88
89 Ok(String::from_utf8_lossy(&output.stdout)
90 .lines()
91 .map(split_help_text)
92 .map(|(name, help)| CompletionCandidate::new(name).help(help))
93 .collect())
94 })
95}
96
97pub fn tracked_bookmarks() -> Vec<CompletionCandidate> {
98 with_jj(|jj, _| {
99 let output = jj
100 .build()
101 .arg("bookmark")
102 .arg("list")
103 .arg("--tracked")
104 .arg("--config")
105 .arg(BOOKMARK_HELP_TEMPLATE)
106 .arg("--template")
107 .arg(r#"if(remote, name ++ '@' ++ remote ++ bookmark_help() ++ "\n")"#)
108 .output()
109 .map_err(user_error)?;
110
111 Ok(String::from_utf8_lossy(&output.stdout)
112 .lines()
113 .map(split_help_text)
114 .filter_map(|(symbol, help)| Some((symbol.split_once('@')?, help)))
115 .dedup_by(|((name1, _), _), ((name2, _), _)| name1 == name2)
118 .map(|((name, _remote), help)| CompletionCandidate::new(name).help(help))
119 .collect())
120 })
121}
122
123pub fn untracked_bookmarks() -> Vec<CompletionCandidate> {
124 with_jj(|jj, _settings| {
125 let remotes = jj
126 .build()
127 .arg("git")
128 .arg("remote")
129 .arg("list")
130 .output()
131 .map_err(user_error)?;
132 let remotes = String::from_utf8_lossy(&remotes.stdout);
133 let remotes = remotes
134 .lines()
135 .filter_map(|l| l.split_whitespace().next())
136 .collect_vec();
137
138 let bookmark_table = jj
139 .build()
140 .arg("bookmark")
141 .arg("list")
142 .arg("--all-remotes")
143 .arg("--config")
144 .arg(BOOKMARK_HELP_TEMPLATE)
145 .arg("--template")
146 .arg(
147 r#"
148 if(remote != "git",
149 if(!remote, name) ++ "\t" ++
150 if(remote, name ++ "@" ++ remote) ++ "\t" ++
151 if(tracked, "tracked") ++ "\t" ++
152 bookmark_help() ++ "\n"
153 )"#,
154 )
155 .output()
156 .map_err(user_error)?;
157 let bookmark_table = String::from_utf8_lossy(&bookmark_table.stdout);
158
159 let mut possible_bookmarks_to_track = Vec::new();
160 let mut already_tracked_bookmarks = HashSet::new();
161
162 for line in bookmark_table.lines() {
163 let [local, remote, tracked, help] =
164 line.split('\t').collect_array().unwrap_or_default();
165
166 if !local.is_empty() {
167 possible_bookmarks_to_track.extend(
168 remotes
169 .iter()
170 .map(|remote| (format!("{local}@{remote}"), help)),
171 );
172 } else if tracked.is_empty() {
173 possible_bookmarks_to_track.push((remote.to_owned(), help));
174 } else {
175 already_tracked_bookmarks.insert(remote);
176 }
177 }
178 possible_bookmarks_to_track
179 .retain(|(bookmark, _help)| !already_tracked_bookmarks.contains(&bookmark.as_str()));
180
181 Ok(possible_bookmarks_to_track
182 .iter()
183 .filter_map(|(symbol, help)| Some((symbol.split_once('@')?, help)))
184 .dedup_by(|((name1, _), _), ((name2, _), _)| name1 == name2)
187 .map(|((name, _remote), help)| {
188 CompletionCandidate::new(name).help(Some(help.to_string().into()))
189 })
190 .collect())
191 })
192}
193
194pub fn bookmarks() -> Vec<CompletionCandidate> {
195 with_jj(|jj, _settings| {
196 let output = jj
197 .build()
198 .arg("bookmark")
199 .arg("list")
200 .arg("--all-remotes")
201 .arg("--config")
202 .arg(BOOKMARK_HELP_TEMPLATE)
203 .arg("--template")
204 .arg(
205 r#"name ++ if(remote, "@" ++ remote, bookmark_help()) ++ "\n""#,
207 )
208 .output()
209 .map_err(user_error)?;
210 let stdout = String::from_utf8_lossy(&output.stdout);
211
212 Ok((&stdout
213 .lines()
214 .map(split_help_text)
215 .chunk_by(|(name, _)| name.split_once('@').map(|t| t.0).unwrap_or(name)))
216 .into_iter()
217 .map(|(bookmark, mut refs)| {
218 let help = refs.find_map(|(_, help)| help);
219 let local = help.is_some();
220 let display_order = match local {
221 true => 0,
222 false => 1,
223 };
224 CompletionCandidate::new(bookmark)
225 .help(help)
226 .display_order(Some(display_order))
227 })
228 .collect())
229 })
230}
231
232pub fn local_tags() -> Vec<CompletionCandidate> {
233 with_jj(|jj, _| {
234 let output = jj
235 .build()
236 .arg("tag")
237 .arg("list")
238 .arg("--config")
239 .arg(TAG_HELP_TEMPLATE)
240 .arg("--template")
241 .arg(r#"if(!remote, name ++ tag_help()) ++ "\n""#)
242 .output()
243 .map_err(user_error)?;
244
245 Ok(String::from_utf8_lossy(&output.stdout)
246 .lines()
247 .map(split_help_text)
248 .map(|(name, help)| CompletionCandidate::new(name).help(help))
249 .collect())
250 })
251}
252
253pub fn git_remotes() -> Vec<CompletionCandidate> {
254 with_jj(|jj, _| {
255 let output = jj
256 .build()
257 .arg("git")
258 .arg("remote")
259 .arg("list")
260 .output()
261 .map_err(user_error)?;
262
263 let stdout = String::from_utf8_lossy(&output.stdout);
264
265 Ok(stdout
266 .lines()
267 .filter_map(|line| line.split_once(' ').map(|(name, _url)| name))
268 .map(CompletionCandidate::new)
269 .collect())
270 })
271}
272
273pub fn template_aliases() -> Vec<CompletionCandidate> {
274 with_jj(|_, settings| {
275 let Ok(template_aliases) = load_template_aliases(&Ui::null(), settings.config()) else {
276 return Ok(Vec::new());
277 };
278 Ok(template_aliases
279 .symbol_names()
280 .map(|name| {
281 let doc = template_aliases.get_symbol(name).unwrap().2;
282 CompletionCandidate::new(name).help(doc.map(|doc| doc.to_owned().into()))
283 })
284 .sorted()
285 .collect())
286 })
287}
288
289pub fn aliases() -> Vec<CompletionCandidate> {
290 with_jj(|_, settings| {
291 Ok(settings
292 .table_keys("aliases")
293 .filter(|alias| alias.len() > 2)
298 .map(|alias| {
299 CompletionCandidate::new(alias).help(
300 settings
301 .get_string(["aliases", alias, "doc"])
302 .ok()
303 .map(|doc| doc.into()),
304 )
305 })
306 .collect())
307 })
308}
309
310fn revisions(match_prefix: &str, revset_filter: Option<&str>) -> Vec<CompletionCandidate> {
311 with_jj(|jj, settings| {
312 const LOCAL_BOOKMARK: usize = 0;
314 const TAG: usize = 1;
315 const CHANGE_ID: usize = 2;
316 const WORKSPACE: usize = 3;
317 const REMOTE_BOOKMARK: usize = 4;
318 const REVSET_ALIAS: usize = 5;
319
320 let mut candidates = Vec::new();
321
322 let mut cmd = jj.build();
325 cmd.arg("bookmark")
326 .arg("list")
327 .arg("--all-remotes")
328 .arg("--config")
329 .arg(BOOKMARK_HELP_TEMPLATE)
330 .arg("--template")
331 .arg(
332 r#"if(remote != "git", name ++ if(remote, "@" ++ remote) ++ bookmark_help() ++ "\n")"#,
333 );
334 if let Some(revs) = revset_filter {
335 cmd.arg("--revisions").arg(revs);
336 }
337 let output = cmd.output().map_err(user_error)?;
338 let stdout = String::from_utf8_lossy(&output.stdout);
339
340 candidates.extend(
341 stdout
342 .lines()
343 .map(split_help_text)
344 .filter(|(bookmark, _)| bookmark.starts_with(match_prefix))
345 .map(|(bookmark, help)| {
346 let local = !bookmark.contains('@');
347 let display_order = match local {
348 true => LOCAL_BOOKMARK,
349 false => REMOTE_BOOKMARK,
350 };
351 CompletionCandidate::new(bookmark)
352 .help(help)
353 .display_order(Some(display_order))
354 }),
355 );
356
357 if revset_filter.is_none() {
364 let output = jj
365 .build()
366 .arg("tag")
367 .arg("list")
368 .arg("--config")
369 .arg(BOOKMARK_HELP_TEMPLATE)
370 .arg("--template")
371 .arg(r#"name ++ bookmark_help() ++ "\n""#)
372 .arg(format!("glob:{}*", globset::escape(match_prefix)))
373 .output()
374 .map_err(user_error)?;
375 let stdout = String::from_utf8_lossy(&output.stdout);
376
377 candidates.extend(stdout.lines().map(|line| {
378 let (name, desc) = split_help_text(line);
379 CompletionCandidate::new(name)
380 .help(desc)
381 .display_order(Some(TAG))
382 }));
383 }
384
385 let output = jj
388 .build()
389 .arg("workspace")
390 .arg("list")
391 .arg("--template")
392 .arg(r#"name ++ "\n""#)
393 .output()
394 .map_err(user_error)?;
395 let stdout = String::from_utf8_lossy(&output.stdout);
396
397 if stdout.lines().count() > 1 {
400 candidates.extend(stdout.lines().filter_map(|name| {
401 let symbol = format!("{name}@");
402 if symbol.starts_with(match_prefix) {
403 Some(
404 CompletionCandidate::new(symbol)
405 .help(Some(
406 format!("The working copy for workspace `{name}`").into(),
407 ))
408 .display_order(Some(WORKSPACE)),
409 )
410 } else {
411 None
412 }
413 }));
414 }
415
416 let revisions = revset_filter
419 .map(String::from)
420 .or_else(|| settings.get_string("revsets.short-prefixes").ok())
421 .or_else(|| settings.get_string("revsets.log").ok())
422 .unwrap_or_default();
423
424 let output = jj
425 .build()
426 .arg("log")
427 .arg("--no-graph")
428 .arg("--limit")
429 .arg("100")
430 .arg("--revisions")
431 .arg(revisions)
432 .arg("--template")
433 .arg(
434 r#"
435 join(" ",
436 separate("/",
437 change_id.shortest(),
438 if(hidden || divergent, change_offset),
439 ),
440 if(description, description.first_line(), "(no description set)"),
441 ) ++ "\n""#,
442 )
443 .output()
444 .map_err(user_error)?;
445 let stdout = String::from_utf8_lossy(&output.stdout);
446
447 candidates.extend(
448 stdout
449 .lines()
450 .map(split_help_text)
451 .filter(|(id, _)| id.starts_with(match_prefix))
452 .map(|(id, desc)| {
453 CompletionCandidate::new(id)
454 .help(desc)
455 .display_order(Some(CHANGE_ID))
456 }),
457 );
458
459 let revset_aliases = load_revset_aliases(&Ui::null(), settings.config())?;
462 let symbol_names = revset_aliases
463 .symbol_names()
464 .sorted_unstable()
465 .collect_vec();
466 candidates.extend(
467 symbol_names
468 .into_iter()
469 .filter(|symbol| symbol.starts_with(match_prefix))
470 .map(|symbol| {
471 let (_, defn, doc) = revset_aliases.get_symbol(symbol).unwrap();
472 let help: String = doc.map(|s| s.to_owned()).unwrap_or_else(|| defn.clone());
474 CompletionCandidate::new(symbol)
475 .help(Some(help.into()))
476 .display_order(Some(REVSET_ALIAS))
477 }),
478 );
479
480 Ok(candidates)
481 })
482}
483
484fn revset_expression(
485 current: &std::ffi::OsStr,
486 revset_filter: Option<&str>,
487) -> Vec<CompletionCandidate> {
488 let Some(current) = current.to_str() else {
489 return Vec::new();
490 };
491 let (prepend, match_prefix) = split_revset_trailing_name(current).unwrap_or(("", current));
492 let candidates = revisions(match_prefix, revset_filter);
493 if prepend.is_empty() {
494 candidates
495 } else {
496 candidates
497 .into_iter()
498 .map(|candidate| candidate.add_prefix(prepend))
499 .collect()
500 }
501}
502
503pub fn revset_expression_all(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
504 revset_expression(current, None)
505}
506
507pub fn revset_expression_mutable(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
508 revset_expression(current, Some("mutable()"))
509}
510
511pub fn revset_expression_mutable_conflicts(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
512 revset_expression(current, Some("mutable() & conflicts()"))
513}
514
515fn split_revset_trailing_name(incomplete_revset_str: &str) -> Option<(&str, &str)> {
528 let final_part = incomplete_revset_str
529 .rsplit_once([':', '~', '|', '&', '(', ','])
530 .map(|(_, rest)| rest)
531 .unwrap_or(incomplete_revset_str);
532 let final_part = final_part
533 .rsplit_once("..")
534 .map(|(_, rest)| rest)
535 .unwrap_or(final_part)
536 .trim_ascii_start();
537
538 let re = regex::Regex::new(r"^(?:[\p{XID_CONTINUE}_/]+[@.+-])*[\p{XID_CONTINUE}_/]*$").unwrap();
539 re.is_match(final_part)
540 .then(|| incomplete_revset_str.split_at(incomplete_revset_str.len() - final_part.len()))
541}
542
543pub fn operations() -> Vec<CompletionCandidate> {
544 with_jj(|jj, _| {
545 let output = jj
546 .build()
547 .arg("operation")
548 .arg("log")
549 .arg("--no-graph")
550 .arg("--limit")
551 .arg("100")
552 .arg("--template")
553 .arg(
554 r#"
555 separate(" ",
556 id.short(),
557 "(" ++ format_timestamp(time.end()) ++ ")",
558 description.first_line(),
559 ) ++ "\n""#,
560 )
561 .output()
562 .map_err(user_error)?;
563
564 Ok(String::from_utf8_lossy(&output.stdout)
565 .lines()
566 .map(|line| {
567 let (id, help) = split_help_text(line);
568 CompletionCandidate::new(id).help(help)
569 })
570 .collect())
571 })
572}
573
574pub fn workspaces() -> Vec<CompletionCandidate> {
575 let template = indoc! {r#"
576 name ++ "\t" ++ if(
577 target.description(),
578 target.description().first_line(),
579 "(no description set)"
580 ) ++ "\n"
581 "#};
582 with_jj(|jj, _| {
583 let output = jj
584 .build()
585 .arg("workspace")
586 .arg("list")
587 .arg("--template")
588 .arg(template)
589 .output()
590 .map_err(user_error)?;
591 let stdout = String::from_utf8_lossy(&output.stdout);
592
593 Ok(stdout
594 .lines()
595 .filter_map(|line| {
596 let res = line.split_once('\t').map(|(name, desc)| {
597 CompletionCandidate::new(name).help(Some(desc.to_string().into()))
598 });
599 if res.is_none() {
600 eprintln!("Error parsing line {line}");
601 }
602 res
603 })
604 .collect())
605 })
606}
607
608fn merge_tools_filtered_by(
609 settings: &UserSettings,
610 condition: impl Fn(ExternalMergeTool) -> bool,
611) -> impl Iterator<Item = &str> {
612 configured_merge_tools(settings).filter(move |name| {
613 let Ok(Some(tool)) = get_external_tool_config(settings, name) else {
614 return false;
615 };
616 condition(tool)
617 })
618}
619
620pub fn merge_editors() -> Vec<CompletionCandidate> {
621 with_jj(|_, settings| {
622 Ok([":builtin", ":ours", ":theirs"]
623 .into_iter()
624 .chain(merge_tools_filtered_by(settings, |tool| {
625 !tool.merge_args.is_empty()
626 }))
627 .map(CompletionCandidate::new)
628 .collect())
629 })
630}
631
632pub fn diff_editors() -> Vec<CompletionCandidate> {
634 with_jj(|_, settings| {
635 Ok(std::iter::once(":builtin")
636 .chain(merge_tools_filtered_by(
637 settings,
638 |tool| !tool.edit_args.is_empty(),
642 ))
643 .map(CompletionCandidate::new)
644 .collect())
645 })
646}
647
648pub fn diff_formatters() -> Vec<CompletionCandidate> {
650 let builtin_format_kinds = crate::diff_util::all_builtin_diff_format_names();
651 with_jj(|_, settings| {
652 Ok(builtin_format_kinds
653 .iter()
654 .map(|s| s.as_str())
655 .chain(merge_tools_filtered_by(
656 settings,
657 |tool| !tool.diff_args.is_empty(),
661 ))
662 .map(CompletionCandidate::new)
663 .collect())
664 })
665}
666
667fn config_keys_rec(
668 prefix: ConfigNamePathBuf,
669 properties: &serde_json::Map<String, serde_json::Value>,
670 acc: &mut Vec<CompletionCandidate>,
671 only_leaves: bool,
672 suffix: &str,
673) {
674 for (key, value) in properties {
675 let mut prefix = prefix.clone();
676 prefix.push(key);
677
678 let value = value.as_object().unwrap();
679 match value.get("type").and_then(|v| v.as_str()) {
680 Some("object") => {
681 if !only_leaves {
682 let help = value
683 .get("description")
684 .map(|desc| desc.as_str().unwrap().to_string().into());
685 let escaped_key = prefix.to_string();
686 acc.push(CompletionCandidate::new(escaped_key).help(help));
687 }
688 let Some(properties) = value.get("properties") else {
689 continue;
690 };
691 let properties = properties.as_object().unwrap();
692 config_keys_rec(prefix, properties, acc, only_leaves, suffix);
693 }
694 _ => {
695 let help = value
696 .get("description")
697 .map(|desc| desc.as_str().unwrap().to_string().into());
698 let escaped_key = format!("{prefix}{suffix}");
699 acc.push(CompletionCandidate::new(escaped_key).help(help));
700 }
701 }
702 }
703}
704
705fn json_keypath<'a>(
706 schema: &'a serde_json::Value,
707 keypath: &str,
708 separator: &str,
709) -> Option<&'a serde_json::Value> {
710 keypath
711 .split(separator)
712 .try_fold(schema, |value, step| value.get(step))
713}
714fn jsonschema_keypath<'a>(
715 schema: &'a serde_json::Value,
716 keypath: &ConfigNamePathBuf,
717) -> Option<&'a serde_json::Value> {
718 keypath.components().try_fold(schema, |value, step| {
719 let value = value.as_object()?;
720 if value.get("type")?.as_str()? != "object" {
721 return None;
722 }
723 let properties = value.get("properties")?.as_object()?;
724 properties.get(step.get())
725 })
726}
727
728fn config_values(path: &ConfigNamePathBuf) -> Option<Vec<String>> {
729 let schema: serde_json::Value = serde_json::from_str(CONFIG_SCHEMA).unwrap();
730
731 let mut config_entry = jsonschema_keypath(&schema, path)?;
732 if let Some(reference) = config_entry.get("$ref") {
733 let reference = reference.as_str()?.strip_prefix("#/")?;
734 config_entry = json_keypath(&schema, reference, "/")?;
735 }
736
737 if let Some(possible_values) = config_entry.get("enum") {
738 return Some(
739 possible_values
740 .as_array()?
741 .iter()
742 .filter_map(|val| val.as_str())
743 .map(ToOwned::to_owned)
744 .collect(),
745 );
746 }
747
748 Some(match config_entry.get("type")?.as_str()? {
749 "boolean" => vec!["false".into(), "true".into()],
750 _ => vec![],
751 })
752}
753
754fn config_keys_impl(only_leaves: bool, suffix: &str) -> Vec<CompletionCandidate> {
755 let schema: serde_json::Value = serde_json::from_str(CONFIG_SCHEMA).unwrap();
756 let schema = schema.as_object().unwrap();
757 let properties = schema["properties"].as_object().unwrap();
758
759 let mut candidates = Vec::new();
760 config_keys_rec(
761 ConfigNamePathBuf::root(),
762 properties,
763 &mut candidates,
764 only_leaves,
765 suffix,
766 );
767 candidates
768}
769
770pub fn config_keys() -> Vec<CompletionCandidate> {
771 config_keys_impl(false, "")
772}
773
774pub fn leaf_config_keys() -> Vec<CompletionCandidate> {
775 config_keys_impl(true, "")
776}
777
778pub fn leaf_config_key_value(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
779 let Some(current) = current.to_str() else {
780 return Vec::new();
781 };
782
783 if let Some((key, current_val)) = current.split_once('=') {
784 let Ok(key) = key.parse() else {
785 return Vec::new();
786 };
787 let possible_values = config_values(&key).unwrap_or_default();
788
789 possible_values
790 .into_iter()
791 .filter(|x| x.starts_with(current_val))
792 .map(|x| CompletionCandidate::new(format!("{key}={x}")))
793 .collect()
794 } else {
795 config_keys_impl(true, "=")
796 .into_iter()
797 .filter(|candidate| candidate.get_value().to_str().unwrap().starts_with(current))
798 .collect()
799 }
800}
801
802pub fn config_keys_to_unset() -> Vec<CompletionCandidate> {
803 let Ok(config_level_flag) = std::env::args()
804 .filter(|arg| matches!(arg.as_str(), "--user" | "--repo" | "--workspace"))
805 .at_most_one()
806 else {
807 return Vec::new();
808 };
809
810 with_jj(|jj, _| {
811 const TEMPLATE: &str = r#"name ++ "\t" ++ source ++ "\t" ++ stringify(value).replace(regex:'\n\s*', " ") ++ "\n""#;
812 let list_output = jj
813 .build()
814 .args(["config", "list"])
815 .args(
818 config_level_flag
819 .is_some()
820 .then_some("--include-overridden"),
821 )
822 .args(config_level_flag)
823 .args(["--template", TEMPLATE])
824 .output()
825 .map_err(user_error)?;
826 Ok(String::from_utf8_lossy(&list_output.stdout)
827 .lines()
828 .filter_map(|line| line.split('\t').collect_tuple())
829 .filter(|(_, source, _)| matches!(*source, "user" | "repo" | "workspace"))
830 .map(|(name, source, value)| {
831 CompletionCandidate::new(name)
832 .tag(Some(source.to_string().into()))
833 .help(Some(format!("{source}: {value}").into()))
834 })
835 .collect())
836 })
837}
838
839pub fn branch_name_equals_any_revision(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
840 let Some(current) = current.to_str() else {
841 return Vec::new();
842 };
843
844 let Some((branch_name, revision)) = current.split_once('=') else {
845 return Vec::new();
847 };
848 revset_expression(revision.as_ref(), None)
849 .into_iter()
850 .map(|rev| rev.add_prefix(format!("{branch_name}=")))
851 .collect()
852}
853
854fn path_completion_candidate_from(
855 current_prefix: &str,
856 normalized_prefix_path: &Path,
857 path: &Path,
858 mode: Option<clap::builder::StyledStr>,
859) -> Option<CompletionCandidate> {
860 let normalized_prefix = match normalized_prefix_path.to_str()? {
861 "." => "", normalized_prefix => normalized_prefix,
863 };
864
865 let path = slash_path(path);
866 let mut remainder = path.to_str()?.strip_prefix(normalized_prefix)?;
867
868 if current_prefix.ends_with(std::path::is_separator) {
872 remainder = remainder.strip_prefix('/').unwrap_or(remainder);
873 }
874
875 match remainder.split_inclusive('/').at_most_one() {
876 Ok(file_completion) => Some(
879 CompletionCandidate::new(format!(
880 "{current_prefix}{}",
881 file_completion.unwrap_or_default()
882 ))
883 .help(mode),
884 ),
885
886 Err(mut components) => Some(CompletionCandidate::new(format!(
888 "{current_prefix}{}",
889 components.next().unwrap()
890 ))),
891 }
892}
893
894fn current_prefix_to_fileset(current: &str) -> String {
895 let cur_esc = globset::escape(current);
896 let dir_pat = format!("{cur_esc}*/**");
897 let path_pat = format!("{cur_esc}*");
898 format!("glob:{dir_pat:?} | glob:{path_pat:?}")
899}
900
901fn all_files_from_rev(rev: String, current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
902 let Some(current) = current.to_str() else {
903 return Vec::new();
904 };
905
906 let normalized_prefix = normalize_path(Path::new(current));
907 let normalized_prefix = slash_path(&normalized_prefix);
908
909 with_jj(|jj, _| {
910 let mut child = jj
911 .build()
912 .arg("file")
913 .arg("list")
914 .arg("--revision")
915 .arg(rev)
916 .arg("--template")
917 .arg(r#"path.display() ++ "\n""#)
918 .arg(current_prefix_to_fileset(current))
919 .stdout(std::process::Stdio::piped())
920 .stderr(std::process::Stdio::null())
921 .spawn()
922 .map_err(user_error)?;
923 let stdout = child.stdout.take().unwrap();
924
925 Ok(std::io::BufReader::new(stdout)
926 .lines()
927 .take(1_000)
928 .map_while(Result::ok)
929 .filter_map(|path| {
930 path_completion_candidate_from(current, &normalized_prefix, Path::new(&path), None)
931 })
932 .dedup() .collect())
934 })
935}
936
937#[derive(Clone, Debug)]
938enum DiffSelection {
939 Revisions(Vec<String>),
940 Range { from: String, to: String },
941}
942
943impl DiffSelection {
944 fn revision(revision: String) -> Self {
945 Self::Revisions(vec![revision])
946 }
947}
948
949fn modified_files_from_selection_with_jj_cmd(
950 selection: &DiffSelection,
951 mut cmd: std::process::Command,
952 current: &std::ffi::OsStr,
953) -> Result<Vec<CompletionCandidate>, CommandError> {
954 let Some(current) = current.to_str() else {
955 return Ok(Vec::new());
956 };
957
958 let normalized_prefix = normalize_path(Path::new(current));
959 let normalized_prefix = slash_path(&normalized_prefix);
960
961 let template = indoc! {r#"
963 concat(
964 status ++ ' ' ++ path.display() ++ "\n",
965 if(status == 'renamed', 'renamed.source ' ++ source.path().display() ++ "\n"),
966 )
967 "#};
968 cmd.arg("diff").args(["--template", template]);
969 match selection {
970 DiffSelection::Revisions(revisions) => {
971 for revision in revisions {
972 cmd.arg("--revisions").arg(revision);
973 }
974 }
975 DiffSelection::Range { from, to } => {
976 cmd.arg("--from").arg(from).arg("--to").arg(to);
977 }
978 }
979 cmd.arg(current_prefix_to_fileset(current));
980
981 let output = cmd.output().map_err(user_error)?;
982 let stdout = String::from_utf8_lossy(&output.stdout);
983
984 let mut include_renames = false;
985 let mut candidates: Vec<_> = stdout
986 .lines()
987 .filter_map(|line| line.split_once(' '))
988 .filter_map(|(mode, path)| {
989 let mode = match mode {
990 "modified" => "Modified".into(),
991 "removed" => "Deleted".into(),
992 "added" => "Added".into(),
993 "renamed" => "Renamed".into(),
994 "renamed.source" => {
995 include_renames = true;
996 "Renamed".into()
997 }
998 "copied" => "Copied".into(),
999 _ => format!("unknown mode: '{mode}'").into(),
1000 };
1001 path_completion_candidate_from(current, &normalized_prefix, Path::new(path), Some(mode))
1002 })
1003 .collect();
1004
1005 if include_renames {
1006 candidates.sort_unstable_by(|a, b| Path::new(a.get_value()).cmp(Path::new(b.get_value())));
1007 }
1008 candidates.dedup();
1009
1010 Ok(candidates)
1011}
1012
1013fn modified_files_from_rev_with_jj_cmd(
1014 rev: String,
1015 cmd: std::process::Command,
1016 current: &std::ffi::OsStr,
1017) -> Result<Vec<CompletionCandidate>, CommandError> {
1018 let selection = DiffSelection::revision(rev);
1019 modified_files_from_selection_with_jj_cmd(&selection, cmd, current)
1020}
1021
1022fn modified_files_from_selection(
1023 selection: DiffSelection,
1024 current: &std::ffi::OsStr,
1025) -> Vec<CompletionCandidate> {
1026 with_jj(|jj, _| modified_files_from_selection_with_jj_cmd(&selection, jj.build(), current))
1027}
1028
1029fn modified_files_from_rev(rev: String, current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
1030 modified_files_from_selection(DiffSelection::revision(rev), current)
1031}
1032
1033fn conflicted_files_from_rev(rev: &str, current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
1034 let Some(current) = current.to_str() else {
1035 return Vec::new();
1036 };
1037
1038 let normalized_prefix = normalize_path(Path::new(current));
1039 let normalized_prefix = slash_path(&normalized_prefix);
1040
1041 with_jj(|jj, _| {
1042 let output = jj
1043 .build()
1044 .arg("resolve")
1045 .arg("--list")
1046 .arg("--revision")
1047 .arg(rev)
1048 .arg(current_prefix_to_fileset(current))
1049 .output()
1050 .map_err(user_error)?;
1051 let stdout = String::from_utf8_lossy(&output.stdout);
1052
1053 Ok(stdout
1054 .lines()
1055 .filter_map(|line| {
1056 let path = line
1057 .split_whitespace()
1058 .next()
1059 .expect("resolve --list should contain whitespace after path");
1060
1061 path_completion_candidate_from(current, &normalized_prefix, Path::new(path), None)
1062 })
1063 .dedup() .collect())
1065 })
1066}
1067
1068pub fn modified_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
1069 modified_files_from_rev("@".into(), current)
1070}
1071
1072pub fn all_revision_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
1073 all_files_from_rev(parse::revision_or_wc(), current)
1074}
1075
1076pub fn modified_revision_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
1077 modified_files_from_rev(parse::revision_or_wc(), current)
1078}
1079
1080pub fn modified_range_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
1081 match parse::range() {
1082 Some((from, to)) => {
1083 modified_files_from_selection(DiffSelection::Range { from, to }, current)
1084 }
1085 None => modified_files_from_rev("@".into(), current),
1086 }
1087}
1088
1089pub fn modified_from_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
1092 modified_files_from_rev(parse::from_or_wc(), current)
1093}
1094
1095pub fn modified_revision_or_range_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
1096 let revisions = parse::revisions();
1097 if revisions.is_empty() {
1098 return modified_range_files(current);
1099 }
1100 modified_files_from_selection(DiffSelection::Revisions(revisions), current)
1101}
1102
1103pub fn modified_changes_in_or_range_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
1104 if let Some(rev) = parse::changes_in() {
1105 return modified_files_from_rev(rev, current);
1106 }
1107 modified_range_files(current)
1108}
1109
1110pub fn revision_conflicted_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
1111 conflicted_files_from_rev(&parse::revision_or_wc(), current)
1112}
1113
1114pub fn squash_revision_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
1116 let rev = parse::squash_revision().unwrap_or_else(|| "@".into());
1117 modified_files_from_rev(rev, current)
1118}
1119
1120pub fn interdiff_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
1122 let Some((from, to)) = parse::range() else {
1123 return Vec::new();
1124 };
1125 with_jj(|jj, _| {
1129 let mut res = modified_files_from_rev_with_jj_cmd(from, jj.build(), current)?;
1130 res.extend(modified_files_from_rev_with_jj_cmd(
1131 to,
1132 jj.build(),
1133 current,
1134 )?);
1135 Ok(res)
1136 })
1137}
1138
1139pub fn log_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
1141 let mut rev = parse::revisions().join(")|(");
1142 if rev.is_empty() {
1143 rev = "@".into();
1144 } else {
1145 rev = format!("latest(heads(({rev})))"); }
1147 all_files_from_rev(rev, current)
1148}
1149
1150fn with_jj<F>(completion_fn: F) -> Vec<CompletionCandidate>
1154where
1155 F: FnOnce(JjBuilder, &UserSettings) -> Result<Vec<CompletionCandidate>, CommandError>,
1156{
1157 get_jj_command()
1158 .and_then(|(jj, settings)| completion_fn(jj, &settings))
1159 .unwrap_or_else(|e| {
1160 eprintln!("{}", e.error);
1161 Vec::new()
1162 })
1163}
1164
1165fn get_jj_command() -> Result<(JjBuilder, UserSettings), CommandError> {
1175 let current_exe = std::env::current_exe().map_err(user_error)?;
1176 let mut cmd_args = Vec::<String>::new();
1177
1178 cmd_args.push("--ignore-working-copy".into());
1181 cmd_args.push("--color=never".into());
1182 cmd_args.push("--no-pager".into());
1183
1184 let app = crate::commands::default_app();
1188 let mut raw_config = config_from_environment(default_config_layers());
1189 let ui = Ui::null();
1190 let cwd = std::env::current_dir()
1191 .and_then(dunce::canonicalize)
1192 .map_err(user_error)?;
1193 let mut config_env = ConfigEnv::from_environment();
1195 let maybe_cwd_workspace_loader = DefaultWorkspaceLoaderFactory.create(find_workspace_dir(&cwd));
1196 config_env.reload_system_config(&mut raw_config).ok();
1197 config_env.reload_user_config(&mut raw_config).ok();
1198 if let Ok(loader) = &maybe_cwd_workspace_loader {
1199 config_env.reset_repo_path(loader.repo_path());
1200 config_env.reload_repo_config(&ui, &mut raw_config).ok();
1201 config_env.reset_workspace_path(loader.workspace_root());
1202 config_env
1203 .reload_workspace_config(&ui, &mut raw_config)
1204 .ok();
1205 }
1206 let mut config = config_env.resolve_config(&raw_config)?;
1207 let args = std::env::args_os().skip(2);
1209 let args = expand_args(&ui, &app, args, &config)?;
1210 let arg_matches = app
1211 .clone()
1212 .disable_version_flag(true)
1213 .disable_help_flag(true)
1214 .ignore_errors(true)
1215 .try_get_matches_from(args)?;
1216 let args: GlobalArgs = GlobalArgs::from_arg_matches(&arg_matches)?;
1217
1218 if let Some(repository) = args.repository {
1219 if let Ok(loader) = DefaultWorkspaceLoaderFactory.create(&cwd.join(&repository)) {
1221 config_env.reset_repo_path(loader.repo_path());
1222 config_env.reload_repo_config(&ui, &mut raw_config).ok();
1223 config_env.reset_workspace_path(loader.workspace_root());
1224 config_env
1225 .reload_workspace_config(&ui, &mut raw_config)
1226 .ok();
1227 if let Ok(new_config) = config_env.resolve_config(&raw_config) {
1228 config = new_config;
1229 }
1230 }
1231 cmd_args.push("--repository".into());
1232 cmd_args.push(repository);
1233 }
1234 if let Some(at_operation) = args.at_operation {
1235 let mut canary_cmd = std::process::Command::new(¤t_exe);
1246 canary_cmd.args(&cmd_args);
1247 canary_cmd.arg("--at-operation");
1248 canary_cmd.arg(&at_operation);
1249 canary_cmd.arg("debug");
1250 canary_cmd.arg("snapshot");
1251
1252 match canary_cmd.output() {
1253 Ok(output) if output.status.success() => {
1254 cmd_args.push("--at-operation".into());
1256 cmd_args.push(at_operation);
1257 }
1258 _ => {} }
1260 }
1261 for (kind, value) in args.early_args.merged_config_args(&arg_matches) {
1262 let arg = match kind {
1263 ConfigArgKind::Item => format!("--config={value}"),
1264 ConfigArgKind::File => format!("--config-file={value}"),
1265 };
1266 cmd_args.push(arg);
1267 }
1268
1269 let builder = JjBuilder {
1270 cmd: current_exe,
1271 args: cmd_args,
1272 };
1273 let settings = UserSettings::from_config(config)?;
1274
1275 Ok((builder, settings))
1276}
1277
1278struct JjBuilder {
1281 cmd: std::path::PathBuf,
1282 args: Vec<String>,
1283}
1284
1285impl JjBuilder {
1286 fn build(&self) -> std::process::Command {
1287 let mut cmd = std::process::Command::new(&self.cmd);
1288 cmd.args(&self.args);
1289 cmd
1290 }
1291}
1292
1293mod parse {
1302 pub(super) fn parse_flag(
1303 candidates: &[&str],
1304 mut args: impl Iterator<Item = String>,
1305 ) -> impl Iterator<Item = String> {
1306 std::iter::from_fn(move || {
1307 for arg in args.by_ref() {
1308 if candidates.contains(&arg.as_ref()) {
1310 match args.next() {
1311 Some(val) if !val.starts_with('-') => {
1312 return Some(strip_shell_quotes(&val).into());
1313 }
1314 _ => return None,
1315 }
1316 }
1317
1318 if let Some(value) = candidates.iter().find_map(|candidate| {
1320 let rest = arg.strip_prefix(candidate)?;
1321 match rest.strip_prefix('=') {
1322 Some(value) => Some(value),
1323
1324 None if candidate.len() == 2 => Some(rest),
1326
1327 None => None,
1328 }
1329 }) {
1330 return Some(strip_shell_quotes(value).into());
1331 }
1332 }
1333 None
1334 })
1335 }
1336
1337 pub fn parse_revision_impl(args: impl Iterator<Item = String>) -> Option<String> {
1338 parse_flag(&["-r", "--revision"], args).next()
1339 }
1340
1341 pub fn revision() -> Option<String> {
1342 parse_revision_impl(std::env::args())
1343 }
1344
1345 pub fn revisions() -> Vec<String> {
1346 let candidates = &["-r", "--revision", "--revisions"];
1347 parse_flag(candidates, std::env::args()).collect()
1348 }
1349
1350 pub fn parse_changes_in_impl(args: impl Iterator<Item = String>) -> Option<String> {
1351 parse_flag(&["-c", "--changes-in"], args).next()
1352 }
1353
1354 pub fn changes_in() -> Option<String> {
1355 parse_changes_in_impl(std::env::args())
1356 }
1357
1358 pub fn revision_or_wc() -> String {
1359 revision().unwrap_or_else(|| "@".into())
1360 }
1361
1362 pub fn from_or_wc() -> String {
1363 parse_flag(&["-f", "--from"], std::env::args())
1364 .next()
1365 .unwrap_or_else(|| "@".into())
1366 }
1367
1368 pub fn parse_range_impl<T>(args: impl Fn() -> T) -> Option<(String, String)>
1369 where
1370 T: Iterator<Item = String>,
1371 {
1372 let from = parse_flag(&["-f", "--from"], args()).next();
1373 let to = parse_flag(&["-t", "--to"], args()).next();
1374 if from.is_none() && to.is_none() {
1375 return None;
1376 }
1377 Some((
1378 from.unwrap_or_else(|| "@".into()),
1379 to.unwrap_or_else(|| "@".into()),
1380 ))
1381 }
1382
1383 pub fn range() -> Option<(String, String)> {
1384 parse_range_impl(std::env::args)
1385 }
1386
1387 pub fn squash_revision() -> Option<String> {
1392 if let Some(rev) = parse_flag(&["-r", "--revision"], std::env::args()).next() {
1393 return Some(rev);
1394 }
1395 parse_flag(&["-f", "--from"], std::env::args()).next()
1396 }
1397
1398 fn strip_shell_quotes(s: &str) -> &str {
1399 if s.len() >= 2
1400 && (s.starts_with('"') && s.ends_with('"') || s.starts_with('\'') && s.ends_with('\''))
1401 {
1402 &s[1..s.len() - 1]
1403 } else {
1404 s
1405 }
1406 }
1407}
1408
1409#[cfg(test)]
1410mod tests {
1411 use super::*;
1412
1413 #[test]
1414 fn test_split_revset_trailing_name() {
1415 assert_eq!(split_revset_trailing_name(""), Some(("", "")));
1416 assert_eq!(split_revset_trailing_name(" "), Some((" ", "")));
1417 assert_eq!(split_revset_trailing_name("foo"), Some(("", "foo")));
1418 assert_eq!(split_revset_trailing_name(" foo"), Some((" ", "foo")));
1419 assert_eq!(split_revset_trailing_name("foo "), None);
1420 assert_eq!(split_revset_trailing_name("foo_"), Some(("", "foo_")));
1421 assert_eq!(split_revset_trailing_name("foo/"), Some(("", "foo/")));
1422 assert_eq!(split_revset_trailing_name("foo/b"), Some(("", "foo/b")));
1423
1424 assert_eq!(split_revset_trailing_name("foo-"), Some(("", "foo-")));
1425 assert_eq!(split_revset_trailing_name("foo+"), Some(("", "foo+")));
1426 assert_eq!(
1427 split_revset_trailing_name("foo-bar-"),
1428 Some(("", "foo-bar-"))
1429 );
1430 assert_eq!(
1431 split_revset_trailing_name("foo-bar-b"),
1432 Some(("", "foo-bar-b"))
1433 );
1434
1435 assert_eq!(split_revset_trailing_name("foo."), Some(("", "foo.")));
1436 assert_eq!(split_revset_trailing_name("foo..b"), Some(("foo..", "b")));
1437 assert_eq!(split_revset_trailing_name("..foo"), Some(("..", "foo")));
1438
1439 assert_eq!(split_revset_trailing_name("foo(bar"), Some(("foo(", "bar")));
1440 assert_eq!(split_revset_trailing_name("foo(bar)"), None);
1441 assert_eq!(split_revset_trailing_name("(f"), Some(("(", "f")));
1442
1443 assert_eq!(split_revset_trailing_name("foo@"), Some(("", "foo@")));
1444 assert_eq!(split_revset_trailing_name("foo@b"), Some(("", "foo@b")));
1445 assert_eq!(split_revset_trailing_name("..foo@"), Some(("..", "foo@")));
1446 assert_eq!(
1447 split_revset_trailing_name("::F(foo@origin.1..bar@origin."),
1448 Some(("::F(foo@origin.1..", "bar@origin."))
1449 );
1450 }
1451
1452 #[test]
1453 fn test_split_revset_trailing_name_with_trailing_operator() {
1454 assert_eq!(split_revset_trailing_name("foo|"), Some(("foo|", "")));
1455 assert_eq!(split_revset_trailing_name("foo | "), Some(("foo | ", "")));
1456 assert_eq!(split_revset_trailing_name("foo&"), Some(("foo&", "")));
1457 assert_eq!(split_revset_trailing_name("foo~"), Some(("foo~", "")));
1458
1459 assert_eq!(split_revset_trailing_name(".."), Some(("..", "")));
1460 assert_eq!(split_revset_trailing_name("foo.."), Some(("foo..", "")));
1461 assert_eq!(split_revset_trailing_name("::"), Some(("::", "")));
1462 assert_eq!(split_revset_trailing_name("foo::"), Some(("foo::", "")));
1463
1464 assert_eq!(split_revset_trailing_name("("), Some(("(", "")));
1465 assert_eq!(split_revset_trailing_name("foo("), Some(("foo(", "")));
1466 assert_eq!(split_revset_trailing_name("foo()"), None);
1467 assert_eq!(split_revset_trailing_name("foo(bar)"), None);
1468 }
1469
1470 #[test]
1471 fn test_config_keys() {
1472 config_keys();
1474 }
1475
1476 #[test]
1477 fn test_parse_revision_impl() {
1478 let good_cases: &[&[&str]] = &[
1479 &["-r", "foo"],
1480 &["-r", "'foo'"],
1481 &["-r", "\"foo\""],
1482 &["-rfoo"],
1483 &["-r'foo'"],
1484 &["-r\"foo\""],
1485 &["--revision", "foo"],
1486 &["-r=foo"],
1487 &["-r='foo'"],
1488 &["-r=\"foo\""],
1489 &["--revision=foo"],
1490 &["--revision='foo'"],
1491 &["--revision=\"foo\""],
1492 &["preceding_arg", "-r", "foo"],
1493 &["-r", "foo", "following_arg"],
1494 ];
1495 for case in good_cases {
1496 let args = case.iter().map(|s| s.to_string());
1497 assert_eq!(
1498 parse::parse_revision_impl(args),
1499 Some("foo".into()),
1500 "case: {case:?}",
1501 );
1502 }
1503 let bad_cases: &[&[&str]] = &[&[], &["-r"], &["foo"], &["-R", "foo"], &["-R=foo"]];
1504 for case in bad_cases {
1505 let args = case.iter().map(|s| s.to_string());
1506 assert_eq!(parse::parse_revision_impl(args), None, "case: {case:?}");
1507 }
1508 }
1509
1510 #[test]
1511 fn test_parse_changes_in_impl() {
1512 let good_cases: &[&[&str]] = &[
1513 &["-c", "foo"],
1514 &["--changes-in", "foo"],
1515 &["-cfoo"],
1516 &["--changes-in=foo"],
1517 ];
1518 for case in good_cases {
1519 let args = case.iter().map(|s| s.to_string());
1520 assert_eq!(
1521 parse::parse_changes_in_impl(args),
1522 Some("foo".into()),
1523 "case: {case:?}",
1524 );
1525 }
1526 let bad_cases: &[&[&str]] = &[&[], &["-c"], &["-r"], &["foo"]];
1527 for case in bad_cases {
1528 let args = case.iter().map(|s| s.to_string());
1529 assert_eq!(parse::parse_revision_impl(args), None, "case: {case:?}");
1530 }
1531 }
1532
1533 #[test]
1534 fn test_parse_range_impl() {
1535 let wc_cases: &[&[&str]] = &[
1536 &["-f", "foo"],
1537 &["--from", "foo"],
1538 &["-f=foo"],
1539 &["preceding_arg", "-f", "foo"],
1540 &["-f", "foo", "following_arg"],
1541 ];
1542 for case in wc_cases {
1543 let args = case.iter().map(|s| s.to_string());
1544 assert_eq!(
1545 parse::parse_range_impl(|| args.clone()),
1546 Some(("foo".into(), "@".into())),
1547 "case: {case:?}",
1548 );
1549 }
1550 let from_working_copy_cases: &[&[&str]] =
1551 &[&["-t", "bar"], &["--to", "bar"], &["-t=bar"], &["--to=bar"]];
1552 for case in from_working_copy_cases {
1553 let args = case.iter().map(|s| s.to_string());
1554 assert_eq!(
1555 parse::parse_range_impl(|| args.clone()),
1556 Some(("@".into(), "bar".into())),
1557 "case: {case:?}",
1558 );
1559 }
1560 let to_cases: &[&[&str]] = &[
1561 &["-f", "foo", "-t", "bar"],
1562 &["-f", "foo", "--to", "bar"],
1563 &["-f=foo", "-t=bar"],
1564 &["-t=bar", "-f=foo"],
1565 ];
1566 for case in to_cases {
1567 let args = case.iter().map(|s| s.to_string());
1568 assert_eq!(
1569 parse::parse_range_impl(|| args.clone()),
1570 Some(("foo".into(), "bar".into())),
1571 "case: {case:?}",
1572 );
1573 }
1574 let bad_cases: &[&[&str]] = &[&[], &["-f"], &["foo"], &["-R", "foo"], &["-R=foo"]];
1575 for case in bad_cases {
1576 let args = case.iter().map(|s| s.to_string());
1577 assert_eq!(
1578 parse::parse_range_impl(|| args.clone()),
1579 None,
1580 "case: {case:?}"
1581 );
1582 }
1583 }
1584
1585 #[test]
1586 fn test_parse_multiple_flags() {
1587 let candidates = &["-r", "--revisions"];
1588 let args = &[
1589 "unrelated_arg_at_the_beginning",
1590 "-r",
1591 "1",
1592 "--revisions",
1593 "2",
1594 "-r=3",
1595 "--revisions=4",
1596 "unrelated_arg_in_the_middle",
1597 "-r5",
1598 "unrelated_arg_at_the_end",
1599 ];
1600 let flags: Vec<_> =
1601 parse::parse_flag(candidates, args.iter().map(|a| a.to_string())).collect();
1602 let expected = ["1", "2", "3", "4", "5"];
1603 assert_eq!(flags, expected);
1604 }
1605}