| OLD | NEW |
| 1 # coding=utf8 | 1 # coding=utf8 |
| 2 # Copyright (c) 2010 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2010 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
| 5 """Commit queue manager class. | 5 """Commit queue manager class. |
| 6 | 6 |
| 7 Security implications: | 7 Security implications: |
| 8 | 8 |
| 9 The following hypothesis are made: | 9 The following hypothesis are made: |
| 10 - Commit queue: | 10 - Commit queue: |
| (...skipping 26 matching lines...) Expand all Loading... |
| 37 breakpad.SendStack(e, | 37 breakpad.SendStack(e, |
| 38 ''.join(traceback.format_tb(sys.exc_info()[2])), | 38 ''.join(traceback.format_tb(sys.exc_info()[2])), |
| 39 maxlen=2000) | 39 maxlen=2000) |
| 40 | 40 |
| 41 | 41 |
| 42 class PendingCommit(base.Verified): | 42 class PendingCommit(base.Verified): |
| 43 """Represents a pending commit that is being processed.""" | 43 """Represents a pending commit that is being processed.""" |
| 44 persistent = base.Verified.persistent + [ | 44 persistent = base.Verified.persistent + [ |
| 45 # Important since they tell if we need to revalidate and send try jobs | 45 # Important since they tell if we need to revalidate and send try jobs |
| 46 # again or not if any of these value changes. | 46 # again or not if any of these value changes. |
| 47 'issue', 'patchset', 'rietveld_server', 'processed', | 47 'issue', 'patchset', 'rietveld_server', 'processed', 'description', |
| 48 # Only a cache, these values can be regenerated. | 48 # Only a cache, these values can be regenerated. |
| 49 'owner', 'reviewers', 'base_url', | 49 'owner', 'reviewers', 'base_url', |
| 50 # Only used after a patch was committed. Keeping here for completeness. | 50 # Only used after a patch was committed. Keeping here for completeness. |
| 51 'description', 'revision', | 51 'revision', |
| 52 ] | 52 ] |
| 53 | 53 |
| 54 def __init__(self, issue, owner, reviewers, patchset, base_url, | 54 def __init__(self, issue, owner, reviewers, patchset, base_url, description, |
| 55 rietveld_server): | 55 rietveld_server): |
| 56 super(PendingCommit, self).__init__() | 56 super(PendingCommit, self).__init__() |
| 57 self.issue = issue | 57 self.issue = issue |
| 58 self.owner = owner | 58 self.owner = owner |
| 59 self.reviewers = reviewers | 59 self.reviewers = reviewers |
| 60 self.patchset = patchset | 60 self.patchset = patchset |
| 61 self.base_url = base_url | 61 self.base_url = base_url |
| 62 self.rietveld_server = rietveld_server | 62 self.rietveld_server = rietveld_server |
| 63 self.description = None | 63 self.description = description |
| 64 self.revision = None | 64 self.revision = None |
| 65 self.processed = False | 65 self.processed = False |
| 66 | 66 |
| 67 def patch_url(self): | 67 def patch_url(self): |
| 68 return ('%s/download/issue%d_%d.diff' % | 68 return ('%s/download/issue%d_%d.diff' % |
| 69 (self.rietveld_server, self.issue, self.patchset)) | 69 (self.rietveld_server, self.issue, self.patchset)) |
| 70 | 70 |
| 71 def pending_name(self): | 71 def pending_name(self): |
| 72 """The name that should be used for try jobs. | 72 """The name that should be used for try jobs. |
| 73 | 73 |
| (...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 111 if issue_id not in known_issues: | 111 if issue_id not in known_issues: |
| 112 issue_data = self.rietveld.get_issue_properties(issue_id) | 112 issue_data = self.rietveld.get_issue_properties(issue_id) |
| 113 if issue_data.get('patchsets', None): | 113 if issue_data.get('patchsets', None): |
| 114 self.queue.pending_commits.append( | 114 self.queue.pending_commits.append( |
| 115 PendingCommit( | 115 PendingCommit( |
| 116 issue_data['issue'], | 116 issue_data['issue'], |
| 117 issue_data['owner_email'], | 117 issue_data['owner_email'], |
| 118 issue_data['reviewers'], | 118 issue_data['reviewers'], |
| 119 issue_data['patchsets'][-1], | 119 issue_data['patchsets'][-1], |
| 120 issue_data['base_url'], | 120 issue_data['base_url'], |
| 121 issue_data['description'].replace('\r', ''), |
| 121 self.rietveld.url)) | 122 self.rietveld.url)) |
| 122 except Exception, e: | 123 except Exception, e: |
| 123 # Swallow every exception in that code and move on. Make sure to send a | 124 # Swallow every exception in that code and move on. Make sure to send a |
| 124 # stack trace though. | 125 # stack trace though. |
| 125 send_stack(e) | 126 send_stack(e) |
| 126 | 127 |
| 127 def _fetch_pending_issues(self): | 128 def _fetch_pending_issues(self): |
| 128 """Returns the list of issue number for reviews on Rietveld with their last | 129 """Returns the list of issue number for reviews on Rietveld with their last |
| 129 patchset with commit+ flag set. | 130 patchset with commit+ flag set. |
| 130 """ | 131 """ |
| (...skipping 14 matching lines...) Expand all Loading... |
| 145 # Silently ignore. | 146 # Silently ignore. |
| 146 continue | 147 continue |
| 147 | 148 |
| 148 # The patch is fine to try. | 149 # The patch is fine to try. |
| 149 revision = self.checkout.prepare() | 150 revision = self.checkout.prepare() |
| 150 self._apply_patch(pending, revision) | 151 self._apply_patch(pending, revision) |
| 151 previous_cwd = os.getcwd() | 152 previous_cwd = os.getcwd() |
| 152 try: | 153 try: |
| 153 os.chdir(self.checkout.project_path) | 154 os.chdir(self.checkout.project_path) |
| 154 for verifier in self.verifiers: | 155 for verifier in self.verifiers: |
| 155 verifier.verify(pending, 'HEAD') | 156 verifier.verify(pending, revision) |
| 156 finally: | 157 finally: |
| 157 os.chdir(previous_cwd) | 158 os.chdir(previous_cwd) |
| 158 except base.DiscardPending, e: | 159 except base.DiscardPending, e: |
| 159 self._discard_pending(e.pending, e.message) | 160 self._discard_pending(e.pending, e.message) |
| 160 except Exception, e: | 161 except Exception, e: |
| 161 # Swallow every exception in that code and move on. Make sure to send a | 162 # Swallow every exception in that code and move on. Make sure to send a |
| 162 # stack trace though. | 163 # stack trace though. |
| 163 send_stack(e) | 164 send_stack(e) |
| 164 | 165 |
| 165 def update_status(self): | 166 def update_status(self): |
| (...skipping 17 matching lines...) Expand all Loading... |
| 183 self._discard_pending(pending, 'Patch verification failed') | 184 self._discard_pending(pending, 'Patch verification failed') |
| 184 elif pending.succeeded(): | 185 elif pending.succeeded(): |
| 185 # The item is removed right away. | 186 # The item is removed right away. |
| 186 self.queue.pending_commits.remove(pending) | 187 self.queue.pending_commits.remove(pending) |
| 187 try: | 188 try: |
| 188 self._last_minute_checks(pending) | 189 self._last_minute_checks(pending) |
| 189 commit_message = '%s\nReview URL: %s%s' % ( | 190 commit_message = '%s\nReview URL: %s%s' % ( |
| 190 pending.description, | 191 pending.description, |
| 191 pending.rietveld_server, | 192 pending.rietveld_server, |
| 192 pending.issue) | 193 pending.issue) |
| 193 pending.revision = self.checkout.commit(commit_message, pending.user) | 194 pending.revision = self.checkout.commit(commit_message, pending.owner) |
| 194 self._close_issue(pending) | 195 self._close_issue(pending) |
| 195 except base.DiscardPending, e: | 196 except base.DiscardPending, e: |
| 196 self._discard_pending(e.pending, e.message) | 197 self._discard_pending(e.pending, e.message) |
| 197 except Exception, e: | 198 except Exception, e: |
| 198 send_stack(e) | 199 send_stack(e) |
| 199 self._discard_pending(pending, 'Commit queue had an internal error') | 200 self._discard_pending(pending, 'Commit queue had an internal error') |
| 200 | 201 |
| 201 def _validity_checks(self, pending): | 202 def _validity_checks(self, pending): |
| 202 """Returns True if this pending is worth looking at.""" | 203 """Returns True if this pending is worth looking at.""" |
| 203 if not any(re.match(r, pending.base_url) for r in self.project_bases): | 204 if not any(re.match(r, pending.base_url) for r in self.project_bases): |
| 204 # Not a repository managed by this instance, silently ignore. | 205 # Not a repository managed by this instance, silently ignore. |
| 205 logging.info('%s not in whitelist' % pending.base_url) | 206 logging.info('%s not in whitelist' % pending.base_url) |
| 206 return False | 207 return False |
| 207 | 208 |
| 208 if any(re.match(r, pending.owner) for r in self.author_white_list): | 209 if not any(re.match(r, pending.owner) for r in self.author_white_list): |
| 209 # Can't commit because the owner is not on the whitelist. | 210 # Can't commit because the owner is not on the whitelist. |
| 210 logging.info('%s not in whitelist' % pending.owner) | 211 logging.info('%s not in whitelist' % pending.owner) |
| 211 return False | 212 return False |
| 212 | 213 |
| 213 # Need at least one reviewer matching at least one regexp in | 214 # Need at least one reviewer matching at least one regexp in |
| 214 # self.reviewers that is not also the owner of the issue. | 215 # self.reviewers that is not also the owner of the issue. |
| 215 def match_reviewer(reviewer): | 216 def match_reviewer(reviewer): |
| 216 return any(re.match(reviewer_regexp, reviewer) | 217 return any(re.match(reviewer_regexp, reviewer) |
| 217 for reviewer_regexp in self.reviewers) | 218 for reviewer_regexp in self.reviewers) |
| 218 | 219 |
| 219 if not any(match_reviewer(reviewer) for reviewer in pending.reviewers | 220 if not any(match_reviewer(reviewer) for reviewer in pending.reviewers |
| 220 if not reviewer == pending.owner): | 221 if not reviewer == pending.owner): |
| 221 return False | 222 return False |
| 222 return True | 223 return True |
| 223 | 224 |
| 224 | 225 |
| 225 def _last_minute_checks(self, pending): | 226 def _last_minute_checks(self, pending): |
| 226 """Does last minute checks before committing a pending patch. | 227 """Does last minute checks before committing a pending patch. |
| 227 | 228 |
| 228 Also prepares the patch to be committed. Throws if the patch should be | 229 Also prepares the patch to be committed. Throws if the patch should be |
| 229 discarded. | 230 discarded. |
| 230 """ | 231 """ |
| 231 revision = self.checkout.prepare() | 232 revision = self.checkout.prepare() |
| 232 self._apply_patch(pending, revision) | 233 self._apply_patch(pending, revision) |
| 233 # Do a last minute verifications just before committing. | 234 # Do a last minute verifications just before committing. |
| 234 pending_data = self.rietveld.get_issue_properties(pending.issue) | 235 pending_data = self.rietveld.get_issue_properties(pending.issue) |
| 235 pending.description = pending_data['description'].replace('\r', '') | 236 if pending.description != pending_data['description'].replace('\r', ''): |
| 237 raise base.DiscardPending(pending, |
| 238 'Commit queue failed due to updated description.') |
| 236 pending.reviewers = pending_data['reviewers'] | 239 pending.reviewers = pending_data['reviewers'] |
| 237 if pending.patchset != pending_data['patchsets'][-1]: | 240 if pending.patchset != pending_data['patchsets'][-1]: |
| 238 raise base.DiscardPending(pending, | 241 raise base.DiscardPending(pending, |
| 239 'Commit queue failed due to new patchset.') | 242 'Commit queue failed due to new patchset.') |
| 240 if pending_data['closed'] == 'true': | 243 if pending_data['closed'] == 'true': |
| 241 raise base.DiscardPending(pending, None) | 244 raise base.DiscardPending(pending, None) |
| 242 if not self._validity_checks(pending): | 245 if not self._validity_checks(pending): |
| 243 raise base.DiscardPending(pending, | 246 raise base.DiscardPending(pending, |
| 244 'This patch fails to pass validity checks to be automatically ' | 247 'This patch fails to pass validity checks to be automatically ' |
| 245 'committed.') | 248 'committed.') |
| 246 TODO('Search for \'lgtm\' in the comments') | 249 TODO('Search for \'lgtm\' in the comments') |
| 247 | 250 |
| 248 def _close_issue(self, pending): | 251 def _close_issue(self, pending): |
| 249 """Closes a issue on Rietveld after a commit succeeded.""" | 252 """Closes a issue on Rietveld after a commit succeeded.""" |
| 250 viewvc_url = self.checkout.get_setting('VIEW_VC') | 253 viewvc_url = self.checkout.get_settings('VIEW_VC') |
| 251 description = pending.description | 254 description = pending.description |
| 252 if viewvc_url: | 255 if viewvc_url: |
| 253 description += '\nCommitted: %s%s' % (viewvc_url, pending.revision) | 256 description += '\nCommitted: %s%s' % (viewvc_url, pending.revision) |
| 254 self.rietveld.close_issue(pending.issue) | 257 self.rietveld.close_issue(pending.issue) |
| 255 self.rietveld.update_description(pending.issue, description) | 258 self.rietveld.update_description(pending.issue, description) |
| 256 | 259 |
| 257 def _discard_pending(self, pending, message): | 260 def _discard_pending(self, pending, message): |
| 258 """Discards a pending commit. Attach an optional message to the review.""" | 261 """Discards a pending commit. Attach an optional message to the review.""" |
| 259 self.rietveld.set_flag(pending.issue, pending.patchset, 'commit', 'False') | 262 self.rietveld.set_flag(pending.issue, pending.patchset, 'commit', 'False') |
| 260 if message: | 263 if message: |
| (...skipping 15 matching lines...) Expand all Loading... |
| 276 'Patch failed to apply against %s.' % revision) | 279 'Patch failed to apply against %s.' % revision) |
| 277 | 280 |
| 278 def load(self, filename): | 281 def load(self, filename): |
| 279 """Loads the commit queue state from a JSON file.""" | 282 """Loads the commit queue state from a JSON file.""" |
| 280 self.queue = model.load_from_json_file(filename) | 283 self.queue = model.load_from_json_file(filename) |
| 281 self.queue.pending_commits = self.queue.pending_commits or [] | 284 self.queue.pending_commits = self.queue.pending_commits or [] |
| 282 | 285 |
| 283 def save(self, filename): | 286 def save(self, filename): |
| 284 """Save the commit queue state in a simple JSON file.""" | 287 """Save the commit queue state in a simple JSON file.""" |
| 285 model.save_to_json_file(filename, self.queue) | 288 model.save_to_json_file(filename, self.queue) |
| OLD | NEW |