1use crate::{
10 error::{Error, GResult},
11 file_system::{Directory, File, FileSystem, FileSystemError},
12 object::{Commit, ObjectId, Tree},
13 parsing::ParseResult,
14 repo::Repo,
15};
16use accessory::Accessors;
17use alloc::vec::Vec;
18use nom::{
19 Parser,
20 branch::alt,
21 bytes::complete::{tag, take_till},
22 character::complete::{char, newline, not_line_ending, space0},
23 combinator::{all_consuming, opt},
24 multi::many0,
25 sequence::{delimited, preceded, terminated},
26};
27
28#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
30pub enum RefName {
31 Head,
33 Ref(Vec<u8>),
35}
36
37impl RefName {
38 pub(crate) async fn open_loose_ref<F: FileSystem>(
39 &self,
40 repo: &Repo<F>,
41 ) -> GResult<Option<F::File>> {
42 use RefName::*;
43 let sub_path = match self {
44 Head => {
45 return Ok(Some(repo.git_dir.open_file(b"HEAD").await?));
46 }
47 Ref(path) => path,
48 };
49 let mut dir = repo.git_dir.open_subdir(b"refs").await?;
50 let mut components = sub_path.split(|b| *b == b'/');
51 let file_name = components
52 .next_back()
53 .ok_or_else(|| Error::RefNotFound(self.clone()))?;
54 for component in components {
55 dir = match dir.open_subdir(component).await {
56 Err(FileSystemError::NotFound(_)) => return Ok(None),
57 Err(e) => return Err(e.into()),
58 Ok(dir) => dir,
59 };
60 }
61 match dir.open_file(file_name).await {
62 Err(FileSystemError::NotFound(_)) => Ok(None),
63 Err(e) => Err(e.into()),
64 Ok(file) => Ok(Some(file)),
65 }
66 }
67}
68
69#[derive(Accessors, Clone)]
71pub struct Ref {
72 #[access(get)]
74 name: RefName,
75
76 #[access(get)]
81 target: RefTarget,
82}
83
84#[derive(Debug, PartialEq, Eq, Clone)]
89pub enum RefTarget {
90 Direct(ObjectId),
92 Symbolic(RefName),
94}
95
96impl Ref {
97 pub(crate) async fn lookup<F: FileSystem>(repo: &Repo<F>, name: &RefName) -> GResult<Ref> {
98 let ref_type = {
99 if let Some(reference) = lookup_loose_ref(repo, name).await? {
100 reference
101 } else {
102 let mut packed_refs_file = repo.git_dir.open_file(b"packed-refs").await?;
103 let packed_refs = read_packed_refs(&mut packed_refs_file).await?;
104 if let Some((object_id, _)) = packed_refs
105 .into_iter()
106 .find(|(_, ref_name)| ref_name == name)
107 {
108 RefTarget::Direct(object_id)
109 } else {
110 return Err(Error::RefNotFound(name.clone()));
111 }
112 }
113 };
114 Ok(Self {
115 name: name.clone(),
116 target: ref_type,
117 })
118 }
119
120 pub async fn resolve_object_id<F: FileSystem>(&self, repo: &Repo<F>) -> GResult<ObjectId> {
123 let mut target: Ref = self.clone();
124 while let RefTarget::Symbolic(name) = target.target {
125 target = repo.lookup_ref(&name).await?;
126 }
127 match target.target {
128 RefTarget::Symbolic(_) => unreachable!(),
129 RefTarget::Direct(oid) => Ok(oid),
130 }
131 }
132
133 pub async fn peel_to_commit<F: FileSystem>(&self, repo: &Repo<F>) -> GResult<Option<Commit>> {
137 let oid = self.resolve_object_id(repo).await?;
138 let object = repo.lookup_object(oid).await?;
139 object.peel_to_commit(repo).await
140 }
141
142 pub async fn peel_to_tree<F: FileSystem>(&self, repo: &Repo<F>) -> GResult<Option<Tree>> {
146 let oid = self.resolve_object_id(repo).await?;
147 let object = repo.lookup_object(oid).await?;
148 object.peel_to_tree(repo).await
149 }
150}
151
152impl RefTarget {
153 pub(crate) fn parse_loose_ref(content: &[u8]) -> ParseResult<&[u8], Self> {
154 all_consuming(terminated(not_line_ending, newline))
155 .and_then(alt((
156 ObjectId::parse.map(RefTarget::Direct),
157 preceded(
158 tag("ref: refs/"),
159 take_till(|_| false)
160 .map(|name: &[u8]| RefTarget::Symbolic(RefName::Ref(name.to_vec()))),
161 ),
162 )))
163 .parse(content)
164 }
165}
166
167pub(crate) async fn read_packed_refs<F: File>(
168 packed_refs_file: &mut F,
169) -> GResult<Vec<(ObjectId, RefName)>> {
170 let packed_refs_data = packed_refs_file.read_all().await?;
171 let parse_one_ref = terminated(
172 (
173 terminated(ObjectId::parse, char(' ')),
174 delimited(
175 tag("refs/"),
176 not_line_ending.map(|name: &[u8]| RefName::Ref(name.to_vec())),
177 newline,
178 ),
179 ),
180 opt(delimited(char('^'), not_line_ending, newline)),
181 )
182 .map(Some);
183 let parse_comment = (space0, char('#'), not_line_ending, opt(newline)).map(|_| None);
184 let mut parser = all_consuming(many0(alt((parse_one_ref, parse_comment))));
185 let (_, refs) = parser
186 .parse(packed_refs_data.as_ref())
187 .map_err(|_| Error::MalformedPackedRefs)?;
188 Ok(refs.into_iter().flatten().collect())
189}
190
191pub(crate) async fn lookup_loose_ref<F: FileSystem>(
192 repo: &Repo<F>,
193 name: &RefName,
194) -> GResult<Option<RefTarget>> {
195 let Some(mut ref_file) = name.open_loose_ref(repo).await? else {
196 return Ok(None);
197 };
198 let ref_content = ref_file.read_all().await?;
199 let (_, ref_type) =
200 RefTarget::parse_loose_ref(&ref_content).map_err(|_| Error::MalformedRef(name.clone()))?;
201 Ok(Some(ref_type))
202}
203
204#[cfg(test)]
205mod test {
206 use crate::{
207 object::{Object, ObjectId},
208 test::helpers::{make_basic_repo, make_packfile_repo},
209 };
210 use core::matches;
211 use futures::executor::block_on;
212 use hex_literal::hex;
213
214 use super::*;
215
216 #[test]
217 fn resolve_head() {
218 let test_repo = make_basic_repo().unwrap();
219 let repo = test_repo.repo();
220 let head = block_on(repo.head()).unwrap();
221 let head_target = match head.target {
222 RefTarget::Direct(_) => panic!(),
223 RefTarget::Symbolic(name) => name,
224 };
225 let head_target = block_on(Ref::lookup(&repo, &head_target)).unwrap();
226 assert!(matches!(head_target.target, RefTarget::Direct(_)));
227 }
228
229 #[test]
230 fn parse_direct_ref() {
231 let content = b"6121d0b97779278fcc32cc8a02754e7c588d9c18\n";
232 let (_, parsed) = RefTarget::parse_loose_ref(content).unwrap();
233 assert_eq!(
234 parsed,
235 RefTarget::Direct(ObjectId::from_bytes(hex!(
236 "6121d0b97779278fcc32cc8a02754e7c588d9c18"
237 )))
238 );
239 }
240
241 #[test]
242 fn parse_symbolic_ref() {
243 let content = b"ref: refs/heads/main\n";
244 let (_, parsed) = RefTarget::parse_loose_ref(content).unwrap();
245 assert_eq!(
246 parsed,
247 RefTarget::Symbolic(RefName::Ref(b"heads/main".to_vec()))
248 );
249 }
250
251 #[test]
252 fn read_thin_packed_ref() {
253 let test_repo = make_packfile_repo().unwrap();
254 let repo = test_repo.repo();
255 let ref_name = RefName::Ref(b"heads/main".to_vec());
256 let reference = block_on(repo.lookup_ref(&ref_name)).unwrap();
257 let oid = block_on(reference.resolve_object_id(&repo)).unwrap();
258 let object = block_on(repo.lookup_object(oid)).unwrap();
259 assert!(matches!(object, Object::Commit(_)));
260 }
261
262 #[test]
263 fn read_fat_packed_ref() {
264 let test_repo = make_packfile_repo().unwrap();
265 let repo = test_repo.repo();
266 let ref_name = RefName::Ref(b"tags/a-fat-tag".to_vec());
267 let reference = block_on(repo.lookup_ref(&ref_name)).unwrap();
268 let oid = block_on(reference.resolve_object_id(&repo)).unwrap();
269 let object = block_on(repo.lookup_object(oid)).unwrap();
270 assert!(matches!(object, Object::Tag(_)));
271 }
272
273 #[test]
274 fn read_stash_packed_ref() {}
275}