##// END OF EJS Templates
rebase: rewrite core algorithm (issue5578) (issue5630)...
rebase: rewrite core algorithm (issue5578) (issue5630) "defineparents" is the core algorithm of rebase. The old code has too many tech debts (like outdated comments, confusing assertions, etc) and is very error-prone to be improved. This patch rewrites "defineparents" to make the code easier to reason about, and solve a bunch of issues, including: - Assertion error: no base found (demonstrated by D212, issue5578) - Asymmetric result (demonstrated by D211, "F with one parent") - Wrong new parent (demonstrated by D262, "C':A'A'") - "revlog index out of range" (demonstrated by D262, issue5630) - "nothing to merge" (demonstrated by D262) As a side effect, merge base decision has been made more solid - rebase now prints out explicitly what could go wrong when it cannot find a unique suitable merge base. .. fix:: Issue 5578, Issue 5630 Core rebase algorithm has been rewritten to be more robust. Differential Revision: https://phab.mercurial-scm.org/D21

File last commit:

r33782:f7d6978a default
r33783:09755061 default
Show More
phabricator.py
631 lines | 23.0 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
Jun Wu
phabricator: verify local tags before trusting them...
r33443 from mercurial.node import bin, nullid
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,
Boris Feld
obsutil: rename allprecursors into allpredecessors...
r33701 obsutil,
Jun Wu
phabricator: add phabsend command to send a stack...
r33196 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(
Jun Wu
phabricator: use Phabricator's last node information...
r33654 '^Differential Revision:\s*(?:.*)D([1-9][0-9]*)$', re.M)
Jun Wu
phabricator: add phabsend command to send a stack...
r33196
Jun Wu
phabricator: finding old nodes in batch...
r33442 def getoldnodedrevmap(repo, nodelist):
"""find previous nodes that has been sent to Phabricator
Jun Wu
phabricator: use Phabricator's last node information...
r33654 return {node: (oldnode, Differential diff, Differential Revision ID)}
Jun Wu
phabricator: finding old nodes in batch...
r33442 for node in nodelist with known previous sent versions, or associated
Jun Wu
phabricator: use Phabricator's last node information...
r33654 Differential Revision IDs. ``oldnode`` and ``Differential diff`` could
be ``None``.
Jun Wu
phabricator: add phabsend command to send a stack...
r33196
Jun Wu
phabricator: use Phabricator's last node information...
r33654 Examines commit messages like "Differential Revision:" to get the
association information.
Jun Wu
phabricator: check associated Differential Revision from commit message...
r33263
Jun Wu
phabricator: use Phabricator's last node information...
r33654 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.
Jun Wu
phabricator: add phabsend command to send a stack...
r33196 """
Jun Wu
phabricator: finding old nodes in batch...
r33442 url, token = readurltoken(repo)
unfi = repo.unfiltered()
Jun Wu
phabricator: add phabsend command to send a stack...
r33196 nodemap = unfi.changelog.nodemap
Jun Wu
phabricator: check associated Differential Revision from commit message...
r33263
Jun Wu
phabricator: use Phabricator's last node information...
r33654 result = {} # {node: (oldnode?, lastdiff?, drev)}
toconfirm = {} # {node: (force, {precnode}, drev)}
Jun Wu
phabricator: finding old nodes in batch...
r33442 for node in nodelist:
ctx = unfi[node]
Jun Wu
phabricator: verify local tags before trusting them...
r33443 # For tags like "D123", put them into "toconfirm" to verify later
Boris Feld
obsutil: rename allprecursors into allpredecessors...
r33701 precnodes = list(obsutil.allpredecessors(unfi.obsstore, [node]))
Jun Wu
phabricator: verify local tags before trusting them...
r33443 for n in precnodes:
Jun Wu
phabricator: finding old nodes in batch...
r33442 if n in nodemap:
for tag in unfi.nodetags(n):
m = _differentialrevisiontagre.match(tag)
if m:
Jun Wu
phabricator: use Phabricator's last node information...
r33654 toconfirm[node] = (0, set(precnodes), int(m.group(1)))
Jun Wu
phabricator: finding old nodes in batch...
r33442 continue
Jun Wu
phabricator: check associated Differential Revision from commit message...
r33263
Jun Wu
phabricator: use Phabricator's last node information...
r33654 # Check commit message
Jun Wu
phabricator: finding old nodes in batch...
r33442 m = _differentialrevisiondescre.search(ctx.description())
if m:
Jun Wu
phabricator: use Phabricator's last node information...
r33654 toconfirm[node] = (1, set(precnodes), int(m.group(1)))
Jun Wu
phabricator: check associated Differential Revision from commit message...
r33263
Jun Wu
phabricator: verify local tags before trusting them...
r33443 # Double check if tags are genuine by collecting all old nodes from
# Phabricator, and expect precursors overlap with it.
if toconfirm:
Jun Wu
phabricator: use Phabricator's last node information...
r33654 drevs = [drev for force, precs, drev in toconfirm.values()]
alldiffs = callconduit(unfi, 'differential.querydiffs',
{'revisionIDs': drevs})
getnode = lambda d: bin(encoding.unitolocal(
getdiffmeta(d).get(r'node', ''))) or None
for newnode, (force, precset, drev) in toconfirm.items():
diffs = [d for d in alldiffs.values()
if int(d[r'revisionID']) == drev]
# "precursors" as known by Phabricator
phprecset = set(getnode(d) for d in diffs)
# Ignore if precursors (Phabricator and local repo) do not overlap,
# and force is not set (when commit message says nothing)
if not force and not bool(phprecset & precset):
Jun Wu
phabricator: verify local tags before trusting them...
r33443 tagname = 'D%d' % drev
tags.tag(repo, tagname, nullid, message=None, user=None,
date=None, local=True)
unfi.ui.warn(_('D%s: local tag removed - does not match '
'Differential history\n') % drev)
Jun Wu
phabricator: use Phabricator's last node information...
r33654 continue
# Find the last node using Phabricator metadata, and make sure it
# exists in the repo
oldnode = lastdiff = None
if diffs:
lastdiff = max(diffs, key=lambda d: int(d[r'id']))
oldnode = getnode(lastdiff)
if oldnode and oldnode not in nodemap:
oldnode = None
result[newnode] = (oldnode, lastdiff, drev)
Jun Wu
phabricator: verify local tags before trusting them...
r33443
Jun Wu
phabricator: finding old nodes in batch...
r33442 return result
Jun Wu
phabricator: add phabsend command to send a stack...
r33196
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)
Jun Wu
phabricator: allow specifying reviewers on phabsend...
r33498 def createdifferentialrevision(ctx, revid=None, parentrevid=None, oldnode=None,
Jun Wu
phabricator: update diff property even if we choose not to create a new diff...
r33655 olddiff=None, actions=None):
Jun Wu
phabricator: add phabsend command to send a stack...
r33196 """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.
Jun Wu
phabricator: do not upload new diff if nothing changes...
r33265
If oldnode is not None, check if the patch content (without commit message
and metadata) has changed before creating another diff.
Jun Wu
phabricator: allow specifying reviewers on phabsend...
r33498
If actions is not None, they will be appended to the transaction.
Jun Wu
phabricator: add phabsend command to send a stack...
r33196 """
repo = ctx.repo()
Jun Wu
phabricator: do not upload new diff if nothing changes...
r33265 if oldnode:
diffopts = mdiff.diffopts(git=True, context=1)
oldctx = repo.unfiltered()[oldnode]
neednewdiff = (getdiff(ctx, diffopts) != getdiff(oldctx, diffopts))
else:
neednewdiff = True
Jun Wu
phabricator: add phabsend command to send a stack...
r33196
Jun Wu
phabricator: do not upload new diff if nothing changes...
r33265 transactions = []
if neednewdiff:
diff = creatediff(ctx)
transactions.append({'type': 'update', 'value': diff[r'phid']})
Jun Wu
phabricator: update diff property even if we choose not to create a new diff...
r33655 else:
# Even if we don't need to upload a new diff because the patch content
# does not change. We might still need to update its metadata so
# pushers could know the correct node metadata.
assert olddiff
diff = olddiff
writediffproperties(ctx, diff)
Jun Wu
phabricator: add phabsend command to send a stack...
r33196
# 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': ' '}]
Jun Wu
phabricator: allow specifying reviewers on phabsend...
r33498 if actions:
transactions += actions
Jun Wu
phabricator: add phabsend command to send a stack...
r33196 # 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
Jun Wu
phabricator: allow specifying reviewers on phabsend...
r33498 def userphids(repo, names):
"""convert user names to PHIDs"""
query = {'constraints': {'usernames': names}}
result = callconduit(repo, 'user.search', query)
# username not found is not an error of the API. So check if we have missed
# some names here.
data = result[r'data']
resolved = set(entry[r'fields'][r'username'] for entry in data)
unresolved = set(names) - resolved
if unresolved:
raise error.Abort(_('unknown username: %s')
% ' '.join(sorted(unresolved)))
return [entry[r'phid'] for entry in data]
Jun Wu
phabricator: add phabsend command to send a stack...
r33196 @command('phabsend',
Jun Wu
phabricator: allow specifying reviewers on phabsend...
r33498 [('r', 'rev', [], _('revisions to send'), _('REV')),
Pulkit Goyal
phabricator: add --confirm option to phabsend command...
r33653 ('', 'reviewer', [], _('specify reviewers')),
('', 'confirm', None, _('ask for confirmation before sending'))],
Jun Wu
phabricator: add phabsend command to send a stack...
r33196 _('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.
Pulkit Goyal
phabricator: add --confirm option to phabsend command...
r33653
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
Jun Wu
phabricator: add phabsend command to send a stack...
r33196 """
revs = list(revs) + opts.get('rev', [])
revs = scmutil.revrange(repo, revs)
Jun Wu
phabricator: abort if phabsend gets empty revs...
r33266 if not revs:
raise error.Abort(_('phabsend requires at least one changeset'))
Pulkit Goyal
phabricator: add --confirm option to phabsend command...
r33653 confirm = ui.configbool('phabsend', 'confirm')
confirm |= bool(opts.get('confirm'))
if confirm:
confirmed = _confirmbeforesend(repo, revs)
if not confirmed:
raise error.Abort(_('phabsend cancelled'))
Jun Wu
phabricator: allow specifying reviewers on phabsend...
r33498 actions = []
reviewers = opts.get('reviewer', [])
if reviewers:
phids = userphids(repo, reviewers)
actions.append({'type': 'reviewers.add', 'value': phids})
Jun Wu
phabricator: use Phabricator's last node information...
r33654 # {newnode: (oldnode, olddiff, olddrev}
oldmap = getoldnodedrevmap(repo, [repo[r].node() for r in revs])
Jun Wu
phabricator: finding old nodes in batch...
r33442
Jun Wu
phabricator: add phabsend command to send a stack...
r33196 # 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
Jun Wu
phabricator: use Phabricator's last node information...
r33654 oldnode, olddiff, revid = oldmap.get(ctx.node(), (None, None, None))
Jun Wu
phabricator: add phabsend command to send a stack...
r33196 if oldnode != ctx.node():
# Create or update Differential Revision
Jun Wu
phabricator: do not upload new diff if nothing changes...
r33265 revision = createdifferentialrevision(ctx, revid, lastrevid,
Jun Wu
phabricator: update diff property even if we choose not to create a new diff...
r33655 oldnode, olddiff, actions)
Jun Wu
phabricator: add phabsend command to send a stack...
r33196 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
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 ')])
Pulkit Goyal
phabricator: add --confirm option to phabsend command...
r33653 def _confirmbeforesend(repo, revs):
ui = repo.ui
for rev in revs:
ctx = repo[rev]
desc = ctx.description().splitlines()[0]
ui.write(('%d: ' % rev), label='phabsend.revnumber')
ui.write(('%s\n' % desc), label='phabsend.desc')
if ui.promptchoice(_('Phabsend the above changes (yn)?'
'$$ &Yes $$ &No')):
return False
return True
Jun Wu
phabricator: rework phabread to reduce memory usage and round-trips...
r33267 def querydrev(repo, params, stack=False):
"""return a list of "Differential Revision" dicts
params is the input of "differential.query" API, and is expected to match
just a single Differential Revision.
A "Differential Revision dict" looks like:
{
"id": "2",
"phid": "PHID-DREV-672qvysjcczopag46qty",
"title": "example",
"uri": "https://phab.example.com/D2",
"dateCreated": "1499181406",
"dateModified": "1499182103",
"authorPHID": "PHID-USER-tv3ohwc4v4jeu34otlye",
"status": "0",
"statusName": "Needs Review",
"properties": [],
"branch": null,
"summary": "",
"testPlan": "",
"lineCount": "2",
"activeDiffPHID": "PHID-DIFF-xoqnjkobbm6k4dk6hi72",
"diffs": [
"3",
"4",
],
"commits": [],
"reviewers": [],
"ccs": [],
"hashes": [],
"auxiliary": {
"phabricator:projects": [],
"phabricator:depends-on": [
"PHID-DREV-gbapp366kutjebt7agcd"
]
},
"repositoryPHID": "PHID-REPO-hub2hx62ieuqeheznasv",
"sourcePath": null
}
If stack is True, return a list of "Differential Revision dict"s in an
order that the latter ones depend on the former ones. Otherwise, return a
list of a unique "Differential Revision dict".
"""
Jun Wu
phabricator: try to fetch differential revisions in batch...
r33269 prefetched = {} # {id or phid: drev}
def fetch(params):
"""params -> single drev or None"""
key = (params.get(r'ids') or params.get(r'phids') or [None])[0]
if key in prefetched:
return prefetched[key]
# Otherwise, send the request. If we're fetching a stack, be smarter
# and fetch more ids in one batch, even if it could be unnecessary.
batchparams = params
if stack and len(params.get(r'ids', [])) == 1:
i = int(params[r'ids'][0])
# developer config: phabricator.batchsize
batchsize = repo.ui.configint('phabricator', 'batchsize', 12)
batchparams = {'ids': range(max(1, i - batchsize), i + 1)}
drevs = callconduit(repo, 'differential.query', batchparams)
# Fill prefetched with the result
for drev in drevs:
prefetched[drev[r'phid']] = drev
prefetched[int(drev[r'id'])] = drev
if key not in prefetched:
raise error.Abort(_('cannot get Differential Revision %r') % params)
return prefetched[key]
Jun Wu
phabricator: do not read a same revision twice...
r33271 visited = set()
Jun Wu
phabricator: rework phabread to reduce memory usage and round-trips...
r33267 result = []
queue = [params]
while queue:
params = queue.pop()
Jun Wu
phabricator: try to fetch differential revisions in batch...
r33269 drev = fetch(params)
Jun Wu
phabricator: do not read a same revision twice...
r33271 if drev[r'id'] in visited:
continue
visited.add(drev[r'id'])
Jun Wu
phabricator: rework phabread to reduce memory usage and round-trips...
r33267 result.append(drev)
if stack:
auxiliary = drev.get(r'auxiliary', {})
depends = auxiliary.get(r'phabricator:depends-on', [])
for phid in depends:
queue.append({'phids': [phid]})
result.reverse()
return result
Jun Wu
phabricator: avoid calling differential.getcommitmessage...
r33268 def getdescfromdrev(drev):
"""get description (commit message) from "Differential Revision"
This is similar to differential.getcommitmessage API. But we only care
about limited fields: title, summary, test plan, and URL.
"""
title = drev[r'title']
summary = drev[r'summary'].rstrip()
testplan = drev[r'testPlan'].rstrip()
if testplan:
testplan = 'Test Plan:\n%s' % testplan
uri = 'Differential Revision: %s' % drev[r'uri']
return '\n\n'.join(filter(None, [title, summary, testplan, uri]))
Jun Wu
phabricator: respect metadata sent by arc...
r33441 def getdiffmeta(diff):
"""get commit metadata (date, node, user, p1) from a diff object
The metadata could be "hg:meta", sent by phabsend, like:
"properties": {
"hg:meta": {
"date": "1499571514 25200",
"node": "98c08acae292b2faf60a279b4189beb6cff1414d",
"user": "Foo Bar <foo@example.com>",
"parent": "6d0abad76b30e4724a37ab8721d630394070fe16"
}
}
Or converted from "local:commits", sent by "arc", like:
"properties": {
"local:commits": {
"98c08acae292b2faf60a279b4189beb6cff1414d": {
"author": "Foo Bar",
"time": 1499546314,
"branch": "default",
"tag": "",
"commit": "98c08acae292b2faf60a279b4189beb6cff1414d",
"rev": "98c08acae292b2faf60a279b4189beb6cff1414d",
"local": "1000",
"parents": ["6d0abad76b30e4724a37ab8721d630394070fe16"],
"summary": "...",
"message": "...",
"authorEmail": "foo@example.com"
}
}
}
Note: metadata extracted from "local:commits" will lose time zone
information.
"""
props = diff.get(r'properties') or {}
meta = props.get(r'hg:meta')
if not meta and props.get(r'local:commits'):
commit = sorted(props[r'local:commits'].values())[0]
meta = {
r'date': r'%d 0' % commit[r'time'],
r'node': commit[r'rev'],
r'user': r'%s <%s>' % (commit[r'author'], commit[r'authorEmail']),
}
if len(commit.get(r'parents', ())) >= 1:
meta[r'parent'] = commit[r'parents'][0]
return meta or {}
Jun Wu
phabricator: rework phabread to reduce memory usage and round-trips...
r33267 def readpatch(repo, params, write, stack=False):
Jun Wu
phabricator: add phabread command to read patches...
r33197 """generate plain-text patch readable by 'hg import'
Jun Wu
phabricator: rework phabread to reduce memory usage and round-trips...
r33267 write is usually ui.write. params is passed to "differential.query". If
stack is True, also write dependent patches.
Jun Wu
phabricator: add phabread command to read patches...
r33197 """
# Differential Revisions
Jun Wu
phabricator: rework phabread to reduce memory usage and round-trips...
r33267 drevs = querydrev(repo, params, stack)
Jun Wu
phabricator: add phabread command to read patches...
r33197
Jun Wu
phabricator: rework phabread to reduce memory usage and round-trips...
r33267 # Prefetch hg:meta property for all diffs
diffids = sorted(set(max(int(v) for v in drev[r'diffs']) for drev in drevs))
diffs = callconduit(repo, 'differential.querydiffs', {'ids': diffids})
Jun Wu
phabricator: add phabread command to read patches...
r33197
Jun Wu
phabricator: rework phabread to reduce memory usage and round-trips...
r33267 # Generate patch for each drev
for drev in drevs:
repo.ui.note(_('reading D%s\n') % drev[r'id'])
Jun Wu
phabricator: add phabread command to read patches...
r33197
Jun Wu
phabricator: rework phabread to reduce memory usage and round-trips...
r33267 diffid = max(int(v) for v in drev[r'diffs'])
body = callconduit(repo, 'differential.getrawdiff', {'diffID': diffid})
Jun Wu
phabricator: avoid calling differential.getcommitmessage...
r33268 desc = getdescfromdrev(drev)
Jun Wu
phabricator: rework phabread to reduce memory usage and round-trips...
r33267 header = '# HG changeset patch\n'
# Try to preserve metadata from hg:meta property. Write hg patch
# headers that can be read by the "import" command. See patchheadermap
# and extract in mercurial/patch.py for supported headers.
Jun Wu
phabricator: respect metadata sent by arc...
r33441 meta = getdiffmeta(diffs[str(diffid)])
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
Jun Wu
phabricator: convert unicode to binary when writing patches...
r33602 content = '%s%s\n%s' % (header, desc, body)
write(encoding.unitolocal(content))
Jun Wu
phabricator: add phabread command to read patches...
r33197
@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)
Jun Wu
phabricator: rework phabread to reduce memory usage and round-trips...
r33267 readpatch(repo, {'ids': [revid]}, ui.write, opts.get('stack'))