From e6f36318cd016dbb8102ecd1b6141b9ad5852c44 Mon Sep 17 00:00:00 2001 From: Fintan Halpenny Date: Thu, 15 Sep 2022 16:07:17 +0100 Subject: [PATCH 1/4] git-storage: port storage from librad This provides a port of the librad::git::storage[0] API, with a focus on being more generic and decomposing writing and reading operations. It also takes heavy inspiration from link_replication::io::refdb[1] for its implementation of the transactional writes. [0]: https://github.com/radicle-dev/radicle-link/blob/master/librad/src/git/storage.rs [1]: https://github.com/radicle-dev/radicle-link/blob/master/link-replication/src/io/refdb.rs Signed-off-by: Fintan Halpenny --- Cargo.toml | 1 + git-storage/Cargo.toml | 38 ++ git-storage/src/backend.rs | 5 + git-storage/src/backend/read.rs | 176 +++++++ git-storage/src/backend/write.rs | 607 ++++++++++++++++++++++++ git-storage/src/backend/write/error.rs | 84 ++++ git-storage/src/config.rs | 225 +++++++++ git-storage/src/glob.rs | 46 ++ git-storage/src/lib.rs | 89 ++++ git-storage/src/odb.rs | 33 ++ git-storage/src/odb/read.rs | 49 ++ git-storage/src/odb/write.rs | 66 +++ git-storage/src/pool.rs | 195 ++++++++ git-storage/src/refdb.rs | 125 +++++ git-storage/src/refdb/iter.rs | 49 ++ git-storage/src/refdb/read.rs | 43 ++ git-storage/src/refdb/write.rs | 149 ++++++ git-storage/src/refdb/write/previous.rs | 132 ++++++ git-storage/src/signature.rs | 19 + 19 files changed, 2131 insertions(+) create mode 100644 git-storage/Cargo.toml create mode 100644 git-storage/src/backend.rs create mode 100644 git-storage/src/backend/read.rs create mode 100644 git-storage/src/backend/write.rs create mode 100644 git-storage/src/backend/write/error.rs create mode 100644 git-storage/src/config.rs create mode 100644 git-storage/src/glob.rs create mode 100644 git-storage/src/lib.rs create mode 100644 git-storage/src/odb.rs create mode 100644 git-storage/src/odb/read.rs create mode 100644 git-storage/src/odb/write.rs create mode 100644 git-storage/src/pool.rs create mode 100644 git-storage/src/refdb.rs create mode 100644 git-storage/src/refdb/iter.rs create mode 100644 git-storage/src/refdb/read.rs create mode 100644 git-storage/src/refdb/write.rs create mode 100644 git-storage/src/refdb/write/previous.rs create mode 100644 git-storage/src/signature.rs diff --git a/Cargo.toml b/Cargo.toml index b48773e..5716e55 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "git-ref-format", + "git-storage", "git-trailers", "link-git", "radicle-git-ext", diff --git a/git-storage/Cargo.toml b/git-storage/Cargo.toml new file mode 100644 index 0000000..1eef56b --- /dev/null +++ b/git-storage/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "git-storage" +version = "0.1.0" +authors = ["Kim Altintop ", "Fintan Halpenny "] +edition = "2021" +license = "GPL-3.0-or-later" + +[dependencies] +async-trait = "0.1" +globset = "0.4" +libc = "0.2" +parking_lot = "0.12" +thiserror = "1" +either = "1.8.0" + +[dependencies.deadpool] +version = "0.7" +default-features = false +features = ["managed"] + +[dependencies.git2] +version = "0.15.0" +default-features = false +features = ["vendored-libgit2"] + +[dependencies.git-ref-format] +path = "../git-ref-format" + +[dependencies.libgit2-sys] +version = ">= 0.12.24" +default-features = false +features = ["vendored"] + +[dependencies.radicle-git-ext] +path = "../radicle-git-ext" + +[dependencies.radicle-std-ext] +path = "../radicle-std-ext" diff --git a/git-storage/src/backend.rs b/git-storage/src/backend.rs new file mode 100644 index 0000000..01c7b45 --- /dev/null +++ b/git-storage/src/backend.rs @@ -0,0 +1,5 @@ +// Copyright © 2022 The Radicle Link Contributors +// SPDX-License-Identifier: GPL-3.0-or-later + +pub mod read; +pub mod write; diff --git a/git-storage/src/backend/read.rs b/git-storage/src/backend/read.rs new file mode 100644 index 0000000..56a38d7 --- /dev/null +++ b/git-storage/src/backend/read.rs @@ -0,0 +1,176 @@ +// Copyright © 2019-2020 The Radicle Foundation +// +// This file is part of radicle-link, distributed under the GPLv3 with Radicle +// Linking Exception. For full terms see the included LICENSE file. + +use std::{path::Path, str::FromStr}; + +use git_ext::{error::is_not_found_err, Oid}; +use std_ext::result::ResultExt as _; + +use crate::{ + config::Config, + glob, + odb::{self, Blob, Commit, Object, Tag, Tree}, + refdb::{self, Reference, References, ReferencesGlob}, +}; + +/// A read-only storage backend for accessing git's odb and refdb. +/// +/// For access to the odb see [`odb::Read`]. +/// For access to the refdb see [`refdb::Read`]. +/// +/// To construct the `Read` storage use [`Read::open`]. +pub struct Read { + pub(super) raw: git2::Repository, +} + +impl Read { + /// Open the [`Read`] storage using the `path` provided. + /// + /// The storage **must** exist already. To initialise the storage for the + /// first time see [`crate::Write::open`]. + /// + /// # Concurrency + /// + /// [`Read`] can be sent between threads, but it can't be shared between + /// threads. _Some_ operations are safe to perform concurrently in much + /// the same way two `git` processes can access the same repository. + /// However, if you need multiple [`Read`]s to be shared between + /// threads, use a [`crate::Pool`] instead. + pub fn open>(path: P) -> Result { + crate::init(); + + let raw = git2::Repository::open(path)?; + + Ok(Self { raw }) + } + + /// Read the identifier from the underlying [`Config`]. The identifier is + /// expected to exist already. + /// + /// See [`crate::config::Owner`] for more information on the identifier. + pub fn rad_identifier(&self) -> Result + where + Id: FromStr, + Id::Err: std::error::Error + Send + Sync + 'static, + { + let config = Config::try_from(&self.raw)?; + Ok(config.rad_identifier::()?) + } +} + +pub mod error { + use thiserror::Error; + + use crate::{config, refdb::error::ParseReference}; + + #[derive(Debug, Error)] + pub enum Init { + #[error(transparent)] + Git(#[from] git2::Error), + } + + #[derive(Debug, Error)] + pub enum Identifier { + #[error(transparent)] + Config(#[from] config::Error), + #[error(transparent)] + Git(#[from] git2::Error), + } + + #[derive(Debug, Error)] + pub enum FindRef { + #[error(transparent)] + Git(#[from] git2::Error), + #[error(transparent)] + Parse(#[from] ParseReference), + } +} + +// impls + +impl<'a> refdb::Read for &'a Read { + type FindRef = error::FindRef; + type FindRefs = refdb::error::Iter; + type FindRefOid = git2::Error; + + type References = References<'a>; + + fn find_reference(&self, reference: Ref) -> Result, Self::FindRef> + where + Ref: AsRef, + { + let reference = self + .raw + .find_reference(reference.as_ref().as_str()) + .map(Some) + .or_matches::(is_not_found_err, || Ok(None))?; + Ok(reference.map(Reference::try_from).transpose()?) + } + + fn find_references(&self, reference: Pat) -> Result + where + Pat: AsRef, + { + Ok(References { + inner: ReferencesGlob { + iter: self.raw.references()?, + glob: glob::RefspecMatcher::from(reference.as_ref().to_owned()), + }, + }) + } + + fn find_reference_oid(&self, reference: Ref) -> Result, Self::FindRefOid> + where + Ref: AsRef, + { + self.raw + .refname_to_id(reference.as_ref().as_str()) + .map(|oid| Some(Oid::from(oid))) + .or_matches(is_not_found_err, || Ok(None)) + } +} + +impl odb::Read for Read { + type FindObj = git2::Error; + type FindBlob = git2::Error; + type FindCommit = git2::Error; + type FindTag = git2::Error; + type FindTree = git2::Error; + + fn find_object(&self, oid: Oid) -> Result, Self::FindObj> { + self.raw + .find_object(oid.into(), None) + .map(Some) + .or_matches::(is_not_found_err, || Ok(None)) + } + + fn find_blob(&self, oid: Oid) -> Result, Self::FindBlob> { + self.raw + .find_blob(oid.into()) + .map(Some) + .or_matches(is_not_found_err, || Ok(None)) + } + + fn find_commit(&self, oid: Oid) -> Result, Self::FindCommit> { + self.raw + .find_commit(oid.into()) + .map(Some) + .or_matches(is_not_found_err, || Ok(None)) + } + + fn find_tag(&self, oid: Oid) -> Result, Self::FindTag> { + self.raw + .find_tag(oid.into()) + .map(Some) + .or_matches(is_not_found_err, || Ok(None)) + } + + fn find_tree(&self, oid: Oid) -> Result, Self::FindTree> { + self.raw + .find_tree(oid.into()) + .map(Some) + .or_matches(is_not_found_err, || Ok(None)) + } +} diff --git a/git-storage/src/backend/write.rs b/git-storage/src/backend/write.rs new file mode 100644 index 0000000..8b496d0 --- /dev/null +++ b/git-storage/src/backend/write.rs @@ -0,0 +1,607 @@ +// Copyright © 2019-2020 The Radicle Foundation +// +// This file is part of radicle-link, distributed under the GPLv3 with Radicle +// Linking Exception. For full terms see the included LICENSE file. + +use std::{ + marker::PhantomData, + path::{Path, PathBuf}, + str::FromStr, +}; + +use either::Either; + +use git_ext::{error::is_not_found_err, Oid}; +use git_ref_format::{Qualified, RefStr, RefString}; +use std_ext::Void; + +use crate::{ + config::{self, Config}, + odb, + refdb::{ + self, + resolve, + write::{previous, Applied, Policy, SymrefTarget, Update, Updated}, + Read as _, + Reference, + Target, + }, + signature::UserInfo, +}; + +use super::read::{self, Read}; + +pub mod error; + +/// A read-write storage backend for accessing git's odb and refdb. +/// +/// For read-only access to the odb see [`odb::Read`]. +/// For write access to the odb see [`odb::Write`]. +/// +/// For read-only access to the refdb see [`refdb::Read`]. +/// For write access to the refdb see [`refdb::Write`]. +/// +/// To construct the `Write` storage use [`Read::open`]. +pub struct Write { + inner: Read, + owner: Owner, + info: UserInfo, +} + +impl Write +where + O: config::Owner, + ::Err: std::error::Error + Send + Sync + 'static, +{ + /// Open the [`Write`] storage, initialising it if it doesn't exist. + /// + /// Note that a [`Write`] is tied to the [`config::Owner`] with which it was + /// initialised, attempting to open it with a different one (that is, a + /// different owner) will return an error. + /// + /// # Concurrency + /// + /// [`Write`] can be sent between threads, but it can't be shared between + /// threads. _Some_ operations are safe to perform concurrently in much + /// the same way two `git` processes can access the same repository. + /// However, if you need multiple [`Write`]s to be shared between + /// threads, use a [`crate::Pool`] instead. + pub fn open>(path: P, info: UserInfo, owner: O) -> Result { + crate::init(); + + let path = path.as_ref(); + let raw = match git2::Repository::open_bare(path) { + Err(e) if is_not_found_err(&e) => { + let mut backend = git2::Repository::init_opts( + path, + git2::RepositoryInitOptions::new() + .bare(true) + .no_reinit(true) + .external_template(false), + )?; + Config::init(&mut backend, &owner, info.clone())?; + + Ok(backend) + }, + Ok(repo) => Ok(repo), + Err(e) => Err(e), + }?; + + let config = Config::try_from(&raw)?; + let actual = config.rad_identifier::()?; + let current = owner.rad_identifier(); + + if actual != current { + return Err(error::Init::OwnerMismatch { + current: current.to_string(), + actual: actual.to_string(), + }); + } + + Ok(Self { + inner: Read { raw }, + owner, + info, + }) + } + + /// Get the underlying [`Config`] of the [`Write`] storage. + pub fn config(&self) -> Result, config::Error> { + Config::try_from(self) + } + + /// Get the read-only [`Config`] of the [`Write`] storage. + pub fn config_readonly(&self) -> Result>, git2::Error> { + Config::try_from(&self.inner.raw) + } + + pub(crate) fn config_path(&self) -> PathBuf { + config::path(&self.inner.raw) + } +} + +impl Write { + /// Return a read-only handle of the storage. + pub fn read_only(&self) -> &Read { + &self.inner + } + + /// Return the owner of the storage. + pub fn owner(&self) -> &O { + &self.owner + } + + /// 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, S> refdb::Read for &'a Write { + type FindRef = <&'a Read as refdb::Read>::FindRef; + type FindRefs = <&'a Read as refdb::Read>::FindRefs; + type FindRefOid = <&'a Read as refdb::Read>::FindRefOid; + + type References = <&'a Read as refdb::Read>::References; + + fn find_reference(&self, reference: Ref) -> Result, Self::FindRef> + where + Ref: AsRef, + { + self.read_only().find_reference(reference) + } + + fn find_references(&self, reference: Pat) -> Result + where + Pat: AsRef, + { + self.read_only().find_references(reference) + } + + fn find_reference_oid(&self, reference: Ref) -> Result, Self::FindRefOid> + where + Ref: AsRef, + { + self.read_only().find_reference_oid(reference) + } +} + +impl<'a, S> refdb::Write for &'a Write { + type UpdateError = error::Update; + + fn update<'b, U>(&mut self, updates: U) -> Result, Self::UpdateError> + where + U: IntoIterator>, + { + let mut refdb = Transaction::new(self)?; + let mut applied = Applied::default(); + for up in updates.into_iter() { + match up { + Update::Direct { + name, + target, + no_ff, + previous, + reflog, + } => match refdb.direct(name, target, no_ff, previous, reflog)? { + Either::Left(update) => applied.rejected.push(update), + Either::Right(updated) => applied.updated.push(updated), + }, + Update::Symbolic { + name, + target, + type_change, + previous, + reflog, + } => match refdb.symbolic(name, target, type_change, previous, reflog)? { + Either::Left(update) => applied.rejected.push(update), + Either::Right(updated) => applied.updated.extend(updated), + }, + Update::Remove { name, previous } => match refdb.remove(name, previous)? { + Either::Left(update) => applied.rejected.push(update), + Either::Right(updated) => applied.updated.push(updated), + }, + } + } + refdb.commit()?; + + Ok(applied) + } +} + +/// An internal struct combining a [`Write`] and [`git2::Transaction`]. +// TODO: include optional namespace +struct Transaction<'a, O> { + refdb: &'a Write, + txn: git2::Transaction<'a>, +} + +impl<'a, S> Transaction<'a, S> { + pub fn new(refdb: &'a Write) -> Result { + let txn = refdb.inner.raw.transaction()?; + Ok(Self { refdb, txn }) + } + + /// Perform an [`Update::Direct`] within the [`Transaction`]. + /// + /// Steps: + /// 1. Get the state of the reference + /// a. if it did not exist create the source and destination references, + /// skip the next steps. + /// b. if it did, then follow the next steps. + /// + /// 2. Guard against the [`previous::Edit`] value, if this fails then reject + /// the [`Update`]. + /// + /// 3. Check the fast-forward policy, either aborting, + /// rejecting, or accepting the update + pub fn direct<'b>( + &mut self, + name: Qualified<'b>, + target: Oid, + no_ff: Policy, + previous: previous::Edit, + reflog: String, + ) -> Result, Updated>, error::Update> { + let prev = self.refdb.find_reference(&name)?; + let given = prev + .as_ref() + .map(|prev| resolve(self.refdb.as_raw(), prev)) + .transpose()?; + if let Err(_err) = previous.guard(given) { + return Ok(Either::Left(Update::Direct { + name, + target, + no_ff, + previous, + reflog, + })); + } + + let not_ff = match given { + Some(prev) => { + if !self.is_ff(&name, target, prev)? { + Some(prev) + } else { + None + } + }, + None => None, + }; + + match not_ff { + // It wasn't an fast-forward so we check our policy + Some(cur) => match no_ff { + Policy::Abort => Err(error::Update::NonFF { + name: name.into(), + new: target, + cur, + }), + Policy::Reject => Ok(Either::Left(Update::Direct { + name, + target, + no_ff, + previous, + reflog, + })), + Policy::Allow => Ok(Either::Right(self.direct_edit(&name, target, &reflog)?)), + }, + // It was an fast-forward so we go ahead and make the edit + None => Ok(Either::Right(self.direct_edit(&name, target, &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>( + &mut self, + name: Qualified<'b>, + target: SymrefTarget<'b>, + type_change: Policy, + previous: previous::Edit, + reflog: String, + ) -> Result, Vec>, error::Update> { + let src = self.refdb.find_reference(&name)?; + 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, &reflog, is_ff)?, + )) + }, + Target::Symbolic { .. } => Err(error::Update::TargetSymbolic(dst.name)), + }, + None => Ok(Either::Right( + self.symbolic_edit(name, target, &reflog, true)?, + )), + } + }, + }, + None => Ok(Either::Right( + self.symbolic_edit(name, target, &reflog, true)?, + )), + } + } + + /// Perform an [`Update::Remove`] within the [`Transaction`]. + /// + /// Steps: + /// 1. Get the state of the reference + /// + /// 2. Guard against the [`previous::Remove`] value, if this fails then + /// reject the [`Update`]. + /// + /// 3. Remove the reference + /// + /// # Panics + /// + /// The `previous` SHOULD guard against the reference not existing, so this + /// will panic if the previous reference was missing AND passed the + /// `previous::guard`. + pub fn remove<'b>( + &mut self, + name: Qualified<'b>, + previous: previous::Remove, + ) -> Result, Updated>, error::Update> { + let prev = self.refdb.find_reference(&name)?; + let given = prev + .as_ref() + .map(|prev| resolve(self.refdb.as_raw(), prev)) + .transpose()?; + if let Err(_err) = previous.guard(given) { + Ok(Either::Left(Update::Remove { name, previous })) + } else { + match given { + None => { + panic!("BUG: the previous value for a reference to be removed was not given, but its existence SHOULD be guarded") + }, + Some(previous) => Ok(Either::Right(self.remove_edit(name, previous)?)), + } + } + } + + pub fn lock(&mut self, reference: R) -> Result<(), error::Transaction> + where + R: AsRef, + { + let reference = reference.as_ref(); + self.txn + .lock_ref(reference.as_str()) + .map_err(|err| error::Transaction::Lock { + reference: reference.to_owned(), + source: err, + }) + } + + pub fn commit(self) -> Result<(), error::Transaction> { + self.txn + .commit() + .map_err(|err| error::Transaction::Commit { source: err }) + } + + pub fn direct_edit( + &mut self, + reference: R, + target: Oid, + reflog: &str, + ) -> Result + where + R: AsRef, + { + let reference = reference.as_ref(); + self.lock(reference)?; + let info = self.refdb.info(); + let sig = info + .signature() + .map_err(|err| error::Transaction::Signature { + name: info.name.to_owned(), + email: info.email.to_owned(), + source: err, + })?; + self.txn + .set_target(reference.as_str(), target.into(), Some(&sig), reflog) + .map_err(|err| error::Transaction::SetDirect { + reference: reference.to_owned(), + target, + source: err, + })?; + + Ok(Updated::Direct { + name: reference.to_owned(), + target, + }) + } + + pub fn symbolic_edit( + &mut self, + reference: R, + target: SymrefTarget, + reflog: &str, + is_ff: bool, + ) -> Result, error::Transaction> + where + R: AsRef, + { + let reference = reference.as_ref(); + self.lock(reference)?; + self.lock(&target.name)?; + + let SymrefTarget { name: dst, target } = 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, target, 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(), + }); + Ok(edits) + } + + pub fn remove_edit( + &mut self, + reference: R, + previous: Oid, + ) -> Result + where + R: AsRef, + { + let reference = reference.as_ref(); + self.lock(reference)?; + self.txn + .remove(reference.as_str()) + .map_err(|err| error::Transaction::Remove { + reference: reference.to_owned(), + source: err, + })?; + Ok(Updated::Removed { + name: reference.to_owned(), + previous, + }) + } + + fn is_ff(&self, name: R, target: Oid, prev: Oid) -> Result + where + R: AsRef, + { + self.refdb + .inner + .raw + .graph_descendant_of(target.into(), prev.into()) + .map_err(|err| error::Transaction::Ancestry { + name: name.as_ref().to_owned(), + new: target, + old: prev, + source: err, + }) + } +} + +// odb impls + +impl odb::Read for Write { + type FindObj = ::FindObj; + type FindBlob = ::FindBlob; + type FindCommit = ::FindCommit; + type FindTag = ::FindTag; + type FindTree = ::FindTree; + + fn find_object(&self, oid: Oid) -> Result, Self::FindObj> { + self.read_only().find_object(oid) + } + + fn find_blob(&self, oid: Oid) -> Result, Self::FindBlob> { + self.read_only().find_blob(oid) + } + + fn find_commit(&self, oid: Oid) -> Result, Self::FindCommit> { + self.read_only().find_commit(oid) + } + + fn find_tag(&self, oid: Oid) -> Result, Self::FindTag> { + self.read_only().find_tag(oid) + } + + fn find_tree(&self, oid: Oid) -> Result, Self::FindTree> { + self.read_only().find_tree(oid) + } +} + +impl odb::Write for Write { + type WriteBlob = git2::Error; + type WriteCommit = git2::Error; + type WriteTag = git2::Error; + type WriteTree = git2::Error; + + fn write_blob(&self, data: &[u8]) -> Result { + self.as_raw().blob(data).map(Oid::from) + } + + fn write_commit<'b>( + &self, + tree: &odb::Tree, + parents: &[&odb::Commit<'b>], + message: &str, + ) -> Result { + let author = self.info.signature()?; + self.as_raw() + .commit(None, &author, &author, message, tree, parents) + .map(Oid::from) + } + + fn write_tag( + &self, + name: R, + target: &odb::Object, + message: &str, + ) -> Result + where + R: AsRef, + { + let tagger = self.info.signature()?; + self.as_raw() + .tag_annotation_create(name.as_ref().as_str(), target, &tagger, message) + .map(Oid::from) + } + + fn write_tree(&self, builder: git2::TreeBuilder) -> Result { + builder.write().map(Oid::from) + } +} diff --git a/git-storage/src/backend/write/error.rs b/git-storage/src/backend/write/error.rs new file mode 100644 index 0000000..7f72843 --- /dev/null +++ b/git-storage/src/backend/write/error.rs @@ -0,0 +1,84 @@ +// Copyright © 2019-2020 The Radicle Foundation +// +// This file is part of radicle-link, distributed under the GPLv3 with Radicle +// Linking Exception. For full terms see the included LICENSE file. + +use thiserror::Error; + +use super::*; + +#[derive(Debug, Error)] +pub enum Init { + #[error(transparent)] + Config(#[from] config::Error), + #[error(transparent)] + Git(#[from] git2::Error), + #[error("the current identifier '{current}' does not match the identifier used to create the storage '{actual}'")] + OwnerMismatch { current: String, actual: String }, +} + +#[derive(Debug, Error)] +pub enum Transaction { + #[error("error determining if {old} is an ancestor of {new} in within {name}")] + Ancestry { + name: RefString, + new: Oid, + old: Oid, + #[source] + source: git2::Error, + }, + #[error("error committing update for storage")] + Commit { + #[source] + source: git2::Error, + }, + #[error("error locking reference '{reference}' when attempting to update storage")] + Lock { + reference: RefString, + #[source] + source: git2::Error, + }, + #[error("error obtaining signature for '{name}' '{email}'")] + Signature { + name: String, + email: String, + #[source] + source: git2::Error, + }, + #[error("error setting the direct reference '{reference}' to the target '{target}'")] + SetDirect { + reference: RefString, + target: Oid, + #[source] + source: git2::Error, + }, + #[error("error setting the symbolic reference '{reference}' to the target '{target}'")] + SetSymbolic { + reference: RefString, + target: RefString, + #[source] + source: git2::Error, + }, + #[error("error removing the reference '{reference}'")] + Remove { + reference: RefString, + #[source] + source: git2::Error, + }, +} + +#[derive(Debug, Error)] +pub enum Update { + #[error("non-fast-forward update of {name} (current: {cur}, new: {new})")] + NonFF { name: RefString, new: Oid, cur: Oid }, + #[error(transparent)] + FindRef(#[from] read::error::FindRef), + #[error(transparent)] + Git(#[from] git2::Error), + #[error("the symref target {0} is itself a symref")] + TargetSymbolic(RefString), + #[error(transparent)] + Transaction(#[from] Transaction), + #[error("rejected changing type of reference '{0}' from symbolic to direct")] + TypeChange(RefString), +} diff --git a/git-storage/src/config.rs b/git-storage/src/config.rs new file mode 100644 index 0000000..f4c1340 --- /dev/null +++ b/git-storage/src/config.rs @@ -0,0 +1,225 @@ +// Copyright © 2019-2020 The Radicle Foundation +// +// This file is part of radicle-link, distributed under the GPLv3 with Radicle +// Linking Exception. For full terms see the included LICENSE file. + +//! The [git config][config] for a particular storage. +//! +//! As well as encapsulating the git config, it also holds the [`Owner`] of the +//! config. +//! +//! A [`Config`] can either be constructed by it's [`Config::init`] constuctor +//! or by its `TryFrom` instances for [`git2::Repository`] and [`Write`]. +//! +//! [config]: https://git-scm.com/docs/git-config + +use std::{convert::TryFrom, marker::PhantomData, path::PathBuf, str::FromStr}; + +use git_ext::is_not_found_err; +use std_ext::prelude::*; +use thiserror::Error; + +use crate::{signature::UserInfo, Write}; + +const CONFIG_USER_NAME: &str = "user.name"; +const CONFIG_USER_EMAIL: &str = "user.email"; +const CONFIG_RAD_ID: &str = "rad.identifier"; + +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum Error { + #[error("storage was already initialised with identifier {0}")] + AlreadyInitialised(String), + + #[error("could not parse the identifier found at 'rad.self' in the storage config")] + Identifier(#[source] Box), + + #[error("could not parse the identifier found at 'rad.peerid' in the storage config")] + RadSelf(#[source] Box), + + #[error(transparent)] + Git(#[from] git2::Error), +} + +/// The `RadIdentifier` is expected to uniquely identify the owner of this +/// repository. The value should be unique and should only be able to be +/// constructed by the owner. For example, this could be a signing key unique to +/// the owner. +pub trait Owner { + type RadIdentifier: ToString + FromStr + PartialEq; + + fn rad_identifier(&self) -> Self::RadIdentifier; +} + +/// The _local_ config for the give [`git2::Repository`]. +/// +/// This is typically `$GIT_DIR/.git/config` for non-bare, and `$GIT_DIR/config` +/// for bare repositories. +pub fn path(repo: &git2::Repository) -> PathBuf { + repo.path().join("config") +} + +/// A git config paired with the owner information. +/// +/// The config is initialised with the following config keys: +/// +/// * `user.name`: the provided name of the user. +/// * `user.email`: the provided email of the user. This does not necessarily +/// have to be a valid email, see [`UserInfo`]. +/// * `rad.identifier`: the unqiue value identifying the owner of this +/// `Config`. +/// +/// # Constructors +/// +/// * [`Config::init`] +/// * `TryFrom<&'a Write>` +/// * `TryFrom { + inner: git2::Config, + owner: &'a O, + info: UserInfo, +} + +impl<'a, O> TryFrom<&'a Write> for Config<'a, O> +where + O: Owner, + ::Err: std::error::Error + Send + Sync + 'static, +{ + type Error = Error; + + fn try_from(storage: &'a Write) -> Result { + let inner = git2::Config::open(&storage.config_path())?; + let mut this = Self { + inner, + owner: storage.owner(), + info: storage.info().clone(), + }; + this.guard_key_change()?; + this.ensure_reflog()?; + + Ok(this) + } +} + +impl TryFrom<&git2::Repository> for Config<'_, PhantomData> { + type Error = git2::Error; + + fn try_from(repo: &git2::Repository) -> Result { + let inner = git2::Config::open(&self::path(repo))?.snapshot()?; + let name = inner.get_string(CONFIG_USER_NAME)?; + let email = inner.get_string(CONFIG_USER_EMAIL)?; + Ok(Self { + inner, + owner: &PhantomData, + info: UserInfo { name, email }, + }) + } +} + +impl<'a, O> Config<'a, O> +where + O: Owner, + ::Err: std::error::Error + Send + Sync + 'static, +{ + fn guard_key_change(&self) -> Result<(), Error> { + let configured_identifier = self + .rad_identifier::() + .map(Some) + .or_matches::( + |err| matches!(err, Error::Git(e) if is_not_found_err(e)), + || Ok(None), + )?; + let owner_identifier = self.owner.rad_identifier(); + match configured_identifier { + Some(initialised_with) if initialised_with != owner_identifier => { + Err(Error::AlreadyInitialised(initialised_with.to_string())) + }, + + _ => Ok(()), + } + } + + fn ensure_reflog(&mut self) -> Result<(), Error> { + if let Err(e) = self.inner.get_bool("core.logAllRefUpdates") { + return if is_not_found_err(&e) { + Ok(self.inner.set_bool("core.logAllRefUpdates", true)?) + } else { + Err(e.into()) + }; + } + + Ok(()) + } + + /// Initialise the [`Config`] with the given `owner` and `info`. + /// + /// # Errors + /// + /// * The identifier of the `owner` differs from the existing identifier + /// stored + pub fn init(repo: &mut git2::Repository, owner: &'a O, info: UserInfo) -> Result { + let identifier = owner.rad_identifier(); + let config = git2::Config::open(&self::path(repo))?; + let mut this = Config { + inner: config, + owner, + info, + }; + this.guard_key_change()?; + this.ensure_reflog()?; + this.set_identifier(identifier)?; + this.set_user_info()?; + + Ok(this) + } + + fn set_user_info(&mut self) -> Result<(), Error> { + self.inner.set_str(CONFIG_USER_NAME, &self.info.name)?; + self.inner.set_str(CONFIG_USER_EMAIL, &self.info.email)?; + + Ok(()) + } + + fn set_identifier(&mut self, rad_identifier: O::RadIdentifier) -> Result<(), Error> { + self.inner + .set_str(CONFIG_RAD_ID, &rad_identifier.to_string()) + .map_err(Error::from) + } +} + +impl Config<'_, O> { + pub fn user(&self) -> &UserInfo { + &self.info + } + + pub fn user_name(&self) -> Result { + self.inner.get_string(CONFIG_USER_NAME).map_err(Error::from) + } + + pub fn user_email(&self) -> Result { + self.inner + .get_string(CONFIG_USER_EMAIL) + .map_err(Error::from) + } + + pub fn rad_identifier(&self) -> Result + where + Id: FromStr, + Id::Err: std::error::Error + Send + Sync + 'static, + { + self.inner + .get_string(CONFIG_RAD_ID) + .map_err(Error::from) + .and_then(|peer_id| { + peer_id + .parse() + .map_err(|err| Error::Identifier(Box::new(err))) + }) + } +} + +impl Config<'_, PhantomData> { + pub fn readonly(repo: &git2::Repository) -> Result { + Self::try_from(repo) + } +} diff --git a/git-storage/src/glob.rs b/git-storage/src/glob.rs new file mode 100644 index 0000000..8989417 --- /dev/null +++ b/git-storage/src/glob.rs @@ -0,0 +1,46 @@ +// Copyright © 2019-2020 The Radicle Foundation +// +// This file is part of radicle-link, distributed under the GPLv3 with Radicle +// Linking Exception. For full terms see the included LICENSE file. + +use std::path::Path; + +use git_ext as ext; +use git_ref_format::refspec::PatternString; + +pub trait Pattern { + fn matches>(&self, path: P) -> bool; +} + +impl Pattern for globset::GlobMatcher { + fn matches>(&self, path: P) -> bool { + self.is_match(path) + } +} + +impl Pattern for globset::GlobSet { + fn matches>(&self, path: P) -> bool { + self.is_match(path) + } +} + +#[derive(Clone, Debug)] +pub struct RefspecMatcher(globset::GlobMatcher); + +impl From for RefspecMatcher { + fn from(pat: ext::RefspecPattern) -> Self { + Self(globset::Glob::new(pat.as_str()).unwrap().compile_matcher()) + } +} + +impl From for RefspecMatcher { + fn from(pat: PatternString) -> Self { + Self(globset::Glob::new(pat.as_str()).unwrap().compile_matcher()) + } +} + +impl Pattern for RefspecMatcher { + fn matches>(&self, path: P) -> bool { + self.0.is_match(path) + } +} diff --git a/git-storage/src/lib.rs b/git-storage/src/lib.rs new file mode 100644 index 0000000..f796a9f --- /dev/null +++ b/git-storage/src/lib.rs @@ -0,0 +1,89 @@ +// 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], in +//! a Radicle context. The particular difference for Radicle is that a storage +//! must be *owned*, generally using a signing key. +//! +//! To first initialise the storage use [`Write::open`]. This will intialise the +//! underlying git repository and its [`config`]. +//! +//! After the storage is initialised, use [`Write::open`] or [`Read::open`] for +//! read-write or read-only access to the underlying storage. These structs will +//! implement the traits below depending on their access levels. +//! +//! ## Read-only access +//! +//! * [`refdb::Read`] +//! * [`odb::Read`] +//! +//! ## Read-write access +//! +//! * [`refdb::Read`] +//! * [`refdb::Write`] +//! * [`odb::Read`] +//! * [`odb::Write`] +//! +//! ## Concurrency +//! +//! +//! [`Read`] and [`Write`] can be sent between threads, but it can't be shared +//! between threads. _Some_ operations are safe to perform concurrently in much +//! the same way two `git` processes can access the same repository. +//! However, if you need multiple [`Read`]/[`Write`]s to be shared between +//! threads, use a [`Pool`] instead. +//! +//! [refs]: https://git-scm.com/book/en/v2/Git-Internals-Git-References +//! [objs]: https://git-scm.com/book/en/v2/Git-Internals-Git-Objects + +#[macro_use] +extern crate async_trait; + +extern crate radicle_git_ext as git_ext; +extern crate radicle_std_ext as std_ext; + +pub mod config; +pub mod glob; + +pub mod pool; +pub use pool::Pool; + +pub mod refdb; +pub use refdb::{Applied, Reference, SymrefTarget, Target, Update, Updated}; + +pub mod odb; +pub use odb::{Blob, Commit, Object, Tag, Tree}; + +mod backend; +pub use backend::{ + read::{self, Read}, + write::{self, Write}, +}; + +pub mod signature; + +/// Initialise the git backend. +/// +/// **SHOULD** be called before all accesses to git functionality. +pub fn init() { + use libc::c_int; + use libgit2_sys as raw_git; + use std::sync::Once; + + static INIT: Once = Once::new(); + + unsafe { + INIT.call_once(|| { + let ret = + raw_git::git_libgit2_opts(raw_git::GIT_OPT_SET_MWINDOW_FILE_LIMIT as c_int, 256); + if ret < 0 { + panic!( + "error setting libgit2 option: {}", + git2::Error::last_error(ret).unwrap() + ) + } + }) + } +} diff --git a/git-storage/src/odb.rs b/git-storage/src/odb.rs new file mode 100644 index 0000000..ed15052 --- /dev/null +++ b/git-storage/src/odb.rs @@ -0,0 +1,33 @@ +// Copyright © 2022 The Radicle Link Contributors +// SPDX-License-Identifier: GPL-3.0-or-later + +//! The `odb` is separated into two traits: [`Read`] and [`Write`], providing +//! access to [git objects][objs]. +//! +//! The [`Read`] trait provides functions for read-only access to the odb. +//! The [`Write`] trait provides functions for read and write access to the +//! odb, thus it implies the [`Read`] trait. +//! +//! The reason for separating these types of actions out is that one can infer +//! what kind of access a function has to the odb by looking at which trait it +//! is using. +//! +//! For implementations of these traits, this crate provides [`crate::Read`] and +//! [`crate::Write`] structs. +//! +//! [objs]: https://git-scm.com/book/en/v2/Git-Internals-Git-Objects + +// TODO: this doesn't abstract over the git2 types very well, but it's too much +// hassle to massage that right now. + +use std::error::Error; + +pub use git2::{Blob, Commit, Object, Tag, Tree}; + +use git_ext::Oid; + +pub mod read; +pub use read::Read; + +pub mod write; +pub use write::Write; diff --git a/git-storage/src/odb/read.rs b/git-storage/src/odb/read.rs new file mode 100644 index 0000000..a878165 --- /dev/null +++ b/git-storage/src/odb/read.rs @@ -0,0 +1,49 @@ +// Copyright © 2022 The Radicle Link Contributors +// SPDX-License-Identifier: GPL-3.0-or-later + +use super::*; + +/// Read-only access to a git refdb. +/// +/// See [`crate::Read`] for an implementation of this trait. +pub trait Read { + /// The error type for finding an object in the refdb. + type FindObj: Error + Send + Sync + 'static; + + /// The error type for finding a blob in the refdb. + type FindBlob: Error + Send + Sync + 'static; + + /// The error type for finding a commit in the refdb. + type FindCommit: Error + Send + Sync + 'static; + + /// The error type for finding a tag in the refdb. + type FindTag: Error + Send + Sync + 'static; + + /// The error type for finding a tree in the refdb. + type FindTree: Error + Send + Sync + 'static; + + /// Find the [`Object`] corresponding to the given `oid`. + /// + /// Returns `None` if the [`Object`] did not exist. + fn find_object(&self, oid: Oid) -> Result, Self::FindObj>; + + /// Find the [`Blob`] corresponding to the given `oid`. + /// + /// Returns `None` if the [`Blob`] did not exist. + fn find_blob(&self, oid: Oid) -> Result, Self::FindBlob>; + + /// Find the [`Commit`] corresponding to the given `oid`. + /// + /// Returns `None` if the [`Commit`] did not exist. + fn find_commit(&self, oid: Oid) -> Result, Self::FindCommit>; + + /// Find the [`Tag`] corresponding to the given `oid`. + /// + /// Returns `None` if the [`Tag`] did not exist. + fn find_tag(&self, oid: Oid) -> Result, Self::FindTag>; + + /// Find the [`Object`] corresponding to the given `oid`. + /// + /// Returns `None` if the [`Tag`] did not exist. + fn find_tree(&self, oid: Oid) -> Result, Self::FindTree>; +} diff --git a/git-storage/src/odb/write.rs b/git-storage/src/odb/write.rs new file mode 100644 index 0000000..6122d96 --- /dev/null +++ b/git-storage/src/odb/write.rs @@ -0,0 +1,66 @@ +// Copyright © 2022 The Radicle Link Contributors +// SPDX-License-Identifier: GPL-3.0-or-later + +use std::error::Error; + +use git_ext::Oid; +use git_ref_format::RefStr; + +use super::{Commit, Object, Read, Tree}; + +/// Read-write access to a git odb. +/// +/// See [`crate::Write`] for an implementation of this trait. +pub trait Write: Read { + /// The error type for writing a blob to the odb. + type WriteBlob: Error + Send + Sync + 'static; + + /// The error type for writing a commit to the odb. + type WriteCommit: Error + Send + Sync + 'static; + + /// The error type for writing a tag to the odb. + type WriteTag: Error + Send + Sync + 'static; + + /// The error type for writing a tree to the odb. + type WriteTree: Error + Send + Sync + 'static; + + /// Write a [`super::Blob`] containing the `data` provided. + fn write_blob(&self, data: &[u8]) -> Result; + + /// Write a [`Commit`] that points to the given `tree` and has the provided + /// `parents`. + /// + /// The signature of the [`Commit`] is expected to be provided by the + /// implementor of the trait. + /// + /// The commit will not be associated with any reference. If this is + /// required then you can use the [`Oid`] as the target for a + /// [`crate::refdb::Update`]. + fn write_commit<'a>( + &self, + tree: &Tree, + parents: &[&Commit<'a>], + message: &str, + ) -> Result; + + /// Write a [`super::Tag`] that points to the given `target`. + /// + /// The signature of the [`super::Tag`] is expected to be provided by the + /// implementor of the trait. + /// + /// No reference is created, however, the `name` is used for naming the + /// [`super::Tag`] object. + /// + /// If a reference is required then you can use the [`Oid`] as the target + /// for a [`crate::refdb::Update`]. + fn write_tag(&self, name: R, target: &Object, message: &str) -> Result + where + R: AsRef; + + /// Write a [`super::Tree`] using the provided `builder`. + // XXX: It's annoying that this exposes git2 but the effort of abstracting the + // tree builder is too high + // TODO: instead of passing the builder, we could use an enum of operations to + // modify a builder + fn write_tree(&self, builder: git2::TreeBuilder) -> Result; +} diff --git a/git-storage/src/pool.rs b/git-storage/src/pool.rs new file mode 100644 index 0000000..05b88bc --- /dev/null +++ b/git-storage/src/pool.rs @@ -0,0 +1,195 @@ +// Copyright © 2019-2020 The Radicle Foundation +// +// This file is part of radicle-link, distributed under the GPLv3 with Radicle +// Linking Exception. For full terms see the included LICENSE file. + +use std::{ + marker::PhantomData, + ops::{Deref, DerefMut}, + path::PathBuf, + str::FromStr, + sync::Arc, +}; + +use deadpool::managed::{self, Manager, Object, RecycleResult}; +use parking_lot::RwLock; +use std_ext::Void; +use thiserror::Error; + +use crate::{config, read, signature::UserInfo, write, Read, Write}; + +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum InitError { + #[error(transparent)] + Read(#[from] read::error::Init), + + #[error(transparent)] + Write(#[from] write::error::Init), +} + +pub type Pool = deadpool::managed::Pool; +pub type PoolError = managed::PoolError; + +#[async_trait] +pub trait Pooled { + async fn get(&self) -> Result, PoolError>; +} + +#[async_trait] +impl Pooled for Pool { + async fn get(&self) -> Result, PoolError> { + self.get().await.map(PooledRef::from) + } +} + +/// A reference to a pooled storage. +/// +/// The `S` parameter can be filled by [`Write`] for read-write access or +/// [`Read`] for read-only access. +pub struct PooledRef(Object); + +impl Deref for PooledRef { + type Target = S; + + fn deref(&self) -> &Self::Target { + self.0.deref() + } +} + +impl DerefMut for PooledRef { + fn deref_mut(&mut self) -> &mut Self::Target { + self.0.deref_mut() + } +} + +impl AsRef for PooledRef { + fn as_ref(&self) -> &S { + self + } +} + +impl AsMut for PooledRef { + fn as_mut(&mut self) -> &mut S { + self + } +} + +impl AsRef for PooledRef> +where + S: config::Owner, + ::Err: std::error::Error + Send + Sync + 'static, +{ + fn as_ref(&self) -> &Read { + self.0.read_only() + } +} + +impl From> for PooledRef { + fn from(obj: Object) -> Self { + Self(obj) + } +} + +#[derive(Clone)] +pub struct Initialised(Arc>); + +impl Initialised { + pub fn no() -> Self { + Self(Arc::new(RwLock::new(false))) + } +} + +pub struct Writer { + signer: S, + init: Initialised, +} + +#[derive(Clone)] +pub struct Config { + root: PathBuf, + info: UserInfo, + write: W, +} + +pub type ReadConfig = Config>; +pub type WriteConfig = Config>; + +impl ReadConfig { + pub fn new(root: PathBuf, info: UserInfo) -> Self { + Config { + root, + info, + write: PhantomData, + } + } + + pub fn write(self, signer: S, init: Initialised) -> WriteConfig { + Config { + root: self.root, + info: self.info, + write: Writer { signer, init }, + } + } +} + +#[async_trait] +impl Manager for ReadConfig { + async fn create(&self) -> Result { + Read::open(&self.root).map_err(InitError::from) + } + + async fn recycle(&self, _: &mut Read) -> RecycleResult { + Ok(()) + } +} + +impl WriteConfig +where + S: config::Owner, + ::Err: std::error::Error + Send + Sync + 'static, +{ + pub fn new(root: PathBuf, info: UserInfo, signer: S, init: Initialised) -> Self { + Self { + root, + info, + write: Writer { signer, init }, + } + } + + fn mk_storage(&self) -> Result, InitError> + where + S: Clone, + { + Write::open(&self.root, self.info.clone(), self.write.signer.clone()) + .map_err(InitError::from) + } +} + +#[async_trait] +impl Manager, InitError> for WriteConfig +where + S: Clone + Send + Sync + 'static, + S: config::Owner, + ::Err: std::error::Error + Send + Sync + 'static, +{ + async fn create(&self) -> Result, InitError> { + let initialised = self.write.init.0.read(); + if *initialised { + self.mk_storage() + } else { + drop(initialised); + let mut initialised = self.write.init.0.write(); + self.mk_storage() + .map(|storage| { + *initialised = true; + storage + }) + .map_err(InitError::from) + } + } + + async fn recycle(&self, _: &mut Write) -> RecycleResult { + Ok(()) + } +} diff --git a/git-storage/src/refdb.rs b/git-storage/src/refdb.rs new file mode 100644 index 0000000..cdb01ec --- /dev/null +++ b/git-storage/src/refdb.rs @@ -0,0 +1,125 @@ +// Copyright © 2022 The Radicle Link Contributors +// SPDX-License-Identifier: GPL-3.0-or-later + +//! The `refdb` is separated into two traits: [`Read`] and [`Write`], providing +//! access to [git references][refs]. +//! +//! The [`Read`] trait provides functions for read-only access to the refdb. +//! The [`Write`] trait provides functions for read and write access to the +//! refdb, thus it implies the [`Read`] trait. +//! +//! The reason for separating these types of actions out is that one can infer +//! what kind of access a function has to the refdb by looking at which trait it +//! is using. +//! +//! For implementations of these traits, this crate provides [`crate::Read`] and +//! [`crate::Write`] structs. +//! +//! [refs]: https://git-scm.com/book/en/v2/Git-Internals-Git-References + +use std::fmt::Debug; + +use git_ext::Oid; +use git_ref_format::RefString; + +pub mod iter; +pub use iter::{References, ReferencesGlob}; + +pub mod read; +pub use read::Read; + +pub mod write; +pub use write::{previous, Applied, Policy, SymrefTarget, Update, Updated, Write}; + +/// A read-only structure representing a git reference. +/// +/// References can be constructed by using the [`Read`] functions: +/// * [`Read::find_reference`] +/// * [`Read::find_references`] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Reference { + /// The name of the reference. + pub name: RefString, + /// The target of the reference. + pub target: Target, +} + +impl<'a> TryFrom> for Reference { + type Error = error::ParseReference; + + fn try_from(value: git2::Reference) -> Result { + use error::ParseReference; + + let name = value + .name() + .ok_or(ParseReference::InvalidUtf8) + .and_then(|name| RefString::try_from(name).map_err(ParseReference::from))?; + let target = match value.target() { + None => { + let name = value + .symbolic_target() + .ok_or(ParseReference::InvalidUtf8) + .and_then(|name| RefString::try_from(name).map_err(ParseReference::from))?; + name.into() + }, + Some(oid) => oid.into(), + }; + + Ok(Reference { name, target }) + } +} + +/// The target a reference points to. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Target { + /// The reference points directly at an `oid`. + Direct { oid: Oid }, + /// The reference is symbolic and points to another reference. + Symbolic { name: RefString }, +} + +impl From for Target { + fn from(oid: Oid) -> Self { + Self::Direct { oid } + } +} + +impl From for Target { + fn from(oid: git2::Oid) -> Self { + Oid::from(oid).into() + } +} + +impl From for Target { + fn from(name: RefString) -> Self { + Self::Symbolic { name } + } +} + +pub mod error { + use thiserror::Error; + + #[derive(Debug, Error)] + pub enum ParseReference { + #[error("reference name did contain valid UTF-8 bytes")] + InvalidUtf8, + #[error(transparent)] + RefString(#[from] git_ref_format::Error), + } + + #[derive(Debug, Error)] + pub enum Iter { + #[error(transparent)] + Git(#[from] git2::Error), + #[error(transparent)] + Parse(#[from] ParseReference), + } +} + +/// Utility function for resolving a [`Reference`] to its [`Oid`] +pub(crate) fn resolve(repo: &git2::Repository, r: &Reference) -> Result { + match &r.target { + Target::Direct { oid } => Ok(*oid), + Target::Symbolic { name } => repo.refname_to_id(name.as_str()).map(Oid::from), + } +} diff --git a/git-storage/src/refdb/iter.rs b/git-storage/src/refdb/iter.rs new file mode 100644 index 0000000..189a7cb --- /dev/null +++ b/git-storage/src/refdb/iter.rs @@ -0,0 +1,49 @@ +// Copyright © 2022 The Radicle Link Contributors +// SPDX-License-Identifier: GPL-3.0-or-later + +//! Iterator adaptors over [`git2::References`]. + +use std::fmt::Debug; + +use crate::glob; + +use super::{error, Reference}; + +/// Iterator for [`Reference`]s where the inner [`glob::Pattern`] supplied +/// filters out any non-matching reference names. +pub struct References<'a> { + pub(crate) inner: ReferencesGlob<'a, glob::RefspecMatcher>, +} + +impl<'a> Iterator for References<'a> { + type Item = Result; + + fn next(&mut self) -> Option { + self.inner.next() + } +} + +pub struct ReferencesGlob<'a, G: glob::Pattern + Debug> { + pub(crate) iter: git2::References<'a>, + pub(crate) glob: G, +} + +impl<'a, G: glob::Pattern + Debug> Iterator for ReferencesGlob<'a, G> { + type Item = Result; + + fn next(&mut self) -> Option { + for reference in &mut self.iter { + match reference { + Ok(reference) => match reference.name() { + Some(name) if self.glob.matches(name) => { + return Some(Reference::try_from(reference).map_err(error::Iter::from)) + }, + _ => continue, + }, + + Err(e) => return Some(Err(e.into())), + } + } + None + } +} diff --git a/git-storage/src/refdb/read.rs b/git-storage/src/refdb/read.rs new file mode 100644 index 0000000..698f65b --- /dev/null +++ b/git-storage/src/refdb/read.rs @@ -0,0 +1,43 @@ +// Copyright © 2022 The Radicle Link Contributors +// SPDX-License-Identifier: GPL-3.0-or-later + +use std::error::Error; + +use git_ext::Oid; +use git_ref_format::{refspec, RefStr}; + +use super::Reference; + +/// Read-only access to a git refdb. +/// +/// See [`crate::Read`] for an implementation of this trait. +pub trait Read { + /// The error type for finding a reference in the refdb. + type FindRef: Error + Send + Sync + 'static; + + /// The error type for finding references in the refdb. + type FindRefs: Error + Send + Sync + 'static; + + /// The error type for finding reference Oid in the refdb. + type FindRefOid: Error + Send + Sync + 'static; + + /// Iterator for references returned by `find_references`. + type References: Iterator>; + + /// Find the reference that corresponds to `name`. If the reference does not + /// exist, then `None` is returned. + fn find_reference(&self, name: Ref) -> Result, Self::FindRef> + where + Ref: AsRef; + + /// Find the references that match `pattern`. + fn find_references(&self, pattern: Pat) -> Result + where + Pat: AsRef; + + /// Find the [`Oid`] for the reference that corresponds to `name`. If the + /// reference does not exist, then `None` is returned. + fn find_reference_oid(&self, name: Ref) -> Result, Self::FindRefOid> + where + Ref: AsRef; +} diff --git a/git-storage/src/refdb/write.rs b/git-storage/src/refdb/write.rs new file mode 100644 index 0000000..af63331 --- /dev/null +++ b/git-storage/src/refdb/write.rs @@ -0,0 +1,149 @@ +// Copyright © 2022 The Radicle Link Contributors +// SPDX-License-Identifier: GPL-3.0-or-later + +use std::error::Error; + +use git_ext::Oid; +use git_ref_format::{Qualified, RefString}; + +use super::read::Read; + +pub mod previous; + +/// Read-write access to a git refdb. +/// +/// See [`crate::Write`] for an implementation of this trait. +pub trait Write: Read { + type UpdateError: Error + Send + Sync + 'static; + + /// Apply the provided `updates` to the refdb. + /// + /// The implementation of `update` is expected to be transactional. All + /// successful updates are returned in [`Applied::updated`] while rejected + /// updates should be returned in [`Applied::rejected`]. + fn update<'a, U>(&mut self, updates: U) -> Result, Self::UpdateError> + where + U: IntoIterator>; +} + +/// Perform an update to a reference in the refdb. +#[derive(Debug, Clone)] +pub enum Update<'a> { + /// Update a direct reference, i.e. one that points directly to an [`Oid`]. + Direct { + /// The `name` of the reference. + name: Qualified<'a>, + /// The `target` to point the reference at. + target: Oid, + /// Policy to apply when an [`Update`] would not apply as a + /// fast-forward. + /// + /// An update is a fast-forward iff: + /// + /// 1. A ref with the same name already exists + /// 2. The ref is a direct ref, and the update is a [`Update::Direct`] + /// 3. Both the existing and the update [`Oid`] point to a commit + /// object without peeling + /// 4. The existing commit is an ancestor of the update commit + /// + /// or: + /// + /// 1. A ref with the same name does not already exist + no_ff: Policy, + /// The expectation of the previous value for the reference before + /// making the update. This allows the update to be rejected in + /// the case of a concurrent modification the reference. + previous: previous::Edit, + /// The [`reflog`][reflog] entry for the update. + /// + /// [reflog]: https://git-scm.com/docs/git-reflog + // TODO: we may want to force the creation of a reflog entry for specific references. + reflog: String, + }, + Symbolic { + /// The `name` of the reference. + name: Qualified<'a>, + /// The `target` to point the reference at. Currently, only supports + /// one-level deep. + target: SymrefTarget<'a>, + /// Policy to apply when the ref already exists, but is a direct ref + /// before the update. + type_change: Policy, + /// The expectation of the previous value for the reference before + /// making the update. This allows the update to be rejected in + /// the case of a concurrent modification the reference. + previous: previous::Edit, + /// The [`reflog`][reflog] entry for the update. + /// + /// [reflog]: https://git-scm.com/docs/git-reflog + reflog: String, + }, + Remove { + /// The `name` of the reference. + name: Qualified<'a>, + /// The expectation of the previous value for the reference before + /// making the update. This allows the update to be rejected in + /// the case of a concurrent modification the reference. + previous: previous::Remove, + }, +} + +/// A target of a [symbolic reference][symref]. +/// +/// [symref]: https://git-scm.com/docs/git-symbolic-ref +#[derive(Debug, Clone)] +pub struct SymrefTarget<'a> { + /// The `name` of the symbolic reference. + pub name: Qualified<'a>, + /// The underlying `target` of the symbolic reference. + pub target: Oid, +} + +// TODO: include the previous values +/// 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 [`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 [`Update::Remove`] was succesful. + Removed { + /// The `name` of the reference that was removed. + name: RefString, + /// The old `target` of the reference that was removed. + previous: Oid, + }, +} + +/// The outcome of running a [`Write::update`]. +#[derive(Clone, Default)] +pub struct Applied<'a> { + /// Any [`Update`]s that were rejected due to their [`previous::Edit`], + /// [`previous::Remove`], or [`Policy`]. + pub rejected: Vec>, + /// The successful [`Update`]s applied to the refdb. + pub updated: Vec, +} + +/// The policy to use when guarding against fast-forwards in the case of +/// [`Update::Direct`] and type changes in the case of [`Update::Symbolic`]. +#[derive(Clone, Copy, Debug)] +pub enum Policy { + /// Abort the entire transaction. + Abort, + /// Reject this update, but continue the transaction. + Reject, + /// Allow the update. + Allow, +} diff --git a/git-storage/src/refdb/write/previous.rs b/git-storage/src/refdb/write/previous.rs new file mode 100644 index 0000000..efe0be1 --- /dev/null +++ b/git-storage/src/refdb/write/previous.rs @@ -0,0 +1,132 @@ +// Copyright © 2022 The Radicle Link Contributors +// SPDX-License-Identifier: GPL-3.0-or-later + +//! The [`Edit`] and [`Remove`] enums provide a mechanism for protecting updates +//! during concurrent writes to the refdb. The value is constructed with the +//! policy, sometimes providing the expected previous value of the [`Oid`] +//! target. If this policy is not satisfied then the update should be rejected. + +use git_ext::Oid; + +use thiserror::Error; + +/// The expectation of the reference's state. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum Edit { + /// No requirements are made towards the current value, and the new value is + /// set unconditionally. + Any, + /// The reference must exist and may have any value. + MustExist, + /// Create the ref only, hence the reference must not exist. + MustNotExist, + /// The ref _must_ exist and have the given value. + MustExistAndMatch(Oid), + /// The ref _may_ exist and have the given value, or may not exist at all. + MayExistAndMatch(Oid), +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Error)] +pub enum EditError { + #[error("the reference does not exist when it was expected to exist")] + DoesNotExist, + #[error("the reference does exist when it was expected not to exist")] + DoesExist, + #[error("the reference does not match - given: '{given}' expected: '{expected}'")] + DoesNotMatch { given: Oid, expected: Oid }, +} + +impl Edit { + /// Guard against the `given` value using this [`Edit`]. + /// + /// The function will return [`Err`] if the `given` value does not pass the + /// policy the policy of the [`Edit`]. + /// + /// The `given` value is presumed non-existing if set to `None`, otherwise + /// it is presumed to be existing. + pub fn guard(&self, given: Option) -> Result<(), EditError> { + use EditError::*; + + match self { + Self::Any => Ok(()), + Self::MustExist => { + if given.is_none() { + Err(DoesNotExist) + } else { + Ok(()) + } + }, + Self::MustNotExist => { + if given.is_some() { + Err(DoesExist) + } else { + Ok(()) + } + }, + Self::MustExistAndMatch(expected) => match given { + Some(given) if &given == expected => Ok(()), + Some(given) => Err(DoesNotMatch { + given, + expected: *expected, + }), + None => Err(DoesNotExist), + }, + Self::MayExistAndMatch(expected) => match given { + Some(given) if &given == expected => Ok(()), + Some(given) => Err(DoesNotMatch { + given, + expected: *expected, + }), + None => Ok(()), + }, + } + } +} + +/// The expectation of the existing reference's state. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum Remove { + /// The reference must exist and may have any value. + MustExist, + /// The ref _must_ exist and have the given value. + MustExistAndMatch(Oid), +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Error)] +pub enum RemoveError { + #[error("the reference does not exist when it was expected to exist")] + DoesNotExist, + #[error("the reference does not match - given: '{given}' expected: '{expected}'")] + DoesNotMatch { given: Oid, expected: Oid }, +} + +impl Remove { + /// Guard against the `given` value using this [`Remove`]. + /// + /// The function will return [`Err`] if the `given` value does not pass the + /// policy the policy of the [`Remove`]. + /// + /// The `given` value is presumed non-existing if set to `None`, otherwise + /// it is presumed to be existing. + pub fn guard(&self, given: Option) -> Result<(), RemoveError> { + use RemoveError::*; + + match self { + Self::MustExist => { + if given.is_none() { + Err(DoesNotExist) + } else { + Ok(()) + } + }, + Self::MustExistAndMatch(expected) => match given { + Some(given) if &given == expected => Ok(()), + Some(given) => Err(DoesNotMatch { + given, + expected: *expected, + }), + None => Err(DoesNotExist), + }, + } + } +} diff --git a/git-storage/src/signature.rs b/git-storage/src/signature.rs new file mode 100644 index 0000000..e223c94 --- /dev/null +++ b/git-storage/src/signature.rs @@ -0,0 +1,19 @@ +// Copyright © 2022 The Radicle Link Contributors +// SPDX-License-Identifier: GPL-3.0-or-later + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct UserInfo { + /// Provided `name` of the user. + pub name: String, + /// Proivded `email` of the user. Note that this does not necessarily have + /// to be an email, but will be used as the email field in [`crate::config`] + /// and the [`git2::Signature`]. + pub email: String, +} + +impl UserInfo { + /// Obtain the [`git2::Signature`] for this `UserInfo`. + pub fn signature(&self) -> Result { + git2::Signature::now(&self.name, &self.email) + } +} From 5898c0ffde59e155598f61b899fa9f5105475c95 Mon Sep 17 00:00:00 2001 From: Fintan Halpenny Date: Fri, 23 Sep 2022 16:44:07 +0100 Subject: [PATCH 2/4] git-storage: return previous targets The Updated values only tell the updated value of the target. It can be useful to also return the previous value, e.g. for reporting, hooks, etc. Add `Option` to the Update::Direct and Update::Symbolic variants. Signed-off-by: Fintan Halpenny --- git-storage/src/backend/write.rs | 33 +++++++++++++++++++++++--------- git-storage/src/refdb/write.rs | 5 ++++- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/git-storage/src/backend/write.rs b/git-storage/src/backend/write.rs index 8b496d0..419daa3 100644 --- a/git-storage/src/backend/write.rs +++ b/git-storage/src/backend/write.rs @@ -290,10 +290,14 @@ impl<'a, S> Transaction<'a, S> { previous, reflog, })), - Policy::Allow => Ok(Either::Right(self.direct_edit(&name, target, &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, &reflog)?)), + None => Ok(Either::Right( + self.direct_edit(&name, target, given, &reflog)?, + )), } } @@ -314,7 +318,7 @@ impl<'a, S> Transaction<'a, S> { /// 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>( + pub fn symbolic<'b: 'a>( &mut self, name: Qualified<'b>, target: SymrefTarget<'b>, @@ -323,6 +327,10 @@ impl<'a, S> Transaction<'a, S> { reflog: String, ) -> Result, Vec>, error::Update> { let src = self.refdb.find_reference(&name)?; + let prev = src + .as_ref() + .map(|src| refdb::resolve(self.refdb.as_raw(), src)) + .transpose()?; match src { Some(src) => match src.target { Target::Direct { .. } if matches!(type_change, Policy::Abort) => { @@ -345,19 +353,19 @@ impl<'a, S> Transaction<'a, S> { let is_ff = target.target != dst && self.is_ff(&target.name, target.target, dst)?; Ok(Either::Right( - self.symbolic_edit(name, target, &reflog, is_ff)?, + 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, &reflog, true)?, + self.symbolic_edit(name, target, prev, &reflog, true)?, )), } }, }, None => Ok(Either::Right( - self.symbolic_edit(name, target, &reflog, true)?, + self.symbolic_edit(name, target, prev, &reflog, true)?, )), } } @@ -422,6 +430,7 @@ impl<'a, S> Transaction<'a, S> { &mut self, reference: R, target: Oid, + prev: Option, reflog: &str, ) -> Result where @@ -448,13 +457,15 @@ impl<'a, S> Transaction<'a, S> { Ok(Updated::Direct { name: reference.to_owned(), target, + previous: prev, }) } pub fn symbolic_edit( &mut self, reference: R, - target: SymrefTarget, + target: SymrefTarget<'a>, + prev: Option, reflog: &str, is_ff: bool, ) -> Result, error::Transaction> @@ -465,7 +476,10 @@ impl<'a, S> Transaction<'a, S> { self.lock(reference)?; self.lock(&target.name)?; - let SymrefTarget { name: dst, target } = target; + let SymrefTarget { + name: dst, + target: dst_target, + } = target; let mut edits = Vec::with_capacity(2); let info = self.refdb.info(); @@ -477,7 +491,7 @@ impl<'a, S> Transaction<'a, S> { source: err, })?; if is_ff { - let direct = self.direct_edit(&dst, target, reflog)?; + let direct = self.direct_edit(&dst, dst_target, prev, reflog)?; edits.push(direct); } @@ -491,6 +505,7 @@ impl<'a, S> Transaction<'a, S> { edits.push(Updated::Symbolic { name: reference.to_owned(), target: dst.into(), + previous: prev, }); Ok(edits) } diff --git a/git-storage/src/refdb/write.rs b/git-storage/src/refdb/write.rs index af63331..adfd689 100644 --- a/git-storage/src/refdb/write.rs +++ b/git-storage/src/refdb/write.rs @@ -99,7 +99,6 @@ pub struct SymrefTarget<'a> { pub target: Oid, } -// TODO: include the previous values /// The successful result of an [`Update`] applied to the refdb. #[derive(Debug, Clone)] pub enum Updated { @@ -109,6 +108,8 @@ pub enum Updated { name: RefString, /// The new `target` of the reference that was updated. target: Oid, + /// The previous target of the reference, if it existed. + previous: Option, }, /// The [`Update::Symbolic`] was succesful. Symbolic { @@ -116,6 +117,8 @@ pub enum Updated { name: RefString, /// The new `target` of the reference that was updated. target: RefString, + /// The previous peeled target of the reference, if it existed. + previous: Option, }, /// The [`Update::Remove`] was succesful. Removed { From ee7a9029ce2c1e5e2632e0a095ecf5b36ac392a4 Mon Sep 17 00:00:00 2001 From: Fintan Halpenny Date: Mon, 26 Sep 2022 11:59:34 +0100 Subject: [PATCH 3/4] git-storage: fallible functions for finding objects When an object does not exist the `find_*` functions will return None. It will be a common case that users of the API will follow the pattern of `find_blob(store, oid)?.ok_or(Error::NotFound)`. Add fallible functions corresponding to each `find_` function returning a `git2::Error` if the object is not found. Signed-off-by: Fintan Halpenny --- git-storage/src/odb.rs | 64 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/git-storage/src/odb.rs b/git-storage/src/odb.rs index ed15052..5b6f59a 100644 --- a/git-storage/src/odb.rs +++ b/git-storage/src/odb.rs @@ -31,3 +31,67 @@ pub use read::Read; pub mod write; pub use write::Write; + +/// Find the [`Object`] corresponding to the given `oid`. +/// +/// Will fail if the object does not exist. +pub fn object(storage: &S, oid: Oid) -> Result +where + S: Read, +{ + storage + .find_object(oid)? + .ok_or_else(|| not_found("object", oid)) +} + +/// Find the [`Blob`] corresponding to the given `oid`. +/// +/// Will fail if the blob does not exist. +pub fn blob(storage: &S, oid: Oid) -> Result +where + S: Read, +{ + storage + .find_blob(oid)? + .ok_or_else(|| not_found("blob", oid)) +} + +/// Find the [`Commit`] corresponding to the given `oid`. +/// +/// Will fail if the commit does not exist. +pub fn commit(storage: &S, oid: Oid) -> Result +where + S: Read, +{ + storage + .find_commit(oid)? + .ok_or_else(|| not_found("commit", oid)) +} + +/// Find the [`Tag`] corresponding to the given `oid`. +/// +/// Will fail if the tag does not exist. +pub fn tag(storage: &S, oid: Oid) -> Result +where + S: Read, +{ + storage.find_tag(oid)?.ok_or_else(|| not_found("tag", oid)) +} + +/// Find the [`Tree`] corresponding to the given `oid`. +/// +/// Will fail if the tree does not exist. +pub fn tree(storage: &S, oid: Oid) -> Result +where + S: Read, +{ + storage + .find_tree(oid)? + .ok_or_else(|| not_found("tree", oid)) +} + +fn not_found(kind: &str, oid: Oid) -> git2::Error { + use git2::{ErrorClass::*, ErrorCode::*}; + + git2::Error::new(NotFound, Object, format!("could not find {kind} {oid}")) +} From 0730453ac5a898353504626b122b15f99b877b88 Mon Sep 17 00:00:00 2001 From: Fintan Halpenny Date: Mon, 26 Sep 2022 14:48:20 +0100 Subject: [PATCH 4/4] git-storage: remove owner and config The idea of an owner came from radicle-link storing the signer in the storage which was needed for performing signing when writing particular rad references. The peer_id and personal identity was also stored within the monorepo config. Since git-storage is just general reading & writing of references and objects, these concepts are not necessarily needed at this level. Remove the owner from Write and remove the config module. Signed-off-by: Fintan Halpenny --- git-storage/src/backend/read.rs | 20 +-- git-storage/src/backend/write.rs | 76 ++------- git-storage/src/backend/write/error.rs | 4 - git-storage/src/config.rs | 225 ------------------------- git-storage/src/lib.rs | 8 +- git-storage/src/pool.rs | 47 ++---- git-storage/src/signature.rs | 6 +- 7 files changed, 35 insertions(+), 351 deletions(-) delete mode 100644 git-storage/src/config.rs diff --git a/git-storage/src/backend/read.rs b/git-storage/src/backend/read.rs index 56a38d7..9132981 100644 --- a/git-storage/src/backend/read.rs +++ b/git-storage/src/backend/read.rs @@ -3,13 +3,12 @@ // 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, str::FromStr}; +use std::path::Path; use git_ext::{error::is_not_found_err, Oid}; use std_ext::result::ResultExt as _; use crate::{ - config::Config, glob, odb::{self, Blob, Commit, Object, Tag, Tree}, refdb::{self, Reference, References, ReferencesGlob}, @@ -45,25 +44,12 @@ impl Read { Ok(Self { raw }) } - - /// Read the identifier from the underlying [`Config`]. The identifier is - /// expected to exist already. - /// - /// See [`crate::config::Owner`] for more information on the identifier. - pub fn rad_identifier(&self) -> Result - where - Id: FromStr, - Id::Err: std::error::Error + Send + Sync + 'static, - { - let config = Config::try_from(&self.raw)?; - Ok(config.rad_identifier::()?) - } } pub mod error { use thiserror::Error; - use crate::{config, refdb::error::ParseReference}; + use crate::refdb::error::ParseReference; #[derive(Debug, Error)] pub enum Init { @@ -73,8 +59,6 @@ pub mod error { #[derive(Debug, Error)] pub enum Identifier { - #[error(transparent)] - Config(#[from] config::Error), #[error(transparent)] Git(#[from] git2::Error), } diff --git a/git-storage/src/backend/write.rs b/git-storage/src/backend/write.rs index 419daa3..c05d4e3 100644 --- a/git-storage/src/backend/write.rs +++ b/git-storage/src/backend/write.rs @@ -3,20 +3,14 @@ // This file is part of radicle-link, distributed under the GPLv3 with Radicle // Linking Exception. For full terms see the included LICENSE file. -use std::{ - marker::PhantomData, - path::{Path, PathBuf}, - str::FromStr, -}; +use std::path::Path; use either::Either; use git_ext::{error::is_not_found_err, Oid}; use git_ref_format::{Qualified, RefStr, RefString}; -use std_ext::Void; use crate::{ - config::{self, Config}, odb, refdb::{ self, @@ -42,23 +36,14 @@ pub mod error; /// For write access to the refdb see [`refdb::Write`]. /// /// To construct the `Write` storage use [`Read::open`]. -pub struct Write { +pub struct Write { inner: Read, - owner: Owner, info: UserInfo, } -impl Write -where - O: config::Owner, - ::Err: std::error::Error + Send + Sync + 'static, -{ +impl Write { /// Open the [`Write`] storage, initialising it if it doesn't exist. /// - /// Note that a [`Write`] is tied to the [`config::Owner`] with which it was - /// initialised, attempting to open it with a different one (that is, a - /// different owner) will return an error. - /// /// # Concurrency /// /// [`Write`] can be sent between threads, but it can't be shared between @@ -66,71 +51,38 @@ where /// the same way two `git` processes can access the same repository. /// However, if you need multiple [`Write`]s to be shared between /// threads, use a [`crate::Pool`] instead. - pub fn open>(path: P, info: UserInfo, owner: O) -> Result { + pub fn open>(path: P, info: UserInfo) -> Result { crate::init(); let path = path.as_ref(); let raw = match git2::Repository::open_bare(path) { Err(e) if is_not_found_err(&e) => { - let mut backend = git2::Repository::init_opts( + let backend = git2::Repository::init_opts( path, git2::RepositoryInitOptions::new() .bare(true) .no_reinit(true) .external_template(false), )?; - Config::init(&mut backend, &owner, info.clone())?; - Ok(backend) }, Ok(repo) => Ok(repo), Err(e) => Err(e), }?; - let config = Config::try_from(&raw)?; - let actual = config.rad_identifier::()?; - let current = owner.rad_identifier(); - - if actual != current { - return Err(error::Init::OwnerMismatch { - current: current.to_string(), - actual: actual.to_string(), - }); - } - Ok(Self { inner: Read { raw }, - owner, info, }) } - - /// Get the underlying [`Config`] of the [`Write`] storage. - pub fn config(&self) -> Result, config::Error> { - Config::try_from(self) - } - - /// Get the read-only [`Config`] of the [`Write`] storage. - pub fn config_readonly(&self) -> Result>, git2::Error> { - Config::try_from(&self.inner.raw) - } - - pub(crate) fn config_path(&self) -> PathBuf { - config::path(&self.inner.raw) - } } -impl Write { +impl Write { /// Return a read-only handle of the storage. pub fn read_only(&self) -> &Read { &self.inner } - /// Return the owner of the storage. - pub fn owner(&self) -> &O { - &self.owner - } - /// Return the [`UserInfo`] of the storage. pub fn info(&self) -> &UserInfo { &self.info @@ -143,7 +95,7 @@ impl Write { // refdb impls -impl<'a, S> refdb::Read for &'a Write { +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; @@ -172,7 +124,7 @@ impl<'a, S> refdb::Read for &'a Write { } } -impl<'a, S> refdb::Write for &'a Write { +impl<'a> refdb::Write for &'a Write { type UpdateError = error::Update; fn update<'b, U>(&mut self, updates: U) -> Result, Self::UpdateError> @@ -217,13 +169,13 @@ impl<'a, S> refdb::Write for &'a Write { /// An internal struct combining a [`Write`] and [`git2::Transaction`]. // TODO: include optional namespace -struct Transaction<'a, O> { - refdb: &'a Write, +struct Transaction<'a> { + refdb: &'a Write, txn: git2::Transaction<'a>, } -impl<'a, S> Transaction<'a, S> { - pub fn new(refdb: &'a Write) -> Result { +impl<'a> Transaction<'a> { + pub fn new(refdb: &'a Write) -> Result { let txn = refdb.inner.raw.transaction()?; Ok(Self { refdb, txn }) } @@ -551,7 +503,7 @@ impl<'a, S> Transaction<'a, S> { // odb impls -impl odb::Read for Write { +impl odb::Read for Write { type FindObj = ::FindObj; type FindBlob = ::FindBlob; type FindCommit = ::FindCommit; @@ -579,7 +531,7 @@ impl odb::Read for Write { } } -impl odb::Write for Write { +impl odb::Write for Write { type WriteBlob = git2::Error; type WriteCommit = git2::Error; type WriteTag = git2::Error; diff --git a/git-storage/src/backend/write/error.rs b/git-storage/src/backend/write/error.rs index 7f72843..514c3b8 100644 --- a/git-storage/src/backend/write/error.rs +++ b/git-storage/src/backend/write/error.rs @@ -9,12 +9,8 @@ use super::*; #[derive(Debug, Error)] pub enum Init { - #[error(transparent)] - Config(#[from] config::Error), #[error(transparent)] Git(#[from] git2::Error), - #[error("the current identifier '{current}' does not match the identifier used to create the storage '{actual}'")] - OwnerMismatch { current: String, actual: String }, } #[derive(Debug, Error)] diff --git a/git-storage/src/config.rs b/git-storage/src/config.rs deleted file mode 100644 index f4c1340..0000000 --- a/git-storage/src/config.rs +++ /dev/null @@ -1,225 +0,0 @@ -// Copyright © 2019-2020 The Radicle Foundation -// -// This file is part of radicle-link, distributed under the GPLv3 with Radicle -// Linking Exception. For full terms see the included LICENSE file. - -//! The [git config][config] for a particular storage. -//! -//! As well as encapsulating the git config, it also holds the [`Owner`] of the -//! config. -//! -//! A [`Config`] can either be constructed by it's [`Config::init`] constuctor -//! or by its `TryFrom` instances for [`git2::Repository`] and [`Write`]. -//! -//! [config]: https://git-scm.com/docs/git-config - -use std::{convert::TryFrom, marker::PhantomData, path::PathBuf, str::FromStr}; - -use git_ext::is_not_found_err; -use std_ext::prelude::*; -use thiserror::Error; - -use crate::{signature::UserInfo, Write}; - -const CONFIG_USER_NAME: &str = "user.name"; -const CONFIG_USER_EMAIL: &str = "user.email"; -const CONFIG_RAD_ID: &str = "rad.identifier"; - -#[derive(Debug, Error)] -#[non_exhaustive] -pub enum Error { - #[error("storage was already initialised with identifier {0}")] - AlreadyInitialised(String), - - #[error("could not parse the identifier found at 'rad.self' in the storage config")] - Identifier(#[source] Box), - - #[error("could not parse the identifier found at 'rad.peerid' in the storage config")] - RadSelf(#[source] Box), - - #[error(transparent)] - Git(#[from] git2::Error), -} - -/// The `RadIdentifier` is expected to uniquely identify the owner of this -/// repository. The value should be unique and should only be able to be -/// constructed by the owner. For example, this could be a signing key unique to -/// the owner. -pub trait Owner { - type RadIdentifier: ToString + FromStr + PartialEq; - - fn rad_identifier(&self) -> Self::RadIdentifier; -} - -/// The _local_ config for the give [`git2::Repository`]. -/// -/// This is typically `$GIT_DIR/.git/config` for non-bare, and `$GIT_DIR/config` -/// for bare repositories. -pub fn path(repo: &git2::Repository) -> PathBuf { - repo.path().join("config") -} - -/// A git config paired with the owner information. -/// -/// The config is initialised with the following config keys: -/// -/// * `user.name`: the provided name of the user. -/// * `user.email`: the provided email of the user. This does not necessarily -/// have to be a valid email, see [`UserInfo`]. -/// * `rad.identifier`: the unqiue value identifying the owner of this -/// `Config`. -/// -/// # Constructors -/// -/// * [`Config::init`] -/// * `TryFrom<&'a Write>` -/// * `TryFrom { - inner: git2::Config, - owner: &'a O, - info: UserInfo, -} - -impl<'a, O> TryFrom<&'a Write> for Config<'a, O> -where - O: Owner, - ::Err: std::error::Error + Send + Sync + 'static, -{ - type Error = Error; - - fn try_from(storage: &'a Write) -> Result { - let inner = git2::Config::open(&storage.config_path())?; - let mut this = Self { - inner, - owner: storage.owner(), - info: storage.info().clone(), - }; - this.guard_key_change()?; - this.ensure_reflog()?; - - Ok(this) - } -} - -impl TryFrom<&git2::Repository> for Config<'_, PhantomData> { - type Error = git2::Error; - - fn try_from(repo: &git2::Repository) -> Result { - let inner = git2::Config::open(&self::path(repo))?.snapshot()?; - let name = inner.get_string(CONFIG_USER_NAME)?; - let email = inner.get_string(CONFIG_USER_EMAIL)?; - Ok(Self { - inner, - owner: &PhantomData, - info: UserInfo { name, email }, - }) - } -} - -impl<'a, O> Config<'a, O> -where - O: Owner, - ::Err: std::error::Error + Send + Sync + 'static, -{ - fn guard_key_change(&self) -> Result<(), Error> { - let configured_identifier = self - .rad_identifier::() - .map(Some) - .or_matches::( - |err| matches!(err, Error::Git(e) if is_not_found_err(e)), - || Ok(None), - )?; - let owner_identifier = self.owner.rad_identifier(); - match configured_identifier { - Some(initialised_with) if initialised_with != owner_identifier => { - Err(Error::AlreadyInitialised(initialised_with.to_string())) - }, - - _ => Ok(()), - } - } - - fn ensure_reflog(&mut self) -> Result<(), Error> { - if let Err(e) = self.inner.get_bool("core.logAllRefUpdates") { - return if is_not_found_err(&e) { - Ok(self.inner.set_bool("core.logAllRefUpdates", true)?) - } else { - Err(e.into()) - }; - } - - Ok(()) - } - - /// Initialise the [`Config`] with the given `owner` and `info`. - /// - /// # Errors - /// - /// * The identifier of the `owner` differs from the existing identifier - /// stored - pub fn init(repo: &mut git2::Repository, owner: &'a O, info: UserInfo) -> Result { - let identifier = owner.rad_identifier(); - let config = git2::Config::open(&self::path(repo))?; - let mut this = Config { - inner: config, - owner, - info, - }; - this.guard_key_change()?; - this.ensure_reflog()?; - this.set_identifier(identifier)?; - this.set_user_info()?; - - Ok(this) - } - - fn set_user_info(&mut self) -> Result<(), Error> { - self.inner.set_str(CONFIG_USER_NAME, &self.info.name)?; - self.inner.set_str(CONFIG_USER_EMAIL, &self.info.email)?; - - Ok(()) - } - - fn set_identifier(&mut self, rad_identifier: O::RadIdentifier) -> Result<(), Error> { - self.inner - .set_str(CONFIG_RAD_ID, &rad_identifier.to_string()) - .map_err(Error::from) - } -} - -impl Config<'_, O> { - pub fn user(&self) -> &UserInfo { - &self.info - } - - pub fn user_name(&self) -> Result { - self.inner.get_string(CONFIG_USER_NAME).map_err(Error::from) - } - - pub fn user_email(&self) -> Result { - self.inner - .get_string(CONFIG_USER_EMAIL) - .map_err(Error::from) - } - - pub fn rad_identifier(&self) -> Result - where - Id: FromStr, - Id::Err: std::error::Error + Send + Sync + 'static, - { - self.inner - .get_string(CONFIG_RAD_ID) - .map_err(Error::from) - .and_then(|peer_id| { - peer_id - .parse() - .map_err(|err| Error::Identifier(Box::new(err))) - }) - } -} - -impl Config<'_, PhantomData> { - pub fn readonly(repo: &git2::Repository) -> Result { - Self::try_from(repo) - } -} diff --git a/git-storage/src/lib.rs b/git-storage/src/lib.rs index f796a9f..e062d50 100644 --- a/git-storage/src/lib.rs +++ b/git-storage/src/lib.rs @@ -3,12 +3,9 @@ //! # `git-storage` //! -//! This crate provides access to git [references][refs] and [objects][objs], in -//! a Radicle context. The particular difference for Radicle is that a storage -//! must be *owned*, generally using a signing key. +//! This crate provides access to git [references][refs] and [objects][objs]. //! -//! To first initialise the storage use [`Write::open`]. This will intialise the -//! underlying git repository and its [`config`]. +//! 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 @@ -44,7 +41,6 @@ extern crate async_trait; extern crate radicle_git_ext as git_ext; extern crate radicle_std_ext as std_ext; -pub mod config; pub mod glob; pub mod pool; diff --git a/git-storage/src/pool.rs b/git-storage/src/pool.rs index 05b88bc..da6faa2 100644 --- a/git-storage/src/pool.rs +++ b/git-storage/src/pool.rs @@ -7,7 +7,6 @@ use std::{ marker::PhantomData, ops::{Deref, DerefMut}, path::PathBuf, - str::FromStr, sync::Arc, }; @@ -16,7 +15,7 @@ use parking_lot::RwLock; use std_ext::Void; use thiserror::Error; -use crate::{config, read, signature::UserInfo, write, Read, Write}; +use crate::{read, signature::UserInfo, write, Read, Write}; #[derive(Debug, Error)] #[non_exhaustive] @@ -75,11 +74,7 @@ impl AsMut for PooledRef { } } -impl AsRef for PooledRef> -where - S: config::Owner, - ::Err: std::error::Error + Send + Sync + 'static, -{ +impl AsRef for PooledRef { fn as_ref(&self) -> &Read { self.0.read_only() } @@ -100,8 +95,7 @@ impl Initialised { } } -pub struct Writer { - signer: S, +pub struct Writer { init: Initialised, } @@ -113,7 +107,7 @@ pub struct Config { } pub type ReadConfig = Config>; -pub type WriteConfig = Config>; +pub type WriteConfig = Config; impl ReadConfig { pub fn new(root: PathBuf, info: UserInfo) -> Self { @@ -124,11 +118,11 @@ impl ReadConfig { } } - pub fn write(self, signer: S, init: Initialised) -> WriteConfig { + pub fn write(self, init: Initialised) -> WriteConfig { Config { root: self.root, info: self.info, - write: Writer { signer, init }, + write: Writer { init }, } } } @@ -144,36 +138,23 @@ impl Manager for ReadConfig { } } -impl WriteConfig -where - S: config::Owner, - ::Err: std::error::Error + Send + Sync + 'static, -{ - pub fn new(root: PathBuf, info: UserInfo, signer: S, init: Initialised) -> Self { +impl WriteConfig { + pub fn new(root: PathBuf, info: UserInfo, init: Initialised) -> Self { Self { root, info, - write: Writer { signer, init }, + write: Writer { init }, } } - fn mk_storage(&self) -> Result, InitError> - where - S: Clone, - { - Write::open(&self.root, self.info.clone(), self.write.signer.clone()) - .map_err(InitError::from) + fn mk_storage(&self) -> Result { + Write::open(&self.root, self.info.clone()).map_err(InitError::from) } } #[async_trait] -impl Manager, InitError> for WriteConfig -where - S: Clone + Send + Sync + 'static, - S: config::Owner, - ::Err: std::error::Error + Send + Sync + 'static, -{ - async fn create(&self) -> Result, InitError> { +impl Manager for WriteConfig { + async fn create(&self) -> Result { let initialised = self.write.init.0.read(); if *initialised { self.mk_storage() @@ -189,7 +170,7 @@ where } } - async fn recycle(&self, _: &mut Write) -> RecycleResult { + async fn recycle(&self, _: &mut Write) -> RecycleResult { Ok(()) } } diff --git a/git-storage/src/signature.rs b/git-storage/src/signature.rs index e223c94..9b67521 100644 --- a/git-storage/src/signature.rs +++ b/git-storage/src/signature.rs @@ -5,9 +5,9 @@ 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 [`crate::config`] - /// and the [`git2::Signature`]. + /// 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, }