1569 lines · 56.0 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::MergeResult;use jj_lib::matchers::EverythingMatcher;use jj_lib::matchers::FilesMatcher;use jj_lib::matchers::Matcher;use jj_lib::matchers::PrefixMatcher;use jj_lib::merge::Merge;use jj_lib::merge::MergeBuilder;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 jj_lib::repo_path::RepoPathComponent;use pollster::FutureExt as _;use pretty_assertions::assert_eq;use testutils::create_single_tree;use testutils::write_file;use testutils::TestRepo; fn file_value(file_id: &FileId) -> TreeValue {    TreeValue::File {        id: file_id.clone(),        executable: false,    }} fn diff_entry_tuple(diff: TreeDiffEntry) -> (RepoPathBuf, (MergedTreeValue, MergedTreeValue)) {    (diff.path, diff.values.unwrap())} 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;    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]fn test_from_legacy_tree() {    let test_repo = TestRepo::init();    let repo = &test_repo.repo;    let store = repo.store();     let mut tree_builder = store.tree_builder(repo.store().empty_tree_id().clone());     // file1: regular file without conflicts    let file1_path = RepoPath::from_internal_string("no_conflict");    let file1_id = write_file(store.as_ref(), file1_path, "foo");    tree_builder.set(file1_path.to_owned(), file_value(&file1_id));     // file2: 3-way conflict    let file2_path = RepoPath::from_internal_string("3way");    let file2_v1_id = write_file(store.as_ref(), file2_path, "file2_v1");    let file2_v2_id = write_file(store.as_ref(), file2_path, "file2_v2");    let file2_v3_id = write_file(store.as_ref(), file2_path, "file2_v3");    let file2_conflict = Merge::from_removes_adds(        vec![Some(file_value(&file2_v1_id))],        vec![            Some(file_value(&file2_v2_id)),            Some(file_value(&file2_v3_id)),        ],    );    let file2_conflict_id = store.write_conflict(file2_path, &file2_conflict).unwrap();    tree_builder.set(        file2_path.to_owned(),        TreeValue::Conflict(file2_conflict_id),    );     // file3: modify/delete conflict    let file3_path = RepoPath::from_internal_string("modify_delete");    let file3_v1_id = write_file(store.as_ref(), file3_path, "file3_v1");    let file3_v2_id = write_file(store.as_ref(), file3_path, "file3_v2");    let file3_conflict = Merge::from_removes_adds(        vec![Some(file_value(&file3_v1_id))],        vec![Some(file_value(&file3_v2_id)), None],    );    let file3_conflict_id = store.write_conflict(file3_path, &file3_conflict).unwrap();    tree_builder.set(        file3_path.to_owned(),        TreeValue::Conflict(file3_conflict_id),    );     // file4: add/add conflict    let file4_path = RepoPath::from_internal_string("add_add");    let file4_v1_id = write_file(store.as_ref(), file4_path, "file4_v1");    let file4_v2_id = write_file(store.as_ref(), file4_path, "file4_v2");    let file4_conflict = Merge::from_removes_adds(        vec![None],        vec![            Some(file_value(&file4_v1_id)),            Some(file_value(&file4_v2_id)),        ],    );    let file4_conflict_id = store.write_conflict(file4_path, &file4_conflict).unwrap();    tree_builder.set(        file4_path.to_owned(),        TreeValue::Conflict(file4_conflict_id),    );     // file5: 5-way conflict    let file5_path = RepoPath::from_internal_string("5way");    let file5_v1_id = write_file(store.as_ref(), file5_path, "file5_v1");    let file5_v2_id = write_file(store.as_ref(), file5_path, "file5_v2");    let file5_v3_id = write_file(store.as_ref(), file5_path, "file5_v3");    let file5_v4_id = write_file(store.as_ref(), file5_path, "file5_v4");    let file5_v5_id = write_file(store.as_ref(), file5_path, "file5_v5");    let file5_conflict = Merge::from_removes_adds(        vec![            Some(file_value(&file5_v1_id)),            Some(file_value(&file5_v2_id)),        ],        vec![            Some(file_value(&file5_v3_id)),            Some(file_value(&file5_v4_id)),            Some(file_value(&file5_v5_id)),        ],    );    let file5_conflict_id = store.write_conflict(file5_path, &file5_conflict).unwrap();    tree_builder.set(        file5_path.to_owned(),        TreeValue::Conflict(file5_conflict_id),    );     // dir1: directory without conflicts    let dir1_basename = RepoPathComponent::new("dir1");    let dir1_filename = RepoPath::root()        .join(dir1_basename)        .join(RepoPathComponent::new("file"));    let dir1_filename_id = write_file(store.as_ref(), &dir1_filename, "file5_v2");    tree_builder.set(dir1_filename.clone(), file_value(&dir1_filename_id));     let tree_id = tree_builder.write_tree().unwrap();    let tree = store.get_tree(RepoPathBuf::root(), &tree_id).unwrap();     let merged_tree = MergedTree::from_legacy_tree(tree.clone()).unwrap();    assert_eq!(        merged_tree.value(RepoPathComponent::new("missing")),        Merge::absent()    );    // file1: regular file without conflicts    assert_eq!(        merged_tree            .value(file1_path.components().next().unwrap())            .cloned(),        Merge::normal(TreeValue::File {            id: file1_id.clone(),            executable: false,        })    );    // file2: 3-way conflict    assert_eq!(        merged_tree            .value(file2_path.components().next().unwrap())            .cloned(),        Merge::from_removes_adds(            vec![Some(file_value(&file2_v1_id)), None],            vec![                Some(file_value(&file2_v2_id)),                Some(file_value(&file2_v3_id)),                None,            ],        )    );    // file3: modify/delete conflict    assert_eq!(        merged_tree            .value(file3_path.components().next().unwrap())            .cloned(),        Merge::from_removes_adds(            vec![Some(file_value(&file3_v1_id)), None],            vec![Some(file_value(&file3_v2_id)), None, None],        )    );    // file4: add/add conflict    assert_eq!(        merged_tree            .value(file4_path.components().next().unwrap())            .cloned(),        Merge::from_removes_adds(            vec![None, None],            vec![                Some(file_value(&file4_v1_id)),                Some(file_value(&file4_v2_id)),                None            ],        )    );    // file5: 5-way conflict    assert_eq!(        merged_tree            .value(file5_path.components().next().unwrap())            .cloned(),        Merge::from_removes_adds(            vec![                Some(file_value(&file5_v1_id)),                Some(file_value(&file5_v2_id)),            ],            vec![                Some(file_value(&file5_v3_id)),                Some(file_value(&file5_v4_id)),                Some(file_value(&file5_v5_id)),            ],        )    );    // file6: directory without conflicts    assert_eq!(        merged_tree.value(dir1_basename),        Merge::resolved(tree.value(dir1_basename))    );     // Create the merged tree by starting from an empty merged tree and adding    // entries from the merged tree we created before    let empty_merged_id_builder: MergeBuilder<_> = std::iter::repeat_n(store.empty_tree_id(), 5)        .cloned()        .collect();    let empty_merged_id = MergedTreeId::Merge(empty_merged_id_builder.build());    let mut tree_builder = MergedTreeBuilder::new(empty_merged_id);    for (path, value) in merged_tree.entries() {        tree_builder.set_or_remove(path, value.unwrap());    }    let recreated_merged_id = tree_builder.write_tree(store).unwrap();    assert_eq!(recreated_merged_id, merged_tree.id());     // Create the merged tree by adding the same (variable-arity) entries as we    // added to the single-tree TreeBuilder.    let mut tree_builder = MergedTreeBuilder::new(MergedTreeId::Merge(Merge::resolved(        store.empty_tree_id().clone(),    )));    // Add the entries out of order, so we test both increasing and reducing the    // arity (going up from 1-way to 3-way to 5-way, then to 3-way again)    tree_builder.set_or_remove(file1_path.to_owned(), Merge::normal(file_value(&file1_id)));    tree_builder.set_or_remove(file2_path.to_owned(), file2_conflict);    tree_builder.set_or_remove(file5_path.to_owned(), file5_conflict);    tree_builder.set_or_remove(file3_path.to_owned(), file3_conflict);    tree_builder.set_or_remove(file4_path.to_owned(), file4_conflict);    tree_builder.set_or_remove(        dir1_filename.clone(),        Merge::normal(file_value(&dir1_filename_id)),    );    let recreated_merged_id = tree_builder.write_tree(store).unwrap();    assert_eq!(recreated_merged_id, merged_tree.id());} /// 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 = RepoPathBuf::from_internal_string("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::Merge(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 = RepoPath::from_internal_string("dir1/subdir/resolved");    let resolved_dir_path = &resolved_file_path.parent().unwrap();    let conflicted_file_path = RepoPath::from_internal_string("dir2/conflicted");    let missing_path = RepoPath::from_internal_string("dir2/missing_file");    let modify_delete_path = RepoPath::from_internal_string("dir2/modify_delete");    let file_dir_conflict_path = RepoPath::from_internal_string("file_dir");    let file_dir_conflict_sub_path = RepoPath::from_internal_string("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, but this file is still resolved    assert_eq!(        merged_tree.path_value(file_dir_conflict_sub_path).unwrap(),        Merge::resolved(tree3.path_value(file_dir_conflict_sub_path).unwrap()),    );     // 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 = RepoPath::from_internal_string("unchanged");    let trivial_file_path = RepoPath::from_internal_string("trivial-file");    let trivial_hunk_path = RepoPath::from_internal_string("trivial-hunk");    let both_added_dir_path = RepoPath::from_internal_string("added-dir");    let both_added_dir_file1_path = &both_added_dir_path.join(RepoPathComponent::new("file1"));    let both_added_dir_file2_path = &both_added_dir_path.join(RepoPathComponent::new("file2"));    let emptied_dir_path = RepoPath::from_internal_string("to-become-empty");    let emptied_dir_file1_path = &emptied_dir_path.join(RepoPathComponent::new("file1"));    let emptied_dir_file2_path = &emptied_dir_path.join(RepoPathComponent::new("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_single_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.resolve().unwrap().take();    let resolved_tree = resolved.as_resolved().unwrap().clone();    assert_eq!(        resolved_tree,        expected,        "actual entries: {:#?}, expected entries {:#?}",        resolved_tree.entries().collect_vec(),        expected.entries().collect_vec()    );} 

Lines 1–500 of 1569.