cob: Implement review code commenting
This commit is contained in:
parent
45e87a63b9
commit
11b32725c8
|
|
@ -644,12 +644,11 @@ async fn patch_update_handler(
|
||||||
} => {
|
} => {
|
||||||
patch.edit_revision(revision, description, &signer)?;
|
patch.edit_revision(revision, description, &signer)?;
|
||||||
}
|
}
|
||||||
patch::Action::EditReview {
|
patch::Action::EditReview { review, summary } => {
|
||||||
revision,
|
patch.edit_review(review, summary, &signer)?;
|
||||||
author,
|
}
|
||||||
summary,
|
patch::Action::EditCodeComment { .. } => {
|
||||||
} => {
|
todo!()
|
||||||
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)?;
|
||||||
|
|
|
||||||
|
|
@ -64,9 +64,6 @@ 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),
|
||||||
|
|
@ -103,10 +100,14 @@ pub enum Action {
|
||||||
description: String,
|
description: String,
|
||||||
},
|
},
|
||||||
EditReview {
|
EditReview {
|
||||||
revision: RevisionId,
|
review: EntryId,
|
||||||
author: ActorId,
|
|
||||||
summary: Option<String>,
|
summary: Option<String>,
|
||||||
},
|
},
|
||||||
|
EditCodeComment {
|
||||||
|
review: EntryId,
|
||||||
|
comment: EntryId,
|
||||||
|
body: String,
|
||||||
|
},
|
||||||
Tag {
|
Tag {
|
||||||
add: Vec<Tag>,
|
add: Vec<Tag>,
|
||||||
remove: Vec<Tag>,
|
remove: Vec<Tag>,
|
||||||
|
|
@ -128,9 +129,8 @@ pub enum Action {
|
||||||
verdict: Option<Verdict>,
|
verdict: Option<Verdict>,
|
||||||
},
|
},
|
||||||
CodeComment {
|
CodeComment {
|
||||||
revision: RevisionId,
|
review: EntryId,
|
||||||
author: ActorId,
|
body: String,
|
||||||
comment: String,
|
|
||||||
location: CodeLocation,
|
location: CodeLocation,
|
||||||
},
|
},
|
||||||
Merge {
|
Merge {
|
||||||
|
|
@ -200,6 +200,8 @@ pub struct Patch {
|
||||||
reviewers: LWWSet<ActorId>,
|
reviewers: LWWSet<ActorId>,
|
||||||
/// Timeline of operations.
|
/// Timeline of operations.
|
||||||
timeline: GSet<(Lamport, EntryId)>,
|
timeline: GSet<(Lamport, EntryId)>,
|
||||||
|
/// Reviews index. Keeps track of reviews for better performance.
|
||||||
|
reviews: GMap<EntryId, Redactable<(EntryId, ActorId)>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Semilattice for Patch {
|
impl Semilattice for Patch {
|
||||||
|
|
@ -212,6 +214,7 @@ impl Semilattice for Patch {
|
||||||
self.revisions.merge(other.revisions);
|
self.revisions.merge(other.revisions);
|
||||||
self.reviewers.merge(other.reviewers);
|
self.reviewers.merge(other.reviewers);
|
||||||
self.timeline.merge(other.timeline);
|
self.timeline.merge(other.timeline);
|
||||||
|
self.reviews.merge(other.reviews);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -226,6 +229,7 @@ impl Default for Patch {
|
||||||
revisions: GMap::default(),
|
revisions: GMap::default(),
|
||||||
reviewers: LWWSet::default(),
|
reviewers: LWWSet::default(),
|
||||||
timeline: GSet::default(),
|
timeline: GSet::default(),
|
||||||
|
reviews: GMap::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -443,19 +447,19 @@ impl store::FromHistory for Patch {
|
||||||
return Err(Error::Missing(revision));
|
return Err(Error::Missing(revision));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Action::EditReview {
|
Action::EditReview { review, summary } => {
|
||||||
revision,
|
let Some(Redactable::Present((revision, author))) =
|
||||||
author,
|
self.reviews.get(&review) else {
|
||||||
summary,
|
return Err(Error::Missing(review));
|
||||||
} => {
|
};
|
||||||
let Some(revision) = self.revisions.get_mut(&revision) else {
|
let Some(rev) = self.revisions.get_mut(revision) else {
|
||||||
return Err(Error::Missing(revision));
|
return Err(Error::Missing(*revision));
|
||||||
};
|
};
|
||||||
// If the revision was redacted concurrently, there's nothing to do.
|
// If the revision was redacted concurrently, there's nothing to do.
|
||||||
// Likewise, if the review 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() {
|
if let Some(rev) = rev.get_mut() {
|
||||||
let Some(review) = revision.reviews.get_mut(&author) else {
|
let Some(review) = rev.reviews.get_mut(author) else {
|
||||||
return Err(Error::MissingActor(author));
|
return Err(Error::Missing(review));
|
||||||
};
|
};
|
||||||
if let Redactable::Present(review) = review {
|
if let Redactable::Present(review) = review {
|
||||||
review.summary.set(summary.map(Max::from), op.clock);
|
review.summary.set(summary.map(Max::from), op.clock);
|
||||||
|
|
@ -492,13 +496,13 @@ impl store::FromHistory for Patch {
|
||||||
ref summary,
|
ref summary,
|
||||||
verdict,
|
verdict,
|
||||||
} => {
|
} => {
|
||||||
let Some(revision) = self.revisions.get_mut(&revision) else {
|
let Some(rev) = self.revisions.get_mut(&revision) else {
|
||||||
return Err(Error::Missing(revision));
|
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
|
// Nb. Applying two reviews by the same author is not allowed and
|
||||||
// results in the review being redacted.
|
// results in the review being redacted.
|
||||||
revision.reviews.insert(
|
rev.reviews.insert(
|
||||||
op.author,
|
op.author,
|
||||||
Redactable::Present(Review::new(
|
Redactable::Present(Review::new(
|
||||||
verdict,
|
verdict,
|
||||||
|
|
@ -507,10 +511,66 @@ impl store::FromHistory for Patch {
|
||||||
op.clock,
|
op.clock,
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
|
// Update reviews index.
|
||||||
|
self.reviews
|
||||||
|
.insert(op.id, Redactable::Present((revision, op.author)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Action::CodeComment { .. } => {
|
Action::EditCodeComment {
|
||||||
// TODO: Not yet implemented.
|
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 } => {
|
Action::Merge { revision, commit } => {
|
||||||
let Some(rev) = self.revisions.get_mut(&revision) else {
|
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.
|
/// Comment author.
|
||||||
pub fn author(&self) -> ActorId {
|
pub fn author(&self) -> ActorId {
|
||||||
self.author
|
self.author
|
||||||
|
|
@ -948,15 +1014,10 @@ impl store::Transaction<Patch> {
|
||||||
|
|
||||||
pub fn edit_review(
|
pub fn edit_review(
|
||||||
&mut self,
|
&mut self,
|
||||||
revision: RevisionId,
|
review: EntryId,
|
||||||
author: ActorId,
|
|
||||||
summary: Option<String>,
|
summary: Option<String>,
|
||||||
) -> Result<(), store::Error> {
|
) -> Result<(), store::Error> {
|
||||||
self.push(Action::EditReview {
|
self.push(Action::EditReview { review, summary })
|
||||||
revision,
|
|
||||||
author,
|
|
||||||
summary,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Redact the revision.
|
/// 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.
|
/// Review a patch revision.
|
||||||
pub fn review(
|
pub fn review(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
|
@ -1119,14 +1194,11 @@ impl<'a, 'g> PatchMut<'a, 'g> {
|
||||||
/// Edit review.
|
/// Edit review.
|
||||||
pub fn edit_review<G: Signer>(
|
pub fn edit_review<G: Signer>(
|
||||||
&mut self,
|
&mut self,
|
||||||
revision: RevisionId,
|
review: EntryId,
|
||||||
author: ActorId,
|
|
||||||
summary: Option<String>,
|
summary: Option<String>,
|
||||||
signer: &G,
|
signer: &G,
|
||||||
) -> Result<EntryId, Error> {
|
) -> Result<EntryId, Error> {
|
||||||
self.transaction("Edit review", signer, |tx| {
|
self.transaction("Edit review", signer, |tx| tx.edit_review(review, summary))
|
||||||
tx.edit_review(revision, author, summary)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Redact a revision.
|
/// Redact a revision.
|
||||||
|
|
@ -1162,6 +1234,19 @@ impl<'a, 'g> PatchMut<'a, 'g> {
|
||||||
self.transaction("Comment", signer, |tx| tx.comment(revision, body, reply_to))
|
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.
|
/// Review a patch revision.
|
||||||
pub fn review<G: Signer>(
|
pub fn review<G: Signer>(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
|
@ -1885,16 +1970,11 @@ mod test {
|
||||||
let (rid, _) = patch.latest();
|
let (rid, _) = patch.latest();
|
||||||
let rid = *rid;
|
let rid = *rid;
|
||||||
|
|
||||||
patch
|
let review = patch
|
||||||
.review(rid, Some(Verdict::Accept), Some("LGTM".to_owned()), signer)
|
.review(rid, Some(Verdict::Accept), Some("LGTM".to_owned()), signer)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
patch
|
patch
|
||||||
.edit_review(
|
.edit_review(review, Some("Whoops!".to_owned()), signer)
|
||||||
rid,
|
|
||||||
*signer.public_key(),
|
|
||||||
Some("Whoops!".to_owned()),
|
|
||||||
signer,
|
|
||||||
)
|
|
||||||
.unwrap(); // Overwrite the comment.
|
.unwrap(); // Overwrite the comment.
|
||||||
//
|
//
|
||||||
let (_, revision) = patch.latest();
|
let (_, revision) = patch.latest();
|
||||||
|
|
@ -1903,6 +1983,50 @@ mod test {
|
||||||
assert_eq!(review.summary(), Some("Whoops!"));
|
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]
|
#[test]
|
||||||
fn test_patch_review_remove_summary() {
|
fn test_patch_review_remove_summary() {
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
|
@ -1924,13 +2048,10 @@ mod test {
|
||||||
|
|
||||||
let (rid, _) = patch.latest();
|
let (rid, _) = patch.latest();
|
||||||
let rid = *rid;
|
let rid = *rid;
|
||||||
|
let review = patch
|
||||||
patch
|
|
||||||
.review(rid, None, Some("Nah".to_owned()), signer)
|
.review(rid, None, Some("Nah".to_owned()), signer)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
patch
|
patch.edit_review(review, None, signer).unwrap();
|
||||||
.edit_review(rid, *signer.public_key(), None, signer)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let id = patch.id;
|
let id = patch.id;
|
||||||
let patch = patches.get_mut(&id).unwrap();
|
let patch = patches.get_mut(&id).unwrap();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue