use std::{
fs,
path::{Path, PathBuf},
};
use objects::fs_atomic::write_file_atomic;
use sley::{
GitObjectType, ObjectFormat, ObjectId, Repository as SleyRepository,
plumbing::sley_object::EncodedObject,
};
use crate::git_core::{GitProjectionError, GitProjectionResult, git_err};
pub const RESIDUALS_DIR_NAME: &str = "git-residuals";
const RESIDUAL_MAGIC: &[u8; 4] = b"HR01";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResidualObject {
pub oid: ObjectId,
pub object_format: ObjectFormat,
pub object_type: GitObjectType,
pub body: Vec<u8>,
}
#[derive(Debug, Clone)]
pub struct ResidualStore {
heddle_dir: PathBuf,
}
impl ResidualStore {
pub fn open(heddle_dir: impl AsRef<Path>) -> Self {
Self {
heddle_dir: heddle_dir.as_ref().to_path_buf(),
}
}
pub fn residuals_dir(&self) -> PathBuf {
self.heddle_dir.join(RESIDUALS_DIR_NAME)
}
pub fn put_residual(
&self,
object_format: ObjectFormat,
object_type: GitObjectType,
body: impl Into<Vec<u8>>,
) -> GitProjectionResult<ObjectId> {
let body = body.into();
let encoded = EncodedObject::new(object_type, body.clone());
let oid = encoded
.object_id(object_format)
.map_err(|error| GitProjectionError::Git(error.to_string()))?;
self.put_residual_verified(oid, object_format, object_type, body)?;
Ok(oid)
}
pub fn put_residual_verified(
&self,
oid: ObjectId,
object_format: ObjectFormat,
object_type: GitObjectType,
body: impl Into<Vec<u8>>,
) -> GitProjectionResult<()> {
if oid.format() != object_format {
return Err(GitProjectionError::Git(format!(
"Raw Git Object Residual format mismatch: oid is {}, store tag is {}",
oid.format().name(),
object_format.name()
)));
}
let body = body.into();
let encoded = EncodedObject::new(object_type, body.clone());
let computed = encoded
.object_id(object_format)
.map_err(|error| GitProjectionError::Git(error.to_string()))?;
if computed != oid {
return Err(GitProjectionError::Git(format!(
"Raw Git Object Residual oid mismatch: body hashes to {computed}, expected {oid}"
)));
}
let path = self.residual_path(object_format, &oid);
if path.is_file() {
if let Ok(existing) = fs::read(&path)
&& existing == encode_residual_file(object_type, &body)
{
return Ok(());
}
}
write_file_atomic(&path, &encode_residual_file(object_type, &body))?;
Ok(())
}
pub fn get_residual(
&self,
object_format: ObjectFormat,
oid: &ObjectId,
) -> GitProjectionResult<Option<ResidualObject>> {
let path = self.residual_path(object_format, oid);
let bytes = match fs::read(&path) {
Ok(bytes) => bytes,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(error) => return Err(error.into()),
};
let (object_type, body) = decode_residual_file(&bytes)?;
let encoded = EncodedObject::new(object_type, body.clone());
let computed = encoded
.object_id(object_format)
.map_err(|error| GitProjectionError::Git(error.to_string()))?;
if &computed != oid {
return Err(GitProjectionError::Git(format!(
"corrupt Raw Git Object Residual at {}: body hashes to {computed}, path claims {oid}",
path.display()
)));
}
Ok(Some(ResidualObject {
oid: *oid,
object_format,
object_type,
body,
}))
}
pub fn has_residual(
&self,
object_format: ObjectFormat,
oid: &ObjectId,
) -> GitProjectionResult<bool> {
Ok(self.residual_path(object_format, oid).is_file())
}
pub fn list_residual_oids(
&self,
object_format: ObjectFormat,
) -> GitProjectionResult<Vec<ObjectId>> {
let format_dir = self.residuals_dir().join(object_format.name());
if !format_dir.is_dir() {
return Ok(Vec::new());
}
let mut oids = Vec::new();
for prefix_entry in fs::read_dir(&format_dir)? {
let prefix_entry = prefix_entry?;
let prefix_path = prefix_entry.path();
if !prefix_path.is_dir() {
continue;
}
let prefix = match prefix_entry.file_name().into_string() {
Ok(name) if name.len() == 2 => name,
_ => continue,
};
for object_entry in fs::read_dir(&prefix_path)? {
let object_entry = object_entry?;
if !object_entry.path().is_file() {
continue;
}
let Ok(rest) = object_entry.file_name().into_string() else {
continue;
};
let hex = format!("{prefix}{rest}");
match ObjectId::from_hex(object_format, &hex) {
Ok(oid) => oids.push(oid),
Err(_) => continue,
}
}
}
Ok(oids)
}
pub fn migrate_object_from_git_repo(
&self,
source: &SleyRepository,
oid: &ObjectId,
) -> GitProjectionResult<bool> {
let format = source.object_format();
if self.has_residual(format, oid)? {
return Ok(true);
}
let object = match source.read_object(oid) {
Ok(object) => object,
Err(_) => return Ok(false),
};
self.put_residual_verified(*oid, format, object.object_type, object.body.clone())?;
Ok(true)
}
pub fn install_into(
&self,
target: &SleyRepository,
oid: &ObjectId,
) -> GitProjectionResult<bool> {
let format = target.object_format();
let Some(residual) = self.get_residual(format, oid)? else {
return Ok(false);
};
let written = target
.write_object(EncodedObject::new(residual.object_type, residual.body))
.map_err(git_err)?;
if written != *oid {
return Err(GitProjectionError::Git(format!(
"installing Raw Git Object Residual wrote {written}, expected {oid}"
)));
}
Ok(true)
}
fn residual_path(&self, object_format: ObjectFormat, oid: &ObjectId) -> PathBuf {
let hex = oid.to_string();
let (prefix, rest) = hex.split_at(2);
self.residuals_dir()
.join(object_format.name())
.join(prefix)
.join(rest)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BridgeMirrorRetirementStatus {
pub mirror_path: PathBuf,
pub mirror_exists: bool,
pub residuals_dir: PathBuf,
pub residual_count: usize,
pub mirror_already_absent: bool,
pub note: String,
}
pub fn bridge_mirror_retirement_status(
heddle_dir: impl AsRef<Path>,
) -> GitProjectionResult<BridgeMirrorRetirementStatus> {
let heddle_dir = heddle_dir.as_ref();
let mirror_path = heddle_dir.join("git");
let store = ResidualStore::open(heddle_dir);
let residual_count = store.list_residual_oids(ObjectFormat::Sha1)?.len()
+ store.list_residual_oids(ObjectFormat::Sha256)?.len();
let mirror_exists = mirror_path.exists();
let note = if !mirror_exists {
"Bridge Mirror absent; no residual migration cleanup required for .heddle/git.".to_string()
} else if residual_count == 0 {
"Bridge Mirror still present and no Raw Git Object Residuals stored yet; \
migrate lossy objects into residuals before considering mirror deletion."
.to_string()
} else {
format!(
"Bridge Mirror still present with {residual_count} residual object(s) stored. \
After all mapped non-reconstructable oids have residuals and no live path \
requires the mirror, delete .heddle/git via explicit maintenance cleanup."
)
};
Ok(BridgeMirrorRetirementStatus {
mirror_path: mirror_path.clone(),
mirror_exists,
residuals_dir: store.residuals_dir(),
residual_count,
mirror_already_absent: !mirror_exists,
note,
})
}
pub fn resolve_lossy_object(
residual_store: &ResidualStore,
mirror_repo: Option<&SleyRepository>,
target_format: ObjectFormat,
oid: &ObjectId,
migrate_from_mirror: bool,
) -> GitProjectionResult<ResidualObject> {
if let Some(residual) = residual_store.get_residual(target_format, oid)? {
return Ok(residual);
}
if let Some(mirror) = mirror_repo
&& mirror.object_format() == target_format
&& let Ok(object) = mirror.read_object(oid)
{
if migrate_from_mirror {
residual_store.put_residual_verified(
*oid,
target_format,
object.object_type,
object.body.clone(),
)?;
}
return Ok(ResidualObject {
oid: *oid,
object_format: target_format,
object_type: object.object_type,
body: object.body.clone(),
});
}
Err(GitProjectionError::Git(format!(
"mapped Git object {oid} is not reconstructable from Heddle state, has no Raw Git Object Residual, \
and is unavailable from the Bridge Mirror; hard fidelity failure (import with residual capture \
or restore residual bytes before export/write-through)"
)))
}
fn encode_residual_file(object_type: GitObjectType, body: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(4 + 1 + body.len());
out.extend_from_slice(RESIDUAL_MAGIC);
out.push(object_type_tag(object_type));
out.extend_from_slice(body);
out
}
fn decode_residual_file(bytes: &[u8]) -> GitProjectionResult<(GitObjectType, Vec<u8>)> {
if bytes.len() < 5 || &bytes[..4] != RESIDUAL_MAGIC {
return Err(GitProjectionError::Git(
"invalid Raw Git Object Residual file: bad magic or truncated header".into(),
));
}
let object_type = object_type_from_tag(bytes[4])?;
Ok((object_type, bytes[5..].to_vec()))
}
fn object_type_tag(object_type: GitObjectType) -> u8 {
match object_type {
GitObjectType::Commit => 1,
GitObjectType::Tree => 2,
GitObjectType::Blob => 3,
GitObjectType::Tag => 4,
}
}
fn object_type_from_tag(tag: u8) -> GitProjectionResult<GitObjectType> {
match tag {
1 => Ok(GitObjectType::Commit),
2 => Ok(GitObjectType::Tree),
3 => Ok(GitObjectType::Blob),
4 => Ok(GitObjectType::Tag),
other => Err(GitProjectionError::Git(format!(
"invalid Raw Git Object Residual type tag {other}"
))),
}
}
#[cfg(test)]
mod tests {
use sley::Repository as SleyRepository;
use super::*;
#[test]
fn put_get_round_trip_and_hash_identity() {
let dir = tempfile::tempdir().unwrap();
let heddle = dir.path().join(".heddle");
fs::create_dir_all(&heddle).unwrap();
let store = ResidualStore::open(&heddle);
let body = b"tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n\
author A <a@e> 1 +0000\n\
committer A <a@e> 1 +0000\n\
\nmsg\n";
let oid = store
.put_residual(ObjectFormat::Sha1, GitObjectType::Commit, body.to_vec())
.unwrap();
assert!(store.has_residual(ObjectFormat::Sha1, &oid).unwrap());
let got = store
.get_residual(ObjectFormat::Sha1, &oid)
.unwrap()
.expect("residual present");
assert_eq!(got.oid, oid);
assert_eq!(got.object_format, ObjectFormat::Sha1);
assert_eq!(got.object_type, GitObjectType::Commit);
assert_eq!(got.body, body);
let oid2 = store
.put_residual(ObjectFormat::Sha1, GitObjectType::Commit, body.to_vec())
.unwrap();
assert_eq!(oid, oid2);
}
#[test]
fn put_verified_rejects_oid_mismatch() {
let dir = tempfile::tempdir().unwrap();
let heddle = dir.path().join(".heddle");
fs::create_dir_all(&heddle).unwrap();
let store = ResidualStore::open(&heddle);
let wrong = ObjectId::from_hex(
ObjectFormat::Sha1,
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
)
.unwrap();
let err = store
.put_residual_verified(
wrong,
ObjectFormat::Sha1,
GitObjectType::Blob,
b"hello".to_vec(),
)
.unwrap_err();
assert!(
err.to_string().contains("oid mismatch"),
"unexpected error: {err}"
);
}
#[test]
fn list_residual_oids_finds_stored_objects() {
let dir = tempfile::tempdir().unwrap();
let heddle = dir.path().join(".heddle");
fs::create_dir_all(&heddle).unwrap();
let store = ResidualStore::open(&heddle);
let oid = store
.put_residual(ObjectFormat::Sha1, GitObjectType::Blob, b"x")
.unwrap();
let listed = store.list_residual_oids(ObjectFormat::Sha1).unwrap();
assert_eq!(listed, vec![oid]);
}
#[test]
fn migrate_from_git_repo_copies_object() {
let dir = tempfile::tempdir().unwrap();
let mirror_path = dir.path().join("mirror");
let heddle = dir.path().join(".heddle");
fs::create_dir_all(&heddle).unwrap();
let mirror = SleyRepository::init_bare(&mirror_path).unwrap();
let oid = mirror.write_blob(b"migrated-bytes\n").unwrap();
let store = ResidualStore::open(&heddle);
assert!(!store.has_residual(ObjectFormat::Sha1, &oid).unwrap());
assert!(store.migrate_object_from_git_repo(&mirror, &oid).unwrap());
assert!(store.has_residual(ObjectFormat::Sha1, &oid).unwrap());
let residual = store
.get_residual(ObjectFormat::Sha1, &oid)
.unwrap()
.unwrap();
assert_eq!(residual.body, b"migrated-bytes\n");
}
#[test]
fn install_into_target_repo() {
let dir = tempfile::tempdir().unwrap();
let heddle = dir.path().join(".heddle");
let target_path = dir.path().join("target");
fs::create_dir_all(&heddle).unwrap();
let store = ResidualStore::open(&heddle);
let oid = store
.put_residual(ObjectFormat::Sha1, GitObjectType::Blob, b"install-me")
.unwrap();
let target = SleyRepository::init_bare(&target_path).unwrap();
assert!(target.read_object(&oid).is_err());
assert!(store.install_into(&target, &oid).unwrap());
let object = target.read_object(&oid).unwrap();
assert_eq!(object.body, b"install-me");
}
#[test]
fn resolve_lossy_prefers_residual_over_mirror() {
let dir = tempfile::tempdir().unwrap();
let heddle = dir.path().join(".heddle");
let mirror_path = dir.path().join("mirror");
fs::create_dir_all(&heddle).unwrap();
let store = ResidualStore::open(&heddle);
let residual_oid = store
.put_residual(ObjectFormat::Sha1, GitObjectType::Blob, b"from-residual")
.unwrap();
let mirror = SleyRepository::init_bare(&mirror_path).unwrap();
let _ = mirror.write_blob(b"from-mirror").unwrap();
let resolved = resolve_lossy_object(
&store,
Some(&mirror),
ObjectFormat::Sha1,
&residual_oid,
false,
)
.unwrap();
assert_eq!(resolved.body, b"from-residual");
}
#[test]
fn resolve_lossy_hard_fails_when_unavailable() {
let dir = tempfile::tempdir().unwrap();
let heddle = dir.path().join(".heddle");
fs::create_dir_all(&heddle).unwrap();
let store = ResidualStore::open(&heddle);
let missing = ObjectId::from_hex(
ObjectFormat::Sha1,
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
)
.unwrap();
let err =
resolve_lossy_object(&store, None, ObjectFormat::Sha1, &missing, false).unwrap_err();
assert!(
err.to_string().contains("hard fidelity failure"),
"unexpected: {err}"
);
}
#[test]
fn bridge_mirror_retirement_status_reports_absence() {
let dir = tempfile::tempdir().unwrap();
let heddle = dir.path().join(".heddle");
fs::create_dir_all(&heddle).unwrap();
let status = bridge_mirror_retirement_status(&heddle).unwrap();
assert!(status.mirror_already_absent);
assert!(!status.mirror_exists);
assert_eq!(status.residual_count, 0);
}
}