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 |