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 * `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`.

View file

@ -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()));

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] #[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
"###); "###);
} }