use crate::parallel_budget::{BudgetRequest, HostResources, worker_budget};
use crate::parallel_runner::{Event, OrderedRun, run_ordered};
use crate::subvolume_index::{IndexOutcome, RewriteContext, StatFields, 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(|left: &LinkedWorktree, right: &LinkedWorktree| { return left.worktree.cmp(&right.worktree); });
return found;
}
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 || {});
}
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 || {});
}
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);
}
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 |path: &[u8]| { return stat_fields(tree, path); },
&mut |path: &[u8], stored: &StoredStat| {
if options.references.is_empty() {
return true;
}
return options.references.iter().any(|reference: &PathBuf| {
return stat_fields(reference.as_path(), path).is_some_and(|now: StatFields| { return matches_stored(&now, stored); });
});
},
);
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) => file.write_all(rewritten.as_slice()).and_then(|()| { return file.sync_all(); }).and_then(|()| { return std::fs::rename(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 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<()> = staged.write_all(bytes).and_then(|()| { return staged.sync_all(); }).and_then(|()| {
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);
});
if written.is_err() {
remove_if_same_file(sibling.as_path(), &staged);
}
return written;
}
pub fn install_index_in(admin: &std::fs::File, seed: &[u8], stats: &mut dyn FnMut(&[u8]) -> Option<StatFields>, hash_length: usize) -> Result<String, String> {
return install_index_in_with(admin, seed, stats, hash_length, &mut InstallSeams { before_publish: &mut || {}, after_publish: &mut || {} });
}
pub struct InstallSeams<'a> {
pub before_publish: &'a mut dyn FnMut(),
pub after_publish: &'a mut dyn FnMut(),
}
pub fn install_index_in_with(admin: &std::fs::File, seed: &[u8], stats: &mut dyn FnMut(&[u8]) -> Option<StatFields>, hash_length: usize, seams: &mut InstallSeams<'_>) -> Result<String, String> {
use crate::fs_beneath::{create_file_at, entry_metadata_at, identity_of, link_open_file_at};
let lock: &OsStr = OsStr::new("index.lock");
let index: &OsStr = OsStr::new("index");
let mut created: std::fs::File = match create_file_at(admin, lock, 0o600) {
Ok(opened) => opened,
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => return Err(String::from("index.lock exists: a Git command is running there")),
Err(error) => return Err(format!("the lock cannot be created ({error})")),
};
let ours = identity_of(&created).map_err(|error| { return format!("the lock cannot be examined ({error})"); })?;
let give_up = |reason: String| -> Result<String, String> {
let _kept = crate::fs_beneath::remove_if_identical(admin, lock, ours);
return Err(reason);
};
match entry_metadata_at(admin, index) {
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(error) => return give_up(format!("the index cannot be examined ({error})")),
Ok(_) => return give_up(String::from("an index appeared in the new worktree while it was made")),
}
let now: std::time::Duration = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default();
let context: RewriteContext = RewriteContext { hash_length: Some(hash_length), index_mtime: Some((now.as_secs() as u32, now.subsec_nanos())) };
let (rewritten, updated, total): (Vec<u8>, usize, usize) = match rewrite_index_stat_with(seed, &context, stats, &mut |_path: &[u8], _stored: &StoredStat| { return true; }) {
IndexOutcome::Rewritten { bytes, updated, total } => (bytes, updated, total),
IndexOutcome::Skipped(reason) => return give_up(format!("the index cannot be refreshed ({reason})")),
};
if let Err(error) = created.write_all(rewritten.as_slice()).and_then(|()| { return created.sync_all(); }) {
return give_up(format!("the new index cannot be written ({error})"));
}
(seams.before_publish)();
if let Err(error) = link_open_file_at(&created, admin, index) {
return give_up(format!("the new index cannot be put in place ({error})"));
}
(seams.after_publish)();
let _released = crate::fs_beneath::remove_if_identical(admin, lock, ours);
return Ok(format!("{updated} of {total} entries refreshed"));
}
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 = header.split(']').next().is_some_and(|name: &str| { return name.trim().eq_ignore_ascii_case("extensions"); });
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);
}
const INDEX_REWRITE_COPIES: u64 = 4;
struct IndexTarget {
index: PathBuf,
tree: PathBuf,
references: Vec<PathBuf>,
}
pub type RewriteOne<'a> = dyn Fn(&Path, &Path, &RefreshOptions<'_>) -> String + Sync + 'a;
pub struct AllIndexes<'a> {
pub root: &'a Path,
pub lock: IndexLock<'a>,
pub references: &'a [PathBuf],
pub host: &'a HostResources,
pub rewrite: &'a RewriteOne<'a>,
}
pub fn refresh_all_indexes(root: &Path, lock: IndexLock<'_>, references: &[PathBuf], say: &mut dyn FnMut(&str)) -> usize {
return refresh_all_indexes_with(&AllIndexes { root, lock, references, host: &HostResources::read(), rewrite: &refresh_index_file_with }, say);
}
pub fn refresh_all_indexes_with(request: &AllIndexes<'_>, say: &mut dyn FnMut(&str)) -> usize {
let AllIndexes { root, lock, references, host, rewrite } = *request;
let hash_length: Option<usize> = repository_hash_length(root.join(".git").as_path());
let mut targets: Vec<IndexTarget> = vec![IndexTarget { index: root.join(".git").join("index"), tree: root.to_path_buf(), references: references.to_vec() }];
for linked in linked_worktrees_inside(root) {
let relative: &Path = linked.worktree.strip_prefix(root).unwrap_or(Path::new(""));
targets.push(IndexTarget {
index: linked.admin_directory.join("index"),
tree: linked.worktree.clone(),
references: references.iter().map(|reference: &PathBuf| { return reference.join(relative); }).collect(),
});
}
let largest: u64 = targets.iter().map(|target: &IndexTarget| { return std::fs::metadata(&target.index).map_or(0, |found: std::fs::Metadata| { return found.len(); }); }).max().unwrap_or(0);
let workers: usize = worker_budget(&BudgetRequest { items: targets.len(), per_item_memory: largest.saturating_mul(INDEX_REWRITE_COPIES), ceiling: None, cap: None, host });
let lanes: Vec<Vec<usize>> = (0..targets.len()).map(|index: usize| { return vec![index]; }).collect();
run_ordered(OrderedRun {
items: targets.as_slice(),
lanes: lanes.as_slice(),
workers,
work: &|_index: usize, target: &IndexTarget, _worker_say: &mut dyn FnMut(&str)| {
return rewrite(target.index.as_path(), target.tree.as_path(), &RefreshOptions { lock, references: target.references.as_slice(), hash_length });
},
on_event: &mut |event: Event<'_, String>| {
if let Event::End(index, line) = event {
say(format!("index {}: {line}", targets[index].index.display()).as_str());
}
},
});
return targets.len();
}
#[cfg(test)]
#[path = "subvolume_index_files_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "subvolume_index_parallel_tests.rs"]
mod parallel_tests;