blob: 033649d99fc2c8569d8e6286ab8502cc9ed176f9 [file] [log] [blame] [edit]
# 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.
"""Recipe module for rolling files copied from remote Git repositories.
This module provides the `CopyRollApi` class, which facilitates updating
a local file by copying its content from a specified source file in a
remote Git repository. Key functionalities include:
- Resolving the latest revision of the source file's repository.
- Fetching the content of the source file at that revision.
- Comparing the fetched content with the local destination file.
- Updating the local file if its content differs from the source.
- Generating `CopyRoll` objects that encapsulate the changes for commit
message formatting.
"""
from __future__ import annotations
import urllib.parse
from collections.abc import Sequence
from recipe_engine import recipe_api
from PB.recipe_modules.pigweed.copy_roll.copy_entry import CopyEntry
from RECIPE_MODULES.fuchsia.roll_commit_message import (
api as roll_commit_message_api,
)
from RECIPE_MODULES.pigweed.checkout import api as checkout_api
class CopyRoll(roll_commit_message_api.BaseRoll):
"""Represents a roll of a file copied from a remote repository.
This class holds information about a file copy operation, including the
source revision, and the old and new content of the file. It's used
to generate commit messages for such rolls.
"""
def __init__(
self,
destination_path: str,
old_value: str,
new_value: str,
revision: str,
*args,
**kwargs,
):
"""Initializes a CopyRoll instance.
Args:
destination_path: The path to the destination file in the local
checkout.
old_value: The previous content of the destination file.
new_value: The new content fetched from the source.
revision: The commit hash from the source repository corresponding
to the new_value.
*args: Additional arguments for the base class.
**kwargs: Additional keyword arguments for the base class.
"""
super().__init__(*args, **kwargs)
self.destination_path = destination_path
self.old_value = old_value
self.new_value = new_value
self.revision = revision
def short_name(self) -> str:
"""Returns a short name for the roll, typically the destination path."""
return self.destination_path
def message_header(self, force_summary_version: bool = False) -> str:
"""Generates the header for the commit message.
Args:
force_summary_version: If True, a more concise header is generated.
Returns:
The commit message header string.
"""
header = self.destination_path
if force_summary_version:
return header
try:
value_string = self.new_value.strip()
if '\n' not in value_string:
new_header = f'{header}: {value_string}'
if len(new_header) <= 62:
header = new_header
except UnicodeDecodeError: # pragma: no cover
pass
return header
def message_body(
self,
*,
force_summary_version: bool = False,
escape_tags: Sequence[str] = (),
filter_tags: Sequence[str] = (),
) -> str:
"""Generates the body of the commit message.
Args:
force_summary_version: If True, a more concise body is generated.
escape_tags: A sequence of tags to escape in the message.
filter_tags: A sequence of tags to filter from the message.
Returns:
The commit message body string.
"""
del force_summary_version, escape_tags, filter_tags # Unused.
result = []
if (
'\n' not in str(self.old_value).strip()
and '\n' not in self.new_value.strip()
):
if self.old_value is None:
result.append('Initialized to') # pragma: no cover
else:
result.append('From')
old_value = self.old_value
if len(old_value.strip()) > 50:
old_value = f'{len(old_value)} chars'
result.append(f' {old_value.strip()}')
result.append('To')
new_value = self.new_value
if len(new_value.strip()) > 50:
new_value = f'{len(new_value)} chars'
result.append(f' {new_value.strip()}')
result.append('')
return '\n'.join(result)
def message_footer(self, *, send_comment: bool) -> str:
"""Generates the footer for the commit message.
Args:
send_comment: If True, indicates a comment should be sent.
Returns:
An empty string, as copy rolls typically don't have footers.
"""
del send_comment # Unused.
return ''
def output_property(self) -> dict[str, str]:
"""Generates a dictionary of properties representing the roll.
Returns:
A dictionary containing details of the roll, such as path,
old/new values, and revision.
"""
return {
'path': self.destination_path,
'old': self.old_value,
'new': self.new_value,
'revision': self.revision,
}
class CopyRollApi(recipe_api.RecipeApi):
"""Provides methods to roll files copied from remote Git repositories."""
CopyRoll = CopyRoll
def update(
self,
checkout: checkout_api.CheckoutContext,
copy_entry: CopyEntry,
) -> list[CopyRoll]:
"""Updates a local file by copying its content from a remote source.
This method fetches the content of a specified file (`source_path`)
from a remote Git repository at its latest revision (or a specified
branch). It then compares this content with the local file
(`destination_path`). If the content differs, the local file is
updated.
Args:
checkout: The checkout context, providing repository information.
copy_entry: A protobuf message defining the copy operation,
including remote URL, source path, destination path, and
branch.
Returns:
A list containing a `CopyRoll` object if the file was updated,
or an empty list if the content was identical.
"""
with self.m.step.nest(copy_entry.destination_path):
new_revision = self.m.git_roll_util.resolve_new_revision(
copy_entry.remote,
copy_entry.branch or 'main',
checkout.remotes_equivalent,
)
parsed_remote = urllib.parse.urlparse(copy_entry.remote)
destination = checkout.root / copy_entry.destination_path
self.m.file.ensure_directory(
f'ensure directory {destination.parent}',
destination.parent,
)
old_value: str | None = None
self.m.path.mock_add_file(destination)
if self.m.path.isfile(destination):
old_value = self.m.file.read_text(
f'read destination {copy_entry.destination_path}',
destination,
)
new_value = self.m.gitiles.fetch(
host=parsed_remote.netloc,
project=parsed_remote.path.lstrip('/'),
ref=new_revision,
path=copy_entry.source_path,
step_name=f'read source {copy_entry.source_path}',
).decode()
new = self.m.step.empty(f'new {copy_entry.source_path}')
new.presentation.step_summary_text = repr(new_value)
if old_value == new_value:
pres = self.m.step.empty('values are identical').presentation
pres.step_summary_text = repr(old_value)
pres.properties[copy_entry.destination_path] = {
'remote': copy_entry.remote,
'revision': new_revision,
}
return []
self.m.file.write_text(
f'write destination {copy_entry.destination_path}',
destination,
new_value,
)
return [
CopyRoll(
destination_path=copy_entry.destination_path,
old_value=old_value,
new_value=new_value,
revision=new_revision,
),
]