c5f3bde4 Andy McCall 1 month ago ↶
1
2
3
4
5
6
7
8
use super :: edit:: { after_web_commit, signer_for} ;
9
use super :: settings:: { csrf, require} ;
10
use super :: { lookup_role, Error, Page, RepoShell, Shell} ;
11
use crate :: auth:: { Access, Viewer} ;
12
use crate :: repos:: edits;
13
use crate :: repos:: view:: { self , FileBody} ;
14
use crate :: Shared;
15
use askama:: Template;
16
use axum:: extract:: { Form, Path, State} ;
17
use axum:: response:: { IntoResponse, Redirect, Response} ;
18
use oxy_core:: repo:: FileEdit;
19
use std:: collections:: HashMap;
20
16cb2b3a Andy McCall 1 month ago ↶
21
22
pub fn vscode_link ( app : & Shared, owner : & str , name : & str , bookmark : & str , path : & str ) -> String {
23
use super :: settings:: urlencode;
24
let url = format! ( " {} /{owner} /{name} " , app.config.base_url ( ) .trim_end_matches ( ' /' ) ) ;
25
format! ( " vscode://oxygen.oxygen-vscode/resolve?url={} &bookmark={} &path={} " , urlencode ( & url) , urlencode ( bookmark) , urlencode ( path) )
26
}
27
c5f3bde4 Andy McCall 1 month ago ↶
28
29
# [ derive ( Clone, Debug, PartialEq, Eq ) ]
30
pub enum Piece {
31
32
Plain( String ) ,
33
Region( Region) ,
34
}
35
36
# [ derive ( Clone, Debug, PartialEq, Eq ) ]
37
pub struct Region {
38
39
pub n : usize ,
40
pub sides : Vec< Side> ,
41
}
42
43
# [ derive ( Clone, Debug, PartialEq, Eq ) ]
44
pub struct Side {
45
46
pub label : String,
47
48
pub text : String,
49
50
51
pub deletion : bool ,
52
}
53
54
impl Side {
55
56
pub fn kept ( & self ) -> & str {
57
if self .deletion {
58
" "
59
} else {
60
& self .text
61
}
62
}
63
}
64
65
impl Region {
66
67
pub fn all ( & self ) -> String {
68
self .sides.iter ( ) .map ( Side:: kept) .collect ( )
69
}
70
}
71
72
73
74
pub fn parse ( marked : & str ) -> Option< Vec< Piece> > {
75
let mut out = Vec :: new( ) ;
76
let mut plain = String :: new( ) ;
77
let mut region: Option< Region> = None ;
78
let mut lines = marked.split_inclusive ( ' \n ' ) .peekable ( ) ;
79
while let Some ( line) = lines.next ( ) {
80
let bare = line.trim_end_matches ( ' \n ' ) ;
81
if let Some ( rest) = bare.strip_prefix ( " <<<<<<< oxy conflict " ) {
82
if region.is_some ( ) {
83
return None ;
84
}
85
if ! plain.is_empty ( ) {
86
out.push ( Piece:: Plain( std:: mem:: take( & mut plain) ) ) ;
87
}
88
let ( n, label) = header ( rest) ? ;
89
let mut side = Side { label, text: String :: new( ) , deletion: false } ;
90
if lines.peek ( ) .map ( | l | l.trim_end_matches ( ' \n ' ) ) = = Some ( " -------" ) {
91
lines.next ( ) ;
92
side.deletion = true ;
93
}
94
region = Some ( Region { n, sides: vec! [ side] } ) ;
95
} else if let Some ( rest) = bare.strip_prefix ( " ======= oxy conflict " ) {
96
let r = region.as_mut ( ) ? ;
97
let ( n, label) = header ( rest) ? ;
98
if n ! = r.n {
99
return None ;
100
}
101
r.sides.push ( Side { label, text: String :: new( ) , deletion: false } ) ;
102
} else if bare.starts_with ( " >>>>>>> oxy conflict " ) {
103
out.push ( Piece:: Region( region.take ( ) ? ) ) ;
104
} else {
105
match region.as_mut ( ) {
106
Some ( r) => r.sides.last_mut ( ) ? .text.push_str ( line) ,
107
None => plain.push_str ( line) ,
108
}
109
}
110
}
111
if region.is_some ( ) {
112
return None ;
113
}
114
if ! plain.is_empty ( ) {
115
out.push ( Piece:: Plain( plain) ) ;
116
}
117
Some ( out)
118
}
119
120
121
fn header ( rest : & str ) -> Option< ( usize , String) > {
122
let ( n, tail) = rest.split_once ( " of " ) ? ;
123
let label = tail.split_once ( " : " ) .map ( | ( _, l ) | l.to_string ( ) ) .unwrap_or_default ( ) ;
124
Some ( ( n.trim ( ) .parse ( ) .ok ( ) ? , label) )
125
}
126
127
128
# [ derive ( Clone, Debug, PartialEq, Eq ) ]
129
pub enum Choice {
130
Side( usize ) ,
131
All,
132
None ,
133
Text( String ) ,
134
}
135
136
137
138
pub fn assemble ( pieces : & [Piece], choices : & HashMap< usize , Choice> ) -> Result< String, String> {
139
let mut out = String :: new( ) ;
140
for p in pieces {
141
match p {
142
Piece:: Plain( t) => out.push_str ( t) ,
143
Piece:: Region( r) => match choices.get ( & r.n) {
144
Some ( Choice:: Side( i) ) => {
145
out.push_str ( r.sides.get ( i.wrapping_sub ( 1 ) ) .ok_or_else ( | | format! ( " conflict {} has no side {i} " , r.n) ) ? .kept ( ) )
146
}
147
Some ( Choice:: All) => out.push_str ( & r.all ( ) ) ,
148
Some ( Choice:: None) => { }
149
Some ( Choice:: Text( t) ) => {
150
let t = t.replace ( " \r \n " , " \n " ) ;
151
out.push_str ( & t) ;
152
if ! t.is_empty ( ) & & ! t.ends_with ( ' \n ' ) {
153
out.push ( ' \n ' ) ;
154
}
155
}
156
None => return Err ( format! ( " choose how to resolve conflict {} " , r.n) ) ,
157
} ,
158
}
159
}
160
Ok ( out)
161
}
162
fd958fbc Andy McCall 1 month ago ↶
163
164
# [ derive ( Clone, Debug ) ]
165
pub struct Candidate {
166
pub path : String,
167
pub content : Vec< u8 > ,
168
169
pub preview : Option< String> ,
170
}
171
172
173
174
175
fn name_candidates ( r : & oxy_core:: repo:: Repo, snapshot : & oxy_core:: state:: Snapshot, path : & str ) -> Result< Option< Vec< Candidate> > , String> {
176
use oxy_core:: objects:: NodeKind;
177
use oxy_core:: state:: FileContent;
178
use oxy_core:: tree:: TreeConflict;
179
let by_node = snapshot.tree.by_node ( ) ;
180
for c in & snapshot.tree.conflicts {
181
let TreeConflict:: Name { parent, name, nodes } = c else { continue } ;
182
let full = if parent.is_root ( ) {
183
name.clone ( )
184
} else {
185
match by_node.get ( parent) {
186
Some ( e) => format! ( " {} /{name} " , e.path) ,
187
None => continue ,
188
}
189
} ;
190
if full ! = path {
191
continue ;
192
}
193
let mut out = Vec :: new( ) ;
194
for n in nodes {
195
let Some ( e) = by_node.get ( n) else { continue } ;
196
let f = snapshot.files.get ( & e.path) .ok_or ( " a candidate is missing" ) ? ;
197
if f.kind ! = NodeKind:: File {
198
return Err ( " This conflict is between directories or links; resolve it from a client." .into ( ) ) ;
199
}
200
let content = match & f.content {
201
FileContent:: Text { bytes, .. } => bytes.clone ( ) ,
202
FileContent:: Opaque { blobs } => match blobs.first ( ) {
203
Some ( b) => r.read_blob ( b) .map_err ( | e | e.to_string ( ) ) ? ,
204
None => vec! [ ] ,
205
} ,
206
FileContent:: Dir => continue ,
207
} ;
208
let preview = std:: str:: from_utf8( & content) .ok ( ) .map ( | t | t.lines ( ) .take ( 40 ) .collect:: < Vec< _ > > ( ) .join ( " \n " ) ) ;
209
out.push ( Candidate { path: e.path.clone ( ) , content, preview } ) ;
210
}
211
out.sort_by ( | a , b | a.path.cmp ( & b.path) ) ;
212
return Ok ( Some ( out) ) ;
213
}
214
Ok ( None )
215
}
216
217
218
enum Target {
219
220
Lines( String ) ,
221
222
Name( Vec< Candidate> ) ,
223
}
224
225
# [ derive ( Template ) ]
226
# [ template ( path = " resolve_name.html" ) ]
227
pub struct NamePage {
228
shell : Shell,
229
repo : RepoShell,
230
bookmark : String,
231
path : String,
232
state : String,
233
candidates : Vec< Candidate> ,
16cb2b3a Andy McCall 1 month ago ↶
234
vscode : String,
fd958fbc Andy McCall 1 month ago ↶
235
delegation : Option< String> ,
236
error : Option< String> ,
237
message : String,
238
}
239
c5f3bde4 Andy McCall 1 month ago ↶
240
# [ derive ( Template ) ]
241
# [ template ( path = " resolve.html" ) ]
242
pub struct ResolvePage {
243
shell : Shell,
244
repo : RepoShell,
245
bookmark : String,
246
path : String,
247
state : String,
248
pieces : Vec< Piece> ,
16cb2b3a Andy McCall 1 month ago ↶
249
250
vscode : String,
c5f3bde4 Andy McCall 1 month ago ↶
251
delegation : Option< String> ,
252
error : Option< String> ,
253
message : String,
254
}
255
fd958fbc Andy McCall 1 month ago ↶
256
257
async fn load ( app : & Shared, shell : & Shell, repo : & crate:: repos:: RepoName, bookmark : & str , path : & str ) -> Result< ( String, Target) , Error> {
c5f3bde4 Andy McCall 1 month ago ↶
258
let limit = app.config.limits.render_limit.0 ;
259
let ( b, p) = ( bookmark.to_string ( ) , path.to_string ( ) ) ;
fd958fbc Andy McCall 1 month ago ↶
260
let found = app
c5f3bde4 Andy McCall 1 month ago ↶
261
.repos
262
.read ( repo, move | r, cache| {
fd958fbc Andy McCall 1 month ago ↶
263
let Some ( st) = r.view ( ) ? .bookmarks.get ( & b) .copied ( ) else { return Ok ( None ) } ;
264
let heads = r.store ( ) .get_state ( & st) ? .heads;
265
let loaded = cache.load ( r, & heads) ? ;
266
let names = name_candidates ( r, & loaded.snapshot, & p) ;
267
let file = if names.as_ref ( ) .is_ok_and ( | n | n.is_some ( ) ) {
268
None
269
} else {
270
let target = view:: resolve( r, cache, & b) ? ;
271
Some ( view:: file( r, cache, target, & p, limit) )
272
} ;
273
Ok ( Some ( ( st.to_hex ( ) , names, file) ) )
c5f3bde4 Andy McCall 1 month ago ↶
274
} )
275
.await
276
.map_err ( | e | Error:: from_repo( shell.clone ( ) , e) ) ? ;
fd958fbc Andy McCall 1 month ago ↶
277
let Some ( ( state, names, file) ) = found else { return Err ( Error:: bad( shell.clone ( ) , " Conflicts are resolved on a bookmark." ) ) } ;
278
match names {
279
Ok ( Some ( c) ) => return Ok ( ( state, Target:: Name( c) ) ) ,
280
Err ( msg) => return Err ( Error:: bad( shell.clone ( ) , & msg) ) ,
281
Ok ( None ) => { }
282
}
283
let file = match file {
284
Some ( Ok ( f) ) => f,
285
Some ( Err ( e) ) => return Err ( Error:: from_repo( shell.clone ( ) , e) ) ,
286
None => return Err ( Error:: bad( shell.clone ( ) , " Nothing to resolve there." ) ) ,
287
} ;
c5f3bde4 Andy McCall 1 month ago ↶
288
match file.body {
fd958fbc Andy McCall 1 month ago ↶
289
FileBody:: Text( t) if file.conflicted => Ok ( ( state, Target:: Lines( t) ) ) ,
c5f3bde4 Andy McCall 1 month ago ↶
290
FileBody:: Text( _ ) => Err ( Error:: bad( shell.clone ( ) , " That file has no conflicts on this bookmark." ) ) ,
fd958fbc Andy McCall 1 month ago ↶
291
_ => Err ( Error:: bad( shell.clone ( ) , " Only text files within the render limit, and name conflicts, are resolved in the browser." ) ) ,
c5f3bde4 Andy McCall 1 month ago ↶
292
}
293
}
294
295
# [ allow ( clippy::too_many_arguments ) ]
296
async fn page (
297
app : & Shared,
298
viewer : & Viewer,
299
owner : & str ,
300
name : & str ,
301
bookmark : & str ,
302
path : & str ,
303
error : Option< String> ,
304
message : Option< String> ,
305
) -> Result< Page< ResolvePage> , Error> {
306
let shell = Shell:: new( app, & format! ( " /{owner} /{name} /resolve/{bookmark} /{path} " ) , viewer) ;
307
let me = require ( viewer, & shell) ? ;
308
let ( repo, row, role) = lookup_role ( app, & shell, viewer, owner, name, Access:: Write) .await? ;
fd958fbc Andy McCall 1 month ago ↶
309
let ( state, target) = load ( app, & shell, & repo, bookmark, path) .await? ;
310
let text = match target {
311
Target:: Lines( t) => t,
312
Target:: Name( _ ) => return Err ( Error:: bad( shell, " That is a name conflict." ) ) ,
313
} ;
c5f3bde4 Andy McCall 1 month ago ↶
314
let Some ( pieces) = parse ( & text) else {
315
return Err ( Error:: bad( shell, " The conflict markers could not be read; resolve the file in the plain editor." ) ) ;
316
} ;
317
let delegation = signer_for ( app, me.account.id) .await.err ( ) ;
318
Ok ( Page( ResolvePage {
319
shell,
320
repo: RepoShell:: new( app, & row, " conflicts" ) .with_role ( role) ,
321
bookmark: bookmark.to_string ( ) ,
322
path: path.to_string ( ) ,
323
state,
324
pieces,
16cb2b3a Andy McCall 1 month ago ↶
325
vscode: vscode_link ( app, owner, name, bookmark, path) ,
c5f3bde4 Andy McCall 1 month ago ↶
326
delegation,
327
error,
328
message: message.unwrap_or_else ( | | format! ( " Resolve the conflicts in {path} " ) ) ,
329
} ) )
330
}
331
332
pub async fn form (
333
State( app ) : State< Shared> ,
334
viewer : Viewer,
335
Path( (owner , name , bookmark , path ) ) : Path< ( String, String, String, String) > ,
336
) -> Result< Response, Error> {
fd958fbc Andy McCall 1 month ago ↶
337
let shell = Shell:: new( & app, & format! ( " /{owner} /{name} /resolve/{bookmark} /{path} " ) , & viewer) ;
c5f3bde4 Andy McCall 1 month ago ↶
338
let ( repo, _ , _ ) = lookup_role ( & app, & shell, & viewer, & owner, & name, Access:: Write) .await? ;
fd958fbc Andy McCall 1 month ago ↶
339
match load ( & app, & shell, & repo, & bookmark, & path) .await? .1 {
340
341
Target:: Lines( text) if parse ( & text) .is_none ( ) => {
342
Ok ( Redirect:: to( & format! ( " /{owner} /{name} /edit/{bookmark} /{path} " ) ) .into_response ( ) )
343
}
344
Target:: Lines( _ ) => Ok ( page ( & app, & viewer, & owner, & name, & bookmark, & path, None , None ) .await? .into_response ( ) ) ,
345
Target:: Name( _ ) => Ok ( name_page ( & app, & viewer, & owner, & name, & bookmark, & path, None , None ) .await? .into_response ( ) ) ,
c5f3bde4 Andy McCall 1 month ago ↶
346
}
347
}
348
fd958fbc Andy McCall 1 month ago ↶
349
# [ allow ( clippy::too_many_arguments ) ]
350
async fn name_page (
351
app : & Shared,
352
viewer : & Viewer,
353
owner : & str ,
354
name : & str ,
355
bookmark : & str ,
356
path : & str ,
357
error : Option< String> ,
358
message : Option< String> ,
359
) -> Result< Page< NamePage> , Error> {
360
let shell = Shell:: new( app, & format! ( " /{owner} /{name} /resolve/{bookmark} /{path} " ) , viewer) ;
361
let me = require ( viewer, & shell) ? ;
362
let ( repo, row, role) = lookup_role ( app, & shell, viewer, owner, name, Access:: Write) .await? ;
363
let ( state, target) = load ( app, & shell, & repo, bookmark, path) .await? ;
364
let Target:: Name( candidates) = target else { return Err ( Error:: bad( shell, " That is not a name conflict." ) ) } ;
365
let delegation = signer_for ( app, me.account.id) .await.err ( ) ;
366
Ok ( Page( NamePage {
367
shell,
368
repo: RepoShell:: new( app, & row, " conflicts" ) .with_role ( role) ,
369
bookmark: bookmark.to_string ( ) ,
370
path: path.to_string ( ) ,
371
state,
372
candidates,
16cb2b3a Andy McCall 1 month ago ↶
373
vscode: vscode_link ( app, owner, name, bookmark, path) ,
fd958fbc Andy McCall 1 month ago ↶
374
delegation,
375
error,
376
message: message.unwrap_or_else ( | | format! ( " Resolve the name conflict at {path} " ) ) ,
377
} ) )
378
}
379
c5f3bde4 Andy McCall 1 month ago ↶
380
pub async fn save (
381
State( app ) : State< Shared> ,
382
viewer : Viewer,
383
Path( (owner , name , bookmark , path ) ) : Path< ( String, String, String, String) > ,
384
Form( f) : Form< HashMap< String, String> > ,
385
) -> Result< Response, Error> {
386
let shell = Shell:: new( & app, & format! ( " /{owner} /{name} /resolve/{bookmark} /{path} " ) , & viewer) ;
387
let me = require ( & viewer, & shell) ? ;
388
csrf ( & viewer, f.get ( " _csrf" ) .map ( String :: as_str) .unwrap_or ( " " ) , & shell) ? ;
389
let ( repo, row, _ ) = lookup_role ( & app, & shell, & viewer, & owner, & name, Access:: Write) .await? ;
390
let message = f.get ( " message" ) .map ( | m | m.trim ( ) .to_string ( ) ) .unwrap_or_default ( ) ;
391
let again = | msg : String| {
392
let ( app, viewer, owner, name, bookmark, path, message) =
393
( app.clone ( ) , viewer.clone ( ) , owner.clone ( ) , name.clone ( ) , bookmark.clone ( ) , path.clone ( ) , message.clone ( ) ) ;
394
async move { page ( & app, & viewer, & owner, & name, & bookmark, & path, Some ( msg) , Some ( message) ) .await.map ( IntoResponse:: into_response) }
395
} ;
fd958fbc Andy McCall 1 month ago ↶
396
let ( state, target) = load ( & app, & shell, & repo, & bookmark, & path) .await? ;
397
let stale = f.get ( " state" ) ! = Some ( & state) ;
398
let text = match target {
399
Target:: Lines( t) => t,
400
Target:: Name( candidates) => {
401
let again = | msg : String| {
402
let ( app, viewer, owner, name, bookmark, path, message) =
403
( app.clone ( ) , viewer.clone ( ) , owner.clone ( ) , name.clone ( ) , bookmark.clone ( ) , path.clone ( ) , message.clone ( ) ) ;
404
async move {
405
name_page ( & app, & viewer, & owner, & name, & bookmark, & path, Some ( msg) , Some ( message) )
406
.await
407
.map ( IntoResponse:: into_response)
408
}
409
} ;
410
if stale {
411
return again ( format! ( " {bookmark} has moved since this page was loaded; the candidates below are as they are now." ) ) .await;
412
}
413
let Some ( keep) = f.get ( " keep" ) .and_then ( | k | k.parse:: < usize > ( ) .ok ( ) ) .and_then ( | k | candidates.get ( k) ) else {
414
return again ( " Choose the candidate to keep." .into ( ) ) .await;
415
} ;
416
if message.is_empty ( ) {
417
return again ( " Describe the resolution." .into ( ) ) .await;
418
}
419
let signer = match signer_for ( & app, me.account.id) .await {
420
Ok ( s) => s,
421
Err ( msg) => return again ( msg) .await,
422
} ;
423
424
425
426
let mut edit: Vec< FileEdit> = candidates.iter ( ) .map ( | c | FileEdit { path: c.path.clone ( ) , content: None } ) .collect ( ) ;
427
edit.push ( FileEdit { path: path.clone ( ) , content: Some ( keep.content.clone ( ) ) } ) ;
428
let done = match edits:: commit( & app.repos, & repo, bookmark.clone ( ) , bookmark.clone ( ) , edit, message.clone ( ) , signer) .await {
429
Ok ( d) => d,
430
Err ( e) => return again ( e.to_string ( ) ) .await,
431
} ;
432
after_web_commit ( & app, & me.account, & row, & owner, & name, & done, & message) .await;
433
return Ok ( Redirect:: to( & format! ( " /{owner} /{name} /change/{} " , done.change.to_hex ( ) ) ) .into_response ( ) ) ;
434
}
435
} ;
436
if stale {
c5f3bde4 Andy McCall 1 month ago ↶
437
return again ( format! ( " {bookmark} has moved since this page was loaded; the conflicts below are as they are now." ) ) .await;
438
}
439
let Some ( pieces) = parse ( & text) else { return again ( " The conflict markers could not be read." .into ( ) ) .await } ;
440
let mut choices = HashMap:: new( ) ;
441
for p in & pieces {
442
if let Piece:: Region( r) = p {
443
let choice = match f.get ( & format! ( " r{} " , r.n) ) .map ( String :: as_str) {
444
Some ( " all" ) => Choice:: All,
445
Some ( " none" ) => Choice:: None,
446
Some ( " text" ) => Choice:: Text( f.get ( & format! ( " t{} " , r.n) ) .cloned ( ) .unwrap_or_default ( ) ) ,
447
Some ( s) => match s.strip_prefix ( " side" ) .and_then ( | i | i.parse ( ) .ok ( ) ) {
448
Some ( i) => Choice:: Side( i) ,
449
None => continue ,
450
} ,
451
None => continue ,
452
} ;
453
choices.insert ( r.n, choice) ;
454
}
455
}
456
let content = match assemble ( & pieces, & choices) {
457
Ok ( c) => c,
458
Err ( msg) => return again ( msg) .await,
459
} ;
460
if message.is_empty ( ) {
461
return again ( " Describe the resolution." .into ( ) ) .await;
462
}
463
let signer = match signer_for ( & app, me.account.id) .await {
464
Ok ( s) => s,
465
Err ( msg) => return again ( msg) .await,
466
} ;
467
let edit = vec! [ FileEdit { path: path.clone ( ) , content: Some ( content.into_bytes ( ) ) } ] ;
468
let done = match edits:: commit( & app.repos, & repo, bookmark.clone ( ) , bookmark.clone ( ) , edit, message.clone ( ) , signer) .await {
469
Ok ( d) => d,
470
Err ( e) => return again ( e.to_string ( ) ) .await,
471
} ;
472
after_web_commit ( & app, & me.account, & row, & owner, & name, & done, & message) .await;
473
Ok ( Redirect:: to( & format! ( " /{owner} /{name} /change/{} " , done.change.to_hex ( ) ) ) .into_response ( ) )
474
}
475
476
# [ cfg ( test ) ]
477
mod tests {
478
use super :: * ;
479
480
const MARKED : & str = " a\n <<<<<<< oxy conflict 1 of 2, side 1 of 2: change 0202\n from-2\n ======= oxy conflict 1 of 2, side 2 of 2: change 0303\n from-3\n more-3\n >>>>>>> oxy conflict 1 of 2 end\n b\n <<<<<<< oxy conflict 2 of 2, side 1 of 2: deleted by change 0404\n -------\n gone\n ======= oxy conflict 2 of 2, side 2 of 2: change 0505\n added\n >>>>>>> oxy conflict 2 of 2 end\n c\n " ;
481
482
# [ test ]
483
fn markers_parse_into_regions_and_sides ( ) {
484
let p = parse ( MARKED ) .unwrap ( ) ;
485
assert_eq! ( p.len ( ) , 5 ) ;
486
assert_eq! ( p[ 0 ] , Piece:: Plain( " a\n " .into ( ) ) ) ;
487
let Piece:: Region( r1) = & p[ 1 ] else { panic! ( ) } ;
488
assert_eq! ( ( r1.n, r1.sides.len ( ) ) , ( 1 , 2 ) ) ;
489
assert_eq! ( ( r1.sides[ 0 ] .label.as_str ( ) , r1.sides[ 0 ] .text.as_str ( ) ) , ( " change 0202" , " from-2\n " ) ) ;
490
assert_eq! ( r1.sides[ 1 ] .text, " from-3\n more-3\n " ) ;
491
let Piece:: Region( r2) = & p[ 3 ] else { panic! ( ) } ;
492
assert! ( r2.sides[ 0 ] .deletion & & r2.sides[ 0 ] .text = = " gone\n " & & r2.sides[ 0 ] .kept ( ) .is_empty ( ) ) ;
493
assert_eq! ( r2.sides[ 0 ] .label, " deleted by change 0404" ) ;
494
assert_eq! ( p[ 4 ] , Piece:: Plain( " c\n " .into ( ) ) ) ;
495
assert! ( parse ( " <<<<<<< oxy conflict 1 of 1, side 1 of 2: change x\n no end\n " ) .is_none ( ) ) ;
496
assert_eq! ( parse ( " plain\n " ) .unwrap ( ) , vec! [ Piece:: Plain( " plain\n " .into ( ) ) ] ) ;
497
}
498
499
# [ test ]
500
fn choices_assemble_the_file ( ) {
501
let p = parse ( MARKED ) .unwrap ( ) ;
502
let pick = | a : Choice, b : Choice| assemble ( & p, & HashMap:: from( [ ( 1 , a) , ( 2 , b) ] ) ) ;
503
assert_eq! ( pick ( Choice:: Side( 2 ) , Choice:: Side( 1 ) ) .unwrap ( ) , " a\n from-3\n more-3\n b\n c\n " , " the deletion wins: nothing" ) ;
504
assert_eq! ( pick ( Choice:: All, Choice:: Side( 2 ) ) .unwrap ( ) , " a\n from-2\n from-3\n more-3\n b\n added\n c\n " ) ;
505
assert_eq! ( pick ( Choice:: None, Choice:: Text( " mine" .into ( ) ) ) .unwrap ( ) , " a\n b\n mine\n c\n " , " own text ends its line" ) ;
506
assert! ( pick ( Choice:: Side( 3 ) , Choice:: None) .unwrap_err ( ) .contains ( " no side 3" ) ) ;
507
assert! ( assemble ( & p, & HashMap:: from( [ ( 1 , Choice:: None) ] ) ) .unwrap_err ( ) .contains ( " conflict 2" ) ) ;
508
}
509
}