blob: 7cf479363141fb00171c1bb77d60cada83bdfb4f [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 Mohr90c712c2022-09-06 15:46:17 +000027 'Bug:',
28 'Fixed:',
29 '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:',
Rob Mohrea2c3d02022-08-30 16:53:53 +000039 re.compile(r'^\w+-?Auto-Submit:', re.IGNORECASE),
Rob Mohr20b35362022-10-10 18:54:39 +000040 'Build-Errors:',
Rob Mohr092a4462020-06-16 10:23:54 -070041 'CC:',
42 'CQ-Do-Not-Cancel-Tryjobs:',
Rob Mohr6f9e5882021-06-21 12:50:36 -070043 'Cq-Include-Trybots:',
Rob Mohr092a4462020-06-16 10:23:54 -070044 'Change-Id:',
45 'Commit-Queue:',
Rob Mohr4c6a5382021-02-25 08:50:44 -080046 'Cq-Cl-Tag:',
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 Mohr3d02b1d2023-04-25 17:31:01 +000052 'Presubmit-Verified:',
Rob Mohrcf656842022-11-29 17:52:08 +000053 re.compile(r'^\w+-?Readability-Trivial:', re.IGNORECASE),
Rob Mohr092a4462020-06-16 10:23:54 -070054 'Reviewed-by:',
Rob Mohr4c6a5382021-02-25 08:50:44 -080055 'Roller-URL:',
Rob Mohr092a4462020-06-16 10:23:54 -070056 'Signed-off-by:',
57 'Testability-Review:',
58 'Tested-by:',
59]
60
61
Rob Mohr7cb53c42020-11-18 10:45:13 -080062def _match_tag(line, tag):
63 if hasattr(tag, 'match'):
64 return tag.match(line)
65 return line.startswith(tag)
66
67
Rob Mohr092a4462020-06-16 10:23:54 -070068def _sanitize_message(message):
69 """Sanitize lines of a commit message.
70
71 Prepend 'Original-' to lines which begin with ESCAPE_TAGS. Filter
72 out lines which begin with FILTER_TAGS.
73 """
Rob Mohr71bb6452022-05-03 09:17:09 -070074
75 lines = message.splitlines()
76
77 # If the first line is really long create a truncated version of it, but
78 # keep the original version of the commit message around.
79 if len(lines[0]) > 80:
80 lines = [lines[0][0:50], ''] + lines
81
Rob Mohr092a4462020-06-16 10:23:54 -070082 return '\n'.join(
83 "Original-" + line
84 if any((line.startswith(tag) for tag in ESCAPE_TAGS))
85 else line
Rob Mohr71bb6452022-05-03 09:17:09 -070086 for line in lines
Rob Mohr7cb53c42020-11-18 10:45:13 -080087 if not any((_match_tag(line, tag) for tag in FILTER_TAGS))
Rob Mohr092a4462020-06-16 10:23:54 -070088 )
89
90
Rob Mohrf7e30152020-11-18 09:56:30 -080091class _Direction(enum.Enum):
92 CURRENT = 'CURRENT'
93 FORWARD = 'FORWARD'
94 BACKWARD = 'BACKWARD'
95 REBASE = 'REBASE'
96
97
Rob Mohr43c669b2022-01-10 12:44:04 -080098# Using a namedtuple instead of attrs because this should be hashable.
99Account = collections.namedtuple('Account', 'name email')
100
101
Rob Mohr092a4462020-06-16 10:23:54 -0700102@attr.s
Rob Mohr82c0e352022-07-27 20:57:40 +0000103class Commit:
Rob Mohr36984592021-03-31 15:46:38 -0700104 hash = attr.ib(type=str)
105 message = attr.ib(type=str)
106 author = attr.ib(type=str)
107 owner = attr.ib(type=str)
108 reviewers = attr.ib(type=tuple)
Rob Mohr092a4462020-06-16 10:23:54 -0700109
110
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700111@attr.s
Rob Mohr82c0e352022-07-27 20:57:40 +0000112class Roll:
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700113 _api = attr.ib()
114 project_name = attr.ib(type=str)
115 old_revision = attr.ib(type=str)
116 new_revision = attr.ib(type=str)
117 proj_dir = attr.ib(type=str)
Rob Mohrf7e30152020-11-18 09:56:30 -0800118 direction = attr.ib(type=str)
Rob Mohr36984592021-03-31 15:46:38 -0700119 commits = attr.ib(type=tuple, default=None)
120 remote = attr.ib(type=str, default=None)
Rob Mohr18549cf2021-07-27 16:15:19 -0700121 _nest_steps = attr.ib(type=bool, default=True)
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700122
Rob Mohrf7e30152020-11-18 09:56:30 -0800123 @direction.validator
124 def check(self, _, value): # pragma: no cover
125 if value not in _Direction:
Rob Mohr4806b6b2023-02-03 18:03:32 +0000126 raise ValueError(f'invalid direction: {value}')
Rob Mohrf7e30152020-11-18 09:56:30 -0800127 if value == _Direction.CURRENT:
128 raise ValueError('attempt to do a no-op roll')
129
Rob Mohr36984592021-03-31 15:46:38 -0700130 def __attrs_post_init__(self):
131 self._set_remote()
Rob Mohr18549cf2021-07-27 16:15:19 -0700132 with self._api.context(cwd=self.proj_dir):
133 if self._nest_steps:
134 with self._api.step.nest(self.project_name):
135 self._set_commits()
136 else:
137 self._set_commits() # pragma: no cover
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700138
Rob Mohr36984592021-03-31 15:46:38 -0700139 def _set_commits(self):
Rob Mohr356720d2021-07-01 13:31:29 -0700140
Rob Mohr18549cf2021-07-27 16:15:19 -0700141 log_cmd = [
142 'log',
Rob Mohr43c669b2022-01-10 12:44:04 -0800143 '--pretty=format:%H\n%an\n%ae\n%B',
Rob Mohr18549cf2021-07-27 16:15:19 -0700144 # Separate entries with null bytes since most entries
145 # will contain newlines ("%B" is the full commit
146 # message, not just the first line.)
147 '-z',
148 ]
Rob Mohr356720d2021-07-01 13:31:29 -0700149
Rob Mohr18549cf2021-07-27 16:15:19 -0700150 if _is_hash(self.old_revision) and self.direction == _Direction.FORWARD:
Rob Mohr4806b6b2023-02-03 18:03:32 +0000151 log_cmd.append(f'{self.old_revision}..{self.new_revision}')
Rob Mohr18549cf2021-07-27 16:15:19 -0700152 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
Rob Mohr4806b6b2023-02-03 18:03:32 +0000170 full_host = f'{self.gerrit_name}-review.googlesource.com'
Rob Mohr18549cf2021-07-27 16:15:19 -0700171
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 Mohr4806b6b2023-02-03 18:03:32 +0000180 f'commit:{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(
Rob Mohr4806b6b2023-02-03 18:03:32 +0000190 f'get {number}',
Rob Mohr18549cf2021-07-27 16:15:19 -0700191 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):
Rob Mohr918d0592022-09-21 15:18:35 +0000246 # There may be multiple remote names. Only get the first one. They
247 # should refer to the same URL so it doesn't matter which we use.
248 name = (
249 api.git(
250 'name',
251 'remote',
252 stdout=api.raw_io.output_text(),
253 step_test_data=lambda: api.raw_io.test_api.stream_output_text(
254 'origin'
255 ),
256 )
257 .stdout.strip()
258 .split('\n')[0]
259 )
Rob Mohrcbee3fc2020-11-30 14:07:42 -0800260
261 remote = api.git(
262 'url',
263 'remote',
264 'get-url',
265 name,
Rob Mohr74396252021-08-27 13:17:50 -0700266 stdout=api.raw_io.output_text(),
267 step_test_data=lambda: api.raw_io.test_api.stream_output_text(
Rob Mohrcbee3fc2020-11-30 14:07:42 -0800268 'sso://pigweed/pigweed/pigweed'
269 ),
270 ).stdout.strip()
271
Rob Mohr36984592021-03-31 15:46:38 -0700272 self.remote = api.sso.sso_to_https(remote)
Rob Mohrcbee3fc2020-11-30 14:07:42 -0800273
274 @property
275 def gerrit_name(self):
Rob Mohr4c504cc2021-08-03 07:37:06 -0700276 return urllib.parse.urlparse(self.remote).netloc.split('.')[0]
Rob Mohrcbee3fc2020-11-30 14:07:42 -0800277
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700278
279@attr.s
Rob Mohr82c0e352022-07-27 20:57:40 +0000280class Message:
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700281 name = attr.ib(type=str)
282 template = attr.ib(type=str)
283 kwargs = attr.ib(type=dict)
284 num_commits = attr.ib(type=int)
285 footer = attr.ib(type=tuple, default=())
286
287 def render(self, with_footer=True):
288 result = [self.template.format(**self.kwargs)]
289 if with_footer:
290 result.extend(x for x in self.footer)
291 return '\n'.join(result)
292
293
Rob Mohr092a4462020-06-16 10:23:54 -0700294def _is_hash(value):
Rob Mohr57204602020-09-23 08:41:18 -0700295 return re.match(r'^[0-9a-fA-F]{40}', value)
Rob Mohr092a4462020-06-16 10:23:54 -0700296
297
Rob Mohr08973532021-09-01 08:34:11 -0700298def _pprint_dict(d):
299 result = []
300 for k, v in sorted(d.items()):
Rob Mohr4806b6b2023-02-03 18:03:32 +0000301 result.append(f'{k!r}: {v!r}\n')
Rob Mohr08973532021-09-01 08:34:11 -0700302 return ''.join(result)
303
304
Rob Mohrd39ea9e2020-09-22 15:01:07 -0700305class RollUtilApi(recipe_api.RecipeApi):
Rob Mohr1eb8f912020-10-07 13:17:17 -0700306 def __init__(self, props, *args, **kwargs):
Rob Mohr78edbd02022-03-23 16:08:16 -0700307 super().__init__(*args, **kwargs)
Rob Mohra7154242021-09-01 08:30:53 -0700308 self.labels_to_set = collections.OrderedDict()
Rob Mohr0bb03b62021-10-05 10:11:17 -0700309 for label in sorted(props.labels_to_set, key=lambda x: x.label):
Rob Mohr0ad390b2021-09-01 08:32:37 -0700310 self.labels_to_set[str(label.label)] = label.value
311 self.labels_to_wait_on = sorted(str(x) for x in props.labels_to_wait_on)
Rob Mohr1c1ef302021-03-08 12:23:14 -0800312 self.footer = []
Rob Mohr3ed6c792022-01-13 07:52:26 -0800313 self._commit_divider = props.commit_divider
Rob Mohr1eb8f912020-10-07 13:17:17 -0700314
Rob Mohr36984592021-03-31 15:46:38 -0700315 def authors(self, *roll):
316 authors = set()
317 for r in roll:
318 for commit in r.commits:
319 if commit.author:
320 authors.add(commit.author)
321 if commit.owner:
322 authors.add(commit.owner)
323 return authors
324
Rob Mohr43c669b2022-01-10 12:44:04 -0800325 def fake_author(self, author):
326 # Update the author's email address so it can be used for attribution
327 # without literally attributing it to the author's account in Gerrit.
Rob Mohr9f78be72022-06-21 09:57:16 -0700328 # Make sure not to add it twice, and there's no need to do this for
329 # service accounts.
Rob Mohre28a5a92022-01-10 15:16:37 -0800330 email = author.email
331 prefix = 'pigweed.infra.roller.'
Rob Mohr9f78be72022-06-21 09:57:16 -0700332 if prefix not in email and not email.endswith('gserviceaccount.com'):
Rob Mohre28a5a92022-01-10 15:16:37 -0800333 user, domain = author.email.split('@')
Rob Mohr4806b6b2023-02-03 18:03:32 +0000334 email = f'{user}@{prefix}{domain}'
Rob Mohre28a5a92022-01-10 15:16:37 -0800335
336 return Account(author.name, email,)
Rob Mohr43c669b2022-01-10 12:44:04 -0800337
Rob Mohr36984592021-03-31 15:46:38 -0700338 def reviewers(self, *roll):
339 reviewers = set()
340 for r in roll:
341 for commit in r.commits:
342 reviewers.update(commit.reviewers)
343 return reviewers
344
Rob Mohrb04227c2021-04-01 12:38:33 -0700345 def can_cc_on_roll(self, email, host):
Rob Mohr36984592021-03-31 15:46:38 -0700346 # Assume all queried accounts exist on Gerrit in testing except for
347 # nobody@google.com.
348 test_data = self.m.json.test_api.output([{'_account_id': 123}])
349 if email == 'nobody@google.com':
350 test_data = self.m.json.test_api.output([])
351
352 return self.m.gerrit.account_query(
Rob Mohr4806b6b2023-02-03 18:03:32 +0000353 email, f'email:{email}', host=host, test_data=test_data,
Rob Mohr36984592021-03-31 15:46:38 -0700354 ).json.output
355
Rob Mohr43c669b2022-01-10 12:44:04 -0800356 def include_cc(self, account, cc_domains, host):
Rob Mohr4806b6b2023-02-03 18:03:32 +0000357 with self.m.step.nest(f'cc {account.email}') as pres:
Rob Mohr43c669b2022-01-10 12:44:04 -0800358 domain = account.email.split('@', 1)[1]
Rob Mohrb04227c2021-04-01 12:38:33 -0700359 if domain.endswith('gserviceaccount.com'):
360 pres.step_summary_text = 'not CCing, robot account'
361 return False
362 if cc_domains and domain not in cc_domains:
363 pres.step_summary_text = 'not CCing, domain excluded'
364 return False
Rob Mohr43c669b2022-01-10 12:44:04 -0800365 if not self.can_cc_on_roll(account.email, host=host):
Rob Mohrb04227c2021-04-01 12:38:33 -0700366 pres.step_summary_text = 'not CCing, no account in Gerrit'
367 return False
368
369 pres.step_summary_text = 'CCing'
370 return True
371
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700372 def _single_commit_roll_message(self, roll):
Rob Mohr57204602020-09-23 08:41:18 -0700373 template = """
Rob Mohr092a4462020-06-16 10:23:54 -0700374[roll {project_name}] {sanitized_message}
375
Rob Mohrfc794bc2021-08-06 11:20:33 -0700376{remote}
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700377{project_name} Rolled-Commits: {old_revision:.15}..{new_revision:.15}
378 """.strip()
379
380 commit = roll.commits[0]
Rob Mohr092a4462020-06-16 10:23:54 -0700381
Rob Mohr57204602020-09-23 08:41:18 -0700382 kwargs = {
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700383 'project_name': roll.project_name,
Rob Mohrfc794bc2021-08-06 11:20:33 -0700384 'remote': roll.remote,
Rob Mohr57204602020-09-23 08:41:18 -0700385 'original_message': commit.message,
386 'sanitized_message': _sanitize_message(commit.message),
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700387 'old_revision': roll.old_revision,
388 'new_revision': roll.new_revision,
Rob Mohr57204602020-09-23 08:41:18 -0700389 }
Rob Mohr092a4462020-06-16 10:23:54 -0700390
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700391 message = Message(
392 name=roll.project_name,
393 template=template,
394 kwargs=kwargs,
395 num_commits=1,
396 footer=tuple(self.footer),
397 )
Rob Mohr092a4462020-06-16 10:23:54 -0700398
Rob Mohr4806b6b2023-02-03 18:03:32 +0000399 with self.m.step.nest(f'message for {roll.project_name}') as pres:
Rob Mohr57204602020-09-23 08:41:18 -0700400 pres.logs['template'] = template
Rob Mohr08973532021-09-01 08:34:11 -0700401 pres.logs['kwargs'] = _pprint_dict(kwargs)
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700402 pres.logs['message'] = message.render()
Rob Mohr092a4462020-06-16 10:23:54 -0700403
Rob Mohr57204602020-09-23 08:41:18 -0700404 return message
Rob Mohr092a4462020-06-16 10:23:54 -0700405
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700406 def _multiple_commits_roll_message(self, roll):
Rob Mohr57204602020-09-23 08:41:18 -0700407 template = """
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700408[{project_name}] Roll {num_commits} commits
Rob Mohr092a4462020-06-16 10:23:54 -0700409
410{one_liners}
411
Rob Mohrfc794bc2021-08-06 11:20:33 -0700412{remote}
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700413{project_name} Rolled-Commits: {old_revision:.15}..{new_revision:.15}
Rob Mohr092a4462020-06-16 10:23:54 -0700414 """.strip()
415
Rob Mohr516bd882022-11-21 19:36:24 +0000416 one_liners = []
417 for commit in roll.commits:
418 # Handle case where the commit message is empty. Example:
419 # https://github.com/google/googletest/commit/148ab827cacc7a879832f40313bda87a65b1e8a3
420 first_line = '(empty commit message)'
421 if commit.message:
422 first_line = commit.message.splitlines()[0]
423 one_liners.append(f'{commit.hash:.15} {first_line[0:50]}')
Rob Mohr092a4462020-06-16 10:23:54 -0700424
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700425 num_commits = len(roll.commits)
Rob Mohr10b99202022-06-07 12:12:27 -0700426
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700427 if not _is_hash(roll.old_revision):
428 num_commits = 'multiple'
Rob Mohr57204602020-09-23 08:41:18 -0700429 one_liners.append('...')
Rob Mohr092a4462020-06-16 10:23:54 -0700430
Rob Mohr10b99202022-06-07 12:12:27 -0700431 if len(one_liners) > 100:
432 one_liners = one_liners[0:5] + ['...'] + one_liners[-5:]
433 # In case both this and the previous condition match.
434 if one_liners[-1] == '...':
435 one_liners.pop() # pragma: no cover
436
Rob Mohr57204602020-09-23 08:41:18 -0700437 kwargs = {
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700438 'project_name': roll.project_name,
Rob Mohrfc794bc2021-08-06 11:20:33 -0700439 'remote': roll.remote,
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700440 'num_commits': num_commits,
Rob Mohr57204602020-09-23 08:41:18 -0700441 'one_liners': '\n'.join(one_liners),
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700442 'old_revision': roll.old_revision,
443 'new_revision': roll.new_revision,
Rob Mohr57204602020-09-23 08:41:18 -0700444 }
Rob Mohr092a4462020-06-16 10:23:54 -0700445
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700446 message = Message(
447 name=roll.project_name,
448 template=template,
449 kwargs=kwargs,
450 num_commits=num_commits,
451 footer=tuple(self.footer),
452 )
Rob Mohr092a4462020-06-16 10:23:54 -0700453
Rob Mohr57204602020-09-23 08:41:18 -0700454 with self.m.step.nest('message') as pres:
455 pres.logs['template'] = template
Rob Mohr08973532021-09-01 08:34:11 -0700456 pres.logs['kwargs'] = _pprint_dict(kwargs)
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700457 pres.logs['message'] = message.render()
Rob Mohr092a4462020-06-16 10:23:54 -0700458
Rob Mohr57204602020-09-23 08:41:18 -0700459 return message
Rob Mohr092a4462020-06-16 10:23:54 -0700460
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700461 def _single_roll_message(self, roll):
462 if len(roll.commits) > 1:
463 return self._multiple_commits_roll_message(roll)
464 return self._single_commit_roll_message(roll)
Rob Mohr092a4462020-06-16 10:23:54 -0700465
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700466 def _multiple_rolls_message(self, *rolls):
467 rolls = sorted(rolls, key=lambda x: x.project_name)
468
469 messages = []
470 for roll in rolls:
471 messages.append(self._single_roll_message(roll))
472
473 texts = [
474 '[roll {}] Roll {} commits'.format(
475 ', '.join(x.name for x in messages),
476 sum(x.num_commits for x in messages),
477 )
478 ]
479 texts.extend(x.render(with_footer=False) for x in messages)
Rob Mohr4806b6b2023-02-03 18:03:32 +0000480 texts.append('\n'.join(f'{x}' for x in self.footer))
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700481
482 return '\n\n'.join(texts)
483
Rob Mohr43c669b2022-01-10 12:44:04 -0800484 def Account(self, name, email):
485 return Account(name, email)
486
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700487 def Roll(self, **kwargs):
488 """Create a Roll. See Roll class above for details."""
489 return Roll(api=self.m, **kwargs)
490
491 def message(self, *rolls):
Rob Mohr57204602020-09-23 08:41:18 -0700492 with self.m.step.nest('roll message'):
Rob Mohrf6ee7cb2020-10-26 14:05:06 -0700493 if len(rolls) > 1:
Rob Mohr3ed6c792022-01-13 07:52:26 -0800494 result = self._multiple_rolls_message(*rolls)
495 else:
496 result = self._single_roll_message(*rolls).render()
497 if self._commit_divider:
Rob Mohr4806b6b2023-02-03 18:03:32 +0000498 result += f'\n{self._commit_divider}'
Rob Mohr3ed6c792022-01-13 07:52:26 -0800499 return result
Rob Mohrd39ea9e2020-09-22 15:01:07 -0700500
Rob Mohrf7e30152020-11-18 09:56:30 -0800501 Direction = _Direction
502
Rob Mohr476a7042020-11-30 13:33:52 -0800503 def get_roll_direction(self, git_dir, old, new, name='get roll direction'):
Rob Mohrf7e30152020-11-18 09:56:30 -0800504 """Return Direction of roll."""
Rob Mohr57204602020-09-23 08:41:18 -0700505 if old == new:
506 with self.m.step.nest(name) as pres:
507 pres.step_summary_text = 'up-to-date'
Rob Mohrf7e30152020-11-18 09:56:30 -0800508 return self.Direction.CURRENT
Rob Mohrd39ea9e2020-09-22 15:01:07 -0700509
Rob Mohr57204602020-09-23 08:41:18 -0700510 with self.m.context(git_dir):
Rob Mohrf7e30152020-11-18 09:56:30 -0800511 with self.m.step.nest(name) as pres:
512 forward = self.m.git(
513 'is forward',
514 'merge-base',
515 '--is-ancestor',
516 old,
517 new,
518 ok_ret=(0, 1),
519 )
Rob Mohrd39ea9e2020-09-22 15:01:07 -0700520
Rob Mohrf7e30152020-11-18 09:56:30 -0800521 backward = self.m.git(
522 'is backward',
523 'merge-base',
524 '--is-ancestor',
525 new,
526 old,
527 ok_ret=(0, 1),
528 )
Rob Mohrd39ea9e2020-09-22 15:01:07 -0700529
Rob Mohrf7e30152020-11-18 09:56:30 -0800530 if (
531 forward.exc_result.retcode == 0
532 and backward.exc_result.retcode != 0
533 ):
534 pres.step_summary_text = 'forward'
535 return self.Direction.FORWARD
Rob Mohrd39ea9e2020-09-22 15:01:07 -0700536
Rob Mohrf7e30152020-11-18 09:56:30 -0800537 if (
538 forward.exc_result.retcode != 0
539 and backward.exc_result.retcode == 0
540 ):
541 pres.step_summary_text = 'backward'
542 return self.Direction.BACKWARD
543
Rob Mohrcbee3fc2020-11-30 14:07:42 -0800544 # If new and old are ancestors of each other then this is the
545 # same commit. We should only hit this during testing because
546 # the comparison at the top of the function should have caught
547 # this situation.
548 if (
549 forward.exc_result.retcode == 0
550 and backward.exc_result.retcode == 0
551 ):
552 with self.m.step.nest(name) as pres:
553 pres.step_summary_text = 'up-to-date'
554 return self.Direction.CURRENT
555
Rob Mohrf7e30152020-11-18 09:56:30 -0800556 # If old is not an ancestor of new and new is not an ancestor
557 # of old then history was rewritten in some manner but we still
558 # need to update the pin.
559 pres.step_summary_text = 'rebase'
560 return self.Direction.REBASE
561
562 def can_roll(self, direction):
563 return direction in (self.Direction.FORWARD, self.Direction.REBASE)
564
565 def skip_roll_step(self, remote, old_revision, new_revision):
Rob Mohr57204602020-09-23 08:41:18 -0700566 with self.m.step.nest('cancelling roll') as pres:
567 fmt = (
568 'not updating from {old} to {new} because {old} is newer '
569 'than {new}'
570 )
571 if old_revision == new_revision:
Rob Mohrad16bc12020-09-28 06:39:55 -0700572 fmt = (
573 'not updating from {old} to {new} because they are '
574 'identical'
575 )
Rob Mohr57204602020-09-23 08:41:18 -0700576 pres.step_summary_text = fmt.format(
577 old=old_revision[0:7], new=new_revision[0:7]
578 )
Rob Mohr4806b6b2023-02-03 18:03:32 +0000579 pres.links[old_revision] = f'{remote}/+/{old_revision}'
580 pres.links[new_revision] = f'{remote}/+/{new_revision}'
Rob Mohr18549cf2021-07-27 16:15:19 -0700581
582 def normalize_remote(self, remote, base):
583 """Convert relative paths to absolute paths.
584
585 Support relative paths. If the top-level project is
586 "https://pigweed.googlesource.com/ex/ample" then a submodule path of
587 "./abc" maps to "https://pigweed.googlesource.com/ex/ample/abc" and
588 "../abc" maps to "https://pigweed.googlesource.com/ex/abc". Minimal
589 error-checking because git does most of these checks for us.
590
591 Also converts sso to https.
592
593 Args:
594 remote (str): Submodule remote URL.
595 base (str): Fully-qualified superproject remote URL.
596 """
597 if remote.startswith('.'):
598 remote = '/'.join((base.rstrip('/'), remote.lstrip('/')))
599
600 changes = 1
601 while changes:
602 changes = 0
603
Rob Mohr98ca1522021-11-03 14:26:12 -0700604 remote, n = re.subn(r'/\./', '/', remote)
Rob Mohr18549cf2021-07-27 16:15:19 -0700605 changes += n
606
Rob Mohr98ca1522021-11-03 14:26:12 -0700607 remote, n = re.subn(r'/[^/]+/\.\./', '/', remote)
Rob Mohr18549cf2021-07-27 16:15:19 -0700608 changes += n
609
610 return self.m.sso.sso_to_https(remote)