| # Copyright 2024 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. |
| |
| from __future__ import annotations |
| |
| import configparser |
| import dataclasses |
| import functools |
| import io |
| import re |
| from typing import TYPE_CHECKING |
| |
| from recipe_engine import recipe_api |
| |
| if TYPE_CHECKING: # pragma: no cover |
| from typing import Generator |
| from recipe_engine import config_types |
| from RECIPE_MODULES.fuchsia.git_roll_util import api as git_roll_util_api |
| from RECIPE_MODULES.pigweed.checkout import api as checkout_api |
| from RECIPE_MODULES.pigweed.roll_util import api as roll_util_api |
| |
| |
| @dataclasses.dataclass |
| class Submodule: |
| path: str |
| name: str |
| branch: str |
| remote: str = dataclasses.field(default=None) |
| dir: config_types.Path = dataclasses.field(default=None) |
| |
| |
| @dataclasses.dataclass |
| class RevisionChange: |
| old: str |
| new: str |
| |
| |
| class SubmoduleRollApi(recipe_api.RecipeApi): |
| |
| Submodule = Submodule |
| RevisionChange = RevisionChange |
| |
| def update_pin( |
| self, |
| checkout: checkout_api.CheckoutContext, |
| path: config_types.Path, |
| new_revision: str, |
| ) -> RevisionChange: |
| with self.m.context(cwd=checkout.top): |
| self.m.git.submodule_update( |
| paths=(path,), |
| timeout=checkout.options.submodule_timeout_sec, |
| ) |
| |
| old_revision = self.m.checkout.get_revision( |
| path, 'get old revision', test_data='1' * 40 |
| ) |
| |
| with self.m.context(cwd=path): |
| self.m.git('git fetch', 'fetch', 'origin', new_revision) |
| self.m.git('git checkout', 'checkout', 'FETCH_HEAD') |
| |
| # In case new_revision is a branch name we need to retrieve the hash it |
| # resolved to. |
| if not re.search(r'^[0-9a-f]{40}$', new_revision): |
| new_revision = self.m.checkout.get_revision( |
| path, 'get new revision', test_data='2' * 40 |
| ) |
| |
| return RevisionChange(old=old_revision, new=new_revision) |
| |
| @functools.cache |
| def read_gitmodules(self, path): |
| # Confirm the given path is actually a submodule. |
| gitmodules = self.m.file.read_text('read .gitmodules', path) |
| # Example .gitmodules file: |
| # [submodule "third_party/pigweed"] |
| # path = third_party/pigweed |
| # url = https://pigweed.googlesource.com/pigweed/pigweed |
| |
| # configparser doesn't like leading whitespace on lines, despite what |
| # its documentation says. |
| gitmodules = re.sub(r'\n\s+', '\n', gitmodules) |
| parser = configparser.RawConfigParser() |
| parser.readfp(io.StringIO(gitmodules)) |
| return parser |
| |
| def update( |
| self, |
| checkout: checkout_api.CheckoutContext, |
| submodule_entry: SubmoduleEntry, |
| ) -> git_roll_util_api.Roll: |
| gitmodules = self.read_gitmodules(checkout.root / '.gitmodules') |
| |
| submodule = Submodule( |
| path=submodule_entry.path, |
| name=submodule_entry.name or submodule_entry.path, |
| branch=submodule_entry.branch, |
| ) |
| submodule.dir = checkout.root / submodule.path |
| |
| with self.m.step.nest(submodule.name) as pres: |
| section = f'submodule "{submodule.name}"' |
| if not gitmodules.has_section(section): |
| sections = gitmodules.sections() |
| submodules = sorted( |
| re.sub(r'^.*"(.*)"$', r'\1', x) for x in sections |
| ) |
| raise self.m.step.StepFailure( |
| 'no submodule "{}" (submodules: {})'.format( |
| submodule.name, |
| ', '.join('"{}"'.format(x) for x in submodules), |
| ) |
| ) |
| |
| if not submodule.branch: |
| try: |
| submodule.branch = gitmodules.get(section, 'branch') |
| except configparser.NoOptionError: |
| submodule.branch = 'main' |
| |
| submodule.remote = self.m.git_roll_util.normalize_remote( |
| gitmodules.get(section, 'url'), |
| checkout.options.remote, |
| ) |
| |
| new_revision = self.m.git_roll_util.resolve_new_revision( |
| submodule.remote, |
| submodule.branch, |
| checkout.remotes_equivalent, |
| ) |
| |
| change = self.update_pin( |
| checkout, |
| submodule.dir, |
| new_revision, |
| ) |
| |
| direction = self.m.roll_util.get_roll_direction( |
| submodule.dir, change.old, change.new |
| ) |
| |
| # If the primary roll is not necessary or is backwards we can |
| # exit immediately and don't need to check deps. |
| if not self.m.roll_util.can_roll(direction): |
| pres.step_summary_text = 'no roll required' |
| self.m.roll_util.skip_roll_step( |
| submodule.remote, change.old, change.new |
| ) |
| return [] |
| |
| return [ |
| self.m.roll_util.create_roll( |
| project_name=str(submodule.path), |
| old_revision=change.old, |
| new_revision=change.new, |
| proj_dir=submodule.dir, |
| direction=direction, |
| nest_steps=False, |
| ) |
| ] |