Skip to content

Commit 290efa7

Browse files
committed
fix(import[provenance]) Guard against non-dict metadata in provenance stamping
why: If a config entry has metadata as a non-dict value (e.g. a string), setdefault("metadata", {}) returns the existing non-dict value and the subsequent dict key assignment raises TypeError. what: - Add isinstance guard before setdefault at both UPDATE_URL and SKIP_UNCHANGED provenance-stamping call sites - Add test_import_provenance_survives_non_dict_metadata verifying non-dict metadata is replaced with a proper dict
1 parent 1f00ace commit 290efa7

File tree

2 files changed

+54
-0
lines changed

2 files changed

+54
-0
lines changed

src/vcspull/cli/import_cmd/_common.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -952,6 +952,8 @@ def _run_import(
952952
updated["repo"] = incoming_url
953953
updated.pop("url", None)
954954
if import_source:
955+
if not isinstance(updated.get("metadata"), (dict, type(None))):
956+
updated["metadata"] = {}
955957
metadata = updated.setdefault("metadata", {})
956958
metadata["imported_from"] = import_source
957959
raw_config[repo_workspace_label][repo.name] = updated
@@ -963,6 +965,8 @@ def _run_import(
963965
if not dry_run and import_source:
964966
live = raw_config[repo_workspace_label].get(repo.name)
965967
if isinstance(live, dict):
968+
if not isinstance(live.get("metadata"), (dict, type(None))):
969+
live["metadata"] = {}
966970
live.setdefault("metadata", {})["imported_from"] = import_source
967971
provenance_tagged_count += 1
968972
elif isinstance(live, str):

tests/cli/test_import_repos.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3547,6 +3547,56 @@ def test_import_provenance_tagging_logs_message(
35473547
assert "Tagged 1 repositories with import provenance" in caplog.text
35483548

35493549

3550+
def test_import_provenance_survives_non_dict_metadata(
3551+
tmp_path: pathlib.Path,
3552+
monkeypatch: MonkeyPatch,
3553+
caplog: pytest.LogCaptureFixture,
3554+
) -> None:
3555+
"""Provenance stamping replaces non-dict metadata with a proper dict."""
3556+
caplog.set_level(logging.INFO)
3557+
monkeypatch.setenv("HOME", str(tmp_path))
3558+
workspace = tmp_path / "repos"
3559+
workspace.mkdir()
3560+
config_file = tmp_path / ".vcspull.yaml"
3561+
3562+
# Existing entry with non-dict metadata — would crash without guard
3563+
save_config_yaml(
3564+
config_file,
3565+
{"~/repos/": {"repo1": {"repo": _SSH, "metadata": "legacy-string"}}},
3566+
)
3567+
3568+
importer = MockImporter(repos=[_make_repo("repo1")])
3569+
_run_import(
3570+
importer,
3571+
service_name="github",
3572+
target="testuser",
3573+
workspace=str(workspace),
3574+
mode="user",
3575+
language=None,
3576+
topics=None,
3577+
min_stars=0,
3578+
include_archived=False,
3579+
include_forks=False,
3580+
limit=100,
3581+
config_path_str=str(config_file),
3582+
dry_run=False,
3583+
yes=True,
3584+
output_json=False,
3585+
output_ndjson=False,
3586+
color="never",
3587+
sync=True,
3588+
import_source="github:testuser",
3589+
)
3590+
3591+
from vcspull._internal.config_reader import ConfigReader
3592+
3593+
final_config = ConfigReader._from_file(config_file)
3594+
assert final_config is not None
3595+
entry = final_config["~/repos/"]["repo1"]
3596+
assert isinstance(entry["metadata"], dict)
3597+
assert entry["metadata"]["imported_from"] == "github:testuser"
3598+
3599+
35503600
# ---------------------------------------------------------------------------
35513601
# --prune standalone flag tests
35523602
# ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)