Skip to content

Commit 4eee157

Browse files
authored
Fixes for discover paths and private roots (#487)
## Changes ### Bug Fixes #### `vcspull discover`: Success logs redact config paths Writes triggered by `vcspull discover` now pass the config file through `PrivatePath`, so confirmations like "✓ Successfully updated ~/.vcspull.yaml" and dry-run notices no longer leak the absolute home directory. #### `vcspull discover`: Fix another workspace dir case Discover now inspects the config scope (user/system/project) before writing, so user-level configs like `~/.vcspull.yaml` prefer tilde-prefixed workspace keys while project-level configs keep their relative `./` sections. Tests cover both behaviors to guard against regressions.
2 parents 14e3002 + 78fd3fe commit 4eee157

File tree

4 files changed

+325
-13
lines changed

4 files changed

+325
-13
lines changed

CHANGES

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,21 @@ $ uvx --from 'vcspull' --prerelease allow vcspull
3333

3434
_Upcoming changes will be written here._
3535

36+
### Bug Fixes
37+
38+
#### `vcspull discover`: Success logs redact config paths (#487)
39+
40+
Writes triggered by `vcspull discover` now pass the config file through
41+
`PrivatePath`, so confirmations like "✓ Successfully updated ~/.vcspull.yaml"
42+
and dry-run notices no longer leak the absolute home directory.
43+
44+
#### `vcspull discover`: Fix another workspace dir case (#487)
45+
46+
Discover now inspects the config scope (user/system/project) before writing,
47+
so user-level configs like `~/.vcspull.yaml` prefer tilde-prefixed workspace
48+
keys while project-level configs keep their relative `./` sections. Tests
49+
cover both behaviors to guard against regressions.
50+
3651
### Development
3752

3853
#### PrivatePath centralizes home-directory redaction (#485)

src/vcspull/cli/discover.py

Lines changed: 91 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,65 @@
2828

2929
log = 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

3291
def 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:

src/vcspull/config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -617,6 +617,7 @@ def normalize_workspace_roots(
617617
*,
618618
cwd: pathlib.Path | None = None,
619619
home: pathlib.Path | None = None,
620+
preserve_cwd_label: bool = True,
620621
) -> tuple[dict[str, t.Any], dict[pathlib.Path, str], list[str], int]:
621622
"""Normalize workspace root labels and merge duplicate sections."""
622623
cwd = cwd or pathlib.Path.cwd()
@@ -636,6 +637,7 @@ def normalize_workspace_roots(
636637
canonical_path,
637638
cwd=cwd,
638639
home=home,
640+
preserve_cwd_label=preserve_cwd_label,
639641
)
640642
path_to_label[canonical_path] = normalized_label
641643

0 commit comments

Comments
 (0)