def check(self, ctx):
        if self.is_uplift:
            return True

        # Ignore merge changesets
        if len(ctx.parents()) > 1:
            return True

        # Ignore backouts
        if is_backout(ctx.description()):
            return True

        # Ignore changes that don't touch sync-messages.ini
        ipc_files = [f for f in ctx.files()
                     if f == 'ipc/ipdl/sync-messages.ini']
        if not ipc_files:
            return True

        # Allow patches authored by peers
        if self._is_peer_email(util.email(ctx.user())):
            return True

        # Allow if reviewed by any peer
        requal = list(parse_requal_reviewers(ctx.description()))
        if any(self._is_peer_nick(nick) for nick in requal):
            return True

        # Reject
        print_banner(self.ui, 'error', MISSING_REVIEW % short(ctx.node()))
        return False
    def check(self, ctx):
        if any(f.endswith('.ftl') for f in ctx.files()):
            requal = parse_requal_reviewers(ctx.description())
            reviewers = [nick for (name, nick) in FTL_DRIVERS]
            if any(nick in reviewers for nick in requal):
                return True

            print_banner(self.ui, 'error', FTL_COMMIT_FOUND)
            return False
        return True
    def check(self, ctx):
        if self.is_uplift:
            return True

        # Ignore merge changesets
        if len(ctx.parents()) > 1:
            return True

        # Ignore backouts
        if is_backout(ctx.description()):
            return True

        # Ignore changes that don't touch .webidl files
        webidl_files = [f for f in ctx.files() if f.endswith('.webidl')]
        if not webidl_files:
            return True

        # Allow patches authored by peers
        if is_peer_email(util.email(ctx.user())):
            return True

        # Categorise files
        file_counts = collections.Counter()
        review_required_files = []
        for f in webidl_files:
            file_counts['total'] += 1
            if f.startswith(CHROME_WEBIDL_ROOT):
                file_counts['chrome'] += 1
            elif f.startswith(SERVO_ROOT):
                file_counts['servo'] += 1
            else:
                review_required_files.append(f)

        # Allow chrome-only and servo-only changes
        if file_counts['chrome'] + file_counts['servo'] == file_counts[
                'total']:
            if file_counts['chrome']:
                print_notice(self.ui, CHROME_ONLY)
            if file_counts['servo']:
                print_notice(self.ui, SERVO_ONLY)
            return True

        # Allow if reviewed by any peer
        requal = list(parse_requal_reviewers(ctx.description()))
        if any(is_peer_nick(nick) for nick in requal):
            return True

        # Reject
        print_banner(
            self.ui, 'error', MISSING_REVIEW %
            (short(ctx.node()), '\n'.join(review_required_files)))
        return False
    def check(self, ctx):
        if len(ctx.parents()) > 1:
            # Skip merge changesets
            return True

        if is_backout(ctx.description()):
            # Ignore backouts
            return True

        if any(f.endswith('.ftl') for f in ctx.files()):
            requal = [
                r.lower() for r in parse_requal_reviewers(ctx.description())
            ]
            reviewers = [nick for (name, nick) in FTL_DRIVERS]
            if any(nick in reviewers for nick in requal):
                return True

            print_banner(self.ui, 'error', FTL_COMMIT_FOUND)
            return False
        return True
    def test_requal_reviewers(self):

        # first with r? reviewer request syntax
        self.assertEqual(list(parse_requal_reviewers('Bug 1 - some stuff; r?romulus')), [])
        self.assertEqual(list(parse_requal_reviewers('Bug 1 - More stuff; r?romulus, r?remus')), [])
        self.assertEqual(list(parse_requal_reviewers('Bug 1 - More stuff; r?romulus,r?remus')), [])
        self.assertEqual(list(parse_requal_reviewers('Bug 1 - More stuff; r?romulus, remus')), [])
        self.assertEqual(list(parse_requal_reviewers('Bug 1 - More stuff; r?romulus,remus')), [])
        self.assertEqual(list(parse_requal_reviewers('Bug 1 - More stuff; (r?romulus)')), [])
        self.assertEqual(list(parse_requal_reviewers('Bug 1 - More stuff; (r?romulus,remus)')),[])
        self.assertEqual(list(parse_requal_reviewers('Bug 1 - More stuff; [r?romulus]')), [])
        self.assertEqual(list(parse_requal_reviewers('Bug 1 - More stuff; [r?remus, r?romulus]')), [])
        self.assertEqual(list(parse_requal_reviewers('Bug 1 - More stuff; r?romulus, a=test-only')), [])

        # now with r= review granted syntax
        self.assertEqual(list(parse_requal_reviewers('Bug 1 - some stuff; r=romulus')), ['romulus'])
        self.assertEqual(list(parse_requal_reviewers('Bug 1 - More stuff; r=romulus, r=remus')), ['romulus', 'remus'])
        self.assertEqual(list(parse_requal_reviewers('Bug 1 - More stuff; r=romulus,r=remus')), ['romulus', 'remus'])
        self.assertEqual(list(parse_requal_reviewers('Bug 1 - More stuff; r=romulus, remus')), ['romulus', 'remus'])
        self.assertEqual(list(parse_requal_reviewers('Bug 1 - More stuff; r=romulus,remus')), ['romulus', 'remus'])
        self.assertEqual(list(parse_requal_reviewers('Bug 1 - More stuff; (r=romulus)')),['romulus'])
        self.assertEqual(list(parse_requal_reviewers('Bug 1 - More stuff; (r=romulus,remus)')), ['romulus', 'remus'])
        self.assertEqual(list(parse_requal_reviewers('Bug 1 - More stuff; [r=romulus]')), ['romulus'])
        self.assertEqual(list(parse_requal_reviewers('Bug 1 - More stuff; [r=remus, r=romulus]')), ['remus', 'romulus'])
        self.assertEqual(list(parse_requal_reviewers('Bug 1 - More stuff; r=romulus, a=test-only')), ['romulus'])

        # oddball real-world examples
        self.assertEqual(list(parse_requal_reviewers(
            'Bug 1094764 - Implement AudioContext.suspend and friends.  r=roc,ehsan\n'
            '- Relevant spec text:\n'
            '- http://webaudio.github.io/web-audio-api/#widl-AudioContext-suspend-Promise\n'
            '- http://webaudio.github.io/web-audio-api/#widl-AudioContext-resume-Promise\n')),
            ['roc', 'ehsan'])

        self.assertEqual(list(parse_requal_reviewers(
            'Bug 380783 - nsStringAPI.h: no equivalent of IsVoid (tell if '
            'string is null), patch by Mook <*****@*****.**>, '
            'r=bsmedberg/dbaron, sr=dbaron, a1.9=bz')),
            ['bsmedberg', 'dbaron'])

        self.assertEqual(list(parse_requal_reviewers(
             'Bumping gaia.json for 2 gaia revision(s) a=gaia-bump\n'
             '\n'
             'https://hg.mozilla.org/integration/gaia-central/rev/2b738dae9970\n'
             'Author: Francisco Jordano <*****@*****.**>\n'
             'Desc: Merge pull request #30407 from arcturus/fix-contacts-test\n'
             'Fixing form test for date fields r=me\n')),
             [])

        self.assertEqual(list(parse_requal_reviewers(
            'Bug 1024110 - Change Aurora\'s default profile behavior to use channel-specific profiles. r=bsmedberg f=gavin,markh')),
            ['bsmedberg'])
    def test_requal_reviewers(self):
        # empty
        self.assertEqual(list(parse_requal_reviewers('')), [])

        # first with r? reviewer request syntax
        self.assertEqual(
            list(parse_requal_reviewers('Bug 1 - some stuff; r?romulus')), [])
        self.assertEqual(
            list(
                parse_requal_reviewers(
                    'Bug 1 - More stuff; r?romulus, r?remus')), [])
        self.assertEqual(
            list(
                parse_requal_reviewers(
                    'Bug 1 - More stuff; r?romulus,r?remus')), [])
        self.assertEqual(
            list(parse_requal_reviewers(
                'Bug 1 - More stuff; r?romulus, remus')), [])
        self.assertEqual(
            list(
                parse_requal_reviewers('Bug 1 - More stuff; r?romulus,remus')),
            [])
        self.assertEqual(
            list(parse_requal_reviewers('Bug 1 - More stuff; (r?romulus)')),
            [])
        self.assertEqual(
            list(
                parse_requal_reviewers(
                    'Bug 1 - More stuff; (r?romulus,remus)')), [])
        self.assertEqual(
            list(parse_requal_reviewers('Bug 1 - More stuff; [r?romulus]')),
            [])
        self.assertEqual(
            list(
                parse_requal_reviewers(
                    'Bug 1 - More stuff; [r?remus, r?romulus]')), [])
        self.assertEqual(
            list(
                parse_requal_reviewers(
                    'Bug 1 - More stuff; r?romulus, a=test-only')), [])

        # now with r= review granted syntax
        self.assertEqual(
            list(parse_requal_reviewers('Bug 1 - some stuff; r=romulus')),
            ['romulus'])
        self.assertEqual(
            list(
                parse_requal_reviewers(
                    'Bug 1 - More stuff; r=romulus, r=remus')),
            ['romulus', 'remus'])
        self.assertEqual(
            list(
                parse_requal_reviewers(
                    'Bug 1 - More stuff; r=romulus,r=remus')),
            ['romulus', 'remus'])
        self.assertEqual(
            list(parse_requal_reviewers(
                'Bug 1 - More stuff; r=romulus, remus')), ['romulus', 'remus'])
        self.assertEqual(
            list(
                parse_requal_reviewers('Bug 1 - More stuff; r=romulus,remus')),
            ['romulus', 'remus'])
        self.assertEqual(
            list(parse_requal_reviewers('Bug 1 - More stuff; (r=romulus)')),
            ['romulus'])
        self.assertEqual(
            list(
                parse_requal_reviewers(
                    'Bug 1 - More stuff; (r=romulus,remus)')),
            ['romulus', 'remus'])
        self.assertEqual(
            list(parse_requal_reviewers('Bug 1 - More stuff; [r=romulus]')),
            ['romulus'])
        self.assertEqual(
            list(
                parse_requal_reviewers(
                    'Bug 1 - More stuff; [r=remus, r=romulus]')),
            ['remus', 'romulus'])
        self.assertEqual(
            list(
                parse_requal_reviewers(
                    'Bug 1 - More stuff; r=romulus, a=test-only')),
            ['romulus'])

        # bare r?
        self.assertEqual(
            list(parse_requal_reviewers('Bug 123 - Blah blah; r?')), [])
        self.assertEqual(
            list(
                parse_requal_reviewers(
                    'Bug 1313324 - Cover the screensharing UI with browser chrome test, r='
                )), [])

        # oddball real-world examples
        self.assertEqual(
            list(
                parse_requal_reviewers(
                    'Bug 1094764 - Implement AudioContext.suspend and friends.  r=roc,ehsan\n'
                    '- Relevant spec text:\n'
                    '- http://webaudio.github.io/web-audio-api/#widl-AudioContext-suspend-Promise\n'
                    '- http://webaudio.github.io/web-audio-api/#widl-AudioContext-resume-Promise\n'
                )), ['roc', 'ehsan'])

        self.assertEqual(
            list(
                parse_requal_reviewers(
                    'Bug 380783 - nsStringAPI.h: no equivalent of IsVoid (tell if '
                    'string is null), patch by Mook <*****@*****.**>, '
                    'r=bsmedberg/dbaron, sr=dbaron, a1.9=bz')),
            ['bsmedberg', 'dbaron'])

        self.assertEqual(
            list(
                parse_requal_reviewers(
                    'Bumping gaia.json for 2 gaia revision(s) a=gaia-bump\n'
                    '\n'
                    'https://hg.mozilla.org/integration/gaia-central/rev/2b738dae9970\n'
                    'Author: Francisco Jordano <*****@*****.**>\n'
                    'Desc: Merge pull request #30407 from arcturus/fix-contacts-test\n'
                    'Fixing form test for date fields r=me\n')), [])

        self.assertEqual(
            list(
                parse_requal_reviewers(
                    'Bug 1024110 - Change Aurora\'s default profile behavior to use channel-specific profiles. r=bsmedberg f=gavin,markh'
                )), ['bsmedberg'])
Example #7
0
def reviewboard(repo, proto, args=None):
    proto.redirect()

    o = parsepayload(proto, args)
    if isinstance(o, ServerError):
        return formatresponse(str(o))

    bzusername = o['bzusername']
    bzapikey = o['bzapikey']

    identifier, nodes, precursors = parseidentifier(o)
    if not identifier:
        return ['error %s' % _('no review identifier in request')]

    diffopts = mdiff.diffopts(context=8, showfunc=True, git=True)

    commits = {
        'individual': [],
        'squashed': {}
    }

    # We do multiple passes over the changesets requested for review because
    # some operations could be slow or may involve queries to external
    # resources. We want to run the fast checks first so we don't waste
    # resources before finding the error. The drawback here is the client
    # will not see the full set of errors. We may revisit this decision
    # later.

    for node in nodes:
        ctx = repo[node]
        # Reviewing merge commits doesn't make much sense and only makes
        # situations more complicated. So disallow the practice.
        if len(ctx.parents()) > 1:
            msg = 'cannot review merge commits (%s)' % short(ctx.node())
            return formatresponse('error %s' % msg)

    # Invalid or confidental bugs will raise errors in the Review Board
    # interface later. Fail fast to minimize wasted time and resources.
    try:
        reviewid = ReviewID(identifier)
    except util.Abort as e:
        return formatresponse('error %s' % e)

    # We use xmlrpc here because the Bugsy REST client doesn't currently handle
    # errors in responses.

    # We don't use available Bugzilla credentials because that's the
    # easiest way to test for confidential bugs. If/when we support posting
    # reviews to confidential bugs, we'll need to change this.
    xmlrpc_url = repo.ui.config('bugzilla', 'url').rstrip('/') + '/xmlrpc.cgi'
    proxy = xmlrpclib.ServerProxy(xmlrpc_url)
    try:
        proxy.Bug.get({'ids': [reviewid.bug]})
    except xmlrpclib.Fault as f:
        if f.faultCode == 101:
            return formatresponse('error bug %s does not exist; '
                'please change the review id (%s)' % (reviewid.bug,
                    reviewid.full))
        elif f.faultCode == 102:
            return formatresponse('error bug %s could not be accessed '
                '(we do not currently allow posting of reviews to '
                'confidential bugs)' % reviewid.bug)

        return formatresponse('error server error verifying bug %s exists; '
            'please retry or report a bug' % reviewid.bug)

    # Find the first public node in the ancestry of this series. This is
    # used by MozReview to query the upstream repo for additional context.
    first_public_ancestor = None
    for node in repo[nodes[0]].ancestors():
        ctx = repo[node]
        if ctx.phase() == phases.public:
            first_public_ancestor = ctx.hex()
            break
    commits['squashed']['first_public_ancestor'] = first_public_ancestor

    # Note patch.diff() appears to accept anything that can be fed into
    # repo[]. However, it blindly does a hex() on the argument as opposed
    # to the changectx, so we need to pass in the binary node.
    base_ctx = repo[nodes[0]].p1()
    base_parent_node = base_ctx.node()
    for i, node in enumerate(nodes):
        ctx = repo[node]
        p1 = ctx.p1().node()
        diff = None
        parent_diff = None

        diff = ''.join(patch.diff(repo, node1=p1, node2=ctx.node(), opts=diffopts)) + '\n'

        if i:
            base_commit_id = nodes[i-1]
        else:
            base_commit_id = base_ctx.hex()

        summary = encoding.fromlocal(ctx.description().splitlines()[0])
        commits['individual'].append({
            'id': node,
            'precursors': precursors.get(node, []),
            'message': encoding.fromlocal(ctx.description()),
            'diff': diff,
            'bug': str(reviewid.bug),
            'base_commit_id': base_commit_id,
            'first_public_ancestor': first_public_ancestor,
            'reviewers': list(commitparser.parse_rquestion_reviewers(summary)),
            'requal_reviewers': list(commitparser.parse_requal_reviewers(summary))
        })

    commits['squashed']['diff'] = ''.join(patch.diff(repo, node1=base_parent_node,
        node2=repo[nodes[-1]].node(), opts=diffopts)) + '\n'
    commits['squashed']['base_commit_id'] = base_ctx.hex()

    rburl = repo.ui.config('reviewboard', 'url', None).rstrip('/')
    repoid = repo.ui.configint('reviewboard', 'repoid', None)
    privleged_rb_username = repo.ui.config('reviewboard', 'username', None)
    privleged_rb_password = repo.ui.config('reviewboard', 'password', None)

    # We support pushing via HTTP and SSH. REMOTE_USER will be set via HTTP.
    # USER via SSH. But USER is a common variable and could also sneak into
    # the HTTP environment.
    #
    # REMOTE_USER values come from Bugzilla. USER values come from LDAP.
    # There is a potential privilege escalation vulnerability if someone
    # obtains a Bugzilla account overlapping with a LDAP user having
    # special privileges. So, we explicitly don't perform an LDAP lookup
    # if REMOTE_USER is present because we could be crossing the user
    # stores.
    ldap_username = os.environ.get('USER')
    remote_user = repo.ui.environ.get('REMOTE_USER', os.environ.get('REMOTE_USER'))

    if ldap_username and not remote_user:
        associate_ldap_username(rburl, ldap_username, privleged_rb_username,
                                privleged_rb_password, username=bzusername,
                                apikey=bzapikey)

    lines = [
        'rburl %s' % rburl,
        'reviewid %s' % identifier,
    ]

    try:
        parentrid, commitmap, reviews = post_reviews(rburl, repoid, identifier,
                                                     commits, lines,
                                                     username=bzusername,
                                                     apikey=bzapikey)
        lines.extend([
            'parentreview %s' % parentrid,
            'reviewdata %s status %s' % (
                parentrid,
                urllib.quote(reviews[parentrid]['status'].encode('utf-8'))),
            'reviewdata %s public %s' % (
                parentrid,
                reviews[parentrid]['public']),
        ])

        for node, rid in commitmap.items():
            rd = reviews[rid]
            lines.append('csetreview %s %s' % (node, rid))
            lines.append('reviewdata %s status %s' % (rid,
                urllib.quote(rd['status'].encode('utf-8'))))
            lines.append('reviewdata %s public %s' % (rid, rd['public']))

            if rd['reviewers']:
                parts = [urllib.quote(r.encode('utf-8'))
                         for r in rd['reviewers']]
                lines.append('reviewdata %s reviewers %s' %
                             (rid, ','.join(parts)))

    except AuthorizationError as e:
        lines.append('error %s' % str(e))
    except BadRequestError as e:
        lines.append('error %s' % str(e))

    res = formatresponse(*lines)
    return res
def _processpushreview(repo, req, ldap_username):
    """Handle a request to turn changesets into review requests.

    ``ldap_username`` is the LDAP username to associate with the MozReview
    account whose credentials are passed as part of the request. We implicitly
    trust the passed LDAP username has been authenticated to belong to the
    MozReview account.
    """
    bzusername = req.get('bzusername')
    bzapikey = req.get('bzapikey')

    if not bzusername or not bzapikey:
        return errorresponse('Bugzilla API keys not configured; see '
            'https://mozilla-version-control-tools.readthedocs.io/en/latest/mozreview/install.html#bugzilla-credentials '
            'for instructions on how to configure your client')

    identifier = req['identifier']
    nodes = []
    precursors = {}
    for cset in req['changesets']:
        node = cset['node']
        nodes.append(node)
        if 'precursors' in cset:
            precursors[node] = cset['precursors']

    diffopts = mdiff.diffopts(context=8, showfunc=True, git=True)

    commits = {
        'individual': [],
        'squashed': {},
        'obsolescence': req.get('obsolescence', False),
    }

    # We do multiple passes over the changesets requested for review because
    # some operations could be slow or may involve queries to external
    # resources. We want to run the fast checks first so we don't waste
    # resources before finding the error. The drawback here is the client
    # will not see the full set of errors. We may revisit this decision
    # later.

    for node in nodes:
        ctx = repo[node]
        # Reviewing merge commits doesn't make much sense and only makes
        # situations more complicated. So disallow the practice.
        if len(ctx.parents()) > 1:
            msg = 'cannot review merge commits (%s)' % short(ctx.node())
            return errorresponse(msg)

    # Invalid or confidental bugs will raise errors in the Review Board
    # interface later. Fail fast to minimize wasted time and resources.
    try:
        reviewid = ReviewID(identifier)
    except error.Abort as e:
        return errorresponse(str(e))

    # We use xmlrpc here because the Bugsy REST client doesn't currently handle
    # errors in responses.

    # We don't use available Bugzilla credentials because that's the
    # easiest way to test for confidential bugs. If/when we support posting
    # reviews to confidential bugs, we'll need to change this.
    xmlrpc_url = repo.ui.config('bugzilla', 'url').rstrip('/') + '/xmlrpc.cgi'
    proxy = xmlrpclib.ServerProxy(xmlrpc_url)
    try:
        proxy.Bug.get({'ids': [reviewid.bug]})
    except xmlrpclib.Fault as f:
        if f.faultCode == 101:
            return errorresponse('bug %s does not exist; '
                'please change the review id (%s)' % (reviewid.bug,
                    reviewid.full))
        elif f.faultCode == 102:
            return errorresponse('bug %s could not be accessed '
                '(we do not currently allow posting of reviews to '
                'confidential bugs)' % reviewid.bug)

        return errorresponse('server error verifying bug %s exists; '
            'please retry or report a bug' % reviewid.bug)

    # Find the first public node in the ancestry of this series. This is
    # used by MozReview to query the upstream repo for additional context.
    first_public_ancestor = None
    for node in repo[nodes[0]].ancestors():
        ctx = repo[node]
        if ctx.phase() == phases.public:
            first_public_ancestor = ctx.hex()
            break
    commits['squashed']['first_public_ancestor'] = first_public_ancestor

    # Note patch.diff() appears to accept anything that can be fed into
    # repo[]. However, it blindly does a hex() on the argument as opposed
    # to the changectx, so we need to pass in the binary node.
    base_ctx = repo[nodes[0]].p1()
    base_parent_node = base_ctx.node()
    for i, node in enumerate(nodes):
        ctx = repo[node]
        p1 = ctx.p1().node()

        diff = ''.join(patch.diff(repo, node1=p1, node2=ctx.node(), opts=diffopts)) + '\n'

        if i:
            base_commit_id = nodes[i-1]
        else:
            base_commit_id = base_ctx.hex()

        summary = encoding.fromlocal(ctx.description().splitlines()[0])
        if req.get('deduce-reviewers', True):
            reviewers = list(commitparser.parse_rquestion_reviewers(summary))
            requal_reviewers = list(commitparser.parse_requal_reviewers(summary))
        else:
            reviewers = []
            requal_reviewers = []
        commits['individual'].append({
            'id': node,
            'author': encoding.fromlocal(ctx.user()),
            'precursors': precursors.get(node, []),
            'message': encoding.fromlocal(ctx.description()),
            # Diffs are arbitrary byte sequences. json.dump() will try to
            # interpret str as UTF-8, which could fail. Instead of trying
            # to coerce the str to a unicode or use ensure_ascii=False (which
            # is a giant pain), just base64 encode the diff in the JSON.
            'diff_b64': diff.encode('base64'),
            'bug': str(reviewid.bug),
            'base_commit_id': base_commit_id,
            'first_public_ancestor': first_public_ancestor,
            'reviewers': reviewers,
            'requal_reviewers': requal_reviewers
        })

    squashed_diff = b''.join(patch.diff(repo,
                                        node1=base_parent_node,
                                        node2=repo[nodes[-1]].node(),
                                        opts=diffopts)) + '\n'

    commits['squashed']['diff_b64'] = squashed_diff.encode('base64')
    commits['squashed']['base_commit_id'] = base_ctx.hex()

    rburl = repo.ui.config('reviewboard', 'url', None).rstrip('/')
    repoid = repo.ui.configint('reviewboard', 'repoid', None)
    privileged_rb_username = repo.ui.config('reviewboard', 'username', None)
    privileged_rb_password = repo.ui.config('reviewboard', 'password', None)

    if ldap_username:
        associate_ldap_username(rburl, ldap_username, privileged_rb_username,
                                privileged_rb_password, username=bzusername,
                                apikey=bzapikey)

    res = {
        'rburl': rburl,
        'reviewid': identifier,
        'reviewrequests': {},
        'display': [],
    }

    try:
        parentrid, commitmap, reviews, warnings = \
            post_reviews(rburl, repoid, identifier, commits,
                         privileged_rb_username, privileged_rb_password,
                         username=bzusername, apikey=bzapikey)

        res['display'].extend(warnings)
        res['parentrrid'] = parentrid
        res['reviewrequests'][parentrid] = {
            'status': reviews[parentrid]['status'],
            'public': reviews[parentrid]['public'],
        }

        for node, rid in commitmap.items():
            rd = reviews[rid]
            res['reviewrequests'][rid] = {
                'node': node,
                'status': rd['status'],
                'public': rd['public'],
            }

            if rd['reviewers']:
                res['reviewrequests'][rid]['reviewers'] = list(rd['reviewers'])

    except AuthorizationError as e:
        return errorresponse(str(e))
    except BadRequestError as e:
        return errorresponse(str(e))

    return res
def doreview(repo, ui, remote, nodes):
    """Do the work of submitting a review to a remote repo.

    :remote is a peerrepository.
    :nodes is a list of nodes to review.
    """
    assert nodes
    assert 'pushreview' in getreviewcaps(remote)

    bzauth = getbugzillaauth(ui)
    if not bzauth:
        ui.warn(_('Bugzilla credentials not available. Not submitting review.\n'))
        return

    identifier = None

    # The review identifier can come from a number of places. In order of
    # priority:
    # 1. --reviewid argument passed to push command
    # 2. The active bookmark
    # 3. The active branch (if it isn't default)
    # 4. A bug number extracted from commit messages

    if repo.reviewid:
        identifier = repo.reviewid

    # TODO The server currently requires a bug number for the identifier.
    # Pull bookmark and branch names in once allowed.
    #elif repo._bookmarkcurrent:
    #    identifier = repo._bookmarkcurrent
    #elif repo.dirstate.branch() != 'default':
    #    identifier = repo.dirstate.branch()

    if not identifier:
        for node in nodes:
            ctx = repo[node]
            bugs = parse_bugs(ctx.description())
            if bugs:
                identifier = 'bz://%s' % bugs[0]
                break

    identifier = ReviewID(identifier)

    if not identifier:
        ui.write(_('Unable to determine review identifier. Review '
            'identifiers are extracted from commit messages automatically. '
            'Try to begin one of your commit messages with "Bug XXXXXX -"\n'))
        return

    # Append irc nick to review identifier.
    # This is an ugly workaround to a limitation in ReviewBoard. RB doesn't
    # really support changing the owner of a review. It is doable, but no
    # history is stored and this leads to faulty attribution. More details
    # in bug 1034188.
    if not identifier.user:
        ircnick = ui.config('mozilla', 'ircnick', None)
        identifier.user = ircnick

    if hasattr(repo, 'mq'):
        for patch in repo.mq.applied:
            if patch.node in nodes:
                ui.warn(_('(You are using mq to develop patches. For the best '
                    'code review experience, use bookmark-based development '
                    'with changeset evolution. Read more at '
                    'http://mozilla-version-control-tools.readthedocs.org/en/latest/mozreview-user.html)\n'))
                break

    lines = commonrequestlines(ui, bzauth)
    lines.append('reviewidentifier %s' % urllib.quote(identifier.full))

    reviews = repo.reviews
    oldparentid = reviews.findparentreview(identifier=identifier.full)

    # Include obsolescence data so server can make intelligent decisions.
    obsstore = repo.obsstore
    for node in nodes:
        lines.append('csetreview %s' % hex(node))
        precursors = [hex(n) for n in obsolete.allprecursors(obsstore, [node])]
        lines.append('precursors %s %s' % (hex(node), ' '.join(precursors)))

    ui.write(_('submitting %d changesets for review\n') % len(nodes))

    res = remote._call('pushreview', data='\n'.join(lines))
    lines = getpayload(res)

    newparentid = None
    nodereviews = {}
    reviewdata = {}

    for line in lines:
        t, d = line.split(' ', 1)

        if t == 'display':
            ui.write('%s\n' % d)
        elif t == 'error':
            raise util.Abort(d)
        elif t == 'parentreview':
            newparentid = d
            reviews.addparentreview(identifier.full, newparentid)
            reviewdata[newparentid] = {}
        elif t == 'csetreview':
            node, rid = d.split(' ', 1)
            node = bin(node)
            reviewdata[rid] = {}
            nodereviews[node] = rid
        elif t == 'reviewdata':
            rid, field, value = d.split(' ', 2)
            reviewdata[rid][field] = decodepossiblelistvalue(value)
        elif t == 'rburl':
            reviews.baseurl = d

    reviews.remoteurl = remote.url()

    for node, rid in nodereviews.items():
        reviews.addnodereview(node, rid, newparentid)

    reviews.write()
    for rid, data in reviewdata.iteritems():
        reviews.savereviewrequest(rid, data)

    havedraft = False

    ui.write('\n')
    for node in nodes:
        rid = nodereviews[node]
        ctx = repo[node]
        # Bug 1065024 use cmdutil.show_changeset() here.
        ui.write('changeset:  %s:%s\n' % (ctx.rev(), ctx.hex()[0:12]))
        ui.write('summary:    %s\n' % ctx.description().splitlines()[0])
        # We want to encourage people to use r? when asking for a review rather
        # than r=.
        if list(parse_requal_reviewers(ctx.description())):
            ui.warn(_('(It appears you are using r= to specify reviewers for a'
                ' patch under review. Please use r? to avoid ambiguity as to'
                ' whether or not review has been granted.)\n'))
        ui.write('review:     %s' % reviews.reviewurl(rid))
        if reviewdata[rid].get('public') == 'False':
            havedraft = True
            ui.write(' (draft)')
        ui.write('\n\n')

    ui.write(_('review id:  %s\n') % identifier.full)
    ui.write(_('review url: %s') % reviews.parentreviewurl(identifier.full))
    if reviewdata[newparentid].get('public', None) == 'False':
        havedraft = True
        ui.write(' (draft)')
    ui.write('\n')

    # Warn people that they have not assigned reviewers for at least some
    # of their commits.
    for node in nodes:
        rd = reviewdata[nodereviews[node]]
        if not rd.get('reviewers', None):
            ui.status(_('(review requests lack reviewers; visit review url '
                        'to assign reviewers)\n'))
            break

    # Make it clear to the user that they need to take action in order for
    # others to see this review series.
    if havedraft:
        # At some point we may want an yes/no/prompt option for autopublish
        # but for safety reasons we only allow no/prompt for now.
        if ui.configbool('reviewboard', 'autopublish', True):
            ui.write('\n')
            publish = ui.promptchoice(
                _('publish these review requests now (Yn)? $$ &Yes $$ &No'))
            if publish == 0:
                publishreviewrequests(ui, remote, bzauth, [newparentid])
            else:
                ui.status(_('(visit review url to publish these review '
                            'requests so others can see them)\n'))
        else:
            ui.status(_('(visit review url to publish these review requests '
                        'so others can see them)\n'))
Example #10
0
def _processpushreview(repo, req, ldap_username):
    """Handle a request to turn changesets into review requests.

    ``ldap_username`` is the LDAP username to associate with the MozReview
    account whose credentials are passed as part of the request. We implicitly
    trust the passed LDAP username has been authenticated to belong to the
    MozReview account.
    """
    bzusername = req.get('bzusername')
    bzapikey = req.get('bzapikey')

    if not bzusername or not bzapikey:
        return errorresponse(
            'Bugzilla API keys not configured; see '
            'https://mozilla-version-control-tools.readthedocs.io/en/latest/mozreview/install.html#obtaining-accounts-credentials-and-privileges '
            'for instructions on how to configure your client')

    identifier = req['identifier']
    nodes = []
    precursors = {}
    for cset in req['changesets']:
        node = cset['node']
        nodes.append(node)
        if 'precursors' in cset:
            precursors[node] = cset['precursors']

    diffopts = mdiff.diffopts(context=8, showfunc=True, git=True)

    commits = {
        'individual': [],
        'squashed': {},
        'obsolescence': req.get('obsolescence', False),
    }

    # We do multiple passes over the changesets requested for review because
    # some operations could be slow or may involve queries to external
    # resources. We want to run the fast checks first so we don't waste
    # resources before finding the error. The drawback here is the client
    # will not see the full set of errors. We may revisit this decision
    # later.

    for node in nodes:
        ctx = repo[node]
        # Reviewing merge commits doesn't make much sense and only makes
        # situations more complicated. So disallow the practice.
        if len(ctx.parents()) > 1:
            msg = 'cannot review merge commits (%s)' % short(ctx.node())
            return errorresponse(msg)

    # Invalid or confidental bugs will raise errors in the Review Board
    # interface later. Fail fast to minimize wasted time and resources.
    try:
        reviewid = ReviewID(identifier)
    except error.Abort as e:
        return errorresponse(str(e))

    # We use xmlrpc here because the Bugsy REST client doesn't currently handle
    # errors in responses.

    # We don't use available Bugzilla credentials because that's the
    # easiest way to test for confidential bugs. If/when we support posting
    # reviews to confidential bugs, we'll need to change this.
    xmlrpc_url = repo.ui.config('bugzilla', 'url').rstrip('/') + '/xmlrpc.cgi'
    proxy = xmlrpclib.ServerProxy(xmlrpc_url)
    try:
        proxy.Bug.get({'ids': [reviewid.bug]})
    except xmlrpclib.Fault as f:
        if f.faultCode == 101:
            return errorresponse('bug %s does not exist; '
                                 'please change the review id (%s)' %
                                 (reviewid.bug, reviewid.full))
        elif f.faultCode == 102:
            return errorresponse(
                'bug %s could not be accessed '
                '(we do not currently allow posting of reviews to '
                'confidential bugs)' % reviewid.bug)

        return errorresponse('server error verifying bug %s exists; '
                             'please retry or report a bug' % reviewid.bug)

    # Find the first public node in the ancestry of this series. This is
    # used by MozReview to query the upstream repo for additional context.
    first_public_ancestor = None
    for node in repo[nodes[0]].ancestors():
        ctx = repo[node]
        if ctx.phase() == phases.public:
            first_public_ancestor = ctx.hex()
            break
    commits['squashed']['first_public_ancestor'] = first_public_ancestor

    # Note patch.diff() appears to accept anything that can be fed into
    # repo[]. However, it blindly does a hex() on the argument as opposed
    # to the changectx, so we need to pass in the binary node.
    base_ctx = repo[nodes[0]].p1()
    base_parent_node = base_ctx.node()
    for i, node in enumerate(nodes):
        ctx = repo[node]
        p1 = ctx.p1().node()

        diff = ''.join(
            patch.diff(repo, node1=p1, node2=ctx.node(), opts=diffopts)) + '\n'

        if i:
            base_commit_id = nodes[i - 1]
        else:
            base_commit_id = base_ctx.hex()

        summary = encoding.fromlocal(ctx.description().splitlines()[0])
        if req.get('deduce-reviewers', True):
            reviewers = list(commitparser.parse_rquestion_reviewers(summary))
            requal_reviewers = list(
                commitparser.parse_requal_reviewers(summary))
        else:
            reviewers = []
            requal_reviewers = []
        commits['individual'].append({
            'id':
            node,
            'author':
            encoding.fromlocal(ctx.user()),
            'precursors':
            precursors.get(node, []),
            'message':
            encoding.fromlocal(ctx.description()),
            # Diffs are arbitrary byte sequences. json.dump() will try to
            # interpret str as UTF-8, which could fail. Instead of trying
            # to coerce the str to a unicode or use ensure_ascii=False (which
            # is a giant pain), just base64 encode the diff in the JSON.
            'diff_b64':
            diff.encode('base64'),
            'bug':
            str(reviewid.bug),
            'base_commit_id':
            base_commit_id,
            'first_public_ancestor':
            first_public_ancestor,
            'reviewers':
            reviewers,
            'requal_reviewers':
            requal_reviewers
        })

    squashed_diff = b''.join(
        patch.diff(repo,
                   node1=base_parent_node,
                   node2=repo[nodes[-1]].node(),
                   opts=diffopts)) + '\n'

    commits['squashed']['diff_b64'] = squashed_diff.encode('base64')
    commits['squashed']['base_commit_id'] = base_ctx.hex()

    rburl = repo.ui.config('reviewboard', 'url', None).rstrip('/')
    repoid = repo.ui.configint('reviewboard', 'repoid', None)
    privileged_rb_username = repo.ui.config('reviewboard', 'username', None)
    privileged_rb_password = repo.ui.config('reviewboard', 'password', None)

    if ldap_username:
        associate_ldap_username(rburl,
                                ldap_username,
                                privileged_rb_username,
                                privileged_rb_password,
                                username=bzusername,
                                apikey=bzapikey)

    res = {
        'rburl': rburl,
        'reviewid': identifier,
        'reviewrequests': {},
        'display': [],
    }

    try:
        parentrid, commitmap, reviews, warnings = \
            post_reviews(rburl, repoid, identifier, commits,
                         privileged_rb_username, privileged_rb_password,
                         username=bzusername, apikey=bzapikey)

        res['display'].extend(warnings)
        res['parentrrid'] = parentrid
        res['reviewrequests'][parentrid] = {
            'status': reviews[parentrid]['status'],
            'public': reviews[parentrid]['public'],
        }

        for node, rid in commitmap.items():
            rd = reviews[rid]
            res['reviewrequests'][rid] = {
                'node': node,
                'status': rd['status'],
                'public': rd['public'],
            }

            if rd['reviewers']:
                res['reviewrequests'][rid]['reviewers'] = list(rd['reviewers'])

    except AuthorizationError as e:
        return errorresponse(str(e))
    except BadRequestError as e:
        return errorresponse(str(e))

    return res
Example #11
0
    def test_requal_reviewers(self):

        # first with r? reviewer request syntax
        self.assertEqual(list(parse_requal_reviewers('Bug 1 - some stuff; r?romulus')), [])
        self.assertEqual(list(parse_requal_reviewers('Bug 1 - More stuff; r?romulus, r?remus')), [])
        self.assertEqual(list(parse_requal_reviewers('Bug 1 - More stuff; r?romulus,r?remus')), [])
        self.assertEqual(list(parse_requal_reviewers('Bug 1 - More stuff; r?romulus, remus')), [])
        self.assertEqual(list(parse_requal_reviewers('Bug 1 - More stuff; r?romulus,remus')), [])
        self.assertEqual(list(parse_requal_reviewers('Bug 1 - More stuff; (r?romulus)')), [])
        self.assertEqual(list(parse_requal_reviewers('Bug 1 - More stuff; (r?romulus,remus)')),[])
        self.assertEqual(list(parse_requal_reviewers('Bug 1 - More stuff; [r?romulus]')), [])
        self.assertEqual(list(parse_requal_reviewers('Bug 1 - More stuff; [r?remus, r?romulus]')), [])
        self.assertEqual(list(parse_requal_reviewers('Bug 1 - More stuff; r?romulus, a=test-only')), [])

        # now with r= review granted syntax
        self.assertEqual(list(parse_requal_reviewers('Bug 1 - some stuff; r=romulus')), ['romulus'])
        self.assertEqual(list(parse_requal_reviewers('Bug 1 - More stuff; r=romulus, r=remus')), ['romulus', 'remus'])
        self.assertEqual(list(parse_requal_reviewers('Bug 1 - More stuff; r=romulus,r=remus')), ['romulus', 'remus'])
        self.assertEqual(list(parse_requal_reviewers('Bug 1 - More stuff; r=romulus, remus')), ['romulus', 'remus'])
        self.assertEqual(list(parse_requal_reviewers('Bug 1 - More stuff; r=romulus,remus')), ['romulus', 'remus'])
        self.assertEqual(list(parse_requal_reviewers('Bug 1 - More stuff; (r=romulus)')),['romulus'])
        self.assertEqual(list(parse_requal_reviewers('Bug 1 - More stuff; (r=romulus,remus)')), ['romulus', 'remus'])
        self.assertEqual(list(parse_requal_reviewers('Bug 1 - More stuff; [r=romulus]')), ['romulus'])
        self.assertEqual(list(parse_requal_reviewers('Bug 1 - More stuff; [r=remus, r=romulus]')), ['remus', 'romulus'])
        self.assertEqual(list(parse_requal_reviewers('Bug 1 - More stuff; r=romulus, a=test-only')), ['romulus'])

        self.assertEqual(list(parse_requal_reviewers(
            'Bug 1094764 - Implement AudioContext.suspend and friends.  r=roc,ehsan\n'
            '- Relevant spec text:\n'
            '- http://webaudio.github.io/web-audio-api/#widl-AudioContext-suspend-Promise\n'
            '- http://webaudio.github.io/web-audio-api/#widl-AudioContext-resume-Promise\n')),
            ['roc', 'ehsan'])

        self.assertEqual(list(parse_requal_reviewers(
            'Bug 380783 - nsStringAPI.h: no equivalent of IsVoid (tell if '
            'string is null), patch by Mook <*****@*****.**>, '
            'r=bsmedberg/dbaron, sr=dbaron, a1.9=bz')),
            ['bsmedberg', 'dbaron'])