use std::io::Write as _;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use super::decommission::WORKTREE_SENTINEL_FILE;
use super::worktree_ownership::{SentinelOwner, WorktreeSentinel, owner_from_payload};
pub(crate) const ADMIN_SENTINEL_NAME: &str = "trusty-mpm-worktree";
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum AdminLocation {
NotGit,
At(PathBuf),
Unresolvable(String),
}
pub(crate) fn admin_location(worktree: &Path) -> AdminLocation {
use AdminLocation::{At, NotGit, Unresolvable};
let dot_git = worktree.join(".git");
match std::fs::symlink_metadata(&dot_git) {
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return NotGit,
Err(e) => return Unresolvable(format!("cannot stat {}: {e}", dot_git.display())),
Ok(_) => {}
}
let meta = match std::fs::metadata(&dot_git) {
Ok(meta) => meta,
Err(e) => return Unresolvable(format!("cannot follow {}: {e}", dot_git.display())),
};
if meta.is_dir() {
return At(dot_git.join(ADMIN_SENTINEL_NAME));
}
if !meta.is_file() {
return Unresolvable(format!("{} is not a file or directory", dot_git.display()));
}
let text = match std::fs::read_to_string(&dot_git) {
Ok(text) => text,
Err(e) => return Unresolvable(format!("cannot read {}: {e}", dot_git.display())),
};
let raw = text
.lines()
.find_map(|l| l.strip_prefix("gitdir:"))
.map(str::trim)
.unwrap_or_default();
if raw.is_empty() {
return Unresolvable(format!("{} names no gitdir", dot_git.display()));
}
let git_dir = if Path::new(raw).is_absolute() {
PathBuf::from(raw)
} else {
worktree.join(raw)
};
if git_dir.is_dir() {
At(git_dir.join(ADMIN_SENTINEL_NAME))
} else {
Unresolvable(format!("gitdir {} does not resolve", git_dir.display()))
}
}
pub(crate) fn admin_sentinel_path(worktree: &Path) -> Option<PathBuf> {
match admin_location(worktree) {
AdminLocation::At(path) => Some(path),
AdminLocation::NotGit | AdminLocation::Unresolvable(_) => None,
}
}
pub(crate) fn legacy_sentinel_path(worktree: &Path) -> PathBuf {
worktree.join(WORKTREE_SENTINEL_FILE)
}
pub(crate) fn sentinel_present(worktree: &Path) -> bool {
legacy_sentinel_path(worktree).exists()
|| admin_sentinel_path(worktree).is_some_and(|p| p.exists())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum MarkerMigration {
NoLegacy,
NoAdminDir,
Migrated,
DuplicateRemoved,
Conflict,
Tracked,
Failed(String),
}
pub(crate) fn legacy_is_tracked(worktree: &Path) -> bool {
let mut ls_files = trusty_common::git::command_in(worktree);
ls_files.args(["ls-files", "--", WORKTREE_SENTINEL_FILE]);
match super::git_ceiling::bounded_git_output(ls_files) {
Ok(out) if out.status.success() => !out.stdout.is_empty(),
_ => true,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum OwnerReadError {
Unreadable { path: PathBuf, reason: String },
Corrupt { path: PathBuf, reason: String },
}
fn read_present(path: &Path) -> Result<Option<Vec<u8>>, OwnerReadError> {
match std::fs::read(path) {
Ok(bytes) => Ok(Some(bytes)),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(OwnerReadError::Unreadable {
path: path.to_path_buf(),
reason: e.to_string(),
}),
}
}
type Found = Result<Option<(PathBuf, Vec<u8>)>, OwnerReadError>;
pub(crate) fn read_sentinel_bytes_strict(worktree: &Path) -> Found {
read_both_with(worktree, &|| {})
}
fn read_both_with(worktree: &Path, between: &dyn Fn()) -> Found {
let legacy = legacy_sentinel_path(worktree);
let legacy_read = read_present(&legacy);
let admin = match admin_location(worktree) {
AdminLocation::NotGit => return legacy_read.map(|b| b.map(|b| (legacy, b))),
AdminLocation::At(admin) => admin,
AdminLocation::Unresolvable(reason) => {
tracing::debug!(
worktree = %worktree.display(),
"ownership marker: the git admin dir cannot be resolved ({reason}); owner unknown (#8511)"
);
return Err(OwnerReadError::Unreadable {
path: worktree.join(".git"),
reason,
});
}
};
between();
match read_present(&admin) {
Ok(Some(bytes)) => Ok(Some((admin, bytes))),
Ok(None) => legacy_read.map(|b| b.map(|b| (legacy, b))),
Err(e) => {
tracing::warn!(
worktree = %worktree.display(),
"ownership marker: the admin-dir marker cannot be read ({e:?}); owner unknown (#8511)"
);
Err(e)
}
}
}
pub(crate) fn read_sentinel_owner_strict(
worktree: &Path,
) -> Result<Option<SentinelOwner>, OwnerReadError> {
let Some((path, bytes)) = read_sentinel_bytes_strict(worktree)? else {
return Ok(None);
};
if bytes.is_empty() {
return Ok(Some(SentinelOwner::Unknown));
}
serde_json::from_slice::<WorktreeSentinel>(&bytes)
.map(|payload| Some(owner_from_payload(payload)))
.map_err(|e| OwnerReadError::Corrupt {
path,
reason: e.to_string(),
})
}
fn write_new(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
let mut file = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(path)?;
file.write_all(bytes)?;
file.sync_all()
}
pub(crate) fn migrate_legacy_sentinel(worktree: &Path) -> MarkerMigration {
migrate_with(worktree, &write_new)
}
fn migrate_with(
worktree: &Path,
copy: &dyn Fn(&Path, &[u8]) -> std::io::Result<()>,
) -> MarkerMigration {
let outcome = match decide(worktree) {
Decision::Settled(outcome) => outcome,
Decision::RemoveLegacy(legacy) => remove_duplicate(&legacy),
Decision::Copy {
legacy,
admin,
bytes,
} => migrate_bytes(&legacy, &admin, &bytes, copy),
};
match &outcome {
MarkerMigration::Failed(reason) => tracing::warn!(
worktree = %worktree.display(),
"ownership marker: legacy marker kept in the tree — {reason} (#8511)"
),
MarkerMigration::Conflict => tracing::warn!(
worktree = %worktree.display(),
"ownership marker: the admin-dir and in-tree markers differ; the admin-dir \
marker wins and both are kept (#8511)"
),
_ => {}
}
outcome
}
pub(crate) fn preview_migration(worktree: &Path) -> MarkerMigration {
match decide(worktree) {
Decision::Settled(outcome) => outcome,
Decision::RemoveLegacy(_) => MarkerMigration::DuplicateRemoved,
Decision::Copy { .. } => MarkerMigration::Migrated,
}
}
enum Decision {
Settled(MarkerMigration),
RemoveLegacy(PathBuf),
Copy {
legacy: PathBuf,
admin: PathBuf,
bytes: Vec<u8>,
},
}
fn decide(worktree: &Path) -> Decision {
use MarkerMigration::{Conflict, Failed, NoAdminDir, NoLegacy, Tracked};
let admin = match admin_location(worktree) {
AdminLocation::NotGit => return Decision::Settled(NoAdminDir),
AdminLocation::At(admin) => Ok(admin),
AdminLocation::Unresolvable(reason) => Err(reason),
};
let legacy = legacy_sentinel_path(worktree);
let bytes = match read_present(&legacy) {
Ok(Some(b)) => b,
Ok(None) => return Decision::Settled(NoLegacy),
Err(e) => {
return Decision::Settled(Failed(format!("the in-tree marker is unreadable: {e:?}")));
}
};
let admin = match admin {
Ok(admin) => admin,
Err(reason) => {
return Decision::Settled(Failed(format!(
"the git admin dir cannot be resolved: {reason}"
)));
}
};
if legacy_is_tracked(worktree) {
return Decision::Settled(Tracked);
}
match read_present(&admin) {
Ok(Some(admin_bytes)) if admin_bytes == bytes => Decision::RemoveLegacy(legacy),
Ok(Some(_)) => Decision::Settled(Conflict),
Ok(None) => Decision::Copy {
legacy,
admin,
bytes,
},
Err(e) => Decision::Settled(Failed(format!("the admin-dir marker is unreadable: {e:?}"))),
}
}
fn remove_duplicate(legacy: &Path) -> MarkerMigration {
match std::fs::remove_file(legacy) {
Ok(()) => MarkerMigration::DuplicateRemoved,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => MarkerMigration::DuplicateRemoved,
Err(e) => MarkerMigration::Failed(format!("could not remove the duplicate: {e}")),
}
}
fn temp_beside(admin: &Path) -> PathBuf {
static SEQ: AtomicU64 = AtomicU64::new(0);
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_nanos());
let name = format!(
"{ADMIN_SENTINEL_NAME}.migrate-{}-{nanos}-{}",
std::process::id(),
SEQ.fetch_add(1, Ordering::Relaxed)
);
admin.with_file_name(name)
}
fn migrate_bytes(
legacy: &Path,
admin: &Path,
bytes: &[u8],
copy: &dyn Fn(&Path, &[u8]) -> std::io::Result<()>,
) -> MarkerMigration {
let temp = temp_beside(admin);
let staged = copy(&temp, bytes)
.map_err(|e| format!("copy to {} failed: {e}", temp.display()))
.and_then(|()| match std::fs::read(&temp) {
Ok(copied) if copied == bytes => Ok(()),
Ok(_) => Err("the copy did not verify byte for byte".to_string()),
Err(e) => Err(format!("the copy could not be read back: {e}")),
});
let linked = staged.and_then(|()| match std::fs::hard_link(&temp, admin) {
Ok(()) => Ok(true),
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => Ok(false),
Err(e) => Err(format!("could not link {}: {e}", admin.display())),
});
if let Err(e) = std::fs::remove_file(&temp)
&& e.kind() != std::io::ErrorKind::NotFound
{
tracing::warn!(temp = %temp.display(), "ownership marker: temp copy not removed ({e}) (#8511)");
}
match linked {
Err(reason) => MarkerMigration::Failed(reason),
Ok(false) => match read_present(admin) {
Ok(Some(admin_bytes)) if admin_bytes == bytes => remove_duplicate(legacy),
Ok(Some(_)) => MarkerMigration::Conflict,
Ok(None) => MarkerMigration::Failed(format!(
"{} refused the link, then vanished",
admin.display()
)),
Err(e) => MarkerMigration::Failed(format!("the admin-dir marker is unreadable: {e:?}")),
},
Ok(true) => match std::fs::remove_file(legacy) {
Ok(()) => MarkerMigration::Migrated,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => MarkerMigration::Migrated,
Err(e) => {
MarkerMigration::Failed(format!("verified copy made, legacy not removed: {e}"))
}
},
}
}
pub(crate) fn write_sentinel_bytes(worktree: &Path, bytes: &[u8]) -> std::io::Result<()> {
let legacy = legacy_sentinel_path(worktree);
let admin = match admin_location(worktree) {
AdminLocation::NotGit => return std::fs::write(legacy, bytes),
AdminLocation::At(admin) => admin,
AdminLocation::Unresolvable(reason) => {
return Err(std::io::Error::other(format!(
"ownership marker not written: the git admin dir cannot be resolved ({reason})"
)));
}
};
std::fs::write(&admin, bytes)?;
if !legacy.exists() || legacy_is_tracked(worktree) {
return Ok(());
}
match std::fs::remove_file(&legacy) {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
Err(e) => tracing::warn!(
worktree = %worktree.display(),
"ownership marker: written to the admin dir, but the old in-tree marker could \
not be removed ({e}) (#8511)"
),
}
Ok(())
}
#[cfg(test)]
#[path = "worktree_ownership_location_tests.rs"]
mod worktree_ownership_location_tests;