comparison hgext/phabricator.py @ 41897:c340a8ac7ef3

phabricator: convert conduit response JSON unicode to bytes inside callconduit Previously the byte conversion was happening piecemeal in callers, and in the case of createdifferentialrevision not at all, leading to UnicodeEncodeErrors when trying to phabsend a commit with a description containing characters not representable in ascii. (issue6040) Remove all the scattered encoding.unitolocal calls and perform it once, inside callconduit, on the entire response dict recursively before returning it, in keeping with the strategy of converting at the earliest opportunity. Convert all keys used on returned object to bytes. Modify the phabsend tests to test this by adding a € to the commit message of alpha. Differential Revision: https://phab.mercurial-scm.org/D6044
author Ian Moody <moz-ian@perix.co.uk>
date Sat, 02 Mar 2019 18:48:23 +0000
parents 570e62f1dcf2
children 2bad8f92cebf
comparison
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