patch: Correctly merge review edits
Signed-off-by: Alexis Sellier <alexis@radicle.xyz>
This commit is contained in:
parent
919e36ab1f
commit
b3faba715b
|
|
@ -2176,6 +2176,7 @@ dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"multibase",
|
"multibase",
|
||||||
"nonempty 0.8.0",
|
"nonempty 0.8.0",
|
||||||
|
"num-traits",
|
||||||
"olpc-cjson",
|
"olpc-cjson",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use num_traits::Bounded;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::ord::Max;
|
use crate::ord::Max;
|
||||||
|
|
@ -43,3 +44,13 @@ impl From<u64> for Lamport {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Bounded for Lamport {
|
||||||
|
fn min_value() -> Self {
|
||||||
|
Self::from(u64::min_value())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn max_value() -> Self {
|
||||||
|
Self::from(u64::max_value())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,10 @@ impl<K: Ord, V: Semilattice, C: PartialOrd + Ord> LWWMap<K, V, C> {
|
||||||
.filter_map(|(k, v)| v.get().as_ref().map(|v| (k, v)))
|
.filter_map(|(k, v)| v.get().as_ref().map(|v| (k, v)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.iter().count()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.iter().next().is_none()
|
self.iter().next().is_none()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,16 @@ impl<T: PartialOrd> Semilattice for Max<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T: Bounded> Bounded for Max<T> {
|
||||||
|
fn min_value() -> Self {
|
||||||
|
Self::from(T::min_value())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn max_value() -> Self {
|
||||||
|
Self::from(T::max_value())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(clippy::derive_ord_xor_partial_ord)]
|
#[allow(clippy::derive_ord_xor_partial_ord)]
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Ord, Serialize, Deserialize)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Ord, Serialize, Deserialize)]
|
||||||
pub struct Min<T>(pub T);
|
pub struct Min<T>(pub T);
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ cyphernet = { version = "0", optional = true }
|
||||||
fastrand = { version = "1.8.0" }
|
fastrand = { version = "1.8.0" }
|
||||||
git-ref-format = { version = "0", features = ["serde", "macro"] }
|
git-ref-format = { version = "0", features = ["serde", "macro"] }
|
||||||
multibase = { version = "0.9.1" }
|
multibase = { version = "0.9.1" }
|
||||||
|
num-traits = { version = "0.2.15", default-features = false, features = ["std"] }
|
||||||
log = { version = "0.4.17", features = ["std"] }
|
log = { version = "0.4.17", features = ["std"] }
|
||||||
once_cell = { version = "1.13" }
|
once_cell = { version = "1.13" }
|
||||||
olpc-cjson = { version = "0.1.1" }
|
olpc-cjson = { version = "0.1.1" }
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ use std::fmt;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
use num_traits::Bounded;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
@ -57,6 +58,20 @@ impl std::ops::Add<u64> for Timestamp {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Bounded for Timestamp {
|
||||||
|
fn min_value() -> Self {
|
||||||
|
Self {
|
||||||
|
seconds: u64::min_value(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn max_value() -> Self {
|
||||||
|
Self {
|
||||||
|
seconds: u64::max_value(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub enum ReactionError {
|
pub enum ReactionError {
|
||||||
#[error("invalid reaction")]
|
#[error("invalid reaction")]
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ use std::str::FromStr;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use radicle_crdt as crdt;
|
use radicle_crdt as crdt;
|
||||||
use radicle_crdt::clock;
|
use radicle_crdt::clock;
|
||||||
use radicle_crdt::{ActorId, ChangeId, LWWMap, LWWReg, LWWSet, Max, Redactable, Semilattice};
|
use radicle_crdt::{ActorId, ChangeId, LWWReg, LWWSet, Max, Redactable, Semilattice};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
|
@ -271,14 +271,17 @@ impl Patch {
|
||||||
ref inline,
|
ref inline,
|
||||||
} => {
|
} => {
|
||||||
// TODO(cloudhead): Test review on redacted revision.
|
// TODO(cloudhead): Test review on redacted revision.
|
||||||
// TODO(cloudhead): Test that updating a review only requires the fields that we
|
|
||||||
// want to update.
|
|
||||||
if let Some(Redactable::Present(revision)) = self.revisions.get_mut(&revision) {
|
if let Some(Redactable::Present(revision)) = self.revisions.get_mut(&revision) {
|
||||||
revision.reviews.insert(
|
revision
|
||||||
change.author,
|
.reviews
|
||||||
Review::new(verdict, comment.to_owned(), inline.to_owned(), timestamp),
|
.entry(change.author)
|
||||||
change.clock,
|
.or_default()
|
||||||
);
|
.merge(Review::new(
|
||||||
|
verdict,
|
||||||
|
comment.to_owned(),
|
||||||
|
inline.to_owned(),
|
||||||
|
timestamp,
|
||||||
|
));
|
||||||
} else {
|
} else {
|
||||||
return Err(ApplyError::Missing(revision));
|
return Err(ApplyError::Missing(revision));
|
||||||
}
|
}
|
||||||
|
|
@ -360,7 +363,7 @@ pub struct Revision {
|
||||||
/// Merges of this revision into other repositories.
|
/// Merges of this revision into other repositories.
|
||||||
pub merges: LWWSet<Max<Merge>>,
|
pub merges: LWWSet<Max<Merge>>,
|
||||||
/// Reviews of this revision's changes (one per actor).
|
/// Reviews of this revision's changes (one per actor).
|
||||||
pub reviews: LWWMap<ActorId, Review>,
|
pub reviews: BTreeMap<ActorId, Review>,
|
||||||
/// When this revision was created.
|
/// When this revision was created.
|
||||||
pub timestamp: Timestamp,
|
pub timestamp: Timestamp,
|
||||||
}
|
}
|
||||||
|
|
@ -373,7 +376,7 @@ impl Revision {
|
||||||
oid,
|
oid,
|
||||||
discussion: Thread::default(),
|
discussion: Thread::default(),
|
||||||
merges: LWWSet::default(),
|
merges: LWWSet::default(),
|
||||||
reviews: LWWMap::default(),
|
reviews: BTreeMap::default(),
|
||||||
timestamp,
|
timestamp,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -470,7 +473,7 @@ pub struct CodeComment {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A patch review on a revision.
|
/// A patch review on a revision.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||||
pub struct Review {
|
pub struct Review {
|
||||||
/// Review verdict.
|
/// Review verdict.
|
||||||
pub verdict: LWWReg<Option<Verdict>>,
|
pub verdict: LWWReg<Option<Verdict>>,
|
||||||
|
|
@ -1019,13 +1022,66 @@ mod test {
|
||||||
let id = patch.id;
|
let id = patch.id;
|
||||||
let patch = patches.get(&id).unwrap().unwrap();
|
let patch = patches.get(&id).unwrap().unwrap();
|
||||||
let (_, revision) = patch.latest().unwrap();
|
let (_, revision) = patch.latest().unwrap();
|
||||||
assert_eq!(revision.reviews.iter().count(), 1);
|
assert_eq!(revision.reviews.len(), 1);
|
||||||
|
|
||||||
let review = revision.reviews.get(signer.public_key()).unwrap();
|
let review = revision.reviews.get(signer.public_key()).unwrap();
|
||||||
assert_eq!(review.verdict(), Some(Verdict::Accept));
|
assert_eq!(review.verdict(), Some(Verdict::Accept));
|
||||||
assert_eq!(review.comment(), Some("LGTM"));
|
assert_eq!(review.comment(), Some("LGTM"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_patch_review_edit() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let (_, signer, project) = test::setup::context(&tmp);
|
||||||
|
let base = git::Oid::from_str("cb18e95ada2bb38aadd8e6cef0963ce37a87add3").unwrap();
|
||||||
|
let oid = git::Oid::from_str("518d5069f94c03427f694bb494ac1cd7d1339380").unwrap();
|
||||||
|
let mut patches = Patches::open(*signer.public_key(), &project).unwrap();
|
||||||
|
let mut patch = patches
|
||||||
|
.create(
|
||||||
|
"My first patch",
|
||||||
|
"Blah blah blah.",
|
||||||
|
MergeTarget::Delegates,
|
||||||
|
base,
|
||||||
|
oid,
|
||||||
|
&[],
|
||||||
|
&signer,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let (rid, _) = patch.latest().unwrap();
|
||||||
|
let rid = *rid;
|
||||||
|
|
||||||
|
patch
|
||||||
|
.review(
|
||||||
|
rid,
|
||||||
|
Some(Verdict::Accept),
|
||||||
|
Some("LGTM".to_owned()),
|
||||||
|
vec![],
|
||||||
|
&signer,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
patch
|
||||||
|
.review(rid, Some(Verdict::Reject), None, vec![], &signer)
|
||||||
|
.unwrap(); // Overwrite the verdict.
|
||||||
|
|
||||||
|
let id = patch.id;
|
||||||
|
let mut patch = patches.get_mut(&id).unwrap();
|
||||||
|
let (_, revision) = patch.latest().unwrap();
|
||||||
|
assert_eq!(revision.reviews.len(), 1, "the reviews were merged");
|
||||||
|
|
||||||
|
let review = revision.reviews.get(signer.public_key()).unwrap();
|
||||||
|
assert_eq!(review.verdict(), Some(Verdict::Reject));
|
||||||
|
assert_eq!(review.comment(), Some("LGTM"));
|
||||||
|
|
||||||
|
patch
|
||||||
|
.review(rid, None, Some("Whoops!".to_owned()), vec![], &signer)
|
||||||
|
.unwrap(); // Overwrite the comment.
|
||||||
|
let (_, revision) = patch.latest().unwrap();
|
||||||
|
let review = revision.reviews.get(signer.public_key()).unwrap();
|
||||||
|
assert_eq!(review.verdict(), Some(Verdict::Reject));
|
||||||
|
assert_eq!(review.comment(), Some("Whoops!"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_patch_update() {
|
fn test_patch_update() {
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue