contrib/phabricator.py
changeset 33787 fa3aa6c98bb7
parent 33785 f7d6978a4da9
child 33831 75fdaf851e83
equal deleted inserted replaced
33786:0975506120fb 33787:fa3aa6c98bb7
    36 import re
    36 import re
    37 
    37 
    38 from mercurial.node import bin, nullid
    38 from mercurial.node import bin, nullid
    39 from mercurial.i18n import _
    39 from mercurial.i18n import _
    40 from mercurial import (
    40 from mercurial import (
       
    41     cmdutil,
       
    42     context,
    41     encoding,
    43     encoding,
    42     error,
    44     error,
    43     mdiff,
    45     mdiff,
    44     obsutil,
    46     obsutil,
    45     patch,
    47     patch,
   313 
   315 
   314     revision = callconduit(repo, 'differential.revision.edit', params)
   316     revision = callconduit(repo, 'differential.revision.edit', params)
   315     if not revision:
   317     if not revision:
   316         raise error.Abort(_('cannot create revision for %s') % ctx)
   318         raise error.Abort(_('cannot create revision for %s') % ctx)
   317 
   319 
   318     return revision
   320     return revision, diff
   319 
   321 
   320 def userphids(repo, names):
   322 def userphids(repo, names):
   321     """convert user names to PHIDs"""
   323     """convert user names to PHIDs"""
   322     query = {'constraints': {'usernames': names}}
   324     query = {'constraints': {'usernames': names}}
   323     result = callconduit(repo, 'user.search', query)
   325     result = callconduit(repo, 'user.search', query)
   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 ')])