##// END OF EJS Templates
errors: stop passing non-strings to Abort's constructor...
errors: stop passing non-strings to Abort's constructor The next patch will change `Abort`'s constructor and `__bytes__` functions and they will start assuming that the first argument is the messages as `bytes`. Differential Revision: https://phab.mercurial-scm.org/D9178

File last commit:

r46074:c7fe0dfb default
r46273:a736ab68 default
Show More
phabricator.py
2312 lines | 78.5 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.
Denis Laxalde
phabricator: add a "phabstatus" show view...
r44291 A "phabstatus" view for :hg:`show` is also provided; it displays status
information of Phabricator differentials associated with unfinished
changesets.
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 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
Ian Moody
phabricator: add the uploadchunks function...
r43457 import base64
Matt Harbison
phabricator: ensure that the return of urlopener.open() is closed...
r41111 import contextlib
Ian Moody
phabricator: add the uploadfile function...
r43458 import hashlib
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 import itertools
import json
Ian Moody
phabricator: add makebinary and addoldbinary functions...
r43459 import mimetypes
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 import operator
import re
Matt Harbison
phabricator: add debug logging to show previous node values in `phabsend`...
r45209 from mercurial.node import bin, nullid, short
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 from mercurial.i18n import _
Gregory Szorc
py3: manually import getattr where it is needed...
r43359 from mercurial.pycompat import getattr
Ian Moody
phabricator: add the phabhunk data structure...
r43453 from mercurial.thirdparty import attr
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 from mercurial import (
cmdutil,
context,
Matt Harbison
phabricator: account for `basectx != ctx` when calculating renames...
r45101 copies,
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 encoding,
error,
Matt Harbison
phabricator: use exthelper to register commands, config, and templates...
r43243 exthelper,
Denis Laxalde
phabricator: add a "phabstatus" show view...
r44291 graphmod,
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 httpconnection as httpconnectionmod,
Matt Harbison
phabricator: use .arcconfig for the callsign if not set locally (issue6243)...
r44586 localrepo,
Denis Laxalde
phabricator: add a "phabstatus" show view...
r44291 logcmdutil,
Ian Moody
phabricator: add the maketext function...
r43456 match,
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 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,
Matt Harbison
phabricator: update the hashes in commit messages as they get submitted...
r45995 rewriteutil,
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 scmutil,
smartset,
tags,
Ian Moody
phabricator: use templatefilters.json in writediffproperties...
r42369 templatefilters,
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,
)
Denis Laxalde
phabricator: add a "phabstatus" show view...
r44291 from . import show
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688
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.
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 testedwith = b'ships-with-hg-core'
Yuya Nishihara
phabricator: add testedwith boilerplate
r39771
Matt Harbison
phabricator: use exthelper to register commands, config, and templates...
r43243 eh = exthelper.exthelper()
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688
Matt Harbison
phabricator: use exthelper to register commands, config, and templates...
r43243 cmdtable = eh.cmdtable
command = eh.command
configtable = eh.configtable
templatekeyword = eh.templatekeyword
Matt Harbison
phabricator: use .arcconfig for the callsign if not set locally (issue6243)...
r44586 uisetup = eh.finaluisetup
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688
# developer config: phabricator.batchsize
Augie Fackler
formatting: blacken the codebase...
r43346 eh.configitem(
b'phabricator', b'batchsize', default=12,
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 )
Augie Fackler
formatting: blacken the codebase...
r43346 eh.configitem(
b'phabricator', b'callsign', default=None,
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 )
Augie Fackler
formatting: blacken the codebase...
r43346 eh.configitem(
b'phabricator', b'curlcmd', default=None,
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 )
Matt Harbison
phabricator: add debug logging to show previous node values in `phabsend`...
r45209 # developer config: phabricator.debug
eh.configitem(
b'phabricator', b'debug', default=False,
)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 # developer config: phabricator.repophid
Augie Fackler
formatting: blacken the codebase...
r43346 eh.configitem(
b'phabricator', b'repophid', default=None,
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 )
Augie Fackler
formatting: blacken the codebase...
r43346 eh.configitem(
b'phabricator', b'url', default=None,
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 )
Augie Fackler
formatting: blacken the codebase...
r43346 eh.configitem(
b'phabsend', b'confirm', default=False,
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 )
Matt Harbison
phabricator: add a config knob to import in the secret phase...
r45040 eh.configitem(
b'phabimport', b'secret', default=False,
)
Matt Harbison
phabricator: add a config knob to create obsolete markers when importing...
r45041 eh.configitem(
b'phabimport', b'obsolete', default=False,
)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688
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'',
Matt Harbison
phabricator: color the status in the "phabstatus" view...
r44310 b'phabricator.status.abandoned': b'magenta dim',
b'phabricator.status.accepted': b'green bold',
b'phabricator.status.closed': b'green',
b'phabricator.status.needsreview': b'yellow',
b'phabricator.status.needsrevision': b'red',
b'phabricator.status.changesplanned': b'red',
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 }
_VCR_FLAGS = [
Augie Fackler
formatting: blacken the codebase...
r43346 (
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)'
),
),
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 ]
Augie Fackler
formatting: blacken the codebase...
r43346
Matt Harbison
phabricator: use .arcconfig for the callsign if not set locally (issue6243)...
r44586 @eh.wrapfunction(localrepo, "loadhgrc")
Martin von Zweigbergk
phabricator: fix loadhgrc() override broken by D8656...
r46074 def _loadhgrc(orig, ui, wdirvfs, hgvfs, requirements, *args, **opts):
Matt Harbison
phabricator: use .arcconfig for the callsign if not set locally (issue6243)...
r44586 """Load ``.arcconfig`` content into a ui instance on repository open.
"""
result = False
arcconfig = {}
try:
# json.loads only accepts bytes from 3.6+
rawparams = encoding.unifromlocal(wdirvfs.read(b".arcconfig"))
# json.loads only returns unicode strings
arcconfig = pycompat.rapply(
lambda x: encoding.unitolocal(x)
if isinstance(x, pycompat.unicode)
else x,
pycompat.json_loads(rawparams),
)
result = True
except ValueError:
ui.warn(_(b"invalid JSON in %s\n") % wdirvfs.join(b".arcconfig"))
except IOError:
pass
Matt Harbison
phabricator: use .arcconfig for `phabricator.url` if not set locally...
r44587 cfg = util.sortdict()
Matt Harbison
phabricator: use .arcconfig for the callsign if not set locally (issue6243)...
r44586 if b"repository.callsign" in arcconfig:
Matt Harbison
phabricator: use .arcconfig for `phabricator.url` if not set locally...
r44587 cfg[(b"phabricator", b"callsign")] = arcconfig[b"repository.callsign"]
if b"phabricator.uri" in arcconfig:
cfg[(b"phabricator", b"url")] = arcconfig[b"phabricator.uri"]
if cfg:
ui.applyconfig(cfg, source=wdirvfs.join(b".arcconfig"))
Matt Harbison
phabricator: use .arcconfig for the callsign if not set locally (issue6243)...
r44586
Pulkit Goyal
localrepo: load the share source .hg/hgrc also in share-safe mode (API)...
r46057 return (
Martin von Zweigbergk
phabricator: fix loadhgrc() override broken by D8656...
r46074 orig(ui, wdirvfs, hgvfs, requirements, *args, **opts) or result
Pulkit Goyal
localrepo: load the share source .hg/hgrc also in share-safe mode (API)...
r46057 ) # Load .hg/hgrc
Matt Harbison
phabricator: use .arcconfig for the callsign if not set locally (issue6243)...
r44586
Pulkit Goyal
phabricator: make `hg debugcallconduit` work outside a hg repo...
r42628 def vcrcommand(name, flags, spec, helpcategory=None, optionalrepo=False):
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 fullflags = flags + _VCR_FLAGS
Augie Fackler
formatting: blacken the codebase...
r43346
Ian Moody
phabricator: add custom vcr matcher to match request bodies...
r42454 def hgmatcher(r1, r2):
if r1.uri != r2.uri or r1.method != r2.method:
return False
Ian Moody
phabricator: update hgmatcher to cope with the new data format...
r43558 r1params = util.urlreq.parseqs(r1.body)
r2params = util.urlreq.parseqs(r2.body)
for key in r1params:
if key not in r2params:
return False
value = r1params[key][0]
# we want to compare json payloads without worrying about ordering
if value.startswith(b'{') and value.endswith(b'}'):
Gregory Szorc
py3: define and use json.loads polyfill...
r43697 r1json = pycompat.json_loads(value)
r2json = pycompat.json_loads(r2params[key][0])
Ian Moody
phabricator: update hgmatcher to cope with the new data format...
r43558 if r1json != r2json:
return False
elif r2params[key][0] != value:
return False
return True
Ian Moody
phabricator: add custom vcr matcher to match request bodies...
r42454
Ian Moody
phabricator: auto-sanitise API tokens and HTTP cookies from VCR recordings...
r42636 def sanitiserequest(request):
request.body = re.sub(
Augie Fackler
formatting: blacken the codebase...
r43346 br'cli-[a-z0-9]+', br'cli-hahayouwish', request.body
Ian Moody
phabricator: auto-sanitise API tokens and HTTP cookies from VCR recordings...
r42636 )
return request
def sanitiseresponse(response):
Augie Fackler
cleanup: remove pointless r-prefixes on single-quoted strings...
r43906 if 'set-cookie' in response['headers']:
del response['headers']['set-cookie']
Ian Moody
phabricator: auto-sanitise API tokens and HTTP cookies from VCR recordings...
r42636 return response
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 def decorate(fn):
def inner(*args, **kwargs):
Ian Moody
phabricator: unconditionally pop `test_vcr` to fix debugcallconduit...
r45806 vcr = kwargs.pop('test_vcr')
if vcr:
cassette = pycompat.fsdecode(vcr)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 import hgdemandimport
Augie Fackler
formatting: blacken the codebase...
r43346
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 with hgdemandimport.deactivated():
import vcr as vcrmod
import vcr.stubs as stubs
Augie Fackler
formatting: blacken the codebase...
r43346
Augie Fackler
phabricator: do more of the VCR work in demandimport.deactivated()...
r40414 vcr = vcrmod.VCR(
Augie Fackler
cleanup: remove pointless r-prefixes on single-quoted strings...
r43906 serializer='json',
Ian Moody
phabricator: auto-sanitise API tokens and HTTP cookies from VCR recordings...
r42636 before_record_request=sanitiserequest,
before_record_response=sanitiseresponse,
Augie Fackler
phabricator: do more of the VCR work in demandimport.deactivated()...
r40414 custom_patches=[
Augie Fackler
formatting: blacken the codebase...
r43346 (
urlmod,
Augie Fackler
cleanup: remove pointless r-prefixes on single-quoted strings...
r43906 'httpconnection',
Augie Fackler
formatting: blacken the codebase...
r43346 stubs.VCRHTTPConnection,
),
(
urlmod,
Augie Fackler
cleanup: remove pointless r-prefixes on single-quoted strings...
r43906 'httpsconnection',
Augie Fackler
formatting: blacken the codebase...
r43346 stubs.VCRHTTPSConnection,
),
],
)
Augie Fackler
cleanup: remove pointless r-prefixes on single-quoted strings...
r43906 vcr.register_matcher('hgmatcher', hgmatcher)
with vcr.use_cassette(cassette, match_on=['hgmatcher']):
Augie Fackler
phabricator: do more of the VCR work in demandimport.deactivated()...
r40414 return fn(*args, **kwargs)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 return fn(*args, **kwargs)
Augie Fackler
formatting: blacken the codebase...
r43346
Matt Harbison
phabricator: avoid a stacktrace when command arguments are missing...
r44918 cmd = util.checksignature(inner, depth=2)
cmd.__name__ = fn.__name__
cmd.__doc__ = fn.__doc__
Augie Fackler
formatting: blacken the codebase...
r43346 return command(
name,
fullflags,
spec,
helpcategory=helpcategory,
optionalrepo=optionalrepo,
Matt Harbison
phabricator: avoid a stacktrace when command arguments are missing...
r44918 )(cmd)
Augie Fackler
formatting: blacken the codebase...
r43346
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 return decorate
Augie Fackler
formatting: blacken the codebase...
r43346
Matt Harbison
phabricator: add debug logging to show previous node values in `phabsend`...
r45209 def _debug(ui, *msg, **opts):
"""write debug output for Phabricator if ``phabricator.debug`` is set
Specifically, this avoids dumping Conduit and HTTP auth chatter that is
printed with the --debug argument.
"""
if ui.configbool(b"phabricator", b"debug"):
flag = ui.debugflag
try:
ui.debugflag = True
ui.write(*msg, **opts)
finally:
ui.debugflag = flag
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 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()
Augie Fackler
formatting: blacken the codebase...
r43346
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 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)
Augie Fackler
formatting: blacken the codebase...
r43346
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 process(b'', params)
return util.urlreq.urlencode(flatparams)
Augie Fackler
formatting: blacken the codebase...
r43346
Pulkit Goyal
phabricator: pass ui into readurltoken instead of passing repo...
r42626 def readurltoken(ui):
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 """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.
"""
Pulkit Goyal
phabricator: pass ui into readurltoken instead of passing repo...
r42626 url = ui.config(b'phabricator', b'url')
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 if not url:
Augie Fackler
formatting: blacken the codebase...
r43346 raise error.Abort(
_(b'config %s.%s is required') % (b'phabricator', b'url')
)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688
Pulkit Goyal
phabricator: pass ui into readurltoken instead of passing repo...
r42626 res = httpconnectionmod.readauthforuri(ui, url, util.url(url).user)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 token = None
if res:
group, auth = res
Pulkit Goyal
phabricator: pass ui into readurltoken instead of passing repo...
r42626 ui.debug(b"using auth.%s.* for authentication\n" % group)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688
token = auth.get(b'phabtoken')
if not token:
Augie Fackler
formatting: blacken the codebase...
r43346 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
Augie Fackler
formatting: blacken the codebase...
r43346
Pulkit Goyal
phabricator: pass ui instead of repo to callconduit...
r42627 def callconduit(ui, name, params):
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 """call Conduit API, params is a dict. return json.loads result, or None"""
Pulkit Goyal
phabricator: pass ui instead of repo to callconduit...
r42627 host, token = readurltoken(ui)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 url, authinfo = util.url(b'/'.join([host, b'api', name])).authinfo()
Pulkit Goyal
phabricator: pass ui instead of repo to callconduit...
r42627 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()
Ian Moody
phabricator: change conduit data format to match arcanist...
r43555 params[b'__conduit__'] = {
b'token': token,
}
rawdata = {
b'params': templatefilters.json(params),
b'output': b'json',
b'__conduit__': 1,
}
data = urlencodenested(rawdata)
Pulkit Goyal
phabricator: pass ui instead of repo to callconduit...
r42627 curlcmd = ui.config(b'phabricator', b'curlcmd')
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 if curlcmd:
Augie Fackler
formatting: blacken the codebase...
r43346 sin, sout = procutil.popen2(
b'%s -d @- %s' % (curlcmd, procutil.shellquote(url))
)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 sin.write(data)
sin.close()
body = sout.read()
else:
Pulkit Goyal
phabricator: pass ui instead of repo to callconduit...
r42627 urlopener = urlmod.opener(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()
Pulkit Goyal
phabricator: pass ui instead of repo to callconduit...
r42627 ui.debug(b'Conduit Response: %s\n' % body)
Ian Moody
phabricator: convert conduit response JSON unicode to bytes inside callconduit...
r42063 parsed = pycompat.rapply(
Augie Fackler
formatting: blacken the codebase...
r43346 lambda x: encoding.unitolocal(x)
if isinstance(x, pycompat.unicode)
Ian Moody
phabricator: convert conduit response JSON unicode to bytes inside callconduit...
r42063 else x,
Ian Moody
py3: fix phabricator's use of json.loads() for py3.5...
r43317 # json.loads only accepts bytes from py3.6+
Gregory Szorc
py3: define and use json.loads polyfill...
r43697 pycompat.json_loads(encoding.unifromlocal(body)),
Ian Moody
phabricator: convert conduit response JSON unicode to bytes inside callconduit...
r42063 )
if parsed.get(b'error_code'):
Augie Fackler
formatting: blacken the codebase...
r43346 msg = _(b'Conduit Error (%s): %s') % (
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
Augie Fackler
formatting: blacken the codebase...
r43346
Pulkit Goyal
phabricator: make `hg debugcallconduit` work outside a hg repo...
r42628 @vcrcommand(b'debugcallconduit', [], _(b'METHOD'), optionalrepo=True)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 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
Augie Fackler
formatting: blacken the codebase...
r43346 params = pycompat.rapply(
lambda x: encoding.unitolocal(x)
if isinstance(x, pycompat.unicode)
else x,
Gregory Szorc
py3: define and use json.loads polyfill...
r43697 pycompat.json_loads(rawparams),
Ian Moody
py3: convert to/from bytes/unicode for json.(dump|load)s in debugcallconduit...
r42137 )
# json.dumps only accepts unicode strings
Augie Fackler
formatting: blacken the codebase...
r43346 result = pycompat.rapply(
lambda x: encoding.unifromlocal(x) if isinstance(x, bytes) else x,
callconduit(ui, name, params),
Ian Moody
py3: convert to/from bytes/unicode for json.(dump|load)s in debugcallconduit...
r42137 )
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
Augie Fackler
formatting: blacken the codebase...
r43346
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
Augie Fackler
formatting: blacken the codebase...
r43346 query = callconduit(
repo.ui,
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
Augie Fackler
formatting: blacken the codebase...
r43346
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(
Augie Fackler
formatting: blacken the codebase...
r43346 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()
index: use `index.has_node` in `phabricator.getoldnodedrevmap`...
r43949 has_node = unfi.changelog.index.has_node
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688
Augie Fackler
formatting: blacken the codebase...
r43346 result = {} # {node: (oldnode?, lastdiff?, drev)}
Matt Harbison
phabricator: add debug logging to show previous node values in `phabsend`...
r45209 # ordered for test stability when printing new -> old mapping below
toconfirm = util.sortdict() # {node: (force, {precnode}, drev)}
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 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:
index: use `index.has_node` in `phabricator.getoldnodedrevmap`...
r43949 if has_node(n):
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 for tag in unfi.nodetags(n):
m = _differentialrevisiontagre.match(tag)
if m:
toconfirm[node] = (0, set(precnodes), int(m.group(1)))
Denis Laxalde
phabricator: fix processing of tags/desc in getoldnodedrevmap()...
r44281 break
else:
continue # move to next predecessor
break # found a tag, stop
else:
# Check commit message
m = _differentialrevisiondescre.search(ctx.description())
if m:
toconfirm[node] = (1, set(precnodes), int(m.group('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()]
Augie Fackler
formatting: blacken the codebase...
r43346 alldiffs = callconduit(
unfi.ui, b'differential.querydiffs', {b'revisionIDs': drevs}
)
Matt Harbison
phabricator: teach `getoldnodedrevmap()` to handle folded reviews...
r45136
def getnodes(d, precset):
# Ignore other nodes that were combined into the Differential
# that aren't predecessors of the current local node.
return [n for n in getlocalcommits(d) if n in precset]
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 for newnode, (force, precset, drev) in toconfirm.items():
Augie Fackler
formatting: blacken the codebase...
r43346 diffs = [
d for d in alldiffs.values() if int(d[b'revisionID']) == drev
]
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688
Matt Harbison
phabricator: teach `getoldnodedrevmap()` to handle folded reviews...
r45136 # local predecessors known by Phabricator
phprecset = {n for d in diffs for n in getnodes(d, precset)}
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688
# Ignore if precursors (Phabricator and local repo) do not overlap,
# and force is not set (when commit message says nothing)
Matt Harbison
phabricator: teach `getoldnodedrevmap()` to handle folded reviews...
r45136 if not force and not phprecset:
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 tagname = b'D%d' % drev
Augie Fackler
formatting: blacken the codebase...
r43346 tags.tag(
repo,
tagname,
nullid,
message=None,
user=None,
date=None,
local=True,
)
unfi.ui.warn(
_(
Ian Moody
py3: use %d to format an int...
r43717 b'D%d: local tag removed - does not match '
Augie Fackler
formatting: blacken the codebase...
r43346 b'Differential history\n'
)
% drev
)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 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']))
Matt Harbison
phabricator: teach `getoldnodedrevmap()` to handle folded reviews...
r45136 oldnodes = getnodes(lastdiff, precset)
Matt Harbison
phabricator: add debug logging to show previous node values in `phabsend`...
r45209 _debug(
unfi.ui,
b"%s mapped to old nodes %s\n"
% (
short(newnode),
stringutil.pprint([short(n) for n in sorted(oldnodes)]),
),
)
Matt Harbison
phabricator: teach `getoldnodedrevmap()` to handle folded reviews...
r45136 # If this commit was the result of `hg fold` after submission,
# and now resubmitted with --fold, the easiest thing to do is
# to leave the node clear. This only results in creating a new
# diff for the _same_ Differential Revision if this commit is
Matt Harbison
phabricator: add an option to fold several commits into one review (issue6244)...
r45211 # the first or last in the selected range. If we picked a node
# from the list instead, it would have to be the lowest if at
# the beginning of the --fold range, or the highest at the end.
# Otherwise, one or more of the nodes wouldn't be considered in
# the diff, and the Differential wouldn't be properly updated.
Matt Harbison
phabricator: teach `getoldnodedrevmap()` to handle folded reviews...
r45136 # If this commit is the result of `hg split` in the same
# scenario, there is a single oldnode here (and multiple
# newnodes mapped to it). That makes it the same as the normal
# case, as the edges of the newnode range cleanly maps to one
# oldnode each.
if len(oldnodes) == 1:
oldnode = oldnodes[0]
index: use `index.has_node` in `phabricator.getoldnodedrevmap`...
r43949 if oldnode and not has_node(oldnode):
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 oldnode = None
result[newnode] = (oldnode, lastdiff, drev)
return result
Augie Fackler
formatting: blacken the codebase...
r43346
Denis Laxalde
phabricator: add a "phabstatus" show view...
r44291 def getdrevmap(repo, revs):
"""Return a dict mapping each rev in `revs` to their Differential Revision
ID or None.
"""
result = {}
for rev in revs:
result[rev] = None
ctx = repo[rev]
# Check commit message
m = _differentialrevisiondescre.search(ctx.description())
if m:
result[rev] = int(m.group('id'))
continue
# Check tags
for tag in repo.nodetags(ctx.node()):
m = _differentialrevisiontagre.match(tag)
if m:
result[rev] = int(m.group(1))
break
return result
Matt Harbison
phabricator: add basectx arguments to file related `phabsend` utilities...
r45100 def getdiff(basectx, ctx, diffopts):
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 """plain-text diff without header (user, commit message, etc)"""
output = util.stringio()
Augie Fackler
formatting: blacken the codebase...
r43346 for chunk, _label in patch.diffui(
Matt Harbison
phabricator: add basectx arguments to file related `phabsend` utilities...
r45100 ctx.repo(), basectx.p1().node(), ctx.node(), None, opts=diffopts
Augie Fackler
formatting: blacken the codebase...
r43346 ):
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 output.write(chunk)
return output.getvalue()
Augie Fackler
formatting: blacken the codebase...
r43346
Ian Moody
phabricator: add the DiffChangeType and DiffFileType constants...
r43452 class DiffChangeType(object):
ADD = 1
CHANGE = 2
DELETE = 3
MOVE_AWAY = 4
COPY_AWAY = 5
MOVE_HERE = 6
COPY_HERE = 7
MULTICOPY = 8
class DiffFileType(object):
TEXT = 1
IMAGE = 2
BINARY = 3
Ian Moody
phabricator: add the phabhunk data structure...
r43453 @attr.s
class phabhunk(dict):
"""Represents a Differential hunk, which is owned by a Differential change
"""
oldOffset = attr.ib(default=0) # camelcase-required
oldLength = attr.ib(default=0) # camelcase-required
newOffset = attr.ib(default=0) # camelcase-required
newLength = attr.ib(default=0) # camelcase-required
corpus = attr.ib(default='')
# These get added to the phabchange's equivalents
addLines = attr.ib(default=0) # camelcase-required
delLines = attr.ib(default=0) # camelcase-required
Ian Moody
phabricator: add the phabchange data structure...
r43454 @attr.s
class phabchange(object):
"""Represents a Differential change, owns Differential hunks and owned by a
Differential diff. Each one represents one file in a diff.
"""
currentPath = attr.ib(default=None) # camelcase-required
oldPath = attr.ib(default=None) # camelcase-required
awayPaths = attr.ib(default=attr.Factory(list)) # camelcase-required
metadata = attr.ib(default=attr.Factory(dict))
oldProperties = attr.ib(default=attr.Factory(dict)) # camelcase-required
newProperties = attr.ib(default=attr.Factory(dict)) # camelcase-required
type = attr.ib(default=DiffChangeType.CHANGE)
fileType = attr.ib(default=DiffFileType.TEXT) # camelcase-required
commitHash = attr.ib(default=None) # camelcase-required
addLines = attr.ib(default=0) # camelcase-required
delLines = attr.ib(default=0) # camelcase-required
hunks = attr.ib(default=attr.Factory(list))
def copynewmetadatatoold(self):
for key in list(self.metadata.keys()):
newkey = key.replace(b'new:', b'old:')
self.metadata[newkey] = self.metadata[key]
def addoldmode(self, value):
self.oldProperties[b'unix:filemode'] = value
def addnewmode(self, value):
self.newProperties[b'unix:filemode'] = value
def addhunk(self, hunk):
if not isinstance(hunk, phabhunk):
raise error.Abort(b'phabchange.addhunk only takes phabhunks')
Ian Moody
phabricator: convert phabhunk and phabchange keys to bytes when finalising...
r43553 self.hunks.append(pycompat.byteskwargs(attr.asdict(hunk)))
Ian Moody
phabricator: add the phabchange data structure...
r43454 # It's useful to include these stats since the Phab web UI shows them,
# and uses them to estimate how large a change a Revision is. Also used
# in email subjects for the [+++--] bit.
self.addLines += hunk.addLines
self.delLines += hunk.delLines
Ian Moody
phabricator: add the phabdiff data structure...
r43455 @attr.s
class phabdiff(object):
"""Represents a Differential diff, owns Differential changes. Corresponds
to a commit.
"""
# Doesn't seem to be any reason to send this (output of uname -n)
sourceMachine = attr.ib(default=b'') # camelcase-required
sourcePath = attr.ib(default=b'/') # camelcase-required
sourceControlBaseRevision = attr.ib(default=b'0' * 40) # camelcase-required
sourceControlPath = attr.ib(default=b'/') # camelcase-required
sourceControlSystem = attr.ib(default=b'hg') # camelcase-required
branch = attr.ib(default=b'default')
bookmark = attr.ib(default=None)
creationMethod = attr.ib(default=b'phabsend') # camelcase-required
lintStatus = attr.ib(default=b'none') # camelcase-required
unitStatus = attr.ib(default=b'none') # camelcase-required
changes = attr.ib(default=attr.Factory(dict))
repositoryPHID = attr.ib(default=None) # camelcase-required
def addchange(self, change):
if not isinstance(change, phabchange):
raise error.Abort(b'phabdiff.addchange only takes phabchanges')
Ian Moody
phabricator: convert phabhunk and phabchange keys to bytes when finalising...
r43553 self.changes[change.currentPath] = pycompat.byteskwargs(
attr.asdict(change)
)
Ian Moody
phabricator: add the phabdiff data structure...
r43455
Matt Harbison
phabricator: add basectx arguments to file related `phabsend` utilities...
r45100 def maketext(pchange, basectx, ctx, fname):
Ian Moody
phabricator: add the maketext function...
r43456 """populate the phabchange for a text file"""
repo = ctx.repo()
fmatcher = match.exact([fname])
diffopts = mdiff.diffopts(git=True, context=32767)
_pfctx, _fctx, header, fhunks = next(
Matt Harbison
phabricator: add basectx arguments to file related `phabsend` utilities...
r45100 patch.diffhunks(repo, basectx.p1(), ctx, fmatcher, opts=diffopts)
Ian Moody
phabricator: add the maketext function...
r43456 )
for fhunk in fhunks:
(oldOffset, oldLength, newOffset, newLength), lines = fhunk
corpus = b''.join(lines[1:])
shunk = list(header)
shunk.extend(lines)
_mf, _mt, addLines, delLines, _hb = patch.diffstatsum(
patch.diffstatdata(util.iterlines(shunk))
)
pchange.addhunk(
phabhunk(
oldOffset,
oldLength,
newOffset,
newLength,
corpus,
addLines,
delLines,
)
)
Ian Moody
phabricator: add the uploadchunks function...
r43457 def uploadchunks(fctx, fphid):
"""upload large binary files as separate chunks.
Phab requests chunking over 8MiB, and splits into 4MiB chunks
"""
ui = fctx.repo().ui
chunks = callconduit(ui, b'file.querychunks', {b'filePHID': fphid})
Ian Moody
phabricator: use context manager form of progress in uploadchunks...
r43810 with ui.makeprogress(
Ian Moody
phabricator: add the uploadchunks function...
r43457 _(b'uploading file chunks'), unit=_(b'chunks'), total=len(chunks)
Ian Moody
phabricator: use context manager form of progress in uploadchunks...
r43810 ) as progress:
for chunk in chunks:
progress.increment()
if chunk[b'complete']:
continue
bstart = int(chunk[b'byteStart'])
bend = int(chunk[b'byteEnd'])
callconduit(
ui,
b'file.uploadchunk',
{
b'filePHID': fphid,
b'byteStart': bstart,
b'data': base64.b64encode(fctx.data()[bstart:bend]),
b'dataEncoding': b'base64',
},
)
Ian Moody
phabricator: add the uploadchunks function...
r43457
Ian Moody
phabricator: add the uploadfile function...
r43458 def uploadfile(fctx):
"""upload binary files to Phabricator"""
repo = fctx.repo()
ui = repo.ui
fname = fctx.path()
size = fctx.size()
fhash = pycompat.bytestr(hashlib.sha256(fctx.data()).hexdigest())
# an allocate call is required first to see if an upload is even required
# (Phab might already have it) and to determine if chunking is needed
allocateparams = {
b'name': fname,
b'contentLength': size,
b'contentHash': fhash,
}
filealloc = callconduit(ui, b'file.allocate', allocateparams)
fphid = filealloc[b'filePHID']
if filealloc[b'upload']:
ui.write(_(b'uploading %s\n') % bytes(fctx))
if not fphid:
uploadparams = {
b'name': fname,
b'data_base64': base64.b64encode(fctx.data()),
}
fphid = callconduit(ui, b'file.upload', uploadparams)
else:
uploadchunks(fctx, fphid)
else:
ui.debug(b'server already has %s\n' % bytes(fctx))
if not fphid:
raise error.Abort(b'Upload of %s failed.' % bytes(fctx))
return fphid
Matt Harbison
phabricator: pass old `fctx` to `addoldbinary()` instead of inferring it...
r44911 def addoldbinary(pchange, oldfctx, fctx):
Ian Moody
phabricator: add makebinary and addoldbinary functions...
r43459 """add the metadata for the previous version of a binary file to the
phabchange for the new version
Matt Harbison
phabricator: pass old `fctx` to `addoldbinary()` instead of inferring it...
r44911
``oldfctx`` is the previous version of the file; ``fctx`` is the new
version of the file, or None if the file is being removed.
Ian Moody
phabricator: add makebinary and addoldbinary functions...
r43459 """
Matt Harbison
phabricator: pass old `fctx` to `addoldbinary()` instead of inferring it...
r44911 if not fctx or fctx.cmp(oldfctx):
Ian Moody
phabricator: add makebinary and addoldbinary functions...
r43459 # Files differ, add the old one
pchange.metadata[b'old:file:size'] = oldfctx.size()
mimeguess, _enc = mimetypes.guess_type(
encoding.unifromlocal(oldfctx.path())
)
if mimeguess:
pchange.metadata[b'old:file:mime-type'] = pycompat.bytestr(
mimeguess
)
fphid = uploadfile(oldfctx)
pchange.metadata[b'old:binary-phid'] = fphid
else:
# If it's left as IMAGE/BINARY web UI might try to display it
pchange.fileType = DiffFileType.TEXT
pchange.copynewmetadatatoold()
def makebinary(pchange, fctx):
"""populate the phabchange for a binary file"""
pchange.fileType = DiffFileType.BINARY
fphid = uploadfile(fctx)
pchange.metadata[b'new:binary-phid'] = fphid
pchange.metadata[b'new:file:size'] = fctx.size()
mimeguess, _enc = mimetypes.guess_type(encoding.unifromlocal(fctx.path()))
if mimeguess:
mimeguess = pycompat.bytestr(mimeguess)
pchange.metadata[b'new:file:mime-type'] = mimeguess
if mimeguess.startswith(b'image/'):
pchange.fileType = DiffFileType.IMAGE
Ian Moody
phabricator: add addremoved and addmodified functions...
r43460 # Copied from mercurial/patch.py
gitmode = {b'l': b'120000', b'x': b'100755', b'': b'100644'}
Ian Moody
phabricator: treat non-utf-8 text files as binary as phabricator requires...
r43557 def notutf8(fctx):
"""detect non-UTF-8 text files since Phabricator requires them to be marked
as binary
"""
try:
fctx.data().decode('utf-8')
return False
except UnicodeDecodeError:
fctx.repo().ui.write(
_(b'file %s detected as non-UTF-8, marked as binary\n')
% fctx.path()
)
return True
Matt Harbison
phabricator: add basectx arguments to file related `phabsend` utilities...
r45100 def addremoved(pdiff, basectx, ctx, removed):
Ian Moody
phabricator: add addremoved and addmodified functions...
r43460 """add removed files to the phabdiff. Shouldn't include moves"""
for fname in removed:
pchange = phabchange(
currentPath=fname, oldPath=fname, type=DiffChangeType.DELETE
)
Matt Harbison
phabricator: add basectx arguments to file related `phabsend` utilities...
r45100 oldfctx = basectx.p1()[fname]
Matt Harbison
phabricator: eliminate a couple of duplicate filectx lookups...
r45099 pchange.addoldmode(gitmode[oldfctx.flags()])
Matt Harbison
phabricator: rename a variable to clarify that it is the parent filecontext...
r44912 if not (oldfctx.isbinary() or notutf8(oldfctx)):
Matt Harbison
phabricator: add basectx arguments to file related `phabsend` utilities...
r45100 maketext(pchange, basectx, ctx, fname)
Ian Moody
phabricator: add addremoved and addmodified functions...
r43460
pdiff.addchange(pchange)
Matt Harbison
phabricator: add basectx arguments to file related `phabsend` utilities...
r45100 def addmodified(pdiff, basectx, ctx, modified):
Ian Moody
phabricator: add addremoved and addmodified functions...
r43460 """add modified files to the phabdiff"""
for fname in modified:
fctx = ctx[fname]
Matt Harbison
phabricator: add basectx arguments to file related `phabsend` utilities...
r45100 oldfctx = basectx.p1()[fname]
Ian Moody
phabricator: add addremoved and addmodified functions...
r43460 pchange = phabchange(currentPath=fname, oldPath=fname)
Matt Harbison
phabricator: eliminate a couple of duplicate filectx lookups...
r45099 filemode = gitmode[fctx.flags()]
originalmode = gitmode[oldfctx.flags()]
Ian Moody
phabricator: add addremoved and addmodified functions...
r43460 if filemode != originalmode:
pchange.addoldmode(originalmode)
pchange.addnewmode(filemode)
Matt Harbison
phabricator: also check parent fctx for binary where it is checked for UTF-8...
r44914 if (
fctx.isbinary()
or notutf8(fctx)
or oldfctx.isbinary()
or notutf8(oldfctx)
):
Ian Moody
phabricator: add addremoved and addmodified functions...
r43460 makebinary(pchange, fctx)
Matt Harbison
phabricator: eliminate a couple of duplicate filectx lookups...
r45099 addoldbinary(pchange, oldfctx, fctx)
Ian Moody
phabricator: add addremoved and addmodified functions...
r43460 else:
Matt Harbison
phabricator: add basectx arguments to file related `phabsend` utilities...
r45100 maketext(pchange, basectx, ctx, fname)
Ian Moody
phabricator: add addremoved and addmodified functions...
r43460
pdiff.addchange(pchange)
Matt Harbison
phabricator: add basectx arguments to file related `phabsend` utilities...
r45100 def addadded(pdiff, basectx, ctx, added, removed):
Ian Moody
phabricator: add addadded function...
r43552 """add file adds to the phabdiff, both new files and copies/moves"""
# Keep track of files that've been recorded as moved/copied, so if there are
# additional copies we can mark them (moves get removed from removed)
copiedchanges = {}
movedchanges = {}
Matt Harbison
phabricator: account for `basectx != ctx` when calculating renames...
r45101
copy = {}
if basectx != ctx:
copy = copies.pathcopies(basectx.p1(), ctx)
Ian Moody
phabricator: add addadded function...
r43552 for fname in added:
fctx = ctx[fname]
Matt Harbison
phabricator: don't infer the old `fctx` in `notutf8()`...
r44913 oldfctx = None
Ian Moody
phabricator: add addadded function...
r43552 pchange = phabchange(currentPath=fname)
Matt Harbison
phabricator: eliminate a couple of duplicate filectx lookups...
r45099 filemode = gitmode[fctx.flags()]
Matt Harbison
phabricator: account for `basectx != ctx` when calculating renames...
r45101
if copy:
originalfname = copy.get(fname, fname)
else:
originalfname = fname
if fctx.renamed():
originalfname = fctx.renamed()[0]
renamed = fname != originalfname
Ian Moody
phabricator: add addadded function...
r43552
if renamed:
Matt Harbison
phabricator: add basectx arguments to file related `phabsend` utilities...
r45100 oldfctx = basectx.p1()[originalfname]
Matt Harbison
phabricator: don't infer the old `fctx` in `notutf8()`...
r44913 originalmode = gitmode[oldfctx.flags()]
Ian Moody
phabricator: add addadded function...
r43552 pchange.oldPath = originalfname
if originalfname in removed:
origpchange = phabchange(
currentPath=originalfname,
oldPath=originalfname,
type=DiffChangeType.MOVE_AWAY,
awayPaths=[fname],
)
movedchanges[originalfname] = origpchange
removed.remove(originalfname)
pchange.type = DiffChangeType.MOVE_HERE
elif originalfname in movedchanges:
movedchanges[originalfname].type = DiffChangeType.MULTICOPY
movedchanges[originalfname].awayPaths.append(fname)
pchange.type = DiffChangeType.COPY_HERE
else: # pure copy
if originalfname not in copiedchanges:
origpchange = phabchange(
currentPath=originalfname, type=DiffChangeType.COPY_AWAY
)
copiedchanges[originalfname] = origpchange
else:
origpchange = copiedchanges[originalfname]
origpchange.awayPaths.append(fname)
pchange.type = DiffChangeType.COPY_HERE
if filemode != originalmode:
pchange.addoldmode(originalmode)
pchange.addnewmode(filemode)
else: # Brand-new file
pchange.addnewmode(gitmode[fctx.flags()])
pchange.type = DiffChangeType.ADD
Matt Harbison
phabricator: also check parent fctx for binary where it is checked for UTF-8...
r44914 if (
fctx.isbinary()
or notutf8(fctx)
or (oldfctx and (oldfctx.isbinary() or notutf8(oldfctx)))
):
Ian Moody
phabricator: add addadded function...
r43552 makebinary(pchange, fctx)
if renamed:
Matt Harbison
phabricator: don't infer the old `fctx` in `notutf8()`...
r44913 addoldbinary(pchange, oldfctx, fctx)
Ian Moody
phabricator: add addadded function...
r43552 else:
Matt Harbison
phabricator: add basectx arguments to file related `phabsend` utilities...
r45100 maketext(pchange, basectx, ctx, fname)
Ian Moody
phabricator: add addadded function...
r43552
pdiff.addchange(pchange)
for _path, copiedchange in copiedchanges.items():
pdiff.addchange(copiedchange)
for _path, movedchange in movedchanges.items():
pdiff.addchange(movedchange)
Matt Harbison
phabricator: add basectx arguments to file related `phabsend` utilities...
r45100 def creatediff(basectx, ctx):
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 """create a Differential Diff"""
repo = ctx.repo()
repophid = getrepophid(repo)
Ian Moody
phabricator: switch to the creatediff endpoint...
r43556 # Create a "Differential Diff" via "differential.creatediff" API
pdiff = phabdiff(
Matt Harbison
phabricator: add basectx arguments to file related `phabsend` utilities...
r45100 sourceControlBaseRevision=b'%s' % basectx.p1().hex(),
Ian Moody
phabricator: switch to the creatediff endpoint...
r43556 branch=b'%s' % ctx.branch(),
)
Matt Harbison
phabricator: add basectx arguments to file related `phabsend` utilities...
r45100 modified, added, removed, _d, _u, _i, _c = basectx.p1().status(ctx)
Ian Moody
phabricator: switch to the creatediff endpoint...
r43556 # addadded will remove moved files from removed, so addremoved won't get
# them
Matt Harbison
phabricator: add basectx arguments to file related `phabsend` utilities...
r45100 addadded(pdiff, basectx, ctx, added, removed)
addmodified(pdiff, basectx, ctx, modified)
addremoved(pdiff, basectx, ctx, removed)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 if repophid:
Ian Moody
phabricator: switch to the creatediff endpoint...
r43556 pdiff.repositoryPHID = repophid
diff = callconduit(
repo.ui,
b'differential.creatediff',
pycompat.byteskwargs(attr.asdict(pdiff)),
)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 if not diff:
Matt Harbison
phabricator: add basectx arguments to file related `phabsend` utilities...
r45100 if basectx != ctx:
msg = _(b'cannot create diff for %s::%s') % (basectx, ctx)
else:
msg = _(b'cannot create diff for %s') % ctx
raise error.Abort(msg)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 return diff
Augie Fackler
formatting: blacken the codebase...
r43346
Matt Harbison
phabricator: record all local commits used to create a Differential revision...
r45133 def writediffproperties(ctxs, diff):
"""write metadata to diff so patches could be applied losslessly
``ctxs`` is the list of commits that created the diff, in ascending order.
The list is generally a single commit, but may be several when using
``phabsend --fold``.
"""
Ian Moody
phabricator: switch to the creatediff endpoint...
r43556 # creatediff returns with a diffid but query returns with an id
diffid = diff.get(b'diffid', diff.get(b'id'))
Matt Harbison
phabricator: record all local commits used to create a Differential revision...
r45133 basectx = ctxs[0]
tipctx = ctxs[-1]
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 params = {
Ian Moody
phabricator: switch to the creatediff endpoint...
r43556 b'diff_id': diffid,
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 b'name': b'hg:meta',
Augie Fackler
formatting: blacken the codebase...
r43346 b'data': templatefilters.json(
{
Matt Harbison
phabricator: record all local commits used to create a Differential revision...
r45133 b'user': tipctx.user(),
b'date': b'%d %d' % tipctx.date(),
b'branch': tipctx.branch(),
b'node': tipctx.hex(),
b'parent': basectx.p1().hex(),
Augie Fackler
formatting: blacken the codebase...
r43346 }
),
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 }
Matt Harbison
phabricator: record all local commits used to create a Differential revision...
r45133 callconduit(basectx.repo().ui, b'differential.setdiffproperty', params)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688
Matt Harbison
phabricator: record all local commits used to create a Differential revision...
r45133 commits = {}
for ctx in ctxs:
commits[ctx.hex()] = {
b'author': stringutil.person(ctx.user()),
b'authorEmail': stringutil.email(ctx.user()),
b'time': int(ctx.date()[0]),
b'commit': ctx.hex(),
b'parents': [ctx.p1().hex()],
b'branch': ctx.branch(),
}
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 params = {
Ian Moody
phabricator: switch to the creatediff endpoint...
r43556 b'diff_id': diffid,
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 b'name': b'local:commits',
Matt Harbison
phabricator: record all local commits used to create a Differential revision...
r45133 b'data': templatefilters.json(commits),
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 }
Matt Harbison
phabricator: record all local commits used to create a Differential revision...
r45133 callconduit(basectx.repo().ui, b'differential.setdiffproperty', params)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688
Augie Fackler
formatting: blacken the codebase...
r43346
def createdifferentialrevision(
Matt Harbison
phabricator: teach createdifferentialrevision() to allow a folded commit range...
r45135 ctxs,
Augie Fackler
formatting: blacken the codebase...
r43346 revid=None,
parentrevphid=None,
Matt Harbison
phabricator: teach createdifferentialrevision() to allow a folded commit range...
r45135 oldbasenode=None,
Augie Fackler
formatting: blacken the codebase...
r43346 oldnode=None,
olddiff=None,
actions=None,
comment=None,
):
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 """create or update a Differential Revision
If revid is None, create a new Differential Revision, otherwise update
Ian Moody
phabricator: use parents.set to always set dependencies...
r42642 revid. If parentrevphid is not None, set it as a dependency.
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688
Matt Harbison
phabricator: teach createdifferentialrevision() to allow a folded commit range...
r45135 If there is a single commit for the new Differential Revision, ``ctxs`` will
be a list of that single context. Otherwise, it is a list that covers the
range of changes for the differential, where ``ctxs[0]`` is the first change
to include and ``ctxs[-1]`` is the last.
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 If oldnode is not None, check if the patch content (without commit message
Matt Harbison
phabricator: teach createdifferentialrevision() to allow a folded commit range...
r45135 and metadata) has changed before creating another diff. For a Revision with
a single commit, ``oldbasenode`` and ``oldnode`` have the same value. For a
Revision covering multiple commits, ``oldbasenode`` corresponds to
``ctxs[0]`` the previous time this Revision was posted, and ``oldnode``
corresponds to ``ctxs[-1]``.
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688
If actions is not None, they will be appended to the transaction.
"""
Matt Harbison
phabricator: teach createdifferentialrevision() to allow a folded commit range...
r45135 ctx = ctxs[-1]
basectx = ctxs[0]
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 repo = ctx.repo()
if oldnode:
diffopts = mdiff.diffopts(git=True, context=32767)
Matt Harbison
phabricator: teach createdifferentialrevision() to allow a folded commit range...
r45135 unfi = repo.unfiltered()
oldctx = unfi[oldnode]
oldbasectx = unfi[oldbasenode]
Matt Harbison
phabricator: add basectx arguments to file related `phabsend` utilities...
r45100 neednewdiff = getdiff(basectx, ctx, diffopts) != getdiff(
oldbasectx, oldctx, diffopts
)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 else:
neednewdiff = True
transactions = []
if neednewdiff:
Matt Harbison
phabricator: add basectx arguments to file related `phabsend` utilities...
r45100 diff = creatediff(basectx, 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']})
Ian Moody
phabricator: add commenting to phabsend for new/updated Diffs...
r42624 if comment:
transactions.append({b'type': b'comment', b'value': comment})
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
Matt Harbison
phabricator: teach createdifferentialrevision() to allow a folded commit range...
r45135 writediffproperties(ctxs, diff)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688
Ian Moody
phabricator: use parents.set to always set dependencies...
r42642 # Set the parent Revision every time, so commit re-ordering is picked-up
if parentrevphid:
Augie Fackler
formatting: blacken the codebase...
r43346 transactions.append(
{b'type': b'parents.set', b'value': [parentrevphid]}
)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688
if actions:
transactions += actions
Matt Harbison
phabricator: combine commit messages into the review when folding commits...
r45134 # When folding multiple local commits into a single review, arcanist will
# take the summary line of the first commit as the title, and then
# concatenate the rest of the remaining messages (including each of their
# first lines) to the rest of the first commit message (each separated by
# an empty line), and use that as the summary field. Do the same here.
# For commits with only a one line message, there is no summary field, as
# this gets assigned to the title.
fields = util.sortdict() # sorted for stable wire protocol in tests
Matt Harbison
phabricator: teach createdifferentialrevision() to allow a folded commit range...
r45135 for i, _ctx in enumerate(ctxs):
Matt Harbison
phabricator: combine commit messages into the review when folding commits...
r45134 # Parse commit message and update related fields.
desc = _ctx.description()
info = callconduit(
repo.ui, b'differential.parsecommitmessage', {b'corpus': desc}
)
for k in [b'title', b'summary', b'testPlan']:
v = info[b'fields'].get(k)
if not v:
continue
if i == 0:
# Title, summary and test plan (if present) are taken verbatim
# for the first commit.
fields[k] = v.rstrip()
continue
elif k == b'title':
# Add subsequent titles (i.e. the first line of the commit
# message) back to the summary.
k = b'summary'
# Append any current field to the existing composite field
fields[k] = b'\n\n'.join(filter(None, [fields.get(k), v.rstrip()]))
for k, v in fields.items():
transactions.append({b'type': k, b'value': v})
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688
params = {b'transactions': transactions}
if revid is not None:
# Update an existing Differential Revision
params[b'objectIdentifier'] = revid
Pulkit Goyal
phabricator: pass ui instead of repo to callconduit...
r42627 revision = callconduit(repo.ui, b'differential.revision.edit', params)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 if not revision:
Matt Harbison
phabricator: teach createdifferentialrevision() to allow a folded commit range...
r45135 if len(ctxs) == 1:
msg = _(b'cannot create revision for %s') % ctx
else:
msg = _(b'cannot create revision for %s::%s') % (basectx, ctx)
raise error.Abort(msg)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688
return revision, diff
Augie Fackler
formatting: blacken the codebase...
r43346
Matt Harbison
phabricator: pass ui instead of repo to `userphids()`...
r44907 def userphids(ui, names):
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 """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}}
Matt Harbison
phabricator: pass ui instead of repo to `userphids()`...
r44907 result = callconduit(ui, b'user.search', query)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 # 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']
Augie Fackler
cleanup: run pyupgrade on our source tree to clean up varying things...
r44937 resolved = {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:
Augie Fackler
formatting: blacken the codebase...
r43346 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
Augie Fackler
formatting: blacken the codebase...
r43346
Matt Harbison
phabricator: extract logic to print the status when posting a commit...
r45138 def _print_phabsend_action(ui, ctx, newrevid, action):
"""print the ``action`` that occurred when posting ``ctx`` for review
This is a utility function for the sending phase of ``phabsend``, which
makes it easier to show a status for all local commits with `--fold``.
"""
actiondesc = ui.label(
{
b'created': _(b'created'),
b'skipped': _(b'skipped'),
b'updated': _(b'updated'),
}[action],
b'phabricator.action.%s' % action,
)
drevdesc = ui.label(b'D%d' % newrevid, b'phabricator.drev')
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))
Matt Harbison
phabricator: extract the logic to amend diff properties to a function...
r45137 def _amend_diff_properties(unfi, drevid, newnodes, diff):
"""update the local commit list for the ``diff`` associated with ``drevid``
This is a utility function for the amend phase of ``phabsend``, which
converts failures to warning messages.
"""
Matt Harbison
phabricator: add debug logging to show previous node values in `phabsend`...
r45209 _debug(
unfi.ui,
b"new commits: %s\n" % stringutil.pprint([short(n) for n in newnodes]),
)
Matt Harbison
phabricator: extract the logic to amend diff properties to a function...
r45137 try:
writediffproperties([unfi[newnode] for newnode in newnodes], diff)
except util.urlerr.urlerror:
# If it fails just warn and keep going, otherwise the DREV
# associations will be lost
unfi.ui.warnnoi18n(b'Failed to update metadata for D%d\n' % drevid)
Augie Fackler
formatting: blacken the codebase...
r43346 @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'blocker', [], _(b'specify blocking reviewers')),
(
b'm',
b'comment',
b'',
_(b'add a comment to Revisions with new/updated Diffs'),
),
(b'', b'confirm', None, _(b'ask for confirmation before sending')),
Matt Harbison
phabricator: add an option to fold several commits into one review (issue6244)...
r45211 (b'', b'fold', False, _(b'combine the revisions into one review')),
Augie Fackler
formatting: blacken the codebase...
r43346 ],
_(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
Matt Harbison
phabricator: add an option to fold several commits into one review (issue6244)...
r45211 By default, a separate review will be created for each commit that is
selected, and will have the same parent/child relationship in Phabricator.
If ``--fold`` is set, multiple commits are rolled up into a single review
as if diffed from the parent of the first revision to the last. The commit
messages are concatenated in the summary field on Phabricator.
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 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)
Matt Harbison
phabricator: post revisions in ascending topological order (issue6241)...
r44533 revs.sort() # ascending order to preserve topological parent/child in phab
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688
if not revs:
raise error.Abort(_(b'phabsend requires at least one changeset'))
if opts.get(b'amend'):
cmdutil.checkunfinished(repo)
Matt Harbison
phabricator: add an option to fold several commits into one review (issue6244)...
r45211 ctxs = [repo[rev] for rev in revs]
Matt Harbison
phabricator: prevent posting obsolete commits...
r45213 if any(c for c in ctxs if c.obsolete()):
raise error.Abort(_(b"obsolete commits cannot be posted for review"))
Matt Harbison
phabricator: ensure that `phabsend` is given a contiguous, linear commit range...
r45281 # Ensure the local commits are an unbroken range. The semantics of the
# --fold option implies this, and the auto restacking of orphans requires
# it. Otherwise A+C in A->B->C will cause B to be orphaned, and C' to
# get A' as a parent.
Matt Harbison
phabricator: clarify the multiple heads/roots error message for phabsend...
r45712 def _fail_nonlinear_revs(revs, revtype):
badnodes = [repo[r].node() for r in revs]
Matt Harbison
phabricator: ensure that `phabsend` is given a contiguous, linear commit range...
r45281 raise error.Abort(
_(b"cannot phabsend multiple %s revisions: %s")
% (revtype, scmutil.nodesummaries(repo, badnodes)),
hint=_(b"the revisions must form a linear chain"),
)
heads = repo.revs(b'heads(%ld)', revs)
if len(heads) > 1:
Matt Harbison
phabricator: clarify the multiple heads/roots error message for phabsend...
r45712 _fail_nonlinear_revs(heads, b"head")
Matt Harbison
phabricator: ensure that `phabsend` is given a contiguous, linear commit range...
r45281
roots = repo.revs(b'roots(%ld)', revs)
if len(roots) > 1:
Matt Harbison
phabricator: clarify the multiple heads/roots error message for phabsend...
r45712 _fail_nonlinear_revs(roots, b"root")
Matt Harbison
phabricator: ensure that `phabsend` is given a contiguous, linear commit range...
r45281
Matt Harbison
phabricator: add an option to fold several commits into one review (issue6244)...
r45211 fold = opts.get(b'fold')
if fold:
if len(revs) == 1:
# TODO: just switch to --no-fold instead?
raise error.Abort(_(b"cannot fold a single revision"))
# There's no clear way to manage multiple commits with a Dxxx tag, so
# require the amend option. (We could append "_nnn", but then it
# becomes jumbled if earlier commits are added to an update.) It should
# lock the repo and ensure that the range is editable, but that would
# make the code pretty convoluted. The default behavior of `arc` is to
# create a new review anyway.
if not opts.get(b"amend"):
raise error.Abort(_(b"cannot fold with --no-amend"))
# It might be possible to bucketize the revisions by the DREV value, and
# iterate over those groups when posting, and then again when amending.
# But for simplicity, require all selected revisions to be for the same
# DREV (if present). Adding local revisions to an existing DREV is
# acceptable.
drevmatchers = [
_differentialrevisiondescre.search(ctx.description())
for ctx in ctxs
]
if len({m.group('url') for m in drevmatchers if m}) > 1:
raise error.Abort(
_(b"cannot fold revisions with different DREV values")
)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 # {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', [])
Ian Moody
phabricator: add --blocker argument to phabsend to specify blocking reviewers...
r42637 blockers = opts.get(b'blocker', [])
phids = []
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 if reviewers:
Matt Harbison
phabricator: pass ui instead of repo to `userphids()`...
r44907 phids.extend(userphids(repo.ui, reviewers))
Ian Moody
phabricator: add --blocker argument to phabsend to specify blocking reviewers...
r42637 if blockers:
Augie Fackler
formatting: blacken the codebase...
r43346 phids.extend(
Matt Harbison
phabricator: pass ui instead of repo to `userphids()`...
r44907 map(
lambda phid: b'blocking(%s)' % phid,
userphids(repo.ui, blockers),
)
Augie Fackler
formatting: blacken the codebase...
r43346 )
Ian Moody
phabricator: add --blocker argument to phabsend to specify blocking reviewers...
r42637 if phids:
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 actions.append({b'type': b'reviewers.add', b'value': phids})
Augie Fackler
formatting: blacken the codebase...
r43346 drevids = [] # [int]
diffmap = {} # {newnode: diff}
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688
Ian Moody
phabricator: use parents.set to always set dependencies...
r42642 # Send patches one by one so we know their Differential Revision PHIDs and
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 # can provide dependency relationship
Ian Moody
phabricator: use parents.set to always set dependencies...
r42642 lastrevphid = None
Matt Harbison
phabricator: add an option to fold several commits into one review (issue6244)...
r45211 for ctx in ctxs:
if fold:
ui.debug(b'sending rev %d::%d\n' % (ctx.rev(), ctxs[-1].rev()))
else:
ui.debug(b'sending rev %d\n' % ctx.rev())
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688
# Get Differential Revision ID
oldnode, olddiff, revid = oldmap.get(ctx.node(), (None, None, None))
Matt Harbison
phabricator: add an option to fold several commits into one review (issue6244)...
r45211 oldbasenode, oldbasediff, oldbaserevid = oldnode, olddiff, revid
if fold:
oldbasenode, oldbasediff, oldbaserevid = oldmap.get(
ctxs[-1].node(), (None, None, None)
)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 if oldnode != ctx.node() or opts.get(b'amend'):
# Create or update Differential Revision
revision, diff = createdifferentialrevision(
Matt Harbison
phabricator: add an option to fold several commits into one review (issue6244)...
r45211 ctxs if fold else [ctx],
Augie Fackler
formatting: blacken the codebase...
r43346 revid,
lastrevphid,
Matt Harbison
phabricator: teach createdifferentialrevision() to allow a folded commit range...
r45135 oldbasenode,
Augie Fackler
formatting: blacken the codebase...
r43346 oldnode,
olddiff,
actions,
opts.get(b'comment'),
)
Matt Harbison
phabricator: add an option to fold several commits into one review (issue6244)...
r45211
if fold:
for ctx in ctxs:
diffmap[ctx.node()] = diff
else:
diffmap[ctx.node()] = diff
Ian Moody
phabricator: convert conduit response JSON unicode to bytes inside callconduit...
r42063 newrevid = int(revision[b'object'][b'id'])
Ian Moody
phabricator: use parents.set to always set dependencies...
r42642 newrevphid = revision[b'object'][b'phid']
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
Matt Harbison
phabricator: add an option to fold several commits into one review (issue6244)...
r45211 if not fold:
m = _differentialrevisiondescre.search(ctx.description())
if not m or int(m.group('id')) != newrevid:
tagname = b'D%d' % newrevid
tags.tag(
repo,
tagname,
ctx.node(),
message=None,
user=None,
date=None,
local=True,
)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 else:
Ian Moody
phabricator: use parents.set to always set dependencies...
r42642 # Nothing changed. But still set "newrevphid" so the next revision
# could depend on this one and "newrevid" for the summary line.
Matt Harbison
phabricator: pass ui instead of repo to `querydrev()`...
r44906 newrevphid = querydrev(repo.ui, b'%d' % revid)[0][b'phid']
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 newrevid = revid
action = b'skipped'
drevids.append(newrevid)
Ian Moody
phabricator: use parents.set to always set dependencies...
r42642 lastrevphid = newrevphid
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688
Matt Harbison
phabricator: add an option to fold several commits into one review (issue6244)...
r45211 if fold:
for c in ctxs:
if oldmap.get(c.node(), (None, None, None))[2]:
action = b'updated'
else:
action = b'created'
_print_phabsend_action(ui, c, newrevid, action)
break
Matt Harbison
phabricator: extract logic to print the status when posting a commit...
r45138 _print_phabsend_action(ui, ctx, newrevid, action)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 # Update commit messages and remove tags
if opts.get(b'amend'):
unfi = repo.unfiltered()
Pulkit Goyal
phabricator: pass ui instead of repo to callconduit...
r42627 drevs = callconduit(ui, b'differential.query', {b'ids': drevids})
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 with repo.wlock(), repo.lock(), repo.transaction(b'phabsend'):
Matt Harbison
phabricator: restack any new orphans created by phabsend (issue6045)...
r45214 # Eagerly evaluate commits to restabilize before creating new
# commits. The selected revisions are excluded because they are
# automatically restacked as part of the submission process.
restack = [
c
for c in repo.set(
b"(%ld::) - (%ld) - unstable() - obsolete() - public()",
revs,
revs,
)
]
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 wnode = unfi[b'.'].node()
Augie Fackler
formatting: blacken the codebase...
r43346 mapping = {} # {oldnode: [newnode]}
Matt Harbison
phabricator: add an option to fold several commits into one review (issue6244)...
r45211 newnodes = []
drevid = drevids[0]
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 for i, rev in enumerate(revs):
old = unfi[rev]
Matt Harbison
phabricator: add an option to fold several commits into one review (issue6244)...
r45211 if not fold:
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]
Matt Harbison
phabricator: add an option to fold several commits into one review (issue6244)...
r45211
newdesc = get_amended_desc(drev, old, fold)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 # Make sure commit message contain "Differential Revision"
Matt Harbison
phabricator: avoid creating unstable children within the review stack...
r45212 if (
old.description() != newdesc
or old.p1().node() in mapping
or old.p2().node() in mapping
):
Matt Harbison
phabricator: warn if unable to amend, instead of aborting after posting...
r41198 if old.phase() == phases.public:
Augie Fackler
formatting: blacken the codebase...
r43346 ui.warn(
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 _(b"warning: not updating public commit %s\n")
Augie Fackler
formatting: blacken the codebase...
r43346 % scmutil.formatchangeid(old)
)
Matt Harbison
phabricator: warn if unable to amend, instead of aborting after posting...
r41198 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],
]
Matt Harbison
phabricator: update the hashes in commit messages as they get submitted...
r45995 newdesc = rewriteutil.update_hash_refs(
repo, newdesc, mapping,
)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 new = context.metadataonlyctx(
Augie Fackler
formatting: blacken the codebase...
r43346 repo,
old,
parents=parents,
text=newdesc,
user=old.user(),
date=old.date(),
extra=old.extra(),
)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688
newnode = new.commit()
mapping[old.node()] = [newnode]
Matt Harbison
phabricator: extract the logic to amend diff properties to a function...
r45137
Matt Harbison
phabricator: add an option to fold several commits into one review (issue6244)...
r45211 if fold:
# Defer updating the (single) Diff until all nodes are
# collected. No tags were created, so none need to be
# removed.
newnodes.append(newnode)
continue
Matt Harbison
phabricator: extract the logic to amend diff properties to a function...
r45137 _amend_diff_properties(
unfi, drevid, [newnode], diffmap[old.node()]
)
Matt Harbison
phabricator: add an option to fold several commits into one review (issue6244)...
r45211
# 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,
)
elif fold:
# When folding multiple commits into one review with
# --fold, track even the commits that weren't amended, so
# that their association isn't lost if the properties are
# rewritten below.
newnodes.append(old.node())
# If the submitted commits are public, no amend takes place so
# there are no newnodes and therefore no diff update to do.
if fold and newnodes:
diff = diffmap[old.node()]
# The diff object in diffmap doesn't have the local commits
# because that could be returned from differential.creatediff,
# not differential.querydiffs. So use the queried diff (if
# present), or force the amend (a new revision is being posted.)
if not olddiff or set(newnodes) != getlocalcommits(olddiff):
_debug(ui, b"updating local commit list for D%d\n" % drevid)
_amend_diff_properties(unfi, drevid, newnodes, diff)
else:
_debug(
ui,
b"local commit list for D%d is already up-to-date\n"
% drevid,
Augie Fackler
formatting: blacken the codebase...
r43346 )
Matt Harbison
phabricator: add an option to fold several commits into one review (issue6244)...
r45211 elif fold:
_debug(ui, b"no newnodes to update\n")
Matt Harbison
phabricator: restack any new orphans created by phabsend (issue6045)...
r45214 # Restack any children of first-time submissions that were orphaned
# in the process. The ctx won't report that it is an orphan until
# the cleanup takes place below.
for old in restack:
parents = [
mapping.get(old.p1().node(), (old.p1(),))[0],
mapping.get(old.p2().node(), (old.p2(),))[0],
]
new = context.metadataonlyctx(
repo,
old,
parents=parents,
Matt Harbison
phabricator: update the hashes in commit messages as they get submitted...
r45995 text=rewriteutil.update_hash_refs(
repo, old.description(), mapping
),
Matt Harbison
phabricator: restack any new orphans created by phabsend (issue6045)...
r45214 user=old.user(),
date=old.date(),
extra=old.extra(),
)
newnode = new.commit()
# Don't obsolete unselected descendants of nodes that have not
# been changed in this transaction- that results in an error.
if newnode != old.node():
mapping[old.node()] = [newnode]
_debug(
ui,
b"restabilizing %s as %s\n"
% (short(old.node()), short(newnode)),
)
else:
_debug(
ui,
b"not restabilizing unchanged %s\n" % short(old.node()),
)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 scmutil.cleanupnodes(repo, mapping, b'phabsend', fixphase=True)
if wnode in mapping:
unfi.setparents(mapping[wnode][0])
Augie Fackler
formatting: blacken the codebase...
r43346
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 # Map from "hg:meta" keys to header understood by "hg import". The order is
# consistent with "hg export" output.
Augie Fackler
formatting: blacken the codebase...
r43346 _metanamemap = util.sortdict(
[
(b'user', b'User'),
(b'date', b'Date'),
(b'branch', b'Branch'),
(b'node', b'Node ID'),
(b'parent', b'Parent '),
]
)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688
def _confirmbeforesend(repo, revs, oldmap):
Pulkit Goyal
phabricator: pass ui into readurltoken instead of passing repo...
r42626 url, token = readurltoken(repo.ui)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 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:
Ian Moody
py3: use %d instead of %s when formatting an int into a bytestring...
r43628 drevdesc = ui.label(b'D%d' % drevid, b'phabricator.drev')
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 else:
drevdesc = ui.label(_(b'NEW'), b'phabricator.drev')
Augie Fackler
formatting: blacken the codebase...
r43346 ui.write(
_(b'%s - %s: %s\n')
% (
drevdesc,
ui.label(bytes(ctx), b'phabricator.node'),
ui.label(desc, b'phabricator.desc'),
)
)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688
Augie Fackler
formatting: blacken the codebase...
r43346 if ui.promptchoice(
Sushil khanchi
phabricator: make it clear what happen when no response...
r45471 _(b'Send the above changes to %s (Y/n)?$$ &Yes $$ &No') % url
Augie Fackler
formatting: blacken the codebase...
r43346 ):
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 return False
return True
Augie Fackler
formatting: blacken the codebase...
r43346
_knownstatusnames = {
b'accepted',
b'needsreview',
b'needsrevision',
b'closed',
b'abandoned',
Matt Harbison
phabricator: add the "Changes Planned" status name...
r44309 b'changesplanned',
Augie Fackler
formatting: blacken the codebase...
r43346 }
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688
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
Augie Fackler
formatting: blacken the codebase...
r43346
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
Augie Fackler
formatting: blacken the codebase...
r43346 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),
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 b'symbol': (0, b'symbol', None, None, None),
Augie Fackler
formatting: blacken the codebase...
r43346 b'end': (0, None, None, None, None),
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 }
Augie Fackler
formatting: blacken the codebase...
r43346
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 def _tokenize(text):
Augie Fackler
formatting: blacken the codebase...
r43346 view = memoryview(text) # zero-copy slice
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 special = b'():+-& '
pos = 0
length = len(text)
while pos < length:
Augie Fackler
formatting: blacken the codebase...
r43346 symbol = b''.join(
itertools.takewhile(
lambda ch: ch not in special, pycompat.iterbytestr(view[pos:])
)
)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 if symbol:
yield (b'symbol', symbol, pos)
pos += len(symbol)
Augie Fackler
formatting: blacken the codebase...
r43346 else: # special char, ignore space
Ian Moody
py3: don't index into bytes in phabricator's _tokenize()...
r43576 if text[pos : pos + 1] != b' ':
yield (text[pos : pos + 1], None, pos)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 pos += 1
yield (b'end', None, pos)
Augie Fackler
formatting: blacken the codebase...
r43346
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 def _parse(text):
tree, pos = parser.parser(_elements).parse(_tokenize(text))
if pos != len(text):
raise error.ParseError(b'invalid token', pos)
return tree
Augie Fackler
formatting: blacken the codebase...
r43346
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 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)
Augie Fackler
formatting: blacken the codebase...
r43346
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 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
Augie Fackler
formatting: blacken the codebase...
r43346
Matt Harbison
phabricator: pass ui instead of repo to `querydrev()`...
r44906 def querydrev(ui, spec):
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 """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:
{
Matt Harbison
phabricator: update the protocol documentation...
r44877 "activeDiffPHID": "PHID-DIFF-xoqnjkobbm6k4dk6hi72",
"authorPHID": "PHID-USER-tv3ohwc4v4jeu34otlye",
"auxiliary": {
"phabricator:depends-on": [
"PHID-DREV-gbapp366kutjebt7agcd"
]
"phabricator:projects": [],
},
"branch": "default",
"ccs": [],
"commits": [],
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 "dateCreated": "1499181406",
"dateModified": "1499182103",
"diffs": [
"3",
"4",
],
Matt Harbison
phabricator: update the protocol documentation...
r44877 "hashes": [],
"id": "2",
"lineCount": "2",
"phid": "PHID-DREV-672qvysjcczopag46qty",
"properties": {},
"repositoryPHID": "PHID-REPO-hub2hx62ieuqeheznasv",
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 "reviewers": [],
"sourcePath": null
Matt Harbison
phabricator: update the protocol documentation...
r44877 "status": "0",
"statusName": "Needs Review",
"summary": "",
"testPlan": "",
"title": "example",
"uri": "https://phab.example.com/D2",
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 }
"""
Matt Harbison
phabricator: update the protocol documentation...
r44877 # TODO: replace differential.query and differential.querydiffs with
# differential.diff.search because the former (and their output) are
# frozen, and planned to be deprecated and removed.
Augie Fackler
formatting: blacken the codebase...
r43346
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 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]
Matt Harbison
phabricator: pass ui instead of repo to `querydrev()`...
r44906 drevs = callconduit(ui, b'differential.query', params)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 # 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:
Augie Fackler
formatting: blacken the codebase...
r43346 raise error.Abort(
_(b'cannot get Differential Revision %r') % params
)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 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
Augie Fackler
formatting: blacken the codebase...
r43346 prefetched = {} # {id or phid: drev}
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688
tree = _parse(spec)
drevs, ancestordrevs = _prefetchdrevs(tree)
# developer config: phabricator.batchsize
Matt Harbison
phabricator: pass ui instead of repo to `querydrev()`...
r44906 batchsize = ui.configint(b'phabricator', b'batchsize')
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688
# 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:
Augie Fackler
formatting: blacken the codebase...
r43346 drevs = [
r
for r in validids
if _getstatusname(prefetched[r]) == tree[1]
]
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 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)]
Augie Fackler
formatting: blacken the codebase...
r43346
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 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]))
Augie Fackler
formatting: blacken the codebase...
r43346
Matt Harbison
phabricator: combine commit messages into the review when folding commits...
r45134 def get_amended_desc(drev, ctx, folded):
"""similar to ``getdescfromdrev``, but supports a folded series of commits
This is used when determining if an individual commit needs to have its
message amended after posting it for review. The determination is made for
each individual commit, even when they were folded into one review.
"""
if not folded:
return getdescfromdrev(drev)
uri = b'Differential Revision: %s' % drev[b'uri']
# Since the commit messages were combined when posting multiple commits
# with --fold, the fields can't be read from Phabricator here, or *all*
# affected local revisions will end up with the same commit message after
# the URI is amended in. Append in the DREV line, or update it if it
# exists. At worst, this means commit message or test plan updates on
# Phabricator aren't propagated back to the repository, but that seems
# reasonable for the case where local commits are effectively combined
# in Phabricator.
m = _differentialrevisiondescre.search(ctx.description())
if not m:
return b'\n\n'.join([ctx.description(), uri])
return _differentialrevisiondescre.sub(uri, ctx.description())
Matt Harbison
phabricator: teach `getoldnodedrevmap()` to handle folded reviews...
r45136 def getlocalcommits(diff):
"""get the set of local commits from a diff object
See ``getdiffmeta()`` for an example diff object.
"""
props = diff.get(b'properties') or {}
commits = props.get(b'local:commits') or {}
if len(commits) > 1:
return {bin(c) for c in commits.keys()}
# Storing the diff metadata predates storing `local:commits`, so continue
# to use that in the --no-fold case.
return {bin(getdiffmeta(diff).get(b'node', b'')) or None}
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 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": {
Matt Harbison
phabricator: update the protocol documentation...
r44877 "branch": "default",
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 "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",
Matt Harbison
phabricator: update the protocol documentation...
r44877 "authorEmail": "foo@example.com"
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 "branch": "default",
"commit": "98c08acae292b2faf60a279b4189beb6cff1414d",
Matt Harbison
phabricator: update the protocol documentation...
r44877 "local": "1000",
"message": "...",
"parents": ["6d0abad76b30e4724a37ab8721d630394070fe16"],
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 "rev": "98c08acae292b2faf60a279b4189beb6cff1414d",
"summary": "...",
Matt Harbison
phabricator: update the protocol documentation...
r44877 "tag": "",
"time": 1499546314,
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 }
}
}
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')
Ian Moody
phabricator: fallback to reading metadata from diff for phabread...
r42442 if not meta:
if props.get(b'local:commits'):
commit = sorted(props[b'local:commits'].values())[0]
meta = {}
if b'author' in commit and b'authorEmail' in commit:
Augie Fackler
formatting: blacken the codebase...
r43346 meta[b'user'] = b'%s <%s>' % (
commit[b'author'],
commit[b'authorEmail'],
)
Ian Moody
phabricator: fallback to reading metadata from diff for phabread...
r42442 if b'time' in commit:
Ian Moody
phabricator: handle local:commits time being string or int...
r42837 meta[b'date'] = b'%d 0' % int(commit[b'time'])
Ian Moody
phabricator: fallback to reading metadata from diff for phabread...
r42442 if b'branch' in commit:
meta[b'branch'] = commit[b'branch']
node = commit.get(b'commit', commit.get(b'rev'))
if node:
meta[b'node'] = node
if len(commit.get(b'parents', ())) >= 1:
meta[b'parent'] = commit[b'parents'][0]
else:
meta = {}
if b'date' not in meta and b'dateCreated' in diff:
meta[b'date'] = b'%s 0' % diff[b'dateCreated']
if b'branch' not in meta and diff.get(b'branch'):
meta[b'branch'] = diff[b'branch']
if b'parent' not in meta and diff.get(b'sourceControlBaseRevision'):
meta[b'parent'] = diff[b'sourceControlBaseRevision']
return meta
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688
Augie Fackler
formatting: blacken the codebase...
r43346
Yuya Nishihara
phabricator: remove *-argument from _getdrevs()...
r45076 def _getdrevs(ui, stack, specs):
Matt Harbison
phabricator: add a helper function to convert DREVSPECs to a DREV dict list...
r45069 """convert user supplied DREVSPECs into "Differential Revision" dicts
See ``hg help phabread`` for how to specify each DREVSPEC.
"""
Yuya Nishihara
phabricator: remove *-argument from _getdrevs()...
r45076 if len(specs) > 0:
Matt Harbison
phabricator: add a helper function to convert DREVSPECs to a DREV dict list...
r45069
def _formatspec(s):
if stack:
s = b':(%s)' % s
return b'(%s)' % s
Yuya Nishihara
phabricator: remove *-argument from _getdrevs()...
r45076 spec = b'+'.join(pycompat.maplist(_formatspec, specs))
Matt Harbison
phabricator: add a helper function to convert DREVSPECs to a DREV dict list...
r45069
drevs = querydrev(ui, spec)
if drevs:
return drevs
raise error.Abort(_(b"empty DREVSPEC set"))
Matt Harbison
phabricator: pass ui instead of repo to `readpatch()`...
r44905 def readpatch(ui, drevs, write):
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 """generate plain-text patch readable by 'hg import'
Matt Harbison
phabricator: refactor `phabread` to write all patches at once...
r44909 write takes a list of (DREV, bytes), where DREV is the differential number
(as bytes, without the "D" prefix) and the bytes are the text of a patch
to be imported. drevs is what "querydrev" returns, results of
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 "differential.query".
"""
# Prefetch hg:meta property for all diffs
Augie Fackler
cleanup: run pyupgrade on our source tree to clean up varying things...
r44937 diffids = sorted({max(int(v) for v in drev[b'diffs']) for drev in drevs})
Matt Harbison
phabricator: pass ui instead of repo to `readpatch()`...
r44905 diffs = callconduit(ui, b'differential.querydiffs', {b'ids': diffids})
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688
Matt Harbison
phabricator: refactor `phabread` to write all patches at once...
r44909 patches = []
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 # Generate patch for each drev
for drev in drevs:
Matt Harbison
phabricator: pass ui instead of repo to `readpatch()`...
r44905 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'])
Matt Harbison
phabricator: pass ui instead of repo to `readpatch()`...
r44905 body = callconduit(ui, b'differential.getrawdiff', {b'diffID': diffid})
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 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)
Matt Harbison
phabricator: refactor `phabread` to write all patches at once...
r44909 patches.append((drev[b'id'], content))
# Write patches to the supplied callback
write(patches)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688
Augie Fackler
formatting: blacken the codebase...
r43346
@vcrcommand(
b'phabread',
[(b'', b'stack', False, _(b'read dependencies'))],
Matt Harbison
phabricator: allow multiple DREVSPEC args to phabread|phabimport|phabupdate...
r45071 _(b'DREVSPEC... [OPTIONS]'),
Augie Fackler
formatting: blacken the codebase...
r43346 helpcategory=command.CATEGORY_IMPORT_EXPORT,
Matt Harbison
phabricator: make `hg phabread` work outside of a repository...
r44910 optionalrepo=True,
Augie Fackler
formatting: blacken the codebase...
r43346 )
Matt Harbison
phabricator: allow multiple DREVSPEC args to phabread|phabimport|phabupdate...
r45071 def phabread(ui, repo, *specs, **opts):
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 """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
Matt Harbison
phabricator: allow multiple DREVSPEC args to phabread|phabimport|phabupdate...
r45071 select a stack. If multiple DREVSPEC values are given, the result is the
union of each individually evaluated value. No attempt is currently made
to reorder the values to run from parent to child.
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688
``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)
Matt Harbison
phabricator: allow multiple DREVSPEC args to phabread|phabimport|phabupdate...
r45071 drevs = _getdrevs(ui, opts.get(b'stack'), specs)
Matt Harbison
phabricator: refactor `phabread` to write all patches at once...
r44909
def _write(patches):
for drev, content in patches:
ui.write(content)
Matt Harbison
phabricator: make `hg phabread` work outside of a repository...
r44910 readpatch(ui, drevs, _write)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688
Augie Fackler
formatting: blacken the codebase...
r43346
@vcrcommand(
Matt Harbison
phabricator: add a `phabimport` command...
r45039 b'phabimport',
[(b'', b'stack', False, _(b'import dependencies as well'))],
Matt Harbison
phabricator: allow multiple DREVSPEC args to phabread|phabimport|phabupdate...
r45071 _(b'DREVSPEC... [OPTIONS]'),
Matt Harbison
phabricator: add a `phabimport` command...
r45039 helpcategory=command.CATEGORY_IMPORT_EXPORT,
)
Matt Harbison
phabricator: allow multiple DREVSPEC args to phabread|phabimport|phabupdate...
r45071 def phabimport(ui, repo, *specs, **opts):
Matt Harbison
phabricator: add a `phabimport` command...
r45039 """import patches from Phabricator for the specified Differential Revisions
The patches are read and applied starting at the parent of the working
directory.
See ``hg help phabread`` for how to specify DREVSPEC.
"""
opts = pycompat.byteskwargs(opts)
# --bypass avoids losing exec and symlink bits when importing on Windows,
# and allows importing with a dirty wdir. It also aborts instead of leaving
# rejects.
opts[b'bypass'] = True
# Mandatory default values, synced with commands.import
opts[b'strip'] = 1
opts[b'prefix'] = b''
# Evolve 9.3.0 assumes this key is present in cmdutil.tryimportone()
opts[b'obsolete'] = False
Matt Harbison
phabricator: add a config knob to import in the secret phase...
r45040 if ui.configbool(b'phabimport', b'secret'):
opts[b'secret'] = True
Matt Harbison
phabricator: add a config knob to create obsolete markers when importing...
r45041 if ui.configbool(b'phabimport', b'obsolete'):
opts[b'obsolete'] = True # Handled by evolve wrapping tryimportone()
Matt Harbison
phabricator: add a config knob to import in the secret phase...
r45040
Matt Harbison
phabricator: add a `phabimport` command...
r45039 def _write(patches):
parents = repo[None].parents()
with repo.wlock(), repo.lock(), repo.transaction(b'phabimport'):
for drev, contents in patches:
ui.status(_(b'applying patch from D%s\n') % drev)
with patch.extract(ui, pycompat.bytesio(contents)) as patchdata:
msg, node, rej = cmdutil.tryimportone(
ui,
repo,
patchdata,
parents,
opts,
[],
None, # Never update wdir to another revision
)
if not node:
raise error.Abort(_(b'D%s: no diffs found') % drev)
ui.note(msg + b'\n')
parents = [repo[node]]
Matt Harbison
phabricator: allow multiple DREVSPEC args to phabread|phabimport|phabupdate...
r45071 drevs = _getdrevs(ui, opts.get(b'stack'), specs)
Matt Harbison
phabricator: add a `phabimport` command...
r45039
readpatch(repo.ui, drevs, _write)
@vcrcommand(
Augie Fackler
formatting: blacken the codebase...
r43346 b'phabupdate',
[
(b'', b'accept', False, _(b'accept revisions')),
(b'', b'reject', False, _(b'reject revisions')),
Matt Harbison
phabupdate: allows revisions to be marked "request-review"...
r45696 (b'', b'request-review', False, _(b'request review on revisions')),
Augie Fackler
formatting: blacken the codebase...
r43346 (b'', b'abandon', False, _(b'abandon revisions')),
(b'', b'reclaim', False, _(b'reclaim revisions')),
Matt Harbison
phabupdate: allow revisions to be marked "closed"...
r45697 (b'', b'close', False, _(b'close revisions')),
Matt Harbison
phabupdate: allow revisions to be reopened...
r45698 (b'', b'reopen', False, _(b'reopen revisions')),
Matt Harbison
phabupdate: allow revisions to be marked with "plan changes"...
r45695 (b'', b'plan-changes', False, _(b'plan changes for revisions')),
Matt Harbison
phabupdate: allow resigning from revisions...
r45700 (b'', b'resign', False, _(b'resign as a reviewer from revisions')),
Matt Harbison
phabupdate: allow revisions to be commandeered...
r45699 (b'', b'commandeer', False, _(b'commandeer revisions')),
Augie Fackler
formatting: blacken the codebase...
r43346 (b'm', b'comment', b'', _(b'comment on the last revision')),
],
Matt Harbison
phabricator: allow multiple DREVSPEC args to phabread|phabimport|phabupdate...
r45071 _(b'DREVSPEC... [OPTIONS]'),
Augie Fackler
formatting: blacken the codebase...
r43346 helpcategory=command.CATEGORY_IMPORT_EXPORT,
Matt Harbison
phabricator: make `hg phabupdate` work outside of a repository...
r44908 optionalrepo=True,
Augie Fackler
formatting: blacken the codebase...
r43346 )
Matt Harbison
phabricator: allow multiple DREVSPEC args to phabread|phabimport|phabupdate...
r45071 def phabupdate(ui, repo, *specs, **opts):
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 """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)
Matt Harbison
phabupdate: allow revisions to be marked with "plan changes"...
r45695 transactions = [
b'abandon',
b'accept',
Matt Harbison
phabupdate: allow revisions to be marked "closed"...
r45697 b'close',
Matt Harbison
phabupdate: allow revisions to be commandeered...
r45699 b'commandeer',
Matt Harbison
phabupdate: allow revisions to be marked with "plan changes"...
r45695 b'plan-changes',
b'reclaim',
b'reject',
Matt Harbison
phabupdate: allow revisions to be reopened...
r45698 b'reopen',
Matt Harbison
phabupdate: allows revisions to be marked "request-review"...
r45696 b'request-review',
Matt Harbison
phabupdate: allow resigning from revisions...
r45700 b'resign',
Matt Harbison
phabupdate: allow revisions to be marked with "plan changes"...
r45695 ]
flags = [n for n in transactions if opts.get(n.replace(b'-', b'_'))]
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 if len(flags) > 1:
raise error.Abort(_(b'%s cannot be used together') % b', '.join(flags))
actions = []
for f in flags:
Ian Moody
phabricator: use True primitive instead of b'true' for phabupdate actions...
r43667 actions.append({b'type': f, b'value': True})
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688
Matt Harbison
phabricator: allow multiple DREVSPEC args to phabread|phabimport|phabupdate...
r45071 drevs = _getdrevs(ui, opts.get(b'stack'), specs)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 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:
Augie Fackler
formatting: blacken the codebase...
r43346 params = {
b'objectIdentifier': drev[b'phid'],
b'transactions': actions,
}
Pulkit Goyal
phabricator: pass ui instead of repo to callconduit...
r42627 callconduit(ui, b'differential.revision.edit', params)
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688
Augie Fackler
formatting: blacken the codebase...
r43346
Matt Harbison
phabricator: use exthelper to register commands, config, and templates...
r43243 @eh.templatekeyword(b'phabreview', requires={b'ctx'})
Augie Fackler
phabricator: move extension from contrib to hgext...
r39688 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
formatting: blacken the codebase...
r43346 return templateutil.hybriddict(
Augie Fackler
cleanup: remove pointless r-prefixes on single-quoted strings...
r43906 {b'url': m.group('url'), b'id': b"D%s" % m.group('id'),}
Augie Fackler
formatting: blacken the codebase...
r43346 )
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
Augie Fackler
formatting: blacken the codebase...
r43346 return templateutil.hybriddict({b'url': url, b'id': t,})
Matt Harbison
phabricator: teach {phabreview} to work without --amend...
r41199 return None
Denis Laxalde
phabricator: add a "phabstatus" show view...
r44291
Denis Laxalde
phabricator: add a "phabstatus" template keyword...
r44292 @eh.templatekeyword(b'phabstatus', requires={b'ctx', b'repo', b'ui'})
def template_status(context, mapping):
""":phabstatus: String. Status of Phabricator differential.
"""
ctx = context.resource(mapping, b'ctx')
repo = context.resource(mapping, b'repo')
ui = context.resource(mapping, b'ui')
rev = ctx.rev()
try:
drevid = getdrevmap(repo, [rev])[rev]
except KeyError:
return None
drevs = callconduit(ui, b'differential.query', {b'ids': [drevid]})
for drev in drevs:
if int(drev[b'id']) == drevid:
return templateutil.hybriddict(
{b'url': drev[b'uri'], b'status': drev[b'statusName'],}
)
return None
Denis Laxalde
phabricator: add a "phabstatus" show view...
r44291 @show.showview(b'phabstatus', csettopic=b'work')
def phabstatusshowview(ui, repo, displayer):
"""Phabricator differiential status"""
revs = repo.revs('sort(_underway(), topo)')
drevmap = getdrevmap(repo, revs)
Augie Fackler
cleanup: run pyupgrade on our source tree to clean up varying things...
r44937 unknownrevs, drevids, revsbydrevid = [], set(), {}
Denis Laxalde
phabricator: add a "phabstatus" show view...
r44291 for rev, drevid in pycompat.iteritems(drevmap):
if drevid is not None:
drevids.add(drevid)
Augie Fackler
cleanup: run pyupgrade on our source tree to clean up varying things...
r44937 revsbydrevid.setdefault(drevid, set()).add(rev)
Denis Laxalde
phabricator: add a "phabstatus" show view...
r44291 else:
unknownrevs.append(rev)
drevs = callconduit(ui, b'differential.query', {b'ids': list(drevids)})
drevsbyrev = {}
for drev in drevs:
for rev in revsbydrevid[int(drev[b'id'])]:
drevsbyrev[rev] = drev
def phabstatus(ctx):
drev = drevsbyrev[ctx.rev()]
Matt Harbison
phabricator: color the status in the "phabstatus" view...
r44310 status = ui.label(
b'%(statusName)s' % drev,
b'phabricator.status.%s' % _getstatusname(drev),
)
ui.write(b"\n%s %s\n" % (drev[b'uri'], status))
Denis Laxalde
phabricator: add a "phabstatus" show view...
r44291
revs -= smartset.baseset(unknownrevs)
revdag = graphmod.dagwalker(repo, revs)
ui.setconfig(b'experimental', b'graphshorten', True)
displayer._exthook = phabstatus
nodelen = show.longestshortest(repo, revs)
logcmdutil.displaygraph(
ui,
repo,
revdag,
displayer,
graphmod.asciiedges,
props={b'nodelen': nodelen},
)