1419 lines · 48.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::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_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::revset_util::load_revset_aliases;use crate::ui::Ui; const BOOKMARK_HELP_TEMPLATE: &str = r#"template-aliases.'bookmark_help()'='''" " ++if(normal_target,    if(normal_target.description(),        normal_target.description().first_line(),        "(no description set)",    ),    "(conflicted bookmark)",)'''"#;const TAG_HELP_TEMPLATE: &str = r#"template-aliases.'tag_help()'='''" " ++if(normal_target,    if(normal_target.description(),        normal_target.description().first_line(),        "(no description set)",    ),    "(conflicted tag)",)'''"#; /// 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)            .map(|(name, 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            .into_iter()            .map(|(bookmark, help)| {                CompletionCandidate::new(bookmark).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(CompletionCandidate::new)            .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(CompletionCandidate::new)            .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 REMOTE_BOOKMARK: usize = 3;        const REVSET_ALIAS: usize = 4;         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))            }));        }         // 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#"change_id.shortest() ++ " " ++ 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 mut symbol_names: Vec<_> = revset_aliases.symbol_names().collect();        symbol_names.sort();        candidates.extend(            symbol_names                .into_iter()                .filter(|symbol| symbol.starts_with(match_prefix))                .map(|symbol| {                    let (_, defn) = revset_aliases.get_symbol(symbol).unwrap();                    CompletionCandidate::new(symbol)                        .help(Some(defn.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()    }} pub fn revset_expression_all(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {    revset_expression(current, None)} pub fn revset_expression_mutable(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {    revset_expression(current, Some("mutable()"))} pub fn revset_expression_mutable_conflicts(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {    revset_expression(current, Some("mutable() & conflicts()"))} /// Identifies if an incomplete expression ends with a name, or may be continued/// with a name.////// If the expression ends with an name or a partial name, returns a tuple that/// splits the string at the point the name starts./// If the expression is empty or ends with a prefix or infix operator that/// could plausibly be followed by a name, returns a tuple where the first/// item is the entire input string, and the second item is empty./// Otherwise, returns `None`.////// The input expression may be incomplete (e.g. missing closing parentheses),/// and the ability to reject invalid expressions is limited.fn split_revset_trailing_name(incomplete_revset_str: &str) -> Option<(&str, &str)> {    let final_part = incomplete_revset_str        .rsplit_once([':', '~', '|', '&', '(', ','])        .map(|(_, rest)| rest)        .unwrap_or(incomplete_revset_str);    let final_part = final_part        .rsplit_once("..")        .map(|(_, rest)| rest)        .unwrap_or(final_part)        .trim_ascii_start();     let re = regex::Regex::new(r"^(?:[\p{XID_CONTINUE}_/]+[@.+-])*[\p{XID_CONTINUE}_/]*$").unwrap();    re.is_match(final_part)        .then(|| incomplete_revset_str.split_at(incomplete_revset_str.len() - final_part.len()))} pub fn operations() -> Vec<CompletionCandidate> {    with_jj(|jj, _| {        let output = jj            .build()            .arg("operation")            .arg("log")            .arg("--no-graph")            .arg("--limit")            .arg("100")            .arg("--template")            .arg(                r#"                separate(" ",                    id.short(),                    "(" ++ format_timestamp(time.end()) ++ ")",                    description.first_line(),                ) ++ "\n""#,            )            .output()            .map_err(user_error)?; 

Lines 1–500 of 1419.