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 git push` now has a flag `--skip-private` to continue pushing other
bookmarks which don't contain private commits.
### Fixed bugs
## [0.27.0] - 2025-03-05

View File

@ -140,6 +140,16 @@ pub struct GitPushArgs {
/// commits are eligible to be pushed.
#[arg(long)]
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)
#[arg(
long,
@ -322,6 +332,13 @@ pub fn cmd_git_push(
&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() {
writeln!(ui.status(), "Nothing changed.")?;
return Ok(());
@ -361,7 +378,12 @@ pub fn cmd_git_push(
if let Some(mut formatter) = ui.status_formatter() {
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 {
@ -380,6 +402,65 @@ pub fn cmd_git_push(
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
/// information, are not conflicted, etc.).
///
@ -465,6 +546,9 @@ fn validate_commits_ready_to_push(
error.add_hint(format!(
"Configured git.private-commits: '{private_revset_str}'",
));
error.add_hint(
"Consider --skip-private to skip pushing bookmarks with private commits",
);
}
return Err(error);
}
@ -529,6 +613,7 @@ fn print_commits_ready_to_push(
formatter: &mut dyn Formatter,
repo: &dyn Repo,
bookmark_updates: &[(String, BookmarkPushUpdate)],
skipped_private_bookmarks: Vec<String>,
) -> io::Result<()> {
let to_direction = |old_target: &CommitId, new_target: &CommitId| {
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(())
}

View File

@ -1,6 +1,7 @@
---
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."
snapshot_kind: text
---
<!-- 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-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.
* `-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)

View File

@ -113,6 +113,7 @@ fn test_git_private_commits_block_pushing() {
Error: Won't push commit aa3058ff8663 since it is private
Hint: Rejected commit: yqosqzyt aa3058ff main* | (empty) private 1
Hint: Configured git.private-commits: 'description(glob:'private*')'
Hint: Consider --skip-private to skip pushing bookmarks with private commits
[EOF]
[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
Hint: Rejected commit: yqosqzyt aa3058ff main* | (empty) private 1
Hint: Configured git.private-commits: 'description(glob:'private*')'
Hint: Consider --skip-private to skip pushing bookmarks with private commits
[EOF]
[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
Hint: Rejected commit: yqosqzyt f1253a9b (empty) private 1
Hint: Configured git.private-commits: 'description(glob:'private*')'
Hint: Consider --skip-private to skip pushing bookmarks with private commits
[EOF]
[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
Hint: Rejected commit: znkkpsqq 36b7ecd1 (empty) private 1
Hint: Configured git.private-commits: 'description(glob:'private*')'
Hint: Consider --skip-private to skip pushing bookmarks with private commits
[EOF]
[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]
fn get_bookmark_output(test_env: &TestEnvironment, repo_path: &Path) -> CommandOutput {
// --quiet to suppress deleted bookmarks hint