cli: Implement automatic issue announcement

This commit is contained in:
Alexis Sellier 2023-03-06 15:37:52 +01:00
parent cb3b7a3765
commit 85df613682
No known key found for this signature in database
6 changed files with 96 additions and 9 deletions

View File

@ -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.

View File

@ -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(())
}

View File

@ -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

View File

@ -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);
}

View File

@ -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
)
}
}
}

View File

@ -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 {