Mercurial > public > mercurial-scm > hg
comparison contrib/phabricator.py @ 33198:36b3febd739f
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 <<EOF
> {"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/
author | Jun Wu <quark@fb.com> |
---|---|
date | Sun, 02 Jul 2017 20:08:09 -0700 |
parents | |
children | 228ad1e58a85 |
comparison
equal
deleted
inserted
replaced
33197:c5a07a3abe7d | 33198:36b3febd739f |
---|---|
1 # phabricator.py - simple Phabricator integration | |
2 # | |
3 # Copyright 2017 Facebook, Inc. | |
4 # | |
5 # This software may be used and distributed according to the terms of the | |
6 # GNU General Public License version 2 or any later version. | |
7 """simple Phabricator integration | |
8 | |
9 Config:: | |
10 | |
11 [phabricator] | |
12 # Phabricator URL | |
13 url = https://phab.example.com/ | |
14 | |
15 # API token. Get it from https://$HOST/conduit/login/ | |
16 token = cli-xxxxxxxxxxxxxxxxxxxxxxxxxxxx | |
17 """ | |
18 | |
19 from __future__ import absolute_import | |
20 | |
21 import json | |
22 | |
23 from mercurial.i18n import _ | |
24 from mercurial import ( | |
25 error, | |
26 registrar, | |
27 url as urlmod, | |
28 util, | |
29 ) | |
30 | |
31 cmdtable = {} | |
32 command = registrar.command(cmdtable) | |
33 | |
34 def urlencodenested(params): | |
35 """like urlencode, but works with nested parameters. | |
36 | |
37 For example, if params is {'a': ['b', 'c'], 'd': {'e': 'f'}}, it will be | |
38 flattened to {'a[0]': 'b', 'a[1]': 'c', 'd[e]': 'f'} and then passed to | |
39 urlencode. Note: the encoding is consistent with PHP's http_build_query. | |
40 """ | |
41 flatparams = util.sortdict() | |
42 def process(prefix, obj): | |
43 items = {list: enumerate, dict: lambda x: x.items()}.get(type(obj)) | |
44 if items is None: | |
45 flatparams[prefix] = obj | |
46 else: | |
47 for k, v in items(obj): | |
48 if prefix: | |
49 process('%s[%s]' % (prefix, k), v) | |
50 else: | |
51 process(k, v) | |
52 process('', params) | |
53 return util.urlreq.urlencode(flatparams) | |
54 | |
55 def readurltoken(repo): | |
56 """return conduit url, token and make sure they exist | |
57 | |
58 Currently read from [phabricator] config section. In the future, it might | |
59 make sense to read from .arcconfig and .arcrc as well. | |
60 """ | |
61 values = [] | |
62 section = 'phabricator' | |
63 for name in ['url', 'token']: | |
64 value = repo.ui.config(section, name) | |
65 if not value: | |
66 raise error.Abort(_('config %s.%s is required') % (section, name)) | |
67 values.append(value) | |
68 return values | |
69 | |
70 def callconduit(repo, name, params): | |
71 """call Conduit API, params is a dict. return json.loads result, or None""" | |
72 host, token = readurltoken(repo) | |
73 url, authinfo = util.url('/'.join([host, 'api', name])).authinfo() | |
74 urlopener = urlmod.opener(repo.ui, authinfo) | |
75 repo.ui.debug('Conduit Call: %s %s\n' % (url, params)) | |
76 params = params.copy() | |
77 params['api.token'] = token | |
78 request = util.urlreq.request(url, data=urlencodenested(params)) | |
79 body = urlopener.open(request).read() | |
80 repo.ui.debug('Conduit Response: %s\n' % body) | |
81 parsed = json.loads(body) | |
82 if parsed.get(r'error_code'): | |
83 msg = (_('Conduit Error (%s): %s') | |
84 % (parsed[r'error_code'], parsed[r'error_info'])) | |
85 raise error.Abort(msg) | |
86 return parsed[r'result'] | |
87 | |
88 @command('debugcallconduit', [], _('METHOD')) | |
89 def debugcallconduit(ui, repo, name): | |
90 """call Conduit API | |
91 | |
92 Call parameters are read from stdin as a JSON blob. Result will be written | |
93 to stdout as a JSON blob. | |
94 """ | |
95 params = json.loads(ui.fin.read()) | |
96 result = callconduit(repo, name, params) | |
97 s = json.dumps(result, sort_keys=True, indent=2, separators=(',', ': ')) | |
98 ui.write('%s\n' % s) |