radicle/cob/
patch.rs

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