use std::collections::{HashSet, VecDeque};
use std::path::Path;
use std::time::SystemTime;
use crate::error::{Error, Result};
use crate::objects::{parse_commit, parse_tag, parse_tree, ObjectId, ObjectKind};
use crate::odb::Odb;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct PruneStats {
pub pruned: usize,
pub kept: usize,
}
pub fn prune_loose_unreachable(
odb: &Odb,
reachable_roots: &[ObjectId],
keep_newer_than: Option<SystemTime>,
) -> Result<PruneStats> {
let reachable = reachable_closure(odb, reachable_roots)?;
let mut stats = PruneStats::default();
for (oid, path) in enumerate_loose_objects(odb)? {
if reachable.contains(&oid) {
stats.kept += 1;
continue;
}
if let Some(cutoff) = keep_newer_than {
let too_new = std::fs::metadata(&path)
.and_then(|m| m.modified())
.map(|mtime| mtime >= cutoff)
.unwrap_or(false);
if too_new {
stats.kept += 1;
continue;
}
}
match std::fs::remove_file(&path) {
Ok(()) => stats.pruned += 1,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => stats.pruned += 1,
Err(e) => return Err(Error::Io(e)),
}
}
Ok(stats)
}
fn reachable_closure(odb: &Odb, roots: &[ObjectId]) -> Result<HashSet<ObjectId>> {
let mut seen: HashSet<ObjectId> = HashSet::new();
let mut queue: VecDeque<ObjectId> = VecDeque::new();
for &root in roots {
if seen.insert(root) {
queue.push_back(root);
}
}
while let Some(oid) = queue.pop_front() {
let obj = odb.read(&oid)?;
match obj.kind {
ObjectKind::Commit => {
let commit = parse_commit(&obj.data)?;
for parent in commit.parents {
if seen.insert(parent) {
queue.push_back(parent);
}
}
if seen.insert(commit.tree) {
queue.push_back(commit.tree);
}
}
ObjectKind::Tree => {
for entry in parse_tree(&obj.data)? {
if entry.mode == 0o160000 {
continue;
}
if seen.insert(entry.oid) {
queue.push_back(entry.oid);
}
}
}
ObjectKind::Tag => {
let tag = parse_tag(&obj.data)?;
if seen.insert(tag.object) {
queue.push_back(tag.object);
}
}
ObjectKind::Blob => {}
}
}
Ok(seen)
}
fn enumerate_loose_objects(odb: &Odb) -> Result<Vec<(ObjectId, std::path::PathBuf)>> {
let objects_dir = odb.objects_dir();
let mut out = Vec::new();
let top = match std::fs::read_dir(objects_dir) {
Ok(rd) => rd,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(out),
Err(e) => return Err(Error::Io(e)),
};
for top_entry in top {
let top_entry = top_entry.map_err(Error::Io)?;
let name = top_entry.file_name();
let Some(prefix) = name.to_str() else {
continue;
};
if prefix.len() != 2 || !prefix.bytes().all(|b| b.is_ascii_hexdigit()) {
continue;
}
if !top_entry.file_type().map_err(Error::Io)?.is_dir() {
continue;
}
let sub = match std::fs::read_dir(top_entry.path()) {
Ok(rd) => rd,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue,
Err(e) => return Err(Error::Io(e)),
};
for sub_entry in sub {
let sub_entry = sub_entry.map_err(Error::Io)?;
let suffix_name = sub_entry.file_name();
let Some(suffix) = suffix_name.to_str() else {
continue;
};
if !ObjectId::is_loose_suffix_len(suffix.len())
|| !suffix.bytes().all(|b| b.is_ascii_hexdigit())
{
continue;
}
let hex = format!("{prefix}{suffix}");
let Ok(oid) = ObjectId::from_hex(&hex) else {
continue;
};
out.push((oid, sub_entry.path()));
}
}
Ok(out)
}
pub fn remote_default_branch_local(remote_git_dir: &Path) -> Result<Option<String>> {
let remote_odb =
Odb::new(&remote_git_dir.join("objects")).with_config_git_dir(remote_git_dir.to_path_buf());
let entries = crate::ls_remote::ls_remote(
remote_git_dir,
&remote_odb,
&crate::ls_remote::Options {
symref: true,
..Default::default()
},
)?;
for entry in &entries {
if entry.name == "HEAD" {
return Ok(entry
.symref_target
.as_ref()
.map(|t| t.strip_prefix("refs/heads/").unwrap_or(t).to_owned()));
}
}
Ok(None)
}
#[derive(Clone, Debug)]
pub struct RefTransactionItem {
pub name: String,
pub new_oid: Option<ObjectId>,
pub expected_old: Option<ObjectId>,
}
pub fn update_refs(git_dir: &Path, updates: &[RefTransactionItem]) -> Result<()> {
for item in updates {
if let Some(expected) = item.expected_old {
let current = crate::refs::resolve_ref(git_dir, &item.name).ok();
if current != Some(expected) {
return Err(Error::Message(format!(
"ref transaction rejected: '{}' expected {} but found {}",
item.name,
expected,
current
.map(|o| o.to_hex())
.unwrap_or_else(|| "<absent>".to_owned()),
)));
}
}
}
for item in updates {
match &item.new_oid {
Some(oid) => crate::refs::write_ref(git_dir, &item.name, oid)?,
None => crate::refs::delete_ref(git_dir, &item.name)?,
}
}
Ok(())
}