Rob Mohr | 092a446 | 2020-06-16 10:23:54 -0700 | [diff] [blame] | 1 | # 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 Mohr | d39ea9e | 2020-09-22 15:01:07 -0700 | [diff] [blame] | 14 | """Utility functions for rollers.""" |
Rob Mohr | 092a446 | 2020-06-16 10:23:54 -0700 | [diff] [blame] | 15 | |
Rob Mohr | a715424 | 2021-09-01 08:30:53 -0700 | [diff] [blame] | 16 | import collections |
Rob Mohr | 092a446 | 2020-06-16 10:23:54 -0700 | [diff] [blame] | 17 | import re |
Rob Mohr | 46c2503 | 2022-07-26 17:01:35 +0000 | [diff] [blame] | 18 | import urllib |
Rob Mohr | 092a446 | 2020-06-16 10:23:54 -0700 | [diff] [blame] | 19 | |
| 20 | import attr |
Rob Mohr | f7e3015 | 2020-11-18 09:56:30 -0800 | [diff] [blame] | 21 | import enum |
Rob Mohr | 092a446 | 2020-06-16 10:23:54 -0700 | [diff] [blame] | 22 | from 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. |
| 26 | ESCAPE_TAGS = [ |
Rob Mohr | 90c712c | 2022-09-06 15:46:17 +0000 | [diff] [blame] | 27 | 'Bug:', |
| 28 | 'Fixed:', |
| 29 | 'Fixes:', |
Rob Mohr | f6ee7cb | 2020-10-26 14:05:06 -0700 | [diff] [blame] | 30 | 'Requires:', |
Rob Mohr | 092a446 | 2020-06-16 10:23:54 -0700 | [diff] [blame] | 31 | 'Reviewed-on:', |
| 32 | ] |
| 33 | |
| 34 | # If we're embedding the original commit message, remove lines which contain |
| 35 | # these tags. |
| 36 | FILTER_TAGS = [ |
| 37 | 'API-Review:', |
| 38 | 'Acked-by:', |
Rob Mohr | ea2c3d0 | 2022-08-30 16:53:53 +0000 | [diff] [blame] | 39 | re.compile(r'^\w+-?Auto-Submit:', re.IGNORECASE), |
Rob Mohr | 20b3536 | 2022-10-10 18:54:39 +0000 | [diff] [blame] | 40 | 'Build-Errors:', |
Rob Mohr | 092a446 | 2020-06-16 10:23:54 -0700 | [diff] [blame] | 41 | 'CC:', |
| 42 | 'CQ-Do-Not-Cancel-Tryjobs:', |
Rob Mohr | 6f9e588 | 2021-06-21 12:50:36 -0700 | [diff] [blame] | 43 | 'Cq-Include-Trybots:', |
Rob Mohr | 092a446 | 2020-06-16 10:23:54 -0700 | [diff] [blame] | 44 | 'Change-Id:', |
| 45 | 'Commit-Queue:', |
Rob Mohr | 4c6a538 | 2021-02-25 08:50:44 -0800 | [diff] [blame] | 46 | 'Cq-Cl-Tag:', |
Rob Mohr | 4a50c2c | 2022-08-30 20:16:26 +0000 | [diff] [blame] | 47 | re.compile(r'Git[ -]?watcher:', re.IGNORECASE), |
Rob Mohr | 8d19b38 | 2021-06-11 18:57:01 -0700 | [diff] [blame] | 48 | 'No-Docs-Update-Reason:', |
Rob Mohr | 6f9e588 | 2021-06-21 12:50:36 -0700 | [diff] [blame] | 49 | 'No-Presubmit:', |
| 50 | 'No-Tree-Checks: true', |
| 51 | 'No-Try: true', |
Rob Mohr | 3d02b1d | 2023-04-25 17:31:01 +0000 | [diff] [blame] | 52 | 'Presubmit-Verified:', |
Rob Mohr | cf65684 | 2022-11-29 17:52:08 +0000 | [diff] [blame] | 53 | re.compile(r'^\w+-?Readability-Trivial:', re.IGNORECASE), |
Rob Mohr | 092a446 | 2020-06-16 10:23:54 -0700 | [diff] [blame] | 54 | 'Reviewed-by:', |
Rob Mohr | 4c6a538 | 2021-02-25 08:50:44 -0800 | [diff] [blame] | 55 | 'Roller-URL:', |
Rob Mohr | 092a446 | 2020-06-16 10:23:54 -0700 | [diff] [blame] | 56 | 'Signed-off-by:', |
| 57 | 'Testability-Review:', |
| 58 | 'Tested-by:', |
| 59 | ] |
| 60 | |
| 61 | |
Rob Mohr | 7cb53c4 | 2020-11-18 10:45:13 -0800 | [diff] [blame] | 62 | def _match_tag(line, tag): |
| 63 | if hasattr(tag, 'match'): |
| 64 | return tag.match(line) |
| 65 | return line.startswith(tag) |
| 66 | |
| 67 | |
Rob Mohr | 092a446 | 2020-06-16 10:23:54 -0700 | [diff] [blame] | 68 | def _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 Mohr | 71bb645 | 2022-05-03 09:17:09 -0700 | [diff] [blame] | 74 | |
| 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 Mohr | 092a446 | 2020-06-16 10:23:54 -0700 | [diff] [blame] | 82 | return '\n'.join( |
| 83 | "Original-" + line |
| 84 | if any((line.startswith(tag) for tag in ESCAPE_TAGS)) |
| 85 | else line |
Rob Mohr | 71bb645 | 2022-05-03 09:17:09 -0700 | [diff] [blame] | 86 | for line in lines |
Rob Mohr | 7cb53c4 | 2020-11-18 10:45:13 -0800 | [diff] [blame] | 87 | if not any((_match_tag(line, tag) for tag in FILTER_TAGS)) |
Rob Mohr | 092a446 | 2020-06-16 10:23:54 -0700 | [diff] [blame] | 88 | ) |
| 89 | |
| 90 | |
Rob Mohr | f7e3015 | 2020-11-18 09:56:30 -0800 | [diff] [blame] | 91 | class _Direction(enum.Enum): |
| 92 | CURRENT = 'CURRENT' |
| 93 | FORWARD = 'FORWARD' |
| 94 | BACKWARD = 'BACKWARD' |
| 95 | REBASE = 'REBASE' |
| 96 | |
| 97 | |
Rob Mohr | 43c669b | 2022-01-10 12:44:04 -0800 | [diff] [blame] | 98 | # Using a namedtuple instead of attrs because this should be hashable. |
| 99 | Account = collections.namedtuple('Account', 'name email') |
| 100 | |
| 101 | |
Rob Mohr | 092a446 | 2020-06-16 10:23:54 -0700 | [diff] [blame] | 102 | @attr.s |
Rob Mohr | 82c0e35 | 2022-07-27 20:57:40 +0000 | [diff] [blame] | 103 | class Commit: |
Rob Mohr | 3698459 | 2021-03-31 15:46:38 -0700 | [diff] [blame] | 104 | 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 Mohr | 092a446 | 2020-06-16 10:23:54 -0700 | [diff] [blame] | 109 | |
| 110 | |
Rob Mohr | f6ee7cb | 2020-10-26 14:05:06 -0700 | [diff] [blame] | 111 | @attr.s |
Rob Mohr | 82c0e35 | 2022-07-27 20:57:40 +0000 | [diff] [blame] | 112 | class Roll: |
Rob Mohr | f6ee7cb | 2020-10-26 14:05:06 -0700 | [diff] [blame] | 113 | _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 Mohr | f7e3015 | 2020-11-18 09:56:30 -0800 | [diff] [blame] | 118 | direction = attr.ib(type=str) |
Rob Mohr | 3698459 | 2021-03-31 15:46:38 -0700 | [diff] [blame] | 119 | commits = attr.ib(type=tuple, default=None) |
| 120 | remote = attr.ib(type=str, default=None) |
Rob Mohr | 18549cf | 2021-07-27 16:15:19 -0700 | [diff] [blame] | 121 | _nest_steps = attr.ib(type=bool, default=True) |
Rob Mohr | f6ee7cb | 2020-10-26 14:05:06 -0700 | [diff] [blame] | 122 | |
Rob Mohr | f7e3015 | 2020-11-18 09:56:30 -0800 | [diff] [blame] | 123 | @direction.validator |
| 124 | def check(self, _, value): # pragma: no cover |
| 125 | if value not in _Direction: |
Rob Mohr | 4806b6b | 2023-02-03 18:03:32 +0000 | [diff] [blame] | 126 | raise ValueError(f'invalid direction: {value}') |
Rob Mohr | f7e3015 | 2020-11-18 09:56:30 -0800 | [diff] [blame] | 127 | if value == _Direction.CURRENT: |
| 128 | raise ValueError('attempt to do a no-op roll') |
| 129 | |
Rob Mohr | 3698459 | 2021-03-31 15:46:38 -0700 | [diff] [blame] | 130 | def __attrs_post_init__(self): |
| 131 | self._set_remote() |
Rob Mohr | 18549cf | 2021-07-27 16:15:19 -0700 | [diff] [blame] | 132 | 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 Mohr | f6ee7cb | 2020-10-26 14:05:06 -0700 | [diff] [blame] | 138 | |
Rob Mohr | 3698459 | 2021-03-31 15:46:38 -0700 | [diff] [blame] | 139 | def _set_commits(self): |
Rob Mohr | 356720d | 2021-07-01 13:31:29 -0700 | [diff] [blame] | 140 | |
Rob Mohr | 18549cf | 2021-07-27 16:15:19 -0700 | [diff] [blame] | 141 | log_cmd = [ |
| 142 | 'log', |
Rob Mohr | 43c669b | 2022-01-10 12:44:04 -0800 | [diff] [blame] | 143 | '--pretty=format:%H\n%an\n%ae\n%B', |
Rob Mohr | 18549cf | 2021-07-27 16:15:19 -0700 | [diff] [blame] | 144 | # 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 Mohr | 356720d | 2021-07-01 13:31:29 -0700 | [diff] [blame] | 149 | |
Rob Mohr | 18549cf | 2021-07-27 16:15:19 -0700 | [diff] [blame] | 150 | if _is_hash(self.old_revision) and self.direction == _Direction.FORWARD: |
Rob Mohr | 4806b6b | 2023-02-03 18:03:32 +0000 | [diff] [blame] | 151 | log_cmd.append(f'{self.old_revision}..{self.new_revision}') |
Rob Mohr | 18549cf | 2021-07-27 16:15:19 -0700 | [diff] [blame] | 152 | else: |
| 153 | log_cmd.extend(('--max-count', '5', self.new_revision)) |
Rob Mohr | 356720d | 2021-07-01 13:31:29 -0700 | [diff] [blame] | 154 | |
Rob Mohr | 7439625 | 2021-08-27 13:17:50 -0700 | [diff] [blame] | 155 | log_kwargs = {'stdout': self._api.raw_io.output_text()} |
Rob Mohr | 356720d | 2021-07-01 13:31:29 -0700 | [diff] [blame] | 156 | |
Rob Mohr | 18549cf | 2021-07-27 16:15:19 -0700 | [diff] [blame] | 157 | 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 Mohr | 43c669b | 2022-01-10 12:44:04 -0800 | [diff] [blame] | 165 | commit_hash, name, email, message = commit.split('\n', 3) |
| 166 | author = Account(name, email) |
Rob Mohr | 18549cf | 2021-07-27 16:15:19 -0700 | [diff] [blame] | 167 | owner = None |
| 168 | reviewers = [] |
| 169 | |
Rob Mohr | 4806b6b | 2023-02-03 18:03:32 +0000 | [diff] [blame] | 170 | full_host = f'{self.gerrit_name}-review.googlesource.com' |
Rob Mohr | 18549cf | 2021-07-27 16:15:19 -0700 | [diff] [blame] | 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 Mohr | 4806b6b | 2023-02-03 18:03:32 +0000 | [diff] [blame] | 180 | f'commit:{commit_hash}', |
Rob Mohr | 18549cf | 2021-07-27 16:15:19 -0700 | [diff] [blame] | 181 | 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 Mohr | 4806b6b | 2023-02-03 18:03:32 +0000 | [diff] [blame] | 190 | f'get {number}', |
Rob Mohr | 18549cf | 2021-07-27 16:15:19 -0700 | [diff] [blame] | 191 | number, |
| 192 | host=full_host, |
| 193 | test_data=self._api.json.test_api.output( |
| 194 | { |
Rob Mohr | 43c669b | 2022-01-10 12:44:04 -0800 | [diff] [blame] | 195 | 'owner': { |
| 196 | 'name': 'author', |
| 197 | 'email': 'author@example.com', |
| 198 | }, |
Rob Mohr | 18549cf | 2021-07-27 16:15:19 -0700 | [diff] [blame] | 199 | 'reviewers': { |
| 200 | 'REVIEWER': [ |
Rob Mohr | 43c669b | 2022-01-10 12:44:04 -0800 | [diff] [blame] | 201 | { |
| 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 Mohr | 18549cf | 2021-07-27 16:15:19 -0700 | [diff] [blame] | 213 | ], |
| 214 | }, |
| 215 | } |
| 216 | ), |
| 217 | ok_ret='any', |
Rob Mohr | 0eac781 | 2021-07-12 08:15:00 -0700 | [diff] [blame] | 218 | ) |
| 219 | |
Rob Mohr | 18549cf | 2021-07-27 16:15:19 -0700 | [diff] [blame] | 220 | if step.exc_result.retcode == 0: |
| 221 | details = step.json.output |
Rob Mohr | 43c669b | 2022-01-10 12:44:04 -0800 | [diff] [blame] | 222 | owner = Account( |
| 223 | details['owner']['name'], details['owner']['email'] |
| 224 | ) |
Rob Mohr | 18549cf | 2021-07-27 16:15:19 -0700 | [diff] [blame] | 225 | for reviewer in details['reviewers']['REVIEWER']: |
Rob Mohr | 43c669b | 2022-01-10 12:44:04 -0800 | [diff] [blame] | 226 | reviewers.append( |
| 227 | Account(reviewer['name'], reviewer['email']), |
| 228 | ) |
Rob Mohr | c7c93b8 | 2021-04-23 09:10:39 -0700 | [diff] [blame] | 229 | |
Rob Mohr | 18549cf | 2021-07-27 16:15:19 -0700 | [diff] [blame] | 230 | commits.append( |
| 231 | Commit( |
Rob Mohr | 3445395 | 2021-08-04 06:46:54 -0700 | [diff] [blame] | 232 | hash=commit_hash, |
Rob Mohr | 18549cf | 2021-07-27 16:15:19 -0700 | [diff] [blame] | 233 | author=author, |
| 234 | owner=owner, |
| 235 | reviewers=tuple(reviewers), |
| 236 | message=message, |
| 237 | ) |
| 238 | ) |
Rob Mohr | c7c93b8 | 2021-04-23 09:10:39 -0700 | [diff] [blame] | 239 | |
Rob Mohr | 18549cf | 2021-07-27 16:15:19 -0700 | [diff] [blame] | 240 | self.commits = tuple(commits) |
Rob Mohr | cbee3fc | 2020-11-30 14:07:42 -0800 | [diff] [blame] | 241 | |
Rob Mohr | 3698459 | 2021-03-31 15:46:38 -0700 | [diff] [blame] | 242 | def _set_remote(self): |
Rob Mohr | cbee3fc | 2020-11-30 14:07:42 -0800 | [diff] [blame] | 243 | api = self._api |
| 244 | |
| 245 | with api.step.nest('remote'), api.context(cwd=self.proj_dir): |
Rob Mohr | 918d059 | 2022-09-21 15:18:35 +0000 | [diff] [blame] | 246 | # 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 Mohr | cbee3fc | 2020-11-30 14:07:42 -0800 | [diff] [blame] | 260 | |
| 261 | remote = api.git( |
| 262 | 'url', |
| 263 | 'remote', |
| 264 | 'get-url', |
| 265 | name, |
Rob Mohr | 7439625 | 2021-08-27 13:17:50 -0700 | [diff] [blame] | 266 | stdout=api.raw_io.output_text(), |
| 267 | step_test_data=lambda: api.raw_io.test_api.stream_output_text( |
Rob Mohr | cbee3fc | 2020-11-30 14:07:42 -0800 | [diff] [blame] | 268 | 'sso://pigweed/pigweed/pigweed' |
| 269 | ), |
| 270 | ).stdout.strip() |
| 271 | |
Rob Mohr | 3698459 | 2021-03-31 15:46:38 -0700 | [diff] [blame] | 272 | self.remote = api.sso.sso_to_https(remote) |
Rob Mohr | cbee3fc | 2020-11-30 14:07:42 -0800 | [diff] [blame] | 273 | |
| 274 | @property |
| 275 | def gerrit_name(self): |
Rob Mohr | 4c504cc | 2021-08-03 07:37:06 -0700 | [diff] [blame] | 276 | return urllib.parse.urlparse(self.remote).netloc.split('.')[0] |
Rob Mohr | cbee3fc | 2020-11-30 14:07:42 -0800 | [diff] [blame] | 277 | |
Rob Mohr | f6ee7cb | 2020-10-26 14:05:06 -0700 | [diff] [blame] | 278 | |
| 279 | @attr.s |
Rob Mohr | 82c0e35 | 2022-07-27 20:57:40 +0000 | [diff] [blame] | 280 | class Message: |
Rob Mohr | f6ee7cb | 2020-10-26 14:05:06 -0700 | [diff] [blame] | 281 | 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 Mohr | 092a446 | 2020-06-16 10:23:54 -0700 | [diff] [blame] | 294 | def _is_hash(value): |
Rob Mohr | 5720460 | 2020-09-23 08:41:18 -0700 | [diff] [blame] | 295 | return re.match(r'^[0-9a-fA-F]{40}', value) |
Rob Mohr | 092a446 | 2020-06-16 10:23:54 -0700 | [diff] [blame] | 296 | |
| 297 | |
Rob Mohr | 0897353 | 2021-09-01 08:34:11 -0700 | [diff] [blame] | 298 | def _pprint_dict(d): |
| 299 | result = [] |
| 300 | for k, v in sorted(d.items()): |
Rob Mohr | 4806b6b | 2023-02-03 18:03:32 +0000 | [diff] [blame] | 301 | result.append(f'{k!r}: {v!r}\n') |
Rob Mohr | 0897353 | 2021-09-01 08:34:11 -0700 | [diff] [blame] | 302 | return ''.join(result) |
| 303 | |
| 304 | |
Rob Mohr | d39ea9e | 2020-09-22 15:01:07 -0700 | [diff] [blame] | 305 | class RollUtilApi(recipe_api.RecipeApi): |
Rob Mohr | 1eb8f91 | 2020-10-07 13:17:17 -0700 | [diff] [blame] | 306 | def __init__(self, props, *args, **kwargs): |
Rob Mohr | 78edbd0 | 2022-03-23 16:08:16 -0700 | [diff] [blame] | 307 | super().__init__(*args, **kwargs) |
Rob Mohr | a715424 | 2021-09-01 08:30:53 -0700 | [diff] [blame] | 308 | self.labels_to_set = collections.OrderedDict() |
Rob Mohr | 0bb03b6 | 2021-10-05 10:11:17 -0700 | [diff] [blame] | 309 | for label in sorted(props.labels_to_set, key=lambda x: x.label): |
Rob Mohr | 0ad390b | 2021-09-01 08:32:37 -0700 | [diff] [blame] | 310 | 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 Mohr | 1c1ef30 | 2021-03-08 12:23:14 -0800 | [diff] [blame] | 312 | self.footer = [] |
Rob Mohr | 3ed6c79 | 2022-01-13 07:52:26 -0800 | [diff] [blame] | 313 | self._commit_divider = props.commit_divider |
Rob Mohr | 1eb8f91 | 2020-10-07 13:17:17 -0700 | [diff] [blame] | 314 | |
Rob Mohr | 3698459 | 2021-03-31 15:46:38 -0700 | [diff] [blame] | 315 | 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 Mohr | 43c669b | 2022-01-10 12:44:04 -0800 | [diff] [blame] | 325 | 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 Mohr | 9f78be7 | 2022-06-21 09:57:16 -0700 | [diff] [blame] | 328 | # Make sure not to add it twice, and there's no need to do this for |
| 329 | # service accounts. |
Rob Mohr | e28a5a9 | 2022-01-10 15:16:37 -0800 | [diff] [blame] | 330 | email = author.email |
| 331 | prefix = 'pigweed.infra.roller.' |
Rob Mohr | 9f78be7 | 2022-06-21 09:57:16 -0700 | [diff] [blame] | 332 | if prefix not in email and not email.endswith('gserviceaccount.com'): |
Rob Mohr | e28a5a9 | 2022-01-10 15:16:37 -0800 | [diff] [blame] | 333 | user, domain = author.email.split('@') |
Rob Mohr | 4806b6b | 2023-02-03 18:03:32 +0000 | [diff] [blame] | 334 | email = f'{user}@{prefix}{domain}' |
Rob Mohr | e28a5a9 | 2022-01-10 15:16:37 -0800 | [diff] [blame] | 335 | |
| 336 | return Account(author.name, email,) |
Rob Mohr | 43c669b | 2022-01-10 12:44:04 -0800 | [diff] [blame] | 337 | |
Rob Mohr | 3698459 | 2021-03-31 15:46:38 -0700 | [diff] [blame] | 338 | 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 Mohr | b04227c | 2021-04-01 12:38:33 -0700 | [diff] [blame] | 345 | def can_cc_on_roll(self, email, host): |
Rob Mohr | 3698459 | 2021-03-31 15:46:38 -0700 | [diff] [blame] | 346 | # 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 Mohr | 4806b6b | 2023-02-03 18:03:32 +0000 | [diff] [blame] | 353 | email, f'email:{email}', host=host, test_data=test_data, |
Rob Mohr | 3698459 | 2021-03-31 15:46:38 -0700 | [diff] [blame] | 354 | ).json.output |
| 355 | |
Rob Mohr | 43c669b | 2022-01-10 12:44:04 -0800 | [diff] [blame] | 356 | def include_cc(self, account, cc_domains, host): |
Rob Mohr | 4806b6b | 2023-02-03 18:03:32 +0000 | [diff] [blame] | 357 | with self.m.step.nest(f'cc {account.email}') as pres: |
Rob Mohr | 43c669b | 2022-01-10 12:44:04 -0800 | [diff] [blame] | 358 | domain = account.email.split('@', 1)[1] |
Rob Mohr | b04227c | 2021-04-01 12:38:33 -0700 | [diff] [blame] | 359 | 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 Mohr | 43c669b | 2022-01-10 12:44:04 -0800 | [diff] [blame] | 365 | if not self.can_cc_on_roll(account.email, host=host): |
Rob Mohr | b04227c | 2021-04-01 12:38:33 -0700 | [diff] [blame] | 366 | 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 Mohr | f6ee7cb | 2020-10-26 14:05:06 -0700 | [diff] [blame] | 372 | def _single_commit_roll_message(self, roll): |
Rob Mohr | 5720460 | 2020-09-23 08:41:18 -0700 | [diff] [blame] | 373 | template = """ |
Rob Mohr | 092a446 | 2020-06-16 10:23:54 -0700 | [diff] [blame] | 374 | [roll {project_name}] {sanitized_message} |
| 375 | |
Rob Mohr | fc794bc | 2021-08-06 11:20:33 -0700 | [diff] [blame] | 376 | {remote} |
Rob Mohr | f6ee7cb | 2020-10-26 14:05:06 -0700 | [diff] [blame] | 377 | {project_name} Rolled-Commits: {old_revision:.15}..{new_revision:.15} |
| 378 | """.strip() |
| 379 | |
| 380 | commit = roll.commits[0] |
Rob Mohr | 092a446 | 2020-06-16 10:23:54 -0700 | [diff] [blame] | 381 | |
Rob Mohr | 5720460 | 2020-09-23 08:41:18 -0700 | [diff] [blame] | 382 | kwargs = { |
Rob Mohr | f6ee7cb | 2020-10-26 14:05:06 -0700 | [diff] [blame] | 383 | 'project_name': roll.project_name, |
Rob Mohr | fc794bc | 2021-08-06 11:20:33 -0700 | [diff] [blame] | 384 | 'remote': roll.remote, |
Rob Mohr | 5720460 | 2020-09-23 08:41:18 -0700 | [diff] [blame] | 385 | 'original_message': commit.message, |
| 386 | 'sanitized_message': _sanitize_message(commit.message), |
Rob Mohr | f6ee7cb | 2020-10-26 14:05:06 -0700 | [diff] [blame] | 387 | 'old_revision': roll.old_revision, |
| 388 | 'new_revision': roll.new_revision, |
Rob Mohr | 5720460 | 2020-09-23 08:41:18 -0700 | [diff] [blame] | 389 | } |
Rob Mohr | 092a446 | 2020-06-16 10:23:54 -0700 | [diff] [blame] | 390 | |
Rob Mohr | f6ee7cb | 2020-10-26 14:05:06 -0700 | [diff] [blame] | 391 | message = Message( |
| 392 | name=roll.project_name, |
| 393 | template=template, |
| 394 | kwargs=kwargs, |
| 395 | num_commits=1, |
| 396 | footer=tuple(self.footer), |
| 397 | ) |
Rob Mohr | 092a446 | 2020-06-16 10:23:54 -0700 | [diff] [blame] | 398 | |
Rob Mohr | 4806b6b | 2023-02-03 18:03:32 +0000 | [diff] [blame] | 399 | with self.m.step.nest(f'message for {roll.project_name}') as pres: |
Rob Mohr | 5720460 | 2020-09-23 08:41:18 -0700 | [diff] [blame] | 400 | pres.logs['template'] = template |
Rob Mohr | 0897353 | 2021-09-01 08:34:11 -0700 | [diff] [blame] | 401 | pres.logs['kwargs'] = _pprint_dict(kwargs) |
Rob Mohr | f6ee7cb | 2020-10-26 14:05:06 -0700 | [diff] [blame] | 402 | pres.logs['message'] = message.render() |
Rob Mohr | 092a446 | 2020-06-16 10:23:54 -0700 | [diff] [blame] | 403 | |
Rob Mohr | 5720460 | 2020-09-23 08:41:18 -0700 | [diff] [blame] | 404 | return message |
Rob Mohr | 092a446 | 2020-06-16 10:23:54 -0700 | [diff] [blame] | 405 | |
Rob Mohr | f6ee7cb | 2020-10-26 14:05:06 -0700 | [diff] [blame] | 406 | def _multiple_commits_roll_message(self, roll): |
Rob Mohr | 5720460 | 2020-09-23 08:41:18 -0700 | [diff] [blame] | 407 | template = """ |
Rob Mohr | f6ee7cb | 2020-10-26 14:05:06 -0700 | [diff] [blame] | 408 | [{project_name}] Roll {num_commits} commits |
Rob Mohr | 092a446 | 2020-06-16 10:23:54 -0700 | [diff] [blame] | 409 | |
| 410 | {one_liners} |
| 411 | |
Rob Mohr | fc794bc | 2021-08-06 11:20:33 -0700 | [diff] [blame] | 412 | {remote} |
Rob Mohr | f6ee7cb | 2020-10-26 14:05:06 -0700 | [diff] [blame] | 413 | {project_name} Rolled-Commits: {old_revision:.15}..{new_revision:.15} |
Rob Mohr | 092a446 | 2020-06-16 10:23:54 -0700 | [diff] [blame] | 414 | """.strip() |
| 415 | |
Rob Mohr | 516bd88 | 2022-11-21 19:36:24 +0000 | [diff] [blame] | 416 | 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 Mohr | 092a446 | 2020-06-16 10:23:54 -0700 | [diff] [blame] | 424 | |
Rob Mohr | f6ee7cb | 2020-10-26 14:05:06 -0700 | [diff] [blame] | 425 | num_commits = len(roll.commits) |
Rob Mohr | 10b9920 | 2022-06-07 12:12:27 -0700 | [diff] [blame] | 426 | |
Rob Mohr | f6ee7cb | 2020-10-26 14:05:06 -0700 | [diff] [blame] | 427 | if not _is_hash(roll.old_revision): |
| 428 | num_commits = 'multiple' |
Rob Mohr | 5720460 | 2020-09-23 08:41:18 -0700 | [diff] [blame] | 429 | one_liners.append('...') |
Rob Mohr | 092a446 | 2020-06-16 10:23:54 -0700 | [diff] [blame] | 430 | |
Rob Mohr | 10b9920 | 2022-06-07 12:12:27 -0700 | [diff] [blame] | 431 | 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 Mohr | 5720460 | 2020-09-23 08:41:18 -0700 | [diff] [blame] | 437 | kwargs = { |
Rob Mohr | f6ee7cb | 2020-10-26 14:05:06 -0700 | [diff] [blame] | 438 | 'project_name': roll.project_name, |
Rob Mohr | fc794bc | 2021-08-06 11:20:33 -0700 | [diff] [blame] | 439 | 'remote': roll.remote, |
Rob Mohr | f6ee7cb | 2020-10-26 14:05:06 -0700 | [diff] [blame] | 440 | 'num_commits': num_commits, |
Rob Mohr | 5720460 | 2020-09-23 08:41:18 -0700 | [diff] [blame] | 441 | 'one_liners': '\n'.join(one_liners), |
Rob Mohr | f6ee7cb | 2020-10-26 14:05:06 -0700 | [diff] [blame] | 442 | 'old_revision': roll.old_revision, |
| 443 | 'new_revision': roll.new_revision, |
Rob Mohr | 5720460 | 2020-09-23 08:41:18 -0700 | [diff] [blame] | 444 | } |
Rob Mohr | 092a446 | 2020-06-16 10:23:54 -0700 | [diff] [blame] | 445 | |
Rob Mohr | f6ee7cb | 2020-10-26 14:05:06 -0700 | [diff] [blame] | 446 | 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 Mohr | 092a446 | 2020-06-16 10:23:54 -0700 | [diff] [blame] | 453 | |
Rob Mohr | 5720460 | 2020-09-23 08:41:18 -0700 | [diff] [blame] | 454 | with self.m.step.nest('message') as pres: |
| 455 | pres.logs['template'] = template |
Rob Mohr | 0897353 | 2021-09-01 08:34:11 -0700 | [diff] [blame] | 456 | pres.logs['kwargs'] = _pprint_dict(kwargs) |
Rob Mohr | f6ee7cb | 2020-10-26 14:05:06 -0700 | [diff] [blame] | 457 | pres.logs['message'] = message.render() |
Rob Mohr | 092a446 | 2020-06-16 10:23:54 -0700 | [diff] [blame] | 458 | |
Rob Mohr | 5720460 | 2020-09-23 08:41:18 -0700 | [diff] [blame] | 459 | return message |
Rob Mohr | 092a446 | 2020-06-16 10:23:54 -0700 | [diff] [blame] | 460 | |
Rob Mohr | f6ee7cb | 2020-10-26 14:05:06 -0700 | [diff] [blame] | 461 | 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 Mohr | 092a446 | 2020-06-16 10:23:54 -0700 | [diff] [blame] | 465 | |
Rob Mohr | f6ee7cb | 2020-10-26 14:05:06 -0700 | [diff] [blame] | 466 | 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 Mohr | 4806b6b | 2023-02-03 18:03:32 +0000 | [diff] [blame] | 480 | texts.append('\n'.join(f'{x}' for x in self.footer)) |
Rob Mohr | f6ee7cb | 2020-10-26 14:05:06 -0700 | [diff] [blame] | 481 | |
| 482 | return '\n\n'.join(texts) |
| 483 | |
Rob Mohr | 43c669b | 2022-01-10 12:44:04 -0800 | [diff] [blame] | 484 | def Account(self, name, email): |
| 485 | return Account(name, email) |
| 486 | |
Rob Mohr | f6ee7cb | 2020-10-26 14:05:06 -0700 | [diff] [blame] | 487 | 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 Mohr | 5720460 | 2020-09-23 08:41:18 -0700 | [diff] [blame] | 492 | with self.m.step.nest('roll message'): |
Rob Mohr | f6ee7cb | 2020-10-26 14:05:06 -0700 | [diff] [blame] | 493 | if len(rolls) > 1: |
Rob Mohr | 3ed6c79 | 2022-01-13 07:52:26 -0800 | [diff] [blame] | 494 | result = self._multiple_rolls_message(*rolls) |
| 495 | else: |
| 496 | result = self._single_roll_message(*rolls).render() |
| 497 | if self._commit_divider: |
Rob Mohr | 4806b6b | 2023-02-03 18:03:32 +0000 | [diff] [blame] | 498 | result += f'\n{self._commit_divider}' |
Rob Mohr | 3ed6c79 | 2022-01-13 07:52:26 -0800 | [diff] [blame] | 499 | return result |
Rob Mohr | d39ea9e | 2020-09-22 15:01:07 -0700 | [diff] [blame] | 500 | |
Rob Mohr | f7e3015 | 2020-11-18 09:56:30 -0800 | [diff] [blame] | 501 | Direction = _Direction |
| 502 | |
Rob Mohr | 476a704 | 2020-11-30 13:33:52 -0800 | [diff] [blame] | 503 | def get_roll_direction(self, git_dir, old, new, name='get roll direction'): |
Rob Mohr | f7e3015 | 2020-11-18 09:56:30 -0800 | [diff] [blame] | 504 | """Return Direction of roll.""" |
Rob Mohr | 5720460 | 2020-09-23 08:41:18 -0700 | [diff] [blame] | 505 | if old == new: |
| 506 | with self.m.step.nest(name) as pres: |
| 507 | pres.step_summary_text = 'up-to-date' |
Rob Mohr | f7e3015 | 2020-11-18 09:56:30 -0800 | [diff] [blame] | 508 | return self.Direction.CURRENT |
Rob Mohr | d39ea9e | 2020-09-22 15:01:07 -0700 | [diff] [blame] | 509 | |
Rob Mohr | 5720460 | 2020-09-23 08:41:18 -0700 | [diff] [blame] | 510 | with self.m.context(git_dir): |
Rob Mohr | f7e3015 | 2020-11-18 09:56:30 -0800 | [diff] [blame] | 511 | 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 Mohr | d39ea9e | 2020-09-22 15:01:07 -0700 | [diff] [blame] | 520 | |
Rob Mohr | f7e3015 | 2020-11-18 09:56:30 -0800 | [diff] [blame] | 521 | backward = self.m.git( |
| 522 | 'is backward', |
| 523 | 'merge-base', |
| 524 | '--is-ancestor', |
| 525 | new, |
| 526 | old, |
| 527 | ok_ret=(0, 1), |
| 528 | ) |
Rob Mohr | d39ea9e | 2020-09-22 15:01:07 -0700 | [diff] [blame] | 529 | |
Rob Mohr | f7e3015 | 2020-11-18 09:56:30 -0800 | [diff] [blame] | 530 | 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 Mohr | d39ea9e | 2020-09-22 15:01:07 -0700 | [diff] [blame] | 536 | |
Rob Mohr | f7e3015 | 2020-11-18 09:56:30 -0800 | [diff] [blame] | 537 | 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 Mohr | cbee3fc | 2020-11-30 14:07:42 -0800 | [diff] [blame] | 544 | # 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 Mohr | f7e3015 | 2020-11-18 09:56:30 -0800 | [diff] [blame] | 556 | # 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 Mohr | 5720460 | 2020-09-23 08:41:18 -0700 | [diff] [blame] | 566 | 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 Mohr | ad16bc1 | 2020-09-28 06:39:55 -0700 | [diff] [blame] | 572 | fmt = ( |
| 573 | 'not updating from {old} to {new} because they are ' |
| 574 | 'identical' |
| 575 | ) |
Rob Mohr | 5720460 | 2020-09-23 08:41:18 -0700 | [diff] [blame] | 576 | pres.step_summary_text = fmt.format( |
| 577 | old=old_revision[0:7], new=new_revision[0:7] |
| 578 | ) |
Rob Mohr | 4806b6b | 2023-02-03 18:03:32 +0000 | [diff] [blame] | 579 | pres.links[old_revision] = f'{remote}/+/{old_revision}' |
| 580 | pres.links[new_revision] = f'{remote}/+/{new_revision}' |
Rob Mohr | 18549cf | 2021-07-27 16:15:19 -0700 | [diff] [blame] | 581 | |
| 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 Mohr | 98ca152 | 2021-11-03 14:26:12 -0700 | [diff] [blame] | 604 | remote, n = re.subn(r'/\./', '/', remote) |
Rob Mohr | 18549cf | 2021-07-27 16:15:19 -0700 | [diff] [blame] | 605 | changes += n |
| 606 | |
Rob Mohr | 98ca152 | 2021-11-03 14:26:12 -0700 | [diff] [blame] | 607 | remote, n = re.subn(r'/[^/]+/\.\./', '/', remote) |
Rob Mohr | 18549cf | 2021-07-27 16:15:19 -0700 | [diff] [blame] | 608 | changes += n |
| 609 | |
| 610 | return self.m.sso.sso_to_https(remote) |