cob: Changes to patch code review CRDT
This patch incorporates several important changes to code review: * Code review verdict is no longer editable * Made reviews redactable instead * Renamed review top-level comment to "summary" * Renamed inline comments to "comments" * Made inline comments addressable by `EntryId` * This is to allow for editing * Removed inline comments from review creation * Added a `CodeComment` action instead * Removed `Semilattice` instance from `Review` * Added a `EditReview` action instead to have more control * Made it valid for revisions to be redacted concurrently with a review/merge Most of these changes are needed to support proper inline code comments.
This commit is contained in:
parent
e4ab523669
commit
45e87a63b9
|
|
@ -252,7 +252,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
} else {
|
} else {
|
||||||
Some(message)
|
Some(message)
|
||||||
};
|
};
|
||||||
patch.review(*revision_id, verdict, message, vec![], &signer)?;
|
patch.review(*revision_id, verdict, message, &signer)?;
|
||||||
|
|
||||||
match verdict {
|
match verdict {
|
||||||
Some(Verdict::Accept) => {
|
Some(Verdict::Accept) => {
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,13 @@ impl<T> Redactable<T> {
|
||||||
Self::Redacted => None,
|
Self::Redacted => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_mut(&mut self) -> Option<&mut T> {
|
||||||
|
match self {
|
||||||
|
Self::Present(ref mut val) => Some(val),
|
||||||
|
Self::Redacted => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> From<Option<T>> for Redactable<T> {
|
impl<T> From<Option<T>> for Redactable<T> {
|
||||||
|
|
|
||||||
|
|
@ -188,8 +188,8 @@ fn review(nid: &NodeId, alias: Option<String>, review: &Review) -> Value {
|
||||||
"alias": alias,
|
"alias": alias,
|
||||||
},
|
},
|
||||||
"verdict": review.verdict(),
|
"verdict": review.verdict(),
|
||||||
"comment": review.comment(),
|
"summary": review.summary(),
|
||||||
"inline": review.inline().collect::<Vec<_>>(),
|
"comments": review.comments().collect::<Vec<_>>(),
|
||||||
"timestamp": review.timestamp(),
|
"timestamp": review.timestamp(),
|
||||||
}),
|
}),
|
||||||
None => json!({
|
None => json!({
|
||||||
|
|
@ -197,8 +197,8 @@ fn review(nid: &NodeId, alias: Option<String>, review: &Review) -> Value {
|
||||||
"id": nid,
|
"id": nid,
|
||||||
},
|
},
|
||||||
"verdict": review.verdict(),
|
"verdict": review.verdict(),
|
||||||
"comment": review.comment(),
|
"summary": review.summary(),
|
||||||
"inline": review.inline().collect::<Vec<_>>(),
|
"comments": review.comments().collect::<Vec<_>>(),
|
||||||
"timestamp": review.timestamp(),
|
"timestamp": review.timestamp(),
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -644,6 +644,13 @@ async fn patch_update_handler(
|
||||||
} => {
|
} => {
|
||||||
patch.edit_revision(revision, description, &signer)?;
|
patch.edit_revision(revision, description, &signer)?;
|
||||||
}
|
}
|
||||||
|
patch::Action::EditReview {
|
||||||
|
revision,
|
||||||
|
author,
|
||||||
|
summary,
|
||||||
|
} => {
|
||||||
|
patch.edit_review(revision, author, summary, &signer)?;
|
||||||
|
}
|
||||||
patch::Action::Tag { add, remove } => {
|
patch::Action::Tag { add, remove } => {
|
||||||
patch.tag(add, remove, &signer)?;
|
patch.tag(add, remove, &signer)?;
|
||||||
}
|
}
|
||||||
|
|
@ -662,11 +669,13 @@ async fn patch_update_handler(
|
||||||
}
|
}
|
||||||
patch::Action::Review {
|
patch::Action::Review {
|
||||||
revision,
|
revision,
|
||||||
comment,
|
summary,
|
||||||
verdict,
|
verdict,
|
||||||
inline,
|
|
||||||
} => {
|
} => {
|
||||||
patch.review(revision, verdict, comment, inline, &signer)?;
|
patch.review(revision, verdict, summary, &signer)?;
|
||||||
|
}
|
||||||
|
patch::Action::CodeComment { .. } => {
|
||||||
|
todo!()
|
||||||
}
|
}
|
||||||
patch::Action::Merge { revision, commit } => {
|
patch::Action::Merge { revision, commit } => {
|
||||||
patch.merge(revision, commit, &signer)?;
|
patch.merge(revision, commit, &signer)?;
|
||||||
|
|
@ -2237,23 +2246,8 @@ mod routes {
|
||||||
let thread_body = serde_json::to_vec(&json!({
|
let thread_body = serde_json::to_vec(&json!({
|
||||||
"type": "review",
|
"type": "review",
|
||||||
"revision": CONTRIBUTOR_PATCH_ID,
|
"revision": CONTRIBUTOR_PATCH_ID,
|
||||||
"comment": "A small review",
|
"summary": "A small review",
|
||||||
"verdict": "accept",
|
"verdict": "accept",
|
||||||
"inline": [
|
|
||||||
{
|
|
||||||
"location": {
|
|
||||||
"blob": "82eb77880c693655bce074e3dbbd9fa711dc018b",
|
|
||||||
"path": "./README.md",
|
|
||||||
"commit": HEAD,
|
|
||||||
"lines": {
|
|
||||||
"start": 1,
|
|
||||||
"end": 3,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"comment": "This is a comment on line 1",
|
|
||||||
"timestamp": TIMESTAMP,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}))
|
}))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let response = patch(
|
let response = patch(
|
||||||
|
|
@ -2305,17 +2299,8 @@ mod routes {
|
||||||
"id": CONTRIBUTOR_NID,
|
"id": CONTRIBUTOR_NID,
|
||||||
},
|
},
|
||||||
"verdict": "accept",
|
"verdict": "accept",
|
||||||
"comment": "A small review",
|
"summary": "A small review",
|
||||||
"inline": [
|
"comments": [],
|
||||||
{
|
|
||||||
"location": {
|
|
||||||
"path": "./README.md",
|
|
||||||
"old": null,
|
|
||||||
"new": null,
|
|
||||||
},
|
|
||||||
"comment": "This is a comment on line 1",
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"timestamp": TIMESTAMP,
|
"timestamp": TIMESTAMP,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,9 @@ pub enum Error {
|
||||||
/// that hasn't happened yet.
|
/// that hasn't happened yet.
|
||||||
#[error("causal dependency {0:?} missing")]
|
#[error("causal dependency {0:?} missing")]
|
||||||
Missing(EntryId),
|
Missing(EntryId),
|
||||||
|
/// Missing actor.
|
||||||
|
#[error("missing actor {0:?}")]
|
||||||
|
MissingActor(ActorId),
|
||||||
/// Error applying an op to the patch thread.
|
/// Error applying an op to the patch thread.
|
||||||
#[error("thread apply failed: {0}")]
|
#[error("thread apply failed: {0}")]
|
||||||
Thread(#[from] thread::Error),
|
Thread(#[from] thread::Error),
|
||||||
|
|
@ -99,6 +102,11 @@ pub enum Action {
|
||||||
revision: RevisionId,
|
revision: RevisionId,
|
||||||
description: String,
|
description: String,
|
||||||
},
|
},
|
||||||
|
EditReview {
|
||||||
|
revision: RevisionId,
|
||||||
|
author: ActorId,
|
||||||
|
summary: Option<String>,
|
||||||
|
},
|
||||||
Tag {
|
Tag {
|
||||||
add: Vec<Tag>,
|
add: Vec<Tag>,
|
||||||
remove: Vec<Tag>,
|
remove: Vec<Tag>,
|
||||||
|
|
@ -116,9 +124,14 @@ pub enum Action {
|
||||||
},
|
},
|
||||||
Review {
|
Review {
|
||||||
revision: RevisionId,
|
revision: RevisionId,
|
||||||
comment: Option<String>,
|
summary: Option<String>,
|
||||||
verdict: Option<Verdict>,
|
verdict: Option<Verdict>,
|
||||||
inline: Vec<CodeComment>,
|
},
|
||||||
|
CodeComment {
|
||||||
|
revision: RevisionId,
|
||||||
|
author: ActorId,
|
||||||
|
comment: String,
|
||||||
|
location: CodeLocation,
|
||||||
},
|
},
|
||||||
Merge {
|
Merge {
|
||||||
revision: RevisionId,
|
revision: RevisionId,
|
||||||
|
|
@ -430,6 +443,25 @@ impl store::FromHistory for Patch {
|
||||||
return Err(Error::Missing(revision));
|
return Err(Error::Missing(revision));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Action::EditReview {
|
||||||
|
revision,
|
||||||
|
author,
|
||||||
|
summary,
|
||||||
|
} => {
|
||||||
|
let Some(revision) = self.revisions.get_mut(&revision) else {
|
||||||
|
return Err(Error::Missing(revision));
|
||||||
|
};
|
||||||
|
// If the revision was redacted concurrently, there's nothing to do.
|
||||||
|
// Likewise, if the review was redacted concurrently, there's nothing to do.
|
||||||
|
if let Some(revision) = revision.get_mut() {
|
||||||
|
let Some(review) = revision.reviews.get_mut(&author) else {
|
||||||
|
return Err(Error::MissingActor(author));
|
||||||
|
};
|
||||||
|
if let Redactable::Present(review) = review {
|
||||||
|
review.summary.set(summary.map(Max::from), op.clock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Action::Revision {
|
Action::Revision {
|
||||||
description,
|
description,
|
||||||
base,
|
base,
|
||||||
|
|
@ -457,27 +489,34 @@ impl store::FromHistory for Patch {
|
||||||
}
|
}
|
||||||
Action::Review {
|
Action::Review {
|
||||||
revision,
|
revision,
|
||||||
ref comment,
|
ref summary,
|
||||||
verdict,
|
verdict,
|
||||||
ref inline,
|
|
||||||
} => {
|
} => {
|
||||||
if let Some(Redactable::Present(revision)) = self.revisions.get_mut(&revision) {
|
let Some(revision) = self.revisions.get_mut(&revision) else {
|
||||||
|
return Err(Error::Missing(revision));
|
||||||
|
};
|
||||||
|
if let Some(revision) = revision.get_mut() {
|
||||||
|
// Nb. Applying two reviews by the same author is not allowed and
|
||||||
|
// results in the review being redacted.
|
||||||
revision.reviews.insert(
|
revision.reviews.insert(
|
||||||
op.author,
|
op.author,
|
||||||
Review::new(
|
Redactable::Present(Review::new(
|
||||||
verdict,
|
verdict,
|
||||||
comment.to_owned(),
|
summary.to_owned(),
|
||||||
inline.to_owned(),
|
|
||||||
timestamp,
|
timestamp,
|
||||||
op.clock,
|
op.clock,
|
||||||
),
|
)),
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
return Err(Error::Missing(revision));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Action::CodeComment { .. } => {
|
||||||
|
// TODO: Not yet implemented.
|
||||||
|
}
|
||||||
Action::Merge { revision, commit } => {
|
Action::Merge { revision, commit } => {
|
||||||
if let Some(Redactable::Present(_)) = self.revisions.get_mut(&revision) {
|
let Some(rev) = self.revisions.get_mut(&revision) else {
|
||||||
|
return Err(Error::Missing(revision));
|
||||||
|
};
|
||||||
|
if rev.get().is_some() {
|
||||||
let doc = repo.identity_doc_at(op.identity)?.verified()?;
|
let doc = repo.identity_doc_at(op.identity)?.verified()?;
|
||||||
|
|
||||||
match self.target() {
|
match self.target() {
|
||||||
|
|
@ -548,8 +587,6 @@ impl store::FromHistory for Patch {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
return Err(Error::Missing(revision));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Action::Thread { revision, action } => {
|
Action::Thread { revision, action } => {
|
||||||
|
|
@ -591,7 +628,7 @@ pub struct Revision {
|
||||||
/// Discussion around this revision.
|
/// Discussion around this revision.
|
||||||
discussion: Thread,
|
discussion: Thread,
|
||||||
/// Reviews of this revision's changes (one per actor).
|
/// Reviews of this revision's changes (one per actor).
|
||||||
reviews: GMap<ActorId, Review>,
|
reviews: GMap<ActorId, Redactable<Review>>,
|
||||||
/// When this revision was created.
|
/// When this revision was created.
|
||||||
timestamp: Timestamp,
|
timestamp: Timestamp,
|
||||||
}
|
}
|
||||||
|
|
@ -647,7 +684,14 @@ impl Revision {
|
||||||
|
|
||||||
/// Reviews of this revision's changes (one per actor).
|
/// Reviews of this revision's changes (one per actor).
|
||||||
pub fn reviews(&self) -> impl DoubleEndedIterator<Item = (&PublicKey, &Review)> {
|
pub fn reviews(&self) -> impl DoubleEndedIterator<Item = (&PublicKey, &Review)> {
|
||||||
self.reviews.iter()
|
self.reviews
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(author, review)| review.get().map(|r| (author, r)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a review by author.
|
||||||
|
pub fn review(&self, author: &ActorId) -> Option<&Review> {
|
||||||
|
self.reviews.get(author).and_then(Redactable::get)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -753,13 +797,62 @@ impl Ord for CodeLocation {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Comment on code diff.
|
/// Comment on code diff.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct CodeComment {
|
pub struct CodeComment {
|
||||||
|
/// Comment author.
|
||||||
|
author: ActorId,
|
||||||
/// Code location of the comment.
|
/// Code location of the comment.
|
||||||
pub location: CodeLocation,
|
location: CodeLocation,
|
||||||
/// Comment.
|
/// Comment edits.
|
||||||
pub comment: String,
|
edits: GMap<Lamport, Max<thread::Edit>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for CodeComment {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::ser::Serializer,
|
||||||
|
{
|
||||||
|
let mut state = serializer.serialize_struct("CodeComment", 3)?;
|
||||||
|
state.serialize_field("author", &self.author())?;
|
||||||
|
state.serialize_field("location", self.location())?;
|
||||||
|
state.serialize_field("body", self.body())?;
|
||||||
|
state.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CodeComment {
|
||||||
|
pub fn new(
|
||||||
|
author: ActorId,
|
||||||
|
body: String,
|
||||||
|
location: CodeLocation,
|
||||||
|
timestamp: Timestamp,
|
||||||
|
) -> Self {
|
||||||
|
let edit = thread::Edit { body, timestamp };
|
||||||
|
|
||||||
|
Self {
|
||||||
|
author,
|
||||||
|
location,
|
||||||
|
edits: GMap::singleton(Lamport::initial(), Max::from(edit)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Comment author.
|
||||||
|
pub fn author(&self) -> ActorId {
|
||||||
|
self.author
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the comment location.
|
||||||
|
pub fn location(&self) -> &CodeLocation {
|
||||||
|
&self.location
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the comment body. If there are multiple edits, gets the value at the latest edit.
|
||||||
|
pub fn body(&self) -> &str {
|
||||||
|
// SAFETY: There is always at least one edit. This is guaranteed by [`CodeComment::new`]
|
||||||
|
// constructor.
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
|
self.edits.values().last().unwrap().get().body.as_str()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A patch review on a revision.
|
/// A patch review on a revision.
|
||||||
|
|
@ -767,18 +860,16 @@ pub struct CodeComment {
|
||||||
pub struct Review {
|
pub struct Review {
|
||||||
/// Review verdict.
|
/// Review verdict.
|
||||||
///
|
///
|
||||||
/// Nb. if the verdict is set and a subsequent review is made with
|
/// The verdict cannot be changed, since revisions are immutable.
|
||||||
/// the verdict as `None`, the original verdict will be nullified.
|
verdict: Option<Verdict>,
|
||||||
verdict: LWWReg<Option<Verdict>>,
|
/// Review summary.
|
||||||
/// Review general comment.
|
|
||||||
///
|
///
|
||||||
/// Nb. if the comment is set and a subsequent review is made with
|
/// Can be edited or set to `None`.
|
||||||
/// the comment as `None`, the original comment will be nullified.
|
summary: LWWReg<Option<Max<String>>>,
|
||||||
comment: LWWReg<Option<Max<String>>>,
|
|
||||||
/// Review inline code comments.
|
/// Review inline code comments.
|
||||||
inline: LWWSet<Max<CodeComment>>,
|
comments: GMap<EntryId, Redactable<CodeComment>>,
|
||||||
/// Review timestamp.
|
/// Review timestamp.
|
||||||
timestamp: Max<Timestamp>,
|
timestamp: Timestamp,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Serialize for Review {
|
impl Serialize for Review {
|
||||||
|
|
@ -788,61 +879,51 @@ impl Serialize for Review {
|
||||||
{
|
{
|
||||||
let mut state = serializer.serialize_struct("Review", 4)?;
|
let mut state = serializer.serialize_struct("Review", 4)?;
|
||||||
state.serialize_field("verdict", &self.verdict())?;
|
state.serialize_field("verdict", &self.verdict())?;
|
||||||
state.serialize_field("comment", &self.comment())?;
|
state.serialize_field("summary", &self.summary())?;
|
||||||
state.serialize_field("inline", &self.inline().collect::<Vec<_>>())?;
|
state.serialize_field(
|
||||||
|
"comments",
|
||||||
|
&self.comments().map(|(_, c)| c.body()).collect::<Vec<_>>(),
|
||||||
|
)?;
|
||||||
state.serialize_field("timestamp", &self.timestamp())?;
|
state.serialize_field("timestamp", &self.timestamp())?;
|
||||||
state.end()
|
state.end()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Semilattice for Review {
|
|
||||||
fn merge(&mut self, other: Self) {
|
|
||||||
self.verdict.merge(other.verdict);
|
|
||||||
self.comment.merge(other.comment);
|
|
||||||
self.inline.merge(other.inline);
|
|
||||||
self.timestamp.merge(other.timestamp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Review {
|
impl Review {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
verdict: Option<Verdict>,
|
verdict: Option<Verdict>,
|
||||||
comment: Option<String>,
|
summary: Option<String>,
|
||||||
inline: Vec<CodeComment>,
|
|
||||||
timestamp: Timestamp,
|
timestamp: Timestamp,
|
||||||
clock: Clock,
|
clock: Clock,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
verdict: LWWReg::new(verdict, clock),
|
verdict,
|
||||||
comment: LWWReg::new(comment.map(Max::from), clock),
|
summary: LWWReg::new(summary.map(Max::from), clock),
|
||||||
inline: LWWSet::from_iter(
|
comments: GMap::default(),
|
||||||
inline
|
timestamp,
|
||||||
.into_iter()
|
|
||||||
.map(Max::from)
|
|
||||||
.zip(std::iter::repeat(clock)),
|
|
||||||
),
|
|
||||||
timestamp: Max::from(timestamp),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Review verdict.
|
/// Review verdict.
|
||||||
pub fn verdict(&self) -> Option<Verdict> {
|
pub fn verdict(&self) -> Option<Verdict> {
|
||||||
self.verdict.get().as_ref().copied()
|
self.verdict
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Review inline code comments.
|
/// Review inline code comments.
|
||||||
pub fn inline(&self) -> impl Iterator<Item = &CodeComment> {
|
pub fn comments(&self) -> impl Iterator<Item = (&EntryId, &CodeComment)> {
|
||||||
self.inline.iter().map(|m| m.get())
|
self.comments
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(id, r)| r.get().map(|comment| (id, comment)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Review general comment.
|
/// Review general comment.
|
||||||
pub fn comment(&self) -> Option<&str> {
|
pub fn summary(&self) -> Option<&str> {
|
||||||
self.comment.get().as_ref().map(|m| m.get().as_str())
|
self.summary.get().as_ref().map(|m| m.get().as_str())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Review timestamp.
|
/// Review timestamp.
|
||||||
pub fn timestamp(&self) -> Timestamp {
|
pub fn timestamp(&self) -> Timestamp {
|
||||||
*self.timestamp.get()
|
self.timestamp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -865,6 +946,19 @@ impl store::Transaction<Patch> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn edit_review(
|
||||||
|
&mut self,
|
||||||
|
revision: RevisionId,
|
||||||
|
author: ActorId,
|
||||||
|
summary: Option<String>,
|
||||||
|
) -> Result<(), store::Error> {
|
||||||
|
self.push(Action::EditReview {
|
||||||
|
revision,
|
||||||
|
author,
|
||||||
|
summary,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Redact the revision.
|
/// Redact the revision.
|
||||||
pub fn redact(&mut self, revision: RevisionId) -> Result<(), store::Error> {
|
pub fn redact(&mut self, revision: RevisionId) -> Result<(), store::Error> {
|
||||||
self.push(Action::Redact { revision })
|
self.push(Action::Redact { revision })
|
||||||
|
|
@ -906,14 +1000,12 @@ impl store::Transaction<Patch> {
|
||||||
&mut self,
|
&mut self,
|
||||||
revision: RevisionId,
|
revision: RevisionId,
|
||||||
verdict: Option<Verdict>,
|
verdict: Option<Verdict>,
|
||||||
comment: Option<String>,
|
summary: Option<String>,
|
||||||
inline: Vec<CodeComment>,
|
|
||||||
) -> Result<(), store::Error> {
|
) -> Result<(), store::Error> {
|
||||||
self.push(Action::Review {
|
self.push(Action::Review {
|
||||||
revision,
|
revision,
|
||||||
comment,
|
summary,
|
||||||
verdict,
|
verdict,
|
||||||
inline,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1024,6 +1116,19 @@ impl<'a, 'g> PatchMut<'a, 'g> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Edit review.
|
||||||
|
pub fn edit_review<G: Signer>(
|
||||||
|
&mut self,
|
||||||
|
revision: RevisionId,
|
||||||
|
author: ActorId,
|
||||||
|
summary: Option<String>,
|
||||||
|
signer: &G,
|
||||||
|
) -> Result<EntryId, Error> {
|
||||||
|
self.transaction("Edit review", signer, |tx| {
|
||||||
|
tx.edit_review(revision, author, summary)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Redact a revision.
|
/// Redact a revision.
|
||||||
pub fn redact<G: Signer>(
|
pub fn redact<G: Signer>(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
|
@ -1063,12 +1168,9 @@ impl<'a, 'g> PatchMut<'a, 'g> {
|
||||||
revision: RevisionId,
|
revision: RevisionId,
|
||||||
verdict: Option<Verdict>,
|
verdict: Option<Verdict>,
|
||||||
comment: Option<String>,
|
comment: Option<String>,
|
||||||
inline: Vec<CodeComment>,
|
|
||||||
signer: &G,
|
signer: &G,
|
||||||
) -> Result<EntryId, Error> {
|
) -> Result<EntryId, Error> {
|
||||||
self.transaction("Review", signer, |tx| {
|
self.transaction("Review", signer, |tx| tx.review(revision, verdict, comment))
|
||||||
tx.review(revision, verdict, comment, inline)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Merge a patch revision.
|
/// Merge a patch revision.
|
||||||
|
|
@ -1330,7 +1432,6 @@ impl<'a> Patches<'a> {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use std::path::Path;
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::{array, iter};
|
use std::{array, iter};
|
||||||
|
|
||||||
|
|
@ -1688,13 +1789,7 @@ mod test {
|
||||||
|
|
||||||
let (rid, _) = patch.latest();
|
let (rid, _) = patch.latest();
|
||||||
patch
|
patch
|
||||||
.review(
|
.review(*rid, Some(Verdict::Accept), Some("LGTM".to_owned()), signer)
|
||||||
*rid,
|
|
||||||
Some(Verdict::Accept),
|
|
||||||
Some("LGTM".to_owned()),
|
|
||||||
vec![],
|
|
||||||
signer,
|
|
||||||
)
|
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let id = patch.id;
|
let id = patch.id;
|
||||||
|
|
@ -1702,13 +1797,13 @@ mod test {
|
||||||
let (_, revision) = patch.latest();
|
let (_, revision) = patch.latest();
|
||||||
assert_eq!(revision.reviews.len(), 1);
|
assert_eq!(revision.reviews.len(), 1);
|
||||||
|
|
||||||
let review = revision.reviews.get(signer.public_key()).unwrap();
|
let review = revision.review(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.summary(), Some("LGTM"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_revision_redacted() {
|
fn test_revision_review_merge_redacted() {
|
||||||
let base = git::Oid::from_str("cb18e95ada2bb38aadd8e6cef0963ce37a87add3").unwrap();
|
let base = git::Oid::from_str("cb18e95ada2bb38aadd8e6cef0963ce37a87add3").unwrap();
|
||||||
let oid = git::Oid::from_str("518d5069f94c03427f694bb494ac1cd7d1339380").unwrap();
|
let oid = git::Oid::from_str("518d5069f94c03427f694bb494ac1cd7d1339380").unwrap();
|
||||||
let mut alice = Actor::new(MockSigner::default());
|
let mut alice = Actor::new(MockSigner::default());
|
||||||
|
|
@ -1723,9 +1818,8 @@ mod test {
|
||||||
let a2 = alice.op(Action::Redact { revision: a1.id() });
|
let a2 = alice.op(Action::Redact { revision: a1.id() });
|
||||||
let a3 = alice.op(Action::Review {
|
let a3 = alice.op(Action::Review {
|
||||||
revision: a1.id(),
|
revision: a1.id(),
|
||||||
comment: None,
|
summary: None,
|
||||||
verdict: Some(Verdict::Accept),
|
verdict: Some(Verdict::Accept),
|
||||||
inline: vec![],
|
|
||||||
});
|
});
|
||||||
let a4 = alice.op(Action::Merge {
|
let a4 = alice.op(Action::Merge {
|
||||||
revision: a1.id(),
|
revision: a1.id(),
|
||||||
|
|
@ -1738,8 +1832,8 @@ mod test {
|
||||||
patch.apply([a2], &repo).unwrap();
|
patch.apply([a2], &repo).unwrap();
|
||||||
assert!(patch.revisions().next().is_none());
|
assert!(patch.revisions().next().is_none());
|
||||||
|
|
||||||
patch.apply([a3], &repo).unwrap_err();
|
patch.apply([a3], &repo).unwrap();
|
||||||
patch.apply([a4], &repo).unwrap_err();
|
patch.apply([a4], &repo).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -1791,112 +1885,26 @@ mod test {
|
||||||
let (rid, _) = patch.latest();
|
let (rid, _) = patch.latest();
|
||||||
let rid = *rid;
|
let rid = *rid;
|
||||||
|
|
||||||
let inline = vec![CodeComment {
|
|
||||||
location: CodeLocation {
|
|
||||||
path: Path::new("file.rs").to_path_buf(),
|
|
||||||
old: Some(1..3),
|
|
||||||
new: Some(1..3),
|
|
||||||
},
|
|
||||||
comment: "Nice!".to_owned(),
|
|
||||||
}];
|
|
||||||
patch
|
patch
|
||||||
.review(
|
.review(rid, Some(Verdict::Accept), Some("LGTM".to_owned()), signer)
|
||||||
rid,
|
|
||||||
Some(Verdict::Accept),
|
|
||||||
Some("LGTM".to_owned()),
|
|
||||||
inline.clone(),
|
|
||||||
signer,
|
|
||||||
)
|
|
||||||
.unwrap();
|
.unwrap();
|
||||||
patch
|
patch
|
||||||
.review(
|
.edit_review(
|
||||||
rid,
|
rid,
|
||||||
Some(Verdict::Reject),
|
*signer.public_key(),
|
||||||
Some("LGTM".to_owned()),
|
|
||||||
vec![],
|
|
||||||
signer,
|
|
||||||
)
|
|
||||||
.unwrap(); // Overwrite the verdict.
|
|
||||||
|
|
||||||
let id = patch.id;
|
|
||||||
let mut patch = patches.get_mut(&id).unwrap();
|
|
||||||
let (_, revision) = patch.latest();
|
|
||||||
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"));
|
|
||||||
assert_eq!(review.inline().cloned().collect::<Vec<_>>(), inline);
|
|
||||||
|
|
||||||
patch
|
|
||||||
.review(
|
|
||||||
rid,
|
|
||||||
Some(Verdict::Reject),
|
|
||||||
Some("Whoops!".to_owned()),
|
Some("Whoops!".to_owned()),
|
||||||
vec![],
|
|
||||||
signer,
|
signer,
|
||||||
)
|
)
|
||||||
.unwrap(); // Overwrite the comment.
|
.unwrap(); // Overwrite the comment.
|
||||||
|
//
|
||||||
let (_, revision) = patch.latest();
|
let (_, revision) = patch.latest();
|
||||||
let review = revision.reviews.get(signer.public_key()).unwrap();
|
let review = revision.review(signer.public_key()).unwrap();
|
||||||
assert_eq!(review.verdict(), Some(Verdict::Reject));
|
|
||||||
assert_eq!(review.comment(), Some("Whoops!"));
|
|
||||||
assert_eq!(review.inline().cloned().collect::<Vec<_>>(), inline);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_patch_reject_to_accept() {
|
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
|
||||||
let ctx = test::setup::Context::new(&tmp);
|
|
||||||
let signer = &ctx.signer;
|
|
||||||
let pr = ctx.branch_with(test::setup::initial_blobs());
|
|
||||||
let mut patches = Patches::open(&ctx.project).unwrap();
|
|
||||||
let mut patch = patches
|
|
||||||
.create(
|
|
||||||
"My first patch",
|
|
||||||
"Blah blah blah.",
|
|
||||||
MergeTarget::Delegates,
|
|
||||||
pr.base,
|
|
||||||
pr.oid,
|
|
||||||
&[],
|
|
||||||
signer,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let (rid, _) = patch.latest();
|
|
||||||
let rid = *rid;
|
|
||||||
|
|
||||||
patch
|
|
||||||
.review(
|
|
||||||
rid,
|
|
||||||
Some(Verdict::Reject),
|
|
||||||
Some("Nah".to_owned()),
|
|
||||||
vec![],
|
|
||||||
signer,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
patch
|
|
||||||
.review(
|
|
||||||
rid,
|
|
||||||
Some(Verdict::Accept),
|
|
||||||
Some("LGTM".to_owned()),
|
|
||||||
vec![],
|
|
||||||
signer,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let id = patch.id;
|
|
||||||
let patch = patches.get_mut(&id).unwrap();
|
|
||||||
let (_, revision) = patch.latest();
|
|
||||||
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::Accept));
|
assert_eq!(review.verdict(), Some(Verdict::Accept));
|
||||||
assert_eq!(review.comment(), Some("LGTM"));
|
assert_eq!(review.summary(), Some("Whoops!"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_patch_review_remove_fields() {
|
fn test_patch_review_remove_summary() {
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
let ctx = test::setup::Context::new(&tmp);
|
let ctx = test::setup::Context::new(&tmp);
|
||||||
let signer = &ctx.signer;
|
let signer = &ctx.signer;
|
||||||
|
|
@ -1918,23 +1926,18 @@ mod test {
|
||||||
let rid = *rid;
|
let rid = *rid;
|
||||||
|
|
||||||
patch
|
patch
|
||||||
.review(
|
.review(rid, None, Some("Nah".to_owned()), signer)
|
||||||
rid,
|
.unwrap();
|
||||||
Some(Verdict::Reject),
|
patch
|
||||||
Some("Nah".to_owned()),
|
.edit_review(rid, *signer.public_key(), None, signer)
|
||||||
vec![],
|
|
||||||
signer,
|
|
||||||
)
|
|
||||||
.unwrap();
|
.unwrap();
|
||||||
patch.review(rid, None, None, vec![], signer).unwrap();
|
|
||||||
|
|
||||||
let id = patch.id;
|
let id = patch.id;
|
||||||
let patch = patches.get_mut(&id).unwrap();
|
let patch = patches.get_mut(&id).unwrap();
|
||||||
let (_, revision) = patch.latest();
|
let (_, revision) = patch.latest();
|
||||||
|
let review = revision.review(signer.public_key()).unwrap();
|
||||||
|
|
||||||
let review = revision.reviews.get(signer.public_key()).unwrap();
|
assert_eq!(review.summary(), None);
|
||||||
assert_eq!(review.verdict(), None);
|
|
||||||
assert_eq!(review.comment(), None);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue