1639 lines · 55.8 KB · Rust
RawDownload
// Copyright 2024 The Jujutsu Authors//// Licensed under the Apache License, Version 2.0 (the "License");// you may not use this file except in compliance with the License.// You may obtain a copy of the License at//// https://www.apache.org/licenses/LICENSE-2.0//// Unless required by applicable law or agreed to in writing, software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.// See the License for the specific language governing permissions and// limitations under the License. use std::collections::HashSet;use std::io::BufRead as _;use std::path::Path; use clap::FromArgMatches as _;use clap::builder::StyledStr;use clap_complete::CompletionCandidate;use indoc::indoc;use itertools::Itertools as _;use jj_lib::config::ConfigNamePathBuf;use jj_lib::default_backend_factories::default_workspace_loader_factory;use jj_lib::file_util::normalize_path;use jj_lib::file_util::slash_path;use jj_lib::settings::UserSettings; use crate::cli_util::GlobalArgs;use crate::cli_util::expand_args;use crate::cli_util::find_workspace_dir;use crate::cli_util::load_revset_aliases;use crate::cli_util::load_template_aliases;use crate::command_error::CommandError;use crate::command_error::user_error;use crate::config::CONFIG_SCHEMA;use crate::config::ConfigArgKind;use crate::config::ConfigEnv;use crate::config::config_from_environment;use crate::config::default_config_layers;use crate::merge_tools::ExternalMergeTool;use crate::merge_tools::configured_merge_tools;use crate::merge_tools::get_external_tool_config;use crate::ui::Ui; const BOOKMARK_HELP_TEMPLATE: &str = r#"template-aliases.'bookmark_help()'='''" " ++coalesce(    if(!present, "(deleted bookmark)"),    if(!normal_target, "(conflicted bookmark)"),    if(!normal_target.description(), "(no description set)"),    normal_target.description().first_line(),)'''"#;const TAG_HELP_TEMPLATE: &str = r#"template-aliases.'tag_help()'='''" " ++coalesce(    if(!present, "(deleted tag)"),    if(!normal_target, "(conflicted tag)"),    if(!normal_target.description(), "(no description set)"),    normal_target.description().first_line(),)'''"#; /// A helper function for various completer functions. It returns/// (candidate, help) assuming they are separated by a space.fn split_help_text(line: &str) -> (&str, Option<StyledStr>) {    match line.split_once(' ') {        Some((name, help)) => (name, Some(help.to_string().into())),        None => (line, None),    }} pub fn local_bookmarks() -> Vec<CompletionCandidate> {    with_jj(|jj, _| {        let output = jj            .build()            .arg("bookmark")            .arg("list")            .arg("--config")            .arg(BOOKMARK_HELP_TEMPLATE)            .arg("--template")            .arg(r#"if(!remote, name ++ bookmark_help()) ++ "\n""#)            .output()            .map_err(user_error)?;         Ok(String::from_utf8_lossy(&output.stdout)            .lines()            .map(split_help_text)            .map(|(name, help)| CompletionCandidate::new(name).help(help))            .collect())    })} pub fn tracked_bookmarks() -> Vec<CompletionCandidate> {    tracked_refs("bookmark", BOOKMARK_HELP_TEMPLATE)} pub fn tracked_tags() -> Vec<CompletionCandidate> {    tracked_refs("tag", TAG_HELP_TEMPLATE)} fn tracked_refs(kind: &str, help_template: &str) -> Vec<CompletionCandidate> {    with_jj(|jj, _| {        let output = jj            .build()            .arg(kind)            .arg("list")            .arg("--tracked")            .arg("--config")            .arg(help_template)            .arg("--template")            .arg(format!(                r#"if(remote, name ++ '@' ++ remote ++ {kind}_help() ++ "\n")"#            ))            .output()            .map_err(user_error)?;         Ok(String::from_utf8_lossy(&output.stdout)            .lines()            .map(split_help_text)            .filter_map(|(symbol, help)| Some((symbol.split_once('@')?, help)))            // There may be multiple remote bookmarks to untrack. Just pick the            // first one for help text.            .dedup_by(|((name1, _), _), ((name2, _), _)| name1 == name2)            .map(|((name, _remote), help)| CompletionCandidate::new(name).help(help))            .collect())    })} pub fn untracked_bookmarks() -> Vec<CompletionCandidate> {    untracked_refs("bookmark", BOOKMARK_HELP_TEMPLATE)} pub fn untracked_tags() -> Vec<CompletionCandidate> {    untracked_refs("tag", TAG_HELP_TEMPLATE)} fn untracked_refs(kind: &str, help_template: &str) -> Vec<CompletionCandidate> {    with_jj(|jj, _settings| {        let remotes = jj            .build()            .arg("git")            .arg("remote")            .arg("list")            .output()            .map_err(user_error)?;        let remotes = String::from_utf8_lossy(&remotes.stdout);        let remotes = remotes            .lines()            .filter_map(|l| l.split_whitespace().next())            .collect_vec();         let ref_table = jj            .build()            .arg(kind)            .arg("list")            .arg("--all-remotes")            .arg("--config")            .arg(help_template)            .arg("--template")            .arg(format!(                r#"                if(remote != "git",                    if(!remote, name) ++ "\t" ++                    if(remote, name ++ "@" ++ remote) ++ "\t" ++                    if(tracked, "tracked") ++ "\t" ++                    {kind}_help() ++ "\n"                )"#,            ))            .output()            .map_err(user_error)?;        let ref_table = String::from_utf8_lossy(&ref_table.stdout);         let mut possible_refs_to_track = Vec::new();        let mut already_tracked_refs = HashSet::new();         for line in ref_table.lines() {            let [local, remote, tracked, help] =                line.split('\t').collect_array().unwrap_or_default();             if !local.is_empty() {                possible_refs_to_track.extend(                    remotes                        .iter()                        .map(|remote| (format!("{local}@{remote}"), help)),                );            } else if tracked.is_empty() {                possible_refs_to_track.push((remote.to_owned(), help));            } else {                already_tracked_refs.insert(remote);            }        }        possible_refs_to_track            .retain(|(symbol, _help)| !already_tracked_refs.contains(&symbol.as_str()));         Ok(possible_refs_to_track            .iter()            .filter_map(|(symbol, help)| Some((symbol.split_once('@')?, help)))            // There may be multiple remote bookmarks to track. Just pick the            // first one for help text.            .dedup_by(|((name1, _), _), ((name2, _), _)| name1 == name2)            .map(|((name, _remote), help)| {                CompletionCandidate::new(name).help(Some(help.to_string().into()))            })            .collect())    })} pub fn bookmarks() -> Vec<CompletionCandidate> {    refs("bookmark", BOOKMARK_HELP_TEMPLATE)} pub fn tags() -> Vec<CompletionCandidate> {    refs("tag", TAG_HELP_TEMPLATE)} fn refs(kind: &str, help_template: &str) -> Vec<CompletionCandidate> {    with_jj(|jj, _settings| {        let output = jj            .build()            .arg(kind)            .arg("list")            .arg("--all-remotes")            .arg("--config")            .arg(help_template)            .arg("--template")            .arg(format!(                // only provide help for local refs, remote could be ambiguous                r#"name ++ if(remote, "@" ++ remote, {kind}_help()) ++ "\n""#,            ))            .output()            .map_err(user_error)?;        let stdout = String::from_utf8_lossy(&output.stdout);         Ok((&stdout            .lines()            .map(split_help_text)            .chunk_by(|(name, _)| name.split_once('@').map(|t| t.0).unwrap_or(name)))            .into_iter()            .map(|(name, mut refs)| {                let help = refs.find_map(|(_, help)| help);                let local = help.is_some();                let display_order = match local {                    true => 0,                    false => 1,                };                CompletionCandidate::new(name)                    .help(help)                    .display_order(Some(display_order))            })            .collect())    })} pub fn local_tags() -> Vec<CompletionCandidate> {    with_jj(|jj, _| {        let output = jj            .build()            .arg("tag")            .arg("list")            .arg("--config")            .arg(TAG_HELP_TEMPLATE)            .arg("--template")            .arg(r#"if(!remote, name ++ tag_help()) ++ "\n""#)            .output()            .map_err(user_error)?;         Ok(String::from_utf8_lossy(&output.stdout)            .lines()            .map(split_help_text)            .map(|(name, help)| CompletionCandidate::new(name).help(help))            .collect())    })} pub fn git_remotes() -> Vec<CompletionCandidate> {    with_jj(|jj, _| {        let output = jj            .build()            .arg("git")            .arg("remote")            .arg("list")            .output()            .map_err(user_error)?;         let stdout = String::from_utf8_lossy(&output.stdout);         Ok(stdout            .lines()            .filter_map(|line| line.split_once(' ').map(|(name, _url)| name))            .map(CompletionCandidate::new)            .collect())    })} pub fn template_aliases() -> Vec<CompletionCandidate> {    with_jj(|_, settings| {        let Ok(template_aliases) = load_template_aliases(&Ui::null(), settings.config()) else {            return Ok(Vec::new());        };        Ok(template_aliases            .symbol_names()            .map(|name| {                let doc = template_aliases.get_symbol(name).unwrap().2;                CompletionCandidate::new(name).help(doc.map(|doc| doc.to_owned().into()))            })            .sorted()            .collect())    })} pub fn aliases() -> Vec<CompletionCandidate> {    with_jj(|_, settings| {        Ok(settings            .table_keys("aliases")            // This is opinionated, but many people probably have several            // single- or two-letter aliases they use all the time. These            // aliases don't need to be completed and they would only clutter            // the output of `jj <TAB>`.            .filter(|alias| alias.len() > 2)            .filter(|alias| {                settings                    .get_bool(["aliases", alias, "enabled"])                    .unwrap_or(true)            })            .map(|alias| {                CompletionCandidate::new(alias).help(                    settings                        .get_string(["aliases", alias, "doc"])                        .ok()                        .map(|doc| doc.into()),                )            })            .collect())    })} fn revisions(match_prefix: &str, revset_filter: Option<&str>) -> Vec<CompletionCandidate> {    with_jj(|jj, settings| {        // display order        const LOCAL_BOOKMARK: usize = 0;        const TAG: usize = 1;        const CHANGE_ID: usize = 2;        const WORKSPACE: usize = 3;        const REMOTE_BOOKMARK: usize = 4;        const REVSET_ALIAS: usize = 5;         let mut candidates = Vec::new();         // bookmarks         let mut cmd = jj.build();        cmd.arg("bookmark")            .arg("list")            .arg("--all-remotes")            .arg("--config")            .arg(BOOKMARK_HELP_TEMPLATE)            .arg("--template")            .arg(                r#"if(remote != "git", name ++ if(remote, "@" ++ remote) ++ bookmark_help() ++ "\n")"#,            );        if let Some(revs) = revset_filter {            cmd.arg("--revisions").arg(revs);        }        let output = cmd.output().map_err(user_error)?;        let stdout = String::from_utf8_lossy(&output.stdout);         candidates.extend(            stdout                .lines()                .map(split_help_text)                .filter(|(bookmark, _)| bookmark.starts_with(match_prefix))                .map(|(bookmark, help)| {                    let local = !bookmark.contains('@');                    let display_order = match local {                        true => LOCAL_BOOKMARK,                        false => REMOTE_BOOKMARK,                    };                    CompletionCandidate::new(bookmark)                        .help(help)                        .display_order(Some(display_order))                }),        );         // tags         // Tags cannot be filtered by revisions. In order to avoid suggesting        // immutable tags for mutable revision args, we skip tags entirely if        // revset_filter is set. This is not a big loss, since tags usually point        // to immutable revisions anyway.        if revset_filter.is_none() {            let output = jj                .build()                .arg("tag")                .arg("list")                .arg("--config")                .arg(BOOKMARK_HELP_TEMPLATE)                .arg("--template")                .arg(r#"name ++ bookmark_help() ++ "\n""#)                .arg(format!("glob:{}*", globset::escape(match_prefix)))                .output()                .map_err(user_error)?;            let stdout = String::from_utf8_lossy(&output.stdout);             candidates.extend(stdout.lines().map(|line| {                let (name, desc) = split_help_text(line);                CompletionCandidate::new(name)                    .help(desc)                    .display_order(Some(TAG))            }));        }         // workspace names         let output = jj            .build()            .arg("workspace")            .arg("list")            .arg("--template")            .arg(r#"name ++ "\n""#)            .output()            .map_err(user_error)?;        let stdout = String::from_utf8_lossy(&output.stdout);         // If there's only one workspace, the user can just use `@`, and they may even        // be confused if they see `default@` as an option.        if stdout.lines().count() > 1 {            candidates.extend(stdout.lines().filter_map(|name| {                let symbol = format!("{name}@");                if symbol.starts_with(match_prefix) {                    Some(                        CompletionCandidate::new(symbol)                            .help(Some(                                format!("The working copy for workspace `{name}`").into(),                            ))                            .display_order(Some(WORKSPACE)),                    )                } else {                    None                }            }));        }         // change IDs         let revisions = revset_filter            .map(String::from)            .or_else(|| settings.get_string("revsets.short-prefixes").ok())            .or_else(|| settings.get_string("revsets.log").ok())            .unwrap_or_default();         let output = jj            .build()            .arg("log")            .arg("--no-graph")            .arg("--limit")            .arg("100")            .arg("--revisions")            .arg(revisions)            .arg("--template")            .arg(                r#"                join(" ",                    separate("/",                        change_id.shortest(),                        if(hidden || divergent, change_offset),                    ),                    if(description, description.first_line(), "(no description set)"),                ) ++ "\n""#,            )            .output()            .map_err(user_error)?;        let stdout = String::from_utf8_lossy(&output.stdout);         candidates.extend(            stdout                .lines()                .map(split_help_text)                .filter(|(id, _)| id.starts_with(match_prefix))                .map(|(id, desc)| {                    CompletionCandidate::new(id)                        .help(desc)                        .display_order(Some(CHANGE_ID))                }),        );         // revset aliases         let revset_aliases = load_revset_aliases(&Ui::null(), settings.config())?;        let symbol_names = revset_aliases            .symbol_names()            .sorted_unstable()            .collect_vec();        candidates.extend(            symbol_names                .into_iter()                .filter(|symbol| symbol.starts_with(match_prefix))                .map(|symbol| {

Lines 1–500 of 1639.