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