Skip to main content

git_async/
repo.rs

1use crate::{
2    error::{Error, GResult},
3    file_system::{Directory, FileSystem, FileSystemError, search_for_files},
4    object::{Object, ObjectId},
5    object_store::{ObjectSize, ObjectType, cache::IndexCache},
6    reference::{Ref, RefName, read_packed_refs},
7};
8use alloc::collections::BTreeSet;
9use alloc::vec::Vec;
10
11/// Configuration for opening a repository
12pub struct RepoConfig {
13    pub(crate) index_offset_cache_max: usize,
14}
15impl RepoConfig {
16    /// Construct a default [`RepoConfig`].
17    ///
18    /// See [`RepoConfig::default()`] for further details.
19    pub fn new() -> Self {
20        Self::default()
21    }
22
23    /// Set the maximum size of the cache that holds object offsets from pack index files.
24    pub fn index_offset_cache_max(&mut self, size: usize) -> &mut Self {
25        self.index_offset_cache_max = size;
26        self
27    }
28
29    /// Open a repo with this configuration.
30    pub async fn open<F: FileSystem>(&self, open_dir: F::Directory) -> GResult<Repo<F>> {
31        Repo::open_with_config(open_dir, self).await
32    }
33}
34
35impl Default for RepoConfig {
36    /// Creates a default [`RepoConfig`].
37    ///
38    /// The default maximum size of the index offset cache is 64 MiB.
39    fn default() -> Self {
40        Self {
41            index_offset_cache_max: 64 * 1024 * 1024,
42        }
43    }
44}
45
46/// A handle to a Git repository
47///
48/// It is generic over the implementation of filesystem operations.
49pub struct Repo<F: FileSystem> {
50    pub(crate) git_dir: F::Directory,
51    pub(crate) pack_dir: F::Directory,
52    pub(crate) index_cache: IndexCache,
53}
54
55impl<F: FileSystem> Repo<F> {
56    pub(crate) async fn open_with_config(
57        open_dir: F::Directory,
58        config: &RepoConfig,
59    ) -> GResult<Self> {
60        let git_dir = Self::resolve_git_dir(open_dir).await?;
61        let pack_dir = git_dir
62            .open_subdir(b"objects")
63            .await?
64            .open_subdir(b"pack")
65            .await?;
66        let index_cache = IndexCache::new(&pack_dir, config).await?;
67        Ok(Repo {
68            git_dir,
69            pack_dir,
70            index_cache,
71        })
72    }
73
74    pub(crate) async fn resolve_git_dir(open_dir: F::Directory) -> GResult<F::Directory> {
75        let head = open_dir.open_file(b"HEAD").await;
76        match head {
77            Ok(_) => Ok(open_dir),
78            Err(FileSystemError::NotFound(_)) => {
79                let git_dir = open_dir.open_subdir(b".git").await?;
80                let head = git_dir.open_file(b"HEAD").await;
81                match head {
82                    Ok(_) => Ok(git_dir),
83                    Err(FileSystemError::NotFound(_)) => Err(Error::NotAGitRepository),
84                    Err(e) => Err(e.into()),
85                }
86            }
87            Err(e) => Err(e.into()),
88        }
89    }
90
91    /// Open the repository located at `git_dir` using a default [`RepoConfig`].
92    pub async fn open(git_dir: F::Directory) -> GResult<Self> {
93        Self::open_with_config(git_dir, &RepoConfig::default()).await
94    }
95
96    /// Collect all the refs tracked by the repository
97    ///
98    /// Includes HEAD, branches, tags, remotes and the stash
99    pub async fn ref_names(&self) -> GResult<BTreeSet<RefName>> {
100        let mut out: BTreeSet<RefName> = BTreeSet::new();
101        out.insert(RefName::Head);
102        match self.git_dir.open_file(b"packed-refs").await {
103            Err(FileSystemError::NotFound(_)) => {}
104            Err(e) => return Err(e.into()),
105            Ok(mut packed_refs_file) => {
106                let packed_refs = read_packed_refs(&mut packed_refs_file).await?;
107                for (_, ref_name) in packed_refs {
108                    out.insert(ref_name);
109                }
110            }
111        }
112        let refs_dir = self.git_dir.open_subdir(b"refs").await?;
113        let refs_paths = search_for_files(&refs_dir).await?;
114        for path in refs_paths {
115            let mut name: Vec<u8> = Vec::new();
116            for component in path {
117                if !name.is_empty() {
118                    name.push(b'/');
119                }
120                name.extend_from_slice(&component);
121            }
122            out.insert(RefName::Ref(name));
123        }
124        Ok(out)
125    }
126
127    /// Get the repository's HEAD ref.
128    pub async fn head(&self) -> GResult<Ref> {
129        Ref::lookup(self, &RefName::Head).await
130    }
131
132    /// Take a ref name and look up its content.
133    pub async fn lookup_ref(&self, name: &RefName) -> GResult<Ref> {
134        Ref::lookup(self, name).await
135    }
136
137    /// Look up a particular object in the repository, reading the entire object
138    /// into memory.
139    pub async fn lookup_object(&self, id: ObjectId) -> GResult<Object> {
140        Object::lookup(self, id).await
141    }
142
143    /// Look up the size and type of an object, without reading it to memory or
144    /// parsing its content.
145    pub async fn lookup_object_size_type(&self, id: ObjectId) -> GResult<(ObjectSize, ObjectType)> {
146        Object::lookup_size_type(self, id).await
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153    use crate::{
154        reference::RefTarget,
155        test::{helpers::make_basic_repo, impls::TestFileSystem, repo::TestRepo},
156    };
157    use futures::executor::block_on;
158
159    #[test]
160    fn read_head() {
161        let test_repo = TestRepo::new().unwrap();
162        let repo = test_repo.repo();
163        let head = block_on(repo.head()).unwrap();
164        assert_eq!(
165            head.target(),
166            &RefTarget::Symbolic(RefName::Ref(Vec::from(b"heads/main")))
167        );
168    }
169
170    #[test]
171    fn read_refs() {
172        let test_repo = make_basic_repo().unwrap();
173        test_repo.run_git(["branch", "a-branch"]).unwrap();
174        test_repo.run_git(["branch", "foo/a-branch"]).unwrap();
175        test_repo.run_git(["tag", "thin-tag"]).unwrap();
176        test_repo.run_git(["tag", "bar/thin-tag"]).unwrap();
177        test_repo
178            .run_git(["tag", "-a", "-m", "a tag message", "fat-tag"])
179            .unwrap();
180        test_repo
181            .run_git(["update-ref", "refs/remotes/origin/main", "HEAD"])
182            .unwrap();
183
184        let repo = test_repo.repo();
185        let refs = block_on(repo.ref_names()).unwrap();
186        let expected: BTreeSet<_> = vec![
187            RefName::Head,
188            RefName::Ref(b"stash".to_vec()),
189            RefName::Ref(b"heads/main".to_vec()),
190            RefName::Ref(b"heads/a-branch".to_vec()),
191            RefName::Ref(b"heads/foo/a-branch".to_vec()),
192            RefName::Ref(b"tags/thin-tag".to_vec()),
193            RefName::Ref(b"tags/bar/thin-tag".to_vec()),
194            RefName::Ref(b"tags/fat-tag".to_vec()),
195            RefName::Ref(b"tags/a-fat-tag".to_vec()),
196            RefName::Ref(b"remotes/origin/main".to_vec()),
197        ]
198        .into_iter()
199        .collect();
200        assert_eq!(&refs, &expected);
201    }
202
203    #[test]
204    fn open_non_bare_repo() {
205        let test_repo = make_basic_repo().unwrap();
206        let root_dir = test_repo.root_dir();
207        block_on(Repo::<TestFileSystem>::open(root_dir)).unwrap();
208    }
209}