##// END OF EJS Templates
compression: introduce a `storage.revlog.zstd.level` configuration...
compression: introduce a `storage.revlog.zstd.level` configuration This option control the zstd compression level used when compressing revlog chunk. The usage of zstd for revlog compression has not graduated from experimental yet, but we intend to fix that soon. The option name for the compression level is more straight forward to pick, so this changesets comes first. Having a dedicated option for each compression engine is useful because they don't support the same range of values. I ran the same measurement as for the zlib compression level (in the parent changesets). The variation in repository size is stay mostly in the same (small) range. The "read/write" performance see smallish variation, but are overall much better than zlib. Write performance show the same tend of having better write performance for when reaching high-end compression. Again, we don't intend to change the default zstd compression level (currently: 3) in this series. However this is worth investigating in the future. The Performance comparison of zlib vs zstd is quite impressive. The repository size stay in the same range, but the performance are much better in all situations. Comparison summary ================== We are looking at: - performance range for zlib - performance range for zstd - comparison of default zstd (level-3) to default zlib (level 6) - comparison of the slowest zstd time to the fastest zlib time Read performance: ----------------- | zlib | zstd | cmp | f2s mercurial | 0.170159 - 0.189219 | 0.144127 - 0.149624 | 80% | 88% pypy | 2.679217 - 2.768691 | 1.532317 - 1.705044 | 60% | 63% netbeans | 122.477027 - 141.620281 | 72.996346 - 89.731560 | 58% | 73% mozilla | 147.867662 - 170.572118 | 91.700995 - 105.853099 | 56% | 71% Write performance: ------------------ | zlib | zstd | cmp | f2s mercurial | 53.250304 - 56.2936129 | 40.877025 - 45.677286 | 75% | 86% pypy | 460.721984 - 476.589918 | 270.545409 - 301.002219 | 63% | 65% netbeans | 520.560316 - 715.930400 | 370.356311 - 428.329652 | 55% | 82% mozilla | 739.803002 - 987.056093 | 505.152906 - 591.930683 | 57% | 80% Raw data -------- repo alg lvl .hg/store size 00manifest.d read write mercurial zlib 1 49,402,813 5,963,475 0.170159 53.250304 mercurial zlib 6 47,197,397 5,875,730 0.182820 56.264320 mercurial zlib 9 47,121,596 5,849,781 0.189219 56.293612 mercurial zstd 1 49,737,084 5,966,355 0.144127 40.877025 mercurial zstd 3 48,961,867 5,895,208 0.146376 42.268142 mercurial zstd 5 48,200,592 5,938,676 0.149624 43.162875 mercurial zstd 10 47,833,520 5,913,353 0.145185 44.012489 mercurial zstd 15 47,314,604 5,728,679 0.147686 45.677286 mercurial zstd 20 47,330,502 5,830,539 0.145789 45.025407 mercurial zstd 22 47,330,076 5,830,539 0.143996 44.690460 pypy zlib 1 370,830,572 28,462,425 2.679217 460.721984 pypy zlib 6 340,112,317 27,648,747 2.768691 467.537158 pypy zlib 9 338,360,736 27,639,003 2.763495 476.589918 pypy zstd 1 362,377,479 27,916,214 1.532317 270.545409 pypy zstd 3 354,137,693 27,905,988 1.686718 294.951509 pypy zstd 5 342,640,043 27,655,774 1.705044 301.002219 pypy zstd 10 334,224,327 27,164,493 1.567287 285.186239 pypy zstd 15 329,000,363 26,645,965 1.637729 299.561332 pypy zstd 20 324,534,039 26,199,547 1.526813 302.149827 pypy zstd 22 324,530,595 26,198,932 1.525718 307.821218 netbeans zlib 1 1,281,847,810 165,495,457 122.477027 520.560316 netbeans zlib 6 1,205,284,353 159,161,207 139.876147 715.930400 netbeans zlib 9 1,197,135,671 155,034,586 141.620281 678.297064 netbeans zstd 1 1,259,581,737 160,840,613 72.996346 370.356311 netbeans zstd 3 1,232,978,122 157,691,551 81.622317 396.733087 netbeans zstd 5 1,208,034,075 160,246,880 83.080549 364.342626 netbeans zstd 10 1,188,624,176 156,083,417 79.323935 403.594602 netbeans zstd 15 1,176,973,589 153,859,477 89.731560 428.329652 netbeans zstd 20 1,162,958,258 151,147,535 82.842667 392.335349 netbeans zstd 22 1,162,707,029 151,150,220 82.565695 402.840655 mozilla zlib 1 2,775,497,186 298,527,987 147.867662 751.263721 mozilla zlib 6 2,596,856,420 286,597,671 170.572118 987.056093 mozilla zlib 9 2,587,542,494 287,018,264 163.622338 739.803002 mozilla zstd 1 2,723,159,348 286,617,532 91.700995 570.042751 mozilla zstd 3 2,665,055,001 286,152,013 95.240155 561.412805 mozilla zstd 5 2,607,819,817 288,060,030 101.978048 505.152906 mozilla zstd 10 2,558,761,085 283,967,648 104.113481 497.771202 mozilla zstd 15 2,526,216,060 275,581,300 105.853099 591.930683 mozilla zstd 20 2,485,114,806 266,478,859 95.268795 576.515389 mozilla zstd 22 2,484,869,080 266,456,505 94.429282 572.785537

File last commit:

r42137:99e00e5c default
r42211:bb271ec2 default
Show More
phabricator.py
1033 lines | 37.6 KiB | text/x-python | PythonLexer
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 # 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.
Augie Fackler
phabricator: mark extension as experimental for now...
r39691 """simple Phabricator integration (EXPERIMENTAL)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688
This extension provides a ``phabsend`` command which sends a stack of
changesets to Phabricator, and a ``phabread`` command which prints a stack of
revisions in a format suitable for :hg:`import`, and a ``phabupdate`` command
to update statuses in batch.
By default, Phabricator requires ``Test Plan`` which might prevent some
changeset from being sent. The requirement could be disabled by changing
``differential.require-test-plan-field`` config server side.
Config::
[phabricator]
# Phabricator URL
url = https://phab.example.com/
# Repo callsign. If a repo has a URL https://$HOST/diffusion/FOO, then its
# callsign is "FOO".
callsign = FOO
# curl command to use. If not set (default), use builtin HTTP library to
# communicate. If set, use the specified curl command. This could be useful
# if you need to specify advanced options that is not easily supported by
# the internal library.
curlcmd = curl --connect-timeout 2 --retry 3 --silent
[auth]
example.schemes = https
example.prefix = phab.example.com
# API token. Get it from https://$HOST/conduit/login/
example.phabtoken = cli-xxxxxxxxxxxxxxxxxxxxxxxxxxxx
"""
from __future__ import absolute_import
Matt Harbison
phabricator: ensure that the return of urlopener.open() is closed...
r41111 import contextlib
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 import itertools
import json
import operator
import re
from mercurial.node import bin, nullid
from mercurial.i18n import _
from mercurial import (
cmdutil,
context,
encoding,
error,
httpconnection as httpconnectionmod,
mdiff,
obsutil,
parser,
patch,
Matt Harbison
phabricator: warn if unable to amend, instead of aborting after posting...
r41198 phases,
Ian Moody
phabricator: convert conduit response JSON unicode to bytes inside callconduit...
r42063 pycompat,
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 registrar,
scmutil,
smartset,
tags,
Augie Fackler
phabricator: fix templating bug by using hybriddict...
r39690 templateutil,
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 url as urlmod,
util,
)
from mercurial.utils import (
procutil,
stringutil,
)
Yuya Nishihara
phabricator: add testedwith boilerplate
r39771 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
# extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
# be specifying the version(s) of Mercurial they are tested with, or
# leave the attribute unspecified.
testedwith = 'ships-with-hg-core'
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 cmdtable = {}
command = registrar.command(cmdtable)
configtable = {}
configitem = registrar.configitem(configtable)
# developer config: phabricator.batchsize
configitem(b'phabricator', b'batchsize',
default=12,
)
configitem(b'phabricator', b'callsign',
default=None,
)
configitem(b'phabricator', b'curlcmd',
default=None,
)
# developer config: phabricator.repophid
configitem(b'phabricator', b'repophid',
default=None,
)
configitem(b'phabricator', b'url',
default=None,
)
configitem(b'phabsend', b'confirm',
default=False,
)
colortable = {
b'phabricator.action.created': b'green',
b'phabricator.action.skipped': b'magenta',
b'phabricator.action.updated': b'magenta',
b'phabricator.desc': b'',
b'phabricator.drev': b'bold',
b'phabricator.node': b'',
}
_VCR_FLAGS = [
(b'', b'test-vcr', b'',
_(b'Path to a vcr file. If nonexistent, will record a new vcr transcript'
b', otherwise will mock all http requests using the specified vcr file.'
b' (ADVANCED)'
)),
]
Matt Harbison
phabricator: assign commands to help categories...
r41103 def vcrcommand(name, flags, spec, helpcategory=None):
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 fullflags = flags + _VCR_FLAGS
def decorate(fn):
def inner(*args, **kwargs):
Ian Moody
py3: use fsencode for vcr recording paths and strings for custom_patches args...
r42064 cassette = pycompat.fsdecode(kwargs.pop(r'test_vcr', None))
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 if cassette:
import hgdemandimport
with hgdemandimport.deactivated():
import vcr as vcrmod
import vcr.stubs as stubs
Augie Fackler
phabricator: do more of the VCR work in demandimport.deactivated()...
r40414 vcr = vcrmod.VCR(
serializer=r'json',
custom_patches=[
Ian Moody
py3: use fsencode for vcr recording paths and strings for custom_patches args...
r42064 (urlmod, r'httpconnection',
stubs.VCRHTTPConnection),
(urlmod, r'httpsconnection',
Augie Fackler
phabricator: do more of the VCR work in demandimport.deactivated()...
r40414 stubs.VCRHTTPSConnection),
])
with vcr.use_cassette(cassette):
return fn(*args, **kwargs)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 return fn(*args, **kwargs)
inner.__name__ = fn.__name__
Matt Harbison
phabricator: ensure the command summaries are available in extension help...
r40617 inner.__doc__ = fn.__doc__
Matt Harbison
phabricator: assign commands to help categories...
r41103 return command(name, fullflags, spec, helpcategory=helpcategory)(inner)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 return decorate
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):
Matt Harbison
phabricator: properly encode boolean types in the request body...
r41073 if isinstance(obj, bool):
obj = {True: b'true', False: b'false'}[obj] # Python -> PHP form
Ian Moody
py3: convert indexes into bytes when enumerating lists in urlencodenested...
r42066 lister = lambda l: [(b'%d' % k, v) for k, v in enumerate(l)]
items = {list: lister, dict: lambda x: x.items()}.get(type(obj))
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 if items is None:
flatparams[prefix] = obj
else:
for k, v in items(obj):
if prefix:
process(b'%s[%s]' % (prefix, k), v)
else:
process(k, v)
process(b'', params)
return util.urlreq.urlencode(flatparams)
def readurltoken(repo):
"""return conduit url, token and make sure they exist
Currently read from [auth] config section. In the future, it might
make sense to read from .arcconfig and .arcrc as well.
"""
url = repo.ui.config(b'phabricator', b'url')
if not url:
raise error.Abort(_(b'config %s.%s is required')
% (b'phabricator', b'url'))
res = httpconnectionmod.readauthforuri(repo.ui, url, util.url(url).user)
token = None
if res:
group, auth = res
repo.ui.debug(b"using auth.%s.* for authentication\n" % group)
token = auth.get(b'phabtoken')
if not token:
Matt Harbison
phabricator: drop support for the legacy phabricator.auth.token config (BC)...
r40187 raise error.Abort(_(b'Can\'t find conduit token associated to %s')
% (url,))
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688
return url, token
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(b'/'.join([host, b'api', name])).authinfo()
Ian Moody
py3: don't try and format a bare dict into a byte string in callconduit...
r42065 repo.ui.debug(b'Conduit Call: %s %s\n' % (url, pycompat.byterepr(params)))
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 params = params.copy()
params[b'api.token'] = token
data = urlencodenested(params)
curlcmd = repo.ui.config(b'phabricator', b'curlcmd')
if curlcmd:
sin, sout = procutil.popen2(b'%s -d @- %s'
% (curlcmd, procutil.shellquote(url)))
sin.write(data)
sin.close()
body = sout.read()
else:
urlopener = urlmod.opener(repo.ui, authinfo)
Ian Moody
py3: convert URL to str before passing it to request...
r42067 request = util.urlreq.request(pycompat.strurl(url), data=data)
Matt Harbison
phabricator: ensure that the return of urlopener.open() is closed...
r41111 with contextlib.closing(urlopener.open(request)) as rsp:
body = rsp.read()
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 repo.ui.debug(b'Conduit Response: %s\n' % body)
Ian Moody
phabricator: convert conduit response JSON unicode to bytes inside callconduit...
r42063 parsed = pycompat.rapply(
lambda x: encoding.unitolocal(x) if isinstance(x, pycompat.unicode)
else x,
json.loads(body)
)
if parsed.get(b'error_code'):
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 msg = (_(b'Conduit Error (%s): %s')
Ian Moody
phabricator: convert conduit response JSON unicode to bytes inside callconduit...
r42063 % (parsed[b'error_code'], parsed[b'error_info']))
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 raise error.Abort(msg)
Ian Moody
phabricator: convert conduit response JSON unicode to bytes inside callconduit...
r42063 return parsed[b'result']
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688
@vcrcommand(b'debugcallconduit', [], _(b'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.
"""
Ian Moody
py3: convert to/from bytes/unicode for json.(dump|load)s in debugcallconduit...
r42137 # json.loads only accepts bytes from 3.6+
rawparams = encoding.unifromlocal(ui.fin.read())
# json.loads only returns unicode strings
params = pycompat.rapply(lambda x:
encoding.unitolocal(x) if isinstance(x, pycompat.unicode) else x,
json.loads(rawparams)
)
# json.dumps only accepts unicode strings
result = pycompat.rapply(lambda x:
encoding.unifromlocal(x) if isinstance(x, bytes) else x,
callconduit(repo, name, params)
)
s = json.dumps(result, sort_keys=True, indent=2, separators=(u',', u': '))
ui.write(b'%s\n' % encoding.unitolocal(s))
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688
def getrepophid(repo):
"""given callsign, return repository PHID or None"""
# developer config: phabricator.repophid
repophid = repo.ui.config(b'phabricator', b'repophid')
if repophid:
return repophid
callsign = repo.ui.config(b'phabricator', b'callsign')
if not callsign:
return None
query = callconduit(repo, b'diffusion.repository.search',
{b'constraints': {b'callsigns': [callsign]}})
Ian Moody
phabricator: convert conduit response JSON unicode to bytes inside callconduit...
r42063 if len(query[b'data']) == 0:
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 return None
Ian Moody
phabricator: convert conduit response JSON unicode to bytes inside callconduit...
r42063 repophid = query[b'data'][0][b'phid']
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 repo.ui.setconfig(b'phabricator', b'repophid', repophid)
return repophid
Gregory Szorc
global: use raw strings for regular expressions with escapes...
r41673 _differentialrevisiontagre = re.compile(br'\AD([1-9][0-9]*)\Z')
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 _differentialrevisiondescre = re.compile(
Gregory Szorc
global: use raw strings for regular expressions with escapes...
r41673 br'^Differential Revision:\s*(?P<url>(?:.*)D(?P<id>[1-9][0-9]*))$', re.M)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688
def getoldnodedrevmap(repo, nodelist):
"""find previous nodes that has been sent to Phabricator
return {node: (oldnode, Differential diff, Differential Revision ID)}
for node in nodelist with known previous sent versions, or associated
Differential Revision IDs. ``oldnode`` and ``Differential diff`` could
be ``None``.
Examines commit messages like "Differential Revision:" to get the
association information.
If such commit message line is not found, examines all precursors and their
tags. Tags with format like "D1234" are considered a match and the node
with that tag, and the number after "D" (ex. 1234) will be returned.
The ``old node``, if not None, is guaranteed to be the last diff of
corresponding Differential Revision, and exist in the repo.
"""
unfi = repo.unfiltered()
nodemap = unfi.changelog.nodemap
result = {} # {node: (oldnode?, lastdiff?, drev)}
toconfirm = {} # {node: (force, {precnode}, drev)}
for node in nodelist:
ctx = unfi[node]
# For tags like "D123", put them into "toconfirm" to verify later
precnodes = list(obsutil.allpredecessors(unfi.obsstore, [node]))
for n in precnodes:
if n in nodemap:
for tag in unfi.nodetags(n):
m = _differentialrevisiontagre.match(tag)
if m:
toconfirm[node] = (0, set(precnodes), int(m.group(1)))
continue
# Check commit message
m = _differentialrevisiondescre.search(ctx.description())
if m:
Ian Moody
py3: use r'' for group name arguments to MatchObjects in phabricator.py...
r42071 toconfirm[node] = (1, set(precnodes), int(m.group(r'id')))
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688
# Double check if tags are genuine by collecting all old nodes from
# Phabricator, and expect precursors overlap with it.
if toconfirm:
drevs = [drev for force, precs, drev in toconfirm.values()]
alldiffs = callconduit(unfi, b'differential.querydiffs',
{b'revisionIDs': drevs})
Ian Moody
phabricator: convert conduit response JSON unicode to bytes inside callconduit...
r42063 getnode = lambda d: bin(
getdiffmeta(d).get(b'node', b'')) or None
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 for newnode, (force, precset, drev) in toconfirm.items():
diffs = [d for d in alldiffs.values()
Ian Moody
phabricator: convert conduit response JSON unicode to bytes inside callconduit...
r42063 if int(d[b'revisionID']) == drev]
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688
# "precursors" as known by Phabricator
phprecset = set(getnode(d) for d in diffs)
# Ignore if precursors (Phabricator and local repo) do not overlap,
# and force is not set (when commit message says nothing)
if not force and not bool(phprecset & precset):
tagname = b'D%d' % drev
tags.tag(repo, tagname, nullid, message=None, user=None,
date=None, local=True)
unfi.ui.warn(_(b'D%s: local tag removed - does not match '
b'Differential history\n') % drev)
continue
# Find the last node using Phabricator metadata, and make sure it
# exists in the repo
oldnode = lastdiff = None
if diffs:
Ian Moody
phabricator: convert conduit response JSON unicode to bytes inside callconduit...
r42063 lastdiff = max(diffs, key=lambda d: int(d[b'id']))
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 oldnode = getnode(lastdiff)
if oldnode and oldnode not in nodemap:
oldnode = None
result[newnode] = (oldnode, lastdiff, drev)
return result
def getdiff(ctx, diffopts):
"""plain-text diff without header (user, commit message, etc)"""
output = util.stringio()
for chunk, _label in patch.diffui(ctx.repo(), ctx.p1().node(), ctx.node(),
None, opts=diffopts):
output.write(chunk)
return output.getvalue()
def creatediff(ctx):
"""create a Differential Diff"""
repo = ctx.repo()
repophid = getrepophid(repo)
# Create a "Differential Diff" via "differential.createrawdiff" API
params = {b'diff': getdiff(ctx, mdiff.diffopts(git=True, context=32767))}
if repophid:
params[b'repositoryPHID'] = repophid
diff = callconduit(repo, b'differential.createrawdiff', params)
if not diff:
raise error.Abort(_(b'cannot create diff for %s') % ctx)
return diff
def writediffproperties(ctx, diff):
"""write metadata to diff so patches could be applied losslessly"""
params = {
Ian Moody
phabricator: convert conduit response JSON unicode to bytes inside callconduit...
r42063 b'diff_id': diff[b'id'],
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 b'name': b'hg:meta',
b'data': json.dumps({
Ian Moody
py3: only pass unicode to json.dumps in writediffproperties...
r42069 u'user': encoding.unifromlocal(ctx.user()),
u'date': u'{:.0f} {}'.format(*ctx.date()),
u'node': encoding.unifromlocal(ctx.hex()),
u'parent': encoding.unifromlocal(ctx.p1().hex()),
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 }),
}
callconduit(ctx.repo(), b'differential.setdiffproperty', params)
params = {
Ian Moody
phabricator: convert conduit response JSON unicode to bytes inside callconduit...
r42063 b'diff_id': diff[b'id'],
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 b'name': b'local:commits',
b'data': json.dumps({
Ian Moody
py3: only pass unicode to json.dumps in writediffproperties...
r42069 encoding.unifromlocal(ctx.hex()): {
u'author': encoding.unifromlocal(stringutil.person(ctx.user())),
u'authorEmail': encoding.unifromlocal(
stringutil.email(ctx.user())),
u'time': u'{:.0f}'.format(ctx.date()[0]),
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 },
}),
}
callconduit(ctx.repo(), b'differential.setdiffproperty', params)
def createdifferentialrevision(ctx, revid=None, parentrevid=None, oldnode=None,
olddiff=None, actions=None):
"""create or update a Differential Revision
If revid is None, create a new Differential Revision, otherwise update
revid. If parentrevid is not None, set it as a dependency.
If oldnode is not None, check if the patch content (without commit message
and metadata) has changed before creating another diff.
If actions is not None, they will be appended to the transaction.
"""
repo = ctx.repo()
if oldnode:
diffopts = mdiff.diffopts(git=True, context=32767)
oldctx = repo.unfiltered()[oldnode]
neednewdiff = (getdiff(ctx, diffopts) != getdiff(oldctx, diffopts))
else:
neednewdiff = True
transactions = []
if neednewdiff:
diff = creatediff(ctx)
Ian Moody
phabricator: convert conduit response JSON unicode to bytes inside callconduit...
r42063 transactions.append({b'type': b'update', b'value': diff[b'phid']})
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 else:
# Even if we don't need to upload a new diff because the patch content
# does not change. We might still need to update its metadata so
# pushers could know the correct node metadata.
assert olddiff
diff = olddiff
writediffproperties(ctx, diff)
# Use a temporary summary to set dependency. There might be better ways but
# I cannot find them for now. But do not do that if we are updating an
# existing revision (revid is not None) since that introduces visible
# churns (someone edited "Summary" twice) on the web page.
if parentrevid and revid is None:
Ian Moody
py3: use %d instead of %s when formatting an int into a byte string...
r42070 summary = b'Depends on D%d' % parentrevid
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 transactions += [{b'type': b'summary', b'value': summary},
{b'type': b'summary', b'value': b' '}]
if actions:
transactions += actions
# Parse commit message and update related fields.
desc = ctx.description()
info = callconduit(repo, b'differential.parsecommitmessage',
{b'corpus': desc})
Ian Moody
phabricator: convert conduit response JSON unicode to bytes inside callconduit...
r42063 for k, v in info[b'fields'].items():
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 if k in [b'title', b'summary', b'testPlan']:
transactions.append({b'type': k, b'value': v})
params = {b'transactions': transactions}
if revid is not None:
# Update an existing Differential Revision
params[b'objectIdentifier'] = revid
revision = callconduit(repo, b'differential.revision.edit', params)
if not revision:
raise error.Abort(_(b'cannot create revision for %s') % ctx)
return revision, diff
def userphids(repo, names):
"""convert user names to PHIDs"""
Julien Cristau
phabricator: make user searches case-insensitive...
r41854 names = [name.lower() for name in names]
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 query = {b'constraints': {b'usernames': names}}
result = callconduit(repo, b'user.search', query)
# username not found is not an error of the API. So check if we have missed
# some names here.
Ian Moody
phabricator: convert conduit response JSON unicode to bytes inside callconduit...
r42063 data = result[b'data']
resolved = set(entry[b'fields'][b'username'].lower() for entry in data)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 unresolved = set(names) - resolved
if unresolved:
raise error.Abort(_(b'unknown username: %s')
% b' '.join(sorted(unresolved)))
Ian Moody
phabricator: convert conduit response JSON unicode to bytes inside callconduit...
r42063 return [entry[b'phid'] for entry in data]
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688
@vcrcommand(b'phabsend',
[(b'r', b'rev', [], _(b'revisions to send'), _(b'REV')),
(b'', b'amend', True, _(b'update commit messages')),
(b'', b'reviewer', [], _(b'specify reviewers')),
(b'', b'confirm', None, _(b'ask for confirmation before sending'))],
Matt Harbison
phabricator: assign commands to help categories...
r41103 _(b'REV [OPTIONS]'),
helpcategory=command.CATEGORY_IMPORT_EXPORT)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 def phabsend(ui, repo, *revs, **opts):
"""upload changesets to Phabricator
If there are multiple revisions specified, they will be send as a stack
with a linear dependencies relationship using the order specified by the
revset.
For the first time uploading changesets, local tags will be created to
maintain the association. After the first time, phabsend will check
obsstore and tags information so it can figure out whether to update an
existing Differential Revision, or create a new one.
If --amend is set, update commit messages so they have the
``Differential Revision`` URL, remove related tags. This is similar to what
arcanist will do, and is more desired in author-push workflows. Otherwise,
use local tags to record the ``Differential Revision`` association.
The --confirm option lets you confirm changesets before sending them. You
can also add following to your configuration file to make it default
behaviour::
[phabsend]
confirm = true
phabsend will check obsstore and the above association to decide whether to
update an existing Differential Revision, or create a new one.
"""
Ian Moody
py3: use pycompat.byteskwargs on opts in phabricator.py...
r42136 opts = pycompat.byteskwargs(opts)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 revs = list(revs) + opts.get(b'rev', [])
revs = scmutil.revrange(repo, revs)
if not revs:
raise error.Abort(_(b'phabsend requires at least one changeset'))
if opts.get(b'amend'):
cmdutil.checkunfinished(repo)
# {newnode: (oldnode, olddiff, olddrev}
oldmap = getoldnodedrevmap(repo, [repo[r].node() for r in revs])
confirm = ui.configbool(b'phabsend', b'confirm')
confirm |= bool(opts.get(b'confirm'))
if confirm:
confirmed = _confirmbeforesend(repo, revs, oldmap)
if not confirmed:
raise error.Abort(_(b'phabsend cancelled'))
actions = []
reviewers = opts.get(b'reviewer', [])
if reviewers:
phids = userphids(repo, reviewers)
actions.append({b'type': b'reviewers.add', b'value': phids})
drevids = [] # [int]
diffmap = {} # {newnode: diff}
# Send patches one by one so we know their Differential Revision IDs and
# can provide dependency relationship
lastrevid = None
for rev in revs:
ui.debug(b'sending rev %d\n' % rev)
ctx = repo[rev]
# Get Differential Revision ID
oldnode, olddiff, revid = oldmap.get(ctx.node(), (None, None, None))
if oldnode != ctx.node() or opts.get(b'amend'):
# Create or update Differential Revision
revision, diff = createdifferentialrevision(
ctx, revid, lastrevid, oldnode, olddiff, actions)
diffmap[ctx.node()] = diff
Ian Moody
phabricator: convert conduit response JSON unicode to bytes inside callconduit...
r42063 newrevid = int(revision[b'object'][b'id'])
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 if revid:
action = b'updated'
else:
action = b'created'
# Create a local tag to note the association, if commit message
# does not have it already
m = _differentialrevisiondescre.search(ctx.description())
Ian Moody
py3: use r'' for group name arguments to MatchObjects in phabricator.py...
r42071 if not m or int(m.group(r'id')) != newrevid:
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 tagname = b'D%d' % newrevid
tags.tag(repo, tagname, ctx.node(), message=None, user=None,
date=None, local=True)
else:
# Nothing changed. But still set "newrevid" so the next revision
# could depend on this one.
newrevid = revid
action = b'skipped'
actiondesc = ui.label(
{b'created': _(b'created'),
b'skipped': _(b'skipped'),
b'updated': _(b'updated')}[action],
b'phabricator.action.%s' % action)
Ian Moody
py3: use %d instead of %s when formatting an int into a byte string...
r42070 drevdesc = ui.label(b'D%d' % newrevid, b'phabricator.drev')
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 nodedesc = ui.label(bytes(ctx), b'phabricator.node')
desc = ui.label(ctx.description().split(b'\n')[0], b'phabricator.desc')
ui.write(_(b'%s - %s - %s: %s\n') % (drevdesc, actiondesc, nodedesc,
desc))
drevids.append(newrevid)
lastrevid = newrevid
# Update commit messages and remove tags
if opts.get(b'amend'):
unfi = repo.unfiltered()
drevs = callconduit(repo, b'differential.query', {b'ids': drevids})
with repo.wlock(), repo.lock(), repo.transaction(b'phabsend'):
wnode = unfi[b'.'].node()
mapping = {} # {oldnode: [newnode]}
for i, rev in enumerate(revs):
old = unfi[rev]
drevid = drevids[i]
Ian Moody
phabricator: convert conduit response JSON unicode to bytes inside callconduit...
r42063 drev = [d for d in drevs if int(d[b'id']) == drevid][0]
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 newdesc = getdescfromdrev(drev)
# Make sure commit message contain "Differential Revision"
if old.description() != newdesc:
Matt Harbison
phabricator: warn if unable to amend, instead of aborting after posting...
r41198 if old.phase() == phases.public:
ui.warn(_("warning: not updating public commit %s\n")
% scmutil.formatchangeid(old))
continue
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 parents = [
mapping.get(old.p1().node(), (old.p1(),))[0],
mapping.get(old.p2().node(), (old.p2(),))[0],
]
new = context.metadataonlyctx(
repo, old, parents=parents, text=newdesc,
user=old.user(), date=old.date(), extra=old.extra())
newnode = new.commit()
mapping[old.node()] = [newnode]
# Update diff property
writediffproperties(unfi[newnode], diffmap[old.node()])
# Remove local tags since it's no longer necessary
tagname = b'D%d' % drevid
if tagname in repo.tags():
tags.tag(repo, tagname, nullid, message=None, user=None,
date=None, local=True)
scmutil.cleanupnodes(repo, mapping, b'phabsend', fixphase=True)
if wnode in mapping:
unfi.setparents(mapping[wnode][0])
# Map from "hg:meta" keys to header understood by "hg import". The order is
# consistent with "hg export" output.
Ian Moody
phabricator: convert conduit response JSON unicode to bytes inside callconduit...
r42063 _metanamemap = util.sortdict([(b'user', b'User'), (b'date', b'Date'),
(b'node', b'Node ID'), (b'parent', b'Parent ')])
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688
def _confirmbeforesend(repo, revs, oldmap):
url, token = readurltoken(repo)
ui = repo.ui
for rev in revs:
ctx = repo[rev]
desc = ctx.description().splitlines()[0]
oldnode, olddiff, drevid = oldmap.get(ctx.node(), (None, None, None))
if drevid:
drevdesc = ui.label(b'D%s' % drevid, b'phabricator.drev')
else:
drevdesc = ui.label(_(b'NEW'), b'phabricator.drev')
ui.write(_(b'%s - %s: %s\n')
% (drevdesc,
ui.label(bytes(ctx), b'phabricator.node'),
ui.label(desc, b'phabricator.desc')))
if ui.promptchoice(_(b'Send the above changes to %s (yn)?'
b'$$ &Yes $$ &No') % url):
return False
return True
_knownstatusnames = {b'accepted', b'needsreview', b'needsrevision', b'closed',
b'abandoned'}
def _getstatusname(drev):
"""get normalized status name from a Differential Revision"""
Ian Moody
phabricator: convert conduit response JSON unicode to bytes inside callconduit...
r42063 return drev[b'statusName'].replace(b' ', b'').lower()
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688
# Small language to specify differential revisions. Support symbols: (), :X,
# +, and -.
_elements = {
# token-type: binding-strength, primary, prefix, infix, suffix
b'(': (12, None, (b'group', 1, b')'), None, None),
b':': (8, None, (b'ancestors', 8), None, None),
b'&': (5, None, None, (b'and_', 5), None),
b'+': (4, None, None, (b'add', 4), None),
b'-': (4, None, None, (b'sub', 4), None),
b')': (0, None, None, None, None),
b'symbol': (0, b'symbol', None, None, None),
b'end': (0, None, None, None, None),
}
def _tokenize(text):
view = memoryview(text) # zero-copy slice
special = b'():+-& '
pos = 0
length = len(text)
while pos < length:
symbol = b''.join(itertools.takewhile(lambda ch: ch not in special,
Ian Moody
py3: use pycompat.iterbytestr to convert memoryview slice to bytestring...
r42116 pycompat.iterbytestr(view[pos:])))
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 if symbol:
yield (b'symbol', symbol, pos)
pos += len(symbol)
else: # special char, ignore space
if text[pos] != b' ':
yield (text[pos], None, pos)
pos += 1
yield (b'end', None, pos)
def _parse(text):
tree, pos = parser.parser(_elements).parse(_tokenize(text))
if pos != len(text):
raise error.ParseError(b'invalid token', pos)
return tree
def _parsedrev(symbol):
"""str -> int or None, ex. 'D45' -> 45; '12' -> 12; 'x' -> None"""
if symbol.startswith(b'D') and symbol[1:].isdigit():
return int(symbol[1:])
if symbol.isdigit():
return int(symbol)
def _prefetchdrevs(tree):
"""return ({single-drev-id}, {ancestor-drev-id}) to prefetch"""
drevs = set()
ancestordrevs = set()
op = tree[0]
if op == b'symbol':
r = _parsedrev(tree[1])
if r:
drevs.add(r)
elif op == b'ancestors':
r, a = _prefetchdrevs(tree[1])
drevs.update(r)
ancestordrevs.update(r)
ancestordrevs.update(a)
else:
for t in tree[1:]:
r, a = _prefetchdrevs(t)
drevs.update(r)
ancestordrevs.update(a)
return drevs, ancestordrevs
def querydrev(repo, spec):
"""return a list of "Differential Revision" dicts
spec is a string using a simple query language, see docstring in phabread
for details.
A "Differential Revision dict" looks like:
{
"id": "2",
"phid": "PHID-DREV-672qvysjcczopag46qty",
"title": "example",
"uri": "https://phab.example.com/D2",
"dateCreated": "1499181406",
"dateModified": "1499182103",
"authorPHID": "PHID-USER-tv3ohwc4v4jeu34otlye",
"status": "0",
"statusName": "Needs Review",
"properties": [],
"branch": null,
"summary": "",
"testPlan": "",
"lineCount": "2",
"activeDiffPHID": "PHID-DIFF-xoqnjkobbm6k4dk6hi72",
"diffs": [
"3",
"4",
],
"commits": [],
"reviewers": [],
"ccs": [],
"hashes": [],
"auxiliary": {
"phabricator:projects": [],
"phabricator:depends-on": [
"PHID-DREV-gbapp366kutjebt7agcd"
]
},
"repositoryPHID": "PHID-REPO-hub2hx62ieuqeheznasv",
"sourcePath": null
}
"""
def fetch(params):
"""params -> single drev or None"""
Ian Moody
py3: fix a few "dict keys as str instead of bytes" issues in phabricator.py...
r42068 key = (params.get(b'ids') or params.get(b'phids') or [None])[0]
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 if key in prefetched:
return prefetched[key]
drevs = callconduit(repo, b'differential.query', params)
# Fill prefetched with the result
for drev in drevs:
Ian Moody
phabricator: convert conduit response JSON unicode to bytes inside callconduit...
r42063 prefetched[drev[b'phid']] = drev
prefetched[int(drev[b'id'])] = drev
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 if key not in prefetched:
raise error.Abort(_(b'cannot get Differential Revision %r')
% params)
return prefetched[key]
def getstack(topdrevids):
"""given a top, get a stack from the bottom, [id] -> [id]"""
visited = set()
result = []
Ian Moody
py3: fix a few "dict keys as str instead of bytes" issues in phabricator.py...
r42068 queue = [{b'ids': [i]} for i in topdrevids]
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 while queue:
params = queue.pop()
drev = fetch(params)
Ian Moody
phabricator: convert conduit response JSON unicode to bytes inside callconduit...
r42063 if drev[b'id'] in visited:
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 continue
Ian Moody
phabricator: convert conduit response JSON unicode to bytes inside callconduit...
r42063 visited.add(drev[b'id'])
result.append(int(drev[b'id']))
auxiliary = drev.get(b'auxiliary', {})
depends = auxiliary.get(b'phabricator:depends-on', [])
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 for phid in depends:
queue.append({b'phids': [phid]})
result.reverse()
return smartset.baseset(result)
# Initialize prefetch cache
prefetched = {} # {id or phid: drev}
tree = _parse(spec)
drevs, ancestordrevs = _prefetchdrevs(tree)
# developer config: phabricator.batchsize
batchsize = repo.ui.configint(b'phabricator', b'batchsize')
# Prefetch Differential Revisions in batch
tofetch = set(drevs)
for r in ancestordrevs:
tofetch.update(range(max(1, r - batchsize), r + 1))
if drevs:
Ian Moody
phabricator: convert conduit response JSON unicode to bytes inside callconduit...
r42063 fetch({b'ids': list(tofetch)})
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 validids = sorted(set(getstack(list(ancestordrevs))) | set(drevs))
# Walk through the tree, return smartsets
def walk(tree):
op = tree[0]
if op == b'symbol':
drev = _parsedrev(tree[1])
if drev:
return smartset.baseset([drev])
elif tree[1] in _knownstatusnames:
drevs = [r for r in validids
if _getstatusname(prefetched[r]) == tree[1]]
return smartset.baseset(drevs)
else:
raise error.Abort(_(b'unknown symbol: %s') % tree[1])
elif op in {b'and_', b'add', b'sub'}:
assert len(tree) == 3
return getattr(operator, op)(walk(tree[1]), walk(tree[2]))
elif op == b'group':
return walk(tree[1])
elif op == b'ancestors':
return getstack(walk(tree[1]))
else:
raise error.ProgrammingError(b'illegal tree: %r' % tree)
return [prefetched[r] for r in walk(tree)]
def getdescfromdrev(drev):
"""get description (commit message) from "Differential Revision"
This is similar to differential.getcommitmessage API. But we only care
about limited fields: title, summary, test plan, and URL.
"""
Ian Moody
phabricator: convert conduit response JSON unicode to bytes inside callconduit...
r42063 title = drev[b'title']
summary = drev[b'summary'].rstrip()
testplan = drev[b'testPlan'].rstrip()
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 if testplan:
testplan = b'Test Plan:\n%s' % testplan
Ian Moody
phabricator: convert conduit response JSON unicode to bytes inside callconduit...
r42063 uri = b'Differential Revision: %s' % drev[b'uri']
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 return b'\n\n'.join(filter(None, [title, summary, testplan, uri]))
def getdiffmeta(diff):
"""get commit metadata (date, node, user, p1) from a diff object
The metadata could be "hg:meta", sent by phabsend, like:
"properties": {
"hg:meta": {
"date": "1499571514 25200",
"node": "98c08acae292b2faf60a279b4189beb6cff1414d",
"user": "Foo Bar <foo@example.com>",
"parent": "6d0abad76b30e4724a37ab8721d630394070fe16"
}
}
Or converted from "local:commits", sent by "arc", like:
"properties": {
"local:commits": {
"98c08acae292b2faf60a279b4189beb6cff1414d": {
"author": "Foo Bar",
"time": 1499546314,
"branch": "default",
"tag": "",
"commit": "98c08acae292b2faf60a279b4189beb6cff1414d",
"rev": "98c08acae292b2faf60a279b4189beb6cff1414d",
"local": "1000",
"parents": ["6d0abad76b30e4724a37ab8721d630394070fe16"],
"summary": "...",
"message": "...",
"authorEmail": "foo@example.com"
}
}
}
Note: metadata extracted from "local:commits" will lose time zone
information.
"""
Ian Moody
phabricator: convert conduit response JSON unicode to bytes inside callconduit...
r42063 props = diff.get(b'properties') or {}
meta = props.get(b'hg:meta')
if not meta and props.get(b'local:commits'):
commit = sorted(props[b'local:commits'].values())[0]
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 meta = {
Ian Moody
phabricator: convert conduit response JSON unicode to bytes inside callconduit...
r42063 b'date': b'%d 0' % commit[b'time'],
b'node': commit[b'rev'],
b'user': b'%s <%s>' % (commit[b'author'], commit[b'authorEmail']),
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 }
Ian Moody
phabricator: convert conduit response JSON unicode to bytes inside callconduit...
r42063 if len(commit.get(b'parents', ())) >= 1:
meta[b'parent'] = commit[b'parents'][0]
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 return meta or {}
def readpatch(repo, drevs, write):
"""generate plain-text patch readable by 'hg import'
write is usually ui.write. drevs is what "querydrev" returns, results of
"differential.query".
"""
# Prefetch hg:meta property for all diffs
Ian Moody
phabricator: convert conduit response JSON unicode to bytes inside callconduit...
r42063 diffids = sorted(set(max(int(v) for v in drev[b'diffs']) for drev in drevs))
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 diffs = callconduit(repo, b'differential.querydiffs', {b'ids': diffids})
# Generate patch for each drev
for drev in drevs:
Ian Moody
phabricator: convert conduit response JSON unicode to bytes inside callconduit...
r42063 repo.ui.note(_(b'reading D%s\n') % drev[b'id'])
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688
Ian Moody
phabricator: convert conduit response JSON unicode to bytes inside callconduit...
r42063 diffid = max(int(v) for v in drev[b'diffs'])
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 body = callconduit(repo, b'differential.getrawdiff',
{b'diffID': diffid})
desc = getdescfromdrev(drev)
header = b'# HG changeset patch\n'
# Try to preserve metadata from hg:meta property. Write hg patch
# headers that can be read by the "import" command. See patchheadermap
# and extract in mercurial/patch.py for supported headers.
Ian Moody
py3: fix a few "dict keys as str instead of bytes" issues in phabricator.py...
r42068 meta = getdiffmeta(diffs[b'%d' % diffid])
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 for k in _metanamemap.keys():
if k in meta:
header += b'# %s %s\n' % (_metanamemap[k], meta[k])
content = b'%s%s\n%s' % (header, desc, body)
Ian Moody
phabricator: convert conduit response JSON unicode to bytes inside callconduit...
r42063 write(content)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688
@vcrcommand(b'phabread',
[(b'', b'stack', False, _(b'read dependencies'))],
Matt Harbison
phabricator: assign commands to help categories...
r41103 _(b'DREVSPEC [OPTIONS]'),
helpcategory=command.CATEGORY_IMPORT_EXPORT)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 def phabread(ui, repo, spec, **opts):
"""print patches from Phabricator suitable for importing
DREVSPEC could be a Differential Revision identity, like ``D123``, or just
the number ``123``. It could also have common operators like ``+``, ``-``,
``&``, ``(``, ``)`` for complex queries. Prefix ``:`` could be used to
select a stack.
``abandoned``, ``accepted``, ``closed``, ``needsreview``, ``needsrevision``
could be used to filter patches by status. For performance reason, they
only represent a subset of non-status selections and cannot be used alone.
For example, ``:D6+8-(2+D4)`` selects a stack up to D6, plus D8 and exclude
D2 and D4. ``:D9 & needsreview`` selects "Needs Review" revisions in a
stack up to D9.
If --stack is given, follow dependencies information and read all patches.
It is equivalent to the ``:`` operator.
"""
Ian Moody
py3: use pycompat.byteskwargs on opts in phabricator.py...
r42136 opts = pycompat.byteskwargs(opts)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 if opts.get(b'stack'):
spec = b':(%s)' % spec
drevs = querydrev(repo, spec)
readpatch(repo, drevs, ui.write)
@vcrcommand(b'phabupdate',
[(b'', b'accept', False, _(b'accept revisions')),
(b'', b'reject', False, _(b'reject revisions')),
(b'', b'abandon', False, _(b'abandon revisions')),
(b'', b'reclaim', False, _(b'reclaim revisions')),
(b'm', b'comment', b'', _(b'comment on the last revision')),
Matt Harbison
phabricator: assign commands to help categories...
r41103 ], _(b'DREVSPEC [OPTIONS]'),
helpcategory=command.CATEGORY_IMPORT_EXPORT)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 def phabupdate(ui, repo, spec, **opts):
"""update Differential Revision in batch
DREVSPEC selects revisions. See :hg:`help phabread` for its usage.
"""
Ian Moody
py3: use pycompat.byteskwargs on opts in phabricator.py...
r42136 opts = pycompat.byteskwargs(opts)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 flags = [n for n in b'accept reject abandon reclaim'.split() if opts.get(n)]
if len(flags) > 1:
raise error.Abort(_(b'%s cannot be used together') % b', '.join(flags))
actions = []
for f in flags:
actions.append({b'type': f, b'value': b'true'})
drevs = querydrev(repo, spec)
for i, drev in enumerate(drevs):
if i + 1 == len(drevs) and opts.get(b'comment'):
actions.append({b'type': b'comment', b'value': opts[b'comment']})
if actions:
Ian Moody
phabricator: convert conduit response JSON unicode to bytes inside callconduit...
r42063 params = {b'objectIdentifier': drev[b'phid'],
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 b'transactions': actions}
callconduit(repo, b'differential.revision.edit', params)
templatekeyword = registrar.templatekeyword()
@templatekeyword(b'phabreview', requires={b'ctx'})
def template_review(context, mapping):
""":phabreview: Object describing the review for this changeset.
Has attributes `url` and `id`.
"""
ctx = context.resource(mapping, b'ctx')
m = _differentialrevisiondescre.search(ctx.description())
if m:
Augie Fackler
phabricator: fix templating bug by using hybriddict...
r39690 return templateutil.hybriddict({
Ian Moody
py3: use r'' for group name arguments to MatchObjects in phabricator.py...
r42071 b'url': m.group(r'url'),
Ian Moody
py3: use % instead of .format() on a bytestring...
r42072 b'id': b"D%s" % m.group(r'id'),
Augie Fackler
phabricator: fix templating bug by using hybriddict...
r39690 })
Matt Harbison
phabricator: teach {phabreview} to work without --amend...
r41199 else:
tags = ctx.repo().nodetags(ctx.node())
for t in tags:
if _differentialrevisiontagre.match(t):
url = ctx.repo().ui.config(b'phabricator', b'url')
if not url.endswith(b'/'):
url += b'/'
url += t
return templateutil.hybriddict({
b'url': url,
b'id': t,
})
return None