radicle: canonical references payload
This commit is contained in:
parent
af6cf03acd
commit
7f646666bc
|
|
@ -21,7 +21,7 @@ $ git commit --amend -m "Neue Änderungen" --allow-empty -q
|
||||||
|
|
||||||
``` ~alice (stderr)
|
``` ~alice (stderr)
|
||||||
$ git push rad master -f
|
$ git push rad master -f
|
||||||
✓ Canonical head updated to 9170c8795d3a78f0381a0ffafb20ea69fb0f5b6b
|
✓ Canonical reference refs/heads/master updated to 9170c8795d3a78f0381a0ffafb20ea69fb0f5b6b
|
||||||
✓ Synced with 1 seed(s)
|
✓ Synced with 1 seed(s)
|
||||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
+ fb25886...9170c87 master -> master (forced update)
|
+ fb25886...9170c87 master -> master (forced update)
|
||||||
|
|
|
||||||
|
|
@ -91,8 +91,7 @@ commit:
|
||||||
|
|
||||||
``` ~alice (stderr)
|
``` ~alice (stderr)
|
||||||
$ git push rad -f
|
$ git push rad -f
|
||||||
warn: could not determine canonical tip for `refs/heads/master`
|
warn: could not determine commit for canonical reference 'refs/heads/master', no commit with at least 3 vote(s) found (threshold not met)
|
||||||
warn: no commit found with at least 3 vote(s) (threshold not met)
|
|
||||||
warn: it is recommended to find a commit to agree upon
|
warn: it is recommended to find a commit to agree upon
|
||||||
✓ Synced with 2 seed(s)
|
✓ Synced with 2 seed(s)
|
||||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
|
|
@ -117,7 +116,7 @@ become the canonical `master`.
|
||||||
|
|
||||||
``` ~bob (stderr)
|
``` ~bob (stderr)
|
||||||
$ git push rad
|
$ git push rad
|
||||||
✓ Canonical head updated to 3a75f66dd0020c9a0355cc6ec21f15de989e2001
|
✓ Canonical reference refs/heads/master updated to 3a75f66dd0020c9a0355cc6ec21f15de989e2001
|
||||||
✓ Synced with 2 seed(s)
|
✓ Synced with 2 seed(s)
|
||||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
|
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
|
||||||
2a37862..0f9bd80 master -> master
|
2a37862..0f9bd80 master -> master
|
||||||
|
|
@ -138,7 +137,7 @@ HEAD is now at 0f9bd80 Merge remote-tracking branch 'eve/master'
|
||||||
|
|
||||||
``` ~eve (stderr)
|
``` ~eve (stderr)
|
||||||
$ git push rad
|
$ git push rad
|
||||||
✓ Canonical head updated to 0f9bd8035c04b3f73f5408e73e8454879b20800b
|
✓ Canonical reference refs/heads/master updated to 0f9bd8035c04b3f73f5408e73e8454879b20800b
|
||||||
✓ Synced with 2 seed(s)
|
✓ Synced with 2 seed(s)
|
||||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z
|
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z
|
||||||
3a75f66..0f9bd80 master -> master
|
3a75f66..0f9bd80 master -> master
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ integrate Bob's changes before pushing ours:
|
||||||
|
|
||||||
``` ~alice (stderr) (fail) RAD_HINT=1
|
``` ~alice (stderr) (fail) RAD_HINT=1
|
||||||
$ git push rad
|
$ git push rad
|
||||||
hint: you are attempting to push a commit that would cause your upstream to diverge from the canonical head
|
hint: you are attempting to push a commit that would cause your upstream to diverge from the canonical reference refs/heads/master
|
||||||
hint: to integrate the remote changes, run `git pull --rebase` and try again
|
hint: to integrate the remote changes, run `git pull --rebase` and try again
|
||||||
error: refusing to update branch to commit that is not a descendant of canonical head
|
error: refusing to update branch to commit that is not a descendant of canonical head
|
||||||
error: failed to push some refs to 'rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi'
|
error: failed to push some refs to 'rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi'
|
||||||
|
|
@ -62,7 +62,7 @@ f2de534 Second commit
|
||||||
```
|
```
|
||||||
``` ~alice RAD_SOCKET=/dev/null (stderr)
|
``` ~alice RAD_SOCKET=/dev/null (stderr)
|
||||||
$ git push rad
|
$ git push rad
|
||||||
✓ Canonical head updated to f6cff86594495e9beccfeda7c20173e55c1dd9fc
|
✓ Canonical reference refs/heads/master updated to f6cff86594495e9beccfeda7c20173e55c1dd9fc
|
||||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
f2de534..f6cff86 master -> master
|
f2de534..f6cff86 master -> master
|
||||||
```
|
```
|
||||||
|
|
@ -75,7 +75,7 @@ $ git reset --hard HEAD^ -q
|
||||||
```
|
```
|
||||||
``` ~alice RAD_SOCKET=/dev/null (stderr)
|
``` ~alice RAD_SOCKET=/dev/null (stderr)
|
||||||
$ git push -f
|
$ git push -f
|
||||||
✓ Canonical head updated to 319a7dc3b195368ded4b099f8c90bbb80addccd3
|
✓ Canonical reference refs/heads/master updated to 319a7dc3b195368ded4b099f8c90bbb80addccd3
|
||||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
+ f6cff86...319a7dc master -> master (forced update)
|
+ f6cff86...319a7dc master -> master (forced update)
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ Fast-forward
|
||||||
|
|
||||||
``` ~alice (stderr)
|
``` ~alice (stderr)
|
||||||
$ git push rad
|
$ git push rad
|
||||||
✓ Canonical head updated to 319a7dc3b195368ded4b099f8c90bbb80addccd3
|
✓ Canonical reference refs/heads/master updated to 319a7dc3b195368ded4b099f8c90bbb80addccd3
|
||||||
✓ Synced with 1 seed(s)
|
✓ Synced with 1 seed(s)
|
||||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
f2de534..319a7dc master -> master
|
f2de534..319a7dc master -> master
|
||||||
|
|
@ -54,7 +54,7 @@ push and the new canonical head becomes the previous commit again:
|
||||||
|
|
||||||
``` ~alice (stderr)
|
``` ~alice (stderr)
|
||||||
$ git push rad -f
|
$ git push rad -f
|
||||||
✓ Canonical head updated to f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
|
✓ Canonical reference refs/heads/master updated to f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
|
||||||
✓ Synced with 1 seed(s)
|
✓ Synced with 1 seed(s)
|
||||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
+ 319a7dc...f2de534 master -> master (forced update)
|
+ 319a7dc...f2de534 master -> master (forced update)
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ $ git commit -v -m "Define power requirements"
|
||||||
|
|
||||||
``` ~alice (stderr) RAD_SOCKET=/dev/null
|
``` ~alice (stderr) RAD_SOCKET=/dev/null
|
||||||
$ git push rad master
|
$ git push rad master
|
||||||
✓ Canonical head updated to 3e674d1a1df90807e934f9ae5da2591dd6848a33
|
✓ Canonical reference refs/heads/master updated to f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
|
||||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
f2de534..3e674d1 master -> master
|
f2de534..3e674d1 master -> master
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ $ git commit --amend --allow-empty -q -m "Amended change"
|
||||||
$ git checkout master -q
|
$ git checkout master -q
|
||||||
$ git merge feature/1 -q
|
$ git merge feature/1 -q
|
||||||
$ git push rad master
|
$ git push rad master
|
||||||
✓ Canonical head updated to 954bcdb5008447ce294a61a21d7eb87afbe7f4a6
|
✓ Canonical reference refs/heads/master updated to 954bcdb5008447ce294a61a21d7eb87afbe7f4a6
|
||||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
f2de534..954bcdb master -> master
|
f2de534..954bcdb master -> master
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ Finally, we push master and expect the patch to be merged.
|
||||||
``` (stderr) RAD_SOCKET=/dev/null
|
``` (stderr) RAD_SOCKET=/dev/null
|
||||||
$ git push rad master
|
$ git push rad master
|
||||||
✓ Patch 696ec5508494692899337afe6713fe1796d0315c merged
|
✓ Patch 696ec5508494692899337afe6713fe1796d0315c merged
|
||||||
✓ Canonical head updated to 737a10cfa29111afeb0d43cf3545cee386b939ec
|
✓ Canonical reference refs/heads/master updated to 737a10cfa29111afeb0d43cf3545cee386b939ec
|
||||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
f2de534..737a10c master -> master
|
f2de534..737a10c master -> master
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ When we push to `rad/master`, we automatically merge the patches:
|
||||||
$ git push rad master
|
$ git push rad master
|
||||||
✓ Patch 356f73863a8920455ff6e77cd9c805d68910551b merged
|
✓ Patch 356f73863a8920455ff6e77cd9c805d68910551b merged
|
||||||
✓ Patch 696ec5508494692899337afe6713fe1796d0315c merged
|
✓ Patch 696ec5508494692899337afe6713fe1796d0315c merged
|
||||||
✓ Canonical head updated to d6399c71702b40bae00825b3c444478d06b4e91c
|
✓ Canonical reference refs/heads/master updated to d6399c71702b40bae00825b3c444478d06b4e91c
|
||||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
f2de534..d6399c7 master -> master
|
f2de534..d6399c7 master -> master
|
||||||
```
|
```
|
||||||
|
|
@ -148,7 +148,7 @@ the first patch, even though they were pushed together.
|
||||||
$ git reset --hard HEAD^
|
$ git reset --hard HEAD^
|
||||||
$ git push -f rad
|
$ git push -f rad
|
||||||
! Patch 356f73863a8920455ff6e77cd9c805d68910551b reverted at revision 356f738
|
! Patch 356f73863a8920455ff6e77cd9c805d68910551b reverted at revision 356f738
|
||||||
✓ Canonical head updated to 20aa5dde6210796c3a2f04079b42316a31d02689
|
✓ Canonical reference refs/heads/master updated to 20aa5dde6210796c3a2f04079b42316a31d02689
|
||||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
+ d6399c7...20aa5dd master -> master (forced update)
|
+ d6399c7...20aa5dd master -> master (forced update)
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ $ git checkout master -q
|
||||||
$ git merge feature/1
|
$ git merge feature/1
|
||||||
$ git push rad master
|
$ git push rad master
|
||||||
✓ Patch 8dfb4dcafc4346158c8160410dd3f2b0616ad4fe merged
|
✓ Patch 8dfb4dcafc4346158c8160410dd3f2b0616ad4fe merged
|
||||||
✓ Canonical head updated to 20aa5dde6210796c3a2f04079b42316a31d02689
|
✓ Canonical reference refs/heads/master updated to 20aa5dde6210796c3a2f04079b42316a31d02689
|
||||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
f2de534..20aa5dd master -> master
|
f2de534..20aa5dd master -> master
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ $ git checkout master -q
|
||||||
$ git merge changes -q
|
$ git merge changes -q
|
||||||
$ git push rad master
|
$ git push rad master
|
||||||
✓ Patch acab0ec777a97d013f30be5d5d1aec32562ecb02 merged
|
✓ Patch acab0ec777a97d013f30be5d5d1aec32562ecb02 merged
|
||||||
✓ Canonical head updated to b2b6432af93f8fe188e32d400263021b602cfec8
|
✓ Canonical reference refs/heads/master updated to b2b6432af93f8fe188e32d400263021b602cfec8
|
||||||
✓ Synced with 1 seed(s)
|
✓ Synced with 1 seed(s)
|
||||||
|
|
||||||
https://app.radicle.xyz/nodes/[..]/rad:z3yXbb1sR6UG6ixxV2YF9jUP7ABra/tree/b2b6432af93f8fe188e32d400263021b602cfec8
|
https://app.radicle.xyz/nodes/[..]/rad:z3yXbb1sR6UG6ixxV2YF9jUP7ABra/tree/b2b6432af93f8fe188e32d400263021b602cfec8
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ Switched to branch 'master'
|
||||||
$ git merge feature/1
|
$ git merge feature/1
|
||||||
$ git push rad master
|
$ git push rad master
|
||||||
✓ Patch 696ec5508494692899337afe6713fe1796d0315c merged
|
✓ Patch 696ec5508494692899337afe6713fe1796d0315c merged
|
||||||
✓ Canonical head updated to 20aa5dde6210796c3a2f04079b42316a31d02689
|
✓ Canonical reference refs/heads/master updated to 20aa5dde6210796c3a2f04079b42316a31d02689
|
||||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
f2de534..20aa5dd master -> master
|
f2de534..20aa5dd master -> master
|
||||||
```
|
```
|
||||||
|
|
@ -50,7 +50,7 @@ When pushing, notice that we're told our patch is reverted.
|
||||||
``` (stderr) RAD_SOCKET=/dev/null
|
``` (stderr) RAD_SOCKET=/dev/null
|
||||||
$ git push rad master --force
|
$ git push rad master --force
|
||||||
! Patch 696ec5508494692899337afe6713fe1796d0315c reverted at revision 696ec55
|
! Patch 696ec5508494692899337afe6713fe1796d0315c reverted at revision 696ec55
|
||||||
✓ Canonical head updated to f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
|
✓ Canonical reference refs/heads/master updated to f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
|
||||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
+ 20aa5dd...f2de534 master -> master (forced update)
|
+ 20aa5dd...f2de534 master -> master (forced update)
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ Fast-forward
|
||||||
``` (stderr)
|
``` (stderr)
|
||||||
$ git push rad master
|
$ git push rad master
|
||||||
✓ Patch e4934b6d9dbe01ce3c7fbb5b77a80d5f1dacdc46 merged at revision 9d62420
|
✓ Patch e4934b6d9dbe01ce3c7fbb5b77a80d5f1dacdc46 merged at revision 9d62420
|
||||||
✓ Canonical head updated to f567f695d25b4e8fb63b5f5ad2a584529826e908
|
✓ Canonical reference refs/heads/master updated to f567f695d25b4e8fb63b5f5ad2a584529826e908
|
||||||
✓ Synced with 1 seed(s)
|
✓ Synced with 1 seed(s)
|
||||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
f2de534..f567f69 master -> master
|
f2de534..f567f69 master -> master
|
||||||
|
|
|
||||||
|
|
@ -7,15 +7,18 @@ use localtime::LocalTime;
|
||||||
|
|
||||||
use radicle::cob::TypedId;
|
use radicle::cob::TypedId;
|
||||||
use radicle::crypto::PublicKey;
|
use radicle::crypto::PublicKey;
|
||||||
|
use radicle::identity::crefs::GetCanonicalRefs as _;
|
||||||
use radicle::identity::DocAt;
|
use radicle::identity::DocAt;
|
||||||
use radicle::prelude::NodeId;
|
use radicle::prelude::NodeId;
|
||||||
use radicle::prelude::RepoId;
|
use radicle::prelude::RepoId;
|
||||||
|
use radicle::storage::git::Repository;
|
||||||
use radicle::storage::refs::RefsAt;
|
use radicle::storage::refs::RefsAt;
|
||||||
use radicle::storage::{
|
use radicle::storage::{
|
||||||
ReadRepository, ReadStorage as _, RefUpdate, RemoteRepository, RepositoryError,
|
ReadRepository, ReadStorage as _, RefUpdate, RemoteRepository, RepositoryError,
|
||||||
WriteRepository as _,
|
WriteRepository as _,
|
||||||
};
|
};
|
||||||
use radicle::{cob, git, node, Storage};
|
use radicle::{cob, git, node, Storage};
|
||||||
|
use radicle_fetch::git::refs::Applied;
|
||||||
use radicle_fetch::{Allowed, BlockList, FetchLimit};
|
use radicle_fetch::{Allowed, BlockList, FetchLimit};
|
||||||
|
|
||||||
use super::channels::ChannelsFlush;
|
use super::channels::ChannelsFlush;
|
||||||
|
|
@ -89,8 +92,6 @@ impl Handle {
|
||||||
remote: PublicKey,
|
remote: PublicKey,
|
||||||
refs_at: Option<Vec<RefsAt>>,
|
refs_at: Option<Vec<RefsAt>>,
|
||||||
) -> Result<FetchResult, error::Fetch> {
|
) -> Result<FetchResult, error::Fetch> {
|
||||||
use git::canonical::QuorumError::{Diverging, NoCandidates};
|
|
||||||
|
|
||||||
let (result, clone, notifs) = match self {
|
let (result, clone, notifs) = match self {
|
||||||
Self::Clone { mut handle, tmp } => {
|
Self::Clone { mut handle, tmp } => {
|
||||||
log::debug!(target: "worker", "{} cloning from {remote}", handle.local());
|
log::debug!(target: "worker", "{} cloning from {remote}", handle.local());
|
||||||
|
|
@ -145,15 +146,19 @@ impl Handle {
|
||||||
log::trace!(target: "worker", "Set HEAD to {}", head.new);
|
log::trace!(target: "worker", "Set HEAD to {}", head.new);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(RepositoryError::Quorum(Diverging(e))) => {
|
Err(RepositoryError::Quorum(radicle::git::canonical::QuorumError::Git(e))) => {
|
||||||
log::warn!(target: "worker", "Fetch could not set HEAD: {e}")
|
return Err(e.into())
|
||||||
}
|
}
|
||||||
Err(RepositoryError::Quorum(NoCandidates(e))) => {
|
Err(RepositoryError::Quorum(e)) => {
|
||||||
log::warn!(target: "worker", "Fetch could not set HEAD: {e}")
|
log::warn!(target: "worker", "Fetch could not set HEAD: {e}")
|
||||||
}
|
}
|
||||||
Err(e) => return Err(e.into()),
|
Err(e) => return Err(e.into()),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Err(e) = set_canonical_refs(&repo, &applied) {
|
||||||
|
log::warn!(target: "worker", "Failed to set canonical references: {e}");
|
||||||
|
}
|
||||||
|
|
||||||
// Notifications are only posted for pulls, not clones.
|
// Notifications are only posted for pulls, not clones.
|
||||||
if let Some(mut store) = notifs {
|
if let Some(mut store) = notifs {
|
||||||
// Only create notifications for repos that we have
|
// Only create notifications for repos that we have
|
||||||
|
|
@ -377,3 +382,71 @@ where
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_canonical_refs(repo: &Repository, applied: &Applied) -> Result<(), error::Canonical> {
|
||||||
|
let identity = repo.identity()?;
|
||||||
|
let rules = match identity
|
||||||
|
.canonical_refs()?
|
||||||
|
.map(|crefs| crefs.rules().clone())
|
||||||
|
.filter(|rules| !rules.is_empty())
|
||||||
|
{
|
||||||
|
None => return Ok(()),
|
||||||
|
Some(rules) => rules,
|
||||||
|
};
|
||||||
|
|
||||||
|
for update in applied.updated.iter() {
|
||||||
|
let name = match update {
|
||||||
|
RefUpdate::Updated { name, .. } | RefUpdate::Created { name, .. } => name,
|
||||||
|
_ => {
|
||||||
|
log::trace!(target: "worker", "Skipping update {update}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let Some(name) = name.clone().into_qualified() else {
|
||||||
|
log::warn!(target: "worker", "Skipping update for canonical reference '{name}' because it is not qualified.");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(name) = name.to_namespaced() else {
|
||||||
|
log::warn!(target: "worker", "Skipping update for canonical reference '{name}' because it is not namespaced.");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let name = name.strip_namespace();
|
||||||
|
|
||||||
|
let canonical = match rules.canonical(name.clone(), repo) {
|
||||||
|
Ok(Some(canonical)) => canonical,
|
||||||
|
Ok(None) => continue,
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!(target: "worker", "Failed to get canonical reference rule for {name}: {e}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match canonical.quorum(&repo.backend) {
|
||||||
|
Err(err) => {
|
||||||
|
log::warn!(
|
||||||
|
target: "worker",
|
||||||
|
"Failed to calculate canonical reference: {}",
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Ok((refname, oid)) => {
|
||||||
|
if let Err(e) = repo.backend.reference(
|
||||||
|
refname.clone().as_str(),
|
||||||
|
*oid,
|
||||||
|
true,
|
||||||
|
"set-canonical-reference from fetch (radicle)",
|
||||||
|
) {
|
||||||
|
log::warn!(
|
||||||
|
target: "worker",
|
||||||
|
"Failed to set canonical reference {}->{}: {e}",
|
||||||
|
refname,
|
||||||
|
oid
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,3 +65,11 @@ pub enum Handle {
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Repository(#[from] radicle::storage::RepositoryError),
|
Repository(#[from] radicle::storage::RepositoryError),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum Canonical {
|
||||||
|
#[error(transparent)]
|
||||||
|
Identity(#[from] radicle::storage::RepositoryError),
|
||||||
|
#[error(transparent)]
|
||||||
|
Payload(#[from] radicle::identity::PayloadError),
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ use std::path::{Path, PathBuf};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::{assert_eq, io};
|
use std::{assert_eq, io};
|
||||||
|
|
||||||
|
use radicle::identity::crefs::GetCanonicalRefs as _;
|
||||||
|
use radicle::identity::doc::DefaultBranchRuleError;
|
||||||
use radicle::node::device::Device;
|
use radicle::node::device::Device;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
|
@ -15,8 +17,7 @@ use radicle::cob::patch::cache::Patches as _;
|
||||||
use radicle::crypto;
|
use radicle::crypto;
|
||||||
use radicle::explorer::ExplorerResource;
|
use radicle::explorer::ExplorerResource;
|
||||||
use radicle::git::canonical;
|
use radicle::git::canonical;
|
||||||
use radicle::git::canonical::Canonical;
|
use radicle::identity::{CanonicalRefs, Did};
|
||||||
use radicle::identity::Did;
|
|
||||||
use radicle::node;
|
use radicle::node;
|
||||||
use radicle::node::{Handle, NodeId};
|
use radicle::node::{Handle, NodeId};
|
||||||
use radicle::storage;
|
use radicle::storage;
|
||||||
|
|
@ -116,6 +117,8 @@ pub enum Error {
|
||||||
/// Quorum error.
|
/// Quorum error.
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Quorum(#[from] radicle::git::canonical::QuorumError),
|
Quorum(#[from] radicle::git::canonical::QuorumError),
|
||||||
|
#[error(transparent)]
|
||||||
|
DefaultBranchRule(#[from] radicle::identity::doc::DefaultBranchRuleError),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Push command.
|
/// Push command.
|
||||||
|
|
@ -203,6 +206,10 @@ pub fn run(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let delegates = stored.delegates()?;
|
let delegates = stored.delegates()?;
|
||||||
|
let identity = stored.identity()?;
|
||||||
|
let project = identity.project()?;
|
||||||
|
let canonical_ref = git::refs::branch(project.default_branch());
|
||||||
|
let mut set_canonical_refs: Vec<(git::Qualified, git::Oid)> = Vec::with_capacity(specs.len());
|
||||||
|
|
||||||
// For each refspec, push a ref or delete a ref.
|
// For each refspec, push a ref or delete a ref.
|
||||||
for spec in specs {
|
for spec in specs {
|
||||||
|
|
@ -262,8 +269,11 @@ pub fn run(
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
let identity = stored.identity()?;
|
let identity = stored.identity()?;
|
||||||
let project = identity.project()?;
|
let crefs = identity.canonical_refs_or_default(|| {
|
||||||
let canonical_ref = git::refs::branch(project.default_branch());
|
let rule = identity.doc().default_branch_rule()?;
|
||||||
|
Ok::<_, DefaultBranchRuleError>(CanonicalRefs::from_iter([rule]))
|
||||||
|
})?;
|
||||||
|
let rules = crefs.rules();
|
||||||
let me = Did::from(nid);
|
let me = Did::from(nid);
|
||||||
|
|
||||||
// If we're trying to update the canonical head, make sure
|
// If we're trying to update the canonical head, make sure
|
||||||
|
|
@ -272,29 +282,21 @@ pub fn run(
|
||||||
//
|
//
|
||||||
// Note that we *do* allow rolling back to a previous commit on the
|
// Note that we *do* allow rolling back to a previous commit on the
|
||||||
// canonical branch.
|
// canonical branch.
|
||||||
if dst == canonical_ref && delegates.contains(&me) && delegates.len() > 1 {
|
if let Some(mut canonical) = rules.canonical(dst.clone(), stored)? {
|
||||||
|
if canonical.is_allowed(&me) {
|
||||||
let head = working.find_reference(src.as_str())?;
|
let head = working.find_reference(src.as_str())?;
|
||||||
let head = head.peel_to_commit()?.id();
|
let head = head.peel_to_commit()?.id();
|
||||||
|
let converges =
|
||||||
|
canonical.converges(&working, (&me, &head.into()))?;
|
||||||
|
|
||||||
let mut canonical = Canonical::default_branch(
|
// If `canonical` is empty then we're creating a new reference.
|
||||||
stored,
|
// If we're the only delegate then we need to modify our vote.
|
||||||
&project,
|
if converges || canonical.has_no_tips() || canonical.is_only(&me) {
|
||||||
identity.delegates().as_ref(),
|
|
||||||
identity.threshold(),
|
|
||||||
)?;
|
|
||||||
let converges = canonical::converges(
|
|
||||||
canonical
|
|
||||||
.tips()
|
|
||||||
.filter_map(|(did, tip)| (*did != me).then_some(tip)),
|
|
||||||
head.into(),
|
|
||||||
&working,
|
|
||||||
)?;
|
|
||||||
if converges {
|
|
||||||
canonical.modify_vote(me, head.into());
|
canonical.modify_vote(me, head.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
match canonical.quorum(&working) {
|
match canonical.quorum(&working) {
|
||||||
Ok(canonical_oid) => {
|
Ok((dst, canonical_oid)) => {
|
||||||
// Canonical head is an ancestor of head.
|
// Canonical head is an ancestor of head.
|
||||||
let is_ff = head == *canonical_oid
|
let is_ff = head == *canonical_oid
|
||||||
|| working.graph_descendant_of(head, *canonical_oid)?;
|
|| working.graph_descendant_of(head, *canonical_oid)?;
|
||||||
|
|
@ -302,8 +304,8 @@ pub fn run(
|
||||||
if !is_ff && !converges {
|
if !is_ff && !converges {
|
||||||
if hints {
|
if hints {
|
||||||
hint(
|
hint(
|
||||||
"you are attempting to push a commit that would cause \
|
format!("you are attempting to push a commit that would cause \
|
||||||
your upstream to diverge from the canonical head",
|
your upstream to diverge from the canonical reference {dst}"),
|
||||||
);
|
);
|
||||||
hint(
|
hint(
|
||||||
"to integrate the remote changes, run `git pull --rebase` \
|
"to integrate the remote changes, run `git pull --rebase` \
|
||||||
|
|
@ -315,23 +317,16 @@ pub fn run(
|
||||||
canonical_oid,
|
canonical_oid,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
set_canonical_refs
|
||||||
|
.push((dst.clone().to_owned(), canonical_oid));
|
||||||
}
|
}
|
||||||
Err(canonical::QuorumError::Diverging(e)) => {
|
Err(canonical::QuorumError::Git(e)) => return Err(e.into()),
|
||||||
warn(format!(
|
Err(e) => {
|
||||||
"could not determine canonical tip for `{canonical_ref}`"
|
|
||||||
));
|
|
||||||
warn(e.to_string());
|
warn(e.to_string());
|
||||||
warn("it is recommended to find a commit to agree upon");
|
warn("it is recommended to find a commit to agree upon");
|
||||||
}
|
}
|
||||||
Err(canonical::QuorumError::NoCandidates(e)) => {
|
|
||||||
warn(format!(
|
|
||||||
"could not determine canonical tip for `{canonical_ref}`"
|
|
||||||
));
|
|
||||||
warn(e.to_string());
|
|
||||||
warn("it is recommended to find a commit to agree upon");
|
|
||||||
}
|
}
|
||||||
Err(e) => return Err(e.into()),
|
}
|
||||||
};
|
|
||||||
}
|
}
|
||||||
push(src, &dst, *force, &nid, &working, stored, patches, &signer)
|
push(src, &dst, *force, &nid, &working, stored, patches, &signer)
|
||||||
}
|
}
|
||||||
|
|
@ -354,17 +349,51 @@ pub fn run(
|
||||||
if !ok.is_empty() {
|
if !ok.is_empty() {
|
||||||
let _ = stored.sign_refs(&signer)?;
|
let _ = stored.sign_refs(&signer)?;
|
||||||
|
|
||||||
// N.b. if an error occurs then there may be no quorum
|
for (refname, oid) in &set_canonical_refs {
|
||||||
if let Ok(head) = stored.set_head() {
|
let print_update = || {
|
||||||
if head.is_updated() {
|
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"{} Canonical head updated to {}",
|
"{} Canonical reference {} updated to {}",
|
||||||
term::format::positive("✓"),
|
term::format::positive("✓"),
|
||||||
term::format::secondary(head.new),
|
term::format::secondary(refname),
|
||||||
);
|
term::format::secondary(oid),
|
||||||
}
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// N.b. special case for handling the canonical ref, since it
|
||||||
|
// creates a symlink to HEAD
|
||||||
|
if *refname == canonical_ref
|
||||||
|
&& stored
|
||||||
|
.set_head()
|
||||||
|
.map(|head| head.is_updated())
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
|
print_update();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
match stored.backend.refname_to_id(refname.as_str()) {
|
||||||
|
Ok(new) if new != **oid => {
|
||||||
|
stored.backend.reference(
|
||||||
|
refname.as_str(),
|
||||||
|
**oid,
|
||||||
|
true,
|
||||||
|
"set-canonical-reference from git-push (radicle)",
|
||||||
|
)?;
|
||||||
|
print_update();
|
||||||
|
}
|
||||||
|
Err(e) if e.code() == git::raw::ErrorCode::NotFound => {
|
||||||
|
stored.backend.reference(
|
||||||
|
refname.as_str(),
|
||||||
|
**oid,
|
||||||
|
true,
|
||||||
|
"set-canonical-reference from git-push (radicle)",
|
||||||
|
)?;
|
||||||
|
print_update();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !opts.no_sync {
|
if !opts.no_sync {
|
||||||
if profile.policies()?.is_seeding(&stored.id)? {
|
if profile.policies()?.is_seeding(&stored.id)? {
|
||||||
// Connect to local node and announce refs to the network.
|
// Connect to local node and announce refs to the network.
|
||||||
|
|
|
||||||
|
|
@ -2,126 +2,64 @@ pub mod rules;
|
||||||
pub use rules::{MatchedRule, RawRule, Rules, ValidRule};
|
pub use rules::{MatchedRule, RawRule, Rules, ValidRule};
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::fmt;
|
|
||||||
|
|
||||||
use nonempty::NonEmpty;
|
|
||||||
use raw::Repository;
|
use raw::Repository;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::prelude::Did;
|
use crate::prelude::Did;
|
||||||
use crate::prelude::Project;
|
|
||||||
use crate::storage::ReadRepository;
|
use crate::storage::ReadRepository;
|
||||||
|
|
||||||
use super::raw;
|
use super::raw;
|
||||||
use super::{lit, Oid, Qualified};
|
use super::{Oid, Qualified};
|
||||||
|
|
||||||
/// A collection of [`Did`]s and their [`Oid`]s that is the tip for a given
|
/// A collection of [`Did`]s and their [`Oid`]s that is the tip for a given
|
||||||
/// reference for that [`Did`].
|
/// reference for that [`Did`].
|
||||||
///
|
///
|
||||||
/// The general construction of `Canonical` is by using the
|
/// The general construction of `Canonical` is by using the [`Canonical::new`]
|
||||||
/// [`Canonical::reference`] constructor. For the default branch of a
|
/// constructor.
|
||||||
/// [`Project`], use [`Canonical::default_branch`].
|
|
||||||
///
|
///
|
||||||
/// `Canonical` can then be used for performing calculations about the
|
/// `Canonical` can then be used for performing calculations about the
|
||||||
/// canonicity of the reference, most importantly the [`Canonical::quorum`].
|
/// canonicity of the reference, most importantly the [`Canonical::quorum`].
|
||||||
|
///
|
||||||
|
/// References to the refname and the matched rule are kept, as they
|
||||||
|
/// are very handy for generating error messages.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Canonical {
|
pub struct Canonical<'a, 'b> {
|
||||||
|
refname: Qualified<'a>,
|
||||||
|
rule: &'b ValidRule,
|
||||||
tips: BTreeMap<Did, Oid>,
|
tips: BTreeMap<Did, Oid>,
|
||||||
threshold: usize,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Error that can occur when calculation the [`Canonical::quorum`].
|
/// Error that can occur when calculation the [`Canonical::quorum`].
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum QuorumError {
|
pub enum QuorumError {
|
||||||
/// Could not determine a quorum [`Oid`], due to diverging tips.
|
/// Could not determine a quorum [`Oid`], due to diverging tips.
|
||||||
#[error("could not determine canonical reference tip, {0}")]
|
#[error("could not determine commit for canonical reference '{refname}', found diverging commits {longest} and {head}, with base commit {base} and threshold {threshold}")]
|
||||||
Diverging(Diverging),
|
Diverging {
|
||||||
|
refname: String,
|
||||||
|
threshold: usize,
|
||||||
|
base: Oid,
|
||||||
|
longest: Oid,
|
||||||
|
head: Oid,
|
||||||
|
},
|
||||||
/// Could not determine a base candidate from the given set of delegates.
|
/// Could not determine a base candidate from the given set of delegates.
|
||||||
#[error("could not determine canonical reference tip, {0}")]
|
#[error("could not determine commit for canonical reference '{refname}', no commit with at least {threshold} vote(s) found (threshold not met)")]
|
||||||
NoCandidates(NoCandidates),
|
NoCandidates { refname: String, threshold: usize },
|
||||||
/// An error occurred from [`git2`].
|
/// An error occurred from [`git2`].
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Git(#[from] git2::Error),
|
Git(#[from] git2::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// No candidates were found for the [`Canonical::quorum`] calculation.
|
impl<'a, 'b> Canonical<'a, 'b> {
|
||||||
///
|
/// Construct the set of canonical tips given for the given `rule` and
|
||||||
/// The [`fmt::Display`] is used in [`QuorumError`], to provide information on
|
/// the reference `refname`.
|
||||||
/// the threshold and delegates in the calculation.
|
pub fn new<S>(repo: &S, refname: Qualified<'a>, rule: &'b ValidRule) -> Result<Self, raw::Error>
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct NoCandidates {
|
|
||||||
threshold: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for NoCandidates {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
let NoCandidates { threshold } = self;
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"no commit found with at least {threshold} vote(s) (threshold not met)"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Diverging commits were found during the [`Canonical::quorum`] calculation.
|
|
||||||
///
|
|
||||||
/// The [`fmt::Display`] is used in [`QuorumError`], to provide information on
|
|
||||||
/// the threshold, base commit, and the two diverging commits, in the
|
|
||||||
/// calculation.
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Diverging {
|
|
||||||
threshold: usize,
|
|
||||||
base: Oid,
|
|
||||||
longest: Oid,
|
|
||||||
head: Oid,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for Diverging {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
let Diverging {
|
|
||||||
threshold,
|
|
||||||
base,
|
|
||||||
longest,
|
|
||||||
head,
|
|
||||||
} = self;
|
|
||||||
write!(f, "found diverging commits {longest} and {head}, with base commit {base} and threshold {threshold}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Canonical {
|
|
||||||
/// Construct the set of canonical tips of the `Project::default_branch` for
|
|
||||||
/// the given `delegates`.
|
|
||||||
pub fn default_branch<S>(
|
|
||||||
repo: &S,
|
|
||||||
project: &Project,
|
|
||||||
delegates: &NonEmpty<Did>,
|
|
||||||
threshold: usize,
|
|
||||||
) -> Result<Self, raw::Error>
|
|
||||||
where
|
|
||||||
S: ReadRepository,
|
|
||||||
{
|
|
||||||
Self::reference(
|
|
||||||
repo,
|
|
||||||
&lit::refs_heads(project.default_branch()).into(),
|
|
||||||
delegates,
|
|
||||||
threshold,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Construct the set of canonical tips given for the given `delegates` and
|
|
||||||
/// the reference `name`.
|
|
||||||
pub fn reference<S>(
|
|
||||||
repo: &S,
|
|
||||||
refname: &Qualified,
|
|
||||||
delegates: &NonEmpty<Did>,
|
|
||||||
threshold: usize,
|
|
||||||
) -> Result<Self, raw::Error>
|
|
||||||
where
|
where
|
||||||
S: ReadRepository,
|
S: ReadRepository,
|
||||||
{
|
{
|
||||||
let mut tips = BTreeMap::new();
|
let mut tips = BTreeMap::new();
|
||||||
for delegate in delegates.iter() {
|
for delegate in rule.allowed().iter() {
|
||||||
match repo.reference_oid(delegate, refname) {
|
match repo.reference_oid(delegate, &refname) {
|
||||||
Ok(tip) => {
|
Ok(tip) => {
|
||||||
tips.insert(*delegate, tip);
|
tips.insert(*delegate, tip);
|
||||||
}
|
}
|
||||||
|
|
@ -135,7 +73,11 @@ impl Canonical {
|
||||||
Err(e) => return Err(e),
|
Err(e) => return Err(e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(Canonical { tips, threshold })
|
Ok(Canonical {
|
||||||
|
refname,
|
||||||
|
tips,
|
||||||
|
rule,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the set of [`Did`]s and their [`Oid`] tip.
|
/// Return the set of [`Did`]s and their [`Oid`] tip.
|
||||||
|
|
@ -143,36 +85,18 @@ impl Canonical {
|
||||||
self.tips.iter()
|
self.tips.iter()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `true` is there were no tips found for any of the delegates for
|
/// Returns `true` if there were no tips found for any of the DIDs for
|
||||||
/// the given reference.
|
/// the given reference.
|
||||||
///
|
///
|
||||||
/// N.b. this may be the case when a new reference is being created.
|
/// N.b. this may be the case when a new reference is being created.
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn has_no_tips(&self) -> bool {
|
||||||
self.tips.is_empty()
|
self.tips.is_empty()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// Check that a given `target` converges with any of the provided `tips`.
|
pub fn refname(&self) -> &Qualified {
|
||||||
///
|
&self.refname
|
||||||
/// It converges if the `target` is either equal to, ahead of, or behind any of
|
|
||||||
/// the tips.
|
|
||||||
pub fn converges<'a>(
|
|
||||||
tips: impl Iterator<Item = &'a Oid>,
|
|
||||||
target: Oid,
|
|
||||||
repo: &Repository,
|
|
||||||
) -> Result<bool, raw::Error> {
|
|
||||||
for tip in tips {
|
|
||||||
match repo.graph_ahead_behind(*target, **tip)? {
|
|
||||||
(0, 0) => return Ok(true),
|
|
||||||
(ahead, behind) if ahead > 0 && behind == 0 => return Ok(true),
|
|
||||||
(ahead, behind) if behind > 0 && ahead == 0 => return Ok(true),
|
|
||||||
(_, _) => {}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Ok(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Canonical {
|
|
||||||
/// In some cases, we allow the vote to be modified. For example, when the
|
/// In some cases, we allow the vote to be modified. For example, when the
|
||||||
/// `did` is pushing a new commit, we may want to see if the new commit will
|
/// `did` is pushing a new commit, we may want to see if the new commit will
|
||||||
/// reach a quorum.
|
/// reach a quorum.
|
||||||
|
|
@ -180,6 +104,41 @@ impl Canonical {
|
||||||
self.tips.insert(did, new);
|
self.tips.insert(did, new);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check that the provided `did` is part of the set of allowed
|
||||||
|
/// DIDs of the matching rule.
|
||||||
|
pub fn is_allowed(&self, did: &Did) -> bool {
|
||||||
|
self.rule.allowed().contains(did)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check that the provided `did` is the only DID in the set of allowed
|
||||||
|
/// DIDs of the matching rule.
|
||||||
|
pub fn is_only(&self, did: &Did) -> bool {
|
||||||
|
self.rule.allowed().is_only(did)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks that setting the given candidate tip would converge with at least
|
||||||
|
/// one other known tip.
|
||||||
|
///
|
||||||
|
/// It converges if the candidate Oid is either equal to, ahead of, or behind any of
|
||||||
|
/// the tips.
|
||||||
|
pub fn converges(
|
||||||
|
&self,
|
||||||
|
repo: &Repository,
|
||||||
|
candidate: (&Did, &Oid),
|
||||||
|
) -> Result<bool, raw::Error> {
|
||||||
|
for tip in self
|
||||||
|
.tips
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(did, tip)| (did != candidate.0).then_some(tip))
|
||||||
|
{
|
||||||
|
let (ahead, behind) = repo.graph_ahead_behind(**candidate.1, **tip)?;
|
||||||
|
if ahead * behind == 0 {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
/// Computes the quorum or "canonical" tip based on the tips, of `Canonical`,
|
/// Computes the quorum or "canonical" tip based on the tips, of `Canonical`,
|
||||||
/// and the threshold. This can be described as the latest commit that is
|
/// and the threshold. This can be described as the latest commit that is
|
||||||
/// included in at least `threshold` histories. In case there are multiple tips
|
/// included in at least `threshold` histories. In case there are multiple tips
|
||||||
|
|
@ -187,7 +146,7 @@ impl Canonical {
|
||||||
///
|
///
|
||||||
/// Also returns an error if `heads` is empty or `threshold` cannot be
|
/// Also returns an error if `heads` is empty or `threshold` cannot be
|
||||||
/// satisified with the number of heads given.
|
/// satisified with the number of heads given.
|
||||||
pub fn quorum(self, repo: &raw::Repository) -> Result<Oid, QuorumError> {
|
pub fn quorum(self, repo: &raw::Repository) -> Result<(Qualified<'a>, Oid), QuorumError> {
|
||||||
let mut candidates = BTreeMap::<_, usize>::new();
|
let mut candidates = BTreeMap::<_, usize>::new();
|
||||||
|
|
||||||
// Build a list of candidate commits and count how many "votes" each of them has.
|
// Build a list of candidate commits and count how many "votes" each of them has.
|
||||||
|
|
@ -212,14 +171,12 @@ impl Canonical {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Keep commits which pass the threshold.
|
// Keep commits which pass the threshold.
|
||||||
candidates.retain(|_, votes| *votes >= self.threshold);
|
candidates.retain(|_, votes| *votes >= self.threshold());
|
||||||
|
|
||||||
let (mut longest, _) =
|
let (mut longest, _) = candidates.pop_first().ok_or(QuorumError::NoCandidates {
|
||||||
candidates
|
refname: self.refname.to_string(),
|
||||||
.pop_first()
|
threshold: self.threshold(),
|
||||||
.ok_or(QuorumError::NoCandidates(NoCandidates {
|
})?;
|
||||||
threshold: self.threshold,
|
|
||||||
}))?;
|
|
||||||
|
|
||||||
// Now that all scores are calculated, figure out what is the longest branch
|
// Now that all scores are calculated, figure out what is the longest branch
|
||||||
// that passes the threshold. In case of divergence, return an error.
|
// that passes the threshold. In case of divergence, return an error.
|
||||||
|
|
@ -253,15 +210,20 @@ impl Canonical {
|
||||||
// o (base)
|
// o (base)
|
||||||
// |
|
// |
|
||||||
//
|
//
|
||||||
return Err(QuorumError::Diverging(Diverging {
|
return Err(QuorumError::Diverging {
|
||||||
threshold: self.threshold,
|
refname: self.refname.to_string(),
|
||||||
|
threshold: self.threshold(),
|
||||||
base: base.into(),
|
base: base.into(),
|
||||||
longest,
|
longest,
|
||||||
head: *head,
|
head: *head,
|
||||||
}));
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok((*longest).into())
|
Ok((self.refname, (*longest).into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn threshold(&self) -> usize {
|
||||||
|
(*self.rule.threshold()).into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -281,7 +243,7 @@ mod tests {
|
||||||
threshold: usize,
|
threshold: usize,
|
||||||
repo: &git::raw::Repository,
|
repo: &git::raw::Repository,
|
||||||
) -> Result<Oid, QuorumError> {
|
) -> Result<Oid, QuorumError> {
|
||||||
let tips = heads
|
let tips: BTreeMap<Did, Oid> = heads
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, head)| {
|
.map(|(i, head)| {
|
||||||
|
|
@ -290,7 +252,24 @@ mod tests {
|
||||||
(did, (*head).into())
|
(did, (*head).into())
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
Canonical { tips, threshold }.quorum(repo)
|
|
||||||
|
let refname =
|
||||||
|
git::refs::branch(git_ext::ref_format::RefStr::try_from_str("master").unwrap());
|
||||||
|
|
||||||
|
let rule: RawRule = crate::git::canonical::rules::Rule::new(
|
||||||
|
crate::git::canonical::rules::Allowed::Delegates,
|
||||||
|
threshold,
|
||||||
|
);
|
||||||
|
let delegates = crate::identity::doc::Delegates::new(tips.keys().cloned()).unwrap();
|
||||||
|
let rule = rule.validate(&mut || delegates.clone()).unwrap();
|
||||||
|
|
||||||
|
Canonical {
|
||||||
|
refname,
|
||||||
|
tips,
|
||||||
|
rule: &rule,
|
||||||
|
}
|
||||||
|
.quorum(repo)
|
||||||
|
.map(|(_, oid)| oid)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -352,9 +331,6 @@ mod tests {
|
||||||
assert_eq!(quorum(&[*c0], 1, &repo).unwrap(), c0);
|
assert_eq!(quorum(&[*c0], 1, &repo).unwrap(), c0);
|
||||||
assert_eq!(quorum(&[*c1], 1, &repo).unwrap(), c1);
|
assert_eq!(quorum(&[*c1], 1, &repo).unwrap(), c1);
|
||||||
assert_eq!(quorum(&[*c2], 1, &repo).unwrap(), c2);
|
assert_eq!(quorum(&[*c2], 1, &repo).unwrap(), c2);
|
||||||
assert_eq!(quorum(&[*c0], 0, &repo).unwrap(), c0);
|
|
||||||
assert_matches!(quorum(&[], 0, &repo), Err(QuorumError::NoCandidates(_)));
|
|
||||||
assert_matches!(quorum(&[*c0], 2, &repo), Err(QuorumError::NoCandidates(_)));
|
|
||||||
|
|
||||||
// C1
|
// C1
|
||||||
// |
|
// |
|
||||||
|
|
@ -380,23 +356,23 @@ mod tests {
|
||||||
// C0
|
// C0
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*c1, *c2, *b2], 1, &repo),
|
quorum(&[*c1, *c2, *b2], 1, &repo),
|
||||||
Err(QuorumError::Diverging(_))
|
Err(QuorumError::Diverging { .. })
|
||||||
);
|
);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*c2, *b2], 1, &repo),
|
quorum(&[*c2, *b2], 1, &repo),
|
||||||
Err(QuorumError::Diverging(_))
|
Err(QuorumError::Diverging { .. })
|
||||||
);
|
);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*b2, *c2], 1, &repo),
|
quorum(&[*b2, *c2], 1, &repo),
|
||||||
Err(QuorumError::Diverging(_))
|
Err(QuorumError::Diverging { .. })
|
||||||
);
|
);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*c2, *b2], 2, &repo),
|
quorum(&[*c2, *b2], 2, &repo),
|
||||||
Err(QuorumError::NoCandidates(_))
|
Err(QuorumError::NoCandidates { .. })
|
||||||
);
|
);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*b2, *c2], 2, &repo),
|
quorum(&[*b2, *c2], 2, &repo),
|
||||||
Err(QuorumError::NoCandidates(_))
|
Err(QuorumError::NoCandidates { .. })
|
||||||
);
|
);
|
||||||
assert_eq!(quorum(&[*c1, *c2, *b2], 2, &repo).unwrap(), c1);
|
assert_eq!(quorum(&[*c1, *c2, *b2], 2, &repo).unwrap(), c1);
|
||||||
assert_eq!(quorum(&[*c1, *c2, *b2], 3, &repo).unwrap(), c1);
|
assert_eq!(quorum(&[*c1, *c2, *b2], 3, &repo).unwrap(), c1);
|
||||||
|
|
@ -404,7 +380,7 @@ mod tests {
|
||||||
assert_eq!(quorum(&[*b2, *c2, *c2], 2, &repo).unwrap(), c2);
|
assert_eq!(quorum(&[*b2, *c2, *c2], 2, &repo).unwrap(), c2);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*b2, *b2, *c2, *c2], 2, &repo),
|
quorum(&[*b2, *b2, *c2, *c2], 2, &repo),
|
||||||
Err(QuorumError::Diverging(_))
|
Err(QuorumError::Diverging { .. })
|
||||||
);
|
);
|
||||||
|
|
||||||
// B2 C2 C3
|
// B2 C2 C3
|
||||||
|
|
@ -415,15 +391,15 @@ mod tests {
|
||||||
assert_eq!(quorum(&[*b2, *c2, *c2], 2, &repo).unwrap(), c2);
|
assert_eq!(quorum(&[*b2, *c2, *c2], 2, &repo).unwrap(), c2);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*b2, *c2, *c2], 3, &repo),
|
quorum(&[*b2, *c2, *c2], 3, &repo),
|
||||||
Err(QuorumError::NoCandidates(_))
|
Err(QuorumError::NoCandidates { .. })
|
||||||
);
|
);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*b2, *c2, *b2, *c2], 3, &repo),
|
quorum(&[*b2, *c2, *b2, *c2], 3, &repo),
|
||||||
Err(QuorumError::NoCandidates(_))
|
Err(QuorumError::NoCandidates { .. })
|
||||||
);
|
);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*c3, *b2, *c2, *b2, *c2, *c3], 3, &repo),
|
quorum(&[*c3, *b2, *c2, *b2, *c2, *c3], 3, &repo),
|
||||||
Err(QuorumError::NoCandidates(_))
|
Err(QuorumError::NoCandidates { .. })
|
||||||
);
|
);
|
||||||
|
|
||||||
// B2 C2
|
// B2 C2
|
||||||
|
|
@ -433,19 +409,19 @@ mod tests {
|
||||||
// C0
|
// C0
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*c2, *b2, *a1], 1, &repo),
|
quorum(&[*c2, *b2, *a1], 1, &repo),
|
||||||
Err(QuorumError::Diverging(_))
|
Err(QuorumError::Diverging { .. })
|
||||||
);
|
);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*c2, *b2, *a1], 2, &repo),
|
quorum(&[*c2, *b2, *a1], 2, &repo),
|
||||||
Err(QuorumError::NoCandidates(_))
|
Err(QuorumError::NoCandidates { .. })
|
||||||
);
|
);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*c2, *b2, *a1], 3, &repo),
|
quorum(&[*c2, *b2, *a1], 3, &repo),
|
||||||
Err(QuorumError::NoCandidates(_))
|
Err(QuorumError::NoCandidates { .. })
|
||||||
);
|
);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*c1, *c2, *b2, *a1], 4, &repo),
|
quorum(&[*c1, *c2, *b2, *a1], 4, &repo),
|
||||||
Err(QuorumError::NoCandidates(_))
|
Err(QuorumError::NoCandidates { .. })
|
||||||
);
|
);
|
||||||
assert_eq!(quorum(&[*c0, *c1, *c2, *b2, *a1], 2, &repo).unwrap(), c1,);
|
assert_eq!(quorum(&[*c0, *c1, *c2, *b2, *a1], 2, &repo).unwrap(), c1,);
|
||||||
assert_eq!(quorum(&[*c0, *c1, *c2, *b2, *a1], 3, &repo).unwrap(), c1,);
|
assert_eq!(quorum(&[*c0, *c1, *c2, *b2, *a1], 3, &repo).unwrap(), c1,);
|
||||||
|
|
@ -453,23 +429,23 @@ mod tests {
|
||||||
assert_eq!(quorum(&[*c0, *c1, *c2, *b2, *a1], 4, &repo).unwrap(), c0,);
|
assert_eq!(quorum(&[*c0, *c1, *c2, *b2, *a1], 4, &repo).unwrap(), c0,);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*a1, *a1, *c2, *c2, *c1], 2, &repo),
|
quorum(&[*a1, *a1, *c2, *c2, *c1], 2, &repo),
|
||||||
Err(QuorumError::Diverging(_))
|
Err(QuorumError::Diverging { .. })
|
||||||
);
|
);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*a1, *a1, *c2, *c2, *c1], 1, &repo),
|
quorum(&[*a1, *a1, *c2, *c2, *c1], 1, &repo),
|
||||||
Err(QuorumError::Diverging(_))
|
Err(QuorumError::Diverging { .. })
|
||||||
);
|
);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*a1, *a1, *c2], 1, &repo),
|
quorum(&[*a1, *a1, *c2], 1, &repo),
|
||||||
Err(QuorumError::Diverging(_))
|
Err(QuorumError::Diverging { .. })
|
||||||
);
|
);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*b2, *b2, *c2, *c2], 1, &repo),
|
quorum(&[*b2, *b2, *c2, *c2], 1, &repo),
|
||||||
Err(QuorumError::Diverging(_))
|
Err(QuorumError::Diverging { .. })
|
||||||
);
|
);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*b2, *b2, *c2, *c2, *a1], 1, &repo),
|
quorum(&[*b2, *b2, *c2, *c2, *a1], 1, &repo),
|
||||||
Err(QuorumError::Diverging(_))
|
Err(QuorumError::Diverging { .. })
|
||||||
);
|
);
|
||||||
|
|
||||||
// M2 M1
|
// M2 M1
|
||||||
|
|
@ -482,27 +458,27 @@ mod tests {
|
||||||
assert_eq!(quorum(&[*m1], 1, &repo).unwrap(), m1);
|
assert_eq!(quorum(&[*m1], 1, &repo).unwrap(), m1);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*m1, *m2], 1, &repo),
|
quorum(&[*m1, *m2], 1, &repo),
|
||||||
Err(QuorumError::Diverging(_))
|
Err(QuorumError::Diverging { .. })
|
||||||
);
|
);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*m2, *m1], 1, &repo),
|
quorum(&[*m2, *m1], 1, &repo),
|
||||||
Err(QuorumError::Diverging(_))
|
Err(QuorumError::Diverging { .. })
|
||||||
);
|
);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*m1, *m2], 2, &repo),
|
quorum(&[*m1, *m2], 2, &repo),
|
||||||
Err(QuorumError::NoCandidates(_))
|
Err(QuorumError::NoCandidates { .. })
|
||||||
);
|
);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*m1, *m2, *c2], 1, &repo),
|
quorum(&[*m1, *m2, *c2], 1, &repo),
|
||||||
Err(QuorumError::Diverging(_))
|
Err(QuorumError::Diverging { .. })
|
||||||
);
|
);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*m1, *a1], 1, &repo),
|
quorum(&[*m1, *a1], 1, &repo),
|
||||||
Err(QuorumError::Diverging(_))
|
Err(QuorumError::Diverging { .. })
|
||||||
);
|
);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*m1, *a1], 2, &repo),
|
quorum(&[*m1, *a1], 2, &repo),
|
||||||
Err(QuorumError::NoCandidates(_))
|
Err(QuorumError::NoCandidates { .. })
|
||||||
);
|
);
|
||||||
assert_eq!(quorum(&[*m1, *m2, *b2, *c1], 4, &repo).unwrap(), c1);
|
assert_eq!(quorum(&[*m1, *m2, *b2, *c1], 4, &repo).unwrap(), c1);
|
||||||
assert_eq!(quorum(&[*m1, *m1, *b2], 2, &repo).unwrap(), m1);
|
assert_eq!(quorum(&[*m1, *m1, *b2], 2, &repo).unwrap(), m1);
|
||||||
|
|
@ -541,11 +517,11 @@ mod tests {
|
||||||
// C0
|
// C0
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*m1, *m2], 1, &repo),
|
quorum(&[*m1, *m2], 1, &repo),
|
||||||
Err(QuorumError::Diverging(_))
|
Err(QuorumError::Diverging { .. })
|
||||||
);
|
);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*m1, *m2], 2, &repo),
|
quorum(&[*m1, *m2], 2, &repo),
|
||||||
Err(QuorumError::NoCandidates(_))
|
Err(QuorumError::NoCandidates { .. })
|
||||||
);
|
);
|
||||||
|
|
||||||
let m3 = fixtures::commit("M3", &[*c2, *c1], &repo);
|
let m3 = fixtures::commit("M3", &[*c2, *c1], &repo);
|
||||||
|
|
@ -557,27 +533,27 @@ mod tests {
|
||||||
// C0
|
// C0
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*m1, *m3], 1, &repo),
|
quorum(&[*m1, *m3], 1, &repo),
|
||||||
Err(QuorumError::Diverging(_))
|
Err(QuorumError::Diverging { .. })
|
||||||
);
|
);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*m1, *m3], 2, &repo),
|
quorum(&[*m1, *m3], 2, &repo),
|
||||||
Err(QuorumError::NoCandidates(_))
|
Err(QuorumError::NoCandidates { .. })
|
||||||
);
|
);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*m3, *m1], 1, &repo),
|
quorum(&[*m3, *m1], 1, &repo),
|
||||||
Err(QuorumError::Diverging(_))
|
Err(QuorumError::Diverging { .. })
|
||||||
);
|
);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*m3, *m1], 2, &repo),
|
quorum(&[*m3, *m1], 2, &repo),
|
||||||
Err(QuorumError::NoCandidates(_))
|
Err(QuorumError::NoCandidates { .. })
|
||||||
);
|
);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*m3, *m2], 1, &repo),
|
quorum(&[*m3, *m2], 1, &repo),
|
||||||
Err(QuorumError::Diverging(_))
|
Err(QuorumError::Diverging { .. })
|
||||||
);
|
);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*m3, *m2], 2, &repo),
|
quorum(&[*m3, *m2], 2, &repo),
|
||||||
Err(QuorumError::NoCandidates(_))
|
Err(QuorumError::NoCandidates { .. })
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -526,16 +526,6 @@ impl MatchedRule<'_> {
|
||||||
pub fn threshold(&self) -> &doc::Threshold {
|
pub fn threshold(&self) -> &doc::Threshold {
|
||||||
self.rule().threshold()
|
self.rule().threshold()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the [`Canonical`] representation for the matched rule.
|
|
||||||
pub fn canonical(&self, repo: &Repository) -> Result<Canonical, git::raw::Error> {
|
|
||||||
Canonical::reference(
|
|
||||||
repo,
|
|
||||||
&self.refname,
|
|
||||||
self.rule.allow.as_ref(),
|
|
||||||
self.rule.threshold.into(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A set of valid [`Rule`]s, where the set of DIDs and threshold are fully
|
/// A set of valid [`Rule`]s, where the set of DIDs and threshold are fully
|
||||||
|
|
@ -577,6 +567,12 @@ impl IntoIterator for Rules {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Extend<(Pattern, ValidRule)> for Rules {
|
||||||
|
fn extend<T: IntoIterator<Item = (Pattern, ValidRule)>>(&mut self, iter: T) {
|
||||||
|
self.rules.extend(iter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<Rules> for RawRules {
|
impl From<Rules> for RawRules {
|
||||||
fn from(Rules { rules }: Rules) -> Self {
|
fn from(Rules { rules }: Rules) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -615,18 +611,32 @@ impl Rules {
|
||||||
Ok(Self { rules: valid })
|
Ok(Self { rules: valid })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the first matching rule for the given `refname`, if there is any.
|
/// Return the matching rules for the given `refname`.
|
||||||
|
pub fn matches<'a>(
|
||||||
|
&self,
|
||||||
|
refname: &Qualified<'a>,
|
||||||
|
) -> impl Iterator<Item = (&Pattern, &ValidRule)> + use<'a, '_> {
|
||||||
|
let refname_cloned = refname.clone();
|
||||||
|
self.rules
|
||||||
|
.iter()
|
||||||
|
.filter(move |(pattern, _)| pattern.matches(&refname_cloned))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Match given refname, take the most specific rule, and prepare evaluation
|
||||||
|
/// as [`Canonical`]
|
||||||
///
|
///
|
||||||
/// N.b. it will find the first rule that is most specific for the given
|
/// N.b. it will find the first rule that is most specific for the given
|
||||||
/// `refname`.
|
/// `refname`.
|
||||||
pub fn matches<'a>(&self, refname: Qualified<'a>) -> Option<MatchedRule<'a>> {
|
pub fn canonical<'a, 'b>(
|
||||||
self.rules
|
&'a self,
|
||||||
.iter()
|
refname: Qualified<'b>,
|
||||||
.find(|(pattern, _)| pattern.matches(&refname))
|
repo: &Repository,
|
||||||
.map(|(_, rule)| MatchedRule {
|
) -> Result<Option<Canonical<'b, 'a>>, git::raw::Error> {
|
||||||
refname,
|
if let Some((_, rule)) = self.matches(&refname).next() {
|
||||||
rule: rule.clone(),
|
Ok(Some(Canonical::new(repo, refname, rule)?))
|
||||||
})
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1179,19 +1189,21 @@ mod tests {
|
||||||
// candidates tag.
|
// candidates tag.
|
||||||
let stored = storage.repository(rid).unwrap();
|
let stored = storage.repository(rid).unwrap();
|
||||||
let failing = git::Qualified::from(git::lit::refs_tags(failing_tag));
|
let failing = git::Qualified::from(git::lit::refs_tags(failing_tag));
|
||||||
for (refname, oid) in tags.iter() {
|
for (refname, oid) in tags.into_iter() {
|
||||||
let matched = rules.matches(refname.clone()).unwrap_or_else(|| {
|
let canonical = rules
|
||||||
|
.canonical(refname.clone(), &stored)
|
||||||
|
.unwrap()
|
||||||
|
.unwrap_or_else(|| {
|
||||||
panic!("there should be a matching rule for {refname}, rules: {rules:#?}")
|
panic!("there should be a matching rule for {refname}, rules: {rules:#?}")
|
||||||
});
|
});
|
||||||
let canonical = matched.canonical(&stored).unwrap();
|
if refname == failing {
|
||||||
if *refname == failing {
|
|
||||||
assert!(canonical.quorum(&repo).is_err());
|
assert!(canonical.quorum(&repo).is_err());
|
||||||
} else {
|
} else {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
canonical
|
canonical
|
||||||
.quorum(&repo)
|
.quorum(&repo)
|
||||||
.unwrap_or_else(|e| panic!("quorum error for {refname}: {e}")),
|
.unwrap_or_else(|e| panic!("quorum error for {refname}: {e}")),
|
||||||
*oid,
|
(refname, oid),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
#![warn(clippy::unwrap_used)]
|
#![warn(clippy::unwrap_used)]
|
||||||
|
pub mod crefs;
|
||||||
pub mod did;
|
pub mod did;
|
||||||
pub mod doc;
|
pub mod doc;
|
||||||
pub mod project;
|
pub mod project;
|
||||||
|
|
||||||
|
pub use crefs::CanonicalRefs;
|
||||||
pub use crypto::PublicKey;
|
pub use crypto::PublicKey;
|
||||||
pub use did::Did;
|
pub use did::Did;
|
||||||
pub use doc::{Doc, DocAt, DocError, IdError, PayloadError, RawDoc, RepoId, Visibility};
|
pub use doc::{Doc, DocAt, DocError, IdError, PayloadError, RawDoc, RepoId, Visibility};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::git::canonical::{
|
||||||
|
rules::{self, RawRules, Rules, ValidationError},
|
||||||
|
ValidRule,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::doc::{Delegates, Payload};
|
||||||
|
|
||||||
|
/// Implemented by any data type or store that can return [`CanonicalRefs`] and
|
||||||
|
/// [`RawCanonicalRefs`].
|
||||||
|
pub trait GetCanonicalRefs {
|
||||||
|
type Error: std::error::Error + Send + Sync + 'static;
|
||||||
|
|
||||||
|
/// Retrieve the [`CanonicalRefs`], returning `Some` if they are not
|
||||||
|
/// present, and `None` if they are missing.
|
||||||
|
///
|
||||||
|
/// [`Self::Error`] is used to return any domain-specific error by the
|
||||||
|
/// implementing type.
|
||||||
|
fn canonical_refs(&self) -> Result<Option<CanonicalRefs>, Self::Error>;
|
||||||
|
|
||||||
|
/// Retrieve the [`RawCanonicalRefs`], returning `Some` if they are not
|
||||||
|
/// present, and `None` if they are missing.
|
||||||
|
///
|
||||||
|
/// [`Self::Error`] is used to return any domain-specific error by the
|
||||||
|
/// implementing type.
|
||||||
|
fn raw_canonical_refs(&self) -> Result<Option<RawCanonicalRefs>, Self::Error>;
|
||||||
|
|
||||||
|
/// Retrieve the [`CanonicalRefs`], and in the case of `None`, then use the
|
||||||
|
/// `default` function to return a default set of [`CanonicalRefs`].
|
||||||
|
fn canonical_refs_or_default<D, E>(&self, default: D) -> Result<CanonicalRefs, E>
|
||||||
|
where
|
||||||
|
D: Fn() -> Result<CanonicalRefs, E>,
|
||||||
|
E: From<Self::Error>,
|
||||||
|
{
|
||||||
|
match self.canonical_refs()? {
|
||||||
|
Some(crefs) => Ok(crefs),
|
||||||
|
None => Ok(default()?),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configuration for canonical references and their rules.
|
||||||
|
///
|
||||||
|
/// `RawCanonicalRefs` are verified into [`CanonicalRefs`].
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct RawCanonicalRefs {
|
||||||
|
rules: RawRules,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RawCanonicalRefs {
|
||||||
|
/// Construct a new [`RawCanonicalRefs`] from a set of [`RawRules`].
|
||||||
|
pub fn new(rules: RawRules) -> Self {
|
||||||
|
Self { rules }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the [`RawRules`].
|
||||||
|
pub fn raw_rules(&self) -> &RawRules {
|
||||||
|
&self.rules
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate the [`RawCanonicalRefs`] into a set of [`CanonicalRefs`].
|
||||||
|
pub fn try_into_canonical_refs<R>(
|
||||||
|
self,
|
||||||
|
resolve: &mut R,
|
||||||
|
) -> Result<CanonicalRefs, ValidationError>
|
||||||
|
where
|
||||||
|
R: Fn() -> Delegates,
|
||||||
|
{
|
||||||
|
let rules = Rules::from_raw(self.rules, resolve)?;
|
||||||
|
Ok(CanonicalRefs { rules })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configuration for canonical references and their [`Rules`].
|
||||||
|
///
|
||||||
|
/// [`CanonicalRefs`] can be converted into a [`Payload`] using its [`From`]
|
||||||
|
/// implementation.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct CanonicalRefs {
|
||||||
|
rules: Rules,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CanonicalRefs {
|
||||||
|
/// Construct a new [`CanonicalRefs`] from a set of [`Rules`].
|
||||||
|
pub fn new(rules: Rules) -> Self {
|
||||||
|
CanonicalRefs { rules }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the [`Rules`].
|
||||||
|
pub fn rules(&self) -> &Rules {
|
||||||
|
&self.rules
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromIterator<(rules::Pattern, ValidRule)> for CanonicalRefs {
|
||||||
|
fn from_iter<T: IntoIterator<Item = (rules::Pattern, ValidRule)>>(iter: T) -> Self {
|
||||||
|
Self::new(Rules::from_iter(iter))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Extend<(rules::Pattern, ValidRule)> for CanonicalRefs {
|
||||||
|
fn extend<T: IntoIterator<Item = (rules::Pattern, ValidRule)>>(&mut self, iter: T) {
|
||||||
|
self.rules.extend(iter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum CanonicalRefsPayloadError {
|
||||||
|
#[error("could not convert canonical references to JSON: {0}")]
|
||||||
|
Json(#[source] serde_json::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<CanonicalRefs> for Payload {
|
||||||
|
type Error = CanonicalRefsPayloadError;
|
||||||
|
|
||||||
|
fn try_from(crefs: CanonicalRefs) -> Result<Self, Self::Error> {
|
||||||
|
let value = serde_json::to_value(crefs).map_err(CanonicalRefsPayloadError::Json)?;
|
||||||
|
Ok(Self::from(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -19,6 +19,7 @@ use crate::cob::identity;
|
||||||
use crate::crypto;
|
use crate::crypto;
|
||||||
use crate::crypto::Signature;
|
use crate::crypto::Signature;
|
||||||
use crate::git;
|
use crate::git;
|
||||||
|
use crate::git::canonical::rules;
|
||||||
use crate::identity::{project::Project, Did};
|
use crate::identity::{project::Project, Did};
|
||||||
use crate::node::device::Device;
|
use crate::node::device::Device;
|
||||||
use crate::storage;
|
use crate::storage;
|
||||||
|
|
@ -27,6 +28,9 @@ use crate::storage::{ReadRepository, RepositoryError};
|
||||||
pub use crypto::PublicKey;
|
pub use crypto::PublicKey;
|
||||||
pub use id::*;
|
pub use id::*;
|
||||||
|
|
||||||
|
use super::crefs::{self, RawCanonicalRefs};
|
||||||
|
use super::CanonicalRefs;
|
||||||
|
|
||||||
/// Path to the identity document in the identity branch.
|
/// Path to the identity document in the identity branch.
|
||||||
pub static PATH: LazyLock<&Path> = LazyLock::new(|| Path::new("radicle.json"));
|
pub static PATH: LazyLock<&Path> = LazyLock::new(|| Path::new("radicle.json"));
|
||||||
/// Maximum length of a string in the identity document.
|
/// Maximum length of a string in the identity document.
|
||||||
|
|
@ -73,6 +77,14 @@ impl DocError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum DefaultBranchRuleError {
|
||||||
|
#[error("could not create rule due to the reference name being invalid: {0}")]
|
||||||
|
Pattern(#[from] rules::PatternError),
|
||||||
|
#[error("could not load `xyz.radicle.project` to get default branch name: {0}")]
|
||||||
|
Payload(#[from] PayloadError),
|
||||||
|
}
|
||||||
|
|
||||||
/// The version number of the identity document.
|
/// The version number of the identity document.
|
||||||
///
|
///
|
||||||
/// It is used to ensure compatibility when parsing identity documents.
|
/// It is used to ensure compatibility when parsing identity documents.
|
||||||
|
|
@ -215,10 +227,20 @@ impl PayloadId {
|
||||||
.expect("PayloadId::project: type name is valid"),
|
.expect("PayloadId::project: type name is valid"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn canonical_refs() -> Self {
|
||||||
|
Self(
|
||||||
|
// SAFETY: We know this is valid.
|
||||||
|
TypeName::from_str("xyz.radicle.crefs")
|
||||||
|
.expect("PayloadId::canonical_refs: type name is valid"),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum PayloadError {
|
pub enum PayloadError {
|
||||||
|
#[error(transparent)]
|
||||||
|
CanonicalRefs(#[from] rules::ValidationError),
|
||||||
#[error("json: {0}")]
|
#[error("json: {0}")]
|
||||||
Json(#[from] serde_json::Error),
|
Json(#[from] serde_json::Error),
|
||||||
#[error("payload '{0}' not found in identity document")]
|
#[error("payload '{0}' not found in identity document")]
|
||||||
|
|
@ -536,6 +558,11 @@ impl Delegates {
|
||||||
self.0.contains(did)
|
self.0.contains(did)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if the `did` is the only delegate of the repository.
|
||||||
|
pub fn is_only(&self, did: &Did) -> bool {
|
||||||
|
self.0.tail.is_empty() && &self.0.head == did
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the number of delegates in the set.
|
/// Get the number of delegates in the set.
|
||||||
pub fn len(&self) -> usize {
|
pub fn len(&self) -> usize {
|
||||||
self.0.len()
|
self.0.len()
|
||||||
|
|
@ -715,6 +742,19 @@ impl Doc {
|
||||||
Ok(proj)
|
Ok(proj)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn default_branch_rule(
|
||||||
|
&self,
|
||||||
|
) -> Result<(rules::Pattern, rules::ValidRule), DefaultBranchRuleError> {
|
||||||
|
let proj = self.project()?;
|
||||||
|
let refname = proj.default_branch();
|
||||||
|
let pattern = rules::Pattern::try_from(git::refs::branch(refname).to_owned())?;
|
||||||
|
let rule = rules::Rule::new(
|
||||||
|
rules::ResolvedDelegates::Delegates(self.delegates.clone()),
|
||||||
|
self.threshold,
|
||||||
|
);
|
||||||
|
Ok((pattern, rule))
|
||||||
|
}
|
||||||
|
|
||||||
/// Return the associated [`Visibility`] of this document.
|
/// Return the associated [`Visibility`] of this document.
|
||||||
pub fn visibility(&self) -> &Visibility {
|
pub fn visibility(&self) -> &Visibility {
|
||||||
&self.visibility
|
&self.visibility
|
||||||
|
|
@ -872,6 +912,28 @@ impl Doc {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl crefs::GetCanonicalRefs for Doc {
|
||||||
|
type Error = PayloadError;
|
||||||
|
|
||||||
|
fn canonical_refs(&self) -> Result<Option<CanonicalRefs>, Self::Error> {
|
||||||
|
self.raw_canonical_refs().and_then(|raw| {
|
||||||
|
raw.map(|raw| {
|
||||||
|
raw.try_into_canonical_refs(&mut || self.delegates.clone())
|
||||||
|
.map_err(PayloadError::from)
|
||||||
|
})
|
||||||
|
.transpose()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw_canonical_refs(&self) -> Result<Option<RawCanonicalRefs>, Self::Error> {
|
||||||
|
let value = self.payload.get(&PayloadId::canonical_refs());
|
||||||
|
let crefs = value
|
||||||
|
.map(|value| serde_json::from_value((**value).clone()).map_err(PayloadError::from))
|
||||||
|
.transpose()?;
|
||||||
|
Ok(crefs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[allow(clippy::unwrap_used)]
|
#[allow(clippy::unwrap_used)]
|
||||||
mod test {
|
mod test {
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ use crate::cob;
|
||||||
use crate::collections::RandomMap;
|
use crate::collections::RandomMap;
|
||||||
use crate::git::{canonical, ext as git_ext};
|
use crate::git::{canonical, ext as git_ext};
|
||||||
use crate::git::{refspec::Refspec, PatternString, Qualified, RefError, RefStr, RefString};
|
use crate::git::{refspec::Refspec, PatternString, Qualified, RefError, RefStr, RefString};
|
||||||
use crate::identity::{Did, PayloadError};
|
use crate::identity::{doc, Did, PayloadError};
|
||||||
use crate::identity::{Doc, DocAt, DocError};
|
use crate::identity::{Doc, DocAt, DocError};
|
||||||
use crate::identity::{Identity, RepoId};
|
use crate::identity::{Identity, RepoId};
|
||||||
use crate::node::device::Device;
|
use crate::node::device::Device;
|
||||||
|
|
@ -120,6 +120,10 @@ pub enum RepositoryError {
|
||||||
Quorum(#[from] canonical::QuorumError),
|
Quorum(#[from] canonical::QuorumError),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Refs(#[from] refs::Error),
|
Refs(#[from] refs::Error),
|
||||||
|
#[error("missing canonical reference rule for default branch")]
|
||||||
|
MissingBranchRule,
|
||||||
|
#[error("could not get the default branch rule: {0}")]
|
||||||
|
DefaultBranchRule(#[from] doc::DefaultBranchRuleError),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RepositoryError {
|
impl RepositoryError {
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,9 @@ use std::{fs, io};
|
||||||
use crypto::Verified;
|
use crypto::Verified;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|
||||||
use crate::git::canonical::Canonical;
|
use crate::identity::crefs::GetCanonicalRefs as _;
|
||||||
use crate::identity::doc::DocError;
|
use crate::identity::doc::DocError;
|
||||||
use crate::identity::{Doc, DocAt, RepoId};
|
use crate::identity::{CanonicalRefs, Doc, DocAt, RepoId};
|
||||||
use crate::identity::{Identity, Project};
|
use crate::identity::{Identity, Project};
|
||||||
use crate::node::device::Device;
|
use crate::node::device::Device;
|
||||||
use crate::node::SyncedAt;
|
use crate::node::SyncedAt;
|
||||||
|
|
@ -749,13 +749,18 @@ impl ReadRepository for Repository {
|
||||||
|
|
||||||
fn canonical_head(&self) -> Result<(Qualified, Oid), RepositoryError> {
|
fn canonical_head(&self) -> Result<(Qualified, Oid), RepositoryError> {
|
||||||
let doc = self.identity_doc()?;
|
let doc = self.identity_doc()?;
|
||||||
let project = doc.project()?;
|
let refname = git::refs::branch(doc.project()?.default_branch());
|
||||||
let branch_ref = git::refs::branch(project.default_branch());
|
let crefs = match doc.canonical_refs()? {
|
||||||
let raw = self.raw();
|
Some(crefs) => crefs,
|
||||||
let oid =
|
// Fallback to constructing the default branch via the project
|
||||||
Canonical::default_branch(self, &project, doc.delegates().into(), doc.threshold())?
|
// payload
|
||||||
.quorum(raw)?;
|
None => CanonicalRefs::from_iter([doc.default_branch_rule()?]),
|
||||||
Ok((branch_ref, oid))
|
};
|
||||||
|
Ok(crefs
|
||||||
|
.rules()
|
||||||
|
.canonical(refname, self)?
|
||||||
|
.ok_or(RepositoryError::MissingBranchRule)?
|
||||||
|
.quorum(self.raw())?)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn identity_head(&self) -> Result<Oid, RepositoryError> {
|
fn identity_head(&self) -> Result<Oid, RepositoryError> {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue