Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
cli: Implement tracking correctly
Alexis Sellier committed 3 years ago
commit a100f1c683c05349866c301b4e243d80bf4d578b
parent 46951b1b42b4550d0cd9f82e6f7138627bc55266
10 files changed +257 -85
modified radicle-cli/src/commands/clone.rs
@@ -84,7 +84,7 @@ pub fn clone(id: Id, _interactive: Interactive, ctx: impl term::Context) -> anyh
    let signer = term::signer(&profile)?;

    // Track & fetch project.
-
    node.track(&id).context("track")?;
+
    node.track_repo(&id).context("track")?;
    node.fetch(&id).context("fetch")?;

    // Create a local fork of the project, under our own id.
modified radicle-cli/src/commands/track.rs
@@ -1,8 +1,8 @@
use std::ffi::OsString;
+
use std::str::FromStr;

use anyhow::{anyhow, Context as _};

-
use radicle::identity::project::Id;
use radicle::node::Handle;
use radicle::prelude::*;
use radicle::storage::WriteStorage;
@@ -12,15 +12,16 @@ use crate::terminal::args::{Args, Error, Help};

pub const HELP: Help = Help {
    name: "track",
-
    description: "Manage project tracking relationships",
+
    description: "Manage project tracking policy",
    version: env!("CARGO_PKG_VERSION"),
    usage: r#"
Usage

-
    rad track [<id>] [--fetch]
+
    rad track <peer> [--fetch] [--alias <name>]

Options

+
    --alias <name>         Add an alias to this peer identifier
    --fetch                Fetch the peer's refs into the working copy
    --verbose, -v          Verbose output
    --help                 Print help
@@ -29,7 +30,8 @@ Options

#[derive(Debug)]
pub struct Options {
-
    pub id: Option<Id>,
+
    pub peer: NodeId,
+
    pub alias: Option<String>,
    pub fetch: bool,
    pub verbose: bool,
}
@@ -39,21 +41,31 @@ impl Args for Options {
        use lexopt::prelude::*;

        let mut parser = lexopt::Parser::from_args(args);
-
        let mut id: Option<Id> = None;
+
        let mut peer: Option<NodeId> = None;
+
        let mut alias: Option<String> = None;
        let mut fetch = true;
        let mut verbose = false;

        while let Some(arg) = parser.next()? {
            match arg {
+
                Long("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());
+
                }
                Long("no-fetch") => fetch = false,
                Long("verbose") | Short('v') => verbose = true,
-
                Value(val) if id.is_none() => {
+
                Value(val) if peer.is_none() => {
                    let val = val.to_string_lossy();

-
                    if let Ok(val) = Id::from_human(&val) {
-
                        id = Some(val);
+
                    if let Ok(val) = NodeId::from_str(&val) {
+
                        peer = Some(val);
                    } else {
-
                        return Err(anyhow!("invalid ID '{}'", val));
+
                        return Err(anyhow!("invalid Node ID '{}'", val));
                    }
                }
                Long("help") => {
@@ -65,18 +77,24 @@ impl Args for Options {
            }
        }

-
        Ok((Options { id, fetch, verbose }, vec![]))
+
        Ok((
+
            Options {
+
                peer: peer.ok_or_else(|| anyhow!("a peer to track must be supplied"))?,
+
                alias,
+
                fetch,
+
                verbose,
+
            },
+
            vec![],
+
        ))
    }
}

pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
-
    let id = options
-
        .id
-
        .or_else(|| radicle::rad::cwd().ok().map(|(_, id)| id))
-
        .context("current directory is not a git repository; please supply an `<id>`")?;
+
    let peer = options.peer;
    let profile = ctx.profile()?;
    let storage = &profile.storage;
-
    let Doc { payload, .. } = storage.repository(id)?.project_of(profile.id())?;
+
    let (_, rid) = radicle::rad::cwd().context("this command must be run within a project")?;
+
    let Doc { payload, .. } = storage.repository(rid)?.project_of(profile.id())?;
    let node = radicle::node::connect(&profile.node())?;

    term::info!(
@@ -85,16 +103,22 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
    );
    term::blank();

-
    let tracked = node.track(&id)?;
-
    term::success!(
-
        "Tracking relationship for {} ({}) {}",
-
        term::format::tertiary(&payload.name),
-
        &id.to_human(),
-
        if tracked { "established" } else { "exists" }
-
    );
+
    let tracked = node.track_node(&peer, options.alias.as_deref())?;
+
    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);
+
    }

    if options.fetch {
-
        node.fetch(&id)?;
+
        node.fetch(&rid)?;
    }

    Ok(())
modified radicle-cli/src/commands/untrack.rs
@@ -89,5 +89,5 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {

pub fn untrack(id: Id, profile: &Profile) -> anyhow::Result<bool> {
    let node = radicle::node::connect(profile.node())?;
-
    node.untrack(&id).map_err(|e| anyhow!(e))
+
    node.untrack_repo(&id).map_err(|e| anyhow!(e))
}
modified radicle-node/src/client/handle.rs
@@ -66,15 +66,31 @@ impl<W: Waker> traits::Handle for Handle<W> {
        receiver.recv().map_err(Error::from)
    }

-
    fn track(&mut self, id: Id) -> Result<bool, Error> {
+
    fn track_node(&mut self, id: NodeId, alias: Option<String>) -> Result<bool, Error> {
        let (sender, receiver) = chan::bounded(1);
-
        self.commands.send(service::Command::Track(id, sender))?;
+
        self.commands
+
            .send(service::Command::TrackNode(id, alias, sender))?;
        receiver.recv().map_err(Error::from)
    }

-
    fn untrack(&mut self, id: Id) -> Result<bool, Error> {
+
    fn untrack_node(&mut self, id: NodeId) -> Result<bool, Error> {
        let (sender, receiver) = chan::bounded(1);
-
        self.commands.send(service::Command::Untrack(id, sender))?;
+
        self.commands
+
            .send(service::Command::UntrackNode(id, sender))?;
+
        receiver.recv().map_err(Error::from)
+
    }
+

+
    fn track_repo(&mut self, id: Id) -> Result<bool, Error> {
+
        let (sender, receiver) = chan::bounded(1);
+
        self.commands
+
            .send(service::Command::TrackRepo(id, sender))?;
+
        receiver.recv().map_err(Error::from)
+
    }
+

+
    fn untrack_repo(&mut self, id: Id) -> Result<bool, Error> {
+
        let (sender, receiver) = chan::bounded(1);
+
        self.commands
+
            .send(service::Command::UntrackRepo(id, sender))?;
        receiver.recv().map_err(Error::from)
    }

@@ -146,9 +162,13 @@ pub mod traits {
        fn fetch(&mut self, id: Id) -> Result<FetchLookup, Error>;
        /// Start tracking the given project. Doesn't do anything if the project is already
        /// tracked.
-
        fn track(&mut self, id: Id) -> Result<bool, Error>;
+
        fn track_repo(&mut self, id: Id) -> Result<bool, Error>;
+
        /// Start tracking the given node.
+
        fn track_node(&mut self, id: NodeId, alias: Option<String>) -> Result<bool, Error>;
        /// Untrack the given project and delete it from storage.
-
        fn untrack(&mut self, id: Id) -> Result<bool, Error>;
+
        fn untrack_repo(&mut self, id: Id) -> Result<bool, Error>;
+
        /// Untrack the given node.
+
        fn untrack_node(&mut self, id: NodeId) -> Result<bool, Error>;
        /// Notify the client that a project has been updated.
        fn announce_refs(&mut self, id: Id) -> Result<(), Error>;
        /// Send a command to the command channel, and wake up the event loop.
modified radicle-node/src/control.rs
@@ -83,9 +83,9 @@ fn drain<H: Handle>(stream: &UnixStream, handle: &mut H) -> Result<(), DrainErro
                    return Err(DrainError::InvalidCommandArg(arg.to_owned()));
                }
            }
-
            Some(("track", arg)) => {
+
            Some(("track-repo", arg)) => {
                if let Ok(id) = arg.parse() {
-
                    match handle.track(id) {
+
                    match handle.track_repo(id) {
                        Ok(updated) => {
                            if updated {
                                writeln!(writer, "{}", node::RESPONSE_OK)?;
@@ -101,9 +101,50 @@ fn drain<H: Handle>(stream: &UnixStream, handle: &mut H) -> Result<(), DrainErro
                    return Err(DrainError::InvalidCommandArg(arg.to_owned()));
                }
            }
-
            Some(("untrack", arg)) => {
+
            Some(("untrack-repo", arg)) => {
                if let Ok(id) = arg.parse() {
-
                    match handle.untrack(id) {
+
                    match handle.untrack_repo(id) {
+
                        Ok(updated) => {
+
                            if updated {
+
                                writeln!(writer, "{}", node::RESPONSE_OK)?;
+
                            } else {
+
                                writeln!(writer, "{}", node::RESPONSE_NOOP)?;
+
                            }
+
                        }
+
                        Err(e) => {
+
                            return Err(DrainError::Client(e));
+
                        }
+
                    }
+
                } else {
+
                    return Err(DrainError::InvalidCommandArg(arg.to_owned()));
+
                }
+
            }
+
            Some(("track-node", args)) => {
+
                let (peer, alias) = if let Some((peer, alias)) = args.split_once(' ') {
+
                    (peer, Some(alias.to_owned()))
+
                } else {
+
                    (args, None)
+
                };
+
                if let Ok(id) = peer.parse() {
+
                    match handle.track_node(id, alias) {
+
                        Ok(updated) => {
+
                            if updated {
+
                                writeln!(writer, "{}", node::RESPONSE_OK)?;
+
                            } else {
+
                                writeln!(writer, "{}", node::RESPONSE_NOOP)?;
+
                            }
+
                        }
+
                        Err(e) => {
+
                            return Err(DrainError::Client(e));
+
                        }
+
                    }
+
                } else {
+
                    return Err(DrainError::InvalidCommandArg(args.to_owned()));
+
                }
+
            }
+
            Some(("untrack-node", arg)) => {
+
                if let Ok(id) = arg.parse() {
+
                    match handle.untrack_node(id) {
                        Ok(updated) => {
                            if updated {
                                writeln!(writer, "{}", node::RESPONSE_OK)?;
@@ -214,7 +255,7 @@ mod tests {
    use super::*;
    use crate::identity::Id;
    use crate::node::Handle;
-
    use crate::node::Node;
+
    use crate::node::{Node, NodeId};
    use crate::test;

    #[test]
@@ -255,6 +296,7 @@ mod tests {
        let tmp = tempfile::tempdir().unwrap();
        let socket = tmp.path().join("node.sock");
        let proj = test::arbitrary::gen::<Id>(1);
+
        let peer = test::arbitrary::gen::<NodeId>(1);

        thread::spawn({
            let socket = socket.clone();
@@ -269,9 +311,14 @@ mod tests {
            }
        };

-
        assert!(handle.track(&proj).unwrap());
-
        assert!(!handle.track(&proj).unwrap());
-
        assert!(handle.untrack(&proj).unwrap());
-
        assert!(!handle.untrack(&proj).unwrap());
+
        assert!(handle.track_repo(&proj).unwrap());
+
        assert!(!handle.track_repo(&proj).unwrap());
+
        assert!(handle.untrack_repo(&proj).unwrap());
+
        assert!(!handle.untrack_repo(&proj).unwrap());
+

+
        assert!(handle.track_node(&peer, Some("alice")).unwrap());
+
        assert!(!handle.track_node(&peer, Some("alice")).unwrap());
+
        assert!(handle.untrack_node(&peer).unwrap());
+
        assert!(!handle.untrack_node(&peer).unwrap());
    }
}
modified radicle-node/src/service.rs
@@ -150,9 +150,13 @@ pub enum Command {
    /// Fetch the given project from the network.
    Fetch(Id, chan::Sender<FetchLookup>),
    /// Track the given project.
-
    Track(Id, chan::Sender<bool>),
+
    TrackRepo(Id, chan::Sender<bool>),
    /// Untrack the given project.
-
    Untrack(Id, chan::Sender<bool>),
+
    UntrackRepo(Id, chan::Sender<bool>),
+
    /// Track the given node.
+
    TrackNode(NodeId, Option<String>, chan::Sender<bool>),
+
    /// Untrack the given node.
+
    UntrackNode(NodeId, chan::Sender<bool>),
    /// Query the internal service state.
    QueryState(Arc<QueryState>, chan::Sender<Result<(), CommandError>>),
}
@@ -163,8 +167,10 @@ impl fmt::Debug for Command {
            Self::AnnounceRefs(id) => write!(f, "AnnounceRefs({})", id),
            Self::Connect(addr) => write!(f, "Connect({})", addr),
            Self::Fetch(id, _) => write!(f, "Fetch({})", id),
-
            Self::Track(id, _) => write!(f, "Track({})", id),
-
            Self::Untrack(id, _) => write!(f, "Untrack({})", id),
+
            Self::TrackRepo(id, _) => write!(f, "TrackRepo({})", id),
+
            Self::UntrackRepo(id, _) => write!(f, "UntrackRepo({})", id),
+
            Self::TrackNode(id, _, _) => write!(f, "TrackNode({})", id),
+
            Self::UntrackNode(id, _) => write!(f, "UntrackNode({})", id),
            Self::QueryState { .. } => write!(f, "QueryState(..)"),
        }
    }
@@ -290,20 +296,20 @@ where
        }
    }

-
    /// Track a project.
+
    /// Track a repository.
    /// Returns whether or not the tracking policy was updated.
-
    pub fn track(&mut self, id: &Id, scope: tracking::Scope) -> Result<bool, tracking::Error> {
+
    pub fn track_repo(&mut self, id: &Id, scope: tracking::Scope) -> Result<bool, tracking::Error> {
        self.out_of_sync = self.tracking.track_repo(id, scope)?;
        self.filter.insert(id);

        Ok(self.out_of_sync)
    }

-
    /// Untrack a project.
+
    /// Untrack a repository.
    /// Returns whether or not the tracking policy was updated.
    /// Note that when untracking, we don't announce anything to the network. This is because by
    /// simply not announcing it anymore, it will eventually be pruned by nodes.
-
    pub fn untrack(&mut self, id: &Id) -> Result<bool, tracking::Error> {
+
    pub fn untrack_repo(&mut self, id: &Id) -> Result<bool, tracking::Error> {
        // Nb. This is potentially slow if we have lots of projects. We should probably
        // only re-compute the filter when we've untracked a certain amount of projects
        // and the filter is really out of date.
@@ -491,18 +497,32 @@ where
                    }
                }
            }
-
            Command::Track(id, resp) => {
+
            Command::TrackRepo(id, resp) => {
                let tracked = self
-
                    .track(&id, tracking::Scope::All)
+
                    .track_repo(&id, tracking::Scope::All)
                    .expect("Service::command: error tracking repository");
                resp.send(tracked).ok();
            }
-
            Command::Untrack(id, resp) => {
+
            Command::UntrackRepo(id, resp) => {
                let untracked = self
-
                    .untrack(&id)
+
                    .untrack_repo(&id)
                    .expect("Service::command: error untracking repository");
                resp.send(untracked).ok();
            }
+
            Command::TrackNode(id, alias, resp) => {
+
                let tracked = self
+
                    .tracking
+
                    .track_node(&id, alias.as_deref())
+
                    .expect("Service::command: error tracking node");
+
                resp.send(tracked).ok();
+
            }
+
            Command::UntrackNode(id, resp) => {
+
                let untracked = self
+
                    .tracking
+
                    .untrack_node(&id)
+
                    .expect("Service::command: error untracking node");
+
                resp.send(untracked).ok();
+
            }
            Command::AnnounceRefs(id) => {
                if let Err(err) = self.announce_refs(id) {
                    error!("Error announcing refs: {}", err);
modified radicle-node/src/test/handle.rs
@@ -8,11 +8,13 @@ use crate::client::handle::Error;
use crate::identity::Id;
use crate::service;
use crate::service::FetchLookup;
+
use crate::service::NodeId;

#[derive(Default, Clone)]
pub struct Handle {
    pub updates: Arc<Mutex<Vec<Id>>>,
-
    pub tracking: HashSet<Id>,
+
    pub tracking_repos: HashSet<Id>,
+
    pub tracking_nodes: HashSet<NodeId>,
}

impl traits::Handle for Handle {
@@ -24,12 +26,20 @@ impl traits::Handle for Handle {
        Ok(FetchLookup::NotFound)
    }

-
    fn track(&mut self, id: Id) -> Result<bool, Error> {
-
        Ok(self.tracking.insert(id))
+
    fn track_repo(&mut self, id: Id) -> Result<bool, Error> {
+
        Ok(self.tracking_repos.insert(id))
    }

-
    fn untrack(&mut self, id: Id) -> Result<bool, Error> {
-
        Ok(self.tracking.remove(&id))
+
    fn untrack_repo(&mut self, id: Id) -> Result<bool, Error> {
+
        Ok(self.tracking_repos.remove(&id))
+
    }
+

+
    fn track_node(&mut self, id: NodeId, _alias: Option<String>) -> Result<bool, Error> {
+
        Ok(self.tracking_nodes.insert(id))
+
    }
+

+
    fn untrack_node(&mut self, id: NodeId) -> Result<bool, Error> {
+
        Ok(self.tracking_nodes.remove(&id))
    }

    fn announce_refs(&mut self, id: Id) -> Result<(), Error> {
modified radicle-node/src/tests.rs
@@ -353,7 +353,7 @@ fn test_tracking() {
    let proj_id: identity::Id = test::arbitrary::gen(1);

    let (sender, receiver) = chan::bounded(1);
-
    alice.command(Command::Track(proj_id, sender));
+
    alice.command(Command::TrackRepo(proj_id, sender));
    let policy_change = receiver
        .recv()
        .map_err(client::handle::Error::from)
@@ -362,7 +362,7 @@ fn test_tracking() {
    assert!(alice.tracking().is_repo_tracked(&proj_id).unwrap());

    let (sender, receiver) = chan::bounded(1);
-
    alice.command(Command::Untrack(proj_id, sender));
+
    alice.command(Command::UntrackRepo(proj_id, sender));
    let policy_change = receiver
        .recv()
        .map_err(client::handle::Error::from)
@@ -556,9 +556,9 @@ fn test_refs_announcement_relay() {
    };
    let bob_inv = bob.inventory().unwrap();

-
    alice.track(&bob_inv[0], tracking::Scope::All).unwrap();
-
    alice.track(&bob_inv[1], tracking::Scope::All).unwrap();
-
    alice.track(&bob_inv[2], tracking::Scope::All).unwrap();
+
    alice.track_repo(&bob_inv[0], tracking::Scope::All).unwrap();
+
    alice.track_repo(&bob_inv[1], tracking::Scope::All).unwrap();
+
    alice.track_repo(&bob_inv[2], tracking::Scope::All).unwrap();
    alice.connect_to(&bob);
    alice.connect_to(&eve);
    alice.receive(&eve.addr(), Message::Subscribe(Subscribe::all()));
@@ -598,7 +598,7 @@ fn test_refs_announcement_no_subscribe() {
    let eve = Peer::new("eve", [9, 9, 9, 9], MockStorage::empty());
    let id = arbitrary::gen(1);

-
    alice.track(&id, tracking::Scope::All).unwrap();
+
    alice.track_repo(&id, tracking::Scope::All).unwrap();
    alice.connect_to(&bob);
    alice.connect_to(&eve);
    alice.receive(&bob.addr(), bob.refs_announcement(id));
@@ -857,11 +857,11 @@ fn test_push_and_pull() {

    // Bob tracks Alice's project.
    let (sender, _) = chan::bounded(1);
-
    bob.command(service::Command::Track(proj_id, sender));
+
    bob.command(service::Command::TrackRepo(proj_id, sender));

    // Eve tracks Alice's project.
    let (sender, _) = chan::bounded(1);
-
    eve.command(service::Command::Track(proj_id, sender));
+
    eve.command(service::Command::TrackRepo(proj_id, sender));

    let mut sim = Simulation::new(
        LocalTime::now(),
modified radicle/src/node.rs
@@ -1,6 +1,5 @@
mod features;

-
use std::fmt;
use std::io;
use std::io::{BufRead, BufReader, Write};
use std::os::unix::net::UnixStream;
@@ -31,11 +30,15 @@ pub enum Error {
pub trait Handle {
    /// Fetch a project from the network. Fails if the project isn't tracked.
    fn fetch(&self, id: &Id) -> Result<(), Error>;
-
    /// Start tracking the given project. Doesn't do anything if the project is already
-
    /// tracked.
-
    fn track(&self, id: &Id) -> Result<bool, Error>;
-
    /// Untrack the given project and delete it from storage.
-
    fn untrack(&self, id: &Id) -> Result<bool, Error>;
+
    /// Start tracking the given node. If the node is already tracked,
+
    /// updates the alias if necessary.
+
    fn track_node(&self, id: &NodeId, alias: Option<&str>) -> Result<bool, Error>;
+
    /// Start tracking the given repository.
+
    fn track_repo(&self, id: &Id) -> Result<bool, Error>;
+
    /// Untrack the given node.
+
    fn untrack_node(&self, id: &NodeId) -> Result<bool, Error>;
+
    /// Untrack the given repository and delete it from storage.
+
    fn untrack_repo(&self, id: &Id) -> Result<bool, Error>;
    /// Notify the network that we have new refs.
    fn announce_refs(&self, id: &Id) -> Result<(), Error>;
    /// Ask the node to shutdown.
@@ -60,12 +63,17 @@ impl Node {
    }

    /// Call a command on the node.
-
    pub fn call<A: fmt::Display>(
+
    pub fn call<A: ToString>(
        &self,
        cmd: &str,
-
        arg: &A,
+
        args: &[A],
    ) -> Result<impl Iterator<Item = Result<String, io::Error>> + '_, io::Error> {
-
        writeln!(&self.stream, "{cmd} {arg}")?;
+
        let args = args
+
            .iter()
+
            .map(ToString::to_string)
+
            .collect::<Vec<_>>()
+
            .join(" ");
+
        writeln!(&self.stream, "{cmd} {args}")?;

        Ok(BufReader::new(&self.stream).lines())
    }
@@ -73,16 +81,23 @@ impl Node {

impl Handle for Node {
    fn fetch(&self, id: &Id) -> Result<(), Error> {
-
        for line in self.call("fetch", id)? {
+
        for line in self.call("fetch", &[id])? {
            let line = line?;
            log::debug!("node: {}", line);
        }
        Ok(())
    }

-
    fn track(&self, id: &Id) -> Result<bool, Error> {
-
        let mut line = self.call("track", id)?;
-
        let line = line.next().ok_or(Error::EmptyResponse { cmd: "track" })??;
+
    fn track_node(&self, id: &NodeId, alias: Option<&str>) -> Result<bool, Error> {
+
        let id = id.to_human();
+
        let mut line = if let Some(alias) = alias {
+
            self.call("track-node", &[id.as_str(), alias])
+
        } else {
+
            self.call("track-node", &[id.as_str()])
+
        }?;
+
        let line = line
+
            .next()
+
            .ok_or(Error::EmptyResponse { cmd: "track-node" })??;

        log::debug!("node: {}", line);

@@ -90,17 +105,53 @@ impl Handle for Node {
            RESPONSE_OK => Ok(true),
            RESPONSE_NOOP => Ok(false),
            _ => Err(Error::InvalidResponse {
-
                cmd: "track",
+
                cmd: "track-node",
                response: line,
            }),
        }
    }

-
    fn untrack(&self, id: &Id) -> Result<bool, Error> {
-
        let mut line = self.call("untrack", id)?;
+
    fn track_repo(&self, id: &Id) -> Result<bool, Error> {
+
        let mut line = self.call("track-repo", &[id])?;
        let line = line
            .next()
-
            .ok_or(Error::EmptyResponse { cmd: "untrack" })??;
+
            .ok_or(Error::EmptyResponse { cmd: "track-repo" })??;
+

+
        log::debug!("node: {}", line);
+

+
        match line.as_str() {
+
            RESPONSE_OK => Ok(true),
+
            RESPONSE_NOOP => Ok(false),
+
            _ => Err(Error::InvalidResponse {
+
                cmd: "track-repo",
+
                response: line,
+
            }),
+
        }
+
    }
+

+
    fn untrack_node(&self, id: &NodeId) -> Result<bool, Error> {
+
        let mut line = self.call("untrack-node", &[id])?;
+
        let line = line.next().ok_or(Error::EmptyResponse {
+
            cmd: "untrack-node",
+
        })??;
+

+
        log::debug!("node: {}", line);
+

+
        match line.as_str() {
+
            RESPONSE_OK => Ok(true),
+
            RESPONSE_NOOP => Ok(false),
+
            _ => Err(Error::InvalidResponse {
+
                cmd: "untrack-node",
+
                response: line,
+
            }),
+
        }
+
    }
+

+
    fn untrack_repo(&self, id: &Id) -> Result<bool, Error> {
+
        let mut line = self.call("untrack-repo", &[id])?;
+
        let line = line.next().ok_or(Error::EmptyResponse {
+
            cmd: "untrack-repo",
+
        })??;

        log::debug!("node: {}", line);

@@ -108,14 +159,14 @@ impl Handle for Node {
            RESPONSE_OK => Ok(true),
            RESPONSE_NOOP => Ok(false),
            _ => Err(Error::InvalidResponse {
-
                cmd: "untrack",
+
                cmd: "untrack-repo",
                response: line,
            }),
        }
    }

    fn announce_refs(&self, id: &Id) -> Result<(), Error> {
-
        for line in self.call("announce-refs", id)? {
+
        for line in self.call("announce-refs", &[id])? {
            let line = line?;
            log::debug!("node: {}", line);
        }
modified radicle/src/rad.rs
@@ -199,7 +199,7 @@ pub fn clone<P: AsRef<Path>, G: Signer, S: storage::WriteStorage, H: node::Handl
    storage: &S,
    handle: &H,
) -> Result<git2::Repository, CloneError> {
-
    let _ = handle.track(&proj)?;
+
    let _ = handle.track_repo(&proj)?;
    let _ = handle.fetch(&proj)?;
    let _ = fork(proj, signer, storage)?;
    let working = checkout(proj, signer.public_key(), path, storage)?;