@@ -2552,3 +2552,192 @@ url = \"git://example.org/repo3_origin\"
25522552 let remotes3 = String :: from_utf8 ( output3. stdout ) . expect ( "utf8 conversion failed" ) ;
25532553 assert ! ( remotes3. is_empty( ) ) ;
25542554}
2555+
2556+ #[ test]
2557+ fn clone_multiple_remotes_prefers_origin ( ) {
2558+ // Test that when cloning a repo with remotes aaa, origin, zzz:
2559+ // - It clones using the origin remote
2560+ // - It adds aaa and zzz as additional remotes
2561+ let temp = temp_folder ( ) ;
2562+
2563+ // Create source repos for each remote
2564+ create_local_repo ( & temp, "source_aaa" ) ;
2565+ create_local_repo ( & temp, "source_origin" ) ;
2566+ create_local_repo ( & temp, "source_zzz" ) ;
2567+
2568+ // Create state with multiple remotes including origin
2569+ let initial_state_toml = r#"[[repos]]
2570+ path = "cloned_repo"
2571+ tags = []
2572+
2573+ [repos.remotes.aaa]
2574+ name = "aaa"
2575+ url = "source_aaa"
2576+
2577+ [repos.remotes.origin]
2578+ name = "origin"
2579+ url = "source_origin"
2580+
2581+ [repos.remotes.zzz]
2582+ name = "zzz"
2583+ url = "source_zzz"
2584+ "# ;
2585+ write_gitopolis_state_toml ( & temp, initial_state_toml) ;
2586+
2587+ // Run clone
2588+ gitopolis_executable ( )
2589+ . current_dir ( & temp)
2590+ . args ( vec ! [ "clone" ] )
2591+ . assert ( )
2592+ . success ( )
2593+ . stdout ( predicate:: str:: contains ( "Cloning source_origin" ) ) ;
2594+
2595+ // Verify all three remotes exist in cloned repo with correct names
2596+ let cloned_path = temp. path ( ) . join ( "cloned_repo" ) ;
2597+
2598+ let remotes_output = Command :: new ( "git" )
2599+ . current_dir ( & cloned_path)
2600+ . args ( vec ! [ "remote" , "--verbose" ] )
2601+ . output ( )
2602+ . expect ( "git remote --verbose failed" ) ;
2603+ let remotes = String :: from_utf8 ( remotes_output. stdout ) . expect ( "utf8 conversion failed" ) ;
2604+
2605+ // git clone converts relative local paths to absolute, so strip the temp path prefix
2606+ // Use canonicalize to resolve symlinks (e.g., /var -> /private/var on macOS)
2607+ let temp_path_canonical = temp. path ( ) . canonicalize ( ) . unwrap ( ) ;
2608+ let temp_path_str = temp_path_canonical. to_str ( ) . unwrap ( ) ;
2609+ let remotes = remotes. replace ( & format ! ( "{}/" , temp_path_str) , "" ) ;
2610+
2611+ let expected_remotes = r#"aaa source_aaa (fetch)
2612+ aaa source_aaa (push)
2613+ origin source_origin (fetch)
2614+ origin source_origin (push)
2615+ zzz source_zzz (fetch)
2616+ zzz source_zzz (push)
2617+ "# ;
2618+ assert_eq ! ( expected_remotes, remotes) ;
2619+ }
2620+
2621+ #[ test]
2622+ fn clone_multiple_remotes_uses_first_when_no_origin ( ) {
2623+ // Test that when cloning a repo with remotes aaa, bbb (no origin):
2624+ // - It clones using the first remote (aaa) alphabetically
2625+ // - The remote is named "aaa" (not "origin")
2626+ // - It adds bbb as an additional remote
2627+ let temp = temp_folder ( ) ;
2628+
2629+ // Create source repos for each remote
2630+ create_local_repo ( & temp, "source_aaa" ) ;
2631+ create_local_repo ( & temp, "source_bbb" ) ;
2632+
2633+ // Create state with multiple remotes, none named origin
2634+ let initial_state_toml = r#"[[repos]]
2635+ path = "cloned_repo"
2636+ tags = []
2637+
2638+ [repos.remotes.aaa]
2639+ name = "aaa"
2640+ url = "source_aaa"
2641+
2642+ [repos.remotes.bbb]
2643+ name = "bbb"
2644+ url = "source_bbb"
2645+ "# ;
2646+ write_gitopolis_state_toml ( & temp, initial_state_toml) ;
2647+
2648+ // Run clone
2649+ gitopolis_executable ( )
2650+ . current_dir ( & temp)
2651+ . args ( vec ! [ "clone" ] )
2652+ . assert ( )
2653+ . success ( )
2654+ . stdout ( predicate:: str:: contains ( "Cloning source_aaa" ) ) ;
2655+
2656+ // Verify remotes in cloned repo
2657+ let cloned_path = temp. path ( ) . join ( "cloned_repo" ) ;
2658+
2659+ let remotes_output = Command :: new ( "git" )
2660+ . current_dir ( & cloned_path)
2661+ . args ( vec ! [ "remote" , "--verbose" ] )
2662+ . output ( )
2663+ . expect ( "git remote --verbose failed" ) ;
2664+ let remotes = String :: from_utf8 ( remotes_output. stdout ) . expect ( "utf8 conversion failed" ) ;
2665+
2666+ // git clone converts relative local paths to absolute, so strip the temp path prefix
2667+ // Use canonicalize to resolve symlinks (e.g., /var -> /private/var on macOS)
2668+ let temp_path_canonical = temp. path ( ) . canonicalize ( ) . unwrap ( ) ;
2669+ let temp_path_str = temp_path_canonical. to_str ( ) . unwrap ( ) ;
2670+ let remotes = remotes. replace ( & format ! ( "{}/" , temp_path_str) , "" ) ;
2671+
2672+ // Should have aaa and bbb remotes, NOT origin
2673+ let expected_remotes = r#"aaa source_aaa (fetch)
2674+ aaa source_aaa (push)
2675+ bbb source_bbb (fetch)
2676+ bbb source_bbb (push)
2677+ "# ;
2678+ assert_eq ! ( expected_remotes, remotes) ;
2679+ }
2680+
2681+ #[ test]
2682+ fn clone_skips_adding_remotes_when_repo_exists ( ) {
2683+ // Test that when a repo already exists, clone skips it entirely
2684+ // and doesn't try to add remotes (which would fail with "remote already exists")
2685+ let temp = temp_folder ( ) ;
2686+
2687+ // Create source repos
2688+ create_local_repo ( & temp, "source_origin" ) ;
2689+ create_local_repo ( & temp, "source_upstream" ) ;
2690+
2691+ // Pre-create the target repo with origin remote already configured
2692+ let cloned_path = temp. path ( ) . join ( "cloned_repo" ) ;
2693+ fs:: create_dir_all ( & cloned_path) . expect ( "create repo dir failed" ) ;
2694+ Command :: new ( "git" )
2695+ . current_dir ( & cloned_path)
2696+ . args ( vec ! [ "init" , "--initial-branch" , "main" ] )
2697+ . output ( )
2698+ . expect ( "git init failed" ) ;
2699+ Command :: new ( "git" )
2700+ . current_dir ( & cloned_path)
2701+ . args ( vec ! [ "remote" , "add" , "origin" , "existing_origin_url" ] )
2702+ . output ( )
2703+ . expect ( "git remote add failed" ) ;
2704+
2705+ // Create state with multiple remotes
2706+ let initial_state_toml = r#"[[repos]]
2707+ path = "cloned_repo"
2708+ tags = []
2709+
2710+ [repos.remotes.origin]
2711+ name = "origin"
2712+ url = "source_origin"
2713+
2714+ [repos.remotes.upstream]
2715+ name = "upstream"
2716+ url = "source_upstream"
2717+ "# ;
2718+ write_gitopolis_state_toml ( & temp, initial_state_toml) ;
2719+
2720+ // Run clone - should skip the existing repo and NOT try to add remotes
2721+ gitopolis_executable ( )
2722+ . current_dir ( & temp)
2723+ . args ( vec ! [ "clone" ] )
2724+ . assert ( )
2725+ . success ( )
2726+ . stdout ( predicate:: str:: contains ( "Already exists, skipped" ) )
2727+ // Should NOT have any warnings about failed remote adds
2728+ . stderr ( predicate:: str:: contains ( "Warning" ) . not ( ) ) ;
2729+
2730+ // Verify the original remote is unchanged (not overwritten)
2731+ let remotes_output = Command :: new ( "git" )
2732+ . current_dir ( & cloned_path)
2733+ . args ( vec ! [ "remote" , "--verbose" ] )
2734+ . output ( )
2735+ . expect ( "git remote --verbose failed" ) ;
2736+ let remotes = String :: from_utf8 ( remotes_output. stdout ) . expect ( "utf8 conversion failed" ) ;
2737+
2738+ // Should only have the original origin remote, not the upstream from config
2739+ let expected_remotes = r#"origin existing_origin_url (fetch)
2740+ origin existing_origin_url (push)
2741+ "# ;
2742+ assert_eq ! ( expected_remotes, remotes) ;
2743+ }
0 commit comments