331 % ' '.join(sorted(unresolved))) |
333 % ' '.join(sorted(unresolved))) |
332 return [entry[r'phid'] for entry in data] |
334 return [entry[r'phid'] for entry in data] |
333 |
335 |
334 @command('phabsend', |
336 @command('phabsend', |
335 [('r', 'rev', [], _('revisions to send'), _('REV')), |
337 [('r', 'rev', [], _('revisions to send'), _('REV')), |
|
338 ('', 'amend', False, _('update commit messages')), |
336 ('', 'reviewer', [], _('specify reviewers')), |
339 ('', 'reviewer', [], _('specify reviewers')), |
337 ('', 'confirm', None, _('ask for confirmation before sending'))], |
340 ('', 'confirm', None, _('ask for confirmation before sending'))], |
338 _('REV [OPTIONS]')) |
341 _('REV [OPTIONS]')) |
339 def phabsend(ui, repo, *revs, **opts): |
342 def phabsend(ui, repo, *revs, **opts): |
340 """upload changesets to Phabricator |
343 """upload changesets to Phabricator |
346 For the first time uploading changesets, local tags will be created to |
349 For the first time uploading changesets, local tags will be created to |
347 maintain the association. After the first time, phabsend will check |
350 maintain the association. After the first time, phabsend will check |
348 obsstore and tags information so it can figure out whether to update an |
351 obsstore and tags information so it can figure out whether to update an |
349 existing Differential Revision, or create a new one. |
352 existing Differential Revision, or create a new one. |
350 |
353 |
|
354 If --amend is set, update commit messages so they have the |
|
355 ``Differential Revision`` URL, remove related tags. This is similar to what |
|
356 arcanist will do, and is more desired in author-push workflows. Otherwise, |
|
357 use local tags to record the ``Differential Revision`` association. |
|
358 |
351 The --confirm option lets you confirm changesets before sending them. You |
359 The --confirm option lets you confirm changesets before sending them. You |
352 can also add following to your configuration file to make it default |
360 can also add following to your configuration file to make it default |
353 behaviour. |
361 behaviour. |
354 |
362 |
355 [phabsend] |
363 [phabsend] |
356 confirm = true |
364 confirm = true |
|
365 |
|
366 phabsend will check obsstore and the above association to decide whether to |
|
367 update an existing Differential Revision, or create a new one. |
357 """ |
368 """ |
358 revs = list(revs) + opts.get('rev', []) |
369 revs = list(revs) + opts.get('rev', []) |
359 revs = scmutil.revrange(repo, revs) |
370 revs = scmutil.revrange(repo, revs) |
360 |
371 |
361 if not revs: |
372 if not revs: |
362 raise error.Abort(_('phabsend requires at least one changeset')) |
373 raise error.Abort(_('phabsend requires at least one changeset')) |
|
374 if opts.get('amend'): |
|
375 cmdutil.checkunfinished(repo) |
363 |
376 |
364 confirm = ui.configbool('phabsend', 'confirm') |
377 confirm = ui.configbool('phabsend', 'confirm') |
365 confirm |= bool(opts.get('confirm')) |
378 confirm |= bool(opts.get('confirm')) |
366 if confirm: |
379 if confirm: |
367 confirmed = _confirmbeforesend(repo, revs) |
380 confirmed = _confirmbeforesend(repo, revs) |
375 actions.append({'type': 'reviewers.add', 'value': phids}) |
388 actions.append({'type': 'reviewers.add', 'value': phids}) |
376 |
389 |
377 # {newnode: (oldnode, olddiff, olddrev} |
390 # {newnode: (oldnode, olddiff, olddrev} |
378 oldmap = getoldnodedrevmap(repo, [repo[r].node() for r in revs]) |
391 oldmap = getoldnodedrevmap(repo, [repo[r].node() for r in revs]) |
379 |
392 |
|
393 drevids = [] # [int] |
|
394 diffmap = {} # {newnode: diff} |
|
395 |
380 # Send patches one by one so we know their Differential Revision IDs and |
396 # Send patches one by one so we know their Differential Revision IDs and |
381 # can provide dependency relationship |
397 # can provide dependency relationship |
382 lastrevid = None |
398 lastrevid = None |
383 for rev in revs: |
399 for rev in revs: |
384 ui.debug('sending rev %d\n' % rev) |
400 ui.debug('sending rev %d\n' % rev) |
385 ctx = repo[rev] |
401 ctx = repo[rev] |
386 |
402 |
387 # Get Differential Revision ID |
403 # Get Differential Revision ID |
388 oldnode, olddiff, revid = oldmap.get(ctx.node(), (None, None, None)) |
404 oldnode, olddiff, revid = oldmap.get(ctx.node(), (None, None, None)) |
389 if oldnode != ctx.node(): |
405 if oldnode != ctx.node() or opts.get('amend'): |
390 # Create or update Differential Revision |
406 # Create or update Differential Revision |
391 revision = createdifferentialrevision(ctx, revid, lastrevid, |
407 revision, diff = createdifferentialrevision( |
392 oldnode, olddiff, actions) |
408 ctx, revid, lastrevid, oldnode, olddiff, actions) |
|
409 diffmap[ctx.node()] = diff |
393 newrevid = int(revision[r'object'][r'id']) |
410 newrevid = int(revision[r'object'][r'id']) |
394 if revid: |
411 if revid: |
395 action = _('updated') |
412 action = _('updated') |
396 else: |
413 else: |
397 action = _('created') |
414 action = _('created') |
398 |
415 |
399 # Create a local tag to note the association |
416 # Create a local tag to note the association, if commit message |
400 tagname = 'D%d' % newrevid |
417 # does not have it already |
401 tags.tag(repo, tagname, ctx.node(), message=None, user=None, |
418 m = _differentialrevisiondescre.search(ctx.description()) |
402 date=None, local=True) |
419 if not m or int(m.group(1)) != newrevid: |
|
420 tagname = 'D%d' % newrevid |
|
421 tags.tag(repo, tagname, ctx.node(), message=None, user=None, |
|
422 date=None, local=True) |
403 else: |
423 else: |
404 # Nothing changed. But still set "newrevid" so the next revision |
424 # Nothing changed. But still set "newrevid" so the next revision |
405 # could depend on this one. |
425 # could depend on this one. |
406 newrevid = revid |
426 newrevid = revid |
407 action = _('skipped') |
427 action = _('skipped') |
408 |
428 |
409 ui.write(_('D%s: %s - %s: %s\n') % (newrevid, action, ctx, |
429 ui.write(_('D%s: %s - %s: %s\n') % (newrevid, action, ctx, |
410 ctx.description().split('\n')[0])) |
430 ctx.description().split('\n')[0])) |
|
431 drevids.append(newrevid) |
411 lastrevid = newrevid |
432 lastrevid = newrevid |
|
433 |
|
434 # Update commit messages and remove tags |
|
435 if opts.get('amend'): |
|
436 unfi = repo.unfiltered() |
|
437 drevs = callconduit(repo, 'differential.query', {'ids': drevids}) |
|
438 with repo.wlock(), repo.lock(), repo.transaction('phabsend'): |
|
439 wnode = unfi['.'].node() |
|
440 mapping = {} # {oldnode: [newnode]} |
|
441 for i, rev in enumerate(revs): |
|
442 old = unfi[rev] |
|
443 drevid = drevids[i] |
|
444 drev = [d for d in drevs if int(d[r'id']) == drevid][0] |
|
445 newdesc = getdescfromdrev(drev) |
|
446 # Make sure commit message contain "Differential Revision" |
|
447 if old.description() != newdesc: |
|
448 parents = [ |
|
449 mapping.get(old.p1().node(), (old.p1(),))[0], |
|
450 mapping.get(old.p2().node(), (old.p2(),))[0], |
|
451 ] |
|
452 new = context.metadataonlyctx( |
|
453 repo, old, parents=parents, text=newdesc, |
|
454 user=old.user(), date=old.date(), extra=old.extra()) |
|
455 newnode = new.commit() |
|
456 mapping[old.node()] = [newnode] |
|
457 # Update diff property |
|
458 writediffproperties(unfi[newnode], diffmap[old.node()]) |
|
459 # Remove local tags since it's no longer necessary |
|
460 tagname = 'D%d' % drevid |
|
461 if tagname in repo.tags(): |
|
462 tags.tag(repo, tagname, nullid, message=None, user=None, |
|
463 date=None, local=True) |
|
464 scmutil.cleanupnodes(repo, mapping, 'phabsend') |
|
465 if wnode in mapping: |
|
466 unfi.setparents(mapping[wnode][0]) |
412 |
467 |
413 # Map from "hg:meta" keys to header understood by "hg import". The order is |
468 # Map from "hg:meta" keys to header understood by "hg import". The order is |
414 # consistent with "hg export" output. |
469 # consistent with "hg export" output. |
415 _metanamemap = util.sortdict([(r'user', 'User'), (r'date', 'Date'), |
470 _metanamemap = util.sortdict([(r'user', 'User'), (r'date', 'Date'), |
416 (r'node', 'Node ID'), (r'parent', 'Parent ')]) |
471 (r'node', 'Node ID'), (r'parent', 'Parent ')]) |