use std::sync::Arc; use anyhow::Context; use gix::{actor::SignatureRef, ObjectId}; use rkyv::{Archive, Serialize}; use rocksdb::{IteratorMode, ReadOptions, WriteBatch}; use time::{OffsetDateTime, UtcOffset}; use tracing::debug; use yoke::{Yoke, Yokeable}; use crate::database::schema::{ prefixes::{COMMIT_COUNT_FAMILY, COMMIT_FAMILY}, repository::RepositoryId, Yoked, }; #[derive(Serialize, Archive, Debug, Yokeable)] pub struct Commit { pub summary: String, pub message: String, pub author: Author, pub committer: Author, pub hash: [u8; 20], } impl Commit { pub fn new( commit: &gix::Commit<'_>, author: SignatureRef<'_>, committer: SignatureRef<'_>, ) -> Result { let message = commit.message()?; Ok(Self { summary: message.summary().to_string(), message: message.body.map(ToString::to_string).unwrap_or_default(), committer: committer.try_into()?, author: author.try_into()?, hash: match commit.id().detach() { ObjectId::Sha1(d) => d, }, }) } pub fn insert(&self, tree: &CommitTree, id: u64, tx: &mut WriteBatch) -> anyhow::Result<()> { tree.insert(id, self, tx) } } #[derive(Serialize, Archive, Debug)] pub struct Author { pub name: String, pub email: String, pub time: (i64, i32), } impl ArchivedAuthor { pub fn time(&self) -> OffsetDateTime { OffsetDateTime::from_unix_timestamp(self.time.0.to_native()) .unwrap() .to_offset(UtcOffset::from_whole_seconds(self.time.1.to_native()).unwrap()) } } impl TryFrom> for Author { type Error = anyhow::Error; fn try_from(author: SignatureRef<'_>) -> Result { Ok(Self { name: author.name.to_string(), email: author.email.to_string(), time: (author.time.seconds, author.time.offset), }) } } pub struct CommitTree { db: Arc, pub prefix: Box<[u8]>, } pub type YokedCommit = Yoked<&'static ::Archived>; impl CommitTree { pub(super) fn new(db: Arc, repository: RepositoryId, reference: &str) -> Self { let mut prefix = Vec::with_capacity(std::mem::size_of::() + reference.len() + 1); prefix.extend_from_slice(&repository.to_be_bytes()); prefix.extend_from_slice(reference.as_bytes()); prefix.push(b'\0'); Self { db, prefix: prefix.into_boxed_slice(), } } pub fn drop_commits(&self) -> anyhow::Result<()> { let mut to = self.prefix.clone(); *to.last_mut().unwrap() += 1; let commit_cf = self .db .cf_handle(COMMIT_FAMILY) .context("commit column family missing")?; self.db.delete_range_cf(commit_cf, &self.prefix, &to)?; let commit_count_cf = self .db .cf_handle(COMMIT_COUNT_FAMILY) .context("missing column family")?; self.db.delete_cf(commit_count_cf, &self.prefix)?; Ok(()) } pub fn update_counter(&self, count: u64, tx: &mut WriteBatch) -> anyhow::Result<()> { let cf = self .db .cf_handle(COMMIT_COUNT_FAMILY) .context("missing column family")?; tx.put_cf(cf, &self.prefix, count.to_be_bytes()); Ok(()) } pub fn len(&self) -> anyhow::Result { let cf = self .db .cf_handle(COMMIT_COUNT_FAMILY) .context("missing column family")?; let Some(res) = self.db.get_pinned_cf(cf, &self.prefix)? else { return Ok(0); }; let out: [u8; std::mem::size_of::()] = res.as_ref().try_into()?; Ok(u64::from_be_bytes(out)) } fn insert(&self, id: u64, commit: &Commit, tx: &mut WriteBatch) -> anyhow::Result<()> { let cf = self .db .cf_handle(COMMIT_FAMILY) .context("missing column family")?; let mut key = self.prefix.to_vec(); key.extend_from_slice(&id.to_be_bytes()); tx.put_cf(cf, key, rkyv::to_bytes::(commit)?); Ok(()) } pub fn fetch_latest_one(&self) -> Result, anyhow::Error> { let mut key = self.prefix.to_vec(); key.extend_from_slice(&(self.len()?.saturating_sub(1)).to_be_bytes()); let cf = self .db .cf_handle(COMMIT_FAMILY) .context("missing column family")?; let Some(value) = self.db.get_cf(cf, key)? else { return Ok(None); }; Yoke::try_attach_to_cart(Box::from(value), |value| { rkyv::access::<_, rkyv::rancor::Error>(&value) }) .context("Failed to deserialize commit") .map(Some) } pub fn fetch_latest( &self, amount: u64, offset: u64, ) -> Result, anyhow::Error> { let cf = self .db .cf_handle(COMMIT_FAMILY) .context("missing column family")?; let latest_commit_id = self.len()?; debug!("Searching from latest commit {latest_commit_id}"); let mut start_key = self.prefix.to_vec(); start_key.extend_from_slice( &latest_commit_id .saturating_sub(offset) .saturating_sub(amount) .to_be_bytes(), ); let mut end_key = self.prefix.to_vec(); end_key.extend_from_slice(&(latest_commit_id.saturating_sub(offset)).to_be_bytes()); let mut opts = ReadOptions::default(); opts.set_iterate_range(start_key.as_slice()..end_key.as_slice()); opts.set_prefix_same_as_start(true); self.db .iterator_cf_opt(cf, opts, IteratorMode::End) .map(|v| { Yoke::try_attach_to_cart(v.context("failed to read commit")?.1, |data| { rkyv::access::<_, rkyv::rancor::Error>(data).context("failed to deserialize") }) }) .collect::, anyhow::Error>>() } }