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.
@@ -186,7 +245,7 @@ def discover_repos(
186245 Fore .CYAN ,
187246 Style .RESET_ALL ,
188247 Fore .BLUE ,
189- config_file_path ,
248+ PrivatePath ( config_file_path ) ,
190249 Style .RESET_ALL ,
191250 )
192251 elif len (home_configs ) > 1 :
@@ -197,6 +256,13 @@ def discover_repos(
197256 else :
198257 config_file_path = home_configs [0 ]
199258
259+ display_config_path = str (PrivatePath (config_file_path ))
260+
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+
200266 raw_config : dict [str , t .Any ]
201267 duplicate_root_occurrences : dict [str , list [t .Any ]]
202268 if config_file_path .exists () and config_file_path .is_file ():
@@ -209,7 +275,7 @@ def discover_repos(
209275 except TypeError :
210276 log .exception (
211277 "Config file %s is not a valid YAML dictionary." ,
212- config_file_path ,
278+ display_config_path ,
213279 )
214280 return
215281 except Exception :
@@ -225,7 +291,7 @@ def discover_repos(
225291 elif not isinstance (raw_config , dict ):
226292 log .error (
227293 "Config file %s is not a valid YAML dictionary." ,
228- config_file_path ,
294+ display_config_path ,
229295 )
230296 return
231297 else :
@@ -236,7 +302,7 @@ def discover_repos(
236302 Fore .CYAN ,
237303 Style .RESET_ALL ,
238304 Fore .BLUE ,
239- config_file_path ,
305+ display_config_path ,
240306 Style .RESET_ALL ,
241307 )
242308
@@ -286,8 +352,8 @@ def discover_repos(
286352 "" if occurrence_count == 1 else "s" ,
287353 )
288354
289- cwd = pathlib . Path . cwd ()
290- home = pathlib . Path . home ()
355+ explicit_relative_override = workspace_root_override in { "." , "./" }
356+ preserve_cwd_label = explicit_relative_override or allow_relative_workspace
291357
292358 if merge_duplicates :
293359 (
@@ -299,6 +365,7 @@ def discover_repos(
299365 raw_config ,
300366 cwd = cwd ,
301367 home = home ,
368+ preserve_cwd_label = preserve_cwd_label ,
302369 )
303370 else :
304371 (
@@ -310,6 +377,7 @@ def discover_repos(
310377 raw_config ,
311378 cwd = cwd ,
312379 home = home ,
380+ preserve_cwd_label = preserve_cwd_label ,
313381 )
314382
315383 for message in merge_conflicts :
@@ -376,7 +444,12 @@ def discover_repos(
376444 for name , url , workspace_path in found_repos :
377445 workspace_label = workspace_map .get (workspace_path )
378446 if workspace_label is None :
379- 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+ )
380453 workspace_map [workspace_path ] = workspace_label
381454 raw_config .setdefault (workspace_label , {})
382455
@@ -414,6 +487,7 @@ def discover_repos(
414487 workspace_path ,
415488 cwd = cwd ,
416489 home = home ,
490+ preserve_cwd_label = preserve_cwd_label ,
417491 )
418492 workspace_map [workspace_path ] = workspace_label
419493 raw_config .setdefault (workspace_label , {})
@@ -432,7 +506,7 @@ def discover_repos(
432506 name ,
433507 Style .RESET_ALL ,
434508 Fore .BLUE ,
435- config_file_path ,
509+ display_config_path ,
436510 Style .RESET_ALL ,
437511 )
438512
@@ -458,7 +532,7 @@ def discover_repos(
458532 Fore .GREEN ,
459533 Style .RESET_ALL ,
460534 Fore .BLUE ,
461- config_file_path ,
535+ display_config_path ,
462536 Style .RESET_ALL ,
463537 )
464538 except Exception :
@@ -499,7 +573,7 @@ def discover_repos(
499573 Fore .YELLOW ,
500574 Style .RESET_ALL ,
501575 Fore .BLUE ,
502- config_file_path ,
576+ display_config_path ,
503577 Style .RESET_ALL ,
504578 )
505579 return
@@ -515,7 +589,12 @@ def discover_repos(
515589 for repo_name , repo_url , workspace_path in repos_to_add :
516590 workspace_label = workspace_map .get (workspace_path )
517591 if workspace_label is None :
518- 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+ )
519598 workspace_map [workspace_path ] = workspace_label
520599
521600 if workspace_label not in raw_config :
@@ -554,7 +633,7 @@ def discover_repos(
554633 Fore .GREEN ,
555634 Style .RESET_ALL ,
556635 Fore .BLUE ,
557- config_file_path ,
636+ display_config_path ,
558637 Style .RESET_ALL ,
559638 )
560639 except Exception :
0 commit comments