cli: git push: add --skip-private flag

This commit is contained in:
Remo Senekowitsch 2025-03-09 18:21:12 +01:00
parent 3318d172ff
commit 2435d27eaf
No known key found for this signature in database
5 changed files with 154 additions and 1 deletions

View File

@ -43,6 +43,9 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
* `jj op log -d` now has an alias for `jj op log --op-diff`. * `jj op log -d` now has an alias for `jj op log --op-diff`.
* `jj git push` now has a flag `--skip-private` to continue pushing other
bookmarks which don't contain private commits.
### Fixed bugs ### Fixed bugs
## [0.27.0] - 2025-03-05 ## [0.27.0] - 2025-03-05

View File

@ -140,6 +140,16 @@ pub struct GitPushArgs {
/// commits are eligible to be pushed. /// commits are eligible to be pushed.
#[arg(long)] #[arg(long)]
allow_private: bool, allow_private: bool,
/// Skip pushing bookmarks that depend on private commits
///
/// If this is omitted, the command will error if any of the bookmarks to
/// be pushed depend of private commits.
///
/// The set of private commits can be configured by the
/// `git.private-commits` setting. The default is `none()`, meaning all
/// commits are eligible to be pushed.
#[arg(long, conflicts_with = "allow_private")]
skip_private: bool,
/// Push bookmarks pointing to these commits (can be repeated) /// Push bookmarks pointing to these commits (can be repeated)
#[arg( #[arg(
long, long,
@ -322,6 +332,13 @@ pub fn cmd_git_push(
&remote &remote
); );
} }
let skipped_private_bookmarks = if args.skip_private {
retain_bookmarks_without_private_commits(ui, &mut bookmark_updates, &remote, &tx)?
} else {
vec![]
};
if bookmark_updates.is_empty() { if bookmark_updates.is_empty() {
writeln!(ui.status(), "Nothing changed.")?; writeln!(ui.status(), "Nothing changed.")?;
return Ok(()); return Ok(());
@ -361,7 +378,12 @@ pub fn cmd_git_push(
if let Some(mut formatter) = ui.status_formatter() { if let Some(mut formatter) = ui.status_formatter() {
writeln!(formatter, "Changes to push to {remote}:")?; writeln!(formatter, "Changes to push to {remote}:")?;
print_commits_ready_to_push(formatter.as_mut(), tx.repo(), &bookmark_updates)?; print_commits_ready_to_push(
formatter.as_mut(),
tx.repo(),
&bookmark_updates,
skipped_private_bookmarks,
)?;
} }
if args.dry_run { if args.dry_run {
@ -380,6 +402,65 @@ pub fn cmd_git_push(
Ok(()) Ok(())
} }
/// Removes any bookmarks from the `bookmark_updates` if they require private
/// commits to be pushed.
///
/// Returns the list of bookmarks that were removed, for informational logging.
fn retain_bookmarks_without_private_commits(
ui: &Ui,
bookmark_updates: &mut Vec<(String, BookmarkPushUpdate)>,
remote: &str,
tx: &WorkspaceCommandTransaction,
) -> Result<Vec<String>, CommandError> {
let workspace_helper = tx.base_workspace_helper();
let repo = workspace_helper.repo();
let settings = workspace_helper.settings();
let old_heads = repo
.view()
.remote_bookmarks(remote)
.flat_map(|(_, old_head)| old_head.target.added_ids())
.cloned()
.collect_vec();
let wont_be_pushed_revset = RevsetExpression::commits(old_heads)
.union(workspace_helper.env().immutable_heads_expression());
let private_revset_str = RevisionArg::from(settings.get_string("git.private-commits")?);
let is_private = workspace_helper
.parse_revset(ui, &private_revset_str)?
.evaluate()?
.containing_fn();
let mut remaining_bookmark_updates = Vec::with_capacity(bookmark_updates.len());
let mut skipped_bookmarks = vec![];
'bookmark_loop: for (bookmark, update) in bookmark_updates.drain(..) {
let Some(new_head) = update.new_target.clone() else {
// bookmark deletion won't lead to private commits being pushed
remaining_bookmark_updates.push((bookmark, update));
continue;
};
let commits_to_push =
wont_be_pushed_revset.range(&RevsetExpression::commits(vec![new_head]));
for commit in workspace_helper
.attach_revset_evaluator(commits_to_push)
.evaluate_to_commits()?
{
if is_private(commit?.id())? {
skipped_bookmarks.push(bookmark);
continue 'bookmark_loop;
}
}
remaining_bookmark_updates.push((bookmark, update));
}
*bookmark_updates = remaining_bookmark_updates;
Ok(skipped_bookmarks)
}
/// Validates that the commits that will be pushed are ready (have authorship /// Validates that the commits that will be pushed are ready (have authorship
/// information, are not conflicted, etc.). /// information, are not conflicted, etc.).
/// ///
@ -465,6 +546,9 @@ fn validate_commits_ready_to_push(
error.add_hint(format!( error.add_hint(format!(
"Configured git.private-commits: '{private_revset_str}'", "Configured git.private-commits: '{private_revset_str}'",
)); ));
error.add_hint(
"Consider --skip-private to skip pushing bookmarks with private commits",
);
} }
return Err(error); return Err(error);
} }
@ -529,6 +613,7 @@ fn print_commits_ready_to_push(
formatter: &mut dyn Formatter, formatter: &mut dyn Formatter,
repo: &dyn Repo, repo: &dyn Repo,
bookmark_updates: &[(String, BookmarkPushUpdate)], bookmark_updates: &[(String, BookmarkPushUpdate)],
skipped_private_bookmarks: Vec<String>,
) -> io::Result<()> { ) -> io::Result<()> {
let to_direction = |old_target: &CommitId, new_target: &CommitId| { let to_direction = |old_target: &CommitId, new_target: &CommitId| {
assert_ne!(old_target, new_target); assert_ne!(old_target, new_target);
@ -584,6 +669,12 @@ fn print_commits_ready_to_push(
} }
} }
} }
for bookmark_name in skipped_private_bookmarks {
writeln!(
formatter,
" Skipped bookmark {bookmark_name} with private commits"
)?;
}
Ok(()) Ok(())
} }

View File

@ -1,6 +1,7 @@
--- ---
source: cli/tests/test_generate_md_cli_help.rs source: cli/tests/test_generate_md_cli_help.rs
description: "AUTO-GENERATED FILE, DO NOT EDIT. This cli reference is generated by a test as an `insta` snapshot. MkDocs includes this snapshot from docs/cli-reference.md." description: "AUTO-GENERATED FILE, DO NOT EDIT. This cli reference is generated by a test as an `insta` snapshot. MkDocs includes this snapshot from docs/cli-reference.md."
snapshot_kind: text
--- ---
<!-- BEGIN MARKDOWN--> <!-- BEGIN MARKDOWN-->
@ -1264,6 +1265,11 @@ Before the command actually moves, creates, or deletes a remote bookmark, it mak
* `--allow-empty-description` — Allow pushing commits with empty descriptions * `--allow-empty-description` — Allow pushing commits with empty descriptions
* `--allow-private` — Allow pushing commits that are private * `--allow-private` — Allow pushing commits that are private
The set of private commits can be configured by the `git.private-commits` setting. The default is `none()`, meaning all commits are eligible to be pushed.
* `--skip-private` — Skip pushing bookmarks that depend on private commits
If this is omitted, the command will error if any of the bookmarks to be pushed depend of private commits.
The set of private commits can be configured by the `git.private-commits` setting. The default is `none()`, meaning all commits are eligible to be pushed. The set of private commits can be configured by the `git.private-commits` setting. The default is `none()`, meaning all commits are eligible to be pushed.
* `-r`, `--revisions <REVSETS>` — Push bookmarks pointing to these commits (can be repeated) * `-r`, `--revisions <REVSETS>` — Push bookmarks pointing to these commits (can be repeated)
* `-c`, `--change <REVSETS>` — Push this commit by creating a bookmark based on its change ID (can be repeated) * `-c`, `--change <REVSETS>` — Push this commit by creating a bookmark based on its change ID (can be repeated)

View File

@ -113,6 +113,7 @@ fn test_git_private_commits_block_pushing() {
Error: Won't push commit aa3058ff8663 since it is private Error: Won't push commit aa3058ff8663 since it is private
Hint: Rejected commit: yqosqzyt aa3058ff main* | (empty) private 1 Hint: Rejected commit: yqosqzyt aa3058ff main* | (empty) private 1
Hint: Configured git.private-commits: 'description(glob:'private*')' Hint: Configured git.private-commits: 'description(glob:'private*')'
Hint: Consider --skip-private to skip pushing bookmarks with private commits
[EOF] [EOF]
[exit status: 1] [exit status: 1]
"); ");
@ -150,6 +151,7 @@ fn test_git_private_commits_can_be_overridden() {
Error: Won't push commit aa3058ff8663 since it is private Error: Won't push commit aa3058ff8663 since it is private
Hint: Rejected commit: yqosqzyt aa3058ff main* | (empty) private 1 Hint: Rejected commit: yqosqzyt aa3058ff main* | (empty) private 1
Hint: Configured git.private-commits: 'description(glob:'private*')' Hint: Configured git.private-commits: 'description(glob:'private*')'
Hint: Consider --skip-private to skip pushing bookmarks with private commits
[EOF] [EOF]
[exit status: 1] [exit status: 1]
"); ");
@ -218,6 +220,7 @@ fn test_git_private_commits_not_directly_in_line_block_pushing() {
Error: Won't push commit f1253a9b1ea9 since it is private Error: Won't push commit f1253a9b1ea9 since it is private
Hint: Rejected commit: yqosqzyt f1253a9b (empty) private 1 Hint: Rejected commit: yqosqzyt f1253a9b (empty) private 1
Hint: Configured git.private-commits: 'description(glob:'private*')' Hint: Configured git.private-commits: 'description(glob:'private*')'
Hint: Consider --skip-private to skip pushing bookmarks with private commits
[EOF] [EOF]
[exit status: 1] [exit status: 1]
"); ");
@ -360,6 +363,7 @@ fn test_git_private_commits_are_evaluated_separately_for_each_remote() {
Error: Won't push commit 36b7ecd11ad9 since it is private Error: Won't push commit 36b7ecd11ad9 since it is private
Hint: Rejected commit: znkkpsqq 36b7ecd1 (empty) private 1 Hint: Rejected commit: znkkpsqq 36b7ecd1 (empty) private 1
Hint: Configured git.private-commits: 'description(glob:'private*')' Hint: Configured git.private-commits: 'description(glob:'private*')'
Hint: Consider --skip-private to skip pushing bookmarks with private commits
[EOF] [EOF]
[exit status: 1] [exit status: 1]
"); ");

View File

@ -2295,6 +2295,55 @@ fn test_git_push_sign_on_push() {
"); ");
} }
#[test]
fn test_git_push_private_commits() {
let (test_env, workspace_root) = set_up();
test_env.add_config(r#"git.private-commits = "description('private')""#);
test_env
.run_jj_in(&workspace_root, ["new", "bookmark1", "-m", "public"])
.success();
test_env
.run_jj_in(&workspace_root, ["bookmark", "set", "bookmark1", "-r@"])
.success();
test_env
.run_jj_in(&workspace_root, ["new", "bookmark2", "-m", "private"])
.success();
test_env
.run_jj_in(&workspace_root, ["bookmark", "set", "bookmark2", "-r@"])
.success();
// fails due to private commits on bookmark2
let output = test_env.run_jj_in(&workspace_root, ["git", "push", "--all"]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Error: Won't push commit f98b6dcb4760 since it is private
Hint: Rejected commit: znkkpsqq f98b6dcb bookmark2* | (empty) private
Hint: Configured git.private-commits: 'description('private')'
Hint: Consider --skip-private to skip pushing bookmarks with private commits
[EOF]
[exit status: 1]
");
// succeeds, but skips bookmark2
let output = test_env.run_jj_in(&workspace_root, ["git", "push", "--all", "--skip-private"]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Changes to push to origin:
Move forward bookmark bookmark1 from d13ecdbda2a2 to 8677da72ba01
Skipped bookmark bookmark2 with private commits
[EOF]
");
// succeeds, pushing bookmark2 with its private commits
let output = test_env.run_jj_in(&workspace_root, ["git", "push", "--all", "--allow-private"]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Changes to push to origin:
Move forward bookmark bookmark2 from 8476341eb395 to f98b6dcb4760
[EOF]
");
}
#[must_use] #[must_use]
fn get_bookmark_output(test_env: &TestEnvironment, repo_path: &Path) -> CommandOutput { fn get_bookmark_output(test_env: &TestEnvironment, repo_path: &Path) -> CommandOutput {
// --quiet to suppress deleted bookmarks hint // --quiet to suppress deleted bookmarks hint