Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
cli: Implement automatic issue announcement
Alexis Sellier committed 3 years ago
commit 85df6136820827bf2679c23477e7563e292a11dd
parent cb3b7a3765680a1a940cca530443526816849493
6 files changed +96 -9
modified radicle-cli/examples/rad-issue.md
@@ -4,7 +4,7 @@ using the 'issue' subcommand.
Let's say the new car you are designing with your peers has a problem with its flux capacitor.

```
-
$ rad issue open --title "flux capacitor underpowered" --description "Flux capacitor power requirements exceed current supply"
+
$ rad issue open --title "flux capacitor underpowered" --description "Flux capacitor power requirements exceed current supply" --no-announce
```

The issue is now listed under our project.
modified radicle-cli/src/commands/issue.rs
@@ -3,16 +3,17 @@ use std::ffi::OsString;
use std::str::FromStr;

use anyhow::{anyhow, Context as _};
+
use radicle::node::Handle;
use radicle::prelude::Did;

use crate::terminal as term;
use crate::terminal::args::{Args, Error, Help};

-
use radicle::cob;
use radicle::cob::common::{Reaction, Tag};
use radicle::cob::issue;
use radicle::cob::issue::{CloseReason, IssueId, Issues, State};
use radicle::storage::WriteStorage;
+
use radicle::{cob, Node};

pub const HELP: Help = Help {
    name: "issue",
@@ -31,7 +32,8 @@ Usage

Options

-
    --help      Print help
+
    --no-announce     Don't announce issue to peers
+
    --help            Print help
"#,
};

@@ -89,6 +91,7 @@ pub enum Operation {
#[derive(Debug)]
pub struct Options {
    pub op: Operation,
+
    pub announce: bool,
}

impl Args for Options {
@@ -103,6 +106,7 @@ impl Args for Options {
        let mut reaction: Option<Reaction> = None;
        let mut description: Option<String> = None;
        let mut state: Option<State> = None;
+
        let mut announce = true;

        while let Some(arg) = parser.next()? {
            match arg {
@@ -142,6 +146,9 @@ impl Args for Options {
                        assigned = Some(Assigned::Me);
                    }
                }
+
                Long("no-announce") => {
+
                    announce = false;
+
                }
                Value(val) if op.is_none() => match val.to_string_lossy().as_ref() {
                    "c" | "show" => op = Some(OperationName::Show),
                    "d" | "delete" => op = Some(OperationName::Delete),
@@ -187,16 +194,25 @@ impl Args for Options {
            OperationName::List => Operation::List { assigned },
        };

-
        Ok((Options { op }, vec![]))
+
        Ok((Options { op, announce }, vec![]))
    }
}

pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
    let profile = ctx.profile()?;
    let signer = term::signer(&profile)?;
-
    let storage = &profile.storage;
-
    let (_, id) = radicle::rad::cwd()?;
-
    let repo = storage.repository_mut(id)?;
+
    let (_, rid) = radicle::rad::cwd()?;
+
    let repo = profile.storage.repository_mut(rid)?;
+
    let announce = options.announce
+
        && matches!(
+
            &options.op,
+
            Operation::Open { .. }
+
                | Operation::React { .. }
+
                | Operation::State { .. }
+
                | Operation::Delete { .. }
+
        );
+

+
    let mut node = Node::new(profile.socket());
    let mut issues = Issues::open(&repo)?;

    match options.op {
@@ -306,6 +322,16 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
        }
    }

+
    if announce {
+
        match node.announce_refs(rid) {
+
            Ok(()) => {}
+
            Err(e) if e.is_connection_err() => {
+
                term::warning("Could not announce issue refs: node is not running");
+
            }
+
            Err(e) => return Err(e.into()),
+
        }
+
    }
+

    Ok(())
}

modified radicle-cli/tests/commands.rs
@@ -346,6 +346,56 @@ fn test_clone_without_seeds() {
}

#[test]
+
fn test_cob_replication() {
+
    logger::init(log::Level::Debug);
+

+
    let mut environment = Environment::new();
+
    let working = tempfile::tempdir().unwrap();
+
    let mut alice = environment.node("alice");
+
    let bob = environment.node("bob");
+

+
    let rid = alice.project("heartwood", "");
+

+
    let mut alice = alice.spawn(Config::default());
+
    let mut bob = bob.spawn(Config::default());
+

+
    alice.handle.track_node(bob.id, None).unwrap();
+
    alice.connect(&bob);
+

+
    bob.routes_to(&[(rid, alice.id)]);
+
    bob.rad("clone", &[rid.to_string().as_str()], working.path())
+
        .unwrap();
+

+
    let bob_repo = bob.storage.repository(rid).unwrap();
+
    let mut bob_issues = radicle::cob::issue::Issues::open(&bob_repo).unwrap();
+
    let issue = bob_issues
+
        .create(
+
            "Something's fishy",
+
            "I don't know what it is",
+
            &[],
+
            &[],
+
            &bob.signer,
+
        )
+
        .unwrap();
+
    log::debug!(target: "test", "Issue {} created", issue.id());
+

+
    // Make sure that Bob's issue refs announcement has a different timestamp than his fork's
+
    // announcement, otherwise Alice will consider it stale.
+
    thread::sleep(time::Duration::from_secs(1));
+

+
    bob.handle.announce_refs(rid).unwrap();
+

+
    // Wait for Alice to fetch the issue refs.
+
    thread::sleep(time::Duration::from_secs(1));
+

+
    let alice_repo = alice.storage.repository(rid).unwrap();
+
    let alice_issues = radicle::cob::issue::Issues::open(&alice_repo).unwrap();
+
    let alice_issue = alice_issues.get(issue.id()).unwrap().unwrap();
+

+
    assert_eq!(alice_issue.title(), "Something's fishy");
+
}
+

+
#[test]
//
//     alice -- seed -- bob
//
modified radicle-node/src/service.rs
@@ -883,7 +883,7 @@ where
                    // Discard announcement messages we've already seen, otherwise update
                    // our last seen time.
                    if !peer.refs_announced(message.rid, timestamp) {
-
                        debug!(target: "service", "Ignoring stale refs announcement from {announcer}");
+
                        debug!(target: "service", "Ignoring stale refs announcement from {announcer} (time={timestamp})");
                        return Ok(false);
                    }

modified radicle-node/src/service/message.rs
@@ -255,7 +255,11 @@ impl fmt::Debug for AnnouncementMessage {
                )
            }
            Self::Refs(message) => {
-
                write!(f, "Refs({}, {:?})", message.rid, message.refs)
+
                write!(
+
                    f,
+
                    "Refs({}, {}, {:?})",
+
                    message.rid, message.timestamp, message.refs
+
                )
            }
        }
    }
modified radicle/src/node.rs
@@ -347,6 +347,13 @@ pub enum Error {
    EmptyResponse { cmd: CommandName },
}

+
impl Error {
+
    /// Check if the error is due to the not being able to connect to the local node.
+
    pub fn is_connection_err(&self) -> bool {
+
        matches!(self, Self::Connect(_))
+
    }
+
}
+

/// Error returned by [`Node::call`] iterator.
#[derive(thiserror::Error, Debug)]
pub enum CallError {