diff --git a/otterdog/models/github_organization.py b/otterdog/models/github_organization.py index 69033c5c..f57a1837 100644 --- a/otterdog/models/github_organization.py +++ b/otterdog/models/github_organization.py @@ -1,5 +1,5 @@ # ******************************************************************************* -# Copyright (c) 2023-2024 Eclipse Foundation and others. +# Copyright (c) 2023-2025 Eclipse Foundation and others. # This program and the accompanying materials are made available # under the terms of the Eclipse Public License 2.0 # which is available at http://www.eclipse.org/legal/epl-v20.html @@ -274,7 +274,9 @@ def from_model_data(cls, data: dict[str, Any]) -> GitHubOrganization: "repositories": OptionalS("repositories", default=[]) >> Forall(lambda x: Repository.from_model_data(x)), } - return cls(**bend(mapping, data)) + org = cls(**bend(mapping, data)) + org.repositories = [x.coerce_from_org_settings(org.settings) for x in org.repositories] + return org def resolve_secrets(self, secret_resolver: Callable[[str], str]) -> None: for webhook in self.webhooks: @@ -344,12 +346,18 @@ def to_jsonnet(self, config: JsonnetConfig, context: PatchContext) -> str: # print teams if len(self.teams) > 0: + teams_by_name = associate_by_key(self.teams, lambda x: x.name) + default_teams_by_name = associate_by_key(default_org.teams, lambda x: x.name) + default_team = Team.from_model_data(config.default_team_config) printer.println("teams+: [") printer.level_up() - for team in self.teams: + for team_name, team in sorted(teams_by_name.items()): + if team_name in default_teams_by_name: + continue + team.to_jsonnet(printer, config, context, False, default_team) printer.level_down() diff --git a/otterdog/models/organization_settings.py b/otterdog/models/organization_settings.py index 03f37bac..9d3c691f 100644 --- a/otterdog/models/organization_settings.py +++ b/otterdog/models/organization_settings.py @@ -1,5 +1,5 @@ # ******************************************************************************* -# Copyright (c) 2023-2024 Eclipse Foundation and others. +# Copyright (c) 2023-2025 Eclipse Foundation and others. # This program and the accompanying materials are made available # under the terms of the Eclipse Public License 2.0 # which is available at http://www.eclipse.org/legal/epl-v20.html @@ -28,6 +28,7 @@ UNSET, Change, IndentingPrinter, + associate_by_key, is_set_and_present, is_set_and_valid, unwrap, @@ -190,14 +191,23 @@ def to_jsonnet( extend: bool, default_object: ModelObject, ) -> None: + default_org_settings = cast(OrganizationSettings, default_object) + patch = self.get_patch_to(default_object) write_patch_object_as_json(patch, printer, False) # print custom properties if is_set_and_present(self.custom_properties) and len(self.custom_properties) > 0: + properties_by_name = associate_by_key(self.custom_properties, lambda x: x.name) + default_properties_by_name = associate_by_key(default_org_settings.custom_properties, lambda x: x.name) + + for default_property_name in set(default_properties_by_name): + if default_property_name in properties_by_name: + properties_by_name.pop(default_property_name) + default_org_custom_property = CustomProperty.from_model_data(config.default_org_custom_property_config) - if len(self.custom_properties) > 0: + if len(properties_by_name) > 0: printer.println("custom_properties+: [") printer.level_up() diff --git a/otterdog/models/repository.py b/otterdog/models/repository.py index 7fe557e2..34d1b321 100644 --- a/otterdog/models/repository.py +++ b/otterdog/models/repository.py @@ -1,5 +1,5 @@ # ******************************************************************************* -# Copyright (c) 2023-2024 Eclipse Foundation and others. +# Copyright (c) 2023-2025 Eclipse Foundation and others. # This program and the accompanying materials are made available # under the terms of the Eclipse Public License 2.0 # which is available at http://www.eclipse.org/legal/epl-v20.html @@ -235,7 +235,7 @@ def add_environment(self, environment: Environment) -> None: def set_environments(self, environments: list[Environment]) -> None: self.environments = environments - def coerce_from_org_settings(self, org_settings: OrganizationSettings) -> Repository: + def coerce_from_org_settings(self, org_settings: OrganizationSettings, for_patch: bool = False) -> Repository: copy = dataclasses.replace(self) if org_settings.has_organization_projects is False: @@ -247,11 +247,16 @@ def coerce_from_org_settings(self, org_settings: OrganizationSettings) -> Reposi if is_set_and_present(self.custom_properties): for custom_property in org_settings.custom_properties: current_property_value = self.custom_properties.get(custom_property.name, None) - if current_property_value is None: + if current_property_value is None and for_patch is False: if custom_property.required is True: if custom_property.default_value is None: raise ValueError("unexpected None value") self.custom_properties[custom_property.name] = custom_property.default_value + elif current_property_value is not None and for_patch is True: + if custom_property.required is True: + if current_property_value == custom_property.default_value: + self.custom_properties.pop(custom_property.name) + return copy def validate(self, context: ValidationContext, parent_object: Any) -> None: @@ -864,7 +869,8 @@ def to_jsonnet( extend: bool, default_object: ModelObject, ) -> None: - patch = self.get_patch_to(default_object) + coerced_repo = self.coerce_from_org_settings(cast(OrganizationSettings, context.org_settings), for_patch=True) + patch = coerced_repo.get_patch_to(default_object) has_webhooks = len(self.webhooks) > 0 has_secrets = len(self.secrets) > 0 diff --git a/otterdog/models/ruleset.py b/otterdog/models/ruleset.py index 24e5aec0..577fc320 100644 --- a/otterdog/models/ruleset.py +++ b/otterdog/models/ruleset.py @@ -1,5 +1,5 @@ # ******************************************************************************* -# Copyright (c) 2023-2024 Eclipse Foundation and others. +# Copyright (c) 2023-2025 Eclipse Foundation and others. # This program and the accompanying materials are made available # under the terms of the Eclipse Public License 2.0 # which is available at http://www.eclipse.org/legal/epl-v20.html @@ -750,14 +750,16 @@ def to_jsonnet( embedded_extend = True if is_set_and_valid(default_pull_request_config): - printer.print(f"required_pull_request{'+' if embedded_extend else ''}:") - self.required_pull_request.to_jsonnet( - printer, - jsonnet_config, - context, - embedded_extend, - default_pull_request_config, - ) + patch = self.required_pull_request.get_patch_to(default_pull_request_config) + if len(patch) > 0: + printer.print(f"required_pull_request{'+' if embedded_extend else ''}:") + self.required_pull_request.to_jsonnet( + printer, + jsonnet_config, + context, + embedded_extend, + default_pull_request_config, + ) if is_set_and_present(self.required_merge_queue): default_merge_queue_config = cast(Ruleset, default_object).required_merge_queue @@ -770,14 +772,16 @@ def to_jsonnet( embedded_extend = True if is_set_and_valid(default_merge_queue_config): - printer.print(f"required_merge_queue{'+' if embedded_extend else ''}:") - self.required_merge_queue.to_jsonnet( - printer, - jsonnet_config, - context, - embedded_extend, - default_merge_queue_config, - ) + patch = self.required_merge_queue.get_patch_to(default_merge_queue_config) + if len(patch) > 0: + printer.print(f"required_merge_queue{'+' if embedded_extend else ''}:") + self.required_merge_queue.to_jsonnet( + printer, + jsonnet_config, + context, + embedded_extend, + default_merge_queue_config, + ) if is_set_and_present(self.required_status_checks): default_status_check_config = cast(Ruleset, default_object).required_status_checks @@ -790,14 +794,16 @@ def to_jsonnet( embedded_extend = True if is_set_and_valid(default_status_check_config): - printer.print(f"required_status_checks{'+' if embedded_extend else ''}:") - self.required_status_checks.to_jsonnet( - printer, - jsonnet_config, - context, - embedded_extend, - default_status_check_config, - ) + patch = self.required_status_checks.get_patch_to(default_status_check_config) + if len(patch) > 0: + printer.print(f"required_status_checks{'+' if embedded_extend else ''}:") + self.required_status_checks.to_jsonnet( + printer, + jsonnet_config, + context, + embedded_extend, + default_status_check_config, + ) # close the object printer.level_down() diff --git a/otterdog/operations/diff_operation.py b/otterdog/operations/diff_operation.py index bd146d87..36ac32f1 100644 --- a/otterdog/operations/diff_operation.py +++ b/otterdog/operations/diff_operation.py @@ -1,5 +1,5 @@ # ******************************************************************************* -# Copyright (c) 2023-2024 Eclipse Foundation and others. +# Copyright (c) 2023-2025 Eclipse Foundation and others. # This program and the accompanying materials are made available # under the terms of the Eclipse Public License 2.0 # which is available at http://www.eclipse.org/legal/epl-v20.html @@ -101,23 +101,28 @@ async def execute( self._org_config = org_config self._print_project_header(org_config, org_index, org_count) + self.printer.level_up() + + try: + return await self.generate_diff(org_config) + finally: + self.printer.level_down() + async def generate_diff(self, org_config: OrganizationConfig) -> int: try: self._gh_client = self.setup_github_client(org_config) except RuntimeError as e: self.printer.print_error(f"invalid credentials\n{e!s}") return 1 - self.printer.level_up() - try: - return await self.generate_diff(org_config) + return await self._generate_diff_internal(org_config) except RuntimeError as e: self.printer.print_error(f"planning aborted: {e!s}") return 1 finally: - self.printer.level_down() - await self._gh_client.close() + if self._gh_client is not None: + await self._gh_client.close() def setup_github_client(self, org_config: OrganizationConfig) -> GitHubProvider: return GitHubProvider(self.get_credentials(org_config, only_token=self.no_web_ui)) @@ -135,7 +140,7 @@ def include_resources_with_secrets(self) -> bool: def resolve_secrets(self) -> bool: return True - async def generate_diff(self, org_config: OrganizationConfig) -> int: + async def _generate_diff_internal(self, org_config: OrganizationConfig) -> int: github_id = org_config.github_id jsonnet_config = org_config.jsonnet_config await jsonnet_config.init_template() diff --git a/otterdog/operations/open_pull_request.py b/otterdog/operations/open_pull_request.py index b8b75d40..6a73bf7a 100644 --- a/otterdog/operations/open_pull_request.py +++ b/otterdog/operations/open_pull_request.py @@ -1,5 +1,5 @@ # ******************************************************************************* -# Copyright (c) 2023-2024 Eclipse Foundation and others. +# Copyright (c) 2023-2025 Eclipse Foundation and others. # This program and the accompanying materials are made available # under the terms of the Eclipse Public License 2.0 # which is available at http://www.eclipse.org/legal/epl-v20.html diff --git a/otterdog/operations/push_config.py b/otterdog/operations/push_config.py index f51256e5..5326c2d8 100644 --- a/otterdog/operations/push_config.py +++ b/otterdog/operations/push_config.py @@ -1,5 +1,5 @@ # ******************************************************************************* -# Copyright (c) 2023-2024 Eclipse Foundation and others. +# Copyright (c) 2023-2025 Eclipse Foundation and others. # This program and the accompanying materials are made available # under the terms of the Eclipse Public License 2.0 # which is available at http://www.eclipse.org/legal/epl-v20.html diff --git a/otterdog/utils.py b/otterdog/utils.py index 73eaad89..72dc36f4 100644 --- a/otterdog/utils.py +++ b/otterdog/utils.py @@ -1,5 +1,5 @@ # ******************************************************************************* -# Copyright (c) 2023-2024 Eclipse Foundation and others. +# Copyright (c) 2023-2025 Eclipse Foundation and others. # This program and the accompanying materials are made available # under the terms of the Eclipse Public License 2.0 # which is available at http://www.eclipse.org/legal/epl-v20.html @@ -131,7 +131,16 @@ def patch_to_other(value: Any, other_value: Any) -> tuple[bool, Any]: else: return False, None else: - raise ValueError("non-empty dictionary values not supported yet") + diff = dict(value) + + for k, v in other_value.items(): + if diff.get(k) == v: + diff.pop(k) + + if len(diff) == 0: + return False, None + else: + return True, diff elif isinstance(value, list): sorted_value_list = sorted(value) @@ -141,7 +150,7 @@ def patch_to_other(value: Any, other_value: Any) -> tuple[bool, Any]: sorted_other_list = sorted(other_value) if sorted_value_list != sorted_other_list: - diff = _diff_list(sorted_value_list, sorted_other_list) + diff = _diff_list(sorted_value_list, sorted_other_list) # type: ignore if len(diff) > 0: return True, diff else: diff --git a/otterdog/webapp/templates/home/organization.html b/otterdog/webapp/templates/home/organization.html index cf089698..c4b754dc 100644 --- a/otterdog/webapp/templates/home/organization.html +++ b/otterdog/webapp/templates/home/organization.html @@ -417,24 +417,24 @@

Status

- +
- - - - - - + + + + + + {% for team in config.teams|sort(attribute='name') %} - - - - - - + + + + +
NameDescriptionPrivacyNotificationsMembers
NameDescriptionPrivacyNotificationsMembers
{{ team.name }}{{ team.description }}{{ team.privacy }}{{ team.notifications }} +
{{ team.name }}{{ team.description }}{{ team.privacy }}{{ team.notifications }}
    {% for member in team.members %}
  • {{ member }}