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