blob: f20e7e18c07ae011fa42852ade461f740700ad12 [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
Rob Mohra7154242021-09-01 08:30:53 -070016import collections
Rob Mohr092a4462020-06-16 10:23:54 -070017import re
Rob Mohr46c25032022-07-26 17:01:35 +000018import urllib
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 = [
Rob Mohrf6ee7cb2020-10-26 14:05:06 -070027 'Requires:',
Rob Mohr092a4462020-06-16 10:23:54 -070028 'Reviewed-on:',
29]
30
31# If we're embedding the original commit message, remove lines which contain
32# these tags.
33FILTER_TAGS = [
34 'API-Review:',
35 'Acked-by:',
Rob Mohr8d19b382021-06-11 18:57:01 -070036 'Auto-Submit',
Rob Mohrea2c3d02022-08-30 16:53:53 +000037 re.compile(r'^\w+-?Auto-Submit:', re.IGNORECASE),
Rob Mohr650ba732022-03-17 09:40:24 -070038 'Bug:',
Rob Mohr092a4462020-06-16 10:23:54 -070039 'CC:',
40 'CQ-Do-Not-Cancel-Tryjobs:',
Rob Mohr6f9e5882021-06-21 12:50:36 -070041 'Cq-Include-Trybots:',
Rob Mohr092a4462020-06-16 10:23:54 -070042 'Change-Id:',
43 'Commit-Queue:',
Rob Mohr4c6a5382021-02-25 08:50:44 -080044 'Cq-Cl-Tag:',
Rob Mohr650ba732022-03-17 09:40:24 -070045 'Fixed:',
46 'Fixes:',
Rob Mohr4a50c2c2022-08-30 20:16:26 +000047 re.compile(r'Git[ -]?watcher:', re.IGNORECASE),
Rob Mohr8d19b382021-06-11 18:57:01 -070048 'No-Docs-Update-Reason:',
Rob Mohr6f9e5882021-06-21 12:50:36 -070049 'No-Presubmit:',
50 'No-Tree-Checks: true',
51 'No-Try: true',
Rob Mohr092a4462020-06-16 10:23:54 -070052 'Reviewed-by:',
Rob Mohr4c6a5382021-02-25 08:50:44 -080053 'Roller-URL:',
Rob Mohr092a4462020-06-16 10:23:54 -070054 'Signed-off-by:',
55 'Testability-Review:',
56 'Tested-by:',
57]
58
59
Rob Mohr7cb53c42020-11-18 10:45:13 -080060def _match_tag(line, tag):
61 if hasattr(tag, 'match'):
62 return tag.match(line)
63 return line.startswith(tag)
64
65
Rob Mohr092a4462020-06-16 10:23:54 -070066def _sanitize_message(message):
67 """Sanitize lines of a commit message.
68
69 Prepend 'Original-' to lines which begin with ESCAPE_TAGS. Filter
70 out lines which begin with FILTER_TAGS.
71 """
Rob Mohr71bb6452022-05-03 09:17:09 -070072
73 lines = message.splitlines()
74
75 # If the first line is really long create a truncated version of it, but
76 # keep the original version of the commit message around.
77 if len(lines[0]) > 80:
78 lines = [lines[0][0:50], ''] + lines
79
Rob Mohr092a4462020-06-16 10:23:54 -070080 return '\n'.join(
81 "Original-" + line
82 if any((line.startswith(tag) for tag in ESCAPE_TAGS))
83 else line
Rob Mohr71bb6452022-05-03 09:17:09 -070084 for line in lines
Rob Mohr7cb53c42020-11-18 10:45:13 -080085 if not any((_match_tag(line, tag) for tag in FILTER_TAGS))
Rob Mohr092a4462020-06-16 10:23:54 -070086 )
87
88
Rob Mohrf7e30152020-11-18 09:56:30 -080089class _Direction(enum.Enum):
90 CURRENT = 'CURRENT'
91 FORWARD = 'FORWARD'
92 BACKWARD = 'BACKWARD'
93 REBASE = 'REBASE'
94
95
Rob Mohr43c669b2022-01-10 12:44:04 -080096# Using a namedtuple instead of attrs because this should be hashable.
97Account = collections.namedtuple('Account', 'name email')
98
99
Rob Mohr092a4462020-06-16 10:23:54 -0700100@attr.s
Rob Mohr82c0e352022-07-27 20:57:40 +0000101class Commit:
Rob Mohr36984592021-03-31 15:46:38 -0700102 hash = attr.ib(type=str)
103 message = attr.ib(type=str)
104 author = attr.ib(type=str)
105 owner = attr.ib(type=str)
106 reviewers = attr.ib(type=tuple)
Rob Mohr092a4462020-06-16 10:23:54 -0700107
108
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700109@attr.s
Rob Mohr82c0e352022-07-27 20:57:40 +0000110class Roll:
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700111 _api = attr.ib()
112 project_name = attr.ib(type=str)
113 old_revision = attr.ib(type=str)
114 new_revision = attr.ib(type=str)
115 proj_dir = attr.ib(type=str)
Rob Mohrf7e30152020-11-18 09:56:30 -0800116 direction = attr.ib(type=str)
Rob Mohr36984592021-03-31 15:46:38 -0700117 commits = attr.ib(type=tuple, default=None)
118 remote = attr.ib(type=str, default=None)
Rob Mohr18549cf2021-07-27 16:15:19 -0700119 _nest_steps = attr.ib(type=bool, default=True)
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700120
Rob Mohrf7e30152020-11-18 09:56:30 -0800121 @direction.validator
122 def check(self, _, value): # pragma: no cover
123 if value not in _Direction:
124 raise ValueError('invalid direction: {}'.format(value))
125 if value == _Direction.CURRENT:
126 raise ValueError('attempt to do a no-op roll')
127
Rob Mohr36984592021-03-31 15:46:38 -0700128 def __attrs_post_init__(self):
129 self._set_remote()
Rob Mohr18549cf2021-07-27 16:15:19 -0700130 with self._api.context(cwd=self.proj_dir):
131 if self._nest_steps:
132 with self._api.step.nest(self.project_name):
133 self._set_commits()
134 else:
135 self._set_commits() # pragma: no cover
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700136
Rob Mohr36984592021-03-31 15:46:38 -0700137 def _set_commits(self):
Rob Mohr356720d2021-07-01 13:31:29 -0700138
Rob Mohr18549cf2021-07-27 16:15:19 -0700139 log_cmd = [
140 'log',
Rob Mohr43c669b2022-01-10 12:44:04 -0800141 '--pretty=format:%H\n%an\n%ae\n%B',
Rob Mohr18549cf2021-07-27 16:15:19 -0700142 # Separate entries with null bytes since most entries
143 # will contain newlines ("%B" is the full commit
144 # message, not just the first line.)
145 '-z',
146 ]
Rob Mohr356720d2021-07-01 13:31:29 -0700147
Rob Mohr18549cf2021-07-27 16:15:19 -0700148 if _is_hash(self.old_revision) and self.direction == _Direction.FORWARD:
149 log_cmd.append(
150 '{}..{}'.format(self.old_revision, self.new_revision)
151 )
152 else:
153 log_cmd.extend(('--max-count', '5', self.new_revision))
Rob Mohr356720d2021-07-01 13:31:29 -0700154
Rob Mohr74396252021-08-27 13:17:50 -0700155 log_kwargs = {'stdout': self._api.raw_io.output_text()}
Rob Mohr356720d2021-07-01 13:31:29 -0700156
Rob Mohr18549cf2021-07-27 16:15:19 -0700157 commit_log = (
158 self._api.git('git log', *log_cmd, **log_kwargs)
159 .stdout.strip('\0')
160 .split('\0')
161 )
162
163 commits = []
164 for i, commit in enumerate(commit_log):
Rob Mohr43c669b2022-01-10 12:44:04 -0800165 commit_hash, name, email, message = commit.split('\n', 3)
166 author = Account(name, email)
Rob Mohr18549cf2021-07-27 16:15:19 -0700167 owner = None
168 reviewers = []
169
170 full_host = '{}-review.googlesource.com'.format(self.gerrit_name)
171
172 changes = []
173
174 # If there are a lot of CLs in this roll only get owner and
175 # reviewer data from the first 10 so we don't make too many
176 # requests of Gerrit.
177 if i < 10:
178 changes = self._api.gerrit.change_query(
179 'get change-id',
Rob Mohr34453952021-08-04 06:46:54 -0700180 'commit:{}'.format(commit_hash),
Rob Mohr18549cf2021-07-27 16:15:19 -0700181 host=full_host,
182 test_data=self._api.json.test_api.output(
183 [{'_number': 12345}]
184 ),
185 ).json.output
186
187 if changes and len(changes) == 1:
188 number = changes[0]['_number']
189 step = self._api.gerrit.change_details(
190 'get {}'.format(number),
191 number,
192 host=full_host,
193 test_data=self._api.json.test_api.output(
194 {
Rob Mohr43c669b2022-01-10 12:44:04 -0800195 'owner': {
196 'name': 'author',
197 'email': 'author@example.com',
198 },
Rob Mohr18549cf2021-07-27 16:15:19 -0700199 'reviewers': {
200 'REVIEWER': [
Rob Mohr43c669b2022-01-10 12:44:04 -0800201 {
202 'name': 'reviewer',
203 'email': 'reviewer@example.com',
204 },
205 {
206 'name': 'nobody',
207 'email': 'nobody@google.com',
208 },
209 {
210 'name': 'robot',
211 'email': 'robot@gserviceaccount.com',
212 },
Rob Mohr18549cf2021-07-27 16:15:19 -0700213 ],
214 },
215 }
216 ),
217 ok_ret='any',
Rob Mohr0eac7812021-07-12 08:15:00 -0700218 )
219
Rob Mohr18549cf2021-07-27 16:15:19 -0700220 if step.exc_result.retcode == 0:
221 details = step.json.output
Rob Mohr43c669b2022-01-10 12:44:04 -0800222 owner = Account(
223 details['owner']['name'], details['owner']['email']
224 )
Rob Mohr18549cf2021-07-27 16:15:19 -0700225 for reviewer in details['reviewers']['REVIEWER']:
Rob Mohr43c669b2022-01-10 12:44:04 -0800226 reviewers.append(
227 Account(reviewer['name'], reviewer['email']),
228 )
Rob Mohrc7c93b82021-04-23 09:10:39 -0700229
Rob Mohr18549cf2021-07-27 16:15:19 -0700230 commits.append(
231 Commit(
Rob Mohr34453952021-08-04 06:46:54 -0700232 hash=commit_hash,
Rob Mohr18549cf2021-07-27 16:15:19 -0700233 author=author,
234 owner=owner,
235 reviewers=tuple(reviewers),
236 message=message,
237 )
238 )
Rob Mohrc7c93b82021-04-23 09:10:39 -0700239
Rob Mohr18549cf2021-07-27 16:15:19 -0700240 self.commits = tuple(commits)
Rob Mohrcbee3fc2020-11-30 14:07:42 -0800241
Rob Mohr36984592021-03-31 15:46:38 -0700242 def _set_remote(self):
Rob Mohrcbee3fc2020-11-30 14:07:42 -0800243 api = self._api
244
245 with api.step.nest('remote'), api.context(cwd=self.proj_dir):
246 name = api.git(
247 'name',
248 'remote',
Rob Mohr74396252021-08-27 13:17:50 -0700249 stdout=api.raw_io.output_text(),
250 step_test_data=lambda: api.raw_io.test_api.stream_output_text(
Rob Mohrcbee3fc2020-11-30 14:07:42 -0800251 'origin'
252 ),
253 ).stdout.strip()
254
255 remote = api.git(
256 'url',
257 'remote',
258 'get-url',
259 name,
Rob Mohr74396252021-08-27 13:17:50 -0700260 stdout=api.raw_io.output_text(),
261 step_test_data=lambda: api.raw_io.test_api.stream_output_text(
Rob Mohrcbee3fc2020-11-30 14:07:42 -0800262 'sso://pigweed/pigweed/pigweed'
263 ),
264 ).stdout.strip()
265
Rob Mohr36984592021-03-31 15:46:38 -0700266 self.remote = api.sso.sso_to_https(remote)
Rob Mohrcbee3fc2020-11-30 14:07:42 -0800267
268 @property
269 def gerrit_name(self):
Rob Mohr4c504cc2021-08-03 07:37:06 -0700270 return urllib.parse.urlparse(self.remote).netloc.split('.')[0]
Rob Mohrcbee3fc2020-11-30 14:07:42 -0800271
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700272
273@attr.s
Rob Mohr82c0e352022-07-27 20:57:40 +0000274class Message:
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700275 name = attr.ib(type=str)
276 template = attr.ib(type=str)
277 kwargs = attr.ib(type=dict)
278 num_commits = attr.ib(type=int)
279 footer = attr.ib(type=tuple, default=())
280
281 def render(self, with_footer=True):
282 result = [self.template.format(**self.kwargs)]
283 if with_footer:
284 result.extend(x for x in self.footer)
285 return '\n'.join(result)
286
287
Rob Mohr092a4462020-06-16 10:23:54 -0700288def _is_hash(value):
Rob Mohr57204602020-09-23 08:41:18 -0700289 return re.match(r'^[0-9a-fA-F]{40}', value)
Rob Mohr092a4462020-06-16 10:23:54 -0700290
291
Rob Mohr08973532021-09-01 08:34:11 -0700292def _pprint_dict(d):
293 result = []
294 for k, v in sorted(d.items()):
295 result.append('{!r}: {!r}\n'.format(k, v))
296 return ''.join(result)
297
298
Rob Mohrd39ea9e2020-09-22 15:01:07 -0700299class RollUtilApi(recipe_api.RecipeApi):
Rob Mohr1eb8f912020-10-07 13:17:17 -0700300 def __init__(self, props, *args, **kwargs):
Rob Mohr78edbd02022-03-23 16:08:16 -0700301 super().__init__(*args, **kwargs)
Rob Mohra7154242021-09-01 08:30:53 -0700302 self.labels_to_set = collections.OrderedDict()
Rob Mohr0bb03b62021-10-05 10:11:17 -0700303 for label in sorted(props.labels_to_set, key=lambda x: x.label):
Rob Mohr0ad390b2021-09-01 08:32:37 -0700304 self.labels_to_set[str(label.label)] = label.value
305 self.labels_to_wait_on = sorted(str(x) for x in props.labels_to_wait_on)
Rob Mohr1c1ef302021-03-08 12:23:14 -0800306 self.footer = []
Rob Mohr3ed6c792022-01-13 07:52:26 -0800307 self._commit_divider = props.commit_divider
Rob Mohr1eb8f912020-10-07 13:17:17 -0700308
Rob Mohr36984592021-03-31 15:46:38 -0700309 def authors(self, *roll):
310 authors = set()
311 for r in roll:
312 for commit in r.commits:
313 if commit.author:
314 authors.add(commit.author)
315 if commit.owner:
316 authors.add(commit.owner)
317 return authors
318
Rob Mohr43c669b2022-01-10 12:44:04 -0800319 def fake_author(self, author):
320 # Update the author's email address so it can be used for attribution
321 # without literally attributing it to the author's account in Gerrit.
Rob Mohr9f78be72022-06-21 09:57:16 -0700322 # Make sure not to add it twice, and there's no need to do this for
323 # service accounts.
Rob Mohre28a5a92022-01-10 15:16:37 -0800324 email = author.email
325 prefix = 'pigweed.infra.roller.'
Rob Mohr9f78be72022-06-21 09:57:16 -0700326 if prefix not in email and not email.endswith('gserviceaccount.com'):
Rob Mohre28a5a92022-01-10 15:16:37 -0800327 user, domain = author.email.split('@')
328 email = '{}@{}{}'.format(user, prefix, domain)
329
330 return Account(author.name, email,)
Rob Mohr43c669b2022-01-10 12:44:04 -0800331
Rob Mohr36984592021-03-31 15:46:38 -0700332 def reviewers(self, *roll):
333 reviewers = set()
334 for r in roll:
335 for commit in r.commits:
336 reviewers.update(commit.reviewers)
337 return reviewers
338
Rob Mohrb04227c2021-04-01 12:38:33 -0700339 def can_cc_on_roll(self, email, host):
Rob Mohr36984592021-03-31 15:46:38 -0700340 # Assume all queried accounts exist on Gerrit in testing except for
341 # nobody@google.com.
342 test_data = self.m.json.test_api.output([{'_account_id': 123}])
343 if email == 'nobody@google.com':
344 test_data = self.m.json.test_api.output([])
345
346 return self.m.gerrit.account_query(
347 email, 'email:{}'.format(email), host=host, test_data=test_data,
348 ).json.output
349
Rob Mohr43c669b2022-01-10 12:44:04 -0800350 def include_cc(self, account, cc_domains, host):
351 with self.m.step.nest('cc {}'.format(account.email)) as pres:
352 domain = account.email.split('@', 1)[1]
Rob Mohrb04227c2021-04-01 12:38:33 -0700353 if domain.endswith('gserviceaccount.com'):
354 pres.step_summary_text = 'not CCing, robot account'
355 return False
356 if cc_domains and domain not in cc_domains:
357 pres.step_summary_text = 'not CCing, domain excluded'
358 return False
Rob Mohr43c669b2022-01-10 12:44:04 -0800359 if not self.can_cc_on_roll(account.email, host=host):
Rob Mohrb04227c2021-04-01 12:38:33 -0700360 pres.step_summary_text = 'not CCing, no account in Gerrit'
361 return False
362
363 pres.step_summary_text = 'CCing'
364 return True
365
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700366 def _single_commit_roll_message(self, roll):
Rob Mohr57204602020-09-23 08:41:18 -0700367 template = """
Rob Mohr092a4462020-06-16 10:23:54 -0700368[roll {project_name}] {sanitized_message}
369
Rob Mohrfc794bc2021-08-06 11:20:33 -0700370{remote}
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700371{project_name} Rolled-Commits: {old_revision:.15}..{new_revision:.15}
372 """.strip()
373
374 commit = roll.commits[0]
Rob Mohr092a4462020-06-16 10:23:54 -0700375
Rob Mohr57204602020-09-23 08:41:18 -0700376 kwargs = {
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700377 'project_name': roll.project_name,
Rob Mohrfc794bc2021-08-06 11:20:33 -0700378 'remote': roll.remote,
Rob Mohr57204602020-09-23 08:41:18 -0700379 'original_message': commit.message,
380 'sanitized_message': _sanitize_message(commit.message),
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700381 'old_revision': roll.old_revision,
382 'new_revision': roll.new_revision,
Rob Mohr57204602020-09-23 08:41:18 -0700383 }
Rob Mohr092a4462020-06-16 10:23:54 -0700384
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700385 message = Message(
386 name=roll.project_name,
387 template=template,
388 kwargs=kwargs,
389 num_commits=1,
390 footer=tuple(self.footer),
391 )
Rob Mohr092a4462020-06-16 10:23:54 -0700392
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700393 with self.m.step.nest(
394 'message for {}'.format(roll.project_name)
395 ) as pres:
Rob Mohr57204602020-09-23 08:41:18 -0700396 pres.logs['template'] = template
Rob Mohr08973532021-09-01 08:34:11 -0700397 pres.logs['kwargs'] = _pprint_dict(kwargs)
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700398 pres.logs['message'] = message.render()
Rob Mohr092a4462020-06-16 10:23:54 -0700399
Rob Mohr57204602020-09-23 08:41:18 -0700400 return message
Rob Mohr092a4462020-06-16 10:23:54 -0700401
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700402 def _multiple_commits_roll_message(self, roll):
Rob Mohr57204602020-09-23 08:41:18 -0700403 template = """
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700404[{project_name}] Roll {num_commits} commits
Rob Mohr092a4462020-06-16 10:23:54 -0700405
406{one_liners}
407
Rob Mohrfc794bc2021-08-06 11:20:33 -0700408{remote}
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700409{project_name} Rolled-Commits: {old_revision:.15}..{new_revision:.15}
Rob Mohr092a4462020-06-16 10:23:54 -0700410 """.strip()
411
Rob Mohr57204602020-09-23 08:41:18 -0700412 one_liners = [
413 '{:.15} {}'.format(commit.hash, commit.message.splitlines()[0][:50])
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700414 for commit in roll.commits
Rob Mohr57204602020-09-23 08:41:18 -0700415 ]
Rob Mohr092a4462020-06-16 10:23:54 -0700416
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700417 num_commits = len(roll.commits)
Rob Mohr10b99202022-06-07 12:12:27 -0700418
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700419 if not _is_hash(roll.old_revision):
420 num_commits = 'multiple'
Rob Mohr57204602020-09-23 08:41:18 -0700421 one_liners.append('...')
Rob Mohr092a4462020-06-16 10:23:54 -0700422
Rob Mohr10b99202022-06-07 12:12:27 -0700423 if len(one_liners) > 100:
424 one_liners = one_liners[0:5] + ['...'] + one_liners[-5:]
425 # In case both this and the previous condition match.
426 if one_liners[-1] == '...':
427 one_liners.pop() # pragma: no cover
428
Rob Mohr57204602020-09-23 08:41:18 -0700429 kwargs = {
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700430 'project_name': roll.project_name,
Rob Mohrfc794bc2021-08-06 11:20:33 -0700431 'remote': roll.remote,
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700432 'num_commits': num_commits,
Rob Mohr57204602020-09-23 08:41:18 -0700433 'one_liners': '\n'.join(one_liners),
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700434 'old_revision': roll.old_revision,
435 'new_revision': roll.new_revision,
Rob Mohr57204602020-09-23 08:41:18 -0700436 }
Rob Mohr092a4462020-06-16 10:23:54 -0700437
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700438 message = Message(
439 name=roll.project_name,
440 template=template,
441 kwargs=kwargs,
442 num_commits=num_commits,
443 footer=tuple(self.footer),
444 )
Rob Mohr092a4462020-06-16 10:23:54 -0700445
Rob Mohr57204602020-09-23 08:41:18 -0700446 with self.m.step.nest('message') as pres:
447 pres.logs['template'] = template
Rob Mohr08973532021-09-01 08:34:11 -0700448 pres.logs['kwargs'] = _pprint_dict(kwargs)
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700449 pres.logs['message'] = message.render()
Rob Mohr092a4462020-06-16 10:23:54 -0700450
Rob Mohr57204602020-09-23 08:41:18 -0700451 return message
Rob Mohr092a4462020-06-16 10:23:54 -0700452
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700453 def _single_roll_message(self, roll):
454 if len(roll.commits) > 1:
455 return self._multiple_commits_roll_message(roll)
456 return self._single_commit_roll_message(roll)
Rob Mohr092a4462020-06-16 10:23:54 -0700457
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700458 def _multiple_rolls_message(self, *rolls):
459 rolls = sorted(rolls, key=lambda x: x.project_name)
460
461 messages = []
462 for roll in rolls:
463 messages.append(self._single_roll_message(roll))
464
465 texts = [
466 '[roll {}] Roll {} commits'.format(
467 ', '.join(x.name for x in messages),
468 sum(x.num_commits for x in messages),
469 )
470 ]
471 texts.extend(x.render(with_footer=False) for x in messages)
472 texts.append('\n'.join('{}'.format(x) for x in self.footer))
473
474 return '\n\n'.join(texts)
475
Rob Mohr43c669b2022-01-10 12:44:04 -0800476 def Account(self, name, email):
477 return Account(name, email)
478
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700479 def Roll(self, **kwargs):
480 """Create a Roll. See Roll class above for details."""
481 return Roll(api=self.m, **kwargs)
482
483 def message(self, *rolls):
Rob Mohr57204602020-09-23 08:41:18 -0700484 with self.m.step.nest('roll message'):
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700485 if len(rolls) > 1:
Rob Mohr3ed6c792022-01-13 07:52:26 -0800486 result = self._multiple_rolls_message(*rolls)
487 else:
488 result = self._single_roll_message(*rolls).render()
489 if self._commit_divider:
490 result += '\n{}'.format(self._commit_divider)
491 return result
Rob Mohrd39ea9e2020-09-22 15:01:07 -0700492
Rob Mohrf7e30152020-11-18 09:56:30 -0800493 Direction = _Direction
494
Rob Mohr476a7042020-11-30 13:33:52 -0800495 def get_roll_direction(self, git_dir, old, new, name='get roll direction'):
Rob Mohrf7e30152020-11-18 09:56:30 -0800496 """Return Direction of roll."""
Rob Mohr57204602020-09-23 08:41:18 -0700497 if old == new:
498 with self.m.step.nest(name) as pres:
499 pres.step_summary_text = 'up-to-date'
Rob Mohrf7e30152020-11-18 09:56:30 -0800500 return self.Direction.CURRENT
Rob Mohrd39ea9e2020-09-22 15:01:07 -0700501
Rob Mohr57204602020-09-23 08:41:18 -0700502 with self.m.context(git_dir):
Rob Mohrf7e30152020-11-18 09:56:30 -0800503 with self.m.step.nest(name) as pres:
504 forward = self.m.git(
505 'is forward',
506 'merge-base',
507 '--is-ancestor',
508 old,
509 new,
510 ok_ret=(0, 1),
511 )
Rob Mohrd39ea9e2020-09-22 15:01:07 -0700512
Rob Mohrf7e30152020-11-18 09:56:30 -0800513 backward = self.m.git(
514 'is backward',
515 'merge-base',
516 '--is-ancestor',
517 new,
518 old,
519 ok_ret=(0, 1),
520 )
Rob Mohrd39ea9e2020-09-22 15:01:07 -0700521
Rob Mohrf7e30152020-11-18 09:56:30 -0800522 if (
523 forward.exc_result.retcode == 0
524 and backward.exc_result.retcode != 0
525 ):
526 pres.step_summary_text = 'forward'
527 return self.Direction.FORWARD
Rob Mohrd39ea9e2020-09-22 15:01:07 -0700528
Rob Mohrf7e30152020-11-18 09:56:30 -0800529 if (
530 forward.exc_result.retcode != 0
531 and backward.exc_result.retcode == 0
532 ):
533 pres.step_summary_text = 'backward'
534 return self.Direction.BACKWARD
535
Rob Mohrcbee3fc2020-11-30 14:07:42 -0800536 # If new and old are ancestors of each other then this is the
537 # same commit. We should only hit this during testing because
538 # the comparison at the top of the function should have caught
539 # this situation.
540 if (
541 forward.exc_result.retcode == 0
542 and backward.exc_result.retcode == 0
543 ):
544 with self.m.step.nest(name) as pres:
545 pres.step_summary_text = 'up-to-date'
546 return self.Direction.CURRENT
547
Rob Mohrf7e30152020-11-18 09:56:30 -0800548 # If old is not an ancestor of new and new is not an ancestor
549 # of old then history was rewritten in some manner but we still
550 # need to update the pin.
551 pres.step_summary_text = 'rebase'
552 return self.Direction.REBASE
553
554 def can_roll(self, direction):
555 return direction in (self.Direction.FORWARD, self.Direction.REBASE)
556
557 def skip_roll_step(self, remote, old_revision, new_revision):
Rob Mohr57204602020-09-23 08:41:18 -0700558 with self.m.step.nest('cancelling roll') as pres:
559 fmt = (
560 'not updating from {old} to {new} because {old} is newer '
561 'than {new}'
562 )
563 if old_revision == new_revision:
Rob Mohrad16bc12020-09-28 06:39:55 -0700564 fmt = (
565 'not updating from {old} to {new} because they are '
566 'identical'
567 )
Rob Mohr57204602020-09-23 08:41:18 -0700568 pres.step_summary_text = fmt.format(
569 old=old_revision[0:7], new=new_revision[0:7]
570 )
571 pres.links[old_revision] = '{}/+/{}'.format(remote, old_revision)
572 pres.links[new_revision] = '{}/+/{}'.format(remote, new_revision)
Rob Mohr18549cf2021-07-27 16:15:19 -0700573
574 def normalize_remote(self, remote, base):
575 """Convert relative paths to absolute paths.
576
577 Support relative paths. If the top-level project is
578 "https://pigweed.googlesource.com/ex/ample" then a submodule path of
579 "./abc" maps to "https://pigweed.googlesource.com/ex/ample/abc" and
580 "../abc" maps to "https://pigweed.googlesource.com/ex/abc". Minimal
581 error-checking because git does most of these checks for us.
582
583 Also converts sso to https.
584
585 Args:
586 remote (str): Submodule remote URL.
587 base (str): Fully-qualified superproject remote URL.
588 """
589 if remote.startswith('.'):
590 remote = '/'.join((base.rstrip('/'), remote.lstrip('/')))
591
592 changes = 1
593 while changes:
594 changes = 0
595
Rob Mohr98ca1522021-11-03 14:26:12 -0700596 remote, n = re.subn(r'/\./', '/', remote)
Rob Mohr18549cf2021-07-27 16:15:19 -0700597 changes += n
598
Rob Mohr98ca1522021-11-03 14:26:12 -0700599 remote, n = re.subn(r'/[^/]+/\.\./', '/', remote)
Rob Mohr18549cf2021-07-27 16:15:19 -0700600 changes += n
601
602 return self.m.sso.sso_to_https(remote)