Add `rad-issue` command
Signed-off-by: xphoniex <dj.2dixx@gmail.com>
This commit is contained in:
parent
dc21692ea3
commit
89bdf59e8e
|
|
@ -1105,6 +1105,12 @@ dependencies = [
|
|||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linked-hash-map"
|
||||
version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.17"
|
||||
|
|
@ -1577,7 +1583,9 @@ dependencies = [
|
|||
"radicle",
|
||||
"radicle-cob",
|
||||
"radicle-crypto",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"thiserror",
|
||||
"timeago",
|
||||
"zeroize",
|
||||
|
|
@ -1945,6 +1953,18 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_yaml"
|
||||
version = "0.8.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"ryu",
|
||||
"serde",
|
||||
"yaml-rust",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.9.9"
|
||||
|
|
@ -2695,6 +2715,15 @@ version = "0.42.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
|
||||
|
||||
[[package]]
|
||||
name = "yaml-rust"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
|
||||
dependencies = [
|
||||
"linked-hash-map",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.5.7"
|
||||
|
|
|
|||
|
|
@ -14,7 +14,9 @@ indicatif = { version = "0.16.2" }
|
|||
json-color = { version = "0.7" }
|
||||
lexopt = { version = "0.2" }
|
||||
log = { version = "0.4", features = ["std"] }
|
||||
serde = { version = "1.0" }
|
||||
serde_json = { version = "1" }
|
||||
serde_yaml = { version = "0.8" }
|
||||
thiserror = { version = "1" }
|
||||
timeago = { version = "0.3", default-features = false }
|
||||
zeroize = { version = "1.1" }
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ pub mod rad_help;
|
|||
pub mod rad_init;
|
||||
#[path = "commands/inspect.rs"]
|
||||
pub mod rad_inspect;
|
||||
#[path = "commands/issue.rs"]
|
||||
pub mod rad_issue;
|
||||
#[path = "commands/ls.rs"]
|
||||
pub mod rad_ls;
|
||||
#[path = "commands/patch.rs"]
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ const COMMANDS: &[Help] = &[
|
|||
rad_edit::HELP,
|
||||
rad_inspect::HELP,
|
||||
rad_rm::HELP,
|
||||
rad_issue::HELP,
|
||||
HELP,
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,247 @@
|
|||
#![allow(clippy::or_fun_call)]
|
||||
use std::ffi::OsString;
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{anyhow, Context as _};
|
||||
|
||||
use crate::terminal as term;
|
||||
use crate::terminal::args::{Args, Error, Help};
|
||||
|
||||
use radicle::cob::issue::{CloseReason, IssueId, State};
|
||||
use radicle::cob::shared::{Label, Reaction};
|
||||
use radicle::cob::store::Store;
|
||||
use radicle::storage::WriteStorage;
|
||||
|
||||
pub const HELP: Help = Help {
|
||||
name: "issue",
|
||||
description: "Manage radicle issues",
|
||||
version: env!("CARGO_PKG_VERSION"),
|
||||
usage: r#"
|
||||
Usage
|
||||
|
||||
rad issue new [--title <title>] [--description <text>]
|
||||
rad issue state <id> [--closed | --open | --solved]
|
||||
rad issue delete <id>
|
||||
rad issue react <id> [--emoji <char>]
|
||||
rad issue list
|
||||
|
||||
Options
|
||||
|
||||
--help Print help
|
||||
"#,
|
||||
};
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize, Debug)]
|
||||
pub struct Metadata {
|
||||
title: String,
|
||||
labels: Vec<Label>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum OperationName {
|
||||
Create,
|
||||
State,
|
||||
React,
|
||||
Delete,
|
||||
List,
|
||||
}
|
||||
|
||||
impl Default for OperationName {
|
||||
fn default() -> Self {
|
||||
Self::List
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Operation {
|
||||
Create {
|
||||
title: Option<String>,
|
||||
description: Option<String>,
|
||||
},
|
||||
State {
|
||||
id: IssueId,
|
||||
state: State,
|
||||
},
|
||||
Delete {
|
||||
id: IssueId,
|
||||
},
|
||||
React {
|
||||
id: IssueId,
|
||||
reaction: Reaction,
|
||||
},
|
||||
List,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Options {
|
||||
pub op: Operation,
|
||||
}
|
||||
|
||||
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 op: Option<OperationName> = None;
|
||||
let mut id: Option<IssueId> = None;
|
||||
let mut title: Option<String> = None;
|
||||
let mut reaction: Option<Reaction> = None;
|
||||
let mut description: Option<String> = None;
|
||||
let mut state: Option<State> = None;
|
||||
|
||||
while let Some(arg) = parser.next()? {
|
||||
match arg {
|
||||
Long("help") => {
|
||||
return Err(Error::Help.into());
|
||||
}
|
||||
Long("title") if op == Some(OperationName::Create) => {
|
||||
title = Some(parser.value()?.to_string_lossy().into());
|
||||
}
|
||||
Long("closed") if op == Some(OperationName::State) => {
|
||||
state = Some(State::Closed {
|
||||
reason: CloseReason::Other,
|
||||
});
|
||||
}
|
||||
Long("open") if op == Some(OperationName::State) => {
|
||||
state = Some(State::Open);
|
||||
}
|
||||
Long("solved") if op == Some(OperationName::State) => {
|
||||
state = Some(State::Closed {
|
||||
reason: CloseReason::Solved,
|
||||
});
|
||||
}
|
||||
Long("reaction") if op == Some(OperationName::React) => {
|
||||
if let Some(emoji) = parser.value()?.to_str() {
|
||||
reaction =
|
||||
Some(Reaction::from_str(emoji).map_err(|_| anyhow!("invalid emoji"))?);
|
||||
}
|
||||
}
|
||||
Long("description") if op == Some(OperationName::Create) => {
|
||||
description = Some(parser.value()?.to_string_lossy().into());
|
||||
}
|
||||
Value(val) if op.is_none() => match val.to_string_lossy().as_ref() {
|
||||
"n" | "new" => op = Some(OperationName::Create),
|
||||
"s" | "state" => op = Some(OperationName::State),
|
||||
"d" | "delete" => op = Some(OperationName::Delete),
|
||||
"l" | "list" => op = Some(OperationName::List),
|
||||
"r" | "react" => op = Some(OperationName::React),
|
||||
|
||||
unknown => anyhow::bail!("unknown operation '{}'", unknown),
|
||||
},
|
||||
Value(val) if op.is_some() => {
|
||||
let val = val
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow!("issue id specified is not UTF-8"))?;
|
||||
|
||||
id = Some(
|
||||
IssueId::from_str(val)
|
||||
.map_err(|_| anyhow!("invalid issue id '{}'", val))?,
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
return Err(anyhow!(arg.unexpected()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let op = match op.unwrap_or_default() {
|
||||
OperationName::Create => Operation::Create { title, description },
|
||||
OperationName::State => Operation::State {
|
||||
id: id.ok_or_else(|| anyhow!("an issue id must be provided"))?,
|
||||
state: state.ok_or_else(|| anyhow!("a state operation must be provided"))?,
|
||||
},
|
||||
OperationName::React => Operation::React {
|
||||
id: id.ok_or_else(|| anyhow!("an issue id must be provided"))?,
|
||||
reaction: reaction.ok_or_else(|| anyhow!("a reaction emoji must be provided"))?,
|
||||
},
|
||||
OperationName::Delete => Operation::Delete {
|
||||
id: id.ok_or_else(|| anyhow!("an issue id to remove must be provided"))?,
|
||||
},
|
||||
OperationName::List => Operation::List,
|
||||
};
|
||||
|
||||
Ok((Options { op }, vec![]))
|
||||
}
|
||||
}
|
||||
|
||||
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(id)?;
|
||||
let cobs = Store::open(*signer.public_key(), &repo)?;
|
||||
let issues = cobs.issues();
|
||||
|
||||
match options.op {
|
||||
Operation::Create {
|
||||
title: Some(title),
|
||||
description: Some(description),
|
||||
} => {
|
||||
issues.create(&title, &description, &[], &signer)?;
|
||||
}
|
||||
Operation::State { id, state } => {
|
||||
issues.lifecycle(&id, state, &signer)?;
|
||||
}
|
||||
Operation::React { id, reaction } => {
|
||||
if let Some(issue) = issues.get(&id)? {
|
||||
let comment_id = term::comment_select(&issue).unwrap();
|
||||
issues.react(&id, comment_id, reaction, &signer)?;
|
||||
}
|
||||
}
|
||||
Operation::Create { title, description } => {
|
||||
let meta = Metadata {
|
||||
title: title.unwrap_or("Enter a title".to_owned()),
|
||||
labels: vec![],
|
||||
};
|
||||
let yaml = serde_yaml::to_string(&meta)?;
|
||||
let doc = format!(
|
||||
"{}---\n\n{}",
|
||||
yaml,
|
||||
description.unwrap_or("Enter a description...".to_owned())
|
||||
);
|
||||
|
||||
if let Some(text) = term::Editor::new().edit(&doc)? {
|
||||
let mut meta = String::new();
|
||||
let mut frontmatter = false;
|
||||
let mut lines = text.lines();
|
||||
|
||||
while let Some(line) = lines.by_ref().next() {
|
||||
if line.trim() == "---" {
|
||||
if frontmatter {
|
||||
break;
|
||||
} else {
|
||||
frontmatter = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if frontmatter {
|
||||
meta.push_str(line);
|
||||
meta.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
let description: String = lines.collect::<Vec<&str>>().join("\n");
|
||||
let meta: Metadata =
|
||||
serde_yaml::from_str(&meta).context("failed to parse yaml front-matter")?;
|
||||
|
||||
issues.create(
|
||||
&meta.title,
|
||||
description.trim(),
|
||||
meta.labels.as_slice(),
|
||||
&signer,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Operation::List => {
|
||||
for (id, issue) in issues.all()? {
|
||||
println!("{} {}", id, issue.title());
|
||||
}
|
||||
}
|
||||
Operation::Delete { id } => {
|
||||
issues.remove(&id, &signer)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -190,6 +190,14 @@ fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option<anyhow::Error>>
|
|||
args.to_vec(),
|
||||
);
|
||||
}
|
||||
"issue" => {
|
||||
term::run_command_args::<rad_issue::Options, _>(
|
||||
rad_issue::HELP,
|
||||
"Command",
|
||||
rad_issue::run,
|
||||
args.to_vec(),
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
let exe = format!("{}-{}", NAME, exe);
|
||||
let status = process::Command::new(exe.clone()).args(args).status();
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use dialoguer::{console::style, console::Style, theme::ColorfulTheme, Input, Password};
|
||||
|
||||
use radicle::cob::issue::Issue;
|
||||
use radicle::cob::shared::CommentId;
|
||||
use radicle::crypto::ssh::keystore::Passphrase;
|
||||
use radicle::crypto::Signer;
|
||||
use radicle::profile::env::RAD_PASSPHRASE;
|
||||
use radicle::profile::Profile;
|
||||
|
||||
use dialoguer::{console::style, console::Style, theme::ColorfulTheme, Input, Password};
|
||||
use radicle_crypto::ssh::keystore::MemorySigner;
|
||||
|
||||
use super::command;
|
||||
|
|
@ -369,6 +372,24 @@ where
|
|||
result.map(|i| &options[i])
|
||||
}
|
||||
|
||||
pub fn comment_select(issue: &Issue) -> Option<CommentId> {
|
||||
let selection = dialoguer::Select::with_theme(&theme())
|
||||
.with_prompt("Which comment do you want to react to?")
|
||||
.item(&issue.description().to_string())
|
||||
.items(
|
||||
&issue
|
||||
.comments()
|
||||
.iter()
|
||||
.map(|p| p.body.clone())
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.default(CommentId::root().into())
|
||||
.interact_opt()
|
||||
.unwrap();
|
||||
|
||||
selection.map(CommentId::from)
|
||||
}
|
||||
|
||||
pub fn markdown(content: &str) {
|
||||
if !content.is_empty() && command::bat(["-p", "-l", "md"], content).is_err() {
|
||||
blob(content);
|
||||
|
|
|
|||
|
|
@ -223,7 +223,7 @@ impl<'a> IssueStore<'a> {
|
|||
}
|
||||
|
||||
/// Remove an issue.
|
||||
pub fn remove<G: Signer>(&self, _issue_id: &IssueId) -> Result<(), Error> {
|
||||
pub fn remove<G: Signer>(&self, _issue_id: &IssueId, _signer: &G) -> Result<(), Error> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue