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