diff --git a/CHANGELOG.md b/CHANGELOG.md index bc1904276..8d2724481 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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`. diff --git a/src/commands/git.rs b/src/commands/git.rs index 95e569244..bc7f0ffaa 100644 --- a/src/commands/git.rs +++ b/src/commands/git.rs @@ -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, /// 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())); diff --git a/tests/test_git_push.rs b/tests/test_git_push.rs index f8f856830..441b5b8b7 100644 --- a/tests/test_git_push.rs +++ b/tests/test_git_push.rs @@ -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 "###); }