use crate::worktree_copy_compare::lstat_or_absent;
use crate::worktree_copy_error::{WorktreeCopyError, failure_with_cause, raw_failure};
use crate::worktree_copy_manifest::{EntryKind, WorktreeCopyEntry};
use crate::worktree_copy_selection::filesystem_path;
use crate::worktree_copy_stage::copy_file_exclusive;
use crate::json_record::quote;
use std::os::unix::fs::{DirBuilderExt, MetadataExt};
use std::path::{Path, PathBuf};
const PRIVATE_DIRECTORY_MODE: u32 = 0o700;
const LINK_UNSUPPORTED_CODES: [i32; 6] = [
libc::EMLINK,
libc::ENOSYS,
libc::ENOTSUP,
libc::EOPNOTSUPP,
libc::EPERM,
libc::EXDEV,
];
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct InstalledPath {
pub device: u64,
pub inode: u64,
pub relative_path: String,
pub selected: bool,
}
pub fn capture_installed_path(
destination_path: &Path,
relative_path: &str,
selected: bool,
) -> Result<InstalledPath, WorktreeCopyError> {
let metadata: std::fs::Metadata =
std::fs::symlink_metadata(destination_path).map_err(|error| return raw_failure(&error))?;
return Ok(InstalledPath {
device: metadata.dev(),
inode: metadata.ino(),
relative_path: String::from(relative_path),
selected,
});
}
pub fn ensure_parents(
destination_root: &Path,
entry: &WorktreeCopyEntry,
created: &mut Vec<InstalledPath>,
) -> Result<(), WorktreeCopyError> {
return ensure_parents_remembering(destination_root, entry, created, &mut std::collections::HashSet::new());
}
pub fn ensure_parents_remembering(
destination_root: &Path,
entry: &WorktreeCopyEntry,
created: &mut Vec<InstalledPath>,
known: &mut std::collections::HashSet<String>,
) -> Result<(), WorktreeCopyError> {
let Some((parent, _name)) = entry.relative_path.rsplit_once('/') else {
return Ok(());
};
if known.contains(parent) {
return Ok(());
}
let components: Vec<&str> = entry.relative_path.split('/').collect();
let parent_count: usize = components.len().saturating_sub(1);
for depth in 1..=parent_count {
let current: String = components[..depth].join("/");
let destination_path: PathBuf = filesystem_path(destination_root, current.as_str())?;
match lstat_or_absent(destination_path.as_path())? {
Some(metadata) => {
if !metadata.is_dir() {
let message: String = format!(
"cli-git: ignored-state parent is not a directory: {} in {}.",
quote(current.as_str()),
quote(destination_root.to_string_lossy().as_ref())
);
return Err(failure_with_cause(message.as_str(), None));
}
}
None => {
std::fs::create_dir(destination_path.as_path())
.map_err(|error| return raw_failure(&error))?;
created.push(capture_installed_path(
destination_path.as_path(),
current.as_str(),
false,
)?);
}
}
}
for depth in 1..=parent_count {
known.insert(components[..depth].join("/"));
}
return Ok(());
}
fn is_link_unsupported(error: &std::io::Error) -> bool {
return error
.raw_os_error()
.is_some_and(|code| return LINK_UNSUPPORTED_CODES.contains(&code));
}
fn install_file(
stage_path: &Path,
destination_path: &Path,
mode: u32,
) -> Result<(), WorktreeCopyError> {
match std::fs::hard_link(stage_path, destination_path) {
Ok(()) => return Ok(()),
Err(error) if is_link_unsupported(&error) => {}
Err(error) => return Err(raw_failure(&error)),
}
copy_file_exclusive(stage_path, destination_path)?;
let applied: std::io::Result<()> = std::fs::set_permissions(
destination_path,
std::os::unix::fs::PermissionsExt::from_mode(mode),
);
if let Err(error) = applied {
let _ignored_removal: std::io::Result<()> = std::fs::remove_file(destination_path);
return Err(raw_failure(&error));
}
return Ok(());
}
pub fn create_selected_entry(
stage_root: &Path,
destination_root: &Path,
entry: &WorktreeCopyEntry,
) -> Result<(), WorktreeCopyError> {
let stage_path: PathBuf = filesystem_path(stage_root, entry.relative_path.as_str())?;
let destination_path: PathBuf =
filesystem_path(destination_root, entry.relative_path.as_str())?;
match entry.kind {
EntryKind::Directory => {
return std::fs::DirBuilder::new()
.mode(PRIVATE_DIRECTORY_MODE)
.create(destination_path.as_path())
.map_err(|error| return raw_failure(&error));
}
EntryKind::File => {
return install_file(stage_path.as_path(), destination_path.as_path(), entry.mode);
}
EntryKind::Symlink => {
let target: PathBuf = std::fs::read_link(stage_path.as_path())
.map_err(|error| return raw_failure(&error))?;
return std::os::unix::fs::symlink(target, destination_path.as_path())
.map_err(|error| return raw_failure(&error));
}
}
}
#[cfg(test)]
#[path = "worktree_copy_install_entry_tests.rs"]
mod tests;