mirror of
https://github.com/facebook/sapling.git
synced 2024-10-11 09:17:30 +03:00
33b2fbc0bc
Summary: Don't let errors propagate out of the gitnode() revset. Always report errors in gitnode() as a translation failure, rather than letting exceptions propagate up and crash mercurial. The code was previously only catching internally generated ConduitError exceptions, but it can also throw HttpError exceptions, and the underlying httplib code can throw its own exceptions as well as socket.error exceptions. This also fixes the test code to use an ephemeral TCP port rather than assuming port 8543 will always be available. Using a fixed TCP port in test code is a very common way to cause bogus failures if the tests are run in parallel. (For instance, testing multiple repositories in parallel on the same build host.) Test Plan: Added unit tests that check the behavior when the server returns a 500 error, and when the server refuses the connection entirely. Reviewers: quark, durham, rmcelroy Reviewed By: rmcelroy Subscribers: net-systems-diffs@fb.com, yogeshwer, mjpieters Differential Revision: https://phabricator.intern.facebook.com/D4556871 Signature: t1:4556871:1487062142:b58d770d46c975d44933bec08cfce8acb25ff16b
261 lines
7.8 KiB
Python
261 lines
7.8 KiB
Python
# fbconduit.py
|
|
#
|
|
# An extension to query remote servers for extra information via conduit RPC
|
|
#
|
|
# Copyright 2015 Facebook, Inc.
|
|
|
|
from mercurial import (
|
|
error,
|
|
extensions,
|
|
node,
|
|
registrar,
|
|
revset,
|
|
templater,
|
|
)
|
|
from mercurial.i18n import _
|
|
|
|
import re
|
|
import json
|
|
from urllib import urlencode
|
|
from mercurial.util import httplib
|
|
|
|
conduit_host = None
|
|
conduit_path = None
|
|
conduit_protocol = None
|
|
connection = None
|
|
|
|
MAX_CONNECT_RETRIES = 3
|
|
|
|
class ConduitError(Exception):
|
|
pass
|
|
|
|
class HttpError(Exception):
|
|
pass
|
|
|
|
githashre = re.compile('g([0-9a-fA-F]{12,40})')
|
|
phabhashre = re.compile('^r([A-Z]+)([0-9a-f]{12,40})$')
|
|
fbsvnhash = re.compile('^r[A-Z]+(\d+)$')
|
|
|
|
def extsetup(ui):
|
|
if not conduit_config(ui):
|
|
ui.warn(_('No conduit host specified in config; disabling fbconduit\n'))
|
|
return
|
|
templater.funcs['mirrornode'] = mirrornode
|
|
|
|
revset.symbols['gitnode'] = gitnode
|
|
extensions.wrapfunction(revset, 'stringset', overridestringset)
|
|
revset.symbols['stringset'] = revset.stringset
|
|
revset.methods['string'] = revset.stringset
|
|
revset.methods['symbol'] = revset.stringset
|
|
|
|
def conduit_config(ui, host=None, path=None, protocol=None):
|
|
global conduit_host, conduit_path, conduit_protocol
|
|
conduit_host = host or ui.config('fbconduit', 'host')
|
|
conduit_path = path or ui.config('fbconduit', 'path')
|
|
conduit_protocol = protocol or ui.config('fbconduit', 'protocol')
|
|
if conduit_host is None:
|
|
return False
|
|
|
|
if conduit_protocol is None:
|
|
conduit_protocol = 'https'
|
|
|
|
return True
|
|
|
|
def call_conduit(method, **kwargs):
|
|
global connection, conduit_host, conduit_path, conduit_protocol
|
|
|
|
# start connection
|
|
if connection is None:
|
|
if conduit_protocol == 'https':
|
|
connection = httplib.HTTPSConnection(conduit_host)
|
|
elif conduit_protocol == 'http':
|
|
connection = httplib.HTTPConnection(conduit_host)
|
|
|
|
# send request
|
|
path = conduit_path + method
|
|
args = urlencode({'params': json.dumps(kwargs)})
|
|
for attempt in xrange(MAX_CONNECT_RETRIES):
|
|
try:
|
|
connection.request('POST', path, args, {'Connection': 'Keep-Alive'})
|
|
break
|
|
except httplib.HTTPException as e:
|
|
connection.connect()
|
|
else:
|
|
raise e
|
|
|
|
# read http response
|
|
response = connection.getresponse()
|
|
if response.status != 200:
|
|
raise HttpError(response.reason)
|
|
result = response.read()
|
|
|
|
# strip jsonp header and parse
|
|
assert result.startswith('for(;;);')
|
|
result = json.loads(result[8:])
|
|
|
|
# check for conduit errors
|
|
if result['error_code']:
|
|
raise ConduitError(result['error_info'])
|
|
|
|
# return RPC result
|
|
return result['result']
|
|
|
|
# don't close the connection b/c we want to avoid the connection overhead
|
|
|
|
def mirrornode(ctx, mapping, args):
|
|
'''template: find this commit in other repositories'''
|
|
|
|
reponame = mapping['repo'].ui.config('fbconduit', 'reponame')
|
|
if not reponame:
|
|
# We don't know who we are, so we can't ask for a translation
|
|
return ''
|
|
|
|
if mapping['ctx'].mutable():
|
|
# Local commits don't have translations
|
|
return ''
|
|
|
|
node = mapping['ctx'].hex()
|
|
args = [f(ctx, mapping, a) for f, a in args]
|
|
if len(args) == 1:
|
|
torepo, totype = reponame, args[0]
|
|
else:
|
|
torepo, totype = args
|
|
|
|
try:
|
|
result = call_conduit('scmquery.get.mirrored.revs',
|
|
from_repo=reponame,
|
|
from_scm='hg',
|
|
to_repo=torepo,
|
|
to_scm=totype,
|
|
revs=[node]
|
|
)
|
|
except ConduitError as e:
|
|
if 'unknown revision' not in str(e.args):
|
|
mapping['repo'].ui.warn((str(e.args) + '\n'))
|
|
return ''
|
|
return result.get(node, '')
|
|
|
|
templatekeyword = registrar.templatekeyword()
|
|
|
|
@templatekeyword("gitnode")
|
|
def showgitnode(repo, ctx, templ, **args):
|
|
"""Return the git revision corresponding to a given hg rev"""
|
|
reponame = repo.ui.config('fbconduit', 'reponame')
|
|
if not reponame:
|
|
# We don't know who we are, so we can't ask for a translation
|
|
return ''
|
|
backingrepos = repo.ui.configlist('fbconduit', 'backingrepos',
|
|
default=[reponame])
|
|
|
|
if ctx.mutable():
|
|
# Local commits don't have translations
|
|
return ''
|
|
|
|
matches = []
|
|
for backingrepo in backingrepos:
|
|
try:
|
|
result = call_conduit('scmquery.get.mirrored.revs',
|
|
from_repo=reponame,
|
|
from_scm='hg',
|
|
to_repo=backingrepo,
|
|
to_scm='git',
|
|
revs=[ctx.hex()]
|
|
)
|
|
githash = result[ctx.hex()]
|
|
if githash != "":
|
|
matches.append((backingrepo, githash))
|
|
except ConduitError:
|
|
pass
|
|
|
|
if len(matches) == 0:
|
|
return ''
|
|
elif len(backingrepos) == 1:
|
|
return matches[0][1]
|
|
else:
|
|
# in case it's not clear, the sort() is to ensure the output is in a
|
|
# deterministic order.
|
|
matches.sort()
|
|
return "; ".join(["{0}: {1}".format(*match)
|
|
for match in matches])
|
|
|
|
def gitnode(repo, subset, x):
|
|
"""``gitnode(id)``
|
|
Return the hg revision corresponding to a given git rev."""
|
|
l = revset.getargs(x, 1, 1, _("id requires one argument"))
|
|
n = revset.getstring(l[0], _("id requires a string"))
|
|
|
|
reponame = repo.ui.config('fbconduit', 'reponame')
|
|
if not reponame:
|
|
# We don't know who we are, so we can't ask for a translation
|
|
return subset.filter(lambda r: False)
|
|
backingrepos = repo.ui.configlist('fbconduit', 'backingrepos',
|
|
default=[reponame])
|
|
|
|
lasterror = None
|
|
hghash = None
|
|
for backingrepo in backingrepos:
|
|
try:
|
|
result = call_conduit('scmquery.get.mirrored.revs',
|
|
from_repo=backingrepo,
|
|
from_scm='git',
|
|
to_repo=reponame,
|
|
to_scm='hg',
|
|
revs=[n]
|
|
)
|
|
hghash = result[n]
|
|
if hghash != '':
|
|
break
|
|
except Exception as ex:
|
|
lasterror = ex
|
|
|
|
if not hghash:
|
|
if lasterror:
|
|
repo.ui.warn(("Could not translate revision {0}: {1}\n".format(
|
|
n, lasterror)))
|
|
else:
|
|
repo.ui.warn(("Could not translate revision {0}\n".format(n)))
|
|
return subset.filter(lambda r: False)
|
|
|
|
rn = repo[node.bin(hghash)].rev()
|
|
return subset.filter(lambda r: r == rn)
|
|
|
|
def overridestringset(orig, repo, subset, x):
|
|
# Is the given revset a phabricator hg hash (ie: rHGEXTaaacb34aacb34aa)
|
|
phabmatch = phabhashre.match(x)
|
|
if phabmatch:
|
|
phabrepo = phabmatch.group(1)
|
|
phabhash = phabmatch.group(2)
|
|
# The hash may be a git hash
|
|
if phabrepo in repo.ui.configlist('fbconduit', 'gitcallsigns', []):
|
|
return overridestringset(orig, repo, subset, 'g%s' % phabhash)
|
|
|
|
if phabhash in repo:
|
|
return orig(repo, subset, phabhash)
|
|
|
|
# Is the given revset a phabricator svn revision (rO11223232323232)?
|
|
svnrev = fbsvnhash.match(x)
|
|
if svnrev and not x in repo:
|
|
try:
|
|
extensions.find('hgsubversion')
|
|
meta = repo.svnmeta()
|
|
|
|
desiredrevision = int(svnrev.group(1))
|
|
# For some odd reason, the key is a tuple instead of a revision num
|
|
# The second member always seems to be None
|
|
revmapkey = (desiredrevision, None)
|
|
hghash = meta.revmap.get(revmapkey)
|
|
if hghash:
|
|
return orig(repo, subset, hghash)
|
|
|
|
except KeyError:
|
|
pass
|
|
|
|
m = githashre.match(x)
|
|
if m is not None:
|
|
githash = m.group(1)
|
|
if len(githash) == 40:
|
|
return gitnode(repo, subset, ('string', githash))
|
|
else:
|
|
raise error.Abort('git hash must be 40 characters')
|
|
return orig(repo, subset, x)
|