mirror of
https://seed.radicle.dev/rad:z6cFWeWpnZNHh9rUW8phgA3b5yGt.git
synced 2026-10-24 05:40:38 +08:00
Merge remote-tracking branch 'origin/storage-port'
* origin/storage-port: git-storage: remove owner and config git-storage: fallible functions for finding objects git-storage: return previous targets git-storage: port storage from librad
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"git-ref-format",
|
||||
"git-storage",
|
||||
"git-trailers",
|
||||
"link-git",
|
||||
"radicle-git-ext",
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
[package]
|
||||
name = "git-storage"
|
||||
version = "0.1.0"
|
||||
authors = ["Kim Altintop <kim@eagain.st>", "Fintan Halpenny <fintan.halpenny@gmail.com>"]
|
||||
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"
|
||||
@@ -0,0 +1,5 @@
|
||||
// Copyright © 2022 The Radicle Link Contributors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
pub mod read;
|
||||
pub mod write;
|
||||
@@ -0,0 +1,160 @@
|
||||
// Copyright © 2019-2020 The Radicle Foundation <hello@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<P: AsRef<Path>>(path: P) -> Result<Self, error::Init> {
|
||||
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<Ref>(&self, reference: Ref) -> Result<Option<Reference>, Self::FindRef>
|
||||
where
|
||||
Ref: AsRef<git_ref_format::RefStr>,
|
||||
{
|
||||
let reference = self
|
||||
.raw
|
||||
.find_reference(reference.as_ref().as_str())
|
||||
.map(Some)
|
||||
.or_matches::<git2::Error, _, _>(is_not_found_err, || Ok(None))?;
|
||||
Ok(reference.map(Reference::try_from).transpose()?)
|
||||
}
|
||||
|
||||
fn find_references<Pat>(&self, reference: Pat) -> Result<Self::References, Self::FindRefs>
|
||||
where
|
||||
Pat: AsRef<git_ref_format::refspec::PatternStr>,
|
||||
{
|
||||
Ok(References {
|
||||
inner: ReferencesGlob {
|
||||
iter: self.raw.references()?,
|
||||
glob: glob::RefspecMatcher::from(reference.as_ref().to_owned()),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn find_reference_oid<Ref>(&self, reference: Ref) -> Result<Option<Oid>, Self::FindRefOid>
|
||||
where
|
||||
Ref: AsRef<git_ref_format::RefStr>,
|
||||
{
|
||||
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<Option<Object>, Self::FindObj> {
|
||||
self.raw
|
||||
.find_object(oid.into(), None)
|
||||
.map(Some)
|
||||
.or_matches::<git2::Error, _, _>(is_not_found_err, || Ok(None))
|
||||
}
|
||||
|
||||
fn find_blob(&self, oid: Oid) -> Result<Option<Blob>, 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<Option<Commit>, 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<Option<Tag>, 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<Option<Tree>, Self::FindTree> {
|
||||
self.raw
|
||||
.find_tree(oid.into())
|
||||
.map(Some)
|
||||
.or_matches(is_not_found_err, || Ok(None))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,574 @@
|
||||
// Copyright © 2019-2020 The Radicle Foundation <hello@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<P: AsRef<Path>>(path: P, info: UserInfo) -> Result<Self, error::Init> {
|
||||
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<Ref>(&self, reference: Ref) -> Result<Option<Reference>, Self::FindRef>
|
||||
where
|
||||
Ref: AsRef<git_ref_format::RefStr>,
|
||||
{
|
||||
self.read_only().find_reference(reference)
|
||||
}
|
||||
|
||||
fn find_references<Pat>(&self, reference: Pat) -> Result<Self::References, Self::FindRefs>
|
||||
where
|
||||
Pat: AsRef<git_ref_format::refspec::PatternStr>,
|
||||
{
|
||||
self.read_only().find_references(reference)
|
||||
}
|
||||
|
||||
fn find_reference_oid<Ref>(&self, reference: Ref) -> Result<Option<Oid>, Self::FindRefOid>
|
||||
where
|
||||
Ref: AsRef<git_ref_format::RefStr>,
|
||||
{
|
||||
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<refdb::write::Applied<'b>, Self::UpdateError>
|
||||
where
|
||||
U: IntoIterator<Item = Update<'b>>,
|
||||
{
|
||||
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<Self, git2::Error> {
|
||||
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<Either<Update<'b>, 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<Either<Update<'b>, Vec<Updated>>, 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<Either<Update<'b>, 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<R>(&mut self, reference: R) -> Result<(), error::Transaction>
|
||||
where
|
||||
R: AsRef<RefStr>,
|
||||
{
|
||||
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<R>(
|
||||
&mut self,
|
||||
reference: R,
|
||||
target: Oid,
|
||||
prev: Option<Oid>,
|
||||
reflog: &str,
|
||||
) -> Result<Updated, error::Transaction>
|
||||
where
|
||||
R: AsRef<RefStr>,
|
||||
{
|
||||
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<R>(
|
||||
&mut self,
|
||||
reference: R,
|
||||
target: SymrefTarget<'a>,
|
||||
prev: Option<Oid>,
|
||||
reflog: &str,
|
||||
is_ff: bool,
|
||||
) -> Result<Vec<Updated>, error::Transaction>
|
||||
where
|
||||
R: AsRef<RefStr>,
|
||||
{
|
||||
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<R>(
|
||||
&mut self,
|
||||
reference: R,
|
||||
previous: Oid,
|
||||
) -> Result<Updated, error::Transaction>
|
||||
where
|
||||
R: AsRef<RefStr>,
|
||||
{
|
||||
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<R>(&self, name: R, target: Oid, prev: Oid) -> Result<bool, error::Transaction>
|
||||
where
|
||||
R: AsRef<RefStr>,
|
||||
{
|
||||
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 = <Read as odb::Read>::FindObj;
|
||||
type FindBlob = <Read as odb::Read>::FindBlob;
|
||||
type FindCommit = <Read as odb::Read>::FindCommit;
|
||||
type FindTag = <Read as odb::Read>::FindTag;
|
||||
type FindTree = <Read as odb::Read>::FindTree;
|
||||
|
||||
fn find_object(&self, oid: Oid) -> Result<Option<crate::Object>, Self::FindObj> {
|
||||
self.read_only().find_object(oid)
|
||||
}
|
||||
|
||||
fn find_blob(&self, oid: Oid) -> Result<Option<git2::Blob>, Self::FindBlob> {
|
||||
self.read_only().find_blob(oid)
|
||||
}
|
||||
|
||||
fn find_commit(&self, oid: Oid) -> Result<Option<git2::Commit>, Self::FindCommit> {
|
||||
self.read_only().find_commit(oid)
|
||||
}
|
||||
|
||||
fn find_tag(&self, oid: Oid) -> Result<Option<git2::Tag>, Self::FindTag> {
|
||||
self.read_only().find_tag(oid)
|
||||
}
|
||||
|
||||
fn find_tree(&self, oid: Oid) -> Result<Option<git2::Tree>, 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<Oid, Self::WriteBlob> {
|
||||
self.as_raw().blob(data).map(Oid::from)
|
||||
}
|
||||
|
||||
fn write_commit<'b>(
|
||||
&self,
|
||||
tree: &odb::Tree,
|
||||
parents: &[&odb::Commit<'b>],
|
||||
message: &str,
|
||||
) -> Result<Oid, Self::WriteCommit> {
|
||||
let author = self.info.signature()?;
|
||||
self.as_raw()
|
||||
.commit(None, &author, &author, message, tree, parents)
|
||||
.map(Oid::from)
|
||||
}
|
||||
|
||||
fn write_tag<R>(
|
||||
&self,
|
||||
name: R,
|
||||
target: &odb::Object,
|
||||
message: &str,
|
||||
) -> Result<Oid, Self::WriteTag>
|
||||
where
|
||||
R: AsRef<RefStr>,
|
||||
{
|
||||
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<Oid, Self::WriteTree> {
|
||||
builder.write().map(Oid::from)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// Copyright © 2019-2020 The Radicle Foundation <hello@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),
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright © 2019-2020 The Radicle Foundation <hello@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<P: AsRef<Path>>(&self, path: P) -> bool;
|
||||
}
|
||||
|
||||
impl Pattern for globset::GlobMatcher {
|
||||
fn matches<P: AsRef<Path>>(&self, path: P) -> bool {
|
||||
self.is_match(path)
|
||||
}
|
||||
}
|
||||
|
||||
impl Pattern for globset::GlobSet {
|
||||
fn matches<P: AsRef<Path>>(&self, path: P) -> bool {
|
||||
self.is_match(path)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RefspecMatcher(globset::GlobMatcher);
|
||||
|
||||
impl From<ext::RefspecPattern> for RefspecMatcher {
|
||||
fn from(pat: ext::RefspecPattern) -> Self {
|
||||
Self(globset::Glob::new(pat.as_str()).unwrap().compile_matcher())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PatternString> for RefspecMatcher {
|
||||
fn from(pat: PatternString) -> Self {
|
||||
Self(globset::Glob::new(pat.as_str()).unwrap().compile_matcher())
|
||||
}
|
||||
}
|
||||
|
||||
impl Pattern for RefspecMatcher {
|
||||
fn matches<P: AsRef<Path>>(&self, path: P) -> bool {
|
||||
self.0.is_match(path)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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<S>(storage: &S, oid: Oid) -> Result<Object, S::FindObj>
|
||||
where
|
||||
S: Read<FindObj = git2::Error>,
|
||||
{
|
||||
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<S>(storage: &S, oid: Oid) -> Result<Blob, S::FindBlob>
|
||||
where
|
||||
S: Read<FindBlob = git2::Error>,
|
||||
{
|
||||
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<S>(storage: &S, oid: Oid) -> Result<Commit, S::FindCommit>
|
||||
where
|
||||
S: Read<FindCommit = git2::Error>,
|
||||
{
|
||||
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<S>(storage: &S, oid: Oid) -> Result<Tag, S::FindTag>
|
||||
where
|
||||
S: Read<FindTag = git2::Error>,
|
||||
{
|
||||
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<S>(storage: &S, oid: Oid) -> Result<Tree, S::FindTree>
|
||||
where
|
||||
S: Read<FindTree = git2::Error>,
|
||||
{
|
||||
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}"))
|
||||
}
|
||||
@@ -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<Option<Object>, 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<Option<Blob>, 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<Option<Commit>, 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<Option<Tag>, 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<Option<Tree>, Self::FindTree>;
|
||||
}
|
||||
@@ -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<Oid, Self::WriteBlob>;
|
||||
|
||||
/// 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<Oid, Self::WriteCommit>;
|
||||
|
||||
/// 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<R>(&self, name: R, target: &Object, message: &str) -> Result<Oid, Self::WriteTag>
|
||||
where
|
||||
R: AsRef<RefStr>;
|
||||
|
||||
/// 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<Oid, Self::WriteTree>;
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
// Copyright © 2019-2020 The Radicle Foundation <hello@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<S> = deadpool::managed::Pool<S, InitError>;
|
||||
pub type PoolError = managed::PoolError<InitError>;
|
||||
|
||||
#[async_trait]
|
||||
pub trait Pooled<S: Send> {
|
||||
async fn get(&self) -> Result<PooledRef<S>, PoolError>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<S: Send> Pooled<S> for Pool<S> {
|
||||
async fn get(&self) -> Result<PooledRef<S>, 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<S>(Object<S, InitError>);
|
||||
|
||||
impl<S> Deref for PooledRef<S> {
|
||||
type Target = S;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.0.deref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> DerefMut for PooledRef<S> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
self.0.deref_mut()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> AsRef<S> for PooledRef<S> {
|
||||
fn as_ref(&self) -> &S {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> AsMut<S> for PooledRef<S> {
|
||||
fn as_mut(&mut self) -> &mut S {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<Read> for PooledRef<Write> {
|
||||
fn as_ref(&self) -> &Read {
|
||||
self.0.read_only()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> From<Object<S, InitError>> for PooledRef<S> {
|
||||
fn from(obj: Object<S, InitError>) -> Self {
|
||||
Self(obj)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Initialised(Arc<RwLock<bool>>);
|
||||
|
||||
impl Initialised {
|
||||
pub fn no() -> Self {
|
||||
Self(Arc::new(RwLock::new(false)))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Writer {
|
||||
init: Initialised,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Config<W> {
|
||||
root: PathBuf,
|
||||
info: UserInfo,
|
||||
write: W,
|
||||
}
|
||||
|
||||
pub type ReadConfig = Config<PhantomData<Void>>;
|
||||
pub type WriteConfig = Config<Writer>;
|
||||
|
||||
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<Read, InitError> for ReadConfig {
|
||||
async fn create(&self) -> Result<Read, InitError> {
|
||||
Read::open(&self.root).map_err(InitError::from)
|
||||
}
|
||||
|
||||
async fn recycle(&self, _: &mut Read) -> RecycleResult<InitError> {
|
||||
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, InitError> {
|
||||
Write::open(&self.root, self.info.clone()).map_err(InitError::from)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Manager<Write, InitError> for WriteConfig {
|
||||
async fn create(&self) -> Result<Write, InitError> {
|
||||
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<InitError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -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<git2::Reference<'a>> for Reference {
|
||||
type Error = error::ParseReference;
|
||||
|
||||
fn try_from(value: git2::Reference) -> Result<Self, Self::Error> {
|
||||
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<Oid> for Target {
|
||||
fn from(oid: Oid) -> Self {
|
||||
Self::Direct { oid }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<git2::Oid> for Target {
|
||||
fn from(oid: git2::Oid) -> Self {
|
||||
Oid::from(oid).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RefString> 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<Oid, git2::Error> {
|
||||
match &r.target {
|
||||
Target::Direct { oid } => Ok(*oid),
|
||||
Target::Symbolic { name } => repo.refname_to_id(name.as_str()).map(Oid::from),
|
||||
}
|
||||
}
|
||||
@@ -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<Reference, error::Iter>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
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<Reference, error::Iter>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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<Item = Result<Reference, Self::FindRefs>>;
|
||||
|
||||
/// Find the reference that corresponds to `name`. If the reference does not
|
||||
/// exist, then `None` is returned.
|
||||
fn find_reference<Ref>(&self, name: Ref) -> Result<Option<Reference>, Self::FindRef>
|
||||
where
|
||||
Ref: AsRef<RefStr>;
|
||||
|
||||
/// Find the references that match `pattern`.
|
||||
fn find_references<Pat>(&self, pattern: Pat) -> Result<Self::References, Self::FindRefs>
|
||||
where
|
||||
Pat: AsRef<refspec::PatternStr>;
|
||||
|
||||
/// Find the [`Oid`] for the reference that corresponds to `name`. If the
|
||||
/// reference does not exist, then `None` is returned.
|
||||
fn find_reference_oid<Ref>(&self, name: Ref) -> Result<Option<Oid>, Self::FindRefOid>
|
||||
where
|
||||
Ref: AsRef<RefStr>;
|
||||
}
|
||||
@@ -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<Applied<'a>, Self::UpdateError>
|
||||
where
|
||||
U: IntoIterator<Item = Update<'a>>;
|
||||
}
|
||||
|
||||
/// 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<Oid>,
|
||||
},
|
||||
/// 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<Oid>,
|
||||
},
|
||||
/// 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<Update<'a>>,
|
||||
/// The successful [`Update`]s applied to the refdb.
|
||||
pub updated: Vec<Updated>,
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
@@ -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<Oid>) -> 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<Oid>) -> 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),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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, git2::Error> {
|
||||
git2::Signature::now(&self.name, &self.email)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user