use crate::worktree_copy_error::raw_failure;
use crate::worktree_copy_error::{WorktreeCopyError, failure_with_cause};
use crate::worktree_copy_prune::Prune;
use crate::worktree_copy_selection::{filesystem_path, js_order};
use crate::json_record::quote;
use std::path::{Path, PathBuf};
#[cfg(unix)]
const PERMISSION_BITS: u32 = 0o7777;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum EntryKind {
Directory,
File,
Symlink,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WorktreeCopyEntry {
pub kind: EntryKind,
pub mode: u32,
pub relative_path: String,
}
struct WalkItem {
filesystem_path: PathBuf,
relative_path: String,
}
fn entry_kind(file_type: std::fs::FileType, path: &Path) -> Result<EntryKind, WorktreeCopyError> {
if file_type.is_dir() {
return Ok(EntryKind::Directory);
}
if file_type.is_file() {
return Ok(EntryKind::File);
}
if file_type.is_symlink() {
return Ok(EntryKind::Symlink);
}
let message: String = format!(
"cli-git: ignored path {} has unsupported filesystem type.",
quote(path.to_string_lossy().as_ref())
);
return Err(failure_with_cause(message.as_str(), None));
}
#[cfg(unix)]
pub fn retained_mode(metadata: &std::fs::Metadata) -> u32 {
use std::os::unix::fs::MetadataExt;
return metadata.mode() & PERMISSION_BITS;
}
#[cfg(not(unix))]
pub fn retained_mode(_metadata: &std::fs::Metadata) -> u32 {
return 0;
}
#[cfg(unix)]
fn readable_by_user(path: &Path, kind: EntryKind) -> bool {
use std::os::unix::ffi::OsStrExt;
if kind == EntryKind::Symlink {
return true;
}
let Ok(name) = std::ffi::CString::new(path.as_os_str().as_bytes()) else {
return false;
};
let mode: libc::c_int = if kind == EntryKind::Directory { libc::R_OK | libc::X_OK } else { libc::R_OK };
return unsafe { libc::access(name.as_ptr(), mode) } == 0;
}
#[cfg(not(unix))]
fn readable_by_user(_path: &Path, _kind: EntryKind) -> bool {
return true;
}
pub fn collect_entry_manifest(
root: &Path,
selected_roots: &[String],
excluded_roots: &[PathBuf],
) -> Result<Vec<WorktreeCopyEntry>, WorktreeCopyError> {
let mut skipped: Vec<String> = Vec::new();
return collect_pruned_manifest(root, selected_roots, excluded_roots, Prune::Never, &mut skipped);
}
pub fn collect_pruned_manifest(
root: &Path,
selected_roots: &[String],
excluded_roots: &[PathBuf],
prune: Prune,
unreadable: &mut Vec<String>,
) -> Result<Vec<WorktreeCopyEntry>, WorktreeCopyError> {
let mut pending: Vec<WalkItem> = Vec::new();
for relative_path in selected_roots.iter().rev() {
pending.push(WalkItem {
filesystem_path: filesystem_path(root, relative_path)?,
relative_path: relative_path.clone(),
});
}
let mut entries: Vec<WorktreeCopyEntry> = Vec::new();
while let Some(item) = pending.pop() {
if excluded_roots
.iter()
.any(|excluded| return item.filesystem_path.starts_with(excluded.as_path()))
|| prune.skips_path(item.relative_path.as_str())
{
continue;
}
let metadata: std::fs::Metadata = std::fs::symlink_metadata(item.filesystem_path.as_path())
.map_err(|error| return raw_failure(&error))?;
let kind: EntryKind = entry_kind(metadata.file_type(), item.filesystem_path.as_path())?;
if !readable_by_user(item.filesystem_path.as_path(), kind) {
unreadable.push(item.relative_path.clone());
continue;
}
if kind != EntryKind::Directory {
entries.push(WorktreeCopyEntry {
kind,
mode: retained_mode(&metadata),
relative_path: item.relative_path.clone(),
});
continue;
}
let mut names: Vec<String> = Vec::new();
for listed in std::fs::read_dir(item.filesystem_path.as_path())
.map_err(|error| return raw_failure(&error))?
{
let child: std::fs::DirEntry = listed.map_err(|error| return raw_failure(&error))?;
names.push(child.file_name().to_string_lossy().into_owned());
}
if prune.skips_checkout(names.as_slice()) {
continue;
}
entries.push(WorktreeCopyEntry {
kind,
mode: retained_mode(&metadata),
relative_path: item.relative_path.clone(),
});
names.sort_by(|left, right| return js_order(left, right));
for name in names.iter().rev() {
let child_relative: String = format!("{}/{name}", item.relative_path);
pending.push(WalkItem {
filesystem_path: filesystem_path(root, child_relative.as_str())?,
relative_path: child_relative,
});
}
}
return Ok(entries);
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct FileStamp {
pub size: u64,
pub modified: (i64, i64),
pub changed: (i64, i64),
pub inode: u64,
}
#[cfg(unix)]
pub fn stamp_of(metadata: &std::fs::Metadata) -> Option<FileStamp> {
use std::os::unix::fs::MetadataExt;
return Some(FileStamp {
size: metadata.len(),
modified: (metadata.mtime(), metadata.mtime_nsec()),
changed: (metadata.ctime(), metadata.ctime_nsec()),
inode: metadata.ino(),
});
}
#[cfg(not(unix))]
pub fn stamp_of(_metadata: &std::fs::Metadata) -> Option<FileStamp> {
return None;
}
#[cfg(unix)]
pub fn apply_entry_modes(
root: &Path,
entries: &[WorktreeCopyEntry],
) -> Result<(), WorktreeCopyError> {
use std::os::unix::fs::PermissionsExt;
let files = entries
.iter()
.filter(|entry| return entry.kind == EntryKind::File);
let directories = entries
.iter()
.rev()
.filter(|entry| return entry.kind == EntryKind::Directory);
for entry in files.chain(directories) {
let path: PathBuf = filesystem_path(root, entry.relative_path.as_str())?;
std::fs::set_permissions(path.as_path(), std::fs::Permissions::from_mode(entry.mode))
.map_err(|error| return raw_failure(&error))?;
}
return Ok(());
}
#[cfg(not(unix))]
pub fn apply_entry_modes(
_root: &Path,
_entries: &[WorktreeCopyEntry],
) -> Result<(), WorktreeCopyError> {
return Ok(());
}
#[cfg(test)]
#[path = "worktree_copy_manifest_tests.rs"]
mod tests;