Make `ReadStorage` more usable

Previously, we were not able to access a `ReadRepository` instance
through `ReadStorage`. With this change, we are able to.
This commit is contained in:
Alexis Sellier 2023-02-14 18:31:18 +01:00
parent 89fcc6de1c
commit 950fae209d
No known key found for this signature in database
31 changed files with 134 additions and 71 deletions

View File

@ -83,7 +83,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
let signer = term::signer(&profile)?;
let storage = &profile.storage;
let (_, id) = radicle::rad::cwd()?;
let repo = storage.repository(id)?;
let repo = storage.repository_mut(id)?;
let mut issues = issue::Issues::open(*signer.public_key(), &repo)?;
let mut issue = issues.get_mut(&options.id).map_err(|err| match err {

View File

@ -7,7 +7,6 @@ use anyhow::Context as _;
use radicle::prelude::*;
use radicle::storage::git::transport;
use radicle::storage::RemoteId;
use radicle::storage::WriteStorage;
use crate::project;
use crate::terminal as term;

View File

@ -15,7 +15,6 @@ use radicle::prelude::*;
use radicle::rad;
use radicle::storage;
use radicle::storage::git::{ProjectError, Storage};
use radicle::storage::WriteStorage;
use crate::commands::rad_checkout as checkout;
use crate::project;

View File

@ -10,7 +10,6 @@ use radicle::cob::store;
use radicle::cob::thread;
use radicle::prelude::*;
use radicle::storage;
use radicle::storage::WriteStorage;
use crate::terminal as term;
use crate::terminal::args::{Args, Error, Help};

View File

@ -19,7 +19,7 @@ where
.get(&profile.public_key, id)?
.context("No project with such ID exists")?;
let repo = storage.repository(id)?;
let repo = storage.repository_mut(id)?;
if !project.is_delegate(me) {
return Err(anyhow::anyhow!(

View File

@ -19,7 +19,7 @@ where
.get(&profile.public_key, id)?
.context("No project with such ID exists")?;
let repo = storage.repository(id)?;
let repo = storage.repository_mut(id)?;
if !project.is_delegate(me) {
return Err(anyhow::anyhow!(

View File

@ -4,7 +4,7 @@ use std::str::FromStr;
use anyhow::{anyhow, Context as _};
use radicle::identity::Id;
use radicle::storage::{ReadStorage, WriteRepository, WriteStorage};
use radicle::storage::{ReadStorage, WriteRepository};
use crate::terminal as term;
use crate::terminal::args::{Args, Error, Help};

View File

@ -5,7 +5,7 @@ use radicle::cob::identity::{self, Proposal, ProposalId, Proposals, Revision, Re
use radicle::git::Oid;
use radicle::identity::Identity;
use radicle::prelude::Doc;
use radicle::storage::WriteStorage as _;
use radicle::storage::ReadStorage as _;
use radicle_crypto::{PublicKey, Verified};
use crate::terminal::args::{Args, Error, Help};

View File

@ -11,7 +11,7 @@ use json_color::{Color, Colorizer};
use radicle::crypto::Unverified;
use radicle::identity::Untrusted;
use radicle::identity::{Doc, Id};
use radicle::storage::{ReadRepository, ReadStorage, WriteStorage};
use radicle::storage::{ReadRepository, ReadStorage};
use crate::terminal as term;
use crate::terminal::args::{Args, Error, Help};

View File

@ -198,7 +198,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
let signer = term::signer(&profile)?;
let storage = &profile.storage;
let (_, id) = radicle::rad::cwd()?;
let repo = storage.repository(id)?;
let repo = storage.repository_mut(id)?;
let mut issues = Issues::open(*signer.public_key(), &repo)?;
match options.op {

View File

@ -3,7 +3,7 @@ use std::ffi::OsString;
use crate::terminal as term;
use crate::terminal::args::{Args, Error, Help};
use radicle::storage::{ReadRepository, WriteStorage};
use radicle::storage::{ReadRepository, ReadStorage};
pub const HELP: Help = Help {
name: "ls",

View File

@ -4,7 +4,7 @@ use std::str::FromStr;
use anyhow::{anyhow, Context as _};
use radicle::node::{Handle, NodeId};
use radicle::storage::WriteStorage;
use radicle::storage::ReadStorage;
use crate::terminal as term;
use crate::terminal::args::{Args, Error, Help};

View File

@ -83,7 +83,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
let signer = term::signer(&profile)?;
let storage = &profile.storage;
let (_, id) = radicle::rad::cwd()?;
let repo = storage.repository(id)?;
let repo = storage.repository_mut(id)?;
let mut issues = issue::Issues::open(*signer.public_key(), &repo)?;
let mut issue = issues.get_mut(&options.id).map_err(|err| match err {

View File

@ -5,7 +5,7 @@ use anyhow::{anyhow, Context as _};
use radicle::identity::Id;
use radicle::node::Handle;
use radicle::prelude::*;
use radicle::storage::WriteStorage;
use radicle::storage::ReadStorage;
use crate::terminal as term;
use crate::terminal::args::{Args, Error, Help};

View File

@ -6,7 +6,7 @@ use std::{thread, time};
use radicle::git;
use radicle::prelude::Id;
use radicle::profile::Home;
use radicle::storage::{ReadRepository, WriteStorage};
use radicle::storage::{ReadRepository, ReadStorage};
use radicle::test::fixtures;
use radicle_cli_test::TestFormula;

View File

@ -14,7 +14,7 @@ use tower_http::cors::{self, CorsLayer};
use radicle::cob::issue::Issues;
use radicle::identity::Id;
use radicle::storage::{ReadRepository, WriteStorage};
use radicle::storage::{ReadRepository, ReadStorage};
use radicle::Profile;
mod auth;

View File

@ -13,7 +13,7 @@ use tower::ServiceExt;
use radicle::cob::issue::Issues;
use radicle::cob::patch::{MergeTarget, Patches};
use radicle::git::raw as git2;
use radicle::storage::WriteStorage;
use radicle::storage::ReadStorage;
use radicle_cli::commands::rad_init;
use radicle_crypto::ssh::keystore::MemorySigner;
use radicle_crypto::Signer;

View File

@ -5,7 +5,7 @@ use axum::{Json, Router};
use radicle::cob::issue::Issues;
use radicle::identity::Did;
use radicle::storage::{ReadRepository, WriteStorage};
use radicle::storage::{ReadRepository, ReadStorage};
use crate::api::axum_extra::{Path, Query};
use crate::api::error::Error;

View File

@ -17,7 +17,7 @@ use radicle::cob::patch::Patches;
use radicle::cob::{thread, ActorId, Tag};
use radicle::identity::Id;
use radicle::node::NodeId;
use radicle::storage::{git::paths, ReadRepository, WriteStorage};
use radicle::storage::{git::paths, ReadRepository, ReadStorage};
use radicle_surf::{Glob, Oid, Repository};
use crate::api::axum_extra::{Path, Query};

View File

@ -10,7 +10,7 @@ use crate::service::filter::Filter;
use crate::service::{NodeId, Timestamp};
use crate::storage;
use crate::storage::refs::SignedRefs;
use crate::storage::{ReadRepository, WriteStorage};
use crate::storage::{ReadRepository, ReadStorage};
use crate::wire;
/// Maximum number of addresses which can be announced to other nodes.
@ -154,8 +154,14 @@ pub struct RefsAnnouncement {
impl RefsAnnouncement {
/// Check if this announcement is "fresh", meaning if it contains refs we do not have.
pub fn is_fresh<S: WriteStorage>(&self, storage: S) -> Result<bool, storage::Error> {
let repo = storage.repository(self.rid)?;
pub fn is_fresh<S: ReadStorage>(&self, storage: S) -> Result<bool, storage::Error> {
let repo = match storage.repository(self.rid) {
// If the repo doesn't exist, we consider this announcement "fresh", since we
// obviously don't have the refs.
Err(e) if e.is_not_found() => return Ok(true),
Err(e) => return Err(e),
Ok(r) => r,
};
for (remote_id, theirs) in self.refs.iter() {
if let Ok(ours) = repo.remote(remote_id) {

View File

@ -411,7 +411,11 @@ impl<S: WriteStorage + 'static, G: Signer> Simulation<S, G> {
Input::Fetched(f, result) => {
let result = Rc::try_unwrap(result).unwrap();
if f.initiated {
let mut repo = p.storage().repository(f.rid).unwrap();
let mut repo = match p.storage().repository_mut(f.rid) {
Ok(repo) => repo,
Err(e) if e.is_not_found() => p.storage().create(f.rid).unwrap(),
Err(e) => panic!("Failed to open repository: {e}"),
};
fetch(&mut repo, &f.remote, f.namespaces.clone()).unwrap();
}
p.fetched(f, result);

View File

@ -2,7 +2,7 @@ use std::{thread, time};
use radicle::crypto::Signer;
use radicle::node::{FetchResult, Handle as _};
use radicle::storage::{ReadRepository, WriteStorage};
use radicle::storage::{ReadRepository, ReadStorage};
use radicle::{assert_matches, rad};
use crate::service;

View File

@ -166,7 +166,11 @@ impl<G: Signer + Ecdh + 'static> Worker<G> {
fetch: &Fetch,
tunnel: &mut Tunnel<WireSession<G>>,
) -> Result<Vec<RefUpdate>, FetchError> {
let repo = self.storage.repository(fetch.rid)?;
let repo = match self.storage.repository_mut(fetch.rid) {
Ok(r) => Ok(r),
Err(e) if e.is_not_found() => self.storage.create(fetch.rid),
Err(e) => Err(e),
}?;
let tunnel_addr = tunnel.local_addr()?;
let mut cmd = process::Command::new("git");
cmd.current_dir(repo.path())

View File

@ -55,7 +55,7 @@ pub fn run(profile: radicle::Profile) -> Result<(), Box<dyn std::error::Error +
// Default to profile key.
let namespace = url.namespace.unwrap_or(profile.public_key);
let proj = profile.storage.repository(url.repo)?;
let proj = profile.storage.repository_mut(url.repo)?;
if proj.is_empty()? {
return Err(Error::RepositoryNotFound(proj.path().to_path_buf()).into());
}

View File

@ -1,6 +1,6 @@
use std::path::Path;
use radicle::{node::Handle, storage::WriteRepository, storage::WriteStorage};
use radicle::{node::Handle, storage::ReadStorage, storage::WriteRepository};
fn main() -> anyhow::Result<()> {
let cwd = Path::new(".").canonicalize()?;

View File

@ -154,7 +154,7 @@ mod test {
use crate::crypto::PublicKey;
use crate::rad;
use crate::storage::git::Storage;
use crate::storage::{ReadStorage, WriteRepository, WriteStorage};
use crate::storage::{ReadStorage, WriteRepository};
use crate::test::fixtures;
use super::did::Did;

View File

@ -371,7 +371,7 @@ mod test {
use crate::rad;
use crate::storage::git::transport;
use crate::storage::git::Storage;
use crate::storage::WriteStorage;
use crate::storage::WriteStorage as _;
use crate::test::arbitrary;
use crate::test::fixtures;
@ -414,7 +414,7 @@ mod test {
let storage = Storage::open(tempdir.path().join("storage")).unwrap();
let remote = arbitrary::gen::<RemoteId>(1);
let proj = arbitrary::gen::<Id>(1);
let repo = storage.repository(proj).unwrap();
let repo = storage.create(proj).unwrap();
let oid = git2::Oid::from_str("2d52a53ce5e4f141148a5f770cfd3ead2d6a45b8").unwrap();
let err = Doc::<Unverified>::head(&remote, &repo).unwrap_err();

View File

@ -14,7 +14,8 @@ use crate::identity::project::Project;
use crate::storage::git::transport;
use crate::storage::git::{ProjectError, Repository, Storage};
use crate::storage::refs::SignedRefs;
use crate::storage::{BranchName, ReadRepository as _, RemoteId, WriteRepository as _};
use crate::storage::WriteRepository;
use crate::storage::{BranchName, ReadRepository as _, RemoteId};
use crate::{identity, storage};
/// Name of the radicle storage remote.
@ -129,7 +130,7 @@ pub fn fork_remote<G: Signer, S: storage::WriteStorage>(
.get(remote, proj)?
.ok_or(ForkError::NotFound(proj))?;
let project = doc.project()?;
let repository = storage.repository(proj)?;
let repository = storage.repository_mut(proj)?;
let raw = repository.raw();
let remote_head = raw.refname_to_id(&git::refs::storage::branch(
@ -162,7 +163,7 @@ pub fn fork<G: Signer, S: storage::WriteStorage>(
storage: &S,
) -> Result<(), ForkError> {
let me = signer.public_key();
let repository = storage.repository(proj)?;
let repository = storage.repository_mut(proj)?;
// TODO: We should get the id branch pointer from a stored canonical reference.
let (canonical_id, _) = repository.identity_doc()?;
let (canonical_branch, canonical_head) = repository.head()?;
@ -302,7 +303,7 @@ mod tests {
use crate::identity::Did;
use crate::storage::git::transport;
use crate::storage::git::Storage;
use crate::storage::{ReadStorage, WriteStorage};
use crate::storage::ReadStorage;
use crate::test::fixtures;
use super::*;

View File

@ -70,6 +70,18 @@ pub enum Error {
Io(#[from] io::Error),
}
impl Error {
/// Whether this error is caused by something not being found.
pub fn is_not_found(&self) -> bool {
match self {
Self::Io(e) if e.kind() == io::ErrorKind::NotFound => true,
Self::Git(e) if git::is_not_found_err(e) => true,
Self::Doc(e) if e.is_not_found() => true,
_ => false,
}
}
}
/// Fetch error.
#[derive(Error, Debug)]
#[allow(clippy::large_enum_variant)]
@ -246,6 +258,8 @@ impl Remote<Verified> {
/// Read-only operations on a storage instance.
pub trait ReadStorage {
type Repository: ReadRepository;
/// Get the storage base path.
fn path(&self) -> &Path;
/// Get an identity document of a repository under a given remote.
@ -258,13 +272,18 @@ pub trait ReadStorage {
fn contains(&self, rid: &Id) -> Result<bool, ProjectError>;
/// Get the inventory of repositories hosted under this storage.
fn inventory(&self) -> Result<Inventory, Error>;
/// Open or create a read-only repository.
fn repository(&self, rid: Id) -> Result<Self::Repository, Error>;
}
/// Allows access to individual storage repositories.
pub trait WriteStorage: ReadStorage {
type Repository: WriteRepository;
type RepositoryMut: WriteRepository;
fn repository(&self, proj: Id) -> Result<Self::Repository, Error>;
/// Open a read-write repository.
fn repository_mut(&self, rid: Id) -> Result<Self::RepositoryMut, Error>;
/// Create a read-write repository.
fn create(&self, rid: Id) -> Result<Self::RepositoryMut, Error>;
}
/// Allows read-only access to a repository.
@ -355,6 +374,8 @@ where
T: Deref<Target = S>,
S: ReadStorage + 'static,
{
type Repository = S::Repository;
fn path(&self) -> &Path {
self.deref().path()
}
@ -374,6 +395,10 @@ where
) -> Result<Option<identity::Doc<Verified>>, ProjectError> {
self.deref().get(remote, proj)
}
fn repository(&self, rid: Id) -> Result<Self::Repository, Error> {
self.deref().repository(rid)
}
}
impl<T, S> WriteStorage for T
@ -381,10 +406,14 @@ where
T: Deref<Target = S>,
S: WriteStorage + 'static,
{
type Repository = S::Repository;
type RepositoryMut = S::RepositoryMut;
fn repository(&self, proj: Id) -> Result<Self::Repository, Error> {
self.deref().repository(proj)
fn repository_mut(&self, rid: Id) -> Result<Self::RepositoryMut, Error> {
self.deref().repository_mut(rid)
}
fn create(&self, rid: Id) -> Result<Self::RepositoryMut, Error> {
self.deref().create(rid)
}
}

View File

@ -95,6 +95,8 @@ pub struct Storage {
}
impl ReadStorage for Storage {
type Repository = Repository;
fn path(&self) -> &Path {
self.path.as_path()
}
@ -108,29 +110,39 @@ impl ReadStorage for Storage {
}
fn get(&self, remote: &RemoteId, proj: Id) -> Result<Option<Doc<Verified>>, ProjectError> {
// TODO: Don't create a repo here if it doesn't exist?
// Perhaps for checking we could have a `contains` method?
match self.repository(proj)?.identity_of(remote) {
let repo = match self.repository(proj) {
Ok(doc) => doc,
Err(e) if e.is_not_found() => return Ok(None),
Err(e) => return Err(e.into()),
};
match repo.identity_of(remote) {
Ok(doc) => Ok(Some(doc)),
Err(err) if err.is_not_found() => Ok(None),
Err(err) => Err(err),
Err(e) if e.is_not_found() => Ok(None),
Err(e) => Err(e),
}
}
fn inventory(&self) -> Result<Inventory, Error> {
self.repositories()
}
}
impl WriteStorage for Storage {
type Repository = Repository;
fn repository(&self, rid: Id) -> Result<Self::Repository, Error> {
Repository::open(paths::repository(self, &rid), rid)
}
}
impl WriteStorage for Storage {
type RepositoryMut = Repository;
fn repository_mut(&self, rid: Id) -> Result<Self::RepositoryMut, Error> {
Repository::open(paths::repository(self, &rid), rid)
}
fn create(&self, rid: Id) -> Result<Self::RepositoryMut, Error> {
Repository::create(paths::repository(self, &rid), rid)
}
}
impl Storage {
// TODO: Return a better error when not found.
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, io::Error> {
@ -207,27 +219,27 @@ pub enum VerifyError {
}
impl Repository {
/// Open an existing repository.
pub fn open<P: AsRef<Path>>(path: P, id: Id) -> Result<Self, Error> {
let backend = match git2::Repository::open_bare(path.as_ref()) {
Err(e) if ext::is_not_found_err(&e) => {
let backend = git2::Repository::init_opts(
&path,
git2::RepositoryInitOptions::new()
.bare(true)
.no_reinit(true)
.external_template(false),
)?;
let mut config = backend.config()?;
let backend = git2::Repository::open_bare(path.as_ref())?;
// TODO: Get ahold of user name and/or key.
config.set_str("user.name", "radicle")?;
config.set_str("user.email", "radicle@localhost")?;
Ok(Self { id, backend })
}
Ok(backend)
}
Ok(repo) => Ok(repo),
Err(e) => Err(e),
}?;
/// Create a new repository.
pub fn create<P: AsRef<Path>>(path: P, id: Id) -> Result<Self, Error> {
let backend = git2::Repository::init_opts(
&path,
git2::RepositoryInitOptions::new()
.bare(true)
.no_reinit(true)
.external_template(false),
)?;
let mut config = backend.config()?;
// TODO: Get ahold of user name and/or key.
config.set_str("user.name", "radicle")?;
config.set_str("user.email", "radicle@localhost")?;
Ok(Self { id, backend })
}
@ -241,7 +253,7 @@ impl Repository {
) -> Result<(Self, git::Oid), Error> {
let (doc_oid, doc) = doc.encode()?;
let id = Id::from(doc_oid);
let repo = Self::open(paths::repository(storage, &id), id)?;
let repo = Self::create(paths::repository(storage, &id), id)?;
let oid = Doc::init(
doc.as_slice(),
remote,
@ -730,7 +742,7 @@ mod tests {
let storage = Storage::open(tmp.path()).unwrap();
let proj_id = arbitrary::gen::<Id>(1);
let alice = *signer.public_key();
let project = storage.repository(proj_id).unwrap();
let project = storage.create(proj_id).unwrap();
let backend = &project.backend;
let sig = git2::Signature::now(&alice.to_string(), "anonymous@radicle.xyz").unwrap();
let head = git::initial_commit(backend, &sig).unwrap();

View File

@ -32,6 +32,8 @@ impl MockStorage {
}
impl ReadStorage for MockStorage {
type Repository = MockRepository;
fn path(&self) -> &Path {
self.path.as_path()
}
@ -51,12 +53,20 @@ impl ReadStorage for MockStorage {
fn inventory(&self) -> Result<Inventory, Error> {
Ok(self.inventory.keys().cloned().collect::<Vec<_>>())
}
fn repository(&self, _proj: Id) -> Result<Self::Repository, Error> {
Ok(MockRepository {})
}
}
impl WriteStorage for MockStorage {
type Repository = MockRepository;
type RepositoryMut = MockRepository;
fn repository(&self, _proj: Id) -> Result<Self::Repository, Error> {
fn repository_mut(&self, _rid: Id) -> Result<Self::RepositoryMut, Error> {
Ok(MockRepository {})
}
fn create(&self, _rid: Id) -> Result<Self::RepositoryMut, Error> {
Ok(MockRepository {})
}
}