def _last_minute_checks(self, pending): """Does last minute checks on Rietvld before committing a pending patch.""" pending_data = self.context.rietveld.get_issue_properties( pending.issue, True) if pending_data['commit'] != True: raise base.DiscardPending(pending, None) if pending_data['closed'] != False: raise base.DiscardPending(pending, None) if pending.description != pending_data['description'].replace('\r', ''): raise base.DiscardPending(pending, self.DESCRIPTION_UPDATED) commit_user = set([self.context.rietveld.email]) expected = set(pending.reviewers) - commit_user actual = set(pending_data['reviewers']) - commit_user # Try to be nice, if there was a drive-by review and the new reviewer left # a lgtm, don't abort. def is_approver(r): return any( m.get('approval') for m in pending_data['messages'] if m['sender'] == r) drivers_by = [r for r in (actual - expected) if not is_approver(r)] if drivers_by: # That annoying driver-by. raise base.DiscardPending( pending, 'List of reviewers changed. %s did a drive-by without LGTM\'ing!' % ','.join(drivers_by)) if pending.patchset != pending_data['patchsets'][-1]: raise base.DiscardPending(pending, 'Commit queue failed due to new patchset.')
def _commit_patch(self, pending): """Commits the pending patch to the repository. Do the checkout and applies the patch. """ try: # Make sure to apply on HEAD. pending.revision = None pending.apply_patch(self.context, True) commit_message = '%s\n\nReview URL: %s/%s' % ( pending.description, self.context.rietveld.url, pending.issue) pending.revision = self.context.checkout.commit( commit_message, pending.owner) self.recent_commit_timestamps.append(time.time()) self.recent_commit_timestamps = ( self.recent_commit_timestamps[-(self.MAX_COMMIT_BURST + 1):]) if not pending.revision: raise base.DiscardPending(pending, 'Failed to commit patch.') self._close_issue(pending) except (checkout.PatchApplicationFailed, patch.UnsupportedPatchFormat), e: raise base.DiscardPending(pending, str(e))
def apply_patch(self, context_obj, prepare): """Applies the pending patch to the checkout and throws if it fails.""" try: if prepare: self.prepare_for_patch(context_obj) patches = context_obj.rietveld.get_patch(self.issue, self.patchset) if not patches: raise base.DiscardPending( self, 'No diff was found for this patchset.') if self.relpath: patches.set_relpath(self.relpath) self.files = [p.filename for p in patches] if not self.files: raise base.DiscardPending( self, 'No file was found in this patchset.') context_obj.checkout.apply_patch(patches) except (checkout.PatchApplicationFailed, patch.UnsupportedPatchFormat) as e: raise base.DiscardPending(self, str(e)) except subprocess2.CalledProcessError as e: out = 'Failed to apply the patch.' if e.stdout: out += '\n%s' % e.stdout raise base.DiscardPending(self, out) except urllib2.HTTPError as e: raise base.DiscardPending(self, ( 'Failed to request the patch to try. Please note that binary files ' 'are still unsupported at the moment, this is being worked on.\n\n' 'Thanks for your patience.\n\n%s') % e)
def _pending_run_verifiers(cls, pending, verifiers): """Runs verifiers on a pending change. Returns True if all Verifiers were run. """ for verifier in verifiers: if verifier.name in pending.verifications: logging.warning( 'Re-running verififer %s for issue %s' % ( verifier.name, pending.issue)) verifier.verify(pending) assert verifier.name in pending.verifications if pending.get_state() == base.IGNORED: assert pending.verifications[verifier.name].get_state() == base.IGNORED # Remove all the other verifiers since we need to keep it in the # 'datastore' to not retry this issue constantly. for key in pending.verifications.keys(): if key != verifier.name: del pending.verifications[key] return False if pending.get_state() == base.FAILED: # Throw if it didn't pass, so the error message is not lost. raise base.DiscardPending( pending, pending.error_message() or cls.FAILED_NO_MESSAGE) return True
def apply_patch(self, context_obj, prepare): """Applies the pending patch to the checkout and throws if it fails.""" try: if prepare: self.prepare_for_patch(context_obj) patches = context_obj.rietveld.get_patch(self.issue, self.patchset) if not patches: raise base.DiscardPending( self, 'No diff was found for this patchset.') if self.relpath: patches.set_relpath(self.relpath) self.files = [p.filename for p in patches] if not self.files: raise base.DiscardPending( self, 'No file was found in this patchset.') context_obj.checkout.apply_patch(patches) except (checkout.PatchApplicationFailed, patch.UnsupportedPatchFormat), e: raise base.DiscardPending(self, str(e))
def _send_jobs(self, pending, jobs, need_prepare, builders_and_tests, job_name): for job in jobs: job.tries = job.tries or 0 job.tries += 1 if job.tries > 4: raise base.DiscardPending(pending, ( 'The commit queue went berserk retrying too often for a\n' 'seemingly flaky test. Builder is %s, revision is %s, job name\n' 'was %s.') % (job.builder, job.revision, job_name)) builders = sorted(job.builder for job in jobs) assert len(set(builders)) == len(builders) revision = set(job.revision for job in jobs) assert len(revision) == 1 revision = revision.pop() clobber = set(job.clobber for job in jobs) assert len(clobber) == 1 clobber = clobber.pop() for job in jobs: job.result = None job.build = None job.name = job_name job.tests = builders_and_tests[job.builder] if need_prepare: # Running from inside update_status(), the patch wasn't applied. Do it # now. pending.revision = revision pending.apply_patch(self.context, True) self._send_job(pending, revision, clobber, builders_and_tests, job_name) for builder in builders: # Signal a new try job was sent. info = { 'builder': builder, 'clobber': job.clobber, 'job_name': job_name, 'revision': revision, } self.send_status(pending, info) for job in jobs: job.sent = time.time()
def _send_job(self, pending, revision, clobber, builders_and_tests, job_name): """Sends a try job.""" assert revision cmd = [ '--no_search', '--revision', '%s@%s' % (self.solution, revision), '--name', job_name, '--user', self.commit_user.split('@', 1)[0], '--email', self.commit_user, '--rietveld_url', self._patch_url(pending), '--issue', str(pending.issue), '--patchset', str(pending.patchset) ] cmd.extend(self.extra_flags) for builder in sorted(builders_and_tests): cmd.append('--bot') tests = builders_and_tests[builder] if tests: cmd.append('%s:%s' % (builder, ','.join(tests))) else: cmd.append(builder) if clobber: cmd.append('--clobber') # TODO(maruel): use GitChange when relevant. change = presubmit_support.SvnChange( job_name, pending.description, self.context.checkout.project_path, [('M', f) for f in pending.files], pending.issue, pending.patchset, pending.owner) prev_dir = os.getcwd() try: os.chdir(self.context.checkout.project_path) trychange.TryChange(cmd, change, swallow_exception=True) except SystemExit, e: logging.error('_send_job(%s, %s, %s, %s, %s) failed!' % (pending.pending_name(), revision, clobber, builders_and_tests, job_name)) raise base.DiscardPending( pending, 'Failed to send try job %s: %s' % (job_name, e))
def prepare_for_patch(self, context_obj): self.revision = context_obj.checkout.prepare(self.revision) # Verify revision consistency. if not self.revision: raise base.DiscardPending( self, 'Internal error: failed to checkout. Please try again.')
class PendingCommit(base.Verified): """Represents a pending commit that is being processed.""" persistent = base.Verified.persistent + [ # Important since they tell if we need to revalidate and send try jobs # again or not if any of these value changes. 'issue', 'patchset', 'description', 'files', # Only a cache, these values can be regenerated. 'owner', 'reviewers', 'base_url', 'messages', 'relpath', # Only used after a patch was committed. Keeping here for try job retries. 'revision', ] def __init__( self, issue, owner, reviewers, patchset, base_url, description, messages): super(PendingCommit, self).__init__() self.issue = issue self.owner = owner self.reviewers = reviewers self.patchset = patchset self.base_url = base_url self.description = description # Convert to unicode whenever necessary. if isinstance(self.description, str): self.description = self.description.decode('utf-8') assert isinstance(self.description, unicode) self.messages = messages for message in self.messages: # Save storage, no verifier really need 'text', just 'approval'. if 'text' in message: del message['text'] self.revision = None self.relpath = '' self.files = [] def pending_name(self): """The name that should be used for try jobs. It makes it possible to regenerate the try_jobs array if ever needed.""" return '%d-%d' % (self.issue, self.patchset) def prepare_for_patch(self, context_obj): self.revision = context_obj.checkout.prepare(self.revision) # Verify revision consistency. if not self.revision: raise base.DiscardPending( self, 'Internal error: failed to checkout. Please try again.') def apply_patch(self, context_obj, prepare): """Applies the pending patch to the checkout and throws if it fails.""" try: if prepare: self.prepare_for_patch(context_obj) patches = context_obj.rietveld.get_patch(self.issue, self.patchset) if not patches: raise base.DiscardPending( self, 'No diff was found for this patchset.') if self.relpath: patches.set_relpath(self.relpath) self.files = [p.filename for p in patches] if not self.files: raise base.DiscardPending( self, 'No file was found in this patchset.') context_obj.checkout.apply_patch(patches) except (checkout.PatchApplicationFailed, patch.UnsupportedPatchFormat), e: raise base.DiscardPending(self, str(e)) except subprocess2.CalledProcessError, e: out = 'Failed to apply the patch.' if e.stdout: out += '\n%s' % e.stdout raise base.DiscardPending(self, out)
self.files = [p.filename for p in patches] if not self.files: raise base.DiscardPending( self, 'No file was found in this patchset.') context_obj.checkout.apply_patch(patches) except (checkout.PatchApplicationFailed, patch.UnsupportedPatchFormat), e: raise base.DiscardPending(self, str(e)) except subprocess2.CalledProcessError, e: out = 'Failed to apply the patch.' if e.stdout: out += '\n%s' % e.stdout raise base.DiscardPending(self, out) except urllib2.HTTPError, e: raise base.DiscardPending( self, ('Failed to request the patch to try. Please note that binary files' 'are still unsupported at the moment, this is being worked on.\n\n' 'Thanks for your patience.\n\n%s') % e) class PendingQueue(model.PersistentMixIn): """Represents the queue of pending commits being processed.""" persistent = ['pending_commits'] def __init__(self): super(PendingQueue, self).__init__() self.pending_commits = [] class PendingManager(object): """Fetch new issues from rietveld, pass the issues through all of verifiers
def _commit_patch(self, pending): """Commits the pending patch to the repository. Do the checkout and applies the patch. """ try: try: # Make sure to apply on HEAD. pending.revision = None pending.apply_patch(self.context, True) # Commit it. commit_desc = git_cl.ChangeDescription(pending.description) if (self.context.server_hooks_missing and self.context.rietveld.email != pending.owner): commit_desc.update_reviewers(pending.reviewers) commit_desc.append_footer('Author: ' + pending.owner) commit_desc.append_footer( 'Review URL: %s/%s' % (self.context.rietveld.url, pending.issue)) pending.revision = self.context.checkout.commit( commit_desc.description, pending.owner) if not pending.revision: raise base.DiscardPending(pending, 'Failed to commit patch.') # Note that the commit succeeded for commit throttling. self.recent_commit_timestamps.append(time.time()) self.recent_commit_timestamps = ( self.recent_commit_timestamps[-(self.MAX_COMMIT_BURST + 1):]) viewvc_url = self.context.checkout.get_settings('VIEW_VC') issue_desc = git_cl.ChangeDescription(pending.description) msg = 'Committed: %s' % pending.revision if viewvc_url: viewvc_url = '%s%s' % (viewvc_url.rstrip('/'), pending.revision) msg = 'Committed: %s' % viewvc_url issue_desc.append_footer(msg) # Update the CQ dashboard. self.context.status.send( pending, { 'verification': 'commit', 'payload': { 'revision': pending.revision, 'output': msg, 'url': viewvc_url } }) # Closes the issue on Rietveld. # TODO(csharp): Retry if exceptions are encountered. try: self.context.rietveld.close_issue(pending.issue) self.context.rietveld.update_description( pending.issue, issue_desc.description) self.context.rietveld.add_comment( pending.issue, 'Change committed as %s' % pending.revision) except (urllib2.HTTPError, urllib2.URLError) as e: # Ignore AppEngine flakiness. logging.warning('Unable to fully close the issue') # And finally remove the issue. If the close_issue() call above failed, # it is possible the dashboard will be confused but it is harmless. try: self.queue.get(pending.issue) except KeyError: logging.error('Internal inconsistency for %d', pending.issue) self.queue.remove(pending.issue) except (checkout.PatchApplicationFailed, patch.UnsupportedPatchFormat) as e: raise base.DiscardPending(pending, str(e)) except subprocess2.CalledProcessError as e: stdout = getattr(e, 'stdout', None) out = 'Failed to apply the patch.' if stdout: out += '\n%s' % stdout raise base.DiscardPending(pending, out) except base.DiscardPending as e: self._discard_pending(e.pending, e.status) except Exception as e: traceback.print_exc() # Swallow every exception in that code and move on. Make sure to send a # stack trace though. errors.send_stack(e)