def _get_mozreview(self, where):
        if not where:
            if 'MOZREVIEW_HOME' in os.environ:
                where = os.environ['MOZREVIEW_HOME']
            else:
                # Check for the path used by start-local-mozreview
                default_path = os.path.abspath(
                    os.path.join(HERE, '..', '..', '..', 'mozreview-test'))
                if os.path.isdir(default_path):
                    where = default_path

        web_image = os.environ.get('DOCKER_BMOWEB_IMAGE')
        hgrb_image = os.environ.get('DOCKER_HGRB_IMAGE')
        ldap_image = os.environ.get('DOCKER_LDAP_IMAGE')
        pulse_image = os.environ.get('DOCKER_PULSE_IMAGE')
        rbweb_image = os.environ.get('DOCKER_RBWEB_IMAGE')
        autolanddb_image = os.environ.get('DOCKER_AUTOLANDDB_IMAGE')
        autoland_image = os.environ.get('DOCKER_AUTOLAND_IMAGE')
        hgweb_image = os.environ.get('DOCKER_HGWEB_IMAGE')
        treestatus_image = os.environ.get('DOCKER_TREESTATUS_IMAGE')

        from vcttesting.mozreview import MozReview
        return MozReview(where, web_image=web_image,
                         hgrb_image=hgrb_image, ldap_image=ldap_image,
                         pulse_image=pulse_image, rbweb_image=rbweb_image,
                         autolanddb_image=autolanddb_image,
                         autoland_image=autoland_image,
                         hgweb_image=hgweb_image,
                         treestatus_image=treestatus_image)
Exemple #2
0
    def __init__(self, context):
        if 'BUGZILLA_URL' in os.environ:
            self.base_url = os.environ['BUGZILLA_URL']
            username = os.environ['BUGZILLA_USERNAME']
            password = os.environ['BUGZILLA_PASSWORD']

            self.b = Bugzilla(self.base_url, username=username, password=password)

        elif 'MOZREVIEW_HOME' in os.environ:
            mr = MozReview(os.environ['MOZREVIEW_HOME'])
            username = os.environ.get('BUGZILLA_USERNAME')
            password = os.environ.get('BUGZILLA_PASSWORD')

            self.b = mr.get_bugzilla(username=username, password=password)

        else:
            raise Exception('Do not know which Bugzilla instance to talk to. '
                    'Set BUGZILLA_URL or MOZREVIEW_HOME environment variables.')
Exemple #3
0
    def setUpClass(cls):
        tmpdir = tempfile.mkdtemp()
        cls._tmpdir = tmpdir

        try:
            mr = MozReview(tmpdir)
        except DockerNotAvailable:
            raise unittest.SkipTest('Docker not available')

        cls.mr = mr

        # If this fails mid-operation, we could have some services running.
        # unittest doesn't call tearDownClass if setUpClass fails. So do it
        # ourselves.
        try:
            start_mozreview(mr)
        except Exception:
            mr.stop()
            shutil.rmtree(tmpdir)
            raise
    def __init__(self, context):
        if 'BUGZILLA_URL' in os.environ:
            self.base_url = os.environ['BUGZILLA_URL']
            username = os.environ['BUGZILLA_USERNAME']
            password = os.environ['BUGZILLA_PASSWORD']

            self.b = Bugzilla(self.base_url,
                              username=username,
                              password=password)

        elif 'MOZREVIEW_HOME' in os.environ:
            mr = MozReview(os.environ['MOZREVIEW_HOME'])
            username = os.environ.get('BUGZILLA_USERNAME')
            password = os.environ.get('BUGZILLA_PASSWORD')

            self.b = mr.get_bugzilla(username=username, password=password)

        else:
            raise Exception(
                'Do not know which Bugzilla instance to talk to. '
                'Set BUGZILLA_URL or MOZREVIEW_HOME environment variables.')
    def setUpClass(cls):
        if 'SKIP_DOCKER_TESTS' in os.environ:
            raise unittest.SkipTest('Skipping tests that require Docker')

        tmpdir = tempfile.mkdtemp()
        cls._tmpdir = tmpdir

        try:
            mr = MozReview(tmpdir)
        except DockerNotAvailable:
            raise unittest.SkipTest('Docker not available')

        cls.mr = mr

        # If this fails mid-operation, we could have some services running.
        # unittest doesn't call tearDownClass if setUpClass fails. So do it
        # ourselves.
        try:
            start_mozreview(mr)
        except Exception:
            mr.stop()
            shutil.rmtree(tmpdir)
            raise
Exemple #6
0
    def __init__(self, context):
        if 'BUGZILLA_URL' in os.environ:
            self.base_url = os.environ['BUGZILLA_URL']
            username = os.environ['BUGZILLA_USERNAME']
            password = os.environ['BUGZILLA_PASSWORD']

            self.b = Bugzilla(self.base_url,
                              username=username,
                              password=password)

        elif 'MOZREVIEW_HOME' in os.environ:
            # Delay import to facilitate module use in limited virtualenvs.
            from vcttesting.mozreview import MozReview

            mr = MozReview(os.environ['MOZREVIEW_HOME'])
            username = os.environ.get('BUGZILLA_USERNAME')
            password = os.environ.get('BUGZILLA_PASSWORD')

            self.b = mr.get_bugzilla(username=username, password=password)

        else:
            raise Exception(
                'Do not know which Bugzilla instance to talk to. '
                'Set BUGZILLA_URL or MOZREVIEW_HOME environment variables.')
Exemple #7
0
 def __init__(self, context):
     from vcttesting.mozreview import MozReview
     if 'MOZREVIEW_HOME' in os.environ:
         self.mr = MozReview(os.environ['MOZREVIEW_HOME'])
     else:
         self.mr = None
Exemple #8
0
class ReviewBoardCommands(object):
    def __init__(self, context):
        from vcttesting.mozreview import MozReview
        if 'MOZREVIEW_HOME' in os.environ:
            self.mr = MozReview(os.environ['MOZREVIEW_HOME'])
        else:
            self.mr = None

    def _get_client(self, username=None, password=None):
        from rbtools.api.client import RBClient
        from rbtools.api.transport.sync import SyncTransport

        class NoCacheTransport(SyncTransport):
            """API transport with disabled caching."""
            def enable_cache(self):
                pass

        # TODO consider moving this to __init__.
        if not self.mr:
            raise Exception('Could not find MozReview cluster instance')

        if username is None or password is None:
            username = os.environ.get('BUGZILLA_USERNAME')
            password = os.environ.get('BUGZILLA_PASSWORD')

        # RBClient is persisting login cookies from call to call
        # in $HOME/.rbtools-cookies. We want to be able to easily switch
        # between users, so we clear that cookie between calls to the
        # server and reauthenticate every time.
        try:
            os.remove(os.path.join(os.environ.get('HOME'), '.rbtools-cookies'))
        except Exception:
            pass

        return RBClient(self.mr.reviewboard_url, username=username,
                        password=password, transport_cls=NoCacheTransport)

    def _get_root(self, username=None, password=None):
        return self._get_client(username=username, password=password).get_root()

    def _get_rb(self, path=None):
        from vcttesting.reviewboard import MozReviewBoard

        if self.mr:
            return self.mr.get_reviewboard()
        elif 'BUGZILLA_HOME' in os.environ and path:
            return MozReviewBoard(None, os.environ['BUGZILLA_URL'],
                pulse_host=os.environ.get('PULSE_HOST'),
                pulse_port=os.environ.get('PULSE_PORT'))
        elif 'REVIEWBOARD_URL' in os.environ:
            return MozReviewBoard(None, None, os.environ['REVIEWBOARD_URL'])
        else:
            raise Exception('Do not know about Bugzilla URL. Cannot talk to '
                            'Review Board. Try running `mozreview start` and '
                            'setting MOZREVIEW_HOME.')

    @Command('dumpreview', category='reviewboard',
        description='Print a representation of a review request.')
    @CommandArgument('rrid', help='Review request id to dump')
    def dumpreview(self, rrid):
        root = self._get_root()
        r = root.get_review_request(review_request_id=rrid)
        print(serialize_review_requests(r))

    @Command('add-reviewer', category='reviewboard',
        description='Add a reviewer to a review request')
    @CommandArgument('rrid', help='Review request id to modify')
    @CommandArgument('--user', action='append',
        help='User from whom to ask for review')
    def add_reviewer(self, rrid, user):
        from rbtools.api.errors import APIError

        root = self._get_root()
        rr = root.get_review_request(review_request_id=rrid)

        people = []
        draft = rr.get_or_create_draft()
        for p in draft.target_people:
            people.append(p.title)

        # Review Board doesn't call into the auth plugin when mapping target
        # people to RB users. So, we perform an API call here to ensure the
        # user is present.
        for u in user:
            if u not in people:
                people.append(u)
                root.get_users(q=u)

        people = ','.join(people)

        draft = rr.get_or_create_draft(target_people=people)
        print('%d people listed on review request' % len(draft.target_people))

    @Command('list-reviewers', category='reviewboard',
        description='List reviewers on a review request')
    @CommandArgument('rrid', help='Review request id for which to list reviewers')
    @CommandArgument('--draft', action='store_true',
            help='List reviewers on the current draft')
    def list_reviewers(self, rrid, draft):
        from rbtools.api.errors import APIError
        root = self._get_root()
        rr = root.get_review_request(review_request_id=rrid)

        people = []
        if draft:
            try:
                for p in rr.get_draft().target_people:
                    people.append(p.title)
            except APIError:
                pass
        else:
            for p in rr.target_people:
                people.append(p.title)

        print(', '.join(sorted(people)))

    @Command('remove-reviewer', category='reviewboard',
        description='Remove a reviewer from a review request')
    @CommandArgument('rrid', help='Review request id to modify')
    @CommandArgument('--user', action='append',
        help='User to remove from review')
    def remove_reviewer(self, rrid, user):
        root = self._get_root()
        rr = root.get_review_request(review_request_id=rrid)

        people = []
        for p in rr.target_people:
            username = p.get().username
            if username not in user:
                people.append(username)

        people = ','.join(people)

        draft = rr.get_or_create_draft(target_people=people)
        print('%d people listed on review request' % len(draft.target_people))

    @Command('publish', category='reviewboard',
        description='Publish a review request')
    @CommandArgument('rrid', help='Review request id to publish')
    def publish(self, rrid):
        from rbtools.api.errors import APIError
        root = self._get_root()
        r = root.get_review_request(review_request_id=rrid)

        try:
            response = r.get_draft().update(public=True)
            # TODO: Dump the response code?
        except APIError as e:
            print('API Error: %s: %s: %s' % (e.http_status, e.error_code,
                e.rsp['err']['msg']))
            return 1

    @Command('get-users', category='reviewboard',
        description='Query the Review Board user list')
    @CommandArgument('q', help='Query string')
    def query_users(self, q=None):
        from rbtools.api.errors import APIError

        root = self._get_root()
        try:
            r = root.get_users(q=q, fullname=True)
        except APIError as e:
            print('API Error: %s: %s: %s' % (e.http_status, e.error_code,
                e.rsp['err']['msg']))
            return 1

        users = []
        for u in r.rsp['users']:
            users.append(dict(
                id=u['id'],
                url=u['url'],
                username=u['username']))

        print(yaml.safe_dump(users, default_flow_style=False).rstrip())

    @Command('create-review', category='reviewboard',
        description='Create a new review on a review request')
    @CommandArgument('rrid', help='Review request to create the review on')
    @CommandArgument('--body-bottom',
            help='Review content below comments')
    @CommandArgument('--body-top',
            help='Review content above comments')
    @CommandArgument('--public', action='store_true',
            help='Whether to make this review public')
    @CommandArgument('--ship-it', action='store_true',
            help='Whether to mark the review "Ship It"')
    def create_review(self, rrid, body_bottom=None, body_top=None, public=False,
            ship_it=False):
        from rbtools.api.errors import APIError
        root = self._get_root()
        reviews = root.get_reviews(review_request_id=rrid)
        # rbtools will convert body_* to str() and insert "None" if we pass
        # an argument.
        args = {'public': public, 'ship_it': ship_it}
        if body_bottom:
            args['body_bottom'] = body_bottom
        if body_top:
            args['body_top'] = body_top

        try:
            r = reviews.create(**args)
        except APIError as e:
            print('API Error: %s: %s: %s' % (e.http_status, e.error_code,
                                             e.rsp['err']['msg']))
            return 1

        print('created review %s' % r.rsp['review']['id'])

    @Command('publish-review', category='reviewboard',
        description='Publish a review')
    @CommandArgument('rrid', help='Review request review is attached to')
    @CommandArgument('rid', help='Review to publish')
    def publish_review(self, rrid, rid):
        from rbtools.api.errors import APIError
        root = self._get_root()
        review = root.get_review(review_request_id=rrid, review_id=rid)

        try:
            review.update(public=True)
        except APIError as e:
            print('API Error: %s: %s: %s' % (e.http_status, e.error_code,
                                             e.rsp['err']['msg']))
            return 1

        print('published review %s' % review.id)

    @Command('create-review-reply', category='reviewboard',
        description='Create a reply to an existing review')
    @CommandArgument('rrid', help='Review request to create reply on')
    @CommandArgument('rid', help='Review to create reply on')
    @CommandArgument('--body-bottom',
        help='Reply content below the comments')
    @CommandArgument('--body-top',
        help='Reply content above the comments')
    @CommandArgument('--public', action='store_true',
        help='Whether to make this reply public')
    @CommandArgument('--text-type', default='plain',
        help='The format of the text')
    def create_review_reply(self, rrid, rid, body_bottom, body_top,
            public, text_type):
        root = self._get_root()
        replies = root.get_replies(review_request_id=rrid, review_id=rid)

        args = {'public': public, 'text_type': text_type}
        if body_bottom:
            args['body_bottom'] = body_bottom
        if body_top:
            args['body_top'] = body_top

        r = replies.create(**args)
        print('created review reply %s' % r.rsp['reply']['id'])

    @Command('create-diff-comment', category='reviewboard',
             description='Create a comment on a diff')
    @CommandArgument('rrid', help='Review request to create comment on')
    @CommandArgument('rid', help='Review to create comment on')
    @CommandArgument('filename', help='File to leave comment on')
    @CommandArgument('first_line', help='Line comment should apply to')
    @CommandArgument('text', help='Text constituting diff comment')
    @CommandArgument('--open-issue', action='store_true',
                     help='Whether to open an issue in this review')
    def create_diff_comment(self, rrid, rid, filename, first_line, text,
                            open_issue=False):
        root = self._get_root()

        diffs = root.get_diffs(review_request_id=rrid)
        diff = diffs[-1]
        files = diff.get_files()

        file_id = None
        for file_diff in files:
            if file_diff.source_file == filename:
                file_id = file_diff.id

        if not file_id:
            print('could not find file in diff: %s' % filename)
            return 1

        reviews = root.get_reviews(review_request_id=rrid)
        review = reviews.create()
        comments = review.get_diff_comments()
        comment = comments.create(filediff_id=file_id, first_line=first_line,
                                  num_lines=1, text=text,
                                  issue_opened=open_issue)
        print('created diff comment %s' % comment.id)

    @Command('update-issue-status', category='reviewboard',
             description='Update issue status on a diff comment.')
    @CommandArgument('rrid', help='Review request for the diff comment review')
    @CommandArgument('rid', help='Review for the diff comment')
    @CommandArgument('cid', help='Diff comment of issue to be updated')
    @CommandArgument('status', help='Desired issue status ("open", "dropped", '
                     'or "resolved")')
    def update_issue_status(self, rrid, rid, cid, status):
        root = self._get_root()

        review = root.get_review(review_request_id=rrid, review_id=rid)
        diff_comment = review.get_diff_comments().get_item(cid)
        diff_comment.update(issue_status=status)
        print('updated issue status on diff comment %s' % cid)

    @Command('closediscarded', category='reviewboard',
        description='Close a review request as discarded.')
    @CommandArgument('rrid', help='Request request to discard')
    def close_discarded(self, rrid):
        root = self._get_root()
        rr = root.get_review_request(review_request_id=rrid)
        rr.update(status='discarded')

    @Command('closesubmitted', category='reviewboard',
        description='Close a review request as submitted.')
    @CommandArgument('rrid', help='Request request to submit')
    def close_submitted(self, rrid):
        root = self._get_root()
        rr = root.get_review_request(review_request_id=rrid)
        rr.update(status='submitted')

    @Command('reopen', category='reviewboard',
        description='Reopen a closed review request')
    @CommandArgument('rrid', help='Review request to reopen')
    def reopen(self, rrid):
        root = self._get_root()
        rr = root.get_review_request(review_request_id=rrid)
        rr.update(status='pending')

    @Command('discard-review-request-draft', category='reviewboard',
        description='Discard (delete) a draft review request.')
    @CommandArgument('rrid', help='Review request whose draft to delete')
    def discard_draft(self, rrid):
        root = self._get_root()
        rr = root.get_review_request(review_request_id=rrid)
        draft = rr.get_draft()

        # Review Board sends an Content-Length 0 response with a JSON content
        # type. rbtools tries to parse this as JSON and raises a ValueError
        # in the process. This is a bug somewhere. Work around it by swallowing
        # the exception.
        try:
            draft.delete()
        except ValueError:
            pass
        print('Discarded draft for review request %s' % rrid)

    @Command('dump-user', category='reviewboard',
        description='Print a representation of a user.')
    @CommandArgument('username', help='Username whose info the print')
    def dump_user(self, username):
        root = self._get_root()
        u = root.get_user(username=username)

        o = {}
        for field in u.iterfields():
            o[field] = getattr(u, field)

        data = {}
        data[u.id] = o

        print(yaml.safe_dump(data, default_flow_style=False).rstrip())

    @Command('dump-user-ldap', category='reviewboard',
        description='Print the ldap username of a Review Board user.')
    @CommandArgument('username', help='Username whose info the print')
    def dump_user_ldap(self, username):
        root = self._get_root(username="******", password="******")
        ext = root.get_extension(
            extension_name='mozreview.extension.MozReviewExtension')
        user = ext.get_ldap_associations().get_item(username).ldap_username

        if user:
            print('ldap username: %s' % user)
        else:
            print('no ldap username associated with %s' % username)

    @Command('associate-ldap-user', category='reviewboard',
        description='Associate an LDAP email address with a user.')
    @CommandArgument('username', help='Username to associate with ldap')
    @CommandArgument('email', help='LDAP email to associate')
    def associate_ldap_user(self, username, email):
        # We use the "mozreview" account which has the special permission
        # to read / associate ldap email addresses.
        root = self._get_root(username="******", password="******")
        ext = root.get_extension(
            extension_name='mozreview.extension.MozReviewExtension')

        association = ext.get_ldap_associations().get_item(username)
        association.update(ldap_username=email)

        print('%s associated with %s' % (email, username))

    @Command('dump-autoland-requests', category='reviewboard',
             description='Dump the table of autoland requests.')
    def dump_autoland_requests(self):
        root = self._get_root()
        ext = root.get_extension(
            extension_name="mozreview.extension.MozReviewExtension")

        requests = ext.get_try_autoland_triggers()
        o = {}
        for request in requests:
            for field in request.iterfields():
                o[field] = getattr(request, field)
            print(yaml.safe_dump(o, default_flow_style=False).rstrip())

    @Command('dump-summary', category='reviewboard',
             description='Return parent and child review-request summary.')
    @CommandArgument('rrid', help='Parent review request id')
    def dump_summary(self, rrid):
        from rbtools.api.errors import APIError
        c = self._get_client()

        try:
            r = c.get_path('/extensions/mozreview.extension.MozReviewExtension'
                           '/summary/%s/' % rrid)
        except APIError as e:
            print('API Error: %s: %s: %s' % (e.http_status, e.error_code,
                e.rsp['err']['msg']))
            return 1

        d = OrderedDict()
        d['parent'] = short_review_request_dict(r['parent'])
        d['children'] = [short_review_request_dict(x) for x in r['children']]

        print(yaml.safe_dump(d, default_flow_style=False).rstrip())

    @Command('dump-summaries-by-bug', category='reviewboard',
             description='Return parent and child review-request summaries '
                         'for a given bug.')
    @CommandArgument('bug', help='Bug id')
    def dump_summaries_by_bug(self, bug):
        from rbtools.api.errors import APIError
        c = self._get_client()

        try:
            r = c.get_path('/extensions/mozreview.extension.MozReviewExtension'
                           '/summary/', bug=bug)
        except APIError as e:
            print('API Error: %s: %s: %s' % (e.http_status, e.error_code,
                                             e.rsp['err']['msg']))
            return 1

        l = []

        for summary in r:
            d = OrderedDict()
            d['parent'] = short_review_request_dict(summary['parent'])
            d['children'] = [short_review_request_dict(x) for x in
                             summary['children']]
            l.append(d)

        print(yaml.safe_dump(l, default_flow_style=False).rstrip())

    @Command('make-admin', category='reviewboard',
        description='Make a user a superuser and staff user')
    @CommandArgument('email', help='Email address of user to modify')
    def make_admin(self, email):
        self._get_rb().make_admin(email)

    @Command('dump-account-profile', category='reviewboard',
         description='Dump the contents of the auth_user table')
    @CommandArgument('username', help='Username whose info the print')
    def dump_account_profile(self, username):
        fields = self._get_rb().get_profile_data(username)
        for k, v in sorted(fields.items()):
            print('%s: %s' % (k, v))

    @Command('convert-draft-rids-to-str', category='reviewboard',
         description='Convert any review ids stored in extra data to strings')
    @CommandArgument('rrid', help='Review request id convert')
    def convert_draft_rids_to_str(self, rrid):
        from rbtools.api.errors import APIError
        import json
        root = self._get_root()
        rr = root.get_review_request(review_request_id=rrid)
        try:
            draft = rr.get_draft()
            d = dict(draft.extra_data.iteritems())

            extra_data = {}
            fields = ['p2rb.commits', 'p2rb.discard_on_publish_rids',
                      'p2rb.unpublished_rids']
            for field in fields:
                if field not in d:
                    continue
                try:
                    value = [[x[0], str(x[1])] for x in json.loads(d[field])]
                except TypeError:
                    value = [str(x) for x in json.loads(d[field])]
                extra_data['extra_data.' + field] = json.dumps(value)
            rr.get_or_create_draft(**extra_data)
        except APIError:
            pass

    @Command('add-repository', category='reviewboard',
             description='Add a repository to the server.')
    @CommandArgument('name', help='Name of repository')
    @CommandArgument('url', help='URL of repository')
    @CommandArgument('--bug-tracker',
                     default='https://bugzilla.mozilla.org/',
                     help='URL for bug tracker')
    def add_repository(self, name, url, bug_tracker=None):
        rb = self._get_rb()
        rid = rb.add_repository(name, url, bugzilla_url=bug_tracker,
                                username=os.environ['BUGZILLA_USERNAME'],
                                password=os.environ['BUGZILLA_PASSWORD'])
        print('Created repository %s' % rid)
 def __init__(self, context):
     from vcttesting.mozreview import MozReview
     if 'MOZREVIEW_HOME' in os.environ:
         self.mr = MozReview(os.environ['MOZREVIEW_HOME'])
     else:
         self.mr = None
class ReviewBoardCommands(object):
    def __init__(self, context):
        from vcttesting.mozreview import MozReview
        if 'MOZREVIEW_HOME' in os.environ:
            self.mr = MozReview(os.environ['MOZREVIEW_HOME'])
        else:
            self.mr = None

    def _get_client(self, username=None, password=None, anonymous=False):
        from rbtools.api.client import RBClient
        from rbtools.api.transport.sync import SyncTransport

        class NoCacheTransport(SyncTransport):
            """API transport with disabled caching."""
            def enable_cache(self):
                pass

        # TODO consider moving this to __init__.
        if not self.mr:
            raise Exception('Could not find MozReview cluster instance')

        if username is None or password is None:
            username = os.environ.get('BUGZILLA_USERNAME')
            password = os.environ.get('BUGZILLA_PASSWORD')

        # RBClient is persisting login cookies from call to call
        # in $HOME/.rbtools-cookies. We want to be able to easily switch
        # between users, so we clear that cookie between calls to the
        # server and reauthenticate every time.
        try:
            os.remove(os.path.join(os.environ.get('HOME'), '.rbtools-cookies'))
        except Exception:
            pass

        if anonymous:
            return RBClient(self.mr.reviewboard_url,
                            transport_cls=NoCacheTransport)

        return RBClient(self.mr.reviewboard_url, username=username,
                        password=password, transport_cls=NoCacheTransport)

    def _get_root(self, username=None, password=None, anonymous=False):
        return self._get_client(username=username, password=password,
                                anonymous=anonymous).get_root()

    def _get_rb(self, path=None):
        from vcttesting.reviewboard import MozReviewBoard

        if self.mr:
            return self.mr.get_reviewboard()
        elif 'BUGZILLA_HOME' in os.environ and path:
            return MozReviewBoard(None, os.environ['BUGZILLA_URL'],
                pulse_host=os.environ.get('PULSE_HOST'),
                pulse_port=os.environ.get('PULSE_PORT'))
        elif 'REVIEWBOARD_URL' in os.environ:
            return MozReviewBoard(None, None, os.environ['REVIEWBOARD_URL'])
        else:
            raise Exception('Do not know about Bugzilla URL. Cannot talk to '
                            'Review Board. Try running `mozreview start` and '
                            'setting MOZREVIEW_HOME.')

    @Command('dumpreview', category='reviewboard',
        description='Print a representation of a review request.')
    @CommandArgument('rrid', help='Review request id to dump')
    def dumpreview(self, rrid):
        client = self._get_client()
        root = client.get_root()
        r = root.get_review_request(review_request_id=rrid)
        print(serialize_review_requests(client, r))

    @Command('dump-raw-diff', category='reviewboard',
             description='Dump the raw content of a diff from the server')
    @CommandArgument('rrid', help='Review request id of diffs to dump')
    def dump_raw_diff(self, rrid):
        from rbtools.api.errors import APIError

        client = self._get_client()
        root = client.get_root()
        rr = root.get_review_request(review_request_id=rrid)

        # mach wraps sys.stdout with a transparent UTF-8 writer. This
        # will interfere with our printing of raw data. So bypass it.
        stdout = os.fdopen(sys.stdout.fileno(), 'w')

        for diff in rr.get_diffs():
            stdout.write(b'ID: %d\n' % diff.id)
            stdout.write(diff.get_patch().data)
            stdout.write(b'\n')

        try:
            draft = rr.get_draft()
            for diff in draft.get_draft_diffs():
                stdout.write(b'ID: %d (draft)\n' % diff.id)
                stdout.write(diff.get_patch().data)
                stdout.write(b'\n')
        except APIError:
            pass

        stdout.close()

    @Command('add-reviewer', category='reviewboard',
        description='Add a reviewer to a review request')
    @CommandArgument('rrid', help='Review request id to modify')
    @CommandArgument('--user', action='append',
        help='User from whom to ask for review')
    def add_reviewer(self, rrid, user):
        from rbtools.api.errors import APIError

        root = self._get_root()
        rr = root.get_review_request(review_request_id=rrid)

        people = []
        draft = rr.get_or_create_draft()
        for p in draft.target_people:
            people.append(p.title)

        # Review Board doesn't call into the auth plugin when mapping target
        # people to RB users. So, we perform an API call here to ensure the
        # user is present.
        for u in user:
            if u not in people:
                people.append(u)
                root.get_users(q=u)

        people = ','.join(people)

        draft = rr.get_or_create_draft(target_people=people)
        print('%d people listed on review request' % len(draft.target_people))

    @Command('list-reviewers', category='reviewboard',
        description='List reviewers on a review request')
    @CommandArgument('rrid', help='Review request id for which to list reviewers')
    @CommandArgument('--draft', action='store_true',
            help='List reviewers on the current draft')
    def list_reviewers(self, rrid, draft):
        from rbtools.api.errors import APIError
        root = self._get_root()
        rr = root.get_review_request(review_request_id=rrid)

        people = []
        if draft:
            try:
                for p in rr.get_draft().target_people:
                    people.append(p.title)
            except APIError:
                pass
        else:
            for p in rr.target_people:
                people.append(p.title)

        print(', '.join(sorted(people)))

    @Command('remove-reviewer', category='reviewboard',
        description='Remove a reviewer from a review request')
    @CommandArgument('rrid', help='Review request id to modify')
    @CommandArgument('--user', action='append',
        help='User to remove from review')
    def remove_reviewer(self, rrid, user):
        root = self._get_root()
        rr = root.get_review_request(review_request_id=rrid)

        people = []
        for p in rr.target_people:
            username = p.get().username
            if username not in user:
                people.append(username)

        people = ','.join(people)

        draft = rr.get_or_create_draft(target_people=people)
        print('%d people listed on review request' % len(draft.target_people))

    @Command('publish', category='reviewboard',
        description='Publish a review request')
    @CommandArgument('rrid', help='Review request id to publish')
    def publish(self, rrid):
        from rbtools.api.errors import APIError
        root = self._get_root()
        r = root.get_review_request(review_request_id=rrid)

        try:
            response = r.get_draft().update(public=True)
            # TODO: Dump the response code?
        except APIError as e:
            print('API Error: %s: %s: %s' % (e.http_status, e.error_code,
                e.rsp['err']['msg']))
            return 1

    @Command(
        'upload-diff',
        category='reviewboard',
        description='Upload a diff, read from stdin, to a review request')
    @CommandArgument(
        'rrid',
        help='Review request id to upload diff to')
    @CommandArgument(
        '--base-commit',
        help='Base commit id to apply diff to')
    def upload_diff(self, rrid, base_commit=None):
        from rbtools.api.errors import APIError
        root = self._get_root()
        diffs = root.get_diffs(only_fields='', review_request_id=rrid)

        try:
            diffs.upload_diff(
                sys.stdin.read(),
                base_commit_id=base_commit)
        except APIError as e:
            print('API Error: %s: %s: %s' % (
                e.http_status,
                e.error_code,
                e.rsp['err']['msg']))
            return 1

    @Command('get-users', category='reviewboard',
        description='Query the Review Board user list')
    @CommandArgument('q', nargs='?', help='Query string')
    def query_users(self, q=None):
        from rbtools.api.errors import APIError

        root = self._get_root()
        try:
            args = {}
            # q=None will pass literal 'None' to the HTTP query!
            if q:
                args['q'] = q
            r = root.get_users(fullname=True, **args)
        except APIError as e:
            print('API Error: %s: %s: %s' % (e.http_status, e.error_code,
                e.rsp['err']['msg']))
            return 1

        users = []
        for u in r.rsp['users']:
            users.append(dict(
                id=u['id'],
                url=u['url'],
                username=u['username']))

        print(yaml.safe_dump(users, default_flow_style=False).rstrip())

    @Command('create-review', category='reviewboard',
        description='Create a new review on a review request')
    @CommandArgument('rrid', help='Review request to create the review on')
    @CommandArgument('--body-bottom',
            help='Review content below comments')
    @CommandArgument('--body-top',
            help='Review content above comments')
    @CommandArgument('--public', action='store_true',
            help='Whether to make this review public')
    @CommandArgument('--review-flag', action='store',
            help='Bugzilla-style review flag to set')
    def create_review(self, rrid, body_bottom=None, body_top=None, public=False,
            review_flag=None):
        from rbtools.api.errors import APIError
        root = self._get_root()
        reviews = root.get_reviews(review_request_id=rrid)
        # rbtools will convert body_* to str() and insert "None" if we pass
        # an argument.
        args = {'public': public}

        if review_flag:
            args['ship_it'] = (review_flag == 'r+')
            args['extra_data.p2rb.review_flag'] = review_flag

        if body_bottom:
            args['body_bottom'] = body_bottom
        if body_top:
            args['body_top'] = body_top

        try:
            r = reviews.create(**args)
        except APIError as e:
            print('API Error: %s: %s: %s' % (e.http_status, e.error_code,
                                             e.rsp['err']['msg']))
            return 1

        print('created review %s' % r.rsp['review']['id'])

    @Command('publish-review', category='reviewboard',
        description='Publish a review')
    @CommandArgument('rrid', help='Review request review is attached to')
    @CommandArgument('rid', help='Review to publish')
    def publish_review(self, rrid, rid):
        from rbtools.api.errors import APIError
        root = self._get_root()
        review = root.get_review(review_request_id=rrid, review_id=rid)

        try:
            review.update(public=True)
        except APIError as e:
            print('API Error: %s: %s: %s' % (e.http_status, e.error_code,
                                             e.rsp['err']['msg']))
            return 1

        print('published review %s' % review.id)

    @Command('create-review-reply', category='reviewboard',
        description='Create a reply to an existing review')
    @CommandArgument('rrid', help='Review request to create reply on')
    @CommandArgument('rid', help='Review to create reply on')
    @CommandArgument('--body-bottom',
        help='Reply content below the comments')
    @CommandArgument('--body-top',
        help='Reply content above the comments')
    @CommandArgument('--public', action='store_true',
        help='Whether to make this reply public')
    @CommandArgument('--text-type', default='plain',
        help='The format of the text')
    def create_review_reply(self, rrid, rid, body_bottom, body_top,
            public, text_type):
        root = self._get_root()
        replies = root.get_replies(review_request_id=rrid, review_id=rid)

        args = {'public': public, 'text_type': text_type}
        if body_bottom:
            args['body_bottom'] = body_bottom
        if body_top:
            args['body_top'] = body_top

        r = replies.create(**args)
        print('created review reply %s' % r.rsp['reply']['id'])

    @Command('create-diff-comment', category='reviewboard',
             description='Create a comment on a diff')
    @CommandArgument('rrid', help='Review request to create comment on')
    @CommandArgument('rid', help='Review to create comment on')
    @CommandArgument('filename', help='File to leave comment on')
    @CommandArgument('first_line', help='Line comment should apply to')
    @CommandArgument('text', help='Text constituting diff comment')
    @CommandArgument('--open-issue', action='store_true',
                     help='Whether to open an issue in this review')
    def create_diff_comment(self, rrid, rid, filename, first_line, text,
                            open_issue=False):
        root = self._get_root()

        diffs = root.get_diffs(review_request_id=rrid)
        diff = diffs[-1]
        files = diff.get_files()

        file_id = None
        for file_diff in files:
            if file_diff.source_file == filename:
                file_id = file_diff.id

        if not file_id:
            print('could not find file in diff: %s' % filename)
            return 1

        reviews = root.get_reviews(review_request_id=rrid)
        review = reviews.create()
        comments = review.get_diff_comments()
        comment = comments.create(filediff_id=file_id, first_line=first_line,
                                  num_lines=1, text=text,
                                  issue_opened=open_issue)
        print('created diff comment %s' % comment.id)

    @Command('update-issue-status', category='reviewboard',
             description='Update issue status on a diff comment.')
    @CommandArgument('rrid', help='Review request for the diff comment review')
    @CommandArgument('rid', help='Review for the diff comment')
    @CommandArgument('cid', help='Diff comment of issue to be updated')
    @CommandArgument('status', help='Desired issue status ("open", "dropped", '
                     'or "resolved")')
    def update_issue_status(self, rrid, rid, cid, status):
        root = self._get_root()

        review = root.get_review(review_request_id=rrid, review_id=rid)
        diff_comment = review.get_diff_comments().get_item(cid)
        diff_comment.update(issue_status=status)
        print('updated issue status on diff comment %s' % cid)

    @Command('closediscarded', category='reviewboard',
        description='Close a review request as discarded.')
    @CommandArgument('rrid', help='Request request to discard')
    def close_discarded(self, rrid):
        root = self._get_root()
        rr = root.get_review_request(review_request_id=rrid)
        rr.update(status='discarded')

    @Command('closesubmitted', category='reviewboard',
        description='Close a review request as submitted.')
    @CommandArgument('rrid', help='Request request to submit')
    def close_submitted(self, rrid):
        root = self._get_root()
        rr = root.get_review_request(review_request_id=rrid)
        rr.update(status='submitted')

    @Command('reopen', category='reviewboard',
        description='Reopen a closed review request')
    @CommandArgument('rrid', help='Review request to reopen')
    def reopen(self, rrid):
        root = self._get_root()
        rr = root.get_review_request(review_request_id=rrid)
        rr.update(status='pending')

    @Command('discard-review-request-draft', category='reviewboard',
        description='Discard (delete) a draft review request.')
    @CommandArgument('rrid', help='Review request whose draft to delete')
    def discard_draft(self, rrid):
        root = self._get_root()
        rr = root.get_review_request(review_request_id=rrid)
        draft = rr.get_draft()

        # Review Board sends an Content-Length 0 response with a JSON content
        # type. rbtools tries to parse this as JSON and raises a ValueError
        # in the process. This is a bug somewhere. Work around it by swallowing
        # the exception.
        try:
            draft.delete()
        except ValueError:
            pass
        print('Discarded draft for review request %s' % rrid)

    @Command('dump-user', category='reviewboard',
        description='Print a representation of a user.')
    @CommandArgument('username', help='Username whose info the print')
    def dump_user(self, username):
        root = self._get_root()
        u = root.get_user(username=username)

        o = {}
        for field in u.iterfields():
            o[field] = getattr(u, field)

        data = {}
        data[u.id] = o

        print(yaml.safe_dump(data, default_flow_style=False).rstrip())

    @Command('dump-user-ldap', category='reviewboard',
        description='Print the ldap username of a Review Board user.')
    @CommandArgument('username', help='Username whose info the print')
    def dump_user_ldap(self, username):
        root = self._get_root(username="******", password="******")
        ext = root.get_extension(
            extension_name='mozreview.extension.MozReviewExtension')
        user = ext.get_ldap_associations().get_item(username).ldap_username

        if user:
            print('ldap username: %s' % user)
        else:
            print('no ldap username associated with %s' % username)

    @Command('associate-ldap-user', category='reviewboard',
             description='Associate an LDAP email address with a user.')
    @CommandArgument('username', help='Username to associate with ldap')
    @CommandArgument('email',
                     help='LDAP email to associate, or "none" to clear')
    @CommandArgument('--request-username',
                     default='mozreview',
                     help='Username to make request with')
    @CommandArgument('--request-password',
                     default='mrpassword',
                     help='Password to make request with')
    @CommandArgument('--anonymous',
                     action='store_true',
                     default=False,
                     help='Make the request anonymously')
    def associate_ldap_user(self, username, email, request_username,
                            request_password, anonymous):
        from rbtools.api.errors import APIError

        # We use the "mozreview" account by default which has the special
        # permission to read / associate ldap email addresses.
        root = self._get_root(username=request_username,
                              password=request_password,
                              anonymous=anonymous)
        ext = root.get_extension(
            extension_name='mozreview.extension.MozReviewExtension')

        if email == "none":
            email = None

        try:
            association = ext.get_ldap_associations().get_item(username)
            association.update(ldap_username=email)
        except APIError as e:
            print('API Error: %s: %s: %s' % (e.http_status, e.error_code,
                  e.rsp['err']['msg']))
            return 1

        if email is None:
            print('association for %s cleared' % email)
        else:
            print('%s associated with %s' % (email, username))

    @Command('associate-employee-ldap', category='reviewboard',
             description='Associate LDAP for Mozilla employees.')
    @CommandArgument('--request-username',
                     default='mozreview',
                     help='Username to make request with')
    @CommandArgument('--request-password',
                     default='mrpassword',
                     help='Password to make request with')
    @CommandArgument('--email',
                     default='',
                     help='LDAP email to associate')
    def associate_employee_ldap(self, email,
                                request_username, request_password):
        from rbtools.api.errors import APIError

        # We use the "mozreview" account by default which has the special
        # permission to read / associate ldap email addresses.
        root = self._get_root(username=request_username,
                              password=request_password)
        ext = root.get_extension(
            extension_name='mozreview.extension.MozReviewExtension')

        try:
            assoc = ext.get_employee_ldap_associations()
            r = assoc.create(email=email)

            if email:
                if r['errors']:
                    print('LDAP association failed.')
                elif r['updated']:
                    print('%s associated with %s'
                          % (email, r['ldap_username'][0]))
                else:
                    print('%s already associated with %s'
                          % (email, r['ldap_username'][0]))

            else:
                print('updated: %s\nskipped: %s\nerrors: %s' %
                      (r['updated'], r['skipped'], r['errors']))

        except APIError as e:
            if e.rsp:
                print('API Error: %s: %s: %s' % (e.http_status, e.error_code,
                      e.rsp['err']['msg']))
            else:
                print('API Error: %s' % e.http_status)

    @Command('dump-autoland-requests', category='reviewboard',
             description='Dump the table of autoland requests.')
    def dump_autoland_requests(self):
        root = self._get_root()
        ext = root.get_extension(
            extension_name="mozreview.extension.MozReviewExtension")

        requests = ext.get_try_autoland_triggers()
        o = {}
        for request in requests:
            for field in request.iterfields():
                o[field] = getattr(request, field)
            print(yaml.safe_dump(o, default_flow_style=False).rstrip())

    @Command('dump-summary', category='reviewboard',
             description='Return parent and child review-request summary.')
    @CommandArgument('rrid', help='Parent review request id')
    def dump_summary(self, rrid):
        from rbtools.api.errors import APIError
        c = self._get_client()

        try:
            r = c.get_path('/extensions/mozreview.extension.MozReviewExtension'
                           '/summary/%s/' % rrid)
        except APIError as e:
            print('API Error: %s: %s: %s' % (e.http_status, e.error_code,
                  e.rsp['err']['msg']))
            return 1

        d = OrderedDict()
        d['parent'] = short_review_request_dict(r['parent'])
        d['children'] = [short_review_request_dict(x) for x in r['children']]

        print(yaml.safe_dump(d, default_flow_style=False).rstrip())

    @Command('dump-summaries-by-bug', category='reviewboard',
             description='Return parent and child review-request summaries '
                         'for a given bug.')
    @CommandArgument('bug', help='Bug id')
    def dump_summaries_by_bug(self, bug):
        from rbtools.api.errors import APIError
        c = self._get_client()

        try:
            r = c.get_path('/extensions/mozreview.extension.MozReviewExtension'
                           '/summary/', bug=bug)
        except APIError as e:
            print('API Error: %s: %s: %s' % (e.http_status, e.error_code,
                                             e.rsp['err']['msg']))
            return 1

        l = []

        for summary in r:
            d = OrderedDict()
            d['parent'] = short_review_request_dict(summary['parent'])
            d['children'] = [short_review_request_dict(x) for x in
                             summary['children']]
            l.append(d)

        print(yaml.safe_dump(l, default_flow_style=False).rstrip())

    @Command('make-admin', category='reviewboard',
        description='Make a user a superuser and staff user')
    @CommandArgument('email', help='Email address of user to modify')
    def make_admin(self, email):
        self._get_rb().make_admin(email)

    @Command('dump-account-profile', category='reviewboard',
         description='Dump the contents of the auth_user table')
    @CommandArgument('username', help='Username whose info the print')
    def dump_account_profile(self, username):
        fields = self._get_rb().get_profile_data(username)
        for k, v in sorted(fields.items()):
            print('%s: %s' % (k, v))

    @Command('convert-draft-rids-to-str', category='reviewboard',
         description='Convert any review ids stored in extra data to strings')
    @CommandArgument('rrid', help='Review request id convert')
    def convert_draft_rids_to_str(self, rrid):
        from rbtools.api.errors import APIError
        import json
        root = self._get_root()
        rr = root.get_review_request(review_request_id=rrid)
        try:
            draft = rr.get_draft()
            d = dict(draft.extra_data.iteritems())

            extra_data = {}
            fields = ['p2rb.commits', 'p2rb.discard_on_publish_rids',
                      'p2rb.unpublished_rids']
            for field in fields:
                if field not in d:
                    continue
                try:
                    value = [[x[0], str(x[1])] for x in json.loads(d[field])]
                except TypeError:
                    value = [str(x) for x in json.loads(d[field])]
                extra_data['extra_data.' + field] = json.dumps(value)
            rr.get_or_create_draft(**extra_data)
        except APIError:
            pass

    @Command('add-repository', category='reviewboard',
             description='Add a repository to the server.')
    @CommandArgument('name', help='Name of repository')
    @CommandArgument('url', help='URL of repository')
    @CommandArgument('--bug-tracker',
                     default='https://bugzilla.mozilla.org/',
                     help='URL for bug tracker')
    def add_repository(self, name, url, bug_tracker=None):
        rb = self._get_rb()
        rid = rb.add_repository(name, url, bugzilla_url=bug_tracker,
                                username=os.environ['BUGZILLA_USERNAME'],
                                password=os.environ['BUGZILLA_PASSWORD'])
        print('Created repository %s' % rid)

    @Command('dump-rewrite-commit', category='reviewboard',
             description='Return the rewritten commit summaries')
    @CommandArgument('rrid', help='Parent review request id')
    def dump_rewrite_commit(self, rrid):
        from rbtools.api.errors import APIError
        c = self._get_client()

        try:
            r = c.get_path('/extensions/mozreview.extension.MozReviewExtension'
                           '/commit_rewrite/%s/' % rrid)
        except APIError as e:
            print('API Error: %s: %s: %s' % (e.http_status, e.error_code,
                                             e.rsp['err']['msg']))
            return 1

        result = OrderedDict()
        result['commits'] = []
        for commit in r:
            d = {}
            d['summary'] = _serialize_text(commit['summary'])
            d['id'] = commit['id']
            d['commit'] = commit['commit']
            for k in ('id', 'commit'):
                if k in commit:
                    d[k] = commit[k]
            d['reviewers'] = list(commit['reviewers'])
            result['commits'].append(d)

        print(yaml.safe_dump(result, default_flow_style=False).rstrip())

    @Command('modify-reviewers', category='reviewboard',
             description='Update reviewers on a child review request')
    @CommandArgument('parent_rrid', help='Parent review request id')
    @CommandArgument('child_rrid', help='Child review request id')
    @CommandArgument('reviewers', help='Comma delimited list of reviewers')
    def modify_reviewers(self, parent_rrid, child_rrid, reviewers):
        from rbtools.api.errors import APIError
        import json
        c = self._get_client()
        try:
            r = c.get_path('/extensions/mozreview.extension.MozReviewExtension'
                           '/modify-reviewers/')
            reviewers = {child_rrid: reviewers.split(',')}
            r.create(parent_request_id=parent_rrid,
                     reviewers=json.dumps(reviewers))
        except APIError as e:
            print('API Error: %s: %s: %s' % (e.http_status, e.error_code,
                                             e.rsp['err']['msg']))
            return 1

    @Command('verify-reviewers', category='reviewboard',
             description='Update reviewers on a child review request')
    @CommandArgument('reviewers', help='Comma delimited list of reviewers')
    def verify_reviewers(self, reviewers):
        from rbtools.api.errors import APIError
        c = self._get_client()
        try:
            r = c.get_path('/extensions/mozreview.extension.MozReviewExtension'
                           '/verify-reviewers/')
            r.create(reviewers=reviewers)
        except APIError as e:
            print('API Error: %s: %s: %s' % (e.http_status, e.error_code,
                                             e.rsp['err']['msg']))
            return 1

    @Command('ensure-drafts', category='reviewboard',
             description='Create drafts on all review request children')
    @CommandArgument('parent_rrid', help='Parent review request id')
    def ensure_drafts(self, parent_rrid):
        from rbtools.api.errors import APIError
        c = self._get_client()
        try:
            r = c.get_path('/extensions/mozreview.extension.MozReviewExtension'
                           '/ensure-drafts/')
            r.create(parent_request_id=parent_rrid)
        except APIError as e:
            print('API Error: %s: %s: %s' % (e.http_status, e.error_code,
                                             e.rsp['err']['msg']))
            return 1