// 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::RemoteNameBuf;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 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)] #[arg(add = ArgValueCandidates::new(complete::git_remotes))] remote: Option<RemoteNameBuf>, /// 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(ref_updates: &GitPushRefTargets) -> String { let kind_updates = [ ("bookmark", "bookmarks", &ref_updates.bookmarks), ("tag", "tags", &ref_updates.tags), ]; kind_updates .into_iter() .filter_map(|(kind, kinds, refs)| match &**refs { [] => None, [(name, _)] => Some(format!("{kind} {}", name.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 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 mut ref_updates = GitPushRefTargets::default(); if args.all { let mut commits_validator = CommitsValidator::new(ui, tx.base_workspace_helper(), remote, args)?; for (name, targets) in view.local_remote_bookmarks(remote) { let remote_symbol = name.to_remote_symbol(remote); let allow_new = true; // implied by --all match classify_bookmark_update(remote_symbol, targets, allow_new, args.deleted) { Ok(Some(update)) => match commits_validator.validate_update(&update).await? { Ok(()) => ref_updates.bookmarks.push((name.to_owned(), update)), Err(reason) => reason.print_bookmark(ui, tx.base_workspace_helper(), name)?, }, Ok(None) => {} Err(reason) => reason.print(ui)?, } } for (name, targets) in view.local_remote_tags(remote) { let remote_symbol = name.to_remote_symbol(remote); let allow_new = true; // implied by --all match classify_tag_update(remote_symbol, targets, allow_new, args.deleted) { Ok(Some(update)) => match commits_validator.validate_update(&update).await? { Ok(()) => ref_updates.tags.push((name.to_owned(), update)), Err(reason) => reason.print_tag(ui, tx.base_workspace_helper(), name)?, }, Ok(None) => {} Err(reason) => reason.print(ui)?, } } } else if args.tracked { let mut commits_validator = CommitsValidator::new(ui, tx.base_workspace_helper(), remote, args)?; for (name, targets) in view.local_remote_bookmarks(remote) { if !targets.remote_ref.is_tracked() { continue; } let remote_symbol = name.to_remote_symbol(remote); let allow_new = false; // doesn't matter match classify_bookmark_update(remote_symbol, targets, allow_new, args.deleted) { Ok(Some(update)) => match commits_validator.validate_update(&update).await? { Ok(()) => ref_updates.bookmarks.push((name.to_owned(), update)), Err(reason) => reason.print_bookmark(ui, tx.base_workspace_helper(), name)?, }, Ok(None) => {} Err(reason) => reason.print(ui)?, } } for (name, targets) in view.local_remote_tags(remote) { if !targets.remote_ref.is_tracked() { continue; } let remote_symbol = name.to_remote_symbol(remote); let allow_new = false; // doesn't matter match classify_tag_update(remote_symbol, targets, allow_new, args.deleted) { Ok(Some(update)) => match commits_validator.validate_update(&update).await? { Ok(()) => ref_updates.tags.push((name.to_owned(), update)), Err(reason) => reason.print_tag(ui, tx.base_workspace_helper(), name)?, }, Ok(None) => {} Err(reason) => reason.print(ui)?, } } } else if args.deleted { // There shouldn't be new heads to push, but we run validation for consistency. let mut commits_validator = CommitsValidator::new(ui, tx.base_workspace_helper(), remote, args)?; for (name, targets) in view.local_remote_bookmarks(remote) { if targets.local_target.is_present() { continue; } let remote_symbol = name.to_remote_symbol(remote); let allow_new = false; // doesn't matter let allow_delete = true; match classify_bookmark_update(remote_symbol, targets, allow_new, allow_delete) { Ok(Some(update)) => match commits_validator.validate_update(&update).await? { Ok(()) => ref_updates.bookmarks.push((name.to_owned(), update)), Err(reason) => reason.print_bookmark(ui, tx.base_workspace_helper(), name)?, }, Ok(None) => {} Err(reason) => reason.print(ui)?, } } for (name, targets) in view.local_remote_tags(remote) { if targets.local_target.is_present() { continue; } let remote_symbol = name.to_remote_symbol(remote); let allow_new = false; // doesn't matter let allow_delete = true; match classify_tag_update(remote_symbol, targets, allow_new, allow_delete) { Ok(Some(update)) => match commits_validator.validate_update(&update).await? { Ok(()) => ref_updates.tags.push((name.to_owned(), update)), Err(reason) => reason.print_tag(ui, tx.base_workspace_helper(), name)?, }, Ok(None) => {} Err(reason) => reason.print(ui)?, } } } else { let mut seen_bookmarks: HashSet<&RefName> = HashSet::new(); let mut seen_tags: 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).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())); } 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 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)) => 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); // 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. let allow_new = !has_tracked_remote_bookmarks(tx.repo(), name); let allow_delete = true; // named explicitly, allow delete without --delete match classify_bookmark_update(remote_symbol, targets, allow_new, allow_delete) { 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); // named explicitly, allow track or delete let allow_new = !has_tracked_remote_tags(tx.repo(), name); let allow_delete = true; match classify_tag_update(remote_symbol, targets, allow_new, allow_delete) { 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? .map_err(|reason| reason.to_command_error(tx.base_workspace_helper()))?; let use_default_revset = args.bookmark.is_empty() && args.tag.is_empty() && args.change.is_empty() && args.revisions.is_empty() && args.named.is_empty(); let target_revisions = if use_default_revset { find_default_target_revisions(ui, tx.base_workspace_helper(), remote).await? } else { find_target_revisions(ui, tx.base_workspace_helper(), &args.revisions).await? }; for (name, targets) in tx.base_repo().view().local_remote_bookmarks(remote) {Lines 1–500 of 1382. Load more
// 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::RemoteNameBuf; 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 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)] #[arg(add = ArgValueCandidates::new(complete::git_remotes))] remote: Option<RemoteNameBuf>, /// 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(ref_updates: &GitPushRefTargets) -> String { let kind_updates = [ ("bookmark", "bookmarks", &ref_updates.bookmarks), ("tag", "tags", &ref_updates.tags), ]; kind_updates .into_iter() .filter_map(|(kind, kinds, refs)| match &**refs { [] => None, [(name, _)] => Some(format!("{kind} {}", name.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 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 mut ref_updates = GitPushRefTargets::default(); if args.all { let mut commits_validator = CommitsValidator::new(ui, tx.base_workspace_helper(), remote, args)?; for (name, targets) in view.local_remote_bookmarks(remote) { let remote_symbol = name.to_remote_symbol(remote); let allow_new = true; // implied by --all match classify_bookmark_update(remote_symbol, targets, allow_new, args.deleted) { Ok(Some(update)) => match commits_validator.validate_update(&update).await? { Ok(()) => ref_updates.bookmarks.push((name.to_owned(), update)), Err(reason) => reason.print_bookmark(ui, tx.base_workspace_helper(), name)?, }, Ok(None) => {} Err(reason) => reason.print(ui)?, } } for (name, targets) in view.local_remote_tags(remote) { let remote_symbol = name.to_remote_symbol(remote); let allow_new = true; // implied by --all match classify_tag_update(remote_symbol, targets, allow_new, args.deleted) { Ok(Some(update)) => match commits_validator.validate_update(&update).await? { Ok(()) => ref_updates.tags.push((name.to_owned(), update)), Err(reason) => reason.print_tag(ui, tx.base_workspace_helper(), name)?, }, Ok(None) => {} Err(reason) => reason.print(ui)?, } } } else if args.tracked { let mut commits_validator = CommitsValidator::new(ui, tx.base_workspace_helper(), remote, args)?; for (name, targets) in view.local_remote_bookmarks(remote) { if !targets.remote_ref.is_tracked() { continue; } let remote_symbol = name.to_remote_symbol(remote); let allow_new = false; // doesn't matter match classify_bookmark_update(remote_symbol, targets, allow_new, args.deleted) { Ok(Some(update)) => match commits_validator.validate_update(&update).await? { Ok(()) => ref_updates.bookmarks.push((name.to_owned(), update)), Err(reason) => reason.print_bookmark(ui, tx.base_workspace_helper(), name)?, }, Ok(None) => {} Err(reason) => reason.print(ui)?, } } for (name, targets) in view.local_remote_tags(remote) { if !targets.remote_ref.is_tracked() { continue; } let remote_symbol = name.to_remote_symbol(remote); let allow_new = false; // doesn't matter match classify_tag_update(remote_symbol, targets, allow_new, args.deleted) { Ok(Some(update)) => match commits_validator.validate_update(&update).await? { Ok(()) => ref_updates.tags.push((name.to_owned(), update)), Err(reason) => reason.print_tag(ui, tx.base_workspace_helper(), name)?, }, Ok(None) => {} Err(reason) => reason.print(ui)?, } } } else if args.deleted { // There shouldn't be new heads to push, but we run validation for consistency. let mut commits_validator = CommitsValidator::new(ui, tx.base_workspace_helper(), remote, args)?; for (name, targets) in view.local_remote_bookmarks(remote) { if targets.local_target.is_present() { continue; } let remote_symbol = name.to_remote_symbol(remote); let allow_new = false; // doesn't matter let allow_delete = true; match classify_bookmark_update(remote_symbol, targets, allow_new, allow_delete) { Ok(Some(update)) => match commits_validator.validate_update(&update).await? { Ok(()) => ref_updates.bookmarks.push((name.to_owned(), update)), Err(reason) => reason.print_bookmark(ui, tx.base_workspace_helper(), name)?, }, Ok(None) => {} Err(reason) => reason.print(ui)?, } } for (name, targets) in view.local_remote_tags(remote) { if targets.local_target.is_present() { continue; } let remote_symbol = name.to_remote_symbol(remote); let allow_new = false; // doesn't matter let allow_delete = true; match classify_tag_update(remote_symbol, targets, allow_new, allow_delete) { Ok(Some(update)) => match commits_validator.validate_update(&update).await? { Ok(()) => ref_updates.tags.push((name.to_owned(), update)), Err(reason) => reason.print_tag(ui, tx.base_workspace_helper(), name)?, }, Ok(None) => {} Err(reason) => reason.print(ui)?, } } } else { let mut seen_bookmarks: HashSet<&RefName> = HashSet::new(); let mut seen_tags: 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).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())); } 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 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)) => 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); // 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. let allow_new = !has_tracked_remote_bookmarks(tx.repo(), name); let allow_delete = true; // named explicitly, allow delete without --delete match classify_bookmark_update(remote_symbol, targets, allow_new, allow_delete) { 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); // named explicitly, allow track or delete let allow_new = !has_tracked_remote_tags(tx.repo(), name); let allow_delete = true; match classify_tag_update(remote_symbol, targets, allow_new, allow_delete) { 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? .map_err(|reason| reason.to_command_error(tx.base_workspace_helper()))?; let use_default_revset = args.bookmark.is_empty() && args.tag.is_empty() && args.change.is_empty() && args.revisions.is_empty() && args.named.is_empty(); let target_revisions = if use_default_revset { find_default_target_revisions(ui, tx.base_workspace_helper(), remote).await? } else { find_target_revisions(ui, tx.base_workspace_helper(), &args.revisions).await? }; for (name, targets) in tx.base_repo().view().local_remote_bookmarks(remote) {