blob: 3a0752387c97000035b268eb31136064b58d6412 [file] [log] [blame]
Rob Mohr092a4462020-06-16 10:23:54 -07001# Copyright 2020 The Pigweed Authors
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may not
4# use this file except in compliance with the License. You may obtain a copy of
5# the License at
6#
7# https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations under
13# the License.
Rob Mohrd39ea9e2020-09-22 15:01:07 -070014"""Utility functions for rollers."""
Rob Mohr092a4462020-06-16 10:23:54 -070015
16import pprint
17import re
18
19import attr
Rob Mohrf7e30152020-11-18 09:56:30 -080020import enum
Rob Mohr092a4462020-06-16 10:23:54 -070021from recipe_engine import recipe_api
22
23# If we're embedding the original commit message, prepend 'Original-' to lines
24# which begin with these tags.
25ESCAPE_TAGS = [
26 'Bug:',
27 'Fixed:',
Rob Mohrf6ee7cb2020-10-26 14:05:06 -070028 'Requires:',
Rob Mohr092a4462020-06-16 10:23:54 -070029 'Reviewed-on:',
30]
31
32# If we're embedding the original commit message, remove lines which contain
33# these tags.
34FILTER_TAGS = [
35 'API-Review:',
36 'Acked-by:',
37 'CC:',
38 'CQ-Do-Not-Cancel-Tryjobs:',
39 'Change-Id:',
40 'Commit-Queue:',
41 'Reviewed-by:',
42 'Signed-off-by:',
43 'Testability-Review:',
44 'Tested-by:',
Rob Mohr7cb53c42020-11-18 10:45:13 -080045 'Auto-Submit',
46 re.compile(r'^\w+-Auto-Submit:'),
Rob Mohr092a4462020-06-16 10:23:54 -070047]
48
49
Rob Mohr7cb53c42020-11-18 10:45:13 -080050def _match_tag(line, tag):
51 if hasattr(tag, 'match'):
52 return tag.match(line)
53 return line.startswith(tag)
54
55
Rob Mohr092a4462020-06-16 10:23:54 -070056def _sanitize_message(message):
57 """Sanitize lines of a commit message.
58
59 Prepend 'Original-' to lines which begin with ESCAPE_TAGS. Filter
60 out lines which begin with FILTER_TAGS.
61 """
62 return '\n'.join(
63 "Original-" + line
64 if any((line.startswith(tag) for tag in ESCAPE_TAGS))
65 else line
66 for line in message.splitlines()
Rob Mohr7cb53c42020-11-18 10:45:13 -080067 if not any((_match_tag(line, tag) for tag in FILTER_TAGS))
Rob Mohr092a4462020-06-16 10:23:54 -070068 )
69
70
Rob Mohrf7e30152020-11-18 09:56:30 -080071class _Direction(enum.Enum):
72 CURRENT = 'CURRENT'
73 FORWARD = 'FORWARD'
74 BACKWARD = 'BACKWARD'
75 REBASE = 'REBASE'
76
77
Rob Mohr092a4462020-06-16 10:23:54 -070078@attr.s
79class Commit(object):
Rob Mohr57204602020-09-23 08:41:18 -070080 hash = attr.ib()
81 message = attr.ib()
Rob Mohr092a4462020-06-16 10:23:54 -070082
83
Rob Mohrf6ee7cb2020-10-26 14:05:06 -070084@attr.s
85class Roll(object):
86 _api = attr.ib()
87 project_name = attr.ib(type=str)
88 old_revision = attr.ib(type=str)
89 new_revision = attr.ib(type=str)
90 proj_dir = attr.ib(type=str)
Rob Mohrf7e30152020-11-18 09:56:30 -080091 direction = attr.ib(type=str)
Rob Mohrf6ee7cb2020-10-26 14:05:06 -070092 _commit_data = attr.ib(default=None)
93
Rob Mohrf7e30152020-11-18 09:56:30 -080094 @direction.validator
95 def check(self, _, value): # pragma: no cover
96 if value not in _Direction:
97 raise ValueError('invalid direction: {}'.format(value))
98 if value == _Direction.CURRENT:
99 raise ValueError('attempt to do a no-op roll')
100
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700101 @property
102 def commits(self):
103 if self._commit_data:
104 return self._commit_data
105
106 with self._api.context(cwd=self.proj_dir):
107 with self._api.step.nest(self.project_name):
108 commits = []
Rob Mohrf7e30152020-11-18 09:56:30 -0800109 if (
110 _is_hash(self.old_revision) and
111 self.direction == _Direction.FORWARD
112 ):
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700113 base = self.old_revision
114 else:
115 base = '{}~5'.format(self.new_revision)
116
117 for commit in (
118 self._api.git(
119 'git log',
120 'log',
121 '{}..{}'.format(base, self.new_revision),
122 '--pretty=format:%H %B',
123 # Separate entries with null bytes since most entries
124 # will contain newlines ("%B" is the full commit
125 # message, not just the first line.)
126 '-z',
127 stdout=self._api.raw_io.output(),
128 )
129 .stdout.strip('\0')
130 .split('\0')
131 ):
132 hash, message = commit.split(' ', 1)
133 commits.append(Commit(hash, message))
134
135 self._commit_data = tuple(commits)
136 return self._commit_data
137
138
139@attr.s
140class Message(object):
141 name = attr.ib(type=str)
142 template = attr.ib(type=str)
143 kwargs = attr.ib(type=dict)
144 num_commits = attr.ib(type=int)
145 footer = attr.ib(type=tuple, default=())
146
147 def render(self, with_footer=True):
148 result = [self.template.format(**self.kwargs)]
149 if with_footer:
150 result.extend(x for x in self.footer)
151 return '\n'.join(result)
152
153
Rob Mohr092a4462020-06-16 10:23:54 -0700154def _is_hash(value):
Rob Mohr57204602020-09-23 08:41:18 -0700155 return re.match(r'^[0-9a-fA-F]{40}', value)
Rob Mohr092a4462020-06-16 10:23:54 -0700156
157
Rob Mohrd39ea9e2020-09-22 15:01:07 -0700158class RollUtilApi(recipe_api.RecipeApi):
Rob Mohr1eb8f912020-10-07 13:17:17 -0700159 def __init__(self, props, *args, **kwargs):
160 super(RollUtilApi, self).__init__(*args, **kwargs)
161 self.labels_to_set = {x.label: x.value for x in props.labels_to_set}
162 self.labels_to_wait_on = props.labels_to_wait_on
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700163 self.footer = ['CQ-Do-Not-Cancel-Tryjobs: true']
Rob Mohr1eb8f912020-10-07 13:17:17 -0700164
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700165 def _single_commit_roll_message(self, roll):
Rob Mohr57204602020-09-23 08:41:18 -0700166 template = """
Rob Mohr092a4462020-06-16 10:23:54 -0700167[roll {project_name}] {sanitized_message}
168
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700169{project_name} Rolled-Commits: {old_revision:.15}..{new_revision:.15}
170 """.strip()
171
172 commit = roll.commits[0]
Rob Mohr092a4462020-06-16 10:23:54 -0700173
Rob Mohr57204602020-09-23 08:41:18 -0700174 kwargs = {
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700175 'project_name': roll.project_name,
Rob Mohr57204602020-09-23 08:41:18 -0700176 'original_message': commit.message,
177 'sanitized_message': _sanitize_message(commit.message),
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700178 'old_revision': roll.old_revision,
179 'new_revision': roll.new_revision,
Rob Mohr57204602020-09-23 08:41:18 -0700180 }
Rob Mohr092a4462020-06-16 10:23:54 -0700181
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700182 message = Message(
183 name=roll.project_name,
184 template=template,
185 kwargs=kwargs,
186 num_commits=1,
187 footer=tuple(self.footer),
188 )
Rob Mohr092a4462020-06-16 10:23:54 -0700189
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700190 with self.m.step.nest(
191 'message for {}'.format(roll.project_name)
192 ) as pres:
Rob Mohr57204602020-09-23 08:41:18 -0700193 pres.logs['template'] = template
194 pres.logs['kwargs'] = pprint.pformat(kwargs)
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700195 pres.logs['message'] = message.render()
Rob Mohr092a4462020-06-16 10:23:54 -0700196
Rob Mohr57204602020-09-23 08:41:18 -0700197 return message
Rob Mohr092a4462020-06-16 10:23:54 -0700198
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700199 def _multiple_commits_roll_message(self, roll):
Rob Mohr57204602020-09-23 08:41:18 -0700200 template = """
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700201[{project_name}] Roll {num_commits} commits
Rob Mohr092a4462020-06-16 10:23:54 -0700202
203{one_liners}
204
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700205{project_name} Rolled-Commits: {old_revision:.15}..{new_revision:.15}
Rob Mohr092a4462020-06-16 10:23:54 -0700206 """.strip()
207
Rob Mohr57204602020-09-23 08:41:18 -0700208 one_liners = [
209 '{:.15} {}'.format(commit.hash, commit.message.splitlines()[0][:50])
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700210 for commit in roll.commits
Rob Mohr57204602020-09-23 08:41:18 -0700211 ]
Rob Mohr092a4462020-06-16 10:23:54 -0700212
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700213 num_commits = len(roll.commits)
214 if not _is_hash(roll.old_revision):
215 num_commits = 'multiple'
Rob Mohr57204602020-09-23 08:41:18 -0700216 one_liners.append('...')
Rob Mohr092a4462020-06-16 10:23:54 -0700217
Rob Mohr57204602020-09-23 08:41:18 -0700218 kwargs = {
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700219 'project_name': roll.project_name,
220 'num_commits': num_commits,
Rob Mohr57204602020-09-23 08:41:18 -0700221 'one_liners': '\n'.join(one_liners),
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700222 'old_revision': roll.old_revision,
223 'new_revision': roll.new_revision,
Rob Mohr57204602020-09-23 08:41:18 -0700224 }
Rob Mohr092a4462020-06-16 10:23:54 -0700225
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700226 message = Message(
227 name=roll.project_name,
228 template=template,
229 kwargs=kwargs,
230 num_commits=num_commits,
231 footer=tuple(self.footer),
232 )
Rob Mohr092a4462020-06-16 10:23:54 -0700233
Rob Mohr57204602020-09-23 08:41:18 -0700234 with self.m.step.nest('message') as pres:
235 pres.logs['template'] = template
236 pres.logs['kwargs'] = pprint.pformat(kwargs)
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700237 pres.logs['message'] = message.render()
Rob Mohr092a4462020-06-16 10:23:54 -0700238
Rob Mohr57204602020-09-23 08:41:18 -0700239 return message
Rob Mohr092a4462020-06-16 10:23:54 -0700240
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700241 def _single_roll_message(self, roll):
242 if len(roll.commits) > 1:
243 return self._multiple_commits_roll_message(roll)
244 return self._single_commit_roll_message(roll)
Rob Mohr092a4462020-06-16 10:23:54 -0700245
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700246 def _multiple_rolls_message(self, *rolls):
247 rolls = sorted(rolls, key=lambda x: x.project_name)
248
249 messages = []
250 for roll in rolls:
251 messages.append(self._single_roll_message(roll))
252
253 texts = [
254 '[roll {}] Roll {} commits'.format(
255 ', '.join(x.name for x in messages),
256 sum(x.num_commits for x in messages),
257 )
258 ]
259 texts.extend(x.render(with_footer=False) for x in messages)
260 texts.append('\n'.join('{}'.format(x) for x in self.footer))
261
262 return '\n\n'.join(texts)
263
264 def Roll(self, **kwargs):
265 """Create a Roll. See Roll class above for details."""
266 return Roll(api=self.m, **kwargs)
267
268 def message(self, *rolls):
Rob Mohr57204602020-09-23 08:41:18 -0700269 with self.m.step.nest('roll message'):
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700270 if len(rolls) > 1:
271 return self._multiple_rolls_message(*rolls)
272 return self._single_roll_message(*rolls).render()
Rob Mohrd39ea9e2020-09-22 15:01:07 -0700273
Rob Mohrf7e30152020-11-18 09:56:30 -0800274 Direction = _Direction
275
276 def get_roll_direction(
277 self, git_dir, old, new, name='get roll direction'
Rob Mohr57204602020-09-23 08:41:18 -0700278 ):
Rob Mohrf7e30152020-11-18 09:56:30 -0800279 """Return Direction of roll."""
Rob Mohr57204602020-09-23 08:41:18 -0700280 if old == new:
281 with self.m.step.nest(name) as pres:
282 pres.step_summary_text = 'up-to-date'
Rob Mohrf7e30152020-11-18 09:56:30 -0800283 return self.Direction.CURRENT
Rob Mohrd39ea9e2020-09-22 15:01:07 -0700284
Rob Mohr57204602020-09-23 08:41:18 -0700285 with self.m.context(git_dir):
Rob Mohrf7e30152020-11-18 09:56:30 -0800286 with self.m.step.nest(name) as pres:
287 forward = self.m.git(
288 'is forward',
289 'merge-base',
290 '--is-ancestor',
291 old,
292 new,
293 ok_ret=(0, 1),
294 )
Rob Mohrd39ea9e2020-09-22 15:01:07 -0700295
Rob Mohrf7e30152020-11-18 09:56:30 -0800296 backward = self.m.git(
297 'is backward',
298 'merge-base',
299 '--is-ancestor',
300 new,
301 old,
302 ok_ret=(0, 1),
303 )
Rob Mohrd39ea9e2020-09-22 15:01:07 -0700304
Rob Mohrf7e30152020-11-18 09:56:30 -0800305 if (
306 forward.exc_result.retcode == 0
307 and backward.exc_result.retcode != 0
308 ):
309 pres.step_summary_text = 'forward'
310 return self.Direction.FORWARD
Rob Mohrd39ea9e2020-09-22 15:01:07 -0700311
Rob Mohrf7e30152020-11-18 09:56:30 -0800312 if (
313 forward.exc_result.retcode != 0
314 and backward.exc_result.retcode == 0
315 ):
316 pres.step_summary_text = 'backward'
317 return self.Direction.BACKWARD
318
319 # If old is not an ancestor of new and new is not an ancestor
320 # of old then history was rewritten in some manner but we still
321 # need to update the pin.
322 pres.step_summary_text = 'rebase'
323 return self.Direction.REBASE
324
325 def can_roll(self, direction):
326 return direction in (self.Direction.FORWARD, self.Direction.REBASE)
327
328 def skip_roll_step(self, remote, old_revision, new_revision):
Rob Mohr57204602020-09-23 08:41:18 -0700329 with self.m.step.nest('cancelling roll') as pres:
330 fmt = (
331 'not updating from {old} to {new} because {old} is newer '
332 'than {new}'
333 )
334 if old_revision == new_revision:
Rob Mohrad16bc12020-09-28 06:39:55 -0700335 fmt = (
336 'not updating from {old} to {new} because they are '
337 'identical'
338 )
Rob Mohr57204602020-09-23 08:41:18 -0700339 pres.step_summary_text = fmt.format(
340 old=old_revision[0:7], new=new_revision[0:7]
341 )
342 pres.links[old_revision] = '{}/+/{}'.format(remote, old_revision)
343 pres.links[new_revision] = '{}/+/{}'.format(remote, new_revision)