1
+ use std:: cmp:: Ordering ;
2
+ use std:: collections:: HashMap ;
1
3
use std:: { path:: PathBuf , vec} ;
2
4
3
5
use anyhow:: { anyhow, Context , Result } ;
4
- use bstr:: ByteSlice ;
5
6
use gitbutler_branch:: BranchCreateRequest ;
6
7
use gitbutler_branch:: { self , GITBUTLER_WORKSPACE_REFERENCE } ;
7
8
use gitbutler_cherry_pick:: RepositoryExt as _;
8
9
use gitbutler_command_context:: CommandContext ;
9
10
use gitbutler_commit:: commit_ext:: CommitExt ;
11
+ use gitbutler_diff:: diff_files_into_hunks;
10
12
use gitbutler_error:: error:: Marker ;
11
- use gitbutler_operating_modes:: OPEN_WORKSPACE_REFS ;
13
+ use gitbutler_operating_modes:: { OPEN_WORKSPACE_REFS , WORKSPACE_BRANCH_REF } ;
12
14
use gitbutler_oxidize:: { git2_to_gix_object_id, gix_to_git2_oid, GixRepositoryExt } ;
13
- use gitbutler_project:: access:: WorktreeWritePermission ;
15
+ use gitbutler_project:: access:: { WorktreeReadPermission , WorktreeWritePermission } ;
14
16
use gitbutler_repo:: logging:: { LogUntil , RepositoryExt as _} ;
15
- use gitbutler_repo:: RepositoryExt ;
17
+ use gitbutler_repo:: rebase :: cherry_rebase_group ;
16
18
use gitbutler_repo:: SignaturePurpose ;
17
- use gitbutler_stack:: { Stack , VirtualBranchesHandle } ;
19
+ use gitbutler_stack:: { Stack , StackId , VirtualBranchesHandle } ;
18
20
use tracing:: instrument;
19
21
22
+ use crate :: compute_workspace_dependencies;
20
23
use crate :: { branch_manager:: BranchManagerExt , conflicts, VirtualBranchesExt } ;
21
24
22
25
const WORKSPACE_HEAD : & str = "Workspace Head" ;
@@ -322,28 +325,29 @@ fn verify_current_branch_name(ctx: &CommandContext) -> Result<&CommandContext> {
322
325
}
323
326
}
324
327
325
- // TODO(ST): Probably there should not be an implicit vbranch creation here.
326
- fn verify_head_is_clean ( ctx : & CommandContext , perm : & mut WorktreeWritePermission ) -> Result < ( ) > {
327
- let head_commit = ctx
328
- . repo ( )
329
- . head ( )
330
- . context ( "failed to get head" ) ?
331
- . peel_to_commit ( )
332
- . context ( "failed to peel to commit" ) ? ;
328
+ # [ derive ( Debug ) ]
329
+ pub enum WorkspaceState {
330
+ OffWorkspaceCommit {
331
+ workspace_commit : git2 :: Oid ,
332
+ extra_commits : Vec < git2 :: Oid > ,
333
+ } ,
334
+ OnWorkspaceCommit ,
335
+ }
333
336
337
+ pub fn workspace_state (
338
+ ctx : & CommandContext ,
339
+ _perm : & WorktreeReadPermission ,
340
+ ) -> Result < WorkspaceState > {
341
+ let repository = ctx. repo ( ) ;
334
342
let vb_handle = VirtualBranchesHandle :: new ( ctx. project ( ) . gb_dir ( ) ) ;
335
- let default_target = vb_handle
336
- . get_default_target ( )
337
- . context ( "failed to get default target" ) ?;
343
+ let default_target = vb_handle. get_default_target ( ) ?;
338
344
339
- let commits = ctx
340
- . repo ( )
341
- . log (
342
- head_commit. id ( ) ,
343
- LogUntil :: Commit ( default_target. sha ) ,
344
- false ,
345
- )
346
- . context ( "failed to get log" ) ?;
345
+ let head_commit = repository. head ( ) ?. peel_to_commit ( ) ?;
346
+ let commits = repository. log (
347
+ head_commit. id ( ) ,
348
+ LogUntil :: Commit ( default_target. sha ) ,
349
+ false ,
350
+ ) ?;
347
351
348
352
let workspace_index = commits
349
353
. iter ( )
@@ -353,69 +357,125 @@ fn verify_head_is_clean(ctx: &CommandContext, perm: &mut WorktreeWritePermission
353
357
|| message. starts_with ( GITBUTLER_INTEGRATION_COMMIT_TITLE )
354
358
} )
355
359
} )
356
- . context ( "GitButler workspace commit not found " ) ?;
360
+ . context ( "" ) ?;
357
361
let workspace_commit = & commits[ workspace_index] ;
358
- let mut extra_commits = commits[ ..workspace_index] . to_vec ( ) ;
359
- extra_commits. reverse ( ) ;
362
+ let extra_commits = commits[ ..workspace_index] . to_vec ( ) ;
360
363
361
364
if extra_commits. is_empty ( ) {
362
365
// no extra commits found, so we're good
363
- return Ok ( ( ) ) ;
366
+ return Ok ( WorkspaceState :: OnWorkspaceCommit ) ;
364
367
}
365
368
366
- ctx. repo ( )
367
- . reset ( workspace_commit. as_object ( ) , git2:: ResetType :: Soft , None )
368
- . context ( "failed to reset to workspace commit" ) ?;
369
-
370
- let branch_manager = ctx. branch_manager ( ) ;
371
- let mut new_branch = branch_manager
372
- . create_virtual_branch (
373
- & BranchCreateRequest {
374
- name : extra_commits
375
- . last ( )
376
- . map ( |commit| commit. message_bstr ( ) . to_string ( ) ) ,
377
- ..Default :: default ( )
378
- } ,
379
- perm,
380
- )
381
- . context ( "failed to create virtual branch" ) ?;
382
-
383
- // rebasing the extra commits onto the new branch
384
- let mut head = new_branch. head ( ) ;
385
- for commit in extra_commits {
386
- let new_branch_head = ctx
387
- . repo ( )
388
- . find_commit ( head)
389
- . context ( "failed to find new branch head" ) ?;
390
-
391
- let rebased_commit_oid = ctx
392
- . repo ( )
393
- . commit_with_signature (
394
- None ,
395
- & commit. author ( ) ,
396
- & commit. committer ( ) ,
397
- & commit. message_bstr ( ) . to_str_lossy ( ) ,
398
- & commit. tree ( ) . unwrap ( ) ,
399
- & [ & new_branch_head] ,
400
- None ,
401
- )
402
- . context ( format ! (
403
- "failed to rebase commit {} onto new branch" ,
404
- commit. id( )
405
- ) ) ?;
369
+ Ok ( WorkspaceState :: OffWorkspaceCommit {
370
+ workspace_commit : workspace_commit. id ( ) ,
371
+ extra_commits : extra_commits
372
+ . iter ( )
373
+ . map ( git2:: Commit :: id)
374
+ . collect :: < Vec < _ > > ( ) ,
375
+ } )
376
+ }
377
+
378
+ // TODO(ST): Probably there should not be an implicit vbranch creation here.
379
+ fn verify_head_is_clean ( ctx : & CommandContext , perm : & mut WorktreeWritePermission ) -> Result < ( ) > {
380
+ let repository = ctx. repo ( ) ;
381
+ let head_commit = repository. head ( ) ?. peel_to_commit ( ) ?;
382
+
383
+ let WorkspaceState :: OffWorkspaceCommit {
384
+ workspace_commit,
385
+ extra_commits,
386
+ } = dbg ! ( workspace_state( ctx, perm. read_permission( ) ) ?)
387
+ else {
388
+ return Ok ( ( ) ) ;
389
+ } ;
406
390
407
- let rebased_commit = ctx. repo ( ) . find_commit ( rebased_commit_oid) . context ( format ! (
408
- "failed to find rebased commit {}" ,
409
- rebased_commit_oid
410
- ) ) ?;
391
+ let best_stack_id = find_best_stack_for_changes ( ctx, perm, head_commit. id ( ) , workspace_commit) ?;
411
392
412
- new_branch. set_stack_head ( ctx, rebased_commit. id ( ) , Some ( rebased_commit. tree_id ( ) ) ) ?;
393
+ if let Some ( best_stack_id) = best_stack_id {
394
+ let vb_handle = VirtualBranchesHandle :: new ( ctx. project ( ) . gb_dir ( ) ) ;
395
+ let mut stack = vb_handle. get_stack_in_workspace ( best_stack_id) ?;
413
396
414
- head = rebased_commit. id ( ) ;
397
+ let new_head = cherry_rebase_group ( repository, stack. head ( ) , & extra_commits, false ) ?;
398
+
399
+ stack. set_stack_head (
400
+ ctx,
401
+ new_head,
402
+ Some ( repository. find_commit ( new_head) ?. tree_id ( ) ) ,
403
+ ) ?;
404
+
405
+ update_workspace_commit ( & vb_handle, ctx) ?;
406
+ } else {
407
+ // There is no stack which can hold the commits so we should just unroll those changes
408
+ repository. reference ( WORKSPACE_BRANCH_REF , workspace_commit, true , "" ) ?;
409
+ repository. set_head ( WORKSPACE_BRANCH_REF ) ?;
415
410
}
411
+
416
412
Ok ( ( ) )
417
413
}
418
414
415
+ fn find_best_stack_for_changes (
416
+ ctx : & CommandContext ,
417
+ perm : & mut WorktreeWritePermission ,
418
+ head_commit : git2:: Oid ,
419
+ workspace_commit : git2:: Oid ,
420
+ ) -> Result < Option < StackId > > {
421
+ let vb_state = VirtualBranchesHandle :: new ( ctx. project ( ) . gb_dir ( ) ) ;
422
+ let default_target = vb_state. get_default_target ( ) ?;
423
+ let repository = ctx. repo ( ) ;
424
+ let stacks = vb_state. list_stacks_in_workspace ( ) ?;
425
+
426
+ let head_commit = repository. find_commit ( head_commit) ?;
427
+
428
+ let diffs = gitbutler_diff:: trees (
429
+ ctx. repo ( ) ,
430
+ & repository. find_commit ( workspace_commit) ?. tree ( ) ?,
431
+ & head_commit. tree ( ) ?,
432
+ true ,
433
+ ) ?;
434
+ let base_diffs: HashMap < _ , _ > = diff_files_into_hunks ( & diffs) . collect ( ) ;
435
+ let workspace_dependencies =
436
+ compute_workspace_dependencies ( ctx, & default_target. sha , & base_diffs, & stacks) ?;
437
+
438
+ match workspace_dependencies. commit_dependent_diffs . len ( ) . cmp ( & 1 ) {
439
+ Ordering :: Greater => {
440
+ // The commits are locked to multiple stacks. We can't correctly assign it
441
+ // to any one stack, so the commits should be undone.
442
+ Ok ( None )
443
+ }
444
+ Ordering :: Equal => {
445
+ // There is one stack which the commits are locked to, so the commits
446
+ // should be added to that particular stack.
447
+ let stack_id = workspace_dependencies
448
+ . commit_dependent_diffs
449
+ . keys ( )
450
+ . next ( )
451
+ . expect ( "Values was asserted length 1 above" ) ;
452
+ Ok ( Some ( * stack_id) )
453
+ }
454
+ Ordering :: Less => {
455
+ // We should return the branch selected for changes, or create a new default branch.
456
+ let mut stacks = vb_state. list_stacks_in_workspace ( ) ?;
457
+ stacks. sort_by_key ( |stack| stack. selected_for_changes . unwrap_or ( 0 ) ) ;
458
+
459
+ if let Some ( stack) = stacks. last ( ) {
460
+ return Ok ( Some ( stack. id ) ) ;
461
+ }
462
+
463
+ let branch_manager = ctx. branch_manager ( ) ;
464
+ let new_stack = branch_manager
465
+ . create_virtual_branch (
466
+ & BranchCreateRequest {
467
+ name : Some ( head_commit. message_bstr ( ) . to_string ( ) ) ,
468
+ ..Default :: default ( )
469
+ } ,
470
+ perm,
471
+ )
472
+ . context ( "failed to create virtual branch" ) ?;
473
+
474
+ Ok ( Some ( new_stack. id ) )
475
+ }
476
+ }
477
+ }
478
+
419
479
fn invalid_head_err ( head_name : & str ) -> anyhow:: Error {
420
480
anyhow ! (
421
481
"project is on {head_name}. Please checkout {} to continue" ,
0 commit comments