diff --git a/radicle-cli/examples/rad-clone-all.md b/radicle-cli/examples/rad-clone-all.md new file mode 100644 index 00000000..8c1e18b9 --- /dev/null +++ b/radicle-cli/examples/rad-clone-all.md @@ -0,0 +1,76 @@ + + + +``` +$ 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 +``` diff --git a/radicle-cli/examples/rad-clone.md b/radicle-cli/examples/rad-clone.md index 5e3f024f..9149f130 100644 --- a/radicle-cli/examples/rad-clone.md +++ b/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/ ``` diff --git a/radicle-cli/examples/rad-remote.md b/radicle-cli/examples/rad-remote.md index 3dce99c9..7aafbc13 100644 --- a/radicle-cli/examples/rad-remote.md +++ b/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 ``` diff --git a/radicle-cli/examples/workflow/5-patching-maintainer.md b/radicle-cli/examples/workflow/5-patching-maintainer.md index 120ba864..e9f88ddb 100644 --- a/radicle-cli/examples/workflow/5-patching-maintainer.md +++ b/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) diff --git a/radicle-cli/src/commands/checkout.rs b/radicle-cli/src/commands/checkout.rs index 6ab3a9d5..a31390cc 100644 --- a/radicle-cli/src/commands/checkout.rs +++ b/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 { // 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 + } else { + Box::new(HashMap::new()) as Box + }; 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, + 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(()) +} diff --git a/radicle-cli/src/commands/clone.rs b/radicle-cli/src/commands/clone.rs index 141d6c8d..dbfb77f1 100644 --- a/radicle-cli/src/commands/clone.rs +++ b/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!( diff --git a/radicle-cli/src/commands/remote.rs b/radicle-cli/src/commands/remote.rs index bd17ff58..b294e7c7 100644 --- a/radicle-cli/src/commands/remote.rs +++ b/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 }, - Rm { name: String }, + Add { id: NodeId, name: Option }, + Rm { name: RefString }, List, } @@ -61,16 +62,17 @@ impl Args for Options { let mut parser = lexopt::Parser::from_args(args); let mut op: Option = None; let mut id: Option = None; - let mut name: Option = None; + let mut name: Option = 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)?, }; diff --git a/radicle-cli/src/commands/remote/add.rs b/radicle-cli/src/commands/remote/add.rs index 5764f721..7075b2b3 100644 --- a/radicle-cli/src/commands/remote/add.rs +++ b/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, + rid: Id, + nid: &PublicKey, + name: Option, + tracking: Option, 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) } diff --git a/radicle-cli/src/git.rs b/radicle-cli/src/git.rs index c7aed975..5cc28da3 100644 --- a/radicle-cli/src/git.rs +++ b/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, + pub name: String, + pub url: radicle::git::Url, + pub pushurl: Option, - #[allow(dead_code)] inner: git2::Remote<'a>, } @@ -104,6 +104,20 @@ impl<'a> TryFrom> 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 { 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> { - 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. /// diff --git a/radicle-cli/src/project.rs b/radicle-cli/src/project.rs index e42b1998..864a6a38 100644 --- a/radicle-cli/src/project.rs +++ b/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, /// 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> { - 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, + node: NodeId, + ) -> anyhow::Result<(git::Remote, Option)> { + 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)) } } diff --git a/radicle-cli/src/terminal/args.rs b/radicle-cli/src/terminal/args.rs index b607c77d..0fba2c10 100644 --- a/radicle-cli/src/terminal/args.rs +++ b/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) -> anyhow::Result<()> { Ok(()) } +pub fn refstring(flag: &str, value: OsString) -> anyhow::Result { + 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 { let val = val.to_string_lossy(); let Ok(peer) = Did::from_str(&val) else { diff --git a/radicle-cli/tests/commands.rs b/radicle-cli/tests/commands.rs index 0298055a..a3cb2e94 100644 --- a/radicle-cli/tests/commands.rs +++ b/radicle-cli/tests/commands.rs @@ -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(); diff --git a/radicle-node/src/test/environment.rs b/radicle-node/src/test/environment.rs index 6d608a2c..6e64a8f5 100644 --- a/radicle-node/src/test/environment.rs +++ b/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 NodeHandle { } } + /// 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>(&self, cmd: &str, args: &[&str], cwd: P) -> io::Result<()> { let cwd = cwd.as_ref();