| # Copyright 2026 The Pigweed Authors |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| # use this file except in compliance with the License. You may obtain a copy of |
| # the License at |
| # |
| # https://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| # License for the specific language governing permissions and limitations under |
| # the License. |
| """Recipe module for merging into a Git repository.""" |
| |
| from __future__ import annotations |
| |
| import dataclasses |
| from collections.abc import Callable |
| |
| from recipe_engine import recipe_api |
| |
| from PB.recipe_modules.pigweed.merge_roll.merge import MergeEntry |
| |
| from RECIPE_MODULES.fuchsia.git_roll_util import api as git_roll_util_api |
| from RECIPE_MODULES.pigweed.checkout import api as checkout_api |
| |
| |
| @dataclasses.dataclass |
| class RevisionChange: |
| """Represents a change in a submodule's pinned revision. |
| |
| Attributes: |
| old: The old commit hash the submodule was pinned to. |
| new: The new commit hash the submodule will be pinned to. |
| finalize: A callable that executes the Git merge command. |
| """ |
| |
| new: str |
| old: str |
| finalize: Callable[[], None] |
| |
| |
| class MergeRollApi(recipe_api.RecipeApi): |
| """API for merging branches.""" |
| |
| RevisionChange = RevisionChange |
| |
| def prepare_merge( |
| self, |
| checkout: checkout_api.CheckoutContext, |
| new_revision: str, |
| ) -> RevisionChange | None: |
| """Prepares and returns a RevisionChange object for a merge. |
| |
| A `finalize` function is created that, when called, will merge the |
| `new_revision`. |
| |
| Args: |
| checkout: The checkout context of the main repository. |
| new_revision: The new commit hash to merge. |
| |
| Returns: |
| A `RevisionChange` object containing the old and new revisions, |
| and a `finalize` callable to apply the update, or None if there's |
| nothing to merge. |
| """ |
| |
| with self.m.context(cwd=checkout.top): |
| self.m.git.fetch(checkout.options.remote, new_revision) |
| |
| new_commits: list[str] = [] |
| for pair in self.m.git.cherry( |
| source=new_revision, |
| destination='HEAD', |
| ): |
| if not pair.has_equivalent: |
| new_commits.append(pair.commit) |
| |
| if not new_commits: |
| return None # pragma: no cover |
| |
| old_revision = self.m.git.rev_parse(f'{new_commits[0]}~1') |
| |
| def finalize() -> None: |
| with self.m.context(cwd=checkout.top): |
| self.m.git.merge(ref=new_revision, commit=False) |
| |
| return RevisionChange( |
| new=new_revision, |
| old=old_revision, |
| finalize=finalize, |
| ) |
| |
| def merge( |
| self, |
| checkout: checkout_api.CheckoutContext, |
| merge_entry: MergeEntry, |
| ) -> list[git_roll_util_api.Roll]: |
| """Merges a branch into the checked out branch.""" |
| with self.m.step.nest(merge_entry.branch): |
| assert merge_entry.branch |
| |
| new_revision = self.m.git_roll_util.resolve_new_revision( |
| checkout.options.remote, |
| merge_entry.branch, |
| checkout.remotes_equivalent, |
| ) |
| |
| change = self.prepare_merge( |
| checkout, |
| new_revision, |
| ) |
| |
| if not change: |
| return [] # pragma: no cover |
| |
| try: |
| roll = self.m.git_roll_util.get_roll( |
| repo_url=checkout.options.remote, |
| repo_short_name=merge_entry.branch, |
| new_rev=change.new, |
| old_rev=change.old, |
| ) |
| |
| change.finalize() |
| |
| return [roll] |
| |
| except self.m.git_roll_util.BackwardsRollError: |
| props = self.m.step.empty('output property').presentation |
| props.properties[merge_entry.branch] = { |
| 'remote': checkout.options.remote, |
| 'revision': change.new, |
| } |
| |
| return [] |