blob: 32d24d77ccde112275e3eb59875453d43ddc54d5 [file] [log] [blame]
# Copyright 2020 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.
"""Utility functions for rollers."""
import pprint
import re
import attr
from recipe_engine import recipe_api
# If we're embedding the original commit message, prepend 'Original-' to lines
# which begin with these tags.
ESCAPE_TAGS = [
'Bug:',
'Fixed:',
'Reviewed-on:',
]
# If we're embedding the original commit message, remove lines which contain
# these tags.
FILTER_TAGS = [
'API-Review:',
'Acked-by:',
'CC:',
'CQ-Do-Not-Cancel-Tryjobs:',
'Change-Id:',
'Commit-Queue:',
'Reviewed-by:',
'Signed-off-by:',
'Testability-Review:',
'Tested-by:',
]
def _sanitize_message(message):
"""Sanitize lines of a commit message.
Prepend 'Original-' to lines which begin with ESCAPE_TAGS. Filter
out lines which begin with FILTER_TAGS.
"""
return '\n'.join(
"Original-" + line
if any((line.startswith(tag) for tag in ESCAPE_TAGS))
else line
for line in message.splitlines()
if not any((tag in line for tag in FILTER_TAGS))
)
@attr.s
class Commit(object):
hash = attr.ib()
message = attr.ib()
def _is_hash(value):
return re.match(r'^[0-9a-fA-F]{40}', value)
class RollUtilApi(recipe_api.RecipeApi):
def _single_commit_roll_message(
self, project_name, commit, old_revision, new_revision
):
template = """
[roll {project_name}] {sanitized_message}
Rolled-Commits: {old_revision:.15}..{new_revision:.15}
CQ-Do-Not-Cancel-Tryjobs: true
""".strip()
kwargs = {
'project_name': project_name,
'original_message': commit.message,
'sanitized_message': _sanitize_message(commit.message),
'old_revision': old_revision,
'new_revision': new_revision,
}
message = template.format(**kwargs)
with self.m.step.nest('message') as pres:
pres.logs['template'] = template
pres.logs['kwargs'] = pprint.pformat(kwargs)
pres.logs['message'] = message
return message
def _multiple_commits_roll_message(
self, project_name, commits, old_revision, new_revision
):
template = """
[{project_name}] Roll {number} commits
{one_liners}
Rolled-Commits: {old_revision:.15}..{new_revision:.15}
CQ-Do-Not-Cancel-Tryjobs: true
""".strip()
one_liners = [
'{:.15} {}'.format(commit.hash, commit.message.splitlines()[0][:50])
for commit in commits
]
number = len(commits)
if not _is_hash(old_revision):
number = 'multiple'
one_liners.append('...')
kwargs = {
'project_name': project_name,
'number': number,
'one_liners': '\n'.join(one_liners),
'old_revision': old_revision,
'new_revision': new_revision,
}
message = template.format(**kwargs)
with self.m.step.nest('message') as pres:
pres.logs['template'] = template
pres.logs['kwargs'] = pprint.pformat(kwargs)
pres.logs['message'] = message
return message
def _commits(self, proj_dir, old_revision, new_revision):
with self.m.context(cwd=proj_dir):
commits = []
if _is_hash(old_revision):
base = old_revision
else:
base = '{}~5'.format(new_revision)
for commit in (
self.m.git(
'log',
'{}..{}'.format(base, new_revision),
'--pretty=format:%H %B',
'-z',
stdout=self.m.raw_io.output(),
)
.stdout.strip('\0')
.split('\0')
):
hash, message = commit.split(' ', 1)
commits.append(Commit(hash, message))
return commits
def message(self, project_name, proj_dir, old_revision, new_revision):
with self.m.step.nest('roll message'):
commits = self._commits(proj_dir, old_revision, new_revision)
if len(commits) > 1:
return self._multiple_commits_roll_message(
project_name, commits, old_revision, new_revision
)
else:
return self._single_commit_roll_message(
project_name, commits[0], old_revision, new_revision
)
def check_roll_direction(
self, git_dir, old, new, name='check roll direction'
):
"""Return if old is an ancestor of new (i.e., a "forward" roll)."""
if old == new:
with self.m.step.nest(name) as pres:
pres.step_summary_text = 'up-to-date'
return False
with self.m.context(git_dir):
step = self.m.git(
'merge-base',
'--is-ancestor',
old,
new,
name=name,
ok_ret=(0, 1),
)
step.presentation.step_summary_text = 'backward'
if step.exc_result.retcode == 0:
step.presentation.step_summary_text = 'forward'
return step.exc_result.retcode == 0
def backwards_roll_step(self, remote, old_revision, new_revision):
with self.m.step.nest('cancelling roll') as pres:
fmt = (
'not updating from {old} to {new} because {old} is newer '
'than {new}'
)
if old_revision == new_revision:
fmt = (
'not updating from {old} to {new} because they are '
'identical'
)
pres.step_summary_text = fmt.format(
old=old_revision[0:7], new=new_revision[0:7]
)
pres.links[old_revision] = '{}/+/{}'.format(remote, old_revision)
pres.links[new_revision] = '{}/+/{}'.format(remote, new_revision)