2017-07-03 06:08:09 +03:00
|
|
|
# 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
|
|
|
|
|
2017-07-03 06:08:09 +03:00
|
|
|
This extension provides a ``phabsend`` command which sends a stack of
|
2017-08-25 02:52:28 +03:00
|
|
|
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.
|
2017-07-03 06:08:09 +03:00
|
|
|
|
|
|
|
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.
|
|
|
|
|
2017-07-03 06:08:09 +03:00
|
|
|
Config::
|
|
|
|
|
|
|
|
[phabricator]
|
|
|
|
# Phabricator URL
|
|
|
|
url = https://phab.example.com/
|
|
|
|
|
|
|
|
# API token. Get it from https://$HOST/conduit/login/
|
|
|
|
token = cli-xxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
2017-07-03 06:08:09 +03:00
|
|
|
|
|
|
|
# Repo callsign. If a repo has a URL https://$HOST/diffusion/FOO, then its
|
|
|
|
# callsign is "FOO".
|
|
|
|
callsign = FOO
|
2017-07-03 06:08:09 +03:00
|
|
|
|
phabricator: add a config to use curl for communication
Not sure why, but I got `phabsend` hang on work network pretty frequently.
The traceback indicates it hangs at `_sslobj.do_handshake()`:
File "mercurial/sslutil.py", line 404, in wrapsocket
sslsocket = sslcontext.wrap_socket(sock, server_hostname=serverhostname)
File "/usr/lib/python2.7/ssl.py", line 363, in wrap_socket
_context=self)
File "/usr/lib/python2.7/ssl.py", line 611, in __init__
self.do_handshake()
File "/usr/lib/python2.7/ssl.py", line 840, in do_handshake
self._sslobj.do_handshake()
I had tried adding `timeout` in various places but they seem not effective.
It seems easier to just allow shelling out to `curl` with retry and timeout
flags.
This could also be helpful for people with an older Python installed without
modern security (SNI).
Differential Revision: https://phab.mercurial-scm.org/D605
2017-09-01 22:13:17 +03:00
|
|
|
# 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
|
2017-07-03 06:08:09 +03:00
|
|
|
"""
|
|
|
|
|
|
|
|
from __future__ import absolute_import
|
|
|
|
|
phabricator: add a small language to query Differential Revisions
Previously, `phabread` can only be used to read a single patch, or a single
stack of patches. In the future, we want to have more complex queries like
filtering with status (open, accepted, closed, etc), or maybe more complex
like filtering by reviewers etc. The command line flag approach won't scale
with that.
Besides, we might want to have other commands to update Differential
Revision status in batch, like accepting a stack using a single command.
Therefore, this patch adds a small language. It has basic set operations:
`&`, `+`, `-` and an ancestor operator to support `--stack`.
Test Plan:
Try querying this Phabricator instance:
hg phabread 1+2 # 1, 2
hg phabread D2+D1 # 2, 1
hg phabread ':118-115+:2-1' # 114, 116, 117, 118, 2
hg phabread '((:118-(D115+117)))&:117' # 114, 116
hg phabread ':2&:117' --debug # differential.query is called only once
Make sure the output is expected and prefetch works.
Differential Revision: https://phab.mercurial-scm.org/D125
2017-07-18 09:19:11 +03:00
|
|
|
import itertools
|
2017-07-03 06:08:09 +03:00
|
|
|
import json
|
phabricator: add a small language to query Differential Revisions
Previously, `phabread` can only be used to read a single patch, or a single
stack of patches. In the future, we want to have more complex queries like
filtering with status (open, accepted, closed, etc), or maybe more complex
like filtering by reviewers etc. The command line flag approach won't scale
with that.
Besides, we might want to have other commands to update Differential
Revision status in batch, like accepting a stack using a single command.
Therefore, this patch adds a small language. It has basic set operations:
`&`, `+`, `-` and an ancestor operator to support `--stack`.
Test Plan:
Try querying this Phabricator instance:
hg phabread 1+2 # 1, 2
hg phabread D2+D1 # 2, 1
hg phabread ':118-115+:2-1' # 114, 116, 117, 118, 2
hg phabread '((:118-(D115+117)))&:117' # 114, 116
hg phabread ':2&:117' --debug # differential.query is called only once
Make sure the output is expected and prefetch works.
Differential Revision: https://phab.mercurial-scm.org/D125
2017-07-18 09:19:11 +03:00
|
|
|
import operator
|
2017-07-03 06:08:09 +03:00
|
|
|
import re
|
2017-07-03 06:08:09 +03:00
|
|
|
|
2017-07-11 18:17:29 +03:00
|
|
|
from mercurial.node import bin, nullid
|
2017-07-03 06:08:09 +03:00
|
|
|
from mercurial.i18n import _
|
|
|
|
from mercurial import (
|
2017-08-04 22:39:29 +03:00
|
|
|
cmdutil,
|
|
|
|
context,
|
2017-07-03 06:08:09 +03:00
|
|
|
encoding,
|
2017-07-03 06:08:09 +03:00
|
|
|
error,
|
2017-07-03 06:08:09 +03:00
|
|
|
mdiff,
|
2017-08-02 20:49:57 +03:00
|
|
|
obsutil,
|
phabricator: add a small language to query Differential Revisions
Previously, `phabread` can only be used to read a single patch, or a single
stack of patches. In the future, we want to have more complex queries like
filtering with status (open, accepted, closed, etc), or maybe more complex
like filtering by reviewers etc. The command line flag approach won't scale
with that.
Besides, we might want to have other commands to update Differential
Revision status in batch, like accepting a stack using a single command.
Therefore, this patch adds a small language. It has basic set operations:
`&`, `+`, `-` and an ancestor operator to support `--stack`.
Test Plan:
Try querying this Phabricator instance:
hg phabread 1+2 # 1, 2
hg phabread D2+D1 # 2, 1
hg phabread ':118-115+:2-1' # 114, 116, 117, 118, 2
hg phabread '((:118-(D115+117)))&:117' # 114, 116
hg phabread ':2&:117' --debug # differential.query is called only once
Make sure the output is expected and prefetch works.
Differential Revision: https://phab.mercurial-scm.org/D125
2017-07-18 09:19:11 +03:00
|
|
|
parser,
|
2017-07-03 06:08:09 +03:00
|
|
|
patch,
|
2017-07-03 06:08:09 +03:00
|
|
|
registrar,
|
2017-07-03 06:08:09 +03:00
|
|
|
scmutil,
|
phabricator: add a small language to query Differential Revisions
Previously, `phabread` can only be used to read a single patch, or a single
stack of patches. In the future, we want to have more complex queries like
filtering with status (open, accepted, closed, etc), or maybe more complex
like filtering by reviewers etc. The command line flag approach won't scale
with that.
Besides, we might want to have other commands to update Differential
Revision status in batch, like accepting a stack using a single command.
Therefore, this patch adds a small language. It has basic set operations:
`&`, `+`, `-` and an ancestor operator to support `--stack`.
Test Plan:
Try querying this Phabricator instance:
hg phabread 1+2 # 1, 2
hg phabread D2+D1 # 2, 1
hg phabread ':118-115+:2-1' # 114, 116, 117, 118, 2
hg phabread '((:118-(D115+117)))&:117' # 114, 116
hg phabread ':2&:117' --debug # differential.query is called only once
Make sure the output is expected and prefetch works.
Differential Revision: https://phab.mercurial-scm.org/D125
2017-07-18 09:19:11 +03:00
|
|
|
smartset,
|
2017-07-03 06:08:09 +03:00
|
|
|
tags,
|
2017-07-03 06:08:09 +03:00
|
|
|
url as urlmod,
|
|
|
|
util,
|
|
|
|
)
|
|
|
|
|
|
|
|
cmdtable = {}
|
|
|
|
command = registrar.command(cmdtable)
|
|
|
|
|
2017-08-25 04:00:23 +03:00
|
|
|
colortable = {
|
|
|
|
'phabricator.action.created': 'green',
|
|
|
|
'phabricator.action.skipped': 'magenta',
|
|
|
|
'phabricator.action.updated': 'magenta',
|
|
|
|
'phabricator.desc': '',
|
|
|
|
'phabricator.drev': 'bold',
|
|
|
|
'phabricator.node': '',
|
|
|
|
}
|
|
|
|
|
2017-07-03 06:08:09 +03:00
|
|
|
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()
|
|
|
|
repo.ui.debug('Conduit Call: %s %s\n' % (url, params))
|
|
|
|
params = params.copy()
|
|
|
|
params['api.token'] = token
|
phabricator: add a config to use curl for communication
Not sure why, but I got `phabsend` hang on work network pretty frequently.
The traceback indicates it hangs at `_sslobj.do_handshake()`:
File "mercurial/sslutil.py", line 404, in wrapsocket
sslsocket = sslcontext.wrap_socket(sock, server_hostname=serverhostname)
File "/usr/lib/python2.7/ssl.py", line 363, in wrap_socket
_context=self)
File "/usr/lib/python2.7/ssl.py", line 611, in __init__
self.do_handshake()
File "/usr/lib/python2.7/ssl.py", line 840, in do_handshake
self._sslobj.do_handshake()
I had tried adding `timeout` in various places but they seem not effective.
It seems easier to just allow shelling out to `curl` with retry and timeout
flags.
This could also be helpful for people with an older Python installed without
modern security (SNI).
Differential Revision: https://phab.mercurial-scm.org/D605
2017-09-01 22:13:17 +03:00
|
|
|
data = urlencodenested(params)
|
|
|
|
curlcmd = repo.ui.config('phabricator', 'curlcmd')
|
|
|
|
if curlcmd:
|
|
|
|
sin, sout = util.popen2('%s -d @- %s' % (curlcmd, util.shellquote(url)))
|
|
|
|
sin.write(data)
|
|
|
|
sin.close()
|
|
|
|
body = sout.read()
|
|
|
|
else:
|
|
|
|
urlopener = urlmod.opener(repo.ui, authinfo)
|
|
|
|
request = util.urlreq.request(url, data=data)
|
|
|
|
body = urlopener.open(request).read()
|
2017-07-03 06:08:09 +03:00
|
|
|
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)
|
2017-07-03 06:08:09 +03:00
|
|
|
|
|
|
|
def getrepophid(repo):
|
|
|
|
"""given callsign, return repository PHID or None"""
|
|
|
|
# developer config: phabricator.repophid
|
|
|
|
repophid = repo.ui.config('phabricator', 'repophid')
|
|
|
|
if repophid:
|
|
|
|
return repophid
|
|
|
|
callsign = repo.ui.config('phabricator', 'callsign')
|
|
|
|
if not callsign:
|
|
|
|
return None
|
|
|
|
query = callconduit(repo, 'diffusion.repository.search',
|
|
|
|
{'constraints': {'callsigns': [callsign]}})
|
|
|
|
if len(query[r'data']) == 0:
|
|
|
|
return None
|
|
|
|
repophid = encoding.strtolocal(query[r'data'][0][r'phid'])
|
|
|
|
repo.ui.setconfig('phabricator', 'repophid', repophid)
|
|
|
|
return repophid
|
|
|
|
|
2017-07-05 02:16:37 +03:00
|
|
|
_differentialrevisiontagre = re.compile('\AD([1-9][0-9]*)\Z')
|
|
|
|
_differentialrevisiondescre = re.compile(
|
2017-07-18 05:52:50 +03:00
|
|
|
'^Differential Revision:\s*(?:.*)D([1-9][0-9]*)$', re.M)
|
2017-07-03 06:08:09 +03:00
|
|
|
|
2017-07-10 23:50:50 +03:00
|
|
|
def getoldnodedrevmap(repo, nodelist):
|
|
|
|
"""find previous nodes that has been sent to Phabricator
|
|
|
|
|
2017-07-18 05:52:50 +03:00
|
|
|
return {node: (oldnode, Differential diff, Differential Revision ID)}
|
2017-07-10 23:50:50 +03:00
|
|
|
for node in nodelist with known previous sent versions, or associated
|
2017-07-18 05:52:50 +03:00
|
|
|
Differential Revision IDs. ``oldnode`` and ``Differential diff`` could
|
|
|
|
be ``None``.
|
2017-07-03 06:08:09 +03:00
|
|
|
|
2017-07-18 05:52:50 +03:00
|
|
|
Examines commit messages like "Differential Revision:" to get the
|
|
|
|
association information.
|
2017-07-05 02:16:37 +03:00
|
|
|
|
2017-07-18 05:52:50 +03:00
|
|
|
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.
|
2017-07-03 06:08:09 +03:00
|
|
|
"""
|
2017-07-10 23:50:50 +03:00
|
|
|
url, token = readurltoken(repo)
|
|
|
|
unfi = repo.unfiltered()
|
2017-07-03 06:08:09 +03:00
|
|
|
nodemap = unfi.changelog.nodemap
|
2017-07-05 02:16:37 +03:00
|
|
|
|
2017-07-18 05:52:50 +03:00
|
|
|
result = {} # {node: (oldnode?, lastdiff?, drev)}
|
|
|
|
toconfirm = {} # {node: (force, {precnode}, drev)}
|
2017-07-10 23:50:50 +03:00
|
|
|
for node in nodelist:
|
|
|
|
ctx = unfi[node]
|
2017-07-11 18:17:29 +03:00
|
|
|
# For tags like "D123", put them into "toconfirm" to verify later
|
2017-08-02 20:49:57 +03:00
|
|
|
precnodes = list(obsutil.allpredecessors(unfi.obsstore, [node]))
|
2017-07-11 18:17:29 +03:00
|
|
|
for n in precnodes:
|
2017-07-10 23:50:50 +03:00
|
|
|
if n in nodemap:
|
|
|
|
for tag in unfi.nodetags(n):
|
|
|
|
m = _differentialrevisiontagre.match(tag)
|
|
|
|
if m:
|
2017-07-18 05:52:50 +03:00
|
|
|
toconfirm[node] = (0, set(precnodes), int(m.group(1)))
|
2017-07-10 23:50:50 +03:00
|
|
|
continue
|
|
|
|
|
2017-07-18 05:52:50 +03:00
|
|
|
# Check commit message
|
2017-07-10 23:50:50 +03:00
|
|
|
m = _differentialrevisiondescre.search(ctx.description())
|
|
|
|
if m:
|
2017-07-18 05:52:50 +03:00
|
|
|
toconfirm[node] = (1, set(precnodes), int(m.group(1)))
|
2017-07-10 23:50:50 +03:00
|
|
|
|
2017-07-11 18:17:29 +03:00
|
|
|
# Double check if tags are genuine by collecting all old nodes from
|
|
|
|
# Phabricator, and expect precursors overlap with it.
|
|
|
|
if toconfirm:
|
2017-07-18 05:52:50 +03:00
|
|
|
drevs = [drev for force, precs, drev in toconfirm.values()]
|
|
|
|
alldiffs = callconduit(unfi, 'differential.querydiffs',
|
|
|
|
{'revisionIDs': drevs})
|
|
|
|
getnode = lambda d: bin(encoding.unitolocal(
|
|
|
|
getdiffmeta(d).get(r'node', ''))) or None
|
|
|
|
for newnode, (force, precset, drev) in toconfirm.items():
|
|
|
|
diffs = [d for d in alldiffs.values()
|
|
|
|
if int(d[r'revisionID']) == drev]
|
|
|
|
|
|
|
|
# "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):
|
2017-07-11 18:17:29 +03:00
|
|
|
tagname = 'D%d' % drev
|
|
|
|
tags.tag(repo, tagname, nullid, message=None, user=None,
|
|
|
|
date=None, local=True)
|
|
|
|
unfi.ui.warn(_('D%s: local tag removed - does not match '
|
|
|
|
'Differential history\n') % drev)
|
2017-07-18 05:52:50 +03:00
|
|
|
continue
|
|
|
|
|
|
|
|
# Find the last node using Phabricator metadata, and make sure it
|
|
|
|
# exists in the repo
|
|
|
|
oldnode = lastdiff = None
|
|
|
|
if diffs:
|
|
|
|
lastdiff = max(diffs, key=lambda d: int(d[r'id']))
|
|
|
|
oldnode = getnode(lastdiff)
|
|
|
|
if oldnode and oldnode not in nodemap:
|
|
|
|
oldnode = None
|
|
|
|
|
|
|
|
result[newnode] = (oldnode, lastdiff, drev)
|
2017-07-11 18:17:29 +03:00
|
|
|
|
2017-07-10 23:50:50 +03:00
|
|
|
return result
|
2017-07-03 06:08:09 +03:00
|
|
|
|
|
|
|
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 = {'diff': getdiff(ctx, mdiff.diffopts(git=True, context=32767))}
|
|
|
|
if repophid:
|
|
|
|
params['repositoryPHID'] = repophid
|
|
|
|
diff = callconduit(repo, 'differential.createrawdiff', params)
|
|
|
|
if not diff:
|
|
|
|
raise error.Abort(_('cannot create diff for %s') % ctx)
|
|
|
|
return diff
|
|
|
|
|
|
|
|
def writediffproperties(ctx, diff):
|
|
|
|
"""write metadata to diff so patches could be applied losslessly"""
|
|
|
|
params = {
|
|
|
|
'diff_id': diff[r'id'],
|
|
|
|
'name': 'hg:meta',
|
|
|
|
'data': json.dumps({
|
|
|
|
'user': ctx.user(),
|
|
|
|
'date': '%d %d' % ctx.date(),
|
2017-07-05 02:36:48 +03:00
|
|
|
'node': ctx.hex(),
|
|
|
|
'parent': ctx.p1().hex(),
|
2017-07-03 06:08:09 +03:00
|
|
|
}),
|
|
|
|
}
|
|
|
|
callconduit(ctx.repo(), 'differential.setdiffproperty', params)
|
|
|
|
|
2017-07-11 18:52:55 +03:00
|
|
|
def createdifferentialrevision(ctx, revid=None, parentrevid=None, oldnode=None,
|
2017-08-04 22:21:23 +03:00
|
|
|
olddiff=None, actions=None):
|
2017-07-03 06:08:09 +03:00
|
|
|
"""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.
|
2017-07-05 02:36:48 +03:00
|
|
|
|
|
|
|
If oldnode is not None, check if the patch content (without commit message
|
|
|
|
and metadata) has changed before creating another diff.
|
2017-07-11 18:52:55 +03:00
|
|
|
|
|
|
|
If actions is not None, they will be appended to the transaction.
|
2017-07-03 06:08:09 +03:00
|
|
|
"""
|
|
|
|
repo = ctx.repo()
|
2017-07-05 02:36:48 +03:00
|
|
|
if oldnode:
|
2017-08-25 03:25:18 +03:00
|
|
|
diffopts = mdiff.diffopts(git=True, context=32767)
|
2017-07-05 02:36:48 +03:00
|
|
|
oldctx = repo.unfiltered()[oldnode]
|
|
|
|
neednewdiff = (getdiff(ctx, diffopts) != getdiff(oldctx, diffopts))
|
|
|
|
else:
|
|
|
|
neednewdiff = True
|
|
|
|
|
|
|
|
transactions = []
|
|
|
|
if neednewdiff:
|
|
|
|
diff = creatediff(ctx)
|
|
|
|
transactions.append({'type': 'update', 'value': diff[r'phid']})
|
2017-08-04 22:21:23 +03:00
|
|
|
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)
|
2017-07-03 06:08:09 +03:00
|
|
|
|
|
|
|
# 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:
|
|
|
|
summary = 'Depends on D%s' % parentrevid
|
|
|
|
transactions += [{'type': 'summary', 'value': summary},
|
|
|
|
{'type': 'summary', 'value': ' '}]
|
|
|
|
|
2017-07-11 18:52:55 +03:00
|
|
|
if actions:
|
|
|
|
transactions += actions
|
|
|
|
|
2017-07-03 06:08:09 +03:00
|
|
|
# Parse commit message and update related fields.
|
|
|
|
desc = ctx.description()
|
|
|
|
info = callconduit(repo, 'differential.parsecommitmessage',
|
|
|
|
{'corpus': desc})
|
|
|
|
for k, v in info[r'fields'].items():
|
|
|
|
if k in ['title', 'summary', 'testPlan']:
|
|
|
|
transactions.append({'type': k, 'value': v})
|
|
|
|
|
|
|
|
params = {'transactions': transactions}
|
|
|
|
if revid is not None:
|
|
|
|
# Update an existing Differential Revision
|
|
|
|
params['objectIdentifier'] = revid
|
|
|
|
|
|
|
|
revision = callconduit(repo, 'differential.revision.edit', params)
|
|
|
|
if not revision:
|
|
|
|
raise error.Abort(_('cannot create revision for %s') % ctx)
|
|
|
|
|
2017-08-04 22:39:29 +03:00
|
|
|
return revision, diff
|
2017-07-03 06:08:09 +03:00
|
|
|
|
2017-07-11 18:52:55 +03:00
|
|
|
def userphids(repo, names):
|
|
|
|
"""convert user names to PHIDs"""
|
|
|
|
query = {'constraints': {'usernames': names}}
|
|
|
|
result = callconduit(repo, 'user.search', query)
|
|
|
|
# username not found is not an error of the API. So check if we have missed
|
|
|
|
# some names here.
|
|
|
|
data = result[r'data']
|
|
|
|
resolved = set(entry[r'fields'][r'username'] for entry in data)
|
|
|
|
unresolved = set(names) - resolved
|
|
|
|
if unresolved:
|
|
|
|
raise error.Abort(_('unknown username: %s')
|
|
|
|
% ' '.join(sorted(unresolved)))
|
|
|
|
return [entry[r'phid'] for entry in data]
|
|
|
|
|
2017-07-03 06:08:09 +03:00
|
|
|
@command('phabsend',
|
2017-07-11 18:52:55 +03:00
|
|
|
[('r', 'rev', [], _('revisions to send'), _('REV')),
|
2017-08-25 02:52:28 +03:00
|
|
|
('', 'amend', True, _('update commit messages')),
|
2017-08-03 00:39:33 +03:00
|
|
|
('', 'reviewer', [], _('specify reviewers')),
|
|
|
|
('', 'confirm', None, _('ask for confirmation before sending'))],
|
2017-07-03 06:08:09 +03:00
|
|
|
_('REV [OPTIONS]'))
|
|
|
|
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.
|
2017-08-03 00:39:33 +03:00
|
|
|
|
2017-08-04 22:39:29 +03:00
|
|
|
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.
|
|
|
|
|
2017-08-03 00:39:33 +03:00
|
|
|
The --confirm option lets you confirm changesets before sending them. You
|
|
|
|
can also add following to your configuration file to make it default
|
2017-08-25 03:26:10 +03:00
|
|
|
behaviour::
|
2017-08-03 00:39:33 +03:00
|
|
|
|
2017-08-25 03:26:10 +03:00
|
|
|
[phabsend]
|
|
|
|
confirm = true
|
2017-08-04 22:39:29 +03:00
|
|
|
|
|
|
|
phabsend will check obsstore and the above association to decide whether to
|
|
|
|
update an existing Differential Revision, or create a new one.
|
2017-07-03 06:08:09 +03:00
|
|
|
"""
|
|
|
|
revs = list(revs) + opts.get('rev', [])
|
|
|
|
revs = scmutil.revrange(repo, revs)
|
|
|
|
|
2017-07-05 02:36:48 +03:00
|
|
|
if not revs:
|
|
|
|
raise error.Abort(_('phabsend requires at least one changeset'))
|
2017-08-04 22:39:29 +03:00
|
|
|
if opts.get('amend'):
|
|
|
|
cmdutil.checkunfinished(repo)
|
2017-07-05 02:36:48 +03:00
|
|
|
|
2017-08-25 03:44:08 +03:00
|
|
|
# {newnode: (oldnode, olddiff, olddrev}
|
|
|
|
oldmap = getoldnodedrevmap(repo, [repo[r].node() for r in revs])
|
|
|
|
|
2017-08-03 00:39:33 +03:00
|
|
|
confirm = ui.configbool('phabsend', 'confirm')
|
|
|
|
confirm |= bool(opts.get('confirm'))
|
|
|
|
if confirm:
|
2017-08-25 03:44:08 +03:00
|
|
|
confirmed = _confirmbeforesend(repo, revs, oldmap)
|
2017-08-03 00:39:33 +03:00
|
|
|
if not confirmed:
|
|
|
|
raise error.Abort(_('phabsend cancelled'))
|
|
|
|
|
2017-07-11 18:52:55 +03:00
|
|
|
actions = []
|
|
|
|
reviewers = opts.get('reviewer', [])
|
|
|
|
if reviewers:
|
|
|
|
phids = userphids(repo, reviewers)
|
|
|
|
actions.append({'type': 'reviewers.add', 'value': phids})
|
|
|
|
|
2017-08-04 22:39:29 +03:00
|
|
|
drevids = [] # [int]
|
|
|
|
diffmap = {} # {newnode: diff}
|
|
|
|
|
2017-07-03 06:08:09 +03:00
|
|
|
# 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('sending rev %d\n' % rev)
|
|
|
|
ctx = repo[rev]
|
|
|
|
|
|
|
|
# Get Differential Revision ID
|
2017-07-18 05:52:50 +03:00
|
|
|
oldnode, olddiff, revid = oldmap.get(ctx.node(), (None, None, None))
|
2017-08-04 22:39:29 +03:00
|
|
|
if oldnode != ctx.node() or opts.get('amend'):
|
2017-07-03 06:08:09 +03:00
|
|
|
# Create or update Differential Revision
|
2017-08-04 22:39:29 +03:00
|
|
|
revision, diff = createdifferentialrevision(
|
|
|
|
ctx, revid, lastrevid, oldnode, olddiff, actions)
|
|
|
|
diffmap[ctx.node()] = diff
|
2017-07-03 06:08:09 +03:00
|
|
|
newrevid = int(revision[r'object'][r'id'])
|
|
|
|
if revid:
|
2017-08-25 04:00:23 +03:00
|
|
|
action = 'updated'
|
2017-07-03 06:08:09 +03:00
|
|
|
else:
|
2017-08-25 04:00:23 +03:00
|
|
|
action = 'created'
|
2017-07-03 06:08:09 +03:00
|
|
|
|
2017-08-04 22:39:29 +03:00
|
|
|
# Create a local tag to note the association, if commit message
|
|
|
|
# does not have it already
|
|
|
|
m = _differentialrevisiondescre.search(ctx.description())
|
|
|
|
if not m or int(m.group(1)) != newrevid:
|
|
|
|
tagname = 'D%d' % newrevid
|
|
|
|
tags.tag(repo, tagname, ctx.node(), message=None, user=None,
|
|
|
|
date=None, local=True)
|
2017-07-03 06:08:09 +03:00
|
|
|
else:
|
|
|
|
# Nothing changed. But still set "newrevid" so the next revision
|
|
|
|
# could depend on this one.
|
|
|
|
newrevid = revid
|
2017-08-25 04:00:23 +03:00
|
|
|
action = 'skipped'
|
|
|
|
|
|
|
|
actiondesc = ui.label(
|
|
|
|
{'created': _('created'),
|
|
|
|
'skipped': _('skipped'),
|
|
|
|
'updated': _('updated')}[action],
|
|
|
|
'phabricator.action.%s' % action)
|
|
|
|
drevdesc = ui.label('D%s' % newrevid, 'phabricator.drev')
|
|
|
|
nodedesc = ui.label(bytes(ctx), 'phabricator.node')
|
|
|
|
desc = ui.label(ctx.description().split('\n')[0], 'phabricator.desc')
|
|
|
|
ui.write(_('%s - %s - %s: %s\n') % (drevdesc, actiondesc, nodedesc,
|
|
|
|
desc))
|
2017-08-04 22:39:29 +03:00
|
|
|
drevids.append(newrevid)
|
2017-07-03 06:08:09 +03:00
|
|
|
lastrevid = newrevid
|
2017-07-03 06:08:09 +03:00
|
|
|
|
2017-08-04 22:39:29 +03:00
|
|
|
# Update commit messages and remove tags
|
|
|
|
if opts.get('amend'):
|
|
|
|
unfi = repo.unfiltered()
|
|
|
|
drevs = callconduit(repo, 'differential.query', {'ids': drevids})
|
|
|
|
with repo.wlock(), repo.lock(), repo.transaction('phabsend'):
|
|
|
|
wnode = unfi['.'].node()
|
|
|
|
mapping = {} # {oldnode: [newnode]}
|
|
|
|
for i, rev in enumerate(revs):
|
|
|
|
old = unfi[rev]
|
|
|
|
drevid = drevids[i]
|
|
|
|
drev = [d for d in drevs if int(d[r'id']) == drevid][0]
|
|
|
|
newdesc = getdescfromdrev(drev)
|
|
|
|
# Make sure commit message contain "Differential Revision"
|
|
|
|
if old.description() != newdesc:
|
|
|
|
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 = '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, 'phabsend')
|
|
|
|
if wnode in mapping:
|
|
|
|
unfi.setparents(mapping[wnode][0])
|
|
|
|
|
2017-07-05 02:36:48 +03:00
|
|
|
# Map from "hg:meta" keys to header understood by "hg import". The order is
|
|
|
|
# consistent with "hg export" output.
|
|
|
|
_metanamemap = util.sortdict([(r'user', 'User'), (r'date', 'Date'),
|
|
|
|
(r'node', 'Node ID'), (r'parent', 'Parent ')])
|
|
|
|
|
2017-08-25 03:44:08 +03:00
|
|
|
def _confirmbeforesend(repo, revs, oldmap):
|
2017-08-25 03:31:33 +03:00
|
|
|
url, token = readurltoken(repo)
|
2017-08-03 00:39:33 +03:00
|
|
|
ui = repo.ui
|
|
|
|
for rev in revs:
|
|
|
|
ctx = repo[rev]
|
|
|
|
desc = ctx.description().splitlines()[0]
|
2017-08-25 03:44:08 +03:00
|
|
|
oldnode, olddiff, drevid = oldmap.get(ctx.node(), (None, None, None))
|
|
|
|
if drevid:
|
|
|
|
drevdesc = ui.label('D%s' % drevid, 'phabricator.drev')
|
|
|
|
else:
|
|
|
|
drevdesc = ui.label(_('NEW'), 'phabricator.drev')
|
|
|
|
|
|
|
|
ui.write(_('%s - %s: %s\n') % (drevdesc,
|
|
|
|
ui.label(bytes(ctx), 'phabricator.node'),
|
|
|
|
ui.label(desc, 'phabricator.desc')))
|
2017-08-03 00:39:33 +03:00
|
|
|
|
2017-08-25 03:31:33 +03:00
|
|
|
if ui.promptchoice(_('Send the above changes to %s (yn)?'
|
|
|
|
'$$ &Yes $$ &No') % url):
|
2017-08-03 00:39:33 +03:00
|
|
|
return False
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
2017-07-18 11:34:55 +03:00
|
|
|
_knownstatusnames = {'accepted', 'needsreview', 'needsrevision', 'closed',
|
|
|
|
'abandoned'}
|
|
|
|
|
|
|
|
def _getstatusname(drev):
|
|
|
|
"""get normalized status name from a Differential Revision"""
|
|
|
|
return drev[r'statusName'].replace(' ', '').lower()
|
|
|
|
|
phabricator: add a small language to query Differential Revisions
Previously, `phabread` can only be used to read a single patch, or a single
stack of patches. In the future, we want to have more complex queries like
filtering with status (open, accepted, closed, etc), or maybe more complex
like filtering by reviewers etc. The command line flag approach won't scale
with that.
Besides, we might want to have other commands to update Differential
Revision status in batch, like accepting a stack using a single command.
Therefore, this patch adds a small language. It has basic set operations:
`&`, `+`, `-` and an ancestor operator to support `--stack`.
Test Plan:
Try querying this Phabricator instance:
hg phabread 1+2 # 1, 2
hg phabread D2+D1 # 2, 1
hg phabread ':118-115+:2-1' # 114, 116, 117, 118, 2
hg phabread '((:118-(D115+117)))&:117' # 114, 116
hg phabread ':2&:117' --debug # differential.query is called only once
Make sure the output is expected and prefetch works.
Differential Revision: https://phab.mercurial-scm.org/D125
2017-07-18 09:19:11 +03:00
|
|
|
# Small language to specify differential revisions. Support symbols: (), :X,
|
|
|
|
# +, and -.
|
|
|
|
|
|
|
|
_elements = {
|
|
|
|
# token-type: binding-strength, primary, prefix, infix, suffix
|
|
|
|
'(': (12, None, ('group', 1, ')'), None, None),
|
|
|
|
':': (8, None, ('ancestors', 8), None, None),
|
|
|
|
'&': (5, None, None, ('and_', 5), None),
|
|
|
|
'+': (4, None, None, ('add', 4), None),
|
|
|
|
'-': (4, None, None, ('sub', 4), None),
|
|
|
|
')': (0, None, None, None, None),
|
|
|
|
'symbol': (0, 'symbol', None, None, None),
|
|
|
|
'end': (0, None, None, None, None),
|
|
|
|
}
|
|
|
|
|
|
|
|
def _tokenize(text):
|
|
|
|
view = memoryview(text) # zero-copy slice
|
2017-07-18 11:34:55 +03:00
|
|
|
special = '():+-& '
|
phabricator: add a small language to query Differential Revisions
Previously, `phabread` can only be used to read a single patch, or a single
stack of patches. In the future, we want to have more complex queries like
filtering with status (open, accepted, closed, etc), or maybe more complex
like filtering by reviewers etc. The command line flag approach won't scale
with that.
Besides, we might want to have other commands to update Differential
Revision status in batch, like accepting a stack using a single command.
Therefore, this patch adds a small language. It has basic set operations:
`&`, `+`, `-` and an ancestor operator to support `--stack`.
Test Plan:
Try querying this Phabricator instance:
hg phabread 1+2 # 1, 2
hg phabread D2+D1 # 2, 1
hg phabread ':118-115+:2-1' # 114, 116, 117, 118, 2
hg phabread '((:118-(D115+117)))&:117' # 114, 116
hg phabread ':2&:117' --debug # differential.query is called only once
Make sure the output is expected and prefetch works.
Differential Revision: https://phab.mercurial-scm.org/D125
2017-07-18 09:19:11 +03:00
|
|
|
pos = 0
|
|
|
|
length = len(text)
|
|
|
|
while pos < length:
|
|
|
|
symbol = ''.join(itertools.takewhile(lambda ch: ch not in special,
|
|
|
|
view[pos:]))
|
|
|
|
if symbol:
|
|
|
|
yield ('symbol', symbol, pos)
|
|
|
|
pos += len(symbol)
|
2017-07-18 11:34:55 +03:00
|
|
|
else: # special char, ignore space
|
|
|
|
if text[pos] != ' ':
|
|
|
|
yield (text[pos], None, pos)
|
phabricator: add a small language to query Differential Revisions
Previously, `phabread` can only be used to read a single patch, or a single
stack of patches. In the future, we want to have more complex queries like
filtering with status (open, accepted, closed, etc), or maybe more complex
like filtering by reviewers etc. The command line flag approach won't scale
with that.
Besides, we might want to have other commands to update Differential
Revision status in batch, like accepting a stack using a single command.
Therefore, this patch adds a small language. It has basic set operations:
`&`, `+`, `-` and an ancestor operator to support `--stack`.
Test Plan:
Try querying this Phabricator instance:
hg phabread 1+2 # 1, 2
hg phabread D2+D1 # 2, 1
hg phabread ':118-115+:2-1' # 114, 116, 117, 118, 2
hg phabread '((:118-(D115+117)))&:117' # 114, 116
hg phabread ':2&:117' --debug # differential.query is called only once
Make sure the output is expected and prefetch works.
Differential Revision: https://phab.mercurial-scm.org/D125
2017-07-18 09:19:11 +03:00
|
|
|
pos += 1
|
|
|
|
yield ('end', None, pos)
|
|
|
|
|
|
|
|
def _parse(text):
|
|
|
|
tree, pos = parser.parser(_elements).parse(_tokenize(text))
|
|
|
|
if pos != len(text):
|
|
|
|
raise error.ParseError('invalid token', pos)
|
|
|
|
return tree
|
|
|
|
|
|
|
|
def _parsedrev(symbol):
|
|
|
|
"""str -> int or None, ex. 'D45' -> 45; '12' -> 12; 'x' -> None"""
|
|
|
|
if symbol.startswith('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 == 'symbol':
|
|
|
|
r = _parsedrev(tree[1])
|
|
|
|
if r:
|
|
|
|
drevs.add(r)
|
|
|
|
elif op == '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):
|
2017-07-05 02:36:48 +03:00
|
|
|
"""return a list of "Differential Revision" dicts
|
|
|
|
|
phabricator: add a small language to query Differential Revisions
Previously, `phabread` can only be used to read a single patch, or a single
stack of patches. In the future, we want to have more complex queries like
filtering with status (open, accepted, closed, etc), or maybe more complex
like filtering by reviewers etc. The command line flag approach won't scale
with that.
Besides, we might want to have other commands to update Differential
Revision status in batch, like accepting a stack using a single command.
Therefore, this patch adds a small language. It has basic set operations:
`&`, `+`, `-` and an ancestor operator to support `--stack`.
Test Plan:
Try querying this Phabricator instance:
hg phabread 1+2 # 1, 2
hg phabread D2+D1 # 2, 1
hg phabread ':118-115+:2-1' # 114, 116, 117, 118, 2
hg phabread '((:118-(D115+117)))&:117' # 114, 116
hg phabread ':2&:117' --debug # differential.query is called only once
Make sure the output is expected and prefetch works.
Differential Revision: https://phab.mercurial-scm.org/D125
2017-07-18 09:19:11 +03:00
|
|
|
spec is a string using a simple query language, see docstring in phabread
|
|
|
|
for details.
|
2017-07-05 02:36:48 +03:00
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
"""
|
2017-07-05 02:36:48 +03:00
|
|
|
def fetch(params):
|
|
|
|
"""params -> single drev or None"""
|
|
|
|
key = (params.get(r'ids') or params.get(r'phids') or [None])[0]
|
|
|
|
if key in prefetched:
|
|
|
|
return prefetched[key]
|
phabricator: add a small language to query Differential Revisions
Previously, `phabread` can only be used to read a single patch, or a single
stack of patches. In the future, we want to have more complex queries like
filtering with status (open, accepted, closed, etc), or maybe more complex
like filtering by reviewers etc. The command line flag approach won't scale
with that.
Besides, we might want to have other commands to update Differential
Revision status in batch, like accepting a stack using a single command.
Therefore, this patch adds a small language. It has basic set operations:
`&`, `+`, `-` and an ancestor operator to support `--stack`.
Test Plan:
Try querying this Phabricator instance:
hg phabread 1+2 # 1, 2
hg phabread D2+D1 # 2, 1
hg phabread ':118-115+:2-1' # 114, 116, 117, 118, 2
hg phabread '((:118-(D115+117)))&:117' # 114, 116
hg phabread ':2&:117' --debug # differential.query is called only once
Make sure the output is expected and prefetch works.
Differential Revision: https://phab.mercurial-scm.org/D125
2017-07-18 09:19:11 +03:00
|
|
|
drevs = callconduit(repo, 'differential.query', params)
|
2017-07-05 02:36:48 +03:00
|
|
|
# Fill prefetched with the result
|
|
|
|
for drev in drevs:
|
|
|
|
prefetched[drev[r'phid']] = drev
|
|
|
|
prefetched[int(drev[r'id'])] = drev
|
|
|
|
if key not in prefetched:
|
|
|
|
raise error.Abort(_('cannot get Differential Revision %r') % params)
|
|
|
|
return prefetched[key]
|
|
|
|
|
phabricator: add a small language to query Differential Revisions
Previously, `phabread` can only be used to read a single patch, or a single
stack of patches. In the future, we want to have more complex queries like
filtering with status (open, accepted, closed, etc), or maybe more complex
like filtering by reviewers etc. The command line flag approach won't scale
with that.
Besides, we might want to have other commands to update Differential
Revision status in batch, like accepting a stack using a single command.
Therefore, this patch adds a small language. It has basic set operations:
`&`, `+`, `-` and an ancestor operator to support `--stack`.
Test Plan:
Try querying this Phabricator instance:
hg phabread 1+2 # 1, 2
hg phabread D2+D1 # 2, 1
hg phabread ':118-115+:2-1' # 114, 116, 117, 118, 2
hg phabread '((:118-(D115+117)))&:117' # 114, 116
hg phabread ':2&:117' --debug # differential.query is called only once
Make sure the output is expected and prefetch works.
Differential Revision: https://phab.mercurial-scm.org/D125
2017-07-18 09:19:11 +03:00
|
|
|
def getstack(topdrevids):
|
|
|
|
"""given a top, get a stack from the bottom, [id] -> [id]"""
|
|
|
|
visited = set()
|
|
|
|
result = []
|
|
|
|
queue = [{r'ids': [i]} for i in topdrevids]
|
|
|
|
while queue:
|
|
|
|
params = queue.pop()
|
|
|
|
drev = fetch(params)
|
|
|
|
if drev[r'id'] in visited:
|
|
|
|
continue
|
|
|
|
visited.add(drev[r'id'])
|
|
|
|
result.append(int(drev[r'id']))
|
2017-07-05 02:36:48 +03:00
|
|
|
auxiliary = drev.get(r'auxiliary', {})
|
|
|
|
depends = auxiliary.get(r'phabricator:depends-on', [])
|
|
|
|
for phid in depends:
|
|
|
|
queue.append({'phids': [phid]})
|
phabricator: add a small language to query Differential Revisions
Previously, `phabread` can only be used to read a single patch, or a single
stack of patches. In the future, we want to have more complex queries like
filtering with status (open, accepted, closed, etc), or maybe more complex
like filtering by reviewers etc. The command line flag approach won't scale
with that.
Besides, we might want to have other commands to update Differential
Revision status in batch, like accepting a stack using a single command.
Therefore, this patch adds a small language. It has basic set operations:
`&`, `+`, `-` and an ancestor operator to support `--stack`.
Test Plan:
Try querying this Phabricator instance:
hg phabread 1+2 # 1, 2
hg phabread D2+D1 # 2, 1
hg phabread ':118-115+:2-1' # 114, 116, 117, 118, 2
hg phabread '((:118-(D115+117)))&:117' # 114, 116
hg phabread ':2&:117' --debug # differential.query is called only once
Make sure the output is expected and prefetch works.
Differential Revision: https://phab.mercurial-scm.org/D125
2017-07-18 09:19:11 +03:00
|
|
|
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('phabricator', 'batchsize', 12)
|
|
|
|
|
|
|
|
# Prefetch Differential Revisions in batch
|
|
|
|
tofetch = set(drevs)
|
|
|
|
for r in ancestordrevs:
|
|
|
|
tofetch.update(range(max(1, r - batchsize), r + 1))
|
|
|
|
if drevs:
|
|
|
|
fetch({r'ids': list(tofetch)})
|
2017-07-18 11:34:55 +03:00
|
|
|
validids = sorted(set(getstack(list(ancestordrevs))) | set(drevs))
|
phabricator: add a small language to query Differential Revisions
Previously, `phabread` can only be used to read a single patch, or a single
stack of patches. In the future, we want to have more complex queries like
filtering with status (open, accepted, closed, etc), or maybe more complex
like filtering by reviewers etc. The command line flag approach won't scale
with that.
Besides, we might want to have other commands to update Differential
Revision status in batch, like accepting a stack using a single command.
Therefore, this patch adds a small language. It has basic set operations:
`&`, `+`, `-` and an ancestor operator to support `--stack`.
Test Plan:
Try querying this Phabricator instance:
hg phabread 1+2 # 1, 2
hg phabread D2+D1 # 2, 1
hg phabread ':118-115+:2-1' # 114, 116, 117, 118, 2
hg phabread '((:118-(D115+117)))&:117' # 114, 116
hg phabread ':2&:117' --debug # differential.query is called only once
Make sure the output is expected and prefetch works.
Differential Revision: https://phab.mercurial-scm.org/D125
2017-07-18 09:19:11 +03:00
|
|
|
|
|
|
|
# Walk through the tree, return smartsets
|
|
|
|
def walk(tree):
|
|
|
|
op = tree[0]
|
|
|
|
if op == 'symbol':
|
|
|
|
drev = _parsedrev(tree[1])
|
|
|
|
if drev:
|
|
|
|
return smartset.baseset([drev])
|
2017-07-18 11:34:55 +03:00
|
|
|
elif tree[1] in _knownstatusnames:
|
|
|
|
drevs = [r for r in validids
|
|
|
|
if _getstatusname(prefetched[r]) == tree[1]]
|
|
|
|
return smartset.baseset(drevs)
|
phabricator: add a small language to query Differential Revisions
Previously, `phabread` can only be used to read a single patch, or a single
stack of patches. In the future, we want to have more complex queries like
filtering with status (open, accepted, closed, etc), or maybe more complex
like filtering by reviewers etc. The command line flag approach won't scale
with that.
Besides, we might want to have other commands to update Differential
Revision status in batch, like accepting a stack using a single command.
Therefore, this patch adds a small language. It has basic set operations:
`&`, `+`, `-` and an ancestor operator to support `--stack`.
Test Plan:
Try querying this Phabricator instance:
hg phabread 1+2 # 1, 2
hg phabread D2+D1 # 2, 1
hg phabread ':118-115+:2-1' # 114, 116, 117, 118, 2
hg phabread '((:118-(D115+117)))&:117' # 114, 116
hg phabread ':2&:117' --debug # differential.query is called only once
Make sure the output is expected and prefetch works.
Differential Revision: https://phab.mercurial-scm.org/D125
2017-07-18 09:19:11 +03:00
|
|
|
else:
|
|
|
|
raise error.Abort(_('unknown symbol: %s') % tree[1])
|
|
|
|
elif op in {'and_', 'add', 'sub'}:
|
|
|
|
assert len(tree) == 3
|
|
|
|
return getattr(operator, op)(walk(tree[1]), walk(tree[2]))
|
|
|
|
elif op == 'group':
|
|
|
|
return walk(tree[1])
|
|
|
|
elif op == 'ancestors':
|
|
|
|
return getstack(walk(tree[1]))
|
|
|
|
else:
|
|
|
|
raise error.ProgrammingError('illegal tree: %r' % tree)
|
|
|
|
|
|
|
|
return [prefetched[r] for r in walk(tree)]
|
2017-07-05 02:36:48 +03:00
|
|
|
|
2017-07-05 02:36:48 +03:00
|
|
|
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.
|
|
|
|
"""
|
|
|
|
title = drev[r'title']
|
|
|
|
summary = drev[r'summary'].rstrip()
|
|
|
|
testplan = drev[r'testPlan'].rstrip()
|
|
|
|
if testplan:
|
|
|
|
testplan = 'Test Plan:\n%s' % testplan
|
|
|
|
uri = 'Differential Revision: %s' % drev[r'uri']
|
|
|
|
return '\n\n'.join(filter(None, [title, summary, testplan, uri]))
|
|
|
|
|
2017-07-11 08:37:33 +03:00
|
|
|
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.
|
|
|
|
"""
|
|
|
|
props = diff.get(r'properties') or {}
|
|
|
|
meta = props.get(r'hg:meta')
|
|
|
|
if not meta and props.get(r'local:commits'):
|
|
|
|
commit = sorted(props[r'local:commits'].values())[0]
|
|
|
|
meta = {
|
|
|
|
r'date': r'%d 0' % commit[r'time'],
|
|
|
|
r'node': commit[r'rev'],
|
|
|
|
r'user': r'%s <%s>' % (commit[r'author'], commit[r'authorEmail']),
|
|
|
|
}
|
|
|
|
if len(commit.get(r'parents', ())) >= 1:
|
|
|
|
meta[r'parent'] = commit[r'parents'][0]
|
|
|
|
return meta or {}
|
|
|
|
|
2017-07-18 09:14:06 +03:00
|
|
|
def readpatch(repo, drevs, write):
|
2017-07-03 06:08:09 +03:00
|
|
|
"""generate plain-text patch readable by 'hg import'
|
|
|
|
|
2017-07-18 09:14:06 +03:00
|
|
|
write is usually ui.write. drevs is what "querydrev" returns, results of
|
|
|
|
"differential.query".
|
2017-07-03 06:08:09 +03:00
|
|
|
"""
|
2017-07-05 02:36:48 +03:00
|
|
|
# Prefetch hg:meta property for all diffs
|
|
|
|
diffids = sorted(set(max(int(v) for v in drev[r'diffs']) for drev in drevs))
|
|
|
|
diffs = callconduit(repo, 'differential.querydiffs', {'ids': diffids})
|
|
|
|
|
|
|
|
# Generate patch for each drev
|
|
|
|
for drev in drevs:
|
|
|
|
repo.ui.note(_('reading D%s\n') % drev[r'id'])
|
|
|
|
|
|
|
|
diffid = max(int(v) for v in drev[r'diffs'])
|
|
|
|
body = callconduit(repo, 'differential.getrawdiff', {'diffID': diffid})
|
2017-07-05 02:36:48 +03:00
|
|
|
desc = getdescfromdrev(drev)
|
2017-07-05 02:36:48 +03:00
|
|
|
header = '# 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.
|
2017-07-11 08:37:33 +03:00
|
|
|
meta = getdiffmeta(diffs[str(diffid)])
|
|
|
|
for k in _metanamemap.keys():
|
|
|
|
if k in meta:
|
|
|
|
header += '# %s %s\n' % (_metanamemap[k], meta[k])
|
2017-07-05 02:36:48 +03:00
|
|
|
|
2017-07-27 22:03:01 +03:00
|
|
|
content = '%s%s\n%s' % (header, desc, body)
|
|
|
|
write(encoding.unitolocal(content))
|
2017-07-03 06:08:09 +03:00
|
|
|
|
|
|
|
@command('phabread',
|
|
|
|
[('', 'stack', False, _('read dependencies'))],
|
phabricator: add a small language to query Differential Revisions
Previously, `phabread` can only be used to read a single patch, or a single
stack of patches. In the future, we want to have more complex queries like
filtering with status (open, accepted, closed, etc), or maybe more complex
like filtering by reviewers etc. The command line flag approach won't scale
with that.
Besides, we might want to have other commands to update Differential
Revision status in batch, like accepting a stack using a single command.
Therefore, this patch adds a small language. It has basic set operations:
`&`, `+`, `-` and an ancestor operator to support `--stack`.
Test Plan:
Try querying this Phabricator instance:
hg phabread 1+2 # 1, 2
hg phabread D2+D1 # 2, 1
hg phabread ':118-115+:2-1' # 114, 116, 117, 118, 2
hg phabread '((:118-(D115+117)))&:117' # 114, 116
hg phabread ':2&:117' --debug # differential.query is called only once
Make sure the output is expected and prefetch works.
Differential Revision: https://phab.mercurial-scm.org/D125
2017-07-18 09:19:11 +03:00
|
|
|
_('DREVSPEC [OPTIONS]'))
|
|
|
|
def phabread(ui, repo, spec, **opts):
|
2017-07-03 06:08:09 +03:00
|
|
|
"""print patches from Phabricator suitable for importing
|
|
|
|
|
phabricator: add a small language to query Differential Revisions
Previously, `phabread` can only be used to read a single patch, or a single
stack of patches. In the future, we want to have more complex queries like
filtering with status (open, accepted, closed, etc), or maybe more complex
like filtering by reviewers etc. The command line flag approach won't scale
with that.
Besides, we might want to have other commands to update Differential
Revision status in batch, like accepting a stack using a single command.
Therefore, this patch adds a small language. It has basic set operations:
`&`, `+`, `-` and an ancestor operator to support `--stack`.
Test Plan:
Try querying this Phabricator instance:
hg phabread 1+2 # 1, 2
hg phabread D2+D1 # 2, 1
hg phabread ':118-115+:2-1' # 114, 116, 117, 118, 2
hg phabread '((:118-(D115+117)))&:117' # 114, 116
hg phabread ':2&:117' --debug # differential.query is called only once
Make sure the output is expected and prefetch works.
Differential Revision: https://phab.mercurial-scm.org/D125
2017-07-18 09:19:11 +03:00
|
|
|
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.
|
|
|
|
|
2017-07-18 11:34:55 +03:00
|
|
|
``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.
|
|
|
|
|
phabricator: add a small language to query Differential Revisions
Previously, `phabread` can only be used to read a single patch, or a single
stack of patches. In the future, we want to have more complex queries like
filtering with status (open, accepted, closed, etc), or maybe more complex
like filtering by reviewers etc. The command line flag approach won't scale
with that.
Besides, we might want to have other commands to update Differential
Revision status in batch, like accepting a stack using a single command.
Therefore, this patch adds a small language. It has basic set operations:
`&`, `+`, `-` and an ancestor operator to support `--stack`.
Test Plan:
Try querying this Phabricator instance:
hg phabread 1+2 # 1, 2
hg phabread D2+D1 # 2, 1
hg phabread ':118-115+:2-1' # 114, 116, 117, 118, 2
hg phabread '((:118-(D115+117)))&:117' # 114, 116
hg phabread ':2&:117' --debug # differential.query is called only once
Make sure the output is expected and prefetch works.
Differential Revision: https://phab.mercurial-scm.org/D125
2017-07-18 09:19:11 +03:00
|
|
|
For example, ``:D6+8-(2+D4)`` selects a stack up to D6, plus D8 and exclude
|
2017-07-18 11:34:55 +03:00
|
|
|
D2 and D4. ``:D9 & needsreview`` selects "Needs Review" revisions in a
|
|
|
|
stack up to D9.
|
2017-07-03 06:08:09 +03:00
|
|
|
|
|
|
|
If --stack is given, follow dependencies information and read all patches.
|
phabricator: add a small language to query Differential Revisions
Previously, `phabread` can only be used to read a single patch, or a single
stack of patches. In the future, we want to have more complex queries like
filtering with status (open, accepted, closed, etc), or maybe more complex
like filtering by reviewers etc. The command line flag approach won't scale
with that.
Besides, we might want to have other commands to update Differential
Revision status in batch, like accepting a stack using a single command.
Therefore, this patch adds a small language. It has basic set operations:
`&`, `+`, `-` and an ancestor operator to support `--stack`.
Test Plan:
Try querying this Phabricator instance:
hg phabread 1+2 # 1, 2
hg phabread D2+D1 # 2, 1
hg phabread ':118-115+:2-1' # 114, 116, 117, 118, 2
hg phabread '((:118-(D115+117)))&:117' # 114, 116
hg phabread ':2&:117' --debug # differential.query is called only once
Make sure the output is expected and prefetch works.
Differential Revision: https://phab.mercurial-scm.org/D125
2017-07-18 09:19:11 +03:00
|
|
|
It is equivalent to the ``:`` operator.
|
2017-07-03 06:08:09 +03:00
|
|
|
"""
|
phabricator: add a small language to query Differential Revisions
Previously, `phabread` can only be used to read a single patch, or a single
stack of patches. In the future, we want to have more complex queries like
filtering with status (open, accepted, closed, etc), or maybe more complex
like filtering by reviewers etc. The command line flag approach won't scale
with that.
Besides, we might want to have other commands to update Differential
Revision status in batch, like accepting a stack using a single command.
Therefore, this patch adds a small language. It has basic set operations:
`&`, `+`, `-` and an ancestor operator to support `--stack`.
Test Plan:
Try querying this Phabricator instance:
hg phabread 1+2 # 1, 2
hg phabread D2+D1 # 2, 1
hg phabread ':118-115+:2-1' # 114, 116, 117, 118, 2
hg phabread '((:118-(D115+117)))&:117' # 114, 116
hg phabread ':2&:117' --debug # differential.query is called only once
Make sure the output is expected and prefetch works.
Differential Revision: https://phab.mercurial-scm.org/D125
2017-07-18 09:19:11 +03:00
|
|
|
if opts.get('stack'):
|
|
|
|
spec = ':(%s)' % spec
|
|
|
|
drevs = querydrev(repo, spec)
|
2017-07-18 09:14:06 +03:00
|
|
|
readpatch(repo, drevs, ui.write)
|
2017-07-18 12:05:19 +03:00
|
|
|
|
|
|
|
@command('phabupdate',
|
|
|
|
[('', 'accept', False, _('accept revisions')),
|
|
|
|
('', 'reject', False, _('reject revisions')),
|
|
|
|
('', 'abandon', False, _('abandon revisions')),
|
|
|
|
('', 'reclaim', False, _('reclaim revisions')),
|
|
|
|
('m', 'comment', '', _('comment on the last revision')),
|
|
|
|
], _('DREVSPEC [OPTIONS]'))
|
|
|
|
def phabupdate(ui, repo, spec, **opts):
|
|
|
|
"""update Differential Revision in batch
|
|
|
|
|
|
|
|
DREVSPEC selects revisions. See :hg:`help phabread` for its usage.
|
|
|
|
"""
|
|
|
|
flags = [n for n in 'accept reject abandon reclaim'.split() if opts.get(n)]
|
|
|
|
if len(flags) > 1:
|
|
|
|
raise error.Abort(_('%s cannot be used together') % ', '.join(flags))
|
|
|
|
|
|
|
|
actions = []
|
|
|
|
for f in flags:
|
|
|
|
actions.append({'type': f, 'value': 'true'})
|
|
|
|
|
|
|
|
drevs = querydrev(repo, spec)
|
|
|
|
for i, drev in enumerate(drevs):
|
|
|
|
if i + 1 == len(drevs) and opts.get('comment'):
|
|
|
|
actions.append({'type': 'comment', 'value': opts['comment']})
|
|
|
|
if actions:
|
|
|
|
params = {'objectIdentifier': drev[r'phid'],
|
|
|
|
'transactions': actions}
|
|
|
|
callconduit(repo, 'differential.revision.edit', params)
|