1018 lines · 38.1 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::fmt;use std::io;use std::io::Write as _;use std::iter; use clap::ArgGroup;use clap_complete::ArgValueCandidates;use clap_complete::ArgValueCompleter;use indexmap::IndexSet;use itertools::Itertools as _;use jj_lib::backend::CommitId;use jj_lib::commit::Commit;use jj_lib::commit::CommitIteratorExt as _;use jj_lib::config::ConfigGetResultExt as _;use jj_lib::git;use jj_lib::git::GitBranchPushTargets;use jj_lib::git::GitPushStats;use jj_lib::op_store::RefTarget;use jj_lib::ref_name::RefName;use jj_lib::ref_name::RefNameBuf;use jj_lib::ref_name::RemoteName;use jj_lib::ref_name::RemoteNameBuf;use jj_lib::ref_name::RemoteRefSymbol;use jj_lib::refs::classify_bookmark_push_action;use jj_lib::refs::BookmarkPushAction;use jj_lib::refs::BookmarkPushUpdate;use jj_lib::refs::LocalAndRemoteRef;use jj_lib::repo::Repo;use jj_lib::revset::RevsetExpression;use jj_lib::settings::UserSettings;use jj_lib::signing::SignBehavior;use jj_lib::str_util::StringPattern;use jj_lib::view::View; use crate::cli_util::has_tracked_remote_bookmarks;use crate::cli_util::short_commit_hash;use crate::cli_util::CommandHelper;use crate::cli_util::RevisionArg;use crate::cli_util::WorkspaceCommandHelper;use crate::cli_util::WorkspaceCommandTransaction;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_hint;use crate::command_error::user_error_with_message;use crate::command_error::CommandError;use crate::commands::git::get_single_remote;use crate::complete;use crate::formatter::Formatter;use crate::git_util::with_remote_git_callbacks;use crate::revset_util::parse_bookmark_name;use crate::ui::Ui; /// Push to a Git remote////// By default, pushes tracking bookmarks pointing to/// `remote_bookmarks(remote=<remote>)..@`. Use `--bookmark` to push specific/// bookmarks. Use `--all` to push all bookmarks. Use `--change` to generate/// bookmark names based on the change IDs of specific commits.////// 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://jj-vcs.github.io/jj/latest/bookmarks/#pushing-bookmarks-safety-checks////// [bookmark conflicts]:///     https://jj-vcs.github.io/jj/latest/bookmarks/#conflicts #[derive(clap::Args, Clone, Debug)]#[command(group(ArgGroup::new("specific").args(&["bookmark", "change", "revisions", "named"]).multiple(true)))]#[command(group(ArgGroup::new("what").args(&["all", "tracked"]).conflicts_with("specific")))]pub struct GitPushArgs {    /// The remote to push to (only named remotes are supported)    ///    /// 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.    #[arg(long, add = ArgValueCandidates::new(complete::git_remotes))]    remote: Option<RemoteNameBuf>,    /// Push only this bookmark, or bookmarks matching a pattern (can be    /// repeated)    ///    /// By default, the specified name matches exactly. Use `glob:` prefix to    /// select bookmarks by [wildcard pattern].    ///    /// [wildcard pattern]:    ///     https://jj-vcs.github.io/jj/latest/revsets#string-patterns    #[arg(        long, short,        alias = "branch",        value_parser = StringPattern::parse,        add = ArgValueCandidates::new(complete::local_bookmarks),    )]    bookmark: Vec<StringPattern>,    /// Push all bookmarks (including new bookmarks)    #[arg(long)]    all: bool,    /// Push all tracked bookmarks    ///    /// This usually means that the bookmark was already pushed to or fetched    /// from the [relevant remote].    ///    /// [relevant remote]:    ///     https://jj-vcs.github.io/jj/latest/bookmarks#remotes-and-tracked-bookmarks    #[arg(long)]    tracked: bool,    /// Push all deleted bookmarks    ///    /// Only tracked bookmarks can be successfully deleted on the remote. A    /// warning will be printed if any untracked bookmarks on the remote    /// correspond to missing local bookmarks.    #[arg(long, conflicts_with = "specific")]    deleted: bool,    /// Allow pushing new bookmarks    ///    /// Newly-created remote bookmarks will be tracked automatically.    ///    /// This can also be turned on by the `git.push-new-bookmarks` setting. If    /// it's set to `true`, `--allow-new` is no-op.    #[arg(long, short = 'N', conflicts_with = "what")]    allow_new: 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,    /// Push bookmarks pointing to these commits (can be repeated)    #[arg(        long,        short,        value_name = "REVSETS",        // While `-r` will often be used with mutable revisions, immutable        // revisions can be useful as parts of revsets or to push        // special-purpose branches.        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,        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.        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=@'    ///    /// Does not require --allow-new.    #[arg(        long,        value_name = "NAME=REVISION",        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,} fn make_bookmark_term(bookmark_names: &[impl fmt::Display]) -> String {    match bookmark_names {        [bookmark_name] => format!("bookmark {bookmark_name}"),        bookmark_names => format!("bookmarks {}", bookmark_names.iter().join(", ")),    }} const DEFAULT_REMOTE: &RemoteName = RemoteName::new("origin"); #[derive(Clone, Copy, Debug, Eq, PartialEq)]enum BookmarkMoveDirection {    Forward,    Backward,    Sideways,} pub fn cmd_git_push(    ui: &mut Ui,    command: &CommandHelper,    args: &GitPushArgs,) -> Result<(), CommandError> {    let mut workspace_command = command.workspace_helper(ui)?;     let default_remote;    let remote = if let Some(name) = &args.remote {        name    } else {        default_remote = get_default_push_remote(ui, &workspace_command)?;        &default_remote    };     let mut tx = workspace_command.start_transaction();    let view = tx.repo().view();    let tx_description;    let mut bookmark_updates = vec![];    if args.all {        for (name, targets) in view.local_remote_bookmarks(remote) {            let allow_new = true; // implied by --all            match classify_bookmark_update(                name.to_remote_symbol(remote),                targets,                allow_new,                args.deleted,            ) {                Ok(Some(update)) => bookmark_updates.push((name.to_owned(), update)),                Ok(None) => {}                Err(reason) => reason.print(ui)?,            }        }        tx_description = format!(            "push all bookmarks to git remote {remote}",            remote = remote.as_symbol()        );    } else if args.tracked {        for (name, targets) in view.local_remote_bookmarks(remote) {            if !targets.remote_ref.is_tracked() {                continue;            }            let allow_new = false; // doesn't matter            match classify_bookmark_update(                name.to_remote_symbol(remote),                targets,                allow_new,                args.deleted,            ) {                Ok(Some(update)) => bookmark_updates.push((name.to_owned(), update)),                Ok(None) => {}                Err(reason) => reason.print(ui)?,            }        }        tx_description = format!(            "push all tracked bookmarks to git remote {remote}",            remote = remote.as_symbol()        );    } else if args.deleted {        for (name, targets) in view.local_remote_bookmarks(remote) {            if targets.local_target.is_present() {                continue;            }            let allow_new = false; // doesn't matter            let allow_delete = true;            match classify_bookmark_update(                name.to_remote_symbol(remote),                targets,                allow_new,                allow_delete,            ) {                Ok(Some(update)) => bookmark_updates.push((name.to_owned(), update)),                Ok(None) => {}                Err(reason) => reason.print(ui)?,            }        }        tx_description = format!(            "push all deleted bookmarks to git remote {remote}",            remote = remote.as_symbol()        );    } else {        let mut seen_bookmarks: HashSet<&RefName> = HashSet::new();         // --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)?;        let created_bookmark_names: Vec<RefNameBuf> = args            .named            .iter()            .map(|name_revision| create_explicitly_named_bookmarks(ui, &mut tx, name_revision))            .try_collect()?;        let created_bookmarks = change_bookmark_names            .iter()            .chain(created_bookmark_names.iter())            .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 allow_new = true; // --change implies creation of remote bookmark            let allow_delete = false; // doesn't matter            match classify_bookmark_update(remote_symbol, targets, allow_new, allow_delete) {                Ok(Some(update)) => bookmark_updates.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 allow_new = args.allow_new || tx.settings().get("git.push-new-bookmarks")?;        let bookmarks_by_name = find_bookmarks_to_push(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 allow_delete = true; // named explicitly, allow delete without --delete            match classify_bookmark_update(remote_symbol, targets, allow_new, allow_delete) {                Ok(Some(update)) => bookmark_updates.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 use_default_revset = args.bookmark.is_empty()            && args.change.is_empty()            && args.revisions.is_empty()            && args.named.is_empty();        let bookmarks_targeted = find_bookmarks_targeted_by_revisions(            ui,            tx.base_workspace_helper(),            remote,            &args.revisions,            use_default_revset,        )?;        for &(name, targets) in &bookmarks_targeted {            if !seen_bookmarks.insert(name) {                continue;            }            let allow_delete = false;            match classify_bookmark_update(                name.to_remote_symbol(remote),                targets,                allow_new,                allow_delete,            ) {                Ok(Some(update)) => bookmark_updates.push((name.to_owned(), update)),                Ok(None) => {}                Err(reason) => reason.print(ui)?,            }        }         tx_description = format!(            "push {names} to git remote {remote}",            names = make_bookmark_term(                &bookmark_updates                    .iter()                    .map(|(name, _)| name.as_symbol())                    .collect_vec()            ),            remote = remote.as_symbol()        );    }    if bookmark_updates.is_empty() {        writeln!(ui.status(), "Nothing changed.")?;        return Ok(());    }     let sign_behavior = if tx.settings().get_bool("git.sign-on-push")? {        Some(SignBehavior::Own)    } else {        None    };    let commits_to_sign =        validate_commits_ready_to_push(ui, &bookmark_updates, remote, &tx, args, sign_behavior)?;    if !args.dry_run && !commits_to_sign.is_empty() {        if let Some(sign_behavior) = sign_behavior {            let num_updated_signatures = commits_to_sign.len();            let num_rebased_descendants;            (num_rebased_descendants, bookmark_updates) = sign_commits_before_push(                &mut tx,                commits_to_sign,                sign_behavior,                bookmark_updates,            )?;            if let Some(mut formatter) = ui.status_formatter() {                writeln!(                    formatter,                    "Updated signatures of {num_updated_signatures} commits"                )?;                if num_rebased_descendants > 0 {                    writeln!(                        formatter,                        "Rebased {num_rebased_descendants} descendant commits"                    )?;                }            }        }    }     if let Some(mut formatter) = ui.status_formatter() {        writeln!(            formatter,            "Changes to push to {remote}:",            remote = remote.as_symbol()        )?;        print_commits_ready_to_push(formatter.as_mut(), tx.repo(), &bookmark_updates)?;    }     if args.dry_run {        writeln!(ui.status(), "Dry-run requested, not pushing.")?;        return Ok(());    }     let targets = GitBranchPushTargets {        branch_updates: bookmark_updates,    };    let git_settings = tx.settings().git_settings()?;    let push_stats = with_remote_git_callbacks(ui, |cb| {        git::push_branches(tx.repo_mut(), &git_settings, remote, &targets, cb)    })?;    process_push_stats(&push_stats)?;    tx.finish(ui, tx_description)?;    Ok(())} fn process_push_stats(push_stats: &GitPushStats) -> Result<(), CommandError> {    if !push_stats.all_ok() {        let mut error = user_error("Failed to push some bookmarks");        if !push_stats.rejected.is_empty() {            error.add_formatted_hint_with(|formatter| {                writeln!(                    formatter,                    "The following references unexpectedly moved on the remote:"                )?;                for (reference, reason) in &push_stats.rejected {                    write!(formatter, "  ")?;                    write!(formatter.labeled("git_ref"), "{}", reference.as_symbol())?;                    if let Some(r) = reason {                        write!(formatter, " (reason: {r})")?;                    }                    writeln!(formatter)?;                }                Ok(())            });            error.add_hint(                "Try fetching from the remote, then make the bookmark point to where you want it \                 to be, and push again.",            );        }        if !push_stats.remote_rejected.is_empty() {            error.add_formatted_hint_with(|formatter| {                writeln!(formatter, "The remote rejected the following updates:")?;                for (reference, reason) in &push_stats.remote_rejected {                    write!(formatter, "  ")?;                    write!(formatter.labeled("git_ref"), "{}", reference.as_symbol())?;                    if let Some(r) = reason {                        write!(formatter, " (reason: {r})")?;                    }                    writeln!(formatter)?;                }                Ok(())            });            error.add_hint("Try checking if you have permission to push to all the bookmarks.");        }        Err(error)    } else {        Ok(())    }} /// Validates that the commits that will be pushed are ready (have authorship/// information, are not conflicted, etc.).

Lines 1–500 of 1018.