cli: improve hint message when suggesting --ignore-immutable

There have been a number of users confused about why
their commits are immutable, or what to do about it, ex.
[https://github.com/jj-vcs/jj/discussions/5659].

Separately, I feel that the cli is too quick to suggest
`--ignore-immutable`, without context of the consequences. A new user
could see that the command is failing, see a helpful hint to make it not
fail, apply it and move on. This has wildly different consequences, from
`jj squash --into someone_elses_branch@origin` rewriting a single commit,
to `jj edit 'root()+'` rewriting your entire history.

This commit changes the immutable hint by doing the following:

* Adds a short description of what immutable commits are used for, and a
  link to the relevant docs, to the hint message.
* Shows the number of immutable commits that would be rewritten if
  the operation had succeeded.
* Removes the suggestion to use `--ignore-immutable`.
This commit is contained in:
Bryce Berger 2025-02-12 21:41:06 -05:00
parent a17ed203ab
commit 708e1c58cd
6 changed files with 215 additions and 80 deletions

View file

@ -88,6 +88,9 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
* Help text is now colored (when stdout is a terminal).
* Commands that used to suggest `--ignore-immutable` now print the number of
immutable commits that would be rewritten if used and a link to the docs.
### Fixed bugs
* `jj status` now shows untracked files under untracked directories.

View file

@ -908,14 +908,19 @@ impl WorkspaceCommandEnvironment {
}
}
/// Returns first immutable commit + lower and upper bounds on number of
/// immutable commits.
fn find_immutable_commit<'a>(
&self,
repo: &dyn Repo,
commits: impl IntoIterator<Item = &'a CommitId>,
) -> Result<Option<CommitId>, CommandError> {
) -> Result<Option<(CommitId, usize, Option<usize>)>, CommandError> {
if self.command.global_args().ignore_immutable {
let root_id = repo.store().root_commit_id();
return Ok(commits.into_iter().find(|id| *id == root_id).cloned());
return Ok(commits
.into_iter()
.find(|id| *id == root_id)
.map(|root| (root.clone(), 1, None)));
}
// Not using self.id_prefix_context() because the disambiguation data
@ -935,7 +940,21 @@ impl WorkspaceCommandEnvironment {
let mut commit_id_iter = expression.evaluate_to_commit_ids().map_err(|e| {
config_error_with_message("Invalid `revset-aliases.immutable_heads()`", e)
})?;
Ok(commit_id_iter.next().transpose()?)
let Some(first_immutable) = commit_id_iter.next().transpose()? else {
return Ok(None);
};
let mut bounds = RevsetExpressionEvaluator::new(
repo,
self.command.revset_extensions().clone(),
&id_prefix_context,
self.immutable_expression(),
);
bounds.intersect_with(&to_rewrite_revset.descendants());
let (lower, upper) = bounds.evaluate()?.count_estimate()?;
Ok(Some((first_immutable, lower, upper)))
}
/// Parses template of the given language into evaluation tree.
@ -1816,7 +1835,7 @@ to the current parents may contain changes from multiple commits.
&self,
commits: impl IntoIterator<Item = &'a CommitId>,
) -> Result<(), CommandError> {
let Some(commit_id) = self
let Some((commit_id, lower_bound, upper_bound)) = self
.env
.find_immutable_commit(self.repo().as_ref(), commits)?
else {
@ -1832,10 +1851,18 @@ to the current parents may contain changes from multiple commits.
self.write_commit_summary(formatter, &commit)?;
Ok(())
});
error.add_hint(
"Pass `--ignore-immutable` or configure the set of immutable commits via \
`revset-aliases.immutable_heads()`.",
);
error.add_hint("Immutable commits are used to protect shared history.");
error.add_hint(indoc::indoc! {"
For more information, see:
- https://jj-vcs.github.io/jj/latest/config/#set-of-immutable-commits
- `jj help -k config`, \"Set of immutable commits\""});
let exact = upper_bound == Some(lower_bound);
let or_more = if exact { "" } else { " or more" };
error.add_hint(format!(
"This operation would rewrite {lower_bound}{or_more} immutable commits."
));
error
};
Err(error)

View file

@ -727,12 +727,16 @@ fn test_absorb_immutable() {
// Immutable revisions shouldn't be rewritten
let stderr = test_env.jj_cmd_failure(&repo_path, &["absorb", "--into=all()"]);
insta::assert_snapshot!(stderr, @r"
insta::assert_snapshot!(stderr, @r##"
Error: Commit 3619e4e52fce is immutable
Hint: Could not modify commit: qpvuntsm 3619e4e5 main | 1
Hint: Pass `--ignore-immutable` or configure the set of immutable commits via `revset-aliases.immutable_heads()`.
Hint: Immutable commits are used to protect shared history.
Hint: For more information, see:
- https://jj-vcs.github.io/jj/latest/config/#set-of-immutable-commits
- `jj help -k config`, "Set of immutable commits"
Hint: This operation would rewrite 1 immutable commits.
[EOF]
");
"##);
insta::assert_snapshot!(get_diffs(&test_env, &repo_path, ".."), @r"
@ mzvwutvl 3021153d (no description set)

View file

@ -712,12 +712,16 @@ fn test_fix_immutable_commit() {
test_env.add_config(r#"revset-aliases."immutable_heads()" = "immutable""#);
let stderr = test_env.jj_cmd_failure(&repo_path, &["fix", "-s", "immutable"]);
insta::assert_snapshot!(stderr, @r"
insta::assert_snapshot!(stderr, @r##"
Error: Commit e4b41a3ce243 is immutable
Hint: Could not modify commit: qpvuntsm e4b41a3c immutable | (no description set)
Hint: Pass `--ignore-immutable` or configure the set of immutable commits via `revset-aliases.immutable_heads()`.
Hint: Immutable commits are used to protect shared history.
Hint: For more information, see:
- https://jj-vcs.github.io/jj/latest/config/#set-of-immutable-commits
- `jj help -k config`, "Set of immutable commits"
Hint: This operation would rewrite 1 immutable commits.
[EOF]
");
"##);
let content = test_env.jj_cmd_success(&repo_path, &["file", "show", "file", "-r", "immutable"]);
insta::assert_snapshot!(content, @"immutable[EOF]");
let content = test_env.jj_cmd_success(&repo_path, &["file", "show", "file", "-r", "mutable"]);

View file

@ -41,20 +41,28 @@ fn test_rewrite_immutable_generic() {
// Cannot rewrite a commit in the configured set
test_env.add_config(r#"revset-aliases."immutable_heads()" = "main""#);
let stderr = test_env.jj_cmd_failure(&repo_path, &["edit", "main"]);
insta::assert_snapshot!(stderr, @r"
insta::assert_snapshot!(stderr, @r##"
Error: Commit 72e1b68cbcf2 is immutable
Hint: Could not modify commit: kkmpptxz 72e1b68c main | b
Hint: Pass `--ignore-immutable` or configure the set of immutable commits via `revset-aliases.immutable_heads()`.
Hint: Immutable commits are used to protect shared history.
Hint: For more information, see:
- https://jj-vcs.github.io/jj/latest/config/#set-of-immutable-commits
- `jj help -k config`, "Set of immutable commits"
Hint: This operation would rewrite 1 immutable commits.
[EOF]
");
"##);
// Cannot rewrite an ancestor of the configured set
let stderr = test_env.jj_cmd_failure(&repo_path, &["edit", "main-"]);
insta::assert_snapshot!(stderr, @r"
insta::assert_snapshot!(stderr, @r##"
Error: Commit b84b821b8a2b is immutable
Hint: Could not modify commit: qpvuntsm b84b821b a
Hint: Pass `--ignore-immutable` or configure the set of immutable commits via `revset-aliases.immutable_heads()`.
Hint: Immutable commits are used to protect shared history.
Hint: For more information, see:
- https://jj-vcs.github.io/jj/latest/config/#set-of-immutable-commits
- `jj help -k config`, "Set of immutable commits"
Hint: This operation would rewrite 2 immutable commits.
[EOF]
");
"##);
// Cannot rewrite the root commit even with an empty set of immutable commits
test_env.add_config(r#"revset-aliases."immutable_heads()" = "none()""#);
let stderr = test_env.jj_cmd_failure(&repo_path, &["edit", "root()"]);
@ -221,164 +229,244 @@ fn test_rewrite_immutable_commands() {
// abandon
let stderr = test_env.jj_cmd_failure(&repo_path, &["abandon", "main"]);
insta::assert_snapshot!(stderr, @r"
insta::assert_snapshot!(stderr, @r##"
Error: Commit bcab555fc80e is immutable
Hint: Could not modify commit: mzvwutvl bcab555f main | (conflict) merge
Hint: Pass `--ignore-immutable` or configure the set of immutable commits via `revset-aliases.immutable_heads()`.
Hint: Immutable commits are used to protect shared history.
Hint: For more information, see:
- https://jj-vcs.github.io/jj/latest/config/#set-of-immutable-commits
- `jj help -k config`, "Set of immutable commits"
Hint: This operation would rewrite 1 immutable commits.
[EOF]
");
"##);
// absorb
let stderr = test_env.jj_cmd_failure(&repo_path, &["absorb", "--into=::@-"]);
insta::assert_snapshot!(stderr, @r"
insta::assert_snapshot!(stderr, @r##"
Error: Commit 72e1b68cbcf2 is immutable
Hint: Could not modify commit: kkmpptxz 72e1b68c b
Hint: Pass `--ignore-immutable` or configure the set of immutable commits via `revset-aliases.immutable_heads()`.
Hint: Immutable commits are used to protect shared history.
Hint: For more information, see:
- https://jj-vcs.github.io/jj/latest/config/#set-of-immutable-commits
- `jj help -k config`, "Set of immutable commits"
Hint: This operation would rewrite 2 immutable commits.
[EOF]
");
"##);
// chmod
let stderr = test_env.jj_cmd_failure(&repo_path, &["file", "chmod", "-r=main", "x", "file"]);
insta::assert_snapshot!(stderr, @r"
insta::assert_snapshot!(stderr, @r##"
Error: Commit bcab555fc80e is immutable
Hint: Could not modify commit: mzvwutvl bcab555f main | (conflict) merge
Hint: Pass `--ignore-immutable` or configure the set of immutable commits via `revset-aliases.immutable_heads()`.
Hint: Immutable commits are used to protect shared history.
Hint: For more information, see:
- https://jj-vcs.github.io/jj/latest/config/#set-of-immutable-commits
- `jj help -k config`, "Set of immutable commits"
Hint: This operation would rewrite 1 immutable commits.
[EOF]
");
"##);
// describe
let stderr = test_env.jj_cmd_failure(&repo_path, &["describe", "main"]);
insta::assert_snapshot!(stderr, @r"
insta::assert_snapshot!(stderr, @r##"
Error: Commit bcab555fc80e is immutable
Hint: Could not modify commit: mzvwutvl bcab555f main | (conflict) merge
Hint: Pass `--ignore-immutable` or configure the set of immutable commits via `revset-aliases.immutable_heads()`.
Hint: Immutable commits are used to protect shared history.
Hint: For more information, see:
- https://jj-vcs.github.io/jj/latest/config/#set-of-immutable-commits
- `jj help -k config`, "Set of immutable commits"
Hint: This operation would rewrite 1 immutable commits.
[EOF]
");
"##);
// diffedit
let stderr = test_env.jj_cmd_failure(&repo_path, &["diffedit", "-r=main"]);
insta::assert_snapshot!(stderr, @r"
insta::assert_snapshot!(stderr, @r##"
Error: Commit bcab555fc80e is immutable
Hint: Could not modify commit: mzvwutvl bcab555f main | (conflict) merge
Hint: Pass `--ignore-immutable` or configure the set of immutable commits via `revset-aliases.immutable_heads()`.
Hint: Immutable commits are used to protect shared history.
Hint: For more information, see:
- https://jj-vcs.github.io/jj/latest/config/#set-of-immutable-commits
- `jj help -k config`, "Set of immutable commits"
Hint: This operation would rewrite 1 immutable commits.
[EOF]
");
"##);
// edit
let stderr = test_env.jj_cmd_failure(&repo_path, &["edit", "main"]);
insta::assert_snapshot!(stderr, @r"
insta::assert_snapshot!(stderr, @r##"
Error: Commit bcab555fc80e is immutable
Hint: Could not modify commit: mzvwutvl bcab555f main | (conflict) merge
Hint: Pass `--ignore-immutable` or configure the set of immutable commits via `revset-aliases.immutable_heads()`.
Hint: Immutable commits are used to protect shared history.
Hint: For more information, see:
- https://jj-vcs.github.io/jj/latest/config/#set-of-immutable-commits
- `jj help -k config`, "Set of immutable commits"
Hint: This operation would rewrite 1 immutable commits.
[EOF]
");
"##);
// new --insert-before
let stderr = test_env.jj_cmd_failure(&repo_path, &["new", "--insert-before", "main"]);
insta::assert_snapshot!(stderr, @r"
insta::assert_snapshot!(stderr, @r##"
Error: Commit bcab555fc80e is immutable
Hint: Could not modify commit: mzvwutvl bcab555f main | (conflict) merge
Hint: Pass `--ignore-immutable` or configure the set of immutable commits via `revset-aliases.immutable_heads()`.
Hint: Immutable commits are used to protect shared history.
Hint: For more information, see:
- https://jj-vcs.github.io/jj/latest/config/#set-of-immutable-commits
- `jj help -k config`, "Set of immutable commits"
Hint: This operation would rewrite 1 immutable commits.
[EOF]
");
"##);
// new --insert-after parent_of_main
let stderr = test_env.jj_cmd_failure(&repo_path, &["new", "--insert-after", "description(b)"]);
insta::assert_snapshot!(stderr, @r"
insta::assert_snapshot!(stderr, @r##"
Error: Commit bcab555fc80e is immutable
Hint: Could not modify commit: mzvwutvl bcab555f main | (conflict) merge
Hint: Pass `--ignore-immutable` or configure the set of immutable commits via `revset-aliases.immutable_heads()`.
Hint: Immutable commits are used to protect shared history.
Hint: For more information, see:
- https://jj-vcs.github.io/jj/latest/config/#set-of-immutable-commits
- `jj help -k config`, "Set of immutable commits"
Hint: This operation would rewrite 1 immutable commits.
[EOF]
");
"##);
// parallelize
let stderr = test_env.jj_cmd_failure(&repo_path, &["parallelize", "description(b)", "main"]);
insta::assert_snapshot!(stderr, @r"
insta::assert_snapshot!(stderr, @r##"
Error: Commit bcab555fc80e is immutable
Hint: Could not modify commit: mzvwutvl bcab555f main | (conflict) merge
Hint: Pass `--ignore-immutable` or configure the set of immutable commits via `revset-aliases.immutable_heads()`.
Hint: Immutable commits are used to protect shared history.
Hint: For more information, see:
- https://jj-vcs.github.io/jj/latest/config/#set-of-immutable-commits
- `jj help -k config`, "Set of immutable commits"
Hint: This operation would rewrite 2 immutable commits.
[EOF]
");
"##);
// rebase -s
let stderr = test_env.jj_cmd_failure(&repo_path, &["rebase", "-s=main", "-d=@"]);
insta::assert_snapshot!(stderr, @r"
insta::assert_snapshot!(stderr, @r##"
Error: Commit bcab555fc80e is immutable
Hint: Could not modify commit: mzvwutvl bcab555f main | (conflict) merge
Hint: Pass `--ignore-immutable` or configure the set of immutable commits via `revset-aliases.immutable_heads()`.
Hint: Immutable commits are used to protect shared history.
Hint: For more information, see:
- https://jj-vcs.github.io/jj/latest/config/#set-of-immutable-commits
- `jj help -k config`, "Set of immutable commits"
Hint: This operation would rewrite 1 immutable commits.
[EOF]
");
"##);
// rebase -b
let stderr = test_env.jj_cmd_failure(&repo_path, &["rebase", "-b=main", "-d=@"]);
insta::assert_snapshot!(stderr, @r"
insta::assert_snapshot!(stderr, @r##"
Error: Commit 77cee210cbf5 is immutable
Hint: Could not modify commit: zsuskuln 77cee210 c
Hint: Pass `--ignore-immutable` or configure the set of immutable commits via `revset-aliases.immutable_heads()`.
Hint: Immutable commits are used to protect shared history.
Hint: For more information, see:
- https://jj-vcs.github.io/jj/latest/config/#set-of-immutable-commits
- `jj help -k config`, "Set of immutable commits"
Hint: This operation would rewrite 2 immutable commits.
[EOF]
");
"##);
// rebase -r
let stderr = test_env.jj_cmd_failure(&repo_path, &["rebase", "-r=main", "-d=@"]);
insta::assert_snapshot!(stderr, @r"
insta::assert_snapshot!(stderr, @r##"
Error: Commit bcab555fc80e is immutable
Hint: Could not modify commit: mzvwutvl bcab555f main | (conflict) merge
Hint: Pass `--ignore-immutable` or configure the set of immutable commits via `revset-aliases.immutable_heads()`.
Hint: Immutable commits are used to protect shared history.
Hint: For more information, see:
- https://jj-vcs.github.io/jj/latest/config/#set-of-immutable-commits
- `jj help -k config`, "Set of immutable commits"
Hint: This operation would rewrite 1 immutable commits.
[EOF]
");
"##);
// resolve
let stderr = test_env.jj_cmd_failure(&repo_path, &["resolve", "-r=description(merge)", "file"]);
insta::assert_snapshot!(stderr, @r"
insta::assert_snapshot!(stderr, @r##"
Error: Commit bcab555fc80e is immutable
Hint: Could not modify commit: mzvwutvl bcab555f main | (conflict) merge
Hint: Pass `--ignore-immutable` or configure the set of immutable commits via `revset-aliases.immutable_heads()`.
Hint: Immutable commits are used to protect shared history.
Hint: For more information, see:
- https://jj-vcs.github.io/jj/latest/config/#set-of-immutable-commits
- `jj help -k config`, "Set of immutable commits"
Hint: This operation would rewrite 1 immutable commits.
[EOF]
");
"##);
// restore -c
let stderr = test_env.jj_cmd_failure(&repo_path, &["restore", "-c=main"]);
insta::assert_snapshot!(stderr, @r"
insta::assert_snapshot!(stderr, @r##"
Error: Commit bcab555fc80e is immutable
Hint: Could not modify commit: mzvwutvl bcab555f main | (conflict) merge
Hint: Pass `--ignore-immutable` or configure the set of immutable commits via `revset-aliases.immutable_heads()`.
Hint: Immutable commits are used to protect shared history.
Hint: For more information, see:
- https://jj-vcs.github.io/jj/latest/config/#set-of-immutable-commits
- `jj help -k config`, "Set of immutable commits"
Hint: This operation would rewrite 1 immutable commits.
[EOF]
");
"##);
// restore --into
let stderr = test_env.jj_cmd_failure(&repo_path, &["restore", "--into=main"]);
insta::assert_snapshot!(stderr, @r"
insta::assert_snapshot!(stderr, @r##"
Error: Commit bcab555fc80e is immutable
Hint: Could not modify commit: mzvwutvl bcab555f main | (conflict) merge
Hint: Pass `--ignore-immutable` or configure the set of immutable commits via `revset-aliases.immutable_heads()`.
Hint: Immutable commits are used to protect shared history.
Hint: For more information, see:
- https://jj-vcs.github.io/jj/latest/config/#set-of-immutable-commits
- `jj help -k config`, "Set of immutable commits"
Hint: This operation would rewrite 1 immutable commits.
[EOF]
");
"##);
// split
let stderr = test_env.jj_cmd_failure(&repo_path, &["split", "-r=main"]);
insta::assert_snapshot!(stderr, @r"
insta::assert_snapshot!(stderr, @r##"
Error: Commit bcab555fc80e is immutable
Hint: Could not modify commit: mzvwutvl bcab555f main | (conflict) merge
Hint: Pass `--ignore-immutable` or configure the set of immutable commits via `revset-aliases.immutable_heads()`.
Hint: Immutable commits are used to protect shared history.
Hint: For more information, see:
- https://jj-vcs.github.io/jj/latest/config/#set-of-immutable-commits
- `jj help -k config`, "Set of immutable commits"
Hint: This operation would rewrite 1 immutable commits.
[EOF]
");
"##);
// squash -r
let stderr = test_env.jj_cmd_failure(&repo_path, &["squash", "-r=description(b)"]);
insta::assert_snapshot!(stderr, @r"
insta::assert_snapshot!(stderr, @r##"
Error: Commit 72e1b68cbcf2 is immutable
Hint: Could not modify commit: kkmpptxz 72e1b68c b
Hint: Pass `--ignore-immutable` or configure the set of immutable commits via `revset-aliases.immutable_heads()`.
Hint: Immutable commits are used to protect shared history.
Hint: For more information, see:
- https://jj-vcs.github.io/jj/latest/config/#set-of-immutable-commits
- `jj help -k config`, "Set of immutable commits"
Hint: This operation would rewrite 4 immutable commits.
[EOF]
");
"##);
// squash --from
let stderr = test_env.jj_cmd_failure(&repo_path, &["squash", "--from=main"]);
insta::assert_snapshot!(stderr, @r"
insta::assert_snapshot!(stderr, @r##"
Error: Commit bcab555fc80e is immutable
Hint: Could not modify commit: mzvwutvl bcab555f main | (conflict) merge
Hint: Pass `--ignore-immutable` or configure the set of immutable commits via `revset-aliases.immutable_heads()`.
Hint: Immutable commits are used to protect shared history.
Hint: For more information, see:
- https://jj-vcs.github.io/jj/latest/config/#set-of-immutable-commits
- `jj help -k config`, "Set of immutable commits"
Hint: This operation would rewrite 1 immutable commits.
[EOF]
");
"##);
// squash --into
let stderr = test_env.jj_cmd_failure(&repo_path, &["squash", "--into=main"]);
insta::assert_snapshot!(stderr, @r"
insta::assert_snapshot!(stderr, @r##"
Error: Commit bcab555fc80e is immutable
Hint: Could not modify commit: mzvwutvl bcab555f main | (conflict) merge
Hint: Pass `--ignore-immutable` or configure the set of immutable commits via `revset-aliases.immutable_heads()`.
Hint: Immutable commits are used to protect shared history.
Hint: For more information, see:
- https://jj-vcs.github.io/jj/latest/config/#set-of-immutable-commits
- `jj help -k config`, "Set of immutable commits"
Hint: This operation would rewrite 1 immutable commits.
[EOF]
");
"##);
// unsquash
let stderr = test_env.jj_cmd_failure(&repo_path, &["unsquash", "-r=main"]);
insta::assert_snapshot!(stderr, @r"
insta::assert_snapshot!(stderr, @r##"
Warning: `jj unsquash` is deprecated; use `jj diffedit --restore-descendants` or `jj squash` instead
Warning: `jj unsquash` will be removed in a future version, and this will be a hard error
Error: Commit bcab555fc80e is immutable
Hint: Could not modify commit: mzvwutvl bcab555f main | (conflict) merge
Hint: Pass `--ignore-immutable` or configure the set of immutable commits via `revset-aliases.immutable_heads()`.
Hint: Immutable commits are used to protect shared history.
Hint: For more information, see:
- https://jj-vcs.github.io/jj/latest/config/#set-of-immutable-commits
- `jj help -k config`, "Set of immutable commits"
Hint: This operation would rewrite 1 immutable commits.
[EOF]
");
"##);
}

View file

@ -343,6 +343,15 @@ To prevent rewriting commits authored by other users:
Ancestors of the configured set are also immutable. The root commit is always
immutable even if the set is empty.
Immutable commits (other than the root commit) can be rewritten using the
`--ignore-immutable` CLI flag.
!!! warning
Using `--ignore-immutable` will allow you to rewrite any commit in the
history, and all descendants, without warning. Use this power wisely, and
remember `jj undo`.
### Behavior of prev and next commands
If you prefer using an "edit-based" workflow, rather than squashing