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> > {
e02bf0cd Andy McCall 1 month ago ↶
768
let heads = self .bookmark_heads ( ) ? ;
769
if self .is_bare ( ) {
770
return self .log_for_path_in ( path, & heads) ;
771
}
c6aa5bd9 Andy McCall 1 month ago ↶
772
let entries = self .log ( ) ? ;
773
let ( _ , wc) = self .working_copy ( ) ? ;
774
let base_snapshot = self .load_state ( & wc.deps) ? .materialize ( ) ;
775
let state_snapshot = self .load_state ( & heads) ? .materialize ( ) ;
776
let node = base_snapshot.files.get ( path) .or_else ( | | state_snapshot.files.get ( path) ) .map ( | f | f.node ) ;
777
let Some ( node) = node else { return fail ( format! ( " {path} is not tracked" ) ) } ;
e02bf0cd Andy McCall 1 month ago ↶
778
Ok ( self .entries_touching ( entries, node) )
779
}
780
781
782
783
pub fn log_for_path_in ( & self , path : & str , heads : & [ChangeHash]) -> Result< Vec< LogEntry> > {
784
let entries = self .log_state ( heads) ? ;
785
let snapshot = self .load_state ( heads) ? .materialize ( ) ;
786
let Some ( node) = snapshot.files.get ( path) .map ( | f | f.node ) else { return fail ( format! ( " {path} is not in that state" ) ) } ;
787
Ok ( self .entries_touching ( entries, node) )
788
}
789
790
fn entries_touching ( & self , entries : Vec< LogEntry> , node : NodeId) -> Vec< LogEntry> {
c6aa5bd9 Andy McCall 1 month ago ↶
791
let mut out = Vec :: new( ) ;
792
for e in entries {
793
let Ok ( c) = self .store.get_change ( & e.hash) else { continue } ;
794
if c.tree_ops.iter ( ) .any ( | o | o.node ( ) = = node ) | | c.edits.iter ( ) .any ( | x | x.node ( ) = = node ) {
795
out.push ( e) ;
796
}
797
}
e02bf0cd Andy McCall 1 month ago ↶
798
out
c6aa5bd9 Andy McCall 1 month ago ↶
799
}
800
}
801
802
803
pub fn diff_files_of ( change : & Change, base : & State, base_snapshot : & Snapshot) -> Vec< FileDiff> {
804
let by_node = base_snapshot.tree.by_node ( ) ;
805
let mut new_names: HashMap< NodeId, ( NodeId, String, NodeKind) > = HashMap:: new( ) ;
806
for op in & change.tree_ops {
807
if let TreeOp:: AddNode { node, parent, name, kind, .. } = op {
808
new_names.insert ( * node, ( * parent, name.clone ( ) , * kind) ) ;
809
}
810
}
811
let path_of = | n : & NodeId| -> String { path_of_node ( n, & by_node, & new_names) .unwrap_or_else ( | | format! ( " <node {} >" , n.short ( 8 ) ) ) } ;
812
let mut files: BTreeMap< NodeId, FileDiff> = BTreeMap:: new( ) ;
813
let mut order: Vec< NodeId> = Vec :: new( ) ;
814
let mut entry = | files : & mut BTreeMap< NodeId, FileDiff> , n : NodeId, kind : & str | {
815
if let std:: collections:: btree_map:: Entry:: Vacant( v) = files.entry ( n) {
816
order.push ( n) ;
817
v.insert ( FileDiff { path: path_of ( & n) , kind: kind.to_string ( ) , hunks: vec! [ ] , from: None } ) ;
818
}
819
} ;
820
for op in & change.tree_ops {
821
match op {
822
TreeOp:: AddNode { node, kind, .. } => {
823
if * kind ! = NodeKind:: Dir {
824
entry ( & mut files, * node, " added" ) ;
825
}
826
}
827
TreeOp:: RemoveNode { node, .. } => {
828
entry ( & mut files, * node, " removed" ) ;
829
if let Some ( e) = by_node.get ( node) {
830
files.get_mut ( node) .unwrap ( ) .path = e.path.clone ( ) ;
831
}
832
}
833
TreeOp:: Move { node, parent, name, .. } => {
834
entry ( & mut files, * node, " renamed" ) ;
835
let f = files.get_mut ( node) .unwrap ( ) ;
836
f.from = by_node.get ( node) .map ( | e | e.path.clone ( ) ) ;
837
let dir = if parent.is_root ( ) { String :: new( ) } else { path_of_node ( parent, & by_node, & new_names) .unwrap_or_default ( ) } ;
838
f.path = if dir.is_empty ( ) { name.clone ( ) } else { format! ( " {dir} /{name} " ) } ;
839
}
840
TreeOp:: SetMode { node, .. } => entry ( & mut files, * node, " mode" ) ,
841
}
842
}
843
844
let mut line_of: HashMap< NodeId, HashMap< ( ChangeHash, u32 ) , u32 > > = HashMap:: new( ) ;
845
let mut lines_for = | n : & NodeId| -> HashMap< ( ChangeHash, u32 ) , u32 > {
846
line_of.entry ( * n) .or_insert_with ( | | base.live_lines ( n) .into_iter ( ) .enumerate ( ) .map ( | ( i , (k , _) ) | ( k, i as u32 + 1 ) ) .collect ( ) ).clone ( )
847
} ;
848
for e in & change.edits {
849
let n = e.node ( ) ;
850
let is_new = new_names.contains_key ( & n) ;
851
entry ( & mut files, n, if is_new { " added" } else { " modified" } ) ;
852
let f = files.get_mut ( & n) .unwrap ( ) ;
853
match e {
854
Edit:: Insert { after, lines, .. } => {
855
let map = lines_for ( & n) ;
856
let base_line = match after {
857
AtomRef:: Start => 0 ,
858
AtomRef:: End => map.len ( ) as u32 ,
859
AtomRef:: Atom { change, index } if change.is_zero ( ) => f.hunks.last ( ) .map ( | h | h.base_line ) .unwrap_or ( 0 ) ,
860
AtomRef:: Atom { change, index } => map.get ( & ( * change, * index) ) .copied ( ) .unwrap_or ( 0 ) ,
861
} ;
862
f.hunks.push ( DiffHunk { base_line, added: lines.clone ( ) , removed: vec! [ ] } ) ;
863
}
864
Edit:: Delete { atoms, .. } => {
865
let map = lines_for ( & n) ;
866
let mut removed = Vec :: new( ) ;
867
for r in atoms {
868
for i in r.from.. r.to {
869
let line = map.get ( & ( r.change, i) ) .copied ( ) .unwrap_or ( 0 ) ;
870
let text = base.file ( & n) .and_then ( | g | g.content ( & ( r.change, i) ) ) .unwrap_or ( b " " ) .to_vec ( ) ;
871
removed.push ( ( line, text) ) ;
872
}
873
}
874
let base_line = removed.first ( ) .map ( | ( l , _) | l.saturating_sub ( 1 ) ) .unwrap_or ( 0 ) ;
875
f.hunks.push ( DiffHunk { base_line, added: vec! [ ] , removed } ) ;
876
}
877
Edit:: Replace { .. } => {
878
if ! is_new {
879
f.kind = " replaced" .into ( ) ;
880
}
881
}
882
}
883
}
884
order.into_iter ( ) .filter_map ( | n | files.remove ( & n) ) .collect ( )
885
}
886
98286406 Andy McCall 1 month ago ↶
887
888
struct ChangeBuilder < 'a > {
889
base : & 'a State,
890
base_snapshot : & 'a Snapshot,
891
tree_ops : Vec< TreeOp> ,
892
edits : Vec< Edit> ,
893
statuses : Vec< PathStatus> ,
894
dir_nodes : HashMap< String, NodeId> ,
895
reuse : HashMap< String, ( NodeId, NodeKind) > ,
896
897
contributors : BTreeSet< ChangeHash> ,
898
resolves : bool ,
cdb5beba Andy McCall 1 month ago ↶
899
attrs : crate:: attributes:: Attributes,
b7f102fc Andy McCall 1 month ago ↶
900
blobs : HashMap< String, ObjectRef> ,
98286406 Andy McCall 1 month ago ↶
901
}
902
903
impl < 'a > ChangeBuilder < 'a > {
904
fn new ( base : & 'a State, base_snapshot : & 'a Snapshot, reuse : HashMap< String, ( NodeId, NodeKind) > ) -> Self {
905
let mut dir_nodes: HashMap< String, NodeId> = HashMap:: new( ) ;
906
dir_nodes.insert ( String :: new( ) , NodeId:: ROOT ) ;
907
for ( p, f) in & base_snapshot.files {
908
if f.kind = = NodeKind:: Dir {
909
dir_nodes.insert ( p.clone ( ) , f.node) ;
910
}
911
}
b7f102fc Andy McCall 1 month ago ↶
912
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 ↶
913
}
914
cdb5beba Andy McCall 1 month ago ↶
915
fn with_attributes ( mut self , attrs : crate:: attributes:: Attributes) -> Self {
916
self .attrs = attrs;
917
self
918
}
919
920
921
b7f102fc Andy McCall 1 month ago ↶
922
923
fn blob_of ( & mut self , repo : & Repo, path : & str , e : & FsEntry, nocompress : bool ) -> Result< ObjectRef> {
924
let b = match e.blob {
925
Some ( b) => b,
926
None => repo.store_blob_for ( & e.content, nocompress) ? ,
927
} ;
928
self .blobs.insert ( path.to_string ( ) , b) ;
929
Ok ( b)
930
}
931
cdb5beba Andy McCall 1 month ago ↶
932
fn opaque_for ( & self , path : & str , e : & FsEntry) -> bool {
b7f102fc Andy McCall 1 month ago ↶
933
if e.blob.is_some ( ) {
934
return true ;
935
}
4dee277c Andy McCall 1 month ago ↶
936
if e.kind = = NodeKind:: Symlink | | e.kind = = NodeKind:: Subrepo {
cdb5beba Andy McCall 1 month ago ↶
937
return true ;
938
}
939
match self .attrs.lookup ( path) .binary {
940
Some ( true ) => true ,
941
Some ( false ) => is_binary ( & e.content) ,
942
None => is_binary ( & e.content) ,
943
}
944
}
945
946
947
fn stored_text ( & self , path : & str , content : & [u8 ]) -> Vec< u8 > {
948
if self .attrs.lookup ( path) .eol.is_some ( ) { crate :: attributes:: normalize_eol( content) } else { content.to_vec ( ) }
949
}
950
98286406 Andy McCall 1 month ago ↶
951
fn reused_node ( & self , path : & str , kind : NodeKind) -> Option< NodeId> {
952
self .reuse.get ( path) .filter ( | ( _, k ) | * k = = kind) .map ( | ( n , _) | * n)
953
}
954
955
fn ensure_dir ( & mut self , path : & str ) -> NodeId {
956
if let Some ( n) = self .dir_nodes.get ( path) {
957
return * n;
958
}
959
let mut parent = NodeId:: ROOT ;
960
let mut prefix = String :: new( ) ;
961
for comp in path.split ( ' /' ) {
962
if ! prefix.is_empty ( ) {
963
prefix.push ( ' /' ) ;
964
}
965
prefix.push_str ( comp) ;
966
parent = match self .dir_nodes.get ( & prefix) {
967
Some ( n) => * n,
968
None => {
969
let node = self .reused_node ( & prefix, NodeKind:: Dir) .unwrap_or_else ( NodeId:: random) ;
970
self .tree_ops.push ( TreeOp:: AddNode { node, kind: NodeKind:: Dir, mode: Mode:: default( ) , parent, name: comp.to_string ( ) } ) ;
971
self .dir_nodes.insert ( prefix.clone ( ) , node) ;
972
node
973
}
974
} ;
975
}
976
parent
977
}
978
979
fn add_path ( & mut self , repo : & Repo, path : & str , e : & FsEntry) -> Result< ( ) > {
980
let ( dir, name) = split_path ( path) ;
981
let parent = self .ensure_dir ( dir) ;
982
let node = self .reused_node ( path, e.kind) .unwrap_or_else ( NodeId:: random) ;
cdb5beba Andy McCall 1 month ago ↶
983
let opaque = self .opaque_for ( path, e) ;
984
let nocompress = self .attrs.lookup ( path) .nocompress;
98286406 Andy McCall 1 month ago ↶
985
self .tree_ops.push ( TreeOp:: AddNode { node, kind: e.kind, mode: Mode { exec: e.exec, opaque } , parent, name: name.to_string ( ) } ) ;
986
if opaque {
b7f102fc Andy McCall 1 month ago ↶
987
let blob = self .blob_of ( repo, path, e, nocompress) ? ;
98286406 Andy McCall 1 month ago ↶
988
self .edits.push ( Edit:: Replace { node, old: None , new: Some ( blob) } ) ;
989
} else if ! e.content.is_empty ( ) {
cdb5beba Andy McCall 1 month ago ↶
990
let content = self .stored_text ( path, & e.content) ;
98286406 Andy McCall 1 month ago ↶
991
self .edits.push ( Edit:: Insert {
992
node,
993
after: crate :: objects:: AtomRef:: Start,
994
before: crate :: objects:: AtomRef:: End,
cdb5beba Andy McCall 1 month ago ↶
995
lines: split_lines ( & content) .iter ( ) .map ( | l | l.to_vec ( ) ) .collect ( ) ,
98286406 Andy McCall 1 month ago ↶
996
} ) ;
997
}
998
self .statuses.push ( PathStatus { path: path.to_string ( ) , status: ChangeStatus:: Added } ) ;
999
Ok ( ( ) )
1000
}
1001
6e723b81 Andy McCall 1 month ago ↶
1002
1003
fn move_path ( & mut self , repo : & Repo, old : & str , new : & str , e : & FsEntry, expected : & crate:: state:: FileState) -> Result< ( ) > {
1004
let entry = & self .base_snapshot.tree.entries[ old] ;
1005
let ( dir, name) = split_path ( new) ;
1006
let parent = self .ensure_dir ( dir) ;
1007
self .tree_ops.push ( TreeOp:: Move { node: expected.node, old: op_ref ( entry.placement) , parent, name: name.to_string ( ) } ) ;
1008
self .update_path ( repo, old, e, expected) ? ;
1009
1010
self .statuses.retain ( | s | s.path ! = old ) ;
1011
self .statuses.push ( PathStatus { path: new.to_string ( ) , status: ChangeStatus:: Renamed } ) ;
1012
Ok ( ( ) )
1013
}
1014
98286406 Andy McCall 1 month ago ↶
1015
fn remove_path ( & mut self , path : & str , expected : & crate:: state:: FileState) {
1016
let entry = & self .base_snapshot.tree.entries[ path] ;
1017
self .tree_ops.push ( TreeOp:: RemoveNode { node: expected.node, placement: op_ref ( entry.placement) } ) ;
1018
if path.contains ( " .oxy-conflict-" ) | | expected.is_conflicted ( ) {
1019
self .resolves = true ;
1020
}
1021
self .statuses.push ( PathStatus { path: path.to_string ( ) , status: ChangeStatus:: Removed } ) ;
1022
}
1023
1024
fn update_path ( & mut self , repo : & Repo, path : & str , e : & FsEntry, expected : & crate:: state:: FileState) -> Result< ( ) > {
1025
if expected.kind ! = e.kind {
1026
self .remove_path ( path, expected) ;
1027
self .statuses.pop ( ) ;
1028
self .add_path ( repo, path, e) ? ;
1029
self .statuses.pop ( ) ;
1030
self .statuses.push ( PathStatus { path: path.to_string ( ) , status: ChangeStatus:: Modified } ) ;
1031
return Ok ( ( ) ) ;
1032
}
1033
let entry = & self .base_snapshot.tree.entries[ path] ;
1034
let node = expected.node;
1035
let mode_atom = entry.mode_atom.map ( op_ref) ;
1036
let mut status: Option< ChangeStatus> = None ;
cdb5beba Andy McCall 1 month ago ↶
1037
let wants_opaque = self .opaque_for ( path, e) ;
1038
let nocompress = self .attrs.lookup ( path) .nocompress;
1039
let disk_text = self .stored_text ( path, & e.content) ;
98286406 Andy McCall 1 month ago ↶
1040
match & expected.content {
1041
FileContent:: Dir => { }
1042
FileContent:: Text { bytes, materialized } => {
1043
if materialized.is_conflicted ( ) {
cdb5beba Andy McCall 1 month ago ↶
1044
if bytes = = & disk_text {
98286406 Andy McCall 1 month ago ↶
1045
return Ok ( ( ) ) ;
1046
}
1047
if has_conflict_markers ( & e.content) {
1048
self .statuses.push ( PathStatus { path: path.to_string ( ) , status: ChangeStatus:: ConflictEdited } ) ;
1049
return Ok ( ( ) ) ;
1050
}
1051
1052
1053
1054
1055
let regions = self .base.conflict_regions ( & node) ;
cdb5beba Andy McCall 1 month ago ↶
1056
let mut edits = text_edits ( node, & regions.plain, & disk_text) ;
98286406 Andy McCall 1 month ago ↶
1057
if ! regions.region_atoms.is_empty ( ) {
1058
edits.insert ( 0 , Edit:: Delete { node, atoms: ranges ( & regions.region_atoms) } ) ;
1059
}
1060
self .edits.extend ( edits) ;
1061
self .contributors.extend ( regions.contributors) ;
1062
self .resolves = true ;
1063
if e.exec ! = expected.mode.exec {
1064
if let Some ( old) = mode_atom {
1065
self .tree_ops.push ( TreeOp:: SetMode { node, old, mode: Mode { exec: e.exec, opaque: false } } ) ;
1066
}
1067
}
1068
self .statuses.push ( PathStatus { path: path.to_string ( ) , status: ChangeStatus:: Resolved } ) ;
1069
return Ok ( ( ) ) ;
1070
}
cdb5beba Andy McCall 1 month ago ↶
1071
if wants_opaque {
1072
1073
if let Some ( old) = mode_atom {
1074
self .tree_ops.push ( TreeOp:: SetMode { node, old, mode: Mode { exec: e.exec, opaque: true } } ) ;
1075
}
1076
let live: Vec< _ > = repo_live_keys ( self .base, & node) ;
1077
if ! live.is_empty ( ) {
1078
self .edits.push ( Edit:: Delete { node, atoms: ranges ( & live) } ) ;
1079
}
b7f102fc Andy McCall 1 month ago ↶
1080
let blob = self .blob_of ( repo, path, e, nocompress) ? ;
cdb5beba Andy McCall 1 month ago ↶
1081
self .edits.push ( Edit:: Replace { node, old: None , new: Some ( blob) } ) ;
1082
status = Some ( ChangeStatus:: Modified) ;
1083
} else if bytes ! = & disk_text {
1084
let old = self .base.live_lines ( & node) ;
1085
let edits = text_edits ( node, & old, & disk_text) ;
1086
if ! edits.is_empty ( ) {
1087
self .edits.extend ( edits) ;
98286406 Andy McCall 1 month ago ↶
1088
status = Some ( ChangeStatus:: Modified) ;
1089
}
1090
}
cdb5beba Andy McCall 1 month ago ↶
1091
let switched_to_opaque = wants_opaque;
98286406 Andy McCall 1 month ago ↶
1092
if ! switched_to_opaque & & e.exec ! = expected.mode.exec {
1093
if let Some ( old) = mode_atom {
1094
self .tree_ops.push ( TreeOp:: SetMode { node, old, mode: Mode { exec: e.exec, opaque: false } } ) ;
1095
status.get_or_insert ( ChangeStatus:: ModeChanged) ;
1096
}
1097
}
1098
}
1099
FileContent:: Opaque { blobs } => {
b7f102fc Andy McCall 1 month ago ↶
1100
let new_ref = e.blob.unwrap_or_else ( | | Repo:: blob_ref( & e.content) ) ;
1101
if e.kind = = NodeKind:: File & & wants_opaque {
1102
1103
self .blobs.insert ( path.to_string ( ) , new_ref) ;
1104
}
98286406 Andy McCall 1 month ago ↶
1105
if blobs.len ( ) > 1 {
1106
if ! blobs.contains ( & new_ref) {
1107
self .statuses.push ( PathStatus { path: path.to_string ( ) , status: ChangeStatus:: ConflictEdited } ) ;
1108
}
1109
return Ok ( ( ) ) ;
1110
}
1111
let current = blobs.first ( ) .copied ( ) ;
1112
if current ! = Some ( new_ref) {
cdb5beba Andy McCall 1 month ago ↶
1113
if e.kind = = NodeKind:: File & & ! wants_opaque {
98286406 Andy McCall 1 month ago ↶
1114
1115
if let Some ( old) = mode_atom {
1116
self .tree_ops.push ( TreeOp:: SetMode { node, old, mode: Mode { exec: e.exec, opaque: false } } ) ;
1117
}
1118
self .edits.push ( Edit:: Replace { node, old: current, new: None } ) ;
1119
if ! e.content.is_empty ( ) {
1120
self .edits.push ( Edit:: Insert {
1121
node,
1122
after: crate :: objects:: AtomRef:: Start,
1123
before: crate :: objects:: AtomRef:: End,
1124
lines: split_lines ( & e.content) .iter ( ) .map ( | l | l.to_vec ( ) ) .collect ( ) ,
1125
} ) ;
1126
}
1127
} else {
b7f102fc Andy McCall 1 month ago ↶
1128
let blob = self .blob_of ( repo, path, e, nocompress) ? ;
98286406 Andy McCall 1 month ago ↶
1129
self .edits.push ( Edit:: Replace { node, old: current, new: Some ( blob) } ) ;
1130
}
1131
status = Some ( ChangeStatus:: Modified) ;
1132
} else if e.kind = = NodeKind:: File & & e.exec ! = expected.mode.exec {
1133
if let Some ( old) = mode_atom {
1134
self .tree_ops.push ( TreeOp:: SetMode { node, old, mode: Mode { exec: e.exec, opaque: true } } ) ;
1135
status = Some ( ChangeStatus:: ModeChanged) ;
1136
}
1137
}
1138
}
1139
}
1140
if let Some ( s) = status {
1141
self .statuses.push ( PathStatus { path: path.to_string ( ) , status: s } ) ;
1142
}
1143
Ok ( ( ) )
1144
}
1145
}
1146
1147
fn repo_live_keys ( base : & State, node : & NodeId) -> Vec< crate:: graph:: AtomKey> {
1148
base.live_lines ( node) .into_iter ( ) .map ( | ( k , _) | k ) .collect ( )
1149
}
1150
1151
1152
1153
pub fn referenced_changes ( change : & Change, base : & State) -> BTreeSet< ChangeHash> {
1154
let mut out = BTreeSet:: new( ) ;
1155
let opref = | r : & OpRef, out : & mut BTreeSet< ChangeHash> | {
1156
if ! r.change.is_zero ( ) {
1157
out.insert ( r.change) ;
1158
}
1159
} ;
1160
let mut nodes: Vec< NodeId> = Vec :: new( ) ;
1161
for op in & change.tree_ops {
1162
match op {
1163
TreeOp:: AddNode { parent, .. } => nodes.push ( * parent) ,
1164
TreeOp:: RemoveNode { node, placement } => {
1165
nodes.push ( * node) ;
1166
opref ( placement, & mut out) ;
1167
}
1168
TreeOp:: Move { node, old, parent, .. } => {
1169
nodes.push ( * node) ;
1170
nodes.push ( * parent) ;
1171
opref ( old, & mut out) ;
1172
}
1173
TreeOp:: SetMode { node, old, .. } => {
1174
nodes.push ( * node) ;
1175
opref ( old, & mut out) ;
1176
}
1177
}
1178
}
1179
for e in & change.edits {
1180
nodes.push ( e.node ( ) ) ;
1181
match e {
1182
Edit:: Insert { after, before, .. } => {
1183
for r in [ after, before] {
1184
if let crate :: objects:: AtomRef:: Atom { change, .. } = r {
1185
if ! change.is_zero ( ) {
1186
out.insert ( * change) ;
1187
}
1188
}
1189
}
1190
}
1191
Edit:: Delete { atoms, .. } => {
1192
for r in atoms {
1193
if ! r.change.is_zero ( ) {
1194
out.insert ( r.change) ;
1195
}
1196
}
1197
}
1198
Edit:: Replace { .. } => { }
1199
}
1200
}
1201
for n in nodes {
1202
if let Some ( h) = base.tree ( ) .introducer ( & n) {
1203
out.insert ( h) ;
1204
}
1205
}
1206
out
1207
}
1208
1209
1210
pub fn added_node_paths ( change : & Change, base_snapshot : & Snapshot) -> HashMap< String, ( NodeId, NodeKind) > {
1211
let by_node = base_snapshot.tree.by_node ( ) ;
1212
let mut new_names: HashMap< NodeId, ( NodeId, String, NodeKind) > = HashMap:: new( ) ;
1213
for op in & change.tree_ops {
1214
if let TreeOp:: AddNode { node, parent, name, kind, .. } = op {
1215
new_names.insert ( * node, ( * parent, name.clone ( ) , * kind) ) ;
1216
}
1217
}
1218
let mut out = HashMap:: new( ) ;
1219
for ( node, ( _ , _ , kind) ) in & new_names {
1220
if let Some ( path) = path_of_node ( node, & by_node, & new_names) {
1221
out.insert ( path, ( * node, * kind) ) ;
1222
}
1223
}
1224
out
1225
}
1226
1227
fn path_of_node (
1228
n : & NodeId,
1229
by_node : & HashMap< NodeId, & crate:: tree:: TreeEntry> ,
1230
new_names : & HashMap< NodeId, ( NodeId, String, NodeKind) > ,
1231
) -> Option< String> {
1232
if let Some ( e) = by_node.get ( n) {
1233
return Some ( e.path.clone ( ) ) ;
1234
}
1235
let mut parts = Vec :: new( ) ;
1236
let mut cur = * n;
1237
loop {
1238
if cur.is_root ( ) {
1239
break ;
1240
}
1241
if let Some ( e) = by_node.get ( & cur) {
1242
if ! e.path.is_empty ( ) {
1243
parts.push ( e.path.clone ( ) ) ;
1244
}
1245
break ;
1246
}
1247
let ( parent, name, _ ) = new_names.get ( & cur) ? ;
1248
parts.push ( name.clone ( ) ) ;
1249
cur = * parent;
1250
}
1251
parts.reverse ( ) ;
1252
Some ( parts.join ( " /" ) )
1253
}
1254
1255
1256
pub fn render_change ( change : & Change, base : & State, base_snapshot : & Snapshot) -> String {
1257
let by_node = base_snapshot.tree.by_node ( ) ;
1258
let mut new_names: HashMap< NodeId, ( NodeId, String, NodeKind) > = HashMap:: new( ) ;
1259
for op in & change.tree_ops {
1260
if let TreeOp:: AddNode { node, parent, name, kind, .. } = op {
1261
new_names.insert ( * node, ( * parent, name.clone ( ) , * kind) ) ;
1262
}
1263
}
1264
let path_of = | n : & NodeId| -> String { path_of_node ( n, & by_node, & new_names) .unwrap_or_else ( | | format! ( " <node {} >" , n.short ( 8 ) ) ) } ;
1265
1266
let mut out = String :: new( ) ;
1267
for op in & change.tree_ops {
1268
match op {
1269
TreeOp:: AddNode { node, kind, mode, .. } => {
1270
out.push_str ( & format! ( " A {} ({:?} {} {} )\n " , path_of ( node) , kind, if mode.exec { " , exec" } else { " " } , if mode.opaque { " , opaque" } else { " " } ) ) ;
1271
}
1272
TreeOp:: RemoveNode { node, .. } => out.push_str ( & format! ( " D {} \n " , path_of ( node) ) ) ,
1273
TreeOp:: Move { node, name, .. } => out.push_str ( & format! ( " R {} -> {} \n " , path_of ( node) , name) ) ,
1274
TreeOp:: SetMode { node, mode, .. } => {
1275
out.push_str ( & format! ( " M {} mode: exec={} opaque={} \n " , path_of ( node) , mode.exec, mode.opaque) ) ;
1276
}
1277
}
1278
}
1279
let mut last_node: Option< NodeId> = None ;
1280
for e in & change.edits {
1281
let n = e.node ( ) ;
1282
if last_node ! = Some ( n) {
1283
out.push_str ( & format! ( " --- {} \n " , path_of ( & n) ) ) ;
1284
last_node = Some ( n) ;
1285
}
1286
match e {
1287
Edit:: Insert { lines, .. } => {
1288
for l in lines {
1289
out.push ( ' +' ) ;
1290
out.push_str ( & String :: from_utf8_lossy( l) ) ;
1291
if ! l.ends_with ( b " \n " ) {
1292
out.push ( ' \n ' ) ;
1293
}
1294
}
1295
}
1296
Edit:: Delete { atoms, .. } => {
1297
for r in atoms {
1298
for i in r.from.. r.to {
1299
let content = base.file ( & n) .and_then ( | g | g.content ( & ( r.change, i) ) ) .unwrap_or ( b " <unknown>\n " ) ;
1300
out.push ( ' -' ) ;
1301
out.push_str ( & String :: from_utf8_lossy( content) ) ;
1302
if ! content.ends_with ( b " \n " ) {
1303
out.push ( ' \n ' ) ;
1304
}
1305
}
1306
}
1307
}
1308
Edit:: Replace { old, new, .. } => {
1309
out.push_str ( & format! (
1310
" opaque content {} -> {} \n " ,
1311
old.map ( | o | o.short ( 12 ) ) .unwrap_or_else ( | | " none" .into ( ) ) ,
1312
new.map ( | o | o.short ( 12 ) ) .unwrap_or_else ( | | " none" .into ( ) )
1313
) ) ;
1314
}
1315
}
1316
}
1317
out
1318
}
6e723b81 Andy McCall 1 month ago ↶
1319
1320
1321
1322
1323
const RENAME_THRESHOLD : u32 = 50 ;
1324
const RENAME_LIMIT : usize = 10_000 ;
1325
1326
fn detect_renames ( base_snapshot : & Snapshot, fs_entries : & BTreeMap< String, FsEntry> , in_scope : impl Fn( &str ) -> bool ) -> Vec< ( String, String) > {
1327
let removed: Vec< ( & String, & crate:: state:: FileState) > = base_snapshot
1328
.files
1329
.iter ( )
1330
.filter ( | ( p , f ) | f.kind ! = NodeKind:: Dir & & ! fs_entries.contains_key ( * p) & & in_scope ( p) & & ! f.is_conflicted ( ) )
1331
.collect ( ) ;
1332
let added: Vec< ( & String, & FsEntry) > = fs_entries.iter ( ) .filter ( | ( p , e ) | e.kind ! = NodeKind:: Dir & & ! base_snapshot.files.contains_key ( * p) ) .collect ( ) ;
1333
if removed.is_empty ( ) | | added.is_empty ( ) {
1334
return vec! [ ] ;
1335
}
1336
let exact_only = removed.len ( ) * added.len ( ) > RENAME_LIMIT ;
1337
let old_content = | f : & crate:: state:: FileState| -> Option< Vec< u8 > > {
1338
match & f.content {
1339
FileContent:: Text { bytes, .. } => Some ( bytes.clone ( ) ) ,
1340
_ => None ,
1341
}
1342
} ;
1343
let mut scored: Vec< ( u32 , usize , usize ) > = Vec :: new( ) ;
1344
for ( ri, ( _ , f) ) in removed.iter ( ) .enumerate ( ) {
1345
let old = old_content ( f) ;
1346
let old_blob = match & f.content {
1347
FileContent:: Opaque { blobs } if blobs.len ( ) = = 1 => Some ( blobs[ 0 ] ) ,
1348
_ => None ,
1349
} ;
1350
for ( ai, ( _ , e) ) in added.iter ( ) .enumerate ( ) {
1351
if e.kind ! = f.kind {
1352
continue ;
1353
}
1354
let score = match ( & old, old_blob) {
1355
( Some ( o) , _ ) => {
1356
if exact_only {
1357
if * o = = e.content { 100 } else { 0 }
1358
} else {
1359
similarity ( o, & e.content)
1360
}
1361
}
b7f102fc Andy McCall 1 month ago ↶
1362
( None , Some ( b) ) if e.blob.unwrap_or_else ( | | Repo:: blob_ref( & e.content) ) = = b => 100 ,
6e723b81 Andy McCall 1 month ago ↶
1363
_ => 0 ,
1364
} ;
1365
if score > = RENAME_THRESHOLD {
1366
scored.push ( ( score, ri, ai) ) ;
1367
}
1368
}
1369
}
1370
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 ) ) ) ;
1371
let mut used_r = vec! [ false ; removed.len ( ) ] ;
1372
let mut used_a = vec! [ false ; added.len ( ) ] ;
1373
let mut out = Vec :: new( ) ;
1374
for ( _ , ri, ai) in scored {
1375
if used_r[ ri] | | used_a[ ai] {
1376
continue ;
1377
}
1378
used_r[ ri] = true ;
1379
used_a[ ai] = true ;
1380
out.push ( ( removed[ ri] .0. clone ( ) , added[ ai] .0. clone ( ) ) ) ;
1381
}
1382
out
1383
}
1384
1385
1386
1387
pub fn similarity ( a : & [u8 ], b : & [u8 ]) -> u32 {
1388
if a = = b {
1389
return 100 ;
1390
}
1391
if a.is_empty ( ) | | b.is_empty ( ) {
1392
return 0 ;
1393
}
1394
let mut counts: HashMap< & [ u8 ] , usize > = HashMap:: new( ) ;
1395
for l in split_lines ( a) {
1396
* counts.entry ( l) .or_default ( ) + = 1 ;
1397
}
1398
let mut shared = 0 usize ;
1399
for l in split_lines ( b) {
1400
if let Some ( n) = counts.get_mut ( l) {
1401
if * n > 0 {
1402
* n - = 1 ;
1403
shared + = l.len ( ) ;
1404
}
1405
}
1406
}
1407
( shared * 100 / a.len ( ) .max ( b.len ( ) ) ) as u32
1408
}
1409