Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
cob: Move action handling into its own function
cloudhead committed 2 years ago
commit 5fc6d960766c89051b360992a6668f7ddfe1cc3f
parent bf8cee2446a7c9933d8459ff75b703dcf52f3f38
2 files changed +402 -373
modified radicle/src/cob/issue.rs
@@ -12,8 +12,9 @@ use crate::cob::store::Transaction;
use crate::cob::store::{FromHistory as _, HistoryAction};
use crate::cob::thread;
use crate::cob::thread::{CommentId, Thread};
-
use crate::cob::{store, Embed, EntryId, ObjectId, TypeName};
+
use crate::cob::{store, ActorId, Embed, EntryId, ObjectId, TypeName};
use crate::crypto::Signer;
+
use crate::git;
use crate::prelude::{Did, ReadRepository};
use crate::storage::WriteRepository;

@@ -139,51 +140,9 @@ impl store::FromHistory for Issue {
        }
    }

-
    fn apply<R: ReadRepository>(&mut self, op: Op, _repo: &R) -> Result<(), Error> {
+
    fn apply<R: ReadRepository>(&mut self, op: Op, repo: &R) -> Result<(), Error> {
        for action in op.actions {
-
            match action {
-
                Action::Assign { assignees } => {
-
                    self.assignees = BTreeSet::from_iter(assignees);
-
                }
-
                Action::Edit { title } => {
-
                    self.title = title;
-
                }
-
                Action::Lifecycle { state } => {
-
                    self.state = state;
-
                }
-
                Action::Label { labels } => {
-
                    self.labels = BTreeSet::from_iter(labels);
-
                }
-
                Action::Comment {
-
                    body,
-
                    reply_to,
-
                    embeds,
-
                } => {
-
                    thread::comment(
-
                        &mut self.thread,
-
                        op.id,
-
                        op.author,
-
                        op.timestamp,
-
                        body,
-
                        reply_to,
-
                        None,
-
                        embeds,
-
                    )?;
-
                }
-
                Action::CommentEdit { id, body, embeds } => {
-
                    thread::edit(&mut self.thread, op.id, id, op.timestamp, body, embeds)?;
-
                }
-
                Action::CommentRedact { id } => {
-
                    thread::redact(&mut self.thread, op.id, id)?;
-
                }
-
                Action::CommentReact {
-
                    id,
-
                    reaction,
-
                    active,
-
                } => {
-
                    thread::react(&mut self.thread, op.id, op.author, id, reaction, active)?;
-
                }
-
            }
+
            self.action(action, op.id, op.author, op.timestamp, op.identity, repo)?;
        }
        Ok(())
    }
@@ -240,6 +199,64 @@ impl Issue {
    }
}

+
impl Issue {
+
    /// Apply a single action to the issue.
+
    fn action<R: ReadRepository>(
+
        &mut self,
+
        action: Action,
+
        entry: EntryId,
+
        author: ActorId,
+
        timestamp: Timestamp,
+
        _identity: git::Oid,
+
        _repo: &R,
+
    ) -> Result<(), Error> {
+
        match action {
+
            Action::Assign { assignees } => {
+
                self.assignees = BTreeSet::from_iter(assignees);
+
            }
+
            Action::Edit { title } => {
+
                self.title = title;
+
            }
+
            Action::Lifecycle { state } => {
+
                self.state = state;
+
            }
+
            Action::Label { labels } => {
+
                self.labels = BTreeSet::from_iter(labels);
+
            }
+
            Action::Comment {
+
                body,
+
                reply_to,
+
                embeds,
+
            } => {
+
                thread::comment(
+
                    &mut self.thread,
+
                    entry,
+
                    author,
+
                    timestamp,
+
                    body,
+
                    reply_to,
+
                    None,
+
                    embeds,
+
                )?;
+
            }
+
            Action::CommentEdit { id, body, embeds } => {
+
                thread::edit(&mut self.thread, entry, id, timestamp, body, embeds)?;
+
            }
+
            Action::CommentRedact { id } => {
+
                thread::redact(&mut self.thread, entry, id)?;
+
            }
+
            Action::CommentReact {
+
                id,
+
                reaction,
+
                active,
+
            } => {
+
                thread::react(&mut self.thread, entry, author, id, reaction, active)?;
+
            }
+
        }
+
        Ok(())
+
    }
+
}
+

impl Deref for Issue {
    type Target = Thread;

modified radicle/src/cob/patch.rs
@@ -525,381 +525,393 @@ impl Patch {
    }
}

-
impl store::FromHistory for Patch {
-
    type Action = Action;
-
    type Error = Error;
-

-
    fn type_name() -> &'static TypeName {
-
        &TYPENAME
-
    }
-

-
    fn validate(&self) -> Result<(), Self::Error> {
-
        if self.revisions.is_empty() {
-
            return Err(Error::Validate("no revisions found"));
-
        }
-
        if self.title().is_empty() {
-
            return Err(Error::Validate("empty title"));
-
        }
-
        Ok(())
-
    }
-

-
    fn apply<R: ReadRepository>(&mut self, op: Op, repo: &R) -> Result<(), Error> {
-
        let author = Author::new(op.author);
-
        let timestamp = op.timestamp;
-

-
        debug_assert!(!self.timeline.contains(&op.id));
-

-
        self.timeline.push(op.id);
-

-
        for action in op.actions {
-
            match action {
-
                Action::Edit { title, target } => {
-
                    self.title = title;
-
                    self.target = target;
+
impl Patch {
+
    /// Apply a single action to the patch.
+
    fn action<R: ReadRepository>(
+
        &mut self,
+
        action: Action,
+
        entry: EntryId,
+
        author: ActorId,
+
        timestamp: Timestamp,
+
        identity: git::Oid,
+
        repo: &R,
+
    ) -> Result<(), Error> {
+
        match action {
+
            Action::Edit { title, target } => {
+
                self.title = title;
+
                self.target = target;
+
            }
+
            Action::Lifecycle { state } => match state {
+
                Lifecycle::Open => {
+
                    self.state = State::Open { conflicts: vec![] };
                }
-
                Action::Lifecycle { state } => match state {
-
                    Lifecycle::Open => {
-
                        self.state = State::Open { conflicts: vec![] };
-
                    }
-
                    Lifecycle::Draft => {
-
                        self.state = State::Draft;
-
                    }
-
                    Lifecycle::Archived => {
-
                        self.state = State::Archived;
-
                    }
-
                },
-
                Action::Label { labels } => {
-
                    self.labels = BTreeSet::from_iter(labels);
+
                Lifecycle::Draft => {
+
                    self.state = State::Draft;
                }
-
                Action::Assign { assignees } => {
-
                    self.assignees = BTreeSet::from_iter(assignees.into_iter().map(ActorId::from));
+
                Lifecycle::Archived => {
+
                    self.state = State::Archived;
                }
-
                Action::RevisionEdit {
-
                    revision,
-
                    description,
-
                } => {
-
                    if let Some(redactable) = self.revisions.get_mut(&revision) {
-
                        // If the revision was redacted concurrently, there's nothing to do.
-
                        if let Some(revision) = redactable {
-
                            revision.description = description;
-
                        }
-
                    } else {
-
                        return Err(Error::Missing(revision.into_inner()));
+
            },
+
            Action::Label { labels } => {
+
                self.labels = BTreeSet::from_iter(labels);
+
            }
+
            Action::Assign { assignees } => {
+
                self.assignees = BTreeSet::from_iter(assignees.into_iter().map(ActorId::from));
+
            }
+
            Action::RevisionEdit {
+
                revision,
+
                description,
+
            } => {
+
                if let Some(redactable) = self.revisions.get_mut(&revision) {
+
                    // If the revision was redacted concurrently, there's nothing to do.
+
                    if let Some(revision) = redactable {
+
                        revision.description = description;
                    }
+
                } else {
+
                    return Err(Error::Missing(revision.into_inner()));
                }
-
                Action::ReviewEdit {
-
                    review,
-
                    summary,
-
                    verdict,
-
                } => {
-
                    let Some(Some((revision, author))) =
+
            }
+
            Action::ReviewEdit {
+
                review,
+
                summary,
+
                verdict,
+
            } => {
+
                let Some(Some((revision, author))) =
                        self.reviews.get(&review) else {
                            return Err(Error::Missing(review.into_inner()));
                    };
-
                    let Some(rev) = self.revisions.get_mut(revision) else {
+
                let Some(rev) = self.revisions.get_mut(revision) else {
                        return Err(Error::Missing(revision.into_inner()));
                    };
-
                    // If the revision was redacted concurrently, there's nothing to do.
-
                    // Likewise, if the review was redacted concurrently, there's nothing to do.
-
                    if let Some(rev) = rev {
-
                        let Some(review) = rev.reviews.get_mut(author) else {
+
                // If the revision was redacted concurrently, there's nothing to do.
+
                // Likewise, if the review was redacted concurrently, there's nothing to do.
+
                if let Some(rev) = rev {
+
                    let Some(review) = rev.reviews.get_mut(author) else {
                            return Err(Error::Missing(review.into_inner()));
                        };
-
                        if let Some(review) = review {
-
                            review.summary = summary;
-
                            review.verdict = verdict;
-
                        }
+
                    if let Some(review) = review {
+
                        review.summary = summary;
+
                        review.verdict = verdict;
                    }
                }
-
                Action::Revision {
-
                    description,
-
                    base,
-
                    oid,
-
                    resolves,
-
                } => {
-
                    debug_assert!(!self.revisions.contains_key(&op.id));
-

-
                    self.revisions.insert(
-
                        RevisionId(op.id),
-
                        Some(Revision::new(
-
                            author.clone(),
-
                            description,
-
                            base,
-
                            oid,
-
                            timestamp,
-
                            resolves,
-
                        )),
-
                    );
-
                }
-
                Action::RevisionReact {
-
                    revision,
-
                    reaction,
-
                    active,
-
                    location,
-
                } => {
-
                    if let Some(revision) = lookup::revision(self, &revision)? {
-
                        let key = (op.author, reaction);
-
                        let reactions = revision.reactions.entry(location).or_default();
-

-
                        if active {
-
                            reactions.insert(key);
-
                        } else {
-
                            reactions.remove(&key);
-
                        }
+
            }
+
            Action::Revision {
+
                description,
+
                base,
+
                oid,
+
                resolves,
+
            } => {
+
                debug_assert!(!self.revisions.contains_key(&entry));
+

+
                self.revisions.insert(
+
                    RevisionId(entry),
+
                    Some(Revision::new(
+
                        author.into(),
+
                        description,
+
                        base,
+
                        oid,
+
                        timestamp,
+
                        resolves,
+
                    )),
+
                );
+
            }
+
            Action::RevisionReact {
+
                revision,
+
                reaction,
+
                active,
+
                location,
+
            } => {
+
                if let Some(revision) = lookup::revision(self, &revision)? {
+
                    let key = (author, reaction);
+
                    let reactions = revision.reactions.entry(location).or_default();
+

+
                    if active {
+
                        reactions.insert(key);
+
                    } else {
+
                        reactions.remove(&key);
                    }
                }
-
                Action::RevisionRedact { revision } => {
-
                    // Redactions must have observed a revision to be valid.
-
                    if let Some(r) = self.revisions.get_mut(&revision) {
-
                        // If the revision has already been merged, ignore the redaction. We
-
                        // don't want to redact merged revisions.
-
                        if self.merges.values().any(|m| m.revision == revision) {
-
                            return Ok(());
-
                        }
-
                        *r = None;
-
                    } else {
-
                        return Err(Error::Missing(revision.into_inner()));
+
            }
+
            Action::RevisionRedact { revision } => {
+
                // Redactions must have observed a revision to be valid.
+
                if let Some(r) = self.revisions.get_mut(&revision) {
+
                    // If the revision has already been merged, ignore the redaction. We
+
                    // don't want to redact merged revisions.
+
                    if self.merges.values().any(|m| m.revision == revision) {
+
                        return Ok(());
                    }
+
                    *r = None;
+
                } else {
+
                    return Err(Error::Missing(revision.into_inner()));
                }
-
                Action::Review {
-
                    revision,
-
                    ref summary,
-
                    verdict,
-
                    labels,
-
                } => {
-
                    let Some(rev) = self.revisions.get_mut(&revision) else {
+
            }
+
            Action::Review {
+
                revision,
+
                ref summary,
+
                verdict,
+
                labels,
+
            } => {
+
                let Some(rev) = self.revisions.get_mut(&revision) else {
                        return Err(Error::Missing(revision.into_inner()));
                    };
-
                    if let Some(rev) = rev {
-
                        // Nb. Applying two reviews by the same author is not allowed and
-
                        // results in the review being redacted.
-
                        rev.reviews.insert(
-
                            op.author,
-
                            Some(Review::new(verdict, summary.to_owned(), labels, timestamp)),
-
                        );
-
                        // Update reviews index.
-
                        self.reviews
-
                            .insert(ReviewId(op.id), Some((revision, op.author)));
-
                    }
+
                if let Some(rev) = rev {
+
                    // Nb. Applying two reviews by the same author is not allowed and
+
                    // results in the review being redacted.
+
                    rev.reviews.insert(
+
                        author,
+
                        Some(Review::new(verdict, summary.to_owned(), labels, timestamp)),
+
                    );
+
                    // Update reviews index.
+
                    self.reviews
+
                        .insert(ReviewId(entry), Some((revision, author)));
                }
-
                Action::ReviewCommentReact {
-
                    review,
-
                    comment,
-
                    reaction,
-
                    active,
-
                } => {
-
                    if let Some(review) = lookup::review(self, &review)? {
-
                        thread::react(
-
                            &mut review.comments,
-
                            op.id,
-
                            author.id.into(),
-
                            comment,
-
                            reaction,
-
                            active,
-
                        )?;
-
                    }
+
            }
+
            Action::ReviewCommentReact {
+
                review,
+
                comment,
+
                reaction,
+
                active,
+
            } => {
+
                if let Some(review) = lookup::review(self, &review)? {
+
                    thread::react(
+
                        &mut review.comments,
+
                        entry,
+
                        author,
+
                        comment,
+
                        reaction,
+
                        active,
+
                    )?;
                }
-
                Action::ReviewCommentRedact { review, comment } => {
-
                    if let Some(review) = lookup::review(self, &review)? {
-
                        thread::redact(&mut review.comments, op.id, comment)?;
-
                    }
+
            }
+
            Action::ReviewCommentRedact { review, comment } => {
+
                if let Some(review) = lookup::review(self, &review)? {
+
                    thread::redact(&mut review.comments, entry, comment)?;
                }
-
                Action::ReviewCommentEdit {
-
                    review,
-
                    comment,
-
                    body,
-
                } => {
-
                    if let Some(review) = lookup::review(self, &review)? {
-
                        thread::edit(
-
                            &mut review.comments,
-
                            op.id,
-
                            comment,
-
                            timestamp,
-
                            body,
-
                            vec![],
-
                        )?;
-
                    }
+
            }
+
            Action::ReviewCommentEdit {
+
                review,
+
                comment,
+
                body,
+
            } => {
+
                if let Some(review) = lookup::review(self, &review)? {
+
                    thread::edit(
+
                        &mut review.comments,
+
                        entry,
+
                        comment,
+
                        timestamp,
+
                        body,
+
                        vec![],
+
                    )?;
                }
-
                Action::ReviewCommentResolve { review, comment } => {
-
                    if let Some(review) = lookup::review(self, &review)? {
-
                        thread::resolve(&mut review.comments, op.id, comment)?;
-
                    }
+
            }
+
            Action::ReviewCommentResolve { review, comment } => {
+
                if let Some(review) = lookup::review(self, &review)? {
+
                    thread::resolve(&mut review.comments, entry, comment)?;
                }
-
                Action::ReviewCommentUnresolve { review, comment } => {
-
                    if let Some(review) = lookup::review(self, &review)? {
-
                        thread::unresolve(&mut review.comments, op.id, comment)?;
-
                    }
+
            }
+
            Action::ReviewCommentUnresolve { review, comment } => {
+
                if let Some(review) = lookup::review(self, &review)? {
+
                    thread::unresolve(&mut review.comments, entry, comment)?;
                }
-
                Action::ReviewComment {
-
                    review,
-
                    body,
-
                    location,
-
                    reply_to,
-
                } => {
-
                    if let Some(review) = lookup::review(self, &review)? {
-
                        thread::comment(
-
                            &mut review.comments,
-
                            op.id,
-
                            author.id.into(),
-
                            timestamp,
-
                            body,
-
                            reply_to,
-
                            location,
-
                            vec![],
-
                        )?;
-
                    }
+
            }
+
            Action::ReviewComment {
+
                review,
+
                body,
+
                location,
+
                reply_to,
+
            } => {
+
                if let Some(review) = lookup::review(self, &review)? {
+
                    thread::comment(
+
                        &mut review.comments,
+
                        entry,
+
                        author,
+
                        timestamp,
+
                        body,
+
                        reply_to,
+
                        location,
+
                        vec![],
+
                    )?;
                }
-
                Action::ReviewRedact { review } => {
-
                    // Redactions must have observed a review to be valid.
-
                    let Some(locator) = self.reviews.get_mut(&review) else {
+
            }
+
            Action::ReviewRedact { review } => {
+
                // Redactions must have observed a review to be valid.
+
                let Some(locator) = self.reviews.get_mut(&review) else {
                        return Err(Error::Missing(review.into_inner()));
                    };
-
                    // If the review is already redacted, do nothing.
-
                    let Some((revision, reviewer)) = locator else {
+
                // If the review is already redacted, do nothing.
+
                let Some((revision, reviewer)) = locator else {
                        return Ok(());
                    };
-
                    // The revision must have existed at some point.
-
                    let Some(redactable) = self.revisions.get_mut(revision) else {
+
                // The revision must have existed at some point.
+
                let Some(redactable) = self.revisions.get_mut(revision) else {
                        return Err(Error::Missing(revision.into_inner()));
                    };
-
                    // But it could be redacted.
-
                    let Some(revision) = redactable else {
+
                // But it could be redacted.
+
                let Some(revision) = redactable else {
                        return Ok(());
                    };
-
                    // The review must have existed as well.
-
                    let Some(review) = revision.reviews.get_mut(reviewer) else {
+
                // The review must have existed as well.
+
                let Some(review) = revision.reviews.get_mut(reviewer) else {
                        return Err(Error::Missing(review.into_inner()));
                    };
-
                    // Set both the review and the locator in the review index to redacted.
-
                    *review = None;
-
                    *locator = None;
-
                }
-
                Action::Merge { revision, commit } => {
-
                    // If the revision was redacted before the merge, ignore the merge.
-
                    if lookup::revision(self, &revision)?.is_none() {
-
                        return Ok(());
-
                    };
-
                    let doc = repo.identity_doc_at(op.identity)?.verified()?;
-

-
                    match self.target() {
-
                        MergeTarget::Delegates => {
-
                            if !doc.is_delegate(&op.author) {
-
                                return Err(Error::InvalidMerge(op.id));
-
                            }
-
                            let proj = doc.project()?;
-
                            let branch = git::refs::branch(proj.default_branch());
-

-
                            // Nb. We don't return an error in case the merge commit is not an
-
                            // ancestor of the default branch. The default branch can change
-
                            // *after* the merge action is created, which is out of the control
-
                            // of the merge author. We simply skip it, which allows archiving in
-
                            // case of a rebase off the master branch, or a redaction of the
-
                            // merge.
-
                            let Ok(head) = repo.reference_oid(&op.author, &branch) else {
-
                                continue;
-
                            };
-
                            if commit != head && !repo.is_ancestor_of(commit, head)? {
-
                                continue;
-
                            }
-
                        }
-
                    }
-
                    self.merges.insert(
-
                        op.author,
-
                        Merge {
-
                            revision,
-
                            commit,
-
                            timestamp,
-
                        },
-
                    );
-

-
                    let mut merges = self.merges.iter().fold(
-
                        HashMap::<(RevisionId, git::Oid), usize>::new(),
-
                        |mut acc, (_, merge)| {
-
                            *acc.entry((merge.revision, merge.commit)).or_default() += 1;
-
                            acc
-
                        },
-
                    );
-
                    // Discard revisions that weren't merged by a threshold of delegates.
-
                    merges.retain(|_, count| *count >= doc.threshold);
-

-
                    match merges.into_keys().collect::<Vec<_>>().as_slice() {
-
                        [] => {
-
                            // None of the revisions met the quorum.
-
                        }
-
                        [(revision, commit)] => {
-
                            // Patch is merged.
-
                            self.state = State::Merged {
-
                                revision: *revision,
-
                                commit: *commit,
-
                            };
+
                // Set both the review and the locator in the review index to redacted.
+
                *review = None;
+
                *locator = None;
+
            }
+
            Action::Merge { revision, commit } => {
+
                // If the revision was redacted before the merge, ignore the merge.
+
                if lookup::revision(self, &revision)?.is_none() {
+
                    return Ok(());
+
                };
+
                let doc = repo.identity_doc_at(identity)?.verified()?;
+

+
                match self.target() {
+
                    MergeTarget::Delegates => {
+
                        if !doc.is_delegate(&author) {
+
                            return Err(Error::InvalidMerge(entry));
                        }
-
                        revisions => {
-
                            // More than one revision met the quorum.
-
                            self.state = State::Open {
-
                                conflicts: revisions.to_vec(),
-
                            };
+
                        let proj = doc.project()?;
+
                        let branch = git::refs::branch(proj.default_branch());
+

+
                        // Nb. We don't return an error in case the merge commit is not an
+
                        // ancestor of the default branch. The default branch can change
+
                        // *after* the merge action is created, which is out of the control
+
                        // of the merge author. We simply skip it, which allows archiving in
+
                        // case of a rebase off the master branch, or a redaction of the
+
                        // merge.
+
                        let Ok(head) = repo.reference_oid(&author, &branch) else {
+
                            return Ok(());
+
                        };
+
                        if commit != head && !repo.is_ancestor_of(commit, head)? {
+
                            return Ok(());
                        }
                    }
                }
-

-
                Action::RevisionComment {
-
                    revision,
-
                    body,
-
                    reply_to,
-
                    ..
-
                } => {
-
                    if let Some(revision) = lookup::revision(self, &revision)? {
-
                        thread::comment(
-
                            &mut revision.discussion,
-
                            op.id,
-
                            op.author,
-
                            op.timestamp,
-
                            body,
-
                            reply_to,
-
                            None,
-
                            vec![],
-
                        )?;
+
                self.merges.insert(
+
                    author,
+
                    Merge {
+
                        revision,
+
                        commit,
+
                        timestamp,
+
                    },
+
                );
+

+
                let mut merges = self.merges.iter().fold(
+
                    HashMap::<(RevisionId, git::Oid), usize>::new(),
+
                    |mut acc, (_, merge)| {
+
                        *acc.entry((merge.revision, merge.commit)).or_default() += 1;
+
                        acc
+
                    },
+
                );
+
                // Discard revisions that weren't merged by a threshold of delegates.
+
                merges.retain(|_, count| *count >= doc.threshold);
+

+
                match merges.into_keys().collect::<Vec<_>>().as_slice() {
+
                    [] => {
+
                        // None of the revisions met the quorum.
                    }
-
                }
-
                Action::RevisionCommentEdit {
-
                    revision,
-
                    comment,
-
                    body,
-
                } => {
-
                    if let Some(revision) = lookup::revision(self, &revision)? {
-
                        thread::edit(
-
                            &mut revision.discussion,
-
                            op.id,
-
                            comment,
-
                            op.timestamp,
-
                            body,
-
                            vec![],
-
                        )?;
+
                    [(revision, commit)] => {
+
                        // Patch is merged.
+
                        self.state = State::Merged {
+
                            revision: *revision,
+
                            commit: *commit,
+
                        };
                    }
-
                }
-
                Action::RevisionCommentRedact { revision, comment } => {
-
                    if let Some(revision) = lookup::revision(self, &revision)? {
-
                        thread::redact(&mut revision.discussion, op.id, comment)?;
+
                    revisions => {
+
                        // More than one revision met the quorum.
+
                        self.state = State::Open {
+
                            conflicts: revisions.to_vec(),
+
                        };
                    }
                }
-
                Action::RevisionCommentReact {
-
                    revision,
-
                    comment,
-
                    reaction,
-
                    active,
-
                } => {
-
                    if let Some(revision) = lookup::revision(self, &revision)? {
-
                        thread::react(
-
                            &mut revision.discussion,
-
                            op.id,
-
                            op.author,
-
                            comment,
-
                            reaction,
-
                            active,
-
                        )?;
-
                    }
+
            }
+

+
            Action::RevisionComment {
+
                revision,
+
                body,
+
                reply_to,
+
                ..
+
            } => {
+
                if let Some(revision) = lookup::revision(self, &revision)? {
+
                    thread::comment(
+
                        &mut revision.discussion,
+
                        entry,
+
                        author,
+
                        timestamp,
+
                        body,
+
                        reply_to,
+
                        None,
+
                        vec![],
+
                    )?;
+
                }
+
            }
+
            Action::RevisionCommentEdit {
+
                revision,
+
                comment,
+
                body,
+
            } => {
+
                if let Some(revision) = lookup::revision(self, &revision)? {
+
                    thread::edit(
+
                        &mut revision.discussion,
+
                        entry,
+
                        comment,
+
                        timestamp,
+
                        body,
+
                        vec![],
+
                    )?;
                }
            }
+
            Action::RevisionCommentRedact { revision, comment } => {
+
                if let Some(revision) = lookup::revision(self, &revision)? {
+
                    thread::redact(&mut revision.discussion, entry, comment)?;
+
                }
+
            }
+
            Action::RevisionCommentReact {
+
                revision,
+
                comment,
+
                reaction,
+
                active,
+
            } => {
+
                if let Some(revision) = lookup::revision(self, &revision)? {
+
                    thread::react(
+
                        &mut revision.discussion,
+
                        entry,
+
                        author,
+
                        comment,
+
                        reaction,
+
                        active,
+
                    )?;
+
                }
+
            }
+
        }
+
        Ok(())
+
    }
+
}
+

+
impl store::FromHistory for Patch {
+
    type Action = Action;
+
    type Error = Error;
+

+
    fn type_name() -> &'static TypeName {
+
        &TYPENAME
+
    }
+

+
    fn validate(&self) -> Result<(), Self::Error> {
+
        if self.revisions.is_empty() {
+
            return Err(Error::Validate("no revisions found"));
+
        }
+
        if self.title().is_empty() {
+
            return Err(Error::Validate("empty title"));
+
        }
+
        Ok(())
+
    }
+

+
    fn apply<R: ReadRepository>(&mut self, op: Op, repo: &R) -> Result<(), Error> {
+
        debug_assert!(!self.timeline.contains(&op.id));
+
        self.timeline.push(op.id);
+

+
        for action in op.actions {
+
            self.action(action, op.id, op.author, op.timestamp, op.identity, repo)?;
        }
        Ok(())
    }