1use std::{collections::BTreeMap, ops::Deref, path::Path, sync::Arc};
2
3use anyhow::{Context, Result};
4use rand::random;
5use rkyv::{Archive, Serialize};
6use rocksdb::IteratorMode;
7use yoke::{Yoke, Yokeable};
8
9use crate::database::schema::{
10 Yoked,
11 commit::CommitTree,
12 prefixes::{COMMIT_FAMILY, REFERENCE_FAMILY, REPOSITORY_FAMILY, TAG_FAMILY},
13 tag::TagTree,
14};
15
16#[derive(Serialize, Archive, Debug, PartialEq, Eq, Hash, Yokeable)]
17pub struct Repository {
18 /// The ID of the repository, as stored in `RocksDB`
19 pub id: RepositoryId,
20 /// The "clean name" of the repository (ie. `hello-world.git`)
21 pub name: String,
22 /// The description of the repository, as it is stored in the `description` file in the
23 /// bare repo root
24 pub description: Option<String>,
25 /// The owner of the repository (`gitweb.owner` in the repository configuration)
26 pub owner: Option<String>,
27 /// The last time this repository was updated, currently read from the directory mtime
28 pub last_modified: (i64, i32),
29 /// The default branch for Git operations
30 pub default_branch: Option<String>,
31 /// Whether the repository is available for HTTP(s) cloning
32 ///
33 /// This is set to `true` based on the presence of `git-daemon-export-ok` in the repository
34 pub exported: bool,
35}
36
37pub type YokedRepository = Yoked<&'static <Repository as Archive>::Archived>;
38
39impl Repository {
40 pub fn exists<P: AsRef<Path>>(database: &rocksdb::DB, path: P) -> Result<bool> {
41 let cf = database
42 .cf_handle(REPOSITORY_FAMILY)
43 .context("repository column family missing")?;
44 let path = path.as_ref().to_str().context("invalid path")?;
45
46 Ok(database.get_pinned_cf(cf, path)?.is_some())
47 }
48
49 pub fn fetch_all(database: &rocksdb::DB) -> Result<BTreeMap<String, YokedRepository>> {
50 let cf = database
51 .cf_handle(REPOSITORY_FAMILY)
52 .context("repository column family missing")?;
53
54 database
55 .iterator_cf(cf, IteratorMode::Start)
56 .filter_map(Result::ok)
57 .map(|(key, value)| {
58 let key = String::from_utf8(key.into_vec()).context("invalid repo name")?;
59 let value = Yoke::try_attach_to_cart(value, |data| {
60 rkyv::access::<_, rkyv::rancor::Error>(data)
61 })?;
62
63 Ok((key, value))
64 })
65 .collect()
66 }
67
68 pub fn insert<P: AsRef<Path>>(&self, database: &rocksdb::DB, path: P) -> Result<()> {
69 let cf = database
70 .cf_handle(REPOSITORY_FAMILY)
71 .context("repository column family missing")?;
72 let path = path.as_ref().to_str().context("invalid path")?;
73
74 database.put_cf(cf, path, rkyv::to_bytes::<rkyv::rancor::Error>(self)?)?;
75
76 Ok(())
77 }
78
79 pub fn open<P: AsRef<Path>>(
80 database: &rocksdb::DB,
81 path: P,
82 ) -> Result<Option<YokedRepository>> {
83 let cf = database
84 .cf_handle(REPOSITORY_FAMILY)
85 .context("repository column family missing")?;
86
87 let path = path.as_ref().to_str().context("invalid path")?;
88 let Some(value) = database.get_cf(cf, path)? else {
89 return Ok(None);
90 };
91
92 Yoke::try_attach_to_cart(value.into_boxed_slice(), |data| {
93 rkyv::access::<_, rkyv::rancor::Error>(data)
94 })
95 .map(Some)
96 .context("Failed to open repository")
97 }
98}
99
100impl ArchivedRepository {
101 pub fn delete<P: AsRef<Path>>(&self, database: &rocksdb::DB, path: P) -> Result<()> {
102 let start_id = self.id.0.to_native().to_be_bytes();
103 let mut end_id = start_id;
104 *end_id.last_mut().unwrap() += 1;
105
106 // delete commits
107 let commit_cf = database
108 .cf_handle(COMMIT_FAMILY)
109 .context("commit column family missing")?;
110 database.delete_range_cf(commit_cf, start_id, end_id)?;
111
112 // delete tags
113 let tag_cf = database
114 .cf_handle(TAG_FAMILY)
115 .context("tag column family missing")?;
116 database.delete_range_cf(tag_cf, start_id, end_id)?;
117
118 // delete self
119 let repo_cf = database
120 .cf_handle(REPOSITORY_FAMILY)
121 .context("repository column family missing")?;
122 let path = path.as_ref().to_str().context("invalid path")?;
123 database.delete_cf(repo_cf, path)?;
124
125 Ok(())
126 }
127
128 pub fn commit_tree(&self, database: Arc<rocksdb::DB>, reference: &str) -> CommitTree {
129 CommitTree::new(database, RepositoryId(self.id.0.to_native()), reference)
130 }
131
132 pub fn tag_tree(&self, database: Arc<rocksdb::DB>) -> TagTree {
133 TagTree::new(database, RepositoryId(self.id.0.to_native()))
134 }
135
136 pub fn replace_heads(&self, database: &rocksdb::DB, new_heads: &Vec<String>) -> Result<()> {
137 let cf = database
138 .cf_handle(REFERENCE_FAMILY)
139 .context("missing reference column family")?;
140
141 database.put_cf(
142 cf,
143 self.id.0.to_native().to_be_bytes(),
144 rkyv::to_bytes::<rkyv::rancor::Error>(new_heads)?,
145 )?;
146
147 Ok(())
148 }
149
150 #[allow(clippy::type_complexity)]
151 pub fn heads(
152 &self,
153 database: &rocksdb::DB,
154 ) -> Result<Option<Yoke<&'static ArchivedHeads, Box<[u8]>>>> {
155 let cf = database
156 .cf_handle(REFERENCE_FAMILY)
157 .context("missing reference column family")?;
158
159 let Some(bytes) = database.get_cf(cf, self.id.0.to_native().to_be_bytes())? else {
160 return Ok(None);
161 };
162
163 Yoke::try_attach_to_cart(Box::from(bytes), |bytes| {
164 rkyv::access::<_, rkyv::rancor::Error>(bytes)
165 })
166 .context("failed to deserialize heads")
167 .map(Some)
168 }
169}
170
171#[derive(Serialize, Archive, Debug, Clone, PartialEq, Eq, Hash)]
172pub struct Heads(pub Vec<String>);
173
174#[derive(Serialize, Archive, Debug, Copy, Clone, PartialEq, Eq, Hash)]
175pub struct RepositoryId(pub u64);
176
177impl RepositoryId {
178 pub fn new() -> Self {
179 Self(random())
180 }
181}
182
183impl Deref for RepositoryId {
184 type Target = u64;
185
186 fn deref(&self) -> &Self::Target {
187 &self.0
188 }
189}