| #!/usr/bin/env python |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| # |
| """Utility to show outstanding reviews for the project |
| |
| Usage: list-missing-shipits [--server=RB_SERVER_URL] |
| """ |
| from __future__ import print_function |
| |
| import datetime |
| import getpass |
| import json |
| import os |
| import subprocess |
| import sys |
| |
| from optparse import OptionParser |
| |
| |
| class Review(object): |
| def __init__(self, raw_review): |
| self._review = raw_review |
| |
| @property |
| def shipit(self): |
| return self._review['ship_it'] |
| |
| @property |
| def timestamp(self): |
| return self._review['timestamp'] |
| |
| @property |
| def reviewer(self): |
| return self._review['links']['user']['title'] |
| |
| |
| class Diff(object): |
| def __init__(self, raw_diff): |
| self._diff = raw_diff |
| |
| @property |
| def timestamp(self): |
| return self._diff['timestamp'] |
| |
| |
| class ReviewRequest(object): |
| SUMMARY_TRUNCATE = 20 |
| |
| def __init__(self, raw_request): |
| self._request = raw_request |
| |
| @property |
| def submitter(self): |
| return self._request['links']['submitter']['title'] |
| |
| @property |
| def ships_required(self): |
| return [p['title'] for p in self._request['target_people']] |
| |
| @property |
| def id(self): |
| return self._request['id'] |
| |
| @property |
| def summary(self): |
| full = self._request['summary'] |
| return ((full[:(self.SUMMARY_TRUNCATE - 3)] + '...') |
| if len(full) > self.SUMMARY_TRUNCATE else full) |
| |
| @property |
| def reviews_href(self): |
| return self._request['links']['reviews']['href'] |
| |
| @property |
| def diffs_href(self): |
| return self._request['links']['diffs']['href'] |
| |
| |
| class Reviewboard(object): |
| def __init__(self, url=None): |
| self._path = os.path.realpath(os.path.join(os.path.dirname(sys.argv[0]), '..', '..', 'rbt')) |
| self._url = url |
| |
| def _get(self, resource, params={}): |
| base_cmd = [self._path, 'api-get'] |
| if self._url is not None: |
| base_cmd += ['--server=%s' % self._url] |
| return json.loads(subprocess.Popen( |
| base_cmd + [resource] + ['--%s=%s' % (k, v) for k, v in params.items()], |
| stdout=subprocess.PIPE).communicate()[0]) |
| |
| def get_server_url(self): |
| return self._get('info')['info']['site']['url'] |
| |
| def get_review_requests(self, params): |
| return [ReviewRequest(r) for r in self._get('review-requests', params)['review_requests']] |
| |
| def get_reviews(self, review_request): |
| return [Review(r) for r in self._get(review_request.reviews_href)['reviews']] |
| |
| def get_diffs(self, review_request): |
| return [Diff(d) for d in self._get(review_request.diffs_href)['diffs']] |
| |
| |
| def main(): |
| parser = OptionParser() |
| parser.add_option('--server', |
| dest='server', |
| help='ReviewBoard server', |
| default=None) |
| (options, args) = parser.parse_args() |
| |
| api = Reviewboard(options.server) |
| |
| server_url = api.get_server_url() |
| def request_url(id): |
| return '%sr/%s' % (server_url, id) |
| |
| pending_requests = api.get_review_requests({ |
| 'to-groups': 'Aurora', |
| 'status': 'pending', |
| }) |
| |
| def to_row(request): |
| reviews = api.get_reviews(request) |
| diffs = api.get_diffs(request) |
| ships_required = set(request.ships_required) |
| for event in sorted(reviews + diffs, key=lambda x: x.timestamp, reverse=True): |
| if isinstance(event, Review): |
| if event.shipit: |
| # A 'ship-it' review leaves remaining reviewers on the hook. |
| ships_required.discard(event.reviewer) |
| continue |
| elif event.reviewer in ships_required: |
| waiting_for = 'awaiting updated diff' |
| break |
| else: |
| ships = [r.reviewer for r in reviews if r.shipit] |
| needed = set(request.ships_required) - set(ships) |
| if not request.ships_required: |
| waiting_for = 'no reviewers specified' |
| elif not needed: |
| waiting_for = 'ready to submit' |
| else: |
| waiting_for = 'need review from %s' % ', '.join(needed) |
| break |
| |
| return (str(event.timestamp), |
| request_url(request.id), |
| request.submitter.ljust(10), |
| request.summary.ljust(ReviewRequest.SUMMARY_TRUNCATE), |
| waiting_for) |
| |
| table = [to_row(r) for r in pending_requests] |
| sorted_table = sorted(table, key=lambda row: row[0]) |
| print('\n'.join('\t'.join(row) for row in sorted_table)) |
| |
| print() |
| |
| recently_submitted_days = 1 |
| updated_from = ((datetime.datetime.utcnow() - datetime.timedelta(days=1)) |
| .replace(microsecond=0).isoformat()) |
| recently_submitted = api.get_review_requests({ |
| 'to-groups': 'Aurora', |
| 'status': 'submitted', |
| 'last-updated-from': updated_from |
| }) |
| print('Recently submitted: %s' % len(recently_submitted)) |
| for request in recently_submitted: |
| print('\t'.join((request_url(request.id), request.submitter, request.summary))) |
| |
| |
| if __name__ == '__main__': |
| main() |