示例#1
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
示例#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
示例#3
0
def push(orig, repo, remote, force=False, revs=None, newbranch=False, **kwargs):
    # If no arguments are specified to `hg push`, Mercurial's default
    # behavior is to try to push all non-remote changesets. The Firefox
    # trees all have hooks that prevent new heads from being created.
    # This default Mercurial behavior can really cause problems when people
    # are doing multi-headed development (e.g. bookmark-based development
    # instead of mq). So, we silently change the default behavior of
    # `hg push` to only push the current changeset when pushing to a Firefox
    # repo.
    tree = resolve_uri_to_tree(remote.url())
    if tree and not revs:
        repo.ui.status(_('no revisions specified to push; '
            'using . to avoid pushing multiple heads\n'))
        revs = [repo['.'].node()]

    res = orig(repo, remote, force=force, revs=revs, newbranch=newbranch,
            **kwargs)

    # If we push to a known tree, update the remote refs.
    # We can ignore result of the push because updateremoterefs() doesn't care:
    # it merely synchronizes state with the remote. Worst case it is a no-op.
    if tree:
        updateremoterefs(repo, remote, tree.encode('utf-8'))

    return res
示例#4
0
def push(orig, repo, remote, force=False, revs=None, newbranch=False, **kwargs):
    # If no arguments are specified to `hg push`, Mercurial's default
    # behavior is to try to push all non-remote changesets. The Firefox
    # trees all have hooks that prevent new heads from being created.
    # This default Mercurial behavior can really cause problems when people
    # are doing multi-headed development (e.g. bookmark-based development
    # instead of mq). So, we silently change the default behavior of
    # `hg push` to only push the current changeset when pushing to a Firefox
    # repo.
    tree = resolve_uri_to_tree(remote.url())
    if tree and not revs:
        repo.ui.status(_('no revisions specified to push; '
            'using . to avoid pushing multiple heads\n'))
        revs = [repo[b'.'].node()]

    res = orig(repo, remote, force=force, revs=revs, newbranch=newbranch,
            **kwargs)

    # If we push to a known tree, update the remote refs.
    # We can ignore result of the push because updateremoterefs() doesn't care:
    # it merely synchronizes state with the remote. Worst case it is a no-op.
    if tree:
        updateremoterefs(repo, remote, tree.encode('utf-8'))

    return res
示例#5
0
def prepushoutgoinghook(local, remote, outgoing):
    """Hook that prevents us from attempting to push multiple heads.

    Firefox repos have hooks that prevent receiving multiple heads. Waiting
    for the hook to fire on the remote wastes time. Implement it locally.
    """
    tree = resolve_uri_to_tree(remote.url())
    if not tree or tree == 'try':
        return

    if len(outgoing.missingheads) > 1:
        raise util.Abort(_('cannot push multiple heads to a Firefox tree; '
            'limit pushed revisions using the -r argument'))
示例#6
0
def pull(orig, repo, remote, *args, **kwargs):
    old_rev = len(repo)
    res = orig(repo, remote, *args, **kwargs)

    if not isfirefoxrepo(repo):
        return res

    lock = repo.lock()
    try:
        if remote.capable('firefoxtrees'):
            lines = remote._call('firefoxtrees').splitlines()
            oldtags = {}
            for tag, node, tree, uri in get_firefoxtrees(repo):
                oldtags[tag] = node
            newtags = {}
            for line in lines:
                tag, node = line.split()
                newtags[tag] = node

                node = bin(node)

                if oldtags.get(tag, None) == node:
                    continue

                repo.firefoxtrees[tag] = node

                between = None
                if tag in oldtags:
                    between = len(list(repo.revs('%s::%s' % (
                        hex(oldtags[tag]), hex(node))))) - 1

                    if not between:
                        continue

                msg = _('updated firefox tree tag %s') % tag
                if between:
                    msg += _(' (+%d commits)') % between
                msg += '\n'
                repo.ui.status(msg)

            writefirefoxtrees(repo)

        tree = resolve_uri_to_tree(remote.url())
        if tree:
            tree = tree.encode('utf-8')
            updateremoterefs(repo, remote, tree)
    finally:
        lock.release()

    return res
示例#7
0
def isfirefoxrepo(repo):
    """Whether a repository is a Firefox repository.

    A Firefox repository is a peer that has a URL of a known tree or a local
    repository whose initial commit is the well-known initial Firefox commit.
    """
    tree = resolve_uri_to_tree(repo.url())
    if tree:
        return True

    if len(repo) and repo[0].hex() in (MOZ_ROOT_REV, COMM_ROOT_REV):
        return True

    # Backdoor for testing.
    return repo.opener.exists('IS_FIREFOX_REPO')
示例#8
0
def isfirefoxrepo(repo):
    """Whether a repository is a Firefox repository.

    A Firefox repository is a peer that has a URL of a known tree or a local
    repository whose initial commit is the well-known initial Firefox commit.
    """
    tree = resolve_uri_to_tree(repo.url())
    if tree:
        return True

    if len(repo) and repo[0].hex() in (MOZ_ROOT_REV, COMM_ROOT_REV):
        return True

    # Backdoor for testing.
    return repo.opener.exists('IS_FIREFOX_REPO')
示例#9
0
def push(orig, repo, remote, *args, **kwargs):
    if not hasattr(repo, 'changetracker'):
        return orig(repo, remote, *args, **kwargs)

    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)

    finally:
        lock.release()

    return res
示例#10
0
def prepushoutgoinghook(*args):
    """Hook that prevents us from attempting to push multiple heads.

    Firefox repos have hooks that prevent receiving multiple heads. Waiting
    for the hook to fire on the remote wastes time. Implement it locally.
    """
    remote = args[0].remote
    outgoing = args[0].outgoing

    tree = resolve_uri_to_tree(remote.url())
    if not tree or tree == 'try':
        return

    if len(outgoing.missingheads) > 1:
        raise error.Abort(_('cannot push multiple heads to a Firefox tree; '
            'limit pushed revisions using the -r argument'))
示例#11
0
def push(orig, repo, remote, *args, **kwargs):
    if not hasattr(repo, 'changetracker'):
        return orig(repo, remote, *args, **kwargs)

    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)

    finally:
        lock.release()

    return res
示例#12
0
def isfirefoxrepo(repo):
    """Whether a repository is a Firefox repository.

    A Firefox repository is a peer that has a URL of a known tree or a local
    repository whose initial commit is the well-known initial Firefox commit.
    """
    tree = resolve_uri_to_tree(repo.url())
    if tree:
        return True

    try:
        if len(repo) and repo[0].hex() in (MOZ_ROOT_REV, COMM_ROOT_REV):
            return True
    except error.FilteredRepoLookupError:
        pass

    # Backdoor for testing.
    return repo.vfs.exists(b'IS_FIREFOX_REPO')
示例#13
0
def exchangepullpushlog(orig, pullop):
    res = orig(pullop)

    if not pullop.remote.capable('pushlog'):
        return res

    # stepsdone added in Mercurial 3.2.
    if hasattr(pullop, 'stepsdone') and 'pushlog' in pullop.stepsdone:
        return res

    repo = pullop.repo

    tree = resolve_uri_to_tree(pullop.remote.url())
    if not tree or not repo.changetracker or tree == "try":
        return res

    repo.ui.status('fetching pushlog\n')
    repo.changetracker.load_pushlog(tree)

    return res
示例#14
0
def wrappedpullobsolete(orig, pullop):
    res = orig(pullop)

    repo = pullop.repo
    remote = pullop.remote

    if not isfirefoxrepo(repo):
        return res

    if remote.capable('firefoxtrees'):
        bmstore = bookmarks.bmstore(repo)
        # remote.local() returns a localrepository or None. If local,
        # just pass it into the wire protocol command/function to simulate
        # the remote command call.
        if remote.local():
            lines = firefoxtrees(remote.local(), None).splitlines()
        else:
            lines = remote._call('firefoxtrees').splitlines()
        oldtags = {}
        for tag, node, tree, uri in get_firefoxtrees(repo):
            oldtags[tag] = node
        newtags = {}
        changes = []
        for line in lines:
            tag, node = line.split()
            newtags[tag] = node

            node = bin(node)

            # A local bookmark of the incoming tag name is already set.
            # Wipe it out - the server takes precedence.
            if tag in bmstore:
                oldtags[tag] = bmstore[tag]
                repo.ui.status('(removing bookmark on %s matching firefoxtree %s)\n' %
                               (short(bmstore[tag]), tag))

                changes.append((tag, None))

                if bmstore.active == tag:
                    repo.ui.status('(deactivating bookmark %s)\n' % tag)
                    bookmarks.deactivate(repo)

            if oldtags.get(tag, None) == node:
                continue

            repo.firefoxtrees[tag] = node

            between = None
            if tag in oldtags:
                between = len(repo.revs('%n::%n', oldtags[tag], node)) - 1

                if not between:
                    continue

            msg = _('updated firefox tree tag %s') % tag
            if between:
                msg += _(' (+%d commits)') % between
            msg += '\n'
            repo.ui.status(msg)

        if changes:
            bmstore.applychanges(repo, pullop.gettransaction(), changes)

        writefirefoxtrees(repo)

    tree = resolve_uri_to_tree(remote.url())
    if tree:
        tree = tree.encode('utf-8')
        updateremoterefs(repo, remote, tree)

    return res
示例#15
0
def exchangepullpushlog(orig, pullop):
    res = orig(pullop)

    if not pullop.remote.capable('pushlog'):
        return res

    # stepsdone added in Mercurial 3.2.
    if hasattr(pullop, 'stepsdone') and 'pushlog' in pullop.stepsdone:
        return res

    repo = pullop.repo

    tree = resolve_uri_to_tree(pullop.remote.url())
    if not tree or not repo.changetracker or tree == "try":
        return res

    # Calling wire protocol commands via SSH requires the server-side wire
    # protocol code to be known by the client. The server-side code is defined
    # by the pushlog extension, so we effectively need the pushlog extension
    # enabled to call the wire protocol method when pulling via SSH. We don't
    # (yet) recommend installing the pushlog extension locally. Furthermore,
    # pulls from hg.mozilla.org should be performed via https://, not ssh://.
    # So just bail on pushlog fetching if pulling via ssh://.
    if isinstance(pullop.remote, sshpeer.sshpeer):
        pullop.repo.ui.warn('cannot fetch pushlog when pulling via ssh://; '
                            'you should be pulling via https://\n')
        return res

    lastpushid = repo.changetracker.last_push_id(tree)
    fetchfrom = lastpushid + 1 if lastpushid is not None else 0

    lines = pullop.remote._call('pushlog', firstpush=str(fetchfrom))
    lines = iter(lines.splitlines())

    statusline = lines.next()
    if statusline[0] == '0':
        raise error.Abort('remote error fetching pushlog: %s' % lines.next())
    elif statusline != '1':
        raise error.Abort('error fetching pushlog: unexpected response: %s\n' %
            statusline)

    pushes = []
    for line in lines:
        pushid, who, when, nodes = line.split(' ', 3)
        nodes = [bin(n) for n in nodes.split()]

        # Verify incoming changesets are known and stop processing when we see
        # an unknown changeset. This can happen when we're pulling a former
        # head instead of all changesets.
        try:
            [repo[n] for n in nodes]
        except error.RepoLookupError:
            repo.ui.warn('received pushlog entry for unknown changeset; ignoring\n')
            break

        pushes.append((int(pushid), who, int(when), nodes))

    if pushes:
        repo.changetracker.add_pushes(tree, pushes)
        repo.ui.status('added %d pushes\n' % len(pushes))

    return res
示例#16
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
示例#17
0
def exchangepullpushlog(orig, pullop):
    res = orig(pullop)

    if not pullop.remote.capable('pushlog'):
        return res

    # stepsdone added in Mercurial 3.2.
    if hasattr(pullop, 'stepsdone') and 'pushlog' in pullop.stepsdone:
        return res

    repo = pullop.repo

    tree = resolve_uri_to_tree(pullop.remote.url())
    if not tree or not repo.changetracker or tree == "try":
        return res

    # Calling wire protocol commands via SSH requires the server-side wire
    # protocol code to be known by the client. The server-side code is defined
    # by the pushlog extension, so we effectively need the pushlog extension
    # enabled to call the wire protocol method when pulling via SSH. We don't
    # (yet) recommend installing the pushlog extension locally. Furthermore,
    # pulls from hg.mozilla.org should be performed via https://, not ssh://.
    # So just bail on pushlog fetching if pulling via ssh://.
    if isinstance(pullop.remote, sshpeer.sshpeer):
        pullop.repo.ui.warn('cannot fetch pushlog when pulling via ssh://; '
                            'you should be pulling via https://\n')
        return res

    lastpushid = repo.changetracker.last_push_id(tree)
    fetchfrom = lastpushid + 1 if lastpushid is not None else 0

    lines = pullop.remote._call('pushlog', firstpush=str(fetchfrom))
    lines = iter(lines.splitlines())

    statusline = lines.next()
    if statusline[0] == '0':
        raise error.Abort('remote error fetching pushlog: %s' % lines.next())
    elif statusline != '1':
        raise error.Abort('error fetching pushlog: unexpected response: %s\n' %
                          statusline)

    pushes = []
    for line in lines:
        pushid, who, when, nodes = line.split(' ', 3)
        nodes = [bin(n) for n in nodes.split()]

        # Verify incoming changesets are known and stop processing when we see
        # an unknown changeset. This can happen when we're pulling a former
        # head instead of all changesets.
        try:
            [repo[n] for n in nodes]
        except error.RepoLookupError:
            repo.ui.warn(
                'received pushlog entry for unknown changeset; ignoring\n')
            break

        pushes.append((int(pushid), who, int(when), nodes))

    if pushes:
        repo.changetracker.add_pushes(tree, pushes)
        repo.ui.status('added %d pushes\n' % len(pushes))

    return res
def pull(orig, repo, remote, *args, **kwargs):
    old_rev = len(repo)
    res = orig(repo, remote, *args, **kwargs)

    if not isfirefoxrepo(repo):
        return res

    lock = repo.lock()
    try:
        if remote.capable('firefoxtrees'):
            lines = remote._call('firefoxtrees').splitlines()
            oldtags = repo.tags()
            newtags = {}
            for line in lines:
                tag, node = line.split()
                newtags[tag] = node

                node = bin(node)

                if oldtags.get(tag, None) == node:
                    continue

                repo.tag(tag, node, message=None, local=True,
                        user=None, date=None)
                between = None
                if tag in oldtags:
                    between = len(list(repo.revs('%s::%s' % (
                        hex(oldtags[tag]), hex(node))))) - 1

                    if not between:
                        continue

                msg = _('updated firefox tree tag %s') % tag
                if between:
                    msg += _(' (+%d commits)') % between
                msg += '\n'
                repo.ui.status(msg)

            # repo.tag will produce multiple entries for a tag. Prune
            # the old ones.
            localdata = repo.opener.tryread('localtags')
            newlines = []
            for line in localdata.splitlines():
                line = line.strip()
                node, tag = line.split()

                if tag not in newtags or newtags[tag] != node:
                    continue

                newlines.append(line)
            if newlines:
                newlines.append('')
            if newlines:
                repo.opener.write('localtags', '\n'.join(newlines))

        tree = resolve_uri_to_tree(remote.url())
        if tree:
            tree = tree.encode('utf-8')
            updateremoterefs(repo, remote, tree)
    finally:
        lock.release()

    return res
示例#19
0
def wrappedpullobsolete(orig, pullop):
    res = orig(pullop)

    repo = pullop.repo
    remote = pullop.remote

    if not isfirefoxrepo(repo):
        return res

    if remote.capable('firefoxtrees'):
        bmstore = bookmarks.bmstore(repo)
        # remote.local() returns a localrepository or None. If local,
        # just pass it into the wire protocol command/function to simulate
        # the remote command call.
        if remote.local():
            lines = firefoxtrees(remote.local(), None).splitlines()
        else:
            lines = remote._call('firefoxtrees').splitlines()
        oldtags = {}
        for tag, node, tree, uri in get_firefoxtrees(repo):
            oldtags[tag] = node
        newtags = {}
        for line in lines:
            tag, node = line.split()
            newtags[tag] = node

            node = bin(node)

            # A local bookmark of the incoming tag name is already set.
            # Wipe it out - the server takes precedence.
            if tag in bmstore:
                oldtags[tag] = bmstore[tag]
                repo.ui.status('(removing bookmark on %s matching firefoxtree %s)\n' %
                               (short(bmstore[tag]), tag))
                del bmstore[tag]
                bmstore.recordchange(pullop.trmanager.transaction())

                if bmstore.active == tag:
                    repo.ui.status('(deactivating bookmark %s)\n' % tag)
                    bookmarks.deactivate(repo)

            if oldtags.get(tag, None) == node:
                continue

            repo.firefoxtrees[tag] = node

            between = None
            if tag in oldtags:
                between = len(repo.revs('%n::%n', oldtags[tag], node)) - 1

                if not between:
                    continue

            msg = _('updated firefox tree tag %s') % tag
            if between:
                msg += _(' (+%d commits)') % between
            msg += '\n'
            repo.ui.status(msg)

        writefirefoxtrees(repo)

    tree = resolve_uri_to_tree(remote.url())
    if tree:
        tree = tree.encode('utf-8')
        updateremoterefs(repo, remote, tree)

    return res
示例#20
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