mirror of
https://seed.radicle.dev/rad:z6cFWeWpnZNHh9rUW8phgA3b5yGt.git
synced 2026-10-18 19:10:31 +08:00
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:
Generated
-26
@@ -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"
|
||||
|
||||
@@ -5,8 +5,5 @@ members = [
|
||||
"radicle-surf",
|
||||
"test",
|
||||
]
|
||||
exclude = [
|
||||
"git-storage",
|
||||
]
|
||||
resolver = "2"
|
||||
package.version = "0.18.0"
|
||||
@@ -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"
|
||||
@@ -1,5 +0,0 @@
|
||||
// Copyright © 2022 The Radicle Link Contributors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
pub mod read;
|
||||
pub mod write;
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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}"))
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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"]
|
||||
@@ -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 }
|
||||
})
|
||||
}
|
||||
@@ -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;
|
||||
@@ -1,4 +0,0 @@
|
||||
// Copyright © 2022 The Radicle Link Contributors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
mod odb;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user