Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
cob: Rename `Change` to `Op`
Alexis Sellier committed 3 years ago
commit cc246992324f85d25ec229c713618f281d7ed563
parent fa3c155865c74a5637dab10c2245081138067133
6 files changed +213 -218
modified radicle/src/cob.rs
@@ -1,16 +1,16 @@
-
pub mod change;
pub mod common;
pub mod issue;
+
pub mod op;
pub mod patch;
pub mod store;
pub mod thread;

-
pub use change::{Actor, ActorId, Change, ChangeId};
pub use cob::{create, get, list, remove, update};
pub use cob::{
    identity, object::collaboration::error, CollaborativeObject, Contents, Create, Entry, History,
    ObjectId, TypeName, Update,
};
pub use common::*;
+
pub use op::{Actor, ActorId, Op, OpId};

use radicle_cob as cob;
deleted radicle/src/cob/change.rs
@@ -1,121 +0,0 @@
-
use std::collections::BTreeMap;
-

-
use serde::{Deserialize, Serialize};
-
use thiserror::Error;
-

-
use radicle_cob::history::EntryWithClock;
-
use radicle_crdt::clock;
-
use radicle_crdt::clock::Lamport;
-
use radicle_crypto::{PublicKey, Signer};
-

-
/// Identifies a change.
-
pub type ChangeId = (Lamport, ActorId);
-
/// The author of a change.
-
pub type ActorId = PublicKey;
-

-
/// Error decoding a change from an entry.
-
#[derive(Error, Debug)]
-
pub enum ChangeDecodeError {
-
    #[error("deserialization from json failed: {0}")]
-
    Deserialize(#[from] serde_json::Error),
-
}
-

-
/// The `Change` is the unit of replication.
-
/// Everything that can be done in the system is represented by a `Change` object.
-
/// Changes are applied to an accumulator to yield a final state.
-
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-
pub struct Change<A> {
-
    /// The action carried out by this change.
-
    pub action: A,
-
    /// The author of the change.
-
    pub author: ActorId,
-
    /// Lamport clock.
-
    pub clock: Lamport,
-
    /// Timestamp of this change.
-
    pub timestamp: clock::Physical,
-
}
-

-
impl<'a: 'de, 'de, A: serde::Deserialize<'de>> TryFrom<&'a EntryWithClock> for Change<A> {
-
    type Error = ChangeDecodeError;
-

-
    fn try_from(entry: &'a EntryWithClock) -> Result<Self, Self::Error> {
-
        let action = serde_json::from_slice(entry.contents())?;
-

-
        Ok(Change {
-
            action,
-
            author: *entry.actor(),
-
            clock: entry.clock().into(),
-
            timestamp: entry.timestamp().into(),
-
        })
-
    }
-
}
-

-
impl<A> Change<A> {
-
    /// Get the change id.
-
    pub fn id(&self) -> ChangeId {
-
        (self.clock, self.author)
-
    }
-
}
-

-
impl<'de, A: Deserialize<'de>> Change<A> {
-
    /// Deserialize a change from a byte string.
-
    pub fn decode(bytes: &'de [u8]) -> Result<Self, serde_json::Error> {
-
        serde_json::from_slice(bytes)
-
    }
-
}
-

-
/// An object that can be used to create and sign changes.
-
#[derive(Default)]
-
pub struct Actor<G, A> {
-
    pub signer: G,
-
    pub clock: Lamport,
-
    pub changes: BTreeMap<(Lamport, PublicKey), Change<A>>,
-
}
-

-
impl<G: Signer, A: Clone + Serialize> Actor<G, A> {
-
    pub fn new(signer: G) -> Self {
-
        Self {
-
            signer,
-
            clock: Lamport::default(),
-
            changes: BTreeMap::default(),
-
        }
-
    }
-

-
    pub fn receive(&mut self, changes: impl IntoIterator<Item = Change<A>>) -> Lamport {
-
        for change in changes {
-
            let clock = change.clock;
-

-
            self.changes.insert((clock, change.author), change);
-
            self.clock.merge(clock);
-
        }
-
        self.clock
-
    }
-

-
    /// Reset actor state to initial state.
-
    pub fn reset(&mut self) {
-
        self.changes.clear();
-
        self.clock = Lamport::default();
-
    }
-

-
    /// Returned an ordered list of events.
-
    pub fn timeline(&self) -> impl Iterator<Item = &Change<A>> {
-
        self.changes.values()
-
    }
-

-
    /// Create a new change.
-
    pub fn change(&mut self, action: A) -> Change<A> {
-
        let author = *self.signer.public_key();
-
        let clock = self.clock;
-
        let timestamp = clock::Physical::now();
-
        let change = Change {
-
            action,
-
            author,
-
            clock,
-
            timestamp,
-
        };
-
        self.changes.insert((self.clock, author), change.clone());
-
        self.clock.tick();
-

-
        change
-
    }
-
}
modified radicle/src/cob/issue.rs
@@ -11,12 +11,12 @@ use crate::cob;
use crate::cob::common::{Author, Reaction, Tag};
use crate::cob::thread;
use crate::cob::thread::{CommentId, Thread};
-
use crate::cob::{store, ChangeId, ObjectId, TypeName};
+
use crate::cob::{store, ObjectId, OpId, TypeName};
use crate::crypto::{PublicKey, Signer};
use crate::storage::git as storage;

-
/// Issue change.
-
pub type Change = crate::cob::Change<Action>;
+
/// Issue operation.
+
pub type Op = crate::cob::Op<Action>;

/// Type name of an issue.
pub static TYPENAME: Lazy<TypeName> =
@@ -111,9 +111,9 @@ impl store::FromHistory for Issue {
        history: &radicle_cob::History,
    ) -> Result<(Self, clock::Lamport), store::Error> {
        let obj = history.traverse(Self::default(), |mut acc, entry| {
-
            if let Ok(change) = Change::try_from(entry) {
-
                if let Err(err) = acc.apply(change) {
-
                    log::warn!("Error applying change to issue state: {err}");
+
            if let Ok(op) = Op::try_from(entry) {
+
                if let Err(err) = acc.apply(op) {
+
                    log::warn!("Error applying op to issue state: {err}");
                    return ControlFlow::Break(acc);
                }
            } else {
@@ -154,28 +154,28 @@ impl Issue {
        self.thread.comments().map(|(id, comment)| (id, comment))
    }

-
    pub fn apply(&mut self, change: Change) -> Result<(), Error> {
-
        match change.action {
+
    pub fn apply(&mut self, op: Op) -> Result<(), Error> {
+
        match op.action {
            Action::Title { title } => {
-
                self.title.set(title, change.clock);
+
                self.title.set(title, op.clock);
            }
            Action::Lifecycle { status } => {
-
                self.status.set(status, change.clock);
+
                self.status.set(status, op.clock);
            }
            Action::Tag { add, remove } => {
                for tag in add {
-
                    self.tags.insert(tag, change.clock);
+
                    self.tags.insert(tag, op.clock);
                }
                for tag in remove {
-
                    self.tags.remove(tag, change.clock);
+
                    self.tags.remove(tag, op.clock);
                }
            }
            Action::Thread { action } => {
-
                self.thread.apply([cob::Change {
+
                self.thread.apply([cob::Op {
                    action,
-
                    author: change.author,
-
                    clock: change.clock,
-
                    timestamp: change.timestamp,
+
                    author: op.author,
+
                    clock: op.clock,
+
                    timestamp: op.timestamp,
                }]);
            }
        }
@@ -205,7 +205,7 @@ impl<'a, 'g> IssueMut<'a, 'g> {
    }

    /// Lifecycle an issue.
-
    pub fn lifecycle<G: Signer>(&mut self, status: Status, signer: &G) -> Result<ChangeId, Error> {
+
    pub fn lifecycle<G: Signer>(&mut self, status: Status, signer: &G) -> Result<OpId, Error> {
        let action = Action::Lifecycle { status };
        self.apply("Lifecycle", action, signer)
    }
@@ -230,7 +230,7 @@ impl<'a, 'g> IssueMut<'a, 'g> {
        add: impl IntoIterator<Item = Tag>,
        remove: impl IntoIterator<Item = Tag>,
        signer: &G,
-
    ) -> Result<ChangeId, Error> {
+
    ) -> Result<OpId, Error> {
        let add = add.into_iter().collect::<Vec<_>>();
        let remove = remove.into_iter().collect::<Vec<_>>();
        let action = Action::Tag { add, remove };
@@ -244,7 +244,7 @@ impl<'a, 'g> IssueMut<'a, 'g> {
        parent: CommentId,
        body: S,
        signer: &G,
-
    ) -> Result<ChangeId, Error> {
+
    ) -> Result<OpId, Error> {
        let body = body.into();

        assert!(self.thread.comment(&parent).is_some());
@@ -262,7 +262,7 @@ impl<'a, 'g> IssueMut<'a, 'g> {
        to: CommentId,
        reaction: Reaction,
        signer: &G,
-
    ) -> Result<ChangeId, Error> {
+
    ) -> Result<OpId, Error> {
        let action = Action::Thread {
            action: thread::Action::React {
                to,
@@ -273,26 +273,26 @@ impl<'a, 'g> IssueMut<'a, 'g> {
        self.apply("React", action, signer)
    }

-
    /// Apply a change to the issue.
+
    /// Apply an op to the issue.
    pub fn apply<G: Signer>(
        &mut self,
        msg: &'static str,
        action: Action,
        signer: &G,
-
    ) -> Result<ChangeId, Error> {
+
    ) -> Result<OpId, Error> {
        let cob = self
            .store
            .update(self.id, msg, action.clone(), signer)
            .map_err(Error::Store)?;
        let clock = cob.history().clock().into();
        let timestamp = cob.history().timestamp().into();
-
        let change = Change {
+
        let op = Op {
            action,
            author: *signer.public_key(),
            clock,
            timestamp,
        };
-
        self.issue.apply(change)?;
+
        self.issue.apply(op)?;

        Ok((clock, *signer.public_key()))
    }
added radicle/src/cob/op.rs
@@ -0,0 +1,116 @@
+
use std::collections::BTreeMap;
+

+
use serde::{Deserialize, Serialize};
+
use thiserror::Error;
+

+
use radicle_cob::history::EntryWithClock;
+
use radicle_crdt::clock;
+
use radicle_crdt::clock::Lamport;
+
use radicle_crypto::{PublicKey, Signer};
+

+
/// Identifies an [`Op`].
+
pub type OpId = (Lamport, ActorId);
+
/// The author of an [`Op`].
+
pub type ActorId = PublicKey;
+

+
/// Error decoding an operation from an entry.
+
#[derive(Error, Debug)]
+
pub enum OpDecodeError {
+
    #[error("deserialization from json failed: {0}")]
+
    Deserialize(#[from] serde_json::Error),
+
}
+

+
/// The `Op` is the operation that is applied onto a state to form a CRDT.
+
///
+
/// Everything that can be done in the system is represented by an `Op`.
+
/// Operations are applied to an accumulator to yield a final state.
+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+
pub struct Op<A> {
+
    /// The action carried out by this operation.
+
    pub action: A,
+
    /// The author of the operation.
+
    pub author: ActorId,
+
    /// Lamport clock.
+
    pub clock: Lamport,
+
    /// Timestamp of this operation.
+
    pub timestamp: clock::Physical,
+
}
+

+
impl<'a: 'de, 'de, A: serde::Deserialize<'de>> TryFrom<&'a EntryWithClock> for Op<A> {
+
    type Error = OpDecodeError;
+

+
    fn try_from(entry: &'a EntryWithClock) -> Result<Self, Self::Error> {
+
        let action = serde_json::from_slice(entry.contents())?;
+

+
        Ok(Op {
+
            action,
+
            author: *entry.actor(),
+
            clock: entry.clock().into(),
+
            timestamp: entry.timestamp().into(),
+
        })
+
    }
+
}
+

+
impl<A> Op<A> {
+
    /// Get the op id.
+
    /// This uniquely identifies each operation in the CRDT.
+
    pub fn id(&self) -> OpId {
+
        (self.clock, self.author)
+
    }
+
}
+

+
/// An object that can be used to create and sign operations.
+
#[derive(Default)]
+
pub struct Actor<G, A> {
+
    pub signer: G,
+
    pub clock: Lamport,
+
    pub ops: BTreeMap<(Lamport, PublicKey), Op<A>>,
+
}
+

+
impl<G: Signer, A: Clone + Serialize> Actor<G, A> {
+
    pub fn new(signer: G) -> Self {
+
        Self {
+
            signer,
+
            clock: Lamport::default(),
+
            ops: BTreeMap::default(),
+
        }
+
    }
+

+
    pub fn receive(&mut self, ops: impl IntoIterator<Item = Op<A>>) -> Lamport {
+
        for op in ops {
+
            let clock = op.clock;
+

+
            self.ops.insert((clock, op.author), op);
+
            self.clock.merge(clock);
+
        }
+
        self.clock
+
    }
+

+
    /// Reset actor state to initial state.
+
    pub fn reset(&mut self) {
+
        self.ops.clear();
+
        self.clock = Lamport::default();
+
    }
+

+
    /// Returned an ordered list of events.
+
    pub fn timeline(&self) -> impl Iterator<Item = &Op<A>> {
+
        self.ops.values()
+
    }
+

+
    /// Create a new operation.
+
    pub fn op(&mut self, action: A) -> Op<A> {
+
        let author = *self.signer.public_key();
+
        let clock = self.clock;
+
        let timestamp = clock::Physical::now();
+
        let op = Op {
+
            action,
+
            author,
+
            clock,
+
            timestamp,
+
        };
+
        self.ops.insert((self.clock, author), op.clone());
+
        self.clock.tick();
+

+
        op
+
    }
+
}
modified radicle/src/cob/patch.rs
@@ -16,27 +16,27 @@ use crate::cob::common::{Author, Tag, Timestamp};
use crate::cob::thread;
use crate::cob::thread::CommentId;
use crate::cob::thread::Thread;
-
use crate::cob::{store, ActorId, ChangeId, ObjectId, TypeName};
+
use crate::cob::{store, ActorId, ObjectId, OpId, TypeName};
use crate::crypto::{PublicKey, Signer};
use crate::git;
use crate::prelude::*;
use crate::storage::git as storage;

-
/// The logical clock we use to order changes to patches.
+
/// The logical clock we use to order operations to patches.
pub use clock::Lamport as Clock;

/// Type name of a patch.
pub static TYPENAME: Lazy<TypeName> =
    Lazy::new(|| FromStr::from_str("xyz.radicle.patch").expect("type name is valid"));

-
/// Patch change.
-
pub type Change = crate::cob::Change<Action>;
+
/// Patch operation.
+
pub type Op = crate::cob::Op<Action>;

/// Identifier for a patch.
pub type PatchId = ObjectId;

/// Unique identifier for a patch revision.
-
pub type RevisionId = ChangeId;
+
pub type RevisionId = OpId;

/// Index of a revision in the revisions list.
pub type RevisionIx = usize;
@@ -49,10 +49,10 @@ pub enum ApplyError {
    /// This error indicates that the operations are not being applied
    /// in causal order, which is a requirement for this CRDT.
    ///
-
    /// For example, this can occur if a change references another change
+
    /// For example, this can occur if an operation references anothern operation
    /// that hasn't happened yet.
    #[error("causal dependency {0:?} missing")]
-
    Missing(ChangeId),
+
    Missing(OpId),
}

/// Error updating or creating patches.
@@ -221,36 +221,36 @@ impl Patch {
        matches!(self.status.get().get(), &Status::Archived)
    }

-
    /// Apply a list of changes to the state.
-
    pub fn apply(&mut self, changes: impl IntoIterator<Item = Change>) -> Result<(), ApplyError> {
-
        for change in changes {
-
            self.apply_one(change)?;
+
    /// Apply a list of operations to the state.
+
    pub fn apply(&mut self, ops: impl IntoIterator<Item = Op>) -> Result<(), ApplyError> {
+
        for op in ops {
+
            self.apply_one(op)?;
        }
        Ok(())
    }

-
    /// Apply a single change to the state.
-
    pub fn apply_one(&mut self, change: Change) -> Result<(), ApplyError> {
-
        let id = change.id();
-
        let author = Author::new(change.author);
-
        let timestamp = change.timestamp;
+
    /// Apply a single op to the state.
+
    pub fn apply_one(&mut self, op: Op) -> Result<(), ApplyError> {
+
        let id = op.id();
+
        let author = Author::new(op.author);
+
        let timestamp = op.timestamp;

-
        match change.action {
+
        match op.action {
            Action::Edit {
                title,
                description,
                target,
            } => {
-
                self.title.set(title, change.clock);
-
                self.description.set(description, change.clock);
-
                self.target.set(target, change.clock);
+
                self.title.set(title, op.clock);
+
                self.description.set(description, op.clock);
+
                self.target.set(target, op.clock);
            }
            Action::Tag { add, remove } => {
                for tag in add {
-
                    self.tags.insert(tag, change.clock);
+
                    self.tags.insert(tag, op.clock);
                }
                for tag in remove {
-
                    self.tags.remove(tag, change.clock);
+
                    self.tags.remove(tag, op.clock);
                }
            }
            Action::Revision { base, oid } => {
@@ -274,7 +274,7 @@ impl Patch {
            } => {
                if let Some(Redactable::Present(revision)) = self.revisions.get_mut(&revision) {
                    revision.reviews.insert(
-
                        change.author,
+
                        op.author,
                        Review::new(verdict, comment.to_owned(), inline.to_owned(), timestamp),
                    );
                } else {
@@ -285,12 +285,12 @@ impl Patch {
                if let Some(Redactable::Present(revision)) = self.revisions.get_mut(&revision) {
                    revision.merges.insert(
                        Merge {
-
                            node: change.author,
+
                            node: op.author,
                            commit,
                            timestamp,
                        }
                        .into(),
-
                        change.clock,
+
                        op.clock,
                    );
                } else {
                    return Err(ApplyError::Missing(revision));
@@ -300,10 +300,10 @@ impl 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::Change {
+
                    revision.discussion.apply([cob::Op {
                        action,
-
                        author: change.author,
-
                        clock: change.clock,
+
                        author: op.author,
+
                        clock: op.clock,
                        timestamp,
                    }]);
                } else {
@@ -326,9 +326,9 @@ impl store::FromHistory for Patch {
        history: &radicle_cob::History,
    ) -> Result<(Self, clock::Lamport), store::Error> {
        let obj = history.traverse(Self::default(), |mut acc, entry| {
-
            if let Ok(change) = Change::try_from(entry) {
-
                if let Err(err) = acc.apply([change]) {
-
                    log::warn!("Error applying change to patch state: {err}");
+
            if let Ok(op) = Op::try_from(entry) {
+
                if let Err(err) = acc.apply([op]) {
+
                    log::warn!("Error applying op to patch state: {err}");
                    return ControlFlow::Break(acc);
                }
            } else {
@@ -554,7 +554,7 @@ impl<'a, 'g> PatchMut<'a, 'g> {
        description: String,
        target: MergeTarget,
        signer: &G,
-
    ) -> Result<ChangeId, Error> {
+
    ) -> Result<OpId, Error> {
        let action = Action::Edit {
            title,
            description,
@@ -589,7 +589,7 @@ impl<'a, 'g> PatchMut<'a, 'g> {
        comment: Option<String>,
        inline: Vec<CodeComment>,
        signer: &G,
-
    ) -> Result<ChangeId, Error> {
+
    ) -> Result<OpId, Error> {
        let action = Action::Review {
            revision,
            comment,
@@ -605,7 +605,7 @@ impl<'a, 'g> PatchMut<'a, 'g> {
        revision: RevisionId,
        commit: git::Oid,
        signer: &G,
-
    ) -> Result<ChangeId, Error> {
+
    ) -> Result<OpId, Error> {
        let action = Action::Merge { revision, commit };
        self.apply("Merge revision", action, signer)
    }
@@ -617,7 +617,7 @@ impl<'a, 'g> PatchMut<'a, 'g> {
        base: impl Into<git::Oid>,
        oid: impl Into<git::Oid>,
        signer: &G,
-
    ) -> Result<ChangeId, Error> {
+
    ) -> Result<OpId, Error> {
        let description = description.into();
        let base = base.into();
        let oid = oid.into();
@@ -637,7 +637,7 @@ impl<'a, 'g> PatchMut<'a, 'g> {
        add: impl IntoIterator<Item = Tag>,
        remove: impl IntoIterator<Item = Tag>,
        signer: &G,
-
    ) -> Result<ChangeId, Error> {
+
    ) -> Result<OpId, Error> {
        let add = add.into_iter().collect::<Vec<_>>();
        let remove = remove.into_iter().collect::<Vec<_>>();
        let action = Action::Tag { add, remove };
@@ -645,26 +645,26 @@ impl<'a, 'g> PatchMut<'a, 'g> {
        self.apply("Tag", action, signer)
    }

-
    /// Apply a change to the patch.
+
    /// Apply an operation to the patch.
    pub fn apply<G: Signer>(
        &mut self,
        msg: &'static str,
        action: Action,
        signer: &G,
-
    ) -> Result<ChangeId, Error> {
+
    ) -> Result<OpId, Error> {
        let cob = self
            .store
            .update(self.id, msg, action.clone(), signer)
            .map_err(Error::Store)?;
        let clock = cob.history().clock().into();
        let timestamp = cob.history().timestamp().into();
-
        let change = Change {
+
        let op = Op {
            action,
            author: *signer.public_key(),
            clock,
            timestamp,
        };
-
        self.patch.apply_one(change)?;
+
        self.patch.apply_one(op)?;

        Ok((clock, *signer.public_key()))
    }
@@ -781,13 +781,13 @@ mod test {
    use quickcheck::{Arbitrary, TestResult};

    use super::*;
-
    use crate::cob::change::{Actor, ActorId};
+
    use crate::cob::op::{Actor, ActorId};
    use crate::crypto::test::signer::MockSigner;
    use crate::test;

    #[derive(Clone)]
    struct Changes<const N: usize> {
-
        permutations: [Vec<Change>; N],
+
        permutations: [Vec<Op>; N],
    }

    impl<const N: usize> std::fmt::Debug for Changes<N> {
@@ -805,7 +805,7 @@ mod test {

    impl<const N: usize> Arbitrary for Changes<N> {
        fn arbitrary(g: &mut quickcheck::Gen) -> Self {
-
            type State = (clock::Lamport, Vec<ChangeId>, Vec<Tag>);
+
            type State = (clock::Lamport, Vec<OpId>, Vec<Tag>);

            let author = ActorId::from([0; 32]);
            let rng = fastrand::Rng::with_seed(u64::arbitrary(g));
@@ -875,11 +875,11 @@ mod test {
                });

            let mut changes = Vec::new();
-
            let mut permutations: [Vec<Change>; N] = array::from_fn(|_| Vec::new());
+
            let mut permutations: [Vec<Op>; N] = array::from_fn(|_| Vec::new());
            let timestamp = Timestamp::now() + rng.u64(..60);

            for (clock, action) in gen.take(g.size()) {
-
                changes.push(Change {
+
                changes.push(Op {
                    action,
                    author,
                    clock,
@@ -1054,15 +1054,15 @@ mod test {
        let mut alice = Actor::<_, Action>::new(MockSigner::default());
        let mut patch = Patch::default();

-
        let a1 = alice.change(Action::Revision { base, oid });
-
        let a2 = alice.change(Action::Redact { revision: a1.id() });
-
        let a3 = alice.change(Action::Review {
+
        let a1 = alice.op(Action::Revision { base, oid });
+
        let a2 = alice.op(Action::Redact { revision: a1.id() });
+
        let a3 = alice.op(Action::Review {
            revision: a1.id(),
            comment: None,
            verdict: Some(Verdict::Accept),
            inline: vec![],
        });
-
        let a4 = alice.change(Action::Merge {
+
        let a4 = alice.op(Action::Merge {
            revision: a1.id(),
            commit: oid,
        });
@@ -1085,8 +1085,8 @@ mod test {
        let mut p1 = Patch::default();
        let mut p2 = Patch::default();

-
        let a1 = alice.change(Action::Revision { base, oid });
-
        let a2 = alice.change(Action::Redact { revision: a1.id() });
+
        let a1 = alice.op(Action::Revision { base, oid });
+
        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();
modified radicle/src/cob/thread.rs
@@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize};
use crate::cob;
use crate::cob::common::{Reaction, Timestamp};
use crate::cob::store;
-
use crate::cob::{ActorId, Change, ChangeId, History, TypeName};
+
use crate::cob::{ActorId, History, Op, OpId, TypeName};
use crate::crypto::Signer;

use crdt::clock::Lamport;
@@ -24,7 +24,7 @@ pub static TYPENAME: Lazy<TypeName> =
    Lazy::new(|| FromStr::from_str("xyz.radicle.thread").expect("type name is valid"));

/// Identifies a comment.
-
pub type CommentId = ChangeId;
+
pub type CommentId = OpId;

/// A comment on a discussion thread.
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
@@ -32,14 +32,14 @@ pub struct Comment {
    /// The comment body.
    pub body: String,
    /// Thread or comment this is a reply to.
-
    pub reply_to: Option<ChangeId>,
+
    pub reply_to: Option<OpId>,
    /// When the comment was authored.
    pub timestamp: Timestamp,
}

impl Comment {
    /// Create a new comment.
-
    pub fn new(body: String, reply_to: Option<ChangeId>, timestamp: Timestamp) -> Self {
+
    pub fn new(body: String, reply_to: Option<OpId>, timestamp: Timestamp) -> Self {
        Self {
            body,
            reply_to,
@@ -66,13 +66,13 @@ pub enum Action {
        /// Comment body.
        body: String,
        /// Another comment this is a reply to.
-
        reply_to: Option<ChangeId>,
+
        reply_to: Option<OpId>,
    },
    /// Redact a change. Not all changes can be redacted.
-
    Redact { id: ChangeId },
+
    Redact { id: OpId },
    /// React to a change.
    React {
-
        to: ChangeId,
+
        to: OpId,
        reaction: Reaction,
        active: bool,
    },
@@ -96,7 +96,7 @@ impl store::FromHistory for Thread {

    fn from_history(history: &History) -> Result<(Self, Lamport), store::Error> {
        let obj = history.traverse(Thread::default(), |mut acc, entry| {
-
            if let Ok(change) = Change::try_from(entry) {
+
            if let Ok(change) = Op::try_from(entry) {
                acc.apply([change]);
                ControlFlow::Continue(acc)
            } else {
@@ -168,7 +168,7 @@ impl Thread {
            .map(|(a, r)| (a, r))
    }

-
    pub fn apply(&mut self, changes: impl IntoIterator<Item = Change<Action>>) {
+
    pub fn apply(&mut self, changes: impl IntoIterator<Item = Op<Action>>) {
        for change in changes.into_iter() {
            let id = change.id();

@@ -257,16 +257,16 @@ impl<G: Signer> Actor<G> {
    }

    /// Create a new comment.
-
    pub fn comment(&mut self, body: &str, reply_to: Option<ChangeId>) -> Change<Action> {
-
        self.change(Action::Comment {
+
    pub fn comment(&mut self, body: &str, reply_to: Option<OpId>) -> Op<Action> {
+
        self.op(Action::Comment {
            body: String::from(body),
            reply_to,
        })
    }

    /// Create a new redaction.
-
    pub fn redact(&mut self, id: ChangeId) -> Change<Action> {
-
        self.change(Action::Redact { id })
+
    pub fn redact(&mut self, id: OpId) -> Op<Action> {
+
        self.op(Action::Redact { id })
    }
}

@@ -299,7 +299,7 @@ mod tests {

    #[derive(Clone)]
    struct Changes<const N: usize> {
-
        permutations: [Vec<Change<Action>>; N],
+
        permutations: [Vec<Op<Action>>; N],
    }

    impl<const N: usize> std::fmt::Debug for Changes<N> {
@@ -320,7 +320,7 @@ mod tests {
            let author = ActorId::from([0; 32]);
            let rng = fastrand::Rng::with_seed(u64::arbitrary(g));
            let gen =
-
                WeightedGenerator::<(Lamport, Action), (Lamport, Vec<ChangeId>)>::new(rng.clone())
+
                WeightedGenerator::<(Lamport, Action), (Lamport, Vec<OpId>)>::new(rng.clone())
                    .variant(3, |(clock, changes), rng| {
                        changes.push((clock.tick(), author));

@@ -357,11 +357,11 @@ mod tests {
                    });

            let mut changes = Vec::new();
-
            let mut permutations: [Vec<Change<Action>>; N] = array::from_fn(|_| Vec::new());
+
            let mut permutations: [Vec<Op<Action>>; N] = array::from_fn(|_| Vec::new());
            let timestamp = Timestamp::now() + rng.u64(..60);

            for (clock, action) in gen.take(g.size().min(8)) {
-
                changes.push(Change {
+
                changes.push(Op {
                    action,
                    author,
                    clock,
@@ -493,9 +493,9 @@ mod tests {
        eve.receive([a2.clone()]);
        bob.receive([a2.clone()]);

-
        assert_eq!(alice.changes.len(), 7);
-
        assert_eq!(bob.changes.len(), 7);
-
        assert_eq!(eve.changes.len(), 7);
+
        assert_eq!(alice.ops.len(), 7);
+
        assert_eq!(bob.ops.len(), 7);
+
        assert_eq!(eve.ops.len(), 7);

        assert_eq!(
            bob.timeline().collect::<Vec<_>>(),