@@ -20,6 +20,9 @@ mod blocking_io {
2020 use gix_ref:: TargetRef ;
2121 use gix_refspec:: parse:: Operation ;
2222
23+ const EXISTING_CONTENT : & [ u8 ] = b"Pre-existing user content.\n " ;
24+ const EXISTING_HEAD_CONTENT : & [ u8 ] = b"ref: refs/heads/pre-existing\n " ;
25+
2326 fn shallow_ids ( repo : & gix:: Repository , expected : & ' static str ) -> crate :: Result < Vec < gix:: ObjectId > > {
2427 let commits = repo. shallow_commits ( ) ?. expect ( expected) ;
2528 Ok ( std:: iter:: once ( commits. head )
@@ -585,6 +588,144 @@ mod blocking_io {
585588 Ok ( ( ) )
586589 }
587590
591+ #[ test]
592+ fn fetch_and_checkout_into_non_empty_directory ( ) -> crate :: Result {
593+ let fixture = gix_testtools:: scripted_fixture_writable ( "make_clone_destinations.sh" ) ?;
594+ let destination = fixture. path ( ) . join ( "non-empty" ) ;
595+ let existing_path = destination. join ( "existing.txt" ) ;
596+
597+ let mut prepare = gix:: clone:: PrepareFetch :: new (
598+ remote:: repo ( "base" ) . path ( ) ,
599+ & destination,
600+ gix:: create:: Kind :: WithWorktree ,
601+ gix:: create:: Options {
602+ destination_must_be_empty : Some ( false ) ,
603+ ..Default :: default ( )
604+ } ,
605+ restricted ( ) ,
606+ ) ?;
607+ let ( mut checkout, _out) =
608+ prepare. fetch_then_checkout ( gix:: progress:: Discard , & std:: sync:: atomic:: AtomicBool :: default ( ) ) ?;
609+ let ( repo, _) = checkout. main_worktree ( gix:: progress:: Discard , & std:: sync:: atomic:: AtomicBool :: default ( ) ) ?;
610+
611+ let index = repo. index ( ) ?;
612+ assert_eq ! ( index. entries( ) . len( ) , 1 , "All entries are known as per HEAD tree" ) ;
613+ assure_index_entries_on_disk ( & index, repo. workdir ( ) . expect ( "non-bare" ) ) ;
614+
615+ assert_eq ! ( std:: fs:: read( & existing_path) ?, EXISTING_CONTENT ) ;
616+ Ok ( ( ) )
617+ }
618+
619+ #[ test]
620+ fn fetch_and_checkout_into_non_empty_directory_does_not_overwrite_pre_existing_tracked_file ( ) -> crate :: Result {
621+ let fixture = gix_testtools:: scripted_fixture_writable ( "make_clone_destinations.sh" ) ?;
622+ let destination = fixture. path ( ) . join ( "non-empty-with-conflicting-file" ) ;
623+ let existing_path = destination. join ( "file" ) ;
624+ let remote_file_content = std:: fs:: read ( remote:: repo ( "base" ) . workdir ( ) . expect ( "non-bare" ) . join ( "file" ) ) ?;
625+ assert_ne ! (
626+ EXISTING_CONTENT , remote_file_content,
627+ "the fixture must differ from the file that checkout would write"
628+ ) ;
629+
630+ let mut prepare = gix:: clone:: PrepareFetch :: new (
631+ remote:: repo ( "base" ) . path ( ) ,
632+ & destination,
633+ gix:: create:: Kind :: WithWorktree ,
634+ gix:: create:: Options {
635+ destination_must_be_empty : Some ( false ) ,
636+ ..Default :: default ( )
637+ } ,
638+ restricted ( ) ,
639+ ) ?;
640+ let ( mut checkout, _out) = prepare. fetch_then_checkout ( gix:: progress:: Discard , & AtomicBool :: default ( ) ) ?;
641+ let ( repo, outcome) = checkout. main_worktree ( gix:: progress:: Discard , & AtomicBool :: default ( ) ) ?;
642+
643+ assert_eq ! (
644+ std:: fs:: read( & existing_path) ?,
645+ EXISTING_CONTENT ,
646+ "checkout must not overwrite the pre-existing tracked path"
647+ ) ;
648+ assert_eq ! ( repo. index( ) ?. entries( ) . len( ) , 1 , "the index is still written" ) ;
649+ assert_eq ! (
650+ outcome. collisions,
651+ [ gix_worktree_state:: checkout:: Collision {
652+ path: BString :: from( "file" ) ,
653+ error_kind: std:: io:: ErrorKind :: AlreadyExists
654+ } ] ,
655+ "the pre-existing tracked path is reported as a normal checkout collision"
656+ ) ;
657+ Ok ( ( ) )
658+ }
659+
660+ #[ test]
661+ fn fetch_and_checkout_into_non_empty_directory_with_existing_dot_git_is_rejected ( ) -> crate :: Result {
662+ let fixture = gix_testtools:: scripted_fixture_writable ( "make_clone_destinations.sh" ) ?;
663+ let destination = fixture. path ( ) . join ( "non-empty-with-dot-git" ) ;
664+ let existing_path = destination. join ( "existing.txt" ) ;
665+ let dot_git = destination. join ( ".git" ) ;
666+ let head_path = dot_git. join ( "HEAD" ) ;
667+
668+ let err = gix:: clone:: PrepareFetch :: new (
669+ remote:: repo ( "base" ) . path ( ) ,
670+ & destination,
671+ gix:: create:: Kind :: WithWorktree ,
672+ gix:: create:: Options {
673+ destination_must_be_empty : Some ( false ) ,
674+ ..Default :: default ( )
675+ } ,
676+ restricted ( ) ,
677+ )
678+ . map ( drop)
679+ . expect_err ( "an existing .git directory must not be reused for clone" ) ;
680+
681+ assert ! (
682+ matches!(
683+ err,
684+ gix:: clone:: Error :: Init ( gix:: init:: Error :: Init ( gix:: create:: Error :: DirectoryExists { ref path } ) )
685+ if * path == dot_git
686+ ) ,
687+ "unexpected error: {err}"
688+ ) ;
689+ assert_eq ! ( std:: fs:: read( & existing_path) ?, EXISTING_CONTENT ) ;
690+ assert_eq ! ( std:: fs:: read( & head_path) ?, EXISTING_HEAD_CONTENT ) ;
691+ Ok ( ( ) )
692+ }
693+
694+ #[ test]
695+ fn drop_after_failed_fetch_into_non_empty_directory_preserves_destination ( ) -> crate :: Result {
696+ let fixture = gix_testtools:: scripted_fixture_writable ( "make_clone_destinations.sh" ) ?;
697+ let destination = fixture. path ( ) . join ( "non-empty" ) ;
698+ let existing_path = destination. join ( "existing.txt" ) ;
699+
700+ let mut prepare = gix:: clone:: PrepareFetch :: new (
701+ remote:: repo ( "base" ) . path ( ) ,
702+ & destination,
703+ gix:: create:: Kind :: WithWorktree ,
704+ gix:: create:: Options {
705+ destination_must_be_empty : Some ( false ) ,
706+ ..Default :: default ( )
707+ } ,
708+ restricted ( ) ,
709+ ) ?
710+ . with_ref_name ( Some ( "does-not-exist" ) ) ?;
711+
712+ prepare
713+ . fetch_then_checkout ( gix:: progress:: Discard , & AtomicBool :: default ( ) )
714+ . expect_err ( "non-existing ref must fail" ) ;
715+ drop ( prepare) ;
716+
717+ assert_eq ! (
718+ std:: fs:: read( & existing_path) ?,
719+ EXISTING_CONTENT ,
720+ "pre-existing user files must survive a failed clone+drop"
721+ ) ;
722+ assert ! (
723+ destination. join( ".git" ) . is_dir( ) ,
724+ "the .git directory we created should remain for user cleanup"
725+ ) ;
726+ Ok ( ( ) )
727+ }
728+
588729 #[ test]
589730 fn fetch_and_checkout_specific_ref ( ) -> crate :: Result {
590731 let tmp = gix_testtools:: tempfile:: TempDir :: new ( ) ?;
@@ -832,6 +973,26 @@ fn clone_and_destination_must_be_empty() -> crate::Result {
832973 Ok ( ( ) )
833974}
834975
976+ #[ test]
977+ fn clone_with_worktree_and_destination_must_be_empty ( ) -> crate :: Result {
978+ let fixture = gix_testtools:: scripted_fixture_writable ( "make_clone_destinations.sh" ) ?;
979+ let destination = fixture. path ( ) . join ( "non-empty" ) ;
980+ let err = gix:: clone:: PrepareFetch :: new (
981+ remote:: repo ( "base" ) . path ( ) ,
982+ & destination,
983+ gix:: create:: Kind :: WithWorktree ,
984+ Default :: default ( ) ,
985+ restricted ( ) ,
986+ )
987+ . map ( drop)
988+ . expect_err ( "this should fail as the directory isn't empty" ) ;
989+ assert ! (
990+ err. to_string( )
991+ . starts_with( "Refusing to initialize the non-empty directory as " )
992+ ) ;
993+ Ok ( ( ) )
994+ }
995+
835996#[ test]
836997fn clone_bare_into_empty_directory_and_early_drop ( ) -> crate :: Result {
837998 let tmp = gix_testtools:: tempfile:: TempDir :: new ( ) ?;
0 commit comments