##// END OF EJS Templates
phabricator: add a small language to query Differential Revisions...
Jun Wu -
r33831:53954177 default
parent child Browse files
Show More
@@ -32,7 +32,9 b' Config::'
32
32
33 from __future__ import absolute_import
33 from __future__ import absolute_import
34
34
35 import itertools
35 import json
36 import json
37 import operator
36 import re
38 import re
37
39
38 from mercurial.node import bin, nullid
40 from mercurial.node import bin, nullid
@@ -44,9 +46,11 b' from mercurial import ('
44 error,
46 error,
45 mdiff,
47 mdiff,
46 obsutil,
48 obsutil,
49 parser,
47 patch,
50 patch,
48 registrar,
51 registrar,
49 scmutil,
52 scmutil,
53 smartset,
50 tags,
54 tags,
51 url as urlmod,
55 url as urlmod,
52 util,
56 util,
@@ -484,11 +488,77 b' def _confirmbeforesend(repo, revs):'
484
488
485 return True
489 return True
486
490
487 def querydrev(repo, params, stack=False):
491 # Small language to specify differential revisions. Support symbols: (), :X,
492 # +, and -.
493
494 _elements = {
495 # token-type: binding-strength, primary, prefix, infix, suffix
496 '(': (12, None, ('group', 1, ')'), None, None),
497 ':': (8, None, ('ancestors', 8), None, None),
498 '&': (5, None, None, ('and_', 5), None),
499 '+': (4, None, None, ('add', 4), None),
500 '-': (4, None, None, ('sub', 4), None),
501 ')': (0, None, None, None, None),
502 'symbol': (0, 'symbol', None, None, None),
503 'end': (0, None, None, None, None),
504 }
505
506 def _tokenize(text):
507 text = text.replace(' ', '') # remove space
508 view = memoryview(text) # zero-copy slice
509 special = '():+-&'
510 pos = 0
511 length = len(text)
512 while pos < length:
513 symbol = ''.join(itertools.takewhile(lambda ch: ch not in special,
514 view[pos:]))
515 if symbol:
516 yield ('symbol', symbol, pos)
517 pos += len(symbol)
518 else: # special char
519 yield (text[pos], None, pos)
520 pos += 1
521 yield ('end', None, pos)
522
523 def _parse(text):
524 tree, pos = parser.parser(_elements).parse(_tokenize(text))
525 if pos != len(text):
526 raise error.ParseError('invalid token', pos)
527 return tree
528
529 def _parsedrev(symbol):
530 """str -> int or None, ex. 'D45' -> 45; '12' -> 12; 'x' -> None"""
531 if symbol.startswith('D') and symbol[1:].isdigit():
532 return int(symbol[1:])
533 if symbol.isdigit():
534 return int(symbol)
535
536 def _prefetchdrevs(tree):
537 """return ({single-drev-id}, {ancestor-drev-id}) to prefetch"""
538 drevs = set()
539 ancestordrevs = set()
540 op = tree[0]
541 if op == 'symbol':
542 r = _parsedrev(tree[1])
543 if r:
544 drevs.add(r)
545 elif op == 'ancestors':
546 r, a = _prefetchdrevs(tree[1])
547 drevs.update(r)
548 ancestordrevs.update(r)
549 ancestordrevs.update(a)
550 else:
551 for t in tree[1:]:
552 r, a = _prefetchdrevs(t)
553 drevs.update(r)
554 ancestordrevs.update(a)
555 return drevs, ancestordrevs
556
557 def querydrev(repo, spec):
488 """return a list of "Differential Revision" dicts
558 """return a list of "Differential Revision" dicts
489
559
490 params is the input of "differential.query" API, and is expected to match
560 spec is a string using a simple query language, see docstring in phabread
491 just a single Differential Revision.
561 for details.
492
562
493 A "Differential Revision dict" looks like:
563 A "Differential Revision dict" looks like:
494
564
@@ -525,26 +595,13 b' def querydrev(repo, params, stack=False)'
525 "repositoryPHID": "PHID-REPO-hub2hx62ieuqeheznasv",
595 "repositoryPHID": "PHID-REPO-hub2hx62ieuqeheznasv",
526 "sourcePath": null
596 "sourcePath": null
527 }
597 }
528
529 If stack is True, return a list of "Differential Revision dict"s in an
530 order that the latter ones depend on the former ones. Otherwise, return a
531 list of a unique "Differential Revision dict".
532 """
598 """
533 prefetched = {} # {id or phid: drev}
534 def fetch(params):
599 def fetch(params):
535 """params -> single drev or None"""
600 """params -> single drev or None"""
536 key = (params.get(r'ids') or params.get(r'phids') or [None])[0]
601 key = (params.get(r'ids') or params.get(r'phids') or [None])[0]
537 if key in prefetched:
602 if key in prefetched:
538 return prefetched[key]
603 return prefetched[key]
539 # Otherwise, send the request. If we're fetching a stack, be smarter
604 drevs = callconduit(repo, 'differential.query', params)
540 # and fetch more ids in one batch, even if it could be unnecessary.
541 batchparams = params
542 if stack and len(params.get(r'ids', [])) == 1:
543 i = int(params[r'ids'][0])
544 # developer config: phabricator.batchsize
545 batchsize = repo.ui.configint('phabricator', 'batchsize', 12)
546 batchparams = {'ids': range(max(1, i - batchsize), i + 1)}
547 drevs = callconduit(repo, 'differential.query', batchparams)
548 # Fill prefetched with the result
605 # Fill prefetched with the result
549 for drev in drevs:
606 for drev in drevs:
550 prefetched[drev[r'phid']] = drev
607 prefetched[drev[r'phid']] = drev
@@ -553,23 +610,62 b' def querydrev(repo, params, stack=False)'
553 raise error.Abort(_('cannot get Differential Revision %r') % params)
610 raise error.Abort(_('cannot get Differential Revision %r') % params)
554 return prefetched[key]
611 return prefetched[key]
555
612
556 visited = set()
613 def getstack(topdrevids):
557 result = []
614 """given a top, get a stack from the bottom, [id] -> [id]"""
558 queue = [params]
615 visited = set()
559 while queue:
616 result = []
560 params = queue.pop()
617 queue = [{r'ids': [i]} for i in topdrevids]
561 drev = fetch(params)
618 while queue:
562 if drev[r'id'] in visited:
619 params = queue.pop()
563 continue
620 drev = fetch(params)
564 visited.add(drev[r'id'])
621 if drev[r'id'] in visited:
565 result.append(drev)
622 continue
566 if stack:
623 visited.add(drev[r'id'])
624 result.append(int(drev[r'id']))
567 auxiliary = drev.get(r'auxiliary', {})
625 auxiliary = drev.get(r'auxiliary', {})
568 depends = auxiliary.get(r'phabricator:depends-on', [])
626 depends = auxiliary.get(r'phabricator:depends-on', [])
569 for phid in depends:
627 for phid in depends:
570 queue.append({'phids': [phid]})
628 queue.append({'phids': [phid]})
571 result.reverse()
629 result.reverse()
572 return result
630 return smartset.baseset(result)
631
632 # Initialize prefetch cache
633 prefetched = {} # {id or phid: drev}
634
635 tree = _parse(spec)
636 drevs, ancestordrevs = _prefetchdrevs(tree)
637
638 # developer config: phabricator.batchsize
639 batchsize = repo.ui.configint('phabricator', 'batchsize', 12)
640
641 # Prefetch Differential Revisions in batch
642 tofetch = set(drevs)
643 for r in ancestordrevs:
644 tofetch.update(range(max(1, r - batchsize), r + 1))
645 if drevs:
646 fetch({r'ids': list(tofetch)})
647 getstack(list(ancestordrevs))
648
649 # Walk through the tree, return smartsets
650 def walk(tree):
651 op = tree[0]
652 if op == 'symbol':
653 drev = _parsedrev(tree[1])
654 if drev:
655 return smartset.baseset([drev])
656 else:
657 raise error.Abort(_('unknown symbol: %s') % tree[1])
658 elif op in {'and_', 'add', 'sub'}:
659 assert len(tree) == 3
660 return getattr(operator, op)(walk(tree[1]), walk(tree[2]))
661 elif op == 'group':
662 return walk(tree[1])
663 elif op == 'ancestors':
664 return getstack(walk(tree[1]))
665 else:
666 raise error.ProgrammingError('illegal tree: %r' % tree)
667
668 return [prefetched[r] for r in walk(tree)]
573
669
574 def getdescfromdrev(drev):
670 def getdescfromdrev(drev):
575 """get description (commit message) from "Differential Revision"
671 """get description (commit message) from "Differential Revision"
@@ -667,18 +763,22 b' def readpatch(repo, drevs, write):'
667
763
668 @command('phabread',
764 @command('phabread',
669 [('', 'stack', False, _('read dependencies'))],
765 [('', 'stack', False, _('read dependencies'))],
670 _('REVID [OPTIONS]'))
766 _('DREVSPEC [OPTIONS]'))
671 def phabread(ui, repo, revid, **opts):
767 def phabread(ui, repo, spec, **opts):
672 """print patches from Phabricator suitable for importing
768 """print patches from Phabricator suitable for importing
673
769
674 REVID could be a Differential Revision identity, like ``D123``, or just the
770 DREVSPEC could be a Differential Revision identity, like ``D123``, or just
675 number ``123``, or a full URL like ``https://phab.example.com/D123``.
771 the number ``123``. It could also have common operators like ``+``, ``-``,
772 ``&``, ``(``, ``)`` for complex queries. Prefix ``:`` could be used to
773 select a stack.
774
775 For example, ``:D6+8-(2+D4)`` selects a stack up to D6, plus D8 and exclude
776 D2 and D4.
676
777
677 If --stack is given, follow dependencies information and read all patches.
778 If --stack is given, follow dependencies information and read all patches.
779 It is equivalent to the ``:`` operator.
678 """
780 """
679 try:
781 if opts.get('stack'):
680 revid = int(revid.split('/')[-1].replace('D', ''))
782 spec = ':(%s)' % spec
681 except ValueError:
783 drevs = querydrev(repo, spec)
682 raise error.Abort(_('invalid Revision ID: %s') % revid)
683 drevs = querydrev(repo, {'ids': [revid]}, opts.get('stack'))
684 readpatch(repo, drevs, ui.write)
784 readpatch(repo, drevs, ui.write)
General Comments 0
You need to be logged in to leave comments. Login now