sapling/hgext/phrevset.py
Jun Wu 584656dff3 codemod: join the auto-formatter party
Summary:
Turned on the auto formatter. Ran `arc lint --apply-patches --take BLACK **/*.py`.
Then run `arc lint` again so some other autofixers like spellchecker etc. looked
at the code base. Manually accept the changes whenever they make sense, or use
a workaround (ex. changing "dict()" to "dict constructor") where autofix is false
positive. Disabled linters on files that are hard (i18n/polib.py) to fix, or less
interesting to fix (hgsubversion tests), or cannot be fixed without breaking
OSS build (FBPYTHON4).

Conflicted linters (test-check-module-imports.t, part of test-check-code.t,
test-check-pyflakes.t) are removed or disabled.

Duplicated linters (test-check-pyflakes.t, test-check-pylint.t) are removed.

An issue of the auto-formatter is lines are no longer guarnateed to be <= 80
chars. But that seems less important comparing with the benefit auto-formatter
provides.

As we're here, also remove test-check-py3-compat.t, as it is currently broken
if `PYTHON3=/bin/python3` is set.

Reviewed By: wez, phillco, simpkins, pkaush, singhsrb

Differential Revision: D8173629

fbshipit-source-id: 90e248ae0c5e6eaadbe25520a6ee42d32005621b
2018-05-25 22:17:29 -07:00

310 lines
9.6 KiB
Python

# phrevset.py - support for Phabricator revsets
#
# Copyright 2013 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.
"""provides support for Phabricator revsets
Allows for queries such as `hg log -r D1234567` to find the commit which
corresponds to a specific Differential revision.
Automatically handles commits already in subversion, or whose hash has
changed since submitting to Differential (due to amends or rebasing).
Requires arcanist to be installed and properly configured.
Repositories should include a callsign in their hgrc.
Example for www:
[phrevset]
callsign = E
"""
import json
import os
import re
import signal
import subprocess
from mercurial import error, extensions, hg, registrar, revset, smartset
from mercurial.i18n import _
try:
from hgsubversion import util as svnutil
except ImportError:
svnutil = None
configtable = {}
configitem = registrar.configitem(configtable)
configitem("phrevset", "callsign", default=None)
DIFFERENTIAL_REGEX = re.compile(
"Differential Revision: http.+?/" # Line start, URL
"D(?P<id>[0-9]+)", # Differential ID, just numeric part
flags=re.LOCALE,
)
DESCRIPTION_REGEX = re.compile(
"Commit r" # Prefix
"(?P<callsign>[A-Z]{1,})" # Callsign
"(?P<id>[a-f0-9]+)", # rev
flags=re.LOCALE,
)
def getdiff(repo, diffid):
"""Perform a Conduit API call by shelling out to `arc`
Returns a subprocess.Popen instance"""
try:
proc = subprocess.Popen(
["arc", "call-conduit", "differential.getdiff"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
preexec_fn=os.setsid,
)
input = json.dumps({"revision_id": diffid})
repo.ui.debug(
"[diffrev] echo '%s' | " "arc call-conduit differential.getdiff\n" % input
)
proc.stdin.write(input)
proc.stdin.close()
return proc
except Exception as e:
raise error.Abort('Could not not call "arc call-conduit": %s' % e)
def finddiff(repo, diffid, proc=None):
"""Scans the changelog for commit lines mentioning the Differential ID
If the optional proc parameter is provided, it must be a subprocess.Popen
instance. It will be polled during the iteration and if it indicates that
the process has returned, the function will raise StopIteration"""
repo.ui.debug("[diffrev] Traversing log for %s\n" % diffid)
# traverse the changelog backwards
for rev in repo.changelog.revs(start=len(repo.changelog), stop=0):
if rev % 100 == 0 and proc and proc.poll() is not None:
raise StopIteration("Parallel proc call completed")
changectx = repo[rev]
desc = changectx.description()
match = DIFFERENTIAL_REGEX.search(desc)
if match and match.group("id") == diffid:
return changectx.rev()
return None
def forksearch(repo, diffid):
"""Perform a log traversal and Conduit call in parallel
Returns a (revisions, arc_response) tuple, where one of the items will be
None, depending on which process terminated first"""
repo.ui.debug("[diffrev] Starting Conduit call\n")
proc = getdiff(repo, diffid)
try:
repo.ui.debug("[diffrev] Starting log walk\n")
rev = finddiff(repo, diffid, proc)
repo.ui.debug("[diffrev] Parallel log walk completed with %s\n" % rev)
os.killpg(proc.pid, signal.SIGTERM)
if rev is None:
# walked the entire repo and couldn't find the diff
raise error.Abort("Could not find diff D%s in changelog" % diffid)
return ([rev], None)
except StopIteration:
# search terminated because arc returned
# if returncode == 0, return arc's output
repo.ui.debug("[diffrev] Conduit call returned %i\n" % proc.returncode)
if proc.returncode != 0:
raise error.Abort("arc call returned status %i" % proc.returncode)
resp = proc.stdout.read()
return (None, resp)
def parsedesc(repo, resp, ignoreparsefailure):
desc = resp["description"]
if desc is None:
if ignoreparsefailure:
return None
else:
raise error.Abort("No Conduit description")
match = DESCRIPTION_REGEX.match(desc)
if not match:
if ignoreparsefailure:
return None
else:
raise error.Abort("Cannot parse Conduit description '%s'" % desc)
callsign = match.group("callsign")
repo_callsign = repo.ui.config("phrevset", "callsign")
if callsign != repo_callsign:
raise error.Abort(
"Diff callsign '%s' is different from repo"
" callsign '%s'" % (callsign, repo_callsign)
)
return match.group("id")
def revsetdiff(repo, subset, diffid):
"""Return a set of revisions corresponding to a given Differential ID """
repo_callsign = repo.ui.config("phrevset", "callsign")
if repo_callsign is None:
msg = _("phrevset.callsign is not set - doing a linear search\n")
hint = _("This will be slow if the diff was not committed recently\n")
repo.ui.warn(msg)
repo.ui.warn(hint)
rev = finddiff(repo, diffid)
if rev is None:
raise error.Abort("Could not find diff D%s in changelog" % diffid)
else:
return [rev]
revs, resp = forksearch(repo, diffid)
if revs is not None:
# The log walk found the diff, nothing more to do
return revs
jsresp = json.loads(resp)
if not jsresp:
raise error.Abort("Could not decode Conduit response")
resp = jsresp.get("response")
if not resp:
e = jsresp.get("errorMessage", "unknown error")
raise error.Abort("Conduit error: %s" % e)
vcs = resp.get("sourceControlSystem")
repo.ui.debug("[diffrev] VCS is %s\n" % vcs)
if vcs == "svn" and svnutil:
# commit has landed in svn, parse the description to get the SVN
# revision and delegate to hgsubversion for the rest
svnrev = parsedesc(repo, resp, ignoreparsefailure=False)
repo.ui.debug("[diffrev] SVN rev is r%s\n" % svnrev)
args = ("string", svnrev)
return svnutil.revset_svnrev(repo, subset, args)
elif vcs == "git":
gitrev = parsedesc(repo, resp, ignoreparsefailure=False)
repo.ui.debug("[diffrev] GIT rev is %s\n" % gitrev)
peerpath = repo.ui.expandpath("default")
remoterepo = hg.peer(repo, {}, peerpath)
remoterev = remoterepo.lookup("_gitlookup_git_%s" % gitrev)
repo.ui.debug("[diffrev] HG rev is %s\n" % remoterev.encode("hex"))
if not remoterev:
repo.ui.debug("[diffrev] Falling back to linear search\n")
linear_search_result = finddiff(repo, diffid)
if linear_search_result is None:
# walked the entire repo and couldn't find the diff
raise error.Abort("Could not find diff D%s in changelog" % diffid)
return [linear_search_result]
return [repo[remoterev].rev()]
elif vcs == "hg":
rev = parsedesc(repo, resp, ignoreparsefailure=True)
if rev:
# The response from phabricator contains a changeset ID.
# Convert it back to a rev number.
try:
node = repo[rev.encode("utf-8")]
except error.RepoLookupError:
raise error.Abort(
"Landed commit for diff D%s not available "
'in current repository: run "hg pull" '
"to retrieve it" % diffid
)
return [node.rev()]
# commit is still local, get its hash
props = resp["properties"]
commits = props["local:commits"]
# the JSON parser returns Unicode strings, convert to `str` in UTF-8
revs = [c["commit"].encode("utf-8") for c in commits.values()]
# verify all revisions exist in the current repo; if not, try to
# find their counterpart by parsing the log
results = set()
for rev in revs:
# TODO: This really should be searching in repo.unfiltered(),
# and then resolving successors if the commit was hidden.
try:
node = repo[rev.encode("utf-8")]
results.add(node.rev())
except error.RepoLookupError:
repo.ui.warn(_("Commit not found - doing a linear search\n"))
parsed_rev = finddiff(repo, diffid)
if not parsed_rev:
raise error.Abort(
"Could not find diff " "D%s in changelog" % diffid
)
results.add(parsed_rev)
if not results:
raise error.Abort("Could not find local commit for D%s" % diffid)
return set(results)
else:
if not vcs:
msg = "D%s does not have an associated version control system\n" "You can view the diff at http://phabricator.fb.com/D%s\n\n"
repo.ui.warn(msg % (diffid, diffid))
return []
else:
raise error.Abort(
"Conduit returned unknown " 'sourceControlSystem "%s"' % vcs
)
def revsetstringset(orig, repo, subset, revstr, *args, **kwargs):
"""Wrapper that recognizes revisions starting with 'D'"""
if revstr.startswith("D") and revstr[1:].isdigit():
return smartset.baseset(revsetdiff(repo, subset, revstr[1:]))
return orig(repo, subset, revstr, *args, **kwargs)
def extsetup(ui):
extensions.wrapfunction(revset, "stringset", revsetstringset)
revset.methods["string"] = revset.stringset
revset.methods["symbol"] = revset.stringset