diff --git a/radicle-cli/examples/rad-node.md b/radicle-cli/examples/rad-node.md index 124d2bcf..d0e86735 100644 --- a/radicle-cli/examples/rad-node.md +++ b/radicle-cli/examples/rad-node.md @@ -35,11 +35,8 @@ those commands we'll first track a peer so that we have something to see. ``` -$ rad track z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --alias Bob -Establishing 🌱 tracking relationship for heartwood - -✓ Tracking relationship with Bob (z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk) established -! Warning: fetch after track is not yet supported +$ rad track did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --alias Bob --no-fetch +✓ Tracking policy updated for z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk (Bob) ``` Now, when we use the `rad node tracking` command we will see diff --git a/radicle-cli/examples/rad-track.md b/radicle-cli/examples/rad-track.md new file mode 100644 index 00000000..ec102fb9 --- /dev/null +++ b/radicle-cli/examples/rad-track.md @@ -0,0 +1,14 @@ +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 --no-fetch +✓ Tracking 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' +``` diff --git a/radicle-cli/src/commands/track.rs b/radicle-cli/src/commands/track.rs index 61e0a989..c829107f 100644 --- a/radicle-cli/src/commands/track.rs +++ b/radicle-cli/src/commands/track.rs @@ -1,36 +1,51 @@ use std::ffi::OsString; -use std::str::FromStr; -use anyhow::{anyhow, Context as _}; +use anyhow::anyhow; +use radicle::node::tracking::{Alias, Scope}; use radicle::node::{Handle, NodeId}; -use radicle::storage::ReadStorage; +use radicle::{prelude::*, Node}; use crate::terminal as term; use crate::terminal::args::{Args, Error, Help}; pub const HELP: Help = Help { name: "track", - description: "Manage project tracking policy", + description: "Manage repository and node tracking policy", version: env!("CARGO_PKG_VERSION"), usage: r#" Usage - rad track [--fetch] [--alias ] + rad track [--[no-]fetch] [--alias ] + rad track [--[no-]fetch] [--scope ] + + The `track` command takes either a DID or an RID. Based on the argument, it will + either update the tracking policy of a node (DID), 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 `. Options - --alias Add an alias to this peer identifier - --fetch Fetch the peer's refs into the working copy + --alias Associate an alias to a tracked node + --fetch Fetch refs after tracking + --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 }, + TrackRepo { rid: Id, scope: Scope }, +} + #[derive(Debug)] pub struct Options { - pub peer: NodeId, - pub alias: Option, + pub op: Operation, pub fetch: bool, pub verbose: bool, } @@ -40,34 +55,48 @@ impl Args for Options { use lexopt::prelude::*; let mut parser = lexopt::Parser::from_args(args); - let mut peer: Option = None; - let mut alias: Option = None; + let mut op: Option = None; let mut fetch = true; let mut verbose = false; while let Some(arg) = parser.next()? { - match arg { - Long("alias") => { + 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 = name .to_str() .to_owned() .ok_or_else(|| anyhow!("alias specified is not UTF-8"))?; - alias = Some(name.to_owned()); + *alias = Some(name.to_owned()); } - Long("no-fetch") => fetch = false, - Long("verbose") | Short('v') => verbose = true, - Value(val) if peer.is_none() => { - let val = val.to_string_lossy(); + (Long("scope"), Some(Operation::TrackRepo { scope, .. })) => { + let val = parser.value()?; - if let Ok(val) = NodeId::from_str(&val) { - peer = Some(val); - } else { - return Err(anyhow!("invalid Node ID '{}'", val)); - } + *scope = val + .to_str() + .to_owned() + .ok_or_else(|| anyhow!("scope specified is not UTF-8"))? + .parse()?; } - Long("help") => { + (Long("no-fetch"), _) => fetch = false, + (Long("verbose") | Short('v'), _) => verbose = true, + (Long("help"), _) => { return Err(Error::Help.into()); } _ => { @@ -78,8 +107,7 @@ impl Args for Options { Ok(( Options { - peer: peer.ok_or_else(|| anyhow!("a peer to track must be supplied"))?, - alias, + op: op.ok_or_else(|| anyhow!("either a DID or an RID must be specified"))?, fetch, verbose, }, @@ -89,32 +117,13 @@ impl Args for Options { } pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> { - let peer = options.peer; let profile = ctx.profile()?; - let storage = &profile.storage; - let (_, rid) = radicle::rad::cwd().context("this command must be run within a project")?; - let project = storage.repository(rid)?.project_of(profile.id())?; let mut node = radicle::Node::new(profile.socket()); - term::info!( - "Establishing 🌱 tracking relationship for {}", - term::format::highlight(project.name()) - ); - term::blank(); - - let tracked = node.track_node(peer, options.alias.clone())?; - let outcome = if tracked { "established" } else { "exists" }; - - if let Some(alias) = options.alias { - term::success!( - "Tracking relationship with {} ({}) {}", - term::format::tertiary(alias), - peer, - outcome - ); - } else { - term::success!("Tracking relationship with {} {}", peer, outcome); - } + match options.op { + Operation::TrackNode { nid, alias } => track_node(nid, alias, &mut node), + Operation::TrackRepo { rid, scope } => track_repo(rid, scope, &mut node), + }?; if options.fetch { // TODO: Run a proper fetch here. @@ -123,3 +132,34 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> { Ok(()) } + +pub fn track_repo(rid: Id, scope: Scope, node: &mut Node) -> anyhow::Result<()> { + let tracked = node.track_repo(rid, scope)?; + 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, node: &mut Node) -> anyhow::Result<()> { + let tracked = node.track_node(nid, alias.clone())?; + 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(()) +} diff --git a/radicle-cli/tests/commands.rs b/radicle-cli/tests/commands.rs index 13435528..f558db18 100644 --- a/radicle-cli/tests/commands.rs +++ b/radicle-cli/tests/commands.rs @@ -272,6 +272,22 @@ fn rad_rm() { test("examples/rad-rm.md", working.path(), Some(home), []).unwrap(); } +#[test] +fn rad_track() { + let mut environment = Environment::new(); + let alice = environment.node("alice"); + let working = tempfile::tempdir().unwrap(); + let alice = alice.spawn(Config::default()); + + test( + "examples/rad-track.md", + working.path(), + Some(&alice.home), + [], + ) + .unwrap(); +} + #[test] fn rad_clone() { logger::init(log::Level::Debug);