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 once_cell::sync::Lazy;
|
||||||
use radicle_crdt as crdt;
|
use radicle_crdt as crdt;
|
||||||
use radicle_crdt::clock;
|
use radicle_crdt::clock;
|
||||||
use radicle_crdt::{ChangeId, LWWReg, Max, Semilattice};
|
use radicle_crdt::{ChangeId, LWWReg, LWWSet, Max, Semilattice};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
|
@ -76,6 +76,7 @@ pub struct Issue {
|
||||||
// TODO(cloudhead): Title should bias towards shorter strings.
|
// TODO(cloudhead): Title should bias towards shorter strings.
|
||||||
title: LWWReg<Max<String>, clock::Lamport>,
|
title: LWWReg<Max<String>, clock::Lamport>,
|
||||||
status: LWWReg<Max<Status>, clock::Lamport>,
|
status: LWWReg<Max<Status>, clock::Lamport>,
|
||||||
|
tags: LWWSet<Tag>,
|
||||||
thread: Thread,
|
thread: Thread,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -92,6 +93,7 @@ impl Default for Issue {
|
||||||
Self {
|
Self {
|
||||||
title: Max::from(String::default()).into(),
|
title: Max::from(String::default()).into(),
|
||||||
status: Max::from(Status::default()).into(),
|
status: Max::from(Status::default()).into(),
|
||||||
|
tags: LWWSet::default(),
|
||||||
thread: Thread::default(),
|
thread: Thread::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -138,7 +140,7 @@ impl Issue {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tags(&self) -> impl Iterator<Item = &Tag> {
|
pub fn tags(&self) -> impl Iterator<Item = &Tag> {
|
||||||
self.thread.tags()
|
self.tags.iter()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn author(&self) -> Option<Author> {
|
pub fn author(&self) -> Option<Author> {
|
||||||
|
|
@ -164,6 +166,14 @@ impl Issue {
|
||||||
Action::Lifecycle { status } => {
|
Action::Lifecycle { status } => {
|
||||||
self.status.set(status, change.clock);
|
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 } => {
|
Action::Thread { action } => {
|
||||||
self.thread.apply([crdt::Change {
|
self.thread.apply([crdt::Change {
|
||||||
action,
|
action,
|
||||||
|
|
@ -221,13 +231,14 @@ impl<'a, 'g> IssueMut<'a, 'g> {
|
||||||
/// Tag an issue.
|
/// Tag an issue.
|
||||||
pub fn tag<G: Signer>(
|
pub fn tag<G: Signer>(
|
||||||
&mut self,
|
&mut self,
|
||||||
tags: impl IntoIterator<Item = Tag>,
|
add: impl IntoIterator<Item = Tag>,
|
||||||
|
remove: impl IntoIterator<Item = Tag>,
|
||||||
signer: &G,
|
signer: &G,
|
||||||
) -> Result<ChangeId, Error> {
|
) -> Result<ChangeId, Error> {
|
||||||
let tags = tags.into_iter().collect::<Vec<_>>();
|
let add = add.into_iter().collect::<Vec<_>>();
|
||||||
let action = Action::Thread {
|
let remove = remove.into_iter().collect::<Vec<_>>();
|
||||||
action: thread::Action::Tag { tags },
|
let action = Action::Tag { add, remove };
|
||||||
};
|
|
||||||
self.apply("Tag", action, signer)
|
self.apply("Tag", action, signer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -362,7 +373,7 @@ impl<'a> Issues<'a> {
|
||||||
};
|
};
|
||||||
|
|
||||||
issue.comment(description, signer)?;
|
issue.comment(description, signer)?;
|
||||||
issue.tag(tags.to_owned(), signer)?;
|
issue.tag(tags.to_owned(), [], signer)?;
|
||||||
|
|
||||||
Ok(issue)
|
Ok(issue)
|
||||||
}
|
}
|
||||||
|
|
@ -378,6 +389,7 @@ impl<'a> Issues<'a> {
|
||||||
pub enum Action {
|
pub enum Action {
|
||||||
Title { title: String },
|
Title { title: String },
|
||||||
Lifecycle { status: Status },
|
Lifecycle { status: Status },
|
||||||
|
Tag { add: Vec<Tag>, remove: Vec<Tag> },
|
||||||
Thread { action: thread::Action },
|
Thread { action: thread::Action },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -519,8 +531,8 @@ mod test {
|
||||||
let bug_tag = Tag::new("bug").unwrap();
|
let bug_tag = Tag::new("bug").unwrap();
|
||||||
let wontfix_tag = Tag::new("wontfix").unwrap();
|
let wontfix_tag = Tag::new("wontfix").unwrap();
|
||||||
|
|
||||||
issue.tag([bug_tag.clone()], &signer).unwrap();
|
issue.tag([bug_tag.clone()], [], &signer).unwrap();
|
||||||
issue.tag([wontfix_tag.clone()], &signer).unwrap();
|
issue.tag([wontfix_tag.clone()], [], &signer).unwrap();
|
||||||
|
|
||||||
let id = issue.id;
|
let id = issue.id;
|
||||||
let issue = issues.get(&id).unwrap().unwrap();
|
let issue = issues.get(&id).unwrap().unwrap();
|
||||||
|
|
|
||||||
|
|
@ -823,7 +823,7 @@ mod test {
|
||||||
|
|
||||||
impl<const N: usize> Arbitrary for Changes<N> {
|
impl<const N: usize> Arbitrary for Changes<N> {
|
||||||
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
|
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 author = ActorId::from([0; 32]);
|
||||||
let rng = fastrand::Rng::with_seed(u64::arbitrary(g));
|
let rng = fastrand::Rng::with_seed(u64::arbitrary(g));
|
||||||
|
|
@ -840,7 +840,7 @@ mod test {
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let gen = WeightedGenerator::<(clock::Lamport, Action), State>::new(rng.clone())
|
let gen = WeightedGenerator::<(clock::Lamport, Action), State>::new(rng.clone())
|
||||||
.variant(1, |(clock, _), rng| {
|
.variant(1, |(clock, _, _), rng| {
|
||||||
Some((
|
Some((
|
||||||
clock.tick(),
|
clock.tick(),
|
||||||
Action::Edit {
|
Action::Edit {
|
||||||
|
|
@ -850,7 +850,7 @@ mod test {
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
.variant(1, |(clock, revisions), rng| {
|
.variant(1, |(clock, revisions, _), rng| {
|
||||||
if revisions.is_empty() {
|
if revisions.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
@ -859,7 +859,22 @@ mod test {
|
||||||
|
|
||||||
Some((clock.tick(), Action::Merge { revision, commit }))
|
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 oid = oids[rng.usize(..oids.len())];
|
||||||
let base = 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 radicle_crdt as crdt;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::cob::common::{Reaction, Tag, Timestamp};
|
use crate::cob::common::{Reaction, Timestamp};
|
||||||
use crate::cob::store;
|
use crate::cob::store;
|
||||||
use crate::cob::{History, TypeName};
|
use crate::cob::{History, TypeName};
|
||||||
use crate::crypto::Signer;
|
use crate::crypto::Signer;
|
||||||
|
|
||||||
use crdt::clock::Lamport;
|
use crdt::clock::Lamport;
|
||||||
use crdt::lwwreg::LWWReg;
|
|
||||||
use crdt::lwwset::LWWSet;
|
use crdt::lwwset::LWWSet;
|
||||||
use crdt::redactable::Redactable;
|
use crdt::redactable::Redactable;
|
||||||
use crdt::{ActorId, Change, ChangeId, Semilattice};
|
use crdt::{ActorId, Change, ChangeId, Semilattice};
|
||||||
|
|
@ -70,10 +69,6 @@ pub enum Action {
|
||||||
},
|
},
|
||||||
/// Redact a change. Not all changes can be redacted.
|
/// Redact a change. Not all changes can be redacted.
|
||||||
Redact { id: ChangeId },
|
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 a change.
|
||||||
React {
|
React {
|
||||||
to: ChangeId,
|
to: ChangeId,
|
||||||
|
|
@ -94,8 +89,6 @@ impl Action {
|
||||||
pub struct Thread {
|
pub struct Thread {
|
||||||
/// The comments under the thread.
|
/// The comments under the thread.
|
||||||
comments: BTreeMap<CommentId, Redactable<Comment>>,
|
comments: BTreeMap<CommentId, Redactable<Comment>>,
|
||||||
/// Associated tags.
|
|
||||||
tags: BTreeMap<Tag, LWWReg<bool, Lamport>>,
|
|
||||||
/// Reactions to changes.
|
/// Reactions to changes.
|
||||||
reactions: BTreeMap<CommentId, LWWSet<(ActorId, Reaction), Lamport>>,
|
reactions: BTreeMap<CommentId, LWWSet<(ActorId, Reaction), Lamport>>,
|
||||||
}
|
}
|
||||||
|
|
@ -128,7 +121,6 @@ impl store::FromHistory for Thread {
|
||||||
impl Semilattice for Thread {
|
impl Semilattice for Thread {
|
||||||
fn merge(&mut self, other: Self) {
|
fn merge(&mut self, other: Self) {
|
||||||
self.comments.merge(other.comments);
|
self.comments.merge(other.comments);
|
||||||
self.tags.merge(other.tags);
|
|
||||||
self.reactions.merge(other.reactions);
|
self.reactions.merge(other.reactions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -211,22 +203,6 @@ impl Thread {
|
||||||
.and_modify(|e| e.merge(Redactable::Redacted))
|
.and_modify(|e| e.merge(Redactable::Redacted))
|
||||||
.or_insert(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 {
|
Action::React {
|
||||||
to,
|
to,
|
||||||
reaction,
|
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.
|
/// 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.
|
/// Create a new redaction.
|
||||||
pub fn redact(&mut self, id: ChangeId) -> Change<Action> {
|
pub fn redact(&mut self, id: ChangeId) -> Change<Action> {
|
||||||
self.change(Action::Redact { id })
|
self.change(Action::Redact { id })
|
||||||
|
|
@ -371,10 +331,8 @@ mod tests {
|
||||||
let author = ActorId::from([0; 32]);
|
let author = ActorId::from([0; 32]);
|
||||||
let rng = fastrand::Rng::with_seed(u64::arbitrary(g));
|
let rng = fastrand::Rng::with_seed(u64::arbitrary(g));
|
||||||
let gen =
|
let gen =
|
||||||
WeightedGenerator::<(Lamport, Action), (Lamport, Vec<Tag>, Vec<ChangeId>)>::new(
|
WeightedGenerator::<(Lamport, Action), (Lamport, Vec<ChangeId>)>::new(rng.clone())
|
||||||
rng.clone(),
|
.variant(3, |(clock, changes), rng| {
|
||||||
)
|
|
||||||
.variant(3, |(clock, _, changes), rng| {
|
|
||||||
changes.push((clock.tick(), author));
|
changes.push((clock.tick(), author));
|
||||||
|
|
||||||
Some((
|
Some((
|
||||||
|
|
@ -385,7 +343,7 @@ mod tests {
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
.variant(2, |(clock, _, changes), rng| {
|
.variant(2, |(clock, changes), rng| {
|
||||||
if changes.is_empty() {
|
if changes.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
@ -400,37 +358,13 @@ mod tests {
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
.variant(2, |(clock, _, changes), rng| {
|
.variant(2, |(clock, changes), rng| {
|
||||||
if changes.is_empty() {
|
if changes.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let id = changes[rng.usize(..changes.len())];
|
let id = changes[rng.usize(..changes.len())];
|
||||||
|
|
||||||
Some((clock.tick(), Action::Redact { id }))
|
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] }))
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut changes = Vec::new();
|
let mut changes = Vec::new();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue