Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
cli: Implement `rad fetch` command
Alexis Sellier committed 3 years ago
commit 43bd5fca00fe5a711ebc56b23bb8bc9e855f1435
parent 5f59493c702ca57442659f8eae27384ec514698a
7 files changed +238 -30
modified radicle-cli/examples/rad-inspect.md
@@ -68,3 +68,6 @@ date Thu, 15 Dec 2022 17:28:04 +0000
 }

```
+

+
The identity document is the metadata associated with a repository, that is
+
only changeable by delegates.
modified radicle-cli/src/commands.rs
@@ -12,6 +12,8 @@ pub mod rad_comment;
pub mod rad_delegate;
#[path = "commands/edit.rs"]
pub mod rad_edit;
+
#[path = "commands/fetch.rs"]
+
pub mod rad_fetch;
#[path = "commands/help.rs"]
pub mod rad_help;
#[path = "commands/id.rs"]
modified radicle-cli/src/commands/clone.rs
@@ -10,13 +10,14 @@ use radicle::git::raw;
use radicle::identity::doc::{DocError, Id};
use radicle::identity::{doc, IdentityError};
use radicle::node;
-
use radicle::node::{FetchResult, Handle as _, Node};
+
use radicle::node::{Handle as _, Node};
use radicle::prelude::*;
use radicle::rad;
use radicle::storage;
use radicle::storage::git::Storage;

use crate::commands::rad_checkout as checkout;
+
use crate::commands::rad_fetch as fetch;
use crate::project;
use crate::terminal as term;
use crate::terminal::args::{Args, Error, Help};
@@ -29,7 +30,7 @@ pub const HELP: Help = Help {
    usage: r#"
Usage

-
    rad clone <id> [<option>...]
+
    rad clone <rid> [<option>...]

Options

@@ -152,8 +153,10 @@ pub enum CloneError {
    Payload(#[from] doc::PayloadError),
    #[error("project error: {0}")]
    Identity(#[from] IdentityError),
-
    #[error("no seeds found for {0}")]
+
    #[error("repository {0} not found")]
    NotFound(Id),
+
    #[error("no seeds found for {0}")]
+
    NoSeeds(Id),
}

pub fn clone<G: Signer>(
@@ -173,34 +176,15 @@ pub fn clone<G: Signer>(
        );
    }

-
    // Get seeds. This consults the local routing table only.
-
    let mut seeds = node.seeds(id)?;
-
    if seeds.has_connections() {
-
        // Fetch from all seeds.
-
        for seed in seeds.connected() {
-
            let spinner = term::spinner(format!(
-
                "Fetching {} from {}..",
-
                term::format::tertiary(id),
-
                term::format::tertiary(term::format::node(seed))
-
            ));
-

-
            // TODO: If none of them succeeds, output an error. Otherwise tell the caller
-
            // how many succeeded.
-
            match node.fetch(id, *seed)? {
-
                FetchResult::Success { .. } => {
-
                    spinner.finish();
-
                }
-
                FetchResult::Failed { reason } => {
-
                    spinner.error(reason);
-
                }
-
            }
-
        }
-
        // TODO: Warn if no seeds were found, and we might be checking out stale data.
-
    }
+
    let results = fetch::fetch(id, node)?;
    let Ok(repository) = storage.repository(id) else {
        // If we don't have the project locally, even after attempting to fetch,
        // there's nothing we can do.
-
        return Err(CloneError::NotFound(id));
+
        if results.is_empty() {
+
            return Err(CloneError::NoSeeds(id));
+
        } else {
+
            return Err(CloneError::NotFound(id));
+
        }
    };

    // Create a local fork of the project, under our own id, unless we have one already.
@@ -227,6 +211,14 @@ pub fn clone<G: Signer>(
    let proj = doc.project()?;
    let path = Path::new(proj.name());

+
    if results.success().next().is_none() {
+
        if results.failed().next().is_some() {
+
            term::warning("Fetching failed, local copy is potentially stale");
+
        } else {
+
            term::warning("No seeds found, local copy is potentially stale");
+
        }
+
    }
+

    // Checkout.
    let spinner = term::spinner(format!(
        "Creating checkout in ./{}..",
added radicle-cli/src/commands/fetch.rs
@@ -0,0 +1,143 @@
+
#![allow(clippy::or_fun_call)]
+
use std::ffi::OsString;
+
use std::path::Path;
+

+
use anyhow::{anyhow, Context};
+

+
use radicle::identity::doc::Id;
+
use radicle::node;
+
use radicle::node::{FetchResult, FetchResults, Handle as _, Node};
+
use radicle::prelude::*;
+

+
use crate::terminal as term;
+
use crate::terminal::args::{Args, Error, Help};
+

+
pub const HELP: Help = Help {
+
    name: "fetch",
+
    description: "Fetch repository refs from the network",
+
    version: env!("CARGO_PKG_VERSION"),
+
    usage: r#"
+
Usage
+

+
    rad fetch <rid> [<option>...]
+

+
    By default, this command will fetch from all connected seeds.
+
    To instead specify a seed, use the `--seed <nid>` option.
+

+
Options
+

+
    --seed <nid>    Fetch seed a specific connected peer
+
    --force, -f     Fetch even if the repository isn't tracked
+
    --help          Print help
+

+
"#,
+
};
+

+
#[derive(Debug)]
+
pub struct Options {
+
    rid: Option<Id>,
+
    seed: Option<NodeId>,
+
    #[allow(dead_code)]
+
    force: bool,
+
}
+

+
impl Args for Options {
+
    fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
+
        use lexopt::prelude::*;
+

+
        let mut parser = lexopt::Parser::from_args(args);
+
        let mut rid: Option<Id> = None;
+
        let mut seed: Option<NodeId> = None;
+
        let mut force = false;
+

+
        while let Some(arg) = parser.next()? {
+
            match arg {
+
                Long("force") | Short('f') => {
+
                    force = true;
+
                }
+
                Long("seed") => {
+
                    let val = parser.value()?;
+
                    let val = term::args::nid(&val)?;
+
                    seed = Some(val);
+
                }
+
                Long("help") => {
+
                    return Err(Error::Help.into());
+
                }
+
                Value(val) if rid.is_none() => {
+
                    let val = term::args::rid(&val)?;
+
                    rid = Some(val);
+
                }
+
                _ => return Err(anyhow!(arg.unexpected())),
+
            }
+
        }
+

+
        Ok((Options { rid, seed, force }, vec![]))
+
    }
+
}
+

+
pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
+
    let profile = ctx.profile()?;
+
    let mut node = radicle::Node::new(profile.socket());
+
    let rid = match options.rid {
+
        Some(rid) => rid,
+
        None => {
+
            let (_, rid) = radicle::rad::repo(Path::new("."))
+
                .context("Current directory is not a Radicle project")?;
+

+
            rid
+
        }
+
    };
+

+
    // TODO(cloudhead): Check that we're tracking the repo, and if not, and `--force` is not
+
    // used, abort with error.
+

+
    let results = if let Some(seed) = options.seed {
+
        let result = fetch_from(rid, &seed, &mut node)?;
+
        FetchResults::from(vec![(seed, result)])
+
    } else {
+
        fetch(rid, &mut node)?
+
    };
+
    let success = results.success().count();
+
    let failed = results.failed().count();
+

+
    if success == 0 {
+
        term::error(format!("Failed to fetch repository from {failed} seed(s)"));
+
    } else {
+
        term::success!("Fetched repository from {success} seed(s)");
+
    }
+
    Ok(())
+
}
+

+
pub fn fetch(rid: Id, node: &mut Node) -> Result<FetchResults, node::Error> {
+
    // Get seeds. This consults the local routing table only.
+
    let mut seeds = node.seeds(rid)?;
+
    let mut results = FetchResults::default();
+

+
    if seeds.has_connections() {
+
        // Fetch from all seeds.
+
        for seed in seeds.connected() {
+
            let result = fetch_from(rid, seed, node)?;
+
            results.push(*seed, result);
+
        }
+
    }
+
    Ok(results)
+
}
+

+
pub fn fetch_from(rid: Id, seed: &NodeId, node: &mut Node) -> Result<FetchResult, node::Error> {
+
    let spinner = term::spinner(format!(
+
        "Fetching {} from {}..",
+
        term::format::tertiary(rid),
+
        term::format::tertiary(term::format::node(seed))
+
    ));
+
    let result = node.fetch(rid, *seed)?;
+

+
    match &result {
+
        FetchResult::Success { .. } => {
+
            spinner.finish();
+
        }
+
        FetchResult::Failed { reason } => {
+
            spinner.error(reason);
+
        }
+
    }
+
    Ok(result)
+
}
modified radicle-cli/src/commands/help.rs
@@ -18,6 +18,7 @@ const COMMANDS: &[Help] = &[
    rad_checkout::HELP,
    rad_clone::HELP,
    rad_edit::HELP,
+
    rad_fetch::HELP,
    rad_help::HELP,
    rad_id::HELP,
    rad_init::HELP,
modified radicle-cli/src/terminal/args.rs
@@ -3,7 +3,7 @@ use std::str::FromStr;

use anyhow::anyhow;
use radicle::crypto;
-
use radicle::prelude::{Did, Id};
+
use radicle::prelude::{Did, Id, NodeId};

#[derive(thiserror::Error, Debug)]
pub enum Error {
@@ -86,7 +86,12 @@ pub fn did(val: &OsString) -> anyhow::Result<Did> {
    Ok(peer)
}

+
pub fn nid(val: &OsString) -> anyhow::Result<NodeId> {
+
    let val = val.to_string_lossy();
+
    NodeId::from_str(&val).map_err(|_| anyhow!("invalid Node ID '{}'", val))
+
}
+

pub fn rid(val: &OsString) -> anyhow::Result<Id> {
    let val = val.to_string_lossy();
-
    Id::from_str(&val).map_err(|_| anyhow!("invalid repository ID '{}'", val))
+
    Id::from_str(&val).map_err(|_| anyhow!("invalid Repository ID '{}'", val))
}
modified radicle/src/node.rs
@@ -2,6 +2,7 @@ mod features;

use std::collections::BTreeSet;
use std::io::{BufRead, BufReader};
+
use std::ops::Deref;
use std::os::unix::net::UnixStream;
use std::path::{Path, PathBuf};
use std::{fmt, io, net};
@@ -270,6 +271,67 @@ impl<S: ToString> From<Result<Vec<RefUpdate>, S>> for FetchResult {
    }
}

+
/// Holds multiple fetch results.
+
#[derive(Debug, Default)]
+
pub struct FetchResults(Vec<(NodeId, FetchResult)>);
+

+
impl FetchResults {
+
    /// Push a fetch result.
+
    pub fn push(&mut self, nid: NodeId, result: FetchResult) {
+
        self.0.push((nid, result));
+
    }
+

+
    /// Iterate over all fetch results.
+
    pub fn iter(&self) -> impl Iterator<Item = (&NodeId, &FetchResult)> {
+
        self.0.iter().map(|(nid, r)| (nid, r))
+
    }
+

+
    /// Iterate over successful fetches.
+
    pub fn success(&self) -> impl Iterator<Item = (&NodeId, &[RefUpdate])> {
+
        self.0.iter().filter_map(|(nid, r)| {
+
            if let FetchResult::Success { updated } = r {
+
                Some((nid, updated.as_slice()))
+
            } else {
+
                None
+
            }
+
        })
+
    }
+

+
    /// Iterate over failed fetches.
+
    pub fn failed(&self) -> impl Iterator<Item = (&NodeId, &str)> {
+
        self.0.iter().filter_map(|(nid, r)| {
+
            if let FetchResult::Failed { reason } = r {
+
                Some((nid, reason.as_str()))
+
            } else {
+
                None
+
            }
+
        })
+
    }
+
}
+

+
impl From<Vec<(NodeId, FetchResult)>> for FetchResults {
+
    fn from(value: Vec<(NodeId, FetchResult)>) -> Self {
+
        Self(value)
+
    }
+
}
+

+
impl Deref for FetchResults {
+
    type Target = [(NodeId, FetchResult)];
+

+
    fn deref(&self) -> &Self::Target {
+
        self.0.as_slice()
+
    }
+
}
+

+
impl IntoIterator for FetchResults {
+
    type Item = (NodeId, FetchResult);
+
    type IntoIter = std::vec::IntoIter<(NodeId, FetchResult)>;
+

+
    fn into_iter(self) -> Self::IntoIter {
+
        self.0.into_iter()
+
    }
+
}
+

/// Error returned by [`Handle`] functions.
#[derive(thiserror::Error, Debug)]
pub enum Error {