cli: Consolidate working copy remote setup

We consolidate the setup done by `rad checkout`, `rad clone` and `rad
remote add`, so that they all check for node aliases, and are all
capable of setting up tracking branches etc.
This commit is contained in:
Alexis Sellier 2023-06-12 12:10:05 +02:00
parent 5502676aa7
commit e9ef0f4aa4
No known key found for this signature in database
13 changed files with 293 additions and 92 deletions

View File

@ -0,0 +1,76 @@
<!-- TODO: Currently, `rad clone`, even with `--scope all` will not fetch all remotes -->
<!-- We have to issue a separate `rad sync --fetch` -->
```
$ rad clone rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --scope all
✓ Tracking relationship established for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji with scope 'all'
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MknSL…StBU8Vi..
✓ Forking under z6Mkux1…nVhib7Z..
✓ Creating checkout in ./heartwood..
✓ Remote z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi added
✓ Remote-tracking branch z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master created for z6MknSL…StBU8Vi
✓ Repository successfully cloned under [..]/heartwood/
```
We can now have a look at the new working copy that was created from the cloned
repository:
```
$ cd heartwood
$ cat README
Hello World!
```
```
$ rad sync --fetch
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MknSL…StBU8Vi..
✓ Fetched repository from 1 seed(s)
```
Let's check that we have all the namespaces in storage:
```
$ rad inspect --refs
.
|-- z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
| `-- refs
| |-- heads
| | `-- master
| `-- rad
| |-- id
| `-- sigrefs
|-- z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
| `-- refs
| |-- heads
| | `-- master
| `-- rad
| |-- id
| `-- sigrefs
`-- z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z
`-- refs
|-- heads
| `-- master
`-- rad
|-- id
`-- sigrefs
```
We can then setup a git remote for `bob`:
```
$ rad remote add z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --name bob
✓ Remote bob added
```
And fetch his refs:
```
$ git fetch --all
Fetching rad
Fetching z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
Fetching bob
$ git branch --remotes
bob/master
rad/master
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master
```

View File

@ -2,12 +2,12 @@ To create a local copy of a repository on the radicle network, we use the
`clone` command, followed by the identifier or *RID* of the repository:
```
$ rad clone rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
✓ Tracking relationship established for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji with scope 'all'
$ rad clone rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --scope trusted
✓ Tracking relationship established for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji with scope 'trusted'
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MknSL…StBU8Vi..
✓ Forking under z6Mkt67…v4N1tRk..
✓ Creating checkout in ./heartwood..
✓ Remote z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi created
✓ Remote z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi added
✓ Remote-tracking branch z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master created for z6MknSL…StBU8Vi
✓ Repository successfully cloned under [..]/heartwood/
```

View File

@ -2,7 +2,7 @@ Now, let's add a bob as a new remote:
```
$ rad remote add did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --name bob
✓ Remote bob added with rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
✓ Remote bob added
```
Now, we can see that there is a new remote in the list of remotes:
@ -14,7 +14,8 @@ rad (canonical upstream) (fetch)
rad z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (push)
```
You can see both `bob` and `rad` as remotes. The `rad` remote is our personal remote of the project.
You can see both `bob` and `rad` as remotes. The `rad` remote is our personal
remote of the project.
When we're finished with the `bob` remote, we can remove it:
@ -23,9 +24,10 @@ $ rad remote rm bob
✓ Remote `bob` removed
```
Now, add another time `bob` but without specify the `name`, so we should be able to fetch the node alias from our db!
Now, add another time `bob` but without specify the `name`, so we should be
able to fetch the node alias from our db!
```
$ rad remote add did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
✓ Remote bob added with rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
✓ Remote bob added
```

View File

@ -15,7 +15,7 @@ peer. Upcoming versions of radicle will not require this step.
```
$ rad remote add z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --name bob
✓ Remote bob added with rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
✓ Remote bob added
```
``` (stderr)

View File

@ -1,9 +1,13 @@
#![allow(clippy::box_default)]
use std::collections::HashMap;
use std::ffi::OsString;
use std::path::PathBuf;
use anyhow::anyhow;
use anyhow::Context as _;
use radicle::git;
use radicle::node::AliasStore;
use radicle::prelude::*;
use radicle::storage::git::transport;
@ -118,31 +122,63 @@ fn execute(options: Options, profile: &Profile) -> anyhow::Result<PathBuf> {
// Setup remote tracking branches for project delegates.
setup_remotes(
project::SetupRemote {
project: id,
default_branch: payload.default_branch().clone(),
rid: id,
tracking: Some(payload.default_branch().clone()),
repo: &repo,
fetch: true,
tracking: true,
},
&remotes,
profile,
)?;
Ok(path)
}
/// Setup a remote and tracking branch for each given remote.
pub fn setup_remotes(setup: project::SetupRemote, remotes: &[NodeId]) -> anyhow::Result<()> {
pub fn setup_remotes(
setup: project::SetupRemote,
remotes: &[NodeId],
profile: &Profile,
) -> anyhow::Result<()> {
let aliases = if let Ok(aliases) = profile.aliases() {
Box::new(aliases) as Box<dyn AliasStore>
} else {
Box::new(HashMap::new()) as Box<dyn AliasStore>
};
for remote_id in remotes {
if let Some((remote, branch)) = setup.run(*remote_id)? {
let remote = remote.name().unwrap(); // Only valid UTF-8 is used.
term::success!("Remote {} created", term::format::tertiary(remote));
term::success!(
"Remote-tracking branch {} created for {}",
term::format::tertiary(branch),
term::format::tertiary(term::format::node(remote_id))
);
if let Err(e) = setup_remote(&setup, remote_id, None, &aliases) {
term::warning(format!("Failed to setup remote for {remote_id}: {e}").as_str());
}
}
Ok(())
}
/// Setup a remote and tracking branch for the given remote.
pub fn setup_remote(
setup: &project::SetupRemote,
remote_id: &NodeId,
remote_name: Option<git::RefString>,
aliases: &impl AliasStore,
) -> anyhow::Result<()> {
let remote_name = if let Some(alias) = remote_name {
alias
} else {
let alias = aliases
.alias(remote_id)
.unwrap_or_else(|| remote_id.to_string());
git::RefString::try_from(alias.clone())
.map_err(|_| anyhow!("invalid remote name: '{alias}'"))?
};
let (remote, branch) = setup.run(remote_name, *remote_id)?;
term::success!("Remote {} added", term::format::tertiary(remote.name));
if let Some(branch) = branch {
term::success!(
"Remote-tracking branch {} created for {}",
term::format::tertiary(branch),
term::format::tertiary(term::format::node(remote_id))
);
}
Ok(())
}

View File

@ -126,13 +126,13 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
radicle::git::configure_repository(&working)?;
checkout::setup_remotes(
project::SetupRemote {
project: options.id,
default_branch,
rid: options.id,
tracking: Some(default_branch),
repo: &working,
fetch: true,
tracking: true,
},
&delegates,
&profile,
)?;
term::success!(

View File

@ -10,9 +10,10 @@ use std::ffi::OsString;
use anyhow::anyhow;
use radicle::git::RefString;
use radicle::prelude::NodeId;
use crate::terminal::args::{self, string, Error};
use crate::terminal::args;
use crate::terminal::{Args, Context, Help};
pub const HELP: Help = Help {
@ -44,8 +45,8 @@ pub enum OperationName {
#[derive(Debug)]
pub enum Operation {
Add { id: NodeId, name: Option<String> },
Rm { name: String },
Add { id: NodeId, name: Option<RefString> },
Rm { name: RefString },
List,
}
@ -61,16 +62,17 @@ impl Args for Options {
let mut parser = lexopt::Parser::from_args(args);
let mut op: Option<OperationName> = None;
let mut id: Option<NodeId> = None;
let mut name: Option<String> = None;
let mut name: Option<RefString> = None;
while let Some(arg) = parser.next()? {
match arg {
Long("help") => {
return Err(Error::Help.into());
return Err(args::Error::Help.into());
}
Long("name") | Short('n') => {
let value = parser.value()?;
let value = string(&value);
let value = args::refstring("name", value)?;
name = Some(value);
}
Value(val) if op.is_none() => match val.to_string_lossy().as_ref() {
@ -84,7 +86,10 @@ impl Args for Options {
id = Some(nid);
}
Value(val) if op == Some(OperationName::Rm) && name.is_none() => {
let val = string(&val);
let val = args::string(&val);
let val = RefString::try_from(val)
.map_err(|e| anyhow!("invalid remote name specified: {e}"))?;
name = Some(val);
}
_ => return Err(anyhow::anyhow!(arg.unexpected())),
@ -114,7 +119,8 @@ pub fn run(options: Options, ctx: impl Context) -> anyhow::Result<()> {
let profile = ctx.profile()?;
match options.op {
Operation::Add { ref id, name } => self::add::run(rid, id, name, &profile, &working)?,
// TODO: Support remote-tracking.
Operation::Add { ref id, name } => self::add::run(rid, id, name, None, &profile, &working)?,
Operation::Rm { ref name } => self::rm::run(name, &working)?,
Operation::List => self::list::run(&working)?,
};

View File

@ -1,31 +1,26 @@
use radicle::{git::Url, prelude::Id, Profile};
use radicle::git::RefString;
use radicle::prelude::*;
use radicle::Profile;
use radicle_crypto::PublicKey;
use crate::git::add_remote;
use crate::{git, terminal as term};
use crate::commands::rad_checkout as checkout;
use crate::git;
use crate::project::SetupRemote;
pub fn run(
id: Id,
pubkey: &PublicKey,
name: Option<String>,
rid: Id,
nid: &PublicKey,
name: Option<RefString>,
tracking: Option<BranchName>,
profile: &Profile,
repository: &git::Repository,
repo: &git::Repository,
) -> anyhow::Result<()> {
let name = match name {
Some(name) => name,
_ => profile
.tracking()?
.node_policy(pubkey)?
.and_then(|node| node.alias)
.ok_or(anyhow::anyhow!("a `name` needs to be specified"))?,
let aliases = profile.aliases()?;
let setup = SetupRemote {
rid,
tracking,
fetch: false,
repo,
};
if git::is_remote(repository, &name)? {
anyhow::bail!("remote `{name}` already exists");
}
let url = Url::from(id).with_namespace(*pubkey);
let remote = add_remote(repository, &name, &url)?;
term::success!("Remote {} added with {url}", remote.name,);
Ok(())
checkout::setup_remote(&setup, nid, name, &aliases)
}

View File

@ -4,6 +4,7 @@ use std::fmt::Display;
use std::fs::{File, OpenOptions};
use std::io;
use std::io::Write;
use std::ops::{Deref, DerefMut};
use std::path::{Path, PathBuf};
use std::process::Command;
use std::str::FromStr;
@ -74,11 +75,10 @@ pub enum RemoteError {
#[derive(Clone)]
pub struct Remote<'a> {
pub(crate) name: String,
pub(crate) url: radicle::git::Url,
pub(crate) pushurl: Option<radicle::git::Url>,
pub name: String,
pub url: radicle::git::Url,
pub pushurl: Option<radicle::git::Url>,
#[allow(dead_code)]
inner: git2::Remote<'a>,
}
@ -104,6 +104,20 @@ impl<'a> TryFrom<git2::Remote<'a>> for Remote<'a> {
}
}
impl<'a> Deref for Remote<'a> {
type Target = git2::Remote<'a>;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl<'a> DerefMut for Remote<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
/// Get the git repository in the current directory.
pub fn repository() -> Result<Repository, anyhow::Error> {
match Repository::open(".") {
@ -273,15 +287,6 @@ pub fn remove_remote(repo: &Repository, rid: &Id) -> anyhow::Result<()> {
}
}
pub fn add_remote<'a>(
repo: &'a Repository,
name: &'a str,
url: &'a radicle::git::Url,
) -> anyhow::Result<Remote<'a>> {
let remote = repo.remote(name, &url.to_string())?;
Ok(Remote::try_from(remote)?)
}
/// Setup an upstream tracking branch for the given remote and branch.
/// Creates the tracking branch if it does not exist.
///

View File

@ -1,46 +1,50 @@
use radicle::git::raw::Remote;
use radicle::git::RefString;
use radicle::prelude::*;
use crate::git;
use radicle::git::RefStr;
/// Setup a project remote and tracking branch.
pub struct SetupRemote<'a> {
/// The project id.
pub project: Id,
/// The project default branch.
pub default_branch: BranchName,
/// The repository in which to setup the remote.
pub repo: &'a git::Repository,
pub rid: Id,
/// Whether or not to setup a remote tracking branch.
pub tracking: Option<BranchName>,
/// Whether or not to fetch the remote immediately.
pub fetch: bool,
/// Whether or not to setup a remote tracking branch.
pub tracking: bool,
/// The repository in which to setup the remote.
pub repo: &'a git::Repository,
}
impl<'a> SetupRemote<'a> {
/// Run the setup for the given peer.
pub fn run(&self, node: NodeId) -> anyhow::Result<Option<(Remote, RefString)>> {
let url = radicle::git::Url::from(self.project).with_namespace(node);
let mut remote = radicle::git::configure_remote(self.repo, &node.to_string(), &url, &url)?;
pub fn run(
&self,
name: impl AsRef<RefStr>,
node: NodeId,
) -> anyhow::Result<(git::Remote, Option<BranchName>)> {
let remote_url = radicle::git::Url::from(self.rid).with_namespace(node);
let remote_name = name.as_ref();
if git::is_remote(self.repo, remote_name)? {
anyhow::bail!("remote `{remote_name}` already exists");
}
let remote =
radicle::git::configure_remote(self.repo, remote_name, &remote_url, &remote_url)?;
let mut remote = git::Remote::try_from(remote)?;
// Fetch the refs into the working copy.
if self.fetch {
remote.fetch::<&str>(&[], None, None)?;
}
// Setup remote-tracking branch.
if self.tracking {
// SAFETY: Node IDs are valid ref strings.
let node_ref = RefString::try_from(node.to_string()).unwrap();
let node_ref = node_ref.as_refstr();
let branch_name = node_ref.join(&self.default_branch);
let local_branch = radicle::git::refs::workdir::branch(
node_ref.join(&self.default_branch).as_refstr(),
);
radicle::git::set_upstream(self.repo, node.to_string(), &branch_name, local_branch)?;
if let Some(branch) = &self.tracking {
let tracking_branch = remote_name.join(branch);
let local_branch = radicle::git::refs::workdir::branch(tracking_branch.as_refstr());
radicle::git::set_upstream(self.repo, remote_name, &tracking_branch, local_branch)?;
return Ok(Some((remote, branch_name)));
return Ok((remote, Some(tracking_branch)));
}
Ok(None)
Ok((remote, None))
}
}

View File

@ -5,6 +5,7 @@ use anyhow::anyhow;
use radicle::cob::{self, issue, patch};
use radicle::crypto;
use radicle::git::RefString;
use radicle::node::Address;
use radicle::prelude::{Did, Id, NodeId};
@ -77,6 +78,20 @@ pub fn finish(unparsed: Vec<OsString>) -> anyhow::Result<()> {
Ok(())
}
pub fn refstring(flag: &str, value: OsString) -> anyhow::Result<RefString> {
RefString::try_from(
value
.into_string()
.map_err(|_| anyhow!("the value specified for '--{}' is not valid UTF-8", flag))?,
)
.map_err(|_| {
anyhow!(
"the value specified for '--{}' is not a valid ref string",
flag
)
})
}
pub fn did(val: &OsString) -> anyhow::Result<Did> {
let val = val.to_string_lossy();
let Ok(peer) = Did::from_str(&val) else {

View File

@ -370,6 +370,47 @@ fn rad_clone() {
test("examples/rad-clone.md", working, Some(&bob.home), []).unwrap();
}
#[test]
fn rad_clone_all() {
logger::init(log::Level::Debug);
let mut environment = Environment::new();
let mut alice = environment.node("alice");
let bob = environment.node("bob");
let eve = environment.node("eve");
let working = environment.tmp().join("working");
// Setup a test project.
let acme = alice.project("heartwood", "Radicle Heartwood Protocol & Stack");
let mut alice = alice.spawn(Config::default());
let mut bob = bob.spawn(Config::default());
let mut eve = eve.spawn(Config::default());
alice.handle.track_repo(acme, Scope::All).unwrap();
bob.connect(&alice).converge([&alice]);
eve.connect(&alice).converge([&alice]);
test(
"examples/rad-clone.md",
working.join("bob"),
Some(&bob.home),
[],
)
.unwrap();
bob.has_inventory_of(&acme, &alice.id);
alice.has_inventory_of(&acme, &bob.id);
test(
"examples/rad-clone-all.md",
working.join("eve"),
Some(&eve.home),
[],
)
.unwrap();
eve.has_inventory_of(&acme, &bob.id);
}
#[test]
fn rad_self() {
let mut environment = Environment::new();

View File

@ -24,7 +24,7 @@ use radicle::node::TRACKING_DB_FILE;
use radicle::profile::Home;
use radicle::profile::Profile;
use radicle::rad;
use radicle::storage::{ReadStorage as _, WriteRepository};
use radicle::storage::{ReadRepository, ReadStorage as _, WriteRepository};
use radicle::test::fixtures;
use radicle::Storage;
@ -220,6 +220,27 @@ impl<G: Signer + cyphernet::Ecdh> NodeHandle<G> {
}
}
/// Wait until this node has the inventory of another node.
#[track_caller]
pub fn has_inventory_of(&self, rid: &Id, nid: &NodeId) {
log::debug!(target: "test", "Waiting for {} to have {rid}/{nid}", self.id);
let events = self.handle.events();
loop {
if let Ok(repo) = self.storage.repository(*rid) {
if repo.identity_of(nid).is_ok() && repo.remote(nid).is_ok() {
break;
}
}
events
.wait(
|e| matches!(e, Event::RefsFetched { .. }),
time::Duration::from_secs(6),
)
.unwrap();
}
}
/// Run a `rad` CLI command.
pub fn rad<P: AsRef<Path>>(&self, cmd: &str, args: &[&str], cwd: P) -> io::Result<()> {
let cwd = cwd.as_ref();