##// END OF EJS Templates
phabricator: add node and p1 to hg:meta property...
phabricator: add node and p1 to hg:meta property The "hg:meta" property is for extra metadata to reconstruct the patch. Previously it does not have node or parent information since I think by reading the patch again, the commit message will be mangled (like, added the "Differential Revision" line) and we cannot preserve the commit hash. However, the "parent" information could be useful. It could be helpful to locate the "base revision" so in case of a conflict applying the patch directly, we might be able to use 3-way merge to resolve it correctly. Note: "local:commits" is an existing "property" used by Phabricator that has the node and parent information. However, it lacks of timezone information and requires "author" and "authorEmail" to be separated. So we are using a different "property" - "hg:meta" to be distinguished from "local:commits".

File last commit:

r33264:26632157 default
r33264:26632157 default
Show More
phabricator.py
363 lines | 12.8 KiB | text/x-python | PythonLexer
Jun Wu
phabricator: add a contrib script...
r33195 # phabricator.py - simple Phabricator integration
#
# Copyright 2017 Facebook, Inc.
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.
"""simple Phabricator integration
Jun Wu
phabricator: add phabsend command to send a stack...
r33196 This extension provides a ``phabsend`` command which sends a stack of
Jun Wu
phabricator: add phabread command to read patches...
r33197 changesets to Phabricator without amending commit messages, and a ``phabread``
command which prints a stack of revisions in a format suitable
for :hg:`import`.
Jun Wu
phabricator: add phabsend command to send a stack...
r33196
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.
Jun Wu
phabricator: add a contrib script...
r33195 Config::
[phabricator]
# Phabricator URL
url = https://phab.example.com/
# API token. Get it from https://$HOST/conduit/login/
token = cli-xxxxxxxxxxxxxxxxxxxxxxxxxxxx
Jun Wu
phabricator: add phabsend command to send a stack...
r33196
# Repo callsign. If a repo has a URL https://$HOST/diffusion/FOO, then its
# callsign is "FOO".
callsign = FOO
Jun Wu
phabricator: add phabread command to read patches...
r33197
Jun Wu
phabricator: add a contrib script...
r33195 """
from __future__ import absolute_import
import json
Jun Wu
phabricator: add phabsend command to send a stack...
r33196 import re
Jun Wu
phabricator: add a contrib script...
r33195
from mercurial.i18n import _
from mercurial import (
Jun Wu
phabricator: add phabsend command to send a stack...
r33196 encoding,
Jun Wu
phabricator: add a contrib script...
r33195 error,
Jun Wu
phabricator: add phabsend command to send a stack...
r33196 mdiff,
obsolete,
patch,
Jun Wu
phabricator: add a contrib script...
r33195 registrar,
Jun Wu
phabricator: add phabsend command to send a stack...
r33196 scmutil,
tags,
Jun Wu
phabricator: add a contrib script...
r33195 url as urlmod,
util,
)
cmdtable = {}
command = registrar.command(cmdtable)
def urlencodenested(params):
"""like urlencode, but works with nested parameters.
For example, if params is {'a': ['b', 'c'], 'd': {'e': 'f'}}, it will be
flattened to {'a[0]': 'b', 'a[1]': 'c', 'd[e]': 'f'} and then passed to
urlencode. Note: the encoding is consistent with PHP's http_build_query.
"""
flatparams = util.sortdict()
def process(prefix, obj):
items = {list: enumerate, dict: lambda x: x.items()}.get(type(obj))
if items is None:
flatparams[prefix] = obj
else:
for k, v in items(obj):
if prefix:
process('%s[%s]' % (prefix, k), v)
else:
process(k, v)
process('', params)
return util.urlreq.urlencode(flatparams)
def readurltoken(repo):
"""return conduit url, token and make sure they exist
Currently read from [phabricator] config section. In the future, it might
make sense to read from .arcconfig and .arcrc as well.
"""
values = []
section = 'phabricator'
for name in ['url', 'token']:
value = repo.ui.config(section, name)
if not value:
raise error.Abort(_('config %s.%s is required') % (section, name))
values.append(value)
return values
def callconduit(repo, name, params):
"""call Conduit API, params is a dict. return json.loads result, or None"""
host, token = readurltoken(repo)
url, authinfo = util.url('/'.join([host, 'api', name])).authinfo()
urlopener = urlmod.opener(repo.ui, authinfo)
repo.ui.debug('Conduit Call: %s %s\n' % (url, params))
params = params.copy()
params['api.token'] = token
request = util.urlreq.request(url, data=urlencodenested(params))
body = urlopener.open(request).read()
repo.ui.debug('Conduit Response: %s\n' % body)
parsed = json.loads(body)
if parsed.get(r'error_code'):
msg = (_('Conduit Error (%s): %s')
% (parsed[r'error_code'], parsed[r'error_info']))
raise error.Abort(msg)
return parsed[r'result']
@command('debugcallconduit', [], _('METHOD'))
def debugcallconduit(ui, repo, name):
"""call Conduit API
Call parameters are read from stdin as a JSON blob. Result will be written
to stdout as a JSON blob.
"""
params = json.loads(ui.fin.read())
result = callconduit(repo, name, params)
s = json.dumps(result, sort_keys=True, indent=2, separators=(',', ': '))
ui.write('%s\n' % s)
Jun Wu
phabricator: add phabsend command to send a stack...
r33196
def getrepophid(repo):
"""given callsign, return repository PHID or None"""
# developer config: phabricator.repophid
repophid = repo.ui.config('phabricator', 'repophid')
if repophid:
return repophid
callsign = repo.ui.config('phabricator', 'callsign')
if not callsign:
return None
query = callconduit(repo, 'diffusion.repository.search',
{'constraints': {'callsigns': [callsign]}})
if len(query[r'data']) == 0:
return None
repophid = encoding.strtolocal(query[r'data'][0][r'phid'])
repo.ui.setconfig('phabricator', 'repophid', repophid)
return repophid
Jun Wu
phabricator: check associated Differential Revision from commit message...
r33263 _differentialrevisiontagre = re.compile('\AD([1-9][0-9]*)\Z')
_differentialrevisiondescre = re.compile(
'^Differential Revision:.*D([1-9][0-9]*)$', re.M)
Jun Wu
phabricator: add phabsend command to send a stack...
r33196
def getmapping(ctx):
"""return (node, associated Differential Revision ID) or (None, None)
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.
Jun Wu
phabricator: check associated Differential Revision from commit message...
r33263
If tags are not found, examine commit message. The "Differential Revision:"
line could associate this changeset to a Differential Revision.
Jun Wu
phabricator: add phabsend command to send a stack...
r33196 """
unfi = ctx.repo().unfiltered()
nodemap = unfi.changelog.nodemap
Jun Wu
phabricator: check associated Differential Revision from commit message...
r33263
# Check tags like "D123"
Jun Wu
phabricator: add phabsend command to send a stack...
r33196 for n in obsolete.allprecursors(unfi.obsstore, [ctx.node()]):
if n in nodemap:
for tag in unfi.nodetags(n):
Jun Wu
phabricator: check associated Differential Revision from commit message...
r33263 m = _differentialrevisiontagre.match(tag)
Jun Wu
phabricator: add phabsend command to send a stack...
r33196 if m:
return n, int(m.group(1))
Jun Wu
phabricator: check associated Differential Revision from commit message...
r33263
# Check commit message
m = _differentialrevisiondescre.search(ctx.description())
if m:
return None, int(m.group(1))
Jun Wu
phabricator: add phabsend command to send a stack...
r33196 return None, None
def getdiff(ctx, diffopts):
"""plain-text diff without header (user, commit message, etc)"""
output = util.stringio()
for chunk, _label in patch.diffui(ctx.repo(), ctx.p1().node(), ctx.node(),
None, opts=diffopts):
output.write(chunk)
return output.getvalue()
def creatediff(ctx):
"""create a Differential Diff"""
repo = ctx.repo()
repophid = getrepophid(repo)
# Create a "Differential Diff" via "differential.createrawdiff" API
params = {'diff': getdiff(ctx, mdiff.diffopts(git=True, context=32767))}
if repophid:
params['repositoryPHID'] = repophid
diff = callconduit(repo, 'differential.createrawdiff', params)
if not diff:
raise error.Abort(_('cannot create diff for %s') % ctx)
return diff
def writediffproperties(ctx, diff):
"""write metadata to diff so patches could be applied losslessly"""
params = {
'diff_id': diff[r'id'],
'name': 'hg:meta',
'data': json.dumps({
'user': ctx.user(),
'date': '%d %d' % ctx.date(),
Jun Wu
phabricator: add node and p1 to hg:meta property...
r33264 'node': ctx.hex(),
'parent': ctx.p1().hex(),
Jun Wu
phabricator: add phabsend command to send a stack...
r33196 }),
}
callconduit(ctx.repo(), 'differential.setdiffproperty', params)
def createdifferentialrevision(ctx, revid=None, parentrevid=None):
"""create or update a Differential Revision
If revid is None, create a new Differential Revision, otherwise update
revid. If parentrevid is not None, set it as a dependency.
"""
repo = ctx.repo()
diff = creatediff(ctx)
writediffproperties(ctx, diff)
transactions = [{'type': 'update', 'value': diff[r'phid']}]
# Use a temporary summary to set dependency. There might be better ways but
# I cannot find them for now. But do not do that if we are updating an
# existing revision (revid is not None) since that introduces visible
# churns (someone edited "Summary" twice) on the web page.
if parentrevid and revid is None:
summary = 'Depends on D%s' % parentrevid
transactions += [{'type': 'summary', 'value': summary},
{'type': 'summary', 'value': ' '}]
# Parse commit message and update related fields.
desc = ctx.description()
info = callconduit(repo, 'differential.parsecommitmessage',
{'corpus': desc})
for k, v in info[r'fields'].items():
if k in ['title', 'summary', 'testPlan']:
transactions.append({'type': k, 'value': v})
params = {'transactions': transactions}
if revid is not None:
# Update an existing Differential Revision
params['objectIdentifier'] = revid
revision = callconduit(repo, 'differential.revision.edit', params)
if not revision:
raise error.Abort(_('cannot create revision for %s') % ctx)
return revision
@command('phabsend',
[('r', 'rev', [], _('revisions to send'), _('REV'))],
_('REV [OPTIONS]'))
def phabsend(ui, repo, *revs, **opts):
"""upload changesets to Phabricator
If there are multiple revisions specified, they will be send as a stack
with a linear dependencies relationship using the order specified by the
revset.
For the first time uploading changesets, local tags will be created to
maintain the association. After the first time, phabsend will check
obsstore and tags information so it can figure out whether to update an
existing Differential Revision, or create a new one.
"""
revs = list(revs) + opts.get('rev', [])
revs = scmutil.revrange(repo, revs)
# Send patches one by one so we know their Differential Revision IDs and
# can provide dependency relationship
lastrevid = None
for rev in revs:
ui.debug('sending rev %d\n' % rev)
ctx = repo[rev]
# Get Differential Revision ID
oldnode, revid = getmapping(ctx)
if oldnode != ctx.node():
# Create or update Differential Revision
revision = createdifferentialrevision(ctx, revid, lastrevid)
newrevid = int(revision[r'object'][r'id'])
if revid:
action = _('updated')
else:
action = _('created')
# Create a local tag to note the association
tagname = 'D%d' % newrevid
tags.tag(repo, tagname, ctx.node(), message=None, user=None,
date=None, local=True)
else:
# Nothing changed. But still set "newrevid" so the next revision
# could depend on this one.
newrevid = revid
action = _('skipped')
ui.write(_('D%s: %s - %s: %s\n') % (newrevid, action, ctx,
ctx.description().split('\n')[0]))
lastrevid = newrevid
Jun Wu
phabricator: add phabread command to read patches...
r33197
_summaryre = re.compile('^Summary:\s*', re.M)
Jun Wu
phabricator: add node and p1 to hg:meta property...
r33264 # Map from "hg:meta" keys to header understood by "hg import". The order is
# consistent with "hg export" output.
_metanamemap = util.sortdict([(r'user', 'User'), (r'date', 'Date'),
(r'node', 'Node ID'), (r'parent', 'Parent ')])
Jun Wu
phabricator: add phabread command to read patches...
r33197 def readpatch(repo, params, recursive=False):
"""generate plain-text patch readable by 'hg import'
params is passed to "differential.query". If recursive is True, also return
dependent patches.
"""
# Differential Revisions
drevs = callconduit(repo, 'differential.query', params)
if len(drevs) == 1:
drev = drevs[0]
else:
raise error.Abort(_('cannot get Differential Revision %r') % params)
repo.ui.note(_('reading D%s\n') % drev[r'id'])
diffid = max(int(v) for v in drev[r'diffs'])
body = callconduit(repo, 'differential.getrawdiff', {'diffID': diffid})
desc = callconduit(repo, 'differential.getcommitmessage',
{'revision_id': drev[r'id']})
header = '# HG changeset patch\n'
# Remove potential empty "Summary:"
desc = _summaryre.sub('', desc)
Jun Wu
phabricator: add node and p1 to hg:meta property...
r33264 # 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.
Jun Wu
phabricator: add phabread command to read patches...
r33197 diffs = callconduit(repo, 'differential.querydiffs', {'ids': [diffid]})
props = diffs[str(diffid)][r'properties'] # could be empty list or dict
if props and r'hg:meta' in props:
meta = props[r'hg:meta']
Jun Wu
phabricator: add node and p1 to hg:meta property...
r33264 for k in _metanamemap.keys():
if k in meta:
header += '# %s %s\n' % (_metanamemap[k], meta[k])
Jun Wu
phabricator: add phabread command to read patches...
r33197
patch = ('%s%s\n%s') % (header, desc, body)
# Check dependencies
if recursive:
auxiliary = drev.get(r'auxiliary', {})
depends = auxiliary.get(r'phabricator:depends-on', [])
for phid in depends:
patch = readpatch(repo, {'phids': [phid]}, recursive=True) + patch
return patch
@command('phabread',
[('', 'stack', False, _('read dependencies'))],
_('REVID [OPTIONS]'))
def phabread(ui, repo, revid, **opts):
"""print patches from Phabricator suitable for importing
REVID could be a Differential Revision identity, like ``D123``, or just the
number ``123``, or a full URL like ``https://phab.example.com/D123``.
If --stack is given, follow dependencies information and read all patches.
"""
try:
revid = int(revid.split('/')[-1].replace('D', ''))
except ValueError:
raise error.Abort(_('invalid Revision ID: %s') % revid)
patch = readpatch(repo, {'ids': [revid]}, recursive=opts.get('stack'))
ui.write(patch)