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:
parent
885f4999da
commit
3bd5589b1a
|
|
@ -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
|
||||
✓ Identity proposal '04603c0d3ea4d137487024a51c9360adfc511114' created
|
||||
✓ Identity proposal '6b73fce909f612d5d92084b91309d73c21fea396' created
|
||||
title: Add Alice
|
||||
description: Add Alice as a delegate
|
||||
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
|
||||
✓ Identity proposal '3f6ae4f8645c8b0cbcd35ea924df7b13aca52774' created
|
||||
✓ Identity proposal '2bf3a85e209d10b11a65e7ed8a8085f6f18ac6bf' created
|
||||
title: Add Bob
|
||||
description: Add Bob as a delegate
|
||||
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.
|
||||
|
||||
```
|
||||
$ rad id accept 04603c0d3ea4d137487024a51c9360adfc511114 --no-confirm
|
||||
$ rad id accept 6b73fce909f612d5d92084b91309d73c21fea396 --no-confirm
|
||||
✓ Accepted proposal ✓
|
||||
title: Add Alice
|
||||
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'
|
||||
title: Add Alice
|
||||
description: Add Alice as a delegate
|
||||
|
|
@ -183,7 +183,7 @@ Quorum Reached
|
|||
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: d96f425412c9f8ad5d9a9a05c9831d0728e2338d =/= 475cdfbc8662853dd132ec564e4f5eb0f152dd7f
|
||||
* 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:
|
||||
|
||||
``` (fail)
|
||||
$ rad id commit 3f6ae4f8645c8b0cbcd35ea924df7b13aca52774 --no-confirm
|
||||
$ rad id commit 2bf3a85e209d10b11a65e7ed8a8085f6f18ac6bf --no-confirm
|
||||
! Warning: Revision is out of date
|
||||
! Warning: d96f425412c9f8ad5d9a9a05c9831d0728e2338d =/= 475cdfbc8662853dd132ec564e4f5eb0f152dd7f
|
||||
* 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:
|
||||
|
||||
```
|
||||
$ rad id rebase 3f6ae4f8645c8b0cbcd35ea924df7b13aca52774 --no-confirm
|
||||
✓ Identity proposal '3f6ae4f8645c8b0cbcd35ea924df7b13aca52774' rebased
|
||||
✓ Revision '42b9428df59ad349f706b1397750b75ea3b42574'
|
||||
$ rad id rebase 2bf3a85e209d10b11a65e7ed8a8085f6f18ac6bf --no-confirm
|
||||
✓ Identity proposal '2bf3a85e209d10b11a65e7ed8a8085f6f18ac6bf' rebased
|
||||
✓ Revision 'e56b3e0842a4dd37c2a997344bcb4113704e4768'
|
||||
title: Add Bob
|
||||
description: Add Bob as a delegate
|
||||
status: ❲open❳
|
||||
|
|
@ -293,9 +293,9 @@ Quorum Reached
|
|||
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
|
||||
✓ Identity proposal '3f6ae4f8645c8b0cbcd35ea924df7b13aca52774' updated
|
||||
✓ Revision '1b4ded759249e4f76d19c3e580b4736bf2a2d1c4'
|
||||
$ rad id update 2bf3a85e209d10b11a65e7ed8a8085f6f18ac6bf --rev e56b3e0842a4dd37c2a997344bcb4113704e4768 --delegates did:key:z6MkedTZGJGqgQ2py2b8kGecfxdt2yRdHWF6JpaZC47fovFn --no-confirm
|
||||
✓ Identity proposal '2bf3a85e209d10b11a65e7ed8a8085f6f18ac6bf' updated
|
||||
✓ Revision 'f15fc641d777cfb005caaa9405e262e516b8ac60'
|
||||
title: Add Bob
|
||||
description: Add Bob as a delegate
|
||||
status: ❲open❳
|
||||
|
|
@ -338,10 +338,10 @@ Quorum Reached
|
|||
Finally, we can accept and commit this proposal, creating the final
|
||||
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 ✓
|
||||
title: Add Bob
|
||||
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'
|
||||
title: Add Bob
|
||||
description: Add Bob as a delegate
|
||||
|
|
|
|||
|
|
@ -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
|
||||
✓ Identity proposal '0d396a83a5e1dda2b8929f7dc401d19dd1a79fb8' created
|
||||
✓ Identity proposal '662a8065f18db50d9ee952bb36eda5b605f161e9' created
|
||||
title: Add Bob
|
||||
description: Add Bob as a delegate
|
||||
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:
|
||||
|
||||
```
|
||||
$ rad id reject 0d396a --no-confirm
|
||||
$ rad id reject 662a8065f18db50d9ee952bb36eda5b605f161e9 --no-confirm
|
||||
✓ Rejected proposal 👎
|
||||
title: Add Bob
|
||||
description: Add Bob as a delegate
|
||||
|
|
@ -145,7 +145,7 @@ increased to `1`.
|
|||
Instead, let's accept the proposal:
|
||||
|
||||
```
|
||||
$ rad id accept 0d396a --no-confirm
|
||||
$ rad id accept 662a8065f18db50d9ee952bb36eda5b605f161e9 --no-confirm
|
||||
✓ Accepted proposal ✓
|
||||
title: Add Bob
|
||||
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:
|
||||
|
||||
```
|
||||
$ rad id commit 0d396a --no-confirm
|
||||
$ rad id commit 662a8065f18db50d9ee952bb36eda5b605f161e9 --no-confirm
|
||||
✓ Committed new identity 'c96e764965aaeff1c6ea3e5b97e2b9828773c8b0'
|
||||
title: Add Bob
|
||||
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
|
||||
✓ Identity proposal 'f435d6e89c8f922ede691287c0d8b7f82afa591e' created
|
||||
✓ Identity proposal 'e6c04862ed0e59739f34232c8690cbad73840a93' created
|
||||
title: Update threshold
|
||||
description: Update to safer threshold
|
||||
status: ❲open❳
|
||||
|
|
@ -298,8 +298,8 @@ Quorum Reached
|
|||
But we change our minds and decide to close the proposal instead:
|
||||
|
||||
```
|
||||
$ rad id close f435d6 --no-confirm
|
||||
✓ Closed identity proposal 'f435d6e89c8f922ede691287c0d8b7f82afa591e'
|
||||
$ rad id close e6c04862ed0e59739f34232c8690cbad73840a93 --no-confirm
|
||||
✓ Closed identity proposal 'e6c04862ed0e59739f34232c8690cbad73840a93'
|
||||
title: Update threshold
|
||||
description: Update to safer threshold
|
||||
status: ❲closed❳
|
||||
|
|
@ -348,15 +348,15 @@ Radicle identity, then we can use the list command:
|
|||
|
||||
```
|
||||
$ rad id list
|
||||
0d396a83a5e1dda2b8929f7dc401d19dd1a79fb8 "Add Bob" ❲committed❳
|
||||
f435d6e89c8f922ede691287c0d8b7f82afa591e "Update threshold" ❲closed❳
|
||||
662a8065f18db50d9ee952bb36eda5b605f161e9 "Add Bob" ❲committed❳
|
||||
e6c04862ed0e59739f34232c8690cbad73840a93 "Update threshold" ❲closed❳
|
||||
```
|
||||
|
||||
And if we want to view the latest state of any proposal we can use the
|
||||
show command:
|
||||
|
||||
```
|
||||
$ rad id show f435d6
|
||||
$ rad id show e6c04862ed0e59739f34232c8690cbad73840a93
|
||||
title: Update threshold
|
||||
description: Update to safer threshold
|
||||
status: ❲closed❳
|
||||
|
|
|
|||
|
|
@ -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
|
||||
╭─────────────────────────────────────────────────────────╮
|
||||
│ Title flux capacitor underpowered │
|
||||
│ Issue 2e8c1bf3fe0532a314778357c886608a966a34bd │
|
||||
│ Issue 42028af21fabc09bfac2f25490f119f7c7e11542 │
|
||||
│ Status open │
|
||||
│ │
|
||||
│ Flux capacitor power requirements exceed current supply │
|
||||
|
|
@ -18,20 +18,20 @@ The issue is now listed under our project.
|
|||
|
||||
```
|
||||
$ rad issue list
|
||||
╭───────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ● ID Title Author Tags Assignees Opened │
|
||||
├───────────────────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ● 2e8c1bf flux capacitor underpowered z6MknSL…StBU8Vi (you) [ .. ] │
|
||||
╰───────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
╭─────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ● ID Title Author Labels Assignees Opened │
|
||||
├─────────────────────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ● 42028af flux capacitor underpowered z6MknSL…StBU8Vi (you) [ .. ] │
|
||||
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
```
|
||||
|
||||
Show the issue information issue.
|
||||
|
||||
```
|
||||
$ rad issue show 2e8c1bf
|
||||
$ rad issue show 42028af
|
||||
╭─────────────────────────────────────────────────────────╮
|
||||
│ Title flux capacitor underpowered │
|
||||
│ Issue 2e8c1bf3fe0532a314778357c886608a966a34bd │
|
||||
│ Issue 42028af21fabc09bfac2f25490f119f7c7e11542 │
|
||||
│ Status open │
|
||||
│ │
|
||||
│ 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.
|
||||
|
||||
```
|
||||
$ 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.
|
||||
|
||||
```
|
||||
$ rad issue list --assigned
|
||||
╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ● ID Title Author Tags Assignees Opened │
|
||||
├─────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ● 2e8c1bf flux capacitor underpowered z6MknSL…StBU8Vi (you) z6MknSL…StBU8Vi [ .. ] │
|
||||
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ● ID Title Author Labels Assignees Opened │
|
||||
├───────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ● 42028af flux capacitor underpowered z6MknSL…StBU8Vi (you) z6MknSL…StBU8Vi [ .. ] │
|
||||
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
```
|
||||
|
||||
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.
|
||||
|
|
@ -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.
|
||||
|
||||
```
|
||||
$ rad comment 2e8c1bf3fe0532a314778357c886608a966a34bd --message 'The flux capacitor needs 1.21 Gigawatts'
|
||||
9822748bd076595a2408aad02b3a0d9f94fec7e0
|
||||
$ rad comment 2e8c1bf3fe0532a314778357c886608a966a34bd --reply-to 9822748bd076595a2408aad02b3a0d9f94fec7e0 --message 'More power!'
|
||||
edec8d07bf3788b98943394c1274910b8f12d35c
|
||||
$ rad comment 42028af21fabc09bfac2f25490f119f7c7e11542 --message 'The flux capacitor needs 1.21 Gigawatts'
|
||||
84492237dc0908b1e5b728d1a4e5f1343b6ffe9b
|
||||
$ rad comment 42028af21fabc09bfac2f25490f119f7c7e11542 --reply-to 84492237dc0908b1e5b728d1a4e5f1343b6ffe9b --message 'More power!'
|
||||
dd679552a15e2db73bbedf3084f5f7c62bb0d724
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
```
|
||||
$ 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 │
|
||||
│ Issue 2e8c1bf3fe0532a314778357c886608a966a34bd │
|
||||
│ Tags bug, good-first-issue │
|
||||
│ Issue 42028af21fabc09bfac2f25490f119f7c7e11542 │
|
||||
│ Labels bug, good-first-issue │
|
||||
│ Status open │
|
||||
│ │
|
||||
│ Flux capacitor power requirements exceed current supply │
|
||||
|
|
@ -22,17 +22,17 @@ $ rad issue show 2e8c1bf3fe0532a314778357c886608a966a34bd
|
|||
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 │
|
||||
│ Issue 2e8c1bf3fe0532a314778357c886608a966a34bd │
|
||||
│ Tags bug │
|
||||
│ Issue 42028af21fabc09bfac2f25490f119f7c7e11542 │
|
||||
│ Labels bug │
|
||||
│ Status open │
|
||||
│ │
|
||||
│ Flux capacitor power requirements exceed current supply │
|
||||
|
|
@ -4,7 +4,7 @@ Let's start by creating a patch.
|
|||
$ git checkout -b feature/1 -q
|
||||
$ git commit --allow-empty -q -m "First change"
|
||||
$ git push rad HEAD:refs/patches
|
||||
✓ Patch 0ec956c94256fa101db4c32956ce195a1aa0edf2 opened
|
||||
✓ Patch 143bb0c962561b09e86478a53ba346b5ff934335 opened
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
* [new reference] HEAD -> refs/patches
|
||||
```
|
||||
|
|
@ -26,8 +26,8 @@ update it, we expect it to be updated and merged:
|
|||
``` (stderr) RAD_SOCKET=/dev/null
|
||||
$ git checkout feature/1 -q
|
||||
$ git push -f
|
||||
✓ Patch 0ec956c updated to 8175b00f4d75059976930cfcb75ef08454c87055
|
||||
✓ Patch 0ec956c94256fa101db4c32956ce195a1aa0edf2 merged
|
||||
✓ Patch 143bb0c updated to e595bf1246bdcee7b0c20615e479f62d2bf02249
|
||||
✓ Patch 143bb0c962561b09e86478a53ba346b5ff934335 merged
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
+ 20aa5dd...954bcdb feature/1 -> patches/0ec956c94256fa101db4c32956ce195a1aa0edf2 (forced update)
|
||||
+ 20aa5dd...954bcdb feature/1 -> patches/143bb0c962561b09e86478a53ba346b5ff934335 (forced update)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ Let's start by creating two patches.
|
|||
$ git checkout -b feature/1 -q
|
||||
$ git commit --allow-empty -q -m "First change"
|
||||
$ git push rad HEAD:refs/patches
|
||||
✓ Patch 0ec956c94256fa101db4c32956ce195a1aa0edf2 opened
|
||||
✓ Patch 143bb0c962561b09e86478a53ba346b5ff934335 opened
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
* [new reference] HEAD -> refs/patches
|
||||
```
|
||||
|
|
@ -12,7 +12,7 @@ To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkE
|
|||
$ git checkout -b feature/2 -q master
|
||||
$ git commit --allow-empty -q -m "Second change"
|
||||
$ git push rad HEAD:refs/patches
|
||||
✓ Patch 928d76e22ef98a8406f2e4e4bcc8878533bbdfe0 opened
|
||||
✓ Patch 5d0e608aa35af59f769e9d6a2c0227ea60ae2740 opened
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
* [new reference] HEAD -> refs/patches
|
||||
```
|
||||
|
|
@ -22,8 +22,8 @@ This creates some remote tracking branches for us:
|
|||
```
|
||||
$ git branch -r
|
||||
rad/master
|
||||
rad/patches/0ec956c94256fa101db4c32956ce195a1aa0edf2
|
||||
rad/patches/928d76e22ef98a8406f2e4e4bcc8878533bbdfe0
|
||||
rad/patches/143bb0c962561b09e86478a53ba346b5ff934335
|
||||
rad/patches/5d0e608aa35af59f769e9d6a2c0227ea60ae2740
|
||||
```
|
||||
|
||||
And some remote refs:
|
||||
|
|
@ -34,13 +34,13 @@ z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
|||
└── refs
|
||||
├── cobs
|
||||
│ └── xyz.radicle.patch
|
||||
│ ├── 0ec956c94256fa101db4c32956ce195a1aa0edf2
|
||||
│ └── 928d76e22ef98a8406f2e4e4bcc8878533bbdfe0
|
||||
│ ├── 143bb0c962561b09e86478a53ba346b5ff934335
|
||||
│ └── 5d0e608aa35af59f769e9d6a2c0227ea60ae2740
|
||||
├── heads
|
||||
│ ├── master
|
||||
│ └── patches
|
||||
│ ├── 0ec956c94256fa101db4c32956ce195a1aa0edf2
|
||||
│ └── 928d76e22ef98a8406f2e4e4bcc8878533bbdfe0
|
||||
│ ├── 143bb0c962561b09e86478a53ba346b5ff934335
|
||||
│ └── 5d0e608aa35af59f769e9d6a2c0227ea60ae2740
|
||||
└── rad
|
||||
├── id
|
||||
└── sigrefs
|
||||
|
|
@ -59,8 +59,8 @@ When we push to `rad/master`, we automatically merge the patches:
|
|||
|
||||
``` (stderr) RAD_SOCKET=/dev/null
|
||||
$ git push rad master
|
||||
✓ Patch 0ec956c94256fa101db4c32956ce195a1aa0edf2 merged
|
||||
✓ Patch 928d76e22ef98a8406f2e4e4bcc8878533bbdfe0 merged
|
||||
✓ Patch 143bb0c962561b09e86478a53ba346b5ff934335 merged
|
||||
✓ Patch 5d0e608aa35af59f769e9d6a2c0227ea60ae2740 merged
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
f2de534..d6399c7 master -> master
|
||||
```
|
||||
|
|
@ -69,8 +69,8 @@ $ rad patch --merged
|
|||
╭────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ● ID Title Author Head + - Updated │
|
||||
├────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ✔ 0ec956c First change z6MknSL…StBU8Vi alice (you) 20aa5dd +0 -0 [ ... ] │
|
||||
│ ✔ 928d76e Second change z6MknSL…StBU8Vi alice (you) daf349f +0 -0 [ ... ] │
|
||||
│ ✔ 143bb0c First change z6MknSL…StBU8Vi alice (you) 20aa5dd +0 -0 [ ... ] │
|
||||
│ ✔ 5d0e608 Second change z6MknSL…StBU8Vi alice (you) daf349f +0 -0 [ ... ] │
|
||||
╰────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
```
|
||||
|
||||
|
|
@ -89,8 +89,8 @@ z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
|||
└── refs
|
||||
├── cobs
|
||||
│ └── xyz.radicle.patch
|
||||
│ ├── 0ec956c94256fa101db4c32956ce195a1aa0edf2
|
||||
│ └── 928d76e22ef98a8406f2e4e4bcc8878533bbdfe0
|
||||
│ ├── 143bb0c962561b09e86478a53ba346b5ff934335
|
||||
│ └── 5d0e608aa35af59f769e9d6a2c0227ea60ae2740
|
||||
├── heads
|
||||
│ └── master
|
||||
└── rad
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ $ git log --graph --decorate --abbrev-commit --pretty=oneline --all
|
|||
Then we create a patch from `feature/1`:
|
||||
``` (stderr)
|
||||
$ git push rad feature/1:refs/patches
|
||||
✓ Patch 866f59c001cd4d78a151f444b34265566c83c264 opened
|
||||
✓ Patch 69ebafb6f654fb29d23f630cc165d83d6cbf525c opened
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
* [new reference] feature/1 -> refs/patches
|
||||
```
|
||||
|
|
@ -48,17 +48,17 @@ $ rad patch list
|
|||
╭─────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ● 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
|
||||
diverged by one commit:
|
||||
```
|
||||
$ rad patch show -v -p 866f59c
|
||||
$ rad patch show -v -p 69ebafb
|
||||
╭────────────────────────────────────────────────────────────────────╮
|
||||
│ Title Add Alan │
|
||||
│ Patch 866f59c001cd4d78a151f444b34265566c83c264 │
|
||||
│ Patch 69ebafb6f654fb29d23f630cc165d83d6cbf525c │
|
||||
│ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │
|
||||
│ Head 5c88a79d75f5c2b4cc51ee6f163d2db91ee198d7 │
|
||||
│ Base f64fb2c8fe28f7c458c72ec8d700373924794943 │
|
||||
|
|
@ -93,7 +93,7 @@ $ git checkout -q -b feature/2 feature/1
|
|||
$ sed -i '$a Mel Farna' CONTRIBUTORS
|
||||
$ git commit -a -q -m "Add Mel"
|
||||
$ git push -o patch.message="Add Mel" rad HEAD:refs/patches
|
||||
✓ Patch 57cb9b2758518e547de324456ac967fda456c6c1 opened
|
||||
✓ Patch 53d5f17aba5fd9b7de7a02ecb6f01de561701eeb opened
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
* [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
|
||||
patch uses the same base as the previous patch:
|
||||
```
|
||||
$ rad patch show -v 57cb9b2758518e547de324456ac967fda456c6c1
|
||||
$ rad patch show -v 53d5f17aba5fd9b7de7a02ecb6f01de561701eeb
|
||||
╭────────────────────────────────────────────────────────────────────╮
|
||||
│ Title Add Mel │
|
||||
│ Patch 57cb9b2758518e547de324456ac967fda456c6c1 │
|
||||
│ Patch 53d5f17aba5fd9b7de7a02ecb6f01de561701eeb │
|
||||
│ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │
|
||||
│ Head 7f63fcbcf23fc39eea784c091ad3d20d7e4bd005 │
|
||||
│ Base f64fb2c8fe28f7c458c72ec8d700373924794943 │
|
||||
|
|
@ -124,7 +124,7 @@ If we want to instead create a "stacked" patch, we can do so with the
|
|||
|
||||
``` (stderr)
|
||||
$ 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
|
||||
* [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`.
|
||||
|
||||
```
|
||||
$ rad patch show -v 395221c5b75fa9bc2de7909d03e69dfd606611c6
|
||||
$ rad patch show -v 459dc67a024ff30c3bca02f0f1e5b746459ce32a
|
||||
╭────────────────────────────────────────────────────────────────────╮
|
||||
│ Title Add Mel #2 │
|
||||
│ Patch 395221c5b75fa9bc2de7909d03e69dfd606611c6 │
|
||||
│ Patch 459dc67a024ff30c3bca02f0f1e5b746459ce32a │
|
||||
│ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │
|
||||
│ Head 7f63fcbcf23fc39eea784c091ad3d20d7e4bd005 │
|
||||
│ Base 5c88a79d75f5c2b4cc51ee6f163d2db91ee198d7 │
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ To open a patch in draft mode, we use the `--draft` option:
|
|||
|
||||
``` (stderr)
|
||||
$ git push -o patch.draft -o patch.message="Nothing yet" rad HEAD:refs/patches
|
||||
✓ Patch c639a0f9895a0fdf2ba2d04533290937cb6fd2f7 drafted
|
||||
✓ Patch 79a1a5138b7f91c6dead5544ecde285dc3d0cb45 drafted
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
* [new reference] HEAD -> refs/patches
|
||||
```
|
||||
|
|
@ -17,10 +17,10 @@ To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkE
|
|||
We can confirm it's a draft by running `show`:
|
||||
|
||||
```
|
||||
$ rad patch show c639a0f9895a0fdf2ba2d04533290937cb6fd2f7
|
||||
$ rad patch show 79a1a5138b7f91c6dead5544ecde285dc3d0cb45
|
||||
╭────────────────────────────────────────────────────────────────────╮
|
||||
│ Title Nothing yet │
|
||||
│ Patch c639a0f9895a0fdf2ba2d04533290937cb6fd2f7 │
|
||||
│ Patch 79a1a5138b7f91c6dead5544ecde285dc3d0cb45 │
|
||||
│ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │
|
||||
│ Head 2a465832b5a76abe25be44a3a5d224bbd7741ba7 │
|
||||
│ Branches cloudhead/draft │
|
||||
|
|
@ -36,14 +36,14 @@ $ rad patch show c639a0f9895a0fdf2ba2d04533290937cb6fd2f7
|
|||
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 │
|
||||
│ Patch c639a0f9895a0fdf2ba2d04533290937cb6fd2f7 │
|
||||
│ Patch 79a1a5138b7f91c6dead5544ecde285dc3d0cb45 │
|
||||
│ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │
|
||||
│ Head 2a465832b5a76abe25be44a3a5d224bbd7741ba7 │
|
||||
│ 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:
|
||||
|
||||
```
|
||||
$ rad patch ready --undo c639a0f9895a0fdf2ba2d04533290937cb6fd2f7
|
||||
$ rad patch show c639a0f9895a0fdf2ba2d04533290937cb6fd2f7
|
||||
$ rad patch ready --undo 79a1a5138b7f91c6dead5544ecde285dc3d0cb45
|
||||
$ rad patch show 79a1a5138b7f91c6dead5544ecde285dc3d0cb45
|
||||
╭─────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ Title Nothing yet │
|
||||
│ Patch c639a0f9895a0fdf2ba2d04533290937cb6fd2f7 │
|
||||
│ Patch 79a1a5138b7f91c6dead5544ecde285dc3d0cb45 │
|
||||
│ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │
|
||||
│ Head 2a465832b5a76abe25be44a3a5d224bbd7741ba7 │
|
||||
│ Branches cloudhead/draft │
|
||||
|
|
|
|||
|
|
@ -40,22 +40,22 @@ $ cd heartwood
|
|||
$ git checkout -b bob/feature -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
|
||||
✓ Patch 627477fdb46b9aaf3f0677c415b569cd21227b76 opened
|
||||
✓ Patch 26e3e563ddc7df8dd0c9f81274c0b3cb1b764568 opened
|
||||
✓ Synced with 1 node(s)
|
||||
To rad://zhbMU4DUXrzB8xT6qAJh6yZ7bFMK/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
|
||||
* [new reference] HEAD -> refs/patches
|
||||
```
|
||||
``` ~bob
|
||||
$ git status --short --branch
|
||||
## bob/feature...rad/patches/627477fdb46b9aaf3f0677c415b569cd21227b76
|
||||
## bob/feature...rad/patches/26e3e563ddc7df8dd0c9f81274c0b3cb1b764568
|
||||
```
|
||||
|
||||
Alice checks it out.
|
||||
|
||||
``` ~alice
|
||||
$ rad patch checkout 627477f
|
||||
✓ Switched to branch patch/627477f
|
||||
✓ Branch patch/627477f setup to track rad/patches/627477fdb46b9aaf3f0677c415b569cd21227b76
|
||||
$ rad patch checkout 26e3e56
|
||||
✓ Switched to branch patch/26e3e56
|
||||
✓ Branch patch/26e3e56 setup to track rad/patches/26e3e563ddc7df8dd0c9f81274c0b3cb1b764568
|
||||
$ git show
|
||||
commit bdcdb30b3c0f513620dd0f1c24ff8f4f71de956b
|
||||
Author: radicle <radicle@localhost>
|
||||
|
|
@ -69,19 +69,19 @@ Bob then updates the patch.
|
|||
``` ~bob (stderr)
|
||||
$ git commit --allow-empty -m "Bob's commit #2" -q
|
||||
$ git push rad -o sync -o patch.message="Updated."
|
||||
✓ Patch 627477f updated to c4114446af35501300c68571cfb07a6f5c7e1eef
|
||||
✓ Patch 26e3e56 updated to c04ef81bad734c65a7d5834cefcdd60c4f0484f7
|
||||
✓ Synced with 1 node(s)
|
||||
To rad://zhbMU4DUXrzB8xT6qAJh6yZ7bFMK/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
|
||||
bdcdb30..cad2666 bob/feature -> patches/627477fdb46b9aaf3f0677c415b569cd21227b76
|
||||
bdcdb30..cad2666 bob/feature -> patches/26e3e563ddc7df8dd0c9f81274c0b3cb1b764568
|
||||
```
|
||||
|
||||
Alice pulls the update.
|
||||
|
||||
``` ~alice
|
||||
$ rad patch show 627477f
|
||||
$ rad patch show 26e3e56
|
||||
╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ Title Bob's patch │
|
||||
│ Patch 627477fdb46b9aaf3f0677c415b569cd21227b76 │
|
||||
│ Patch 26e3e563ddc7df8dd0c9f81274c0b3cb1b764568 │
|
||||
│ Author did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk │
|
||||
│ Head cad2666a8a2250e4dee175ed5044be2c251ff08b │
|
||||
│ Commits ahead 2, behind 0 │
|
||||
|
|
@ -91,16 +91,16 @@ $ rad patch show 627477f
|
|||
│ bdcdb30 Bob's commit #1 │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ● opened by bob (z6Mkt67…v4N1tRk) [ ... ] │
|
||||
│ ↑ updated to c4114446af35501300c68571cfb07a6f5c7e1eef (cad2666) [ ... ] │
|
||||
│ ↑ updated to c04ef81bad734c65a7d5834cefcdd60c4f0484f7 (cad2666) [ ... ] │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯
|
||||
$ git ls-remote rad
|
||||
f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354 refs/heads/master
|
||||
cad2666a8a2250e4dee175ed5044be2c251ff08b refs/heads/patches/627477fdb46b9aaf3f0677c415b569cd21227b76
|
||||
cad2666a8a2250e4dee175ed5044be2c251ff08b refs/heads/patches/26e3e563ddc7df8dd0c9f81274c0b3cb1b764568
|
||||
```
|
||||
``` ~alice
|
||||
$ git fetch rad
|
||||
$ git status --short --branch
|
||||
## patch/627477f...rad/patches/627477fdb46b9aaf3f0677c415b569cd21227b76 [behind 1]
|
||||
## patch/26e3e56...rad/patches/26e3e563ddc7df8dd0c9f81274c0b3cb1b764568 [behind 1]
|
||||
```
|
||||
``` ~alice
|
||||
$ git pull
|
||||
|
|
|
|||
|
|
@ -6,16 +6,16 @@ $ git commit -q -m "Not a real change" --allow-empty
|
|||
```
|
||||
``` (stderr)
|
||||
$ git push rad HEAD:refs/patches
|
||||
✓ Patch 51e0d0bc168ccdc541b7b1aeab2eb9e048c2fcdd opened
|
||||
✓ Patch ea6fa6c274c55d0f4fdf203a192cbf1330b51221 opened
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
* [new reference] HEAD -> refs/patches
|
||||
```
|
||||
|
||||
```
|
||||
$ rad patch show 51e0d0bc168ccdc541b7b1aeab2eb9e048c2fcdd
|
||||
$ rad patch show ea6fa6c274c55d0f4fdf203a192cbf1330b51221
|
||||
╭────────────────────────────────────────────────────────────────────╮
|
||||
│ Title Not a real change │
|
||||
│ Patch 51e0d0bc168ccdc541b7b1aeab2eb9e048c2fcdd │
|
||||
│ Patch ea6fa6c274c55d0f4fdf203a192cbf1330b51221 │
|
||||
│ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │
|
||||
│ Head 51b2f0f77b9849bfaa3e9d3ff68ee2f57771d20c │
|
||||
│ Branches feature/1 │
|
||||
|
|
@ -46,17 +46,17 @@ Now, instead of using `git push` to update the patch, as we normally would,
|
|||
we run:
|
||||
|
||||
```
|
||||
$ rad patch update 51e0d0bc168ccdc541b7b1aeab2eb9e048c2fcdd -m "Updated patch"
|
||||
c10012c2cb9c0c9bfeba7ef28cae10e4b8db3469
|
||||
$ rad patch update ea6fa6c274c55d0f4fdf203a192cbf1330b51221 -m "Updated patch"
|
||||
59bbb5c5d3c9f18a686113e6354b1372eebafda4
|
||||
```
|
||||
|
||||
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 │
|
||||
│ Patch 51e0d0bc168ccdc541b7b1aeab2eb9e048c2fcdd │
|
||||
│ Patch ea6fa6c274c55d0f4fdf203a192cbf1330b51221 │
|
||||
│ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │
|
||||
│ Head 4d272148458a17620541555b1f0905c01658aa9f │
|
||||
│ Branches feature/1 │
|
||||
|
|
@ -67,6 +67,6 @@ $ rad patch show 51e0d0bc168ccdc541b7b1aeab2eb9e048c2fcdd
|
|||
│ 51b2f0f Not a real change │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ● opened by (you) [ ... ] │
|
||||
│ ↑ updated to c10012c2cb9c0c9bfeba7ef28cae10e4b8db3469 (4d27214) [ ... ] │
|
||||
│ ↑ updated to 59bbb5c5d3c9f18a686113e6354b1372eebafda4 (4d27214) [ ... ] │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯
|
||||
```
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ $ git checkout -b feature/1
|
|||
Switched to a new branch 'feature/1'
|
||||
$ 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
|
||||
✓ Patch 2647168c23e7c2b2c1936d695443944e143bc3f7 opened
|
||||
✓ Patch 90c77f2c33b7e472e058de4a586156f8a7fec7d6 opened
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
* [new reference] HEAD -> refs/patches
|
||||
```
|
||||
|
|
@ -16,10 +16,10 @@ To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkE
|
|||
We can see a patch was created:
|
||||
|
||||
```
|
||||
$ rad patch show 2647168
|
||||
$ rad patch show 90c77f2
|
||||
╭────────────────────────────────────────────────────────────────────╮
|
||||
│ Title Add things #1 │
|
||||
│ Patch 2647168c23e7c2b2c1936d695443944e143bc3f7 │
|
||||
│ Patch 90c77f2c33b7e472e058de4a586156f8a7fec7d6 │
|
||||
│ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │
|
||||
│ Head 42d894a83c9c356552a57af09ccdbd5587a99045 │
|
||||
│ Branches feature/1 │
|
||||
|
|
@ -39,7 +39,7 @@ branch associated with this patch:
|
|||
|
||||
```
|
||||
$ 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
|
||||
```
|
||||
|
||||
|
|
@ -47,7 +47,7 @@ Let's check that it's up to date with our local head:
|
|||
|
||||
```
|
||||
$ git status --short --branch
|
||||
## feature/1...rad/patches/2647168c23e7c2b2c1936d695443944e143bc3f7
|
||||
## feature/1...rad/patches/90c77f2c33b7e472e058de4a586156f8a7fec7d6
|
||||
$ git fetch
|
||||
$ git push
|
||||
```
|
||||
|
|
@ -59,13 +59,13 @@ $ git show-ref
|
|||
42d894a83c9c356552a57af09ccdbd5587a99045 refs/heads/feature/1
|
||||
f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354 refs/heads/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/*'
|
||||
42d894a83c9c356552a57af09ccdbd5587a99045 refs/heads/patches/2647168c23e7c2b2c1936d695443944e143bc3f7
|
||||
42d894a83c9c356552a57af09ccdbd5587a99045 refs/heads/patches/90c77f2c33b7e472e058de4a586156f8a7fec7d6
|
||||
$ 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:
|
||||
|
|
@ -74,7 +74,7 @@ We can create another patch:
|
|||
$ git checkout -b feature/2 -q master
|
||||
$ git commit -a -m "Add more things" -q --allow-empty
|
||||
$ git push rad HEAD:refs/patches
|
||||
✓ Patch b8ab1c99c1c8205680a3494f04fb3934ec738ddd opened
|
||||
✓ Patch fedf0e4dcb74ff6db1d5e30a6a254b77f02ff60b opened
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
* [new reference] HEAD -> refs/patches
|
||||
```
|
||||
|
|
@ -83,8 +83,8 @@ We see both branches with upstreams now:
|
|||
|
||||
```
|
||||
$ git branch -vv
|
||||
feature/1 42d894a [rad/patches/2647168c23e7c2b2c1936d695443944e143bc3f7] Add things
|
||||
* feature/2 8b0ea80 [rad/patches/b8ab1c99c1c8205680a3494f04fb3934ec738ddd] Add more things
|
||||
feature/1 42d894a [rad/patches/90c77f2c33b7e472e058de4a586156f8a7fec7d6] Add things
|
||||
* feature/2 8b0ea80 [rad/patches/fedf0e4dcb74ff6db1d5e30a6a254b77f02ff60b] Add more things
|
||||
master f2de534 [rad/master] Second commit
|
||||
```
|
||||
|
||||
|
|
@ -95,8 +95,8 @@ $ rad patch
|
|||
╭────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ● ID Title Author Head + - Updated │
|
||||
├────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ● 2647168 Add things #1 z6MknSL…StBU8Vi (you) 42d894a +0 -0 [ ... ] │
|
||||
│ ● b8ab1c9 Add more things z6MknSL…StBU8Vi (you) 8b0ea80 +0 -0 [ ... ] │
|
||||
│ ● 90c77f2 Add things #1 z6MknSL…StBU8Vi (you) 42d894a +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)
|
||||
$ git push
|
||||
✓ Patch b8ab1c9 updated to 8767880c31b9e4a04cdb07ad6faa9ce453980399
|
||||
✓ Patch fedf0e4 updated to d0018fcc21d87c91a1ff9155aed6b4e57535566b
|
||||
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
|
||||
|
|
@ -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:
|
||||
|
||||
```
|
||||
$ rad patch show b8ab1c9
|
||||
$ rad patch show fedf0e4
|
||||
╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ Title Add more things │
|
||||
│ Patch b8ab1c99c1c8205680a3494f04fb3934ec738ddd │
|
||||
│ Patch fedf0e4dcb74ff6db1d5e30a6a254b77f02ff60b │
|
||||
│ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │
|
||||
│ Head 02bef3fac41b2f98bb3c02b868a53ddfecb55b5f │
|
||||
│ Branches feature/2 │
|
||||
|
|
@ -142,7 +142,7 @@ $ rad patch show b8ab1c9
|
|||
│ 8b0ea80 Add more things │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ● opened by (you) [ ... ] │
|
||||
│ ↑ updated to 8767880c31b9e4a04cdb07ad6faa9ce453980399 (02bef3f) [ ... ] │
|
||||
│ ↑ updated to d0018fcc21d87c91a1ff9155aed6b4e57535566b (02bef3f) [ ... ] │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯
|
||||
```
|
||||
|
||||
|
|
@ -155,14 +155,14 @@ $ git rev-parse HEAD
|
|||
|
||||
```
|
||||
$ 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
|
||||
$ git ls-remote rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi refs/heads/patches/b8ab1c99c1c8205680a3494f04fb3934ec738ddd
|
||||
02bef3fac41b2f98bb3c02b868a53ddfecb55b5f refs/heads/patches/b8ab1c99c1c8205680a3494f04fb3934ec738ddd
|
||||
$ git ls-remote rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi refs/heads/patches/fedf0e4dcb74ff6db1d5e30a6a254b77f02ff60b
|
||||
02bef3fac41b2f98bb3c02b868a53ddfecb55b5f refs/heads/patches/fedf0e4dcb74ff6db1d5e30a6a254b77f02ff60b
|
||||
```
|
||||
|
||||
## Force push
|
||||
|
|
@ -183,7 +183,7 @@ Now let's push to the patch head.
|
|||
``` (stderr) (fail)
|
||||
$ git push
|
||||
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'
|
||||
hint: Updates were rejected because a pushed branch tip is behind its remote
|
||||
hint: counterpart. Check out this branch and integrate the remote changes
|
||||
|
|
@ -196,18 +196,18 @@ use `--force` to force the update.
|
|||
|
||||
``` (stderr)
|
||||
$ git push --force
|
||||
✓ Patch b8ab1c9 updated to f24334f8cea7b7a5bcaf3bc6deb1408c9bf507ad
|
||||
✓ Patch fedf0e4 updated to 31ecf28817c44d90686b5c3c624c1f4a534b6478
|
||||
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`:
|
||||
|
||||
```
|
||||
$ rad patch show b8ab1c9
|
||||
$ rad patch show fedf0e4
|
||||
╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ Title Add more things │
|
||||
│ Patch b8ab1c99c1c8205680a3494f04fb3934ec738ddd │
|
||||
│ Patch fedf0e4dcb74ff6db1d5e30a6a254b77f02ff60b │
|
||||
│ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │
|
||||
│ Head 9304dbc445925187994a7a93222a3f8bde73b785 │
|
||||
│ Branches feature/2 │
|
||||
|
|
@ -218,8 +218,8 @@ $ rad patch show b8ab1c9
|
|||
│ 8b0ea80 Add more things │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ● opened by (you) [ ... ] │
|
||||
│ ↑ updated to 8767880c31b9e4a04cdb07ad6faa9ce453980399 (02bef3f) [ ... ] │
|
||||
│ ↑ updated to f24334f8cea7b7a5bcaf3bc6deb1408c9bf507ad (9304dbc) [ ... ] │
|
||||
│ ↑ updated to d0018fcc21d87c91a1ff9155aed6b4e57535566b (02bef3f) [ ... ] │
|
||||
│ ↑ updated to 31ecf28817c44d90686b5c3c624c1f4a534b6478 (9304dbc) [ ... ] │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ Once the code is ready, we open (or create) a patch with our changes for the pro
|
|||
|
||||
``` (stderr)
|
||||
$ 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
|
||||
* [new reference] HEAD -> refs/patches
|
||||
```
|
||||
|
|
@ -38,14 +38,14 @@ $ rad patch
|
|||
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ● 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 │
|
||||
│ Patch 077e4bbe9a6e5546f400ef5951768c37a76f13a4 │
|
||||
│ Patch 73b73f376e93e09e0419664766ac9e433bf7d389 │
|
||||
│ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │
|
||||
│ Head 3e674d1a1df90807e934f9ae5da2591dd6848a33 │
|
||||
│ Branches flux-capacitor-power │
|
||||
|
|
@ -74,7 +74,7 @@ index 0000000..e69de29
|
|||
We can also see that it set an upstream for our patch branch:
|
||||
```
|
||||
$ 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
|
||||
```
|
||||
|
||||
|
|
@ -90,45 +90,45 @@ $ git commit --message "Add README, just for the fun"
|
|||
```
|
||||
``` (stderr)
|
||||
$ 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
|
||||
3e674d1..27857ec flux-capacitor-power -> patches/077e4bbe9a6e5546f400ef5951768c37a76f13a4
|
||||
3e674d1..27857ec flux-capacitor-power -> patches/73b73f376e93e09e0419664766ac9e433bf7d389
|
||||
```
|
||||
|
||||
And let's leave a quick comment for our team:
|
||||
|
||||
```
|
||||
$ rad comment 077e4bbe9a6e5546f400ef5951768c37a76f13a4 --message 'I cannot wait to get back to the 90s!'
|
||||
31a07b8e7758af2027e74e521a74bea4574280e7
|
||||
$ rad comment 077e4bbe9a6e5546f400ef5951768c37a76f13a4 --message 'I cannot wait to get back to the 90s!' --reply-to 31a07b8e7758af2027e74e521a74bea4574280e7
|
||||
d66bcb6bfe2e06e57636e8b1ba3ef8098a8bb250
|
||||
$ rad comment 73b73f376e93e09e0419664766ac9e433bf7d389 --message 'I cannot wait to get back to the 90s!'
|
||||
de198e9b1613d827ce294a5b36cecdb8e65abcf1
|
||||
$ rad comment 73b73f376e93e09e0419664766ac9e433bf7d389 --message 'I cannot wait to get back to the 90s!' --reply-to de198e9b1613d827ce294a5b36cecdb8e65abcf1
|
||||
bd53b38140cf8249ef31c7464d35a4c960258e3f
|
||||
```
|
||||
|
||||
Now, let's checkout the patch that we just created:
|
||||
|
||||
```
|
||||
$ rad patch checkout 077e4bb
|
||||
✓ Switched to branch patch/077e4bb
|
||||
✓ Branch patch/077e4bb setup to track rad/patches/077e4bbe9a6e5546f400ef5951768c37a76f13a4
|
||||
$ rad patch checkout 73b73f3
|
||||
✓ Switched to branch patch/73b73f3
|
||||
✓ Branch patch/73b73f3 setup to track rad/patches/73b73f376e93e09e0419664766ac9e433bf7d389
|
||||
```
|
||||
|
||||
We can also add a review verdict as such:
|
||||
|
||||
```
|
||||
$ rad review 077e4bbe9a6e5546f400ef5951768c37a76f13a4 --accept --no-message --no-sync
|
||||
✓ Patch 077e4bb accepted
|
||||
$ rad review 73b73f376e93e09e0419664766ac9e433bf7d389 --accept --no-message --no-sync
|
||||
✓ Patch 73b73f3 accepted
|
||||
```
|
||||
|
||||
Showing the patch list now will reveal the favorable verdict:
|
||||
|
||||
```
|
||||
$ rad patch show 077e4bb
|
||||
$ rad patch show 73b73f3
|
||||
╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ Title Define power requirements │
|
||||
│ Patch 077e4bbe9a6e5546f400ef5951768c37a76f13a4 │
|
||||
│ Patch 73b73f376e93e09e0419664766ac9e433bf7d389 │
|
||||
│ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │
|
||||
│ Head 27857ec9eb04c69cacab516e8bf4b5fd36090f66 │
|
||||
│ Branches flux-capacitor-power, patch/077e4bb │
|
||||
│ Branches flux-capacitor-power, patch/73b73f3 │
|
||||
│ Commits ahead 2, behind 0 │
|
||||
│ Status open │
|
||||
│ │
|
||||
|
|
@ -138,7 +138,7 @@ $ rad patch show 077e4bb
|
|||
│ 3e674d1 Define power requirements │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ● opened by (you) [ ... ] │
|
||||
│ ↑ updated to 5cdcd2e14411e2bfec7b11bcf4667e2e0fc4d417 (27857ec) [ ... ] │
|
||||
│ ↑ updated to 5605784ae81dad91ba47ea55e19dd16f6280d44b (27857ec) [ ... ] │
|
||||
│ ✓ 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!
|
||||
|
||||
```
|
||||
$ rad patch edit 077e4bb --message "Define power requirements" --message "Add requirements file"
|
||||
$ rad patch show 077e4bb
|
||||
$ rad patch edit 73b73f3 --message "Define power requirements" --message "Add requirements file"
|
||||
$ rad patch show 73b73f3
|
||||
╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ Title Define power requirements │
|
||||
│ Patch 077e4bbe9a6e5546f400ef5951768c37a76f13a4 │
|
||||
│ Patch 73b73f376e93e09e0419664766ac9e433bf7d389 │
|
||||
│ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │
|
||||
│ Head 27857ec9eb04c69cacab516e8bf4b5fd36090f66 │
|
||||
│ Branches flux-capacitor-power, patch/077e4bb │
|
||||
│ Branches flux-capacitor-power, patch/73b73f3 │
|
||||
│ Commits ahead 2, behind 0 │
|
||||
│ Status open │
|
||||
│ │
|
||||
|
|
@ -163,7 +163,7 @@ $ rad patch show 077e4bb
|
|||
│ 3e674d1 Define power requirements │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ● opened by (you) [ ... ] │
|
||||
│ ↑ updated to 5cdcd2e14411e2bfec7b11bcf4667e2e0fc4d417 (27857ec) [ ... ] │
|
||||
│ ↑ updated to 5605784ae81dad91ba47ea55e19dd16f6280d44b (27857ec) [ ... ] │
|
||||
│ ✓ accepted by (you) [ ... ] │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯
|
||||
```
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ $ git commit -q -m "Update files"
|
|||
|
||||
``` (stderr)
|
||||
$ git push rad HEAD:refs/patches
|
||||
✓ Patch a05c57911e6ec9eba4e5539a095a6a86e2bf7c2c opened
|
||||
✓ Patch 7f6ed9bd562a36eb1d5689f95600d09247726a23 opened
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
* [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`:
|
||||
|
||||
```
|
||||
$ rad review --no-sync --patch -U5 a05c57911e6ec9eba4e5539a095a6a86e2bf7c2c
|
||||
$ rad review --no-sync --patch -U5 7f6ed9bd562a36eb1d5689f95600d09247726a23
|
||||
diff --git a/.gitignore b/.gitignore
|
||||
deleted file mode 100644
|
||||
index 7937fb3..0000000
|
||||
|
|
@ -116,8 +116,8 @@ rename to notes/INSTRUCTIONS.txt
|
|||
Now let's accept these hunks one by one..
|
||||
|
||||
```
|
||||
$ rad review --no-sync --patch --accept --hunk 1 a05c57911e6ec9eba4e5539a095a6a86e2bf7c2c
|
||||
✓ Loaded existing review ([..]) for patch a05c57911e6ec9eba4e5539a095a6a86e2bf7c2c
|
||||
$ rad review --no-sync --patch --accept --hunk 1 7f6ed9bd562a36eb1d5689f95600d09247726a23
|
||||
✓ Loaded existing review ([..]) for patch 7f6ed9bd562a36eb1d5689f95600d09247726a23
|
||||
diff --git a/.gitignore b/.gitignore
|
||||
deleted file mode 100644
|
||||
index 7937fb3..0000000
|
||||
|
|
@ -127,8 +127,8 @@ index 7937fb3..0000000
|
|||
-*.draft
|
||||
```
|
||||
```
|
||||
$ rad review --no-sync --patch --accept --hunk 1 a05c57911e6ec9eba4e5539a095a6a86e2bf7c2c
|
||||
✓ Loaded existing review ([..]) for patch a05c57911e6ec9eba4e5539a095a6a86e2bf7c2c
|
||||
$ rad review --no-sync --patch --accept --hunk 1 7f6ed9bd562a36eb1d5689f95600d09247726a23
|
||||
✓ Loaded existing review ([..]) for patch 7f6ed9bd562a36eb1d5689f95600d09247726a23
|
||||
diff --git a/DISCLAIMER.txt b/DISCLAIMER.txt
|
||||
new file mode 100644
|
||||
index 0000000..2b5bd86
|
||||
|
|
@ -138,8 +138,8 @@ index 0000000..2b5bd86
|
|||
+All food is served as-is, with no warranty!
|
||||
```
|
||||
```
|
||||
$ rad review --no-sync --patch --accept -U3 --hunk 1 a05c57911e6ec9eba4e5539a095a6a86e2bf7c2c
|
||||
✓ Loaded existing review ([..]) for patch a05c57911e6ec9eba4e5539a095a6a86e2bf7c2c
|
||||
$ rad review --no-sync --patch --accept -U3 --hunk 1 7f6ed9bd562a36eb1d5689f95600d09247726a23
|
||||
✓ Loaded existing review ([..]) for patch 7f6ed9bd562a36eb1d5689f95600d09247726a23
|
||||
diff --git a/MENU.txt b/MENU.txt
|
||||
index 867958c..3af9741 100644
|
||||
--- a/MENU.txt
|
||||
|
|
@ -153,8 +153,8 @@ index 867958c..3af9741 100644
|
|||
[..]
|
||||
```
|
||||
```
|
||||
$ rad review --no-sync --patch --accept -U3 --hunk 1 a05c57911e6ec9eba4e5539a095a6a86e2bf7c2c
|
||||
✓ Loaded existing review ([..]) for patch a05c57911e6ec9eba4e5539a095a6a86e2bf7c2c
|
||||
$ rad review --no-sync --patch --accept -U3 --hunk 1 7f6ed9bd562a36eb1d5689f95600d09247726a23
|
||||
✓ Loaded existing review ([..]) for patch 7f6ed9bd562a36eb1d5689f95600d09247726a23
|
||||
diff --git a/MENU.txt b/MENU.txt
|
||||
index 4e2e828..3af9741 100644
|
||||
--- a/MENU.txt
|
||||
|
|
@ -169,8 +169,8 @@ index 4e2e828..3af9741 100644
|
|||
```
|
||||
|
||||
```
|
||||
$ rad review --no-sync --patch --accept --hunk 1 a05c57911e6ec9eba4e5539a095a6a86e2bf7c2c
|
||||
✓ Loaded existing review ([..]) for patch a05c57911e6ec9eba4e5539a095a6a86e2bf7c2c
|
||||
$ rad review --no-sync --patch --accept --hunk 1 7f6ed9bd562a36eb1d5689f95600d09247726a23
|
||||
✓ Loaded existing review ([..]) for patch 7f6ed9bd562a36eb1d5689f95600d09247726a23
|
||||
diff --git a/INSTRUCTIONS.txt b/notes/INSTRUCTIONS.txt
|
||||
similarity index 100%
|
||||
rename from INSTRUCTIONS.txt
|
||||
|
|
@ -178,7 +178,7 @@ rename to notes/INSTRUCTIONS.txt
|
|||
```
|
||||
|
||||
```
|
||||
$ rad review --no-sync --patch --accept --hunk 1 a05c57911e6ec9eba4e5539a095a6a86e2bf7c2c
|
||||
✓ Loaded existing review ([..]) for patch a05c57911e6ec9eba4e5539a095a6a86e2bf7c2c
|
||||
$ rad review --no-sync --patch --accept --hunk 1 7f6ed9bd562a36eb1d5689f95600d09247726a23
|
||||
✓ Loaded existing review ([..]) for patch 7f6ed9bd562a36eb1d5689f95600d09247726a23
|
||||
✓ All hunks have been reviewed
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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
|
||||
╭─────────────────────────────────────────────────────────╮
|
||||
│ Title flux capacitor underpowered │
|
||||
│ Issue b05e945bb63c11bf80320f4e26ad1d1f7c51f755 │
|
||||
│ Issue 2f6eb49efac492327f71437b6bfc01b49afa0981 │
|
||||
│ Status open │
|
||||
│ │
|
||||
│ Flux capacitor power requirements exceed current supply │
|
||||
|
|
@ -18,11 +18,11 @@ The issue is now listed under our project.
|
|||
|
||||
```
|
||||
$ rad issue list
|
||||
╭───────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ● ID Title Author Tags Assignees Opened │
|
||||
├───────────────────────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ● b05e945 flux capacitor underpowered z6Mkt67…v4N1tRk bob (you) [ .. ] │
|
||||
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ● ID Title Author Labels Assignees Opened │
|
||||
├─────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ● 2f6eb49 flux capacitor underpowered z6Mkt67…v4N1tRk bob (you) [ .. ] │
|
||||
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
```
|
||||
$ 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.
|
||||
|
||||
```
|
||||
$ rad issue list --assigned
|
||||
╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ● ID Title Author Tags Assignees Opened │
|
||||
├───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ● b05e945 flux capacitor underpowered z6Mkt67…v4N1tRk bob (you) bob (z6Mkt67…v4N1tRk) [ .. ] │
|
||||
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ● ID Title Author Labels Assignees Opened │
|
||||
├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ● 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`.
|
||||
|
||||
```
|
||||
$ rad issue show b05e945
|
||||
$ rad issue show 2f6eb49
|
||||
╭─────────────────────────────────────────────────────────╮
|
||||
│ Title flux capacitor underpowered │
|
||||
│ Issue b05e945bb63c11bf80320f4e26ad1d1f7c51f755 │
|
||||
│ Issue 2f6eb49efac492327f71437b6bfc01b49afa0981 │
|
||||
│ Assignees z6Mkt67…v4N1tRk │
|
||||
│ 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.
|
||||
|
||||
```
|
||||
$ rad comment b05e945bb63c11bf80320f4e26ad1d1f7c51f755 --message 'The flux capacitor needs 1.21 Gigawatts'
|
||||
8b9ee0f0a530f0318e100ea8b9ed3a723bd584f6
|
||||
$ rad comment 2f6eb49efac492327f71437b6bfc01b49afa0981 --message 'The flux capacitor needs 1.21 Gigawatts'
|
||||
24ab347afda760e77d565f9cb013c6db560f44fd
|
||||
```
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ Once the code is ready, we open a patch with our changes.
|
|||
|
||||
``` (stderr)
|
||||
$ 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
|
||||
* [new reference] HEAD -> refs/patches
|
||||
```
|
||||
|
|
@ -38,12 +38,12 @@ $ rad patch
|
|||
╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ● 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 │
|
||||
│ Patch 50e29a111972f3b7d2123c5057de5bdf09bc7b1c │
|
||||
│ Patch 69e881c606639691330051d7d8f013854f32fb87 │
|
||||
│ Author did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk │
|
||||
│ Head 3e674d1a1df90807e934f9ae5da2591dd6848a33 │
|
||||
│ 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/*
|
||||
3e674d1a1df90807e934f9ae5da2591dd6848a33 refs/heads/patches/50e29a111972f3b7d2123c5057de5bdf09bc7b1c
|
||||
3e674d1a1df90807e934f9ae5da2591dd6848a33 refs/heads/patches/69e881c606639691330051d7d8f013854f32fb87
|
||||
```
|
||||
|
||||
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
|
||||
$ 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
|
||||
3e674d1..27857ec flux-capacitor-power -> patches/50e29a111972f3b7d2123c5057de5bdf09bc7b1c
|
||||
3e674d1..27857ec flux-capacitor-power -> patches/69e881c606639691330051d7d8f013854f32fb87
|
||||
```
|
||||
|
||||
And let's leave a quick comment for our team:
|
||||
|
||||
```
|
||||
$ rad comment 50e29a111972f3b7d2123c5057de5bdf09bc7b1c --message 'I cannot wait to get back to the 90s!'
|
||||
4a9d780cf088769722d226d83a1b4663ab176f8e
|
||||
$ rad comment 69e881c606639691330051d7d8f013854f32fb87 --message 'I cannot wait to get back to the 90s!'
|
||||
f95ef6c0fb97a5dd05db49f7012010f0c49d59bc
|
||||
```
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ $ rad remote add z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --name bob
|
|||
$ git fetch bob
|
||||
From rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
|
||||
* [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.
|
||||
|
|
@ -31,12 +31,12 @@ The contributor's changes are now visible to us.
|
|||
```
|
||||
$ git branch -r
|
||||
bob/master
|
||||
bob/patches/50e29a111972f3b7d2123c5057de5bdf09bc7b1c
|
||||
bob/patches/69e881c606639691330051d7d8f013854f32fb87
|
||||
rad/master
|
||||
$ rad patch show 50e29a1
|
||||
$ rad patch show 69e881c
|
||||
╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ Title Define power requirements │
|
||||
│ Patch 50e29a111972f3b7d2123c5057de5bdf09bc7b1c │
|
||||
│ Patch 69e881c606639691330051d7d8f013854f32fb87 │
|
||||
│ Author did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk │
|
||||
│ Head 27857ec9eb04c69cacab516e8bf4b5fd36090f66 │
|
||||
│ Commits ahead 2, behind 0 │
|
||||
|
|
@ -48,7 +48,7 @@ $ rad patch show 50e29a1
|
|||
│ 3e674d1 Define power requirements │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ● 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.
|
||||
|
||||
```
|
||||
$ rad patch checkout 50e29a111972f3b7d2123c5057de5bdf09bc7b1c
|
||||
✓ Switched to branch patch/50e29a1
|
||||
✓ Branch patch/50e29a1 setup to track rad/patches/50e29a111972f3b7d2123c5057de5bdf09bc7b1c
|
||||
$ rad patch checkout 69e881c606639691330051d7d8f013854f32fb87
|
||||
✓ Switched to branch patch/69e881c
|
||||
✓ Branch patch/69e881c setup to track rad/patches/69e881c606639691330051d7d8f013854f32fb87
|
||||
$ git mv REQUIREMENTS REQUIREMENTS.md
|
||||
$ 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(-)
|
||||
rename REQUIREMENTS => REQUIREMENTS.md (100%)
|
||||
```
|
||||
``` (stderr)
|
||||
$ 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
|
||||
* [new branch] patch/50e29a1 -> patches/50e29a111972f3b7d2123c5057de5bdf09bc7b1c
|
||||
* [new branch] patch/69e881c -> patches/69e881c606639691330051d7d8f013854f32fb87
|
||||
```
|
||||
|
||||
Great, all fixed up, lets merge the code.
|
||||
|
|
@ -79,7 +79,7 @@ Great, all fixed up, lets merge the code.
|
|||
```
|
||||
$ git checkout master
|
||||
Your branch is up to date with 'rad/master'.
|
||||
$ git merge patch/50e29a1
|
||||
$ git merge patch/69e881c
|
||||
Updating f2de534..f567f69
|
||||
Fast-forward
|
||||
README.md | 0
|
||||
|
|
@ -93,13 +93,13 @@ $ git push rad master
|
|||
The patch is now merged and closed :).
|
||||
|
||||
```
|
||||
$ rad patch show 50e29a1
|
||||
$ rad patch show 69e881c
|
||||
╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ Title Define power requirements │
|
||||
│ Patch 50e29a111972f3b7d2123c5057de5bdf09bc7b1c │
|
||||
│ Patch 69e881c606639691330051d7d8f013854f32fb87 │
|
||||
│ Author did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk │
|
||||
│ Head f567f695d25b4e8fb63b5f5ad2a584529826e908 │
|
||||
│ Branches master, patch/50e29a1 │
|
||||
│ Branches master, patch/69e881c │
|
||||
│ Commits up to date │
|
||||
│ Status merged │
|
||||
│ │
|
||||
|
|
@ -110,8 +110,8 @@ $ rad patch show 50e29a1
|
|||
│ 3e674d1 Define power requirements │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ● opened by bob (z6Mkt67…v4N1tRk) [ ... ] │
|
||||
│ ↑ updated to 3530243d46a2e7a8e4eac7afcbb17cc7c56b3d29 (27857ec) [ ... ] │
|
||||
│ ↑ updated to 744c1f0a75b1c42833c9aa32f79cd40443925d66 (f567f69) [ ... ] │
|
||||
│ ↑ updated to dcf3e6dd97c95cf8653cbb8ce47df20d28eb1821 (27857ec) [ ... ] │
|
||||
│ ↑ updated to ab05fcdca93cf4d5b22da8913e2fe0b6d8c79338 (f567f69) [ ... ] │
|
||||
│ ✓ merged by alice (you) [ ... ] │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯
|
||||
```
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ pub mod rad_init;
|
|||
pub mod rad_inspect;
|
||||
#[path = "commands/issue.rs"]
|
||||
pub mod rad_issue;
|
||||
#[path = "commands/label.rs"]
|
||||
pub mod rad_label;
|
||||
#[path = "commands/ls.rs"]
|
||||
pub mod rad_ls;
|
||||
#[path = "commands/node.rs"]
|
||||
|
|
@ -42,14 +44,12 @@ pub mod rad_rm;
|
|||
pub mod rad_self;
|
||||
#[path = "commands/sync.rs"]
|
||||
pub mod rad_sync;
|
||||
#[path = "commands/tag.rs"]
|
||||
pub mod rad_tag;
|
||||
#[path = "commands/track.rs"]
|
||||
pub mod rad_track;
|
||||
#[path = "commands/unassign.rs"]
|
||||
pub mod rad_unassign;
|
||||
#[path = "commands/untag.rs"]
|
||||
pub mod rad_untag;
|
||||
#[path = "commands/unlabel.rs"]
|
||||
pub mod rad_unlabel;
|
||||
#[path = "commands/untrack.rs"]
|
||||
pub mod rad_untrack;
|
||||
#[path = "commands/web.rs"]
|
||||
|
|
|
|||
|
|
@ -31,10 +31,10 @@ const COMMANDS: &[Help] = &[
|
|||
rad_review::HELP,
|
||||
rad_rm::HELP,
|
||||
rad_self::HELP,
|
||||
rad_tag::HELP,
|
||||
rad_label::HELP,
|
||||
rad_track::HELP,
|
||||
rad_unassign::HELP,
|
||||
rad_untag::HELP,
|
||||
rad_unlabel::HELP,
|
||||
rad_untrack::HELP,
|
||||
rad_remote::HELP,
|
||||
rad_sync::HELP,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use std::str::FromStr;
|
|||
|
||||
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::{CloseReason, Issues, State};
|
||||
use radicle::cob::thread;
|
||||
|
|
@ -36,7 +36,7 @@ Usage
|
|||
rad issue delete <issue-id> [<option>...]
|
||||
rad issue edit <issue-id> [<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 show <issue-id> [<option>...]
|
||||
rad issue state <issue-id> [--closed | --open | --solved] [<option>...]
|
||||
|
|
@ -52,7 +52,7 @@ Options
|
|||
#[derive(serde::Deserialize, serde::Serialize, Debug)]
|
||||
pub struct Metadata {
|
||||
title: String,
|
||||
tags: Vec<Tag>,
|
||||
labels: Vec<Label>,
|
||||
assignees: Vec<Did>,
|
||||
}
|
||||
|
||||
|
|
@ -86,7 +86,7 @@ pub enum Operation {
|
|||
Open {
|
||||
title: Option<String>,
|
||||
description: Option<String>,
|
||||
tags: Vec<Tag>,
|
||||
labels: Vec<Label>,
|
||||
},
|
||||
Show {
|
||||
id: Rev,
|
||||
|
|
@ -129,7 +129,7 @@ impl Args for Options {
|
|||
let mut comment_id: Option<thread::CommentId> = None;
|
||||
let mut description: Option<String> = None;
|
||||
let mut state: Option<State> = Some(State::Open);
|
||||
let mut tags = Vec::new();
|
||||
let mut labels = Vec::new();
|
||||
let mut announce = true;
|
||||
let mut quiet = false;
|
||||
|
||||
|
|
@ -157,12 +157,12 @@ impl Args for Options {
|
|||
Long("title") if op == Some(OperationName::Open) => {
|
||||
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 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) => {
|
||||
state = Some(State::Closed {
|
||||
|
|
@ -234,7 +234,7 @@ impl Args for Options {
|
|||
OperationName::Open => Operation::Open {
|
||||
title,
|
||||
description,
|
||||
tags,
|
||||
labels,
|
||||
},
|
||||
OperationName::Show => Operation::Show {
|
||||
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 {
|
||||
title: Some(title),
|
||||
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 {
|
||||
show_issue(&issue, issue.id())?;
|
||||
}
|
||||
|
|
@ -329,12 +329,12 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
|||
Operation::Open {
|
||||
ref title,
|
||||
ref description,
|
||||
ref tags,
|
||||
ref labels,
|
||||
} => {
|
||||
open(
|
||||
title.clone(),
|
||||
description.clone(),
|
||||
tags.to_vec(),
|
||||
labels.to_vec(),
|
||||
&options,
|
||||
&mut issues,
|
||||
&signer,
|
||||
|
|
@ -387,7 +387,7 @@ fn list<R: WriteRepository + cob::Store>(
|
|||
};
|
||||
|
||||
if let Some(a) = assignee {
|
||||
if !issue.assigned().any(|v| v == Did::from(a)) {
|
||||
if !issue.assigned().any(|v| v == &Did::from(a)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
|
@ -413,7 +413,7 @@ fn list<R: WriteRepository + cob::Store>(
|
|||
term::format::bold(String::from("Title")).into(),
|
||||
term::format::bold(String::from("Author")).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("Opened")).into(),
|
||||
]);
|
||||
|
|
@ -424,7 +424,7 @@ fn list<R: WriteRepository + cob::Store>(
|
|||
for (id, issue) in all {
|
||||
let assigned: String = issue
|
||||
.assigned()
|
||||
.map(|ref p| {
|
||||
.map(|p| {
|
||||
if let Some(alias) = aliases.alias(p) {
|
||||
format!("{alias} ({})", term::format::did(p))
|
||||
} else {
|
||||
|
|
@ -434,8 +434,8 @@ fn list<R: WriteRepository + cob::Store>(
|
|||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
|
||||
let mut tags = issue.tags().map(|t| t.to_string()).collect::<Vec<_>>();
|
||||
tags.sort();
|
||||
let mut labels = issue.labels().map(|t| t.to_string()).collect::<Vec<_>>();
|
||||
labels.sort();
|
||||
|
||||
let author = issue.author().id;
|
||||
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::did(&issue.author().id).dim().into(),
|
||||
display.alias(),
|
||||
term::format::secondary(tags.join(", ")).into(),
|
||||
term::format::secondary(labels.join(", ")).into(),
|
||||
if assigned.is_empty() {
|
||||
term::format::dim(String::default()).into()
|
||||
} else {
|
||||
|
|
@ -473,7 +473,7 @@ fn list<R: WriteRepository + cob::Store>(
|
|||
fn prompt_issue(
|
||||
title: &str,
|
||||
description: &str,
|
||||
tags: &[Tag],
|
||||
labels: &[Label],
|
||||
assignees: &[Did],
|
||||
) -> anyhow::Result<Option<(Metadata, String)>> {
|
||||
let title = if title.is_empty() {
|
||||
|
|
@ -491,7 +491,7 @@ fn prompt_issue(
|
|||
|
||||
let meta = Metadata {
|
||||
title: title.to_string(),
|
||||
tags: tags.to_vec(),
|
||||
labels: labels.to_vec(),
|
||||
assignees: assignees.to_vec(),
|
||||
};
|
||||
let yaml = serde_yaml::to_string(&meta)?;
|
||||
|
|
@ -550,7 +550,7 @@ fn prompt_issue(
|
|||
fn open<R: WriteRepository + cob::Store, G: Signer>(
|
||||
title: Option<String>,
|
||||
description: Option<String>,
|
||||
tags: Vec<Tag>,
|
||||
labels: Vec<Label>,
|
||||
options: &Options,
|
||||
issues: &mut Issues<R>,
|
||||
signer: &G,
|
||||
|
|
@ -558,7 +558,7 @@ fn open<R: WriteRepository + cob::Store, G: Signer>(
|
|||
let Some((meta, description)) = prompt_issue(
|
||||
&title.unwrap_or_default(),
|
||||
&description.unwrap_or_default(),
|
||||
&tags,
|
||||
&labels,
|
||||
&[],
|
||||
)? else {
|
||||
return Ok(());
|
||||
|
|
@ -567,12 +567,8 @@ fn open<R: WriteRepository + cob::Store, G: Signer>(
|
|||
let issue = issues.create(
|
||||
&meta.title,
|
||||
description.trim(),
|
||||
meta.tags.as_slice(),
|
||||
meta.assignees
|
||||
.into_iter()
|
||||
.map(cob::ActorId::from)
|
||||
.collect::<Vec<_>>()
|
||||
.as_slice(),
|
||||
meta.labels.as_slice(),
|
||||
meta.assignees.as_slice(),
|
||||
signer,
|
||||
)?;
|
||||
if !options.quiet {
|
||||
|
|
@ -611,49 +607,23 @@ fn edit<R: WriteRepository + cob::Store, G: radicle::crypto::Signer>(
|
|||
}
|
||||
|
||||
// Editing by editor
|
||||
let tags: Vec<_> = issue.tags().cloned().collect();
|
||||
let assigned: Vec<_> = issue.assigned().collect();
|
||||
let labels: Vec<_> = issue.labels().cloned().collect();
|
||||
let assigned: Vec<_> = issue.assigned().cloned().collect();
|
||||
|
||||
let Some((meta, description)) = prompt_issue(
|
||||
let Some((edited, description)) = prompt_issue(
|
||||
issue.title(),
|
||||
issue_desc,
|
||||
&tags,
|
||||
&labels,
|
||||
&assigned,
|
||||
)? else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
issue.transaction("Edit", signer, |tx| {
|
||||
tx.edit(meta.title)?;
|
||||
tx.edit(edited.title)?;
|
||||
tx.edit_comment(desc_id, description)?;
|
||||
|
||||
let add: Vec<_> = meta
|
||||
.tags
|
||||
.iter()
|
||||
.filter(|t| !tags.contains(t))
|
||||
.cloned()
|
||||
.collect();
|
||||
let remove: Vec<_> = tags
|
||||
.iter()
|
||||
.filter(|t| !meta.tags.contains(t))
|
||||
.cloned()
|
||||
.collect();
|
||||
tx.tag(add, remove)?;
|
||||
|
||||
let assign: Vec<_> = meta
|
||||
.assignees
|
||||
.iter()
|
||||
.filter(|t| !assigned.contains(t))
|
||||
.cloned()
|
||||
.map(cob::ActorId::from)
|
||||
.collect();
|
||||
let unassign: Vec<_> = assigned
|
||||
.iter()
|
||||
.filter(|t| !meta.assignees.contains(t))
|
||||
.cloned()
|
||||
.map(cob::ActorId::from)
|
||||
.collect();
|
||||
tx.assign(assign, unassign)?;
|
||||
tx.label(edited.labels)?;
|
||||
tx.assign(edited.assignees)?;
|
||||
|
||||
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<()> {
|
||||
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
|
||||
.assigned()
|
||||
.map(|a| term::format::did(&a).to_string())
|
||||
.map(|a| term::format::did(a).to_string())
|
||||
.collect();
|
||||
|
||||
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()),
|
||||
]);
|
||||
|
||||
if !tags.is_empty() {
|
||||
if !labels.is_empty() {
|
||||
attrs.push([
|
||||
term::format::tertiary("Tags".to_owned()),
|
||||
term::format::secondary(tags.join(", ")),
|
||||
term::format::tertiary("Labels".to_owned()),
|
||||
term::format::secondary(labels.join(", ")),
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use anyhow::anyhow;
|
|||
use nonempty::NonEmpty;
|
||||
|
||||
use radicle::cob;
|
||||
use radicle::cob::common::Tag;
|
||||
use radicle::cob::common::Label;
|
||||
use radicle::cob::{issue, patch, store};
|
||||
use radicle::crypto::Signer;
|
||||
use radicle::storage::{self, WriteStorage};
|
||||
|
|
@ -14,13 +14,15 @@ use crate::terminal as term;
|
|||
use crate::terminal::args::{Args, Error, Help};
|
||||
|
||||
pub const HELP: Help = Help {
|
||||
name: "tag",
|
||||
description: "Tag an issue or patch",
|
||||
name: "label",
|
||||
description: "Label an issue or patch",
|
||||
version: env!("CARGO_PKG_VERSION"),
|
||||
usage: r#"
|
||||
Usage
|
||||
|
||||
rad tag <issue-id> <tag>... [<option>...]
|
||||
rad label <issue-id> <label>... [<option>...]
|
||||
|
||||
Adds the given labels to the patch or issue.
|
||||
|
||||
Options
|
||||
|
||||
|
|
@ -31,7 +33,7 @@ Options
|
|||
#[derive(Debug)]
|
||||
pub struct Options {
|
||||
pub id: cob::ObjectId,
|
||||
pub tags: NonEmpty<Tag>,
|
||||
pub labels: NonEmpty<Label>,
|
||||
}
|
||||
|
||||
impl Args for Options {
|
||||
|
|
@ -40,7 +42,7 @@ impl Args for Options {
|
|||
|
||||
let mut parser = lexopt::Parser::from_args(args);
|
||||
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()? {
|
||||
match arg {
|
||||
|
|
@ -52,9 +54,9 @@ impl Args for Options {
|
|||
}
|
||||
Value(ref val) if id.is_some() => {
|
||||
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()));
|
||||
|
|
@ -65,15 +67,15 @@ impl Args for Options {
|
|||
Ok((
|
||||
Options {
|
||||
id: id.ok_or_else(|| anyhow!("an issue or patch must be specified"))?,
|
||||
tags: NonEmpty::from_vec(tags)
|
||||
.ok_or_else(|| anyhow!("at least one tag must be specified"))?,
|
||||
labels: NonEmpty::from_vec(labels)
|
||||
.ok_or_else(|| anyhow!("at least one label must be specified"))?,
|
||||
},
|
||||
vec![],
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn tag(
|
||||
fn label(
|
||||
options: Options,
|
||||
repo: &storage::git::Repository,
|
||||
signer: impl Signer,
|
||||
|
|
@ -81,7 +83,13 @@ fn tag(
|
|||
let mut issues = issue::Issues::open(repo)?;
|
||||
match issues.get_mut(&options.id) {
|
||||
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(());
|
||||
}
|
||||
|
|
@ -92,7 +100,13 @@ fn tag(
|
|||
let mut patches = patch::Patches::open(repo)?;
|
||||
match patches.get_mut(&options.id) {
|
||||
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(());
|
||||
}
|
||||
|
|
@ -109,7 +123,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
|||
let repo = profile.storage.repository_mut(id)?;
|
||||
let signer = term::signer(&profile)?;
|
||||
|
||||
tag(options, &repo, signer)?;
|
||||
label(options, &repo, signer)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -256,7 +256,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
|||
} else {
|
||||
Some(message)
|
||||
};
|
||||
patch.review(revision_id, verdict, message, &signer)?;
|
||||
patch.review(revision_id, verdict, message, vec![], &signer)?;
|
||||
|
||||
match verdict {
|
||||
Some(Verdict::Accept) => {
|
||||
|
|
|
|||
|
|
@ -86,8 +86,13 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
|||
_ => e.into(),
|
||||
})?;
|
||||
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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use anyhow::anyhow;
|
|||
use nonempty::NonEmpty;
|
||||
|
||||
use radicle::cob;
|
||||
use radicle::cob::common::Tag;
|
||||
use radicle::cob::common::Label;
|
||||
use radicle::cob::{issue, patch, store};
|
||||
use radicle::crypto::Signer;
|
||||
use radicle::storage::{self, WriteStorage};
|
||||
|
|
@ -31,7 +31,7 @@ Options
|
|||
#[derive(Debug)]
|
||||
pub struct Options {
|
||||
pub id: cob::ObjectId,
|
||||
pub tags: NonEmpty<Tag>,
|
||||
pub labels: NonEmpty<Label>,
|
||||
}
|
||||
|
||||
impl Args for Options {
|
||||
|
|
@ -40,7 +40,7 @@ impl Args for Options {
|
|||
|
||||
let mut parser = lexopt::Parser::from_args(args);
|
||||
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()? {
|
||||
match arg {
|
||||
|
|
@ -52,9 +52,9 @@ impl Args for Options {
|
|||
}
|
||||
Value(ref val) if id.is_some() => {
|
||||
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()));
|
||||
|
|
@ -65,15 +65,15 @@ impl Args for Options {
|
|||
Ok((
|
||||
Options {
|
||||
id: id.ok_or_else(|| anyhow!("an issue or patch must be specified"))?,
|
||||
tags: NonEmpty::from_vec(tags)
|
||||
.ok_or_else(|| anyhow!("at least one tag must be specified"))?,
|
||||
labels: NonEmpty::from_vec(labels)
|
||||
.ok_or_else(|| anyhow!("at least one label must be specified"))?,
|
||||
},
|
||||
vec![],
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn untag(
|
||||
fn unlabel(
|
||||
options: Options,
|
||||
repo: &storage::git::Repository,
|
||||
signer: impl Signer,
|
||||
|
|
@ -81,7 +81,12 @@ fn untag(
|
|||
let mut issues = issue::Issues::open(repo)?;
|
||||
match issues.get_mut(&options.id) {
|
||||
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(());
|
||||
}
|
||||
|
|
@ -92,7 +97,12 @@ fn untag(
|
|||
let mut patches = patch::Patches::open(repo)?;
|
||||
match patches.get_mut(&options.id) {
|
||||
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(());
|
||||
}
|
||||
|
|
@ -109,7 +119,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
|||
let repo = profile.storage.repository_mut(id)?;
|
||||
let signer = term::signer(&profile)?;
|
||||
|
||||
untag(options, &repo, signer)?;
|
||||
unlabel(options, &repo, signer)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -265,11 +265,11 @@ fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option<anyhow::Error>>
|
|||
args.to_vec(),
|
||||
);
|
||||
}
|
||||
"tag" => {
|
||||
term::run_command_args::<rad_tag::Options, _>(
|
||||
rad_tag::HELP,
|
||||
"Tag",
|
||||
rad_tag::run,
|
||||
"label" => {
|
||||
term::run_command_args::<rad_label::Options, _>(
|
||||
rad_label::HELP,
|
||||
"Label",
|
||||
rad_label::run,
|
||||
args.to_vec(),
|
||||
);
|
||||
}
|
||||
|
|
@ -289,11 +289,11 @@ fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option<anyhow::Error>>
|
|||
args.to_vec(),
|
||||
);
|
||||
}
|
||||
"untag" => {
|
||||
term::run_command_args::<rad_untag::Options, _>(
|
||||
rad_untag::HELP,
|
||||
"Untag",
|
||||
rad_untag::run,
|
||||
"unlabel" => {
|
||||
term::run_command_args::<rad_unlabel::Options, _>(
|
||||
rad_unlabel::HELP,
|
||||
"Unlabel",
|
||||
rad_unlabel::run,
|
||||
args.to_vec(),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ fn rad_issue() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn rad_tag() {
|
||||
fn rad_label() {
|
||||
let mut environment = Environment::new();
|
||||
let profile = environment.profile("alice");
|
||||
let home = &profile.home;
|
||||
|
|
@ -102,7 +102,7 @@ fn rad_tag() {
|
|||
|
||||
test("examples/rad-init.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]
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ use git_ext::Oid;
|
|||
use nonempty::NonEmpty;
|
||||
use radicle_git_ext::commit::trailers::OwnedTrailer;
|
||||
|
||||
use crate::change::store::Version;
|
||||
use crate::history::entry::Timestamp;
|
||||
use crate::signatures;
|
||||
use crate::{
|
||||
|
|
@ -98,17 +99,12 @@ impl change::Storage for git2::Repository {
|
|||
Signer: crypto::Signer,
|
||||
{
|
||||
let change::Template {
|
||||
typename,
|
||||
history_type,
|
||||
type_name,
|
||||
tips,
|
||||
message,
|
||||
contents,
|
||||
} = spec;
|
||||
let manifest = store::Manifest {
|
||||
typename,
|
||||
history_type,
|
||||
};
|
||||
|
||||
let manifest = store::Manifest::new(type_name, Version::default());
|
||||
let revision = write_manifest(self, &manifest, &contents)?;
|
||||
let tree = self.find_tree(revision)?;
|
||||
let signature = {
|
||||
|
|
@ -211,6 +207,7 @@ fn load_manifest(
|
|||
let manifest_blob = manifest_object
|
||||
.as_blob()
|
||||
.ok_or_else(|| error::Load::ManifestIsNotBlob(tree.id().into()))?;
|
||||
|
||||
serde_json::from_slice(manifest_blob.content()).map_err(|err| error::Load::InvalidManifest {
|
||||
id: tree.id().into(),
|
||||
err,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Copyright © 2022 The Radicle Link Contributors
|
||||
|
||||
use std::{error::Error, fmt};
|
||||
use std::{error::Error, fmt, num::NonZeroUsize};
|
||||
|
||||
use nonempty::NonEmpty;
|
||||
use radicle_git_ext::Oid;
|
||||
|
|
@ -45,8 +45,7 @@ pub trait Storage {
|
|||
|
||||
/// Change template, used to create a new change.
|
||||
pub struct Template<Id> {
|
||||
pub typename: TypeName,
|
||||
pub history_type: String,
|
||||
pub type_name: TypeName,
|
||||
pub tips: Vec<Id>,
|
||||
pub message: String,
|
||||
pub contents: NonEmpty<Vec<u8>>,
|
||||
|
|
@ -89,8 +88,8 @@ impl<Resource, Id, Signatures> Change<Resource, Id, Signatures> {
|
|||
&self.id
|
||||
}
|
||||
|
||||
pub fn typename(&self) -> &TypeName {
|
||||
&self.manifest.typename
|
||||
pub fn type_name(&self) -> &TypeName {
|
||||
&self.manifest.type_name
|
||||
}
|
||||
|
||||
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 {
|
||||
/// The name given to the type of collaborative object.
|
||||
pub typename: TypeName,
|
||||
/// The type of history for the collaborative oject.
|
||||
pub history_type: String,
|
||||
#[serde(alias = "typename")] // Deprecated name for compatibility reasons.
|
||||
pub type_name: TypeName,
|
||||
/// 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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ impl ChangeGraph {
|
|||
change.resource,
|
||||
change.contents().clone(),
|
||||
change.timestamp,
|
||||
change.manifest.clone(),
|
||||
);
|
||||
let id = *entry.id();
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ use radicle_dag::Dag;
|
|||
pub mod entry;
|
||||
pub use entry::{Contents, Entry, EntryId, Timestamp};
|
||||
|
||||
use crate::Manifest;
|
||||
|
||||
/// The DAG of changes making up the history of a collaborative object.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct History {
|
||||
|
|
@ -40,6 +42,7 @@ impl History {
|
|||
resource: Oid,
|
||||
contents: Contents,
|
||||
timestamp: Timestamp,
|
||||
manifest: Manifest,
|
||||
) -> Self
|
||||
where
|
||||
Id: Into<EntryId>,
|
||||
|
|
@ -51,6 +54,7 @@ impl History {
|
|||
resource,
|
||||
contents,
|
||||
timestamp,
|
||||
manifest,
|
||||
};
|
||||
|
||||
Self {
|
||||
|
|
@ -111,12 +115,20 @@ impl History {
|
|||
new_resource: Oid,
|
||||
new_contents: Contents,
|
||||
new_timestamp: Timestamp,
|
||||
manifest: Manifest,
|
||||
) where
|
||||
Id: Into<EntryId>,
|
||||
{
|
||||
let tips = self.tips();
|
||||
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);
|
||||
|
||||
|
|
@ -141,7 +153,10 @@ impl History {
|
|||
}
|
||||
|
||||
/// Get the root entry.
|
||||
pub fn root(&self) -> EntryId {
|
||||
self.root
|
||||
pub fn root(&self) -> &Entry {
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use nonempty::NonEmpty;
|
|||
use radicle_crypto::PublicKey;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{object, ObjectId};
|
||||
use crate::{object, Manifest, ObjectId, Version};
|
||||
|
||||
/// Entry contents.
|
||||
/// This is the change payload.
|
||||
|
|
@ -90,6 +90,8 @@ pub struct Entry {
|
|||
pub(super) contents: Contents,
|
||||
/// The entry timestamp, as seconds since epoch.
|
||||
pub(super) timestamp: Timestamp,
|
||||
/// COB manifest.
|
||||
pub(super) manifest: Manifest,
|
||||
}
|
||||
|
||||
impl Entry {
|
||||
|
|
@ -99,6 +101,7 @@ impl Entry {
|
|||
resource: Oid,
|
||||
contents: Contents,
|
||||
timestamp: Timestamp,
|
||||
manifest: Manifest,
|
||||
) -> Self
|
||||
where
|
||||
Id: Into<EntryId>,
|
||||
|
|
@ -109,6 +112,7 @@ impl Entry {
|
|||
resource,
|
||||
contents,
|
||||
timestamp,
|
||||
manifest,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -122,6 +126,16 @@ impl Entry {
|
|||
&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.
|
||||
pub fn timestamp(&self) -> Timestamp {
|
||||
self.timestamp
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ mod change_graph;
|
|||
mod trailers;
|
||||
|
||||
pub mod change;
|
||||
pub use change::store::{Manifest, Version};
|
||||
pub use change::Change;
|
||||
|
||||
pub mod history;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use std::collections::BTreeSet;
|
|||
|
||||
use git_ext::Oid;
|
||||
|
||||
use crate::change::store::Manifest;
|
||||
use crate::change::store::{Manifest, Version};
|
||||
use crate::{change, History, ObjectId, TypeName};
|
||||
|
||||
pub mod error;
|
||||
|
|
@ -47,7 +47,7 @@ impl CollaborativeObject {
|
|||
}
|
||||
|
||||
pub fn typename(&self) -> &TypeName {
|
||||
&self.manifest.typename
|
||||
&self.manifest.type_name
|
||||
}
|
||||
|
||||
pub fn manifest(&self) -> &Manifest {
|
||||
|
|
|
|||
|
|
@ -8,21 +8,20 @@ use super::*;
|
|||
|
||||
/// The metadata required for creating a new [`CollaborativeObject`].
|
||||
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.
|
||||
pub contents: NonEmpty<Vec<u8>>,
|
||||
/// The typename for this object.
|
||||
pub typename: TypeName,
|
||||
pub type_name: TypeName,
|
||||
/// The message to add when creating this object.
|
||||
pub message: String,
|
||||
/// COB version.
|
||||
pub version: Version,
|
||||
}
|
||||
|
||||
impl Create {
|
||||
fn template(&self) -> change::Template<git_ext::Oid> {
|
||||
change::Template {
|
||||
typename: self.typename.clone(),
|
||||
history_type: self.history_type.clone(),
|
||||
type_name: self.type_name.clone(),
|
||||
tips: Vec::new(),
|
||||
message: self.message.clone(),
|
||||
contents: self.contents.clone(),
|
||||
|
|
@ -60,14 +59,14 @@ where
|
|||
S: Store<I>,
|
||||
G: crypto::Signer,
|
||||
{
|
||||
let Create { ref typename, .. } = &args;
|
||||
let Create { type_name, .. } = &args;
|
||||
let init_change = storage
|
||||
.store(resource, parents, signer, args.template())
|
||||
.map_err(error::Create::from)?;
|
||||
let object_id = init_change.id().into();
|
||||
|
||||
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) })?;
|
||||
|
||||
let history = History::new_from_root(
|
||||
|
|
@ -76,13 +75,11 @@ where
|
|||
resource,
|
||||
init_change.contents,
|
||||
init_change.timestamp,
|
||||
init_change.manifest,
|
||||
);
|
||||
|
||||
Ok(CollaborativeObject {
|
||||
manifest: Manifest {
|
||||
typename: args.typename,
|
||||
history_type: args.history_type,
|
||||
},
|
||||
manifest: Manifest::new(args.type_name, args.version),
|
||||
history,
|
||||
id: object_id,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -18,14 +18,12 @@ pub struct Updated {
|
|||
|
||||
/// The data required to update an object
|
||||
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.
|
||||
pub changes: NonEmpty<Vec<u8>>,
|
||||
/// The object ID of the object to be updated.
|
||||
pub object_id: ObjectId,
|
||||
/// The typename of the object to be updated.
|
||||
pub typename: TypeName,
|
||||
pub type_name: TypeName,
|
||||
/// The message to add when updating this object.
|
||||
pub message: String,
|
||||
}
|
||||
|
|
@ -62,9 +60,8 @@ where
|
|||
G: crypto::Signer,
|
||||
{
|
||||
let Update {
|
||||
ref typename,
|
||||
type_name: ref typename,
|
||||
object_id,
|
||||
history_type,
|
||||
changes,
|
||||
message,
|
||||
} = args;
|
||||
|
|
@ -83,9 +80,8 @@ where
|
|||
signer,
|
||||
change::Template {
|
||||
tips: object.tips().iter().cloned().collect(),
|
||||
history_type,
|
||||
contents: changes,
|
||||
typename: typename.clone(),
|
||||
type_name: typename.clone(),
|
||||
message,
|
||||
},
|
||||
)?;
|
||||
|
|
@ -100,6 +96,7 @@ where
|
|||
change.resource,
|
||||
change.contents,
|
||||
change.timestamp,
|
||||
change.manifest,
|
||||
);
|
||||
|
||||
Ok(Updated {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use radicle_crypto::Signer;
|
|||
|
||||
use crate::{
|
||||
create, get, list, object, test::arbitrary::Invalid, update, Create, ObjectId, TypeName,
|
||||
Update, Updated,
|
||||
Update, Updated, Version,
|
||||
};
|
||||
|
||||
use super::test;
|
||||
|
|
@ -31,10 +31,10 @@ fn roundtrip() {
|
|||
vec![],
|
||||
&proj.identifier(),
|
||||
Create {
|
||||
history_type: "test".to_string(),
|
||||
contents: nonempty!(Vec::new()),
|
||||
typename: typename.clone(),
|
||||
type_name: typename.clone(),
|
||||
message: "creating xyz.rad.issue".to_string(),
|
||||
version: Version::default(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
|
@ -64,10 +64,10 @@ fn list_cobs() {
|
|||
vec![],
|
||||
&proj.identifier(),
|
||||
Create {
|
||||
history_type: "test".to_string(),
|
||||
contents: nonempty!(b"issue 1".to_vec()),
|
||||
typename: typename.clone(),
|
||||
type_name: typename.clone(),
|
||||
message: "creating xyz.rad.issue".to_string(),
|
||||
version: Version::default(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
|
@ -79,10 +79,10 @@ fn list_cobs() {
|
|||
vec![],
|
||||
&proj.identifier(),
|
||||
Create {
|
||||
history_type: "test".to_string(),
|
||||
contents: nonempty!(b"issue 2".to_vec()),
|
||||
typename: typename.clone(),
|
||||
type_name: typename.clone(),
|
||||
message: "commenting xyz.rad.issue".to_string(),
|
||||
version: Version::default(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
|
@ -114,10 +114,10 @@ fn update_cob() {
|
|||
vec![],
|
||||
&proj.identifier(),
|
||||
Create {
|
||||
history_type: "test".to_string(),
|
||||
contents: nonempty!(Vec::new()),
|
||||
typename: typename.clone(),
|
||||
type_name: typename.clone(),
|
||||
message: "creating xyz.rad.issue".to_string(),
|
||||
version: Version::default(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
|
@ -134,9 +134,8 @@ fn update_cob() {
|
|||
&proj.identifier(),
|
||||
Update {
|
||||
changes: nonempty!(b"issue 1".to_vec()),
|
||||
history_type: "test".to_string(),
|
||||
object_id: *cob.id(),
|
||||
typename: typename.clone(),
|
||||
type_name: typename.clone(),
|
||||
message: "commenting xyz.rad.issue".to_string(),
|
||||
},
|
||||
)
|
||||
|
|
@ -175,9 +174,9 @@ fn traverse_cobs() {
|
|||
&terry_proj.identifier(),
|
||||
Create {
|
||||
contents: nonempty!(b"issue 1".to_vec()),
|
||||
history_type: "test".to_string(),
|
||||
typename: typename.clone(),
|
||||
type_name: typename.clone(),
|
||||
message: "creating xyz.rad.issue".to_string(),
|
||||
version: Version::default(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
|
@ -198,9 +197,8 @@ fn traverse_cobs() {
|
|||
&neil_proj.identifier(),
|
||||
Update {
|
||||
changes: nonempty!(b"issue 2".to_vec()),
|
||||
history_type: "test".to_string(),
|
||||
object_id: *cob.id(),
|
||||
typename,
|
||||
type_name: typename,
|
||||
message: "commenting on xyz.rad.issue".to_string(),
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -77,20 +77,29 @@ pub enum Error {
|
|||
/// Routing store error.
|
||||
#[error(transparent)]
|
||||
RoutingStore(#[from] radicle::node::routing::Error),
|
||||
|
||||
/// Invalid update to issue or patch.
|
||||
#[error("{0}")]
|
||||
BadRequest(String),
|
||||
}
|
||||
|
||||
impl IntoResponse for Error {
|
||||
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::CobStore(radicle::cob::store::Error::NotFound(_, _)) => {
|
||||
(StatusCode::NOT_FOUND, None)
|
||||
}
|
||||
Error::Auth(msg) => (StatusCode::BAD_REQUEST, Some(msg.to_string())),
|
||||
Error::Crypto(msg) => (StatusCode::BAD_REQUEST, Some(msg.to_string())),
|
||||
Error::Git2(e) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Some(e.message().to_owned()),
|
||||
),
|
||||
Error::BadRequest(msg) => (StatusCode::BAD_REQUEST, Some(msg)),
|
||||
other => {
|
||||
tracing::error!("Error: {:?}", &self);
|
||||
tracing::error!("Error: {message}");
|
||||
|
||||
if cfg!(debug_assertions) {
|
||||
(
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ pub(crate) fn issue(id: IssueId, issue: Issue, aliases: &impl AliasStore) -> Val
|
|||
.comments()
|
||||
.map(|(id, comment)| Comment::new(id, comment, aliases))
|
||||
.collect::<Vec<_>>(),
|
||||
"tags": issue.tags().collect::<Vec<_>>(),
|
||||
"labels": issue.labels().collect::<Vec<_>>(),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -125,9 +125,9 @@ pub(crate) fn patch(
|
|||
"title": patch.title(),
|
||||
"state": patch.state(),
|
||||
"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<_>>(),
|
||||
"reviewers": patch.reviewers().collect::<Vec<_>>(),
|
||||
"assignees": patch.assignees().collect::<Vec<_>>(),
|
||||
"revisions": patch.revisions().map(|(id, rev)| {
|
||||
json!({
|
||||
"id": id,
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ use serde::{Deserialize, Serialize};
|
|||
use serde_json::json;
|
||||
use tower_http::set_header::SetResponseHeaderLayer;
|
||||
|
||||
use radicle::cob::{issue, patch, thread, ActorId, Tag};
|
||||
use radicle::identity::Id;
|
||||
use radicle::cob::{issue, patch, Label};
|
||||
use radicle::identity::{Did, Id};
|
||||
use radicle::node::routing::Store;
|
||||
use radicle::node::AliasStore;
|
||||
use radicle::node::NodeId;
|
||||
|
|
@ -481,8 +481,8 @@ async fn issues_handler(
|
|||
pub struct IssueCreate {
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub tags: Vec<Tag>,
|
||||
pub assignees: Vec<ActorId>,
|
||||
pub labels: Vec<Label>,
|
||||
pub assignees: Vec<Did>,
|
||||
}
|
||||
|
||||
/// Create a new issue.
|
||||
|
|
@ -505,7 +505,7 @@ async fn issue_create_handler(
|
|||
.create(
|
||||
issue.title,
|
||||
issue.description,
|
||||
&issue.tags,
|
||||
&issue.labels,
|
||||
&issue.assignees,
|
||||
&signer,
|
||||
)
|
||||
|
|
@ -526,6 +526,7 @@ async fn issue_update_handler(
|
|||
Json(action): Json<issue::Action>,
|
||||
) -> impl IntoResponse {
|
||||
api::auth::validate(&ctx, &token).await?;
|
||||
|
||||
let storage = &ctx.profile.storage;
|
||||
let signer = ctx.profile.signer().unwrap();
|
||||
let repo = storage.repository(project)?;
|
||||
|
|
@ -533,37 +534,34 @@ async fn issue_update_handler(
|
|||
let mut issue = issues.get_mut(&issue_id.into())?;
|
||||
|
||||
match action {
|
||||
issue::Action::Assign { add, remove } => {
|
||||
issue.assign(add, &signer)?;
|
||||
issue.unassign(remove, &signer)?;
|
||||
issue::Action::Assign { assignees } => {
|
||||
issue.assign(assignees, &signer)?;
|
||||
}
|
||||
issue::Action::Lifecycle { state } => {
|
||||
issue.lifecycle(state, &signer)?;
|
||||
}
|
||||
issue::Action::Tag { add, remove } => {
|
||||
issue.tag(add, remove, &signer)?;
|
||||
issue::Action::Label { labels } => {
|
||||
issue.label(labels, &signer)?;
|
||||
}
|
||||
issue::Action::Edit { title } => {
|
||||
issue.edit(title, &signer)?;
|
||||
}
|
||||
issue::Action::Thread { action } => match action {
|
||||
thread::Action::Comment { body, reply_to } => {
|
||||
if let Some(reply_to) = reply_to {
|
||||
issue.comment(body, reply_to, &signer)?;
|
||||
} else {
|
||||
issue.thread(body, &signer)?;
|
||||
}
|
||||
issue::Action::Comment { body, reply_to } => {
|
||||
if let Some(to) = reply_to {
|
||||
issue.comment(body, to, &signer)?;
|
||||
} else {
|
||||
return Err(Error::BadRequest("`replyTo` missing".to_owned()));
|
||||
}
|
||||
thread::Action::React { to, reaction, .. } => {
|
||||
issue.react(to, reaction, &signer)?;
|
||||
}
|
||||
thread::Action::Edit { .. } => {
|
||||
todo!();
|
||||
}
|
||||
thread::Action::Redact { .. } => {
|
||||
todo!();
|
||||
}
|
||||
},
|
||||
}
|
||||
issue::Action::CommentReact { id, reaction, .. } => {
|
||||
issue.react(id, reaction, &signer)?;
|
||||
}
|
||||
issue::Action::CommentEdit { .. } => {
|
||||
todo!();
|
||||
}
|
||||
issue::Action::CommentRedact { .. } => {
|
||||
todo!();
|
||||
}
|
||||
};
|
||||
|
||||
Ok::<_, Error>(Json(json!({ "success": true })))
|
||||
|
|
@ -591,7 +589,7 @@ pub struct PatchCreate {
|
|||
pub description: String,
|
||||
pub target: Oid,
|
||||
pub oid: Oid,
|
||||
pub tags: Vec<Tag>,
|
||||
pub labels: Vec<Label>,
|
||||
}
|
||||
|
||||
/// Create a new patch.
|
||||
|
|
@ -619,7 +617,7 @@ async fn patch_create_handler(
|
|||
patch::MergeTarget::default(),
|
||||
base_oid,
|
||||
patch.oid,
|
||||
&patch.tags,
|
||||
&patch.labels,
|
||||
&signer,
|
||||
)
|
||||
.map_err(Error::from)?;
|
||||
|
|
@ -650,35 +648,33 @@ async fn patch_update_handler(
|
|||
patch::Action::Edit { title, target } => {
|
||||
patch.edit(title, target, &signer)?;
|
||||
}
|
||||
patch::Action::EditRevision {
|
||||
patch::Action::RevisionEdit {
|
||||
revision,
|
||||
description,
|
||||
} => {
|
||||
patch.edit_revision(revision, description, &signer)?;
|
||||
}
|
||||
patch::Action::EditReview { review, summary } => {
|
||||
patch::Action::ReviewEdit { review, summary } => {
|
||||
patch.edit_review(review, summary, &signer)?;
|
||||
}
|
||||
patch::Action::EditCodeComment {
|
||||
patch::Action::ReviewCommentEdit {
|
||||
review,
|
||||
comment,
|
||||
body,
|
||||
} => {
|
||||
patch.edit_code_comment(review, comment, body, &signer)?;
|
||||
patch.edit_review_comment(review, comment, body, &signer)?;
|
||||
}
|
||||
patch::Action::Tag { add, remove } => {
|
||||
patch.tag(add, remove, &signer)?;
|
||||
patch::Action::Label { labels } => {
|
||||
patch.label(labels, &signer)?;
|
||||
}
|
||||
patch::Action::Revision {
|
||||
description,
|
||||
base,
|
||||
oid,
|
||||
..
|
||||
} => {
|
||||
patch.update(description, base, oid, &signer)?;
|
||||
}
|
||||
patch::Action::Redact { .. } => {
|
||||
todo!()
|
||||
}
|
||||
patch::Action::Lifecycle { state } => {
|
||||
patch.lifecycle(state, &signer)?;
|
||||
}
|
||||
|
|
@ -686,33 +682,24 @@ async fn patch_update_handler(
|
|||
revision,
|
||||
summary,
|
||||
verdict,
|
||||
labels,
|
||||
} => {
|
||||
patch.review(revision, verdict, summary, &signer)?;
|
||||
}
|
||||
patch::Action::CodeComment { .. } => {
|
||||
todo!()
|
||||
patch.review(revision, verdict, summary, labels, &signer)?;
|
||||
}
|
||||
patch::Action::Merge { revision, commit } => {
|
||||
patch.merge(revision, commit, &signer)?;
|
||||
}
|
||||
patch::Action::Thread { action, revision } => match action {
|
||||
thread::Action::Comment { body, reply_to } => {
|
||||
if let Some(reply_to) = reply_to {
|
||||
patch.comment(revision, body, Some(reply_to), &signer)?;
|
||||
} else {
|
||||
patch.thread(revision, body, &signer)?;
|
||||
}
|
||||
}
|
||||
thread::Action::Edit { .. } => {
|
||||
todo!();
|
||||
}
|
||||
thread::Action::Redact { .. } => {
|
||||
todo!();
|
||||
}
|
||||
thread::Action::React { .. } => {
|
||||
todo!();
|
||||
}
|
||||
},
|
||||
patch::Action::RevisionComment {
|
||||
revision,
|
||||
body,
|
||||
reply_to,
|
||||
..
|
||||
} => {
|
||||
patch.comment(revision, body, reply_to, &signer)?;
|
||||
}
|
||||
_ => {
|
||||
todo!();
|
||||
}
|
||||
};
|
||||
|
||||
Ok::<_, Error>(Json(json!({ "success": true })))
|
||||
|
|
@ -1635,7 +1622,7 @@ mod routes {
|
|||
"replyTo": null
|
||||
}
|
||||
],
|
||||
"tags": []
|
||||
"labels": []
|
||||
}
|
||||
])
|
||||
);
|
||||
|
|
@ -1643,7 +1630,7 @@ mod routes {
|
|||
|
||||
#[tokio::test]
|
||||
async fn test_projects_issues_create() {
|
||||
const CREATED_ISSUE_ID: &str = "b457364fbe2ef0eac69a835a087f60ee13ccb367";
|
||||
const CREATED_ISSUE_ID: &str = "c7cff5ab610408470406e023baa1ab087ce78adc";
|
||||
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let ctx = contributor(tmp.path());
|
||||
|
|
@ -1654,7 +1641,7 @@ mod routes {
|
|||
let body = serde_json::to_vec(&json!({
|
||||
"title": "Issue #2",
|
||||
"description": "Change 'hello world' to 'hello everyone'",
|
||||
"tags": ["bug"],
|
||||
"labels": ["bug"],
|
||||
"assignees": [],
|
||||
}))
|
||||
.unwrap();
|
||||
|
|
@ -1701,7 +1688,7 @@ mod routes {
|
|||
"timestamp": TIMESTAMP,
|
||||
"replyTo": null,
|
||||
}],
|
||||
"tags": [
|
||||
"labels": [
|
||||
"bug",
|
||||
],
|
||||
})
|
||||
|
|
@ -1717,11 +1704,9 @@ mod routes {
|
|||
create_session(ctx).await;
|
||||
|
||||
let body = serde_json::to_vec(&json!({
|
||||
"type": "thread",
|
||||
"action": {
|
||||
"type": "comment",
|
||||
"body": "This is first-level comment",
|
||||
}
|
||||
"type": "comment",
|
||||
"body": "This is first-level comment",
|
||||
"replyTo": CONTRIBUTOR_ISSUE_ID,
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
|
|
@ -1737,13 +1722,10 @@ mod routes {
|
|||
assert_eq!(response.json().await, json!({ "success": true }));
|
||||
|
||||
let body = serde_json::to_vec(&json!({
|
||||
"type": "thread",
|
||||
"action": {
|
||||
"type": "react",
|
||||
"to": "9685b141c2e939c3d60f8ca34f8c7bf01a609af1",
|
||||
"reaction": "🚀",
|
||||
"active": true,
|
||||
}
|
||||
"type": "comment.react",
|
||||
"id": "26cadcc7cb51ee9c56b6232023e9bf63b7b0df60",
|
||||
"reaction": "🚀",
|
||||
"active": true,
|
||||
}))
|
||||
.unwrap();
|
||||
patch(
|
||||
|
|
@ -1784,7 +1766,7 @@ mod routes {
|
|||
"replyTo": null,
|
||||
},
|
||||
{
|
||||
"id": "9685b141c2e939c3d60f8ca34f8c7bf01a609af1",
|
||||
"id": "26cadcc7cb51ee9c56b6232023e9bf63b7b0df60",
|
||||
"author": {
|
||||
"id": CONTRIBUTOR_DID,
|
||||
},
|
||||
|
|
@ -1796,10 +1778,10 @@ mod routes {
|
|||
],
|
||||
],
|
||||
"timestamp": TIMESTAMP,
|
||||
"replyTo": null,
|
||||
"replyTo": CONTRIBUTOR_ISSUE_ID,
|
||||
},
|
||||
],
|
||||
"tags": [],
|
||||
"labels": [],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
@ -1813,12 +1795,9 @@ mod routes {
|
|||
create_session(ctx).await;
|
||||
|
||||
let body = serde_json::to_vec(&json!({
|
||||
"type": "thread",
|
||||
"action": {
|
||||
"type": "comment",
|
||||
"body": "This is a reply to the first comment",
|
||||
"replyTo": ISSUE_DISCUSSION_ID,
|
||||
}
|
||||
"type": "comment",
|
||||
"body": "This is a reply to the first comment",
|
||||
"replyTo": ISSUE_DISCUSSION_ID,
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
|
|
@ -1874,7 +1853,7 @@ mod routes {
|
|||
"replyTo": ISSUE_DISCUSSION_ID,
|
||||
},
|
||||
],
|
||||
"tags": [],
|
||||
"labels": [],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
@ -1898,9 +1877,9 @@ mod routes {
|
|||
"title": "A new `hello world`",
|
||||
"state": { "status": "open" },
|
||||
"target": "delegates",
|
||||
"tags": [],
|
||||
"labels": [],
|
||||
"merges": [],
|
||||
"reviewers": [],
|
||||
"assignees": [],
|
||||
"revisions": [
|
||||
{
|
||||
"id": CONTRIBUTOR_PATCH_ID,
|
||||
|
|
@ -1940,9 +1919,9 @@ mod routes {
|
|||
"title": "A new `hello world`",
|
||||
"state": { "status": "open" },
|
||||
"target": "delegates",
|
||||
"tags": [],
|
||||
"labels": [],
|
||||
"merges": [],
|
||||
"reviewers": [],
|
||||
"assignees": [],
|
||||
"revisions": [
|
||||
{
|
||||
"id": CONTRIBUTOR_PATCH_ID,
|
||||
|
|
@ -1967,7 +1946,7 @@ mod routes {
|
|||
|
||||
#[tokio::test]
|
||||
async fn test_projects_create_patches() {
|
||||
const CREATED_PATCH_ID: &str = "768e76ae6611d9392f04122a5aa7a587b47b9e19";
|
||||
const CREATED_PATCH_ID: &str = "9cffd66099cceb0439a0f67c4aa99bde5e868eaa";
|
||||
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let ctx = contributor(tmp.path());
|
||||
|
|
@ -1980,7 +1959,7 @@ mod routes {
|
|||
"description": "Do some changes to README",
|
||||
"target": INITIAL_COMMIT,
|
||||
"oid": HEAD,
|
||||
"tags": [],
|
||||
"labels": [],
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
|
|
@ -2021,9 +2000,9 @@ mod routes {
|
|||
"title": "Update README",
|
||||
"state": { "status": "open" },
|
||||
"target": "delegates",
|
||||
"tags": [],
|
||||
"labels": [],
|
||||
"merges": [],
|
||||
"reviewers": [],
|
||||
"assignees": [],
|
||||
"revisions": [
|
||||
{
|
||||
"id": CREATED_PATCH_ID,
|
||||
|
|
@ -2053,9 +2032,8 @@ mod routes {
|
|||
let app = super::router(ctx.to_owned());
|
||||
create_session(ctx).await;
|
||||
let body = serde_json::to_vec(&json!({
|
||||
"type": "tag",
|
||||
"add": ["bug","design"],
|
||||
"remove": []
|
||||
"type": "label",
|
||||
"labels": ["bug","design"],
|
||||
}))
|
||||
.unwrap();
|
||||
let response = patch(
|
||||
|
|
@ -2084,12 +2062,12 @@ mod routes {
|
|||
"title": "A new `hello world`",
|
||||
"state": { "status": "open" },
|
||||
"target": "delegates",
|
||||
"tags": [
|
||||
"labels": [
|
||||
"bug",
|
||||
"design"
|
||||
],
|
||||
"merges": [],
|
||||
"reviewers": [],
|
||||
"assignees": [],
|
||||
"revisions": [
|
||||
{
|
||||
"id": CONTRIBUTOR_PATCH_ID,
|
||||
|
|
@ -2150,9 +2128,9 @@ mod routes {
|
|||
"title": "A new `hello world`",
|
||||
"state": { "status": "open" },
|
||||
"target": "delegates",
|
||||
"tags": [],
|
||||
"labels": [],
|
||||
"merges": [],
|
||||
"reviewers": [],
|
||||
"assignees": [],
|
||||
"revisions": [
|
||||
{
|
||||
"id": CONTRIBUTOR_PATCH_ID,
|
||||
|
|
@ -2170,7 +2148,7 @@ mod routes {
|
|||
"reviews": [],
|
||||
},
|
||||
{
|
||||
"id": "181e4219bc132e7716126a84200d4dbd628dd6be",
|
||||
"id": "b1f68feacb7040b089a77c1a0bff60a0411e6c1e",
|
||||
"author": {
|
||||
"id": CONTRIBUTOR_DID,
|
||||
},
|
||||
|
|
@ -2228,9 +2206,9 @@ mod routes {
|
|||
"title": "This is a updated title",
|
||||
"state": { "status": "open" },
|
||||
"target": "delegates",
|
||||
"tags": [],
|
||||
"labels": [],
|
||||
"merges": [],
|
||||
"reviewers": [],
|
||||
"assignees": [],
|
||||
"revisions": [
|
||||
{
|
||||
"id": CONTRIBUTOR_PATCH_ID,
|
||||
|
|
@ -2259,12 +2237,9 @@ mod routes {
|
|||
let app = super::router(ctx.to_owned());
|
||||
create_session(ctx).await;
|
||||
let thread_body = serde_json::to_vec(&json!({
|
||||
"type": "thread",
|
||||
"type": "revision.comment",
|
||||
"revision": CONTRIBUTOR_PATCH_ID,
|
||||
"action": {
|
||||
"type": "comment",
|
||||
"body": "This is a root level comment"
|
||||
}
|
||||
"body": "This is a root level comment"
|
||||
}))
|
||||
.unwrap();
|
||||
let response = patch(
|
||||
|
|
@ -2278,13 +2253,10 @@ mod routes {
|
|||
assert_eq!(response.status(), StatusCode::OK);
|
||||
|
||||
let reply_body = serde_json::to_vec(&json!({
|
||||
"type": "thread",
|
||||
"type": "revision.comment",
|
||||
"revision": CONTRIBUTOR_PATCH_ID,
|
||||
"action": {
|
||||
"type": "comment",
|
||||
"body": "This is a root level comment",
|
||||
"replyTo": CONTRIBUTOR_COMMENT_1,
|
||||
}
|
||||
"body": "This is a root level comment",
|
||||
"replyTo": CONTRIBUTOR_COMMENT_1,
|
||||
}))
|
||||
.unwrap();
|
||||
let response = patch(
|
||||
|
|
@ -2313,9 +2285,9 @@ mod routes {
|
|||
"title": "A new `hello world`",
|
||||
"state": { "status": "open" },
|
||||
"target": "delegates",
|
||||
"tags": [],
|
||||
"labels": [],
|
||||
"merges": [],
|
||||
"reviewers": [],
|
||||
"assignees": [],
|
||||
"revisions": [
|
||||
{
|
||||
"id": CONTRIBUTOR_PATCH_ID,
|
||||
|
|
@ -2397,9 +2369,9 @@ mod routes {
|
|||
"title": "A new `hello world`",
|
||||
"state": { "status": "open" },
|
||||
"target": "delegates",
|
||||
"tags": [],
|
||||
"labels": [],
|
||||
"merges": [],
|
||||
"reviewers": [],
|
||||
"assignees": [],
|
||||
"revisions": [
|
||||
{
|
||||
"id": CONTRIBUTOR_PATCH_ID,
|
||||
|
|
@ -2473,7 +2445,7 @@ mod routes {
|
|||
"commit": PARENT,
|
||||
},
|
||||
"target": "delegates",
|
||||
"tags": [],
|
||||
"labels": [],
|
||||
"merges": [{
|
||||
"author": {
|
||||
"id": CONTRIBUTOR_NID,
|
||||
|
|
@ -2482,7 +2454,7 @@ mod routes {
|
|||
"commit": PARENT,
|
||||
"timestamp": TIMESTAMP,
|
||||
}],
|
||||
"reviewers": [],
|
||||
"assignees": [],
|
||||
"revisions": [
|
||||
{
|
||||
"id": CONTRIBUTOR_PATCH_ID,
|
||||
|
|
|
|||
|
|
@ -33,18 +33,18 @@ pub const HEAD: &str = "e8c676b9e3b42308dc9d218b70faa5408f8e58ca";
|
|||
pub const PARENT: &str = "ee8d6a29304623a78ebfa5eeed5af674d0e58f83";
|
||||
pub const INITIAL_COMMIT: &str = "f604ce9fd5b7cc77b7609beda45ea8760bee78f7";
|
||||
pub const DID: &str = "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi";
|
||||
pub const ISSUE_ID: &str = "5ad77fa3f476beed9a26f49b2b3b844e61bef792";
|
||||
pub const ISSUE_DISCUSSION_ID: &str = "f1dff128a22e8183a23516dd9812e72e80914c92";
|
||||
pub const ISSUE_COMMENT_ID: &str = "845218041bf9eb8155bfa4aaa8f0c91ce18e5c13";
|
||||
pub const ISSUE_ID: &str = "0b0b8ca3b75e109971f87d92c1a6c930e87484c6";
|
||||
pub const ISSUE_DISCUSSION_ID: &str = "7466975f0bef37b459887824a9655f3e78262522";
|
||||
pub const ISSUE_COMMENT_ID: &str = "24ee306c508cd731a8427612dbdd826209096f99";
|
||||
pub const SESSION_ID: &str = "u9MGAkkfkMOv0uDDB2WeUHBT7HbsO2Dy";
|
||||
pub const TIMESTAMP: u64 = 1671125284;
|
||||
pub const CONTRIBUTOR_RID: &str = "rad:z4XaCmN3jLSeiMvW15YTDpNbDHFhG";
|
||||
pub const CONTRIBUTOR_DID: &str = "did:key:z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8";
|
||||
pub const CONTRIBUTOR_NID: &str = "z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8";
|
||||
pub const CONTRIBUTOR_ISSUE_ID: &str = "f1dff128a22e8183a23516dd9812e72e80914c92";
|
||||
pub const CONTRIBUTOR_PATCH_ID: &str = "044b577cc7551cd09d4b2f03566a553762180de4";
|
||||
pub const CONTRIBUTOR_COMMENT_1: &str = "92aab76516ae7f60a9b2952043ba578383de7d46";
|
||||
pub const CONTRIBUTOR_COMMENT_2: &str = "cb360eee0ec70563d5c4c3613fdc076648523248";
|
||||
pub const CONTRIBUTOR_ISSUE_ID: &str = "7466975f0bef37b459887824a9655f3e78262522";
|
||||
pub const CONTRIBUTOR_PATCH_ID: &str = "e651ae5869a2c1ac8ad4f6deae4cc835656ffa25";
|
||||
pub const CONTRIBUTOR_COMMENT_1: &str = "d0bb75b2c72ab8b5486d39f6cf5f41f104b63cb1";
|
||||
pub const CONTRIBUTOR_COMMENT_2: &str = "2a4ec5bcb1be09c1f2213f418c0159fff894b989";
|
||||
|
||||
/// Create a new 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()
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Response(axum::response::Response);
|
||||
|
||||
impl Response {
|
||||
|
|
|
|||
|
|
@ -7,13 +7,13 @@ use std::{assert_eq, io};
|
|||
|
||||
use thiserror::Error;
|
||||
|
||||
use radicle::cob::object::ParseObjectId;
|
||||
use radicle::cob::patch;
|
||||
use radicle::crypto::{PublicKey, Signer};
|
||||
use radicle::node;
|
||||
use radicle::node::{Handle, NodeId};
|
||||
use radicle::prelude::Id;
|
||||
use radicle::storage;
|
||||
use radicle::storage::git::cob::object::ParseObjectId;
|
||||
use radicle::storage::git::transport::local::Url;
|
||||
use radicle::storage::{ReadRepository, SignRepository as _, WriteRepository};
|
||||
use radicle::Profile;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ use radicle::Profile;
|
|||
|
||||
use radicle::cob::issue::{Issue, IssueId, State as IssueState};
|
||||
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::tui::text::{Span, Spans};
|
||||
|
|
@ -172,8 +172,8 @@ pub struct IssueItem {
|
|||
title: String,
|
||||
/// Issue author.
|
||||
author: AuthorItem,
|
||||
/// Issue tags.
|
||||
tags: Vec<Tag>,
|
||||
/// Issue labels.
|
||||
labels: Vec<Label>,
|
||||
/// Issue assignees.
|
||||
assignees: Vec<AuthorItem>,
|
||||
/// Time when issue was opened.
|
||||
|
|
@ -197,8 +197,8 @@ impl IssueItem {
|
|||
&self.author
|
||||
}
|
||||
|
||||
pub fn tags(&self) -> &Vec<Tag> {
|
||||
&self.tags
|
||||
pub fn labels(&self) -> &Vec<Label> {
|
||||
&self.labels
|
||||
}
|
||||
|
||||
pub fn assignees(&self) -> &Vec<AuthorItem> {
|
||||
|
|
@ -222,9 +222,10 @@ impl From<(&Profile, &Repository, IssueId, Issue)> for IssueItem {
|
|||
did: issue.author().id,
|
||||
is_you: *issue.author().id == *profile.did(),
|
||||
},
|
||||
tags: issue.tags().cloned().collect(),
|
||||
labels: issue.labels().cloned().collect(),
|
||||
assignees: issue
|
||||
.assigned()
|
||||
.cloned()
|
||||
.map(|did| AuthorItem {
|
||||
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))
|
||||
.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));
|
||||
|
||||
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 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());
|
||||
|
||||
if tags.peek().is_some() {
|
||||
if labels.peek().is_some() {
|
||||
output.push(',');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ impl IssueBrowser {
|
|||
common::label("ID"),
|
||||
common::label("Title"),
|
||||
common::label("Author"),
|
||||
common::label("Tags"),
|
||||
common::label("Labels"),
|
||||
common::label("Assignees"),
|
||||
common::label("Opened"),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -90,9 +90,9 @@ impl Details {
|
|||
common::label(item.title()).foreground(theme.colors.browser_list_title),
|
||||
);
|
||||
|
||||
let tags = Property::new(
|
||||
common::label("Tags").foreground(theme.colors.property_name_fg),
|
||||
common::label(&cob::format_tags(item.tags()))
|
||||
let labels = Property::new(
|
||||
common::label("Labels").foreground(theme.colors.property_name_fg),
|
||||
common::label(&cob::format_labels(item.labels()))
|
||||
.foreground(theme.colors.browser_list_tags),
|
||||
);
|
||||
|
||||
|
|
@ -113,12 +113,11 @@ impl Details {
|
|||
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(
|
||||
theme,
|
||||
vec![
|
||||
Widget::new(title),
|
||||
Widget::new(tags),
|
||||
Widget::new(labels),
|
||||
Widget::new(assignees),
|
||||
Widget::new(state),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
pub mod common;
|
||||
pub mod identity;
|
||||
pub mod issue;
|
||||
pub mod legacy;
|
||||
pub mod op;
|
||||
pub mod patch;
|
||||
pub mod store;
|
||||
|
|
@ -11,7 +12,7 @@ pub mod test;
|
|||
|
||||
pub use cob::{
|
||||
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 common::*;
|
||||
|
|
|
|||
|
|
@ -44,11 +44,30 @@ pub struct Reaction {
|
|||
}
|
||||
|
||||
impl Reaction {
|
||||
/// Create a new reaction from an emoji.
|
||||
pub fn new(emoji: char) -> Result<Self, ReactionError> {
|
||||
if emoji.is_whitespace() || emoji.is_ascii() || emoji.is_alphanumeric() {
|
||||
return Err(ReactionError::InvalidReaction);
|
||||
let val = emoji as u32;
|
||||
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 })
|
||||
} else {
|
||||
Err(ReactionError::InvalidReaction)
|
||||
}
|
||||
Ok(Self { emoji })
|
||||
}
|
||||
|
||||
/// Get the reaction emoji.
|
||||
pub fn emoji(&self) -> char {
|
||||
self.emoji
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -108,21 +127,21 @@ impl FromStr for Reaction {
|
|||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum TagError {
|
||||
pub enum LabelError {
|
||||
#[error("invalid tag name: `{0}`")]
|
||||
InvalidName(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct Tag(String);
|
||||
pub struct Label(String);
|
||||
|
||||
impl Tag {
|
||||
pub fn new(name: impl ToString) -> Result<Self, TagError> {
|
||||
impl Label {
|
||||
pub fn new(name: impl ToString) -> Result<Self, LabelError> {
|
||||
let name = name.to_string();
|
||||
|
||||
if name.chars().any(|c| c.is_whitespace()) || name.is_empty() {
|
||||
return Err(TagError::InvalidName(name));
|
||||
return Err(LabelError::InvalidName(name));
|
||||
}
|
||||
Ok(Self(name))
|
||||
}
|
||||
|
|
@ -132,22 +151,22 @@ impl Tag {
|
|||
}
|
||||
}
|
||||
|
||||
impl FromStr for Tag {
|
||||
type Err = TagError;
|
||||
impl FromStr for Label {
|
||||
type Err = LabelError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Self::new(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Tag {
|
||||
impl Display for Label {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Tag> for String {
|
||||
fn from(Tag(name): Tag) -> Self {
|
||||
impl From<Label> for String {
|
||||
fn from(Label(name): Label) -> Self {
|
||||
name
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ use crate::{
|
|||
cob::{
|
||||
self,
|
||||
store::{self, FromHistory as _, HistoryAction, Transaction},
|
||||
Timestamp,
|
||||
Reaction, Timestamp,
|
||||
},
|
||||
identity::{doc::DocError, Did, Identity, IdentityError},
|
||||
prelude::{Doc, ReadRepository},
|
||||
|
|
@ -37,33 +37,68 @@ pub type RevisionId = EntryId;
|
|||
|
||||
/// Proposal operation.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
#[serde(tag = "type")]
|
||||
pub enum Action {
|
||||
#[serde(rename = "accept")]
|
||||
Accept {
|
||||
revision: RevisionId,
|
||||
signature: Signature,
|
||||
},
|
||||
#[serde(rename = "close")]
|
||||
Close,
|
||||
#[serde(rename = "edit")]
|
||||
Edit {
|
||||
title: String,
|
||||
description: String,
|
||||
},
|
||||
Commit,
|
||||
Redact {
|
||||
#[serde(rename = "revision.redact")]
|
||||
RevisionRedact {
|
||||
revision: RevisionId,
|
||||
},
|
||||
#[serde(rename = "reject")]
|
||||
Reject {
|
||||
revision: RevisionId,
|
||||
},
|
||||
#[serde(rename = "revision")]
|
||||
Revision {
|
||||
// N.b. the `Oid` is a blob identifier and not a commit, so we
|
||||
// do not need to propagate it via HistoryAction.
|
||||
current: Oid,
|
||||
proposed: Doc<Verified>,
|
||||
},
|
||||
Thread {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(rename = "revision.comment")]
|
||||
RevisionComment {
|
||||
/// The revision to comment on.
|
||||
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,
|
||||
#[error(transparent)]
|
||||
Commit(#[from] CommitError),
|
||||
#[error("the revision {0:?} is redacted")]
|
||||
Redacted(EntryId),
|
||||
/// Error applying an op to the proposal thread.
|
||||
#[error("thread apply failed: {0}")]
|
||||
Thread(#[from] thread::Error),
|
||||
|
|
@ -104,9 +137,7 @@ pub enum CommitError {
|
|||
Closed(EntryId),
|
||||
#[error("the revision {0} is missing")]
|
||||
Missing(EntryId),
|
||||
#[error(
|
||||
"the identity hashes do match '{current} =/= {expected}' for the revision '{revision}'"
|
||||
)]
|
||||
#[error("the identity hashes do not match for revision {revision} ({current} =/= {expected})")]
|
||||
Mismatch {
|
||||
current: Oid,
|
||||
expected: Oid,
|
||||
|
|
@ -299,7 +330,7 @@ impl store::FromHistory for Proposal {
|
|||
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 author = Author::new(op.author);
|
||||
let timestamp = op.timestamp;
|
||||
|
|
@ -313,50 +344,86 @@ impl store::FromHistory for Proposal {
|
|||
Action::Accept {
|
||||
revision,
|
||||
signature,
|
||||
} => match self.revisions.get_mut(&revision) {
|
||||
Some(Some(revision)) => revision.accept(op.author, signature),
|
||||
Some(None) => return Err(ApplyError::Redacted(revision)),
|
||||
None => return Err(ApplyError::Missing(revision)),
|
||||
},
|
||||
} => {
|
||||
if let Some(revision) = lookup::revision(self, &revision)? {
|
||||
revision.accept(op.author, signature);
|
||||
}
|
||||
}
|
||||
Action::Close => self.state = State::Closed,
|
||||
Action::Edit { title, description } => {
|
||||
self.title = title;
|
||||
self.description = description;
|
||||
}
|
||||
Action::Commit => self.state = State::Committed,
|
||||
Action::Redact { revision } => {
|
||||
Action::RevisionRedact { revision } => {
|
||||
if let Some(revision) = self.revisions.get_mut(&revision) {
|
||||
*revision = None;
|
||||
} else {
|
||||
return Err(ApplyError::Missing(revision));
|
||||
}
|
||||
}
|
||||
Action::Reject { revision } => match self.revisions.get_mut(&revision) {
|
||||
Some(Some(revision)) => revision.reject(op.author),
|
||||
Some(None) => return Err(ApplyError::Redacted(revision)),
|
||||
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::Reject { revision } => {
|
||||
if let Some(revision) = lookup::revision(self, &revision)? {
|
||||
revision.reject(op.author);
|
||||
}
|
||||
}
|
||||
Action::Revision { current, proposed } => {
|
||||
debug_assert!(!self.revisions.contains_key(&id));
|
||||
|
||||
self.revisions.insert(
|
||||
id,
|
||||
Some(Revision::new(author.clone(), current, proposed, timestamp)),
|
||||
);
|
||||
}
|
||||
|
||||
Action::Thread { revision, action } => match self.revisions.get_mut(&revision) {
|
||||
Some(Some(revision)) => revision.discussion.apply(
|
||||
cob::Op::new(op.id, action, op.author, op.timestamp, op.identity),
|
||||
repo,
|
||||
)?,
|
||||
Some(None) => return Err(ApplyError::Redacted(revision)),
|
||||
None => return Err(ApplyError::Missing(revision)),
|
||||
},
|
||||
Action::RevisionComment {
|
||||
revision,
|
||||
body,
|
||||
reply_to,
|
||||
} => {
|
||||
if let Some(revision) = lookup::revision(self, &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)]
|
||||
pub enum Verdict {
|
||||
/// 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> {
|
||||
self.push(Action::Redact { revision })
|
||||
self.push(Action::RevisionRedact { revision })
|
||||
}
|
||||
|
||||
pub fn revision(&mut self, current: Oid, proposed: Doc<Verified>) -> Result<(), store::Error> {
|
||||
|
|
@ -500,12 +584,10 @@ impl store::Transaction<Proposal> {
|
|||
revision: RevisionId,
|
||||
body: S,
|
||||
) -> Result<(), store::Error> {
|
||||
self.push(Action::Thread {
|
||||
self.push(Action::RevisionComment {
|
||||
revision,
|
||||
action: thread::Action::Comment {
|
||||
body: body.to_string(),
|
||||
reply_to: None,
|
||||
},
|
||||
body: body.to_string(),
|
||||
reply_to: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -516,12 +598,10 @@ impl store::Transaction<Proposal> {
|
|||
body: S,
|
||||
reply_to: thread::CommentId,
|
||||
) -> Result<(), store::Error> {
|
||||
self.push(Action::Thread {
|
||||
self.push(Action::RevisionComment {
|
||||
revision,
|
||||
action: thread::Action::Comment {
|
||||
body: body.to_string(),
|
||||
reply_to: Some(reply_to),
|
||||
},
|
||||
body: body.to_string(),
|
||||
reply_to: Some(reply_to),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,12 +7,12 @@ use serde::{Deserialize, Serialize};
|
|||
use thiserror::Error;
|
||||
|
||||
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::{FromHistory as _, HistoryAction};
|
||||
use crate::cob::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::prelude::{Did, ReadRepository};
|
||||
use crate::storage::WriteRepository;
|
||||
|
|
@ -93,15 +93,15 @@ impl State {
|
|||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct Issue {
|
||||
/// Actors assigned to this issue.
|
||||
assignees: BTreeSet<ActorId>,
|
||||
pub(super) assignees: BTreeSet<Did>,
|
||||
/// Title of the issue.
|
||||
title: String,
|
||||
pub(super) title: String,
|
||||
/// Current state of the issue.
|
||||
state: State,
|
||||
/// Associated tags.
|
||||
tags: BTreeSet<Tag>,
|
||||
pub(super) state: State,
|
||||
/// Associated labels.
|
||||
pub(super) labels: BTreeSet<Label>,
|
||||
/// Discussion around this issue.
|
||||
thread: Thread,
|
||||
pub(super) thread: Thread,
|
||||
}
|
||||
|
||||
impl store::FromHistory for Issue {
|
||||
|
|
@ -122,16 +122,28 @@ impl store::FromHistory for Issue {
|
|||
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 {
|
||||
match action {
|
||||
Action::Assign { add, remove } => {
|
||||
for assignee in add {
|
||||
self.assignees.insert(assignee);
|
||||
}
|
||||
for assignee in remove {
|
||||
self.assignees.remove(&assignee);
|
||||
}
|
||||
Action::Assign { assignees } => {
|
||||
self.assignees = BTreeSet::from_iter(assignees);
|
||||
}
|
||||
Action::Edit { title } => {
|
||||
self.title = title;
|
||||
|
|
@ -139,20 +151,33 @@ impl store::FromHistory for Issue {
|
|||
Action::Lifecycle { state } => {
|
||||
self.state = state;
|
||||
}
|
||||
Action::Tag { add, remove } => {
|
||||
for tag in add {
|
||||
self.tags.insert(tag);
|
||||
}
|
||||
for tag in remove {
|
||||
self.tags.remove(&tag);
|
||||
}
|
||||
Action::Label { labels } => {
|
||||
self.labels = BTreeSet::from_iter(labels);
|
||||
}
|
||||
Action::Thread { action } => {
|
||||
self.thread.apply(
|
||||
cob::Op::new(op.id, action, op.author, op.timestamp, op.identity),
|
||||
repo,
|
||||
Action::Comment { body, reply_to } => {
|
||||
thread::comment(
|
||||
&mut self.thread,
|
||||
op.id,
|
||||
op.author,
|
||||
op.timestamp,
|
||||
body,
|
||||
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(())
|
||||
|
|
@ -160,8 +185,8 @@ impl store::FromHistory for Issue {
|
|||
}
|
||||
|
||||
impl Issue {
|
||||
pub fn assigned(&self) -> impl Iterator<Item = Did> + '_ {
|
||||
self.assignees.iter().map(Did::from)
|
||||
pub fn assigned(&self) -> impl Iterator<Item = &Did> + '_ {
|
||||
self.assignees.iter()
|
||||
}
|
||||
|
||||
pub fn title(&self) -> &str {
|
||||
|
|
@ -172,8 +197,8 @@ impl Issue {
|
|||
&self.state
|
||||
}
|
||||
|
||||
pub fn tags(&self) -> impl Iterator<Item = &Tag> {
|
||||
self.tags.iter()
|
||||
pub fn labels(&self) -> impl Iterator<Item = &Label> {
|
||||
self.labels.iter()
|
||||
}
|
||||
|
||||
pub fn timestamp(&self) -> Timestamp {
|
||||
|
|
@ -219,24 +244,18 @@ impl Deref for Issue {
|
|||
}
|
||||
|
||||
impl store::Transaction<Issue> {
|
||||
pub fn assign(
|
||||
&mut self,
|
||||
add: impl IntoIterator<Item = ActorId>,
|
||||
remove: impl IntoIterator<Item = ActorId>,
|
||||
) -> Result<(), store::Error> {
|
||||
let add = add.into_iter().collect::<Vec<_>>();
|
||||
let remove = remove.into_iter().collect::<Vec<_>>();
|
||||
|
||||
self.push(Action::Assign { add, remove })
|
||||
/// Assign DIDs to the issue.
|
||||
pub fn assign(&mut self, assignees: impl IntoIterator<Item = Did>) -> Result<(), store::Error> {
|
||||
self.push(Action::Assign {
|
||||
assignees: assignees.into_iter().collect(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Edit an issue comment.
|
||||
pub fn edit_comment(&mut self, id: CommentId, body: impl ToString) -> Result<(), store::Error> {
|
||||
self.push(Action::Thread {
|
||||
action: thread::Action::Edit {
|
||||
id,
|
||||
body: body.to_string(),
|
||||
},
|
||||
self.push(Action::CommentEdit {
|
||||
id,
|
||||
body: body.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -252,46 +271,41 @@ impl store::Transaction<Issue> {
|
|||
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.
|
||||
pub fn comment<S: ToString>(
|
||||
&mut self,
|
||||
body: S,
|
||||
reply_to: CommentId,
|
||||
) -> Result<(), store::Error> {
|
||||
self.push(Action::from(thread::Action::Comment {
|
||||
self.push(Action::Comment {
|
||||
body: body.to_string(),
|
||||
reply_to: Some(reply_to),
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
/// Tag an issue.
|
||||
pub fn tag(
|
||||
&mut self,
|
||||
add: impl IntoIterator<Item = Tag>,
|
||||
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 })
|
||||
/// Label an issue.
|
||||
pub fn label(&mut self, labels: impl IntoIterator<Item = Label>) -> Result<(), store::Error> {
|
||||
self.push(Action::Label {
|
||||
labels: labels.into_iter().collect(),
|
||||
})
|
||||
}
|
||||
|
||||
/// React to an issue comment.
|
||||
pub fn react(&mut self, to: CommentId, reaction: Reaction) -> Result<(), store::Error> {
|
||||
self.push(Action::Thread {
|
||||
action: thread::Action::React {
|
||||
to,
|
||||
reaction,
|
||||
active: true,
|
||||
},
|
||||
pub fn react(&mut self, id: CommentId, reaction: Reaction) -> Result<(), store::Error> {
|
||||
self.push(Action::CommentReact {
|
||||
id,
|
||||
reaction,
|
||||
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>,
|
||||
}
|
||||
|
||||
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>
|
||||
where
|
||||
R: WriteRepository + cob::Store,
|
||||
|
|
@ -324,10 +347,10 @@ where
|
|||
/// Assign one or more actors to an issue.
|
||||
pub fn assign<G: Signer>(
|
||||
&mut self,
|
||||
assignees: impl IntoIterator<Item = ActorId>,
|
||||
assignees: impl IntoIterator<Item = Did>,
|
||||
signer: &G,
|
||||
) -> Result<EntryId, Error> {
|
||||
self.transaction("Assign", signer, |tx| tx.assign(assignees, []))
|
||||
self.transaction("Assign", signer, |tx| tx.assign(assignees))
|
||||
}
|
||||
|
||||
/// Set the issue title.
|
||||
|
|
@ -355,15 +378,6 @@ where
|
|||
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.
|
||||
pub fn comment<G: Signer, S: ToString>(
|
||||
&mut self,
|
||||
|
|
@ -378,14 +392,13 @@ where
|
|||
self.transaction("Comment", signer, |tx| tx.comment(body, reply_to))
|
||||
}
|
||||
|
||||
/// Tag an issue.
|
||||
pub fn tag<G: Signer>(
|
||||
/// Label an issue.
|
||||
pub fn label<G: Signer>(
|
||||
&mut self,
|
||||
add: impl IntoIterator<Item = Tag>,
|
||||
remove: impl IntoIterator<Item = Tag>,
|
||||
labels: impl IntoIterator<Item = Label>,
|
||||
signer: &G,
|
||||
) -> 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.
|
||||
|
|
@ -398,16 +411,6 @@ where
|
|||
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>(
|
||||
&mut self,
|
||||
message: &str,
|
||||
|
|
@ -491,15 +494,15 @@ where
|
|||
&'g mut self,
|
||||
title: impl ToString,
|
||||
description: impl ToString,
|
||||
tags: &[Tag],
|
||||
assignees: &[ActorId],
|
||||
labels: &[Label],
|
||||
assignees: &[Did],
|
||||
signer: &G,
|
||||
) -> Result<IssueMut<'a, 'g, R>, Error> {
|
||||
let (id, issue) = Transaction::initial("Create issue", &mut self.raw, signer, |tx| {
|
||||
tx.thread(description)?;
|
||||
tx.assign(assignees.to_owned(), [])?;
|
||||
tx.assign(assignees.to_owned())?;
|
||||
tx.edit(title)?;
|
||||
tx.tag(tags.to_owned(), [])?;
|
||||
tx.label(labels.to_owned())?;
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
|
|
@ -533,43 +536,64 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
/// Issue operation.
|
||||
/// Issue action.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum Action {
|
||||
Assign {
|
||||
add: Vec<ActorId>,
|
||||
remove: Vec<ActorId>,
|
||||
/// Assign issue to an actor.
|
||||
#[serde(rename = "assign")]
|
||||
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,
|
||||
},
|
||||
Lifecycle {
|
||||
state: State,
|
||||
},
|
||||
Tag {
|
||||
add: Vec<Tag>,
|
||||
remove: Vec<Tag>,
|
||||
},
|
||||
Thread {
|
||||
action: thread::Action,
|
||||
|
||||
/// Edit a comment.
|
||||
#[serde(rename = "comment.edit")]
|
||||
CommentEdit { id: CommentId, body: String },
|
||||
|
||||
/// Redact a change. Not all changes can be redacted.
|
||||
#[serde(rename = "comment.redact")]
|
||||
CommentRedact { id: CommentId },
|
||||
|
||||
/// React to a comment.
|
||||
#[serde(rename = "comment.react")]
|
||||
CommentReact {
|
||||
id: CommentId,
|
||||
reaction: Reaction,
|
||||
active: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl HistoryAction for Action {}
|
||||
|
||||
impl From<thread::Action> for Action {
|
||||
fn from(action: thread::Action) -> Self {
|
||||
Self::Thread { action }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
use crate::cob::Reaction;
|
||||
use crate::cob::{ActorId, Reaction};
|
||||
use crate::test;
|
||||
use crate::test::arbitrary;
|
||||
|
||||
|
|
@ -660,8 +684,8 @@ mod test {
|
|||
let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
|
||||
let mut issues = Issues::open(&*repo).unwrap();
|
||||
|
||||
let assignee: ActorId = arbitrary::gen(1);
|
||||
let assignee_two: ActorId = arbitrary::gen(1);
|
||||
let assignee = Did::from(arbitrary::gen::<ActorId>(1));
|
||||
let assignee_two = Did::from(arbitrary::gen::<ActorId>(1));
|
||||
let issue = issues
|
||||
.create(
|
||||
"My first issue",
|
||||
|
|
@ -674,21 +698,23 @@ mod test {
|
|||
|
||||
let id = issue.id;
|
||||
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!(assignees.contains(&Did::from(assignee)));
|
||||
assert!(assignees.contains(&assignee));
|
||||
|
||||
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 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!(assignees.contains(&Did::from(assignee)));
|
||||
assert!(assignees.contains(&Did::from(assignee_two)));
|
||||
assert!(assignees.contains(&assignee));
|
||||
assert!(assignees.contains(&assignee_two));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -696,8 +722,8 @@ mod test {
|
|||
let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
|
||||
let mut issues = Issues::open(&*repo).unwrap();
|
||||
|
||||
let assignee: ActorId = arbitrary::gen(1);
|
||||
let assignee_two: ActorId = arbitrary::gen(1);
|
||||
let assignee = Did::from(arbitrary::gen::<ActorId>(1));
|
||||
let assignee_two = Did::from(arbitrary::gen::<ActorId>(1));
|
||||
let mut issue = issues
|
||||
.create(
|
||||
"My first issue",
|
||||
|
|
@ -710,14 +736,17 @@ mod test {
|
|||
|
||||
issue.assign([assignee_two], &node.signer).unwrap();
|
||||
issue.assign([assignee_two], &node.signer).unwrap();
|
||||
issue.reload().unwrap();
|
||||
|
||||
let id = issue.id;
|
||||
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!(assignees.contains(&Did::from(assignee)));
|
||||
assert!(assignees.contains(&Did::from(assignee_two)));
|
||||
assert_eq!(1, assignees.len());
|
||||
assert!(assignees.contains(&assignee_two));
|
||||
|
||||
issue.assign([], &node.signer).unwrap();
|
||||
issue.reload().unwrap();
|
||||
|
||||
assert_eq!(0, issue.assigned().count());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -777,8 +806,8 @@ mod test {
|
|||
let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
|
||||
let mut issues = Issues::open(&*repo).unwrap();
|
||||
|
||||
let assignee: ActorId = arbitrary::gen(1);
|
||||
let assignee_two: ActorId = arbitrary::gen(1);
|
||||
let assignee = Did::from(arbitrary::gen::<ActorId>(1));
|
||||
let assignee_two = Did::from(arbitrary::gen::<ActorId>(1));
|
||||
let mut issue = issues
|
||||
.create(
|
||||
"My first issue",
|
||||
|
|
@ -788,15 +817,15 @@ mod test {
|
|||
&node.signer,
|
||||
)
|
||||
.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 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!(assignees.contains(&Did::from(assignee_two)));
|
||||
assert!(assignees.contains(&assignee_two));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -895,32 +924,39 @@ mod test {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_issue_tag() {
|
||||
fn test_issue_label() {
|
||||
let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
|
||||
let mut issues = Issues::open(&*repo).unwrap();
|
||||
let bug_tag = Tag::new("bug").unwrap();
|
||||
let ux_tag = Tag::new("ux").unwrap();
|
||||
let wontfix_tag = Tag::new("wontfix").unwrap();
|
||||
let bug_label = Label::new("bug").unwrap();
|
||||
let ux_label = Label::new("ux").unwrap();
|
||||
let wontfix_label = Label::new("wontfix").unwrap();
|
||||
let mut issue = issues
|
||||
.create(
|
||||
"My first issue",
|
||||
"Blah blah blah.",
|
||||
&[ux_tag.clone()],
|
||||
&[ux_label.clone()],
|
||||
&[],
|
||||
&node.signer,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
issue.tag([bug_tag.clone()], [], &node.signer).unwrap();
|
||||
issue.tag([wontfix_tag.clone()], [], &node.signer).unwrap();
|
||||
issue
|
||||
.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 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!(tags.contains(&bug_tag));
|
||||
assert!(tags.contains(&wontfix_tag));
|
||||
assert!(labels.contains(&ux_label));
|
||||
assert!(labels.contains(&bug_label));
|
||||
assert!(labels.contains(&wontfix_label));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
pub mod issue;
|
||||
pub mod patch;
|
||||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
use nonempty::NonEmpty;
|
||||
use radicle_cob::Manifest;
|
||||
use thiserror::Error;
|
||||
|
||||
use radicle_cob::history::{Entry, EntryId};
|
||||
|
|
@ -35,6 +36,8 @@ pub struct Op<A> {
|
|||
pub timestamp: Timestamp,
|
||||
/// Head of identity document committed to by this operation.
|
||||
pub identity: git::Oid,
|
||||
/// Object manifest.
|
||||
pub manifest: Manifest,
|
||||
}
|
||||
|
||||
impl<A: Eq> PartialOrd for Op<A> {
|
||||
|
|
@ -56,6 +59,7 @@ impl<A> Op<A> {
|
|||
author: ActorId,
|
||||
timestamp: impl Into<Timestamp>,
|
||||
identity: git::Oid,
|
||||
manifest: Manifest,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
|
|
@ -63,6 +67,7 @@ impl<A> Op<A> {
|
|||
author,
|
||||
timestamp: timestamp.into(),
|
||||
identity,
|
||||
manifest,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -85,6 +90,7 @@ where
|
|||
.iter()
|
||||
.map(|blob| serde_json::from_slice(blob.as_slice()))
|
||||
.collect::<Result<_, _>>()?;
|
||||
let manifest = entry.manifest().clone();
|
||||
|
||||
// SAFETY: Entry is guaranteed to have at least one operation.
|
||||
#[allow(clippy::unwrap_used)]
|
||||
|
|
@ -95,6 +101,7 @@ where
|
|||
author: *entry.actor(),
|
||||
timestamp: Timestamp::from_secs(entry.timestamp()),
|
||||
identity,
|
||||
manifest,
|
||||
};
|
||||
|
||||
Ok(op)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -10,16 +10,13 @@ use serde::{Deserialize, Serialize};
|
|||
|
||||
use crate::cob::common::Timestamp;
|
||||
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::prelude::*;
|
||||
use crate::storage::git as storage;
|
||||
use crate::storage::SignRepository;
|
||||
use crate::{cob, identity};
|
||||
|
||||
/// History type for standard radicle COBs.
|
||||
pub const HISTORY_TYPE: &str = "radicle";
|
||||
|
||||
pub trait HistoryAction: std::fmt::Debug {
|
||||
/// Parent objects this action depends on. For example, patch revisions
|
||||
/// have the commit objects as their parent.
|
||||
|
|
@ -51,28 +48,7 @@ pub trait FromHistory: Sized + Default + PartialEq {
|
|||
|
||||
/// Create an object from a history.
|
||||
fn from_history<R: ReadRepository>(history: &History, repo: &R) -> Result<Self, Self::Error> {
|
||||
let obj = history.traverse(Self::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}", 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)
|
||||
self::from_history::<R, Self>(history, repo)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
|
|
@ -104,8 +107,6 @@ pub enum Error {
|
|||
Identity(#[from] identity::IdentityError),
|
||||
#[error(transparent)]
|
||||
Serialize(#[from] serde_json::Error),
|
||||
#[error("unexpected history type '{0}'")]
|
||||
HistoryType(String),
|
||||
#[error("object `{1}` of type `{0}` was not found")]
|
||||
NotFound(TypeName, ObjectId),
|
||||
#[error("apply: {0}")]
|
||||
|
|
@ -177,8 +178,7 @@ where
|
|||
signer.public_key(),
|
||||
Update {
|
||||
object_id,
|
||||
history_type: HISTORY_TYPE.to_owned(),
|
||||
typename: T::type_name().clone(),
|
||||
type_name: T::type_name().clone(),
|
||||
message: message.to_owned(),
|
||||
changes,
|
||||
},
|
||||
|
|
@ -206,8 +206,8 @@ where
|
|||
parents,
|
||||
signer.public_key(),
|
||||
Create {
|
||||
history_type: HISTORY_TYPE.to_owned(),
|
||||
typename: T::type_name().clone(),
|
||||
type_name: T::type_name().clone(),
|
||||
version: Version::default(),
|
||||
message: message.to_owned(),
|
||||
contents,
|
||||
},
|
||||
|
|
@ -252,9 +252,6 @@ where
|
|||
let cob = cob::get(self.repo, T::type_name(), id)?;
|
||||
|
||||
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)?;
|
||||
|
||||
Ok(Some(obj))
|
||||
|
|
@ -357,12 +354,14 @@ impl<T: FromHistory> Transaction<T> {
|
|||
let author = self.actor;
|
||||
let timestamp = Timestamp::from_secs(object.history().timestamp());
|
||||
let identity = store.identity;
|
||||
let manifest = object.manifest().clone();
|
||||
let op = cob::Op {
|
||||
id,
|
||||
actions,
|
||||
author,
|
||||
timestamp,
|
||||
identity,
|
||||
manifest,
|
||||
};
|
||||
|
||||
Ok((op, id))
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use crate::cob::op::Op;
|
|||
use crate::cob::patch;
|
||||
use crate::cob::patch::Patch;
|
||||
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::git;
|
||||
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> {
|
||||
let resource = arbitrary::oid();
|
||||
let (data, root) = encoded::<T, _>(action, time, [], signer);
|
||||
let manifest = Manifest::new(T::type_name().clone(), Version::default());
|
||||
|
||||
Self {
|
||||
history: History::new_from_root(
|
||||
|
|
@ -67,6 +68,7 @@ where
|
|||
resource,
|
||||
NonEmpty::new(data),
|
||||
time.as_secs(),
|
||||
manifest,
|
||||
),
|
||||
time,
|
||||
resource,
|
||||
|
|
@ -74,7 +76,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
pub fn root(&self) -> EntryId {
|
||||
pub fn root(&self) -> &Entry {
|
||||
self.history.root()
|
||||
}
|
||||
|
||||
|
|
@ -86,6 +88,7 @@ where
|
|||
let timestamp = self.time;
|
||||
let tips = self.tips();
|
||||
let (data, oid) = encoded::<T, _>(action, timestamp, tips, signer);
|
||||
let manifest = Manifest::new(T::type_name().clone(), Version::default());
|
||||
|
||||
self.history.extend(
|
||||
oid,
|
||||
|
|
@ -93,6 +96,7 @@ where
|
|||
self.resource,
|
||||
NonEmpty::new(data),
|
||||
timestamp.as_secs(),
|
||||
manifest,
|
||||
);
|
||||
oid
|
||||
}
|
||||
|
|
@ -137,12 +141,15 @@ impl<G> Actor<G> {
|
|||
|
||||
impl<G: Signer> Actor<G> {
|
||||
/// Create a new operation.
|
||||
pub fn op_with<A: Clone + Serialize>(
|
||||
pub fn op_with<T: FromHistory>(
|
||||
&mut self,
|
||||
action: A,
|
||||
action: T::Action,
|
||||
identity: Oid,
|
||||
timestamp: Timestamp,
|
||||
) -> Op<A> {
|
||||
) -> Op<T::Action>
|
||||
where
|
||||
T::Action: Clone + Serialize,
|
||||
{
|
||||
let data = encoding::encode(serde_json::json!({
|
||||
"action": action,
|
||||
"nonce": fastrand::u64(..),
|
||||
|
|
@ -152,6 +159,7 @@ impl<G: Signer> Actor<G> {
|
|||
let id = oid.into();
|
||||
let author = *self.signer.public_key();
|
||||
let actions = NonEmpty::new(action);
|
||||
let manifest = Manifest::new(T::type_name().clone(), Version::default());
|
||||
|
||||
Op {
|
||||
id,
|
||||
|
|
@ -159,15 +167,19 @@ impl<G: Signer> Actor<G> {
|
|||
author,
|
||||
timestamp,
|
||||
identity,
|
||||
manifest,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 timestamp = Timestamp::now();
|
||||
|
||||
self.op_with(action, identity, timestamp)
|
||||
self.op_with::<T>(action, identity, timestamp)
|
||||
}
|
||||
|
||||
/// Get the actor's DID.
|
||||
|
|
@ -188,12 +200,13 @@ impl<G: Signer> Actor<G> {
|
|||
) -> Result<Patch, patch::Error> {
|
||||
Patch::from_ops(
|
||||
[
|
||||
self.op(patch::Action::Revision {
|
||||
self.op::<Patch>(patch::Action::Revision {
|
||||
description: description.to_string(),
|
||||
base,
|
||||
oid,
|
||||
resolves: Default::default(),
|
||||
}),
|
||||
self.op(patch::Action::Edit {
|
||||
self.op::<Patch>(patch::Action::Edit {
|
||||
title: title.to_string(),
|
||||
target: patch::MergeTarget::default(),
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use std::collections::{BTreeMap, BTreeSet};
|
|||
use std::str::FromStr;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::{ser::SerializeStruct, Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::cob;
|
||||
|
|
@ -53,7 +53,7 @@ pub struct Edit {
|
|||
|
||||
/// A comment on a discussion thread.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Comment {
|
||||
pub struct Comment<T = ()> {
|
||||
/// Comment author.
|
||||
author: ActorId,
|
||||
/// The comment body.
|
||||
|
|
@ -63,14 +63,36 @@ pub struct Comment {
|
|||
/// Comment this is a reply to.
|
||||
/// Should always be set, except for the root comment.
|
||||
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.
|
||||
pub fn new(
|
||||
author: ActorId,
|
||||
body: String,
|
||||
reply_to: Option<CommentId>,
|
||||
location: Option<L>,
|
||||
timestamp: Timestamp,
|
||||
) -> Self {
|
||||
let edit = Edit { body, timestamp };
|
||||
|
|
@ -80,6 +102,7 @@ impl Comment {
|
|||
reactions: BTreeSet::default(),
|
||||
edits: vec![edit],
|
||||
reply_to,
|
||||
location,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -124,9 +147,14 @@ impl Comment {
|
|||
pub fn reactions(&self) -> impl Iterator<Item = (&ActorId, &Reaction)> {
|
||||
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> {
|
||||
if self == other {
|
||||
Some(Ordering::Equal)
|
||||
|
|
@ -171,16 +199,25 @@ impl From<Action> for nonempty::NonEmpty<Action> {
|
|||
}
|
||||
|
||||
/// A discussion thread.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub struct Thread {
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Thread<T = Comment<()>> {
|
||||
/// The comments under the thread.
|
||||
comments: BTreeMap<CommentId, Option<Comment>>,
|
||||
comments: BTreeMap<CommentId, Option<T>>,
|
||||
/// Comment timeline.
|
||||
timeline: Vec<CommentId>,
|
||||
}
|
||||
|
||||
impl Thread {
|
||||
pub fn new(id: CommentId, comment: Comment) -> Self {
|
||||
impl<T> Default for Thread<T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
comments: BTreeMap::default(),
|
||||
timeline: Vec::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Thread<T> {
|
||||
pub fn new(id: CommentId, comment: T) -> Self {
|
||||
Self {
|
||||
comments: BTreeMap::from_iter([(id, Some(comment))]),
|
||||
timeline: Vec::default(),
|
||||
|
|
@ -199,26 +236,37 @@ impl Thread {
|
|||
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())
|
||||
}
|
||||
|
||||
pub fn root(&self) -> (&CommentId, &Comment) {
|
||||
pub fn root(&self) -> (&CommentId, &T) {
|
||||
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()
|
||||
}
|
||||
|
||||
pub fn last(&self) -> Option<(&CommentId, &Comment)> {
|
||||
pub fn last(&self) -> Option<(&CommentId, &T)> {
|
||||
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>(
|
||||
&'a self,
|
||||
to: &'a CommentId,
|
||||
) -> impl Iterator<Item = (&CommentId, &Comment)> {
|
||||
) -> impl Iterator<Item = (&CommentId, &Comment<L>)> {
|
||||
self.comments().filter_map(move |(id, c)| {
|
||||
if let Some(reply_to) = c.reply_to {
|
||||
if &reply_to == to {
|
||||
|
|
@ -228,15 +276,6 @@ impl Thread {
|
|||
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 {
|
||||
|
|
@ -259,62 +298,23 @@ impl cob::store::FromHistory for Thread {
|
|||
let author = op.author;
|
||||
let timestamp = op.timestamp;
|
||||
|
||||
debug_assert!(!self.timeline.contains(&op.id));
|
||||
|
||||
self.timeline.push(op.id);
|
||||
|
||||
for action in op.into_iter() {
|
||||
for action in op.actions {
|
||||
match action {
|
||||
Action::Comment { body, reply_to } => {
|
||||
if body.is_empty() {
|
||||
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)));
|
||||
comment(self, id, author, timestamp, body, reply_to, None)?;
|
||||
}
|
||||
Action::Edit { id, body } => {
|
||||
if body.is_empty() {
|
||||
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));
|
||||
}
|
||||
edit(self, op.id, id, timestamp, body)?;
|
||||
}
|
||||
Action::Redact { id } => {
|
||||
if let Some(comment) = self.comments.get_mut(&id) {
|
||||
*comment = None;
|
||||
} else {
|
||||
return Err(Error::Missing(id));
|
||||
}
|
||||
redact(self, op.id, id)?;
|
||||
}
|
||||
Action::React {
|
||||
to,
|
||||
reaction,
|
||||
active,
|
||||
} => {
|
||||
let key = (author, reaction);
|
||||
if let Some(comment) = self.comments.get_mut(&to) {
|
||||
if let Some(comment) = comment {
|
||||
if active {
|
||||
comment.reactions.insert(key);
|
||||
} else {
|
||||
comment.reactions.remove(&key);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(Error::Missing(id));
|
||||
}
|
||||
react(self, op.id, author, to, reaction, active)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -322,6 +322,96 @@ impl cob::store::FromHistory for Thread {
|
|||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
comment.reactions.insert(key);
|
||||
} else {
|
||||
comment.reactions.remove(&key);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
|
@ -361,7 +451,7 @@ mod tests {
|
|||
|
||||
/// Create a new comment.
|
||||
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),
|
||||
reply_to,
|
||||
})
|
||||
|
|
@ -369,12 +459,12 @@ mod tests {
|
|||
|
||||
/// Create a new redaction.
|
||||
pub fn redact(&mut self, id: CommentId) -> Op<Action> {
|
||||
self.op(Action::Redact { id })
|
||||
self.op::<Thread>(Action::Redact { id })
|
||||
}
|
||||
|
||||
/// Edit a comment.
|
||||
pub fn edit(&mut self, id: CommentId, body: &str) -> Op<Action> {
|
||||
self.op(Action::Edit {
|
||||
self.op::<Thread>(Action::Edit {
|
||||
id,
|
||||
body: body.to_owned(),
|
||||
})
|
||||
|
|
@ -456,13 +546,13 @@ mod tests {
|
|||
time,
|
||||
&alice,
|
||||
);
|
||||
a.comment("Alice comment", Some(a.root()), &alice);
|
||||
a.comment("Alice comment", Some(*a.root().id()), &alice);
|
||||
|
||||
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 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!(b.as_ref().len(), 3);
|
||||
|
|
@ -561,14 +651,14 @@ mod tests {
|
|||
|
||||
let e1 = h1.commit(
|
||||
&Action::Edit {
|
||||
id: h0.root(),
|
||||
id: *h0.root().id(),
|
||||
body: String::from("Bye World."),
|
||||
},
|
||||
&alice,
|
||||
);
|
||||
let e2 = h2.commit(
|
||||
&Action::Edit {
|
||||
id: h0.root(),
|
||||
id: *h0.root().id(),
|
||||
body: String::from("Hi World."),
|
||||
},
|
||||
&bob,
|
||||
|
|
@ -588,7 +678,7 @@ mod tests {
|
|||
|
||||
let _e3 = h1.commit(
|
||||
&Action::Edit {
|
||||
id: h0.root(),
|
||||
id: *h0.root().id(),
|
||||
body: String::from("Hoho World!"),
|
||||
},
|
||||
&alice,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ pub enum DidError {
|
|||
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")]
|
||||
pub struct Did(crypto::PublicKey);
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use radicle_cob as cob;
|
|||
use radicle_cob::change;
|
||||
use storage::SignRepository;
|
||||
|
||||
use crate::git::*;
|
||||
use crate::storage;
|
||||
use crate::storage::Error;
|
||||
use crate::storage::{
|
||||
|
|
@ -17,9 +18,6 @@ use crate::{
|
|||
identity::{doc::DocError, IdentityError},
|
||||
};
|
||||
|
||||
pub use crate::git::*;
|
||||
pub use cob::*;
|
||||
|
||||
use super::{RemoteId, Repository};
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ labels="$(echo "$response" | jq -r '.labels | .[].name')"
|
|||
|
||||
tags=()
|
||||
for label in $labels; do
|
||||
tags+=("--tag" "$label")
|
||||
tags+=("--label" "$label")
|
||||
done
|
||||
|
||||
rad issue open --title "$title" "${tags[@]}" --description "$body" --no-announce
|
||||
|
|
|
|||
Loading…
Reference in New Issue