##// END OF EJS Templates
phabricator: handle local:commits time being string or int...
Ian Moody -
r42824:c9be100e default draft
parent child Browse files
Show More
@@ -1,1089 +1,1089 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 615 newrevphid = querydrev(repo, str(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 writediffproperties(unfi[newnode], diffmap[old.node()])
663 663 # Remove local tags since it's no longer necessary
664 664 tagname = b'D%d' % drevid
665 665 if tagname in repo.tags():
666 666 tags.tag(repo, tagname, nullid, message=None, user=None,
667 667 date=None, local=True)
668 668 scmutil.cleanupnodes(repo, mapping, b'phabsend', fixphase=True)
669 669 if wnode in mapping:
670 670 unfi.setparents(mapping[wnode][0])
671 671
672 672 # Map from "hg:meta" keys to header understood by "hg import". The order is
673 673 # consistent with "hg export" output.
674 674 _metanamemap = util.sortdict([(b'user', b'User'), (b'date', b'Date'),
675 675 (b'branch', b'Branch'), (b'node', b'Node ID'),
676 676 (b'parent', b'Parent ')])
677 677
678 678 def _confirmbeforesend(repo, revs, oldmap):
679 679 url, token = readurltoken(repo.ui)
680 680 ui = repo.ui
681 681 for rev in revs:
682 682 ctx = repo[rev]
683 683 desc = ctx.description().splitlines()[0]
684 684 oldnode, olddiff, drevid = oldmap.get(ctx.node(), (None, None, None))
685 685 if drevid:
686 686 drevdesc = ui.label(b'D%s' % drevid, b'phabricator.drev')
687 687 else:
688 688 drevdesc = ui.label(_(b'NEW'), b'phabricator.drev')
689 689
690 690 ui.write(_(b'%s - %s: %s\n')
691 691 % (drevdesc,
692 692 ui.label(bytes(ctx), b'phabricator.node'),
693 693 ui.label(desc, b'phabricator.desc')))
694 694
695 695 if ui.promptchoice(_(b'Send the above changes to %s (yn)?'
696 696 b'$$ &Yes $$ &No') % url):
697 697 return False
698 698
699 699 return True
700 700
701 701 _knownstatusnames = {b'accepted', b'needsreview', b'needsrevision', b'closed',
702 702 b'abandoned'}
703 703
704 704 def _getstatusname(drev):
705 705 """get normalized status name from a Differential Revision"""
706 706 return drev[b'statusName'].replace(b' ', b'').lower()
707 707
708 708 # Small language to specify differential revisions. Support symbols: (), :X,
709 709 # +, and -.
710 710
711 711 _elements = {
712 712 # token-type: binding-strength, primary, prefix, infix, suffix
713 713 b'(': (12, None, (b'group', 1, b')'), None, None),
714 714 b':': (8, None, (b'ancestors', 8), None, None),
715 715 b'&': (5, None, None, (b'and_', 5), None),
716 716 b'+': (4, None, None, (b'add', 4), None),
717 717 b'-': (4, None, None, (b'sub', 4), None),
718 718 b')': (0, None, None, None, None),
719 719 b'symbol': (0, b'symbol', None, None, None),
720 720 b'end': (0, None, None, None, None),
721 721 }
722 722
723 723 def _tokenize(text):
724 724 view = memoryview(text) # zero-copy slice
725 725 special = b'():+-& '
726 726 pos = 0
727 727 length = len(text)
728 728 while pos < length:
729 729 symbol = b''.join(itertools.takewhile(lambda ch: ch not in special,
730 730 pycompat.iterbytestr(view[pos:])))
731 731 if symbol:
732 732 yield (b'symbol', symbol, pos)
733 733 pos += len(symbol)
734 734 else: # special char, ignore space
735 735 if text[pos] != b' ':
736 736 yield (text[pos], None, pos)
737 737 pos += 1
738 738 yield (b'end', None, pos)
739 739
740 740 def _parse(text):
741 741 tree, pos = parser.parser(_elements).parse(_tokenize(text))
742 742 if pos != len(text):
743 743 raise error.ParseError(b'invalid token', pos)
744 744 return tree
745 745
746 746 def _parsedrev(symbol):
747 747 """str -> int or None, ex. 'D45' -> 45; '12' -> 12; 'x' -> None"""
748 748 if symbol.startswith(b'D') and symbol[1:].isdigit():
749 749 return int(symbol[1:])
750 750 if symbol.isdigit():
751 751 return int(symbol)
752 752
753 753 def _prefetchdrevs(tree):
754 754 """return ({single-drev-id}, {ancestor-drev-id}) to prefetch"""
755 755 drevs = set()
756 756 ancestordrevs = set()
757 757 op = tree[0]
758 758 if op == b'symbol':
759 759 r = _parsedrev(tree[1])
760 760 if r:
761 761 drevs.add(r)
762 762 elif op == b'ancestors':
763 763 r, a = _prefetchdrevs(tree[1])
764 764 drevs.update(r)
765 765 ancestordrevs.update(r)
766 766 ancestordrevs.update(a)
767 767 else:
768 768 for t in tree[1:]:
769 769 r, a = _prefetchdrevs(t)
770 770 drevs.update(r)
771 771 ancestordrevs.update(a)
772 772 return drevs, ancestordrevs
773 773
774 774 def querydrev(repo, spec):
775 775 """return a list of "Differential Revision" dicts
776 776
777 777 spec is a string using a simple query language, see docstring in phabread
778 778 for details.
779 779
780 780 A "Differential Revision dict" looks like:
781 781
782 782 {
783 783 "id": "2",
784 784 "phid": "PHID-DREV-672qvysjcczopag46qty",
785 785 "title": "example",
786 786 "uri": "https://phab.example.com/D2",
787 787 "dateCreated": "1499181406",
788 788 "dateModified": "1499182103",
789 789 "authorPHID": "PHID-USER-tv3ohwc4v4jeu34otlye",
790 790 "status": "0",
791 791 "statusName": "Needs Review",
792 792 "properties": [],
793 793 "branch": null,
794 794 "summary": "",
795 795 "testPlan": "",
796 796 "lineCount": "2",
797 797 "activeDiffPHID": "PHID-DIFF-xoqnjkobbm6k4dk6hi72",
798 798 "diffs": [
799 799 "3",
800 800 "4",
801 801 ],
802 802 "commits": [],
803 803 "reviewers": [],
804 804 "ccs": [],
805 805 "hashes": [],
806 806 "auxiliary": {
807 807 "phabricator:projects": [],
808 808 "phabricator:depends-on": [
809 809 "PHID-DREV-gbapp366kutjebt7agcd"
810 810 ]
811 811 },
812 812 "repositoryPHID": "PHID-REPO-hub2hx62ieuqeheznasv",
813 813 "sourcePath": null
814 814 }
815 815 """
816 816 def fetch(params):
817 817 """params -> single drev or None"""
818 818 key = (params.get(b'ids') or params.get(b'phids') or [None])[0]
819 819 if key in prefetched:
820 820 return prefetched[key]
821 821 drevs = callconduit(repo.ui, b'differential.query', params)
822 822 # Fill prefetched with the result
823 823 for drev in drevs:
824 824 prefetched[drev[b'phid']] = drev
825 825 prefetched[int(drev[b'id'])] = drev
826 826 if key not in prefetched:
827 827 raise error.Abort(_(b'cannot get Differential Revision %r')
828 828 % params)
829 829 return prefetched[key]
830 830
831 831 def getstack(topdrevids):
832 832 """given a top, get a stack from the bottom, [id] -> [id]"""
833 833 visited = set()
834 834 result = []
835 835 queue = [{b'ids': [i]} for i in topdrevids]
836 836 while queue:
837 837 params = queue.pop()
838 838 drev = fetch(params)
839 839 if drev[b'id'] in visited:
840 840 continue
841 841 visited.add(drev[b'id'])
842 842 result.append(int(drev[b'id']))
843 843 auxiliary = drev.get(b'auxiliary', {})
844 844 depends = auxiliary.get(b'phabricator:depends-on', [])
845 845 for phid in depends:
846 846 queue.append({b'phids': [phid]})
847 847 result.reverse()
848 848 return smartset.baseset(result)
849 849
850 850 # Initialize prefetch cache
851 851 prefetched = {} # {id or phid: drev}
852 852
853 853 tree = _parse(spec)
854 854 drevs, ancestordrevs = _prefetchdrevs(tree)
855 855
856 856 # developer config: phabricator.batchsize
857 857 batchsize = repo.ui.configint(b'phabricator', b'batchsize')
858 858
859 859 # Prefetch Differential Revisions in batch
860 860 tofetch = set(drevs)
861 861 for r in ancestordrevs:
862 862 tofetch.update(range(max(1, r - batchsize), r + 1))
863 863 if drevs:
864 864 fetch({b'ids': list(tofetch)})
865 865 validids = sorted(set(getstack(list(ancestordrevs))) | set(drevs))
866 866
867 867 # Walk through the tree, return smartsets
868 868 def walk(tree):
869 869 op = tree[0]
870 870 if op == b'symbol':
871 871 drev = _parsedrev(tree[1])
872 872 if drev:
873 873 return smartset.baseset([drev])
874 874 elif tree[1] in _knownstatusnames:
875 875 drevs = [r for r in validids
876 876 if _getstatusname(prefetched[r]) == tree[1]]
877 877 return smartset.baseset(drevs)
878 878 else:
879 879 raise error.Abort(_(b'unknown symbol: %s') % tree[1])
880 880 elif op in {b'and_', b'add', b'sub'}:
881 881 assert len(tree) == 3
882 882 return getattr(operator, op)(walk(tree[1]), walk(tree[2]))
883 883 elif op == b'group':
884 884 return walk(tree[1])
885 885 elif op == b'ancestors':
886 886 return getstack(walk(tree[1]))
887 887 else:
888 888 raise error.ProgrammingError(b'illegal tree: %r' % tree)
889 889
890 890 return [prefetched[r] for r in walk(tree)]
891 891
892 892 def getdescfromdrev(drev):
893 893 """get description (commit message) from "Differential Revision"
894 894
895 895 This is similar to differential.getcommitmessage API. But we only care
896 896 about limited fields: title, summary, test plan, and URL.
897 897 """
898 898 title = drev[b'title']
899 899 summary = drev[b'summary'].rstrip()
900 900 testplan = drev[b'testPlan'].rstrip()
901 901 if testplan:
902 902 testplan = b'Test Plan:\n%s' % testplan
903 903 uri = b'Differential Revision: %s' % drev[b'uri']
904 904 return b'\n\n'.join(filter(None, [title, summary, testplan, uri]))
905 905
906 906 def getdiffmeta(diff):
907 907 """get commit metadata (date, node, user, p1) from a diff object
908 908
909 909 The metadata could be "hg:meta", sent by phabsend, like:
910 910
911 911 "properties": {
912 912 "hg:meta": {
913 913 "date": "1499571514 25200",
914 914 "node": "98c08acae292b2faf60a279b4189beb6cff1414d",
915 915 "user": "Foo Bar <foo@example.com>",
916 916 "parent": "6d0abad76b30e4724a37ab8721d630394070fe16"
917 917 }
918 918 }
919 919
920 920 Or converted from "local:commits", sent by "arc", like:
921 921
922 922 "properties": {
923 923 "local:commits": {
924 924 "98c08acae292b2faf60a279b4189beb6cff1414d": {
925 925 "author": "Foo Bar",
926 926 "time": 1499546314,
927 927 "branch": "default",
928 928 "tag": "",
929 929 "commit": "98c08acae292b2faf60a279b4189beb6cff1414d",
930 930 "rev": "98c08acae292b2faf60a279b4189beb6cff1414d",
931 931 "local": "1000",
932 932 "parents": ["6d0abad76b30e4724a37ab8721d630394070fe16"],
933 933 "summary": "...",
934 934 "message": "...",
935 935 "authorEmail": "foo@example.com"
936 936 }
937 937 }
938 938 }
939 939
940 940 Note: metadata extracted from "local:commits" will lose time zone
941 941 information.
942 942 """
943 943 props = diff.get(b'properties') or {}
944 944 meta = props.get(b'hg:meta')
945 945 if not meta:
946 946 if props.get(b'local:commits'):
947 947 commit = sorted(props[b'local:commits'].values())[0]
948 948 meta = {}
949 949 if b'author' in commit and b'authorEmail' in commit:
950 950 meta[b'user'] = b'%s <%s>' % (commit[b'author'],
951 951 commit[b'authorEmail'])
952 952 if b'time' in commit:
953 meta[b'date'] = b'%d 0' % commit[b'time']
953 meta[b'date'] = b'%d 0' % int(commit[b'time'])
954 954 if b'branch' in commit:
955 955 meta[b'branch'] = commit[b'branch']
956 956 node = commit.get(b'commit', commit.get(b'rev'))
957 957 if node:
958 958 meta[b'node'] = node
959 959 if len(commit.get(b'parents', ())) >= 1:
960 960 meta[b'parent'] = commit[b'parents'][0]
961 961 else:
962 962 meta = {}
963 963 if b'date' not in meta and b'dateCreated' in diff:
964 964 meta[b'date'] = b'%s 0' % diff[b'dateCreated']
965 965 if b'branch' not in meta and diff.get(b'branch'):
966 966 meta[b'branch'] = diff[b'branch']
967 967 if b'parent' not in meta and diff.get(b'sourceControlBaseRevision'):
968 968 meta[b'parent'] = diff[b'sourceControlBaseRevision']
969 969 return meta
970 970
971 971 def readpatch(repo, drevs, write):
972 972 """generate plain-text patch readable by 'hg import'
973 973
974 974 write is usually ui.write. drevs is what "querydrev" returns, results of
975 975 "differential.query".
976 976 """
977 977 # Prefetch hg:meta property for all diffs
978 978 diffids = sorted(set(max(int(v) for v in drev[b'diffs']) for drev in drevs))
979 979 diffs = callconduit(repo.ui, b'differential.querydiffs', {b'ids': diffids})
980 980
981 981 # Generate patch for each drev
982 982 for drev in drevs:
983 983 repo.ui.note(_(b'reading D%s\n') % drev[b'id'])
984 984
985 985 diffid = max(int(v) for v in drev[b'diffs'])
986 986 body = callconduit(repo.ui, b'differential.getrawdiff',
987 987 {b'diffID': diffid})
988 988 desc = getdescfromdrev(drev)
989 989 header = b'# HG changeset patch\n'
990 990
991 991 # Try to preserve metadata from hg:meta property. Write hg patch
992 992 # headers that can be read by the "import" command. See patchheadermap
993 993 # and extract in mercurial/patch.py for supported headers.
994 994 meta = getdiffmeta(diffs[b'%d' % diffid])
995 995 for k in _metanamemap.keys():
996 996 if k in meta:
997 997 header += b'# %s %s\n' % (_metanamemap[k], meta[k])
998 998
999 999 content = b'%s%s\n%s' % (header, desc, body)
1000 1000 write(content)
1001 1001
1002 1002 @vcrcommand(b'phabread',
1003 1003 [(b'', b'stack', False, _(b'read dependencies'))],
1004 1004 _(b'DREVSPEC [OPTIONS]'),
1005 1005 helpcategory=command.CATEGORY_IMPORT_EXPORT)
1006 1006 def phabread(ui, repo, spec, **opts):
1007 1007 """print patches from Phabricator suitable for importing
1008 1008
1009 1009 DREVSPEC could be a Differential Revision identity, like ``D123``, or just
1010 1010 the number ``123``. It could also have common operators like ``+``, ``-``,
1011 1011 ``&``, ``(``, ``)`` for complex queries. Prefix ``:`` could be used to
1012 1012 select a stack.
1013 1013
1014 1014 ``abandoned``, ``accepted``, ``closed``, ``needsreview``, ``needsrevision``
1015 1015 could be used to filter patches by status. For performance reason, they
1016 1016 only represent a subset of non-status selections and cannot be used alone.
1017 1017
1018 1018 For example, ``:D6+8-(2+D4)`` selects a stack up to D6, plus D8 and exclude
1019 1019 D2 and D4. ``:D9 & needsreview`` selects "Needs Review" revisions in a
1020 1020 stack up to D9.
1021 1021
1022 1022 If --stack is given, follow dependencies information and read all patches.
1023 1023 It is equivalent to the ``:`` operator.
1024 1024 """
1025 1025 opts = pycompat.byteskwargs(opts)
1026 1026 if opts.get(b'stack'):
1027 1027 spec = b':(%s)' % spec
1028 1028 drevs = querydrev(repo, spec)
1029 1029 readpatch(repo, drevs, ui.write)
1030 1030
1031 1031 @vcrcommand(b'phabupdate',
1032 1032 [(b'', b'accept', False, _(b'accept revisions')),
1033 1033 (b'', b'reject', False, _(b'reject revisions')),
1034 1034 (b'', b'abandon', False, _(b'abandon revisions')),
1035 1035 (b'', b'reclaim', False, _(b'reclaim revisions')),
1036 1036 (b'm', b'comment', b'', _(b'comment on the last revision')),
1037 1037 ], _(b'DREVSPEC [OPTIONS]'),
1038 1038 helpcategory=command.CATEGORY_IMPORT_EXPORT)
1039 1039 def phabupdate(ui, repo, spec, **opts):
1040 1040 """update Differential Revision in batch
1041 1041
1042 1042 DREVSPEC selects revisions. See :hg:`help phabread` for its usage.
1043 1043 """
1044 1044 opts = pycompat.byteskwargs(opts)
1045 1045 flags = [n for n in b'accept reject abandon reclaim'.split() if opts.get(n)]
1046 1046 if len(flags) > 1:
1047 1047 raise error.Abort(_(b'%s cannot be used together') % b', '.join(flags))
1048 1048
1049 1049 actions = []
1050 1050 for f in flags:
1051 1051 actions.append({b'type': f, b'value': b'true'})
1052 1052
1053 1053 drevs = querydrev(repo, spec)
1054 1054 for i, drev in enumerate(drevs):
1055 1055 if i + 1 == len(drevs) and opts.get(b'comment'):
1056 1056 actions.append({b'type': b'comment', b'value': opts[b'comment']})
1057 1057 if actions:
1058 1058 params = {b'objectIdentifier': drev[b'phid'],
1059 1059 b'transactions': actions}
1060 1060 callconduit(ui, b'differential.revision.edit', params)
1061 1061
1062 1062 templatekeyword = registrar.templatekeyword()
1063 1063
1064 1064 @templatekeyword(b'phabreview', requires={b'ctx'})
1065 1065 def template_review(context, mapping):
1066 1066 """:phabreview: Object describing the review for this changeset.
1067 1067 Has attributes `url` and `id`.
1068 1068 """
1069 1069 ctx = context.resource(mapping, b'ctx')
1070 1070 m = _differentialrevisiondescre.search(ctx.description())
1071 1071 if m:
1072 1072 return templateutil.hybriddict({
1073 1073 b'url': m.group(r'url'),
1074 1074 b'id': b"D%s" % m.group(r'id'),
1075 1075 })
1076 1076 else:
1077 1077 tags = ctx.repo().nodetags(ctx.node())
1078 1078 for t in tags:
1079 1079 if _differentialrevisiontagre.match(t):
1080 1080 url = ctx.repo().ui.config(b'phabricator', b'url')
1081 1081 if not url.endswith(b'/'):
1082 1082 url += b'/'
1083 1083 url += t
1084 1084
1085 1085 return templateutil.hybriddict({
1086 1086 b'url': url,
1087 1087 b'id': t,
1088 1088 })
1089 1089 return None
@@ -1,178 +1,153 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 Phabreading a DREV with a local:commits time as a string:
135 BROKEN: shouldn't error
136 135 $ hg phabread --test-vcr "$VCR/phabread-str-time.json" D1285
137 ** unknown exception encountered, please report by visiting
138 ** https://mercurial-scm.org/wiki/BugTracker
139 ** Python * (glob)
140 ** Mercurial Distributed SCM (version *) (glob)
141 ** Extensions loaded: phabricator
142 Traceback (most recent call last):
143 File "*/install/bin/hg", line *, in <module> (glob)
144 dispatch.run()
145 File "*/install/lib/python/mercurial/dispatch.py", line *, in run (glob)
146 status = dispatch(req)
147 File "*/install/lib/python/mercurial/dispatch.py", line *, in dispatch (glob)
148 ret = _runcatch(req) or 0
149 File "*/install/lib/python/mercurial/dispatch.py", line *, in _runcatch (glob)
150 return _callcatch(ui, _runcatchfunc)
151 File "*/install/lib/python/mercurial/dispatch.py", line *, in _callcatch (glob)
152 return scmutil.callcatch(ui, func)
153 File "*/install/lib/python/mercurial/scmutil.py", line *, in callcatch (glob)
154 return func()
155 File "*/install/lib/python/mercurial/dispatch.py", line *, in _runcatchfunc (glob)
156 return _dispatch(req)
157 File "*/install/lib/python/mercurial/dispatch.py", line *, in _dispatch (glob)
158 cmdpats, cmdoptions)
159 File "*/install/lib/python/mercurial/dispatch.py", line *, in runcommand (glob)
160 ret = _runcommand(ui, options, cmd, d)
161 File "*/install/lib/python/mercurial/dispatch.py", line *, in _runcommand (glob)
162 return cmdfunc()
163 File "*/install/lib/python/mercurial/dispatch.py", line *, in <lambda> (glob)
164 d = lambda: util.checksignature(func)(ui, *args, **strcmdopt)
165 File "*/install/lib/python/mercurial/util.py", line *, in check (glob)
166 return func(*args, **kwargs)
167 File "*/install/lib/python/hgext/phabricator.py", line *, in inner (glob)
168 return fn(*args, **kwargs)
169 File "*/install/lib/python/hgext/phabricator.py", line *, in phabread (glob)
170 readpatch(repo, drevs, ui.write)
171 File "*/install/lib/python/hgext/phabricator.py", line *, in readpatch (glob)
172 meta = getdiffmeta(diffs[b'%d' % diffid])
173 File "*/install/lib/python/hgext/phabricator.py", line *, in getdiffmeta (glob)
174 meta[b'date'] = b'%d 0' % commit[b'time']
175 TypeError: %d format: a number is required, not str
176 [1]
136 # HG changeset patch
137 # User test <test>
138 # Date 1562019844 0
139 # Branch default
140 # Node ID da5c8c6bf23a36b6e3af011bc3734460692c23ce
141 # Parent 1f634396406d03e565ed645370e5fecd062cf215
142 test string time
143
144 Differential Revision: https://phab.mercurial-scm.org/D1285
145 diff --git a/test b/test
146 new file mode 100644
147 --- /dev/null
148 +++ b/test
149 @@ * @@ (glob)
150 +test
151
177 152
178 153 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now