1492 lines · 54.8 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::collections::HashMap;use std::collections::HashSet;use std::future;use std::io;use std::io::Write as _;use std::iter;use std::sync::Arc; use clap::ArgGroup;use clap_complete::ArgValueCandidates;use clap_complete::ArgValueCompleter;use futures::StreamExt as _;use futures::TryStreamExt as _;use futures::future::try_join_all;use indexmap::IndexSet;use itertools::Itertools as _;use jj_lib::backend::CommitId;use jj_lib::commit::Commit;use jj_lib::config::ConfigGetResultExt as _;use jj_lib::git;use jj_lib::git::GitPushOptions;use jj_lib::git::GitPushRefTargets;use jj_lib::git::GitSettings;use jj_lib::index::IndexResult;use jj_lib::merge::Diff;use jj_lib::op_store::RefTarget;use jj_lib::operation::Operation;use jj_lib::ref_name::RefName;use jj_lib::ref_name::RefNameBuf;use jj_lib::ref_name::RemoteName;use jj_lib::ref_name::RemoteRefSymbol;use jj_lib::refs::LocalAndRemoteRef;use jj_lib::refs::RefPushAction;use jj_lib::refs::classify_ref_push_action;use jj_lib::repo::Repo;use jj_lib::revset::RemoteRefSymbolExpression;use jj_lib::revset::ResolvedRevsetExpression;use jj_lib::revset::RevsetContainingFn;use jj_lib::revset::RevsetEvaluationError;use jj_lib::revset::RevsetExpression;use jj_lib::revset::RevsetStreamExt as _;use jj_lib::revset::UserRevsetExpression;use jj_lib::rewrite::CommitRewriter;use jj_lib::signing::SignBehavior;use jj_lib::str_util::StringExpression;use jj_lib::view::View; use crate::cli_util::CommandHelper;use crate::cli_util::RevisionArg;use crate::cli_util::WorkspaceCommandHelper;use crate::cli_util::WorkspaceCommandTransaction;use crate::cli_util::has_tracked_remote_bookmarks;use crate::cli_util::has_tracked_remote_tags;use crate::cli_util::print_updated_commits;use crate::cli_util::short_change_hash;use crate::cli_util::short_commit_hash;use crate::command_error::CommandError;use crate::command_error::cli_error;use crate::command_error::cli_error_with_message;use crate::command_error::user_error;use crate::command_error::user_error_with_message;use crate::commands::git::get_single_remote;use crate::complete;use crate::formatter::Formatter;use crate::git_util::GitSubprocessUi;use crate::git_util::print_push_stats;use crate::progress::ProgressWriter;use crate::revset_util::parse_bookmark_name;use crate::revset_util::parse_union_name_patterns;use crate::ui::Ui; /// Push to a Git remote////// By default, pushes tracking bookmarks and tags pointing to/// `remote_bookmarks(remote=<remote>)..@`. Use `--bookmark`/`--tag` to push/// specific bookmarks or tags. Use `--all` to push all bookmarks and tags. Use/// `--change` to generate bookmark names based on the change IDs of specific/// commits.////// When pushing a bookmark or tag, the command pushes all commits in the range/// from the remote's current position up to and including its target/// commit. Any descendant commits beyond the reference are not pushed.////// If the local reference has changed from the last fetch, push will update the/// remote reference to the new position after passing safety checks. This is/// similar to `git push --force-with-lease` - the remote is updated only if its/// current state matches what Jujutsu last fetched.////// Unlike in Git, the remote to push to is not derived from the tracked remote/// bookmarks. Use `--remote` to select the remote Git repository by name. There/// is no option to push to multiple remotes.////// Before the command actually moves, creates, or deletes a remote bookmark, it/// makes several [safety checks]. If there is a problem, you may need to run/// `jj git fetch --remote <remote name>` and/or resolve some [bookmark/// conflicts].////// [safety checks]:///     https://docs.jj-vcs.dev/latest/bookmarks/#pushing-bookmarks-safety-checks////// [bookmark conflicts]:///     https://docs.jj-vcs.dev/latest/bookmarks/#conflicts #[derive(clap::Args, Clone, Debug)]#[command(group(ArgGroup::new("specific").multiple(true)))]#[command(group(ArgGroup::new("what").conflicts_with("specific")))]pub struct GitPushArgs {    /// The remote(s) to push to (can be repeated)    ///    /// This defaults to the `git.push` 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>>,     /// Push only this bookmark, or bookmarks matching a pattern (can be    /// repeated)    ///    /// If a bookmark isn't tracking anything yet, the remote bookmark will be    /// tracked automatically.    ///    /// By default, the specified pattern matches bookmark names with glob    /// syntax. You can also use other [string pattern syntax].    ///    /// [string pattern syntax]:    ///     https://docs.jj-vcs.dev/latest/revsets/#string-patterns    #[arg(long, short, alias = "branch", group = "specific")]    #[arg(add = ArgValueCandidates::new(complete::local_bookmarks))]    bookmark: Vec<String>,     /// Push only this tag, or tags matching a pattern (can be repeated)    ///    /// If a tag isn't tracking anything yet, the remote tag will be tracked    /// automatically.    ///    /// By default, the specified pattern matches tag names with glob syntax.    /// You can also use other [string pattern syntax].    ///    /// [string pattern syntax]:    ///     https://docs.jj-vcs.dev/latest/revsets/#string-patterns    #[arg(long, short, group = "specific")]    tag: Vec<String>,     /// Push all bookmarks and tags (including new ones)    #[arg(long, group = "what")]    all: bool,     /// Push all tracked bookmarks and tags    ///    /// This usually means that the bookmark or tag was already pushed to or    /// fetched from the [relevant remote].    ///    /// [relevant remote]:    ///     https://docs.jj-vcs.dev/latest/bookmarks#remotes-and-tracked-bookmarks    #[arg(long, group = "what")]    tracked: bool,     /// Push all deleted bookmarks and tags    ///    /// Only tracked bookmarks and tags can be successfully deleted on the    /// remote. A warning will be printed if any untracked bookmarks or tags on    /// the remote correspond to missing local bookmarks or tags.    #[arg(long, conflicts_with = "specific")]    deleted: bool,     /// Allow pushing commits with empty descriptions    #[arg(long)]    allow_empty_description: bool,     /// Allow pushing commits that are private    ///    /// The set of private commits can be configured by the    /// `git.private-commits` setting. The default is `none()`, meaning all    /// commits are eligible to be pushed.    #[arg(long)]    allow_private: bool,     /// Allow pushing commits that contain conflicts    #[arg(long)]    allow_conflicts: bool,     /// Push bookmarks and tags pointing to these commits (can be repeated)    #[arg(        long = "revision",        short,        group = "specific",        value_name = "REVSETS",        alias = "revisions"    )]    // While `-r` will often be used with mutable revisions, immutable revisions    // can be useful as parts of revsets or to push special-purpose branches.    #[arg(add = ArgValueCompleter::new(complete::revset_expression_all))]    revisions: Vec<RevisionArg>,     /// Push this commit by creating a bookmark (can be repeated)    ///    /// The created bookmark will be tracked automatically. Use the    /// `templates.git_push_bookmark` setting to customize the generated    /// bookmark name. The default is `"push-" ++ change_id.short()`.    #[arg(long, short, group = "specific", value_name = "REVSETS")]    // I'm guessing that `git push -c` is almost exclusively used with recently    // created mutable revisions, even though it can in theory be used with    // immutable ones as well. We can change it if the guess turns out to be    // wrong.    #[arg(add = ArgValueCompleter::new(complete::revset_expression_mutable))]    change: Vec<RevisionArg>,     /// Specify a new bookmark name and a revision to push under that name, e.g.    /// '--named myfeature=@'    ///    /// Automatically tracks the bookmark if it is new.    #[arg(long, group = "specific", value_name = "NAME=REVISION")]    #[arg(add = ArgValueCompleter::new(complete::branch_name_equals_any_revision))]    named: Vec<String>,     /// Only display what will change on the remote    #[arg(long)]    dry_run: bool,     /// Git push options    #[arg(long, short)]    option: Vec<String>,} fn make_updates_term<'a>(ref_updates: impl IntoIterator<Item = &'a GitPushRefTargets>) -> String {    let mut bookmarks = IndexSet::new();    let mut tags = IndexSet::new();     for updates in ref_updates {        bookmarks.extend(updates.bookmarks.iter().map(|(name, _)| name));        tags.extend(updates.tags.iter().map(|(name, _)| name));    }     let kind_updates = [("bookmark", "bookmarks", bookmarks), ("tag", "tags", tags)];    kind_updates        .into_iter()        .filter_map(|(kind, kinds, refs)| match refs.len() {            0 => None,            1 => Some(format!("{kind} {}", refs[0].as_symbol())),            _ => Some(format!(                "{kinds} {}",                refs.iter().map(|name| name.as_symbol()).join(", ")            )),        })        .join(", ")} const DEFAULT_REMOTE: &RemoteName = RemoteName::new("origin"); const TX_DESC_PUSH: &str = "push "; #[derive(Clone, Copy, Debug, Eq, PartialEq)]enum BookmarkMoveDirection {    Forward,    Backward,    Sideways,} pub async fn cmd_git_push(    ui: &mut Ui,    command: &CommandHelper,    args: &GitPushArgs,) -> Result<(), CommandError> {    if command.global_args().no_integrate_operation {        // Pushed refs shouldn't be left unintegrated.        return Err(cli_error(            "--no-integrate-operation is not respected; use --dry-run",        ));    }    let mut workspace_command = command.workspace_helper(ui).await?;     let remote_expr = if let Some(remotes) = &args.remotes {        parse_union_name_patterns(ui, remotes)?    } else {        get_default_push_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 = all_remotes        .iter()        .filter(|r| remote_matcher.is_match(r.as_str()))        .map(AsRef::as_ref)        .collect_vec();    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 push to"));    }     let mut tx = workspace_command.start_transaction();    let mut by_remote = Vec::with_capacity(matching_remotes.len());     if args.all {        let workspace_helper = tx.base_workspace_helper();         let params = ClassifyParams {            allow_new: true, // implied by --all            allow_delete: args.deleted,        };         for remote in matching_remotes {            let mut commits_validator = CommitsValidator::new(ui, workspace_helper, remote, args)?;             let ref_updates = classify_tags_and_bookmark_updates(                ui,                workspace_helper,                &mut commits_validator,                remote,                |_targets| true,                params,            )            .await?;             by_remote.push((remote, ref_updates));        }    } else if args.tracked {        let workspace_helper = tx.base_workspace_helper();         let params = ClassifyParams {            allow_new: true, // doesn't matter            allow_delete: args.deleted,        };         for remote in matching_remotes {            let mut commits_validator = CommitsValidator::new(ui, workspace_helper, remote, args)?;             let ref_updates = classify_tags_and_bookmark_updates(                ui,                workspace_helper,                &mut commits_validator,                remote,                |targets| targets.remote_ref.is_tracked(),                params,            )            .await?;             by_remote.push((remote, ref_updates));        }    } else if args.deleted {        let workspace_helper = tx.base_workspace_helper();         // There shouldn't be new heads to push, but we run validation for consistency.        let params = ClassifyParams {            allow_new: true, // doesn't matter            allow_delete: true,        };         for remote in matching_remotes {            let mut commits_validator = CommitsValidator::new(ui, workspace_helper, remote, args)?;             let ref_updates = classify_tags_and_bookmark_updates(                ui,                workspace_helper,                &mut commits_validator,                remote,                |targets| targets.local_target.is_absent(),                params,            )            .await?;             by_remote.push((remote, ref_updates));        }    } else {        // --change and --named don't move existing bookmarks. If they did, be        // careful to not select old state by -r/--revisions and bookmark names.        let change_bookmark_names = create_change_bookmarks(ui, &mut tx, &args.change).await?;        let named_bookmark_commits = try_join_all(args.named.iter().map(|arg| async {            let (name, revision_arg) = parse_named_bookmark(arg)?;            let commit = tx                .base_workspace_helper()                .resolve_single_rev(ui, &revision_arg)                .await?;            Ok::<_, CommandError>((name, commit))        }))        .await?;        for (name, commit) in &named_bookmark_commits {            ensure_new_bookmark_name(tx.repo(), name)?;            tx.repo_mut()                .set_local_bookmark_target(name, RefTarget::normal(commit.id().clone()));        }        for remote in matching_remotes {            let mut seen_bookmarks: HashSet<&RefName> = HashSet::new();            let mut seen_tags: HashSet<&RefName> = HashSet::new();            let mut ref_updates = GitPushRefTargets::default();            let created_bookmarks = change_bookmark_names                .iter()                .chain(named_bookmark_commits.iter().map(|(name, _)| name))                .map(|name| {                    let remote_symbol = name.to_remote_symbol(remote);                    let targets = LocalAndRemoteRef {                        local_target: tx.repo().view().get_local_bookmark(name),                        remote_ref: tx.repo().view().get_remote_bookmark(remote_symbol),                    };                    (remote_symbol, targets)                });            for (remote_symbol, targets) in created_bookmarks {                let name = remote_symbol.name;                if !seen_bookmarks.insert(name) {                    continue;                }                let params = ClassifyParams {                    allow_new: true,     // --change implies creation of remote bookmark                    allow_delete: false, // doesn't matter                };                match classify_bookmark_update(remote_symbol, targets, params) {                    Ok(Some(update)) => ref_updates.bookmarks.push((name.to_owned(), update)),                    Ok(None) => writeln!(                        ui.status(),                        "Bookmark {remote_symbol} already matches {name}",                        name = name.as_symbol()                    )?,                    Err(reason) => return Err(reason.into()),                }            }             let view = tx.repo().view();            let bookmarks_by_name = find_bookmarks_to_push(ui, view, &args.bookmark, remote)?;            for &(name, targets) in &bookmarks_by_name {                if !seen_bookmarks.insert(name) {                    continue;                }                let remote_symbol = name.to_remote_symbol(remote);                let params = ClassifyParams {                    // Override allow_new if the bookmark is not tracked with any remote                    // already. The user has specified --bookmark, so their intent which                    // bookmarks to push is clear.                    allow_new: !has_tracked_remote_bookmarks(tx.repo(), name),                    allow_delete: true, // named explicitly, allow delete without --delete                };                match classify_bookmark_update(remote_symbol, targets, params) {                    Ok(Some(update)) => ref_updates.bookmarks.push((name.to_owned(), update)),                    Ok(None) => writeln!(                        ui.status(),                        "Bookmark {remote_symbol} already matches {name}",                        name = name.as_symbol()                    )?,                    Err(reason) => return Err(reason.into()),                }            }             let tags_by_name = find_tags_to_push(ui, view, &args.tag, remote)?;            for &(name, targets) in &tags_by_name {                if !seen_tags.insert(name) {                    continue;                }                let remote_symbol = name.to_remote_symbol(remote);                let params = ClassifyParams {                    // named explicitly, allow track or delete                    allow_new: !has_tracked_remote_tags(tx.repo(), name),                    allow_delete: true,                };                match classify_tag_update(remote_symbol, targets, params) {                    Ok(Some(update)) => ref_updates.tags.push((name.to_owned(), update)),                    Ok(None) => writeln!(                        ui.status(),                        "Tag {remote_symbol} already matches {name}",                        name = name.as_symbol()                    )?,                    Err(reason) => return Err(reason.into()),                }            }             let mut commits_validator =                CommitsValidator::new(ui, tx.base_workspace_helper(), remote, args)?;            // Error out if explicitly-specified targets can't be pushed.            commits_validator                .validate_updates(&ref_updates)                .await?

Lines 1–500 of 1492.