jj_cli/
complete.rs

1// Copyright 2024 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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_template_aliases;
35use crate::command_error::CommandError;
36use crate::command_error::user_error;
37use crate::config::CONFIG_SCHEMA;
38use crate::config::ConfigArgKind;
39use crate::config::ConfigEnv;
40use crate::config::config_from_environment;
41use crate::config::default_config_layers;
42use crate::merge_tools::ExternalMergeTool;
43use crate::merge_tools::configured_merge_tools;
44use crate::merge_tools::get_external_tool_config;
45use crate::revset_util::load_revset_aliases;
46use crate::ui::Ui;
47
48const BOOKMARK_HELP_TEMPLATE: &str = r#"template-aliases.'bookmark_help()'='''
49" " ++
50if(normal_target,
51    if(normal_target.description(),
52        normal_target.description().first_line(),
53        "(no description set)",
54    ),
55    "(conflicted bookmark)",
56)
57'''"#;
58const TAG_HELP_TEMPLATE: &str = r#"template-aliases.'tag_help()'='''
59" " ++
60if(normal_target,
61    if(normal_target.description(),
62        normal_target.description().first_line(),
63        "(no description set)",
64    ),
65    "(conflicted tag)",
66)
67'''"#;
68
69/// A helper function for various completer functions. It returns
70/// (candidate, help) assuming they are separated by a space.
71fn split_help_text(line: &str) -> (&str, Option<StyledStr>) {
72    match line.split_once(' ') {
73        Some((name, help)) => (name, Some(help.to_string().into())),
74        None => (line, None),
75    }
76}
77
78pub fn local_bookmarks() -> Vec<CompletionCandidate> {
79    with_jj(|jj, _| {
80        let output = jj
81            .build()
82            .arg("bookmark")
83            .arg("list")
84            .arg("--config")
85            .arg(BOOKMARK_HELP_TEMPLATE)
86            .arg("--template")
87            .arg(r#"if(!remote, name ++ bookmark_help()) ++ "\n""#)
88            .output()
89            .map_err(user_error)?;
90
91        Ok(String::from_utf8_lossy(&output.stdout)
92            .lines()
93            .map(split_help_text)
94            .map(|(name, help)| CompletionCandidate::new(name).help(help))
95            .collect())
96    })
97}
98
99pub fn tracked_bookmarks() -> Vec<CompletionCandidate> {
100    with_jj(|jj, _| {
101        let output = jj
102            .build()
103            .arg("bookmark")
104            .arg("list")
105            .arg("--tracked")
106            .arg("--config")
107            .arg(BOOKMARK_HELP_TEMPLATE)
108            .arg("--template")
109            .arg(r#"if(remote, name ++ '@' ++ remote ++ bookmark_help() ++ "\n")"#)
110            .output()
111            .map_err(user_error)?;
112
113        Ok(String::from_utf8_lossy(&output.stdout)
114            .lines()
115            .map(split_help_text)
116            .map(|(name, help)| CompletionCandidate::new(name).help(help))
117            .collect())
118    })
119}
120
121pub fn untracked_bookmarks() -> Vec<CompletionCandidate> {
122    with_jj(|jj, _settings| {
123        let remotes = jj
124            .build()
125            .arg("git")
126            .arg("remote")
127            .arg("list")
128            .output()
129            .map_err(user_error)?;
130        let remotes = String::from_utf8_lossy(&remotes.stdout);
131        let remotes = remotes
132            .lines()
133            .filter_map(|l| l.split_whitespace().next())
134            .collect_vec();
135
136        let bookmark_table = jj
137            .build()
138            .arg("bookmark")
139            .arg("list")
140            .arg("--all-remotes")
141            .arg("--config")
142            .arg(BOOKMARK_HELP_TEMPLATE)
143            .arg("--template")
144            .arg(
145                r#"
146                if(remote != "git",
147                    if(!remote, name) ++ "\t" ++
148                    if(remote, name ++ "@" ++ remote) ++ "\t" ++
149                    if(tracked, "tracked") ++ "\t" ++
150                    bookmark_help() ++ "\n"
151                )"#,
152            )
153            .output()
154            .map_err(user_error)?;
155        let bookmark_table = String::from_utf8_lossy(&bookmark_table.stdout);
156
157        let mut possible_bookmarks_to_track = Vec::new();
158        let mut already_tracked_bookmarks = HashSet::new();
159
160        for line in bookmark_table.lines() {
161            let [local, remote, tracked, help] =
162                line.split('\t').collect_array().unwrap_or_default();
163
164            if !local.is_empty() {
165                possible_bookmarks_to_track.extend(
166                    remotes
167                        .iter()
168                        .map(|remote| (format!("{local}@{remote}"), help)),
169                );
170            } else if tracked.is_empty() {
171                possible_bookmarks_to_track.push((remote.to_owned(), help));
172            } else {
173                already_tracked_bookmarks.insert(remote);
174            }
175        }
176        possible_bookmarks_to_track
177            .retain(|(bookmark, _help)| !already_tracked_bookmarks.contains(&bookmark.as_str()));
178
179        Ok(possible_bookmarks_to_track
180            .into_iter()
181            .map(|(bookmark, help)| {
182                CompletionCandidate::new(bookmark).help(Some(help.to_string().into()))
183            })
184            .collect())
185    })
186}
187
188pub fn bookmarks() -> Vec<CompletionCandidate> {
189    with_jj(|jj, _settings| {
190        let output = jj
191            .build()
192            .arg("bookmark")
193            .arg("list")
194            .arg("--all-remotes")
195            .arg("--config")
196            .arg(BOOKMARK_HELP_TEMPLATE)
197            .arg("--template")
198            .arg(
199                // only provide help for local refs, remote could be ambiguous
200                r#"name ++ if(remote, "@" ++ remote, bookmark_help()) ++ "\n""#,
201            )
202            .output()
203            .map_err(user_error)?;
204        let stdout = String::from_utf8_lossy(&output.stdout);
205
206        Ok((&stdout
207            .lines()
208            .map(split_help_text)
209            .chunk_by(|(name, _)| name.split_once('@').map(|t| t.0).unwrap_or(name)))
210            .into_iter()
211            .map(|(bookmark, mut refs)| {
212                let help = refs.find_map(|(_, help)| help);
213                let local = help.is_some();
214                let display_order = match local {
215                    true => 0,
216                    false => 1,
217                };
218                CompletionCandidate::new(bookmark)
219                    .help(help)
220                    .display_order(Some(display_order))
221            })
222            .collect())
223    })
224}
225
226pub fn local_tags() -> Vec<CompletionCandidate> {
227    with_jj(|jj, _| {
228        let output = jj
229            .build()
230            .arg("tag")
231            .arg("list")
232            .arg("--config")
233            .arg(TAG_HELP_TEMPLATE)
234            .arg("--template")
235            .arg(r#"if(!remote, name ++ tag_help()) ++ "\n""#)
236            .output()
237            .map_err(user_error)?;
238
239        Ok(String::from_utf8_lossy(&output.stdout)
240            .lines()
241            .map(split_help_text)
242            .map(|(name, help)| CompletionCandidate::new(name).help(help))
243            .collect())
244    })
245}
246
247pub fn git_remotes() -> Vec<CompletionCandidate> {
248    with_jj(|jj, _| {
249        let output = jj
250            .build()
251            .arg("git")
252            .arg("remote")
253            .arg("list")
254            .output()
255            .map_err(user_error)?;
256
257        let stdout = String::from_utf8_lossy(&output.stdout);
258
259        Ok(stdout
260            .lines()
261            .filter_map(|line| line.split_once(' ').map(|(name, _url)| name))
262            .map(CompletionCandidate::new)
263            .collect())
264    })
265}
266
267pub fn template_aliases() -> Vec<CompletionCandidate> {
268    with_jj(|_, settings| {
269        let Ok(template_aliases) = load_template_aliases(&Ui::null(), settings.config()) else {
270            return Ok(Vec::new());
271        };
272        Ok(template_aliases
273            .symbol_names()
274            .map(CompletionCandidate::new)
275            .sorted()
276            .collect())
277    })
278}
279
280pub fn aliases() -> Vec<CompletionCandidate> {
281    with_jj(|_, settings| {
282        Ok(settings
283            .table_keys("aliases")
284            // This is opinionated, but many people probably have several
285            // single- or two-letter aliases they use all the time. These
286            // aliases don't need to be completed and they would only clutter
287            // the output of `jj <TAB>`.
288            .filter(|alias| alias.len() > 2)
289            .map(CompletionCandidate::new)
290            .collect())
291    })
292}
293
294fn revisions(match_prefix: &str, revset_filter: Option<&str>) -> Vec<CompletionCandidate> {
295    with_jj(|jj, settings| {
296        // display order
297        const LOCAL_BOOKMARK: usize = 0;
298        const TAG: usize = 1;
299        const CHANGE_ID: usize = 2;
300        const REMOTE_BOOKMARK: usize = 3;
301        const REVSET_ALIAS: usize = 4;
302
303        let mut candidates = Vec::new();
304
305        // bookmarks
306
307        let mut cmd = jj.build();
308        cmd.arg("bookmark")
309            .arg("list")
310            .arg("--all-remotes")
311            .arg("--config")
312            .arg(BOOKMARK_HELP_TEMPLATE)
313            .arg("--template")
314            .arg(
315                r#"if(remote != "git", name ++ if(remote, "@" ++ remote) ++ bookmark_help() ++ "\n")"#,
316            );
317        if let Some(revs) = revset_filter {
318            cmd.arg("--revisions").arg(revs);
319        }
320        let output = cmd.output().map_err(user_error)?;
321        let stdout = String::from_utf8_lossy(&output.stdout);
322
323        candidates.extend(
324            stdout
325                .lines()
326                .map(split_help_text)
327                .filter(|(bookmark, _)| bookmark.starts_with(match_prefix))
328                .map(|(bookmark, help)| {
329                    let local = !bookmark.contains('@');
330                    let display_order = match local {
331                        true => LOCAL_BOOKMARK,
332                        false => REMOTE_BOOKMARK,
333                    };
334                    CompletionCandidate::new(bookmark)
335                        .help(help)
336                        .display_order(Some(display_order))
337                }),
338        );
339
340        // tags
341
342        // Tags cannot be filtered by revisions. In order to avoid suggesting
343        // immutable tags for mutable revision args, we skip tags entirely if
344        // revset_filter is set. This is not a big loss, since tags usually point
345        // to immutable revisions anyway.
346        if revset_filter.is_none() {
347            let output = jj
348                .build()
349                .arg("tag")
350                .arg("list")
351                .arg("--config")
352                .arg(BOOKMARK_HELP_TEMPLATE)
353                .arg("--template")
354                .arg(r#"name ++ bookmark_help() ++ "\n""#)
355                .arg(format!("glob:{}*", globset::escape(match_prefix)))
356                .output()
357                .map_err(user_error)?;
358            let stdout = String::from_utf8_lossy(&output.stdout);
359
360            candidates.extend(stdout.lines().map(|line| {
361                let (name, desc) = split_help_text(line);
362                CompletionCandidate::new(name)
363                    .help(desc)
364                    .display_order(Some(TAG))
365            }));
366        }
367
368        // change IDs
369
370        let revisions = revset_filter
371            .map(String::from)
372            .or_else(|| settings.get_string("revsets.short-prefixes").ok())
373            .or_else(|| settings.get_string("revsets.log").ok())
374            .unwrap_or_default();
375
376        let output = jj
377            .build()
378            .arg("log")
379            .arg("--no-graph")
380            .arg("--limit")
381            .arg("100")
382            .arg("--revisions")
383            .arg(revisions)
384            .arg("--template")
385            .arg(r#"change_id.shortest() ++ " " ++ if(description, description.first_line(), "(no description set)") ++ "\n""#)
386            .output()
387            .map_err(user_error)?;
388        let stdout = String::from_utf8_lossy(&output.stdout);
389
390        candidates.extend(
391            stdout
392                .lines()
393                .map(split_help_text)
394                .filter(|(id, _)| id.starts_with(match_prefix))
395                .map(|(id, desc)| {
396                    CompletionCandidate::new(id)
397                        .help(desc)
398                        .display_order(Some(CHANGE_ID))
399                }),
400        );
401
402        // revset aliases
403
404        let revset_aliases = load_revset_aliases(&Ui::null(), settings.config())?;
405        let mut symbol_names: Vec<_> = revset_aliases.symbol_names().collect();
406        symbol_names.sort();
407        candidates.extend(
408            symbol_names
409                .into_iter()
410                .filter(|symbol| symbol.starts_with(match_prefix))
411                .map(|symbol| {
412                    let (_, defn) = revset_aliases.get_symbol(symbol).unwrap();
413                    CompletionCandidate::new(symbol)
414                        .help(Some(defn.into()))
415                        .display_order(Some(REVSET_ALIAS))
416                }),
417        );
418
419        Ok(candidates)
420    })
421}
422
423fn revset_expression(
424    current: &std::ffi::OsStr,
425    revset_filter: Option<&str>,
426) -> Vec<CompletionCandidate> {
427    let Some(current) = current.to_str() else {
428        return Vec::new();
429    };
430    let (prepend, match_prefix) = split_revset_trailing_name(current).unwrap_or(("", current));
431    let candidates = revisions(match_prefix, revset_filter);
432    if prepend.is_empty() {
433        candidates
434    } else {
435        candidates
436            .into_iter()
437            .map(|candidate| candidate.add_prefix(prepend))
438            .collect()
439    }
440}
441
442pub fn revset_expression_all(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
443    revset_expression(current, None)
444}
445
446pub fn revset_expression_mutable(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
447    revset_expression(current, Some("mutable()"))
448}
449
450pub fn revset_expression_mutable_conflicts(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
451    revset_expression(current, Some("mutable() & conflicts()"))
452}
453
454/// Identifies if an incomplete expression ends with a name, or may be continued
455/// with a name.
456///
457/// If the expression ends with an name or a partial name, returns a tuple that
458/// splits the string at the point the name starts.
459/// If the expression is empty or ends with a prefix or infix operator that
460/// could plausibly be followed by a name, returns a tuple where the first
461/// item is the entire input string, and the second item is empty.
462/// Otherwise, returns `None`.
463///
464/// The input expression may be incomplete (e.g. missing closing parentheses),
465/// and the ability to reject invalid expressions is limited.
466fn split_revset_trailing_name(incomplete_revset_str: &str) -> Option<(&str, &str)> {
467    let final_part = incomplete_revset_str
468        .rsplit_once([':', '~', '|', '&', '(', ','])
469        .map(|(_, rest)| rest)
470        .unwrap_or(incomplete_revset_str);
471    let final_part = final_part
472        .rsplit_once("..")
473        .map(|(_, rest)| rest)
474        .unwrap_or(final_part)
475        .trim_ascii_start();
476
477    let re = regex::Regex::new(r"^(?:[\p{XID_CONTINUE}_/]+[@.+-])*[\p{XID_CONTINUE}_/]*$").unwrap();
478    re.is_match(final_part)
479        .then(|| incomplete_revset_str.split_at(incomplete_revset_str.len() - final_part.len()))
480}
481
482pub fn operations() -> Vec<CompletionCandidate> {
483    with_jj(|jj, _| {
484        let output = jj
485            .build()
486            .arg("operation")
487            .arg("log")
488            .arg("--no-graph")
489            .arg("--limit")
490            .arg("100")
491            .arg("--template")
492            .arg(
493                r#"
494                separate(" ",
495                    id.short(),
496                    "(" ++ format_timestamp(time.end()) ++ ")",
497                    description.first_line(),
498                ) ++ "\n""#,
499            )
500            .output()
501            .map_err(user_error)?;
502
503        Ok(String::from_utf8_lossy(&output.stdout)
504            .lines()
505            .map(|line| {
506                let (id, help) = split_help_text(line);
507                CompletionCandidate::new(id).help(help)
508            })
509            .collect())
510    })
511}
512
513pub fn workspaces() -> Vec<CompletionCandidate> {
514    let template = indoc! {r#"
515        name ++ "\t" ++ if(
516            target.description(),
517            target.description().first_line(),
518            "(no description set)"
519        ) ++ "\n"
520    "#};
521    with_jj(|jj, _| {
522        let output = jj
523            .build()
524            .arg("workspace")
525            .arg("list")
526            .arg("--template")
527            .arg(template)
528            .output()
529            .map_err(user_error)?;
530        let stdout = String::from_utf8_lossy(&output.stdout);
531
532        Ok(stdout
533            .lines()
534            .filter_map(|line| {
535                let res = line.split_once("\t").map(|(name, desc)| {
536                    CompletionCandidate::new(name).help(Some(desc.to_string().into()))
537                });
538                if res.is_none() {
539                    eprintln!("Error parsing line {line}");
540                }
541                res
542            })
543            .collect())
544    })
545}
546
547fn merge_tools_filtered_by(
548    settings: &UserSettings,
549    condition: impl Fn(ExternalMergeTool) -> bool,
550) -> impl Iterator<Item = &str> {
551    configured_merge_tools(settings).filter(move |name| {
552        let Ok(Some(tool)) = get_external_tool_config(settings, name) else {
553            return false;
554        };
555        condition(tool)
556    })
557}
558
559pub fn merge_editors() -> Vec<CompletionCandidate> {
560    with_jj(|_, settings| {
561        Ok([":builtin", ":ours", ":theirs"]
562            .into_iter()
563            .chain(merge_tools_filtered_by(settings, |tool| {
564                !tool.merge_args.is_empty()
565            }))
566            .map(CompletionCandidate::new)
567            .collect())
568    })
569}
570
571/// Approximate list of known diff editors
572pub fn diff_editors() -> Vec<CompletionCandidate> {
573    with_jj(|_, settings| {
574        Ok(std::iter::once(":builtin")
575            .chain(merge_tools_filtered_by(
576                settings,
577                // The args are empty only if `edit-args` are explicitly set to
578                // `[]` in TOML. If they are not specified, the default
579                // `["$left", "$right"]` value would be used.
580                |tool| !tool.edit_args.is_empty(),
581            ))
582            .map(CompletionCandidate::new)
583            .collect())
584    })
585}
586
587/// Approximate list of known diff tools
588pub fn diff_formatters() -> Vec<CompletionCandidate> {
589    let builtin_format_kinds = crate::diff_util::all_builtin_diff_format_names();
590    with_jj(|_, settings| {
591        Ok(builtin_format_kinds
592            .iter()
593            .map(|s| s.as_str())
594            .chain(merge_tools_filtered_by(
595                settings,
596                // The args are empty only if `diff-args` are explicitly set to
597                // `[]` in TOML. If they are not specified, the default
598                // `["$left", "$right"]` value would be used.
599                |tool| !tool.diff_args.is_empty(),
600            ))
601            .map(CompletionCandidate::new)
602            .collect())
603    })
604}
605
606fn config_keys_rec(
607    prefix: ConfigNamePathBuf,
608    properties: &serde_json::Map<String, serde_json::Value>,
609    acc: &mut Vec<CompletionCandidate>,
610    only_leaves: bool,
611    suffix: &str,
612) {
613    for (key, value) in properties {
614        let mut prefix = prefix.clone();
615        prefix.push(key);
616
617        let value = value.as_object().unwrap();
618        match value.get("type").and_then(|v| v.as_str()) {
619            Some("object") => {
620                if !only_leaves {
621                    let help = value
622                        .get("description")
623                        .map(|desc| desc.as_str().unwrap().to_string().into());
624                    let escaped_key = prefix.to_string();
625                    acc.push(CompletionCandidate::new(escaped_key).help(help));
626                }
627                let Some(properties) = value.get("properties") else {
628                    continue;
629                };
630                let properties = properties.as_object().unwrap();
631                config_keys_rec(prefix, properties, acc, only_leaves, suffix);
632            }
633            _ => {
634                let help = value
635                    .get("description")
636                    .map(|desc| desc.as_str().unwrap().to_string().into());
637                let escaped_key = format!("{prefix}{suffix}");
638                acc.push(CompletionCandidate::new(escaped_key).help(help));
639            }
640        }
641    }
642}
643
644fn json_keypath<'a>(
645    schema: &'a serde_json::Value,
646    keypath: &str,
647    separator: &str,
648) -> Option<&'a serde_json::Value> {
649    keypath
650        .split(separator)
651        .try_fold(schema, |value, step| value.get(step))
652}
653fn jsonschema_keypath<'a>(
654    schema: &'a serde_json::Value,
655    keypath: &ConfigNamePathBuf,
656) -> Option<&'a serde_json::Value> {
657    keypath.components().try_fold(schema, |value, step| {
658        let value = value.as_object()?;
659        if value.get("type")?.as_str()? != "object" {
660            return None;
661        }
662        let properties = value.get("properties")?.as_object()?;
663        properties.get(step.get())
664    })
665}
666
667fn config_values(path: &ConfigNamePathBuf) -> Option<Vec<String>> {
668    let schema: serde_json::Value = serde_json::from_str(CONFIG_SCHEMA).unwrap();
669
670    let mut config_entry = jsonschema_keypath(&schema, path)?;
671    if let Some(reference) = config_entry.get("$ref") {
672        let reference = reference.as_str()?.strip_prefix("#/")?;
673        config_entry = json_keypath(&schema, reference, "/")?;
674    };
675
676    if let Some(possible_values) = config_entry.get("enum") {
677        return Some(
678            possible_values
679                .as_array()?
680                .iter()
681                .filter_map(|val| val.as_str())
682                .map(ToOwned::to_owned)
683                .collect(),
684        );
685    }
686
687    Some(match config_entry.get("type")?.as_str()? {
688        "boolean" => vec!["false".into(), "true".into()],
689        _ => vec![],
690    })
691}
692
693fn config_keys_impl(only_leaves: bool, suffix: &str) -> Vec<CompletionCandidate> {
694    let schema: serde_json::Value = serde_json::from_str(CONFIG_SCHEMA).unwrap();
695    let schema = schema.as_object().unwrap();
696    let properties = schema["properties"].as_object().unwrap();
697
698    let mut candidates = Vec::new();
699    config_keys_rec(
700        ConfigNamePathBuf::root(),
701        properties,
702        &mut candidates,
703        only_leaves,
704        suffix,
705    );
706    candidates
707}
708
709pub fn config_keys() -> Vec<CompletionCandidate> {
710    config_keys_impl(false, "")
711}
712
713pub fn leaf_config_keys() -> Vec<CompletionCandidate> {
714    config_keys_impl(true, "")
715}
716
717pub fn leaf_config_key_value(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
718    let Some(current) = current.to_str() else {
719        return Vec::new();
720    };
721
722    if let Some((key, current_val)) = current.split_once('=') {
723        let Ok(key) = key.parse() else {
724            return Vec::new();
725        };
726        let possible_values = config_values(&key).unwrap_or_default();
727
728        possible_values
729            .into_iter()
730            .filter(|x| x.starts_with(current_val))
731            .map(|x| CompletionCandidate::new(format!("{key}={x}")))
732            .collect()
733    } else {
734        config_keys_impl(true, "=")
735            .into_iter()
736            .filter(|candidate| candidate.get_value().to_str().unwrap().starts_with(current))
737            .collect()
738    }
739}
740
741pub fn branch_name_equals_any_revision(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
742    let Some(current) = current.to_str() else {
743        return Vec::new();
744    };
745
746    let Some((branch_name, revision)) = current.split_once('=') else {
747        // Don't complete branch names since we want to create a new branch
748        return Vec::new();
749    };
750    revset_expression(revision.as_ref(), None)
751        .into_iter()
752        .map(|rev| rev.add_prefix(format!("{branch_name}=")))
753        .collect()
754}
755
756fn path_completion_candidate_from(
757    current_prefix: &str,
758    normalized_prefix_path: &Path,
759    path: &Path,
760    mode: Option<clap::builder::StyledStr>,
761) -> Option<CompletionCandidate> {
762    let normalized_prefix = match normalized_prefix_path.to_str()? {
763        "." => "", // `.` cannot be normalized further, but doesn't prefix `path`.
764        normalized_prefix => normalized_prefix,
765    };
766
767    let path = slash_path(path);
768    let mut remainder = path.to_str()?.strip_prefix(normalized_prefix)?;
769
770    // Trailing slash might have been normalized away in which case we need to strip
771    // the leading slash in the remainder away, or else the slash would appear
772    // twice.
773    if current_prefix.ends_with(std::path::is_separator) {
774        remainder = remainder.strip_prefix('/').unwrap_or(remainder);
775    }
776
777    match remainder.split_inclusive('/').at_most_one() {
778        // Completed component is the final component in `path`, so we're completing the file to
779        // which `mode` refers.
780        Ok(file_completion) => Some(
781            CompletionCandidate::new(format!(
782                "{current_prefix}{}",
783                file_completion.unwrap_or_default()
784            ))
785            .help(mode),
786        ),
787
788        // Omit `mode` when completing only up to the next directory.
789        Err(mut components) => Some(CompletionCandidate::new(format!(
790            "{current_prefix}{}",
791            components.next().unwrap()
792        ))),
793    }
794}
795
796fn current_prefix_to_fileset(current: &str) -> String {
797    let cur_esc = globset::escape(current);
798    let dir_pat = format!("{cur_esc}*/**");
799    let path_pat = format!("{cur_esc}*");
800    format!("glob:{dir_pat:?} | glob:{path_pat:?}")
801}
802
803fn all_files_from_rev(rev: String, current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
804    let Some(current) = current.to_str() else {
805        return Vec::new();
806    };
807
808    let normalized_prefix = normalize_path(Path::new(current));
809    let normalized_prefix = slash_path(&normalized_prefix);
810
811    with_jj(|jj, _| {
812        let mut child = jj
813            .build()
814            .arg("file")
815            .arg("list")
816            .arg("--revision")
817            .arg(rev)
818            .arg("--template")
819            .arg(r#"path.display() ++ "\n""#)
820            .arg(current_prefix_to_fileset(current))
821            .stdout(std::process::Stdio::piped())
822            .stderr(std::process::Stdio::null())
823            .spawn()
824            .map_err(user_error)?;
825        let stdout = child.stdout.take().unwrap();
826
827        Ok(std::io::BufReader::new(stdout)
828            .lines()
829            .take(1_000)
830            .map_while(Result::ok)
831            .filter_map(|path| {
832                path_completion_candidate_from(current, &normalized_prefix, Path::new(&path), None)
833            })
834            .dedup() // directories may occur multiple times
835            .collect())
836    })
837}
838
839fn modified_files_from_rev_with_jj_cmd(
840    rev: (String, Option<String>),
841    mut cmd: std::process::Command,
842    current: &std::ffi::OsStr,
843) -> Result<Vec<CompletionCandidate>, CommandError> {
844    let Some(current) = current.to_str() else {
845        return Ok(Vec::new());
846    };
847
848    let normalized_prefix = normalize_path(Path::new(current));
849    let normalized_prefix = slash_path(&normalized_prefix);
850
851    // In case of a rename, one entry of `diff` results in two suggestions.
852    let template = indoc! {r#"
853        concat(
854          status ++ ' ' ++ path.display() ++ "\n",
855          if(status == 'renamed', 'renamed.source ' ++ source.path().display() ++ "\n"),
856        )
857    "#};
858    cmd.arg("diff")
859        .args(["--template", template])
860        .arg(current_prefix_to_fileset(current));
861    match rev {
862        (rev, None) => cmd.arg("--revisions").arg(rev),
863        (from, Some(to)) => cmd.arg("--from").arg(from).arg("--to").arg(to),
864    };
865    let output = cmd.output().map_err(user_error)?;
866    let stdout = String::from_utf8_lossy(&output.stdout);
867
868    let mut include_renames = false;
869    let mut candidates: Vec<_> = stdout
870        .lines()
871        .filter_map(|line| line.split_once(' '))
872        .filter_map(|(mode, path)| {
873            let mode = match mode {
874                "modified" => "Modified".into(),
875                "removed" => "Deleted".into(),
876                "added" => "Added".into(),
877                "renamed" => "Renamed".into(),
878                "renamed.source" => {
879                    include_renames = true;
880                    "Renamed".into()
881                }
882                "copied" => "Copied".into(),
883                _ => format!("unknown mode: '{mode}'").into(),
884            };
885            path_completion_candidate_from(current, &normalized_prefix, Path::new(path), Some(mode))
886        })
887        .collect();
888
889    if include_renames {
890        candidates.sort_unstable_by(|a, b| Path::new(a.get_value()).cmp(Path::new(b.get_value())));
891    }
892    candidates.dedup();
893
894    Ok(candidates)
895}
896
897fn modified_files_from_rev(
898    rev: (String, Option<String>),
899    current: &std::ffi::OsStr,
900) -> Vec<CompletionCandidate> {
901    with_jj(|jj, _| modified_files_from_rev_with_jj_cmd(rev, jj.build(), current))
902}
903
904fn conflicted_files_from_rev(rev: &str, current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
905    let Some(current) = current.to_str() else {
906        return Vec::new();
907    };
908
909    let normalized_prefix = normalize_path(Path::new(current));
910    let normalized_prefix = slash_path(&normalized_prefix);
911
912    with_jj(|jj, _| {
913        let output = jj
914            .build()
915            .arg("resolve")
916            .arg("--list")
917            .arg("--revision")
918            .arg(rev)
919            .arg(current_prefix_to_fileset(current))
920            .output()
921            .map_err(user_error)?;
922        let stdout = String::from_utf8_lossy(&output.stdout);
923
924        Ok(stdout
925            .lines()
926            .filter_map(|line| {
927                let path = line
928                    .split_whitespace()
929                    .next()
930                    .expect("resolve --list should contain whitespace after path");
931
932                path_completion_candidate_from(current, &normalized_prefix, Path::new(path), None)
933            })
934            .dedup() // directories may occur multiple times
935            .collect())
936    })
937}
938
939pub fn modified_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
940    modified_files_from_rev(("@".into(), None), current)
941}
942
943pub fn all_revision_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
944    all_files_from_rev(parse::revision_or_wc(), current)
945}
946
947pub fn modified_revision_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
948    modified_files_from_rev((parse::revision_or_wc(), None), current)
949}
950
951pub fn modified_range_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
952    match parse::range() {
953        Some((from, to)) => modified_files_from_rev((from, Some(to)), current),
954        None => modified_files_from_rev(("@".into(), None), current),
955    }
956}
957
958/// Completes files in `@` *or* the `--from` revision (not the diff between
959/// `--from` and `@`)
960pub fn modified_from_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
961    modified_files_from_rev((parse::from_or_wc(), None), current)
962}
963
964pub fn modified_revision_or_range_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
965    if let Some(rev) = parse::revision() {
966        return modified_files_from_rev((rev, None), current);
967    }
968    modified_range_files(current)
969}
970
971pub fn modified_changes_in_or_range_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
972    if let Some(rev) = parse::changes_in() {
973        return modified_files_from_rev((rev, None), current);
974    }
975    modified_range_files(current)
976}
977
978pub fn revision_conflicted_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
979    conflicted_files_from_rev(&parse::revision_or_wc(), current)
980}
981
982/// Specific function for completing file paths for `jj squash`
983pub fn squash_revision_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
984    let rev = parse::squash_revision().unwrap_or_else(|| "@".into());
985    modified_files_from_rev((rev, None), current)
986}
987
988/// Specific function for completing file paths for `jj interdiff`
989pub fn interdiff_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
990    let Some((from, to)) = parse::range() else {
991        return Vec::new();
992    };
993    // Complete all modified files in "from" and "to". This will also suggest
994    // files that are the same in both, which is a false positive. This approach
995    // is more lightweight than actually doing a temporary rebase here.
996    with_jj(|jj, _| {
997        let mut res = modified_files_from_rev_with_jj_cmd((from, None), jj.build(), current)?;
998        res.extend(modified_files_from_rev_with_jj_cmd(
999            (to, None),
1000            jj.build(),
1001            current,
1002        )?);
1003        Ok(res)
1004    })
1005}
1006
1007/// Specific function for completing file paths for `jj log`
1008pub fn log_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
1009    let mut rev = parse::log_revisions().join(")|(");
1010    if rev.is_empty() {
1011        rev = "@".into();
1012    } else {
1013        rev = format!("latest(heads(({rev})))"); // limit to one
1014    };
1015    all_files_from_rev(rev, current)
1016}
1017
1018/// Shell out to jj during dynamic completion generation
1019///
1020/// In case of errors, print them and early return an empty vector.
1021fn with_jj<F>(completion_fn: F) -> Vec<CompletionCandidate>
1022where
1023    F: FnOnce(JjBuilder, &UserSettings) -> Result<Vec<CompletionCandidate>, CommandError>,
1024{
1025    get_jj_command()
1026        .and_then(|(jj, settings)| completion_fn(jj, &settings))
1027        .unwrap_or_else(|e| {
1028            eprintln!("{}", e.error);
1029            Vec::new()
1030        })
1031}
1032
1033/// Shell out to jj during dynamic completion generation
1034///
1035/// This is necessary because dynamic completion code needs to be aware of
1036/// global configuration like custom storage backends. Dynamic completion
1037/// code via clap_complete doesn't accept arguments, so they cannot be passed
1038/// that way. Another solution would've been to use global mutable state, to
1039/// give completion code access to custom backends. Shelling out was chosen as
1040/// the preferred method, because it's more maintainable and the performance
1041/// requirements of completions aren't very high.
1042fn get_jj_command() -> Result<(JjBuilder, UserSettings), CommandError> {
1043    let current_exe = std::env::current_exe().map_err(user_error)?;
1044    let mut cmd_args = Vec::<String>::new();
1045
1046    // Snapshotting could make completions much slower in some situations
1047    // and be undesired by the user.
1048    cmd_args.push("--ignore-working-copy".into());
1049    cmd_args.push("--color=never".into());
1050    cmd_args.push("--no-pager".into());
1051
1052    // Parse some of the global args we care about for passing along to the
1053    // child process. This shouldn't fail, since none of the global args are
1054    // required.
1055    let app = crate::commands::default_app();
1056    let mut raw_config = config_from_environment(default_config_layers());
1057    let ui = Ui::null();
1058    let cwd = std::env::current_dir()
1059        .and_then(dunce::canonicalize)
1060        .map_err(user_error)?;
1061    // No config migration for completion. Simply ignore deprecated variables.
1062    let mut config_env = ConfigEnv::from_environment();
1063    let maybe_cwd_workspace_loader = DefaultWorkspaceLoaderFactory.create(find_workspace_dir(&cwd));
1064    let _ = config_env.reload_user_config(&mut raw_config);
1065    if let Ok(loader) = &maybe_cwd_workspace_loader {
1066        config_env.reset_repo_path(loader.repo_path());
1067        let _ = config_env.reload_repo_config(&mut raw_config);
1068        config_env.reset_workspace_path(loader.workspace_root());
1069        let _ = config_env.reload_workspace_config(&mut raw_config);
1070    }
1071    let mut config = config_env.resolve_config(&raw_config)?;
1072    // skip 2 because of the clap_complete prelude: jj -- jj <actual args...>
1073    let args = std::env::args_os().skip(2);
1074    let args = expand_args(&ui, &app, args, &config)?;
1075    let arg_matches = app
1076        .clone()
1077        .disable_version_flag(true)
1078        .disable_help_flag(true)
1079        .ignore_errors(true)
1080        .try_get_matches_from(args)?;
1081    let args: GlobalArgs = GlobalArgs::from_arg_matches(&arg_matches)?;
1082
1083    if let Some(repository) = args.repository {
1084        // Try to update repo-specific config on a best-effort basis.
1085        if let Ok(loader) = DefaultWorkspaceLoaderFactory.create(&cwd.join(&repository)) {
1086            config_env.reset_repo_path(loader.repo_path());
1087            let _ = config_env.reload_repo_config(&mut raw_config);
1088            config_env.reset_workspace_path(loader.workspace_root());
1089            let _ = config_env.reload_workspace_config(&mut raw_config);
1090            if let Ok(new_config) = config_env.resolve_config(&raw_config) {
1091                config = new_config;
1092            }
1093        }
1094        cmd_args.push("--repository".into());
1095        cmd_args.push(repository);
1096    }
1097    if let Some(at_operation) = args.at_operation {
1098        // We cannot assume that the value of at_operation is valid, because
1099        // the user may be requesting completions precisely for this invalid
1100        // operation ID. Additionally, the user may have mistyped the ID,
1101        // in which case adding the argument blindly would break all other
1102        // completions, even unrelated ones.
1103        //
1104        // To avoid this, we shell out to ourselves once with the argument
1105        // and check the exit code. There is some performance overhead to this,
1106        // but this code path is probably only executed in exceptional
1107        // situations.
1108        let mut canary_cmd = std::process::Command::new(&current_exe);
1109        canary_cmd.args(&cmd_args);
1110        canary_cmd.arg("--at-operation");
1111        canary_cmd.arg(&at_operation);
1112        canary_cmd.arg("debug");
1113        canary_cmd.arg("snapshot");
1114
1115        match canary_cmd.output() {
1116            Ok(output) if output.status.success() => {
1117                // Operation ID is valid, add it to the completion command.
1118                cmd_args.push("--at-operation".into());
1119                cmd_args.push(at_operation);
1120            }
1121            _ => {} // Invalid operation ID, ignore.
1122        }
1123    }
1124    for (kind, value) in args.early_args.merged_config_args(&arg_matches) {
1125        let arg = match kind {
1126            ConfigArgKind::Item => format!("--config={value}"),
1127            ConfigArgKind::File => format!("--config-file={value}"),
1128        };
1129        cmd_args.push(arg);
1130    }
1131
1132    let builder = JjBuilder {
1133        cmd: current_exe,
1134        args: cmd_args,
1135    };
1136    let settings = UserSettings::from_config(config)?;
1137
1138    Ok((builder, settings))
1139}
1140
1141/// A helper struct to allow completion functions to call jj multiple times with
1142/// different arguments.
1143struct JjBuilder {
1144    cmd: std::path::PathBuf,
1145    args: Vec<String>,
1146}
1147
1148impl JjBuilder {
1149    fn build(&self) -> std::process::Command {
1150        let mut cmd = std::process::Command::new(&self.cmd);
1151        cmd.args(&self.args);
1152        cmd
1153    }
1154}
1155
1156/// Functions for parsing revisions and revision ranges from the command line.
1157/// Parsing is done on a best-effort basis and relies on the heuristic that
1158/// most command line flags are consistent across different subcommands.
1159///
1160/// In some cases, this parsing will be incorrect, but it's not worth the effort
1161/// to fix that. For example, if the user specifies any of the relevant flags
1162/// multiple times, the parsing will pick any of the available ones, while the
1163/// actual execution of the command would fail.
1164mod parse {
1165    pub(super) fn parse_flag(
1166        candidates: &[&str],
1167        mut args: impl Iterator<Item = String>,
1168    ) -> impl Iterator<Item = String> {
1169        std::iter::from_fn(move || {
1170            for arg in args.by_ref() {
1171                // -r REV syntax
1172                if candidates.contains(&arg.as_ref()) {
1173                    match args.next() {
1174                        Some(val) if !val.starts_with('-') => {
1175                            return Some(strip_shell_quotes(&val).into());
1176                        }
1177                        _ => return None,
1178                    }
1179                }
1180
1181                // -r=REV syntax
1182                if let Some(value) = candidates.iter().find_map(|candidate| {
1183                    let rest = arg.strip_prefix(candidate)?;
1184                    match rest.strip_prefix('=') {
1185                        Some(value) => Some(value),
1186
1187                        // -rREV syntax
1188                        None if candidate.len() == 2 => Some(rest),
1189
1190                        None => None,
1191                    }
1192                }) {
1193                    return Some(strip_shell_quotes(value).into());
1194                };
1195            }
1196            None
1197        })
1198    }
1199
1200    pub fn parse_revision_impl(args: impl Iterator<Item = String>) -> Option<String> {
1201        parse_flag(&["-r", "--revision"], args).next()
1202    }
1203
1204    pub fn revision() -> Option<String> {
1205        parse_revision_impl(std::env::args())
1206    }
1207
1208    pub fn parse_changes_in_impl(args: impl Iterator<Item = String>) -> Option<String> {
1209        parse_flag(&["-c", "--changes-in"], args).next()
1210    }
1211
1212    pub fn changes_in() -> Option<String> {
1213        parse_changes_in_impl(std::env::args())
1214    }
1215
1216    pub fn revision_or_wc() -> String {
1217        revision().unwrap_or_else(|| "@".into())
1218    }
1219
1220    pub fn from_or_wc() -> String {
1221        parse_flag(&["-f", "--from"], std::env::args())
1222            .next()
1223            .unwrap_or_else(|| "@".into())
1224    }
1225
1226    pub fn parse_range_impl<T>(args: impl Fn() -> T) -> Option<(String, String)>
1227    where
1228        T: Iterator<Item = String>,
1229    {
1230        let from = parse_flag(&["-f", "--from"], args()).next()?;
1231        let to = parse_flag(&["-t", "--to"], args())
1232            .next()
1233            .unwrap_or_else(|| "@".into());
1234
1235        Some((from, to))
1236    }
1237
1238    pub fn range() -> Option<(String, String)> {
1239        parse_range_impl(std::env::args)
1240    }
1241
1242    // Special parse function only for `jj squash`. While squash has --from and
1243    // --to arguments, only files within --from should be completed, because
1244    // the files changed only in some other revision in the range between
1245    // --from and --to cannot be squashed into --to like that.
1246    pub fn squash_revision() -> Option<String> {
1247        if let Some(rev) = parse_flag(&["-r", "--revision"], std::env::args()).next() {
1248            return Some(rev);
1249        }
1250        parse_flag(&["-f", "--from"], std::env::args()).next()
1251    }
1252
1253    // Special parse function only for `jj log`. It has a --revisions flag,
1254    // instead of the usual --revision, and it can be supplied multiple times.
1255    pub fn log_revisions() -> Vec<String> {
1256        let candidates = &["-r", "--revisions"];
1257        parse_flag(candidates, std::env::args()).collect()
1258    }
1259
1260    fn strip_shell_quotes(s: &str) -> &str {
1261        if s.len() >= 2
1262            && (s.starts_with('"') && s.ends_with('"') || s.starts_with('\'') && s.ends_with('\''))
1263        {
1264            &s[1..s.len() - 1]
1265        } else {
1266            s
1267        }
1268    }
1269}
1270
1271#[cfg(test)]
1272mod tests {
1273    use super::*;
1274
1275    #[test]
1276    fn test_split_revset_trailing_name() {
1277        assert_eq!(split_revset_trailing_name(""), Some(("", "")));
1278        assert_eq!(split_revset_trailing_name(" "), Some((" ", "")));
1279        assert_eq!(split_revset_trailing_name("foo"), Some(("", "foo")));
1280        assert_eq!(split_revset_trailing_name(" foo"), Some((" ", "foo")));
1281        assert_eq!(split_revset_trailing_name("foo "), None);
1282        assert_eq!(split_revset_trailing_name("foo_"), Some(("", "foo_")));
1283        assert_eq!(split_revset_trailing_name("foo/"), Some(("", "foo/")));
1284        assert_eq!(split_revset_trailing_name("foo/b"), Some(("", "foo/b")));
1285
1286        assert_eq!(split_revset_trailing_name("foo-"), Some(("", "foo-")));
1287        assert_eq!(split_revset_trailing_name("foo+"), Some(("", "foo+")));
1288        assert_eq!(
1289            split_revset_trailing_name("foo-bar-"),
1290            Some(("", "foo-bar-"))
1291        );
1292        assert_eq!(
1293            split_revset_trailing_name("foo-bar-b"),
1294            Some(("", "foo-bar-b"))
1295        );
1296
1297        assert_eq!(split_revset_trailing_name("foo."), Some(("", "foo.")));
1298        assert_eq!(split_revset_trailing_name("foo..b"), Some(("foo..", "b")));
1299        assert_eq!(split_revset_trailing_name("..foo"), Some(("..", "foo")));
1300
1301        assert_eq!(split_revset_trailing_name("foo(bar"), Some(("foo(", "bar")));
1302        assert_eq!(split_revset_trailing_name("foo(bar)"), None);
1303        assert_eq!(split_revset_trailing_name("(f"), Some(("(", "f")));
1304
1305        assert_eq!(split_revset_trailing_name("foo@"), Some(("", "foo@")));
1306        assert_eq!(split_revset_trailing_name("foo@b"), Some(("", "foo@b")));
1307        assert_eq!(split_revset_trailing_name("..foo@"), Some(("..", "foo@")));
1308        assert_eq!(
1309            split_revset_trailing_name("::F(foo@origin.1..bar@origin."),
1310            Some(("::F(foo@origin.1..", "bar@origin."))
1311        );
1312    }
1313
1314    #[test]
1315    fn test_split_revset_trailing_name_with_trailing_operator() {
1316        assert_eq!(split_revset_trailing_name("foo|"), Some(("foo|", "")));
1317        assert_eq!(split_revset_trailing_name("foo | "), Some(("foo | ", "")));
1318        assert_eq!(split_revset_trailing_name("foo&"), Some(("foo&", "")));
1319        assert_eq!(split_revset_trailing_name("foo~"), Some(("foo~", "")));
1320
1321        assert_eq!(split_revset_trailing_name(".."), Some(("..", "")));
1322        assert_eq!(split_revset_trailing_name("foo.."), Some(("foo..", "")));
1323        assert_eq!(split_revset_trailing_name("::"), Some(("::", "")));
1324        assert_eq!(split_revset_trailing_name("foo::"), Some(("foo::", "")));
1325
1326        assert_eq!(split_revset_trailing_name("("), Some(("(", "")));
1327        assert_eq!(split_revset_trailing_name("foo("), Some(("foo(", "")));
1328        assert_eq!(split_revset_trailing_name("foo()"), None);
1329        assert_eq!(split_revset_trailing_name("foo(bar)"), None);
1330    }
1331
1332    #[test]
1333    fn test_split_revset_trailing_name_with_modifier() {
1334        assert_eq!(split_revset_trailing_name("all:"), Some(("all:", "")));
1335        assert_eq!(split_revset_trailing_name("all: "), Some(("all: ", "")));
1336        assert_eq!(split_revset_trailing_name("all:f"), Some(("all:", "f")));
1337        assert_eq!(split_revset_trailing_name("all: f"), Some(("all: ", "f")));
1338    }
1339
1340    #[test]
1341    fn test_config_keys() {
1342        // Just make sure the schema is parsed without failure.
1343        let _ = config_keys();
1344    }
1345
1346    #[test]
1347    fn test_parse_revision_impl() {
1348        let good_cases: &[&[&str]] = &[
1349            &["-r", "foo"],
1350            &["-r", "'foo'"],
1351            &["-r", "\"foo\""],
1352            &["-rfoo"],
1353            &["-r'foo'"],
1354            &["-r\"foo\""],
1355            &["--revision", "foo"],
1356            &["-r=foo"],
1357            &["-r='foo'"],
1358            &["-r=\"foo\""],
1359            &["--revision=foo"],
1360            &["--revision='foo'"],
1361            &["--revision=\"foo\""],
1362            &["preceding_arg", "-r", "foo"],
1363            &["-r", "foo", "following_arg"],
1364        ];
1365        for case in good_cases {
1366            let args = case.iter().map(|s| s.to_string());
1367            assert_eq!(
1368                parse::parse_revision_impl(args),
1369                Some("foo".into()),
1370                "case: {case:?}",
1371            );
1372        }
1373        let bad_cases: &[&[&str]] = &[&[], &["-r"], &["foo"], &["-R", "foo"], &["-R=foo"]];
1374        for case in bad_cases {
1375            let args = case.iter().map(|s| s.to_string());
1376            assert_eq!(parse::parse_revision_impl(args), None, "case: {case:?}");
1377        }
1378    }
1379
1380    #[test]
1381    fn test_parse_changes_in_impl() {
1382        let good_cases: &[&[&str]] = &[
1383            &["-c", "foo"],
1384            &["--changes-in", "foo"],
1385            &["-cfoo"],
1386            &["--changes-in=foo"],
1387        ];
1388        for case in good_cases {
1389            let args = case.iter().map(|s| s.to_string());
1390            assert_eq!(
1391                parse::parse_changes_in_impl(args),
1392                Some("foo".into()),
1393                "case: {case:?}",
1394            );
1395        }
1396        let bad_cases: &[&[&str]] = &[&[], &["-c"], &["-r"], &["foo"]];
1397        for case in bad_cases {
1398            let args = case.iter().map(|s| s.to_string());
1399            assert_eq!(parse::parse_revision_impl(args), None, "case: {case:?}");
1400        }
1401    }
1402
1403    #[test]
1404    fn test_parse_range_impl() {
1405        let wc_cases: &[&[&str]] = &[
1406            &["-f", "foo"],
1407            &["--from", "foo"],
1408            &["-f=foo"],
1409            &["preceding_arg", "-f", "foo"],
1410            &["-f", "foo", "following_arg"],
1411        ];
1412        for case in wc_cases {
1413            let args = case.iter().map(|s| s.to_string());
1414            assert_eq!(
1415                parse::parse_range_impl(|| args.clone()),
1416                Some(("foo".into(), "@".into())),
1417                "case: {case:?}",
1418            );
1419        }
1420        let to_cases: &[&[&str]] = &[
1421            &["-f", "foo", "-t", "bar"],
1422            &["-f", "foo", "--to", "bar"],
1423            &["-f=foo", "-t=bar"],
1424            &["-t=bar", "-f=foo"],
1425        ];
1426        for case in to_cases {
1427            let args = case.iter().map(|s| s.to_string());
1428            assert_eq!(
1429                parse::parse_range_impl(|| args.clone()),
1430                Some(("foo".into(), "bar".into())),
1431                "case: {case:?}",
1432            );
1433        }
1434        let bad_cases: &[&[&str]] = &[&[], &["-f"], &["foo"], &["-R", "foo"], &["-R=foo"]];
1435        for case in bad_cases {
1436            let args = case.iter().map(|s| s.to_string());
1437            assert_eq!(
1438                parse::parse_range_impl(|| args.clone()),
1439                None,
1440                "case: {case:?}"
1441            );
1442        }
1443    }
1444
1445    #[test]
1446    fn test_parse_multiple_flags() {
1447        let candidates = &["-r", "--revisions"];
1448        let args = &[
1449            "unrelated_arg_at_the_beginning",
1450            "-r",
1451            "1",
1452            "--revisions",
1453            "2",
1454            "-r=3",
1455            "--revisions=4",
1456            "unrelated_arg_in_the_middle",
1457            "-r5",
1458            "unrelated_arg_at_the_end",
1459        ];
1460        let flags: Vec<_> =
1461            parse::parse_flag(candidates, args.iter().map(|a| a.to_string())).collect();
1462        let expected = ["1", "2", "3", "4", "5"];
1463        assert_eq!(flags, expected);
1464    }
1465}