Skip to content

Commit 055e31a

Browse files
authored
More private path fixes (#488)
More private path fixes### Bug fixes #### Path privacy improvements (#488) Path privacy added to: - `vcspull fmt`: summary banners, success logs, and `--all` listings cache the `PrivatePath` display value so config paths always render as `~/.vcspull.yaml`. - `vcspull add`: invalid-config errors reuse the redacted path, keeping failure logs consistent with other config operations. - `vcspull discover`: warnings for repos without remotes and "no repos found" notices collapse their target directories via `PrivatePath`. - `vcspull sync`: human "Synced …" lines now mirror the structured JSON payloads by showing tilde-collapsed repository paths.
2 parents 6bea3c4 + 76def59 commit 055e31a

File tree

9 files changed

+201
-17
lines changed

9 files changed

+201
-17
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+
#### Path privacy improvements (#488)
39+
40+
Path privacy added to:
41+
42+
- `vcspull fmt`: summary banners, success logs, and `--all` listings cache the
43+
`PrivatePath` display value so config paths always render as `~/.vcspull.yaml`.
44+
- `vcspull add`: invalid-config errors reuse the redacted path, keeping failure
45+
logs consistent with other config operations.
46+
- `vcspull discover`: warnings for repos without remotes and "no repos found"
47+
notices collapse their target directories via `PrivatePath`.
48+
- `vcspull sync`: human "Synced …" lines now mirror the structured JSON payloads
49+
by showing tilde-collapsed repository paths.
50+
3651
## vcspull v1.47.0 (2025-11-15)
3752

3853
### Bug Fixes

src/vcspull/cli/add.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,7 @@ def add_repo(
400400
except TypeError:
401401
log.exception(
402402
"Config file %s is not a valid YAML dictionary.",
403-
config_file_path,
403+
display_config_path,
404404
)
405405
return
406406
except Exception:

src/vcspull/cli/discover.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,7 @@ def discover_repos(
404404
log.warning(
405405
"Could not determine remote URL for git repository "
406406
"at %s. Skipping.",
407-
repo_path,
407+
PrivatePath(repo_path),
408408
)
409409
continue
410410

@@ -420,7 +420,7 @@ def discover_repos(
420420
log.warning(
421421
"Could not determine remote URL for git repository "
422422
"at %s. Skipping.",
423-
item,
423+
PrivatePath(item),
424424
)
425425
continue
426426

@@ -433,7 +433,7 @@ def discover_repos(
433433
Fore.YELLOW,
434434
Style.RESET_ALL,
435435
Fore.BLUE,
436-
scan_dir,
436+
PrivatePath(scan_dir),
437437
Style.RESET_ALL,
438438
)
439439
return

src/vcspull/cli/fmt.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -159,14 +159,17 @@ def format_single_config(
159159
bool
160160
True if formatting was successful, False otherwise
161161
"""
162+
# Precompute redacted path for CLI output.
163+
display_config_path = str(PrivatePath(config_file_path))
164+
162165
# Check if file exists
163166
if not config_file_path.exists():
164167
log.error(
165168
"%s✗%s Config file %s%s%s not found.",
166169
Fore.RED,
167170
Style.RESET_ALL,
168171
Fore.BLUE,
169-
config_file_path,
172+
display_config_path,
170173
Style.RESET_ALL,
171174
)
172175
return False
@@ -258,7 +261,7 @@ def format_single_config(
258261
Fore.GREEN,
259262
Style.RESET_ALL,
260263
Fore.BLUE,
261-
config_file_path,
264+
display_config_path,
262265
Style.RESET_ALL,
263266
)
264267
return True
@@ -273,7 +276,7 @@ def format_single_config(
273276
Style.RESET_ALL,
274277
"issue" if change_count == 1 else "issues",
275278
Fore.BLUE,
276-
config_file_path,
279+
display_config_path,
277280
Style.RESET_ALL,
278281
)
279282

@@ -362,7 +365,7 @@ def format_single_config(
362365
Fore.GREEN,
363366
Style.RESET_ALL,
364367
Fore.BLUE,
365-
config_file_path,
368+
display_config_path,
366369
Style.RESET_ALL,
367370
)
368371
except Exception:
@@ -438,12 +441,13 @@ def format_config_file(
438441
)
439442

440443
for config_file in config_files:
444+
display_config_file = str(PrivatePath(config_file))
441445
log.info(
442446
" %s•%s %s%s%s",
443447
Fore.BLUE,
444448
Style.RESET_ALL,
445449
Fore.CYAN,
446-
config_file,
450+
display_config_file,
447451
Style.RESET_ALL,
448452
)
449453

src/vcspull/cli/sync.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -718,13 +718,14 @@ def silent_progress(output: str, timestamp: datetime) -> None:
718718
repo_name = repo.get("name", "unknown")
719719
repo_path = repo.get("path", "unknown")
720720
workspace_label = repo.get("workspace_root", "")
721+
display_repo_path = str(PrivatePath(repo_path))
721722

722723
summary["total"] += 1
723724

724725
event: dict[str, t.Any] = {
725726
"reason": "sync",
726727
"name": repo_name,
727-
"path": str(PrivatePath(repo_path)),
728+
"path": display_repo_path,
728729
"workspace_root": str(workspace_label),
729730
}
730731

@@ -783,7 +784,7 @@ def silent_progress(output: str, timestamp: datetime) -> None:
783784
formatter.emit(event)
784785
formatter.emit_text(
785786
f"{colors.success('✓')} Synced {colors.info(repo_name)} "
786-
f"{colors.muted('→')} {repo_path}",
787+
f"{colors.muted('→')} {display_repo_path}",
787788
)
788789

789790
formatter.emit(

tests/cli/test_add.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,32 @@ def test_add_repo_creates_new_file(
325325
assert "newrepo" in config["~/"]
326326

327327

328+
def test_add_repo_invalid_config_logs_private_path(
329+
user_path: pathlib.Path,
330+
caplog: pytest.LogCaptureFixture,
331+
) -> None:
332+
"""Errors for invalid configs should report redacted paths."""
333+
config_file = user_path / ".vcspull.yaml"
334+
config_file.write_text("- repo: url\n", encoding="utf-8")
335+
336+
repo_dir = user_path / "projects" / "demo"
337+
repo_dir.mkdir(parents=True, exist_ok=True)
338+
339+
caplog.set_level(logging.ERROR)
340+
341+
add_repo(
342+
name="demo",
343+
url="git+https://github.com/example/demo.git",
344+
config_file_path_str=str(config_file),
345+
path=str(repo_dir),
346+
workspace_root_path=None,
347+
dry_run=True,
348+
)
349+
350+
expected_path = str(PrivatePath(config_file))
351+
assert expected_path in caplog.text
352+
353+
328354
class AddDuplicateMergeFixture(t.NamedTuple):
329355
"""Fixture describing duplicate merge toggles for add_repo."""
330356

tests/cli/test_discover.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import logging
56
import pathlib
67
import re
78
import subprocess
@@ -809,6 +810,41 @@ def _fake_save(path: pathlib.Path, data: dict[str, t.Any]) -> None:
809810
assert "Successfully updated" in caplog.text
810811

811812

813+
def test_discover_logs_redact_scan_and_repo_paths(
814+
user_path: pathlib.Path,
815+
caplog: pytest.LogCaptureFixture,
816+
monkeypatch: MonkeyPatch,
817+
) -> None:
818+
"""Warnings and info logs should display PrivatePath-collapsed values."""
819+
scan_dir = user_path / "projects"
820+
repo_dir = scan_dir / "demo"
821+
(repo_dir / ".git").mkdir(parents=True, exist_ok=True)
822+
823+
caplog.set_level(logging.INFO, logger="vcspull.cli.discover")
824+
825+
monkeypatch.setattr(
826+
"vcspull.cli.discover.get_git_origin_url",
827+
lambda _path: None,
828+
)
829+
830+
config_file = user_path / ".vcspull.yaml"
831+
config_file.write_text("{}\n", encoding="utf-8")
832+
833+
discover_repos(
834+
scan_dir_str=str(scan_dir),
835+
config_file_path_str=str(config_file),
836+
recursive=False,
837+
workspace_root_override=None,
838+
yes=True,
839+
dry_run=True,
840+
)
841+
842+
repo_display = str(PrivatePath(repo_dir))
843+
scan_display = str(PrivatePath(scan_dir))
844+
assert repo_display in caplog.text
845+
assert scan_display in caplog.text
846+
847+
812848
def test_discover_user_config_prefers_absolute_workspace_label(
813849
tmp_path: pathlib.Path,
814850
monkeypatch: MonkeyPatch,

tests/cli/test_fmt.py

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import pytest
1111
import yaml
1212

13+
from vcspull._internal.private_path import PrivatePath
1314
from vcspull.cli import cli
1415
from vcspull.cli.fmt import format_config, format_config_file, normalize_repo_config
1516
from vcspull.config import (
@@ -303,6 +304,10 @@ def test_format_config_file_missing_config(
303304
"""Formatting without available config should emit an error."""
304305
monkeypatch.chdir(tmp_path)
305306

307+
home_config = pathlib.Path("~/.vcspull.yaml").expanduser()
308+
if home_config.exists():
309+
home_config.unlink()
310+
306311
with caplog.at_level(logging.ERROR):
307312
format_config_file(None, write=False, format_all=False)
308313

@@ -337,16 +342,40 @@ def test_format_config_file_reports_changes(
337342
assert "Run with --write to apply" in text
338343

339344

345+
def test_format_config_file_uses_private_path_in_logs(
346+
user_path: pathlib.Path,
347+
caplog: LogCaptureFixture,
348+
) -> None:
349+
"""CLI logs should redact the home directory via PrivatePath."""
350+
config_file = user_path / ".vcspull.yaml"
351+
config_file.write_text(
352+
"""~/projects/:
353+
zebra: url1
354+
alpha: url2
355+
""",
356+
encoding="utf-8",
357+
)
358+
359+
with caplog.at_level(logging.INFO):
360+
format_config_file(str(config_file), write=True, format_all=False)
361+
362+
expected_path = str(PrivatePath(config_file))
363+
text = caplog.text
364+
assert f"in {expected_path}" in text
365+
assert f"Successfully formatted {expected_path}" in text
366+
367+
340368
def test_format_all_configs(
341369
tmp_path: pathlib.Path,
342370
caplog: LogCaptureFixture,
343371
monkeypatch: pytest.MonkeyPatch,
372+
user_path: pathlib.Path,
344373
) -> None:
345374
"""format_config_file with --all should process discovered configs."""
346-
config_dir = tmp_path / ".config" / "vcspull"
347-
config_dir.mkdir(parents=True)
375+
config_dir = user_path / ".config" / "vcspull"
376+
config_dir.mkdir(parents=True, exist_ok=True)
348377

349-
home_config = tmp_path / ".vcspull.yaml"
378+
home_config = user_path / ".vcspull.yaml"
350379
home_config.write_text(
351380
yaml.dump({"~/projects/": {"repo1": {"repo": "url1"}}}),
352381
encoding="utf-8",
@@ -396,9 +425,9 @@ def fake_find_home_config_files(
396425

397426
text = caplog.text
398427
assert "Found 3 configuration files to format" in text
399-
assert str(home_config) in text
400-
assert str(work_config) in text
401-
assert str(local_config) in text
428+
assert str(PrivatePath(home_config)) in text
429+
assert str(PrivatePath(work_config)) in text
430+
assert str(PrivatePath(local_config)) in text
402431
assert "already formatted correctly" in text
403432
assert "Repositories in ~/work/ will be sorted alphabetically" in text
404433
assert "All 3 configuration files processed successfully" in text

tests/test_cli.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import yaml
1515

1616
from vcspull.__about__ import __version__
17+
from vcspull._internal.private_path import PrivatePath
1718
from vcspull.cli import cli
1819
from vcspull.cli._output import PlanAction, PlanEntry, PlanResult, PlanSummary
1920
from vcspull.cli.sync import EXIT_ON_ERROR_MSG, NO_REPOS_FOR_TERM_MSG
@@ -974,3 +975,75 @@ def test_sync_dry_run_plan_progress(
974975
output = f"{captured.out}{captured.err}"
975976
assert "Progress:" in output
976977
assert "Plan:" in output
978+
979+
980+
def test_sync_human_output_redacts_repo_paths(
981+
capsys: pytest.CaptureFixture[str],
982+
monkeypatch: pytest.MonkeyPatch,
983+
user_path: pathlib.Path,
984+
) -> None:
985+
"""Synced log lines should collapse repo paths via PrivatePath."""
986+
repo_path = user_path / "repos" / "private"
987+
repo_path.mkdir(parents=True, exist_ok=True)
988+
989+
repo_config = {
990+
"name": "private",
991+
"url": "git+https://example.com/private.git",
992+
"path": str(repo_path),
993+
"workspace_root": "~/repos/",
994+
}
995+
996+
monkeypatch.setattr(
997+
sync_module,
998+
"load_configs",
999+
lambda _paths: [repo_config],
1000+
)
1001+
monkeypatch.setattr(
1002+
sync_module,
1003+
"find_config_files",
1004+
lambda include_home=True: [],
1005+
)
1006+
1007+
def _fake_filter_repos(
1008+
_configs: list[dict[str, t.Any]],
1009+
*,
1010+
path: str | None = None,
1011+
vcs_url: str | None = None,
1012+
name: str | None = None,
1013+
) -> list[dict[str, t.Any]]:
1014+
if name and name != repo_config["name"]:
1015+
return []
1016+
if path and path != repo_config["path"]:
1017+
return []
1018+
if vcs_url and vcs_url != repo_config["url"]:
1019+
return []
1020+
return [repo_config]
1021+
1022+
monkeypatch.setattr(sync_module, "filter_repos", _fake_filter_repos)
1023+
monkeypatch.setattr(
1024+
sync_module,
1025+
"update_repo",
1026+
lambda _repo, progress_callback=None: None,
1027+
)
1028+
1029+
sync_module.sync(
1030+
repo_patterns=[repo_config["name"]],
1031+
config=None,
1032+
workspace_root=None,
1033+
dry_run=False,
1034+
output_json=False,
1035+
output_ndjson=False,
1036+
color="never",
1037+
exit_on_error=False,
1038+
show_unchanged=False,
1039+
summary_only=False,
1040+
long_view=False,
1041+
relative_paths=False,
1042+
fetch=False,
1043+
offline=False,
1044+
verbosity=0,
1045+
)
1046+
1047+
captured = capsys.readouterr()
1048+
expected_path = str(PrivatePath(repo_path))
1049+
assert expected_path in captured.out

0 commit comments

Comments
 (0)