blob: 4845e20be93cc5a72caf041886e112925c1b3a61 [file]
# 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 []