contrib/phabricator.py
changeset 33198 36b3febd739f
child 33199 228ad1e58a85
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)