httpd: Add `assignees` to `Issues::create`

Signed-off-by: Sebastian Martinez <me@sebastinez.dev>
This commit is contained in:
Sebastian Martinez 2023-02-09 15:31:36 -03:00 committed by Alexis Sellier
parent 540c56cac6
commit 881e9af219
No known key found for this signature in database
5 changed files with 104 additions and 45 deletions

View File

@ -11,7 +11,7 @@ The issue is now listed under our project.
```
$ rad issue list
17e52fd6b2cd2e0f9317e18371dc978a691ed1e3 "flux capacitor underpowered"
2b4650e3c66d568132034de0d02871a2fbf9c5b5 "flux capacitor underpowered"
```
Great! Now we've documented the issue for ourselves and others.
@ -22,20 +22,20 @@ others to work on. This is to ensure work is not duplicated.
Let's assign ourselves to this one.
```
$ rad assign 17e52fd6b2cd2e0f9317e18371dc978a691ed1e3 z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
$ rad assign 2b4650e3c66d568132034de0d02871a2fbf9c5b5 z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
```
It will now show in the list of issues assigned to us.
```
$ rad issue list --assigned
17e52fd6b2cd2e0f9317e18371dc978a691ed1e3 "flux capacitor underpowered" z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
2b4650e3c66d568132034de0d02871a2fbf9c5b5 "flux capacitor underpowered" z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
```
Note: this can always be undone with the `unassign` subcommand.
```
$ rad unassign 17e52fd6b2cd2e0f9317e18371dc978a691ed1e3 z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
$ rad unassign 2b4650e3c66d568132034de0d02871a2fbf9c5b5 z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
```
Great, now we have communicated to the world about our car's defect.
@ -44,8 +44,8 @@ But wait! We've found an important detail about the car's power requirements.
It will help whoever works on a fix.
```
$ rad comment 17e52fd6b2cd2e0f9317e18371dc978a691ed1e3 --message 'The flux capacitor needs 1.21 Gigawatts'
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/6
$ rad comment 17e52fd6b2cd2e0f9317e18371dc978a691ed1e3 --reply-to z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/6 --message 'More power!'
$ rad comment 2b4650e3c66d568132034de0d02871a2fbf9c5b5 --message 'The flux capacitor needs 1.21 Gigawatts'
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/7
$ rad comment 2b4650e3c66d568132034de0d02871a2fbf9c5b5 --reply-to z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/7 --message 'More power!'
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/8
```

View File

@ -38,6 +38,7 @@ Options
pub struct Metadata {
title: String,
labels: Vec<Tag>,
assignees: Vec<cob::ActorId>,
}
#[derive(Default, Debug, PartialEq, Eq)]
@ -205,7 +206,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
title: Some(title),
description: Some(description),
} => {
issues.create(title, description, &[], &signer)?;
issues.create(title, description, &[], &[], &signer)?;
}
Operation::Show { id } => {
let issue = issues
@ -227,6 +228,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
let meta = Metadata {
title: title.unwrap_or("Enter a title".to_owned()),
labels: vec![],
assignees: vec![],
};
let yaml = serde_yaml::to_string(&meta)?;
let doc = format!(
@ -263,6 +265,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
&meta.title,
description.trim(),
meta.labels.as_slice(),
meta.assignees.as_slice(),
&signer,
)?;
}

View File

@ -23,7 +23,7 @@ use crate::api::{auth, Context};
pub const HEAD: &str = "1e978d19f251cd9821d9d9a76d1bd436bf0690d5";
pub const HEAD_1: &str = "f604ce9fd5b7cc77b7609beda45ea8760bee78f7";
pub const PATCH_ID: &str = "4250f0117659ee4de9af99e699a63395cd6ffa1c";
pub const ISSUE_ID: &str = "d8131af9738258fac139c4c96b71c02411f94892";
pub const ISSUE_ID: &str = "8adca8aad2a2cb99b9847d20193930cde2042a57";
const PASSWORD: &str = "radicle";
@ -113,6 +113,7 @@ pub fn seed(dir: &Path) -> Context {
"Issue #1".to_string(),
"Change 'hello world' to 'hello everyone'".to_string(),
&[],
&[],
&signer,
)
.unwrap();

View File

@ -14,7 +14,7 @@ use tower_http::set_header::SetResponseHeaderLayer;
use radicle::cob::issue::{Action, Issues};
use radicle::cob::patch::Patches;
use radicle::cob::{thread, Tag};
use radicle::cob::{thread, ActorId, Tag};
use radicle::identity::Id;
use radicle::node::NodeId;
use radicle::storage::{git::paths, ReadRepository, WriteStorage};
@ -404,6 +404,7 @@ pub struct IssueCreate {
pub title: String,
pub description: String,
pub tags: Vec<Tag>,
pub assignees: Vec<ActorId>,
}
/// Create a new issue.
@ -424,7 +425,13 @@ async fn issue_create_handler(
let repo = storage.repository(project)?;
let mut issues = Issues::open(ctx.profile.public_key, &repo)?;
let issue = issues
.create(issue.title, issue.description, &issue.tags, &signer)
.create(
issue.title,
issue.description,
&issue.tags,
&issue.assignees,
&signer,
)
.map_err(Error::from)?;
Ok::<_, Error>((
@ -551,7 +558,7 @@ mod routes {
use crate::api::test::{self, get, patch, post, HEAD, HEAD_1, ISSUE_ID, PATCH_ID};
const CREATED_ISSUE_ID: &str = "768c6735912a34856552ae6a9ca77d728962bf31";
const CREATED_ISSUE_ID: &str = "b56febfba1e7dd20f4aea43ca2fe9dcf1fd448ba";
#[tokio::test]
async fn test_projects_root() {
@ -1041,6 +1048,7 @@ mod routes {
"title": "Issue #2",
"description": "Change 'hello world' to 'hello everyone'",
"tags": ["bug"],
"assignees": [],
}))
.unwrap();
let response = post(
@ -1147,7 +1155,7 @@ mod routes {
"replyTo": null,
},
{
"id": "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/4",
"id": "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/5",
"author": {
"id": "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
},
@ -1217,7 +1225,7 @@ mod routes {
"replyTo": null,
},
{
"id": "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/4",
"id": "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/5",
"author": {
"id": "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
},

View File

@ -193,7 +193,11 @@ impl Deref for Issue {
}
impl store::Transaction<Issue> {
pub fn assign(&mut self, add: Vec<ActorId>, remove: Vec<ActorId>) -> OpId {
pub fn assign(
&mut self,
add: impl IntoIterator<Item = ActorId>,
remove: impl IntoIterator<Item = ActorId>,
) -> OpId {
let add = add.into_iter().collect::<Vec<_>>();
let remove = remove.into_iter().collect::<Vec<_>>();
@ -273,10 +277,10 @@ impl<'a, 'g> IssueMut<'a, 'g> {
/// Assign one or more actors to an issue.
pub fn assign<G: Signer>(
&mut self,
assignees: Vec<ActorId>,
assignees: impl IntoIterator<Item = ActorId>,
signer: &G,
) -> Result<OpId, Error> {
self.transaction("Assign", signer, |tx| tx.assign(assignees, vec![]))
self.transaction("Assign", signer, |tx| tx.assign(assignees, []))
}
/// Set the issue title.
@ -332,10 +336,10 @@ impl<'a, 'g> IssueMut<'a, 'g> {
/// Unassign one or more actors from an issue.
pub fn unassign<G: Signer>(
&mut self,
assignees: Vec<ActorId>,
assignees: impl IntoIterator<Item = ActorId>,
signer: &G,
) -> Result<OpId, Error> {
self.transaction("Unassign", signer, |tx| tx.assign(vec![], assignees))
self.transaction("Unassign", signer, |tx| tx.assign([], assignees))
}
pub fn transaction<G, F, T>(
@ -416,16 +420,18 @@ impl<'a> Issues<'a> {
title: impl ToString,
description: impl ToString,
tags: &[Tag],
assignees: &[ActorId],
signer: &G,
) -> Result<IssueMut<'a, 'g>, Error> {
let (id, issue, clock) =
Transaction::initial("Create issue", &mut self.raw, signer, |tx| {
tx.thread(description);
tx.assign(assignees.to_owned(), []);
tx.edit(title);
tx.tag(tags.to_owned(), []);
})?;
// Just a sanity check that our clock is advancing as expected.
debug_assert_eq!(clock.get(), 3);
debug_assert_eq!(clock.get(), 4);
Ok(IssueMut {
id,
@ -498,15 +504,30 @@ mod test {
let assignee: ActorId = arbitrary::gen(1);
let assignee_two: ActorId = arbitrary::gen(1);
let mut issue = issues
.create("My first issue", "Blah blah blah.", &[], &signer)
let issue = issues
.create(
"My first issue",
"Blah blah blah.",
&[],
&[assignee],
&signer,
)
.unwrap();
issue.assign(vec![assignee, assignee_two], &signer).unwrap();
let id = issue.id;
let issue = issues.get(&id).unwrap().unwrap();
let assignees: Vec<_> = issue.assigned().cloned().collect::<Vec<_>>();
assert_eq!(1, assignees.len());
assert!(assignees.contains(&assignee));
let mut issue = issues.get_mut(&id).unwrap();
issue.assign([assignee_two], &signer).unwrap();
let id = issue.id;
let issue = issues.get(&id).unwrap().unwrap();
let assignees: Vec<_> = issue.assigned().cloned().collect::<Vec<_>>();
assert_eq!(2, assignees.len());
assert!(assignees.contains(&assignee));
assert!(assignees.contains(&assignee_two));
@ -519,18 +540,27 @@ mod test {
let mut issues = Issues::open(*signer.public_key(), &project).unwrap();
let assignee: ActorId = arbitrary::gen(1);
let assignee_two: ActorId = arbitrary::gen(1);
let mut issue = issues
.create("My first issue", "Blah blah blah.", &[], &signer)
.create(
"My first issue",
"Blah blah blah.",
&[],
&[assignee],
&signer,
)
.unwrap();
issue.assign(vec![assignee], &signer).unwrap();
issue.assign(vec![assignee], &signer).unwrap();
issue.assign([assignee_two], &signer).unwrap();
issue.assign([assignee_two], &signer).unwrap();
let id = issue.id;
let issue = issues.get(&id).unwrap().unwrap();
let assignees: Vec<_> = issue.assigned().cloned().collect::<Vec<_>>();
assert_eq!(1, assignees.len());
assert_eq!(2, assignees.len());
assert!(assignees.contains(&assignee));
assert!(assignees.contains(&assignee_two));
}
#[test]
@ -539,9 +569,10 @@ mod test {
let (_, signer, project) = test::setup::context(&tmp);
let mut issues = Issues::open(*signer.public_key(), &project).unwrap();
let created = issues
.create("My first issue", "Blah blah blah.", &[], &signer)
.create("My first issue", "Blah blah blah.", &[], &[], &signer)
.unwrap();
assert_eq!(created.clock().get(), 3);
assert_eq!(created.clock().get(), 4);
let (id, created) = (created.id, created.issue);
let issue = issues.get(&id).unwrap().unwrap();
@ -560,7 +591,7 @@ mod test {
let (_, signer, project) = test::setup::context(&tmp);
let mut issues = Issues::open(*signer.public_key(), &project).unwrap();
let mut issue = issues
.create("My first issue", "Blah blah blah.", &[], &signer)
.create("My first issue", "Blah blah blah.", &[], &[], &signer)
.unwrap();
issue
@ -574,6 +605,7 @@ mod test {
let id = issue.id;
let mut issue = issues.get_mut(&id).unwrap();
assert_eq!(
*issue.state(),
State::Closed {
@ -583,6 +615,7 @@ mod test {
issue.lifecycle(State::Open, &signer).unwrap();
let issue = issues.get(&id).unwrap().unwrap();
assert_eq!(*issue.state(), State::Open);
}
@ -595,15 +628,21 @@ mod test {
let assignee: ActorId = arbitrary::gen(1);
let assignee_two: ActorId = arbitrary::gen(1);
let mut issue = issues
.create("My first issue", "Blah blah blah.", &[], &signer)
.create(
"My first issue",
"Blah blah blah.",
&[],
&[assignee, assignee_two],
&signer,
)
.unwrap();
issue.assign(vec![assignee, assignee_two], &signer).unwrap();
issue.unassign(vec![assignee], &signer).unwrap();
issue.unassign([assignee], &signer).unwrap();
let id = issue.id;
let issue = issues.get(&id).unwrap().unwrap();
let assignees: Vec<_> = issue.assigned().cloned().collect::<Vec<_>>();
assert_eq!(1, assignees.len());
assert!(assignees.contains(&assignee_two));
}
@ -614,7 +653,7 @@ mod test {
let (_, signer, project) = test::setup::context(&tmp);
let mut issues = Issues::open(*signer.public_key(), &project).unwrap();
let mut issue = issues
.create("My first issue", "Blah blah blah.", &[], &signer)
.create("My first issue", "Blah blah blah.", &[], &[], &signer)
.unwrap();
issue.edit("Sorry typo", &signer).unwrap();
@ -632,7 +671,7 @@ mod test {
let (_, signer, project) = test::setup::context(&tmp);
let mut issues = Issues::open(*signer.public_key(), &project).unwrap();
let mut issue = issues
.create("My first issue", "Blah blah blah.", &[], &signer)
.create("My first issue", "Blah blah blah.", &[], &[], &signer)
.unwrap();
let comment = OpId::initial(*signer.public_key());
@ -655,7 +694,7 @@ mod test {
let author = *signer.public_key();
let mut issues = Issues::open(*signer.public_key(), &project).unwrap();
let mut issue = issues
.create("My first issue", "Blah blah blah.", &[], &signer)
.create("My first issue", "Blah blah blah.", &[], &[], &signer)
.unwrap();
let root = OpId::root(author);
@ -676,6 +715,7 @@ mod test {
issue.comment("Re: Ha. Ha. Ha.", c2, &signer).unwrap();
let issue = issues.get(&id).unwrap().unwrap();
assert_eq!(issue.replies(&c1).nth(0).unwrap().1.body(), "Re: Hi.");
assert_eq!(issue.replies(&c2).nth(0).unwrap().1.body(), "Re: Ha.");
assert_eq!(issue.replies(&c2).nth(1).unwrap().1.body(), "Re: Ha. Ha.");
@ -690,12 +730,18 @@ mod test {
let tmp = tempfile::tempdir().unwrap();
let (_, signer, project) = test::setup::context(&tmp);
let mut issues = Issues::open(*signer.public_key(), &project).unwrap();
let mut issue = issues
.create("My first issue", "Blah blah blah.", &[], &signer)
.unwrap();
let bug_tag = Tag::new("bug").unwrap();
let ux_tag = Tag::new("ux").unwrap();
let wontfix_tag = Tag::new("wontfix").unwrap();
let mut issue = issues
.create(
"My first issue",
"Blah blah blah.",
&[ux_tag.clone()],
&[],
&signer,
)
.unwrap();
issue.tag([bug_tag.clone()], [], &signer).unwrap();
issue.tag([wontfix_tag.clone()], [], &signer).unwrap();
@ -704,6 +750,7 @@ mod test {
let issue = issues.get(&id).unwrap().unwrap();
let tags = issue.tags().cloned().collect::<Vec<_>>();
assert!(tags.contains(&ux_tag));
assert!(tags.contains(&bug_tag));
assert!(tags.contains(&wontfix_tag));
}
@ -715,7 +762,7 @@ mod test {
let author = *signer.public_key();
let mut issues = Issues::open(*signer.public_key(), &project).unwrap();
let mut issue = issues
.create("My first issue", "Blah blah blah.", &[], &signer)
.create("My first issue", "Blah blah blah.", &[], &[], &signer)
.unwrap();
// The root thread op id is always the same.
@ -760,9 +807,9 @@ mod test {
let (_, signer, project) = test::setup::context(&tmp);
let mut issues = Issues::open(*signer.public_key(), &project).unwrap();
issues.create("First", "Blah", &[], &signer).unwrap();
issues.create("Second", "Blah", &[], &signer).unwrap();
issues.create("Third", "Blah", &[], &signer).unwrap();
issues.create("First", "Blah", &[], &[], &signer).unwrap();
issues.create("Second", "Blah", &[], &[], &signer).unwrap();
issues.create("Third", "Blah", &[], &[], &signer).unwrap();
let issues = issues
.all()