def test_bug(self):
     self.assertEqual(parse_bugs('bug 1'), [1])
     self.assertEqual(parse_bugs('bug 123456'), [123456])
     self.assertEqual(parse_bugs('testb=1234x'), [])
     self.assertEqual(parse_bugs('ab4665521e2f'), [])
     self.assertEqual(parse_bugs('Aug 2008'), [])
     self.assertEqual(parse_bugs('b=#12345'), [12345])
     self.assertEqual(parse_bugs('GECKO_191a2_20080815_RELBRANCH'), [])
     self.assertEqual(parse_bugs('12345 is a bug'), [12345])
     self.assertEqual(parse_bugs(' 123456 whitespace!'), [123456])
Ejemplo n.º 2
0
def pull(orig, repo, remote, *args, **kwargs):
    """Wraps exchange.pull to add remote tracking refs."""
    if not hasattr(repo, 'changetracker'):
        return orig(repo, remote, *args, **kwargs)

    old_rev = len(repo)
    res = orig(repo, remote, *args, **kwargs)
    lock = repo.wlock()
    try:
        tree = resolve_uri_to_tree(remote.url())

        if tree:
            repo._update_remote_refs(remote, tree)
            if repo.changetracker:
                repo.changetracker.load_pushlog(tree)

        # Sync bug info.
        for rev in repo.changelog.revs(old_rev + 1):
            ctx = repo[rev]
            bugs = parse_bugs(ctx.description())
            if bugs and repo.changetracker:
                repo.changetracker.associate_bugs_with_changeset(bugs,
                    ctx.node())

    finally:
        lock.release()

    return res
Ejemplo n.º 3
0
 def bugsgen(_context):
     '''Generator for bugs list'''
     for bug in commitparser.parse_bugs(description):
         yield {
             'no': str(bug),
             'url': 'https://bugzilla.mozilla.org/show_bug.cgi?id=%s' % bug,
         }
Ejemplo n.º 4
0
def pull(orig, repo, remote, *args, **kwargs):
    """Wraps exchange.pull to add remote tracking refs."""
    if not hasattr(repo, 'changetracker'):
        return orig(repo, remote, *args, **kwargs)

    old_rev = len(repo)
    res = orig(repo, remote, *args, **kwargs)
    lock = repo.wlock()
    try:
        tree = resolve_uri_to_tree(remote.url())

        if tree:
            repo._update_remote_refs(remote, tree)

        # Sync bug info.
        for rev in repo.changelog.revs(old_rev + 1):
            ctx = repo[rev]
            bugs = parse_bugs(ctx.description())
            if bugs and repo.changetracker:
                repo.changetracker.associate_bugs_with_changeset(
                    bugs, ctx.node())

    finally:
        lock.release()

    return res
Ejemplo n.º 5
0
 def bug(self):
     bugs = commitparser.parse_bugs(self.commit.message.split("\n")[0])
     if len(bugs) > 1:
         logger.warning("Got multiple bugs for commit %s: %s" %
                        (self.canonical_rev, ", ".join(str(item) for item in bugs)))
     if not bugs:
         return None
     return str(bugs[0])
Ejemplo n.º 6
0
def parse_bug_id(desc):
    """Parse a Firefox VCS commit message and return the bug ID, if possible."""
    ids = commitparser.parse_bugs(desc)
    if ids:
        # Multiple bug id#s doesn't happen in practice.
        return int(ids[0])
    else:
        return np.NaN
Ejemplo n.º 7
0
def template_bugs(repo, ctx, **args):
    """:bugs: List of ints. The bugs associated with this changeset."""
    bugs = parse_bugs(ctx.description())

    # TRACKING hg47
    if templateutil:
        bugs = templateutil.hybridlist(bugs, 'bugs')

    return bugs
 def bugsgen(_context):
     '''Generator for bugs list'''
     for bug in commitparser.parse_bugs(description):
         bug = pycompat.bytestr(bug)
         yield {
             b'no': bug,
             b'url':
             b'https://bugzilla.mozilla.org/show_bug.cgi?id=%s' % bug,
         }
    def test_bug(self):
        self.assertEqual(parse_bugs('bug 1'), [1])
        self.assertEqual(parse_bugs('bug 123456'), [123456])
        self.assertEqual(parse_bugs('testb=1234x'), [])
        self.assertEqual(parse_bugs('ab4665521e2f'), [])
        self.assertEqual(parse_bugs('Aug 2008'), [])
        self.assertEqual(parse_bugs('b=#12345'), [12345])
        self.assertEqual(parse_bugs('GECKO_191a2_20080815_RELBRANCH'), [])
        self.assertEqual(parse_bugs('12345 is a bug'), [12345])
        self.assertEqual(parse_bugs(' 123456 whitespace!'), [123456])

        # Duplicate bug numbers should be stripped.
        msg = '''Bug 1235097 - Add support for overriding the site root

On brasstacks, `web.ctx.home` is incorrect (see bug 1235097 comment 23), which
means that the URL used by mohawk to verify the authenticated request hashes
differs from that used to generate the hash.'''
        self.assertEqual(parse_bugs(msg), [1235097])
Ejemplo n.º 10
0
 def bug(self):
     # type: () -> Optional[int]
     bugs = commitparser.parse_bugs(self.msg.splitlines()[0])
     if len(bugs) > 1:
         logger.warning(u"Got multiple bugs for commit %s: %s" %
                        (self.canonical_rev,
                         u", ".join(str(item) for item in bugs)))
     if not bugs:
         return None
     assert isinstance(bugs[0], int)
     return bugs[0]
Ejemplo n.º 11
0
        def sync_bug_database(self):
            if not self.changetracker:
                return

            for rev in self:
                ui.progress('changeset', rev, total=len(self))
                ctx = self[rev]
                bugs = parse_bugs(ctx.description())
                if bugs:
                    self.changetracker.associate_bugs_with_changeset(
                        bugs, ctx.node())

            ui.progress('changeset', None)
Ejemplo n.º 12
0
        def sync_bug_database(self):
            if not self.changetracker:
                return

            for rev in self:
                ui.progress('changeset', rev, total=len(self))
                ctx = self[rev]
                bugs = parse_bugs(ctx.description())
                if bugs:
                    self.changetracker.associate_bugs_with_changeset(bugs,
                        ctx.node())

            ui.progress('changeset', None)
Ejemplo n.º 13
0
def parse_bug_ids(string):
    """
    Parses a given string of a commit message into a set of bug IDs.

    Args:
        string (str): the string representing the commit message

    Returns:
        set of bytes: a set of strings representing bug IDs
    """
    return {
        pycompat.bytestr(b)
        for b in commitparser.parse_bugs(string, conservative=True)
    }
Ejemplo n.º 14
0
def revset_bug(repo, subset, x):
    """``bug(N)```
    Changesets referencing a specified Bugzilla bug. e.g. bug(123456).
    """
    err = _('bug() requires an integer argument.')
    bugstring = revset.getstring(x, err)

    try:
        bug = int(bugstring)
    except Exception:
        raise ParseError(err)

    # We do a simple string test first because avoiding regular expressions
    # is good for performance.
    return [r for r in subset
            if bugstring in repo[r].description() and
                bug in parse_bugs(repo[r].description())]
Ejemplo n.º 15
0
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])
        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"))
Ejemplo n.º 16
0
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:
        identifiers = set()
        for node in nodes:
            ctx = repo[node]
            bugs = parse_bugs(ctx.description().split('\n')[0])
            if bugs:
                identifier = 'bz://%s' % bugs[0]
                identifiers.add(identifier)

        if len(identifiers) > 1:
            raise util.Abort('cannot submit reviews referencing multiple '
                             'bugs', hint='limit reviewed changesets '
                             'with "-c" or "-r" arguments')

    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

    req = commonrequestdict(ui, bzauth)
    req['identifier'] = identifier.full
    req['changesets'] = []
    req['obsolescence'] = obsolete.isenabled(repo, obsolete.createmarkersopt)

    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:
        precursors = [hex(n) for n in obsolete.allprecursors(obsstore, [node])]
        req['changesets'].append({
            'node': hex(node),
            'precursors': precursors,
        })

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

    res = calljsoncommand(ui, remote, 'pushreview', data=req, httpcap='submithttp',
                          httpcommand='mozreviewsubmitseries')
    if 'error' in res:
        raise error.Abort(res['error'])

    for w in res['display']:
        ui.write('%s\n' % w)

    reviews.baseurl = res['rburl']
    newparentid = res['parentrrid']
    reviews.addparentreview(identifier.full, newparentid)

    nodereviews = {}
    reviewdata = {}

    for rid, info in sorted(res['reviewrequests'].iteritems()):
        if 'node' in info:
            node = bin(info['node'])
            nodereviews[node] = rid

        reviewdata[rid] = {
            'status': info['status'],
            'public': info['public'],
        }

        if 'reviewers' in info:
            reviewdata[rid]['reviewers'] = info['reviewers']

    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])
        ui.write('review:     %s' % reviews.reviewurl(rid))
        if not reviewdata[rid].get('public'):
            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 not reviewdata[newparentid].get('public'):
        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'))
Ejemplo n.º 17
0
 def fltr(x):
     # We do a simple string test first because avoiding regular expressions
     # is good for performance.
     desc = repo[x].description()
     return bugstring in desc and bug in parse_bugs(desc)
Ejemplo n.º 18
0
def addmetadata(repo, ctx, d, onlycheap=False):
    """Add changeset metadata for hgweb templates."""
    bugs = list(set(commitparser.parse_bugs(ctx.description())))
    d['bugs'] = []
    for bug in commitparser.parse_bugs(ctx.description()):
        d['bugs'].append({
            'no': str(bug),
            'url': 'https://bugzilla.mozilla.org/show_bug.cgi?id=%s' % bug,
        })

    d['reviewers'] = []
    for reviewer in commitparser.parse_reviewers(ctx.description()):
        d['reviewers'].append({
            'name': reviewer,
            'revset': 'reviewer(%s)' % reviewer,
        })

    d['backsoutnodes'] = []
    backouts = commitparser.parse_backouts(ctx.description())
    if backouts:
        for node in backouts[0]:
            try:
                bctx = repo[node]
                d['backsoutnodes'].append({'node': bctx.hex()})
            except error.LookupError:
                pass

    # Repositories can define which TreeHerder repository they are associated
    # with.
    treeherder = repo.ui.config('mozilla', 'treeherder_repo')
    if treeherder:
        d['treeherderrepourl'] = 'https://treeherder.mozilla.org/#/jobs?repo=%s' % treeherder

    if onlycheap:
        return

    # Obtain the Gecko/app version/milestone.
    #
    # We could probably only do this if the repo is a known app repo (by
    # looking at the initial changeset). But, path based lookup is relatively
    # fast, so just do it. However, we need this in the "onlycheap"
    # section because resolving manifests is relatively slow and resolving
    # several on changelist pages may add seconds to page load times.
    try:
        fctx = repo.filectx('config/milestone.txt', changeid=ctx.node())
        lines = fctx.data().splitlines()
        lines = [l for l in lines if not l.startswith('#') and l.strip()]

        if lines:
            d['milestone'] = lines[0].strip()
    except error.LookupError:
        pass

    # Look for changesets that back out this one.
    #
    # We limit the distance we search for backouts because an exhaustive
    # search could be very intensive. e.g. you load up the root commit
    # on a repository with 200,000 changesets and that commit is never
    # backed out. This finds most backouts because backouts typically happen
    # shortly after a bad commit is introduced.
    thisshort = short(ctx.node())
    count = 0
    searchlimit = repo.ui.configint('hgmo', 'backoutsearchlimit', 100)
    for bctx in repo.set('%ld::', [ctx.rev()]):
        count += 1
        if count >= searchlimit:
            break

        backouts = commitparser.parse_backouts(bctx.description())
        if backouts and thisshort in backouts[0]:
            d['backedoutbynode'] = bctx.hex()
            break
Ejemplo n.º 19
0
def revset_nobug(repo, subset, x):
    if x:
        raise ParseError(_('nobug() does not take any arguments'))

    return subset.filter(lambda x: not parse_bugs(repo[x].description()))
 def test_bug_conservatively(self):
     self.assertEqual(parse_bugs(b'bug 1', conservative=True), [1])
     self.assertEqual(parse_bugs(b'bug 123456', conservative=True),
                      [123456])
     self.assertEqual(parse_bugs(b'Bug 123456', conservative=True),
                      [123456])
     self.assertEqual(parse_bugs(b'debug 123456', conservative=True), [])
     self.assertEqual(parse_bugs(b'testb=1234x', conservative=True), [])
     self.assertEqual(parse_bugs(b'ab4665521e2f', conservative=True), [])
     self.assertEqual(parse_bugs(b'Aug 2008', conservative=True), [])
     self.assertEqual(parse_bugs(b'b=#12345', conservative=True), [])
     self.assertEqual(parse_bugs(b'b=12345', conservative=True), [12345])
     self.assertEqual(
         parse_bugs(b'GECKO_191a2_20080815_RELBRANCH', conservative=True),
         [])
     self.assertEqual(parse_bugs(b'12345 is a bug', conservative=True), [])
     self.assertEqual(parse_bugs(b' 123456 whitespace!', conservative=True),
                      [])
Ejemplo n.º 21
0
def wrappedpushbookmark(orig, pushop):
    result = orig(pushop)

    # pushop.ret was renamed to pushop.cgresult in Mercurial 3.2. We can drop
    # this branch once we drop <3.2 support.
    if hasattr(pushop, 'cgresult'):
        origresult = pushop.cgresult
    else:
        origresult = pushop.ret

    # Don't do anything if error from push.
    if not origresult:
        return result

    remoteurl = pushop.remote.url()
    tree = repository.resolve_uri_to_tree(remoteurl)
    # We don't support release trees (yet) because they have special flags
    # that need to get updated.
    if tree and tree in repository.RELEASE_TREES:
        return result

    ui = pushop.ui
    if tree and tree in ui.configlist('bzpost', 'excludetrees', default=[]):
        return result

    if tree:
        baseuri = repository.resolve_trees_to_uris([tree
                                                    ])[0][1].encode('utf-8')
        assert baseuri
    else:
        # This isn't a known Firefox tree. Fall back to resolving URLs by
        # hostname.

        # Only attend Mozilla's server.
        if not updateunknown(remoteurl, repository.BASE_WRITE_URI, ui):
            return result

        baseuri = remoteurl.replace(repository.BASE_WRITE_URI,
                                    repository.BASE_READ_URI).rstrip('/')

    bugsmap = {}
    lastbug = None
    lastnode = None

    for node in pushop.outgoing.missing:
        ctx = pushop.repo[node]

        # Don't do merge commits.
        if len(ctx.parents()) > 1:
            continue

        # Our bug parser is buggy for Gaia bump commit messages.
        if '<*****@*****.**>' in ctx.user():
            continue

        # Pushing to Try (and possibly other repos) could push unrelated
        # changesets that have been pushed to an official tree but aren't yet
        # on this specific remote. We use the phase information as a proxy
        # for "already pushed" and prune public changesets from consideration.
        if tree == 'try' and ctx.phase() == phases.public:
            continue

        bugs = parse_bugs(ctx.description())

        if not bugs:
            continue

        bugsmap.setdefault(bugs[0], []).append(ctx.hex())
        lastbug = bugs[0]
        lastnode = ctx.hex()

    if not bugsmap:
        return result

    bzauth = getbugzillaauth(ui)
    if not bzauth:
        return result

    bzurl = ui.config('bugzilla', 'url', 'https://bugzilla.mozilla.org/rest')

    bugsy = Bugsy(username=bzauth.username,
                  password=bzauth.password,
                  userid=bzauth.userid,
                  cookie=bzauth.cookie,
                  api_key=bzauth.apikey,
                  bugzilla_url=bzurl)

    def public_url_for_bug(bug):
        '''Turn 123 into "https://bugzilla.mozilla.org/show_bug.cgi?id=123".'''
        public_baseurl = bzurl.replace('rest', '').rstrip('/')
        return '%s/show_bug.cgi?id=%s' % (public_baseurl, bug)

    # If this is a try push, we paste the Treeherder link for the tip commit, because
    # the per-commit URLs don't have much value.
    # TODO roll this into normal pushing so we get a Treeherder link in bugs as well.
    if tree == 'try' and lastbug:
        treeherderurl = repository.treeherder_url(tree, lastnode)

        bug = bugsy.get(lastbug)
        comments = bug.get_comments()
        for comment in comments:
            if treeherderurl in comment.text:
                return result

        ui.write(
            _('recording Treeherder push at %s\n') %
            public_url_for_bug(lastbug))
        bug.add_comment(treeherderurl)
        return result

    for bugnumber, nodes in bugsmap.items():
        bug = bugsy.get(bugnumber)

        comments = bug.get_comments()
        missing_nodes = []

        # When testing whether this changeset URL is referenced in a
        # comment, we only need to test for the node fragment. The
        # important side-effect is that each unique node for a changeset
        # is recorded in the bug.
        for node in nodes:
            if not any(node in comment.text for comment in comments):
                missing_nodes.append(node)

        if not missing_nodes:
            ui.write(
                _('bug %s already knows about pushed changesets\n') %
                bugnumber)
            continue

        lines = []

        for node in missing_nodes:
            ctx = pushop.repo[node]
            lines.append('%s/rev/%s' % (baseuri, ctx.hex()))
            # description is using local encodings. Depending on the
            # configured encoding, replacement characters could be involved. We
            # use encoding.fromlocal() to get the raw bytes, which should be
            # valid UTF-8.
            lines.append(encoding.fromlocal(ctx.description()).splitlines()[0])
            lines.append('')

        comment = '\n'.join(lines)

        ui.write(_('recording push at %s\n') % public_url_for_bug(bugnumber))
        bug.add_comment(comment)

    return result
Ejemplo n.º 22
0
 def bug_numbers(self):
     return commitparser.parse_bugs(self.msg)
def doreview(repo, ui, remote, reviewnode, basenode=None):
    """Do the work of submitting a review to a remote repo.

    :remote is a peerrepository.
    :reviewnode is the node of the tip to review.
    :basenode is the bottom node to review. If not specified, we will review
    all non-public ancestors of :reviewnode.
    """
    assert remote.capable('reviewboard')

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

    # Given a tip node, we need to find all changesets to review.
    #
    # A solution that works most of the time is to find all non-public
    # ancestors of that node. This is our default.
    #
    # If basenode is specified, we stop the traversal when we encounter it.
    #
    # Note that we will still refuse to review a public changeset even with
    # basenode. This decision is somewhat arbitrary and can be revisited later
    # if there is an actual need to review public changesets.
    nodes = [reviewnode]
    for node in repo[reviewnode].ancestors():
        ctx = repo[node]

        if ctx.phase() == phases.public:
            break
        if basenode and ctx.node() == basenode:
            nodes.insert(0, ctx.node())
            break

        nodes.insert(0, ctx.node())

    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 = [
        '1',
        'reviewidentifier %s' % urllib.quote(identifier.full),
    ]

    for p in ('username', 'password', 'userid', 'cookie'):
        if getattr(bzauth, p, None):
            lines.append('bz%s %s' % (p, urllib.quote(getattr(bzauth, p))))

    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))

    # All protocol versions begin with: <version>\n
    try:
        off = res.index('\n')
        version = int(res[0:off])

        if version != 1:
            raise util.Abort(_('do not know how to handle response from server.'))
    except ValueError:
        raise util.Abort(_('invalid response from server.'))

    assert version == 1
    lines = res.split('\n')[1:]

    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)
            reviews.addnodereview(node, rid, newparentid)
            reviewdata[rid] = {}
            nodereviews[node] = rid
        elif t == 'reviewdata':
            rid, field, value = d.split(' ', 2)
            value = urllib.unquote(value)
            reviewdata[rid][field] = value
        elif t == 'rburl':
            reviews.baseurl = d

    reviews.remoteurl = remote.url()

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

    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])
        ui.write('review:     %s' % reviews.reviewurl(rid))
        if reviewdata[rid].get('status') == 'pending':
            ui.write(' (pending)')
        ui.write('\n\n')

    ispending = reviewdata[newparentid].get('status', None) == 'pending'
    ui.write(_('review id:  %s\n') % identifier.full)
    ui.write(_('review url: %s') % reviews.parentreviewurl(identifier.full))
    if ispending:
        ui.write(' (pending)')
    ui.write('\n')

    # Make it clear to the user that they need to take action in order for
    # others to see this review series.
    if ispending:
        ui.status(_('(visit review url to publish this review request so others can see it)\n'))
Ejemplo n.º 24
0
def landable_commits(git_gecko,
                     git_wpt,
                     prev_wpt_head,
                     wpt_head=None,
                     include_incomplete=False):
    """Get the list of commits that are able to land.

    :param prev_wpt_head: The sha1 of the previous wpt commit landed to gecko.
    :param wpt_head: The sha1 of the latest possible commit to land to gecko,
                     or None to use the head of the master branch"
    :param include_incomplete: By default we don't attempt to land anything that
                               hasn't completed a metadata update. This flag disables
                               that and just lands everything up to the specified commit."""
    if wpt_head is None:
        wpt_head = "origin/master"
    pr_commits = unlanded_wpt_commits_by_pr(git_gecko, git_wpt, prev_wpt_head,
                                            wpt_head)
    landable_commits = []
    for pr, commits in pr_commits:
        last = False
        if not pr:
            # Assume this was some trivial fixup:
            continue

        def upstream_sync(bug_number):
            syncs = upstream.UpstreamSync.for_bug(git_gecko,
                                                  git_wpt,
                                                  bug_number,
                                                  flat=True)
            for sync in syncs:
                # Only check the first commit since later ones could be added in the PR
                if (commits[0].metadata["gecko-commit"] in {
                        item.canonical_rev
                        for item in sync.upstreamed_gecko_commits
                }):
                    break
            else:
                sync = None
            return sync

        sync = None
        if upstream.UpstreamSync.has_metadata(commits[0].msg):
            sync = upstream_sync(
                bug.bug_number_from_url(commits[0].metadata["bugzilla-url"]))
        if sync is None:
            sync = downstream.DownstreamSync.for_pr(git_gecko, git_wpt, pr)
            if sync and "affected-tests" in sync.data and sync.data[
                    "affected-tests"] is None:
                del sync.data["affected-tests"]
        if sync is None:
            # Last ditch attempt at finding an upstream sync for this commit in the
            # case that the metadata happens to be broken
            bugs = commitparser.parse_bugs(commits[0].msg.split("\n")[0])
            for bug_number in bugs:
                sync = upstream_sync(bug_number)
                if sync:
                    break
        if not include_incomplete:
            if not sync:
                # TODO: schedule a downstream sync for this pr
                logger.info("PR %s has no corresponding sync" % pr)
                last = True
            elif (isinstance(sync, downstream.DownstreamSync)
                  and not (sync.skip or sync.metadata_ready)):
                logger.info("Metadata pending for PR %s" % pr)
                last = True
            if last:
                break
        landable_commits.append((pr, sync, commits))

    if not landable_commits:
        logger.info("No new commits are landable")
        return None

    wpt_head = landable_commits[-1][2][-1].sha1
    logger.info("Landing up to commit %s" % wpt_head)

    return wpt_head, landable_commits
Ejemplo n.º 25
0
def handle_pending_mozreview_pullrequests(logger, dbconn):
    gh = github.connect()
    if not gh:
        return

    bzurl = config.get('bugzilla')['url']

    cursor = dbconn.cursor()

    query = """
        select id,ghuser,repo,pullrequest,destination,bzuserid,bzcookie,bugid,
               pingback_url
        from MozreviewPullRequest
        where landed is null
    """
    cursor.execute(query)

    finished_revisions = []
    mozreview_updates = []
    for row in cursor.fetchall():
        (transplant_id, ghuser, repo, pullrequest, destination, bzuserid,
         bzcookie, bugid, pingback_url) = row

        logger.info('attempting to import pullrequest: %s' % transplant_id)

        # see if we can extract the bug from the commit message
        if bugid is None:
            title, body = github.retrieve_issue(gh, ghuser, repo, pullrequest)
            bugs = parse_bugs(title)
            if bugs:
                bugid = bugs[0]
                logger.info('using bug %s from issue title' % bugid)
                finished_revisions.append([bugid, None, None, transplant_id])

        # still no luck, attempt to autofile a bug on the user's behalf
        if bugid is None:
            logger.info('attempting to autofile bug for: %s' % transplant_id)

            b = bugsy.Bugsy(userid=bzuserid, cookie=bzcookie,
                            bugzilla_url=bzurl)
            if not b:
                logger.info('could not connect to bugzilla instance at %s for '
                            'pullrequest id %s' % (bzurl, transplant_id))
                error = 'could not connect to bugzilla. bad credentials?'
            else:
                bug = bugsy.Bug()

                # Summary is required, the rest have defaults or are optional
                bug.summary = title

                if config.testing():
                    bug.product = 'TestProduct'
                    bug.component = 'TestComponent'
                else:
                    # TODO: determine a better product & component than the
                    # defaults provided by Bugsy
                    pass

                pr_url = github.url_for_pullrequest(ghuser,repo, pullrequest)
                bug.add_comment('%s\n\nImported from: %s' % (body, pr_url))

                try:
                    b.put(bug)
                    bugid = bug.id
                    logger.info('created bug: %s ' % bugid)
                    finished_revisions.append([bugid, None, None, transplant_id])
                except bugsy.BugsyException as e:
                    logger.info('transplant failed: could not create new bug: %s '
                                % e.msg)
                    finished_revisions.append([None, False, e.msg, transplant_id])

                    # set up data to be posted back to mozreview
                    data = {
                        'request_id': transplant_id,
                        'bugid': None,
                        'landed': False,
                        'error_msg': 'could not create new bug: ' + e.msg,
                        'result': ''
                    }

                    mozreview_updates.append([transplant_id, pingback_url,
                                              json.dumps(data)])

        landed, result = transplant.transplant_to_mozreview(gh, destination,
                                                            ghuser, repo,
                                                            pullrequest,
                                                            bzuserid, bzcookie,
                                                            bugid)

        if landed:
            logger.info(('transplanted from'
                         ' https://github.com/%s/%s/pull/%s'
                         ' to destination: %s new revision: %s') %
                        (ghuser, repo, pullrequest, destination, result))
        else:
            logger.info(('transplant failed'
                         ' https://github.com/%s/%s/pull/%s'
                         ' destination: %s error: %s') %
                        (ghuser, repo, pullrequest, destination, result))

        finished_revisions.append([bugid, landed, result, transplant_id])

        # set up data to be posted back to mozreview
        data = {
            'request_id': transplant_id,
            'bugid': bugid,
            'landed': landed,
            'error_msg': '',
            'result': ''
        }

        if landed:
            data['result'] = result
        else:
            data['error_msg'] = result

        mozreview_updates.append([transplant_id, pingback_url, json.dumps(data)])

    if finished_revisions:
        query = """
            update MozreviewPullRequest set bugid=%s,landed=%s,result=%s
            where id=%s
        """
        cursor.executemany(query, finished_revisions)
        dbconn.commit()

    if mozreview_updates:
        query = """
            insert into MozreviewUpdate(request_id,pingback_url,data)
            values(%s,%s,%s)
        """
        cursor.executemany(query, mozreview_updates)
        dbconn.commit()
Ejemplo n.º 26
0
def wrappedpushbookmark(orig, pushop):
    result = orig(pushop)

    # pushop.ret was renamed to pushop.cgresult in Mercurial 3.2. We can drop
    # this branch once we drop <3.2 support.
    if hasattr(pushop, 'cgresult'):
        origresult = pushop.cgresult
    else:
        origresult = pushop.ret

    # Don't do anything if error from push.
    if not origresult:
        return result

    remoteurl = pushop.remote.url()
    tree = repository.resolve_uri_to_tree(remoteurl)
    # We don't support release trees (yet) because they have special flags
    # that need to get updated.
    if tree and tree in repository.RELEASE_TREES:
        return result

    ui = pushop.ui
    if tree and tree in ui.configlist('bzpost', 'excludetrees', default=[]):
        return result

    if tree:
        baseuri = repository.resolve_trees_to_uris([tree])[0][1].encode('utf-8')
        assert baseuri
    else:
        # This isn't a known Firefox tree. Fall back to resolving URLs by
        # hostname.

        # Only attend Mozilla's server.
        if not updateunknown(remoteurl, repository.BASE_WRITE_URI, ui):
            return result

        baseuri = remoteurl.replace(repository.BASE_WRITE_URI, repository.BASE_READ_URI).rstrip('/')

    bugsmap = {}
    lastbug = None
    lastnode = None

    for node in pushop.outgoing.missing:
        ctx = pushop.repo[node]

        # Don't do merge commits.
        if len(ctx.parents()) > 1:
            continue

        # Our bug parser is buggy for Gaia bump commit messages.
        if '<*****@*****.**>' in ctx.user():
            continue

        # Pushing to Try (and possibly other repos) could push unrelated
        # changesets that have been pushed to an official tree but aren't yet
        # on this specific remote. We use the phase information as a proxy
        # for "already pushed" and prune public changesets from consideration.
        if tree == 'try' and ctx.phase() == phases.public:
            continue

        bugs = parse_bugs(ctx.description())

        if not bugs:
            continue

        bugsmap.setdefault(bugs[0], []).append(ctx.hex()[0:12])
        lastbug = bugs[0]
        lastnode = ctx.hex()[0:12]

    if not bugsmap:
        return result

    bzauth = getbugzillaauth(ui)
    if not bzauth:
        return result

    bzurl = ui.config('bugzilla', 'url', 'https://bugzilla.mozilla.org/rest')

    bugsy = Bugsy(username=bzauth.username, password=bzauth.password,
                  userid=bzauth.userid, cookie=bzauth.cookie,
                  api_key=bzauth.apikey, bugzilla_url=bzurl)

    def public_url_for_bug(bug):
        '''Turn 123 into "https://bugzilla.mozilla.org/show_bug.cgi?id=123".'''
        public_baseurl = bzurl.replace('rest', '').rstrip('/')
        return '%s/show_bug.cgi?id=%s' % (public_baseurl, bug)

    # If this is a try push, we paste the Treeherder link for the tip commit, because
    # the per-commit URLs don't have much value.
    # TODO roll this into normal pushing so we get a Treeherder link in bugs as well.
    if tree == 'try' and lastbug:
        treeherderurl = repository.treeherder_url(tree, lastnode)

        bug = bugsy.get(lastbug)
        comments = bug.get_comments()
        for comment in comments:
            if treeherderurl in comment.text:
                return result

        ui.write(_('recording Treeherder push at %s\n') % public_url_for_bug(lastbug))
        bug.add_comment(treeherderurl)
        return result

    for bugnumber, nodes in bugsmap.items():
        bug = bugsy.get(bugnumber)

        comments = bug.get_comments()
        missing_nodes = []

        # When testing whether this changeset URL is referenced in a
        # comment, we only need to test for the node fragment. The
        # important side-effect is that each unique node for a changeset
        # is recorded in the bug.
        for node in nodes:
            if not any(node in comment.text for comment in comments):
                missing_nodes.append(node)

        if not missing_nodes:
            ui.write(_('bug %s already knows about pushed changesets\n') %
                     bugnumber)
            continue

        lines = []

        for node in missing_nodes:
            ctx = pushop.repo[node]
            lines.append('%s/rev/%s' % (baseuri, ctx.hex()))
            # description is using local encodings. Depending on the
            # configured encoding, replacement characters could be involved. We
            # use encoding.fromlocal() to get the raw bytes, which should be
            # valid UTF-8.
            lines.append(encoding.fromlocal(ctx.description()).splitlines()[0])
            lines.append('')

        comment = '\n'.join(lines)

        ui.write(_('recording push at %s\n') % public_url_for_bug(bugnumber))
        bug.add_comment(comment)

    return result
Ejemplo n.º 27
0
 def fltr(x):
     # We do a simple string test first because avoiding regular expressions
     # is good for performance.
     desc = repo[x].description()
     return bugstring in desc and bug in parse_bugs(desc)
Ejemplo n.º 28
0
def template_bug(repo, ctx, **args):
    """:bug: String. The bug this changeset is most associated with."""
    bugs = parse_bugs(ctx.description())
    return bugs[0] if bugs else None
Ejemplo n.º 29
0
def template_bug(repo, ctx, **args):
    """:bug: String. The bug this changeset is most associated with."""
    bugs = parse_bugs(ctx.description())
    return bugs[0] if bugs else None
Ejemplo n.º 30
0
def template_bugs(repo, ctx, **args):
    """:bugs: List of ints. The bugs associated with this changeset."""
    return parse_bugs(ctx.description())
Ejemplo n.º 31
0
def template_bugs(repo, ctx, **args):
    """:bugs: List of ints. The bugs associated with this changeset."""
    return parse_bugs(ctx.description())
Ejemplo n.º 32
0
def revset_nobug(repo, subset, x):
    if x:
        raise ParseError(_('nobug() does not take any arguments'))

    return [r for r in subset if not parse_bugs(repo[r].description())]
Ejemplo n.º 33
0
    def test_bug(self):
        self.assertEqual(parse_bugs(b'bug 1'), [1])
        self.assertEqual(parse_bugs(b'bug 123456'), [123456])
        self.assertEqual(parse_bugs(b'testb=1234x'), [])
        self.assertEqual(parse_bugs(b'ab4665521e2f'), [])
        self.assertEqual(parse_bugs(b'Aug 2008'), [])
        self.assertEqual(parse_bugs(b'b=#12345'), [12345])
        self.assertEqual(parse_bugs(b'GECKO_191a2_20080815_RELBRANCH'), [])
        self.assertEqual(parse_bugs(b'12345 is a bug'), [12345])
        self.assertEqual(parse_bugs(b' 123456 whitespace!'), [123456])

        # Duplicate bug numbers should be stripped.
        msg = b'''Bug 1235097 - Add support for overriding the site root

On brasstacks, `web.ctx.home` is incorrect (see bug 1235097 comment 23), which
means that the URL used by mohawk to verify the authenticated request hashes
differs from that used to generate the hash.'''
        self.assertEqual(parse_bugs(msg), [1235097])

        # Merge numbers should not be considered bug numbers.
        msg = b'''servo: Merge #19754 - Implement element.innerText getter (from ferjm:innertext); r=mbrubeck

Source-Repo: https://github.com/servo/servo
Source-Revision: 9e64008e759a678a3971d04977c2b20b66fa8229'''
        self.assertEqual(parse_bugs(msg), [])

        msg = b'''Bug 123456 - Fix all of the things

Source-Repo: https://github.com/mozilla/foo'''
        self.assertEqual(parse_bugs(msg), [123456])

        msg = b'''Merge #4256

This fixes #9000 and bug 324521

Source-Repo: https://github.com/mozilla/foo'''
        self.assertEqual(parse_bugs(msg), [324521])

        msg = b'''Bumping gaia.json for 2 gaia-central revision(s)

========

https://hg.mozilla.org/integration/gaia-central/rev/bb795b36fc34
Author: Arthur Chen <*****@*****.**>
Desc: Merge pull request #11050 from fabi1cazenave/i18nDefaultMediaLocation-bug892788

Bug 892788 - [Settings] "Cancel" and "Change" are not localized r=arthurcc

========

https://hg.mozilla.org/integration/gaia-central/rev/532b7b572923
Author: Fabien Cazenave <*****@*****.**>
Desc:  Bug 892788 - [Settings] "Cancel" and "Change" are not localized'''
        self.assertEqual(parse_bugs(msg), [892788])

        msg = b'''Bumping gaia.json for 3 gaia revision(s) a=gaia-bump

========

https://hg.mozilla.org/integration/gaia-central/rev/acfb759dfe5a
Author: Dale Harvey <*****@*****.**>
Desc: Bug 952098 - Add places as a rocketbar provider. r=kgrandon

========

https://hg.mozilla.org/integration/gaia-central/rev/13833975424a
Author: lissyx <*****@*****.**>
Desc: Merge pull request #15404 from lissyx/bug960081

Bug 960081 - Make use of shared mock Notification in Dialer call log tests r=etienne

========

https://hg.mozilla.org/integration/gaia-central/rev/c010e5ae36e4
Author: Alexandre Lissy <*****@*****.**>
Desc: Bug 960081 - Make use of shared mock Notification in Dialer call log tests'''
        self.assertEqual(parse_bugs(msg), [952098, 960081])
Ejemplo n.º 34
0
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)

    # Ensure a color for ui.warning is defined.
    try:
        color = extensions.find('color')
        if 'ui.warning' not in color._styles:
            color._styles['ui.warning'] = 'red'
    except Exception:
        pass

    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:
        identifiers = set()
        for node in nodes:
            ctx = repo[node]
            bugs = parse_bugs(ctx.description().split('\n')[0])
            if bugs:
                identifier = 'bz://%s' % bugs[0]
                identifiers.add(identifier)

        if len(identifiers) > 1:
            raise util.Abort(
                'cannot submit reviews referencing multiple '
                'bugs',
                hint='limit reviewed changesets '
                'with "-c" or "-r" arguments')

    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 '
                      'https://mozilla-version-control-tools.readthedocs.io/en/latest/mozreview-user.html)\n'
                      ))
                break

    req = commonrequestdict(ui, bzauth)
    req['identifier'] = identifier.full
    req['changesets'] = []
    req['obsolescence'] = obsolete.isenabled(repo, obsolete.createmarkersopt)
    req['deduce-reviewers'] = ui.configbool('reviewboard', 'deduce-reviewers',
                                            True)

    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:
        precursors = [hex(n) for n in obsolete.allprecursors(obsstore, [node])]
        req['changesets'].append({
            'node': hex(node),
            'precursors': precursors,
        })

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

    res = calljsoncommand(ui,
                          remote,
                          'pushreview',
                          data=req,
                          httpcap='submithttp',
                          httpcommand='mozreviewsubmitseries')

    # Re-encode all items in res from u'' to utf-8 byte str to avoid
    # exceptions during str operations.
    reencoderesponseinplace(res)

    if 'error' in res:
        raise error.Abort(res['error'])

    for w in res['display']:
        ui.write('%s\n' % w)

    reviews.baseurl = res['rburl']
    newparentid = res['parentrrid']
    reviews.addparentreview(identifier.full, newparentid)

    nodereviews = {}
    reviewdata = {}

    for rid, info in sorted(res['reviewrequests'].iteritems()):
        if 'node' in info:
            node = bin(info['node'])
            nodereviews[node] = rid

        reviewdata[rid] = {
            'status': info['status'],
            'public': info['public'],
        }

        if 'reviewers' in info:
            reviewdata[rid]['reviewers'] = info['reviewers']

    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])
        ui.write('review:     %s' % reviews.reviewurl(rid))
        if not reviewdata[rid].get('public'):
            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 not reviewdata[newparentid].get('public'):
        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.write('\n')
            ui.warn(
                _('(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:
        # If there is no configuration value specified for
        # reviewboard.autopublish, prompt the user. Otherwise, publish
        # automatically or not based on this value.
        if ui.config('reviewboard', 'autopublish', None) is None:
            ui.write('\n')
            publish = ui.promptchoice(
                _('publish these review '
                  'requests now (Yn)? '
                  '$$ &Yes $$ &No')) == 0
        else:
            publish = ui.configbool('reviewboard', 'autopublish')

        if publish:
            publishreviewrequests(ui, remote, bzauth, [newparentid])
        else:
            ui.status(
                _('(visit review url to publish these review '
                  'requests so others can see them)\n'))
Ejemplo n.º 35
0
def revset_nobug(repo, subset, x):
    if x:
        raise ParseError(_('nobug() does not take any arguments'))

    return subset.filter(lambda x: not parse_bugs(repo[x].description()))
Ejemplo n.º 36
0
def determine_review_system(revision_json):
    """Look for review system markers and guess which review system was used.

    Args:
        revision_json: A JSON structure for a specific changeset ID.  The
            structure is return by Mercurial's hgweb. For example:
            https://hg.mozilla.org/mozilla-central/json-rev/deafa2891c61

    Returns:
        A ReviewSystem enum value representing our guess about which review
        system was used, if any.
    """
    fulldesc = revision_json['desc']
    summary = split_summary(fulldesc)
    changeset = revision_json['node']
    author = revision_json['user']

    # Check for changesets that don't need review.
    if has_backout_markers(summary):
        log.info(f'changeset {changeset}: changeset is a back-out commit')
        return ReviewSystem.review_unneeded
    elif has_ignore_this_changeset_marker(summary):
        log.info(
            f'changeset {changeset}: changeset summary has "ignore-this-changeset" flag'
        )
        return ReviewSystem.not_applicable
    elif has_merge_markers(revision_json):
        log.info(f'changeset {changeset}: is a merge commit')
        return ReviewSystem.not_applicable
    elif has_no_bug_marker(summary):
        log.info(f'changeset {changeset}: summary is marked "no bug"')
        return ReviewSystem.no_bug
    elif has_uplift_markers(summary):
        log.info(f'changeset {changeset}: summary is marked uplift')
        return ReviewSystem.review_unneeded
    elif has_wpt_uplift_markers(author, summary):
        log.info(
            f'changeset {changeset}: changeset was requested by moz-wptsync-bot'
        )
        return ReviewSystem.review_unneeded

    # TODO handle multiple bugs?
    try:
        # Take the first bug # found.  For Firefox commits this is usually at
        # the front of the string, like "Bug XXXXXX - fix the bar".  try to
        # avoid messages where there is a second ID in the message, like
        # 'Bug 1458766 [wpt PR 10812] - [LayoutNG] ...'.
        # NOTE: Bugs with the BMO bug # at the end will still get the wrong ID,
        # such as:
        # '[wpt PR 10812] blah blah (bug 1111111) r=foo'
        bug_id = parse_bugs(summary)[0]
        log.debug(f'changeset {changeset}: parsed bug ID {bug_id}')
    except IndexError:
        log.info(
            f'could not determine review system for changeset {changeset}: unable to '
            f'find a bug id in the changeset summary')
        sentry.captureMessage(
            "could not determine review system for changeset",
            level=logging.INFO)
        return ReviewSystem.unknown

    try:
        attachments = fetch_attachments(bug_id)
        bug_history = fetch_bug_history(bug_id)
    except NotAuthorized:
        log.info(f'changeset {changeset}: not authorized to view bug {bug_id}')
        # For reporting purposes explicitly lump commits with confidential
        # bugs in with commits that have a 'no bug - do stuff' summary line.
        return ReviewSystem.no_bug
    except requests.exceptions.HTTPError as err:
        log.info(
            f'could not determine review system for changeset {changeset} with bug '
            f'{bug_id}: {err}')
        sentry.captureMessage(
            "could not determine review system for changeset",
            level=logging.INFO)
        return ReviewSystem.unknown

    review_attachments = collect_review_attachments(attachments)

    if has_phab_markers(review_attachments):
        return ReviewSystem.phabricator

    # Check for a review using just BMO attachments, e.g. splinter
    if has_bmo_patch_review_markers(review_attachments, bug_history):
        return ReviewSystem.bmo

    log.info(
        f'could not determine review system for changeset {changeset} with bug '
        f'{bug_id}: the changeset is missing all known review system markers')
    sentry.captureMessage("could not determine review system for changeset",
                          level=logging.INFO)
    return ReviewSystem.unknown
    def test_bug(self):
        self.assertEqual(parse_bugs('bug 1'), [1])
        self.assertEqual(parse_bugs('bug 123456'), [123456])
        self.assertEqual(parse_bugs('testb=1234x'), [])
        self.assertEqual(parse_bugs('ab4665521e2f'), [])
        self.assertEqual(parse_bugs('Aug 2008'), [])
        self.assertEqual(parse_bugs('b=#12345'), [12345])
        self.assertEqual(parse_bugs('GECKO_191a2_20080815_RELBRANCH'), [])
        self.assertEqual(parse_bugs('12345 is a bug'), [12345])
        self.assertEqual(parse_bugs(' 123456 whitespace!'), [123456])

        # Duplicate bug numbers should be stripped.
        msg = '''Bug 1235097 - Add support for overriding the site root

On brasstacks, `web.ctx.home` is incorrect (see bug 1235097 comment 23), which
means that the URL used by mohawk to verify the authenticated request hashes
differs from that used to generate the hash.'''
        self.assertEqual(parse_bugs(msg), [1235097])

        # Merge numbers should not be considered bug numbers.
        msg = '''servo: Merge #19754 - Implement element.innerText getter (from ferjm:innertext); r=mbrubeck

Source-Repo: https://github.com/servo/servo
Source-Revision: 9e64008e759a678a3971d04977c2b20b66fa8229'''
        self.assertEqual(parse_bugs(msg), [])

        msg = '''Bug 123456 - Fix all of the things

Source-Repo: https://github.com/mozilla/foo'''
        self.assertEqual(parse_bugs(msg), [123456])

        msg = '''Merge #4256

This fixes #9000 and bug 324521

Source-Repo: https://github.com/mozilla/foo'''
        self.assertEqual(parse_bugs(msg), [324521])
Ejemplo n.º 38
0
def addmetadata(repo, ctx, d, onlycheap=False):
    """Add changeset metadata for hgweb templates."""
    description = encoding.fromlocal(ctx.description())

    d['bugs'] = []
    for bug in commitparser.parse_bugs(description):
        d['bugs'].append({
            'no':
            str(bug),
            'url':
            'https://bugzilla.mozilla.org/show_bug.cgi?id=%s' % bug,
        })

    d['reviewers'] = []
    for reviewer in commitparser.parse_reviewers(description):
        d['reviewers'].append({
            'name': reviewer,
            'revset': 'reviewer(%s)' % reviewer,
        })

    d['backsoutnodes'] = []
    backouts = commitparser.parse_backouts(description)
    if backouts:
        for node in backouts[0]:
            try:
                bctx = repo[node]
                d['backsoutnodes'].append({'node': bctx.hex()})
            except error.RepoLookupError:
                pass

    # Repositories can define which TreeHerder repository they are associated
    # with.
    treeherder = repo.ui.config('mozilla', 'treeherder_repo')
    if treeherder:
        d['treeherderrepourl'] = 'https://treeherder.mozilla.org/#/jobs?repo=%s' % treeherder
        d['treeherderrepo'] = treeherder

        push = repo.pushlog.pushfromchangeset(ctx)
        # Don't print Perfherder link on non-publishing repos (like Try)
        # because the previous push likely has nothing to do with this
        # push.
        if push and push.nodes and repo.ui.configbool('phases', 'publish',
                                                      True):
            lastpushhead = repo[push.nodes[0]].hex()
            d['perfherderurl'] = (
                'https://treeherder.mozilla.org/perf.html#/compare?'
                'originalProject=%s&'
                'originalRevision=%s&'
                'newProject=%s&'
                'newRevision=%s') % (treeherder, push.nodes[-1], treeherder,
                                     lastpushhead)

    # If this changeset was converted from another one and we know which repo
    # it came from, add that metadata.
    convertrevision = ctx.extra().get('convert_revision')
    if convertrevision:
        sourcerepo = repo.ui.config('hgmo', 'convertsource')
        if sourcerepo:
            d['convertsourcepath'] = sourcerepo
            d['convertsourcenode'] = convertrevision

    if onlycheap:
        return

    # Obtain the Gecko/app version/milestone.
    #
    # We could probably only do this if the repo is a known app repo (by
    # looking at the initial changeset). But, path based lookup is relatively
    # fast, so just do it. However, we need this in the "onlycheap"
    # section because resolving manifests is relatively slow and resolving
    # several on changelist pages may add seconds to page load times.
    try:
        fctx = repo.filectx('config/milestone.txt', changeid=ctx.node())
        lines = fctx.data().splitlines()
        lines = [l for l in lines if not l.startswith('#') and l.strip()]

        if lines:
            d['milestone'] = lines[0].strip()
    except error.LookupError:
        pass

    # Look for changesets that back out this one.
    #
    # We limit the distance we search for backouts because an exhaustive
    # search could be very intensive. e.g. you load up the root commit
    # on a repository with 200,000 changesets and that commit is never
    # backed out. This finds most backouts because backouts typically happen
    # shortly after a bad commit is introduced.
    thisshort = short(ctx.node())
    count = 0
    searchlimit = repo.ui.configint('hgmo', 'backoutsearchlimit', 100)
    for bctx in repo.set('%ld::', [ctx.rev()]):
        count += 1
        if count >= searchlimit:
            break

        backouts = commitparser.parse_backouts(
            encoding.fromlocal(bctx.description()))
        if backouts and thisshort in backouts[0]:
            d['backedoutbynode'] = bctx.hex()
            break
Ejemplo n.º 39
0
def phabsend(ui, repo, *revs, **opts):
    """upload changesets to Phabricator

    If there are multiple revisions specified, they will be send as a stack
    with a linear dependencies relationship using the order specified by the
    revset.

    For the first time uploading changesets, local tags will be created to
    maintain the association. After the first time, phabsend will check
    obsstore and tags information so it can figure out whether to update an
    existing Differential Revision, or create a new one.

    If --amend is set, update commit messages so they have the
    ``Differential Revision`` URL, remove related tags. This is similar to what
    arcanist will do, and is more desired in author-push workflows. Otherwise,
    use local tags to record the ``Differential Revision`` association.

    The --confirm option lets you confirm changesets before sending them. You
    can also add following to your configuration file to make it default
    behaviour::

        [phabsend]
        confirm = true

    phabsend will check obsstore and the above association to decide whether to
    update an existing Differential Revision, or create a new one.
    """
    revs = list(revs) + opts.get(b'rev', [])
    revs = scmutil.revrange(repo, revs)

    if not revs:
        raise error.Abort(_(b'phabsend requires at least one changeset'))
    if opts.get(b'amend'):
        cmdutil.checkunfinished(repo)

    # {newnode: (oldnode, olddiff, olddrev}
    oldmap = getoldnodedrevmap(repo, [repo[r].node() for r in revs])

    confirm = ui.configbool(b'phabsend', b'confirm')
    confirm |= bool(opts.get(b'confirm'))
    if confirm:
        confirmed = _confirmbeforesend(repo, revs, oldmap)
        if not confirmed:
            raise error.Abort(_(b'phabsend cancelled'))

    actions = []
    reviewers = opts.get(b'reviewer', [])
    if reviewers:
        phids = user_group_phids(repo, reviewers)
        actions.append({b'type': b'reviewers.add', b'value': phids})

    drevids = [] # [int]
    diffmap = {} # {newnode: diff}

    # Send patches one by one so we know their Differential Revision IDs and
    # can provide dependency relationship
    lastrevid = None
    for rev in revs:
        ui.debug(b'sending rev %d\n' % rev)
        ctx = repo[rev]

        acts = list(actions)

        reviewers = list(commitparser.parse_reviewers(ctx.description()))
        if reviewers:
            phids = user_group_phids(repo, reviewers)
            acts.append({b'type': b'reviewers.add', b'value': phids})

        bugs = commitparser.parse_bugs(ctx.description())
        if bugs:
            acts.append({b'type': b'bugzilla.bug-id',
                         b'value': str(bugs[0]).encode('utf-8')})

        # Get Differential Revision ID
        oldnode, olddiff, revid = oldmap.get(ctx.node(), (None, None, None))
        if oldnode != ctx.node() or opts.get(b'amend'):
            # Create or update Differential Revision
            revision, diff = createdifferentialrevision(
                ctx, revid, lastrevid, oldnode, olddiff, acts)
            diffmap[ctx.node()] = diff
            newrevid = int(revision[r'object'][r'id'])
            if revid:
                action = b'updated'
            else:
                action = b'created'

            # Create a local tag to note the association, if commit message
            # does not have it already
            m = _differentialrevisiondescre.search(ctx.description())
            if not m or int(m.group(b'id')) != newrevid:
                tagname = b'D%d' % newrevid
                tags.tag(repo, tagname, ctx.node(), message=None, user=None,
                         date=None, local=True)
        else:
            # Nothing changed. But still set "newrevid" so the next revision
            # could depend on this one.
            newrevid = revid
            action = b'skipped'

        actiondesc = ui.label(
            {b'created': _(b'created'),
             b'skipped': _(b'skipped'),
             b'updated': _(b'updated')}[action],
            b'phabricator.action.%s' % action)
        drevdesc = ui.label(b'D%s' % newrevid, b'phabricator.drev')
        nodedesc = ui.label(bytes(ctx), b'phabricator.node')
        desc = ui.label(ctx.description().split(b'\n')[0], b'phabricator.desc')
        ui.write(_(b'%s - %s - %s: %s\n') % (drevdesc, actiondesc, nodedesc,
                                             desc))
        drevids.append(newrevid)
        lastrevid = newrevid

    # Update commit messages and remove tags
    if opts.get(b'amend'):
        unfi = repo.unfiltered()
        drevs = callconduit(repo, b'differential.query', {b'ids': drevids})
        with repo.wlock(), repo.lock(), repo.transaction(b'phabsend'):
            wnode = unfi[b'.'].node()
            mapping = {} # {oldnode: [newnode]}
            for i, rev in enumerate(revs):
                old = unfi[rev]
                drevid = drevids[i]
                drev = [d for d in drevs if int(d[r'id']) == drevid][0]
                newdesc = getdescfromdrev(drev)
                # Make sure commit message contain "Differential Revision"
                if old.description() != newdesc:
                    parents = [
                        mapping.get(old.p1().node(), (old.p1(),))[0],
                        mapping.get(old.p2().node(), (old.p2(),))[0],
                    ]
                    new = context.metadataonlyctx(
                        repo, old, parents=parents, text=newdesc,
                        user=old.user(), date=old.date(), extra=old.extra())

                    newnode = new.commit()

                    mapping[old.node()] = [newnode]
                    # Update diff property
                    writediffproperties(unfi[newnode], diffmap[old.node()])
                # Remove local tags since it's no longer necessary
                tagname = b'D%d' % drevid
                if tagname in repo.tags():
                    tags.tag(repo, tagname, nullid, message=None, user=None,
                             date=None, local=True)
            scmutil.cleanupnodes(repo, mapping, b'phabsend')
            if wnode in mapping:
                unfi.setparents(mapping[wnode][0])
Ejemplo n.º 40
0
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')

    havereviewers = bool(nodes)
    for node in nodes:
        rd = reviewdata[nodereviews[node]]
        if not rd.get('reviewers', None):
            havereviewers = False
            break

    # Make it clear to the user that they need to take action in order for
    # others to see this review series.
    if havedraft:
        # If the series is ready for publishing, prompt the user to perform the
        # publishing.
        if havereviewers:
            caps = getreviewcaps(remote)
            if 'publish' in caps:
                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'))
        else:
            ui.status(_('(review requests lack reviewers; visit review url '
                        'to assign reviewers and publish these review '
                        'requests)\n'))