Set patch status to `merged` automatically
When a merge is applied to a patch, we check whether the threshold of delegates has been reached, at which point we set the patch status to `merged`. This requires threading the identity document oid through the "apply" process.
This commit is contained in:
parent
6b44955cd9
commit
33461e4be1
|
|
@ -76,7 +76,7 @@ Fast-forward
|
|||
The patch is now merged and closed :).
|
||||
|
||||
```
|
||||
$ rad patch
|
||||
$ rad patch --merged
|
||||
╭───────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ Define power requirements a07ef77 R2 f6484e0 (flux-capacitor-power, master) ahead 3, behind 0 │
|
||||
├───────────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ use std::ffi::OsString;
|
|||
|
||||
use anyhow::anyhow;
|
||||
|
||||
use radicle::cob::patch;
|
||||
use radicle::cob::patch::PatchId;
|
||||
use radicle::{prelude::*, Node};
|
||||
|
||||
|
|
@ -34,7 +35,7 @@ pub const HELP: Help = Help {
|
|||
Usage
|
||||
|
||||
rad patch
|
||||
rad patch list
|
||||
rad patch list [--all|--merged|--open|--archived]
|
||||
rad patch show <id>
|
||||
rad patch open [<option>...]
|
||||
rad patch update <id> [<option>...]
|
||||
|
|
@ -48,7 +49,14 @@ Create/Update options
|
|||
-m, --message [<string>] Provide a comment message to the patch or revision (default: prompt)
|
||||
--no-message Leave the patch or revision comment message blank
|
||||
|
||||
Options
|
||||
List options
|
||||
|
||||
--all Show all patches, including merged and archived patches
|
||||
--archived Show only archived patches
|
||||
--merged Show only merged patches
|
||||
--open Show only open patches (default)
|
||||
|
||||
Other options
|
||||
|
||||
--help Print help
|
||||
"#,
|
||||
|
|
@ -83,7 +91,9 @@ pub enum Operation {
|
|||
Checkout {
|
||||
patch_id: Rev,
|
||||
},
|
||||
List,
|
||||
List {
|
||||
filter: Option<patch::State>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
@ -107,6 +117,7 @@ impl Args for Options {
|
|||
let mut patch_id = None;
|
||||
let mut message = Message::default();
|
||||
let mut push = true;
|
||||
let mut filter = Some(patch::State::Open);
|
||||
|
||||
while let Some(arg) = parser.next()? {
|
||||
match arg {
|
||||
|
|
@ -140,6 +151,20 @@ impl Args for Options {
|
|||
push = false;
|
||||
}
|
||||
|
||||
// List options.
|
||||
Long("all") => {
|
||||
filter = None;
|
||||
}
|
||||
Long("archived") => {
|
||||
filter = Some(patch::State::Archived);
|
||||
}
|
||||
Long("merged") => {
|
||||
filter = Some(patch::State::Merged);
|
||||
}
|
||||
Long("open") => {
|
||||
filter = Some(patch::State::Open);
|
||||
}
|
||||
|
||||
// Common.
|
||||
Long("verbose") | Short('v') => {
|
||||
verbose = true;
|
||||
|
|
@ -169,7 +194,7 @@ impl Args for Options {
|
|||
|
||||
let op = match op.unwrap_or_default() {
|
||||
OperationName::Open => Operation::Open { message },
|
||||
OperationName::List => Operation::List,
|
||||
OperationName::List => Operation::List { filter },
|
||||
OperationName::Show => Operation::Show {
|
||||
patch_id: patch_id.ok_or_else(|| anyhow!("a patch id must be provided"))?,
|
||||
},
|
||||
|
|
@ -210,8 +235,8 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
|||
Operation::Open { ref message } => {
|
||||
create::run(&repository, &profile, &workdir, message.clone(), options)?;
|
||||
}
|
||||
Operation::List => {
|
||||
list::run(&repository, &profile, Some(workdir))?;
|
||||
Operation::List { filter } => {
|
||||
list::run(&repository, &profile, Some(workdir), filter)?;
|
||||
}
|
||||
Operation::Show { patch_id } => {
|
||||
let patch_id = patch_id.resolve(&repository.backend)?;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
use anyhow::anyhow;
|
||||
|
||||
use radicle::cob::patch;
|
||||
use radicle::cob::patch::{Patch, PatchId, Patches, Revision, Verdict};
|
||||
use radicle::git;
|
||||
use radicle::prelude::*;
|
||||
|
|
@ -16,17 +17,25 @@ pub fn run(
|
|||
repository: &Repository,
|
||||
profile: &Profile,
|
||||
workdir: Option<git::raw::Repository>,
|
||||
filter: Option<patch::State>,
|
||||
) -> anyhow::Result<()> {
|
||||
let me = *profile.id();
|
||||
let patches = Patches::open(repository)?;
|
||||
let proposed = patches.proposed()?;
|
||||
let all = patches.all()?;
|
||||
|
||||
// Patches the user authored.
|
||||
let mut own = Vec::new();
|
||||
// Patches other users authored.
|
||||
let mut other = Vec::new();
|
||||
|
||||
for (id, patch, _) in proposed {
|
||||
for patch in all {
|
||||
let (id, patch, _) = patch?;
|
||||
|
||||
if let Some(filter) = filter {
|
||||
if patch.state() != filter {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if patch.author().id().as_key() == &me {
|
||||
own.push((id, patch));
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ mod routes {
|
|||
"open": 1,
|
||||
"draft": 0,
|
||||
"archived": 0,
|
||||
"merged": 0,
|
||||
},
|
||||
"issues": {
|
||||
"open": 1,
|
||||
|
|
|
|||
|
|
@ -751,6 +751,7 @@ mod routes {
|
|||
"open": 1,
|
||||
"draft": 0,
|
||||
"archived": 0,
|
||||
"merged": 0,
|
||||
},
|
||||
"issues": {
|
||||
"open": 1,
|
||||
|
|
@ -781,6 +782,7 @@ mod routes {
|
|||
"open": 1,
|
||||
"draft": 0,
|
||||
"archived": 0,
|
||||
"merged": 0,
|
||||
},
|
||||
"issues": {
|
||||
"open": 1,
|
||||
|
|
@ -2267,7 +2269,7 @@ mod routes {
|
|||
},
|
||||
"title": "A new `hello world`",
|
||||
"description": "change `hello world` in README to something else",
|
||||
"state": { "status": "open" },
|
||||
"state": { "status": "merged" },
|
||||
"target": "delegates",
|
||||
"tags": [],
|
||||
"revisions": [
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ use crate::{
|
|||
store::{self, FromHistory as _, Transaction},
|
||||
},
|
||||
identity::{doc::DocError, Did, Identity, IdentityError},
|
||||
prelude::Doc,
|
||||
prelude::{Doc, ReadRepository},
|
||||
storage::{git as storage, RemoteId, WriteRepository},
|
||||
};
|
||||
|
||||
|
|
@ -307,7 +307,11 @@ impl store::FromHistory for Proposal {
|
|||
&*TYPENAME
|
||||
}
|
||||
|
||||
fn apply(&mut self, ops: impl IntoIterator<Item = Op>) -> Result<(), Self::Error> {
|
||||
fn apply<R: ReadRepository>(
|
||||
&mut self,
|
||||
ops: impl IntoIterator<Item = Op>,
|
||||
repo: &R,
|
||||
) -> Result<(), Self::Error> {
|
||||
for op in ops {
|
||||
let id = op.id;
|
||||
let author = Author::new(op.author);
|
||||
|
|
@ -358,15 +362,17 @@ impl store::FromHistory for Proposal {
|
|||
}
|
||||
|
||||
Action::Thread { revision, action } => match self.revisions.get_mut(&revision) {
|
||||
Some(Redactable::Present(revision)) => {
|
||||
revision.discussion.apply([cob::Op::new(
|
||||
Some(Redactable::Present(revision)) => revision.discussion.apply(
|
||||
[cob::Op::new(
|
||||
op.id,
|
||||
action,
|
||||
op.author,
|
||||
op.timestamp,
|
||||
op.clock,
|
||||
)])?
|
||||
}
|
||||
op.identity,
|
||||
)],
|
||||
repo,
|
||||
)?,
|
||||
Some(Redactable::Redacted) => return Err(ApplyError::Redacted(revision)),
|
||||
None => return Err(ApplyError::Missing(revision)),
|
||||
},
|
||||
|
|
@ -578,7 +584,7 @@ impl<'a, 'g> ProposalMut<'a, 'g> {
|
|||
operations(&mut tx)?;
|
||||
let (ops, clock, commit) = tx.commit(message, self.id, &mut self.store.raw, signer)?;
|
||||
|
||||
self.proposal.apply(ops)?;
|
||||
self.proposal.apply(ops, self.store.as_ref())?;
|
||||
self.clock = clock;
|
||||
|
||||
Ok(commit)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ use crate::cob::thread;
|
|||
use crate::cob::thread::{CommentId, Thread};
|
||||
use crate::cob::{store, ActorId, EntryId, ObjectId, TypeName};
|
||||
use crate::crypto::Signer;
|
||||
use crate::prelude::Did;
|
||||
use crate::prelude::{Did, ReadRepository};
|
||||
use crate::storage::git as storage;
|
||||
|
||||
/// Issue operation.
|
||||
|
|
@ -122,7 +122,11 @@ impl store::FromHistory for Issue {
|
|||
&*TYPENAME
|
||||
}
|
||||
|
||||
fn apply(&mut self, ops: impl IntoIterator<Item = Op>) -> Result<(), Error> {
|
||||
fn apply<R: ReadRepository>(
|
||||
&mut self,
|
||||
ops: impl IntoIterator<Item = Op>,
|
||||
repo: &R,
|
||||
) -> Result<(), Error> {
|
||||
for op in ops {
|
||||
match op.action {
|
||||
Action::Assign { add, remove } => {
|
||||
|
|
@ -148,13 +152,17 @@ impl store::FromHistory for Issue {
|
|||
}
|
||||
}
|
||||
Action::Thread { action } => {
|
||||
self.thread.apply([cob::Op::new(
|
||||
self.thread.apply(
|
||||
[cob::Op::new(
|
||||
op.id,
|
||||
action,
|
||||
op.author,
|
||||
op.timestamp,
|
||||
op.clock,
|
||||
)])?;
|
||||
op.identity,
|
||||
)],
|
||||
repo,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -389,7 +397,7 @@ impl<'a, 'g> IssueMut<'a, 'g> {
|
|||
operations(&mut tx)?;
|
||||
let (ops, clock, commit) = tx.commit(message, self.id, &mut self.store.raw, signer)?;
|
||||
|
||||
self.issue.apply(ops)?;
|
||||
self.issue.apply(ops, self.store.as_ref())?;
|
||||
self.clock = clock;
|
||||
|
||||
Ok(commit)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ use radicle_crdt::clock;
|
|||
use radicle_crdt::clock::Lamport;
|
||||
use radicle_crypto::PublicKey;
|
||||
|
||||
use crate::git;
|
||||
|
||||
/// The author of an [`Op`].
|
||||
pub type ActorId = PublicKey;
|
||||
|
||||
|
|
@ -34,6 +36,8 @@ pub struct Op<A> {
|
|||
pub clock: Lamport,
|
||||
/// Timestamp of this operation.
|
||||
pub timestamp: clock::Physical,
|
||||
/// Head of identity document committed to by this operation.
|
||||
pub identity: git::Oid,
|
||||
}
|
||||
|
||||
impl<A: Eq> PartialOrd for Op<A> {
|
||||
|
|
@ -55,6 +59,7 @@ impl<A> Op<A> {
|
|||
author: ActorId,
|
||||
timestamp: impl Into<clock::Physical>,
|
||||
clock: Lamport,
|
||||
identity: git::Oid,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
|
|
@ -62,6 +67,7 @@ impl<A> Op<A> {
|
|||
author,
|
||||
clock,
|
||||
timestamp: timestamp.into(),
|
||||
identity,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -80,6 +86,7 @@ where
|
|||
|
||||
fn try_from(entry: &'a EntryWithClock) -> Result<Self, Self::Error> {
|
||||
let id = *entry.id();
|
||||
let identity = entry.resource();
|
||||
let ops = entry
|
||||
.changes()
|
||||
.map(|blob| {
|
||||
|
|
@ -90,6 +97,7 @@ where
|
|||
author: *entry.actor(),
|
||||
clock: entry.clock().into(),
|
||||
timestamp: entry.timestamp().into(),
|
||||
identity,
|
||||
};
|
||||
Ok::<_, Self::Error>(op)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ use crate::cob::thread::Thread;
|
|||
use crate::cob::{store, ActorId, EntryId, ObjectId, TypeName};
|
||||
use crate::crypto::{PublicKey, Signer};
|
||||
use crate::git;
|
||||
use crate::identity::doc::DocError;
|
||||
use crate::prelude::*;
|
||||
use crate::storage::git as storage;
|
||||
|
||||
|
|
@ -60,6 +61,9 @@ pub enum ApplyError {
|
|||
/// Error applying an op to the patch thread.
|
||||
#[error("thread apply failed: {0}")]
|
||||
Thread(#[from] thread::OpError),
|
||||
/// Error loading the identity document committed to by an operation.
|
||||
#[error("identity doc failed to load: {0}")]
|
||||
Doc(#[from] DocError),
|
||||
}
|
||||
|
||||
/// Error updating or creating patches.
|
||||
|
|
@ -275,7 +279,11 @@ impl store::FromHistory for Patch {
|
|||
&*TYPENAME
|
||||
}
|
||||
|
||||
fn apply(&mut self, ops: impl IntoIterator<Item = Op>) -> Result<(), ApplyError> {
|
||||
fn apply<R: ReadRepository>(
|
||||
&mut self,
|
||||
ops: impl IntoIterator<Item = Op>,
|
||||
repo: &R,
|
||||
) -> Result<(), ApplyError> {
|
||||
for op in ops {
|
||||
let id = op.id;
|
||||
let author = Author::new(op.author);
|
||||
|
|
@ -376,6 +384,16 @@ impl store::FromHistory for Patch {
|
|||
.into(),
|
||||
op.clock,
|
||||
);
|
||||
let doc = repo.identity_doc_at(op.identity)?;
|
||||
|
||||
if revision
|
||||
.merges()
|
||||
.filter(|m| doc.is_delegate(&m.node))
|
||||
.count()
|
||||
>= doc.threshold
|
||||
{
|
||||
self.state.set(State::Merged, op.clock);
|
||||
}
|
||||
} else {
|
||||
return Err(ApplyError::Missing(revision));
|
||||
}
|
||||
|
|
@ -384,9 +402,17 @@ impl store::FromHistory for Patch {
|
|||
// TODO(cloudhead): Make sure we can deal with redacted revisions which are added
|
||||
// to out of order, like in the `Merge` case.
|
||||
if let Some(Redactable::Present(revision)) = self.revisions.get_mut(&revision) {
|
||||
revision
|
||||
.discussion
|
||||
.apply([cob::Op::new(op.id, action, op.author, timestamp, op.clock)])?;
|
||||
revision.discussion.apply(
|
||||
[cob::Op::new(
|
||||
op.id,
|
||||
action,
|
||||
op.author,
|
||||
timestamp,
|
||||
op.clock,
|
||||
op.identity,
|
||||
)],
|
||||
repo,
|
||||
)?;
|
||||
} else {
|
||||
return Err(ApplyError::Missing(revision));
|
||||
}
|
||||
|
|
@ -487,6 +513,7 @@ pub enum State {
|
|||
Open,
|
||||
Draft,
|
||||
Archived,
|
||||
Merged,
|
||||
}
|
||||
|
||||
impl fmt::Display for State {
|
||||
|
|
@ -495,6 +522,7 @@ impl fmt::Display for State {
|
|||
Self::Archived => write!(f, "archived"),
|
||||
Self::Draft => write!(f, "draft"),
|
||||
Self::Open => write!(f, "open"),
|
||||
Self::Merged => write!(f, "merged"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -830,7 +858,7 @@ impl<'a, 'g> PatchMut<'a, 'g> {
|
|||
operations(&mut tx)?;
|
||||
let (ops, clock, commit) = tx.commit(message, self.id, &mut self.store.raw, signer)?;
|
||||
|
||||
self.patch.apply(ops)?;
|
||||
self.patch.apply(ops, self.store.as_ref())?;
|
||||
self.clock = clock;
|
||||
|
||||
Ok(commit)
|
||||
|
|
@ -953,6 +981,7 @@ pub struct PatchCounts {
|
|||
pub open: usize,
|
||||
pub draft: usize,
|
||||
pub archived: usize,
|
||||
pub merged: usize,
|
||||
}
|
||||
|
||||
pub struct Patches<'a> {
|
||||
|
|
@ -1010,6 +1039,7 @@ impl<'a> Patches<'a> {
|
|||
State::Draft => state.draft += 1,
|
||||
State::Open => state.open += 1,
|
||||
State::Archived => state.archived += 1,
|
||||
State::Merged => state.merged += 1,
|
||||
}
|
||||
state
|
||||
});
|
||||
|
|
@ -1059,7 +1089,7 @@ impl<'a> Patches<'a> {
|
|||
/// Get proposed patches.
|
||||
pub fn proposed(
|
||||
&self,
|
||||
) -> Result<impl Iterator<Item = (PatchId, Patch, clock::Lamport)>, Error> {
|
||||
) -> Result<impl Iterator<Item = (PatchId, Patch, clock::Lamport)> + 'a, Error> {
|
||||
let all = self.all()?;
|
||||
|
||||
Ok(all
|
||||
|
|
@ -1094,6 +1124,8 @@ mod test {
|
|||
use crate::cob::test::Actor;
|
||||
use crate::crypto::test::signer::MockSigner;
|
||||
use crate::test;
|
||||
use crate::test::arbitrary::gen;
|
||||
use crate::test::storage::MockRepository;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Changes<const N: usize> {
|
||||
|
|
@ -1212,22 +1244,22 @@ mod test {
|
|||
|
||||
#[test]
|
||||
fn prop_invariants() {
|
||||
fn property(log: Changes<3>) -> TestResult {
|
||||
fn property(repo: MockRepository, log: Changes<3>) -> TestResult {
|
||||
let t = Patch::default();
|
||||
let [p1, p2, p3] = log.permutations;
|
||||
|
||||
let mut t1 = t.clone();
|
||||
if t1.apply(p1).is_err() {
|
||||
if t1.apply(p1, &repo).is_err() {
|
||||
return TestResult::discard();
|
||||
}
|
||||
|
||||
let mut t2 = t.clone();
|
||||
if t2.apply(p2).is_err() {
|
||||
if t2.apply(p2, &repo).is_err() {
|
||||
return TestResult::discard();
|
||||
}
|
||||
|
||||
let mut t3 = t;
|
||||
if t3.apply(p3).is_err() {
|
||||
if t3.apply(p3, &repo).is_err() {
|
||||
return TestResult::discard();
|
||||
}
|
||||
|
||||
|
|
@ -1241,7 +1273,7 @@ mod test {
|
|||
qcheck::QuickCheck::new()
|
||||
.min_tests_passed(100)
|
||||
.gen(qcheck::Gen::new(7))
|
||||
.quickcheck(property as fn(Changes<3>) -> TestResult);
|
||||
.quickcheck(property as fn(MockRepository, Changes<3>) -> TestResult);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -1413,6 +1445,7 @@ mod test {
|
|||
let oid = git::Oid::from_str("518d5069f94c03427f694bb494ac1cd7d1339380").unwrap();
|
||||
let mut alice = Actor::<_, Action>::new(MockSigner::default());
|
||||
let mut patch = Patch::default();
|
||||
let repo = gen::<MockRepository>(1);
|
||||
|
||||
let a1 = alice.op(Action::Revision {
|
||||
description: String::new(),
|
||||
|
|
@ -1431,20 +1464,21 @@ mod test {
|
|||
commit: oid,
|
||||
});
|
||||
|
||||
patch.apply([a1]).unwrap();
|
||||
patch.apply([a1], &repo).unwrap();
|
||||
assert!(patch.revisions().next().is_some());
|
||||
|
||||
patch.apply([a2]).unwrap();
|
||||
patch.apply([a2], &repo).unwrap();
|
||||
assert!(patch.revisions().next().is_none());
|
||||
|
||||
patch.apply([a3]).unwrap_err();
|
||||
patch.apply([a4]).unwrap_err();
|
||||
patch.apply([a3], &repo).unwrap_err();
|
||||
patch.apply([a4], &repo).unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_revision_redact_reinsert() {
|
||||
let base = git::Oid::from_str("cb18e95ada2bb38aadd8e6cef0963ce37a87add3").unwrap();
|
||||
let oid = git::Oid::from_str("518d5069f94c03427f694bb494ac1cd7d1339380").unwrap();
|
||||
let repo = gen::<MockRepository>(1);
|
||||
let mut alice = Actor::<_, Action>::new(MockSigner::default());
|
||||
let mut p1 = Patch::default();
|
||||
let mut p2 = Patch::default();
|
||||
|
|
@ -1456,8 +1490,9 @@ mod test {
|
|||
});
|
||||
let a2 = alice.op(Action::Redact { revision: a1.id() });
|
||||
|
||||
p1.apply([a1.clone(), a2.clone(), a1.clone()]).unwrap();
|
||||
p2.apply([a1.clone(), a1, a2]).unwrap();
|
||||
p1.apply([a1.clone(), a2.clone(), a1.clone()], &repo)
|
||||
.unwrap();
|
||||
p2.apply([a1.clone(), a1, a2], &repo).unwrap();
|
||||
|
||||
assert_eq!(p1, p2);
|
||||
}
|
||||
|
|
@ -1466,6 +1501,7 @@ mod test {
|
|||
fn test_revision_merge_reinsert() {
|
||||
let base = git::Oid::from_str("cb18e95ada2bb38aadd8e6cef0963ce37a87add3").unwrap();
|
||||
let oid = git::Oid::from_str("518d5069f94c03427f694bb494ac1cd7d1339380").unwrap();
|
||||
let repo = gen::<MockRepository>(1);
|
||||
let mut alice = Actor::<_, Action>::new(MockSigner::default());
|
||||
let mut p1 = Patch::default();
|
||||
let mut p2 = Patch::default();
|
||||
|
|
@ -1480,8 +1516,9 @@ mod test {
|
|||
commit: oid,
|
||||
});
|
||||
|
||||
p1.apply([a1.clone(), a2.clone(), a1.clone()]).unwrap();
|
||||
p2.apply([a1.clone(), a1, a2]).unwrap();
|
||||
p1.apply([a1.clone(), a2.clone(), a1.clone()], &repo)
|
||||
.unwrap();
|
||||
p2.apply([a1.clone(), a1, a2], &repo).unwrap();
|
||||
|
||||
assert_eq!(p1, p2);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,15 +30,21 @@ pub trait FromHistory: Sized + Default {
|
|||
fn type_name() -> &'static TypeName;
|
||||
|
||||
/// Apply a list of operations to the state.
|
||||
fn apply(&mut self, ops: impl IntoIterator<Item = Op<Self::Action>>)
|
||||
-> Result<(), Self::Error>;
|
||||
fn apply<R: ReadRepository>(
|
||||
&mut self,
|
||||
ops: impl IntoIterator<Item = Op<Self::Action>>,
|
||||
repo: &R,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Create an object from a history.
|
||||
fn from_history(history: &History) -> Result<(Self, Lamport), Error> {
|
||||
fn from_history<R: ReadRepository>(
|
||||
history: &History,
|
||||
repo: &R,
|
||||
) -> Result<(Self, Lamport), Error> {
|
||||
let obj = history.traverse(Self::default(), |mut acc, entry| {
|
||||
match Ops::try_from(entry) {
|
||||
Ok(Ops(ops)) => {
|
||||
if let Err(err) = acc.apply(ops) {
|
||||
if let Err(err) = acc.apply(ops, repo) {
|
||||
log::warn!("Error applying op to `{}` state: {err}", Self::type_name());
|
||||
return ControlFlow::Break(acc);
|
||||
}
|
||||
|
|
@ -59,9 +65,12 @@ pub trait FromHistory: Sized + Default {
|
|||
|
||||
/// Create an object from individual operations.
|
||||
/// Returns an error if any of the operations fails to apply.
|
||||
fn from_ops(ops: impl IntoIterator<Item = Op<Self::Action>>) -> Result<Self, Self::Error> {
|
||||
fn from_ops<R: ReadRepository>(
|
||||
ops: impl IntoIterator<Item = Op<Self::Action>>,
|
||||
repo: &R,
|
||||
) -> Result<Self, Self::Error> {
|
||||
let mut state = Self::default();
|
||||
state.apply(ops)?;
|
||||
state.apply(ops, repo)?;
|
||||
|
||||
Ok(state)
|
||||
}
|
||||
|
|
@ -168,7 +177,7 @@ where
|
|||
contents,
|
||||
},
|
||||
)?;
|
||||
let (object, clock) = T::from_history(cob.history())?;
|
||||
let (object, clock) = T::from_history(cob.history(), self.repo)?;
|
||||
|
||||
self.repo.sign_refs(signer).map_err(Error::SignRefs)?;
|
||||
|
||||
|
|
@ -183,7 +192,7 @@ where
|
|||
if cob.manifest().history_type != HISTORY_TYPE {
|
||||
return Err(Error::HistoryType(cob.manifest().history_type.clone()));
|
||||
}
|
||||
let (obj, clock) = T::from_history(cob.history())?;
|
||||
let (obj, clock) = T::from_history(cob.history(), self.repo)?;
|
||||
|
||||
Ok(Some((obj, clock)))
|
||||
} else {
|
||||
|
|
@ -194,11 +203,11 @@ where
|
|||
/// Return all objects.
|
||||
pub fn all(
|
||||
&self,
|
||||
) -> Result<impl Iterator<Item = Result<(ObjectId, T, Lamport), Error>>, Error> {
|
||||
) -> Result<impl Iterator<Item = Result<(ObjectId, T, Lamport), Error>> + 'a, Error> {
|
||||
let raw = cob::list(self.repo, T::type_name())?;
|
||||
|
||||
Ok(raw.into_iter().map(|o| {
|
||||
let (obj, clock) = T::from_history(o.history())?;
|
||||
let (obj, clock) = T::from_history(o.history(), self.repo)?;
|
||||
Ok((*o.id(), obj, clock))
|
||||
}))
|
||||
}
|
||||
|
|
@ -295,6 +304,7 @@ impl<T: FromHistory> Transaction<T> {
|
|||
let author = self.actor;
|
||||
let timestamp = object.history().timestamp().into();
|
||||
let clock = self.clock.tick();
|
||||
let identity = store.parent;
|
||||
|
||||
// The history clock should be in sync with the tx clock.
|
||||
assert_eq!(object.history().clock(), self.clock.get());
|
||||
|
|
@ -308,6 +318,7 @@ impl<T: FromHistory> Transaction<T> {
|
|||
author,
|
||||
clock,
|
||||
timestamp,
|
||||
identity,
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
|
|
|||
|
|
@ -151,12 +151,14 @@ impl<G: Signer, A: Clone + Serialize> Actor<G, A> {
|
|||
let author = *self.signer.public_key();
|
||||
let clock = self.clock.tick();
|
||||
let timestamp = clock::Physical::now();
|
||||
let identity = arbitrary::oid();
|
||||
let op = Op {
|
||||
id,
|
||||
action,
|
||||
author,
|
||||
clock,
|
||||
timestamp,
|
||||
identity,
|
||||
};
|
||||
self.ops.insert((self.clock, author), op.clone());
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ use thiserror::Error;
|
|||
use crate::cob;
|
||||
use crate::cob::common::{Reaction, Timestamp};
|
||||
use crate::cob::{ActorId, EntryId, Op};
|
||||
use crate::prelude::ReadRepository;
|
||||
|
||||
use crdt::clock::Lamport;
|
||||
use crdt::{GMap, GSet, LWWSet, Max, Redactable, Semilattice};
|
||||
|
|
@ -261,7 +262,11 @@ impl cob::store::FromHistory for Thread {
|
|||
&*TYPENAME
|
||||
}
|
||||
|
||||
fn apply(&mut self, ops: impl IntoIterator<Item = Op<Action>>) -> Result<(), OpError> {
|
||||
fn apply<R: ReadRepository>(
|
||||
&mut self,
|
||||
ops: impl IntoIterator<Item = Op<Action>>,
|
||||
_repo: &R,
|
||||
) -> Result<(), OpError> {
|
||||
for op in ops.into_iter() {
|
||||
let id = op.id;
|
||||
let author = op.author;
|
||||
|
|
@ -331,6 +336,8 @@ mod tests {
|
|||
use crate::cob::test;
|
||||
use crate::crypto::test::signer::MockSigner;
|
||||
use crate::crypto::Signer;
|
||||
use crate::test::arbitrary::gen;
|
||||
use crate::test::storage::MockRepository;
|
||||
|
||||
/// An object that can be used to create and sign changes.
|
||||
pub struct Actor<G> {
|
||||
|
|
@ -489,6 +496,7 @@ mod tests {
|
|||
fn test_redact_comment() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let (_, signer, _) = radicle::test::setup::context(&tmp);
|
||||
let repo = gen::<MockRepository>(1);
|
||||
let mut alice = Actor::new(signer);
|
||||
let mut thread = Thread::default();
|
||||
|
||||
|
|
@ -496,12 +504,12 @@ mod tests {
|
|||
let a1 = alice.comment("Second comment", Some(a0.id()));
|
||||
let a2 = alice.comment("Third comment", Some(a0.id()));
|
||||
|
||||
thread.apply([a0, a1.clone(), a2]).unwrap();
|
||||
thread.apply([a0, a1.clone(), a2], &repo).unwrap();
|
||||
assert_eq!(thread.comments().count(), 3);
|
||||
|
||||
// Redact the second comment.
|
||||
let a3 = alice.redact(a1.id());
|
||||
thread.apply([a3]).unwrap();
|
||||
thread.apply([a3], &repo).unwrap();
|
||||
|
||||
let (_, comment0) = thread.comments().nth(0).unwrap();
|
||||
let (_, comment1) = thread.comments().nth(1).unwrap();
|
||||
|
|
@ -514,13 +522,15 @@ mod tests {
|
|||
#[test]
|
||||
fn test_edit_comment() {
|
||||
let mut alice = Actor::<MockSigner>::default();
|
||||
let repo = gen::<MockRepository>(1);
|
||||
|
||||
let c0 = alice.comment("Hello world!", None);
|
||||
let c1 = alice.edit(c0.id(), "Goodbye world.");
|
||||
let c2 = alice.edit(c0.id(), "Goodbye world!");
|
||||
|
||||
let mut t1 = Thread::default();
|
||||
t1.apply([c0.clone(), c1.clone(), c2.clone()]).unwrap();
|
||||
t1.apply([c0.clone(), c1.clone(), c2.clone()], &repo)
|
||||
.unwrap();
|
||||
|
||||
let comment = t1.comment(&c0.id());
|
||||
let edits = comment.unwrap().edits().collect::<Vec<_>>();
|
||||
|
|
@ -531,7 +541,7 @@ mod tests {
|
|||
assert_eq!(t1.comment(&c0.id()).unwrap().body(), "Goodbye world!");
|
||||
|
||||
let mut t2 = Thread::default();
|
||||
t2.apply([c0, c2, c1]).unwrap(); // Apply in different order.
|
||||
t2.apply([c0, c2, c1], &repo).unwrap(); // Apply in different order.
|
||||
|
||||
assert_eq!(t1, t2);
|
||||
}
|
||||
|
|
@ -610,6 +620,8 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_histories() {
|
||||
let repo = gen::<MockRepository>(1);
|
||||
|
||||
let mut alice = Actor::<MockSigner>::default();
|
||||
let mut bob = Actor::<MockSigner>::default();
|
||||
let mut eve = Actor::<MockSigner>::default();
|
||||
|
|
@ -628,15 +640,17 @@ mod tests {
|
|||
a.merge(b);
|
||||
a.merge(e);
|
||||
|
||||
let (expected, _) = Thread::from_history(&a).unwrap();
|
||||
let (expected, _) = Thread::from_history(&a, &repo).unwrap();
|
||||
for permutation in a.permutations(2) {
|
||||
let actual = Thread::from_ops(permutation).unwrap();
|
||||
let actual = Thread::from_ops(permutation, &repo).unwrap();
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duplicate_comments() {
|
||||
let repo = gen::<MockRepository>(1);
|
||||
|
||||
let mut alice = Actor::<MockSigner>::default();
|
||||
let mut bob = Actor::<MockSigner>::default();
|
||||
|
||||
|
|
@ -649,7 +663,7 @@ mod tests {
|
|||
b.append(&b0);
|
||||
a.merge(b);
|
||||
|
||||
let (thread, _) = Thread::from_history(&a).unwrap();
|
||||
let (thread, _) = Thread::from_history(&a, &repo).unwrap();
|
||||
|
||||
assert_eq!(thread.comments().count(), 2);
|
||||
|
||||
|
|
@ -662,6 +676,8 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_duplicate_comments_same_author() {
|
||||
let repo = gen::<MockRepository>(1);
|
||||
|
||||
let mut alice = Actor::<MockSigner>::default();
|
||||
|
||||
let a0 = alice.comment("Hello World!", None);
|
||||
|
|
@ -681,7 +697,7 @@ mod tests {
|
|||
h3.merge(h1);
|
||||
h3.merge(h2);
|
||||
|
||||
let (thread, _) = Thread::from_history(&h3).unwrap();
|
||||
let (thread, _) = Thread::from_history(&h3, &repo).unwrap();
|
||||
|
||||
// The three comments, distinct yet identical in terms of content, are preserved.
|
||||
assert_eq!(thread.comments().count(), 3);
|
||||
|
|
@ -700,6 +716,8 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_comment_edit_reinsert() {
|
||||
let repo = gen::<MockRepository>(1);
|
||||
|
||||
let mut alice = Actor::<MockSigner>::default();
|
||||
let mut t1 = Thread::default();
|
||||
let mut t2 = Thread::default();
|
||||
|
|
@ -707,30 +725,31 @@ mod tests {
|
|||
let a1 = alice.comment("Hello.", None);
|
||||
let a2 = alice.edit(a1.id(), "Hello World.");
|
||||
|
||||
t1.apply([a1.clone(), a2.clone(), a1.clone()]).unwrap();
|
||||
t2.apply([a1.clone(), a1, a2]).unwrap();
|
||||
t1.apply([a1.clone(), a2.clone(), a1.clone()], &repo)
|
||||
.unwrap();
|
||||
t2.apply([a1.clone(), a1, a2], &repo).unwrap();
|
||||
|
||||
assert_eq!(t1, t2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prop_invariants() {
|
||||
fn property(log: Changes<3>) -> TestResult {
|
||||
fn property(repo: MockRepository, log: Changes<3>) -> TestResult {
|
||||
let t = Thread::default();
|
||||
let [p1, p2, p3] = log.permutations;
|
||||
|
||||
let mut t1 = t.clone();
|
||||
if t1.apply(p1).is_err() {
|
||||
if t1.apply(p1, &repo).is_err() {
|
||||
return TestResult::discard();
|
||||
}
|
||||
|
||||
let mut t2 = t.clone();
|
||||
if t2.apply(p2).is_err() {
|
||||
if t2.apply(p2, &repo).is_err() {
|
||||
return TestResult::discard();
|
||||
}
|
||||
|
||||
let mut t3 = t;
|
||||
if t3.apply(p3).is_err() {
|
||||
if t3.apply(p3, &repo).is_err() {
|
||||
return TestResult::discard();
|
||||
}
|
||||
|
||||
|
|
@ -744,6 +763,6 @@ mod tests {
|
|||
.min_tests_passed(100)
|
||||
.max_tests(10000)
|
||||
.gen(qcheck::Gen::new(7))
|
||||
.quickcheck(property as fn(Changes<3>) -> TestResult);
|
||||
.quickcheck(property as fn(MockRepository, Changes<3>) -> TestResult);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -127,6 +127,14 @@ pub struct DocAt {
|
|||
pub sigs: HashMap<PublicKey, Signature>,
|
||||
}
|
||||
|
||||
impl Deref for DocAt {
|
||||
type Target = Doc<Verified>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.doc
|
||||
}
|
||||
}
|
||||
|
||||
/// An identity document.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
|
|
|||
|
|
@ -369,7 +369,15 @@ pub trait ReadRepository {
|
|||
}
|
||||
|
||||
/// Get the repository's identity document.
|
||||
fn identity_doc(&self) -> Result<(Oid, identity::Doc<Unverified>), IdentityError>;
|
||||
fn identity_doc(&self) -> Result<(Oid, identity::Doc<Unverified>), IdentityError> {
|
||||
let head = self.identity_head()?;
|
||||
let doc = self.identity_doc_at(head)?;
|
||||
|
||||
Ok((head, doc))
|
||||
}
|
||||
|
||||
/// Get the repository's identity document at a specific commit.
|
||||
fn identity_doc_at(&self, head: Oid) -> Result<identity::Doc<Unverified>, DocError>;
|
||||
}
|
||||
|
||||
/// Allows read-write access to a repository.
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ use once_cell::sync::Lazy;
|
|||
|
||||
use crate::git;
|
||||
use crate::identity;
|
||||
use crate::identity::doc::DocError;
|
||||
use crate::identity::{Doc, Id};
|
||||
use crate::identity::{Identity, IdentityError, Project};
|
||||
use crate::storage::refs;
|
||||
|
|
@ -496,12 +497,8 @@ impl ReadRepository for Repository {
|
|||
Ok(Remotes::from_iter(remotes))
|
||||
}
|
||||
|
||||
fn identity_doc(&self) -> Result<(Oid, identity::Doc<Unverified>), IdentityError> {
|
||||
let head = self.identity_head()?;
|
||||
|
||||
Doc::<Unverified>::load_at(head, self)
|
||||
.map(|(doc, _)| (head, doc))
|
||||
.map_err(IdentityError::from)
|
||||
fn identity_doc_at(&self, head: Oid) -> Result<identity::Doc<Unverified>, DocError> {
|
||||
Doc::<Unverified>::load_at(head, self).map(|(doc, _)| doc)
|
||||
}
|
||||
|
||||
fn head(&self) -> Result<(Qualified, Oid), IdentityError> {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ use crate::identity::{
|
|||
use crate::node::Address;
|
||||
use crate::storage;
|
||||
use crate::storage::refs::{Refs, SignedRefs};
|
||||
use crate::test::storage::MockStorage;
|
||||
use crate::test::storage::{MockRepository, MockStorage};
|
||||
|
||||
pub fn oid() -> storage::Oid {
|
||||
let oid_bytes: [u8; 20] = gen(1);
|
||||
|
|
@ -180,6 +180,15 @@ impl Arbitrary for MockStorage {
|
|||
}
|
||||
}
|
||||
|
||||
impl Arbitrary for MockRepository {
|
||||
fn arbitrary(g: &mut qcheck::Gen) -> Self {
|
||||
let rid = Id::arbitrary(g);
|
||||
let doc = Doc::<Verified>::arbitrary(g);
|
||||
|
||||
Self::new(rid, doc)
|
||||
}
|
||||
}
|
||||
|
||||
impl Arbitrary for storage::Remote<crypto::Verified> {
|
||||
fn arbitrary(g: &mut qcheck::Gen) -> Self {
|
||||
let refs = Refs::arbitrary(g);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use git_ref_format as fmt;
|
|||
use radicle_git_ext as git_ext;
|
||||
|
||||
use crate::crypto::{Signer, Verified};
|
||||
use crate::identity::doc::{Doc, Id};
|
||||
use crate::identity::doc::{Doc, DocError, Id};
|
||||
use crate::identity::IdentityError;
|
||||
use crate::node::NodeId;
|
||||
|
||||
|
|
@ -113,6 +113,16 @@ pub struct MockRepository {
|
|||
remotes: HashMap<NodeId, refs::SignedRefs<Verified>>,
|
||||
}
|
||||
|
||||
impl MockRepository {
|
||||
pub fn new(id: Id, doc: Doc<Verified>) -> Self {
|
||||
Self {
|
||||
id,
|
||||
doc,
|
||||
remotes: HashMap::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ReadRepository for MockRepository {
|
||||
fn id(&self) -> Id {
|
||||
self.id
|
||||
|
|
@ -195,6 +205,13 @@ impl ReadRepository for MockRepository {
|
|||
Ok((git2::Oid::zero().into(), self.doc.clone().unverified()))
|
||||
}
|
||||
|
||||
fn identity_doc_at(
|
||||
&self,
|
||||
_head: Oid,
|
||||
) -> Result<crate::identity::Doc<crate::crypto::Unverified>, DocError> {
|
||||
Ok(self.doc.clone().unverified())
|
||||
}
|
||||
|
||||
fn identity_head(&self) -> Result<Oid, IdentityError> {
|
||||
todo!()
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue