use std::fs::{self, OpenOptions};
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use thiserror::Error;
use super::worktree_create::{gitfile_line, path_line, relative_path, validate_path};
use super::{Repository, WorktreeLinkStyle, metadata_path};
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum WorktreeAdminError {
#[error("worktree state is uncertain at {0}")]
Uncertain(PathBuf),
#[error("worktree registration is protected at {0}")]
Protected(PathBuf),
#[error("worktree administration is busy at {0}")]
Busy(PathBuf),
#[error("unsupported worktree metadata path: {0}")]
Path(PathBuf),
#[error("worktree administration failed at {path}")]
Io {
path: PathBuf,
written: Vec<PathBuf>,
#[source]
source: io::Error,
},
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum WorktreeRetirement {
Confirmed,
}
pub(super) struct AdminLock {
path: PathBuf,
cleanup: crate::locks::Guard,
}
impl Drop for AdminLock {
fn drop(&mut self) {
let _lock_operation = crate::locks::operation();
if self.cleanup.check_path().is_ok() {
let _ = fs::remove_file(&self.path);
}
}
}
pub(super) fn lock(registration: &Path) -> Result<AdminLock, WorktreeAdminError> {
let _lock_operation = crate::locks::operation();
let path = std::path::absolute(registration.join("girt-admin.lock")).map_err(|source| {
WorktreeAdminError::Io {
path: registration.to_owned(),
written: Vec::new(),
source,
}
})?;
match OpenOptions::new().write(true).create_new(true).open(&path) {
Ok(file) => {
let identity = crate::locks::capture_identity(&path, &file).map_err(|source| {
WorktreeAdminError::Io {
path: path.clone(),
written: Vec::new(),
source,
}
})?;
let file = crate::locks::SharedFile::from(file);
let cleanup = crate::locks::Guard::new(&path, &file, identity);
Ok(AdminLock { path, cleanup })
}
Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {
Err(WorktreeAdminError::Busy(path))
}
Err(source) => Err(WorktreeAdminError::Io {
path,
written: Vec::new(),
source,
}),
}
}
pub(super) fn checked_registration(
common_dir: &Path,
path: &Path,
) -> Result<PathBuf, WorktreeAdminError> {
let root = common_dir.join("worktrees");
if path.parent() != Some(root.as_path()) {
return Err(WorktreeAdminError::Uncertain(path.into()));
}
let metadata =
fs::symlink_metadata(path).map_err(|_| WorktreeAdminError::Uncertain(path.into()))?;
if !metadata.is_dir()
|| fs::canonicalize(path).map_err(|_| WorktreeAdminError::Uncertain(path.into()))? != path
{
return Err(WorktreeAdminError::Uncertain(path.into()));
}
Ok(path.into())
}
fn read_link(path: &Path, prefix: &[u8]) -> Result<PathBuf, WorktreeAdminError> {
let bytes = fs::read(path).map_err(|_| WorktreeAdminError::Uncertain(path.into()))?;
let bytes = bytes
.strip_prefix(prefix)
.ok_or_else(|| WorktreeAdminError::Uncertain(path.into()))?;
metadata_path(path, bytes).map_err(|_| WorktreeAdminError::Uncertain(path.into()))
}
fn resolved(source: &Path, target: &Path) -> PathBuf {
source.parent().unwrap_or(Path::new(".")).join(target)
}
pub(super) fn replace(
path: &Path,
bytes: &[u8],
written: &mut Vec<PathBuf>,
) -> Result<(), WorktreeAdminError> {
let temp = path.with_extension("girt-repair-temp");
let mut file = OpenOptions::new()
.write(true)
.create_new(true)
.open(&temp)
.map_err(|source| WorktreeAdminError::Io {
path: temp.clone(),
written: written.clone(),
source,
})?;
file.write_all(bytes)
.map_err(|source| WorktreeAdminError::Io {
path: temp.clone(),
written: written.clone(),
source,
})?;
file.sync_all().map_err(|source| WorktreeAdminError::Io {
path: temp.clone(),
written: written.clone(),
source,
})?;
fs::rename(&temp, path).map_err(|source| WorktreeAdminError::Io {
path: path.into(),
written: written.clone(),
source,
})?;
written.push(path.into());
Ok(())
}
impl Repository {
pub fn repair_worktree(
&self,
registration: impl AsRef<Path>,
checkout: impl AsRef<Path>,
) -> Result<(), WorktreeAdminError> {
self.repair_worktree_with_link_style(registration, checkout, WorktreeLinkStyle::Configured)
}
pub fn repair_worktree_with_link_style(
&self,
registration: impl AsRef<Path>,
checkout: impl AsRef<Path>,
link_style: WorktreeLinkStyle,
) -> Result<(), WorktreeAdminError> {
let _lock_operation = crate::locks::operation();
let registration = checked_registration(&self.common_dir, registration.as_ref())?;
let checkout = fs::canonicalize(checkout.as_ref())
.map_err(|_| WorktreeAdminError::Uncertain(checkout.as_ref().into()))?;
if !checkout.is_dir() {
return Err(WorktreeAdminError::Uncertain(checkout));
}
let gitfile = checkout.join(".git");
let _guard = lock(®istration)?;
let old_forward = read_link(&gitfile, b"gitdir: ")?;
let old_target = resolved(&gitfile, &old_forward);
if old_target.file_name() != registration.file_name()
|| old_target.parent().and_then(Path::file_name)
!= Some(std::ffi::OsStr::new("worktrees"))
{
return Err(WorktreeAdminError::Protected(gitfile));
}
match fs::symlink_metadata(&old_target) {
Ok(_) => {
let old_canonical = fs::canonicalize(&old_target)
.map_err(|_| WorktreeAdminError::Uncertain(old_target.clone()))?;
if old_canonical != registration {
return Err(WorktreeAdminError::Protected(gitfile));
}
}
Err(error) if error.kind() == io::ErrorKind::NotFound => (),
Err(_) => return Err(WorktreeAdminError::Uncertain(old_target)),
}
let backlink = registration.join("gitdir");
let _ = read_link(&backlink, b"")?;
let commondir = registration.join("commondir");
let _ = read_link(&commondir, b"")?;
let relative = self.common_dir.join("config");
let config =
fs::read(&relative).map_err(|_| WorktreeAdminError::Uncertain(relative.clone()))?;
let config = crate::Config::parse(&config)
.map_err(|_| WorktreeAdminError::Uncertain(relative.clone()))?;
let configured_relative = super::extension_boolean(&config, &relative, "relativeworktrees")
.map_err(|_| WorktreeAdminError::Uncertain(relative))?;
let use_relative = link_style.uses_relative(configured_relative);
let forward = if use_relative {
relative_path(&checkout, ®istration)
} else {
registration.clone()
};
let back = if use_relative {
relative_path(®istration, &gitfile)
} else {
gitfile.clone()
};
validate_path(&forward).map_err(|_| WorktreeAdminError::Path(forward.clone()))?;
validate_path(&back).map_err(|_| WorktreeAdminError::Path(back.clone()))?;
let mut written = Vec::new();
replace(&commondir, b"../..\n", &mut written)?;
replace(&gitfile, &gitfile_line(&forward), &mut written)?;
replace(&backlink, &path_line(&back), &mut written)?;
Ok(())
}
pub fn lock_worktree(
&self,
registration: impl AsRef<Path>,
reason: &str,
) -> Result<(), WorktreeAdminError> {
let _lock_operation = crate::locks::operation();
let registration = checked_registration(&self.common_dir, registration.as_ref())?;
if reason.contains(['\n', '\r']) {
return Err(WorktreeAdminError::Uncertain(registration));
}
let _guard = lock(®istration)?;
let path = registration.join("locked");
let mut file = OpenOptions::new()
.write(true)
.create_new(true)
.open(&path)
.map_err(|source| WorktreeAdminError::Io {
path: path.clone(),
written: Vec::new(),
source,
})?;
file.write_all(reason.as_bytes())
.map_err(|source| WorktreeAdminError::Io {
path,
written: Vec::new(),
source,
})
}
pub fn unlock_worktree(
&self,
registration: impl AsRef<Path>,
reason: &str,
) -> Result<(), WorktreeAdminError> {
let _lock_operation = crate::locks::operation();
let registration = checked_registration(&self.common_dir, registration.as_ref())?;
let _guard = lock(®istration)?;
let path = registration.join("locked");
if fs::read(&path).map_err(|_| WorktreeAdminError::Uncertain(path.clone()))?
!= reason.as_bytes()
{
return Err(WorktreeAdminError::Protected(path));
}
fs::remove_file(&path).map_err(|source| WorktreeAdminError::Io {
path,
written: Vec::new(),
source,
})
}
pub fn prune_worktree(
&self,
registration: impl AsRef<Path>,
expire_before: SystemTime,
_retirement: WorktreeRetirement,
) -> Result<(), WorktreeAdminError> {
let _lock_operation = crate::locks::operation();
let registration = checked_registration(&self.common_dir, registration.as_ref())?;
let _guard = lock(®istration)?;
let locked = registration.join("locked");
match fs::symlink_metadata(&locked) {
Ok(_) => return Err(WorktreeAdminError::Protected(locked)),
Err(error) if error.kind() == io::ErrorKind::NotFound => (),
Err(_) => return Err(WorktreeAdminError::Uncertain(locked)),
}
let gitdir = registration.join("gitdir");
let target = resolved(&gitdir, &read_link(&gitdir, b"")?);
let age = fs::metadata(&gitdir)
.map_err(|_| WorktreeAdminError::Uncertain(gitdir.clone()))?
.modified()
.map_err(|_| WorktreeAdminError::Uncertain(gitdir.clone()))?;
if age >= expire_before {
return Err(WorktreeAdminError::Protected(registration));
}
match fs::symlink_metadata(&target) {
Ok(_) => return Err(WorktreeAdminError::Protected(target)),
Err(error) if error.kind() == io::ErrorKind::NotFound => (),
Err(_) => return Err(WorktreeAdminError::Uncertain(target)),
}
fs::remove_dir_all(®istration).map_err(|source| WorktreeAdminError::Io {
path: registration,
written: Vec::new(),
source,
})
}
}