diff --git a/CHANGELOG.md b/CHANGELOG.md index 15009357ad..622494fb88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `jj branch rename` will now warn if the renamed branch has a remote branch, since those will have to be manually renamed outside of `jj`. +* `jj git push` gained a `--tracked` option, to push all the tracked branches. + * There's now a virtual root operation, similar to the [virtual root commit](docs/glossary.md#root-commit). It appears at the end of `jj op log`. diff --git a/cli/src/commands/git.rs b/cli/src/commands/git.rs index 00f9811381..b426b44a49 100644 --- a/cli/src/commands/git.rs +++ b/cli/src/commands/git.rs @@ -193,7 +193,7 @@ pub struct GitCloneArgs { /// branch names based on the change IDs of specific commits. #[derive(clap::Args, Clone, Debug)] #[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", "tracked"]).conflicts_with("specific")))] pub struct GitPushArgs { /// The remote to push to (only named remotes are supported) #[arg(long)] @@ -205,6 +205,16 @@ pub struct GitPushArgs { /// https://martinvonz.github.io/jj/latest/revsets#string-patterns. #[arg(long, short, value_parser = parse_string_pattern)] branch: Vec, + /// Push all tracked branches (including deleted branches) + /// + /// This usually means that the branch was already pushed to or fetched from + /// the relevant remote. For details, see + /// https://martinvonz.github.io/jj/latest/branches#remotes-and-tracked-branches + /// + /// Not yet implemented, TODO(#2946): `jj git push --tracked --deleted` + /// would make sense, but is not currently allowed. + #[arg(long)] + tracked: bool, /// Push all branches (including deleted branches) #[arg(long)] all: bool, @@ -741,6 +751,18 @@ fn cmd_git_push( } } tx_description = format!("push all branches to git remote {remote}"); + } else if args.tracked { + for (branch_name, targets) in repo.view().local_remote_branches(&remote) { + if !targets.remote_ref.is_tracking() { + continue; + } + match classify_branch_update(branch_name, &remote, targets) { + Ok(Some(update)) => branch_updates.push((branch_name.to_owned(), update)), + Ok(None) => {} + Err(reason) => reason.print(ui)?, + } + } + tx_description = format!("push all tracked branches to git remote {remote}"); } else if args.deleted { for (branch_name, targets) in repo.view().local_remote_branches(&remote) { if targets.local_target.is_present() { diff --git a/cli/tests/cli-reference@.md.snap b/cli/tests/cli-reference@.md.snap index d2f4dceec5..90e8cc7878 100644 --- a/cli/tests/cli-reference@.md.snap +++ b/cli/tests/cli-reference@.md.snap @@ -894,6 +894,10 @@ By default, pushes any branches pointing to `remote_branches(remote=)..@ * `--remote ` — The remote to push to (only named remotes are supported) * `-b`, `--branch ` — Push only this branch (can be repeated) +* `--tracked` — Push all tracked branches (including deleted branches) + + Possible values: `true`, `false` + * `--all` — Push all branches (including deleted branches) Possible values: `true`, `false` diff --git a/cli/tests/test_git_push.rs b/cli/tests/test_git_push.rs index b6e8406ae5..86592e4d65 100644 --- a/cli/tests/test_git_push.rs +++ b/cli/tests/test_git_push.rs @@ -779,6 +779,76 @@ fn test_git_push_deleted_untracked() { "###); } +#[test] +fn test_git_push_tracked_vs_all() { + let (test_env, workspace_root) = set_up(); + test_env.jj_cmd_ok(&workspace_root, &["new", "branch1", "-mmoved branch1"]); + test_env.jj_cmd_ok(&workspace_root, &["branch", "set", "branch1"]); + test_env.jj_cmd_ok(&workspace_root, &["new", "branch2", "-mmoved branch2"]); + test_env.jj_cmd_ok(&workspace_root, &["branch", "delete", "branch2"]); + test_env.jj_cmd_ok(&workspace_root, &["branch", "untrack", "branch1@origin"]); + test_env.jj_cmd_ok(&workspace_root, &["branch", "create", "branch3"]); + let stdout = test_env.jj_cmd_success(&workspace_root, &["branch", "list", "--all"]); + insta::assert_snapshot!(stdout, @r###" + branch1: vruxwmqv a25f24af (empty) moved branch1 + branch1@origin: lzmmnrxq 45a3aa29 (empty) description 1 + branch2 (deleted) + @origin: rlzusymt 8476341e (empty) description 2 + (this branch will be *deleted permanently* on the remote on the next `jj git push`. Use `jj branch forget` to prevent this) + branch3: znkkpsqq 998d6a78 (empty) moved branch2 + "###); + + // At this point, only branch2 is still tracked. `jj git push --tracked` would + // try to push it and no other branches. + let (_stdout, stderr) = + test_env.jj_cmd_ok(&workspace_root, &["git", "push", "--tracked", "--dry-run"]); + insta::assert_snapshot!(stderr, @r###" + Branch changes to push to origin: + Delete branch branch2 from 8476341eb395 + Dry-run requested, not pushing. + "###); + + // Untrack the last remaining tracked branch. + test_env.jj_cmd_ok(&workspace_root, &["branch", "untrack", "branch2@origin"]); + let stdout = test_env.jj_cmd_success(&workspace_root, &["branch", "list", "--all"]); + insta::assert_snapshot!(stdout, @r###" + branch1: vruxwmqv a25f24af (empty) moved branch1 + branch1@origin: lzmmnrxq 45a3aa29 (empty) description 1 + branch2@origin: rlzusymt 8476341e (empty) description 2 + branch3: znkkpsqq 998d6a78 (empty) moved branch2 + "###); + + // Now, no branches are tracked. --tracked does not push anything + let (_stdout, stderr) = test_env.jj_cmd_ok(&workspace_root, &["git", "push", "--tracked"]); + insta::assert_snapshot!(stderr, @r###" + Nothing changed. + "###); + + // All branches are still untracked. + // - --all tries to push branch1, but fails because a branch with the same + // name exist on the remote. + // - --all succeeds in pushing branch3, since there is no branch of the same + // name on the remote. + // - It does not try to push branch2. + // + // TODO: Not trying to push branch2 could be considered correct, or perhaps + // we want to consider this as a deletion of the branch that failed because + // the branch was untracked. In the latter case, an error message should be + // printed. Some considerations: + // - Whatever we do should be consistent with what `jj branch list` does; it + // currently does *not* list branches like branch2 as "about to be deleted", + // as can be seen above. + // - We could consider showing some hint on `jj branch untrack branch2@origin` + // instead of showing an error here. + let (_stdout, stderr) = test_env.jj_cmd_ok(&workspace_root, &["git", "push", "--all"]); + insta::assert_snapshot!(stderr, @r###" + Non-tracking remote branch branch1@origin exists + Hint: Run `jj branch track branch1@origin` to import the remote branch. + Branch changes to push to origin: + Add branch branch3 to 998d6a7853d9 + "###); +} + #[test] fn test_git_push_moved_forward_untracked() { let (test_env, workspace_root) = set_up();