use std::collections::{HashSet, VecDeque};
use std::collections::HashMap;
use crate::error::{Error, Result};
use crate::objects::{parse_commit, parse_tag, parse_tree, Object, ObjectId, ObjectKind};
use crate::refs;
use crate::repo::Repository;
fn commit_closure_from_all_refs(repo: &Repository) -> Result<HashSet<ObjectId>> {
let mut seeds = Vec::new();
for prefix in &[
"refs/heads/",
"refs/tags/",
"refs/remotes/",
"refs/bundles/",
] {
if let Ok(entries) = refs::list_refs(&repo.git_dir, prefix) {
for (_, oid) in entries {
seeds.push(oid);
}
}
}
let mut seen = HashSet::new();
let mut queue: VecDeque<ObjectId> = VecDeque::new();
for oid in seeds {
let obj = match repo.odb.read(&oid) {
Ok(o) => o,
Err(_) => continue,
};
match obj.kind {
ObjectKind::Commit => {
if seen.insert(oid) {
queue.push_back(oid);
}
}
ObjectKind::Tag => {
if let Some(target) = peel_tag_to_object(repo, &obj.data)? {
if let Ok(tobj) = repo.odb.read(&target) {
if tobj.kind == ObjectKind::Commit && seen.insert(target) {
queue.push_back(target);
}
}
}
}
_ => {}
}
}
while let Some(c) = queue.pop_front() {
let obj = repo.odb.read(&c)?;
if obj.kind != ObjectKind::Commit {
continue;
}
let commit = parse_commit(&obj.data)?;
for p in commit.parents {
if seen.insert(p) {
queue.push_back(p);
}
}
}
Ok(seen)
}
pub fn bundle_prerequisites_connected_to_refs(
repo: &Repository,
prerequisites: &[ObjectId],
) -> Result<bool> {
if prerequisites.is_empty() {
return Ok(true);
}
let closure = commit_closure_from_all_refs(repo)?;
Ok(prerequisites.iter().all(|p| closure.contains(p)))
}
fn peel_tag_to_object(repo: &Repository, tag_data: &[u8]) -> Result<Option<ObjectId>> {
let tag = parse_tag(tag_data)?;
let mut oid = tag.object;
loop {
let obj = repo.odb.read(&oid)?;
match obj.kind {
ObjectKind::Tag => {
let inner = parse_tag(&obj.data)?;
oid = inner.object;
}
_ => return Ok(Some(oid)),
}
}
}
fn tree_entry_is_tree(mode: u32) -> bool {
mode == 0o040000
}
fn tree_entry_is_blob(mode: u32) -> bool {
matches!(mode, 0o100644 | 0o100755 | 0o120000)
}
fn walk_commit_graph_for_push(
repo: &Repository,
roots: &[ObjectId],
parent_stop: Option<&HashSet<ObjectId>>,
) -> Result<bool> {
let mut seen_commits = HashSet::new();
let mut seen_trees = HashSet::new();
let mut seen_blobs = HashSet::new();
let mut commit_q: VecDeque<ObjectId> = VecDeque::new();
let mut tree_q: VecDeque<ObjectId> = VecDeque::new();
for &root in roots {
let tip_obj = match repo.odb.read(&root) {
Err(_) => return Ok(false),
Ok(o) => o,
};
if tip_obj.kind != ObjectKind::Commit {
return Err(Error::CorruptObject(format!(
"object {root} is not a commit"
)));
}
if seen_commits.insert(root) {
commit_q.push_back(root);
}
}
while let Some(c) = commit_q.pop_front() {
let obj = match repo.odb.read(&c) {
Err(_) => return Ok(false),
Ok(o) => o,
};
if obj.kind != ObjectKind::Commit {
return Err(Error::CorruptObject(format!("object {c} is not a commit")));
}
let commit = parse_commit(&obj.data)?;
for p in &commit.parents {
if parent_stop.is_some_and(|s| s.contains(p)) {
continue;
}
if seen_commits.insert(*p) {
commit_q.push_back(*p);
}
}
if seen_trees.insert(commit.tree) {
tree_q.push_back(commit.tree);
}
}
while let Some(t) = tree_q.pop_front() {
let obj = match repo.odb.read(&t) {
Err(_) => return Ok(false),
Ok(o) => o,
};
if obj.kind != ObjectKind::Tree {
return Err(Error::CorruptObject(format!("object {t} is not a tree")));
}
for entry in parse_tree(&obj.data)? {
if entry.mode == 0o160000 {
} else if tree_entry_is_tree(entry.mode) {
if seen_trees.insert(entry.oid) {
tree_q.push_back(entry.oid);
}
} else if tree_entry_is_blob(entry.mode) {
if !seen_blobs.insert(entry.oid) {
continue;
}
if repo.odb.read(&entry.oid).is_err() {
return Ok(false);
}
}
}
}
Ok(true)
}
pub fn push_tip_objects_exist(repo: &Repository, tip: ObjectId) -> Result<bool> {
walk_commit_graph_for_push(repo, &[tip], None)
}
pub fn push_tip_objects_exist_with_parent_exceptions(
repo: &Repository,
tip: ObjectId,
parent_exceptions: &HashSet<ObjectId>,
) -> Result<bool> {
walk_commit_graph_for_push(repo, &[tip], Some(parent_exceptions))
}
fn read_object_for_push(
repo: &Repository,
pack_objects: Option<&HashMap<ObjectId, Object>>,
oid: &ObjectId,
) -> Result<Object> {
if let Some(m) = pack_objects {
if let Some(o) = m.get(oid) {
return Ok(o.clone());
}
}
repo.odb.read(oid)
}
pub fn push_tip_connected_to_refs(
repo: &Repository,
tip: ObjectId,
extra_root_commits: &HashSet<ObjectId>,
pack_objects: Option<&HashMap<ObjectId, Object>>,
) -> Result<bool> {
let mut existing = commit_closure_from_all_refs(repo)?;
existing.extend(extra_root_commits.iter().copied());
if existing.contains(&tip) {
return Ok(true);
}
let mut seen_commits = HashSet::new();
let mut seen_trees = HashSet::new();
let mut seen_blobs = HashSet::new();
let mut commit_q: VecDeque<ObjectId> = VecDeque::new();
let mut tree_q: VecDeque<ObjectId> = VecDeque::new();
seen_commits.insert(tip);
commit_q.push_back(tip);
while let Some(c) = commit_q.pop_front() {
let obj = match read_object_for_push(repo, pack_objects, &c) {
Err(_) => return Ok(false),
Ok(o) => o,
};
if obj.kind != ObjectKind::Commit {
return Err(Error::CorruptObject(format!("object {c} is not a commit")));
}
let commit = parse_commit(&obj.data)?;
for p in &commit.parents {
if existing.contains(p) {
continue;
}
if seen_commits.insert(*p) {
commit_q.push_back(*p);
}
}
if seen_trees.insert(commit.tree) {
tree_q.push_back(commit.tree);
}
}
while let Some(t) = tree_q.pop_front() {
let obj = match read_object_for_push(repo, pack_objects, &t) {
Err(_) => return Ok(false),
Ok(o) => o,
};
if obj.kind != ObjectKind::Tree {
return Err(Error::CorruptObject(format!("object {t} is not a tree")));
}
for entry in parse_tree(&obj.data)? {
if entry.mode == 0o160000 {
} else if tree_entry_is_tree(entry.mode) {
if seen_trees.insert(entry.oid) {
tree_q.push_back(entry.oid);
}
} else if tree_entry_is_blob(entry.mode) {
if !seen_blobs.insert(entry.oid) {
continue;
}
if read_object_for_push(repo, pack_objects, &entry.oid).is_err() {
return Ok(false);
}
}
}
}
Ok(true)
}
pub fn diagnose_push_connectivity_failure(
repo: &Repository,
tip: ObjectId,
extra_root_commits: &HashSet<ObjectId>,
pack_objects: Option<&HashMap<ObjectId, Object>>,
) -> Result<Option<(ObjectId, ObjectId)>> {
let mut existing = commit_closure_from_all_refs(repo)?;
existing.extend(extra_root_commits.iter().copied());
if existing.contains(&tip) {
return Ok(None);
}
let mut seen_commits = HashSet::new();
let mut seen_trees = HashSet::new();
let mut seen_blobs = HashSet::new();
let mut commit_q: VecDeque<ObjectId> = VecDeque::new();
let mut tree_q: VecDeque<ObjectId> = VecDeque::new();
seen_commits.insert(tip);
commit_q.push_back(tip);
while let Some(c) = commit_q.pop_front() {
let obj = match read_object_for_push(repo, pack_objects, &c) {
Err(_) => return Ok(Some((c, tip))),
Ok(o) => o,
};
if obj.kind != ObjectKind::Commit {
return Err(Error::CorruptObject(format!("object {c} is not a commit")));
}
let commit = parse_commit(&obj.data)?;
for p in &commit.parents {
if existing.contains(p) {
continue;
}
if read_object_for_push(repo, pack_objects, p).is_err() {
return Ok(Some((*p, c)));
}
if seen_commits.insert(*p) {
commit_q.push_back(*p);
}
}
if seen_trees.insert(commit.tree) {
tree_q.push_back(commit.tree);
}
}
while let Some(t) = tree_q.pop_front() {
let obj = match read_object_for_push(repo, pack_objects, &t) {
Err(_) => return Ok(Some((t, tip))),
Ok(o) => o,
};
if obj.kind != ObjectKind::Tree {
return Err(Error::CorruptObject(format!("object {t} is not a tree")));
}
for entry in parse_tree(&obj.data)? {
if entry.mode == 0o160000 {
continue;
} else if tree_entry_is_tree(entry.mode) {
if seen_trees.insert(entry.oid) {
tree_q.push_back(entry.oid);
}
} else if tree_entry_is_blob(entry.mode) {
if !seen_blobs.insert(entry.oid) {
continue;
}
if read_object_for_push(repo, pack_objects, &entry.oid).is_err() {
return Ok(Some((entry.oid, tip)));
}
}
}
}
Ok(None)
}