diff --git a/Cargo.lock b/Cargo.lock index fbfdfb9e..eff00b6a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1825,6 +1825,7 @@ dependencies = [ "json-color", "lexopt", "log", + "nonempty 0.8.1", "pretty_assertions", "radicle", "radicle-cli-test", diff --git a/radicle-cli/Cargo.toml b/radicle-cli/Cargo.toml index a0002d4b..a95ada96 100644 --- a/radicle-cli/Cargo.toml +++ b/radicle-cli/Cargo.toml @@ -16,6 +16,7 @@ chrono = { version = "0.4", default-features = false, features = ["clock", "std" json-color = { version = "0.7" } lexopt = { version = "0.2" } log = { version = "0.4", features = ["std"] } +nonempty = { version = "0.8" } serde = { version = "1.0" } serde_json = { version = "1" } serde_yaml = { version = "0.8" } diff --git a/radicle-cli/examples/rad-issue.md b/radicle-cli/examples/rad-issue.md index 38226cd1..191f9399 100644 --- a/radicle-cli/examples/rad-issue.md +++ b/radicle-cli/examples/rad-issue.md @@ -28,7 +28,7 @@ others to work on. This is to ensure work is not duplicated. Let's assign ourselves to this one. ``` -$ rad assign 2e8c1bf3fe0532a314778357c886608a966a34bd did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi +$ rad assign 2e8c1bf3fe0532a314778357c886608a966a34bd --to did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi ``` It will now show in the list of issues assigned to us. @@ -41,7 +41,7 @@ $ rad issue list --assigned Note: this can always be undone with the `unassign` subcommand. ``` -$ rad unassign 2e8c1bf3fe0532a314778357c886608a966a34bd did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi +$ rad unassign 2e8c1bf3fe0532a314778357c886608a966a34bd --from did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi ``` Great, now we have communicated to the world about our car's defect. diff --git a/radicle-cli/src/commands/assign.rs b/radicle-cli/src/commands/assign.rs index b9239de5..5e1f09f6 100644 --- a/radicle-cli/src/commands/assign.rs +++ b/radicle-cli/src/commands/assign.rs @@ -2,61 +2,65 @@ use std::ffi::OsString; use std::str::FromStr; use anyhow::anyhow; +use nonempty::NonEmpty; use radicle::prelude::Did; use crate::terminal as term; -use crate::terminal::args; +use crate::terminal::args::{Args, Error, Help}; use radicle::cob; use radicle::cob::issue; use radicle::storage::WriteStorage; -pub const HELP: args::Help = args::Help { +pub const HELP: Help = Help { name: "assign", - description: "assign an issue", + description: "Assign an issue", version: env!("CARGO_PKG_VERSION"), usage: r#" Usage - rad assign + rad assign --to + + To assign multiple users to an issue, you may repeat + the `--to` option. Options - --help Print help + --to Assignee to add to the issue + --help Print help "#, }; #[derive(Debug)] pub struct Options { pub id: issue::IssueId, - pub peer: Did, + pub to: NonEmpty, } -impl args::Args for Options { +impl Args for Options { fn from_args(args: Vec) -> anyhow::Result<(Self, Vec)> { use lexopt::prelude::*; let mut parser = lexopt::Parser::from_args(args); let mut id: Option = None; - let mut peer: Option = None; + let mut to: Vec = Vec::new(); while let Some(arg) = parser.next()? { match arg { Long("help") => { - return Err(args::Error::Help.into()); + return Err(Error::Help.into()); } - Value(ref val) => { - if id.is_none() { - let val = val.to_string_lossy(); - let Ok(val) = issue::IssueId::from_str(&val) else { - return Err(anyhow!("invalid issue ID '{}'", val)); - }; + Long("to") => { + let val = parser.value()?; + let did = term::args::did(&val)?; - id = Some(val); - } else if peer.is_none() { - peer = Some(term::args::did(val)?); - } else { - return Err(anyhow!(arg.unexpected())); - } + to.push(did); + } + Value(ref val) if id.is_none() => { + let val = val.to_string_lossy(); + let Ok(val) = issue::IssueId::from_str(&val) else { + return Err(anyhow!("invalid Issue ID '{}'", val)); + }; + id = Some(val); } _ => { return Err(anyhow!(arg.unexpected())); @@ -66,8 +70,9 @@ impl args::Args for Options { Ok(( Options { - id: id.unwrap(), - peer: peer.unwrap(), + id: id.ok_or_else(|| anyhow!("an issue must be specified"))?, + to: NonEmpty::from_vec(to) + .ok_or_else(|| anyhow!("an assignee must be specified"))?, }, vec![], )) @@ -76,17 +81,16 @@ impl args::Args for Options { 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 repo = profile.storage.repository_mut(id)?; let mut issues = issue::Issues::open(&repo)?; - - let mut issue = issues.get_mut(&options.id).map_err(|err| match err { - cob::store::Error::NotFound(_, _) => anyhow!("issue not found '{}'", options.id), - _ => err.into(), + let mut issue = issues.get_mut(&options.id).map_err(|e| match e { + cob::store::Error::NotFound(_, _) => anyhow!("issue {} not found", options.id), + _ => e.into(), })?; - issue.assign(vec![*options.peer], &signer)?; + let signer = term::signer(&profile)?; + + issue.assign(options.to.into_iter().map(Did::into), &signer)?; Ok(()) } diff --git a/radicle-cli/src/commands/unassign.rs b/radicle-cli/src/commands/unassign.rs index 0173e393..f545c418 100644 --- a/radicle-cli/src/commands/unassign.rs +++ b/radicle-cli/src/commands/unassign.rs @@ -2,6 +2,7 @@ use std::ffi::OsString; use std::str::FromStr; use anyhow::anyhow; +use nonempty::NonEmpty; use radicle::prelude::Did; use crate::terminal as term; @@ -12,23 +13,27 @@ use radicle::storage::WriteStorage; pub const HELP: Help = Help { name: "unassign", - description: "unassign an issue", + description: "Unassign an issue", version: env!("CARGO_PKG_VERSION"), usage: r#" Usage - rad unassign + rad unassign --from + + To unassign multiple users from an issue, you may repeat + the `--from` option. Options - --help Print help + --from Assignee to remove from the issue + --help Print help "#, }; #[derive(Debug)] pub struct Options { pub id: issue::IssueId, - pub peer: Did, + pub from: NonEmpty, } impl Args for Options { @@ -37,26 +42,25 @@ impl Args for Options { let mut parser = lexopt::Parser::from_args(args); let mut id: Option = None; - let mut peer: Option = None; + let mut from: Vec = Vec::new(); while let Some(arg) = parser.next()? { match arg { Long("help") => { return Err(Error::Help.into()); } - Value(ref val) => { - if id.is_none() { - let val = val.to_string_lossy(); - let Ok(val) = issue::IssueId::from_str(&val) else { - return Err(anyhow!("invalid issue ID '{}'", val)); - }; + Long("from") => { + let val = parser.value()?; + let did = term::args::did(&val)?; - id = Some(val); - } else if peer.is_none() { - peer = Some(term::args::did(val)?); - } else { - return Err(anyhow!(arg.unexpected())); - } + from.push(did); + } + Value(ref val) if id.is_none() => { + let val = val.to_string_lossy(); + let Ok(val) = issue::IssueId::from_str(&val) else { + return Err(anyhow!("invalid Issue ID '{}'", val)); + }; + id = Some(val); } _ => { return Err(anyhow!(arg.unexpected())); @@ -66,8 +70,9 @@ impl Args for Options { Ok(( Options { - id: id.unwrap(), - peer: peer.unwrap(), + id: id.ok_or_else(|| anyhow!("an issue must be specified"))?, + from: NonEmpty::from_vec(from) + .ok_or_else(|| anyhow!("an assignee must be specified"))?, }, vec![], )) @@ -76,17 +81,16 @@ impl Args for Options { 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 repo = profile.storage.repository_mut(id)?; let mut issues = issue::Issues::open(&repo)?; - - let mut issue = issues.get_mut(&options.id).map_err(|err| match err { - cob::store::Error::NotFound(_, _) => anyhow!("issue '{}' not found", options.id), - _ => err.into(), + let mut issue = issues.get_mut(&options.id).map_err(|e| match e { + cob::store::Error::NotFound(_, _) => anyhow!("issue {} not found", options.id), + _ => e.into(), })?; - issue.unassign(vec![*options.peer], &signer)?; + let signer = term::signer(&profile)?; + + issue.unassign(options.from.into_iter().map(Did::into), &signer)?; Ok(()) }