1448 lines · 49.6 KB · Rust
RawDownload
// 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 futures::StreamExt as _;use itertools::Itertools as _;use jj_lib::backend::CommitId;use jj_lib::backend::CopyRecord;use jj_lib::backend::FileId;use jj_lib::backend::MergedTreeId;use jj_lib::backend::TreeValue;use jj_lib::copies::CopiesTreeDiffEntryPath;use jj_lib::copies::CopyOperation;use jj_lib::copies::CopyRecords;use jj_lib::files;use jj_lib::matchers::EverythingMatcher;use jj_lib::matchers::FilesMatcher;use jj_lib::matchers::Matcher;use jj_lib::matchers::PrefixMatcher;use jj_lib::merge::Diff;use jj_lib::merge::Merge;use jj_lib::merge::MergedTreeValue;use jj_lib::merged_tree::MergedTree;use jj_lib::merged_tree::MergedTreeBuilder;use jj_lib::merged_tree::TreeDiffEntry;use jj_lib::merged_tree::TreeDiffIterator;use jj_lib::merged_tree::TreeDiffStreamImpl;use jj_lib::repo::Repo as _;use jj_lib::repo_path::RepoPath;use jj_lib::repo_path::RepoPathBuf;use pollster::FutureExt as _;use pretty_assertions::assert_eq;use testutils::TestRepo;use testutils::create_single_tree;use testutils::create_tree;use testutils::repo_path;use testutils::repo_path_buf;use testutils::repo_path_component; fn diff_entry_tuple(diff: TreeDiffEntry) -> (RepoPathBuf, (MergedTreeValue, MergedTreeValue)) {    let values = diff.values.unwrap();    (diff.path, (values.before, values.after))} fn diff_stream_equals_iter(tree1: &MergedTree, tree2: &MergedTree, matcher: &dyn Matcher) {    let trees1 = tree1.as_merge();    let trees2 = tree2.as_merge();    let iter_diff: Vec<_> = TreeDiffIterator::new(trees1, trees2, matcher)        .map(|diff| (diff.path, diff.values.unwrap()))        .collect();    let max_concurrent_reads = 10;    tree1.store().clear_caches();    let stream_diff: Vec<_> =        TreeDiffStreamImpl::new(trees1, trees2, matcher, max_concurrent_reads)            .map(|diff| (diff.path, diff.values.unwrap()))            .collect()            .block_on();    assert_eq!(stream_diff, iter_diff);} /// Test that a tree built with no changes on top of an add/add conflict gets/// resolved.#[test]fn test_merged_tree_builder_resolves_conflict() {    let test_repo = TestRepo::init();    let repo = &test_repo.repo;    let store = repo.store();     let path1 = repo_path("dir/file");    let tree1 = create_single_tree(repo, &[(path1, "foo")]);    let tree2 = create_single_tree(repo, &[(path1, "bar")]);    let tree3 = create_single_tree(repo, &[(path1, "bar")]);     let base_tree_id = MergedTreeId::new(Merge::from_removes_adds(        [tree1.id().clone()],        [tree2.id().clone(), tree3.id().clone()],    ));    let tree_builder = MergedTreeBuilder::new(base_tree_id);    let tree_id = tree_builder.write_tree(store).unwrap();    assert_eq!(tree_id, MergedTreeId::resolved(tree2.id().clone()));} #[test]fn test_path_value_and_entries() {    let test_repo = TestRepo::init();    let repo = &test_repo.repo;     // Create a MergedTree    let resolved_file_path = repo_path("dir1/subdir/resolved");    let resolved_dir_path = &resolved_file_path.parent().unwrap();    let conflicted_file_path = repo_path("dir2/conflicted");    let missing_path = repo_path("dir2/missing_file");    let modify_delete_path = repo_path("dir2/modify_delete");    let file_dir_conflict_path = repo_path("file_dir");    let file_dir_conflict_sub_path = repo_path("file_dir/file");    let tree1 = create_single_tree(        repo,        &[            (resolved_file_path, "unchanged"),            (conflicted_file_path, "1"),            (modify_delete_path, "1"),            (file_dir_conflict_path, "1"),        ],    );    let tree2 = create_single_tree(        repo,        &[            (resolved_file_path, "unchanged"),            (conflicted_file_path, "2"),            (modify_delete_path, "2"),            (file_dir_conflict_path, "2"),        ],    );    let tree3 = create_single_tree(        repo,        &[            (resolved_file_path, "unchanged"),            (conflicted_file_path, "3"),            // No modify_delete_path in this tree            (file_dir_conflict_sub_path, "1"),        ],    );    let merged_tree = MergedTree::new(Merge::from_removes_adds(        vec![tree1.clone()],        vec![tree2.clone(), tree3.clone()],    ));     // Get the root tree    assert_eq!(        merged_tree.path_value(RepoPath::root()).unwrap(),        Merge::from_removes_adds(            vec![Some(TreeValue::Tree(tree1.id().clone()))],            vec![                Some(TreeValue::Tree(tree2.id().clone())),                Some(TreeValue::Tree(tree3.id().clone())),            ]        )    );    // Get file path without conflict    assert_eq!(        merged_tree.path_value(resolved_file_path).unwrap(),        Merge::resolved(tree1.path_value(resolved_file_path).unwrap()),    );    // Get directory path without conflict    assert_eq!(        merged_tree.path_value(resolved_dir_path).unwrap(),        Merge::resolved(tree1.path_value(resolved_dir_path).unwrap()),    );    // Get missing path    assert_eq!(        merged_tree.path_value(missing_path).unwrap(),        Merge::absent()    );    // Get modify/delete conflict (some None values)    assert_eq!(        merged_tree.path_value(modify_delete_path).unwrap(),        Merge::from_removes_adds(            vec![tree1.path_value(modify_delete_path).unwrap()],            vec![tree2.path_value(modify_delete_path).unwrap(), None]        ),    );    // Get file/dir conflict path    assert_eq!(        merged_tree.path_value(file_dir_conflict_path).unwrap(),        Merge::from_removes_adds(            vec![tree1.path_value(file_dir_conflict_path).unwrap()],            vec![                tree2.path_value(file_dir_conflict_path).unwrap(),                tree3.path_value(file_dir_conflict_path).unwrap()            ]        ),    );    // Get file inside file/dir conflict    // There is a conflict in the parent directory, so it is considered to not be a    // directory in the merged tree, making the file hidden until the directory    // conflict has been resolved.    assert_eq!(        merged_tree.path_value(file_dir_conflict_sub_path).unwrap(),        Merge::absent(),    );     // Test entries()    let actual_entries = merged_tree        .entries()        .map(|(path, result)| (path, result.unwrap()))        .collect_vec();    // missing_path, resolved_dir_path, and file_dir_conflict_sub_path should not    // appear    let expected_entries = [        resolved_file_path,        conflicted_file_path,        modify_delete_path,        file_dir_conflict_path,    ]    .iter()    .sorted()    .map(|&path| (path.to_owned(), merged_tree.path_value(path).unwrap()))    .collect_vec();    assert_eq!(actual_entries, expected_entries);     let actual_entries = merged_tree        .entries_matching(&FilesMatcher::new([            &resolved_file_path,            &modify_delete_path,            &file_dir_conflict_sub_path,        ]))        .map(|(path, result)| (path, result.unwrap()))        .collect_vec();    let expected_entries = [resolved_file_path, modify_delete_path]        .iter()        .sorted()        .map(|&path| (path.to_owned(), merged_tree.path_value(path).unwrap()))        .collect_vec();    assert_eq!(actual_entries, expected_entries);} #[test]fn test_resolve_success() {    let test_repo = TestRepo::init();    let repo = &test_repo.repo;     let unchanged_path = repo_path("unchanged");    let trivial_file_path = repo_path("trivial-file");    let trivial_hunk_path = repo_path("trivial-hunk");    let both_added_dir_path = repo_path("added-dir");    let both_added_dir_file1_path = &both_added_dir_path.join(repo_path_component("file1"));    let both_added_dir_file2_path = &both_added_dir_path.join(repo_path_component("file2"));    let emptied_dir_path = repo_path("to-become-empty");    let emptied_dir_file1_path = &emptied_dir_path.join(repo_path_component("file1"));    let emptied_dir_file2_path = &emptied_dir_path.join(repo_path_component("file2"));    let base1 = create_single_tree(        repo,        &[            (unchanged_path, "unchanged"),            (trivial_file_path, "base1"),            (trivial_hunk_path, "line1\nline2\nline3\n"),            (emptied_dir_file1_path, "base1"),            (emptied_dir_file2_path, "base1"),        ],    );    let side1 = create_single_tree(        repo,        &[            (unchanged_path, "unchanged"),            (trivial_file_path, "base1"),            (trivial_hunk_path, "line1 side1\nline2\nline3\n"),            (both_added_dir_file1_path, "side1"),            (emptied_dir_file2_path, "base1"),        ],    );    let side2 = create_single_tree(        repo,        &[            (unchanged_path, "unchanged"),            (trivial_file_path, "side2"),            (trivial_hunk_path, "line1\nline2\nline3 side2\n"),            (both_added_dir_file2_path, "side2"),            (emptied_dir_file1_path, "base1"),        ],    );    let expected = create_tree(        repo,        &[            (unchanged_path, "unchanged"),            (trivial_file_path, "side2"),            (trivial_hunk_path, "line1 side1\nline2\nline3 side2\n"),            (both_added_dir_file1_path, "side1"),            (both_added_dir_file2_path, "side2"),        ],    );     let tree = MergedTree::new(Merge::from_removes_adds(vec![base1], vec![side1, side2]));    let resolved_tree = tree.resolve().block_on().unwrap();    assert!(resolved_tree.as_merge().is_resolved());    assert_eq!(        resolved_tree,        expected,        "actual entries: {:#?}, expected entries {:#?}",        resolved_tree.entries().collect_vec(),        expected.entries().collect_vec()    );} #[test]fn test_resolve_root_becomes_empty() {    let test_repo = TestRepo::init();    let repo = &test_repo.repo;    let store = repo.store();     let path1 = repo_path("dir1/file");    let path2 = repo_path("dir2/file");    let base1 = create_single_tree(repo, &[(path1, "base1"), (path2, "base1")]);    let side1 = create_single_tree(repo, &[(path2, "base1")]);    let side2 = create_single_tree(repo, &[(path1, "base1")]);     let tree = MergedTree::new(Merge::from_removes_adds(vec![base1], vec![side1, side2]));    let resolved = tree.resolve().block_on().unwrap();    assert_eq!(resolved.id(), store.empty_merged_tree_id());} #[test]fn test_resolve_with_conflict() {    let test_repo = TestRepo::init();    let repo = &test_repo.repo;     // The trivial conflict should be resolved but the non-trivial should not (and    // cannot)    let trivial_path = repo_path("dir1/trivial");    let conflict_path = repo_path("dir2/file_conflict");    let base1 = create_single_tree(repo, &[(trivial_path, "base1"), (conflict_path, "base1")]);    let side1 = create_single_tree(repo, &[(trivial_path, "side1"), (conflict_path, "side1")]);    let side2 = create_single_tree(repo, &[(trivial_path, "base1"), (conflict_path, "side2")]);    let expected_base1 =        create_single_tree(repo, &[(trivial_path, "side1"), (conflict_path, "base1")]);    let expected_side1 =        create_single_tree(repo, &[(trivial_path, "side1"), (conflict_path, "side1")]);    let expected_side2 =        create_single_tree(repo, &[(trivial_path, "side1"), (conflict_path, "side2")]);     let tree = MergedTree::new(Merge::from_removes_adds(vec![base1], vec![side1, side2]));    let resolved_tree = tree.resolve().block_on().unwrap();    assert_eq!(        resolved_tree,        MergedTree::new(Merge::from_removes_adds(            vec![expected_base1],            vec![expected_side1, expected_side2]        ))    );} #[test]fn test_resolve_with_conflict_containing_empty_subtree() {    let test_repo = TestRepo::init();    let repo = &test_repo.repo;     // Since "dir" in side2 is absent, side2's root tree should be empty as    // well. If it were added to the root tree, side2.id() would differ.    let conflict_path = repo_path("dir/file_conflict");    let base1 = create_single_tree(repo, &[(conflict_path, "base1")]);    let side1 = create_single_tree(repo, &[(conflict_path, "side1")]);    let side2 = create_single_tree(repo, &[]);     let tree = MergedTree::new(Merge::from_removes_adds(vec![base1], vec![side1, side2]));    let resolved_tree = tree.clone().resolve().block_on().unwrap();    assert_eq!(resolved_tree, tree);} #[test]fn test_conflict_iterator() {    let test_repo = TestRepo::init();    let repo = &test_repo.repo;     let unchanged_path = repo_path("dir/subdir/unchanged");    let trivial_path = repo_path("dir/subdir/trivial");    let trivial_hunk_path = repo_path("dir/non_trivial");    let file_conflict_path = repo_path("dir/subdir/file_conflict");    let modify_delete_path = repo_path("dir/subdir/modify_delete");    let same_add_path = repo_path("dir/subdir/same_add");    let different_add_path = repo_path("dir/subdir/different_add");    let dir_file_path = repo_path("dir/subdir/dir_file");    let added_dir_path = repo_path("dir/new_dir");    let modify_delete_dir_path = repo_path("dir/modify_delete_dir");    let base1 = create_single_tree(        repo,        &[            (unchanged_path, "unchanged"),            (trivial_path, "base"),            (trivial_hunk_path, "line1\nline2\nline3\n"),            (file_conflict_path, "base"),            (modify_delete_path, "base"),            // no same_add_path            // no different_add_path            (dir_file_path, "base"),            // no added_dir_path            (                &modify_delete_dir_path.join(repo_path_component("base")),                "base",            ),        ],    );    let side1 = create_single_tree(        repo,        &[            (unchanged_path, "unchanged"),            (trivial_path, "base"),            (file_conflict_path, "side1"),            (trivial_hunk_path, "line1 side1\nline2\nline3\n"),            (modify_delete_path, "modified"),            (same_add_path, "same"),            (different_add_path, "side1"),            (dir_file_path, "side1"),            (&added_dir_path.join(repo_path_component("side1")), "side1"),            (                &modify_delete_dir_path.join(repo_path_component("side1")),                "side1",            ),        ],    );    let side2 = create_single_tree(        repo,        &[            (unchanged_path, "unchanged"),            (trivial_path, "side2"),            (file_conflict_path, "side2"),            (trivial_hunk_path, "line1\nline2\nline3 side2\n"),            // no modify_delete_path            (same_add_path, "same"),            (different_add_path, "side2"),            (&dir_file_path.join(repo_path_component("dir")), "new"),            (&added_dir_path.join(repo_path_component("side2")), "side2"),            // no modify_delete_dir_path        ],    );     let tree = MergedTree::new(Merge::from_removes_adds(        vec![base1.clone()],        vec![side1.clone(), side2.clone()],    ));    let conflicts = tree        .conflicts()        .map(|(path, conflict)| (path, conflict.unwrap()))        .collect_vec();    let conflict_at = |path: &RepoPath| {        Merge::from_removes_adds(            vec![base1.path_value(path).unwrap()],            vec![                side1.path_value(path).unwrap(),                side2.path_value(path).unwrap(),            ],        )    };    // We initially also get a conflict in trivial_hunk_path because we had    // forgotten to resolve conflicts    assert_eq!(        conflicts,        vec![            (trivial_hunk_path.to_owned(), conflict_at(trivial_hunk_path)),            (                different_add_path.to_owned(),                conflict_at(different_add_path)            ),            (dir_file_path.to_owned(), conflict_at(dir_file_path)),            (                file_conflict_path.to_owned(),                conflict_at(file_conflict_path)            ),            (                modify_delete_path.to_owned(),                conflict_at(modify_delete_path)            ),        ]    );     // After we resolve conflicts, there are only non-trivial conflicts left    let tree = tree.resolve().block_on().unwrap();    let conflicts = tree        .conflicts()        .map(|(path, conflict)| (path, conflict.unwrap()))        .collect_vec();    assert_eq!(        conflicts,        vec![            (                different_add_path.to_owned(),                conflict_at(different_add_path)            ),            (dir_file_path.to_owned(), conflict_at(dir_file_path)),            (                file_conflict_path.to_owned(),                conflict_at(file_conflict_path)            ),            (                modify_delete_path.to_owned(),                conflict_at(modify_delete_path)            ),        ]    );} #[test]fn test_conflict_iterator_higher_arity() {    let test_repo = TestRepo::init();    let repo = &test_repo.repo;     let two_sided_path = repo_path("dir/2-sided");    let three_sided_path = repo_path("dir/3-sided");    let base1 = create_single_tree(        repo,        &[(two_sided_path, "base1"), (three_sided_path, "base1")],    );

Lines 1–500 of 1448.