cob: Move tags out of `Thread`

Signed-off-by: Alexis Sellier <alexis@radicle.xyz>
This commit is contained in:
Alexis Sellier 2022-12-01 13:21:22 +01:00
parent 64ce15dbe2
commit ce11d69f23
No known key found for this signature in database
3 changed files with 74 additions and 113 deletions

View File

@ -4,7 +4,7 @@ use std::str::FromStr;
use once_cell::sync::Lazy;
use radicle_crdt as crdt;
use radicle_crdt::clock;
use radicle_crdt::{ChangeId, LWWReg, Max, Semilattice};
use radicle_crdt::{ChangeId, LWWReg, LWWSet, Max, Semilattice};
use serde::{Deserialize, Serialize};
use thiserror::Error;
@ -76,6 +76,7 @@ pub struct Issue {
// TODO(cloudhead): Title should bias towards shorter strings.
title: LWWReg<Max<String>, clock::Lamport>,
status: LWWReg<Max<Status>, clock::Lamport>,
tags: LWWSet<Tag>,
thread: Thread,
}
@ -92,6 +93,7 @@ impl Default for Issue {
Self {
title: Max::from(String::default()).into(),
status: Max::from(Status::default()).into(),
tags: LWWSet::default(),
thread: Thread::default(),
}
}
@ -138,7 +140,7 @@ impl Issue {
}
pub fn tags(&self) -> impl Iterator<Item = &Tag> {
self.thread.tags()
self.tags.iter()
}
pub fn author(&self) -> Option<Author> {
@ -164,6 +166,14 @@ impl Issue {
Action::Lifecycle { status } => {
self.status.set(status, change.clock);
}
Action::Tag { add, remove } => {
for tag in add {
self.tags.insert(tag, change.clock);
}
for tag in remove {
self.tags.remove(tag, change.clock);
}
}
Action::Thread { action } => {
self.thread.apply([crdt::Change {
action,
@ -221,13 +231,14 @@ impl<'a, 'g> IssueMut<'a, 'g> {
/// Tag an issue.
pub fn tag<G: Signer>(
&mut self,
tags: impl IntoIterator<Item = Tag>,
add: impl IntoIterator<Item = Tag>,
remove: impl IntoIterator<Item = Tag>,
signer: &G,
) -> Result<ChangeId, Error> {
let tags = tags.into_iter().collect::<Vec<_>>();
let action = Action::Thread {
action: thread::Action::Tag { tags },
};
let add = add.into_iter().collect::<Vec<_>>();
let remove = remove.into_iter().collect::<Vec<_>>();
let action = Action::Tag { add, remove };
self.apply("Tag", action, signer)
}
@ -362,7 +373,7 @@ impl<'a> Issues<'a> {
};
issue.comment(description, signer)?;
issue.tag(tags.to_owned(), signer)?;
issue.tag(tags.to_owned(), [], signer)?;
Ok(issue)
}
@ -378,6 +389,7 @@ impl<'a> Issues<'a> {
pub enum Action {
Title { title: String },
Lifecycle { status: Status },
Tag { add: Vec<Tag>, remove: Vec<Tag> },
Thread { action: thread::Action },
}
@ -519,8 +531,8 @@ mod test {
let bug_tag = Tag::new("bug").unwrap();
let wontfix_tag = Tag::new("wontfix").unwrap();
issue.tag([bug_tag.clone()], &signer).unwrap();
issue.tag([wontfix_tag.clone()], &signer).unwrap();
issue.tag([bug_tag.clone()], [], &signer).unwrap();
issue.tag([wontfix_tag.clone()], [], &signer).unwrap();
let id = issue.id;
let issue = issues.get(&id).unwrap().unwrap();

View File

@ -823,7 +823,7 @@ mod test {
impl<const N: usize> Arbitrary for Changes<N> {
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
type State = (clock::Lamport, Vec<ChangeId>);
type State = (clock::Lamport, Vec<ChangeId>, Vec<Tag>);
let author = ActorId::from([0; 32]);
let rng = fastrand::Rng::with_seed(u64::arbitrary(g));
@ -840,7 +840,7 @@ mod test {
.collect::<Vec<_>>();
let gen = WeightedGenerator::<(clock::Lamport, Action), State>::new(rng.clone())
.variant(1, |(clock, _), rng| {
.variant(1, |(clock, _, _), rng| {
Some((
clock.tick(),
Action::Edit {
@ -850,7 +850,7 @@ mod test {
},
))
})
.variant(1, |(clock, revisions), rng| {
.variant(1, |(clock, revisions, _), rng| {
if revisions.is_empty() {
return None;
}
@ -859,7 +859,22 @@ mod test {
Some((clock.tick(), Action::Merge { revision, commit }))
})
.variant(1, |(clock, revisions), rng| {
.variant(1, |(clock, _, tags), rng| {
let add = iter::repeat_with(|| rng.alphabetic())
.take(rng.usize(0..=3))
.map(|c| Tag::new(c).unwrap())
.collect::<Vec<_>>();
let remove = tags
.iter()
.take(rng.usize(0..=tags.len()))
.cloned()
.collect();
for tag in &add {
tags.push(tag.clone());
}
Some((clock.tick(), Action::Tag { add, remove }))
})
.variant(1, |(clock, revisions, _), rng| {
let oid = oids[rng.usize(..oids.len())];
let base = oids[rng.usize(..oids.len())];

View File

@ -8,13 +8,12 @@ use once_cell::sync::Lazy;
use radicle_crdt as crdt;
use serde::{Deserialize, Serialize};
use crate::cob::common::{Reaction, Tag, Timestamp};
use crate::cob::common::{Reaction, Timestamp};
use crate::cob::store;
use crate::cob::{History, TypeName};
use crate::crypto::Signer;
use crdt::clock::Lamport;
use crdt::lwwreg::LWWReg;
use crdt::lwwset::LWWSet;
use crdt::redactable::Redactable;
use crdt::{ActorId, Change, ChangeId, Semilattice};
@ -70,10 +69,6 @@ pub enum Action {
},
/// Redact a change. Not all changes can be redacted.
Redact { id: ChangeId },
/// Add tags to the thread.
Tag { tags: Vec<Tag> },
/// Remove tags from the thread.
Untag { tags: Vec<Tag> },
/// React to a change.
React {
to: ChangeId,
@ -94,8 +89,6 @@ impl Action {
pub struct Thread {
/// The comments under the thread.
comments: BTreeMap<CommentId, Redactable<Comment>>,
/// Associated tags.
tags: BTreeMap<Tag, LWWReg<bool, Lamport>>,
/// Reactions to changes.
reactions: BTreeMap<CommentId, LWWSet<(ActorId, Reaction), Lamport>>,
}
@ -128,7 +121,6 @@ impl store::FromHistory for Thread {
impl Semilattice for Thread {
fn merge(&mut self, other: Self) {
self.comments.merge(other.comments);
self.tags.merge(other.tags);
self.reactions.merge(other.reactions);
}
}
@ -211,22 +203,6 @@ impl Thread {
.and_modify(|e| e.merge(Redactable::Redacted))
.or_insert(Redactable::Redacted);
}
Action::Tag { tags } => {
for tag in tags {
self.tags
.entry(tag)
.and_modify(|r| r.set(true, change.clock))
.or_insert_with(|| LWWReg::new(true, change.clock));
}
}
Action::Untag { tags } => {
for tag in tags {
self.tags
.entry(tag)
.and_modify(|r| r.set(false, change.clock))
.or_insert_with(|| LWWReg::new(false, change.clock));
}
}
Action::React {
to,
reaction,
@ -264,12 +240,6 @@ impl Thread {
}
})
}
pub fn tags(&self) -> impl Iterator<Item = &Tag> + '_ {
self.tags
.iter()
.filter_map(|(tag, r)| if *r.get() { Some(tag) } else { None })
}
}
/// An object that can be used to create and sign changes.
@ -305,16 +275,6 @@ impl<G: Signer> Actor<G> {
})
}
/// Add a tag.
pub fn tag(&mut self, tag: Tag) -> Change<Action> {
self.change(Action::Tag { tags: vec![tag] })
}
/// Remove a tag.
pub fn untag(&mut self, tag: Tag) -> Change<Action> {
self.change(Action::Untag { tags: vec![tag] })
}
/// Create a new redaction.
pub fn redact(&mut self, id: ChangeId) -> Change<Action> {
self.change(Action::Redact { id })
@ -371,67 +331,41 @@ mod tests {
let author = ActorId::from([0; 32]);
let rng = fastrand::Rng::with_seed(u64::arbitrary(g));
let gen =
WeightedGenerator::<(Lamport, Action), (Lamport, Vec<Tag>, Vec<ChangeId>)>::new(
rng.clone(),
)
.variant(3, |(clock, _, changes), rng| {
changes.push((clock.tick(), author));
WeightedGenerator::<(Lamport, Action), (Lamport, Vec<ChangeId>)>::new(rng.clone())
.variant(3, |(clock, changes), rng| {
changes.push((clock.tick(), author));
Some((
*clock,
Action::Comment {
body: iter::repeat_with(|| rng.alphabetic()).take(16).collect(),
reply_to: None,
},
))
})
.variant(2, |(clock, _, changes), rng| {
if changes.is_empty() {
return None;
}
let to = changes[rng.usize(..changes.len())];
Some((
*clock,
Action::Comment {
body: iter::repeat_with(|| rng.alphabetic()).take(16).collect(),
reply_to: None,
},
))
})
.variant(2, |(clock, changes), rng| {
if changes.is_empty() {
return None;
}
let to = changes[rng.usize(..changes.len())];
Some((
clock.tick(),
Action::React {
to,
reaction: Reaction::new('✨').unwrap(),
active: rng.bool(),
},
))
})
.variant(2, |(clock, _, changes), rng| {
if changes.is_empty() {
return None;
}
let id = changes[rng.usize(..changes.len())];
Some((
clock.tick(),
Action::React {
to,
reaction: Reaction::new('✨').unwrap(),
active: rng.bool(),
},
))
})
.variant(2, |(clock, changes), rng| {
if changes.is_empty() {
return None;
}
let id = changes[rng.usize(..changes.len())];
Some((clock.tick(), Action::Redact { id }))
})
.variant(2, |(clock, tags, _), rng| {
let tag = if tags.is_empty() || rng.bool() {
let tag = iter::repeat_with(|| rng.alphabetic())
.take(8)
.collect::<String>();
let tag = Tag::new(tag).unwrap();
tags.push(tag.clone());
tag
} else {
tags[rng.usize(..tags.len())].clone()
};
Some((clock.tick(), Action::Tag { tags: vec![tag] }))
})
.variant(2, |(clock, tags, _), rng| {
if tags.is_empty() {
return None;
}
let tag = tags[rng.usize(..tags.len())].clone();
clock.tick();
Some((clock.tick(), Action::Untag { tags: vec![tag] }))
});
Some((clock.tick(), Action::Redact { id }))
});
let mut changes = Vec::new();
let mut permutations: [Vec<Change<Action>>; N] = array::from_fn(|_| Vec::new());