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