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
storage. In this scenario, we know that the project is
`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
Tracking policy updated for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji with scope 'trusted'
$ rad seed rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --no-fetch
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
our local storage. Note that the `track` command can also be told to fetch
Now that the project is seeding we can fetch it and we will have it in
our local storage. Note that the `seed` command can also be told to fetch
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
$ rad track rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu
Tracking policy updated for rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu with scope 'trusted'
$ rad seed rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu --scope trusted
Seeding policy updated for rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu with scope 'trusted'
$ rad ls
```
``` ~bob (fail)

View File

@ -18,22 +18,22 @@ $ rad node status
✓ Node is running.
```
The node also allows us to query data that it has access too such as
the tracking relationships and the routing table. Before we explore
those commands we'll first track a peer so that we have something to
The node also allows us to query data that it has access to such as
the follow policies and the routing table. Before we explore
those commands we'll first follow a peer so that we have something to
see.
```
$ rad track did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --alias Bob
Tracking policy updated for z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk (Bob)
$ rad follow did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --alias Bob
Follow policy updated for z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk (Bob)
```
Now, when we use the `rad node tracking` command we will see
information for repositories that we track -- in this case a
Now, when we use the `rad node seeding` command we will see
information for repositories that we seed -- in this case a
repository that was already created:
```
$ rad node tracking
$ rad node seeding
╭──────────────────────────────────────────────────────╮
│ 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
which nodes we are specifically tracking, then we use the `--nodes`
flag:
If we wish to see which nodes we are following:
```
$ rad node tracking --nodes
$ rad node following
╭───────────────────────────────────────────────────────────────────────────╮
│ 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`
```
Note that tracking works fine without a running node:
Note that seeding works fine without a running node:
``` ~alice
$ rad track rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Tracking policy updated for rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 with scope 'trusted'
$ rad seed rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
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":
```
$ rad track did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --alias eve
Tracking policy updated for z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk (eve)
$ rad follow did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --alias eve
Follow policy updated for z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk (eve)
```
Now let's track one of Eve's repositories:
```
$ rad track rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --scope trusted --no-fetch
Tracking policy updated for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji with scope 'trusted'
$ rad seed rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --scope trusted --no-fetch
Seeding policy updated for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji with scope 'trusted'
```

View File

@ -1,11 +1,11 @@
Back to being the project maintainer.
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
Tracking policy updated for z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk (bob)
$ rad follow did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --alias bob
Follow policy updated for z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk (bob)
```
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;
#[path = "commands/diff.rs"]
pub mod rad_diff;
#[path = "commands/follow.rs"]
pub mod rad_follow;
#[path = "commands/fork.rs"]
pub mod rad_fork;
#[path = "commands/help.rs"]
@ -36,11 +38,11 @@ pub mod rad_path;
pub mod rad_publish;
#[path = "commands/remote.rs"]
pub mod rad_remote;
#[path = "commands/seed.rs"]
pub mod rad_seed;
#[path = "commands/self.rs"]
pub mod rad_self;
#[path = "commands/sync.rs"]
pub mod rad_sync;
#[path = "commands/track.rs"]
pub mod rad_track;
#[path = "commands/untrack.rs"]
pub mod rad_untrack;
#[path = "commands/unfollow.rs"]
pub mod rad_unfollow;

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_clean::HELP,
rad_self::HELP,
rad_track::HELP,
rad_untrack::HELP,
rad_seed::HELP,
rad_follow::HELP,
rad_unfollow::HELP,
rad_remote::HELP,
rad_sync::HELP,
];

View File

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

View File

@ -1,24 +1,13 @@
use radicle::crypto::PublicKey;
use radicle::node::{tracking, AliasStore, TRACKING_DB_FILE};
use radicle::node::{tracking, AliasStore};
use radicle::prelude::Did;
use radicle::Profile;
use crate::terminal as term;
use term::Element;
use super::TrackingMode;
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<()> {
pub fn seeding(profile: &Profile) -> anyhow::Result<()> {
let store = profile.tracking()?;
let mut t = term::Table::new(term::table::TableOptions::bordered());
t.push([
term::format::default(String::from("RID")),
@ -43,10 +32,9 @@ fn print_repos(store: &tracking::store::ConfigReader) -> anyhow::Result<()> {
Ok(())
}
fn print_nodes(
store: &tracking::store::ConfigReader,
aliases: &impl AliasStore,
) -> anyhow::Result<()> {
pub fn following(profile: &Profile) -> anyhow::Result<()> {
let store = profile.tracking()?;
let aliases = profile.aliases();
let mut t = term::Table::new(term::table::TableOptions::bordered());
t.push([
term::format::default(String::from("DID")),
@ -59,7 +47,7 @@ fn print_nodes(
t.push([
term::format::highlight(Did::from(id).to_string()),
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()),
},
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(),
);
}
"follow" => {
term::run_command_args::<rad_follow::Options, _>(
rad_follow::HELP,
rad_follow::run,
args.to_vec(),
);
}
"fork" => {
term::run_command_args::<rad_fork::Options, _>(
rad_fork::HELP,
@ -229,17 +236,17 @@ fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option<anyhow::Error>>
args.to_vec(),
);
}
"track" => {
term::run_command_args::<rad_track::Options, _>(
rad_track::HELP,
rad_track::run,
"seed" => {
term::run_command_args::<rad_seed::Options, _>(
rad_seed::HELP,
rad_seed::run,
args.to_vec(),
);
}
"untrack" => {
term::run_command_args::<rad_untrack::Options, _>(
rad_untrack::HELP,
rad_untrack::run,
"unfollow" => {
term::run_command_args::<rad_unfollow::Options, _>(
rad_unfollow::HELP,
rad_unfollow::run,
args.to_vec(),
);
}

View File

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