// 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. Load more
// 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()