use std::fs;
use std::path::{Path, PathBuf};
use super::worktree_admin::{checked_registration, lock, replace};
use super::worktree_create::{gitfile_line, path_line, relative_path};
use super::{RepositoryLocation, WorktreeAdminError, metadata_path};
#[derive(Debug)]
pub struct WorktreeRepair {
registration: PathBuf,
registration_identity: fs::Metadata,
common: PathBuf,
common_identity: fs::Metadata,
forward: CapturedLink,
back: CapturedLink,
common_link: CapturedLink,
}
#[derive(Debug)]
struct CapturedLink {
path: PathBuf,
bytes: Vec<u8>,
target: PathBuf,
identity: fs::Metadata,
}
impl CapturedLink {
fn read(path: PathBuf, prefix: &[u8]) -> Result<Self, WorktreeAdminError> {
let identity = regular_file(&path)?;
let bytes = fs::read(&path).map_err(|_| uncertain(&path))?;
let value = bytes.strip_prefix(prefix).ok_or_else(|| uncertain(&path))?;
let target = metadata_path(&path, value).map_err(|_| uncertain(&path))?;
check_identity(&path, &identity, ®ular_file(&path)?)?;
Ok(Self {
path,
bytes,
target,
identity,
})
}
fn check_at(&self, path: &Path) -> Result<(), WorktreeAdminError> {
check_identity(path, &self.identity, ®ular_file(path)?)?;
if fs::read(path).map_err(|_| uncertain(path))? != self.bytes {
return Err(uncertain(path));
}
Ok(())
}
fn resolved(&self) -> Result<PathBuf, WorktreeAdminError> {
let parent = self.path.parent().ok_or_else(|| uncertain(&self.path))?;
fs::canonicalize(parent.join(&self.target)).map_err(|_| uncertain(&self.path))
}
}
impl RepositoryLocation {
pub fn prepare_worktree_repair(&self) -> Result<WorktreeRepair, WorktreeAdminError> {
let source = self
.inferred_worktree
.as_ref()
.ok_or_else(|| uncertain(&self.git_dir))?;
let registration = checked_registration(&self.common_dir, &self.git_dir)?;
let _guard = lock(®istration)?;
let registration_identity = directory(®istration)?;
let common_identity = directory(&self.common_dir)?;
let forward = CapturedLink::read(source.join(".git"), b"gitdir: ")?;
directory(&source.join(&forward.target))?;
let back = CapturedLink::read(registration.join("gitdir"), b"")?;
let common_link = CapturedLink::read(registration.join("commondir"), b"")?;
if forward.resolved()? != registration
|| back.resolved()? != forward.path
|| common_link.resolved()? != self.common_dir
{
return Err(uncertain(®istration));
}
Ok(WorktreeRepair {
registration,
registration_identity,
common: self.common_dir.clone(),
common_identity,
forward,
back,
common_link,
})
}
}
impl WorktreeRepair {
pub fn repair(self, checkout: impl AsRef<Path>) -> Result<(), WorktreeAdminError> {
let checkout =
fs::canonicalize(checkout.as_ref()).map_err(|_| uncertain(checkout.as_ref()))?;
directory(&checkout)?;
checked_registration(&self.common, &self.registration)?;
let _guard = lock(&self.registration)?;
self.check_captured()?;
let gitfile = checkout.join(".git");
self.forward.check_at(&gitfile)?;
let forward = if self.forward.target.is_absolute() {
self.registration.clone()
} else {
relative_path(&checkout, &self.registration)
};
let back = if self.back.target.is_absolute() {
gitfile.clone()
} else {
relative_path(&self.registration, &gitfile)
};
validate_repair_path(&forward)?;
validate_repair_path(&back)?;
let mut written = Vec::new();
let forward_bytes = gitfile_line(&forward);
replace(&gitfile, &forward_bytes, &mut written)?;
let recheck = || -> Result<(), WorktreeAdminError> {
self.check_captured()?;
let published = CapturedLink::read(gitfile.clone(), b"gitdir: ")?;
if published.bytes != forward_bytes || published.resolved()? != self.registration {
return Err(uncertain(&gitfile));
}
Ok(())
};
recheck().map_err(|source| WorktreeAdminError::Io {
path: gitfile.clone(),
written: written.clone(),
source: std::io::Error::other(source),
})?;
replace(&self.back.path, &path_line(&back), &mut written)?;
Ok(())
}
fn check_captured(&self) -> Result<(), WorktreeAdminError> {
check_identity(
&self.registration,
&self.registration_identity,
&directory(&self.registration)?,
)?;
check_identity(
&self.common,
&self.common_identity,
&directory(&self.common)?,
)?;
self.forward.check_at(&self.forward.path)?;
directory(
&self
.forward
.path
.parent()
.unwrap()
.join(&self.forward.target),
)?;
self.back.check_at(&self.back.path)?;
self.common_link.check_at(&self.common_link.path)?;
if self.forward.resolved()? != self.registration
|| self.back.resolved()? != self.forward.path
|| self.common_link.resolved()? != self.common
{
return Err(uncertain(&self.registration));
}
Ok(())
}
}
fn uncertain(path: &Path) -> WorktreeAdminError {
WorktreeAdminError::Uncertain(path.into())
}
fn regular_file(path: &Path) -> Result<fs::Metadata, WorktreeAdminError> {
let metadata = fs::symlink_metadata(path).map_err(|_| uncertain(path))?;
if !metadata.is_file() {
return Err(uncertain(path));
}
Ok(metadata)
}
fn directory(path: &Path) -> Result<fs::Metadata, WorktreeAdminError> {
let metadata = fs::symlink_metadata(path).map_err(|_| uncertain(path))?;
if !metadata.is_dir() {
return Err(uncertain(path));
}
Ok(metadata)
}
fn check_identity(
path: &Path,
before: &fs::Metadata,
after: &fs::Metadata,
) -> Result<(), WorktreeAdminError> {
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
if before.dev() != after.dev() || before.ino() != after.ino() {
return Err(uncertain(path));
}
}
#[cfg(not(unix))]
let _ = (path, before, after);
Ok(())
}
fn validate_repair_path(path: &Path) -> Result<(), WorktreeAdminError> {
#[cfg(not(unix))]
if path.to_str().is_none() {
return Err(WorktreeAdminError::Path(path.into()));
}
let line = path_line(path);
let bytes = &line[..line.len() - 1];
if bytes.is_empty() || bytes.contains(&0) || matches!(bytes.last(), Some(b'\r' | b'\n')) {
return Err(WorktreeAdminError::Path(path.into()));
}
Ok(())
}