use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::{fs, io};
use super::{OpenError, Repository, canonical, io_error, metadata_path, read, read_gitfile};
#[derive(Debug)]
#[non_exhaustive]
pub struct Worktree {
pub git_dir: PathBuf,
pub path: Option<PathBuf>,
pub state: WorktreeState,
}
#[derive(Debug)]
#[non_exhaustive]
pub enum WorktreeState {
Available,
Missing,
Inaccessible(OpenError),
Invalid(OpenError),
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum WorktreeError {
#[error(transparent)]
Open(#[from] OpenError),
#[error("worktree registration limit exceeded")]
Limit,
#[error("worktree enumeration cancelled")]
Cancelled,
}
impl Repository {
pub fn worktrees(
&self,
max_entries: usize,
cancel: &AtomicBool,
) -> Result<Vec<Worktree>, WorktreeError> {
#[cfg(feature = "tracing")]
let span = tracing::debug_span!(target: "girt", "repository.worktrees", outcome = "incomplete", failure_class = tracing::field::Empty, effects = tracing::field::Empty);
let operation = || {
check(cancel)?;
let directory = self.common_dir.join("worktrees");
let entries = match fs::read_dir(&directory) {
Ok(entries) => entries,
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(error) => return Err(io_error(&directory, error).into()),
};
let mut paths = Vec::new();
for entry in entries {
check(cancel)?;
let entry = entry.map_err(|error| io_error(&directory, error))?;
if paths.len() == max_entries {
return Err(WorktreeError::Limit);
}
paths.push(entry.path());
}
paths.sort();
let mut result = Vec::with_capacity(paths.len());
for git_dir in paths {
check(cancel)?;
let mut entry = Worktree {
git_dir,
path: None,
state: WorktreeState::Available,
};
entry.state = match inspect(&mut entry, &self.common_dir) {
Ok(()) => WorktreeState::Available,
Err(error) => match &error {
OpenError::Io { source, .. } => match source.kind() {
io::ErrorKind::NotFound => WorktreeState::Missing,
io::ErrorKind::NotADirectory | io::ErrorKind::IsADirectory => {
WorktreeState::Invalid(error)
}
_ => WorktreeState::Inaccessible(error),
},
_ => WorktreeState::Invalid(error),
},
};
result.push(entry);
}
Ok(result)
};
#[cfg(feature = "tracing")]
let result = span.in_scope(operation);
#[cfg(not(feature = "tracing"))]
let result = operation();
#[cfg(feature = "tracing")]
crate::trace::finish(&span, &result, |error| match error {
WorktreeError::Open(_) => "io",
WorktreeError::Limit => "limit",
WorktreeError::Cancelled => "cancelled",
});
result
}
pub(crate) fn open_retained_worktree(&self, git_dir: &Path) -> Result<Self, OpenError> {
let registrations = self.common_dir.join("worktrees");
for directory in [®istrations, git_dir] {
let metadata =
fs::symlink_metadata(directory).map_err(|error| io_error(directory, error))?;
if !metadata.is_dir() || canonical(directory)? != *directory {
return Err(super::malformed(
directory,
"registration directory identity changed",
));
}
}
if git_dir.parent() != Some(registrations.as_path()) {
return Err(super::malformed(
git_dir,
"not a registered metadata directory",
));
}
for name in ["gitdir", "commondir", "HEAD"] {
let path = git_dir.join(name);
let metadata = fs::symlink_metadata(&path).map_err(|error| io_error(&path, error))?;
if !metadata.is_file() {
return Err(super::malformed(
&path,
"registration metadata is not a regular file",
));
}
}
let backlink = git_dir.join("gitdir");
let target = metadata_path(&backlink, &read(&backlink)?)?;
if target.as_os_str().as_encoded_bytes().contains(&b'\n') {
return Err(super::malformed(&backlink, "backlink spans several lines"));
}
let repository = self.open_worktree(git_dir)?;
if repository.git_dir != git_dir {
return Err(super::malformed(
git_dir,
"registration resolves to another directory",
));
}
Ok(repository)
}
pub fn open_worktree(&self, path: impl AsRef<Path>) -> Result<Self, OpenError> {
let repository = Self::open(path)?;
if repository.common_dir != self.common_dir {
return Err(OpenError::Unrelated(repository.common_dir));
}
Ok(repository)
}
}
fn inspect(entry: &mut Worktree, common: &Path) -> Result<(), OpenError> {
let metadata =
fs::symlink_metadata(&entry.git_dir).map_err(|error| io_error(&entry.git_dir, error))?;
if !metadata.is_dir() {
return Err(super::malformed(
&entry.git_dir,
"registration is not a directory",
));
}
let backlink = entry.git_dir.join("gitdir");
let target = entry
.git_dir
.join(metadata_path(&backlink, &read(&backlink)?)?);
entry.path = target.parent().map(Path::to_path_buf);
let commondir = entry.git_dir.join("commondir");
let shared = entry
.git_dir
.join(metadata_path(&commondir, &read(&commondir)?)?);
let shared = canonical(&shared)?;
if shared != common {
return Err(OpenError::Unrelated(shared));
}
let checkout = target
.parent()
.ok_or_else(|| super::malformed(&backlink, "backlink has no parent"))?;
let checkout_metadata = fs::metadata(checkout).map_err(|error| io_error(checkout, error))?;
if !checkout_metadata.is_dir() {
return Err(super::malformed(checkout, "checkout is not a directory"));
}
fs::metadata(&target).map_err(|error| io_error(&target, error))?;
if read_gitfile(&target)? != canonical(&entry.git_dir)? {
return Err(super::malformed(
&backlink,
"backlink points to another repository",
));
}
entry.path = target.parent().map(canonical).transpose()?;
Ok(())
}
fn check(cancel: &AtomicBool) -> Result<(), WorktreeError> {
if cancel.load(Ordering::Relaxed) {
Err(WorktreeError::Cancelled)
} else {
Ok(())
}
}