Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
httpd: Add `assignees` to `Issues::create`
Sebastian Martinez committed 3 years ago
commit 881e9af2191a5d545ef62f43d43f0a396e649362
parent 540c56cac6c0753d5d8a9057fc7d3596b172807a
5 files changed +103 -44
modified radicle-cli/examples/rad-issue.md
@@ -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
```
modified radicle-cli/src/commands/issue.rs
@@ -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,
                )?;
            }
modified radicle-httpd/src/api/test.rs
@@ -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();
modified radicle-httpd/src/api/v1/projects.rs
@@ -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",
                  },
modified radicle/src/cob/issue.rs
@@ -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()