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