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