Skip to content
This repository has been archived by the owner on Aug 1, 2022. It is now read-only.

Commit

Permalink
fix(proxy): checkout peer (#1010)
Browse files Browse the repository at this point in the history
* WIP: Attempt to test the checkout behaviour between 3 peers

* Rework checkout logic

We set up checking out projects differing by where the project is coming
from.
If it's a owned by the user then we simply clone the repo calling
the remote `rad`.
If it's coming from another peer's branch then we set it up via their
remote. We then have to create the `rad` remote to set that up as our
upstream.

This all culminates in testing that we can clone from a specific peer
and that we don't end up getting changes from another peer that we know
about.

* Bump librad

* Bump librad

* Update Cargo.lock

* Update docs on handle field of Remote

* Fix test after the merge

* Reorganise and document

We move the clone functions as methods on Ownership to make things
clearer and compartmentalised.

We also document the checkout verbosely, because it's not very clear
from the actions take but is clearer when described and shown what the
final config looks like.

* Fixes to the UI code

- remote should have been called peerId and is optional
- passing the PeerId was stubbed so we pass down the currentPeerId now
- the endpoint didn't have the full path since it was missing
`/checkout`. It must have been a fluke that it was a match regardless.

* Fix fetch refspecs

It turns out we were using the refspecs and the URLs wrong. We create the
correct refspecs here and clean up the documentation and parameter
passing.

* Formattergit diffgit diffgit diff!

* Whoop(s)

* Move guards to http

Since we need the guard for self peer id in checkout as well, we move
them to the http module so that they can be reused. But they should
eventually be removed once we figure out how to handle this better.

* Cliippeeee

* Pass the peer_id to get_project

* Get out the clippers
  • Loading branch information
FintanH committed Oct 15, 2020
1 parent 77326ba commit bdcec04
Show file tree
Hide file tree
Showing 9 changed files with 337 additions and 74 deletions.
20 changes: 10 additions & 10 deletions proxy/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 31 additions & 0 deletions proxy/api/src/http.rs
Expand Up @@ -207,6 +207,37 @@ where
.boxed()
}

/// Guard against access of wrong paths by the owners peer id.
#[must_use]
pub fn guard_self_peer_id(
state: &coco::State,
peer_id: Option<coco::PeerId>,
) -> Option<coco::PeerId> {
match peer_id {
Some(peer_id) if peer_id == state.peer_id() => None,
Some(peer_id) => Some(peer_id),
None => None,
}
}

/// Guard against access of the wrong paths by the owners peer id when inside a `Revision`.
#[must_use]
pub fn guard_self_revision(
state: &coco::State,
revision: Option<coco::Revision<coco::PeerId>>,
) -> Option<coco::Revision<coco::PeerId>> {
revision.map(|r| {
if let coco::Revision::Branch { name, peer_id } = r {
coco::Revision::Branch {
name,
peer_id: guard_self_peer_id(state, peer_id),
}
} else {
r
}
})
}

#[cfg(test)]
mod test {
use super::*;
Expand Down
1 change: 1 addition & 0 deletions proxy/api/src/http/project.rs
Expand Up @@ -176,6 +176,7 @@ mod handler {
urn: coco::Urn,
super::CheckoutInput { path, peer_id }: super::CheckoutInput,
) -> Result<impl Reply, Rejection> {
let peer_id = http::guard_self_peer_id(&ctx.state, peer_id);
let path = ctx
.state
.checkout(urn, peer_id, path)
Expand Down
40 changes: 7 additions & 33 deletions proxy/api/src/http/source.rs
Expand Up @@ -115,7 +115,7 @@ fn tree_filter(
mod handler {
use warp::{path::Tail, reply, Rejection, Reply};

use coco::{oid, PeerId};
use coco::oid;

use crate::{context, error, session, session::settings};

Expand All @@ -131,8 +131,8 @@ mod handler {
}: super::BlobQuery,
) -> Result<impl Reply, Rejection> {
let current_session = session::current(ctx.state.clone(), &ctx.store).await?;
let peer_id = guard_self_peer_id(&ctx.state, peer_id);
let revision = guard_self_revision(&ctx.state, revision);
let peer_id = super::http::guard_self_peer_id(&ctx.state, peer_id);
let revision = super::http::guard_self_revision(&ctx.state, revision);

let theme = if let Some(true) = highlight {
Some(match &current_session.settings.appearance.theme {
Expand Down Expand Up @@ -165,7 +165,7 @@ mod handler {
project_urn: coco::Urn,
super::BranchQuery { peer_id }: super::BranchQuery,
) -> Result<impl Reply, Rejection> {
let peer_id = guard_self_peer_id(&ctx.state, peer_id);
let peer_id = super::http::guard_self_peer_id(&ctx.state, peer_id);
let default_branch = ctx
.state
.get_branch(project_urn, peer_id, None)
Expand Down Expand Up @@ -210,7 +210,7 @@ mod handler {
project_urn: coco::Urn,
mut query: super::CommitsQuery,
) -> Result<impl Reply, Rejection> {
let peer_id = guard_self_peer_id(&ctx.state, query.peer_id);
let peer_id = super::http::guard_self_peer_id(&ctx.state, query.peer_id);
query.peer_id = peer_id;

let default_branch = ctx
Expand Down Expand Up @@ -304,8 +304,8 @@ mod handler {
revision,
}: super::TreeQuery,
) -> Result<impl Reply, Rejection> {
let peer_id = guard_self_peer_id(&ctx.state, peer_id);
let revision = guard_self_revision(&ctx.state, revision);
let peer_id = super::http::guard_self_peer_id(&ctx.state, peer_id);
let revision = super::http::guard_self_revision(&ctx.state, revision);
let branch = ctx
.state
.get_branch(project_urn, peer_id, None)
Expand All @@ -321,32 +321,6 @@ mod handler {

Ok(reply::json(&tree))
}

/// Guard against access of wrong paths by the owners peer id.
fn guard_self_peer_id(state: &coco::State, peer_id: Option<PeerId>) -> Option<PeerId> {
match peer_id {
Some(peer_id) if peer_id == state.peer_id() => None,
Some(peer_id) => Some(peer_id),
None => None,
}
}

/// Guard against access of the wrong paths by the owners peer id when inside a `Revision`.
fn guard_self_revision(
state: &coco::State,
revision: Option<coco::Revision<PeerId>>,
) -> Option<coco::Revision<PeerId>> {
revision.map(|r| {
if let coco::Revision::Branch { name, peer_id } = r {
coco::Revision::Branch {
name,
peer_id: guard_self_peer_id(state, peer_id),
}
} else {
r
}
})
}
}

/// Bundled query params to pass to the commits handler.
Expand Down
164 changes: 147 additions & 17 deletions proxy/coco/src/project/checkout.rs
@@ -1,15 +1,23 @@
use std::{
ffi,
marker::PhantomData,
path::{self, PathBuf},
};

pub use librad::meta::project::Project;
use librad::{
git::{include, local::url::LocalUrl, types::remote::Remote},
git::{
include,
local::url::LocalUrl,
types::{remote::Remote, FlatRef, Force},
},
peer::PeerId,
uri::RadUrn,
};
use radicle_surf::vcs::git::git2;

use crate::config;

/// When checking out a working copy, we can run into several I/O failures.
#[derive(Debug, thiserror::Error)]
pub enum Error {
Expand All @@ -35,6 +43,95 @@ where
include_path: PathBuf,
}

/// We want to know whether we're checking out from one of our own copies, or if we're checking out
/// based off of a remote's branch.
pub enum Ownership {
/// We're checking out our own copy of the project.
Local(PeerId),
/// We're checking out a remote's version of the project.
Remote {
/// The handle of the remote peer gives themselves via their user profile. For example,
/// `90s-kid` -- the name of the remote will then be `90s-kid@<urn.id>`.
handle: String,
/// The `PeerId` of the remote.
remote: PeerId,
/// Our own `PeerId`.
local: PeerId,
},
}

impl Ownership {
/// Clone a project based off of the `Ownership` value. See [`Checkout::run`] for more details.
///
/// # Errors
/// * If the cloning of the working copy fails.
/// * In the case of a remote clone, if the pushing of the default branch fails.
pub fn clone(
self,
urn: RadUrn,
default_branch: &str,
path: &path::Path,
builder: &mut git2::build::RepoBuilder,
) -> Result<git2::Repository, git2::Error> {
match self {
Self::Local(peer_id) => {
let url = LocalUrl::from_urn(urn, peer_id);
Self::local(&url, path, builder)
},
Self::Remote {
handle,
remote,
local,
} => {
let url = LocalUrl::from_urn(urn, local);
Self::remote(&handle, remote, url, default_branch, path, builder)
},
}
}

/// See [`Checkout::run`].
fn local(
url: &LocalUrl,
path: &path::Path,
builder: &mut git2::build::RepoBuilder,
) -> Result<git2::Repository, git2::Error> {
builder.remote_create(|repo, _remote_name, url| repo.remote(config::RAD_REMOTE, url));
git2::build::RepoBuilder::clone(builder, &url.to_string(), path)
}

/// See [`Checkout::run`].
fn remote(
handle: &str,
peer: PeerId,
url: LocalUrl,
default_branch: &str,
path: &path::Path,
builder: &mut git2::build::RepoBuilder,
) -> Result<git2::Repository, git2::Error> {
let name = format!("{}@{}", handle, peer);
{
builder.remote_create(move |repo, _remote_name, url| {
let mut remote = Remote::new(url, name.clone());
let heads = FlatRef::heads(PhantomData, peer).with_name("heads/*");
let remotes = FlatRef::heads(PhantomData, name.clone());
remote.fetch_spec = Some(remotes.refspec(heads, Force::True).into_dyn());
remote.create(repo)
});
}

let repo = git2::build::RepoBuilder::clone(builder, &url.to_string(), path)?;

// Create a rad remote and push the default branch so we can set it as the
// upstream.
{
let mut remote = Remote::rad_remote(url, None).create(&repo)?;
remote.push(&[&format!("refs/heads/{}", default_branch)], None)?;
}

Ok(repo)
}
}

impl<P, ST> Checkout<P, ST>
where
P: AsRef<path::Path>,
Expand All @@ -49,17 +146,52 @@ where
}
}

/// Checkout a working copy of a [`Project`].
/// Based off of the `Ownership`, clone the project using the provided inputs.
///
/// NOTE: `RAD_HOME` should be expected to be set if using a custom root for
/// [`librad::paths::Paths`]. If it is not set the underlying binary will delegate to the
/// `ProjectDirs` setup of the `Paths`.
/// ## Local Clone
///
/// # Errors
/// If the `Ownership` is `Local` this means that we are cloning based off the user's own
/// project and so the `url` used to clone will be built from the user's `PeerId`. The only
/// remote that will be created is `rad` remote, pointing to the `url` built from the
/// provided `urn` and the user's `PeerId`.
///
/// ## Remote Clone
///
/// If the `Ownership` is `Remote` this means that we are cloning based off of a peer's
/// project.
/// Due to this we need to point the remote to the specific remote in our project's hierarchy.
/// What this means is that we need to set up a fetch refspec in the form of
/// `refs/remotes/<peer_id>/heads/*` where the name of the remote is given by
/// `<user_handle>@<peer_id>` -- this keeps in line with `librad::git::include`. To finalise
/// the setup of the clone, we also want to add the `rad` remote, which is the designated
/// remote the user pushes their own work to update their monorepo for this project.
/// To do this, we create a `url` that is built using the provided `urn` and the user's `PeerId`
/// and create the `rad` remote. Finally, we initialise the `default_branch` of the proejct --
/// think upstream branch in git. We do this by pushing to the `rad` remote. This means that
/// the working copy will be now setup where when we open it up we see the initial branch as
/// being `default_branch`.
///
/// * We couldn't resolve the executable path.
/// * The checkout process failed.
pub fn run(self, peer_id: PeerId) -> Result<PathBuf, Error> {
/// To illustrate further, the `config` of the final repository will look similar to:
///
/// ```text
/// [remote "rad"]
/// url = rad://hyymr17h1fg5zk7duikgc7xoqonqorhwnxxs98kdb63f9etnsjxxmo@hwd1yrerzpjbmtshsqw6ajokqtqrwaswty6p7kfeer3yt1n76t46iqggzcr.git
/// fetch = +refs/heads/*:refs/remotes/rad/*
/// [remote "banana@hyy36ey56mfayah398n7w4i8hy5ywci43hbyhwf1krfwonc1ur87ch"]
/// url = rad://hyymr17h1fg5zk7duikgc7xoqonqorhwnxxs98kdb63f9etnsjxxmo@hwd1yrerzpjbmtshsqw6ajokqtqrwaswty6p7kfeer3yt1n76t46iqggzcr.git
/// fetch = +refs/remotes/hyy36ey56mfayah398n7w4i8hy5ywci43hbyhwf1krfwonc1ur87ch/heads/*:refs/remotes/banana@hyy36ey56mfayah398n7w4i8hy5ywci43hbyhwf1krfwonc1ur87ch/*
/// [branch "master"]
/// remote = rad
/// merge = refs/heads/master
/// [include]
/// path = /home/user/.config/radicle/git-includes/hwd1yrerzpjbmtshsqw6ajokqtqrwaswty6p7kfeer3yt1n76t46iqggzcr.inc
/// ```
///
/// # Errors
/// * If the project cloning fails.
/// * If we cannot set the upstream branch for the `rad` remote.
/// * If we cannot set the include path for the working copy.
pub fn run(self, ownership: Ownership) -> Result<PathBuf, Error> {
// Check if the path provided ends in the 'directory_name' provided. If not we create the
// full path to that name.
let path = &self.path.as_ref();
Expand All @@ -76,20 +208,18 @@ where
path.join(&self.project.name().to_string())
};

// Clone the repository
let mut builder = git2::build::RepoBuilder::new();
builder.branch(self.project.default_branch());
builder.remote_create(|repo, _, url| {
let remote = Remote::rad_remote(url, None).create(repo)?;
Ok(remote)
});
let repo = git2::build::RepoBuilder::clone(
&mut builder,
&LocalUrl::from_urn(self.project.urn(), peer_id).to_string(),
let repo = ownership.clone(
self.project.urn(),
self.project.default_branch(),
&project_path,
&mut builder,
)?;

// Set configurations
super::set_rad_upstream(&repo, self.project.default_branch())?;

include::set_include_path(&repo, self.include_path)?;

Ok(project_path)
Expand Down

0 comments on commit bdcec04

Please sign in to comment.