diff --git a/CHANGELOG.md b/CHANGELOG.md index 479dca6a1..cfb6b2ab8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Support for the Watchman filesystem monitor is now bundled by default. Set `core.fsmonitor = "watchman"` in your repo to enable. +* You can now configure the set of immutable commits via + `revset-aliases.immutable_heads()`. For example, set it to + `"remote_branches() | tags()"` to prevent rewriting those those. Their + ancestors are implicitly also immutable. + * `jj op log` now supports `--no-graph`. * Templates now support an additional escape: `\0`. This will output a literal diff --git a/cli/src/cli_util.rs b/cli/src/cli_util.rs index d7524f86c..dca74ddd0 100644 --- a/cli/src/cli_util.rs +++ b/cli/src/cli_util.rs @@ -1259,16 +1259,28 @@ Set which revision the branch points to with `jj branch set {branch_name} -r String { diff --git a/cli/tests/test_branch_command.rs b/cli/tests/test_branch_command.rs index b655dbdcd..325a9e544 100644 --- a/cli/tests/test_branch_command.rs +++ b/cli/tests/test_branch_command.rs @@ -449,6 +449,7 @@ fn test_branch_forget_deleted_or_nonexistent_branch() { #[test] fn test_branch_list_filtered_by_revset() { let test_env = TestEnvironment::default(); + test_env.add_config(r#"revset-aliases."immutable_heads()" = "none()""#); // Initialize remote refs test_env.jj_cmd_success(test_env.env_root(), &["init", "remote", "--git"]); diff --git a/cli/tests/test_edit_command.rs b/cli/tests/test_edit_command.rs index b202a5f53..8921176e5 100644 --- a/cli/tests/test_edit_command.rs +++ b/cli/tests/test_edit_command.rs @@ -110,14 +110,3 @@ fn get_log_output(test_env: &TestEnvironment, cwd: &Path) -> String { let template = r#"commit_id.short() ++ " " ++ description"#; test_env.jj_cmd_success(cwd, &["log", "-T", template]) } - -#[test] -fn test_edit_root() { - let test_env = TestEnvironment::default(); - test_env.jj_cmd_success(test_env.env_root(), &["init", "repo", "--git"]); - let repo_path = test_env.env_root().join("repo"); - let stderr = test_env.jj_cmd_failure(&repo_path, &["edit", "root()"]); - insta::assert_snapshot!(stderr, @r###" - Error: Cannot rewrite commit 000000000000 - "###); -} diff --git a/cli/tests/test_git_fetch.rs b/cli/tests/test_git_fetch.rs index 6512457ff..e3a557747 100644 --- a/cli/tests/test_git_fetch.rs +++ b/cli/tests/test_git_fetch.rs @@ -382,6 +382,7 @@ fn create_trunk2_and_rebase_branches(test_env: &TestEnvironment, repo_path: &Pat #[test] fn test_git_fetch_all() { let test_env = TestEnvironment::default(); + test_env.add_config(r#"revset-aliases."immutable_heads()" = "none()""#); let source_git_repo_path = test_env.env_root().join("source"); let _git_repo = git2::Repository::init(source_git_repo_path.clone()).unwrap(); @@ -505,6 +506,7 @@ fn test_git_fetch_all() { #[test] fn test_git_fetch_some_of_many_branches() { let test_env = TestEnvironment::default(); + test_env.add_config(r#"revset-aliases."immutable_heads()" = "none()""#); let source_git_repo_path = test_env.env_root().join("source"); let _git_repo = git2::Repository::init(source_git_repo_path.clone()).unwrap(); diff --git a/cli/tests/test_git_push.rs b/cli/tests/test_git_push.rs index d6e8c317c..ba41faee1 100644 --- a/cli/tests/test_git_push.rs +++ b/cli/tests/test_git_push.rs @@ -60,6 +60,7 @@ fn test_git_push_nothing() { #[test] fn test_git_push_current_branch() { let (test_env, workspace_root) = set_up(); + test_env.add_config(r#"revset-aliases."immutable_heads()" = "none()""#); // Update some branches. `branch1` is not a current branch, but `branch2` and // `my-branch` are. test_env.jj_cmd_success( @@ -105,6 +106,7 @@ fn test_git_push_current_branch() { #[test] fn test_git_push_parent_branch() { let (test_env, workspace_root) = set_up(); + test_env.add_config(r#"revset-aliases."immutable_heads()" = "none()""#); test_env.jj_cmd_success(&workspace_root, &["edit", "branch1"]); test_env.jj_cmd_success( &workspace_root, @@ -151,6 +153,7 @@ fn test_git_push_matching_branch_unchanged() { #[test] fn test_git_push_other_remote_has_branch() { let (test_env, workspace_root) = set_up(); + test_env.add_config(r#"revset-aliases."immutable_heads()" = "none()""#); // Create another remote (but actually the same) let other_remote_path = test_env .env_root() diff --git a/cli/tests/test_immutable_commits.rs b/cli/tests/test_immutable_commits.rs new file mode 100644 index 000000000..01b57863d --- /dev/null +++ b/cli/tests/test_immutable_commits.rs @@ -0,0 +1,196 @@ +// Copyright 2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::common::TestEnvironment; + +pub mod common; + +#[test] +fn test_rewrite_immutable_generic() { + let test_env = TestEnvironment::default(); + test_env.jj_cmd_success(test_env.env_root(), &["init", "repo", "--git"]); + let repo_path = test_env.env_root().join("repo"); + std::fs::write(repo_path.join("file"), "a").unwrap(); + test_env.jj_cmd_success(&repo_path, &["describe", "-m=a"]); + test_env.jj_cmd_success(&repo_path, &["new", "-m=b"]); + std::fs::write(repo_path.join("file"), "b").unwrap(); + test_env.jj_cmd_success(&repo_path, &["branch", "create", "main"]); + test_env.jj_cmd_success(&repo_path, &["new", "main-", "-m=c"]); + std::fs::write(repo_path.join("file"), "c").unwrap(); + let stdout = test_env.jj_cmd_success(&repo_path, &["log"]); + insta::assert_snapshot!(stdout, @r###" + @ mzvwutvl test.user@example.com 2001-02-03 04:05:12.000 +07:00 78ebd449 + │ c + │ ◉ kkmpptxz test.user@example.com 2001-02-03 04:05:10.000 +07:00 main c8d4c7ca + ├─╯ b + ◉ qpvuntsm test.user@example.com 2001-02-03 04:05:08.000 +07:00 46a8dc51 + │ a + ◉ zzzzzzzz root() 00000000 + "###); + + // 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###" + Error: Commit c8d4c7ca95d0 is immutable + Hint: 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()`. + "###); + // 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()"]); + insta::assert_snapshot!(stderr, @r###" + Error: Commit 000000000000 is immutable + Hint: Configure the set of immutable commits via `revset-aliases.immutable_heads()`. + "###); + // Error if we redefine immutable_heads() with an argument + test_env.add_config(r#"revset-aliases."immutable_heads(foo)" = "none()""#); + let stderr = test_env.jj_cmd_failure(&repo_path, &["edit", "root()"]); + insta::assert_snapshot!(stderr, @r###" + Error: The `revset-aliases.immutable_heads()` function must be declared without arguments. + "###); +} + +#[test] +fn test_rewrite_immutable_commands() { + let test_env = TestEnvironment::default(); + test_env.jj_cmd_success(test_env.env_root(), &["init", "repo", "--git"]); + let repo_path = test_env.env_root().join("repo"); + std::fs::write(repo_path.join("file"), "a").unwrap(); + test_env.jj_cmd_success(&repo_path, &["describe", "-m=a"]); + test_env.jj_cmd_success(&repo_path, &["new", "-m=b"]); + std::fs::write(repo_path.join("file"), "b").unwrap(); + test_env.jj_cmd_success(&repo_path, &["new", "@-", "-m=c"]); + std::fs::write(repo_path.join("file"), "c").unwrap(); + test_env.jj_cmd_success(&repo_path, &["new", "all:visible_heads()", "-m=merge"]); + test_env.jj_cmd_success(&repo_path, &["branch", "create", "main"]); + test_env.jj_cmd_success(&repo_path, &["new", "description(b)"]); + test_env.add_config(r#"revset-aliases."immutable_heads()" = "main""#); + let stdout = test_env.jj_cmd_success(&repo_path, &["log"]); + insta::assert_snapshot!(stdout, @r###" + @ yqosqzyt test.user@example.com 2001-02-03 04:05:13.000 +07:00 3f89addf + │ (empty) (no description set) + │ ◉ mzvwutvl test.user@example.com 2001-02-03 04:05:11.000 +07:00 main d809c5d9 conflict + ╭─┤ (empty) merge + ◉ │ kkmpptxz test.user@example.com 2001-02-03 04:05:10.000 +07:00 c8d4c7ca + │ │ b + │ ◉ zsuskuln test.user@example.com 2001-02-03 04:05:11.000 +07:00 6e11f430 + ├─╯ c + ◉ qpvuntsm test.user@example.com 2001-02-03 04:05:08.000 +07:00 46a8dc51 + │ a + ◉ zzzzzzzz root() 00000000 + "###); + + // abandon + let stderr = test_env.jj_cmd_failure(&repo_path, &["abandon", "main"]); + insta::assert_snapshot!(stderr, @r###" + Error: Commit d809c5d93710 is immutable + Hint: 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 d809c5d93710 is immutable + Hint: 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 d809c5d93710 is immutable + Hint: 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 d809c5d93710 is immutable + Hint: 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 d809c5d93710 is immutable + Hint: 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"]); + insta::assert_snapshot!(stderr, @r###" + Error: Commit d809c5d93710 is immutable + Hint: 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"]); + insta::assert_snapshot!(stderr, @r###" + Error: Commit d809c5d93710 is immutable + Hint: 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 d809c5d93710 is immutable + Hint: 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()`. + "###); + // rebase -r + let stderr = test_env.jj_cmd_failure(&repo_path, &["rebase", "-r=main", "-d=@"]); + insta::assert_snapshot!(stderr, @r###" + Error: Commit d809c5d93710 is immutable + Hint: 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 d809c5d93710 is immutable + Hint: 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 d809c5d93710 is immutable + Hint: 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 d809c5d93710 is immutable + Hint: 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 d809c5d93710 is immutable + Hint: Configure the set of immutable commits via `revset-aliases.immutable_heads()`. + "###); + // squash + let stderr = test_env.jj_cmd_failure(&repo_path, &["squash", "-r=main"]); + insta::assert_snapshot!(stderr, @r###" + Error: Commit d809c5d93710 is immutable + Hint: 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 d809c5d93710 is immutable + Hint: Configure the set of immutable commits via `revset-aliases.immutable_heads()`. + "###); +} diff --git a/cli/tests/test_restore_command.rs b/cli/tests/test_restore_command.rs index 70621c713..6ffc42441 100644 --- a/cli/tests/test_restore_command.rs +++ b/cli/tests/test_restore_command.rs @@ -68,18 +68,12 @@ fn test_restore() { let stdout = test_env.jj_cmd_success(&repo_path, &["diff", "-s", "-r=@-"]); insta::assert_snapshot!(stdout, @""); - // Cannot restore the root revision - let stderr = test_env.jj_cmd_failure(&repo_path, &["restore", "-c=root()"]); - insta::assert_snapshot!(stderr, @r###" - Error: Cannot rewrite commit 000000000000 - "###); - // Can restore this revision from another revision test_env.jj_cmd_success(&repo_path, &["undo"]); let stdout = test_env.jj_cmd_success(&repo_path, &["restore", "--from", "@--"]); insta::assert_snapshot!(stdout, @r###" - Created kkmpptxz 237116e2 (no description set) - Working copy now at: kkmpptxz 237116e2 (no description set) + Created kkmpptxz 1dd6eb63 (no description set) + Working copy now at: kkmpptxz 1dd6eb63 (no description set) Parent commit : rlvkpnrz 1a986a27 (no description set) Added 1 files, modified 0 files, removed 2 files "###); @@ -92,10 +86,10 @@ fn test_restore() { test_env.jj_cmd_success(&repo_path, &["undo"]); let stdout = test_env.jj_cmd_success(&repo_path, &["restore", "--to", "@-"]); insta::assert_snapshot!(stdout, @r###" - Created rlvkpnrz 887f8f96 (no description set) + Created rlvkpnrz ec9d5b59 (no description set) Rebased 1 descendant commits - Working copy now at: kkmpptxz d2725e6e (empty) (no description set) - Parent commit : rlvkpnrz 887f8f96 (no description set) + Working copy now at: kkmpptxz d6f3c681 (empty) (no description set) + Parent commit : rlvkpnrz ec9d5b59 (no description set) "###); let stdout = test_env.jj_cmd_success(&repo_path, &["diff", "-s"]); insta::assert_snapshot!(stdout, @""); @@ -110,10 +104,10 @@ fn test_restore() { test_env.jj_cmd_success(&repo_path, &["undo"]); let stdout = test_env.jj_cmd_success(&repo_path, &["restore", "--from", "@", "--to", "@-"]); insta::assert_snapshot!(stdout, @r###" - Created rlvkpnrz 50c1fe09 (no description set) + Created rlvkpnrz 5f6eb3d5 (no description set) Rebased 1 descendant commits - Working copy now at: kkmpptxz c63aab8e (empty) (no description set) - Parent commit : rlvkpnrz 50c1fe09 (no description set) + Working copy now at: kkmpptxz 525afd5d (empty) (no description set) + Parent commit : rlvkpnrz 5f6eb3d5 (no description set) "###); let stdout = test_env.jj_cmd_success(&repo_path, &["diff", "-s"]); insta::assert_snapshot!(stdout, @""); @@ -128,8 +122,8 @@ fn test_restore() { test_env.jj_cmd_success(&repo_path, &["undo"]); let stdout = test_env.jj_cmd_success(&repo_path, &["restore", "file2", "file3"]); insta::assert_snapshot!(stdout, @r###" - Created kkmpptxz 48f89f52 (no description set) - Working copy now at: kkmpptxz 48f89f52 (no description set) + Created kkmpptxz 569ce73d (no description set) + Working copy now at: kkmpptxz 569ce73d (no description set) Parent commit : rlvkpnrz 1a986a27 (no description set) Added 0 files, modified 1 files, removed 1 files "###); diff --git a/cli/tests/test_templater.rs b/cli/tests/test_templater.rs index 377921c6e..b356d0f5c 100644 --- a/cli/tests/test_templater.rs +++ b/cli/tests/test_templater.rs @@ -21,6 +21,7 @@ pub mod common; #[test] fn test_templater_branches() { let test_env = TestEnvironment::default(); + test_env.add_config(r#"revset-aliases."immutable_heads()" = "none()""#); test_env.jj_cmd_success(test_env.env_root(), &["init", "--git", "origin"]); let origin_path = test_env.env_root().join("origin"); diff --git a/cli/tests/test_undo.rs b/cli/tests/test_undo.rs index aa353af69..291e4392e 100644 --- a/cli/tests/test_undo.rs +++ b/cli/tests/test_undo.rs @@ -51,6 +51,7 @@ fn test_undo_rewrite_with_child() { #[test] fn test_git_push_undo() { let test_env = TestEnvironment::default(); + test_env.add_config(r#"revset-aliases."immutable_heads()" = "none()""#); let git_repo_path = test_env.env_root().join("git-repo"); git2::Repository::init_bare(git_repo_path).unwrap(); test_env.jj_cmd_success(test_env.env_root(), &["git", "clone", "git-repo", "repo"]); @@ -122,6 +123,7 @@ fn test_git_push_undo() { #[test] fn test_git_push_undo_with_import() { let test_env = TestEnvironment::default(); + test_env.add_config(r#"revset-aliases."immutable_heads()" = "none()""#); let git_repo_path = test_env.env_root().join("git-repo"); git2::Repository::init_bare(git_repo_path).unwrap(); test_env.jj_cmd_success(test_env.env_root(), &["git", "clone", "git-repo", "repo"]); @@ -198,6 +200,7 @@ fn test_git_push_undo_with_import() { #[test] fn test_git_push_undo_colocated() { let test_env = TestEnvironment::default(); + test_env.add_config(r#"revset-aliases."immutable_heads()" = "none()""#); let git_repo_path = test_env.env_root().join("git-repo"); git2::Repository::init_bare(git_repo_path.clone()).unwrap(); let repo_path = test_env.env_root().join("clone"); @@ -274,6 +277,7 @@ fn test_git_push_undo_colocated() { #[test] fn test_git_push_undo_repo_only() { let test_env = TestEnvironment::default(); + test_env.add_config(r#"revset-aliases."immutable_heads()" = "none()""#); let git_repo_path = test_env.env_root().join("git-repo"); git2::Repository::init_bare(git_repo_path).unwrap(); test_env.jj_cmd_success(test_env.env_root(), &["git", "clone", "git-repo", "repo"]); diff --git a/docs/config.md b/docs/config.md index 18e10ac4b..54b89c0b2 100644 --- a/docs/config.md +++ b/docs/config.md @@ -176,13 +176,29 @@ diff-args = ["--color=always", "$left", "$right"] - `$left` and `$right` are replaced with the paths to the left and right directories to diff respectively. +### Set of immutable commits + +You can configure the set of immutable commits via `revset-aliases."immutable_heads()"`. +The default set of immutable heads is `trunk() | tags()`. For example, to +prevent rewriting commits on `main@origin` and commits authored by other +users: + +```toml +# The `main.. &` bit is an optimization to scan for non-`mine()` commits only +# among commits that are not in `main`. +revset-aliases."immutable_heads()" = "main@origin | (main@origin.. & ~mine())" +``` + +Ancestors of the configured set are also immutable. The root commit always +immutable even if the set is empty. + ### Default revisions to log You can configure the revisions `jj log` without `-r` should show. ```toml -# Show commits that are not in `main` -revsets.log = "main.." +# Show commits that are not in `main@origin` +revsets.log = "main@origin.." ``` ### Graph style diff --git a/lib/src/revset.rs b/lib/src/revset.rs index 246c70c63..06dfe52ea 100644 --- a/lib/src/revset.rs +++ b/lib/src/revset.rs @@ -783,11 +783,11 @@ impl RevsetAliasesMap { Ok(()) } - fn get_symbol(&self, name: &str) -> Option<&str> { + pub fn get_symbol(&self, name: &str) -> Option<&str> { self.symbol_aliases.get(name).map(|defn| defn.as_ref()) } - fn get_function(&self, name: &str) -> Option<(&[String], &str)> { + pub fn get_function(&self, name: &str) -> Option<(&[String], &str)> { self.function_aliases .get(name) .map(|(params, defn)| (params.as_ref(), defn.as_ref()))