cob: Introduce `xyz.radicle.id` COB
Introduces a new COB type to store repository identity documents. The reason for this change is to: 1. Simplify the code, as the identity document logic resembled a COB, yet it had custom logic. This allows existing COB code to be used for identities. 2. Make identity document update logic more flexible, since COB actions can be added in the future. 3. Re-purpose existing tools around COBs to work on identities, eg. `rad cob`. 4. Unify the concept of an identity change proposal, with regular identity changes. This means we can remove the `id.proposal` COB in favor of using the `id` COB itself. Notes ----- * Each repository has one Identity COB. * The `Proposal` COB has been repurposed into the `Identity` COB. * Identity documents are stored as *embeds* inside the Identity COB actions. * The action that contains new document versions is called `revision`, just like the Patches COB. * The namespaced `rad/id` ref is a symbolic reference to that Identity COB. * The canonical `rad/id` ref is a direct reference to the commit in the Identity COB that contains the latest *accepted* revision of the document. * All commands for managing identities have been folded into `rad id`. Hence `rad delegate` and `rad edit` are removed. * The concept of "rebasing" an identity document is gone. * The `rad id` output has been updated to match the style of other commands. * When a revision has enough signatures, it is automatically adopted as the current identity, there is no longer the need for a "commit" action. * When an identity revision is proposed, and the current identity has a threshold of `1`, that identity is automatically accepted due to the above point. * The idea of "verifying a peer's identity branch" no longer applies, as COBs cannot be verified one history at a time. * Since the root commit of the Identity COB does not have a "Resource" to point to (it would have to point to itself, which is impossible), we've made the resource id optional for COBs.
This commit is contained in:
parent
c99d9a6aed
commit
1d167581f2
|
|
@ -1824,7 +1824,6 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"similar",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"timeago",
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ radicle-surf = { version = "0.17.0" }
|
|||
serde = { version = "1.0" }
|
||||
serde_json = { version = "1" }
|
||||
serde_yaml = { version = "0.8" }
|
||||
similar = { version = "2.2.1" }
|
||||
tempfile = { version = "3.3.0" }
|
||||
thiserror = { version = "1" }
|
||||
timeago = { version = "0.3", default-features = false }
|
||||
tree-sitter = { version = "0.20.0" }
|
||||
|
|
@ -73,4 +73,3 @@ path = "../radicle-term"
|
|||
pretty_assertions = { version = "1.3.0" }
|
||||
radicle = { path = "../radicle", features = ["test"] }
|
||||
radicle-node = { path = "../radicle-node", features = ["test"] }
|
||||
tempfile = { version = "3.3.0" }
|
||||
|
|
|
|||
|
|
@ -5,9 +5,8 @@ canonical head.
|
|||
First we add a second delegate, Bob, to our repo:
|
||||
|
||||
``` ~alice
|
||||
$ rad delegate add did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --to rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
|
||||
Added delegate 'did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk'
|
||||
✓ Update successful!
|
||||
$ rad id update --title "Add Bob" --description "" --delegate did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji -q
|
||||
60cbee1bbc49fcbb8063249bc4112c8886a756ba
|
||||
$ rad remote add did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
|
||||
✓ Remote bob@z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk added
|
||||
✓ Remote-tracking branch bob@z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk/master created for z6Mkt67…v4N1tRk
|
||||
|
|
|
|||
|
|
@ -39,6 +39,9 @@ Let's check that we have all the namespaces in storage:
|
|||
$ rad inspect --refs
|
||||
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
└── refs
|
||||
├── cobs
|
||||
│ └── xyz.radicle.id
|
||||
│ └── [...]
|
||||
├── heads
|
||||
│ └── master
|
||||
└── rad
|
||||
|
|
@ -49,14 +52,12 @@ z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
|
|||
├── heads
|
||||
│ └── master
|
||||
└── rad
|
||||
├── id
|
||||
└── sigrefs
|
||||
z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z
|
||||
└── refs
|
||||
├── heads
|
||||
│ └── master
|
||||
└── rad
|
||||
├── id
|
||||
└── sigrefs
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ First create an issue.
|
|||
$ rad issue open --title "flux capacitor underpowered" --description "Flux capacitor power requirements exceed current supply" --no-announce
|
||||
╭─────────────────────────────────────────────────────────╮
|
||||
│ Title flux capacitor underpowered │
|
||||
│ Issue 42028af21fabc09bfac2f25490f119f7c7e11542 │
|
||||
│ Issue 9bf82c141d5a9c54bb1d6b4517eb3bb7da8fb30d │
|
||||
│ Author z6MknSL…StBU8Vi (you) │
|
||||
│ Status open │
|
||||
│ │
|
||||
|
|
@ -21,7 +21,7 @@ $ rad issue list
|
|||
╭─────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ● ID Title Author Labels Assignees Opened │
|
||||
├─────────────────────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ● 42028af flux capacitor underpowered z6MknSL…StBU8Vi (you) [ .. ] │
|
||||
│ ● 9bf82c1 flux capacitor underpowered z6MknSL…StBU8Vi (you) [ .. ] │
|
||||
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
```
|
||||
|
||||
|
|
@ -45,7 +45,7 @@ $ rad patch
|
|||
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ● ID Title Author Head + - Updated │
|
||||
├──────────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ● 73b73f3 Define power requirements z6MknSL…StBU8Vi (you) 3e674d1 +0 -0 [ ... ] │
|
||||
│ ● a892664 Define power requirements z6MknSL…StBU8Vi (you) 3e674d1 +0 -0 [ ... ] │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
```
|
||||
|
||||
|
|
@ -53,17 +53,17 @@ Both issue and patch COBs can be listed.
|
|||
|
||||
```
|
||||
$ rad cob list --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --type xyz.radicle.issue
|
||||
42028af21fabc09bfac2f25490f119f7c7e11542
|
||||
9bf82c141d5a9c54bb1d6b4517eb3bb7da8fb30d
|
||||
$ rad cob list --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --type xyz.radicle.patch
|
||||
73b73f376e93e09e0419664766ac9e433bf7d389
|
||||
a8926643a8f6a65bc386b0131621994000485d4d
|
||||
```
|
||||
|
||||
We can look at the issue COB.
|
||||
|
||||
```
|
||||
$ rad cob show --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --type xyz.radicle.issue --object 42028af21fabc09bfac2f25490f119f7c7e11542
|
||||
commit 42028af21fabc09bfac2f25490f119f7c7e11542
|
||||
parent 175267b8910895ba87760313af254c2900743912
|
||||
$ rad cob show --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --type xyz.radicle.issue --object 9bf82c141d5a9c54bb1d6b4517eb3bb7da8fb30d
|
||||
commit 9bf82c141d5a9c54bb1d6b4517eb3bb7da8fb30d
|
||||
parent 2317f74de0494c489a233ca6f29f2b8bff6d4f15
|
||||
author z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
date Thu, 15 Dec 2022 17:28:04 +0000
|
||||
|
||||
|
|
@ -92,9 +92,9 @@ date Thu, 15 Dec 2022 17:28:04 +0000
|
|||
We can look at the patch COB too.
|
||||
|
||||
```
|
||||
$ rad cob show --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --type xyz.radicle.patch --object 73b73f376e93e09e0419664766ac9e433bf7d389
|
||||
commit 73b73f376e93e09e0419664766ac9e433bf7d389
|
||||
parent 175267b8910895ba87760313af254c2900743912
|
||||
$ rad cob show --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --type xyz.radicle.patch --object a8926643a8f6a65bc386b0131621994000485d4d
|
||||
commit a8926643a8f6a65bc386b0131621994000485d4d
|
||||
parent 2317f74de0494c489a233ca6f29f2b8bff6d4f15
|
||||
parent 3e674d1a1df90807e934f9ae5da2591dd6848a33
|
||||
parent f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
|
||||
author z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
|
|
|
|||
|
|
@ -1,46 +0,0 @@
|
|||
Delegates are the authorized keys that can manage a project's
|
||||
metadata, including adding a new delegate.
|
||||
|
||||
Let's list the current set of delegates for a project.
|
||||
|
||||
```
|
||||
$ rad delegate list rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
|
||||
[
|
||||
"did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
|
||||
]
|
||||
```
|
||||
|
||||
We want to add a new maintainer to the project to help out with the
|
||||
work.
|
||||
|
||||
```
|
||||
$ rad delegate add did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG --to rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
|
||||
Added delegate 'did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG'
|
||||
✓ Update successful!
|
||||
```
|
||||
|
||||
Let's convince ourselves that there's another delegate.
|
||||
|
||||
```
|
||||
$ rad delegate list rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
|
||||
[
|
||||
"did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
|
||||
"did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG"
|
||||
]
|
||||
```
|
||||
|
||||
And finally, we no longer want to be part of the project so we pass on
|
||||
the torch and remove ourselves from the delegate set.
|
||||
|
||||
```
|
||||
$ rad delegate remove did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi --to rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
|
||||
Removed delegate 'did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi'
|
||||
✓ Update successful!
|
||||
```
|
||||
|
||||
```
|
||||
$ rad delegate list rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
|
||||
[
|
||||
"did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG"
|
||||
]
|
||||
```
|
||||
|
|
@ -7,6 +7,9 @@ NID. This is demonstrated below where our NID is
|
|||
$ rad inspect rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --refs
|
||||
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
└── refs
|
||||
├── cobs
|
||||
│ └── xyz.radicle.id
|
||||
│ └── [...]
|
||||
├── heads
|
||||
│ └── master
|
||||
└── rad
|
||||
|
|
@ -29,6 +32,9 @@ have a copy of the main set of refs:
|
|||
$ rad inspect rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --refs
|
||||
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
└── refs
|
||||
├── cobs
|
||||
│ └── xyz.radicle.id
|
||||
│ └── [...]
|
||||
├── heads
|
||||
│ └── master
|
||||
└── rad
|
||||
|
|
@ -39,7 +45,6 @@ z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
|
|||
├── heads
|
||||
│ └── master
|
||||
└── rad
|
||||
├── id
|
||||
└── sigrefs
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,104 @@
|
|||
First let's add Bob as a delegate, and sync the changes to Bob:
|
||||
|
||||
``` ~alice
|
||||
$ rad id update --title "Add Bob" --description "Add Bob as a delegate" --delegate did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --threshold 2 -q
|
||||
bd41a1cc152a7cb8b9fb84261e4214ffa4cdb7a4
|
||||
```
|
||||
``` ~bob
|
||||
$ cd heartwood
|
||||
$ rad sync --fetch rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
|
||||
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MknSL…StBU8Vi..
|
||||
✓ Fetched repository from 1 seed(s)
|
||||
```
|
||||
|
||||
One thing that can happen is that two delegates propose a revision at the same
|
||||
time:
|
||||
|
||||
``` ~alice
|
||||
$ rad id update --title "Edit project name" --description "" --payload "xyz.radicle.project" "name" '"heart"' -q
|
||||
6c07e4e604d855f6730f884dc56216c5698ef7f8
|
||||
```
|
||||
``` ~bob
|
||||
$ rad id update --title "Edit project name" --description "" --payload "xyz.radicle.project" "name" '"wood"' -q
|
||||
fae22d07f7d386b89f14ac353b079c9eef71f948
|
||||
```
|
||||
|
||||
When Alice syncs with Bob, she notices the problem: there are two active
|
||||
revisions.
|
||||
|
||||
``` ~alice
|
||||
$ rad sync --fetch rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
|
||||
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6Mkt67…v4N1tRk..
|
||||
✓ Fetched repository from 1 seed(s)
|
||||
$ rad id list
|
||||
╭──────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ● ID Title Author Status Created │
|
||||
├──────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ● fae22d0 Edit project name bob z6Mkt67…v4N1tRk active [ ... ] │
|
||||
│ ● 6c07e4e Edit project name alice (you) active [ ... ] │
|
||||
│ ● bd41a1c Add Bob alice (you) accepted [ ... ] │
|
||||
│ ● 2317f74 Initial revision alice (you) accepted [ ... ] │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────╯
|
||||
```
|
||||
|
||||
This isn't a problem as long as we don't try to accept both. So let's accept
|
||||
Bob's:
|
||||
|
||||
``` ~alice
|
||||
$ rad id accept fae22d0 -q
|
||||
$ rad id list
|
||||
╭──────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ● ID Title Author Status Created │
|
||||
├──────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ● fae22d0 Edit project name bob z6Mkt67…v4N1tRk accepted [ ... ] │
|
||||
│ ● 6c07e4e Edit project name alice (you) stale [ ... ] │
|
||||
│ ● bd41a1c Add Bob alice (you) accepted [ ... ] │
|
||||
│ ● 2317f74 Initial revision alice (you) accepted [ ... ] │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────╯
|
||||
```
|
||||
|
||||
Doing so voided the other conflicting revision, and it can no longer be
|
||||
accepted now.
|
||||
|
||||
``` ~bob
|
||||
$ rad sync --fetch rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
|
||||
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MknSL…StBU8Vi..
|
||||
✓ Fetched repository from 1 seed(s)
|
||||
```
|
||||
``` ~bob (fail)
|
||||
$ rad id accept 6c07e4e -q
|
||||
✗ Error: cannot vote on revision that is stale
|
||||
$ rad id reject 6c07e4e -q
|
||||
✗ Error: cannot vote on revision that is stale
|
||||
```
|
||||
``` ~bob
|
||||
$ rad id show 6c07e4e
|
||||
╭────────────────────────────────────────────────────────────────────────╮
|
||||
│ Title Edit project name │
|
||||
│ Revision 6c07e4e604d855f6730f884dc56216c5698ef7f8 │
|
||||
│ Blob e93aa3e3c5c448bacd3537a81daf1437eccd046a │
|
||||
│ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │
|
||||
│ State stale │
|
||||
│ Quorum no │
|
||||
├────────────────────────────────────────────────────────────────────────┤
|
||||
│ ✓ did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi alice │
|
||||
│ ? did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk bob (you) │
|
||||
╰────────────────────────────────────────────────────────────────────────╯
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"payload": {
|
||||
"xyz.radicle.project": {
|
||||
"defaultBranch": "master",
|
||||
"description": "Radicle Heartwood Protocol & Stack",
|
||||
- "name": "heartwood"
|
||||
+ "name": "heart"
|
||||
}
|
||||
},
|
||||
"delegates": [
|
||||
"did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
|
||||
"did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk"
|
||||
],
|
||||
"threshold": 2
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,186 @@
|
|||
``` ~alice
|
||||
$ rad id update --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --title "Add Bob" --description "" --threshold 2 --delegate did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --no-confirm -q
|
||||
5666f744e2bc2333cab30ea0256bc4b61c3205bf
|
||||
```
|
||||
|
||||
``` ~bob
|
||||
$ rad sync --fetch rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
|
||||
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MknSL…StBU8Vi..
|
||||
✓ Fetched repository from 1 seed(s)
|
||||
$ rad id update --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --title "Add Eve" --description "" --delegate did:key:z6MkedTZGJGqgQ2py2b8kGecfxdt2yRdHWF6JpaZC47fovFn --no-confirm
|
||||
✓ Identity revision 9d9031417f1d86a6c0ed5ec2c4bf5820dca0eec9 created
|
||||
╭────────────────────────────────────────────────────────────────────────╮
|
||||
│ Title Add Eve │
|
||||
│ Revision 9d9031417f1d86a6c0ed5ec2c4bf5820dca0eec9 │
|
||||
│ Blob 4c7fd4c7b7d7fd5d7088a7c952556fab99a034e9 │
|
||||
│ Author did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk │
|
||||
│ State active │
|
||||
│ Quorum no │
|
||||
├────────────────────────────────────────────────────────────────────────┤
|
||||
│ ✓ did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk bob (you) │
|
||||
│ ? did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi alice │
|
||||
╰────────────────────────────────────────────────────────────────────────╯
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
{
|
||||
"payload": {
|
||||
"xyz.radicle.project": {
|
||||
"defaultBranch": "master",
|
||||
"description": "Radicle Heartwood Protocol & Stack",
|
||||
"name": "heartwood"
|
||||
}
|
||||
},
|
||||
"delegates": [
|
||||
"did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
|
||||
- "did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk"
|
||||
+ "did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk",
|
||||
+ "did:key:z6MkedTZGJGqgQ2py2b8kGecfxdt2yRdHWF6JpaZC47fovFn"
|
||||
],
|
||||
"threshold": 2
|
||||
}
|
||||
```
|
||||
|
||||
``` ~alice
|
||||
$ rad sync --fetch rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
|
||||
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6Mkt67…v4N1tRk..
|
||||
✓ Fetched repository from 1 seed(s)
|
||||
$ rad inspect rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --delegates
|
||||
did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (alice)
|
||||
did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk (bob)
|
||||
$ rad id accept 9d9031417f1d86a6c0ed5ec2c4bf5820dca0eec9 --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --no-confirm
|
||||
✓ Revision 9d9031417f1d86a6c0ed5ec2c4bf5820dca0eec9 accepted
|
||||
╭────────────────────────────────────────────────────────────────────────╮
|
||||
│ Title Add Eve │
|
||||
│ Revision 9d9031417f1d86a6c0ed5ec2c4bf5820dca0eec9 │
|
||||
│ Blob 4c7fd4c7b7d7fd5d7088a7c952556fab99a034e9 │
|
||||
│ Author did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk │
|
||||
│ State accepted │
|
||||
│ Quorum yes │
|
||||
├────────────────────────────────────────────────────────────────────────┤
|
||||
│ ✓ did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi alice (you) │
|
||||
│ ✓ did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk bob │
|
||||
╰────────────────────────────────────────────────────────────────────────╯
|
||||
$ rad inspect rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --delegates
|
||||
did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (alice)
|
||||
did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk (bob)
|
||||
did:key:z6MkedTZGJGqgQ2py2b8kGecfxdt2yRdHWF6JpaZC47fovFn
|
||||
```
|
||||
|
||||
``` ~alice
|
||||
$ rad id update --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --title "Make private" --description "" --visibility private --no-confirm -q
|
||||
1483814d54ad1321fc4ddb1f8bf7d90454de1790
|
||||
```
|
||||
|
||||
We can list all revisions:
|
||||
|
||||
``` ~alice
|
||||
$ rad id list
|
||||
╭─────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ● ID Title Author Status Created │
|
||||
├─────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ● 1483814 Make private alice (you) active [ ... ] │
|
||||
│ ● 9d90314 Add Eve bob z6Mkt67…v4N1tRk accepted [ ... ] │
|
||||
│ ● 5666f74 Add Bob alice (you) accepted [ ... ] │
|
||||
│ ● 2317f74 Initial revision alice (you) accepted [ ... ] │
|
||||
╰─────────────────────────────────────────────────────────────────────────────────────╯
|
||||
```
|
||||
|
||||
Despite being a delegate, Bob can't edit or redact Alice's revision:
|
||||
|
||||
``` ~bob (fail)
|
||||
$ rad id redact 1483814d54ad1321fc4ddb1f8bf7d90454de1790
|
||||
[..]
|
||||
```
|
||||
``` ~bob (fail)
|
||||
$ rad id edit --title "Boo!" --description "Boo!" 1483814d54ad1321fc4ddb1f8bf7d90454de1790
|
||||
[..]
|
||||
```
|
||||
|
||||
Alice can edit:
|
||||
|
||||
``` ~alice
|
||||
$ rad id edit --title "Make private" --description "Privacy is cool." 1483814d54ad1321fc4ddb1f8bf7d90454de1790
|
||||
✓ Revision 1483814d54ad1321fc4ddb1f8bf7d90454de1790 edited
|
||||
$ rad id show 1483814d54ad1321fc4ddb1f8bf7d90454de1790
|
||||
╭────────────────────────────────────────────────────────────────────────╮
|
||||
│ Title Make private │
|
||||
│ Revision 1483814d54ad1321fc4ddb1f8bf7d90454de1790 │
|
||||
│ Blob 79bc5c39103e811a3c9f11744f9a4029f063a5de │
|
||||
│ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │
|
||||
│ State active │
|
||||
│ Quorum no │
|
||||
│ │
|
||||
│ Privacy is cool. │
|
||||
├────────────────────────────────────────────────────────────────────────┤
|
||||
│ ✓ did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi alice (you) │
|
||||
│ ? did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk bob │
|
||||
│ ? did:key:z6MkedTZGJGqgQ2py2b8kGecfxdt2yRdHWF6JpaZC47fovFn │
|
||||
╰────────────────────────────────────────────────────────────────────────╯
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
{
|
||||
"payload": {
|
||||
"xyz.radicle.project": {
|
||||
"defaultBranch": "master",
|
||||
"description": "Radicle Heartwood Protocol & Stack",
|
||||
"name": "heartwood"
|
||||
}
|
||||
},
|
||||
"delegates": [
|
||||
"did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
|
||||
"did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk",
|
||||
"did:key:z6MkedTZGJGqgQ2py2b8kGecfxdt2yRdHWF6JpaZC47fovFn"
|
||||
],
|
||||
- "threshold": 2
|
||||
+ "threshold": 2,
|
||||
+ "visibility": {
|
||||
+ "type": "private"
|
||||
+ }
|
||||
}
|
||||
```
|
||||
|
||||
And she can redact her revision:
|
||||
|
||||
``` ~alice
|
||||
$ rad id redact 1483814d54ad1321fc4ddb1f8bf7d90454de1790
|
||||
✓ Revision 1483814d54ad1321fc4ddb1f8bf7d90454de1790 redacted
|
||||
```
|
||||
``` ~alice (fail)
|
||||
$ rad id show 1483814d54ad1321fc4ddb1f8bf7d90454de1790
|
||||
✗ Error: revision `1483814d54ad1321fc4ddb1f8bf7d90454de1790` not found
|
||||
```
|
||||
|
||||
Finally, Alice can also propose to remove Bob:
|
||||
``` ~alice
|
||||
$ rad id update --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --title "Remove Bob" --description "" --rescind did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --no-confirm
|
||||
✓ Identity revision ea60049b8265f60f3dcca21798ce50ef67779421 created
|
||||
╭────────────────────────────────────────────────────────────────────────╮
|
||||
│ Title Remove Bob │
|
||||
│ Revision ea60049b8265f60f3dcca21798ce50ef67779421 │
|
||||
│ Blob 7109c1c201c223dd4e9fdb10f7330dc6f0310258 │
|
||||
│ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │
|
||||
│ State active │
|
||||
│ Quorum no │
|
||||
├────────────────────────────────────────────────────────────────────────┤
|
||||
│ ✓ did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi alice (you) │
|
||||
│ ? did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk bob │
|
||||
│ ? did:key:z6MkedTZGJGqgQ2py2b8kGecfxdt2yRdHWF6JpaZC47fovFn │
|
||||
╰────────────────────────────────────────────────────────────────────────╯
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
{
|
||||
"payload": {
|
||||
"xyz.radicle.project": {
|
||||
"defaultBranch": "master",
|
||||
"description": "Radicle Heartwood Protocol & Stack",
|
||||
"name": "heartwood"
|
||||
}
|
||||
},
|
||||
"delegates": [
|
||||
"did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
|
||||
- "did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk",
|
||||
"did:key:z6MkedTZGJGqgQ2py2b8kGecfxdt2yRdHWF6JpaZC47fovFn"
|
||||
],
|
||||
"threshold": 2
|
||||
}
|
||||
```
|
||||
|
|
@ -1,429 +0,0 @@
|
|||
In this example, we're going to see what happens when a proposal
|
||||
drifts away from the latest Radicle identity.
|
||||
|
||||
First off, we will create two proposals -- we can imagine two
|
||||
delegates creating proposals concurrently.
|
||||
|
||||
```
|
||||
$ rad id edit --title "Add Alice" --description "Add Alice as a delegate" --delegates did:key:z6MkedTZGJGqgQ2py2b8kGecfxdt2yRdHWF6JpaZC47fovFn --no-confirm
|
||||
✓ Identity proposal '6b73fce909f612d5d92084b91309d73c21fea396' created
|
||||
title: Add Alice
|
||||
description: Add Alice as a delegate
|
||||
status: ❲open❳
|
||||
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
|
||||
Document Diff
|
||||
|
||||
{
|
||||
"payload": {
|
||||
"xyz.radicle.project": {
|
||||
"defaultBranch": "master",
|
||||
"description": "Radicle Heartwood Protocol & Stack",
|
||||
"name": "heartwood"
|
||||
}
|
||||
},
|
||||
"delegates": [
|
||||
- "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
|
||||
+ "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
|
||||
+ "did:key:z6MkedTZGJGqgQ2py2b8kGecfxdt2yRdHWF6JpaZC47fovFn"
|
||||
],
|
||||
"threshold": 1
|
||||
}
|
||||
|
||||
|
||||
Accepted
|
||||
|
||||
total: 0
|
||||
keys: []
|
||||
|
||||
Rejected
|
||||
|
||||
total: 0
|
||||
keys: []
|
||||
|
||||
Quorum Reached
|
||||
|
||||
👎 no
|
||||
```
|
||||
|
||||
```
|
||||
$ rad id edit --title "Add Bob" --description "Add Bob as a delegate" --delegates did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG --no-confirm
|
||||
✓ Identity proposal '2bf3a85e209d10b11a65e7ed8a8085f6f18ac6bf' created
|
||||
title: Add Bob
|
||||
description: Add Bob as a delegate
|
||||
status: ❲open❳
|
||||
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
|
||||
Document Diff
|
||||
|
||||
{
|
||||
"payload": {
|
||||
"xyz.radicle.project": {
|
||||
"defaultBranch": "master",
|
||||
"description": "Radicle Heartwood Protocol & Stack",
|
||||
"name": "heartwood"
|
||||
}
|
||||
},
|
||||
"delegates": [
|
||||
- "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
|
||||
+ "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
|
||||
+ "did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG"
|
||||
],
|
||||
"threshold": 1
|
||||
}
|
||||
|
||||
|
||||
Accepted
|
||||
|
||||
total: 0
|
||||
keys: []
|
||||
|
||||
Rejected
|
||||
|
||||
total: 0
|
||||
keys: []
|
||||
|
||||
Quorum Reached
|
||||
|
||||
👎 no
|
||||
```
|
||||
|
||||
Now, if the first proposal was accepted and committed before the
|
||||
second proposal, then the identity would be out of date. So let's run
|
||||
through that and see what happens.
|
||||
|
||||
```
|
||||
$ rad id accept 6b73fce909f612d5d92084b91309d73c21fea396 --no-confirm
|
||||
✓ Accepted proposal ✓
|
||||
title: Add Alice
|
||||
description: Add Alice as a delegate
|
||||
status: ❲open❳
|
||||
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
|
||||
Document Diff
|
||||
|
||||
{
|
||||
"payload": {
|
||||
"xyz.radicle.project": {
|
||||
"defaultBranch": "master",
|
||||
"description": "Radicle Heartwood Protocol & Stack",
|
||||
"name": "heartwood"
|
||||
}
|
||||
},
|
||||
"delegates": [
|
||||
- "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
|
||||
+ "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
|
||||
+ "did:key:z6MkedTZGJGqgQ2py2b8kGecfxdt2yRdHWF6JpaZC47fovFn"
|
||||
],
|
||||
"threshold": 1
|
||||
}
|
||||
|
||||
|
||||
Accepted
|
||||
|
||||
total: 1
|
||||
keys: [
|
||||
"did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
|
||||
]
|
||||
|
||||
Rejected
|
||||
|
||||
total: 0
|
||||
keys: []
|
||||
|
||||
Quorum Reached
|
||||
|
||||
👍 yes
|
||||
```
|
||||
|
||||
```
|
||||
$ rad id commit 6b73fce909f612d5d92084b91309d73c21fea396 --no-confirm
|
||||
✓ Committed new identity '29ae4b72f5a315328f06fbd68dc1c396a2d5c45e'
|
||||
title: Add Alice
|
||||
description: Add Alice as a delegate
|
||||
status: ❲committed❳
|
||||
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
|
||||
Document Diff
|
||||
|
||||
{
|
||||
"payload": {
|
||||
"xyz.radicle.project": {
|
||||
"defaultBranch": "master",
|
||||
"description": "Radicle Heartwood Protocol & Stack",
|
||||
"name": "heartwood"
|
||||
}
|
||||
},
|
||||
"delegates": [
|
||||
- "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
|
||||
+ "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
|
||||
+ "did:key:z6MkedTZGJGqgQ2py2b8kGecfxdt2yRdHWF6JpaZC47fovFn"
|
||||
],
|
||||
"threshold": 1
|
||||
}
|
||||
|
||||
|
||||
Accepted
|
||||
|
||||
total: 1
|
||||
keys: [
|
||||
"did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
|
||||
]
|
||||
|
||||
Rejected
|
||||
|
||||
total: 0
|
||||
keys: []
|
||||
|
||||
Quorum Reached
|
||||
|
||||
👍 yes
|
||||
```
|
||||
|
||||
Now, when we go to accept the second proposal:
|
||||
|
||||
```
|
||||
$ 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
|
||||
✓ Accepted proposal ✓
|
||||
title: Add Bob
|
||||
description: Add Bob as a delegate
|
||||
status: ❲open❳
|
||||
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
|
||||
Document Diff
|
||||
|
||||
{
|
||||
"payload": {
|
||||
"xyz.radicle.project": {
|
||||
"defaultBranch": "master",
|
||||
"description": "Radicle Heartwood Protocol & Stack",
|
||||
"name": "heartwood"
|
||||
}
|
||||
},
|
||||
"delegates": [
|
||||
"did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
|
||||
- "did:key:z6MkedTZGJGqgQ2py2b8kGecfxdt2yRdHWF6JpaZC47fovFn"
|
||||
+ "did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG"
|
||||
],
|
||||
"threshold": 1
|
||||
}
|
||||
|
||||
|
||||
Accepted
|
||||
|
||||
total: 1
|
||||
keys: [
|
||||
"did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
|
||||
]
|
||||
|
||||
Rejected
|
||||
|
||||
total: 0
|
||||
keys: []
|
||||
|
||||
Quorum Reached
|
||||
|
||||
👍 yes
|
||||
```
|
||||
|
||||
Note that a warning was emitted:
|
||||
|
||||
! Warning: Revision is out of date
|
||||
! Warning: d96f425412c9f8ad5d9a9a05c9831d0728e2338d =/= 475cdfbc8662853dd132ec564e4f5eb0f152dd7f
|
||||
* Consider using 'rad id rebase' to update the proposal to the latest identity
|
||||
|
||||
If we attempt to commit this revision, the command will fail:
|
||||
|
||||
``` (fail)
|
||||
$ 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
|
||||
✗ Error: 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 2bf3a85e209d10b11a65e7ed8a8085f6f18ac6bf --no-confirm
|
||||
✓ Identity proposal '2bf3a85e209d10b11a65e7ed8a8085f6f18ac6bf' rebased
|
||||
✓ Revision 'e56b3e0842a4dd37c2a997344bcb4113704e4768'
|
||||
title: Add Bob
|
||||
description: Add Bob as a delegate
|
||||
status: ❲open❳
|
||||
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
|
||||
Document Diff
|
||||
|
||||
{
|
||||
"payload": {
|
||||
"xyz.radicle.project": {
|
||||
"defaultBranch": "master",
|
||||
"description": "Radicle Heartwood Protocol & Stack",
|
||||
"name": "heartwood"
|
||||
}
|
||||
},
|
||||
"delegates": [
|
||||
"did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
|
||||
- "did:key:z6MkedTZGJGqgQ2py2b8kGecfxdt2yRdHWF6JpaZC47fovFn"
|
||||
+ "did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG"
|
||||
],
|
||||
"threshold": 1
|
||||
}
|
||||
|
||||
|
||||
Accepted
|
||||
|
||||
total: 0
|
||||
keys: []
|
||||
|
||||
Rejected
|
||||
|
||||
total: 0
|
||||
keys: []
|
||||
|
||||
Quorum Reached
|
||||
|
||||
👎 no
|
||||
```
|
||||
|
||||
We can now update the proposal to have both keys in the delegates set:
|
||||
|
||||
```
|
||||
$ 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❳
|
||||
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
|
||||
Document Diff
|
||||
|
||||
{
|
||||
"payload": {
|
||||
"xyz.radicle.project": {
|
||||
"defaultBranch": "master",
|
||||
"description": "Radicle Heartwood Protocol & Stack",
|
||||
"name": "heartwood"
|
||||
}
|
||||
},
|
||||
"delegates": [
|
||||
"did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
|
||||
+ "did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG",
|
||||
"did:key:z6MkedTZGJGqgQ2py2b8kGecfxdt2yRdHWF6JpaZC47fovFn"
|
||||
],
|
||||
"threshold": 1
|
||||
}
|
||||
|
||||
|
||||
Accepted
|
||||
|
||||
total: 0
|
||||
keys: []
|
||||
|
||||
Rejected
|
||||
|
||||
total: 0
|
||||
keys: []
|
||||
|
||||
Quorum Reached
|
||||
|
||||
👎 no
|
||||
```
|
||||
|
||||
Finally, we can accept and commit this proposal, creating the final
|
||||
state of our new Radicle identity:
|
||||
|
||||
$ rad id show 2bf3a85e209d10b11a65e7ed8a8085f6f18ac6bf --revisions
|
||||
|
||||
```
|
||||
$ rad id accept 2bf3a85e209d10b11a65e7ed8a8085f6f18ac6bf --rev f15fc641d777cfb005caaa9405e262e516b8ac60 --no-confirm
|
||||
✓ Accepted proposal ✓
|
||||
title: Add Bob
|
||||
description: Add Bob as a delegate
|
||||
status: ❲open❳
|
||||
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
|
||||
Document Diff
|
||||
|
||||
{
|
||||
"payload": {
|
||||
"xyz.radicle.project": {
|
||||
"defaultBranch": "master",
|
||||
"description": "Radicle Heartwood Protocol & Stack",
|
||||
"name": "heartwood"
|
||||
}
|
||||
},
|
||||
"delegates": [
|
||||
"did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
|
||||
+ "did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG",
|
||||
"did:key:z6MkedTZGJGqgQ2py2b8kGecfxdt2yRdHWF6JpaZC47fovFn"
|
||||
],
|
||||
"threshold": 1
|
||||
}
|
||||
|
||||
|
||||
Accepted
|
||||
|
||||
total: 1
|
||||
keys: [
|
||||
"did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
|
||||
]
|
||||
|
||||
Rejected
|
||||
|
||||
total: 0
|
||||
keys: []
|
||||
|
||||
Quorum Reached
|
||||
|
||||
👍 yes
|
||||
```
|
||||
|
||||
```
|
||||
$ rad id commit 2bf3a85e209d10b11a65e7ed8a8085f6f18ac6bf --rev f15fc641d777cfb005caaa9405e262e516b8ac60 --no-confirm
|
||||
✓ Committed new identity '60de897bc24898f6908fd1272633c0b15aa4096f'
|
||||
title: Add Bob
|
||||
description: Add Bob as a delegate
|
||||
status: ❲committed❳
|
||||
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
|
||||
Document Diff
|
||||
|
||||
{
|
||||
"payload": {
|
||||
"xyz.radicle.project": {
|
||||
"defaultBranch": "master",
|
||||
"description": "Radicle Heartwood Protocol & Stack",
|
||||
"name": "heartwood"
|
||||
}
|
||||
},
|
||||
"delegates": [
|
||||
"did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
|
||||
+ "did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG",
|
||||
"did:key:z6MkedTZGJGqgQ2py2b8kGecfxdt2yRdHWF6JpaZC47fovFn"
|
||||
],
|
||||
"threshold": 1
|
||||
}
|
||||
|
||||
|
||||
Accepted
|
||||
|
||||
total: 1
|
||||
keys: [
|
||||
"did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
|
||||
]
|
||||
|
||||
Rejected
|
||||
|
||||
total: 0
|
||||
keys: []
|
||||
|
||||
Quorum Reached
|
||||
|
||||
👍 yes
|
||||
```
|
||||
|
|
@ -4,24 +4,32 @@ maintainer. This requires adding them as a `delegate` and possibly
|
|||
editing the `threshold` for passing new changes to the identity of the
|
||||
project.
|
||||
|
||||
For cases where `threshold = 1`, it is enough to use the `rad
|
||||
delegate` command. For cases where `threshold > 1`, it is necessary to
|
||||
gather a quorum of signatures to update the Radicle identity. To do
|
||||
this, we use the `rad id` command.
|
||||
For cases where `threshold > 1`, it is necessary to gather a quorum of
|
||||
signatures to update the Radicle identity. To do this, we use the `rad id`
|
||||
command. For now, since we are the only delegate, and `treshold` is `1`, we
|
||||
can update the identity ourselves.
|
||||
|
||||
Let's add Bob as a delegate using their DID
|
||||
`did:key:z6MkedTZGJGqgQ2py2b8kGecfxdt2yRdHWF6JpaZC47fovFn`.
|
||||
Let's add Bob as a delegate using their DID,
|
||||
`did:key:z6MkedTZGJGqgQ2py2b8kGecfxdt2yRdHWF6JpaZC47fovFn`, and update the
|
||||
threshold to `2`.
|
||||
|
||||
```
|
||||
$ rad id edit --title "Add Bob" --description "Add Bob as a delegate" --delegates did:key:z6MkedTZGJGqgQ2py2b8kGecfxdt2yRdHWF6JpaZC47fovFn --no-confirm
|
||||
✓ Identity proposal '662a8065f18db50d9ee952bb36eda5b605f161e9' created
|
||||
title: Add Bob
|
||||
description: Add Bob as a delegate
|
||||
status: ❲open❳
|
||||
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
|
||||
Document Diff
|
||||
$ rad id update --title "Add Bob" --description "Add Bob as a delegate" --delegate did:key:z6MkedTZGJGqgQ2py2b8kGecfxdt2yRdHWF6JpaZC47fovFn --threshold 2
|
||||
✓ Identity revision d067ffc7bf65d318dd68ca4b8c7adf61a369ea2f created
|
||||
╭───────────────────────────────────────────────────────────────────╮
|
||||
│ Title Add Bob │
|
||||
│ Revision d067ffc7bf65d318dd68ca4b8c7adf61a369ea2f │
|
||||
│ Blob 7109c1c201c223dd4e9fdb10f7330dc6f0310258 │
|
||||
│ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │
|
||||
│ State accepted │
|
||||
│ Quorum yes │
|
||||
│ │
|
||||
│ Add Bob as a delegate │
|
||||
├───────────────────────────────────────────────────────────────────┤
|
||||
│ ✓ did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (you) │
|
||||
╰───────────────────────────────────────────────────────────────────╯
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
{
|
||||
"payload": {
|
||||
"xyz.radicle.project": {
|
||||
|
|
@ -35,367 +43,97 @@ Document Diff
|
|||
+ "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
|
||||
+ "did:key:z6MkedTZGJGqgQ2py2b8kGecfxdt2yRdHWF6JpaZC47fovFn"
|
||||
],
|
||||
"threshold": 1
|
||||
- "threshold": 1
|
||||
+ "threshold": 2
|
||||
}
|
||||
|
||||
|
||||
Accepted
|
||||
|
||||
total: 0
|
||||
keys: []
|
||||
|
||||
Rejected
|
||||
|
||||
total: 0
|
||||
keys: []
|
||||
|
||||
Quorum Reached
|
||||
|
||||
👎 no
|
||||
```
|
||||
|
||||
Before moving on, let's take a few notes on this output. The first
|
||||
thing we'll notice is that the difference between the current identity
|
||||
document and the proposed changes are shown. Specifically, we changed
|
||||
the delegates:
|
||||
the delegates and threshold:
|
||||
|
||||
"delegates": [
|
||||
- "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
|
||||
+ "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
|
||||
+ "did:key:z6MkedTZGJGqgQ2py2b8kGecfxdt2yRdHWF6JpaZC47fovFn"
|
||||
],
|
||||
"delegates": [
|
||||
- "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
|
||||
+ "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
|
||||
+ "did:key:z6MkedTZGJGqgQ2py2b8kGecfxdt2yRdHWF6JpaZC47fovFn"
|
||||
],
|
||||
...
|
||||
- "threshold": 1
|
||||
+ "threshold": 2
|
||||
|
||||
Next we have the number of `Accepted` reviews from delegates, starting
|
||||
off with none:
|
||||
Next we have the number of signatures from delegates, which includes our own:
|
||||
|
||||
Accepted
|
||||
|
||||
total: 0
|
||||
keys: []
|
||||
|
||||
The same with `Rejected` reviews:
|
||||
|
||||
Rejected
|
||||
|
||||
total: 0
|
||||
keys: []
|
||||
✓ did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
|
||||
Finally, we can see whether the `Quorum` was reached:
|
||||
|
||||
Quorum Reached
|
||||
Quorum yes
|
||||
|
||||
👎 no
|
||||
|
||||
Let's see what happens when we reject the change:
|
||||
Since the threshold was previously `1`, this change is now in effect. We
|
||||
can verify that by listing the current identity document:
|
||||
|
||||
```
|
||||
$ rad id reject 662a8065f18db50d9ee952bb36eda5b605f161e9 --no-confirm
|
||||
✓ Rejected proposal 👎
|
||||
title: Add Bob
|
||||
description: Add Bob as a delegate
|
||||
status: ❲open❳
|
||||
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
|
||||
Document Diff
|
||||
|
||||
{
|
||||
"payload": {
|
||||
"xyz.radicle.project": {
|
||||
"defaultBranch": "master",
|
||||
"description": "Radicle Heartwood Protocol & Stack",
|
||||
"name": "heartwood"
|
||||
}
|
||||
},
|
||||
"delegates": [
|
||||
- "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
|
||||
+ "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
|
||||
+ "did:key:z6MkedTZGJGqgQ2py2b8kGecfxdt2yRdHWF6JpaZC47fovFn"
|
||||
],
|
||||
"threshold": 1
|
||||
}
|
||||
|
||||
|
||||
Accepted
|
||||
|
||||
total: 0
|
||||
keys: []
|
||||
|
||||
Rejected
|
||||
|
||||
total: 1
|
||||
keys: [
|
||||
"did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
|
||||
]
|
||||
|
||||
Quorum Reached
|
||||
|
||||
👎 no
|
||||
$ rad inspect --identity
|
||||
{
|
||||
"payload": {
|
||||
"xyz.radicle.project": {
|
||||
"defaultBranch": "master",
|
||||
"description": "Radicle Heartwood Protocol & Stack",
|
||||
"name": "heartwood"
|
||||
}
|
||||
},
|
||||
"delegates": [
|
||||
"did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
|
||||
"did:key:z6MkedTZGJGqgQ2py2b8kGecfxdt2yRdHWF6JpaZC47fovFn"
|
||||
],
|
||||
"threshold": 2
|
||||
}
|
||||
```
|
||||
|
||||
Our key was added to the `Rejected` set of `keys` and the `total`
|
||||
increased to `1`.
|
||||
We can also look at the document's COB directly:
|
||||
```
|
||||
$ rad cob show --object 2317f74de0494c489a233ca6f29f2b8bff6d4f15 --type xyz.radicle.id --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
|
||||
commit d067ffc7bf65d318dd68ca4b8c7adf61a369ea2f
|
||||
parent 2317f74de0494c489a233ca6f29f2b8bff6d4f15
|
||||
author z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
date Thu, 15 Dec 2022 17:28:04 +0000
|
||||
|
||||
Rejected
|
||||
{
|
||||
"blob": "7109c1c201c223dd4e9fdb10f7330dc6f0310258",
|
||||
"description": "Add Bob as a delegate",
|
||||
"parent": "2317f74de0494c489a233ca6f29f2b8bff6d4f15",
|
||||
"signature": "z3sne3sdReZ4AtgxQmn7R1pQnz7E9ZEUoRfCJDJ8ytgnBMFW4DJqRHuBz2h1NK4QdGEy3QCpyVoJKfE95tNoivXwz",
|
||||
"title": "Add Bob",
|
||||
"type": "revision"
|
||||
}
|
||||
|
||||
total: 1
|
||||
keys: [
|
||||
"z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
|
||||
]
|
||||
commit 2317f74de0494c489a233ca6f29f2b8bff6d4f15
|
||||
author z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
date Thu, 15 Dec 2022 17:28:04 +0000
|
||||
|
||||
Instead, let's accept the proposal:
|
||||
{
|
||||
"blob": "d96f425412c9f8ad5d9a9a05c9831d0728e2338d",
|
||||
"parent": null,
|
||||
"signature": "z5nGqUvrmfiSyLjNCHWTWYvVMcPUZcvo9TxPKzEKXYBdSgUzbrqf1cYsmpGgbQvYunnsrLSsubEmxZaRdKM4quqQR",
|
||||
"title": "Initial revision",
|
||||
"type": "revision"
|
||||
}
|
||||
|
||||
```
|
||||
$ rad id accept 662a8065f18db50d9ee952bb36eda5b605f161e9 --no-confirm
|
||||
✓ Accepted proposal ✓
|
||||
title: Add Bob
|
||||
description: Add Bob as a delegate
|
||||
status: ❲open❳
|
||||
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
|
||||
Document Diff
|
||||
Note that once a revision is accepted, it can't be edited, redacted or otherwise
|
||||
acted upon:
|
||||
|
||||
{
|
||||
"payload": {
|
||||
"xyz.radicle.project": {
|
||||
"defaultBranch": "master",
|
||||
"description": "Radicle Heartwood Protocol & Stack",
|
||||
"name": "heartwood"
|
||||
}
|
||||
},
|
||||
"delegates": [
|
||||
- "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
|
||||
+ "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
|
||||
+ "did:key:z6MkedTZGJGqgQ2py2b8kGecfxdt2yRdHWF6JpaZC47fovFn"
|
||||
],
|
||||
"threshold": 1
|
||||
}
|
||||
|
||||
|
||||
Accepted
|
||||
|
||||
total: 1
|
||||
keys: [
|
||||
"did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
|
||||
]
|
||||
|
||||
Rejected
|
||||
|
||||
total: 0
|
||||
keys: []
|
||||
|
||||
Quorum Reached
|
||||
|
||||
👍 yes
|
||||
``` (fail)
|
||||
$ rad id redact d067ffc7bf65d318dd68ca4b8c7adf61a369ea2f
|
||||
✗ Error: [..]
|
||||
```
|
||||
|
||||
Our key has changed from the `Rejected` set to the `Accepted` set
|
||||
instead:
|
||||
|
||||
Accepted
|
||||
|
||||
total: 1
|
||||
keys: [
|
||||
"did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
|
||||
]
|
||||
|
||||
As well as that, the `Quorum` has now been reached:
|
||||
|
||||
Quorum Reached
|
||||
|
||||
👍 yes
|
||||
|
||||
At this point, we can commit the proposal and update the identity:
|
||||
|
||||
``` (fail)
|
||||
$ rad id reject d067ffc7bf65d318dd68ca4b8c7adf61a369ea2f
|
||||
✗ Error: [..]
|
||||
```
|
||||
$ rad id commit 662a8065f18db50d9ee952bb36eda5b605f161e9 --no-confirm
|
||||
✓ Committed new identity 'c96e764965aaeff1c6ea3e5b97e2b9828773c8b0'
|
||||
title: Add Bob
|
||||
description: Add Bob as a delegate
|
||||
status: ❲committed❳
|
||||
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
|
||||
Document Diff
|
||||
|
||||
{
|
||||
"payload": {
|
||||
"xyz.radicle.project": {
|
||||
"defaultBranch": "master",
|
||||
"description": "Radicle Heartwood Protocol & Stack",
|
||||
"name": "heartwood"
|
||||
}
|
||||
},
|
||||
"delegates": [
|
||||
- "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
|
||||
+ "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
|
||||
+ "did:key:z6MkedTZGJGqgQ2py2b8kGecfxdt2yRdHWF6JpaZC47fovFn"
|
||||
],
|
||||
"threshold": 1
|
||||
}
|
||||
|
||||
|
||||
Accepted
|
||||
|
||||
total: 1
|
||||
keys: [
|
||||
"did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
|
||||
]
|
||||
|
||||
Rejected
|
||||
|
||||
total: 0
|
||||
keys: []
|
||||
|
||||
Quorum Reached
|
||||
|
||||
👍 yes
|
||||
``` (fail)
|
||||
$ rad id accept d067ffc7bf65d318dd68ca4b8c7adf61a369ea2f
|
||||
✗ Error: [..]
|
||||
```
|
||||
|
||||
Let's say we decide to also change the `threshold`, we can do so using
|
||||
the `--threshold` option:
|
||||
|
||||
```
|
||||
$ rad id edit --title "Update threshold" --description "Update to safer threshold" --threshold 2 --no-confirm
|
||||
✓ Identity proposal 'e6c04862ed0e59739f34232c8690cbad73840a93' created
|
||||
title: Update threshold
|
||||
description: Update to safer threshold
|
||||
status: ❲open❳
|
||||
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
|
||||
Document Diff
|
||||
|
||||
{
|
||||
"payload": {
|
||||
"xyz.radicle.project": {
|
||||
"defaultBranch": "master",
|
||||
"description": "Radicle Heartwood Protocol & Stack",
|
||||
"name": "heartwood"
|
||||
}
|
||||
},
|
||||
"delegates": [
|
||||
"did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
|
||||
"did:key:z6MkedTZGJGqgQ2py2b8kGecfxdt2yRdHWF6JpaZC47fovFn"
|
||||
],
|
||||
- "threshold": 1
|
||||
+ "threshold": 2
|
||||
}
|
||||
|
||||
|
||||
Accepted
|
||||
|
||||
total: 0
|
||||
keys: []
|
||||
|
||||
Rejected
|
||||
|
||||
total: 0
|
||||
keys: []
|
||||
|
||||
Quorum Reached
|
||||
|
||||
👎 no
|
||||
```
|
||||
|
||||
But we change our minds and decide to close the proposal instead:
|
||||
|
||||
```
|
||||
$ rad id close e6c04862ed0e59739f34232c8690cbad73840a93 --no-confirm
|
||||
✓ Closed identity proposal 'e6c04862ed0e59739f34232c8690cbad73840a93'
|
||||
title: Update threshold
|
||||
description: Update to safer threshold
|
||||
status: ❲closed❳
|
||||
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
|
||||
Document Diff
|
||||
|
||||
{
|
||||
"payload": {
|
||||
"xyz.radicle.project": {
|
||||
"defaultBranch": "master",
|
||||
"description": "Radicle Heartwood Protocol & Stack",
|
||||
"name": "heartwood"
|
||||
}
|
||||
},
|
||||
"delegates": [
|
||||
"did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
|
||||
"did:key:z6MkedTZGJGqgQ2py2b8kGecfxdt2yRdHWF6JpaZC47fovFn"
|
||||
],
|
||||
- "threshold": 1
|
||||
+ "threshold": 2
|
||||
}
|
||||
|
||||
|
||||
Accepted
|
||||
|
||||
total: 0
|
||||
keys: []
|
||||
|
||||
Rejected
|
||||
|
||||
total: 0
|
||||
keys: []
|
||||
|
||||
Quorum Reached
|
||||
|
||||
👎 no
|
||||
```
|
||||
|
||||
The proposal is now closed and cannot be committed. If at a later date
|
||||
we want to update the document with the same change we have to open a
|
||||
new proposal.
|
||||
|
||||
If at any time we want to see what proposals have been made to this
|
||||
Radicle identity, then we can use the list command:
|
||||
|
||||
```
|
||||
$ rad id list
|
||||
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 e6c04862ed0e59739f34232c8690cbad73840a93
|
||||
title: Update threshold
|
||||
description: Update to safer threshold
|
||||
status: ❲closed❳
|
||||
author: did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
|
||||
Document Diff
|
||||
|
||||
{
|
||||
"payload": {
|
||||
"xyz.radicle.project": {
|
||||
"defaultBranch": "master",
|
||||
"description": "Radicle Heartwood Protocol & Stack",
|
||||
"name": "heartwood"
|
||||
}
|
||||
},
|
||||
"delegates": [
|
||||
"did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
|
||||
"did:key:z6MkedTZGJGqgQ2py2b8kGecfxdt2yRdHWF6JpaZC47fovFn"
|
||||
],
|
||||
- "threshold": 1
|
||||
+ "threshold": 2
|
||||
}
|
||||
|
||||
|
||||
Accepted
|
||||
|
||||
total: 0
|
||||
keys: []
|
||||
|
||||
Rejected
|
||||
|
||||
total: 0
|
||||
keys: []
|
||||
|
||||
Quorum Reached
|
||||
|
||||
👎 no
|
||||
```
|
||||
|
||||
On a final note, these examples used `--no-confirm`. The default mode
|
||||
for making proposals is to select and confirm any actions being
|
||||
performed on the proposal.
|
||||
|
|
|
|||
|
|
@ -16,11 +16,8 @@ She allows Bob to view the repository. And when she syncs, one node (Bob) gets
|
|||
the refs.
|
||||
|
||||
``` ~alice
|
||||
$ rad id edit --allow did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk -q
|
||||
e98cd6a0a3e94837b382e59e02b3ea83991a8244
|
||||
$ rad id accept e98cd6a0a3e94837b382e59e02b3ea83991a8244 -q
|
||||
$ rad id commit e98cd6a0a3e94837b382e59e02b3ea83991a8244 -q
|
||||
c568f8aac97db40a5e63e1261872bfbd9a3a61e4
|
||||
$ rad id update --title "Allow Bob" --description "" --allow did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk -q
|
||||
...
|
||||
$ rad sync --announce --timeout 3
|
||||
✓ Synced with 1 node(s)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@ It's also possible to display all of the repository's git references:
|
|||
$ rad inspect --refs
|
||||
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
└── refs
|
||||
├── cobs
|
||||
│ └── xyz.radicle.id
|
||||
│ └── 2317f74de0494c489a233ca6f29f2b8bff6d4f15
|
||||
├── heads
|
||||
│ └── master
|
||||
└── rad
|
||||
|
|
@ -46,18 +49,13 @@ history:
|
|||
|
||||
```
|
||||
$ rad inspect --history
|
||||
commit 175267b8910895ba87760313af254c2900743912
|
||||
commit 2317f74de0494c489a233ca6f29f2b8bff6d4f15
|
||||
blob d96f425412c9f8ad5d9a9a05c9831d0728e2338d
|
||||
date Thu, 15 Dec 2022 17:28:04 +0000
|
||||
|
||||
Initialize Radicle
|
||||
|
||||
Rad-Signature: z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi z5nGqUvrmfiSyLjNCHWTWYvVMcPUZcvo9TxPKzEKXYBdSgUzbrqf1cYsmpGgbQvYunnsrLSsubEmxZaRdKM4quqQR
|
||||
Initialize identity
|
||||
|
||||
{
|
||||
"delegates": [
|
||||
"did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
|
||||
],
|
||||
"payload": {
|
||||
"xyz.radicle.project": {
|
||||
"defaultBranch": "master",
|
||||
|
|
@ -65,6 +63,9 @@ date Thu, 15 Dec 2022 17:28:04 +0000
|
|||
"name": "heartwood"
|
||||
}
|
||||
},
|
||||
"delegates": [
|
||||
"did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
|
||||
],
|
||||
"threshold": 1
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 42028af21fabc09bfac2f25490f119f7c7e11542 │
|
||||
│ Issue 9bf82c141d5a9c54bb1d6b4517eb3bb7da8fb30d │
|
||||
│ Author z6MknSL…StBU8Vi (you) │
|
||||
│ Status open │
|
||||
│ │
|
||||
|
|
@ -22,17 +22,17 @@ $ rad issue list
|
|||
╭─────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ● ID Title Author Labels Assignees Opened │
|
||||
├─────────────────────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ● 42028af flux capacitor underpowered z6MknSL…StBU8Vi (you) [ .. ] │
|
||||
│ ● 9bf82c1 flux capacitor underpowered z6MknSL…StBU8Vi (you) [ .. ] │
|
||||
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
```
|
||||
|
||||
Show the issue information issue.
|
||||
|
||||
```
|
||||
$ rad issue show 42028af
|
||||
$ rad issue show 9bf82c1
|
||||
╭─────────────────────────────────────────────────────────╮
|
||||
│ Title flux capacitor underpowered │
|
||||
│ Issue 42028af21fabc09bfac2f25490f119f7c7e11542 │
|
||||
│ Issue 9bf82c141d5a9c54bb1d6b4517eb3bb7da8fb30d │
|
||||
│ Author z6MknSL…StBU8Vi (you) │
|
||||
│ Status open │
|
||||
│ │
|
||||
|
|
@ -49,7 +49,7 @@ others to work on. This is to ensure work is not duplicated.
|
|||
Let's assign ourselves to this one.
|
||||
|
||||
```
|
||||
$ rad assign 42028af21fabc09bfac2f25490f119f7c7e11542 --to did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
$ rad assign 9bf82c141d5a9c54bb1d6b4517eb3bb7da8fb30d --to did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
```
|
||||
|
||||
It will now show in the list of issues assigned to us.
|
||||
|
|
@ -59,14 +59,14 @@ $ rad issue list --assigned
|
|||
╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ● ID Title Author Labels Assignees Opened │
|
||||
├───────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ● 42028af flux capacitor underpowered z6MknSL…StBU8Vi (you) z6MknSL…StBU8Vi [ .. ] │
|
||||
│ ● 9bf82c1 flux capacitor underpowered z6MknSL…StBU8Vi (you) z6MknSL…StBU8Vi [ .. ] │
|
||||
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
```
|
||||
|
||||
Note: this can always be undone with the `unassign` subcommand.
|
||||
|
||||
```
|
||||
$ rad unassign 42028af21fabc09bfac2f25490f119f7c7e11542 --from did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
$ rad unassign 9bf82c141d5a9c54bb1d6b4517eb3bb7da8fb30d --from did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
```
|
||||
|
||||
Great, now we have communicated to the world about our car's defect.
|
||||
|
|
@ -75,28 +75,28 @@ But wait! We've found an important detail about the car's power requirements.
|
|||
It will help whoever works on a fix.
|
||||
|
||||
```
|
||||
$ rad issue comment 42028af21fabc09bfac2f25490f119f7c7e11542 --message 'The flux capacitor needs 1.21 Gigawatts' -q
|
||||
84492237dc0908b1e5b728d1a4e5f1343b6ffe9b
|
||||
$ rad issue comment 42028af21fabc09bfac2f25490f119f7c7e11542 --reply-to 84492237dc0908b1e5b728d1a4e5f1343b6ffe9b --message 'More power!' -q
|
||||
dd679552a15e2db73bbedf3084f5f7c62bb0d724
|
||||
$ rad issue comment 9bf82c141d5a9c54bb1d6b4517eb3bb7da8fb30d --message 'The flux capacitor needs 1.21 Gigawatts' -q
|
||||
1a8e9d3d62d22b247064b12d1d89ad8598504129
|
||||
$ rad issue comment 9bf82c141d5a9c54bb1d6b4517eb3bb7da8fb30d --reply-to 1a8e9d3d62d22b247064b12d1d89ad8598504129 --message 'More power!' -q
|
||||
fb6ab7e0ca5be3c34688bcae37d7302bb824decf
|
||||
```
|
||||
|
||||
We can see our comments by showing the issue:
|
||||
|
||||
```
|
||||
$ rad issue show 42028af21fabc09bfac2f25490f119f7c7e11542
|
||||
$ rad issue show 9bf82c141d5a9c54bb1d6b4517eb3bb7da8fb30d
|
||||
╭─────────────────────────────────────────────────────────╮
|
||||
│ Title flux capacitor underpowered │
|
||||
│ Issue 42028af21fabc09bfac2f25490f119f7c7e11542 │
|
||||
│ Issue 9bf82c141d5a9c54bb1d6b4517eb3bb7da8fb30d │
|
||||
│ Author z6MknSL…StBU8Vi (you) │
|
||||
│ Status open │
|
||||
│ │
|
||||
│ Flux capacitor power requirements exceed current supply │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ z6MknSL…StBU8Vi (you) [ ... ] 8449223 │
|
||||
│ z6MknSL…StBU8Vi (you) [ ... ] 1a8e9d3 │
|
||||
│ The flux capacitor needs 1.21 Gigawatts │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ z6MknSL…StBU8Vi (you) [ ... ] dd67955 │
|
||||
│ z6MknSL…StBU8Vi (you) [ ... ] fb6ab7e │
|
||||
│ More power! │
|
||||
╰─────────────────────────────────────────────────────────╯
|
||||
```
|
||||
|
|
|
|||
|
|
@ -2,16 +2,16 @@ Labeling an issue is easy, let's add the `bug` and `good-first-issue` labels to
|
|||
some issue:
|
||||
|
||||
```
|
||||
$ rad label 42028af21fabc09bfac2f25490f119f7c7e11542 bug good-first-issue
|
||||
$ rad label 9bf82c141d5a9c54bb1d6b4517eb3bb7da8fb30d bug good-first-issue
|
||||
```
|
||||
|
||||
We can now show the issue to check whether those labels were added:
|
||||
|
||||
```
|
||||
$ rad issue show 42028af21fabc09bfac2f25490f119f7c7e11542 --format header
|
||||
$ rad issue show 9bf82c141d5a9c54bb1d6b4517eb3bb7da8fb30d --format header
|
||||
╭─────────────────────────────────────────────────────────╮
|
||||
│ Title flux capacitor underpowered │
|
||||
│ Issue 42028af21fabc09bfac2f25490f119f7c7e11542 │
|
||||
│ Issue 9bf82c141d5a9c54bb1d6b4517eb3bb7da8fb30d │
|
||||
│ Author z6MknSL…StBU8Vi (you) │
|
||||
│ Labels bug, good-first-issue │
|
||||
│ Status open │
|
||||
|
|
@ -23,16 +23,16 @@ $ rad issue show 42028af21fabc09bfac2f25490f119f7c7e11542 --format header
|
|||
Untagging an issue is very similar:
|
||||
|
||||
```
|
||||
$ rad unlabel 42028af21fabc09bfac2f25490f119f7c7e11542 good-first-issue
|
||||
$ rad unlabel 9bf82c141d5a9c54bb1d6b4517eb3bb7da8fb30d good-first-issue
|
||||
```
|
||||
|
||||
Notice that the `good-first-issue` label has disappeared:
|
||||
|
||||
```
|
||||
$ rad issue show 42028af21fabc09bfac2f25490f119f7c7e11542 --format header
|
||||
$ rad issue show 9bf82c141d5a9c54bb1d6b4517eb3bb7da8fb30d --format header
|
||||
╭─────────────────────────────────────────────────────────╮
|
||||
│ Title flux capacitor underpowered │
|
||||
│ Issue 42028af21fabc09bfac2f25490f119f7c7e11542 │
|
||||
│ Issue 9bf82c141d5a9c54bb1d6b4517eb3bb7da8fb30d │
|
||||
│ Author z6MknSL…StBU8Vi (you) │
|
||||
│ Labels bug │
|
||||
│ Status open │
|
||||
|
|
|
|||
|
|
@ -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 143bb0c962561b09e86478a53ba346b5ff934335 opened
|
||||
✓ Patch f6c96cca58521d6dbb6cd4e6b7124342b9a86945 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 143bb0c updated to e595bf1246bdcee7b0c20615e479f62d2bf02249
|
||||
✓ Patch 143bb0c962561b09e86478a53ba346b5ff934335 merged
|
||||
✓ Patch f6c96cc updated to [...]
|
||||
✓ Patch f6c96cca58521d6dbb6cd4e6b7124342b9a86945 merged
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
+ 20aa5dd...954bcdb feature/1 -> patches/143bb0c962561b09e86478a53ba346b5ff934335 (forced update)
|
||||
+ 20aa5dd...954bcdb feature/1 -> patches/f6c96cca58521d6dbb6cd4e6b7124342b9a86945 (forced update)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ First, let's create a patch.
|
|||
$ git checkout -b feature/1 -q
|
||||
$ git commit --allow-empty -q -m "First change"
|
||||
$ git push rad HEAD:refs/patches
|
||||
✓ Patch 143bb0c962561b09e86478a53ba346b5ff934335 opened
|
||||
✓ Patch f6c96cca58521d6dbb6cd4e6b7124342b9a86945 opened
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
* [new reference] HEAD -> refs/patches
|
||||
```
|
||||
|
|
@ -36,7 +36,7 @@ committer radicle <radicle@localhost> 1671125284 +0000
|
|||
Finally, we push master and expect the patch to be merged.
|
||||
``` (stderr) RAD_SOCKET=/dev/null
|
||||
$ git push rad master
|
||||
✓ Patch 143bb0c962561b09e86478a53ba346b5ff934335 merged
|
||||
✓ Patch f6c96cca58521d6dbb6cd4e6b7124342b9a86945 merged
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
f2de534..737a10c master -> master
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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 143bb0c962561b09e86478a53ba346b5ff934335 opened
|
||||
✓ Patch f6c96cca58521d6dbb6cd4e6b7124342b9a86945 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 5d0e608aa35af59f769e9d6a2c0227ea60ae2740 opened
|
||||
✓ Patch 3b8203713e2945a6c46b238e6a432bd2711d3ccf 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/143bb0c962561b09e86478a53ba346b5ff934335
|
||||
rad/patches/5d0e608aa35af59f769e9d6a2c0227ea60ae2740
|
||||
rad/patches/3b8203713e2945a6c46b238e6a432bd2711d3ccf
|
||||
rad/patches/f6c96cca58521d6dbb6cd4e6b7124342b9a86945
|
||||
```
|
||||
|
||||
And some remote refs:
|
||||
|
|
@ -33,14 +33,16 @@ $ rad inspect --refs
|
|||
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
└── refs
|
||||
├── cobs
|
||||
│ ├── xyz.radicle.id
|
||||
│ │ └── 2317f74de0494c489a233ca6f29f2b8bff6d4f15
|
||||
│ └── xyz.radicle.patch
|
||||
│ ├── 143bb0c962561b09e86478a53ba346b5ff934335
|
||||
│ └── 5d0e608aa35af59f769e9d6a2c0227ea60ae2740
|
||||
│ ├── 3b8203713e2945a6c46b238e6a432bd2711d3ccf
|
||||
│ └── f6c96cca58521d6dbb6cd4e6b7124342b9a86945
|
||||
├── heads
|
||||
│ ├── master
|
||||
│ └── patches
|
||||
│ ├── 143bb0c962561b09e86478a53ba346b5ff934335
|
||||
│ └── 5d0e608aa35af59f769e9d6a2c0227ea60ae2740
|
||||
│ ├── 3b8203713e2945a6c46b238e6a432bd2711d3ccf
|
||||
│ └── f6c96cca58521d6dbb6cd4e6b7124342b9a86945
|
||||
└── rad
|
||||
├── id
|
||||
└── sigrefs
|
||||
|
|
@ -59,8 +61,8 @@ When we push to `rad/master`, we automatically merge the patches:
|
|||
|
||||
``` (stderr) RAD_SOCKET=/dev/null
|
||||
$ git push rad master
|
||||
✓ Patch 143bb0c962561b09e86478a53ba346b5ff934335 merged
|
||||
✓ Patch 5d0e608aa35af59f769e9d6a2c0227ea60ae2740 merged
|
||||
✓ Patch 3b8203713e2945a6c46b238e6a432bd2711d3ccf merged
|
||||
✓ Patch f6c96cca58521d6dbb6cd4e6b7124342b9a86945 merged
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
f2de534..d6399c7 master -> master
|
||||
```
|
||||
|
|
@ -69,8 +71,8 @@ $ rad patch --merged
|
|||
╭─────────────────────────────────────────────────────────────────────────╮
|
||||
│ ● ID Title Author Head + - Updated │
|
||||
├─────────────────────────────────────────────────────────────────────────┤
|
||||
│ ✔ 143bb0c First change alice (you) 20aa5dd +0 -0 [ ... ] │
|
||||
│ ✔ 5d0e608 Second change alice (you) daf349f +0 -0 [ ... ] │
|
||||
│ ✔ [ ... ] Second change alice (you) daf349f +0 -0 [ ... ] │
|
||||
│ ✔ [ ... ] First change alice (you) 20aa5dd +0 -0 [ ... ] │
|
||||
╰─────────────────────────────────────────────────────────────────────────╯
|
||||
```
|
||||
|
||||
|
|
@ -88,9 +90,11 @@ $ rad inspect --refs
|
|||
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
└── refs
|
||||
├── cobs
|
||||
│ ├── xyz.radicle.id
|
||||
│ │ └── 2317f74de0494c489a233ca6f29f2b8bff6d4f15
|
||||
│ └── xyz.radicle.patch
|
||||
│ ├── 143bb0c962561b09e86478a53ba346b5ff934335
|
||||
│ └── 5d0e608aa35af59f769e9d6a2c0227ea60ae2740
|
||||
│ ├── 3b8203713e2945a6c46b238e6a432bd2711d3ccf
|
||||
│ └── f6c96cca58521d6dbb6cd4e6b7124342b9a86945
|
||||
├── 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 69ebafb6f654fb29d23f630cc165d83d6cbf525c opened
|
||||
✓ Patch 71e51dfcf7ca124a75ec6e0cb21b13bf86b8bb2e opened
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
* [new reference] feature/1 -> refs/patches
|
||||
```
|
||||
|
|
@ -48,17 +48,17 @@ $ rad patch list
|
|||
╭─────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ● ID Title Author Head + - Updated │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ● 69ebafb Add Alan z6MknSL…StBU8Vi (you) 5c88a79 +1 -0 [ ... ] │
|
||||
│ ● 71e51df 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 69ebafb
|
||||
$ rad patch show -v -p 71e51df
|
||||
╭────────────────────────────────────────────────────╮
|
||||
│ Title Add Alan │
|
||||
│ Patch 69ebafb6f654fb29d23f630cc165d83d6cbf525c │
|
||||
│ Patch 71e51dfcf7ca124a75ec6e0cb21b13bf86b8bb2e │
|
||||
│ Author z6MknSL…StBU8Vi (you) │
|
||||
│ 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 53d5f17aba5fd9b7de7a02ecb6f01de561701eeb opened
|
||||
✓ Patch 364cc2809f14c1bc74a8868159e87eb3844eb7e2 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 53d5f17aba5fd9b7de7a02ecb6f01de561701eeb
|
||||
$ rad patch show -v 364cc2809f14c1bc74a8868159e87eb3844eb7e2
|
||||
╭────────────────────────────────────────────────────╮
|
||||
│ Title Add Mel │
|
||||
│ Patch 53d5f17aba5fd9b7de7a02ecb6f01de561701eeb │
|
||||
│ Patch 364cc2809f14c1bc74a8868159e87eb3844eb7e2 │
|
||||
│ Author z6MknSL…StBU8Vi (you) │
|
||||
│ 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 459dc67a024ff30c3bca02f0f1e5b746459ce32a opened
|
||||
✓ Patch 11ab7fbec82c3aed393d7a696d6b3c7714735056 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 459dc67a024ff30c3bca02f0f1e5b746459ce32a
|
||||
$ rad patch show -v 11ab7fbec82c3aed393d7a696d6b3c7714735056
|
||||
╭────────────────────────────────────────────────────╮
|
||||
│ Title Add Mel #2 │
|
||||
│ Patch 459dc67a024ff30c3bca02f0f1e5b746459ce32a │
|
||||
│ Patch 11ab7fbec82c3aed393d7a696d6b3c7714735056 │
|
||||
│ Author z6MknSL…StBU8Vi (you) │
|
||||
│ 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 79a1a5138b7f91c6dead5544ecde285dc3d0cb45 drafted
|
||||
✓ Patch 78fcb007b4a3a898379f1e220d4b9fb54ad04cfc 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 79a1a5138b7f91c6dead5544ecde285dc3d0cb45
|
||||
$ rad patch show 78fcb007b4a3a898379f1e220d4b9fb54ad04cfc
|
||||
╭────────────────────────────────────────────────────╮
|
||||
│ Title Nothing yet │
|
||||
│ Patch 79a1a5138b7f91c6dead5544ecde285dc3d0cb45 │
|
||||
│ Patch 78fcb007b4a3a898379f1e220d4b9fb54ad04cfc │
|
||||
│ Author z6MknSL…StBU8Vi (you) │
|
||||
│ Head 2a465832b5a76abe25be44a3a5d224bbd7741ba7 │
|
||||
│ Branches cloudhead/draft │
|
||||
|
|
@ -36,14 +36,14 @@ $ rad patch show 79a1a5138b7f91c6dead5544ecde285dc3d0cb45
|
|||
Once the patch is ready for review, we can use the `ready` command:
|
||||
|
||||
```
|
||||
$ rad patch ready 79a1a5138b7f91c6dead5544ecde285dc3d0cb45
|
||||
$ rad patch ready 78fcb007b4a3a898379f1e220d4b9fb54ad04cfc
|
||||
```
|
||||
|
||||
```
|
||||
$ rad patch show 79a1a5138b7f91c6dead5544ecde285dc3d0cb45
|
||||
$ rad patch show 78fcb007b4a3a898379f1e220d4b9fb54ad04cfc
|
||||
╭────────────────────────────────────────────────────╮
|
||||
│ Title Nothing yet │
|
||||
│ Patch 79a1a5138b7f91c6dead5544ecde285dc3d0cb45 │
|
||||
│ Patch 78fcb007b4a3a898379f1e220d4b9fb54ad04cfc │
|
||||
│ Author z6MknSL…StBU8Vi (you) │
|
||||
│ 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 79a1a5138b7f91c6dead5544ecde285dc3d0cb45
|
||||
$ rad patch show 79a1a5138b7f91c6dead5544ecde285dc3d0cb45
|
||||
$ rad patch ready --undo 78fcb007b4a3a898379f1e220d4b9fb54ad04cfc
|
||||
$ rad patch show 78fcb007b4a3a898379f1e220d4b9fb54ad04cfc
|
||||
╭────────────────────────────────────────────────────╮
|
||||
│ Title Nothing yet │
|
||||
│ Patch 79a1a5138b7f91c6dead5544ecde285dc3d0cb45 │
|
||||
│ Patch 78fcb007b4a3a898379f1e220d4b9fb54ad04cfc │
|
||||
│ Author z6MknSL…StBU8Vi (you) │
|
||||
│ Head 2a465832b5a76abe25be44a3a5d224bbd7741ba7 │
|
||||
│ Branches cloudhead/draft │
|
||||
|
|
|
|||
|
|
@ -49,22 +49,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 26e3e563ddc7df8dd0c9f81274c0b3cb1b764568 opened
|
||||
✓ Patch 6d260fc8388e74d8fefb5dabc5a798e125ec3cf9 opened
|
||||
✓ Synced with 1 node(s)
|
||||
To rad://zhbMU4DUXrzB8xT6qAJh6yZ7bFMK/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
|
||||
* [new reference] HEAD -> refs/patches
|
||||
```
|
||||
``` ~bob
|
||||
$ git status --short --branch
|
||||
## bob/feature...rad/patches/26e3e563ddc7df8dd0c9f81274c0b3cb1b764568
|
||||
## bob/feature...rad/patches/6d260fc8388e74d8fefb5dabc5a798e125ec3cf9
|
||||
```
|
||||
|
||||
Alice checks it out.
|
||||
|
||||
``` ~alice
|
||||
$ rad patch checkout 26e3e56
|
||||
✓ Switched to branch patch/26e3e56
|
||||
✓ Branch patch/26e3e56 setup to track rad/patches/26e3e563ddc7df8dd0c9f81274c0b3cb1b764568
|
||||
$ rad patch checkout 6d260fc8388e74d8fefb5dabc5a798e125ec3cf9
|
||||
✓ Switched to branch patch/6d260fc
|
||||
✓ Branch patch/6d260fc setup to track rad/patches/6d260fc8388e74d8fefb5dabc5a798e125ec3cf9
|
||||
$ git show
|
||||
commit bdcdb30b3c0f513620dd0f1c24ff8f4f71de956b
|
||||
Author: radicle <radicle@localhost>
|
||||
|
|
@ -78,19 +78,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 26e3e56 updated to c04ef81bad734c65a7d5834cefcdd60c4f0484f7
|
||||
✓ Patch 6d260fc updated to 750081b35a3f831f428653bd2240eb4674ccae71
|
||||
✓ Synced with 1 node(s)
|
||||
To rad://zhbMU4DUXrzB8xT6qAJh6yZ7bFMK/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
|
||||
bdcdb30..cad2666 bob/feature -> patches/26e3e563ddc7df8dd0c9f81274c0b3cb1b764568
|
||||
bdcdb30..cad2666 bob/feature -> patches/6d260fc8388e74d8fefb5dabc5a798e125ec3cf9
|
||||
```
|
||||
|
||||
Alice pulls the update.
|
||||
|
||||
``` ~alice
|
||||
$ rad patch show 26e3e56
|
||||
$ rad patch show 6d260fc
|
||||
╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ Title Bob's patch │
|
||||
│ Patch 26e3e563ddc7df8dd0c9f81274c0b3cb1b764568 │
|
||||
│ Patch 6d260fc8388e74d8fefb5dabc5a798e125ec3cf9 │
|
||||
│ Author bob z6Mkt67…v4N1tRk │
|
||||
│ Head cad2666a8a2250e4dee175ed5044be2c251ff08b │
|
||||
│ Commits ahead 2, behind 0 │
|
||||
|
|
@ -100,16 +100,16 @@ $ rad patch show 26e3e56
|
|||
│ bdcdb30 Bob's commit #1 │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ● opened by bob z6Mkt67…v4N1tRk [ ... ] │
|
||||
│ ↑ updated to c04ef81bad734c65a7d5834cefcdd60c4f0484f7 (cad2666) [ ... ] │
|
||||
│ ↑ updated to 750081b35a3f831f428653bd2240eb4674ccae71 (cad2666) [ ... ] │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯
|
||||
$ git ls-remote rad
|
||||
f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354 refs/heads/master
|
||||
cad2666a8a2250e4dee175ed5044be2c251ff08b refs/heads/patches/26e3e563ddc7df8dd0c9f81274c0b3cb1b764568
|
||||
cad2666a8a2250e4dee175ed5044be2c251ff08b refs/heads/patches/6d260fc8388e74d8fefb5dabc5a798e125ec3cf9
|
||||
```
|
||||
``` ~alice
|
||||
$ git fetch rad
|
||||
$ git status --short --branch
|
||||
## patch/26e3e56...rad/patches/26e3e563ddc7df8dd0c9f81274c0b3cb1b764568 [behind 1]
|
||||
## patch/6d260fc...rad/patches/6d260fc8388e74d8fefb5dabc5a798e125ec3cf9 [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 ea6fa6c274c55d0f4fdf203a192cbf1330b51221 opened
|
||||
✓ Patch 2541d346ba0b9377b3d38852dfded43f23833fc1 opened
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
* [new reference] HEAD -> refs/patches
|
||||
```
|
||||
|
||||
```
|
||||
$ rad patch show ea6fa6c274c55d0f4fdf203a192cbf1330b51221
|
||||
$ rad patch show 2541d346ba0b9377b3d38852dfded43f23833fc1
|
||||
╭────────────────────────────────────────────────────╮
|
||||
│ Title Not a real change │
|
||||
│ Patch ea6fa6c274c55d0f4fdf203a192cbf1330b51221 │
|
||||
│ Patch 2541d346ba0b9377b3d38852dfded43f23833fc1 │
|
||||
│ Author z6MknSL…StBU8Vi (you) │
|
||||
│ 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 ea6fa6c274c55d0f4fdf203a192cbf1330b51221 -m "Updated patch"
|
||||
59bbb5c5d3c9f18a686113e6354b1372eebafda4
|
||||
$ rad patch update 2541d346ba0b9377b3d38852dfded43f23833fc1 -m "Updated patch"
|
||||
d9c9ef902f2957d746bb53e744e69a5c3aa564bc
|
||||
```
|
||||
|
||||
The command outputs the new Revision ID, which we can now see here:
|
||||
|
||||
```
|
||||
$ rad patch show ea6fa6c274c55d0f4fdf203a192cbf1330b51221
|
||||
$ rad patch show 2541d346ba0b9377b3d38852dfded43f23833fc1
|
||||
╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ Title Not a real change │
|
||||
│ Patch ea6fa6c274c55d0f4fdf203a192cbf1330b51221 │
|
||||
│ Patch 2541d346ba0b9377b3d38852dfded43f23833fc1 │
|
||||
│ Author z6MknSL…StBU8Vi (you) │
|
||||
│ Head 4d272148458a17620541555b1f0905c01658aa9f │
|
||||
│ Branches feature/1 │
|
||||
|
|
@ -67,6 +67,6 @@ $ rad patch show ea6fa6c274c55d0f4fdf203a192cbf1330b51221
|
|||
│ 51b2f0f Not a real change │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ● opened by z6MknSL…StBU8Vi (you) [ ... ] │
|
||||
│ ↑ updated to 59bbb5c5d3c9f18a686113e6354b1372eebafda4 (4d27214) [ ... ] │
|
||||
│ ↑ updated to d9c9ef902f2957d746bb53e744e69a5c3aa564bc (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 90c77f2c33b7e472e058de4a586156f8a7fec7d6 opened
|
||||
✓ Patch e49e64637ab4a29e5a16c73000dacd2afa918d9d 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 90c77f2
|
||||
$ rad patch show e49e64637ab4a29e5a16c73000dacd2afa918d9d
|
||||
╭────────────────────────────────────────────────────╮
|
||||
│ Title Add things #1 │
|
||||
│ Patch 90c77f2c33b7e472e058de4a586156f8a7fec7d6 │
|
||||
│ Patch e49e64637ab4a29e5a16c73000dacd2afa918d9d │
|
||||
│ Author z6MknSL…StBU8Vi (you) │
|
||||
│ Head 42d894a83c9c356552a57af09ccdbd5587a99045 │
|
||||
│ Branches feature/1 │
|
||||
|
|
@ -39,7 +39,7 @@ branch associated with this patch:
|
|||
|
||||
```
|
||||
$ git branch -vv
|
||||
* feature/1 42d894a [rad/patches/90c77f2c33b7e472e058de4a586156f8a7fec7d6] Add things
|
||||
* feature/1 42d894a [rad/patches/e49e64637ab4a29e5a16c73000dacd2afa918d9d] 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/90c77f2c33b7e472e058de4a586156f8a7fec7d6
|
||||
## feature/1...rad/patches/e49e64637ab4a29e5a16c73000dacd2afa918d9d
|
||||
$ git fetch
|
||||
$ git push
|
||||
```
|
||||
|
|
@ -59,13 +59,14 @@ $ git show-ref
|
|||
42d894a83c9c356552a57af09ccdbd5587a99045 refs/heads/feature/1
|
||||
f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354 refs/heads/master
|
||||
f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354 refs/remotes/rad/master
|
||||
42d894a83c9c356552a57af09ccdbd5587a99045 refs/remotes/rad/patches/90c77f2c33b7e472e058de4a586156f8a7fec7d6
|
||||
42d894a83c9c356552a57af09ccdbd5587a99045 refs/remotes/rad/patches/e49e64637ab4a29e5a16c73000dacd2afa918d9d
|
||||
```
|
||||
```
|
||||
$ git ls-remote rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji 'refs/heads/patches/*'
|
||||
42d894a83c9c356552a57af09ccdbd5587a99045 refs/heads/patches/90c77f2c33b7e472e058de4a586156f8a7fec7d6
|
||||
42d894a83c9c356552a57af09ccdbd5587a99045 refs/heads/patches/e49e64637ab4a29e5a16c73000dacd2afa918d9d
|
||||
$ git ls-remote rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi 'refs/cobs/*'
|
||||
90c77f2c33b7e472e058de4a586156f8a7fec7d6 refs/cobs/xyz.radicle.patch/90c77f2c33b7e472e058de4a586156f8a7fec7d6
|
||||
2317f74de0494c489a233ca6f29f2b8bff6d4f15 refs/cobs/xyz.radicle.id/2317f74de0494c489a233ca6f29f2b8bff6d4f15
|
||||
e49e64637ab4a29e5a16c73000dacd2afa918d9d refs/cobs/xyz.radicle.patch/e49e64637ab4a29e5a16c73000dacd2afa918d9d
|
||||
```
|
||||
|
||||
We can create another patch:
|
||||
|
|
@ -74,7 +75,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 fedf0e4dcb74ff6db1d5e30a6a254b77f02ff60b opened
|
||||
✓ Patch b1fd7b6883dca2ef11e0e486a7097e759ea90cdb opened
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
* [new reference] HEAD -> refs/patches
|
||||
```
|
||||
|
|
@ -83,8 +84,8 @@ We see both branches with upstreams now:
|
|||
|
||||
```
|
||||
$ git branch -vv
|
||||
feature/1 42d894a [rad/patches/90c77f2c33b7e472e058de4a586156f8a7fec7d6] Add things
|
||||
* feature/2 8b0ea80 [rad/patches/fedf0e4dcb74ff6db1d5e30a6a254b77f02ff60b] Add more things
|
||||
feature/1 42d894a [rad/patches/e49e64637ab4a29e5a16c73000dacd2afa918d9d] Add things
|
||||
* feature/2 8b0ea80 [rad/patches/b1fd7b6883dca2ef11e0e486a7097e759ea90cdb] Add more things
|
||||
master f2de534 [rad/master] Second commit
|
||||
```
|
||||
|
||||
|
|
@ -95,8 +96,8 @@ $ rad patch
|
|||
╭────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ● ID Title Author Head + - Updated │
|
||||
├────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ● 90c77f2 Add things #1 z6MknSL…StBU8Vi (you) 42d894a +0 -0 [ ... ] │
|
||||
│ ● fedf0e4 Add more things z6MknSL…StBU8Vi (you) 8b0ea80 +0 -0 [ ... ] │
|
||||
│ ● b1fd7b6 Add more things z6MknSL…StBU8Vi (you) 8b0ea80 +0 -0 [ ... ] │
|
||||
│ ● e49e646 Add things #1 z6MknSL…StBU8Vi (you) 42d894a +0 -0 [ ... ] │
|
||||
╰────────────────────────────────────────────────────────────────────────────────────╯
|
||||
```
|
||||
|
||||
|
|
@ -108,9 +109,9 @@ $ git commit -a -m "Improve code" -q --allow-empty
|
|||
|
||||
``` (stderr)
|
||||
$ git push
|
||||
✓ Patch fedf0e4 updated to d0018fcc21d87c91a1ff9155aed6b4e57535566b
|
||||
✓ Patch b1fd7b6 updated to c867846b9f294c271e8934820dfac2c5924ecd5a
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
8b0ea80..02bef3f feature/2 -> patches/fedf0e4dcb74ff6db1d5e30a6a254b77f02ff60b
|
||||
8b0ea80..02bef3f feature/2 -> patches/b1fd7b6883dca2ef11e0e486a7097e759ea90cdb
|
||||
```
|
||||
|
||||
This last `git push` worked without specifying an upstream branch despite the
|
||||
|
|
@ -128,10 +129,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 fedf0e4
|
||||
$ rad patch show b1fd7b6
|
||||
╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ Title Add more things │
|
||||
│ Patch fedf0e4dcb74ff6db1d5e30a6a254b77f02ff60b │
|
||||
│ Patch b1fd7b6883dca2ef11e0e486a7097e759ea90cdb │
|
||||
│ Author z6MknSL…StBU8Vi (you) │
|
||||
│ Head 02bef3fac41b2f98bb3c02b868a53ddfecb55b5f │
|
||||
│ Branches feature/2 │
|
||||
|
|
@ -142,7 +143,7 @@ $ rad patch show fedf0e4
|
|||
│ 8b0ea80 Add more things │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ● opened by z6MknSL…StBU8Vi (you) [ ... ] │
|
||||
│ ↑ updated to d0018fcc21d87c91a1ff9155aed6b4e57535566b (02bef3f) [ ... ] │
|
||||
│ ↑ updated to c867846b9f294c271e8934820dfac2c5924ecd5a (02bef3f) [ ... ] │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯
|
||||
```
|
||||
|
||||
|
|
@ -155,14 +156,14 @@ $ git rev-parse HEAD
|
|||
|
||||
```
|
||||
$ git status --short --branch
|
||||
## feature/2...rad/patches/fedf0e4dcb74ff6db1d5e30a6a254b77f02ff60b
|
||||
## feature/2...rad/patches/b1fd7b6883dca2ef11e0e486a7097e759ea90cdb
|
||||
```
|
||||
|
||||
```
|
||||
$ git rev-parse refs/remotes/rad/patches/fedf0e4dcb74ff6db1d5e30a6a254b77f02ff60b
|
||||
$ git rev-parse refs/remotes/rad/patches/b1fd7b6883dca2ef11e0e486a7097e759ea90cdb
|
||||
02bef3fac41b2f98bb3c02b868a53ddfecb55b5f
|
||||
$ git ls-remote rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi refs/heads/patches/fedf0e4dcb74ff6db1d5e30a6a254b77f02ff60b
|
||||
02bef3fac41b2f98bb3c02b868a53ddfecb55b5f refs/heads/patches/fedf0e4dcb74ff6db1d5e30a6a254b77f02ff60b
|
||||
$ git ls-remote rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi refs/heads/patches/b1fd7b6883dca2ef11e0e486a7097e759ea90cdb
|
||||
02bef3fac41b2f98bb3c02b868a53ddfecb55b5f refs/heads/patches/b1fd7b6883dca2ef11e0e486a7097e759ea90cdb
|
||||
```
|
||||
|
||||
## Force push
|
||||
|
|
@ -183,7 +184,7 @@ Now let's push to the patch head.
|
|||
``` (stderr) (fail)
|
||||
$ git push
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
! [rejected] feature/2 -> patches/fedf0e4dcb74ff6db1d5e30a6a254b77f02ff60b (non-fast-forward)
|
||||
! [rejected] feature/2 -> patches/b1fd7b6883dca2ef11e0e486a7097e759ea90cdb (non-fast-forward)
|
||||
error: failed to push some refs to 'rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi'
|
||||
hint: [..]
|
||||
hint: [..]
|
||||
|
|
@ -196,18 +197,18 @@ use `--force` to force the update.
|
|||
|
||||
``` (stderr)
|
||||
$ git push --force
|
||||
✓ Patch fedf0e4 updated to 31ecf28817c44d90686b5c3c624c1f4a534b6478
|
||||
✓ Patch b1fd7b6 updated to cf4d8577a1ec8aaa21a7ccca67ad8627c3304024
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
+ 02bef3f...9304dbc feature/2 -> patches/fedf0e4dcb74ff6db1d5e30a6a254b77f02ff60b (forced update)
|
||||
+ 02bef3f...9304dbc feature/2 -> patches/b1fd7b6883dca2ef11e0e486a7097e759ea90cdb (forced update)
|
||||
```
|
||||
|
||||
That worked. We can see the new revision if we call `rad patch show`:
|
||||
|
||||
```
|
||||
$ rad patch show fedf0e4
|
||||
$ rad patch show b1fd7b6
|
||||
╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ Title Add more things │
|
||||
│ Patch fedf0e4dcb74ff6db1d5e30a6a254b77f02ff60b │
|
||||
│ Patch b1fd7b6883dca2ef11e0e486a7097e759ea90cdb │
|
||||
│ Author z6MknSL…StBU8Vi (you) │
|
||||
│ Head 9304dbc445925187994a7a93222a3f8bde73b785 │
|
||||
│ Branches feature/2 │
|
||||
|
|
@ -218,8 +219,8 @@ $ rad patch show fedf0e4
|
|||
│ 8b0ea80 Add more things │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ● opened by z6MknSL…StBU8Vi (you) [ ... ] │
|
||||
│ ↑ updated to d0018fcc21d87c91a1ff9155aed6b4e57535566b (02bef3f) [ ... ] │
|
||||
│ ↑ updated to 31ecf28817c44d90686b5c3c624c1f4a534b6478 (9304dbc) [ ... ] │
|
||||
│ ↑ updated to c867846b9f294c271e8934820dfac2c5924ecd5a (02bef3f) [ ... ] │
|
||||
│ ↑ updated to cf4d8577a1ec8aaa21a7ccca67ad8627c3304024 (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 73b73f376e93e09e0419664766ac9e433bf7d389 opened
|
||||
✓ Patch a8926643a8f6a65bc386b0131621994000485d4d opened
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
* [new reference] HEAD -> refs/patches
|
||||
```
|
||||
|
|
@ -38,14 +38,14 @@ $ rad patch
|
|||
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ● ID Title Author Head + - Updated │
|
||||
├──────────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ● 73b73f3 Define power requirements z6MknSL…StBU8Vi (you) 3e674d1 +0 -0 [ ... ] │
|
||||
│ ● a892664 Define power requirements z6MknSL…StBU8Vi (you) 3e674d1 +0 -0 [ ... ] │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
```
|
||||
```
|
||||
$ rad patch show 73b73f376e93e09e0419664766ac9e433bf7d389 -p
|
||||
$ rad patch show a8926643a8f6a65bc386b0131621994000485d4d -p
|
||||
╭────────────────────────────────────────────────────╮
|
||||
│ Title Define power requirements │
|
||||
│ Patch 73b73f376e93e09e0419664766ac9e433bf7d389 │
|
||||
│ Patch a8926643a8f6a65bc386b0131621994000485d4d │
|
||||
│ Author z6MknSL…StBU8Vi (you) │
|
||||
│ 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/73b73f376e93e09e0419664766ac9e433bf7d389] Define power requirements
|
||||
* flux-capacitor-power 3e674d1 [rad/patches/a8926643a8f6a65bc386b0131621994000485d4d] Define power requirements
|
||||
master f2de534 [rad/master] Second commit
|
||||
```
|
||||
|
||||
|
|
@ -90,48 +90,48 @@ $ git commit --message "Add README, just for the fun"
|
|||
```
|
||||
``` (stderr)
|
||||
$ git push rad -o patch.message="Add README, just for the fun"
|
||||
✓ Patch 73b73f3 updated to 5605784ae81dad91ba47ea55e19dd16f6280d44b
|
||||
✓ Patch a892664 updated to 8d8aa0887a11f2a37fa8ed0d5723efa96fd727ed
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
3e674d1..27857ec flux-capacitor-power -> patches/73b73f376e93e09e0419664766ac9e433bf7d389
|
||||
3e674d1..27857ec flux-capacitor-power -> patches/a8926643a8f6a65bc386b0131621994000485d4d
|
||||
```
|
||||
|
||||
And let's leave a quick comment for our team:
|
||||
|
||||
```
|
||||
$ rad patch comment 73b73f376e93e09e0419664766ac9e433bf7d389 --message 'I cannot wait to get back to the 90s!'
|
||||
$ rad patch comment a8926643a8f6a65bc386b0131621994000485d4d --message 'I cannot wait to get back to the 90s!'
|
||||
╭────────────────────────────────────────────╮
|
||||
│ z6MknSL…StBU8Vi (you) [ ... ] 5c418a5 │
|
||||
│ z6MknSL…StBU8Vi (you) [ ... ] b97a27f │
|
||||
│ I cannot wait to get back to the 90s! │
|
||||
╰────────────────────────────────────────────╯
|
||||
$ rad patch comment 73b73f376e93e09e0419664766ac9e433bf7d389 --message 'My favorite decade!' --reply-to 5c418a5 -q
|
||||
729cdf63ce4793ab3cabffbe0dce24db16e45549
|
||||
$ rad patch comment a8926643a8f6a65bc386b0131621994000485d4d --message 'My favorite decade!' --reply-to b97a27f -q
|
||||
a3a462bc8ab52e2a6f3568c28a11ba53cf40bbc8
|
||||
```
|
||||
|
||||
Now, let's checkout the patch that we just created:
|
||||
|
||||
```
|
||||
$ rad patch checkout 73b73f3
|
||||
✓ Switched to branch patch/73b73f3
|
||||
✓ Branch patch/73b73f3 setup to track rad/patches/73b73f376e93e09e0419664766ac9e433bf7d389
|
||||
$ rad patch checkout a892664
|
||||
✓ Switched to branch patch/a892664
|
||||
✓ Branch patch/a892664 setup to track rad/patches/a8926643a8f6a65bc386b0131621994000485d4d
|
||||
```
|
||||
|
||||
We can also add a review verdict as such:
|
||||
|
||||
```
|
||||
$ rad review 73b73f376e93e09e0419664766ac9e433bf7d389 --accept --no-message --no-sync
|
||||
✓ Patch 73b73f3 accepted
|
||||
$ rad review a8926643a8f6a65bc386b0131621994000485d4d --accept --no-message --no-sync
|
||||
✓ Patch a892664 accepted
|
||||
```
|
||||
|
||||
Showing the patch list now will reveal the favorable verdict:
|
||||
|
||||
```
|
||||
$ rad patch show 73b73f3
|
||||
$ rad patch show a892664
|
||||
╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ Title Define power requirements │
|
||||
│ Patch 73b73f376e93e09e0419664766ac9e433bf7d389 │
|
||||
│ Patch a8926643a8f6a65bc386b0131621994000485d4d │
|
||||
│ Author z6MknSL…StBU8Vi (you) │
|
||||
│ Head 27857ec9eb04c69cacab516e8bf4b5fd36090f66 │
|
||||
│ Branches flux-capacitor-power, patch/73b73f3 │
|
||||
│ Branches flux-capacitor-power, patch/a892664 │
|
||||
│ Commits ahead 2, behind 0 │
|
||||
│ Status open │
|
||||
│ │
|
||||
|
|
@ -141,7 +141,7 @@ $ rad patch show 73b73f3
|
|||
│ 3e674d1 Define power requirements │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ● opened by z6MknSL…StBU8Vi (you) [ ... ] │
|
||||
│ ↑ updated to 5605784ae81dad91ba47ea55e19dd16f6280d44b (27857ec) [ ... ] │
|
||||
│ ↑ updated to 8d8aa0887a11f2a37fa8ed0d5723efa96fd727ed (27857ec) [ ... ] │
|
||||
│ ✓ accepted by z6MknSL…StBU8Vi (you) [ ... ] │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯
|
||||
```
|
||||
|
|
@ -149,14 +149,14 @@ $ rad patch show 73b73f3
|
|||
If you make a mistake on the patch description, you can always change it!
|
||||
|
||||
```
|
||||
$ rad patch edit 73b73f3 --message "Define power requirements" --message "Add requirements file"
|
||||
$ rad patch show 73b73f3
|
||||
$ rad patch edit a892664 --message "Define power requirements" --message "Add requirements file"
|
||||
$ rad patch show a892664
|
||||
╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ Title Define power requirements │
|
||||
│ Patch 73b73f376e93e09e0419664766ac9e433bf7d389 │
|
||||
│ Patch a8926643a8f6a65bc386b0131621994000485d4d │
|
||||
│ Author z6MknSL…StBU8Vi (you) │
|
||||
│ Head 27857ec9eb04c69cacab516e8bf4b5fd36090f66 │
|
||||
│ Branches flux-capacitor-power, patch/73b73f3 │
|
||||
│ Branches flux-capacitor-power, patch/a892664 │
|
||||
│ Commits ahead 2, behind 0 │
|
||||
│ Status open │
|
||||
│ │
|
||||
|
|
@ -166,7 +166,7 @@ $ rad patch show 73b73f3
|
|||
│ 3e674d1 Define power requirements │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ● opened by z6MknSL…StBU8Vi (you) [ ... ] │
|
||||
│ ↑ updated to 5605784ae81dad91ba47ea55e19dd16f6280d44b (27857ec) [ ... ] │
|
||||
│ ↑ updated to 8d8aa0887a11f2a37fa8ed0d5723efa96fd727ed (27857ec) [ ... ] │
|
||||
│ ✓ accepted by z6MknSL…StBU8Vi (you) [ ... ] │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯
|
||||
```
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ $ git commit -q -m "Update files"
|
|||
|
||||
``` (stderr)
|
||||
$ git push rad HEAD:refs/patches
|
||||
✓ Patch 7f6ed9bd562a36eb1d5689f95600d09247726a23 opened
|
||||
✓ Patch 4f4fdb0fbb3327975b92fd1fa88e7427b1b141e2 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 7f6ed9bd562a36eb1d5689f95600d09247726a23
|
||||
$ rad review --no-sync --patch -U5 4f4fdb0fbb3327975b92fd1fa88e7427b1b141e2
|
||||
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 7f6ed9bd562a36eb1d5689f95600d09247726a23
|
||||
✓ Loaded existing review ([..]) for patch 7f6ed9bd562a36eb1d5689f95600d09247726a23
|
||||
$ rad review --no-sync --patch --accept --hunk 1 4f4fdb0fbb3327975b92fd1fa88e7427b1b141e2
|
||||
✓ Loaded existing review ([..]) for patch 4f4fdb0fbb3327975b92fd1fa88e7427b1b141e2
|
||||
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 7f6ed9bd562a36eb1d5689f95600d09247726a23
|
||||
✓ Loaded existing review ([..]) for patch 7f6ed9bd562a36eb1d5689f95600d09247726a23
|
||||
$ rad review --no-sync --patch --accept --hunk 1 4f4fdb0fbb3327975b92fd1fa88e7427b1b141e2
|
||||
✓ Loaded existing review ([..]) for patch 4f4fdb0fbb3327975b92fd1fa88e7427b1b141e2
|
||||
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 7f6ed9bd562a36eb1d5689f95600d09247726a23
|
||||
✓ Loaded existing review ([..]) for patch 7f6ed9bd562a36eb1d5689f95600d09247726a23
|
||||
$ rad review --no-sync --patch --accept -U3 --hunk 1 4f4fdb0fbb3327975b92fd1fa88e7427b1b141e2
|
||||
✓ Loaded existing review ([..]) for patch 4f4fdb0fbb3327975b92fd1fa88e7427b1b141e2
|
||||
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 7f6ed9bd562a36eb1d5689f95600d09247726a23
|
||||
✓ Loaded existing review ([..]) for patch 7f6ed9bd562a36eb1d5689f95600d09247726a23
|
||||
$ rad review --no-sync --patch --accept -U3 --hunk 1 4f4fdb0fbb3327975b92fd1fa88e7427b1b141e2
|
||||
✓ Loaded existing review ([..]) for patch 4f4fdb0fbb3327975b92fd1fa88e7427b1b141e2
|
||||
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 7f6ed9bd562a36eb1d5689f95600d09247726a23
|
||||
✓ Loaded existing review ([..]) for patch 7f6ed9bd562a36eb1d5689f95600d09247726a23
|
||||
$ rad review --no-sync --patch --accept --hunk 1 4f4fdb0fbb3327975b92fd1fa88e7427b1b141e2
|
||||
✓ Loaded existing review ([..]) for patch 4f4fdb0fbb3327975b92fd1fa88e7427b1b141e2
|
||||
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 7f6ed9bd562a36eb1d5689f95600d09247726a23
|
||||
✓ Loaded existing review ([..]) for patch 7f6ed9bd562a36eb1d5689f95600d09247726a23
|
||||
$ rad review --no-sync --patch --accept --hunk 1 4f4fdb0fbb3327975b92fd1fa88e7427b1b141e2
|
||||
✓ Loaded existing review ([..]) for patch 4f4fdb0fbb3327975b92fd1fa88e7427b1b141e2
|
||||
✓ 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 2f6eb49efac492327f71437b6bfc01b49afa0981 │
|
||||
│ Issue d457c205de780d37d1c16efbe5333aed4662a6c3 │
|
||||
│ Author bob (you) │
|
||||
│ Status open │
|
||||
│ │
|
||||
|
|
@ -22,7 +22,7 @@ $ rad issue list
|
|||
╭────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ● ID Title Author Labels Assignees Opened │
|
||||
├────────────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ● 2f6eb49 flux capacitor underpowered bob (you) [ .. ] │
|
||||
│ ● d457c20 flux capacitor underpowered bob (you) [ .. ] │
|
||||
╰────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
```
|
||||
|
||||
|
|
@ -31,6 +31,6 @@ found an important detail about the car's power requirements. It will help
|
|||
whoever works on a fix.
|
||||
|
||||
```
|
||||
$ rad issue comment 2f6eb49efac492327f71437b6bfc01b49afa0981 --message 'The flux capacitor needs 1.21 Gigawatts' -q
|
||||
d2b50873009b93680698aef4f57f43f7e850b651
|
||||
$ rad issue comment d457c205de780d37d1c16efbe5333aed4662a6c3 --message 'The flux capacitor needs 1.21 Gigawatts' -q
|
||||
2e8b9538aedaea418e90e90d19e61edf3f022933
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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 69e881c606639691330051d7d8f013854f32fb87 opened
|
||||
✓ Patch 4bfb6fe940f815e3fcce6a2796e051df85db9fe1 opened
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
|
||||
* [new reference] HEAD -> refs/patches
|
||||
```
|
||||
|
|
@ -38,12 +38,12 @@ $ rad patch
|
|||
╭─────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ● ID Title Author Head + - Updated │
|
||||
├─────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ● 69e881c Define power requirements bob (you) 3e674d1 +0 -0 [ ... ] │
|
||||
│ ● 4bfb6fe Define power requirements bob (you) 3e674d1 +0 -0 [ ... ] │
|
||||
╰─────────────────────────────────────────────────────────────────────────────────────╯
|
||||
$ rad patch show 69e881c606639691330051d7d8f013854f32fb87
|
||||
$ rad patch show 4bfb6fe940f815e3fcce6a2796e051df85db9fe1
|
||||
╭────────────────────────────────────────────────────╮
|
||||
│ Title Define power requirements │
|
||||
│ Patch 69e881c606639691330051d7d8f013854f32fb87 │
|
||||
│ Patch 4bfb6fe940f815e3fcce6a2796e051df85db9fe1 │
|
||||
│ Author bob (you) │
|
||||
│ 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/69e881c606639691330051d7d8f013854f32fb87
|
||||
3e674d1a1df90807e934f9ae5da2591dd6848a33 refs/heads/patches/4bfb6fe940f815e3fcce6a2796e051df85db9fe1
|
||||
```
|
||||
|
||||
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 69e881c updated to dcf3e6dd97c95cf8653cbb8ce47df20d28eb1821
|
||||
✓ Patch 4bfb6fe updated to 7782e60eb51b6e852abb184b092249327354c625
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
|
||||
3e674d1..27857ec flux-capacitor-power -> patches/69e881c606639691330051d7d8f013854f32fb87
|
||||
3e674d1..27857ec flux-capacitor-power -> patches/4bfb6fe940f815e3fcce6a2796e051df85db9fe1
|
||||
```
|
||||
|
||||
And let's leave a quick comment for our team:
|
||||
|
||||
```
|
||||
$ rad patch comment 69e881c606639691330051d7d8f013854f32fb87 --message 'I cannot wait to get back to the 90s!' -q
|
||||
c758bd868bb7d6c8509ee9168b3876082a8e377c
|
||||
$ rad patch comment 4bfb6fe940f815e3fcce6a2796e051df85db9fe1 --message 'I cannot wait to get back to the 90s!' -q
|
||||
a3ba1df99fbd874affc790c95cd18607940f1a90
|
||||
```
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ $ git fetch bob
|
|||
✓ Synced with 1 peer(s)
|
||||
From rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
|
||||
* [new branch] master -> bob/master
|
||||
* [new branch] patches/69e881c606639691330051d7d8f013854f32fb87 -> bob/patches/69e881c606639691330051d7d8f013854f32fb87
|
||||
* [new branch] patches/4bfb6fe940f815e3fcce6a2796e051df85db9fe1 -> bob/patches/4bfb6fe940f815e3fcce6a2796e051df85db9fe1
|
||||
```
|
||||
|
||||
The contributor's changes are now visible to us.
|
||||
|
|
@ -30,12 +30,12 @@ The contributor's changes are now visible to us.
|
|||
```
|
||||
$ git branch -r
|
||||
bob/master
|
||||
bob/patches/69e881c606639691330051d7d8f013854f32fb87
|
||||
bob/patches/4bfb6fe940f815e3fcce6a2796e051df85db9fe1
|
||||
rad/master
|
||||
$ rad patch show 69e881c
|
||||
$ rad patch show 4bfb6fe
|
||||
╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ Title Define power requirements │
|
||||
│ Patch 69e881c606639691330051d7d8f013854f32fb87 │
|
||||
│ Patch 4bfb6fe940f815e3fcce6a2796e051df85db9fe1 │
|
||||
│ Author bob z6Mkt67…v4N1tRk │
|
||||
│ Head 27857ec9eb04c69cacab516e8bf4b5fd36090f66 │
|
||||
│ Commits ahead 2, behind 0 │
|
||||
|
|
@ -47,7 +47,7 @@ $ rad patch show 69e881c
|
|||
│ 3e674d1 Define power requirements │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ● opened by bob z6Mkt67…v4N1tRk [ ... ] │
|
||||
│ ↑ updated to dcf3e6dd97c95cf8653cbb8ce47df20d28eb1821 (27857ec) [ ... ] │
|
||||
│ ↑ updated to 7782e60eb51b6e852abb184b092249327354c625 (27857ec) [ ... ] │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯
|
||||
```
|
||||
|
||||
|
|
@ -57,20 +57,20 @@ way will tell others about the corrections we needed before merging the
|
|||
changes.
|
||||
|
||||
```
|
||||
$ rad patch checkout 69e881c606639691330051d7d8f013854f32fb87
|
||||
✓ Switched to branch patch/69e881c
|
||||
✓ Branch patch/69e881c setup to track rad/patches/69e881c606639691330051d7d8f013854f32fb87
|
||||
$ rad patch checkout 4bfb6fe940f815e3fcce6a2796e051df85db9fe1
|
||||
✓ Switched to branch patch/4bfb6fe
|
||||
✓ Branch patch/4bfb6fe setup to track rad/patches/4bfb6fe940f815e3fcce6a2796e051df85db9fe1
|
||||
$ git mv REQUIREMENTS REQUIREMENTS.md
|
||||
$ git commit -m "Use markdown for requirements"
|
||||
[patch/69e881c f567f69] Use markdown for requirements
|
||||
[patch/4bfb6fe 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 69e881c updated to 70dd9a31882d184a9fe8f1f590471f5543c4d85b
|
||||
✓ Patch 4bfb6fe updated to fab4fddf6bcae7d55432417cdf5a7d0270d0d7d3
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
* [new branch] patch/69e881c -> patches/69e881c606639691330051d7d8f013854f32fb87
|
||||
* [new branch] patch/4bfb6fe -> patches/4bfb6fe940f815e3fcce6a2796e051df85db9fe1
|
||||
```
|
||||
|
||||
Great, all fixed up, lets merge the code.
|
||||
|
|
@ -78,7 +78,7 @@ Great, all fixed up, lets merge the code.
|
|||
```
|
||||
$ git checkout master
|
||||
Your branch is up to date with 'rad/master'.
|
||||
$ git merge patch/69e881c
|
||||
$ git merge patch/4bfb6fe
|
||||
Updating f2de534..f567f69
|
||||
Fast-forward
|
||||
README.md | 0
|
||||
|
|
@ -89,7 +89,7 @@ Fast-forward
|
|||
```
|
||||
``` (stderr)
|
||||
$ git push rad master
|
||||
✓ Patch 69e881c606639691330051d7d8f013854f32fb87 merged at revision 70dd9a3
|
||||
✓ Patch 4bfb6fe940f815e3fcce6a2796e051df85db9fe1 merged at revision fab4fdd
|
||||
✓ Synced with 1 node(s)
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
f2de534..f567f69 master -> master
|
||||
|
|
@ -98,10 +98,10 @@ To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkE
|
|||
The patch is now merged and closed :).
|
||||
|
||||
```
|
||||
$ rad patch show 69e881c
|
||||
$ rad patch show 4bfb6fe
|
||||
╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ Title Define power requirements │
|
||||
│ Patch 69e881c606639691330051d7d8f013854f32fb87 │
|
||||
│ Patch 4bfb6fe940f815e3fcce6a2796e051df85db9fe1 │
|
||||
│ Author bob z6Mkt67…v4N1tRk │
|
||||
│ Head 27857ec9eb04c69cacab516e8bf4b5fd36090f66 │
|
||||
│ Commits ahead 0, behind 1 │
|
||||
|
|
@ -113,9 +113,9 @@ $ rad patch show 69e881c
|
|||
│ 3e674d1 Define power requirements │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ● opened by bob z6Mkt67…v4N1tRk [ ... ] │
|
||||
│ ↑ updated to dcf3e6dd97c95cf8653cbb8ce47df20d28eb1821 (27857ec) [ ... ] │
|
||||
│ * revised by alice (you) in 70dd9a3 (f567f69) [ ... ] │
|
||||
│ ✓ merged by alice (you) at revision 70dd9a3 (f567f69) [ ... ] │
|
||||
│ ↑ updated to 7782e60eb51b6e852abb184b092249327354c625 (27857ec) [ ... ] │
|
||||
│ * revised by alice (you) in fab4fdd (f567f69) [ ... ] │
|
||||
│ ✓ merged by alice (you) at revision fab4fdd (f567f69) [ ... ] │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -8,12 +8,8 @@ pub mod rad_checkout;
|
|||
pub mod rad_clone;
|
||||
#[path = "commands/cob.rs"]
|
||||
pub mod rad_cob;
|
||||
#[path = "commands/delegate.rs"]
|
||||
pub mod rad_delegate;
|
||||
#[path = "commands/diff.rs"]
|
||||
pub mod rad_diff;
|
||||
#[path = "commands/edit.rs"]
|
||||
pub mod rad_edit;
|
||||
#[path = "commands/fork.rs"]
|
||||
pub mod rad_fork;
|
||||
#[path = "commands/help.rs"]
|
||||
|
|
|
|||
|
|
@ -82,10 +82,11 @@ fn execute(options: Options, profile: &Profile) -> anyhow::Result<PathBuf> {
|
|||
let id = options.id;
|
||||
let storage = &profile.storage;
|
||||
let remote = options.remote.unwrap_or(profile.did());
|
||||
let doc = storage
|
||||
let doc: Doc<_> = storage
|
||||
.repository(id)?
|
||||
.identity_doc_of(&remote)
|
||||
.context("project could not be found in local storage")?;
|
||||
.identity_doc()
|
||||
.context("project could not be found in local storage")?
|
||||
.into();
|
||||
let payload = doc.project()?;
|
||||
let path = PathBuf::from(payload.name());
|
||||
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ use thiserror::Error;
|
|||
|
||||
use radicle::cob;
|
||||
use radicle::git::raw;
|
||||
use radicle::identity::doc;
|
||||
use radicle::identity::doc::{DocError, Id};
|
||||
use radicle::identity::{doc, IdentityError};
|
||||
use radicle::node;
|
||||
use radicle::node::tracking::Scope;
|
||||
use radicle::node::{Handle as _, Node};
|
||||
|
|
@ -18,6 +18,7 @@ use radicle::prelude::*;
|
|||
use radicle::rad;
|
||||
use radicle::storage;
|
||||
use radicle::storage::git::Storage;
|
||||
use radicle::storage::RepositoryError;
|
||||
|
||||
use crate::commands::rad_checkout as checkout;
|
||||
use crate::commands::rad_sync as sync;
|
||||
|
|
@ -188,8 +189,8 @@ pub enum CloneError {
|
|||
Doc(#[from] DocError),
|
||||
#[error("payload: {0}")]
|
||||
Payload(#[from] doc::PayloadError),
|
||||
#[error("project error: {0}")]
|
||||
Identity(#[from] IdentityError),
|
||||
#[error(transparent)]
|
||||
Repository(#[from] RepositoryError),
|
||||
#[error("repository {0} not found")]
|
||||
NotFound(Id),
|
||||
#[error("no seeds found for {0}")]
|
||||
|
|
@ -258,7 +259,7 @@ pub fn clone<G: Signer>(
|
|||
}
|
||||
}
|
||||
|
||||
let doc = repository.identity_doc_of(&me)?;
|
||||
let doc = repository.identity_doc()?;
|
||||
let proj = doc.project()?;
|
||||
let path = Path::new(proj.name());
|
||||
|
||||
|
|
@ -279,5 +280,5 @@ pub fn clone<G: Signer>(
|
|||
|
||||
spinner.finish();
|
||||
|
||||
Ok((working, repository, doc, proj))
|
||||
Ok((working, repository, doc.into(), proj))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -137,7 +137,9 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
|||
.to_rfc2822();
|
||||
|
||||
term::print(term::format::yellow(format!("commit {}", op.id)));
|
||||
term::print(term::format::tertiary(format!("parent {}", op.identity)));
|
||||
if let Some(oid) = op.identity {
|
||||
term::print(term::format::tertiary(format!("parent {oid}")));
|
||||
}
|
||||
for parent in op.parents {
|
||||
term::print(format!("parent {}", parent));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,129 +0,0 @@
|
|||
use std::ffi::OsString;
|
||||
|
||||
use anyhow::{anyhow, Context as _};
|
||||
|
||||
use radicle::identity::Id;
|
||||
use radicle::prelude::Did;
|
||||
|
||||
use crate::terminal as term;
|
||||
use crate::terminal::args::{Args, Error, Help};
|
||||
|
||||
#[path = "delegate/add.rs"]
|
||||
mod add;
|
||||
#[path = "delegate/list.rs"]
|
||||
mod list;
|
||||
#[path = "delegate/remove.rs"]
|
||||
mod remove;
|
||||
|
||||
pub const HELP: Help = Help {
|
||||
name: "delegate",
|
||||
description: "Manage the delegates of an identity",
|
||||
version: env!("CARGO_PKG_VERSION"),
|
||||
usage: r#"
|
||||
Usage
|
||||
|
||||
rad delegate add <did> [--to <rid>] [<option>...]
|
||||
rad delegate remove <did> [--to <rid>] [<option>...]
|
||||
rad delegate list [<rid>] [<option>...]
|
||||
|
||||
The `add` and `remove` commands are limited to managing delegates
|
||||
where the `threshold` for the quorum is exactly `1`. Otherwise,
|
||||
the verification of the document will not be able to gather enough
|
||||
signatures to pass the quorum.
|
||||
|
||||
Options
|
||||
|
||||
--help Print help
|
||||
"#,
|
||||
};
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq)]
|
||||
pub enum OperationName {
|
||||
Add,
|
||||
Remove,
|
||||
#[default]
|
||||
List,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum Operation {
|
||||
Add { id: Option<Id>, did: Did },
|
||||
Remove { id: Option<Id>, did: Did },
|
||||
List { id: Option<Id> },
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub struct Options {
|
||||
pub op: Operation,
|
||||
}
|
||||
|
||||
impl Args for Options {
|
||||
fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
|
||||
use lexopt::prelude::*;
|
||||
|
||||
let mut parser = lexopt::Parser::from_args(args);
|
||||
let mut id: Option<Id> = None;
|
||||
let mut op: Option<OperationName> = None;
|
||||
let mut did: Option<Did> = None;
|
||||
|
||||
while let Some(arg) = parser.next()? {
|
||||
match arg {
|
||||
Long("help") | Short('h') => {
|
||||
return Err(Error::Help.into());
|
||||
}
|
||||
Long("to") => {
|
||||
id = Some(parser.value()?.parse::<Id>()?);
|
||||
}
|
||||
Value(val) if op.is_none() => match val.to_string_lossy().as_ref() {
|
||||
"a" | "add" => op = Some(OperationName::Add),
|
||||
"r" | "remove" => op = Some(OperationName::Remove),
|
||||
"l" | "list" => op = Some(OperationName::List),
|
||||
|
||||
unknown => anyhow::bail!("unknown operation '{}'", unknown),
|
||||
},
|
||||
Value(val) if op.is_some() => match op {
|
||||
Some(OperationName::Add) | Some(OperationName::Remove) => {
|
||||
did = Some(term::args::did(&val)?);
|
||||
}
|
||||
Some(OperationName::List) => {
|
||||
id = Some(term::args::rid(&val)?);
|
||||
}
|
||||
None => continue,
|
||||
},
|
||||
_ => return Err(anyhow!(arg.unexpected())),
|
||||
}
|
||||
}
|
||||
|
||||
let op = match op.unwrap_or_default() {
|
||||
OperationName::List => Operation::List { id },
|
||||
OperationName::Add => Operation::Add {
|
||||
id,
|
||||
did: did.ok_or_else(|| anyhow!("a delegate DID must be provided"))?,
|
||||
},
|
||||
OperationName::Remove => Operation::Remove {
|
||||
id,
|
||||
did: did.ok_or_else(|| anyhow!("a delegate DID must be provided"))?,
|
||||
},
|
||||
};
|
||||
|
||||
Ok((Options { op }, vec![]))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||
let profile = ctx.profile()?;
|
||||
let storage = &profile.storage;
|
||||
|
||||
match options.op {
|
||||
Operation::Add { id, did } => add::run(get_id(id)?, *did, &profile, storage)?,
|
||||
Operation::Remove { id, did } => remove::run(get_id(id)?, &did, &profile, storage)?,
|
||||
Operation::List { id } => list::run(get_id(id)?, &profile, storage)?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_id(id: Option<Id>) -> anyhow::Result<Id> {
|
||||
id.or_else(|| radicle::rad::cwd().ok().map(|(_, id)| id))
|
||||
.context("Couldn't get the RID from either command line or cwd")
|
||||
}
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
use anyhow::Context as _;
|
||||
|
||||
use radicle::{
|
||||
prelude::{Did, Id},
|
||||
storage::{SignRepository, WriteRepository as _, WriteStorage},
|
||||
Profile,
|
||||
};
|
||||
use radicle_crypto::PublicKey;
|
||||
|
||||
use crate::terminal as term;
|
||||
|
||||
pub fn run<S>(id: Id, key: PublicKey, profile: &Profile, storage: &S) -> anyhow::Result<()>
|
||||
where
|
||||
S: WriteStorage,
|
||||
{
|
||||
let signer = term::signer(profile)?;
|
||||
let me = signer.public_key();
|
||||
|
||||
let mut project = storage
|
||||
.get(&profile.public_key, id)?
|
||||
.context("No project with the given RID exists")?;
|
||||
|
||||
let repo = storage.repository_mut(id)?;
|
||||
|
||||
if !project.is_delegate(me) {
|
||||
return Err(anyhow::anyhow!(
|
||||
"'{}' is not a delegate of the project, only a delegate may add this key",
|
||||
me
|
||||
));
|
||||
}
|
||||
|
||||
if project.threshold > 1 {
|
||||
return Err(anyhow::anyhow!("project threshold > 1"));
|
||||
}
|
||||
|
||||
if project.delegate(&key) {
|
||||
project.sign(&signer).and_then(|(_, sig)| {
|
||||
project.update(
|
||||
signer.public_key(),
|
||||
"Updated payload",
|
||||
&[(signer.public_key(), sig)],
|
||||
repo.raw(),
|
||||
)
|
||||
})?;
|
||||
repo.sign_refs(&signer)?;
|
||||
repo.set_identity_head()?;
|
||||
term::info!("Added delegate '{}'", Did::from(key));
|
||||
term::success!("Update successful!");
|
||||
Ok(())
|
||||
} else {
|
||||
term::info!("the delegate for '{}' already exists", key);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
use anyhow::Context as _;
|
||||
|
||||
use radicle::{prelude::Id, storage::ReadStorage, Profile};
|
||||
|
||||
use crate::terminal as term;
|
||||
|
||||
pub fn run<S>(id: Id, profile: &Profile, storage: &S) -> anyhow::Result<()>
|
||||
where
|
||||
S: ReadStorage,
|
||||
{
|
||||
let project = storage
|
||||
.get(&profile.public_key, id)?
|
||||
.context("No project with the given RID exists")?;
|
||||
|
||||
term::info!("{}", serde_json::to_string_pretty(&project.delegates)?);
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
use anyhow::Context as _;
|
||||
|
||||
use radicle::{
|
||||
prelude::Id,
|
||||
storage::{WriteRepository as _, WriteStorage},
|
||||
Profile,
|
||||
};
|
||||
use radicle_crypto::PublicKey;
|
||||
|
||||
use crate::terminal as term;
|
||||
|
||||
pub fn run<S>(id: Id, key: &PublicKey, profile: &Profile, storage: &S) -> anyhow::Result<()>
|
||||
where
|
||||
S: WriteStorage,
|
||||
{
|
||||
let signer = term::signer(profile)?;
|
||||
let me = signer.public_key();
|
||||
|
||||
let mut project = storage
|
||||
.get(&profile.public_key, id)?
|
||||
.context("No project with the given RID exists")?;
|
||||
|
||||
let repo = storage.repository_mut(id)?;
|
||||
|
||||
if !project.is_delegate(me) {
|
||||
return Err(anyhow::anyhow!(
|
||||
"'{}' is not a delegate of the project, only a delegate may remove this key",
|
||||
me
|
||||
));
|
||||
}
|
||||
|
||||
if project.threshold > 1 {
|
||||
return Err(anyhow::anyhow!("project threshold > 1"));
|
||||
}
|
||||
|
||||
match project.rescind(key)? {
|
||||
Some(delegate) => {
|
||||
project.sign(&signer).and_then(|(_, sig)| {
|
||||
project.update(
|
||||
signer.public_key(),
|
||||
"Updated payload",
|
||||
&[(signer.public_key(), sig)],
|
||||
repo.raw(),
|
||||
)
|
||||
})?;
|
||||
term::info!("Removed delegate '{}'", delegate);
|
||||
term::success!("Update successful!");
|
||||
Ok(())
|
||||
}
|
||||
None => {
|
||||
term::info!("the delegate for '{}' did not exist", key);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
use std::ffi::OsString;
|
||||
|
||||
use anyhow::{anyhow, Context as _};
|
||||
|
||||
use radicle::identity::Id;
|
||||
use radicle::storage::{ReadStorage, WriteRepository};
|
||||
|
||||
use crate::terminal as term;
|
||||
use crate::terminal::args::{Args, Error, Help};
|
||||
|
||||
pub const HELP: Help = Help {
|
||||
name: "edit",
|
||||
description: "Edit an identity doc",
|
||||
version: env!("CARGO_PKG_VERSION"),
|
||||
usage: r#"
|
||||
Usage
|
||||
|
||||
rad edit [<rid>] [<option>...]
|
||||
|
||||
Edits the identity document pointed to by the RID. If it isn't specified,
|
||||
the current project is edited.
|
||||
|
||||
Options
|
||||
|
||||
--help Print help
|
||||
"#,
|
||||
};
|
||||
|
||||
#[derive(Default, Debug, Eq, PartialEq)]
|
||||
pub struct Options {
|
||||
pub id: Option<Id>,
|
||||
}
|
||||
|
||||
impl Args for Options {
|
||||
fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
|
||||
use lexopt::prelude::*;
|
||||
|
||||
let mut parser = lexopt::Parser::from_args(args);
|
||||
let mut id: Option<Id> = None;
|
||||
|
||||
while let Some(arg) = parser.next()? {
|
||||
match arg {
|
||||
Long("help") | Short('h') => {
|
||||
return Err(Error::Help.into());
|
||||
}
|
||||
Value(val) if id.is_none() => {
|
||||
id = Some(term::args::rid(&val)?);
|
||||
}
|
||||
_ => return Err(anyhow::anyhow!(arg.unexpected())),
|
||||
}
|
||||
}
|
||||
|
||||
Ok((Options { id }, vec![]))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||
let profile = ctx.profile()?;
|
||||
let signer = term::signer(&profile)?;
|
||||
let storage = &profile.storage;
|
||||
|
||||
let id = options
|
||||
.id
|
||||
.or_else(|| radicle::rad::cwd().ok().map(|(_, id)| id))
|
||||
.context("Couldn't get RID from either command line or cwd")?;
|
||||
|
||||
let mut project = storage
|
||||
.get(signer.public_key(), id)?
|
||||
.context("No project with the given RID exists")?;
|
||||
|
||||
let repo = storage.repository(id)?;
|
||||
|
||||
let payload = serde_json::to_string_pretty(&project.payload)?;
|
||||
match term::Editor::new().extension("json").edit(payload)? {
|
||||
Some(updated_payload) => {
|
||||
project.payload = serde_json::from_str(&updated_payload)?;
|
||||
project.sign(&signer).and_then(|(_, sig)| {
|
||||
project.update(
|
||||
signer.public_key(),
|
||||
"Update payload",
|
||||
&[(signer.public_key(), sig)],
|
||||
repo.raw(),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
_ => return Err(anyhow!("Operation aborted!")),
|
||||
}
|
||||
|
||||
term::success!("Update successful!");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -17,7 +17,6 @@ const COMMANDS: &[Help] = &[
|
|||
rad_auth::HELP,
|
||||
rad_checkout::HELP,
|
||||
rad_clone::HELP,
|
||||
rad_edit::HELP,
|
||||
rad_fork::HELP,
|
||||
rad_help::HELP,
|
||||
rad_id::HELP,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -8,9 +8,8 @@ use anyhow::{anyhow, Context as _};
|
|||
use chrono::prelude::*;
|
||||
use json_color::{Color, Colorizer};
|
||||
|
||||
use radicle::crypto::{Unverified, Verified};
|
||||
use radicle::identity::Untrusted;
|
||||
use radicle::identity::{Doc, Id};
|
||||
use radicle::identity::Id;
|
||||
use radicle::identity::Identity;
|
||||
use radicle::node::tracking::Policy;
|
||||
use radicle::node::AliasStore as _;
|
||||
use radicle::storage::{ReadRepository, ReadStorage};
|
||||
|
|
@ -142,7 +141,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
|||
let repo = storage
|
||||
.repository(rid)
|
||||
.context("No project with the given RID exists")?;
|
||||
let project = Doc::<Verified>::canonical(&repo)?;
|
||||
let project = repo.identity_doc()?;
|
||||
|
||||
match options.target {
|
||||
Target::Refs => {
|
||||
|
|
@ -197,14 +196,21 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
|||
println!("{}", term::format::visibility(&project.doc.visibility));
|
||||
}
|
||||
Target::History => {
|
||||
let head = Doc::<Untrusted>::head(signer.public_key(), &repo)?;
|
||||
let identity = Identity::load(&repo)?;
|
||||
let head = repo.identity_head_of(signer.public_key())?;
|
||||
let history = repo.revwalk(head)?;
|
||||
|
||||
for oid in history {
|
||||
let oid = oid?.into();
|
||||
let tip = repo.commit(oid)?;
|
||||
let blob = Doc::<Unverified>::blob_at(oid, &repo)?;
|
||||
let content: serde_json::Value = serde_json::from_slice(blob.content())?;
|
||||
|
||||
let Some(revision) = identity.revision(&tip.id().into()) else {
|
||||
continue;
|
||||
};
|
||||
if !revision.is_accepted() {
|
||||
continue;
|
||||
}
|
||||
let doc = &revision.doc;
|
||||
let timezone = if tip.time().sign() == '+' {
|
||||
#[allow(deprecated)]
|
||||
FixedOffset::east(tip.time().offset_minutes() * 60)
|
||||
|
|
@ -227,7 +233,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
|||
if let Ok(parent) = tip.parent_id(0) {
|
||||
println!("parent {parent}");
|
||||
}
|
||||
println!("blob {}", blob.id());
|
||||
println!("blob {}", revision.blob);
|
||||
println!("date {time}");
|
||||
println!();
|
||||
|
||||
|
|
@ -242,8 +248,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
|||
term::blank();
|
||||
}
|
||||
|
||||
let json =
|
||||
colorizer().colorize_json_str(&serde_json::to_string_pretty(&content)?)?;
|
||||
let json = colorizer().colorize_json_str(&serde_json::to_string_pretty(&doc)?)?;
|
||||
for line in json.lines() {
|
||||
println!(" {line}");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,7 +93,6 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
|||
if !doc.visibility.is_public() && !options.private && options.public {
|
||||
continue;
|
||||
}
|
||||
let doc = doc.verified()?;
|
||||
let proj = doc.project()?;
|
||||
let head = term::format::oid(head).into();
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use std::ffi::OsString;
|
|||
|
||||
use anyhow::{anyhow, Context as _};
|
||||
|
||||
use radicle::identity::Visibility;
|
||||
use radicle::identity::{Identity, Visibility};
|
||||
use radicle::node::Handle as _;
|
||||
use radicle::prelude::Id;
|
||||
use radicle::storage::{ReadRepository, SignRepository, WriteRepository, WriteStorage};
|
||||
|
|
@ -76,8 +76,8 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
|||
};
|
||||
|
||||
let repo = profile.storage.repository_mut(rid)?;
|
||||
let (_, doc) = repo.identity_doc()?;
|
||||
let mut doc = doc.verified()?;
|
||||
let mut identity = Identity::load_mut(&repo)?;
|
||||
let mut doc = identity.doc().clone();
|
||||
|
||||
if doc.visibility.is_public() {
|
||||
return Err(Error::WithHint {
|
||||
|
|
@ -102,14 +102,8 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
|||
|
||||
// Update identity document.
|
||||
doc.visibility = Visibility::Public;
|
||||
doc.sign(&signer).and_then(|(_, sig)| {
|
||||
doc.update(
|
||||
profile.id(),
|
||||
"Publish repository",
|
||||
&[(signer.public_key(), sig)],
|
||||
repo.raw(),
|
||||
)
|
||||
})?;
|
||||
|
||||
identity.update("Publish repository", "", &doc, &signer)?;
|
||||
repo.sign_refs(&signer)?;
|
||||
repo.set_identity_head()?;
|
||||
repo.validate()?;
|
||||
|
|
|
|||
|
|
@ -204,7 +204,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
|||
let signer = term::signer(&profile)?;
|
||||
let repository = profile.storage.repository(id)?;
|
||||
let _project = repository
|
||||
.identity_doc_of(profile.id())
|
||||
.identity_doc()
|
||||
.context(format!("couldn't load project {id} from local state"))?;
|
||||
let mut patches = Patches::open(&repository)?;
|
||||
|
||||
|
|
|
|||
|
|
@ -256,7 +256,7 @@ fn announce_refs(
|
|||
profile: &Profile,
|
||||
) -> anyhow::Result<()> {
|
||||
let repo = profile.storage.repository(rid)?;
|
||||
let (_, doc) = repo.identity_doc()?;
|
||||
let doc = repo.identity_doc()?;
|
||||
let connected: Vec<_> = if doc.visibility.is_public() {
|
||||
let seeds = node.seeds(rid)?;
|
||||
seeds.connected().map(|s| s.nid).collect()
|
||||
|
|
|
|||
|
|
@ -132,13 +132,6 @@ fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option<anyhow::Error>>
|
|||
args.to_vec(),
|
||||
);
|
||||
}
|
||||
"delegate" => {
|
||||
term::run_command_args::<rad_delegate::Options, _>(
|
||||
rad_delegate::HELP,
|
||||
rad_delegate::run,
|
||||
args.to_vec(),
|
||||
);
|
||||
}
|
||||
"diff" => {
|
||||
term::run_command_args::<rad_diff::Options, _>(
|
||||
rad_diff::HELP,
|
||||
|
|
@ -146,13 +139,6 @@ fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option<anyhow::Error>>
|
|||
args.to_vec(),
|
||||
);
|
||||
}
|
||||
"edit" => {
|
||||
term::run_command_args::<rad_edit::Options, _>(
|
||||
rad_edit::HELP,
|
||||
rad_edit::run,
|
||||
args.to_vec(),
|
||||
);
|
||||
}
|
||||
"fork" => {
|
||||
term::run_command_args::<rad_fork::Options, _>(
|
||||
rad_fork::HELP,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ pub mod args;
|
|||
pub use args::{Args, Error, Help};
|
||||
pub mod format;
|
||||
pub mod io;
|
||||
pub use io::{proposal, signer};
|
||||
pub use io::signer;
|
||||
pub mod comment;
|
||||
pub mod highlight;
|
||||
pub mod issue;
|
||||
|
|
|
|||
|
|
@ -142,6 +142,18 @@ impl<'a> Author<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn alias(&self) -> Option<term::Label> {
|
||||
self.alias.as_ref().map(|a| a.to_string().into())
|
||||
}
|
||||
|
||||
pub fn you(&self) -> Option<term::Label> {
|
||||
if self.you {
|
||||
Some(term::format::primary("(you)").dim().italic().into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn labels(self) -> (term::Label, term::Label) {
|
||||
let alias = match &self.alias {
|
||||
Some(alias) => term::format::primary(alias).into(),
|
||||
|
|
@ -149,8 +161,8 @@ impl<'a> Author<'a> {
|
|||
.dim()
|
||||
.into(),
|
||||
};
|
||||
if self.you {
|
||||
(alias, term::format::primary("(you)").dim().italic().into())
|
||||
if let Some(you) = self.you() {
|
||||
(alias, you)
|
||||
} else {
|
||||
(
|
||||
alias,
|
||||
|
|
|
|||
|
|
@ -67,71 +67,3 @@ pub fn comment_select(issue: &Issue) -> Option<(&CommentId, &Comment)> {
|
|||
|
||||
comments.get(selection).copied()
|
||||
}
|
||||
|
||||
pub mod proposal {
|
||||
use std::fmt::Write as _;
|
||||
|
||||
use radicle::{
|
||||
cob::identity::{self, Proposal},
|
||||
git::Oid,
|
||||
identity::Identity,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
use crate::terminal::format;
|
||||
|
||||
pub fn revision_select(
|
||||
proposal: &Proposal,
|
||||
) -> Option<(&identity::RevisionId, &identity::Revision)> {
|
||||
let revisions = proposal.revisions().collect::<Vec<_>>();
|
||||
let selection = Select::new(
|
||||
"Which revision do you want to select?",
|
||||
(0..revisions.len()).collect(),
|
||||
)
|
||||
.with_vim_mode(true)
|
||||
.with_formatter(&|ix| revisions[ix.index].0.to_string())
|
||||
.with_render_config(*CONFIG)
|
||||
.prompt()
|
||||
.ok()?;
|
||||
|
||||
revisions.get(selection).copied()
|
||||
}
|
||||
|
||||
pub fn revision_commit_select<'a>(
|
||||
proposal: &'a Proposal,
|
||||
previous: &'a Identity<Oid>,
|
||||
) -> Option<(&'a identity::RevisionId, &'a identity::Revision)> {
|
||||
let revisions = proposal
|
||||
.revisions()
|
||||
.filter(|(_, r)| r.is_quorum_reached(previous))
|
||||
.collect::<Vec<_>>();
|
||||
let selection = Select::new(
|
||||
"Which revision do you want to commit?",
|
||||
(0..revisions.len()).collect(),
|
||||
)
|
||||
.with_formatter(&|ix| revisions[ix.index].0.to_string())
|
||||
.with_render_config(*CONFIG)
|
||||
.prompt()
|
||||
.ok()?;
|
||||
|
||||
revisions.get(selection).copied()
|
||||
}
|
||||
|
||||
pub fn diff(proposal: &identity::Revision, previous: &Identity<Oid>) -> anyhow::Result<String> {
|
||||
use similar::{ChangeTag, TextDiff};
|
||||
|
||||
let new = serde_json::to_string_pretty(&proposal.proposed)?;
|
||||
let previous = serde_json::to_string_pretty(&previous.doc)?;
|
||||
let diff = TextDiff::from_lines(&previous, &new);
|
||||
let mut buf = String::new();
|
||||
for change in diff.iter_all_changes() {
|
||||
match change.tag() {
|
||||
ChangeTag::Delete => write!(buf, "{}", format::negative(format!("-{change}")))?,
|
||||
ChangeTag::Insert => write!(buf, "{}", format::positive(format!("+{change}")))?,
|
||||
ChangeTag::Equal => write!(buf, " {change}")?,
|
||||
};
|
||||
}
|
||||
|
||||
Ok(buf)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,29 +35,7 @@ pub fn get_title_description(
|
|||
title: Option<String>,
|
||||
description: Option<String>,
|
||||
) -> io::Result<Option<(String, String)>> {
|
||||
let mut placeholder = String::new();
|
||||
|
||||
if let Some(title) = title {
|
||||
placeholder.push_str(title.trim());
|
||||
placeholder.push('\n');
|
||||
}
|
||||
if let Some(description) = description {
|
||||
placeholder.push('\n');
|
||||
placeholder.push_str(description.trim());
|
||||
placeholder.push('\n');
|
||||
}
|
||||
placeholder.push_str(OPEN_MSG);
|
||||
|
||||
let output = term::patch::Message::Edit.get(&placeholder)?;
|
||||
let Some((title, description)) = output.split_once("\n\n") else {
|
||||
return Ok(None);
|
||||
};
|
||||
let (title, description) = (title.trim(), description.trim());
|
||||
|
||||
if title.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some((title.to_owned(), description.to_owned())))
|
||||
term::patch::Message::edit_title_description(title, description, OPEN_MSG)
|
||||
}
|
||||
|
||||
pub fn show(
|
||||
|
|
|
|||
|
|
@ -56,6 +56,38 @@ impl Message {
|
|||
Ok(comment.to_owned())
|
||||
}
|
||||
|
||||
/// Open the editor with the given title and description (if any).
|
||||
/// Returns the edited title and description, or nothing if it couldn't be parsed.
|
||||
pub fn edit_title_description(
|
||||
title: Option<String>,
|
||||
description: Option<String>,
|
||||
help: &str,
|
||||
) -> std::io::Result<Option<(String, String)>> {
|
||||
let mut placeholder = String::new();
|
||||
|
||||
if let Some(title) = title {
|
||||
placeholder.push_str(title.trim());
|
||||
placeholder.push('\n');
|
||||
}
|
||||
if let Some(description) = description {
|
||||
placeholder.push('\n');
|
||||
placeholder.push_str(description.trim());
|
||||
placeholder.push('\n');
|
||||
}
|
||||
placeholder.push_str(help);
|
||||
|
||||
let output = Self::Edit.get(&placeholder)?;
|
||||
let Some((title, description)) = output.split_once("\n\n") else {
|
||||
return Ok(None);
|
||||
};
|
||||
let (title, description) = (title.trim(), description.trim());
|
||||
|
||||
if title.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some((title.to_owned(), description.to_owned())))
|
||||
}
|
||||
|
||||
pub fn append(&mut self, arg: &str) {
|
||||
if let Message::Text(v) = self {
|
||||
v.extend(["\n\n", arg]);
|
||||
|
|
|
|||
|
|
@ -16,10 +16,9 @@ use radicle::test::fixtures;
|
|||
use radicle_cli_test::TestFormula;
|
||||
use radicle_node::service::tracking::{Policy, Scope};
|
||||
use radicle_node::service::Event;
|
||||
use radicle_node::test::{
|
||||
environment::{Config, Environment},
|
||||
logger,
|
||||
};
|
||||
use radicle_node::test::environment::{Config, Environment};
|
||||
#[allow(unused_imports)]
|
||||
use radicle_node::test::logger;
|
||||
|
||||
/// Seed used in tests.
|
||||
const RAD_SEED: &str = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff";
|
||||
|
|
@ -229,20 +228,6 @@ fn rad_checkout() {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rad_delegate() {
|
||||
let mut environment = Environment::new();
|
||||
let profile = environment.profile("alice");
|
||||
let working = tempfile::tempdir().unwrap();
|
||||
let home = &profile.home;
|
||||
|
||||
// Setup a test repository.
|
||||
fixtures::repository(working.path());
|
||||
|
||||
test("examples/rad-init.md", working.path(), Some(home), []).unwrap();
|
||||
test("examples/rad-delegate.md", working.path(), Some(home), []).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rad_id() {
|
||||
let mut environment = Environment::new();
|
||||
|
|
@ -258,23 +243,109 @@ fn rad_id() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn rad_id_rebase() {
|
||||
fn rad_id_multi_delegate() {
|
||||
let mut environment = Environment::new();
|
||||
let profile = environment.profile("alice");
|
||||
let alice = environment.node(Config::test(Alias::new("alice")));
|
||||
let bob = environment.node(Config::test(Alias::new("bob")));
|
||||
let working = tempfile::tempdir().unwrap();
|
||||
let home = &profile.home;
|
||||
let working = working.path();
|
||||
let acme = Id::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
|
||||
|
||||
// Setup a test repository.
|
||||
fixtures::repository(working.path());
|
||||
fixtures::repository(working.join("alice"));
|
||||
|
||||
test("examples/rad-init.md", working.path(), Some(home), []).unwrap();
|
||||
test("examples/rad-id-rebase.md", working.path(), Some(home), []).unwrap();
|
||||
test(
|
||||
"examples/rad-init.md",
|
||||
working.join("alice"),
|
||||
Some(&alice.home),
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut alice = alice.spawn();
|
||||
let mut bob = bob.spawn();
|
||||
|
||||
bob.handle.track_repo(acme, Scope::All).unwrap();
|
||||
alice.connect(&bob).converge([&bob]);
|
||||
|
||||
test(
|
||||
"examples/rad-clone.md",
|
||||
working.join("bob"),
|
||||
Some(&bob.home),
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// TODO: Have formula with two connected nodes and a tracked project.
|
||||
|
||||
formula(&environment.tmp(), "examples/rad-id-multi-delegate.md")
|
||||
.unwrap()
|
||||
.home(
|
||||
"alice",
|
||||
working.join("alice"),
|
||||
[("RAD_HOME", alice.home.path().display())],
|
||||
)
|
||||
.home(
|
||||
"bob",
|
||||
working.join("bob"),
|
||||
[("RAD_HOME", bob.home.path().display())],
|
||||
)
|
||||
.run()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rad_id_conflict() {
|
||||
let mut environment = Environment::new();
|
||||
let alice = environment.node(Config::test(Alias::new("alice")));
|
||||
let bob = environment.node(Config::test(Alias::new("bob")));
|
||||
let working = tempfile::tempdir().unwrap();
|
||||
let working = working.path();
|
||||
let acme = Id::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
|
||||
|
||||
// Setup a test repository.
|
||||
fixtures::repository(working.join("alice"));
|
||||
|
||||
test(
|
||||
"examples/rad-init.md",
|
||||
working.join("alice"),
|
||||
Some(&alice.home),
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut alice = alice.spawn();
|
||||
let mut bob = bob.spawn();
|
||||
|
||||
bob.handle.track_repo(acme, Scope::All).unwrap();
|
||||
alice.connect(&bob).converge([&bob]);
|
||||
|
||||
test(
|
||||
"examples/rad-clone.md",
|
||||
working.join("bob"),
|
||||
Some(&bob.home),
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
formula(&environment.tmp(), "examples/rad-id-conflict.md")
|
||||
.unwrap()
|
||||
.home(
|
||||
"alice",
|
||||
working.join("alice"),
|
||||
[("RAD_HOME", alice.home.path().display())],
|
||||
)
|
||||
.home(
|
||||
"bob",
|
||||
working.join("bob"),
|
||||
[("RAD_HOME", bob.home.path().display())],
|
||||
)
|
||||
.run()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rad_node_connect() {
|
||||
logger::init(log::Level::Debug);
|
||||
|
||||
let mut environment = Environment::new();
|
||||
let alice = environment.node(Config::test(Alias::new("alice")));
|
||||
let bob = environment.node(Config::test(Alias::new("bob")));
|
||||
|
|
@ -300,8 +371,6 @@ fn rad_node_connect() {
|
|||
|
||||
#[test]
|
||||
fn rad_node() {
|
||||
logger::init(log::Level::Debug);
|
||||
|
||||
let mut environment = Environment::new();
|
||||
let alice = environment.node(Config::test(Alias::new("alice")));
|
||||
let working = tempfile::tempdir().unwrap();
|
||||
|
|
@ -408,8 +477,6 @@ fn rad_patch_draft() {
|
|||
|
||||
#[test]
|
||||
fn rad_patch_via_push() {
|
||||
logger::init(log::Level::Debug);
|
||||
|
||||
let mut environment = Environment::new();
|
||||
let profile = environment.profile("alice");
|
||||
let working = tempfile::tempdir().unwrap();
|
||||
|
|
@ -431,8 +498,6 @@ fn rad_patch_via_push() {
|
|||
#[test]
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn rad_review_by_hunk() {
|
||||
logger::init(log::Level::Debug);
|
||||
|
||||
let mut environment = Environment::new();
|
||||
let profile = environment.profile("alice");
|
||||
let working = tempfile::tempdir().unwrap();
|
||||
|
|
@ -483,8 +548,6 @@ fn rad_track() {
|
|||
|
||||
#[test]
|
||||
fn rad_clone() {
|
||||
logger::init(log::Level::Debug);
|
||||
|
||||
let mut environment = Environment::new();
|
||||
let mut alice = environment.node(Config::test(Alias::new("alice")));
|
||||
let bob = environment.node(Config::test(Alias::new("bob")));
|
||||
|
|
@ -505,8 +568,6 @@ fn rad_clone() {
|
|||
|
||||
#[test]
|
||||
fn rad_clone_all() {
|
||||
logger::init(log::Level::Debug);
|
||||
|
||||
let mut environment = Environment::new();
|
||||
let mut alice = environment.node(Config::test(Alias::new("alice")));
|
||||
let bob = environment.node(Config::test(Alias::new("bob")));
|
||||
|
|
@ -658,8 +719,6 @@ fn rad_self() {
|
|||
|
||||
#[test]
|
||||
fn rad_clone_unknown() {
|
||||
logger::init(log::Level::Debug);
|
||||
|
||||
let mut environment = Environment::new();
|
||||
let alice = environment.node(Config::test(Alias::new("alice")));
|
||||
let working = environment.tmp().join("working");
|
||||
|
|
@ -677,8 +736,6 @@ fn rad_clone_unknown() {
|
|||
|
||||
#[test]
|
||||
fn rad_init_sync_and_clone() {
|
||||
logger::init(log::Level::Debug);
|
||||
|
||||
let mut environment = Environment::new();
|
||||
let alice = environment.node(Config::test(Alias::new("alice")));
|
||||
let bob = environment.node(Config::test(Alias::new("bob")));
|
||||
|
|
@ -795,8 +852,6 @@ fn rad_fork() {
|
|||
#[test]
|
||||
// User tries to clone; no seeds are available, but user has the repo locally.
|
||||
fn test_clone_without_seeds() {
|
||||
logger::init(log::Level::Debug);
|
||||
|
||||
let mut environment = Environment::new();
|
||||
let mut alice = environment.node(Config::test(Alias::new("alice")));
|
||||
let working = environment.tmp().join("working");
|
||||
|
|
@ -818,8 +873,6 @@ fn test_clone_without_seeds() {
|
|||
|
||||
#[test]
|
||||
fn test_cob_replication() {
|
||||
logger::init(log::Level::Debug);
|
||||
|
||||
let mut environment = Environment::new();
|
||||
let working = tempfile::tempdir().unwrap();
|
||||
let mut alice = environment.node(Config::test(Alias::new("alice")));
|
||||
|
|
@ -934,8 +987,6 @@ fn test_cob_deletion() {
|
|||
|
||||
#[test]
|
||||
fn rad_sync() {
|
||||
logger::init(log::Level::Debug);
|
||||
|
||||
let mut environment = Environment::new();
|
||||
let working = environment.tmp().join("working");
|
||||
let alice = environment.node(Config::test(Alias::new("alice")));
|
||||
|
|
@ -1108,8 +1159,6 @@ fn rad_remote() {
|
|||
|
||||
#[test]
|
||||
fn rad_merge_via_push() {
|
||||
logger::init(log::Level::Debug);
|
||||
|
||||
let mut environment = Environment::new();
|
||||
let alice = environment.node(Config::test(Alias::new("alice")));
|
||||
let working = environment.tmp().join("working");
|
||||
|
|
@ -1189,8 +1238,6 @@ fn rad_merge_no_ff() {
|
|||
|
||||
#[test]
|
||||
fn rad_patch_pull_update() {
|
||||
logger::init(log::Level::Debug);
|
||||
|
||||
let mut environment = Environment::new();
|
||||
let alice = environment.node(Config::test(Alias::new("alice")));
|
||||
let bob = environment.node(Config::test(Alias::new("bob")));
|
||||
|
|
@ -1238,8 +1285,6 @@ fn rad_init_private() {
|
|||
|
||||
#[test]
|
||||
fn rad_init_private_clone() {
|
||||
logger::init(log::Level::Debug);
|
||||
|
||||
let mut environment = Environment::new();
|
||||
let alice = environment.node(Config::test(Alias::new("alice")));
|
||||
let bob = environment.node(Config::test(Alias::new("bob")));
|
||||
|
|
@ -1303,8 +1348,6 @@ fn rad_publish() {
|
|||
|
||||
#[test]
|
||||
fn framework_home() {
|
||||
logger::init(log::Level::Debug);
|
||||
|
||||
let mut environment = Environment::new();
|
||||
let alice = environment.node(Config::test(Alias::new("alice")));
|
||||
let bob = environment.node(Config::test(Alias::new("bob")));
|
||||
|
|
@ -1327,8 +1370,6 @@ fn framework_home() {
|
|||
|
||||
#[test]
|
||||
fn git_push_diverge() {
|
||||
logger::init(log::Level::Debug);
|
||||
|
||||
let mut environment = Environment::new();
|
||||
let alice = environment.node(Config::test(Alias::new("alice")));
|
||||
let bob = environment.node(Config::test(Alias::new("bob")));
|
||||
|
|
@ -1375,8 +1416,6 @@ fn git_push_diverge() {
|
|||
|
||||
#[test]
|
||||
fn git_push_and_pull() {
|
||||
logger::init(log::Level::Debug);
|
||||
|
||||
let mut environment = Environment::new();
|
||||
let alice = environment.node(Config::test(Alias::new("alice")));
|
||||
let bob = environment.node(Config::test(Alias::new("bob")));
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
use std::collections::BTreeMap;
|
||||
use std::convert::TryFrom;
|
||||
use std::iter;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use git_ext::author::Author;
|
||||
|
|
@ -95,7 +94,7 @@ impl change::Storage for git2::Repository {
|
|||
|
||||
fn store<Signer>(
|
||||
&self,
|
||||
resource: Self::Parent,
|
||||
resource: Option<Self::Parent>,
|
||||
mut parents: Vec<Self::Parent>,
|
||||
signer: &Signer,
|
||||
spec: store::Template<Self::ObjectId>,
|
||||
|
|
@ -125,15 +124,16 @@ impl change::Storage for git2::Repository {
|
|||
|
||||
let (id, timestamp) = write_commit(
|
||||
self,
|
||||
*resource,
|
||||
resource.map(|o| *o),
|
||||
// Commit to tips, extra parents and resource.
|
||||
tips.iter()
|
||||
.cloned()
|
||||
.chain(parents.clone())
|
||||
.chain(iter::once(resource))
|
||||
.chain(resource)
|
||||
.map(git2::Oid::from),
|
||||
message,
|
||||
signature.clone(),
|
||||
vec![],
|
||||
tree,
|
||||
)?;
|
||||
|
||||
|
|
@ -164,7 +164,7 @@ impl change::Storage for git2::Repository {
|
|||
let parents = commit
|
||||
.parents()
|
||||
.map(Oid::from)
|
||||
.filter(|p| *p != resource)
|
||||
.filter(|p| Some(*p) != resource)
|
||||
.collect();
|
||||
let mut signatures = Signatures::try_from(&commit)?
|
||||
.into_iter()
|
||||
|
|
@ -195,19 +195,17 @@ impl change::Storage for git2::Repository {
|
|||
|
||||
fn parse_resource_trailer<'a>(
|
||||
trailers: impl Iterator<Item = &'a OwnedTrailer>,
|
||||
) -> Result<Oid, error::Load> {
|
||||
) -> Result<Option<Oid>, error::Load> {
|
||||
for trailer in trailers {
|
||||
match trailers::ResourceCommitTrailer::try_from(trailer) {
|
||||
Err(trailers::error::InvalidResourceTrailer::WrongToken) => {
|
||||
continue;
|
||||
}
|
||||
Err(err) => return Err(err.into()),
|
||||
Ok(resource) => return Ok(resource.oid().into()),
|
||||
Ok(resource) => return Ok(Some(resource.oid().into())),
|
||||
}
|
||||
}
|
||||
Err(error::Load::from(
|
||||
trailers::error::InvalidResourceTrailer::NoTrailer,
|
||||
))
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn load_manifest(
|
||||
|
|
@ -253,13 +251,17 @@ fn load_contents(repo: &git2::Repository, tree: &git2::Tree) -> Result<Contents,
|
|||
|
||||
fn write_commit(
|
||||
repo: &git2::Repository,
|
||||
resource: git2::Oid,
|
||||
resource: Option<git2::Oid>,
|
||||
parents: impl IntoIterator<Item = git2::Oid>,
|
||||
message: String,
|
||||
signature: ExtendedSignature,
|
||||
trailers: impl IntoIterator<Item = OwnedTrailer>,
|
||||
tree: git2::Tree,
|
||||
) -> Result<(Oid, Timestamp), error::Create> {
|
||||
let trailers: Vec<OwnedTrailer> = vec![trailers::ResourceCommitTrailer::from(resource).into()];
|
||||
let trailers: Vec<OwnedTrailer> = trailers
|
||||
.into_iter()
|
||||
.chain(resource.map(|r| trailers::ResourceCommitTrailer::from(r).into()))
|
||||
.collect();
|
||||
let author = repo.signature()?;
|
||||
#[allow(unused_variables)]
|
||||
let timestamp = author.when().seconds();
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ pub trait Storage {
|
|||
#[allow(clippy::type_complexity)]
|
||||
fn store<G>(
|
||||
&self,
|
||||
resource: Self::Parent,
|
||||
resource: Option<Self::Parent>,
|
||||
parents: Vec<Self::Parent>,
|
||||
signer: &G,
|
||||
template: Template<Self::ObjectId>,
|
||||
|
|
@ -61,22 +61,22 @@ pub type EntryId = Oid;
|
|||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Entry<Resource, Id, Signature> {
|
||||
/// The content address of the `Change` itself.
|
||||
/// The content address of the entry itself.
|
||||
pub id: Id,
|
||||
/// The content address of the tree of the `Change`.
|
||||
/// The content address of the tree of the entry.
|
||||
pub revision: Id,
|
||||
/// The cryptographic signature(s) and their public keys of the
|
||||
/// authors.
|
||||
pub signature: Signature,
|
||||
/// The parent resource that this change lives under. For example,
|
||||
/// this change could be for a patch of a project.
|
||||
pub resource: Resource,
|
||||
pub resource: Option<Resource>,
|
||||
/// Other parents this change depends on.
|
||||
pub parents: Vec<Resource>,
|
||||
/// The manifest describing the type of object as well as the type
|
||||
/// of history for this `Change`.
|
||||
/// of history for this entry.
|
||||
pub manifest: Manifest,
|
||||
/// The contents that describe `Change`.
|
||||
/// The contents that describe entry.
|
||||
pub contents: Contents,
|
||||
/// Timestamp of change.
|
||||
pub timestamp: Timestamp,
|
||||
|
|
@ -87,7 +87,7 @@ where
|
|||
Id: fmt::Display,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "Change {{ id: {} }}", self.id)
|
||||
write!(f, "Entry {{ id: {} }}", self.id)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -104,8 +104,8 @@ impl<Resource, Id, Signatures> Entry<Resource, Id, Signatures> {
|
|||
&self.contents
|
||||
}
|
||||
|
||||
pub fn resource(&self) -> &Resource {
|
||||
&self.resource
|
||||
pub fn resource(&self) -> Option<&Resource> {
|
||||
self.resource.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -142,20 +142,12 @@ pub struct Manifest {
|
|||
/// 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,
|
||||
}
|
||||
Self { type_name, version }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -163,7 +163,7 @@ impl GraphBuilder {
|
|||
where
|
||||
S: change::Storage<ObjectId = Oid, Parent = Oid, Signatures = ExtendedSignature>,
|
||||
{
|
||||
let resource_commit = *change.resource();
|
||||
let resource = change.resource().copied();
|
||||
|
||||
if !self.graph.contains(&commit_id) {
|
||||
self.graph.node(commit_id, change);
|
||||
|
|
@ -173,7 +173,7 @@ impl GraphBuilder {
|
|||
.parents_of(&commit_id)?
|
||||
.into_iter()
|
||||
.filter_map(move |parent| {
|
||||
if parent != resource_commit && !self.graph.has_dependency(&commit_id, &parent) {
|
||||
if Some(parent) != resource && !self.graph.has_dependency(&commit_id, &parent) {
|
||||
Some((parent, commit_id))
|
||||
} else {
|
||||
None
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ impl Create {
|
|||
pub fn create<T, S, G>(
|
||||
storage: &S,
|
||||
signer: &G,
|
||||
resource: Oid,
|
||||
resource: Option<Oid>,
|
||||
parents: Vec<Oid>,
|
||||
identifier: &PublicKey,
|
||||
args: Create,
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ pub struct Update {
|
|||
pub fn update<T, S, G>(
|
||||
storage: &S,
|
||||
signer: &G,
|
||||
resource: Oid,
|
||||
resource: Option<Oid>,
|
||||
parents: Vec<Oid>,
|
||||
identifier: &PublicKey,
|
||||
args: Update,
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ impl change::Storage for Storage {
|
|||
|
||||
fn store<Signer>(
|
||||
&self,
|
||||
authority: Self::Parent,
|
||||
authority: Option<Self::Parent>,
|
||||
parents: Vec<Self::Parent>,
|
||||
signer: &Signer,
|
||||
spec: change::Template<Self::ObjectId>,
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ fn roundtrip() {
|
|||
let cob = create::<NonEmpty<Entry>, _, _>(
|
||||
&storage,
|
||||
&signer,
|
||||
proj.project.content_id,
|
||||
Some(proj.project.content_id),
|
||||
vec![],
|
||||
signer.public_key(),
|
||||
Create {
|
||||
|
|
@ -61,7 +61,7 @@ fn list_cobs() {
|
|||
let issue_1 = create::<NonEmpty<Entry>, _, _>(
|
||||
&storage,
|
||||
&signer,
|
||||
proj.project.content_id,
|
||||
Some(proj.project.content_id),
|
||||
vec![],
|
||||
signer.public_key(),
|
||||
Create {
|
||||
|
|
@ -77,7 +77,7 @@ fn list_cobs() {
|
|||
let issue_2 = create(
|
||||
&storage,
|
||||
&signer,
|
||||
proj.project.content_id,
|
||||
Some(proj.project.content_id),
|
||||
vec![],
|
||||
signer.public_key(),
|
||||
Create {
|
||||
|
|
@ -113,7 +113,7 @@ fn update_cob() {
|
|||
let cob = create::<NonEmpty<Entry>, _, _>(
|
||||
&storage,
|
||||
&signer,
|
||||
proj.project.content_id,
|
||||
Some(proj.project.content_id),
|
||||
vec![],
|
||||
signer.public_key(),
|
||||
Create {
|
||||
|
|
@ -133,7 +133,7 @@ fn update_cob() {
|
|||
let Updated { object, .. } = update(
|
||||
&storage,
|
||||
&signer,
|
||||
proj.project.content_id,
|
||||
Some(proj.project.content_id),
|
||||
vec![],
|
||||
signer.public_key(),
|
||||
Update {
|
||||
|
|
@ -174,7 +174,7 @@ fn traverse_cobs() {
|
|||
let cob = create::<NonEmpty<Entry>, _, _>(
|
||||
&storage,
|
||||
&terry_signer,
|
||||
terry_proj.project.content_id,
|
||||
Some(terry_proj.project.content_id),
|
||||
vec![],
|
||||
terry_signer.public_key(),
|
||||
Create {
|
||||
|
|
@ -198,7 +198,7 @@ fn traverse_cobs() {
|
|||
let Updated { object, .. } = update::<NonEmpty<Entry>, _, _>(
|
||||
&storage,
|
||||
&neil_signer,
|
||||
neil_proj.project.content_id,
|
||||
Some(neil_proj.project.content_id),
|
||||
vec![],
|
||||
neil_signer.public_key(),
|
||||
Update {
|
||||
|
|
|
|||
|
|
@ -10,8 +10,6 @@ pub mod error {
|
|||
pub enum InvalidResourceTrailer {
|
||||
#[error("found wrong token for Rad-Resource tailer")]
|
||||
WrongToken,
|
||||
#[error("no Rad-Resource")]
|
||||
NoTrailer,
|
||||
#[error("no value for Rad-Resource")]
|
||||
NoValue,
|
||||
#[error("invalid git OID")]
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ use tower_http::cors::{self, CorsLayer};
|
|||
|
||||
use radicle::cob::patch;
|
||||
use radicle::cob::{issue, Uri};
|
||||
use radicle::identity::Id;
|
||||
use radicle::identity::{DocAt, Id};
|
||||
use radicle::node::routing::Store;
|
||||
use radicle::storage::{ReadRepository, ReadStorage};
|
||||
use radicle::Profile;
|
||||
|
|
@ -54,7 +54,7 @@ impl Context {
|
|||
let storage = &self.profile.storage;
|
||||
let repo = storage.repository(id)?;
|
||||
let (_, head) = repo.head()?;
|
||||
let doc = repo.identity_doc()?.1.verified()?;
|
||||
let DocAt { doc, .. } = repo.identity_doc()?;
|
||||
let payload = doc.project()?;
|
||||
let delegates = doc.delegates;
|
||||
let issues = issue::Issues::open(&repo)?.counts()?;
|
||||
|
|
|
|||
|
|
@ -42,9 +42,9 @@ pub enum Error {
|
|||
#[error(transparent)]
|
||||
CobStore(#[from] radicle::cob::store::Error),
|
||||
|
||||
/// Identity error.
|
||||
/// Repository error.
|
||||
#[error(transparent)]
|
||||
Identity(#[from] radicle::identity::IdentityError),
|
||||
Repository(#[from] radicle::storage::RepositoryError),
|
||||
|
||||
/// Project doc error.
|
||||
#[error(transparent)]
|
||||
|
|
@ -115,10 +115,7 @@ impl IntoResponse for Error {
|
|||
tracing::error!("Error: {message}");
|
||||
|
||||
if cfg!(debug_assertions) {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Some(format!("{other:?}")),
|
||||
)
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, Some(other.to_string()))
|
||||
} else {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, None)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use axum::{Json, Router};
|
|||
|
||||
use radicle::cob::issue::Issues;
|
||||
use radicle::cob::patch::Patches;
|
||||
use radicle::identity::Did;
|
||||
use radicle::identity::{Did, DocAt};
|
||||
use radicle::node::routing::Store;
|
||||
use radicle::storage::{ReadRepository, ReadStorage};
|
||||
|
||||
|
|
@ -42,12 +42,10 @@ async fn delegates_projects_handler(
|
|||
.filter_map(|id| {
|
||||
let Ok(repo) = storage.repository(id) else { return None };
|
||||
let Ok((_, head)) = repo.head() else { return None };
|
||||
let Ok((_, doc)) = repo.identity_doc() else { return None };
|
||||
let Ok(doc) = doc.verified() else { return None };
|
||||
let Ok(DocAt { doc, .. }) = repo.identity_doc() else { return None };
|
||||
let Ok(payload) = doc.project() else { return None };
|
||||
|
||||
let delegates = doc.delegates;
|
||||
if !delegates.iter().any(|d| *d == delegate) {
|
||||
if !doc.delegates.iter().any(|d| *d == delegate) {
|
||||
return None;
|
||||
}
|
||||
|
||||
|
|
@ -56,6 +54,7 @@ async fn delegates_projects_handler(
|
|||
let Ok(patches) = Patches::open(&repo) else { return None };
|
||||
let Ok(patches) = patches.counts() else { return None };
|
||||
|
||||
let delegates = doc.delegates;
|
||||
let trackings = routing.count(&id).unwrap_or_default();
|
||||
|
||||
Some(Info {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ use serde_json::{json, Value};
|
|||
use tower_http::set_header::SetResponseHeaderLayer;
|
||||
|
||||
use radicle::cob::{issue, patch, Embed, Label, Uri};
|
||||
use radicle::identity::{Did, Id};
|
||||
use radicle::identity::{Did, DocAt, Id};
|
||||
use radicle::node::routing::Store;
|
||||
use radicle::node::AliasStore;
|
||||
use radicle::node::NodeId;
|
||||
|
|
@ -89,8 +89,7 @@ async fn project_root_handler(
|
|||
.filter_map(|id| {
|
||||
let Ok(repo) = storage.repository(id) else { return None };
|
||||
let Ok((_, head)) = repo.head() else { return None };
|
||||
let Ok((_, doc)) = repo.identity_doc() else { return None };
|
||||
let Ok(doc) = doc.verified() else { return None };
|
||||
let Ok(DocAt { doc, .. }) = repo.identity_doc() else { return None };
|
||||
let Ok(payload) = doc.project() else { return None };
|
||||
let Ok(issues) = issue::Issues::open(&repo) else { return None };
|
||||
let Ok(issues) = issues.counts() else { return None };
|
||||
|
|
@ -686,24 +685,16 @@ async fn issue_update_handler(
|
|||
api::auth::validate(&ctx, &token).await?;
|
||||
|
||||
let storage = &ctx.profile.storage;
|
||||
let signer = ctx.profile.signer().unwrap();
|
||||
let signer = ctx.profile.signer()?;
|
||||
let repo = storage.repository(project)?;
|
||||
let mut issues = issue::Issues::open(&repo)?;
|
||||
let mut issue = issues.get_mut(&issue_id.into())?;
|
||||
|
||||
match action {
|
||||
issue::Action::Assign { assignees } => {
|
||||
issue.assign(assignees, &signer)?;
|
||||
}
|
||||
issue::Action::Lifecycle { state } => {
|
||||
issue.lifecycle(state, &signer)?;
|
||||
}
|
||||
issue::Action::Label { labels } => {
|
||||
issue.label(labels, &signer)?;
|
||||
}
|
||||
issue::Action::Edit { title } => {
|
||||
issue.edit(title, &signer)?;
|
||||
}
|
||||
let id = match action {
|
||||
issue::Action::Assign { assignees } => issue.assign(assignees, &signer)?,
|
||||
issue::Action::Lifecycle { state } => issue.lifecycle(state, &signer)?,
|
||||
issue::Action::Label { labels } => issue.label(labels, &signer)?,
|
||||
issue::Action::Edit { title } => issue.edit(title, &signer)?,
|
||||
issue::Action::Comment {
|
||||
body,
|
||||
reply_to,
|
||||
|
|
@ -720,7 +711,7 @@ async fn issue_update_handler(
|
|||
})
|
||||
.collect();
|
||||
if let Some(to) = reply_to {
|
||||
issue.comment(body, to, embeds, &signer)?;
|
||||
issue.comment(body, to, embeds, &signer)?
|
||||
} else {
|
||||
return Err(Error::BadRequest("`replyTo` missing".to_owned()));
|
||||
}
|
||||
|
|
@ -729,9 +720,7 @@ async fn issue_update_handler(
|
|||
id,
|
||||
reaction,
|
||||
active,
|
||||
} => {
|
||||
issue.react(id, reaction, active, &signer)?;
|
||||
}
|
||||
} => issue.react(id, reaction, active, &signer)?,
|
||||
issue::Action::CommentEdit { id, body, embeds } => {
|
||||
let embeds: Vec<Embed> = embeds
|
||||
.into_iter()
|
||||
|
|
@ -743,14 +732,12 @@ async fn issue_update_handler(
|
|||
})
|
||||
})
|
||||
.collect();
|
||||
issue.edit_comment(id, body, embeds, &signer)?;
|
||||
}
|
||||
issue::Action::CommentRedact { id } => {
|
||||
issue.redact_comment(id, &signer)?;
|
||||
issue.edit_comment(id, body, embeds, &signer)?
|
||||
}
|
||||
issue::Action::CommentRedact { id } => issue.redact_comment(id, &signer)?,
|
||||
};
|
||||
|
||||
Ok::<_, Error>(Json(json!({ "success": true })))
|
||||
Ok::<_, Error>(Json(json!({ "success": true, "id": id })))
|
||||
}
|
||||
|
||||
/// Get project issue.
|
||||
|
|
@ -830,41 +817,27 @@ async fn patch_update_handler(
|
|||
let repo = storage.repository(project)?;
|
||||
let mut patches = patch::Patches::open(&repo)?;
|
||||
let mut patch = patches.get_mut(&patch_id.into())?;
|
||||
match action {
|
||||
patch::Action::Edit { title, target } => {
|
||||
patch.edit(title, target, &signer)?;
|
||||
}
|
||||
patch::Action::Label { labels } => {
|
||||
patch.label(labels, &signer)?;
|
||||
}
|
||||
patch::Action::Lifecycle { state } => {
|
||||
patch.lifecycle(state, &signer)?;
|
||||
}
|
||||
patch::Action::Assign { assignees } => {
|
||||
patch.assign(assignees, &signer)?;
|
||||
}
|
||||
let id = match action {
|
||||
patch::Action::Edit { title, target } => patch.edit(title, target, &signer)?,
|
||||
patch::Action::Label { labels } => patch.label(labels, &signer)?,
|
||||
patch::Action::Lifecycle { state } => patch.lifecycle(state, &signer)?,
|
||||
patch::Action::Assign { assignees } => patch.assign(assignees, &signer)?,
|
||||
patch::Action::Merge { revision, commit } => {
|
||||
// TODO: We should cleanup the stored copy at least.
|
||||
let _ = patch.merge(revision, commit, &signer)?;
|
||||
patch.merge(revision, commit, &signer)?.entry
|
||||
}
|
||||
patch::Action::Review {
|
||||
revision,
|
||||
summary,
|
||||
verdict,
|
||||
labels,
|
||||
} => {
|
||||
patch.review(revision, verdict, summary, labels, &signer)?;
|
||||
}
|
||||
} => *patch.review(revision, verdict, summary, labels, &signer)?,
|
||||
patch::Action::ReviewEdit {
|
||||
review,
|
||||
summary,
|
||||
verdict,
|
||||
} => {
|
||||
patch.edit_review(review, summary, verdict, &signer)?;
|
||||
}
|
||||
patch::Action::ReviewRedact { review } => {
|
||||
patch.redact_review(review, &signer)?;
|
||||
}
|
||||
} => patch.edit_review(review, summary, verdict, &signer)?,
|
||||
patch::Action::ReviewRedact { review } => patch.redact_review(review, &signer)?,
|
||||
patch::Action::ReviewComment {
|
||||
review,
|
||||
body,
|
||||
|
|
@ -882,7 +855,7 @@ async fn patch_update_handler(
|
|||
})
|
||||
})
|
||||
.collect();
|
||||
patch.review_comment(review, body, location, reply_to, embeds, &signer)?;
|
||||
patch.review_comment(review, body, location, reply_to, embeds, &signer)?
|
||||
}
|
||||
patch::Action::ReviewCommentEdit {
|
||||
review,
|
||||
|
|
@ -900,42 +873,34 @@ async fn patch_update_handler(
|
|||
})
|
||||
})
|
||||
.collect();
|
||||
patch.edit_review_comment(review, comment, body, embeds, &signer)?;
|
||||
patch.edit_review_comment(review, comment, body, embeds, &signer)?
|
||||
}
|
||||
patch::Action::ReviewCommentReact {
|
||||
review,
|
||||
comment,
|
||||
reaction,
|
||||
active,
|
||||
} => {
|
||||
patch.react_review_comment(review, comment, reaction, active, &signer)?;
|
||||
}
|
||||
} => patch.react_review_comment(review, comment, reaction, active, &signer)?,
|
||||
patch::Action::ReviewCommentRedact { review, comment } => {
|
||||
patch.redact_review_comment(review, comment, &signer)?;
|
||||
patch.redact_review_comment(review, comment, &signer)?
|
||||
}
|
||||
patch::Action::ReviewCommentResolve { review, comment } => {
|
||||
patch.resolve_review_comment(review, comment, &signer)?;
|
||||
patch.resolve_review_comment(review, comment, &signer)?
|
||||
}
|
||||
patch::Action::ReviewCommentUnresolve { review, comment } => {
|
||||
patch.unresolve_review_comment(review, comment, &signer)?;
|
||||
patch.unresolve_review_comment(review, comment, &signer)?
|
||||
}
|
||||
patch::Action::Revision {
|
||||
description,
|
||||
base,
|
||||
oid,
|
||||
..
|
||||
} => {
|
||||
patch.update(description, base, oid, &signer)?;
|
||||
}
|
||||
} => patch.update(description, base, oid, &signer)?.into(),
|
||||
patch::Action::RevisionEdit {
|
||||
revision,
|
||||
description,
|
||||
} => {
|
||||
patch.edit_revision(revision, description, &signer)?;
|
||||
}
|
||||
patch::Action::RevisionRedact { revision } => {
|
||||
patch.redact(revision, &signer)?;
|
||||
}
|
||||
} => patch.edit_revision(revision, description, &signer)?,
|
||||
patch::Action::RevisionRedact { revision } => patch.redact(revision, &signer)?,
|
||||
patch::Action::RevisionComment {
|
||||
revision,
|
||||
body,
|
||||
|
|
@ -953,7 +918,7 @@ async fn patch_update_handler(
|
|||
})
|
||||
})
|
||||
.collect();
|
||||
patch.comment(revision, body, reply_to, location, embeds, &signer)?;
|
||||
patch.comment(revision, body, reply_to, location, embeds, &signer)?
|
||||
}
|
||||
patch::Action::RevisionCommentEdit {
|
||||
revision,
|
||||
|
|
@ -971,25 +936,23 @@ async fn patch_update_handler(
|
|||
})
|
||||
})
|
||||
.collect();
|
||||
patch.comment_edit(revision, comment, body, embeds, &signer)?;
|
||||
patch.comment_edit(revision, comment, body, embeds, &signer)?
|
||||
}
|
||||
patch::Action::RevisionCommentReact {
|
||||
revision,
|
||||
comment,
|
||||
reaction,
|
||||
active,
|
||||
} => {
|
||||
patch.comment_react(revision, comment, reaction, active, &signer)?;
|
||||
}
|
||||
} => patch.comment_react(revision, comment, reaction, active, &signer)?,
|
||||
patch::Action::RevisionCommentRedact { revision, comment } => {
|
||||
patch.comment_redact(revision, comment, &signer)?;
|
||||
patch.comment_redact(revision, comment, &signer)?
|
||||
}
|
||||
_ => {
|
||||
todo!();
|
||||
}
|
||||
};
|
||||
|
||||
Ok::<_, Error>(Json(json!({ "success": true })))
|
||||
Ok::<_, Error>(Json(json!({ "success": true, "id": id })))
|
||||
}
|
||||
|
||||
/// Get project patches list.
|
||||
|
|
@ -2058,7 +2021,7 @@ mod routes {
|
|||
|
||||
#[tokio::test]
|
||||
async fn test_projects_issues_create() {
|
||||
const CREATED_ISSUE_ID: &str = "e712eb0b5874d5256022fb620f26caf847d96723";
|
||||
const CREATED_ISSUE_ID: &str = "8b42657072f6192cba9e08561582576a975656cd";
|
||||
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let ctx = contributor(tmp.path());
|
||||
|
|
@ -2166,7 +2129,7 @@ mod routes {
|
|||
.await;
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(response.json().await, json!({ "success": true }));
|
||||
assert_eq!(response.success().await, true);
|
||||
|
||||
let body = serde_json::to_vec(&json!({
|
||||
"type": "comment.react",
|
||||
|
|
@ -2199,11 +2162,11 @@ mod routes {
|
|||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(response.json().await, json!({ "success": true }));
|
||||
assert_eq!(response.success().await, true);
|
||||
|
||||
let body = serde_json::to_vec(&json!({
|
||||
"type": "comment.redact",
|
||||
"id": "6fe9fdec2ec9f6436f2875dbcbedb95dd215b863",
|
||||
"id": "918fff44966e4305523c077c80ac93b1196f1a7e",
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
|
|
@ -2215,7 +2178,7 @@ mod routes {
|
|||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(response.json().await, json!({ "success": true }));
|
||||
assert_eq!(response.success().await, true);
|
||||
|
||||
let response = get(
|
||||
&app,
|
||||
|
|
@ -2284,7 +2247,7 @@ mod routes {
|
|||
.await;
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(response.json().await, json!({ "success": true }));
|
||||
assert_eq!(response.success().await, true);
|
||||
|
||||
let response = get(
|
||||
&app,
|
||||
|
|
@ -2423,7 +2386,7 @@ mod routes {
|
|||
|
||||
#[tokio::test]
|
||||
async fn test_projects_create_patches() {
|
||||
const CREATED_PATCH_ID: &str = "9cffd66099cceb0439a0f67c4aa99bde5e868eaa";
|
||||
const CREATED_PATCH_ID: &str = "beaed2e1d3b9b01ef10326a9a1c951799ba5fb25";
|
||||
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let ctx = contributor(tmp.path());
|
||||
|
|
@ -2686,7 +2649,7 @@ mod routes {
|
|||
"reviews": [],
|
||||
},
|
||||
{
|
||||
"id": "b1f68feacb7040b089a77c1a0bff60a0411e6c1e",
|
||||
"id": "341ba93c6db54e5891fbd3be4a4f64f4715681fa",
|
||||
"author": {
|
||||
"id": CONTRIBUTOR_DID,
|
||||
},
|
||||
|
|
@ -2857,10 +2820,12 @@ mod routes {
|
|||
.await;
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
|
||||
let comment_id = response.id().await.to_string();
|
||||
let comment_react_body = serde_json::to_vec(&json!({
|
||||
"type": "revision.comment.react",
|
||||
"revision": CONTRIBUTOR_PATCH_ID,
|
||||
"comment": CONTRIBUTOR_COMMENT_1,
|
||||
"comment": comment_id,
|
||||
"reaction": "🚀",
|
||||
"active": true
|
||||
}))
|
||||
|
|
@ -2876,7 +2841,7 @@ mod routes {
|
|||
let comment_edit = serde_json::to_vec(&json!({
|
||||
"type": "revision.comment.edit",
|
||||
"revision": CONTRIBUTOR_PATCH_ID,
|
||||
"comment": CONTRIBUTOR_COMMENT_1,
|
||||
"comment": comment_id,
|
||||
"body": "EDIT: This is a root level comment",
|
||||
"embeds": [
|
||||
{
|
||||
|
|
@ -2899,7 +2864,7 @@ mod routes {
|
|||
"type": "revision.comment",
|
||||
"revision": CONTRIBUTOR_PATCH_ID,
|
||||
"body": "This is a root level comment",
|
||||
"replyTo": CONTRIBUTOR_COMMENT_1,
|
||||
"replyTo": comment_id,
|
||||
"embeds": [],
|
||||
}))
|
||||
.unwrap();
|
||||
|
|
@ -2912,6 +2877,7 @@ mod routes {
|
|||
.await;
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let comment_id_2 = response.id().await.to_string();
|
||||
|
||||
let response = get(
|
||||
&app,
|
||||
|
|
@ -2946,7 +2912,7 @@ mod routes {
|
|||
],
|
||||
"discussions": [
|
||||
{
|
||||
"id": CONTRIBUTOR_COMMENT_1,
|
||||
"id": comment_id,
|
||||
"author": {
|
||||
"id": CONTRIBUTOR_DID,
|
||||
},
|
||||
|
|
@ -2963,7 +2929,7 @@ mod routes {
|
|||
"resolved": false,
|
||||
},
|
||||
{
|
||||
"id": CONTRIBUTOR_COMMENT_2,
|
||||
"id": comment_id_2,
|
||||
"author": {
|
||||
"id": CONTRIBUTOR_DID,
|
||||
},
|
||||
|
|
@ -2971,7 +2937,7 @@ mod routes {
|
|||
"embeds": [],
|
||||
"reactions": [],
|
||||
"timestamp": TIMESTAMP,
|
||||
"replyTo": CONTRIBUTOR_COMMENT_1,
|
||||
"replyTo": comment_id,
|
||||
"resolved": false,
|
||||
},
|
||||
],
|
||||
|
|
@ -3006,9 +2972,10 @@ mod routes {
|
|||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
|
||||
let review_id = response.id().await.to_string();
|
||||
let review_comment_body = serde_json::to_vec(&json!({
|
||||
"type": "review.comment",
|
||||
"review": CONTRIBUTOR_PATCH_REVIEW,
|
||||
"review": review_id,
|
||||
"body": "This is a comment on a review",
|
||||
"embeds": [],
|
||||
"location": {
|
||||
|
|
@ -3023,7 +2990,7 @@ mod routes {
|
|||
}
|
||||
}))
|
||||
.unwrap();
|
||||
patch(
|
||||
let response = patch(
|
||||
&app,
|
||||
format!("/projects/{CONTRIBUTOR_RID}/patches/{CONTRIBUTOR_PATCH_ID}"),
|
||||
Some(Body::from(review_comment_body)),
|
||||
|
|
@ -3031,10 +2998,11 @@ mod routes {
|
|||
)
|
||||
.await;
|
||||
|
||||
let comment_id = response.id().await.to_string();
|
||||
let review_comment_edit_body = serde_json::to_vec(&json!({
|
||||
"type": "review.comment.edit",
|
||||
"review": CONTRIBUTOR_PATCH_REVIEW,
|
||||
"comment": CONTRIBUTOR_COMMENT_3,
|
||||
"review": review_id,
|
||||
"comment": comment_id,
|
||||
"embeds": [],
|
||||
"body": "EDIT: This is a comment on a review",
|
||||
}))
|
||||
|
|
@ -3049,8 +3017,8 @@ mod routes {
|
|||
|
||||
let review_react_body = serde_json::to_vec(&json!({
|
||||
"type": "review.comment.react",
|
||||
"review": CONTRIBUTOR_PATCH_REVIEW,
|
||||
"comment": CONTRIBUTOR_COMMENT_3,
|
||||
"review": review_id,
|
||||
"comment": comment_id,
|
||||
"reaction": "🚀",
|
||||
"active": true
|
||||
}))
|
||||
|
|
@ -3065,8 +3033,8 @@ mod routes {
|
|||
|
||||
let review_resolve_body = serde_json::to_vec(&json!({
|
||||
"type": "review.comment.resolve",
|
||||
"review": CONTRIBUTOR_PATCH_REVIEW,
|
||||
"comment": CONTRIBUTOR_COMMENT_3,
|
||||
"review": review_id,
|
||||
"comment": comment_id,
|
||||
}))
|
||||
.unwrap();
|
||||
patch(
|
||||
|
|
@ -3118,7 +3086,7 @@ mod routes {
|
|||
"verdict": "accept",
|
||||
"summary": "A small review",
|
||||
"comments": [[
|
||||
CONTRIBUTOR_COMMENT_3,
|
||||
comment_id,
|
||||
{
|
||||
"author": CONTRIBUTOR_NID,
|
||||
"location": {
|
||||
|
|
|
|||
|
|
@ -143,6 +143,7 @@ pub mod logger {
|
|||
pub fn subscriber() -> impl tracing::Subscriber {
|
||||
tracing_subscriber::FmtSubscriber::builder()
|
||||
.with_target(false)
|
||||
.with_max_level(tracing::Level::DEBUG)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,20 +34,16 @@ 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 = "0b0b8ca3b75e109971f87d92c1a6c930e87484c6";
|
||||
pub const ISSUE_DISCUSSION_ID: &str = "7466975f0bef37b459887824a9655f3e78262522";
|
||||
pub const ISSUE_COMMENT_ID: &str = "24ee306c508cd731a8427612dbdd826209096f99";
|
||||
pub const ISSUE_ID: &str = "4f98396a1ac987af59ec069de9b80d9917b27050";
|
||||
pub const ISSUE_DISCUSSION_ID: &str = "ceafc6629ec8dc0a17644fb5a66726aaafc3ed1c";
|
||||
pub const ISSUE_COMMENT_ID: &str = "59d35e164a21502bc91ad3391ce49baa32ea6a74";
|
||||
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 = "7466975f0bef37b459887824a9655f3e78262522";
|
||||
pub const CONTRIBUTOR_PATCH_ID: &str = "e651ae5869a2c1ac8ad4f6deae4cc835656ffa25";
|
||||
pub const CONTRIBUTOR_PATCH_REVIEW: &str = "ee3eeba95f4ec418b3d0714e18e0d1ff605dc0e6";
|
||||
pub const CONTRIBUTOR_COMMENT_1: &str = "d8ff07edbc8d2229e54e70f2f5bc31614287f0dc";
|
||||
pub const CONTRIBUTOR_COMMENT_2: &str = "f3fca0add53f85bc51a85198efed3273fe13b88e";
|
||||
pub const CONTRIBUTOR_COMMENT_3: &str = "06990ff59faa12463f693dae7a98eb33d75afd2e";
|
||||
pub const CONTRIBUTOR_ISSUE_ID: &str = "ceafc6629ec8dc0a17644fb5a66726aaafc3ed1c";
|
||||
pub const CONTRIBUTOR_PATCH_ID: &str = "4ff2ec53a2d165da7f54705023e847d4f9230bc3";
|
||||
|
||||
/// Create a new profile.
|
||||
pub fn profile(home: &Path, seed: [u8; 32]) -> radicle::Profile {
|
||||
|
|
@ -96,6 +92,8 @@ fn seed_with_signer<G: Signer>(dir: &Path, profile: radicle::Profile, signer: &G
|
|||
let routing_db = dir.join("radicle").join("node").join("routing.db");
|
||||
let addresses_db = dir.join("radicle").join("node").join("addresses.db");
|
||||
|
||||
crate::logger::init().ok();
|
||||
|
||||
TrackingStore::Config::open(tracking_db).unwrap();
|
||||
RoutingStore::Table::open(routing_db).unwrap();
|
||||
AddressStore::Book::open(addresses_db).unwrap();
|
||||
|
|
@ -191,7 +189,7 @@ fn seed_with_signer<G: Signer>(dir: &Path, profile: radicle::Profile, signer: &G
|
|||
let storage = &profile.storage;
|
||||
let repo = storage.repository(id).unwrap();
|
||||
let mut issues = Issues::open(&repo).unwrap();
|
||||
let _ = issues
|
||||
let issue = issues
|
||||
.create(
|
||||
"Issue #1".to_string(),
|
||||
"Change 'hello world' to 'hello everyone'".to_string(),
|
||||
|
|
@ -201,12 +199,13 @@ fn seed_with_signer<G: Signer>(dir: &Path, profile: radicle::Profile, signer: &G
|
|||
signer,
|
||||
)
|
||||
.unwrap();
|
||||
tracing::debug!(target: "test", "Contributor issue: {}", issue.id());
|
||||
|
||||
// eq. rad patch open
|
||||
let mut patches = Patches::open(&repo).unwrap();
|
||||
let oid = radicle::git::Oid::from_str(HEAD).unwrap();
|
||||
let base = radicle::git::Oid::from_str(PARENT).unwrap();
|
||||
let _ = patches
|
||||
let patch = patches
|
||||
.create(
|
||||
"A new `hello world`",
|
||||
"change `hello world` in README to something else",
|
||||
|
|
@ -217,6 +216,7 @@ fn seed_with_signer<G: Signer>(dir: &Path, profile: radicle::Profile, signer: &G
|
|||
signer,
|
||||
)
|
||||
.unwrap();
|
||||
tracing::debug!(target: "test", "Contributor patch: {}", patch.id());
|
||||
|
||||
let options = crate::Options {
|
||||
aliases: std::collections::HashMap::new(),
|
||||
|
|
@ -318,10 +318,24 @@ pub struct Response(axum::response::Response);
|
|||
|
||||
impl Response {
|
||||
pub async fn json(self) -> Value {
|
||||
let body = hyper::body::to_bytes(self.0.into_body()).await.unwrap();
|
||||
let body = self.body().await;
|
||||
serde_json::from_slice(&body).unwrap()
|
||||
}
|
||||
|
||||
pub async fn id(self) -> radicle::git::Oid {
|
||||
let json = self.json().await;
|
||||
let string = json["id"].as_str().unwrap();
|
||||
|
||||
radicle::git::Oid::from_str(string).unwrap()
|
||||
}
|
||||
|
||||
pub async fn success(self) -> bool {
|
||||
let json = self.json().await;
|
||||
let success = json["success"].as_bool();
|
||||
|
||||
success.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn status(&self) -> axum::http::StatusCode {
|
||||
self.0.status()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,15 +20,14 @@ use localtime::{LocalDuration, LocalTime};
|
|||
use log::*;
|
||||
use nonempty::NonEmpty;
|
||||
|
||||
use radicle::identity;
|
||||
use radicle::node::address;
|
||||
use radicle::node::address::{AddressBook, KnownAddress};
|
||||
use radicle::node::config::PeerConfig;
|
||||
use radicle::node::ConnectOptions;
|
||||
use radicle::storage::RepositoryError;
|
||||
|
||||
use crate::crypto;
|
||||
use crate::crypto::{Signer, Verified};
|
||||
use crate::identity::IdentityError;
|
||||
use crate::identity::{Doc, Id};
|
||||
use crate::node::routing;
|
||||
use crate::node::routing::InsertResult;
|
||||
|
|
@ -118,7 +117,7 @@ pub enum Error {
|
|||
#[error(transparent)]
|
||||
Tracking(#[from] tracking::Error),
|
||||
#[error(transparent)]
|
||||
Identity(#[from] identity::IdentityError),
|
||||
Repository(#[from] radicle::storage::RepositoryError),
|
||||
#[error("namespaces error: {0}")]
|
||||
Namespaces(#[from] NamespacesError),
|
||||
}
|
||||
|
|
@ -387,11 +386,11 @@ where
|
|||
}
|
||||
|
||||
/// Lookup a project, both locally and in the routing table.
|
||||
pub fn lookup(&self, id: Id) -> Result<Lookup, LookupError> {
|
||||
let remote = self.routing.get(&id)?.iter().cloned().collect();
|
||||
pub fn lookup(&self, rid: Id) -> Result<Lookup, LookupError> {
|
||||
let remote = self.routing.get(&rid)?.iter().cloned().collect();
|
||||
|
||||
Ok(Lookup {
|
||||
local: self.storage.get(&self.node_id(), id)?,
|
||||
local: self.storage.get(rid)?,
|
||||
remote,
|
||||
})
|
||||
}
|
||||
|
|
@ -1289,7 +1288,7 @@ where
|
|||
remotes: impl IntoIterator<Item = NodeId>,
|
||||
) -> Result<(), Error> {
|
||||
let repo = self.storage.repository(rid)?;
|
||||
let (_, doc) = repo.identity_doc()?;
|
||||
let doc = repo.identity_doc()?;
|
||||
let peers = self.sessions.connected().map(|(_, p)| p);
|
||||
let timestamp = self.time();
|
||||
let mut refs = BoundedVec::<_, REF_REMOTE_LIMIT>::new();
|
||||
|
|
@ -1612,8 +1611,8 @@ pub trait ServiceState {
|
|||
fn nid(&self) -> &NodeId;
|
||||
/// Get the existing sessions.
|
||||
fn sessions(&self) -> &Sessions;
|
||||
/// Get a repository from storage, using the local node's key.
|
||||
fn get(&self, proj: Id) -> Result<Option<Doc<Verified>>, IdentityError>;
|
||||
/// Get a repository from storage.
|
||||
fn get(&self, rid: Id) -> Result<Option<Doc<Verified>>, RepositoryError>;
|
||||
/// Get the clock.
|
||||
fn clock(&self) -> &LocalTime;
|
||||
/// Get the clock mutably.
|
||||
|
|
@ -1636,8 +1635,8 @@ where
|
|||
&self.sessions
|
||||
}
|
||||
|
||||
fn get(&self, proj: Id) -> Result<Option<Doc<Verified>>, IdentityError> {
|
||||
self.storage.get(&self.node_id(), proj)
|
||||
fn get(&self, rid: Id) -> Result<Option<Doc<Verified>>, RepositoryError> {
|
||||
self.storage.get(rid)
|
||||
}
|
||||
|
||||
fn clock(&self) -> &LocalTime {
|
||||
|
|
@ -1715,12 +1714,10 @@ pub struct Lookup {
|
|||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum LookupError {
|
||||
#[error(transparent)]
|
||||
Storage(#[from] storage::Error),
|
||||
#[error(transparent)]
|
||||
Routing(#[from] routing::Error),
|
||||
#[error(transparent)]
|
||||
Identity(#[from] IdentityError),
|
||||
Repository(#[from] RepositoryError),
|
||||
}
|
||||
|
||||
/// Keeps track of the most recent announcements of a node.
|
||||
|
|
|
|||
|
|
@ -6,8 +6,7 @@ use log::error;
|
|||
use thiserror::Error;
|
||||
|
||||
use radicle::crypto::PublicKey;
|
||||
use radicle::identity::IdentityError;
|
||||
use radicle::storage::{Namespaces, ReadRepository as _, ReadStorage};
|
||||
use radicle::storage::{Namespaces, ReadRepository as _, ReadStorage, RepositoryError};
|
||||
|
||||
use crate::prelude::Id;
|
||||
use crate::service::NodeId;
|
||||
|
|
@ -37,7 +36,7 @@ pub enum NamespacesError {
|
|||
FailedDelegates {
|
||||
rid: Id,
|
||||
#[source]
|
||||
err: IdentityError,
|
||||
err: RepositoryError,
|
||||
},
|
||||
#[error("Could not find any trusted nodes for {rid}")]
|
||||
NoTrusted { rid: Id },
|
||||
|
|
|
|||
|
|
@ -250,7 +250,7 @@ impl<G: Signer + cyphernet::Ecdh> NodeHandle<G> {
|
|||
|
||||
loop {
|
||||
if let Ok(repo) = self.storage.repository(*rid) {
|
||||
if repo.identity_of(nid).is_ok() && repo.remote(nid).is_ok() {
|
||||
if repo.remote(nid).is_ok() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1289,22 +1289,6 @@ fn test_push_and_pull() {
|
|||
)
|
||||
.unwrap();
|
||||
|
||||
// Bob tracks Alice's project.
|
||||
let (sender, _) = chan::bounded(1);
|
||||
bob.command(service::Command::TrackRepo(
|
||||
proj_id,
|
||||
tracking::Scope::default(),
|
||||
sender,
|
||||
));
|
||||
|
||||
// Eve tracks Alice's project.
|
||||
let (sender, _) = chan::bounded(1);
|
||||
eve.command(service::Command::TrackRepo(
|
||||
proj_id,
|
||||
tracking::Scope::default(),
|
||||
sender,
|
||||
));
|
||||
|
||||
let mut sim = Simulation::new(
|
||||
LocalTime::now(),
|
||||
alice.rng.clone(),
|
||||
|
|
@ -1314,13 +1298,25 @@ fn test_push_and_pull() {
|
|||
|
||||
let bob_events = bob.events();
|
||||
|
||||
// Here we expect Alice to connect to Eve.
|
||||
sim.run_while([&mut alice, &mut bob, &mut eve], |s| !s.is_settled());
|
||||
|
||||
// Neither Eve nor Bob have Alice's project for now.
|
||||
assert!(eve.get(proj_id).unwrap().is_none());
|
||||
assert!(bob.get(proj_id).unwrap().is_none());
|
||||
|
||||
// Bob tracks Alice's project.
|
||||
let (sender, _) = chan::bounded(1);
|
||||
bob.command(service::Command::TrackRepo(
|
||||
proj_id,
|
||||
tracking::Scope::default(),
|
||||
sender,
|
||||
));
|
||||
// Eve tracks Alice's project.
|
||||
let (sender, _) = chan::bounded(1);
|
||||
eve.command(service::Command::TrackRepo(
|
||||
proj_id,
|
||||
tracking::Scope::default(),
|
||||
sender,
|
||||
));
|
||||
|
||||
let (send, _) = chan::bounded(1);
|
||||
// Alice announces her inventory.
|
||||
// We now expect Eve to fetch Alice's project from Alice.
|
||||
|
|
@ -1332,16 +1328,8 @@ fn test_push_and_pull() {
|
|||
|
||||
// TODO: Refs should be compared between the two peers.
|
||||
|
||||
assert!(eve
|
||||
.storage()
|
||||
.get(&alice.node_id(), proj_id)
|
||||
.unwrap()
|
||||
.is_some());
|
||||
assert!(bob
|
||||
.storage()
|
||||
.get(&alice.node_id(), proj_id)
|
||||
.unwrap()
|
||||
.is_some());
|
||||
assert!(eve.storage().get(proj_id).unwrap().is_some());
|
||||
assert!(bob.storage().get(proj_id).unwrap().is_some());
|
||||
|
||||
bob_events
|
||||
.iter()
|
||||
|
|
|
|||
|
|
@ -611,8 +611,8 @@ fn test_large_fetch() {
|
|||
)
|
||||
.unwrap();
|
||||
|
||||
let (_, doc) = bob.storage.repository(rid).unwrap().identity_doc().unwrap();
|
||||
let proj = doc.verified().unwrap().project().unwrap();
|
||||
let doc = bob.storage.repository(rid).unwrap().identity_doc().unwrap();
|
||||
let proj = doc.project().unwrap();
|
||||
|
||||
assert_eq!(proj.name(), "acme");
|
||||
}
|
||||
|
|
@ -699,24 +699,24 @@ fn test_concurrent_fetches() {
|
|||
}
|
||||
|
||||
for rid in &bob_repos {
|
||||
let (_, doc) = alice
|
||||
let doc = alice
|
||||
.storage
|
||||
.repository(*rid)
|
||||
.unwrap()
|
||||
.identity_doc()
|
||||
.unwrap();
|
||||
let proj = doc.verified().unwrap().project().unwrap();
|
||||
let proj = doc.project().unwrap();
|
||||
|
||||
assert!(proj.name().starts_with("bob"));
|
||||
}
|
||||
for rid in &alice_repos {
|
||||
let (_, doc) = bob
|
||||
let doc = bob
|
||||
.storage
|
||||
.repository(*rid)
|
||||
.unwrap()
|
||||
.identity_doc()
|
||||
.unwrap();
|
||||
let proj = doc.verified().unwrap().project().unwrap();
|
||||
let proj = doc.project().unwrap();
|
||||
|
||||
assert!(proj.name().starts_with("alice"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,7 +72,9 @@ pub enum UploadError {
|
|||
#[error(transparent)]
|
||||
Storage(#[from] radicle::storage::Error),
|
||||
#[error(transparent)]
|
||||
Identity(#[from] radicle::identity::IdentityError),
|
||||
Identity(#[from] radicle::identity::DocError),
|
||||
#[error(transparent)]
|
||||
Repository(#[from] radicle::storage::RepositoryError),
|
||||
}
|
||||
|
||||
impl UploadError {
|
||||
|
|
@ -323,7 +325,7 @@ impl Worker {
|
|||
log::debug!(target: "worker", "Received Git request pktline for {rid}..");
|
||||
|
||||
let repo = self.storage.repository(rid)?;
|
||||
let (_, doc) = repo.identity_doc()?;
|
||||
let doc = repo.identity_doc()?;
|
||||
|
||||
if !doc.is_visible_to(&remote) {
|
||||
return Err(UploadError::Unauthorized(remote, rid));
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ pub mod error;
|
|||
use std::collections::{BTreeMap, BTreeSet, HashSet};
|
||||
use std::ops::Deref;
|
||||
|
||||
use radicle::crypto::{PublicKey, Unverified, Verified};
|
||||
use radicle::crypto::{PublicKey, Verified};
|
||||
use radicle::git::refspec;
|
||||
use radicle::git::{url, Namespaced};
|
||||
use radicle::prelude::{Doc, Id, NodeId};
|
||||
|
|
@ -188,8 +188,7 @@ impl<'a> StagingPhaseInitial<'a> {
|
|||
log::debug!(target: "worker", "Loading remotes for clone of {}", repo.id);
|
||||
let oid = ReadRepository::identity_head(&repo)?;
|
||||
log::trace!(target: "worker", "Loading 'rad/id' @ {oid}");
|
||||
let (doc, _) = Doc::<Unverified>::load_at(oid, &repo)?;
|
||||
let doc = doc.verified()?;
|
||||
let doc = Doc::<Verified>::load_at(oid, &repo)?.doc;
|
||||
let mut trusted = match self.namespaces.clone() {
|
||||
Namespaces::All => HashSet::new(),
|
||||
Namespaces::Trusted(trusted) => trusted,
|
||||
|
|
@ -520,10 +519,11 @@ impl<'a> StagingPhaseFinal<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
let verification = match self.repo.identity_doc_of(&remote_id) {
|
||||
// Nb. We aren't verifying this specific remote's identity branch.
|
||||
let verification = match self.repo.identity_doc() {
|
||||
Ok(doc) => match self.repo.validate_remote(&remote) {
|
||||
Ok(unsigned) => VerifiedRemote::Success {
|
||||
_doc: doc,
|
||||
_doc: doc.into(),
|
||||
remote,
|
||||
unsigned,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -17,9 +17,11 @@ pub enum Setup {
|
|||
#[error(transparent)]
|
||||
Git(#[from] git::raw::Error),
|
||||
#[error(transparent)]
|
||||
Identity(#[from] identity::IdentityError),
|
||||
Identity(#[from] identity::DocError),
|
||||
#[error(transparent)]
|
||||
Storage(#[from] storage::Error),
|
||||
#[error(transparent)]
|
||||
Repository(#[from] radicle::storage::RepositoryError),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
|
|
@ -27,21 +29,23 @@ pub enum Transfer {
|
|||
#[error(transparent)]
|
||||
Git(#[from] git::raw::Error),
|
||||
#[error(transparent)]
|
||||
Identity(#[from] identity::IdentityError),
|
||||
Identity(#[from] identity::DocError),
|
||||
#[error(transparent)]
|
||||
Storage(#[from] storage::Error),
|
||||
#[error("no delegates in transfer")]
|
||||
NoDelegates,
|
||||
#[error(transparent)]
|
||||
Repository(#[from] radicle::storage::RepositoryError),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Transition {
|
||||
#[error(transparent)]
|
||||
Doc(#[from] identity::doc::DocError),
|
||||
#[error(transparent)]
|
||||
Git(#[from] git::raw::Error),
|
||||
#[error(transparent)]
|
||||
Identity(#[from] identity::IdentityError),
|
||||
Identity(#[from] identity::DocError),
|
||||
#[error(transparent)]
|
||||
Refs(#[from] refs::Error),
|
||||
#[error(transparent)]
|
||||
Repository(#[from] radicle::storage::RepositoryError),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,13 +13,16 @@ pub enum Error {
|
|||
Storage(#[from] radicle::storage::Error),
|
||||
/// Identity error.
|
||||
#[error(transparent)]
|
||||
Identity(#[from] radicle::identity::IdentityError),
|
||||
Identity(#[from] radicle::identity::DocError),
|
||||
/// Git error.
|
||||
#[error(transparent)]
|
||||
Git(#[from] radicle::git::ext::Error),
|
||||
/// COB store error.
|
||||
#[error(transparent)]
|
||||
CobStore(#[from] cob::store::Error),
|
||||
/// General repository error.
|
||||
#[error(transparent)]
|
||||
Repository(#[from] radicle::storage::RepositoryError),
|
||||
}
|
||||
|
||||
/// List refs for fetching (`git fetch` and `git ls-remote`).
|
||||
|
|
|
|||
|
|
@ -73,9 +73,6 @@ pub enum Error {
|
|||
/// Profile error.
|
||||
#[error(transparent)]
|
||||
Profile(#[from] radicle::profile::Error),
|
||||
/// Identity error.
|
||||
#[error(transparent)]
|
||||
Identity(#[from] radicle::identity::IdentityError),
|
||||
/// Parse error for object IDs.
|
||||
#[error(transparent)]
|
||||
ParseObjectId(#[from] ParseObjectId),
|
||||
|
|
@ -94,6 +91,9 @@ pub enum Error {
|
|||
/// COB store error.
|
||||
#[error(transparent)]
|
||||
Cob(#[from] radicle::cob::store::Error),
|
||||
/// General repository error.
|
||||
#[error(transparent)]
|
||||
Repository(#[from] radicle::storage::RepositoryError),
|
||||
}
|
||||
|
||||
enum Command {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ pub mod table;
|
|||
pub mod textarea;
|
||||
pub mod vstack;
|
||||
|
||||
use std::fmt;
|
||||
use std::io::IsTerminal;
|
||||
|
||||
pub use ansi::Color;
|
||||
pub use ansi::{paint, Filled, Paint, Style};
|
||||
pub use editor::Editor;
|
||||
|
|
@ -34,6 +37,10 @@ pub enum Interactive {
|
|||
}
|
||||
|
||||
impl Interactive {
|
||||
pub fn new(term: impl IsTerminal) -> Self {
|
||||
Self::from(term.is_terminal())
|
||||
}
|
||||
|
||||
pub fn yes(&self) -> bool {
|
||||
(*self).into()
|
||||
}
|
||||
|
|
@ -41,6 +48,14 @@ impl Interactive {
|
|||
pub fn no(&self) -> bool {
|
||||
!self.yes()
|
||||
}
|
||||
|
||||
pub fn confirm(&self, prompt: impl fmt::Display) -> bool {
|
||||
if self.yes() {
|
||||
confirm(prompt)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Interactive> for bool {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use std::fmt::{self, Display};
|
||||
use std::fmt;
|
||||
use std::fmt::Display;
|
||||
use std::str::FromStr;
|
||||
|
||||
use localtime::LocalTime;
|
||||
|
|
@ -30,6 +31,12 @@ impl Author {
|
|||
}
|
||||
}
|
||||
|
||||
impl Display for Author {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.id)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PublicKey> for Author {
|
||||
fn from(value: PublicKey) -> Self {
|
||||
Self::new(value)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -14,10 +14,9 @@ use crate::cob::thread;
|
|||
use crate::cob::thread::{Comment, CommentId, Thread};
|
||||
use crate::cob::{op, store, ActorId, Embed, EntryId, ObjectId, TypeName};
|
||||
use crate::crypto::Signer;
|
||||
use crate::git;
|
||||
use crate::identity::doc::{Doc, DocError};
|
||||
use crate::prelude::{Did, ReadRepository, Verified};
|
||||
use crate::storage::WriteRepository;
|
||||
use crate::storage::{RepositoryError, WriteRepository};
|
||||
|
||||
/// Issue operation.
|
||||
pub type Op = cob::Op<Action>;
|
||||
|
|
@ -48,6 +47,9 @@ pub enum Error {
|
|||
/// Title is invalid.
|
||||
#[error("invalid title: {0:?}")]
|
||||
InvalidTitle(String),
|
||||
/// The identity doc is missing.
|
||||
#[error("identity document missing")]
|
||||
MissingIdentity,
|
||||
/// General error initializing an issue.
|
||||
#[error("initialization failed: {0}")]
|
||||
Init(&'static str),
|
||||
|
|
@ -127,6 +129,7 @@ impl store::Cob for Issue {
|
|||
}
|
||||
|
||||
fn from_root<R: ReadRepository>(op: Op, repo: &R) -> Result<Self, Self::Error> {
|
||||
let doc = op.identity_doc(repo)?.ok_or(Error::MissingIdentity)?;
|
||||
let mut actions = op.actions.into_iter();
|
||||
let Some(Action::Comment { body, reply_to: None, embeds }) = actions.next() else {
|
||||
return Err(Error::Init("the first action must be of type `comment`"));
|
||||
|
|
@ -136,17 +139,17 @@ impl store::Cob for Issue {
|
|||
let mut issue = Issue::new(thread);
|
||||
|
||||
for action in actions {
|
||||
issue.action(action, op.id, op.author, op.timestamp, op.identity, repo)?;
|
||||
issue.action(action, op.id, op.author, op.timestamp, &doc, repo)?;
|
||||
}
|
||||
Ok(issue)
|
||||
}
|
||||
|
||||
fn op<R: ReadRepository>(&mut self, op: Op, repo: &R) -> Result<(), Error> {
|
||||
let doc = repo.identity_doc_at(op.identity)?.verified()?;
|
||||
let doc = op.identity_doc(repo)?.ok_or(Error::MissingIdentity)?;
|
||||
for action in op.actions {
|
||||
match self.authorization(&action, &op.author, &doc)? {
|
||||
Authorization::Allow => {
|
||||
self.action(action, op.id, op.author, op.timestamp, op.identity, repo)?;
|
||||
self.action(action, op.id, op.author, op.timestamp, &doc, repo)?;
|
||||
}
|
||||
Authorization::Deny => {
|
||||
return Err(Error::NotAuthorized(op.author, action));
|
||||
|
|
@ -312,7 +315,7 @@ impl Issue {
|
|||
entry: EntryId,
|
||||
author: ActorId,
|
||||
timestamp: Timestamp,
|
||||
_identity: git::Oid,
|
||||
_doc: &Doc<Verified>,
|
||||
_repo: &R,
|
||||
) -> Result<(), Error> {
|
||||
match action {
|
||||
|
|
@ -658,8 +661,9 @@ where
|
|||
R: ReadRepository + cob::Store,
|
||||
{
|
||||
/// Open an issues store.
|
||||
pub fn open(repository: &'a R) -> Result<Self, store::Error> {
|
||||
let raw = store::Store::open(repository)?;
|
||||
pub fn open(repository: &'a R) -> Result<Self, RepositoryError> {
|
||||
let identity = repository.identity_head()?;
|
||||
let raw = store::Store::open(repository)?.identity(identity);
|
||||
|
||||
Ok(Self { raw })
|
||||
}
|
||||
|
|
@ -1546,7 +1550,7 @@ mod test {
|
|||
|
||||
let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
|
||||
let eve = MockSigner::default();
|
||||
let identity = repo.identity().unwrap().head;
|
||||
let identity = repo.identity().unwrap().head();
|
||||
let missing = arbitrary::oid();
|
||||
let type_name = Issue::type_name().clone();
|
||||
let mut issues = Issues::open(&*repo).unwrap();
|
||||
|
|
@ -1578,7 +1582,7 @@ mod test {
|
|||
let contents = NonEmpty::new(action);
|
||||
let invalid = repo
|
||||
.store(
|
||||
identity,
|
||||
Some(identity),
|
||||
vec![],
|
||||
&eve,
|
||||
cob::change::Template {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,9 @@ use radicle_cob::history::{Entry, EntryId};
|
|||
use radicle_crypto::PublicKey;
|
||||
|
||||
use crate::cob::Timestamp;
|
||||
use crate::git;
|
||||
use crate::identity::DocAt;
|
||||
use crate::storage::ReadRepository;
|
||||
use crate::{git, identity};
|
||||
|
||||
/// The author of an [`Op`].
|
||||
pub type ActorId = PublicKey;
|
||||
|
|
@ -37,7 +39,7 @@ pub struct Op<A> {
|
|||
/// Parent operations.
|
||||
pub parents: Vec<EntryId>,
|
||||
/// Head of identity document committed to by this operation.
|
||||
pub identity: git::Oid,
|
||||
pub identity: Option<git::Oid>,
|
||||
/// Object manifest.
|
||||
pub manifest: Manifest,
|
||||
}
|
||||
|
|
@ -60,7 +62,7 @@ impl<A> Op<A> {
|
|||
actions: impl Into<NonEmpty<A>>,
|
||||
author: ActorId,
|
||||
timestamp: impl Into<Timestamp>,
|
||||
identity: git::Oid,
|
||||
identity: Option<git::Oid>,
|
||||
manifest: Manifest,
|
||||
) -> Self {
|
||||
Self {
|
||||
|
|
@ -77,6 +79,16 @@ impl<A> Op<A> {
|
|||
pub fn id(&self) -> EntryId {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn identity_doc<R: ReadRepository>(
|
||||
&self,
|
||||
repo: &R,
|
||||
) -> Result<Option<DocAt>, identity::DocError> {
|
||||
match self.identity {
|
||||
None => Ok(None),
|
||||
Some(head) => repo.identity_doc_at(head).map(Some),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Entry> for Op<Vec<u8>> {
|
||||
|
|
@ -101,7 +113,7 @@ where
|
|||
|
||||
fn try_from(entry: &'a Entry) -> Result<Self, Self::Error> {
|
||||
let id = *entry.id();
|
||||
let identity = *entry.resource();
|
||||
let identity = entry.resource().copied();
|
||||
let actions: Vec<_> = entry
|
||||
.contents()
|
||||
.iter()
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ use amplify::Wrapper;
|
|||
use once_cell::sync::Lazy;
|
||||
use serde::ser::SerializeStruct;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use storage::RepositoryError;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::cob;
|
||||
|
|
@ -22,7 +23,6 @@ use crate::cob::thread::{Comment, CommentId, Reactions};
|
|||
use crate::cob::{op, store, ActorId, Embed, EntryId, ObjectId, TypeName, Uri};
|
||||
use crate::crypto::{PublicKey, Signer};
|
||||
use crate::git;
|
||||
use crate::identity;
|
||||
use crate::identity::doc::DocError;
|
||||
use crate::identity::PayloadError;
|
||||
use crate::prelude::*;
|
||||
|
|
@ -99,6 +99,9 @@ pub enum Error {
|
|||
/// Error loading the identity document committed to by an operation.
|
||||
#[error("identity doc failed to load: {0}")]
|
||||
Doc(#[from] DocError),
|
||||
/// Identity document is missing.
|
||||
#[error("missing identity docuemnt")]
|
||||
MissingIdentity,
|
||||
/// Error loading the document payload.
|
||||
#[error("payload failed to load: {0}")]
|
||||
Payload(#[from] PayloadError),
|
||||
|
|
@ -340,7 +343,7 @@ pub enum MergeTarget {
|
|||
|
||||
impl MergeTarget {
|
||||
/// Get the head of the target branch.
|
||||
pub fn head<R: ReadRepository>(&self, repo: &R) -> Result<git::Oid, identity::IdentityError> {
|
||||
pub fn head<R: ReadRepository>(&self, repo: &R) -> Result<git::Oid, RepositoryError> {
|
||||
match self {
|
||||
MergeTarget::Delegates => {
|
||||
let (_, target) = repo.head()?;
|
||||
|
|
@ -1067,6 +1070,7 @@ impl store::Cob for Patch {
|
|||
}
|
||||
|
||||
fn from_root<R: ReadRepository>(op: Op, repo: &R) -> Result<Self, Self::Error> {
|
||||
let doc = op.identity_doc(repo)?.ok_or(Error::MissingIdentity)?;
|
||||
let mut actions = op.actions.into_iter();
|
||||
let Some(Action::Revision { description, base, oid, resolves }) = actions.next() else {
|
||||
return Err(Error::Init("the first action must be of type `revision`"));
|
||||
|
|
@ -1083,7 +1087,6 @@ impl store::Cob for Patch {
|
|||
resolves,
|
||||
);
|
||||
let mut patch = Patch::new(title, target, (RevisionId(op.id), revision));
|
||||
let doc = repo.identity_doc_at(op.identity)?.verified()?;
|
||||
|
||||
for action in actions {
|
||||
patch.action(action, op.id, op.author, op.timestamp, &doc, repo)?;
|
||||
|
|
@ -1095,7 +1098,7 @@ impl store::Cob for Patch {
|
|||
debug_assert!(!self.timeline.contains(&op.id));
|
||||
self.timeline.push(op.id);
|
||||
|
||||
let doc = repo.identity_doc_at(op.identity)?.verified()?;
|
||||
let doc = op.identity_doc(repo)?.ok_or(Error::MissingIdentity)?;
|
||||
|
||||
for action in op.actions {
|
||||
match self.authorization(&action, &op.author, &doc)? {
|
||||
|
|
@ -2162,9 +2165,10 @@ impl<'a, R> Patches<'a, R>
|
|||
where
|
||||
R: ReadRepository + cob::Store + 'static,
|
||||
{
|
||||
/// Open an patches store.
|
||||
pub fn open(repository: &'a R) -> Result<Self, store::Error> {
|
||||
let raw = store::Store::open(repository)?;
|
||||
/// Open a patches store.
|
||||
pub fn open(repository: &'a R) -> Result<Self, RepositoryError> {
|
||||
let identity = repository.identity_head()?;
|
||||
let raw = store::Store::open(repository)?.identity(identity);
|
||||
|
||||
Ok(Self { raw })
|
||||
}
|
||||
|
|
@ -2338,6 +2342,7 @@ mod test {
|
|||
use super::*;
|
||||
use crate::cob::test::Actor;
|
||||
use crate::crypto::test::signer::MockSigner;
|
||||
use crate::identity;
|
||||
use crate::test;
|
||||
use crate::test::arbitrary;
|
||||
use crate::test::arbitrary::gen;
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ pub enum Error {
|
|||
#[error("remove error: {0}")]
|
||||
Remove(#[from] cob::error::Remove),
|
||||
#[error(transparent)]
|
||||
Identity(#[from] identity::IdentityError),
|
||||
Identity(#[from] identity::doc::DocError),
|
||||
#[error(transparent)]
|
||||
Serialize(#[from] serde_json::Error),
|
||||
#[error("object `{1}` of type `{0}` was not found")]
|
||||
|
|
@ -100,7 +100,7 @@ pub enum Error {
|
|||
|
||||
/// Storage for collaborative objects of a specific type `T` in a single repository.
|
||||
pub struct Store<'a, T, R> {
|
||||
identity: git::Oid,
|
||||
identity: Option<git::Oid>,
|
||||
repo: &'a R,
|
||||
witness: PhantomData<T>,
|
||||
}
|
||||
|
|
@ -111,17 +111,24 @@ impl<'a, T, R> AsRef<R> for Store<'a, T, R> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, T, R: ReadRepository> Store<'a, T, R> {
|
||||
impl<'a, T, R: ReadRepository + cob::Store> Store<'a, T, R> {
|
||||
/// Open a new generic store.
|
||||
pub fn open(repo: &'a R) -> Result<Self, Error> {
|
||||
let identity = repo.identity()?;
|
||||
|
||||
Ok(Self {
|
||||
repo,
|
||||
identity: identity.head,
|
||||
identity: None,
|
||||
witness: PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
/// Return a new store with the attached identity.
|
||||
pub fn identity(self, identity: git::Oid) -> Self {
|
||||
Self {
|
||||
repo: self.repo,
|
||||
witness: self.witness,
|
||||
identity: Some(identity),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T, R> Store<'a, T, R>
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ use super::thread;
|
|||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct HistoryBuilder<T> {
|
||||
history: History,
|
||||
resource: Oid,
|
||||
resource: Option<Oid>,
|
||||
time: Timestamp,
|
||||
witness: PhantomData<T>,
|
||||
}
|
||||
|
|
@ -59,7 +59,7 @@ where
|
|||
T::Action: for<'de> Deserialize<'de> + Serialize + Eq + 'static,
|
||||
{
|
||||
pub fn new<G: Signer>(actions: &[T::Action], time: Timestamp, signer: &G) -> HistoryBuilder<T> {
|
||||
let resource = arbitrary::oid();
|
||||
let resource = Some(arbitrary::oid());
|
||||
let revision = arbitrary::oid();
|
||||
let (contents, oids): (Vec<Vec<u8>>, Vec<Oid>) = actions
|
||||
.iter()
|
||||
|
|
@ -163,7 +163,7 @@ impl<G: Signer> Actor<G> {
|
|||
pub fn op_with<T: Cob>(
|
||||
&mut self,
|
||||
actions: impl IntoIterator<Item = T::Action>,
|
||||
identity: Oid,
|
||||
identity: Option<Oid>,
|
||||
timestamp: Timestamp,
|
||||
) -> Op<T::Action>
|
||||
where
|
||||
|
|
@ -201,7 +201,7 @@ impl<G: Signer> Actor<G> {
|
|||
let identity = arbitrary::oid();
|
||||
let timestamp = Timestamp::now();
|
||||
|
||||
self.op_with::<T>(actions, identity, timestamp)
|
||||
self.op_with::<T>(actions, Some(identity), timestamp)
|
||||
}
|
||||
|
||||
/// Get the actor's DID.
|
||||
|
|
|
|||
|
|
@ -30,6 +30,9 @@ pub enum Error {
|
|||
/// that hasn't happened yet.
|
||||
#[error("causal dependency {0:?} missing")]
|
||||
Missing(EntryId),
|
||||
/// The identity doc is missing.
|
||||
#[error("identity document missing")]
|
||||
MissingIdentity,
|
||||
/// Error with comment operation.
|
||||
#[error("comment {0} is invalid")]
|
||||
Comment(EntryId),
|
||||
|
|
@ -378,6 +381,7 @@ impl cob::store::Cob for Thread {
|
|||
let author = op.author;
|
||||
let entry = op.id;
|
||||
let timestamp = op.timestamp;
|
||||
let identity = op.identity.ok_or(Error::MissingIdentity)?;
|
||||
let mut actions = op.actions.into_iter();
|
||||
let Some(Action::Comment { body, reply_to: None }) = actions.next() else {
|
||||
return Err(Error::Init("missing initial comment"));
|
||||
|
|
@ -396,14 +400,15 @@ impl cob::store::Cob for Thread {
|
|||
)?;
|
||||
|
||||
for action in actions {
|
||||
thread.action(action, entry, author, timestamp, op.identity, repo)?;
|
||||
thread.action(action, entry, author, timestamp, identity, repo)?;
|
||||
}
|
||||
Ok(thread)
|
||||
}
|
||||
|
||||
fn op<R: ReadRepository>(&mut self, op: Op<Action>, repo: &R) -> Result<(), Error> {
|
||||
let identity = op.identity.ok_or(Error::MissingIdentity)?;
|
||||
for action in op.actions {
|
||||
self.action(action, op.id, op.author, op.timestamp, op.identity, repo)?;
|
||||
self.action(action, op.id, op.author, op.timestamp, identity, repo)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -110,12 +110,12 @@ pub fn version() -> Result<Version, VersionError> {
|
|||
pub enum RefError {
|
||||
#[error("ref name is not valid UTF-8")]
|
||||
InvalidName,
|
||||
#[error("unexpected symbolic ref: {0}")]
|
||||
Symbolic(RefString),
|
||||
#[error("unexpected unqualified ref: {0}")]
|
||||
Unqualified(RefString),
|
||||
#[error("invalid ref format: {0}")]
|
||||
Format(#[from] format::Error),
|
||||
#[error("reference has no target")]
|
||||
NoTarget,
|
||||
#[error("expected ref to begin with 'refs/namespaces' but found '{0}'")]
|
||||
MissingNamespace(format::RefString),
|
||||
#[error("ref name contains invalid namespace identifier '{name}'")]
|
||||
|
|
@ -124,6 +124,8 @@ pub enum RefError {
|
|||
#[source]
|
||||
err: Box<dyn std::error::Error + Send + Sync + 'static>,
|
||||
},
|
||||
#[error(transparent)]
|
||||
Other(#[from] git2::Error),
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
|
|
@ -142,9 +144,7 @@ pub mod refs {
|
|||
pub fn qualified_from<'a>(r: &'a git2::Reference) -> Result<(Qualified<'a>, Oid), RefError> {
|
||||
let name = r.name().ok_or(RefError::InvalidName)?;
|
||||
let refstr = RefStr::try_from_str(name)?;
|
||||
let target = r
|
||||
.target()
|
||||
.ok_or_else(|| RefError::Symbolic(refstr.to_owned()))?;
|
||||
let target = r.resolve()?.target().ok_or(RefError::NoTarget)?;
|
||||
let qualified = Qualified::from_refstr(refstr)
|
||||
.ok_or_else(|| RefError::Unqualified(refstr.to_owned()))?;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,301 +1,11 @@
|
|||
#![warn(clippy::unwrap_used)]
|
||||
pub mod did;
|
||||
pub mod doc;
|
||||
pub mod project;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use radicle_git_ext::Oid;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::crypto;
|
||||
use crate::crypto::{Signature, Verified};
|
||||
use crate::git;
|
||||
use crate::storage;
|
||||
use crate::storage::git::QuorumError;
|
||||
use crate::storage::{refs, ReadRepository, RemoteId};
|
||||
|
||||
pub use crypto::PublicKey;
|
||||
pub use did::Did;
|
||||
pub use doc::{Doc, Id, IdError, PayloadError, Visibility};
|
||||
pub use doc::{Doc, DocAt, DocError, Id, IdError, PayloadError, Visibility};
|
||||
pub use project::Project;
|
||||
|
||||
/// Untrusted, well-formed input.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Untrusted;
|
||||
/// Signed by quorum of the previous delegation.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Trusted;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum IdentityError {
|
||||
#[error("git: {0}")]
|
||||
Git(#[from] git2::Error),
|
||||
#[error("git: {0}")]
|
||||
GitExt(#[from] git::Error),
|
||||
#[error("identity branches diverge from each other")]
|
||||
BranchesDiverge,
|
||||
#[error("root hash `{0}` does not match project")]
|
||||
MismatchedRoot(Oid),
|
||||
#[error("the identity branch is missing")]
|
||||
MissingBranch,
|
||||
#[error("the document root is missing")]
|
||||
MissingRoot,
|
||||
#[error("root commit is missing one or more delegate signatures")]
|
||||
MissingRootSignatures,
|
||||
#[error("quorum failed: {0}")]
|
||||
Quorum(#[from] QuorumError),
|
||||
#[error(transparent)]
|
||||
Payload(#[from] PayloadError),
|
||||
#[error("commit signature for {0} is invalid: {1}")]
|
||||
InvalidSignature(PublicKey, crypto::Error),
|
||||
#[error("threshold not reached: {0} signatures for a threshold of {1}")]
|
||||
ThresholdNotReached(usize, usize),
|
||||
#[error("identity document error: {0}")]
|
||||
Doc(#[from] doc::DocError),
|
||||
#[error(transparent)]
|
||||
Refs(#[from] refs::Error),
|
||||
#[error(transparent)]
|
||||
Storage(#[from] storage::Error),
|
||||
}
|
||||
|
||||
impl IdentityError {
|
||||
/// Whether this error is caused by something not being found.
|
||||
pub fn is_not_found(&self) -> bool {
|
||||
match self {
|
||||
Self::Doc(doc) => doc.is_not_found(),
|
||||
Self::Refs(refs) => refs.is_not_found(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Identity<I> {
|
||||
/// The head of the identity branch. This points to a commit that
|
||||
/// contains the current document blob.
|
||||
pub head: Oid,
|
||||
/// The canonical identifier for this identity.
|
||||
/// This is the object id of the initial document blob.
|
||||
pub root: I,
|
||||
/// The object id of the current document blob.
|
||||
pub current: Oid,
|
||||
/// Revision number. The initial document has a revision of `0`.
|
||||
pub revision: u32,
|
||||
/// The current document.
|
||||
pub doc: Doc<Verified>,
|
||||
/// Signatures over this identity.
|
||||
pub signatures: HashMap<PublicKey, Signature>,
|
||||
}
|
||||
|
||||
impl Identity<Oid> {
|
||||
pub fn verified(self, id: doc::Id) -> Result<Identity<doc::Id>, IdentityError> {
|
||||
// The root hash must be equal to the id.
|
||||
if self.root != *id {
|
||||
return Err(IdentityError::MismatchedRoot(self.root));
|
||||
}
|
||||
|
||||
Ok(Identity {
|
||||
root: id,
|
||||
head: self.head,
|
||||
current: self.current,
|
||||
revision: self.revision,
|
||||
doc: self.doc,
|
||||
signatures: self.signatures,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Identity<Untrusted> {
|
||||
pub fn load<R: ReadRepository>(
|
||||
remote: &RemoteId,
|
||||
repo: &R,
|
||||
) -> Result<Identity<Oid>, IdentityError> {
|
||||
let head = Doc::<Untrusted>::head(remote, repo)?;
|
||||
|
||||
Self::load_at(head, repo)
|
||||
}
|
||||
|
||||
pub fn load_at<R: ReadRepository>(head: Oid, repo: &R) -> Result<Identity<Oid>, IdentityError> {
|
||||
let mut history = repo.revwalk(head)?.collect::<Vec<_>>();
|
||||
|
||||
// Retrieve root document.
|
||||
let root_oid = history.pop().ok_or(IdentityError::MissingRoot)??.into();
|
||||
let root = Doc::<Verified>::load_at(root_oid, repo)?;
|
||||
let revision = history.len() as u32;
|
||||
|
||||
// Every identity founder must have signed the root document.
|
||||
for founder in &root.doc.delegates {
|
||||
if !root.sigs.iter().any(|(k, _)| k == &**founder) {
|
||||
return Err(IdentityError::MissingRootSignatures);
|
||||
}
|
||||
}
|
||||
|
||||
let mut current = root.blob;
|
||||
let mut trusted = root.doc;
|
||||
let mut signatures = root.sigs;
|
||||
|
||||
// Traverse the history chronologically.
|
||||
for oid in history.into_iter().rev() {
|
||||
let oid = oid?;
|
||||
let untrusted = Doc::<Verified>::load_at(oid.into(), repo)?;
|
||||
|
||||
// Check that enough delegates signed this next version.
|
||||
let quorum = untrusted
|
||||
.sigs
|
||||
.iter()
|
||||
.filter(|(key, _)| trusted.delegates.iter().any(|d| **d == **key))
|
||||
.count();
|
||||
if quorum < trusted.threshold {
|
||||
return Err(IdentityError::ThresholdNotReached(
|
||||
quorum,
|
||||
trusted.threshold,
|
||||
));
|
||||
}
|
||||
|
||||
current = untrusted.blob;
|
||||
trusted = untrusted.doc;
|
||||
signatures = untrusted.sigs;
|
||||
}
|
||||
|
||||
Ok(Identity {
|
||||
root: root.blob,
|
||||
head,
|
||||
current,
|
||||
revision,
|
||||
doc: trusted,
|
||||
signatures: signatures.into_iter().collect(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use qcheck_macros::quickcheck;
|
||||
use radicle_crypto::test::signer::MockSigner;
|
||||
use radicle_crypto::Signer as _;
|
||||
|
||||
use crate::crypto::PublicKey;
|
||||
use crate::rad;
|
||||
use crate::storage::git::Storage;
|
||||
use crate::storage::{ReadStorage, WriteRepository};
|
||||
use crate::test::fixtures;
|
||||
|
||||
use super::did::Did;
|
||||
use super::doc::PayloadId;
|
||||
use super::*;
|
||||
|
||||
#[quickcheck]
|
||||
fn prop_json_eq_str(pk: PublicKey, proj: Id, did: Did) {
|
||||
let json = serde_json::to_string(&pk).unwrap();
|
||||
assert_eq!(format!("\"{pk}\""), json);
|
||||
|
||||
let json = serde_json::to_string(&proj).unwrap();
|
||||
assert_eq!(format!("\"{}\"", proj.urn()), json);
|
||||
|
||||
let json = serde_json::to_string(&did).unwrap();
|
||||
assert_eq!(format!("\"{did}\""), json);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valid_identity() {
|
||||
let tempdir = tempfile::tempdir().unwrap();
|
||||
let mut rng = fastrand::Rng::new();
|
||||
|
||||
let alice = MockSigner::new(&mut rng);
|
||||
let bob = MockSigner::new(&mut rng);
|
||||
let eve = MockSigner::new(&mut rng);
|
||||
|
||||
let storage = Storage::open(tempdir.path().join("storage")).unwrap();
|
||||
let (id, _, _, _) =
|
||||
fixtures::project(tempdir.path().join("copy"), &storage, &alice).unwrap();
|
||||
|
||||
// Bob and Eve fork the project from Alice.
|
||||
rad::fork_remote(id, alice.public_key(), &bob, &storage).unwrap();
|
||||
rad::fork_remote(id, alice.public_key(), &eve, &storage).unwrap();
|
||||
|
||||
// TODO: In some cases we want to get the repo and the project, but don't
|
||||
// want to have to create a repository object twice. Perhaps there should
|
||||
// be a way of getting a project from a repo.
|
||||
let mut doc = storage.get(alice.public_key(), id).unwrap().unwrap();
|
||||
let prj = doc.project().unwrap();
|
||||
let repo = storage.repository(id).unwrap();
|
||||
|
||||
// Make a change to the description and sign it.
|
||||
let desc = prj.description().to_owned() + "!";
|
||||
let prj = prj.update(None, desc, None).unwrap();
|
||||
doc.payload.insert(PayloadId::project(), prj.clone().into());
|
||||
doc.sign(&alice)
|
||||
.and_then(|(_, sig)| {
|
||||
doc.update(
|
||||
alice.public_key(),
|
||||
"Update description",
|
||||
&[(alice.public_key(), sig)],
|
||||
repo.raw(),
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// Add Bob as a delegate, and sign it.
|
||||
doc.delegate(bob.public_key());
|
||||
doc.threshold = 2;
|
||||
doc.sign(&alice)
|
||||
.and_then(|(_, sig)| {
|
||||
doc.update(
|
||||
alice.public_key(),
|
||||
"Add bob",
|
||||
&[(alice.public_key(), sig)],
|
||||
repo.raw(),
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// Add Eve as a delegate, and sign it.
|
||||
doc.delegate(eve.public_key());
|
||||
doc.sign(&alice)
|
||||
.and_then(|(_, alice_sig)| {
|
||||
doc.sign(&bob).and_then(|(_, bob_sig)| {
|
||||
doc.update(
|
||||
alice.public_key(),
|
||||
"Add eve",
|
||||
&[(alice.public_key(), alice_sig), (bob.public_key(), bob_sig)],
|
||||
repo.raw(),
|
||||
)
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// Update description again with signatures by Eve and Bob.
|
||||
let desc = prj.description().to_owned() + "?";
|
||||
let prj = prj.update(None, desc, None).unwrap();
|
||||
doc.payload.insert(PayloadId::project(), prj.into());
|
||||
let (current, head) = doc
|
||||
.sign(&bob)
|
||||
.and_then(|(_, bob_sig)| {
|
||||
doc.sign(&eve).and_then(|(blob_id, eve_sig)| {
|
||||
doc.update(
|
||||
alice.public_key(),
|
||||
"Update description",
|
||||
&[(bob.public_key(), bob_sig), (eve.public_key(), eve_sig)],
|
||||
repo.raw(),
|
||||
)
|
||||
.map(|head| (blob_id, head))
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let identity: Identity<Id> = Identity::load(alice.public_key(), &repo)
|
||||
.unwrap()
|
||||
.verified(id)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(identity.signatures.len(), 2);
|
||||
assert_eq!(identity.revision, 4);
|
||||
assert_eq!(identity.root, id);
|
||||
assert_eq!(identity.current, current);
|
||||
assert_eq!(identity.head, head);
|
||||
assert_eq!(identity.doc, doc);
|
||||
|
||||
let doc = storage.get(alice.public_key(), id).unwrap().unwrap();
|
||||
assert_eq!(doc.project().unwrap().description(), "Acme's repository!?");
|
||||
}
|
||||
}
|
||||
pub use crate::cob::identity::{Error, Identity, IdentityMut};
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@ impl Deref for Did {
|
|||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
mod id;
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::fmt;
|
||||
use std::fmt::Write as _;
|
||||
use std::marker::PhantomData;
|
||||
use std::ops::{Deref, Not};
|
||||
use std::path::Path;
|
||||
|
|
@ -10,18 +9,19 @@ use std::str::FromStr;
|
|||
|
||||
use nonempty::NonEmpty;
|
||||
use once_cell::sync::Lazy;
|
||||
use radicle_cob::type_name::{TypeName, TypeNameParse};
|
||||
use radicle_git_ext::Oid;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::canonical::formatter::CanonicalFormatter;
|
||||
use crate::cob::identity;
|
||||
use crate::crypto;
|
||||
use crate::crypto::{Signature, Unverified, Verified};
|
||||
use crate::git;
|
||||
use crate::identity::{project::Project, Did};
|
||||
use crate::storage;
|
||||
use crate::storage::git::trailers;
|
||||
use crate::storage::{ReadRepository, RemoteId};
|
||||
use crate::storage::{ReadRepository, RepositoryError};
|
||||
|
||||
pub use crypto::PublicKey;
|
||||
pub use id::*;
|
||||
|
|
@ -35,24 +35,18 @@ pub const MAX_DELEGATES: usize = 255;
|
|||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum DocError {
|
||||
#[error("invalid commit: {0}")]
|
||||
Commit(&'static str),
|
||||
#[error("json: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
#[error("invalid delegates: {0}")]
|
||||
Delegates(&'static str),
|
||||
#[error("invalid signature for {0}: {1}")]
|
||||
Signature(PublicKey, crypto::Error),
|
||||
#[error("invalid commit trailers: {0}")]
|
||||
Trailers(#[from] trailers::Error),
|
||||
#[error("invalid version `{0}`")]
|
||||
Version(u32),
|
||||
#[error("invalid threshold `{0}`: {1}")]
|
||||
Threshold(usize, &'static str),
|
||||
#[error("git: {0}")]
|
||||
GitExt(#[from] git::Error),
|
||||
#[error("git: {0}")]
|
||||
Git(#[from] git2::Error),
|
||||
#[error("missing identity document")]
|
||||
Missing,
|
||||
}
|
||||
|
||||
impl DocError {
|
||||
|
|
@ -70,8 +64,7 @@ impl DocError {
|
|||
/// Identifies an identity document payload type.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
// TODO: Restrict values.
|
||||
pub struct PayloadId(String);
|
||||
pub struct PayloadId(TypeName);
|
||||
|
||||
impl fmt::Display for PayloadId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
|
|
@ -79,10 +72,22 @@ impl fmt::Display for PayloadId {
|
|||
}
|
||||
}
|
||||
|
||||
impl FromStr for PayloadId {
|
||||
type Err = TypeNameParse;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
TypeName::from_str(s).map(Self)
|
||||
}
|
||||
}
|
||||
|
||||
impl PayloadId {
|
||||
/// Project payload type.
|
||||
pub fn project() -> Self {
|
||||
Self(String::from("xyz.radicle.project"))
|
||||
Self(
|
||||
// SAFETY: We know this is valid.
|
||||
TypeName::from_str("xyz.radicle.project")
|
||||
.expect("PayloadId::project: type name is valid"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -101,6 +106,15 @@ pub struct Payload {
|
|||
value: serde_json::Value,
|
||||
}
|
||||
|
||||
impl Payload {
|
||||
/// Get a mutable reference to the JSON map, or `None` if the payload is not a map.
|
||||
pub fn as_object_mut(
|
||||
&mut self,
|
||||
) -> Option<&mut serde_json::value::Map<String, serde_json::Value>> {
|
||||
self.value.as_object_mut()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Value> for Payload {
|
||||
fn from(value: serde_json::Value) -> Self {
|
||||
Self { value }
|
||||
|
|
@ -124,8 +138,6 @@ pub struct DocAt {
|
|||
pub blob: Oid,
|
||||
/// The parsed document.
|
||||
pub doc: Doc<Verified>,
|
||||
/// The validated commit signatures.
|
||||
pub sigs: HashMap<PublicKey, Signature>,
|
||||
}
|
||||
|
||||
impl Deref for DocAt {
|
||||
|
|
@ -136,6 +148,18 @@ impl Deref for DocAt {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<DocAt> for Doc<Verified> {
|
||||
fn from(value: DocAt) -> Self {
|
||||
value.doc
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<Doc<Verified>> for DocAt {
|
||||
fn as_ref(&self) -> &Doc<Verified> {
|
||||
&self.doc
|
||||
}
|
||||
}
|
||||
|
||||
/// Repository visibility.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", tag = "type")]
|
||||
|
|
@ -209,21 +233,33 @@ impl<V> Doc<V> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn canonical_head(repo: &storage::git::Repository) -> Result<Oid, DocError> {
|
||||
repo.backend
|
||||
.refname_to_id(storage::git::CANONICAL_IDENTITY.as_str())
|
||||
.map(Oid::from)
|
||||
.map_err(DocError::from)
|
||||
/// Validate signature using this document's delegates, against a given document blob.
|
||||
pub fn verify_signature(
|
||||
&self,
|
||||
key: &PublicKey,
|
||||
signature: &Signature,
|
||||
blob: Oid,
|
||||
) -> Result<(), PublicKey> {
|
||||
if !self.is_delegate(key) {
|
||||
return Err(*key);
|
||||
}
|
||||
if key.verify(blob.as_bytes(), signature).is_err() {
|
||||
return Err(*key);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn head<R: ReadRepository>(remote: &RemoteId, repo: &R) -> Result<Oid, DocError> {
|
||||
repo.reference_oid(remote, &git::refs::storage::IDENTITY_BRANCH)
|
||||
.map_err(DocError::from)
|
||||
pub fn is_majority(&self, votes: usize) -> bool {
|
||||
votes >= self.majority()
|
||||
}
|
||||
|
||||
pub fn majority(&self) -> usize {
|
||||
self.delegates.len() / 2 + 1
|
||||
}
|
||||
|
||||
pub fn blob_at<R: ReadRepository>(commit: Oid, repo: &R) -> Result<git2::Blob, DocError> {
|
||||
repo.blob_at(commit, Path::new(&*PATH))
|
||||
.map_err(DocError::from)
|
||||
let path = Path::new("embeds").join(*PATH);
|
||||
repo.blob_at(commit, path.as_path()).map_err(DocError::from)
|
||||
}
|
||||
|
||||
pub fn is_delegate(&self, key: &crypto::PublicKey) -> bool {
|
||||
|
|
@ -282,112 +318,58 @@ impl Doc<Verified> {
|
|||
Ok(proj)
|
||||
}
|
||||
|
||||
pub fn sign<G: crypto::Signer>(&self, signer: &G) -> Result<(git::Oid, Signature), DocError> {
|
||||
let (oid, _) = self.encode()?;
|
||||
pub fn sign<G: crypto::Signer>(
|
||||
&self,
|
||||
signer: &G,
|
||||
) -> Result<(git::Oid, Vec<u8>, Signature), DocError> {
|
||||
let (oid, bytes) = self.encode()?;
|
||||
let sig = signer.sign(oid.as_bytes());
|
||||
|
||||
Ok((oid, sig))
|
||||
Ok((oid, bytes, sig))
|
||||
}
|
||||
|
||||
pub fn canonical(repo: &storage::git::Repository) -> Result<DocAt, DocError> {
|
||||
let oid = Self::canonical_head(repo)?;
|
||||
Self::load_at(oid, repo)
|
||||
pub fn signature_of<G: crypto::Signer>(&self, signer: &G) -> Result<Signature, DocError> {
|
||||
let (_, _, sig) = self.sign(signer)?;
|
||||
|
||||
Ok(sig)
|
||||
}
|
||||
|
||||
pub fn load_at<R: ReadRepository>(oid: Oid, repo: &R) -> Result<DocAt, DocError> {
|
||||
let blob = Self::blob_at(oid, repo)?;
|
||||
let doc = Doc::from_json(blob.content())?.verified()?;
|
||||
let commit = repo.commit(oid)?;
|
||||
let msg = commit
|
||||
.message_raw()
|
||||
.ok_or(DocError::Commit("commit message is not UTF-8"))?;
|
||||
let sigs = trailers::parse_signatures(msg)?;
|
||||
pub fn load_at<R: ReadRepository>(commit: Oid, repo: &R) -> Result<DocAt, DocError> {
|
||||
let blob = Self::blob_at(commit, repo)?;
|
||||
let doc = Doc::from_blob(&blob)?;
|
||||
|
||||
for (pk, sig) in &sigs {
|
||||
if let Err(err) = pk.verify(blob.id().as_bytes(), sig) {
|
||||
return Err(DocError::Signature(*pk, err));
|
||||
}
|
||||
}
|
||||
Ok(DocAt {
|
||||
commit: oid,
|
||||
commit,
|
||||
doc,
|
||||
blob: blob.id().into(),
|
||||
sigs,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn init(
|
||||
doc: &[u8],
|
||||
remote: &RemoteId,
|
||||
signatures: &[(&PublicKey, Signature)],
|
||||
repo: &git2::Repository,
|
||||
) -> Result<git::Oid, DocError> {
|
||||
let tree = git::write_tree(*PATH, doc, repo)?;
|
||||
let oid = Doc::commit(remote, &tree, "Initialize Radicle\n", &[], signatures, repo)?;
|
||||
|
||||
Ok(oid)
|
||||
pub fn from_blob(blob: &git2::Blob) -> Result<Self, DocError> {
|
||||
Doc::from_json(blob.content())?.verified()
|
||||
}
|
||||
|
||||
pub fn update(
|
||||
pub fn init<G: crypto::Signer>(
|
||||
&self,
|
||||
remote: &RemoteId,
|
||||
msg: &str,
|
||||
signatures: &[(&PublicKey, Signature)],
|
||||
repo: &git2::Repository,
|
||||
) -> Result<git::Oid, DocError> {
|
||||
let (_, doc) = self.encode()?;
|
||||
let tree = git::write_tree(*PATH, doc.as_slice(), repo)?;
|
||||
let id_ref = git::refs::storage::id(remote);
|
||||
let head = repo.find_reference(&id_ref)?.peel_to_commit()?;
|
||||
let oid = Doc::commit(remote, &tree, msg, &[&head], signatures, repo)?;
|
||||
repo: &storage::git::Repository,
|
||||
signer: &G,
|
||||
) -> Result<git::Oid, RepositoryError> {
|
||||
let cob = identity::Identity::initialize(self, repo, signer)?;
|
||||
let id_ref = git::refs::storage::id(signer.public_key());
|
||||
let cob_ref = git::refs::storage::cob(
|
||||
signer.public_key(),
|
||||
&crate::cob::identity::TYPENAME,
|
||||
&cob.id,
|
||||
);
|
||||
// Set `.../refs/rad/id` -> `.../refs/cobs/xyz.radicle.id/<id>`
|
||||
repo.backend.reference_symbolic(
|
||||
id_ref.as_str(),
|
||||
cob_ref.as_str(),
|
||||
false,
|
||||
"Create `rad/id` reference to point to new identity COB",
|
||||
)?;
|
||||
|
||||
Ok(oid)
|
||||
}
|
||||
|
||||
fn commit(
|
||||
remote: &RemoteId,
|
||||
tree: &git2::Tree,
|
||||
msg: &str,
|
||||
parents: &[&git2::Commit],
|
||||
signatures: &[(&PublicKey, Signature)],
|
||||
repo: &git2::Repository,
|
||||
) -> Result<git::Oid, DocError> {
|
||||
let sig = repo
|
||||
.signature()
|
||||
.or_else(|_| git2::Signature::now("radicle", remote.to_string().as_str()))?;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
let sig = if let Ok(s) = std::env::var("RAD_COMMIT_TIME") {
|
||||
// SAFETY: Only used in test code.
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let timestamp = s.trim().parse::<i64>().unwrap();
|
||||
let time = git2::Time::new(timestamp, 0);
|
||||
git2::Signature::new("radicle", remote.to_string().as_str(), &time)?
|
||||
} else {
|
||||
sig
|
||||
};
|
||||
|
||||
let mut msg = format!("{}\n\n", msg.trim());
|
||||
for (key, sig) in signatures {
|
||||
writeln!(&mut msg, "{}: {key} {sig}", trailers::SIGNATURE_TRAILER)
|
||||
.expect("in-memory writes don't fail");
|
||||
}
|
||||
|
||||
let id_ref = git::refs::storage::id(remote);
|
||||
let oid = repo.commit(Some(&id_ref), &sig, &sig, &msg, tree, parents)?;
|
||||
|
||||
Ok(oid.into())
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test"))]
|
||||
pub(crate) fn unverified(self) -> Doc<Unverified> {
|
||||
Doc {
|
||||
payload: self.payload,
|
||||
delegates: self.delegates,
|
||||
threshold: self.threshold,
|
||||
visibility: self.visibility,
|
||||
verified: PhantomData,
|
||||
}
|
||||
Ok(*cob.id)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -446,22 +428,10 @@ impl Doc<Unverified> {
|
|||
verified: PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn load_at<R: ReadRepository>(commit: Oid, repo: &R) -> Result<(Self, Oid), DocError> {
|
||||
let blob = Self::blob_at(commit, repo)?;
|
||||
let doc = Doc::from_json(blob.content())?;
|
||||
|
||||
Ok((doc, blob.id().into()))
|
||||
}
|
||||
|
||||
pub fn load<R: ReadRepository>(remote: &RemoteId, repo: &R) -> Result<(Self, Oid), DocError> {
|
||||
let oid = Self::head(remote, repo)?;
|
||||
|
||||
Self::load_at(oid, repo)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod test {
|
||||
use radicle_crypto::test::signer::MockSigner;
|
||||
use radicle_crypto::Signer as _;
|
||||
|
|
@ -469,7 +439,7 @@ mod test {
|
|||
use crate::rad;
|
||||
use crate::storage::git::transport;
|
||||
use crate::storage::git::Storage;
|
||||
use crate::storage::{ReadStorage as _, WriteStorage as _};
|
||||
use crate::storage::{ReadStorage as _, RemoteId, WriteStorage as _};
|
||||
use crate::test::arbitrary;
|
||||
use crate::test::fixtures;
|
||||
|
||||
|
|
@ -516,10 +486,10 @@ mod test {
|
|||
let repo = storage.create(proj).unwrap();
|
||||
let oid = git2::Oid::from_str("2d52a53ce5e4f141148a5f770cfd3ead2d6a45b8").unwrap();
|
||||
|
||||
let err = Doc::<Unverified>::head(&remote, &repo).unwrap_err();
|
||||
assert!(err.is_not_found());
|
||||
let err = repo.identity_head_of(&remote).unwrap_err();
|
||||
matches!(err, git::ext::Error::NotFound(_));
|
||||
|
||||
let err = Doc::<Unverified>::load_at(oid.into(), &repo).unwrap_err();
|
||||
let err = Doc::<Verified>::load_at(oid.into(), &repo).unwrap_err();
|
||||
assert!(err.is_not_found());
|
||||
}
|
||||
|
||||
|
|
@ -544,7 +514,7 @@ mod test {
|
|||
.unwrap();
|
||||
let repo = storage.repository(rid).unwrap();
|
||||
|
||||
assert_eq!(doc, Doc::canonical(&repo).unwrap().doc);
|
||||
assert_eq!(doc, repo.identity_doc().unwrap().doc);
|
||||
}
|
||||
|
||||
#[quickcheck]
|
||||
|
|
|
|||
|
|
@ -136,6 +136,7 @@ impl From<&Id> for Component<'_> {
|
|||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use qcheck_macros::quickcheck;
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ pub mod storage;
|
|||
pub mod test;
|
||||
pub mod version;
|
||||
|
||||
pub use cob::{issue, patch};
|
||||
pub use node::Node;
|
||||
pub use profile::Profile;
|
||||
pub use storage::git::Storage;
|
||||
|
|
|
|||
|
|
@ -9,12 +9,13 @@ use thiserror::Error;
|
|||
use crate::cob::ObjectId;
|
||||
use crate::crypto::{Signer, Verified};
|
||||
use crate::git;
|
||||
use crate::identity::doc;
|
||||
use crate::identity::doc::{DocError, Id, Visibility};
|
||||
use crate::identity::project::Project;
|
||||
use crate::identity::{doc, IdentityError};
|
||||
use crate::storage::git::transport;
|
||||
use crate::storage::git::Repository;
|
||||
use crate::storage::refs::SignedRefs;
|
||||
use crate::storage::RepositoryError;
|
||||
use crate::storage::{BranchName, ReadRepository as _, RemoteId, SignRepository as _};
|
||||
use crate::storage::{WriteRepository, WriteStorage};
|
||||
use crate::{identity, storage};
|
||||
|
|
@ -30,8 +31,8 @@ pub static PATCHES_REFNAME: Lazy<git::RefString> = Lazy::new(|| git::refname!("r
|
|||
pub enum InitError {
|
||||
#[error("doc: {0}")]
|
||||
Doc(#[from] DocError),
|
||||
#[error("project: {0}")]
|
||||
Identity(#[from] IdentityError),
|
||||
#[error("repository: {0}")]
|
||||
Repository(#[from] RepositoryError),
|
||||
#[error("project payload: {0}")]
|
||||
ProjectPayload(String),
|
||||
#[error("git: {0}")]
|
||||
|
|
@ -40,12 +41,6 @@ pub enum InitError {
|
|||
Io(#[from] io::Error),
|
||||
#[error("storage: {0}")]
|
||||
Storage(#[from] storage::Error),
|
||||
#[error("cannot initialize project inside a bare repository")]
|
||||
BareRepo,
|
||||
#[error("cannot initialize project from detached head state")]
|
||||
DetachedHead,
|
||||
#[error("HEAD reference is not valid UTF-8")]
|
||||
InvalidHead,
|
||||
}
|
||||
|
||||
/// Initialize a new radicle project from a git repository.
|
||||
|
|
@ -75,7 +70,7 @@ pub fn init<G: Signer, S: WriteStorage>(
|
|||
)
|
||||
})?;
|
||||
let doc = identity::Doc::initial(proj, delegate, visibility).verified()?;
|
||||
let (project, _) = Repository::init(&doc, pk, storage, signer)?;
|
||||
let (project, _) = Repository::init(&doc, storage, signer)?;
|
||||
let url = git::Url::from(project.id);
|
||||
|
||||
git::configure_repository(repo)?;
|
||||
|
|
@ -88,33 +83,26 @@ pub fn init<G: Signer, S: WriteStorage>(
|
|||
&git::fmt::lit::refs_heads(&default_branch).into(),
|
||||
)],
|
||||
)?;
|
||||
|
||||
let signed = project.sign_refs(signer)?;
|
||||
let _head = project.set_head()?;
|
||||
let _head = project.set_identity_head()?;
|
||||
let _head = project.set_head()?;
|
||||
|
||||
Ok((project.id, doc, signed))
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ForkError {
|
||||
#[error("ref string: {0}")]
|
||||
RefString(#[from] git::fmt::Error),
|
||||
#[error("git: {0}")]
|
||||
Git(#[from] git2::Error),
|
||||
#[error("git: {0}")]
|
||||
GitExt(#[from] git::Error),
|
||||
#[error("storage: {0}")]
|
||||
Storage(#[from] storage::Error),
|
||||
#[error("payload: {0}")]
|
||||
Payload(#[from] doc::PayloadError),
|
||||
#[error("project `{0}` was not found in storage")]
|
||||
NotFound(Id),
|
||||
#[error("project identity error: {0}")]
|
||||
InvalidIdentity(#[from] IdentityError),
|
||||
#[error("project identity document error: {0}")]
|
||||
Doc(#[from] DocError),
|
||||
#[error("git: invalid reference")]
|
||||
InvalidReference,
|
||||
#[error("repository: {0}")]
|
||||
Repository(#[from] RepositoryError),
|
||||
}
|
||||
|
||||
/// Create a local tree for an existing project, from an existing remote.
|
||||
|
|
@ -129,14 +117,11 @@ pub fn fork_remote<G: Signer, S: storage::WriteStorage>(
|
|||
// Creates or copies the following references:
|
||||
//
|
||||
// refs/namespaces/<pk>/refs/heads/master
|
||||
// refs/namespaces/<pk>/refs/rad/id
|
||||
// refs/namespaces/<pk>/refs/rad/sigrefs
|
||||
// refs/namespaces/<pk>/refs/tags/*
|
||||
|
||||
let me = signer.public_key();
|
||||
let doc = storage
|
||||
.get(remote, proj)?
|
||||
.ok_or(ForkError::NotFound(proj))?;
|
||||
let doc = storage.get(proj)?.ok_or(ForkError::NotFound(proj))?;
|
||||
let project = doc.project()?;
|
||||
let repository = storage.repository_mut(proj)?;
|
||||
|
||||
|
|
@ -151,15 +136,6 @@ pub fn fork_remote<G: Signer, S: storage::WriteStorage>(
|
|||
false,
|
||||
&format!("creating default branch for {me}"),
|
||||
)?;
|
||||
|
||||
let remote_id = raw.refname_to_id(&git::refs::storage::id(remote))?;
|
||||
raw.reference(
|
||||
&git::refs::storage::id(me),
|
||||
remote_id,
|
||||
false,
|
||||
&format!("creating identity branch for {me}"),
|
||||
)?;
|
||||
|
||||
repository.sign_refs(signer)?;
|
||||
|
||||
Ok(())
|
||||
|
|
@ -172,7 +148,6 @@ pub fn fork<G: Signer, S: storage::WriteStorage>(
|
|||
) -> Result<(), ForkError> {
|
||||
let me = signer.public_key();
|
||||
let repository = storage.repository_mut(rid)?;
|
||||
let (canonical_id, _) = repository.identity_doc()?;
|
||||
let (canonical_branch, canonical_head) = repository.head()?;
|
||||
let raw = repository.raw();
|
||||
|
||||
|
|
@ -182,12 +157,6 @@ pub fn fork<G: Signer, S: storage::WriteStorage>(
|
|||
true,
|
||||
&format!("creating default branch for {me}"),
|
||||
)?;
|
||||
raw.reference(
|
||||
&git::refs::storage::id(me),
|
||||
canonical_id.into(),
|
||||
true,
|
||||
&format!("creating identity branch for {me}"),
|
||||
)?;
|
||||
repository.sign_refs(signer)?;
|
||||
|
||||
Ok(())
|
||||
|
|
@ -199,14 +168,12 @@ pub enum CheckoutError {
|
|||
Fetch(#[source] git2::Error),
|
||||
#[error("git: {0}")]
|
||||
Git(#[from] git2::Error),
|
||||
#[error("storage: {0}")]
|
||||
Storage(#[from] storage::Error),
|
||||
#[error("payload: {0}")]
|
||||
Payload(#[from] doc::PayloadError),
|
||||
#[error("project `{0}` was not found in storage")]
|
||||
NotFound(Id),
|
||||
#[error("project error: {0}")]
|
||||
Identity(#[from] IdentityError),
|
||||
#[error("repository: {0}")]
|
||||
Repository(#[from] RepositoryError),
|
||||
}
|
||||
|
||||
/// Checkout a project from storage as a working copy.
|
||||
|
|
@ -219,9 +186,7 @@ pub fn checkout<P: AsRef<Path>, S: storage::ReadStorage>(
|
|||
) -> Result<git2::Repository, CheckoutError> {
|
||||
// TODO: Decide on whether we can use `clone_local`
|
||||
// TODO: Look into sharing object databases.
|
||||
let doc = storage
|
||||
.get(remote, proj)?
|
||||
.ok_or(CheckoutError::NotFound(proj))?;
|
||||
let doc = storage.get(proj)?.ok_or(CheckoutError::NotFound(proj))?;
|
||||
let project = doc.project()?;
|
||||
|
||||
let mut opts = git2::RepositoryInitOptions::new();
|
||||
|
|
@ -364,6 +329,7 @@ pub fn setup_patch_upstream<'a>(
|
|||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
use radicle_crypto::test::signer::MockSigner;
|
||||
|
||||
use crate::git::{name::component, qualified};
|
||||
|
|
@ -396,7 +362,7 @@ mod tests {
|
|||
)
|
||||
.unwrap();
|
||||
|
||||
let doc = storage.get(&public_key, proj).unwrap().unwrap();
|
||||
let doc = storage.get(proj).unwrap().unwrap();
|
||||
let project = doc.project().unwrap();
|
||||
let remotes: HashMap<_, _> = storage
|
||||
.repository(proj)
|
||||
|
|
|
|||
|
|
@ -14,13 +14,13 @@ use crypto::{PublicKey, Signer, Unverified, Verified};
|
|||
pub use git::VerifyError;
|
||||
pub use radicle_git_ext::Oid;
|
||||
|
||||
use crate::cob;
|
||||
use crate::collections::RandomMap;
|
||||
use crate::git::ext as git_ext;
|
||||
use crate::git::{refspec::Refspec, PatternString, Qualified, RefError, RefString};
|
||||
use crate::identity;
|
||||
use crate::identity::doc::DocError;
|
||||
use crate::identity::Did;
|
||||
use crate::identity::{Id, Identity, IdentityError};
|
||||
use crate::identity::{Did, PayloadError};
|
||||
use crate::identity::{Doc, DocAt, DocError};
|
||||
use crate::identity::{Id, Identity};
|
||||
use crate::storage::git::NAMESPACES_GLOB;
|
||||
use crate::storage::refs::Refs;
|
||||
|
||||
|
|
@ -68,6 +68,27 @@ impl FromIterator<PublicKey> for Namespaces {
|
|||
}
|
||||
}
|
||||
|
||||
/// Repository error.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum RepositoryError {
|
||||
#[error(transparent)]
|
||||
Storage(#[from] Error),
|
||||
#[error(transparent)]
|
||||
Store(#[from] cob::store::Error),
|
||||
#[error(transparent)]
|
||||
Doc(#[from] DocError),
|
||||
#[error(transparent)]
|
||||
Payload(#[from] PayloadError),
|
||||
#[error(transparent)]
|
||||
Git(#[from] git::raw::Error),
|
||||
#[error(transparent)]
|
||||
GitExt(#[from] git_ext::Error),
|
||||
#[error(transparent)]
|
||||
Quorum(#[from] git::QuorumError),
|
||||
#[error(transparent)]
|
||||
Refs(#[from] refs::Error),
|
||||
}
|
||||
|
||||
/// Storage error.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
|
|
@ -113,9 +134,10 @@ pub enum FetchError {
|
|||
Verify(#[from] git::VerifyError),
|
||||
#[error(transparent)]
|
||||
Storage(#[from] Error),
|
||||
// TODO: This should wrap a more specific error.
|
||||
#[error("repository head: {0}")]
|
||||
SetHead(#[from] IdentityError),
|
||||
SetHead(#[from] DocError),
|
||||
#[error("repository: {0}")]
|
||||
Repository(#[from] RepositoryError),
|
||||
}
|
||||
|
||||
pub type RemoteId = PublicKey;
|
||||
|
|
@ -294,19 +316,21 @@ pub trait ReadStorage {
|
|||
fn path(&self) -> &Path;
|
||||
/// Get a repository's path.
|
||||
fn path_of(&self, rid: &Id) -> PathBuf;
|
||||
/// Get an identity document of a repository under a given remote.
|
||||
fn get(
|
||||
&self,
|
||||
remote: &RemoteId,
|
||||
rid: Id,
|
||||
) -> Result<Option<identity::Doc<Verified>>, IdentityError>;
|
||||
/// Check whether storage contains a repository.
|
||||
fn contains(&self, rid: &Id) -> Result<bool, IdentityError>;
|
||||
fn contains(&self, rid: &Id) -> Result<bool, RepositoryError>;
|
||||
/// Get the inventory of repositories hosted under this storage.
|
||||
/// This function should typically only return public repositories.
|
||||
fn inventory(&self) -> Result<Inventory, Error>;
|
||||
/// Open or create a read-only repository.
|
||||
fn repository(&self, rid: Id) -> Result<Self::Repository, Error>;
|
||||
/// Get a repository's identity if it exists.
|
||||
fn get(&self, rid: Id) -> Result<Option<Doc<Verified>>, RepositoryError> {
|
||||
match self.repository(rid) {
|
||||
Ok(repo) => Ok(Some(repo.identity_doc()?.into())),
|
||||
Err(e) if e.is_not_found() => Ok(None),
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Allows access to individual storage repositories.
|
||||
|
|
@ -331,8 +355,7 @@ pub trait ReadRepository: Sized {
|
|||
fn path(&self) -> &Path;
|
||||
|
||||
/// Get a blob in this repository at the given commit and path.
|
||||
fn blob_at<'a>(&'a self, commit: Oid, path: &'a Path)
|
||||
-> Result<git2::Blob<'a>, git_ext::Error>;
|
||||
fn blob_at<P: AsRef<Path>>(&self, commit: Oid, path: P) -> Result<git2::Blob, git_ext::Error>;
|
||||
|
||||
/// Get a blob in this repository, given its id.
|
||||
fn blob(&self, oid: Oid) -> Result<git2::Blob, git_ext::Error>;
|
||||
|
|
@ -357,32 +380,50 @@ pub trait ReadRepository: Sized {
|
|||
/// head using [`ReadRepository::canonical_head`].
|
||||
///
|
||||
/// Returns the [`Oid`] as well as the qualified reference name.
|
||||
fn head(&self) -> Result<(Qualified, Oid), IdentityError>;
|
||||
fn head(&self) -> Result<(Qualified, Oid), RepositoryError>;
|
||||
|
||||
/// Compute the canonical head of this repository.
|
||||
///
|
||||
/// Ignores any existing `HEAD` reference.
|
||||
///
|
||||
/// Returns the [`Oid`] as well as the qualified reference name.
|
||||
fn canonical_head(&self) -> Result<(Qualified, Oid), IdentityError>;
|
||||
fn canonical_head(&self) -> Result<(Qualified, Oid), RepositoryError>;
|
||||
|
||||
/// Get the head of the `rad/id` reference in this repository.
|
||||
///
|
||||
/// Returns the reference pointed to by `rad/id` if it is set. Otherwise, computes the canonical
|
||||
/// `rad/id` using [`ReadRepository::canonical_identity_head`].
|
||||
fn identity_head(&self) -> Result<Oid, IdentityError>;
|
||||
fn identity_head(&self) -> Result<Oid, RepositoryError>;
|
||||
|
||||
/// Load the canonical identity.
|
||||
fn identity(&self) -> Result<Identity<Oid>, IdentityError> {
|
||||
let head = self.identity_head()?;
|
||||
/// Get the identity head of a specific remote.
|
||||
fn identity_head_of(&self, remote: &RemoteId) -> Result<Oid, git::ext::Error>;
|
||||
|
||||
Identity::load_at(head, self)
|
||||
/// Get the root commit of the canonical identity branch.
|
||||
fn identity_root(&self) -> Result<Oid, RepositoryError>;
|
||||
|
||||
/// Get the root commit of the identity branch of a sepcific remote.
|
||||
fn identity_root_of(&self, remote: &RemoteId) -> Result<Oid, RepositoryError>;
|
||||
|
||||
/// Load the identity history.
|
||||
fn identity(&self) -> Result<Identity, RepositoryError>
|
||||
where
|
||||
Self: cob::Store,
|
||||
{
|
||||
Identity::load(self)
|
||||
}
|
||||
|
||||
/// Compute the canonical `rad/id` of this repository.
|
||||
///
|
||||
/// Ignores any existing `rad/id` reference.
|
||||
fn canonical_identity_head(&self) -> Result<Oid, IdentityError>;
|
||||
fn canonical_identity_head(&self) -> Result<Oid, RepositoryError>;
|
||||
|
||||
/// Compute the canonical identity document.
|
||||
fn canonical_identity_doc(&self) -> Result<DocAt, RepositoryError> {
|
||||
let head = self.canonical_identity_head()?;
|
||||
let doc = self.identity_doc_at(head)?;
|
||||
|
||||
Ok(doc)
|
||||
}
|
||||
|
||||
/// Get the `reference` for the given `remote`.
|
||||
///
|
||||
|
|
@ -431,23 +472,22 @@ pub trait ReadRepository: Sized {
|
|||
fn remotes(&self) -> Result<Remotes<Verified>, refs::Error>;
|
||||
|
||||
/// Get repository delegates.
|
||||
fn delegates(&self) -> Result<NonEmpty<Did>, IdentityError> {
|
||||
let (_, doc) = self.identity_doc()?;
|
||||
let doc = doc.verified()?;
|
||||
fn delegates(&self) -> Result<NonEmpty<Did>, RepositoryError> {
|
||||
let doc: Doc<_> = self.identity_doc()?.into();
|
||||
|
||||
Ok(doc.delegates)
|
||||
}
|
||||
|
||||
/// Get the repository's identity document.
|
||||
fn identity_doc(&self) -> Result<(Oid, identity::Doc<Unverified>), IdentityError> {
|
||||
fn identity_doc(&self) -> Result<DocAt, RepositoryError> {
|
||||
let head = self.identity_head()?;
|
||||
let doc = self.identity_doc_at(head)?;
|
||||
|
||||
Ok((head, doc))
|
||||
Ok(doc)
|
||||
}
|
||||
|
||||
/// Get the repository's identity document at a specific commit.
|
||||
fn identity_doc_at(&self, head: Oid) -> Result<identity::Doc<Unverified>, DocError>;
|
||||
fn identity_doc_at(&self, head: Oid) -> Result<DocAt, DocError>;
|
||||
|
||||
/// Get the merge base of two commits.
|
||||
fn merge_base(&self, left: &Oid, right: &Oid) -> Result<Oid, git::ext::Error>;
|
||||
|
|
@ -457,9 +497,16 @@ pub trait ReadRepository: Sized {
|
|||
pub trait WriteRepository: ReadRepository + SignRepository {
|
||||
/// Set the repository head to the canonical branch.
|
||||
/// This computes the head based on the delegate set.
|
||||
fn set_head(&self) -> Result<Oid, IdentityError>;
|
||||
fn set_head(&self) -> Result<Oid, RepositoryError>;
|
||||
/// Set the repository 'rad/id' to the canonical commit, agreed by quorum.
|
||||
fn set_identity_head(&self) -> Result<Oid, IdentityError>;
|
||||
fn set_identity_head(&self) -> Result<Oid, RepositoryError> {
|
||||
let head = self.canonical_identity_head()?;
|
||||
self.set_identity_head_to(head)?;
|
||||
|
||||
Ok(head)
|
||||
}
|
||||
/// Set the repository 'rad/id' to the given commit.
|
||||
fn set_identity_head_to(&self, commit: Oid) -> Result<(), RepositoryError>;
|
||||
/// Get the underlying git repository.
|
||||
fn raw(&self) -> &git2::Repository;
|
||||
}
|
||||
|
|
@ -485,7 +532,7 @@ where
|
|||
self.deref().path_of(rid)
|
||||
}
|
||||
|
||||
fn contains(&self, rid: &Id) -> Result<bool, IdentityError> {
|
||||
fn contains(&self, rid: &Id) -> Result<bool, RepositoryError> {
|
||||
self.deref().contains(rid)
|
||||
}
|
||||
|
||||
|
|
@ -493,12 +540,8 @@ where
|
|||
self.deref().inventory()
|
||||
}
|
||||
|
||||
fn get(
|
||||
&self,
|
||||
remote: &RemoteId,
|
||||
proj: Id,
|
||||
) -> Result<Option<identity::Doc<Verified>>, IdentityError> {
|
||||
self.deref().get(remote, proj)
|
||||
fn get(&self, rid: Id) -> Result<Option<Doc<Verified>>, RepositoryError> {
|
||||
self.deref().get(rid)
|
||||
}
|
||||
|
||||
fn repository(&self, rid: Id) -> Result<Self::Repository, Error> {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
#![warn(clippy::unwrap_used)]
|
||||
pub mod cob;
|
||||
pub mod transport;
|
||||
|
||||
|
|
@ -5,19 +6,19 @@ use std::collections::{BTreeMap, HashMap, HashSet};
|
|||
use std::path::{Path, PathBuf};
|
||||
use std::{fs, io};
|
||||
|
||||
use crypto::{Signer, Unverified, Verified};
|
||||
use crypto::{Signer, Verified};
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use crate::crypto::Unverified;
|
||||
use crate::git;
|
||||
use crate::identity;
|
||||
use crate::identity::doc::DocError;
|
||||
use crate::identity::{Doc, Id};
|
||||
use crate::identity::{Identity, IdentityError, Project};
|
||||
use crate::identity::{doc::DocAt, Doc, Id};
|
||||
use crate::identity::{Identity, Project};
|
||||
use crate::storage::refs;
|
||||
use crate::storage::refs::{Refs, SignedRefs};
|
||||
use crate::storage::{
|
||||
Inventory, ReadRepository, ReadStorage, Remote, Remotes, SignRepository, WriteRepository,
|
||||
WriteStorage,
|
||||
Inventory, ReadRepository, ReadStorage, Remote, Remotes, RepositoryError, SignRepository,
|
||||
WriteRepository, WriteStorage,
|
||||
};
|
||||
|
||||
pub use crate::git::*;
|
||||
|
|
@ -66,10 +67,8 @@ impl<'a> TryFrom<git2::Reference<'a>> for Ref {
|
|||
Err(RefError::MissingNamespace(refname)) => (None, refname),
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
let Some(oid) = r.target() else {
|
||||
// Ignore symbolic refs, eg. `HEAD`.
|
||||
return Err(RefError::Symbolic(name));
|
||||
};
|
||||
let oid = r.resolve()?.target().ok_or(RefError::NoTarget)?;
|
||||
|
||||
Ok(Self {
|
||||
namespace,
|
||||
name,
|
||||
|
|
@ -94,7 +93,7 @@ impl ReadStorage for Storage {
|
|||
paths::repository(&self, rid)
|
||||
}
|
||||
|
||||
fn contains(&self, rid: &Id) -> Result<bool, IdentityError> {
|
||||
fn contains(&self, rid: &Id) -> Result<bool, RepositoryError> {
|
||||
if paths::repository(&self, rid).exists() {
|
||||
let _ = self.repository(*rid)?.head()?;
|
||||
return Ok(true);
|
||||
|
|
@ -102,19 +101,6 @@ impl ReadStorage for Storage {
|
|||
Ok(false)
|
||||
}
|
||||
|
||||
fn get(&self, remote: &RemoteId, proj: Id) -> Result<Option<Doc<Verified>>, IdentityError> {
|
||||
let repo = match self.repository(proj) {
|
||||
Ok(doc) => doc,
|
||||
Err(e) if e.is_not_found() => return Ok(None),
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
match repo.identity_doc_of(remote) {
|
||||
Ok(doc) => Ok(Some(doc)),
|
||||
Err(e) if e.is_not_found() => Ok(None),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
fn inventory(&self) -> Result<Inventory, Error> {
|
||||
let repos = self.repositories()?;
|
||||
|
||||
|
|
@ -160,7 +146,7 @@ impl Storage {
|
|||
self.path.as_path()
|
||||
}
|
||||
|
||||
pub fn repositories(&self) -> Result<Vec<RepositoryInfo<Unverified>>, Error> {
|
||||
pub fn repositories(&self) -> Result<Vec<RepositoryInfo<Verified>>, Error> {
|
||||
let mut repos = Vec::new();
|
||||
|
||||
for result in fs::read_dir(&self.path)? {
|
||||
|
|
@ -185,7 +171,7 @@ impl Storage {
|
|||
}
|
||||
};
|
||||
let doc = match repo.identity_doc() {
|
||||
Ok((_, doc)) => doc,
|
||||
Ok(doc) => doc.into(),
|
||||
Err(e) => {
|
||||
log::warn!(target: "storage", "Repository {rid} is invalid: looking up doc: {e}");
|
||||
continue;
|
||||
|
|
@ -213,7 +199,7 @@ impl Storage {
|
|||
for r in repo.raw().references()? {
|
||||
let r = r?;
|
||||
let name = r.name().ok_or(Error::InvalidRef)?;
|
||||
let oid = r.target().ok_or(Error::InvalidRef)?;
|
||||
let oid = r.resolve()?.target().ok_or(Error::InvalidRef)?;
|
||||
|
||||
println!("{} {oid} {name}", rid.urn());
|
||||
}
|
||||
|
|
@ -222,25 +208,22 @@ impl Storage {
|
|||
}
|
||||
}
|
||||
|
||||
/// Git implementation of [`WriteRepository`] using the `git2` crate.
|
||||
pub struct Repository {
|
||||
/// The repository identifier (RID).
|
||||
pub id: Id,
|
||||
/// The backing Git repository.
|
||||
pub backend: git2::Repository,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum VerifyError {
|
||||
#[error("invalid remote `{0}`")]
|
||||
InvalidRemote(RemoteId),
|
||||
#[error("invalid target `{2}` for reference `{1}` of remote `{0}`")]
|
||||
InvalidRefTarget(RemoteId, RefString, git2::Oid),
|
||||
#[error("invalid identity: {0}")]
|
||||
InvalidIdentity(#[from] IdentityError),
|
||||
#[error("refs error: {0}")]
|
||||
Refs(#[from] refs::Error),
|
||||
#[error("missing reference `{1}` in remote `{0}`")]
|
||||
MissingRef(RemoteId, git::RefString),
|
||||
#[error("git: {0}")]
|
||||
Git(#[from] git2::Error),
|
||||
#[error(transparent)]
|
||||
Storage(#[from] Error),
|
||||
}
|
||||
|
|
@ -274,28 +257,22 @@ impl Repository {
|
|||
/// Create the repository's identity branch.
|
||||
pub fn init<G: Signer, S: WriteStorage>(
|
||||
doc: &Doc<Verified>,
|
||||
remote: &RemoteId,
|
||||
storage: S,
|
||||
signer: &G,
|
||||
) -> Result<(Self, git::Oid), Error> {
|
||||
let (doc_oid, doc) = doc.encode()?;
|
||||
) -> Result<(Self, git::Oid), RepositoryError> {
|
||||
let (doc_oid, _) = doc.encode()?;
|
||||
let id = Id::from(doc_oid);
|
||||
let repo = Self::create(paths::repository(&storage, &id), id)?;
|
||||
let oid = Doc::init(
|
||||
doc.as_slice(),
|
||||
remote,
|
||||
&[(signer.public_key(), signer.sign(doc_oid.as_bytes()))],
|
||||
repo.raw(),
|
||||
)?;
|
||||
let commit = doc.init(&repo, signer)?;
|
||||
|
||||
Ok((repo, oid))
|
||||
Ok((repo, commit))
|
||||
}
|
||||
|
||||
pub fn inspect(&self) -> Result<(), Error> {
|
||||
for r in self.backend.references()? {
|
||||
let r = r?;
|
||||
let name = r.name().ok_or(Error::InvalidRef)?;
|
||||
let oid = r.target().ok_or(Error::InvalidRef)?;
|
||||
let oid = r.resolve()?.target().ok_or(Error::InvalidRef)?;
|
||||
|
||||
println!("{oid} {name}");
|
||||
}
|
||||
|
|
@ -313,7 +290,6 @@ impl Repository {
|
|||
let r = reference?;
|
||||
|
||||
match Ref::try_from(r) {
|
||||
Err(RefError::Symbolic(_)) => Ok(None),
|
||||
Err(err) => Err(err.into()),
|
||||
Ok(r) => Ok(Some(r)),
|
||||
}
|
||||
|
|
@ -323,24 +299,18 @@ impl Repository {
|
|||
Ok(refs)
|
||||
}
|
||||
|
||||
pub fn identity_of(&self, remote: &RemoteId) -> Result<Identity<Oid>, IdentityError> {
|
||||
Identity::load(remote, self)
|
||||
}
|
||||
|
||||
/// Get the canonical project information.
|
||||
pub fn project(&self) -> Result<Project, IdentityError> {
|
||||
pub fn project(&self) -> Result<Project, RepositoryError> {
|
||||
let head = self.identity_head()?;
|
||||
let doc = self.identity_doc_at(head)?;
|
||||
let proj = doc.verified()?.project()?;
|
||||
let proj = doc.project()?;
|
||||
|
||||
Ok(proj)
|
||||
}
|
||||
|
||||
pub fn identity_doc_of(&self, remote: &RemoteId) -> Result<Doc<Verified>, IdentityError> {
|
||||
let (doc, _) = identity::Doc::load(remote, self)?;
|
||||
let verified = doc.verified()?;
|
||||
|
||||
Ok(verified)
|
||||
pub fn identity_doc_of(&self, remote: &RemoteId) -> Result<Doc<Verified>, DocError> {
|
||||
let oid = self.identity_head_of(remote)?;
|
||||
Doc::load_at(oid, self).map(|d| d.into())
|
||||
}
|
||||
|
||||
pub fn remote_ids(
|
||||
|
|
@ -392,12 +362,18 @@ impl ReadRepository for Repository {
|
|||
self.backend.path()
|
||||
}
|
||||
|
||||
fn blob_at<'a>(&'a self, commit: Oid, path: &'a Path) -> Result<git2::Blob<'a>, git::Error> {
|
||||
git::ext::Blob::At {
|
||||
object: commit.into(),
|
||||
path,
|
||||
}
|
||||
.get(&self.backend)
|
||||
fn blob_at<P: AsRef<Path>>(&self, commit: Oid, path: P) -> Result<git2::Blob, git::Error> {
|
||||
let commit = self.backend.find_commit(*commit)?;
|
||||
let tree = commit.tree()?;
|
||||
let entry = tree.get_path(path.as_ref())?;
|
||||
let obj = entry.to_object(&self.backend)?;
|
||||
let blob = obj.into_blob().map_err(|_| {
|
||||
git::Error::NotFound(git::NotFound::NoSuchBlob(
|
||||
path.as_ref().display().to_string(),
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(blob)
|
||||
}
|
||||
|
||||
fn blob(&self, oid: Oid) -> Result<git2::Blob, git::Error> {
|
||||
|
|
@ -429,8 +405,8 @@ impl ReadRepository for Repository {
|
|||
if let Some((name, _)) = signed.into_iter().next() {
|
||||
return Err(VerifyError::MissingRef(remote.id, name));
|
||||
}
|
||||
// Finally, verify the identity history of remote.
|
||||
self.identity_of(&remote.id)?.verified(self.id)?;
|
||||
// Nb. As it stands, it doesn't make sense to verify a single remote's identity branch,
|
||||
// since it is a COB.
|
||||
|
||||
Ok(unsigned)
|
||||
}
|
||||
|
|
@ -489,7 +465,7 @@ impl ReadRepository for Repository {
|
|||
let e = e?;
|
||||
let name = e.name().ok_or(Error::InvalidRef)?;
|
||||
let (_, refname) = git::parse_ref::<RemoteId>(name)?;
|
||||
let oid = e.target().ok_or(Error::InvalidRef)?;
|
||||
let oid = e.resolve()?.target().ok_or(Error::InvalidRef)?;
|
||||
let (_, category, _, _) = refname.non_empty_components();
|
||||
|
||||
if [
|
||||
|
|
@ -536,11 +512,11 @@ impl ReadRepository for Repository {
|
|||
Ok(Remotes::from_iter(remotes))
|
||||
}
|
||||
|
||||
fn identity_doc_at(&self, head: Oid) -> Result<identity::Doc<Unverified>, DocError> {
|
||||
Doc::<Unverified>::load_at(head, self).map(|(doc, _)| doc)
|
||||
fn identity_doc_at(&self, head: Oid) -> Result<DocAt, DocError> {
|
||||
Doc::<Verified>::load_at(head, self)
|
||||
}
|
||||
|
||||
fn head(&self) -> Result<(Qualified, Oid), IdentityError> {
|
||||
fn head(&self) -> Result<(Qualified, Oid), RepositoryError> {
|
||||
// If `HEAD` is already set locally, just return that.
|
||||
if let Ok(head) = self.backend.head() {
|
||||
if let Ok((name, oid)) = git::refs::qualified_from(&head) {
|
||||
|
|
@ -550,9 +526,8 @@ impl ReadRepository for Repository {
|
|||
self.canonical_head()
|
||||
}
|
||||
|
||||
fn canonical_head(&self) -> Result<(Qualified, Oid), IdentityError> {
|
||||
let (_, doc) = self.identity_doc()?;
|
||||
let doc = doc.verified()?;
|
||||
fn canonical_head(&self) -> Result<(Qualified, Oid), RepositoryError> {
|
||||
let doc = self.identity_doc()?;
|
||||
let project = doc.project()?;
|
||||
let branch_ref = git::refs::branch(project.default_branch());
|
||||
let raw = self.raw();
|
||||
|
|
@ -568,60 +543,63 @@ impl ReadRepository for Repository {
|
|||
Ok((branch_ref, quorum))
|
||||
}
|
||||
|
||||
fn identity_head(&self) -> Result<Oid, IdentityError> {
|
||||
match Doc::<Verified>::canonical_head(self) {
|
||||
fn identity_head(&self) -> Result<Oid, RepositoryError> {
|
||||
let result = self
|
||||
.backend
|
||||
.refname_to_id(CANONICAL_IDENTITY.as_str())
|
||||
.map(Oid::from);
|
||||
|
||||
match result {
|
||||
Ok(oid) => Ok(oid),
|
||||
Err(err) if err.is_not_found() => self.canonical_identity_head(),
|
||||
Err(err) if git::ext::is_not_found_err(&err) => self.canonical_identity_head(),
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn canonical_identity_head(&self) -> Result<Oid, IdentityError> {
|
||||
let mut heads = Vec::new();
|
||||
fn identity_head_of(&self, remote: &RemoteId) -> Result<Oid, git::ext::Error> {
|
||||
self.reference_oid(remote, &git::refs::storage::IDENTITY_BRANCH)
|
||||
}
|
||||
|
||||
fn identity_root(&self) -> Result<Oid, RepositoryError> {
|
||||
let oid = self.backend.refname_to_id(CANONICAL_IDENTITY.as_str())?;
|
||||
let walk = self.revwalk(oid.into())?.collect::<Vec<_>>();
|
||||
let root = walk
|
||||
.into_iter()
|
||||
.last()
|
||||
.ok_or(RepositoryError::Doc(DocError::Missing))??;
|
||||
|
||||
Ok(root.into())
|
||||
}
|
||||
|
||||
fn identity_root_of(&self, remote: &RemoteId) -> Result<Oid, RepositoryError> {
|
||||
let oid = self.identity_head_of(remote)?;
|
||||
let walk = self.revwalk(oid)?.collect::<Vec<_>>();
|
||||
let root = walk
|
||||
.into_iter()
|
||||
.last()
|
||||
.ok_or(RepositoryError::Doc(DocError::Missing))??;
|
||||
|
||||
Ok(root.into())
|
||||
}
|
||||
|
||||
fn canonical_identity_head(&self) -> Result<Oid, RepositoryError> {
|
||||
for remote in self.remote_ids()? {
|
||||
let remote = remote?;
|
||||
let oid = Doc::<Unverified>::head(&remote, self)?;
|
||||
// Nb. A remote may not have an identity document if the user has not contributed
|
||||
// any changes to the identity COB.
|
||||
let Ok(root) = self.identity_root_of(&remote) else {
|
||||
continue;
|
||||
};
|
||||
let blob = Doc::<Unverified>::blob_at(root, self)?;
|
||||
|
||||
heads.push(oid.into());
|
||||
}
|
||||
// Keep track of the longest identity branch.
|
||||
let mut longest = heads.pop().ok_or(IdentityError::MissingBranch)?;
|
||||
// We've got an identity that goes back to the correct root.
|
||||
if blob.id() == **self.id {
|
||||
let identity = Identity::get(&root.into(), self)?;
|
||||
|
||||
for head in &heads {
|
||||
let base = self.raw().merge_base(*head, longest)?;
|
||||
|
||||
if base == longest {
|
||||
// `head` is a successor of `longest`. Update `longest`.
|
||||
//
|
||||
// o head
|
||||
// |
|
||||
// o longest (base)
|
||||
// |
|
||||
//
|
||||
longest = *head;
|
||||
} else if base == *head || *head == longest {
|
||||
// `head` is an ancestor of `longest`, or equal to it. Do nothing.
|
||||
//
|
||||
// o longest o longest, head (base)
|
||||
// | |
|
||||
// o head (base) OR o
|
||||
// | |
|
||||
//
|
||||
} else {
|
||||
// The merge base between `head` and `longest` (`base`)
|
||||
// is neither `head` nor `longest`. Therefore, the branches have
|
||||
// diverged.
|
||||
//
|
||||
// longest head
|
||||
// \ /
|
||||
// o (base)
|
||||
// |
|
||||
//
|
||||
return Err(IdentityError::BranchesDiverge);
|
||||
return Ok(identity.head());
|
||||
}
|
||||
}
|
||||
Ok(longest.into())
|
||||
Err(DocError::Missing.into())
|
||||
}
|
||||
|
||||
fn merge_base(&self, left: &Oid, right: &Oid) -> Result<Oid, ext::Error> {
|
||||
|
|
@ -633,7 +611,7 @@ impl ReadRepository for Repository {
|
|||
}
|
||||
|
||||
impl WriteRepository for Repository {
|
||||
fn set_head(&self) -> Result<Oid, IdentityError> {
|
||||
fn set_head(&self) -> Result<Oid, RepositoryError> {
|
||||
let head_ref = refname!("HEAD");
|
||||
let (branch_ref, head) = self.canonical_head()?;
|
||||
|
||||
|
|
@ -648,18 +626,15 @@ impl WriteRepository for Repository {
|
|||
Ok(head)
|
||||
}
|
||||
|
||||
fn set_identity_head(&self) -> Result<Oid, IdentityError> {
|
||||
let head = self.canonical_identity_head()?;
|
||||
|
||||
log::debug!(target: "storage", "Setting ref: {} -> {}", *CANONICAL_IDENTITY, head);
|
||||
fn set_identity_head_to(&self, commit: Oid) -> Result<(), RepositoryError> {
|
||||
log::debug!(target: "storage", "Setting ref: {} -> {}", *CANONICAL_IDENTITY, commit);
|
||||
self.raw().reference(
|
||||
CANONICAL_IDENTITY.as_str(),
|
||||
*head,
|
||||
*commit,
|
||||
true,
|
||||
"set-local-branch (radicle)",
|
||||
)?;
|
||||
|
||||
Ok(head)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn raw(&self) -> &git2::Repository {
|
||||
|
|
@ -670,7 +645,11 @@ impl WriteRepository for Repository {
|
|||
impl SignRepository for Repository {
|
||||
fn sign_refs<G: Signer>(&self, signer: &G) -> Result<SignedRefs<Verified>, Error> {
|
||||
let remote = signer.public_key();
|
||||
let refs = self.references_of(remote)?;
|
||||
let mut refs = self.references_of(remote)?;
|
||||
// Don't sign the `rad/sigrefs` ref itself, and don't sign invalid OIDs.
|
||||
refs.retain(|name, oid| {
|
||||
name.as_refstr() != refs::SIGREFS_BRANCH.as_ref() && !oid.is_zero()
|
||||
});
|
||||
let signed = refs.signed(signer)?;
|
||||
|
||||
signed.save(self)?;
|
||||
|
|
@ -839,6 +818,7 @@ pub mod paths {
|
|||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
use crypto::test::signer::MockSigner;
|
||||
|
||||
|
|
@ -1057,11 +1037,13 @@ mod tests {
|
|||
|
||||
transport::local::register(storage.clone());
|
||||
|
||||
let (id, _, _, _) =
|
||||
let (rid, _, _, _) =
|
||||
fixtures::project(tmp.path().join("project"), &storage, &signer).unwrap();
|
||||
let proj = storage.repository(id).unwrap();
|
||||
let repo = storage.repository(rid).unwrap();
|
||||
let id = repo.identity().unwrap().head();
|
||||
let cob = format!("refs/cobs/xyz.radicle.id/{id}");
|
||||
|
||||
let mut refs = proj
|
||||
let mut refs = repo
|
||||
.references_of(signer.public_key())
|
||||
.unwrap()
|
||||
.iter()
|
||||
|
|
@ -1071,7 +1053,7 @@ mod tests {
|
|||
|
||||
assert_eq!(
|
||||
refs,
|
||||
vec!["refs/heads/master", "refs/rad/id", "refs/rad/sigrefs"]
|
||||
vec![&cob, "refs/heads/master", "refs/rad/id", "refs/rad/sigrefs"]
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
//! COB storage Git backend.
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
|
||||
use cob::object::Objects;
|
||||
use radicle_cob as cob;
|
||||
use radicle_cob::change;
|
||||
use storage::RepositoryError;
|
||||
use storage::SignRepository;
|
||||
|
||||
use crate::git::*;
|
||||
|
|
@ -15,11 +17,13 @@ use crate::storage::{
|
|||
};
|
||||
use crate::{
|
||||
git, identity,
|
||||
identity::{doc::DocError, IdentityError, PublicKey},
|
||||
identity::{doc::DocError, PublicKey},
|
||||
};
|
||||
|
||||
use super::{RemoteId, Repository};
|
||||
|
||||
pub use crate::cob::{store, ObjectId};
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ObjectsError {
|
||||
#[error(transparent)]
|
||||
|
|
@ -54,7 +58,7 @@ impl change::Storage for Repository {
|
|||
|
||||
fn store<Signer>(
|
||||
&self,
|
||||
authority: Self::Parent,
|
||||
authority: Option<Self::Parent>,
|
||||
parents: Vec<Self::Parent>,
|
||||
signer: &Signer,
|
||||
spec: change::Template<Self::ObjectId>,
|
||||
|
|
@ -194,7 +198,7 @@ impl<'a, R: storage::WriteRepository> change::Storage for DraftStore<'a, R> {
|
|||
|
||||
fn store<Signer>(
|
||||
&self,
|
||||
authority: Self::Parent,
|
||||
authority: Option<Self::Parent>,
|
||||
parents: Vec<Self::Parent>,
|
||||
signer: &Signer,
|
||||
spec: change::Template<Self::ObjectId>,
|
||||
|
|
@ -236,11 +240,11 @@ impl<'a, R: storage::ReadRepository> ReadRepository for DraftStore<'a, R> {
|
|||
self.repo.is_empty()
|
||||
}
|
||||
|
||||
fn head(&self) -> Result<(fmt::Qualified, Oid), identity::IdentityError> {
|
||||
fn head(&self) -> Result<(fmt::Qualified, Oid), RepositoryError> {
|
||||
self.repo.head()
|
||||
}
|
||||
|
||||
fn canonical_head(&self) -> Result<(fmt::Qualified, Oid), identity::IdentityError> {
|
||||
fn canonical_head(&self) -> Result<(fmt::Qualified, Oid), RepositoryError> {
|
||||
self.repo.canonical_head()
|
||||
}
|
||||
|
||||
|
|
@ -275,11 +279,11 @@ impl<'a, R: storage::ReadRepository> ReadRepository for DraftStore<'a, R> {
|
|||
self.repo.is_ancestor_of(ancestor, head)
|
||||
}
|
||||
|
||||
fn blob_at<'b>(
|
||||
&'b self,
|
||||
fn blob_at<P: AsRef<Path>>(
|
||||
&self,
|
||||
oid: git_ext::Oid,
|
||||
path: &'b std::path::Path,
|
||||
) -> Result<git2::Blob<'b>, git_ext::Error> {
|
||||
path: P,
|
||||
) -> Result<git2::Blob, git_ext::Error> {
|
||||
self.repo.blob_at(oid, path)
|
||||
}
|
||||
|
||||
|
|
@ -314,24 +318,31 @@ impl<'a, R: storage::ReadRepository> ReadRepository for DraftStore<'a, R> {
|
|||
self.repo.references_glob(pattern)
|
||||
}
|
||||
|
||||
fn identity_doc(
|
||||
&self,
|
||||
) -> Result<(Oid, crate::identity::Doc<crate::crypto::Unverified>), IdentityError> {
|
||||
fn identity_doc(&self) -> Result<crate::identity::DocAt, RepositoryError> {
|
||||
self.repo.identity_doc()
|
||||
}
|
||||
|
||||
fn identity_doc_at(
|
||||
&self,
|
||||
head: Oid,
|
||||
) -> Result<crate::identity::Doc<crate::crypto::Unverified>, DocError> {
|
||||
fn identity_doc_at(&self, head: Oid) -> Result<crate::identity::DocAt, DocError> {
|
||||
self.repo.identity_doc_at(head)
|
||||
}
|
||||
|
||||
fn identity_head(&self) -> Result<Oid, IdentityError> {
|
||||
fn identity_head(&self) -> Result<Oid, RepositoryError> {
|
||||
self.repo.identity_head()
|
||||
}
|
||||
|
||||
fn canonical_identity_head(&self) -> Result<Oid, IdentityError> {
|
||||
fn identity_head_of(&self, remote: &RemoteId) -> Result<Oid, super::ext::Error> {
|
||||
self.repo.identity_head_of(remote)
|
||||
}
|
||||
|
||||
fn identity_root(&self) -> Result<Oid, RepositoryError> {
|
||||
self.repo.identity_root()
|
||||
}
|
||||
|
||||
fn identity_root_of(&self, remote: &RemoteId) -> Result<Oid, RepositoryError> {
|
||||
self.repo.identity_root_of(remote)
|
||||
}
|
||||
|
||||
fn canonical_identity_head(&self) -> Result<Oid, RepositoryError> {
|
||||
self.repo.canonical_identity_head()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ impl FromStr for Url {
|
|||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@ impl FromStr for Url {
|
|||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
|
|
|
|||
|
|
@ -139,11 +139,8 @@ impl Refs {
|
|||
|
||||
pub fn canonical(&self) -> Vec<u8> {
|
||||
let mut buf = String::new();
|
||||
let refs = self
|
||||
.iter()
|
||||
.filter(|(name, oid)| name.as_refstr() != SIGREFS_BRANCH.as_ref() && !oid.is_zero());
|
||||
|
||||
for (name, oid) in refs {
|
||||
for (name, oid) in self.iter() {
|
||||
buf.push_str(&oid.to_string());
|
||||
buf.push(' ');
|
||||
buf.push_str(name);
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use qcheck::Arbitrary;
|
|||
use crate::collections::RandomMap;
|
||||
use crate::identity::doc::Visibility;
|
||||
use crate::identity::{
|
||||
doc::{Doc, Id},
|
||||
doc::{Doc, DocAt, Id},
|
||||
project::Project,
|
||||
Did,
|
||||
};
|
||||
|
|
@ -68,9 +68,7 @@ pub fn vec<T: Eq + Arbitrary>(size: usize) -> Vec<T> {
|
|||
pub fn nonempty_storage(size: usize) -> MockStorage {
|
||||
let mut storage = gen::<MockStorage>(size);
|
||||
for _ in 0..size {
|
||||
storage
|
||||
.inventory
|
||||
.insert(gen::<Id>(1), gen::<Doc<Verified>>(1));
|
||||
storage.inventory.insert(gen::<Id>(1), gen::<DocAt>(1));
|
||||
}
|
||||
storage
|
||||
}
|
||||
|
|
@ -155,6 +153,18 @@ impl Arbitrary for Doc<Verified> {
|
|||
}
|
||||
}
|
||||
|
||||
impl Arbitrary for DocAt {
|
||||
fn arbitrary(g: &mut qcheck::Gen) -> Self {
|
||||
let doc = Doc::<Verified>::arbitrary(g);
|
||||
|
||||
DocAt {
|
||||
commit: self::oid(),
|
||||
blob: self::oid(),
|
||||
doc,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Arbitrary for SignedRefs<Unverified> {
|
||||
fn arbitrary(g: &mut qcheck::Gen) -> Self {
|
||||
let bytes: [u8; 64] = Arbitrary::arbitrary(g);
|
||||
|
|
|
|||
|
|
@ -6,8 +6,7 @@ use std::str::FromStr;
|
|||
use git_ext::ref_format as fmt;
|
||||
|
||||
use crate::crypto::{Signer, Verified};
|
||||
use crate::identity::doc::{Doc, DocError, Id};
|
||||
use crate::identity::IdentityError;
|
||||
use crate::identity::doc::{Doc, DocAt, DocError, Id};
|
||||
use crate::node::NodeId;
|
||||
|
||||
pub use crate::storage::*;
|
||||
|
|
@ -15,7 +14,7 @@ pub use crate::storage::*;
|
|||
#[derive(Clone, Debug)]
|
||||
pub struct MockStorage {
|
||||
pub path: PathBuf,
|
||||
pub inventory: HashMap<Id, Doc<Verified>>,
|
||||
pub inventory: HashMap<Id, DocAt>,
|
||||
|
||||
/// All refs keyed by RID.
|
||||
/// Each value is a map of refs keyed by node Id (public key).
|
||||
|
|
@ -23,7 +22,7 @@ pub struct MockStorage {
|
|||
}
|
||||
|
||||
impl MockStorage {
|
||||
pub fn new(inventory: Vec<(Id, Doc<Verified>)>) -> Self {
|
||||
pub fn new(inventory: Vec<(Id, DocAt)>) -> Self {
|
||||
Self {
|
||||
path: PathBuf::default(),
|
||||
inventory: inventory.into_iter().collect(),
|
||||
|
|
@ -64,14 +63,10 @@ impl ReadStorage for MockStorage {
|
|||
self.path().join(rid.canonical())
|
||||
}
|
||||
|
||||
fn contains(&self, rid: &Id) -> Result<bool, IdentityError> {
|
||||
fn contains(&self, rid: &Id) -> Result<bool, RepositoryError> {
|
||||
Ok(self.inventory.contains_key(rid))
|
||||
}
|
||||
|
||||
fn get(&self, _remote: &RemoteId, proj: Id) -> Result<Option<Doc<Verified>>, IdentityError> {
|
||||
Ok(self.inventory.get(&proj).cloned())
|
||||
}
|
||||
|
||||
fn inventory(&self) -> Result<Inventory, Error> {
|
||||
Ok(self.inventory.keys().cloned().collect::<Vec<_>>())
|
||||
}
|
||||
|
|
@ -109,15 +104,21 @@ impl WriteStorage for MockStorage {
|
|||
#[derive(Clone, Debug)]
|
||||
pub struct MockRepository {
|
||||
id: Id,
|
||||
doc: Doc<Verified>,
|
||||
doc: DocAt,
|
||||
remotes: HashMap<NodeId, refs::SignedRefs<Verified>>,
|
||||
}
|
||||
|
||||
impl MockRepository {
|
||||
pub fn new(id: Id, doc: Doc<Verified>) -> Self {
|
||||
let (blob, _) = doc.encode().unwrap();
|
||||
|
||||
Self {
|
||||
id,
|
||||
doc,
|
||||
doc: DocAt {
|
||||
commit: Oid::from_str("ffffffffffffffffffffffffffffffffffffffff").unwrap(),
|
||||
blob,
|
||||
doc,
|
||||
},
|
||||
remotes: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
|
@ -132,11 +133,11 @@ impl ReadRepository for MockRepository {
|
|||
Ok(self.remotes.is_empty())
|
||||
}
|
||||
|
||||
fn head(&self) -> Result<(fmt::Qualified, Oid), IdentityError> {
|
||||
fn head(&self) -> Result<(fmt::Qualified, Oid), RepositoryError> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn canonical_head(&self) -> Result<(fmt::Qualified, Oid), IdentityError> {
|
||||
fn canonical_head(&self) -> Result<(fmt::Qualified, Oid), RepositoryError> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
|
|
@ -182,11 +183,11 @@ impl ReadRepository for MockRepository {
|
|||
todo!()
|
||||
}
|
||||
|
||||
fn blob_at<'a>(
|
||||
&'a self,
|
||||
fn blob_at<P: AsRef<std::path::Path>>(
|
||||
&self,
|
||||
_oid: git_ext::Oid,
|
||||
_path: &'a std::path::Path,
|
||||
) -> Result<git2::Blob<'a>, git_ext::Error> {
|
||||
_path: P,
|
||||
) -> Result<git2::Blob, git_ext::Error> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
|
|
@ -217,24 +218,31 @@ impl ReadRepository for MockRepository {
|
|||
todo!()
|
||||
}
|
||||
|
||||
fn identity_doc(
|
||||
&self,
|
||||
) -> Result<(Oid, crate::identity::Doc<crate::crypto::Unverified>), IdentityError> {
|
||||
Ok((git2::Oid::zero().into(), self.doc.clone().unverified()))
|
||||
fn identity_doc(&self) -> Result<crate::identity::DocAt, RepositoryError> {
|
||||
Ok(self.doc.clone())
|
||||
}
|
||||
|
||||
fn identity_doc_at(
|
||||
&self,
|
||||
_head: Oid,
|
||||
) -> Result<crate::identity::Doc<crate::crypto::Unverified>, DocError> {
|
||||
Ok(self.doc.clone().unverified())
|
||||
fn identity_doc_at(&self, _head: Oid) -> Result<crate::identity::DocAt, DocError> {
|
||||
Ok(self.doc.clone())
|
||||
}
|
||||
|
||||
fn identity_head(&self) -> Result<Oid, IdentityError> {
|
||||
fn identity_head(&self) -> Result<Oid, RepositoryError> {
|
||||
self.canonical_identity_head()
|
||||
}
|
||||
|
||||
fn canonical_identity_head(&self) -> Result<Oid, IdentityError> {
|
||||
fn identity_head_of(&self, _remote: &RemoteId) -> Result<Oid, git::ext::Error> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn identity_root(&self) -> Result<Oid, RepositoryError> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn identity_root_of(&self, _remote: &RemoteId) -> Result<Oid, RepositoryError> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn canonical_identity_head(&self) -> Result<Oid, RepositoryError> {
|
||||
Ok(Oid::from_str("cccccccccccccccccccccccccccccccccccccccc").unwrap())
|
||||
}
|
||||
|
||||
|
|
@ -248,11 +256,11 @@ impl WriteRepository for MockRepository {
|
|||
todo!()
|
||||
}
|
||||
|
||||
fn set_head(&self) -> Result<Oid, IdentityError> {
|
||||
fn set_head(&self) -> Result<Oid, RepositoryError> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn set_identity_head(&self) -> Result<Oid, IdentityError> {
|
||||
fn set_identity_head_to(&self, _commit: Oid) -> Result<(), RepositoryError> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue