cli: From "tracking" to "seeding" and "following"

Split the commands and change the naming.

* Tracking a repo is now called "seeding"
* Tracking a peer is now called "following"

In this commit, we don't change most of the internal code, only the
user-facing part.
This commit is contained in:
cloudhead 2023-12-01 12:54:42 +01:00
parent ee9ee69130
commit 4605348d68
No known key found for this signature in database
17 changed files with 438 additions and 400 deletions

View File

@ -6,15 +6,15 @@ necessary.
Instead, we want to fetch the project from the network into our local Instead, we want to fetch the project from the network into our local
storage. In this scenario, we know that the project is storage. In this scenario, we know that the project is
`rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji`. In order to fetch it, we first `rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji`. In order to fetch it, we first
have to track the project. have to update our seeding policy for the project.
``` ```
$ rad track rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --no-fetch $ rad seed rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --no-fetch
Tracking policy updated for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji with scope 'trusted' Seeding policy updated for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji with scope 'all'
``` ```
Now that the project is tracked we can fetch it and we will have it in Now that the project is seeding we can fetch it and we will have it in
our local storage. Note that the `track` command can also be told to fetch our local storage. Note that the `seed` command can also be told to fetch
by passing the `--fetch` option. by passing the `--fetch` option.
``` ```

View File

@ -2,8 +2,8 @@ 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 track rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu $ rad seed rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu --scope trusted
Tracking policy updated for rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu with scope 'trusted' Seeding policy updated for rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu with scope 'trusted'
$ rad ls $ rad ls
``` ```
``` ~bob (fail) ``` ~bob (fail)

View File

@ -18,22 +18,22 @@ $ rad node status
✓ Node is running. ✓ Node is running.
``` ```
The node also allows us to query data that it has access too such as The node also allows us to query data that it has access to such as
the tracking relationships and the routing table. Before we explore the follow policies and the routing table. Before we explore
those commands we'll first track a peer so that we have something to those commands we'll first follow a peer so that we have something to
see. see.
``` ```
$ rad track did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --alias Bob $ rad follow did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --alias Bob
Tracking policy updated for z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk (Bob) Follow policy updated for z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk (Bob)
``` ```
Now, when we use the `rad node tracking` command we will see Now, when we use the `rad node seeding` command we will see
information for repositories that we track -- in this case a information for repositories that we seed -- in this case a
repository that was already created: repository that was already created:
``` ```
$ rad node tracking $ rad node seeding
╭──────────────────────────────────────────────────────╮ ╭──────────────────────────────────────────────────────╮
│ RID Scope Policy │ │ RID Scope Policy │
├──────────────────────────────────────────────────────┤ ├──────────────────────────────────────────────────────┤
@ -41,12 +41,10 @@ $ rad node tracking
╰──────────────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────╯
``` ```
This is the same as using the `--repos` flag, but if we wish to see If we wish to see which nodes we are following:
which nodes we are specifically tracking, then we use the `--nodes`
flag:
``` ```
$ rad node tracking --nodes $ rad node following
╭───────────────────────────────────────────────────────────────────────────╮ ╭───────────────────────────────────────────────────────────────────────────╮
│ DID Alias Policy │ │ DID Alias Policy │
├───────────────────────────────────────────────────────────────────────────┤ ├───────────────────────────────────────────────────────────────────────────┤

View File

@ -10,9 +10,9 @@ $ rad sync --fetch rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 --seed z6MksmpU5b1dS7oaqF2b
✗ Error: to sync a repository, your node must be running. To start it, run `rad node start` ✗ Error: to sync a repository, your node must be running. To start it, run `rad node start`
``` ```
Note that tracking works fine without a running node: Note that seeding works fine without a running node:
``` ~alice ``` ~alice
$ rad track rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 $ rad seed rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Tracking policy updated for rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 with scope 'trusted' Seeding policy updated for rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 with scope 'all'
``` ```

View File

@ -2,13 +2,13 @@ To configure our node's tracking policy, we can use the `rad track` command.
For example, let's track a remote node we know about, and alias it to "eve": For example, let's track a remote node we know about, and alias it to "eve":
``` ```
$ rad track did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --alias eve $ rad follow did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --alias eve
Tracking policy updated for z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk (eve) Follow policy updated for z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk (eve)
``` ```
Now let's track one of Eve's repositories: Now let's track one of Eve's repositories:
``` ```
$ rad track rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --scope trusted --no-fetch $ rad seed rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --scope trusted --no-fetch
Tracking policy updated for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji with scope 'trusted' Seeding policy updated for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji with scope 'trusted'
``` ```

View File

@ -1,11 +1,11 @@
Back to being the project maintainer. Back to being the project maintainer.
Changes have been proposed by another person (or peer) via a radicle patch. To Changes have been proposed by another person (or peer) via a radicle patch. To
follow changes by another, we must 'track' them. follow changes by another, we must 'follow' them.
``` ```
$ rad track did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --alias bob $ rad follow did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --alias bob
Tracking policy updated for z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk (bob) Follow policy updated for z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk (bob)
``` ```
Additionally, we need to add a new 'git remote' to our working copy for the Additionally, we need to add a new 'git remote' to our working copy for the

View File

@ -12,6 +12,8 @@ pub mod rad_cob;
pub mod rad_config; pub mod rad_config;
#[path = "commands/diff.rs"] #[path = "commands/diff.rs"]
pub mod rad_diff; pub mod rad_diff;
#[path = "commands/follow.rs"]
pub mod rad_follow;
#[path = "commands/fork.rs"] #[path = "commands/fork.rs"]
pub mod rad_fork; pub mod rad_fork;
#[path = "commands/help.rs"] #[path = "commands/help.rs"]
@ -36,11 +38,11 @@ pub mod rad_path;
pub mod rad_publish; pub mod rad_publish;
#[path = "commands/remote.rs"] #[path = "commands/remote.rs"]
pub mod rad_remote; pub mod rad_remote;
#[path = "commands/seed.rs"]
pub mod rad_seed;
#[path = "commands/self.rs"] #[path = "commands/self.rs"]
pub mod rad_self; pub mod rad_self;
#[path = "commands/sync.rs"] #[path = "commands/sync.rs"]
pub mod rad_sync; pub mod rad_sync;
#[path = "commands/track.rs"] #[path = "commands/unfollow.rs"]
pub mod rad_track; pub mod rad_unfollow;
#[path = "commands/untrack.rs"]
pub mod rad_untrack;

View File

@ -0,0 +1,124 @@
use std::ffi::OsString;
use anyhow::anyhow;
use radicle::node::tracking::Alias;
use radicle::node::{Handle, NodeId};
use radicle::{prelude::*, Node};
use crate::terminal as term;
use crate::terminal::args::{Args, Error, Help};
pub const HELP: Help = Help {
name: "follow",
description: "Manage node follow policies",
version: env!("CARGO_PKG_VERSION"),
usage: r#"
Usage
rad follow <nid> [--alias <name>] [<option>...]
The `follow` command takes a Node ID, optionally in DID format, and updates the follow
policy for that peer.
Options
--alias <name> Associate an alias to a followed peer
--verbose, -v Verbose output
--help Print help
"#,
};
#[derive(Debug)]
pub struct Options {
pub nid: NodeId,
pub alias: Option<Alias>,
pub verbose: 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 verbose = false;
let mut nid: Option<NodeId> = None;
let mut alias: Option<Alias> = None;
while let Some(arg) = parser.next()? {
match &arg {
Value(val) if nid.is_none() => {
if let Ok(did) = term::args::did(val) {
nid = Some(did.into());
} else if let Ok(val) = term::args::nid(val) {
nid = Some(val);
} else {
anyhow::bail!("invalid Node ID `{}` specified", val.to_string_lossy());
}
}
Long("alias") if alias.is_none() => {
let name = parser.value()?;
let name = term::args::alias(&name)?;
alias = Some(name.to_owned());
}
Long("verbose") | Short('v') => verbose = true,
Long("help") | Short('h') => {
return Err(Error::Help.into());
}
_ => {
return Err(anyhow!(arg.unexpected()));
}
}
}
Ok((
Options {
nid: nid.ok_or_else(|| anyhow!("a Node ID must be specified"))?,
alias,
verbose,
},
vec![],
))
}
}
pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
let profile = ctx.profile()?;
let mut node = radicle::Node::new(profile.socket());
follow(options.nid, options.alias, &mut node, &profile)?;
Ok(())
}
pub fn follow(
nid: NodeId,
alias: Option<Alias>,
node: &mut Node,
profile: &Profile,
) -> Result<(), anyhow::Error> {
let followed = match node.track_node(nid, alias.clone()) {
Ok(updated) => updated,
Err(e) if e.is_connection_err() => {
let mut config = profile.tracking_mut()?;
config.track_node(&nid, alias.as_deref())?
}
Err(e) => return Err(e.into()),
};
let outcome = if followed { "updated" } else { "exists" };
if let Some(alias) = alias {
term::success!(
"Follow policy {outcome} for {} ({alias})",
term::format::tertiary(nid),
);
} else {
term::success!(
"Follow policy {outcome} for {}",
term::format::tertiary(nid),
);
}
Ok(())
}

View File

@ -28,8 +28,9 @@ const COMMANDS: &[Help] = &[
rad_path::HELP, rad_path::HELP,
rad_clean::HELP, rad_clean::HELP,
rad_self::HELP, rad_self::HELP,
rad_track::HELP, rad_seed::HELP,
rad_untrack::HELP, rad_follow::HELP,
rad_unfollow::HELP,
rad_remote::HELP, rad_remote::HELP,
rad_sync::HELP, rad_sync::HELP,
]; ];

View File

@ -14,10 +14,10 @@ use crate::terminal::Element as _;
mod control; mod control;
#[path = "node/events.rs"] #[path = "node/events.rs"]
mod events; mod events;
#[path = "node/policies.rs"]
mod policies;
#[path = "node/routing.rs"] #[path = "node/routing.rs"]
mod routing; mod routing;
#[path = "node/tracking.rs"]
mod tracking;
pub const HELP: Help = Help { pub const HELP: Help = Help {
name: "node", name: "node",
@ -32,7 +32,8 @@ Usage
rad node logs [-n <lines>] rad node logs [-n <lines>]
rad node connect <nid>@<addr> [<option>...] rad node connect <nid>@<addr> [<option>...]
rad node routing [--rid <rid>] [--nid <nid>] [--json] [<option>...] rad node routing [--rid <rid>] [--nid <nid>] [--json] [<option>...]
rad node tracking [--repos | --nodes] [<option>...] rad node following [<option>...]
rad node seeding [<option>...]
rad node events [--timeout <secs>] [-n <count>] [<option>...] rad node events [--timeout <secs>] [-n <count>] [<option>...]
rad node config rad node config
@ -49,11 +50,6 @@ Routing options
--nid <nid> Show the routing table entries for the given NID --nid <nid> Show the routing table entries for the given NID
--json Output the routing table as json --json Output the routing table as json
Tracking options
--repos Show the tracked repositories table
--nodes Show the tracked nodes table
Events options Events options
--timeout <secs> How long to wait to receive an event before giving up --timeout <secs> How long to wait to receive an event before giving up
@ -95,16 +91,8 @@ pub enum Operation {
Status, Status,
Sessions, Sessions,
Stop, Stop,
Tracking { Following,
mode: TrackingMode, Seeding,
},
}
#[derive(Default)]
pub enum TrackingMode {
#[default]
Repos,
Nodes,
} }
#[derive(Default, PartialEq, Eq)] #[derive(Default, PartialEq, Eq)]
@ -119,7 +107,8 @@ pub enum OperationName {
Status, Status,
Sessions, Sessions,
Stop, Stop,
Tracking, Following,
Seeding,
} }
impl Args for Options { impl Args for Options {
@ -130,7 +119,6 @@ impl Args for Options {
let mut options = vec![]; let mut options = vec![];
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 tracking_mode = TrackingMode::default();
let mut nid: Option<NodeId> = None; let mut nid: Option<NodeId> = None;
let mut rid: Option<Id> = None; let mut rid: Option<Id> = None;
let mut json: bool = false; let mut json: bool = false;
@ -154,7 +142,8 @@ impl Args for Options {
"start" => op = Some(OperationName::Start), "start" => op = Some(OperationName::Start),
"status" => op = Some(OperationName::Status), "status" => op = Some(OperationName::Status),
"stop" => op = Some(OperationName::Stop), "stop" => op = Some(OperationName::Stop),
"tracking" => op = Some(OperationName::Tracking), "seeding" => op = Some(OperationName::Seeding),
"following" => op = Some(OperationName::Following),
"sessions" => op = Some(OperationName::Sessions), "sessions" => op = Some(OperationName::Sessions),
unknown => anyhow::bail!("unknown operation '{}'", unknown), unknown => anyhow::bail!("unknown operation '{}'", unknown),
@ -181,12 +170,6 @@ impl Args for Options {
let val = parser.value()?; let val = parser.value()?;
count = term::args::number(&val)?; count = term::args::number(&val)?;
} }
Long("repos") if matches!(op, Some(OperationName::Tracking)) => {
tracking_mode = TrackingMode::Repos
}
Long("nodes") if matches!(op, Some(OperationName::Tracking)) => {
tracking_mode = TrackingMode::Nodes;
}
Long("foreground") if matches!(op, Some(OperationName::Start)) => { Long("foreground") if matches!(op, Some(OperationName::Start)) => {
foreground = true; foreground = true;
} }
@ -222,9 +205,8 @@ impl Args for Options {
OperationName::Status => Operation::Status, OperationName::Status => Operation::Status,
OperationName::Sessions => Operation::Sessions, OperationName::Sessions => Operation::Sessions,
OperationName::Stop => Operation::Stop, OperationName::Stop => Operation::Stop,
OperationName::Tracking => Operation::Tracking { OperationName::Seeding => Operation::Seeding,
mode: tracking_mode, OperationName::Following => Operation::Following,
},
}; };
Ok((Options { op }, vec![])) Ok((Options { op }, vec![]))
} }
@ -266,7 +248,8 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
Operation::Stop => { Operation::Stop => {
control::stop(node)?; control::stop(node)?;
} }
Operation::Tracking { mode } => tracking::run(&profile, mode)?, Operation::Seeding => policies::seeding(&profile)?,
Operation::Following => policies::following(&profile)?,
} }
Ok(()) Ok(())

View File

@ -1,24 +1,13 @@
use radicle::crypto::PublicKey; use radicle::crypto::PublicKey;
use radicle::node::{tracking, AliasStore, TRACKING_DB_FILE}; use radicle::node::{tracking, AliasStore};
use radicle::prelude::Did; use radicle::prelude::Did;
use radicle::Profile; use radicle::Profile;
use crate::terminal as term; use crate::terminal as term;
use term::Element; use term::Element;
use super::TrackingMode; pub fn seeding(profile: &Profile) -> anyhow::Result<()> {
let store = profile.tracking()?;
pub fn run(profile: &Profile, mode: TrackingMode) -> anyhow::Result<()> {
let store =
radicle::node::tracking::store::Config::reader(profile.home.node().join(TRACKING_DB_FILE))?;
match mode {
TrackingMode::Repos => print_repos(&store)?,
TrackingMode::Nodes => print_nodes(&store, &profile.aliases())?,
}
Ok(())
}
fn print_repos(store: &tracking::store::ConfigReader) -> anyhow::Result<()> {
let mut t = term::Table::new(term::table::TableOptions::bordered()); let mut t = term::Table::new(term::table::TableOptions::bordered());
t.push([ t.push([
term::format::default(String::from("RID")), term::format::default(String::from("RID")),
@ -43,10 +32,9 @@ fn print_repos(store: &tracking::store::ConfigReader) -> anyhow::Result<()> {
Ok(()) Ok(())
} }
fn print_nodes( pub fn following(profile: &Profile) -> anyhow::Result<()> {
store: &tracking::store::ConfigReader, let store = profile.tracking()?;
aliases: &impl AliasStore, let aliases = profile.aliases();
) -> anyhow::Result<()> {
let mut t = term::Table::new(term::table::TableOptions::bordered()); let mut t = term::Table::new(term::table::TableOptions::bordered());
t.push([ t.push([
term::format::default(String::from("DID")), term::format::default(String::from("DID")),
@ -59,7 +47,7 @@ fn print_nodes(
t.push([ t.push([
term::format::highlight(Did::from(id).to_string()), term::format::highlight(Did::from(id).to_string()),
match alias { match alias {
None => term::format::secondary(fallback_alias(&id, aliases)), None => term::format::secondary(fallback_alias(&id, &aliases)),
Some(alias) => term::format::secondary(alias.to_string()), Some(alias) => term::format::secondary(alias.to_string()),
}, },
term::format::secondary(policy.to_string()), term::format::secondary(policy.to_string()),

View File

@ -0,0 +1,150 @@
use std::ffi::OsString;
use std::time;
use anyhow::anyhow;
use radicle::node::tracking::Scope;
use radicle::node::Handle;
use radicle::{prelude::*, Node};
use crate::commands::rad_sync as sync;
use crate::terminal::args::{Args, Error, Help};
use crate::{project, terminal as term};
pub const HELP: Help = Help {
name: "seed",
description: "Manage repository seeding policies",
version: env!("CARGO_PKG_VERSION"),
usage: r#"
Usage
rad seed <rid> [-d | --delete] [--[no-]fetch] [--scope <scope>] [<option>...]
The `seed` command takes a Repository ID (<rid>) and updates the seeding policy
for that repository. By default, a seeding policy will be created or updated.
To delete a policy, use the `--delete` flag.
When seeding a repository, a scope can be specified: this can be either `all` or
`trusted`. When using `all`, all remote nodes will be followed for that repository.
On the other hand, with `trusted`, only the repository delegates will be followed,
plus any remote that is explicitly followed via `rad follow <nid>`.
Options
--delete, -d Delete the seeding policy
--[no-]fetch Fetch repository after updating seeding policy
--scope <scope> Peer follow scope for a repository
--verbose, -v Verbose output
--help Print help
"#,
};
#[derive(Debug)]
pub struct Options {
pub rid: Id,
pub scope: Scope,
pub delete: bool,
pub fetch: bool,
pub verbose: 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 scope: Option<Scope> = None;
let mut fetch = true;
let mut delete = false;
let mut verbose = false;
while let Some(arg) = parser.next()? {
match &arg {
Value(val) => {
rid = Some(term::args::rid(val)?);
}
Long("scope") if scope.is_none() => {
let val = parser.value()?;
scope = Some(term::args::parse_value("scope", val)?);
}
Long("delete") | Short('d') => delete = true,
Long("fetch") => fetch = true,
Long("no-fetch") => fetch = false,
Long("verbose") | Short('v') => verbose = true,
Long("help") | Short('h') => {
return Err(Error::Help.into());
}
_ => {
return Err(anyhow!(arg.unexpected()));
}
}
}
if scope.is_some() && delete {
anyhow::bail!("`--scope` may not be used with `--delete` or `-d`");
}
if fetch && delete {
anyhow::bail!("`--fetch` may not be used with `--delete` or `-d`");
}
Ok((
Options {
rid: rid.ok_or_else(|| anyhow!("a Repository ID must be specified"))?,
scope: scope.unwrap_or(Scope::All),
delete,
fetch,
verbose,
},
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 = options.rid;
let scope = options.scope;
if options.delete {
delete(rid, &mut node, &profile)?;
} else {
update(rid, scope, &mut node, &profile)?;
if options.fetch && node.is_running() {
sync::fetch(
rid,
sync::RepoSync::default(),
time::Duration::from_secs(6),
&mut node,
)?;
}
}
Ok(())
}
pub fn update(
rid: Id,
scope: Scope,
node: &mut Node,
profile: &Profile,
) -> Result<(), anyhow::Error> {
let updated = project::track(rid, scope, node, profile)?;
let outcome = if updated { "updated" } else { "exists" };
term::success!(
"Seeding policy {outcome} for {} with scope '{scope}'",
term::format::tertiary(rid),
);
Ok(())
}
pub fn delete(rid: Id, node: &mut Node, profile: &Profile) -> anyhow::Result<()> {
if project::untrack(rid, node, profile)? {
term::success!("Seeding policy for {} removed", term::format::tertiary(rid));
}
Ok(())
}

View File

@ -1,185 +0,0 @@
use std::ffi::OsString;
use std::time;
use anyhow::anyhow;
use radicle::node::tracking::{Alias, Scope};
use radicle::node::{Handle, NodeId};
use radicle::{prelude::*, Node};
use crate::commands::rad_sync as sync;
use crate::terminal::args::{Args, Error, Help};
use crate::{project, terminal as term};
pub const HELP: Help = Help {
name: "track",
description: "Manage repository and node tracking policy",
version: env!("CARGO_PKG_VERSION"),
usage: r#"
Usage
rad track <nid> [--alias <name>] [<option>...]
rad track <rid> [--[no-]fetch] [--scope <scope>] [<option>...]
The `track` command takes either an NID or an RID. Based on the argument, it will
either update the tracking policy of a node (NID), or a repository (RID).
When tracking a repository, a scope can be specified: this can be either `all` or
`trusted`. When using `all`, all remote nodes will be tracked for that repository.
On the other hand, with `trusted`, only the repository delegates will be tracked,
plus any remote that is explicitly tracked via `rad track <nid>`.
Options
--alias <name> Associate an alias to a tracked node
--[no-]fetch Fetch refs after tracking
--scope <scope> Node (remote) tracking scope for a repository
--verbose, -v Verbose output
--help Print help
"#,
};
#[derive(Debug)]
pub enum Operation {
TrackNode { nid: NodeId, alias: Option<Alias> },
TrackRepo { rid: Id, scope: Scope },
}
#[derive(Debug)]
pub struct Options {
pub op: Operation,
pub fetch: bool,
pub verbose: 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 op: Option<Operation> = None;
let mut fetch = true;
let mut verbose = false;
while let Some(arg) = parser.next()? {
match (&arg, &mut op) {
(Value(val), None) => {
if let Ok(rid) = term::args::rid(val) {
op = Some(Operation::TrackRepo {
rid,
scope: Scope::default(),
});
} else if let Ok(did) = term::args::did(val) {
op = Some(Operation::TrackNode {
nid: did.into(),
alias: None,
});
} else if let Ok(nid) = term::args::nid(val) {
op = Some(Operation::TrackNode { nid, alias: None });
}
}
(Long("alias"), Some(Operation::TrackNode { alias, .. })) => {
let name = parser.value()?;
let name = term::args::alias(&name)?;
*alias = Some(name.to_owned());
}
(Long("scope"), Some(Operation::TrackRepo { scope, .. })) => {
let val = parser.value()?;
*scope = term::args::parse_value("scope", val)?;
}
(Long("fetch"), Some(Operation::TrackRepo { .. })) => fetch = true,
(Long("no-fetch"), Some(Operation::TrackRepo { .. })) => fetch = false,
(Long("verbose") | Short('v'), _) => verbose = true,
(Long("help") | Short('h'), _) => {
return Err(Error::Help.into());
}
_ => {
return Err(anyhow!(arg.unexpected()));
}
}
}
Ok((
Options {
op: op.ok_or_else(|| anyhow!("either a NID or an RID must be specified"))?,
fetch,
verbose,
},
vec![],
))
}
}
pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
let profile = ctx.profile()?;
let mut node = radicle::Node::new(profile.socket());
match options.op {
Operation::TrackNode { nid, alias } => {
track_node(nid, alias, &mut node, &profile)?;
}
Operation::TrackRepo { rid, scope } => {
track_repo(rid, scope, &mut node, &profile)?;
if options.fetch && node.is_running() {
sync::fetch(
rid,
sync::RepoSync::default(),
time::Duration::from_secs(6),
&mut node,
)?;
}
}
}
Ok(())
}
pub fn track_repo(
rid: Id,
scope: Scope,
node: &mut Node,
profile: &Profile,
) -> Result<(), anyhow::Error> {
let tracked = project::track(rid, scope, node, profile)?;
let outcome = if tracked { "updated" } else { "exists" };
term::success!(
"Tracking policy {outcome} for {} with scope '{scope}'",
term::format::tertiary(rid),
);
Ok(())
}
pub fn track_node(
nid: NodeId,
alias: Option<Alias>,
node: &mut Node,
profile: &Profile,
) -> Result<(), anyhow::Error> {
let tracked = match node.track_node(nid, alias.clone()) {
Ok(updated) => updated,
Err(e) if e.is_connection_err() => {
let mut config = profile.tracking_mut()?;
config.track_node(&nid, alias.as_deref())?
}
Err(e) => return Err(e.into()),
};
let outcome = if tracked { "updated" } else { "exists" };
if let Some(alias) = alias {
term::success!(
"Tracking policy {outcome} for {} ({alias})",
term::format::tertiary(nid),
);
} else {
term::success!(
"Tracking policy {outcome} for {}",
term::format::tertiary(nid),
);
}
Ok(())
}

View File

@ -0,0 +1,91 @@
use std::ffi::OsString;
use anyhow::anyhow;
use radicle::node::{Handle, NodeId};
use crate::terminal as term;
use crate::terminal::args::{Args, Error, Help};
pub const HELP: Help = Help {
name: "unfollow",
description: "Unfollow a peer",
version: env!("CARGO_PKG_VERSION"),
usage: r#"
Usage
rad unfollow <nid> [<option>...]
The `unfollow` command takes a Node ID (<nid>), optionally in DID format,
and removes the follow policy for that peer.
Options
--verbose, -v Verbose output
--help Print help
"#,
};
#[derive(Debug)]
pub struct Options {
pub nid: NodeId,
pub verbose: 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 nid: Option<NodeId> = None;
let mut verbose = false;
while let Some(arg) = parser.next()? {
match &arg {
Value(val) if nid.is_none() => {
if let Ok(did) = term::args::did(val) {
nid = Some(did.into());
} else if let Ok(val) = term::args::nid(val) {
nid = Some(val);
} else {
anyhow::bail!("invalid Node ID `{}` specified", val.to_string_lossy());
}
}
Long("verbose") | Short('v') => verbose = true,
Long("help") | Short('h') => {
return Err(Error::Help.into());
}
_ => {
return Err(anyhow!(arg.unexpected()));
}
}
}
Ok((
Options {
nid: nid.ok_or_else(|| anyhow!("a Node ID must be specified"))?,
verbose,
},
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 nid = options.nid;
let unfollowed = match node.untrack_node(nid) {
Ok(updated) => updated,
Err(e) if e.is_connection_err() => {
let mut config = profile.tracking_mut()?;
config.untrack_node(&nid)?
}
Err(e) => return Err(e.into()),
};
if unfollowed {
term::success!("Follow policy for {} removed", term::format::tertiary(nid),);
}
Ok(())
}

View File

@ -1,121 +0,0 @@
use std::ffi::OsString;
use anyhow::anyhow;
use radicle::node::{Handle, NodeId};
use radicle::{prelude::*, Node};
use crate::project;
use crate::terminal as term;
use crate::terminal::args::{Args, Error, Help};
pub const HELP: Help = Help {
name: "untrack",
description: "Untrack a repository or node",
version: env!("CARGO_PKG_VERSION"),
usage: r#"
Usage
rad untrack <nid> [<option>...]
rad untrack <rid> [<option>...]
The `untrack` command takes either an NID or an RID. Based on the argument, it will
either update the tracking policy of a node (NID), or a repository (RID).
Options
--verbose, -v Verbose output
--help Print help
"#,
};
#[derive(Debug)]
pub enum Operation {
UntrackNode { nid: NodeId },
UntrackRepo { rid: Id },
}
#[derive(Debug)]
pub struct Options {
pub op: Operation,
pub verbose: 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 op: Option<Operation> = None;
let mut verbose = false;
while let Some(arg) = parser.next()? {
match (&arg, &mut op) {
(Value(val), None) => {
if let Ok(rid) = term::args::rid(val) {
op = Some(Operation::UntrackRepo { rid });
} else if let Ok(did) = term::args::did(val) {
op = Some(Operation::UntrackNode { nid: did.into() });
} else if let Ok(nid) = term::args::nid(val) {
op = Some(Operation::UntrackNode { nid });
}
}
(Long("verbose") | Short('v'), _) => verbose = true,
(Long("help") | Short('h'), _) => {
return Err(Error::Help.into());
}
_ => {
return Err(anyhow!(arg.unexpected()));
}
}
}
Ok((
Options {
op: op.ok_or_else(|| anyhow!("either an NID or an RID must be specified"))?,
verbose,
},
vec![],
))
}
}
pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
let profile = ctx.profile()?;
let mut node = radicle::Node::new(profile.socket());
match options.op {
Operation::UntrackNode { nid } => untrack_node(nid, &mut node, &profile),
Operation::UntrackRepo { rid } => untrack_repo(rid, &mut node, &profile),
}?;
Ok(())
}
pub fn untrack_repo(rid: Id, node: &mut Node, profile: &Profile) -> anyhow::Result<()> {
if project::untrack(rid, node, profile)? {
term::success!(
"Tracking policy for {} removed",
term::format::tertiary(rid),
);
}
Ok(())
}
pub fn untrack_node(nid: NodeId, node: &mut Node, profile: &Profile) -> anyhow::Result<()> {
let untracked = match node.untrack_node(nid) {
Ok(updated) => updated,
Err(e) if e.is_connection_err() => {
let mut config = profile.tracking_mut()?;
config.untrack_node(&nid)?
}
Err(e) => return Err(e.into()),
};
if untracked {
term::success!(
"Tracking policy for {} removed",
term::format::tertiary(nid),
);
}
Ok(())
}

View File

@ -139,6 +139,13 @@ fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option<anyhow::Error>>
args.to_vec(), args.to_vec(),
); );
} }
"follow" => {
term::run_command_args::<rad_follow::Options, _>(
rad_follow::HELP,
rad_follow::run,
args.to_vec(),
);
}
"fork" => { "fork" => {
term::run_command_args::<rad_fork::Options, _>( term::run_command_args::<rad_fork::Options, _>(
rad_fork::HELP, rad_fork::HELP,
@ -229,17 +236,17 @@ fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option<anyhow::Error>>
args.to_vec(), args.to_vec(),
); );
} }
"track" => { "seed" => {
term::run_command_args::<rad_track::Options, _>( term::run_command_args::<rad_seed::Options, _>(
rad_track::HELP, rad_seed::HELP,
rad_track::run, rad_seed::run,
args.to_vec(), args.to_vec(),
); );
} }
"untrack" => { "unfollow" => {
term::run_command_args::<rad_untrack::Options, _>( term::run_command_args::<rad_unfollow::Options, _>(
rad_untrack::HELP, rad_unfollow::HELP,
rad_untrack::run, rad_unfollow::run,
args.to_vec(), args.to_vec(),
); );
} }

View File

@ -1297,7 +1297,7 @@ fn test_replication_via_seed() {
.unwrap(); .unwrap();
alice alice
.rad("track", &[&bob.id.to_human()], working.join("alice")) .rad("follow", &[&bob.id.to_human()], working.join("alice"))
.unwrap(); .unwrap();
alice.routes_to(&[(rid, alice.id), (rid, seed.id)]); alice.routes_to(&[(rid, alice.id), (rid, seed.id)]);