@@ -3783,6 +3783,208 @@ def test_import_dry_run_shows_summary_counts(
37833783 assert "Dry run complete" in caplog .text
37843784
37853785
3786+ # ---------------------------------------------------------------------------
3787+ # Collision / cross-source prune tests
3788+ # ---------------------------------------------------------------------------
3789+
3790+
3791+ def test_import_sync_prune_same_name_different_sources (
3792+ tmp_path : pathlib .Path ,
3793+ monkeypatch : MonkeyPatch ,
3794+ ) -> None :
3795+ """Pruning source A leaves source B entry with the same repo name intact."""
3796+ monkeypatch .setenv ("HOME" , str (tmp_path ))
3797+ workspace_a = tmp_path / "code"
3798+ workspace_a .mkdir ()
3799+ workspace_b = tmp_path / "work"
3800+ workspace_b .mkdir ()
3801+ config_file = tmp_path / ".vcspull.yaml"
3802+
3803+ # Two workspaces, each with "shared-name" from different sources
3804+ save_config_yaml (
3805+ config_file ,
3806+ {
3807+ "~/code/" : {
3808+ "shared-name" : {
3809+ "repo" : "git+git@github.com:org-a/shared-name.git" ,
3810+ "metadata" : {"imported_from" : "github:org-a" },
3811+ },
3812+ },
3813+ "~/work/" : {
3814+ "shared-name" : {
3815+ "repo" : "git+git@github.com:org-b/shared-name.git" ,
3816+ "metadata" : {"imported_from" : "github:org-b" },
3817+ },
3818+ },
3819+ },
3820+ )
3821+
3822+ # Sync with org-a, but org-a no longer has "shared-name" → prune from org-a
3823+ importer = MockImporter (repos = [_make_repo ("other-repo" , owner = "org-a" )])
3824+ _run_import (
3825+ importer ,
3826+ service_name = "github" ,
3827+ target = "org-a" ,
3828+ workspace = str (workspace_a ),
3829+ mode = "user" ,
3830+ language = None ,
3831+ topics = None ,
3832+ min_stars = 0 ,
3833+ include_archived = False ,
3834+ include_forks = False ,
3835+ limit = 100 ,
3836+ config_path_str = str (config_file ),
3837+ dry_run = False ,
3838+ yes = True ,
3839+ output_json = False ,
3840+ output_ndjson = False ,
3841+ color = "never" ,
3842+ sync = True ,
3843+ import_source = "github:org-a" ,
3844+ )
3845+
3846+ from vcspull ._internal .config_reader import ConfigReader
3847+
3848+ final_config = ConfigReader ._from_file (config_file )
3849+ assert final_config is not None
3850+
3851+ # org-a's shared-name should be pruned
3852+ assert "shared-name" not in final_config .get ("~/code/" , {})
3853+ # org-b's shared-name should be untouched
3854+ assert "shared-name" in final_config ["~/work/" ]
3855+ entry_b = final_config ["~/work/" ]["shared-name" ]
3856+ assert entry_b ["metadata" ]["imported_from" ] == "github:org-b"
3857+
3858+
3859+ def test_import_prune_cross_workspace_same_name (
3860+ tmp_path : pathlib .Path ,
3861+ monkeypatch : MonkeyPatch ,
3862+ ) -> None :
3863+ """Pruning one workspace leaves the other workspace's same-name entry intact."""
3864+ monkeypatch .setenv ("HOME" , str (tmp_path ))
3865+ workspace_code = tmp_path / "code"
3866+ workspace_code .mkdir ()
3867+ workspace_work = tmp_path / "work"
3868+ workspace_work .mkdir ()
3869+ config_file = tmp_path / ".vcspull.yaml"
3870+
3871+ # Both workspaces have "myrepo" with different import tags
3872+ save_config_yaml (
3873+ config_file ,
3874+ {
3875+ "~/code/" : {
3876+ "myrepo" : {
3877+ "repo" : "git+git@github.com:user-a/myrepo.git" ,
3878+ "metadata" : {"imported_from" : "github:user-a" },
3879+ },
3880+ },
3881+ "~/work/" : {
3882+ "myrepo" : {
3883+ "repo" : "git+git@github.com:user-b/myrepo.git" ,
3884+ "metadata" : {"imported_from" : "github:user-b" },
3885+ },
3886+ },
3887+ },
3888+ )
3889+
3890+ # Prune user-a: remote has other-repo but not myrepo → myrepo stale
3891+ importer = MockImporter (repos = [_make_repo ("other-repo" , owner = "user-a" )])
3892+ _run_import (
3893+ importer ,
3894+ service_name = "github" ,
3895+ target = "user-a" ,
3896+ workspace = str (workspace_code ),
3897+ mode = "user" ,
3898+ language = None ,
3899+ topics = None ,
3900+ min_stars = 0 ,
3901+ include_archived = False ,
3902+ include_forks = False ,
3903+ limit = 100 ,
3904+ config_path_str = str (config_file ),
3905+ dry_run = False ,
3906+ yes = True ,
3907+ output_json = False ,
3908+ output_ndjson = False ,
3909+ color = "never" ,
3910+ prune = True ,
3911+ import_source = "github:user-a" ,
3912+ )
3913+
3914+ from vcspull ._internal .config_reader import ConfigReader
3915+
3916+ final_config = ConfigReader ._from_file (config_file )
3917+ assert final_config is not None
3918+
3919+ # user-a's entry should be pruned
3920+ assert "myrepo" not in final_config .get ("~/code/" , {})
3921+ # user-b's entry should be untouched
3922+ assert "myrepo" in final_config ["~/work/" ]
3923+ assert (
3924+ final_config ["~/work/" ]["myrepo" ]["metadata" ]["imported_from" ]
3925+ == "github:user-b"
3926+ )
3927+
3928+
3929+ def test_import_sync_same_name_from_remote_not_pruned (
3930+ tmp_path : pathlib .Path ,
3931+ monkeypatch : MonkeyPatch ,
3932+ ) -> None :
3933+ """A repo matching fetched_repo_names is NOT pruned even if URL changed."""
3934+ monkeypatch .setenv ("HOME" , str (tmp_path ))
3935+ workspace = tmp_path / "repos"
3936+ workspace .mkdir ()
3937+ config_file = tmp_path / ".vcspull.yaml"
3938+
3939+ # Existing repo with old URL, tagged from same source
3940+ save_config_yaml (
3941+ config_file ,
3942+ {
3943+ "~/repos/" : {
3944+ "repo-x" : {
3945+ "repo" : "git+git@github.com:testuser/repo-x-OLD.git" ,
3946+ "metadata" : {"imported_from" : "github:testuser" },
3947+ },
3948+ },
3949+ },
3950+ )
3951+
3952+ # Remote still has "repo-x" (new URL) → UPDATE_URL, NOT prune
3953+ importer = MockImporter (repos = [_make_repo ("repo-x" )])
3954+ _run_import (
3955+ importer ,
3956+ service_name = "github" ,
3957+ target = "testuser" ,
3958+ workspace = str (workspace ),
3959+ mode = "user" ,
3960+ language = None ,
3961+ topics = None ,
3962+ min_stars = 0 ,
3963+ include_archived = False ,
3964+ include_forks = False ,
3965+ limit = 100 ,
3966+ config_path_str = str (config_file ),
3967+ dry_run = False ,
3968+ yes = True ,
3969+ output_json = False ,
3970+ output_ndjson = False ,
3971+ color = "never" ,
3972+ sync = True ,
3973+ import_source = "github:testuser" ,
3974+ )
3975+
3976+ from vcspull ._internal .config_reader import ConfigReader
3977+
3978+ final_config = ConfigReader ._from_file (config_file )
3979+ assert final_config is not None
3980+
3981+ # repo-x should still exist (not pruned) with updated URL
3982+ assert "repo-x" in final_config ["~/repos/" ]
3983+ entry = final_config ["~/repos/" ]["repo-x" ]
3984+ assert entry ["repo" ] == _SSH .replace ("repo1" , "repo-x" )
3985+ assert entry ["metadata" ]["imported_from" ] == "github:testuser"
3986+
3987+
37863988def test_import_parser_has_prune_flag () -> None :
37873989 """The shared parent parser must expose --prune."""
37883990 from vcspull .cli .import_cmd ._common import _create_shared_parent
0 commit comments