radicle/cob/
patch.rs

1pub mod cache;
2
3use std::collections::btree_map;
4use std::collections::{BTreeMap, BTreeSet, HashMap};
5use std::fmt;
6use std::ops::Deref;
7use std::str::FromStr;
8
9use amplify::Wrapper;
10use nonempty::NonEmpty;
11use once_cell::sync::Lazy;
12use serde::{Deserialize, Serialize};
13use storage::{HasRepoId, RepositoryError};
14use thiserror::Error;
15
16use crate::cob;
17use crate::cob::common::{Author, Authorization, CodeLocation, Label, Reaction, Timestamp};
18use crate::cob::store::Transaction;
19use crate::cob::store::{Cob, CobAction};
20use crate::cob::thread;
21use crate::cob::thread::Thread;
22use crate::cob::thread::{Comment, CommentId, Edit, Reactions};
23use crate::cob::{op, store, ActorId, Embed, EntryId, ObjectId, TypeName, Uri};
24use crate::crypto::PublicKey;
25use crate::git;
26use crate::identity::doc::{DocAt, DocError};
27use crate::identity::PayloadError;
28use crate::node::device::Device;
29use crate::prelude::*;
30use crate::storage;
31
32pub use cache::Cache;
33
34/// Type name of a patch.
35pub static TYPENAME: Lazy<TypeName> =
36    Lazy::new(|| FromStr::from_str("xyz.radicle.patch").expect("type name is valid"));
37
38/// Patch operation.
39pub type Op = cob::Op<Action>;
40
41/// Identifier for a patch.
42pub type PatchId = ObjectId;
43
44/// Unique identifier for a patch revision.
45#[derive(
46    Wrapper,
47    Debug,
48    Clone,
49    Copy,
50    Serialize,
51    Deserialize,
52    PartialEq,
53    Eq,
54    PartialOrd,
55    Ord,
56    Hash,
57    From,
58    Display,
59)]
60#[display(inner)]
61#[wrap(Deref)]
62pub struct RevisionId(EntryId);
63
64/// Unique identifier for a patch review.
65#[derive(
66    Wrapper,
67    Debug,
68    Clone,
69    Copy,
70    Serialize,
71    Deserialize,
72    PartialEq,
73    Eq,
74    PartialOrd,
75    Ord,
76    Hash,
77    From,
78    Display,
79)]
80#[display(inner)]
81#[wrapper(Deref)]
82pub struct ReviewId(EntryId);
83
84/// Index of a revision in the revisions list.
85pub type RevisionIx = usize;
86
87/// Error applying an operation onto a state.
88#[derive(Debug, Error)]
89pub enum Error {
90    /// Causal dependency missing.
91    ///
92    /// This error indicates that the operations are not being applied
93    /// in causal order, which is a requirement for this CRDT.
94    ///
95    /// For example, this can occur if an operation references another operation
96    /// that hasn't happened yet.
97    #[error("causal dependency {0:?} missing")]
98    Missing(EntryId),
99    /// Error applying an op to the patch thread.
100    #[error("thread apply failed: {0}")]
101    Thread(#[from] thread::Error),
102    /// Error loading the identity document committed to by an operation.
103    #[error("identity doc failed to load: {0}")]
104    Doc(#[from] DocError),
105    /// Identity document is missing.
106    #[error("missing identity document")]
107    MissingIdentity,
108    /// Review is empty.
109    #[error("empty review; verdict or summary not provided")]
110    EmptyReview,
111    /// Duplicate review.
112    #[error("review {0} of {1} already exists by author {2}")]
113    DuplicateReview(ReviewId, RevisionId, NodeId),
114    /// Error loading the document payload.
115    #[error("payload failed to load: {0}")]
116    Payload(#[from] PayloadError),
117    /// Git error.
118    #[error("git: {0}")]
119    Git(#[from] git::ext::Error),
120    /// Store error.
121    #[error("store: {0}")]
122    Store(#[from] store::Error),
123    #[error("op decoding failed: {0}")]
124    Op(#[from] op::OpEncodingError),
125    /// Action not authorized by the author
126    #[error("{0} not authorized to apply {1:?}")]
127    NotAuthorized(ActorId, Action),
128    /// An illegal action.
129    #[error("action is not allowed: {0}")]
130    NotAllowed(EntryId),
131    /// Revision not found.
132    #[error("revision not found: {0}")]
133    RevisionNotFound(RevisionId),
134    /// Initialization failed.
135    #[error("initialization failed: {0}")]
136    Init(&'static str),
137    #[error("failed to update patch {id} in cache: {err}")]
138    CacheUpdate {
139        id: PatchId,
140        #[source]
141        err: Box<dyn std::error::Error + Send + Sync + 'static>,
142    },
143    #[error("failed to remove patch {id} from cache: {err}")]
144    CacheRemove {
145        id: PatchId,
146        #[source]
147        err: Box<dyn std::error::Error + Send + Sync + 'static>,
148    },
149    #[error("failed to remove patches from cache: {err}")]
150    CacheRemoveAll {
151        #[source]
152        err: Box<dyn std::error::Error + Send + Sync + 'static>,
153    },
154}
155
156/// Patch operation.
157#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
158#[serde(tag = "type", rename_all = "camelCase")]
159pub enum Action {
160    //
161    // Actions on patch.
162    //
163    #[serde(rename = "edit")]
164    Edit { title: String, target: MergeTarget },
165    #[serde(rename = "label")]
166    Label { labels: BTreeSet<Label> },
167    #[serde(rename = "lifecycle")]
168    Lifecycle { state: Lifecycle },
169    #[serde(rename = "assign")]
170    Assign { assignees: BTreeSet<Did> },
171    #[serde(rename = "merge")]
172    Merge {
173        revision: RevisionId,
174        commit: git::Oid,
175    },
176
177    //
178    // Review actions
179    //
180    #[serde(rename = "review")]
181    Review {
182        revision: RevisionId,
183        #[serde(default, skip_serializing_if = "Option::is_none")]
184        summary: Option<String>,
185        #[serde(default, skip_serializing_if = "Option::is_none")]
186        verdict: Option<Verdict>,
187        #[serde(default, skip_serializing_if = "Vec::is_empty")]
188        labels: Vec<Label>,
189    },
190    #[serde(rename = "review.edit")]
191    ReviewEdit {
192        review: ReviewId,
193        #[serde(default, skip_serializing_if = "Option::is_none")]
194        summary: Option<String>,
195        #[serde(default, skip_serializing_if = "Option::is_none")]
196        verdict: Option<Verdict>,
197        #[serde(default, skip_serializing_if = "Vec::is_empty")]
198        labels: Vec<Label>,
199    },
200    #[serde(rename = "review.redact")]
201    ReviewRedact { review: ReviewId },
202    #[serde(rename = "review.comment")]
203    ReviewComment {
204        review: ReviewId,
205        body: String,
206        #[serde(default, skip_serializing_if = "Option::is_none")]
207        location: Option<CodeLocation>,
208        /// Comment this is a reply to.
209        /// Should be [`None`] if it's the first comment.
210        /// Should be [`Some`] otherwise.
211        #[serde(default, skip_serializing_if = "Option::is_none")]
212        reply_to: Option<CommentId>,
213        /// Embeded content.
214        #[serde(default, skip_serializing_if = "Vec::is_empty")]
215        embeds: Vec<Embed<Uri>>,
216    },
217    #[serde(rename = "review.comment.edit")]
218    ReviewCommentEdit {
219        review: ReviewId,
220        comment: EntryId,
221        body: String,
222        embeds: Vec<Embed<Uri>>,
223    },
224    #[serde(rename = "review.comment.redact")]
225    ReviewCommentRedact { review: ReviewId, comment: EntryId },
226    #[serde(rename = "review.comment.react")]
227    ReviewCommentReact {
228        review: ReviewId,
229        comment: EntryId,
230        reaction: Reaction,
231        active: bool,
232    },
233    #[serde(rename = "review.comment.resolve")]
234    ReviewCommentResolve { review: ReviewId, comment: EntryId },
235    #[serde(rename = "review.comment.unresolve")]
236    ReviewCommentUnresolve { review: ReviewId, comment: EntryId },
237
238    //
239    // Revision actions
240    //
241    #[serde(rename = "revision")]
242    Revision {
243        description: String,
244        base: git::Oid,
245        oid: git::Oid,
246        /// Review comments resolved by this revision.
247        #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
248        resolves: BTreeSet<(EntryId, CommentId)>,
249    },
250    #[serde(rename = "revision.edit")]
251    RevisionEdit {
252        revision: RevisionId,
253        description: String,
254        /// Embeded content.
255        #[serde(default, skip_serializing_if = "Vec::is_empty")]
256        embeds: Vec<Embed<Uri>>,
257    },
258    /// React to the revision.
259    #[serde(rename = "revision.react")]
260    RevisionReact {
261        revision: RevisionId,
262        #[serde(default, skip_serializing_if = "Option::is_none")]
263        location: Option<CodeLocation>,
264        reaction: Reaction,
265        active: bool,
266    },
267    #[serde(rename = "revision.redact")]
268    RevisionRedact { revision: RevisionId },
269    #[serde(rename_all = "camelCase")]
270    #[serde(rename = "revision.comment")]
271    RevisionComment {
272        /// The revision to comment on.
273        revision: RevisionId,
274        /// For comments on the revision code.
275        #[serde(default, skip_serializing_if = "Option::is_none")]
276        location: Option<CodeLocation>,
277        /// Comment body.
278        body: String,
279        /// Comment this is a reply to.
280        /// Should be [`None`] if it's the top-level comment.
281        /// Should be the root [`CommentId`] if it's a top-level comment.
282        #[serde(default, skip_serializing_if = "Option::is_none")]
283        reply_to: Option<CommentId>,
284        /// Embeded content.
285        #[serde(default, skip_serializing_if = "Vec::is_empty")]
286        embeds: Vec<Embed<Uri>>,
287    },
288    /// Edit a revision comment.
289    #[serde(rename = "revision.comment.edit")]
290    RevisionCommentEdit {
291        revision: RevisionId,
292        comment: CommentId,
293        body: String,
294        embeds: Vec<Embed<Uri>>,
295    },
296    /// Redact a revision comment.
297    #[serde(rename = "revision.comment.redact")]
298    RevisionCommentRedact {
299        revision: RevisionId,
300        comment: CommentId,
301    },
302    /// React to a revision comment.
303    #[serde(rename = "revision.comment.react")]
304    RevisionCommentReact {
305        revision: RevisionId,
306        comment: CommentId,
307        reaction: Reaction,
308        active: bool,
309    },
310}
311
312impl CobAction for Action {
313    fn parents(&self) -> Vec<git::Oid> {
314        match self {
315            Self::Revision { base, oid, .. } => {
316                vec![*base, *oid]
317            }
318            Self::Merge { commit, .. } => {
319                vec![*commit]
320            }
321            _ => vec![],
322        }
323    }
324
325    fn produces_identifier(&self) -> bool {
326        matches!(
327            self,
328            Self::Revision { .. }
329                | Self::RevisionComment { .. }
330                | Self::Review { .. }
331                | Self::ReviewComment { .. }
332        )
333    }
334}
335
336/// Output of a merge.
337#[derive(Debug)]
338#[must_use]
339pub struct Merged<'a, R> {
340    pub patch: PatchId,
341    pub entry: EntryId,
342
343    stored: &'a R,
344}
345
346impl<R: WriteRepository> Merged<'_, R> {
347    /// Cleanup after merging a patch.
348    ///
349    /// This removes Git refs relating to the patch, both in the working copy,
350    /// and the stored copy; and updates `rad/sigrefs`.
351    pub fn cleanup<G>(
352        self,
353        working: &git::raw::Repository,
354        signer: &Device<G>,
355    ) -> Result<(), storage::RepositoryError>
356    where
357        G: crypto::signature::Signer<crypto::Signature>,
358    {
359        let nid = signer.public_key();
360        let stored_ref = git::refs::patch(&self.patch).with_namespace(nid.into());
361        let working_ref = git::refs::workdir::patch_upstream(&self.patch);
362
363        working
364            .find_reference(&working_ref)
365            .and_then(|mut r| r.delete())
366            .ok();
367
368        self.stored
369            .raw()
370            .find_reference(&stored_ref)
371            .and_then(|mut r| r.delete())
372            .ok();
373        self.stored.sign_refs(signer)?;
374
375        Ok(())
376    }
377}
378
379/// Where a patch is intended to be merged.
380#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
381#[serde(rename_all = "camelCase")]
382pub enum MergeTarget {
383    /// Intended for the default branch of the project delegates.
384    /// Note that if the delegations change while the patch is open,
385    /// this will always mean whatever the "current" delegation set is.
386    /// If it were otherwise, patches could become un-mergeable.
387    #[default]
388    Delegates,
389}
390
391impl MergeTarget {
392    /// Get the head of the target branch.
393    pub fn head<R: ReadRepository>(&self, repo: &R) -> Result<git::Oid, RepositoryError> {
394        match self {
395            MergeTarget::Delegates => {
396                let (_, target) = repo.head()?;
397                Ok(target)
398            }
399        }
400    }
401}
402
403/// Patch state.
404#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
405#[serde(rename_all = "camelCase")]
406pub struct Patch {
407    /// Title of the patch.
408    pub(super) title: String,
409    /// Patch author.
410    pub(super) author: Author,
411    /// Current state of the patch.
412    pub(super) state: State,
413    /// Target this patch is meant to be merged in.
414    pub(super) target: MergeTarget,
415    /// Associated labels.
416    /// Labels can be added and removed at will.
417    pub(super) labels: BTreeSet<Label>,
418    /// Patch merges.
419    ///
420    /// Only one merge is allowed per user.
421    ///
422    /// Merges can be removed and replaced, but not modified. Generally, once a revision is merged,
423    /// it stays that way. Being able to remove merges may be useful in case of force updates
424    /// on the target branch.
425    pub(super) merges: BTreeMap<ActorId, Merge>,
426    /// List of patch revisions. The initial changeset is part of the
427    /// first revision.
428    ///
429    /// Revisions can be redacted, but are otherwise immutable.
430    pub(super) revisions: BTreeMap<RevisionId, Option<Revision>>,
431    /// Users assigned to review this patch.
432    pub(super) assignees: BTreeSet<ActorId>,
433    /// Timeline of operations.
434    pub(super) timeline: Vec<EntryId>,
435    /// Reviews index. Keeps track of reviews for better performance.
436    pub(super) reviews: BTreeMap<ReviewId, Option<(RevisionId, ActorId)>>,
437}
438
439impl Patch {
440    /// Construct a new patch object from a revision.
441    pub fn new(title: String, target: MergeTarget, (id, revision): (RevisionId, Revision)) -> Self {
442        Self {
443            title,
444            author: revision.author.clone(),
445            state: State::default(),
446            target,
447            labels: BTreeSet::default(),
448            merges: BTreeMap::default(),
449            revisions: BTreeMap::from_iter([(id, Some(revision))]),
450            assignees: BTreeSet::default(),
451            timeline: vec![id.into_inner()],
452            reviews: BTreeMap::default(),
453        }
454    }
455
456    /// Title of the patch.
457    pub fn title(&self) -> &str {
458        self.title.as_str()
459    }
460
461    /// Current state of the patch.
462    pub fn state(&self) -> &State {
463        &self.state
464    }
465
466    /// Target this patch is meant to be merged in.
467    pub fn target(&self) -> MergeTarget {
468        self.target
469    }
470
471    /// Timestamp of the first revision of the patch.
472    pub fn timestamp(&self) -> Timestamp {
473        self.updates()
474            .next()
475            .map(|(_, r)| r)
476            .expect("Patch::timestamp: at least one revision is present")
477            .timestamp
478    }
479
480    /// Associated labels.
481    pub fn labels(&self) -> impl Iterator<Item = &Label> {
482        self.labels.iter()
483    }
484
485    /// Patch description.
486    pub fn description(&self) -> &str {
487        let (_, r) = self.root();
488        r.description()
489    }
490
491    /// Patch embeds.
492    pub fn embeds(&self) -> &[Embed<Uri>] {
493        let (_, r) = self.root();
494        r.embeds()
495    }
496
497    /// Author of the first revision of the patch.
498    pub fn author(&self) -> &Author {
499        &self.author
500    }
501
502    /// All revision authors.
503    pub fn authors(&self) -> BTreeSet<&Author> {
504        self.revisions
505            .values()
506            .filter_map(|r| r.as_ref())
507            .map(|r| &r.author)
508            .collect()
509    }
510
511    /// Get the `Revision` by its `RevisionId`.
512    ///
513    /// None is returned if the `Revision` has been redacted (deleted).
514    pub fn revision(&self, id: &RevisionId) -> Option<&Revision> {
515        self.revisions.get(id).and_then(|o| o.as_ref())
516    }
517
518    /// List of patch revisions by the patch author. The initial changeset is part of the
519    /// first revision.
520    pub fn updates(&self) -> impl DoubleEndedIterator<Item = (RevisionId, &Revision)> {
521        self.revisions_by(self.author().public_key())
522    }
523
524    /// List of all patch revisions by all authors.
525    pub fn revisions(&self) -> impl DoubleEndedIterator<Item = (RevisionId, &Revision)> {
526        self.timeline.iter().filter_map(move |id| {
527            self.revisions
528                .get(id)
529                .and_then(|o| o.as_ref())
530                .map(|rev| (RevisionId(*id), rev))
531        })
532    }
533
534    /// List of patch revisions by the given author.
535    pub fn revisions_by<'a>(
536        &'a self,
537        author: &'a PublicKey,
538    ) -> impl DoubleEndedIterator<Item = (RevisionId, &'a Revision)> {
539        self.revisions()
540            .filter(move |(_, r)| (r.author.public_key() == author))
541    }
542
543    /// List of patch reviews of the given revision.
544    pub fn reviews_of(&self, rev: RevisionId) -> impl Iterator<Item = (&ReviewId, &Review)> {
545        self.reviews.iter().filter_map(move |(review_id, t)| {
546            t.and_then(|(rev_id, pk)| {
547                if rev == rev_id {
548                    self.revision(&rev_id)
549                        .and_then(|r| r.review_by(&pk))
550                        .map(|r| (review_id, r))
551                } else {
552                    None
553                }
554            })
555        })
556    }
557
558    /// List of patch assignees.
559    pub fn assignees(&self) -> impl Iterator<Item = Did> + '_ {
560        self.assignees.iter().map(Did::from)
561    }
562
563    /// Get the merges.
564    pub fn merges(&self) -> impl Iterator<Item = (&ActorId, &Merge)> {
565        self.merges.iter()
566    }
567
568    /// Reference to the Git object containing the code on the latest revision.
569    pub fn head(&self) -> &git::Oid {
570        &self.latest().1.oid
571    }
572
573    /// Get the commit of the target branch on which this patch is based.
574    /// This can change via a patch update.
575    pub fn base(&self) -> &git::Oid {
576        &self.latest().1.base
577    }
578
579    /// Get the merge base of this patch.
580    pub fn merge_base<R: ReadRepository>(&self, repo: &R) -> Result<git::Oid, git::ext::Error> {
581        repo.merge_base(self.base(), self.head())
582    }
583
584    /// Get the commit range of this patch.
585    pub fn range(&self) -> Result<(git::Oid, git::Oid), git::ext::Error> {
586        Ok((*self.base(), *self.head()))
587    }
588
589    /// Index of latest revision in the revisions list.
590    pub fn version(&self) -> RevisionIx {
591        self.revisions
592            .len()
593            .checked_sub(1)
594            .expect("Patch::version: at least one revision is present")
595    }
596
597    /// Root revision.
598    ///
599    /// This is the revision that was created with the patch.
600    pub fn root(&self) -> (RevisionId, &Revision) {
601        self.updates()
602            .next()
603            .expect("Patch::root: there is always a root revision")
604    }
605
606    /// Latest revision by the patch author.
607    pub fn latest(&self) -> (RevisionId, &Revision) {
608        self.latest_by(self.author().public_key())
609            .expect("Patch::latest: there is always at least one revision")
610    }
611
612    /// Latest revision by the given author.
613    pub fn latest_by<'a>(&'a self, author: &'a PublicKey) -> Option<(RevisionId, &'a Revision)> {
614        self.revisions_by(author).next_back()
615    }
616
617    /// Time of last update.
618    pub fn updated_at(&self) -> Timestamp {
619        self.latest().1.timestamp()
620    }
621
622    /// Check if the patch is merged.
623    pub fn is_merged(&self) -> bool {
624        matches!(self.state(), State::Merged { .. })
625    }
626
627    /// Check if the patch is open.
628    pub fn is_open(&self) -> bool {
629        matches!(self.state(), State::Open { .. })
630    }
631
632    /// Check if the patch is archived.
633    pub fn is_archived(&self) -> bool {
634        matches!(self.state(), State::Archived)
635    }
636
637    /// Check if the patch is a draft.
638    pub fn is_draft(&self) -> bool {
639        matches!(self.state(), State::Draft)
640    }
641
642    /// Apply authorization rules on patch actions.
643    pub fn authorization(
644        &self,
645        action: &Action,
646        actor: &ActorId,
647        doc: &Doc,
648    ) -> Result<Authorization, Error> {
649        if doc.is_delegate(&actor.into()) {
650            // A delegate is authorized to do all actions.
651            return Ok(Authorization::Allow);
652        }
653        let author = self.author().id().as_key();
654        let outcome = match action {
655            // The patch author can edit the patch and change its state.
656            Action::Edit { .. } => Authorization::from(actor == author),
657            Action::Lifecycle { state } => Authorization::from(match state {
658                Lifecycle::Open { .. } => actor == author,
659                Lifecycle::Draft { .. } => actor == author,
660                Lifecycle::Archived { .. } => actor == author,
661            }),
662            // Only delegates can carry out these actions.
663            Action::Label { labels } => {
664                if labels == &self.labels {
665                    // No-op is allowed for backwards compatibility.
666                    Authorization::Allow
667                } else {
668                    Authorization::Deny
669                }
670            }
671            Action::Assign { .. } => Authorization::Deny,
672            Action::Merge { .. } => match self.target() {
673                MergeTarget::Delegates => Authorization::Deny,
674            },
675            // Anyone can submit a review.
676            Action::Review { .. } => Authorization::Allow,
677            Action::ReviewRedact { review, .. } | Action::ReviewEdit { review, .. } => {
678                if let Some((_, review)) = lookup::review(self, review)? {
679                    Authorization::from(actor == review.author.public_key())
680                } else {
681                    // Redacted.
682                    Authorization::Unknown
683                }
684            }
685            // Anyone can comment on a review.
686            Action::ReviewComment { .. } => Authorization::Allow,
687            // The comment author can edit and redact their own comment.
688            Action::ReviewCommentEdit {
689                review, comment, ..
690            }
691            | Action::ReviewCommentRedact { review, comment } => {
692                if let Some((_, review)) = lookup::review(self, review)? {
693                    if let Some(comment) = review.comments.comment(comment) {
694                        return Ok(Authorization::from(*actor == comment.author()));
695                    }
696                }
697                // Redacted.
698                Authorization::Unknown
699            }
700            // Anyone can react to a review comment.
701            Action::ReviewCommentReact { .. } => Authorization::Allow,
702            // The reviewer, commenter or revision author can resolve and unresolve review comments.
703            Action::ReviewCommentResolve { review, comment }
704            | Action::ReviewCommentUnresolve { review, comment } => {
705                if let Some((revision, review)) = lookup::review(self, review)? {
706                    if let Some(comment) = review.comments.comment(comment) {
707                        return Ok(Authorization::from(
708                            actor == &comment.author()
709                                || actor == review.author.public_key()
710                                || actor == revision.author.public_key(),
711                        ));
712                    }
713                }
714                // Redacted.
715                Authorization::Unknown
716            }
717            // Anyone can propose revisions.
718            Action::Revision { .. } => Authorization::Allow,
719            // Only the revision author can edit or redact their revision.
720            Action::RevisionEdit { revision, .. } | Action::RevisionRedact { revision, .. } => {
721                if let Some(revision) = lookup::revision(self, revision)? {
722                    Authorization::from(actor == revision.author.public_key())
723                } else {
724                    // Redacted.
725                    Authorization::Unknown
726                }
727            }
728            // Anyone can react to or comment on a revision.
729            Action::RevisionReact { .. } => Authorization::Allow,
730            Action::RevisionComment { .. } => Authorization::Allow,
731            // Only the comment author can edit or redact their comment.
732            Action::RevisionCommentEdit {
733                revision, comment, ..
734            }
735            | Action::RevisionCommentRedact {
736                revision, comment, ..
737            } => {
738                if let Some(revision) = lookup::revision(self, revision)? {
739                    if let Some(comment) = revision.discussion.comment(comment) {
740                        return Ok(Authorization::from(actor == &comment.author()));
741                    }
742                }
743                // Redacted.
744                Authorization::Unknown
745            }
746            // Anyone can react to a revision.
747            Action::RevisionCommentReact { .. } => Authorization::Allow,
748        };
749        Ok(outcome)
750    }
751}
752
753impl Patch {
754    /// Apply an action after checking if it's authorized.
755    fn op_action<R: ReadRepository>(
756        &mut self,
757        action: Action,
758        id: EntryId,
759        author: ActorId,
760        timestamp: Timestamp,
761        concurrent: &[&cob::Entry],
762        doc: &DocAt,
763        repo: &R,
764    ) -> Result<(), Error> {
765        match self.authorization(&action, &author, doc)? {
766            Authorization::Allow => {
767                self.action(action, id, author, timestamp, concurrent, doc, repo)
768            }
769            Authorization::Deny => Err(Error::NotAuthorized(author, action)),
770            Authorization::Unknown => {
771                // In this case, since there is not enough information to determine
772                // whether the action is authorized or not, we simply ignore it.
773                // It's likely that the target object was redacted, and we can't
774                // verify whether the action would have been allowed or not.
775                Ok(())
776            }
777        }
778    }
779
780    /// Apply a single action to the patch.
781    fn action<R: ReadRepository>(
782        &mut self,
783        action: Action,
784        entry: EntryId,
785        author: ActorId,
786        timestamp: Timestamp,
787        _concurrent: &[&cob::Entry],
788        identity: &Doc,
789        repo: &R,
790    ) -> Result<(), Error> {
791        match action {
792            Action::Edit { title, target } => {
793                self.title = title;
794                self.target = target;
795            }
796            Action::Lifecycle { state } => {
797                let valid = self.state == State::Draft
798                    || self.state == State::Archived
799                    || self.state == State::Open { conflicts: vec![] };
800
801                if valid {
802                    match state {
803                        Lifecycle::Open => {
804                            self.state = State::Open { conflicts: vec![] };
805                        }
806                        Lifecycle::Draft => {
807                            self.state = State::Draft;
808                        }
809                        Lifecycle::Archived => {
810                            self.state = State::Archived;
811                        }
812                    }
813                }
814            }
815            Action::Label { labels } => {
816                self.labels = BTreeSet::from_iter(labels);
817            }
818            Action::Assign { assignees } => {
819                self.assignees = BTreeSet::from_iter(assignees.into_iter().map(ActorId::from));
820            }
821            Action::RevisionEdit {
822                revision,
823                description,
824                embeds,
825            } => {
826                if let Some(redactable) = self.revisions.get_mut(&revision) {
827                    // If the revision was redacted concurrently, there's nothing to do.
828                    if let Some(revision) = redactable {
829                        revision.description.push(Edit::new(
830                            author,
831                            description,
832                            timestamp,
833                            embeds,
834                        ));
835                    }
836                } else {
837                    return Err(Error::Missing(revision.into_inner()));
838                }
839            }
840            Action::Revision {
841                description,
842                base,
843                oid,
844                resolves,
845            } => {
846                debug_assert!(!self.revisions.contains_key(&entry));
847                let id = RevisionId(entry);
848
849                self.revisions.insert(
850                    id,
851                    Some(Revision::new(
852                        id,
853                        author.into(),
854                        description,
855                        base,
856                        oid,
857                        timestamp,
858                        resolves,
859                    )),
860                );
861            }
862            Action::RevisionReact {
863                revision,
864                reaction,
865                active,
866                location,
867            } => {
868                if let Some(revision) = lookup::revision_mut(self, &revision)? {
869                    let key = (author, reaction);
870                    let reactions = revision.reactions.entry(location).or_default();
871
872                    if active {
873                        reactions.insert(key);
874                    } else {
875                        reactions.remove(&key);
876                    }
877                }
878            }
879            Action::RevisionRedact { revision } => {
880                // Not allowed to delete the root revision.
881                let (root, _) = self.root();
882                if revision == root {
883                    return Err(Error::NotAllowed(entry));
884                }
885                // Redactions must have observed a revision to be valid.
886                if let Some(r) = self.revisions.get_mut(&revision) {
887                    // If the revision has already been merged, ignore the redaction. We
888                    // don't want to redact merged revisions.
889                    if self.merges.values().any(|m| m.revision == revision) {
890                        return Ok(());
891                    }
892                    *r = None;
893                } else {
894                    return Err(Error::Missing(revision.into_inner()));
895                }
896            }
897            Action::Review {
898                revision,
899                ref summary,
900                verdict,
901                labels,
902            } => {
903                let Some(rev) = self.revisions.get_mut(&revision) else {
904                    // If the revision was redacted concurrently, there's nothing to do.
905                    return Ok(());
906                };
907                if let Some(rev) = rev {
908                    // Insert a review if there isn't already one. Otherwise we just ignore
909                    // this operation
910                    if let btree_map::Entry::Vacant(e) = rev.reviews.entry(author) {
911                        let id = ReviewId(entry);
912
913                        e.insert(Review::new(
914                            id,
915                            Author::new(author),
916                            verdict,
917                            summary.to_owned(),
918                            labels,
919                            timestamp,
920                        ));
921                        // Update reviews index.
922                        self.reviews.insert(id, Some((revision, author)));
923                    } else {
924                        log::error!(
925                            target: "patch",
926                            "Review by {author} for {revision} already exists, ignoring action.."
927                        );
928                    }
929                }
930            }
931            Action::ReviewEdit {
932                review,
933                summary,
934                verdict,
935                labels,
936            } => {
937                if summary.is_none() && verdict.is_none() {
938                    return Err(Error::EmptyReview);
939                }
940                let Some(review) = lookup::review_mut(self, &review)? else {
941                    return Ok(());
942                };
943                review.verdict = verdict;
944                review.summary = summary;
945                review.labels = labels;
946            }
947            Action::ReviewCommentReact {
948                review,
949                comment,
950                reaction,
951                active,
952            } => {
953                if let Some(review) = lookup::review_mut(self, &review)? {
954                    thread::react(
955                        &mut review.comments,
956                        entry,
957                        author,
958                        comment,
959                        reaction,
960                        active,
961                    )?;
962                }
963            }
964            Action::ReviewCommentRedact { review, comment } => {
965                if let Some(review) = lookup::review_mut(self, &review)? {
966                    thread::redact(&mut review.comments, entry, comment)?;
967                }
968            }
969            Action::ReviewCommentEdit {
970                review,
971                comment,
972                body,
973                embeds,
974            } => {
975                if let Some(review) = lookup::review_mut(self, &review)? {
976                    thread::edit(
977                        &mut review.comments,
978                        entry,
979                        author,
980                        comment,
981                        timestamp,
982                        body,
983                        embeds,
984                    )?;
985                }
986            }
987            Action::ReviewCommentResolve { review, comment } => {
988                if let Some(review) = lookup::review_mut(self, &review)? {
989                    thread::resolve(&mut review.comments, entry, comment)?;
990                }
991            }
992            Action::ReviewCommentUnresolve { review, comment } => {
993                if let Some(review) = lookup::review_mut(self, &review)? {
994                    thread::unresolve(&mut review.comments, entry, comment)?;
995                }
996            }
997            Action::ReviewComment {
998                review,
999                body,
1000                location,
1001                reply_to,
1002                embeds,
1003            } => {
1004                if let Some(review) = lookup::review_mut(self, &review)? {
1005                    thread::comment(
1006                        &mut review.comments,
1007                        entry,
1008                        author,
1009                        timestamp,
1010                        body,
1011                        reply_to,
1012                        location,
1013                        embeds,
1014                    )?;
1015                }
1016            }
1017            Action::ReviewRedact { review } => {
1018                // Redactions must have observed a review to be valid.
1019                let Some(locator) = self.reviews.get_mut(&review) else {
1020                    return Err(Error::Missing(review.into_inner()));
1021                };
1022                // If the review is already redacted, do nothing.
1023                let Some((revision, reviewer)) = locator else {
1024                    return Ok(());
1025                };
1026                // The revision must have existed at some point.
1027                let Some(redactable) = self.revisions.get_mut(revision) else {
1028                    return Err(Error::Missing(revision.into_inner()));
1029                };
1030                // But it could be redacted.
1031                let Some(revision) = redactable else {
1032                    return Ok(());
1033                };
1034                // Remove review for this author.
1035                if let Some(r) = revision.reviews.remove(reviewer) {
1036                    debug_assert_eq!(r.id, review);
1037                } else {
1038                    log::error!(
1039                        target: "patch", "Review {review} not found in revision {}", revision.id
1040                    );
1041                }
1042                // Set the review locator in the review index to redacted.
1043                *locator = None;
1044            }
1045            Action::Merge { revision, commit } => {
1046                // If the revision was redacted before the merge, ignore the merge.
1047                if lookup::revision_mut(self, &revision)?.is_none() {
1048                    return Ok(());
1049                };
1050                match self.target() {
1051                    MergeTarget::Delegates => {
1052                        let proj = identity.project()?;
1053                        let branch = git::refs::branch(proj.default_branch());
1054
1055                        // Nb. We don't return an error in case the merge commit is not an
1056                        // ancestor of the default branch. The default branch can change
1057                        // *after* the merge action is created, which is out of the control
1058                        // of the merge author. We simply skip it, which allows archiving in
1059                        // case of a rebase off the master branch, or a redaction of the
1060                        // merge.
1061                        let Ok(head) = repo.reference_oid(&author, &branch) else {
1062                            return Ok(());
1063                        };
1064                        if commit != head && !repo.is_ancestor_of(commit, head)? {
1065                            return Ok(());
1066                        }
1067                    }
1068                }
1069                self.merges.insert(
1070                    author,
1071                    Merge {
1072                        revision,
1073                        commit,
1074                        timestamp,
1075                    },
1076                );
1077
1078                let mut merges = self.merges.iter().fold(
1079                    HashMap::<(RevisionId, git::Oid), usize>::new(),
1080                    |mut acc, (_, merge)| {
1081                        *acc.entry((merge.revision, merge.commit)).or_default() += 1;
1082                        acc
1083                    },
1084                );
1085                // Discard revisions that weren't merged by a threshold of delegates.
1086                merges.retain(|_, count| *count >= identity.threshold());
1087
1088                match merges.into_keys().collect::<Vec<_>>().as_slice() {
1089                    [] => {
1090                        // None of the revisions met the quorum.
1091                    }
1092                    [(revision, commit)] => {
1093                        // Patch is merged.
1094                        self.state = State::Merged {
1095                            revision: *revision,
1096                            commit: *commit,
1097                        };
1098                    }
1099                    revisions => {
1100                        // More than one revision met the quorum.
1101                        self.state = State::Open {
1102                            conflicts: revisions.to_vec(),
1103                        };
1104                    }
1105                }
1106            }
1107
1108            Action::RevisionComment {
1109                revision,
1110                body,
1111                reply_to,
1112                embeds,
1113                location,
1114            } => {
1115                if let Some(revision) = lookup::revision_mut(self, &revision)? {
1116                    thread::comment(
1117                        &mut revision.discussion,
1118                        entry,
1119                        author,
1120                        timestamp,
1121                        body,
1122                        reply_to,
1123                        location,
1124                        embeds,
1125                    )?;
1126                }
1127            }
1128            Action::RevisionCommentEdit {
1129                revision,
1130                comment,
1131                body,
1132                embeds,
1133            } => {
1134                if let Some(revision) = lookup::revision_mut(self, &revision)? {
1135                    thread::edit(
1136                        &mut revision.discussion,
1137                        entry,
1138                        author,
1139                        comment,
1140                        timestamp,
1141                        body,
1142                        embeds,
1143                    )?;
1144                }
1145            }
1146            Action::RevisionCommentRedact { revision, comment } => {
1147                if let Some(revision) = lookup::revision_mut(self, &revision)? {
1148                    thread::redact(&mut revision.discussion, entry, comment)?;
1149                }
1150            }
1151            Action::RevisionCommentReact {
1152                revision,
1153                comment,
1154                reaction,
1155                active,
1156            } => {
1157                if let Some(revision) = lookup::revision_mut(self, &revision)? {
1158                    thread::react(
1159                        &mut revision.discussion,
1160                        entry,
1161                        author,
1162                        comment,
1163                        reaction,
1164                        active,
1165                    )?;
1166                }
1167            }
1168        }
1169        Ok(())
1170    }
1171}
1172
1173impl cob::store::CobWithType for Patch {
1174    fn type_name() -> &'static TypeName {
1175        &TYPENAME
1176    }
1177}
1178
1179impl store::Cob for Patch {
1180    type Action = Action;
1181    type Error = Error;
1182
1183    fn from_root<R: ReadRepository>(op: Op, repo: &R) -> Result<Self, Self::Error> {
1184        let doc = op.identity_doc(repo)?.ok_or(Error::MissingIdentity)?;
1185        let mut actions = op.actions.into_iter();
1186        let Some(Action::Revision {
1187            description,
1188            base,
1189            oid,
1190            resolves,
1191        }) = actions.next()
1192        else {
1193            return Err(Error::Init("the first action must be of type `revision`"));
1194        };
1195        let Some(Action::Edit { title, target }) = actions.next() else {
1196            return Err(Error::Init("the second action must be of type `edit`"));
1197        };
1198        let revision = Revision::new(
1199            RevisionId(op.id),
1200            op.author.into(),
1201            description,
1202            base,
1203            oid,
1204            op.timestamp,
1205            resolves,
1206        );
1207        let mut patch = Patch::new(title, target, (RevisionId(op.id), revision));
1208
1209        for action in actions {
1210            match patch.authorization(&action, &op.author, &doc)? {
1211                Authorization::Allow => {
1212                    patch.action(action, op.id, op.author, op.timestamp, &[], &doc, repo)?;
1213                }
1214                Authorization::Deny => {
1215                    return Err(Error::NotAuthorized(op.author, action));
1216                }
1217                Authorization::Unknown => {
1218                    // Note that this shouldn't really happen since there's no concurrency in the
1219                    // root operation.
1220                    continue;
1221                }
1222            }
1223        }
1224        Ok(patch)
1225    }
1226
1227    fn op<'a, R: ReadRepository, I: IntoIterator<Item = &'a cob::Entry>>(
1228        &mut self,
1229        op: Op,
1230        concurrent: I,
1231        repo: &R,
1232    ) -> Result<(), Error> {
1233        debug_assert!(!self.timeline.contains(&op.id));
1234        self.timeline.push(op.id);
1235
1236        let doc = op.identity_doc(repo)?.ok_or(Error::MissingIdentity)?;
1237        let concurrent = concurrent.into_iter().collect::<Vec<_>>();
1238
1239        for action in op.actions {
1240            log::trace!(target: "patch", "Applying {} {action:?}", op.id);
1241
1242            if let Err(e) = self.op_action(
1243                action,
1244                op.id,
1245                op.author,
1246                op.timestamp,
1247                &concurrent,
1248                &doc,
1249                repo,
1250            ) {
1251                log::error!(target: "patch", "Error applying {}: {e}", op.id);
1252                return Err(e);
1253            }
1254        }
1255        Ok(())
1256    }
1257}
1258
1259impl<R: ReadRepository> cob::Evaluate<R> for Patch {
1260    type Error = Error;
1261
1262    fn init(entry: &cob::Entry, repo: &R) -> Result<Self, Self::Error> {
1263        let op = Op::try_from(entry)?;
1264        let object = Patch::from_root(op, repo)?;
1265
1266        Ok(object)
1267    }
1268
1269    fn apply<'a, I: Iterator<Item = (&'a EntryId, &'a cob::Entry)>>(
1270        &mut self,
1271        entry: &cob::Entry,
1272        concurrent: I,
1273        repo: &R,
1274    ) -> Result<(), Self::Error> {
1275        let op = Op::try_from(entry)?;
1276
1277        self.op(op, concurrent.map(|(_, e)| e), repo)
1278    }
1279}
1280
1281mod lookup {
1282    use super::*;
1283
1284    pub fn revision<'a>(
1285        patch: &'a Patch,
1286        revision: &RevisionId,
1287    ) -> Result<Option<&'a Revision>, Error> {
1288        match patch.revisions.get(revision) {
1289            Some(Some(revision)) => Ok(Some(revision)),
1290            // Redacted.
1291            Some(None) => Ok(None),
1292            // Missing. Causal error.
1293            None => Err(Error::Missing(revision.into_inner())),
1294        }
1295    }
1296
1297    pub fn revision_mut<'a>(
1298        patch: &'a mut Patch,
1299        revision: &RevisionId,
1300    ) -> Result<Option<&'a mut Revision>, Error> {
1301        match patch.revisions.get_mut(revision) {
1302            Some(Some(revision)) => Ok(Some(revision)),
1303            // Redacted.
1304            Some(None) => Ok(None),
1305            // Missing. Causal error.
1306            None => Err(Error::Missing(revision.into_inner())),
1307        }
1308    }
1309
1310    pub fn review<'a>(
1311        patch: &'a Patch,
1312        review: &ReviewId,
1313    ) -> Result<Option<(&'a Revision, &'a Review)>, Error> {
1314        match patch.reviews.get(review) {
1315            Some(Some((revision, author))) => {
1316                match patch.revisions.get(revision) {
1317                    Some(Some(rev)) => {
1318                        let r = rev
1319                            .reviews
1320                            .get(author)
1321                            .ok_or_else(|| Error::Missing(review.into_inner()))?;
1322                        debug_assert_eq!(&r.id, review);
1323
1324                        Ok(Some((rev, r)))
1325                    }
1326                    Some(None) => {
1327                        // If the revision was redacted concurrently, there's nothing to do.
1328                        // Likewise, if the review was redacted concurrently, there's nothing to do.
1329                        Ok(None)
1330                    }
1331                    None => Err(Error::Missing(revision.into_inner())),
1332                }
1333            }
1334            Some(None) => {
1335                // Redacted.
1336                Ok(None)
1337            }
1338            None => Err(Error::Missing(review.into_inner())),
1339        }
1340    }
1341
1342    pub fn review_mut<'a>(
1343        patch: &'a mut Patch,
1344        review: &ReviewId,
1345    ) -> Result<Option<&'a mut Review>, Error> {
1346        match patch.reviews.get(review) {
1347            Some(Some((revision, author))) => {
1348                match patch.revisions.get_mut(revision) {
1349                    Some(Some(rev)) => {
1350                        let r = rev
1351                            .reviews
1352                            .get_mut(author)
1353                            .ok_or_else(|| Error::Missing(review.into_inner()))?;
1354                        debug_assert_eq!(&r.id, review);
1355
1356                        Ok(Some(r))
1357                    }
1358                    Some(None) => {
1359                        // If the revision was redacted concurrently, there's nothing to do.
1360                        // Likewise, if the review was redacted concurrently, there's nothing to do.
1361                        Ok(None)
1362                    }
1363                    None => Err(Error::Missing(revision.into_inner())),
1364                }
1365            }
1366            Some(None) => {
1367                // Redacted.
1368                Ok(None)
1369            }
1370            None => Err(Error::Missing(review.into_inner())),
1371        }
1372    }
1373}
1374
1375/// A patch revision.
1376#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1377#[serde(rename_all = "camelCase")]
1378pub struct Revision {
1379    /// Revision identifier.
1380    pub(super) id: RevisionId,
1381    /// Author of the revision.
1382    pub(super) author: Author,
1383    /// Revision description.
1384    pub(super) description: NonEmpty<Edit>,
1385    /// Base branch commit, used as a merge base.
1386    pub(super) base: git::Oid,
1387    /// Reference to the Git object containing the code (revision head).
1388    pub(super) oid: git::Oid,
1389    /// Discussion around this revision.
1390    pub(super) discussion: Thread<Comment<CodeLocation>>,
1391    /// Reviews of this revision's changes (all review edits are kept).
1392    pub(super) reviews: BTreeMap<ActorId, Review>,
1393    /// When this revision was created.
1394    pub(super) timestamp: Timestamp,
1395    /// Review comments resolved by this revision.
1396    pub(super) resolves: BTreeSet<(EntryId, CommentId)>,
1397    /// Reactions on code locations and revision itself
1398    #[serde(
1399        serialize_with = "ser::serialize_reactions",
1400        deserialize_with = "ser::deserialize_reactions"
1401    )]
1402    pub(super) reactions: BTreeMap<Option<CodeLocation>, Reactions>,
1403}
1404
1405impl Revision {
1406    pub fn new(
1407        id: RevisionId,
1408        author: Author,
1409        description: String,
1410        base: git::Oid,
1411        oid: git::Oid,
1412        timestamp: Timestamp,
1413        resolves: BTreeSet<(EntryId, CommentId)>,
1414    ) -> Self {
1415        let description = Edit::new(*author.public_key(), description, timestamp, Vec::default());
1416
1417        Self {
1418            id,
1419            author,
1420            description: NonEmpty::new(description),
1421            base,
1422            oid,
1423            discussion: Thread::default(),
1424            reviews: BTreeMap::default(),
1425            timestamp,
1426            resolves,
1427            reactions: Default::default(),
1428        }
1429    }
1430
1431    pub fn id(&self) -> RevisionId {
1432        self.id
1433    }
1434
1435    pub fn description(&self) -> &str {
1436        self.description.last().body.as_str()
1437    }
1438
1439    pub fn edits(&self) -> impl Iterator<Item = &Edit> {
1440        self.description.iter()
1441    }
1442
1443    pub fn embeds(&self) -> &[Embed<Uri>] {
1444        &self.description.last().embeds
1445    }
1446
1447    pub fn reactions(&self) -> &BTreeMap<Option<CodeLocation>, BTreeSet<(PublicKey, Reaction)>> {
1448        &self.reactions
1449    }
1450
1451    /// Author of the revision.
1452    pub fn author(&self) -> &Author {
1453        &self.author
1454    }
1455
1456    /// Base branch commit, used as a merge base.
1457    pub fn base(&self) -> &git::Oid {
1458        &self.base
1459    }
1460
1461    /// Reference to the Git object containing the code (revision head).
1462    pub fn head(&self) -> git::Oid {
1463        self.oid
1464    }
1465
1466    /// Get the commit range of this revision.
1467    pub fn range(&self) -> (git::Oid, git::Oid) {
1468        (self.base, self.oid)
1469    }
1470
1471    /// When this revision was created.
1472    pub fn timestamp(&self) -> Timestamp {
1473        self.timestamp
1474    }
1475
1476    /// Discussion around this revision.
1477    pub fn discussion(&self) -> &Thread<Comment<CodeLocation>> {
1478        &self.discussion
1479    }
1480
1481    /// Review comments resolved by this revision.
1482    pub fn resolves(&self) -> &BTreeSet<(EntryId, CommentId)> {
1483        &self.resolves
1484    }
1485
1486    /// Iterate over all top-level replies.
1487    pub fn replies(&self) -> impl Iterator<Item = (&CommentId, &thread::Comment<CodeLocation>)> {
1488        self.discussion.comments()
1489    }
1490
1491    /// Reviews of this revision's changes (one per actor).
1492    pub fn reviews(&self) -> impl DoubleEndedIterator<Item = (&PublicKey, &Review)> {
1493        self.reviews.iter()
1494    }
1495
1496    /// Get a review by author.
1497    pub fn review_by(&self, author: &ActorId) -> Option<&Review> {
1498        self.reviews.get(author)
1499    }
1500}
1501
1502/// Patch state.
1503#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1504#[serde(rename_all = "camelCase", tag = "status")]
1505pub enum State {
1506    Draft,
1507    Open {
1508        /// Revisions that were merged and are conflicting.
1509        #[serde(skip_serializing_if = "Vec::is_empty")]
1510        #[serde(default)]
1511        conflicts: Vec<(RevisionId, git::Oid)>,
1512    },
1513    Archived,
1514    Merged {
1515        /// The revision that was merged.
1516        revision: RevisionId,
1517        /// The commit in the target branch that contains the changes.
1518        commit: git::Oid,
1519    },
1520}
1521
1522impl Default for State {
1523    fn default() -> Self {
1524        Self::Open { conflicts: vec![] }
1525    }
1526}
1527
1528impl fmt::Display for State {
1529    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1530        match self {
1531            Self::Archived => write!(f, "archived"),
1532            Self::Draft => write!(f, "draft"),
1533            Self::Open { .. } => write!(f, "open"),
1534            Self::Merged { .. } => write!(f, "merged"),
1535        }
1536    }
1537}
1538
1539impl From<&State> for Status {
1540    fn from(value: &State) -> Self {
1541        match value {
1542            State::Draft => Self::Draft,
1543            State::Open { .. } => Self::Open,
1544            State::Archived => Self::Archived,
1545            State::Merged { .. } => Self::Merged,
1546        }
1547    }
1548}
1549
1550/// A simplified enumeration of a [`State`] that can be used for
1551/// filtering purposes.
1552#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1553pub enum Status {
1554    Draft,
1555    #[default]
1556    Open,
1557    Archived,
1558    Merged,
1559}
1560
1561impl fmt::Display for Status {
1562    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1563        match self {
1564            Self::Archived => write!(f, "archived"),
1565            Self::Draft => write!(f, "draft"),
1566            Self::Open => write!(f, "open"),
1567            Self::Merged => write!(f, "merged"),
1568        }
1569    }
1570}
1571
1572/// A lifecycle operation, resulting in a new state.
1573#[derive(Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1574#[serde(rename_all = "camelCase", tag = "status")]
1575pub enum Lifecycle {
1576    #[default]
1577    Open,
1578    Draft,
1579    Archived,
1580}
1581
1582/// A merged patch revision.
1583#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
1584#[serde(rename_all = "camelCase")]
1585pub struct Merge {
1586    /// Revision that was merged.
1587    pub revision: RevisionId,
1588    /// Base branch commit that contains the revision.
1589    pub commit: git::Oid,
1590    /// When this merge was performed.
1591    pub timestamp: Timestamp,
1592}
1593
1594/// A patch review verdict.
1595#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1596#[serde(rename_all = "camelCase")]
1597pub enum Verdict {
1598    /// Accept patch.
1599    Accept,
1600    /// Reject patch.
1601    Reject,
1602}
1603
1604impl fmt::Display for Verdict {
1605    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1606        match self {
1607            Self::Accept => write!(f, "accept"),
1608            Self::Reject => write!(f, "reject"),
1609        }
1610    }
1611}
1612
1613/// A patch review on a revision.
1614#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1615#[serde(rename_all = "camelCase")]
1616pub struct Review {
1617    /// Review identifier.
1618    pub(super) id: ReviewId,
1619    /// Review author.
1620    pub(super) author: Author,
1621    /// Review verdict.
1622    ///
1623    /// The verdict cannot be changed, since revisions are immutable.
1624    pub(super) verdict: Option<Verdict>,
1625    /// Review summary.
1626    ///
1627    /// Can be edited or set to `None`.
1628    pub(super) summary: Option<String>,
1629    /// Review comments.
1630    pub(super) comments: Thread<Comment<CodeLocation>>,
1631    /// Labels qualifying the review. For example if this review only looks at the
1632    /// concept or intention of the patch, it could have a "concept" label.
1633    pub(super) labels: Vec<Label>,
1634    /// Review timestamp.
1635    pub(super) timestamp: Timestamp,
1636}
1637
1638impl Review {
1639    pub fn new(
1640        id: ReviewId,
1641        author: Author,
1642        verdict: Option<Verdict>,
1643        summary: Option<String>,
1644        labels: Vec<Label>,
1645        timestamp: Timestamp,
1646    ) -> Self {
1647        Self {
1648            id,
1649            author,
1650            verdict,
1651            summary,
1652            comments: Thread::default(),
1653            labels,
1654            timestamp,
1655        }
1656    }
1657
1658    /// Review identifier.
1659    pub fn id(&self) -> ReviewId {
1660        self.id
1661    }
1662
1663    /// Review author.
1664    pub fn author(&self) -> &Author {
1665        &self.author
1666    }
1667
1668    /// Review verdict.
1669    pub fn verdict(&self) -> Option<Verdict> {
1670        self.verdict
1671    }
1672
1673    /// Review inline code comments.
1674    pub fn comments(&self) -> impl DoubleEndedIterator<Item = (&EntryId, &Comment<CodeLocation>)> {
1675        self.comments.comments()
1676    }
1677
1678    /// Review labels.
1679    pub fn labels(&self) -> impl Iterator<Item = &Label> {
1680        self.labels.iter()
1681    }
1682
1683    /// Review general comment.
1684    pub fn summary(&self) -> Option<&str> {
1685        self.summary.as_deref()
1686    }
1687
1688    /// Review timestamp.
1689    pub fn timestamp(&self) -> Timestamp {
1690        self.timestamp
1691    }
1692}
1693
1694impl<R: ReadRepository> store::Transaction<Patch, R> {
1695    pub fn edit(&mut self, title: impl ToString, target: MergeTarget) -> Result<(), store::Error> {
1696        self.push(Action::Edit {
1697            title: title.to_string(),
1698            target,
1699        })
1700    }
1701
1702    pub fn edit_revision(
1703        &mut self,
1704        revision: RevisionId,
1705        description: impl ToString,
1706        embeds: Vec<Embed<Uri>>,
1707    ) -> Result<(), store::Error> {
1708        self.embed(embeds.clone())?;
1709        self.push(Action::RevisionEdit {
1710            revision,
1711            description: description.to_string(),
1712            embeds,
1713        })
1714    }
1715
1716    /// Redact the revision.
1717    pub fn redact(&mut self, revision: RevisionId) -> Result<(), store::Error> {
1718        self.push(Action::RevisionRedact { revision })
1719    }
1720
1721    /// Start a patch revision discussion.
1722    pub fn thread<S: ToString>(
1723        &mut self,
1724        revision: RevisionId,
1725        body: S,
1726    ) -> Result<(), store::Error> {
1727        self.push(Action::RevisionComment {
1728            revision,
1729            body: body.to_string(),
1730            reply_to: None,
1731            location: None,
1732            embeds: vec![],
1733        })
1734    }
1735
1736    /// React on a patch revision.
1737    pub fn react(
1738        &mut self,
1739        revision: RevisionId,
1740        reaction: Reaction,
1741        location: Option<CodeLocation>,
1742        active: bool,
1743    ) -> Result<(), store::Error> {
1744        self.push(Action::RevisionReact {
1745            revision,
1746            reaction,
1747            location,
1748            active,
1749        })
1750    }
1751
1752    /// Comment on a patch revision.
1753    pub fn comment<S: ToString>(
1754        &mut self,
1755        revision: RevisionId,
1756        body: S,
1757        reply_to: Option<CommentId>,
1758        location: Option<CodeLocation>,
1759        embeds: Vec<Embed<Uri>>,
1760    ) -> Result<(), store::Error> {
1761        self.embed(embeds.clone())?;
1762        self.push(Action::RevisionComment {
1763            revision,
1764            body: body.to_string(),
1765            reply_to,
1766            location,
1767            embeds,
1768        })
1769    }
1770
1771    /// Edit a comment on a patch revision.
1772    pub fn comment_edit<S: ToString>(
1773        &mut self,
1774        revision: RevisionId,
1775        comment: CommentId,
1776        body: S,
1777        embeds: Vec<Embed<Uri>>,
1778    ) -> Result<(), store::Error> {
1779        self.embed(embeds.clone())?;
1780        self.push(Action::RevisionCommentEdit {
1781            revision,
1782            comment,
1783            body: body.to_string(),
1784            embeds,
1785        })
1786    }
1787
1788    /// React a comment on a patch revision.
1789    pub fn comment_react(
1790        &mut self,
1791        revision: RevisionId,
1792        comment: CommentId,
1793        reaction: Reaction,
1794        active: bool,
1795    ) -> Result<(), store::Error> {
1796        self.push(Action::RevisionCommentReact {
1797            revision,
1798            comment,
1799            reaction,
1800            active,
1801        })
1802    }
1803
1804    /// Redact a comment on a patch revision.
1805    pub fn comment_redact(
1806        &mut self,
1807        revision: RevisionId,
1808        comment: CommentId,
1809    ) -> Result<(), store::Error> {
1810        self.push(Action::RevisionCommentRedact { revision, comment })
1811    }
1812
1813    /// Comment on a review.
1814    pub fn review_comment<S: ToString>(
1815        &mut self,
1816        review: ReviewId,
1817        body: S,
1818        location: Option<CodeLocation>,
1819        reply_to: Option<CommentId>,
1820        embeds: Vec<Embed<Uri>>,
1821    ) -> Result<(), store::Error> {
1822        self.embed(embeds.clone())?;
1823        self.push(Action::ReviewComment {
1824            review,
1825            body: body.to_string(),
1826            location,
1827            reply_to,
1828            embeds,
1829        })
1830    }
1831
1832    /// Resolve a review comment.
1833    pub fn review_comment_resolve(
1834        &mut self,
1835        review: ReviewId,
1836        comment: CommentId,
1837    ) -> Result<(), store::Error> {
1838        self.push(Action::ReviewCommentResolve { review, comment })
1839    }
1840
1841    /// Unresolve a review comment.
1842    pub fn review_comment_unresolve(
1843        &mut self,
1844        review: ReviewId,
1845        comment: CommentId,
1846    ) -> Result<(), store::Error> {
1847        self.push(Action::ReviewCommentUnresolve { review, comment })
1848    }
1849
1850    /// Edit review comment.
1851    pub fn edit_review_comment<S: ToString>(
1852        &mut self,
1853        review: ReviewId,
1854        comment: EntryId,
1855        body: S,
1856        embeds: Vec<Embed<Uri>>,
1857    ) -> Result<(), store::Error> {
1858        self.embed(embeds.clone())?;
1859        self.push(Action::ReviewCommentEdit {
1860            review,
1861            comment,
1862            body: body.to_string(),
1863            embeds,
1864        })
1865    }
1866
1867    /// React to a review comment.
1868    pub fn react_review_comment(
1869        &mut self,
1870        review: ReviewId,
1871        comment: EntryId,
1872        reaction: Reaction,
1873        active: bool,
1874    ) -> Result<(), store::Error> {
1875        self.push(Action::ReviewCommentReact {
1876            review,
1877            comment,
1878            reaction,
1879            active,
1880        })
1881    }
1882
1883    /// Redact a review comment.
1884    pub fn redact_review_comment(
1885        &mut self,
1886        review: ReviewId,
1887        comment: EntryId,
1888    ) -> Result<(), store::Error> {
1889        self.push(Action::ReviewCommentRedact { review, comment })
1890    }
1891
1892    /// Review a patch revision.
1893    /// Does nothing if a review for that revision already exists by the author.
1894    pub fn review(
1895        &mut self,
1896        revision: RevisionId,
1897        verdict: Option<Verdict>,
1898        summary: Option<String>,
1899        labels: Vec<Label>,
1900    ) -> Result<(), store::Error> {
1901        self.push(Action::Review {
1902            revision,
1903            summary,
1904            verdict,
1905            labels,
1906        })
1907    }
1908
1909    /// Edit a review.
1910    pub fn review_edit(
1911        &mut self,
1912        review: ReviewId,
1913        verdict: Option<Verdict>,
1914        summary: Option<String>,
1915        labels: Vec<Label>,
1916    ) -> Result<(), store::Error> {
1917        self.push(Action::ReviewEdit {
1918            review,
1919            summary,
1920            verdict,
1921            labels,
1922        })
1923    }
1924
1925    /// Redact a patch review.
1926    pub fn redact_review(&mut self, review: ReviewId) -> Result<(), store::Error> {
1927        self.push(Action::ReviewRedact { review })
1928    }
1929
1930    /// Merge a patch revision.
1931    pub fn merge(&mut self, revision: RevisionId, commit: git::Oid) -> Result<(), store::Error> {
1932        self.push(Action::Merge { revision, commit })
1933    }
1934
1935    /// Update a patch with a new revision.
1936    pub fn revision(
1937        &mut self,
1938        description: impl ToString,
1939        base: impl Into<git::Oid>,
1940        oid: impl Into<git::Oid>,
1941    ) -> Result<(), store::Error> {
1942        self.push(Action::Revision {
1943            description: description.to_string(),
1944            base: base.into(),
1945            oid: oid.into(),
1946            resolves: BTreeSet::new(),
1947        })
1948    }
1949
1950    /// Lifecycle a patch.
1951    pub fn lifecycle(&mut self, state: Lifecycle) -> Result<(), store::Error> {
1952        self.push(Action::Lifecycle { state })
1953    }
1954
1955    /// Assign a patch.
1956    pub fn assign(&mut self, assignees: BTreeSet<Did>) -> Result<(), store::Error> {
1957        self.push(Action::Assign { assignees })
1958    }
1959
1960    /// Label a patch.
1961    pub fn label(&mut self, labels: impl IntoIterator<Item = Label>) -> Result<(), store::Error> {
1962        self.push(Action::Label {
1963            labels: labels.into_iter().collect(),
1964        })
1965    }
1966}
1967
1968pub struct PatchMut<'a, 'g, R, C> {
1969    pub id: ObjectId,
1970
1971    patch: Patch,
1972    store: &'g mut Patches<'a, R>,
1973    cache: &'g mut C,
1974}
1975
1976impl<'a, 'g, R, C> PatchMut<'a, 'g, R, C>
1977where
1978    C: cob::cache::Update<Patch>,
1979    R: ReadRepository + SignRepository + cob::Store<Namespace = NodeId>,
1980{
1981    pub fn new(id: ObjectId, patch: Patch, cache: &'g mut Cache<Patches<'a, R>, C>) -> Self {
1982        Self {
1983            id,
1984            patch,
1985            store: &mut cache.store,
1986            cache: &mut cache.cache,
1987        }
1988    }
1989
1990    pub fn id(&self) -> &ObjectId {
1991        &self.id
1992    }
1993
1994    /// Reload the patch data from storage.
1995    pub fn reload(&mut self) -> Result<(), store::Error> {
1996        self.patch = self
1997            .store
1998            .get(&self.id)?
1999            .ok_or_else(|| store::Error::NotFound(TYPENAME.clone(), self.id))?;
2000
2001        Ok(())
2002    }
2003
2004    pub fn transaction<G, F>(
2005        &mut self,
2006        message: &str,
2007        signer: &Device<G>,
2008        operations: F,
2009    ) -> Result<EntryId, Error>
2010    where
2011        G: crypto::signature::Signer<crypto::Signature>,
2012        F: FnOnce(&mut Transaction<Patch, R>) -> Result<(), store::Error>,
2013    {
2014        let mut tx = Transaction::default();
2015        operations(&mut tx)?;
2016
2017        let (patch, commit) = tx.commit(message, self.id, &mut self.store.raw, signer)?;
2018        self.cache
2019            .update(&self.store.as_ref().id(), &self.id, &patch)
2020            .map_err(|e| Error::CacheUpdate {
2021                id: self.id,
2022                err: e.into(),
2023            })?;
2024        self.patch = patch;
2025
2026        Ok(commit)
2027    }
2028
2029    /// Edit patch metadata.
2030    pub fn edit<G, S>(
2031        &mut self,
2032        title: String,
2033        target: MergeTarget,
2034        signer: &Device<G>,
2035    ) -> Result<EntryId, Error>
2036    where
2037        G: crypto::signature::Signer<crypto::Signature>,
2038        S: ToString,
2039    {
2040        self.transaction("Edit", signer, |tx| tx.edit(title, target))
2041    }
2042
2043    /// Edit revision metadata.
2044    pub fn edit_revision<G, S>(
2045        &mut self,
2046        revision: RevisionId,
2047        description: S,
2048        embeds: impl IntoIterator<Item = Embed<Uri>>,
2049        signer: &Device<G>,
2050    ) -> Result<EntryId, Error>
2051    where
2052        G: crypto::signature::Signer<crypto::Signature>,
2053        S: ToString,
2054    {
2055        self.transaction("Edit revision", signer, |tx| {
2056            tx.edit_revision(revision, description, embeds.into_iter().collect())
2057        })
2058    }
2059
2060    /// Redact a revision.
2061    pub fn redact<G>(&mut self, revision: RevisionId, signer: &Device<G>) -> Result<EntryId, Error>
2062    where
2063        G: crypto::signature::Signer<crypto::Signature>,
2064    {
2065        self.transaction("Redact revision", signer, |tx| tx.redact(revision))
2066    }
2067
2068    /// Create a thread on a patch revision.
2069    pub fn thread<G, S>(
2070        &mut self,
2071        revision: RevisionId,
2072        body: S,
2073        signer: &Device<G>,
2074    ) -> Result<CommentId, Error>
2075    where
2076        G: crypto::signature::Signer<crypto::Signature>,
2077        S: ToString,
2078    {
2079        self.transaction("Create thread", signer, |tx| tx.thread(revision, body))
2080    }
2081
2082    /// Comment on a patch revision.
2083    pub fn comment<G, S>(
2084        &mut self,
2085        revision: RevisionId,
2086        body: S,
2087        reply_to: Option<CommentId>,
2088        location: Option<CodeLocation>,
2089        embeds: impl IntoIterator<Item = Embed<Uri>>,
2090        signer: &Device<G>,
2091    ) -> Result<EntryId, Error>
2092    where
2093        G: crypto::signature::Signer<crypto::Signature>,
2094        S: ToString,
2095    {
2096        self.transaction("Comment", signer, |tx| {
2097            tx.comment(
2098                revision,
2099                body,
2100                reply_to,
2101                location,
2102                embeds.into_iter().collect(),
2103            )
2104        })
2105    }
2106
2107    /// React on a patch revision.
2108    pub fn react<G>(
2109        &mut self,
2110        revision: RevisionId,
2111        reaction: Reaction,
2112        location: Option<CodeLocation>,
2113        active: bool,
2114        signer: &Device<G>,
2115    ) -> Result<EntryId, Error>
2116    where
2117        G: crypto::signature::Signer<crypto::Signature>,
2118    {
2119        self.transaction("React", signer, |tx| {
2120            tx.react(revision, reaction, location, active)
2121        })
2122    }
2123
2124    /// Edit a comment on a patch revision.
2125    pub fn comment_edit<G, S>(
2126        &mut self,
2127        revision: RevisionId,
2128        comment: CommentId,
2129        body: S,
2130        embeds: impl IntoIterator<Item = Embed<Uri>>,
2131        signer: &Device<G>,
2132    ) -> Result<EntryId, Error>
2133    where
2134        G: crypto::signature::Signer<crypto::Signature>,
2135        S: ToString,
2136    {
2137        self.transaction("Edit comment", signer, |tx| {
2138            tx.comment_edit(revision, comment, body, embeds.into_iter().collect())
2139        })
2140    }
2141
2142    /// React to a comment on a patch revision.
2143    pub fn comment_react<G>(
2144        &mut self,
2145        revision: RevisionId,
2146        comment: CommentId,
2147        reaction: Reaction,
2148        active: bool,
2149        signer: &Device<G>,
2150    ) -> Result<EntryId, Error>
2151    where
2152        G: crypto::signature::Signer<crypto::Signature>,
2153    {
2154        self.transaction("React comment", signer, |tx| {
2155            tx.comment_react(revision, comment, reaction, active)
2156        })
2157    }
2158
2159    /// Redact a comment on a patch revision.
2160    pub fn comment_redact<G>(
2161        &mut self,
2162        revision: RevisionId,
2163        comment: CommentId,
2164        signer: &Device<G>,
2165    ) -> Result<EntryId, Error>
2166    where
2167        G: crypto::signature::Signer<crypto::Signature>,
2168    {
2169        self.transaction("Redact comment", signer, |tx| {
2170            tx.comment_redact(revision, comment)
2171        })
2172    }
2173
2174    /// Comment on a line of code as part of a review.
2175    pub fn review_comment<G, S>(
2176        &mut self,
2177        review: ReviewId,
2178        body: S,
2179        location: Option<CodeLocation>,
2180        reply_to: Option<CommentId>,
2181        embeds: impl IntoIterator<Item = Embed<Uri>>,
2182        signer: &Device<G>,
2183    ) -> Result<EntryId, Error>
2184    where
2185        G: crypto::signature::Signer<crypto::Signature>,
2186        S: ToString,
2187    {
2188        self.transaction("Review comment", signer, |tx| {
2189            tx.review_comment(
2190                review,
2191                body,
2192                location,
2193                reply_to,
2194                embeds.into_iter().collect(),
2195            )
2196        })
2197    }
2198
2199    /// Edit review comment.
2200    pub fn edit_review_comment<G, S>(
2201        &mut self,
2202        review: ReviewId,
2203        comment: EntryId,
2204        body: S,
2205        embeds: impl IntoIterator<Item = Embed<Uri>>,
2206        signer: &Device<G>,
2207    ) -> Result<EntryId, Error>
2208    where
2209        G: crypto::signature::Signer<crypto::Signature>,
2210        S: ToString,
2211    {
2212        self.transaction("Edit review comment", signer, |tx| {
2213            tx.edit_review_comment(review, comment, body, embeds.into_iter().collect())
2214        })
2215    }
2216
2217    /// React to a review comment.
2218    pub fn react_review_comment<G>(
2219        &mut self,
2220        review: ReviewId,
2221        comment: EntryId,
2222        reaction: Reaction,
2223        active: bool,
2224        signer: &Device<G>,
2225    ) -> Result<EntryId, Error>
2226    where
2227        G: crypto::signature::Signer<crypto::Signature>,
2228    {
2229        self.transaction("React to review comment", signer, |tx| {
2230            tx.react_review_comment(review, comment, reaction, active)
2231        })
2232    }
2233
2234    /// React to a review comment.
2235    pub fn redact_review_comment<G>(
2236        &mut self,
2237        review: ReviewId,
2238        comment: EntryId,
2239        signer: &Device<G>,
2240    ) -> Result<EntryId, Error>
2241    where
2242        G: crypto::signature::Signer<crypto::Signature>,
2243    {
2244        self.transaction("Redact review comment", signer, |tx| {
2245            tx.redact_review_comment(review, comment)
2246        })
2247    }
2248
2249    /// Review a patch revision.
2250    pub fn review<G>(
2251        &mut self,
2252        revision: RevisionId,
2253        verdict: Option<Verdict>,
2254        summary: Option<String>,
2255        labels: Vec<Label>,
2256        signer: &Device<G>,
2257    ) -> Result<ReviewId, Error>
2258    where
2259        G: crypto::signature::Signer<crypto::Signature>,
2260    {
2261        if verdict.is_none() && summary.is_none() {
2262            return Err(Error::EmptyReview);
2263        }
2264        self.transaction("Review", signer, |tx| {
2265            tx.review(revision, verdict, summary, labels)
2266        })
2267        .map(ReviewId)
2268    }
2269
2270    /// Edit a review.
2271    pub fn review_edit<G>(
2272        &mut self,
2273        review: ReviewId,
2274        verdict: Option<Verdict>,
2275        summary: Option<String>,
2276        labels: Vec<Label>,
2277        signer: &Device<G>,
2278    ) -> Result<EntryId, Error>
2279    where
2280        G: crypto::signature::Signer<crypto::Signature>,
2281    {
2282        self.transaction("Edit review", signer, |tx| {
2283            tx.review_edit(review, verdict, summary, labels)
2284        })
2285    }
2286
2287    /// Redact a patch review.
2288    pub fn redact_review<G>(
2289        &mut self,
2290        review: ReviewId,
2291        signer: &Device<G>,
2292    ) -> Result<EntryId, Error>
2293    where
2294        G: crypto::signature::Signer<crypto::Signature>,
2295    {
2296        self.transaction("Redact review", signer, |tx| tx.redact_review(review))
2297    }
2298
2299    /// Resolve a patch review comment.
2300    pub fn resolve_review_comment<G>(
2301        &mut self,
2302        review: ReviewId,
2303        comment: CommentId,
2304        signer: &Device<G>,
2305    ) -> Result<EntryId, Error>
2306    where
2307        G: crypto::signature::Signer<crypto::Signature>,
2308    {
2309        self.transaction("Resolve review comment", signer, |tx| {
2310            tx.review_comment_resolve(review, comment)
2311        })
2312    }
2313
2314    /// Unresolve a patch review comment.
2315    pub fn unresolve_review_comment<G>(
2316        &mut self,
2317        review: ReviewId,
2318        comment: CommentId,
2319        signer: &Device<G>,
2320    ) -> Result<EntryId, Error>
2321    where
2322        G: crypto::signature::Signer<crypto::Signature>,
2323    {
2324        self.transaction("Unresolve review comment", signer, |tx| {
2325            tx.review_comment_unresolve(review, comment)
2326        })
2327    }
2328
2329    /// Merge a patch revision.
2330    pub fn merge<G>(
2331        &mut self,
2332        revision: RevisionId,
2333        commit: git::Oid,
2334        signer: &Device<G>,
2335    ) -> Result<Merged<R>, Error>
2336    where
2337        G: crypto::signature::Signer<crypto::Signature>,
2338    {
2339        // TODO: Don't allow merging the same revision twice?
2340        let entry = self.transaction("Merge revision", signer, |tx| tx.merge(revision, commit))?;
2341
2342        Ok(Merged {
2343            entry,
2344            patch: self.id,
2345            stored: self.store.as_ref(),
2346        })
2347    }
2348
2349    /// Update a patch with a new revision.
2350    pub fn update<G>(
2351        &mut self,
2352        description: impl ToString,
2353        base: impl Into<git::Oid>,
2354        oid: impl Into<git::Oid>,
2355        signer: &Device<G>,
2356    ) -> Result<RevisionId, Error>
2357    where
2358        G: crypto::signature::Signer<crypto::Signature>,
2359    {
2360        self.transaction("Add revision", signer, |tx| {
2361            tx.revision(description, base, oid)
2362        })
2363        .map(RevisionId)
2364    }
2365
2366    /// Lifecycle a patch.
2367    pub fn lifecycle<G>(&mut self, state: Lifecycle, signer: &Device<G>) -> Result<EntryId, Error>
2368    where
2369        G: crypto::signature::Signer<crypto::Signature>,
2370    {
2371        self.transaction("Lifecycle", signer, |tx| tx.lifecycle(state))
2372    }
2373
2374    /// Assign a patch.
2375    pub fn assign<G>(
2376        &mut self,
2377        assignees: BTreeSet<Did>,
2378        signer: &Device<G>,
2379    ) -> Result<EntryId, Error>
2380    where
2381        G: crypto::signature::Signer<crypto::Signature>,
2382    {
2383        self.transaction("Assign", signer, |tx| tx.assign(assignees))
2384    }
2385
2386    /// Archive a patch.
2387    pub fn archive<G>(&mut self, signer: &Device<G>) -> Result<bool, Error>
2388    where
2389        G: crypto::signature::Signer<crypto::Signature>,
2390    {
2391        self.lifecycle(Lifecycle::Archived, signer)?;
2392
2393        Ok(true)
2394    }
2395
2396    /// Mark an archived patch as ready to be reviewed again.
2397    /// Returns `false` if the patch was not archived.
2398    pub fn unarchive<G>(&mut self, signer: &Device<G>) -> Result<bool, Error>
2399    where
2400        G: crypto::signature::Signer<crypto::Signature>,
2401    {
2402        if !self.is_archived() {
2403            return Ok(false);
2404        }
2405        self.lifecycle(Lifecycle::Open, signer)?;
2406
2407        Ok(true)
2408    }
2409
2410    /// Mark a patch as ready to be reviewed.
2411    /// Returns `false` if the patch was not a draft.
2412    pub fn ready<G>(&mut self, signer: &Device<G>) -> Result<bool, Error>
2413    where
2414        G: crypto::signature::Signer<crypto::Signature>,
2415    {
2416        if !self.is_draft() {
2417            return Ok(false);
2418        }
2419        self.lifecycle(Lifecycle::Open, signer)?;
2420
2421        Ok(true)
2422    }
2423
2424    /// Mark an open patch as a draft.
2425    /// Returns `false` if the patch was not open and free of merges.
2426    pub fn unready<G>(&mut self, signer: &Device<G>) -> Result<bool, Error>
2427    where
2428        G: crypto::signature::Signer<crypto::Signature>,
2429    {
2430        if !matches!(self.state(), State::Open { conflicts } if conflicts.is_empty()) {
2431            return Ok(false);
2432        }
2433        self.lifecycle(Lifecycle::Draft, signer)?;
2434
2435        Ok(true)
2436    }
2437
2438    /// Label a patch.
2439    pub fn label<G>(
2440        &mut self,
2441        labels: impl IntoIterator<Item = Label>,
2442        signer: &Device<G>,
2443    ) -> Result<EntryId, Error>
2444    where
2445        G: crypto::signature::Signer<crypto::Signature>,
2446    {
2447        self.transaction("Label", signer, |tx| tx.label(labels))
2448    }
2449}
2450
2451impl<R, C> Deref for PatchMut<'_, '_, R, C> {
2452    type Target = Patch;
2453
2454    fn deref(&self) -> &Self::Target {
2455        &self.patch
2456    }
2457}
2458
2459/// Detailed information on patch states
2460#[derive(Debug, Default, PartialEq, Eq, Serialize)]
2461#[serde(rename_all = "camelCase")]
2462pub struct PatchCounts {
2463    pub open: usize,
2464    pub draft: usize,
2465    pub archived: usize,
2466    pub merged: usize,
2467}
2468
2469impl PatchCounts {
2470    /// Total count.
2471    pub fn total(&self) -> usize {
2472        self.open + self.draft + self.archived + self.merged
2473    }
2474}
2475
2476/// Result of looking up a `Patch`'s `Revision`.
2477///
2478/// See [`Patches::find_by_revision`].
2479#[derive(Debug, PartialEq, Eq)]
2480pub struct ByRevision {
2481    pub id: PatchId,
2482    pub patch: Patch,
2483    pub revision_id: RevisionId,
2484    pub revision: Revision,
2485}
2486
2487pub struct Patches<'a, R> {
2488    raw: store::Store<'a, Patch, R>,
2489}
2490
2491impl<'a, R> Deref for Patches<'a, R> {
2492    type Target = store::Store<'a, Patch, R>;
2493
2494    fn deref(&self) -> &Self::Target {
2495        &self.raw
2496    }
2497}
2498
2499impl<R> HasRepoId for Patches<'_, R>
2500where
2501    R: ReadRepository,
2502{
2503    fn rid(&self) -> RepoId {
2504        self.as_ref().id()
2505    }
2506}
2507
2508impl<'a, R> Patches<'a, R>
2509where
2510    R: ReadRepository + cob::Store<Namespace = NodeId>,
2511{
2512    /// Open a patches store.
2513    pub fn open(repository: &'a R) -> Result<Self, RepositoryError> {
2514        let identity = repository.identity_head()?;
2515        let raw = store::Store::open(repository)?.identity(identity);
2516
2517        Ok(Self { raw })
2518    }
2519
2520    /// Patches count by state.
2521    pub fn counts(&self) -> Result<PatchCounts, store::Error> {
2522        let all = self.all()?;
2523        let state_groups =
2524            all.filter_map(|s| s.ok())
2525                .fold(PatchCounts::default(), |mut state, (_, p)| {
2526                    match p.state() {
2527                        State::Draft => state.draft += 1,
2528                        State::Open { .. } => state.open += 1,
2529                        State::Archived => state.archived += 1,
2530                        State::Merged { .. } => state.merged += 1,
2531                    }
2532                    state
2533                });
2534
2535        Ok(state_groups)
2536    }
2537
2538    /// Find the `Patch` containing the given `Revision`.
2539    pub fn find_by_revision(&self, revision: &RevisionId) -> Result<Option<ByRevision>, Error> {
2540        // Revision may be the patch's first, making it have the same ID.
2541        let p_id = ObjectId::from(revision.into_inner());
2542        if let Some(p) = self.get(&p_id)? {
2543            return Ok(p.revision(revision).map(|r| ByRevision {
2544                id: p_id,
2545                patch: p.clone(),
2546                revision_id: *revision,
2547                revision: r.clone(),
2548            }));
2549        }
2550        let result = self
2551            .all()?
2552            .filter_map(|result| result.ok())
2553            .find_map(|(p_id, p)| {
2554                p.revision(revision).map(|r| ByRevision {
2555                    id: p_id,
2556                    patch: p.clone(),
2557                    revision_id: *revision,
2558                    revision: r.clone(),
2559                })
2560            });
2561
2562        Ok(result)
2563    }
2564
2565    /// Get a patch.
2566    pub fn get(&self, id: &ObjectId) -> Result<Option<Patch>, store::Error> {
2567        self.raw.get(id)
2568    }
2569
2570    /// Get proposed patches.
2571    pub fn proposed(&self) -> Result<impl Iterator<Item = (PatchId, Patch)> + '_, Error> {
2572        let all = self.all()?;
2573
2574        Ok(all
2575            .into_iter()
2576            .filter_map(|result| result.ok())
2577            .filter(|(_, p)| p.is_open()))
2578    }
2579
2580    /// Get patches proposed by the given key.
2581    pub fn proposed_by<'b>(
2582        &'b self,
2583        who: &'b Did,
2584    ) -> Result<impl Iterator<Item = (PatchId, Patch)> + 'b, Error> {
2585        Ok(self
2586            .proposed()?
2587            .filter(move |(_, p)| p.author().id() == who))
2588    }
2589}
2590
2591impl<'a, R> Patches<'a, R>
2592where
2593    R: ReadRepository + SignRepository + cob::Store<Namespace = NodeId>,
2594{
2595    /// Open a new patch.
2596    pub fn create<'g, C, G>(
2597        &'g mut self,
2598        title: impl ToString,
2599        description: impl ToString,
2600        target: MergeTarget,
2601        base: impl Into<git::Oid>,
2602        oid: impl Into<git::Oid>,
2603        labels: &[Label],
2604        cache: &'g mut C,
2605        signer: &Device<G>,
2606    ) -> Result<PatchMut<'a, 'g, R, C>, Error>
2607    where
2608        C: cob::cache::Update<Patch>,
2609        G: crypto::signature::Signer<crypto::Signature>,
2610    {
2611        self._create(
2612            title,
2613            description,
2614            target,
2615            base,
2616            oid,
2617            labels,
2618            Lifecycle::default(),
2619            cache,
2620            signer,
2621        )
2622    }
2623
2624    /// Draft a patch. This patch will be created in a [`State::Draft`] state.
2625    pub fn draft<'g, C, G>(
2626        &'g mut self,
2627        title: impl ToString,
2628        description: impl ToString,
2629        target: MergeTarget,
2630        base: impl Into<git::Oid>,
2631        oid: impl Into<git::Oid>,
2632        labels: &[Label],
2633        cache: &'g mut C,
2634        signer: &Device<G>,
2635    ) -> Result<PatchMut<'a, 'g, R, C>, Error>
2636    where
2637        C: cob::cache::Update<Patch>,
2638        G: crypto::signature::Signer<crypto::Signature>,
2639    {
2640        self._create(
2641            title,
2642            description,
2643            target,
2644            base,
2645            oid,
2646            labels,
2647            Lifecycle::Draft,
2648            cache,
2649            signer,
2650        )
2651    }
2652
2653    /// Get a patch mutably.
2654    pub fn get_mut<'g, C>(
2655        &'g mut self,
2656        id: &ObjectId,
2657        cache: &'g mut C,
2658    ) -> Result<PatchMut<'a, 'g, R, C>, store::Error> {
2659        let patch = self
2660            .raw
2661            .get(id)?
2662            .ok_or_else(move || store::Error::NotFound(TYPENAME.clone(), *id))?;
2663
2664        Ok(PatchMut {
2665            id: *id,
2666            patch,
2667            store: self,
2668            cache,
2669        })
2670    }
2671
2672    /// Create a patch. This is an internal function used by `create` and `draft`.
2673    fn _create<'g, C, G>(
2674        &'g mut self,
2675        title: impl ToString,
2676        description: impl ToString,
2677        target: MergeTarget,
2678        base: impl Into<git::Oid>,
2679        oid: impl Into<git::Oid>,
2680        labels: &[Label],
2681        state: Lifecycle,
2682        cache: &'g mut C,
2683        signer: &Device<G>,
2684    ) -> Result<PatchMut<'a, 'g, R, C>, Error>
2685    where
2686        C: cob::cache::Update<Patch>,
2687        G: crypto::signature::Signer<crypto::Signature>,
2688    {
2689        let (id, patch) = Transaction::initial("Create patch", &mut self.raw, signer, |tx, _| {
2690            tx.revision(description, base, oid)?;
2691            tx.edit(title, target)?;
2692
2693            if !labels.is_empty() {
2694                tx.label(labels.to_owned())?;
2695            }
2696            if state != Lifecycle::default() {
2697                tx.lifecycle(state)?;
2698            }
2699            Ok(())
2700        })?;
2701        cache
2702            .update(&self.raw.as_ref().id(), &id, &patch)
2703            .map_err(|e| Error::CacheUpdate { id, err: e.into() })?;
2704
2705        Ok(PatchMut {
2706            id,
2707            patch,
2708            store: self,
2709            cache,
2710        })
2711    }
2712}
2713
2714/// Models a comparison between two commit ranges,
2715/// commonly obtained from two revisions (likely of the same patch).
2716/// This can be used to generate a `git range-diff` command.
2717/// See <https://git-scm.com/docs/git-range-diff>.
2718#[derive(Debug, Clone, PartialEq, Eq, Copy)]
2719pub struct RangeDiff {
2720    old: (git::Oid, git::Oid),
2721    new: (git::Oid, git::Oid),
2722}
2723
2724impl RangeDiff {
2725    const COMMAND: &str = "git";
2726    const SUBCOMMAND: &str = "range-diff";
2727
2728    pub fn new(old: &Revision, new: &Revision) -> Self {
2729        Self {
2730            old: old.range(),
2731            new: new.range(),
2732        }
2733    }
2734
2735    pub fn to_command(&self) -> String {
2736        let range = if self.has_same_base() {
2737            format!("{} {} {}", self.old.0, self.old.1, self.new.1)
2738        } else {
2739            format!(
2740                "{}..{} {}..{}",
2741                self.old.0, self.old.1, self.new.0, self.new.1,
2742            )
2743        };
2744
2745        Self::COMMAND.to_string() + " " + Self::SUBCOMMAND + " " + &range
2746    }
2747
2748    fn has_same_base(&self) -> bool {
2749        self.old.0 == self.new.0
2750    }
2751}
2752
2753impl From<RangeDiff> for std::process::Command {
2754    fn from(range_diff: RangeDiff) -> Self {
2755        let mut command = std::process::Command::new(RangeDiff::COMMAND);
2756
2757        command.arg(RangeDiff::SUBCOMMAND);
2758
2759        if range_diff.has_same_base() {
2760            command.args([
2761                range_diff.old.0.to_string(),
2762                range_diff.old.1.to_string(),
2763                range_diff.new.1.to_string(),
2764            ]);
2765        } else {
2766            command.args([
2767                format!("{}..{}", range_diff.old.0, range_diff.old.1),
2768                format!("{}..{}", range_diff.new.0, range_diff.new.1),
2769            ]);
2770        }
2771        command
2772    }
2773}
2774
2775/// Helpers for de/serialization of patch data types.
2776mod ser {
2777    use std::collections::{BTreeMap, BTreeSet};
2778
2779    use serde::ser::SerializeSeq;
2780
2781    use crate::cob::{thread::Reactions, ActorId, CodeLocation};
2782
2783    /// Serialize a `Revision`'s reaction as an object containing the
2784    /// `location`, `emoji`, and all `authors` that have performed the
2785    /// same reaction.
2786    #[derive(Debug, serde::Serialize, serde::Deserialize)]
2787    #[serde(rename_all = "camelCase")]
2788    struct Reaction {
2789        location: Option<CodeLocation>,
2790        emoji: super::Reaction,
2791        authors: Vec<ActorId>,
2792    }
2793
2794    impl Reaction {
2795        fn as_revision_reactions(
2796            reactions: Vec<Reaction>,
2797        ) -> BTreeMap<Option<CodeLocation>, Reactions> {
2798            reactions.into_iter().fold(
2799                BTreeMap::<Option<CodeLocation>, Reactions>::new(),
2800                |mut reactions,
2801                 Reaction {
2802                     location,
2803                     emoji,
2804                     authors,
2805                 }| {
2806                    let mut inner = authors
2807                        .into_iter()
2808                        .map(|author| (author, emoji))
2809                        .collect::<BTreeSet<_>>();
2810                    let entry = reactions.entry(location).or_default();
2811                    entry.append(&mut inner);
2812                    reactions
2813                },
2814            )
2815        }
2816    }
2817
2818    /// Helper to serialize a `Revision`'s reactions, since
2819    /// `CodeLocation` cannot be a key for a JSON object.
2820    ///
2821    /// The set `reactions` are first turned into a set of
2822    /// [`Reaction`]s and then serialized via a `Vec`.
2823    pub fn serialize_reactions<S>(
2824        reactions: &BTreeMap<Option<CodeLocation>, Reactions>,
2825        serializer: S,
2826    ) -> Result<S::Ok, S::Error>
2827    where
2828        S: serde::Serializer,
2829    {
2830        let reactions = reactions
2831            .iter()
2832            .flat_map(|(location, reaction)| {
2833                let reactions = reaction.iter().fold(
2834                    BTreeMap::new(),
2835                    |mut acc: BTreeMap<&super::Reaction, Vec<_>>, (author, emoji)| {
2836                        acc.entry(emoji).or_default().push(*author);
2837                        acc
2838                    },
2839                );
2840                reactions
2841                    .into_iter()
2842                    .map(|(emoji, authors)| Reaction {
2843                        location: location.clone(),
2844                        emoji: *emoji,
2845                        authors,
2846                    })
2847                    .collect::<Vec<_>>()
2848            })
2849            .collect::<Vec<_>>();
2850        let mut s = serializer.serialize_seq(Some(reactions.len()))?;
2851        for r in &reactions {
2852            s.serialize_element(r)?;
2853        }
2854        s.end()
2855    }
2856
2857    /// Helper to deserialize a `Revision`'s reactions, the inverse of
2858    /// `serialize_reactions`.
2859    ///
2860    /// The `Vec` of [`Reaction`]s are deserialized and converted to a
2861    /// `BTreeMap<Option<CodeLocation>, Reactions>`.
2862    pub fn deserialize_reactions<'de, D>(
2863        deserializer: D,
2864    ) -> Result<BTreeMap<Option<CodeLocation>, Reactions>, D::Error>
2865    where
2866        D: serde::Deserializer<'de>,
2867    {
2868        struct ReactionsVisitor;
2869
2870        impl<'de> serde::de::Visitor<'de> for ReactionsVisitor {
2871            type Value = Vec<Reaction>;
2872
2873            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
2874                formatter.write_str("a reaction of the form {'location', 'emoji', 'authors'}")
2875            }
2876
2877            fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
2878            where
2879                A: serde::de::SeqAccess<'de>,
2880            {
2881                let mut reactions = Vec::new();
2882                while let Some(reaction) = seq.next_element()? {
2883                    reactions.push(reaction);
2884                }
2885                Ok(reactions)
2886            }
2887        }
2888
2889        let reactions = deserializer.deserialize_seq(ReactionsVisitor)?;
2890        Ok(Reaction::as_revision_reactions(reactions))
2891    }
2892}
2893
2894#[cfg(test)]
2895#[allow(clippy::unwrap_used)]
2896mod test {
2897    use std::path::PathBuf;
2898    use std::str::FromStr;
2899    use std::vec;
2900
2901    use pretty_assertions::assert_eq;
2902
2903    use super::*;
2904    use crate::cob::common::CodeRange;
2905    use crate::cob::test::Actor;
2906    use crate::crypto::test::signer::MockSigner;
2907    use crate::identity;
2908    use crate::patch::cache::Patches as _;
2909    use crate::profile::env;
2910    use crate::test;
2911    use crate::test::arbitrary;
2912    use crate::test::arbitrary::gen;
2913    use crate::test::storage::MockRepository;
2914
2915    use cob::migrate;
2916
2917    #[test]
2918    fn test_json_serialization() {
2919        let edit = Action::Label {
2920            labels: BTreeSet::new(),
2921        };
2922        assert_eq!(
2923            serde_json::to_string(&edit).unwrap(),
2924            String::from(r#"{"type":"label","labels":[]}"#)
2925        );
2926    }
2927
2928    #[test]
2929    fn test_reactions_json_serialization() {
2930        #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
2931        #[serde(rename_all = "camelCase")]
2932        struct TestReactions {
2933            #[serde(
2934                serialize_with = "super::ser::serialize_reactions",
2935                deserialize_with = "super::ser::deserialize_reactions"
2936            )]
2937            inner: BTreeMap<Option<CodeLocation>, Reactions>,
2938        }
2939
2940        let reactions = TestReactions {
2941            inner: [(
2942                None,
2943                [
2944                    (
2945                        "z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8"
2946                            .parse()
2947                            .unwrap(),
2948                        Reaction::new('🚀').unwrap(),
2949                    ),
2950                    (
2951                        "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
2952                            .parse()
2953                            .unwrap(),
2954                        Reaction::new('🙏').unwrap(),
2955                    ),
2956                ]
2957                .into_iter()
2958                .collect(),
2959            )]
2960            .into_iter()
2961            .collect(),
2962        };
2963
2964        assert_eq!(
2965            reactions,
2966            serde_json::from_str(&serde_json::to_string(&reactions).unwrap()).unwrap()
2967        );
2968    }
2969
2970    #[test]
2971    fn test_patch_create_and_get() {
2972        let alice = test::setup::NodeWithRepo::default();
2973        let checkout = alice.repo.checkout();
2974        let branch = checkout.branch_with([("README", b"Hello World!")]);
2975        let mut patches = Cache::no_cache(&*alice.repo).unwrap();
2976        let author: Did = alice.signer.public_key().into();
2977        let target = MergeTarget::Delegates;
2978        let patch = patches
2979            .create(
2980                "My first patch",
2981                "Blah blah blah.",
2982                target,
2983                branch.base,
2984                branch.oid,
2985                &[],
2986                &alice.signer,
2987            )
2988            .unwrap();
2989
2990        let patch_id = patch.id;
2991        let patch = patches.get(&patch_id).unwrap().unwrap();
2992
2993        assert_eq!(patch.title(), "My first patch");
2994        assert_eq!(patch.description(), "Blah blah blah.");
2995        assert_eq!(patch.author().id(), &author);
2996        assert_eq!(patch.state(), &State::Open { conflicts: vec![] });
2997        assert_eq!(patch.target(), target);
2998        assert_eq!(patch.version(), 0);
2999
3000        let (rev_id, revision) = patch.latest();
3001
3002        assert_eq!(revision.author.id(), &author);
3003        assert_eq!(revision.description(), "Blah blah blah.");
3004        assert_eq!(revision.discussion.len(), 0);
3005        assert_eq!(revision.oid, branch.oid);
3006        assert_eq!(revision.base, branch.base);
3007
3008        let ByRevision { id, .. } = patches.find_by_revision(&rev_id).unwrap().unwrap();
3009        assert_eq!(id, patch_id);
3010    }
3011
3012    #[test]
3013    fn test_patch_discussion() {
3014        let alice = test::setup::NodeWithRepo::default();
3015        let checkout = alice.repo.checkout();
3016        let branch = checkout.branch_with([("README", b"Hello World!")]);
3017        let mut patches = Cache::no_cache(&*alice.repo).unwrap();
3018        let patch = patches
3019            .create(
3020                "My first patch",
3021                "Blah blah blah.",
3022                MergeTarget::Delegates,
3023                branch.base,
3024                branch.oid,
3025                &[],
3026                &alice.signer,
3027            )
3028            .unwrap();
3029
3030        let id = patch.id;
3031        let mut patch = patches.get_mut(&id).unwrap();
3032        let (revision_id, _) = patch.revisions().last().unwrap();
3033        assert!(
3034            patch
3035                .comment(revision_id, "patch comment", None, None, [], &alice.signer)
3036                .is_ok(),
3037            "can comment on patch"
3038        );
3039
3040        let (_, revision) = patch.revisions().last().unwrap();
3041        let (_, comment) = revision.discussion.first().unwrap();
3042        assert_eq!("patch comment", comment.body(), "comment body untouched");
3043    }
3044
3045    #[test]
3046    fn test_patch_merge() {
3047        let alice = test::setup::NodeWithRepo::default();
3048        let checkout = alice.repo.checkout();
3049        let branch = checkout.branch_with([("README", b"Hello World!")]);
3050        let mut patches = Cache::no_cache(&*alice.repo).unwrap();
3051        let mut patch = patches
3052            .create(
3053                "My first patch",
3054                "Blah blah blah.",
3055                MergeTarget::Delegates,
3056                branch.base,
3057                branch.oid,
3058                &[],
3059                &alice.signer,
3060            )
3061            .unwrap();
3062
3063        let id = patch.id;
3064        let (rid, _) = patch.revisions().next().unwrap();
3065        let _merge = patch.merge(rid, branch.base, &alice.signer).unwrap();
3066        let patch = patches.get(&id).unwrap().unwrap();
3067
3068        let merges = patch.merges.iter().collect::<Vec<_>>();
3069        assert_eq!(merges.len(), 1);
3070
3071        let (merger, merge) = merges.first().unwrap();
3072        assert_eq!(*merger, alice.signer.public_key());
3073        assert_eq!(merge.commit, branch.base);
3074    }
3075
3076    #[test]
3077    fn test_patch_review() {
3078        let alice = test::setup::NodeWithRepo::default();
3079        let checkout = alice.repo.checkout();
3080        let branch = checkout.branch_with([("README", b"Hello World!")]);
3081        let mut patches = Cache::no_cache(&*alice.repo).unwrap();
3082        let mut patch = patches
3083            .create(
3084                "My first patch",
3085                "Blah blah blah.",
3086                MergeTarget::Delegates,
3087                branch.base,
3088                branch.oid,
3089                &[],
3090                &alice.signer,
3091            )
3092            .unwrap();
3093
3094        let (revision_id, _) = patch.latest();
3095        let review_id = patch
3096            .review(
3097                revision_id,
3098                Some(Verdict::Accept),
3099                Some("LGTM".to_owned()),
3100                vec![],
3101                &alice.signer,
3102            )
3103            .unwrap();
3104
3105        let id = patch.id;
3106        let mut patch = patches.get_mut(&id).unwrap();
3107        let (_, revision) = patch.latest();
3108        assert_eq!(revision.reviews.len(), 1);
3109
3110        let review = revision.review_by(alice.signer.public_key()).unwrap();
3111        assert_eq!(review.verdict(), Some(Verdict::Accept));
3112        assert_eq!(review.summary(), Some("LGTM"));
3113
3114        patch.redact_review(review_id, &alice.signer).unwrap();
3115        patch.reload().unwrap();
3116
3117        let (_, revision) = patch.latest();
3118        assert_eq!(revision.reviews().count(), 0);
3119
3120        // This is fine, redacting an already-redacted review is a no-op.
3121        patch.redact_review(review_id, &alice.signer).unwrap();
3122        // If the review never existed, it's an error.
3123        patch
3124            .redact_review(ReviewId(arbitrary::entry_id()), &alice.signer)
3125            .unwrap_err();
3126    }
3127
3128    #[test]
3129    fn test_patch_review_revision_redact() {
3130        let alice = test::setup::NodeWithRepo::default();
3131        let checkout = alice.repo.checkout();
3132        let branch = checkout.branch_with([("README", b"Hello World!")]);
3133        let mut patches = Cache::no_cache(&*alice.repo).unwrap();
3134        let mut patch = patches
3135            .create(
3136                "My first patch",
3137                "Blah blah blah.",
3138                MergeTarget::Delegates,
3139                branch.base,
3140                branch.oid,
3141                &[],
3142                &alice.signer,
3143            )
3144            .unwrap();
3145
3146        let update = checkout.branch_with([("README", b"Hello Radicle!")]);
3147        let updated = patch
3148            .update("I've made changes.", branch.base, update.oid, &alice.signer)
3149            .unwrap();
3150
3151        // It's fine to redact a review from a redacted revision.
3152        let review = patch
3153            .review(updated, Some(Verdict::Accept), None, vec![], &alice.signer)
3154            .unwrap();
3155        patch.redact(updated, &alice.signer).unwrap();
3156        patch.redact_review(review, &alice.signer).unwrap();
3157    }
3158
3159    #[test]
3160    fn test_revision_review_merge_redacted() {
3161        let base = git::Oid::from_str("cb18e95ada2bb38aadd8e6cef0963ce37a87add3").unwrap();
3162        let oid = git::Oid::from_str("518d5069f94c03427f694bb494ac1cd7d1339380").unwrap();
3163        let mut alice = Actor::<MockSigner>::default();
3164        let rid = gen::<RepoId>(1);
3165        let doc = RawDoc::new(
3166            gen::<Project>(1),
3167            vec![alice.did()],
3168            1,
3169            identity::Visibility::Public,
3170        )
3171        .verified()
3172        .unwrap();
3173        let repo = MockRepository::new(rid, doc);
3174
3175        let a1 = alice.op::<Patch>([
3176            Action::Revision {
3177                description: String::new(),
3178                base,
3179                oid,
3180                resolves: Default::default(),
3181            },
3182            Action::Edit {
3183                title: String::from("My patch"),
3184                target: MergeTarget::Delegates,
3185            },
3186        ]);
3187        let a2 = alice.op::<Patch>([Action::Revision {
3188            description: String::from("Second revision"),
3189            base,
3190            oid,
3191            resolves: Default::default(),
3192        }]);
3193        let a3 = alice.op::<Patch>([Action::RevisionRedact {
3194            revision: RevisionId(a2.id()),
3195        }]);
3196        let a4 = alice.op::<Patch>([Action::Review {
3197            revision: RevisionId(a2.id()),
3198            summary: None,
3199            verdict: Some(Verdict::Accept),
3200            labels: vec![],
3201        }]);
3202        let a5 = alice.op::<Patch>([Action::Merge {
3203            revision: RevisionId(a2.id()),
3204            commit: oid,
3205        }]);
3206
3207        let mut patch = Patch::from_ops([a1, a2], &repo).unwrap();
3208        assert_eq!(patch.revisions().count(), 2);
3209
3210        patch.op(a3, [], &repo).unwrap();
3211        assert_eq!(patch.revisions().count(), 1);
3212
3213        patch.op(a4, [], &repo).unwrap();
3214        patch.op(a5, [], &repo).unwrap();
3215    }
3216
3217    #[test]
3218    fn test_revision_edit_redact() {
3219        let base = arbitrary::oid();
3220        let oid = arbitrary::oid();
3221        let repo = gen::<MockRepository>(1);
3222        let time = env::local_time();
3223        let alice = MockSigner::default();
3224        let bob = MockSigner::default();
3225        let mut h0: cob::test::HistoryBuilder<Patch> = cob::test::history(
3226            &[
3227                Action::Revision {
3228                    description: String::from("Original"),
3229                    base,
3230                    oid,
3231                    resolves: Default::default(),
3232                },
3233                Action::Edit {
3234                    title: String::from("Some patch"),
3235                    target: MergeTarget::Delegates,
3236                },
3237            ],
3238            time.into(),
3239            &alice,
3240        );
3241        let r1 = h0.commit(
3242            &Action::Revision {
3243                description: String::from("New"),
3244                base,
3245                oid,
3246                resolves: Default::default(),
3247            },
3248            &alice,
3249        );
3250        let patch = Patch::from_history(&h0, &repo).unwrap();
3251        assert_eq!(patch.revisions().count(), 2);
3252
3253        let mut h1 = h0.clone();
3254        h1.commit(
3255            &Action::RevisionRedact {
3256                revision: RevisionId(r1),
3257            },
3258            &alice,
3259        );
3260
3261        let mut h2 = h0.clone();
3262        h2.commit(
3263            &Action::RevisionEdit {
3264                revision: RevisionId(*h0.root().id()),
3265                description: String::from("Edited"),
3266                embeds: Vec::default(),
3267            },
3268            &bob,
3269        );
3270
3271        h0.merge(h1);
3272        h0.merge(h2);
3273
3274        let patch = Patch::from_history(&h0, &repo).unwrap();
3275        assert_eq!(patch.revisions().count(), 1);
3276    }
3277
3278    #[test]
3279    fn test_revision_reaction() {
3280        let base = git::Oid::from_str("cb18e95ada2bb38aadd8e6cef0963ce37a87add3").unwrap();
3281        let oid = git::Oid::from_str("518d5069f94c03427f694bb494ac1cd7d1339380").unwrap();
3282        let mut alice = Actor::<MockSigner>::default();
3283        let repo = gen::<MockRepository>(1);
3284        let reaction = Reaction::new('👍').expect("failed to create a reaction");
3285
3286        let a1 = alice.op::<Patch>([
3287            Action::Revision {
3288                description: String::new(),
3289                base,
3290                oid,
3291                resolves: Default::default(),
3292            },
3293            Action::Edit {
3294                title: String::from("My patch"),
3295                target: MergeTarget::Delegates,
3296            },
3297        ]);
3298        let a2 = alice.op::<Patch>([Action::RevisionReact {
3299            revision: RevisionId(a1.id()),
3300            location: None,
3301            reaction,
3302            active: true,
3303        }]);
3304        let patch = Patch::from_ops([a1, a2], &repo).unwrap();
3305
3306        let (_, r1) = patch.revisions().next().unwrap();
3307        assert!(!r1.reactions.is_empty());
3308
3309        let mut reactions = r1.reactions.get(&None).unwrap().clone();
3310        assert!(!reactions.is_empty());
3311
3312        let (_, first_reaction) = reactions.pop_first().unwrap();
3313        assert_eq!(first_reaction, reaction);
3314    }
3315
3316    #[test]
3317    fn test_patch_review_edit() {
3318        let alice = test::setup::NodeWithRepo::default();
3319        let checkout = alice.repo.checkout();
3320        let branch = checkout.branch_with([("README", b"Hello World!")]);
3321        let mut patches = Cache::no_cache(&*alice.repo).unwrap();
3322        let mut patch = patches
3323            .create(
3324                "My first patch",
3325                "Blah blah blah.",
3326                MergeTarget::Delegates,
3327                branch.base,
3328                branch.oid,
3329                &[],
3330                &alice.signer,
3331            )
3332            .unwrap();
3333
3334        let (rid, _) = patch.latest();
3335        let review = patch
3336            .review(
3337                rid,
3338                Some(Verdict::Accept),
3339                Some("LGTM".to_owned()),
3340                vec![],
3341                &alice.signer,
3342            )
3343            .unwrap();
3344        patch
3345            .review_edit(
3346                review,
3347                Some(Verdict::Reject),
3348                Some("Whoops!".to_owned()),
3349                vec![],
3350                &alice.signer,
3351            )
3352            .unwrap(); // Overwrite the comment.
3353
3354        let (_, revision) = patch.latest();
3355        let review = revision.review_by(alice.signer.public_key()).unwrap();
3356        assert_eq!(review.verdict(), Some(Verdict::Reject));
3357        assert_eq!(review.summary(), Some("Whoops!"));
3358    }
3359
3360    #[test]
3361    fn test_patch_review_duplicate() {
3362        let alice = test::setup::NodeWithRepo::default();
3363        let checkout = alice.repo.checkout();
3364        let branch = checkout.branch_with([("README", b"Hello World!")]);
3365        let mut patches = Cache::no_cache(&*alice.repo).unwrap();
3366        let mut patch = patches
3367            .create(
3368                "My first patch",
3369                "Blah blah blah.",
3370                MergeTarget::Delegates,
3371                branch.base,
3372                branch.oid,
3373                &[],
3374                &alice.signer,
3375            )
3376            .unwrap();
3377
3378        let (rid, _) = patch.latest();
3379        patch
3380            .review(rid, Some(Verdict::Accept), None, vec![], &alice.signer)
3381            .unwrap();
3382        patch
3383            .review(rid, Some(Verdict::Reject), None, vec![], &alice.signer)
3384            .unwrap(); // This review is ignored, since there is already a review by this author.
3385
3386        let (_, revision) = patch.latest();
3387        let review = revision.review_by(alice.signer.public_key()).unwrap();
3388        assert_eq!(review.verdict(), Some(Verdict::Accept));
3389    }
3390
3391    #[test]
3392    fn test_patch_review_edit_comment() {
3393        let alice = test::setup::NodeWithRepo::default();
3394        let checkout = alice.repo.checkout();
3395        let branch = checkout.branch_with([("README", b"Hello World!")]);
3396        let mut patches = Cache::no_cache(&*alice.repo).unwrap();
3397        let mut patch = patches
3398            .create(
3399                "My first patch",
3400                "Blah blah blah.",
3401                MergeTarget::Delegates,
3402                branch.base,
3403                branch.oid,
3404                &[],
3405                &alice.signer,
3406            )
3407            .unwrap();
3408
3409        let (rid, _) = patch.latest();
3410        let review = patch
3411            .review(rid, Some(Verdict::Accept), None, vec![], &alice.signer)
3412            .unwrap();
3413        patch
3414            .review_comment(review, "First comment!", None, None, [], &alice.signer)
3415            .unwrap();
3416
3417        let _review = patch
3418            .review_edit(review, Some(Verdict::Reject), None, vec![], &alice.signer)
3419            .unwrap();
3420        patch
3421            .review_comment(review, "Second comment!", None, None, [], &alice.signer)
3422            .unwrap();
3423
3424        let (_, revision) = patch.latest();
3425        let review = revision.review_by(alice.signer.public_key()).unwrap();
3426        assert_eq!(review.verdict(), Some(Verdict::Reject));
3427        assert_eq!(review.comments().count(), 2);
3428        assert_eq!(review.comments().nth(0).unwrap().1.body(), "First comment!");
3429        assert_eq!(
3430            review.comments().nth(1).unwrap().1.body(),
3431            "Second comment!"
3432        );
3433    }
3434
3435    #[test]
3436    fn test_patch_review_comment() {
3437        let alice = test::setup::NodeWithRepo::default();
3438        let checkout = alice.repo.checkout();
3439        let branch = checkout.branch_with([("README", b"Hello World!")]);
3440        let mut patches = Cache::no_cache(&*alice.repo).unwrap();
3441        let mut patch = patches
3442            .create(
3443                "My first patch",
3444                "Blah blah blah.",
3445                MergeTarget::Delegates,
3446                branch.base,
3447                branch.oid,
3448                &[],
3449                &alice.signer,
3450            )
3451            .unwrap();
3452
3453        let (rid, _) = patch.latest();
3454        let location = CodeLocation {
3455            commit: branch.oid,
3456            path: PathBuf::from_str("README").unwrap(),
3457            old: None,
3458            new: Some(CodeRange::Lines { range: 5..8 }),
3459        };
3460        let review = patch
3461            .review(rid, Some(Verdict::Accept), None, vec![], &alice.signer)
3462            .unwrap();
3463        patch
3464            .review_comment(
3465                review,
3466                "I like these lines of code",
3467                Some(location.clone()),
3468                None,
3469                [],
3470                &alice.signer,
3471            )
3472            .unwrap();
3473
3474        let (_, revision) = patch.latest();
3475        let review = revision.review_by(alice.signer.public_key()).unwrap();
3476        let (_, comment) = review.comments().next().unwrap();
3477
3478        assert_eq!(comment.body(), "I like these lines of code");
3479        assert_eq!(comment.location(), Some(&location));
3480    }
3481
3482    #[test]
3483    fn test_patch_review_remove_summary() {
3484        let alice = test::setup::NodeWithRepo::default();
3485        let checkout = alice.repo.checkout();
3486        let branch = checkout.branch_with([("README", b"Hello World!")]);
3487        let mut patches = Cache::no_cache(&*alice.repo).unwrap();
3488        let mut patch = patches
3489            .create(
3490                "My first patch",
3491                "Blah blah blah.",
3492                MergeTarget::Delegates,
3493                branch.base,
3494                branch.oid,
3495                &[],
3496                &alice.signer,
3497            )
3498            .unwrap();
3499
3500        let (rid, _) = patch.latest();
3501        let review = patch
3502            .review(
3503                rid,
3504                Some(Verdict::Accept),
3505                Some("Nah".to_owned()),
3506                vec![],
3507                &alice.signer,
3508            )
3509            .unwrap();
3510        patch
3511            .review_edit(review, Some(Verdict::Accept), None, vec![], &alice.signer)
3512            .unwrap();
3513
3514        let id = patch.id;
3515        let patch = patches.get_mut(&id).unwrap();
3516        let (_, revision) = patch.latest();
3517        let review = revision.review_by(alice.signer.public_key()).unwrap();
3518
3519        assert_eq!(review.verdict(), Some(Verdict::Accept));
3520        assert_eq!(review.summary(), None);
3521    }
3522
3523    #[test]
3524    fn test_patch_update() {
3525        let alice = test::setup::NodeWithRepo::default();
3526        let checkout = alice.repo.checkout();
3527        let branch = checkout.branch_with([("README", b"Hello World!")]);
3528        let mut patches = {
3529            let path = alice.tmp.path().join("cobs.db");
3530            let mut db = cob::cache::Store::open(path).unwrap();
3531            let store = cob::patch::Patches::open(&*alice.repo).unwrap();
3532
3533            db.migrate(migrate::ignore).unwrap();
3534            cob::patch::Cache::open(store, db)
3535        };
3536        let mut patch = patches
3537            .create(
3538                "My first patch",
3539                "Blah blah blah.",
3540                MergeTarget::Delegates,
3541                branch.base,
3542                branch.oid,
3543                &[],
3544                &alice.signer,
3545            )
3546            .unwrap();
3547
3548        assert_eq!(patch.description(), "Blah blah blah.");
3549        assert_eq!(patch.version(), 0);
3550
3551        let update = checkout.branch_with([("README", b"Hello Radicle!")]);
3552        let _ = patch
3553            .update("I've made changes.", branch.base, update.oid, &alice.signer)
3554            .unwrap();
3555
3556        let id = patch.id;
3557        let patch = patches.get(&id).unwrap().unwrap();
3558        assert_eq!(patch.version(), 1);
3559        assert_eq!(patch.revisions.len(), 2);
3560        assert_eq!(patch.revisions().count(), 2);
3561        assert_eq!(
3562            patch.revisions().nth(0).unwrap().1.description(),
3563            "Blah blah blah."
3564        );
3565        assert_eq!(
3566            patch.revisions().nth(1).unwrap().1.description(),
3567            "I've made changes."
3568        );
3569
3570        let (_, revision) = patch.latest();
3571
3572        assert_eq!(patch.version(), 1);
3573        assert_eq!(revision.oid, update.oid);
3574        assert_eq!(revision.description(), "I've made changes.");
3575    }
3576
3577    #[test]
3578    fn test_patch_redact() {
3579        let alice = test::setup::Node::default();
3580        let repo = alice.project();
3581        let branch = repo
3582            .checkout()
3583            .branch_with([("README.md", b"Hello, World!")]);
3584        let mut patches = Cache::no_cache(&*repo).unwrap();
3585        let mut patch = patches
3586            .create(
3587                "My first patch",
3588                "Blah blah blah.",
3589                MergeTarget::Delegates,
3590                branch.base,
3591                branch.oid,
3592                &[],
3593                &alice.signer,
3594            )
3595            .unwrap();
3596        let patch_id = patch.id;
3597
3598        let update = repo
3599            .checkout()
3600            .branch_with([("README.md", b"Hello, Radicle!")]);
3601        let revision_id = patch
3602            .update("I've made changes.", branch.base, update.oid, &alice.signer)
3603            .unwrap();
3604        assert_eq!(patch.revisions().count(), 2);
3605
3606        patch.redact(revision_id, &alice.signer).unwrap();
3607        assert_eq!(patch.latest().0, RevisionId(*patch_id));
3608        assert_eq!(patch.revisions().count(), 1);
3609
3610        // The patch's root must always exist.
3611        assert_eq!(patch.latest(), patch.root());
3612        assert!(patch.redact(patch.latest().0, &alice.signer).is_err());
3613    }
3614
3615    #[test]
3616    fn test_json() {
3617        use serde_json::json;
3618
3619        assert_eq!(
3620            serde_json::to_value(Action::Lifecycle {
3621                state: Lifecycle::Draft
3622            })
3623            .unwrap(),
3624            json!({
3625                "type": "lifecycle",
3626                "state": { "status": "draft" }
3627            })
3628        );
3629
3630        let revision = RevisionId(arbitrary::entry_id());
3631        assert_eq!(
3632            serde_json::to_value(Action::Review {
3633                revision,
3634                summary: None,
3635                verdict: None,
3636                labels: vec![],
3637            })
3638            .unwrap(),
3639            json!({
3640                "type": "review",
3641                "revision": revision,
3642            })
3643        );
3644
3645        assert_eq!(
3646            serde_json::to_value(CodeRange::Lines { range: 4..8 }).unwrap(),
3647            json!({
3648                "type": "lines",
3649                "range": { "start": 4, "end": 8 },
3650            })
3651        );
3652    }
3653}