2123 lines · 80.7 KB · Rust
RawDownload
// Copyright 2020 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 std::fs::File;#[cfg(unix)]use std::os::unix::fs::PermissionsExt as _;use std::path::Path;use std::path::PathBuf;use std::sync::Arc;use std::time::Duration;use std::time::SystemTime; use assert_matches::assert_matches;use indoc::indoc;use itertools::Itertools as _;use jj_lib::backend::CopyId;use jj_lib::backend::MergedTreeId;use jj_lib::backend::TreeId;use jj_lib::backend::TreeValue;use jj_lib::file_util::check_symlink_support;use jj_lib::file_util::try_symlink;use jj_lib::fsmonitor::FsmonitorSettings;use jj_lib::gitignore::GitIgnoreFile;use jj_lib::local_working_copy::LocalWorkingCopy;use jj_lib::local_working_copy::TreeState;use jj_lib::local_working_copy::TreeStateSettings;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::op_store::OperationId;use jj_lib::ref_name::WorkspaceName;use jj_lib::repo::ReadonlyRepo;use jj_lib::repo::Repo as _;use jj_lib::repo_path::RepoPath;use jj_lib::repo_path::RepoPathBuf;use jj_lib::secret_backend::SecretBackend;use jj_lib::tree_builder::TreeBuilder;use jj_lib::working_copy::CheckoutError;use jj_lib::working_copy::CheckoutStats;use jj_lib::working_copy::SnapshotOptions;use jj_lib::working_copy::UntrackedReason;use jj_lib::working_copy::WorkingCopy as _;use jj_lib::workspace::Workspace;use jj_lib::workspace::default_working_copy_factories;use pollster::FutureExt as _;use test_case::test_case;use testutils::TestRepo;use testutils::TestRepoBackend;use testutils::TestWorkspace;use testutils::commit_with_tree;use testutils::create_tree;use testutils::create_tree_with;use testutils::empty_snapshot_options;use testutils::repo_path;use testutils::repo_path_buf;use testutils::repo_path_component;use testutils::write_random_commit; fn check_icase_fs(dir: &Path) -> bool {    let test_file = tempfile::Builder::new()        .prefix("icase-")        .tempfile_in(dir)        .unwrap();    let orig_name = test_file.path().file_name().unwrap().to_str().unwrap();    let upper_name = orig_name.to_ascii_uppercase();    assert_ne!(orig_name, upper_name);    dir.join(upper_name).try_exists().unwrap()} /// Returns true if the directory appears to ignore some unicode zero-width/// characters, as in HFS+.fn check_hfs_plus(dir: &Path) -> bool {    let test_file = tempfile::Builder::new()        .prefix("hfs-plus-\u{200c}-")        .tempfile_in(dir)        .unwrap();    let orig_name = test_file.path().file_name().unwrap().to_str().unwrap();    let stripped_name = orig_name.replace('\u{200c}', "");    assert_ne!(orig_name, stripped_name);    dir.join(stripped_name).try_exists().unwrap()} /// Returns true if the directory appears to support Windows short file names.fn check_vfat(dir: &Path) -> bool {    let _test_file = tempfile::Builder::new()        .prefix("vfattest-")        .tempfile_in(dir)        .unwrap();    let short_name = "VFATTE~1";    dir.join(short_name).try_exists().unwrap()} fn to_owned_path_vec(paths: &[&RepoPath]) -> Vec<RepoPathBuf> {    paths.iter().map(|&path| path.to_owned()).collect()} fn tree_entries(tree: &MergedTree) -> Vec<(RepoPathBuf, Option<MergedTreeValue>)> {    tree.entries()        .map(|(path, result)| (path, result.ok()))        .collect_vec()} #[test]fn test_root() {    // Test that the working copy is clean and empty after init.    let mut test_workspace = TestWorkspace::init();     let wc = test_workspace.workspace.working_copy();    assert_eq!(wc.sparse_patterns().unwrap(), vec![RepoPathBuf::root()]);    let new_tree = test_workspace.snapshot().unwrap();    let repo = &test_workspace.repo;    let wc_commit_id = repo        .view()        .get_wc_commit_id(WorkspaceName::DEFAULT)        .unwrap();    let wc_commit = repo.store().get_commit(wc_commit_id).unwrap();    assert_eq!(new_tree.id(), *wc_commit.tree_id());    assert_eq!(new_tree.id(), repo.store().empty_merged_tree_id());} #[test_case(TestRepoBackend::Simple ; "simple backend")]#[test_case(TestRepoBackend::Git ; "git backend")]fn test_checkout_file_transitions(backend: TestRepoBackend) {    // Tests switching between commits where a certain path is of one type in one    // commit and another type in the other. Includes a "missing" type, so we cover    // additions and removals as well.     let mut test_workspace = TestWorkspace::init_with_backend(backend);    let repo = &test_workspace.repo;    let store = repo.store().clone();    let workspace_root = test_workspace.workspace.workspace_root().to_owned();     #[derive(Debug, PartialEq, Eq, Clone, Copy)]    enum Kind {        Missing,        Normal,        Executable,        // Executable, but same content as Normal, to test transition where only the bit changed        ExecutableNormalContent,        Conflict,        // Same content as Executable, to test that transition preserves the executable bit        ConflictedExecutableContent,        Symlink,        Tree,        GitSubmodule,    }     fn write_path(        repo: &Arc<ReadonlyRepo>,        tree_builder: &mut MergedTreeBuilder,        kind: Kind,        path: &RepoPath,    ) {        let store = repo.store();        let copy_id = CopyId::placeholder();        let value = match kind {            Kind::Missing => Merge::absent(),            Kind::Normal => {                let id = testutils::write_file(store, path, "normal file contents");                Merge::normal(TreeValue::File {                    id,                    executable: false,                    copy_id,                })            }            Kind::Executable => {                let id: jj_lib::backend::FileId =                    testutils::write_file(store, path, "executable file contents");                Merge::normal(TreeValue::File {                    id,                    executable: true,                    copy_id,                })            }            Kind::ExecutableNormalContent => {                let id = testutils::write_file(store, path, "normal file contents");                Merge::normal(TreeValue::File {                    id,                    executable: true,                    copy_id,                })            }            Kind::Conflict => {                let base_file_id = testutils::write_file(store, path, "base file contents");                let left_file_id = testutils::write_file(store, path, "left file contents");                let right_file_id = testutils::write_file(store, path, "right file contents");                Merge::from_removes_adds(                    vec![Some(TreeValue::File {                        id: base_file_id,                        executable: false,                        copy_id: copy_id.clone(),                    })],                    vec![                        Some(TreeValue::File {                            id: left_file_id,                            executable: false,                            copy_id: copy_id.clone(),                        }),                        Some(TreeValue::File {                            id: right_file_id,                            executable: false,                            copy_id: copy_id.clone(),                        }),                    ],                )            }            Kind::ConflictedExecutableContent => {                let base_file_id = testutils::write_file(store, path, "executable file contents");                let left_file_id =                    testutils::write_file(store, path, "left executable file contents");                let right_file_id =                    testutils::write_file(store, path, "right executable file contents");                Merge::from_removes_adds(                    vec![Some(TreeValue::File {                        id: base_file_id,                        executable: true,                        copy_id: copy_id.clone(),                    })],                    vec![                        Some(TreeValue::File {                            id: left_file_id,                            executable: true,                            copy_id: copy_id.clone(),                        }),                        Some(TreeValue::File {                            id: right_file_id,                            executable: true,                            copy_id: copy_id.clone(),                        }),                    ],                )            }            Kind::Symlink => {                let id = store.write_symlink(path, "target").block_on().unwrap();                Merge::normal(TreeValue::Symlink(id))            }            Kind::Tree => {                let file_path = path.join(repo_path_component("file"));                let id = testutils::write_file(store, &file_path, "normal file contents");                let value = TreeValue::File {                    id,                    executable: false,                    copy_id: copy_id.clone(),                };                tree_builder.set_or_remove(file_path, Merge::normal(value));                return;            }            Kind::GitSubmodule => {                let mut tx = repo.start_transaction();                let id = write_random_commit(tx.repo_mut()).id().clone();                tx.commit("test").unwrap();                Merge::normal(TreeValue::GitSubmodule(id))            }        };        tree_builder.set_or_remove(path.to_owned(), value);    }     let mut kinds = vec![        Kind::Missing,        Kind::Normal,        Kind::Executable,        Kind::ExecutableNormalContent,        Kind::Conflict,        Kind::ConflictedExecutableContent,        Kind::Tree,    ];    kinds.push(Kind::Symlink);    if backend == TestRepoBackend::Git {        kinds.push(Kind::GitSubmodule);    }    let mut left_tree_builder = MergedTreeBuilder::new(store.empty_merged_tree_id());    let mut right_tree_builder = MergedTreeBuilder::new(store.empty_merged_tree_id());    let mut files = vec![];    for left_kind in &kinds {        for right_kind in &kinds {            let path = repo_path_buf(format!("{left_kind:?}_{right_kind:?}"));            write_path(repo, &mut left_tree_builder, *left_kind, &path);            write_path(repo, &mut right_tree_builder, *right_kind, &path);            files.push((*left_kind, *right_kind, path.clone()));        }    }    let left_tree_id = left_tree_builder.write_tree(&store).unwrap();    let right_tree_id = right_tree_builder.write_tree(&store).unwrap();    let left_commit = commit_with_tree(&store, left_tree_id);    let right_commit = commit_with_tree(&store, right_tree_id.clone());     let ws = &mut test_workspace.workspace;    ws.check_out(repo.op_id().clone(), None, &left_commit)        .unwrap();    ws.check_out(repo.op_id().clone(), None, &right_commit)        .unwrap();     // Check that the working copy is clean.    let new_tree = test_workspace.snapshot().unwrap();    assert_eq!(new_tree.id(), right_tree_id);     for (_left_kind, right_kind, path) in &files {        let wc_path = workspace_root.join(path.as_internal_file_string());        let maybe_metadata = wc_path.symlink_metadata();        match right_kind {            Kind::Missing => {                assert!(maybe_metadata.is_err(), "{path:?} should not exist");            }            Kind::Normal => {                assert!(maybe_metadata.is_ok(), "{path:?} should exist");                let metadata = maybe_metadata.unwrap();                assert!(metadata.is_file(), "{path:?} should be a file");                #[cfg(unix)]                assert_eq!(                    metadata.permissions().mode() & 0o111,                    0,                    "{path:?} should not be executable"                );            }            Kind::Executable | Kind::ExecutableNormalContent => {                assert!(maybe_metadata.is_ok(), "{path:?} should exist");                let metadata = maybe_metadata.unwrap();                assert!(metadata.is_file(), "{path:?} should be a file");                #[cfg(unix)]                assert_ne!(                    metadata.permissions().mode() & 0o111,                    0,                    "{path:?} should be executable"                );            }            Kind::Conflict => {                assert!(maybe_metadata.is_ok(), "{path:?} should exist");                let metadata = maybe_metadata.unwrap();                assert!(metadata.is_file(), "{path:?} should be a file");                #[cfg(unix)]                assert_eq!(                    metadata.permissions().mode() & 0o111,                    0,                    "{path:?} should not be executable"                );            }            Kind::ConflictedExecutableContent => {                assert!(maybe_metadata.is_ok(), "{path:?} should exist");                let metadata = maybe_metadata.unwrap();                assert!(metadata.is_file(), "{path:?} should be a file");                #[cfg(unix)]                assert_ne!(                    metadata.permissions().mode() & 0o111,                    0,                    "{path:?} should be executable"                );            }            Kind::Symlink => {                assert!(maybe_metadata.is_ok(), "{path:?} should exist");                let metadata = maybe_metadata.unwrap();                if check_symlink_support().unwrap_or(false) {                    assert!(                        metadata.file_type().is_symlink(),                        "{path:?} should be a symlink"                    );                }            }            Kind::Tree => {                assert!(maybe_metadata.is_ok(), "{path:?} should exist");                let metadata = maybe_metadata.unwrap();                assert!(metadata.is_dir(), "{path:?} should be a directory");            }            Kind::GitSubmodule => {                // Not supported for now                assert!(maybe_metadata.is_err(), "{path:?} should not exist");            }        };    }} #[test]fn test_checkout_no_op() {    // Check out another commit with the same tree that's already checked out. The    // recorded operation should be updated even though the tree is unchanged.    let mut test_workspace = TestWorkspace::init();    let repo = test_workspace.repo.clone();     let file_path = repo_path("file");     let tree = create_tree(&repo, &[(file_path, "contents")]);    let commit1 = commit_with_tree(repo.store(), tree.id());    let commit2 = commit_with_tree(repo.store(), tree.id());     let ws = &mut test_workspace.workspace;    ws.check_out(repo.op_id().clone(), None, &commit1).unwrap();     // Test the setup: the file should exist on in the tree state.    let wc: &LocalWorkingCopy = ws.working_copy().downcast_ref().unwrap();    assert!(wc.file_states().unwrap().contains_path(file_path));     // Update to commit2 (same tree as commit1)    let new_op_id = OperationId::from_bytes(b"whatever");    let stats = ws.check_out(new_op_id.clone(), None, &commit2).unwrap();    assert_eq!(stats, CheckoutStats::default());     // The tree state is unchanged but the recorded operation id is updated.    let wc: &LocalWorkingCopy = ws.working_copy().downcast_ref().unwrap();    assert!(wc.file_states().unwrap().contains_path(file_path));    assert_eq!(*wc.operation_id(), new_op_id);} // Test case for issue #2165#[test]fn test_conflict_subdirectory() {    let mut test_workspace = TestWorkspace::init();    let repo = &test_workspace.repo;     let path = repo_path("sub/file");    let empty_tree = create_tree(repo, &[]);    let tree1 = create_tree(repo, &[(path, "0")]);    let commit1 = commit_with_tree(repo.store(), tree1.id());    let tree2 = create_tree(repo, &[(path, "1")]);    let merged_tree = tree1.merge(empty_tree, tree2).block_on().unwrap();    let merged_commit = commit_with_tree(repo.store(), merged_tree.id());    let repo = &test_workspace.repo;    let ws = &mut test_workspace.workspace;    ws.check_out(repo.op_id().clone(), None, &commit1).unwrap();    ws.check_out(repo.op_id().clone(), None, &merged_commit)        .unwrap();} #[test]fn test_acl() {    let settings = testutils::user_settings();    let test_workspace =        TestWorkspace::init_with_backend_and_settings(TestRepoBackend::Git, &settings);    let repo = &test_workspace.repo;    let workspace_root = test_workspace.workspace.workspace_root().to_owned();     let secret_modified_path = repo_path("secret/modified");    let secret_added_path = repo_path("secret/added");    let secret_deleted_path = repo_path("secret/deleted");    let became_secret_path = repo_path("file1");    let became_public_path = repo_path("file2");    let tree1 = create_tree(        repo,        &[            (secret_modified_path, "0"),            (secret_deleted_path, "0"),            (became_secret_path, "public"),            (became_public_path, "secret"),        ],    );    let tree2 = create_tree(        repo,        &[            (secret_modified_path, "1"),            (secret_added_path, "1"),            (became_secret_path, "secret"),            (became_public_path, "public"),        ],    );    let commit1 = commit_with_tree(repo.store(), tree1.id());    let commit2 = commit_with_tree(repo.store(), tree2.id());    SecretBackend::adopt_git_repo(&workspace_root);     let mut ws = Workspace::load(        &settings,        &workspace_root,        &test_workspace.env.default_store_factories(),        &default_working_copy_factories(),    )    .unwrap();    // Reload commits from the store associated with the workspace    let repo = ws.repo_loader().load_at(repo.operation()).unwrap();    let commit1 = repo.store().get_commit(commit1.id()).unwrap();    let commit2 = repo.store().get_commit(commit2.id()).unwrap();     ws.check_out(repo.op_id().clone(), None, &commit1).unwrap();    assert!(        !secret_modified_path            .to_fs_path_unchecked(&workspace_root)            .is_file()    );    assert!(        !secret_added_path            .to_fs_path_unchecked(&workspace_root)            .is_file()    );    assert!(        !secret_deleted_path            .to_fs_path_unchecked(&workspace_root)            .is_file()    );    assert!(        became_secret_path            .to_fs_path_unchecked(&workspace_root)            .is_file()

Lines 1–500 of 2123.