cob: Stabilize formats

> I'm going to be making a set of breaking changes to COBs in order to
stabilize the data formats. This is hopefully a one-time change that
bundles various breaking changes.

All COBs have been reworked: issue, patch, id.

The changes included are:

* Revise the assign and tag actions to take a single list of
  assignees/tags to set, instead of an "add" and a "remove" list. This
  makes API usage simpler when editing issues, and simplifies the apply
  function
* Rename "tags" to "labels", and the tag action to label. This is
  because tag is confusing in the context of git, as it could mean a git
  tag. Using label removes that confusion.
* Use DIDs instead of PublicKeys for assignees -- this is more
  future-proof
* Modify the manifest file format in the COB tree. Mainly, remove the
  `history_type` key which is redundant, and use camelCase for keys
* Flatten the `Thread` actions into the parent action type
* Ensure that operations on redacted objects do not fail, since
  redactions could have happened concurrently
* Use a consistent naming scheme for actions, using `.` as separator
* Consolidate comment types and remove `CodeComment`, by adding an
  optional `location` field to `Comment`
* Add many placeholder actions that are not yet implemented

To preserve backwards compatibility, a `legacy` module is created with
the old `apply` function. When loading the manifest, we check whether it
is a legacy COB or a "stable" COB, and in the legacy case, use the
legacy code to materialize the state and then convert the object into
the stable type. Eventually, we'll delete the legacy code.
This commit is contained in:
Alexis Sellier 2023-07-26 12:32:20 +02:00 committed by cloudhead
parent 885f4999da
commit 3bd5589b1a
No known key found for this signature in database
58 changed files with 2261 additions and 1263 deletions

View File

@ -6,7 +6,7 @@ delegates creating proposals concurrently.
``` ```
$ rad id edit --title "Add Alice" --description "Add Alice as a delegate" --delegates did:key:z6MkedTZGJGqgQ2py2b8kGecfxdt2yRdHWF6JpaZC47fovFn --no-confirm $ rad id edit --title "Add Alice" --description "Add Alice as a delegate" --delegates did:key:z6MkedTZGJGqgQ2py2b8kGecfxdt2yRdHWF6JpaZC47fovFn --no-confirm
✓ Identity proposal '04603c0d3ea4d137487024a51c9360adfc511114' created ✓ Identity proposal '6b73fce909f612d5d92084b91309d73c21fea396' created
title: Add Alice title: Add Alice
description: Add Alice as a delegate description: Add Alice as a delegate
status: open status: open
@ -48,7 +48,7 @@ Quorum Reached
``` ```
$ rad id edit --title "Add Bob" --description "Add Bob as a delegate" --delegates did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG --no-confirm $ rad id edit --title "Add Bob" --description "Add Bob as a delegate" --delegates did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG --no-confirm
✓ Identity proposal '3f6ae4f8645c8b0cbcd35ea924df7b13aca52774' created ✓ Identity proposal '2bf3a85e209d10b11a65e7ed8a8085f6f18ac6bf' created
title: Add Bob title: Add Bob
description: Add Bob as a delegate description: Add Bob as a delegate
status: open status: open
@ -93,7 +93,7 @@ second proposal, then the identity would be out of date. So let's run
through that and see what happens. through that and see what happens.
``` ```
$ rad id accept 04603c0d3ea4d137487024a51c9360adfc511114 --no-confirm $ rad id accept 6b73fce909f612d5d92084b91309d73c21fea396 --no-confirm
✓ Accepted proposal ✓ ✓ Accepted proposal ✓
title: Add Alice title: Add Alice
description: Add Alice as a delegate description: Add Alice as a delegate
@ -137,7 +137,7 @@ Quorum Reached
``` ```
``` ```
$ rad id commit 04603c0d3ea4d137487024a51c9360adfc511114 --no-confirm $ rad id commit 6b73fce909f612d5d92084b91309d73c21fea396 --no-confirm
✓ Committed new identity '29ae4b72f5a315328f06fbd68dc1c396a2d5c45e' ✓ Committed new identity '29ae4b72f5a315328f06fbd68dc1c396a2d5c45e'
title: Add Alice title: Add Alice
description: Add Alice as a delegate description: Add Alice as a delegate
@ -183,7 +183,7 @@ Quorum Reached
Now, when we go to accept the second proposal: Now, when we go to accept the second proposal:
``` ```
$ rad id accept 3f6ae4f8645c8b0cbcd35ea924df7b13aca52774 --no-confirm $ rad id accept 2bf3a85e209d10b11a65e7ed8a8085f6f18ac6bf --no-confirm
! Warning: Revision is out of date ! Warning: Revision is out of date
! Warning: d96f425412c9f8ad5d9a9a05c9831d0728e2338d =/= 475cdfbc8662853dd132ec564e4f5eb0f152dd7f ! Warning: d96f425412c9f8ad5d9a9a05c9831d0728e2338d =/= 475cdfbc8662853dd132ec564e4f5eb0f152dd7f
* Consider using 'rad id rebase' to update the proposal to the latest identity * Consider using 'rad id rebase' to update the proposal to the latest identity
@ -238,19 +238,19 @@ Note that a warning was emitted:
If we attempt to commit this revision, the command will fail: If we attempt to commit this revision, the command will fail:
``` (fail) ``` (fail)
$ rad id commit 3f6ae4f8645c8b0cbcd35ea924df7b13aca52774 --no-confirm $ rad id commit 2bf3a85e209d10b11a65e7ed8a8085f6f18ac6bf --no-confirm
! Warning: Revision is out of date ! Warning: Revision is out of date
! Warning: d96f425412c9f8ad5d9a9a05c9831d0728e2338d =/= 475cdfbc8662853dd132ec564e4f5eb0f152dd7f ! Warning: d96f425412c9f8ad5d9a9a05c9831d0728e2338d =/= 475cdfbc8662853dd132ec564e4f5eb0f152dd7f
* Consider using 'rad id rebase' to update the proposal to the latest identity * Consider using 'rad id rebase' to update the proposal to the latest identity
✗ Id failed: the identity hashes do match 'd96f425412c9f8ad5d9a9a05c9831d0728e2338d =/= 475cdfbc8662853dd132ec564e4f5eb0f152dd7f' for the revision '3f6ae4f8645c8b0cbcd35ea924df7b13aca52774' ✗ Id failed: the identity hashes do not match for revision 2bf3a85e209d10b11a65e7ed8a8085f6f18ac6bf (d96f425412c9f8ad5d9a9a05c9831d0728e2338d =/= 475cdfbc8662853dd132ec564e4f5eb0f152dd7f)
``` ```
So, let's fix this by running a rebase on the proposal's revision: So, let's fix this by running a rebase on the proposal's revision:
``` ```
$ rad id rebase 3f6ae4f8645c8b0cbcd35ea924df7b13aca52774 --no-confirm $ rad id rebase 2bf3a85e209d10b11a65e7ed8a8085f6f18ac6bf --no-confirm
✓ Identity proposal '3f6ae4f8645c8b0cbcd35ea924df7b13aca52774' rebased ✓ Identity proposal '2bf3a85e209d10b11a65e7ed8a8085f6f18ac6bf' rebased
✓ Revision '42b9428df59ad349f706b1397750b75ea3b42574' ✓ Revision 'e56b3e0842a4dd37c2a997344bcb4113704e4768'
title: Add Bob title: Add Bob
description: Add Bob as a delegate description: Add Bob as a delegate
status: open status: open
@ -293,9 +293,9 @@ Quorum Reached
We can now update the proposal to have both keys in the delegates set: We can now update the proposal to have both keys in the delegates set:
``` ```
$ rad id update 3f6ae4f8645c8b0cbcd35ea924df7b13aca52774 --rev 42b9428df59ad349f706b1397750b75ea3b42574 --delegates did:key:z6MkedTZGJGqgQ2py2b8kGecfxdt2yRdHWF6JpaZC47fovFn --no-confirm $ rad id update 2bf3a85e209d10b11a65e7ed8a8085f6f18ac6bf --rev e56b3e0842a4dd37c2a997344bcb4113704e4768 --delegates did:key:z6MkedTZGJGqgQ2py2b8kGecfxdt2yRdHWF6JpaZC47fovFn --no-confirm
✓ Identity proposal '3f6ae4f8645c8b0cbcd35ea924df7b13aca52774' updated ✓ Identity proposal '2bf3a85e209d10b11a65e7ed8a8085f6f18ac6bf' updated
✓ Revision '1b4ded759249e4f76d19c3e580b4736bf2a2d1c4' ✓ Revision 'f15fc641d777cfb005caaa9405e262e516b8ac60'
title: Add Bob title: Add Bob
description: Add Bob as a delegate description: Add Bob as a delegate
status: open status: open
@ -338,10 +338,10 @@ Quorum Reached
Finally, we can accept and commit this proposal, creating the final Finally, we can accept and commit this proposal, creating the final
state of our new Radicle identity: state of our new Radicle identity:
$ rad id show 3f6ae4f8645c8b0cbcd35ea924df7b13aca52774 --revisions $ rad id show 2bf3a85e209d10b11a65e7ed8a8085f6f18ac6bf --revisions
``` ```
$ rad id accept 3f6ae4f8645c8b0cbcd35ea924df7b13aca52774 --rev 1b4ded759249e4f76d19c3e580b4736bf2a2d1c4 --no-confirm $ rad id accept 2bf3a85e209d10b11a65e7ed8a8085f6f18ac6bf --rev f15fc641d777cfb005caaa9405e262e516b8ac60 --no-confirm
✓ Accepted proposal ✓ ✓ Accepted proposal ✓
title: Add Bob title: Add Bob
description: Add Bob as a delegate description: Add Bob as a delegate
@ -385,7 +385,7 @@ Quorum Reached
``` ```
``` ```
$ rad id commit 3f6ae4f8645c8b0cbcd35ea924df7b13aca52774 --rev 1b4ded759249e4f76d19c3e580b4736bf2a2d1c4 --no-confirm $ rad id commit 2bf3a85e209d10b11a65e7ed8a8085f6f18ac6bf --rev f15fc641d777cfb005caaa9405e262e516b8ac60 --no-confirm
✓ Committed new identity '60de897bc24898f6908fd1272633c0b15aa4096f' ✓ Committed new identity '60de897bc24898f6908fd1272633c0b15aa4096f'
title: Add Bob title: Add Bob
description: Add Bob as a delegate description: Add Bob as a delegate

View File

@ -14,7 +14,7 @@ Let's add Bob as a delegate using their DID
``` ```
$ rad id edit --title "Add Bob" --description "Add Bob as a delegate" --delegates did:key:z6MkedTZGJGqgQ2py2b8kGecfxdt2yRdHWF6JpaZC47fovFn --no-confirm $ rad id edit --title "Add Bob" --description "Add Bob as a delegate" --delegates did:key:z6MkedTZGJGqgQ2py2b8kGecfxdt2yRdHWF6JpaZC47fovFn --no-confirm
✓ Identity proposal '0d396a83a5e1dda2b8929f7dc401d19dd1a79fb8' created ✓ Identity proposal '662a8065f18db50d9ee952bb36eda5b605f161e9' created
title: Add Bob title: Add Bob
description: Add Bob as a delegate description: Add Bob as a delegate
status: open status: open
@ -89,7 +89,7 @@ Finally, we can see whether the `Quorum` was reached:
Let's see what happens when we reject the change: Let's see what happens when we reject the change:
``` ```
$ rad id reject 0d396a --no-confirm $ rad id reject 662a8065f18db50d9ee952bb36eda5b605f161e9 --no-confirm
✓ Rejected proposal 👎 ✓ Rejected proposal 👎
title: Add Bob title: Add Bob
description: Add Bob as a delegate description: Add Bob as a delegate
@ -145,7 +145,7 @@ increased to `1`.
Instead, let's accept the proposal: Instead, let's accept the proposal:
``` ```
$ rad id accept 0d396a --no-confirm $ rad id accept 662a8065f18db50d9ee952bb36eda5b605f161e9 --no-confirm
✓ Accepted proposal ✓ ✓ Accepted proposal ✓
title: Add Bob title: Add Bob
description: Add Bob as a delegate description: Add Bob as a delegate
@ -207,7 +207,7 @@ As well as that, the `Quorum` has now been reached:
At this point, we can commit the proposal and update the identity: At this point, we can commit the proposal and update the identity:
``` ```
$ rad id commit 0d396a --no-confirm $ rad id commit 662a8065f18db50d9ee952bb36eda5b605f161e9 --no-confirm
✓ Committed new identity 'c96e764965aaeff1c6ea3e5b97e2b9828773c8b0' ✓ Committed new identity 'c96e764965aaeff1c6ea3e5b97e2b9828773c8b0'
title: Add Bob title: Add Bob
description: Add Bob as a delegate description: Add Bob as a delegate
@ -255,7 +255,7 @@ the `--threshold` option:
``` ```
$ rad id edit --title "Update threshold" --description "Update to safer threshold" --threshold 2 --no-confirm $ rad id edit --title "Update threshold" --description "Update to safer threshold" --threshold 2 --no-confirm
✓ Identity proposal 'f435d6e89c8f922ede691287c0d8b7f82afa591e' created ✓ Identity proposal 'e6c04862ed0e59739f34232c8690cbad73840a93' created
title: Update threshold title: Update threshold
description: Update to safer threshold description: Update to safer threshold
status: open status: open
@ -298,8 +298,8 @@ Quorum Reached
But we change our minds and decide to close the proposal instead: But we change our minds and decide to close the proposal instead:
``` ```
$ rad id close f435d6 --no-confirm $ rad id close e6c04862ed0e59739f34232c8690cbad73840a93 --no-confirm
✓ Closed identity proposal 'f435d6e89c8f922ede691287c0d8b7f82afa591e' ✓ Closed identity proposal 'e6c04862ed0e59739f34232c8690cbad73840a93'
title: Update threshold title: Update threshold
description: Update to safer threshold description: Update to safer threshold
status: closed status: closed
@ -348,15 +348,15 @@ Radicle identity, then we can use the list command:
``` ```
$ rad id list $ rad id list
0d396a83a5e1dda2b8929f7dc401d19dd1a79fb8 "Add Bob" committed 662a8065f18db50d9ee952bb36eda5b605f161e9 "Add Bob" committed
f435d6e89c8f922ede691287c0d8b7f82afa591e "Update threshold" closed e6c04862ed0e59739f34232c8690cbad73840a93 "Update threshold" closed
``` ```
And if we want to view the latest state of any proposal we can use the And if we want to view the latest state of any proposal we can use the
show command: show command:
``` ```
$ rad id show f435d6 $ rad id show e6c04862ed0e59739f34232c8690cbad73840a93
title: Update threshold title: Update threshold
description: Update to safer threshold description: Update to safer threshold
status: closed status: closed

View File

@ -7,7 +7,7 @@ Let's say the new car you are designing with your peers has a problem with its f
$ rad issue open --title "flux capacitor underpowered" --description "Flux capacitor power requirements exceed current supply" --no-announce $ rad issue open --title "flux capacitor underpowered" --description "Flux capacitor power requirements exceed current supply" --no-announce
╭─────────────────────────────────────────────────────────╮ ╭─────────────────────────────────────────────────────────╮
│ Title flux capacitor underpowered │ │ Title flux capacitor underpowered │
│ Issue 2e8c1bf3fe0532a314778357c886608a966a34bd │ Issue 42028af21fabc09bfac2f25490f119f7c7e11542
│ Status open │ │ Status open │
│ │ │ │
│ Flux capacitor power requirements exceed current supply │ │ Flux capacitor power requirements exceed current supply │
@ -18,20 +18,20 @@ The issue is now listed under our project.
``` ```
$ rad issue list $ rad issue list
╭───────────────────────────────────────────────────────────────────────────────────────────────────────╮ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────
│ ● ID Title Author Tags Assignees Opened │ │ ● ID Title Author Labels Assignees Opened │
├───────────────────────────────────────────────────────────────────────────────────────────────────────┤ ├─────────────────────────────────────────────────────────────────────────────────────────────────────────
│ ● 2e8c1bf flux capacitor underpowered z6MknSL…StBU8Vi (you) [ .. ] │ │ ● 42028af flux capacitor underpowered z6MknSL…StBU8Vi (you) [ .. ] │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────
``` ```
Show the issue information issue. Show the issue information issue.
``` ```
$ rad issue show 2e8c1bf $ rad issue show 42028af
╭─────────────────────────────────────────────────────────╮ ╭─────────────────────────────────────────────────────────╮
│ Title flux capacitor underpowered │ │ Title flux capacitor underpowered │
│ Issue 2e8c1bf3fe0532a314778357c886608a966a34bd │ Issue 42028af21fabc09bfac2f25490f119f7c7e11542
│ Status open │ │ Status open │
│ │ │ │
│ Flux capacitor power requirements exceed current supply │ │ Flux capacitor power requirements exceed current supply │
@ -47,24 +47,24 @@ 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.
``` ```
$ rad assign 2e8c1bf3fe0532a314778357c886608a966a34bd --to did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi $ rad assign 42028af21fabc09bfac2f25490f119f7c7e11542 --to did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
``` ```
It will now show in the list of issues assigned to us. It will now show in the list of issues assigned to us.
``` ```
$ rad issue list --assigned $ rad issue list --assigned
╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────
│ ● ID Title Author Tags Assignees Opened │ │ ● ID Title Author Labels Assignees Opened │
├─────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ ├───────────────────────────────────────────────────────────────────────────────────────────────────────────────
│ ● 2e8c1bf flux capacitor underpowered z6MknSL…StBU8Vi (you) z6MknSL…StBU8Vi [ .. ] │ │ ● 42028af flux capacitor underpowered z6MknSL…StBU8Vi (you) z6MknSL…StBU8Vi [ .. ] │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────
``` ```
Note: this can always be undone with the `unassign` subcommand. Note: this can always be undone with the `unassign` subcommand.
``` ```
$ rad unassign 2e8c1bf3fe0532a314778357c886608a966a34bd --from did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi $ rad unassign 42028af21fabc09bfac2f25490f119f7c7e11542 --from did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
``` ```
Great, now we have communicated to the world about our car's defect. Great, now we have communicated to the world about our car's defect.
@ -73,8 +73,8 @@ But wait! We've found an important detail about the car's power requirements.
It will help whoever works on a fix. It will help whoever works on a fix.
``` ```
$ rad comment 2e8c1bf3fe0532a314778357c886608a966a34bd --message 'The flux capacitor needs 1.21 Gigawatts' $ rad comment 42028af21fabc09bfac2f25490f119f7c7e11542 --message 'The flux capacitor needs 1.21 Gigawatts'
9822748bd076595a2408aad02b3a0d9f94fec7e0 84492237dc0908b1e5b728d1a4e5f1343b6ffe9b
$ rad comment 2e8c1bf3fe0532a314778357c886608a966a34bd --reply-to 9822748bd076595a2408aad02b3a0d9f94fec7e0 --message 'More power!' $ rad comment 42028af21fabc09bfac2f25490f119f7c7e11542 --reply-to 84492237dc0908b1e5b728d1a4e5f1343b6ffe9b --message 'More power!'
edec8d07bf3788b98943394c1274910b8f12d35c dd679552a15e2db73bbedf3084f5f7c62bb0d724
``` ```

View File

@ -1,18 +1,18 @@
Tagging an issue is easy, let's add the `bug` and `good-first-issue` tags to Labeling an issue is easy, let's add the `bug` and `good-first-issue` labels to
some issue: some issue:
``` ```
$ rad tag 2e8c1bf3fe0532a314778357c886608a966a34bd bug good-first-issue $ rad label 42028af21fabc09bfac2f25490f119f7c7e11542 bug good-first-issue
``` ```
We can now show the issue to check whether those tags were added: We can now show the issue to check whether those labels were added:
``` ```
$ rad issue show 2e8c1bf3fe0532a314778357c886608a966a34bd $ rad issue show 42028af21fabc09bfac2f25490f119f7c7e11542
╭─────────────────────────────────────────────────────────╮ ╭─────────────────────────────────────────────────────────╮
│ Title flux capacitor underpowered │ │ Title flux capacitor underpowered │
│ Issue 2e8c1bf3fe0532a314778357c886608a966a34bd │ Issue 42028af21fabc09bfac2f25490f119f7c7e11542
Tags bug, good-first-issue │ Labels bug, good-first-issue │
│ Status open │ │ Status open │
│ │ │ │
│ Flux capacitor power requirements exceed current supply │ │ Flux capacitor power requirements exceed current supply │
@ -22,17 +22,17 @@ $ rad issue show 2e8c1bf3fe0532a314778357c886608a966a34bd
Untagging an issue is very similar: Untagging an issue is very similar:
``` ```
$ rad untag 2e8c1bf3fe0532a314778357c886608a966a34bd good-first-issue $ rad unlabel 42028af21fabc09bfac2f25490f119f7c7e11542 good-first-issue
``` ```
Notice that the `good-first-issue` tag has disappeared: Notice that the `good-first-issue` label has disappeared:
``` ```
$ rad issue show 2e8c1bf3fe0532a314778357c886608a966a34bd $ rad issue show 42028af21fabc09bfac2f25490f119f7c7e11542
╭─────────────────────────────────────────────────────────╮ ╭─────────────────────────────────────────────────────────╮
│ Title flux capacitor underpowered │ │ Title flux capacitor underpowered │
│ Issue 2e8c1bf3fe0532a314778357c886608a966a34bd │ Issue 42028af21fabc09bfac2f25490f119f7c7e11542
Tags bug │ Labels bug │
│ Status open │ │ Status open │
│ │ │ │
│ Flux capacitor power requirements exceed current supply │ │ Flux capacitor power requirements exceed current supply │

View File

@ -4,7 +4,7 @@ Let's start by creating a patch.
$ git checkout -b feature/1 -q $ git checkout -b feature/1 -q
$ git commit --allow-empty -q -m "First change" $ git commit --allow-empty -q -m "First change"
$ git push rad HEAD:refs/patches $ git push rad HEAD:refs/patches
✓ Patch 0ec956c94256fa101db4c32956ce195a1aa0edf2 opened ✓ Patch 143bb0c962561b09e86478a53ba346b5ff934335 opened
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
* [new reference] HEAD -> refs/patches * [new reference] HEAD -> refs/patches
``` ```
@ -26,8 +26,8 @@ update it, we expect it to be updated and merged:
``` (stderr) RAD_SOCKET=/dev/null ``` (stderr) RAD_SOCKET=/dev/null
$ git checkout feature/1 -q $ git checkout feature/1 -q
$ git push -f $ git push -f
✓ Patch 0ec956c updated to 8175b00f4d75059976930cfcb75ef08454c87055 ✓ Patch 143bb0c updated to e595bf1246bdcee7b0c20615e479f62d2bf02249
✓ Patch 0ec956c94256fa101db4c32956ce195a1aa0edf2 merged ✓ Patch 143bb0c962561b09e86478a53ba346b5ff934335 merged
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+ 20aa5dd...954bcdb feature/1 -> patches/0ec956c94256fa101db4c32956ce195a1aa0edf2 (forced update) + 20aa5dd...954bcdb feature/1 -> patches/143bb0c962561b09e86478a53ba346b5ff934335 (forced update)
``` ```

View File

@ -4,7 +4,7 @@ Let's start by creating two patches.
$ git checkout -b feature/1 -q $ git checkout -b feature/1 -q
$ git commit --allow-empty -q -m "First change" $ git commit --allow-empty -q -m "First change"
$ git push rad HEAD:refs/patches $ git push rad HEAD:refs/patches
✓ Patch 0ec956c94256fa101db4c32956ce195a1aa0edf2 opened ✓ Patch 143bb0c962561b09e86478a53ba346b5ff934335 opened
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
* [new reference] HEAD -> refs/patches * [new reference] HEAD -> refs/patches
``` ```
@ -12,7 +12,7 @@ To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkE
$ git checkout -b feature/2 -q master $ git checkout -b feature/2 -q master
$ git commit --allow-empty -q -m "Second change" $ git commit --allow-empty -q -m "Second change"
$ git push rad HEAD:refs/patches $ git push rad HEAD:refs/patches
✓ Patch 928d76e22ef98a8406f2e4e4bcc8878533bbdfe0 opened ✓ Patch 5d0e608aa35af59f769e9d6a2c0227ea60ae2740 opened
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
* [new reference] HEAD -> refs/patches * [new reference] HEAD -> refs/patches
``` ```
@ -22,8 +22,8 @@ This creates some remote tracking branches for us:
``` ```
$ git branch -r $ git branch -r
rad/master rad/master
rad/patches/0ec956c94256fa101db4c32956ce195a1aa0edf2 rad/patches/143bb0c962561b09e86478a53ba346b5ff934335
rad/patches/928d76e22ef98a8406f2e4e4bcc8878533bbdfe0 rad/patches/5d0e608aa35af59f769e9d6a2c0227ea60ae2740
``` ```
And some remote refs: And some remote refs:
@ -34,13 +34,13 @@ z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
└── refs └── refs
   ├── cobs    ├── cobs
   │   └── xyz.radicle.patch    │   └── xyz.radicle.patch
   │      ├── 0ec956c94256fa101db4c32956ce195a1aa0edf2    │      ├── 143bb0c962561b09e86478a53ba346b5ff934335
   │      └── 928d76e22ef98a8406f2e4e4bcc8878533bbdfe0    │      └── 5d0e608aa35af59f769e9d6a2c0227ea60ae2740
   ├── heads    ├── heads
   │   ├── master    │   ├── master
   │   └── patches    │   └── patches
   │      ├── 0ec956c94256fa101db4c32956ce195a1aa0edf2    │      ├── 143bb0c962561b09e86478a53ba346b5ff934335
   │      └── 928d76e22ef98a8406f2e4e4bcc8878533bbdfe0    │      └── 5d0e608aa35af59f769e9d6a2c0227ea60ae2740
   └── rad    └── rad
      ├── id       ├── id
      └── sigrefs       └── sigrefs
@ -59,8 +59,8 @@ When we push to `rad/master`, we automatically merge the patches:
``` (stderr) RAD_SOCKET=/dev/null ``` (stderr) RAD_SOCKET=/dev/null
$ git push rad master $ git push rad master
✓ Patch 0ec956c94256fa101db4c32956ce195a1aa0edf2 merged ✓ Patch 143bb0c962561b09e86478a53ba346b5ff934335 merged
✓ Patch 928d76e22ef98a8406f2e4e4bcc8878533bbdfe0 merged ✓ Patch 5d0e608aa35af59f769e9d6a2c0227ea60ae2740 merged
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
f2de534..d6399c7 master -> master f2de534..d6399c7 master -> master
``` ```
@ -69,8 +69,8 @@ $ rad patch --merged
╭────────────────────────────────────────────────────────────────────────────────────────╮ ╭────────────────────────────────────────────────────────────────────────────────────────╮
│ ● ID Title Author Head + - Updated │ │ ● ID Title Author Head + - Updated │
├────────────────────────────────────────────────────────────────────────────────────────┤ ├────────────────────────────────────────────────────────────────────────────────────────┤
│ ✔ 0ec956c First change z6MknSL…StBU8Vi alice (you) 20aa5dd +0 -0 [ ... ] │ │ ✔ 143bb0c First change z6MknSL…StBU8Vi alice (you) 20aa5dd +0 -0 [ ... ] │
│ ✔ 928d76e Second change z6MknSL…StBU8Vi alice (you) daf349f +0 -0 [ ... ] │ │ ✔ 5d0e608 Second change z6MknSL…StBU8Vi alice (you) daf349f +0 -0 [ ... ] │
╰────────────────────────────────────────────────────────────────────────────────────────╯ ╰────────────────────────────────────────────────────────────────────────────────────────╯
``` ```
@ -89,8 +89,8 @@ z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
└── refs └── refs
   ├── cobs    ├── cobs
   │   └── xyz.radicle.patch    │   └── xyz.radicle.patch
   │      ├── 0ec956c94256fa101db4c32956ce195a1aa0edf2    │      ├── 143bb0c962561b09e86478a53ba346b5ff934335
   │      └── 928d76e22ef98a8406f2e4e4bcc8878533bbdfe0    │      └── 5d0e608aa35af59f769e9d6a2c0227ea60ae2740
   ├── heads    ├── heads
   │   └── master    │   └── master
   └── rad    └── rad

View File

@ -37,7 +37,7 @@ $ git log --graph --decorate --abbrev-commit --pretty=oneline --all
Then we create a patch from `feature/1`: Then we create a patch from `feature/1`:
``` (stderr) ``` (stderr)
$ git push rad feature/1:refs/patches $ git push rad feature/1:refs/patches
✓ Patch 866f59c001cd4d78a151f444b34265566c83c264 opened ✓ Patch 69ebafb6f654fb29d23f630cc165d83d6cbf525c opened
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
* [new reference] feature/1 -> refs/patches * [new reference] feature/1 -> refs/patches
``` ```
@ -48,17 +48,17 @@ $ rad patch list
╭─────────────────────────────────────────────────────────────────────────────╮ ╭─────────────────────────────────────────────────────────────────────────────╮
│ ● ID Title Author Head + - Updated │ │ ● ID Title Author Head + - Updated │
├─────────────────────────────────────────────────────────────────────────────┤ ├─────────────────────────────────────────────────────────────────────────────┤
│ ● 866f59c Add Alan z6MknSL…StBU8Vi (you) 5c88a79 +1 -0 [ ... ] │ │ ● 69ebafb Add Alan z6MknSL…StBU8Vi (you) 5c88a79 +1 -0 [ ... ] │
╰─────────────────────────────────────────────────────────────────────────────╯ ╰─────────────────────────────────────────────────────────────────────────────╯
``` ```
When showing the patch, we see that it is `ahead 1, behind 1`, since master has When showing the patch, we see that it is `ahead 1, behind 1`, since master has
diverged by one commit: diverged by one commit:
``` ```
$ rad patch show -v -p 866f59c $ rad patch show -v -p 69ebafb
╭────────────────────────────────────────────────────────────────────╮ ╭────────────────────────────────────────────────────────────────────╮
│ Title Add Alan │ │ Title Add Alan │
│ Patch 866f59c001cd4d78a151f444b34265566c83c264 │ Patch 69ebafb6f654fb29d23f630cc165d83d6cbf525c
│ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │ │ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │
│ Head 5c88a79d75f5c2b4cc51ee6f163d2db91ee198d7 │ │ Head 5c88a79d75f5c2b4cc51ee6f163d2db91ee198d7 │
│ Base f64fb2c8fe28f7c458c72ec8d700373924794943 │ │ Base f64fb2c8fe28f7c458c72ec8d700373924794943 │
@ -93,7 +93,7 @@ $ git checkout -q -b feature/2 feature/1
$ sed -i '$a Mel Farna' CONTRIBUTORS $ sed -i '$a Mel Farna' CONTRIBUTORS
$ git commit -a -q -m "Add Mel" $ git commit -a -q -m "Add Mel"
$ git push -o patch.message="Add Mel" rad HEAD:refs/patches $ git push -o patch.message="Add Mel" rad HEAD:refs/patches
✓ Patch 57cb9b2758518e547de324456ac967fda456c6c1 opened ✓ Patch 53d5f17aba5fd9b7de7a02ecb6f01de561701eeb opened
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
* [new reference] HEAD -> refs/patches * [new reference] HEAD -> refs/patches
``` ```
@ -101,10 +101,10 @@ To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkE
When we look at the patch, we see that it has both commits, because this new When we look at the patch, we see that it has both commits, because this new
patch uses the same base as the previous patch: patch uses the same base as the previous patch:
``` ```
$ rad patch show -v 57cb9b2758518e547de324456ac967fda456c6c1 $ rad patch show -v 53d5f17aba5fd9b7de7a02ecb6f01de561701eeb
╭────────────────────────────────────────────────────────────────────╮ ╭────────────────────────────────────────────────────────────────────╮
│ Title Add Mel │ │ Title Add Mel │
│ Patch 57cb9b2758518e547de324456ac967fda456c6c1 │ Patch 53d5f17aba5fd9b7de7a02ecb6f01de561701eeb
│ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │ │ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │
│ Head 7f63fcbcf23fc39eea784c091ad3d20d7e4bd005 │ │ Head 7f63fcbcf23fc39eea784c091ad3d20d7e4bd005 │
│ Base f64fb2c8fe28f7c458c72ec8d700373924794943 │ │ Base f64fb2c8fe28f7c458c72ec8d700373924794943 │
@ -124,7 +124,7 @@ If we want to instead create a "stacked" patch, we can do so with the
``` (stderr) ``` (stderr)
$ git push -o patch.message="Add Mel #2" -o patch.base=5c88a79d75f5c2b4cc51ee6f163d2db91ee198d7 rad HEAD:refs/patches $ git push -o patch.message="Add Mel #2" -o patch.base=5c88a79d75f5c2b4cc51ee6f163d2db91ee198d7 rad HEAD:refs/patches
✓ Patch 395221c5b75fa9bc2de7909d03e69dfd606611c6 opened ✓ Patch 459dc67a024ff30c3bca02f0f1e5b746459ce32a opened
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
* [new reference] HEAD -> refs/patches * [new reference] HEAD -> refs/patches
``` ```
@ -136,10 +136,10 @@ However, since the patch is still intended to be merged into `master`, we see
that it is still two commits ahead and one behind from `master`. that it is still two commits ahead and one behind from `master`.
``` ```
$ rad patch show -v 395221c5b75fa9bc2de7909d03e69dfd606611c6 $ rad patch show -v 459dc67a024ff30c3bca02f0f1e5b746459ce32a
╭────────────────────────────────────────────────────────────────────╮ ╭────────────────────────────────────────────────────────────────────╮
│ Title Add Mel #2 │ Title Add Mel #2
│ Patch 395221c5b75fa9bc2de7909d03e69dfd606611c6 │ Patch 459dc67a024ff30c3bca02f0f1e5b746459ce32a
│ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │ │ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │
│ Head 7f63fcbcf23fc39eea784c091ad3d20d7e4bd005 │ │ Head 7f63fcbcf23fc39eea784c091ad3d20d7e4bd005 │
│ Base 5c88a79d75f5c2b4cc51ee6f163d2db91ee198d7 │ │ Base 5c88a79d75f5c2b4cc51ee6f163d2db91ee198d7 │

View File

@ -9,7 +9,7 @@ To open a patch in draft mode, we use the `--draft` option:
``` (stderr) ``` (stderr)
$ git push -o patch.draft -o patch.message="Nothing yet" rad HEAD:refs/patches $ git push -o patch.draft -o patch.message="Nothing yet" rad HEAD:refs/patches
✓ Patch c639a0f9895a0fdf2ba2d04533290937cb6fd2f7 drafted ✓ Patch 79a1a5138b7f91c6dead5544ecde285dc3d0cb45 drafted
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
* [new reference] HEAD -> refs/patches * [new reference] HEAD -> refs/patches
``` ```
@ -17,10 +17,10 @@ To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkE
We can confirm it's a draft by running `show`: We can confirm it's a draft by running `show`:
``` ```
$ rad patch show c639a0f9895a0fdf2ba2d04533290937cb6fd2f7 $ rad patch show 79a1a5138b7f91c6dead5544ecde285dc3d0cb45
╭────────────────────────────────────────────────────────────────────╮ ╭────────────────────────────────────────────────────────────────────╮
│ Title Nothing yet │ │ Title Nothing yet │
│ Patch c639a0f9895a0fdf2ba2d04533290937cb6fd2f7 │ Patch 79a1a5138b7f91c6dead5544ecde285dc3d0cb45
│ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │ │ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │
│ Head 2a465832b5a76abe25be44a3a5d224bbd7741ba7 │ │ Head 2a465832b5a76abe25be44a3a5d224bbd7741ba7 │
│ Branches cloudhead/draft │ │ Branches cloudhead/draft │
@ -36,14 +36,14 @@ $ rad patch show c639a0f9895a0fdf2ba2d04533290937cb6fd2f7
Once the patch is ready for review, we can use the `ready` command: Once the patch is ready for review, we can use the `ready` command:
``` ```
$ rad patch ready c639a0f9895a0fdf2ba2d04533290937cb6fd2f7 $ rad patch ready 79a1a5138b7f91c6dead5544ecde285dc3d0cb45
``` ```
``` ```
$ rad patch show c639a0f9895a0fdf2ba2d04533290937cb6fd2f7 $ rad patch show 79a1a5138b7f91c6dead5544ecde285dc3d0cb45
╭────────────────────────────────────────────────────────────────────╮ ╭────────────────────────────────────────────────────────────────────╮
│ Title Nothing yet │ │ Title Nothing yet │
│ Patch c639a0f9895a0fdf2ba2d04533290937cb6fd2f7 │ Patch 79a1a5138b7f91c6dead5544ecde285dc3d0cb45
│ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │ │ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │
│ Head 2a465832b5a76abe25be44a3a5d224bbd7741ba7 │ │ Head 2a465832b5a76abe25be44a3a5d224bbd7741ba7 │
│ Branches cloudhead/draft │ │ Branches cloudhead/draft │
@ -60,11 +60,11 @@ If for whatever reason, it needed to go back into draft mode, we could use
the `--undo` flag: the `--undo` flag:
``` ```
$ rad patch ready --undo c639a0f9895a0fdf2ba2d04533290937cb6fd2f7 $ rad patch ready --undo 79a1a5138b7f91c6dead5544ecde285dc3d0cb45
$ rad patch show c639a0f9895a0fdf2ba2d04533290937cb6fd2f7 $ rad patch show 79a1a5138b7f91c6dead5544ecde285dc3d0cb45
╭─────────────────────────────────────────────────────────────────────────────────────────╮ ╭─────────────────────────────────────────────────────────────────────────────────────────╮
│ Title Nothing yet │ │ Title Nothing yet │
│ Patch c639a0f9895a0fdf2ba2d04533290937cb6fd2f7 │ Patch 79a1a5138b7f91c6dead5544ecde285dc3d0cb45
│ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │ │ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │
│ Head 2a465832b5a76abe25be44a3a5d224bbd7741ba7 │ │ Head 2a465832b5a76abe25be44a3a5d224bbd7741ba7 │
│ Branches cloudhead/draft │ │ Branches cloudhead/draft │

View File

@ -40,22 +40,22 @@ $ cd heartwood
$ git checkout -b bob/feature -q $ git checkout -b bob/feature -q
$ git commit --allow-empty -m "Bob's commit #1" -q $ git commit --allow-empty -m "Bob's commit #1" -q
$ git push rad -o sync -o patch.message="Bob's patch" HEAD:refs/patches $ git push rad -o sync -o patch.message="Bob's patch" HEAD:refs/patches
✓ Patch 627477fdb46b9aaf3f0677c415b569cd21227b76 opened ✓ Patch 26e3e563ddc7df8dd0c9f81274c0b3cb1b764568 opened
✓ Synced with 1 node(s) ✓ Synced with 1 node(s)
To rad://zhbMU4DUXrzB8xT6qAJh6yZ7bFMK/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk To rad://zhbMU4DUXrzB8xT6qAJh6yZ7bFMK/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
* [new reference] HEAD -> refs/patches * [new reference] HEAD -> refs/patches
``` ```
``` ~bob ``` ~bob
$ git status --short --branch $ git status --short --branch
## bob/feature...rad/patches/627477fdb46b9aaf3f0677c415b569cd21227b76 ## bob/feature...rad/patches/26e3e563ddc7df8dd0c9f81274c0b3cb1b764568
``` ```
Alice checks it out. Alice checks it out.
``` ~alice ``` ~alice
$ rad patch checkout 627477f $ rad patch checkout 26e3e56
✓ Switched to branch patch/627477f ✓ Switched to branch patch/26e3e56
✓ Branch patch/627477f setup to track rad/patches/627477fdb46b9aaf3f0677c415b569cd21227b76 ✓ Branch patch/26e3e56 setup to track rad/patches/26e3e563ddc7df8dd0c9f81274c0b3cb1b764568
$ git show $ git show
commit bdcdb30b3c0f513620dd0f1c24ff8f4f71de956b commit bdcdb30b3c0f513620dd0f1c24ff8f4f71de956b
Author: radicle <radicle@localhost> Author: radicle <radicle@localhost>
@ -69,19 +69,19 @@ Bob then updates the patch.
``` ~bob (stderr) ``` ~bob (stderr)
$ git commit --allow-empty -m "Bob's commit #2" -q $ git commit --allow-empty -m "Bob's commit #2" -q
$ git push rad -o sync -o patch.message="Updated." $ git push rad -o sync -o patch.message="Updated."
✓ Patch 627477f updated to c4114446af35501300c68571cfb07a6f5c7e1eef ✓ Patch 26e3e56 updated to c04ef81bad734c65a7d5834cefcdd60c4f0484f7
✓ Synced with 1 node(s) ✓ Synced with 1 node(s)
To rad://zhbMU4DUXrzB8xT6qAJh6yZ7bFMK/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk To rad://zhbMU4DUXrzB8xT6qAJh6yZ7bFMK/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
bdcdb30..cad2666 bob/feature -> patches/627477fdb46b9aaf3f0677c415b569cd21227b76 bdcdb30..cad2666 bob/feature -> patches/26e3e563ddc7df8dd0c9f81274c0b3cb1b764568
``` ```
Alice pulls the update. Alice pulls the update.
``` ~alice ``` ~alice
$ rad patch show 627477f $ rad patch show 26e3e56
╭──────────────────────────────────────────────────────────────────────────────╮ ╭──────────────────────────────────────────────────────────────────────────────╮
│ Title Bob's patch │ │ Title Bob's patch │
│ Patch 627477fdb46b9aaf3f0677c415b569cd21227b76 │ Patch 26e3e563ddc7df8dd0c9f81274c0b3cb1b764568
│ Author did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk │ │ Author did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk │
│ Head cad2666a8a2250e4dee175ed5044be2c251ff08b │ │ Head cad2666a8a2250e4dee175ed5044be2c251ff08b │
│ Commits ahead 2, behind 0 │ │ Commits ahead 2, behind 0 │
@ -91,16 +91,16 @@ $ rad patch show 627477f
│ bdcdb30 Bob's commit #1 │ bdcdb30 Bob's commit #1
├──────────────────────────────────────────────────────────────────────────────┤ ├──────────────────────────────────────────────────────────────────────────────┤
│ ● opened by bob (z6Mkt67…v4N1tRk) [ ... ] │ │ ● opened by bob (z6Mkt67…v4N1tRk) [ ... ] │
│ ↑ updated to c4114446af35501300c68571cfb07a6f5c7e1eef (cad2666) [ ... ] │ │ ↑ updated to c04ef81bad734c65a7d5834cefcdd60c4f0484f7 (cad2666) [ ... ] │
╰──────────────────────────────────────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────────────────────────────╯
$ git ls-remote rad $ git ls-remote rad
f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354 refs/heads/master f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354 refs/heads/master
cad2666a8a2250e4dee175ed5044be2c251ff08b refs/heads/patches/627477fdb46b9aaf3f0677c415b569cd21227b76 cad2666a8a2250e4dee175ed5044be2c251ff08b refs/heads/patches/26e3e563ddc7df8dd0c9f81274c0b3cb1b764568
``` ```
``` ~alice ``` ~alice
$ git fetch rad $ git fetch rad
$ git status --short --branch $ git status --short --branch
## patch/627477f...rad/patches/627477fdb46b9aaf3f0677c415b569cd21227b76 [behind 1] ## patch/26e3e56...rad/patches/26e3e563ddc7df8dd0c9f81274c0b3cb1b764568 [behind 1]
``` ```
``` ~alice ``` ~alice
$ git pull $ git pull

View File

@ -6,16 +6,16 @@ $ git commit -q -m "Not a real change" --allow-empty
``` ```
``` (stderr) ``` (stderr)
$ git push rad HEAD:refs/patches $ git push rad HEAD:refs/patches
✓ Patch 51e0d0bc168ccdc541b7b1aeab2eb9e048c2fcdd opened ✓ Patch ea6fa6c274c55d0f4fdf203a192cbf1330b51221 opened
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
* [new reference] HEAD -> refs/patches * [new reference] HEAD -> refs/patches
``` ```
``` ```
$ rad patch show 51e0d0bc168ccdc541b7b1aeab2eb9e048c2fcdd $ rad patch show ea6fa6c274c55d0f4fdf203a192cbf1330b51221
╭────────────────────────────────────────────────────────────────────╮ ╭────────────────────────────────────────────────────────────────────╮
│ Title Not a real change │ │ Title Not a real change │
│ Patch 51e0d0bc168ccdc541b7b1aeab2eb9e048c2fcdd │ Patch ea6fa6c274c55d0f4fdf203a192cbf1330b51221
│ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │ │ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │
│ Head 51b2f0f77b9849bfaa3e9d3ff68ee2f57771d20c │ │ Head 51b2f0f77b9849bfaa3e9d3ff68ee2f57771d20c │
│ Branches feature/1 │ │ Branches feature/1 │
@ -46,17 +46,17 @@ Now, instead of using `git push` to update the patch, as we normally would,
we run: we run:
``` ```
$ rad patch update 51e0d0bc168ccdc541b7b1aeab2eb9e048c2fcdd -m "Updated patch" $ rad patch update ea6fa6c274c55d0f4fdf203a192cbf1330b51221 -m "Updated patch"
c10012c2cb9c0c9bfeba7ef28cae10e4b8db3469 59bbb5c5d3c9f18a686113e6354b1372eebafda4
``` ```
The command outputs the new Revision ID, which we can now see here: The command outputs the new Revision ID, which we can now see here:
``` ```
$ rad patch show 51e0d0bc168ccdc541b7b1aeab2eb9e048c2fcdd $ rad patch show ea6fa6c274c55d0f4fdf203a192cbf1330b51221
╭──────────────────────────────────────────────────────────────────────────────╮ ╭──────────────────────────────────────────────────────────────────────────────╮
│ Title Not a real change │ │ Title Not a real change │
│ Patch 51e0d0bc168ccdc541b7b1aeab2eb9e048c2fcdd │ Patch ea6fa6c274c55d0f4fdf203a192cbf1330b51221
│ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │ │ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │
│ Head 4d272148458a17620541555b1f0905c01658aa9f │ │ Head 4d272148458a17620541555b1f0905c01658aa9f │
│ Branches feature/1 │ │ Branches feature/1 │
@ -67,6 +67,6 @@ $ rad patch show 51e0d0bc168ccdc541b7b1aeab2eb9e048c2fcdd
│ 51b2f0f Not a real change │ │ 51b2f0f Not a real change │
├──────────────────────────────────────────────────────────────────────────────┤ ├──────────────────────────────────────────────────────────────────────────────┤
│ ● opened by (you) [ ... ] │ │ ● opened by (you) [ ... ] │
│ ↑ updated to c10012c2cb9c0c9bfeba7ef28cae10e4b8db3469 (4d27214) [ ... ] │ │ ↑ updated to 59bbb5c5d3c9f18a686113e6354b1372eebafda4 (4d27214) [ ... ] │
╰──────────────────────────────────────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────────────────────────────╯
``` ```

View File

@ -8,7 +8,7 @@ $ git checkout -b feature/1
Switched to a new branch 'feature/1' Switched to a new branch 'feature/1'
$ git commit -a -m "Add things" -q --allow-empty $ git commit -a -m "Add things" -q --allow-empty
$ git push -o patch.message="Add things #1" -o patch.message="See commits for details." rad HEAD:refs/patches $ git push -o patch.message="Add things #1" -o patch.message="See commits for details." rad HEAD:refs/patches
✓ Patch 2647168c23e7c2b2c1936d695443944e143bc3f7 opened ✓ Patch 90c77f2c33b7e472e058de4a586156f8a7fec7d6 opened
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
* [new reference] HEAD -> refs/patches * [new reference] HEAD -> refs/patches
``` ```
@ -16,10 +16,10 @@ To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkE
We can see a patch was created: We can see a patch was created:
``` ```
$ rad patch show 2647168 $ rad patch show 90c77f2
╭────────────────────────────────────────────────────────────────────╮ ╭────────────────────────────────────────────────────────────────────╮
│ Title Add things #1 │ Title Add things #1
│ Patch 2647168c23e7c2b2c1936d695443944e143bc3f7 │ Patch 90c77f2c33b7e472e058de4a586156f8a7fec7d6
│ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │ │ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │
│ Head 42d894a83c9c356552a57af09ccdbd5587a99045 │ │ Head 42d894a83c9c356552a57af09ccdbd5587a99045 │
│ Branches feature/1 │ │ Branches feature/1 │
@ -39,7 +39,7 @@ branch associated with this patch:
``` ```
$ git branch -vv $ git branch -vv
* feature/1 42d894a [rad/patches/2647168c23e7c2b2c1936d695443944e143bc3f7] Add things * feature/1 42d894a [rad/patches/90c77f2c33b7e472e058de4a586156f8a7fec7d6] Add things
master f2de534 [rad/master] Second commit master f2de534 [rad/master] Second commit
``` ```
@ -47,7 +47,7 @@ Let's check that it's up to date with our local head:
``` ```
$ git status --short --branch $ git status --short --branch
## feature/1...rad/patches/2647168c23e7c2b2c1936d695443944e143bc3f7 ## feature/1...rad/patches/90c77f2c33b7e472e058de4a586156f8a7fec7d6
$ git fetch $ git fetch
$ git push $ git push
``` ```
@ -59,13 +59,13 @@ $ git show-ref
42d894a83c9c356552a57af09ccdbd5587a99045 refs/heads/feature/1 42d894a83c9c356552a57af09ccdbd5587a99045 refs/heads/feature/1
f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354 refs/heads/master f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354 refs/heads/master
f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354 refs/remotes/rad/master f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354 refs/remotes/rad/master
42d894a83c9c356552a57af09ccdbd5587a99045 refs/remotes/rad/patches/2647168c23e7c2b2c1936d695443944e143bc3f7 42d894a83c9c356552a57af09ccdbd5587a99045 refs/remotes/rad/patches/90c77f2c33b7e472e058de4a586156f8a7fec7d6
``` ```
``` ```
$ git ls-remote rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji 'refs/heads/patches/*' $ git ls-remote rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji 'refs/heads/patches/*'
42d894a83c9c356552a57af09ccdbd5587a99045 refs/heads/patches/2647168c23e7c2b2c1936d695443944e143bc3f7 42d894a83c9c356552a57af09ccdbd5587a99045 refs/heads/patches/90c77f2c33b7e472e058de4a586156f8a7fec7d6
$ git ls-remote rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi 'refs/cobs/*' $ git ls-remote rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi 'refs/cobs/*'
2647168c23e7c2b2c1936d695443944e143bc3f7 refs/cobs/xyz.radicle.patch/2647168c23e7c2b2c1936d695443944e143bc3f7 90c77f2c33b7e472e058de4a586156f8a7fec7d6 refs/cobs/xyz.radicle.patch/90c77f2c33b7e472e058de4a586156f8a7fec7d6
``` ```
We can create another patch: We can create another patch:
@ -74,7 +74,7 @@ We can create another patch:
$ git checkout -b feature/2 -q master $ git checkout -b feature/2 -q master
$ git commit -a -m "Add more things" -q --allow-empty $ git commit -a -m "Add more things" -q --allow-empty
$ git push rad HEAD:refs/patches $ git push rad HEAD:refs/patches
✓ Patch b8ab1c99c1c8205680a3494f04fb3934ec738ddd opened ✓ Patch fedf0e4dcb74ff6db1d5e30a6a254b77f02ff60b opened
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
* [new reference] HEAD -> refs/patches * [new reference] HEAD -> refs/patches
``` ```
@ -83,8 +83,8 @@ We see both branches with upstreams now:
``` ```
$ git branch -vv $ git branch -vv
feature/1 42d894a [rad/patches/2647168c23e7c2b2c1936d695443944e143bc3f7] Add things feature/1 42d894a [rad/patches/90c77f2c33b7e472e058de4a586156f8a7fec7d6] Add things
* feature/2 8b0ea80 [rad/patches/b8ab1c99c1c8205680a3494f04fb3934ec738ddd] Add more things * feature/2 8b0ea80 [rad/patches/fedf0e4dcb74ff6db1d5e30a6a254b77f02ff60b] Add more things
master f2de534 [rad/master] Second commit master f2de534 [rad/master] Second commit
``` ```
@ -95,8 +95,8 @@ $ rad patch
╭────────────────────────────────────────────────────────────────────────────────────╮ ╭────────────────────────────────────────────────────────────────────────────────────╮
│ ● ID Title Author Head + - Updated │ │ ● ID Title Author Head + - Updated │
├────────────────────────────────────────────────────────────────────────────────────┤ ├────────────────────────────────────────────────────────────────────────────────────┤
│ ● 2647168 Add things #1 z6MknSL…StBU8Vi (you) 42d894a +0 -0 [ ... ] │ │ ● 90c77f2 Add things #1 z6MknSL…StBU8Vi (you) 42d894a +0 -0 [ ... ] │
│ ● b8ab1c9 Add more things z6MknSL…StBU8Vi (you) 8b0ea80 +0 -0 [ ... ] │ │ ● fedf0e4 Add more things z6MknSL…StBU8Vi (you) 8b0ea80 +0 -0 [ ... ] │
╰────────────────────────────────────────────────────────────────────────────────────╯ ╰────────────────────────────────────────────────────────────────────────────────────╯
``` ```
@ -108,9 +108,9 @@ $ git commit -a -m "Improve code" -q --allow-empty
``` (stderr) ``` (stderr)
$ git push $ git push
✓ Patch b8ab1c9 updated to 8767880c31b9e4a04cdb07ad6faa9ce453980399 ✓ Patch fedf0e4 updated to d0018fcc21d87c91a1ff9155aed6b4e57535566b
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
8b0ea80..02bef3f feature/2 -> patches/b8ab1c99c1c8205680a3494f04fb3934ec738ddd 8b0ea80..02bef3f feature/2 -> patches/fedf0e4dcb74ff6db1d5e30a6a254b77f02ff60b
``` ```
This last `git push` worked without specifying an upstream branch despite the This last `git push` worked without specifying an upstream branch despite the
@ -128,10 +128,10 @@ This allows for pushing to the remote patch branch without using the full
We can then see that the patch head has moved: We can then see that the patch head has moved:
``` ```
$ rad patch show b8ab1c9 $ rad patch show fedf0e4
╭──────────────────────────────────────────────────────────────────────────────╮ ╭──────────────────────────────────────────────────────────────────────────────╮
│ Title Add more things │ │ Title Add more things │
│ Patch b8ab1c99c1c8205680a3494f04fb3934ec738ddd │ Patch fedf0e4dcb74ff6db1d5e30a6a254b77f02ff60b
│ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │ │ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │
│ Head 02bef3fac41b2f98bb3c02b868a53ddfecb55b5f │ │ Head 02bef3fac41b2f98bb3c02b868a53ddfecb55b5f │
│ Branches feature/2 │ │ Branches feature/2 │
@ -142,7 +142,7 @@ $ rad patch show b8ab1c9
│ 8b0ea80 Add more things │ │ 8b0ea80 Add more things │
├──────────────────────────────────────────────────────────────────────────────┤ ├──────────────────────────────────────────────────────────────────────────────┤
│ ● opened by (you) [ ... ] │ │ ● opened by (you) [ ... ] │
│ ↑ updated to 8767880c31b9e4a04cdb07ad6faa9ce453980399 (02bef3f) [ ... ] │ │ ↑ updated to d0018fcc21d87c91a1ff9155aed6b4e57535566b (02bef3f) [ ... ] │
╰──────────────────────────────────────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────────────────────────────╯
``` ```
@ -155,14 +155,14 @@ $ git rev-parse HEAD
``` ```
$ git status --short --branch $ git status --short --branch
## feature/2...rad/patches/b8ab1c99c1c8205680a3494f04fb3934ec738ddd ## feature/2...rad/patches/fedf0e4dcb74ff6db1d5e30a6a254b77f02ff60b
``` ```
``` ```
$ git rev-parse refs/remotes/rad/patches/b8ab1c99c1c8205680a3494f04fb3934ec738ddd $ git rev-parse refs/remotes/rad/patches/fedf0e4dcb74ff6db1d5e30a6a254b77f02ff60b
02bef3fac41b2f98bb3c02b868a53ddfecb55b5f 02bef3fac41b2f98bb3c02b868a53ddfecb55b5f
$ git ls-remote rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi refs/heads/patches/b8ab1c99c1c8205680a3494f04fb3934ec738ddd $ git ls-remote rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi refs/heads/patches/fedf0e4dcb74ff6db1d5e30a6a254b77f02ff60b
02bef3fac41b2f98bb3c02b868a53ddfecb55b5f refs/heads/patches/b8ab1c99c1c8205680a3494f04fb3934ec738ddd 02bef3fac41b2f98bb3c02b868a53ddfecb55b5f refs/heads/patches/fedf0e4dcb74ff6db1d5e30a6a254b77f02ff60b
``` ```
## Force push ## Force push
@ -183,7 +183,7 @@ Now let's push to the patch head.
``` (stderr) (fail) ``` (stderr) (fail)
$ git push $ git push
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
! [rejected] feature/2 -> patches/b8ab1c99c1c8205680a3494f04fb3934ec738ddd (non-fast-forward) ! [rejected] feature/2 -> patches/fedf0e4dcb74ff6db1d5e30a6a254b77f02ff60b (non-fast-forward)
error: failed to push some refs to 'rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi' error: failed to push some refs to 'rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi'
hint: Updates were rejected because a pushed branch tip is behind its remote hint: Updates were rejected because a pushed branch tip is behind its remote
hint: counterpart. Check out this branch and integrate the remote changes hint: counterpart. Check out this branch and integrate the remote changes
@ -196,18 +196,18 @@ use `--force` to force the update.
``` (stderr) ``` (stderr)
$ git push --force $ git push --force
✓ Patch b8ab1c9 updated to f24334f8cea7b7a5bcaf3bc6deb1408c9bf507ad ✓ Patch fedf0e4 updated to 31ecf28817c44d90686b5c3c624c1f4a534b6478
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+ 02bef3f...9304dbc feature/2 -> patches/b8ab1c99c1c8205680a3494f04fb3934ec738ddd (forced update) + 02bef3f...9304dbc feature/2 -> patches/fedf0e4dcb74ff6db1d5e30a6a254b77f02ff60b (forced update)
``` ```
That worked. We can see the new revision if we call `rad patch show`: That worked. We can see the new revision if we call `rad patch show`:
``` ```
$ rad patch show b8ab1c9 $ rad patch show fedf0e4
╭──────────────────────────────────────────────────────────────────────────────╮ ╭──────────────────────────────────────────────────────────────────────────────╮
│ Title Add more things │ │ Title Add more things │
│ Patch b8ab1c99c1c8205680a3494f04fb3934ec738ddd │ Patch fedf0e4dcb74ff6db1d5e30a6a254b77f02ff60b
│ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │ │ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │
│ Head 9304dbc445925187994a7a93222a3f8bde73b785 │ │ Head 9304dbc445925187994a7a93222a3f8bde73b785 │
│ Branches feature/2 │ │ Branches feature/2 │
@ -218,8 +218,8 @@ $ rad patch show b8ab1c9
│ 8b0ea80 Add more things │ │ 8b0ea80 Add more things │
├──────────────────────────────────────────────────────────────────────────────┤ ├──────────────────────────────────────────────────────────────────────────────┤
│ ● opened by (you) [ ... ] │ │ ● opened by (you) [ ... ] │
│ ↑ updated to 8767880c31b9e4a04cdb07ad6faa9ce453980399 (02bef3f) [ ... ] │ │ ↑ updated to d0018fcc21d87c91a1ff9155aed6b4e57535566b (02bef3f) [ ... ] │
│ ↑ updated to f24334f8cea7b7a5bcaf3bc6deb1408c9bf507ad (9304dbc) [ ... ] │ │ ↑ updated to 31ecf28817c44d90686b5c3c624c1f4a534b6478 (9304dbc) [ ... ] │
╰──────────────────────────────────────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────────────────────────────╯
``` ```

View File

@ -26,7 +26,7 @@ Once the code is ready, we open (or create) a patch with our changes for the pro
``` (stderr) ``` (stderr)
$ git push rad -o patch.message="Define power requirements" -o patch.message="See details." HEAD:refs/patches $ git push rad -o patch.message="Define power requirements" -o patch.message="See details." HEAD:refs/patches
✓ Patch 077e4bbe9a6e5546f400ef5951768c37a76f13a4 opened ✓ Patch 73b73f376e93e09e0419664766ac9e433bf7d389 opened
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
* [new reference] HEAD -> refs/patches * [new reference] HEAD -> refs/patches
``` ```
@ -38,14 +38,14 @@ $ rad patch
╭──────────────────────────────────────────────────────────────────────────────────────────────╮ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮
│ ● ID Title Author Head + - Updated │ │ ● ID Title Author Head + - Updated │
├──────────────────────────────────────────────────────────────────────────────────────────────┤ ├──────────────────────────────────────────────────────────────────────────────────────────────┤
│ ● 077e4bb Define power requirements z6MknSL…StBU8Vi (you) 3e674d1 +0 -0 [ ... ] │ │ ● 73b73f3 Define power requirements z6MknSL…StBU8Vi (you) 3e674d1 +0 -0 [ ... ] │
╰──────────────────────────────────────────────────────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯
``` ```
``` ```
$ rad patch show 077e4bbe9a6e5546f400ef5951768c37a76f13a4 -p $ rad patch show 73b73f376e93e09e0419664766ac9e433bf7d389 -p
╭────────────────────────────────────────────────────────────────────╮ ╭────────────────────────────────────────────────────────────────────╮
│ Title Define power requirements │ │ Title Define power requirements │
│ Patch 077e4bbe9a6e5546f400ef5951768c37a76f13a4 │ Patch 73b73f376e93e09e0419664766ac9e433bf7d389
│ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │ │ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │
│ Head 3e674d1a1df90807e934f9ae5da2591dd6848a33 │ │ Head 3e674d1a1df90807e934f9ae5da2591dd6848a33 │
│ Branches flux-capacitor-power │ │ Branches flux-capacitor-power │
@ -74,7 +74,7 @@ index 0000000..e69de29
We can also see that it set an upstream for our patch branch: We can also see that it set an upstream for our patch branch:
``` ```
$ git branch -vv $ git branch -vv
* flux-capacitor-power 3e674d1 [rad/patches/077e4bbe9a6e5546f400ef5951768c37a76f13a4] Define power requirements * flux-capacitor-power 3e674d1 [rad/patches/73b73f376e93e09e0419664766ac9e433bf7d389] Define power requirements
master f2de534 [rad/master] Second commit master f2de534 [rad/master] Second commit
``` ```
@ -90,45 +90,45 @@ $ git commit --message "Add README, just for the fun"
``` ```
``` (stderr) ``` (stderr)
$ git push rad -o patch.message="Add README, just for the fun" $ git push rad -o patch.message="Add README, just for the fun"
✓ Patch 077e4bb updated to 5cdcd2e14411e2bfec7b11bcf4667e2e0fc4d417 ✓ Patch 73b73f3 updated to 5605784ae81dad91ba47ea55e19dd16f6280d44b
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
3e674d1..27857ec flux-capacitor-power -> patches/077e4bbe9a6e5546f400ef5951768c37a76f13a4 3e674d1..27857ec flux-capacitor-power -> patches/73b73f376e93e09e0419664766ac9e433bf7d389
``` ```
And let's leave a quick comment for our team: And let's leave a quick comment for our team:
``` ```
$ rad comment 077e4bbe9a6e5546f400ef5951768c37a76f13a4 --message 'I cannot wait to get back to the 90s!' $ rad comment 73b73f376e93e09e0419664766ac9e433bf7d389 --message 'I cannot wait to get back to the 90s!'
31a07b8e7758af2027e74e521a74bea4574280e7 de198e9b1613d827ce294a5b36cecdb8e65abcf1
$ rad comment 077e4bbe9a6e5546f400ef5951768c37a76f13a4 --message 'I cannot wait to get back to the 90s!' --reply-to 31a07b8e7758af2027e74e521a74bea4574280e7 $ rad comment 73b73f376e93e09e0419664766ac9e433bf7d389 --message 'I cannot wait to get back to the 90s!' --reply-to de198e9b1613d827ce294a5b36cecdb8e65abcf1
d66bcb6bfe2e06e57636e8b1ba3ef8098a8bb250 bd53b38140cf8249ef31c7464d35a4c960258e3f
``` ```
Now, let's checkout the patch that we just created: Now, let's checkout the patch that we just created:
``` ```
$ rad patch checkout 077e4bb $ rad patch checkout 73b73f3
✓ Switched to branch patch/077e4bb ✓ Switched to branch patch/73b73f3
✓ Branch patch/077e4bb setup to track rad/patches/077e4bbe9a6e5546f400ef5951768c37a76f13a4 ✓ Branch patch/73b73f3 setup to track rad/patches/73b73f376e93e09e0419664766ac9e433bf7d389
``` ```
We can also add a review verdict as such: We can also add a review verdict as such:
``` ```
$ rad review 077e4bbe9a6e5546f400ef5951768c37a76f13a4 --accept --no-message --no-sync $ rad review 73b73f376e93e09e0419664766ac9e433bf7d389 --accept --no-message --no-sync
✓ Patch 077e4bb accepted ✓ Patch 73b73f3 accepted
``` ```
Showing the patch list now will reveal the favorable verdict: Showing the patch list now will reveal the favorable verdict:
``` ```
$ rad patch show 077e4bb $ rad patch show 73b73f3
╭──────────────────────────────────────────────────────────────────────────────╮ ╭──────────────────────────────────────────────────────────────────────────────╮
│ Title Define power requirements │ │ Title Define power requirements │
│ Patch 077e4bbe9a6e5546f400ef5951768c37a76f13a4 │ Patch 73b73f376e93e09e0419664766ac9e433bf7d389
│ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │ │ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │
│ Head 27857ec9eb04c69cacab516e8bf4b5fd36090f66 │ │ Head 27857ec9eb04c69cacab516e8bf4b5fd36090f66 │
│ Branches flux-capacitor-power, patch/077e4bb │ Branches flux-capacitor-power, patch/73b73f3
│ Commits ahead 2, behind 0 │ │ Commits ahead 2, behind 0 │
│ Status open │ │ Status open │
│ │ │ │
@ -138,7 +138,7 @@ $ rad patch show 077e4bb
│ 3e674d1 Define power requirements │ │ 3e674d1 Define power requirements │
├──────────────────────────────────────────────────────────────────────────────┤ ├──────────────────────────────────────────────────────────────────────────────┤
│ ● opened by (you) [ ... ] │ │ ● opened by (you) [ ... ] │
│ ↑ updated to 5cdcd2e14411e2bfec7b11bcf4667e2e0fc4d417 (27857ec) [ ... ] │ │ ↑ updated to 5605784ae81dad91ba47ea55e19dd16f6280d44b (27857ec) [ ... ] │
│ ✓ accepted by (you) [ ... ] │ │ ✓ accepted by (you) [ ... ] │
╰──────────────────────────────────────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────────────────────────────╯
``` ```
@ -146,14 +146,14 @@ $ rad patch show 077e4bb
If you make a mistake on the patch description, you can always change it! If you make a mistake on the patch description, you can always change it!
``` ```
$ rad patch edit 077e4bb --message "Define power requirements" --message "Add requirements file" $ rad patch edit 73b73f3 --message "Define power requirements" --message "Add requirements file"
$ rad patch show 077e4bb $ rad patch show 73b73f3
╭──────────────────────────────────────────────────────────────────────────────╮ ╭──────────────────────────────────────────────────────────────────────────────╮
│ Title Define power requirements │ │ Title Define power requirements │
│ Patch 077e4bbe9a6e5546f400ef5951768c37a76f13a4 │ Patch 73b73f376e93e09e0419664766ac9e433bf7d389
│ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │ │ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │
│ Head 27857ec9eb04c69cacab516e8bf4b5fd36090f66 │ │ Head 27857ec9eb04c69cacab516e8bf4b5fd36090f66 │
│ Branches flux-capacitor-power, patch/077e4bb │ Branches flux-capacitor-power, patch/73b73f3
│ Commits ahead 2, behind 0 │ │ Commits ahead 2, behind 0 │
│ Status open │ │ Status open │
│ │ │ │
@ -163,7 +163,7 @@ $ rad patch show 077e4bb
│ 3e674d1 Define power requirements │ │ 3e674d1 Define power requirements │
├──────────────────────────────────────────────────────────────────────────────┤ ├──────────────────────────────────────────────────────────────────────────────┤
│ ● opened by (you) [ ... ] │ │ ● opened by (you) [ ... ] │
│ ↑ updated to 5cdcd2e14411e2bfec7b11bcf4667e2e0fc4d417 (27857ec) [ ... ] │ │ ↑ updated to 5605784ae81dad91ba47ea55e19dd16f6280d44b (27857ec) [ ... ] │
│ ✓ accepted by (you) [ ... ] │ │ ✓ accepted by (you) [ ... ] │
╰──────────────────────────────────────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────────────────────────────╯
``` ```

View File

@ -61,7 +61,7 @@ $ git commit -q -m "Update files"
``` (stderr) ``` (stderr)
$ git push rad HEAD:refs/patches $ git push rad HEAD:refs/patches
✓ Patch a05c57911e6ec9eba4e5539a095a6a86e2bf7c2c opened ✓ Patch 7f6ed9bd562a36eb1d5689f95600d09247726a23 opened
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
* [new reference] HEAD -> refs/patches * [new reference] HEAD -> refs/patches
``` ```
@ -70,7 +70,7 @@ Finally, we do a review of the patch by hunk. The output of this command should
match `git diff master -W100% -U5 --patience`: match `git diff master -W100% -U5 --patience`:
``` ```
$ rad review --no-sync --patch -U5 a05c57911e6ec9eba4e5539a095a6a86e2bf7c2c $ rad review --no-sync --patch -U5 7f6ed9bd562a36eb1d5689f95600d09247726a23
diff --git a/.gitignore b/.gitignore diff --git a/.gitignore b/.gitignore
deleted file mode 100644 deleted file mode 100644
index 7937fb3..0000000 index 7937fb3..0000000
@ -116,8 +116,8 @@ rename to notes/INSTRUCTIONS.txt
Now let's accept these hunks one by one.. Now let's accept these hunks one by one..
``` ```
$ rad review --no-sync --patch --accept --hunk 1 a05c57911e6ec9eba4e5539a095a6a86e2bf7c2c $ rad review --no-sync --patch --accept --hunk 1 7f6ed9bd562a36eb1d5689f95600d09247726a23
✓ Loaded existing review ([..]) for patch a05c57911e6ec9eba4e5539a095a6a86e2bf7c2c ✓ Loaded existing review ([..]) for patch 7f6ed9bd562a36eb1d5689f95600d09247726a23
diff --git a/.gitignore b/.gitignore diff --git a/.gitignore b/.gitignore
deleted file mode 100644 deleted file mode 100644
index 7937fb3..0000000 index 7937fb3..0000000
@ -127,8 +127,8 @@ index 7937fb3..0000000
-*.draft -*.draft
``` ```
``` ```
$ rad review --no-sync --patch --accept --hunk 1 a05c57911e6ec9eba4e5539a095a6a86e2bf7c2c $ rad review --no-sync --patch --accept --hunk 1 7f6ed9bd562a36eb1d5689f95600d09247726a23
✓ Loaded existing review ([..]) for patch a05c57911e6ec9eba4e5539a095a6a86e2bf7c2c ✓ Loaded existing review ([..]) for patch 7f6ed9bd562a36eb1d5689f95600d09247726a23
diff --git a/DISCLAIMER.txt b/DISCLAIMER.txt diff --git a/DISCLAIMER.txt b/DISCLAIMER.txt
new file mode 100644 new file mode 100644
index 0000000..2b5bd86 index 0000000..2b5bd86
@ -138,8 +138,8 @@ index 0000000..2b5bd86
+All food is served as-is, with no warranty! +All food is served as-is, with no warranty!
``` ```
``` ```
$ rad review --no-sync --patch --accept -U3 --hunk 1 a05c57911e6ec9eba4e5539a095a6a86e2bf7c2c $ rad review --no-sync --patch --accept -U3 --hunk 1 7f6ed9bd562a36eb1d5689f95600d09247726a23
✓ Loaded existing review ([..]) for patch a05c57911e6ec9eba4e5539a095a6a86e2bf7c2c ✓ Loaded existing review ([..]) for patch 7f6ed9bd562a36eb1d5689f95600d09247726a23
diff --git a/MENU.txt b/MENU.txt diff --git a/MENU.txt b/MENU.txt
index 867958c..3af9741 100644 index 867958c..3af9741 100644
--- a/MENU.txt --- a/MENU.txt
@ -153,8 +153,8 @@ index 867958c..3af9741 100644
[..] [..]
``` ```
``` ```
$ rad review --no-sync --patch --accept -U3 --hunk 1 a05c57911e6ec9eba4e5539a095a6a86e2bf7c2c $ rad review --no-sync --patch --accept -U3 --hunk 1 7f6ed9bd562a36eb1d5689f95600d09247726a23
✓ Loaded existing review ([..]) for patch a05c57911e6ec9eba4e5539a095a6a86e2bf7c2c ✓ Loaded existing review ([..]) for patch 7f6ed9bd562a36eb1d5689f95600d09247726a23
diff --git a/MENU.txt b/MENU.txt diff --git a/MENU.txt b/MENU.txt
index 4e2e828..3af9741 100644 index 4e2e828..3af9741 100644
--- a/MENU.txt --- a/MENU.txt
@ -169,8 +169,8 @@ index 4e2e828..3af9741 100644
``` ```
``` ```
$ rad review --no-sync --patch --accept --hunk 1 a05c57911e6ec9eba4e5539a095a6a86e2bf7c2c $ rad review --no-sync --patch --accept --hunk 1 7f6ed9bd562a36eb1d5689f95600d09247726a23
✓ Loaded existing review ([..]) for patch a05c57911e6ec9eba4e5539a095a6a86e2bf7c2c ✓ Loaded existing review ([..]) for patch 7f6ed9bd562a36eb1d5689f95600d09247726a23
diff --git a/INSTRUCTIONS.txt b/notes/INSTRUCTIONS.txt diff --git a/INSTRUCTIONS.txt b/notes/INSTRUCTIONS.txt
similarity index 100% similarity index 100%
rename from INSTRUCTIONS.txt rename from INSTRUCTIONS.txt
@ -178,7 +178,7 @@ rename to notes/INSTRUCTIONS.txt
``` ```
``` ```
$ rad review --no-sync --patch --accept --hunk 1 a05c57911e6ec9eba4e5539a095a6a86e2bf7c2c $ rad review --no-sync --patch --accept --hunk 1 7f6ed9bd562a36eb1d5689f95600d09247726a23
✓ Loaded existing review ([..]) for patch a05c57911e6ec9eba4e5539a095a6a86e2bf7c2c ✓ Loaded existing review ([..]) for patch 7f6ed9bd562a36eb1d5689f95600d09247726a23
✓ All hunks have been reviewed ✓ All hunks have been reviewed
``` ```

View File

@ -7,7 +7,7 @@ Let's say the new car you are designing with your peers has a problem with its f
$ rad issue open --title "flux capacitor underpowered" --description "Flux capacitor power requirements exceed current supply" --no-announce $ rad issue open --title "flux capacitor underpowered" --description "Flux capacitor power requirements exceed current supply" --no-announce
╭─────────────────────────────────────────────────────────╮ ╭─────────────────────────────────────────────────────────╮
│ Title flux capacitor underpowered │ │ Title flux capacitor underpowered │
│ Issue b05e945bb63c11bf80320f4e26ad1d1f7c51f755 │ Issue 2f6eb49efac492327f71437b6bfc01b49afa0981
│ Status open │ │ Status open │
│ │ │ │
│ Flux capacitor power requirements exceed current supply │ │ Flux capacitor power requirements exceed current supply │
@ -18,11 +18,11 @@ The issue is now listed under our project.
``` ```
$ rad issue list $ rad issue list
╭───────────────────────────────────────────────────────────────────────────────────────────────────────────╮ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────
│ ● ID Title Author Tags Assignees Opened │ │ ● ID Title Author Labels Assignees Opened │
├───────────────────────────────────────────────────────────────────────────────────────────────────────────┤ ├─────────────────────────────────────────────────────────────────────────────────────────────────────────────
│ ● b05e945 flux capacitor underpowered z6Mkt67…v4N1tRk bob (you) [ .. ] │ │ ● 2f6eb49 flux capacitor underpowered z6Mkt67…v4N1tRk bob (you) [ .. ] │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────
``` ```
Great! Now we've documented the issue for ourselves and others. Great! Now we've documented the issue for ourselves and others.
@ -33,27 +33,27 @@ others to work on. This is to ensure work is not duplicated.
Let's assign this issue to ourself. Let's assign this issue to ourself.
``` ```
$ rad assign b05e945bb63c11bf80320f4e26ad1d1f7c51f755 --to did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk $ rad assign 2f6eb49efac492327f71437b6bfc01b49afa0981 --to did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
``` ```
It will now show in the list of issues assigned to us. It will now show in the list of issues assigned to us.
``` ```
$ rad issue list --assigned $ rad issue list --assigned
╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
│ ● ID Title Author Tags Assignees Opened │ │ ● ID Title Author Labels Assignees Opened │
├───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ ├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
│ ● b05e945 flux capacitor underpowered z6Mkt67…v4N1tRk bob (you) bob (z6Mkt67…v4N1tRk) [ .. ] │ │ ● 2f6eb49 flux capacitor underpowered z6Mkt67…v4N1tRk bob (you) bob (z6Mkt67…v4N1tRk) [ .. ] │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
``` ```
In addition, you can see that when you run `rad issue show` you are listed under the `Assignees`. In addition, you can see that when you run `rad issue show` you are listed under the `Assignees`.
``` ```
$ rad issue show b05e945 $ rad issue show 2f6eb49
╭─────────────────────────────────────────────────────────╮ ╭─────────────────────────────────────────────────────────╮
│ Title flux capacitor underpowered │ │ Title flux capacitor underpowered │
│ Issue b05e945bb63c11bf80320f4e26ad1d1f7c51f755 │ Issue 2f6eb49efac492327f71437b6bfc01b49afa0981
│ Assignees z6Mkt67…v4N1tRk │ │ Assignees z6Mkt67…v4N1tRk │
│ Status open │ │ Status open │
│ │ │ │
@ -69,6 +69,6 @@ But wait! We've found an important detail about the car's power requirements.
It will help whoever works on a fix. It will help whoever works on a fix.
``` ```
$ rad comment b05e945bb63c11bf80320f4e26ad1d1f7c51f755 --message 'The flux capacitor needs 1.21 Gigawatts' $ rad comment 2f6eb49efac492327f71437b6bfc01b49afa0981 --message 'The flux capacitor needs 1.21 Gigawatts'
8b9ee0f0a530f0318e100ea8b9ed3a723bd584f6 24ab347afda760e77d565f9cb013c6db560f44fd
``` ```

View File

@ -26,7 +26,7 @@ Once the code is ready, we open a patch with our changes.
``` (stderr) ``` (stderr)
$ git push rad -o no-sync -o patch.message="Define power requirements" -o patch.message="See details." HEAD:refs/patches $ git push rad -o no-sync -o patch.message="Define power requirements" -o patch.message="See details." HEAD:refs/patches
✓ Patch 50e29a111972f3b7d2123c5057de5bdf09bc7b1c opened ✓ Patch 69e881c606639691330051d7d8f013854f32fb87 opened
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
* [new reference] HEAD -> refs/patches * [new reference] HEAD -> refs/patches
``` ```
@ -38,12 +38,12 @@ $ rad patch
╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ ● ID Title Author Head + - Updated │ │ ● ID Title Author Head + - Updated │
├──────────────────────────────────────────────────────────────────────────────────────────────────┤ ├──────────────────────────────────────────────────────────────────────────────────────────────────┤
│ ● 50e29a1 Define power requirements z6Mkt67…v4N1tRk bob (you) 3e674d1 +0 -0 [ ... ] │ │ ● 69e881c Define power requirements z6Mkt67…v4N1tRk bob (you) 3e674d1 +0 -0 [ ... ] │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
$ rad patch show 50e29a111972f3b7d2123c5057de5bdf09bc7b1c $ rad patch show 69e881c606639691330051d7d8f013854f32fb87
╭────────────────────────────────────────────────────────────────────╮ ╭────────────────────────────────────────────────────────────────────╮
│ Title Define power requirements │ │ Title Define power requirements │
│ Patch 50e29a111972f3b7d2123c5057de5bdf09bc7b1c │ Patch 69e881c606639691330051d7d8f013854f32fb87
│ Author did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk │ │ Author did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk │
│ Head 3e674d1a1df90807e934f9ae5da2591dd6848a33 │ │ Head 3e674d1a1df90807e934f9ae5da2591dd6848a33 │
│ Branches flux-capacitor-power │ │ Branches flux-capacitor-power │
@ -62,7 +62,7 @@ We can also confirm that the patch branch is in storage:
``` ```
$ git ls-remote rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk refs/heads/patches/* $ git ls-remote rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk refs/heads/patches/*
3e674d1a1df90807e934f9ae5da2591dd6848a33 refs/heads/patches/50e29a111972f3b7d2123c5057de5bdf09bc7b1c 3e674d1a1df90807e934f9ae5da2591dd6848a33 refs/heads/patches/69e881c606639691330051d7d8f013854f32fb87
``` ```
Wait, let's add a README too! Just for fun. Wait, let's add a README too! Just for fun.
@ -77,14 +77,14 @@ $ git commit --message "Add README, just for the fun"
``` ```
``` (stderr) RAD_SOCKET=/dev/null ``` (stderr) RAD_SOCKET=/dev/null
$ git push -o patch.message="Add README, just for the fun" $ git push -o patch.message="Add README, just for the fun"
✓ Patch 50e29a1 updated to 3530243d46a2e7a8e4eac7afcbb17cc7c56b3d29 ✓ Patch 69e881c updated to dcf3e6dd97c95cf8653cbb8ce47df20d28eb1821
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
3e674d1..27857ec flux-capacitor-power -> patches/50e29a111972f3b7d2123c5057de5bdf09bc7b1c 3e674d1..27857ec flux-capacitor-power -> patches/69e881c606639691330051d7d8f013854f32fb87
``` ```
And let's leave a quick comment for our team: And let's leave a quick comment for our team:
``` ```
$ rad comment 50e29a111972f3b7d2123c5057de5bdf09bc7b1c --message 'I cannot wait to get back to the 90s!' $ rad comment 69e881c606639691330051d7d8f013854f32fb87 --message 'I cannot wait to get back to the 90s!'
4a9d780cf088769722d226d83a1b4663ab176f8e f95ef6c0fb97a5dd05db49f7012010f0c49d59bc
``` ```

View File

@ -23,7 +23,7 @@ $ rad remote add z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --name bob
$ git fetch bob $ git fetch bob
From rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk From rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
* [new branch] master -> bob/master * [new branch] master -> bob/master
* [new branch] patches/50e29a111972f3b7d2123c5057de5bdf09bc7b1c -> bob/patches/50e29a111972f3b7d2123c5057de5bdf09bc7b1c * [new branch] patches/69e881c606639691330051d7d8f013854f32fb87 -> bob/patches/69e881c606639691330051d7d8f013854f32fb87
``` ```
The contributor's changes are now visible to us. The contributor's changes are now visible to us.
@ -31,12 +31,12 @@ The contributor's changes are now visible to us.
``` ```
$ git branch -r $ git branch -r
bob/master bob/master
bob/patches/50e29a111972f3b7d2123c5057de5bdf09bc7b1c bob/patches/69e881c606639691330051d7d8f013854f32fb87
rad/master rad/master
$ rad patch show 50e29a1 $ rad patch show 69e881c
╭──────────────────────────────────────────────────────────────────────────────╮ ╭──────────────────────────────────────────────────────────────────────────────╮
│ Title Define power requirements │ │ Title Define power requirements │
│ Patch 50e29a111972f3b7d2123c5057de5bdf09bc7b1c │ Patch 69e881c606639691330051d7d8f013854f32fb87
│ Author did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk │ │ Author did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk │
│ Head 27857ec9eb04c69cacab516e8bf4b5fd36090f66 │ │ Head 27857ec9eb04c69cacab516e8bf4b5fd36090f66 │
│ Commits ahead 2, behind 0 │ │ Commits ahead 2, behind 0 │
@ -48,7 +48,7 @@ $ rad patch show 50e29a1
│ 3e674d1 Define power requirements │ │ 3e674d1 Define power requirements │
├──────────────────────────────────────────────────────────────────────────────┤ ├──────────────────────────────────────────────────────────────────────────────┤
│ ● opened by bob (z6Mkt67…v4N1tRk) [ ... ] │ │ ● opened by bob (z6Mkt67…v4N1tRk) [ ... ] │
│ ↑ updated to 3530243d46a2e7a8e4eac7afcbb17cc7c56b3d29 (27857ec) [ ... ] │ │ ↑ updated to dcf3e6dd97c95cf8653cbb8ce47df20d28eb1821 (27857ec) [ ... ] │
╰──────────────────────────────────────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────────────────────────────╯
``` ```
@ -58,20 +58,20 @@ way will tell others about the corrections we needed before merging the
changes. changes.
``` ```
$ rad patch checkout 50e29a111972f3b7d2123c5057de5bdf09bc7b1c $ rad patch checkout 69e881c606639691330051d7d8f013854f32fb87
✓ Switched to branch patch/50e29a1 ✓ Switched to branch patch/69e881c
✓ Branch patch/50e29a1 setup to track rad/patches/50e29a111972f3b7d2123c5057de5bdf09bc7b1c ✓ Branch patch/69e881c setup to track rad/patches/69e881c606639691330051d7d8f013854f32fb87
$ git mv REQUIREMENTS REQUIREMENTS.md $ git mv REQUIREMENTS REQUIREMENTS.md
$ git commit -m "Use markdown for requirements" $ git commit -m "Use markdown for requirements"
[patch/50e29a1 f567f69] Use markdown for requirements [patch/69e881c f567f69] Use markdown for requirements
1 file changed, 0 insertions(+), 0 deletions(-) 1 file changed, 0 insertions(+), 0 deletions(-)
rename REQUIREMENTS => REQUIREMENTS.md (100%) rename REQUIREMENTS => REQUIREMENTS.md (100%)
``` ```
``` (stderr) ``` (stderr)
$ git push rad -o no-sync -o patch.message="Use markdown for requirements" $ git push rad -o no-sync -o patch.message="Use markdown for requirements"
✓ Patch 50e29a1 updated to 744c1f0a75b1c42833c9aa32f79cd40443925d66 ✓ Patch 69e881c updated to ab05fcdca93cf4d5b22da8913e2fe0b6d8c79338
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
* [new branch] patch/50e29a1 -> patches/50e29a111972f3b7d2123c5057de5bdf09bc7b1c * [new branch] patch/69e881c -> patches/69e881c606639691330051d7d8f013854f32fb87
``` ```
Great, all fixed up, lets merge the code. Great, all fixed up, lets merge the code.
@ -79,7 +79,7 @@ Great, all fixed up, lets merge the code.
``` ```
$ git checkout master $ git checkout master
Your branch is up to date with 'rad/master'. Your branch is up to date with 'rad/master'.
$ git merge patch/50e29a1 $ git merge patch/69e881c
Updating f2de534..f567f69 Updating f2de534..f567f69
Fast-forward Fast-forward
README.md | 0 README.md | 0
@ -93,13 +93,13 @@ $ git push rad master
The patch is now merged and closed :). The patch is now merged and closed :).
``` ```
$ rad patch show 50e29a1 $ rad patch show 69e881c
╭──────────────────────────────────────────────────────────────────────────────╮ ╭──────────────────────────────────────────────────────────────────────────────╮
│ Title Define power requirements │ │ Title Define power requirements │
│ Patch 50e29a111972f3b7d2123c5057de5bdf09bc7b1c │ Patch 69e881c606639691330051d7d8f013854f32fb87
│ Author did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk │ │ Author did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk │
│ Head f567f695d25b4e8fb63b5f5ad2a584529826e908 │ │ Head f567f695d25b4e8fb63b5f5ad2a584529826e908 │
│ Branches master, patch/50e29a1 │ Branches master, patch/69e881c
│ Commits up to date │ │ Commits up to date │
│ Status merged │ │ Status merged │
│ │ │ │
@ -110,8 +110,8 @@ $ rad patch show 50e29a1
│ 3e674d1 Define power requirements │ │ 3e674d1 Define power requirements │
├──────────────────────────────────────────────────────────────────────────────┤ ├──────────────────────────────────────────────────────────────────────────────┤
│ ● opened by bob (z6Mkt67…v4N1tRk) [ ... ] │ │ ● opened by bob (z6Mkt67…v4N1tRk) [ ... ] │
│ ↑ updated to 3530243d46a2e7a8e4eac7afcbb17cc7c56b3d29 (27857ec) [ ... ] │ │ ↑ updated to dcf3e6dd97c95cf8653cbb8ce47df20d28eb1821 (27857ec) [ ... ] │
│ ↑ updated to 744c1f0a75b1c42833c9aa32f79cd40443925d66 (f567f69) [ ... ] │ │ ↑ updated to ab05fcdca93cf4d5b22da8913e2fe0b6d8c79338 (f567f69) [ ... ] │
│ ✓ merged by alice (you) [ ... ] │ │ ✓ merged by alice (you) [ ... ] │
╰──────────────────────────────────────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────────────────────────────╯
``` ```

View File

@ -24,6 +24,8 @@ pub mod rad_init;
pub mod rad_inspect; pub mod rad_inspect;
#[path = "commands/issue.rs"] #[path = "commands/issue.rs"]
pub mod rad_issue; pub mod rad_issue;
#[path = "commands/label.rs"]
pub mod rad_label;
#[path = "commands/ls.rs"] #[path = "commands/ls.rs"]
pub mod rad_ls; pub mod rad_ls;
#[path = "commands/node.rs"] #[path = "commands/node.rs"]
@ -42,14 +44,12 @@ pub mod rad_rm;
pub mod rad_self; pub mod rad_self;
#[path = "commands/sync.rs"] #[path = "commands/sync.rs"]
pub mod rad_sync; pub mod rad_sync;
#[path = "commands/tag.rs"]
pub mod rad_tag;
#[path = "commands/track.rs"] #[path = "commands/track.rs"]
pub mod rad_track; pub mod rad_track;
#[path = "commands/unassign.rs"] #[path = "commands/unassign.rs"]
pub mod rad_unassign; pub mod rad_unassign;
#[path = "commands/untag.rs"] #[path = "commands/unlabel.rs"]
pub mod rad_untag; pub mod rad_unlabel;
#[path = "commands/untrack.rs"] #[path = "commands/untrack.rs"]
pub mod rad_untrack; pub mod rad_untrack;
#[path = "commands/web.rs"] #[path = "commands/web.rs"]

View File

@ -31,10 +31,10 @@ const COMMANDS: &[Help] = &[
rad_review::HELP, rad_review::HELP,
rad_rm::HELP, rad_rm::HELP,
rad_self::HELP, rad_self::HELP,
rad_tag::HELP, rad_label::HELP,
rad_track::HELP, rad_track::HELP,
rad_unassign::HELP, rad_unassign::HELP,
rad_untag::HELP, rad_unlabel::HELP,
rad_untrack::HELP, rad_untrack::HELP,
rad_remote::HELP, rad_remote::HELP,
rad_sync::HELP, rad_sync::HELP,

View File

@ -5,7 +5,7 @@ use std::str::FromStr;
use anyhow::{anyhow, Context as _}; use anyhow::{anyhow, Context as _};
use radicle::cob::common::{Reaction, Tag}; use radicle::cob::common::{Label, Reaction};
use radicle::cob::issue; use radicle::cob::issue;
use radicle::cob::issue::{CloseReason, Issues, State}; use radicle::cob::issue::{CloseReason, Issues, State};
use radicle::cob::thread; use radicle::cob::thread;
@ -36,7 +36,7 @@ Usage
rad issue delete <issue-id> [<option>...] rad issue delete <issue-id> [<option>...]
rad issue edit <issue-id> [<option>...] rad issue edit <issue-id> [<option>...]
rad issue list [--assigned <did>] [--all | --closed | --open | --solved] [<option>...] rad issue list [--assigned <did>] [--all | --closed | --open | --solved] [<option>...]
rad issue open [--title <title>] [--description <text>] [--tag <tag>] [<option>...] rad issue open [--title <title>] [--description <text>] [--label <label>] [<option>...]
rad issue react <issue-id> [--emoji <char>] [--to <comment>] [<option>...] rad issue react <issue-id> [--emoji <char>] [--to <comment>] [<option>...]
rad issue show <issue-id> [<option>...] rad issue show <issue-id> [<option>...]
rad issue state <issue-id> [--closed | --open | --solved] [<option>...] rad issue state <issue-id> [--closed | --open | --solved] [<option>...]
@ -52,7 +52,7 @@ Options
#[derive(serde::Deserialize, serde::Serialize, Debug)] #[derive(serde::Deserialize, serde::Serialize, Debug)]
pub struct Metadata { pub struct Metadata {
title: String, title: String,
tags: Vec<Tag>, labels: Vec<Label>,
assignees: Vec<Did>, assignees: Vec<Did>,
} }
@ -86,7 +86,7 @@ pub enum Operation {
Open { Open {
title: Option<String>, title: Option<String>,
description: Option<String>, description: Option<String>,
tags: Vec<Tag>, labels: Vec<Label>,
}, },
Show { Show {
id: Rev, id: Rev,
@ -129,7 +129,7 @@ impl Args for Options {
let mut comment_id: Option<thread::CommentId> = None; let mut comment_id: Option<thread::CommentId> = None;
let mut description: Option<String> = None; let mut description: Option<String> = None;
let mut state: Option<State> = Some(State::Open); let mut state: Option<State> = Some(State::Open);
let mut tags = Vec::new(); let mut labels = Vec::new();
let mut announce = true; let mut announce = true;
let mut quiet = false; let mut quiet = false;
@ -157,12 +157,12 @@ impl Args for Options {
Long("title") if op == Some(OperationName::Open) => { Long("title") if op == Some(OperationName::Open) => {
title = Some(parser.value()?.to_string_lossy().into()); title = Some(parser.value()?.to_string_lossy().into());
} }
Long("tag") if op == Some(OperationName::Open) => { Long("label") if op == Some(OperationName::Open) => {
let val = parser.value()?; let val = parser.value()?;
let name = term::args::string(&val); let name = term::args::string(&val);
let tag = Tag::new(name)?; let label = Label::new(name)?;
tags.push(tag); labels.push(label);
} }
Long("closed") if op == Some(OperationName::State) => { Long("closed") if op == Some(OperationName::State) => {
state = Some(State::Closed { state = Some(State::Closed {
@ -234,7 +234,7 @@ impl Args for Options {
OperationName::Open => Operation::Open { OperationName::Open => Operation::Open {
title, title,
description, description,
tags, labels,
}, },
OperationName::Show => Operation::Show { OperationName::Show => Operation::Show {
id: id.ok_or_else(|| anyhow!("an issue must be provided"))?, id: id.ok_or_else(|| anyhow!("an issue must be provided"))?,
@ -293,9 +293,9 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
Operation::Open { Operation::Open {
title: Some(title), title: Some(title),
description: Some(description), description: Some(description),
tags, labels,
} => { } => {
let issue = issues.create(title, description, tags.as_slice(), &[], &signer)?; let issue = issues.create(title, description, labels.as_slice(), &[], &signer)?;
if !options.quiet { if !options.quiet {
show_issue(&issue, issue.id())?; show_issue(&issue, issue.id())?;
} }
@ -329,12 +329,12 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
Operation::Open { Operation::Open {
ref title, ref title,
ref description, ref description,
ref tags, ref labels,
} => { } => {
open( open(
title.clone(), title.clone(),
description.clone(), description.clone(),
tags.to_vec(), labels.to_vec(),
&options, &options,
&mut issues, &mut issues,
&signer, &signer,
@ -387,7 +387,7 @@ fn list<R: WriteRepository + cob::Store>(
}; };
if let Some(a) = assignee { if let Some(a) = assignee {
if !issue.assigned().any(|v| v == Did::from(a)) { if !issue.assigned().any(|v| v == &Did::from(a)) {
continue; continue;
} }
} }
@ -413,7 +413,7 @@ fn list<R: WriteRepository + cob::Store>(
term::format::bold(String::from("Title")).into(), term::format::bold(String::from("Title")).into(),
term::format::bold(String::from("Author")).into(), term::format::bold(String::from("Author")).into(),
term::format::bold(String::new()).into(), term::format::bold(String::new()).into(),
term::format::bold(String::from("Tags")).into(), term::format::bold(String::from("Labels")).into(),
term::format::bold(String::from("Assignees")).into(), term::format::bold(String::from("Assignees")).into(),
term::format::bold(String::from("Opened")).into(), term::format::bold(String::from("Opened")).into(),
]); ]);
@ -424,7 +424,7 @@ fn list<R: WriteRepository + cob::Store>(
for (id, issue) in all { for (id, issue) in all {
let assigned: String = issue let assigned: String = issue
.assigned() .assigned()
.map(|ref p| { .map(|p| {
if let Some(alias) = aliases.alias(p) { if let Some(alias) = aliases.alias(p) {
format!("{alias} ({})", term::format::did(p)) format!("{alias} ({})", term::format::did(p))
} else { } else {
@ -434,8 +434,8 @@ fn list<R: WriteRepository + cob::Store>(
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(", "); .join(", ");
let mut tags = issue.tags().map(|t| t.to_string()).collect::<Vec<_>>(); let mut labels = issue.labels().map(|t| t.to_string()).collect::<Vec<_>>();
tags.sort(); labels.sort();
let author = issue.author().id; let author = issue.author().id;
let alias = aliases.alias(&author); let alias = aliases.alias(&author);
@ -452,7 +452,7 @@ fn list<R: WriteRepository + cob::Store>(
term::format::default(issue.title().to_owned()).into(), term::format::default(issue.title().to_owned()).into(),
term::format::did(&issue.author().id).dim().into(), term::format::did(&issue.author().id).dim().into(),
display.alias(), display.alias(),
term::format::secondary(tags.join(", ")).into(), term::format::secondary(labels.join(", ")).into(),
if assigned.is_empty() { if assigned.is_empty() {
term::format::dim(String::default()).into() term::format::dim(String::default()).into()
} else { } else {
@ -473,7 +473,7 @@ fn list<R: WriteRepository + cob::Store>(
fn prompt_issue( fn prompt_issue(
title: &str, title: &str,
description: &str, description: &str,
tags: &[Tag], labels: &[Label],
assignees: &[Did], assignees: &[Did],
) -> anyhow::Result<Option<(Metadata, String)>> { ) -> anyhow::Result<Option<(Metadata, String)>> {
let title = if title.is_empty() { let title = if title.is_empty() {
@ -491,7 +491,7 @@ fn prompt_issue(
let meta = Metadata { let meta = Metadata {
title: title.to_string(), title: title.to_string(),
tags: tags.to_vec(), labels: labels.to_vec(),
assignees: assignees.to_vec(), assignees: assignees.to_vec(),
}; };
let yaml = serde_yaml::to_string(&meta)?; let yaml = serde_yaml::to_string(&meta)?;
@ -550,7 +550,7 @@ fn prompt_issue(
fn open<R: WriteRepository + cob::Store, G: Signer>( fn open<R: WriteRepository + cob::Store, G: Signer>(
title: Option<String>, title: Option<String>,
description: Option<String>, description: Option<String>,
tags: Vec<Tag>, labels: Vec<Label>,
options: &Options, options: &Options,
issues: &mut Issues<R>, issues: &mut Issues<R>,
signer: &G, signer: &G,
@ -558,7 +558,7 @@ fn open<R: WriteRepository + cob::Store, G: Signer>(
let Some((meta, description)) = prompt_issue( let Some((meta, description)) = prompt_issue(
&title.unwrap_or_default(), &title.unwrap_or_default(),
&description.unwrap_or_default(), &description.unwrap_or_default(),
&tags, &labels,
&[], &[],
)? else { )? else {
return Ok(()); return Ok(());
@ -567,12 +567,8 @@ fn open<R: WriteRepository + cob::Store, G: Signer>(
let issue = issues.create( let issue = issues.create(
&meta.title, &meta.title,
description.trim(), description.trim(),
meta.tags.as_slice(), meta.labels.as_slice(),
meta.assignees meta.assignees.as_slice(),
.into_iter()
.map(cob::ActorId::from)
.collect::<Vec<_>>()
.as_slice(),
signer, signer,
)?; )?;
if !options.quiet { if !options.quiet {
@ -611,49 +607,23 @@ fn edit<R: WriteRepository + cob::Store, G: radicle::crypto::Signer>(
} }
// Editing by editor // Editing by editor
let tags: Vec<_> = issue.tags().cloned().collect(); let labels: Vec<_> = issue.labels().cloned().collect();
let assigned: Vec<_> = issue.assigned().collect(); let assigned: Vec<_> = issue.assigned().cloned().collect();
let Some((meta, description)) = prompt_issue( let Some((edited, description)) = prompt_issue(
issue.title(), issue.title(),
issue_desc, issue_desc,
&tags, &labels,
&assigned, &assigned,
)? else { )? else {
return Ok(()); return Ok(());
}; };
issue.transaction("Edit", signer, |tx| { issue.transaction("Edit", signer, |tx| {
tx.edit(meta.title)?; tx.edit(edited.title)?;
tx.edit_comment(desc_id, description)?; tx.edit_comment(desc_id, description)?;
tx.label(edited.labels)?;
let add: Vec<_> = meta tx.assign(edited.assignees)?;
.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(()) Ok(())
})?; })?;
@ -664,10 +634,10 @@ fn edit<R: WriteRepository + cob::Store, G: radicle::crypto::Signer>(
} }
fn show_issue(issue: &issue::Issue, id: &cob::ObjectId) -> anyhow::Result<()> { fn show_issue(issue: &issue::Issue, id: &cob::ObjectId) -> anyhow::Result<()> {
let tags: Vec<String> = issue.tags().cloned().map(|t| t.into()).collect(); let labels: Vec<String> = issue.labels().cloned().map(|t| t.into()).collect();
let assignees: Vec<String> = issue let assignees: Vec<String> = issue
.assigned() .assigned()
.map(|a| term::format::did(&a).to_string()) .map(|a| term::format::did(a).to_string())
.collect(); .collect();
let mut attrs = Table::<2, Paint<String>>::new(TableOptions { let mut attrs = Table::<2, Paint<String>>::new(TableOptions {
@ -685,10 +655,10 @@ fn show_issue(issue: &issue::Issue, id: &cob::ObjectId) -> anyhow::Result<()> {
term::format::bold(id.to_string()), term::format::bold(id.to_string()),
]); ]);
if !tags.is_empty() { if !labels.is_empty() {
attrs.push([ attrs.push([
term::format::tertiary("Tags".to_owned()), term::format::tertiary("Labels".to_owned()),
term::format::secondary(tags.join(", ")), term::format::secondary(labels.join(", ")),
]); ]);
} }

View File

@ -5,7 +5,7 @@ use anyhow::anyhow;
use nonempty::NonEmpty; use nonempty::NonEmpty;
use radicle::cob; use radicle::cob;
use radicle::cob::common::Tag; use radicle::cob::common::Label;
use radicle::cob::{issue, patch, store}; use radicle::cob::{issue, patch, store};
use radicle::crypto::Signer; use radicle::crypto::Signer;
use radicle::storage::{self, WriteStorage}; use radicle::storage::{self, WriteStorage};
@ -14,13 +14,15 @@ use crate::terminal as term;
use crate::terminal::args::{Args, Error, Help}; use crate::terminal::args::{Args, Error, Help};
pub const HELP: Help = Help { pub const HELP: Help = Help {
name: "tag", name: "label",
description: "Tag an issue or patch", description: "Label an issue or patch",
version: env!("CARGO_PKG_VERSION"), version: env!("CARGO_PKG_VERSION"),
usage: r#" usage: r#"
Usage Usage
rad tag <issue-id> <tag>... [<option>...] rad label <issue-id> <label>... [<option>...]
Adds the given labels to the patch or issue.
Options Options
@ -31,7 +33,7 @@ Options
#[derive(Debug)] #[derive(Debug)]
pub struct Options { pub struct Options {
pub id: cob::ObjectId, pub id: cob::ObjectId,
pub tags: NonEmpty<Tag>, pub labels: NonEmpty<Label>,
} }
impl Args for Options { impl Args for Options {
@ -40,7 +42,7 @@ impl Args for Options {
let mut parser = lexopt::Parser::from_args(args); let mut parser = lexopt::Parser::from_args(args);
let mut id: Option<cob::ObjectId> = None; let mut id: Option<cob::ObjectId> = None;
let mut tags: Vec<Tag> = Vec::new(); let mut labels: Vec<Label> = Vec::new();
while let Some(arg) = parser.next()? { while let Some(arg) = parser.next()? {
match arg { match arg {
@ -52,9 +54,9 @@ impl Args for Options {
} }
Value(ref val) if id.is_some() => { Value(ref val) if id.is_some() => {
let s: String = val.parse()?; let s: String = val.parse()?;
let tag = Tag::from_str(&s)?; let label = Label::from_str(&s)?;
tags.push(tag); labels.push(label);
} }
_ => { _ => {
return Err(anyhow!(arg.unexpected())); return Err(anyhow!(arg.unexpected()));
@ -65,15 +67,15 @@ impl Args for Options {
Ok(( Ok((
Options { Options {
id: id.ok_or_else(|| anyhow!("an issue or patch must be specified"))?, id: id.ok_or_else(|| anyhow!("an issue or patch must be specified"))?,
tags: NonEmpty::from_vec(tags) labels: NonEmpty::from_vec(labels)
.ok_or_else(|| anyhow!("at least one tag must be specified"))?, .ok_or_else(|| anyhow!("at least one label must be specified"))?,
}, },
vec![], vec![],
)) ))
} }
} }
fn tag( fn label(
options: Options, options: Options,
repo: &storage::git::Repository, repo: &storage::git::Repository,
signer: impl Signer, signer: impl Signer,
@ -81,7 +83,13 @@ fn tag(
let mut issues = issue::Issues::open(repo)?; let mut issues = issue::Issues::open(repo)?;
match issues.get_mut(&options.id) { match issues.get_mut(&options.id) {
Ok(mut issue) => { Ok(mut issue) => {
issue.tag(options.tags.into_iter(), [], &signer)?; let labels = issue
.labels()
.cloned()
.chain(options.labels.into_iter())
.collect::<Vec<_>>();
issue.label(labels, &signer)?;
return Ok(()); return Ok(());
} }
@ -92,7 +100,13 @@ fn tag(
let mut patches = patch::Patches::open(repo)?; let mut patches = patch::Patches::open(repo)?;
match patches.get_mut(&options.id) { match patches.get_mut(&options.id) {
Ok(mut patch) => { Ok(mut patch) => {
patch.tag(options.tags.into_iter(), [], &signer)?; let labels = patch
.labels()
.cloned()
.chain(options.labels.into_iter())
.collect::<Vec<_>>();
patch.label(labels, &signer)?;
return Ok(()); return Ok(());
} }
@ -109,7 +123,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
let repo = profile.storage.repository_mut(id)?; let repo = profile.storage.repository_mut(id)?;
let signer = term::signer(&profile)?; let signer = term::signer(&profile)?;
tag(options, &repo, signer)?; label(options, &repo, signer)?;
Ok(()) Ok(())
} }

View File

@ -256,7 +256,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
} else { } else {
Some(message) Some(message)
}; };
patch.review(revision_id, verdict, message, &signer)?; patch.review(revision_id, verdict, message, vec![], &signer)?;
match verdict { match verdict {
Some(Verdict::Accept) => { Some(Verdict::Accept) => {

View File

@ -86,8 +86,13 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
_ => e.into(), _ => e.into(),
})?; })?;
let signer = term::signer(&profile)?; let signer = term::signer(&profile)?;
let assigned = issue
.assigned()
.cloned()
.filter(|did| !options.from.contains(did))
.collect::<Vec<_>>();
issue.unassign(options.from.into_iter().map(Did::into), &signer)?; issue.assign(assigned, &signer)?;
Ok(()) Ok(())
} }

View File

@ -5,7 +5,7 @@ use anyhow::anyhow;
use nonempty::NonEmpty; use nonempty::NonEmpty;
use radicle::cob; use radicle::cob;
use radicle::cob::common::Tag; use radicle::cob::common::Label;
use radicle::cob::{issue, patch, store}; use radicle::cob::{issue, patch, store};
use radicle::crypto::Signer; use radicle::crypto::Signer;
use radicle::storage::{self, WriteStorage}; use radicle::storage::{self, WriteStorage};
@ -31,7 +31,7 @@ Options
#[derive(Debug)] #[derive(Debug)]
pub struct Options { pub struct Options {
pub id: cob::ObjectId, pub id: cob::ObjectId,
pub tags: NonEmpty<Tag>, pub labels: NonEmpty<Label>,
} }
impl Args for Options { impl Args for Options {
@ -40,7 +40,7 @@ impl Args for Options {
let mut parser = lexopt::Parser::from_args(args); let mut parser = lexopt::Parser::from_args(args);
let mut id: Option<cob::ObjectId> = None; let mut id: Option<cob::ObjectId> = None;
let mut tags: Vec<Tag> = Vec::new(); let mut labels: Vec<Label> = Vec::new();
while let Some(arg) = parser.next()? { while let Some(arg) = parser.next()? {
match arg { match arg {
@ -52,9 +52,9 @@ impl Args for Options {
} }
Value(ref val) if id.is_some() => { Value(ref val) if id.is_some() => {
let s: String = val.parse()?; let s: String = val.parse()?;
let tag = Tag::from_str(&s)?; let label = Label::from_str(&s)?;
tags.push(tag); labels.push(label);
} }
_ => { _ => {
return Err(anyhow!(arg.unexpected())); return Err(anyhow!(arg.unexpected()));
@ -65,15 +65,15 @@ impl Args for Options {
Ok(( Ok((
Options { Options {
id: id.ok_or_else(|| anyhow!("an issue or patch must be specified"))?, id: id.ok_or_else(|| anyhow!("an issue or patch must be specified"))?,
tags: NonEmpty::from_vec(tags) labels: NonEmpty::from_vec(labels)
.ok_or_else(|| anyhow!("at least one tag must be specified"))?, .ok_or_else(|| anyhow!("at least one label must be specified"))?,
}, },
vec![], vec![],
)) ))
} }
} }
fn untag( fn unlabel(
options: Options, options: Options,
repo: &storage::git::Repository, repo: &storage::git::Repository,
signer: impl Signer, signer: impl Signer,
@ -81,7 +81,12 @@ fn untag(
let mut issues = issue::Issues::open(repo)?; let mut issues = issue::Issues::open(repo)?;
match issues.get_mut(&options.id) { match issues.get_mut(&options.id) {
Ok(mut issue) => { Ok(mut issue) => {
issue.tag([], options.tags.into_iter(), &signer)?; let labels = issue
.labels()
.cloned()
.filter(|l| !options.labels.contains(l))
.collect::<Vec<_>>();
issue.label(labels, &signer)?;
return Ok(()); return Ok(());
} }
@ -92,7 +97,12 @@ fn untag(
let mut patches = patch::Patches::open(repo)?; let mut patches = patch::Patches::open(repo)?;
match patches.get_mut(&options.id) { match patches.get_mut(&options.id) {
Ok(mut patch) => { Ok(mut patch) => {
patch.tag([], options.tags.into_iter(), &signer)?; let labels = patch
.labels()
.cloned()
.filter(|l| !options.labels.contains(l))
.collect::<Vec<_>>();
patch.label(labels, &signer)?;
return Ok(()); return Ok(());
} }
@ -109,7 +119,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
let repo = profile.storage.repository_mut(id)?; let repo = profile.storage.repository_mut(id)?;
let signer = term::signer(&profile)?; let signer = term::signer(&profile)?;
untag(options, &repo, signer)?; unlabel(options, &repo, signer)?;
Ok(()) Ok(())
} }

View File

@ -265,11 +265,11 @@ fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option<anyhow::Error>>
args.to_vec(), args.to_vec(),
); );
} }
"tag" => { "label" => {
term::run_command_args::<rad_tag::Options, _>( term::run_command_args::<rad_label::Options, _>(
rad_tag::HELP, rad_label::HELP,
"Tag", "Label",
rad_tag::run, rad_label::run,
args.to_vec(), args.to_vec(),
); );
} }
@ -289,11 +289,11 @@ fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option<anyhow::Error>>
args.to_vec(), args.to_vec(),
); );
} }
"untag" => { "unlabel" => {
term::run_command_args::<rad_untag::Options, _>( term::run_command_args::<rad_unlabel::Options, _>(
rad_untag::HELP, rad_unlabel::HELP,
"Untag", "Unlabel",
rad_untag::run, rad_unlabel::run,
args.to_vec(), args.to_vec(),
); );
} }

View File

@ -91,7 +91,7 @@ fn rad_issue() {
} }
#[test] #[test]
fn rad_tag() { fn rad_label() {
let mut environment = Environment::new(); let mut environment = Environment::new();
let profile = environment.profile("alice"); let profile = environment.profile("alice");
let home = &profile.home; let home = &profile.home;
@ -102,7 +102,7 @@ fn rad_tag() {
test("examples/rad-init.md", &working, Some(home), []).unwrap(); test("examples/rad-init.md", &working, Some(home), []).unwrap();
test("examples/rad-issue.md", &working, Some(home), []).unwrap(); test("examples/rad-issue.md", &working, Some(home), []).unwrap();
test("examples/rad-tag.md", &working, Some(home), []).unwrap(); test("examples/rad-label.md", &working, Some(home), []).unwrap();
} }
#[test] #[test]

View File

@ -9,6 +9,7 @@ use git_ext::Oid;
use nonempty::NonEmpty; use nonempty::NonEmpty;
use radicle_git_ext::commit::trailers::OwnedTrailer; use radicle_git_ext::commit::trailers::OwnedTrailer;
use crate::change::store::Version;
use crate::history::entry::Timestamp; use crate::history::entry::Timestamp;
use crate::signatures; use crate::signatures;
use crate::{ use crate::{
@ -98,17 +99,12 @@ impl change::Storage for git2::Repository {
Signer: crypto::Signer, Signer: crypto::Signer,
{ {
let change::Template { let change::Template {
typename, type_name,
history_type,
tips, tips,
message, message,
contents, contents,
} = spec; } = spec;
let manifest = store::Manifest { let manifest = store::Manifest::new(type_name, Version::default());
typename,
history_type,
};
let revision = write_manifest(self, &manifest, &contents)?; let revision = write_manifest(self, &manifest, &contents)?;
let tree = self.find_tree(revision)?; let tree = self.find_tree(revision)?;
let signature = { let signature = {
@ -211,6 +207,7 @@ fn load_manifest(
let manifest_blob = manifest_object let manifest_blob = manifest_object
.as_blob() .as_blob()
.ok_or_else(|| error::Load::ManifestIsNotBlob(tree.id().into()))?; .ok_or_else(|| error::Load::ManifestIsNotBlob(tree.id().into()))?;
serde_json::from_slice(manifest_blob.content()).map_err(|err| error::Load::InvalidManifest { serde_json::from_slice(manifest_blob.content()).map_err(|err| error::Load::InvalidManifest {
id: tree.id().into(), id: tree.id().into(),
err, err,

View File

@ -1,6 +1,6 @@
// Copyright © 2022 The Radicle Link Contributors // Copyright © 2022 The Radicle Link Contributors
use std::{error::Error, fmt}; use std::{error::Error, fmt, num::NonZeroUsize};
use nonempty::NonEmpty; use nonempty::NonEmpty;
use radicle_git_ext::Oid; use radicle_git_ext::Oid;
@ -45,8 +45,7 @@ pub trait Storage {
/// Change template, used to create a new change. /// Change template, used to create a new change.
pub struct Template<Id> { pub struct Template<Id> {
pub typename: TypeName, pub type_name: TypeName,
pub history_type: String,
pub tips: Vec<Id>, pub tips: Vec<Id>,
pub message: String, pub message: String,
pub contents: NonEmpty<Vec<u8>>, pub contents: NonEmpty<Vec<u8>>,
@ -89,8 +88,8 @@ impl<Resource, Id, Signatures> Change<Resource, Id, Signatures> {
&self.id &self.id
} }
pub fn typename(&self) -> &TypeName { pub fn type_name(&self) -> &TypeName {
&self.manifest.typename &self.manifest.type_name
} }
pub fn contents(&self) -> &Contents { pub fn contents(&self) -> &Contents {
@ -122,10 +121,61 @@ where
} }
} }
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Manifest { pub struct Manifest {
/// The name given to the type of collaborative object. /// The name given to the type of collaborative object.
pub typename: TypeName, #[serde(alias = "typename")] // Deprecated name for compatibility reasons.
/// The type of history for the collaborative oject. pub type_name: TypeName,
pub history_type: String, /// Version number.
#[serde(default)]
pub version: Version,
/// History type (deprecated).
#[serde(alias = "history_type")]
_history_type: Option<String>,
}
impl Manifest {
/// Create a new manifest.
pub fn new(type_name: TypeName, version: Version) -> Self {
Self {
type_name,
version,
_history_type: None,
}
}
/// Whether this is an old COB. Remove this function when support for legacy COBs is over.
pub fn is_legacy(&self) -> bool {
self._history_type.is_some()
}
}
/// COB version.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Version(NonZeroUsize);
impl Default for Version {
fn default() -> Self {
Version(NonZeroUsize::MIN)
}
}
impl From<Version> for usize {
fn from(value: Version) -> Self {
value.0.into()
}
}
impl From<NonZeroUsize> for Version {
fn from(value: NonZeroUsize) -> Self {
Self(value)
}
}
impl Version {
pub fn new(version: usize) -> Option<Self> {
NonZeroUsize::new(version).map(Self)
}
} }

View File

@ -102,6 +102,7 @@ impl ChangeGraph {
change.resource, change.resource,
change.contents().clone(), change.contents().clone(),
change.timestamp, change.timestamp,
change.manifest.clone(),
); );
let id = *entry.id(); let id = *entry.id();

View File

@ -9,6 +9,8 @@ use radicle_dag::Dag;
pub mod entry; pub mod entry;
pub use entry::{Contents, Entry, EntryId, Timestamp}; pub use entry::{Contents, Entry, EntryId, Timestamp};
use crate::Manifest;
/// The DAG of changes making up the history of a collaborative object. /// The DAG of changes making up the history of a collaborative object.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct History { pub struct History {
@ -40,6 +42,7 @@ impl History {
resource: Oid, resource: Oid,
contents: Contents, contents: Contents,
timestamp: Timestamp, timestamp: Timestamp,
manifest: Manifest,
) -> Self ) -> Self
where where
Id: Into<EntryId>, Id: Into<EntryId>,
@ -51,6 +54,7 @@ impl History {
resource, resource,
contents, contents,
timestamp, timestamp,
manifest,
}; };
Self { Self {
@ -111,12 +115,20 @@ impl History {
new_resource: Oid, new_resource: Oid,
new_contents: Contents, new_contents: Contents,
new_timestamp: Timestamp, new_timestamp: Timestamp,
manifest: Manifest,
) where ) where
Id: Into<EntryId>, Id: Into<EntryId>,
{ {
let tips = self.tips(); let tips = self.tips();
let new_id = new_id.into(); let new_id = new_id.into();
let new_entry = Entry::new(new_id, new_actor, new_resource, new_contents, new_timestamp); let new_entry = Entry::new(
new_id,
new_actor,
new_resource,
new_contents,
new_timestamp,
manifest,
);
self.graph.node(new_id, new_entry); self.graph.node(new_id, new_entry);
@ -141,7 +153,10 @@ impl History {
} }
/// Get the root entry. /// Get the root entry.
pub fn root(&self) -> EntryId { pub fn root(&self) -> &Entry {
self.root // SAFETY: We don't allow construction of histories without a root.
self.graph
.get(&self.root)
.expect("History::root: the root entry must be present in the graph")
} }
} }

View File

@ -9,7 +9,7 @@ use nonempty::NonEmpty;
use radicle_crypto::PublicKey; use radicle_crypto::PublicKey;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{object, ObjectId}; use crate::{object, Manifest, ObjectId, Version};
/// Entry contents. /// Entry contents.
/// This is the change payload. /// This is the change payload.
@ -90,6 +90,8 @@ pub struct Entry {
pub(super) contents: Contents, pub(super) contents: Contents,
/// The entry timestamp, as seconds since epoch. /// The entry timestamp, as seconds since epoch.
pub(super) timestamp: Timestamp, pub(super) timestamp: Timestamp,
/// COB manifest.
pub(super) manifest: Manifest,
} }
impl Entry { impl Entry {
@ -99,6 +101,7 @@ impl Entry {
resource: Oid, resource: Oid,
contents: Contents, contents: Contents,
timestamp: Timestamp, timestamp: Timestamp,
manifest: Manifest,
) -> Self ) -> Self
where where
Id: Into<EntryId>, Id: Into<EntryId>,
@ -109,6 +112,7 @@ impl Entry {
resource, resource,
contents, contents,
timestamp, timestamp,
manifest,
} }
} }
@ -122,6 +126,16 @@ impl Entry {
&self.actor &self.actor
} }
/// The COB version of this entry.
pub fn version(&self) -> &Version {
&self.manifest.version
}
/// The COB manifest.
pub fn manifest(&self) -> &Manifest {
&self.manifest
}
/// The entry timestamp. /// The entry timestamp.
pub fn timestamp(&self) -> Timestamp { pub fn timestamp(&self) -> Timestamp {
self.timestamp self.timestamp

View File

@ -80,6 +80,7 @@ mod change_graph;
mod trailers; mod trailers;
pub mod change; pub mod change;
pub use change::store::{Manifest, Version};
pub use change::Change; pub use change::Change;
pub mod history; pub mod history;

View File

@ -4,7 +4,7 @@ use std::collections::BTreeSet;
use git_ext::Oid; use git_ext::Oid;
use crate::change::store::Manifest; use crate::change::store::{Manifest, Version};
use crate::{change, History, ObjectId, TypeName}; use crate::{change, History, ObjectId, TypeName};
pub mod error; pub mod error;
@ -47,7 +47,7 @@ impl CollaborativeObject {
} }
pub fn typename(&self) -> &TypeName { pub fn typename(&self) -> &TypeName {
&self.manifest.typename &self.manifest.type_name
} }
pub fn manifest(&self) -> &Manifest { pub fn manifest(&self) -> &Manifest {

View File

@ -8,21 +8,20 @@ use super::*;
/// The metadata required for creating a new [`CollaborativeObject`]. /// The metadata required for creating a new [`CollaborativeObject`].
pub struct Create { pub struct Create {
/// The type of history that will be used for this object.
pub history_type: String,
/// The CRDT history to initialize this object with. /// The CRDT history to initialize this object with.
pub contents: NonEmpty<Vec<u8>>, pub contents: NonEmpty<Vec<u8>>,
/// The typename for this object. /// The typename for this object.
pub typename: TypeName, pub type_name: TypeName,
/// The message to add when creating this object. /// The message to add when creating this object.
pub message: String, pub message: String,
/// COB version.
pub version: Version,
} }
impl Create { impl Create {
fn template(&self) -> change::Template<git_ext::Oid> { fn template(&self) -> change::Template<git_ext::Oid> {
change::Template { change::Template {
typename: self.typename.clone(), type_name: self.type_name.clone(),
history_type: self.history_type.clone(),
tips: Vec::new(), tips: Vec::new(),
message: self.message.clone(), message: self.message.clone(),
contents: self.contents.clone(), contents: self.contents.clone(),
@ -60,14 +59,14 @@ where
S: Store<I>, S: Store<I>,
G: crypto::Signer, G: crypto::Signer,
{ {
let Create { ref typename, .. } = &args; let Create { type_name, .. } = &args;
let init_change = storage let init_change = storage
.store(resource, parents, signer, args.template()) .store(resource, parents, signer, args.template())
.map_err(error::Create::from)?; .map_err(error::Create::from)?;
let object_id = init_change.id().into(); let object_id = init_change.id().into();
storage storage
.update(identifier, typename, &object_id, &init_change) .update(identifier, type_name, &object_id, &init_change)
.map_err(|err| error::Create::Refs { err: Box::new(err) })?; .map_err(|err| error::Create::Refs { err: Box::new(err) })?;
let history = History::new_from_root( let history = History::new_from_root(
@ -76,13 +75,11 @@ where
resource, resource,
init_change.contents, init_change.contents,
init_change.timestamp, init_change.timestamp,
init_change.manifest,
); );
Ok(CollaborativeObject { Ok(CollaborativeObject {
manifest: Manifest { manifest: Manifest::new(args.type_name, args.version),
typename: args.typename,
history_type: args.history_type,
},
history, history,
id: object_id, id: object_id,
}) })

View File

@ -18,14 +18,12 @@ pub struct Updated {
/// The data required to update an object /// The data required to update an object
pub struct Update { pub struct Update {
/// The type of history that will be used for this object.
pub history_type: String,
/// The CRDT changes to add to the object. /// The CRDT changes to add to the object.
pub changes: NonEmpty<Vec<u8>>, pub changes: NonEmpty<Vec<u8>>,
/// The object ID of the object to be updated. /// The object ID of the object to be updated.
pub object_id: ObjectId, pub object_id: ObjectId,
/// The typename of the object to be updated. /// The typename of the object to be updated.
pub typename: TypeName, pub type_name: TypeName,
/// The message to add when updating this object. /// The message to add when updating this object.
pub message: String, pub message: String,
} }
@ -62,9 +60,8 @@ where
G: crypto::Signer, G: crypto::Signer,
{ {
let Update { let Update {
ref typename, type_name: ref typename,
object_id, object_id,
history_type,
changes, changes,
message, message,
} = args; } = args;
@ -83,9 +80,8 @@ where
signer, signer,
change::Template { change::Template {
tips: object.tips().iter().cloned().collect(), tips: object.tips().iter().cloned().collect(),
history_type,
contents: changes, contents: changes,
typename: typename.clone(), type_name: typename.clone(),
message, message,
}, },
)?; )?;
@ -100,6 +96,7 @@ where
change.resource, change.resource,
change.contents, change.contents,
change.timestamp, change.timestamp,
change.manifest,
); );
Ok(Updated { Ok(Updated {

View File

@ -8,7 +8,7 @@ use radicle_crypto::Signer;
use crate::{ use crate::{
create, get, list, object, test::arbitrary::Invalid, update, Create, ObjectId, TypeName, create, get, list, object, test::arbitrary::Invalid, update, Create, ObjectId, TypeName,
Update, Updated, Update, Updated, Version,
}; };
use super::test; use super::test;
@ -31,10 +31,10 @@ fn roundtrip() {
vec![], vec![],
&proj.identifier(), &proj.identifier(),
Create { Create {
history_type: "test".to_string(),
contents: nonempty!(Vec::new()), contents: nonempty!(Vec::new()),
typename: typename.clone(), type_name: typename.clone(),
message: "creating xyz.rad.issue".to_string(), message: "creating xyz.rad.issue".to_string(),
version: Version::default(),
}, },
) )
.unwrap(); .unwrap();
@ -64,10 +64,10 @@ fn list_cobs() {
vec![], vec![],
&proj.identifier(), &proj.identifier(),
Create { Create {
history_type: "test".to_string(),
contents: nonempty!(b"issue 1".to_vec()), contents: nonempty!(b"issue 1".to_vec()),
typename: typename.clone(), type_name: typename.clone(),
message: "creating xyz.rad.issue".to_string(), message: "creating xyz.rad.issue".to_string(),
version: Version::default(),
}, },
) )
.unwrap(); .unwrap();
@ -79,10 +79,10 @@ fn list_cobs() {
vec![], vec![],
&proj.identifier(), &proj.identifier(),
Create { Create {
history_type: "test".to_string(),
contents: nonempty!(b"issue 2".to_vec()), contents: nonempty!(b"issue 2".to_vec()),
typename: typename.clone(), type_name: typename.clone(),
message: "commenting xyz.rad.issue".to_string(), message: "commenting xyz.rad.issue".to_string(),
version: Version::default(),
}, },
) )
.unwrap(); .unwrap();
@ -114,10 +114,10 @@ fn update_cob() {
vec![], vec![],
&proj.identifier(), &proj.identifier(),
Create { Create {
history_type: "test".to_string(),
contents: nonempty!(Vec::new()), contents: nonempty!(Vec::new()),
typename: typename.clone(), type_name: typename.clone(),
message: "creating xyz.rad.issue".to_string(), message: "creating xyz.rad.issue".to_string(),
version: Version::default(),
}, },
) )
.unwrap(); .unwrap();
@ -134,9 +134,8 @@ fn update_cob() {
&proj.identifier(), &proj.identifier(),
Update { Update {
changes: nonempty!(b"issue 1".to_vec()), changes: nonempty!(b"issue 1".to_vec()),
history_type: "test".to_string(),
object_id: *cob.id(), object_id: *cob.id(),
typename: typename.clone(), type_name: typename.clone(),
message: "commenting xyz.rad.issue".to_string(), message: "commenting xyz.rad.issue".to_string(),
}, },
) )
@ -175,9 +174,9 @@ fn traverse_cobs() {
&terry_proj.identifier(), &terry_proj.identifier(),
Create { Create {
contents: nonempty!(b"issue 1".to_vec()), contents: nonempty!(b"issue 1".to_vec()),
history_type: "test".to_string(), type_name: typename.clone(),
typename: typename.clone(),
message: "creating xyz.rad.issue".to_string(), message: "creating xyz.rad.issue".to_string(),
version: Version::default(),
}, },
) )
.unwrap(); .unwrap();
@ -198,9 +197,8 @@ fn traverse_cobs() {
&neil_proj.identifier(), &neil_proj.identifier(),
Update { Update {
changes: nonempty!(b"issue 2".to_vec()), changes: nonempty!(b"issue 2".to_vec()),
history_type: "test".to_string(),
object_id: *cob.id(), object_id: *cob.id(),
typename, type_name: typename,
message: "commenting on xyz.rad.issue".to_string(), message: "commenting on xyz.rad.issue".to_string(),
}, },
) )

View File

@ -77,20 +77,29 @@ pub enum Error {
/// Routing store error. /// Routing store error.
#[error(transparent)] #[error(transparent)]
RoutingStore(#[from] radicle::node::routing::Error), RoutingStore(#[from] radicle::node::routing::Error),
/// Invalid update to issue or patch.
#[error("{0}")]
BadRequest(String),
} }
impl IntoResponse for Error { impl IntoResponse for Error {
fn into_response(self) -> Response { fn into_response(self) -> Response {
let (status, msg) = match &self { let message = self.to_string();
let (status, msg) = match self {
Error::NotFound => (StatusCode::NOT_FOUND, None), Error::NotFound => (StatusCode::NOT_FOUND, None),
Error::CobStore(radicle::cob::store::Error::NotFound(_, _)) => {
(StatusCode::NOT_FOUND, None)
}
Error::Auth(msg) => (StatusCode::BAD_REQUEST, Some(msg.to_string())), Error::Auth(msg) => (StatusCode::BAD_REQUEST, Some(msg.to_string())),
Error::Crypto(msg) => (StatusCode::BAD_REQUEST, Some(msg.to_string())), Error::Crypto(msg) => (StatusCode::BAD_REQUEST, Some(msg.to_string())),
Error::Git2(e) => ( Error::Git2(e) => (
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
Some(e.message().to_owned()), Some(e.message().to_owned()),
), ),
Error::BadRequest(msg) => (StatusCode::BAD_REQUEST, Some(msg)),
other => { other => {
tracing::error!("Error: {:?}", &self); tracing::error!("Error: {message}");
if cfg!(debug_assertions) { if cfg!(debug_assertions) {
( (

View File

@ -108,7 +108,7 @@ pub(crate) fn issue(id: IssueId, issue: Issue, aliases: &impl AliasStore) -> Val
.comments() .comments()
.map(|(id, comment)| Comment::new(id, comment, aliases)) .map(|(id, comment)| Comment::new(id, comment, aliases))
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
"tags": issue.tags().collect::<Vec<_>>(), "labels": issue.labels().collect::<Vec<_>>(),
}) })
} }
@ -125,9 +125,9 @@ pub(crate) fn patch(
"title": patch.title(), "title": patch.title(),
"state": patch.state(), "state": patch.state(),
"target": patch.target(), "target": patch.target(),
"tags": patch.tags().collect::<Vec<_>>(), "labels": patch.labels().collect::<Vec<_>>(),
"merges": patch.merges().map(|(nid, m)| merge(m, nid, aliases.alias(nid))).collect::<Vec<_>>(), "merges": patch.merges().map(|(nid, m)| merge(m, nid, aliases.alias(nid))).collect::<Vec<_>>(),
"reviewers": patch.reviewers().collect::<Vec<_>>(), "assignees": patch.assignees().collect::<Vec<_>>(),
"revisions": patch.revisions().map(|(id, rev)| { "revisions": patch.revisions().map(|(id, rev)| {
json!({ json!({
"id": id, "id": id,

View File

@ -12,8 +12,8 @@ use serde::{Deserialize, Serialize};
use serde_json::json; use serde_json::json;
use tower_http::set_header::SetResponseHeaderLayer; use tower_http::set_header::SetResponseHeaderLayer;
use radicle::cob::{issue, patch, thread, ActorId, Tag}; use radicle::cob::{issue, patch, Label};
use radicle::identity::Id; use radicle::identity::{Did, Id};
use radicle::node::routing::Store; use radicle::node::routing::Store;
use radicle::node::AliasStore; use radicle::node::AliasStore;
use radicle::node::NodeId; use radicle::node::NodeId;
@ -481,8 +481,8 @@ async fn issues_handler(
pub struct IssueCreate { pub struct IssueCreate {
pub title: String, pub title: String,
pub description: String, pub description: String,
pub tags: Vec<Tag>, pub labels: Vec<Label>,
pub assignees: Vec<ActorId>, pub assignees: Vec<Did>,
} }
/// Create a new issue. /// Create a new issue.
@ -505,7 +505,7 @@ async fn issue_create_handler(
.create( .create(
issue.title, issue.title,
issue.description, issue.description,
&issue.tags, &issue.labels,
&issue.assignees, &issue.assignees,
&signer, &signer,
) )
@ -526,6 +526,7 @@ async fn issue_update_handler(
Json(action): Json<issue::Action>, Json(action): Json<issue::Action>,
) -> impl IntoResponse { ) -> impl IntoResponse {
api::auth::validate(&ctx, &token).await?; api::auth::validate(&ctx, &token).await?;
let storage = &ctx.profile.storage; let storage = &ctx.profile.storage;
let signer = ctx.profile.signer().unwrap(); let signer = ctx.profile.signer().unwrap();
let repo = storage.repository(project)?; let repo = storage.repository(project)?;
@ -533,37 +534,34 @@ async fn issue_update_handler(
let mut issue = issues.get_mut(&issue_id.into())?; let mut issue = issues.get_mut(&issue_id.into())?;
match action { match action {
issue::Action::Assign { add, remove } => { issue::Action::Assign { assignees } => {
issue.assign(add, &signer)?; issue.assign(assignees, &signer)?;
issue.unassign(remove, &signer)?;
} }
issue::Action::Lifecycle { state } => { issue::Action::Lifecycle { state } => {
issue.lifecycle(state, &signer)?; issue.lifecycle(state, &signer)?;
} }
issue::Action::Tag { add, remove } => { issue::Action::Label { labels } => {
issue.tag(add, remove, &signer)?; issue.label(labels, &signer)?;
} }
issue::Action::Edit { title } => { issue::Action::Edit { title } => {
issue.edit(title, &signer)?; issue.edit(title, &signer)?;
} }
issue::Action::Thread { action } => match action { issue::Action::Comment { body, reply_to } => {
thread::Action::Comment { body, reply_to } => { if let Some(to) = reply_to {
if let Some(reply_to) = reply_to { issue.comment(body, to, &signer)?;
issue.comment(body, reply_to, &signer)?;
} else { } else {
issue.thread(body, &signer)?; return Err(Error::BadRequest("`replyTo` missing".to_owned()));
} }
} }
thread::Action::React { to, reaction, .. } => { issue::Action::CommentReact { id, reaction, .. } => {
issue.react(to, reaction, &signer)?; issue.react(id, reaction, &signer)?;
} }
thread::Action::Edit { .. } => { issue::Action::CommentEdit { .. } => {
todo!(); todo!();
} }
thread::Action::Redact { .. } => { issue::Action::CommentRedact { .. } => {
todo!(); todo!();
} }
},
}; };
Ok::<_, Error>(Json(json!({ "success": true }))) Ok::<_, Error>(Json(json!({ "success": true })))
@ -591,7 +589,7 @@ pub struct PatchCreate {
pub description: String, pub description: String,
pub target: Oid, pub target: Oid,
pub oid: Oid, pub oid: Oid,
pub tags: Vec<Tag>, pub labels: Vec<Label>,
} }
/// Create a new patch. /// Create a new patch.
@ -619,7 +617,7 @@ async fn patch_create_handler(
patch::MergeTarget::default(), patch::MergeTarget::default(),
base_oid, base_oid,
patch.oid, patch.oid,
&patch.tags, &patch.labels,
&signer, &signer,
) )
.map_err(Error::from)?; .map_err(Error::from)?;
@ -650,35 +648,33 @@ async fn patch_update_handler(
patch::Action::Edit { title, target } => { patch::Action::Edit { title, target } => {
patch.edit(title, target, &signer)?; patch.edit(title, target, &signer)?;
} }
patch::Action::EditRevision { patch::Action::RevisionEdit {
revision, revision,
description, description,
} => { } => {
patch.edit_revision(revision, description, &signer)?; patch.edit_revision(revision, description, &signer)?;
} }
patch::Action::EditReview { review, summary } => { patch::Action::ReviewEdit { review, summary } => {
patch.edit_review(review, summary, &signer)?; patch.edit_review(review, summary, &signer)?;
} }
patch::Action::EditCodeComment { patch::Action::ReviewCommentEdit {
review, review,
comment, comment,
body, body,
} => { } => {
patch.edit_code_comment(review, comment, body, &signer)?; patch.edit_review_comment(review, comment, body, &signer)?;
} }
patch::Action::Tag { add, remove } => { patch::Action::Label { labels } => {
patch.tag(add, remove, &signer)?; patch.label(labels, &signer)?;
} }
patch::Action::Revision { patch::Action::Revision {
description, description,
base, base,
oid, oid,
..
} => { } => {
patch.update(description, base, oid, &signer)?; patch.update(description, base, oid, &signer)?;
} }
patch::Action::Redact { .. } => {
todo!()
}
patch::Action::Lifecycle { state } => { patch::Action::Lifecycle { state } => {
patch.lifecycle(state, &signer)?; patch.lifecycle(state, &signer)?;
} }
@ -686,33 +682,24 @@ async fn patch_update_handler(
revision, revision,
summary, summary,
verdict, verdict,
labels,
} => { } => {
patch.review(revision, verdict, summary, &signer)?; patch.review(revision, verdict, summary, labels, &signer)?;
}
patch::Action::CodeComment { .. } => {
todo!()
} }
patch::Action::Merge { revision, commit } => { patch::Action::Merge { revision, commit } => {
patch.merge(revision, commit, &signer)?; patch.merge(revision, commit, &signer)?;
} }
patch::Action::Thread { action, revision } => match action { patch::Action::RevisionComment {
thread::Action::Comment { body, reply_to } => { revision,
if let Some(reply_to) = reply_to { body,
patch.comment(revision, body, Some(reply_to), &signer)?; reply_to,
} else { ..
patch.thread(revision, body, &signer)?; } => {
patch.comment(revision, body, reply_to, &signer)?;
} }
} _ => {
thread::Action::Edit { .. } => {
todo!(); todo!();
} }
thread::Action::Redact { .. } => {
todo!();
}
thread::Action::React { .. } => {
todo!();
}
},
}; };
Ok::<_, Error>(Json(json!({ "success": true }))) Ok::<_, Error>(Json(json!({ "success": true })))
@ -1635,7 +1622,7 @@ mod routes {
"replyTo": null "replyTo": null
} }
], ],
"tags": [] "labels": []
} }
]) ])
); );
@ -1643,7 +1630,7 @@ mod routes {
#[tokio::test] #[tokio::test]
async fn test_projects_issues_create() { async fn test_projects_issues_create() {
const CREATED_ISSUE_ID: &str = "b457364fbe2ef0eac69a835a087f60ee13ccb367"; const CREATED_ISSUE_ID: &str = "c7cff5ab610408470406e023baa1ab087ce78adc";
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
let ctx = contributor(tmp.path()); let ctx = contributor(tmp.path());
@ -1654,7 +1641,7 @@ mod routes {
let body = serde_json::to_vec(&json!({ let body = serde_json::to_vec(&json!({
"title": "Issue #2", "title": "Issue #2",
"description": "Change 'hello world' to 'hello everyone'", "description": "Change 'hello world' to 'hello everyone'",
"tags": ["bug"], "labels": ["bug"],
"assignees": [], "assignees": [],
})) }))
.unwrap(); .unwrap();
@ -1701,7 +1688,7 @@ mod routes {
"timestamp": TIMESTAMP, "timestamp": TIMESTAMP,
"replyTo": null, "replyTo": null,
}], }],
"tags": [ "labels": [
"bug", "bug",
], ],
}) })
@ -1717,11 +1704,9 @@ mod routes {
create_session(ctx).await; create_session(ctx).await;
let body = serde_json::to_vec(&json!({ let body = serde_json::to_vec(&json!({
"type": "thread",
"action": {
"type": "comment", "type": "comment",
"body": "This is first-level comment", "body": "This is first-level comment",
} "replyTo": CONTRIBUTOR_ISSUE_ID,
})) }))
.unwrap(); .unwrap();
@ -1737,13 +1722,10 @@ mod routes {
assert_eq!(response.json().await, json!({ "success": true })); assert_eq!(response.json().await, json!({ "success": true }));
let body = serde_json::to_vec(&json!({ let body = serde_json::to_vec(&json!({
"type": "thread", "type": "comment.react",
"action": { "id": "26cadcc7cb51ee9c56b6232023e9bf63b7b0df60",
"type": "react",
"to": "9685b141c2e939c3d60f8ca34f8c7bf01a609af1",
"reaction": "🚀", "reaction": "🚀",
"active": true, "active": true,
}
})) }))
.unwrap(); .unwrap();
patch( patch(
@ -1784,7 +1766,7 @@ mod routes {
"replyTo": null, "replyTo": null,
}, },
{ {
"id": "9685b141c2e939c3d60f8ca34f8c7bf01a609af1", "id": "26cadcc7cb51ee9c56b6232023e9bf63b7b0df60",
"author": { "author": {
"id": CONTRIBUTOR_DID, "id": CONTRIBUTOR_DID,
}, },
@ -1796,10 +1778,10 @@ mod routes {
], ],
], ],
"timestamp": TIMESTAMP, "timestamp": TIMESTAMP,
"replyTo": null, "replyTo": CONTRIBUTOR_ISSUE_ID,
}, },
], ],
"tags": [], "labels": [],
}) })
); );
} }
@ -1813,12 +1795,9 @@ mod routes {
create_session(ctx).await; create_session(ctx).await;
let body = serde_json::to_vec(&json!({ let body = serde_json::to_vec(&json!({
"type": "thread",
"action": {
"type": "comment", "type": "comment",
"body": "This is a reply to the first comment", "body": "This is a reply to the first comment",
"replyTo": ISSUE_DISCUSSION_ID, "replyTo": ISSUE_DISCUSSION_ID,
}
})) }))
.unwrap(); .unwrap();
@ -1874,7 +1853,7 @@ mod routes {
"replyTo": ISSUE_DISCUSSION_ID, "replyTo": ISSUE_DISCUSSION_ID,
}, },
], ],
"tags": [], "labels": [],
}) })
); );
} }
@ -1898,9 +1877,9 @@ mod routes {
"title": "A new `hello world`", "title": "A new `hello world`",
"state": { "status": "open" }, "state": { "status": "open" },
"target": "delegates", "target": "delegates",
"tags": [], "labels": [],
"merges": [], "merges": [],
"reviewers": [], "assignees": [],
"revisions": [ "revisions": [
{ {
"id": CONTRIBUTOR_PATCH_ID, "id": CONTRIBUTOR_PATCH_ID,
@ -1940,9 +1919,9 @@ mod routes {
"title": "A new `hello world`", "title": "A new `hello world`",
"state": { "status": "open" }, "state": { "status": "open" },
"target": "delegates", "target": "delegates",
"tags": [], "labels": [],
"merges": [], "merges": [],
"reviewers": [], "assignees": [],
"revisions": [ "revisions": [
{ {
"id": CONTRIBUTOR_PATCH_ID, "id": CONTRIBUTOR_PATCH_ID,
@ -1967,7 +1946,7 @@ mod routes {
#[tokio::test] #[tokio::test]
async fn test_projects_create_patches() { async fn test_projects_create_patches() {
const CREATED_PATCH_ID: &str = "768e76ae6611d9392f04122a5aa7a587b47b9e19"; const CREATED_PATCH_ID: &str = "9cffd66099cceb0439a0f67c4aa99bde5e868eaa";
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
let ctx = contributor(tmp.path()); let ctx = contributor(tmp.path());
@ -1980,7 +1959,7 @@ mod routes {
"description": "Do some changes to README", "description": "Do some changes to README",
"target": INITIAL_COMMIT, "target": INITIAL_COMMIT,
"oid": HEAD, "oid": HEAD,
"tags": [], "labels": [],
})) }))
.unwrap(); .unwrap();
@ -2021,9 +2000,9 @@ mod routes {
"title": "Update README", "title": "Update README",
"state": { "status": "open" }, "state": { "status": "open" },
"target": "delegates", "target": "delegates",
"tags": [], "labels": [],
"merges": [], "merges": [],
"reviewers": [], "assignees": [],
"revisions": [ "revisions": [
{ {
"id": CREATED_PATCH_ID, "id": CREATED_PATCH_ID,
@ -2053,9 +2032,8 @@ mod routes {
let app = super::router(ctx.to_owned()); let app = super::router(ctx.to_owned());
create_session(ctx).await; create_session(ctx).await;
let body = serde_json::to_vec(&json!({ let body = serde_json::to_vec(&json!({
"type": "tag", "type": "label",
"add": ["bug","design"], "labels": ["bug","design"],
"remove": []
})) }))
.unwrap(); .unwrap();
let response = patch( let response = patch(
@ -2084,12 +2062,12 @@ mod routes {
"title": "A new `hello world`", "title": "A new `hello world`",
"state": { "status": "open" }, "state": { "status": "open" },
"target": "delegates", "target": "delegates",
"tags": [ "labels": [
"bug", "bug",
"design" "design"
], ],
"merges": [], "merges": [],
"reviewers": [], "assignees": [],
"revisions": [ "revisions": [
{ {
"id": CONTRIBUTOR_PATCH_ID, "id": CONTRIBUTOR_PATCH_ID,
@ -2150,9 +2128,9 @@ mod routes {
"title": "A new `hello world`", "title": "A new `hello world`",
"state": { "status": "open" }, "state": { "status": "open" },
"target": "delegates", "target": "delegates",
"tags": [], "labels": [],
"merges": [], "merges": [],
"reviewers": [], "assignees": [],
"revisions": [ "revisions": [
{ {
"id": CONTRIBUTOR_PATCH_ID, "id": CONTRIBUTOR_PATCH_ID,
@ -2170,7 +2148,7 @@ mod routes {
"reviews": [], "reviews": [],
}, },
{ {
"id": "181e4219bc132e7716126a84200d4dbd628dd6be", "id": "b1f68feacb7040b089a77c1a0bff60a0411e6c1e",
"author": { "author": {
"id": CONTRIBUTOR_DID, "id": CONTRIBUTOR_DID,
}, },
@ -2228,9 +2206,9 @@ mod routes {
"title": "This is a updated title", "title": "This is a updated title",
"state": { "status": "open" }, "state": { "status": "open" },
"target": "delegates", "target": "delegates",
"tags": [], "labels": [],
"merges": [], "merges": [],
"reviewers": [], "assignees": [],
"revisions": [ "revisions": [
{ {
"id": CONTRIBUTOR_PATCH_ID, "id": CONTRIBUTOR_PATCH_ID,
@ -2259,12 +2237,9 @@ mod routes {
let app = super::router(ctx.to_owned()); let app = super::router(ctx.to_owned());
create_session(ctx).await; create_session(ctx).await;
let thread_body = serde_json::to_vec(&json!({ let thread_body = serde_json::to_vec(&json!({
"type": "thread", "type": "revision.comment",
"revision": CONTRIBUTOR_PATCH_ID, "revision": CONTRIBUTOR_PATCH_ID,
"action": {
"type": "comment",
"body": "This is a root level comment" "body": "This is a root level comment"
}
})) }))
.unwrap(); .unwrap();
let response = patch( let response = patch(
@ -2278,13 +2253,10 @@ mod routes {
assert_eq!(response.status(), StatusCode::OK); assert_eq!(response.status(), StatusCode::OK);
let reply_body = serde_json::to_vec(&json!({ let reply_body = serde_json::to_vec(&json!({
"type": "thread", "type": "revision.comment",
"revision": CONTRIBUTOR_PATCH_ID, "revision": CONTRIBUTOR_PATCH_ID,
"action": {
"type": "comment",
"body": "This is a root level comment", "body": "This is a root level comment",
"replyTo": CONTRIBUTOR_COMMENT_1, "replyTo": CONTRIBUTOR_COMMENT_1,
}
})) }))
.unwrap(); .unwrap();
let response = patch( let response = patch(
@ -2313,9 +2285,9 @@ mod routes {
"title": "A new `hello world`", "title": "A new `hello world`",
"state": { "status": "open" }, "state": { "status": "open" },
"target": "delegates", "target": "delegates",
"tags": [], "labels": [],
"merges": [], "merges": [],
"reviewers": [], "assignees": [],
"revisions": [ "revisions": [
{ {
"id": CONTRIBUTOR_PATCH_ID, "id": CONTRIBUTOR_PATCH_ID,
@ -2397,9 +2369,9 @@ mod routes {
"title": "A new `hello world`", "title": "A new `hello world`",
"state": { "status": "open" }, "state": { "status": "open" },
"target": "delegates", "target": "delegates",
"tags": [], "labels": [],
"merges": [], "merges": [],
"reviewers": [], "assignees": [],
"revisions": [ "revisions": [
{ {
"id": CONTRIBUTOR_PATCH_ID, "id": CONTRIBUTOR_PATCH_ID,
@ -2473,7 +2445,7 @@ mod routes {
"commit": PARENT, "commit": PARENT,
}, },
"target": "delegates", "target": "delegates",
"tags": [], "labels": [],
"merges": [{ "merges": [{
"author": { "author": {
"id": CONTRIBUTOR_NID, "id": CONTRIBUTOR_NID,
@ -2482,7 +2454,7 @@ mod routes {
"commit": PARENT, "commit": PARENT,
"timestamp": TIMESTAMP, "timestamp": TIMESTAMP,
}], }],
"reviewers": [], "assignees": [],
"revisions": [ "revisions": [
{ {
"id": CONTRIBUTOR_PATCH_ID, "id": CONTRIBUTOR_PATCH_ID,

View File

@ -33,18 +33,18 @@ pub const HEAD: &str = "e8c676b9e3b42308dc9d218b70faa5408f8e58ca";
pub const PARENT: &str = "ee8d6a29304623a78ebfa5eeed5af674d0e58f83"; pub const PARENT: &str = "ee8d6a29304623a78ebfa5eeed5af674d0e58f83";
pub const INITIAL_COMMIT: &str = "f604ce9fd5b7cc77b7609beda45ea8760bee78f7"; pub const INITIAL_COMMIT: &str = "f604ce9fd5b7cc77b7609beda45ea8760bee78f7";
pub const DID: &str = "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"; pub const DID: &str = "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi";
pub const ISSUE_ID: &str = "5ad77fa3f476beed9a26f49b2b3b844e61bef792"; pub const ISSUE_ID: &str = "0b0b8ca3b75e109971f87d92c1a6c930e87484c6";
pub const ISSUE_DISCUSSION_ID: &str = "f1dff128a22e8183a23516dd9812e72e80914c92"; pub const ISSUE_DISCUSSION_ID: &str = "7466975f0bef37b459887824a9655f3e78262522";
pub const ISSUE_COMMENT_ID: &str = "845218041bf9eb8155bfa4aaa8f0c91ce18e5c13"; pub const ISSUE_COMMENT_ID: &str = "24ee306c508cd731a8427612dbdd826209096f99";
pub const SESSION_ID: &str = "u9MGAkkfkMOv0uDDB2WeUHBT7HbsO2Dy"; pub const SESSION_ID: &str = "u9MGAkkfkMOv0uDDB2WeUHBT7HbsO2Dy";
pub const TIMESTAMP: u64 = 1671125284; pub const TIMESTAMP: u64 = 1671125284;
pub const CONTRIBUTOR_RID: &str = "rad:z4XaCmN3jLSeiMvW15YTDpNbDHFhG"; pub const CONTRIBUTOR_RID: &str = "rad:z4XaCmN3jLSeiMvW15YTDpNbDHFhG";
pub const CONTRIBUTOR_DID: &str = "did:key:z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8"; pub const CONTRIBUTOR_DID: &str = "did:key:z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8";
pub const CONTRIBUTOR_NID: &str = "z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8"; pub const CONTRIBUTOR_NID: &str = "z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8";
pub const CONTRIBUTOR_ISSUE_ID: &str = "f1dff128a22e8183a23516dd9812e72e80914c92"; pub const CONTRIBUTOR_ISSUE_ID: &str = "7466975f0bef37b459887824a9655f3e78262522";
pub const CONTRIBUTOR_PATCH_ID: &str = "044b577cc7551cd09d4b2f03566a553762180de4"; pub const CONTRIBUTOR_PATCH_ID: &str = "e651ae5869a2c1ac8ad4f6deae4cc835656ffa25";
pub const CONTRIBUTOR_COMMENT_1: &str = "92aab76516ae7f60a9b2952043ba578383de7d46"; pub const CONTRIBUTOR_COMMENT_1: &str = "d0bb75b2c72ab8b5486d39f6cf5f41f104b63cb1";
pub const CONTRIBUTOR_COMMENT_2: &str = "cb360eee0ec70563d5c4c3613fdc076648523248"; pub const CONTRIBUTOR_COMMENT_2: &str = "2a4ec5bcb1be09c1f2213f418c0159fff894b989";
/// Create a new profile. /// Create a new profile.
pub fn profile(home: &Path, seed: [u8; 32]) -> radicle::Profile { pub fn profile(home: &Path, seed: [u8; 32]) -> radicle::Profile {
@ -297,6 +297,7 @@ fn request(
request.body(body.unwrap_or_else(Body::empty)).unwrap() request.body(body.unwrap_or_else(Body::empty)).unwrap()
} }
#[derive(Debug)]
pub struct Response(axum::response::Response); pub struct Response(axum::response::Response);
impl Response { impl Response {

View File

@ -7,13 +7,13 @@ use std::{assert_eq, io};
use thiserror::Error; use thiserror::Error;
use radicle::cob::object::ParseObjectId;
use radicle::cob::patch; use radicle::cob::patch;
use radicle::crypto::{PublicKey, Signer}; use radicle::crypto::{PublicKey, Signer};
use radicle::node; use radicle::node;
use radicle::node::{Handle, NodeId}; use radicle::node::{Handle, NodeId};
use radicle::prelude::Id; use radicle::prelude::Id;
use radicle::storage; use radicle::storage;
use radicle::storage::git::cob::object::ParseObjectId;
use radicle::storage::git::transport::local::Url; use radicle::storage::git::transport::local::Url;
use radicle::storage::{ReadRepository, SignRepository as _, WriteRepository}; use radicle::storage::{ReadRepository, SignRepository as _, WriteRepository};
use radicle::Profile; use radicle::Profile;

View File

@ -10,7 +10,7 @@ use radicle::Profile;
use radicle::cob::issue::{Issue, IssueId, State as IssueState}; use radicle::cob::issue::{Issue, IssueId, State as IssueState};
use radicle::cob::patch::{Patch, PatchId, State as PatchState}; use radicle::cob::patch::{Patch, PatchId, State as PatchState};
use radicle::cob::{Tag, Timestamp}; use radicle::cob::{Label, Timestamp};
use tuirealm::props::{Color, Style}; use tuirealm::props::{Color, Style};
use tuirealm::tui::text::{Span, Spans}; use tuirealm::tui::text::{Span, Spans};
@ -172,8 +172,8 @@ pub struct IssueItem {
title: String, title: String,
/// Issue author. /// Issue author.
author: AuthorItem, author: AuthorItem,
/// Issue tags. /// Issue labels.
tags: Vec<Tag>, labels: Vec<Label>,
/// Issue assignees. /// Issue assignees.
assignees: Vec<AuthorItem>, assignees: Vec<AuthorItem>,
/// Time when issue was opened. /// Time when issue was opened.
@ -197,8 +197,8 @@ impl IssueItem {
&self.author &self.author
} }
pub fn tags(&self) -> &Vec<Tag> { pub fn labels(&self) -> &Vec<Label> {
&self.tags &self.labels
} }
pub fn assignees(&self) -> &Vec<AuthorItem> { pub fn assignees(&self) -> &Vec<AuthorItem> {
@ -222,9 +222,10 @@ impl From<(&Profile, &Repository, IssueId, Issue)> for IssueItem {
did: issue.author().id, did: issue.author().id,
is_you: *issue.author().id == *profile.did(), is_you: *issue.author().id == *profile.did(),
}, },
tags: issue.tags().cloned().collect(), labels: issue.labels().cloned().collect(),
assignees: issue assignees: issue
.assigned() .assigned()
.cloned()
.map(|did| AuthorItem { .map(|did| AuthorItem {
did, did,
is_you: did == profile.did(), is_you: did == profile.did(),
@ -249,7 +250,7 @@ impl TableItem<7> for IssueItem {
let author = Cell::from(format_author(&self.author.did, self.author.is_you)) let author = Cell::from(format_author(&self.author.did, self.author.is_you))
.style(Style::default().fg(theme.colors.browser_list_author)); .style(Style::default().fg(theme.colors.browser_list_author));
let tags = Cell::from(format_tags(&self.tags)) let tags = Cell::from(format_labels(&self.labels))
.style(Style::default().fg(theme.colors.browser_list_tags)); .style(Style::default().fg(theme.colors.browser_list_tags));
let assignees = self let assignees = self
@ -331,14 +332,14 @@ pub fn format_issue_state(state: &IssueState) -> (String, Color) {
} }
} }
pub fn format_tags(tags: &[Tag]) -> String { pub fn format_labels(labels: &[Label]) -> String {
let mut output = String::new(); let mut output = String::new();
let mut tags = tags.iter().peekable(); let mut labels = labels.iter().peekable();
while let Some(tag) = tags.next() { while let Some(tag) = labels.next() {
output.push_str(&tag.to_string()); output.push_str(&tag.to_string());
if tags.peek().is_some() { if labels.peek().is_some() {
output.push(','); output.push(',');
} }
} }

View File

@ -61,7 +61,7 @@ impl IssueBrowser {
common::label("ID"), common::label("ID"),
common::label("Title"), common::label("Title"),
common::label("Author"), common::label("Author"),
common::label("Tags"), common::label("Labels"),
common::label("Assignees"), common::label("Assignees"),
common::label("Opened"), common::label("Opened"),
]; ];

View File

@ -90,9 +90,9 @@ impl Details {
common::label(item.title()).foreground(theme.colors.browser_list_title), common::label(item.title()).foreground(theme.colors.browser_list_title),
); );
let tags = Property::new( let labels = Property::new(
common::label("Tags").foreground(theme.colors.property_name_fg), common::label("Labels").foreground(theme.colors.property_name_fg),
common::label(&cob::format_tags(item.tags())) common::label(&cob::format_labels(item.labels()))
.foreground(theme.colors.browser_list_tags), .foreground(theme.colors.browser_list_tags),
); );
@ -113,12 +113,11 @@ impl Details {
common::label(&item.state().to_string()).foreground(theme.colors.browser_list_title), common::label(&item.state().to_string()).foreground(theme.colors.browser_list_title),
); );
// let table = common::property_table(theme, vec![title, tags, assignees, state]);
let table = common::property_table( let table = common::property_table(
theme, theme,
vec![ vec![
Widget::new(title), Widget::new(title),
Widget::new(tags), Widget::new(labels),
Widget::new(assignees), Widget::new(assignees),
Widget::new(state), Widget::new(state),
], ],

View File

@ -1,6 +1,7 @@
pub mod common; pub mod common;
pub mod identity; pub mod identity;
pub mod issue; pub mod issue;
pub mod legacy;
pub mod op; pub mod op;
pub mod patch; pub mod patch;
pub mod store; pub mod store;
@ -11,7 +12,7 @@ pub mod test;
pub use cob::{ pub use cob::{
change, history::EntryId, object, object::collaboration::error, CollaborativeObject, Contents, change, history::EntryId, object, object::collaboration::error, CollaborativeObject, Contents,
Create, Entry, History, ObjectId, Store, TypeName, Update, Updated, Create, Entry, History, Manifest, ObjectId, Store, TypeName, Update, Updated, Version,
}; };
pub use cob::{create, get, list, remove, update}; pub use cob::{create, get, list, remove, update};
pub use common::*; pub use common::*;

View File

@ -44,11 +44,30 @@ pub struct Reaction {
} }
impl Reaction { impl Reaction {
/// Create a new reaction from an emoji.
pub fn new(emoji: char) -> Result<Self, ReactionError> { pub fn new(emoji: char) -> Result<Self, ReactionError> {
if emoji.is_whitespace() || emoji.is_ascii() || emoji.is_alphanumeric() { let val = emoji as u32;
return Err(ReactionError::InvalidReaction); let emoticons = 0x1F600..=0x1F64F;
} let misc = 0x1F300..=0x1F5FF; // Miscellaneous Symbols and Pictographs
let dingbats = 0x2700..=0x27BF;
let supp = 0x1F900..=0x1F9FF; // Supplemental Symbols and Pictographs
let transport = 0x1F680..=0x1F6FF;
if emoticons.contains(&val)
|| misc.contains(&val)
|| dingbats.contains(&val)
|| supp.contains(&val)
|| transport.contains(&val)
{
Ok(Self { emoji }) Ok(Self { emoji })
} else {
Err(ReactionError::InvalidReaction)
}
}
/// Get the reaction emoji.
pub fn emoji(&self) -> char {
self.emoji
} }
} }
@ -108,21 +127,21 @@ impl FromStr for Reaction {
} }
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
pub enum TagError { pub enum LabelError {
#[error("invalid tag name: `{0}`")] #[error("invalid tag name: `{0}`")]
InvalidName(String), InvalidName(String),
} }
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)] #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)]
#[serde(transparent)] #[serde(transparent)]
pub struct Tag(String); pub struct Label(String);
impl Tag { impl Label {
pub fn new(name: impl ToString) -> Result<Self, TagError> { pub fn new(name: impl ToString) -> Result<Self, LabelError> {
let name = name.to_string(); let name = name.to_string();
if name.chars().any(|c| c.is_whitespace()) || name.is_empty() { if name.chars().any(|c| c.is_whitespace()) || name.is_empty() {
return Err(TagError::InvalidName(name)); return Err(LabelError::InvalidName(name));
} }
Ok(Self(name)) Ok(Self(name))
} }
@ -132,22 +151,22 @@ impl Tag {
} }
} }
impl FromStr for Tag { impl FromStr for Label {
type Err = TagError; type Err = LabelError;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new(s) Self::new(s)
} }
} }
impl Display for Tag { impl Display for Label {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0) write!(f, "{}", self.0)
} }
} }
impl From<Tag> for String { impl From<Label> for String {
fn from(Tag(name): Tag) -> Self { fn from(Label(name): Label) -> Self {
name name
} }
} }

View File

@ -13,7 +13,7 @@ use crate::{
cob::{ cob::{
self, self,
store::{self, FromHistory as _, HistoryAction, Transaction}, store::{self, FromHistory as _, HistoryAction, Transaction},
Timestamp, Reaction, Timestamp,
}, },
identity::{doc::DocError, Did, Identity, IdentityError}, identity::{doc::DocError, Did, Identity, IdentityError},
prelude::{Doc, ReadRepository}, prelude::{Doc, ReadRepository},
@ -37,33 +37,68 @@ pub type RevisionId = EntryId;
/// Proposal operation. /// Proposal operation.
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")] #[serde(tag = "type")]
pub enum Action { pub enum Action {
#[serde(rename = "accept")]
Accept { Accept {
revision: RevisionId, revision: RevisionId,
signature: Signature, signature: Signature,
}, },
#[serde(rename = "close")]
Close, Close,
#[serde(rename = "edit")]
Edit { Edit {
title: String, title: String,
description: String, description: String,
}, },
Commit, Commit,
Redact { #[serde(rename = "revision.redact")]
RevisionRedact {
revision: RevisionId, revision: RevisionId,
}, },
#[serde(rename = "reject")]
Reject { Reject {
revision: RevisionId, revision: RevisionId,
}, },
#[serde(rename = "revision")]
Revision { Revision {
// N.b. the `Oid` is a blob identifier and not a commit, so we // N.b. the `Oid` is a blob identifier and not a commit, so we
// do not need to propagate it via HistoryAction. // do not need to propagate it via HistoryAction.
current: Oid, current: Oid,
proposed: Doc<Verified>, proposed: Doc<Verified>,
}, },
Thread { #[serde(rename_all = "camelCase")]
#[serde(rename = "revision.comment")]
RevisionComment {
/// The revision to comment on.
revision: RevisionId, revision: RevisionId,
action: thread::Action, /// Comment body.
body: String,
/// Comment this is a reply to.
/// Should be [`None`] if it's the top-level comment.
/// Should be the root [`EntryId`] if it's a top-level comment.
reply_to: Option<EntryId>,
},
/// Edit a revision comment.
#[serde(rename = "revision.comment.edit")]
RevisionCommentEdit {
revision: RevisionId,
comment: EntryId,
body: String,
},
/// Redact a revision comment.
#[serde(rename = "revision.comment.redact")]
RevisionCommentRedact {
revision: RevisionId,
comment: EntryId,
},
/// React to a revision comment.
#[serde(rename = "revision.comment.react")]
RevisionCommentReact {
revision: RevisionId,
comment: EntryId,
reaction: Reaction,
active: bool,
}, },
} }
@ -85,8 +120,6 @@ pub enum ApplyError {
Committed, Committed,
#[error(transparent)] #[error(transparent)]
Commit(#[from] CommitError), Commit(#[from] CommitError),
#[error("the revision {0:?} is redacted")]
Redacted(EntryId),
/// Error applying an op to the proposal thread. /// Error applying an op to the proposal thread.
#[error("thread apply failed: {0}")] #[error("thread apply failed: {0}")]
Thread(#[from] thread::Error), Thread(#[from] thread::Error),
@ -104,9 +137,7 @@ pub enum CommitError {
Closed(EntryId), Closed(EntryId),
#[error("the revision {0} is missing")] #[error("the revision {0} is missing")]
Missing(EntryId), Missing(EntryId),
#[error( #[error("the identity hashes do not match for revision {revision} ({current} =/= {expected})")]
"the identity hashes do match '{current} =/= {expected}' for the revision '{revision}'"
)]
Mismatch { Mismatch {
current: Oid, current: Oid,
expected: Oid, expected: Oid,
@ -299,7 +330,7 @@ impl store::FromHistory for Proposal {
Ok(()) Ok(())
} }
fn apply<R: ReadRepository>(&mut self, op: Op, repo: &R) -> Result<(), Self::Error> { fn apply<R: ReadRepository>(&mut self, op: Op, _repo: &R) -> Result<(), Self::Error> {
let id = op.id; let id = op.id;
let author = Author::new(op.author); let author = Author::new(op.author);
let timestamp = op.timestamp; let timestamp = op.timestamp;
@ -313,50 +344,86 @@ impl store::FromHistory for Proposal {
Action::Accept { Action::Accept {
revision, revision,
signature, signature,
} => match self.revisions.get_mut(&revision) { } => {
Some(Some(revision)) => revision.accept(op.author, signature), if let Some(revision) = lookup::revision(self, &revision)? {
Some(None) => return Err(ApplyError::Redacted(revision)), revision.accept(op.author, signature);
None => return Err(ApplyError::Missing(revision)), }
}, }
Action::Close => self.state = State::Closed, Action::Close => self.state = State::Closed,
Action::Edit { title, description } => { Action::Edit { title, description } => {
self.title = title; self.title = title;
self.description = description; self.description = description;
} }
Action::Commit => self.state = State::Committed, Action::Commit => self.state = State::Committed,
Action::Redact { revision } => { Action::RevisionRedact { revision } => {
if let Some(revision) = self.revisions.get_mut(&revision) { if let Some(revision) = self.revisions.get_mut(&revision) {
*revision = None; *revision = None;
} else { } else {
return Err(ApplyError::Missing(revision)); return Err(ApplyError::Missing(revision));
} }
} }
Action::Reject { revision } => match self.revisions.get_mut(&revision) { Action::Reject { revision } => {
Some(Some(revision)) => revision.reject(op.author), if let Some(revision) = lookup::revision(self, &revision)? {
Some(None) => return Err(ApplyError::Redacted(revision)), revision.reject(op.author);
None => return Err(ApplyError::Missing(revision)),
},
Action::Revision { current, proposed } => {
// Since revisions are keyed by content hash, we shouldn't re-insert a revision
// if it already exists, otherwise this will be resolved via the `merge`
// operation of `Redactable`.
if self.revisions.contains_key(&id) {
continue;
} }
}
Action::Revision { current, proposed } => {
debug_assert!(!self.revisions.contains_key(&id));
self.revisions.insert( self.revisions.insert(
id, id,
Some(Revision::new(author.clone(), current, proposed, timestamp)), Some(Revision::new(author.clone(), current, proposed, timestamp)),
); );
} }
Action::Thread { revision, action } => match self.revisions.get_mut(&revision) { Action::RevisionComment {
Some(Some(revision)) => revision.discussion.apply( revision,
cob::Op::new(op.id, action, op.author, op.timestamp, op.identity), body,
repo, reply_to,
)?, } => {
Some(None) => return Err(ApplyError::Redacted(revision)), if let Some(revision) = lookup::revision(self, &revision)? {
None => return Err(ApplyError::Missing(revision)), thread::comment(
}, &mut revision.discussion,
op.id,
op.author,
op.timestamp,
body,
reply_to,
None,
)?;
}
}
Action::RevisionCommentEdit {
revision,
comment,
body,
} => {
if let Some(revision) = lookup::revision(self, &revision)? {
thread::edit(&mut revision.discussion, op.id, comment, op.timestamp, body)?;
}
}
Action::RevisionCommentRedact { revision, comment } => {
if let Some(revision) = lookup::revision(self, &revision)? {
thread::redact(&mut revision.discussion, op.id, comment)?;
}
}
Action::RevisionCommentReact {
revision,
comment,
reaction,
active,
} => {
if let Some(revision) = lookup::revision(self, &revision)? {
thread::react(
&mut revision.discussion,
op.id,
op.author,
comment,
reaction,
active,
)?;
}
}
} }
} }
@ -364,6 +431,23 @@ impl store::FromHistory for Proposal {
} }
} }
mod lookup {
use super::*;
pub fn revision<'a>(
proposal: &'a mut Proposal,
revision: &RevisionId,
) -> Result<Option<&'a mut Revision>, ApplyError> {
match proposal.revisions.get_mut(revision) {
Some(Some(revision)) => Ok(Some(revision)),
// Redacted.
Some(None) => Ok(None),
// Missing. Causal error.
None => Err(ApplyError::Missing(*revision)),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub enum Verdict { pub enum Verdict {
/// An accepting verdict must supply the [`Signature`] over the /// An accepting verdict must supply the [`Signature`] over the
@ -487,7 +571,7 @@ impl store::Transaction<Proposal> {
} }
pub fn redact(&mut self, revision: RevisionId) -> Result<(), store::Error> { pub fn redact(&mut self, revision: RevisionId) -> Result<(), store::Error> {
self.push(Action::Redact { revision }) self.push(Action::RevisionRedact { revision })
} }
pub fn revision(&mut self, current: Oid, proposed: Doc<Verified>) -> Result<(), store::Error> { pub fn revision(&mut self, current: Oid, proposed: Doc<Verified>) -> Result<(), store::Error> {
@ -500,12 +584,10 @@ impl store::Transaction<Proposal> {
revision: RevisionId, revision: RevisionId,
body: S, body: S,
) -> Result<(), store::Error> { ) -> Result<(), store::Error> {
self.push(Action::Thread { self.push(Action::RevisionComment {
revision, revision,
action: thread::Action::Comment {
body: body.to_string(), body: body.to_string(),
reply_to: None, reply_to: None,
},
}) })
} }
@ -516,12 +598,10 @@ impl store::Transaction<Proposal> {
body: S, body: S,
reply_to: thread::CommentId, reply_to: thread::CommentId,
) -> Result<(), store::Error> { ) -> Result<(), store::Error> {
self.push(Action::Thread { self.push(Action::RevisionComment {
revision, revision,
action: thread::Action::Comment {
body: body.to_string(), body: body.to_string(),
reply_to: Some(reply_to), reply_to: Some(reply_to),
},
}) })
} }
} }

View File

@ -7,12 +7,12 @@ use serde::{Deserialize, Serialize};
use thiserror::Error; use thiserror::Error;
use crate::cob; use crate::cob;
use crate::cob::common::{Author, Reaction, Tag, Timestamp}; use crate::cob::common::{Author, Label, Reaction, Timestamp};
use crate::cob::store::Transaction; use crate::cob::store::Transaction;
use crate::cob::store::{FromHistory as _, HistoryAction}; use crate::cob::store::{FromHistory as _, HistoryAction};
use crate::cob::thread; use crate::cob::thread;
use crate::cob::thread::{CommentId, Thread}; use crate::cob::thread::{CommentId, Thread};
use crate::cob::{store, ActorId, EntryId, ObjectId, TypeName}; use crate::cob::{store, EntryId, ObjectId, TypeName};
use crate::crypto::Signer; use crate::crypto::Signer;
use crate::prelude::{Did, ReadRepository}; use crate::prelude::{Did, ReadRepository};
use crate::storage::WriteRepository; use crate::storage::WriteRepository;
@ -93,15 +93,15 @@ impl State {
#[derive(Debug, Clone, PartialEq, Eq, Default)] #[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Issue { pub struct Issue {
/// Actors assigned to this issue. /// Actors assigned to this issue.
assignees: BTreeSet<ActorId>, pub(super) assignees: BTreeSet<Did>,
/// Title of the issue. /// Title of the issue.
title: String, pub(super) title: String,
/// Current state of the issue. /// Current state of the issue.
state: State, pub(super) state: State,
/// Associated tags. /// Associated labels.
tags: BTreeSet<Tag>, pub(super) labels: BTreeSet<Label>,
/// Discussion around this issue. /// Discussion around this issue.
thread: Thread, pub(super) thread: Thread,
} }
impl store::FromHistory for Issue { impl store::FromHistory for Issue {
@ -122,16 +122,28 @@ impl store::FromHistory for Issue {
Ok(()) Ok(())
} }
fn apply<R: ReadRepository>(&mut self, op: Op, repo: &R) -> Result<(), Error> { fn from_history<R: ReadRepository>(
history: &radicle_cob::History,
repo: &R,
) -> Result<Self, Self::Error> {
let root = history.root();
// Deprecated. Remove when we drop legacy support.
if root.manifest().is_legacy() {
let legacy = super::legacy::issue::Issue::from_history(history, repo)?;
let issue = legacy.into();
Ok(issue)
} else {
store::from_history::<R, Self>(history, repo)
}
}
fn apply<R: ReadRepository>(&mut self, op: Op, _repo: &R) -> Result<(), Error> {
for action in op.actions { for action in op.actions {
match action { match action {
Action::Assign { add, remove } => { Action::Assign { assignees } => {
for assignee in add { self.assignees = BTreeSet::from_iter(assignees);
self.assignees.insert(assignee);
}
for assignee in remove {
self.assignees.remove(&assignee);
}
} }
Action::Edit { title } => { Action::Edit { title } => {
self.title = title; self.title = title;
@ -139,20 +151,33 @@ impl store::FromHistory for Issue {
Action::Lifecycle { state } => { Action::Lifecycle { state } => {
self.state = state; self.state = state;
} }
Action::Tag { add, remove } => { Action::Label { labels } => {
for tag in add { self.labels = BTreeSet::from_iter(labels);
self.tags.insert(tag);
} }
for tag in remove { Action::Comment { body, reply_to } => {
self.tags.remove(&tag); thread::comment(
} &mut self.thread,
} op.id,
Action::Thread { action } => { op.author,
self.thread.apply( op.timestamp,
cob::Op::new(op.id, action, op.author, op.timestamp, op.identity), body,
repo, reply_to,
None,
)?; )?;
} }
Action::CommentEdit { id, body } => {
thread::edit(&mut self.thread, op.id, id, op.timestamp, body)?;
}
Action::CommentRedact { id } => {
thread::redact(&mut self.thread, op.id, id)?;
}
Action::CommentReact {
id,
reaction,
active,
} => {
thread::react(&mut self.thread, op.id, op.author, id, reaction, active)?;
}
} }
} }
Ok(()) Ok(())
@ -160,8 +185,8 @@ impl store::FromHistory for Issue {
} }
impl Issue { impl Issue {
pub fn assigned(&self) -> impl Iterator<Item = Did> + '_ { pub fn assigned(&self) -> impl Iterator<Item = &Did> + '_ {
self.assignees.iter().map(Did::from) self.assignees.iter()
} }
pub fn title(&self) -> &str { pub fn title(&self) -> &str {
@ -172,8 +197,8 @@ impl Issue {
&self.state &self.state
} }
pub fn tags(&self) -> impl Iterator<Item = &Tag> { pub fn labels(&self) -> impl Iterator<Item = &Label> {
self.tags.iter() self.labels.iter()
} }
pub fn timestamp(&self) -> Timestamp { pub fn timestamp(&self) -> Timestamp {
@ -219,24 +244,18 @@ impl Deref for Issue {
} }
impl store::Transaction<Issue> { impl store::Transaction<Issue> {
pub fn assign( /// Assign DIDs to the issue.
&mut self, pub fn assign(&mut self, assignees: impl IntoIterator<Item = Did>) -> Result<(), store::Error> {
add: impl IntoIterator<Item = ActorId>, self.push(Action::Assign {
remove: impl IntoIterator<Item = ActorId>, assignees: assignees.into_iter().collect(),
) -> Result<(), store::Error> { })
let add = add.into_iter().collect::<Vec<_>>();
let remove = remove.into_iter().collect::<Vec<_>>();
self.push(Action::Assign { add, remove })
} }
/// Edit an issue comment. /// Edit an issue comment.
pub fn edit_comment(&mut self, id: CommentId, body: impl ToString) -> Result<(), store::Error> { pub fn edit_comment(&mut self, id: CommentId, body: impl ToString) -> Result<(), store::Error> {
self.push(Action::Thread { self.push(Action::CommentEdit {
action: thread::Action::Edit {
id, id,
body: body.to_string(), body: body.to_string(),
},
}) })
} }
@ -252,46 +271,41 @@ impl store::Transaction<Issue> {
self.push(Action::Lifecycle { state }) self.push(Action::Lifecycle { state })
} }
/// Create the issue thread.
pub fn thread<S: ToString>(&mut self, body: S) -> Result<(), store::Error> {
self.push(Action::from(thread::Action::Comment {
body: body.to_string(),
reply_to: None,
}))
}
/// Comment on an issue. /// Comment on an issue.
pub fn comment<S: ToString>( pub fn comment<S: ToString>(
&mut self, &mut self,
body: S, body: S,
reply_to: CommentId, reply_to: CommentId,
) -> Result<(), store::Error> { ) -> Result<(), store::Error> {
self.push(Action::from(thread::Action::Comment { self.push(Action::Comment {
body: body.to_string(), body: body.to_string(),
reply_to: Some(reply_to), reply_to: Some(reply_to),
})) })
} }
/// Tag an issue. /// Label an issue.
pub fn tag( pub fn label(&mut self, labels: impl IntoIterator<Item = Label>) -> Result<(), store::Error> {
&mut self, self.push(Action::Label {
add: impl IntoIterator<Item = Tag>, labels: labels.into_iter().collect(),
remove: impl IntoIterator<Item = Tag>, })
) -> Result<(), store::Error> {
let add = add.into_iter().collect::<Vec<_>>();
let remove = remove.into_iter().collect::<Vec<_>>();
self.push(Action::Tag { add, remove })
} }
/// React to an issue comment. /// React to an issue comment.
pub fn react(&mut self, to: CommentId, reaction: Reaction) -> Result<(), store::Error> { pub fn react(&mut self, id: CommentId, reaction: Reaction) -> Result<(), store::Error> {
self.push(Action::Thread { self.push(Action::CommentReact {
action: thread::Action::React { id,
to,
reaction, reaction,
active: true, active: true,
}, })
}
////////////////////////////////////////////////////////////////////////////////////////////////
/// Create the issue thread.
fn thread<S: ToString>(&mut self, body: S) -> Result<(), store::Error> {
self.push(Action::Comment {
body: body.to_string(),
reply_to: None,
}) })
} }
} }
@ -302,6 +316,15 @@ pub struct IssueMut<'a, 'g, R> {
store: &'g mut Issues<'a, R>, store: &'g mut Issues<'a, R>,
} }
impl<'a, 'g, R> std::fmt::Debug for IssueMut<'a, 'g, R> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.debug_struct("IssueMut")
.field("id", &self.id)
.field("issue", &self.issue)
.finish()
}
}
impl<'a, 'g, R> IssueMut<'a, 'g, R> impl<'a, 'g, R> IssueMut<'a, 'g, R>
where where
R: WriteRepository + cob::Store, R: WriteRepository + cob::Store,
@ -324,10 +347,10 @@ where
/// Assign one or more actors to an issue. /// Assign one or more actors to an issue.
pub fn assign<G: Signer>( pub fn assign<G: Signer>(
&mut self, &mut self,
assignees: impl IntoIterator<Item = ActorId>, assignees: impl IntoIterator<Item = Did>,
signer: &G, signer: &G,
) -> Result<EntryId, Error> { ) -> Result<EntryId, Error> {
self.transaction("Assign", signer, |tx| tx.assign(assignees, [])) self.transaction("Assign", signer, |tx| tx.assign(assignees))
} }
/// Set the issue title. /// Set the issue title.
@ -355,15 +378,6 @@ where
self.transaction("Lifecycle", signer, |tx| tx.lifecycle(state)) self.transaction("Lifecycle", signer, |tx| tx.lifecycle(state))
} }
/// Create the issue thread.
pub fn thread<G: Signer, S: ToString>(
&mut self,
body: S,
signer: &G,
) -> Result<EntryId, Error> {
self.transaction("Create thread", signer, |tx| tx.thread(body))
}
/// Comment on an issue. /// Comment on an issue.
pub fn comment<G: Signer, S: ToString>( pub fn comment<G: Signer, S: ToString>(
&mut self, &mut self,
@ -378,14 +392,13 @@ where
self.transaction("Comment", signer, |tx| tx.comment(body, reply_to)) self.transaction("Comment", signer, |tx| tx.comment(body, reply_to))
} }
/// Tag an issue. /// Label an issue.
pub fn tag<G: Signer>( pub fn label<G: Signer>(
&mut self, &mut self,
add: impl IntoIterator<Item = Tag>, labels: impl IntoIterator<Item = Label>,
remove: impl IntoIterator<Item = Tag>,
signer: &G, signer: &G,
) -> Result<EntryId, Error> { ) -> Result<EntryId, Error> {
self.transaction("Tag", signer, |tx| tx.tag(add, remove)) self.transaction("Label", signer, |tx| tx.label(labels))
} }
/// React to an issue comment. /// React to an issue comment.
@ -398,16 +411,6 @@ where
self.transaction("React", signer, |tx| tx.react(to, reaction)) self.transaction("React", signer, |tx| tx.react(to, reaction))
} }
/// Unassign one or more actors from an issue.
pub fn unassign<G: Signer>(
&mut self,
assignees: impl IntoIterator<Item = ActorId>,
signer: &G,
) -> Result<EntryId, Error> {
self.transaction("Unassign", signer, |tx| tx.assign([], assignees))
.map_err(Error::from)
}
pub fn transaction<G, F>( pub fn transaction<G, F>(
&mut self, &mut self,
message: &str, message: &str,
@ -491,15 +494,15 @@ where
&'g mut self, &'g mut self,
title: impl ToString, title: impl ToString,
description: impl ToString, description: impl ToString,
tags: &[Tag], labels: &[Label],
assignees: &[ActorId], assignees: &[Did],
signer: &G, signer: &G,
) -> Result<IssueMut<'a, 'g, R>, Error> { ) -> Result<IssueMut<'a, 'g, R>, Error> {
let (id, issue) = Transaction::initial("Create issue", &mut self.raw, signer, |tx| { let (id, issue) = Transaction::initial("Create issue", &mut self.raw, signer, |tx| {
tx.thread(description)?; tx.thread(description)?;
tx.assign(assignees.to_owned(), [])?; tx.assign(assignees.to_owned())?;
tx.edit(title)?; tx.edit(title)?;
tx.tag(tags.to_owned(), [])?; tx.label(labels.to_owned())?;
Ok(()) Ok(())
})?; })?;
@ -533,43 +536,64 @@ where
} }
} }
/// Issue operation. /// Issue action.
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")] #[serde(tag = "type", rename_all = "camelCase")]
pub enum Action { pub enum Action {
Assign { /// Assign issue to an actor.
add: Vec<ActorId>, #[serde(rename = "assign")]
remove: Vec<ActorId>, Assign { assignees: BTreeSet<Did> },
/// Edit issue title.
#[serde(rename = "edit")]
Edit { title: String },
/// Transition to a different state.
#[serde(rename = "lifecycle")]
Lifecycle { state: State },
/// Modify issue labels.
#[serde(rename = "label")]
Label { labels: BTreeSet<Label> },
/// Comment on a thread.
#[serde(rename_all = "camelCase")]
#[serde(rename = "comment")]
Comment {
/// Comment body.
body: String,
/// Comment this is a reply to.
/// Should be [`None`] if it's the top-level comment.
/// Should be the root [`CommentId`] if it's a top-level comment.
#[serde(default, skip_serializing_if = "Option::is_none")]
reply_to: Option<CommentId>,
}, },
Edit {
title: String, /// Edit a comment.
}, #[serde(rename = "comment.edit")]
Lifecycle { CommentEdit { id: CommentId, body: String },
state: State,
}, /// Redact a change. Not all changes can be redacted.
Tag { #[serde(rename = "comment.redact")]
add: Vec<Tag>, CommentRedact { id: CommentId },
remove: Vec<Tag>,
}, /// React to a comment.
Thread { #[serde(rename = "comment.react")]
action: thread::Action, CommentReact {
id: CommentId,
reaction: Reaction,
active: bool,
}, },
} }
impl HistoryAction for Action {} impl HistoryAction for Action {}
impl From<thread::Action> for Action {
fn from(action: thread::Action) -> Self {
Self::Thread { action }
}
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use super::*; use super::*;
use crate::cob::Reaction; use crate::cob::{ActorId, Reaction};
use crate::test; use crate::test;
use crate::test::arbitrary; use crate::test::arbitrary;
@ -660,8 +684,8 @@ mod test {
let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default(); let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
let mut issues = Issues::open(&*repo).unwrap(); let mut issues = Issues::open(&*repo).unwrap();
let assignee: ActorId = arbitrary::gen(1); let assignee = Did::from(arbitrary::gen::<ActorId>(1));
let assignee_two: ActorId = arbitrary::gen(1); let assignee_two = Did::from(arbitrary::gen::<ActorId>(1));
let issue = issues let issue = issues
.create( .create(
"My first issue", "My first issue",
@ -674,21 +698,23 @@ 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 assignees: Vec<_> = issue.assigned().collect::<Vec<_>>(); let assignees: Vec<_> = issue.assigned().cloned().collect::<Vec<_>>();
assert_eq!(1, assignees.len()); assert_eq!(1, assignees.len());
assert!(assignees.contains(&Did::from(assignee))); assert!(assignees.contains(&assignee));
let mut issue = issues.get_mut(&id).unwrap(); let mut issue = issues.get_mut(&id).unwrap();
issue.assign([assignee_two], &node.signer).unwrap(); issue
.assign([assignee, assignee_two], &node.signer)
.unwrap();
let id = issue.id; let id = issue.id;
let issue = issues.get(&id).unwrap().unwrap(); let issue = issues.get(&id).unwrap().unwrap();
let assignees: Vec<_> = issue.assigned().collect::<Vec<_>>(); let assignees: Vec<_> = issue.assigned().cloned().collect::<Vec<_>>();
assert_eq!(2, assignees.len()); assert_eq!(2, assignees.len());
assert!(assignees.contains(&Did::from(assignee))); assert!(assignees.contains(&assignee));
assert!(assignees.contains(&Did::from(assignee_two))); assert!(assignees.contains(&assignee_two));
} }
#[test] #[test]
@ -696,8 +722,8 @@ mod test {
let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default(); let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
let mut issues = Issues::open(&*repo).unwrap(); let mut issues = Issues::open(&*repo).unwrap();
let assignee: ActorId = arbitrary::gen(1); let assignee = Did::from(arbitrary::gen::<ActorId>(1));
let assignee_two: ActorId = arbitrary::gen(1); let assignee_two = Did::from(arbitrary::gen::<ActorId>(1));
let mut issue = issues let mut issue = issues
.create( .create(
"My first issue", "My first issue",
@ -710,14 +736,17 @@ mod test {
issue.assign([assignee_two], &node.signer).unwrap(); issue.assign([assignee_two], &node.signer).unwrap();
issue.assign([assignee_two], &node.signer).unwrap(); issue.assign([assignee_two], &node.signer).unwrap();
issue.reload().unwrap();
let id = issue.id; let assignees: Vec<_> = issue.assigned().cloned().collect::<Vec<_>>();
let issue = issues.get(&id).unwrap().unwrap();
let assignees: Vec<_> = issue.assigned().collect::<Vec<_>>();
assert_eq!(2, assignees.len()); assert_eq!(1, assignees.len());
assert!(assignees.contains(&Did::from(assignee))); assert!(assignees.contains(&assignee_two));
assert!(assignees.contains(&Did::from(assignee_two)));
issue.assign([], &node.signer).unwrap();
issue.reload().unwrap();
assert_eq!(0, issue.assigned().count());
} }
#[test] #[test]
@ -777,8 +806,8 @@ mod test {
let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default(); let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
let mut issues = Issues::open(&*repo).unwrap(); let mut issues = Issues::open(&*repo).unwrap();
let assignee: ActorId = arbitrary::gen(1); let assignee = Did::from(arbitrary::gen::<ActorId>(1));
let assignee_two: ActorId = arbitrary::gen(1); let assignee_two = Did::from(arbitrary::gen::<ActorId>(1));
let mut issue = issues let mut issue = issues
.create( .create(
"My first issue", "My first issue",
@ -788,15 +817,15 @@ mod test {
&node.signer, &node.signer,
) )
.unwrap(); .unwrap();
assert_eq!(2, issue.assigned().count());
issue.unassign([assignee], &node.signer).unwrap(); issue.assign([assignee_two], &node.signer).unwrap();
issue.reload().unwrap();
let id = issue.id; let assignees: Vec<_> = issue.assigned().cloned().collect::<Vec<_>>();
let issue = issues.get(&id).unwrap().unwrap();
let assignees: Vec<_> = issue.assigned().collect::<Vec<_>>();
assert_eq!(1, assignees.len()); assert_eq!(1, assignees.len());
assert!(assignees.contains(&Did::from(assignee_two))); assert!(assignees.contains(&assignee_two));
} }
#[test] #[test]
@ -895,32 +924,39 @@ mod test {
} }
#[test] #[test]
fn test_issue_tag() { fn test_issue_label() {
let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default(); let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
let mut issues = Issues::open(&*repo).unwrap(); let mut issues = Issues::open(&*repo).unwrap();
let bug_tag = Tag::new("bug").unwrap(); let bug_label = Label::new("bug").unwrap();
let ux_tag = Tag::new("ux").unwrap(); let ux_label = Label::new("ux").unwrap();
let wontfix_tag = Tag::new("wontfix").unwrap(); let wontfix_label = Label::new("wontfix").unwrap();
let mut issue = issues let mut issue = issues
.create( .create(
"My first issue", "My first issue",
"Blah blah blah.", "Blah blah blah.",
&[ux_tag.clone()], &[ux_label.clone()],
&[], &[],
&node.signer, &node.signer,
) )
.unwrap(); .unwrap();
issue.tag([bug_tag.clone()], [], &node.signer).unwrap(); issue
issue.tag([wontfix_tag.clone()], [], &node.signer).unwrap(); .label([ux_label.clone(), bug_label.clone()], &node.signer)
.unwrap();
issue
.label(
[ux_label.clone(), bug_label.clone(), wontfix_label.clone()],
&node.signer,
)
.unwrap();
let id = issue.id; let id = issue.id;
let issue = issues.get(&id).unwrap().unwrap(); let issue = issues.get(&id).unwrap().unwrap();
let tags = issue.tags().cloned().collect::<Vec<_>>(); let labels = issue.labels().cloned().collect::<Vec<_>>();
assert!(tags.contains(&ux_tag)); assert!(labels.contains(&ux_label));
assert!(tags.contains(&bug_tag)); assert!(labels.contains(&bug_label));
assert!(tags.contains(&wontfix_tag)); assert!(labels.contains(&wontfix_label));
} }
#[test] #[test]

View File

@ -0,0 +1,2 @@
pub mod issue;
pub mod patch;

View File

@ -0,0 +1,119 @@
use serde::{Deserialize, Serialize};
use crate::cob;
use crate::cob::common::Label;
use crate::cob::issue;
use crate::cob::store::HistoryAction;
use crate::cob::thread;
use crate::cob::{store, ActorId, TypeName};
use crate::prelude::ReadRepository;
/// Issue operation.
pub type Op = cob::Op<Action>;
/// Error type.
pub type Error = issue::Error;
/// Issue state. Accumulates [`Action`].
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Issue(issue::Issue);
impl From<Issue> for issue::Issue {
fn from(issue: Issue) -> issue::Issue {
issue.0
}
}
impl store::FromHistory for Issue {
type Action = Action;
type Error = Error;
fn type_name() -> &'static TypeName {
&*issue::TYPENAME
}
fn validate(&self) -> Result<(), Self::Error> {
if self.0.title.is_empty() {
return Err(Error::Validate("title is empty"));
}
if self.0.thread.validate().is_err() {
return Err(Error::Validate("invalid thread"));
}
Ok(())
}
fn apply<R: ReadRepository>(&mut self, op: Op, repo: &R) -> Result<(), Error> {
let issue = &mut self.0;
for action in op.actions {
match action {
Action::Assign { add, remove } => {
for assignee in add {
issue.assignees.insert(assignee.into());
}
for assignee in remove {
issue.assignees.remove(&assignee.into());
}
}
Action::Edit { title } => {
issue.title = title;
}
Action::Lifecycle { state } => {
issue.state = state;
}
Action::Tag { add, remove } => {
for tag in add {
issue.labels.insert(tag);
}
for tag in remove {
issue.labels.remove(&tag);
}
}
Action::Thread { action } => {
issue.thread.apply(
cob::Op::new(
op.id,
action,
op.author,
op.timestamp,
op.identity,
op.manifest.clone(),
),
repo,
)?;
}
}
}
Ok(())
}
}
/// Issue operation.
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum Action {
Assign {
add: Vec<ActorId>,
remove: Vec<ActorId>,
},
Edit {
title: String,
},
Lifecycle {
state: issue::State,
},
Tag {
add: Vec<Label>,
remove: Vec<Label>,
},
Thread {
action: thread::Action,
},
}
impl HistoryAction for Action {}
impl From<thread::Action> for Action {
fn from(action: thread::Action) -> Self {
Self::Thread { action }
}
}

View File

@ -0,0 +1,383 @@
#![allow(clippy::too_many_arguments)]
use std::collections::{BTreeSet, HashMap};
use serde::{Deserialize, Serialize};
use crate::cob;
use crate::cob::common::{Author, Label};
use crate::cob::patch;
use crate::cob::patch::{
CodeLocation, Error, Merge, MergeTarget, Review, Revision, RevisionId, State, Verdict, TYPENAME,
};
use crate::cob::store::HistoryAction;
use crate::cob::thread;
use crate::cob::{store, EntryId, TypeName};
use crate::git;
use crate::prelude::*;
/// Patch operation.
pub type Op = cob::Op<Action>;
/// Patch operation.
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum Action {
Edit {
title: String,
target: MergeTarget,
},
EditRevision {
revision: RevisionId,
description: String,
},
EditReview {
review: EntryId,
summary: Option<String>,
},
EditCodeComment {
review: EntryId,
comment: EntryId,
body: String,
},
Tag {
add: Vec<Label>,
remove: Vec<Label>,
},
Revision {
description: String,
base: git::Oid,
oid: git::Oid,
},
Lifecycle {
state: State,
},
Redact {
revision: RevisionId,
},
Review {
revision: RevisionId,
summary: Option<String>,
verdict: Option<Verdict>,
},
CodeComment {
review: EntryId,
body: String,
location: CodeLocation,
},
Merge {
revision: RevisionId,
commit: git::Oid,
},
Thread {
revision: RevisionId,
action: thread::Action,
},
}
impl HistoryAction for Action {
fn parents(&self) -> Vec<git::Oid> {
match self {
Self::Revision { base, oid, .. } => {
vec![*base, *oid]
}
Self::Merge { commit, .. } => {
vec![*commit]
}
_ => vec![],
}
}
}
/// Patch state.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Patch(patch::Patch);
impl From<Patch> for patch::Patch {
fn from(patch: Patch) -> patch::Patch {
patch.0
}
}
impl store::FromHistory for Patch {
type Action = Action;
type Error = Error;
fn type_name() -> &'static TypeName {
&*TYPENAME
}
fn validate(&self) -> Result<(), Self::Error> {
if self.0.revisions.is_empty() {
return Err(Error::Validate("no revisions found"));
}
if self.0.title.is_empty() {
return Err(Error::Validate("empty title"));
}
Ok(())
}
fn apply<R: ReadRepository>(&mut self, op: Op, repo: &R) -> Result<(), Error> {
let id = op.id;
let author = Author::new(op.author);
let timestamp = op.timestamp;
let patch = &mut self.0;
debug_assert!(!patch.timeline.contains(&op.id));
patch.timeline.push(op.id);
for action in op.actions {
match action {
Action::Edit { title, target } => {
patch.title = title;
patch.target = target;
}
Action::Lifecycle { state } => {
patch.state = state;
}
Action::Tag { add, remove } => {
for tag in add {
patch.labels.insert(tag);
}
for tag in remove {
patch.labels.remove(&tag);
}
}
Action::EditRevision {
revision,
description,
} => {
if let Some(redactable) = patch.revisions.get_mut(&revision) {
// If the revision was redacted concurrently, there's nothing to do.
if let Some(revision) = redactable {
revision.description = description;
}
} else {
return Err(Error::Missing(revision));
}
}
Action::EditReview { review, summary } => {
let Some(Some((revision, author))) =
patch.reviews.get(&review) else {
return Err(Error::Missing(review));
};
let Some(rev) = patch.revisions.get_mut(revision) else {
return Err(Error::Missing(*revision));
};
// If the revision was redacted concurrently, there's nothing to do.
// Likewise, if the review was redacted concurrently, there's nothing to do.
if let Some(rev) = rev {
let Some(review) = rev.reviews.get_mut(author) else {
return Err(Error::Missing(review));
};
if let Some(review) = review {
review.summary = summary;
}
}
}
Action::Revision {
description,
base,
oid,
} => {
patch.revisions.insert(
id,
Some(Revision::new(
author.clone(),
description,
base,
oid,
timestamp,
BTreeSet::new(),
)),
);
}
Action::Redact { revision } => {
// Redactions must have observed a revision to be valid.
if let Some(revision) = patch.revisions.get_mut(&revision) {
*revision = None;
} else {
return Err(Error::Missing(revision));
}
}
Action::Review {
revision,
ref summary,
verdict,
} => {
let Some(rev) = patch.revisions.get_mut(&revision) else {
return Err(Error::Missing(revision));
};
if let Some(rev) = rev {
// Nb. Applying two reviews by the same author is not allowed and
// results in the review being redacted.
rev.reviews.insert(
op.author,
Some(Review::new(verdict, summary.to_owned(), vec![], timestamp)),
);
// Update reviews index.
patch.reviews.insert(op.id, Some((revision, op.author)));
}
}
Action::EditCodeComment {
review,
comment,
body,
} => {
match patch.reviews.get(&review) {
Some(Some((revision, author))) => {
let Some(rev) = patch.revisions.get_mut(revision) else {
return Err(Error::Missing(*revision));
};
// If the revision was redacted concurrently, there's nothing to do.
// Likewise, if the review was redacted concurrently, there's nothing to do.
if let Some(rev) = rev {
let Some(review) = rev.reviews.get_mut(author) else {
return Err(Error::Missing(review));
};
if let Some(review) = review {
thread::edit(
&mut review.comments,
op.id,
comment,
timestamp,
body,
)?;
}
}
}
Some(None) => {
// Redacted.
}
None => return Err(Error::Missing(review)),
}
}
Action::CodeComment {
review,
body,
location,
} => {
match patch.reviews.get(&review) {
Some(Some((revision, author))) => {
let Some(rev) = patch.revisions.get_mut(revision) else {
return Err(Error::Missing(*revision));
};
// If the revision was redacted concurrently, there's nothing to do.
// Likewise, if the review was redacted concurrently, there's nothing to do.
if let Some(rev) = rev {
let Some(review) = rev.reviews.get_mut(author) else {
return Err(Error::Missing(review));
};
if let Some(review) = review {
thread::comment(
&mut review.comments,
op.id,
*author,
timestamp,
body,
None,
Some(location),
)?;
}
}
}
Some(None) => {
// Redacted.
}
None => return Err(Error::Missing(review)),
}
}
Action::Merge { revision, commit } => {
let Some(rev) = patch.revisions.get_mut(&revision) else {
return Err(Error::Missing(revision));
};
if rev.is_some() {
let doc = repo.identity_doc_at(op.identity)?.verified()?;
match patch.target {
MergeTarget::Delegates => {
if !doc.is_delegate(&op.author) {
return Err(Error::InvalidMerge(op.id));
}
let proj = doc.project()?;
let branch = git::refs::branch(proj.default_branch());
// Nb. We don't return an error in case the merge commit is not an
// ancestor of the default branch. The default branch can change
// *after* the merge action is created, which is out of the control
// of the merge author. We simply skip it, which allows archiving in
// case of a rebase off the master branch, or a redaction of the
// merge.
let Ok(head) = repo.reference_oid(&op.author, &branch) else {
continue;
};
if commit != head && !repo.is_ancestor_of(commit, head)? {
continue;
}
}
}
patch.merges.insert(
op.author,
Merge {
revision,
commit,
timestamp,
},
);
let mut merges = patch.merges.iter().fold(
HashMap::<(RevisionId, git::Oid), usize>::new(),
|mut acc, (_, merge)| {
*acc.entry((merge.revision, merge.commit)).or_default() += 1;
acc
},
);
// Discard revisions that weren't merged by a threshold of delegates.
merges.retain(|_, count| *count >= doc.threshold);
match merges.into_keys().collect::<Vec<_>>().as_slice() {
[] => {
// None of the revisions met the quorum.
}
[(revision, commit)] => {
// Patch is merged.
patch.state = State::Merged {
revision: *revision,
commit: *commit,
};
}
revisions => {
// More than one revision met the quorum.
patch.state = State::Open {
conflicts: revisions.to_vec(),
};
}
}
}
}
Action::Thread { revision, action } => {
match patch.revisions.get_mut(&revision) {
Some(Some(revision)) => {
revision.discussion.apply(
cob::Op::new(
op.id,
action,
op.author,
timestamp,
op.identity,
op.manifest.clone(),
),
repo,
)?;
}
Some(None) => {
// Redacted.
}
None => return Err(Error::Missing(revision)),
}
}
}
}
Ok(())
}
}

View File

@ -1,4 +1,5 @@
use nonempty::NonEmpty; use nonempty::NonEmpty;
use radicle_cob::Manifest;
use thiserror::Error; use thiserror::Error;
use radicle_cob::history::{Entry, EntryId}; use radicle_cob::history::{Entry, EntryId};
@ -35,6 +36,8 @@ pub struct Op<A> {
pub timestamp: Timestamp, pub timestamp: Timestamp,
/// Head of identity document committed to by this operation. /// Head of identity document committed to by this operation.
pub identity: git::Oid, pub identity: git::Oid,
/// Object manifest.
pub manifest: Manifest,
} }
impl<A: Eq> PartialOrd for Op<A> { impl<A: Eq> PartialOrd for Op<A> {
@ -56,6 +59,7 @@ impl<A> Op<A> {
author: ActorId, author: ActorId,
timestamp: impl Into<Timestamp>, timestamp: impl Into<Timestamp>,
identity: git::Oid, identity: git::Oid,
manifest: Manifest,
) -> Self { ) -> Self {
Self { Self {
id, id,
@ -63,6 +67,7 @@ impl<A> Op<A> {
author, author,
timestamp: timestamp.into(), timestamp: timestamp.into(),
identity, identity,
manifest,
} }
} }
@ -85,6 +90,7 @@ where
.iter() .iter()
.map(|blob| serde_json::from_slice(blob.as_slice())) .map(|blob| serde_json::from_slice(blob.as_slice()))
.collect::<Result<_, _>>()?; .collect::<Result<_, _>>()?;
let manifest = entry.manifest().clone();
// SAFETY: Entry is guaranteed to have at least one operation. // SAFETY: Entry is guaranteed to have at least one operation.
#[allow(clippy::unwrap_used)] #[allow(clippy::unwrap_used)]
@ -95,6 +101,7 @@ where
author: *entry.actor(), author: *entry.actor(),
timestamp: Timestamp::from_secs(entry.timestamp()), timestamp: Timestamp::from_secs(entry.timestamp()),
identity, identity,
manifest,
}; };
Ok(op) Ok(op)

File diff suppressed because it is too large Load Diff

View File

@ -10,16 +10,13 @@ use serde::{Deserialize, Serialize};
use crate::cob::common::Timestamp; use crate::cob::common::Timestamp;
use crate::cob::op::Op; use crate::cob::op::Op;
use crate::cob::{ActorId, Create, EntryId, History, ObjectId, TypeName, Update, Updated}; use crate::cob::{ActorId, Create, EntryId, History, ObjectId, TypeName, Update, Updated, Version};
use crate::git; use crate::git;
use crate::prelude::*; use crate::prelude::*;
use crate::storage::git as storage; use crate::storage::git as storage;
use crate::storage::SignRepository; use crate::storage::SignRepository;
use crate::{cob, identity}; use crate::{cob, identity};
/// History type for standard radicle COBs.
pub const HISTORY_TYPE: &str = "radicle";
pub trait HistoryAction: std::fmt::Debug { pub trait HistoryAction: std::fmt::Debug {
/// Parent objects this action depends on. For example, patch revisions /// Parent objects this action depends on. For example, patch revisions
/// have the commit objects as their parent. /// have the commit objects as their parent.
@ -51,28 +48,7 @@ pub trait FromHistory: Sized + Default + PartialEq {
/// Create an object from a history. /// Create an object from a history.
fn from_history<R: ReadRepository>(history: &History, repo: &R) -> Result<Self, Self::Error> { fn from_history<R: ReadRepository>(history: &History, repo: &R) -> Result<Self, Self::Error> {
let obj = history.traverse(Self::default(), |mut acc, _, entry| { self::from_history::<R, Self>(history, repo)
match Op::try_from(entry) {
Ok(op) => {
if let Err(err) = acc.apply(op, repo) {
log::warn!("Error applying op to `{}` state: {err}", Self::type_name());
return ControlFlow::Break(acc);
}
}
Err(err) => {
log::warn!(
"Error decoding ops for `{}` state: {err}",
Self::type_name()
);
return ControlFlow::Break(acc);
}
}
ControlFlow::Continue(acc)
});
obj.validate()?;
Ok(obj)
} }
/// Create an object from individual operations. /// Create an object from individual operations.
@ -89,6 +65,33 @@ pub trait FromHistory: Sized + Default + PartialEq {
} }
} }
/// Turn a history into a concrete type, by traversing the history and applying each operation
/// to the state, skipping branches that return errors.
pub fn from_history<R: ReadRepository, T: FromHistory>(
history: &History,
repo: &R,
) -> Result<T, T::Error> {
let obj = history.traverse(T::default(), |mut acc, _, entry| {
match Op::try_from(entry) {
Ok(op) => {
if let Err(err) = acc.apply(op, repo) {
log::warn!("Error applying op to `{}` state: {err}", T::type_name());
return ControlFlow::Break(acc);
}
}
Err(err) => {
log::warn!("Error decoding ops for `{}` state: {err}", T::type_name());
return ControlFlow::Break(acc);
}
}
ControlFlow::Continue(acc)
});
obj.validate()?;
Ok(obj)
}
/// Store error. /// Store error.
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum Error { pub enum Error {
@ -104,8 +107,6 @@ pub enum Error {
Identity(#[from] identity::IdentityError), Identity(#[from] identity::IdentityError),
#[error(transparent)] #[error(transparent)]
Serialize(#[from] serde_json::Error), Serialize(#[from] serde_json::Error),
#[error("unexpected history type '{0}'")]
HistoryType(String),
#[error("object `{1}` of type `{0}` was not found")] #[error("object `{1}` of type `{0}` was not found")]
NotFound(TypeName, ObjectId), NotFound(TypeName, ObjectId),
#[error("apply: {0}")] #[error("apply: {0}")]
@ -177,8 +178,7 @@ where
signer.public_key(), signer.public_key(),
Update { Update {
object_id, object_id,
history_type: HISTORY_TYPE.to_owned(), type_name: T::type_name().clone(),
typename: T::type_name().clone(),
message: message.to_owned(), message: message.to_owned(),
changes, changes,
}, },
@ -206,8 +206,8 @@ where
parents, parents,
signer.public_key(), signer.public_key(),
Create { Create {
history_type: HISTORY_TYPE.to_owned(), type_name: T::type_name().clone(),
typename: T::type_name().clone(), version: Version::default(),
message: message.to_owned(), message: message.to_owned(),
contents, contents,
}, },
@ -252,9 +252,6 @@ where
let cob = cob::get(self.repo, T::type_name(), id)?; let cob = cob::get(self.repo, T::type_name(), id)?;
if let Some(cob) = cob { if let Some(cob) = cob {
if cob.manifest().history_type != HISTORY_TYPE {
return Err(Error::HistoryType(cob.manifest().history_type.clone()));
}
let obj = T::from_history(cob.history(), self.repo).map_err(Error::apply)?; let obj = T::from_history(cob.history(), self.repo).map_err(Error::apply)?;
Ok(Some(obj)) Ok(Some(obj))
@ -357,12 +354,14 @@ impl<T: FromHistory> Transaction<T> {
let author = self.actor; let author = self.actor;
let timestamp = Timestamp::from_secs(object.history().timestamp()); let timestamp = Timestamp::from_secs(object.history().timestamp());
let identity = store.identity; let identity = store.identity;
let manifest = object.manifest().clone();
let op = cob::Op { let op = cob::Op {
id, id,
actions, actions,
author, author,
timestamp, timestamp,
identity, identity,
manifest,
}; };
Ok((op, id)) Ok((op, id))

View File

@ -8,7 +8,7 @@ use crate::cob::op::Op;
use crate::cob::patch; use crate::cob::patch;
use crate::cob::patch::Patch; use crate::cob::patch::Patch;
use crate::cob::store::encoding; use crate::cob::store::encoding;
use crate::cob::{EntryId, History, Timestamp}; use crate::cob::{Entry, History, Manifest, Timestamp, Version};
use crate::crypto::Signer; use crate::crypto::Signer;
use crate::git; use crate::git;
use crate::git::ext::author::Author; use crate::git::ext::author::Author;
@ -59,6 +59,7 @@ where
pub fn new<G: Signer>(action: &T::Action, time: Timestamp, signer: &G) -> HistoryBuilder<T> { pub fn new<G: Signer>(action: &T::Action, time: Timestamp, signer: &G) -> HistoryBuilder<T> {
let resource = arbitrary::oid(); let resource = arbitrary::oid();
let (data, root) = encoded::<T, _>(action, time, [], signer); let (data, root) = encoded::<T, _>(action, time, [], signer);
let manifest = Manifest::new(T::type_name().clone(), Version::default());
Self { Self {
history: History::new_from_root( history: History::new_from_root(
@ -67,6 +68,7 @@ where
resource, resource,
NonEmpty::new(data), NonEmpty::new(data),
time.as_secs(), time.as_secs(),
manifest,
), ),
time, time,
resource, resource,
@ -74,7 +76,7 @@ where
} }
} }
pub fn root(&self) -> EntryId { pub fn root(&self) -> &Entry {
self.history.root() self.history.root()
} }
@ -86,6 +88,7 @@ where
let timestamp = self.time; let timestamp = self.time;
let tips = self.tips(); let tips = self.tips();
let (data, oid) = encoded::<T, _>(action, timestamp, tips, signer); let (data, oid) = encoded::<T, _>(action, timestamp, tips, signer);
let manifest = Manifest::new(T::type_name().clone(), Version::default());
self.history.extend( self.history.extend(
oid, oid,
@ -93,6 +96,7 @@ where
self.resource, self.resource,
NonEmpty::new(data), NonEmpty::new(data),
timestamp.as_secs(), timestamp.as_secs(),
manifest,
); );
oid oid
} }
@ -137,12 +141,15 @@ impl<G> Actor<G> {
impl<G: Signer> Actor<G> { impl<G: Signer> Actor<G> {
/// Create a new operation. /// Create a new operation.
pub fn op_with<A: Clone + Serialize>( pub fn op_with<T: FromHistory>(
&mut self, &mut self,
action: A, action: T::Action,
identity: Oid, identity: Oid,
timestamp: Timestamp, timestamp: Timestamp,
) -> Op<A> { ) -> Op<T::Action>
where
T::Action: Clone + Serialize,
{
let data = encoding::encode(serde_json::json!({ let data = encoding::encode(serde_json::json!({
"action": action, "action": action,
"nonce": fastrand::u64(..), "nonce": fastrand::u64(..),
@ -152,6 +159,7 @@ impl<G: Signer> Actor<G> {
let id = oid.into(); let id = oid.into();
let author = *self.signer.public_key(); let author = *self.signer.public_key();
let actions = NonEmpty::new(action); let actions = NonEmpty::new(action);
let manifest = Manifest::new(T::type_name().clone(), Version::default());
Op { Op {
id, id,
@ -159,15 +167,19 @@ impl<G: Signer> Actor<G> {
author, author,
timestamp, timestamp,
identity, identity,
manifest,
} }
} }
/// Create a new operation. /// Create a new operation.
pub fn op<A: Clone + Serialize>(&mut self, action: A) -> Op<A> { pub fn op<T: FromHistory>(&mut self, action: T::Action) -> Op<T::Action>
where
T::Action: Clone + Serialize,
{
let identity = arbitrary::oid(); let identity = arbitrary::oid();
let timestamp = Timestamp::now(); let timestamp = Timestamp::now();
self.op_with(action, identity, timestamp) self.op_with::<T>(action, identity, timestamp)
} }
/// Get the actor's DID. /// Get the actor's DID.
@ -188,12 +200,13 @@ impl<G: Signer> Actor<G> {
) -> Result<Patch, patch::Error> { ) -> Result<Patch, patch::Error> {
Patch::from_ops( Patch::from_ops(
[ [
self.op(patch::Action::Revision { self.op::<Patch>(patch::Action::Revision {
description: description.to_string(), description: description.to_string(),
base, base,
oid, oid,
resolves: Default::default(),
}), }),
self.op(patch::Action::Edit { self.op::<Patch>(patch::Action::Edit {
title: title.to_string(), title: title.to_string(),
target: patch::MergeTarget::default(), target: patch::MergeTarget::default(),
}), }),

View File

@ -3,7 +3,7 @@ use std::collections::{BTreeMap, BTreeSet};
use std::str::FromStr; use std::str::FromStr;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize}; use serde::{ser::SerializeStruct, Deserialize, Serialize};
use thiserror::Error; use thiserror::Error;
use crate::cob; use crate::cob;
@ -53,7 +53,7 @@ pub struct Edit {
/// A comment on a discussion thread. /// A comment on a discussion thread.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct Comment { pub struct Comment<T = ()> {
/// Comment author. /// Comment author.
author: ActorId, author: ActorId,
/// The comment body. /// The comment body.
@ -63,14 +63,36 @@ pub struct Comment {
/// Comment this is a reply to. /// Comment this is a reply to.
/// Should always be set, except for the root comment. /// Should always be set, except for the root comment.
reply_to: Option<CommentId>, reply_to: Option<CommentId>,
/// Location of comment, if this is an inline comment.
location: Option<T>,
} }
impl Comment { impl<T: Serialize> Serialize for Comment<T> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::ser::Serializer,
{
let mut state = serializer.serialize_struct("Comment", 5)?;
state.serialize_field("author", &self.author())?;
if let Some(loc) = &self.location {
state.serialize_field("location", loc)?;
}
if let Some(to) = self.reply_to {
state.serialize_field("replyTo", &to)?;
}
state.serialize_field("reactions", &self.reactions)?;
state.serialize_field("body", self.body())?;
state.end()
}
}
impl<L> Comment<L> {
/// Create a new comment. /// Create a new comment.
pub fn new( pub fn new(
author: ActorId, author: ActorId,
body: String, body: String,
reply_to: Option<CommentId>, reply_to: Option<CommentId>,
location: Option<L>,
timestamp: Timestamp, timestamp: Timestamp,
) -> Self { ) -> Self {
let edit = Edit { body, timestamp }; let edit = Edit { body, timestamp };
@ -80,6 +102,7 @@ impl Comment {
reactions: BTreeSet::default(), reactions: BTreeSet::default(),
edits: vec![edit], edits: vec![edit],
reply_to, reply_to,
location,
} }
} }
@ -124,9 +147,14 @@ impl Comment {
pub fn reactions(&self) -> impl Iterator<Item = (&ActorId, &Reaction)> { pub fn reactions(&self) -> impl Iterator<Item = (&ActorId, &Reaction)> {
self.reactions.iter().map(|(a, r)| (a, r)) self.reactions.iter().map(|(a, r)| (a, r))
} }
/// Get comment location, if any.
pub fn location(&self) -> Option<&L> {
self.location.as_ref()
}
} }
impl PartialOrd for Comment { impl<T: PartialOrd> PartialOrd for Comment<T> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> { fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
if self == other { if self == other {
Some(Ordering::Equal) Some(Ordering::Equal)
@ -171,16 +199,25 @@ impl From<Action> for nonempty::NonEmpty<Action> {
} }
/// A discussion thread. /// A discussion thread.
#[derive(Debug, Default, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct Thread { pub struct Thread<T = Comment<()>> {
/// The comments under the thread. /// The comments under the thread.
comments: BTreeMap<CommentId, Option<Comment>>, comments: BTreeMap<CommentId, Option<T>>,
/// Comment timeline. /// Comment timeline.
timeline: Vec<CommentId>, timeline: Vec<CommentId>,
} }
impl Thread { impl<T> Default for Thread<T> {
pub fn new(id: CommentId, comment: Comment) -> Self { fn default() -> Self {
Self {
comments: BTreeMap::default(),
timeline: Vec::default(),
}
}
}
impl<T> Thread<T> {
pub fn new(id: CommentId, comment: T) -> Self {
Self { Self {
comments: BTreeMap::from_iter([(id, Some(comment))]), comments: BTreeMap::from_iter([(id, Some(comment))]),
timeline: Vec::default(), timeline: Vec::default(),
@ -199,26 +236,37 @@ impl Thread {
self.comments.len() self.comments.len()
} }
pub fn comment(&self, id: &CommentId) -> Option<&Comment> { pub fn comment(&self, id: &CommentId) -> Option<&T> {
self.comments.get(id).and_then(|o| o.as_ref()) self.comments.get(id).and_then(|o| o.as_ref())
} }
pub fn root(&self) -> (&CommentId, &Comment) { pub fn root(&self) -> (&CommentId, &T) {
self.first().expect("Thread::root: thread is empty") self.first().expect("Thread::root: thread is empty")
} }
pub fn first(&self) -> Option<(&CommentId, &Comment)> { pub fn first(&self) -> Option<(&CommentId, &T)> {
self.comments().next() self.comments().next()
} }
pub fn last(&self) -> Option<(&CommentId, &Comment)> { pub fn last(&self) -> Option<(&CommentId, &T)> {
self.comments().next_back() self.comments().next_back()
} }
pub fn comments(&self) -> impl DoubleEndedIterator<Item = (&CommentId, &T)> + '_ {
self.timeline.iter().filter_map(|id| {
self.comments
.get(id)
.and_then(|o| o.as_ref())
.map(|comment| (id, comment))
})
}
}
impl<L> Thread<Comment<L>> {
pub fn replies<'a>( pub fn replies<'a>(
&'a self, &'a self,
to: &'a CommentId, to: &'a CommentId,
) -> impl Iterator<Item = (&CommentId, &Comment)> { ) -> impl Iterator<Item = (&CommentId, &Comment<L>)> {
self.comments().filter_map(move |(id, c)| { self.comments().filter_map(move |(id, c)| {
if let Some(reply_to) = c.reply_to { if let Some(reply_to) = c.reply_to {
if &reply_to == to { if &reply_to == to {
@ -228,15 +276,6 @@ impl Thread {
None None
}) })
} }
pub fn comments(&self) -> impl DoubleEndedIterator<Item = (&CommentId, &Comment)> + '_ {
self.timeline.iter().filter_map(|id| {
self.comments
.get(id)
.and_then(|o| o.as_ref())
.map(|comment| (id, comment))
})
}
} }
impl cob::store::FromHistory for Thread { impl cob::store::FromHistory for Thread {
@ -259,67 +298,118 @@ impl cob::store::FromHistory for Thread {
let author = op.author; let author = op.author;
let timestamp = op.timestamp; let timestamp = op.timestamp;
debug_assert!(!self.timeline.contains(&op.id)); for action in op.actions {
self.timeline.push(op.id);
for action in op.into_iter() {
match action { match action {
Action::Comment { body, reply_to } => { Action::Comment { body, reply_to } => {
if body.is_empty() { comment(self, id, author, timestamp, body, reply_to, None)?;
return Err(Error::Comment(id));
}
// Nb. If a comment is already present, it must be redacted, because the
// underlying store guarantees exactly-once delivery of ops.
self.comments
.insert(id, Some(Comment::new(author, body, reply_to, timestamp)));
} }
Action::Edit { id, body } => { Action::Edit { id, body } => {
if body.is_empty() { edit(self, op.id, id, timestamp, body)?;
return Err(Error::Edit(id));
}
// It's possible for a comment to be redacted before we're able to edit it, in
// case of a concurrent update.
//
// However, it's *not* possible for the comment to be absent. Therefore we treat
// that as an error.
if let Some(comment) = self.comments.get_mut(&id) {
if let Some(comment) = comment {
comment.edit(body, timestamp);
}
} else {
return Err(Error::Missing(id));
}
} }
Action::Redact { id } => { Action::Redact { id } => {
if let Some(comment) = self.comments.get_mut(&id) { redact(self, op.id, id)?;
*comment = None;
} else {
return Err(Error::Missing(id));
}
} }
Action::React { Action::React {
to, to,
reaction, reaction,
active, active,
} => { } => {
let key = (author, reaction); react(self, op.id, author, to, reaction, active)?;
if let Some(comment) = self.comments.get_mut(&to) { }
}
}
Ok(())
}
}
pub fn comment<L>(
thread: &mut Thread<Comment<L>>,
id: EntryId,
author: ActorId,
timestamp: Timestamp,
body: String,
reply_to: Option<CommentId>,
location: Option<L>,
) -> Result<(), Error> {
if body.is_empty() {
return Err(Error::Comment(id));
}
debug_assert!(!thread.timeline.contains(&id));
thread.timeline.push(id);
// Nb. If a comment is already present, it must be redacted, because the
// underlying store guarantees exactly-once delivery of ops.
thread.comments.insert(
id,
Some(Comment::new(author, body, reply_to, location, timestamp)),
);
Ok(())
}
pub fn edit<L>(
thread: &mut Thread<Comment<L>>,
id: EntryId,
comment: EntryId,
timestamp: Timestamp,
body: String,
) -> Result<(), Error> {
if body.is_empty() {
return Err(Error::Edit(id));
}
debug_assert!(!thread.timeline.contains(&id));
thread.timeline.push(id);
// It's possible for a comment to be redacted before we're able to edit it, in
// case of a concurrent update.
//
// However, it's *not* possible for the comment to be absent. Therefore we treat
// that as an error.
if let Some(comment) = thread.comments.get_mut(&comment) {
if let Some(comment) = comment { if let Some(comment) = comment {
comment.edit(body, timestamp);
}
} else {
return Err(Error::Missing(comment));
}
Ok(())
}
pub fn redact<T>(thread: &mut Thread<T>, id: EntryId, comment: EntryId) -> Result<(), Error> {
if let Some(comment) = thread.comments.get_mut(&comment) {
debug_assert!(!thread.timeline.contains(&id));
thread.timeline.push(id);
*comment = None;
} else {
return Err(Error::Missing(id));
}
Ok(())
}
pub fn react<T>(
thread: &mut Thread<Comment<T>>,
id: EntryId,
author: ActorId,
comment: EntryId,
reaction: Reaction,
active: bool,
) -> Result<(), Error> {
let key = (author, reaction);
let Some(comment) = thread.comments.get_mut(&comment) else {
return Err(Error::Missing(comment));
};
if let Some(comment) = comment {
debug_assert!(!thread.timeline.contains(&id));
thread.timeline.push(id);
if active { if active {
comment.reactions.insert(key); comment.reactions.insert(key);
} else { } else {
comment.reactions.remove(&key); comment.reactions.remove(&key);
} }
} }
} else {
return Err(Error::Missing(id));
}
}
}
}
Ok(()) Ok(())
}
} }
#[cfg(test)] #[cfg(test)]
@ -361,7 +451,7 @@ mod tests {
/// Create a new comment. /// Create a new comment.
pub fn comment(&mut self, body: &str, reply_to: Option<CommentId>) -> Op<Action> { pub fn comment(&mut self, body: &str, reply_to: Option<CommentId>) -> Op<Action> {
self.op(Action::Comment { self.op::<Thread>(Action::Comment {
body: String::from(body), body: String::from(body),
reply_to, reply_to,
}) })
@ -369,12 +459,12 @@ mod tests {
/// Create a new redaction. /// Create a new redaction.
pub fn redact(&mut self, id: CommentId) -> Op<Action> { pub fn redact(&mut self, id: CommentId) -> Op<Action> {
self.op(Action::Redact { id }) self.op::<Thread>(Action::Redact { id })
} }
/// Edit a comment. /// Edit a comment.
pub fn edit(&mut self, id: CommentId, body: &str) -> Op<Action> { pub fn edit(&mut self, id: CommentId, body: &str) -> Op<Action> {
self.op(Action::Edit { self.op::<Thread>(Action::Edit {
id, id,
body: body.to_owned(), body: body.to_owned(),
}) })
@ -456,13 +546,13 @@ mod tests {
time, time,
&alice, &alice,
); );
a.comment("Alice comment", Some(a.root()), &alice); a.comment("Alice comment", Some(*a.root().id()), &alice);
let mut b = a.clone(); let mut b = a.clone();
let b1 = b.comment("Bob comment", Some(a.root()), &bob); let b1 = b.comment("Bob comment", Some(*a.root().id()), &bob);
let mut e = a.clone(); let mut e = a.clone();
let e1 = e.comment("Eve comment", Some(a.root()), &eve); let e1 = e.comment("Eve comment", Some(*a.root().id()), &eve);
assert_eq!(a.as_ref().len(), 2); assert_eq!(a.as_ref().len(), 2);
assert_eq!(b.as_ref().len(), 3); assert_eq!(b.as_ref().len(), 3);
@ -561,14 +651,14 @@ mod tests {
let e1 = h1.commit( let e1 = h1.commit(
&Action::Edit { &Action::Edit {
id: h0.root(), id: *h0.root().id(),
body: String::from("Bye World."), body: String::from("Bye World."),
}, },
&alice, &alice,
); );
let e2 = h2.commit( let e2 = h2.commit(
&Action::Edit { &Action::Edit {
id: h0.root(), id: *h0.root().id(),
body: String::from("Hi World."), body: String::from("Hi World."),
}, },
&bob, &bob,
@ -588,7 +678,7 @@ mod tests {
let _e3 = h1.commit( let _e3 = h1.commit(
&Action::Edit { &Action::Edit {
id: h0.root(), id: *h0.root().id(),
body: String::from("Hoho World!"), body: String::from("Hoho World!"),
}, },
&alice, &alice,

View File

@ -14,7 +14,7 @@ pub enum DidError {
PublicKey(#[from] crypto::PublicKeyError), PublicKey(#[from] crypto::PublicKeyError),
} }
#[derive(Serialize, Deserialize, PartialEq, Eq, Hash, Clone, Copy)] #[derive(Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
#[serde(into = "String", try_from = "String")] #[serde(into = "String", try_from = "String")]
pub struct Did(crypto::PublicKey); pub struct Did(crypto::PublicKey);

View File

@ -6,6 +6,7 @@ use radicle_cob as cob;
use radicle_cob::change; use radicle_cob::change;
use storage::SignRepository; use storage::SignRepository;
use crate::git::*;
use crate::storage; use crate::storage;
use crate::storage::Error; use crate::storage::Error;
use crate::storage::{ use crate::storage::{
@ -17,9 +18,6 @@ use crate::{
identity::{doc::DocError, IdentityError}, identity::{doc::DocError, IdentityError},
}; };
pub use crate::git::*;
pub use cob::*;
use super::{RemoteId, Repository}; use super::{RemoteId, Repository};
#[derive(Error, Debug)] #[derive(Error, Debug)]

View File

@ -56,7 +56,7 @@ labels="$(echo "$response" | jq -r '.labels | .[].name')"
tags=() tags=()
for label in $labels; do for label in $labels; do
tags+=("--tag" "$label") tags+=("--label" "$label")
done done
rad issue open --title "$title" "${tags[@]}" --description "$body" --no-announce rad issue open --title "$title" "${tags[@]}" --description "$body" --no-announce