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

View File

@ -93,7 +93,7 @@ We can also label patches as well as assign DIDs to the patch to help
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 show 6ff4f09
╭────────────────────────────────────────────────────╮

View File

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

View File

@ -30,7 +30,6 @@ use std::ffi::OsString;
use anyhow::anyhow;
use nonempty::NonEmpty;
use radicle::cob::patch::PatchId;
use radicle::cob::{patch, Label};
use radicle::prelude::*;
@ -57,8 +56,7 @@ Usage
rad patch delete <patch-id> [<option>...]
rad patch redact <revision-id> [<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 label <revision-id> [--add <label>] [--remove <label>] [<option>...]
rad patch ready <patch-id> [--undo] [<option>...]
rad patch edit <patch-id> [<option>...]
rad patch set <patch-id> [<option>...]
@ -87,11 +85,11 @@ Assign 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
-l, --label Remove the provided label from the patch (may be specified multiple times)
-r, --remove <label> Remove an assignee from the issue (may be specified multiple times).
Note: --add will take precedence over --remove
Update options
@ -137,7 +135,6 @@ pub enum OperationName {
Comment,
Ready,
Label,
Unlabel,
#[default]
List,
Edit,
@ -151,6 +148,12 @@ pub struct AssignOptions {
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);
impl Filter {
@ -208,11 +211,7 @@ pub enum Operation {
},
Label {
patch_id: Rev,
labels: NonEmpty<Label>,
},
Unlabel {
patch_id: Rev,
labels: NonEmpty<Label>,
opts: LabelOptions,
},
List {
filter: Filter,
@ -260,8 +259,8 @@ impl Args for Options {
let mut undo = false;
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();
let mut label_opts = LabelOptions::default();
while let Some(arg) = parser.next()? {
match arg {
@ -343,15 +342,21 @@ impl Args for Options {
.insert(term::args::did(&parser.value()?)?);
}
// Un/Label options.
Short('l') | Long("label")
if matches!(op, Some(OperationName::Label | OperationName::Unlabel)) =>
{
// 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)?;
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.
@ -403,7 +408,6 @@ impl Args for Options {
"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),
"set" => op = Some(OperationName::Set),
unknown => anyhow::bail!("unknown operation '{}'", unknown),
@ -426,7 +430,6 @@ impl Args for Options {
Some(OperationName::Set),
Some(OperationName::Assign),
Some(OperationName::Label),
Some(OperationName::Unlabel),
]
.contains(&op) =>
{
@ -481,13 +484,7 @@ impl Args for Options {
},
OperationName::Label => Operation::Label {
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::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"))?,
opts: label_opts,
},
OperationName::Set => Operation::Set {
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)?;
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)?;
label::add(&patch_id, labels, &profile, &repository)?;
}
Operation::Unlabel { patch_id, labels } => {
let patch_id = patch_id.resolve(&repository.backend)?;
label::remove(&patch_id, labels, &profile, &repository)?;
label::run(&patch_id, add, remove, &profile, &repository)?;
}
Operation::Set { patch_id } => {
let patches = radicle::cob::patch::Patches::open(&repository)?;

View File

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