ff0af1ee unknown 1 month ago
1
2
3
4
5
6
7
use crate :: hash:: ChangeHash;
8
use crate :: objects:: AtomRange;
9
use std:: collections:: { BTreeMap, BTreeSet, HashMap, HashSet} ;
10
11
12
pub type AtomKey = ( ChangeHash, u32 ) ;
13
14
# [ derive ( Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug ) ]
15
pub enum Vertex {
16
Start,
17
Atom( AtomKey) ,
18
End,
19
}
20
21
22
pub trait DepOracle {
23
fn depends_on ( & self , a : & ChangeHash, b : & ChangeHash) -> bool ;
24
}
25
26
27
pub struct NoDeps ;
28
impl DepOracle for NoDeps {
29
fn depends_on ( & self , a : & ChangeHash, b : & ChangeHash) -> bool {
30
a = = b
31
}
32
}
33
34
# [ derive ( Default, Clone, Debug ) ]
35
struct AtomInfo {
36
37
content : Option< Vec< u8 > > ,
38
deleted_by : BTreeSet< ChangeHash> ,
39
}
40
41
# [ derive ( Clone, Debug ) ]
42
pub struct FileGraph {
43
atoms : HashMap< AtomKey, AtomInfo> ,
44
succ : HashMap< Vertex, BTreeSet< Vertex> > ,
45
pred : HashMap< Vertex, BTreeSet< Vertex> > ,
46
}
47
48
impl Default for FileGraph {
49
fn default ( ) -> Self {
50
Self :: new( )
51
}
52
}
53
54
55
# [ derive ( Clone, Debug, PartialEq, Eq ) ]
56
pub enum Segment {
57
Lines( Vec< AtomKey> ) ,
58
59
Conflict( Vec< Vec< AtomKey> > ) ,
60
61
62
Zombie { deleted: Vec< AtomKey> , inserted: Vec< AtomKey> } ,
63
}
64
65
# [ derive ( Clone, Debug, PartialEq, Eq, Default ) ]
66
pub struct Materialized {
67
pub segments : Vec< Segment> ,
68
}
69
70
impl Materialized {
71
pub fn is_conflicted ( & self ) -> bool {
72
self .segments.iter ( ) .any ( | s | ! matches! ( s, Segment:: Lines( _ ) ) )
73
}
74
pub fn conflict_count ( & self ) -> usize {
75
self .segments.iter ( ) .filter ( | s | ! matches! ( s, Segment:: Lines( _ ) ) ) .count ( )
76
}
77
78
pub fn live_atoms ( & self ) -> Vec< AtomKey> {
79
let mut out = Vec :: new( ) ;
80
for s in & self .segments {
81
match s {
82
Segment:: Lines( a) => out.extend ( a) ,
83
Segment:: Conflict( sides) => sides.iter ( ) .for_each ( | x | out.extend ( x) ) ,
84
Segment:: Zombie { inserted, .. } => out.extend ( inserted) ,
85
}
86
}
87
out
88
}
89
}
90
91
impl FileGraph {
92
pub fn new ( ) -> Self {
93
let mut g = FileGraph { atoms: HashMap:: new( ) , succ: HashMap:: new( ) , pred: HashMap:: new( ) } ;
94
g.edge ( Vertex:: Start, Vertex:: End) ;
95
g
96
}
97
98
fn edge ( & mut self , a : Vertex, b : Vertex) {
99
self .succ.entry ( a) .or_default ( ) .insert ( b) ;
100
self .pred.entry ( b) .or_default ( ) .insert ( a) ;
101
self .succ.entry ( b) .or_default ( ) ;
102
self .pred.entry ( a) .or_default ( ) ;
103
}
104
105
106
pub fn insert ( & mut self , change : ChangeHash, first_index : u32 , after : Vertex, before : Vertex, lines : & [Vec< u8 > ]) {
107
let mut prev = after;
108
for ( i, line) in lines.iter ( ) .enumerate ( ) {
109
let key = ( change, first_index + i as u32 ) ;
110
self .atoms.entry ( key) .or_default ( ) .content = Some ( line.clone ( ) ) ;
111
let v = Vertex:: Atom( key) ;
112
self .edge ( prev, v) ;
113
prev = v;
114
}
115
self .edge ( prev, before) ;
116
}
117
118
119
pub fn delete ( & mut self , by : ChangeHash, range : & AtomRange) {
120
for i in range.from.. range.to {
121
self .atoms.entry ( ( range.change, i) ) .or_default ( ) .deleted_by.insert ( by) ;
122
}
123
}
124
125
pub fn is_live ( & self , k : & AtomKey) -> bool {
126
self .atoms.get ( k) .is_some_and ( | a | a.content.is_some ( ) & & a.deleted_by.is_empty ( ) )
127
}
128
129
pub fn content ( & self , k : & AtomKey) -> Option< & [ u8 ] > {
130
self .atoms.get ( k) .and_then ( | a | a.content.as_deref ( ) )
131
}
132
133
pub fn deleters ( & self , k : & AtomKey) -> impl Iterator< Item = & ChangeHash> {
134
self .atoms.get ( k) .into_iter ( ) .flat_map ( | a | a.deleted_by.iter ( ) )
135
}
136
137
138
pub fn atom_count ( & self ) -> usize {
139
self .atoms.len ( )
140
}
141
142
fn is_ghost ( & self , k : & AtomKey, info : & AtomInfo, deps : & dyn DepOracle) -> bool {
143
144
145
self .succ.get ( & Vertex:: Atom( * k) ) .is_some_and ( | ss | {
146
ss.iter ( ) .any ( | s | match s {
147
Vertex:: Atom( y) if self .is_live ( y) => ! info.deleted_by.iter ( ) .any ( | d | deps.depends_on ( d, & y.0 ) ) ,
148
_ => false ,
149
} )
150
} )
151
}
152
153
154
fn reduced_succ ( & self , v : Vertex, kept : & HashSet< Vertex> , memo : & mut HashMap< Vertex, BTreeSet< Vertex> > ) -> BTreeSet< Vertex> {
155
let mut out = BTreeSet:: new( ) ;
156
let mut stack: Vec< Vertex> = self .succ.get ( & v) .map ( | s | s.iter ( ) .copied ( ) .collect ( ) ) .unwrap_or_default ( ) ;
157
let mut seen = HashSet:: new( ) ;
158
while let Some ( s) = stack.pop ( ) {
159
if kept.contains ( & s) {
160
out.insert ( s) ;
161
} else if seen.insert ( s) {
162
if let Some ( m) = memo.get ( & s) {
163
out.extend ( m.iter ( ) .copied ( ) ) ;
164
} else if let Some ( next) = self .succ.get ( & s) {
165
stack.extend ( next.iter ( ) .copied ( ) ) ;
166
}
167
}
168
}
169
out
170
}
171
172
173
pub fn materialize ( & self , deps : & dyn DepOracle) -> Materialized {
174
175
let mut kept: HashSet< Vertex> = HashSet:: new( ) ;
176
let mut ghosts: HashSet< AtomKey> = HashSet:: new( ) ;
177
kept.insert ( Vertex:: Start) ;
178
kept.insert ( Vertex:: End) ;
179
for ( k, info) in & self .atoms {
180
if info.content.is_none ( ) {
181
continue ;
182
}
183
if info.deleted_by.is_empty ( ) {
184
kept.insert ( Vertex:: Atom( * k) ) ;
185
} else if self .is_ghost ( k, info, deps) {
186
kept.insert ( Vertex:: Atom( * k) ) ;
187
ghosts.insert ( * k) ;
188
}
189
}
190
191
192
let mut memo = HashMap:: new( ) ;
193
let mut rsucc: HashMap< Vertex, BTreeSet< Vertex> > = HashMap:: new( ) ;
194
let mut indeg: HashMap< Vertex, usize > = HashMap:: new( ) ;
195
for & u in & kept {
196
indeg.entry ( u) .or_insert ( 0 ) ;
197
let rs = self .reduced_succ ( u, & kept, & mut memo) ;
198
for & w in & rs {
199
* indeg.entry ( w) .or_insert ( 0 ) + = 1 ;
200
}
201
rsucc.insert ( u, rs) ;
202
}
203
204
205
let mut ready: BTreeSet< Vertex> = indeg.iter ( ) .filter ( | ( _, &d ) | d = = 0 ) .map ( | ( v , _) | * v) .collect ( ) ;
206
let mut side: HashMap< Vertex, u32 > = HashMap:: new( ) ;
207
let mut joins: HashSet< Vertex> = HashSet:: new( ) ;
208
let mut next_side = 0 u32 ;
209
let mut plain: Vec< Vertex> = Vec :: new( ) ;
210
let mut conflict: Option< BTreeMap< u32 , Vec< Vertex> > > = None ;
211
let mut segments: Vec< Segment> = Vec :: new( ) ;
212
213
let flush_plain = | plain : & mut Vec< Vertex> , segments : & mut Vec< Segment> | {
214
let atoms: Vec< AtomKey> = plain.iter ( ) .filter_map ( | v | if let Vertex:: Atom( k) = v { Some ( * k) } else { None } ) .collect ( ) ;
215
plain.clear ( ) ;
216
if ! atoms.is_empty ( ) {
217
segments.extend ( split_zombies ( atoms, & ghosts) ) ;
218
}
219
} ;
220
let finish_conflict = | c : BTreeMap< u32 , Vec< Vertex> > , segments : & mut Vec< Segment> | {
221
let sides: Vec< Vec< AtomKey> > = c
222
.into_values ( )
223
.map ( | vs | {
224
vs.into_iter ( )
225
.filter_map ( | v | match v {
226
Vertex:: Atom( k) if ! ghosts.contains ( & k) => Some ( k) ,
227
_ => None ,
228
} )
229
.collect:: < Vec< _ > > ( )
230
} )
231
.filter ( | s | ! s.is_empty ( ) )
232
.collect ( ) ;
233
match sides.len ( ) {
234
0 => { }
235
1 => segments.push ( Segment:: Lines( sides.into_iter ( ) .next ( ) .unwrap ( ) ) ) ,
236
_ => segments.push ( Segment:: Conflict( sides) ) ,
237
}
238
} ;
239
240
while ! ready.is_empty ( ) {
241
if ready.len ( ) > = 2 {
242
if conflict.is_none ( ) {
243
flush_plain ( & mut plain, & mut segments) ;
244
conflict = Some ( BTreeMap:: new( ) ) ;
245
}
246
for r in & ready {
247
if ! side.contains_key ( r) {
248
side.insert ( * r, next_side) ;
249
next_side + = 1 ;
250
}
251
}
252
}
253
let v = ready.pop_first ( ) .unwrap ( ) ;
254
let my_side = side.get ( & v) .copied ( ) ;
255
match ( & mut conflict, my_side) {
256
( Some ( c) , Some ( s) ) => c.entry ( s) .or_default ( ) .push ( v) ,
257
( Some ( _ ) , None ) => {
258
let c = conflict.take ( ) .unwrap ( ) ;
259
finish_conflict ( c, & mut segments) ;
260
plain.push ( v) ;
261
}
262
( None , _ ) => plain.push ( v) ,
263
}
264
for & w in & rsucc[ & v] {
265
let d = indeg.get_mut ( & w) .unwrap ( ) ;
266
* d - = 1 ;
267
if conflict.is_some ( ) & & ! joins.contains ( & w) {
268
match ( my_side, side.get ( & w) .copied ( ) ) {
269
( Some ( s) , None ) => {
270
side.insert ( w, s) ;
271
}
272
( Some ( s) , Some ( t) ) if s ! = t => {
273
side.remove ( & w) ;
274
joins.insert ( w) ;
275
}
276
( None , Some ( _ ) ) => {
277
side.remove ( & w) ;
278
joins.insert ( w) ;
279
}
280
_ => { }
281
}
282
}
283
if * d = = 0 {
284
ready.insert ( w) ;
285
}
286
}
287
}
288
if let Some ( c) = conflict.take ( ) {
289
finish_conflict ( c, & mut segments) ;
290
}
291
flush_plain ( & mut plain, & mut segments) ;
292
Materialized { segments }
293
}
294
295
296
297
pub fn render ( & self , m : & Materialized, label : & dyn Fn( &ChangeHash ) -> String) -> Vec< u8 > {
298
let total = m.conflict_count ( ) ;
299
let mut n = 0 ;
300
let mut out: Vec< u8 > = Vec :: new( ) ;
301
let push_atoms = | out : & mut Vec< u8 > , atoms : & [AtomKey]| {
302
for a in atoms {
303
if let Some ( c) = self .content ( a) {
304
out.extend_from_slice ( c) ;
305
}
306
}
307
} ;
308
let newline_if_needed = | out : & mut Vec< u8 > | {
309
if ! out.is_empty ( ) & & * out.last ( ) .unwrap ( ) ! = b ' \n ' {
310
out.push ( b ' \n ' ) ;
311
}
312
} ;
313
for seg in & m.segments {
314
match seg {
315
Segment:: Lines( a) => push_atoms ( & mut out, a) ,
316
Segment:: Conflict( sides) => {
317
n + = 1 ;
318
let k = sides.len ( ) ;
319
for ( i, s) in sides.iter ( ) .enumerate ( ) {
320
newline_if_needed ( & mut out) ;
321
let who = s.first ( ) .map ( | a | label ( & a.0 ) ) .unwrap_or_default ( ) ;
322
let marker = if i = = 0 { " <<<<<<<" } else { " =======" } ;
323
out.extend_from_slice (
324
format! ( " {marker} oxy conflict {n} of {total} , side {} of {k} : change {who} \n " , i + 1 ) .as_bytes ( ) ,
325
) ;
326
push_atoms ( & mut out, s) ;
327
}
328
newline_if_needed ( & mut out) ;
329
out.extend_from_slice ( format! ( " >>>>>>> oxy conflict {n} of {total} end\n " ) .as_bytes ( ) ) ;
330
}
331
Segment:: Zombie { deleted, inserted } => {
332
n + = 1 ;
333
newline_if_needed ( & mut out) ;
334
let deleter = deleted
335
.first ( )
336
.and_then ( | a | self .deleters ( a) .next ( ) .copied ( ) )
337
.map ( | d | label ( & d) )
338
.unwrap_or_default ( ) ;
339
let inserter = inserted.first ( ) .map ( | a | label ( & a.0 ) ) .unwrap_or_default ( ) ;
340
out.extend_from_slice (
341
format! ( " <<<<<<< oxy conflict {n} of {total} , side 1 of 2: deleted by change {deleter} \n -------\n " ) .as_bytes ( ) ,
342
) ;
343
push_atoms ( & mut out, deleted) ;
344
newline_if_needed ( & mut out) ;
345
out.extend_from_slice ( format! ( " ======= oxy conflict {n} of {total} , side 2 of 2: change {inserter} \n " ) .as_bytes ( ) ) ;
346
push_atoms ( & mut out, inserted) ;
347
newline_if_needed ( & mut out) ;
348
out.extend_from_slice ( format! ( " >>>>>>> oxy conflict {n} of {total} end\n " ) .as_bytes ( ) ) ;
349
}
350
}
351
}
352
out
353
}
354
}
355
356
357
fn split_zombies ( atoms : Vec< AtomKey> , ghosts : & HashSet< AtomKey> ) -> Vec< Segment> {
358
let mut out = Vec :: new( ) ;
359
let mut lines: Vec< AtomKey> = Vec :: new( ) ;
360
let mut i = 0 ;
361
while i < atoms.len ( ) {
362
if ! ghosts.contains ( & atoms[ i] ) {
363
lines.push ( atoms[ i] ) ;
364
i + = 1 ;
365
continue ;
366
}
367
if ! lines.is_empty ( ) {
368
out.push ( Segment:: Lines( std:: mem:: take( & mut lines) ) ) ;
369
}
370
let mut deleted = Vec :: new( ) ;
371
while i < atoms.len ( ) & & ghosts.contains ( & atoms[ i] ) {
372
deleted.push ( atoms[ i] ) ;
373
i + = 1 ;
374
}
375
let mut inserted = Vec :: new( ) ;
376
if i < atoms.len ( ) {
377
let ch = atoms[ i] .0 ;
378
while i < atoms.len ( ) & & atoms[ i] .0 = = ch & & ! ghosts.contains ( & atoms[ i] ) {
379
inserted.push ( atoms[ i] ) ;
380
i + = 1 ;
381
}
382
}
383
out.push ( Segment:: Zombie { deleted, inserted } ) ;
384
}
385
if ! lines.is_empty ( ) {
386
out.push ( Segment:: Lines( lines) ) ;
387
}
388
out
389
}
390
391
# [ cfg ( test ) ]
392
mod tests {
393
use super :: * ;
394
use crate :: hash:: ObjectRef;
395
396
fn h ( n : u8 ) -> ChangeHash {
397
ObjectRef( [ n; 32 ] )
398
}
399
fn lines ( v : & [& str ]) -> Vec< Vec< u8 > > {
400
v.iter ( ) .map ( | s | s.as_bytes ( ) .to_vec ( ) ) .collect ( )
401
}
402
fn text ( g : & FileGraph, m : & Materialized) -> String {
403
String :: from_utf8( g.render ( m, & | c| c.short ( 4 ) ) ) .unwrap ( )
404
}
405
406
struct Deps ( HashMap< ChangeHash, Vec< ChangeHash> > ) ;
407
impl DepOracle for Deps {
408
fn depends_on ( & self , a : & ChangeHash, b : & ChangeHash) -> bool {
409
if a = = b {
410
return true ;
411
}
412
let mut stack = vec! [ * a] ;
413
let mut seen = HashSet:: new( ) ;
414
while let Some ( x) = stack.pop ( ) {
415
if let Some ( ds) = self .0. get ( & x) {
416
for d in ds {
417
if d = = b {
418
return true ;
419
}
420
if seen.insert ( * d) {
421
stack.push ( * d) ;
422
}
423
}
424
}
425
}
426
false
427
}
428
}
429
430
# [ test ]
431
fn sequential_inserts_and_deletes ( ) {
432
let mut g = FileGraph:: new( ) ;
433
g.insert ( h ( 1 ) , 0 , Vertex:: Start, Vertex:: End, & lines ( & [ " a\n " , " b\n " , " c\n " ] ) ) ;
434
let m = g.materialize ( & NoDeps) ;
435
assert! ( ! m.is_conflicted ( ) ) ;
436
assert_eq! ( text ( & g, & m) , " a\n b\n c\n " ) ;
437
438
439
g.insert ( h ( 2 ) , 0 , Vertex:: Atom( ( h ( 1 ) , 1 ) ) , Vertex:: Atom( ( h ( 1 ) , 2 ) ) , & lines ( & [ " x\n " ] ) ) ;
440
g.delete ( h ( 2 ) , & AtomRange { change: h ( 1 ) , from: 0 , to: 1 } ) ;
441
let deps = Deps( [ ( h ( 2 ) , vec! [ h ( 1 ) ] ) ] .into_iter ( ) .collect ( ) ) ;
442
let m = g.materialize ( & deps) ;
443
assert! ( ! m.is_conflicted ( ) ) ;
444
assert_eq! ( text ( & g, & m) , " b\n x\n c\n " ) ;
445
assert_eq! ( m.live_atoms ( ) , vec! [ ( h ( 1 ) , 1 ) , ( h ( 2 ) , 0 ) , ( h ( 1 ) , 2 ) ] ) ;
446
}
447
448
# [ test ]
449
fn concurrent_inserts_at_same_anchor_conflict ( ) {
450
let mut g = FileGraph:: new( ) ;
451
g.insert ( h ( 1 ) , 0 , Vertex:: Start, Vertex:: End, & lines ( & [ " a\n " , " b\n " ] ) ) ;
452
g.insert ( h ( 2 ) , 0 , Vertex:: Atom( ( h ( 1 ) , 0 ) ) , Vertex:: Atom( ( h ( 1 ) , 1 ) ) , & lines ( & [ " from-2\n " ] ) ) ;
453
g.insert ( h ( 3 ) , 0 , Vertex:: Atom( ( h ( 1 ) , 0 ) ) , Vertex:: Atom( ( h ( 1 ) , 1 ) ) , & lines ( & [ " from-3\n " , " more-3\n " ] ) ) ;
454
let deps = Deps( [ ( h ( 2 ) , vec! [ h ( 1 ) ] ) , ( h ( 3 ) , vec! [ h ( 1 ) ] ) ] .into_iter ( ) .collect ( ) ) ;
455
let m = g.materialize ( & deps) ;
456
assert! ( m.is_conflicted ( ) ) ;
457
assert_eq! ( m.conflict_count ( ) , 1 ) ;
458
assert_eq! (
459
m.segments,
460
vec! [
461
Segment:: Lines( vec! [ ( h ( 1 ) , 0 ) ] ) ,
462
Segment:: Conflict( vec! [ vec! [ ( h ( 2 ) , 0 ) ] , vec! [ ( h ( 3 ) , 0 ) , ( h ( 3 ) , 1 ) ] ] ) ,
463
Segment:: Lines( vec! [ ( h ( 1 ) , 1 ) ] ) ,
464
]
465
) ;
466
let t = text ( & g, & m) ;
467
assert! ( t.starts_with ( " a\n <<<<<<< oxy conflict 1 of 1, side 1 of 2: change 0202\n from-2\n ======= oxy conflict 1 of 1, side 2 of 2: change 0303\n from-3\n more-3\n >>>>>>> oxy conflict 1 of 1 end\n b\n " ) , " {t}" ) ;
468
}
469
470
# [ test ]
471
fn concurrent_inserts_at_different_anchors_do_not_conflict ( ) {
472
let mut g = FileGraph:: new( ) ;
473
g.insert ( h ( 1 ) , 0 , Vertex:: Start, Vertex:: End, & lines ( & [ " a\n " , " b\n " , " c\n " ] ) ) ;
474
g.insert ( h ( 2 ) , 0 , Vertex:: Start, Vertex:: Atom( ( h ( 1 ) , 0 ) ) , & lines ( & [ " top\n " ] ) ) ;
475
g.insert ( h ( 3 ) , 0 , Vertex:: Atom( ( h ( 1 ) , 2 ) ) , Vertex:: End, & lines ( & [ " bottom\n " ] ) ) ;
476
let deps = Deps( [ ( h ( 2 ) , vec! [ h ( 1 ) ] ) , ( h ( 3 ) , vec! [ h ( 1 ) ] ) ] .into_iter ( ) .collect ( ) ) ;
477
let m = g.materialize ( & deps) ;
478
assert! ( ! m.is_conflicted ( ) ) ;
479
assert_eq! ( text ( & g, & m) , " top\n a\n b\n c\n bottom\n " ) ;
480
}
481
482
# [ test ]
483
fn concurrent_deletes_of_the_same_line_agree ( ) {
484
let mut g = FileGraph:: new( ) ;
485
g.insert ( h ( 1 ) , 0 , Vertex:: Start, Vertex:: End, & lines ( & [ " a\n " , " b\n " ] ) ) ;
486
g.delete ( h ( 2 ) , & AtomRange { change: h ( 1 ) , from: 0 , to: 1 } ) ;
487
g.delete ( h ( 3 ) , & AtomRange { change: h ( 1 ) , from: 0 , to: 1 } ) ;
488
let deps = Deps( [ ( h ( 2 ) , vec! [ h ( 1 ) ] ) , ( h ( 3 ) , vec! [ h ( 1 ) ] ) ] .into_iter ( ) .collect ( ) ) ;
489
let m = g.materialize ( & deps) ;
490
assert! ( ! m.is_conflicted ( ) ) ;
491
assert_eq! ( text ( & g, & m) , " b\n " ) ;
492
}
493
494
# [ test ]
495
fn delete_insert_conflict_is_a_zombie ( ) {
496
let mut g = FileGraph:: new( ) ;
497
g.insert ( h ( 1 ) , 0 , Vertex:: Start, Vertex:: End, & lines ( & [ " a\n " , " b\n " ] ) ) ;
498
499
g.delete ( h ( 2 ) , & AtomRange { change: h ( 1 ) , from: 0 , to: 1 } ) ;
500
g.insert ( h ( 3 ) , 0 , Vertex:: Atom( ( h ( 1 ) , 0 ) ) , Vertex:: Atom( ( h ( 1 ) , 1 ) ) , & lines ( & [ " after-a\n " ] ) ) ;
501
let deps = Deps( [ ( h ( 2 ) , vec! [ h ( 1 ) ] ) , ( h ( 3 ) , vec! [ h ( 1 ) ] ) ] .into_iter ( ) .collect ( ) ) ;
502
let m = g.materialize ( & deps) ;
503
assert! ( m.is_conflicted ( ) ) ;
504
assert_eq! (
505
m.segments,
506
vec! [
507
Segment:: Zombie { deleted: vec! [ ( h ( 1 ) , 0 ) ] , inserted: vec! [ ( h ( 3 ) , 0 ) ] } ,
508
Segment:: Lines( vec! [ ( h ( 1 ) , 1 ) ] ) ,
509
]
510
) ;
511
let t = text ( & g, & m) ;
512
assert! ( t.contains ( " -------\n a\n =======" ) , " {t}" ) ;
513
assert! ( t.contains ( " after-a\n >>>>>>>" ) , " {t}" ) ;
514
515
516
let deps = Deps( [ ( h ( 2 ) , vec! [ h ( 1 ) , h ( 3 ) ] ) , ( h ( 3 ) , vec! [ h ( 1 ) ] ) ] .into_iter ( ) .collect ( ) ) ;
517
let m = g.materialize ( & deps) ;
518
assert! ( ! m.is_conflicted ( ) ) ;
519
assert_eq! ( text ( & g, & m) , " after-a\n b\n " ) ;
520
}
521
522
# [ test ]
523
fn resolution_collapses_a_conflict ( ) {
524
let mut g = FileGraph:: new( ) ;
525
g.insert ( h ( 1 ) , 0 , Vertex:: Start, Vertex:: End, & lines ( & [ " a\n " ] ) ) ;
526
g.insert ( h ( 2 ) , 0 , Vertex:: Start, Vertex:: Atom( ( h ( 1 ) , 0 ) ) , & lines ( & [ " x\n " ] ) ) ;
527
g.insert ( h ( 3 ) , 0 , Vertex:: Start, Vertex:: Atom( ( h ( 1 ) , 0 ) ) , & lines ( & [ " y\n " ] ) ) ;
528
let mut deps: HashMap< _ , _ > = [ ( h ( 2 ) , vec! [ h ( 1 ) ] ) , ( h ( 3 ) , vec! [ h ( 1 ) ] ) ] .into_iter ( ) .collect ( ) ;
529
assert! ( g.materialize ( & Deps( deps.clone ( ) ) ) .is_conflicted ( ) ) ;
530
531
g.delete ( h ( 4 ) , & AtomRange { change: h ( 2 ) , from: 0 , to: 1 } ) ;
532
deps.insert ( h ( 4 ) , vec! [ h ( 2 ) , h ( 3 ) ] ) ;
533
let m = g.materialize ( & Deps( deps) ) ;
534
assert! ( ! m.is_conflicted ( ) ) ;
535
assert_eq! ( text ( & g, & m) , " y\n a\n " ) ;
536
}
537
538
# [ test ]
539
fn empty_and_unterminated_files ( ) {
540
let g = FileGraph:: new( ) ;
541
let m = g.materialize ( & NoDeps) ;
542
assert_eq! ( m.segments, vec! [ ] ) ;
543
assert_eq! ( text ( & g, & m) , " " ) ;
544
let mut g = FileGraph:: new( ) ;
545
g.insert ( h ( 1 ) , 0 , Vertex:: Start, Vertex:: End, & lines ( & [ " no newline" ] ) ) ;
546
assert_eq! ( text ( & g, & g.materialize ( & NoDeps) ) , " no newline" ) ;
547
}
548
549
550
# [ test ]
551
fn application_order_is_irrelevant ( ) {
552
use rand:: seq:: SliceRandom;
553
type Op = ( ChangeHash, Vec< ( u32 , Vertex, Vertex, Vec< Vec< u8 > > ) > , Vec< AtomRange> ) ;
554
let ops: Vec< Op> = vec! [
555
( h ( 1 ) , vec! [ ( 0 , Vertex:: Start, Vertex:: End, lines ( & [ " a\n " , " b\n " , " c\n " ] ) ) ] , vec! [ ] ) ,
556
( h ( 2 ) , vec! [ ( 0 , Vertex:: Atom( ( h ( 1 ) , 0 ) ) , Vertex:: Atom( ( h ( 1 ) , 1 ) ) , lines ( & [ " p\n " ] ) ) ] , vec! [ ] ) ,
557
( h ( 3 ) , vec! [ ( 0 , Vertex:: Atom( ( h ( 1 ) , 0 ) ) , Vertex:: Atom( ( h ( 1 ) , 1 ) ) , lines ( & [ " q\n " , " r\n " ] ) ) ] , vec! [ AtomRange { change: h ( 1 ) , from: 2 , to: 3 } ] ) ,
558
( h ( 4 ) , vec! [ ( 0 , Vertex:: Atom( ( h ( 3 ) , 1 ) ) , Vertex:: Atom( ( h ( 1 ) , 1 ) ) , lines ( & [ " s\n " ] ) ) ] , vec! [ AtomRange { change: h ( 1 ) , from: 1 , to: 2 } ] ) ,
559
( h ( 5 ) , vec! [ ( 0 , Vertex:: Atom( ( h ( 1 ) , 2 ) ) , Vertex:: End, lines ( & [ " z\n " ] ) ) ] , vec! [ ] ) ,
560
] ;
561
let deps = Deps(
562
[ ( h ( 2 ) , vec! [ h ( 1 ) ] ) , ( h ( 3 ) , vec! [ h ( 1 ) ] ) , ( h ( 4 ) , vec! [ h ( 3 ) , h ( 1 ) ] ) , ( h ( 5 ) , vec! [ h ( 1 ) ] ) ] .into_iter ( ) .collect ( ) ,
563
) ;
564
let build = | order : & [usize ]| {
565
let mut g = FileGraph:: new( ) ;
566
for & i in order {
567
let ( c, ins, dels) = & ops[ i] ;
568
for ( idx, a, b, l) in ins {
569
g.insert ( * c, * idx, * a, * b, l) ;
570
}
571
for d in dels {
572
g.delete ( * c, d) ;
573
}
574
}
575
let m = g.materialize ( & deps) ;
576
( m.clone ( ) , text ( & g, & m) )
577
} ;
578
let reference = build ( & [ 0 , 1 , 2 , 3 , 4 ] ) ;
579
assert! ( reference.0. is_conflicted ( ) ) ;
580
let mut rng = rand:: thread_rng( ) ;
581
let mut order: Vec< usize > = ( 0 .. ops.len ( ) ) .collect ( ) ;
582
for _ in 0 .. 50 {
583
order.shuffle ( & mut rng) ;
584
assert_eq! ( build ( & order) , reference, " order {order:?}" ) ;
585
}
586
}
587
}