cli: move rad assign/unassign under rad issue and rad patch

It is more natural for the assign and unassign commands to exist as
subcommands of the object they are assigning to, i.e. issues and
patches.

Remove the rad assign and rad unassign commands, moving their
functionality to the rad issue and rad patch commands. The
rad-issue.md and rad-patch.md tests are modified to include a assign
test.

Signed-off-by: Fintan Halpenny <fintan.halpenny@gmail.com>
X-Clacks-Overhead: GNU Terry Pratchett
This commit is contained in:
Fintan Halpenny 2023-11-30 17:16:35 +00:00 committed by cloudhead
parent dce5316cbb
commit d32a10b4b9
No known key found for this signature in database
10 changed files with 157 additions and 235 deletions

View File

@ -51,7 +51,7 @@ Let's assign ourselves to this one, this is to ensure work is not
duplicated. While we're at it, let's add a label.
```
$ rad assign d185ee16a00bac874c0bcbc2a8ad80fdce5e1e6 --to did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
$ rad issue assign d185ee1 --add did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
$ rad issue label d185ee1 -l good-first-issue
```
@ -69,7 +69,7 @@ $ rad issue list --assigned
Note: this can always be undone with the `unassign` subcommand.
```
$ rad unassign d185ee16a00bac874c0bcbc2a8ad80fdce5e1e61 --from did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
$ rad issue assign d185ee1 --remove did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
```
Great, now we have communicated to the world about our car's defect.

View File

@ -89,10 +89,13 @@ $ git branch -vv
master f2de534 [rad/master] Second commit
```
We also want to label the patch after we've created it:
We can also label patches as well as assign DIDs to the patch to help
organise your workflow:
```
$ rad patch label 6ff4f09c1b5a81347981f59b02ef43a31a07cdae -l fun
$ rad patch show 6ff4f09c1b5a81347981f59b02ef43a31a07cdae
$ rad patch label 6ff4f09 -l fun
$ rad patch assign 6ff4f09 --add did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
$ rad patch show 6ff4f09
╭────────────────────────────────────────────────────╮
│ Title Define power requirements │
│ Patch 6ff4f09c1b5a81347981f59b02ef43a31a07cdae │
@ -123,7 +126,7 @@ $ git commit --message "Add README, just for the fun"
```
``` (stderr)
$ git push rad -o patch.message="Add README, just for the fun"
✓ Patch 6ff4f09 updated to 873e637a66be511c45f4ef7b04fddc9def8f072c
✓ Patch 6ff4f09 updated to e0fd9f00b51e10e1ca88868e68e46e859ed371d7
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
3e674d1..27857ec flux-capacitor-power -> patches/6ff4f09c1b5a81347981f59b02ef43a31a07cdae
```
@ -131,13 +134,13 @@ To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkE
And let's leave a quick comment for our team:
```
$ rad patch comment 6ff4f09c1b5a81347981f59b02ef43a31a07cdae --message 'I cannot wait to get back to the 90s!'
$ rad patch comment 6ff4f09 --message 'I cannot wait to get back to the 90s!'
╭───────────────────────────────────────╮
│ z6MknSL…StBU8Vi (you) now efaf6fb │
│ z6MknSL…StBU8Vi (you) now f5b4613
│ I cannot wait to get back to the 90s! │
╰───────────────────────────────────────╯
$ rad patch comment 6ff4f09c1b5a81347981f59b02ef43a31a07cdae --message 'My favorite decade!' --reply-to efaf6fb -q
2cb22a1c87af86c25368c7be9fc385720fd6086f
$ rad patch comment 6ff4f09 --message 'My favorite decade!' --reply-to f5b4613 -q
611df66ccb3803b604a59f2efa9a42d72256dd49
```
Now, let's checkout the patch that we just created:
@ -151,7 +154,7 @@ $ rad patch checkout 6ff4f09
We can also add a review verdict as such:
```
$ rad review 6ff4f09c1b5a81347981f59b02ef43a31a07cdae --accept --no-message --no-sync
$ rad review 6ff4f09 --accept --no-message --no-sync
✓ Patch 6ff4f09 accepted
```
@ -175,7 +178,7 @@ $ rad patch show 6ff4f09
│ 3e674d1 Define power requirements │
├─────────────────────────────────────────────────────────────────────┤
│ ● opened by z6MknSL…StBU8Vi (you) now │
│ ↑ updated to 873e637a66be511c45f4ef7b04fddc9def8f072c (27857ec) now │
│ ↑ updated to e0fd9f00b51e10e1ca88868e68e46e859ed371d7 (27857ec) now │
│ ✓ accepted by z6MknSL…StBU8Vi (you) now │
╰─────────────────────────────────────────────────────────────────────╯
```
@ -201,7 +204,7 @@ $ rad patch show 6ff4f09
│ 3e674d1 Define power requirements │
├─────────────────────────────────────────────────────────────────────┤
│ ● opened by z6MknSL…StBU8Vi (you) now │
│ ↑ updated to 873e637a66be511c45f4ef7b04fddc9def8f072c (27857ec) now │
│ ↑ updated to e0fd9f00b51e10e1ca88868e68e46e859ed371d7 (27857ec) now │
│ ✓ accepted by z6MknSL…StBU8Vi (you) now │
╰─────────────────────────────────────────────────────────────────────╯
```

View File

@ -1,5 +1,3 @@
#[path = "commands/assign.rs"]
pub mod rad_assign;
#[path = "commands/auth.rs"]
pub mod rad_auth;
#[path = "commands/checkout.rs"]
@ -46,7 +44,5 @@ pub mod rad_self;
pub mod rad_sync;
#[path = "commands/track.rs"]
pub mod rad_track;
#[path = "commands/unassign.rs"]
pub mod rad_unassign;
#[path = "commands/untrack.rs"]
pub mod rad_untrack;

View File

@ -1,95 +0,0 @@
use std::ffi::OsString;
use anyhow::anyhow;
use nonempty::NonEmpty;
use radicle::cob;
use radicle::cob::issue;
use radicle::prelude::Did;
use radicle::storage::WriteStorage;
use crate::git::Rev;
use crate::terminal as term;
use crate::terminal::args::{string, Args, Error, Help};
pub const HELP: Help = Help {
name: "assign",
description: "Assign an issue",
version: env!("CARGO_PKG_VERSION"),
usage: r#"
Usage
rad assign <issue-id> --to <did> [<option>...]
To assign multiple users to an issue, you may repeat
the `--to` option.
--to <did> Assignee to add to the issue
Options
--help Print help
"#,
};
#[derive(Debug)]
pub struct Options {
pub id: Rev,
pub to: NonEmpty<Did>,
}
impl Args for Options {
fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
use lexopt::prelude::*;
let mut parser = lexopt::Parser::from_args(args);
let mut id: Option<Rev> = None;
let mut to: Vec<Did> = Vec::new();
while let Some(arg) = parser.next()? {
match arg {
Long("help") | Short('h') => {
return Err(Error::Help.into());
}
Long("to") => {
let val = parser.value()?;
let did = term::args::did(&val)?;
to.push(did);
}
Value(ref val) if id.is_none() => {
let val = string(val);
id = Some(Rev::from(val));
}
_ => {
return Err(anyhow!(arg.unexpected()));
}
}
}
Ok((
Options {
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![],
))
}
}
pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
let profile = ctx.profile()?;
let (_, id) = radicle::rad::cwd()?;
let repo = profile.storage.repository_mut(id)?;
let oid = options.id.resolve(&repo.backend)?;
let mut issues = issue::Issues::open(&repo)?;
let mut issue = issues.get_mut(&oid).map_err(|e| match e {
cob::store::Error::NotFound(_, _) => anyhow!("issue {} not found", options.id),
_ => e.into(),
})?;
let signer = term::signer(&profile)?;
issue.assign(options.to.into_iter().map(Did::into), &signer)?;
Ok(())
}

View File

@ -13,7 +13,6 @@ pub const HELP: Help = Help {
};
const COMMANDS: &[Help] = &[
rad_assign::HELP,
rad_auth::HELP,
rad_checkout::HELP,
rad_clone::HELP,
@ -31,7 +30,6 @@ const COMMANDS: &[Help] = &[
rad_clean::HELP,
rad_self::HELP,
rad_track::HELP,
rad_unassign::HELP,
rad_untrack::HELP,
rad_remote::HELP,
rad_sync::HELP,

View File

@ -1,4 +1,4 @@
#![allow(clippy::or_fun_call)]
use std::collections::BTreeSet;
use std::ffi::OsString;
use std::str::FromStr;
@ -39,31 +39,41 @@ Usage
rad issue list [--assigned <did>] [--all | --closed | --open | --solved] [<option>...]
rad issue open [--title <title>] [--description <text>] [--label <label>] [<option>...]
rad issue react <issue-id> [--emoji <char>] [--to <comment>] [<option>...]
rad issue assign <issue-id> [--add <did>] [--remove <did>] [<option>...]
rad issue label <issue-id> --label <label> [<option>...]
rad issue unlabel <issue-id> --label <label> [<option>...]
rad issue comment <issue-id> [--message <message>] [--reply-to <comment-id>] [<option>...]
rad issue show <issue-id> [<option>...]
rad issue state <issue-id> [--closed | --open | --solved] [<option>...]
Assign options
-a, --add <did> Add an assignee to the issue (may be specified multiple times).
Note: --add will take precedence over --remove
-r, --remove <did> Remove an assignee from the issue (may be specified multiple times).
Note: --add will take precedence over --remove
Label options
-l, --label Label the issue with the provided label (may be specified multiple times)
-l, --label Label the issue with the provided label (may be specified multiple times)
Unlabel options
-l, --label Remove the provided label from the issue (may be specified multiple times)
-l, --label Remove the provided label from the issue (may be specified multiple times)
Options
--no-announce Don't announce issue to peers
--header Show only the issue header, hiding the comments
--quiet, -q Don't print anything
--help Print help
--no-announce Don't announce issue to peers
--header Show only the issue header, hiding the comments
--quiet, -q Don't print anything
--help Print help
"#,
};
#[derive(Default, Debug, PartialEq, Eq)]
pub enum OperationName {
Assign,
Edit,
Open,
Comment,
@ -119,6 +129,10 @@ pub enum Operation {
reaction: Reaction,
comment_id: Option<thread::CommentId>,
},
Assign {
id: Rev,
opts: AssignOptions,
},
Label {
id: Rev,
labels: NonEmpty<Label>,
@ -133,6 +147,12 @@ pub enum Operation {
},
}
#[derive(Debug, Default, PartialEq, Eq)]
pub struct AssignOptions {
pub add: BTreeSet<Did>,
pub remove: BTreeSet<Did>,
}
#[derive(Debug)]
pub struct Options {
pub op: Operation,
@ -160,6 +180,7 @@ impl Args for Options {
let mut reply_to = None;
let mut announce = true;
let mut quiet = false;
let mut assign_opts = AssignOptions::default();
while let Some(arg) = parser.next()? {
match arg {
@ -262,6 +283,17 @@ impl Args for Options {
Long("no-announce") => {
announce = false;
}
// Assign options
Short('a') | Long("add") if op == Some(OperationName::Assign) => {
assign_opts.add.insert(term::args::did(&parser.value()?)?);
}
Short('r') | Long("remove") if op == Some(OperationName::Assign) => {
assign_opts
.remove
.insert(term::args::did(&parser.value()?)?);
}
Long("quiet") | Short('q') => {
quiet = true;
}
@ -274,6 +306,7 @@ impl Args for Options {
"o" | "open" => op = Some(OperationName::Open),
"r" | "react" => op = Some(OperationName::React),
"s" | "state" => op = Some(OperationName::State),
"assign" => op = Some(OperationName::Assign),
"label" => op = Some(OperationName::Label),
"unlabel" => op = Some(OperationName::Unlabel),
@ -322,6 +355,10 @@ impl Args for Options {
OperationName::Delete => Operation::Delete {
id: id.ok_or_else(|| anyhow!("an issue to remove must be provided"))?,
},
OperationName::Assign => Operation::Assign {
id: id.ok_or_else(|| anyhow!("an issue to label must be provided"))?,
opts: assign_opts,
},
OperationName::Label => Operation::Label {
id: id.ok_or_else(|| anyhow!("an issue to label must be provided"))?,
labels: NonEmpty::from_vec(labels)
@ -358,6 +395,9 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
| Operation::React { .. }
| Operation::State { .. }
| Operation::Delete { .. }
| Operation::Assign { .. }
| Operation::Label { .. }
| Operation::Unlabel { .. }
);
let mut node = Node::new(profile.socket());
@ -445,6 +485,22 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
&profile,
)?;
}
Operation::Assign {
id,
opts: AssignOptions { add, remove },
} => {
let id = id.resolve(&repo.backend)?;
let Ok(mut issue) = issues.get_mut(&id) else {
anyhow::bail!("Issue `{id}` not found");
};
let assignees = issue
.assignees()
.filter(|did| !remove.contains(did))
.chain(add.iter())
.cloned()
.collect::<Vec<_>>();
issue.assign(assignees, &signer)?;
}
Operation::Label { id, labels } => {
let id = id.resolve(&repo.backend)?;
let Ok(mut issue) = issues.get_mut(&id) else {

View File

@ -1,5 +1,7 @@
#[path = "patch/archive.rs"]
mod archive;
#[path = "patch/assign.rs"]
mod assign;
#[path = "patch/checkout.rs"]
mod checkout;
#[path = "patch/comment.rs"]
@ -54,8 +56,9 @@ Usage
rad patch checkout <patch-id> [<option>...]
rad patch delete <patch-id> [<option>...]
rad patch redact <revision-id> [<option>...]
rad patch label <patch-id> --label <label> [<option>...]
rad patch unlabel <patch-id> --label <label> [<option>...]
rad patch assign <revision-id> [--add <did>] [--remove <did>] [<option>...]
rad patch label <revision-id> --label <label> [<option>...]
rad patch unlabel <revision-id> --label <label> [<option>...]
rad patch ready <patch-id> [--undo] [<option>...]
rad patch edit <patch-id> [<option>...]
rad patch set <patch-id> [<option>...]
@ -74,6 +77,14 @@ Edit options
-m, --message [<string>] Provide a comment message to the patch or revision (default: prompt)
Assign options
-a, --add <did> Add an assignee to the issue (may be specified multiple times).
Note: --add will take precedence over --remove
-r, --remove <did> Remove an assignee from the issue (may be specified multiple times).
Note: --add will take precedence over --remove
Label options
-l, --label Label the patch with the provided label (may be specified multiple times)
@ -117,6 +128,7 @@ Other options
#[derive(Debug, Default, PartialEq, Eq)]
pub enum OperationName {
Assign,
Show,
Update,
Archive,
@ -133,6 +145,12 @@ pub enum OperationName {
Set,
}
#[derive(Debug, Default, PartialEq, Eq)]
pub struct AssignOptions {
pub add: BTreeSet<Did>,
pub remove: BTreeSet<Did>,
}
pub struct Filter(fn(&patch::State) -> bool);
impl Filter {
@ -184,6 +202,10 @@ pub enum Operation {
message: Message,
reply_to: Option<Rev>,
},
Assign {
patch_id: Rev,
opts: AssignOptions,
},
Label {
patch_id: Rev,
labels: NonEmpty<Label>,
@ -239,6 +261,7 @@ impl Args for Options {
let mut reply_to: Option<Rev> = None;
let mut checkout_opts = checkout::Options::default();
let mut labels = Vec::new();
let mut assign_opts = AssignOptions::default();
while let Some(arg) = parser.next()? {
match arg {
@ -309,6 +332,17 @@ impl Args for Options {
checkout_opts.name = Some(term::args::refstring("name", val)?);
}
// Assign options.
Short('a') | Long("add") if matches!(op, Some(OperationName::Assign)) => {
assign_opts.add.insert(term::args::did(&parser.value()?)?);
}
Short('r') | Long("remove") if matches!(op, Some(OperationName::Assign)) => {
assign_opts
.remove
.insert(term::args::did(&parser.value()?)?);
}
// Un/Label options.
Short('l') | Long("label")
if matches!(op, Some(OperationName::Label | OperationName::Unlabel)) =>
@ -367,6 +401,7 @@ impl Args for Options {
"y" | "ready" => op = Some(OperationName::Ready),
"e" | "edit" => op = Some(OperationName::Edit),
"r" | "redact" => op = Some(OperationName::Redact),
"assign" => op = Some(OperationName::Assign),
"label" => op = Some(OperationName::Label),
"unlabel" => op = Some(OperationName::Unlabel),
"comment" => op = Some(OperationName::Comment),
@ -389,6 +424,7 @@ impl Args for Options {
Some(OperationName::Comment),
Some(OperationName::Edit),
Some(OperationName::Set),
Some(OperationName::Assign),
Some(OperationName::Label),
Some(OperationName::Unlabel),
]
@ -439,6 +475,10 @@ impl Args for Options {
OperationName::Redact => Operation::Redact {
revision_id: revision_id.ok_or_else(|| anyhow!("a revision must be provided"))?,
},
OperationName::Assign => Operation::Assign {
patch_id: patch_id.ok_or_else(|| anyhow!("a patch must be provided"))?,
opts: assign_opts,
},
OperationName::Label => Operation::Label {
patch_id: patch_id.ok_or_else(|| anyhow!("a patch must be provided"))?,
labels: NonEmpty::from_vec(labels)
@ -555,6 +595,13 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
Operation::Redact { revision_id } => {
redact::run(&revision_id, &profile, &repository)?;
}
Operation::Assign {
patch_id,
opts: AssignOptions { add, remove },
} => {
let patch_id = patch_id.resolve(&repository.backend)?;
assign::run(&patch_id, add, remove, &profile, &repository)?;
}
Operation::Label { patch_id, labels } => {
let patch_id = patch_id.resolve(&repository.backend)?;
label::add(&patch_id, labels, &profile, &repository)?;

View File

@ -0,0 +1,29 @@
use std::collections::BTreeSet;
use super::*;
use radicle::prelude::Did;
use radicle::storage::git::Repository;
use crate::terminal as term;
pub fn run(
patch_id: &PatchId,
add: BTreeSet<Did>,
remove: BTreeSet<Did>,
profile: &Profile,
repository: &Repository,
) -> anyhow::Result<()> {
let signer = term::signer(profile)?;
let mut patches = radicle::cob::patch::Patches::open(repository)?;
let Ok(mut patch) = patches.get_mut(patch_id) else {
anyhow::bail!("Patch `{patch_id}` not found");
};
let assignees = patch
.assignees()
.filter(|did| !remove.contains(did))
.chain(add)
.collect::<BTreeSet<_>>();
patch.assign(assignees, &signer)?;
Ok(())
}

View File

@ -1,98 +0,0 @@
use std::ffi::OsString;
use anyhow::anyhow;
use nonempty::NonEmpty;
use radicle::cob;
use radicle::cob::issue;
use radicle::prelude::Did;
use radicle::storage::WriteStorage;
use crate::terminal as term;
use crate::terminal::args::{Args, Error, Help};
pub const HELP: Help = Help {
name: "unassign",
description: "Unassign an issue",
version: env!("CARGO_PKG_VERSION"),
usage: r#"
Usage
rad unassign <issue-id> --from <did> [<option>...]
To unassign multiple users from an issue, you may repeat
the `--from` option.
--from <did> Assignee to remove from the issue
Options
--help Print help
"#,
};
#[derive(Debug)]
pub struct Options {
pub id: issue::IssueId,
pub from: NonEmpty<Did>,
}
impl Args for Options {
fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
use lexopt::prelude::*;
let mut parser = lexopt::Parser::from_args(args);
let mut id: Option<issue::IssueId> = None;
let mut from: Vec<Did> = Vec::new();
while let Some(arg) = parser.next()? {
match arg {
Long("help") | Short('h') => {
return Err(Error::Help.into());
}
Long("from") => {
let val = parser.value()?;
let did = term::args::did(&val)?;
from.push(did);
}
Value(ref val) if id.is_none() => {
id = Some(term::args::issue(val)?);
}
_ => {
return Err(anyhow!(arg.unexpected()));
}
}
}
Ok((
Options {
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![],
))
}
}
pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
let profile = ctx.profile()?;
let (_, id) = radicle::rad::cwd()?;
let repo = profile.storage.repository_mut(id)?;
let mut issues = issue::Issues::open(&repo)?;
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(),
})?;
let signer = term::signer(&profile)?;
let assigned = issue
.assignees()
.cloned()
.filter(|did| !options.from.contains(did))
.collect::<Vec<_>>();
issue.assign(assigned, &signer)?;
Ok(())
}

View File

@ -97,13 +97,6 @@ fn run(command: Command) -> Result<(), Option<anyhow::Error>> {
fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option<anyhow::Error>> {
match exe {
"assign" => {
term::run_command_args::<rad_assign::Options, _>(
rad_assign::HELP,
rad_assign::run,
args.to_vec(),
);
}
"auth" => {
term::run_command_args::<rad_auth::Options, _>(
rad_auth::HELP,
@ -250,13 +243,6 @@ fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option<anyhow::Error>>
args.to_vec(),
);
}
"unassign" => {
term::run_command_args::<rad_unassign::Options, _>(
rad_unassign::HELP,
rad_unassign::run,
args.to_vec(),
);
}
"untrack" => {
term::run_command_args::<rad_untrack::Options, _>(
rad_untrack::HELP,