use std::fs;
use std::io;
use std::path::Path;
use crate::error::{Error, Result};
use crate::objects::ObjectId;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Ref {
Direct(ObjectId),
Symbolic(String),
}
pub fn read_ref_file(path: &Path) -> Result<Ref> {
let content = fs::read_to_string(path).map_err(Error::Io)?;
let content = content.trim_end_matches('\n');
parse_ref_content(content)
}
pub(crate) fn parse_ref_content(content: &str) -> Result<Ref> {
if let Some(target) = content.strip_prefix("ref: ") {
Ok(Ref::Symbolic(target.trim().to_owned()))
} else if content.len() == 40 && content.chars().all(|c| c.is_ascii_hexdigit()) {
let oid: ObjectId = content.parse()?;
Ok(Ref::Direct(oid))
} else {
Err(Error::InvalidRef(content.to_owned()))
}
}
pub fn resolve_ref(git_dir: &Path, refname: &str) -> Result<ObjectId> {
resolve_ref_depth(git_dir, refname, 0)
}
fn resolve_ref_depth(git_dir: &Path, refname: &str, depth: usize) -> Result<ObjectId> {
if depth > 10 {
return Err(Error::InvalidRef(format!(
"ref symlink too deep: {refname}"
)));
}
let path = git_dir.join(refname);
match read_ref_file(&path) {
Ok(Ref::Direct(oid)) => return Ok(oid),
Ok(Ref::Symbolic(target)) => {
return resolve_ref_depth(git_dir, &target, depth + 1);
}
Err(Error::Io(ref e)) if e.kind() == io::ErrorKind::NotFound => {}
Err(e) => return Err(e),
}
if let Some(oid) = lookup_packed_ref(git_dir, refname)? {
return Ok(oid);
}
Err(Error::InvalidRef(format!("ref not found: {refname}")))
}
fn lookup_packed_ref(git_dir: &Path, refname: &str) -> Result<Option<ObjectId>> {
let packed = git_dir.join("packed-refs");
let content = match fs::read_to_string(&packed) {
Ok(c) => c,
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(None),
Err(e) => return Err(Error::Io(e)),
};
for line in content.lines() {
if line.starts_with('#') || line.starts_with('^') {
continue;
}
let mut parts = line.splitn(2, ' ');
let hash = parts.next().unwrap_or("");
let name = parts.next().unwrap_or("").trim();
if name == refname && hash.len() == 40 {
let oid: ObjectId = hash.parse()?;
return Ok(Some(oid));
}
}
Ok(None)
}
pub fn write_ref(git_dir: &Path, refname: &str, oid: &ObjectId) -> Result<()> {
let path = git_dir.join(refname);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let content = format!("{oid}\n");
let lock = path.with_extension("lock");
fs::write(&lock, &content)?;
fs::rename(&lock, &path)?;
Ok(())
}
pub fn delete_ref(git_dir: &Path, refname: &str) -> Result<()> {
let path = git_dir.join(refname);
match fs::remove_file(&path) {
Ok(()) => Ok(()),
Err(e) if e.kind() == io::ErrorKind::NotFound => {
Err(Error::InvalidRef(format!("cannot delete '{refname}': not found")))
}
Err(e) => Err(Error::Io(e)),
}
}
pub fn read_head(git_dir: &Path) -> Result<Option<String>> {
match read_ref_file(&git_dir.join("HEAD"))? {
Ref::Symbolic(target) => Ok(Some(target)),
Ref::Direct(_) => Ok(None),
}
}
pub fn read_symbolic_ref(git_dir: &Path, refname: &str) -> Result<Option<String>> {
let path = git_dir.join(refname);
match read_ref_file(&path) {
Ok(Ref::Symbolic(target)) => Ok(Some(target)),
Ok(Ref::Direct(_)) => Ok(None),
Err(Error::Io(ref e)) if e.kind() == io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(e),
}
}
pub fn append_reflog(
git_dir: &Path,
refname: &str,
old_oid: &ObjectId,
new_oid: &ObjectId,
identity: &str,
message: &str,
) -> Result<()> {
let log_path = git_dir.join("logs").join(refname);
if let Some(parent) = log_path.parent() {
fs::create_dir_all(parent)?;
}
let line = format!("{old_oid} {new_oid} {identity}\t{message}\n");
let mut file = fs::OpenOptions::new()
.create(true)
.append(true)
.open(&log_path)?;
use io::Write;
file.write_all(line.as_bytes())?;
Ok(())
}
pub fn list_refs(git_dir: &Path, prefix: &str) -> Result<Vec<(String, ObjectId)>> {
let base = git_dir.join(prefix);
let mut results = Vec::new();
collect_refs(&base, prefix, git_dir, &mut results)?;
results.sort_by(|a, b| a.0.cmp(&b.0));
Ok(results)
}
pub fn list_refs_glob(git_dir: &Path, pattern: &str) -> Result<Vec<(String, ObjectId)>> {
let glob_pos = pattern.find(|c: char| c == '*' || c == '?' || c == '[');
let prefix = match glob_pos {
Some(pos) => match pattern[..pos].rfind('/') {
Some(slash) => &pattern[..=slash],
None => "",
},
None => pattern,
};
let all = list_refs(git_dir, prefix)?;
let mut results = Vec::new();
for (refname, oid) in all {
if glob_match(pattern, &refname) {
results.push((refname, oid));
}
}
Ok(results)
}
fn glob_match(pattern: &str, text: &str) -> bool {
let pat = pattern.as_bytes();
let txt = text.as_bytes();
let (mut pi, mut ti) = (0, 0);
let (mut star_pi, mut star_ti) = (usize::MAX, 0);
while ti < txt.len() {
if pi < pat.len() && (pat[pi] == b'?' || pat[pi] == txt[ti]) {
pi += 1;
ti += 1;
} else if pi < pat.len() && pat[pi] == b'*' {
star_pi = pi;
star_ti = ti;
pi += 1;
} else if star_pi != usize::MAX {
pi = star_pi + 1;
star_ti += 1;
ti = star_ti;
} else {
return false;
}
}
while pi < pat.len() && pat[pi] == b'*' {
pi += 1;
}
pi == pat.len()
}
fn collect_refs(
dir: &Path,
prefix: &str,
git_dir: &Path,
out: &mut Vec<(String, ObjectId)>,
) -> Result<()> {
let read = match fs::read_dir(dir) {
Ok(r) => r,
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
Err(e) => return Err(Error::Io(e)),
};
for entry in read {
let entry = entry?;
let file_type = entry.file_type()?;
let name = entry.file_name();
let name_str = name.to_string_lossy();
let refname = format!("{prefix}{name_str}");
if file_type.is_dir() {
collect_refs(&entry.path(), &format!("{refname}/"), git_dir, out)?;
} else if file_type.is_file() {
if let Ok(oid) = resolve_ref(git_dir, &refname) {
out.push((refname, oid))
}
}
}
Ok(())
}