hgext/firefoxreleases/__init__.py (209 lines of code) (raw):
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.
"""Expose Firefox release information."""
import collections
import os
from mercurial.i18n import _
from mercurial import (
configitems,
error,
extensions,
pycompat,
registrar,
revset,
templateutil,
)
from mercurial.hgweb import (
webcommands,
webutil,
)
OUR_DIR = os.path.normpath(os.path.dirname(__file__))
with open(os.path.join(OUR_DIR, "..", "bootstrap.py")) as f:
exec(f.read())
import mozautomation.releasedb as releasedb
from mozhg.util import (
is_firefox_repo,
)
minimumhgversion = b"4.8"
testedwith = b"4.8 4.9 5.0 5.1 5.2 5.3 5.4 5.5 5.9"
configtable = {}
configitem = registrar.configitem(configtable)
configitem(
b"mozilla",
b"enablefirefoxreleases", # deprecated, use firefox_releasing
default=configitems.dynamicdefault,
)
configitem(b"mozilla", b"firefox_releasing", default=configitems.dynamicdefault)
configitem(b"mozilla", b"firefoxreleasesdb", default=configitems.dynamicdefault)
revsetpredicate = registrar.revsetpredicate()
def extsetup(ui):
extensions.wrapfunction(webutil, "changesetentry", changesetentry)
webcommands.firefoxreleases = webcommands.webcommand(b"firefoxreleases")(
firefox_releases_web_command
)
def db_for_repo(repo):
"""Obtain a FirefoxReleaseDatabase for a repo or None."""
if not repo.local():
return None
if not repo.ui.configbool(
b"mozilla", b"enablefirefoxreleases", False
) and not repo.ui.configbool( # deprecated
b"mozilla", b"firefox_releasing", False
):
return None
if not is_firefox_repo(repo):
return None
default = repo.vfs.join(b"firefoxreleases.db")
path = repo.ui.config(b"mozilla", b"firefoxreleasesdb", default)
if not os.path.exists(path):
return None
return releasedb.FirefoxReleaseDatabase(
pycompat.sysstr(path), bytestype=pycompat.bytestr
)
def release_builds(db, repo, filter_unknown_revision=True):
"""Obtain Firefox release builds.
By default, only builds associated with revisions in this repo are returned.
"""
for build in db.builds():
if filter_unknown_revision and build[b"revision"] not in repo:
continue
yield build
def release_builds_by_revision(db, repo):
"""Obtain builds indexed by integer revision."""
by_rev = collections.defaultdict(list)
for build in db.builds():
try:
ctx = repo[build[b"revision"]]
except error.RepoLookupError:
continue
by_rev[ctx.rev()].append(build)
return by_rev
def release_configurations(db, repo):
"""Obtain a set of release configurations seen in this repo.
Essentially, finds the set of (channel, platform) tuples that are present
in this repo.
"""
return db.unique_release_configurations(
fltr=lambda build: build[b"revision"] in repo
)
def _releases_mapped_generator(context, builds):
"""Generates build object mappings for use in the template layer"""
for i, build in enumerate(builds):
build[b"parity"] = pycompat.bytestr(i % 2)
build[b"anchor"] = releasedb.build_anchor(build)
yield build
def firefox_releases_web_command(web):
"""Show information about Firefox releases."""
req = web.req
repo = web.repo
db = db_for_repo(repo)
if not db:
error_message = b"Firefox release info not available"
return web.sendtemplate(b"error", error=error_message)
platform = req.qsparams[b"platform"] if b"platform" in req.qsparams else None
builds = []
for build in release_builds(db, repo):
if platform and build[b"platform"] != platform:
continue
builds.append(build)
releases_mapping_generator = templateutil.mappinggenerator(
_releases_mapped_generator, args=(builds,)
)
return web.sendtemplate(b"firefoxreleases", releases=releases_mapping_generator)
def release_config(build):
return build[b"channel"], build[b"platform"]
def changesetentry(orig, web, ctx):
"""Add metadata for an individual changeset in hgweb."""
d = orig(web, ctx)
d = pycompat.byteskwargs(d)
repo = web.repo
db = db_for_repo(repo)
if not db:
return pycompat.strkwargs(d)
releases = release_info_for_changeset(db, repo, ctx)
if releases[b"this"]:
d[b"firefox_releases_here"] = []
d[b"firefox_releases_first"] = []
for config, build in sorted(releases[b"this"].items()):
build[b"anchor"] = releasedb.build_anchor(build)
# Set links to previous and future releases.
if config in releases[b"previous"]:
build[b"previousnode"] = releases[b"previous"][config][b"revision"]
d[b"firefox_releases_here"].append(build)
d[b"firefox_releases_first"].append(build)
if releases[b"future"]:
d.setdefault(b"firefox_releases_first", [])
for config, build in sorted(releases[b"future"].items()):
build[b"anchor"] = releasedb.build_anchor(build)
if build not in d[b"firefox_releases_first"]:
d[b"firefox_releases_first"].append(build)
if releases[b"previous"]:
d[b"firefox_releases_last"] = []
for config, build in sorted(releases[b"previous"].items()):
build[b"anchor"] = releasedb.build_anchor(build)
d[b"firefox_releases_last"].append(build)
# Used so we don't display "first release with" and "last release without".
# We omit displaying in this scenario because we're not confident in the
# data and don't want to take chances with inaccurate data.
if b"firefox_releases_first" in d and b"firefox_releases_last" in d:
d[b"have_first_and_last_firefox_releases"] = True
# Do some template fixes
# TODO build via a generator
if b"firefox_releases_first" in d:
d[b"firefox_releases_first"] = templateutil.mappinglist(
d[b"firefox_releases_first"]
)
if b"firefox_releases_last" in d:
d[b"firefox_releases_last"] = templateutil.mappinglist(
d[b"firefox_releases_last"]
)
if b"firefox_releases_here" in d:
d[b"firefox_releases_here"] = templateutil.mappinglist(
d[b"firefox_releases_here"]
)
return pycompat.strkwargs(d)
def release_info_for_changeset(db, repo, ctx):
"""Given a changeset, obtain relevant release info."""
# Find the previous release before this changeset. We walk ancestors
# and store the first seen build entry for each release configuration.
with db.cache_builds():
revisions = release_builds_by_revision(db, repo)
configs = release_configurations(db, repo)
previous_releases = {}
cl = repo.changelog
for rev in cl.ancestors([ctx.rev()]):
if rev not in revisions:
continue
for build in revisions[rev]:
config = release_config(build)
previous_releases.setdefault(config, build)
# Found all release configurations. All previous releases identified.
if len(configs) == len(previous_releases):
break
# Find releases on exactly this changeset.
this_releases = {}
for build in revisions.get(ctx.rev(), []):
this_releases[release_config(build)] = build
# Now find the first releases with this changeset. This is similar to above
# except we "walk" descendants. Actual descendant walking can be slow
# because data is indexed by ancestors. Since changelog data is ordered
# and we have a mapping from revision to builds, we instead iterate over
# future revisions. When we find a revision with builds, we verify the
# start node is an ancestor otherwise we keep going.
#
# There is potential for this code to consume a lot of CPU. If the
# start revision is early in the repo and we're searching for a config
# that doesn't exist, we could perform many isancestor() checks as we
# traverse to the end of the repo. This can be fixed by capping search
# length. Another strategy would be conditionally checking isancestor()
# if that revision has a config we're interested in. The latter is only
# partial mitigation. But it might be good enough as a first step.
# Of course, if the underlying data is append-only, then the mapping can
# be cached.
first_releases = dict(this_releases)
for rev in cl.revs(ctx.rev()):
if len(configs) == len(first_releases):
break
if rev not in revisions:
continue
# We're not walking descendants. Verify start rev actually is ancestor.
if not cl.isancestor(ctx.node(), repo[rev].node()):
continue
for build in revisions[rev]:
config = release_config(build)
first_releases.setdefault(config, build)
future_releases = {
k: v for k, v in first_releases.items() if k not in this_releases
}
return {
b"previous": previous_releases,
b"this": this_releases,
b"first": first_releases,
b"future": future_releases,
}
@revsetpredicate(b"firefoxrelease")
def revset_firefoxrelease(repo, subset, x):
"""``firefoxrelease([channel=], [platform=])
Changesets that have Firefox releases built from them.
Accepts the following named arguments:
channel
Which release channel to look at. e.g. ``nightly``. Multiple channels
can be delimited by spaces.
platform
Which platform to limit builds to. e.g. ``win32``. Multiple platforms
can be delimited by spaces.
If multiple filters are requested filters are combined using logical AND.
If no filters are specified, all revisions having a Firefox release are
matched.
"""
args = revset.getargsdict(x, b"firefoxrelease", b"channel platform")
channels = set()
if b"channel" in args:
channels = set(
revset.getstring(args[b"channel"], _(b"channel requires a string")).split()
)
platforms = set()
if b"platform" in args:
platforms = set(
revset.getstring(
args[b"platform"], _(b"platform requires a string")
).split()
)
db = db_for_repo(repo)
if not db:
repo.ui.warn(_(b"(warning: firefoxrelease() revset not available)\n"))
return revset.baseset()
def get_revs():
for rev, builds in release_builds_by_revision(db, repo).items():
for build in builds:
if channels and build[b"channel"] not in channels:
continue
if platforms and build[b"platform"] not in platforms:
continue
yield rev
break
return subset & revset.generatorset(get_revs())