sapling/edenscm/hgext/stablerev.py
Jun Wu 9dc21f8d0b codemod: import from the edenscm package
Summary:
D13853115 adds `edenscm/` to `sys.path` and code still uses `import mercurial`.
That has nasty problems if both `import mercurial` and
`import edenscm.mercurial` are used, because Python would think `mercurial.foo`
and `edenscm.mercurial.foo` are different modules so code like
`try: ... except mercurial.error.Foo: ...`, or `isinstance(x, mercurial.foo.Bar)`
would fail to handle the `edenscm.mercurial` version. There are also some
module-level states (ex. `extensions._extensions`) that would cause trouble if
they have multiple versions in a single process.

Change imports to use the `edenscm` so ideally the `mercurial` is no longer
imported at all. Add checks in extensions.py to catch unexpected extensions
importing modules from the old (wrong) locations when running tests.

Reviewed By: phillco

Differential Revision: D13868981

fbshipit-source-id: f4e2513766957fd81d85407994f7521a08e4de48
2019-01-29 17:25:32 -08:00

166 lines
5.3 KiB
Python

# stablerev.py
#
# Copyright 2018 Facebook, Inc.
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.
"""provide a way to expose the "stable" commit via a revset
In some repos, newly pushed commits undergo CI testing continuously. This means
`master` is often in an unknown state until it's tested; an older public commit
(e.g. `master~N`) will be the newest known-good commit. This extension will call
this the "stable" commit.
Using this stable commit instead of master can be useful during development
(e.g. when rebasing, to prevent rebasing onto a broken commit). This extension
provides a revset to access it easily.
The actual implementation of fetching the stable commit hash is left up to the
repository owner. Since the returned hash may not be in the repo, the revset
can optionally pull if the returned commit isn't known locally.
Lastly, it supports taking an optional argument (the "target") that's passed to
the script. This is useful for large repos that contain multiple projects, and
thus multiple stable commits.
"""
import re
import subprocess
from edenscm.mercurial import commands, error, json, registrar
from edenscm.mercurial.i18n import _
from edenscm.mercurial.revsetlang import getargsdict, getstring
from edenscm.mercurial.smartset import baseset
revsetpredicate = registrar.revsetpredicate()
namespacepredicate = registrar.namespacepredicate()
# revspecs can be hashes, rev numbers, bookmark/tag names, etc., so this should
# be permissive:
validrevspec = re.compile(r"([0-9a-z\-_]+)", re.IGNORECASE)
def _getargumentornone(x):
try:
return getstring(x, _("must pass a target"))
except error.ParseError:
return None
def _validatetarget(ui, target):
# The "target" parameter can be used or not depending on the configuration.
# Can be "required", "optional", or "forbidden"
targetconfig = ui.config("stablerev", "targetarg", "forbidden")
if target is None and targetconfig == "required":
raise error.Abort(_("must pass a target"))
elif target is not None and targetconfig == "forbidden":
raise error.Abort(_("targets are not supported in this repo"))
return target
def _execute(ui, repo, target=None):
script = ui.config("stablerev", "script")
if script is None:
raise error.ConfigError(_("must set stablerev.script"))
# Assume the specified script is relative to the repo:
abspath = repo.wvfs.join(script)
args = [abspath]
if target is not None:
args.extend(["--target", str(target)])
try:
p = subprocess.Popen(
args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True
)
return p.communicate()
except subprocess.CalledProcessError as e:
raise error.Abort(_("couldn't fetch stable rev: %s") % e)
def _validaterevspec(ui, node):
"""Verifies the given node looks like a revspec"""
script = ui.config("stablerev", "script")
if len(node) == 0:
raise error.Abort(_("stable rev returned by script (%s) was empty") % script)
if not validrevspec.match(node):
raise error.Abort(_("stable rev returned by script (%s) was invalid") % script)
return node
def _executeandparse(ui, repo, target=None):
stdout, stderr = _execute(ui, repo, target)
# The stderr can optionally provide useful context, so print it.
ui.write_err(stderr)
try:
# Prefer JSON output first.
data = json.loads(stdout)
if "node" in data:
return _validaterevspec(ui, data["node"])
except Exception:
pass
# Fall back to stdout:
return _validaterevspec(ui, stdout.strip())
def _lookup(ui, repo, revspec, trypull=False):
try:
return repo[revspec]
except error.RepoLookupError:
if trypull:
ui.warn(
_("stable commit (%s) not in repo; pulling to get it...\n") % revspec
)
commands.pull(repo.ui, repo)
# Rerun with trypull=False so we'll give up if it doesn't exist.
return _lookup(ui, repo, revspec, trypull=False)
else:
raise error.Abort(
_("stable commit (%s) not in the repo") % revspec,
hint="try hg pull first",
)
@revsetpredicate("getstablerev([target])", safe=False, weight=30)
def getstablerev(repo, subset, x):
"""Returns the "stable" revision.
The script to run is set via config::
[stablerev]
script = scripts/get_stable_rev.py
The revset takes an optional "target" argument that is passed to the
script (as `--target $TARGET`). This argumement can be made `optional`,
`required`, or `forbidden`::
[stablerev]
targetarg = forbidden
The revset can automatically pull if the returned commit doesn't exist
locally::
[stablerev]
pullonmissing = False
"""
ui = repo.ui
target = None
args = getargsdict(x, "getstablerev", "target")
if "target" in args:
target = getstring(args["target"], _("target argument must be a string"))
_validatetarget(ui, target)
revspec = _executeandparse(ui, repo, target)
trypull = ui.configbool("stablerev", "pullonmissing", False)
commitctx = _lookup(ui, repo, revspec, trypull=trypull)
return subset & baseset([commitctx.rev()])