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 |