Skip to content

Commit

Permalink
fix: default to creating file-symlinks if it is dangling on Windows (#…
Browse files Browse the repository at this point in the history
…1354)

This behaviour is the same as in Git.
  • Loading branch information
Byron committed May 13, 2024
1 parent 185eb51 commit 78dedeb
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 19 deletions.
12 changes: 11 additions & 1 deletion gix-fs/src/symlink.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,20 @@ pub fn remove(path: &Path) -> io::Result<()> {

#[cfg(windows)]
/// Create a new symlink at `link` which points to `original`.
///
/// Note that if a symlink target (the `original`) isn't present on disk, it's assumed to be a
/// file, creating a dangling file symlink. This is similar to a dangling symlink on Unix,
/// which doesn't have to care about the target type though.
pub fn create(original: &Path, link: &Path) -> io::Result<()> {
use std::os::windows::fs::{symlink_dir, symlink_file};
// TODO: figure out if links to links count as files or whatever they point at
if std::fs::metadata(link.parent().expect("dir for link").join(original))?.is_dir() {
let orig_abs = link.parent().expect("dir for link").join(original);
let is_dir = match std::fs::metadata(orig_abs) {
Ok(m) => m.is_dir(),
Err(err) if err.kind() == io::ErrorKind::NotFound => false,
Err(err) => return Err(err),
};
if is_dir {
symlink_dir(original, link)
} else {
symlink_file(original, link)
Expand Down
Binary file not shown.
11 changes: 11 additions & 0 deletions gix-worktree-state/tests/fixtures/make_dangling_symlink.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/bin/bash
set -eu -o pipefail

git init -q

target_oid=$(echo -n "non-existing-target" | git hash-object -w --stdin)
git update-index --index-info <<-EOF
120000 $target_oid dangling
EOF

git commit -m "dangling symlink in index"
61 changes: 43 additions & 18 deletions gix-worktree-state/tests/state/checkout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ fn driver_exe() -> String {
exe
}

fn assure_is_empty(dir: impl AsRef<Path>) -> std::io::Result<()> {
assert_eq!(std::fs::read_dir(dir)?.count(), 0);
Ok(())
}

#[test]
fn submodules_are_instantiated_as_directories() -> crate::Result {
let mut opts = opts_from_probe();
Expand All @@ -57,11 +62,6 @@ fn submodules_are_instantiated_as_directories() -> crate::Result {
Ok(())
}

fn assure_is_empty(dir: impl AsRef<Path>) -> std::io::Result<()> {
assert_eq!(std::fs::read_dir(dir)?.count(), 0);
Ok(())
}

#[test]
fn accidental_writes_through_symlinks_are_prevented_if_overwriting_is_forbidden() {
let mut opts = opts_from_probe();
Expand Down Expand Up @@ -125,7 +125,7 @@ fn writes_through_symlinks_are_prevented_even_if_overwriting_is_allowed() {
if cfg!(windows) { "A-dir\\a" } else { "A-dir/a" },
"A-file",
"FAKE-DIR",
if cfg!(windows) { "fake-file" } else { "FAKE-FILE" }
"FAKE-FILE"
]),
);
assert!(outcome.collisions.is_empty());
Expand Down Expand Up @@ -257,6 +257,30 @@ fn symlinks_become_files_if_disabled() -> crate::Result {
Ok(())
}

#[test]
fn dangling_symlinks_can_be_created() -> crate::Result {
let opts = opts_from_probe();
if !opts.fs.symlink {
eprintln!("Skipping dangling symlink test on filesystem that doesn't support it");
return Ok(());
}

let (_source_tree, destination, _index, outcome) =
checkout_index_in_tmp_dir(opts.clone(), "make_dangling_symlink")?;
let worktree_files = dir_structure(&destination);
let worktree_files_stripped = stripped_prefix(&destination, &worktree_files);

assert_eq!(worktree_files_stripped, paths(["dangling"]));
let symlink_path = &worktree_files[0];
assert!(symlink_path
.symlink_metadata()
.expect("dangling symlink is on disk")
.is_symlink());
assert_eq!(std::fs::read_link(symlink_path)?, Path::new("non-existing-target"));
assert!(outcome.collisions.is_empty());
Ok(())
}

#[test]
fn allow_or_disallow_symlinks() -> crate::Result {
let mut opts = opts_from_probe();
Expand Down Expand Up @@ -303,12 +327,7 @@ fn keep_going_collects_results() {
.iter()
.map(|r| r.path.to_path_lossy().into_owned())
.collect::<Vec<_>>(),
paths(if cfg!(unix) {
[".gitattributes", "dir/content"]
} else {
// not actually a symlink anymore, even though symlinks are supported but git think differently.
["dir/content", "dir/sub-dir/symlink"]
})
paths([".gitattributes", "dir/content"])
);
}

Expand All @@ -322,11 +341,15 @@ fn keep_going_collects_results() {
} else {
assert_eq!(
stripped_prefix(&destination, &dir_structure(&destination)),
paths(if cfg!(unix) {
Box::new(["dir/sub-dir/symlink", "empty", "executable"].into_iter()) as Box<dyn Iterator<Item = &str>>
} else {
Box::new(["empty", "executable"].into_iter())
}),
paths([
if cfg!(unix) {
"dir/sub-dir/symlink"
} else {
"dir\\sub-dir\\symlink"
},
"empty",
"executable",
]),
"some files could not be created"
);
}
Expand Down Expand Up @@ -550,8 +573,10 @@ fn probe_gitoxide_dir() -> crate::Result<gix_fs::Capabilities> {
}

fn opts_from_probe() -> gix_worktree_state::checkout::Options {
static CAPABILITIES: Lazy<gix_fs::Capabilities> = Lazy::new(|| probe_gitoxide_dir().unwrap());

gix_worktree_state::checkout::Options {
fs: probe_gitoxide_dir().unwrap(),
fs: *CAPABILITIES,
destination_is_initially_empty: true,
thread_limit: gix_features::parallel::num_threads(None).into(),
..Default::default()
Expand Down

0 comments on commit 78dedeb

Please sign in to comment.