use std::fs::{self, File, OpenOptions};
use std::io::{self, Read, Write};
use std::path::{Path, PathBuf};
use thiserror::Error;
use super::{Entry, Error, Index, Limits};
use crate::Repository;
#[derive(Debug)]
pub struct IndexEdit {
destination: PathBuf,
lock_path: PathBuf,
file: Option<File>,
original: Option<Vec<u8>>,
shared: Option<(PathBuf, Vec<u8>)>,
lock_identity: fs::Metadata,
index: Index,
limits: Limits,
published: bool,
}
#[derive(Debug, Error)]
pub enum StorageError {
#[error("{operation}; additionally, lock cleanup failed: {cleanup}")]
Cleanup {
#[source]
operation: Box<StorageError>,
cleanup: Box<StorageError>,
},
#[error("cannot {operation} {path}: {source}")]
Io {
operation: &'static str,
path: PathBuf,
#[source]
source: io::Error,
},
#[error("index lock already exists: {0}")]
Locked(PathBuf),
#[error("index changed while locked: {0}")]
Changed(PathBuf),
#[error("missing shared index: {0}")]
MissingShared(PathBuf),
#[error("index is not a regular file: {0}")]
NotRegular(PathBuf),
#[error("invalid index at {path}: {source}")]
Format {
path: PathBuf,
#[source]
source: Error,
},
}
impl Repository {
pub fn read_index(&self, limits: Limits) -> Result<Option<Index>, StorageError> {
#[cfg(feature = "tracing")]
let span = tracing::debug_span!(
target: "girt",
"index.read_index",
outcome = "incomplete",
failure_class = tracing::field::Empty,
effects = tracing::field::Empty,
);
let operation = || {
let path = self.git_dir().join("index");
read_bytes(&path, limits)?
.map(|bytes| {
parse_storage(self.object_format(), &path, &bytes, limits)
.map(|(index, _)| index)
})
.transpose()
};
#[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| crate::trace::index(error, &span));
result
}
pub fn edit_index(&self, limits: Limits) -> Result<IndexEdit, StorageError> {
#[cfg(feature = "tracing")]
let span = tracing::debug_span!(
target: "girt",
"index.edit_index",
outcome = "incomplete",
failure_class = tracing::field::Empty,
effects = tracing::field::Empty,
);
let operation = || {
let destination = self.git_dir().join("index");
let lock_path = self.git_dir().join("index.lock");
let file = OpenOptions::new()
.write(true)
.create_new(true)
.open(&lock_path)
.map_err(|source| {
if source.kind() == io::ErrorKind::AlreadyExists {
StorageError::Locked(lock_path.clone())
} else {
io_error("create lock", &lock_path, source)
}
})?;
let lock_identity = file.metadata().map_err(|source| StorageError::Cleanup {
operation: Box::new(io_error("inspect acquired lock", &lock_path, source)),
cleanup: Box::new(io_error(
"identify lock for cleanup",
&lock_path,
io::Error::other("cannot safely remove lock without its file identity"),
)),
})?;
let mut edit = IndexEdit {
destination,
lock_path,
lock_identity,
file: Some(file),
original: None,
shared: None,
index: Index::empty(self.object_format()),
limits,
published: false,
};
let result = (|| {
edit.original = read_bytes(&edit.destination, limits)?;
if let Some(bytes) = &edit.original {
(edit.index, edit.shared) =
parse_storage(self.object_format(), &edit.destination, bytes, limits)?;
}
Ok(())
})();
if let Err(operation) = result {
return Err(with_cleanup(operation, edit.abort()));
}
Ok(edit)
};
#[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| crate::trace::index(error, &span));
result
}
}
impl IndexEdit {
pub fn index(&self) -> &Index {
&self.index
}
pub fn replace_entries(&mut self, entries: Vec<Entry>) -> Result<(), Error> {
self.index.replace_entries(entries, self.limits)
}
pub fn expand_sparse(
&mut self,
objects: &crate::Objects,
limits: super::SparseLimits,
cancelled: &std::sync::atomic::AtomicBool,
) -> Result<(), super::SparseError> {
self.index
.expand_sparse(objects, self.limits, limits, cancelled)
}
pub fn replace_entries_reusing_stat(&mut self, mut entries: Vec<Entry>) -> Result<(), Error> {
let old = self.index.entries();
for entry in &mut entries {
let found = old.binary_search_by(|candidate| {
candidate
.path
.cmp(&entry.path)
.then(candidate.stage.cmp(&entry.stage))
});
if let Ok(position) = found {
let previous = &old[position];
if previous.id == entry.id
&& previous.mode == entry.mode
&& previous.assume_valid == entry.assume_valid
&& previous.intent_to_add == entry.intent_to_add
&& previous.skip_worktree == entry.skip_worktree
{
entry.stat = previous.stat;
}
}
}
self.replace_entries(entries)
}
pub fn set_version(&mut self, version: super::Version) -> Result<(), Error> {
self.index.set_version(version, self.limits)
}
pub fn invalidate_tree_cache(&mut self) -> Result<(), Error> {
self.index.invalidate_tree_cache(self.limits)
}
pub fn commit(mut self) -> Result<(), StorageError> {
#[cfg(feature = "tracing")]
let span = tracing::debug_span!(
target: "girt",
"index.commit",
outcome = "incomplete",
failure_class = tracing::field::Empty,
effects = tracing::field::Empty,
);
let operation = || {
if let Err(operation) = self.publish() {
return Err(with_cleanup(operation, self.abort()));
}
Ok(())
};
#[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| crate::trace::index(error, &span));
result
}
pub fn abort(mut self) -> Result<(), StorageError> {
#[cfg(feature = "tracing")]
let span = tracing::debug_span!(
target: "girt",
"index.abort",
outcome = "incomplete",
failure_class = tracing::field::Empty,
effects = tracing::field::Empty,
);
let operation = || {
self.published = true; self.check_lock_identity()?;
drop(self.file.take());
fs::remove_file(&self.lock_path)
.map_err(|source| io_error("remove owned index lock", &self.lock_path, source))
};
#[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| crate::trace::index(error, &span));
result
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub(crate) fn discard_tree_cache(&mut self) {
self.index.discard_tree_cache();
}
pub(crate) fn publish(&mut self) -> Result<(), StorageError> {
self.publish_with_rename(|from, to| fs::rename(from, to))
}
fn publish_with_rename(
&mut self,
rename: impl FnOnce(&Path, &Path) -> io::Result<()>,
) -> Result<(), StorageError> {
let bytes = self
.index
.encode(self.limits)
.map_err(|source| StorageError::Format {
path: self.destination.clone(),
source,
})?;
self.check_original()?;
self.write_lock(&bytes)?;
self.check_original()?;
self.check_lock_identity()?;
drop(self.file.take());
rename(&self.lock_path, &self.destination)
.map_err(|source| io_error("replace index", &self.destination, source))?;
self.published = true;
Ok(())
}
fn check_lock_identity(&self) -> Result<(), StorageError> {
let current = fs::symlink_metadata(&self.lock_path)
.map_err(|source| io_error("inspect owned index lock", &self.lock_path, source))?;
if !current.is_file() {
return Err(StorageError::Changed(self.lock_path.clone()));
}
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
let owned = &self.lock_identity;
if current.dev() != owned.dev() || current.ino() != owned.ino() {
return Err(StorageError::Changed(self.lock_path.clone()));
}
}
#[cfg(not(unix))]
let _ = &self.lock_identity;
Ok(())
}
fn check_original(&self) -> Result<(), StorageError> {
if read_bytes(&self.destination, self.limits)? != self.original {
return Err(StorageError::Changed(self.destination.clone()));
}
if let Some((path, bytes)) = &self.shared
&& read_bytes(path, self.limits)?.as_ref() != Some(bytes)
{
return Err(StorageError::Changed(path.clone()));
}
Ok(())
}
fn write_lock(&mut self, bytes: &[u8]) -> Result<(), StorageError> {
let file = self.file.as_mut().expect("unpublished guard owns its file");
file.write_all(bytes)
.map_err(|source| io_error("write lock", &self.lock_path, source))?;
file.flush()
.map_err(|source| io_error("flush lock", &self.lock_path, source))?;
file.set_modified(std::time::UNIX_EPOCH + std::time::Duration::from_secs(1))
.map_err(|source| io_error("set lock timestamp", &self.lock_path, source))?;
Ok(())
}
}
impl Drop for IndexEdit {
fn drop(&mut self) {
if !self.published && self.check_lock_identity().is_ok() {
drop(self.file.take());
let _ = fs::remove_file(&self.lock_path);
}
}
}
fn read_bytes(path: &Path, limits: Limits) -> Result<Option<Vec<u8>>, StorageError> {
let metadata = match fs::symlink_metadata(path) {
Ok(metadata) => metadata,
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None),
Err(source) => return Err(io_error("inspect index", path, source)),
};
if !metadata.is_file() {
return Err(StorageError::NotRegular(path.into()));
}
let file = File::open(path).map_err(|source| io_error("open index", path, source))?;
let mut bytes = Vec::new();
let limit = (limits.max_bytes as u64).saturating_add(1);
file.take(limit)
.read_to_end(&mut bytes)
.map_err(|source| io_error("read index", path, source))?;
if bytes.len() > limits.max_bytes {
return Err(StorageError::Format {
path: path.into(),
source: Error::Limit("bytes"),
});
}
Ok(Some(bytes))
}
type SharedFile = Option<(PathBuf, Vec<u8>)>;
fn parse_storage(
format: crate::ObjectFormat,
path: &Path,
bytes: &[u8],
limits: Limits,
) -> Result<(Index, SharedFile), StorageError> {
let contextual = |source| StorageError::Format {
path: path.into(),
source,
};
let index = Index::parse_file(format, bytes, limits).map_err(contextual)?;
let shared = if let Some(id) = index.shared_index_id() {
let shared_path = path.with_file_name(format!("sharedindex.{id}"));
let remaining = Limits {
max_bytes: limits.max_bytes.saturating_sub(bytes.len()),
..limits
};
let shared_bytes = read_bytes(&shared_path, remaining)?
.ok_or_else(|| StorageError::MissingShared(shared_path.clone()))?;
Some((shared_path, shared_bytes))
} else {
None
};
let index = index
.resolve_shared(shared.as_ref().map(|(_, bytes)| bytes.as_slice()), limits)
.map_err(contextual)?;
Ok((index, shared))
}
fn io_error(operation: &'static str, path: &Path, source: io::Error) -> StorageError {
StorageError::Io {
operation,
path: path.into(),
source,
}
}
#[cfg(test)]
mod tests;
fn with_cleanup(operation: StorageError, cleanup: Result<(), StorageError>) -> StorageError {
match cleanup {
Ok(()) => operation,
Err(cleanup) => StorageError::Cleanup {
operation: Box::new(operation),
cleanup: Box::new(cleanup),
},
}
}