Martin von Zweigbergkcli: make all command functions async2bbf14d8 months agoHistory
320 lines · 11.9 KB · Rust
RawDownload
// Copyright 2020-2023 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::io; use clap_complete::ArgValueCandidates;use itertools::Itertools as _;use jj_lib::config::ConfigGetResultExt as _;use jj_lib::git;use jj_lib::git::FetchTagsOverride;use jj_lib::git::GitFetch;use jj_lib::git::GitFetchRefExpression;use jj_lib::git::GitSettings;use jj_lib::git::IgnoredRefspec;use jj_lib::git::IgnoredRefspecs;use jj_lib::git::expand_fetch_refspecs;use jj_lib::git::get_git_backend;use jj_lib::git::load_default_fetch_bookmarks;use jj_lib::ref_name::RefName;use jj_lib::ref_name::RemoteName;use jj_lib::repo::Repo as _;use jj_lib::str_util::StringExpression; use crate::cli_util::CommandHelper;use crate::cli_util::WorkspaceCommandHelper;use crate::cli_util::WorkspaceCommandTransaction;use crate::command_error::CommandError;use crate::command_error::user_error;use crate::commands::git::get_single_remote;use crate::complete;use crate::git_util::GitSubprocessUi;use crate::git_util::load_git_import_options;use crate::git_util::print_git_import_stats;use crate::revset_util::parse_union_name_patterns;use crate::ui::Ui; /// Fetch from a Git remote////// If no branches nor tags are specified, the default fetch refspecs are read/// from the Git configuration.////// If a working-copy commit gets abandoned, it will be given a new, empty/// commit. This is true in general; it is not specific to this command.#[derive(clap::Args, Clone, Debug)]#[command(group(clap::ArgGroup::new("specific").multiple(true)))]pub struct GitFetchArgs {    /// Name of the branch to fetch (can be repeated)    ///    /// By default, the specified pattern matches branch names with glob syntax,    /// but only `*` is expanded. Other wildcard characters such as `?` are    /// *not* supported. Patterns can be repeated or combined with [logical    /// operators] to specify multiple branches, but only union and negative    /// intersection are supported.    ///    /// Examples: `push-*`, `(push-* | foo/*) ~ foo/unwanted`    ///    /// [logical operators]:    ///     https://docs.jj-vcs.dev/latest/revsets/#string-patterns    #[arg(        long = "branch",        short,        alias = "bookmark",        group = "specific",        value_name = "BRANCH"    )]    #[arg(add = ArgValueCandidates::new(complete::bookmarks))]    branches: Option<Vec<String>>,     /// Fetch only some of the tags (can be repeated)    ///    /// By default, the specified pattern matches tag names with glob syntax,    /// but only `*` is expanded. Other wildcard characters such as `?` are    /// *not* supported. Patterns can be repeated or combined with [logical    /// operators] to specify multiple tags, but only union and negative    /// intersection are supported.    ///    /// [logical operators]:    ///     https://docs.jj-vcs.dev/latest/revsets/#string-patterns    #[arg(long = "tag", short, group = "specific", value_name = "TAG")]    #[arg(hide = true)] // TODO: unhide when this gets stabilized (#7528)    tags: Option<Vec<String>>,     /// Fetch only tracked bookmarks    ///    /// This fetches only bookmarks that are already tracked from the specified    /// remote(s).    #[arg(long, conflicts_with = "specific")]    tracked: bool,     /// The remote to fetch from (only named remotes are supported, can be    /// repeated)    ///    /// This defaults to the `git.fetch` setting. If that is not configured, and    /// if there are multiple remotes, the remote named "origin" will be used.    ///    /// By default, the specified pattern matches remote names with glob syntax,    /// e.g. `--remote '*'`. You can also use other [string pattern syntax].    ///    /// [string pattern syntax]:    ///     https://docs.jj-vcs.dev/latest/revsets/#string-patterns    #[arg(long = "remote", value_name = "REMOTE")]    #[arg(add = ArgValueCandidates::new(complete::git_remotes))]    remotes: Option<Vec<String>>,     /// Fetch from all remotes    #[arg(long, conflicts_with = "remotes")]    all_remotes: bool,} #[tracing::instrument(skip_all)]pub async fn cmd_git_fetch(    ui: &mut Ui,    command: &CommandHelper,    args: &GitFetchArgs,) -> Result<(), CommandError> {    let mut workspace_command = command.workspace_helper(ui)?;    let remote_expr = if args.all_remotes {        StringExpression::all()    } else if let Some(remotes) = &args.remotes {        parse_union_name_patterns(ui, remotes)?    } else {        get_default_fetch_remotes(ui, &workspace_command)?    };    let remote_matcher = remote_expr.to_matcher();     let all_remotes = git::get_all_remote_names(workspace_command.repo().store())?;    let matching_remotes: Vec<&RemoteName> = all_remotes        .iter()        .filter(|r| remote_matcher.is_match(r.as_str()))        .map(AsRef::as_ref)        .collect();    let mut unmatched_remotes = remote_expr        .exact_strings()        .map(RemoteName::new)        // do linear search. all_remotes should be small.        .filter(|&name| all_remotes.iter().all(|r| r != name))        .peekable();    if unmatched_remotes.peek().is_some() {        writeln!(            ui.warning_default(),            "No matching remotes for names: {}",            unmatched_remotes.map(|name| name.as_symbol()).join(", ")        )?;    }    if matching_remotes.is_empty() {        return Err(user_error("No git remotes to fetch from"));    }     let mut tx = workspace_command.start_transaction();     let is_specific = args.branches.is_some() || args.tags.is_some();    let common_bookmark_expr = match &args.branches {        Some(texts) => Some(parse_union_name_patterns(ui, texts)?),        None => is_specific.then(StringExpression::none),    };    let common_tag_expr = match &args.tags {        Some(texts) => Some(parse_union_name_patterns(ui, texts)?),        None => is_specific.then(StringExpression::none),    };    let mut expansions = Vec::with_capacity(matching_remotes.len());    if args.tracked {        for remote in &matching_remotes {            let bookmark = StringExpression::union_all(                tx.repo()                    .view()                    .local_remote_bookmarks(remote)                    .filter(|(_, targets)| targets.remote_ref.is_tracked())                    .map(|(name, _)| StringExpression::exact(name))                    .collect(),            );            let tag = StringExpression::union_all(                tx.repo()                    .view()                    .local_remote_tags(remote)                    .filter(|(_, targets)| targets.remote_ref.is_tracked())                    .map(|(name, _)| StringExpression::exact(name))                    .collect(),            );            let ref_expr = GitFetchRefExpression { bookmark, tag };            expansions.push((remote, expand_fetch_refspecs(remote, ref_expr)?));        }    } else {        let git_repo = get_git_backend(tx.repo_mut().store())?.git_repo();        for remote in &matching_remotes {            // TODO: add native config for default bookmark/tag patterns? (#7819)            let bookmark = if let Some(expr) = &common_bookmark_expr {                expr.clone()            } else {                let (ignored, expr) = load_default_fetch_bookmarks(remote, &git_repo)?;                warn_ignored_refspecs(ui, remote, ignored)?;                expr            };            let tag = common_tag_expr                .clone()                // TODO: disable implicit fetching and set this to "all" (#7528)                .unwrap_or_else(StringExpression::none);            let ref_expr = GitFetchRefExpression { bookmark, tag };            let expanded = expand_fetch_refspecs(remote, ref_expr)?;            expansions.push((remote, expanded));        }    }     let git_settings = GitSettings::from_settings(tx.settings())?;    let remote_settings = tx.settings().remote_settings()?;    let import_options = load_git_import_options(ui, &git_settings, &remote_settings)?;    let mut git_fetch = GitFetch::new(        tx.repo_mut(),        git_settings.to_subprocess_options(),        &import_options,    )?;    // Disable implicit tag fetching if patterns are explicitly set. NoTags will    // be the default when this feature gets stabilized. (#7528)    let fetch_tags = (args.tags.is_some() || args.tracked).then_some(FetchTagsOverride::NoTags);     for (remote, expanded) in expansions {        let mut callback = GitSubprocessUi::new(ui);        git_fetch.fetch(remote, expanded, &mut callback, None, fetch_tags)?;    }     let import_stats = git_fetch.import_refs()?;    print_git_import_stats(ui, &tx, &import_stats)?;     if let Some(bookmark_expr) = &common_bookmark_expr {        warn_if_branches_not_found(ui, &tx, bookmark_expr, &matching_remotes)?;    }    // TODO: warn_if_tags_not_found()    tx.finish(        ui,        format!(            "fetch from git remote(s) {}",            matching_remotes.iter().map(|n| n.as_symbol()).join(",")        ),    )?;    Ok(())} const DEFAULT_REMOTE: &RemoteName = RemoteName::new("origin"); fn get_default_fetch_remotes(    ui: &Ui,    workspace_command: &WorkspaceCommandHelper,) -> Result<StringExpression, CommandError> {    const KEY: &str = "git.fetch";    let settings = workspace_command.settings();    if let Ok(remotes) = settings.get::<Vec<String>>(KEY) {        parse_union_name_patterns(ui, &remotes)    } else if let Some(remote) = settings.get_string(KEY).optional()? {        parse_union_name_patterns(ui, [&remote])    } else if let Some(remote) = get_single_remote(workspace_command.repo().store())? {        // if nothing was explicitly configured, try to guess        if remote != DEFAULT_REMOTE {            writeln!(                ui.hint_default(),                "Fetching from the only existing remote: {remote}",                remote = remote.as_symbol()            )?;        }        Ok(StringExpression::exact(remote))    } else {        Ok(StringExpression::exact(DEFAULT_REMOTE))    }} fn warn_if_branches_not_found(    ui: &mut Ui,    tx: &WorkspaceCommandTransaction,    bookmark_expr: &StringExpression,    remotes: &[&RemoteName],) -> io::Result<()> {    let bookmark_matcher = bookmark_expr.to_matcher();    let mut missing_branches = bookmark_expr        .exact_strings()        .filter(|name| bookmark_matcher.is_match(name)) // exclude negative patterns        .map(RefName::new)        .filter(|name| {            remotes.iter().all(|&remote| {                let symbol = name.to_remote_symbol(remote);                let view = tx.repo().view();                let base_view = tx.base_repo().view();                view.get_remote_bookmark(symbol).is_absent()                    && base_view.get_remote_bookmark(symbol).is_absent()            })        })        .peekable();    if missing_branches.peek().is_none() {        return Ok(());    }    writeln!(        ui.warning_default(),        "No matching branches found on any specified/configured remote: {}",        missing_branches.map(|name| name.as_symbol()).join(", ")    )} fn warn_ignored_refspecs(    ui: &Ui,    remote_name: &RemoteName,    IgnoredRefspecs(ignored_refspecs): IgnoredRefspecs,) -> Result<(), CommandError> {    let remote_name = remote_name.as_symbol();    for IgnoredRefspec { refspec, reason } in ignored_refspecs {        writeln!(            ui.warning_default(),            "Ignored refspec `{refspec}` from `{remote_name}`: {reason}",        )?;    }     Ok(())}