##// END OF EJS Templates
phabricator: add --amend option to phabsend...
Jun Wu -
r33784:fa3aa6c9 default
parent child Browse files
Show More
@@ -1,631 +1,686 b''
1 1 # phabricator.py - simple Phabricator integration
2 2 #
3 3 # Copyright 2017 Facebook, Inc.
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7 """simple Phabricator integration
8 8
9 9 This extension provides a ``phabsend`` command which sends a stack of
10 10 changesets to Phabricator without amending commit messages, and a ``phabread``
11 11 command which prints a stack of revisions in a format suitable
12 12 for :hg:`import`.
13 13
14 14 By default, Phabricator requires ``Test Plan`` which might prevent some
15 15 changeset from being sent. The requirement could be disabled by changing
16 16 ``differential.require-test-plan-field`` config server side.
17 17
18 18 Config::
19 19
20 20 [phabricator]
21 21 # Phabricator URL
22 22 url = https://phab.example.com/
23 23
24 24 # API token. Get it from https://$HOST/conduit/login/
25 25 token = cli-xxxxxxxxxxxxxxxxxxxxxxxxxxxx
26 26
27 27 # Repo callsign. If a repo has a URL https://$HOST/diffusion/FOO, then its
28 28 # callsign is "FOO".
29 29 callsign = FOO
30 30
31 31 """
32 32
33 33 from __future__ import absolute_import
34 34
35 35 import json
36 36 import re
37 37
38 38 from mercurial.node import bin, nullid
39 39 from mercurial.i18n import _
40 40 from mercurial import (
41 cmdutil,
42 context,
41 43 encoding,
42 44 error,
43 45 mdiff,
44 46 obsutil,
45 47 patch,
46 48 registrar,
47 49 scmutil,
48 50 tags,
49 51 url as urlmod,
50 52 util,
51 53 )
52 54
53 55 cmdtable = {}
54 56 command = registrar.command(cmdtable)
55 57
56 58 def urlencodenested(params):
57 59 """like urlencode, but works with nested parameters.
58 60
59 61 For example, if params is {'a': ['b', 'c'], 'd': {'e': 'f'}}, it will be
60 62 flattened to {'a[0]': 'b', 'a[1]': 'c', 'd[e]': 'f'} and then passed to
61 63 urlencode. Note: the encoding is consistent with PHP's http_build_query.
62 64 """
63 65 flatparams = util.sortdict()
64 66 def process(prefix, obj):
65 67 items = {list: enumerate, dict: lambda x: x.items()}.get(type(obj))
66 68 if items is None:
67 69 flatparams[prefix] = obj
68 70 else:
69 71 for k, v in items(obj):
70 72 if prefix:
71 73 process('%s[%s]' % (prefix, k), v)
72 74 else:
73 75 process(k, v)
74 76 process('', params)
75 77 return util.urlreq.urlencode(flatparams)
76 78
77 79 def readurltoken(repo):
78 80 """return conduit url, token and make sure they exist
79 81
80 82 Currently read from [phabricator] config section. In the future, it might
81 83 make sense to read from .arcconfig and .arcrc as well.
82 84 """
83 85 values = []
84 86 section = 'phabricator'
85 87 for name in ['url', 'token']:
86 88 value = repo.ui.config(section, name)
87 89 if not value:
88 90 raise error.Abort(_('config %s.%s is required') % (section, name))
89 91 values.append(value)
90 92 return values
91 93
92 94 def callconduit(repo, name, params):
93 95 """call Conduit API, params is a dict. return json.loads result, or None"""
94 96 host, token = readurltoken(repo)
95 97 url, authinfo = util.url('/'.join([host, 'api', name])).authinfo()
96 98 urlopener = urlmod.opener(repo.ui, authinfo)
97 99 repo.ui.debug('Conduit Call: %s %s\n' % (url, params))
98 100 params = params.copy()
99 101 params['api.token'] = token
100 102 request = util.urlreq.request(url, data=urlencodenested(params))
101 103 body = urlopener.open(request).read()
102 104 repo.ui.debug('Conduit Response: %s\n' % body)
103 105 parsed = json.loads(body)
104 106 if parsed.get(r'error_code'):
105 107 msg = (_('Conduit Error (%s): %s')
106 108 % (parsed[r'error_code'], parsed[r'error_info']))
107 109 raise error.Abort(msg)
108 110 return parsed[r'result']
109 111
110 112 @command('debugcallconduit', [], _('METHOD'))
111 113 def debugcallconduit(ui, repo, name):
112 114 """call Conduit API
113 115
114 116 Call parameters are read from stdin as a JSON blob. Result will be written
115 117 to stdout as a JSON blob.
116 118 """
117 119 params = json.loads(ui.fin.read())
118 120 result = callconduit(repo, name, params)
119 121 s = json.dumps(result, sort_keys=True, indent=2, separators=(',', ': '))
120 122 ui.write('%s\n' % s)
121 123
122 124 def getrepophid(repo):
123 125 """given callsign, return repository PHID or None"""
124 126 # developer config: phabricator.repophid
125 127 repophid = repo.ui.config('phabricator', 'repophid')
126 128 if repophid:
127 129 return repophid
128 130 callsign = repo.ui.config('phabricator', 'callsign')
129 131 if not callsign:
130 132 return None
131 133 query = callconduit(repo, 'diffusion.repository.search',
132 134 {'constraints': {'callsigns': [callsign]}})
133 135 if len(query[r'data']) == 0:
134 136 return None
135 137 repophid = encoding.strtolocal(query[r'data'][0][r'phid'])
136 138 repo.ui.setconfig('phabricator', 'repophid', repophid)
137 139 return repophid
138 140
139 141 _differentialrevisiontagre = re.compile('\AD([1-9][0-9]*)\Z')
140 142 _differentialrevisiondescre = re.compile(
141 143 '^Differential Revision:\s*(?:.*)D([1-9][0-9]*)$', re.M)
142 144
143 145 def getoldnodedrevmap(repo, nodelist):
144 146 """find previous nodes that has been sent to Phabricator
145 147
146 148 return {node: (oldnode, Differential diff, Differential Revision ID)}
147 149 for node in nodelist with known previous sent versions, or associated
148 150 Differential Revision IDs. ``oldnode`` and ``Differential diff`` could
149 151 be ``None``.
150 152
151 153 Examines commit messages like "Differential Revision:" to get the
152 154 association information.
153 155
154 156 If such commit message line is not found, examines all precursors and their
155 157 tags. Tags with format like "D1234" are considered a match and the node
156 158 with that tag, and the number after "D" (ex. 1234) will be returned.
157 159
158 160 The ``old node``, if not None, is guaranteed to be the last diff of
159 161 corresponding Differential Revision, and exist in the repo.
160 162 """
161 163 url, token = readurltoken(repo)
162 164 unfi = repo.unfiltered()
163 165 nodemap = unfi.changelog.nodemap
164 166
165 167 result = {} # {node: (oldnode?, lastdiff?, drev)}
166 168 toconfirm = {} # {node: (force, {precnode}, drev)}
167 169 for node in nodelist:
168 170 ctx = unfi[node]
169 171 # For tags like "D123", put them into "toconfirm" to verify later
170 172 precnodes = list(obsutil.allpredecessors(unfi.obsstore, [node]))
171 173 for n in precnodes:
172 174 if n in nodemap:
173 175 for tag in unfi.nodetags(n):
174 176 m = _differentialrevisiontagre.match(tag)
175 177 if m:
176 178 toconfirm[node] = (0, set(precnodes), int(m.group(1)))
177 179 continue
178 180
179 181 # Check commit message
180 182 m = _differentialrevisiondescre.search(ctx.description())
181 183 if m:
182 184 toconfirm[node] = (1, set(precnodes), int(m.group(1)))
183 185
184 186 # Double check if tags are genuine by collecting all old nodes from
185 187 # Phabricator, and expect precursors overlap with it.
186 188 if toconfirm:
187 189 drevs = [drev for force, precs, drev in toconfirm.values()]
188 190 alldiffs = callconduit(unfi, 'differential.querydiffs',
189 191 {'revisionIDs': drevs})
190 192 getnode = lambda d: bin(encoding.unitolocal(
191 193 getdiffmeta(d).get(r'node', ''))) or None
192 194 for newnode, (force, precset, drev) in toconfirm.items():
193 195 diffs = [d for d in alldiffs.values()
194 196 if int(d[r'revisionID']) == drev]
195 197
196 198 # "precursors" as known by Phabricator
197 199 phprecset = set(getnode(d) for d in diffs)
198 200
199 201 # Ignore if precursors (Phabricator and local repo) do not overlap,
200 202 # and force is not set (when commit message says nothing)
201 203 if not force and not bool(phprecset & precset):
202 204 tagname = 'D%d' % drev
203 205 tags.tag(repo, tagname, nullid, message=None, user=None,
204 206 date=None, local=True)
205 207 unfi.ui.warn(_('D%s: local tag removed - does not match '
206 208 'Differential history\n') % drev)
207 209 continue
208 210
209 211 # Find the last node using Phabricator metadata, and make sure it
210 212 # exists in the repo
211 213 oldnode = lastdiff = None
212 214 if diffs:
213 215 lastdiff = max(diffs, key=lambda d: int(d[r'id']))
214 216 oldnode = getnode(lastdiff)
215 217 if oldnode and oldnode not in nodemap:
216 218 oldnode = None
217 219
218 220 result[newnode] = (oldnode, lastdiff, drev)
219 221
220 222 return result
221 223
222 224 def getdiff(ctx, diffopts):
223 225 """plain-text diff without header (user, commit message, etc)"""
224 226 output = util.stringio()
225 227 for chunk, _label in patch.diffui(ctx.repo(), ctx.p1().node(), ctx.node(),
226 228 None, opts=diffopts):
227 229 output.write(chunk)
228 230 return output.getvalue()
229 231
230 232 def creatediff(ctx):
231 233 """create a Differential Diff"""
232 234 repo = ctx.repo()
233 235 repophid = getrepophid(repo)
234 236 # Create a "Differential Diff" via "differential.createrawdiff" API
235 237 params = {'diff': getdiff(ctx, mdiff.diffopts(git=True, context=32767))}
236 238 if repophid:
237 239 params['repositoryPHID'] = repophid
238 240 diff = callconduit(repo, 'differential.createrawdiff', params)
239 241 if not diff:
240 242 raise error.Abort(_('cannot create diff for %s') % ctx)
241 243 return diff
242 244
243 245 def writediffproperties(ctx, diff):
244 246 """write metadata to diff so patches could be applied losslessly"""
245 247 params = {
246 248 'diff_id': diff[r'id'],
247 249 'name': 'hg:meta',
248 250 'data': json.dumps({
249 251 'user': ctx.user(),
250 252 'date': '%d %d' % ctx.date(),
251 253 'node': ctx.hex(),
252 254 'parent': ctx.p1().hex(),
253 255 }),
254 256 }
255 257 callconduit(ctx.repo(), 'differential.setdiffproperty', params)
256 258
257 259 def createdifferentialrevision(ctx, revid=None, parentrevid=None, oldnode=None,
258 260 olddiff=None, actions=None):
259 261 """create or update a Differential Revision
260 262
261 263 If revid is None, create a new Differential Revision, otherwise update
262 264 revid. If parentrevid is not None, set it as a dependency.
263 265
264 266 If oldnode is not None, check if the patch content (without commit message
265 267 and metadata) has changed before creating another diff.
266 268
267 269 If actions is not None, they will be appended to the transaction.
268 270 """
269 271 repo = ctx.repo()
270 272 if oldnode:
271 273 diffopts = mdiff.diffopts(git=True, context=1)
272 274 oldctx = repo.unfiltered()[oldnode]
273 275 neednewdiff = (getdiff(ctx, diffopts) != getdiff(oldctx, diffopts))
274 276 else:
275 277 neednewdiff = True
276 278
277 279 transactions = []
278 280 if neednewdiff:
279 281 diff = creatediff(ctx)
280 282 transactions.append({'type': 'update', 'value': diff[r'phid']})
281 283 else:
282 284 # Even if we don't need to upload a new diff because the patch content
283 285 # does not change. We might still need to update its metadata so
284 286 # pushers could know the correct node metadata.
285 287 assert olddiff
286 288 diff = olddiff
287 289 writediffproperties(ctx, diff)
288 290
289 291 # Use a temporary summary to set dependency. There might be better ways but
290 292 # I cannot find them for now. But do not do that if we are updating an
291 293 # existing revision (revid is not None) since that introduces visible
292 294 # churns (someone edited "Summary" twice) on the web page.
293 295 if parentrevid and revid is None:
294 296 summary = 'Depends on D%s' % parentrevid
295 297 transactions += [{'type': 'summary', 'value': summary},
296 298 {'type': 'summary', 'value': ' '}]
297 299
298 300 if actions:
299 301 transactions += actions
300 302
301 303 # Parse commit message and update related fields.
302 304 desc = ctx.description()
303 305 info = callconduit(repo, 'differential.parsecommitmessage',
304 306 {'corpus': desc})
305 307 for k, v in info[r'fields'].items():
306 308 if k in ['title', 'summary', 'testPlan']:
307 309 transactions.append({'type': k, 'value': v})
308 310
309 311 params = {'transactions': transactions}
310 312 if revid is not None:
311 313 # Update an existing Differential Revision
312 314 params['objectIdentifier'] = revid
313 315
314 316 revision = callconduit(repo, 'differential.revision.edit', params)
315 317 if not revision:
316 318 raise error.Abort(_('cannot create revision for %s') % ctx)
317 319
318 return revision
320 return revision, diff
319 321
320 322 def userphids(repo, names):
321 323 """convert user names to PHIDs"""
322 324 query = {'constraints': {'usernames': names}}
323 325 result = callconduit(repo, 'user.search', query)
324 326 # username not found is not an error of the API. So check if we have missed
325 327 # some names here.
326 328 data = result[r'data']
327 329 resolved = set(entry[r'fields'][r'username'] for entry in data)
328 330 unresolved = set(names) - resolved
329 331 if unresolved:
330 332 raise error.Abort(_('unknown username: %s')
331 333 % ' '.join(sorted(unresolved)))
332 334 return [entry[r'phid'] for entry in data]
333 335
334 336 @command('phabsend',
335 337 [('r', 'rev', [], _('revisions to send'), _('REV')),
338 ('', 'amend', False, _('update commit messages')),
336 339 ('', 'reviewer', [], _('specify reviewers')),
337 340 ('', 'confirm', None, _('ask for confirmation before sending'))],
338 341 _('REV [OPTIONS]'))
339 342 def phabsend(ui, repo, *revs, **opts):
340 343 """upload changesets to Phabricator
341 344
342 345 If there are multiple revisions specified, they will be send as a stack
343 346 with a linear dependencies relationship using the order specified by the
344 347 revset.
345 348
346 349 For the first time uploading changesets, local tags will be created to
347 350 maintain the association. After the first time, phabsend will check
348 351 obsstore and tags information so it can figure out whether to update an
349 352 existing Differential Revision, or create a new one.
350 353
354 If --amend is set, update commit messages so they have the
355 ``Differential Revision`` URL, remove related tags. This is similar to what
356 arcanist will do, and is more desired in author-push workflows. Otherwise,
357 use local tags to record the ``Differential Revision`` association.
358
351 359 The --confirm option lets you confirm changesets before sending them. You
352 360 can also add following to your configuration file to make it default
353 361 behaviour.
354 362
355 363 [phabsend]
356 364 confirm = true
365
366 phabsend will check obsstore and the above association to decide whether to
367 update an existing Differential Revision, or create a new one.
357 368 """
358 369 revs = list(revs) + opts.get('rev', [])
359 370 revs = scmutil.revrange(repo, revs)
360 371
361 372 if not revs:
362 373 raise error.Abort(_('phabsend requires at least one changeset'))
374 if opts.get('amend'):
375 cmdutil.checkunfinished(repo)
363 376
364 377 confirm = ui.configbool('phabsend', 'confirm')
365 378 confirm |= bool(opts.get('confirm'))
366 379 if confirm:
367 380 confirmed = _confirmbeforesend(repo, revs)
368 381 if not confirmed:
369 382 raise error.Abort(_('phabsend cancelled'))
370 383
371 384 actions = []
372 385 reviewers = opts.get('reviewer', [])
373 386 if reviewers:
374 387 phids = userphids(repo, reviewers)
375 388 actions.append({'type': 'reviewers.add', 'value': phids})
376 389
377 390 # {newnode: (oldnode, olddiff, olddrev}
378 391 oldmap = getoldnodedrevmap(repo, [repo[r].node() for r in revs])
379 392
393 drevids = [] # [int]
394 diffmap = {} # {newnode: diff}
395
380 396 # Send patches one by one so we know their Differential Revision IDs and
381 397 # can provide dependency relationship
382 398 lastrevid = None
383 399 for rev in revs:
384 400 ui.debug('sending rev %d\n' % rev)
385 401 ctx = repo[rev]
386 402
387 403 # Get Differential Revision ID
388 404 oldnode, olddiff, revid = oldmap.get(ctx.node(), (None, None, None))
389 if oldnode != ctx.node():
405 if oldnode != ctx.node() or opts.get('amend'):
390 406 # Create or update Differential Revision
391 revision = createdifferentialrevision(ctx, revid, lastrevid,
392 oldnode, olddiff, actions)
407 revision, diff = createdifferentialrevision(
408 ctx, revid, lastrevid, oldnode, olddiff, actions)
409 diffmap[ctx.node()] = diff
393 410 newrevid = int(revision[r'object'][r'id'])
394 411 if revid:
395 412 action = _('updated')
396 413 else:
397 414 action = _('created')
398 415
399 # Create a local tag to note the association
400 tagname = 'D%d' % newrevid
401 tags.tag(repo, tagname, ctx.node(), message=None, user=None,
402 date=None, local=True)
416 # Create a local tag to note the association, if commit message
417 # does not have it already
418 m = _differentialrevisiondescre.search(ctx.description())
419 if not m or int(m.group(1)) != newrevid:
420 tagname = 'D%d' % newrevid
421 tags.tag(repo, tagname, ctx.node(), message=None, user=None,
422 date=None, local=True)
403 423 else:
404 424 # Nothing changed. But still set "newrevid" so the next revision
405 425 # could depend on this one.
406 426 newrevid = revid
407 427 action = _('skipped')
408 428
409 429 ui.write(_('D%s: %s - %s: %s\n') % (newrevid, action, ctx,
410 430 ctx.description().split('\n')[0]))
431 drevids.append(newrevid)
411 432 lastrevid = newrevid
412 433
434 # Update commit messages and remove tags
435 if opts.get('amend'):
436 unfi = repo.unfiltered()
437 drevs = callconduit(repo, 'differential.query', {'ids': drevids})
438 with repo.wlock(), repo.lock(), repo.transaction('phabsend'):
439 wnode = unfi['.'].node()
440 mapping = {} # {oldnode: [newnode]}
441 for i, rev in enumerate(revs):
442 old = unfi[rev]
443 drevid = drevids[i]
444 drev = [d for d in drevs if int(d[r'id']) == drevid][0]
445 newdesc = getdescfromdrev(drev)
446 # Make sure commit message contain "Differential Revision"
447 if old.description() != newdesc:
448 parents = [
449 mapping.get(old.p1().node(), (old.p1(),))[0],
450 mapping.get(old.p2().node(), (old.p2(),))[0],
451 ]
452 new = context.metadataonlyctx(
453 repo, old, parents=parents, text=newdesc,
454 user=old.user(), date=old.date(), extra=old.extra())
455 newnode = new.commit()
456 mapping[old.node()] = [newnode]
457 # Update diff property
458 writediffproperties(unfi[newnode], diffmap[old.node()])
459 # Remove local tags since it's no longer necessary
460 tagname = 'D%d' % drevid
461 if tagname in repo.tags():
462 tags.tag(repo, tagname, nullid, message=None, user=None,
463 date=None, local=True)
464 scmutil.cleanupnodes(repo, mapping, 'phabsend')
465 if wnode in mapping:
466 unfi.setparents(mapping[wnode][0])
467
413 468 # Map from "hg:meta" keys to header understood by "hg import". The order is
414 469 # consistent with "hg export" output.
415 470 _metanamemap = util.sortdict([(r'user', 'User'), (r'date', 'Date'),
416 471 (r'node', 'Node ID'), (r'parent', 'Parent ')])
417 472
418 473 def _confirmbeforesend(repo, revs):
419 474 ui = repo.ui
420 475 for rev in revs:
421 476 ctx = repo[rev]
422 477 desc = ctx.description().splitlines()[0]
423 478 ui.write(('%d: ' % rev), label='phabsend.revnumber')
424 479 ui.write(('%s\n' % desc), label='phabsend.desc')
425 480
426 481 if ui.promptchoice(_('Phabsend the above changes (yn)?'
427 482 '$$ &Yes $$ &No')):
428 483 return False
429 484
430 485 return True
431 486
432 487 def querydrev(repo, params, stack=False):
433 488 """return a list of "Differential Revision" dicts
434 489
435 490 params is the input of "differential.query" API, and is expected to match
436 491 just a single Differential Revision.
437 492
438 493 A "Differential Revision dict" looks like:
439 494
440 495 {
441 496 "id": "2",
442 497 "phid": "PHID-DREV-672qvysjcczopag46qty",
443 498 "title": "example",
444 499 "uri": "https://phab.example.com/D2",
445 500 "dateCreated": "1499181406",
446 501 "dateModified": "1499182103",
447 502 "authorPHID": "PHID-USER-tv3ohwc4v4jeu34otlye",
448 503 "status": "0",
449 504 "statusName": "Needs Review",
450 505 "properties": [],
451 506 "branch": null,
452 507 "summary": "",
453 508 "testPlan": "",
454 509 "lineCount": "2",
455 510 "activeDiffPHID": "PHID-DIFF-xoqnjkobbm6k4dk6hi72",
456 511 "diffs": [
457 512 "3",
458 513 "4",
459 514 ],
460 515 "commits": [],
461 516 "reviewers": [],
462 517 "ccs": [],
463 518 "hashes": [],
464 519 "auxiliary": {
465 520 "phabricator:projects": [],
466 521 "phabricator:depends-on": [
467 522 "PHID-DREV-gbapp366kutjebt7agcd"
468 523 ]
469 524 },
470 525 "repositoryPHID": "PHID-REPO-hub2hx62ieuqeheznasv",
471 526 "sourcePath": null
472 527 }
473 528
474 529 If stack is True, return a list of "Differential Revision dict"s in an
475 530 order that the latter ones depend on the former ones. Otherwise, return a
476 531 list of a unique "Differential Revision dict".
477 532 """
478 533 prefetched = {} # {id or phid: drev}
479 534 def fetch(params):
480 535 """params -> single drev or None"""
481 536 key = (params.get(r'ids') or params.get(r'phids') or [None])[0]
482 537 if key in prefetched:
483 538 return prefetched[key]
484 539 # Otherwise, send the request. If we're fetching a stack, be smarter
485 540 # and fetch more ids in one batch, even if it could be unnecessary.
486 541 batchparams = params
487 542 if stack and len(params.get(r'ids', [])) == 1:
488 543 i = int(params[r'ids'][0])
489 544 # developer config: phabricator.batchsize
490 545 batchsize = repo.ui.configint('phabricator', 'batchsize', 12)
491 546 batchparams = {'ids': range(max(1, i - batchsize), i + 1)}
492 547 drevs = callconduit(repo, 'differential.query', batchparams)
493 548 # Fill prefetched with the result
494 549 for drev in drevs:
495 550 prefetched[drev[r'phid']] = drev
496 551 prefetched[int(drev[r'id'])] = drev
497 552 if key not in prefetched:
498 553 raise error.Abort(_('cannot get Differential Revision %r') % params)
499 554 return prefetched[key]
500 555
501 556 visited = set()
502 557 result = []
503 558 queue = [params]
504 559 while queue:
505 560 params = queue.pop()
506 561 drev = fetch(params)
507 562 if drev[r'id'] in visited:
508 563 continue
509 564 visited.add(drev[r'id'])
510 565 result.append(drev)
511 566 if stack:
512 567 auxiliary = drev.get(r'auxiliary', {})
513 568 depends = auxiliary.get(r'phabricator:depends-on', [])
514 569 for phid in depends:
515 570 queue.append({'phids': [phid]})
516 571 result.reverse()
517 572 return result
518 573
519 574 def getdescfromdrev(drev):
520 575 """get description (commit message) from "Differential Revision"
521 576
522 577 This is similar to differential.getcommitmessage API. But we only care
523 578 about limited fields: title, summary, test plan, and URL.
524 579 """
525 580 title = drev[r'title']
526 581 summary = drev[r'summary'].rstrip()
527 582 testplan = drev[r'testPlan'].rstrip()
528 583 if testplan:
529 584 testplan = 'Test Plan:\n%s' % testplan
530 585 uri = 'Differential Revision: %s' % drev[r'uri']
531 586 return '\n\n'.join(filter(None, [title, summary, testplan, uri]))
532 587
533 588 def getdiffmeta(diff):
534 589 """get commit metadata (date, node, user, p1) from a diff object
535 590
536 591 The metadata could be "hg:meta", sent by phabsend, like:
537 592
538 593 "properties": {
539 594 "hg:meta": {
540 595 "date": "1499571514 25200",
541 596 "node": "98c08acae292b2faf60a279b4189beb6cff1414d",
542 597 "user": "Foo Bar <foo@example.com>",
543 598 "parent": "6d0abad76b30e4724a37ab8721d630394070fe16"
544 599 }
545 600 }
546 601
547 602 Or converted from "local:commits", sent by "arc", like:
548 603
549 604 "properties": {
550 605 "local:commits": {
551 606 "98c08acae292b2faf60a279b4189beb6cff1414d": {
552 607 "author": "Foo Bar",
553 608 "time": 1499546314,
554 609 "branch": "default",
555 610 "tag": "",
556 611 "commit": "98c08acae292b2faf60a279b4189beb6cff1414d",
557 612 "rev": "98c08acae292b2faf60a279b4189beb6cff1414d",
558 613 "local": "1000",
559 614 "parents": ["6d0abad76b30e4724a37ab8721d630394070fe16"],
560 615 "summary": "...",
561 616 "message": "...",
562 617 "authorEmail": "foo@example.com"
563 618 }
564 619 }
565 620 }
566 621
567 622 Note: metadata extracted from "local:commits" will lose time zone
568 623 information.
569 624 """
570 625 props = diff.get(r'properties') or {}
571 626 meta = props.get(r'hg:meta')
572 627 if not meta and props.get(r'local:commits'):
573 628 commit = sorted(props[r'local:commits'].values())[0]
574 629 meta = {
575 630 r'date': r'%d 0' % commit[r'time'],
576 631 r'node': commit[r'rev'],
577 632 r'user': r'%s <%s>' % (commit[r'author'], commit[r'authorEmail']),
578 633 }
579 634 if len(commit.get(r'parents', ())) >= 1:
580 635 meta[r'parent'] = commit[r'parents'][0]
581 636 return meta or {}
582 637
583 638 def readpatch(repo, params, write, stack=False):
584 639 """generate plain-text patch readable by 'hg import'
585 640
586 641 write is usually ui.write. params is passed to "differential.query". If
587 642 stack is True, also write dependent patches.
588 643 """
589 644 # Differential Revisions
590 645 drevs = querydrev(repo, params, stack)
591 646
592 647 # Prefetch hg:meta property for all diffs
593 648 diffids = sorted(set(max(int(v) for v in drev[r'diffs']) for drev in drevs))
594 649 diffs = callconduit(repo, 'differential.querydiffs', {'ids': diffids})
595 650
596 651 # Generate patch for each drev
597 652 for drev in drevs:
598 653 repo.ui.note(_('reading D%s\n') % drev[r'id'])
599 654
600 655 diffid = max(int(v) for v in drev[r'diffs'])
601 656 body = callconduit(repo, 'differential.getrawdiff', {'diffID': diffid})
602 657 desc = getdescfromdrev(drev)
603 658 header = '# HG changeset patch\n'
604 659
605 660 # Try to preserve metadata from hg:meta property. Write hg patch
606 661 # headers that can be read by the "import" command. See patchheadermap
607 662 # and extract in mercurial/patch.py for supported headers.
608 663 meta = getdiffmeta(diffs[str(diffid)])
609 664 for k in _metanamemap.keys():
610 665 if k in meta:
611 666 header += '# %s %s\n' % (_metanamemap[k], meta[k])
612 667
613 668 content = '%s%s\n%s' % (header, desc, body)
614 669 write(encoding.unitolocal(content))
615 670
616 671 @command('phabread',
617 672 [('', 'stack', False, _('read dependencies'))],
618 673 _('REVID [OPTIONS]'))
619 674 def phabread(ui, repo, revid, **opts):
620 675 """print patches from Phabricator suitable for importing
621 676
622 677 REVID could be a Differential Revision identity, like ``D123``, or just the
623 678 number ``123``, or a full URL like ``https://phab.example.com/D123``.
624 679
625 680 If --stack is given, follow dependencies information and read all patches.
626 681 """
627 682 try:
628 683 revid = int(revid.split('/')[-1].replace('D', ''))
629 684 except ValueError:
630 685 raise error.Abort(_('invalid Revision ID: %s') % revid)
631 686 readpatch(repo, {'ids': [revid]}, ui.write, opts.get('stack'))
General Comments 0
You need to be logged in to leave comments. Login now