diff --git a/CHANGELOG.md b/CHANGELOG.md index 91173f954..5c3c7f9e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,6 +65,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Added `latest(x[, n])` revset function to select the latest `n` commits. +* Added `conflict()` revset function to select commits with conflicts. + * `jj squash` AKA `jj amend` now accepts a `--message` option to set the description of the squashed commit on the command-line. diff --git a/docs/revsets.md b/docs/revsets.md index 9892b92c4..2aceac64d 100644 --- a/docs/revsets.md +++ b/docs/revsets.md @@ -120,6 +120,7 @@ revsets (expressions) as arguments. user modifications and `root`. * `file(pattern..)`: Commits modifying the paths specified by the `pattern..`. Paths are relative to the directory `jj` was invoked from. +* `conflict()`: Commits with conflicts. * `present(x)`: Same as `x`, but evaluated to `none()` if any of the commits in `x` doesn't exist (e.g. is an unknown branch name.) diff --git a/lib/src/default_revset_engine.rs b/lib/src/default_revset_engine.rs index d3b717780..6cdb6a547 100644 --- a/lib/src/default_revset_engine.rs +++ b/lib/src/default_revset_engine.rs @@ -944,6 +944,10 @@ fn build_predicate_fn<'index>( has_diff_from_parent(&store, index, entry, matcher.as_ref()) }) } + RevsetFilterPredicate::HasConflict => pure_predicate_fn(move |entry| { + let commit = store.get_commit(&entry.commit_id()).unwrap(); + commit.tree().has_conflict() + }), } } diff --git a/lib/src/revset.rs b/lib/src/revset.rs index 10933ed0f..3d659eb6a 100644 --- a/lib/src/revset.rs +++ b/lib/src/revset.rs @@ -205,6 +205,8 @@ pub enum RevsetFilterPredicate { Committer(String), /// Commits modifying the paths specified by the pattern. File(Option>), // TODO: embed matcher expression? + /// Commits with conflicts + HasConflict, } #[derive(Debug, PartialEq, Eq, Clone)] @@ -928,6 +930,10 @@ static BUILTIN_FUNCTION_MAP: Lazy> = Lazy: )) } }); + map.insert("conflict", |name, arguments_pair, _state| { + expect_no_arguments(name, arguments_pair)?; + Ok(RevsetExpression::filter(RevsetFilterPredicate::HasConflict)) + }); map.insert("present", |name, arguments_pair, state| { let arg = expect_one_argument(name, arguments_pair)?; let expression = parse_expression_rule(arg.into_inner(), state)?; diff --git a/lib/tests/test_revset.rs b/lib/tests/test_revset.rs index fdf91952a..b927050ce 100644 --- a/lib/tests/test_revset.rs +++ b/lib/tests/test_revset.rs @@ -29,6 +29,7 @@ use jujutsu_lib::revset::{ RevsetResolutionError, RevsetWorkspaceContext, }; use jujutsu_lib::settings::GitSettings; +use jujutsu_lib::tree::merge_trees; use jujutsu_lib::workspace::Workspace; use test_case::test_case; use testutils::{ @@ -2142,6 +2143,49 @@ fn test_evaluate_expression_file(use_git: bool) { ); } +#[test_case(false ; "local backend")] +#[test_case(true ; "git backend")] +fn test_evaluate_expression_conflict(use_git: bool) { + let settings = testutils::user_settings(); + let test_workspace = TestWorkspace::init(&settings, use_git); + let repo = &test_workspace.repo; + + let mut tx = repo.start_transaction(&settings, "test"); + let mut_repo = tx.mut_repo(); + + // Create a few trees, including one with a conflict in `file1` + let file_path1 = RepoPath::from_internal_string("file1"); + let file_path2 = RepoPath::from_internal_string("file2"); + let tree1 = testutils::create_tree(repo, &[(&file_path1, "1"), (&file_path2, "1")]); + let tree2 = testutils::create_tree(repo, &[(&file_path1, "2"), (&file_path2, "2")]); + let tree3 = testutils::create_tree(repo, &[(&file_path1, "3"), (&file_path2, "1")]); + let tree_id4 = merge_trees(&tree2, &tree1, &tree3).unwrap(); + let tree4 = mut_repo + .store() + .get_tree(&RepoPath::root(), &tree_id4) + .unwrap(); + + let mut create_commit = |parent_ids, tree_id| { + mut_repo + .new_commit(&settings, parent_ids, tree_id) + .write() + .unwrap() + }; + let commit1 = create_commit( + vec![repo.store().root_commit_id().clone()], + tree1.id().clone(), + ); + let commit2 = create_commit(vec![commit1.id().clone()], tree2.id().clone()); + let commit3 = create_commit(vec![commit2.id().clone()], tree3.id().clone()); + let commit4 = create_commit(vec![commit3.id().clone()], tree4.id().clone()); + + // Only commit4 has a conflict + assert_eq!( + resolve_commit_ids(mut_repo, "conflict()"), + vec![commit4.id().clone()] + ); +} + #[test] fn test_reverse_graph_iterator() { let settings = testutils::user_settings(); diff --git a/testing/bench-revsets-git.txt b/testing/bench-revsets-git.txt index cd724154a..e901c9b3e 100644 --- a/testing/bench-revsets-git.txt +++ b/testing/bench-revsets-git.txt @@ -43,6 +43,7 @@ tags()+ # Filter that doesn't read commit object merges() ~merges() -# Files are unbearably slow, so only filter within small set +# These are unbearably slow, so only filter within small set file(Makefile) & v1.0.0..v1.2.0 empty() & v1.0.0..v1.2.0 +conflict() & v1.0.0..v1.2.0