Repository: aurora
Updated Branches:
  refs/heads/master 9176b6423 -> 1ae77d596


Support in-flight review chains via `depends-on`.

This adds support for following `depends-on` chains of in-flight RBs to
form patch sets ultimately based off master.

Request processing logic is factored up into a helper class that main
drives in a loop over pending RBs.

Reviewed at https://reviews.apache.org/r/41659/


Project: http://git-wip-us.apache.org/repos/asf/aurora/repo
Commit: http://git-wip-us.apache.org/repos/asf/aurora/commit/1ae77d59
Tree: http://git-wip-us.apache.org/repos/asf/aurora/tree/1ae77d59
Diff: http://git-wip-us.apache.org/repos/asf/aurora/diff/1ae77d59

Branch: refs/heads/master
Commit: 1ae77d59654f7a4d5610fe016f6a1ef76f73f993
Parents: 9176b64
Author: John Sirois <john.sir...@gmail.com>
Authored: Thu Dec 24 10:25:51 2015 -0600
Committer: Joshua Cohen <jco...@apache.org>
Committed: Thu Dec 24 10:25:51 2015 -0600

----------------------------------------------------------------------
 build-support/jenkins/review_feedback.py | 250 +++++++++++++++-----------
 1 file changed, 147 insertions(+), 103 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/aurora/blob/1ae77d59/build-support/jenkins/review_feedback.py
----------------------------------------------------------------------
diff --git a/build-support/jenkins/review_feedback.py 
b/build-support/jenkins/review_feedback.py
index ee37742..aa2c836 100755
--- a/build-support/jenkins/review_feedback.py
+++ b/build-support/jenkins/review_feedback.py
@@ -25,6 +25,7 @@ import sys
 import urllib
 import urllib2
 
+
 class ReviewBoard(object):
   def __init__(self, host, user, password):
     self._base_url = 'https://%s' % host if not host.startswith('http') else 
host
@@ -51,68 +52,153 @@ class ReviewBoard(object):
     return json.loads(self.get_resource_data(href, args=args, data=data))
 
 
-# Thrown when the patch from a review diff could not be applied.
-class PatchApplyError(Exception):
-  pass
+class RequestProcessor(object):
+  def __init__(self, server, command, git_clean_excludes=None, tail_lines=20):
+    self._server = server
+    self._command = command
+    self._git_clean_excludes = git_clean_excludes or []
+    self._tail_lines = tail_lines
 
+  class PatchApplyError(Exception):
+    """Indicates the patch from a review diff could not be applied."""
 
-def _apply_patch(patch_data, clean_excludes):
-  subprocess.check_call(['git', 'clean', '-fdx'] + ['--exclude=%s' % e for e 
in clean_excludes])
-  subprocess.check_call(['git', 'reset', '--hard', 'origin/master'])
+  def _apply_patch(self, patch_data, basis=None, reset=True):
+    if reset:
+      subprocess.check_call(
+          ['git', 'clean', '-fdx'] + ['--exclude=%s' % e for e in 
self._git_clean_excludes])
+      subprocess.check_call(['git', 'reset', '--hard', 'origin/master'])
 
-  patch_file = 'diff.patch'
-  with open(patch_file, 'w') as f:
-    f.write(patch_data)
-  try:
-    subprocess.check_call(['git', 'apply', patch_file])
-  except subprocess.CalledProcessError:
-    raise PatchApplyError()
+    sha = subprocess.check_output(['git', 'rev-parse', '--short', 
'HEAD']).strip()
+    patch_file = 'diff.patch'
+    with open(patch_file, 'w') as f:
+      f.write(patch_data)
+    try:
+      subprocess.check_call(['git', 'apply', '-3', patch_file])
+      return sha
+    except subprocess.CalledProcessError:
+      raise self.PatchApplyError(
+          'This patch does not apply cleanly against %s (%s), do you need to 
rebase?'
+          % (basis or 'master', sha))
 
+  def _get_latest_diff_time(self, review_request):
+    diffs = 
self._server.get_resource(review_request['links']['diffs']['href'])['diffs']
+    return diffs[-1]['timestamp']
 
-def _get_latest_diff_time(server, request):
-  diffs = server.get_resource(request['links']['diffs']['href'])['diffs']
-  return diffs[-1]['timestamp']
+  REPLY_REQUEST = '@ReviewBot retry'
 
+  def _get_latest_user_request(self, reviews):
+    reply_requests = [r for r in reviews if self.REPLY_REQUEST.lower() in 
r['body_top'].lower()]
+    if reply_requests:
+      return reply_requests[-1]['timestamp']
 
-REPLY_REQUEST = '@ReviewBot retry'
+  def _needs_reply(self, review_request):
+    print('Inspecting review %d: %s' % (review_request['id'], 
review_request['summary']))
+    reviews_response = 
self._server.get_resource(review_request['links']['reviews']['href'])
+    reviews = reviews_response['reviews']
+    # The default response limit is 25.  When the responses are limited, a 
'next' link will be
+    # included.  When that happens, continue to walk until there are no 
reviews left.
+    while 'next' in reviews_response['links']:
+      print('Fetching next page of reviews.')
+      reviews_response = 
self._server.get_resource(reviews_response['links']['next']['href'])
+      reviews.extend(reviews_response['reviews'])
+    feedback_reviews = [r for r in reviews if r['links']['user']['title'] == 
self._server.user]
+    if feedback_reviews:
+      # Determine whether another round of feedback is necessary.
+      latest_feedback_time = feedback_reviews[-1]['timestamp']
+      latest_request = self._get_latest_user_request(reviews)
+      latest_diff = self._get_latest_diff_time(review_request)
+      print('Latest feedback was given at        %s' % latest_feedback_time)
+      print('Latest build request from a user at %s' % latest_request)
+      print('Latest diff was posted at           %s' % latest_diff)
+      return ((latest_request and (latest_request > latest_feedback_time)) or
+              (latest_diff and (latest_diff > latest_feedback_time)))
+    return True
 
+  def _missing_tests(self, diff):
+    # Get files that were modified by the change, flag if test coverage 
appears lacking.
+    diff_files = 
self._server.get_resource(diff['links']['files']['href'])['files']
+    paths = [f['source_file'] for f in diff_files]
+    return (filter(lambda p: p.startswith('src/main/'), paths) and not
+            filter(lambda p: p.startswith('src/test/'), paths))
 
-def _get_latest_user_request(reviews):
-  reply_requests = [r for r in reviews if REPLY_REQUEST.lower() in 
r['body_top'].lower()]
-  if reply_requests:
-    return reply_requests[-1]['timestamp']
+  def _patch(self, review_request, basis=None, reset=True):
+    diffs = 
self._server.get_resource(review_request['links']['diffs']['href'])['diffs']
+    if not diffs:
+      return None
 
+    dependencies = review_request['depends_on']
+    if len(dependencies) > 1:
+      raise self.PatchApplyError(
+          'Can only test reviews with 1 parent, found %d: %s' %
+          (len(dependencies), ' '.join('RB#%d' % d['id'] for d in 
dependencies)))
+    elif len(dependencies) == 1:
+      parent_request = 
self._server.get_resource(dependencies[0]['href'])['review_request']
+      basis = 'RB#%d' % parent_request['id']
+      parent_status = parent_request['status']
+      if parent_status == 'submitted':
+        print('Skipping application of %s diffs - dependency already submitted 
to master.' % basis)
+      elif parent_status == 'pending':
+        self._patch(parent_request, basis=basis, reset=reset)
+        print('Applied patch for %s dependency.' % basis)
+        reset = False
+      elif parent_status == 'discarded':
+        raise self.PatchApplyError(
+            "RB#%d depends on discarded %s. Please adjust the 'depends-on' 
field and rebase "
+            "as-needed." % (review_request['id'], basis))
+      else:
+        print('WARNING: unexpected RB status %s for %s' % (parent_status, 
basis))
 
-def _needs_reply(server, request):
-  print('Inspecting review %d: %s' % (request['id'], request['summary']))
-  reviews_response = server.get_resource(request['links']['reviews']['href'])
-  reviews = reviews_response['reviews']
-  # The default response limit is 25.  When the responses are limited, a 
'next' link will be
-  # included.  When that happens, continue to walk until there are no reviews 
left.
-  while 'next' in reviews_response['links']:
-    print('Fetching next page of reviews.')
-    reviews_response = 
server.get_resource(reviews_response['links']['next']['href'])
-    reviews.extend(reviews_response['reviews'])
-  feedback_reviews = [r for r in reviews if r['links']['user']['title'] == 
server.user]
-  if feedback_reviews:
-    # Determine whether another round of feedback is necessary.
-    latest_feedback_time = feedback_reviews[-1]['timestamp']
-    latest_request = _get_latest_user_request(reviews)
-    latest_diff = _get_latest_diff_time(server, request)
-    print('Latest feedback was given at        %s' % latest_feedback_time)
-    print('Latest build request from a user at %s' % latest_request)
-    print('Latest diff was posted at           %s' % latest_diff)
-    return ((latest_request and (latest_request > latest_feedback_time))
-            or (latest_diff and (latest_diff > latest_feedback_time)))
-  return True
+    latest_diff = diffs[-1]
+    print('Applying diff %d' % latest_diff['id'])
+    patch_data = self._server.get_resource_data(
+        latest_diff['links']['self']['href'],
+        accept='text/x-patch')
+    sha = self._apply_patch(patch_data, basis=basis, reset=reset)
+    return sha, latest_diff
+
+  def process(self, review_request):
+    if not self._needs_reply(review_request):
+      return
+
+    ship = False
+    try:
+      result = self._patch(review_request)
+      if not result:
+        return
+      sha, latest_diff = result
 
+      print('Running build command.')
+      build_output = 'build_output'
+      # Pipe to a file in case output is large, also tee the output to simplify
+      # debugging.  Since we pipe the output, we must set pipefail to ensure
+      # a failing build command fails the bash pipeline.
+      result = subprocess.call([
+        'bash',
+        '-c',
+        'set -o pipefail; %s 2>&1 | tee %s' % (self._command, build_output)])
+      if result == 0:
+        review_text = 'Master (%s) is green with this patch.\n  %s' % (sha, 
self._command)
+        if self._missing_tests(latest_diff):
+          review_text = '%s\n\nHowever, it appears that it might lack test 
coverage.' % review_text
+        else:
+          ship = True
+      else:
+        build_tail = subprocess.check_output(['tail', '-n', 
str(self._tail_lines), build_output])
+        review_text = (
+          'Master (%s) is red with this patch.\n  %s\n\n%s' % (sha, 
self._command, build_tail))
+    except self.PatchApplyError as e:
+      review_text = str(e)
 
-def _missing_tests(server, diff):
-  # Get files that were modified by the change, flag if test coverage appears 
lacking.
-  diff_files = server.get_resource(diff['links']['files']['href'])['files']
-  paths = [f['source_file'] for f in diff_files]
-  return (filter(lambda f: f.startswith('src/main/'), paths) and not
-          filter(lambda f: f.startswith('src/test/'), paths))
+    review_text = ('%s\n\nI will refresh this build result if you post a 
review containing "%s"'
+                   % (review_text, self.REPLY_REQUEST))
+    print('Replying to review %d:\n%s' % (review_request['id'], review_text))
+    print(self._server.get_resource(
+        review_request['links']['reviews']['href'],
+        data=urllib.urlencode({
+          'body_top': review_text,
+          'public': 'true',
+          'ship_it': 'true' if ship else 'false'
+        })))
 
 
 def main():
@@ -137,10 +223,10 @@ def main():
       default=20,
       help='Number of lines of command output to include in red build 
reviews.')
   parser.add_argument(
-    '--git-clean-exclude',
-    help='Patterns to pass to git-clean --exclude.',
-    nargs='*',
-    default=[])
+      '--git-clean-exclude',
+      help='Patterns to pass to git-clean --exclude.',
+      nargs='*',
+      default=[])
   args = parser.parse_args()
 
   credentials = args.reviewboard_credentials_file.readlines()
@@ -164,58 +250,16 @@ def main():
     'status': 'pending'
   })['review_requests']
   print('Found %d review requests to inspect' % len(requests))
-  for request in requests:
-    if not _needs_reply(server, request):
-      continue
-
-    diffs = server.get_resource(request['links']['diffs']['href'])['diffs']
-    if not diffs:
-      continue
 
-    latest_diff = diffs[-1]
-    print('Applying diff %d' % latest_diff['id'])
-    patch_data = server.get_resource_data(
-        latest_diff['links']['self']['href'],
-        accept='text/x-patch')
-    sha = subprocess.check_output(['git', 'rev-parse', '--short', 
'HEAD']).strip()
-    ship = False
-    try:
-      _apply_patch(patch_data, args.git_clean_exclude)
-      print('Running build command.')
-      build_output = 'build_output'
-      command = args.command
-      # Pipe to a file in case output is large, also tee the output to simplify
-      # debugging.  Since we pipe the output, we must set pipefail to ensure
-      # a failing build command fails the bash pipeline.
-      result = subprocess.call([
-          'bash',
-          '-c',
-          'set -o pipefail; %s 2>&1 | tee %s' % (command, build_output)])
-      if result == 0:
-        review_text = 'Master (%s) is green with this patch.\n  %s' % (sha, 
command)
-        if _missing_tests(server, latest_diff):
-          review_text = '%s\n\nHowever, it appears that it might lack test 
coverage.' % review_text
-        else:
-          ship = True
-      else:
-        build_tail = subprocess.check_output(['tail', '-n', 
str(args.tail_lines), build_output])
-        review_text = (
-            'Master (%s) is red with this patch.\n  %s\n\n%s' % (sha, command, 
build_tail))
-    except PatchApplyError:
-      review_text = (
-          'This patch does not apply cleanly on master (%s), do you need to 
rebase?' % sha)
+  request_processor = RequestProcessor(
+      server,
+      args.command,
+      args.git_clean_exclude,
+      args.tail_lines)
 
-    review_text = ('%s\n\nI will refresh this build result if you post a 
review containing "%s"'
-                   % (review_text, REPLY_REQUEST))
-    print('Replying to review %d:\n%s' % (request['id'], review_text))
-    print(server.get_resource(
-        request['links']['reviews']['href'],
-        data=urllib.urlencode({
-            'body_top': review_text,
-            'public': 'true',
-            'ship_it': 'true' if ship else 'false'
-        })))
+  for review_request in requests:
+    request_processor.process(review_request)
 
 
-if __name__=="__main__":
+if __name__ == "__main__":
   main()

Reply via email to