cli: unify label and unlabel

Make the CLI easier by unifying the label and unlabel, using `--add`
and `--remove` options instead.

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:56:01 +00:00 committed by cloudhead
parent d32a10b4b9
commit 67cbf15e45
No known key found for this signature in database
5 changed files with 86 additions and 105 deletions

View File

@ -52,7 +52,7 @@ duplicated. While we're at it, let's add a label.
``` ```
$ rad issue assign d185ee1 --add did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi $ rad issue assign d185ee1 --add did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
$ rad issue label d185ee1 -l good-first-issue $ rad issue label d185ee1 --add good-first-issue
``` ```
It will now show in the list of issues assigned to us, along with the new label. It will now show in the list of issues assigned to us, along with the new label.

View File

@ -93,7 +93,7 @@ We can also label patches as well as assign DIDs to the patch to help
organise your workflow: organise your workflow:
``` ```
$ rad patch label 6ff4f09 -l fun $ rad patch label 6ff4f09 --add fun
$ rad patch assign 6ff4f09 --add did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi $ rad patch assign 6ff4f09 --add did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
$ rad patch show 6ff4f09 $ rad patch show 6ff4f09
╭────────────────────────────────────────────────────╮ ╭────────────────────────────────────────────────────╮

View File

@ -4,7 +4,6 @@ use std::str::FromStr;
use anyhow::{anyhow, Context as _}; use anyhow::{anyhow, Context as _};
use nonempty::NonEmpty;
use radicle::cob::common::{Label, Reaction}; use radicle::cob::common::{Label, Reaction};
use radicle::cob::issue; use radicle::cob::issue;
use radicle::cob::issue::{CloseReason, Issues, State}; use radicle::cob::issue::{CloseReason, Issues, State};
@ -40,7 +39,7 @@ Usage
rad issue open [--title <title>] [--description <text>] [--label <label>] [<option>...] rad issue open [--title <title>] [--description <text>] [--label <label>] [<option>...]
rad issue react <issue-id> [--emoji <char>] [--to <comment>] [<option>...] rad issue react <issue-id> [--emoji <char>] [--to <comment>] [<option>...]
rad issue assign <issue-id> [--add <did>] [--remove <did>] [<option>...] rad issue assign <issue-id> [--add <did>] [--remove <did>] [<option>...]
rad issue label <issue-id> --label <label> [<option>...] rad issue label <issue-id> [--add <label>] [--remove <did>] [<option>...]
rad issue unlabel <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 comment <issue-id> [--message <message>] [--reply-to <comment-id>] [<option>...]
rad issue show <issue-id> [<option>...] rad issue show <issue-id> [<option>...]
@ -48,26 +47,26 @@ Usage
Assign options Assign options
-a, --add <did> Add an assignee to the issue (may be specified multiple times). -a, --add <did> Add an assignee to the issue (may be specified multiple times).
Note: --add will take precedence over --remove Note: --add will take precedence over --remove
-r, --remove <did> Remove an assignee from the issue (may be specified multiple times). -r, --remove <did> Remove an assignee from the issue (may be specified multiple times).
Note: --add will take precedence over --remove Note: --add will take precedence over --remove
Label options Label options
-l, --label Label the issue with the provided label (may be specified multiple times) -a, --add <label> Add a label to the issue (may be specified multiple times).
Note: --add will take precedence over --remove
Unlabel options -r, --remove <label> Remove a label from the issue (may be specified multiple times).
Note: --add will take precedence over --remove
-l, --label Remove the provided label from the issue (may be specified multiple times)
Options Options
--no-announce Don't announce issue to peers --no-announce Don't announce issue to peers
--header Show only the issue header, hiding the comments --header Show only the issue header, hiding the comments
--quiet, -q Don't print anything --quiet, -q Don't print anything
--help Print help --help Print help
"#, "#,
}; };
@ -84,7 +83,6 @@ pub enum OperationName {
React, React,
Show, Show,
State, State,
Unlabel,
} }
/// Command line Peer argument. /// Command line Peer argument.
@ -135,11 +133,7 @@ pub enum Operation {
}, },
Label { Label {
id: Rev, id: Rev,
labels: NonEmpty<Label>, opts: LabelOptions,
},
Unlabel {
id: Rev,
labels: NonEmpty<Label>,
}, },
List { List {
assigned: Option<Assigned>, assigned: Option<Assigned>,
@ -153,6 +147,12 @@ pub struct AssignOptions {
pub remove: BTreeSet<Did>, pub remove: BTreeSet<Did>,
} }
#[derive(Debug, Default, PartialEq, Eq)]
pub struct LabelOptions {
pub add: BTreeSet<Label>,
pub remove: BTreeSet<Label>,
}
#[derive(Debug)] #[derive(Debug)]
pub struct Options { pub struct Options {
pub op: Operation, pub op: Operation,
@ -181,6 +181,7 @@ impl Args for Options {
let mut announce = true; let mut announce = true;
let mut quiet = false; let mut quiet = false;
let mut assign_opts = AssignOptions::default(); let mut assign_opts = AssignOptions::default();
let mut label_opts = LabelOptions::default();
while let Some(arg) = parser.next()? { while let Some(arg) = parser.next()? {
match arg { match arg {
@ -206,12 +207,7 @@ impl Args for Options {
Long("title") if op == Some(OperationName::Open) => { Long("title") if op == Some(OperationName::Open) => {
title = Some(parser.value()?.to_string_lossy().into()); title = Some(parser.value()?.to_string_lossy().into());
} }
Short('l') | Long("label") Short('l') | Long("label") if matches!(op, Some(OperationName::Open)) => {
if matches!(
op,
Some(OperationName::Open | OperationName::Label | OperationName::Unlabel)
) =>
{
let val = parser.value()?; let val = parser.value()?;
let name = term::args::string(&val); let name = term::args::string(&val);
let label = Label::new(name)?; let label = Label::new(name)?;
@ -294,6 +290,22 @@ impl Args for Options {
.insert(term::args::did(&parser.value()?)?); .insert(term::args::did(&parser.value()?)?);
} }
// Label options
Short('a') | Long("add") if matches!(op, Some(OperationName::Label)) => {
let val = parser.value()?;
let name = term::args::string(&val);
let label = Label::new(name)?;
label_opts.add.insert(label);
}
Short('r') | Long("remove") if matches!(op, Some(OperationName::Label)) => {
let val = parser.value()?;
let name = term::args::string(&val);
let label = Label::new(name)?;
label_opts.remove.insert(label);
}
Long("quiet") | Short('q') => { Long("quiet") | Short('q') => {
quiet = true; quiet = true;
} }
@ -308,7 +320,6 @@ impl Args for Options {
"s" | "state" => op = Some(OperationName::State), "s" | "state" => op = Some(OperationName::State),
"assign" => op = Some(OperationName::Assign), "assign" => op = Some(OperationName::Assign),
"label" => op = Some(OperationName::Label), "label" => op = Some(OperationName::Label),
"unlabel" => op = Some(OperationName::Unlabel),
unknown => anyhow::bail!("unknown operation '{}'", unknown), unknown => anyhow::bail!("unknown operation '{}'", unknown),
}, },
@ -361,13 +372,7 @@ impl Args for Options {
}, },
OperationName::Label => Operation::Label { OperationName::Label => Operation::Label {
id: id.ok_or_else(|| anyhow!("an issue to label must be provided"))?, id: id.ok_or_else(|| anyhow!("an issue to label must be provided"))?,
labels: NonEmpty::from_vec(labels) opts: label_opts,
.ok_or_else(|| anyhow!("at least one label must be specified"))?,
},
OperationName::Unlabel => Operation::Unlabel {
id: id.ok_or_else(|| anyhow!("an issue to label must be provided"))?,
labels: NonEmpty::from_vec(labels)
.ok_or_else(|| anyhow!("at least one label must be specified"))?,
}, },
OperationName::List => Operation::List { assigned, state }, OperationName::List => Operation::List { assigned, state },
}; };
@ -397,7 +402,6 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
| Operation::Delete { .. } | Operation::Delete { .. }
| Operation::Assign { .. } | Operation::Assign { .. }
| Operation::Label { .. } | Operation::Label { .. }
| Operation::Unlabel { .. }
); );
let mut node = Node::new(profile.socket()); let mut node = Node::new(profile.socket());
@ -501,22 +505,18 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
issue.assign(assignees, &signer)?; issue.assign(assignees, &signer)?;
} }
Operation::Label { id, labels } => { Operation::Label {
let id = id.resolve(&repo.backend)?; id,
let Ok(mut issue) = issues.get_mut(&id) else { opts: LabelOptions { add, remove },
anyhow::bail!("Issue `{id}` not found"); } => {
};
let labels = issue.labels().cloned().chain(labels).collect::<Vec<_>>();
issue.label(labels, &signer)?;
}
Operation::Unlabel { id, labels } => {
let id = id.resolve(&repo.backend)?; let id = id.resolve(&repo.backend)?;
let Ok(mut issue) = issues.get_mut(&id) else { let Ok(mut issue) = issues.get_mut(&id) else {
anyhow::bail!("Issue `{id}` not found"); anyhow::bail!("Issue `{id}` not found");
}; };
let labels = issue let labels = issue
.labels() .labels()
.filter(|&l| !labels.contains(l)) .filter(|did| !remove.contains(did))
.chain(add.iter())
.cloned() .cloned()
.collect::<Vec<_>>(); .collect::<Vec<_>>();
issue.label(labels, &signer)?; issue.label(labels, &signer)?;

View File

@ -30,7 +30,6 @@ use std::ffi::OsString;
use anyhow::anyhow; use anyhow::anyhow;
use nonempty::NonEmpty;
use radicle::cob::patch::PatchId; use radicle::cob::patch::PatchId;
use radicle::cob::{patch, Label}; use radicle::cob::{patch, Label};
use radicle::prelude::*; use radicle::prelude::*;
@ -57,8 +56,7 @@ Usage
rad patch delete <patch-id> [<option>...] rad patch delete <patch-id> [<option>...]
rad patch redact <revision-id> [<option>...] rad patch redact <revision-id> [<option>...]
rad patch assign <revision-id> [--add <did>] [--remove <did>] [<option>...] rad patch assign <revision-id> [--add <did>] [--remove <did>] [<option>...]
rad patch label <revision-id> --label <label> [<option>...] rad patch label <revision-id> [--add <label>] [--remove <label>] [<option>...]
rad patch unlabel <revision-id> --label <label> [<option>...]
rad patch ready <patch-id> [--undo] [<option>...] rad patch ready <patch-id> [--undo] [<option>...]
rad patch edit <patch-id> [<option>...] rad patch edit <patch-id> [<option>...]
rad patch set <patch-id> [<option>...] rad patch set <patch-id> [<option>...]
@ -79,19 +77,19 @@ Edit options
Assign options Assign options
-a, --add <did> Add an assignee to the issue (may be specified multiple times). -a, --add <did> Add an assignee to the issue (may be specified multiple times).
Note: --add will take precedence over --remove Note: --add will take precedence over --remove
-r, --remove <did> Remove an assignee from the issue (may be specified multiple times). -r, --remove <did> Remove an assignee from the issue (may be specified multiple times).
Note: --add will take precedence over --remove Note: --add will take precedence over --remove
Label options Label options
-l, --label Label the patch with the provided label (may be specified multiple times) -a, --add <label> Add an assignee to the issue (may be specified multiple times).
Note: --add will take precedence over --remove
Unlabel options -r, --remove <label> Remove an assignee from the issue (may be specified multiple times).
Note: --add will take precedence over --remove
-l, --label Remove the provided label from the patch (may be specified multiple times)
Update options Update options
@ -137,7 +135,6 @@ pub enum OperationName {
Comment, Comment,
Ready, Ready,
Label, Label,
Unlabel,
#[default] #[default]
List, List,
Edit, Edit,
@ -151,6 +148,12 @@ pub struct AssignOptions {
pub remove: BTreeSet<Did>, pub remove: BTreeSet<Did>,
} }
#[derive(Debug, Default, PartialEq, Eq)]
pub struct LabelOptions {
pub add: BTreeSet<Label>,
pub remove: BTreeSet<Label>,
}
pub struct Filter(fn(&patch::State) -> bool); pub struct Filter(fn(&patch::State) -> bool);
impl Filter { impl Filter {
@ -208,11 +211,7 @@ pub enum Operation {
}, },
Label { Label {
patch_id: Rev, patch_id: Rev,
labels: NonEmpty<Label>, opts: LabelOptions,
},
Unlabel {
patch_id: Rev,
labels: NonEmpty<Label>,
}, },
List { List {
filter: Filter, filter: Filter,
@ -260,8 +259,8 @@ impl Args for Options {
let mut undo = false; let mut undo = false;
let mut reply_to: Option<Rev> = None; let mut reply_to: Option<Rev> = None;
let mut checkout_opts = checkout::Options::default(); let mut checkout_opts = checkout::Options::default();
let mut labels = Vec::new();
let mut assign_opts = AssignOptions::default(); let mut assign_opts = AssignOptions::default();
let mut label_opts = LabelOptions::default();
while let Some(arg) = parser.next()? { while let Some(arg) = parser.next()? {
match arg { match arg {
@ -343,15 +342,21 @@ impl Args for Options {
.insert(term::args::did(&parser.value()?)?); .insert(term::args::did(&parser.value()?)?);
} }
// Un/Label options. // Label options.
Short('l') | Long("label") Short('a') | Long("add") if matches!(op, Some(OperationName::Label)) => {
if matches!(op, Some(OperationName::Label | OperationName::Unlabel)) =>
{
let val = parser.value()?; let val = parser.value()?;
let name = term::args::string(&val); let name = term::args::string(&val);
let label = Label::new(name)?; let label = Label::new(name)?;
labels.push(label); label_opts.add.insert(label);
}
Short('r') | Long("remove") if matches!(op, Some(OperationName::Label)) => {
let val = parser.value()?;
let name = term::args::string(&val);
let label = Label::new(name)?;
label_opts.remove.insert(label);
} }
// List options. // List options.
@ -403,7 +408,6 @@ impl Args for Options {
"r" | "redact" => op = Some(OperationName::Redact), "r" | "redact" => op = Some(OperationName::Redact),
"assign" => op = Some(OperationName::Assign), "assign" => op = Some(OperationName::Assign),
"label" => op = Some(OperationName::Label), "label" => op = Some(OperationName::Label),
"unlabel" => op = Some(OperationName::Unlabel),
"comment" => op = Some(OperationName::Comment), "comment" => op = Some(OperationName::Comment),
"set" => op = Some(OperationName::Set), "set" => op = Some(OperationName::Set),
unknown => anyhow::bail!("unknown operation '{}'", unknown), unknown => anyhow::bail!("unknown operation '{}'", unknown),
@ -426,7 +430,6 @@ impl Args for Options {
Some(OperationName::Set), Some(OperationName::Set),
Some(OperationName::Assign), Some(OperationName::Assign),
Some(OperationName::Label), Some(OperationName::Label),
Some(OperationName::Unlabel),
] ]
.contains(&op) => .contains(&op) =>
{ {
@ -481,13 +484,7 @@ impl Args for Options {
}, },
OperationName::Label => Operation::Label { OperationName::Label => Operation::Label {
patch_id: patch_id.ok_or_else(|| anyhow!("a patch must be provided"))?, patch_id: patch_id.ok_or_else(|| anyhow!("a patch must be provided"))?,
labels: NonEmpty::from_vec(labels) opts: label_opts,
.ok_or_else(|| anyhow!("at least one label must be specified"))?,
},
OperationName::Unlabel => Operation::Unlabel {
patch_id: patch_id.ok_or_else(|| anyhow!("a patch must be provided"))?,
labels: NonEmpty::from_vec(labels)
.ok_or_else(|| anyhow!("at least one label must be specified"))?,
}, },
OperationName::Set => Operation::Set { OperationName::Set => Operation::Set {
patch_id: patch_id.ok_or_else(|| anyhow!("a patch must be provided"))?, patch_id: patch_id.ok_or_else(|| anyhow!("a patch must be provided"))?,
@ -602,13 +599,12 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
let patch_id = patch_id.resolve(&repository.backend)?; let patch_id = patch_id.resolve(&repository.backend)?;
assign::run(&patch_id, add, remove, &profile, &repository)?; assign::run(&patch_id, add, remove, &profile, &repository)?;
} }
Operation::Label { patch_id, labels } => { Operation::Label {
patch_id,
opts: LabelOptions { add, remove },
} => {
let patch_id = patch_id.resolve(&repository.backend)?; let patch_id = patch_id.resolve(&repository.backend)?;
label::add(&patch_id, labels, &profile, &repository)?; label::run(&patch_id, add, remove, &profile, &repository)?;
}
Operation::Unlabel { patch_id, labels } => {
let patch_id = patch_id.resolve(&repository.backend)?;
label::remove(&patch_id, labels, &profile, &repository)?;
} }
Operation::Set { patch_id } => { Operation::Set { patch_id } => {
let patches = radicle::cob::patch::Patches::open(&repository)?; let patches = radicle::cob::patch::Patches::open(&repository)?;

View File

@ -1,29 +1,13 @@
use super::*; use super::*;
use nonempty::NonEmpty;
use radicle::storage::git::Repository; use radicle::storage::git::Repository;
use crate::terminal as term; use crate::terminal as term;
pub fn add( pub fn run(
patch_id: &PatchId, patch_id: &PatchId,
labels: NonEmpty<Label>, add: BTreeSet<Label>,
profile: &Profile, remove: BTreeSet<Label>,
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 labels = patch.labels().cloned().chain(labels).collect::<Vec<_>>();
patch.label(labels, &signer)?;
Ok(())
}
pub fn remove(
patch_id: &PatchId,
labels: NonEmpty<Label>,
profile: &Profile, profile: &Profile,
repository: &Repository, repository: &Repository,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
@ -34,7 +18,8 @@ pub fn remove(
}; };
let labels = patch let labels = patch
.labels() .labels()
.filter(|&l| !labels.contains(l)) .filter(|l| !remove.contains(l))
.chain(add.iter())
.cloned() .cloned()
.collect::<Vec<_>>(); .collect::<Vec<_>>();
patch.label(labels, &signer)?; patch.label(labels, &signer)?;