cli: Implement `rad fetch` command
This commit is contained in:
parent
5f59493c70
commit
43bd5fca00
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ pub mod rad_comment;
|
||||||
pub mod rad_delegate;
|
pub mod rad_delegate;
|
||||||
#[path = "commands/edit.rs"]
|
#[path = "commands/edit.rs"]
|
||||||
pub mod rad_edit;
|
pub mod rad_edit;
|
||||||
|
#[path = "commands/fetch.rs"]
|
||||||
|
pub mod rad_fetch;
|
||||||
#[path = "commands/help.rs"]
|
#[path = "commands/help.rs"]
|
||||||
pub mod rad_help;
|
pub mod rad_help;
|
||||||
#[path = "commands/id.rs"]
|
#[path = "commands/id.rs"]
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,14 @@ use radicle::git::raw;
|
||||||
use radicle::identity::doc::{DocError, Id};
|
use radicle::identity::doc::{DocError, Id};
|
||||||
use radicle::identity::{doc, IdentityError};
|
use radicle::identity::{doc, IdentityError};
|
||||||
use radicle::node;
|
use radicle::node;
|
||||||
use radicle::node::{FetchResult, Handle as _, Node};
|
use radicle::node::{Handle as _, Node};
|
||||||
use radicle::prelude::*;
|
use radicle::prelude::*;
|
||||||
use radicle::rad;
|
use radicle::rad;
|
||||||
use radicle::storage;
|
use radicle::storage;
|
||||||
use radicle::storage::git::Storage;
|
use radicle::storage::git::Storage;
|
||||||
|
|
||||||
use crate::commands::rad_checkout as checkout;
|
use crate::commands::rad_checkout as checkout;
|
||||||
|
use crate::commands::rad_fetch as fetch;
|
||||||
use crate::project;
|
use crate::project;
|
||||||
use crate::terminal as term;
|
use crate::terminal as term;
|
||||||
use crate::terminal::args::{Args, Error, Help};
|
use crate::terminal::args::{Args, Error, Help};
|
||||||
|
|
@ -29,7 +30,7 @@ pub const HELP: Help = Help {
|
||||||
usage: r#"
|
usage: r#"
|
||||||
Usage
|
Usage
|
||||||
|
|
||||||
rad clone <id> [<option>...]
|
rad clone <rid> [<option>...]
|
||||||
|
|
||||||
Options
|
Options
|
||||||
|
|
||||||
|
|
@ -152,8 +153,10 @@ pub enum CloneError {
|
||||||
Payload(#[from] doc::PayloadError),
|
Payload(#[from] doc::PayloadError),
|
||||||
#[error("project error: {0}")]
|
#[error("project error: {0}")]
|
||||||
Identity(#[from] IdentityError),
|
Identity(#[from] IdentityError),
|
||||||
#[error("no seeds found for {0}")]
|
#[error("repository {0} not found")]
|
||||||
NotFound(Id),
|
NotFound(Id),
|
||||||
|
#[error("no seeds found for {0}")]
|
||||||
|
NoSeeds(Id),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clone<G: Signer>(
|
pub fn clone<G: Signer>(
|
||||||
|
|
@ -173,34 +176,15 @@ pub fn clone<G: Signer>(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get seeds. This consults the local routing table only.
|
let results = fetch::fetch(id, node)?;
|
||||||
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 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.
|
||||||
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.
|
// 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 proj = doc.project()?;
|
||||||
let path = Path::new(proj.name());
|
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.
|
// Checkout.
|
||||||
let spinner = term::spinner(format!(
|
let spinner = term::spinner(format!(
|
||||||
"Creating checkout in ./{}..",
|
"Creating checkout in ./{}..",
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -18,6 +18,7 @@ const COMMANDS: &[Help] = &[
|
||||||
rad_checkout::HELP,
|
rad_checkout::HELP,
|
||||||
rad_clone::HELP,
|
rad_clone::HELP,
|
||||||
rad_edit::HELP,
|
rad_edit::HELP,
|
||||||
|
rad_fetch::HELP,
|
||||||
rad_help::HELP,
|
rad_help::HELP,
|
||||||
rad_id::HELP,
|
rad_id::HELP,
|
||||||
rad_init::HELP,
|
rad_init::HELP,
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ use std::str::FromStr;
|
||||||
|
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use radicle::crypto;
|
use radicle::crypto;
|
||||||
use radicle::prelude::{Did, Id};
|
use radicle::prelude::{Did, Id, NodeId};
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
|
|
@ -86,7 +86,12 @@ pub fn did(val: &OsString) -> anyhow::Result<Did> {
|
||||||
Ok(peer)
|
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> {
|
pub fn rid(val: &OsString) -> anyhow::Result<Id> {
|
||||||
let val = val.to_string_lossy();
|
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))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ mod features;
|
||||||
|
|
||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
use std::io::{BufRead, BufReader};
|
use std::io::{BufRead, BufReader};
|
||||||
|
use std::ops::Deref;
|
||||||
use std::os::unix::net::UnixStream;
|
use std::os::unix::net::UnixStream;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::{fmt, io, net};
|
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.
|
/// Error returned by [`Handle`] functions.
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue