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