Sebastian Thielmake fmt7cf35454 years agoHistory
536 lines · 17.8 KB · Rust
RawDownload
mod cache {    use std::path::Path;     use git_index::entry::Mode;    use git_worktree::index::checkout::PathCache;    use tempfile::{tempdir, TempDir};     #[test]    fn root_is_assumed_to_exist_and_files_in_root_do_not_create_directory() {        let dir = tempdir().unwrap();        let mut cache = PathCache::new(dir.path().join("non-existing-root"));        assert_eq!(cache.test_mkdir_calls, 0);         let path = cache            .append_relative_path_assure_leading_dir("hello", Mode::FILE)            .unwrap();        assert!(!path.parent().unwrap().exists(), "prefix itself is never created");        assert_eq!(cache.test_mkdir_calls, 0);    }     #[test]    fn directory_paths_are_created_in_full() {        let (mut cache, _tmp) = new_cache();         for (name, mode) in &[            ("dir", Mode::DIR),            ("submodule", Mode::COMMIT),            ("file", Mode::FILE),            ("exe", Mode::FILE_EXECUTABLE),            ("link", Mode::SYMLINK),        ] {            let path = cache                .append_relative_path_assure_leading_dir(Path::new("dir").join(name), *mode)                .unwrap();            assert!(path.parent().unwrap().is_dir(), "dir exists");        }         assert_eq!(cache.test_mkdir_calls, 3);    }     #[test]    fn existing_directories_are_fine() {        let (mut cache, tmp) = new_cache();        std::fs::create_dir(tmp.path().join("dir")).unwrap();         let path = cache            .append_relative_path_assure_leading_dir("dir/file", Mode::FILE)            .unwrap();        assert!(path.parent().unwrap().is_dir(), "directory is still present");        assert!(!path.exists(), "it won't create the file");        assert_eq!(cache.test_mkdir_calls, 1);    }     #[test]    fn symlinks_or_files_in_path_are_forbidden_or_unlinked_when_forced() {        let (mut cache, tmp) = new_cache();        let forbidden = tmp.path().join("forbidden");        std::fs::create_dir(&forbidden).unwrap();        symlink::symlink_dir(&forbidden, tmp.path().join("link-to-dir")).unwrap();        std::fs::write(tmp.path().join("file-in-dir"), &[]).unwrap();         for dirname in &["file-in-dir", "link-to-dir"] {            cache.unlink_on_collision = false;            let relative_path = format!("{}/file", dirname);            assert_eq!(                cache                    .append_relative_path_assure_leading_dir(&relative_path, Mode::FILE)                    .unwrap_err()                    .kind(),                std::io::ErrorKind::AlreadyExists            );        }        assert_eq!(            cache.test_mkdir_calls, 2,            "it tries to create each directory once, but it's a file"        );        cache.test_mkdir_calls = 0;        for dirname in &["link-to-dir", "file-in-dir"] {            cache.unlink_on_collision = true;            let relative_path = format!("{}/file", dirname);            let path = cache                .append_relative_path_assure_leading_dir(&relative_path, Mode::FILE)                .unwrap();            assert!(path.parent().unwrap().is_dir(), "directory was forcefully created");            assert!(!path.exists());        }        assert_eq!(            cache.test_mkdir_calls, 4,            "like before, but it unlinks what's there and tries again"        );    }     fn new_cache() -> (PathCache, TempDir) {        let dir = tempdir().unwrap();        let cache = PathCache::new(dir.path());        (cache, dir)    }} #[cfg(unix)]use std::os::unix::prelude::MetadataExt;use std::{    fs,    io::{ErrorKind, ErrorKind::AlreadyExists},    path::{Path, PathBuf},    sync::atomic::{AtomicBool, AtomicUsize, Ordering},}; use git_features::progress;use git_object::bstr::ByteSlice;use git_odb::FindExt;use git_worktree::{fs::Capabilities, index, index::checkout::Collision};use tempfile::TempDir; use crate::fixture_path; #[test]fn accidental_writes_through_symlinks_are_prevented_if_overwriting_is_forbidden() {    let mut opts = opts_from_probe();    // without overwrite mode, everything is safe.    opts.overwrite_existing = false;    let (source_tree, destination, _index, outcome) =        checkout_index_in_tmp_dir(opts, "make_dangerous_symlink").unwrap();     let source_files = dir_structure(&source_tree);    let worktree_files = dir_structure(&destination);     if opts.fs.ignore_case {        assert_eq!(            stripped_prefix(&source_tree, &source_files),            stripped_prefix(&destination, &worktree_files),        );        if multi_threaded() {            assert_eq!(outcome.collisions.len(), 2);        } else {            assert_eq!(                outcome.collisions,                vec![                    Collision {                        path: "FAKE-DIR".into(),                        error_kind: AlreadyExists                    },                    Collision {                        path: "FAKE-FILE".into(),                        error_kind: AlreadyExists                    }                ]            );        }    } else {        let expected = ["A-dir/a", "A-file", "FAKE-DIR", "FAKE-FILE", "fake-dir/b", "fake-file"];        assert_eq!(stripped_prefix(&source_tree, &source_files), paths(expected));        assert_eq!(stripped_prefix(&destination, &worktree_files), paths(expected));        assert!(outcome.collisions.is_empty());    };} #[test]fn writes_through_symlinks_are_prevented_even_if_overwriting_is_allowed() {    let mut opts = opts_from_probe();    // with overwrite mode    opts.overwrite_existing = true;    let (source_tree, destination, _index, outcome) =        checkout_index_in_tmp_dir(opts, "make_dangerous_symlink").unwrap();     let source_files = dir_structure(&source_tree);    let worktree_files = dir_structure(&destination);     if opts.fs.ignore_case {        assert_eq!(            stripped_prefix(&source_tree, &source_files),            paths(["A-dir/a", "A-file", "fake-dir/b", "fake-file"]),        );        assert_eq!(            stripped_prefix(&destination, &worktree_files),            paths(["A-dir/a", "A-file", "FAKE-DIR", "FAKE-FILE"]),        );        assert!(outcome.collisions.is_empty());    } else {        let expected = ["A-dir/a", "A-file", "FAKE-DIR", "FAKE-FILE", "fake-dir/b", "fake-file"];        assert_eq!(stripped_prefix(&source_tree, &source_files), paths(expected));        assert_eq!(stripped_prefix(&destination, &worktree_files), paths(expected));        assert!(outcome.collisions.is_empty());    };} #[test]fn overwriting_files_and_lone_directories_works() {    let mut opts = opts_from_probe();    opts.overwrite_existing = true;    opts.destination_is_initially_empty = false;    let (_source_tree, destination, _index, outcome) = checkout_index_in_tmp_dir_opts(        opts,        "make_mixed_without_submodules",        |_| true,        |d| {            let empty = d.join("empty");            symlink::symlink_dir(d.join(".."), &empty)?; // empty is symlink to the directory above            std::fs::write(d.join("executable"), b"foo")?; // executable is regular file and has different content            let dir = d.join("dir");            std::fs::create_dir(&dir)?;            std::fs::create_dir(dir.join("content"))?; // 'content' is a directory now             let dir = dir.join("sub-dir");            std::fs::create_dir(&dir)?;             symlink::symlink_dir(empty, dir.join("symlink"))?; // 'symlink' is a symlink to another file            Ok(())        },    )    .unwrap();     assert!(outcome.collisions.is_empty());     assert_eq!(        stripped_prefix(&destination, &dir_structure(&destination)),        paths(["dir/content", "dir/sub-dir/symlink", "empty", "executable"])    );    let meta = std::fs::symlink_metadata(destination.path().join("empty")).unwrap();    assert!(meta.is_file(), "'empty' is now a file");    assert_eq!(meta.len(), 0, "'empty' is indeed empty");     let exe = destination.path().join("executable");    assert_eq!(        std::fs::read(&exe).unwrap(),        b"content",        "'exe' has the correct content"    );     let meta = std::fs::symlink_metadata(exe).unwrap();    assert!(meta.is_file());    if opts.fs.executable_bit {        #[cfg(unix)]        assert_eq!(meta.mode() & 0o700, 0o700, "the executable bit is set where supported");    }     assert_eq!(        std::fs::read(destination.path().join("dir/content")).unwrap(),        b"other content"    );     let symlink = destination.path().join("dir/sub-dir/symlink");    // on windows, git won't create symlinks as its probe won't detect the capability, even though we do.    assert_eq!(std::fs::symlink_metadata(&symlink).unwrap().is_symlink(), cfg!(unix));    assert_eq!(std::fs::read(symlink).unwrap(), b"other content");} #[test]fn symlinks_become_files_if_disabled() -> crate::Result {    let mut opts = opts_from_probe();    opts.fs.symlink = false;    let (source_tree, destination, _index, outcome) = checkout_index_in_tmp_dir(opts, "make_mixed_without_submodules")?;     assert_equality(&source_tree, &destination, opts.fs.symlink)?;    assert!(outcome.collisions.is_empty());    Ok(())} #[test]fn allow_or_disallow_symlinks() -> crate::Result {    let mut opts = opts_from_probe();    for allowed in &[false, true] {        opts.fs.symlink = *allowed;        let (source_tree, destination, _index, outcome) =            checkout_index_in_tmp_dir(opts, "make_mixed_without_submodules")?;         assert_equality(&source_tree, &destination, opts.fs.symlink)?;        assert!(outcome.collisions.is_empty());    }    Ok(())} #[test]fn keep_going_collects_results() {    let mut opts = opts_from_probe();    opts.keep_going = true;    let count = AtomicUsize::default();    let (_source_tree, destination, _index, outcome) = checkout_index_in_tmp_dir_opts(        opts,        "make_mixed_without_submodules",        |_id| {            if let Ok(_) = count.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |current| {                (current < 2).then(|| current + 1)            }) {                false            } else {                true            }        },        |_| Ok(()),    )    .unwrap();     if multi_threaded() {        assert_eq!(            outcome.errors.len(),            2,            "content changes due to non-deterministic nature of racy threads"        )    } else {        assert_eq!(            outcome                .errors                .iter()                .map(|r| r.path.to_path_lossy().into_owned())                .collect::<Vec<_>>(),            paths(if cfg!(unix) {                ["dir/content", "empty"]            } else {                // not actually a symlink anymore, even though symlinks are supported but git think differently.                ["dir/content", "dir/sub-dir/symlink"]            })        );    }     if multi_threaded() {        assert_eq!(dir_structure(&destination).len(), 2);    } else {        assert_eq!(            stripped_prefix(&destination, &dir_structure(&destination)),            paths(if cfg!(unix) {                ["dir/sub-dir/symlink", "executable"]            } else {                ["empty", "executable"]            }),            "some files could not be created"        );    }     assert!(outcome.collisions.is_empty());} #[test]fn no_case_related_collisions_on_case_sensitive_filesystem() {    let opts = opts_from_probe();    if opts.fs.ignore_case {        eprintln!("Skipping case-sensitive testing on what would be a case-insensitive file system");        return;    }    let (source_tree, destination, index, outcome) =        checkout_index_in_tmp_dir(opts, "make_ignorecase_collisions").unwrap();     assert!(outcome.collisions.is_empty());    let num_files = assert_equality(&source_tree, &destination, opts.fs.symlink).unwrap();    assert_eq!(num_files, index.entries().len(), "it checks out all files");} #[test]fn collisions_are_detected_on_a_case_insensitive_filesystem() {    let opts = opts_from_probe();    if !opts.fs.ignore_case {        eprintln!("Skipping case-insensitive testing on what would be a case-sensitive file system");        return;    }    let (source_tree, destination, _index, outcome) =        checkout_index_in_tmp_dir(opts, "make_ignorecase_collisions").unwrap();     let source_files = dir_structure(&source_tree);    assert_eq!(        stripped_prefix(&source_tree, &source_files),        paths(["d", "file_x", "link-to-X", "x"]),        "plenty of collisions prevent a checkout"    );     let dest_files = dir_structure(&destination);    if multi_threaded() {        assert_eq!(            dest_files.len(),            5,            "can only assert on number as it's racily creating files so unclear which one clashes"        );    } else {        assert_eq!(            stripped_prefix(&destination, &dest_files),            paths(["D/B", "D/C", "FILE_X", "X", "link-to-X"]),            "we checkout files in order and generally handle collision detection differently, hence the difference"        );    }     let error_kind = ErrorKind::AlreadyExists;    #[cfg(windows)]    let error_kind_dir = ErrorKind::PermissionDenied;    #[cfg(not(windows))]    let error_kind_dir = error_kind;     if multi_threaded() {        assert_eq!(            outcome.collisions.len(),            5,            "can only assert on number as it's racily creating files so unclear which one clashes"        );    } else {        assert_eq!(            outcome.collisions,            vec![                Collision {                    path: "FILE_x".into(),                    error_kind,                },                Collision {                    path: "d".into(),                    error_kind: error_kind_dir,                },                Collision {                    path: "file_X".into(),                    error_kind,                },                Collision {                    path: "file_x".into(),                    error_kind,                },                Collision {                    path: "x".into(),                    error_kind,                },            ],            "these files couldn't be checked out"        );    }} fn multi_threaded() -> bool {    git_features::parallel::num_threads(None) > 1} fn assert_equality(source_tree: &Path, destination: &TempDir, allow_symlinks: bool) -> crate::Result<usize> {    let source_files = dir_structure(source_tree);    let worktree_files = dir_structure(&destination);     assert_eq!(        stripped_prefix(source_tree, &source_files),        stripped_prefix(&destination, &worktree_files),    );     let mut count = 0;    for (source_file, worktree_file) in source_files.iter().zip(worktree_files.iter()) {        count += 1;        if !allow_symlinks && source_file.is_symlink() {            assert!(!worktree_file.is_symlink());            assert_eq!(fs::read(worktree_file)?.to_path()?, fs::read_link(source_file)?);        } else {            assert_eq!(fs::read(source_file)?, fs::read(worktree_file)?);            #[cfg(unix)]            assert_eq!(                fs::symlink_metadata(source_file)?.mode() & 0o700,                fs::symlink_metadata(worktree_file)?.mode() & 0o700,                "permissions of source and checked out file are comparable"            );        }    }    Ok(count)} pub fn dir_structure<P: AsRef<std::path::Path>>(path: P) -> Vec<std::path::PathBuf> {    let path = path.as_ref();    let mut files: Vec<_> = walkdir::WalkDir::new(path)        .follow_links(false)        .into_iter()        .filter_entry(|e| e.path() == path || !e.file_name().to_string_lossy().starts_with('.'))        .flatten()        .filter_map(|e| (!e.path().symlink_metadata().map_or(true, |m| m.is_dir())).then(|| e.path().to_path_buf()))        .collect();    files.sort();    files} fn checkout_index_in_tmp_dir(    opts: index::checkout::Options,    name: &str,) -> crate::Result<(    PathBuf,    TempDir,    git_index::File,    git_worktree::index::checkout::Outcome,)> {    checkout_index_in_tmp_dir_opts(opts, name, |_d| true, |_| Ok(()))} fn checkout_index_in_tmp_dir_opts(    opts: index::checkout::Options,    name: &str,    mut allow_return_object: impl FnMut(&git_hash::oid) -> bool + Send + Clone,    prep_dest: impl Fn(&Path) -> std::io::Result<()>,) -> crate::Result<(    PathBuf,    TempDir,    git_index::File,    git_worktree::index::checkout::Outcome,)> {    let source_tree = fixture_path(name);    let git_dir = source_tree.join(".git");    let mut index = git_index::File::at(git_dir.join("index"), Default::default())?;    let odb = git_odb::at(git_dir.join("objects"))?.into_inner().into_arc()?;    let destination = tempfile::tempdir_in(std::env::current_dir()?)?;    prep_dest(destination.path())?;     let outcome = index::checkout(        &mut index,        destination.path(),        move |oid, buf| {

Lines 1–500 of 536.