cli: Support `rad issue edit`
Support editing an issue via the command line.
This commit is contained in:
parent
478b61f875
commit
f2d4b9acc2
|
|
@ -11,6 +11,7 @@ use radicle::crypto::Signer;
|
||||||
use radicle::node::Handle;
|
use radicle::node::Handle;
|
||||||
use radicle::prelude::Did;
|
use radicle::prelude::Did;
|
||||||
use radicle::profile;
|
use radicle::profile;
|
||||||
|
use radicle::storage;
|
||||||
use radicle::storage::WriteStorage;
|
use radicle::storage::WriteStorage;
|
||||||
use radicle::{cob, Node};
|
use radicle::{cob, Node};
|
||||||
use radicle_term::table::TableOptions;
|
use radicle_term::table::TableOptions;
|
||||||
|
|
@ -30,6 +31,7 @@ Usage
|
||||||
|
|
||||||
rad issue [<option>...]
|
rad issue [<option>...]
|
||||||
rad issue delete <issue-id> [<option>...]
|
rad issue delete <issue-id> [<option>...]
|
||||||
|
rad issue edit <issue-id> [<option>...]
|
||||||
rad issue list [--assigned <did>] [<option>...]
|
rad issue list [--assigned <did>] [<option>...]
|
||||||
rad issue open [--title <title>] [--description <text>] [--tag <tag>] [<option>...]
|
rad issue open [--title <title>] [--description <text>] [--tag <tag>] [<option>...]
|
||||||
rad issue react <issue-id> [--emoji <char>] [<option>...]
|
rad issue react <issue-id> [--emoji <char>] [<option>...]
|
||||||
|
|
@ -53,6 +55,7 @@ pub struct Metadata {
|
||||||
|
|
||||||
#[derive(Default, Debug, PartialEq, Eq)]
|
#[derive(Default, Debug, PartialEq, Eq)]
|
||||||
pub enum OperationName {
|
pub enum OperationName {
|
||||||
|
Edit,
|
||||||
Open,
|
Open,
|
||||||
Delete,
|
Delete,
|
||||||
#[default]
|
#[default]
|
||||||
|
|
@ -72,6 +75,11 @@ pub enum Assigned {
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub enum Operation {
|
pub enum Operation {
|
||||||
|
Edit {
|
||||||
|
id: Rev,
|
||||||
|
title: Option<String>,
|
||||||
|
description: Option<String>,
|
||||||
|
},
|
||||||
Open {
|
Open {
|
||||||
title: Option<String>,
|
title: Option<String>,
|
||||||
description: Option<String>,
|
description: Option<String>,
|
||||||
|
|
@ -191,6 +199,11 @@ impl Args for Options {
|
||||||
}
|
}
|
||||||
|
|
||||||
let op = match op.unwrap_or_default() {
|
let op = match op.unwrap_or_default() {
|
||||||
|
OperationName::Edit => Operation::Edit {
|
||||||
|
id: id.ok_or_else(|| anyhow!("an issue must be provided"))?,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
},
|
||||||
OperationName::Open => Operation::Open {
|
OperationName::Open => Operation::Open {
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
|
|
@ -242,6 +255,13 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
let mut issues = Issues::open(&repo)?;
|
let mut issues = Issues::open(&repo)?;
|
||||||
|
|
||||||
match options.op {
|
match options.op {
|
||||||
|
Operation::Edit {
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
} => {
|
||||||
|
edit(&mut issues, &signer, &repo, id, title, description)?;
|
||||||
|
}
|
||||||
Operation::Open {
|
Operation::Open {
|
||||||
title: Some(title),
|
title: Some(title),
|
||||||
description: Some(description),
|
description: Some(description),
|
||||||
|
|
@ -407,6 +427,7 @@ fn list(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get Issue meta-data and description from the user through the editor.
|
||||||
fn prompt_issue(
|
fn prompt_issue(
|
||||||
title: &str,
|
title: &str,
|
||||||
description: &str,
|
description: &str,
|
||||||
|
|
@ -497,6 +518,87 @@ fn open<G: Signer>(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn edit<G: radicle::crypto::Signer>(
|
||||||
|
issues: &mut issue::Issues,
|
||||||
|
signer: &G,
|
||||||
|
repo: &storage::git::Repository,
|
||||||
|
id: Rev,
|
||||||
|
title: Option<String>,
|
||||||
|
description: Option<String>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let id = id.resolve(&repo.backend)?;
|
||||||
|
let mut issue = issues.get_mut(&id)?;
|
||||||
|
let (desc_id, issue_desc) = issue.description();
|
||||||
|
let desc_id = *desc_id;
|
||||||
|
|
||||||
|
if title.is_some() || description.is_some() {
|
||||||
|
// Editing by command line arguments
|
||||||
|
issue.transaction("Edit", signer, |tx| {
|
||||||
|
if let Some(t) = title {
|
||||||
|
tx.edit(t)?;
|
||||||
|
}
|
||||||
|
if let Some(d) = description {
|
||||||
|
tx.edit_comment(desc_id, d)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Editing by editor
|
||||||
|
let tags: Vec<_> = issue.tags().cloned().collect();
|
||||||
|
let assigned: Vec<_> = issue.assigned().collect();
|
||||||
|
|
||||||
|
let Some((meta, description)) = prompt_issue(
|
||||||
|
issue.title(),
|
||||||
|
issue_desc,
|
||||||
|
&tags,
|
||||||
|
&assigned,
|
||||||
|
)? else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
issue.transaction("Edit", signer, |tx| {
|
||||||
|
tx.edit(meta.title)?;
|
||||||
|
tx.edit_comment(desc_id, description)?;
|
||||||
|
|
||||||
|
let add: Vec<_> = meta
|
||||||
|
.tags
|
||||||
|
.iter()
|
||||||
|
.filter(|t| !tags.contains(t))
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
let remove: Vec<_> = tags
|
||||||
|
.iter()
|
||||||
|
.filter(|t| !meta.tags.contains(t))
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
tx.tag(add, remove)?;
|
||||||
|
|
||||||
|
let assign: Vec<_> = meta
|
||||||
|
.assignees
|
||||||
|
.iter()
|
||||||
|
.filter(|t| !assigned.contains(t))
|
||||||
|
.cloned()
|
||||||
|
.map(cob::ActorId::from)
|
||||||
|
.collect();
|
||||||
|
let unassign: Vec<_> = assigned
|
||||||
|
.iter()
|
||||||
|
.filter(|t| !meta.assignees.contains(t))
|
||||||
|
.cloned()
|
||||||
|
.map(cob::ActorId::from)
|
||||||
|
.collect();
|
||||||
|
tx.assign(assign, unassign)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
show_issue(&issue)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn show_issue(issue: &issue::Issue) -> anyhow::Result<()> {
|
fn show_issue(issue: &issue::Issue) -> anyhow::Result<()> {
|
||||||
let tags: Vec<String> = issue.tags().cloned().map(|t| t.into()).collect();
|
let tags: Vec<String> = issue.tags().cloned().map(|t| t.into()).collect();
|
||||||
let assignees: Vec<String> = issue
|
let assignees: Vec<String> = issue
|
||||||
|
|
@ -540,7 +642,7 @@ fn show_issue(issue: &issue::Issue) -> anyhow::Result<()> {
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let description = issue.description().unwrap_or_default();
|
let (_, description) = issue.description();
|
||||||
let widget = VStack::default()
|
let widget = VStack::default()
|
||||||
.border(Some(term::colors::FAINT))
|
.border(Some(term::colors::FAINT))
|
||||||
.child(attrs)
|
.child(attrs)
|
||||||
|
|
|
||||||
|
|
@ -228,8 +228,12 @@ impl Issue {
|
||||||
.expect("Issue::author: at least one comment is present")
|
.expect("Issue::author: at least one comment is present")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn description(&self) -> Option<&str> {
|
pub fn description(&self) -> (&CommentId, &str) {
|
||||||
self.thread.comments().next().map(|(_, c)| c.body())
|
self.thread
|
||||||
|
.comments()
|
||||||
|
.next()
|
||||||
|
.map(|(id, c)| (id, c.body()))
|
||||||
|
.expect("Issue::description: at least one comment is present")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn thread(&self) -> &Thread {
|
pub fn thread(&self) -> &Thread {
|
||||||
|
|
@ -701,7 +705,7 @@ mod test {
|
||||||
assert_eq!(created, issue);
|
assert_eq!(created, issue);
|
||||||
assert_eq!(issue.title(), "My first issue");
|
assert_eq!(issue.title(), "My first issue");
|
||||||
assert_eq!(issue.author().id, Did::from(signer.public_key()));
|
assert_eq!(issue.author().id, Did::from(signer.public_key()));
|
||||||
assert_eq!(issue.description(), Some("Blah blah blah."));
|
assert_eq!(issue.description().1, "Blah blah blah.");
|
||||||
assert_eq!(issue.comments().count(), 1);
|
assert_eq!(issue.comments().count(), 1);
|
||||||
assert_eq!(issue.state(), &State::Open);
|
assert_eq!(issue.state(), &State::Open);
|
||||||
}
|
}
|
||||||
|
|
@ -809,9 +813,9 @@ mod test {
|
||||||
|
|
||||||
let id = issue.id;
|
let id = issue.id;
|
||||||
let issue = issues.get(&id).unwrap().unwrap();
|
let issue = issues.get(&id).unwrap().unwrap();
|
||||||
let r = issue.description().unwrap();
|
let (_, desc) = issue.description();
|
||||||
|
|
||||||
assert_eq!(r, "Bob Loblaw law blog");
|
assert_eq!(desc, "Bob Loblaw law blog");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -1013,7 +1017,7 @@ mod test {
|
||||||
assert_eq!(created, issue);
|
assert_eq!(created, issue);
|
||||||
assert_eq!(issue.title(), "My first issue");
|
assert_eq!(issue.title(), "My first issue");
|
||||||
assert_eq!(issue.author().id, Did::from(signer.public_key()));
|
assert_eq!(issue.author().id, Did::from(signer.public_key()));
|
||||||
assert_eq!(issue.description(), Some("Blah blah blah.\nYah yah yah"));
|
assert_eq!(issue.description().1, "Blah blah blah.\nYah yah yah");
|
||||||
assert_eq!(issue.comments().count(), 1);
|
assert_eq!(issue.comments().count(), 1);
|
||||||
assert_eq!(issue.state(), &State::Open);
|
assert_eq!(issue.state(), &State::Open);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue