use std::path::{Path, PathBuf};
#[must_use]
#[cfg_attr(not(windows), allow(dead_code))]
pub fn resolve_symlink_lookup_path(link_parent: &Path, target: &Path) -> PathBuf {
if target.is_absolute() {
target.to_path_buf()
} else {
link_parent.join(target)
}
}
#[must_use]
#[cfg_attr(not(windows), allow(dead_code))]
pub fn symlink_target_is_directory(link_parent: &Path, target: &Path) -> bool {
resolve_symlink_lookup_path(link_parent, target).is_dir()
}
pub fn create_worktree_symlink(link: &Path, target: &Path) -> std::io::Result<()> {
#[cfg(unix)]
{
std::os::unix::fs::symlink(target, link)
}
#[cfg(windows)]
{
use std::os::windows::fs::{symlink_dir, symlink_file};
let parent = link.parent().unwrap_or_else(|| Path::new("."));
if symlink_target_is_directory(parent, target) {
symlink_dir(target, link)
} else {
symlink_file(target, link)
}
}
#[cfg(all(not(unix), not(windows)))]
{
let _ = (link, target);
Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"symlinks are not supported on this platform",
))
}
}
pub fn symlink_target_path_from_bytes(bytes: &[u8]) -> std::io::Result<PathBuf> {
#[cfg(unix)]
{
use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt;
Ok(PathBuf::from(OsStr::from_bytes(bytes)))
}
#[cfg(windows)]
{
let text = std::str::from_utf8(bytes).map_err(|_| {
std::io::Error::new(
std::io::ErrorKind::Unsupported,
"symlink target must be UTF-8 on Windows",
)
})?;
Ok(PathBuf::from(text))
}
#[cfg(all(not(unix), not(windows)))]
{
let _ = bytes;
Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"symlinks are not supported on this platform",
))
}
}
pub fn create_worktree_symlink_from_blob(link: &Path, bytes: &[u8]) -> std::io::Result<()> {
let target = symlink_target_path_from_bytes(bytes)?;
if let Some(parent) = link.parent() {
std::fs::create_dir_all(parent)?;
}
create_worktree_symlink(link, &target)
}
pub fn write_index_symlink_to_worktree(
link: &Path,
target: &str,
has_symlinks: bool,
) -> std::io::Result<()> {
if has_symlinks {
create_worktree_symlink(link, Path::new(target))
} else {
std::fs::write(link, target.as_bytes())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn relative_directory_target_uses_parent_not_cwd() {
let tmp = tempfile::tempdir().expect("tempdir");
let parent = tmp.path().join("parent");
let real_dir = tmp.path().join("real");
fs::create_dir_all(&real_dir).expect("mkdir real");
fs::create_dir_all(&parent).expect("mkdir parent");
let rel = Path::new("../real");
assert!(symlink_target_is_directory(&parent, rel));
let file = tmp.path().join("plain.txt");
fs::write(&file, b"x").expect("write");
assert!(!symlink_target_is_directory(
&parent,
Path::new("../plain.txt")
));
}
#[test]
fn dangling_relative_target_is_not_directory() {
let tmp = tempfile::tempdir().expect("tempdir");
let parent = tmp.path().join("dir");
fs::create_dir_all(&parent).expect("mkdir");
assert!(!symlink_target_is_directory(&parent, Path::new("missing")));
}
#[test]
fn disabled_symlinks_write_plain_file() {
let tmp = tempfile::tempdir().expect("tempdir");
let link = tmp.path().join("link.txt");
write_index_symlink_to_worktree(&link, "target.txt", false).expect("write");
let meta = fs::symlink_metadata(&link).expect("meta");
assert!(meta.is_file());
#[cfg(unix)]
assert!(!meta.file_type().is_symlink());
assert_eq!(fs::read_to_string(&link).expect("read"), "target.txt");
}
}