cli: Move label commands to `issue` and `patch`
It is more natural for the label and unlabel commands to exist as subcommands of the object they are labelling, i.e. issues and patches. Remove the `rad label` and `rad unlabel` 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 label test. The `rad patch show` command did not support showing labels as part of its output. This was also included to confirm the test above was working. Signed-off-by: Fintan Halpenny <fintan.halpenny@gmail.com> X-Clacks-Overhead: GNU Terry Pratchett
This commit is contained in:
parent
511165bfc5
commit
1af9480b08
|
|
@ -24,6 +24,8 @@ rad-patch - Manage radicle patches.
|
|||
*rad patch* _edit_ <patch-id> [<option>...] +
|
||||
*rad patch* _set_ <patch-id> [<option>...] +
|
||||
*rad patch* _comment_ <revision-id> [<option>...] +
|
||||
*rad patch* _label_ <patch-id> --label <label> [<option>...] +
|
||||
*rad patch* _unlabel_ <patch-id> --label <label> [<option>...] +
|
||||
|
||||
== Description
|
||||
|
||||
|
|
|
|||
|
|
@ -43,24 +43,27 @@ $ rad issue show d185ee1
|
|||
|
||||
Great! Now we've documented the issue for ourselves and others.
|
||||
|
||||
Just like with other project management systems, the issue can be assigned to
|
||||
others to work on. This is to ensure work is not duplicated.
|
||||
Just like with other project management systems, the issue can be
|
||||
labeled and assigned to others to work on. This is to ensure work is
|
||||
not duplicated.
|
||||
|
||||
Let's assign ourselves to this one.
|
||||
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 d185ee16a00bac874c0bcbc2a8ad80fdce5e1e61 --to did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
$ rad assign d185ee16a00bac874c0bcbc2a8ad80fdce5e1e6 --to did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
$ rad issue label d185ee1 -l good-first-issue
|
||||
```
|
||||
|
||||
It will now show in the list of issues assigned to us.
|
||||
It will now show in the list of issues assigned to us, along with the new label.
|
||||
|
||||
```
|
||||
$ rad issue list --assigned
|
||||
╭─────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ● ID Title Author Labels Assignees Opened │
|
||||
├─────────────────────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ● d185ee1 flux capacitor underpowered z6MknSL…StBU8Vi (you) z6MknSL…StBU8Vi now │
|
||||
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ● ID Title Author Labels Assignees Opened │
|
||||
├───────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ● d185ee1 flux capacitor underpowered z6MknSL…StBU8Vi (you) good-first-issue z6MknSL…StBU8Vi now │
|
||||
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
```
|
||||
|
||||
Note: this can always be undone with the `unassign` subcommand.
|
||||
|
|
@ -76,9 +79,9 @@ It will help whoever works on a fix.
|
|||
|
||||
```
|
||||
$ rad issue comment d185ee16a00bac874c0bcbc2a8ad80fdce5e1e61 --message 'The flux capacitor needs 1.21 Gigawatts' -q
|
||||
14019611935fd1c66458111a5b49f0a7350c226f
|
||||
$ rad issue comment d185ee16a00bac874c0bcbc2a8ad80fdce5e1e61 --reply-to 14019611935fd1c66458111a5b49f0a7350c226f --message 'More power!' -q
|
||||
e342e47b7dd93451d47c806a0faeb0b5e957da2c
|
||||
80ef590710edb64dfa57e8e940d6e4d0b0ae4217
|
||||
$ rad issue comment d185ee16a00bac874c0bcbc2a8ad80fdce5e1e61 --reply-to 80ef590710edb64dfa57e8e940d6e4d0b0ae4217 --message 'More power!' -q
|
||||
91009820ca0996d93b9afd5739a4d2158a2ec898
|
||||
```
|
||||
|
||||
We can see our comments by showing the issue:
|
||||
|
|
@ -89,14 +92,15 @@ $ rad issue show d185ee16a00bac874c0bcbc2a8ad80fdce5e1e61
|
|||
│ Title flux capacitor underpowered │
|
||||
│ Issue d185ee16a00bac874c0bcbc2a8ad80fdce5e1e61 │
|
||||
│ Author z6MknSL…StBU8Vi (you) │
|
||||
│ Labels good-first-issue │
|
||||
│ Status open │
|
||||
│ │
|
||||
│ Flux capacitor power requirements exceed current supply │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ z6MknSL…StBU8Vi (you) now 1401961 │
|
||||
│ z6MknSL…StBU8Vi (you) now 80ef590 │
|
||||
│ The flux capacitor needs 1.21 Gigawatts │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ z6MknSL…StBU8Vi (you) now e342e47 │
|
||||
│ z6MknSL…StBU8Vi (you) now 9100982 │
|
||||
│ More power! │
|
||||
╰─────────────────────────────────────────────────────────╯
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,42 +0,0 @@
|
|||
Labeling an issue is easy, let's add the `bug` and `good-first-issue` labels to
|
||||
some issue:
|
||||
|
||||
```
|
||||
$ rad label d185ee16a00bac874c0bcbc2a8ad80fdce5e1e61 bug good-first-issue
|
||||
```
|
||||
|
||||
We can now show the issue to check whether those labels were added:
|
||||
|
||||
```
|
||||
$ rad issue show d185ee16a00bac874c0bcbc2a8ad80fdce5e1e61 --format header
|
||||
╭─────────────────────────────────────────────────────────╮
|
||||
│ Title flux capacitor underpowered │
|
||||
│ Issue d185ee16a00bac874c0bcbc2a8ad80fdce5e1e61 │
|
||||
│ Author z6MknSL…StBU8Vi (you) │
|
||||
│ Labels bug, good-first-issue │
|
||||
│ Status open │
|
||||
│ │
|
||||
│ Flux capacitor power requirements exceed current supply │
|
||||
╰─────────────────────────────────────────────────────────╯
|
||||
```
|
||||
|
||||
Untagging an issue is very similar:
|
||||
|
||||
```
|
||||
$ rad unlabel d185ee16a00bac874c0bcbc2a8ad80fdce5e1e61 good-first-issue
|
||||
```
|
||||
|
||||
Notice that the `good-first-issue` label has disappeared:
|
||||
|
||||
```
|
||||
$ rad issue show d185ee16a00bac874c0bcbc2a8ad80fdce5e1e61 --format header
|
||||
╭─────────────────────────────────────────────────────────╮
|
||||
│ Title flux capacitor underpowered │
|
||||
│ Issue d185ee16a00bac874c0bcbc2a8ad80fdce5e1e61 │
|
||||
│ Author z6MknSL…StBU8Vi (you) │
|
||||
│ Labels bug │
|
||||
│ Status open │
|
||||
│ │
|
||||
│ Flux capacitor power requirements exceed current supply │
|
||||
╰─────────────────────────────────────────────────────────╯
|
||||
```
|
||||
|
|
@ -89,6 +89,28 @@ $ git branch -vv
|
|||
master f2de534 [rad/master] Second commit
|
||||
```
|
||||
|
||||
We also want to label the patch after we've created it:
|
||||
```
|
||||
$ rad patch label 6ff4f09c1b5a81347981f59b02ef43a31a07cdae -l fun
|
||||
$ rad patch show 6ff4f09c1b5a81347981f59b02ef43a31a07cdae
|
||||
╭────────────────────────────────────────────────────╮
|
||||
│ Title Define power requirements │
|
||||
│ Patch 6ff4f09c1b5a81347981f59b02ef43a31a07cdae │
|
||||
│ Author z6MknSL…StBU8Vi (you) │
|
||||
│ Labels fun │
|
||||
│ Head 3e674d1a1df90807e934f9ae5da2591dd6848a33 │
|
||||
│ Branches flux-capacitor-power │
|
||||
│ Commits ahead 1, behind 0 │
|
||||
│ Status open │
|
||||
│ │
|
||||
│ See details. │
|
||||
├────────────────────────────────────────────────────┤
|
||||
│ 3e674d1 Define power requirements │
|
||||
├────────────────────────────────────────────────────┤
|
||||
│ ● opened by z6MknSL…StBU8Vi (you) now │
|
||||
╰────────────────────────────────────────────────────╯
|
||||
```
|
||||
|
||||
Wait, let's add a README too! Just for fun.
|
||||
|
||||
```
|
||||
|
|
@ -101,7 +123,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 0c0942e2ff2488617d950ede15567ca39a29972e
|
||||
✓ Patch 6ff4f09 updated to 873e637a66be511c45f4ef7b04fddc9def8f072c
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
3e674d1..27857ec flux-capacitor-power -> patches/6ff4f09c1b5a81347981f59b02ef43a31a07cdae
|
||||
```
|
||||
|
|
@ -111,11 +133,11 @@ And let's leave a quick comment for our team:
|
|||
```
|
||||
$ rad patch comment 6ff4f09c1b5a81347981f59b02ef43a31a07cdae --message 'I cannot wait to get back to the 90s!'
|
||||
╭───────────────────────────────────────╮
|
||||
│ z6MknSL…StBU8Vi (you) now cd811db │
|
||||
│ z6MknSL…StBU8Vi (you) now efaf6fb │
|
||||
│ I cannot wait to get back to the 90s! │
|
||||
╰───────────────────────────────────────╯
|
||||
$ rad patch comment 6ff4f09c1b5a81347981f59b02ef43a31a07cdae --message 'My favorite decade!' --reply-to cd811db -q
|
||||
b6a76fe394de87eb34cbc3823a0edc80ff98cb97
|
||||
$ rad patch comment 6ff4f09c1b5a81347981f59b02ef43a31a07cdae --message 'My favorite decade!' --reply-to efaf6fb -q
|
||||
2cb22a1c87af86c25368c7be9fc385720fd6086f
|
||||
```
|
||||
|
||||
Now, let's checkout the patch that we just created:
|
||||
|
|
@ -141,6 +163,7 @@ $ rad patch show 6ff4f09
|
|||
│ Title Define power requirements │
|
||||
│ Patch 6ff4f09c1b5a81347981f59b02ef43a31a07cdae │
|
||||
│ Author z6MknSL…StBU8Vi (you) │
|
||||
│ Labels fun │
|
||||
│ Head 27857ec9eb04c69cacab516e8bf4b5fd36090f66 │
|
||||
│ Branches flux-capacitor-power, patch/6ff4f09 │
|
||||
│ Commits ahead 2, behind 0 │
|
||||
|
|
@ -152,7 +175,7 @@ $ rad patch show 6ff4f09
|
|||
│ 3e674d1 Define power requirements │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ ● opened by z6MknSL…StBU8Vi (you) now │
|
||||
│ ↑ updated to 0c0942e2ff2488617d950ede15567ca39a29972e (27857ec) now │
|
||||
│ ↑ updated to 873e637a66be511c45f4ef7b04fddc9def8f072c (27857ec) now │
|
||||
│ ✓ accepted by z6MknSL…StBU8Vi (you) now │
|
||||
╰─────────────────────────────────────────────────────────────────────╯
|
||||
```
|
||||
|
|
@ -166,6 +189,7 @@ $ rad patch show 6ff4f09
|
|||
│ Title Define power requirements │
|
||||
│ Patch 6ff4f09c1b5a81347981f59b02ef43a31a07cdae │
|
||||
│ Author z6MknSL…StBU8Vi (you) │
|
||||
│ Labels fun │
|
||||
│ Head 27857ec9eb04c69cacab516e8bf4b5fd36090f66 │
|
||||
│ Branches flux-capacitor-power, patch/6ff4f09 │
|
||||
│ Commits ahead 2, behind 0 │
|
||||
|
|
@ -177,7 +201,7 @@ $ rad patch show 6ff4f09
|
|||
│ 3e674d1 Define power requirements │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ ● opened by z6MknSL…StBU8Vi (you) now │
|
||||
│ ↑ updated to 0c0942e2ff2488617d950ede15567ca39a29972e (27857ec) now │
|
||||
│ ↑ updated to 873e637a66be511c45f4ef7b04fddc9def8f072c (27857ec) now │
|
||||
│ ✓ accepted by z6MknSL…StBU8Vi (you) now │
|
||||
╰─────────────────────────────────────────────────────────────────────╯
|
||||
```
|
||||
|
|
|
|||
|
|
@ -26,8 +26,6 @@ pub mod rad_init;
|
|||
pub mod rad_inspect;
|
||||
#[path = "commands/issue.rs"]
|
||||
pub mod rad_issue;
|
||||
#[path = "commands/label.rs"]
|
||||
pub mod rad_label;
|
||||
#[path = "commands/ls.rs"]
|
||||
pub mod rad_ls;
|
||||
#[path = "commands/node.rs"]
|
||||
|
|
@ -50,7 +48,5 @@ pub mod rad_sync;
|
|||
pub mod rad_track;
|
||||
#[path = "commands/unassign.rs"]
|
||||
pub mod rad_unassign;
|
||||
#[path = "commands/unlabel.rs"]
|
||||
pub mod rad_unlabel;
|
||||
#[path = "commands/untrack.rs"]
|
||||
pub mod rad_untrack;
|
||||
|
|
|
|||
|
|
@ -30,10 +30,8 @@ const COMMANDS: &[Help] = &[
|
|||
rad_review::HELP,
|
||||
rad_clean::HELP,
|
||||
rad_self::HELP,
|
||||
rad_label::HELP,
|
||||
rad_track::HELP,
|
||||
rad_unassign::HELP,
|
||||
rad_unlabel::HELP,
|
||||
rad_untrack::HELP,
|
||||
rad_remote::HELP,
|
||||
rad_sync::HELP,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ 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};
|
||||
|
|
@ -38,10 +39,20 @@ 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 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>...]
|
||||
|
||||
Label options
|
||||
|
||||
-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)
|
||||
|
||||
Options
|
||||
|
||||
--no-announce Don't announce issue to peers
|
||||
|
|
@ -57,11 +68,13 @@ pub enum OperationName {
|
|||
Open,
|
||||
Comment,
|
||||
Delete,
|
||||
Label,
|
||||
#[default]
|
||||
List,
|
||||
React,
|
||||
Show,
|
||||
State,
|
||||
Unlabel,
|
||||
}
|
||||
|
||||
/// Command line Peer argument.
|
||||
|
|
@ -106,6 +119,14 @@ pub enum Operation {
|
|||
reaction: Reaction,
|
||||
comment_id: Option<thread::CommentId>,
|
||||
},
|
||||
Label {
|
||||
id: Rev,
|
||||
labels: NonEmpty<Label>,
|
||||
},
|
||||
Unlabel {
|
||||
id: Rev,
|
||||
labels: NonEmpty<Label>,
|
||||
},
|
||||
List {
|
||||
assigned: Option<Assigned>,
|
||||
state: Option<State>,
|
||||
|
|
@ -164,7 +185,12 @@ impl Args for Options {
|
|||
Long("title") if op == Some(OperationName::Open) => {
|
||||
title = Some(parser.value()?.to_string_lossy().into());
|
||||
}
|
||||
Long("label") if op == Some(OperationName::Open) => {
|
||||
Short('l') | Long("label")
|
||||
if matches!(
|
||||
op,
|
||||
Some(OperationName::Open | OperationName::Label | OperationName::Unlabel)
|
||||
) =>
|
||||
{
|
||||
let val = parser.value()?;
|
||||
let name = term::args::string(&val);
|
||||
let label = Label::new(name)?;
|
||||
|
|
@ -248,6 +274,8 @@ impl Args for Options {
|
|||
"o" | "open" => op = Some(OperationName::Open),
|
||||
"r" | "react" => op = Some(OperationName::React),
|
||||
"s" | "state" => op = Some(OperationName::State),
|
||||
"label" => op = Some(OperationName::Label),
|
||||
"unlabel" => op = Some(OperationName::Unlabel),
|
||||
|
||||
unknown => anyhow::bail!("unknown operation '{}'", unknown),
|
||||
},
|
||||
|
|
@ -294,6 +322,16 @@ impl Args for Options {
|
|||
OperationName::Delete => Operation::Delete {
|
||||
id: id.ok_or_else(|| anyhow!("an issue to remove must be provided"))?,
|
||||
},
|
||||
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"))?,
|
||||
},
|
||||
OperationName::List => Operation::List { assigned, state },
|
||||
};
|
||||
|
||||
|
|
@ -407,6 +445,26 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
|||
&profile,
|
||||
)?;
|
||||
}
|
||||
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 } => {
|
||||
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))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
issue.label(labels, &signer)?;
|
||||
}
|
||||
Operation::List { assigned, state } => {
|
||||
list(&issues, &assigned, &state, &profile)?;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,129 +0,0 @@
|
|||
use std::ffi::OsString;
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use nonempty::NonEmpty;
|
||||
|
||||
use radicle::cob;
|
||||
use radicle::cob::common::Label;
|
||||
use radicle::cob::{issue, patch, store};
|
||||
use radicle::crypto::Signer;
|
||||
use radicle::storage::{self, WriteStorage};
|
||||
|
||||
use crate::terminal as term;
|
||||
use crate::terminal::args::{Args, Error, Help};
|
||||
|
||||
pub const HELP: Help = Help {
|
||||
name: "label",
|
||||
description: "Label an issue or patch",
|
||||
version: env!("CARGO_PKG_VERSION"),
|
||||
usage: r#"
|
||||
Usage
|
||||
|
||||
rad label <issue-id> <label>... [<option>...]
|
||||
|
||||
Adds the given labels to the patch or issue.
|
||||
|
||||
Options
|
||||
|
||||
--help Print help
|
||||
"#,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Options {
|
||||
pub id: cob::ObjectId,
|
||||
pub labels: NonEmpty<Label>,
|
||||
}
|
||||
|
||||
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<cob::ObjectId> = None;
|
||||
let mut labels: Vec<Label> = Vec::new();
|
||||
|
||||
while let Some(arg) = parser.next()? {
|
||||
match arg {
|
||||
Long("help") | Short('h') => {
|
||||
return Err(Error::Help.into());
|
||||
}
|
||||
Value(ref val) if id.is_none() => {
|
||||
id = Some(term::args::cob(val)?);
|
||||
}
|
||||
Value(ref val) if id.is_some() => {
|
||||
let s: String = val.parse()?;
|
||||
let label = Label::from_str(&s)?;
|
||||
|
||||
labels.push(label);
|
||||
}
|
||||
_ => {
|
||||
return Err(anyhow!(arg.unexpected()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok((
|
||||
Options {
|
||||
id: id.ok_or_else(|| anyhow!("an issue or patch must be specified"))?,
|
||||
labels: NonEmpty::from_vec(labels)
|
||||
.ok_or_else(|| anyhow!("at least one label must be specified"))?,
|
||||
},
|
||||
vec![],
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn label(
|
||||
options: Options,
|
||||
repo: &storage::git::Repository,
|
||||
signer: impl Signer,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut issues = issue::Issues::open(repo)?;
|
||||
match issues.get_mut(&options.id) {
|
||||
Ok(mut issue) => {
|
||||
let labels = issue
|
||||
.labels()
|
||||
.cloned()
|
||||
.chain(options.labels)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
issue.label(labels, &signer)?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
Err(store::Error::NotFound(_, _)) => {}
|
||||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
|
||||
let mut patches = patch::Patches::open(repo)?;
|
||||
match patches.get_mut(&options.id) {
|
||||
Ok(mut patch) => {
|
||||
let labels = patch
|
||||
.labels()
|
||||
.cloned()
|
||||
.chain(options.labels)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
patch.label(labels, &signer)?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
Err(store::Error::NotFound(_, _)) => {}
|
||||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
|
||||
anyhow::bail!("Couldn't find issue or patch {}", options.id)
|
||||
}
|
||||
|
||||
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 signer = term::signer(&profile)?;
|
||||
|
||||
label(options, &repo, signer)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -10,6 +10,8 @@ mod common;
|
|||
mod delete;
|
||||
#[path = "patch/edit.rs"]
|
||||
mod edit;
|
||||
#[path = "patch/label.rs"]
|
||||
mod label;
|
||||
#[path = "patch/list.rs"]
|
||||
mod list;
|
||||
#[path = "patch/ready.rs"]
|
||||
|
|
@ -26,8 +28,9 @@ use std::ffi::OsString;
|
|||
|
||||
use anyhow::anyhow;
|
||||
|
||||
use radicle::cob::patch;
|
||||
use nonempty::NonEmpty;
|
||||
use radicle::cob::patch::PatchId;
|
||||
use radicle::cob::{patch, Label};
|
||||
use radicle::prelude::*;
|
||||
use radicle::storage::git::transport;
|
||||
|
||||
|
|
@ -51,6 +54,8 @@ 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 ready <patch-id> [--undo] [<option>...]
|
||||
rad patch edit <patch-id> [<option>...]
|
||||
rad patch set <patch-id> [<option>...]
|
||||
|
|
@ -69,6 +74,14 @@ Edit options
|
|||
|
||||
-m, --message [<string>] Provide a comment message to the patch or revision (default: prompt)
|
||||
|
||||
Label options
|
||||
|
||||
-l, --label Label the patch with the provided label (may be specified multiple times)
|
||||
|
||||
Unlabel options
|
||||
|
||||
-l, --label Remove the provided label from the patch (may be specified multiple times)
|
||||
|
||||
Update options
|
||||
|
||||
-m, --message [<string>] Provide a comment message to the patch or revision (default: prompt)
|
||||
|
|
@ -111,6 +124,8 @@ pub enum OperationName {
|
|||
Checkout,
|
||||
Comment,
|
||||
Ready,
|
||||
Label,
|
||||
Unlabel,
|
||||
#[default]
|
||||
List,
|
||||
Edit,
|
||||
|
|
@ -169,6 +184,14 @@ pub enum Operation {
|
|||
message: Message,
|
||||
reply_to: Option<Rev>,
|
||||
},
|
||||
Label {
|
||||
patch_id: Rev,
|
||||
labels: NonEmpty<Label>,
|
||||
},
|
||||
Unlabel {
|
||||
patch_id: Rev,
|
||||
labels: NonEmpty<Label>,
|
||||
},
|
||||
List {
|
||||
filter: Filter,
|
||||
},
|
||||
|
|
@ -215,6 +238,7 @@ 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();
|
||||
|
||||
while let Some(arg) = parser.next()? {
|
||||
match arg {
|
||||
|
|
@ -285,6 +309,17 @@ impl Args for Options {
|
|||
checkout_opts.name = Some(term::args::refstring("name", val)?);
|
||||
}
|
||||
|
||||
// Un/Label options.
|
||||
Short('l') | Long("label")
|
||||
if matches!(op, Some(OperationName::Label | OperationName::Unlabel)) =>
|
||||
{
|
||||
let val = parser.value()?;
|
||||
let name = term::args::string(&val);
|
||||
let label = Label::new(name)?;
|
||||
|
||||
labels.push(label);
|
||||
}
|
||||
|
||||
// List options.
|
||||
Long("all") => {
|
||||
filter = Filter::all();
|
||||
|
|
@ -332,6 +367,8 @@ impl Args for Options {
|
|||
"y" | "ready" => op = Some(OperationName::Ready),
|
||||
"e" | "edit" => op = Some(OperationName::Edit),
|
||||
"r" | "redact" => op = Some(OperationName::Redact),
|
||||
"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),
|
||||
|
|
@ -352,6 +389,8 @@ impl Args for Options {
|
|||
Some(OperationName::Comment),
|
||||
Some(OperationName::Edit),
|
||||
Some(OperationName::Set),
|
||||
Some(OperationName::Label),
|
||||
Some(OperationName::Unlabel),
|
||||
]
|
||||
.contains(&op) =>
|
||||
{
|
||||
|
|
@ -400,6 +439,16 @@ impl Args for Options {
|
|||
OperationName::Redact => Operation::Redact {
|
||||
revision_id: revision_id.ok_or_else(|| anyhow!("a revision must be provided"))?,
|
||||
},
|
||||
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"))?,
|
||||
},
|
||||
OperationName::Set => Operation::Set {
|
||||
patch_id: patch_id.ok_or_else(|| anyhow!("a patch must be provided"))?,
|
||||
},
|
||||
|
|
@ -506,6 +555,14 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
|||
Operation::Redact { revision_id } => {
|
||||
redact::run(&revision_id, &profile, &repository)?;
|
||||
}
|
||||
Operation::Label { patch_id, labels } => {
|
||||
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)?;
|
||||
}
|
||||
Operation::Set { patch_id } => {
|
||||
let patches = radicle::cob::patch::Patches::open(&repository)?;
|
||||
let patch_id = patch_id.resolve(&repository.backend)?;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
use super::*;
|
||||
|
||||
use nonempty::NonEmpty;
|
||||
use radicle::storage::git::Repository;
|
||||
|
||||
use crate::terminal as term;
|
||||
|
||||
pub fn add(
|
||||
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>,
|
||||
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()
|
||||
.filter(|&l| !labels.contains(l))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
patch.label(labels, &signer)?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -76,6 +76,7 @@ pub fn run(
|
|||
)?;
|
||||
let author = patch.author();
|
||||
let author = term::format::Author::new(author.id(), profile);
|
||||
let labels = patch.labels().map(|l| l.to_string()).collect::<Vec<_>>();
|
||||
|
||||
let mut attrs = Table::<2, term::Line>::new(TableOptions {
|
||||
spacing: 2,
|
||||
|
|
@ -93,6 +94,12 @@ pub fn run(
|
|||
term::format::tertiary("Author".to_owned()).into(),
|
||||
author.line(),
|
||||
]);
|
||||
if !labels.is_empty() {
|
||||
attrs.push([
|
||||
term::format::tertiary("Labels".to_owned()).into(),
|
||||
term::format::secondary(labels.join(", ")).into(),
|
||||
]);
|
||||
}
|
||||
attrs.push([
|
||||
term::format::tertiary("Head".to_owned()).into(),
|
||||
term::format::secondary(revision.head().to_string()).into(),
|
||||
|
|
|
|||
|
|
@ -1,125 +0,0 @@
|
|||
use std::ffi::OsString;
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use nonempty::NonEmpty;
|
||||
|
||||
use radicle::cob;
|
||||
use radicle::cob::common::Label;
|
||||
use radicle::cob::{issue, patch, store};
|
||||
use radicle::crypto::Signer;
|
||||
use radicle::storage::{self, WriteStorage};
|
||||
|
||||
use crate::terminal as term;
|
||||
use crate::terminal::args::{Args, Error, Help};
|
||||
|
||||
pub const HELP: Help = Help {
|
||||
name: "untag",
|
||||
description: "Untag an issue or patch",
|
||||
version: env!("CARGO_PKG_VERSION"),
|
||||
usage: r#"
|
||||
Usage
|
||||
|
||||
rad untag <cob-id> <tag>... [<option>...]
|
||||
|
||||
Options
|
||||
|
||||
--help Print help
|
||||
"#,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Options {
|
||||
pub id: cob::ObjectId,
|
||||
pub labels: NonEmpty<Label>,
|
||||
}
|
||||
|
||||
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<cob::ObjectId> = None;
|
||||
let mut labels: Vec<Label> = Vec::new();
|
||||
|
||||
while let Some(arg) = parser.next()? {
|
||||
match arg {
|
||||
Long("help") | Short('h') => {
|
||||
return Err(Error::Help.into());
|
||||
}
|
||||
Value(ref val) if id.is_none() => {
|
||||
id = Some(term::args::cob(val)?);
|
||||
}
|
||||
Value(ref val) if id.is_some() => {
|
||||
let s: String = val.parse()?;
|
||||
let label = Label::from_str(&s)?;
|
||||
|
||||
labels.push(label);
|
||||
}
|
||||
_ => {
|
||||
return Err(anyhow!(arg.unexpected()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok((
|
||||
Options {
|
||||
id: id.ok_or_else(|| anyhow!("an issue or patch must be specified"))?,
|
||||
labels: NonEmpty::from_vec(labels)
|
||||
.ok_or_else(|| anyhow!("at least one label must be specified"))?,
|
||||
},
|
||||
vec![],
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn unlabel(
|
||||
options: Options,
|
||||
repo: &storage::git::Repository,
|
||||
signer: impl Signer,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut issues = issue::Issues::open(repo)?;
|
||||
match issues.get_mut(&options.id) {
|
||||
Ok(mut issue) => {
|
||||
let labels = issue
|
||||
.labels()
|
||||
.filter(|&l| !options.labels.contains(l))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
issue.label(labels, &signer)?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
Err(store::Error::NotFound(_, _)) => {}
|
||||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
|
||||
let mut patches = patch::Patches::open(repo)?;
|
||||
match patches.get_mut(&options.id) {
|
||||
Ok(mut patch) => {
|
||||
let labels = patch
|
||||
.labels()
|
||||
.filter(|&l| !options.labels.contains(l))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
patch.label(labels, &signer)?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
Err(store::Error::NotFound(_, _)) => {}
|
||||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
|
||||
anyhow::bail!("Couldn't find issue or patch {}", options.id)
|
||||
}
|
||||
|
||||
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 signer = term::signer(&profile)?;
|
||||
|
||||
unlabel(options, &repo, signer)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -243,13 +243,6 @@ fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option<anyhow::Error>>
|
|||
args.to_vec(),
|
||||
);
|
||||
}
|
||||
"label" => {
|
||||
term::run_command_args::<rad_label::Options, _>(
|
||||
rad_label::HELP,
|
||||
rad_label::run,
|
||||
args.to_vec(),
|
||||
);
|
||||
}
|
||||
"track" => {
|
||||
term::run_command_args::<rad_track::Options, _>(
|
||||
rad_track::HELP,
|
||||
|
|
@ -264,13 +257,6 @@ fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option<anyhow::Error>>
|
|||
args.to_vec(),
|
||||
);
|
||||
}
|
||||
"unlabel" => {
|
||||
term::run_command_args::<rad_unlabel::Options, _>(
|
||||
rad_unlabel::HELP,
|
||||
rad_unlabel::run,
|
||||
args.to_vec(),
|
||||
);
|
||||
}
|
||||
"untrack" => {
|
||||
term::run_command_args::<rad_untrack::Options, _>(
|
||||
rad_untrack::HELP,
|
||||
|
|
|
|||
|
|
@ -117,21 +117,6 @@ fn rad_cob() {
|
|||
test("examples/rad-cob.md", &working, Some(home), []).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rad_label() {
|
||||
let mut environment = Environment::new();
|
||||
let profile = environment.profile("alice");
|
||||
let home = &profile.home;
|
||||
let working = environment.tmp().join("working");
|
||||
|
||||
// Setup a test repository.
|
||||
fixtures::repository(&working);
|
||||
|
||||
test("examples/rad-init.md", &working, Some(home), []).unwrap();
|
||||
test("examples/rad-issue.md", &working, Some(home), []).unwrap();
|
||||
test("examples/rad-label.md", &working, Some(home), []).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rad_init() {
|
||||
let mut environment = Environment::new();
|
||||
|
|
|
|||
Loading…
Reference in New Issue