diff --git a/CHANGELOG.md b/CHANGELOG.md index 67a804b51..e79b16ea0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * You can check whether Watchman fsmonitor is enabled or installed with the new `jj debug watchman status` command. +* A new global flag `--ignore-immutable` lets you rewrite immutable commits. + ### Fixed bugs * Revsets now support `\`-escapes in string literal. diff --git a/cli/src/cli_util.rs b/cli/src/cli_util.rs index 4be76be21..3e615cc1c 100644 --- a/cli/src/cli_util.rs +++ b/cli/src/cli_util.rs @@ -1065,6 +1065,17 @@ impl WorkspaceCommandHelper { &self, commits: impl IntoIterator, ) -> Result<(), CommandError> { + if self.global_args.ignore_immutable { + let root_id = self.repo().store().root_commit_id(); + return if commits.into_iter().contains(root_id) { + Err(user_error(format!( + "The root commit {} is immutable", + short_commit_hash(root_id), + ))) + } else { + Ok(()) + }; + } let to_rewrite_revset = RevsetExpression::commits(commits.into_iter().cloned().collect_vec()); let immutable = revset_util::parse_immutable_expression(&self.revset_parse_context()) @@ -1087,7 +1098,7 @@ impl WorkspaceCommandHelper { } else { user_error_with_hint( format!("Commit {} is immutable", short_commit_hash(&commit_id)), - "Configure the set of immutable commits via \ + "Pass `--ignore-immutable` or configure the set of immutable commits via \ `revset-aliases.immutable_heads()`.", ) }; @@ -2330,6 +2341,16 @@ pub struct GlobalArgs { /// implies `--ignore-working-copy`. #[arg(long, global = true)] pub ignore_working_copy: bool, + /// Don't prevent rewriting immutable commits + /// + /// By default, Jujutsu prevents rewriting commits in the configured set of + /// immutable commits. This option disables that check and lets you rewrite + /// any commit but the root commit. + /// + /// This option only affects the check. It does not affect the + /// `immutable_heads()` revset or the `immutable` template keyword. + #[arg(long, global = true)] + pub ignore_immutable: bool, /// Operation to load the repo at /// /// Operation to load the repo at. By default, Jujutsu loads the repo at the diff --git a/cli/tests/cli-reference@.md.snap b/cli/tests/cli-reference@.md.snap index 8c5a2e61e..76bc68816 100644 --- a/cli/tests/cli-reference@.md.snap +++ b/cli/tests/cli-reference@.md.snap @@ -153,6 +153,10 @@ To get started, see the tutorial at https://github.com/martinvonz/jj/blob/main/d Possible values: `true`, `false` +* `--ignore-immutable` — Don't prevent rewriting immutable commits + + Possible values: `true`, `false` + * `--at-operation ` — Operation to load the repo at Default value: `@` diff --git a/cli/tests/test_global_opts.rs b/cli/tests/test_global_opts.rs index 9b55fd521..0dd196f26 100644 --- a/cli/tests/test_global_opts.rs +++ b/cli/tests/test_global_opts.rs @@ -592,6 +592,7 @@ fn test_help() { Global Options: -R, --repository Path to repository to operate on --ignore-working-copy Don't snapshot the working copy, and don't update it + --ignore-immutable Don't prevent rewriting immutable commits --at-operation Operation to load the repo at [default: @] [aliases: at-op] --debug Enable debug logging --color When to colorize output (always, never, auto) diff --git a/cli/tests/test_immutable_commits.rs b/cli/tests/test_immutable_commits.rs index 32d03cd8e..1a5ce85ad 100644 --- a/cli/tests/test_immutable_commits.rs +++ b/cli/tests/test_immutable_commits.rs @@ -42,13 +42,13 @@ fn test_rewrite_immutable_generic() { let stderr = test_env.jj_cmd_failure(&repo_path, &["edit", "main"]); insta::assert_snapshot!(stderr, @r###" Error: Commit c8d4c7ca95d0 is immutable - Hint: Configure the set of immutable commits via `revset-aliases.immutable_heads()`. + Hint: Pass `--ignore-immutable` or configure the set of immutable commits via `revset-aliases.immutable_heads()`. "###); // Cannot rewrite an ancestor of the configured set let stderr = test_env.jj_cmd_failure(&repo_path, &["edit", "main-"]); insta::assert_snapshot!(stderr, @r###" Error: Commit 46a8dc5175be is immutable - Hint: Configure the set of immutable commits via `revset-aliases.immutable_heads()`. + Hint: Pass `--ignore-immutable` or configure the set of immutable commits via `revset-aliases.immutable_heads()`. "###); // Cannot rewrite the root commit even with an empty set of immutable commits test_env.add_config(r#"revset-aliases."immutable_heads()" = "none()""#); @@ -67,6 +67,22 @@ fn test_rewrite_immutable_generic() { For help, see https://github.com/martinvonz/jj/blob/main/docs/config.md. "###); + // Can use --ignore-immutable to override + test_env.add_config(r#"revset-aliases."immutable_heads()" = "main""#); + let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["--ignore-immutable", "edit", "main"]); + insta::assert_snapshot!(stdout, @r###" + "###); + insta::assert_snapshot!(stderr, @r###" + Working copy now at: kkmpptxz c8d4c7ca main | b + Parent commit : qpvuntsm 46a8dc51 a + Added 0 files, modified 1 files, removed 0 files + "###); + // ... but not the root commit + let stderr = test_env.jj_cmd_failure(&repo_path, &["--ignore-immutable", "edit", "root()"]); + insta::assert_snapshot!(stderr, @r###" + Error: The root commit 000000000000 is immutable + "###); + // Mutating the repo works if ref is wrapped in present() test_env.add_config( r#"revset-aliases."immutable_heads()" = "present(branch_that_does_not_exist)""#, @@ -75,9 +91,8 @@ fn test_rewrite_immutable_generic() { insta::assert_snapshot!(stdout, @r###" "###); insta::assert_snapshot!(stderr, @r###" - Working copy now at: kpqxywon dbce15b4 (empty) (no description set) + Working copy now at: wqnwkozp de8b93b4 (empty) (no description set) Parent commit : kkmpptxz c8d4c7ca main | b - Added 0 files, modified 1 files, removed 0 files "###); // Error if we redefine immutable_heads() with an argument @@ -130,31 +145,31 @@ fn test_rewrite_immutable_commands() { let stderr = test_env.jj_cmd_failure(&repo_path, &["abandon", "main"]); insta::assert_snapshot!(stderr, @r###" Error: Commit 406c181c04d8 is immutable - Hint: Configure the set of immutable commits via `revset-aliases.immutable_heads()`. + Hint: Pass `--ignore-immutable` or configure the set of immutable commits via `revset-aliases.immutable_heads()`. "###); // chmod let stderr = test_env.jj_cmd_failure(&repo_path, &["chmod", "-r=main", "x", "file"]); insta::assert_snapshot!(stderr, @r###" Error: Commit 406c181c04d8 is immutable - Hint: Configure the set of immutable commits via `revset-aliases.immutable_heads()`. + Hint: Pass `--ignore-immutable` or configure the set of immutable commits via `revset-aliases.immutable_heads()`. "###); // describe let stderr = test_env.jj_cmd_failure(&repo_path, &["describe", "main"]); insta::assert_snapshot!(stderr, @r###" Error: Commit 406c181c04d8 is immutable - Hint: Configure the set of immutable commits via `revset-aliases.immutable_heads()`. + Hint: Pass `--ignore-immutable` or configure the set of immutable commits via `revset-aliases.immutable_heads()`. "###); // diffedit let stderr = test_env.jj_cmd_failure(&repo_path, &["diffedit", "-r=main"]); insta::assert_snapshot!(stderr, @r###" Error: Commit 406c181c04d8 is immutable - Hint: Configure the set of immutable commits via `revset-aliases.immutable_heads()`. + Hint: Pass `--ignore-immutable` or configure the set of immutable commits via `revset-aliases.immutable_heads()`. "###); // edit let stderr = test_env.jj_cmd_failure(&repo_path, &["edit", "main"]); insta::assert_snapshot!(stderr, @r###" Error: Commit 406c181c04d8 is immutable - Hint: Configure the set of immutable commits via `revset-aliases.immutable_heads()`. + Hint: Pass `--ignore-immutable` or configure the set of immutable commits via `revset-aliases.immutable_heads()`. "###); // move --from let stderr = test_env.jj_cmd_failure(&repo_path, &["move", "--from=main"]); @@ -162,7 +177,7 @@ fn test_rewrite_immutable_commands() { Warning: `jj move` is deprecated; use `jj squash` instead, which is equivalent Warning: `jj move` will be removed in a future version, and this will be a hard error Error: Commit 406c181c04d8 is immutable - Hint: Configure the set of immutable commits via `revset-aliases.immutable_heads()`. + Hint: Pass `--ignore-immutable` or configure the set of immutable commits via `revset-aliases.immutable_heads()`. "###); // move --to let stderr = test_env.jj_cmd_failure(&repo_path, &["move", "--to=main"]); @@ -170,78 +185,78 @@ fn test_rewrite_immutable_commands() { Warning: `jj move` is deprecated; use `jj squash` instead, which is equivalent Warning: `jj move` will be removed in a future version, and this will be a hard error Error: Commit 406c181c04d8 is immutable - Hint: Configure the set of immutable commits via `revset-aliases.immutable_heads()`. + Hint: Pass `--ignore-immutable` or configure the set of immutable commits via `revset-aliases.immutable_heads()`. "###); // new --insert-before let stderr = test_env.jj_cmd_failure(&repo_path, &["new", "--insert-before", "main"]); insta::assert_snapshot!(stderr, @r###" Error: Commit 406c181c04d8 is immutable - Hint: Configure the set of immutable commits via `revset-aliases.immutable_heads()`. + Hint: Pass `--ignore-immutable` or configure the set of immutable commits via `revset-aliases.immutable_heads()`. "###); // 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###" Error: Commit 406c181c04d8 is immutable - Hint: Configure the set of immutable commits via `revset-aliases.immutable_heads()`. + Hint: Pass `--ignore-immutable` or configure the set of immutable commits via `revset-aliases.immutable_heads()`. "###); // parallelize let stderr = test_env.jj_cmd_failure(&repo_path, &["parallelize", "description(b)", "main"]); insta::assert_snapshot!(stderr, @r###" Error: Commit 406c181c04d8 is immutable - Hint: Configure the set of immutable commits via `revset-aliases.immutable_heads()`. + Hint: Pass `--ignore-immutable` or configure the set of immutable commits via `revset-aliases.immutable_heads()`. "###); // rebase -s let stderr = test_env.jj_cmd_failure(&repo_path, &["rebase", "-s=main", "-d=@"]); insta::assert_snapshot!(stderr, @r###" Error: Commit 406c181c04d8 is immutable - Hint: Configure the set of immutable commits via `revset-aliases.immutable_heads()`. + Hint: Pass `--ignore-immutable` or configure the set of immutable commits via `revset-aliases.immutable_heads()`. "###); // rebase -b let stderr = test_env.jj_cmd_failure(&repo_path, &["rebase", "-b=main", "-d=@"]); insta::assert_snapshot!(stderr, @r###" Error: Commit 6e11f430f297 is immutable - Hint: Configure the set of immutable commits via `revset-aliases.immutable_heads()`. + Hint: Pass `--ignore-immutable` or configure the set of immutable commits via `revset-aliases.immutable_heads()`. "###); // rebase -r let stderr = test_env.jj_cmd_failure(&repo_path, &["rebase", "-r=main", "-d=@"]); insta::assert_snapshot!(stderr, @r###" Error: Commit 406c181c04d8 is immutable - Hint: Configure the set of immutable commits via `revset-aliases.immutable_heads()`. + Hint: Pass `--ignore-immutable` or configure the set of immutable commits via `revset-aliases.immutable_heads()`. "###); // resolve let stderr = test_env.jj_cmd_failure(&repo_path, &["resolve", "-r=description(merge)", "file"]); insta::assert_snapshot!(stderr, @r###" Error: Commit 406c181c04d8 is immutable - Hint: Configure the set of immutable commits via `revset-aliases.immutable_heads()`. + Hint: Pass `--ignore-immutable` or configure the set of immutable commits via `revset-aliases.immutable_heads()`. "###); // restore -c let stderr = test_env.jj_cmd_failure(&repo_path, &["restore", "-c=main"]); insta::assert_snapshot!(stderr, @r###" Error: Commit 406c181c04d8 is immutable - Hint: Configure the set of immutable commits via `revset-aliases.immutable_heads()`. + Hint: Pass `--ignore-immutable` or configure the set of immutable commits via `revset-aliases.immutable_heads()`. "###); // restore --to let stderr = test_env.jj_cmd_failure(&repo_path, &["restore", "--to=main"]); insta::assert_snapshot!(stderr, @r###" Error: Commit 406c181c04d8 is immutable - Hint: Configure the set of immutable commits via `revset-aliases.immutable_heads()`. + Hint: Pass `--ignore-immutable` or configure the set of immutable commits via `revset-aliases.immutable_heads()`. "###); // split let stderr = test_env.jj_cmd_failure(&repo_path, &["split", "-r=main"]); insta::assert_snapshot!(stderr, @r###" Error: Commit 406c181c04d8 is immutable - Hint: Configure the set of immutable commits via `revset-aliases.immutable_heads()`. + Hint: Pass `--ignore-immutable` or configure the set of immutable commits via `revset-aliases.immutable_heads()`. "###); // squash let stderr = test_env.jj_cmd_failure(&repo_path, &["squash", "-r=description(b)"]); insta::assert_snapshot!(stderr, @r###" Error: Commit c8d4c7ca95d0 is immutable - Hint: Configure the set of immutable commits via `revset-aliases.immutable_heads()`. + Hint: Pass `--ignore-immutable` or configure the set of immutable commits via `revset-aliases.immutable_heads()`. "###); // unsquash let stderr = test_env.jj_cmd_failure(&repo_path, &["unsquash", "-r=main"]); insta::assert_snapshot!(stderr, @r###" Error: Commit 406c181c04d8 is immutable - Hint: Configure the set of immutable commits via `revset-aliases.immutable_heads()`. + Hint: Pass `--ignore-immutable` or configure the set of immutable commits via `revset-aliases.immutable_heads()`. "###); }