gitscope
← rgit / 0.1.3 / src/database/schema/commit.rs
https://dev.kronoz.win/r/rgit.git
src/database/schema/commit.rs · Rust
Blame Raw
  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}