mirror of
https://github.com/martinvonz/jj.git
synced 2025-05-05 23:42:50 +00:00
cli: git push: add --skip-private flag
This commit is contained in:
parent
3318d172ff
commit
2435d27eaf
@ -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
|
||||||
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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]
|
||||||
");
|
");
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user