diff --git a/Cargo.toml b/Cargo.toml index b48773e..5716e55 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "git-ref-format", + "git-storage", "git-trailers", "link-git", "radicle-git-ext", diff --git a/git-storage/Cargo.toml b/git-storage/Cargo.toml new file mode 100644 index 0000000..1eef56b --- /dev/null +++ b/git-storage/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "git-storage" +version = "0.1.0" +authors = ["Kim Altintop ", "Fintan Halpenny "] +edition = "2021" +license = "GPL-3.0-or-later" + +[dependencies] +async-trait = "0.1" +globset = "0.4" +libc = "0.2" +parking_lot = "0.12" +thiserror = "1" +either = "1.8.0" + +[dependencies.deadpool] +version = "0.7" +default-features = false +features = ["managed"] + +[dependencies.git2] +version = "0.15.0" +default-features = false +features = ["vendored-libgit2"] + +[dependencies.git-ref-format] +path = "../git-ref-format" + +[dependencies.libgit2-sys] +version = ">= 0.12.24" +default-features = false +features = ["vendored"] + +[dependencies.radicle-git-ext] +path = "../radicle-git-ext" + +[dependencies.radicle-std-ext] +path = "../radicle-std-ext" diff --git a/git-storage/src/backend.rs b/git-storage/src/backend.rs new file mode 100644 index 0000000..01c7b45 --- /dev/null +++ b/git-storage/src/backend.rs @@ -0,0 +1,5 @@ +// Copyright © 2022 The Radicle Link Contributors +// SPDX-License-Identifier: GPL-3.0-or-later + +pub mod read; +pub mod write; diff --git a/git-storage/src/backend/read.rs b/git-storage/src/backend/read.rs new file mode 100644 index 0000000..9132981 --- /dev/null +++ b/git-storage/src/backend/read.rs @@ -0,0 +1,160 @@ +// Copyright © 2019-2020 The Radicle Foundation +// +// This file is part of radicle-link, distributed under the GPLv3 with Radicle +// Linking Exception. For full terms see the included LICENSE file. + +use std::path::Path; + +use git_ext::{error::is_not_found_err, Oid}; +use std_ext::result::ResultExt as _; + +use crate::{ + glob, + odb::{self, Blob, Commit, Object, Tag, Tree}, + refdb::{self, Reference, References, ReferencesGlob}, +}; + +/// A read-only storage backend for accessing git's odb and refdb. +/// +/// For access to the odb see [`odb::Read`]. +/// For access to the refdb see [`refdb::Read`]. +/// +/// To construct the `Read` storage use [`Read::open`]. +pub struct Read { + pub(super) raw: git2::Repository, +} + +impl Read { + /// Open the [`Read`] storage using the `path` provided. + /// + /// The storage **must** exist already. To initialise the storage for the + /// first time see [`crate::Write::open`]. + /// + /// # Concurrency + /// + /// [`Read`] can be sent between threads, but it can't be shared between + /// threads. _Some_ operations are safe to perform concurrently in much + /// the same way two `git` processes can access the same repository. + /// However, if you need multiple [`Read`]s to be shared between + /// threads, use a [`crate::Pool`] instead. + pub fn open>(path: P) -> Result { + crate::init(); + + let raw = git2::Repository::open(path)?; + + Ok(Self { raw }) + } +} + +pub mod error { + use thiserror::Error; + + use crate::refdb::error::ParseReference; + + #[derive(Debug, Error)] + pub enum Init { + #[error(transparent)] + Git(#[from] git2::Error), + } + + #[derive(Debug, Error)] + pub enum Identifier { + #[error(transparent)] + Git(#[from] git2::Error), + } + + #[derive(Debug, Error)] + pub enum FindRef { + #[error(transparent)] + Git(#[from] git2::Error), + #[error(transparent)] + Parse(#[from] ParseReference), + } +} + +// impls + +impl<'a> refdb::Read for &'a Read { + type FindRef = error::FindRef; + type FindRefs = refdb::error::Iter; + type FindRefOid = git2::Error; + + type References = References<'a>; + + fn find_reference(&self, reference: Ref) -> Result, Self::FindRef> + where + Ref: AsRef, + { + let reference = self + .raw + .find_reference(reference.as_ref().as_str()) + .map(Some) + .or_matches::(is_not_found_err, || Ok(None))?; + Ok(reference.map(Reference::try_from).transpose()?) + } + + fn find_references(&self, reference: Pat) -> Result + where + Pat: AsRef, + { + Ok(References { + inner: ReferencesGlob { + iter: self.raw.references()?, + glob: glob::RefspecMatcher::from(reference.as_ref().to_owned()), + }, + }) + } + + fn find_reference_oid(&self, reference: Ref) -> Result, Self::FindRefOid> + where + Ref: AsRef, + { + self.raw + .refname_to_id(reference.as_ref().as_str()) + .map(|oid| Some(Oid::from(oid))) + .or_matches(is_not_found_err, || Ok(None)) + } +} + +impl odb::Read for Read { + type FindObj = git2::Error; + type FindBlob = git2::Error; + type FindCommit = git2::Error; + type FindTag = git2::Error; + type FindTree = git2::Error; + + fn find_object(&self, oid: Oid) -> Result, Self::FindObj> { + self.raw + .find_object(oid.into(), None) + .map(Some) + .or_matches::(is_not_found_err, || Ok(None)) + } + + fn find_blob(&self, oid: Oid) -> Result, Self::FindBlob> { + self.raw + .find_blob(oid.into()) + .map(Some) + .or_matches(is_not_found_err, || Ok(None)) + } + + fn find_commit(&self, oid: Oid) -> Result, Self::FindCommit> { + self.raw + .find_commit(oid.into()) + .map(Some) + .or_matches(is_not_found_err, || Ok(None)) + } + + fn find_tag(&self, oid: Oid) -> Result, Self::FindTag> { + self.raw + .find_tag(oid.into()) + .map(Some) + .or_matches(is_not_found_err, || Ok(None)) + } + + fn find_tree(&self, oid: Oid) -> Result, Self::FindTree> { + self.raw + .find_tree(oid.into()) + .map(Some) + .or_matches(is_not_found_err, || Ok(None)) + } +} diff --git a/git-storage/src/backend/write.rs b/git-storage/src/backend/write.rs new file mode 100644 index 0000000..c05d4e3 --- /dev/null +++ b/git-storage/src/backend/write.rs @@ -0,0 +1,574 @@ +// Copyright © 2019-2020 The Radicle Foundation +// +// This file is part of radicle-link, distributed under the GPLv3 with Radicle +// Linking Exception. For full terms see the included LICENSE file. + +use std::path::Path; + +use either::Either; + +use git_ext::{error::is_not_found_err, Oid}; +use git_ref_format::{Qualified, RefStr, RefString}; + +use crate::{ + odb, + refdb::{ + self, + resolve, + write::{previous, Applied, Policy, SymrefTarget, Update, Updated}, + Read as _, + Reference, + Target, + }, + signature::UserInfo, +}; + +use super::read::{self, Read}; + +pub mod error; + +/// A read-write storage backend for accessing git's odb and refdb. +/// +/// For read-only access to the odb see [`odb::Read`]. +/// For write access to the odb see [`odb::Write`]. +/// +/// For read-only access to the refdb see [`refdb::Read`]. +/// For write access to the refdb see [`refdb::Write`]. +/// +/// To construct the `Write` storage use [`Read::open`]. +pub struct Write { + inner: Read, + info: UserInfo, +} + +impl Write { + /// Open the [`Write`] storage, initialising it if it doesn't exist. + /// + /// # Concurrency + /// + /// [`Write`] can be sent between threads, but it can't be shared between + /// threads. _Some_ operations are safe to perform concurrently in much + /// the same way two `git` processes can access the same repository. + /// However, if you need multiple [`Write`]s to be shared between + /// threads, use a [`crate::Pool`] instead. + pub fn open>(path: P, info: UserInfo) -> Result { + crate::init(); + + let path = path.as_ref(); + let raw = match git2::Repository::open_bare(path) { + Err(e) if is_not_found_err(&e) => { + let backend = git2::Repository::init_opts( + path, + git2::RepositoryInitOptions::new() + .bare(true) + .no_reinit(true) + .external_template(false), + )?; + Ok(backend) + }, + Ok(repo) => Ok(repo), + Err(e) => Err(e), + }?; + + Ok(Self { + inner: Read { raw }, + info, + }) + } +} + +impl Write { + /// Return a read-only handle of the storage. + pub fn read_only(&self) -> &Read { + &self.inner + } + + /// Return the [`UserInfo`] of the storage. + pub fn info(&self) -> &UserInfo { + &self.info + } + + fn as_raw(&self) -> &git2::Repository { + &self.inner.raw + } +} + +// refdb impls + +impl<'a> refdb::Read for &'a Write { + type FindRef = <&'a Read as refdb::Read>::FindRef; + type FindRefs = <&'a Read as refdb::Read>::FindRefs; + type FindRefOid = <&'a Read as refdb::Read>::FindRefOid; + + type References = <&'a Read as refdb::Read>::References; + + fn find_reference(&self, reference: Ref) -> Result, Self::FindRef> + where + Ref: AsRef, + { + self.read_only().find_reference(reference) + } + + fn find_references(&self, reference: Pat) -> Result + where + Pat: AsRef, + { + self.read_only().find_references(reference) + } + + fn find_reference_oid(&self, reference: Ref) -> Result, Self::FindRefOid> + where + Ref: AsRef, + { + self.read_only().find_reference_oid(reference) + } +} + +impl<'a> refdb::Write for &'a Write { + type UpdateError = error::Update; + + fn update<'b, U>(&mut self, updates: U) -> Result, Self::UpdateError> + where + U: IntoIterator>, + { + let mut refdb = Transaction::new(self)?; + let mut applied = Applied::default(); + for up in updates.into_iter() { + match up { + Update::Direct { + name, + target, + no_ff, + previous, + reflog, + } => match refdb.direct(name, target, no_ff, previous, reflog)? { + Either::Left(update) => applied.rejected.push(update), + Either::Right(updated) => applied.updated.push(updated), + }, + Update::Symbolic { + name, + target, + type_change, + previous, + reflog, + } => match refdb.symbolic(name, target, type_change, previous, reflog)? { + Either::Left(update) => applied.rejected.push(update), + Either::Right(updated) => applied.updated.extend(updated), + }, + Update::Remove { name, previous } => match refdb.remove(name, previous)? { + Either::Left(update) => applied.rejected.push(update), + Either::Right(updated) => applied.updated.push(updated), + }, + } + } + refdb.commit()?; + + Ok(applied) + } +} + +/// An internal struct combining a [`Write`] and [`git2::Transaction`]. +// TODO: include optional namespace +struct Transaction<'a> { + refdb: &'a Write, + txn: git2::Transaction<'a>, +} + +impl<'a> Transaction<'a> { + pub fn new(refdb: &'a Write) -> Result { + let txn = refdb.inner.raw.transaction()?; + Ok(Self { refdb, txn }) + } + + /// Perform an [`Update::Direct`] within the [`Transaction`]. + /// + /// Steps: + /// 1. Get the state of the reference + /// a. if it did not exist create the source and destination references, + /// skip the next steps. + /// b. if it did, then follow the next steps. + /// + /// 2. Guard against the [`previous::Edit`] value, if this fails then reject + /// the [`Update`]. + /// + /// 3. Check the fast-forward policy, either aborting, + /// rejecting, or accepting the update + pub fn direct<'b>( + &mut self, + name: Qualified<'b>, + target: Oid, + no_ff: Policy, + previous: previous::Edit, + reflog: String, + ) -> Result, Updated>, error::Update> { + let prev = self.refdb.find_reference(&name)?; + let given = prev + .as_ref() + .map(|prev| resolve(self.refdb.as_raw(), prev)) + .transpose()?; + if let Err(_err) = previous.guard(given) { + return Ok(Either::Left(Update::Direct { + name, + target, + no_ff, + previous, + reflog, + })); + } + + let not_ff = match given { + Some(prev) => { + if !self.is_ff(&name, target, prev)? { + Some(prev) + } else { + None + } + }, + None => None, + }; + + match not_ff { + // It wasn't an fast-forward so we check our policy + Some(cur) => match no_ff { + Policy::Abort => Err(error::Update::NonFF { + name: name.into(), + new: target, + cur, + }), + Policy::Reject => Ok(Either::Left(Update::Direct { + name, + target, + no_ff, + previous, + reflog, + })), + Policy::Allow => Ok(Either::Right( + self.direct_edit(&name, target, given, &reflog)?, + )), + }, + // It was an fast-forward so we go ahead and make the edit + None => Ok(Either::Right( + self.direct_edit(&name, target, given, &reflog)?, + )), + } + } + + /// Perform an [`Update::Symbolic`] within the [`Transaction`]. + /// + /// Steps: + /// 1. Get the state of the source reference + /// a. if it did not exist create the source and destination references, + /// skip the next steps. + /// b. if it did, then follow the next steps. + /// + /// 2. Guard against the `type_change`, aborting or rejecting depending on + /// the [`Policy`]. + /// + /// 3. Get the state of the destination reference. + /// + /// 4. Check the target of the desitination: + /// a. if it's direct then make the edit depending on the fast-forward + /// status. + /// b. if it's symbolic then this is an error. + pub fn symbolic<'b: 'a>( + &mut self, + name: Qualified<'b>, + target: SymrefTarget<'b>, + type_change: Policy, + previous: previous::Edit, + reflog: String, + ) -> Result, Vec>, error::Update> { + let src = self.refdb.find_reference(&name)?; + let prev = src + .as_ref() + .map(|src| refdb::resolve(self.refdb.as_raw(), src)) + .transpose()?; + match src { + Some(src) => match src.target { + Target::Direct { .. } if matches!(type_change, Policy::Abort) => { + Err(error::Update::TypeChange(name.into())) + }, + Target::Direct { .. } if matches!(type_change, Policy::Reject) => { + Ok(Either::Left(Update::Symbolic { + name, + target, + type_change, + previous, + reflog, + })) + }, + _ => { + let dst = self.refdb.find_reference(&target.name)?; + match dst { + Some(dst) => match dst.target { + Target::Direct { oid: dst } => { + let is_ff = target.target != dst + && self.is_ff(&target.name, target.target, dst)?; + Ok(Either::Right( + self.symbolic_edit(name, target, prev, &reflog, is_ff)?, + )) + }, + Target::Symbolic { .. } => Err(error::Update::TargetSymbolic(dst.name)), + }, + None => Ok(Either::Right( + self.symbolic_edit(name, target, prev, &reflog, true)?, + )), + } + }, + }, + None => Ok(Either::Right( + self.symbolic_edit(name, target, prev, &reflog, true)?, + )), + } + } + + /// Perform an [`Update::Remove`] within the [`Transaction`]. + /// + /// Steps: + /// 1. Get the state of the reference + /// + /// 2. Guard against the [`previous::Remove`] value, if this fails then + /// reject the [`Update`]. + /// + /// 3. Remove the reference + /// + /// # Panics + /// + /// The `previous` SHOULD guard against the reference not existing, so this + /// will panic if the previous reference was missing AND passed the + /// `previous::guard`. + pub fn remove<'b>( + &mut self, + name: Qualified<'b>, + previous: previous::Remove, + ) -> Result, Updated>, error::Update> { + let prev = self.refdb.find_reference(&name)?; + let given = prev + .as_ref() + .map(|prev| resolve(self.refdb.as_raw(), prev)) + .transpose()?; + if let Err(_err) = previous.guard(given) { + Ok(Either::Left(Update::Remove { name, previous })) + } else { + match given { + None => { + panic!("BUG: the previous value for a reference to be removed was not given, but its existence SHOULD be guarded") + }, + Some(previous) => Ok(Either::Right(self.remove_edit(name, previous)?)), + } + } + } + + pub fn lock(&mut self, reference: R) -> Result<(), error::Transaction> + where + R: AsRef, + { + let reference = reference.as_ref(); + self.txn + .lock_ref(reference.as_str()) + .map_err(|err| error::Transaction::Lock { + reference: reference.to_owned(), + source: err, + }) + } + + pub fn commit(self) -> Result<(), error::Transaction> { + self.txn + .commit() + .map_err(|err| error::Transaction::Commit { source: err }) + } + + pub fn direct_edit( + &mut self, + reference: R, + target: Oid, + prev: Option, + reflog: &str, + ) -> Result + where + R: AsRef, + { + let reference = reference.as_ref(); + self.lock(reference)?; + let info = self.refdb.info(); + let sig = info + .signature() + .map_err(|err| error::Transaction::Signature { + name: info.name.to_owned(), + email: info.email.to_owned(), + source: err, + })?; + self.txn + .set_target(reference.as_str(), target.into(), Some(&sig), reflog) + .map_err(|err| error::Transaction::SetDirect { + reference: reference.to_owned(), + target, + source: err, + })?; + + Ok(Updated::Direct { + name: reference.to_owned(), + target, + previous: prev, + }) + } + + pub fn symbolic_edit( + &mut self, + reference: R, + target: SymrefTarget<'a>, + prev: Option, + reflog: &str, + is_ff: bool, + ) -> Result, error::Transaction> + where + R: AsRef, + { + let reference = reference.as_ref(); + self.lock(reference)?; + self.lock(&target.name)?; + + let SymrefTarget { + name: dst, + target: dst_target, + } = target; + + let mut edits = Vec::with_capacity(2); + let info = self.refdb.info(); + let sig = info + .signature() + .map_err(|err| error::Transaction::Signature { + name: info.name.to_owned(), + email: info.email.to_owned(), + source: err, + })?; + if is_ff { + let direct = self.direct_edit(&dst, dst_target, prev, reflog)?; + edits.push(direct); + } + + self.txn + .set_symbolic_target(reference.as_str(), dst.as_str(), Some(&sig), reflog) + .map_err(|err| error::Transaction::SetSymbolic { + reference: reference.to_owned(), + target: dst.clone().into(), + source: err, + })?; + edits.push(Updated::Symbolic { + name: reference.to_owned(), + target: dst.into(), + previous: prev, + }); + Ok(edits) + } + + pub fn remove_edit( + &mut self, + reference: R, + previous: Oid, + ) -> Result + where + R: AsRef, + { + let reference = reference.as_ref(); + self.lock(reference)?; + self.txn + .remove(reference.as_str()) + .map_err(|err| error::Transaction::Remove { + reference: reference.to_owned(), + source: err, + })?; + Ok(Updated::Removed { + name: reference.to_owned(), + previous, + }) + } + + fn is_ff(&self, name: R, target: Oid, prev: Oid) -> Result + where + R: AsRef, + { + self.refdb + .inner + .raw + .graph_descendant_of(target.into(), prev.into()) + .map_err(|err| error::Transaction::Ancestry { + name: name.as_ref().to_owned(), + new: target, + old: prev, + source: err, + }) + } +} + +// odb impls + +impl odb::Read for Write { + type FindObj = ::FindObj; + type FindBlob = ::FindBlob; + type FindCommit = ::FindCommit; + type FindTag = ::FindTag; + type FindTree = ::FindTree; + + fn find_object(&self, oid: Oid) -> Result, Self::FindObj> { + self.read_only().find_object(oid) + } + + fn find_blob(&self, oid: Oid) -> Result, Self::FindBlob> { + self.read_only().find_blob(oid) + } + + fn find_commit(&self, oid: Oid) -> Result, Self::FindCommit> { + self.read_only().find_commit(oid) + } + + fn find_tag(&self, oid: Oid) -> Result, Self::FindTag> { + self.read_only().find_tag(oid) + } + + fn find_tree(&self, oid: Oid) -> Result, Self::FindTree> { + self.read_only().find_tree(oid) + } +} + +impl odb::Write for Write { + type WriteBlob = git2::Error; + type WriteCommit = git2::Error; + type WriteTag = git2::Error; + type WriteTree = git2::Error; + + fn write_blob(&self, data: &[u8]) -> Result { + self.as_raw().blob(data).map(Oid::from) + } + + fn write_commit<'b>( + &self, + tree: &odb::Tree, + parents: &[&odb::Commit<'b>], + message: &str, + ) -> Result { + let author = self.info.signature()?; + self.as_raw() + .commit(None, &author, &author, message, tree, parents) + .map(Oid::from) + } + + fn write_tag( + &self, + name: R, + target: &odb::Object, + message: &str, + ) -> Result + where + R: AsRef, + { + let tagger = self.info.signature()?; + self.as_raw() + .tag_annotation_create(name.as_ref().as_str(), target, &tagger, message) + .map(Oid::from) + } + + fn write_tree(&self, builder: git2::TreeBuilder) -> Result { + builder.write().map(Oid::from) + } +} diff --git a/git-storage/src/backend/write/error.rs b/git-storage/src/backend/write/error.rs new file mode 100644 index 0000000..514c3b8 --- /dev/null +++ b/git-storage/src/backend/write/error.rs @@ -0,0 +1,80 @@ +// Copyright © 2019-2020 The Radicle Foundation +// +// This file is part of radicle-link, distributed under the GPLv3 with Radicle +// Linking Exception. For full terms see the included LICENSE file. + +use thiserror::Error; + +use super::*; + +#[derive(Debug, Error)] +pub enum Init { + #[error(transparent)] + Git(#[from] git2::Error), +} + +#[derive(Debug, Error)] +pub enum Transaction { + #[error("error determining if {old} is an ancestor of {new} in within {name}")] + Ancestry { + name: RefString, + new: Oid, + old: Oid, + #[source] + source: git2::Error, + }, + #[error("error committing update for storage")] + Commit { + #[source] + source: git2::Error, + }, + #[error("error locking reference '{reference}' when attempting to update storage")] + Lock { + reference: RefString, + #[source] + source: git2::Error, + }, + #[error("error obtaining signature for '{name}' '{email}'")] + Signature { + name: String, + email: String, + #[source] + source: git2::Error, + }, + #[error("error setting the direct reference '{reference}' to the target '{target}'")] + SetDirect { + reference: RefString, + target: Oid, + #[source] + source: git2::Error, + }, + #[error("error setting the symbolic reference '{reference}' to the target '{target}'")] + SetSymbolic { + reference: RefString, + target: RefString, + #[source] + source: git2::Error, + }, + #[error("error removing the reference '{reference}'")] + Remove { + reference: RefString, + #[source] + source: git2::Error, + }, +} + +#[derive(Debug, Error)] +pub enum Update { + #[error("non-fast-forward update of {name} (current: {cur}, new: {new})")] + NonFF { name: RefString, new: Oid, cur: Oid }, + #[error(transparent)] + FindRef(#[from] read::error::FindRef), + #[error(transparent)] + Git(#[from] git2::Error), + #[error("the symref target {0} is itself a symref")] + TargetSymbolic(RefString), + #[error(transparent)] + Transaction(#[from] Transaction), + #[error("rejected changing type of reference '{0}' from symbolic to direct")] + TypeChange(RefString), +} diff --git a/git-storage/src/glob.rs b/git-storage/src/glob.rs new file mode 100644 index 0000000..8989417 --- /dev/null +++ b/git-storage/src/glob.rs @@ -0,0 +1,46 @@ +// Copyright © 2019-2020 The Radicle Foundation +// +// This file is part of radicle-link, distributed under the GPLv3 with Radicle +// Linking Exception. For full terms see the included LICENSE file. + +use std::path::Path; + +use git_ext as ext; +use git_ref_format::refspec::PatternString; + +pub trait Pattern { + fn matches>(&self, path: P) -> bool; +} + +impl Pattern for globset::GlobMatcher { + fn matches>(&self, path: P) -> bool { + self.is_match(path) + } +} + +impl Pattern for globset::GlobSet { + fn matches>(&self, path: P) -> bool { + self.is_match(path) + } +} + +#[derive(Clone, Debug)] +pub struct RefspecMatcher(globset::GlobMatcher); + +impl From for RefspecMatcher { + fn from(pat: ext::RefspecPattern) -> Self { + Self(globset::Glob::new(pat.as_str()).unwrap().compile_matcher()) + } +} + +impl From for RefspecMatcher { + fn from(pat: PatternString) -> Self { + Self(globset::Glob::new(pat.as_str()).unwrap().compile_matcher()) + } +} + +impl Pattern for RefspecMatcher { + fn matches>(&self, path: P) -> bool { + self.0.is_match(path) + } +} diff --git a/git-storage/src/lib.rs b/git-storage/src/lib.rs new file mode 100644 index 0000000..e062d50 --- /dev/null +++ b/git-storage/src/lib.rs @@ -0,0 +1,85 @@ +// Copyright © 2022 The Radicle Link Contributors +// SPDX-License-Identifier: GPL-3.0-or-later + +//! # `git-storage` +//! +//! This crate provides access to git [references][refs] and [objects][objs]. +//! +//! To first initialise the storage use [`Write::open`]. +//! +//! After the storage is initialised, use [`Write::open`] or [`Read::open`] for +//! read-write or read-only access to the underlying storage. These structs will +//! implement the traits below depending on their access levels. +//! +//! ## Read-only access +//! +//! * [`refdb::Read`] +//! * [`odb::Read`] +//! +//! ## Read-write access +//! +//! * [`refdb::Read`] +//! * [`refdb::Write`] +//! * [`odb::Read`] +//! * [`odb::Write`] +//! +//! ## Concurrency +//! +//! +//! [`Read`] and [`Write`] can be sent between threads, but it can't be shared +//! between threads. _Some_ operations are safe to perform concurrently in much +//! the same way two `git` processes can access the same repository. +//! However, if you need multiple [`Read`]/[`Write`]s to be shared between +//! threads, use a [`Pool`] instead. +//! +//! [refs]: https://git-scm.com/book/en/v2/Git-Internals-Git-References +//! [objs]: https://git-scm.com/book/en/v2/Git-Internals-Git-Objects + +#[macro_use] +extern crate async_trait; + +extern crate radicle_git_ext as git_ext; +extern crate radicle_std_ext as std_ext; + +pub mod glob; + +pub mod pool; +pub use pool::Pool; + +pub mod refdb; +pub use refdb::{Applied, Reference, SymrefTarget, Target, Update, Updated}; + +pub mod odb; +pub use odb::{Blob, Commit, Object, Tag, Tree}; + +mod backend; +pub use backend::{ + read::{self, Read}, + write::{self, Write}, +}; + +pub mod signature; + +/// Initialise the git backend. +/// +/// **SHOULD** be called before all accesses to git functionality. +pub fn init() { + use libc::c_int; + use libgit2_sys as raw_git; + use std::sync::Once; + + static INIT: Once = Once::new(); + + unsafe { + INIT.call_once(|| { + let ret = + raw_git::git_libgit2_opts(raw_git::GIT_OPT_SET_MWINDOW_FILE_LIMIT as c_int, 256); + if ret < 0 { + panic!( + "error setting libgit2 option: {}", + git2::Error::last_error(ret).unwrap() + ) + } + }) + } +} diff --git a/git-storage/src/odb.rs b/git-storage/src/odb.rs new file mode 100644 index 0000000..5b6f59a --- /dev/null +++ b/git-storage/src/odb.rs @@ -0,0 +1,97 @@ +// Copyright © 2022 The Radicle Link Contributors +// SPDX-License-Identifier: GPL-3.0-or-later + +//! The `odb` is separated into two traits: [`Read`] and [`Write`], providing +//! access to [git objects][objs]. +//! +//! The [`Read`] trait provides functions for read-only access to the odb. +//! The [`Write`] trait provides functions for read and write access to the +//! odb, thus it implies the [`Read`] trait. +//! +//! The reason for separating these types of actions out is that one can infer +//! what kind of access a function has to the odb by looking at which trait it +//! is using. +//! +//! For implementations of these traits, this crate provides [`crate::Read`] and +//! [`crate::Write`] structs. +//! +//! [objs]: https://git-scm.com/book/en/v2/Git-Internals-Git-Objects + +// TODO: this doesn't abstract over the git2 types very well, but it's too much +// hassle to massage that right now. + +use std::error::Error; + +pub use git2::{Blob, Commit, Object, Tag, Tree}; + +use git_ext::Oid; + +pub mod read; +pub use read::Read; + +pub mod write; +pub use write::Write; + +/// Find the [`Object`] corresponding to the given `oid`. +/// +/// Will fail if the object does not exist. +pub fn object(storage: &S, oid: Oid) -> Result +where + S: Read, +{ + storage + .find_object(oid)? + .ok_or_else(|| not_found("object", oid)) +} + +/// Find the [`Blob`] corresponding to the given `oid`. +/// +/// Will fail if the blob does not exist. +pub fn blob(storage: &S, oid: Oid) -> Result +where + S: Read, +{ + storage + .find_blob(oid)? + .ok_or_else(|| not_found("blob", oid)) +} + +/// Find the [`Commit`] corresponding to the given `oid`. +/// +/// Will fail if the commit does not exist. +pub fn commit(storage: &S, oid: Oid) -> Result +where + S: Read, +{ + storage + .find_commit(oid)? + .ok_or_else(|| not_found("commit", oid)) +} + +/// Find the [`Tag`] corresponding to the given `oid`. +/// +/// Will fail if the tag does not exist. +pub fn tag(storage: &S, oid: Oid) -> Result +where + S: Read, +{ + storage.find_tag(oid)?.ok_or_else(|| not_found("tag", oid)) +} + +/// Find the [`Tree`] corresponding to the given `oid`. +/// +/// Will fail if the tree does not exist. +pub fn tree(storage: &S, oid: Oid) -> Result +where + S: Read, +{ + storage + .find_tree(oid)? + .ok_or_else(|| not_found("tree", oid)) +} + +fn not_found(kind: &str, oid: Oid) -> git2::Error { + use git2::{ErrorClass::*, ErrorCode::*}; + + git2::Error::new(NotFound, Object, format!("could not find {kind} {oid}")) +} diff --git a/git-storage/src/odb/read.rs b/git-storage/src/odb/read.rs new file mode 100644 index 0000000..a878165 --- /dev/null +++ b/git-storage/src/odb/read.rs @@ -0,0 +1,49 @@ +// Copyright © 2022 The Radicle Link Contributors +// SPDX-License-Identifier: GPL-3.0-or-later + +use super::*; + +/// Read-only access to a git refdb. +/// +/// See [`crate::Read`] for an implementation of this trait. +pub trait Read { + /// The error type for finding an object in the refdb. + type FindObj: Error + Send + Sync + 'static; + + /// The error type for finding a blob in the refdb. + type FindBlob: Error + Send + Sync + 'static; + + /// The error type for finding a commit in the refdb. + type FindCommit: Error + Send + Sync + 'static; + + /// The error type for finding a tag in the refdb. + type FindTag: Error + Send + Sync + 'static; + + /// The error type for finding a tree in the refdb. + type FindTree: Error + Send + Sync + 'static; + + /// Find the [`Object`] corresponding to the given `oid`. + /// + /// Returns `None` if the [`Object`] did not exist. + fn find_object(&self, oid: Oid) -> Result, Self::FindObj>; + + /// Find the [`Blob`] corresponding to the given `oid`. + /// + /// Returns `None` if the [`Blob`] did not exist. + fn find_blob(&self, oid: Oid) -> Result, Self::FindBlob>; + + /// Find the [`Commit`] corresponding to the given `oid`. + /// + /// Returns `None` if the [`Commit`] did not exist. + fn find_commit(&self, oid: Oid) -> Result, Self::FindCommit>; + + /// Find the [`Tag`] corresponding to the given `oid`. + /// + /// Returns `None` if the [`Tag`] did not exist. + fn find_tag(&self, oid: Oid) -> Result, Self::FindTag>; + + /// Find the [`Object`] corresponding to the given `oid`. + /// + /// Returns `None` if the [`Tag`] did not exist. + fn find_tree(&self, oid: Oid) -> Result, Self::FindTree>; +} diff --git a/git-storage/src/odb/write.rs b/git-storage/src/odb/write.rs new file mode 100644 index 0000000..6122d96 --- /dev/null +++ b/git-storage/src/odb/write.rs @@ -0,0 +1,66 @@ +// Copyright © 2022 The Radicle Link Contributors +// SPDX-License-Identifier: GPL-3.0-or-later + +use std::error::Error; + +use git_ext::Oid; +use git_ref_format::RefStr; + +use super::{Commit, Object, Read, Tree}; + +/// Read-write access to a git odb. +/// +/// See [`crate::Write`] for an implementation of this trait. +pub trait Write: Read { + /// The error type for writing a blob to the odb. + type WriteBlob: Error + Send + Sync + 'static; + + /// The error type for writing a commit to the odb. + type WriteCommit: Error + Send + Sync + 'static; + + /// The error type for writing a tag to the odb. + type WriteTag: Error + Send + Sync + 'static; + + /// The error type for writing a tree to the odb. + type WriteTree: Error + Send + Sync + 'static; + + /// Write a [`super::Blob`] containing the `data` provided. + fn write_blob(&self, data: &[u8]) -> Result; + + /// Write a [`Commit`] that points to the given `tree` and has the provided + /// `parents`. + /// + /// The signature of the [`Commit`] is expected to be provided by the + /// implementor of the trait. + /// + /// The commit will not be associated with any reference. If this is + /// required then you can use the [`Oid`] as the target for a + /// [`crate::refdb::Update`]. + fn write_commit<'a>( + &self, + tree: &Tree, + parents: &[&Commit<'a>], + message: &str, + ) -> Result; + + /// Write a [`super::Tag`] that points to the given `target`. + /// + /// The signature of the [`super::Tag`] is expected to be provided by the + /// implementor of the trait. + /// + /// No reference is created, however, the `name` is used for naming the + /// [`super::Tag`] object. + /// + /// If a reference is required then you can use the [`Oid`] as the target + /// for a [`crate::refdb::Update`]. + fn write_tag(&self, name: R, target: &Object, message: &str) -> Result + where + R: AsRef; + + /// Write a [`super::Tree`] using the provided `builder`. + // XXX: It's annoying that this exposes git2 but the effort of abstracting the + // tree builder is too high + // TODO: instead of passing the builder, we could use an enum of operations to + // modify a builder + fn write_tree(&self, builder: git2::TreeBuilder) -> Result; +} diff --git a/git-storage/src/pool.rs b/git-storage/src/pool.rs new file mode 100644 index 0000000..da6faa2 --- /dev/null +++ b/git-storage/src/pool.rs @@ -0,0 +1,176 @@ +// Copyright © 2019-2020 The Radicle Foundation +// +// This file is part of radicle-link, distributed under the GPLv3 with Radicle +// Linking Exception. For full terms see the included LICENSE file. + +use std::{ + marker::PhantomData, + ops::{Deref, DerefMut}, + path::PathBuf, + sync::Arc, +}; + +use deadpool::managed::{self, Manager, Object, RecycleResult}; +use parking_lot::RwLock; +use std_ext::Void; +use thiserror::Error; + +use crate::{read, signature::UserInfo, write, Read, Write}; + +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum InitError { + #[error(transparent)] + Read(#[from] read::error::Init), + + #[error(transparent)] + Write(#[from] write::error::Init), +} + +pub type Pool = deadpool::managed::Pool; +pub type PoolError = managed::PoolError; + +#[async_trait] +pub trait Pooled { + async fn get(&self) -> Result, PoolError>; +} + +#[async_trait] +impl Pooled for Pool { + async fn get(&self) -> Result, PoolError> { + self.get().await.map(PooledRef::from) + } +} + +/// A reference to a pooled storage. +/// +/// The `S` parameter can be filled by [`Write`] for read-write access or +/// [`Read`] for read-only access. +pub struct PooledRef(Object); + +impl Deref for PooledRef { + type Target = S; + + fn deref(&self) -> &Self::Target { + self.0.deref() + } +} + +impl DerefMut for PooledRef { + fn deref_mut(&mut self) -> &mut Self::Target { + self.0.deref_mut() + } +} + +impl AsRef for PooledRef { + fn as_ref(&self) -> &S { + self + } +} + +impl AsMut for PooledRef { + fn as_mut(&mut self) -> &mut S { + self + } +} + +impl AsRef for PooledRef { + fn as_ref(&self) -> &Read { + self.0.read_only() + } +} + +impl From> for PooledRef { + fn from(obj: Object) -> Self { + Self(obj) + } +} + +#[derive(Clone)] +pub struct Initialised(Arc>); + +impl Initialised { + pub fn no() -> Self { + Self(Arc::new(RwLock::new(false))) + } +} + +pub struct Writer { + init: Initialised, +} + +#[derive(Clone)] +pub struct Config { + root: PathBuf, + info: UserInfo, + write: W, +} + +pub type ReadConfig = Config>; +pub type WriteConfig = Config; + +impl ReadConfig { + pub fn new(root: PathBuf, info: UserInfo) -> Self { + Config { + root, + info, + write: PhantomData, + } + } + + pub fn write(self, init: Initialised) -> WriteConfig { + Config { + root: self.root, + info: self.info, + write: Writer { init }, + } + } +} + +#[async_trait] +impl Manager for ReadConfig { + async fn create(&self) -> Result { + Read::open(&self.root).map_err(InitError::from) + } + + async fn recycle(&self, _: &mut Read) -> RecycleResult { + Ok(()) + } +} + +impl WriteConfig { + pub fn new(root: PathBuf, info: UserInfo, init: Initialised) -> Self { + Self { + root, + info, + write: Writer { init }, + } + } + + fn mk_storage(&self) -> Result { + Write::open(&self.root, self.info.clone()).map_err(InitError::from) + } +} + +#[async_trait] +impl Manager for WriteConfig { + async fn create(&self) -> Result { + let initialised = self.write.init.0.read(); + if *initialised { + self.mk_storage() + } else { + drop(initialised); + let mut initialised = self.write.init.0.write(); + self.mk_storage() + .map(|storage| { + *initialised = true; + storage + }) + .map_err(InitError::from) + } + } + + async fn recycle(&self, _: &mut Write) -> RecycleResult { + Ok(()) + } +} diff --git a/git-storage/src/refdb.rs b/git-storage/src/refdb.rs new file mode 100644 index 0000000..cdb01ec --- /dev/null +++ b/git-storage/src/refdb.rs @@ -0,0 +1,125 @@ +// Copyright © 2022 The Radicle Link Contributors +// SPDX-License-Identifier: GPL-3.0-or-later + +//! The `refdb` is separated into two traits: [`Read`] and [`Write`], providing +//! access to [git references][refs]. +//! +//! The [`Read`] trait provides functions for read-only access to the refdb. +//! The [`Write`] trait provides functions for read and write access to the +//! refdb, thus it implies the [`Read`] trait. +//! +//! The reason for separating these types of actions out is that one can infer +//! what kind of access a function has to the refdb by looking at which trait it +//! is using. +//! +//! For implementations of these traits, this crate provides [`crate::Read`] and +//! [`crate::Write`] structs. +//! +//! [refs]: https://git-scm.com/book/en/v2/Git-Internals-Git-References + +use std::fmt::Debug; + +use git_ext::Oid; +use git_ref_format::RefString; + +pub mod iter; +pub use iter::{References, ReferencesGlob}; + +pub mod read; +pub use read::Read; + +pub mod write; +pub use write::{previous, Applied, Policy, SymrefTarget, Update, Updated, Write}; + +/// A read-only structure representing a git reference. +/// +/// References can be constructed by using the [`Read`] functions: +/// * [`Read::find_reference`] +/// * [`Read::find_references`] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Reference { + /// The name of the reference. + pub name: RefString, + /// The target of the reference. + pub target: Target, +} + +impl<'a> TryFrom> for Reference { + type Error = error::ParseReference; + + fn try_from(value: git2::Reference) -> Result { + use error::ParseReference; + + let name = value + .name() + .ok_or(ParseReference::InvalidUtf8) + .and_then(|name| RefString::try_from(name).map_err(ParseReference::from))?; + let target = match value.target() { + None => { + let name = value + .symbolic_target() + .ok_or(ParseReference::InvalidUtf8) + .and_then(|name| RefString::try_from(name).map_err(ParseReference::from))?; + name.into() + }, + Some(oid) => oid.into(), + }; + + Ok(Reference { name, target }) + } +} + +/// The target a reference points to. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Target { + /// The reference points directly at an `oid`. + Direct { oid: Oid }, + /// The reference is symbolic and points to another reference. + Symbolic { name: RefString }, +} + +impl From for Target { + fn from(oid: Oid) -> Self { + Self::Direct { oid } + } +} + +impl From for Target { + fn from(oid: git2::Oid) -> Self { + Oid::from(oid).into() + } +} + +impl From for Target { + fn from(name: RefString) -> Self { + Self::Symbolic { name } + } +} + +pub mod error { + use thiserror::Error; + + #[derive(Debug, Error)] + pub enum ParseReference { + #[error("reference name did contain valid UTF-8 bytes")] + InvalidUtf8, + #[error(transparent)] + RefString(#[from] git_ref_format::Error), + } + + #[derive(Debug, Error)] + pub enum Iter { + #[error(transparent)] + Git(#[from] git2::Error), + #[error(transparent)] + Parse(#[from] ParseReference), + } +} + +/// Utility function for resolving a [`Reference`] to its [`Oid`] +pub(crate) fn resolve(repo: &git2::Repository, r: &Reference) -> Result { + match &r.target { + Target::Direct { oid } => Ok(*oid), + Target::Symbolic { name } => repo.refname_to_id(name.as_str()).map(Oid::from), + } +} diff --git a/git-storage/src/refdb/iter.rs b/git-storage/src/refdb/iter.rs new file mode 100644 index 0000000..189a7cb --- /dev/null +++ b/git-storage/src/refdb/iter.rs @@ -0,0 +1,49 @@ +// Copyright © 2022 The Radicle Link Contributors +// SPDX-License-Identifier: GPL-3.0-or-later + +//! Iterator adaptors over [`git2::References`]. + +use std::fmt::Debug; + +use crate::glob; + +use super::{error, Reference}; + +/// Iterator for [`Reference`]s where the inner [`glob::Pattern`] supplied +/// filters out any non-matching reference names. +pub struct References<'a> { + pub(crate) inner: ReferencesGlob<'a, glob::RefspecMatcher>, +} + +impl<'a> Iterator for References<'a> { + type Item = Result; + + fn next(&mut self) -> Option { + self.inner.next() + } +} + +pub struct ReferencesGlob<'a, G: glob::Pattern + Debug> { + pub(crate) iter: git2::References<'a>, + pub(crate) glob: G, +} + +impl<'a, G: glob::Pattern + Debug> Iterator for ReferencesGlob<'a, G> { + type Item = Result; + + fn next(&mut self) -> Option { + for reference in &mut self.iter { + match reference { + Ok(reference) => match reference.name() { + Some(name) if self.glob.matches(name) => { + return Some(Reference::try_from(reference).map_err(error::Iter::from)) + }, + _ => continue, + }, + + Err(e) => return Some(Err(e.into())), + } + } + None + } +} diff --git a/git-storage/src/refdb/read.rs b/git-storage/src/refdb/read.rs new file mode 100644 index 0000000..698f65b --- /dev/null +++ b/git-storage/src/refdb/read.rs @@ -0,0 +1,43 @@ +// Copyright © 2022 The Radicle Link Contributors +// SPDX-License-Identifier: GPL-3.0-or-later + +use std::error::Error; + +use git_ext::Oid; +use git_ref_format::{refspec, RefStr}; + +use super::Reference; + +/// Read-only access to a git refdb. +/// +/// See [`crate::Read`] for an implementation of this trait. +pub trait Read { + /// The error type for finding a reference in the refdb. + type FindRef: Error + Send + Sync + 'static; + + /// The error type for finding references in the refdb. + type FindRefs: Error + Send + Sync + 'static; + + /// The error type for finding reference Oid in the refdb. + type FindRefOid: Error + Send + Sync + 'static; + + /// Iterator for references returned by `find_references`. + type References: Iterator>; + + /// Find the reference that corresponds to `name`. If the reference does not + /// exist, then `None` is returned. + fn find_reference(&self, name: Ref) -> Result, Self::FindRef> + where + Ref: AsRef; + + /// Find the references that match `pattern`. + fn find_references(&self, pattern: Pat) -> Result + where + Pat: AsRef; + + /// Find the [`Oid`] for the reference that corresponds to `name`. If the + /// reference does not exist, then `None` is returned. + fn find_reference_oid(&self, name: Ref) -> Result, Self::FindRefOid> + where + Ref: AsRef; +} diff --git a/git-storage/src/refdb/write.rs b/git-storage/src/refdb/write.rs new file mode 100644 index 0000000..adfd689 --- /dev/null +++ b/git-storage/src/refdb/write.rs @@ -0,0 +1,152 @@ +// Copyright © 2022 The Radicle Link Contributors +// SPDX-License-Identifier: GPL-3.0-or-later + +use std::error::Error; + +use git_ext::Oid; +use git_ref_format::{Qualified, RefString}; + +use super::read::Read; + +pub mod previous; + +/// Read-write access to a git refdb. +/// +/// See [`crate::Write`] for an implementation of this trait. +pub trait Write: Read { + type UpdateError: Error + Send + Sync + 'static; + + /// Apply the provided `updates` to the refdb. + /// + /// The implementation of `update` is expected to be transactional. All + /// successful updates are returned in [`Applied::updated`] while rejected + /// updates should be returned in [`Applied::rejected`]. + fn update<'a, U>(&mut self, updates: U) -> Result, Self::UpdateError> + where + U: IntoIterator>; +} + +/// Perform an update to a reference in the refdb. +#[derive(Debug, Clone)] +pub enum Update<'a> { + /// Update a direct reference, i.e. one that points directly to an [`Oid`]. + Direct { + /// The `name` of the reference. + name: Qualified<'a>, + /// The `target` to point the reference at. + target: Oid, + /// Policy to apply when an [`Update`] would not apply as a + /// fast-forward. + /// + /// An update is a fast-forward iff: + /// + /// 1. A ref with the same name already exists + /// 2. The ref is a direct ref, and the update is a [`Update::Direct`] + /// 3. Both the existing and the update [`Oid`] point to a commit + /// object without peeling + /// 4. The existing commit is an ancestor of the update commit + /// + /// or: + /// + /// 1. A ref with the same name does not already exist + no_ff: Policy, + /// The expectation of the previous value for the reference before + /// making the update. This allows the update to be rejected in + /// the case of a concurrent modification the reference. + previous: previous::Edit, + /// The [`reflog`][reflog] entry for the update. + /// + /// [reflog]: https://git-scm.com/docs/git-reflog + // TODO: we may want to force the creation of a reflog entry for specific references. + reflog: String, + }, + Symbolic { + /// The `name` of the reference. + name: Qualified<'a>, + /// The `target` to point the reference at. Currently, only supports + /// one-level deep. + target: SymrefTarget<'a>, + /// Policy to apply when the ref already exists, but is a direct ref + /// before the update. + type_change: Policy, + /// The expectation of the previous value for the reference before + /// making the update. This allows the update to be rejected in + /// the case of a concurrent modification the reference. + previous: previous::Edit, + /// The [`reflog`][reflog] entry for the update. + /// + /// [reflog]: https://git-scm.com/docs/git-reflog + reflog: String, + }, + Remove { + /// The `name` of the reference. + name: Qualified<'a>, + /// The expectation of the previous value for the reference before + /// making the update. This allows the update to be rejected in + /// the case of a concurrent modification the reference. + previous: previous::Remove, + }, +} + +/// A target of a [symbolic reference][symref]. +/// +/// [symref]: https://git-scm.com/docs/git-symbolic-ref +#[derive(Debug, Clone)] +pub struct SymrefTarget<'a> { + /// The `name` of the symbolic reference. + pub name: Qualified<'a>, + /// The underlying `target` of the symbolic reference. + pub target: Oid, +} + +/// The successful result of an [`Update`] applied to the refdb. +#[derive(Debug, Clone)] +pub enum Updated { + /// The [`Update::Direct`] was succesful. + Direct { + /// The `name` of the reference that was updated. + name: RefString, + /// The new `target` of the reference that was updated. + target: Oid, + /// The previous target of the reference, if it existed. + previous: Option, + }, + /// The [`Update::Symbolic`] was succesful. + Symbolic { + /// The `name` of the reference that was updated. + name: RefString, + /// The new `target` of the reference that was updated. + target: RefString, + /// The previous peeled target of the reference, if it existed. + previous: Option, + }, + /// The [`Update::Remove`] was succesful. + Removed { + /// The `name` of the reference that was removed. + name: RefString, + /// The old `target` of the reference that was removed. + previous: Oid, + }, +} + +/// The outcome of running a [`Write::update`]. +#[derive(Clone, Default)] +pub struct Applied<'a> { + /// Any [`Update`]s that were rejected due to their [`previous::Edit`], + /// [`previous::Remove`], or [`Policy`]. + pub rejected: Vec>, + /// The successful [`Update`]s applied to the refdb. + pub updated: Vec, +} + +/// The policy to use when guarding against fast-forwards in the case of +/// [`Update::Direct`] and type changes in the case of [`Update::Symbolic`]. +#[derive(Clone, Copy, Debug)] +pub enum Policy { + /// Abort the entire transaction. + Abort, + /// Reject this update, but continue the transaction. + Reject, + /// Allow the update. + Allow, +} diff --git a/git-storage/src/refdb/write/previous.rs b/git-storage/src/refdb/write/previous.rs new file mode 100644 index 0000000..efe0be1 --- /dev/null +++ b/git-storage/src/refdb/write/previous.rs @@ -0,0 +1,132 @@ +// Copyright © 2022 The Radicle Link Contributors +// SPDX-License-Identifier: GPL-3.0-or-later + +//! The [`Edit`] and [`Remove`] enums provide a mechanism for protecting updates +//! during concurrent writes to the refdb. The value is constructed with the +//! policy, sometimes providing the expected previous value of the [`Oid`] +//! target. If this policy is not satisfied then the update should be rejected. + +use git_ext::Oid; + +use thiserror::Error; + +/// The expectation of the reference's state. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum Edit { + /// No requirements are made towards the current value, and the new value is + /// set unconditionally. + Any, + /// The reference must exist and may have any value. + MustExist, + /// Create the ref only, hence the reference must not exist. + MustNotExist, + /// The ref _must_ exist and have the given value. + MustExistAndMatch(Oid), + /// The ref _may_ exist and have the given value, or may not exist at all. + MayExistAndMatch(Oid), +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Error)] +pub enum EditError { + #[error("the reference does not exist when it was expected to exist")] + DoesNotExist, + #[error("the reference does exist when it was expected not to exist")] + DoesExist, + #[error("the reference does not match - given: '{given}' expected: '{expected}'")] + DoesNotMatch { given: Oid, expected: Oid }, +} + +impl Edit { + /// Guard against the `given` value using this [`Edit`]. + /// + /// The function will return [`Err`] if the `given` value does not pass the + /// policy the policy of the [`Edit`]. + /// + /// The `given` value is presumed non-existing if set to `None`, otherwise + /// it is presumed to be existing. + pub fn guard(&self, given: Option) -> Result<(), EditError> { + use EditError::*; + + match self { + Self::Any => Ok(()), + Self::MustExist => { + if given.is_none() { + Err(DoesNotExist) + } else { + Ok(()) + } + }, + Self::MustNotExist => { + if given.is_some() { + Err(DoesExist) + } else { + Ok(()) + } + }, + Self::MustExistAndMatch(expected) => match given { + Some(given) if &given == expected => Ok(()), + Some(given) => Err(DoesNotMatch { + given, + expected: *expected, + }), + None => Err(DoesNotExist), + }, + Self::MayExistAndMatch(expected) => match given { + Some(given) if &given == expected => Ok(()), + Some(given) => Err(DoesNotMatch { + given, + expected: *expected, + }), + None => Ok(()), + }, + } + } +} + +/// The expectation of the existing reference's state. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum Remove { + /// The reference must exist and may have any value. + MustExist, + /// The ref _must_ exist and have the given value. + MustExistAndMatch(Oid), +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Error)] +pub enum RemoveError { + #[error("the reference does not exist when it was expected to exist")] + DoesNotExist, + #[error("the reference does not match - given: '{given}' expected: '{expected}'")] + DoesNotMatch { given: Oid, expected: Oid }, +} + +impl Remove { + /// Guard against the `given` value using this [`Remove`]. + /// + /// The function will return [`Err`] if the `given` value does not pass the + /// policy the policy of the [`Remove`]. + /// + /// The `given` value is presumed non-existing if set to `None`, otherwise + /// it is presumed to be existing. + pub fn guard(&self, given: Option) -> Result<(), RemoveError> { + use RemoveError::*; + + match self { + Self::MustExist => { + if given.is_none() { + Err(DoesNotExist) + } else { + Ok(()) + } + }, + Self::MustExistAndMatch(expected) => match given { + Some(given) if &given == expected => Ok(()), + Some(given) => Err(DoesNotMatch { + given, + expected: *expected, + }), + None => Err(DoesNotExist), + }, + } + } +} diff --git a/git-storage/src/signature.rs b/git-storage/src/signature.rs new file mode 100644 index 0000000..9b67521 --- /dev/null +++ b/git-storage/src/signature.rs @@ -0,0 +1,19 @@ +// Copyright © 2022 The Radicle Link Contributors +// SPDX-License-Identifier: GPL-3.0-or-later + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct UserInfo { + /// Provided `name` of the user. + pub name: String, + /// Proivded `email` of the user. Note that this does not + /// necessarily have to be an email, but will be used as the email + /// field in the [`git2::Signature`]. + pub email: String, +} + +impl UserInfo { + /// Obtain the [`git2::Signature`] for this `UserInfo`. + pub fn signature(&self) -> Result { + git2::Signature::now(&self.name, &self.email) + } +}