use super::fs_beneath::{ObjectId, identity_of, open_file_at, open_root};
use std::ffi::OsStr;
use std::fs::File;
use std::os::unix::ffi::OsStrExt;
use std::path::{Path, PathBuf};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Binding {
pub destination: PathBuf,
pub destination_id: ObjectId,
pub admin: PathBuf,
pub admin_id: ObjectId,
pub pointer: Vec<u8>,
}
#[derive(Debug)]
pub struct Bound {
pub binding: Binding,
pub destination: File,
pub admin: File,
}
fn hex(bytes: &[u8]) -> String {
return bytes.iter().map(|byte: &u8| { return format!("{byte:02x}"); }).collect();
}
fn unhex(text: &str) -> Option<Vec<u8>> {
if text.len() % 2 != 0 {
return None;
}
return (0..text.len()).step_by(2).map(|start: usize| { return u8::from_str_radix(text.get(start..start + 2)?, 16).ok(); }).collect();
}
pub fn encode_path(path: &Path) -> String {
return hex(path.as_os_str().as_bytes());
}
pub fn decode_path(text: &str) -> Option<PathBuf> {
use std::os::unix::ffi::OsStringExt;
return Some(PathBuf::from(std::ffi::OsString::from_vec(unhex(text)?)));
}
impl Binding {
pub fn encode(&self) -> String {
return format!(
"destination={}\nphase=registered\ndestination_id={}\nadmin={}\nadmin_id={}\npointer={}\n",
encode_path(self.destination.as_path()),
self.destination_id.encode(),
encode_path(self.admin.as_path()),
self.admin_id.encode(),
hex(self.pointer.as_slice()),
);
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Recorded {
Planned(PathBuf),
Registered(Binding),
}
pub fn decode_record(text: &str) -> Option<Recorded> {
let field = |key: &str| -> Option<&str> { return text.lines().find_map(|line: &str| { return line.strip_prefix(key).and_then(|rest: &str| { return rest.strip_prefix('='); }); }); };
let destination: PathBuf = decode_path(field("destination")?)?;
if field("phase")? == "planned" {
return Some(Recorded::Planned(destination));
}
if field("phase")? != "registered" {
return None;
}
return Some(Recorded::Registered(Binding {
destination,
destination_id: ObjectId::decode(field("destination_id")?)?,
admin: decode_path(field("admin")?)?,
admin_id: ObjectId::decode(field("admin_id")?)?,
pointer: unhex(field("pointer")?)?,
}));
}
pub fn planned_record(destination: &Path) -> String {
return format!("destination={}\nphase=planned\n", encode_path(destination));
}
pub fn path_identity(path: &Path) -> Option<ObjectId> {
let parent: File = open_root(path.parent()?).ok()?;
return super::fs_beneath::identity_at(&parent, path.file_name()?).ok();
}
pub fn bind_registration(common_dir: &Path, destination: &Path) -> Result<Bound, String> {
let shown: String = destination.display().to_string();
let held: File = open_root(destination).map_err(|error| { return format!("{shown} is not a real directory ({error})"); })?;
let pointer: Vec<u8> = read_through(&held, OsStr::new(".git")).map_err(|error| { return format!("{shown}/.git is unreadable ({error})"); })?;
let pointer_path: PathBuf = destination.join(".git");
let admin_path: PathBuf = pointer_target(destination, pointer.as_slice())?;
let admin: File = open_root(admin_path.as_path()).map_err(|error| { return not_canonical(shown.as_str(), admin_path.as_path(), error.to_string().as_str()); })?;
let worktrees: PathBuf = std::fs::canonicalize(common_dir.join("worktrees")).map_err(|error| { return format!("the worktrees of the repository are unreadable ({error})"); })?;
if admin_path.parent() != Some(worktrees.as_path()) {
return Err(format!("{shown} is not registered in this repository"));
}
let recorded: Vec<u8> = read_through(&admin, OsStr::new("gitdir")).map_err(|error| { return format!("the registration of {shown} is unreadable ({error})"); })?;
let recorded_line: &[u8] = recorded.strip_suffix(b"\n").unwrap_or(recorded.as_slice());
if recorded_line != pointer_path.as_os_str().as_bytes() {
return Err(format!("the registration {} does not name {shown}", admin_path.display()));
}
let binding: Binding = Binding {
destination: destination.to_path_buf(),
destination_id: identity_of(&held).map_err(|error| { return format!("{shown} cannot be examined ({error})"); })?,
admin: admin_path,
admin_id: identity_of(&admin).map_err(|error| { return format!("the registration cannot be examined ({error})"); })?,
pointer,
};
return Ok(Bound { binding, destination: held, admin });
}
fn not_canonical(shown: &str, target: &Path, why: &str) -> String {
return format!("the pointer in {shown}/.git names {}, which cannot be resolved without a symbolic link to the registration ({why}); the add is left as Git made it: make the worktree with a canonical path, or replace the link in the pointer with the real directory", target.display());
}
fn pointer_target(destination: &Path, pointer: &[u8]) -> Result<PathBuf, String> {
let shown: String = destination.display().to_string();
let line: &[u8] = pointer.strip_suffix(b"\n").unwrap_or(pointer);
let named: &[u8] = line.strip_prefix(b"gitdir: ").ok_or_else(|| { return format!("{shown}/.git is not a gitdir pointer"); })?;
let mut resolved: PathBuf = PathBuf::new();
for part in destination.join(OsStr::from_bytes(named)).components() {
match part {
std::path::Component::CurDir => {}
std::path::Component::ParentDir => {
if !resolved.pop() {
return Err(format!("the pointer in {shown}/.git climbs out of the file system root"));
}
}
other => resolved.push(other.as_os_str()),
}
}
return Ok(resolved);
}
fn read_through(directory: &File, name: &OsStr) -> std::io::Result<Vec<u8>> {
let mut file: File = open_file_at(directory, name)?;
let mut bytes: Vec<u8> = Vec::new();
std::io::Read::read_to_end(&mut file, &mut bytes)?;
return Ok(bytes);
}
pub fn verify_association(bound: &Bound) -> Result<(), String> {
let binding: &Binding = &bound.binding;
let shown: String = binding.destination.display().to_string();
if path_identity(binding.destination.as_path()) != Some(binding.destination_id) {
return Err(format!("{shown} is no longer the directory Git made for this add"));
}
if path_identity(binding.admin.as_path()) != Some(binding.admin_id) {
return Err(format!("the registration {} is no longer the one Git made for this add", binding.admin.display()));
}
let pointer: Vec<u8> = read_through(&bound.destination, OsStr::new(".git")).map_err(|error| { return format!("{shown}/.git is unreadable ({error})"); })?;
if pointer != binding.pointer {
return Err(format!("{shown}/.git is no longer the pointer Git wrote"));
}
let target: PathBuf = pointer_target(binding.destination.as_path(), pointer.as_slice())?;
let resolved: File = open_root(target.as_path()).map_err(|error| { return not_canonical(shown.as_str(), target.as_path(), error.to_string().as_str()); })?;
if identity_of(&resolved).ok() != Some(binding.admin_id) || identity_of(&bound.admin).ok() != Some(binding.admin_id) {
return Err(format!("the pointer in {shown}/.git now resolves to another registration than the one Git made for this add"));
}
let recorded: Vec<u8> = read_through(&bound.admin, OsStr::new("gitdir")).map_err(|error| { return format!("the registration of {shown} is unreadable ({error})"); })?;
let line: &[u8] = recorded.strip_suffix(b"\n").unwrap_or(recorded.as_slice());
if line != binding.destination.join(".git").as_os_str().as_bytes() {
return Err(format!("the registration {} no longer names {shown}", binding.admin.display()));
}
return Ok(());
}
#[cfg(test)]
#[path = "worktree_add_binding_tests.rs"]
mod tests;