1560 lines · 53.7 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::file_util::normalize_path;use jj_lib::file_util::slash_path;use jj_lib::settings::UserSettings;use jj_lib::workspace::DefaultWorkspaceLoaderFactory;use jj_lib::workspace::WorkspaceLoaderFactory as _; 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> {    with_jj(|jj, _| {        let output = jj            .build()            .arg("bookmark")            .arg("list")            .arg("--tracked")            .arg("--config")            .arg(BOOKMARK_HELP_TEMPLATE)            .arg("--template")            .arg(r#"if(remote, name ++ '@' ++ remote ++ bookmark_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> {    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 bookmark_table = jj            .build()            .arg("bookmark")            .arg("list")            .arg("--all-remotes")            .arg("--config")            .arg(BOOKMARK_HELP_TEMPLATE)            .arg("--template")            .arg(                r#"                if(remote != "git",                    if(!remote, name) ++ "\t" ++                    if(remote, name ++ "@" ++ remote) ++ "\t" ++                    if(tracked, "tracked") ++ "\t" ++                    bookmark_help() ++ "\n"                )"#,            )            .output()            .map_err(user_error)?;        let bookmark_table = String::from_utf8_lossy(&bookmark_table.stdout);         let mut possible_bookmarks_to_track = Vec::new();        let mut already_tracked_bookmarks = HashSet::new();         for line in bookmark_table.lines() {            let [local, remote, tracked, help] =                line.split('\t').collect_array().unwrap_or_default();             if !local.is_empty() {                possible_bookmarks_to_track.extend(                    remotes                        .iter()                        .map(|remote| (format!("{local}@{remote}"), help)),                );            } else if tracked.is_empty() {                possible_bookmarks_to_track.push((remote.to_owned(), help));            } else {                already_tracked_bookmarks.insert(remote);            }        }        possible_bookmarks_to_track            .retain(|(bookmark, _help)| !already_tracked_bookmarks.contains(&bookmark.as_str()));         Ok(possible_bookmarks_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> {    with_jj(|jj, _settings| {        let output = jj            .build()            .arg("bookmark")            .arg("list")            .arg("--all-remotes")            .arg("--config")            .arg(BOOKMARK_HELP_TEMPLATE)            .arg("--template")            .arg(                // only provide help for local refs, remote could be ambiguous                r#"name ++ if(remote, "@" ++ remote, bookmark_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(|(bookmark, 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(bookmark)                    .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)            .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| {                    let (_, defn, doc) = revset_aliases.get_symbol(symbol).unwrap();                    // Prefer TOML `.doc` over definition text                    let help: String = doc.map(|s| s.to_owned()).unwrap_or_else(|| defn.clone());                    CompletionCandidate::new(symbol)                        .help(Some(help.into()))                        .display_order(Some(REVSET_ALIAS))                }),        );         Ok(candidates)    })} fn revset_expression(    current: &std::ffi::OsStr,    revset_filter: Option<&str>,) -> Vec<CompletionCandidate> {    let Some(current) = current.to_str() else {        return Vec::new();    };    let (prepend, match_prefix) = split_revset_trailing_name(current).unwrap_or(("", current));    let candidates = revisions(match_prefix, revset_filter);    if prepend.is_empty() {        candidates    } else {        candidates            .into_iter()            .map(|candidate| candidate.add_prefix(prepend))            .collect()    }

Lines 1–500 of 1560.