1use std::sync::Arc;
2
3use anyhow::Context;
4use gix::{ObjectId, actor::SignatureRef, objs::CommitRef};
5use rkyv::{Archive, Serialize};
6use rocksdb::{IteratorMode, ReadOptions, WriteBatch};
7use time::{OffsetDateTime, UtcOffset};
8use tracing::debug;
9use yoke::{Yoke, Yokeable};
10
11use crate::database::schema::{
12 Yoked,
13 prefixes::{COMMIT_COUNT_FAMILY, COMMIT_FAMILY},
14 repository::RepositoryId,
15};
16
17#[derive(Serialize, Archive, Debug, Yokeable)]
18pub struct Commit {
19 pub summary: String,
20 pub message: String,
21 pub author: Author,
22 pub committer: Author,
23 pub hash: [u8; 20],
24 pub tree: u64,
25}
26
27impl Commit {
28 pub fn new(
29 oid: ObjectId,
30 commit: &CommitRef<'_>,
31 author: SignatureRef<'_>,
32 committer: SignatureRef<'_>,
33 tree: u64,
34 ) -> Result<Self, anyhow::Error> {
35 let message = commit.message();
36
37 Ok(Self {
38 summary: message.summary().to_string(),
39 message: message.body.map(ToString::to_string).unwrap_or_default(),
40 committer: committer.try_into()?,
41 author: author.try_into()?,
42 hash: match oid {
43 ObjectId::Sha1(d) => d,
44 },
45 tree,
46 })
47 }
48
49 pub fn insert(&self, tree: &CommitTree, id: u64, tx: &mut WriteBatch) -> anyhow::Result<()> {
50 tree.insert(id, self, tx)
51 }
52}
53
54#[derive(Serialize, Archive, Debug)]
55pub struct Author {
56 pub name: String,
57 pub email: String,
58 pub time: (i64, i32),
59}
60
61impl ArchivedAuthor {
62 pub fn time(&self) -> OffsetDateTime {
63 OffsetDateTime::from_unix_timestamp(self.time.0.to_native())
64 .unwrap()
65 .to_offset(UtcOffset::from_whole_seconds(self.time.1.to_native()).unwrap())
66 }
67}
68
69impl TryFrom<SignatureRef<'_>> for Author {
70 type Error = anyhow::Error;
71
72 fn try_from(author: SignatureRef<'_>) -> Result<Self, anyhow::Error> {
73 Ok(Self {
74 name: author.name.to_string(),
75 email: author.email.to_string(),
76 time: (author.time.seconds, author.time.offset),
77 })
78 }
79}
80
81pub struct CommitTree {
82 db: Arc<rocksdb::DB>,
83 pub prefix: Box<[u8]>,
84}
85
86pub type YokedCommit = Yoked<&'static <Commit as Archive>::Archived>;
87
88impl CommitTree {
89 pub(super) fn new(db: Arc<rocksdb::DB>, repository: RepositoryId, reference: &str) -> Self {
90 let mut prefix = Vec::with_capacity(std::mem::size_of::<u64>() + reference.len() + 1);
91 prefix.extend_from_slice(&repository.to_be_bytes());
92 prefix.extend_from_slice(reference.as_bytes());
93 prefix.push(b'\0');
94
95 Self {
96 db,
97 prefix: prefix.into_boxed_slice(),
98 }
99 }
100
101 pub fn drop_commits(&self) -> anyhow::Result<()> {
102 let mut to = self.prefix.clone();
103 *to.last_mut().unwrap() += 1;
104
105 let commit_cf = self
106 .db
107 .cf_handle(COMMIT_FAMILY)
108 .context("commit column family missing")?;
109 self.db.delete_range_cf(commit_cf, &self.prefix, &to)?;
110
111 let commit_count_cf = self
112 .db
113 .cf_handle(COMMIT_COUNT_FAMILY)
114 .context("missing column family")?;
115 self.db.delete_cf(commit_count_cf, &self.prefix)?;
116
117 Ok(())
118 }
119
120 pub fn update_counter(&self, count: u64, tx: &mut WriteBatch) -> anyhow::Result<()> {
121 let cf = self
122 .db
123 .cf_handle(COMMIT_COUNT_FAMILY)
124 .context("missing column family")?;
125
126 tx.put_cf(cf, &self.prefix, count.to_be_bytes());
127
128 Ok(())
129 }
130
131 pub fn len(&self) -> anyhow::Result<u64> {
132 let cf = self
133 .db
134 .cf_handle(COMMIT_COUNT_FAMILY)
135 .context("missing column family")?;
136
137 let Some(res) = self.db.get_pinned_cf(cf, &self.prefix)? else {
138 return Ok(0);
139 };
140
141 let out: [u8; std::mem::size_of::<u64>()] = res.as_ref().try_into()?;
142 Ok(u64::from_be_bytes(out))
143 }
144
145 fn insert(&self, id: u64, commit: &Commit, tx: &mut WriteBatch) -> anyhow::Result<()> {
146 let cf = self
147 .db
148 .cf_handle(COMMIT_FAMILY)
149 .context("missing column family")?;
150
151 let mut key = self.prefix.to_vec();
152 key.extend_from_slice(&id.to_be_bytes());
153
154 tx.put_cf(cf, key, rkyv::to_bytes::<rkyv::rancor::Error>(commit)?);
155
156 Ok(())
157 }
158
159 pub fn fetch_latest_one(&self) -> Result<Option<YokedCommit>, anyhow::Error> {
160 let mut key = self.prefix.to_vec();
161 key.extend_from_slice(&(self.len()?.saturating_sub(1)).to_be_bytes());
162
163 let cf = self
164 .db
165 .cf_handle(COMMIT_FAMILY)
166 .context("missing column family")?;
167
168 let Some(value) = self.db.get_cf(cf, key)? else {
169 return Ok(None);
170 };
171
172 Yoke::try_attach_to_cart(Box::from(value), |value| {
173 rkyv::access::<_, rkyv::rancor::Error>(value)
174 })
175 .context("Failed to deserialize commit")
176 .map(Some)
177 }
178
179 pub fn fetch_latest(
180 &self,
181 amount: u64,
182 offset: u64,
183 ) -> Result<Vec<YokedCommit>, anyhow::Error> {
184 let cf = self
185 .db
186 .cf_handle(COMMIT_FAMILY)
187 .context("missing column family")?;
188
189 let latest_commit_id = self.len()?;
190 debug!("Searching from latest commit {latest_commit_id}");
191
192 let mut start_key = self.prefix.to_vec();
193 start_key.extend_from_slice(
194 &latest_commit_id
195 .saturating_sub(offset)
196 .saturating_sub(amount)
197 .to_be_bytes(),
198 );
199
200 let mut end_key = self.prefix.to_vec();
201 end_key.extend_from_slice(&(latest_commit_id.saturating_sub(offset)).to_be_bytes());
202
203 let mut opts = ReadOptions::default();
204 opts.set_iterate_range(start_key.as_slice()..end_key.as_slice());
205
206 opts.set_prefix_same_as_start(true);
207
208 self.db
209 .iterator_cf_opt(cf, opts, IteratorMode::End)
210 .map(|v| {
211 Yoke::try_attach_to_cart(v.context("failed to read commit")?.1, |data| {
212 rkyv::access::<_, rkyv::rancor::Error>(data).context("failed to deserialize")
213 })
214 })
215 .collect::<Result<Vec<_>, anyhow::Error>>()
216 }
217}