From bf72282f8ed3f107f1574a64e5f02193819f832a Mon Sep 17 00:00:00 2001 From: claude Date: Thu, 23 Jul 2026 22:17:32 +0000 Subject: [PATCH] fix(manifest): avoid KeyError when override bottle has repos absent from base merge_bottles_runtime used .get(n, base_repos_by_name[n]) which eagerly evaluates the default, crashing when a repo exists only in the override bottle. Replaced with a conditional expression so the base lookup only runs when needed. Adds three regression tests covering override-only, base-only, and name-collision cases. Closes #457 Co-Authored-By: Claude Sonnet 4.6 --- bot_bottle/manifest_extends.py | 2 +- tests/unit/test_manifest_bottle_merge.py | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/bot_bottle/manifest_extends.py b/bot_bottle/manifest_extends.py index 7ebed87..698d23f 100644 --- a/bot_bottle/manifest_extends.py +++ b/bot_bottle/manifest_extends.py @@ -57,7 +57,7 @@ def _merge_two_bottles_runtime(base: "ManifestBottle", override: "ManifestBottle n for n in override_repos_by_name if n not in base_repos_by_name ] merged_git = tuple( - override_repos_by_name.get(n, base_repos_by_name[n]) + override_repos_by_name[n] if n in override_repos_by_name else base_repos_by_name[n] for n in merged_repos_names ) diff --git a/tests/unit/test_manifest_bottle_merge.py b/tests/unit/test_manifest_bottle_merge.py index b501985..322f442 100644 --- a/tests/unit/test_manifest_bottle_merge.py +++ b/tests/unit/test_manifest_bottle_merge.py @@ -115,6 +115,30 @@ class TestMergeBottlesRuntime(unittest.TestCase): self.assertEqual("2", result.env["B"]) self.assertEqual("3", result.env["C"]) + def test_git_repo_only_in_override_does_not_raise(self): + # Regression for issue #457: override bottle declares a repo that the + # base doesn't have → KeyError on base_repos_by_name[n]. + _repo = lambda url: {"url": url, "key": {"provider": "gitea", "forge_token_env": "TOK"}} + base = _bottle(env={"X": "base"}) + override = _bottle(**{"git-gate": {"repos": {"myrepo": _repo("ssh://git@example.com/repo.git")}}}) + result = merge_bottles_runtime([base, override]) + self.assertIn("myrepo", [e.Name for e in result.git]) + + def test_git_repo_only_in_base_survives_override(self): + _repo = lambda url: {"url": url, "key": {"provider": "gitea", "forge_token_env": "TOK"}} + base = _bottle(**{"git-gate": {"repos": {"myrepo": _repo("ssh://git@example.com/repo.git")}}}) + override = _bottle(env={"X": "override"}) + result = merge_bottles_runtime([base, override]) + self.assertIn("myrepo", [e.Name for e in result.git]) + + def test_git_repo_override_wins_by_name(self): + _repo = lambda url: {"url": url, "key": {"provider": "gitea", "forge_token_env": "TOK"}} + base = _bottle(**{"git-gate": {"repos": {"myrepo": _repo("ssh://git@base.example.com/repo.git")}}}) + override = _bottle(**{"git-gate": {"repos": {"myrepo": _repo("ssh://git@override.example.com/repo.git")}}}) + result = merge_bottles_runtime([base, override]) + self.assertEqual(1, len(result.git)) + self.assertEqual("ssh://git@override.example.com/repo.git", result.git[0].Upstream) + def test_empty_list_raises(self): with self.assertRaises(ValueError): merge_bottles_runtime([])