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 commit::CommitTree,
11 prefixes::{COMMIT_FAMILY, REFERENCE_FAMILY, REPOSITORY_FAMILY, TAG_FAMILY},
12 tag::TagTree,
13 Yoked,
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}
32
33pub type YokedRepository = Yoked<&'static <Repository as Archive>::Archived>;
34
35impl Repository {
36 pub fn exists<P: AsRef<Path>>(database: &rocksdb::DB, path: P) -> Result<bool> {
37 let cf = database
38 .cf_handle(REPOSITORY_FAMILY)
39 .context("repository column family missing")?;
40 let path = path.as_ref().to_str().context("invalid path")?;
41
42 Ok(database.get_pinned_cf(cf, path)?.is_some())
43 }
44
45 pub fn fetch_all(database: &rocksdb::DB) -> Result<BTreeMap<String, YokedRepository>> {
46 let cf = database
47 .cf_handle(REPOSITORY_FAMILY)
48 .context("repository column family missing")?;
49
50 database
51 .iterator_cf(cf, IteratorMode::Start)
52 .filter_map(Result::ok)
53 .map(|(key, value)| {
54 let key = String::from_utf8(key.into_vec()).context("invalid repo name")?;
55 let value = Yoke::try_attach_to_cart(value, |data| {
56 rkyv::access::<_, rkyv::rancor::Error>(data)
57 })?;
58
59 Ok((key, value))
60 })
61 .collect()
62 }
63
64 pub fn insert<P: AsRef<Path>>(&self, database: &rocksdb::DB, path: P) -> Result<()> {
65 let cf = database
66 .cf_handle(REPOSITORY_FAMILY)
67 .context("repository column family missing")?;
68 let path = path.as_ref().to_str().context("invalid path")?;
69
70 database.put_cf(cf, path, rkyv::to_bytes::<rkyv::rancor::Error>(self)?)?;
71
72 Ok(())
73 }
74
75 pub fn open<P: AsRef<Path>>(
76 database: &rocksdb::DB,
77 path: P,
78 ) -> Result<Option<YokedRepository>> {
79 let cf = database
80 .cf_handle(REPOSITORY_FAMILY)
81 .context("repository column family missing")?;
82
83 let path = path.as_ref().to_str().context("invalid path")?;
84 let Some(value) = database.get_cf(cf, path)? else {
85 return Ok(None);
86 };
87
88 Yoke::try_attach_to_cart(value.into_boxed_slice(), |data| {
89 rkyv::access::<_, rkyv::rancor::Error>(data)
90 })
91 .map(Some)
92 .context("Failed to open repository")
93 }
94}
95
96impl ArchivedRepository {
97 pub fn delete<P: AsRef<Path>>(&self, database: &rocksdb::DB, path: P) -> Result<()> {
98 let start_id = self.id.0.to_native().to_be_bytes();
99 let mut end_id = start_id;
100 *end_id.last_mut().unwrap() += 1;
101
102 // delete commits
103 let commit_cf = database
104 .cf_handle(COMMIT_FAMILY)
105 .context("commit column family missing")?;
106 database.delete_range_cf(commit_cf, start_id, end_id)?;
107
108 // delete tags
109 let tag_cf = database
110 .cf_handle(TAG_FAMILY)
111 .context("tag column family missing")?;
112 database.delete_range_cf(tag_cf, start_id, end_id)?;
113
114 // delete self
115 let repo_cf = database
116 .cf_handle(REPOSITORY_FAMILY)
117 .context("repository column family missing")?;
118 let path = path.as_ref().to_str().context("invalid path")?;
119 database.delete_cf(repo_cf, path)?;
120
121 Ok(())
122 }
123
124 pub fn commit_tree(&self, database: Arc<rocksdb::DB>, reference: &str) -> CommitTree {
125 CommitTree::new(database, RepositoryId(self.id.0.to_native()), reference)
126 }
127
128 pub fn tag_tree(&self, database: Arc<rocksdb::DB>) -> TagTree {
129 TagTree::new(database, RepositoryId(self.id.0.to_native()))
130 }
131
132 pub fn replace_heads(&self, database: &rocksdb::DB, new_heads: &Vec<String>) -> Result<()> {
133 let cf = database
134 .cf_handle(REFERENCE_FAMILY)
135 .context("missing reference column family")?;
136
137 database.put_cf(
138 cf,
139 self.id.0.to_native().to_be_bytes(),
140 rkyv::to_bytes::<rkyv::rancor::Error>(new_heads)?,
141 )?;
142
143 Ok(())
144 }
145
146 #[allow(clippy::type_complexity)]
147 pub fn heads(
148 &self,
149 database: &rocksdb::DB,
150 ) -> Result<Option<Yoke<&'static ArchivedHeads, Box<[u8]>>>> {
151 let cf = database
152 .cf_handle(REFERENCE_FAMILY)
153 .context("missing reference column family")?;
154
155 let Some(bytes) = database.get_cf(cf, self.id.0.to_native().to_be_bytes())? else {
156 return Ok(None);
157 };
158
159 Yoke::try_attach_to_cart(Box::from(bytes), |bytes| {
160 rkyv::access::<_, rkyv::rancor::Error>(bytes)
161 })
162 .context("failed to deserialize heads")
163 .map(Some)
164 }
165}
166
167#[derive(Serialize, Archive, Debug, Clone, PartialEq, Eq, Hash)]
168pub struct Heads(pub Vec<String>);
169
170#[derive(Serialize, Archive, Debug, Copy, Clone, PartialEq, Eq, Hash)]
171pub struct RepositoryId(pub u64);
172
173impl RepositoryId {
174 pub fn new() -> Self {
175 Self(random())
176 }
177}
178
179impl Deref for RepositoryId {
180 type Target = u64;
181
182 fn deref(&self) -> &Self::Target {
183 &self.0
184 }
185}