use crate::{
error::{Error, GResult},
file_system::{Directory, File, FileSystem, FileSystemError},
object::{Commit, ObjectId, Tree},
parsing::ParseResult,
repo::Repo,
};
use accessory::Accessors;
use alloc::vec::Vec;
use nom::{
Parser,
branch::alt,
bytes::complete::{tag, take_till},
character::complete::{char, newline, not_line_ending, space0},
combinator::{all_consuming, opt},
multi::many0,
sequence::{delimited, preceded, terminated},
};
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
pub enum RefName {
Head,
Ref(Vec<u8>),
}
impl RefName {
pub(crate) async fn open_loose_ref<F: FileSystem>(
&self,
repo: &Repo<F>,
) -> GResult<Option<F::File>> {
use RefName::*;
let sub_path = match self {
Head => {
return Ok(Some(repo.git_dir.open_file(b"HEAD").await?));
}
Ref(path) => path,
};
let mut dir = repo.git_dir.open_subdir(b"refs").await?;
let mut components = sub_path.split(|b| *b == b'/');
let file_name = components
.next_back()
.ok_or_else(|| Error::RefNotFound(self.clone()))?;
for component in components {
dir = match dir.open_subdir(component).await {
Err(FileSystemError::NotFound(_)) => return Ok(None),
Err(e) => return Err(e.into()),
Ok(dir) => dir,
};
}
match dir.open_file(file_name).await {
Err(FileSystemError::NotFound(_)) => Ok(None),
Err(e) => Err(e.into()),
Ok(file) => Ok(Some(file)),
}
}
}
#[derive(Accessors, Clone)]
pub struct Ref {
#[access(get)]
name: RefName,
#[access(get)]
target: RefTarget,
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum RefTarget {
Direct(ObjectId),
Symbolic(RefName),
}
impl Ref {
pub(crate) async fn lookup<F: FileSystem>(repo: &Repo<F>, name: &RefName) -> GResult<Ref> {
let ref_type = {
if let Some(reference) = lookup_loose_ref(repo, name).await? {
reference
} else {
let mut packed_refs_file = repo.git_dir.open_file(b"packed-refs").await?;
let packed_refs = read_packed_refs(&mut packed_refs_file).await?;
if let Some((object_id, _)) = packed_refs
.into_iter()
.find(|(_, ref_name)| ref_name == name)
{
RefTarget::Direct(object_id)
} else {
return Err(Error::RefNotFound(name.clone()));
}
}
};
Ok(Self {
name: name.clone(),
target: ref_type,
})
}
pub async fn resolve_object_id<F: FileSystem>(&self, repo: &Repo<F>) -> GResult<ObjectId> {
let mut target: Ref = self.clone();
while let RefTarget::Symbolic(name) = target.target {
target = repo.lookup_ref(&name).await?;
}
match target.target {
RefTarget::Symbolic(_) => unreachable!(),
RefTarget::Direct(oid) => Ok(oid),
}
}
pub async fn peel_to_commit<F: FileSystem>(&self, repo: &Repo<F>) -> GResult<Option<Commit>> {
let oid = self.resolve_object_id(repo).await?;
let object = repo.lookup_object(oid).await?;
object.peel_to_commit(repo).await
}
pub async fn peel_to_tree<F: FileSystem>(&self, repo: &Repo<F>) -> GResult<Option<Tree>> {
let oid = self.resolve_object_id(repo).await?;
let object = repo.lookup_object(oid).await?;
object.peel_to_tree(repo).await
}
}
impl RefTarget {
pub(crate) fn parse_loose_ref(content: &[u8]) -> ParseResult<&[u8], Self> {
all_consuming(terminated(not_line_ending, newline))
.and_then(alt((
ObjectId::parse.map(RefTarget::Direct),
preceded(
tag("ref: refs/"),
take_till(|_| false)
.map(|name: &[u8]| RefTarget::Symbolic(RefName::Ref(name.to_vec()))),
),
)))
.parse(content)
}
}
pub(crate) async fn read_packed_refs<F: File>(
packed_refs_file: &mut F,
) -> GResult<Vec<(ObjectId, RefName)>> {
let packed_refs_data = packed_refs_file.read_all().await?;
let parse_one_ref = terminated(
(
terminated(ObjectId::parse, char(' ')),
delimited(
tag("refs/"),
not_line_ending.map(|name: &[u8]| RefName::Ref(name.to_vec())),
newline,
),
),
opt(delimited(char('^'), not_line_ending, newline)),
)
.map(Some);
let parse_comment = (space0, char('#'), not_line_ending, opt(newline)).map(|_| None);
let mut parser = all_consuming(many0(alt((parse_one_ref, parse_comment))));
let (_, refs) = parser
.parse(packed_refs_data.as_ref())
.map_err(|_| Error::MalformedPackedRefs)?;
Ok(refs.into_iter().flatten().collect())
}
pub(crate) async fn lookup_loose_ref<F: FileSystem>(
repo: &Repo<F>,
name: &RefName,
) -> GResult<Option<RefTarget>> {
let Some(mut ref_file) = name.open_loose_ref(repo).await? else {
return Ok(None);
};
let ref_content = ref_file.read_all().await?;
let (_, ref_type) =
RefTarget::parse_loose_ref(&ref_content).map_err(|_| Error::MalformedRef(name.clone()))?;
Ok(Some(ref_type))
}
#[cfg(test)]
mod test {
use crate::{
object::{Object, ObjectId},
test::helpers::{make_basic_repo, make_packfile_repo},
};
use core::matches;
use futures::executor::block_on;
use hex_literal::hex;
use super::*;
#[test]
fn resolve_head() {
let test_repo = make_basic_repo().unwrap();
let repo = test_repo.repo();
let head = block_on(repo.head()).unwrap();
let head_target = match head.target {
RefTarget::Direct(_) => panic!(),
RefTarget::Symbolic(name) => name,
};
let head_target = block_on(Ref::lookup(&repo, &head_target)).unwrap();
assert!(matches!(head_target.target, RefTarget::Direct(_)));
}
#[test]
fn parse_direct_ref() {
let content = b"6121d0b97779278fcc32cc8a02754e7c588d9c18\n";
let (_, parsed) = RefTarget::parse_loose_ref(content).unwrap();
assert_eq!(
parsed,
RefTarget::Direct(ObjectId::from_bytes(hex!(
"6121d0b97779278fcc32cc8a02754e7c588d9c18"
)))
);
}
#[test]
fn parse_symbolic_ref() {
let content = b"ref: refs/heads/main\n";
let (_, parsed) = RefTarget::parse_loose_ref(content).unwrap();
assert_eq!(
parsed,
RefTarget::Symbolic(RefName::Ref(b"heads/main".to_vec()))
);
}
#[test]
fn read_thin_packed_ref() {
let test_repo = make_packfile_repo().unwrap();
let repo = test_repo.repo();
let ref_name = RefName::Ref(b"heads/main".to_vec());
let reference = block_on(repo.lookup_ref(&ref_name)).unwrap();
let oid = block_on(reference.resolve_object_id(&repo)).unwrap();
let object = block_on(repo.lookup_object(oid)).unwrap();
assert!(matches!(object, Object::Commit(_)));
}
#[test]
fn read_fat_packed_ref() {
let test_repo = make_packfile_repo().unwrap();
let repo = test_repo.repo();
let ref_name = RefName::Ref(b"tags/a-fat-tag".to_vec());
let reference = block_on(repo.lookup_ref(&ref_name)).unwrap();
let oid = block_on(reference.resolve_object_id(&repo)).unwrap();
let object = block_on(repo.lookup_object(oid)).unwrap();
assert!(matches!(object, Object::Tag(_)));
}
#[test]
fn read_stash_packed_ref() {}
}