mod colocation;
mod command_layout;
mod discover;
mod init;
mod object_values;
mod operation;
mod reflog_policy;
mod shallow;
mod worktree_admin;
mod worktree_create;
mod worktree_repair;
mod worktrees;
use std::path::{Path, PathBuf};
use std::{fs, io};
pub use colocation::{ColocationEdit, ColocationError};
pub use init::{InitError, InitKind, InitOptions};
pub(crate) use init::{initial_branch, initial_config};
pub use operation::{OperationCleanupError, OperationError, OperationLimits, OperationState};
pub use reflog_policy::ReflogPolicyError;
pub use shallow::{ShallowError, ShallowRoots};
use thiserror::Error;
pub use worktree_admin::{WorktreeAdminError, WorktreeRetirement};
pub use worktree_create::{CreateWorktreeError, OrphanWorktreeOptions, WorktreeDurability};
pub use worktree_repair::WorktreeRepair;
pub use worktrees::{Worktree, WorktreeError, WorktreeState};
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
#[non_exhaustive]
pub enum WorktreeLinkStyle {
#[default]
Configured,
Relative,
Absolute,
}
impl WorktreeLinkStyle {
fn uses_relative(self, configured: bool) -> bool {
match self {
Self::Configured => configured,
Self::Relative => true,
Self::Absolute => false,
}
}
}
use crate::config::{
ConfigFile, ConfigInputs, ConfigScope, IncludePlacement, ResolveError,
boolean as config_boolean, integer,
};
use crate::{Config, ConfigError, LooseObjects, ObjectFormat};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RepositoryLocation {
git_dir: PathBuf,
common_dir: PathBuf,
object_dir: Option<PathBuf>,
common_dir_redirected: bool,
logical_git_dir: Option<PathBuf>,
inferred_worktree: Option<PathBuf>,
}
impl RepositoryLocation {
pub fn at_git_dir(path: impl AsRef<Path>) -> Result<Self, OpenError> {
Self::resolve(path.as_ref(), true)
}
pub fn git_dir(&self) -> &Path {
&self.git_dir
}
pub fn common_dir(&self) -> &Path {
&self.common_dir
}
pub fn open_with_config(&self, inputs: &ConfigInputs) -> Result<Repository, OpenError> {
Repository::open_location(self, inputs, crate::refs::reftable::StackLimits::default())
}
pub fn read_metadata_with_config(
&self,
inputs: &ConfigInputs,
) -> Result<RepositoryMetadata, OpenError> {
self.read_metadata_with_config_and_include_placement(inputs, IncludePlacement::InPlace)
}
pub fn read_metadata_with_config_and_include_placement(
&self,
inputs: &ConfigInputs,
placement: IncludePlacement,
) -> Result<RepositoryMetadata, OpenError> {
RepositoryMetadata::read_location(
self,
inputs,
crate::refs::reftable::StackLimits::default(),
None,
placement,
)
}
fn resolve(path: &Path, exact: bool) -> Result<Self, OpenError> {
Self::resolve_storage(path, exact, None, None)
}
fn resolve_storage(
path: &Path,
exact: bool,
common_override: Option<&Path>,
object_override: Option<&Path>,
) -> Result<Self, OpenError> {
let input = path;
let metadata = match fs::metadata(input) {
Ok(metadata) => metadata,
Err(error) if error.kind() == io::ErrorKind::NotFound => {
return Err(OpenError::NotFound(input.into()));
}
Err(source) => return Err(io_error(input, source)),
};
let logical_input = if input.is_absolute() {
input.to_path_buf()
} else {
std::path::absolute(input).map_err(|error| io_error(input, error))?
};
let logical_git_dir = if !exact && metadata.is_dir() && logical_input.join(".git").is_dir()
{
Some(logical_input.join(".git"))
} else if metadata.is_dir() && (exact || !logical_input.join(".git").exists()) {
Some(logical_input.clone())
} else if metadata.is_file() {
Some(gitfile_target(&logical_input)?)
} else if metadata.is_dir() && logical_input.join(".git").is_file() {
Some(gitfile_target(&logical_input.join(".git"))?)
} else {
None
};
let input = canonical(input)?;
let (git_dir, inferred_worktree) = if metadata.is_file() {
(
read_gitfile(&logical_input)?,
logical_input.parent().map(canonical).transpose()?,
)
} else if !exact && entry_exists(&input.join(".git"))? {
let dotgit = input.join(".git");
let git_dir = if dotgit.is_dir() {
canonical(&dotgit)?
} else {
read_gitfile(&dotgit)?
};
(git_dir, Some(input.clone()))
} else {
let parent = logical_input.parent();
let dotgit_identity = if exact {
(logical_input.file_name() == Some(std::ffi::OsStr::new(".git")))
.then(|| input.clone())
} else {
parent.and_then(|parent| fs::canonicalize(parent.join(".git")).ok())
};
let inferred = parent
.filter(|_| dotgit_identity.as_deref() == Some(input.as_path()))
.map(canonical)
.transpose()?;
(input.clone(), inferred)
};
require_directory(&git_dir)?;
if !exact {
if !exists(&git_dir.join("HEAD"))? {
if inferred_worktree.is_some() || exists(&git_dir.join("objects"))? {
return Err(malformed(&git_dir, "missing HEAD marker"));
}
return Err(OpenError::NotFound(input));
}
validate_head(&git_dir.join("HEAD"))?;
}
let common_file = git_dir.join("commondir");
let (common_dir, common_dir_redirected) = if let Some(common) = common_override {
(canonical(common)?, true)
} else if exists(&common_file)? {
let relative = metadata_path(&common_file, &read(&common_file)?)?;
(canonical(&git_dir.join(relative))?, true)
} else {
(git_dir.clone(), false)
};
require_directory(&common_dir)?;
Ok(Self {
git_dir,
common_dir,
object_dir: object_override.map(Path::to_path_buf),
common_dir_redirected,
logical_git_dir,
inferred_worktree,
})
}
}
#[derive(Clone, Debug)]
pub struct RepositoryMetadata {
git_dir: PathBuf,
common_dir: PathBuf,
object_dir: PathBuf,
worktree: Option<PathBuf>,
bare: bool,
config: Config,
format_version: u32,
object_format: ObjectFormat,
reference_backend: crate::refs::Backend,
worktree_config_conflict: bool,
}
impl RepositoryMetadata {
pub fn git_dir(&self) -> &Path {
&self.git_dir
}
pub fn common_dir(&self) -> &Path {
&self.common_dir
}
pub fn object_dir(&self) -> &Path {
&self.object_dir
}
pub fn worktree(&self) -> Option<&Path> {
self.worktree.as_deref()
}
pub fn is_bare(&self) -> bool {
self.bare
}
pub fn format_version(&self) -> u32 {
self.format_version
}
pub fn object_format(&self) -> ObjectFormat {
self.object_format
}
pub fn reference_backend(&self) -> crate::refs::Backend {
self.reference_backend
}
pub fn config(&self) -> &Config {
&self.config
}
pub fn open_storage(&self) -> Result<Repository, OpenError> {
let shallow = self.read_shallow()?;
Ok(self.clone().into_storage(shallow))
}
fn read_shallow(&self) -> Result<ShallowRoots, OpenError> {
Ok(ShallowRoots::read(
self.common_dir.join("shallow"),
self.object_format,
16 * 1024 * 1024,
&std::sync::atomic::AtomicBool::new(false),
)?)
}
fn into_storage(self, shallow: ShallowRoots) -> Repository {
Repository {
git_dir: self.git_dir,
common_dir: self.common_dir,
object_dir: self.object_dir,
worktree: self.worktree,
bare: self.bare,
config: self.config,
format_version: self.format_version,
object_format: self.object_format,
shallow,
reference_backend: self.reference_backend,
}
}
fn read_location(
location: &RepositoryLocation,
inputs: &ConfigInputs,
reference_limits: crate::refs::reftable::StackLimits,
command: Option<command_layout::CommandWorktree<'_>>,
placement: IncludePlacement,
) -> Result<Self, OpenError> {
let RepositoryLocation {
git_dir,
common_dir,
object_dir,
common_dir_redirected,
logical_git_dir,
inferred_worktree,
} = location.clone();
if !exists(&git_dir.join("HEAD"))? {
if inferred_worktree.is_some() || exists(&git_dir.join("objects"))? {
return Err(malformed(&git_dir, "missing HEAD marker"));
}
return Err(OpenError::NotFound(git_dir.clone()));
}
validate_head(&git_dir.join("HEAD"))?;
let object_dir = object_dir.unwrap_or_else(|| common_dir.join("objects"));
require_directory(&object_dir)?;
require_directory(&common_dir.join("refs"))?;
let config_path = common_dir.join("config");
let bytes = if exists(&config_path)? {
read_config(&config_path, inputs.limits.bytes)?
} else {
Vec::new()
};
let config = Config::parse(&bytes).map_err(|source| OpenError::Config {
path: config_path.clone(),
source,
})?;
if command.is_some()
&& config
.value("core", None, "repositoryformatversion")
.is_none()
{
return Err(unsupported(
&config_path,
"command layout without repository format version",
));
}
let (format_version, object_format) = validate_config(&config, &config_path)?;
let reference_backend = match config.value("extensions", None, "refstorage") {
Some(Some(b"reftable")) => crate::refs::Backend::Reftable,
_ => crate::refs::Backend::Files,
};
let mut inputs = inputs.clone();
inputs.context.git_dirs.push(git_dir.clone());
if let Some(logical_git_dir) = logical_git_dir {
inputs.context.git_dirs.push(logical_git_dir);
}
let head = read(&git_dir.join("HEAD"))?;
let head = head.strip_suffix(b"\n").unwrap_or(&head);
let head = head.strip_suffix(b"\r").unwrap_or(head);
inputs.context.branch = if reference_backend == crate::refs::Backend::Reftable {
let snapshot = crate::refs::reftable::Snapshot::read(
&git_dir.join("reftable"),
object_format,
reference_limits,
&std::sync::atomic::AtomicBool::new(false),
)?;
snapshot
.table
.references
.into_iter()
.find(|record| record.name.as_bytes() == b"HEAD")
.and_then(|record| match record.target {
Some(crate::refs::Target::Symbolic(name)) => name
.as_bytes()
.strip_prefix(b"refs/heads/")
.map(<[u8]>::to_vec),
_ => None,
})
} else {
head.strip_prefix(b"ref: refs/heads/").map(<[u8]>::to_vec)
};
inputs.files.push(ConfigFile {
path: config_path.clone(),
scope: ConfigScope::Local,
optional: true,
skip_unreadable: false,
});
let worktree_config_enabled = extension_boolean(&config, &config_path, "worktreeconfig")?;
let ignore_common_layout = if command.is_some() {
common_dir_redirected
} else {
git_dir != common_dir && !worktree_config_enabled
};
let mut layout_config = if ignore_common_layout {
Config::parse(b"").expect("empty configuration")
} else {
config.clone()
};
if worktree_config_enabled {
let worktree_path = git_dir.join("config.worktree");
if exists(&worktree_path)? {
let worktree_config =
Config::parse(&read_config(&worktree_path, inputs.limits.bytes)?).map_err(
|source| OpenError::Config {
path: worktree_path.clone(),
source,
},
)?;
layout_config.append(&worktree_config);
}
inputs.files.push(ConfigFile {
path: worktree_path,
scope: ConfigScope::Worktree,
optional: true,
skip_unreadable: false,
});
}
let (worktree, bare, worktree_config_conflict) = if let Some(command) = command {
command.resolve(&git_dir, &layout_config, &config_path)?
} else {
let worktree = resolve_worktree(
&git_dir,
&common_dir,
inferred_worktree,
&layout_config,
&config_path,
)?;
let bare = boolean(&layout_config, &config_path, "bare")?.unwrap_or(worktree.is_none());
(worktree, bare, false)
};
let config = Config::resolve_with_include_placement(&inputs, placement)?;
Ok(Self {
git_dir,
common_dir,
object_dir,
worktree,
bare,
config,
format_version,
object_format,
reference_backend,
worktree_config_conflict,
})
}
}
#[derive(Clone)]
pub struct Repository {
git_dir: PathBuf,
common_dir: PathBuf,
object_dir: PathBuf,
worktree: Option<PathBuf>,
bare: bool,
config: Config,
format_version: u32,
object_format: ObjectFormat,
shallow: ShallowRoots,
reference_backend: crate::refs::Backend,
}
impl std::fmt::Debug for Repository {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Repository")
.field("git_dir", &self.git_dir)
.field("common_dir", &self.common_dir)
.field("worktree", &self.worktree)
.field("bare", &self.bare)
.field("object_format", &self.object_format)
.field("reference_backend", &self.reference_backend)
.finish_non_exhaustive()
}
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum OpenError {
#[error(transparent)]
References(#[from] crate::refs::ReferenceError),
#[error("unrelated common repository: {0}")]
Unrelated(PathBuf),
#[error(transparent)]
Shallow(#[from] ShallowError),
#[error("no repository at {0}")]
NotFound(PathBuf),
#[error("cannot read {path}")]
Io {
path: PathBuf,
#[source]
source: io::Error,
},
#[error("malformed repository metadata at {path}: {reason}")]
Malformed {
path: PathBuf,
reason: String,
},
#[error("invalid configuration at {path}")]
Config {
path: PathBuf,
#[source]
source: ConfigError,
},
#[error(transparent)]
Resolve(#[from] ResolveError),
#[error("unsupported repository feature at {path}: {feature}")]
Unsupported {
path: PathBuf,
feature: String,
},
}
impl Repository {
pub(crate) fn with_private_object_directory(&self, directory: &Path) -> Self {
let mut repository = self.clone();
repository.object_dir = directory.to_owned();
repository
}
pub fn reference_backend(&self) -> crate::refs::Backend {
self.reference_backend
}
pub fn references(&self) -> Result<crate::refs::References<'_>, crate::refs::ReferenceError> {
crate::refs::References::new(self)
}
pub fn head(&self) -> Result<crate::refs::Resolution, crate::refs::ReferenceError> {
self.references()?.resolve(&crate::refs::RefName::HEAD)
}
pub fn open(path: impl AsRef<Path>) -> Result<Self, OpenError> {
Self::open_with_config(path, &ConfigInputs::default())
}
pub fn open_with_config(
path: impl AsRef<Path>,
inputs: &ConfigInputs,
) -> Result<Self, OpenError> {
Self::open_with_config_and_reference_limits(
path,
inputs,
crate::refs::reftable::StackLimits::default(),
)
}
pub fn open_with_config_and_reference_limits(
path: impl AsRef<Path>,
inputs: &ConfigInputs,
reference_limits: crate::refs::reftable::StackLimits,
) -> Result<Self, OpenError> {
let location = RepositoryLocation::resolve(path.as_ref(), false)?;
Self::open_location(&location, inputs, reference_limits)
}
fn open_location(
location: &RepositoryLocation,
inputs: &ConfigInputs,
reference_limits: crate::refs::reftable::StackLimits,
) -> Result<Self, OpenError> {
let metadata = RepositoryMetadata::read_location(
location,
inputs,
reference_limits,
None,
IncludePlacement::InPlace,
)?;
let shallow = metadata.read_shallow()?;
Ok(metadata.into_storage(shallow))
}
pub fn shallow_roots(&self) -> &ShallowRoots {
&self.shallow
}
pub fn refresh_shallow(
&mut self,
max_bytes: usize,
cancel: &std::sync::atomic::AtomicBool,
) -> Result<(), ShallowError> {
let roots = ShallowRoots::read(
self.common_dir.join("shallow"),
self.object_format,
max_bytes,
cancel,
)?;
self.shallow = roots;
Ok(())
}
pub fn git_dir(&self) -> &Path {
&self.git_dir
}
pub fn common_dir(&self) -> &Path {
&self.common_dir
}
pub fn object_dir(&self) -> &Path {
&self.object_dir
}
pub fn worktree(&self) -> Option<&Path> {
self.worktree.as_deref()
}
pub fn is_bare(&self) -> bool {
self.bare
}
pub fn config(&self) -> &Config {
&self.config
}
pub fn format_version(&self) -> u32 {
self.format_version
}
pub fn object_format(&self) -> ObjectFormat {
self.object_format
}
pub fn objects_with(
&self,
limits: crate::PackLimits,
) -> Result<crate::Objects, crate::ObjectReadError> {
self.objects_with_alternates(limits, crate::AlternateLimits::default())
}
pub fn objects(&self) -> Result<crate::Objects, crate::ObjectReadError> {
self.objects_with(crate::PackLimits::default())
}
pub fn objects_with_alternates(
&self,
packs: crate::PackLimits,
alternates: crate::AlternateLimits,
) -> Result<crate::Objects, crate::ObjectReadError> {
let mut objects =
crate::Objects::open(self.object_format, &self.object_dir, packs, alternates)?;
objects.shallow = self.shallow.clone();
Ok(objects)
}
pub fn objects_controlled(
&self,
packs: crate::PackLimits,
alternates: crate::AlternateLimits,
cancelled: &std::sync::atomic::AtomicBool,
) -> Result<crate::Objects, crate::ObjectReadError> {
let mut objects = crate::Objects::open_controlled(
self.object_format,
&self.object_dir,
packs,
alternates,
cancelled,
)?;
objects.shallow = self.shallow.clone();
Ok(objects)
}
pub fn loose_objects(&self) -> LooseObjects {
LooseObjects::new(&self.object_dir, self.object_format())
}
}
fn read_config(path: &Path, max_bytes: usize) -> Result<Vec<u8>, OpenError> {
use std::io::Read;
let file = fs::File::open(path).map_err(|error| io_error(path, error))?;
let mut bytes = Vec::new();
file.take(max_bytes.saturating_add(1) as u64)
.read_to_end(&mut bytes)
.map_err(|error| io_error(path, error))?;
if bytes.len() > max_bytes {
return Err(ResolveError {
location: crate::config::SourceLocation {
path: Some(path.into()),
line: 1,
},
included_from: Vec::new(),
source: crate::config::ResolveFailure::Limit("bootstrap configuration bytes"),
}
.into());
}
Ok(bytes)
}
fn validate_config(config: &Config, path: &Path) -> Result<(u32, ObjectFormat), OpenError> {
for entry in config.entries() {
if entry.section.eq_ignore_ascii_case(b"extensions")
&& (entry.subsection.is_some()
|| !entry.name.eq_ignore_ascii_case(b"objectformat")
&& !entry.name.eq_ignore_ascii_case(b"worktreeconfig")
&& !entry.name.eq_ignore_ascii_case(b"relativeworktrees")
&& !entry.name.eq_ignore_ascii_case(b"noop")
&& !entry.name.eq_ignore_ascii_case(b"preciousobjects")
&& !entry.name.eq_ignore_ascii_case(b"partialclone")
&& !entry.name.eq_ignore_ascii_case(b"refstorage"))
{
return Err(unsupported(
path,
&format!(
"extensions.{}",
String::from_utf8_lossy(&entry.name.to_ascii_lowercase())
),
));
}
}
let version = match config.value("core", None, "repositoryformatversion") {
None => 0,
Some(Some(value)) => parse_version(value)
.ok_or_else(|| malformed(path, "invalid core.repositoryformatversion"))?,
Some(None) => return Err(malformed(path, "implicit core.repositoryformatversion")),
};
if version > 1 {
return Err(unsupported(
path,
&format!("repository format version {version}"),
));
}
extension_boolean(config, path, "relativeworktrees")?;
extension_boolean(config, path, "preciousobjects")?;
if config.value("extensions", None, "partialclone") == Some(None) {
return Err(malformed(path, "implicit extensions.partialClone"));
}
if let Some(value) = config.value("extensions", None, "refstorage")
&& (version != 1 || !matches!(value, Some(b"files" | b"reftable")))
{
return Err(unsupported(
path,
"unknown or version-zero extensions.refStorage",
));
}
let mut object_format = ObjectFormat::Sha1;
if let Some(value) = config.value("extensions", None, "objectformat") {
if version == 0 {
return Err(unsupported(
path,
"extensions.objectFormat requires repository version 1",
));
}
match value {
Some(b"sha1") => (),
Some(b"sha256") => object_format = ObjectFormat::Sha256,
_ => return Err(unsupported(path, "unknown extensions.objectFormat")),
}
}
Ok((version, object_format))
}
fn extension_boolean(config: &Config, path: &Path, name: &str) -> Result<bool, OpenError> {
match config.value("extensions", None, name) {
None => Ok(false),
Some(value) => {
config_boolean(value).ok_or_else(|| malformed(path, "invalid extension boolean"))
}
}
}
fn parse_version(value: &[u8]) -> Option<u32> {
u32::try_from(integer(value)?).ok()
}
fn boolean(config: &Config, path: &Path, name: &str) -> Result<Option<bool>, OpenError> {
match config.value("core", None, name) {
None => Ok(None),
Some(value) => config_boolean(value)
.map(Some)
.ok_or_else(|| malformed(path, &format!("invalid core.{name} boolean"))),
}
}
fn resolve_worktree(
git_dir: &Path,
common_dir: &Path,
inferred: Option<PathBuf>,
config: &Config,
source: &Path,
) -> Result<Option<PathBuf>, OpenError> {
let bare = boolean(config, source, "bare")?;
let configured = config.value("core", None, "worktree");
if git_dir != common_dir && configured.is_none() && bare != Some(true) {
let backlink = git_dir.join("gitdir");
let target = git_dir.join(metadata_path(&backlink, &read(&backlink)?)?);
let root = target
.parent()
.ok_or_else(|| malformed(&backlink, "backlink has no parent"))?;
if let Some(inferred) = inferred {
if canonical(root)? != inferred || read_gitfile(&target)? != git_dir {
return Err(malformed(&backlink, "worktree and backlink disagree"));
}
return Ok(Some(inferred));
}
return Ok(Some(available_path(root)?));
}
if let Some(value) = configured {
if bare == Some(true) {
return Err(malformed(source, "core.bare and core.worktree conflict"));
}
let value = value
.filter(|value| !value.is_empty())
.ok_or_else(|| malformed(source, "empty core.worktree"))?;
if value.starts_with(b"~") || value.starts_with(b"%(") {
return Err(unsupported(source, "core.worktree path interpolation"));
}
return Ok(Some(available_path(
&git_dir.join(path_bytes(source, value)?),
)?));
}
if bare == Some(true) {
return Ok(None);
}
if let Some(root) = inferred {
return Ok(Some(root));
}
Ok(None)
}
fn validate_head(path: &Path) -> Result<(), OpenError> {
let bytes = read(path)?;
let bytes = bytes.strip_suffix(b"\n").unwrap_or(&bytes);
let bytes = bytes.strip_suffix(b"\r").unwrap_or(bytes);
let symbolic = bytes.strip_prefix(b"ref: ").is_some_and(|name| {
name.starts_with(b"refs/") && !name.iter().any(|b| b.is_ascii_whitespace() || *b == 0)
});
let detached = matches!(bytes.len(), 40 | 64) && bytes.iter().all(u8::is_ascii_hexdigit);
if !symbolic && !detached {
return Err(malformed(
path,
"invalid HEAD marker (reference resolution is not performed)",
));
}
Ok(())
}
fn read_gitfile(path: &Path) -> Result<PathBuf, OpenError> {
canonical(&gitfile_target(path)?)
}
fn gitfile_target(path: &Path) -> Result<PathBuf, OpenError> {
let bytes = read(path)?;
let value = bytes
.strip_prefix(b"gitdir: ")
.ok_or_else(|| malformed(path, "expected gitdir: indirection"))?;
let target = metadata_path(path, value)?;
Ok(path.parent().unwrap_or(Path::new(".")).join(target))
}
fn metadata_path(source: &Path, bytes: &[u8]) -> Result<PathBuf, OpenError> {
let end = bytes
.iter()
.rposition(|byte| !matches!(byte, b'\r' | b'\n'));
let bytes = &bytes[..end.map_or(0, |index| index + 1)];
if bytes.is_empty() || bytes.contains(&0) {
return Err(malformed(source, "empty path or NUL in path"));
}
path_bytes(source, bytes)
}
fn path_bytes(source: &Path, bytes: &[u8]) -> Result<PathBuf, OpenError> {
if bytes.contains(&0) {
return Err(malformed(source, "NUL in path"));
}
#[cfg(unix)]
{
use std::os::unix::ffi::OsStrExt;
Ok(PathBuf::from(std::ffi::OsStr::from_bytes(bytes)))
}
#[cfg(not(unix))]
{
std::str::from_utf8(bytes)
.map(PathBuf::from)
.map_err(|_| unsupported(source, "non-UTF-8 path"))
}
}
fn entry_exists(path: &Path) -> Result<bool, OpenError> {
match fs::symlink_metadata(path) {
Ok(_) => Ok(true),
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(false),
Err(source) => Err(io_error(path, source)),
}
}
fn exists(path: &Path) -> Result<bool, OpenError> {
match fs::metadata(path) {
Ok(_) => Ok(true),
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(false),
Err(source) => Err(io_error(path, source)),
}
}
fn read(path: &Path) -> Result<Vec<u8>, OpenError> {
fs::read(path).map_err(|source| {
if source.kind() == io::ErrorKind::NotFound {
malformed(path, "missing required metadata")
} else {
io_error(path, source)
}
})
}
fn canonical(path: &Path) -> Result<PathBuf, OpenError> {
fs::canonicalize(path).map_err(|source| {
if source.kind() == io::ErrorKind::NotFound {
malformed(path, "metadata path does not exist")
} else {
io_error(path, source)
}
})
}
fn io_error(path: &Path, source: io::Error) -> OpenError {
OpenError::Io {
path: path.into(),
source,
}
}
fn malformed(path: &Path, reason: &str) -> OpenError {
OpenError::Malformed {
path: path.into(),
reason: reason.into(),
}
}
fn unsupported(path: &Path, feature: &str) -> OpenError {
OpenError::Unsupported {
path: path.into(),
feature: feature.into(),
}
}
fn available_path(path: &Path) -> Result<PathBuf, OpenError> {
match fs::canonicalize(path) {
Ok(path) => Ok(path),
Err(error)
if matches!(
error.kind(),
io::ErrorKind::NotFound | io::ErrorKind::PermissionDenied
) =>
{
std::path::absolute(path).map_err(|error| io_error(path, error))
}
Err(error) => Err(io_error(path, error)),
}
}
fn require_directory(path: &Path) -> Result<(), OpenError> {
match fs::metadata(path) {
Ok(metadata) if metadata.is_dir() => Ok(()),
Ok(_) => Err(malformed(path, "expected directory")),
Err(error) if error.kind() == io::ErrorKind::NotFound => {
Err(malformed(path, "missing required directory"))
}
Err(error) => Err(io_error(path, error)),
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[test]
fn exact_location_accepts_structure_without_repository_contents() {
let root = tempfile::tempdir().unwrap();
let location = RepositoryLocation::at_git_dir(root.path()).unwrap();
assert_eq!(location.git_dir(), canonical(root.path()).unwrap());
assert_eq!(location.common_dir(), location.git_dir());
assert!(matches!(
location.open_with_config(&ConfigInputs::default()),
Err(OpenError::NotFound(_))
));
}
#[rstest]
#[case::zero(b"0", Some(0))]
#[case::plus(b"+1", Some(1))]
#[case::suffix(b"1k", Some(1024))]
#[case::invalid(b"no", None)]
fn versions(#[case] bytes: &[u8], #[case] expected: Option<u32>) {
assert_eq!(parse_version(bytes), expected);
}
#[rstest]
#[case::octal(b"010", Some(8))]
#[case::hex(b"0x1", Some(1))]
#[case::negative(b"-1", Some(-1))]
#[case::invalid_octal(b"08", None)]
#[case::overflow(b"99999999999999999999", None)]
fn git_integers(#[case] bytes: &[u8], #[case] expected: Option<i64>) {
assert_eq!(integer(bytes), expected);
}
#[rstest]
#[case::false_value(b"[core]\nbare=off\n", Some(false))]
#[case::implicit(b"[core]\nbare\n", Some(true))]
#[case::absent(b"[core]\n", None)]
fn bare_values(#[case] bytes: &[u8], #[case] expected: Option<bool>) {
let config = Config::parse(bytes).unwrap();
assert_eq!(
boolean(&config, Path::new("config"), "bare").unwrap(),
expected
);
}
#[cfg(unix)]
#[test]
fn metadata_paths_preserve_bytes() {
use std::os::unix::ffi::OsStrExt;
let path = path_bytes(Path::new("config"), b"work-\xff").unwrap();
assert_eq!(path.as_os_str().as_bytes(), b"work-\xff");
}
#[test]
fn missing_path_is_not_created() {
let root = tempfile::tempdir().unwrap();
let path = root.path().join("absent");
assert!(matches!(
Repository::open(&path),
Err(OpenError::NotFound(_))
));
assert!(!path.exists());
}
}
#[cfg(doctest)]
#[doc = include_str!("../site/src/content/docs/guides/open-a-repository.md")]
mod website_repository_guide {}