@@ -45,9 +45,9 @@ pub(crate) mod function {
45
45
use gix:: diff:: rewrites:: tracker:: { Change as _, ChangeKind } ;
46
46
use gix:: diff:: tree:: visit;
47
47
use gix:: index:: entry:: Stage ;
48
- use gix:: object :: tree:: EntryKind ;
48
+ use gix:: merge :: tree:: TreatAsUnresolved ;
49
49
use gix:: objs:: TreeRefIter ;
50
- use gix:: prelude:: ObjectIdExt as _;
50
+ use gix:: prelude:: { ObjectIdExt as _, Write } ;
51
51
use gix:: refs:: Target ;
52
52
use gix:: refs:: transaction:: { Change , LogChange , PreviousValue , RefEdit , RefLog } ;
53
53
use std:: collections:: { BTreeSet , VecDeque } ;
@@ -113,7 +113,7 @@ pub(crate) mod function {
113
113
) -> anyhow:: Result < Outcome > {
114
114
let source_tree = current_head_id. attach ( repo) . object ( ) ?. peel_to_tree ( ) ?;
115
115
let new_object = new_head_id. attach ( repo) . object ( ) ?;
116
- let destination_tree = new_object. clone ( ) . peel_to_tree ( ) ?;
116
+ let mut destination_tree = new_object. clone ( ) . peel_to_tree ( ) ?;
117
117
118
118
let mut delegate = Delegate :: default ( ) ;
119
119
gix:: diff:: tree (
@@ -125,7 +125,8 @@ pub(crate) mod function {
125
125
) ?;
126
126
127
127
let mut opts = git2:: build:: CheckoutBuilder :: new ( ) ;
128
- let snapshot_tree = if !delegate. changed_files . is_empty ( ) {
128
+ let mut snapshot_tree = None ;
129
+ if !delegate. changed_files . is_empty ( ) {
129
130
let changes = but_core:: diff:: worktree_changes_no_renames ( repo) ?;
130
131
if !changes. changes . is_empty ( ) {
131
132
let actual_head_tree_id = repo. head_tree_id_or_empty ( ) ?;
@@ -138,7 +139,7 @@ pub(crate) mod function {
138
139
// Figure out which added or modified files are actually touched. Deletions we ignore, and allow
139
140
// these files to be recreated during checkout even if they were part in a rename
140
141
// (we don't do rename tracking here)
141
- let mut change_lut = repo . empty_tree ( ) . edit ( ) ? ;
142
+ let mut change_lut = tree :: Lut :: default ( ) ;
142
143
for change in & changes. changes {
143
144
match change. status {
144
145
TreeStatus :: Deletion { .. } => {
@@ -148,27 +149,25 @@ pub(crate) mod function {
148
149
}
149
150
TreeStatus :: Addition { .. } | TreeStatus :: Modification { .. } => {
150
151
// It's not about the actual values, just to have a lookup for overlapping paths.
151
- change_lut. upsert (
152
- & change. path ,
153
- EntryKind :: Blob ,
154
- repo. object_hash ( ) . empty_blob ( ) ,
155
- ) ?;
152
+ change_lut. track_file ( change. path . as_ref ( ) ) ;
156
153
}
157
154
TreeStatus :: Rename { .. } => {
158
155
unreachable ! ( "rename tracking was disabled" )
159
156
}
160
157
}
161
158
}
162
159
163
- let selection_of_changes_checkout_would_affect = BTreeSet :: new ( ) ;
164
- // TODO: find uncommitted that would be overwritten.
165
- for _file_to_be_modified in & delegate. changed_files {
166
- // selection.extend(change_lut.extend_leafs(&file_to_be_modified))
160
+ let mut selection_of_changes_checkout_would_affect = BTreeSet :: new ( ) ;
161
+ for ( _kind, file_to_be_modified) in & delegate. changed_files {
162
+ change_lut. get_intersecting (
163
+ file_to_be_modified. as_ref ( ) ,
164
+ & mut selection_of_changes_checkout_would_affect,
165
+ ) ;
167
166
}
168
167
169
168
if !selection_of_changes_checkout_would_affect. is_empty ( ) {
170
- let repo_in_memory = repo. clone ( ) . with_object_memory ( ) ;
171
- let _out = crate :: snapshot:: create_tree (
169
+ let mut repo_in_memory = repo. clone ( ) . with_object_memory ( ) ;
170
+ let out = crate :: snapshot:: create_tree (
172
171
source_tree. id . attach ( & repo_in_memory) ,
173
172
snapshot:: create_tree:: State {
174
173
changes,
@@ -178,20 +177,45 @@ pub(crate) mod function {
178
177
no_workspace_and_meta ( ) ,
179
178
) ?;
180
179
181
- match uncommitted_changes {
182
- UncommitedWorktreeChanges :: KeepAndAbortOnConflict => { }
183
- UncommitedWorktreeChanges :: KeepConflictingInSnapshotAndOverwrite => { }
180
+ if !out. is_empty ( ) {
181
+ let resolve = crate :: snapshot:: resolve_tree (
182
+ out. snapshot_tree . attach ( & repo_in_memory) ,
183
+ destination_tree. id ,
184
+ snapshot:: resolve_tree:: Options {
185
+ worktree_cherry_pick : None ,
186
+ } ,
187
+ ) ?;
188
+ if let Some ( mut worktree_cherry_pick) = resolve. worktree_cherry_pick {
189
+ // re-apply snapshot of just what we need and see if they apply cleanly.
190
+ match uncommitted_changes {
191
+ UncommitedWorktreeChanges :: KeepAndAbortOnConflict => {
192
+ let unresolved = TreatAsUnresolved :: git ( ) ;
193
+ if worktree_cherry_pick. has_unresolved_conflicts ( unresolved) {
194
+ let paths = worktree_cherry_pick
195
+ . conflicts
196
+ . iter ( )
197
+ . filter ( |c| c. is_unresolved ( unresolved) ) . map ( |c| format ! ( "{:?}" , c. ours. location( ) ) )
198
+ . collect :: < Vec < _ > > ( ) ;
199
+ bail ! ( "Worktree changes would be overwritten by checkout: {}" , paths. join( ", " ) ) ;
200
+ }
201
+ }
202
+ UncommitedWorktreeChanges :: KeepConflictingInSnapshotAndOverwrite => { }
203
+ }
204
+ destination_tree. id = worktree_cherry_pick. tree . write ( ) ?. detach ( ) ;
205
+ if let Some ( memory) = repo_in_memory. objects . take_object_memory ( ) {
206
+ for ( kind, data) in memory. values ( ) {
207
+ repo_in_memory
208
+ . write_buf ( * kind, data)
209
+ . map_err ( anyhow:: Error :: from_boxed) ?;
210
+ }
211
+ }
212
+ }
213
+ snapshot_tree = Some ( out. snapshot_tree ) ;
214
+ // TODO: deal with index, but to do that it needs to be merged with destination tree!
184
215
}
185
- todo ! ( "deal with snapshot" )
186
- } else {
187
- None
188
216
}
189
- } else {
190
- None
191
217
}
192
- } else {
193
- None
194
- } ;
218
+ }
195
219
196
220
// Finally, perform the actual checkout
197
221
// TODO(gix): use unconditional `gix` checkout implementation as pre-cursor to the real deal (not needed here).
@@ -277,6 +301,206 @@ pub(crate) mod function {
277
301
} )
278
302
}
279
303
304
+ mod tree {
305
+ use bstr:: { BStr , BString , ByteVec } ;
306
+ use std:: collections:: { BTreeSet , HashMap } ;
307
+
308
+ /// A lookup table as a tree, with trees represented by their path component.
309
+ pub struct Lut {
310
+ nodes : Vec < TreeNode > ,
311
+ }
312
+
313
+ impl Default for Lut {
314
+ fn default ( ) -> Self {
315
+ Lut {
316
+ nodes : vec ! [ TreeNode {
317
+ children: Default :: default ( ) ,
318
+ } ] ,
319
+ }
320
+ }
321
+ }
322
+
323
+ #[ allow( clippy:: indexing_slicing) ]
324
+ impl Lut {
325
+ /// Insert a node for each component in slash-separated `rela_path`.
326
+ pub fn track_file ( & mut self , rela_path : & BStr ) {
327
+ let mut next_index = self . nodes . len ( ) ;
328
+ let mut cursor = & mut self . nodes [ 0 ] ;
329
+ for component in to_components ( rela_path) {
330
+ match cursor. children . get ( component) . copied ( ) {
331
+ None => {
332
+ cursor. children . insert ( component. to_owned ( ) , next_index) ;
333
+ self . nodes . push ( TreeNode :: default ( ) ) ;
334
+ cursor = & mut self . nodes [ next_index] ;
335
+ next_index += 1 ;
336
+ }
337
+ Some ( existing_idx) => {
338
+ cursor = & mut self . nodes [ existing_idx] ;
339
+ }
340
+ }
341
+ }
342
+ }
343
+
344
+ /// Given `rela_path`, place all leaf paths into `out` which is partially or fully
345
+ /// intersecting with `rela_path`.
346
+ /// Note that all paths in `out` will be `/` separated relative paths.
347
+ ///
348
+ /// For example, in a tree with paths `a`, `b/c` and `b/d`, `rela_path` of `a/b` will match `a`,
349
+ /// while `b` would match `b/c` and `b/d`.
350
+ pub fn get_intersecting ( & self , rela_path : & BStr , out : & mut BTreeSet < BString > ) {
351
+ let mut cur_path = BString :: default ( ) ;
352
+ let mut cursor = & self . nodes [ 0 ] ;
353
+
354
+ for component in to_components ( rela_path) {
355
+ match cursor. children . get ( component) . copied ( ) {
356
+ None => {
357
+ if !cur_path. is_empty ( ) {
358
+ out. insert ( cur_path. clone ( ) ) ;
359
+ }
360
+ return ;
361
+ }
362
+ Some ( existing_idx) => {
363
+ cursor = & self . nodes [ existing_idx] ;
364
+ push_component ( & mut cur_path, component)
365
+ }
366
+ }
367
+ }
368
+
369
+ if cursor. children . is_empty ( ) {
370
+ if !cur_path. is_empty ( ) {
371
+ out. insert ( cur_path. clone ( ) ) ;
372
+ }
373
+ return ;
374
+ }
375
+
376
+ let mut queue: Vec < _ > = cursor
377
+ . children
378
+ . iter ( )
379
+ . map ( |( component, idx) | ( cur_path. len ( ) , ( component, * idx) ) )
380
+ . collect ( ) ;
381
+
382
+ while let Some ( ( cur_path_len, ( component, idx) ) ) = queue. pop ( ) {
383
+ cur_path. truncate ( cur_path_len) ;
384
+ push_component ( & mut cur_path, component. as_ref ( ) ) ;
385
+
386
+ let node = & self . nodes [ idx] ;
387
+ if node. children . is_empty ( ) {
388
+ if !cur_path. is_empty ( ) {
389
+ out. insert ( cur_path. clone ( ) ) ;
390
+ }
391
+ } else {
392
+ queue. extend (
393
+ node. children
394
+ . iter ( )
395
+ . map ( |( component, idx) | ( cur_path. len ( ) , ( component, * idx) ) ) ,
396
+ ) ;
397
+ }
398
+ }
399
+ }
400
+ }
401
+
402
+ #[ derive( Debug , Default , Clone ) ]
403
+ struct TreeNode {
404
+ /// A mapping of path component names (always without slash) to their entry in the [`Lut::nodes`].
405
+ /// Note that the node is only a leaf if it has no `children` itself, which is when it can be assumed to be a file.
406
+ children : HashMap < BString , usize > ,
407
+ }
408
+
409
+ pub fn to_components ( rela_path : & BStr ) -> impl Iterator < Item = & BStr > {
410
+ rela_path. split ( |b| * b == b'/' ) . map ( Into :: into)
411
+ }
412
+
413
+ pub fn push_component ( path : & mut BString , component : & BStr ) {
414
+ if !path. is_empty ( ) {
415
+ path. push_byte ( b'/' ) ;
416
+ }
417
+ path. extend_from_slice ( component) ;
418
+ }
419
+
420
+ #[ cfg( test) ]
421
+ mod tests {
422
+ use super :: * ;
423
+
424
+ #[ test]
425
+ fn journey ( ) {
426
+ let mut lut = Lut :: default ( ) ;
427
+ for path in [ "a" , "b/c" , "b/d" ] {
428
+ lut. track_file ( path. into ( ) ) ;
429
+ }
430
+ let mut out = BTreeSet :: new ( ) ;
431
+ lut. get_intersecting ( "" . into ( ) , & mut out) ;
432
+ assert_eq ! ( out. len( ) , 0 , "empty means nothing, instead of everything" ) ;
433
+
434
+ lut. get_intersecting ( "d" . into ( ) , & mut out) ;
435
+
436
+ // This one could not be found.
437
+ insta:: assert_debug_snapshot!( out, @r"{}" ) ;
438
+
439
+ lut. get_intersecting ( "a" . into ( ) , & mut out) ;
440
+ // Perfect match
441
+ insta:: assert_compact_debug_snapshot!( out, @r#"{"a"}"# ) ;
442
+
443
+ out. clear ( ) ;
444
+ lut. get_intersecting ( "a/b" . into ( ) , & mut out) ;
445
+ // indirect match, prefix
446
+ insta:: assert_compact_debug_snapshot!( out, @r#"{"a"}"# ) ;
447
+
448
+ out. clear ( ) ;
449
+ lut. get_intersecting ( "b" . into ( ) , & mut out) ;
450
+ // indirect match, suffix/leafs
451
+ insta:: assert_debug_snapshot!( out, @r#"
452
+ {
453
+ "b/c",
454
+ "b/d",
455
+ }
456
+ "# ) ;
457
+ }
458
+
459
+ #[ test]
460
+ fn complex_journey ( ) {
461
+ let mut lut = Lut :: default ( ) ;
462
+ for path in [
463
+ "a/1/2/4/5" ,
464
+ "a/1/2/3" ,
465
+ "a/2/3" ,
466
+ "a/3" ,
467
+ "b/3/4/5" ,
468
+ "b/3/2/1" ,
469
+ "b/2/3" ,
470
+ "b/1" ,
471
+ ] {
472
+ lut. track_file ( path. into ( ) ) ;
473
+ }
474
+ let mut out = BTreeSet :: new ( ) ;
475
+
476
+ lut. get_intersecting ( "a" . into ( ) , & mut out) ;
477
+ insta:: assert_debug_snapshot!( out, @r#"
478
+ {
479
+ "a/1/2/3",
480
+ "a/1/2/4/5",
481
+ "a/2/3",
482
+ "a/3",
483
+ }
484
+ "# ) ;
485
+
486
+ // It's additive
487
+ lut. get_intersecting ( "b" . into ( ) , & mut out) ;
488
+ insta:: assert_debug_snapshot!( out, @r#"
489
+ {
490
+ "a/1/2/3",
491
+ "a/1/2/4/5",
492
+ "a/2/3",
493
+ "a/3",
494
+ "b/1",
495
+ "b/2/3",
496
+ "b/3/2/1",
497
+ "b/3/4/5",
498
+ }
499
+ "# ) ;
500
+ }
501
+ }
502
+ }
503
+
280
504
#[ derive( Default ) ]
281
505
struct Delegate {
282
506
path_deque : VecDeque < BString > ,
0 commit comments