Add `id` to `Revision`, forbid duplicate reviews

Some changes to the `Patch` state needed for code review.
This commit is contained in:
cloudhead 2024-07-23 10:13:04 +02:00
parent 81a3bc3d1b
commit 7cecabed42
No known key found for this signature in database
4 changed files with 53 additions and 23 deletions

View File

@ -113,6 +113,7 @@ $ rad cob show --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --type xyz.radicle.patch
"merges": {},
"revisions": {
"d1f7f869fde9fac19c1779c4c2e77e8361333f91": {
"id": "d1f7f869fde9fac19c1779c4c2e77e8361333f91",
"author": {
"id": "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
},

View File

@ -47,7 +47,7 @@ pub struct Read;
#[derive(Clone)]
pub struct Write;
/// A file-backed database storing information about the network.
/// A file-backed database storing materialized COBs.
#[derive(Clone)]
pub struct Store<T> {
pub(super) db: Arc<sql::ConnectionThreadSafe>,

View File

@ -1,5 +1,6 @@
pub mod cache;
use std::collections::btree_map::Entry;
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::fmt;
use std::ops::Deref;
@ -106,6 +107,9 @@ pub enum Error {
/// Review is empty.
#[error("empty review; verdict or summary not provided")]
EmptyReview,
/// Duplicate review.
#[error("review {0} of {1} already exists by author {2}")]
DuplicateReview(ReviewId, RevisionId, NodeId),
/// Error loading the document payload.
#[error("payload failed to load: {0}")]
Payload(#[from] PayloadError),
@ -521,7 +525,7 @@ impl Patch {
t.and_then(|(rev_id, pk)| {
if rev == rev_id {
self.revision(&rev_id)
.and_then(|r| r.review(&pk))
.and_then(|r| r.review_by(&pk))
.map(|r| (review_id, r))
} else {
None
@ -825,10 +829,12 @@ impl Patch {
resolves,
} => {
debug_assert!(!self.revisions.contains_key(&entry));
let id = RevisionId(entry);
self.revisions.insert(
RevisionId(entry),
id,
Some(Revision::new(
id,
author.into(),
description,
base,
@ -886,21 +892,25 @@ impl Patch {
return Err(Error::EmptyReview);
}
if let Some(rev) = rev {
// Nb. Applying two reviews by the same author is not allowed and
// results in the review being redacted.
rev.reviews.insert(
author,
Some(Review::new(
Author::new(author),
verdict,
summary.to_owned(),
labels,
timestamp,
)),
);
let id = ReviewId(entry);
// Nb. Applying two reviews by the same author is not allowed.
match rev.reviews.entry(author) {
Entry::Occupied(_) => {
return Err(Error::DuplicateReview(id, revision, author));
}
Entry::Vacant(e) => {
e.insert(Some(Review::new(
id,
Author::new(author),
verdict,
summary.to_owned(),
labels,
timestamp,
)));
}
}
// Update reviews index.
self.reviews
.insert(ReviewId(entry), Some((revision, author)));
self.reviews.insert(id, Some((revision, author)));
}
}
Action::ReviewCommentReact {
@ -1150,6 +1160,7 @@ impl store::Cob for Patch {
return Err(Error::Init("the second action must be of type `edit`"));
};
let revision = Revision::new(
RevisionId(op.id),
op.author.into(),
description,
base,
@ -1332,6 +1343,8 @@ mod lookup {
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Revision {
/// Revision identifier.
pub(super) id: RevisionId,
/// Author of the revision.
pub(super) author: Author,
/// Revision description.
@ -1358,6 +1371,7 @@ pub struct Revision {
impl Revision {
pub fn new(
id: RevisionId,
author: Author,
description: String,
base: git::Oid,
@ -1368,6 +1382,7 @@ impl Revision {
let description = Edit::new(*author.public_key(), description, timestamp, Vec::default());
Self {
id,
author,
description: NonEmpty::new(description),
base,
@ -1380,6 +1395,10 @@ impl Revision {
}
}
pub fn id(&self) -> RevisionId {
self.id
}
pub fn description(&self) -> &str {
self.description.last().body.as_str()
}
@ -1439,7 +1458,7 @@ impl Revision {
}
/// Get a review by author.
pub fn review(&self, author: &ActorId) -> Option<&Review> {
pub fn review_by(&self, author: &ActorId) -> Option<&Review> {
self.reviews.get(author).and_then(|o| o.as_ref())
}
}
@ -1559,6 +1578,8 @@ impl fmt::Display for Verdict {
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Review {
/// Review identifier.
pub(super) id: ReviewId,
/// Review author.
pub(super) author: Author,
/// Review verdict.
@ -1580,6 +1601,7 @@ pub struct Review {
impl Review {
pub fn new(
id: ReviewId,
author: Author,
verdict: Option<Verdict>,
summary: Option<String>,
@ -1587,6 +1609,7 @@ impl Review {
timestamp: Timestamp,
) -> Self {
Self {
id,
author,
verdict,
summary,
@ -1596,6 +1619,11 @@ impl Review {
}
}
/// Review identifier.
pub fn id(&self) -> ReviewId {
self.id
}
/// Review author.
pub fn author(&self) -> &Author {
&self.author
@ -2891,7 +2919,7 @@ mod test {
let (_, revision) = patch.latest();
assert_eq!(revision.reviews.len(), 1);
let review = revision.review(alice.signer.public_key()).unwrap();
let review = revision.review_by(alice.signer.public_key()).unwrap();
assert_eq!(review.verdict(), Some(Verdict::Accept));
assert_eq!(review.summary(), Some("LGTM"));
@ -3135,7 +3163,7 @@ mod test {
.unwrap(); // Overwrite the comment.
//
let (_, revision) = patch.latest();
let review = revision.review(alice.signer.public_key()).unwrap();
let review = revision.review_by(alice.signer.public_key()).unwrap();
assert_eq!(review.verdict(), Some(Verdict::Reject));
assert_eq!(review.summary(), Some("Whoops!"));
}
@ -3180,7 +3208,7 @@ mod test {
.unwrap();
let (_, revision) = patch.latest();
let review = revision.review(alice.signer.public_key()).unwrap();
let review = revision.review_by(alice.signer.public_key()).unwrap();
let (_, comment) = review.comments().next().unwrap();
assert_eq!(comment.body(), "I like these lines of code");
@ -3216,7 +3244,7 @@ mod test {
let id = patch.id;
let patch = patches.get_mut(&id).unwrap();
let (_, revision) = patch.latest();
let review = revision.review(alice.signer.public_key()).unwrap();
let review = revision.review_by(alice.signer.public_key()).unwrap();
assert_eq!(review.summary(), None);
}

View File

@ -712,7 +712,9 @@ mod tests {
let oid = arbitrary::oid();
let timestamp = env::local_time();
let resolves = BTreeSet::new();
let id = RevisionId::from(arbitrary::oid());
let mut revision = Revision::new(
id,
Author { id: author },
description,
base,
@ -730,7 +732,6 @@ mod tests {
);
let thread = Thread::new(arbitrary::oid(), comment);
revision.discussion = thread;
let id = RevisionId::from(arbitrary::oid());
(id, revision)
}