Set repository canonical refs
We set the `HEAD` and eg. `refs/heads/master` refs when updating repositories, which allows users to easily clone with git. For now, `HEAD` is chosen based on a unanimity quorum of delegates. This can be made more sophisticated in the future. We've also had to ignore some refs during verification, since these have cropped in for eg. due to the staging copy clone and the changes included here. Signed-off-by: Alexis Sellier <alexis@radicle.xyz>
This commit is contained in:
parent
10d8519101
commit
3bff9a9dc1
|
|
@ -8,7 +8,7 @@ use thiserror::Error;
|
||||||
use radicle::crypto::{PublicKey, Signer};
|
use radicle::crypto::{PublicKey, Signer};
|
||||||
use radicle::node::Handle;
|
use radicle::node::Handle;
|
||||||
use radicle::ssh;
|
use radicle::ssh;
|
||||||
use radicle::storage::{ReadRepository, WriteStorage};
|
use radicle::storage::{ReadRepository, WriteRepository, WriteStorage};
|
||||||
|
|
||||||
/// The service invoked by git on the remote repository, during a push.
|
/// The service invoked by git on the remote repository, during a push.
|
||||||
const GIT_RECEIVE_PACK: &str = "git-receive-pack";
|
const GIT_RECEIVE_PACK: &str = "git-receive-pack";
|
||||||
|
|
@ -180,6 +180,7 @@ pub fn run(profile: radicle::Profile) -> Result<(), Box<dyn std::error::Error +
|
||||||
if child.wait()?.success() {
|
if child.wait()?.success() {
|
||||||
if *service == GIT_RECEIVE_PACK {
|
if *service == GIT_RECEIVE_PACK {
|
||||||
profile.storage.sign_refs(&proj, &profile.signer)?;
|
profile.storage.sign_refs(&proj, &profile.signer)?;
|
||||||
|
proj.set_head()?;
|
||||||
// Connect to local node and announce refs to the network.
|
// Connect to local node and announce refs to the network.
|
||||||
// If our node is not running, we simply skip this step, as the
|
// If our node is not running, we simply skip this step, as the
|
||||||
// refs will be announced eventually, when the node restarts.
|
// refs will be announced eventually, when the node restarts.
|
||||||
|
|
|
||||||
|
|
@ -118,24 +118,47 @@ pub fn remote_refs(url: &Url) -> Result<HashMap<RemoteId, Refs>, ListRefsError>
|
||||||
|
|
||||||
let refs = remote.list()?;
|
let refs = remote.list()?;
|
||||||
for r in refs {
|
for r in refs {
|
||||||
let (id, refname) = parse_ref::<PublicKey>(r.name())?;
|
// Skip the `HEAD` reference, as it is untrusted.
|
||||||
let entry = remotes.entry(id).or_insert_with(Refs::default);
|
if r.name() == "HEAD" {
|
||||||
|
continue;
|
||||||
entry.insert(refname.into(), r.oid().into());
|
}
|
||||||
|
// Nb. skip refs that don't have a public key namespace.
|
||||||
|
if let (Some(id), refname) = parse_ref::<PublicKey>(r.name())? {
|
||||||
|
let entry = remotes.entry(id).or_insert_with(Refs::default);
|
||||||
|
entry.insert(refname.into(), r.oid().into());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(remotes)
|
Ok(remotes)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a ref string.
|
/// Parse a ref string. Returns an error if it isn't namespaced.
|
||||||
pub fn parse_ref<T>(s: &str) -> Result<(T, format::Qualified), RefError>
|
pub fn parse_ref_namespaced<T>(s: &str) -> Result<(T, format::Qualified), RefError>
|
||||||
|
where
|
||||||
|
T: FromStr,
|
||||||
|
T::Err: std::error::Error + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
match parse_ref::<T>(s) {
|
||||||
|
Ok((None, refname)) => Err(RefError::MissingNamespace(refname.to_ref_string())),
|
||||||
|
Ok((Some(t), r)) => Ok((t, r)),
|
||||||
|
Err(err) => Err(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a ref string. Optionally returns a namespace.
|
||||||
|
pub fn parse_ref<T>(s: &str) -> Result<(Option<T>, format::Qualified), RefError>
|
||||||
where
|
where
|
||||||
T: FromStr,
|
T: FromStr,
|
||||||
T::Err: std::error::Error + Send + Sync + 'static,
|
T::Err: std::error::Error + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
let input = format::RefStr::try_from_str(s)?;
|
let input = format::RefStr::try_from_str(s)?;
|
||||||
match input.to_namespaced() {
|
match input.to_namespaced() {
|
||||||
None => Err(RefError::MissingNamespace(input.to_owned())),
|
None => {
|
||||||
|
let refname = Qualified::from_refstr(input)
|
||||||
|
.ok_or_else(|| RefError::InvalidName(input.to_owned()))?;
|
||||||
|
|
||||||
|
Ok((None, refname))
|
||||||
|
}
|
||||||
Some(ns) => {
|
Some(ns) => {
|
||||||
let id = ns
|
let id = ns
|
||||||
.namespace()
|
.namespace()
|
||||||
|
|
@ -146,7 +169,8 @@ where
|
||||||
err: Box::new(err),
|
err: Box::new(err),
|
||||||
})?;
|
})?;
|
||||||
let rest = ns.strip_namespace();
|
let rest = ns.strip_namespace();
|
||||||
Ok((id, rest))
|
|
||||||
|
Ok((Some(id), rest))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -509,7 +509,7 @@ mod test {
|
||||||
let oid = git2::Oid::from_str("2d52a53ce5e4f141148a5f770cfd3ead2d6a45b8").unwrap();
|
let oid = git2::Oid::from_str("2d52a53ce5e4f141148a5f770cfd3ead2d6a45b8").unwrap();
|
||||||
|
|
||||||
let err = Doc::<Unverified>::head(&remote, &repo).unwrap_err();
|
let err = Doc::<Unverified>::head(&remote, &repo).unwrap_err();
|
||||||
assert!(dbg!(err).is_not_found());
|
assert!(err.is_not_found());
|
||||||
|
|
||||||
let err = Doc::load_at(oid.into(), &repo).unwrap_err();
|
let err = Doc::load_at(oid.into(), &repo).unwrap_err();
|
||||||
assert!(err.is_not_found());
|
assert!(err.is_not_found());
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ use std::io;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use git_ref_format::{lit, Qualified};
|
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
|
@ -23,6 +22,8 @@ pub static REMOTE_NAME: Lazy<git::RefString> = Lazy::new(|| git::refname!("rad")
|
||||||
pub enum InitError {
|
pub enum InitError {
|
||||||
#[error("doc: {0}")]
|
#[error("doc: {0}")]
|
||||||
Doc(#[from] identity::project::DocError),
|
Doc(#[from] identity::project::DocError),
|
||||||
|
#[error("project: {0}")]
|
||||||
|
Project(#[from] storage::git::ProjectError),
|
||||||
#[error("doc: {0}")]
|
#[error("doc: {0}")]
|
||||||
DocVerification(#[from] identity::project::VerificationError),
|
DocVerification(#[from] identity::project::VerificationError),
|
||||||
#[error("git: {0}")]
|
#[error("git: {0}")]
|
||||||
|
|
@ -76,6 +77,7 @@ pub fn init<G: Signer, S: storage::WriteStorage>(
|
||||||
git::configure_remote(repo, &REMOTE_NAME, &url)?;
|
git::configure_remote(repo, &REMOTE_NAME, &url)?;
|
||||||
git::push(repo, &REMOTE_NAME, pk, [(&default_branch, &default_branch)])?;
|
git::push(repo, &REMOTE_NAME, pk, [(&default_branch, &default_branch)])?;
|
||||||
let signed = storage.sign_refs(&project, signer)?;
|
let signed = storage.sign_refs(&project, signer)?;
|
||||||
|
let _head = project.set_head()?;
|
||||||
|
|
||||||
Ok((id, signed))
|
Ok((id, signed))
|
||||||
}
|
}
|
||||||
|
|
@ -158,27 +160,15 @@ pub fn fork<G: Signer, S: storage::WriteStorage>(
|
||||||
let me = signer.public_key();
|
let me = signer.public_key();
|
||||||
let repository = storage.repository(proj)?;
|
let repository = storage.repository(proj)?;
|
||||||
let (canonical_id, project) = repository.project_identity()?;
|
let (canonical_id, project) = repository.project_identity()?;
|
||||||
|
let (canonical_head, _) = repository.head()?;
|
||||||
let raw = repository.raw();
|
let raw = repository.raw();
|
||||||
// TODO: Test the fork functions in isolation.
|
|
||||||
// TODO: Move to function on `Repository`.
|
|
||||||
let canonical_head = {
|
|
||||||
let mut heads = Vec::new();
|
|
||||||
for delegate in project.delegates.iter() {
|
|
||||||
let refname = Qualified::from(lit::refs_heads(&project.default_branch));
|
|
||||||
let r = repository.reference_oid(&delegate.id, &refname)?.into();
|
|
||||||
heads.push(r);
|
|
||||||
}
|
|
||||||
|
|
||||||
match heads.as_slice() {
|
// TODO: We should only get the project HEAD once we've stored the canonical identity
|
||||||
[head] => Ok(*head),
|
// branch on disk. This way it can use what we stored, instead of recomputing it.
|
||||||
// FIXME: This branch is not tested.
|
|
||||||
heads => raw.merge_base_many(heads),
|
|
||||||
}
|
|
||||||
}?;
|
|
||||||
|
|
||||||
raw.reference(
|
raw.reference(
|
||||||
&git::refs::storage::branch(me, &project.default_branch),
|
&git::refs::storage::branch(me, &project.default_branch),
|
||||||
canonical_head,
|
*canonical_head,
|
||||||
false,
|
false,
|
||||||
&format!("creating default branch for {me}"),
|
&format!("creating default branch for {me}"),
|
||||||
)?;
|
)?;
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,9 @@ pub enum FetchError {
|
||||||
Verify(#[from] git::VerifyError),
|
Verify(#[from] git::VerifyError),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Storage(#[from] Error),
|
Storage(#[from] Error),
|
||||||
|
// TODO: This should wrap a more specific error.
|
||||||
|
#[error("repository head: {0}")]
|
||||||
|
SetHead(#[from] ProjectError),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type RemoteId = PublicKey;
|
pub type RemoteId = PublicKey;
|
||||||
|
|
@ -229,6 +232,7 @@ pub trait WriteStorage: ReadStorage {
|
||||||
type Repository: WriteRepository;
|
type Repository: WriteRepository;
|
||||||
|
|
||||||
fn repository(&self, proj: Id) -> Result<Self::Repository, Error>;
|
fn repository(&self, proj: Id) -> Result<Self::Repository, Error>;
|
||||||
|
// TODO: Move this to `WriteRepository`.
|
||||||
fn sign_refs<G: Signer>(
|
fn sign_refs<G: Signer>(
|
||||||
&self,
|
&self,
|
||||||
repository: &Self::Repository,
|
repository: &Self::Repository,
|
||||||
|
|
@ -246,6 +250,11 @@ pub trait ReadRepository {
|
||||||
|
|
||||||
fn blob_at<'a>(&'a self, oid: Oid, path: &'a Path) -> Result<git2::Blob<'a>, git_ext::Error>;
|
fn blob_at<'a>(&'a self, oid: Oid, path: &'a Path) -> Result<git2::Blob<'a>, git_ext::Error>;
|
||||||
|
|
||||||
|
/// Get the canonical head of this repository.
|
||||||
|
///
|
||||||
|
/// Returns the [`Oid`] as well as the qualified reference name.
|
||||||
|
fn head(&self) -> Result<(Oid, Qualified), ProjectError>;
|
||||||
|
|
||||||
/// Get the `reference` for the given `remote`.
|
/// Get the `reference` for the given `remote`.
|
||||||
///
|
///
|
||||||
/// Returns `None` is the reference did not exist.
|
/// Returns `None` is the reference did not exist.
|
||||||
|
|
@ -276,6 +285,7 @@ pub trait ReadRepository {
|
||||||
|
|
||||||
pub trait WriteRepository: ReadRepository {
|
pub trait WriteRepository: ReadRepository {
|
||||||
fn fetch(&mut self, url: &Url) -> Result<Vec<RefUpdate>, FetchError>;
|
fn fetch(&mut self, url: &Url) -> Result<Vec<RefUpdate>, FetchError>;
|
||||||
|
fn set_head(&self) -> Result<Oid, ProjectError>;
|
||||||
fn raw(&self) -> &git2::Repository;
|
fn raw(&self) -> &git2::Repository;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ pub static REMOTES_GLOB: Lazy<refspec::PatternString> =
|
||||||
pub static SIGNATURES_GLOB: Lazy<refspec::PatternString> =
|
pub static SIGNATURES_GLOB: Lazy<refspec::PatternString> =
|
||||||
Lazy::new(|| refspec::pattern!("refs/namespaces/*/radicle/signature"));
|
Lazy::new(|| refspec::pattern!("refs/namespaces/*/radicle/signature"));
|
||||||
|
|
||||||
|
// TODO: Is this is the wrong place for this type?
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum ProjectError {
|
pub enum ProjectError {
|
||||||
#[error("identity branches diverge from each other")]
|
#[error("identity branches diverge from each other")]
|
||||||
|
|
@ -228,12 +229,9 @@ impl Repository {
|
||||||
Ok(Self { id, backend })
|
Ok(Self { id, backend })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn head(&self) -> Result<git2::Commit, git2::Error> {
|
/// Verify all references in the repository, checking that they are signed
|
||||||
// TODO: Find longest history, get document and get head.
|
/// as part of 'sigrefs'. Also verify that no signed reference is missing
|
||||||
// Perhaps we should even set a local `HEAD` or at least `refs/heads/master`
|
/// from the repository.
|
||||||
todo!();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn verify(&self) -> Result<(), VerifyError> {
|
pub fn verify(&self) -> Result<(), VerifyError> {
|
||||||
let mut remotes: HashMap<RemoteId, Refs> = self
|
let mut remotes: HashMap<RemoteId, Refs> = self
|
||||||
.remotes()?
|
.remotes()?
|
||||||
|
|
@ -243,15 +241,32 @@ impl Repository {
|
||||||
})
|
})
|
||||||
.collect::<Result<_, VerifyError>>()?;
|
.collect::<Result<_, VerifyError>>()?;
|
||||||
|
|
||||||
|
// TODO: We could create a higher level `references` method that skips
|
||||||
|
// canonical/unverifiable refs.
|
||||||
for r in self.backend.references()? {
|
for r in self.backend.references()? {
|
||||||
let r = r?;
|
let r = r?;
|
||||||
|
|
||||||
let name = r.name().ok_or(VerifyError::InvalidRef)?;
|
let name = r.name().ok_or(VerifyError::InvalidRef)?;
|
||||||
let oid = r.target().ok_or(VerifyError::InvalidRef)?;
|
let oid = if let Some(oid) = r.target() {
|
||||||
|
oid
|
||||||
|
} else {
|
||||||
|
// Ignore symbolic refs, eg. `HEAD`.
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
let (remote_id, refname) = git::parse_ref::<RemoteId>(name)?;
|
let (remote_id, refname) = git::parse_ref::<RemoteId>(name)?;
|
||||||
|
let remote_id = if let Some(remote_id) = remote_id {
|
||||||
|
remote_id
|
||||||
|
} else {
|
||||||
|
// Ignore refs that aren't part of a namespace, eg. canonical refs.
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
if refname == *refs::SIGNATURE_REF {
|
if refname == *refs::SIGNATURE_REF {
|
||||||
|
// Ignore the signed-refs reference, as this is what we're verifying.
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let remote = remotes
|
let remote = remotes
|
||||||
.get_mut(&remote_id)
|
.get_mut(&remote_id)
|
||||||
.ok_or(VerifyError::InvalidRemote(remote_id))?;
|
.ok_or(VerifyError::InvalidRemote(remote_id))?;
|
||||||
|
|
@ -358,7 +373,7 @@ impl Repository {
|
||||||
|reference| -> Result<RemoteId, refs::Error> {
|
|reference| -> Result<RemoteId, refs::Error> {
|
||||||
let r = reference?;
|
let r = reference?;
|
||||||
let name = r.name().ok_or(refs::Error::InvalidRef)?;
|
let name = r.name().ok_or(refs::Error::InvalidRef)?;
|
||||||
let (id, _) = git::parse_ref::<RemoteId>(name)?;
|
let (id, _) = git::parse_ref_namespaced::<RemoteId>(name)?;
|
||||||
|
|
||||||
Ok(id)
|
Ok(id)
|
||||||
},
|
},
|
||||||
|
|
@ -376,7 +391,7 @@ impl Repository {
|
||||||
|reference| -> Result<_, _> {
|
|reference| -> Result<_, _> {
|
||||||
let r = reference?;
|
let r = reference?;
|
||||||
let name = r.name().ok_or(refs::Error::InvalidRef)?;
|
let name = r.name().ok_or(refs::Error::InvalidRef)?;
|
||||||
let (id, _) = git::parse_ref::<RemoteId>(name)?;
|
let (id, _) = git::parse_ref_namespaced::<RemoteId>(name)?;
|
||||||
let remote = self.remote(&id)?;
|
let remote = self.remote(&id)?;
|
||||||
|
|
||||||
Ok((id, remote))
|
Ok((id, remote))
|
||||||
|
|
@ -440,6 +455,7 @@ impl ReadRepository for Repository {
|
||||||
remote: &RemoteId,
|
remote: &RemoteId,
|
||||||
reference: &git::Qualified,
|
reference: &git::Qualified,
|
||||||
) -> Result<Oid, git::Error> {
|
) -> Result<Oid, git::Error> {
|
||||||
|
// TODO: Use native git2 function for this.
|
||||||
let oid = self
|
let oid = self
|
||||||
.reference(remote, reference)?
|
.reference(remote, reference)?
|
||||||
.target()
|
.target()
|
||||||
|
|
@ -486,6 +502,31 @@ impl ReadRepository for Repository {
|
||||||
fn project_identity(&self) -> Result<(Oid, identity::Doc<Unverified>), ProjectError> {
|
fn project_identity(&self) -> Result<(Oid, identity::Doc<Unverified>), ProjectError> {
|
||||||
Repository::project(self)
|
Repository::project(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn head(&self) -> Result<(Oid, Qualified), ProjectError> {
|
||||||
|
// TODO: We shouldn't need to re-construct the history here; we should use the cached
|
||||||
|
// document head of the "trusted" (local) user for this project.
|
||||||
|
// In the `fork` function for example, we call Repository::project_identity again,
|
||||||
|
// This should only be necessary once.
|
||||||
|
let (_, project) = self.project_identity()?;
|
||||||
|
let branch_ref = Qualified::from(lit::refs_heads(&project.default_branch));
|
||||||
|
let raw = self.raw();
|
||||||
|
|
||||||
|
let mut heads = Vec::new();
|
||||||
|
for delegate in project.delegates.iter() {
|
||||||
|
let r = self.reference_oid(&delegate.id, &branch_ref)?.into();
|
||||||
|
|
||||||
|
heads.push(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
let oid = match heads.as_slice() {
|
||||||
|
[head] => Ok(*head),
|
||||||
|
// FIXME: This branch is not tested.
|
||||||
|
heads => raw.merge_base_many(heads),
|
||||||
|
}?;
|
||||||
|
|
||||||
|
Ok((oid.into(), branch_ref))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WriteRepository for Repository {
|
impl WriteRepository for Repository {
|
||||||
|
|
@ -598,10 +639,27 @@ impl WriteRepository for Repository {
|
||||||
// Fetch from the staging copy into the canonical repo.
|
// Fetch from the staging copy into the canonical repo.
|
||||||
remote.fetch(refs, Some(&mut opts), None)?;
|
remote.fetch(refs, Some(&mut opts), None)?;
|
||||||
}
|
}
|
||||||
|
// Set repository HEAD for git cloning support.
|
||||||
|
self.set_head()?;
|
||||||
|
|
||||||
Ok(updates)
|
Ok(updates)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_head(&self) -> Result<Oid, ProjectError> {
|
||||||
|
let head_ref = refname!("HEAD");
|
||||||
|
let (head, branch_ref) = self.head()?;
|
||||||
|
|
||||||
|
log::debug!("Setting ref {:?} -> {:?}", &branch_ref, head);
|
||||||
|
self.raw()
|
||||||
|
.reference(&branch_ref, *head, true, "set-local-branch (radicle)")?;
|
||||||
|
|
||||||
|
log::debug!("Setting ref {:?} -> {:?}", head_ref, branch_ref);
|
||||||
|
self.raw()
|
||||||
|
.reference_symbolic(&head_ref, &branch_ref, true, "set-head (radicle)")?;
|
||||||
|
|
||||||
|
Ok(head)
|
||||||
|
}
|
||||||
|
|
||||||
fn raw(&self) -> &git2::Repository {
|
fn raw(&self) -> &git2::Repository {
|
||||||
&self.backend
|
&self.backend
|
||||||
}
|
}
|
||||||
|
|
@ -788,6 +846,7 @@ mod tests {
|
||||||
.id();
|
.id();
|
||||||
git::push(&proj_repo, "rad", alice_id, [(&refname, &refname)]).unwrap();
|
git::push(&proj_repo, "rad", alice_id, [(&refname, &refname)]).unwrap();
|
||||||
alice.sign_refs(&alice_proj_storage, &alice_signer).unwrap();
|
alice.sign_refs(&alice_proj_storage, &alice_signer).unwrap();
|
||||||
|
alice_proj_storage.set_head().unwrap();
|
||||||
|
|
||||||
// Have Bob fetch Alice's new commit.
|
// Have Bob fetch Alice's new commit.
|
||||||
let updates = bob.repository(proj_id).unwrap().fetch(&alice_url).unwrap();
|
let updates = bob.repository(proj_id).unwrap().fetch(&alice_url).unwrap();
|
||||||
|
|
@ -887,7 +946,11 @@ mod tests {
|
||||||
target
|
target
|
||||||
.remote_anonymous(&format!("rad://{}", proj))
|
.remote_anonymous(&format!("rad://{}", proj))
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.fetch(&["refs/*:refs/*"], Some(&mut opts), None)
|
.fetch(
|
||||||
|
&["refs/namespaces/*:refs/namespaces/*"],
|
||||||
|
Some(&mut opts),
|
||||||
|
None,
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
stream.shutdown(net::Shutdown::Both).unwrap();
|
stream.shutdown(net::Shutdown::Both).unwrap();
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use git_ref_format as fmt;
|
||||||
use git_url::Url;
|
use git_url::Url;
|
||||||
use radicle_git_ext as git_ext;
|
use radicle_git_ext as git_ext;
|
||||||
|
|
||||||
|
|
@ -85,6 +86,10 @@ impl ReadRepository for MockRepository {
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn head(&self) -> Result<(Oid, fmt::Qualified), ProjectError> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
fn path(&self) -> &std::path::Path {
|
fn path(&self) -> &std::path::Path {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
@ -152,4 +157,8 @@ impl WriteRepository for MockRepository {
|
||||||
fn raw(&self) -> &git2::Repository {
|
fn raw(&self) -> &git2::Repository {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_head(&self) -> Result<Oid, ProjectError> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue