use std::fs;
use std::path::Path;
use sley_core::{GitError, ObjectFormat, ObjectId, Result};
use sley_protocol::ProtocolV2FetchShallowInfo;
pub fn read_shallow(git_dir: &Path, format: ObjectFormat) -> Result<Vec<ObjectId>> {
let path = git_dir.join("shallow");
let contents = match fs::read_to_string(&path) {
Ok(contents) => contents,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(err) => return Err(err.into()),
};
let mut oids = Vec::new();
for line in contents.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
oids.push(ObjectId::from_hex(format, line).map_err(|err| {
GitError::InvalidFormat(format!("invalid oid in {}: {err}", path.display()))
})?);
}
Ok(oids)
}
pub fn write_shallow(git_dir: &Path, oids: &[ObjectId]) -> Result<()> {
let path = git_dir.join("shallow");
let mut hexes = oids.iter().map(ObjectId::to_hex).collect::<Vec<_>>();
hexes.sort();
hexes.dedup();
if hexes.is_empty() {
match fs::remove_file(&path) {
Ok(()) => {}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
Err(err) => return Err(err.into()),
}
return Ok(());
}
let mut contents = String::with_capacity(hexes.iter().map(|hex| hex.len() + 1).sum());
for hex in &hexes {
contents.push_str(hex);
contents.push('\n');
}
fs::write(&path, contents)?;
Ok(())
}
pub fn apply_shallow_info(
git_dir: &Path,
format: ObjectFormat,
entries: &[ProtocolV2FetchShallowInfo],
) -> Result<()> {
if entries.is_empty() {
return Ok(());
}
let mut oids = read_shallow(git_dir, format)?;
for entry in entries {
match entry {
ProtocolV2FetchShallowInfo::Shallow(oid) => {
if !oids.contains(oid) {
oids.push(*oid);
}
}
ProtocolV2FetchShallowInfo::Unshallow(oid) => oids.retain(|existing| existing != oid),
}
}
write_shallow(git_dir, &oids)
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicU64, Ordering};
static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
fn temp_dir() -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!(
"sley-remote-shallow-{}-{}",
std::process::id(),
TEMP_COUNTER.fetch_add(1, Ordering::Relaxed)
));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).expect("create temp dir");
dir
}
fn oid(hex_byte: &str) -> ObjectId {
ObjectId::from_hex(ObjectFormat::Sha1, &hex_byte.repeat(40)).expect("valid oid")
}
#[test]
fn read_missing_shallow_is_empty() {
let dir = temp_dir();
assert!(
read_shallow(&dir, ObjectFormat::Sha1)
.expect("test operation should succeed")
.is_empty()
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn write_sorts_dedups_and_round_trips() {
let dir = temp_dir();
let a = oid("1");
let b = oid("2");
write_shallow(&dir, &[b.clone(), a.clone(), b.clone()])
.expect("test operation should succeed");
let contents =
fs::read_to_string(dir.join("shallow")).expect("test operation should succeed");
assert_eq!(contents, format!("{}\n{}\n", a.to_hex(), b.to_hex()));
assert_eq!(
read_shallow(&dir, ObjectFormat::Sha1).expect("test operation should succeed"),
vec![a, b]
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn write_empty_removes_file() {
let dir = temp_dir();
write_shallow(&dir, &[oid("3")]).expect("test operation should succeed");
assert!(dir.join("shallow").exists());
write_shallow(&dir, &[]).expect("test operation should succeed");
assert!(!dir.join("shallow").exists());
write_shallow(&dir, &[]).expect("test operation should succeed");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn apply_adds_shallow_and_drops_unshallow() {
let dir = temp_dir();
let keep = oid("a");
let added = oid("b");
let removed = oid("c");
write_shallow(&dir, &[keep.clone(), removed.clone()])
.expect("test operation should succeed");
apply_shallow_info(
&dir,
ObjectFormat::Sha1,
&[
ProtocolV2FetchShallowInfo::Shallow(added.clone()),
ProtocolV2FetchShallowInfo::Unshallow(removed),
],
)
.expect("test operation should succeed");
assert_eq!(
read_shallow(&dir, ObjectFormat::Sha1).expect("test operation should succeed"),
vec![keep, added]
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn apply_empty_entries_is_noop() {
let dir = temp_dir();
let existing = oid("d");
write_shallow(&dir, std::slice::from_ref(&existing))
.expect("test operation should succeed");
apply_shallow_info(&dir, ObjectFormat::Sha1, &[]).expect("test operation should succeed");
assert_eq!(
read_shallow(&dir, ObjectFormat::Sha1).expect("test operation should succeed"),
vec![existing]
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn apply_unshallowing_last_boundary_removes_file() {
let dir = temp_dir();
let boundary = oid("e");
write_shallow(&dir, std::slice::from_ref(&boundary))
.expect("test operation should succeed");
apply_shallow_info(
&dir,
ObjectFormat::Sha1,
&[ProtocolV2FetchShallowInfo::Unshallow(boundary)],
)
.expect("test operation should succeed");
assert!(!dir.join("shallow").exists());
let _ = fs::remove_dir_all(&dir);
}
}