roll_util: Support multi-roll commit messages
Support roll messages for rolls of multiple submodules or repo projects.
Not yet used.
Change-Id: I8a5c4c5b3e141835e720cfd16e77f9122717b3e6
Reviewed-on: https://pigweed-review.googlesource.com/c/infra/recipes/+/22404
Commit-Queue: Rob Mohr <mohrr@google.com>
Reviewed-by: Oliver Newman <olivernewman@google.com>
diff --git a/recipe_modules/roll_util/api.py b/recipe_modules/roll_util/api.py
index 96f8e5b..27d10b7 100644
--- a/recipe_modules/roll_util/api.py
+++ b/recipe_modules/roll_util/api.py
@@ -24,6 +24,7 @@
ESCAPE_TAGS = [
'Bug:',
'Fixed:',
+ 'Requires:',
'Reviewed-on:',
]
@@ -64,6 +65,65 @@
message = attr.ib()
+@attr.s
+class Roll(object):
+ _api = attr.ib()
+ project_name = attr.ib(type=str)
+ old_revision = attr.ib(type=str)
+ new_revision = attr.ib(type=str)
+ proj_dir = attr.ib(type=str)
+ _commit_data = attr.ib(default=None)
+
+ @property
+ def commits(self):
+ if self._commit_data:
+ return self._commit_data
+
+ with self._api.context(cwd=self.proj_dir):
+ with self._api.step.nest(self.project_name):
+ commits = []
+ if _is_hash(self.old_revision):
+ base = self.old_revision
+ else:
+ base = '{}~5'.format(self.new_revision)
+
+ for commit in (
+ self._api.git(
+ 'git log',
+ 'log',
+ '{}..{}'.format(base, self.new_revision),
+ '--pretty=format:%H %B',
+ # Separate entries with null bytes since most entries
+ # will contain newlines ("%B" is the full commit
+ # message, not just the first line.)
+ '-z',
+ stdout=self._api.raw_io.output(),
+ )
+ .stdout.strip('\0')
+ .split('\0')
+ ):
+ hash, message = commit.split(' ', 1)
+ commits.append(Commit(hash, message))
+
+ self._commit_data = tuple(commits)
+ return self._commit_data
+
+
+@attr.s
+class Message(object):
+ name = attr.ib(type=str)
+ template = attr.ib(type=str)
+ kwargs = attr.ib(type=dict)
+ num_commits = attr.ib(type=int)
+ footer = attr.ib(type=tuple, default=())
+
+ def render(self, with_footer=True):
+ result = [self.template.format(**self.kwargs)]
+ if with_footer:
+ result.extend(x for x in self.footer)
+ return '\n'.join(result)
+
+
def _is_hash(value):
return re.match(r'^[0-9a-fA-F]{40}', value)
@@ -73,110 +133,117 @@
super(RollUtilApi, self).__init__(*args, **kwargs)
self.labels_to_set = {x.label: x.value for x in props.labels_to_set}
self.labels_to_wait_on = props.labels_to_wait_on
+ self.footer = ['CQ-Do-Not-Cancel-Tryjobs: true']
- def _single_commit_roll_message(
- self, project_name, commit, old_revision, new_revision
- ):
+ def _single_commit_roll_message(self, roll):
template = """
[roll {project_name}] {sanitized_message}
-Rolled-Commits: {old_revision:.15}..{new_revision:.15}
-CQ-Do-Not-Cancel-Tryjobs: true
- """.strip()
+{project_name} Rolled-Commits: {old_revision:.15}..{new_revision:.15}
+ """.strip()
+
+ commit = roll.commits[0]
kwargs = {
- 'project_name': project_name,
+ 'project_name': roll.project_name,
'original_message': commit.message,
'sanitized_message': _sanitize_message(commit.message),
- 'old_revision': old_revision,
- 'new_revision': new_revision,
+ 'old_revision': roll.old_revision,
+ 'new_revision': roll.new_revision,
}
- message = template.format(**kwargs)
+ message = Message(
+ name=roll.project_name,
+ template=template,
+ kwargs=kwargs,
+ num_commits=1,
+ footer=tuple(self.footer),
+ )
- with self.m.step.nest('message') as pres:
+ with self.m.step.nest(
+ 'message for {}'.format(roll.project_name)
+ ) as pres:
pres.logs['template'] = template
pres.logs['kwargs'] = pprint.pformat(kwargs)
- pres.logs['message'] = message
+ pres.logs['message'] = message.render()
return message
- def _multiple_commits_roll_message(
- self, project_name, commits, old_revision, new_revision
- ):
+ def _multiple_commits_roll_message(self, roll):
template = """
-[{project_name}] Roll {number} commits
+[{project_name}] Roll {num_commits} commits
{one_liners}
-Rolled-Commits: {old_revision:.15}..{new_revision:.15}
-CQ-Do-Not-Cancel-Tryjobs: true
+{project_name} Rolled-Commits: {old_revision:.15}..{new_revision:.15}
""".strip()
one_liners = [
'{:.15} {}'.format(commit.hash, commit.message.splitlines()[0][:50])
- for commit in commits
+ for commit in roll.commits
]
- number = len(commits)
- if not _is_hash(old_revision):
- number = 'multiple'
+ num_commits = len(roll.commits)
+ if not _is_hash(roll.old_revision):
+ num_commits = 'multiple'
one_liners.append('...')
kwargs = {
- 'project_name': project_name,
- 'number': number,
+ 'project_name': roll.project_name,
+ 'num_commits': num_commits,
'one_liners': '\n'.join(one_liners),
- 'old_revision': old_revision,
- 'new_revision': new_revision,
+ 'old_revision': roll.old_revision,
+ 'new_revision': roll.new_revision,
}
- message = template.format(**kwargs)
+ message = Message(
+ name=roll.project_name,
+ template=template,
+ kwargs=kwargs,
+ num_commits=num_commits,
+ footer=tuple(self.footer),
+ )
with self.m.step.nest('message') as pres:
pres.logs['template'] = template
pres.logs['kwargs'] = pprint.pformat(kwargs)
- pres.logs['message'] = message
+ pres.logs['message'] = message.render()
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)
+ def _single_roll_message(self, roll):
+ if len(roll.commits) > 1:
+ return self._multiple_commits_roll_message(roll)
+ return self._single_commit_roll_message(roll)
- for commit in (
- self.m.git(
- 'git log',
- '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):
+ def _multiple_rolls_message(self, *rolls):
+ rolls = sorted(rolls, key=lambda x: x.project_name)
+
+ messages = []
+ for roll in rolls:
+ messages.append(self._single_roll_message(roll))
+
+ texts = [
+ '[roll {}] Roll {} commits'.format(
+ ', '.join(x.name for x in messages),
+ sum(x.num_commits for x in messages),
+ )
+ ]
+ texts.extend(x.render(with_footer=False) for x in messages)
+ texts.append('\n'.join('{}'.format(x) for x in self.footer))
+
+ return '\n\n'.join(texts)
+
+ def Roll(self, **kwargs):
+ """Create a Roll. See Roll class above for details."""
+ return Roll(api=self.m, **kwargs)
+
+ def message(self, *rolls):
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
- )
+ if len(rolls) > 1:
+ return self._multiple_rolls_message(*rolls)
+ return self._single_roll_message(*rolls).render()
def check_roll_direction(
self, git_dir, old, new, name='check roll direction'