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