blob: 76d76673de04c3f3fe574f69e8fdb71bf42afe57 [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
Rob Mohrcbee3fc2020-11-30 14:07:42 -080018import urlparse
Rob Mohr092a4462020-06-16 10:23:54 -070019
20import attr
Rob Mohrf7e30152020-11-18 09:56:30 -080021import enum
Rob Mohr092a4462020-06-16 10:23:54 -070022from recipe_engine import recipe_api
23
24# If we're embedding the original commit message, prepend 'Original-' to lines
25# which begin with these tags.
26ESCAPE_TAGS = [
27 'Bug:',
28 'Fixed:',
Rob Mohr2ec49a02020-12-15 08:37:21 -080029 'Fixes:',
Rob Mohrf6ee7cb2020-10-26 14:05:06 -070030 'Requires:',
Rob Mohr092a4462020-06-16 10:23:54 -070031 'Reviewed-on:',
32]
33
34# If we're embedding the original commit message, remove lines which contain
35# these tags.
36FILTER_TAGS = [
37 'API-Review:',
38 'Acked-by:',
39 'CC:',
40 'CQ-Do-Not-Cancel-Tryjobs:',
41 'Change-Id:',
42 'Commit-Queue:',
Rob Mohr4c6a5382021-02-25 08:50:44 -080043 'Cq-Cl-Tag:',
Rob Mohr092a4462020-06-16 10:23:54 -070044 'Reviewed-by:',
Rob Mohr4c6a5382021-02-25 08:50:44 -080045 'Roller-URL:',
Rob Mohr092a4462020-06-16 10:23:54 -070046 'Signed-off-by:',
47 'Testability-Review:',
48 'Tested-by:',
Rob Mohr7cb53c42020-11-18 10:45:13 -080049 'Auto-Submit',
50 re.compile(r'^\w+-Auto-Submit:'),
Rob Mohr092a4462020-06-16 10:23:54 -070051]
52
53
Rob Mohr7cb53c42020-11-18 10:45:13 -080054def _match_tag(line, tag):
55 if hasattr(tag, 'match'):
56 return tag.match(line)
57 return line.startswith(tag)
58
59
Rob Mohr092a4462020-06-16 10:23:54 -070060def _sanitize_message(message):
61 """Sanitize lines of a commit message.
62
63 Prepend 'Original-' to lines which begin with ESCAPE_TAGS. Filter
64 out lines which begin with FILTER_TAGS.
65 """
66 return '\n'.join(
67 "Original-" + line
68 if any((line.startswith(tag) for tag in ESCAPE_TAGS))
69 else line
70 for line in message.splitlines()
Rob Mohr7cb53c42020-11-18 10:45:13 -080071 if not any((_match_tag(line, tag) for tag in FILTER_TAGS))
Rob Mohr092a4462020-06-16 10:23:54 -070072 )
73
74
Rob Mohrf7e30152020-11-18 09:56:30 -080075class _Direction(enum.Enum):
76 CURRENT = 'CURRENT'
77 FORWARD = 'FORWARD'
78 BACKWARD = 'BACKWARD'
79 REBASE = 'REBASE'
80
81
Rob Mohr092a4462020-06-16 10:23:54 -070082@attr.s
83class Commit(object):
Rob Mohr57204602020-09-23 08:41:18 -070084 hash = attr.ib()
85 message = attr.ib()
Rob Mohr092a4462020-06-16 10:23:54 -070086
87
Rob Mohrf6ee7cb2020-10-26 14:05:06 -070088@attr.s
89class Roll(object):
90 _api = attr.ib()
91 project_name = attr.ib(type=str)
92 old_revision = attr.ib(type=str)
93 new_revision = attr.ib(type=str)
94 proj_dir = attr.ib(type=str)
Rob Mohrf7e30152020-11-18 09:56:30 -080095 direction = attr.ib(type=str)
Rob Mohrf6ee7cb2020-10-26 14:05:06 -070096 _commit_data = attr.ib(default=None)
Rob Mohrcbee3fc2020-11-30 14:07:42 -080097 _remote = attr.ib(type=str, default=None)
Rob Mohrf6ee7cb2020-10-26 14:05:06 -070098
Rob Mohrf7e30152020-11-18 09:56:30 -080099 @direction.validator
100 def check(self, _, value): # pragma: no cover
101 if value not in _Direction:
102 raise ValueError('invalid direction: {}'.format(value))
103 if value == _Direction.CURRENT:
104 raise ValueError('attempt to do a no-op roll')
105
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700106 @property
107 def commits(self):
108 if self._commit_data:
109 return self._commit_data
110
111 with self._api.context(cwd=self.proj_dir):
112 with self._api.step.nest(self.project_name):
113 commits = []
Rob Mohrf7e30152020-11-18 09:56:30 -0800114 if (
Rob Mohr476a7042020-11-30 13:33:52 -0800115 _is_hash(self.old_revision)
116 and self.direction == _Direction.FORWARD
Rob Mohrf7e30152020-11-18 09:56:30 -0800117 ):
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700118 base = self.old_revision
119 else:
120 base = '{}~5'.format(self.new_revision)
121
122 for commit in (
123 self._api.git(
124 'git log',
125 'log',
126 '{}..{}'.format(base, self.new_revision),
127 '--pretty=format:%H %B',
128 # Separate entries with null bytes since most entries
129 # will contain newlines ("%B" is the full commit
130 # message, not just the first line.)
131 '-z',
132 stdout=self._api.raw_io.output(),
133 )
134 .stdout.strip('\0')
135 .split('\0')
136 ):
137 hash, message = commit.split(' ', 1)
138 commits.append(Commit(hash, message))
139
140 self._commit_data = tuple(commits)
141 return self._commit_data
142
Rob Mohrcbee3fc2020-11-30 14:07:42 -0800143 @property
144 def remote(self):
145 if self._remote:
146 return self._remote
147
148 api = self._api
149
150 with api.step.nest('remote'), api.context(cwd=self.proj_dir):
151 name = api.git(
152 'name',
153 'remote',
154 stdout=api.raw_io.output(),
155 step_test_data=lambda: api.raw_io.test_api.stream_output(
156 'origin'
157 ),
158 ).stdout.strip()
159
160 remote = api.git(
161 'url',
162 'remote',
163 'get-url',
164 name,
165 stdout=api.raw_io.output(),
166 step_test_data=lambda: api.raw_io.test_api.stream_output(
167 'sso://pigweed/pigweed/pigweed'
168 ),
169 ).stdout.strip()
170
171 self._remote = api.sso.sso_to_https(remote)
172 return self._remote
173
174 @property
175 def gerrit_name(self):
176 return urlparse.urlparse(self.remote).netloc.split('.')[0]
177
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700178
179@attr.s
180class Message(object):
181 name = attr.ib(type=str)
182 template = attr.ib(type=str)
183 kwargs = attr.ib(type=dict)
184 num_commits = attr.ib(type=int)
185 footer = attr.ib(type=tuple, default=())
186
187 def render(self, with_footer=True):
188 result = [self.template.format(**self.kwargs)]
189 if with_footer:
190 result.extend(x for x in self.footer)
191 return '\n'.join(result)
192
193
Rob Mohr092a4462020-06-16 10:23:54 -0700194def _is_hash(value):
Rob Mohr57204602020-09-23 08:41:18 -0700195 return re.match(r'^[0-9a-fA-F]{40}', value)
Rob Mohr092a4462020-06-16 10:23:54 -0700196
197
Rob Mohrd39ea9e2020-09-22 15:01:07 -0700198class RollUtilApi(recipe_api.RecipeApi):
Rob Mohr1eb8f912020-10-07 13:17:17 -0700199 def __init__(self, props, *args, **kwargs):
200 super(RollUtilApi, self).__init__(*args, **kwargs)
201 self.labels_to_set = {x.label: x.value for x in props.labels_to_set}
202 self.labels_to_wait_on = props.labels_to_wait_on
Rob Mohr1c1ef302021-03-08 12:23:14 -0800203 self.footer = []
Rob Mohr1eb8f912020-10-07 13:17:17 -0700204
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700205 def _single_commit_roll_message(self, roll):
Rob Mohr57204602020-09-23 08:41:18 -0700206 template = """
Rob Mohr092a4462020-06-16 10:23:54 -0700207[roll {project_name}] {sanitized_message}
208
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700209{project_name} Rolled-Commits: {old_revision:.15}..{new_revision:.15}
210 """.strip()
211
212 commit = roll.commits[0]
Rob Mohr092a4462020-06-16 10:23:54 -0700213
Rob Mohr57204602020-09-23 08:41:18 -0700214 kwargs = {
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700215 'project_name': roll.project_name,
Rob Mohr57204602020-09-23 08:41:18 -0700216 'original_message': commit.message,
217 'sanitized_message': _sanitize_message(commit.message),
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700218 'old_revision': roll.old_revision,
219 'new_revision': roll.new_revision,
Rob Mohr57204602020-09-23 08:41:18 -0700220 }
Rob Mohr092a4462020-06-16 10:23:54 -0700221
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700222 message = Message(
223 name=roll.project_name,
224 template=template,
225 kwargs=kwargs,
226 num_commits=1,
227 footer=tuple(self.footer),
228 )
Rob Mohr092a4462020-06-16 10:23:54 -0700229
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700230 with self.m.step.nest(
231 'message for {}'.format(roll.project_name)
232 ) as pres:
Rob Mohr57204602020-09-23 08:41:18 -0700233 pres.logs['template'] = template
234 pres.logs['kwargs'] = pprint.pformat(kwargs)
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700235 pres.logs['message'] = message.render()
Rob Mohr092a4462020-06-16 10:23:54 -0700236
Rob Mohr57204602020-09-23 08:41:18 -0700237 return message
Rob Mohr092a4462020-06-16 10:23:54 -0700238
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700239 def _multiple_commits_roll_message(self, roll):
Rob Mohr57204602020-09-23 08:41:18 -0700240 template = """
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700241[{project_name}] Roll {num_commits} commits
Rob Mohr092a4462020-06-16 10:23:54 -0700242
243{one_liners}
244
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700245{project_name} Rolled-Commits: {old_revision:.15}..{new_revision:.15}
Rob Mohr092a4462020-06-16 10:23:54 -0700246 """.strip()
247
Rob Mohr57204602020-09-23 08:41:18 -0700248 one_liners = [
249 '{:.15} {}'.format(commit.hash, commit.message.splitlines()[0][:50])
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700250 for commit in roll.commits
Rob Mohr57204602020-09-23 08:41:18 -0700251 ]
Rob Mohr092a4462020-06-16 10:23:54 -0700252
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700253 num_commits = len(roll.commits)
254 if not _is_hash(roll.old_revision):
255 num_commits = 'multiple'
Rob Mohr57204602020-09-23 08:41:18 -0700256 one_liners.append('...')
Rob Mohr092a4462020-06-16 10:23:54 -0700257
Rob Mohr57204602020-09-23 08:41:18 -0700258 kwargs = {
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700259 'project_name': roll.project_name,
260 'num_commits': num_commits,
Rob Mohr57204602020-09-23 08:41:18 -0700261 'one_liners': '\n'.join(one_liners),
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700262 'old_revision': roll.old_revision,
263 'new_revision': roll.new_revision,
Rob Mohr57204602020-09-23 08:41:18 -0700264 }
Rob Mohr092a4462020-06-16 10:23:54 -0700265
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700266 message = Message(
267 name=roll.project_name,
268 template=template,
269 kwargs=kwargs,
270 num_commits=num_commits,
271 footer=tuple(self.footer),
272 )
Rob Mohr092a4462020-06-16 10:23:54 -0700273
Rob Mohr57204602020-09-23 08:41:18 -0700274 with self.m.step.nest('message') as pres:
275 pres.logs['template'] = template
276 pres.logs['kwargs'] = pprint.pformat(kwargs)
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700277 pres.logs['message'] = message.render()
Rob Mohr092a4462020-06-16 10:23:54 -0700278
Rob Mohr57204602020-09-23 08:41:18 -0700279 return message
Rob Mohr092a4462020-06-16 10:23:54 -0700280
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700281 def _single_roll_message(self, roll):
282 if len(roll.commits) > 1:
283 return self._multiple_commits_roll_message(roll)
284 return self._single_commit_roll_message(roll)
Rob Mohr092a4462020-06-16 10:23:54 -0700285
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700286 def _multiple_rolls_message(self, *rolls):
287 rolls = sorted(rolls, key=lambda x: x.project_name)
288
289 messages = []
290 for roll in rolls:
291 messages.append(self._single_roll_message(roll))
292
293 texts = [
294 '[roll {}] Roll {} commits'.format(
295 ', '.join(x.name for x in messages),
296 sum(x.num_commits for x in messages),
297 )
298 ]
299 texts.extend(x.render(with_footer=False) for x in messages)
300 texts.append('\n'.join('{}'.format(x) for x in self.footer))
301
302 return '\n\n'.join(texts)
303
304 def Roll(self, **kwargs):
305 """Create a Roll. See Roll class above for details."""
306 return Roll(api=self.m, **kwargs)
307
308 def message(self, *rolls):
Rob Mohr57204602020-09-23 08:41:18 -0700309 with self.m.step.nest('roll message'):
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700310 if len(rolls) > 1:
311 return self._multiple_rolls_message(*rolls)
312 return self._single_roll_message(*rolls).render()
Rob Mohrd39ea9e2020-09-22 15:01:07 -0700313
Rob Mohrf7e30152020-11-18 09:56:30 -0800314 Direction = _Direction
315
Rob Mohr476a7042020-11-30 13:33:52 -0800316 def get_roll_direction(self, git_dir, old, new, name='get roll direction'):
Rob Mohrf7e30152020-11-18 09:56:30 -0800317 """Return Direction of roll."""
Rob Mohr57204602020-09-23 08:41:18 -0700318 if old == new:
319 with self.m.step.nest(name) as pres:
320 pres.step_summary_text = 'up-to-date'
Rob Mohrf7e30152020-11-18 09:56:30 -0800321 return self.Direction.CURRENT
Rob Mohrd39ea9e2020-09-22 15:01:07 -0700322
Rob Mohr57204602020-09-23 08:41:18 -0700323 with self.m.context(git_dir):
Rob Mohrf7e30152020-11-18 09:56:30 -0800324 with self.m.step.nest(name) as pres:
325 forward = self.m.git(
326 'is forward',
327 'merge-base',
328 '--is-ancestor',
329 old,
330 new,
331 ok_ret=(0, 1),
332 )
Rob Mohrd39ea9e2020-09-22 15:01:07 -0700333
Rob Mohrf7e30152020-11-18 09:56:30 -0800334 backward = self.m.git(
335 'is backward',
336 'merge-base',
337 '--is-ancestor',
338 new,
339 old,
340 ok_ret=(0, 1),
341 )
Rob Mohrd39ea9e2020-09-22 15:01:07 -0700342
Rob Mohrf7e30152020-11-18 09:56:30 -0800343 if (
344 forward.exc_result.retcode == 0
345 and backward.exc_result.retcode != 0
346 ):
347 pres.step_summary_text = 'forward'
348 return self.Direction.FORWARD
Rob Mohrd39ea9e2020-09-22 15:01:07 -0700349
Rob Mohrf7e30152020-11-18 09:56:30 -0800350 if (
351 forward.exc_result.retcode != 0
352 and backward.exc_result.retcode == 0
353 ):
354 pres.step_summary_text = 'backward'
355 return self.Direction.BACKWARD
356
Rob Mohrcbee3fc2020-11-30 14:07:42 -0800357 # If new and old are ancestors of each other then this is the
358 # same commit. We should only hit this during testing because
359 # the comparison at the top of the function should have caught
360 # this situation.
361 if (
362 forward.exc_result.retcode == 0
363 and backward.exc_result.retcode == 0
364 ):
365 with self.m.step.nest(name) as pres:
366 pres.step_summary_text = 'up-to-date'
367 return self.Direction.CURRENT
368
Rob Mohrf7e30152020-11-18 09:56:30 -0800369 # If old is not an ancestor of new and new is not an ancestor
370 # of old then history was rewritten in some manner but we still
371 # need to update the pin.
372 pres.step_summary_text = 'rebase'
373 return self.Direction.REBASE
374
375 def can_roll(self, direction):
376 return direction in (self.Direction.FORWARD, self.Direction.REBASE)
377
378 def skip_roll_step(self, remote, old_revision, new_revision):
Rob Mohr57204602020-09-23 08:41:18 -0700379 with self.m.step.nest('cancelling roll') as pres:
380 fmt = (
381 'not updating from {old} to {new} because {old} is newer '
382 'than {new}'
383 )
384 if old_revision == new_revision:
Rob Mohrad16bc12020-09-28 06:39:55 -0700385 fmt = (
386 'not updating from {old} to {new} because they are '
387 'identical'
388 )
Rob Mohr57204602020-09-23 08:41:18 -0700389 pres.step_summary_text = fmt.format(
390 old=old_revision[0:7], new=new_revision[0:7]
391 )
392 pres.links[old_revision] = '{}/+/{}'.format(remote, old_revision)
393 pres.links[new_revision] = '{}/+/{}'.format(remote, new_revision)