|
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) |