@@ -238,6 +238,19 @@ fn import_patch_request(repo: &TempDir, patch_path: &Path) -> ImportPatchRequest
238238 ImportPatchRequest {
239239 repo_path : repo. path ( ) . to_str ( ) . unwrap ( ) . to_string ( ) ,
240240 patch_path : patch_path. to_str ( ) . unwrap ( ) . to_string ( ) ,
241+ three_way : false ,
242+ }
243+ }
244+
245+ fn import_patch_request_with_three_way (
246+ repo : & TempDir ,
247+ patch_path : & Path ,
248+ three_way : bool ,
249+ ) -> ImportPatchRequest {
250+ ImportPatchRequest {
251+ repo_path : repo. path ( ) . to_str ( ) . unwrap ( ) . to_string ( ) ,
252+ patch_path : patch_path. to_str ( ) . unwrap ( ) . to_string ( ) ,
253+ three_way,
241254 }
242255}
243256
@@ -255,6 +268,23 @@ fn export_patch_request(
255268 }
256269}
257270
271+ fn assert_patch_contains_full_index ( output : & str ) {
272+ let index_line = output
273+ . lines ( )
274+ . find ( |line| line. starts_with ( "index " ) )
275+ . expect ( "patch has index line" ) ;
276+ let ids = index_line
277+ . trim_start_matches ( "index " )
278+ . split_whitespace ( )
279+ . next ( )
280+ . expect ( "index line has object ids" ) ;
281+ let ( old, new) = ids
282+ . split_once ( ".." )
283+ . expect ( "index line separates object ids" ) ;
284+ assert_eq ! ( old. len( ) , 40 ) ;
285+ assert_eq ! ( new. len( ) , 40 ) ;
286+ }
287+
258288fn export_commit_patch_request (
259289 repo : & TempDir ,
260290 patch_path : & Path ,
@@ -420,6 +450,104 @@ fn import_patch_rejects_non_applicable_patch_without_modifying_files() {
420450 assert_eq ! ( read_file( dir. path( ) , "file.txt" ) , "different\n " ) ;
421451}
422452
453+ #[ test]
454+ fn import_patch_three_way_returns_conflict_result_for_drifted_full_index_patch ( ) {
455+ let dir = init_repo ( ) ;
456+ write_file ( dir. path ( ) , "file.txt" , "base\n " ) ;
457+ git ( dir. path ( ) , & [ "add" , "file.txt" ] ) ;
458+ git ( dir. path ( ) , & [ "commit" , "-m" , "add base file" ] ) ;
459+ let base_hash = head_hash ( dir. path ( ) ) ;
460+
461+ write_file ( dir. path ( ) , "file.txt" , "patch\n " ) ;
462+ git ( dir. path ( ) , & [ "add" , "file.txt" ] ) ;
463+ git ( dir. path ( ) , & [ "commit" , "-m" , "patch change" ] ) ;
464+ let patch = dir. path ( ) . join ( "change.patch" ) ;
465+ let patch_content = git_stdout (
466+ dir. path ( ) ,
467+ & [ "diff" , "--full-index" , "--binary" , & base_hash, "HEAD" , "--" ] ,
468+ ) ;
469+ fs:: write ( & patch, format ! ( "{patch_content}\n " ) ) . expect ( "write patch" ) ;
470+
471+ git ( dir. path ( ) , & [ "reset" , "--hard" , & base_hash] ) ;
472+ write_file ( dir. path ( ) , "file.txt" , "target\n " ) ;
473+ git ( dir. path ( ) , & [ "add" , "file.txt" ] ) ;
474+ git ( dir. path ( ) , & [ "commit" , "-m" , "target drift" ] ) ;
475+
476+ let result = handler ( )
477+ . import_patch_file ( & import_patch_request_with_three_way ( & dir, & patch, true ) )
478+ . expect ( "three-way import result" ) ;
479+
480+ assert_eq ! ( result. message, "GITMUN_PATCH_IMPORT_CONFLICTS" ) ;
481+ assert ! (
482+ git_stdout( dir. path( ) , & [ "status" , "--porcelain" ] )
483+ . lines( )
484+ . any( |line| line. starts_with( "UU file.txt" ) )
485+ ) ;
486+ assert ! ( read_file( dir. path( ) , "file.txt" ) . contains( "<<<<<<<" ) ) ;
487+ }
488+
489+ #[ test]
490+ fn import_patch_three_way_failure_without_blob_data_leaves_files_unchanged ( ) {
491+ let dir = init_repo ( ) ;
492+ write_file ( dir. path ( ) , "file.txt" , "different\n " ) ;
493+ git ( dir. path ( ) , & [ "add" , "file.txt" ] ) ;
494+ git ( dir. path ( ) , & [ "commit" , "-m" , "add file" ] ) ;
495+
496+ let patch = dir. path ( ) . join ( "change.patch" ) ;
497+ fs:: write (
498+ & patch,
499+ "diff --git a/file.txt b/file.txt\n \
500+ index 624785c..172491a 100644\n \
501+ --- a/file.txt\n \
502+ +++ b/file.txt\n \
503+ @@ -1 +1 @@\n \
504+ -before\n \
505+ +after\n ",
506+ )
507+ . expect ( "write patch" ) ;
508+
509+ let result =
510+ handler ( ) . import_patch_file ( & import_patch_request_with_three_way ( & dir, & patch, true ) ) ;
511+
512+ assert ! ( result. is_err( ) ) ;
513+ assert_eq ! ( read_file( dir. path( ) , "file.txt" ) , "different\n " ) ;
514+ assert ! (
515+ git_stdout( dir. path( ) , & [ "status" , "--porcelain" ] )
516+ . lines( )
517+ . all( |line| !line. starts_with( "UU " ) )
518+ ) ;
519+ }
520+
521+ #[ test]
522+ fn import_patch_three_way_blocks_dirty_tracked_files ( ) {
523+ let dir = init_repo ( ) ;
524+ write_file ( dir. path ( ) , "file.txt" , "base\n " ) ;
525+ git ( dir. path ( ) , & [ "add" , "file.txt" ] ) ;
526+ git ( dir. path ( ) , & [ "commit" , "-m" , "add base file" ] ) ;
527+ let base_hash = head_hash ( dir. path ( ) ) ;
528+
529+ write_file ( dir. path ( ) , "file.txt" , "patch\n " ) ;
530+ git ( dir. path ( ) , & [ "add" , "file.txt" ] ) ;
531+ git ( dir. path ( ) , & [ "commit" , "-m" , "patch change" ] ) ;
532+ let patch = dir. path ( ) . join ( "change.patch" ) ;
533+ let patch_content = git_stdout (
534+ dir. path ( ) ,
535+ & [ "diff" , "--full-index" , "--binary" , & base_hash, "HEAD" , "--" ] ,
536+ ) ;
537+ fs:: write ( & patch, format ! ( "{patch_content}\n " ) ) . expect ( "write patch" ) ;
538+
539+ git ( dir. path ( ) , & [ "reset" , "--hard" , & base_hash] ) ;
540+ write_file ( dir. path ( ) , "file.txt" , "local\n " ) ;
541+
542+ let error = handler ( )
543+ . import_patch_file ( & import_patch_request_with_three_way ( & dir, & patch, true ) )
544+ . expect_err ( "dirty files block three-way apply" )
545+ . to_string ( ) ;
546+
547+ assert ! ( error. contains( "GITMUN_ERROR_PATCH_IMPORT_THREE_WAY_BLOCKED" ) ) ;
548+ assert_eq ! ( read_file( dir. path( ) , "file.txt" ) , "local\n " ) ;
549+ }
550+
423551#[ test]
424552fn export_staged_patch_contains_only_staged_changes ( ) {
425553 let dir = init_repo ( ) ;
@@ -444,6 +572,7 @@ fn export_staged_patch_contains_only_staged_changes() {
444572
445573 assert ! ( output. contains( "diff --git a/staged.txt b/staged.txt" ) ) ;
446574 assert ! ( !output. contains( "unstaged.txt" ) ) ;
575+ assert_patch_contains_full_index ( & output) ;
447576}
448577
449578#[ test]
@@ -469,6 +598,7 @@ fn export_unstaged_patch_contains_tracked_unstaged_and_untracked_files() {
469598 assert ! ( output. contains( "diff --git a/tracked.txt b/tracked.txt" ) ) ;
470599 assert ! ( output. contains( "diff --git a/new.txt b/new.txt" ) ) ;
471600 assert ! ( output. contains( "new file mode" ) ) ;
601+ assert_patch_contains_full_index ( & output) ;
472602}
473603
474604#[ test]
@@ -497,6 +627,7 @@ fn export_all_concatenates_staged_unstaged_and_untracked_changes() {
497627 assert ! ( output. contains( "diff --git a/staged.txt b/staged.txt" ) ) ;
498628 assert ! ( output. contains( "diff --git a/unstaged.txt b/unstaged.txt" ) ) ;
499629 assert ! ( output. contains( "diff --git a/new.txt b/new.txt" ) ) ;
630+ assert_patch_contains_full_index ( & output) ;
500631}
501632
502633#[ test]
@@ -544,6 +675,7 @@ fn export_selected_honours_staged_and_unstaged_file_selections() {
544675 assert ! ( output. contains( "diff --git a/unstaged.txt b/unstaged.txt" ) ) ;
545676 assert ! ( output. contains( "diff --git a/new.txt b/new.txt" ) ) ;
546677 assert ! ( !output. contains( "ignored.txt" ) ) ;
678+ assert_patch_contains_full_index ( & output) ;
547679}
548680
549681#[ test]
@@ -562,6 +694,7 @@ fn export_commit_patch_contains_selected_commit_changes() {
562694
563695 assert ! ( output. contains( "diff --git a/commit.txt b/commit.txt" ) ) ;
564696 assert ! ( output. contains( "+first" ) ) ;
697+ assert_patch_contains_full_index ( & output) ;
565698}
566699
567700#[ test]
0 commit comments