cob: Move tags out of `Thread`
Signed-off-by: Alexis Sellier <alexis@radicle.xyz>
This commit is contained in:
parent
64ce15dbe2
commit
ce11d69f23
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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())];
|
||||
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
Loading…
Reference in New Issue