use std::path::Path;
use crate::error::{GitError, Result};
use crate::hash::GitHash;
#[must_use]
pub fn list_refs(git_dir: &Path) -> Vec<(String, GitHash)> {
list_refs_checked(git_dir).unwrap_or_default()
}
pub fn list_refs_checked(git_dir: &Path) -> Result<Vec<(String, GitHash)>> {
let mut out: Vec<(String, GitHash)> = Vec::new();
let refs_root = git_dir.join("refs");
collect_loose_refs_checked(&refs_root, "refs", git_dir, &mut out)?;
match std::fs::read_to_string(git_dir.join("packed-refs")) {
Ok(text) => {
for line in text.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') || line.starts_with('^') {
continue;
}
if let Some((sha, name)) = line.split_once(' ') {
if out.iter().any(|(n, _)| n == name) {
continue; }
if let Ok(hash) = GitHash::from_hex(sha.trim()) {
out.push((name.trim().to_string(), hash));
}
}
}
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
Err(e) => return Err(GitError::Io(e)),
}
match resolve_ref(git_dir, "HEAD") {
Ok(hash) => {
if !out.iter().any(|(n, _)| n == "HEAD") {
out.push(("HEAD".to_string(), hash));
}
}
Err(GitError::RefNotFound(_)) => {}
Err(e) => return Err(e),
}
Ok(out)
}
fn collect_loose_refs_checked(
dir: &Path,
prefix: &str,
git_dir: &Path,
out: &mut Vec<(String, GitHash)>,
) -> Result<()> {
let entries = match std::fs::read_dir(dir) {
Ok(e) => e,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
Err(e) => return Err(GitError::Io(e)),
};
for entry in entries {
let entry = entry.map_err(GitError::Io)?;
let name = entry.file_name();
let Some(name) = name.to_str() else {
continue;
};
let refname = format!("{prefix}/{name}");
let path = entry.path();
if path.is_dir() {
collect_loose_refs_checked(&path, &refname, git_dir, out)?;
} else {
match resolve_ref(git_dir, &refname) {
Ok(hash) => out.push((refname, hash)),
Err(GitError::RefNotFound(_)) => {} Err(e) => return Err(e), }
}
}
Ok(())
}
pub fn resolve_ref(git_dir: &Path, refname: &str) -> Result<GitHash> {
if refname.len() == 40 && refname.chars().all(|c| c.is_ascii_hexdigit()) {
return GitHash::from_hex(refname);
}
let ref_path = git_dir.join(refname);
let content = std::fs::read_to_string(&ref_path).map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
GitError::RefNotFound(refname.to_string())
} else {
GitError::Io(e)
}
})?;
let content = content.trim();
if let Some(target) = content.strip_prefix("ref: ") {
return resolve_ref(git_dir, target);
}
GitHash::from_hex(content)
.map_err(|_| GitError::RefNotFound(format!("{refname}: invalid hash {content:?}")))
}
#[cfg(test)]
mod refs_bootstrap_tests {
use super::*;
#[test]
fn list_refs_checked_errs_on_unreadable_refs() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(tmp.path().join("refs"), b"corrupt").unwrap();
assert!(
list_refs_checked(tmp.path()).is_err(),
"an unreadable refs/ must surface as an error, not empty refs"
);
}
#[test]
fn list_refs_checked_ok_when_refs_genuinely_absent() {
let tmp = tempfile::tempdir().unwrap();
assert_eq!(list_refs_checked(tmp.path()).unwrap(), Vec::new());
}
}