98286406 Andy McCall 1 month ago ↶
1
2
3
use super :: * ;
957fee30 Andy McCall 1 month ago ↶
4
use crate :: objects:: AtomRef;
98286406 Andy McCall 1 month ago ↶
5
6
# [ derive ( Clone, Copy, Debug, PartialEq, Eq ) ]
7
pub enum ChangeStatus {
8
Added,
9
Modified,
10
Removed,
6e723b81 Andy McCall 1 month ago ↶
11
12
Renamed,
98286406 Andy McCall 1 month ago ↶
13
ModeChanged,
14
15
ConflictEdited,
16
17
Resolved,
18
}
19
20
21
pub fn has_conflict_markers ( content : & [u8 ]) -> bool {
22
split_lines ( content) .iter ( ) .any ( | l | {
23
l.starts_with ( b " <<<<<<< oxy conflict" ) | | l.starts_with ( b " ======= oxy conflict" ) | | l.starts_with ( b " >>>>>>> oxy conflict" )
24
} )
25
}
26
27
# [ derive ( Clone, Debug, PartialEq, Eq ) ]
28
pub struct PathStatus {
29
pub path : String,
30
pub status : ChangeStatus,
31
}
32
614541c3 Andy McCall 1 month ago ↶
33
34
pub ( crate ) struct BuiltChange {
35
pub tree_ops : Vec< TreeOp> ,
36
pub edits : Vec< Edit> ,
37
pub contributors : Vec< ChangeHash> ,
38
pub resolves : bool ,
39
pub statuses : Vec< PathStatus> ,
b7f102fc Andy McCall 1 month ago ↶
40
41
pub blobs : HashMap< String, ObjectRef> ,
614541c3 Andy McCall 1 month ago ↶
42
}
43
98286406 Andy McCall 1 month ago ↶
44
45
pub struct SnapshotResult {
46
pub wc_hash : ChangeHash,
47
pub wc : Change,
48
49
pub base : State,
50
pub base_snapshot : Snapshot,
51
pub statuses : Vec< PathStatus> ,
52
53
pub changed : bool ,
54
55
pub resolves : bool ,
56
57
pub contributors : Vec< ChangeHash> ,
58
}
59
60
pub fn is_binary ( content : & [u8 ]) -> bool {
61
content[ .. content.len ( ) .min ( BINARY_SNIFF ) ] .contains ( & 0 )
62
}
63
64
impl Repo {
614541c3 Andy McCall 1 month ago ↶
65
66
67
68
pub ( crate ) fn build_against ( & self , base : & State, base_snapshot : & Snapshot, reuse : HashMap< String, ( NodeId, NodeKind) > ) -> Result< BuiltChange> {
4dee277c Andy McCall 1 month ago ↶
69
let fs_entries = self .walk_working_copy_with ( None , & Self :: tracked_kinds( base_snapshot) ) ? ;
c3069537 Andy McCall 1 month ago ↶
70
self .build_from_entries ( base, base_snapshot, reuse, & fs_entries)
71
}
72
73
74
pub ( crate ) fn build_from_entries ( & self , base : & State, base_snapshot : & Snapshot, reuse : HashMap< String, ( NodeId, NodeKind) > , fs_entries : & BTreeMap< String, FsEntry> ) -> Result< BuiltChange> {
cdb5beba Andy McCall 1 month ago ↶
75
76
77
let attrs = crate :: attributes:: Attributes:: from_files(
78
fs_entries.iter ( ) .filter ( | ( p , _) | p.as_str ( ) = = " .oxyattributes" | | p.ends_with ( " /.oxyattributes" ) ) .map ( | ( p , e ) | ( p.as_str ( ) , e.content.as_slice ( ) ) ) ,
79
) ;
80
let mut b = ChangeBuilder:: new( base, base_snapshot, reuse) .with_attributes ( attrs) ;
6e723b81 Andy McCall 1 month ago ↶
81
82
83
84
let renames = detect_renames ( base_snapshot, fs_entries, | p | self .in_scope ( p) ) ;
85
let renamed_from: HashMap< & str , & str > = renames.iter ( ) .map ( | ( old , new ) | ( new.as_str ( ) , old.as_str ( ) ) ) .collect ( ) ;
86
let renamed_to: HashMap< & str , & str > = renames.iter ( ) .map ( | ( old , new ) | ( old.as_str ( ) , new.as_str ( ) ) ) .collect ( ) ;
c3069537 Andy McCall 1 month ago ↶
87
for ( path, fs_entry) in fs_entries {
98286406 Andy McCall 1 month ago ↶
88
match base_snapshot.files.get ( path) {
6e723b81 Andy McCall 1 month ago ↶
89
None => match renamed_from.get ( path.as_str ( ) ) {
90
Some ( old) => b.move_path ( self , old, path, fs_entry, & base_snapshot.files[ * old] ) ? ,
91
None => b.add_path ( self , path, fs_entry) ? ,
92
} ,
98286406 Andy McCall 1 month ago ↶
93
Some ( expected) => b.update_path ( self , path, fs_entry, expected) ? ,
94
}
95
}
96
for ( path, expected) in & base_snapshot.files {
6e723b81 Andy McCall 1 month ago ↶
97
if fs_entries.contains_key ( path) | | ! self .in_scope ( path) | | renamed_to.contains_key ( path.as_str ( ) ) {
98286406 Andy McCall 1 month ago ↶
98
continue ;
99
}
7421309f Andy McCall 1 month ago ↶
100
101
let still_needed = expected.kind = = NodeKind:: Dir
102
& & ( fs_entries.keys ( ) .any ( | p | p.starts_with ( & format! ( " {path} /" ) ) )
103
| | base_snapshot.files.keys ( ) .any ( | p | p.starts_with ( & format! ( " {path} /" ) ) & & ! self .in_scope ( p) ) ) ;
98286406 Andy McCall 1 month ago ↶
104
if ! still_needed {
105
b.remove_path ( path, expected) ;
106
}
107
}
614541c3 Andy McCall 1 month ago ↶
108
let mut statuses = b.statuses;
109
statuses.sort_by ( | a , b | a.path.cmp ( & b.path) ) ;
b7f102fc Andy McCall 1 month ago ↶
110
Ok ( BuiltChange { tree_ops: b.tree_ops, edits: b.edits, contributors: b.contributors.into_iter ( ) .collect ( ) , resolves: b.resolves, statuses, blobs: b.blobs } )
614541c3 Andy McCall 1 month ago ↶
111
}
98286406 Andy McCall 1 month ago ↶
112
c07e0e7d Andy McCall 1 month ago ↶
113
114
115
614541c3 Andy McCall 1 month ago ↶
116
pub ( crate ) fn store_signed ( & self , change : & Change) -> Result< ChangeHash> {
c07e0e7d Andy McCall 1 month ago ↶
117
let ours = change.author = = self .author ( ) ;
118
if self .config.sign_changes & & ours & & self .secret_key ( ) ? .is_none ( ) {
614541c3 Andy McCall 1 month ago ↶
119
return fail ( " core.sign is \" change\" but no secret key is available for author.key" ) ;
120
}
121
let hash = self .store.put_change ( change) ? ;
c07e0e7d Andy McCall 1 month ago ↶
122
if self .config.sign_changes & & ours {
614541c3 Andy McCall 1 month ago ↶
123
self .sign_change ( & hash) ? ;
124
}
125
Ok ( hash)
126
}
127
128
129
pub fn snapshot ( & self , command : & str ) -> Result< SnapshotResult> {
4e88e774 Andy McCall 1 month ago ↶
130
let _lock = self .lock_snapshot ( ) ? ;
8f10a07a Andy McCall 1 month ago ↶
131
let batch = self .store.begin_batch ( ) ? ;
132
let result = self .snapshot_locked ( command) ;
133
batch.finish ( ) ? ;
134
result
135
}
136
137
fn snapshot_locked ( & self , command : & str ) -> Result< SnapshotResult> {
614541c3 Andy McCall 1 month ago ↶
138
let ( op_ref, op) = self .head_op ( ) ? ;
139
let ( old_hash, old_wc) = self .working_copy ( ) ? ;
140
let base = self .load_state ( & old_wc.deps) ? ;
141
let base_snapshot = base.materialize ( ) ;
142
143
144
145
let reuse = added_node_paths ( & old_wc, & base_snapshot) ;
b7f102fc Andy McCall 1 month ago ↶
146
147
let manifest = self .read_manifest ( ) ? .map ( | ( _, _, m ) | m ) ;
4dee277c Andy McCall 1 month ago ↶
148
let fs_entries = self .walk_working_copy_with ( manifest.as_ref ( ) , & Self :: tracked_kinds( & base_snapshot) ) ? ;
b7f102fc Andy McCall 1 month ago ↶
149
let built = self .build_from_entries ( & base, & base_snapshot, reuse, & fs_entries) ? ;
614541c3 Andy McCall 1 month ago ↶
150
98286406 Andy McCall 1 month ago ↶
151
let mut wc = Change {
152
change_id: old_wc.change_id,
153
deps: vec! [ ] ,
154
author: self .author ( ) ,
155
hlc: old_wc.hlc,
156
message: old_wc.message.clone ( ) ,
614541c3 Andy McCall 1 month ago ↶
157
tree_ops: built.tree_ops,
158
edits: built.edits,
98286406 Andy McCall 1 month ago ↶
159
kind: ChangeKind:: WorkingCopy,
160
} ;
161
let base_heads = base.maximal ( & old_wc.deps) ;
162
let mut deps: BTreeSet< ChangeHash> = base_heads.into_iter ( ) .collect ( ) ;
163
deps.extend ( referenced_changes ( & wc, & base) ) ;
614541c3 Andy McCall 1 month ago ↶
164
deps.extend ( built.contributors.iter ( ) .copied ( ) ) ;
98286406 Andy McCall 1 month ago ↶
165
wc.deps = deps.into_iter ( ) .collect ( ) ;
166
wc.normalize ( ) ;
614541c3 Andy McCall 1 month ago ↶
167
let resolves = built.resolves;
168
let contributors = built.contributors;
169
let statuses = built.statuses;
98286406 Andy McCall 1 month ago ↶
170
171
let new_hash = wc.hash ( ) ;
172
let changed = new_hash ! = old_hash;
173
if changed {
174
self .store.put_change ( & wc) ? ;
175
let mut view = op.view.clone ( ) ;
176
view.workspaces.insert ( self .workspace, new_hash) ;
177
self .record_op ( & [ op_ref] , & format! ( " {command} {SNAPSHOT_SUFFIX} " ) , view) ? ;
178
}
135212de Andy McCall 1 month ago ↶
179
b7f102fc Andy McCall 1 month ago ↶
180
self .write_manifest_entries ( base_snapshot.is_conflicted ( ) , ! wc.tree_ops.is_empty ( ) | | ! wc.edits.is_empty ( ) , & fs_entries, & built.blobs) ? ;
98286406 Andy McCall 1 month ago ↶
181
Ok ( SnapshotResult { wc_hash: new_hash, wc, base, base_snapshot, statuses, changed, resolves, contributors } )
182
}
183
50a046e4 Andy McCall 1 month ago ↶
184
185
186
187
pub fn status_json ( & self , snap : & SnapshotResult) -> Result< String> {
188
let heads = self .bookmark_heads ( ) ? ;
189
let base_snapshot = & snap.base_snapshot;
190
let by_node = base_snapshot.tree.by_node ( ) ;
191
let path_of = | n : & NodeId| by_node.get ( n) .map ( | e | e.path.clone ( ) ) ;
192
let id_of = | h : & ChangeHash| snap.base.get ( h) .map ( | m | m.change_id.to_hex ( ) ) .unwrap_or_else ( | | h.to_hex ( ) ) ;
193
let paths = json_array ( snap.statuses.iter ( ) .map ( | s | format! ( " {{ \" path\" :{} ,\" status\" :{} }} " , json_str ( & s.path) , json_str ( & format! ( " {:?} " , s.status) ) ) ) ) ;
194
let mut conflicts: Vec< String> = Vec :: new( ) ;
195
for ( p, f) in & base_snapshot.files {
196
if ! f.is_conflicted ( ) {
197
continue ;
198
}
199
let ( kind, changes) = match & f.content {
200
FileContent:: Text { .. } => ( " text" , snap.base.conflict_regions ( & f.node) .contributors) ,
201
FileContent:: Opaque { blobs } => ( " replace" , snap.base.blob_contributors ( & f.node, blobs) ) ,
202
FileContent:: Dir => continue ,
203
} ;
204
let resolved = match snap.statuses.iter ( ) .find ( | s | s.path = = * p ) .map ( | s | s.status ) {
205
Some ( ChangeStatus:: Resolved) => " true" ,
206
Some ( ChangeStatus:: ConflictEdited) => " \" markers\" " ,
207
_ => " false" ,
208
} ;
209
conflicts.push ( format! (
210
" {{ \" kind\" :{} ,\" path\" :{} ,\" node\" :{} ,\" changes\" :{} ,\" resolved\" :{resolved} }} " ,
211
json_str ( kind) ,
212
json_str ( p) ,
213
json_str ( & f.node.to_hex ( ) ) ,
214
json_array ( changes.iter ( ) .map ( | h | json_str ( & id_of ( h) ) ) )
215
) ) ;
216
}
217
for c in & base_snapshot.tree.conflicts {
218
let ( kind, nodes) : ( & str , Vec< NodeId> ) = match c {
219
crate :: tree:: TreeConflict:: Name { nodes, .. } => ( " name" , nodes.clone ( ) ) ,
220
crate :: tree:: TreeConflict:: MultiPlacement { node, .. } => ( " multi-placement" , vec! [ * node] ) ,
221
crate :: tree:: TreeConflict:: Orphan { node, .. } => ( " orphan" , vec! [ * node] ) ,
222
crate :: tree:: TreeConflict:: Cycle { node } => ( " cycle" , vec! [ * node] ) ,
223
crate :: tree:: TreeConflict:: Mode { node, .. } => ( " mode" , vec! [ * node] ) ,
224
} ;
225
let detail = match c {
226
crate :: tree:: TreeConflict:: Name { name, parent, .. } => {
227
let dir = if * parent = = NodeId:: ROOT { String :: new( ) } else { path_of ( parent) .unwrap_or_default ( ) } ;
228
format! ( " ,\" path\" :{} " , json_str ( & if dir.is_empty ( ) { name.clone ( ) } else { format! ( " {dir} /{name} " ) } ) )
229
}
230
_ => nodes.first ( ) .and_then ( path_of) .map ( | p | format! ( " ,\" path\" :{} " , json_str ( & p) ) ) .unwrap_or_default ( ) ,
231
} ;
232
conflicts.push ( format! (
233
" {{ \" kind\" :{} ,\" nodes\" :{} {detail} }} " ,
234
json_str ( kind) ,
235
json_array ( nodes.iter ( ) .map ( | n | json_str ( & n.to_hex ( ) ) ) )
236
) ) ;
237
}
238
Ok ( format! (
239
" {{ \" bookmark\" :{} ,\" working_copy\" :{} ,\" heads\" :{} ,\" clean\" :{} ,\" conflicted\" :{} ,\" paths\" :{paths} ,\" conflicts\" :{} }} " ,
240
json_str ( & self .config.bookmark) ,
241
json_str ( & snap.wc.change_id.to_hex ( ) ) ,
242
heads.len ( ) ,
243
snap.statuses.is_empty ( ) ,
244
base_snapshot.is_conflicted ( ) ,
245
json_array ( conflicts)
246
) )
247
}
248
98286406 Andy McCall 1 month ago ↶
249
250
pub fn commit ( & self , message : & str , extra_deps : & [ChangeHash]) -> Result< ChangeHash> {
957fee30 Andy McCall 1 month ago ↶
251
self .commit_selection ( message, extra_deps, & Selection:: All)
252
}
253
254
255
256
pub fn hunks ( & self ) -> Result< ( SnapshotResult, Vec< Hunk> ) > {
257
let snap = self .snapshot ( " oxy commit" ) ? ;
258
let hunks = render_hunks ( & snap.wc, & snap.base, & snap.base_snapshot) ;
259
Ok ( ( snap, hunks) )
260
}
261
262
263
264
265
266
pub fn commit_selection ( & self , message : & str , extra_deps : & [ChangeHash], selection : & Selection) -> Result< ChangeHash> {
98286406 Andy McCall 1 month ago ↶
267
let snap = self .snapshot ( " oxy commit" ) ? ;
268
if snap.wc.tree_ops.is_empty ( ) & & snap.wc.edits.is_empty ( ) {
269
return fail ( " nothing to commit: the working copy matches its base" ) ;
270
}
271
if let Some ( s) = snap.statuses.iter ( ) .find ( | s | s.status = = ChangeStatus:: ConflictEdited ) {
272
return fail ( format! ( " {} still contains conflict markers" , s.path) ) ;
273
}
274
if message.trim ( ) .is_empty ( ) {
275
return fail ( " a commit message is required" ) ;
276
}
957fee30 Andy McCall 1 month ago ↶
277
let ( tree_ops, edits, partial) = match selection {
278
Selection:: All => ( snap.wc.tree_ops.clone ( ) , snap.wc.edits.clone ( ) , false ) ,
279
Selection:: Paths( paths) => {
280
let ( o, e) = select_paths ( & snap.wc, & snap.base_snapshot, paths) ;
281
let partial = o.len ( ) ! = snap.wc.tree_ops.len ( ) | | e.len ( ) ! = snap.wc.edits.len ( ) ;
282
( o, e, partial)
283
}
284
Selection:: Hunks( indices) => {
285
let ( o, e) = select_hunks ( & snap.wc, indices) ;
286
let partial = o.len ( ) ! = snap.wc.tree_ops.len ( ) | | e.len ( ) ! = snap.wc.edits.len ( ) ;
287
( o, e, partial)
288
}
289
} ;
290
if tree_ops.is_empty ( ) & & edits.is_empty ( ) {
291
return fail ( " nothing selected to commit" ) ;
292
}
293
if partial & & snap.resolves {
294
return fail ( " a conflict resolution must be committed whole" ) ;
295
}
6d3eefbc Andy McCall 1 month ago ↶
296
297
self .run_hook (
298
" pre-commit" ,
299
& format! (
300
" {{ \" event\" :\" pre-commit\" ,\" message\" :{} ,\" paths\" :{} }} " ,
301
json_str ( message) ,
302
json_array ( snap.statuses.iter ( ) .map ( | s | format! ( " {{ \" path\" :{} ,\" status\" :{} }} " , json_str ( & s.path) , json_str ( & format! ( " {:?} " , s.status) ) ) ) )
303
) ,
304
) ? ;
98286406 Andy McCall 1 month ago ↶
305
let ( op_ref, op) = self .head_op ( ) ? ;
306
let mut deps: BTreeSet< ChangeHash> = referenced_changes ( & snap.wc, & snap.base) ;
6d3eefbc Andy McCall 1 month ago ↶
307
308
if self .config.semantic_deps {
309
let touched: Vec< String> = snap.statuses.iter ( ) .map ( | s | s.path.clone ( ) ) .collect ( ) ;
310
let by_node = snap.base_snapshot.tree.by_node ( ) ;
311
let mut candidates: Vec< ( ChangeId, ChangeHash, Vec< String> ) > = Vec :: new( ) ;
312
let mut seen: BTreeSet< ChangeHash> = BTreeSet:: new( ) ;
313
for h in deps.iter ( ) .chain ( snap.base.maximal ( & snap.wc.deps) .iter ( ) ) {
314
if ! seen.insert ( * h) {
315
continue ;
316
}
317
let c = self .store.get_change ( h) ? ;
318
let mut paths: BTreeSet< String> = BTreeSet:: new( ) ;
319
for n in c.tree_ops.iter ( ) .map ( | o | o.node ( ) ) .chain ( c.edits.iter ( ) .map ( | e | e.node ( ) ) ) {
320
if let Some ( e) = by_node.get ( & n) {
321
paths.insert ( e.path.clone ( ) ) ;
322
}
323
}
324
candidates.push ( ( c.change_id, * h, paths.into_iter ( ) .collect ( ) ) ) ;
325
}
326
for d in self .semantic_deps ( & snap.wc.change_id, & touched, & candidates) ? {
327
if snap.base.contains ( & d) {
328
deps.insert ( d) ;
329
}
330
}
331
}
98286406 Andy McCall 1 month ago ↶
332
333
334
335
deps.extend ( snap.contributors.iter ( ) .copied ( ) ) ;
336
for d in extra_deps {
337
if ! snap.base.contains ( d) {
338
return fail ( format! ( " --depends-on {d} : not in the working copy's base state" ) ) ;
339
}
340
deps.insert ( * d) ;
341
}
342
let mut change = Change {
957fee30 Andy McCall 1 month ago ↶
343
change_id: if partial { ChangeId:: random( ) } else { snap.wc.change_id } ,
344
deps: vec! [ ] ,
98286406 Andy McCall 1 month ago ↶
345
author: self .author ( ) ,
346
hlc: Hlc:: now( ) ,
347
message: message.trim_end ( ) .to_string ( ) ,
957fee30 Andy McCall 1 month ago ↶
348
tree_ops,
349
edits,
98286406 Andy McCall 1 month ago ↶
350
kind: if snap.resolves { ChangeKind:: Resolution } else { ChangeKind:: Normal } ,
351
} ;
957fee30 Andy McCall 1 month ago ↶
352
if partial {
353
354
355
deps = referenced_changes ( & change, & snap.base) ;
356
for d in extra_deps {
357
deps.insert ( * d) ;
358
}
359
}
360
change.deps = deps.into_iter ( ) .collect ( ) ;
98286406 Andy McCall 1 month ago ↶
361
change.normalize ( ) ;
614541c3 Andy McCall 1 month ago ↶
362
let hash = self .store_signed ( & change) ? ;
98286406 Andy McCall 1 month ago ↶
363
364
365
366
367
368
let old_heads = self .heads_of ( & op.view, & self .config.bookmark) ? ;
369
let old_base = snap.base.maximal ( & snap.wc.deps) ;
370
let mut all: Vec< ChangeHash> = old_heads.clone ( ) ;
371
all.extend ( old_base.iter ( ) .copied ( ) ) ;
372
all.push ( hash) ;
373
let mut st = self .load_state ( & all) ? ;
374
st.add ( change) ? ;
375
let mut with_new = old_heads.clone ( ) ;
376
with_new.push ( hash) ;
377
let new_heads = st.maximal ( & with_new) ;
252e8cb2 Andy McCall 1 month ago ↶
378
379
let since = self .latest_checkpoint ( & self .config.bookmark) ? .map ( | ( _, cp ) | cp.count ) .unwrap_or ( 0 ) ;
380
if self .config.checkpoint_interval > 0 & & ( st.len ( ) as u64 ) .saturating_sub ( since) > = self .config.checkpoint_interval {
381
let cp_state = self .load_state_full ( & new_heads) ? ;
382
self .write_checkpoint ( & self .config.bookmark, & cp_state, & new_heads) ? ;
383
}
98286406 Andy McCall 1 month ago ↶
384
let mut wc_base = old_base.clone ( ) ;
385
wc_base.push ( hash) ;
386
let wc_base = st.maximal ( & wc_base) ;
387
388
let wc = self .new_working_copy ( wc_base, " " ) ;
389
let wc_hash = self .store.put_change ( & wc) ? ;
390
let mut view = op.view.clone ( ) ;
391
view.bookmarks.insert ( self .config.bookmark.clone ( ) , self .store.put_state ( & StateObject:: new( new_heads) ) ? ) ;
392
view.workspaces.insert ( self .workspace, wc_hash) ;
393
self .record_op ( & [ op_ref] , & format! ( " oxy commit -m {message:?} " ) , view) ? ;
957fee30 Andy McCall 1 month ago ↶
394
if partial {
395
396
self .drop_manifest ( ) ;
397
} else {
398
self .rewrite_manifest_flags ( snap.base_snapshot.is_conflicted ( ) & & ! snap.resolves, false ) ? ;
399
}
d02e545d Andy McCall 1 month ago ↶
400
self .maybe_autopack ( ) ? ;
6d3eefbc Andy McCall 1 month ago ↶
401
let _ = self .run_hook ( " post-commit" , & format! ( " {{ \" event\" :\" post-commit\" ,\" change_id\" :{} ,\" hash\" :{} ,\" message\" :{} }} " , json_str ( & snap.wc.change_id.to_hex ( ) ) , json_str ( & hash.to_hex ( ) ) , json_str ( message) ) ) ;
98286406 Andy McCall 1 month ago ↶
402
Ok ( hash)
403
}
404
}
405
957fee30 Andy McCall 1 month ago ↶
406
407
# [ derive ( Clone, Debug ) ]
408
pub enum Selection {
409
All,
410
411
Paths( Vec< String> ) ,
412
413
Hunks( Vec< usize > ) ,
414
}
415
416
417
# [ derive ( Clone, Debug ) ]
418
pub struct Hunk {
419
pub index : usize ,
420
pub path : String,
421
422
pub text : String,
423
}
424
425
426
427
fn select_paths ( wc : & Change, base_snapshot : & Snapshot, paths : & [String]) -> ( Vec< TreeOp> , Vec< Edit> ) {
428
let selected = | p : & str | paths.iter ( ) .any ( | x | p = = x | | p.starts_with ( & format! ( " {} /" , x.trim_end_matches ( ' /' ) ) ) ) ;
429
let by_node = base_snapshot.tree.by_node ( ) ;
430
let mut new_names: HashMap< NodeId, ( NodeId, String, NodeKind) > = HashMap:: new( ) ;
431
for op in & wc.tree_ops {
432
if let TreeOp:: AddNode { node, parent, name, kind, .. } = op {
433
new_names.insert ( * node, ( * parent, name.clone ( ) , * kind) ) ;
434
}
435
}
436
let mut nodes: BTreeSet< NodeId> = BTreeSet:: new( ) ;
437
for n in wc.tree_ops.iter ( ) .map ( | o | o.node ( ) ) .chain ( wc.edits.iter ( ) .map ( | e | e.node ( ) ) ) {
438
if path_of_node ( & n, & by_node, & new_names) .is_some_and ( | p | selected ( & p) ) {
439
nodes.insert ( n) ;
440
}
441
}
442
443
let mut changed = true ;
444
while changed {
445
changed = false ;
446
for ( n, ( parent, _ , _ ) ) in & new_names {
447
if nodes.contains ( n) & & new_names.contains_key ( parent) & & nodes.insert ( * parent) {
448
changed = true ;
449
}
450
}
451
}
452
let ops = wc.tree_ops.iter ( ) .filter ( | o | nodes.contains ( & o.node ( ) ) ) .cloned ( ) .collect ( ) ;
453
let edits = wc.edits.iter ( ) .filter ( | e | nodes.contains ( & e.node ( ) ) ) .cloned ( ) .collect ( ) ;
454
( ops, edits)
455
}
456
457
458
459
460
fn select_hunks ( wc : & Change, indices : & [usize ]) -> ( Vec< TreeOp> , Vec< Edit> ) {
461
let n_ops = wc.tree_ops.len ( ) ;
462
let mut ops_sel = vec! [ false ; n_ops] ;
463
let mut edits_sel = vec! [ false ; wc.edits.len ( ) ] ;
464
for & i in indices {
465
if i < n_ops {
466
ops_sel[ i] = true ;
467
} else if i - n_ops < wc.edits.len ( ) {
468
edits_sel[ i - n_ops] = true ;
469
}
470
}
471
472
let mut per_node: HashMap< NodeId, u32 > = HashMap:: new( ) ;
473
let mut insert_ranges: Vec< Option< ( NodeId, u32 , u32 ) > > = Vec :: new( ) ;
474
for e in & wc.edits {
475
insert_ranges.push ( match e {
476
Edit:: Insert { node, lines, .. } => {
477
let start = per_node.entry ( * node) .or_default ( ) ;
478
let r = ( * node, * start, * start + lines.len ( ) as u32 ) ;
479
* start + = lines.len ( ) as u32 ;
480
Some ( r)
481
}
482
_ => None ,
483
} ) ;
484
}
485
let add_index: HashMap< NodeId, usize > = wc.tree_ops.iter ( ) .enumerate ( ) .filter_map ( | ( i , o ) | if let TreeOp:: AddNode { node, .. } = o { Some ( ( * node, i) ) } else { None } ) .collect ( ) ;
486
let insert_for = | node : & NodeId, index : u32 | -> Option< usize > { insert_ranges.iter ( ) .position ( | r | matches! ( r, Some ( ( n, a, b) ) if n = = node & & * a < = index & & index < * b) ) } ;
487
488
let mut changed = true ;
489
while changed {
490
changed = false ;
491
for i in 0 .. n_ops {
492
if ! ops_sel[ i] {
493
continue ;
494
}
495
if let TreeOp:: AddNode { parent, .. } = & wc.tree_ops[ i] {
496
if let Some ( & p) = add_index.get ( parent) {
497
if ! ops_sel[ p] {
498
ops_sel[ p] = true ;
499
changed = true ;
500
}
501
}
502
}
503
}
504
for j in 0 .. wc.edits.len ( ) {
505
if ! edits_sel[ j] {
506
continue ;
507
}
508
let e = & wc.edits[ j] ;
509
if let Some ( & a) = add_index.get ( & e.node ( ) ) {
510
if ! ops_sel[ a] {
511
ops_sel[ a] = true ;
512
changed = true ;
513
}
514
}
515
if let Edit:: Insert { node, after, before, .. } = e {
516
for r in [ after, before] {
517
if let AtomRef:: Atom { change, index } = r {
518
if change.is_zero ( ) {
519
if let Some ( k) = insert_for ( node, * index) {
520
if ! edits_sel[ k] {
521
edits_sel[ k] = true ;
522
changed = true ;
523
}
524
}
525
}
526
}
527
}
528
}
529
}
530
}
531
532
let mut remap: HashMap< ( NodeId, u32 ) , u32 > = HashMap:: new( ) ;
533
let mut new_counts: HashMap< NodeId, u32 > = HashMap:: new( ) ;
534
for ( j, r) in insert_ranges.iter ( ) .enumerate ( ) {
535
if let ( true , Some ( ( node, a, b) ) ) = ( edits_sel[ j] , r) {
536
let next = new_counts.entry ( * node) .or_default ( ) ;
537
for old in * a.. * b {
538
remap.insert ( ( * node, old) , * next) ;
539
* next + = 1 ;
540
}
541
}
542
}
543
let map_ref = | r : AtomRef, node : & NodeId| -> AtomRef {
544
match r {
545
AtomRef:: Atom { change, index } if change.is_zero ( ) => AtomRef:: Atom { change, index: remap.get ( & ( * node, index) ) .copied ( ) .unwrap_or ( index) } ,
546
other => other,
547
}
548
} ;
549
let ops = wc.tree_ops.iter ( ) .enumerate ( ) .filter ( | ( i , _) | ops_sel[ * i] ) .map ( | ( _, o ) | o.clone ( ) ) .collect ( ) ;
550
let edits = wc
551
.edits
552
.iter ( )
553
.enumerate ( )
554
.filter ( | ( j , _) | edits_sel[ * j] )
555
.map ( | ( _, e ) | match e {
556
Edit:: Insert { node, after, before, lines } => Edit:: Insert { node: * node, after: map_ref ( * after, node) , before: map_ref ( * before, node) , lines: lines.clone ( ) } ,
557
other => other.clone ( ) ,
558
} )
559
.collect ( ) ;
560
( ops, edits)
561
}
562
563
564
pub fn render_hunks ( change : & Change, base : & State, base_snapshot : & Snapshot) -> Vec< Hunk> {
565
let by_node = base_snapshot.tree.by_node ( ) ;
566
let mut new_names: HashMap< NodeId, ( NodeId, String, NodeKind) > = HashMap:: new( ) ;
567
for op in & change.tree_ops {
568
if let TreeOp:: AddNode { node, parent, name, kind, .. } = op {
569
new_names.insert ( * node, ( * parent, name.clone ( ) , * kind) ) ;
570
}
571
}
572
let mut out = Vec :: new( ) ;
573
for ( i, op) in change.tree_ops.iter ( ) .enumerate ( ) {
574
let one = Change { tree_ops: vec! [ op.clone ( ) ] , edits: vec! [ ] , .. change.clone ( ) } ;
575
let path = path_of_node ( & op.node ( ) , & by_node, & new_names) .unwrap_or_default ( ) ;
576
out.push ( Hunk { index: i, path, text: render_change ( & one, base, base_snapshot) } ) ;
577
}
578
for ( j, e) in change.edits.iter ( ) .enumerate ( ) {
579
let one = Change { tree_ops: vec! [ ] , edits: vec! [ e.clone ( ) ] , .. change.clone ( ) } ;
580
let path = path_of_node ( & e.node ( ) , & by_node, & new_names) .unwrap_or_default ( ) ;
581
out.push ( Hunk { index: change.tree_ops.len ( ) + j, path, text: render_change ( & one, base, base_snapshot) } ) ;
582
}
583
out
584
}
585
c6aa5bd9 Andy McCall 1 month ago ↶
586
587
# [ derive ( Clone, Debug ) ]
588
pub struct FileDiff {
589
pub path : String,
590
591
pub kind : String,
592
pub hunks : Vec< DiffHunk> ,
593
594
pub from : Option< String> ,
595
}
596
597
598
599
# [ derive ( Clone, Debug, Default ) ]
600
pub struct DiffHunk {
601
pub base_line : u32 ,
602
pub added : Vec< Vec< u8 > > ,
603
pub removed : Vec< ( u32 , Vec< u8 > ) > ,
604
}
605
606
impl Repo {
607
608
pub fn file_at ( & self , path : & str , heads : Option< & [ ChangeHash] > ) -> Result< Vec< u8 > > {
609
let snapshot = match heads {
610
Some ( h) => self .load_state ( h) ? .materialize ( ) ,
611
None => {
612
let ( _ , wc) = self .working_copy ( ) ? ;
613
self .load_state ( & wc.deps) ? .materialize ( )
614
}
615
} ;
616
let Some ( f) = snapshot.files.get ( path) else { return fail ( format! ( " {path} is not in that state" ) ) } ;
617
match & f.content {
618
FileContent:: Dir => fail ( format! ( " {path} is a directory" ) ) ,
619
FileContent:: Text { bytes, .. } => Ok ( bytes.clone ( ) ) ,
620
FileContent:: Opaque { blobs } => match blobs.first ( ) {
621
Some ( b) => self .read_blob ( b) ,
622
None => Ok ( vec! [ ] ) ,
623
} ,
624
}
625
}
626
627
628
629
630
pub fn discard ( & self , paths : & [String]) -> Result< usize > {
631
let snap = self .snapshot ( " oxy discard" ) ? ;
632
let under = | p : & str | paths.iter ( ) .any ( | x | p = = x | | p.starts_with ( & format! ( " {} /" , x.trim_end_matches ( ' /' ) ) ) ) ;
633
let attrs = Repo:: state_attributes( & snap.base_snapshot) ;
634
let mut n = 0 ;
635
for s in & snap.statuses {
636
if ! under ( & s.path) {
637
continue ;
638
}
639
match snap.base_snapshot.files.get ( & s.path) {
640
None => {
641
642
let full = self .root.join ( & s.path) ;
643
if full.is_dir ( ) {
644
let _ = fs:: remove_dir_all( & full) ;
645
} else {
646
let _ = fs:: remove_file( & full) ;
647
}
648
n + = 1 ;
649
}
650
Some ( f) => {
651
match & f.content {
652
FileContent:: Dir => fs:: create_dir_all( self .root.join ( & s.path) ) ? ,
653
FileContent:: Text { bytes, .. } => {
654
let on_disk: std:: borrow:: Cow< [ u8 ] > = if attrs.lookup ( & s.path) .eol = = Some ( crate :: attributes:: Eol:: Crlf) { crate :: attributes:: to_crlf( bytes) .into ( ) } else { bytes.as_slice ( ) .into ( ) } ;
655
self .write_file ( & s.path, & on_disk, f.mode.exec) ? ;
656
}
657
FileContent:: Opaque { blobs } => {
658
let bytes = match blobs.first ( ) {
659
Some ( b) => self .read_blob ( b) ? ,
660
None => vec! [ ] ,
661
} ;
662
if f.kind = = NodeKind:: Symlink {
663
self .write_symlink ( & s.path, & bytes) ? ;
664
} else if f.kind = = NodeKind:: File {
665
self .write_file ( & s.path, & bytes, f.mode.exec) ? ;
666
}
667
}
668
}
669
n + = 1 ;
670
}
671
}
672
}
673
674
for ( p, f) in & snap.base_snapshot.files {
675
if under ( p) & & ! self .root.join ( p) .exists ( ) & & f.kind ! = NodeKind:: Dir & & f.kind ! = NodeKind:: Subrepo & & ! snap.statuses.iter ( ) .any ( | s | s.path = = * p ) {
676
let bytes = match & f.content {
677
FileContent:: Text { bytes, .. } => bytes.clone ( ) ,
678
FileContent:: Opaque { blobs } => blobs.first ( ) .map ( | b | self .read_blob ( b) ) .transpose ( ) ? .unwrap_or_default ( ) ,
679
FileContent:: Dir => continue ,
680
} ;
681
if f.kind = = NodeKind:: Symlink { self .write_symlink ( p, & bytes) ? } else { self .write_file ( p, & bytes, f.mode.exec) ? }
682
n + = 1 ;
683
}
684
}
685
self .drop_manifest ( ) ;
686
Ok ( n)
687
}
688
689
690
691
692
pub fn discard_hunks ( & self , indices : & [usize ]) -> Result< usize > {
693
let ( snap, hunks) = self .hunks ( ) ? ;
694
let keep: Vec< usize > = hunks.iter ( ) .map ( | h | h.index ) .filter ( | i | ! indices.contains ( i) ) .collect ( ) ;
695
let ( ops, edits) = select_hunks ( & snap.wc, & keep) ;
696
let dropped_nodes: BTreeSet< NodeId> = hunks
697
.iter ( )
698
.filter ( | h | indices.contains ( & h.index) )
699
.map ( | h | if h.index < snap.wc.tree_ops.len ( ) { snap.wc.tree_ops[ h.index] .node ( ) } else { snap.wc.edits[ h.index - snap.wc.tree_ops.len ( ) ] .node ( ) } )
700
.collect ( ) ;
701
let mut partial = snap.wc.clone ( ) ;
702
partial.tree_ops = ops;
703
partial.edits = edits;
704
partial.normalize ( ) ;
705
let mut st = snap.base.clone ( ) ;
706
st.add_unchecked ( partial) ;
707
let target = st.materialize ( ) ;
708
let by_node_target = target.tree.by_node ( ) ;
709
let by_node_base = snap.base_snapshot.tree.by_node ( ) ;
710
let attrs = Repo:: state_attributes( & target) ;
711
let mut n = 0 ;
712
for node in dropped_nodes {
713
match by_node_target.get ( & node) {
714
Some ( e) => {
715
let f = & target.files[ & e.path] ;
716
match & f.content {
717
FileContent:: Dir => { }
718
FileContent:: Text { bytes, .. } => {
719
let on_disk: std:: borrow:: Cow< [ u8 ] > = if attrs.lookup ( & e.path) .eol = = Some ( crate :: attributes:: Eol:: Crlf) { crate :: attributes:: to_crlf( bytes) .into ( ) } else { bytes.as_slice ( ) .into ( ) } ;
720
self .write_file ( & e.path, & on_disk, f.mode.exec) ? ;
721
}
722
FileContent:: Opaque { blobs } => {
723
let bytes = blobs.first ( ) .map ( | b | self .read_blob ( b) ) .transpose ( ) ? .unwrap_or_default ( ) ;
724
if f.kind = = NodeKind:: Symlink { self .write_symlink ( & e.path, & bytes) ? } else { self .write_file ( & e.path, & bytes, f.mode.exec) ? }
725
}
726
}
727
728
if let Some ( old_path) = snap.statuses.iter ( ) .find ( | s | s.status = = ChangeStatus:: Renamed & & ! target.files.contains_key ( & s.path) ) .map ( | s | s.path.clone ( ) ) {
729
let _ = fs:: remove_file( self .root.join ( old_path) ) ;
730
}
731
n + = 1 ;
732
}
733
None => {
734
735
if let Some ( added) = snap.statuses.iter ( ) .find ( | s | s.status = = ChangeStatus:: Added & & ! target.files.contains_key ( & s.path) & & ! by_node_base.contains_key ( & node) ) {
736
let full = self .root.join ( & added.path) ;
737
if full.is_dir ( ) { let _ = fs:: remove_dir_all( & full) ; } else { let _ = fs:: remove_file( & full) ; }
738
n + = 1 ;
739
}
740
}
741
}
742
}
743
self .drop_manifest ( ) ;
744
Ok ( n)
745
}
746
747
748
749
pub fn diff_files ( & self , change : Option< & ChangeHash> ) -> Result< Vec< FileDiff> > {
750
let ( c, base) = match change {
751
Some ( h) => {
752
let c = self .store.get_change ( h) ? ;
753
let base = self .load_state ( & c.deps) ? ;
754
( c, base)
755
}
756
None => {
757
let snap = self .snapshot ( " oxy diff" ) ? ;
758
( snap.wc, snap.base)
759
}
760
} ;
761
let base_snapshot = base.materialize ( ) ;
762
Ok ( diff_files_of ( & c, & base, & base_snapshot) )
763
}
764
765
766
767
pub fn log_for_path ( & self , path : & str ) -> Result< Vec< LogEntry> > {
768
let entries = self .log ( ) ? ;
769
let ( _ , wc) = self .working_copy ( ) ? ;
770
let base_snapshot = self .load_state ( & wc.deps) ? .materialize ( ) ;
771
let heads = self .bookmark_heads ( ) ? ;
772
let state_snapshot = self .load_state ( & heads) ? .materialize ( ) ;
773
let node = base_snapshot.files.get ( path) .or_else ( | | state_snapshot.files.get ( path) ) .map ( | f | f.node ) ;
774
let Some ( node) = node else { return fail ( format! ( " {path} is not tracked" ) ) } ;
775
let mut out = Vec :: new( ) ;
776
for e in entries {
777
let Ok ( c) = self .store.get_change ( & e.hash) else { continue } ;
778
if c.tree_ops.iter ( ) .any ( | o | o.node ( ) = = node ) | | c.edits.iter ( ) .any ( | x | x.node ( ) = = node ) {
779
out.push ( e) ;
780
}
781
}
782
Ok ( out)
783
}
784
}
785
786
787
pub fn diff_files_of ( change : & Change, base : & State, base_snapshot : & Snapshot) -> Vec< FileDiff> {
788
let by_node = base_snapshot.tree.by_node ( ) ;
789
let mut new_names: HashMap< NodeId, ( NodeId, String, NodeKind) > = HashMap:: new( ) ;
790
for op in & change.tree_ops {
791
if let TreeOp:: AddNode { node, parent, name, kind, .. } = op {
792
new_names.insert ( * node, ( * parent, name.clone ( ) , * kind) ) ;
793
}
794
}
795
let path_of = | n : & NodeId| -> String { path_of_node ( n, & by_node, & new_names) .unwrap_or_else ( | | format! ( " <node {} >" , n.short ( 8 ) ) ) } ;
796
let mut files: BTreeMap< NodeId, FileDiff> = BTreeMap:: new( ) ;
797
let mut order: Vec< NodeId> = Vec :: new( ) ;
798
let mut entry = | files : & mut BTreeMap< NodeId, FileDiff> , n : NodeId, kind : & str | {
799
if let std:: collections:: btree_map:: Entry:: Vacant( v) = files.entry ( n) {
800
order.push ( n) ;
801
v.insert ( FileDiff { path: path_of ( & n) , kind: kind.to_string ( ) , hunks: vec! [ ] , from: None } ) ;
802
}
803
} ;
804
for op in & change.tree_ops {
805
match op {
806
TreeOp:: AddNode { node, kind, .. } => {
807
if * kind ! = NodeKind:: Dir {
808
entry ( & mut files, * node, " added" ) ;
809
}
810
}
811
TreeOp:: RemoveNode { node, .. } => {
812
entry ( & mut files, * node, " removed" ) ;
813
if let Some ( e) = by_node.get ( node) {
814
files.get_mut ( node) .unwrap ( ) .path = e.path.clone ( ) ;
815
}
816
}
817
TreeOp:: Move { node, parent, name, .. } => {
818
entry ( & mut files, * node, " renamed" ) ;
819
let f = files.get_mut ( node) .unwrap ( ) ;
820
f.from = by_node.get ( node) .map ( | e | e.path.clone ( ) ) ;
821
let dir = if parent.is_root ( ) { String :: new( ) } else { path_of_node ( parent, & by_node, & new_names) .unwrap_or_default ( ) } ;
822
f.path = if dir.is_empty ( ) { name.clone ( ) } else { format! ( " {dir} /{name} " ) } ;
823
}
824
TreeOp:: SetMode { node, .. } => entry ( & mut files, * node, " mode" ) ,
825
}
826
}
827
828
let mut line_of: HashMap< NodeId, HashMap< ( ChangeHash, u32 ) , u32 > > = HashMap:: new( ) ;
829
let mut lines_for = | n : & NodeId| -> HashMap< ( ChangeHash, u32 ) , u32 > {
830
line_of.entry ( * n) .or_insert_with ( | | base.live_lines ( n) .into_iter ( ) .enumerate ( ) .map ( | ( i , (k , _) ) | ( k, i as u32 + 1 ) ) .collect ( ) ).clone ( )
831
} ;
832
for e in & change.edits {
833
let n = e.node ( ) ;
834
let is_new = new_names.contains_key ( & n) ;
835
entry ( & mut files, n, if is_new { " added" } else { " modified" } ) ;
836
let f = files.get_mut ( & n) .unwrap ( ) ;
837
match e {
838
Edit:: Insert { after, lines, .. } => {
839
let map = lines_for ( & n) ;
840
let base_line = match after {
841
AtomRef:: Start => 0 ,
842
AtomRef:: End => map.len ( ) as u32 ,
843
AtomRef:: Atom { change, index } if change.is_zero ( ) => f.hunks.last ( ) .map ( | h | h.base_line ) .unwrap_or ( 0 ) ,
844
AtomRef:: Atom { change, index } => map.get ( & ( * change, * index) ) .copied ( ) .unwrap_or ( 0 ) ,
845
} ;
846
f.hunks.push ( DiffHunk { base_line, added: lines.clone ( ) , removed: vec! [ ] } ) ;
847
}
848
Edit:: Delete { atoms, .. } => {
849
let map = lines_for ( & n) ;
850
let mut removed = Vec :: new( ) ;
851
for r in atoms {
852
for i in r.from.. r.to {
853
let line = map.get ( & ( r.change, i) ) .copied ( ) .unwrap_or ( 0 ) ;
854
let text = base.file ( & n) .and_then ( | g | g.content ( & ( r.change, i) ) ) .unwrap_or ( b " " ) .to_vec ( ) ;
855
removed.push ( ( line, text) ) ;
856
}
857
}
858
let base_line = removed.first ( ) .map ( | ( l , _) | l.saturating_sub ( 1 ) ) .unwrap_or ( 0 ) ;
859
f.hunks.push ( DiffHunk { base_line, added: vec! [ ] , removed } ) ;
860
}
861
Edit:: Replace { .. } => {
862
if ! is_new {
863
f.kind = " replaced" .into ( ) ;
864
}
865
}
866
}
867
}
868
order.into_iter ( ) .filter_map ( | n | files.remove ( & n) ) .collect ( )
869
}
870
98286406 Andy McCall 1 month ago ↶
871
872
struct ChangeBuilder < 'a > {
873
base : & 'a State,
874
base_snapshot : & 'a Snapshot,
875
tree_ops : Vec< TreeOp> ,
876
edits : Vec< Edit> ,
877
statuses : Vec< PathStatus> ,
878
dir_nodes : HashMap< String, NodeId> ,
879
reuse : HashMap< String, ( NodeId, NodeKind) > ,
880
881
contributors : BTreeSet< ChangeHash> ,
882
resolves : bool ,
cdb5beba Andy McCall 1 month ago ↶
883
attrs : crate:: attributes:: Attributes,
b7f102fc Andy McCall 1 month ago ↶
884
blobs : HashMap< String, ObjectRef> ,
98286406 Andy McCall 1 month ago ↶
885
}
886
887
impl < 'a > ChangeBuilder < 'a > {
888
fn new ( base : & 'a State, base_snapshot : & 'a Snapshot, reuse : HashMap< String, ( NodeId, NodeKind) > ) -> Self {
889
let mut dir_nodes: HashMap< String, NodeId> = HashMap:: new( ) ;
890
dir_nodes.insert ( String :: new( ) , NodeId:: ROOT ) ;
891
for ( p, f) in & base_snapshot.files {
892
if f.kind = = NodeKind:: Dir {
893
dir_nodes.insert ( p.clone ( ) , f.node) ;
894
}
895
}
b7f102fc Andy McCall 1 month ago ↶
896
ChangeBuilder { base, base_snapshot, tree_ops: vec! [ ] , edits: vec! [ ] , statuses: vec! [ ] , dir_nodes, reuse, contributors: BTreeSet:: new( ) , resolves: false , attrs: crate :: attributes:: Attributes:: default( ) , blobs: HashMap:: new( ) }
98286406 Andy McCall 1 month ago ↶
897
}
898
cdb5beba Andy McCall 1 month ago ↶
899
fn with_attributes ( mut self , attrs : crate:: attributes:: Attributes) -> Self {
900
self .attrs = attrs;
901
self
902
}
903
904
905
b7f102fc Andy McCall 1 month ago ↶
906
907
fn blob_of ( & mut self , repo : & Repo, path : & str , e : & FsEntry, nocompress : bool ) -> Result< ObjectRef> {
908
let b = match e.blob {
909
Some ( b) => b,
910
None => repo.store_blob_for ( & e.content, nocompress) ? ,
911
} ;
912
self .blobs.insert ( path.to_string ( ) , b) ;
913
Ok ( b)
914
}
915
cdb5beba Andy McCall 1 month ago ↶
916
fn opaque_for ( & self , path : & str , e : & FsEntry) -> bool {
b7f102fc Andy McCall 1 month ago ↶
917
if e.blob.is_some ( ) {
918
return true ;
919
}
4dee277c Andy McCall 1 month ago ↶
920
if e.kind = = NodeKind:: Symlink | | e.kind = = NodeKind:: Subrepo {
cdb5beba Andy McCall 1 month ago ↶
921
return true ;
922
}
923
match self .attrs.lookup ( path) .binary {
924
Some ( true ) => true ,
925
Some ( false ) => is_binary ( & e.content) ,
926
None => is_binary ( & e.content) ,
927
}
928
}
929
930
931
fn stored_text ( & self , path : & str , content : & [u8 ]) -> Vec< u8 > {
932
if self .attrs.lookup ( path) .eol.is_some ( ) { crate :: attributes:: normalize_eol( content) } else { content.to_vec ( ) }
933
}
934
98286406 Andy McCall 1 month ago ↶
935
fn reused_node ( & self , path : & str , kind : NodeKind) -> Option< NodeId> {
936
self .reuse.get ( path) .filter ( | ( _, k ) | * k = = kind) .map ( | ( n , _) | * n)
937
}
938
939
fn ensure_dir ( & mut self , path : & str ) -> NodeId {
940
if let Some ( n) = self .dir_nodes.get ( path) {
941
return * n;
942
}
943
let mut parent = NodeId:: ROOT ;
944
let mut prefix = String :: new( ) ;
945
for comp in path.split ( ' /' ) {
946
if ! prefix.is_empty ( ) {
947
prefix.push ( ' /' ) ;
948
}
949
prefix.push_str ( comp) ;
950
parent = match self .dir_nodes.get ( & prefix) {
951
Some ( n) => * n,
952
None => {
953
let node = self .reused_node ( & prefix, NodeKind:: Dir) .unwrap_or_else ( NodeId:: random) ;
954
self .tree_ops.push ( TreeOp:: AddNode { node, kind: NodeKind:: Dir, mode: Mode:: default( ) , parent, name: comp.to_string ( ) } ) ;
955
self .dir_nodes.insert ( prefix.clone ( ) , node) ;
956
node
957
}
958
} ;
959
}
960
parent
961
}
962
963
fn add_path ( & mut self , repo : & Repo, path : & str , e : & FsEntry) -> Result< ( ) > {
964
let ( dir, name) = split_path ( path) ;
965
let parent = self .ensure_dir ( dir) ;
966
let node = self .reused_node ( path, e.kind) .unwrap_or_else ( NodeId:: random) ;
cdb5beba Andy McCall 1 month ago ↶
967
let opaque = self .opaque_for ( path, e) ;
968
let nocompress = self .attrs.lookup ( path) .nocompress;
98286406 Andy McCall 1 month ago ↶
969
self .tree_ops.push ( TreeOp:: AddNode { node, kind: e.kind, mode: Mode { exec: e.exec, opaque } , parent, name: name.to_string ( ) } ) ;
970
if opaque {
b7f102fc Andy McCall 1 month ago ↶
971
let blob = self .blob_of ( repo, path, e, nocompress) ? ;
98286406 Andy McCall 1 month ago ↶
972
self .edits.push ( Edit:: Replace { node, old: None , new: Some ( blob) } ) ;
973
} else if ! e.content.is_empty ( ) {
cdb5beba Andy McCall 1 month ago ↶
974
let content = self .stored_text ( path, & e.content) ;
98286406 Andy McCall 1 month ago ↶
975
self .edits.push ( Edit:: Insert {
976
node,
977
after: crate :: objects:: AtomRef:: Start,
978
before: crate :: objects:: AtomRef:: End,
cdb5beba Andy McCall 1 month ago ↶
979
lines: split_lines ( & content) .iter ( ) .map ( | l | l.to_vec ( ) ) .collect ( ) ,
98286406 Andy McCall 1 month ago ↶
980
} ) ;
981
}
982
self .statuses.push ( PathStatus { path: path.to_string ( ) , status: ChangeStatus:: Added } ) ;
983
Ok ( ( ) )
984
}
985
6e723b81 Andy McCall 1 month ago ↶
986
987
fn move_path ( & mut self , repo : & Repo, old : & str , new : & str , e : & FsEntry, expected : & crate:: state:: FileState) -> Result< ( ) > {
988
let entry = & self .base_snapshot.tree.entries[ old] ;
989
let ( dir, name) = split_path ( new) ;
990
let parent = self .ensure_dir ( dir) ;
991
self .tree_ops.push ( TreeOp:: Move { node: expected.node, old: op_ref ( entry.placement) , parent, name: name.to_string ( ) } ) ;
992
self .update_path ( repo, old, e, expected) ? ;
993
994
self .statuses.retain ( | s | s.path ! = old ) ;
995
self .statuses.push ( PathStatus { path: new.to_string ( ) , status: ChangeStatus:: Renamed } ) ;
996
Ok ( ( ) )
997
}
998
98286406 Andy McCall 1 month ago ↶
999
fn remove_path ( & mut self , path : & str , expected : & crate:: state:: FileState) {
1000
let entry = & self .base_snapshot.tree.entries[ path] ;
1001
self .tree_ops.push ( TreeOp:: RemoveNode { node: expected.node, placement: op_ref ( entry.placement) } ) ;
1002
if path.contains ( " .oxy-conflict-" ) | | expected.is_conflicted ( ) {
1003
self .resolves = true ;
1004
}
1005
self .statuses.push ( PathStatus { path: path.to_string ( ) , status: ChangeStatus:: Removed } ) ;
1006
}
1007
1008
fn update_path ( & mut self , repo : & Repo, path : & str , e : & FsEntry, expected : & crate:: state:: FileState) -> Result< ( ) > {
1009
if expected.kind ! = e.kind {
1010
self .remove_path ( path, expected) ;
1011
self .statuses.pop ( ) ;
1012
self .add_path ( repo, path, e) ? ;
1013
self .statuses.pop ( ) ;
1014
self .statuses.push ( PathStatus { path: path.to_string ( ) , status: ChangeStatus:: Modified } ) ;
1015
return Ok ( ( ) ) ;
1016
}
1017
let entry = & self .base_snapshot.tree.entries[ path] ;
1018
let node = expected.node;
1019
let mode_atom = entry.mode_atom.map ( op_ref) ;
1020
let mut status: Option< ChangeStatus> = None ;
cdb5beba Andy McCall 1 month ago ↶
1021
let wants_opaque = self .opaque_for ( path, e) ;
1022
let nocompress = self .attrs.lookup ( path) .nocompress;
1023
let disk_text = self .stored_text ( path, & e.content) ;
98286406 Andy McCall 1 month ago ↶
1024
match & expected.content {
1025
FileContent:: Dir => { }
1026
FileContent:: Text { bytes, materialized } => {
1027
if materialized.is_conflicted ( ) {
cdb5beba Andy McCall 1 month ago ↶
1028
if bytes = = & disk_text {
98286406 Andy McCall 1 month ago ↶
1029
return Ok ( ( ) ) ;
1030
}
1031
if has_conflict_markers ( & e.content) {
1032
self .statuses.push ( PathStatus { path: path.to_string ( ) , status: ChangeStatus:: ConflictEdited } ) ;
1033
return Ok ( ( ) ) ;
1034
}
1035
1036
1037
1038
1039
let regions = self .base.conflict_regions ( & node) ;
cdb5beba Andy McCall 1 month ago ↶
1040
let mut edits = text_edits ( node, & regions.plain, & disk_text) ;
98286406 Andy McCall 1 month ago ↶
1041
if ! regions.region_atoms.is_empty ( ) {
1042
edits.insert ( 0 , Edit:: Delete { node, atoms: ranges ( & regions.region_atoms) } ) ;
1043
}
1044
self .edits.extend ( edits) ;
1045
self .contributors.extend ( regions.contributors) ;
1046
self .resolves = true ;
1047
if e.exec ! = expected.mode.exec {
1048
if let Some ( old) = mode_atom {
1049
self .tree_ops.push ( TreeOp:: SetMode { node, old, mode: Mode { exec: e.exec, opaque: false } } ) ;
1050
}
1051
}
1052
self .statuses.push ( PathStatus { path: path.to_string ( ) , status: ChangeStatus:: Resolved } ) ;
1053
return Ok ( ( ) ) ;
1054
}
cdb5beba Andy McCall 1 month ago ↶
1055
if wants_opaque {
1056
1057
if let Some ( old) = mode_atom {
1058
self .tree_ops.push ( TreeOp:: SetMode { node, old, mode: Mode { exec: e.exec, opaque: true } } ) ;
1059
}
1060
let live: Vec< _ > = repo_live_keys ( self .base, & node) ;
1061
if ! live.is_empty ( ) {
1062
self .edits.push ( Edit:: Delete { node, atoms: ranges ( & live) } ) ;
1063
}
b7f102fc Andy McCall 1 month ago ↶
1064
let blob = self .blob_of ( repo, path, e, nocompress) ? ;
cdb5beba Andy McCall 1 month ago ↶
1065
self .edits.push ( Edit:: Replace { node, old: None , new: Some ( blob) } ) ;
1066
status = Some ( ChangeStatus:: Modified) ;
1067
} else if bytes ! = & disk_text {
1068
let old = self .base.live_lines ( & node) ;
1069
let edits = text_edits ( node, & old, & disk_text) ;
1070
if ! edits.is_empty ( ) {
1071
self .edits.extend ( edits) ;
98286406 Andy McCall 1 month ago ↶
1072
status = Some ( ChangeStatus:: Modified) ;
1073
}
1074
}
cdb5beba Andy McCall 1 month ago ↶
1075
let switched_to_opaque = wants_opaque;
98286406 Andy McCall 1 month ago ↶
1076
if ! switched_to_opaque & & e.exec ! = expected.mode.exec {
1077
if let Some ( old) = mode_atom {
1078
self .tree_ops.push ( TreeOp:: SetMode { node, old, mode: Mode { exec: e.exec, opaque: false } } ) ;
1079
status.get_or_insert ( ChangeStatus:: ModeChanged) ;
1080
}
1081
}
1082
}
1083
FileContent:: Opaque { blobs } => {
b7f102fc Andy McCall 1 month ago ↶
1084
let new_ref = e.blob.unwrap_or_else ( | | Repo:: blob_ref( & e.content) ) ;
1085
if e.kind = = NodeKind:: File & & wants_opaque {
1086
1087
self .blobs.insert ( path.to_string ( ) , new_ref) ;
1088
}
98286406 Andy McCall 1 month ago ↶
1089
if blobs.len ( ) > 1 {
1090
if ! blobs.contains ( & new_ref) {
1091
self .statuses.push ( PathStatus { path: path.to_string ( ) , status: ChangeStatus:: ConflictEdited } ) ;
1092
}
1093
return Ok ( ( ) ) ;
1094
}
1095
let current = blobs.first ( ) .copied ( ) ;
1096
if current ! = Some ( new_ref) {
cdb5beba Andy McCall 1 month ago ↶
1097
if e.kind = = NodeKind:: File & & ! wants_opaque {
98286406 Andy McCall 1 month ago ↶
1098
1099
if let Some ( old) = mode_atom {
1100
self .tree_ops.push ( TreeOp:: SetMode { node, old, mode: Mode { exec: e.exec, opaque: false } } ) ;
1101
}
1102
self .edits.push ( Edit:: Replace { node, old: current, new: None } ) ;
1103
if ! e.content.is_empty ( ) {
1104
self .edits.push ( Edit:: Insert {
1105
node,
1106
after: crate :: objects:: AtomRef:: Start,
1107
before: crate :: objects:: AtomRef:: End,
1108
lines: split_lines ( & e.content) .iter ( ) .map ( | l | l.to_vec ( ) ) .collect ( ) ,
1109
} ) ;
1110
}
1111
} else {
b7f102fc Andy McCall 1 month ago ↶
1112
let blob = self .blob_of ( repo, path, e, nocompress) ? ;
98286406 Andy McCall 1 month ago ↶
1113
self .edits.push ( Edit:: Replace { node, old: current, new: Some ( blob) } ) ;
1114
}
1115
status = Some ( ChangeStatus:: Modified) ;
1116
} else if e.kind = = NodeKind:: File & & e.exec ! = expected.mode.exec {
1117
if let Some ( old) = mode_atom {
1118
self .tree_ops.push ( TreeOp:: SetMode { node, old, mode: Mode { exec: e.exec, opaque: true } } ) ;
1119
status = Some ( ChangeStatus:: ModeChanged) ;
1120
}
1121
}
1122
}
1123
}
1124
if let Some ( s) = status {
1125
self .statuses.push ( PathStatus { path: path.to_string ( ) , status: s } ) ;
1126
}
1127
Ok ( ( ) )
1128
}
1129
}
1130
1131
fn repo_live_keys ( base : & State, node : & NodeId) -> Vec< crate:: graph:: AtomKey> {
1132
base.live_lines ( node) .into_iter ( ) .map ( | ( k , _) | k ) .collect ( )
1133
}
1134
1135
1136
1137
pub fn referenced_changes ( change : & Change, base : & State) -> BTreeSet< ChangeHash> {
1138
let mut out = BTreeSet:: new( ) ;
1139
let opref = | r : & OpRef, out : & mut BTreeSet< ChangeHash> | {
1140
if ! r.change.is_zero ( ) {
1141
out.insert ( r.change) ;
1142
}
1143
} ;
1144
let mut nodes: Vec< NodeId> = Vec :: new( ) ;
1145
for op in & change.tree_ops {
1146
match op {
1147
TreeOp:: AddNode { parent, .. } => nodes.push ( * parent) ,
1148
TreeOp:: RemoveNode { node, placement } => {
1149
nodes.push ( * node) ;
1150
opref ( placement, & mut out) ;
1151
}
1152
TreeOp:: Move { node, old, parent, .. } => {
1153
nodes.push ( * node) ;
1154
nodes.push ( * parent) ;
1155
opref ( old, & mut out) ;
1156
}
1157
TreeOp:: SetMode { node, old, .. } => {
1158
nodes.push ( * node) ;
1159
opref ( old, & mut out) ;
1160
}
1161
}
1162
}
1163
for e in & change.edits {
1164
nodes.push ( e.node ( ) ) ;
1165
match e {
1166
Edit:: Insert { after, before, .. } => {
1167
for r in [ after, before] {
1168
if let crate :: objects:: AtomRef:: Atom { change, .. } = r {
1169
if ! change.is_zero ( ) {
1170
out.insert ( * change) ;
1171
}
1172
}
1173
}
1174
}
1175
Edit:: Delete { atoms, .. } => {
1176
for r in atoms {
1177
if ! r.change.is_zero ( ) {
1178
out.insert ( r.change) ;
1179
}
1180
}
1181
}
1182
Edit:: Replace { .. } => { }
1183
}
1184
}
1185
for n in nodes {
1186
if let Some ( h) = base.tree ( ) .introducer ( & n) {
1187
out.insert ( h) ;
1188
}
1189
}
1190
out
1191
}
1192
1193
1194
pub fn added_node_paths ( change : & Change, base_snapshot : & Snapshot) -> HashMap< String, ( NodeId, NodeKind) > {
1195
let by_node = base_snapshot.tree.by_node ( ) ;
1196
let mut new_names: HashMap< NodeId, ( NodeId, String, NodeKind) > = HashMap:: new( ) ;
1197
for op in & change.tree_ops {
1198
if let TreeOp:: AddNode { node, parent, name, kind, .. } = op {
1199
new_names.insert ( * node, ( * parent, name.clone ( ) , * kind) ) ;
1200
}
1201
}
1202
let mut out = HashMap:: new( ) ;
1203
for ( node, ( _ , _ , kind) ) in & new_names {
1204
if let Some ( path) = path_of_node ( node, & by_node, & new_names) {
1205
out.insert ( path, ( * node, * kind) ) ;
1206
}
1207
}
1208
out
1209
}
1210
1211
fn path_of_node (
1212
n : & NodeId,
1213
by_node : & HashMap< NodeId, & crate:: tree:: TreeEntry> ,
1214
new_names : & HashMap< NodeId, ( NodeId, String, NodeKind) > ,
1215
) -> Option< String> {
1216
if let Some ( e) = by_node.get ( n) {
1217
return Some ( e.path.clone ( ) ) ;
1218
}
1219
let mut parts = Vec :: new( ) ;
1220
let mut cur = * n;
1221
loop {
1222
if cur.is_root ( ) {
1223
break ;
1224
}
1225
if let Some ( e) = by_node.get ( & cur) {
1226
if ! e.path.is_empty ( ) {
1227
parts.push ( e.path.clone ( ) ) ;
1228
}
1229
break ;
1230
}
1231
let ( parent, name, _ ) = new_names.get ( & cur) ? ;
1232
parts.push ( name.clone ( ) ) ;
1233
cur = * parent;
1234
}
1235
parts.reverse ( ) ;
1236
Some ( parts.join ( " /" ) )
1237
}
1238
1239
1240
pub fn render_change ( change : & Change, base : & State, base_snapshot : & Snapshot) -> String {
1241
let by_node = base_snapshot.tree.by_node ( ) ;
1242
let mut new_names: HashMap< NodeId, ( NodeId, String, NodeKind) > = HashMap:: new( ) ;
1243
for op in & change.tree_ops {
1244
if let TreeOp:: AddNode { node, parent, name, kind, .. } = op {
1245
new_names.insert ( * node, ( * parent, name.clone ( ) , * kind) ) ;
1246
}
1247
}
1248
let path_of = | n : & NodeId| -> String { path_of_node ( n, & by_node, & new_names) .unwrap_or_else ( | | format! ( " <node {} >" , n.short ( 8 ) ) ) } ;
1249
1250
let mut out = String :: new( ) ;
1251
for op in & change.tree_ops {
1252
match op {
1253
TreeOp:: AddNode { node, kind, mode, .. } => {
1254
out.push_str ( & format! ( " A {} ({:?} {} {} )\n " , path_of ( node) , kind, if mode.exec { " , exec" } else { " " } , if mode.opaque { " , opaque" } else { " " } ) ) ;
1255
}
1256
TreeOp:: RemoveNode { node, .. } => out.push_str ( & format! ( " D {} \n " , path_of ( node) ) ) ,
1257
TreeOp:: Move { node, name, .. } => out.push_str ( & format! ( " R {} -> {} \n " , path_of ( node) , name) ) ,
1258
TreeOp:: SetMode { node, mode, .. } => {
1259
out.push_str ( & format! ( " M {} mode: exec={} opaque={} \n " , path_of ( node) , mode.exec, mode.opaque) ) ;
1260
}
1261
}
1262
}
1263
let mut last_node: Option< NodeId> = None ;
1264
for e in & change.edits {
1265
let n = e.node ( ) ;
1266
if last_node ! = Some ( n) {
1267
out.push_str ( & format! ( " --- {} \n " , path_of ( & n) ) ) ;
1268
last_node = Some ( n) ;
1269
}
1270
match e {
1271
Edit:: Insert { lines, .. } => {
1272
for l in lines {
1273
out.push ( ' +' ) ;
1274
out.push_str ( & String :: from_utf8_lossy( l) ) ;
1275
if ! l.ends_with ( b " \n " ) {
1276
out.push ( ' \n ' ) ;
1277
}
1278
}
1279
}
1280
Edit:: Delete { atoms, .. } => {
1281
for r in atoms {
1282
for i in r.from.. r.to {
1283
let content = base.file ( & n) .and_then ( | g | g.content ( & ( r.change, i) ) ) .unwrap_or ( b " <unknown>\n " ) ;
1284
out.push ( ' -' ) ;
1285
out.push_str ( & String :: from_utf8_lossy( content) ) ;
1286
if ! content.ends_with ( b " \n " ) {
1287
out.push ( ' \n ' ) ;
1288
}
1289
}
1290
}
1291
}
1292
Edit:: Replace { old, new, .. } => {
1293
out.push_str ( & format! (
1294
" opaque content {} -> {} \n " ,
1295
old.map ( | o | o.short ( 12 ) ) .unwrap_or_else ( | | " none" .into ( ) ) ,
1296
new.map ( | o | o.short ( 12 ) ) .unwrap_or_else ( | | " none" .into ( ) )
1297
) ) ;
1298
}
1299
}
1300
}
1301
out
1302
}
6e723b81 Andy McCall 1 month ago ↶
1303
1304
1305
1306
1307
const RENAME_THRESHOLD : u32 = 50 ;
1308
const RENAME_LIMIT : usize = 10_000 ;
1309
1310
fn detect_renames ( base_snapshot : & Snapshot, fs_entries : & BTreeMap< String, FsEntry> , in_scope : impl Fn( &str ) -> bool ) -> Vec< ( String, String) > {
1311
let removed: Vec< ( & String, & crate:: state:: FileState) > = base_snapshot
1312
.files
1313
.iter ( )
1314
.filter ( | ( p , f ) | f.kind ! = NodeKind:: Dir & & ! fs_entries.contains_key ( * p) & & in_scope ( p) & & ! f.is_conflicted ( ) )
1315
.collect ( ) ;
1316
let added: Vec< ( & String, & FsEntry) > = fs_entries.iter ( ) .filter ( | ( p , e ) | e.kind ! = NodeKind:: Dir & & ! base_snapshot.files.contains_key ( * p) ) .collect ( ) ;
1317
if removed.is_empty ( ) | | added.is_empty ( ) {
1318
return vec! [ ] ;
1319
}
1320
let exact_only = removed.len ( ) * added.len ( ) > RENAME_LIMIT ;
1321
let old_content = | f : & crate:: state:: FileState| -> Option< Vec< u8 > > {
1322
match & f.content {
1323
FileContent:: Text { bytes, .. } => Some ( bytes.clone ( ) ) ,
1324
_ => None ,
1325
}
1326
} ;
1327
let mut scored: Vec< ( u32 , usize , usize ) > = Vec :: new( ) ;
1328
for ( ri, ( _ , f) ) in removed.iter ( ) .enumerate ( ) {
1329
let old = old_content ( f) ;
1330
let old_blob = match & f.content {
1331
FileContent:: Opaque { blobs } if blobs.len ( ) = = 1 => Some ( blobs[ 0 ] ) ,
1332
_ => None ,
1333
} ;
1334
for ( ai, ( _ , e) ) in added.iter ( ) .enumerate ( ) {
1335
if e.kind ! = f.kind {
1336
continue ;
1337
}
1338
let score = match ( & old, old_blob) {
1339
( Some ( o) , _ ) => {
1340
if exact_only {
1341
if * o = = e.content { 100 } else { 0 }
1342
} else {
1343
similarity ( o, & e.content)
1344
}
1345
}
b7f102fc Andy McCall 1 month ago ↶
1346
( None , Some ( b) ) if e.blob.unwrap_or_else ( | | Repo:: blob_ref( & e.content) ) = = b => 100 ,
6e723b81 Andy McCall 1 month ago ↶
1347
_ => 0 ,
1348
} ;
1349
if score > = RENAME_THRESHOLD {
1350
scored.push ( ( score, ri, ai) ) ;
1351
}
1352
}
1353
}
1354
scored.sort_by ( | a , b | b.0. cmp ( & a.0 ) .then ( removed[ a.1 ] .0. cmp ( removed[ b.1 ] .0 ) ) .then ( added[ a.2 ] .0. cmp ( added[ b.2 ] .0 ) ) ) ;
1355
let mut used_r = vec! [ false ; removed.len ( ) ] ;
1356
let mut used_a = vec! [ false ; added.len ( ) ] ;
1357
let mut out = Vec :: new( ) ;
1358
for ( _ , ri, ai) in scored {
1359
if used_r[ ri] | | used_a[ ai] {
1360
continue ;
1361
}
1362
used_r[ ri] = true ;
1363
used_a[ ai] = true ;
1364
out.push ( ( removed[ ri] .0. clone ( ) , added[ ai] .0. clone ( ) ) ) ;
1365
}
1366
out
1367
}
1368
1369
1370
1371
pub fn similarity ( a : & [u8 ], b : & [u8 ]) -> u32 {
1372
if a = = b {
1373
return 100 ;
1374
}
1375
if a.is_empty ( ) | | b.is_empty ( ) {
1376
return 0 ;
1377
}
1378
let mut counts: HashMap< & [ u8 ] , usize > = HashMap:: new( ) ;
1379
for l in split_lines ( a) {
1380
* counts.entry ( l) .or_default ( ) + = 1 ;
1381
}
1382
let mut shared = 0 usize ;
1383
for l in split_lines ( b) {
1384
if let Some ( n) = counts.get_mut ( l) {
1385
if * n > 0 {
1386
* n - = 1 ;
1387
shared + = l.len ( ) ;
1388
}
1389
}
1390
}
1391
( shared * 100 / a.len ( ) .max ( b.len ( ) ) ) as u32
1392
}
1393