pub use crate::subvolume_index_install_into_new_worktree::{InstallSeams, install_index_in, install_index_in_with};
use crate::subvolume_index::{IndexOutcome, RewriteContext, StatFields, StatLookup, StatTrust, StoredStat, rewrite_index_stat_with};
use crate::subvolume_lock::is_ours;
use std::ffi::OsStr;
use std::io::Write;
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::MetadataExt;
use std::path::{Path, PathBuf};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LinkedWorktree {
pub admin_directory: PathBuf,
pub worktree: PathBuf,
}
pub fn linked_worktrees_inside(root: &Path) -> Vec<LinkedWorktree> {
let admin: PathBuf = root.join(".git").join("worktrees");
let Ok(names) = std::fs::read_dir(&admin) else { return Vec::new() };
let mut found: Vec<LinkedWorktree> = Vec::new();
for name in names.flatten() {
let Ok(recorded) = std::fs::read(name.path().join("gitdir")) else { continue };
let Some(worktree) = Path::new(OsStr::from_bytes(recorded.trim_ascii())).parent() else { continue };
if worktree.starts_with(root) && worktree != root && worktree.is_dir() {
found.push(LinkedWorktree { admin_directory: name.path(), worktree: worktree.to_path_buf() });
}
}
found.sort_by(by_worktree_path);
return found;
}
fn by_worktree_path(left: &LinkedWorktree, right: &LinkedWorktree) -> std::cmp::Ordering {
return left.worktree.cmp(&right.worktree);
}
fn do_nothing() {}
pub(crate) fn stat_fields(tree: &Path, path: &[u8]) -> Option<StatFields> {
let metadata: std::fs::Metadata = std::fs::symlink_metadata(tree.join(OsStr::from_bytes(path))).ok()?;
return Some(fields_of(&metadata));
}
pub(crate) fn fields_of(metadata: &std::fs::Metadata) -> StatFields {
return StatFields {
ctime_seconds: metadata.ctime() as u32,
ctime_nanos: metadata.ctime_nsec() as u32,
mtime_seconds: metadata.mtime() as u32,
mtime_nanos: metadata.mtime_nsec() as u32,
dev: metadata.dev() as u32,
ino: metadata.ino() as u32,
uid: metadata.uid(),
gid: metadata.gid(),
size: metadata.size() as u32,
};
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum IndexLock<'a> {
Acquire,
Held(&'a str),
}
#[derive(Clone, Copy, Debug)]
pub struct RefreshOptions<'a> {
pub lock: IndexLock<'a>,
pub references: &'a [PathBuf],
pub hash_length: Option<usize>,
}
pub(crate) fn matches_stored(now: &StatFields, stored: &StoredStat) -> bool {
return now.size == stored.size
&& now.ino == stored.ino
&& now.uid == stored.uid
&& now.gid == stored.gid
&& now.ctime_seconds == stored.ctime_seconds
&& (stored.ctime_nanos == 0 || now.ctime_nanos == stored.ctime_nanos)
&& now.mtime_seconds == stored.mtime_seconds
&& (stored.mtime_nanos == 0 || now.mtime_nanos == stored.mtime_nanos);
}
pub fn refresh_index_file_with(index_path: &Path, tree: &Path, options: &RefreshOptions<'_>) -> String {
return refresh_index_file_seamed(index_path, tree, options, &mut do_nothing);
}
pub fn refresh_index_file_seamed(index_path: &Path, tree: &Path, options: &RefreshOptions<'_>, after_rewrite: &mut dyn FnMut()) -> String {
return refresh_index_file_report_seamed(index_path, tree, options, after_rewrite).line;
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RefreshStatus {
Refreshed,
NoIndex,
Skipped,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RefreshReport {
pub line: String,
pub status: RefreshStatus,
}
pub fn refresh_index_file_report(index_path: &Path, tree: &Path, options: &RefreshOptions<'_>) -> RefreshReport {
return refresh_index_file_report_seamed(index_path, tree, options, &mut do_nothing);
}
pub fn refresh_index_file_report_seamed(index_path: &Path, tree: &Path, options: &RefreshOptions<'_>, after_rewrite: &mut dyn FnMut()) -> RefreshReport {
let mut lock_name: std::ffi::OsString = index_path.as_os_str().to_os_string();
lock_name.push(".lock");
let lock_path: PathBuf = PathBuf::from(lock_name);
let mut created: Option<std::fs::File> = None;
match options.lock {
IndexLock::Acquire => match std::fs::OpenOptions::new().write(true).create_new(true).open(&lock_path) {
Ok(opened) => created = Some(opened),
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
return RefreshReport { line: String::from("skipped (index.lock exists: a Git command is running there; its first status will be slow)"), status: RefreshStatus::Skipped };
}
Err(error) => return RefreshReport { line: format!("skipped (cannot create the lock: {error})"), status: RefreshStatus::Skipped },
},
IndexLock::Held(token) => {
if !is_ours(lock_path.as_path(), token) {
return RefreshReport { line: String::from("skipped (index.lock is held by another Git command; its first status will be slow)"), status: RefreshStatus::Skipped };
}
}
}
let (outcome, status): (String, RefreshStatus) = rewrite_under_lock(index_path, tree, options, created.as_mut(), &lock_path);
after_rewrite();
if let (Some(file), true) = (created.as_ref(), status != RefreshStatus::Refreshed) {
remove_if_same_file(lock_path.as_path(), file);
}
return RefreshReport { line: outcome, status };
}
fn remove_if_same_file(path: &Path, held: &std::fs::File) {
let same: bool = match (std::fs::symlink_metadata(path), held.metadata()) {
(Ok(named), Ok(own)) => named.dev() == own.dev() && named.ino() == own.ino() && named.file_type().is_file(),
_ => false,
};
if same {
let _removed = std::fs::remove_file(path);
}
}
fn rewrite_under_lock(index_path: &Path, tree: &Path, options: &RefreshOptions<'_>, lock_file: Option<&mut std::fs::File>, lock_path: &Path) -> (String, RefreshStatus) {
let index_mtime: Option<(u32, u32)> = match std::fs::metadata(index_path) {
Ok(metadata) => Some((metadata.mtime() as u32, metadata.mtime_nsec() as u32)),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return (String::from("no index file"), RefreshStatus::NoIndex),
Err(error) => return (format!("skipped (cannot examine it: {error})"), RefreshStatus::Skipped),
};
let bytes: Vec<u8> = match std::fs::read(index_path) {
Ok(found) => found,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return (String::from("no index file"), RefreshStatus::NoIndex),
Err(error) => return (format!("skipped (cannot read it: {error})"), RefreshStatus::Skipped),
};
return rewrite_and_install(bytes.as_slice(), index_mtime, [index_path, tree, lock_path], options, lock_file);
}
struct StatOfTree<'a> {
tree: &'a Path,
}
impl StatLookup for StatOfTree<'_> {
fn stat_now(&mut self, path: &[u8]) -> Option<StatFields> {
return stat_fields(self.tree, path);
}
}
struct AgreeWithReferences<'a> {
references: &'a [PathBuf],
}
impl StatTrust for AgreeWithReferences<'_> {
fn is_trusted(&mut self, path: &[u8], stored: &StoredStat) -> bool {
return agrees_with_a_reference(self.references, path, stored);
}
}
fn rewrite_and_install(bytes: &[u8], index_mtime: Option<(u32, u32)>, paths: [&Path; 3], options: &RefreshOptions<'_>, lock_file: Option<&mut std::fs::File>) -> (String, RefreshStatus) {
let [index_path, tree, lock_path]: [&Path; 3] = paths;
let context: RewriteContext = RewriteContext { hash_length: options.hash_length, index_mtime };
let outcome: IndexOutcome = rewrite_index_stat_with(bytes, &context, &mut StatOfTree { tree }, &mut AgreeWithReferences { references: options.references });
let (rewritten, updated, total): (Vec<u8>, usize, usize) = match outcome {
IndexOutcome::Rewritten { bytes: new_bytes, updated, total } => (new_bytes, updated, total),
IndexOutcome::Skipped(reason) => return (format!("skipped ({reason})"), RefreshStatus::Skipped),
};
let installed: std::io::Result<()> = match lock_file {
Some(file) => write_sync_rename(file, rewritten.as_slice(), lock_path, index_path),
None => stage_and_replace(index_path, rewritten.as_slice()),
};
if let Err(error) = installed {
return (format!("skipped (cannot install the new index: {error})"), RefreshStatus::Skipped);
}
return (format!("{updated} of {total} entries refreshed"), RefreshStatus::Refreshed);
}
fn agrees_with_a_reference(references: &[PathBuf], path: &[u8], stored: &StoredStat) -> bool {
if references.is_empty() {
return true;
}
for reference in references {
if let Some(now) = stat_fields(reference.as_path(), path)
&& matches_stored(&now, stored)
{
return true;
}
}
return false;
}
fn write_sync_rename(file: &mut std::fs::File, bytes: &[u8], from: &Path, to: &Path) -> std::io::Result<()> {
file.write_all(bytes)?;
file.sync_all()?;
return std::fs::rename(from, to);
}
fn stage_and_replace(index_path: &Path, bytes: &[u8]) -> std::io::Result<()> {
let mut sibling_name: std::ffi::OsString = index_path.as_os_str().to_os_string();
sibling_name.push(".cli-git-new");
let sibling: PathBuf = PathBuf::from(sibling_name);
let mut staged: std::fs::File = std::fs::OpenOptions::new().write(true).create_new(true).open(&sibling)?;
let written: std::io::Result<()> = write_sync_and_rename_if_same(&mut staged, bytes, sibling.as_path(), index_path);
if written.is_err() {
remove_if_same_file(sibling.as_path(), &staged);
}
return written;
}
fn write_sync_and_rename_if_same(staged: &mut std::fs::File, bytes: &[u8], sibling: &Path, index_path: &Path) -> std::io::Result<()> {
staged.write_all(bytes)?;
staged.sync_all()?;
let named = std::fs::symlink_metadata(sibling)?;
let own = staged.metadata()?;
if named.dev() != own.dev() || named.ino() != own.ino() {
return Err(std::io::Error::other("the staging file was replaced while the index was written"));
}
return std::fs::rename(sibling, index_path);
}
pub fn refresh_index_file_locked(index_path: &Path, tree: &Path, lock: IndexLock<'_>) -> String {
return refresh_index_file_with(index_path, tree, &RefreshOptions { lock, references: &[], hash_length: None });
}
pub fn refresh_index_file(index_path: &Path, tree: &Path) -> String {
return refresh_index_file_locked(index_path, tree, IndexLock::Acquire);
}
pub fn repository_hash_length(git_directory: &Path) -> Option<usize> {
let text: String = String::from_utf8_lossy(std::fs::read(git_directory.join("config")).ok()?.as_slice()).into_owned();
let mut in_extensions: bool = false;
for raw in text.lines() {
let line: &str = raw.trim();
if let Some(header) = line.strip_prefix('[') {
in_extensions = match header.split(']').next() {
Some(name) => name.trim().eq_ignore_ascii_case("extensions"),
None => false,
};
continue;
}
if !in_extensions {
continue;
}
let Some((key, value)) = line.split_once('=') else { continue };
if key.trim().eq_ignore_ascii_case("objectformat") && value.trim().eq_ignore_ascii_case("sha256") {
return Some(32);
}
}
return Some(20);
}
#[cfg(test)]
#[path = "subvolume_index_files_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "subvolume_index_parallel_tests.rs"]
mod parallel_tests;