Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
cli: From "tracking" to "seeding" and "following"
cloudhead committed 2 years ago
commit 4605348d68d5ebc27cf578cbec759b56b0fc1ba7
parent ee9ee691306a8accc627a00835a3116e9d235814
18 files changed +496 -458
modified radicle-cli/examples/rad-fetch.md
@@ -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.

```
modified radicle-cli/examples/rad-init-private-clone.md
@@ -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)
modified radicle-cli/examples/rad-node.md
@@ -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 │
├───────────────────────────────────────────────────────────────────────────┤
modified radicle-cli/examples/rad-sync-without-node.md
@@ -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'
```
modified radicle-cli/examples/rad-track.md
@@ -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'
```
modified radicle-cli/examples/workflow/5-patching-maintainer.md
@@ -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
modified radicle-cli/src/commands.rs
@@ -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;
added radicle-cli/src/commands/follow.rs
@@ -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(())
+
}
modified radicle-cli/src/commands/help.rs
@@ -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,
];
modified radicle-cli/src/commands/node.rs
@@ -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(())
added radicle-cli/src/commands/node/policies.rs
@@ -0,0 +1,65 @@
+
use radicle::crypto::PublicKey;
+
use radicle::node::{tracking, AliasStore};
+
use radicle::prelude::Did;
+
use radicle::Profile;
+

+
use crate::terminal as term;
+
use term::Element;
+

+
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")),
+
        term::format::default(String::from("Scope")),
+
        term::format::default(String::from("Policy")),
+
    ]);
+
    t.divider();
+

+
    for tracking::Repo { id, scope, policy } in store.repo_policies()? {
+
        let id = id.to_string();
+
        let scope = scope.to_string();
+
        let policy = policy.to_string();
+

+
        t.push([
+
            term::format::highlight(id),
+
            term::format::secondary(scope),
+
            term::format::secondary(policy),
+
        ])
+
    }
+
    t.print();
+

+
    Ok(())
+
}
+

+
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")),
+
        term::format::default(String::from("Alias")),
+
        term::format::default(String::from("Policy")),
+
    ]);
+
    t.divider();
+

+
    for tracking::Node { id, alias, policy } in store.node_policies()? {
+
        t.push([
+
            term::format::highlight(Did::from(id).to_string()),
+
            match alias {
+
                None => term::format::secondary(fallback_alias(&id, &aliases)),
+
                Some(alias) => term::format::secondary(alias.to_string()),
+
            },
+
            term::format::secondary(policy.to_string()),
+
        ]);
+
    }
+
    t.print();
+

+
    Ok(())
+
}
+

+
fn fallback_alias(nid: &PublicKey, aliases: &impl AliasStore) -> String {
+
    aliases
+
        .alias(nid)
+
        .map_or("n/a".to_string(), |alias| alias.to_string())
+
}
deleted radicle-cli/src/commands/node/tracking.rs
@@ -1,77 +0,0 @@
-
use radicle::crypto::PublicKey;
-
use radicle::node::{tracking, AliasStore, TRACKING_DB_FILE};
-
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<()> {
-
    let mut t = term::Table::new(term::table::TableOptions::bordered());
-
    t.push([
-
        term::format::default(String::from("RID")),
-
        term::format::default(String::from("Scope")),
-
        term::format::default(String::from("Policy")),
-
    ]);
-
    t.divider();
-

-
    for tracking::Repo { id, scope, policy } in store.repo_policies()? {
-
        let id = id.to_string();
-
        let scope = scope.to_string();
-
        let policy = policy.to_string();
-

-
        t.push([
-
            term::format::highlight(id),
-
            term::format::secondary(scope),
-
            term::format::secondary(policy),
-
        ])
-
    }
-
    t.print();
-

-
    Ok(())
-
}
-

-
fn print_nodes(
-
    store: &tracking::store::ConfigReader,
-
    aliases: &impl AliasStore,
-
) -> anyhow::Result<()> {
-
    let mut t = term::Table::new(term::table::TableOptions::bordered());
-
    t.push([
-
        term::format::default(String::from("DID")),
-
        term::format::default(String::from("Alias")),
-
        term::format::default(String::from("Policy")),
-
    ]);
-
    t.divider();
-

-
    for tracking::Node { id, alias, policy } in store.node_policies()? {
-
        t.push([
-
            term::format::highlight(Did::from(id).to_string()),
-
            match alias {
-
                None => term::format::secondary(fallback_alias(&id, aliases)),
-
                Some(alias) => term::format::secondary(alias.to_string()),
-
            },
-
            term::format::secondary(policy.to_string()),
-
        ]);
-
    }
-
    t.print();
-

-
    Ok(())
-
}
-

-
fn fallback_alias(nid: &PublicKey, aliases: &impl AliasStore) -> String {
-
    aliases
-
        .alias(nid)
-
        .map_or("n/a".to_string(), |alias| alias.to_string())
-
}
added radicle-cli/src/commands/seed.rs
@@ -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(())
+
}
deleted radicle-cli/src/commands/track.rs
@@ -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(())
-
}
added radicle-cli/src/commands/unfollow.rs
@@ -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(())
+
}
deleted radicle-cli/src/commands/untrack.rs
@@ -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(())
-
}
modified radicle-cli/src/main.rs
@@ -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(),
            );
        }
modified radicle-cli/tests/commands.rs
@@ -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)]);