blob: 2f7628d596105fd8b042e859afbe530eceba517e [file] [log] [blame]
# 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 Callable, 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
@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
finalize: Callable[[], None]
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
)
def finalize() -> None:
with self.m.context(cwd=path):
self.m.git('git fetch', 'fetch', 'origin', new_revision)
self.m.git('git checkout', 'checkout', 'FETCH_HEAD')
return RevisionChange(
old=old_revision,
new=new_revision,
finalize=finalize,
)
@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,
)
try:
roll = self.m.git_roll_util.get_roll(
repo_url=submodule.remote,
repo_short_name=submodule.path,
old_rev=change.old,
new_rev=change.new,
)
except self.m.git_roll_util.BackwardsRollError:
return []
change.finalize()
return [roll]