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