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