cli: Support `--seed` on `rad clone`

Allows specifying one or more seeds to clone from. This is useful for
cloning private repositories which are not in the routing table.
This commit is contained in:
cloudhead 2024-01-03 14:07:19 +01:00
parent 150130e99b
commit 04bfbca15d
No known key found for this signature in database
4 changed files with 129 additions and 13 deletions

View File

@ -0,0 +1,45 @@
Given a private repo `rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu` belonging to Alice,
Alice allows Bob to fetch it, and Bob, without the updated identity document
is able to fetch it by specifiying Alice as a seed.
``` ~alice
$ rad id update --title "Allow Bob" --description "" --allow did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk -q
...
$ rad inspect --identity
{
"payload": {
"xyz.radicle.project": {
"defaultBranch": "master",
"description": "radicle heartwood protocol & stack",
"name": "heartwood"
}
},
"delegates": [
"did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
],
"threshold": 1,
"visibility": {
"type": "private",
"allow": [
"did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk"
]
}
}
```
``` ~bob
$ rad ls --all --private
$ rad clone rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu --seed z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi --timeout 1
✓ Seeding policy updated for rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu with scope 'all'
✓ Fetching rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu from z6MknSL…StBU8Vi..
✓ Creating checkout in ./heartwood..
✓ Remote alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi added
✓ Remote-tracking branch alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master created for z6MknSL…StBU8Vi
✓ Repository successfully cloned under [...]/.radicle/heartwood/
╭────────────────────────────────────╮
│ heartwood │
│ radicle heartwood protocol & stack │
│ 0 issues · 0 patches │
╰────────────────────────────────────╯
Run `cd ./heartwood` to go to the project directory.
```

View File

@ -2,14 +2,13 @@ Given a private repo `rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu` belonging to Alice,
Bob tries to fetch it, and even though he's connected to Alice, it fails. Bob tries to fetch it, and even though he's connected to Alice, it fails.
``` ~bob ``` ~bob
$ rad seed rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu --scope followed
✓ Seeding policy updated for rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu with scope 'followed'
$ rad ls $ rad ls
``` ```
``` ~bob (fail) ``` ~bob (fail)
$ rad sync rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu --fetch --seed z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi --timeout 1 $ rad clone rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu --seed z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi --timeout 1
✓ Seeding policy updated for rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu with scope 'all'
✗ Fetching rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu from z6MknSL…StBU8Vi.. error: failed to perform fetch handshake ✗ Fetching rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu from z6MknSL…StBU8Vi.. error: failed to perform fetch handshake
✗ Error: repository fetch from 1 seed(s) failed ✗ Error: repository rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu not found
``` ```
She allows Bob to view the repository. And when she syncs, one node (Bob) gets She allows Bob to view the repository. And when she syncs, one node (Bob) gets

View File

@ -36,10 +36,18 @@ Usage
rad clone <rid> [<directory>] [--scope <scope>] [<option>...] rad clone <rid> [<directory>] [--scope <scope>] [<option>...]
The `clone` command will use your local node's routing table to find seeds from
which it can clone the repository.
For private repositories, the `--seed` option can be passed to clone directly
from a known seed in the privacy set.
Options Options
--scope <scope> Follow scope (default: all) --scope <scope> Follow scope (default: all)
--help Print help -s, --seed <nid> Clone from this seed (may be specified multiple times)
--timeout <secs> Timeout for fetching repository (default: 9)
--help Print help
"#, "#,
}; };
@ -52,6 +60,10 @@ pub struct Options {
directory: Option<PathBuf>, directory: Option<PathBuf>,
/// The seeding scope of the repository. /// The seeding scope of the repository.
scope: Scope, scope: Scope,
/// Sync mode.
mode: sync::RepoSync,
/// Fetch timeout.
timeout: time::Duration,
} }
impl Args for Options { impl Args for Options {
@ -61,15 +73,33 @@ impl Args for Options {
let mut parser = lexopt::Parser::from_args(args); let mut parser = lexopt::Parser::from_args(args);
let mut id: Option<Id> = None; let mut id: Option<Id> = None;
let mut scope = Scope::All; let mut scope = Scope::All;
let mut mode = sync::RepoSync::default();
let mut timeout = time::Duration::from_secs(9);
let mut directory = None; let mut directory = None;
while let Some(arg) = parser.next()? { while let Some(arg) = parser.next()? {
match arg { match arg {
Long("seed") | Short('s') => {
let value = parser.value()?;
let value = term::args::nid(&value)?;
if let sync::RepoSync::Seeds(seeds) = &mut mode {
seeds.push(value);
} else {
mode = sync::RepoSync::Seeds(vec![value]);
}
}
Long("scope") => { Long("scope") => {
let value = parser.value()?; let value = parser.value()?;
scope = term::args::parse_value("scope", value)?; scope = term::args::parse_value("scope", value)?;
} }
Long("timeout") => {
let value = parser.value()?;
let secs = term::args::number(&value)?;
timeout = time::Duration::from_secs(secs as u64);
}
Long("no-confirm") => { Long("no-confirm") => {
// We keep this flag here for consistency though it doesn't have any effect, // We keep this flag here for consistency though it doesn't have any effect,
// since the command is fully non-interactive. // since the command is fully non-interactive.
@ -99,6 +129,8 @@ impl Args for Options {
id, id,
directory, directory,
scope, scope,
mode,
timeout,
}, },
vec![], vec![],
)) ))
@ -120,6 +152,8 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
options.id, options.id,
options.directory.clone(), options.directory.clone(),
options.scope, options.scope,
options.mode,
options.timeout,
&mut node, &mut node,
&signer, &signer,
&profile.storage, &profile.storage,
@ -204,6 +238,8 @@ pub fn clone<G: Signer>(
id: Id, id: Id,
directory: Option<PathBuf>, directory: Option<PathBuf>,
scope: Scope, scope: Scope,
mode: sync::RepoSync,
timeout: time::Duration,
node: &mut Node, node: &mut Node,
signer: &G, signer: &G,
storage: &Storage, storage: &Storage,
@ -218,7 +254,7 @@ pub fn clone<G: Signer>(
> { > {
let me = *signer.public_key(); let me = *signer.public_key();
// Track. // Seed repository.
if node.seed(id, scope)? { if node.seed(id, scope)? {
term::success!( term::success!(
"Seeding policy updated for {} with scope '{scope}'", "Seeding policy updated for {} with scope '{scope}'",
@ -226,12 +262,7 @@ pub fn clone<G: Signer>(
); );
} }
let results = sync::fetch( let results = sync::fetch(id, mode, timeout, node)?;
id,
sync::RepoSync::default(),
time::Duration::from_secs(9),
node,
)?;
let Ok(repository) = storage.repository(id) else { let Ok(repository) = storage.repository(id) else {
// If we don't have the project locally, even after attempting to fetch, // If we don't have the project locally, even after attempting to fetch,
// there's nothing we can do. // there's nothing we can do.

View File

@ -1683,6 +1683,47 @@ fn rad_init_private_clone() {
.unwrap(); .unwrap();
} }
#[test]
fn rad_init_private_clone_seed() {
let mut environment = Environment::new();
let alice = environment.node(Config::test(Alias::new("alice")));
let bob = environment.node(Config::test(Alias::new("bob")));
let working = environment.tmp().join("working");
fixtures::repository(working.join("alice"));
let alice = alice.spawn();
let mut bob = bob.spawn();
test(
"examples/rad-init-private.md",
working.join("alice"),
Some(&alice.home),
[],
)
.unwrap();
bob.connect(&alice).converge([&alice]);
formula(
&environment.tmp(),
"examples/rad-init-private-clone-seed.md",
)
.unwrap()
.home(
"alice",
working.join("alice"),
[("RAD_HOME", alice.home.path().display())],
)
.home(
"bob",
bob.home.path(),
[("RAD_HOME", bob.home.path().display())],
)
.run()
.unwrap();
}
#[test] #[test]
fn rad_publish() { fn rad_publish() {
let mut environment = Environment::new(); let mut environment = Environment::new();