From 85df6136820827bf2679c23477e7563e292a11dd Mon Sep 17 00:00:00 2001 From: Alexis Sellier Date: Mon, 6 Mar 2023 15:37:52 +0100 Subject: [PATCH] cli: Implement automatic issue announcement --- radicle-cli/examples/rad-issue.md | 2 +- radicle-cli/src/commands/issue.rs | 38 ++++++++++++++++++---- radicle-cli/tests/commands.rs | 50 +++++++++++++++++++++++++++++ radicle-node/src/service.rs | 2 +- radicle-node/src/service/message.rs | 6 +++- radicle/src/node.rs | 7 ++++ 6 files changed, 96 insertions(+), 9 deletions(-) diff --git a/radicle-cli/examples/rad-issue.md b/radicle-cli/examples/rad-issue.md index 47d5555d..206b1dfb 100644 --- a/radicle-cli/examples/rad-issue.md +++ b/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. diff --git a/radicle-cli/src/commands/issue.rs b/radicle-cli/src/commands/issue.rs index ca41a6ab..5f44739b 100644 --- a/radicle-cli/src/commands/issue.rs +++ b/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 = None; let mut description: Option = None; let mut state: Option = 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(()) } diff --git a/radicle-cli/tests/commands.rs b/radicle-cli/tests/commands.rs index 230b8995..b3d2ae6b 100644 --- a/radicle-cli/tests/commands.rs +++ b/radicle-cli/tests/commands.rs @@ -345,6 +345,56 @@ fn test_clone_without_seeds() { .unwrap(); } +#[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 diff --git a/radicle-node/src/service.rs b/radicle-node/src/service.rs index 6172abad..45a30fde 100644 --- a/radicle-node/src/service.rs +++ b/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); } diff --git a/radicle-node/src/service/message.rs b/radicle-node/src/service/message.rs index 129a0494..9a4d9468 100644 --- a/radicle-node/src/service/message.rs +++ b/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 + ) } } } diff --git a/radicle/src/node.rs b/radicle/src/node.rs index 1a01af1c..9efcfb2f 100644 --- a/radicle/src/node.rs +++ b/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 {