radicle/cob/
patch.rs

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