cob: Implement review code commenting

This commit is contained in:
Alexis Sellier 2023-06-28 13:31:08 +02:00
parent 45e87a63b9
commit 11b32725c8
No known key found for this signature in database
2 changed files with 173 additions and 53 deletions

View File

@ -644,12 +644,11 @@ async fn patch_update_handler(
} => {
patch.edit_revision(revision, description, &signer)?;
}
patch::Action::EditReview {
revision,
author,
summary,
} => {
patch.edit_review(revision, author, summary, &signer)?;
patch::Action::EditReview { review, summary } => {
patch.edit_review(review, summary, &signer)?;
}
patch::Action::EditCodeComment { .. } => {
todo!()
}
patch::Action::Tag { add, remove } => {
patch.tag(add, remove, &signer)?;

View File

@ -64,9 +64,6 @@ pub enum Error {
/// that hasn't happened yet.
#[error("causal dependency {0:?} missing")]
Missing(EntryId),
/// Missing actor.
#[error("missing actor {0:?}")]
MissingActor(ActorId),
/// Error applying an op to the patch thread.
#[error("thread apply failed: {0}")]
Thread(#[from] thread::Error),
@ -103,10 +100,14 @@ pub enum Action {
description: String,
},
EditReview {
revision: RevisionId,
author: ActorId,
review: EntryId,
summary: Option<String>,
},
EditCodeComment {
review: EntryId,
comment: EntryId,
body: String,
},
Tag {
add: Vec<Tag>,
remove: Vec<Tag>,
@ -128,9 +129,8 @@ pub enum Action {
verdict: Option<Verdict>,
},
CodeComment {
revision: RevisionId,
author: ActorId,
comment: String,
review: EntryId,
body: String,
location: CodeLocation,
},
Merge {
@ -200,6 +200,8 @@ pub struct Patch {
reviewers: LWWSet<ActorId>,
/// Timeline of operations.
timeline: GSet<(Lamport, EntryId)>,
/// Reviews index. Keeps track of reviews for better performance.
reviews: GMap<EntryId, Redactable<(EntryId, ActorId)>>,
}
impl Semilattice for Patch {
@ -212,6 +214,7 @@ impl Semilattice for Patch {
self.revisions.merge(other.revisions);
self.reviewers.merge(other.reviewers);
self.timeline.merge(other.timeline);
self.reviews.merge(other.reviews);
}
}
@ -226,6 +229,7 @@ impl Default for Patch {
revisions: GMap::default(),
reviewers: LWWSet::default(),
timeline: GSet::default(),
reviews: GMap::default(),
}
}
}
@ -443,19 +447,19 @@ impl store::FromHistory for Patch {
return Err(Error::Missing(revision));
}
}
Action::EditReview {
revision,
author,
summary,
} => {
let Some(revision) = self.revisions.get_mut(&revision) else {
return Err(Error::Missing(revision));
Action::EditReview { review, summary } => {
let Some(Redactable::Present((revision, author))) =
self.reviews.get(&review) else {
return Err(Error::Missing(review));
};
let Some(rev) = 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 Some(rev) = rev.get_mut() {
let Some(review) = rev.reviews.get_mut(author) else {
return Err(Error::Missing(review));
};
if let Redactable::Present(review) = review {
review.summary.set(summary.map(Max::from), op.clock);
@ -492,13 +496,13 @@ impl store::FromHistory for Patch {
ref summary,
verdict,
} => {
let Some(revision) = self.revisions.get_mut(&revision) else {
let Some(rev) = self.revisions.get_mut(&revision) else {
return Err(Error::Missing(revision));
};
if let Some(revision) = revision.get_mut() {
if let Some(rev) = rev.get_mut() {
// Nb. Applying two reviews by the same author is not allowed and
// results in the review being redacted.
revision.reviews.insert(
rev.reviews.insert(
op.author,
Redactable::Present(Review::new(
verdict,
@ -507,10 +511,66 @@ impl store::FromHistory for Patch {
op.clock,
)),
);
// Update reviews index.
self.reviews
.insert(op.id, Redactable::Present((revision, op.author)));
}
}
Action::CodeComment { .. } => {
// TODO: Not yet implemented.
Action::EditCodeComment {
review,
comment,
body,
} => {
let Some(Redactable::Present((revision, author))) =
self.reviews.get(&review) else {
return Err(Error::Missing(review));
};
let Some(rev) = 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(rev) = rev.get_mut() {
let Some(review) = rev.reviews.get_mut(author) else {
return Err(Error::Missing(review));
};
if let Some(review) = review.get_mut() {
let Some(comment) = review.comments.get_mut(&comment) else {
return Err(Error::Missing(comment));
};
if let Some(comment) = comment.get_mut() {
comment.edit(op.clock, body, timestamp);
}
}
}
}
Action::CodeComment {
review,
body,
location,
} => {
let Some(Redactable::Present((revision, author))) =
self.reviews.get(&review) else {
return Err(Error::Missing(review));
};
let Some(rev) = 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(rev) = rev.get_mut() {
let Some(review) = rev.reviews.get_mut(author) else {
return Err(Error::Missing(review));
};
if let Redactable::Present(review) = review {
review.comments.insert(
id,
Redactable::Present(CodeComment::new(
op.author, body, location, timestamp,
)),
);
}
}
}
Action::Merge { revision, commit } => {
let Some(rev) = self.revisions.get_mut(&revision) else {
@ -836,6 +896,12 @@ impl CodeComment {
}
}
/// Add an edit.
pub fn edit(&mut self, clock: Lamport, body: String, timestamp: Timestamp) {
self.edits
.insert(clock, thread::Edit { body, timestamp }.into())
}
/// Comment author.
pub fn author(&self) -> ActorId {
self.author
@ -948,15 +1014,10 @@ impl store::Transaction<Patch> {
pub fn edit_review(
&mut self,
revision: RevisionId,
author: ActorId,
review: EntryId,
summary: Option<String>,
) -> Result<(), store::Error> {
self.push(Action::EditReview {
revision,
author,
summary,
})
self.push(Action::EditReview { review, summary })
}
/// Redact the revision.
@ -995,6 +1056,20 @@ impl store::Transaction<Patch> {
})
}
/// Comment on code.
pub fn code_comment<S: ToString>(
&mut self,
review: EntryId,
body: S,
location: CodeLocation,
) -> Result<(), store::Error> {
self.push(Action::CodeComment {
review,
body: body.to_string(),
location,
})
}
/// Review a patch revision.
pub fn review(
&mut self,
@ -1119,14 +1194,11 @@ impl<'a, 'g> PatchMut<'a, 'g> {
/// Edit review.
pub fn edit_review<G: Signer>(
&mut self,
revision: RevisionId,
author: ActorId,
review: EntryId,
summary: Option<String>,
signer: &G,
) -> Result<EntryId, Error> {
self.transaction("Edit review", signer, |tx| {
tx.edit_review(revision, author, summary)
})
self.transaction("Edit review", signer, |tx| tx.edit_review(review, summary))
}
/// Redact a revision.
@ -1162,6 +1234,19 @@ impl<'a, 'g> PatchMut<'a, 'g> {
self.transaction("Comment", signer, |tx| tx.comment(revision, body, reply_to))
}
/// Comment on a line of code as part of a review.
pub fn code_comment<G: Signer, S: ToString>(
&mut self,
review: EntryId,
body: S,
location: CodeLocation,
signer: &G,
) -> Result<EntryId, Error> {
self.transaction("Code comment", signer, |tx| {
tx.code_comment(review, body, location)
})
}
/// Review a patch revision.
pub fn review<G: Signer>(
&mut self,
@ -1885,16 +1970,11 @@ mod test {
let (rid, _) = patch.latest();
let rid = *rid;
patch
let review = patch
.review(rid, Some(Verdict::Accept), Some("LGTM".to_owned()), signer)
.unwrap();
patch
.edit_review(
rid,
*signer.public_key(),
Some("Whoops!".to_owned()),
signer,
)
.edit_review(review, Some("Whoops!".to_owned()), signer)
.unwrap(); // Overwrite the comment.
//
let (_, revision) = patch.latest();
@ -1903,6 +1983,50 @@ mod test {
assert_eq!(review.summary(), Some("Whoops!"));
}
#[test]
fn test_patch_review_comment() {
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;
let location = CodeLocation {
path: PathBuf::from_str("README.md").unwrap(),
old: None,
new: Some(5..8),
};
let review = patch.review(rid, None, None, signer).unwrap();
patch
.code_comment(
review,
"I like these lines of code",
location.clone(),
signer,
)
.unwrap();
let (_, revision) = patch.latest();
let review = revision.review(signer.public_key()).unwrap();
let (_, comment) = review.comments().next().unwrap();
assert_eq!(comment.body(), "I like these lines of code");
assert_eq!(comment.location(), &location);
}
#[test]
fn test_patch_review_remove_summary() {
let tmp = tempfile::tempdir().unwrap();
@ -1924,13 +2048,10 @@ mod test {
let (rid, _) = patch.latest();
let rid = *rid;
patch
let review = patch
.review(rid, None, Some("Nah".to_owned()), signer)
.unwrap();
patch
.edit_review(rid, *signer.public_key(), None, signer)
.unwrap();
patch.edit_review(review, None, signer).unwrap();
let id = patch.id;
let patch = patches.get_mut(&id).unwrap();