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