ok/jj
1
0
Fork 0
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:
Martin von Zweigbergk 2023-06-02 22:39:33 -07:00 committed by Martin von Zweigbergk
parent 9f7180ff55
commit 7f3d07e35f
3 changed files with 119 additions and 21 deletions

View file

@ -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
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
command can be overridden by setting `ui.default-command`.

View file

@ -22,8 +22,9 @@ use jujutsu_lib::workspace::Workspace;
use maplit::hashset;
use crate::cli_util::{
print_failed_git_export, short_change_hash, short_commit_hash, user_error,
user_error_with_hint, CommandError, CommandHelper, RevisionArg, WorkspaceCommandHelper,
print_failed_git_export, resolve_multiple_nonempty_revsets, short_change_hash,
short_commit_hash, user_error, user_error_with_hint, CommandError, CommandHelper, RevisionArg,
WorkspaceCommandHelper,
};
use crate::commands::make_branch_term;
use crate::progress::Progress;
@ -119,7 +120,7 @@ pub struct GitCloneArgs {
/// all branches. Use `--change` to generate branch names based on the change
/// IDs of specific commits.
#[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")))]
pub struct GitPushArgs {
/// The remote to push to (only named remotes are supported)
@ -134,6 +135,9 @@ pub struct GitPushArgs {
/// Push all deleted branches
#[arg(long)]
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
/// repeated)
#[arg(long)]
@ -632,6 +636,17 @@ fn cmd_git_push(
.iter()
.map(|change_str| workspace_command.resolve_single_rev(change_str))
.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 tx_description;
let mut branch_updates = vec![];
@ -659,7 +674,7 @@ fn cmd_git_push(
if args.deleted { "deleted " } else { "" },
&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 {
if !seen_branches.insert(branch_name.clone()) {
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!(
"push {} to git remote {}",
make_branch_term(
@ -738,18 +778,6 @@ fn cmd_git_push(
return Err(user_error("Nothing checked out in this workspace"));
}
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 @
let mut branches =
find_branches_targeting(repo.view(), &RefTarget::Normal(wc_commit.clone()));

View file

@ -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]
fn test_git_push_mixed() {
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", "my-branch"]);
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();
let stdout = test_env.jj_cmd_success(
&workspace_root,
&["git", "push", "--change=@-", "--branch=my-branch"],
&["git", "push", "--change=@--", "--branch=branch-1", "-r=@"],
);
insta::assert_snapshot!(stdout, @r###"
Creating branch push-yqosqzytrlsw for revision @-
Creating branch push-yqosqzytrlsw for revision @--
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 branch-2a to 1b45449e18d0
Add branch branch-2b to 1b45449e18d0
"###);
}