Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
cli: Consolidate working copy remote setup
Alexis Sellier committed 2 years ago
commit e9ef0f4aa4b53a3f896ea9a06e14f8c367f8e00d
parent 5502676aa7ceb599c2434534d566567746f58a3f
13 files changed +293 -92
added radicle-cli/examples/rad-clone-all.md
@@ -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
+
```
modified radicle-cli/examples/rad-clone.md
@@ -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/
```
modified radicle-cli/examples/rad-remote.md
@@ -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
```
modified radicle-cli/examples/workflow/5-patching-maintainer.md
@@ -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)
modified radicle-cli/src/commands/checkout.rs
@@ -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(())
+
}
modified radicle-cli/src/commands/clone.rs
@@ -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!(
modified radicle-cli/src/commands/remote.rs
@@ -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)?,
    };
modified radicle-cli/src/commands/remote/add.rs
@@ -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)
}
modified radicle-cli/src/git.rs
@@ -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.
///
modified radicle-cli/src/project.rs
@@ -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))
    }
}
modified radicle-cli/src/terminal/args.rs
@@ -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 {
modified radicle-cli/tests/commands.rs
@@ -371,6 +371,47 @@ fn rad_clone() {
}

#[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();
    let alice = environment.node("alice");
modified radicle-node/src/test/environment.rs
@@ -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();