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)
|
||||
$ git push rad master -f
|
||||
✓ Canonical head updated to 9170c8795d3a78f0381a0ffafb20ea69fb0f5b6b
|
||||
✓ Canonical reference refs/heads/master updated to 9170c8795d3a78f0381a0ffafb20ea69fb0f5b6b
|
||||
✓ Synced with 1 seed(s)
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
+ fb25886...9170c87 master -> master (forced update)
|
||||
|
|
|
|||
|
|
@ -91,8 +91,7 @@ commit:
|
|||
|
||||
``` ~alice (stderr)
|
||||
$ git push rad -f
|
||||
warn: could not determine canonical tip for `refs/heads/master`
|
||||
warn: no commit found with at least 3 vote(s) (threshold not met)
|
||||
warn: could not determine commit for canonical reference 'refs/heads/master', no commit with at least 3 vote(s) found (threshold not met)
|
||||
warn: it is recommended to find a commit to agree upon
|
||||
✓ Synced with 2 seed(s)
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
|
|
@ -117,7 +116,7 @@ become the canonical `master`.
|
|||
|
||||
``` ~bob (stderr)
|
||||
$ git push rad
|
||||
✓ Canonical head updated to 3a75f66dd0020c9a0355cc6ec21f15de989e2001
|
||||
✓ Canonical reference refs/heads/master updated to 3a75f66dd0020c9a0355cc6ec21f15de989e2001
|
||||
✓ Synced with 2 seed(s)
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
|
||||
2a37862..0f9bd80 master -> master
|
||||
|
|
@ -138,7 +137,7 @@ HEAD is now at 0f9bd80 Merge remote-tracking branch 'eve/master'
|
|||
|
||||
``` ~eve (stderr)
|
||||
$ git push rad
|
||||
✓ Canonical head updated to 0f9bd8035c04b3f73f5408e73e8454879b20800b
|
||||
✓ Canonical reference refs/heads/master updated to 0f9bd8035c04b3f73f5408e73e8454879b20800b
|
||||
✓ Synced with 2 seed(s)
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z
|
||||
3a75f66..0f9bd80 master -> master
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ integrate Bob's changes before pushing ours:
|
|||
|
||||
``` ~alice (stderr) (fail) RAD_HINT=1
|
||||
$ 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
|
||||
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'
|
||||
|
|
@ -62,7 +62,7 @@ f2de534 Second commit
|
|||
```
|
||||
``` ~alice RAD_SOCKET=/dev/null (stderr)
|
||||
$ git push rad
|
||||
✓ Canonical head updated to f6cff86594495e9beccfeda7c20173e55c1dd9fc
|
||||
✓ Canonical reference refs/heads/master updated to f6cff86594495e9beccfeda7c20173e55c1dd9fc
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
f2de534..f6cff86 master -> master
|
||||
```
|
||||
|
|
@ -75,7 +75,7 @@ $ git reset --hard HEAD^ -q
|
|||
```
|
||||
``` ~alice RAD_SOCKET=/dev/null (stderr)
|
||||
$ git push -f
|
||||
✓ Canonical head updated to 319a7dc3b195368ded4b099f8c90bbb80addccd3
|
||||
✓ Canonical reference refs/heads/master updated to 319a7dc3b195368ded4b099f8c90bbb80addccd3
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
+ f6cff86...319a7dc master -> master (forced update)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ Fast-forward
|
|||
|
||||
``` ~alice (stderr)
|
||||
$ git push rad
|
||||
✓ Canonical head updated to 319a7dc3b195368ded4b099f8c90bbb80addccd3
|
||||
✓ Canonical reference refs/heads/master updated to 319a7dc3b195368ded4b099f8c90bbb80addccd3
|
||||
✓ Synced with 1 seed(s)
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
f2de534..319a7dc master -> master
|
||||
|
|
@ -54,7 +54,7 @@ push and the new canonical head becomes the previous commit again:
|
|||
|
||||
``` ~alice (stderr)
|
||||
$ git push rad -f
|
||||
✓ Canonical head updated to f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
|
||||
✓ Canonical reference refs/heads/master updated to f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
|
||||
✓ Synced with 1 seed(s)
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
+ 319a7dc...f2de534 master -> master (forced update)
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ $ git commit -v -m "Define power requirements"
|
|||
|
||||
``` ~alice (stderr) RAD_SOCKET=/dev/null
|
||||
$ git push rad master
|
||||
✓ Canonical head updated to 3e674d1a1df90807e934f9ae5da2591dd6848a33
|
||||
✓ Canonical reference refs/heads/master updated to f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
f2de534..3e674d1 master -> master
|
||||
```
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ $ git commit --amend --allow-empty -q -m "Amended change"
|
|||
$ git checkout master -q
|
||||
$ git merge feature/1 -q
|
||||
$ git push rad master
|
||||
✓ Canonical head updated to 954bcdb5008447ce294a61a21d7eb87afbe7f4a6
|
||||
✓ Canonical reference refs/heads/master updated to 954bcdb5008447ce294a61a21d7eb87afbe7f4a6
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
f2de534..954bcdb master -> master
|
||||
```
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ Finally, we push master and expect the patch to be merged.
|
|||
``` (stderr) RAD_SOCKET=/dev/null
|
||||
$ git push rad master
|
||||
✓ Patch 696ec5508494692899337afe6713fe1796d0315c merged
|
||||
✓ Canonical head updated to 737a10cfa29111afeb0d43cf3545cee386b939ec
|
||||
✓ Canonical reference refs/heads/master updated to 737a10cfa29111afeb0d43cf3545cee386b939ec
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
f2de534..737a10c master -> master
|
||||
```
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ When we push to `rad/master`, we automatically merge the patches:
|
|||
$ git push rad master
|
||||
✓ Patch 356f73863a8920455ff6e77cd9c805d68910551b merged
|
||||
✓ Patch 696ec5508494692899337afe6713fe1796d0315c merged
|
||||
✓ Canonical head updated to d6399c71702b40bae00825b3c444478d06b4e91c
|
||||
✓ Canonical reference refs/heads/master updated to d6399c71702b40bae00825b3c444478d06b4e91c
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
f2de534..d6399c7 master -> master
|
||||
```
|
||||
|
|
@ -148,7 +148,7 @@ the first patch, even though they were pushed together.
|
|||
$ git reset --hard HEAD^
|
||||
$ git push -f rad
|
||||
! Patch 356f73863a8920455ff6e77cd9c805d68910551b reverted at revision 356f738
|
||||
✓ Canonical head updated to 20aa5dde6210796c3a2f04079b42316a31d02689
|
||||
✓ Canonical reference refs/heads/master updated to 20aa5dde6210796c3a2f04079b42316a31d02689
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
+ d6399c7...20aa5dd master -> master (forced update)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ $ git checkout master -q
|
|||
$ git merge feature/1
|
||||
$ git push rad master
|
||||
✓ Patch 8dfb4dcafc4346158c8160410dd3f2b0616ad4fe merged
|
||||
✓ Canonical head updated to 20aa5dde6210796c3a2f04079b42316a31d02689
|
||||
✓ Canonical reference refs/heads/master updated to 20aa5dde6210796c3a2f04079b42316a31d02689
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
f2de534..20aa5dd master -> master
|
||||
```
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ $ git checkout master -q
|
|||
$ git merge changes -q
|
||||
$ git push rad master
|
||||
✓ Patch acab0ec777a97d013f30be5d5d1aec32562ecb02 merged
|
||||
✓ Canonical head updated to b2b6432af93f8fe188e32d400263021b602cfec8
|
||||
✓ Canonical reference refs/heads/master updated to b2b6432af93f8fe188e32d400263021b602cfec8
|
||||
✓ Synced with 1 seed(s)
|
||||
|
||||
https://app.radicle.xyz/nodes/[..]/rad:z3yXbb1sR6UG6ixxV2YF9jUP7ABra/tree/b2b6432af93f8fe188e32d400263021b602cfec8
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ Switched to branch 'master'
|
|||
$ git merge feature/1
|
||||
$ git push rad master
|
||||
✓ Patch 696ec5508494692899337afe6713fe1796d0315c merged
|
||||
✓ Canonical head updated to 20aa5dde6210796c3a2f04079b42316a31d02689
|
||||
✓ Canonical reference refs/heads/master updated to 20aa5dde6210796c3a2f04079b42316a31d02689
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
f2de534..20aa5dd master -> master
|
||||
```
|
||||
|
|
@ -50,7 +50,7 @@ When pushing, notice that we're told our patch is reverted.
|
|||
``` (stderr) RAD_SOCKET=/dev/null
|
||||
$ git push rad master --force
|
||||
! Patch 696ec5508494692899337afe6713fe1796d0315c reverted at revision 696ec55
|
||||
✓ Canonical head updated to f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
|
||||
✓ Canonical reference refs/heads/master updated to f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
+ 20aa5dd...f2de534 master -> master (forced update)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ Fast-forward
|
|||
``` (stderr)
|
||||
$ git push rad master
|
||||
✓ Patch e4934b6d9dbe01ce3c7fbb5b77a80d5f1dacdc46 merged at revision 9d62420
|
||||
✓ Canonical head updated to f567f695d25b4e8fb63b5f5ad2a584529826e908
|
||||
✓ Canonical reference refs/heads/master updated to f567f695d25b4e8fb63b5f5ad2a584529826e908
|
||||
✓ Synced with 1 seed(s)
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
f2de534..f567f69 master -> master
|
||||
|
|
|
|||
|
|
@ -7,15 +7,18 @@ use localtime::LocalTime;
|
|||
|
||||
use radicle::cob::TypedId;
|
||||
use radicle::crypto::PublicKey;
|
||||
use radicle::identity::crefs::GetCanonicalRefs as _;
|
||||
use radicle::identity::DocAt;
|
||||
use radicle::prelude::NodeId;
|
||||
use radicle::prelude::RepoId;
|
||||
use radicle::storage::git::Repository;
|
||||
use radicle::storage::refs::RefsAt;
|
||||
use radicle::storage::{
|
||||
ReadRepository, ReadStorage as _, RefUpdate, RemoteRepository, RepositoryError,
|
||||
WriteRepository as _,
|
||||
};
|
||||
use radicle::{cob, git, node, Storage};
|
||||
use radicle_fetch::git::refs::Applied;
|
||||
use radicle_fetch::{Allowed, BlockList, FetchLimit};
|
||||
|
||||
use super::channels::ChannelsFlush;
|
||||
|
|
@ -89,8 +92,6 @@ impl Handle {
|
|||
remote: PublicKey,
|
||||
refs_at: Option<Vec<RefsAt>>,
|
||||
) -> Result<FetchResult, error::Fetch> {
|
||||
use git::canonical::QuorumError::{Diverging, NoCandidates};
|
||||
|
||||
let (result, clone, notifs) = match self {
|
||||
Self::Clone { mut handle, tmp } => {
|
||||
log::debug!(target: "worker", "{} cloning from {remote}", handle.local());
|
||||
|
|
@ -145,15 +146,19 @@ impl Handle {
|
|||
log::trace!(target: "worker", "Set HEAD to {}", head.new);
|
||||
}
|
||||
}
|
||||
Err(RepositoryError::Quorum(Diverging(e))) => {
|
||||
log::warn!(target: "worker", "Fetch could not set HEAD: {e}")
|
||||
Err(RepositoryError::Quorum(radicle::git::canonical::QuorumError::Git(e))) => {
|
||||
return Err(e.into())
|
||||
}
|
||||
Err(RepositoryError::Quorum(NoCandidates(e))) => {
|
||||
Err(RepositoryError::Quorum(e)) => {
|
||||
log::warn!(target: "worker", "Fetch could not set HEAD: {e}")
|
||||
}
|
||||
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.
|
||||
if let Some(mut store) = notifs {
|
||||
// Only create notifications for repos that we have
|
||||
|
|
@ -377,3 +382,71 @@ where
|
|||
|
||||
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)]
|
||||
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::{assert_eq, io};
|
||||
|
||||
use radicle::identity::crefs::GetCanonicalRefs as _;
|
||||
use radicle::identity::doc::DefaultBranchRuleError;
|
||||
use radicle::node::device::Device;
|
||||
use thiserror::Error;
|
||||
|
||||
|
|
@ -15,8 +17,7 @@ use radicle::cob::patch::cache::Patches as _;
|
|||
use radicle::crypto;
|
||||
use radicle::explorer::ExplorerResource;
|
||||
use radicle::git::canonical;
|
||||
use radicle::git::canonical::Canonical;
|
||||
use radicle::identity::Did;
|
||||
use radicle::identity::{CanonicalRefs, Did};
|
||||
use radicle::node;
|
||||
use radicle::node::{Handle, NodeId};
|
||||
use radicle::storage;
|
||||
|
|
@ -116,6 +117,8 @@ pub enum Error {
|
|||
/// Quorum error.
|
||||
#[error(transparent)]
|
||||
Quorum(#[from] radicle::git::canonical::QuorumError),
|
||||
#[error(transparent)]
|
||||
DefaultBranchRule(#[from] radicle::identity::doc::DefaultBranchRuleError),
|
||||
}
|
||||
|
||||
/// Push command.
|
||||
|
|
@ -203,6 +206,10 @@ pub fn run(
|
|||
}
|
||||
}
|
||||
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 spec in specs {
|
||||
|
|
@ -262,8 +269,11 @@ pub fn run(
|
|||
)
|
||||
} else {
|
||||
let identity = stored.identity()?;
|
||||
let project = identity.project()?;
|
||||
let canonical_ref = git::refs::branch(project.default_branch());
|
||||
let crefs = identity.canonical_refs_or_default(|| {
|
||||
let rule = identity.doc().default_branch_rule()?;
|
||||
Ok::<_, DefaultBranchRuleError>(CanonicalRefs::from_iter([rule]))
|
||||
})?;
|
||||
let rules = crefs.rules();
|
||||
let me = Did::from(nid);
|
||||
|
||||
// If we're trying to update the canonical head, make sure
|
||||
|
|
@ -272,66 +282,51 @@ pub fn run(
|
|||
//
|
||||
// Note that we *do* allow rolling back to a previous commit on the
|
||||
// canonical branch.
|
||||
if dst == canonical_ref && delegates.contains(&me) && delegates.len() > 1 {
|
||||
let head = working.find_reference(src.as_str())?;
|
||||
let head = head.peel_to_commit()?.id();
|
||||
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 = head.peel_to_commit()?.id();
|
||||
let converges =
|
||||
canonical.converges(&working, (&me, &head.into()))?;
|
||||
|
||||
let mut canonical = Canonical::default_branch(
|
||||
stored,
|
||||
&project,
|
||||
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());
|
||||
}
|
||||
// If `canonical` is empty then we're creating a new reference.
|
||||
// If we're the only delegate then we need to modify our vote.
|
||||
if converges || canonical.has_no_tips() || canonical.is_only(&me) {
|
||||
canonical.modify_vote(me, head.into());
|
||||
}
|
||||
|
||||
match canonical.quorum(&working) {
|
||||
Ok(canonical_oid) => {
|
||||
// Canonical head is an ancestor of head.
|
||||
let is_ff = head == *canonical_oid
|
||||
|| working.graph_descendant_of(head, *canonical_oid)?;
|
||||
match canonical.quorum(&working) {
|
||||
Ok((dst, canonical_oid)) => {
|
||||
// Canonical head is an ancestor of head.
|
||||
let is_ff = head == *canonical_oid
|
||||
|| working.graph_descendant_of(head, *canonical_oid)?;
|
||||
|
||||
if !is_ff && !converges {
|
||||
if hints {
|
||||
hint(
|
||||
"you are attempting to push a commit that would cause \
|
||||
your upstream to diverge from the canonical head",
|
||||
);
|
||||
hint(
|
||||
"to integrate the remote changes, run `git pull --rebase` \
|
||||
and try again",
|
||||
);
|
||||
if !is_ff && !converges {
|
||||
if hints {
|
||||
hint(
|
||||
format!("you are attempting to push a commit that would cause \
|
||||
your upstream to diverge from the canonical reference {dst}"),
|
||||
);
|
||||
hint(
|
||||
"to integrate the remote changes, run `git pull --rebase` \
|
||||
and try again",
|
||||
);
|
||||
}
|
||||
return Err(Error::HeadsDiverge(
|
||||
head.into(),
|
||||
canonical_oid,
|
||||
));
|
||||
}
|
||||
return Err(Error::HeadsDiverge(
|
||||
head.into(),
|
||||
canonical_oid,
|
||||
));
|
||||
set_canonical_refs
|
||||
.push((dst.clone().to_owned(), canonical_oid));
|
||||
}
|
||||
Err(canonical::QuorumError::Git(e)) => return Err(e.into()),
|
||||
Err(e) => {
|
||||
warn(e.to_string());
|
||||
warn("it is recommended to find a commit to agree upon");
|
||||
}
|
||||
}
|
||||
Err(canonical::QuorumError::Diverging(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(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)
|
||||
}
|
||||
|
|
@ -354,16 +349,50 @@ pub fn run(
|
|||
if !ok.is_empty() {
|
||||
let _ = stored.sign_refs(&signer)?;
|
||||
|
||||
// N.b. if an error occurs then there may be no quorum
|
||||
if let Ok(head) = stored.set_head() {
|
||||
if head.is_updated() {
|
||||
for (refname, oid) in &set_canonical_refs {
|
||||
let print_update = || {
|
||||
eprintln!(
|
||||
"{} Canonical head updated to {}",
|
||||
"{} Canonical reference {} updated to {}",
|
||||
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 profile.policies()?.is_seeding(&stored.id)? {
|
||||
|
|
|
|||
|
|
@ -2,126 +2,64 @@ pub mod rules;
|
|||
pub use rules::{MatchedRule, RawRule, Rules, ValidRule};
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
|
||||
use nonempty::NonEmpty;
|
||||
use raw::Repository;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::prelude::Did;
|
||||
use crate::prelude::Project;
|
||||
use crate::storage::ReadRepository;
|
||||
|
||||
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
|
||||
/// reference for that [`Did`].
|
||||
///
|
||||
/// The general construction of `Canonical` is by using the
|
||||
/// [`Canonical::reference`] constructor. For the default branch of a
|
||||
/// [`Project`], use [`Canonical::default_branch`].
|
||||
/// The general construction of `Canonical` is by using the [`Canonical::new`]
|
||||
/// constructor.
|
||||
///
|
||||
/// `Canonical` can then be used for performing calculations about the
|
||||
/// 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)]
|
||||
pub struct Canonical {
|
||||
pub struct Canonical<'a, 'b> {
|
||||
refname: Qualified<'a>,
|
||||
rule: &'b ValidRule,
|
||||
tips: BTreeMap<Did, Oid>,
|
||||
threshold: usize,
|
||||
}
|
||||
|
||||
/// Error that can occur when calculation the [`Canonical::quorum`].
|
||||
#[derive(Debug, Error)]
|
||||
pub enum QuorumError {
|
||||
/// Could not determine a quorum [`Oid`], due to diverging tips.
|
||||
#[error("could not determine canonical reference tip, {0}")]
|
||||
Diverging(Diverging),
|
||||
#[error("could not determine commit for canonical reference '{refname}', found diverging commits {longest} and {head}, with base commit {base} and threshold {threshold}")]
|
||||
Diverging {
|
||||
refname: String,
|
||||
threshold: usize,
|
||||
base: Oid,
|
||||
longest: Oid,
|
||||
head: Oid,
|
||||
},
|
||||
/// Could not determine a base candidate from the given set of delegates.
|
||||
#[error("could not determine canonical reference tip, {0}")]
|
||||
NoCandidates(NoCandidates),
|
||||
#[error("could not determine commit for canonical reference '{refname}', no commit with at least {threshold} vote(s) found (threshold not met)")]
|
||||
NoCandidates { refname: String, threshold: usize },
|
||||
/// An error occurred from [`git2`].
|
||||
#[error(transparent)]
|
||||
Git(#[from] git2::Error),
|
||||
}
|
||||
|
||||
/// No candidates were found for the [`Canonical::quorum`] calculation.
|
||||
///
|
||||
/// The [`fmt::Display`] is used in [`QuorumError`], to provide information on
|
||||
/// the threshold and delegates in the calculation.
|
||||
#[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>
|
||||
impl<'a, 'b> Canonical<'a, 'b> {
|
||||
/// Construct the set of canonical tips given for the given `rule` and
|
||||
/// the reference `refname`.
|
||||
pub fn new<S>(repo: &S, refname: Qualified<'a>, rule: &'b ValidRule) -> Result<Self, raw::Error>
|
||||
where
|
||||
S: ReadRepository,
|
||||
{
|
||||
let mut tips = BTreeMap::new();
|
||||
for delegate in delegates.iter() {
|
||||
match repo.reference_oid(delegate, refname) {
|
||||
for delegate in rule.allowed().iter() {
|
||||
match repo.reference_oid(delegate, &refname) {
|
||||
Ok(tip) => {
|
||||
tips.insert(*delegate, tip);
|
||||
}
|
||||
|
|
@ -135,7 +73,11 @@ impl Canonical {
|
|||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
Ok(Canonical { tips, threshold })
|
||||
Ok(Canonical {
|
||||
refname,
|
||||
tips,
|
||||
rule,
|
||||
})
|
||||
}
|
||||
|
||||
/// Return the set of [`Did`]s and their [`Oid`] tip.
|
||||
|
|
@ -143,36 +85,18 @@ impl Canonical {
|
|||
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.
|
||||
///
|
||||
/// 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()
|
||||
}
|
||||
}
|
||||
|
||||
/// Check that a given `target` converges with any of the provided `tips`.
|
||||
///
|
||||
/// 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),
|
||||
(_, _) => {}
|
||||
}
|
||||
pub fn refname(&self) -> &Qualified {
|
||||
&self.refname
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
impl Canonical {
|
||||
/// 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
|
||||
/// reach a quorum.
|
||||
|
|
@ -180,6 +104,41 @@ impl Canonical {
|
|||
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`,
|
||||
/// 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
|
||||
|
|
@ -187,7 +146,7 @@ impl Canonical {
|
|||
///
|
||||
/// Also returns an error if `heads` is empty or `threshold` cannot be
|
||||
/// 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();
|
||||
|
||||
// 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.
|
||||
candidates.retain(|_, votes| *votes >= self.threshold);
|
||||
candidates.retain(|_, votes| *votes >= self.threshold());
|
||||
|
||||
let (mut longest, _) =
|
||||
candidates
|
||||
.pop_first()
|
||||
.ok_or(QuorumError::NoCandidates(NoCandidates {
|
||||
threshold: self.threshold,
|
||||
}))?;
|
||||
let (mut longest, _) = candidates.pop_first().ok_or(QuorumError::NoCandidates {
|
||||
refname: self.refname.to_string(),
|
||||
threshold: self.threshold(),
|
||||
})?;
|
||||
|
||||
// Now that all scores are calculated, figure out what is the longest branch
|
||||
// that passes the threshold. In case of divergence, return an error.
|
||||
|
|
@ -253,15 +210,20 @@ impl Canonical {
|
|||
// o (base)
|
||||
// |
|
||||
//
|
||||
return Err(QuorumError::Diverging(Diverging {
|
||||
threshold: self.threshold,
|
||||
return Err(QuorumError::Diverging {
|
||||
refname: self.refname.to_string(),
|
||||
threshold: self.threshold(),
|
||||
base: base.into(),
|
||||
longest,
|
||||
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,
|
||||
repo: &git::raw::Repository,
|
||||
) -> Result<Oid, QuorumError> {
|
||||
let tips = heads
|
||||
let tips: BTreeMap<Did, Oid> = heads
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, head)| {
|
||||
|
|
@ -290,7 +252,24 @@ mod tests {
|
|||
(did, (*head).into())
|
||||
})
|
||||
.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]
|
||||
|
|
@ -352,9 +331,6 @@ mod tests {
|
|||
assert_eq!(quorum(&[*c0], 1, &repo).unwrap(), c0);
|
||||
assert_eq!(quorum(&[*c1], 1, &repo).unwrap(), c1);
|
||||
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
|
||||
// |
|
||||
|
|
@ -380,23 +356,23 @@ mod tests {
|
|||
// C0
|
||||
assert_matches!(
|
||||
quorum(&[*c1, *c2, *b2], 1, &repo),
|
||||
Err(QuorumError::Diverging(_))
|
||||
Err(QuorumError::Diverging { .. })
|
||||
);
|
||||
assert_matches!(
|
||||
quorum(&[*c2, *b2], 1, &repo),
|
||||
Err(QuorumError::Diverging(_))
|
||||
Err(QuorumError::Diverging { .. })
|
||||
);
|
||||
assert_matches!(
|
||||
quorum(&[*b2, *c2], 1, &repo),
|
||||
Err(QuorumError::Diverging(_))
|
||||
Err(QuorumError::Diverging { .. })
|
||||
);
|
||||
assert_matches!(
|
||||
quorum(&[*c2, *b2], 2, &repo),
|
||||
Err(QuorumError::NoCandidates(_))
|
||||
Err(QuorumError::NoCandidates { .. })
|
||||
);
|
||||
assert_matches!(
|
||||
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], 3, &repo).unwrap(), c1);
|
||||
|
|
@ -404,7 +380,7 @@ mod tests {
|
|||
assert_eq!(quorum(&[*b2, *c2, *c2], 2, &repo).unwrap(), c2);
|
||||
assert_matches!(
|
||||
quorum(&[*b2, *b2, *c2, *c2], 2, &repo),
|
||||
Err(QuorumError::Diverging(_))
|
||||
Err(QuorumError::Diverging { .. })
|
||||
);
|
||||
|
||||
// B2 C2 C3
|
||||
|
|
@ -415,15 +391,15 @@ mod tests {
|
|||
assert_eq!(quorum(&[*b2, *c2, *c2], 2, &repo).unwrap(), c2);
|
||||
assert_matches!(
|
||||
quorum(&[*b2, *c2, *c2], 3, &repo),
|
||||
Err(QuorumError::NoCandidates(_))
|
||||
Err(QuorumError::NoCandidates { .. })
|
||||
);
|
||||
assert_matches!(
|
||||
quorum(&[*b2, *c2, *b2, *c2], 3, &repo),
|
||||
Err(QuorumError::NoCandidates(_))
|
||||
Err(QuorumError::NoCandidates { .. })
|
||||
);
|
||||
assert_matches!(
|
||||
quorum(&[*c3, *b2, *c2, *b2, *c2, *c3], 3, &repo),
|
||||
Err(QuorumError::NoCandidates(_))
|
||||
Err(QuorumError::NoCandidates { .. })
|
||||
);
|
||||
|
||||
// B2 C2
|
||||
|
|
@ -433,19 +409,19 @@ mod tests {
|
|||
// C0
|
||||
assert_matches!(
|
||||
quorum(&[*c2, *b2, *a1], 1, &repo),
|
||||
Err(QuorumError::Diverging(_))
|
||||
Err(QuorumError::Diverging { .. })
|
||||
);
|
||||
assert_matches!(
|
||||
quorum(&[*c2, *b2, *a1], 2, &repo),
|
||||
Err(QuorumError::NoCandidates(_))
|
||||
Err(QuorumError::NoCandidates { .. })
|
||||
);
|
||||
assert_matches!(
|
||||
quorum(&[*c2, *b2, *a1], 3, &repo),
|
||||
Err(QuorumError::NoCandidates(_))
|
||||
Err(QuorumError::NoCandidates { .. })
|
||||
);
|
||||
assert_matches!(
|
||||
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], 3, &repo).unwrap(), c1,);
|
||||
|
|
@ -453,23 +429,23 @@ mod tests {
|
|||
assert_eq!(quorum(&[*c0, *c1, *c2, *b2, *a1], 4, &repo).unwrap(), c0,);
|
||||
assert_matches!(
|
||||
quorum(&[*a1, *a1, *c2, *c2, *c1], 2, &repo),
|
||||
Err(QuorumError::Diverging(_))
|
||||
Err(QuorumError::Diverging { .. })
|
||||
);
|
||||
assert_matches!(
|
||||
quorum(&[*a1, *a1, *c2, *c2, *c1], 1, &repo),
|
||||
Err(QuorumError::Diverging(_))
|
||||
Err(QuorumError::Diverging { .. })
|
||||
);
|
||||
assert_matches!(
|
||||
quorum(&[*a1, *a1, *c2], 1, &repo),
|
||||
Err(QuorumError::Diverging(_))
|
||||
Err(QuorumError::Diverging { .. })
|
||||
);
|
||||
assert_matches!(
|
||||
quorum(&[*b2, *b2, *c2, *c2], 1, &repo),
|
||||
Err(QuorumError::Diverging(_))
|
||||
Err(QuorumError::Diverging { .. })
|
||||
);
|
||||
assert_matches!(
|
||||
quorum(&[*b2, *b2, *c2, *c2, *a1], 1, &repo),
|
||||
Err(QuorumError::Diverging(_))
|
||||
Err(QuorumError::Diverging { .. })
|
||||
);
|
||||
|
||||
// M2 M1
|
||||
|
|
@ -482,27 +458,27 @@ mod tests {
|
|||
assert_eq!(quorum(&[*m1], 1, &repo).unwrap(), m1);
|
||||
assert_matches!(
|
||||
quorum(&[*m1, *m2], 1, &repo),
|
||||
Err(QuorumError::Diverging(_))
|
||||
Err(QuorumError::Diverging { .. })
|
||||
);
|
||||
assert_matches!(
|
||||
quorum(&[*m2, *m1], 1, &repo),
|
||||
Err(QuorumError::Diverging(_))
|
||||
Err(QuorumError::Diverging { .. })
|
||||
);
|
||||
assert_matches!(
|
||||
quorum(&[*m1, *m2], 2, &repo),
|
||||
Err(QuorumError::NoCandidates(_))
|
||||
Err(QuorumError::NoCandidates { .. })
|
||||
);
|
||||
assert_matches!(
|
||||
quorum(&[*m1, *m2, *c2], 1, &repo),
|
||||
Err(QuorumError::Diverging(_))
|
||||
Err(QuorumError::Diverging { .. })
|
||||
);
|
||||
assert_matches!(
|
||||
quorum(&[*m1, *a1], 1, &repo),
|
||||
Err(QuorumError::Diverging(_))
|
||||
Err(QuorumError::Diverging { .. })
|
||||
);
|
||||
assert_matches!(
|
||||
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, *m1, *b2], 2, &repo).unwrap(), m1);
|
||||
|
|
@ -541,11 +517,11 @@ mod tests {
|
|||
// C0
|
||||
assert_matches!(
|
||||
quorum(&[*m1, *m2], 1, &repo),
|
||||
Err(QuorumError::Diverging(_))
|
||||
Err(QuorumError::Diverging { .. })
|
||||
);
|
||||
assert_matches!(
|
||||
quorum(&[*m1, *m2], 2, &repo),
|
||||
Err(QuorumError::NoCandidates(_))
|
||||
Err(QuorumError::NoCandidates { .. })
|
||||
);
|
||||
|
||||
let m3 = fixtures::commit("M3", &[*c2, *c1], &repo);
|
||||
|
|
@ -557,27 +533,27 @@ mod tests {
|
|||
// C0
|
||||
assert_matches!(
|
||||
quorum(&[*m1, *m3], 1, &repo),
|
||||
Err(QuorumError::Diverging(_))
|
||||
Err(QuorumError::Diverging { .. })
|
||||
);
|
||||
assert_matches!(
|
||||
quorum(&[*m1, *m3], 2, &repo),
|
||||
Err(QuorumError::NoCandidates(_))
|
||||
Err(QuorumError::NoCandidates { .. })
|
||||
);
|
||||
assert_matches!(
|
||||
quorum(&[*m3, *m1], 1, &repo),
|
||||
Err(QuorumError::Diverging(_))
|
||||
Err(QuorumError::Diverging { .. })
|
||||
);
|
||||
assert_matches!(
|
||||
quorum(&[*m3, *m1], 2, &repo),
|
||||
Err(QuorumError::NoCandidates(_))
|
||||
Err(QuorumError::NoCandidates { .. })
|
||||
);
|
||||
assert_matches!(
|
||||
quorum(&[*m3, *m2], 1, &repo),
|
||||
Err(QuorumError::Diverging(_))
|
||||
Err(QuorumError::Diverging { .. })
|
||||
);
|
||||
assert_matches!(
|
||||
quorum(&[*m3, *m2], 2, &repo),
|
||||
Err(QuorumError::NoCandidates(_))
|
||||
Err(QuorumError::NoCandidates { .. })
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -526,16 +526,6 @@ impl MatchedRule<'_> {
|
|||
pub fn threshold(&self) -> &doc::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
|
||||
|
|
@ -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 {
|
||||
fn from(Rules { rules }: Rules) -> Self {
|
||||
Self {
|
||||
|
|
@ -615,18 +611,32 @@ impl Rules {
|
|||
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
|
||||
/// `refname`.
|
||||
pub fn matches<'a>(&self, refname: Qualified<'a>) -> Option<MatchedRule<'a>> {
|
||||
self.rules
|
||||
.iter()
|
||||
.find(|(pattern, _)| pattern.matches(&refname))
|
||||
.map(|(_, rule)| MatchedRule {
|
||||
refname,
|
||||
rule: rule.clone(),
|
||||
})
|
||||
pub fn canonical<'a, 'b>(
|
||||
&'a self,
|
||||
refname: Qualified<'b>,
|
||||
repo: &Repository,
|
||||
) -> Result<Option<Canonical<'b, 'a>>, git::raw::Error> {
|
||||
if let Some((_, rule)) = self.matches(&refname).next() {
|
||||
Ok(Some(Canonical::new(repo, refname, rule)?))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1179,19 +1189,21 @@ mod tests {
|
|||
// candidates tag.
|
||||
let stored = storage.repository(rid).unwrap();
|
||||
let failing = git::Qualified::from(git::lit::refs_tags(failing_tag));
|
||||
for (refname, oid) in tags.iter() {
|
||||
let matched = rules.matches(refname.clone()).unwrap_or_else(|| {
|
||||
panic!("there should be a matching rule for {refname}, rules: {rules:#?}")
|
||||
});
|
||||
let canonical = matched.canonical(&stored).unwrap();
|
||||
if *refname == failing {
|
||||
for (refname, oid) in tags.into_iter() {
|
||||
let canonical = rules
|
||||
.canonical(refname.clone(), &stored)
|
||||
.unwrap()
|
||||
.unwrap_or_else(|| {
|
||||
panic!("there should be a matching rule for {refname}, rules: {rules:#?}")
|
||||
});
|
||||
if refname == failing {
|
||||
assert!(canonical.quorum(&repo).is_err());
|
||||
} else {
|
||||
assert_eq!(
|
||||
canonical
|
||||
.quorum(&repo)
|
||||
.unwrap_or_else(|e| panic!("quorum error for {refname}: {e}")),
|
||||
*oid,
|
||||
(refname, oid),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
#![warn(clippy::unwrap_used)]
|
||||
pub mod crefs;
|
||||
pub mod did;
|
||||
pub mod doc;
|
||||
pub mod project;
|
||||
|
||||
pub use crefs::CanonicalRefs;
|
||||
pub use crypto::PublicKey;
|
||||
pub use did::Did;
|
||||
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::Signature;
|
||||
use crate::git;
|
||||
use crate::git::canonical::rules;
|
||||
use crate::identity::{project::Project, Did};
|
||||
use crate::node::device::Device;
|
||||
use crate::storage;
|
||||
|
|
@ -27,6 +28,9 @@ use crate::storage::{ReadRepository, RepositoryError};
|
|||
pub use crypto::PublicKey;
|
||||
pub use id::*;
|
||||
|
||||
use super::crefs::{self, RawCanonicalRefs};
|
||||
use super::CanonicalRefs;
|
||||
|
||||
/// Path to the identity document in the identity branch.
|
||||
pub static PATH: LazyLock<&Path> = LazyLock::new(|| Path::new("radicle.json"));
|
||||
/// 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.
|
||||
///
|
||||
/// It is used to ensure compatibility when parsing identity documents.
|
||||
|
|
@ -215,10 +227,20 @@ impl PayloadId {
|
|||
.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)]
|
||||
pub enum PayloadError {
|
||||
#[error(transparent)]
|
||||
CanonicalRefs(#[from] rules::ValidationError),
|
||||
#[error("json: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
#[error("payload '{0}' not found in identity document")]
|
||||
|
|
@ -536,6 +558,11 @@ impl Delegates {
|
|||
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.
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
|
|
@ -715,6 +742,19 @@ impl Doc {
|
|||
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.
|
||||
pub fn 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)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod test {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ use crate::cob;
|
|||
use crate::collections::RandomMap;
|
||||
use crate::git::{canonical, ext as git_ext};
|
||||
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::{Identity, RepoId};
|
||||
use crate::node::device::Device;
|
||||
|
|
@ -120,6 +120,10 @@ pub enum RepositoryError {
|
|||
Quorum(#[from] canonical::QuorumError),
|
||||
#[error(transparent)]
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@ use std::{fs, io};
|
|||
use crypto::Verified;
|
||||
use tempfile::TempDir;
|
||||
|
||||
use crate::git::canonical::Canonical;
|
||||
use crate::identity::crefs::GetCanonicalRefs as _;
|
||||
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::node::device::Device;
|
||||
use crate::node::SyncedAt;
|
||||
|
|
@ -749,13 +749,18 @@ impl ReadRepository for Repository {
|
|||
|
||||
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();
|
||||
let oid =
|
||||
Canonical::default_branch(self, &project, doc.delegates().into(), doc.threshold())?
|
||||
.quorum(raw)?;
|
||||
Ok((branch_ref, oid))
|
||||
let refname = git::refs::branch(doc.project()?.default_branch());
|
||||
let crefs = match doc.canonical_refs()? {
|
||||
Some(crefs) => crefs,
|
||||
// Fallback to constructing the default branch via the project
|
||||
// payload
|
||||
None => CanonicalRefs::from_iter([doc.default_branch_rule()?]),
|
||||
};
|
||||
Ok(crefs
|
||||
.rules()
|
||||
.canonical(refname, self)?
|
||||
.ok_or(RepositoryError::MissingBranchRule)?
|
||||
.quorum(self.raw())?)
|
||||
}
|
||||
|
||||
fn identity_head(&self) -> Result<Oid, RepositoryError> {
|
||||
|
|
|
|||
Loading…
Reference in New Issue