1use std::{borrow::Cow, ops::Deref, sync::Arc};
2
3use anyhow::Context;
4use git2::{Oid, Signature};
5use rocksdb::{IteratorMode, ReadOptions, WriteBatch};
6use serde::{Deserialize, Deserializer, Serialize, Serializer};
7use time::OffsetDateTime;
8use tracing::debug;
9use yoke::{Yoke, Yokeable};
10
11use crate::database::schema::{
12 prefixes::{COMMIT_COUNT_FAMILY, COMMIT_FAMILY},
13 repository::RepositoryId,
14 Yoked,
15};
16
17#[derive(Serialize, Deserialize, Debug, Yokeable)]
18pub struct Commit<'a> {
19 #[serde(borrow)]
20 pub summary: Cow<'a, str>,
21 #[serde(borrow)]
22 pub message: Cow<'a, str>,
23 pub author: Author<'a>,
24 pub committer: Author<'a>,
25 pub hash: CommitHash<'a>,
26}
27
28impl<'a> Commit<'a> {
29 pub fn new(
30 commit: &'a git2::Commit<'_>,
31 author: &'a git2::Signature<'_>,
32 committer: &'a git2::Signature<'_>,
33 ) -> Self {
34 Self {
35 summary: commit
36 .summary_bytes()
37 .map_or(Cow::Borrowed(""), String::from_utf8_lossy),
38 message: commit
39 .body_bytes()
40 .map_or(Cow::Borrowed(""), String::from_utf8_lossy),
41 committer: committer.into(),
42 author: author.into(),
43 hash: CommitHash::Oid(commit.id()),
44 }
45 }
46
47 pub fn insert(&self, tree: &CommitTree, id: u64, tx: &mut WriteBatch) -> anyhow::Result<()> {
48 tree.insert(id, self, tx)
49 }
50}
51
52#[derive(Debug)]
53pub enum CommitHash<'a> {
54 Oid(Oid),
55 Bytes(&'a [u8]),
56}
57
58impl<'a> Deref for CommitHash<'a> {
59 type Target = [u8];
60
61 fn deref(&self) -> &Self::Target {
62 match self {
63 CommitHash::Oid(v) => v.as_bytes(),
64 CommitHash::Bytes(v) => v,
65 }
66 }
67}
68
69impl Serialize for CommitHash<'_> {
70 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
71 where
72 S: Serializer,
73 {
74 match self {
75 CommitHash::Oid(v) => serializer.serialize_bytes(v.as_bytes()),
76 CommitHash::Bytes(v) => serializer.serialize_bytes(v),
77 }
78 }
79}
80
81impl<'a, 'de: 'a> Deserialize<'de> for CommitHash<'a> {
82 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
83 where
84 D: Deserializer<'de>,
85 {
86 let bytes = <&'a [u8]>::deserialize(deserializer)?;
87 Ok(Self::Bytes(bytes))
88 }
89}
90
91#[derive(Serialize, Deserialize, Debug)]
92pub struct Author<'a> {
93 #[serde(borrow)]
94 pub name: Cow<'a, str>,
95 #[serde(borrow)]
96 pub email: Cow<'a, str>,
97 pub time: OffsetDateTime,
98}
99
100impl<'a> From<&'a git2::Signature<'_>> for Author<'a> {
101 fn from(author: &'a Signature<'_>) -> Self {
102 Self {
103 name: String::from_utf8_lossy(author.name_bytes()),
104 email: String::from_utf8_lossy(author.email_bytes()),
105 // TODO: this needs to deal with offset
106 time: OffsetDateTime::from_unix_timestamp(author.when().seconds()).unwrap(),
107 }
108 }
109}
110
111pub struct CommitTree {
112 db: Arc<rocksdb::DB>,
113 pub prefix: Box<[u8]>,
114}
115
116pub type YokedCommit = Yoked<Commit<'static>>;
117
118impl CommitTree {
119 pub(super) fn new(db: Arc<rocksdb::DB>, repository: RepositoryId, reference: &str) -> Self {
120 let mut prefix = Vec::with_capacity(std::mem::size_of::<u64>() + reference.len() + 1);
121 prefix.extend_from_slice(&repository.to_be_bytes());
122 prefix.extend_from_slice(reference.as_bytes());
123 prefix.push(b'\0');
124
125 Self {
126 db,
127 prefix: prefix.into_boxed_slice(),
128 }
129 }
130
131 pub fn drop_commits(&self) -> anyhow::Result<()> {
132 let mut to = self.prefix.clone();
133 *to.last_mut().unwrap() += 1;
134
135 let commit_cf = self
136 .db
137 .cf_handle(COMMIT_FAMILY)
138 .context("commit column family missing")?;
139 self.db.delete_range_cf(commit_cf, &self.prefix, &to)?;
140
141 let commit_count_cf = self
142 .db
143 .cf_handle(COMMIT_COUNT_FAMILY)
144 .context("missing column family")?;
145 self.db.delete_cf(commit_count_cf, &self.prefix)?;
146
147 Ok(())
148 }
149
150 pub fn update_counter(&self, count: u64, tx: &mut WriteBatch) -> anyhow::Result<()> {
151 let cf = self
152 .db
153 .cf_handle(COMMIT_COUNT_FAMILY)
154 .context("missing column family")?;
155
156 tx.put_cf(cf, &self.prefix, count.to_be_bytes());
157
158 Ok(())
159 }
160
161 pub fn len(&self) -> anyhow::Result<u64> {
162 let cf = self
163 .db
164 .cf_handle(COMMIT_COUNT_FAMILY)
165 .context("missing column family")?;
166
167 let Some(res) = self.db.get_pinned_cf(cf, &self.prefix)? else {
168 return Ok(0);
169 };
170
171 let mut out = [0_u8; std::mem::size_of::<u64>()];
172 out.copy_from_slice(&res);
173 Ok(u64::from_be_bytes(out))
174 }
175
176 fn insert(&self, id: u64, commit: &Commit<'_>, tx: &mut WriteBatch) -> anyhow::Result<()> {
177 let cf = self
178 .db
179 .cf_handle(COMMIT_FAMILY)
180 .context("missing column family")?;
181
182 let mut key = self.prefix.to_vec();
183 key.extend_from_slice(&id.to_be_bytes());
184
185 tx.put_cf(cf, key, bincode::serialize(commit)?);
186
187 Ok(())
188 }
189
190 pub fn fetch_latest_one(&self) -> Result<Option<YokedCommit>, anyhow::Error> {
191 let mut key = self.prefix.to_vec();
192 key.extend_from_slice(&(self.len()?.saturating_sub(1)).to_be_bytes());
193
194 let cf = self
195 .db
196 .cf_handle(COMMIT_FAMILY)
197 .context("missing column family")?;
198
199 let Some(value) = self.db.get_cf(cf, key)? else {
200 return Ok(None);
201 };
202
203 Yoke::try_attach_to_cart(Box::from(value), |data| bincode::deserialize(data))
204 .map(Some)
205 .context("Failed to deserialize commit")
206 }
207
208 pub fn fetch_latest(
209 &self,
210 amount: u64,
211 offset: u64,
212 ) -> Result<Vec<YokedCommit>, anyhow::Error> {
213 let cf = self
214 .db
215 .cf_handle(COMMIT_FAMILY)
216 .context("missing column family")?;
217
218 let latest_commit_id = self.len()?;
219 debug!("Searching from latest commit {latest_commit_id}");
220
221 let mut start_key = self.prefix.to_vec();
222 start_key.extend_from_slice(
223 &latest_commit_id
224 .saturating_sub(offset)
225 .saturating_sub(amount)
226 .to_be_bytes(),
227 );
228
229 let mut end_key = self.prefix.to_vec();
230 end_key.extend_from_slice(&(latest_commit_id.saturating_sub(offset)).to_be_bytes());
231
232 let mut opts = ReadOptions::default();
233 opts.set_iterate_range(start_key.as_slice()..end_key.as_slice());
234
235 opts.set_prefix_same_as_start(true);
236
237 self.db
238 .iterator_cf_opt(cf, opts, IteratorMode::End)
239 .map(|v| {
240 Yoke::try_attach_to_cart(v.context("failed to read commit")?.1, |data| {
241 bincode::deserialize(data).context("failed to deserialize")
242 })
243 })
244 .collect::<Result<Vec<_>, anyhow::Error>>()
245 }
246}