forked from mirrors/jj
cli: add jj git push -r
for pushing branches pointing to revset
I think I will find this useful in at least two cases: 1. When you already have a branch pointing to some commit, it's easier to do `jj git push -r xyz` than `jj git push --branch push-xyzxyzyxzxyz`. 2. When you have a stack of changes, it's useful to be able to push all of them at once. I think we should also update the default behavior of `jj git push` to be `jj git push -r 'remote_branches()..@'` or something like that. That removes the ugliness of having a default behavior that the user can't reproduce using flags. I'll leave that change for a separate PR.
This commit is contained in:
parent
9f7180ff55
commit
7f3d07e35f
3 changed files with 119 additions and 21 deletions
|
@ -85,7 +85,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
* `jj git fetch` and `jj git push` will now use the single defined remote even
|
* `jj git fetch` and `jj git push` will now use the single defined remote even
|
||||||
if it is not named "origin".
|
if it is not named "origin".
|
||||||
|
|
||||||
* `jj git push` now accepts `--branch` and `--change` arguments together.
|
* `jj git push` now accepts `--branch` and `--change` arguments together.
|
||||||
|
|
||||||
|
* `jj git push` now accepts a `-r/--revisions` flag to specify revisions to
|
||||||
|
push. All branches pointing to any of the specified revisions will be pushed.
|
||||||
|
The flag can be used together with `--branch` and `--change`.
|
||||||
|
|
||||||
* `jj` with no subcommand now defaults to `jj log` instead of showing help. This
|
* `jj` with no subcommand now defaults to `jj log` instead of showing help. This
|
||||||
command can be overridden by setting `ui.default-command`.
|
command can be overridden by setting `ui.default-command`.
|
||||||
|
|
|
@ -22,8 +22,9 @@ use jujutsu_lib::workspace::Workspace;
|
||||||
use maplit::hashset;
|
use maplit::hashset;
|
||||||
|
|
||||||
use crate::cli_util::{
|
use crate::cli_util::{
|
||||||
print_failed_git_export, short_change_hash, short_commit_hash, user_error,
|
print_failed_git_export, resolve_multiple_nonempty_revsets, short_change_hash,
|
||||||
user_error_with_hint, CommandError, CommandHelper, RevisionArg, WorkspaceCommandHelper,
|
short_commit_hash, user_error, user_error_with_hint, CommandError, CommandHelper, RevisionArg,
|
||||||
|
WorkspaceCommandHelper,
|
||||||
};
|
};
|
||||||
use crate::commands::make_branch_term;
|
use crate::commands::make_branch_term;
|
||||||
use crate::progress::Progress;
|
use crate::progress::Progress;
|
||||||
|
@ -119,7 +120,7 @@ pub struct GitCloneArgs {
|
||||||
/// all branches. Use `--change` to generate branch names based on the change
|
/// all branches. Use `--change` to generate branch names based on the change
|
||||||
/// IDs of specific commits.
|
/// IDs of specific commits.
|
||||||
#[derive(clap::Args, Clone, Debug)]
|
#[derive(clap::Args, Clone, Debug)]
|
||||||
#[command(group(ArgGroup::new("specific").args(&["branch", "change"]).multiple(true)))]
|
#[command(group(ArgGroup::new("specific").args(&["branch", "change", "revisions"]).multiple(true)))]
|
||||||
#[command(group(ArgGroup::new("what").args(&["all", "deleted"]).conflicts_with("specific")))]
|
#[command(group(ArgGroup::new("what").args(&["all", "deleted"]).conflicts_with("specific")))]
|
||||||
pub struct GitPushArgs {
|
pub struct GitPushArgs {
|
||||||
/// The remote to push to (only named remotes are supported)
|
/// The remote to push to (only named remotes are supported)
|
||||||
|
@ -134,6 +135,9 @@ pub struct GitPushArgs {
|
||||||
/// Push all deleted branches
|
/// Push all deleted branches
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
deleted: bool,
|
deleted: bool,
|
||||||
|
/// Push branches pointing to these commits
|
||||||
|
#[arg(long, short)]
|
||||||
|
revisions: Vec<RevisionArg>,
|
||||||
/// Push this commit by creating a branch based on its change ID (can be
|
/// Push this commit by creating a branch based on its change ID (can be
|
||||||
/// repeated)
|
/// repeated)
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
|
@ -632,6 +636,17 @@ fn cmd_git_push(
|
||||||
.iter()
|
.iter()
|
||||||
.map(|change_str| workspace_command.resolve_single_rev(change_str))
|
.map(|change_str| workspace_command.resolve_single_rev(change_str))
|
||||||
.try_collect()?;
|
.try_collect()?;
|
||||||
|
let revision_commits = resolve_multiple_nonempty_revsets(&args.revisions, &workspace_command)?;
|
||||||
|
fn find_branches_targeting<'a>(
|
||||||
|
view: &'a View,
|
||||||
|
target: &RefTarget,
|
||||||
|
) -> Vec<(&'a String, &'a BranchTarget)> {
|
||||||
|
view.branches()
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, branch_target)| branch_target.local_target.as_ref() == Some(target))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
let mut tx = workspace_command.start_transaction("");
|
let mut tx = workspace_command.start_transaction("");
|
||||||
let tx_description;
|
let tx_description;
|
||||||
let mut branch_updates = vec![];
|
let mut branch_updates = vec![];
|
||||||
|
@ -659,7 +674,7 @@ fn cmd_git_push(
|
||||||
if args.deleted { "deleted " } else { "" },
|
if args.deleted { "deleted " } else { "" },
|
||||||
&remote
|
&remote
|
||||||
);
|
);
|
||||||
} else if !args.branch.is_empty() || !args.change.is_empty() {
|
} else if !args.branch.is_empty() || !args.change.is_empty() || !args.revisions.is_empty() {
|
||||||
for branch_name in &args.branch {
|
for branch_name in &args.branch {
|
||||||
if !seen_branches.insert(branch_name.clone()) {
|
if !seen_branches.insert(branch_name.clone()) {
|
||||||
continue;
|
continue;
|
||||||
|
@ -722,6 +737,31 @@ fn cmd_git_push(
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut any_revisions_targeted = false;
|
||||||
|
for commit in revision_commits {
|
||||||
|
for (branch_name, branch_target) in
|
||||||
|
find_branches_targeting(repo.view(), &RefTarget::Normal(commit.id().clone()))
|
||||||
|
{
|
||||||
|
any_revisions_targeted = true;
|
||||||
|
if !seen_branches.insert(branch_name.clone()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let push_action = classify_branch_push_action(branch_target, &remote);
|
||||||
|
match push_action {
|
||||||
|
BranchPushAction::AlreadyMatches
|
||||||
|
| BranchPushAction::LocalConflicted
|
||||||
|
| BranchPushAction::RemoteConflicted => {}
|
||||||
|
BranchPushAction::Update(update) => {
|
||||||
|
branch_updates.push((branch_name.clone(), update));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !args.revisions.is_empty() && !any_revisions_targeted {
|
||||||
|
return Err(user_error("No branches point to the specified revisions."));
|
||||||
|
}
|
||||||
|
|
||||||
tx_description = format!(
|
tx_description = format!(
|
||||||
"push {} to git remote {}",
|
"push {} to git remote {}",
|
||||||
make_branch_term(
|
make_branch_term(
|
||||||
|
@ -738,18 +778,6 @@ fn cmd_git_push(
|
||||||
return Err(user_error("Nothing checked out in this workspace"));
|
return Err(user_error("Nothing checked out in this workspace"));
|
||||||
}
|
}
|
||||||
Some(wc_commit) => {
|
Some(wc_commit) => {
|
||||||
fn find_branches_targeting<'a>(
|
|
||||||
view: &'a View,
|
|
||||||
target: &RefTarget,
|
|
||||||
) -> Vec<(&'a String, &'a BranchTarget)> {
|
|
||||||
view.branches()
|
|
||||||
.iter()
|
|
||||||
.filter(|(_, branch_target)| {
|
|
||||||
branch_target.local_target.as_ref() == Some(target)
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search for branches targeting @
|
// Search for branches targeting @
|
||||||
let mut branches =
|
let mut branches =
|
||||||
find_branches_targeting(repo.view(), &RefTarget::Normal(wc_commit.clone()));
|
find_branches_targeting(repo.view(), &RefTarget::Normal(wc_commit.clone()));
|
||||||
|
|
|
@ -279,24 +279,90 @@ fn test_git_push_changes() {
|
||||||
"###);
|
"###);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_git_push_revisions() {
|
||||||
|
let (test_env, workspace_root) = set_up();
|
||||||
|
test_env.jj_cmd_success(&workspace_root, &["describe", "-m", "foo"]);
|
||||||
|
std::fs::write(workspace_root.join("file"), "contents").unwrap();
|
||||||
|
test_env.jj_cmd_success(&workspace_root, &["new", "-m", "bar"]);
|
||||||
|
test_env.jj_cmd_success(&workspace_root, &["branch", "create", "branch-1"]);
|
||||||
|
std::fs::write(workspace_root.join("file"), "modified").unwrap();
|
||||||
|
test_env.jj_cmd_success(&workspace_root, &["new", "-m", "baz"]);
|
||||||
|
test_env.jj_cmd_success(&workspace_root, &["branch", "create", "branch-2a"]);
|
||||||
|
test_env.jj_cmd_success(&workspace_root, &["branch", "create", "branch-2b"]);
|
||||||
|
std::fs::write(workspace_root.join("file"), "modified again").unwrap();
|
||||||
|
|
||||||
|
// Push an empty set
|
||||||
|
let stderr = test_env.jj_cmd_failure(&workspace_root, &["git", "push", "-r=none()"]);
|
||||||
|
insta::assert_snapshot!(stderr, @r###"
|
||||||
|
Error: Empty revision set
|
||||||
|
"###);
|
||||||
|
// Push a revision with no branches
|
||||||
|
let stderr = test_env.jj_cmd_failure(&workspace_root, &["git", "push", "-r=@--"]);
|
||||||
|
insta::assert_snapshot!(stderr, @r###"
|
||||||
|
Error: No branches point to the specified revisions.
|
||||||
|
"###);
|
||||||
|
// Push a revision with a single branch
|
||||||
|
let stdout = test_env.jj_cmd_success(&workspace_root, &["git", "push", "-r=@-", "--dry-run"]);
|
||||||
|
insta::assert_snapshot!(stdout, @r###"
|
||||||
|
Branch changes to push to origin:
|
||||||
|
Add branch branch-1 to 7decc7932d9c
|
||||||
|
Dry-run requested, not pushing.
|
||||||
|
"###);
|
||||||
|
// Push multiple revisions of which some have branches
|
||||||
|
let stdout = test_env.jj_cmd_success(
|
||||||
|
&workspace_root,
|
||||||
|
&["git", "push", "-r=@--", "-r=@-", "--dry-run"],
|
||||||
|
);
|
||||||
|
insta::assert_snapshot!(stdout, @r###"
|
||||||
|
Branch changes to push to origin:
|
||||||
|
Add branch branch-1 to 7decc7932d9c
|
||||||
|
Dry-run requested, not pushing.
|
||||||
|
"###);
|
||||||
|
// Push a revision with a multiple branches
|
||||||
|
let stdout = test_env.jj_cmd_success(&workspace_root, &["git", "push", "-r=@", "--dry-run"]);
|
||||||
|
insta::assert_snapshot!(stdout, @r###"
|
||||||
|
Branch changes to push to origin:
|
||||||
|
Add branch branch-2a to 1b45449e18d0
|
||||||
|
Add branch branch-2b to 1b45449e18d0
|
||||||
|
Dry-run requested, not pushing.
|
||||||
|
"###);
|
||||||
|
// Repeating a commit doesn't result in repeated messages about the branch
|
||||||
|
let stdout = test_env.jj_cmd_success(
|
||||||
|
&workspace_root,
|
||||||
|
&["git", "push", "-r=@-", "-r=@-", "--dry-run"],
|
||||||
|
);
|
||||||
|
insta::assert_snapshot!(stdout, @r###"
|
||||||
|
Branch changes to push to origin:
|
||||||
|
Add branch branch-1 to 7decc7932d9c
|
||||||
|
Dry-run requested, not pushing.
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_git_push_mixed() {
|
fn test_git_push_mixed() {
|
||||||
let (test_env, workspace_root) = set_up();
|
let (test_env, workspace_root) = set_up();
|
||||||
test_env.jj_cmd_success(&workspace_root, &["describe", "-m", "foo"]);
|
test_env.jj_cmd_success(&workspace_root, &["describe", "-m", "foo"]);
|
||||||
std::fs::write(workspace_root.join("file"), "contents").unwrap();
|
std::fs::write(workspace_root.join("file"), "contents").unwrap();
|
||||||
test_env.jj_cmd_success(&workspace_root, &["new", "-m", "bar"]);
|
test_env.jj_cmd_success(&workspace_root, &["new", "-m", "bar"]);
|
||||||
test_env.jj_cmd_success(&workspace_root, &["branch", "create", "my-branch"]);
|
test_env.jj_cmd_success(&workspace_root, &["branch", "create", "branch-1"]);
|
||||||
std::fs::write(workspace_root.join("file"), "modified").unwrap();
|
std::fs::write(workspace_root.join("file"), "modified").unwrap();
|
||||||
|
test_env.jj_cmd_success(&workspace_root, &["new", "-m", "baz"]);
|
||||||
|
test_env.jj_cmd_success(&workspace_root, &["branch", "create", "branch-2a"]);
|
||||||
|
test_env.jj_cmd_success(&workspace_root, &["branch", "create", "branch-2b"]);
|
||||||
|
std::fs::write(workspace_root.join("file"), "modified again").unwrap();
|
||||||
|
|
||||||
let stdout = test_env.jj_cmd_success(
|
let stdout = test_env.jj_cmd_success(
|
||||||
&workspace_root,
|
&workspace_root,
|
||||||
&["git", "push", "--change=@-", "--branch=my-branch"],
|
&["git", "push", "--change=@--", "--branch=branch-1", "-r=@"],
|
||||||
);
|
);
|
||||||
insta::assert_snapshot!(stdout, @r###"
|
insta::assert_snapshot!(stdout, @r###"
|
||||||
Creating branch push-yqosqzytrlsw for revision @-
|
Creating branch push-yqosqzytrlsw for revision @--
|
||||||
Branch changes to push to origin:
|
Branch changes to push to origin:
|
||||||
Add branch my-branch to 7decc7932d9c
|
Add branch branch-1 to 7decc7932d9c
|
||||||
Add branch push-yqosqzytrlsw to fa16a14170fb
|
Add branch push-yqosqzytrlsw to fa16a14170fb
|
||||||
|
Add branch branch-2a to 1b45449e18d0
|
||||||
|
Add branch branch-2b to 1b45449e18d0
|
||||||
"###);
|
"###);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue