2828
2929log = logging .getLogger (__name__ )
3030
31+ ConfigScope = t .Literal ["system" , "user" , "project" , "external" ]
32+
33+
34+ def _classify_config_scope (
35+ config_path : pathlib .Path ,
36+ * ,
37+ cwd : pathlib .Path ,
38+ home : pathlib .Path ,
39+ ) -> ConfigScope :
40+ """Determine whether a config lives in user, system, project, or external scope."""
41+ resolved = config_path .expanduser ().resolve ()
42+ home = home .expanduser ().resolve ()
43+ cwd = cwd .expanduser ().resolve ()
44+
45+ default_user_configs = {
46+ (home / ".vcspull.yaml" ).resolve (),
47+ (home / ".vcspull.json" ).resolve (),
48+ }
49+ if resolved in default_user_configs :
50+ return "user"
51+
52+ xdg_config_home = (
53+ pathlib .Path (os .environ .get ("XDG_CONFIG_HOME" , home / ".config" ))
54+ .expanduser ()
55+ .resolve ()
56+ )
57+ user_config_root = (xdg_config_home / "vcspull" ).resolve ()
58+ try :
59+ resolved .relative_to (user_config_root )
60+ except ValueError :
61+ pass
62+ else :
63+ return "user"
64+
65+ xdg_config_dirs_value = os .environ .get ("XDG_CONFIG_DIRS" )
66+ if xdg_config_dirs_value :
67+ config_dir_bases = [
68+ pathlib .Path (entry ).expanduser ().resolve ()
69+ for entry in xdg_config_dirs_value .split (os .pathsep )
70+ if entry
71+ ]
72+ else :
73+ config_dir_bases = [pathlib .Path ("/etc/xdg" ).resolve ()]
74+
75+ for base in config_dir_bases :
76+ candidate = (base / "vcspull" ).resolve ()
77+ try :
78+ resolved .relative_to (candidate )
79+ except ValueError :
80+ continue
81+ else :
82+ return "system"
83+
84+ try :
85+ resolved .relative_to (cwd )
86+ except ValueError :
87+ return "external"
88+ return "project"
89+
3190
3291def get_git_origin_url (repo_path : pathlib .Path ) -> str | None :
3392 """Get the origin URL from a git repository.
@@ -199,6 +258,11 @@ def discover_repos(
199258
200259 display_config_path = str (PrivatePath (config_file_path ))
201260
261+ cwd = pathlib .Path .cwd ()
262+ home = pathlib .Path .home ()
263+ config_scope = _classify_config_scope (config_file_path , cwd = cwd , home = home )
264+ allow_relative_workspace = config_scope == "project"
265+
202266 raw_config : dict [str , t .Any ]
203267 duplicate_root_occurrences : dict [str , list [t .Any ]]
204268 if config_file_path .exists () and config_file_path .is_file ():
@@ -288,8 +352,8 @@ def discover_repos(
288352 "" if occurrence_count == 1 else "s" ,
289353 )
290354
291- cwd = pathlib . Path . cwd ()
292- home = pathlib . Path . home ()
355+ explicit_relative_override = workspace_root_override in { "." , "./" }
356+ preserve_cwd_label = explicit_relative_override or allow_relative_workspace
293357
294358 if merge_duplicates :
295359 (
@@ -301,6 +365,7 @@ def discover_repos(
301365 raw_config ,
302366 cwd = cwd ,
303367 home = home ,
368+ preserve_cwd_label = preserve_cwd_label ,
304369 )
305370 else :
306371 (
@@ -312,6 +377,7 @@ def discover_repos(
312377 raw_config ,
313378 cwd = cwd ,
314379 home = home ,
380+ preserve_cwd_label = preserve_cwd_label ,
315381 )
316382
317383 for message in merge_conflicts :
@@ -378,7 +444,12 @@ def discover_repos(
378444 for name , url , workspace_path in found_repos :
379445 workspace_label = workspace_map .get (workspace_path )
380446 if workspace_label is None :
381- workspace_label = workspace_root_label (workspace_path , cwd = cwd , home = home )
447+ workspace_label = workspace_root_label (
448+ workspace_path ,
449+ cwd = cwd ,
450+ home = home ,
451+ preserve_cwd_label = preserve_cwd_label ,
452+ )
382453 workspace_map [workspace_path ] = workspace_label
383454 raw_config .setdefault (workspace_label , {})
384455
@@ -416,6 +487,7 @@ def discover_repos(
416487 workspace_path ,
417488 cwd = cwd ,
418489 home = home ,
490+ preserve_cwd_label = preserve_cwd_label ,
419491 )
420492 workspace_map [workspace_path ] = workspace_label
421493 raw_config .setdefault (workspace_label , {})
@@ -517,7 +589,12 @@ def discover_repos(
517589 for repo_name , repo_url , workspace_path in repos_to_add :
518590 workspace_label = workspace_map .get (workspace_path )
519591 if workspace_label is None :
520- workspace_label = workspace_root_label (workspace_path , cwd = cwd , home = home )
592+ workspace_label = workspace_root_label (
593+ workspace_path ,
594+ cwd = cwd ,
595+ home = home ,
596+ preserve_cwd_label = preserve_cwd_label ,
597+ )
521598 workspace_map [workspace_path ] = workspace_label
522599
523600 if workspace_label not in raw_config :
0 commit comments