def test_rquestion_reviewers(self):

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

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

        # oddball real-world examples
        self.assertEqual(list(parse_rquestion_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')),
            [])

        self.assertEqual(list(parse_rquestion_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')),
            [])

        self.assertEqual(list(parse_rquestion_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_rquestion_reviewers(
            'Bug 1024110 - Change Aurora\'s default profile behavior to use channel-specific profiles. r=bsmedberg f=gavin,markh')),
            [])
    def test_rquestion_reviewers(self):
        # empty
        self.assertEqual(list(parse_rquestion_reviewers('')), [])

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

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

        # oddball real-world examples
        self.assertEqual(
            list(
                parse_rquestion_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'
                )), [])

        self.assertEqual(
            list(
                parse_rquestion_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')), [])

        self.assertEqual(
            list(
                parse_rquestion_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_rquestion_reviewers(
                    'Bug 1024110 - Change Aurora\'s default profile behavior to use channel-specific profiles. r=bsmedberg f=gavin,markh'
                )), [])
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
Exemplo n.º 4
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
Exemplo n.º 5
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