# HG changeset patch # User Jun Wu # Date 1499051289 25200 # Node ID 36b3febd739fb4cbee802652186f0ff6e09439fa # Parent c5a07a3abe7dc4bb9a0bb9680a8a67f975a0081e phabricator: add a contrib script The default Phabricator client arcanist is not friendly to send a stack of changesets. It works better when a feature branch is reviewed as a single review unit. However, we want multiple revisions per feature branch. To be able to have an `hg email`-like UX to send and receive a stack of commits easily, it seems we have to re-invent things. This patch adds `phabricator.py` speaking Conduit API [1] in `contrib` as the first step. This may also be an option for people who don't want to run PHP. Config could be done in `hgrc` (instead of `arcrc` or `arcconfig`): [phabricator] # API token. Get it from https://phab.mercurial-scm.org/conduit/login/ token = cli-xxxxxxxxxxxxxxxxxxxxxxxxxxxx url = https://phab.mercurial-scm.org/ # callsign is used by the next patch callsign = HG This patch only adds a single command: `debugcallconduit` to keep the patch size small. To test it, having the above config, and run: $ hg debugcallconduit diffusion.repository.search < {"constraints": {"callsigns": ["HG"]}} > EOF The result will be printed in prettified JSON format. [1]: Conduit APIs are listed at https://phab.mercurial-scm.org/conduit/ diff -r c5a07a3abe7d -r 36b3febd739f contrib/phabricator.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/contrib/phabricator.py Sun Jul 02 20:08:09 2017 -0700 @@ -0,0 +1,98 @@ +# phabricator.py - simple Phabricator integration +# +# Copyright 2017 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. +"""simple Phabricator integration + +Config:: + + [phabricator] + # Phabricator URL + url = https://phab.example.com/ + + # API token. Get it from https://$HOST/conduit/login/ + token = cli-xxxxxxxxxxxxxxxxxxxxxxxxxxxx +""" + +from __future__ import absolute_import + +import json + +from mercurial.i18n import _ +from mercurial import ( + error, + registrar, + url as urlmod, + util, +) + +cmdtable = {} +command = registrar.command(cmdtable) + +def urlencodenested(params): + """like urlencode, but works with nested parameters. + + For example, if params is {'a': ['b', 'c'], 'd': {'e': 'f'}}, it will be + flattened to {'a[0]': 'b', 'a[1]': 'c', 'd[e]': 'f'} and then passed to + urlencode. Note: the encoding is consistent with PHP's http_build_query. + """ + flatparams = util.sortdict() + def process(prefix, obj): + items = {list: enumerate, dict: lambda x: x.items()}.get(type(obj)) + if items is None: + flatparams[prefix] = obj + else: + for k, v in items(obj): + if prefix: + process('%s[%s]' % (prefix, k), v) + else: + process(k, v) + process('', params) + return util.urlreq.urlencode(flatparams) + +def readurltoken(repo): + """return conduit url, token and make sure they exist + + Currently read from [phabricator] config section. In the future, it might + make sense to read from .arcconfig and .arcrc as well. + """ + values = [] + section = 'phabricator' + for name in ['url', 'token']: + value = repo.ui.config(section, name) + if not value: + raise error.Abort(_('config %s.%s is required') % (section, name)) + values.append(value) + return values + +def callconduit(repo, name, params): + """call Conduit API, params is a dict. return json.loads result, or None""" + host, token = readurltoken(repo) + url, authinfo = util.url('/'.join([host, 'api', name])).authinfo() + urlopener = urlmod.opener(repo.ui, authinfo) + repo.ui.debug('Conduit Call: %s %s\n' % (url, params)) + params = params.copy() + params['api.token'] = token + request = util.urlreq.request(url, data=urlencodenested(params)) + body = urlopener.open(request).read() + repo.ui.debug('Conduit Response: %s\n' % body) + parsed = json.loads(body) + if parsed.get(r'error_code'): + msg = (_('Conduit Error (%s): %s') + % (parsed[r'error_code'], parsed[r'error_info'])) + raise error.Abort(msg) + return parsed[r'result'] + +@command('debugcallconduit', [], _('METHOD')) +def debugcallconduit(ui, repo, name): + """call Conduit API + + Call parameters are read from stdin as a JSON blob. Result will be written + to stdout as a JSON blob. + """ + params = json.loads(ui.fin.read()) + result = callconduit(repo, name, params) + s = json.dumps(result, sort_keys=True, indent=2, separators=(',', ': ')) + ui.write('%s\n' % s)