use std::ffi::{OsStr, OsString};
use std::os::unix::ffi::{OsStrExt, OsStringExt};
use std::path::{Path, PathBuf};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Registration {
pub id: OsString,
pub admin: PathBuf,
pub worktrees: PathBuf,
pub gitdir_text: Vec<u8>,
}
fn without_newline(text: &[u8]) -> &[u8] {
return text.strip_suffix(b"\n").unwrap_or(text);
}
fn ends_with_name(candidate: &[u8], operand: &[u8]) -> bool {
if operand.is_empty() || operand.len() > candidate.len() {
return false;
}
let start: usize = candidate.len() - operand.len();
let at_boundary: bool = start == 0 || candidate[start - 1] == b'/';
return at_boundary && candidate[start..].eq_ignore_ascii_case(operand);
}
fn registered_paths(common_dir: &Path, worktrees: &Path) -> Result<Vec<Vec<u8>>, String> {
let mut paths: Vec<Vec<u8>> = Vec::new();
let main: &[u8] = common_dir.as_os_str().as_bytes();
paths.push(main.strip_suffix(b"/.git").unwrap_or(main).to_vec());
let listing = match std::fs::read_dir(worktrees) {
Ok(listing) => listing,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(paths),
Err(error) => return Err(format!("the registrations in {} cannot be listed ({error})", worktrees.display())),
};
for found in listing {
let entry: std::fs::DirEntry = found.map_err(|error: std::io::Error| { return format!("the registrations in {} cannot be listed ({error})", worktrees.display()); })?;
let Ok(text) = std::fs::read(entry.path().join("gitdir")) else { continue };
let trimmed: &[u8] = text.trim_ascii_end();
if trimmed.is_empty() {
continue;
}
let path: &[u8] = trimmed.strip_suffix(b"/.git").unwrap_or(trimmed);
if path.starts_with(b"/") {
paths.push(path.to_vec());
} else {
let joined: PathBuf = entry.path().join(OsStr::from_bytes(path));
let resolved: PathBuf = std::fs::canonicalize(joined.as_path()).unwrap_or(joined);
paths.push(resolved.into_os_string().into_vec());
}
}
return Ok(paths);
}
pub fn registration_of(common_dir: &Path, target: &Path, operand: &OsStr) -> Result<Registration, String> {
let pointer_path: PathBuf = target.join(".git");
let pointer: Vec<u8> = std::fs::read(pointer_path.as_path()).map_err(|error: std::io::Error| { return format!("{} cannot be read as a file ({error}), so it is not a linked worktree", pointer_path.display()); })?;
let line: &[u8] = without_newline(pointer.as_slice());
let named: &[u8] = line.strip_prefix(b"gitdir: ").ok_or_else(|| { return format!("{} does not start with `gitdir: `", pointer_path.display()); })?;
let admin: PathBuf = std::fs::canonicalize(target.join(OsStr::from_bytes(named)))
.map_err(|error: std::io::Error| { return format!("the administrative directory {} that {} names cannot be resolved ({error})", String::from_utf8_lossy(named), pointer_path.display()); })?;
let worktrees: PathBuf = std::fs::canonicalize(common_dir.join("worktrees"))
.map_err(|error: std::io::Error| { return format!("{} cannot be resolved ({error}), so this repository has no linked worktree", common_dir.join("worktrees").display()); })?;
if admin.parent() != Some(worktrees.as_path()) {
return Err(format!(
"the administrative directory {} of {} is not one of the registrations in {}, the repository this command runs in",
admin.display(),
target.display(),
worktrees.display()
));
}
let id: OsString = admin.file_name().ok_or_else(|| { return format!("{} has no name", admin.display()); })?.to_os_string();
let gitdir_text: Vec<u8> = std::fs::read(admin.join("gitdir")).map_err(|error: std::io::Error| { return format!("{} cannot be read ({error})", admin.join("gitdir").display()); })?;
let recorded: PathBuf = PathBuf::from(OsStr::from_bytes(without_newline(gitdir_text.as_slice())));
if recorded != pointer_path {
return Err(format!("{} records {}, not {}", admin.join("gitdir").display(), recorded.display(), pointer_path.display()));
}
let registered: Vec<Vec<u8>> = registered_paths(common_dir, worktrees.as_path())?;
for path in registered {
if ends_with_name(path.as_slice(), operand.as_bytes()) && path.as_slice() != target.as_os_str().as_bytes() {
return Err(format!(
"Git matches the name {} to the registered worktree {} by its last components, not to {}",
operand.to_string_lossy(),
String::from_utf8_lossy(path.as_slice()),
target.display()
));
}
}
return Ok(Registration { id, admin, worktrees, gitdir_text });
}
#[cfg(test)]
#[path = "worktree_remove_registry_tests.rs"]
mod tests;