hgext/phabricator.py
changeset 41897 c340a8ac7ef3
parent 41705 570e62f1dcf2
child 41898 2bad8f92cebf
equal deleted inserted replaced
41896:94faa2e84094 41897:c340a8ac7ef3
    58     mdiff,
    58     mdiff,
    59     obsutil,
    59     obsutil,
    60     parser,
    60     parser,
    61     patch,
    61     patch,
    62     phases,
    62     phases,
       
    63     pycompat,
    63     registrar,
    64     registrar,
    64     scmutil,
    65     scmutil,
    65     smartset,
    66     smartset,
    66     tags,
    67     tags,
    67     templateutil,
    68     templateutil,
   217         urlopener = urlmod.opener(repo.ui, authinfo)
   218         urlopener = urlmod.opener(repo.ui, authinfo)
   218         request = util.urlreq.request(url, data=data)
   219         request = util.urlreq.request(url, data=data)
   219         with contextlib.closing(urlopener.open(request)) as rsp:
   220         with contextlib.closing(urlopener.open(request)) as rsp:
   220             body = rsp.read()
   221             body = rsp.read()
   221     repo.ui.debug(b'Conduit Response: %s\n' % body)
   222     repo.ui.debug(b'Conduit Response: %s\n' % body)
   222     parsed = json.loads(body)
   223     parsed = pycompat.rapply(
   223     if parsed.get(r'error_code'):
   224         lambda x: encoding.unitolocal(x) if isinstance(x, pycompat.unicode)
       
   225         else x,
       
   226         json.loads(body)
       
   227     )
       
   228     if parsed.get(b'error_code'):
   224         msg = (_(b'Conduit Error (%s): %s')
   229         msg = (_(b'Conduit Error (%s): %s')
   225                % (parsed[r'error_code'], parsed[r'error_info']))
   230                % (parsed[b'error_code'], parsed[b'error_info']))
   226         raise error.Abort(msg)
   231         raise error.Abort(msg)
   227     return parsed[r'result']
   232     return parsed[b'result']
   228 
   233 
   229 @vcrcommand(b'debugcallconduit', [], _(b'METHOD'))
   234 @vcrcommand(b'debugcallconduit', [], _(b'METHOD'))
   230 def debugcallconduit(ui, repo, name):
   235 def debugcallconduit(ui, repo, name):
   231     """call Conduit API
   236     """call Conduit API
   232 
   237 
   247     callsign = repo.ui.config(b'phabricator', b'callsign')
   252     callsign = repo.ui.config(b'phabricator', b'callsign')
   248     if not callsign:
   253     if not callsign:
   249         return None
   254         return None
   250     query = callconduit(repo, b'diffusion.repository.search',
   255     query = callconduit(repo, b'diffusion.repository.search',
   251                         {b'constraints': {b'callsigns': [callsign]}})
   256                         {b'constraints': {b'callsigns': [callsign]}})
   252     if len(query[r'data']) == 0:
   257     if len(query[b'data']) == 0:
   253         return None
   258         return None
   254     repophid = encoding.strtolocal(query[r'data'][0][r'phid'])
   259     repophid = query[b'data'][0][b'phid']
   255     repo.ui.setconfig(b'phabricator', b'repophid', repophid)
   260     repo.ui.setconfig(b'phabricator', b'repophid', repophid)
   256     return repophid
   261     return repophid
   257 
   262 
   258 _differentialrevisiontagre = re.compile(br'\AD([1-9][0-9]*)\Z')
   263 _differentialrevisiontagre = re.compile(br'\AD([1-9][0-9]*)\Z')
   259 _differentialrevisiondescre = re.compile(
   264 _differentialrevisiondescre = re.compile(
   303     # Phabricator, and expect precursors overlap with it.
   308     # Phabricator, and expect precursors overlap with it.
   304     if toconfirm:
   309     if toconfirm:
   305         drevs = [drev for force, precs, drev in toconfirm.values()]
   310         drevs = [drev for force, precs, drev in toconfirm.values()]
   306         alldiffs = callconduit(unfi, b'differential.querydiffs',
   311         alldiffs = callconduit(unfi, b'differential.querydiffs',
   307                                {b'revisionIDs': drevs})
   312                                {b'revisionIDs': drevs})
   308         getnode = lambda d: bin(encoding.unitolocal(
   313         getnode = lambda d: bin(
   309             getdiffmeta(d).get(r'node', b''))) or None
   314             getdiffmeta(d).get(b'node', b'')) or None
   310         for newnode, (force, precset, drev) in toconfirm.items():
   315         for newnode, (force, precset, drev) in toconfirm.items():
   311             diffs = [d for d in alldiffs.values()
   316             diffs = [d for d in alldiffs.values()
   312                      if int(d[r'revisionID']) == drev]
   317                      if int(d[b'revisionID']) == drev]
   313 
   318 
   314             # "precursors" as known by Phabricator
   319             # "precursors" as known by Phabricator
   315             phprecset = set(getnode(d) for d in diffs)
   320             phprecset = set(getnode(d) for d in diffs)
   316 
   321 
   317             # Ignore if precursors (Phabricator and local repo) do not overlap,
   322             # Ignore if precursors (Phabricator and local repo) do not overlap,
   326 
   331 
   327             # Find the last node using Phabricator metadata, and make sure it
   332             # Find the last node using Phabricator metadata, and make sure it
   328             # exists in the repo
   333             # exists in the repo
   329             oldnode = lastdiff = None
   334             oldnode = lastdiff = None
   330             if diffs:
   335             if diffs:
   331                 lastdiff = max(diffs, key=lambda d: int(d[r'id']))
   336                 lastdiff = max(diffs, key=lambda d: int(d[b'id']))
   332                 oldnode = getnode(lastdiff)
   337                 oldnode = getnode(lastdiff)
   333                 if oldnode and oldnode not in nodemap:
   338                 if oldnode and oldnode not in nodemap:
   334                     oldnode = None
   339                     oldnode = None
   335 
   340 
   336             result[newnode] = (oldnode, lastdiff, drev)
   341             result[newnode] = (oldnode, lastdiff, drev)
   359     return diff
   364     return diff
   360 
   365 
   361 def writediffproperties(ctx, diff):
   366 def writediffproperties(ctx, diff):
   362     """write metadata to diff so patches could be applied losslessly"""
   367     """write metadata to diff so patches could be applied losslessly"""
   363     params = {
   368     params = {
   364         b'diff_id': diff[r'id'],
   369         b'diff_id': diff[b'id'],
   365         b'name': b'hg:meta',
   370         b'name': b'hg:meta',
   366         b'data': json.dumps({
   371         b'data': json.dumps({
   367             b'user': ctx.user(),
   372             b'user': ctx.user(),
   368             b'date': b'%d %d' % ctx.date(),
   373             b'date': b'%d %d' % ctx.date(),
   369             b'node': ctx.hex(),
   374             b'node': ctx.hex(),
   371         }),
   376         }),
   372     }
   377     }
   373     callconduit(ctx.repo(), b'differential.setdiffproperty', params)
   378     callconduit(ctx.repo(), b'differential.setdiffproperty', params)
   374 
   379 
   375     params = {
   380     params = {
   376         b'diff_id': diff[r'id'],
   381         b'diff_id': diff[b'id'],
   377         b'name': b'local:commits',
   382         b'name': b'local:commits',
   378         b'data': json.dumps({
   383         b'data': json.dumps({
   379             ctx.hex(): {
   384             ctx.hex(): {
   380                 b'author': stringutil.person(ctx.user()),
   385                 b'author': stringutil.person(ctx.user()),
   381                 b'authorEmail': stringutil.email(ctx.user()),
   386                 b'authorEmail': stringutil.email(ctx.user()),
   406         neednewdiff = True
   411         neednewdiff = True
   407 
   412 
   408     transactions = []
   413     transactions = []
   409     if neednewdiff:
   414     if neednewdiff:
   410         diff = creatediff(ctx)
   415         diff = creatediff(ctx)
   411         transactions.append({b'type': b'update', b'value': diff[r'phid']})
   416         transactions.append({b'type': b'update', b'value': diff[b'phid']})
   412     else:
   417     else:
   413         # Even if we don't need to upload a new diff because the patch content
   418         # Even if we don't need to upload a new diff because the patch content
   414         # does not change. We might still need to update its metadata so
   419         # does not change. We might still need to update its metadata so
   415         # pushers could know the correct node metadata.
   420         # pushers could know the correct node metadata.
   416         assert olddiff
   421         assert olddiff
   431 
   436 
   432     # Parse commit message and update related fields.
   437     # Parse commit message and update related fields.
   433     desc = ctx.description()
   438     desc = ctx.description()
   434     info = callconduit(repo, b'differential.parsecommitmessage',
   439     info = callconduit(repo, b'differential.parsecommitmessage',
   435                        {b'corpus': desc})
   440                        {b'corpus': desc})
   436     for k, v in info[r'fields'].items():
   441     for k, v in info[b'fields'].items():
   437         if k in [b'title', b'summary', b'testPlan']:
   442         if k in [b'title', b'summary', b'testPlan']:
   438             transactions.append({b'type': k, b'value': v})
   443             transactions.append({b'type': k, b'value': v})
   439 
   444 
   440     params = {b'transactions': transactions}
   445     params = {b'transactions': transactions}
   441     if revid is not None:
   446     if revid is not None:
   453     names = [name.lower() for name in names]
   458     names = [name.lower() for name in names]
   454     query = {b'constraints': {b'usernames': names}}
   459     query = {b'constraints': {b'usernames': names}}
   455     result = callconduit(repo, b'user.search', query)
   460     result = callconduit(repo, b'user.search', query)
   456     # username not found is not an error of the API. So check if we have missed
   461     # username not found is not an error of the API. So check if we have missed
   457     # some names here.
   462     # some names here.
   458     data = result[r'data']
   463     data = result[b'data']
   459     resolved = set(entry[r'fields'][r'username'].lower() for entry in data)
   464     resolved = set(entry[b'fields'][b'username'].lower() for entry in data)
   460     unresolved = set(names) - resolved
   465     unresolved = set(names) - resolved
   461     if unresolved:
   466     if unresolved:
   462         raise error.Abort(_(b'unknown username: %s')
   467         raise error.Abort(_(b'unknown username: %s')
   463                           % b' '.join(sorted(unresolved)))
   468                           % b' '.join(sorted(unresolved)))
   464     return [entry[r'phid'] for entry in data]
   469     return [entry[b'phid'] for entry in data]
   465 
   470 
   466 @vcrcommand(b'phabsend',
   471 @vcrcommand(b'phabsend',
   467          [(b'r', b'rev', [], _(b'revisions to send'), _(b'REV')),
   472          [(b'r', b'rev', [], _(b'revisions to send'), _(b'REV')),
   468           (b'', b'amend', True, _(b'update commit messages')),
   473           (b'', b'amend', True, _(b'update commit messages')),
   469           (b'', b'reviewer', [], _(b'specify reviewers')),
   474           (b'', b'reviewer', [], _(b'specify reviewers')),
   536         if oldnode != ctx.node() or opts.get(b'amend'):
   541         if oldnode != ctx.node() or opts.get(b'amend'):
   537             # Create or update Differential Revision
   542             # Create or update Differential Revision
   538             revision, diff = createdifferentialrevision(
   543             revision, diff = createdifferentialrevision(
   539                 ctx, revid, lastrevid, oldnode, olddiff, actions)
   544                 ctx, revid, lastrevid, oldnode, olddiff, actions)
   540             diffmap[ctx.node()] = diff
   545             diffmap[ctx.node()] = diff
   541             newrevid = int(revision[r'object'][r'id'])
   546             newrevid = int(revision[b'object'][b'id'])
   542             if revid:
   547             if revid:
   543                 action = b'updated'
   548                 action = b'updated'
   544             else:
   549             else:
   545                 action = b'created'
   550                 action = b'created'
   546 
   551 
   578             wnode = unfi[b'.'].node()
   583             wnode = unfi[b'.'].node()
   579             mapping = {} # {oldnode: [newnode]}
   584             mapping = {} # {oldnode: [newnode]}
   580             for i, rev in enumerate(revs):
   585             for i, rev in enumerate(revs):
   581                 old = unfi[rev]
   586                 old = unfi[rev]
   582                 drevid = drevids[i]
   587                 drevid = drevids[i]
   583                 drev = [d for d in drevs if int(d[r'id']) == drevid][0]
   588                 drev = [d for d in drevs if int(d[b'id']) == drevid][0]
   584                 newdesc = getdescfromdrev(drev)
   589                 newdesc = getdescfromdrev(drev)
   585                 newdesc = encoding.unitolocal(newdesc)
       
   586                 # Make sure commit message contain "Differential Revision"
   590                 # Make sure commit message contain "Differential Revision"
   587                 if old.description() != newdesc:
   591                 if old.description() != newdesc:
   588                     if old.phase() == phases.public:
   592                     if old.phase() == phases.public:
   589                         ui.warn(_("warning: not updating public commit %s\n")
   593                         ui.warn(_("warning: not updating public commit %s\n")
   590                                 % scmutil.formatchangeid(old))
   594                                 % scmutil.formatchangeid(old))
   611             if wnode in mapping:
   615             if wnode in mapping:
   612                 unfi.setparents(mapping[wnode][0])
   616                 unfi.setparents(mapping[wnode][0])
   613 
   617 
   614 # Map from "hg:meta" keys to header understood by "hg import". The order is
   618 # Map from "hg:meta" keys to header understood by "hg import". The order is
   615 # consistent with "hg export" output.
   619 # consistent with "hg export" output.
   616 _metanamemap = util.sortdict([(r'user', b'User'), (r'date', b'Date'),
   620 _metanamemap = util.sortdict([(b'user', b'User'), (b'date', b'Date'),
   617                               (r'node', b'Node ID'), (r'parent', b'Parent ')])
   621                               (b'node', b'Node ID'), (b'parent', b'Parent ')])
   618 
   622 
   619 def _confirmbeforesend(repo, revs, oldmap):
   623 def _confirmbeforesend(repo, revs, oldmap):
   620     url, token = readurltoken(repo)
   624     url, token = readurltoken(repo)
   621     ui = repo.ui
   625     ui = repo.ui
   622     for rev in revs:
   626     for rev in revs:
   642 _knownstatusnames = {b'accepted', b'needsreview', b'needsrevision', b'closed',
   646 _knownstatusnames = {b'accepted', b'needsreview', b'needsrevision', b'closed',
   643                      b'abandoned'}
   647                      b'abandoned'}
   644 
   648 
   645 def _getstatusname(drev):
   649 def _getstatusname(drev):
   646     """get normalized status name from a Differential Revision"""
   650     """get normalized status name from a Differential Revision"""
   647     return drev[r'statusName'].replace(b' ', b'').lower()
   651     return drev[b'statusName'].replace(b' ', b'').lower()
   648 
   652 
   649 # Small language to specify differential revisions. Support symbols: (), :X,
   653 # Small language to specify differential revisions. Support symbols: (), :X,
   650 # +, and -.
   654 # +, and -.
   651 
   655 
   652 _elements = {
   656 _elements = {
   760         if key in prefetched:
   764         if key in prefetched:
   761             return prefetched[key]
   765             return prefetched[key]
   762         drevs = callconduit(repo, b'differential.query', params)
   766         drevs = callconduit(repo, b'differential.query', params)
   763         # Fill prefetched with the result
   767         # Fill prefetched with the result
   764         for drev in drevs:
   768         for drev in drevs:
   765             prefetched[drev[r'phid']] = drev
   769             prefetched[drev[b'phid']] = drev
   766             prefetched[int(drev[r'id'])] = drev
   770             prefetched[int(drev[b'id'])] = drev
   767         if key not in prefetched:
   771         if key not in prefetched:
   768             raise error.Abort(_(b'cannot get Differential Revision %r')
   772             raise error.Abort(_(b'cannot get Differential Revision %r')
   769                               % params)
   773                               % params)
   770         return prefetched[key]
   774         return prefetched[key]
   771 
   775 
   775         result = []
   779         result = []
   776         queue = [{r'ids': [i]} for i in topdrevids]
   780         queue = [{r'ids': [i]} for i in topdrevids]
   777         while queue:
   781         while queue:
   778             params = queue.pop()
   782             params = queue.pop()
   779             drev = fetch(params)
   783             drev = fetch(params)
   780             if drev[r'id'] in visited:
   784             if drev[b'id'] in visited:
   781                 continue
   785                 continue
   782             visited.add(drev[r'id'])
   786             visited.add(drev[b'id'])
   783             result.append(int(drev[r'id']))
   787             result.append(int(drev[b'id']))
   784             auxiliary = drev.get(r'auxiliary', {})
   788             auxiliary = drev.get(b'auxiliary', {})
   785             depends = auxiliary.get(r'phabricator:depends-on', [])
   789             depends = auxiliary.get(b'phabricator:depends-on', [])
   786             for phid in depends:
   790             for phid in depends:
   787                 queue.append({b'phids': [phid]})
   791                 queue.append({b'phids': [phid]})
   788         result.reverse()
   792         result.reverse()
   789         return smartset.baseset(result)
   793         return smartset.baseset(result)
   790 
   794 
   800     # Prefetch Differential Revisions in batch
   804     # Prefetch Differential Revisions in batch
   801     tofetch = set(drevs)
   805     tofetch = set(drevs)
   802     for r in ancestordrevs:
   806     for r in ancestordrevs:
   803         tofetch.update(range(max(1, r - batchsize), r + 1))
   807         tofetch.update(range(max(1, r - batchsize), r + 1))
   804     if drevs:
   808     if drevs:
   805         fetch({r'ids': list(tofetch)})
   809         fetch({b'ids': list(tofetch)})
   806     validids = sorted(set(getstack(list(ancestordrevs))) | set(drevs))
   810     validids = sorted(set(getstack(list(ancestordrevs))) | set(drevs))
   807 
   811 
   808     # Walk through the tree, return smartsets
   812     # Walk through the tree, return smartsets
   809     def walk(tree):
   813     def walk(tree):
   810         op = tree[0]
   814         op = tree[0]
   834     """get description (commit message) from "Differential Revision"
   838     """get description (commit message) from "Differential Revision"
   835 
   839 
   836     This is similar to differential.getcommitmessage API. But we only care
   840     This is similar to differential.getcommitmessage API. But we only care
   837     about limited fields: title, summary, test plan, and URL.
   841     about limited fields: title, summary, test plan, and URL.
   838     """
   842     """
   839     title = drev[r'title']
   843     title = drev[b'title']
   840     summary = drev[r'summary'].rstrip()
   844     summary = drev[b'summary'].rstrip()
   841     testplan = drev[r'testPlan'].rstrip()
   845     testplan = drev[b'testPlan'].rstrip()
   842     if testplan:
   846     if testplan:
   843         testplan = b'Test Plan:\n%s' % testplan
   847         testplan = b'Test Plan:\n%s' % testplan
   844     uri = b'Differential Revision: %s' % drev[r'uri']
   848     uri = b'Differential Revision: %s' % drev[b'uri']
   845     return b'\n\n'.join(filter(None, [title, summary, testplan, uri]))
   849     return b'\n\n'.join(filter(None, [title, summary, testplan, uri]))
   846 
   850 
   847 def getdiffmeta(diff):
   851 def getdiffmeta(diff):
   848     """get commit metadata (date, node, user, p1) from a diff object
   852     """get commit metadata (date, node, user, p1) from a diff object
   849 
   853 
   879         }
   883         }
   880 
   884 
   881     Note: metadata extracted from "local:commits" will lose time zone
   885     Note: metadata extracted from "local:commits" will lose time zone
   882     information.
   886     information.
   883     """
   887     """
   884     props = diff.get(r'properties') or {}
   888     props = diff.get(b'properties') or {}
   885     meta = props.get(r'hg:meta')
   889     meta = props.get(b'hg:meta')
   886     if not meta and props.get(r'local:commits'):
   890     if not meta and props.get(b'local:commits'):
   887         commit = sorted(props[r'local:commits'].values())[0]
   891         commit = sorted(props[b'local:commits'].values())[0]
   888         meta = {
   892         meta = {
   889             r'date': r'%d 0' % commit[r'time'],
   893             b'date': b'%d 0' % commit[b'time'],
   890             r'node': commit[r'rev'],
   894             b'node': commit[b'rev'],
   891             r'user': r'%s <%s>' % (commit[r'author'], commit[r'authorEmail']),
   895             b'user': b'%s <%s>' % (commit[b'author'], commit[b'authorEmail']),
   892         }
   896         }
   893         if len(commit.get(r'parents', ())) >= 1:
   897         if len(commit.get(b'parents', ())) >= 1:
   894             meta[r'parent'] = commit[r'parents'][0]
   898             meta[b'parent'] = commit[b'parents'][0]
   895     return meta or {}
   899     return meta or {}
   896 
   900 
   897 def readpatch(repo, drevs, write):
   901 def readpatch(repo, drevs, write):
   898     """generate plain-text patch readable by 'hg import'
   902     """generate plain-text patch readable by 'hg import'
   899 
   903 
   900     write is usually ui.write. drevs is what "querydrev" returns, results of
   904     write is usually ui.write. drevs is what "querydrev" returns, results of
   901     "differential.query".
   905     "differential.query".
   902     """
   906     """
   903     # Prefetch hg:meta property for all diffs
   907     # Prefetch hg:meta property for all diffs
   904     diffids = sorted(set(max(int(v) for v in drev[r'diffs']) for drev in drevs))
   908     diffids = sorted(set(max(int(v) for v in drev[b'diffs']) for drev in drevs))
   905     diffs = callconduit(repo, b'differential.querydiffs', {b'ids': diffids})
   909     diffs = callconduit(repo, b'differential.querydiffs', {b'ids': diffids})
   906 
   910 
   907     # Generate patch for each drev
   911     # Generate patch for each drev
   908     for drev in drevs:
   912     for drev in drevs:
   909         repo.ui.note(_(b'reading D%s\n') % drev[r'id'])
   913         repo.ui.note(_(b'reading D%s\n') % drev[b'id'])
   910 
   914 
   911         diffid = max(int(v) for v in drev[r'diffs'])
   915         diffid = max(int(v) for v in drev[b'diffs'])
   912         body = callconduit(repo, b'differential.getrawdiff',
   916         body = callconduit(repo, b'differential.getrawdiff',
   913                            {b'diffID': diffid})
   917                            {b'diffID': diffid})
   914         desc = getdescfromdrev(drev)
   918         desc = getdescfromdrev(drev)
   915         header = b'# HG changeset patch\n'
   919         header = b'# HG changeset patch\n'
   916 
   920 
   921         for k in _metanamemap.keys():
   925         for k in _metanamemap.keys():
   922             if k in meta:
   926             if k in meta:
   923                 header += b'# %s %s\n' % (_metanamemap[k], meta[k])
   927                 header += b'# %s %s\n' % (_metanamemap[k], meta[k])
   924 
   928 
   925         content = b'%s%s\n%s' % (header, desc, body)
   929         content = b'%s%s\n%s' % (header, desc, body)
   926         write(encoding.unitolocal(content))
   930         write(content)
   927 
   931 
   928 @vcrcommand(b'phabread',
   932 @vcrcommand(b'phabread',
   929          [(b'', b'stack', False, _(b'read dependencies'))],
   933          [(b'', b'stack', False, _(b'read dependencies'))],
   930          _(b'DREVSPEC [OPTIONS]'),
   934          _(b'DREVSPEC [OPTIONS]'),
   931          helpcategory=command.CATEGORY_IMPORT_EXPORT)
   935          helpcategory=command.CATEGORY_IMPORT_EXPORT)
   977     drevs = querydrev(repo, spec)
   981     drevs = querydrev(repo, spec)
   978     for i, drev in enumerate(drevs):
   982     for i, drev in enumerate(drevs):
   979         if i + 1 == len(drevs) and opts.get(b'comment'):
   983         if i + 1 == len(drevs) and opts.get(b'comment'):
   980             actions.append({b'type': b'comment', b'value': opts[b'comment']})
   984             actions.append({b'type': b'comment', b'value': opts[b'comment']})
   981         if actions:
   985         if actions:
   982             params = {b'objectIdentifier': drev[r'phid'],
   986             params = {b'objectIdentifier': drev[b'phid'],
   983                       b'transactions': actions}
   987                       b'transactions': actions}
   984             callconduit(repo, b'differential.revision.edit', params)
   988             callconduit(repo, b'differential.revision.edit', params)
   985 
   989 
   986 templatekeyword = registrar.templatekeyword()
   990 templatekeyword = registrar.templatekeyword()
   987 
   991