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