##// END OF EJS Templates
py3: pass a bytestring into querydrev instead of a string that'll TypeError...
Ian Moody -
r43220:6fb281f3 default
parent child Browse files
Show More
@@ -1,1094 +1,1094 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 (EXPERIMENTAL)
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 # Repo callsign. If a repo has a URL https://$HOST/diffusion/FOO, then its
25 25 # callsign is "FOO".
26 26 callsign = FOO
27 27
28 28 # curl command to use. If not set (default), use builtin HTTP library to
29 29 # communicate. If set, use the specified curl command. This could be useful
30 30 # if you need to specify advanced options that is not easily supported by
31 31 # the internal library.
32 32 curlcmd = curl --connect-timeout 2 --retry 3 --silent
33 33
34 34 [auth]
35 35 example.schemes = https
36 36 example.prefix = phab.example.com
37 37
38 38 # API token. Get it from https://$HOST/conduit/login/
39 39 example.phabtoken = cli-xxxxxxxxxxxxxxxxxxxxxxxxxxxx
40 40 """
41 41
42 42 from __future__ import absolute_import
43 43
44 44 import contextlib
45 45 import itertools
46 46 import json
47 47 import operator
48 48 import re
49 49
50 50 from mercurial.node import bin, nullid
51 51 from mercurial.i18n import _
52 52 from mercurial import (
53 53 cmdutil,
54 54 context,
55 55 encoding,
56 56 error,
57 57 httpconnection as httpconnectionmod,
58 58 mdiff,
59 59 obsutil,
60 60 parser,
61 61 patch,
62 62 phases,
63 63 pycompat,
64 64 registrar,
65 65 scmutil,
66 66 smartset,
67 67 tags,
68 68 templatefilters,
69 69 templateutil,
70 70 url as urlmod,
71 71 util,
72 72 )
73 73 from mercurial.utils import (
74 74 procutil,
75 75 stringutil,
76 76 )
77 77
78 78 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
79 79 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
80 80 # be specifying the version(s) of Mercurial they are tested with, or
81 81 # leave the attribute unspecified.
82 82 testedwith = 'ships-with-hg-core'
83 83
84 84 cmdtable = {}
85 85 command = registrar.command(cmdtable)
86 86
87 87 configtable = {}
88 88 configitem = registrar.configitem(configtable)
89 89
90 90 # developer config: phabricator.batchsize
91 91 configitem(b'phabricator', b'batchsize',
92 92 default=12,
93 93 )
94 94 configitem(b'phabricator', b'callsign',
95 95 default=None,
96 96 )
97 97 configitem(b'phabricator', b'curlcmd',
98 98 default=None,
99 99 )
100 100 # developer config: phabricator.repophid
101 101 configitem(b'phabricator', b'repophid',
102 102 default=None,
103 103 )
104 104 configitem(b'phabricator', b'url',
105 105 default=None,
106 106 )
107 107 configitem(b'phabsend', b'confirm',
108 108 default=False,
109 109 )
110 110
111 111 colortable = {
112 112 b'phabricator.action.created': b'green',
113 113 b'phabricator.action.skipped': b'magenta',
114 114 b'phabricator.action.updated': b'magenta',
115 115 b'phabricator.desc': b'',
116 116 b'phabricator.drev': b'bold',
117 117 b'phabricator.node': b'',
118 118 }
119 119
120 120 _VCR_FLAGS = [
121 121 (b'', b'test-vcr', b'',
122 122 _(b'Path to a vcr file. If nonexistent, will record a new vcr transcript'
123 123 b', otherwise will mock all http requests using the specified vcr file.'
124 124 b' (ADVANCED)'
125 125 )),
126 126 ]
127 127
128 128 def vcrcommand(name, flags, spec, helpcategory=None, optionalrepo=False):
129 129 fullflags = flags + _VCR_FLAGS
130 130 def hgmatcher(r1, r2):
131 131 if r1.uri != r2.uri or r1.method != r2.method:
132 132 return False
133 133 r1params = r1.body.split(b'&')
134 134 r2params = r2.body.split(b'&')
135 135 return set(r1params) == set(r2params)
136 136
137 137 def sanitiserequest(request):
138 138 request.body = re.sub(
139 139 r'cli-[a-z0-9]+',
140 140 r'cli-hahayouwish',
141 141 request.body
142 142 )
143 143 return request
144 144
145 145 def sanitiseresponse(response):
146 146 if r'set-cookie' in response[r'headers']:
147 147 del response[r'headers'][r'set-cookie']
148 148 return response
149 149
150 150 def decorate(fn):
151 151 def inner(*args, **kwargs):
152 152 cassette = pycompat.fsdecode(kwargs.pop(r'test_vcr', None))
153 153 if cassette:
154 154 import hgdemandimport
155 155 with hgdemandimport.deactivated():
156 156 import vcr as vcrmod
157 157 import vcr.stubs as stubs
158 158 vcr = vcrmod.VCR(
159 159 serializer=r'json',
160 160 before_record_request=sanitiserequest,
161 161 before_record_response=sanitiseresponse,
162 162 custom_patches=[
163 163 (urlmod, r'httpconnection',
164 164 stubs.VCRHTTPConnection),
165 165 (urlmod, r'httpsconnection',
166 166 stubs.VCRHTTPSConnection),
167 167 ])
168 168 vcr.register_matcher(r'hgmatcher', hgmatcher)
169 169 with vcr.use_cassette(cassette, match_on=[r'hgmatcher']):
170 170 return fn(*args, **kwargs)
171 171 return fn(*args, **kwargs)
172 172 inner.__name__ = fn.__name__
173 173 inner.__doc__ = fn.__doc__
174 174 return command(name, fullflags, spec, helpcategory=helpcategory,
175 175 optionalrepo=optionalrepo)(inner)
176 176 return decorate
177 177
178 178 def urlencodenested(params):
179 179 """like urlencode, but works with nested parameters.
180 180
181 181 For example, if params is {'a': ['b', 'c'], 'd': {'e': 'f'}}, it will be
182 182 flattened to {'a[0]': 'b', 'a[1]': 'c', 'd[e]': 'f'} and then passed to
183 183 urlencode. Note: the encoding is consistent with PHP's http_build_query.
184 184 """
185 185 flatparams = util.sortdict()
186 186 def process(prefix, obj):
187 187 if isinstance(obj, bool):
188 188 obj = {True: b'true', False: b'false'}[obj] # Python -> PHP form
189 189 lister = lambda l: [(b'%d' % k, v) for k, v in enumerate(l)]
190 190 items = {list: lister, dict: lambda x: x.items()}.get(type(obj))
191 191 if items is None:
192 192 flatparams[prefix] = obj
193 193 else:
194 194 for k, v in items(obj):
195 195 if prefix:
196 196 process(b'%s[%s]' % (prefix, k), v)
197 197 else:
198 198 process(k, v)
199 199 process(b'', params)
200 200 return util.urlreq.urlencode(flatparams)
201 201
202 202 def readurltoken(ui):
203 203 """return conduit url, token and make sure they exist
204 204
205 205 Currently read from [auth] config section. In the future, it might
206 206 make sense to read from .arcconfig and .arcrc as well.
207 207 """
208 208 url = ui.config(b'phabricator', b'url')
209 209 if not url:
210 210 raise error.Abort(_(b'config %s.%s is required')
211 211 % (b'phabricator', b'url'))
212 212
213 213 res = httpconnectionmod.readauthforuri(ui, url, util.url(url).user)
214 214 token = None
215 215
216 216 if res:
217 217 group, auth = res
218 218
219 219 ui.debug(b"using auth.%s.* for authentication\n" % group)
220 220
221 221 token = auth.get(b'phabtoken')
222 222
223 223 if not token:
224 224 raise error.Abort(_(b'Can\'t find conduit token associated to %s')
225 225 % (url,))
226 226
227 227 return url, token
228 228
229 229 def callconduit(ui, name, params):
230 230 """call Conduit API, params is a dict. return json.loads result, or None"""
231 231 host, token = readurltoken(ui)
232 232 url, authinfo = util.url(b'/'.join([host, b'api', name])).authinfo()
233 233 ui.debug(b'Conduit Call: %s %s\n' % (url, pycompat.byterepr(params)))
234 234 params = params.copy()
235 235 params[b'api.token'] = token
236 236 data = urlencodenested(params)
237 237 curlcmd = ui.config(b'phabricator', b'curlcmd')
238 238 if curlcmd:
239 239 sin, sout = procutil.popen2(b'%s -d @- %s'
240 240 % (curlcmd, procutil.shellquote(url)))
241 241 sin.write(data)
242 242 sin.close()
243 243 body = sout.read()
244 244 else:
245 245 urlopener = urlmod.opener(ui, authinfo)
246 246 request = util.urlreq.request(pycompat.strurl(url), data=data)
247 247 with contextlib.closing(urlopener.open(request)) as rsp:
248 248 body = rsp.read()
249 249 ui.debug(b'Conduit Response: %s\n' % body)
250 250 parsed = pycompat.rapply(
251 251 lambda x: encoding.unitolocal(x) if isinstance(x, pycompat.unicode)
252 252 else x,
253 253 json.loads(body)
254 254 )
255 255 if parsed.get(b'error_code'):
256 256 msg = (_(b'Conduit Error (%s): %s')
257 257 % (parsed[b'error_code'], parsed[b'error_info']))
258 258 raise error.Abort(msg)
259 259 return parsed[b'result']
260 260
261 261 @vcrcommand(b'debugcallconduit', [], _(b'METHOD'), optionalrepo=True)
262 262 def debugcallconduit(ui, repo, name):
263 263 """call Conduit API
264 264
265 265 Call parameters are read from stdin as a JSON blob. Result will be written
266 266 to stdout as a JSON blob.
267 267 """
268 268 # json.loads only accepts bytes from 3.6+
269 269 rawparams = encoding.unifromlocal(ui.fin.read())
270 270 # json.loads only returns unicode strings
271 271 params = pycompat.rapply(lambda x:
272 272 encoding.unitolocal(x) if isinstance(x, pycompat.unicode) else x,
273 273 json.loads(rawparams)
274 274 )
275 275 # json.dumps only accepts unicode strings
276 276 result = pycompat.rapply(lambda x:
277 277 encoding.unifromlocal(x) if isinstance(x, bytes) else x,
278 278 callconduit(ui, name, params)
279 279 )
280 280 s = json.dumps(result, sort_keys=True, indent=2, separators=(u',', u': '))
281 281 ui.write(b'%s\n' % encoding.unitolocal(s))
282 282
283 283 def getrepophid(repo):
284 284 """given callsign, return repository PHID or None"""
285 285 # developer config: phabricator.repophid
286 286 repophid = repo.ui.config(b'phabricator', b'repophid')
287 287 if repophid:
288 288 return repophid
289 289 callsign = repo.ui.config(b'phabricator', b'callsign')
290 290 if not callsign:
291 291 return None
292 292 query = callconduit(repo.ui, b'diffusion.repository.search',
293 293 {b'constraints': {b'callsigns': [callsign]}})
294 294 if len(query[b'data']) == 0:
295 295 return None
296 296 repophid = query[b'data'][0][b'phid']
297 297 repo.ui.setconfig(b'phabricator', b'repophid', repophid)
298 298 return repophid
299 299
300 300 _differentialrevisiontagre = re.compile(br'\AD([1-9][0-9]*)\Z')
301 301 _differentialrevisiondescre = re.compile(
302 302 br'^Differential Revision:\s*(?P<url>(?:.*)D(?P<id>[1-9][0-9]*))$', re.M)
303 303
304 304 def getoldnodedrevmap(repo, nodelist):
305 305 """find previous nodes that has been sent to Phabricator
306 306
307 307 return {node: (oldnode, Differential diff, Differential Revision ID)}
308 308 for node in nodelist with known previous sent versions, or associated
309 309 Differential Revision IDs. ``oldnode`` and ``Differential diff`` could
310 310 be ``None``.
311 311
312 312 Examines commit messages like "Differential Revision:" to get the
313 313 association information.
314 314
315 315 If such commit message line is not found, examines all precursors and their
316 316 tags. Tags with format like "D1234" are considered a match and the node
317 317 with that tag, and the number after "D" (ex. 1234) will be returned.
318 318
319 319 The ``old node``, if not None, is guaranteed to be the last diff of
320 320 corresponding Differential Revision, and exist in the repo.
321 321 """
322 322 unfi = repo.unfiltered()
323 323 nodemap = unfi.changelog.nodemap
324 324
325 325 result = {} # {node: (oldnode?, lastdiff?, drev)}
326 326 toconfirm = {} # {node: (force, {precnode}, drev)}
327 327 for node in nodelist:
328 328 ctx = unfi[node]
329 329 # For tags like "D123", put them into "toconfirm" to verify later
330 330 precnodes = list(obsutil.allpredecessors(unfi.obsstore, [node]))
331 331 for n in precnodes:
332 332 if n in nodemap:
333 333 for tag in unfi.nodetags(n):
334 334 m = _differentialrevisiontagre.match(tag)
335 335 if m:
336 336 toconfirm[node] = (0, set(precnodes), int(m.group(1)))
337 337 continue
338 338
339 339 # Check commit message
340 340 m = _differentialrevisiondescre.search(ctx.description())
341 341 if m:
342 342 toconfirm[node] = (1, set(precnodes), int(m.group(r'id')))
343 343
344 344 # Double check if tags are genuine by collecting all old nodes from
345 345 # Phabricator, and expect precursors overlap with it.
346 346 if toconfirm:
347 347 drevs = [drev for force, precs, drev in toconfirm.values()]
348 348 alldiffs = callconduit(unfi.ui, b'differential.querydiffs',
349 349 {b'revisionIDs': drevs})
350 350 getnode = lambda d: bin(
351 351 getdiffmeta(d).get(b'node', b'')) or None
352 352 for newnode, (force, precset, drev) in toconfirm.items():
353 353 diffs = [d for d in alldiffs.values()
354 354 if int(d[b'revisionID']) == drev]
355 355
356 356 # "precursors" as known by Phabricator
357 357 phprecset = set(getnode(d) for d in diffs)
358 358
359 359 # Ignore if precursors (Phabricator and local repo) do not overlap,
360 360 # and force is not set (when commit message says nothing)
361 361 if not force and not bool(phprecset & precset):
362 362 tagname = b'D%d' % drev
363 363 tags.tag(repo, tagname, nullid, message=None, user=None,
364 364 date=None, local=True)
365 365 unfi.ui.warn(_(b'D%s: local tag removed - does not match '
366 366 b'Differential history\n') % drev)
367 367 continue
368 368
369 369 # Find the last node using Phabricator metadata, and make sure it
370 370 # exists in the repo
371 371 oldnode = lastdiff = None
372 372 if diffs:
373 373 lastdiff = max(diffs, key=lambda d: int(d[b'id']))
374 374 oldnode = getnode(lastdiff)
375 375 if oldnode and oldnode not in nodemap:
376 376 oldnode = None
377 377
378 378 result[newnode] = (oldnode, lastdiff, drev)
379 379
380 380 return result
381 381
382 382 def getdiff(ctx, diffopts):
383 383 """plain-text diff without header (user, commit message, etc)"""
384 384 output = util.stringio()
385 385 for chunk, _label in patch.diffui(ctx.repo(), ctx.p1().node(), ctx.node(),
386 386 None, opts=diffopts):
387 387 output.write(chunk)
388 388 return output.getvalue()
389 389
390 390 def creatediff(ctx):
391 391 """create a Differential Diff"""
392 392 repo = ctx.repo()
393 393 repophid = getrepophid(repo)
394 394 # Create a "Differential Diff" via "differential.createrawdiff" API
395 395 params = {b'diff': getdiff(ctx, mdiff.diffopts(git=True, context=32767))}
396 396 if repophid:
397 397 params[b'repositoryPHID'] = repophid
398 398 diff = callconduit(repo.ui, b'differential.createrawdiff', params)
399 399 if not diff:
400 400 raise error.Abort(_(b'cannot create diff for %s') % ctx)
401 401 return diff
402 402
403 403 def writediffproperties(ctx, diff):
404 404 """write metadata to diff so patches could be applied losslessly"""
405 405 params = {
406 406 b'diff_id': diff[b'id'],
407 407 b'name': b'hg:meta',
408 408 b'data': templatefilters.json({
409 409 b'user': ctx.user(),
410 410 b'date': b'%d %d' % ctx.date(),
411 411 b'branch': ctx.branch(),
412 412 b'node': ctx.hex(),
413 413 b'parent': ctx.p1().hex(),
414 414 }),
415 415 }
416 416 callconduit(ctx.repo().ui, b'differential.setdiffproperty', params)
417 417
418 418 params = {
419 419 b'diff_id': diff[b'id'],
420 420 b'name': b'local:commits',
421 421 b'data': templatefilters.json({
422 422 ctx.hex(): {
423 423 b'author': stringutil.person(ctx.user()),
424 424 b'authorEmail': stringutil.email(ctx.user()),
425 425 b'time': int(ctx.date()[0]),
426 426 b'commit': ctx.hex(),
427 427 b'parents': [ctx.p1().hex()],
428 428 b'branch': ctx.branch(),
429 429 },
430 430 }),
431 431 }
432 432 callconduit(ctx.repo().ui, b'differential.setdiffproperty', params)
433 433
434 434 def createdifferentialrevision(ctx, revid=None, parentrevphid=None,
435 435 oldnode=None, olddiff=None, actions=None,
436 436 comment=None):
437 437 """create or update a Differential Revision
438 438
439 439 If revid is None, create a new Differential Revision, otherwise update
440 440 revid. If parentrevphid is not None, set it as a dependency.
441 441
442 442 If oldnode is not None, check if the patch content (without commit message
443 443 and metadata) has changed before creating another diff.
444 444
445 445 If actions is not None, they will be appended to the transaction.
446 446 """
447 447 repo = ctx.repo()
448 448 if oldnode:
449 449 diffopts = mdiff.diffopts(git=True, context=32767)
450 450 oldctx = repo.unfiltered()[oldnode]
451 451 neednewdiff = (getdiff(ctx, diffopts) != getdiff(oldctx, diffopts))
452 452 else:
453 453 neednewdiff = True
454 454
455 455 transactions = []
456 456 if neednewdiff:
457 457 diff = creatediff(ctx)
458 458 transactions.append({b'type': b'update', b'value': diff[b'phid']})
459 459 if comment:
460 460 transactions.append({b'type': b'comment', b'value': comment})
461 461 else:
462 462 # Even if we don't need to upload a new diff because the patch content
463 463 # does not change. We might still need to update its metadata so
464 464 # pushers could know the correct node metadata.
465 465 assert olddiff
466 466 diff = olddiff
467 467 writediffproperties(ctx, diff)
468 468
469 469 # Set the parent Revision every time, so commit re-ordering is picked-up
470 470 if parentrevphid:
471 471 transactions.append({b'type': b'parents.set',
472 472 b'value': [parentrevphid]})
473 473
474 474 if actions:
475 475 transactions += actions
476 476
477 477 # Parse commit message and update related fields.
478 478 desc = ctx.description()
479 479 info = callconduit(repo.ui, b'differential.parsecommitmessage',
480 480 {b'corpus': desc})
481 481 for k, v in info[b'fields'].items():
482 482 if k in [b'title', b'summary', b'testPlan']:
483 483 transactions.append({b'type': k, b'value': v})
484 484
485 485 params = {b'transactions': transactions}
486 486 if revid is not None:
487 487 # Update an existing Differential Revision
488 488 params[b'objectIdentifier'] = revid
489 489
490 490 revision = callconduit(repo.ui, b'differential.revision.edit', params)
491 491 if not revision:
492 492 raise error.Abort(_(b'cannot create revision for %s') % ctx)
493 493
494 494 return revision, diff
495 495
496 496 def userphids(repo, names):
497 497 """convert user names to PHIDs"""
498 498 names = [name.lower() for name in names]
499 499 query = {b'constraints': {b'usernames': names}}
500 500 result = callconduit(repo.ui, b'user.search', query)
501 501 # username not found is not an error of the API. So check if we have missed
502 502 # some names here.
503 503 data = result[b'data']
504 504 resolved = set(entry[b'fields'][b'username'].lower() for entry in data)
505 505 unresolved = set(names) - resolved
506 506 if unresolved:
507 507 raise error.Abort(_(b'unknown username: %s')
508 508 % b' '.join(sorted(unresolved)))
509 509 return [entry[b'phid'] for entry in data]
510 510
511 511 @vcrcommand(b'phabsend',
512 512 [(b'r', b'rev', [], _(b'revisions to send'), _(b'REV')),
513 513 (b'', b'amend', True, _(b'update commit messages')),
514 514 (b'', b'reviewer', [], _(b'specify reviewers')),
515 515 (b'', b'blocker', [], _(b'specify blocking reviewers')),
516 516 (b'm', b'comment', b'',
517 517 _(b'add a comment to Revisions with new/updated Diffs')),
518 518 (b'', b'confirm', None, _(b'ask for confirmation before sending'))],
519 519 _(b'REV [OPTIONS]'),
520 520 helpcategory=command.CATEGORY_IMPORT_EXPORT)
521 521 def phabsend(ui, repo, *revs, **opts):
522 522 """upload changesets to Phabricator
523 523
524 524 If there are multiple revisions specified, they will be send as a stack
525 525 with a linear dependencies relationship using the order specified by the
526 526 revset.
527 527
528 528 For the first time uploading changesets, local tags will be created to
529 529 maintain the association. After the first time, phabsend will check
530 530 obsstore and tags information so it can figure out whether to update an
531 531 existing Differential Revision, or create a new one.
532 532
533 533 If --amend is set, update commit messages so they have the
534 534 ``Differential Revision`` URL, remove related tags. This is similar to what
535 535 arcanist will do, and is more desired in author-push workflows. Otherwise,
536 536 use local tags to record the ``Differential Revision`` association.
537 537
538 538 The --confirm option lets you confirm changesets before sending them. You
539 539 can also add following to your configuration file to make it default
540 540 behaviour::
541 541
542 542 [phabsend]
543 543 confirm = true
544 544
545 545 phabsend will check obsstore and the above association to decide whether to
546 546 update an existing Differential Revision, or create a new one.
547 547 """
548 548 opts = pycompat.byteskwargs(opts)
549 549 revs = list(revs) + opts.get(b'rev', [])
550 550 revs = scmutil.revrange(repo, revs)
551 551
552 552 if not revs:
553 553 raise error.Abort(_(b'phabsend requires at least one changeset'))
554 554 if opts.get(b'amend'):
555 555 cmdutil.checkunfinished(repo)
556 556
557 557 # {newnode: (oldnode, olddiff, olddrev}
558 558 oldmap = getoldnodedrevmap(repo, [repo[r].node() for r in revs])
559 559
560 560 confirm = ui.configbool(b'phabsend', b'confirm')
561 561 confirm |= bool(opts.get(b'confirm'))
562 562 if confirm:
563 563 confirmed = _confirmbeforesend(repo, revs, oldmap)
564 564 if not confirmed:
565 565 raise error.Abort(_(b'phabsend cancelled'))
566 566
567 567 actions = []
568 568 reviewers = opts.get(b'reviewer', [])
569 569 blockers = opts.get(b'blocker', [])
570 570 phids = []
571 571 if reviewers:
572 572 phids.extend(userphids(repo, reviewers))
573 573 if blockers:
574 574 phids.extend(map(
575 575 lambda phid: b'blocking(%s)' % phid, userphids(repo, blockers)
576 576 ))
577 577 if phids:
578 578 actions.append({b'type': b'reviewers.add', b'value': phids})
579 579
580 580 drevids = [] # [int]
581 581 diffmap = {} # {newnode: diff}
582 582
583 583 # Send patches one by one so we know their Differential Revision PHIDs and
584 584 # can provide dependency relationship
585 585 lastrevphid = None
586 586 for rev in revs:
587 587 ui.debug(b'sending rev %d\n' % rev)
588 588 ctx = repo[rev]
589 589
590 590 # Get Differential Revision ID
591 591 oldnode, olddiff, revid = oldmap.get(ctx.node(), (None, None, None))
592 592 if oldnode != ctx.node() or opts.get(b'amend'):
593 593 # Create or update Differential Revision
594 594 revision, diff = createdifferentialrevision(
595 595 ctx, revid, lastrevphid, oldnode, olddiff, actions,
596 596 opts.get(b'comment'))
597 597 diffmap[ctx.node()] = diff
598 598 newrevid = int(revision[b'object'][b'id'])
599 599 newrevphid = revision[b'object'][b'phid']
600 600 if revid:
601 601 action = b'updated'
602 602 else:
603 603 action = b'created'
604 604
605 605 # Create a local tag to note the association, if commit message
606 606 # does not have it already
607 607 m = _differentialrevisiondescre.search(ctx.description())
608 608 if not m or int(m.group(r'id')) != newrevid:
609 609 tagname = b'D%d' % newrevid
610 610 tags.tag(repo, tagname, ctx.node(), message=None, user=None,
611 611 date=None, local=True)
612 612 else:
613 613 # Nothing changed. But still set "newrevphid" so the next revision
614 614 # could depend on this one and "newrevid" for the summary line.
615 newrevphid = querydrev(repo, str(revid))[0][b'phid']
615 newrevphid = querydrev(repo, b'%d' % revid)[0][b'phid']
616 616 newrevid = revid
617 617 action = b'skipped'
618 618
619 619 actiondesc = ui.label(
620 620 {b'created': _(b'created'),
621 621 b'skipped': _(b'skipped'),
622 622 b'updated': _(b'updated')}[action],
623 623 b'phabricator.action.%s' % action)
624 624 drevdesc = ui.label(b'D%d' % newrevid, b'phabricator.drev')
625 625 nodedesc = ui.label(bytes(ctx), b'phabricator.node')
626 626 desc = ui.label(ctx.description().split(b'\n')[0], b'phabricator.desc')
627 627 ui.write(_(b'%s - %s - %s: %s\n') % (drevdesc, actiondesc, nodedesc,
628 628 desc))
629 629 drevids.append(newrevid)
630 630 lastrevphid = newrevphid
631 631
632 632 # Update commit messages and remove tags
633 633 if opts.get(b'amend'):
634 634 unfi = repo.unfiltered()
635 635 drevs = callconduit(ui, b'differential.query', {b'ids': drevids})
636 636 with repo.wlock(), repo.lock(), repo.transaction(b'phabsend'):
637 637 wnode = unfi[b'.'].node()
638 638 mapping = {} # {oldnode: [newnode]}
639 639 for i, rev in enumerate(revs):
640 640 old = unfi[rev]
641 641 drevid = drevids[i]
642 642 drev = [d for d in drevs if int(d[b'id']) == drevid][0]
643 643 newdesc = getdescfromdrev(drev)
644 644 # Make sure commit message contain "Differential Revision"
645 645 if old.description() != newdesc:
646 646 if old.phase() == phases.public:
647 647 ui.warn(_("warning: not updating public commit %s\n")
648 648 % scmutil.formatchangeid(old))
649 649 continue
650 650 parents = [
651 651 mapping.get(old.p1().node(), (old.p1(),))[0],
652 652 mapping.get(old.p2().node(), (old.p2(),))[0],
653 653 ]
654 654 new = context.metadataonlyctx(
655 655 repo, old, parents=parents, text=newdesc,
656 656 user=old.user(), date=old.date(), extra=old.extra())
657 657
658 658 newnode = new.commit()
659 659
660 660 mapping[old.node()] = [newnode]
661 661 # Update diff property
662 662 # If it fails just warn and keep going, otherwise the DREV
663 663 # associations will be lost
664 664 try:
665 665 writediffproperties(unfi[newnode], diffmap[old.node()])
666 666 except util.urlerr.urlerror:
667 667 ui.warn(b'Failed to update metadata for D%s\n' % drevid)
668 668 # Remove local tags since it's no longer necessary
669 669 tagname = b'D%d' % drevid
670 670 if tagname in repo.tags():
671 671 tags.tag(repo, tagname, nullid, message=None, user=None,
672 672 date=None, local=True)
673 673 scmutil.cleanupnodes(repo, mapping, b'phabsend', fixphase=True)
674 674 if wnode in mapping:
675 675 unfi.setparents(mapping[wnode][0])
676 676
677 677 # Map from "hg:meta" keys to header understood by "hg import". The order is
678 678 # consistent with "hg export" output.
679 679 _metanamemap = util.sortdict([(b'user', b'User'), (b'date', b'Date'),
680 680 (b'branch', b'Branch'), (b'node', b'Node ID'),
681 681 (b'parent', b'Parent ')])
682 682
683 683 def _confirmbeforesend(repo, revs, oldmap):
684 684 url, token = readurltoken(repo.ui)
685 685 ui = repo.ui
686 686 for rev in revs:
687 687 ctx = repo[rev]
688 688 desc = ctx.description().splitlines()[0]
689 689 oldnode, olddiff, drevid = oldmap.get(ctx.node(), (None, None, None))
690 690 if drevid:
691 691 drevdesc = ui.label(b'D%s' % drevid, b'phabricator.drev')
692 692 else:
693 693 drevdesc = ui.label(_(b'NEW'), b'phabricator.drev')
694 694
695 695 ui.write(_(b'%s - %s: %s\n')
696 696 % (drevdesc,
697 697 ui.label(bytes(ctx), b'phabricator.node'),
698 698 ui.label(desc, b'phabricator.desc')))
699 699
700 700 if ui.promptchoice(_(b'Send the above changes to %s (yn)?'
701 701 b'$$ &Yes $$ &No') % url):
702 702 return False
703 703
704 704 return True
705 705
706 706 _knownstatusnames = {b'accepted', b'needsreview', b'needsrevision', b'closed',
707 707 b'abandoned'}
708 708
709 709 def _getstatusname(drev):
710 710 """get normalized status name from a Differential Revision"""
711 711 return drev[b'statusName'].replace(b' ', b'').lower()
712 712
713 713 # Small language to specify differential revisions. Support symbols: (), :X,
714 714 # +, and -.
715 715
716 716 _elements = {
717 717 # token-type: binding-strength, primary, prefix, infix, suffix
718 718 b'(': (12, None, (b'group', 1, b')'), None, None),
719 719 b':': (8, None, (b'ancestors', 8), None, None),
720 720 b'&': (5, None, None, (b'and_', 5), None),
721 721 b'+': (4, None, None, (b'add', 4), None),
722 722 b'-': (4, None, None, (b'sub', 4), None),
723 723 b')': (0, None, None, None, None),
724 724 b'symbol': (0, b'symbol', None, None, None),
725 725 b'end': (0, None, None, None, None),
726 726 }
727 727
728 728 def _tokenize(text):
729 729 view = memoryview(text) # zero-copy slice
730 730 special = b'():+-& '
731 731 pos = 0
732 732 length = len(text)
733 733 while pos < length:
734 734 symbol = b''.join(itertools.takewhile(lambda ch: ch not in special,
735 735 pycompat.iterbytestr(view[pos:])))
736 736 if symbol:
737 737 yield (b'symbol', symbol, pos)
738 738 pos += len(symbol)
739 739 else: # special char, ignore space
740 740 if text[pos] != b' ':
741 741 yield (text[pos], None, pos)
742 742 pos += 1
743 743 yield (b'end', None, pos)
744 744
745 745 def _parse(text):
746 746 tree, pos = parser.parser(_elements).parse(_tokenize(text))
747 747 if pos != len(text):
748 748 raise error.ParseError(b'invalid token', pos)
749 749 return tree
750 750
751 751 def _parsedrev(symbol):
752 752 """str -> int or None, ex. 'D45' -> 45; '12' -> 12; 'x' -> None"""
753 753 if symbol.startswith(b'D') and symbol[1:].isdigit():
754 754 return int(symbol[1:])
755 755 if symbol.isdigit():
756 756 return int(symbol)
757 757
758 758 def _prefetchdrevs(tree):
759 759 """return ({single-drev-id}, {ancestor-drev-id}) to prefetch"""
760 760 drevs = set()
761 761 ancestordrevs = set()
762 762 op = tree[0]
763 763 if op == b'symbol':
764 764 r = _parsedrev(tree[1])
765 765 if r:
766 766 drevs.add(r)
767 767 elif op == b'ancestors':
768 768 r, a = _prefetchdrevs(tree[1])
769 769 drevs.update(r)
770 770 ancestordrevs.update(r)
771 771 ancestordrevs.update(a)
772 772 else:
773 773 for t in tree[1:]:
774 774 r, a = _prefetchdrevs(t)
775 775 drevs.update(r)
776 776 ancestordrevs.update(a)
777 777 return drevs, ancestordrevs
778 778
779 779 def querydrev(repo, spec):
780 780 """return a list of "Differential Revision" dicts
781 781
782 782 spec is a string using a simple query language, see docstring in phabread
783 783 for details.
784 784
785 785 A "Differential Revision dict" looks like:
786 786
787 787 {
788 788 "id": "2",
789 789 "phid": "PHID-DREV-672qvysjcczopag46qty",
790 790 "title": "example",
791 791 "uri": "https://phab.example.com/D2",
792 792 "dateCreated": "1499181406",
793 793 "dateModified": "1499182103",
794 794 "authorPHID": "PHID-USER-tv3ohwc4v4jeu34otlye",
795 795 "status": "0",
796 796 "statusName": "Needs Review",
797 797 "properties": [],
798 798 "branch": null,
799 799 "summary": "",
800 800 "testPlan": "",
801 801 "lineCount": "2",
802 802 "activeDiffPHID": "PHID-DIFF-xoqnjkobbm6k4dk6hi72",
803 803 "diffs": [
804 804 "3",
805 805 "4",
806 806 ],
807 807 "commits": [],
808 808 "reviewers": [],
809 809 "ccs": [],
810 810 "hashes": [],
811 811 "auxiliary": {
812 812 "phabricator:projects": [],
813 813 "phabricator:depends-on": [
814 814 "PHID-DREV-gbapp366kutjebt7agcd"
815 815 ]
816 816 },
817 817 "repositoryPHID": "PHID-REPO-hub2hx62ieuqeheznasv",
818 818 "sourcePath": null
819 819 }
820 820 """
821 821 def fetch(params):
822 822 """params -> single drev or None"""
823 823 key = (params.get(b'ids') or params.get(b'phids') or [None])[0]
824 824 if key in prefetched:
825 825 return prefetched[key]
826 826 drevs = callconduit(repo.ui, b'differential.query', params)
827 827 # Fill prefetched with the result
828 828 for drev in drevs:
829 829 prefetched[drev[b'phid']] = drev
830 830 prefetched[int(drev[b'id'])] = drev
831 831 if key not in prefetched:
832 832 raise error.Abort(_(b'cannot get Differential Revision %r')
833 833 % params)
834 834 return prefetched[key]
835 835
836 836 def getstack(topdrevids):
837 837 """given a top, get a stack from the bottom, [id] -> [id]"""
838 838 visited = set()
839 839 result = []
840 840 queue = [{b'ids': [i]} for i in topdrevids]
841 841 while queue:
842 842 params = queue.pop()
843 843 drev = fetch(params)
844 844 if drev[b'id'] in visited:
845 845 continue
846 846 visited.add(drev[b'id'])
847 847 result.append(int(drev[b'id']))
848 848 auxiliary = drev.get(b'auxiliary', {})
849 849 depends = auxiliary.get(b'phabricator:depends-on', [])
850 850 for phid in depends:
851 851 queue.append({b'phids': [phid]})
852 852 result.reverse()
853 853 return smartset.baseset(result)
854 854
855 855 # Initialize prefetch cache
856 856 prefetched = {} # {id or phid: drev}
857 857
858 858 tree = _parse(spec)
859 859 drevs, ancestordrevs = _prefetchdrevs(tree)
860 860
861 861 # developer config: phabricator.batchsize
862 862 batchsize = repo.ui.configint(b'phabricator', b'batchsize')
863 863
864 864 # Prefetch Differential Revisions in batch
865 865 tofetch = set(drevs)
866 866 for r in ancestordrevs:
867 867 tofetch.update(range(max(1, r - batchsize), r + 1))
868 868 if drevs:
869 869 fetch({b'ids': list(tofetch)})
870 870 validids = sorted(set(getstack(list(ancestordrevs))) | set(drevs))
871 871
872 872 # Walk through the tree, return smartsets
873 873 def walk(tree):
874 874 op = tree[0]
875 875 if op == b'symbol':
876 876 drev = _parsedrev(tree[1])
877 877 if drev:
878 878 return smartset.baseset([drev])
879 879 elif tree[1] in _knownstatusnames:
880 880 drevs = [r for r in validids
881 881 if _getstatusname(prefetched[r]) == tree[1]]
882 882 return smartset.baseset(drevs)
883 883 else:
884 884 raise error.Abort(_(b'unknown symbol: %s') % tree[1])
885 885 elif op in {b'and_', b'add', b'sub'}:
886 886 assert len(tree) == 3
887 887 return getattr(operator, op)(walk(tree[1]), walk(tree[2]))
888 888 elif op == b'group':
889 889 return walk(tree[1])
890 890 elif op == b'ancestors':
891 891 return getstack(walk(tree[1]))
892 892 else:
893 893 raise error.ProgrammingError(b'illegal tree: %r' % tree)
894 894
895 895 return [prefetched[r] for r in walk(tree)]
896 896
897 897 def getdescfromdrev(drev):
898 898 """get description (commit message) from "Differential Revision"
899 899
900 900 This is similar to differential.getcommitmessage API. But we only care
901 901 about limited fields: title, summary, test plan, and URL.
902 902 """
903 903 title = drev[b'title']
904 904 summary = drev[b'summary'].rstrip()
905 905 testplan = drev[b'testPlan'].rstrip()
906 906 if testplan:
907 907 testplan = b'Test Plan:\n%s' % testplan
908 908 uri = b'Differential Revision: %s' % drev[b'uri']
909 909 return b'\n\n'.join(filter(None, [title, summary, testplan, uri]))
910 910
911 911 def getdiffmeta(diff):
912 912 """get commit metadata (date, node, user, p1) from a diff object
913 913
914 914 The metadata could be "hg:meta", sent by phabsend, like:
915 915
916 916 "properties": {
917 917 "hg:meta": {
918 918 "date": "1499571514 25200",
919 919 "node": "98c08acae292b2faf60a279b4189beb6cff1414d",
920 920 "user": "Foo Bar <foo@example.com>",
921 921 "parent": "6d0abad76b30e4724a37ab8721d630394070fe16"
922 922 }
923 923 }
924 924
925 925 Or converted from "local:commits", sent by "arc", like:
926 926
927 927 "properties": {
928 928 "local:commits": {
929 929 "98c08acae292b2faf60a279b4189beb6cff1414d": {
930 930 "author": "Foo Bar",
931 931 "time": 1499546314,
932 932 "branch": "default",
933 933 "tag": "",
934 934 "commit": "98c08acae292b2faf60a279b4189beb6cff1414d",
935 935 "rev": "98c08acae292b2faf60a279b4189beb6cff1414d",
936 936 "local": "1000",
937 937 "parents": ["6d0abad76b30e4724a37ab8721d630394070fe16"],
938 938 "summary": "...",
939 939 "message": "...",
940 940 "authorEmail": "foo@example.com"
941 941 }
942 942 }
943 943 }
944 944
945 945 Note: metadata extracted from "local:commits" will lose time zone
946 946 information.
947 947 """
948 948 props = diff.get(b'properties') or {}
949 949 meta = props.get(b'hg:meta')
950 950 if not meta:
951 951 if props.get(b'local:commits'):
952 952 commit = sorted(props[b'local:commits'].values())[0]
953 953 meta = {}
954 954 if b'author' in commit and b'authorEmail' in commit:
955 955 meta[b'user'] = b'%s <%s>' % (commit[b'author'],
956 956 commit[b'authorEmail'])
957 957 if b'time' in commit:
958 958 meta[b'date'] = b'%d 0' % int(commit[b'time'])
959 959 if b'branch' in commit:
960 960 meta[b'branch'] = commit[b'branch']
961 961 node = commit.get(b'commit', commit.get(b'rev'))
962 962 if node:
963 963 meta[b'node'] = node
964 964 if len(commit.get(b'parents', ())) >= 1:
965 965 meta[b'parent'] = commit[b'parents'][0]
966 966 else:
967 967 meta = {}
968 968 if b'date' not in meta and b'dateCreated' in diff:
969 969 meta[b'date'] = b'%s 0' % diff[b'dateCreated']
970 970 if b'branch' not in meta and diff.get(b'branch'):
971 971 meta[b'branch'] = diff[b'branch']
972 972 if b'parent' not in meta and diff.get(b'sourceControlBaseRevision'):
973 973 meta[b'parent'] = diff[b'sourceControlBaseRevision']
974 974 return meta
975 975
976 976 def readpatch(repo, drevs, write):
977 977 """generate plain-text patch readable by 'hg import'
978 978
979 979 write is usually ui.write. drevs is what "querydrev" returns, results of
980 980 "differential.query".
981 981 """
982 982 # Prefetch hg:meta property for all diffs
983 983 diffids = sorted(set(max(int(v) for v in drev[b'diffs']) for drev in drevs))
984 984 diffs = callconduit(repo.ui, b'differential.querydiffs', {b'ids': diffids})
985 985
986 986 # Generate patch for each drev
987 987 for drev in drevs:
988 988 repo.ui.note(_(b'reading D%s\n') % drev[b'id'])
989 989
990 990 diffid = max(int(v) for v in drev[b'diffs'])
991 991 body = callconduit(repo.ui, b'differential.getrawdiff',
992 992 {b'diffID': diffid})
993 993 desc = getdescfromdrev(drev)
994 994 header = b'# HG changeset patch\n'
995 995
996 996 # Try to preserve metadata from hg:meta property. Write hg patch
997 997 # headers that can be read by the "import" command. See patchheadermap
998 998 # and extract in mercurial/patch.py for supported headers.
999 999 meta = getdiffmeta(diffs[b'%d' % diffid])
1000 1000 for k in _metanamemap.keys():
1001 1001 if k in meta:
1002 1002 header += b'# %s %s\n' % (_metanamemap[k], meta[k])
1003 1003
1004 1004 content = b'%s%s\n%s' % (header, desc, body)
1005 1005 write(content)
1006 1006
1007 1007 @vcrcommand(b'phabread',
1008 1008 [(b'', b'stack', False, _(b'read dependencies'))],
1009 1009 _(b'DREVSPEC [OPTIONS]'),
1010 1010 helpcategory=command.CATEGORY_IMPORT_EXPORT)
1011 1011 def phabread(ui, repo, spec, **opts):
1012 1012 """print patches from Phabricator suitable for importing
1013 1013
1014 1014 DREVSPEC could be a Differential Revision identity, like ``D123``, or just
1015 1015 the number ``123``. It could also have common operators like ``+``, ``-``,
1016 1016 ``&``, ``(``, ``)`` for complex queries. Prefix ``:`` could be used to
1017 1017 select a stack.
1018 1018
1019 1019 ``abandoned``, ``accepted``, ``closed``, ``needsreview``, ``needsrevision``
1020 1020 could be used to filter patches by status. For performance reason, they
1021 1021 only represent a subset of non-status selections and cannot be used alone.
1022 1022
1023 1023 For example, ``:D6+8-(2+D4)`` selects a stack up to D6, plus D8 and exclude
1024 1024 D2 and D4. ``:D9 & needsreview`` selects "Needs Review" revisions in a
1025 1025 stack up to D9.
1026 1026
1027 1027 If --stack is given, follow dependencies information and read all patches.
1028 1028 It is equivalent to the ``:`` operator.
1029 1029 """
1030 1030 opts = pycompat.byteskwargs(opts)
1031 1031 if opts.get(b'stack'):
1032 1032 spec = b':(%s)' % spec
1033 1033 drevs = querydrev(repo, spec)
1034 1034 readpatch(repo, drevs, ui.write)
1035 1035
1036 1036 @vcrcommand(b'phabupdate',
1037 1037 [(b'', b'accept', False, _(b'accept revisions')),
1038 1038 (b'', b'reject', False, _(b'reject revisions')),
1039 1039 (b'', b'abandon', False, _(b'abandon revisions')),
1040 1040 (b'', b'reclaim', False, _(b'reclaim revisions')),
1041 1041 (b'm', b'comment', b'', _(b'comment on the last revision')),
1042 1042 ], _(b'DREVSPEC [OPTIONS]'),
1043 1043 helpcategory=command.CATEGORY_IMPORT_EXPORT)
1044 1044 def phabupdate(ui, repo, spec, **opts):
1045 1045 """update Differential Revision in batch
1046 1046
1047 1047 DREVSPEC selects revisions. See :hg:`help phabread` for its usage.
1048 1048 """
1049 1049 opts = pycompat.byteskwargs(opts)
1050 1050 flags = [n for n in b'accept reject abandon reclaim'.split() if opts.get(n)]
1051 1051 if len(flags) > 1:
1052 1052 raise error.Abort(_(b'%s cannot be used together') % b', '.join(flags))
1053 1053
1054 1054 actions = []
1055 1055 for f in flags:
1056 1056 actions.append({b'type': f, b'value': b'true'})
1057 1057
1058 1058 drevs = querydrev(repo, spec)
1059 1059 for i, drev in enumerate(drevs):
1060 1060 if i + 1 == len(drevs) and opts.get(b'comment'):
1061 1061 actions.append({b'type': b'comment', b'value': opts[b'comment']})
1062 1062 if actions:
1063 1063 params = {b'objectIdentifier': drev[b'phid'],
1064 1064 b'transactions': actions}
1065 1065 callconduit(ui, b'differential.revision.edit', params)
1066 1066
1067 1067 templatekeyword = registrar.templatekeyword()
1068 1068
1069 1069 @templatekeyword(b'phabreview', requires={b'ctx'})
1070 1070 def template_review(context, mapping):
1071 1071 """:phabreview: Object describing the review for this changeset.
1072 1072 Has attributes `url` and `id`.
1073 1073 """
1074 1074 ctx = context.resource(mapping, b'ctx')
1075 1075 m = _differentialrevisiondescre.search(ctx.description())
1076 1076 if m:
1077 1077 return templateutil.hybriddict({
1078 1078 b'url': m.group(r'url'),
1079 1079 b'id': b"D%s" % m.group(r'id'),
1080 1080 })
1081 1081 else:
1082 1082 tags = ctx.repo().nodetags(ctx.node())
1083 1083 for t in tags:
1084 1084 if _differentialrevisiontagre.match(t):
1085 1085 url = ctx.repo().ui.config(b'phabricator', b'url')
1086 1086 if not url.endswith(b'/'):
1087 1087 url += b'/'
1088 1088 url += t
1089 1089
1090 1090 return templateutil.hybriddict({
1091 1091 b'url': url,
1092 1092 b'id': t,
1093 1093 })
1094 1094 return None
@@ -1,209 +1,157 b''
1 1 #require vcr
2 2 $ cat >> $HGRCPATH <<EOF
3 3 > [extensions]
4 4 > phabricator =
5 5 > EOF
6 6 $ hg init repo
7 7 $ cd repo
8 8 $ cat >> .hg/hgrc <<EOF
9 9 > [phabricator]
10 10 > url = https://phab.mercurial-scm.org/
11 11 > callsign = HG
12 12 >
13 13 > [auth]
14 14 > hgphab.schemes = https
15 15 > hgphab.prefix = phab.mercurial-scm.org
16 16 > # When working on the extension and making phabricator interaction
17 17 > # changes, edit this to be a real phabricator token. When done, edit
18 18 > # it back. The VCR transcripts will be auto-sanitised to replace your real
19 19 > # token with this value.
20 20 > hgphab.phabtoken = cli-hahayouwish
21 21 > EOF
22 22 $ VCR="$TESTDIR/phabricator"
23 23
24 24 Error is handled reasonably. We override the phabtoken here so that
25 25 when you're developing changes to phabricator.py you can edit the
26 26 above config and have a real token in the test but not have to edit
27 27 this test.
28 28 $ hg phabread --config auth.hgphab.phabtoken=cli-notavalidtoken \
29 29 > --test-vcr "$VCR/phabread-conduit-error.json" D4480 | head
30 30 abort: Conduit Error (ERR-INVALID-AUTH): API token "cli-notavalidtoken" has the wrong length. API tokens should be 32 characters long.
31 31
32 32 Basic phabread:
33 33 $ hg phabread --test-vcr "$VCR/phabread-4480.json" D4480 | head
34 34 # HG changeset patch
35 35 # Date 1536771503 0
36 36 # Parent a5de21c9e3703f8e8eb064bd7d893ff2f703c66a
37 37 exchangev2: start to implement pull with wire protocol v2
38 38
39 39 Wire protocol version 2 will take a substantially different
40 40 approach to exchange than version 1 (at least as far as pulling
41 41 is concerned).
42 42
43 43 This commit establishes a new exchangev2 module for holding
44 44
45 45 phabupdate with an accept:
46 46 $ hg phabupdate --accept D4564 \
47 47 > -m 'I think I like where this is headed. Will read rest of series later.'\
48 48 > --test-vcr "$VCR/accept-4564.json"
49 49
50 50 Create a differential diff:
51 51 $ HGENCODING=utf-8; export HGENCODING
52 52 $ echo alpha > alpha
53 53 $ hg ci --addremove -m 'create alpha for phabricator test €'
54 54 adding alpha
55 55 $ hg phabsend -r . --test-vcr "$VCR/phabsend-create-alpha.json"
56 56 D1190 - created - d386117f30e6: create alpha for phabricator test \xe2\x82\xac (esc)
57 57 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/d386117f30e6-24ffe649-phabsend.hg
58 58 $ echo more >> alpha
59 59 $ HGEDITOR=true hg ci --amend
60 60 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/a86ed7d85e86-b7a54f3b-amend.hg
61 61 $ echo beta > beta
62 62 $ hg ci --addremove -m 'create beta for phabricator test'
63 63 adding beta
64 64 $ hg phabsend -r ".^::" --test-vcr "$VCR/phabsend-update-alpha-create-beta.json"
65 65 D1190 - updated - d940d39fb603: create alpha for phabricator test \xe2\x82\xac (esc)
66 66 D1191 - created - 4b2486dfc8c7: create beta for phabricator test
67 67 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/4b2486dfc8c7-d90584fa-phabsend.hg
68 68 $ unset HGENCODING
69 69
70 70 The amend won't explode after posting a public commit. The local tag is left
71 71 behind to identify it.
72 72
73 73 $ echo 'public change' > beta
74 74 $ hg ci -m 'create public change for phabricator testing'
75 75 $ hg phase --public .
76 76 $ echo 'draft change' > alpha
77 77 $ hg ci -m 'create draft change for phabricator testing'
78 78 $ hg phabsend --amend -r '.^::' --test-vcr "$VCR/phabsend-create-public.json"
79 79 D1192 - created - 24ffd6bca53a: create public change for phabricator testing
80 80 D1193 - created - ac331633be79: create draft change for phabricator testing
81 81 warning: not updating public commit 2:24ffd6bca53a
82 82 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/ac331633be79-719b961c-phabsend.hg
83 83 $ hg tags -v
84 84 tip 3:a19f1434f9a5
85 85 D1192 2:24ffd6bca53a local
86 86
87 87 $ hg debugcallconduit user.search --test-vcr "$VCR/phab-conduit.json" <<EOF
88 88 > {
89 89 > "constraints": {
90 90 > "isBot": true
91 91 > }
92 92 > }
93 93 > EOF
94 94 {
95 95 "cursor": {
96 96 "after": null,
97 97 "before": null,
98 98 "limit": 100,
99 99 "order": null
100 100 },
101 101 "data": [],
102 102 "maps": {},
103 103 "query": {
104 104 "queryKey": null
105 105 }
106 106 }
107 107
108 108 Template keywords
109 109 $ hg log -T'{rev} {phabreview|json}\n'
110 110 3 {"id": "D1193", "url": "https://phab.mercurial-scm.org/D1193"}
111 111 2 {"id": "D1192", "url": "https://phab.mercurial-scm.org/D1192"}
112 112 1 {"id": "D1191", "url": "https://phab.mercurial-scm.org/D1191"}
113 113 0 {"id": "D1190", "url": "https://phab.mercurial-scm.org/D1190"}
114 114
115 115 $ hg log -T'{rev} {if(phabreview, "{phabreview.url} {phabreview.id}")}\n'
116 116 3 https://phab.mercurial-scm.org/D1193 D1193
117 117 2 https://phab.mercurial-scm.org/D1192 D1192
118 118 1 https://phab.mercurial-scm.org/D1191 D1191
119 119 0 https://phab.mercurial-scm.org/D1190 D1190
120 120
121 121 Commenting when phabsending:
122 122 $ echo comment > comment
123 123 $ hg ci --addremove -m "create comment for phabricator test"
124 124 adding comment
125 125 $ hg phabsend -r . -m "For default branch" --test-vcr "$VCR/phabsend-comment-created.json"
126 126 D1253 - created - a7ee4bac036a: create comment for phabricator test
127 127 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/a7ee4bac036a-8009b5a0-phabsend.hg
128 128 $ echo comment2 >> comment
129 129 $ hg ci --amend
130 130 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/81fce7de1b7d-05339e5b-amend.hg
131 131 $ hg phabsend -r . -m "Address review comments" --test-vcr "$VCR/phabsend-comment-updated.json"
132 132 D1253 - updated - 1acd4b60af38: create comment for phabricator test
133 133
134 134 Phabsending a skipped commit:
135 #if no-py3
136 135 $ hg phabsend --no-amend -r . --test-vcr "$VCR/phabsend-skipped.json"
137 136 D1253 - skipped - 1acd4b60af38: create comment for phabricator test
138 #endif
139 BROKEN: shouldn't error under py3
140 #if py3
141 $ hg phabsend --no-amend -r . --test-vcr "$VCR/phabsend-skipped.json"
142 ** unknown exception encountered, please report by visiting
143 ** https://mercurial-scm.org/wiki/BugTracker
144 ** Python 3* (glob)
145 ** Mercurial Distributed SCM (version *) (glob)
146 ** Extensions loaded: phabricator
147 Traceback (most recent call last):
148 File "*/install/bin/hg", line *, in <module> (glob)
149 dispatch.run()
150 File "*/install/lib/python/mercurial/dispatch.py", line *, in run (glob)
151 status = dispatch(req)
152 File "*/install/lib/python/mercurial/dispatch.py", line *, in dispatch (glob)
153 ret = _runcatch(req) or 0
154 File "*/install/lib/python/mercurial/dispatch.py", line *, in _runcatch (glob)
155 return _callcatch(ui, _runcatchfunc)
156 File "*/install/lib/python/mercurial/dispatch.py", line *, in _callcatch (glob)
157 return scmutil.callcatch(ui, func)
158 File "*/install/lib/python/mercurial/scmutil.py", line *, in callcatch (glob)
159 return func()
160 File "*/install/lib/python/mercurial/dispatch.py", line *, in _runcatchfunc (glob)
161 return _dispatch(req)
162 File "*/install/lib/python/mercurial/dispatch.py", line *, in _dispatch (glob)
163 cmdpats, cmdoptions)
164 File "*/install/lib/python/mercurial/dispatch.py", line *, in runcommand (glob)
165 ret = _runcommand(ui, options, cmd, d)
166 File "*/install/lib/python/mercurial/dispatch.py", line *, in _runcommand (glob)
167 return cmdfunc()
168 File "*/install/lib/python/mercurial/dispatch.py", line *, in <lambda> (glob)
169 d = lambda: util.checksignature(func)(ui, *args, **strcmdopt)
170 File "*/install/lib/python/mercurial/util.py", line *, in check (glob)
171 return func(*args, **kwargs)
172 File "*/install/lib/python/hgext/phabricator.py", line *, in inner (glob)
173 return fn(*args, **kwargs)
174 File "*/install/lib/python/hgext/phabricator.py", line *, in phabsend (glob)
175 newrevphid = querydrev(repo, str(revid))[0][b'phid']
176 File "*/install/lib/python/hgext/phabricator.py", line *, in querydrev (glob)
177 tree = _parse(spec)
178 File "*/install/lib/python/hgext/phabricator.py", line *, in _parse (glob)
179 tree, pos = parser.parser(_elements).parse(_tokenize(text))
180 File "*/install/lib/python/mercurial/parser.py", line *, in parse (glob)
181 self._advance()
182 File "*/install/lib/python/mercurial/parser.py", line *, in _advance (glob)
183 self.current = next(self._iter, None)
184 File "*/install/lib/python/hgext/phabricator.py", line *, in _tokenize (glob)
185 view = memoryview(text) # zero-copy slice
186 TypeError: memoryview: a bytes-like object is required, not 'str'
187 [1]
188 #endif
189 137
190 138 Phabreading a DREV with a local:commits time as a string:
191 139 $ hg phabread --test-vcr "$VCR/phabread-str-time.json" D1285
192 140 # HG changeset patch
193 141 # User test <test>
194 142 # Date 1562019844 0
195 143 # Branch default
196 144 # Node ID da5c8c6bf23a36b6e3af011bc3734460692c23ce
197 145 # Parent 1f634396406d03e565ed645370e5fecd062cf215
198 146 test string time
199 147
200 148 Differential Revision: https://phab.mercurial-scm.org/D1285
201 149 diff --git a/test b/test
202 150 new file mode 100644
203 151 --- /dev/null
204 152 +++ b/test
205 153 @@ * @@ (glob)
206 154 +test
207 155
208 156
209 157 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now