git-storage: remove crate

This crate is not being published and is not used anywhere, so it is
being removed from the set of crates.
This commit is contained in:
Fintan Halpenny
2025-09-11 09:05:28 +01:00
parent b3af979faa
commit 3e3c5e0f0e
25 changed files with 0 additions and 2095 deletions
Generated
-26
View File
@@ -298,32 +298,6 @@ dependencies = [
"syn",
]
[[package]]
name = "git-storage"
version = "0.1.0"
dependencies = [
"either",
"git2",
"globset",
"libc",
"libgit2-sys",
"parking_lot",
"radicle-git-ext",
"radicle-std-ext",
"thiserror",
]
[[package]]
name = "git-storage-test"
version = "0.1.0"
dependencies = [
"git-storage",
"git2",
"proptest",
"radicle-git-ext",
"test-helpers",
]
[[package]]
name = "git2"
version = "0.19.0"
-3
View File
@@ -5,8 +5,5 @@ members = [
"radicle-surf",
"test",
]
exclude = [
"git-storage",
]
resolver = "2"
package.version = "0.18.0"
-29
View File
@@ -1,29 +0,0 @@
[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]
globset = "0.4"
libc = "0.2"
parking_lot = "0.12"
thiserror = "1"
either = "1.8.0"
[dependencies.git2]
version = "0.19"
default-features = false
features = ["vendored-libgit2"]
[dependencies.libgit2-sys]
version = ">= 0.14.2"
default-features = false
features = ["vendored"]
[dependencies.radicle-git-ext]
path = "../radicle-git-ext"
[dependencies.radicle-std-ext]
path = "../radicle-std-ext"
-5
View File
@@ -1,5 +0,0 @@
// Copyright © 2022 The Radicle Link Contributors
// SPDX-License-Identifier: GPL-3.0-or-later
pub mod read;
pub mod write;
-166
View File
@@ -1,166 +0,0 @@
// 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::{fmt, path::Path};
use git_ext::{error::is_not_found_err, ref_format, 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.
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, error::Init> {
crate::init();
let raw = git2::Repository::open(path)?;
Ok(Self { raw })
}
}
impl fmt::Debug for Read {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Read")
.field("raw", &self.raw.path())
.finish()
}
}
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<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<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<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))
}
}
-585
View File
@@ -1,585 +0,0 @@
// 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,
ref_format::{self, Qualified, RefStr, RefString},
Oid,
};
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`].
#[derive(Debug)]
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.
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<ref_format::RefStr>,
{
self.read_only().find_reference(reference)
}
fn find_references<Pat>(&self, reference: Pat) -> Result<Self::References, Self::FindRefs>
where
Pat: AsRef<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<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(
&self,
tree: &odb::Tree,
parents: &[&odb::Commit<'_>],
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: odb::TreeBuilder) -> Result<Oid, Self::WriteTree> {
let repo = self.as_raw();
let mut tree = repo.treebuilder(None)?;
for entry in builder.iter() {
match entry {
odb::TreeEntry::Insert {
name,
oid,
filemode,
} => tree.insert(name, (*oid).into(), *filemode).map(|_| ())?,
odb::TreeEntry::Remove { name } => tree.remove(name)?,
}
}
tree.write().map(Oid::from)
}
}
-80
View File
@@ -1,80 +0,0 @@
// 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),
}
-39
View File
@@ -1,39 +0,0 @@
// 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::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<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)
}
}
-77
View File
@@ -1,77 +0,0 @@
// 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.
//!
//! [refs]: https://git-scm.com/book/en/v2/Git-Internals-Git-References
//! [objs]: https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
extern crate radicle_git_ext as git_ext;
extern crate radicle_std_ext as std_ext;
pub mod glob;
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()
)
}
})
}
}
-165
View File
@@ -1,165 +0,0 @@
// 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,
path::{Path, PathBuf},
};
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;
/// A `TreeBuilder` represents a series of operations to build a git tree, see
/// *Tree Objects* [here][git-objects].
///
/// The [`TreeEntry`] variants represent a limited set of the actions one can
/// perform [git-objects]: <https://git-scm.com/book/en/v2/Git-Internals-Git-Objects>
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct TreeBuilder {
entries: Vec<TreeEntry>,
}
impl TreeBuilder {
pub fn new() -> Self {
Self {
entries: Vec::new(),
}
}
/// [`TreeBuilder::insert`] creates a [`TreeEntry::Insert`] entry in the
/// `TreeBuilder`.
pub fn insert<P, M>(mut self, name: P, oid: Oid, mode: M) -> Self
where
P: AsRef<Path>,
M: Into<i32>,
{
let name = name.as_ref().to_path_buf();
let filemode = mode.into();
self.entries.push(TreeEntry::Insert {
name,
oid,
filemode,
});
self
}
/// [`TreeBuilder::remove`] creates a [`TreeEntry::Remove`] entry in the
/// `TreeBuilder`.
pub fn remove<P>(mut self, name: P) -> Self
where
P: AsRef<Path>,
{
let name = name.as_ref().to_path_buf();
self.entries.push(TreeEntry::Remove { name });
self
}
/// Iterate over the [`TreeEntry`]'s found in the `TreeBuilder`.
pub fn iter(&self) -> impl Iterator<Item = &TreeEntry> {
self.entries.iter()
}
}
/// A `TreeEntry` represents a single operation within a [`TreeBuilder`].
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum TreeEntry {
/// Insert the `oid` under the `name` path in the resulting tree.
/// The `filemode` determines the file mode for the file found at `name`.
Insert {
name: PathBuf,
oid: Oid,
filemode: i32,
},
/// Remove the `name` path from the resulting tree.
Remove { name: PathBuf },
}
/// 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}"))
}
-49
View File
@@ -1,49 +0,0 @@
// 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>;
}
-61
View File
@@ -1,61 +0,0 @@
// Copyright © 2022 The Radicle Link Contributors
// SPDX-License-Identifier: GPL-3.0-or-later
use std::error::Error;
use git_ext::{ref_format::RefStr, Oid};
use super::{Commit, Object, Read, Tree, TreeBuilder};
/// 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(
&self,
tree: &Tree,
parents: &[&Commit<'_>],
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`.
fn write_tree(&self, builder: TreeBuilder) -> Result<Oid, Self::WriteTree>;
}
-124
View File
@@ -1,124 +0,0 @@
// 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::{ref_format::RefString, Oid};
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] radicle_git_ext::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),
}
}
-49
View File
@@ -1,49 +0,0 @@
// 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
}
}
-45
View File
@@ -1,45 +0,0 @@
// Copyright © 2022 The Radicle Link Contributors
// SPDX-License-Identifier: GPL-3.0-or-later
use std::error::Error;
use git_ext::{
ref_format::{refspec, RefStr},
Oid,
};
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>;
}
-154
View File
@@ -1,154 +0,0 @@
// Copyright © 2022 The Radicle Link Contributors
// SPDX-License-Identifier: GPL-3.0-or-later
use std::error::Error;
use git_ext::{
ref_format::{Qualified, RefString},
Oid,
};
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,
}
-132
View File
@@ -1,132 +0,0 @@
// 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),
},
}
}
}
-19
View File
@@ -1,19 +0,0 @@
// 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)
}
}
-40
View File
@@ -1,40 +0,0 @@
[package]
name = "git-storage-test"
version = "0.1.0"
edition = "2021"
license = "GPL-3.0-or-later"
publish = false
[lib]
doctest = false
test = true
doc = false
[features]
test = []
[dependencies]
proptest = "1"
[dependencies.git-storage]
path = ".."
[dependencies.git2]
version = "0.19"
default-features = false
features = ["vendored-libgit2"]
[dependencies.radicle-git-ext]
path = "../../radicle-git-ext/"
[dependencies.test-helpers]
path = "../../test/test-helpers"
[dev-dependencies.uuid]
version = "1"
features = ["v4"]
[dev-dependencies.radicle-git-ext-test]
path = "../../radicle-git-ext/t"
features = ["test"]
-108
View File
@@ -1,108 +0,0 @@
// Copyright © 2022 The Radicle Link Contributors
// SPDX-License-Identifier: GPL-3.0-or-later
use std::path::PathBuf;
use git_storage::{
odb::{self, write::Write as _},
signature::UserInfo,
Write,
};
use proptest::prelude::*;
use radicle_git_ext::Oid;
use git2::FileMode;
/// Represents a file in the git tree but without linking it to the repo yet
#[derive(Clone, Debug)]
pub struct File {
pub path: PathBuf,
pub inner: Vec<u8>,
pub mode: git2::FileMode,
pub oid: git2::Oid,
}
/// Represents a Tree to be written with the ODB writer.
///
/// This Tree does not have an explicit link to a repository, linking is
/// performed by writing it to the repo using the ODB writer.
///
/// Used as a replacement of git2::TreeBuilder, which is more complicated to
/// build since it requires a repository from the beginning.
#[derive(Clone, Debug)]
pub struct Tree {
builder: odb::TreeBuilder,
files: Vec<File>,
}
impl Tree {
/// Write the files to the filesystem and the repository `storage`
pub fn write(self, storage: &Write) -> Result<Oid, git2::Error> {
self.write_files(storage)?;
storage.write_tree(self.builder)
}
/// Write the files to the filesystem
pub fn write_files(&self, storage: &Write) -> Result<(), git2::Error> {
for file in &self.files {
storage.write_blob(&file.inner)?;
}
Ok(())
}
}
/// Any valid filename
pub fn trivial() -> impl Strategy<Value = String> {
"[a-zA-Z0-9]+"
}
pub fn gen_signature() -> impl Strategy<Value = UserInfo> {
trivial().prop_map(move |name| {
UserInfo {
name: name.clone(),
// TODO: is it worth to make this more realistic?
email: format!("{}@{}.com", &name, &name),
}
})
}
pub fn gen_bytes() -> impl Strategy<Value = Vec<u8>> {
any::<Vec<u8>>()
}
pub fn gen_mode() -> impl Strategy<Value = FileMode> {
prop_oneof![Just(FileMode::Blob), Just(FileMode::BlobExecutable)]
}
prop_compose! {
pub fn gen_file()
(path in trivial(),
blob in gen_bytes(),
mode in gen_mode())
-> File {
let oid = git2::Oid::hash_object(git2::ObjectType::Blob, &blob).unwrap();
File {
path: PathBuf::from(path),
inner: blob,
mode,
oid,
}
}
}
pub fn gen_file_set(max_size: u8) -> impl Strategy<Value = Vec<File>> {
prop::collection::vec(gen_file(), 0..(max_size as usize))
}
/// Generates a [`odb::TreeBuilder`] and a set of [`File`]s.
///
/// To write the resulting `Tree`, you MUST write the [`File::oid`] to the
/// repository first.
pub fn gen_tree(max_size: u8) -> impl Strategy<Value = Tree> {
gen_file_set(max_size).prop_map(move |files| {
let builder = files.iter().fold(odb::TreeBuilder::new(), |tree, file| {
tree.insert(file.path.clone(), file.oid.into(), file.mode)
});
Tree { builder, files }
})
}
-9
View File
@@ -1,9 +0,0 @@
// Copyright © 2022 The Radicle Link Contributors
// SPDX-License-Identifier: GPL-3.0-or-later
#[cfg(any(test, feature = "test"))]
pub mod gen;
#[cfg(test)]
mod properties;
#[cfg(any(test, feature = "test"))]
pub mod tmp;
-4
View File
@@ -1,4 +0,0 @@
// Copyright © 2022 The Radicle Link Contributors
// SPDX-License-Identifier: GPL-3.0-or-later
mod odb;
-108
View File
@@ -1,108 +0,0 @@
// Copyright © 2022 The Radicle Link Contributors
// SPDX-License-Identifier: GPL-3.0-or-later
use git2::ObjectType;
use git_storage::{
odb::{Read as _, Write as _},
signature::UserInfo,
};
use radicle_git_ext::ref_format::RefString;
use radicle_git_ext_test::git_ref_format::gen::valid;
use proptest::prelude::*;
use crate::{gen, tmp};
// NOTE: It's enough to check the `Oid`s. If the contents were different the
// hash would be different.
pub mod prop {
use super::*;
pub fn roundtrip_blob(user: UserInfo, bytes: &[u8]) {
let writer = tmp::writer(user);
let oid = writer.write_blob(bytes).unwrap();
let readback = writer.find_blob(oid).unwrap().unwrap();
assert_eq!(oid, readback.id().into());
let readback = writer.find_object(oid).unwrap().unwrap();
assert_eq!(readback.kind(), Some(ObjectType::Blob));
}
pub fn roundtrip_tree(user: UserInfo, tree: gen::Tree) {
let writer = tmp::writer(user);
let oid = tree.write(&writer).unwrap();
let readback = writer.find_tree(oid).unwrap().unwrap();
assert_eq!(oid, readback.id().into());
let readback = writer.find_object(oid).unwrap().unwrap();
assert_eq!(readback.kind(), Some(ObjectType::Tree));
}
pub fn roundtrip_commit(user: UserInfo, tree: gen::Tree, message: &str) {
let writer = tmp::writer(user);
let tree_oid = tree.write(&writer).unwrap();
let tree = writer.find_tree(tree_oid).unwrap().unwrap();
let commit_oid = writer.write_commit(&tree, &[], message).unwrap();
let readback = writer.find_commit(commit_oid).unwrap().unwrap();
assert_eq!(readback.id(), commit_oid.into());
let readback = writer.find_object(commit_oid).unwrap().unwrap();
assert_eq!(readback.kind(), Some(ObjectType::Commit));
}
pub fn roundtrip_tag(user: UserInfo, tree: gen::Tree, tag_name: RefString, message: &str) {
let writer = tmp::writer(user);
let tree_oid = tree.write(&writer).unwrap();
let tree = writer.find_tree(tree_oid).unwrap().unwrap();
let commit_oid = writer.write_commit(&tree, &[], message).unwrap();
let commit_object = writer.find_object(commit_oid).unwrap().unwrap();
let tag_oid = writer.write_tag(tag_name, &commit_object, message).unwrap();
let readback = writer.find_tag(tag_oid).unwrap().unwrap();
assert_eq!(tag_oid, readback.id().into());
let readback = writer.find_object(tag_oid).unwrap().unwrap();
assert_eq!(readback.kind(), Some(ObjectType::Tag));
}
}
proptest! {
#[test]
fn roundtrip_blob(user in gen::gen_signature(), bytes in gen::gen_bytes()) {
prop::roundtrip_blob(user, &bytes)
}
#[test]
fn roundtrip_tree(
user in gen::gen_signature(),
tree in gen::gen_tree(10),
) {
prop::roundtrip_tree(user, tree)
}
#[test]
fn roundtrip_commit(
user in gen::gen_signature(),
tree in gen::gen_tree(10),
message in gen::trivial()
) {
prop::roundtrip_commit(user, tree, &message)
}
#[test]
fn roundtrip_tag(
user in gen::gen_signature(),
tree in gen::gen_tree(10),
message in gen::trivial(),
tag_name in valid()
) {
let tag_name = RefString::try_from(tag_name).unwrap();
prop::roundtrip_tag(user, tree, tag_name, &message);
}
}
-14
View File
@@ -1,14 +0,0 @@
use git2::Repository;
use git_storage::{signature::UserInfo, Write};
use test_helpers::tempdir::WithTmpDir;
pub type TmpWriter = WithTmpDir<Write>;
pub type TmpRepo = WithTmpDir<Repository>;
pub fn writer(user: UserInfo) -> TmpWriter {
WithTmpDir::new(|path| {
Write::open(path, user)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))
})
.unwrap()
}
-4
View File
@@ -18,7 +18,3 @@ features = ["test"]
[dev-dependencies.radicle-surf-test]
path = "../radicle-surf/t"
features = ["test"]
[dev-dependencies.git-storage-test]
path = "../git-storage/t"
features = ["test"]