use std::cell::RefCell;
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::path::{Path, PathBuf};
struct Snapshot {
repository: git2::Repository,
tips: BTreeMap<String, git2::Oid>,
symbolic: BTreeMap<String, String>,
worktrees: Vec<(PathBuf, Option<String>)>,
objects: PathBuf,
#[cfg(unix)]
directory: PathBuf,
#[cfg(unix)]
common: PathBuf,
}
type Lending = (PathBuf, Option<PathBuf>);
thread_local! {
static OBJECT_REPOSITORIES: RefCell<HashMap<(PathBuf, Option<PathBuf>), git2::Repository>> = RefCell::new(HashMap::new());
static SNAPSHOTS: RefCell<HashMap<PathBuf,Option<Snapshot>>> = RefCell::new(HashMap::new());
static STORES: RefCell<HashMap<PathBuf, Option<git2::Repository>>> = RefCell::new(HashMap::new());
static BORROWED: RefCell<HashMap<Lending, Vec<PathBuf>>> = RefCell::new(HashMap::new());
static CONFIGURATIONS: RefCell<HashMap<PathBuf, git2::Config>> = RefCell::new(HashMap::new());
static CANONICAL: RefCell<HashMap<PathBuf, Option<PathBuf>>> = RefCell::new(HashMap::new());
}
pub(crate) fn clear() {
crate::native_objects::clear();
CONFIGURATIONS.with(|configurations| configurations.borrow_mut().clear());
SNAPSHOTS.with(|snapshots| snapshots.borrow_mut().clear());
OBJECT_REPOSITORIES.with(|repositories| repositories.borrow_mut().clear());
STORES.with(|stores| stores.borrow_mut().clear());
BORROWED.with(|borrowed| borrowed.borrow_mut().clear());
CANONICAL.with(|canonical| canonical.borrow_mut().clear());
}
pub(crate) fn canonical(path: &Path) -> Option<PathBuf> {
use std::path::Component;
if !crate::recovery_cache::enabled()
|| !path.is_absolute()
|| path
.components()
.any(|part| matches!(part, Component::CurDir | Component::ParentDir))
{
return std::fs::canonicalize(path).ok();
}
if let Some(known) = CANONICAL.with(|canonical| canonical.borrow().get(path).cloned()) {
return known;
}
let resolved = match (path.parent(), path.file_name()) {
(Some(parent), Some(name)) => canonical(parent).and_then(|parent| {
let candidate = parent.join(name);
match std::fs::symlink_metadata(&candidate) {
Ok(meta) if meta.file_type().is_symlink() => std::fs::canonicalize(&candidate).ok(),
Ok(_) => Some(candidate),
Err(_) => None,
}
}),
_ => std::fs::canonicalize(path).ok(),
};
CANONICAL.with(|canonical| {
canonical
.borrow_mut()
.insert(path.to_owned(), resolved.clone())
});
resolved
}
fn read<T>(repo: &Path, choose: impl FnOnce(&Snapshot) -> Option<T>) -> Option<T> {
if !crate::recovery_cache::enabled() {
return None;
}
SNAPSHOTS.with(|snapshots| {
let mut snapshots = snapshots.borrow_mut();
let snapshot = snapshots
.entry(repo.to_owned())
.or_insert_with(|| settled_snapshot(repo))
.as_ref()?;
choose(snapshot)
})
}
pub(crate) fn is_repo(repo: &Path) -> Option<bool> {
read(repo, |_| Some(true))
}
pub(crate) fn objects_dir(repo: &Path) -> Option<PathBuf> {
read(repo, |snapshot| Some(snapshot.objects.clone()))
}
#[cfg(unix)]
pub(crate) fn layout(repo: &Path) -> Option<(PathBuf, PathBuf)> {
read(repo, |snapshot| {
Some((snapshot.directory.clone(), snapshot.common.clone()))
})
}
#[cfg(unix)]
pub(crate) fn configuration(repo: &Path) -> Option<String> {
read(repo, |snapshot| {
let _ = snapshot;
let mut output = String::new();
frozen(repo, |configuration| {
let mut entries = configuration.entries(None).ok()?;
while let Some(entry) = entries.next() {
let entry = entry.ok()?;
let name = entry.name().ok()?;
if name.starts_with("include.") || name.starts_with("includeif.") {
return None;
}
output.push_str(&format!(
"native:{:?}\0{name}\n{}\0",
entry.level(),
entry.value().ok()?
));
}
Some(())
})?;
Some(output)
})
}
fn frozen<T>(repo: &Path, read: impl FnOnce(&git2::Config) -> Option<T>) -> Option<T> {
CONFIGURATIONS.with(|configurations| read(configurations.borrow().get(repo)?))
}
pub(crate) fn remote_url(repo: &Path, remote: &str) -> Option<String> {
read(repo, |snapshot| {
let _ = snapshot;
frozen(repo, |configuration| {
let mut entries = configuration.entries(None).ok()?;
let key = format!("remote.{remote}.url");
let mut url = None;
while let Some(entry) = entries.next() {
let entry = entry.ok()?;
let name = entry.name().ok()?;
if name.starts_with("include.")
|| name.starts_with("includeif.")
|| name.starts_with("url.")
{
return None;
}
if name == key {
if url.is_some() {
return None;
}
url = Some(entry.value().ok()?.trim().to_owned());
}
}
url
})
})
}
pub(crate) fn symbolic(repo: &Path, name: &str) -> Option<Option<String>> {
read(repo, |snapshot| Some(snapshot.symbolic.get(name).cloned()))
}
pub(crate) fn tip(repo: &Path, name: &str) -> Option<Option<String>> {
tip_with_objects(repo, name, None)
}
pub(crate) fn tip_with_objects(
repo: &Path,
name: &str,
borrowing: Option<&Path>,
) -> Option<Option<String>> {
if borrowing.is_some_and(|path| {
!path.is_absolute()
|| path
.to_str()
.is_none_or(|path| path.contains([':', '"', '\n']))
}) {
return None;
}
if crate::git::ObjectId::parse(name).is_some() || !crate::git::plainly_a_ref_name(name) {
return None;
}
if name == "@" {
return None;
}
if name != "HEAD" && !name.starts_with("refs/") {
match std::fs::symlink_metadata(repo.join(".git").join(name)) {
Err(error) if error.kind() == std::io::ErrorKind::NotFound => (),
_ => return None,
}
}
let oid = read(repo, |snapshot| {
Some(
[
name.to_owned(),
format!("refs/{name}"),
format!("refs/tags/{name}"),
format!("refs/heads/{name}"),
format!("refs/remotes/{name}"),
format!("refs/remotes/{name}/HEAD"),
]
.iter()
.find_map(|candidate| snapshot.tips.get(candidate).copied()),
)
})?;
Some(oid.and_then(|oid| {
with_objects(repo, borrowing, |repository| {
repository
.find_object(oid, None)
.ok()?
.peel_to_commit()
.ok()
.map(|commit| commit.id().to_string())
})
}))
}
pub(crate) fn raw_tip(repo: &Path, name: &str) -> Option<Option<String>> {
read(repo, |snapshot| {
Some(snapshot.tips.get(name).map(git2::Oid::to_string))
})
}
pub(crate) fn heads(repo: &Path) -> Option<Vec<(String, String)>> {
read(repo, |snapshot| {
let mut heads = Vec::new();
for (reference, tip) in &snapshot.tips {
let Some(branch) = reference.strip_prefix("refs/heads/") else {
continue;
};
if branch == "HEAD"
|| [
format!("refs/{branch}"),
format!("refs/tags/{branch}"),
format!("refs/remotes/{branch}"),
format!("refs/remotes/{branch}/HEAD"),
]
.iter()
.any(|name| snapshot.tips.contains_key(name))
{
return None;
}
heads.push((branch.to_owned(), tip.to_string()));
}
Some(heads)
})
}
pub(crate) fn remote_heads(repo: &Path, remote: &str) -> Option<BTreeMap<String, String>> {
let prefix = format!("refs/remotes/{remote}/");
read(repo, |snapshot| {
Some(
snapshot
.tips
.iter()
.filter_map(|(name, tip)| {
let name = name.strip_prefix(&prefix)?;
(name != "HEAD").then(|| (name.to_owned(), tip.to_string()))
})
.collect(),
)
})
}
pub(crate) fn worktrees(repo: &Path) -> Option<Vec<(PathBuf, Option<String>)>> {
read(repo, |snapshot| Some(snapshot.worktrees.clone()))
}
pub(crate) fn remote_tips(repo: &Path, remote: &str) -> Option<BTreeSet<String>> {
let prefix = format!("refs/remotes/{remote}/");
read(repo, |snapshot| {
Some(
snapshot
.tips
.iter()
.filter(|(name, _)| name.starts_with(&prefix))
.map(|(_, tip)| tip.to_string())
.collect(),
)
})
}
pub(crate) fn with_objects<T>(
at: &Path,
borrowing: Option<&Path>,
choose: impl FnOnce(&git2::Repository) -> Option<T>,
) -> Option<T> {
let borrowing = beyond_alternates(at, borrowing);
let mut choose = Some(choose);
if borrowing.is_none() {
if let Some(answer) = read(at, |snapshot| Some(choose.take()?(&snapshot.repository))) {
return answer;
}
}
let choose = choose?;
OBJECT_REPOSITORIES.with(|repositories| {
let mut repositories = repositories.borrow_mut();
let key = (at.to_owned(), borrowing.map(Path::to_owned));
if !repositories.contains_key(&key) {
let repository = git2::Repository::open(at).ok()?;
if let Some(path) = borrowing {
repository
.odb()
.ok()?
.add_disk_alternate(path.to_str()?)
.ok()?;
}
repositories.insert(key.clone(), repository);
}
choose(repositories.get(&key)?)
})
}
pub(crate) fn with_objects_holding<T>(
at: &Path,
borrowing: Option<&Path>,
named: &[git2::Oid],
choose: impl Fn(&git2::Repository) -> Option<T>,
) -> Option<T> {
for store in borrowed(at, borrowing) {
let answer = with_store(&store, |repository| {
let odb = repository.odb().ok()?;
named
.iter()
.all(|id| odb.exists_ext(*id, git2::OdbLookupFlags::NO_REFRESH))
.then(|| choose(repository))
.flatten()
});
if answer.is_some() {
return answer;
}
}
with_objects(at, borrowing, choose)
}
pub(crate) fn with_store<T>(
store: &Path,
choose: impl FnOnce(&git2::Repository) -> Option<T>,
) -> Option<T> {
STORES.with(|stores| {
let mut stores = stores.borrow_mut();
let repository = stores
.entry(store.to_owned())
.or_insert_with(|| {
let odb = git2::Odb::new().ok()?;
odb.add_disk_alternate(store.to_str()?).ok()?;
git2::Repository::from_odb(odb).ok()
})
.as_ref()?;
choose(repository)
})
}
pub(crate) fn beyond_alternates<'a>(at: &Path, borrowing: Option<&'a Path>) -> Option<&'a Path> {
borrowing.filter(|lent| {
let lent_canonical = canonical(lent);
!borrowed(at, None).iter().any(|alternate| {
alternate == lent
|| (lent_canonical.is_some() && canonical(alternate) == lent_canonical)
})
})
}
pub(crate) fn borrowed(at: &Path, borrowing: Option<&Path>) -> Vec<PathBuf> {
let key = (at.to_owned(), borrowing.map(Path::to_owned));
if let Some(known) = BORROWED.with(|borrowed| borrowed.borrow().get(&key).cloned()) {
return known;
}
let mut stores: Vec<PathBuf> = objects_dir(at)
.and_then(|objects| std::fs::read_to_string(objects.join("info/alternates")).ok())
.map(|raw| {
raw.lines()
.map(PathBuf::from)
.filter(|path| path.is_absolute())
.collect()
})
.unwrap_or_default();
if let Some(lent) = borrowing {
if !stores.iter().any(|store| store == lent) {
stores.push(lent.to_owned());
}
}
BORROWED.with(|borrowed| borrowed.borrow_mut().insert(key, stores.clone()));
stores
}
fn with_object_repository<T>(
at: &Path,
env: &[(String, String)],
named: &[git2::Oid],
choose: impl Fn(&git2::Repository) -> Option<T>,
) -> Option<T> {
read(at, |_| Some(()))?;
with_objects_holding(at, lent(env)?, named, choose)
}
fn lent(env: &[(String, String)]) -> Option<Option<&Path>> {
match env {
[] => Some(None),
[(name, path)]
if name == "GIT_ALTERNATE_OBJECT_DIRECTORIES"
&& Path::new(path).is_absolute()
&& !path.contains([':', '"', '\n']) =>
{
Some(Some(Path::new(path)))
}
_ => None,
}
}
pub(crate) fn has_commit(at: &Path, env: &[(String, String)], name: &str) -> Option<bool> {
crate::git::ObjectId::parse(name)?;
let named = [git2::Oid::from_str(name).ok()?];
with_object_repository(at, env, &named, |repository| {
let oid = git2::Oid::from_str(name).ok()?;
let present = repository
.find_object(oid, None)
.and_then(|object| object.peel_to_commit())
.is_ok();
Some(present)
})
}
pub(crate) fn is_ancestor(
at: &Path,
env: &[(String, String)],
ancestor: &str,
descendant: &str,
) -> Option<bool> {
crate::git::ObjectId::parse(ancestor)?;
crate::git::ObjectId::parse(descendant)?;
read(at, |_| Some(()))?;
let named = [
git2::Oid::from_str(ancestor).ok()?,
git2::Oid::from_str(descendant).ok()?,
];
if let Some(answer) = lent(env)
.filter(|borrowing| crate::recovery_cache::readable_in_process(at, *borrowing))
.and_then(|borrowing| crate::native_objects::is_ancestor(at, borrowing, named[0], named[1]))
{
return Some(answer);
}
with_object_repository(at, env, &named, |repository| {
let ancestor = repository
.find_object(git2::Oid::from_str(ancestor).ok()?, None)
.ok()?
.peel_to_commit()
.ok()?
.id();
let descendant = repository
.find_object(git2::Oid::from_str(descendant).ok()?, None)
.ok()?
.peel_to_commit()
.ok()?
.id();
if ancestor == descendant {
Some(true)
} else {
repository.graph_descendant_of(descendant, ancestor).ok()
}
})
}
fn settled_snapshot(at: &Path) -> Option<Snapshot> {
let mut wait = std::time::Duration::from_millis(2);
for _ in 0..5 {
if let Some(snapshot) = snapshot(at) {
return Some(snapshot);
}
if !refs_moving(at) {
return None;
}
std::thread::sleep(wait);
wait *= 2;
}
None
}
fn refs_moving(at: &Path) -> bool {
fn common(at: &Path) -> Option<PathBuf> {
let dot = at.join(".git");
if dot.is_dir() {
return Some(dot);
}
let raw = std::fs::read_to_string(&dot).ok()?;
let gitdir = at.join(raw.strip_prefix("gitdir: ")?.trim_end());
let shared = std::fs::read_to_string(gitdir.join("commondir")).ok()?;
Some(gitdir.join(shared.trim_end()))
}
fn recent(meta: &std::fs::Metadata) -> bool {
meta.modified()
.ok()
.and_then(|at| at.elapsed().ok())
.is_some_and(|age| age < std::time::Duration::from_secs(2))
}
fn moving(path: &Path) -> bool {
let Ok(entries) = std::fs::read_dir(path) else {
return false;
};
if std::fs::metadata(path).is_ok_and(|meta| recent(&meta)) {
return true;
}
entries.flatten().any(|entry| {
entry.file_name().to_string_lossy().ends_with(".lock")
|| (entry.file_type().is_ok_and(|kind| kind.is_dir()) && moving(&entry.path()))
})
}
let Some(common) = common(at) else {
return false;
};
common.join("packed-refs.lock").exists()
|| std::fs::metadata(common.join("packed-refs")).is_ok_and(|meta| recent(&meta))
|| moving(&common.join("refs"))
}
fn snapshot(at: &Path) -> Option<Snapshot> {
if !Path::new(crate::git::git_program()).is_absolute()
|| crate::recovery_cache::has_git_overrides()
{
return None;
}
let repo = git2::Repository::open(at).ok()?;
if repo.is_bare() || canonical(repo.workdir()?)? != canonical(at)? {
return None;
}
let directory = canonical(repo.path())?;
let common = canonical(repo.commondir())?;
if common.file_name()?.to_str() != Some(".git") {
return None;
}
if at.join(".git").is_file() {
let raw = std::fs::read_to_string(at.join(".git")).ok()?;
let target = raw.strip_prefix("gitdir: ")?.strip_suffix('\n')?;
let target = Path::new(target);
let target = if target.is_absolute() {
target.to_owned()
} else {
at.join(target)
};
if canonical(&target)? != directory {
return None;
}
} else if canonical(&at.join(".git"))? != directory {
return None;
}
if ["reftable", "refs/replace", "info/grafts", "shallow"]
.iter()
.any(|path| common.join(path).exists())
{
return None;
}
let mut names = BTreeSet::new();
canonical_loose(&common.join("refs"), &common, &mut names)?;
if common != directory {
canonical_loose(&directory.join("refs"), &directory, &mut names)?;
}
canonical_value(&std::fs::read(directory.join("HEAD")).ok()?)?;
if common.join("packed-refs").exists() {
let packed = std::fs::read_to_string(common.join("packed-refs")).ok()?;
let mut previous = false;
let mut packed_names = BTreeSet::new();
for line in packed.lines() {
if line.starts_with('#') {
continue;
}
if let Some(peeled) = line.strip_prefix('^') {
if !previous || !canonical_oid(peeled) {
return None;
}
previous = false;
} else {
let (oid, name) = line.split_once(' ')?;
if !canonical_oid(oid) || !crate::git::plainly_a_ref_name(name) {
return None;
}
if !packed_names.insert(name.to_owned()) {
return None;
}
names.insert(name.to_owned());
previous = true;
}
}
}
if names.iter().any(|name| name.starts_with("refs/replace/")) {
return None;
}
let configuration = repo.config().ok()?.snapshot().ok()?;
for key in [
"core.worktree",
"core.warnAmbiguousRefs",
"extensions.refStorage",
"extensions.objectFormat",
] {
match configuration.get_entry(key) {
Err(error) if error.code() == git2::ErrorCode::NotFound => (),
_ => return None,
}
}
let mut tips = BTreeMap::new();
let mut symbolic = BTreeMap::new();
for reference in repo.references().ok()? {
let reference = reference.ok()?;
let name = reference.name().ok()?.to_owned();
if !crate::git::plainly_a_ref_name(&name) {
return None;
}
if let Some(target) = reference.symbolic_target().ok()? {
symbolic.insert(name.clone(), target.to_owned());
}
let oid = match reference.target() {
Some(oid) => oid,
None => reference.resolve().ok()?.target()?,
};
tips.insert(name, oid);
}
if tips.keys().cloned().collect::<BTreeSet<_>>() != names {
return None;
}
let head = repo.find_reference("HEAD").ok()?;
if let Some(target) = head.symbolic_target().ok()? {
symbolic.insert("HEAD".into(), target.to_owned());
}
if let Ok(resolved) = head.resolve() {
let oid = resolved.target()?;
tips.insert("HEAD".into(), oid);
}
drop(head);
let primary = common.parent()?.to_owned();
let primary_head = std::fs::read(common.join("HEAD")).ok()?;
canonical_value(&primary_head)?;
let branch = std::str::from_utf8(&primary_head)
.ok()?
.strip_suffix('\n')?
.strip_prefix("ref: refs/heads/")
.map(str::to_owned);
let mut worktrees = vec![(primary, branch)];
for name in repo.worktrees().ok()?.iter() {
let name = name.ok()??;
let worktree = repo.find_worktree(name).ok()?;
let path = worktree.path();
path.to_str()?;
let gitdir = common.join("worktrees").join(name);
canonical_value(&std::fs::read(gitdir.join("HEAD")).ok()?)?;
let linked = git2::Repository::open(path).ok()?;
let head = linked.find_reference("HEAD").ok()?;
let branch = head
.symbolic_target()
.ok()?
.and_then(|target| target.strip_prefix("refs/heads/"))
.map(str::to_owned);
worktrees.push((path.to_owned(), branch));
}
worktrees[1..].sort_by(|a, b| a.0.cmp(&b.0));
CONFIGURATIONS.with(|configurations| {
configurations
.borrow_mut()
.insert(at.to_owned(), configuration)
});
Some(Snapshot {
repository: repo,
tips,
symbolic,
worktrees,
objects: common.join("objects"),
#[cfg(unix)]
directory,
#[cfg(unix)]
common,
})
}
fn canonical_oid(value: &str) -> bool {
value.len() == 40
&& value
.bytes()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
}
fn canonical_value(raw: &[u8]) -> Option<()> {
let value = std::str::from_utf8(raw).ok()?;
let value = value.strip_suffix('\n')?;
if canonical_oid(value)
|| value
.strip_prefix("ref: ")
.is_some_and(crate::git::plainly_a_ref_name)
{
Some(())
} else {
None
}
}
fn canonical_loose(path: &Path, directory: &Path, names: &mut BTreeSet<String>) -> Option<()> {
if !path.exists() {
return Some(());
}
for entry in std::fs::read_dir(path).ok()? {
let entry = entry.ok()?;
let metadata = entry.file_type().ok()?;
if metadata.is_dir() {
canonical_loose(&entry.path(), directory, names)?;
} else if metadata.is_file() {
let path = entry.path();
let name = path.strip_prefix(directory).ok()?.to_str()?;
if !crate::git::plainly_a_ref_name(name) {
return None;
}
names.insert(name.to_owned());
canonical_value(&std::fs::read(path).ok()?)?;
} else {
return None;
}
}
Some(())
}
#[cfg(all(test, unix))]
mod tests {
use super::*;
fn isolate(test: &str) -> bool {
let variables = std::env::vars_os()
.filter(|(name, _)| name.to_string_lossy().starts_with("GIT_"))
.collect::<Vec<_>>();
if variables.is_empty() {
return false;
}
let mut command = std::process::Command::new(std::env::current_exe().unwrap());
command.args([
"--exact",
&format!("native_refs::tests::{test}"),
"--nocapture",
]);
for (name, _) in variables {
command.env_remove(name);
}
assert!(
command.status().unwrap().success(),
"isolated real Git differential journey"
);
true
}
#[test]
fn noninteractive_editor_and_pager_preserve_reads_and_other_overrides_refuse() {
const CHILD: &str = "ONEVCS_READER_ENV_CHILD";
if std::env::var_os(CHILD).is_none() {
let mut child = std::process::Command::new(std::env::current_exe().unwrap());
child.args([
"--exact",
"native_refs::tests::noninteractive_editor_and_pager_preserve_reads_and_other_overrides_refuse",
"--nocapture",
]);
for (name, _) in
std::env::vars_os().filter(|(name, _)| name.to_string_lossy().starts_with("GIT_"))
{
child.env_remove(name);
}
child
.env(CHILD, "1")
.env("GIT_EDITOR", "/missing/onevcs-editor")
.env("GIT_PAGER", "/missing/onevcs-pager");
assert!(
child.status().unwrap().success(),
"isolated real Git environment journey"
);
return;
}
let home = tempfile::tempdir().unwrap();
std::env::set_var("HOME", home.path());
std::env::set_var("XDG_CONFIG_HOME", home.path());
let root = fixture("sha1");
let repo = root.path();
let tip = git(repo, &["rev-parse", "HEAD"]);
differential(repo, &["main"]);
crate::recovery_cache::scope(|| {
assert_eq!(
is_repo(repo),
Some(true),
"inert editor and pager retain the native read"
);
assert!(
crate::recovery_cache::query(
&["log", "-1", "--format=%B", &tip, "--"],
Some(repo),
&[]
)
.is_some(),
"immutable proof reuse stays available"
);
});
std::env::set_var("GIT_CONFIG_COUNT", "1");
crate::recovery_cache::scope(|| {
assert_eq!(is_repo(repo), None);
assert!(crate::recovery_cache::query(
&["log", "-1", "--format=%B", &tip, "--"],
Some(repo),
&[]
)
.is_none());
assert!(
crate::git::heads(repo).is_err(),
"Git's malformed configuration refusal survives fallback"
);
});
std::env::remove_var("GIT_CONFIG_COUNT");
differential(repo, &["main"]);
crate::recovery_cache::scope(|| assert_eq!(is_repo(repo), Some(true)));
}
fn git(repo: &Path, args: &[&str]) -> String {
let output = std::process::Command::new(crate::git::git_program())
.current_dir(repo)
.args(args)
.output()
.expect("real Git");
assert!(
output.status.success(),
"{args:?}: {}",
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8(output.stdout)
.expect("Git output")
.trim()
.to_owned()
}
#[test]
fn borrowed_object_contexts_do_not_leak_and_clear_observes_removal() {
if isolate("borrowed_object_contexts_do_not_leak_and_clear_observes_removal") {
return;
}
let source = fixture("sha1");
let destination = fixture("sha1");
let oid = git(source.path(), &["hash-object", "-w", "--stdin"]);
let store = source.path().join(".git/objects");
let read = |borrowed: Option<&Path>| {
with_objects(destination.path(), borrowed, |repo| {
let odb = repo.odb().ok()?;
Some(odb.exists(git2::Oid::from_str(&oid).ok()?))
})
};
clear();
assert_eq!(read(Some(&store)), Some(true));
assert_eq!(read(None), Some(false));
let raw = std::process::Command::new(crate::git::git_program())
.current_dir(destination.path())
.args(["cat-file", "-e", &oid])
.env("GIT_ALTERNATE_OBJECT_DIRECTORIES", &store)
.output()
.unwrap();
assert!(raw.status.success());
std::fs::remove_file(store.join(&oid[..2]).join(&oid[2..])).unwrap();
clear();
assert_eq!(read(Some(&store)), Some(false));
let raw = std::process::Command::new(crate::git::git_program())
.current_dir(destination.path())
.args(["cat-file", "-e", &oid])
.env("GIT_ALTERNATE_OBJECT_DIRECTORIES", &store)
.output()
.unwrap();
assert!(!raw.status.success());
}
fn fixture(format: &str) -> tempfile::TempDir {
let root = tempfile::tempdir().unwrap();
git(
root.path(),
&["init", "-b", "main", &format!("--object-format={format}")],
);
git(
root.path(),
&[
"-c",
"user.name=Reader",
"-c",
"user.email=reader@example.invalid",
"commit",
"--allow-empty",
"-m",
"base",
],
);
root
}
fn differential(repo: &Path, names: &[&str]) {
let baseline = crate::git::heads(repo).map_err(|error| error.to_string());
let tips = names
.iter()
.map(|name| crate::git::tip(repo, name))
.collect::<Vec<_>>();
let locals = names
.iter()
.map(|name| crate::git::local_tip(repo, name))
.collect::<Vec<_>>();
let unpublished =
crate::git::unpublished_branches_among(repo, |_| true, &BTreeSet::new(), |_| None)
.map_err(|error| error.to_string());
let present = tips.iter().flatten().cloned().collect::<Vec<_>>();
let ancestry = present
.iter()
.map(|tip| {
(
tip.clone(),
crate::git::is_ancestor(repo, &present[0], tip).map_err(|e| e.to_string()),
)
})
.collect::<Vec<_>>();
crate::recovery_cache::scope(|| {
assert_eq!(
crate::git::heads(repo).map_err(|error| error.to_string()),
baseline
);
assert_eq!(
crate::git::unpublished_branches_among(repo, |_| true, &BTreeSet::new(), |_| None)
.map_err(|error| error.to_string()),
unpublished
);
for (tip, expected) in &ancestry {
assert!(crate::git::has_commit(repo, &crate::host::Sha(tip.clone())));
assert_eq!(
crate::git::is_ancestor(repo, &present[0], tip).map_err(|e| e.to_string()),
*expected
);
}
for ((name, expected), local) in names.iter().zip(tips).zip(locals) {
assert_eq!(
crate::git::local_tip(repo, name),
local,
"raw local object for {name}"
);
assert_eq!(
crate::git::tip(repo, name),
expected,
"complete value for {name}"
);
}
});
}
#[test]
fn linked_worktrees_read_their_head_and_shared_refs_like_git() {
if isolate("linked_worktrees_read_their_head_and_shared_refs_like_git") {
return;
}
let root = fixture("sha1");
let repo = root.path();
let linked = repo.join("linked");
git(
repo,
&[
"worktree",
"add",
"-b",
"topic/a.b/版本",
linked.to_str().unwrap(),
],
);
let names = ["HEAD", "main", "topic/a.b/版本"];
differential(&linked, &names);
let baseline = crate::git::worktrees(&linked).unwrap();
let objects = crate::git::objects_dir(&linked).unwrap();
crate::recovery_cache::scope(|| {
assert!(
heads(&linked).is_some(),
"ordinary linked layouts retain the native path"
);
assert_eq!(crate::git::worktrees(&linked).unwrap(), baseline);
assert_eq!(
std::fs::canonicalize(crate::git::objects_dir(&linked).unwrap()).unwrap(),
std::fs::canonicalize(&objects).unwrap()
);
});
git(repo, &["pack-refs", "--all"]);
differential(&linked, &names);
std::fs::write(
repo.join(".git/shallow"),
format!("{}\n", git(repo, &["rev-parse", "main"])),
)
.unwrap();
differential(&linked, &names);
std::fs::remove_file(repo.join(".git/shallow")).unwrap();
differential(&linked, &names);
}
#[test]
fn complete_ref_values_and_names_match_git_including_fallbacks() {
if isolate("complete_ref_values_and_names_match_git_including_fallbacks") {
return;
}
let root = fixture("sha1");
let repo = root.path();
let tip = git(repo, &["rev-parse", "HEAD"]);
let names = [
"main",
"topic/版本",
"topic/punct!#$%&()+;=",
"topic/a.b/c.d",
"topic/non\u{a0}breaking",
];
for name in &names[1..] {
git(repo, &["branch", name]);
}
differential(repo, &names);
crate::recovery_cache::scope(|| {
assert!(
heads(repo).is_some(),
"valid unrelated names retain the fast path"
)
});
git(repo, &["pack-refs", "--all"]);
git(
repo,
&[
"-c",
"user.name=Reader",
"-c",
"user.email=reader@example.invalid",
"commit",
"--allow-empty",
"-m",
"new loose tip",
],
);
differential(repo, &names);
crate::recovery_cache::scope(|| {
assert!(
heads(repo).is_some(),
"loose refs override packed ones in process"
)
});
let raw = repo.join(".git/refs/heads/raw");
for value in [
format!("{tip}\n"),
format!("{tip} \n"),
format!("\t{tip}\n"),
format!("{tip}\u{a0}\n"),
format!("{}\n", tip.to_uppercase()),
format!("{}\n", "a".repeat(64)),
"ref: refs/heads/main\n".into(),
"ref: refs/heads/absent\n".into(),
"not-an-object\n".into(),
] {
std::fs::write(&raw, value).unwrap();
differential(repo, &["main", "raw"]);
}
std::fs::remove_file(&raw).unwrap();
differential(repo, &names);
crate::recovery_cache::scope(|| assert!(heads(repo).is_some(), "repair is observed"));
git(
repo,
&[
"-c",
"user.name=Reader",
"-c",
"user.email=reader@example.invalid",
"tag",
"-a",
"annotated",
"-m",
"tag object",
],
);
crate::recovery_cache::scope(|| {
assert!(
heads(repo).is_some(),
"unrelated annotated tags retain the fast path"
)
});
let tag = git(repo, &["rev-parse", "refs/tags/annotated"]);
std::fs::write(&raw, format!("{tag}\n")).unwrap();
differential(repo, &["raw", "annotated", "HEAD", "@"]);
let blob = git(repo, &["hash-object", "-w", "--stdin"]);
std::fs::write(&raw, format!("{blob}\n")).unwrap();
differential(repo, &["raw"]);
std::fs::remove_file(&raw).unwrap();
let old = git(repo, &["rev-parse", "HEAD~1"]);
std::fs::write(repo.join(".git/MERGE_HEAD"), format!("{old}\n")).unwrap();
differential(repo, &["MERGE_HEAD", "@", "HEAD"]);
std::fs::remove_file(repo.join(".git/MERGE_HEAD")).unwrap();
differential(repo, &["MERGE_HEAD"]);
git(repo, &["tag", "main"]);
differential(repo, &["main", "HEAD"]);
}
#[test]
fn sha256_repository_delegates_without_truncating_values() {
if isolate("sha256_repository_delegates_without_truncating_values") {
return;
}
let root = fixture("sha256");
assert_eq!(git(root.path(), &["rev-parse", "HEAD"]).len(), 64);
differential(root.path(), &["main", "HEAD"]);
crate::recovery_cache::scope(|| assert!(heads(root.path()).is_none()));
}
}