Skip to main content

git_async/
reference.rs

1//! A module for working with git refs
2//!
3//! A git ref is a file which points to an object or to another ref.
4//!
5//! To look up a ref, first construct a [`RefName`] and then use the
6//! [`Repo::lookup_ref`] method. A list of all refs in the repository can also
7//! be accessed using the [`Repo::ref_names`] method.
8
9use 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/// The name of a git ref
29#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
30pub enum RefName {
31    /// The head of a repo, which is called `HEAD` by git.
32    Head,
33    /// A non-HEAD ref such as `v1.7.0` or `origin/main`.
34    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/// The contents of a git ref
70#[derive(Accessors, Clone)]
71pub struct Ref {
72    /// The name of the ref
73    #[access(get)]
74    name: RefName,
75
76    /// The target of the ref
77    ///
78    /// Refs can be either direct (pointing to an object) or symbolic (pointing
79    /// to another ref).
80    #[access(get)]
81    target: RefTarget,
82}
83
84/// The target of a git ref
85///
86/// Refs can be either direct (pointing to an object) or symbolic (pointing to
87/// another ref).
88#[derive(Debug, PartialEq, Eq, Clone)]
89pub enum RefTarget {
90    /// A direct ref, pointing to an object
91    Direct(ObjectId),
92    /// A symbolic ref, pointing to another ref
93    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    /// Follow a chain of refs until a direct ref is obtained, and return the
121    /// object ID that it points to.
122    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    /// Peel the ref to a commit object.
134    ///
135    /// Returns `None` if the ref does not point to a commit object.
136    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    /// Peel the ref to a tree object.
143    ///
144    /// Returns `None` if the ref does not point to a commit or a tree object.
145    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}