mirror of
https://github.com/facebook/sapling.git
synced 2024-10-13 02:07:31 +03:00
584656dff3
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
310 lines
9.6 KiB
Python
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
|