1use crate::entry::Entry;
4use crate::hlc::Hlc;
5use crate::id::ReplicaId;
6use crate::vv::{Dot, VersionVector};
7
8fn winner_key(e: &Entry) -> (bool, Hlc, ReplicaId, u64) {
11 (
12 !e.content.is_tombstone(),
13 e.hlc,
14 e.dot.replica,
15 e.dot.counter,
16 )
17}
18
19pub fn winner(versions: &[Entry]) -> &Entry {
21 versions
22 .iter()
23 .max_by_key(|e| winner_key(e))
24 .expect("a path state always has at least one version")
25}
26
27pub fn join(
30 local: Option<(&[Entry], &VersionVector)>,
31 incoming: &[Entry],
32 incoming_seen: &VersionVector,
33) -> Option<(Vec<Entry>, VersionVector)> {
34 if incoming.is_empty() && local.is_none_or(|(versions, _)| versions.is_empty()) {
38 tracing::error!("join of two empty sibling sets; keeping the local state");
39 return None;
40 }
41 let Some((local, local_seen)) = local else {
42 let mut versions = incoming.to_vec();
43 versions.sort_by_key(|e| e.dot);
44 return Some((versions, incoming_seen.clone()));
45 };
46
47 let mut versions: Vec<Entry> = local
48 .iter()
49 .filter(|e| !incoming_seen.covers_dot(&e.dot) || incoming.iter().any(|i| i.dot == e.dot))
50 .cloned()
51 .collect();
52 for e in incoming {
53 let keep = !local_seen.covers_dot(&e.dot) || local.iter().any(|l| l.dot == e.dot);
54 if keep && !versions.iter().any(|v| v.dot == e.dot) {
55 versions.push(e.clone());
56 }
57 }
58 versions.sort_by_key(|e| e.dot);
59
60 let mut seen = local_seen.clone();
61 seen.merge(incoming_seen);
62
63 if versions.is_empty() {
64 tracing::error!("join produced an empty sibling set; keeping the local state");
68 return None;
69 }
70 if versions.as_slice() == local && seen == *local_seen {
71 return None;
72 }
73 Some((versions, seen))
74}
75
76pub fn needs_copy(loser: &Entry, winner: &Entry) -> bool {
78 !loser.content.is_tombstone() && loser.content.hash() != winner.content.hash()
79}
80
81pub fn conflict_path(path: &str, loser: &Dot) -> String {
84 let (dir, name) = match path.rfind('/') {
85 Some(i) => (&path[..=i], &path[i + 1..]),
86 None => ("", path),
87 };
88 let tag = format!("conflict-{}-{}", loser.replica.display_id(), loser.counter);
89 match name.rfind('.') {
90 Some(i) if i > 0 => format!("{dir}{}.{tag}.{}", &name[..i], &name[i + 1..]),
91 _ => format!("{dir}{name}.{tag}"),
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98 use crate::entry::Content;
99 use crate::hash::ContentHash;
100 use crate::hlc::Hlc;
101 use crate::id::ReplicaId;
102 use grain_id::GrainId;
103 use uuid::Uuid;
104
105 fn rid(n: u128) -> ReplicaId {
106 ReplicaId(Uuid::from_u128(n))
107 }
108
109 fn dot(r: u128, c: u64) -> Dot {
110 Dot {
111 replica: rid(r),
112 counter: c,
113 }
114 }
115
116 fn vv(dots: &[Dot]) -> VersionVector {
117 let mut v = VersionVector::new();
118 for d in dots {
119 v.add_dot(d);
120 }
121 v
122 }
123
124 fn file(d: Dot, wall: u64, body: &str, ctx: &[Dot]) -> Entry {
125 Entry {
126 path: "a.txt".into(),
127 content: Content::File {
128 hash: ContentHash::of_bytes(body.as_bytes()),
129 len: body.len() as u64,
130 },
131 hlc: Hlc {
132 wall_ms: wall,
133 logical: 0,
134 },
135 dot: d,
136 context: vv(ctx),
137 author: GrainId::NIL,
138 }
139 }
140
141 fn tomb(d: Dot, wall: u64, ctx: &[Dot]) -> Entry {
142 Entry {
143 content: Content::Tombstone,
144 ..file(d, wall, "", ctx)
145 }
146 }
147
148 fn written(e: Entry) -> (Vec<Entry>, VersionVector) {
150 let mut seen = e.context.clone();
151 seen.add_dot(&e.dot);
152 (vec![e], seen)
153 }
154
155 fn j(
156 a: &(Vec<Entry>, VersionVector),
157 b: &(Vec<Entry>, VersionVector),
158 ) -> (Vec<Entry>, VersionVector) {
159 join(Some((&a.0, &a.1)), &b.0, &b.1).unwrap_or_else(|| a.clone())
160 }
161
162 #[test]
163 fn into_nothing_takes_the_incoming_state() {
164 let x = written(file(dot(1, 1), 10, "x", &[]));
165 assert_eq!(join(None, &x.0, &x.1), Some(x.clone()));
166 }
167
168 #[test]
169 fn a_descendant_replaces_its_ancestor() {
170 let x = written(file(dot(1, 1), 10, "x", &[]));
171 let y = written(file(dot(2, 1), 20, "y", &[dot(1, 1)]));
172 let got = j(&x, &y);
173 assert_eq!(got, y);
174 assert_eq!(join(Some((&got.0, &got.1)), &x.0, &x.1), None);
176 }
177
178 #[test]
179 fn concurrent_versions_become_siblings() {
180 let x = written(file(dot(1, 1), 10, "x", &[]));
181 let y = written(file(dot(2, 1), 20, "y", &[]));
182 let got = j(&x, &y);
183 assert_eq!(got.0.len(), 2);
184 assert_eq!(got.1, vv(&[dot(1, 1), dot(2, 1)]));
185 assert_eq!(winner(&got.0).dot, dot(2, 1), "later hlc wins");
186 }
187
188 #[test]
189 fn an_edit_beats_a_concurrent_delete_even_if_older() {
190 let x = written(file(dot(1, 1), 10, "x", &[]));
191 let d = written(tomb(dot(2, 1), 99, &[]));
192 let got = j(&x, &d);
193 assert_eq!(winner(&got.0).dot, dot(1, 1));
194 }
195
196 #[test]
197 fn join_is_idempotent() {
198 let x = written(file(dot(1, 1), 10, "x", &[]));
199 let y = written(file(dot(2, 1), 20, "y", &[]));
200 let s = j(&x, &y);
201 assert_eq!(join(Some((&s.0, &s.1)), &s.0, &s.1), None);
202 }
203
204 #[test]
207 fn join_is_associative_and_commutative_with_deletes() {
208 let a = written(file(dot(1, 1), 50, "x", &[]));
209 let b = written(tomb(dot(2, 1), 60, &[dot(1, 1)]));
210 let c = written(file(dot(3, 1), 40, "z", &[]));
211 let orders = [
212 j(&j(&a, &b), &c),
213 j(&a, &j(&b, &c)),
214 j(&j(&a, &c), &b),
215 j(&j(&c, &a), &b),
216 j(&j(&b, &c), &a),
217 ];
218 for got in &orders[1..] {
219 assert_eq!(got, &orders[0]);
220 }
221 let dots: Vec<Dot> = orders[0].0.iter().map(|e| e.dot).collect();
222 assert_eq!(
223 dots,
224 vec![dot(2, 1), dot(3, 1)],
225 "X is superseded; the delete and Z are siblings"
226 );
227 assert_eq!(winner(&orders[0].0).dot, dot(3, 1));
228 }
229
230 #[test]
233 fn an_empty_join_never_returns_an_empty_sibling_set() {
234 let seen = vv(&[dot(1, 1)]);
235 assert_eq!(join(None, &[], &seen), None);
236 assert_eq!(join(Some((&[], &seen)), &[], &seen), None);
237 let x = written(file(dot(1, 1), 10, "x", &[]));
238 for got in [
239 join(Some((&x.0, &x.1)), &[], &seen),
240 join(Some((&[], &seen)), &x.0, &x.1),
241 ]
242 .into_iter()
243 .flatten()
244 {
245 assert!(!got.0.is_empty());
246 }
247 }
248
249 #[test]
250 fn needs_copy_only_for_differing_live_losers() {
251 let w = file(dot(1, 1), 10, "same", &[]);
252 assert!(!needs_copy(&file(dot(2, 1), 5, "same", &[]), &w));
253 assert!(!needs_copy(&tomb(dot(2, 1), 5, &[]), &w));
254 assert!(needs_copy(&file(dot(2, 1), 5, "other", &[]), &w));
255 }
256
257 #[test]
258 fn conflict_path_naming() {
259 let d = Dot {
260 replica: ReplicaId(Uuid::from_u128(0x0190_0000_0000_7000_8000_00ff_ffff_ffff)),
261 counter: 42,
262 };
263 let g = GrainId::MAX.to_string();
264 assert_eq!(
265 conflict_path("note.md", &d),
266 format!("note.conflict-{g}-42.md")
267 );
268 assert_eq!(
269 conflict_path("dir/a.tar.gz", &d),
270 format!("dir/a.tar.conflict-{g}-42.gz")
271 );
272 assert_eq!(
273 conflict_path("dir/Makefile", &d),
274 format!("dir/Makefile.conflict-{g}-42")
275 );
276 assert_eq!(
277 conflict_path(".sapphireignore", &d),
278 format!(".sapphireignore.conflict-{g}-42")
279 );
280 }
281}