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: `clone` command, followed by the identifier or *RID* of the repository:
``` ```
$ rad clone rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji $ rad clone rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --scope trusted
✓ Tracking relationship established for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji with scope 'all' ✓ Tracking relationship established for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji with scope 'trusted'
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MknSL…StBU8Vi.. ✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MknSL…StBU8Vi..
✓ Forking under z6Mkt67…v4N1tRk.. ✓ Forking under z6Mkt67…v4N1tRk..
✓ Creating checkout in ./heartwood.. ✓ Creating checkout in ./heartwood..
✓ Remote z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi created ✓ Remote z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi added
✓ Remote-tracking branch z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master created for z6MknSL…StBU8Vi ✓ Remote-tracking branch z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master created for z6MknSL…StBU8Vi
✓ Repository successfully cloned under [..]/heartwood/ ✓ 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 $ 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: 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) 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: When we're finished with the `bob` remote, we can remove it:
@ -23,9 +24,10 @@ $ rad remote rm bob
✓ Remote `bob` removed ✓ 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 $ 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 $ rad remote add z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --name bob
✓ Remote bob added with rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk ✓ Remote bob added
``` ```
``` (stderr) ``` (stderr)

View File

@ -1,9 +1,13 @@
#![allow(clippy::box_default)]
use std::collections::HashMap;
use std::ffi::OsString; use std::ffi::OsString;
use std::path::PathBuf; use std::path::PathBuf;
use anyhow::anyhow; use anyhow::anyhow;
use anyhow::Context as _; use anyhow::Context as _;
use radicle::git;
use radicle::node::AliasStore;
use radicle::prelude::*; use radicle::prelude::*;
use radicle::storage::git::transport; 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 remote tracking branches for project delegates.
setup_remotes( setup_remotes(
project::SetupRemote { project::SetupRemote {
project: id, rid: id,
default_branch: payload.default_branch().clone(), tracking: Some(payload.default_branch().clone()),
repo: &repo, repo: &repo,
fetch: true, fetch: true,
tracking: true,
}, },
&remotes, &remotes,
profile,
)?; )?;
Ok(path) Ok(path)
} }
/// Setup a remote and tracking branch for each given remote. /// 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 { for remote_id in remotes {
if let Some((remote, branch)) = setup.run(*remote_id)? { if let Err(e) = setup_remote(&setup, remote_id, None, &aliases) {
let remote = remote.name().unwrap(); // Only valid UTF-8 is used. term::warning(format!("Failed to setup remote for {remote_id}: {e}").as_str());
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))
);
} }
} }
Ok(()) 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)?; radicle::git::configure_repository(&working)?;
checkout::setup_remotes( checkout::setup_remotes(
project::SetupRemote { project::SetupRemote {
project: options.id, rid: options.id,
default_branch, tracking: Some(default_branch),
repo: &working, repo: &working,
fetch: true, fetch: true,
tracking: true,
}, },
&delegates, &delegates,
&profile,
)?; )?;
term::success!( term::success!(

View File

@ -10,9 +10,10 @@ use std::ffi::OsString;
use anyhow::anyhow; use anyhow::anyhow;
use radicle::git::RefString;
use radicle::prelude::NodeId; use radicle::prelude::NodeId;
use crate::terminal::args::{self, string, Error}; use crate::terminal::args;
use crate::terminal::{Args, Context, Help}; use crate::terminal::{Args, Context, Help};
pub const HELP: Help = Help { pub const HELP: Help = Help {
@ -44,8 +45,8 @@ pub enum OperationName {
#[derive(Debug)] #[derive(Debug)]
pub enum Operation { pub enum Operation {
Add { id: NodeId, name: Option<String> }, Add { id: NodeId, name: Option<RefString> },
Rm { name: String }, Rm { name: RefString },
List, List,
} }
@ -61,16 +62,17 @@ impl Args for Options {
let mut parser = lexopt::Parser::from_args(args); let mut parser = lexopt::Parser::from_args(args);
let mut op: Option<OperationName> = None; let mut op: Option<OperationName> = None;
let mut id: Option<NodeId> = 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()? { while let Some(arg) = parser.next()? {
match arg { match arg {
Long("help") => { Long("help") => {
return Err(Error::Help.into()); return Err(args::Error::Help.into());
} }
Long("name") | Short('n') => { Long("name") | Short('n') => {
let value = parser.value()?; let value = parser.value()?;
let value = string(&value); let value = args::refstring("name", value)?;
name = Some(value); name = Some(value);
} }
Value(val) if op.is_none() => match val.to_string_lossy().as_ref() { Value(val) if op.is_none() => match val.to_string_lossy().as_ref() {
@ -84,7 +86,10 @@ impl Args for Options {
id = Some(nid); id = Some(nid);
} }
Value(val) if op == Some(OperationName::Rm) && name.is_none() => { 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); name = Some(val);
} }
_ => return Err(anyhow::anyhow!(arg.unexpected())), _ => return Err(anyhow::anyhow!(arg.unexpected())),
@ -114,7 +119,8 @@ pub fn run(options: Options, ctx: impl Context) -> anyhow::Result<()> {
let profile = ctx.profile()?; let profile = ctx.profile()?;
match options.op { 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::Rm { ref name } => self::rm::run(name, &working)?,
Operation::List => self::list::run(&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 radicle_crypto::PublicKey;
use crate::git::add_remote; use crate::commands::rad_checkout as checkout;
use crate::{git, terminal as term}; use crate::git;
use crate::project::SetupRemote;
pub fn run( pub fn run(
id: Id, rid: Id,
pubkey: &PublicKey, nid: &PublicKey,
name: Option<String>, name: Option<RefString>,
tracking: Option<BranchName>,
profile: &Profile, profile: &Profile,
repository: &git::Repository, repo: &git::Repository,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let name = match name { let aliases = profile.aliases()?;
Some(name) => name, let setup = SetupRemote {
_ => profile rid,
.tracking()? tracking,
.node_policy(pubkey)? fetch: false,
.and_then(|node| node.alias) repo,
.ok_or(anyhow::anyhow!("a `name` needs to be specified"))?,
}; };
if git::is_remote(repository, &name)? { checkout::setup_remote(&setup, nid, name, &aliases)
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(())
} }

View File

@ -4,6 +4,7 @@ use std::fmt::Display;
use std::fs::{File, OpenOptions}; use std::fs::{File, OpenOptions};
use std::io; use std::io;
use std::io::Write; use std::io::Write;
use std::ops::{Deref, DerefMut};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::Command; use std::process::Command;
use std::str::FromStr; use std::str::FromStr;
@ -74,11 +75,10 @@ pub enum RemoteError {
#[derive(Clone)] #[derive(Clone)]
pub struct Remote<'a> { pub struct Remote<'a> {
pub(crate) name: String, pub name: String,
pub(crate) url: radicle::git::Url, pub url: radicle::git::Url,
pub(crate) pushurl: Option<radicle::git::Url>, pub pushurl: Option<radicle::git::Url>,
#[allow(dead_code)]
inner: git2::Remote<'a>, 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. /// Get the git repository in the current directory.
pub fn repository() -> Result<Repository, anyhow::Error> { pub fn repository() -> Result<Repository, anyhow::Error> {
match Repository::open(".") { 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. /// Setup an upstream tracking branch for the given remote and branch.
/// Creates the tracking branch if it does not exist. /// 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 radicle::prelude::*;
use crate::git; use crate::git;
use radicle::git::RefStr;
/// Setup a project remote and tracking branch. /// Setup a project remote and tracking branch.
pub struct SetupRemote<'a> { pub struct SetupRemote<'a> {
/// The project id. /// The project id.
pub project: Id, pub rid: Id,
/// The project default branch. /// Whether or not to setup a remote tracking branch.
pub default_branch: BranchName, pub tracking: Option<BranchName>,
/// The repository in which to setup the remote.
pub repo: &'a git::Repository,
/// Whether or not to fetch the remote immediately. /// Whether or not to fetch the remote immediately.
pub fetch: bool, pub fetch: bool,
/// Whether or not to setup a remote tracking branch. /// The repository in which to setup the remote.
pub tracking: bool, pub repo: &'a git::Repository,
} }
impl<'a> SetupRemote<'a> { impl<'a> SetupRemote<'a> {
/// Run the setup for the given peer. /// Run the setup for the given peer.
pub fn run(&self, node: NodeId) -> anyhow::Result<Option<(Remote, RefString)>> { pub fn run(
let url = radicle::git::Url::from(self.project).with_namespace(node); &self,
let mut remote = radicle::git::configure_remote(self.repo, &node.to_string(), &url, &url)?; 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. // Fetch the refs into the working copy.
if self.fetch { if self.fetch {
remote.fetch::<&str>(&[], None, None)?; remote.fetch::<&str>(&[], None, None)?;
} }
// Setup remote-tracking branch. // Setup remote-tracking branch.
if self.tracking { if let Some(branch) = &self.tracking {
// SAFETY: Node IDs are valid ref strings. let tracking_branch = remote_name.join(branch);
let node_ref = RefString::try_from(node.to_string()).unwrap(); let local_branch = radicle::git::refs::workdir::branch(tracking_branch.as_refstr());
let node_ref = node_ref.as_refstr(); radicle::git::set_upstream(self.repo, remote_name, &tracking_branch, local_branch)?;
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)?;
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::cob::{self, issue, patch};
use radicle::crypto; use radicle::crypto;
use radicle::git::RefString;
use radicle::node::Address; use radicle::node::Address;
use radicle::prelude::{Did, Id, NodeId}; use radicle::prelude::{Did, Id, NodeId};
@ -77,6 +78,20 @@ pub fn finish(unparsed: Vec<OsString>) -> anyhow::Result<()> {
Ok(()) 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> { pub fn did(val: &OsString) -> anyhow::Result<Did> {
let val = val.to_string_lossy(); let val = val.to_string_lossy();
let Ok(peer) = Did::from_str(&val) else { 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("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] #[test]
fn rad_self() { fn rad_self() {
let mut environment = Environment::new(); let mut environment = Environment::new();

View File

@ -24,7 +24,7 @@ use radicle::node::TRACKING_DB_FILE;
use radicle::profile::Home; use radicle::profile::Home;
use radicle::profile::Profile; use radicle::profile::Profile;
use radicle::rad; use radicle::rad;
use radicle::storage::{ReadStorage as _, WriteRepository}; use radicle::storage::{ReadRepository, ReadStorage as _, WriteRepository};
use radicle::test::fixtures; use radicle::test::fixtures;
use radicle::Storage; 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. /// Run a `rad` CLI command.
pub fn rad<P: AsRef<Path>>(&self, cmd: &str, args: &[&str], cwd: P) -> io::Result<()> { pub fn rad<P: AsRef<Path>>(&self, cmd: &str, args: &[&str], cwd: P) -> io::Result<()> {
let cwd = cwd.as_ref(); let cwd = cwd.as_ref();