Patchwork [1,of,3] phabricator: add a contrib script

login
register
mail settings
Submitter Jun Wu
Date July 3, 2017, 3:10 a.m.
Message ID <dde88a5ead6982082263.1499051418@x1c>
Download mbox | patch
Permalink /patch/21945/
State Accepted
Headers show

Comments

Jun Wu - July 3, 2017, 3:10 a.m.
# HG changeset patch
# User Jun Wu <quark@fb.com>
# Date 1499051289 25200
#      Sun Jul 02 20:08:09 2017 -0700
# Node ID dde88a5ead698208226301a62a5f2aa2629d7839
# Parent  c077eac329e26a4ca7da8b80071ba9161994bcfe
# Available At https://bitbucket.org/quark-zju/hg-draft
#              hg pull https://bitbucket.org/quark-zju/hg-draft -r dde88a5ead69
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/
Siddharth Agarwal - July 3, 2017, 4:42 a.m.
On 7/2/17 8:10 PM, Jun Wu wrote:
> # HG changeset patch
> # User Jun Wu <quark@fb.com>
> # Date 1499051289 25200
> #      Sun Jul 02 20:08:09 2017 -0700
> # Node ID dde88a5ead698208226301a62a5f2aa2629d7839
> # Parent  c077eac329e26a4ca7da8b80071ba9161994bcfe
> # Available At https://bitbucket.org/quark-zju/hg-draft
> #              hg pull https://bitbucket.org/quark-zju/hg-draft -r dde88a5ead69
> phabricator: add a contrib script

Does this series mean we can provide a basically equivalent experience 
to email? (Maybe even better than that because applying changes looks 
pretty easy?)


>
> 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/
>
> diff --git a/contrib/phabricator.py b/contrib/phabricator.py
> new file mode 100644
> --- /dev/null
> +++ b/contrib/phabricator.py
> @@ -0,0 +1,98 @@
> +# phabricator.py - simple Phabricator integration
> +#
> +# Copyright 2017 Facebook, Inc.
> +#
> +# This software may be used and distributed according to the terms of the
> +# GNU General Public License version 2 or any later version.
> +"""simple Phabricator integration
> +
> +Config::
> +
> +    [phabricator]
> +    # Phabricator URL
> +    url = https://phab.example.com/
> +
> +    # API token. Get it from https://$HOST/conduit/login/
> +    token = cli-xxxxxxxxxxxxxxxxxxxxxxxxxxxx
> +"""
> +
> +from __future__ import absolute_import
> +
> +import json
> +
> +from mercurial.i18n import _
> +from mercurial import (
> +    error,
> +    registrar,
> +    url as urlmod,
> +    util,
> +)
> +
> +cmdtable = {}
> +command = registrar.command(cmdtable)
> +
> +def urlencodenested(params):
> +    """like urlencode, but works with nested parameters.
> +
> +    For example, if params is {'a': ['b', 'c'], 'd': {'e': 'f'}}, it will be
> +    flattened to {'a[0]': 'b', 'a[1]': 'c', 'd[e]': 'f'} and then passed to
> +    urlencode. Note: the encoding is consistent with PHP's http_build_query.
> +    """
> +    flatparams = util.sortdict()
> +    def process(prefix, obj):
> +        items = {list: enumerate, dict: lambda x: x.items()}.get(type(obj))
> +        if items is None:
> +            flatparams[prefix] = obj
> +        else:
> +            for k, v in items(obj):
> +                if prefix:
> +                    process('%s[%s]' % (prefix, k), v)
> +                else:
> +                    process(k, v)
> +    process('', params)
> +    return util.urlreq.urlencode(flatparams)
> +
> +def readurltoken(repo):
> +    """return conduit url, token and make sure they exist
> +
> +    Currently read from [phabricator] config section. In the future, it might
> +    make sense to read from .arcconfig and .arcrc as well.
> +    """
> +    values = []
> +    section = 'phabricator'
> +    for name in ['url', 'token']:
> +        value = repo.ui.config(section, name)
> +        if not value:
> +            raise error.Abort(_('config %s.%s is required') % (section, name))
> +        values.append(value)
> +    return values
> +
> +def callconduit(repo, name, params):
> +    """call Conduit API, params is a dict. return json.loads result, or None"""
> +    host, token = readurltoken(repo)
> +    url, authinfo = util.url('/'.join([host, 'api', name])).authinfo()
> +    urlopener = urlmod.opener(repo.ui, authinfo)
> +    repo.ui.debug('Conduit Call: %s %s\n' % (url, params))
> +    params = params.copy()
> +    params['api.token'] = token
> +    request = util.urlreq.request(url, data=urlencodenested(params))
> +    body = urlopener.open(request).read()
> +    repo.ui.debug('Conduit Response: %s\n' % body)
> +    parsed = json.loads(body)
> +    if parsed.get(r'error_code'):
> +        msg = (_('Conduit Error (%s): %s')
> +               % (parsed[r'error_code'], parsed[r'error_info']))
> +        raise error.Abort(msg)
> +    return parsed[r'result']
> +
> +@command('debugcallconduit', [], _('METHOD'))
> +def debugcallconduit(ui, repo, name):
> +    """call Conduit API
> +
> +    Call parameters are read from stdin as a JSON blob. Result will be written
> +    to stdout as a JSON blob.
> +    """
> +    params = json.loads(ui.fin.read())
> +    result = callconduit(repo, name, params)
> +    s = json.dumps(result, sort_keys=True, indent=2, separators=(',', ': '))
> +    ui.write('%s\n' % s)
> _______________________________________________
> Mercurial-devel mailing list
> Mercurial-devel@mercurial-scm.org
> https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
Phillip Cohen - July 3, 2017, 4:42 a.m.
This is a great idea, thanks for implementing it. :)

On Sun, Jul 2, 2017 at 9:42 PM, Siddharth Agarwal <sid@less-broken.com> wrote:
> On 7/2/17 8:10 PM, Jun Wu wrote:
>>
>> # HG changeset patch
>> # User Jun Wu <quark@fb.com>
>> # Date 1499051289 25200
>> #      Sun Jul 02 20:08:09 2017 -0700
>> # Node ID dde88a5ead698208226301a62a5f2aa2629d7839
>> # Parent  c077eac329e26a4ca7da8b80071ba9161994bcfe
>> # Available At https://bitbucket.org/quark-zju/hg-draft
>> #              hg pull https://bitbucket.org/quark-zju/hg-draft -r
>> dde88a5ead69
>> phabricator: add a contrib script
>
>
> Does this series mean we can provide a basically equivalent experience to
> email? (Maybe even better than that because applying changes looks pretty
> easy?)
>
>
>
>>
>> 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/
>>
>> diff --git a/contrib/phabricator.py b/contrib/phabricator.py
>> new file mode 100644
>> --- /dev/null
>> +++ b/contrib/phabricator.py
>> @@ -0,0 +1,98 @@
>> +# phabricator.py - simple Phabricator integration
>> +#
>> +# Copyright 2017 Facebook, Inc.
>> +#
>> +# This software may be used and distributed according to the terms of the
>> +# GNU General Public License version 2 or any later version.
>> +"""simple Phabricator integration
>> +
>> +Config::
>> +
>> +    [phabricator]
>> +    # Phabricator URL
>> +    url = https://phab.example.com/
>> +
>> +    # API token. Get it from https://$HOST/conduit/login/
>> +    token = cli-xxxxxxxxxxxxxxxxxxxxxxxxxxxx
>> +"""
>> +
>> +from __future__ import absolute_import
>> +
>> +import json
>> +
>> +from mercurial.i18n import _
>> +from mercurial import (
>> +    error,
>> +    registrar,
>> +    url as urlmod,
>> +    util,
>> +)
>> +
>> +cmdtable = {}
>> +command = registrar.command(cmdtable)
>> +
>> +def urlencodenested(params):
>> +    """like urlencode, but works with nested parameters.
>> +
>> +    For example, if params is {'a': ['b', 'c'], 'd': {'e': 'f'}}, it will
>> be
>> +    flattened to {'a[0]': 'b', 'a[1]': 'c', 'd[e]': 'f'} and then passed
>> to
>> +    urlencode. Note: the encoding is consistent with PHP's
>> http_build_query.
>> +    """
>> +    flatparams = util.sortdict()
>> +    def process(prefix, obj):
>> +        items = {list: enumerate, dict: lambda x:
>> x.items()}.get(type(obj))
>> +        if items is None:
>> +            flatparams[prefix] = obj
>> +        else:
>> +            for k, v in items(obj):
>> +                if prefix:
>> +                    process('%s[%s]' % (prefix, k), v)
>> +                else:
>> +                    process(k, v)
>> +    process('', params)
>> +    return util.urlreq.urlencode(flatparams)
>> +
>> +def readurltoken(repo):
>> +    """return conduit url, token and make sure they exist
>> +
>> +    Currently read from [phabricator] config section. In the future, it
>> might
>> +    make sense to read from .arcconfig and .arcrc as well.
>> +    """
>> +    values = []
>> +    section = 'phabricator'
>> +    for name in ['url', 'token']:
>> +        value = repo.ui.config(section, name)
>> +        if not value:
>> +            raise error.Abort(_('config %s.%s is required') % (section,
>> name))
>> +        values.append(value)
>> +    return values
>> +
>> +def callconduit(repo, name, params):
>> +    """call Conduit API, params is a dict. return json.loads result, or
>> None"""
>> +    host, token = readurltoken(repo)
>> +    url, authinfo = util.url('/'.join([host, 'api', name])).authinfo()
>> +    urlopener = urlmod.opener(repo.ui, authinfo)
>> +    repo.ui.debug('Conduit Call: %s %s\n' % (url, params))
>> +    params = params.copy()
>> +    params['api.token'] = token
>> +    request = util.urlreq.request(url, data=urlencodenested(params))
>> +    body = urlopener.open(request).read()
>> +    repo.ui.debug('Conduit Response: %s\n' % body)
>> +    parsed = json.loads(body)
>> +    if parsed.get(r'error_code'):
>> +        msg = (_('Conduit Error (%s): %s')
>> +               % (parsed[r'error_code'], parsed[r'error_info']))
>> +        raise error.Abort(msg)
>> +    return parsed[r'result']
>> +
>> +@command('debugcallconduit', [], _('METHOD'))
>> +def debugcallconduit(ui, repo, name):
>> +    """call Conduit API
>> +
>> +    Call parameters are read from stdin as a JSON blob. Result will be
>> written
>> +    to stdout as a JSON blob.
>> +    """
>> +    params = json.loads(ui.fin.read())
>> +    result = callconduit(repo, name, params)
>> +    s = json.dumps(result, sort_keys=True, indent=2, separators=(',', ':
>> '))
>> +    ui.write('%s\n' % s)
>> _______________________________________________
>> Mercurial-devel mailing list
>> Mercurial-devel@mercurial-scm.org
>> https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
>
>
>
> _______________________________________________
> Mercurial-devel mailing list
> Mercurial-devel@mercurial-scm.org
> https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
Jun Wu - July 3, 2017, 12:33 p.m.
Excerpts from Siddharth Agarwal's message of 2017-07-02 21:42:04 -0700:
> Does this series mean we can provide a basically equivalent experience 
> to email? (Maybe even better than that because applying changes looks 
> pretty easy?)

Hopefully. I think a major benefit is V2 will be updating existing review
units so reviewers could check the difference between V2 and V1 easily.

One feature I missed is the "threading" view. If we have too many
Differential Revisions listed, it might make sense to have some in-browser
user script to collapse diffs by author.

Patch

diff --git a/contrib/phabricator.py b/contrib/phabricator.py
new file mode 100644
--- /dev/null
+++ b/contrib/phabricator.py
@@ -0,0 +1,98 @@ 
+# phabricator.py - simple Phabricator integration
+#
+# Copyright 2017 Facebook, Inc.
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+"""simple Phabricator integration
+
+Config::
+
+    [phabricator]
+    # Phabricator URL
+    url = https://phab.example.com/
+
+    # API token. Get it from https://$HOST/conduit/login/
+    token = cli-xxxxxxxxxxxxxxxxxxxxxxxxxxxx
+"""
+
+from __future__ import absolute_import
+
+import json
+
+from mercurial.i18n import _
+from mercurial import (
+    error,
+    registrar,
+    url as urlmod,
+    util,
+)
+
+cmdtable = {}
+command = registrar.command(cmdtable)
+
+def urlencodenested(params):
+    """like urlencode, but works with nested parameters.
+
+    For example, if params is {'a': ['b', 'c'], 'd': {'e': 'f'}}, it will be
+    flattened to {'a[0]': 'b', 'a[1]': 'c', 'd[e]': 'f'} and then passed to
+    urlencode. Note: the encoding is consistent with PHP's http_build_query.
+    """
+    flatparams = util.sortdict()
+    def process(prefix, obj):
+        items = {list: enumerate, dict: lambda x: x.items()}.get(type(obj))
+        if items is None:
+            flatparams[prefix] = obj
+        else:
+            for k, v in items(obj):
+                if prefix:
+                    process('%s[%s]' % (prefix, k), v)
+                else:
+                    process(k, v)
+    process('', params)
+    return util.urlreq.urlencode(flatparams)
+
+def readurltoken(repo):
+    """return conduit url, token and make sure they exist
+
+    Currently read from [phabricator] config section. In the future, it might
+    make sense to read from .arcconfig and .arcrc as well.
+    """
+    values = []
+    section = 'phabricator'
+    for name in ['url', 'token']:
+        value = repo.ui.config(section, name)
+        if not value:
+            raise error.Abort(_('config %s.%s is required') % (section, name))
+        values.append(value)
+    return values
+
+def callconduit(repo, name, params):
+    """call Conduit API, params is a dict. return json.loads result, or None"""
+    host, token = readurltoken(repo)
+    url, authinfo = util.url('/'.join([host, 'api', name])).authinfo()
+    urlopener = urlmod.opener(repo.ui, authinfo)
+    repo.ui.debug('Conduit Call: %s %s\n' % (url, params))
+    params = params.copy()
+    params['api.token'] = token
+    request = util.urlreq.request(url, data=urlencodenested(params))
+    body = urlopener.open(request).read()
+    repo.ui.debug('Conduit Response: %s\n' % body)
+    parsed = json.loads(body)
+    if parsed.get(r'error_code'):
+        msg = (_('Conduit Error (%s): %s')
+               % (parsed[r'error_code'], parsed[r'error_info']))
+        raise error.Abort(msg)
+    return parsed[r'result']
+
+@command('debugcallconduit', [], _('METHOD'))
+def debugcallconduit(ui, repo, name):
+    """call Conduit API
+
+    Call parameters are read from stdin as a JSON blob. Result will be written
+    to stdout as a JSON blob.
+    """
+    params = json.loads(ui.fin.read())
+    result = callconduit(repo, name, params)
+    s = json.dumps(result, sort_keys=True, indent=2, separators=(',', ': '))
+    ui.write('%s\n' % s)