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::ext::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>(&self, repo: &R) -> Result<git::Oid, git::ext::Error> {
605 repo.merge_base(self.base(), self.head())
606 }
607
608 pub fn range(&self) -> Result<(git::Oid, git::Oid), git::ext::Error> {
610 Ok((*self.base(), *self.head()))
611 }
612
613 pub fn version(&self) -> RevisionIx {
615 self.revisions
616 .len()
617 .checked_sub(1)
618 .expect("Patch::version: at least one revision is present")
619 }
620
621 pub fn root(&self) -> (RevisionId, &Revision) {
625 self.updates()
626 .next()
627 .expect("Patch::root: there is always a root revision")
628 }
629
630 pub fn latest(&self) -> (RevisionId, &Revision) {
632 self.latest_by(self.author().public_key())
633 .expect("Patch::latest: there is always at least one revision")
634 }
635
636 pub fn latest_by<'a>(&'a self, author: &'a PublicKey) -> Option<(RevisionId, &'a Revision)> {
638 self.revisions_by(author).next_back()
639 }
640
641 pub fn updated_at(&self) -> Timestamp {
643 self.latest().1.timestamp()
644 }
645
646 pub fn is_merged(&self) -> bool {
648 matches!(self.state(), State::Merged { .. })
649 }
650
651 pub fn is_open(&self) -> bool {
653 matches!(self.state(), State::Open { .. })
654 }
655
656 pub fn is_archived(&self) -> bool {
658 matches!(self.state(), State::Archived)
659 }
660
661 pub fn is_draft(&self) -> bool {
663 matches!(self.state(), State::Draft)
664 }
665
666 pub fn authorization(
668 &self,
669 action: &Action,
670 actor: &ActorId,
671 doc: &Doc,
672 ) -> Result<Authorization, Error> {
673 if doc.is_delegate(&actor.into()) {
674 return Ok(Authorization::Allow);
676 }
677 let author = self.author().id().as_key();
678 let outcome = match action {
679 Action::Edit { .. } => Authorization::from(actor == author),
681 Action::Lifecycle { state } => Authorization::from(match state {
682 Lifecycle::Open => actor == author,
683 Lifecycle::Draft => actor == author,
684 Lifecycle::Archived => actor == author,
685 }),
686 Action::Label { labels } => {
688 if labels == &self.labels {
689 Authorization::Allow
691 } else {
692 Authorization::Deny
693 }
694 }
695 Action::Assign { .. } => Authorization::Deny,
696 Action::Merge { .. } => match self.target() {
697 MergeTarget::Delegates => Authorization::Deny,
698 },
699 Action::Review { .. } => Authorization::Allow,
701 Action::ReviewRedact { review, .. } => {
702 if let Some((_, review)) = lookup::review(self, review)? {
703 Authorization::from(actor == review.author.public_key())
704 } else {
705 Authorization::Unknown
707 }
708 }
709 Action::ReviewEdit(edit) => {
710 if let Some((_, review)) = lookup::review(self, edit.review_id())? {
711 Authorization::from(actor == review.author.public_key())
712 } else {
713 Authorization::Unknown
715 }
716 }
717 Action::ReviewComment { .. } => Authorization::Allow,
719 Action::ReviewCommentEdit {
721 review, comment, ..
722 }
723 | Action::ReviewCommentRedact { review, comment } => {
724 if let Some((_, review)) = lookup::review(self, review)? {
725 if let Some(comment) = review.comments.comment(comment) {
726 return Ok(Authorization::from(*actor == comment.author()));
727 }
728 }
729 Authorization::Unknown
731 }
732 Action::ReviewCommentReact { .. } => Authorization::Allow,
734 Action::ReviewCommentResolve { review, comment }
736 | Action::ReviewCommentUnresolve { review, comment } => {
737 if let Some((revision, review)) = lookup::review(self, review)? {
738 if let Some(comment) = review.comments.comment(comment) {
739 return Ok(Authorization::from(
740 actor == &comment.author()
741 || actor == review.author.public_key()
742 || actor == revision.author.public_key(),
743 ));
744 }
745 }
746 Authorization::Unknown
748 }
749 Action::ReviewReact { .. } => Authorization::Allow,
750 Action::Revision { .. } => Authorization::Allow,
752 Action::RevisionEdit { revision, .. } | Action::RevisionRedact { revision, .. } => {
754 if let Some(revision) = lookup::revision(self, revision)? {
755 Authorization::from(actor == revision.author.public_key())
756 } else {
757 Authorization::Unknown
759 }
760 }
761 Action::RevisionReact { .. } => Authorization::Allow,
763 Action::RevisionComment { .. } => Authorization::Allow,
764 Action::RevisionCommentEdit {
766 revision, comment, ..
767 }
768 | Action::RevisionCommentRedact {
769 revision, comment, ..
770 } => {
771 if let Some(revision) = lookup::revision(self, revision)? {
772 if let Some(comment) = revision.discussion.comment(comment) {
773 return Ok(Authorization::from(actor == &comment.author()));
774 }
775 }
776 Authorization::Unknown
778 }
779 Action::RevisionCommentReact { .. } => Authorization::Allow,
781 };
782 Ok(outcome)
783 }
784}
785
786impl Patch {
787 fn op_action<R: ReadRepository>(
789 &mut self,
790 action: Action,
791 id: EntryId,
792 author: ActorId,
793 timestamp: Timestamp,
794 concurrent: &[&cob::Entry],
795 doc: &DocAt,
796 repo: &R,
797 ) -> Result<(), Error> {
798 match self.authorization(&action, &author, doc)? {
799 Authorization::Allow => {
800 self.action(action, id, author, timestamp, concurrent, doc, repo)
801 }
802 Authorization::Deny => Err(Error::NotAuthorized(author, Box::new(action))),
803 Authorization::Unknown => {
804 Ok(())
809 }
810 }
811 }
812
813 fn action<R: ReadRepository>(
815 &mut self,
816 action: Action,
817 entry: EntryId,
818 author: ActorId,
819 timestamp: Timestamp,
820 _concurrent: &[&cob::Entry],
821 identity: &Doc,
822 repo: &R,
823 ) -> Result<(), Error> {
824 match action {
825 Action::Edit { title, target } => {
826 self.title = title;
827 self.target = target;
828 }
829 Action::Lifecycle { state } => {
830 let valid = self.state == State::Draft
831 || self.state == State::Archived
832 || self.state == State::Open { conflicts: vec![] };
833
834 if valid {
835 match state {
836 Lifecycle::Open => {
837 self.state = State::Open { conflicts: vec![] };
838 }
839 Lifecycle::Draft => {
840 self.state = State::Draft;
841 }
842 Lifecycle::Archived => {
843 self.state = State::Archived;
844 }
845 }
846 }
847 }
848 Action::Label { labels } => {
849 self.labels = BTreeSet::from_iter(labels);
850 }
851 Action::Assign { assignees } => {
852 self.assignees = BTreeSet::from_iter(assignees.into_iter().map(ActorId::from));
853 }
854 Action::RevisionEdit {
855 revision,
856 description,
857 embeds,
858 } => {
859 if let Some(redactable) = self.revisions.get_mut(&revision) {
860 if let Some(revision) = redactable {
862 revision.description.push(Edit::new(
863 author,
864 description,
865 timestamp,
866 embeds,
867 ));
868 }
869 } else {
870 return Err(Error::Missing(revision.into_inner()));
871 }
872 }
873 Action::Revision {
874 description,
875 base,
876 oid,
877 resolves,
878 } => {
879 debug_assert!(!self.revisions.contains_key(&entry));
880 let id = RevisionId(entry);
881
882 self.revisions.insert(
883 id,
884 Some(Revision::new(
885 id,
886 author.into(),
887 description,
888 base,
889 oid,
890 timestamp,
891 resolves,
892 )),
893 );
894 }
895 Action::RevisionReact {
896 revision,
897 reaction,
898 active,
899 location,
900 } => {
901 if let Some(revision) = lookup::revision_mut(self, &revision)? {
902 let key = (author, reaction);
903 let reactions = revision.reactions.entry(location).or_default();
904
905 if active {
906 reactions.insert(key);
907 } else {
908 reactions.remove(&key);
909 }
910 }
911 }
912 Action::RevisionRedact { revision } => {
913 let (root, _) = self.root();
915 if revision == root {
916 return Err(Error::NotAllowed(entry));
917 }
918 if let Some(r) = self.revisions.get_mut(&revision) {
920 if self.merges.values().any(|m| m.revision == revision) {
923 return Ok(());
924 }
925 *r = None;
926 } else {
927 return Err(Error::Missing(revision.into_inner()));
928 }
929 }
930 Action::Review {
931 revision,
932 summary,
933 verdict,
934 labels,
935 } => {
936 let Some(rev) = self.revisions.get_mut(&revision) else {
937 return Ok(());
939 };
940 if let Some(rev) = rev {
941 if let btree_map::Entry::Vacant(e) = rev.reviews.entry(author) {
944 let id = ReviewId(entry);
945
946 e.insert(Review::new(
947 id,
948 Author::new(author),
949 verdict,
950 summary.unwrap_or_default(),
951 labels,
952 vec![],
953 timestamp,
954 ));
955 self.reviews.insert(id, Some((revision, author)));
957 } else {
958 log::error!(
959 target: "patch",
960 "Review by {author} for {revision} already exists, ignoring action.."
961 );
962 }
963 }
964 }
965 Action::ReviewEdit(edit) => edit.run(author, timestamp, self)?,
966 Action::ReviewCommentReact {
967 review,
968 comment,
969 reaction,
970 active,
971 } => {
972 if let Some(review) = lookup::review_mut(self, &review)? {
973 thread::react(
974 &mut review.comments,
975 entry,
976 author,
977 comment,
978 reaction,
979 active,
980 )?;
981 }
982 }
983 Action::ReviewCommentRedact { review, comment } => {
984 if let Some(review) = lookup::review_mut(self, &review)? {
985 thread::redact(&mut review.comments, entry, comment)?;
986 }
987 }
988 Action::ReviewCommentEdit {
989 review,
990 comment,
991 body,
992 embeds,
993 } => {
994 if let Some(review) = lookup::review_mut(self, &review)? {
995 thread::edit(
996 &mut review.comments,
997 entry,
998 author,
999 comment,
1000 timestamp,
1001 body,
1002 embeds,
1003 )?;
1004 }
1005 }
1006 Action::ReviewCommentResolve { review, comment } => {
1007 if let Some(review) = lookup::review_mut(self, &review)? {
1008 thread::resolve(&mut review.comments, entry, comment)?;
1009 }
1010 }
1011 Action::ReviewCommentUnresolve { review, comment } => {
1012 if let Some(review) = lookup::review_mut(self, &review)? {
1013 thread::unresolve(&mut review.comments, entry, comment)?;
1014 }
1015 }
1016 Action::ReviewComment {
1017 review,
1018 body,
1019 location,
1020 reply_to,
1021 embeds,
1022 } => {
1023 if let Some(review) = lookup::review_mut(self, &review)? {
1024 thread::comment(
1025 &mut review.comments,
1026 entry,
1027 author,
1028 timestamp,
1029 body,
1030 reply_to,
1031 location,
1032 embeds,
1033 )?;
1034 }
1035 }
1036 Action::ReviewRedact { review } => {
1037 let Some(locator) = self.reviews.get_mut(&review) else {
1039 return Err(Error::Missing(review.into_inner()));
1040 };
1041 let Some((revision, reviewer)) = locator else {
1043 return Ok(());
1044 };
1045 let Some(redactable) = self.revisions.get_mut(revision) else {
1047 return Err(Error::Missing(revision.into_inner()));
1048 };
1049 let Some(revision) = redactable else {
1051 return Ok(());
1052 };
1053 if let Some(r) = revision.reviews.remove(reviewer) {
1055 debug_assert_eq!(r.id, review);
1056 } else {
1057 log::error!(
1058 target: "patch", "Review {review} not found in revision {}", revision.id
1059 );
1060 }
1061 *locator = None;
1063 }
1064 Action::ReviewReact {
1065 review,
1066 reaction,
1067 active,
1068 } => {
1069 if let Some(review) = lookup::review_mut(self, &review)? {
1070 if active {
1071 review.reactions.insert((author, reaction));
1072 } else {
1073 review.reactions.remove(&(author, reaction));
1074 }
1075 }
1076 }
1077 Action::Merge { revision, commit } => {
1078 if lookup::revision_mut(self, &revision)?.is_none() {
1080 return Ok(());
1081 };
1082 match self.target() {
1083 MergeTarget::Delegates => {
1084 let proj = identity.project()?;
1085 let branch = git::refs::branch(proj.default_branch());
1086
1087 let Ok(head) = repo.reference_oid(&author, &branch) else {
1094 return Ok(());
1095 };
1096 if commit != head && !repo.is_ancestor_of(commit, head)? {
1097 return Ok(());
1098 }
1099 }
1100 }
1101 self.merges.insert(
1102 author,
1103 Merge {
1104 revision,
1105 commit,
1106 timestamp,
1107 },
1108 );
1109
1110 let mut merges = self.merges.iter().fold(
1111 HashMap::<(RevisionId, git::Oid), usize>::new(),
1112 |mut acc, (_, merge)| {
1113 *acc.entry((merge.revision, merge.commit)).or_default() += 1;
1114 acc
1115 },
1116 );
1117 merges.retain(|_, count| *count >= identity.threshold());
1119
1120 match merges.into_keys().collect::<Vec<_>>().as_slice() {
1121 [] => {
1122 }
1124 [(revision, commit)] => {
1125 self.state = State::Merged {
1127 revision: *revision,
1128 commit: *commit,
1129 };
1130 }
1131 revisions => {
1132 self.state = State::Open {
1134 conflicts: revisions.to_vec(),
1135 };
1136 }
1137 }
1138 }
1139
1140 Action::RevisionComment {
1141 revision,
1142 body,
1143 reply_to,
1144 embeds,
1145 location,
1146 } => {
1147 if let Some(revision) = lookup::revision_mut(self, &revision)? {
1148 thread::comment(
1149 &mut revision.discussion,
1150 entry,
1151 author,
1152 timestamp,
1153 body,
1154 reply_to,
1155 location,
1156 embeds,
1157 )?;
1158 }
1159 }
1160 Action::RevisionCommentEdit {
1161 revision,
1162 comment,
1163 body,
1164 embeds,
1165 } => {
1166 if let Some(revision) = lookup::revision_mut(self, &revision)? {
1167 thread::edit(
1168 &mut revision.discussion,
1169 entry,
1170 author,
1171 comment,
1172 timestamp,
1173 body,
1174 embeds,
1175 )?;
1176 }
1177 }
1178 Action::RevisionCommentRedact { revision, comment } => {
1179 if let Some(revision) = lookup::revision_mut(self, &revision)? {
1180 thread::redact(&mut revision.discussion, entry, comment)?;
1181 }
1182 }
1183 Action::RevisionCommentReact {
1184 revision,
1185 comment,
1186 reaction,
1187 active,
1188 } => {
1189 if let Some(revision) = lookup::revision_mut(self, &revision)? {
1190 thread::react(
1191 &mut revision.discussion,
1192 entry,
1193 author,
1194 comment,
1195 reaction,
1196 active,
1197 )?;
1198 }
1199 }
1200 }
1201 Ok(())
1202 }
1203}
1204
1205impl cob::store::CobWithType for Patch {
1206 fn type_name() -> &'static TypeName {
1207 &TYPENAME
1208 }
1209}
1210
1211impl store::Cob for Patch {
1212 type Action = Action;
1213 type Error = Error;
1214
1215 fn from_root<R: ReadRepository>(op: Op, repo: &R) -> Result<Self, Self::Error> {
1216 let doc = op.identity_doc(repo)?.ok_or(Error::MissingIdentity)?;
1217 let mut actions = op.actions.into_iter();
1218 let Some(Action::Revision {
1219 description,
1220 base,
1221 oid,
1222 resolves,
1223 }) = actions.next()
1224 else {
1225 return Err(Error::Init("the first action must be of type `revision`"));
1226 };
1227 let Some(Action::Edit { title, target }) = actions.next() else {
1228 return Err(Error::Init("the second action must be of type `edit`"));
1229 };
1230 let revision = Revision::new(
1231 RevisionId(op.id),
1232 op.author.into(),
1233 description,
1234 base,
1235 oid,
1236 op.timestamp,
1237 resolves,
1238 );
1239 let mut patch = Patch::new(title, target, (RevisionId(op.id), revision));
1240
1241 for action in actions {
1242 match patch.authorization(&action, &op.author, &doc)? {
1243 Authorization::Allow => {
1244 patch.action(action, op.id, op.author, op.timestamp, &[], &doc, repo)?;
1245 }
1246 Authorization::Deny => {
1247 return Err(Error::NotAuthorized(op.author, Box::new(action)));
1248 }
1249 Authorization::Unknown => {
1250 continue;
1253 }
1254 }
1255 }
1256 Ok(patch)
1257 }
1258
1259 fn op<'a, R: ReadRepository, I: IntoIterator<Item = &'a cob::Entry>>(
1260 &mut self,
1261 op: Op,
1262 concurrent: I,
1263 repo: &R,
1264 ) -> Result<(), Error> {
1265 debug_assert!(!self.timeline.contains(&op.id));
1266 self.timeline.push(op.id);
1267
1268 let doc = op.identity_doc(repo)?.ok_or(Error::MissingIdentity)?;
1269 let concurrent = concurrent.into_iter().collect::<Vec<_>>();
1270
1271 for action in op.actions {
1272 log::trace!(target: "patch", "Applying {} {action:?}", op.id);
1273
1274 if let Err(e) = self.op_action(
1275 action,
1276 op.id,
1277 op.author,
1278 op.timestamp,
1279 &concurrent,
1280 &doc,
1281 repo,
1282 ) {
1283 log::error!(target: "patch", "Error applying {}: {e}", op.id);
1284 return Err(e);
1285 }
1286 }
1287 Ok(())
1288 }
1289}
1290
1291impl<R: ReadRepository> cob::Evaluate<R> for Patch {
1292 type Error = Error;
1293
1294 fn init(entry: &cob::Entry, repo: &R) -> Result<Self, Self::Error> {
1295 let op = Op::try_from(entry)?;
1296 let object = Patch::from_root(op, repo)?;
1297
1298 Ok(object)
1299 }
1300
1301 fn apply<'a, I: Iterator<Item = (&'a EntryId, &'a cob::Entry)>>(
1302 &mut self,
1303 entry: &cob::Entry,
1304 concurrent: I,
1305 repo: &R,
1306 ) -> Result<(), Self::Error> {
1307 let op = Op::try_from(entry)?;
1308
1309 self.op(op, concurrent.map(|(_, e)| e), repo)
1310 }
1311}
1312
1313mod lookup {
1314 use super::*;
1315
1316 pub fn revision<'a>(
1317 patch: &'a Patch,
1318 revision: &RevisionId,
1319 ) -> Result<Option<&'a Revision>, Error> {
1320 match patch.revisions.get(revision) {
1321 Some(Some(revision)) => Ok(Some(revision)),
1322 Some(None) => Ok(None),
1324 None => Err(Error::Missing(revision.into_inner())),
1326 }
1327 }
1328
1329 pub fn revision_mut<'a>(
1330 patch: &'a mut Patch,
1331 revision: &RevisionId,
1332 ) -> Result<Option<&'a mut Revision>, Error> {
1333 match patch.revisions.get_mut(revision) {
1334 Some(Some(revision)) => Ok(Some(revision)),
1335 Some(None) => Ok(None),
1337 None => Err(Error::Missing(revision.into_inner())),
1339 }
1340 }
1341
1342 pub fn review<'a>(
1343 patch: &'a Patch,
1344 review: &ReviewId,
1345 ) -> Result<Option<(&'a Revision, &'a Review)>, Error> {
1346 match patch.reviews.get(review) {
1347 Some(Some((revision, author))) => {
1348 match patch.revisions.get(revision) {
1349 Some(Some(rev)) => {
1350 let r = rev
1351 .reviews
1352 .get(author)
1353 .ok_or_else(|| Error::Missing(review.into_inner()))?;
1354 debug_assert_eq!(&r.id, review);
1355
1356 Ok(Some((rev, r)))
1357 }
1358 Some(None) => {
1359 Ok(None)
1362 }
1363 None => Err(Error::Missing(revision.into_inner())),
1364 }
1365 }
1366 Some(None) => {
1367 Ok(None)
1369 }
1370 None => Err(Error::Missing(review.into_inner())),
1371 }
1372 }
1373
1374 pub fn review_mut<'a>(
1375 patch: &'a mut Patch,
1376 review: &ReviewId,
1377 ) -> Result<Option<&'a mut Review>, Error> {
1378 match patch.reviews.get(review) {
1379 Some(Some((revision, author))) => {
1380 match patch.revisions.get_mut(revision) {
1381 Some(Some(rev)) => {
1382 let r = rev
1383 .reviews
1384 .get_mut(author)
1385 .ok_or_else(|| Error::Missing(review.into_inner()))?;
1386 debug_assert_eq!(&r.id, review);
1387
1388 Ok(Some(r))
1389 }
1390 Some(None) => {
1391 Ok(None)
1394 }
1395 None => Err(Error::Missing(revision.into_inner())),
1396 }
1397 }
1398 Some(None) => {
1399 Ok(None)
1401 }
1402 None => Err(Error::Missing(review.into_inner())),
1403 }
1404 }
1405}
1406
1407#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1409#[serde(rename_all = "camelCase")]
1410pub struct Revision {
1411 pub(super) id: RevisionId,
1413 pub(super) author: Author,
1415 pub(super) description: NonEmpty<Edit>,
1417 pub(super) base: git::Oid,
1419 pub(super) oid: git::Oid,
1421 pub(super) discussion: Thread<Comment<CodeLocation>>,
1423 pub(super) reviews: BTreeMap<ActorId, Review>,
1425 pub(super) timestamp: Timestamp,
1427 pub(super) resolves: BTreeSet<(EntryId, CommentId)>,
1429 #[serde(
1431 serialize_with = "ser::serialize_reactions",
1432 deserialize_with = "ser::deserialize_reactions"
1433 )]
1434 pub(super) reactions: BTreeMap<Option<CodeLocation>, Reactions>,
1435}
1436
1437impl Revision {
1438 pub fn new(
1439 id: RevisionId,
1440 author: Author,
1441 description: String,
1442 base: git::Oid,
1443 oid: git::Oid,
1444 timestamp: Timestamp,
1445 resolves: BTreeSet<(EntryId, CommentId)>,
1446 ) -> Self {
1447 let description = Edit::new(*author.public_key(), description, timestamp, Vec::default());
1448
1449 Self {
1450 id,
1451 author,
1452 description: NonEmpty::new(description),
1453 base,
1454 oid,
1455 discussion: Thread::default(),
1456 reviews: BTreeMap::default(),
1457 timestamp,
1458 resolves,
1459 reactions: Default::default(),
1460 }
1461 }
1462
1463 pub fn id(&self) -> RevisionId {
1464 self.id
1465 }
1466
1467 pub fn description(&self) -> &str {
1468 self.description.last().body.as_str()
1469 }
1470
1471 pub fn edits(&self) -> impl Iterator<Item = &Edit> {
1472 self.description.iter()
1473 }
1474
1475 pub fn embeds(&self) -> &[Embed<Uri>] {
1476 &self.description.last().embeds
1477 }
1478
1479 pub fn reactions(&self) -> &BTreeMap<Option<CodeLocation>, BTreeSet<(PublicKey, Reaction)>> {
1480 &self.reactions
1481 }
1482
1483 pub fn author(&self) -> &Author {
1485 &self.author
1486 }
1487
1488 pub fn base(&self) -> &git::Oid {
1490 &self.base
1491 }
1492
1493 pub fn head(&self) -> git::Oid {
1495 self.oid
1496 }
1497
1498 pub fn range(&self) -> (git::Oid, git::Oid) {
1500 (self.base, self.oid)
1501 }
1502
1503 pub fn timestamp(&self) -> Timestamp {
1505 self.timestamp
1506 }
1507
1508 pub fn discussion(&self) -> &Thread<Comment<CodeLocation>> {
1510 &self.discussion
1511 }
1512
1513 pub fn resolves(&self) -> &BTreeSet<(EntryId, CommentId)> {
1515 &self.resolves
1516 }
1517
1518 pub fn replies(&self) -> impl Iterator<Item = (&CommentId, &thread::Comment<CodeLocation>)> {
1520 self.discussion.comments()
1521 }
1522
1523 pub fn reviews(&self) -> impl DoubleEndedIterator<Item = (&PublicKey, &Review)> {
1525 self.reviews.iter()
1526 }
1527
1528 pub fn review_by(&self, author: &ActorId) -> Option<&Review> {
1530 self.reviews.get(author)
1531 }
1532}
1533
1534#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1536#[serde(rename_all = "camelCase", tag = "status")]
1537pub enum State {
1538 Draft,
1539 Open {
1540 #[serde(skip_serializing_if = "Vec::is_empty")]
1542 #[serde(default)]
1543 conflicts: Vec<(RevisionId, git::Oid)>,
1544 },
1545 Archived,
1546 Merged {
1547 revision: RevisionId,
1549 commit: git::Oid,
1551 },
1552}
1553
1554impl Default for State {
1555 fn default() -> Self {
1556 Self::Open { conflicts: vec![] }
1557 }
1558}
1559
1560impl fmt::Display for State {
1561 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1562 match self {
1563 Self::Archived => write!(f, "archived"),
1564 Self::Draft => write!(f, "draft"),
1565 Self::Open { .. } => write!(f, "open"),
1566 Self::Merged { .. } => write!(f, "merged"),
1567 }
1568 }
1569}
1570
1571impl From<&State> for Status {
1572 fn from(value: &State) -> Self {
1573 match value {
1574 State::Draft => Self::Draft,
1575 State::Open { .. } => Self::Open,
1576 State::Archived => Self::Archived,
1577 State::Merged { .. } => Self::Merged,
1578 }
1579 }
1580}
1581
1582#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1585pub enum Status {
1586 Draft,
1587 #[default]
1588 Open,
1589 Archived,
1590 Merged,
1591}
1592
1593impl fmt::Display for Status {
1594 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1595 match self {
1596 Self::Archived => write!(f, "archived"),
1597 Self::Draft => write!(f, "draft"),
1598 Self::Open => write!(f, "open"),
1599 Self::Merged => write!(f, "merged"),
1600 }
1601 }
1602}
1603
1604#[derive(Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1606#[serde(rename_all = "camelCase", tag = "status")]
1607pub enum Lifecycle {
1608 #[default]
1609 Open,
1610 Draft,
1611 Archived,
1612}
1613
1614#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
1616#[serde(rename_all = "camelCase")]
1617pub struct Merge {
1618 pub revision: RevisionId,
1620 pub commit: git::Oid,
1622 pub timestamp: Timestamp,
1624}
1625
1626#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1628#[serde(rename_all = "camelCase")]
1629pub enum Verdict {
1630 Accept,
1632 Reject,
1634}
1635
1636impl fmt::Display for Verdict {
1637 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1638 match self {
1639 Self::Accept => write!(f, "accept"),
1640 Self::Reject => write!(f, "reject"),
1641 }
1642 }
1643}
1644
1645#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1647#[serde(rename_all = "camelCase")]
1648#[serde(from = "encoding::review::Review")]
1649pub struct Review {
1650 pub(super) id: ReviewId,
1652 pub(super) author: Author,
1654 pub(super) verdict: Option<Verdict>,
1658 pub(super) summary: NonEmpty<Edit>,
1665 pub(super) comments: Thread<Comment<CodeLocation>>,
1667 pub(super) labels: Vec<Label>,
1670 #[serde(skip_serializing_if = "BTreeSet::is_empty")]
1671 pub(super) reactions: Reactions,
1673 pub(super) timestamp: Timestamp,
1675}
1676
1677impl Review {
1678 pub fn new(
1679 id: ReviewId,
1680 author: Author,
1681 verdict: Option<Verdict>,
1682 summary: String,
1683 labels: Vec<Label>,
1684 embeds: Vec<Embed<Uri>>,
1685 timestamp: Timestamp,
1686 ) -> Self {
1687 let summary = NonEmpty::new(Edit::new(*author.public_key(), summary, timestamp, embeds));
1688 Self {
1689 id,
1690 author,
1691 verdict,
1692 summary,
1693 comments: Thread::default(),
1694 reactions: BTreeSet::new(),
1695 labels,
1696 timestamp,
1697 }
1698 }
1699
1700 pub fn id(&self) -> ReviewId {
1702 self.id
1703 }
1704
1705 pub fn author(&self) -> &Author {
1707 &self.author
1708 }
1709
1710 pub fn verdict(&self) -> Option<Verdict> {
1712 self.verdict
1713 }
1714
1715 pub fn comments(&self) -> impl DoubleEndedIterator<Item = (&EntryId, &Comment<CodeLocation>)> {
1717 self.comments.comments()
1718 }
1719
1720 pub fn labels(&self) -> impl Iterator<Item = &Label> {
1722 self.labels.iter()
1723 }
1724
1725 pub fn summary(&self) -> &str {
1727 self.summary.last().body.as_str()
1728 }
1729
1730 pub fn embeds(&self) -> &[Embed<Uri>] {
1732 &self.summary.last().embeds
1733 }
1734
1735 pub fn reactions(&self) -> &Reactions {
1737 &self.reactions
1738 }
1739
1740 pub fn edits(&self) -> impl Iterator<Item = &Edit> {
1742 self.summary.iter()
1743 }
1744
1745 pub fn timestamp(&self) -> Timestamp {
1747 self.timestamp
1748 }
1749}
1750
1751impl<R: ReadRepository> store::Transaction<Patch, R> {
1752 pub fn edit(&mut self, title: cob::Title, target: MergeTarget) -> Result<(), store::Error> {
1753 self.push(Action::Edit { title, target })
1754 }
1755
1756 pub fn edit_revision(
1757 &mut self,
1758 revision: RevisionId,
1759 description: impl ToString,
1760 embeds: Vec<Embed<Uri>>,
1761 ) -> Result<(), store::Error> {
1762 self.embed(embeds.clone())?;
1763 self.push(Action::RevisionEdit {
1764 revision,
1765 description: description.to_string(),
1766 embeds,
1767 })
1768 }
1769
1770 pub fn redact(&mut self, revision: RevisionId) -> Result<(), store::Error> {
1772 self.push(Action::RevisionRedact { revision })
1773 }
1774
1775 pub fn thread<S: ToString>(
1777 &mut self,
1778 revision: RevisionId,
1779 body: S,
1780 ) -> Result<(), store::Error> {
1781 self.push(Action::RevisionComment {
1782 revision,
1783 body: body.to_string(),
1784 reply_to: None,
1785 location: None,
1786 embeds: vec![],
1787 })
1788 }
1789
1790 pub fn react(
1792 &mut self,
1793 revision: RevisionId,
1794 reaction: Reaction,
1795 location: Option<CodeLocation>,
1796 active: bool,
1797 ) -> Result<(), store::Error> {
1798 self.push(Action::RevisionReact {
1799 revision,
1800 reaction,
1801 location,
1802 active,
1803 })
1804 }
1805
1806 pub fn comment<S: ToString>(
1808 &mut self,
1809 revision: RevisionId,
1810 body: S,
1811 reply_to: Option<CommentId>,
1812 location: Option<CodeLocation>,
1813 embeds: Vec<Embed<Uri>>,
1814 ) -> Result<(), store::Error> {
1815 self.embed(embeds.clone())?;
1816 self.push(Action::RevisionComment {
1817 revision,
1818 body: body.to_string(),
1819 reply_to,
1820 location,
1821 embeds,
1822 })
1823 }
1824
1825 pub fn comment_edit<S: ToString>(
1827 &mut self,
1828 revision: RevisionId,
1829 comment: CommentId,
1830 body: S,
1831 embeds: Vec<Embed<Uri>>,
1832 ) -> Result<(), store::Error> {
1833 self.embed(embeds.clone())?;
1834 self.push(Action::RevisionCommentEdit {
1835 revision,
1836 comment,
1837 body: body.to_string(),
1838 embeds,
1839 })
1840 }
1841
1842 pub fn comment_react(
1844 &mut self,
1845 revision: RevisionId,
1846 comment: CommentId,
1847 reaction: Reaction,
1848 active: bool,
1849 ) -> Result<(), store::Error> {
1850 self.push(Action::RevisionCommentReact {
1851 revision,
1852 comment,
1853 reaction,
1854 active,
1855 })
1856 }
1857
1858 pub fn comment_redact(
1860 &mut self,
1861 revision: RevisionId,
1862 comment: CommentId,
1863 ) -> Result<(), store::Error> {
1864 self.push(Action::RevisionCommentRedact { revision, comment })
1865 }
1866
1867 pub fn review_comment<S: ToString>(
1869 &mut self,
1870 review: ReviewId,
1871 body: S,
1872 location: Option<CodeLocation>,
1873 reply_to: Option<CommentId>,
1874 embeds: Vec<Embed<Uri>>,
1875 ) -> Result<(), store::Error> {
1876 self.embed(embeds.clone())?;
1877 self.push(Action::ReviewComment {
1878 review,
1879 body: body.to_string(),
1880 location,
1881 reply_to,
1882 embeds,
1883 })
1884 }
1885
1886 pub fn review_comment_resolve(
1888 &mut self,
1889 review: ReviewId,
1890 comment: CommentId,
1891 ) -> Result<(), store::Error> {
1892 self.push(Action::ReviewCommentResolve { review, comment })
1893 }
1894
1895 pub fn review_comment_unresolve(
1897 &mut self,
1898 review: ReviewId,
1899 comment: CommentId,
1900 ) -> Result<(), store::Error> {
1901 self.push(Action::ReviewCommentUnresolve { review, comment })
1902 }
1903
1904 pub fn edit_review_comment<S: ToString>(
1906 &mut self,
1907 review: ReviewId,
1908 comment: EntryId,
1909 body: S,
1910 embeds: Vec<Embed<Uri>>,
1911 ) -> Result<(), store::Error> {
1912 self.embed(embeds.clone())?;
1913 self.push(Action::ReviewCommentEdit {
1914 review,
1915 comment,
1916 body: body.to_string(),
1917 embeds,
1918 })
1919 }
1920
1921 pub fn react_review_comment(
1923 &mut self,
1924 review: ReviewId,
1925 comment: EntryId,
1926 reaction: Reaction,
1927 active: bool,
1928 ) -> Result<(), store::Error> {
1929 self.push(Action::ReviewCommentReact {
1930 review,
1931 comment,
1932 reaction,
1933 active,
1934 })
1935 }
1936
1937 pub fn redact_review_comment(
1939 &mut self,
1940 review: ReviewId,
1941 comment: EntryId,
1942 ) -> Result<(), store::Error> {
1943 self.push(Action::ReviewCommentRedact { review, comment })
1944 }
1945
1946 pub fn review(
1949 &mut self,
1950 revision: RevisionId,
1951 verdict: Option<Verdict>,
1952 summary: Option<String>,
1953 labels: Vec<Label>,
1954 ) -> Result<(), store::Error> {
1955 self.push(Action::Review {
1956 revision,
1957 summary,
1958 verdict,
1959 labels,
1960 })
1961 }
1962
1963 pub fn review_edit(
1965 &mut self,
1966 review: ReviewId,
1967 verdict: Option<Verdict>,
1968 summary: String,
1969 labels: Vec<Label>,
1970 embeds: impl IntoIterator<Item = Embed<Uri>>,
1971 ) -> Result<(), store::Error> {
1972 self.push(Action::ReviewEdit(actions::ReviewEdit::new(
1973 review,
1974 summary,
1975 verdict,
1976 labels,
1977 embeds.into_iter().collect(),
1978 )))
1979 }
1980
1981 pub fn review_react(
1983 &mut self,
1984 review: ReviewId,
1985 reaction: Reaction,
1986 active: bool,
1987 ) -> Result<(), store::Error> {
1988 self.push(Action::ReviewReact {
1989 review,
1990 reaction,
1991 active,
1992 })
1993 }
1994
1995 pub fn redact_review(&mut self, review: ReviewId) -> Result<(), store::Error> {
1997 self.push(Action::ReviewRedact { review })
1998 }
1999
2000 pub fn merge(&mut self, revision: RevisionId, commit: git::Oid) -> Result<(), store::Error> {
2002 self.push(Action::Merge { revision, commit })
2003 }
2004
2005 pub fn revision(
2007 &mut self,
2008 description: impl ToString,
2009 base: impl Into<git::Oid>,
2010 oid: impl Into<git::Oid>,
2011 ) -> Result<(), store::Error> {
2012 self.push(Action::Revision {
2013 description: description.to_string(),
2014 base: base.into(),
2015 oid: oid.into(),
2016 resolves: BTreeSet::new(),
2017 })
2018 }
2019
2020 pub fn lifecycle(&mut self, state: Lifecycle) -> Result<(), store::Error> {
2022 self.push(Action::Lifecycle { state })
2023 }
2024
2025 pub fn assign(&mut self, assignees: BTreeSet<Did>) -> Result<(), store::Error> {
2027 self.push(Action::Assign { assignees })
2028 }
2029
2030 pub fn label(&mut self, labels: impl IntoIterator<Item = Label>) -> Result<(), store::Error> {
2032 self.push(Action::Label {
2033 labels: labels.into_iter().collect(),
2034 })
2035 }
2036}
2037
2038pub struct PatchMut<'a, 'g, R, C> {
2039 pub id: ObjectId,
2040
2041 patch: Patch,
2042 store: &'g mut Patches<'a, R>,
2043 cache: &'g mut C,
2044}
2045
2046impl<'a, 'g, R, C> PatchMut<'a, 'g, R, C>
2047where
2048 C: cob::cache::Update<Patch>,
2049 R: ReadRepository + SignRepository + cob::Store<Namespace = NodeId>,
2050{
2051 pub fn new(id: ObjectId, patch: Patch, cache: &'g mut Cache<Patches<'a, R>, C>) -> Self {
2052 Self {
2053 id,
2054 patch,
2055 store: &mut cache.store,
2056 cache: &mut cache.cache,
2057 }
2058 }
2059
2060 pub fn id(&self) -> &ObjectId {
2061 &self.id
2062 }
2063
2064 pub fn reload(&mut self) -> Result<(), store::Error> {
2066 self.patch = self
2067 .store
2068 .get(&self.id)?
2069 .ok_or_else(|| store::Error::NotFound(TYPENAME.clone(), self.id))?;
2070
2071 Ok(())
2072 }
2073
2074 pub fn transaction<G, F>(
2075 &mut self,
2076 message: &str,
2077 signer: &Device<G>,
2078 operations: F,
2079 ) -> Result<EntryId, Error>
2080 where
2081 G: crypto::signature::Signer<crypto::Signature>,
2082 F: FnOnce(&mut Transaction<Patch, R>) -> Result<(), store::Error>,
2083 {
2084 let mut tx = Transaction::default();
2085 operations(&mut tx)?;
2086
2087 let (patch, commit) = tx.commit(message, self.id, &mut self.store.raw, signer)?;
2088 self.cache
2089 .update(&self.store.as_ref().id(), &self.id, &patch)
2090 .map_err(|e| Error::CacheUpdate {
2091 id: self.id,
2092 err: e.into(),
2093 })?;
2094 self.patch = patch;
2095
2096 Ok(commit)
2097 }
2098
2099 pub fn edit<G, S>(
2101 &mut self,
2102 title: cob::Title,
2103 target: MergeTarget,
2104 signer: &Device<G>,
2105 ) -> Result<EntryId, Error>
2106 where
2107 G: crypto::signature::Signer<crypto::Signature>,
2108 S: ToString,
2109 {
2110 self.transaction("Edit", signer, |tx| tx.edit(title, target))
2111 }
2112
2113 pub fn edit_revision<G, S>(
2115 &mut self,
2116 revision: RevisionId,
2117 description: S,
2118 embeds: impl IntoIterator<Item = Embed<Uri>>,
2119 signer: &Device<G>,
2120 ) -> Result<EntryId, Error>
2121 where
2122 G: crypto::signature::Signer<crypto::Signature>,
2123 S: ToString,
2124 {
2125 self.transaction("Edit revision", signer, |tx| {
2126 tx.edit_revision(revision, description, embeds.into_iter().collect())
2127 })
2128 }
2129
2130 pub fn redact<G>(&mut self, revision: RevisionId, signer: &Device<G>) -> Result<EntryId, Error>
2132 where
2133 G: crypto::signature::Signer<crypto::Signature>,
2134 {
2135 self.transaction("Redact revision", signer, |tx| tx.redact(revision))
2136 }
2137
2138 pub fn thread<G, S>(
2140 &mut self,
2141 revision: RevisionId,
2142 body: S,
2143 signer: &Device<G>,
2144 ) -> Result<CommentId, Error>
2145 where
2146 G: crypto::signature::Signer<crypto::Signature>,
2147 S: ToString,
2148 {
2149 self.transaction("Create thread", signer, |tx| tx.thread(revision, body))
2150 }
2151
2152 pub fn comment<G, S>(
2154 &mut self,
2155 revision: RevisionId,
2156 body: S,
2157 reply_to: Option<CommentId>,
2158 location: Option<CodeLocation>,
2159 embeds: impl IntoIterator<Item = Embed<Uri>>,
2160 signer: &Device<G>,
2161 ) -> Result<EntryId, Error>
2162 where
2163 G: crypto::signature::Signer<crypto::Signature>,
2164 S: ToString,
2165 {
2166 self.transaction("Comment", signer, |tx| {
2167 tx.comment(
2168 revision,
2169 body,
2170 reply_to,
2171 location,
2172 embeds.into_iter().collect(),
2173 )
2174 })
2175 }
2176
2177 pub fn react<G>(
2179 &mut self,
2180 revision: RevisionId,
2181 reaction: Reaction,
2182 location: Option<CodeLocation>,
2183 active: bool,
2184 signer: &Device<G>,
2185 ) -> Result<EntryId, Error>
2186 where
2187 G: crypto::signature::Signer<crypto::Signature>,
2188 {
2189 self.transaction("React", signer, |tx| {
2190 tx.react(revision, reaction, location, active)
2191 })
2192 }
2193
2194 pub fn comment_edit<G, S>(
2196 &mut self,
2197 revision: RevisionId,
2198 comment: CommentId,
2199 body: S,
2200 embeds: impl IntoIterator<Item = Embed<Uri>>,
2201 signer: &Device<G>,
2202 ) -> Result<EntryId, Error>
2203 where
2204 G: crypto::signature::Signer<crypto::Signature>,
2205 S: ToString,
2206 {
2207 self.transaction("Edit comment", signer, |tx| {
2208 tx.comment_edit(revision, comment, body, embeds.into_iter().collect())
2209 })
2210 }
2211
2212 pub fn comment_react<G>(
2214 &mut self,
2215 revision: RevisionId,
2216 comment: CommentId,
2217 reaction: Reaction,
2218 active: bool,
2219 signer: &Device<G>,
2220 ) -> Result<EntryId, Error>
2221 where
2222 G: crypto::signature::Signer<crypto::Signature>,
2223 {
2224 self.transaction("React comment", signer, |tx| {
2225 tx.comment_react(revision, comment, reaction, active)
2226 })
2227 }
2228
2229 pub fn comment_redact<G>(
2231 &mut self,
2232 revision: RevisionId,
2233 comment: CommentId,
2234 signer: &Device<G>,
2235 ) -> Result<EntryId, Error>
2236 where
2237 G: crypto::signature::Signer<crypto::Signature>,
2238 {
2239 self.transaction("Redact comment", signer, |tx| {
2240 tx.comment_redact(revision, comment)
2241 })
2242 }
2243
2244 pub fn review_comment<G, S>(
2246 &mut self,
2247 review: ReviewId,
2248 body: S,
2249 location: Option<CodeLocation>,
2250 reply_to: Option<CommentId>,
2251 embeds: impl IntoIterator<Item = Embed<Uri>>,
2252 signer: &Device<G>,
2253 ) -> Result<EntryId, Error>
2254 where
2255 G: crypto::signature::Signer<crypto::Signature>,
2256 S: ToString,
2257 {
2258 self.transaction("Review comment", signer, |tx| {
2259 tx.review_comment(
2260 review,
2261 body,
2262 location,
2263 reply_to,
2264 embeds.into_iter().collect(),
2265 )
2266 })
2267 }
2268
2269 pub fn edit_review_comment<G, S>(
2271 &mut self,
2272 review: ReviewId,
2273 comment: EntryId,
2274 body: S,
2275 embeds: impl IntoIterator<Item = Embed<Uri>>,
2276 signer: &Device<G>,
2277 ) -> Result<EntryId, Error>
2278 where
2279 G: crypto::signature::Signer<crypto::Signature>,
2280 S: ToString,
2281 {
2282 self.transaction("Edit review comment", signer, |tx| {
2283 tx.edit_review_comment(review, comment, body, embeds.into_iter().collect())
2284 })
2285 }
2286
2287 pub fn react_review_comment<G>(
2289 &mut self,
2290 review: ReviewId,
2291 comment: EntryId,
2292 reaction: Reaction,
2293 active: bool,
2294 signer: &Device<G>,
2295 ) -> Result<EntryId, Error>
2296 where
2297 G: crypto::signature::Signer<crypto::Signature>,
2298 {
2299 self.transaction("React to review comment", signer, |tx| {
2300 tx.react_review_comment(review, comment, reaction, active)
2301 })
2302 }
2303
2304 pub fn redact_review_comment<G>(
2306 &mut self,
2307 review: ReviewId,
2308 comment: EntryId,
2309 signer: &Device<G>,
2310 ) -> Result<EntryId, Error>
2311 where
2312 G: crypto::signature::Signer<crypto::Signature>,
2313 {
2314 self.transaction("Redact review comment", signer, |tx| {
2315 tx.redact_review_comment(review, comment)
2316 })
2317 }
2318
2319 pub fn review<G>(
2321 &mut self,
2322 revision: RevisionId,
2323 verdict: Option<Verdict>,
2324 summary: Option<String>,
2325 labels: Vec<Label>,
2326 signer: &Device<G>,
2327 ) -> Result<ReviewId, Error>
2328 where
2329 G: crypto::signature::Signer<crypto::Signature>,
2330 {
2331 if verdict.is_none() && summary.is_none() {
2332 return Err(Error::EmptyReview);
2333 }
2334 self.transaction("Review", signer, |tx| {
2335 tx.review(revision, verdict, summary, labels)
2336 })
2337 .map(ReviewId)
2338 }
2339
2340 pub fn review_edit<G>(
2342 &mut self,
2343 review: ReviewId,
2344 verdict: Option<Verdict>,
2345 summary: String,
2346 labels: Vec<Label>,
2347 embeds: impl IntoIterator<Item = Embed<Uri>>,
2348 signer: &Device<G>,
2349 ) -> Result<EntryId, Error>
2350 where
2351 G: crypto::signature::Signer<crypto::Signature>,
2352 {
2353 self.transaction("Edit review", signer, |tx| {
2354 tx.review_edit(review, verdict, summary, labels, embeds)
2355 })
2356 }
2357
2358 pub fn review_react<G>(
2360 &mut self,
2361 review: ReviewId,
2362 reaction: Reaction,
2363 active: bool,
2364 signer: &Device<G>,
2365 ) -> Result<EntryId, Error>
2366 where
2367 G: crypto::signature::Signer<crypto::Signature>,
2368 {
2369 self.transaction("React to review", signer, |tx| {
2370 tx.review_react(review, reaction, active)
2371 })
2372 }
2373
2374 pub fn redact_review<G>(
2376 &mut self,
2377 review: ReviewId,
2378 signer: &Device<G>,
2379 ) -> Result<EntryId, Error>
2380 where
2381 G: crypto::signature::Signer<crypto::Signature>,
2382 {
2383 self.transaction("Redact review", signer, |tx| tx.redact_review(review))
2384 }
2385
2386 pub fn resolve_review_comment<G>(
2388 &mut self,
2389 review: ReviewId,
2390 comment: CommentId,
2391 signer: &Device<G>,
2392 ) -> Result<EntryId, Error>
2393 where
2394 G: crypto::signature::Signer<crypto::Signature>,
2395 {
2396 self.transaction("Resolve review comment", signer, |tx| {
2397 tx.review_comment_resolve(review, comment)
2398 })
2399 }
2400
2401 pub fn unresolve_review_comment<G>(
2403 &mut self,
2404 review: ReviewId,
2405 comment: CommentId,
2406 signer: &Device<G>,
2407 ) -> Result<EntryId, Error>
2408 where
2409 G: crypto::signature::Signer<crypto::Signature>,
2410 {
2411 self.transaction("Unresolve review comment", signer, |tx| {
2412 tx.review_comment_unresolve(review, comment)
2413 })
2414 }
2415
2416 pub fn merge<G>(
2418 &mut self,
2419 revision: RevisionId,
2420 commit: git::Oid,
2421 signer: &Device<G>,
2422 ) -> Result<Merged<R>, Error>
2423 where
2424 G: crypto::signature::Signer<crypto::Signature>,
2425 {
2426 let entry = self.transaction("Merge revision", signer, |tx| tx.merge(revision, commit))?;
2428
2429 Ok(Merged {
2430 entry,
2431 patch: self.id,
2432 stored: self.store.as_ref(),
2433 })
2434 }
2435
2436 pub fn update<G>(
2438 &mut self,
2439 description: impl ToString,
2440 base: impl Into<git::Oid>,
2441 oid: impl Into<git::Oid>,
2442 signer: &Device<G>,
2443 ) -> Result<RevisionId, Error>
2444 where
2445 G: crypto::signature::Signer<crypto::Signature>,
2446 {
2447 self.transaction("Add revision", signer, |tx| {
2448 tx.revision(description, base, oid)
2449 })
2450 .map(RevisionId)
2451 }
2452
2453 pub fn lifecycle<G>(&mut self, state: Lifecycle, signer: &Device<G>) -> Result<EntryId, Error>
2455 where
2456 G: crypto::signature::Signer<crypto::Signature>,
2457 {
2458 self.transaction("Lifecycle", signer, |tx| tx.lifecycle(state))
2459 }
2460
2461 pub fn assign<G>(
2463 &mut self,
2464 assignees: BTreeSet<Did>,
2465 signer: &Device<G>,
2466 ) -> Result<EntryId, Error>
2467 where
2468 G: crypto::signature::Signer<crypto::Signature>,
2469 {
2470 self.transaction("Assign", signer, |tx| tx.assign(assignees))
2471 }
2472
2473 pub fn archive<G>(&mut self, signer: &Device<G>) -> Result<bool, Error>
2475 where
2476 G: crypto::signature::Signer<crypto::Signature>,
2477 {
2478 self.lifecycle(Lifecycle::Archived, signer)?;
2479
2480 Ok(true)
2481 }
2482
2483 pub fn unarchive<G>(&mut self, signer: &Device<G>) -> Result<bool, Error>
2486 where
2487 G: crypto::signature::Signer<crypto::Signature>,
2488 {
2489 if !self.is_archived() {
2490 return Ok(false);
2491 }
2492 self.lifecycle(Lifecycle::Open, signer)?;
2493
2494 Ok(true)
2495 }
2496
2497 pub fn ready<G>(&mut self, signer: &Device<G>) -> Result<bool, Error>
2500 where
2501 G: crypto::signature::Signer<crypto::Signature>,
2502 {
2503 if !self.is_draft() {
2504 return Ok(false);
2505 }
2506 self.lifecycle(Lifecycle::Open, signer)?;
2507
2508 Ok(true)
2509 }
2510
2511 pub fn unready<G>(&mut self, signer: &Device<G>) -> Result<bool, Error>
2514 where
2515 G: crypto::signature::Signer<crypto::Signature>,
2516 {
2517 if !matches!(self.state(), State::Open { conflicts } if conflicts.is_empty()) {
2518 return Ok(false);
2519 }
2520 self.lifecycle(Lifecycle::Draft, signer)?;
2521
2522 Ok(true)
2523 }
2524
2525 pub fn label<G>(
2527 &mut self,
2528 labels: impl IntoIterator<Item = Label>,
2529 signer: &Device<G>,
2530 ) -> Result<EntryId, Error>
2531 where
2532 G: crypto::signature::Signer<crypto::Signature>,
2533 {
2534 self.transaction("Label", signer, |tx| tx.label(labels))
2535 }
2536}
2537
2538impl<R, C> Deref for PatchMut<'_, '_, R, C> {
2539 type Target = Patch;
2540
2541 fn deref(&self) -> &Self::Target {
2542 &self.patch
2543 }
2544}
2545
2546#[derive(Debug, Default, PartialEq, Eq, Serialize)]
2548#[serde(rename_all = "camelCase")]
2549pub struct PatchCounts {
2550 pub open: usize,
2551 pub draft: usize,
2552 pub archived: usize,
2553 pub merged: usize,
2554}
2555
2556impl PatchCounts {
2557 pub fn total(&self) -> usize {
2559 self.open + self.draft + self.archived + self.merged
2560 }
2561}
2562
2563#[derive(Debug, PartialEq, Eq)]
2567pub struct ByRevision {
2568 pub id: PatchId,
2569 pub patch: Patch,
2570 pub revision_id: RevisionId,
2571 pub revision: Revision,
2572}
2573
2574pub struct Patches<'a, R> {
2575 raw: store::Store<'a, Patch, R>,
2576}
2577
2578impl<'a, R> Deref for Patches<'a, R> {
2579 type Target = store::Store<'a, Patch, R>;
2580
2581 fn deref(&self) -> &Self::Target {
2582 &self.raw
2583 }
2584}
2585
2586impl<R> HasRepoId for Patches<'_, R>
2587where
2588 R: ReadRepository,
2589{
2590 fn rid(&self) -> RepoId {
2591 self.as_ref().id()
2592 }
2593}
2594
2595impl<'a, R> Patches<'a, R>
2596where
2597 R: ReadRepository + cob::Store<Namespace = NodeId>,
2598{
2599 pub fn open(repository: &'a R) -> Result<Self, RepositoryError> {
2601 let identity = repository.identity_head()?;
2602 let raw = store::Store::open(repository)?.identity(identity);
2603
2604 Ok(Self { raw })
2605 }
2606
2607 pub fn counts(&self) -> Result<PatchCounts, store::Error> {
2609 let all = self.all()?;
2610 let state_groups =
2611 all.filter_map(|s| s.ok())
2612 .fold(PatchCounts::default(), |mut state, (_, p)| {
2613 match p.state() {
2614 State::Draft => state.draft += 1,
2615 State::Open { .. } => state.open += 1,
2616 State::Archived => state.archived += 1,
2617 State::Merged { .. } => state.merged += 1,
2618 }
2619 state
2620 });
2621
2622 Ok(state_groups)
2623 }
2624
2625 pub fn find_by_revision(&self, revision: &RevisionId) -> Result<Option<ByRevision>, Error> {
2627 let p_id = ObjectId::from(revision.into_inner());
2629 if let Some(p) = self.get(&p_id)? {
2630 return Ok(p.revision(revision).map(|r| ByRevision {
2631 id: p_id,
2632 patch: p.clone(),
2633 revision_id: *revision,
2634 revision: r.clone(),
2635 }));
2636 }
2637 let result = self
2638 .all()?
2639 .filter_map(|result| result.ok())
2640 .find_map(|(p_id, p)| {
2641 p.revision(revision).map(|r| ByRevision {
2642 id: p_id,
2643 patch: p.clone(),
2644 revision_id: *revision,
2645 revision: r.clone(),
2646 })
2647 });
2648
2649 Ok(result)
2650 }
2651
2652 pub fn get(&self, id: &ObjectId) -> Result<Option<Patch>, store::Error> {
2654 self.raw.get(id)
2655 }
2656
2657 pub fn proposed(&self) -> Result<impl Iterator<Item = (PatchId, Patch)> + '_, Error> {
2659 let all = self.all()?;
2660
2661 Ok(all
2662 .into_iter()
2663 .filter_map(|result| result.ok())
2664 .filter(|(_, p)| p.is_open()))
2665 }
2666
2667 pub fn proposed_by<'b>(
2669 &'b self,
2670 who: &'b Did,
2671 ) -> Result<impl Iterator<Item = (PatchId, Patch)> + 'b, Error> {
2672 Ok(self
2673 .proposed()?
2674 .filter(move |(_, p)| p.author().id() == who))
2675 }
2676}
2677
2678impl<'a, R> Patches<'a, R>
2679where
2680 R: ReadRepository + SignRepository + cob::Store<Namespace = NodeId>,
2681{
2682 pub fn create<'g, C, G>(
2684 &'g mut self,
2685 title: cob::Title,
2686 description: impl ToString,
2687 target: MergeTarget,
2688 base: impl Into<git::Oid>,
2689 oid: impl Into<git::Oid>,
2690 labels: &[Label],
2691 cache: &'g mut C,
2692 signer: &Device<G>,
2693 ) -> Result<PatchMut<'a, 'g, R, C>, Error>
2694 where
2695 C: cob::cache::Update<Patch>,
2696 G: crypto::signature::Signer<crypto::Signature>,
2697 {
2698 self._create(
2699 title,
2700 description,
2701 target,
2702 base,
2703 oid,
2704 labels,
2705 Lifecycle::default(),
2706 cache,
2707 signer,
2708 )
2709 }
2710
2711 pub fn draft<'g, C, G>(
2713 &'g mut self,
2714 title: cob::Title,
2715 description: impl ToString,
2716 target: MergeTarget,
2717 base: impl Into<git::Oid>,
2718 oid: impl Into<git::Oid>,
2719 labels: &[Label],
2720 cache: &'g mut C,
2721 signer: &Device<G>,
2722 ) -> Result<PatchMut<'a, 'g, R, C>, Error>
2723 where
2724 C: cob::cache::Update<Patch>,
2725 G: crypto::signature::Signer<crypto::Signature>,
2726 {
2727 self._create(
2728 title,
2729 description,
2730 target,
2731 base,
2732 oid,
2733 labels,
2734 Lifecycle::Draft,
2735 cache,
2736 signer,
2737 )
2738 }
2739
2740 pub fn get_mut<'g, C>(
2742 &'g mut self,
2743 id: &ObjectId,
2744 cache: &'g mut C,
2745 ) -> Result<PatchMut<'a, 'g, R, C>, store::Error> {
2746 let patch = self
2747 .raw
2748 .get(id)?
2749 .ok_or_else(move || store::Error::NotFound(TYPENAME.clone(), *id))?;
2750
2751 Ok(PatchMut {
2752 id: *id,
2753 patch,
2754 store: self,
2755 cache,
2756 })
2757 }
2758
2759 fn _create<'g, C, G>(
2761 &'g mut self,
2762 title: cob::Title,
2763 description: impl ToString,
2764 target: MergeTarget,
2765 base: impl Into<git::Oid>,
2766 oid: impl Into<git::Oid>,
2767 labels: &[Label],
2768 state: Lifecycle,
2769 cache: &'g mut C,
2770 signer: &Device<G>,
2771 ) -> Result<PatchMut<'a, 'g, R, C>, Error>
2772 where
2773 C: cob::cache::Update<Patch>,
2774 G: crypto::signature::Signer<crypto::Signature>,
2775 {
2776 let (id, patch) = Transaction::initial("Create patch", &mut self.raw, signer, |tx, _| {
2777 tx.revision(description, base, oid)?;
2778 tx.edit(title, target)?;
2779
2780 if !labels.is_empty() {
2781 tx.label(labels.to_owned())?;
2782 }
2783 if state != Lifecycle::default() {
2784 tx.lifecycle(state)?;
2785 }
2786 Ok(())
2787 })?;
2788 cache
2789 .update(&self.raw.as_ref().id(), &id, &patch)
2790 .map_err(|e| Error::CacheUpdate { id, err: e.into() })?;
2791
2792 Ok(PatchMut {
2793 id,
2794 patch,
2795 store: self,
2796 cache,
2797 })
2798 }
2799}
2800
2801#[derive(Debug, Clone, PartialEq, Eq, Copy)]
2806pub struct RangeDiff {
2807 old: (git::Oid, git::Oid),
2808 new: (git::Oid, git::Oid),
2809}
2810
2811impl RangeDiff {
2812 const COMMAND: &str = "git";
2813 const SUBCOMMAND: &str = "range-diff";
2814
2815 pub fn new(old: &Revision, new: &Revision) -> Self {
2816 Self {
2817 old: old.range(),
2818 new: new.range(),
2819 }
2820 }
2821
2822 pub fn to_command(&self) -> String {
2823 let range = if self.has_same_base() {
2824 format!("{} {} {}", self.old.0, self.old.1, self.new.1)
2825 } else {
2826 format!(
2827 "{}..{} {}..{}",
2828 self.old.0, self.old.1, self.new.0, self.new.1,
2829 )
2830 };
2831
2832 Self::COMMAND.to_string() + " " + Self::SUBCOMMAND + " " + &range
2833 }
2834
2835 fn has_same_base(&self) -> bool {
2836 self.old.0 == self.new.0
2837 }
2838}
2839
2840impl From<RangeDiff> for std::process::Command {
2841 fn from(range_diff: RangeDiff) -> Self {
2842 let mut command = std::process::Command::new(RangeDiff::COMMAND);
2843
2844 command.arg(RangeDiff::SUBCOMMAND);
2845
2846 if range_diff.has_same_base() {
2847 command.args([
2848 range_diff.old.0.to_string(),
2849 range_diff.old.1.to_string(),
2850 range_diff.new.1.to_string(),
2851 ]);
2852 } else {
2853 command.args([
2854 format!("{}..{}", range_diff.old.0, range_diff.old.1),
2855 format!("{}..{}", range_diff.new.0, range_diff.new.1),
2856 ]);
2857 }
2858 command
2859 }
2860}
2861
2862mod ser {
2864 use std::collections::{BTreeMap, BTreeSet};
2865
2866 use serde::ser::SerializeSeq;
2867
2868 use crate::cob::{thread::Reactions, ActorId, CodeLocation};
2869
2870 #[derive(Debug, serde::Serialize, serde::Deserialize)]
2874 #[serde(rename_all = "camelCase")]
2875 struct Reaction {
2876 location: Option<CodeLocation>,
2877 emoji: super::Reaction,
2878 authors: Vec<ActorId>,
2879 }
2880
2881 impl Reaction {
2882 fn as_revision_reactions(
2883 reactions: Vec<Reaction>,
2884 ) -> BTreeMap<Option<CodeLocation>, Reactions> {
2885 reactions.into_iter().fold(
2886 BTreeMap::<Option<CodeLocation>, Reactions>::new(),
2887 |mut reactions,
2888 Reaction {
2889 location,
2890 emoji,
2891 authors,
2892 }| {
2893 let mut inner = authors
2894 .into_iter()
2895 .map(|author| (author, emoji))
2896 .collect::<BTreeSet<_>>();
2897 let entry = reactions.entry(location).or_default();
2898 entry.append(&mut inner);
2899 reactions
2900 },
2901 )
2902 }
2903 }
2904
2905 pub fn serialize_reactions<S>(
2911 reactions: &BTreeMap<Option<CodeLocation>, Reactions>,
2912 serializer: S,
2913 ) -> Result<S::Ok, S::Error>
2914 where
2915 S: serde::Serializer,
2916 {
2917 let reactions = reactions
2918 .iter()
2919 .flat_map(|(location, reaction)| {
2920 let reactions = reaction.iter().fold(
2921 BTreeMap::new(),
2922 |mut acc: BTreeMap<&super::Reaction, Vec<_>>, (author, emoji)| {
2923 acc.entry(emoji).or_default().push(*author);
2924 acc
2925 },
2926 );
2927 reactions
2928 .into_iter()
2929 .map(|(emoji, authors)| Reaction {
2930 location: location.clone(),
2931 emoji: *emoji,
2932 authors,
2933 })
2934 .collect::<Vec<_>>()
2935 })
2936 .collect::<Vec<_>>();
2937 let mut s = serializer.serialize_seq(Some(reactions.len()))?;
2938 for r in &reactions {
2939 s.serialize_element(r)?;
2940 }
2941 s.end()
2942 }
2943
2944 pub fn deserialize_reactions<'de, D>(
2950 deserializer: D,
2951 ) -> Result<BTreeMap<Option<CodeLocation>, Reactions>, D::Error>
2952 where
2953 D: serde::Deserializer<'de>,
2954 {
2955 struct ReactionsVisitor;
2956
2957 impl<'de> serde::de::Visitor<'de> for ReactionsVisitor {
2958 type Value = Vec<Reaction>;
2959
2960 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
2961 formatter.write_str("a reaction of the form {'location', 'emoji', 'authors'}")
2962 }
2963
2964 fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
2965 where
2966 A: serde::de::SeqAccess<'de>,
2967 {
2968 let mut reactions = Vec::new();
2969 while let Some(reaction) = seq.next_element()? {
2970 reactions.push(reaction);
2971 }
2972 Ok(reactions)
2973 }
2974 }
2975
2976 let reactions = deserializer.deserialize_seq(ReactionsVisitor)?;
2977 Ok(Reaction::as_revision_reactions(reactions))
2978 }
2979}
2980
2981#[cfg(test)]
2982#[allow(clippy::unwrap_used)]
2983mod test {
2984 use std::path::PathBuf;
2985 use std::str::FromStr;
2986 use std::vec;
2987
2988 use pretty_assertions::assert_eq;
2989
2990 use super::*;
2991 use crate::cob::common::CodeRange;
2992 use crate::cob::test::Actor;
2993 use crate::crypto::test::signer::MockSigner;
2994 use crate::identity;
2995 use crate::patch::cache::Patches as _;
2996 use crate::profile::env;
2997 use crate::test;
2998 use crate::test::arbitrary;
2999 use crate::test::arbitrary::gen;
3000 use crate::test::storage::MockRepository;
3001
3002 use cob::migrate;
3003
3004 #[test]
3005 fn test_json_serialization() {
3006 let edit = Action::Label {
3007 labels: BTreeSet::new(),
3008 };
3009 assert_eq!(
3010 serde_json::to_string(&edit).unwrap(),
3011 String::from(r#"{"type":"label","labels":[]}"#)
3012 );
3013 }
3014
3015 #[test]
3016 fn test_reactions_json_serialization() {
3017 #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
3018 #[serde(rename_all = "camelCase")]
3019 struct TestReactions {
3020 #[serde(
3021 serialize_with = "super::ser::serialize_reactions",
3022 deserialize_with = "super::ser::deserialize_reactions"
3023 )]
3024 inner: BTreeMap<Option<CodeLocation>, Reactions>,
3025 }
3026
3027 let reactions = TestReactions {
3028 inner: [(
3029 None,
3030 [
3031 (
3032 "z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8"
3033 .parse()
3034 .unwrap(),
3035 Reaction::new('🚀').unwrap(),
3036 ),
3037 (
3038 "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
3039 .parse()
3040 .unwrap(),
3041 Reaction::new('🙏').unwrap(),
3042 ),
3043 ]
3044 .into_iter()
3045 .collect(),
3046 )]
3047 .into_iter()
3048 .collect(),
3049 };
3050
3051 assert_eq!(
3052 reactions,
3053 serde_json::from_str(&serde_json::to_string(&reactions).unwrap()).unwrap()
3054 );
3055 }
3056
3057 #[test]
3058 fn test_patch_create_and_get() {
3059 let alice = test::setup::NodeWithRepo::default();
3060 let checkout = alice.repo.checkout();
3061 let branch = checkout.branch_with([("README", b"Hello World!")]);
3062 let mut patches = Cache::no_cache(&*alice.repo).unwrap();
3063 let author: Did = alice.signer.public_key().into();
3064 let target = MergeTarget::Delegates;
3065 let patch = patches
3066 .create(
3067 cob::Title::new("My first patch").unwrap(),
3068 "Blah blah blah.",
3069 target,
3070 branch.base,
3071 branch.oid,
3072 &[],
3073 &alice.signer,
3074 )
3075 .unwrap();
3076
3077 let patch_id = patch.id;
3078 let patch = patches.get(&patch_id).unwrap().unwrap();
3079
3080 assert_eq!(patch.title(), "My first patch");
3081 assert_eq!(patch.description(), "Blah blah blah.");
3082 assert_eq!(patch.author().id(), &author);
3083 assert_eq!(patch.state(), &State::Open { conflicts: vec![] });
3084 assert_eq!(patch.target(), target);
3085 assert_eq!(patch.version(), 0);
3086
3087 let (rev_id, revision) = patch.latest();
3088
3089 assert_eq!(revision.author.id(), &author);
3090 assert_eq!(revision.description(), "Blah blah blah.");
3091 assert_eq!(revision.discussion.len(), 0);
3092 assert_eq!(revision.oid, branch.oid);
3093 assert_eq!(revision.base, branch.base);
3094
3095 let ByRevision { id, .. } = patches.find_by_revision(&rev_id).unwrap().unwrap();
3096 assert_eq!(id, patch_id);
3097 }
3098
3099 #[test]
3100 fn test_patch_discussion() {
3101 let alice = test::setup::NodeWithRepo::default();
3102 let checkout = alice.repo.checkout();
3103 let branch = checkout.branch_with([("README", b"Hello World!")]);
3104 let mut patches = Cache::no_cache(&*alice.repo).unwrap();
3105 let patch = patches
3106 .create(
3107 cob::Title::new("My first patch").unwrap(),
3108 "Blah blah blah.",
3109 MergeTarget::Delegates,
3110 branch.base,
3111 branch.oid,
3112 &[],
3113 &alice.signer,
3114 )
3115 .unwrap();
3116
3117 let id = patch.id;
3118 let mut patch = patches.get_mut(&id).unwrap();
3119 let (revision_id, _) = patch.revisions().last().unwrap();
3120 assert!(
3121 patch
3122 .comment(revision_id, "patch comment", None, None, [], &alice.signer)
3123 .is_ok(),
3124 "can comment on patch"
3125 );
3126
3127 let (_, revision) = patch.revisions().last().unwrap();
3128 let (_, comment) = revision.discussion.first().unwrap();
3129 assert_eq!("patch comment", comment.body(), "comment body untouched");
3130 }
3131
3132 #[test]
3133 fn test_patch_merge() {
3134 let alice = test::setup::NodeWithRepo::default();
3135 let checkout = alice.repo.checkout();
3136 let branch = checkout.branch_with([("README", b"Hello World!")]);
3137 let mut patches = Cache::no_cache(&*alice.repo).unwrap();
3138 let mut patch = patches
3139 .create(
3140 cob::Title::new("My first patch").unwrap(),
3141 "Blah blah blah.",
3142 MergeTarget::Delegates,
3143 branch.base,
3144 branch.oid,
3145 &[],
3146 &alice.signer,
3147 )
3148 .unwrap();
3149
3150 let id = patch.id;
3151 let (rid, _) = patch.revisions().next().unwrap();
3152 let _merge = patch.merge(rid, branch.base, &alice.signer).unwrap();
3153 let patch = patches.get(&id).unwrap().unwrap();
3154
3155 let merges = patch.merges.iter().collect::<Vec<_>>();
3156 assert_eq!(merges.len(), 1);
3157
3158 let (merger, merge) = merges.first().unwrap();
3159 assert_eq!(*merger, alice.signer.public_key());
3160 assert_eq!(merge.commit, branch.base);
3161 }
3162
3163 #[test]
3164 fn test_patch_review() {
3165 let alice = test::setup::NodeWithRepo::default();
3166 let checkout = alice.repo.checkout();
3167 let branch = checkout.branch_with([("README", b"Hello World!")]);
3168 let mut patches = Cache::no_cache(&*alice.repo).unwrap();
3169 let mut patch = patches
3170 .create(
3171 cob::Title::new("My first patch").unwrap(),
3172 "Blah blah blah.",
3173 MergeTarget::Delegates,
3174 branch.base,
3175 branch.oid,
3176 &[],
3177 &alice.signer,
3178 )
3179 .unwrap();
3180
3181 let (revision_id, _) = patch.latest();
3182 let review_id = patch
3183 .review(
3184 revision_id,
3185 Some(Verdict::Accept),
3186 Some("LGTM".to_owned()),
3187 vec![],
3188 &alice.signer,
3189 )
3190 .unwrap();
3191
3192 let id = patch.id;
3193 let mut patch = patches.get_mut(&id).unwrap();
3194 let (_, revision) = patch.latest();
3195 assert_eq!(revision.reviews.len(), 1);
3196
3197 let review = revision.review_by(alice.signer.public_key()).unwrap();
3198 assert_eq!(review.verdict(), Some(Verdict::Accept));
3199 assert_eq!(review.summary(), "LGTM");
3200
3201 patch.redact_review(review_id, &alice.signer).unwrap();
3202 patch.reload().unwrap();
3203
3204 let (_, revision) = patch.latest();
3205 assert_eq!(revision.reviews().count(), 0);
3206
3207 patch.redact_review(review_id, &alice.signer).unwrap();
3209 patch
3211 .redact_review(ReviewId(arbitrary::entry_id()), &alice.signer)
3212 .unwrap_err();
3213 }
3214
3215 #[test]
3216 fn test_patch_review_revision_redact() {
3217 let alice = test::setup::NodeWithRepo::default();
3218 let checkout = alice.repo.checkout();
3219 let branch = checkout.branch_with([("README", b"Hello World!")]);
3220 let mut patches = Cache::no_cache(&*alice.repo).unwrap();
3221 let mut patch = patches
3222 .create(
3223 cob::Title::new("My first patch").unwrap(),
3224 "Blah blah blah.",
3225 MergeTarget::Delegates,
3226 branch.base,
3227 branch.oid,
3228 &[],
3229 &alice.signer,
3230 )
3231 .unwrap();
3232
3233 let update = checkout.branch_with([("README", b"Hello Radicle!")]);
3234 let updated = patch
3235 .update("I've made changes.", branch.base, update.oid, &alice.signer)
3236 .unwrap();
3237
3238 let review = patch
3240 .review(updated, Some(Verdict::Accept), None, vec![], &alice.signer)
3241 .unwrap();
3242 patch.redact(updated, &alice.signer).unwrap();
3243 patch.redact_review(review, &alice.signer).unwrap();
3244 }
3245
3246 #[test]
3247 fn test_revision_review_merge_redacted() {
3248 let base = git::Oid::from_str("cb18e95ada2bb38aadd8e6cef0963ce37a87add3").unwrap();
3249 let oid = git::Oid::from_str("518d5069f94c03427f694bb494ac1cd7d1339380").unwrap();
3250 let mut alice = Actor::<MockSigner>::default();
3251 let rid = gen::<RepoId>(1);
3252 let doc = RawDoc::new(
3253 gen::<Project>(1),
3254 vec![alice.did()],
3255 1,
3256 identity::Visibility::Public,
3257 )
3258 .verified()
3259 .unwrap();
3260 let repo = MockRepository::new(rid, doc);
3261
3262 let a1 = alice.op::<Patch>([
3263 Action::Revision {
3264 description: String::new(),
3265 base,
3266 oid,
3267 resolves: Default::default(),
3268 },
3269 Action::Edit {
3270 title: cob::Title::new("My patch").unwrap(),
3271 target: MergeTarget::Delegates,
3272 },
3273 ]);
3274 let a2 = alice.op::<Patch>([Action::Revision {
3275 description: String::from("Second revision"),
3276 base,
3277 oid,
3278 resolves: Default::default(),
3279 }]);
3280 let a3 = alice.op::<Patch>([Action::RevisionRedact {
3281 revision: RevisionId(a2.id()),
3282 }]);
3283 let a4 = alice.op::<Patch>([Action::Review {
3284 revision: RevisionId(a2.id()),
3285 summary: None,
3286 verdict: Some(Verdict::Accept),
3287 labels: vec![],
3288 }]);
3289 let a5 = alice.op::<Patch>([Action::Merge {
3290 revision: RevisionId(a2.id()),
3291 commit: oid,
3292 }]);
3293
3294 let mut patch = Patch::from_ops([a1, a2], &repo).unwrap();
3295 assert_eq!(patch.revisions().count(), 2);
3296
3297 patch.op(a3, [], &repo).unwrap();
3298 assert_eq!(patch.revisions().count(), 1);
3299
3300 patch.op(a4, [], &repo).unwrap();
3301 patch.op(a5, [], &repo).unwrap();
3302 }
3303
3304 #[test]
3305 fn test_revision_edit_redact() {
3306 let base = arbitrary::oid();
3307 let oid = arbitrary::oid();
3308 let repo = gen::<MockRepository>(1);
3309 let time = env::local_time();
3310 let alice = MockSigner::default();
3311 let bob = MockSigner::default();
3312 let mut h0: cob::test::HistoryBuilder<Patch> = cob::test::history(
3313 &[
3314 Action::Revision {
3315 description: String::from("Original"),
3316 base,
3317 oid,
3318 resolves: Default::default(),
3319 },
3320 Action::Edit {
3321 title: cob::Title::new("Some patch").unwrap(),
3322 target: MergeTarget::Delegates,
3323 },
3324 ],
3325 time.into(),
3326 &alice,
3327 );
3328 let r1 = h0.commit(
3329 &Action::Revision {
3330 description: String::from("New"),
3331 base,
3332 oid,
3333 resolves: Default::default(),
3334 },
3335 &alice,
3336 );
3337 let patch = Patch::from_history(&h0, &repo).unwrap();
3338 assert_eq!(patch.revisions().count(), 2);
3339
3340 let mut h1 = h0.clone();
3341 h1.commit(
3342 &Action::RevisionRedact {
3343 revision: RevisionId(r1),
3344 },
3345 &alice,
3346 );
3347
3348 let mut h2 = h0.clone();
3349 h2.commit(
3350 &Action::RevisionEdit {
3351 revision: RevisionId(*h0.root().id()),
3352 description: String::from("Edited"),
3353 embeds: Vec::default(),
3354 },
3355 &bob,
3356 );
3357
3358 h0.merge(h1);
3359 h0.merge(h2);
3360
3361 let patch = Patch::from_history(&h0, &repo).unwrap();
3362 assert_eq!(patch.revisions().count(), 1);
3363 }
3364
3365 #[test]
3366 fn test_revision_reaction() {
3367 let base = git::Oid::from_str("cb18e95ada2bb38aadd8e6cef0963ce37a87add3").unwrap();
3368 let oid = git::Oid::from_str("518d5069f94c03427f694bb494ac1cd7d1339380").unwrap();
3369 let mut alice = Actor::<MockSigner>::default();
3370 let repo = gen::<MockRepository>(1);
3371 let reaction = Reaction::new('👍').expect("failed to create a reaction");
3372
3373 let a1 = alice.op::<Patch>([
3374 Action::Revision {
3375 description: String::new(),
3376 base,
3377 oid,
3378 resolves: Default::default(),
3379 },
3380 Action::Edit {
3381 title: cob::Title::new("My patch").unwrap(),
3382 target: MergeTarget::Delegates,
3383 },
3384 ]);
3385 let a2 = alice.op::<Patch>([Action::RevisionReact {
3386 revision: RevisionId(a1.id()),
3387 location: None,
3388 reaction,
3389 active: true,
3390 }]);
3391 let patch = Patch::from_ops([a1, a2], &repo).unwrap();
3392
3393 let (_, r1) = patch.revisions().next().unwrap();
3394 assert!(!r1.reactions.is_empty());
3395
3396 let mut reactions = r1.reactions.get(&None).unwrap().clone();
3397 assert!(!reactions.is_empty());
3398
3399 let (_, first_reaction) = reactions.pop_first().unwrap();
3400 assert_eq!(first_reaction, reaction);
3401 }
3402
3403 #[test]
3404 fn test_patch_review_edit() {
3405 let alice = test::setup::NodeWithRepo::default();
3406 let checkout = alice.repo.checkout();
3407 let branch = checkout.branch_with([("README", b"Hello World!")]);
3408 let mut patches = Cache::no_cache(&*alice.repo).unwrap();
3409 let mut patch = patches
3410 .create(
3411 cob::Title::new("My first patch").unwrap(),
3412 "Blah blah blah.",
3413 MergeTarget::Delegates,
3414 branch.base,
3415 branch.oid,
3416 &[],
3417 &alice.signer,
3418 )
3419 .unwrap();
3420
3421 let (rid, _) = patch.latest();
3422 let review = patch
3423 .review(
3424 rid,
3425 Some(Verdict::Accept),
3426 Some("LGTM".to_owned()),
3427 vec![],
3428 &alice.signer,
3429 )
3430 .unwrap();
3431 patch
3432 .review_edit(
3433 review,
3434 Some(Verdict::Reject),
3435 "Whoops!".to_owned(),
3436 vec![],
3437 vec![],
3438 &alice.signer,
3439 )
3440 .unwrap(); let (_, revision) = patch.latest();
3443 let review = revision.review_by(alice.signer.public_key()).unwrap();
3444 assert_eq!(review.verdict(), Some(Verdict::Reject));
3445 assert_eq!(review.summary(), "Whoops!");
3446 }
3447
3448 #[test]
3449 fn test_patch_review_duplicate() {
3450 let alice = test::setup::NodeWithRepo::default();
3451 let checkout = alice.repo.checkout();
3452 let branch = checkout.branch_with([("README", b"Hello World!")]);
3453 let mut patches = Cache::no_cache(&*alice.repo).unwrap();
3454 let mut patch = patches
3455 .create(
3456 cob::Title::new("My first patch").unwrap(),
3457 "Blah blah blah.",
3458 MergeTarget::Delegates,
3459 branch.base,
3460 branch.oid,
3461 &[],
3462 &alice.signer,
3463 )
3464 .unwrap();
3465
3466 let (rid, _) = patch.latest();
3467 patch
3468 .review(rid, Some(Verdict::Accept), None, vec![], &alice.signer)
3469 .unwrap();
3470 patch
3471 .review(rid, Some(Verdict::Reject), None, vec![], &alice.signer)
3472 .unwrap(); let (_, revision) = patch.latest();
3475 let review = revision.review_by(alice.signer.public_key()).unwrap();
3476 assert_eq!(review.verdict(), Some(Verdict::Accept));
3477 }
3478
3479 #[test]
3480 fn test_patch_review_edit_comment() {
3481 let alice = test::setup::NodeWithRepo::default();
3482 let checkout = alice.repo.checkout();
3483 let branch = checkout.branch_with([("README", b"Hello World!")]);
3484 let mut patches = Cache::no_cache(&*alice.repo).unwrap();
3485 let mut patch = patches
3486 .create(
3487 cob::Title::new("My first patch").unwrap(),
3488 "Blah blah blah.",
3489 MergeTarget::Delegates,
3490 branch.base,
3491 branch.oid,
3492 &[],
3493 &alice.signer,
3494 )
3495 .unwrap();
3496
3497 let (rid, _) = patch.latest();
3498 let review = patch
3499 .review(rid, Some(Verdict::Accept), None, vec![], &alice.signer)
3500 .unwrap();
3501 patch
3502 .review_comment(review, "First comment!", None, None, [], &alice.signer)
3503 .unwrap();
3504
3505 let _review = patch
3506 .review_edit(
3507 review,
3508 Some(Verdict::Reject),
3509 "".to_string(),
3510 vec![],
3511 vec![],
3512 &alice.signer,
3513 )
3514 .unwrap();
3515 patch
3516 .review_comment(review, "Second comment!", None, None, [], &alice.signer)
3517 .unwrap();
3518
3519 let (_, revision) = patch.latest();
3520 let review = revision.review_by(alice.signer.public_key()).unwrap();
3521 assert_eq!(review.verdict(), Some(Verdict::Reject));
3522 assert_eq!(review.comments().count(), 2);
3523 assert_eq!(review.comments().nth(0).unwrap().1.body(), "First comment!");
3524 assert_eq!(
3525 review.comments().nth(1).unwrap().1.body(),
3526 "Second comment!"
3527 );
3528 }
3529
3530 #[test]
3531 fn test_patch_review_comment() {
3532 let alice = test::setup::NodeWithRepo::default();
3533 let checkout = alice.repo.checkout();
3534 let branch = checkout.branch_with([("README", b"Hello World!")]);
3535 let mut patches = Cache::no_cache(&*alice.repo).unwrap();
3536 let mut patch = patches
3537 .create(
3538 cob::Title::new("My first patch").unwrap(),
3539 "Blah blah blah.",
3540 MergeTarget::Delegates,
3541 branch.base,
3542 branch.oid,
3543 &[],
3544 &alice.signer,
3545 )
3546 .unwrap();
3547
3548 let (rid, _) = patch.latest();
3549 let location = CodeLocation {
3550 commit: branch.oid,
3551 path: PathBuf::from_str("README").unwrap(),
3552 old: None,
3553 new: Some(CodeRange::Lines { range: 5..8 }),
3554 };
3555 let review = patch
3556 .review(rid, Some(Verdict::Accept), None, vec![], &alice.signer)
3557 .unwrap();
3558 patch
3559 .review_comment(
3560 review,
3561 "I like these lines of code",
3562 Some(location.clone()),
3563 None,
3564 [],
3565 &alice.signer,
3566 )
3567 .unwrap();
3568
3569 let (_, revision) = patch.latest();
3570 let review = revision.review_by(alice.signer.public_key()).unwrap();
3571 let (_, comment) = review.comments().next().unwrap();
3572
3573 assert_eq!(comment.body(), "I like these lines of code");
3574 assert_eq!(comment.location(), Some(&location));
3575 }
3576
3577 #[test]
3578 fn test_patch_review_remove_summary() {
3579 let alice = test::setup::NodeWithRepo::default();
3580 let checkout = alice.repo.checkout();
3581 let branch = checkout.branch_with([("README", b"Hello World!")]);
3582 let mut patches = Cache::no_cache(&*alice.repo).unwrap();
3583 let mut patch = patches
3584 .create(
3585 cob::Title::new("My first patch").unwrap(),
3586 "Blah blah blah.",
3587 MergeTarget::Delegates,
3588 branch.base,
3589 branch.oid,
3590 &[],
3591 &alice.signer,
3592 )
3593 .unwrap();
3594
3595 let (rid, _) = patch.latest();
3596 let review = patch
3597 .review(
3598 rid,
3599 Some(Verdict::Accept),
3600 Some("Nah".to_owned()),
3601 vec![],
3602 &alice.signer,
3603 )
3604 .unwrap();
3605 patch
3606 .review_edit(
3607 review,
3608 Some(Verdict::Accept),
3609 "".to_string(),
3610 vec![],
3611 vec![],
3612 &alice.signer,
3613 )
3614 .unwrap();
3615
3616 let id = patch.id;
3617 let patch = patches.get_mut(&id).unwrap();
3618 let (_, revision) = patch.latest();
3619 let review = revision.review_by(alice.signer.public_key()).unwrap();
3620
3621 assert_eq!(review.verdict(), Some(Verdict::Accept));
3622 assert_eq!(review.summary(), "");
3623 }
3624
3625 #[test]
3626 fn test_patch_update() {
3627 let alice = test::setup::NodeWithRepo::default();
3628 let checkout = alice.repo.checkout();
3629 let branch = checkout.branch_with([("README", b"Hello World!")]);
3630 let mut patches = {
3631 let path = alice.tmp.path().join("cobs.db");
3632 let mut db = cob::cache::Store::open(path).unwrap();
3633 let store = cob::patch::Patches::open(&*alice.repo).unwrap();
3634
3635 db.migrate(migrate::ignore).unwrap();
3636 cob::patch::Cache::open(store, db)
3637 };
3638 let mut patch = patches
3639 .create(
3640 cob::Title::new("My first patch").unwrap(),
3641 "Blah blah blah.",
3642 MergeTarget::Delegates,
3643 branch.base,
3644 branch.oid,
3645 &[],
3646 &alice.signer,
3647 )
3648 .unwrap();
3649
3650 assert_eq!(patch.description(), "Blah blah blah.");
3651 assert_eq!(patch.version(), 0);
3652
3653 let update = checkout.branch_with([("README", b"Hello Radicle!")]);
3654 let _ = patch
3655 .update("I've made changes.", branch.base, update.oid, &alice.signer)
3656 .unwrap();
3657
3658 let id = patch.id;
3659 let patch = patches.get(&id).unwrap().unwrap();
3660 assert_eq!(patch.version(), 1);
3661 assert_eq!(patch.revisions.len(), 2);
3662 assert_eq!(patch.revisions().count(), 2);
3663 assert_eq!(
3664 patch.revisions().nth(0).unwrap().1.description(),
3665 "Blah blah blah."
3666 );
3667 assert_eq!(
3668 patch.revisions().nth(1).unwrap().1.description(),
3669 "I've made changes."
3670 );
3671
3672 let (_, revision) = patch.latest();
3673
3674 assert_eq!(patch.version(), 1);
3675 assert_eq!(revision.oid, update.oid);
3676 assert_eq!(revision.description(), "I've made changes.");
3677 }
3678
3679 #[test]
3680 fn test_patch_redact() {
3681 let alice = test::setup::Node::default();
3682 let repo = alice.project();
3683 let branch = repo
3684 .checkout()
3685 .branch_with([("README.md", b"Hello, World!")]);
3686 let mut patches = Cache::no_cache(&*repo).unwrap();
3687 let mut patch = patches
3688 .create(
3689 cob::Title::new("My first patch").unwrap(),
3690 "Blah blah blah.",
3691 MergeTarget::Delegates,
3692 branch.base,
3693 branch.oid,
3694 &[],
3695 &alice.signer,
3696 )
3697 .unwrap();
3698 let patch_id = patch.id;
3699
3700 let update = repo
3701 .checkout()
3702 .branch_with([("README.md", b"Hello, Radicle!")]);
3703 let revision_id = patch
3704 .update("I've made changes.", branch.base, update.oid, &alice.signer)
3705 .unwrap();
3706 assert_eq!(patch.revisions().count(), 2);
3707
3708 patch.redact(revision_id, &alice.signer).unwrap();
3709 assert_eq!(patch.latest().0, RevisionId(*patch_id));
3710 assert_eq!(patch.revisions().count(), 1);
3711
3712 assert_eq!(patch.latest(), patch.root());
3714 assert!(patch.redact(patch.latest().0, &alice.signer).is_err());
3715 }
3716
3717 #[test]
3718 fn test_json() {
3719 use serde_json::json;
3720
3721 assert_eq!(
3722 serde_json::to_value(Action::Lifecycle {
3723 state: Lifecycle::Draft
3724 })
3725 .unwrap(),
3726 json!({
3727 "type": "lifecycle",
3728 "state": { "status": "draft" }
3729 })
3730 );
3731
3732 let revision = RevisionId(arbitrary::entry_id());
3733 assert_eq!(
3734 serde_json::to_value(Action::Review {
3735 revision,
3736 summary: None,
3737 verdict: None,
3738 labels: vec![],
3739 })
3740 .unwrap(),
3741 json!({
3742 "type": "review",
3743 "revision": revision,
3744 })
3745 );
3746
3747 assert_eq!(
3748 serde_json::to_value(CodeRange::Lines { range: 4..8 }).unwrap(),
3749 json!({
3750 "type": "lines",
3751 "range": { "start": 4, "end": 8 },
3752 })
3753 );
3754 }
3755}