mirror of
https://github.com/facebook/sapling.git
synced 2024-10-11 01:07:15 +03:00
63b7bb19c5
Summary: Many of the template keywords in our extensions were being registered incorrectly, causing their help output to be rendered incorrectly in the "hg help templates" output. The ones in smartlog.py were particularly bad, as most of them showed only their description, without displaying the name of the template. In smartlog.py only singlepublicsuccessor was being displayed correctly, because it's docstring explicitly included it's own name at the start. This fixes all of our extensions to consistently use the registrar.templatekeyword() decorator to register the keywords. This decorator automatically prefixes the help message with the keyword name. The mercurial/extensions.py code will explicitly check to see if an extension contains an "templatekeyword" attribute, and if so it will register any keywords contained in this registry after calling extsetup(). Test Plan: Added new unit tests to check the output of "hg help templates" for the affected keywords. Reviewers: #sourcecontrol, kulshrax, ikostia, rmcelroy Reviewed By: rmcelroy Subscribers: rmcelroy, net-systems-diffs@, yogeshwer, mjpieters Differential Revision: https://phabricator.intern.facebook.com/D4427729 Signature: t1:4427729:1484831476:17b478a5e867dfc3f85402588c381bf8b1831107
257 lines
7.7 KiB
Python
257 lines
7.7 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
|
|
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])
|
|
|
|
translationerror = False
|
|
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 ConduitError:
|
|
pass
|
|
else:
|
|
translationerror = True
|
|
|
|
if translationerror or result[n] == "":
|
|
repo.ui.warn(("Could not translate revision {0}.\n".format(n)))
|
|
return subset.filter(lambda r: False)
|
|
|
|
rn = repo[node.bin(result[n])].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)
|