D125: phabricator: add a small language to query Differential Revisions

2017-08-01 Thread quark (Jun Wu)
quark updated this revision to Diff 487.

REPOSITORY
  rHG Mercurial

CHANGES SINCE LAST UPDATE
  https://phab.mercurial-scm.org/D125?vs=237&id=487

REVISION DETAIL
  https://phab.mercurial-scm.org/D125

AFFECTED FILES
  contrib/phabricator.py

CHANGE DETAILS

diff --git a/contrib/phabricator.py b/contrib/phabricator.py
--- a/contrib/phabricator.py
+++ b/contrib/phabricator.py
@@ -32,7 +32,9 @@
 
 from __future__ import absolute_import
 
+import itertools
 import json
+import operator
 import re
 
 from mercurial.node import bin, nullid
@@ -44,9 +46,11 @@
 error,
 mdiff,
 obsolete,
+parser,
 patch,
 registrar,
 scmutil,
+smartset,
 tags,
 url as urlmod,
 util,
@@ -434,11 +438,77 @@
 _metanamemap = util.sortdict([(r'user', 'User'), (r'date', 'Date'),
   (r'node', 'Node ID'), (r'parent', 'Parent ')])
 
-def querydrev(repo, params, stack=False):
+# Small language to specify differential revisions. Support symbols: (), :X,
+# +, and -.
+
+_elements = {
+# token-type: binding-strength, primary, prefix, infix, suffix
+'(':  (12, None, ('group', 1, ')'), None, None),
+':':  (8, None, ('ancestors', 8), None, None),
+'&':  (5,  None, None, ('and_', 5), None),
+'+':  (4,  None, None, ('add', 4), None),
+'-':  (4,  None, None, ('sub', 4), None),
+')':  (0,  None, None, None, None),
+'symbol': (0, 'symbol', None, None, None),
+'end':(0, None, None, None, None),
+}
+
+def _tokenize(text):
+text = text.replace(' ', '') # remove space
+view = memoryview(text) # zero-copy slice
+special = '():+-&'
+pos = 0
+length = len(text)
+while pos < length:
+symbol = ''.join(itertools.takewhile(lambda ch: ch not in special,
+ view[pos:]))
+if symbol:
+yield ('symbol', symbol, pos)
+pos += len(symbol)
+else: # special char
+yield (text[pos], None, pos)
+pos += 1
+yield ('end', None, pos)
+
+def _parse(text):
+tree, pos = parser.parser(_elements).parse(_tokenize(text))
+if pos != len(text):
+raise error.ParseError('invalid token', pos)
+return tree
+
+def _parsedrev(symbol):
+"""str -> int or None, ex. 'D45' -> 45; '12' -> 12; 'x' -> None"""
+if symbol.startswith('D') and symbol[1:].isdigit():
+return int(symbol[1:])
+if symbol.isdigit():
+return int(symbol)
+
+def _prefetchdrevs(tree):
+"""return ({single-drev-id}, {ancestor-drev-id}) to prefetch"""
+drevs = set()
+ancestordrevs = set()
+op = tree[0]
+if op == 'symbol':
+r = _parsedrev(tree[1])
+if r:
+drevs.add(r)
+elif op == 'ancestors':
+r, a = _prefetchdrevs(tree[1])
+drevs.update(r)
+ancestordrevs.update(r)
+ancestordrevs.update(a)
+else:
+for t in tree[1:]:
+r, a = _prefetchdrevs(t)
+drevs.update(r)
+ancestordrevs.update(a)
+return drevs, ancestordrevs
+
+def querydrev(repo, spec):
 """return a list of "Differential Revision" dicts
 
-params is the input of "differential.query" API, and is expected to match
-just a single Differential Revision.
+spec is a string using a simple query language, see docstring in phabread
+for details.
 
 A "Differential Revision dict" looks like:
 
@@ -475,51 +545,77 @@
 "repositoryPHID": "PHID-REPO-hub2hx62ieuqeheznasv",
 "sourcePath": null
 }
-
-If stack is True, return a list of "Differential Revision dict"s in an
-order that the latter ones depend on the former ones. Otherwise, return a
-list of a unique "Differential Revision dict".
 """
-prefetched = {} # {id or phid: drev}
 def fetch(params):
 """params -> single drev or None"""
 key = (params.get(r'ids') or params.get(r'phids') or [None])[0]
 if key in prefetched:
 return prefetched[key]
-# Otherwise, send the request. If we're fetching a stack, be smarter
-# and fetch more ids in one batch, even if it could be unnecessary.
-batchparams = params
-if stack and len(params.get(r'ids', [])) == 1:
-i = int(params[r'ids'][0])
-# developer config: phabricator.batchsize
-batchsize = repo.ui.configint('phabricator', 'batchsize', 12)
-batchparams = {'ids': range(max(1, i - batchsize), i + 1)}
-drevs = callconduit(repo, 'differential.query', batchparams)
+drevs = callconduit(repo, 'differential.query', params)
 # Fill prefetched with the result
 for drev in drevs:
 prefetched[drev[r'phid']] = drev
 prefetched[int(drev[r'id'])] = drev
 if key not in prefetched:
 raise error.Abort(_('cannot get Differential Revision %r') % 
params)
 return prefetched[key]
 
-visited = 

D125: phabricator: add a small language to query Differential Revisions

2017-08-04 Thread quark (Jun Wu)
quark updated this revision to Diff 555.

REPOSITORY
  rHG Mercurial

CHANGES SINCE LAST UPDATE
  https://phab.mercurial-scm.org/D125?vs=487&id=555

REVISION DETAIL
  https://phab.mercurial-scm.org/D125

AFFECTED FILES
  contrib/phabricator.py

CHANGE DETAILS

diff --git a/contrib/phabricator.py b/contrib/phabricator.py
--- a/contrib/phabricator.py
+++ b/contrib/phabricator.py
@@ -32,7 +32,9 @@
 
 from __future__ import absolute_import
 
+import itertools
 import json
+import operator
 import re
 
 from mercurial.node import bin, nullid
@@ -44,9 +46,11 @@
 error,
 mdiff,
 obsolete,
+parser,
 patch,
 registrar,
 scmutil,
+smartset,
 tags,
 url as urlmod,
 util,
@@ -451,11 +455,77 @@
 _metanamemap = util.sortdict([(r'user', 'User'), (r'date', 'Date'),
   (r'node', 'Node ID'), (r'parent', 'Parent ')])
 
-def querydrev(repo, params, stack=False):
+# Small language to specify differential revisions. Support symbols: (), :X,
+# +, and -.
+
+_elements = {
+# token-type: binding-strength, primary, prefix, infix, suffix
+'(':  (12, None, ('group', 1, ')'), None, None),
+':':  (8, None, ('ancestors', 8), None, None),
+'&':  (5,  None, None, ('and_', 5), None),
+'+':  (4,  None, None, ('add', 4), None),
+'-':  (4,  None, None, ('sub', 4), None),
+')':  (0,  None, None, None, None),
+'symbol': (0, 'symbol', None, None, None),
+'end':(0, None, None, None, None),
+}
+
+def _tokenize(text):
+text = text.replace(' ', '') # remove space
+view = memoryview(text) # zero-copy slice
+special = '():+-&'
+pos = 0
+length = len(text)
+while pos < length:
+symbol = ''.join(itertools.takewhile(lambda ch: ch not in special,
+ view[pos:]))
+if symbol:
+yield ('symbol', symbol, pos)
+pos += len(symbol)
+else: # special char
+yield (text[pos], None, pos)
+pos += 1
+yield ('end', None, pos)
+
+def _parse(text):
+tree, pos = parser.parser(_elements).parse(_tokenize(text))
+if pos != len(text):
+raise error.ParseError('invalid token', pos)
+return tree
+
+def _parsedrev(symbol):
+"""str -> int or None, ex. 'D45' -> 45; '12' -> 12; 'x' -> None"""
+if symbol.startswith('D') and symbol[1:].isdigit():
+return int(symbol[1:])
+if symbol.isdigit():
+return int(symbol)
+
+def _prefetchdrevs(tree):
+"""return ({single-drev-id}, {ancestor-drev-id}) to prefetch"""
+drevs = set()
+ancestordrevs = set()
+op = tree[0]
+if op == 'symbol':
+r = _parsedrev(tree[1])
+if r:
+drevs.add(r)
+elif op == 'ancestors':
+r, a = _prefetchdrevs(tree[1])
+drevs.update(r)
+ancestordrevs.update(r)
+ancestordrevs.update(a)
+else:
+for t in tree[1:]:
+r, a = _prefetchdrevs(t)
+drevs.update(r)
+ancestordrevs.update(a)
+return drevs, ancestordrevs
+
+def querydrev(repo, spec):
 """return a list of "Differential Revision" dicts
 
-params is the input of "differential.query" API, and is expected to match
-just a single Differential Revision.
+spec is a string using a simple query language, see docstring in phabread
+for details.
 
 A "Differential Revision dict" looks like:
 
@@ -492,51 +562,77 @@
 "repositoryPHID": "PHID-REPO-hub2hx62ieuqeheznasv",
 "sourcePath": null
 }
-
-If stack is True, return a list of "Differential Revision dict"s in an
-order that the latter ones depend on the former ones. Otherwise, return a
-list of a unique "Differential Revision dict".
 """
-prefetched = {} # {id or phid: drev}
 def fetch(params):
 """params -> single drev or None"""
 key = (params.get(r'ids') or params.get(r'phids') or [None])[0]
 if key in prefetched:
 return prefetched[key]
-# Otherwise, send the request. If we're fetching a stack, be smarter
-# and fetch more ids in one batch, even if it could be unnecessary.
-batchparams = params
-if stack and len(params.get(r'ids', [])) == 1:
-i = int(params[r'ids'][0])
-# developer config: phabricator.batchsize
-batchsize = repo.ui.configint('phabricator', 'batchsize', 12)
-batchparams = {'ids': range(max(1, i - batchsize), i + 1)}
-drevs = callconduit(repo, 'differential.query', batchparams)
+drevs = callconduit(repo, 'differential.query', params)
 # Fill prefetched with the result
 for drev in drevs:
 prefetched[drev[r'phid']] = drev
 prefetched[int(drev[r'id'])] = drev
 if key not in prefetched:
 raise error.Abort(_('cannot get Differential Revision %r') % 
params)
 return prefetched[key]
 
-visited = 

D125: phabricator: add a small language to query Differential Revisions

2017-08-04 Thread quark (Jun Wu)
quark updated this revision to Diff 570.

REPOSITORY
  rHG Mercurial

CHANGES SINCE LAST UPDATE
  https://phab.mercurial-scm.org/D125?vs=555&id=570

REVISION DETAIL
  https://phab.mercurial-scm.org/D125

AFFECTED FILES
  contrib/phabricator.py

CHANGE DETAILS

diff --git a/contrib/phabricator.py b/contrib/phabricator.py
--- a/contrib/phabricator.py
+++ b/contrib/phabricator.py
@@ -32,7 +32,9 @@
 
 from __future__ import absolute_import
 
+import itertools
 import json
+import operator
 import re
 
 from mercurial.node import bin, nullid
@@ -44,9 +46,11 @@
 error,
 mdiff,
 obsolete,
+parser,
 patch,
 registrar,
 scmutil,
+smartset,
 tags,
 url as urlmod,
 util,
@@ -485,11 +489,77 @@
 
 return True
 
-def querydrev(repo, params, stack=False):
+# Small language to specify differential revisions. Support symbols: (), :X,
+# +, and -.
+
+_elements = {
+# token-type: binding-strength, primary, prefix, infix, suffix
+'(':  (12, None, ('group', 1, ')'), None, None),
+':':  (8, None, ('ancestors', 8), None, None),
+'&':  (5,  None, None, ('and_', 5), None),
+'+':  (4,  None, None, ('add', 4), None),
+'-':  (4,  None, None, ('sub', 4), None),
+')':  (0,  None, None, None, None),
+'symbol': (0, 'symbol', None, None, None),
+'end':(0, None, None, None, None),
+}
+
+def _tokenize(text):
+text = text.replace(' ', '') # remove space
+view = memoryview(text) # zero-copy slice
+special = '():+-&'
+pos = 0
+length = len(text)
+while pos < length:
+symbol = ''.join(itertools.takewhile(lambda ch: ch not in special,
+ view[pos:]))
+if symbol:
+yield ('symbol', symbol, pos)
+pos += len(symbol)
+else: # special char
+yield (text[pos], None, pos)
+pos += 1
+yield ('end', None, pos)
+
+def _parse(text):
+tree, pos = parser.parser(_elements).parse(_tokenize(text))
+if pos != len(text):
+raise error.ParseError('invalid token', pos)
+return tree
+
+def _parsedrev(symbol):
+"""str -> int or None, ex. 'D45' -> 45; '12' -> 12; 'x' -> None"""
+if symbol.startswith('D') and symbol[1:].isdigit():
+return int(symbol[1:])
+if symbol.isdigit():
+return int(symbol)
+
+def _prefetchdrevs(tree):
+"""return ({single-drev-id}, {ancestor-drev-id}) to prefetch"""
+drevs = set()
+ancestordrevs = set()
+op = tree[0]
+if op == 'symbol':
+r = _parsedrev(tree[1])
+if r:
+drevs.add(r)
+elif op == 'ancestors':
+r, a = _prefetchdrevs(tree[1])
+drevs.update(r)
+ancestordrevs.update(r)
+ancestordrevs.update(a)
+else:
+for t in tree[1:]:
+r, a = _prefetchdrevs(t)
+drevs.update(r)
+ancestordrevs.update(a)
+return drevs, ancestordrevs
+
+def querydrev(repo, spec):
 """return a list of "Differential Revision" dicts
 
-params is the input of "differential.query" API, and is expected to match
-just a single Differential Revision.
+spec is a string using a simple query language, see docstring in phabread
+for details.
 
 A "Differential Revision dict" looks like:
 
@@ -526,51 +596,77 @@
 "repositoryPHID": "PHID-REPO-hub2hx62ieuqeheznasv",
 "sourcePath": null
 }
-
-If stack is True, return a list of "Differential Revision dict"s in an
-order that the latter ones depend on the former ones. Otherwise, return a
-list of a unique "Differential Revision dict".
 """
-prefetched = {} # {id or phid: drev}
 def fetch(params):
 """params -> single drev or None"""
 key = (params.get(r'ids') or params.get(r'phids') or [None])[0]
 if key in prefetched:
 return prefetched[key]
-# Otherwise, send the request. If we're fetching a stack, be smarter
-# and fetch more ids in one batch, even if it could be unnecessary.
-batchparams = params
-if stack and len(params.get(r'ids', [])) == 1:
-i = int(params[r'ids'][0])
-# developer config: phabricator.batchsize
-batchsize = repo.ui.configint('phabricator', 'batchsize', 12)
-batchparams = {'ids': range(max(1, i - batchsize), i + 1)}
-drevs = callconduit(repo, 'differential.query', batchparams)
+drevs = callconduit(repo, 'differential.query', params)
 # Fill prefetched with the result
 for drev in drevs:
 prefetched[drev[r'phid']] = drev
 prefetched[int(drev[r'id'])] = drev
 if key not in prefetched:
 raise error.Abort(_('cannot get Differential Revision %r') % 
params)
 return prefetched[key]
 
-visited = set()
-result = []
-queue = [params]
-while queue:
-params = queue.pop()
-drev = fetch(params)
-

D125: phabricator: add a small language to query Differential Revisions

2017-08-10 Thread quark (Jun Wu)
quark updated this revision to Diff 743.

REPOSITORY
  rHG Mercurial

CHANGES SINCE LAST UPDATE
  https://phab.mercurial-scm.org/D125?vs=570&id=743

REVISION DETAIL
  https://phab.mercurial-scm.org/D125

AFFECTED FILES
  contrib/phabricator.py

CHANGE DETAILS

diff --git a/contrib/phabricator.py b/contrib/phabricator.py
--- a/contrib/phabricator.py
+++ b/contrib/phabricator.py
@@ -32,7 +32,9 @@
 
 from __future__ import absolute_import
 
+import itertools
 import json
+import operator
 import re
 
 from mercurial.node import bin, nullid
@@ -44,9 +46,11 @@
 error,
 mdiff,
 obsutil,
+parser,
 patch,
 registrar,
 scmutil,
+smartset,
 tags,
 url as urlmod,
 util,
@@ -485,11 +489,77 @@
 
 return True
 
-def querydrev(repo, params, stack=False):
+# Small language to specify differential revisions. Support symbols: (), :X,
+# +, and -.
+
+_elements = {
+# token-type: binding-strength, primary, prefix, infix, suffix
+'(':  (12, None, ('group', 1, ')'), None, None),
+':':  (8, None, ('ancestors', 8), None, None),
+'&':  (5,  None, None, ('and_', 5), None),
+'+':  (4,  None, None, ('add', 4), None),
+'-':  (4,  None, None, ('sub', 4), None),
+')':  (0,  None, None, None, None),
+'symbol': (0, 'symbol', None, None, None),
+'end':(0, None, None, None, None),
+}
+
+def _tokenize(text):
+text = text.replace(' ', '') # remove space
+view = memoryview(text) # zero-copy slice
+special = '():+-&'
+pos = 0
+length = len(text)
+while pos < length:
+symbol = ''.join(itertools.takewhile(lambda ch: ch not in special,
+ view[pos:]))
+if symbol:
+yield ('symbol', symbol, pos)
+pos += len(symbol)
+else: # special char
+yield (text[pos], None, pos)
+pos += 1
+yield ('end', None, pos)
+
+def _parse(text):
+tree, pos = parser.parser(_elements).parse(_tokenize(text))
+if pos != len(text):
+raise error.ParseError('invalid token', pos)
+return tree
+
+def _parsedrev(symbol):
+"""str -> int or None, ex. 'D45' -> 45; '12' -> 12; 'x' -> None"""
+if symbol.startswith('D') and symbol[1:].isdigit():
+return int(symbol[1:])
+if symbol.isdigit():
+return int(symbol)
+
+def _prefetchdrevs(tree):
+"""return ({single-drev-id}, {ancestor-drev-id}) to prefetch"""
+drevs = set()
+ancestordrevs = set()
+op = tree[0]
+if op == 'symbol':
+r = _parsedrev(tree[1])
+if r:
+drevs.add(r)
+elif op == 'ancestors':
+r, a = _prefetchdrevs(tree[1])
+drevs.update(r)
+ancestordrevs.update(r)
+ancestordrevs.update(a)
+else:
+for t in tree[1:]:
+r, a = _prefetchdrevs(t)
+drevs.update(r)
+ancestordrevs.update(a)
+return drevs, ancestordrevs
+
+def querydrev(repo, spec):
 """return a list of "Differential Revision" dicts
 
-params is the input of "differential.query" API, and is expected to match
-just a single Differential Revision.
+spec is a string using a simple query language, see docstring in phabread
+for details.
 
 A "Differential Revision dict" looks like:
 
@@ -526,51 +596,77 @@
 "repositoryPHID": "PHID-REPO-hub2hx62ieuqeheznasv",
 "sourcePath": null
 }
-
-If stack is True, return a list of "Differential Revision dict"s in an
-order that the latter ones depend on the former ones. Otherwise, return a
-list of a unique "Differential Revision dict".
 """
-prefetched = {} # {id or phid: drev}
 def fetch(params):
 """params -> single drev or None"""
 key = (params.get(r'ids') or params.get(r'phids') or [None])[0]
 if key in prefetched:
 return prefetched[key]
-# Otherwise, send the request. If we're fetching a stack, be smarter
-# and fetch more ids in one batch, even if it could be unnecessary.
-batchparams = params
-if stack and len(params.get(r'ids', [])) == 1:
-i = int(params[r'ids'][0])
-# developer config: phabricator.batchsize
-batchsize = repo.ui.configint('phabricator', 'batchsize', 12)
-batchparams = {'ids': range(max(1, i - batchsize), i + 1)}
-drevs = callconduit(repo, 'differential.query', batchparams)
+drevs = callconduit(repo, 'differential.query', params)
 # Fill prefetched with the result
 for drev in drevs:
 prefetched[drev[r'phid']] = drev
 prefetched[int(drev[r'id'])] = drev
 if key not in prefetched:
 raise error.Abort(_('cannot get Differential Revision %r') % 
params)
 return prefetched[key]
 
-visited = set()
-result = []
-queue = [params]
-while queue:
-params = queue.pop()
-drev = fetch(params)
- 

D125: phabricator: add a small language to query Differential Revisions

2017-08-12 Thread quark (Jun Wu)
quark updated this revision to Diff 840.

REPOSITORY
  rHG Mercurial

CHANGES SINCE LAST UPDATE
  https://phab.mercurial-scm.org/D125?vs=743&id=840

REVISION DETAIL
  https://phab.mercurial-scm.org/D125

AFFECTED FILES
  contrib/phabricator.py

CHANGE DETAILS

diff --git a/contrib/phabricator.py b/contrib/phabricator.py
--- a/contrib/phabricator.py
+++ b/contrib/phabricator.py
@@ -32,7 +32,9 @@
 
 from __future__ import absolute_import
 
+import itertools
 import json
+import operator
 import re
 
 from mercurial.node import bin, nullid
@@ -44,9 +46,11 @@
 error,
 mdiff,
 obsutil,
+parser,
 patch,
 registrar,
 scmutil,
+smartset,
 tags,
 url as urlmod,
 util,
@@ -484,11 +488,77 @@
 
 return True
 
-def querydrev(repo, params, stack=False):
+# Small language to specify differential revisions. Support symbols: (), :X,
+# +, and -.
+
+_elements = {
+# token-type: binding-strength, primary, prefix, infix, suffix
+'(':  (12, None, ('group', 1, ')'), None, None),
+':':  (8, None, ('ancestors', 8), None, None),
+'&':  (5,  None, None, ('and_', 5), None),
+'+':  (4,  None, None, ('add', 4), None),
+'-':  (4,  None, None, ('sub', 4), None),
+')':  (0,  None, None, None, None),
+'symbol': (0, 'symbol', None, None, None),
+'end':(0, None, None, None, None),
+}
+
+def _tokenize(text):
+text = text.replace(' ', '') # remove space
+view = memoryview(text) # zero-copy slice
+special = '():+-&'
+pos = 0
+length = len(text)
+while pos < length:
+symbol = ''.join(itertools.takewhile(lambda ch: ch not in special,
+ view[pos:]))
+if symbol:
+yield ('symbol', symbol, pos)
+pos += len(symbol)
+else: # special char
+yield (text[pos], None, pos)
+pos += 1
+yield ('end', None, pos)
+
+def _parse(text):
+tree, pos = parser.parser(_elements).parse(_tokenize(text))
+if pos != len(text):
+raise error.ParseError('invalid token', pos)
+return tree
+
+def _parsedrev(symbol):
+"""str -> int or None, ex. 'D45' -> 45; '12' -> 12; 'x' -> None"""
+if symbol.startswith('D') and symbol[1:].isdigit():
+return int(symbol[1:])
+if symbol.isdigit():
+return int(symbol)
+
+def _prefetchdrevs(tree):
+"""return ({single-drev-id}, {ancestor-drev-id}) to prefetch"""
+drevs = set()
+ancestordrevs = set()
+op = tree[0]
+if op == 'symbol':
+r = _parsedrev(tree[1])
+if r:
+drevs.add(r)
+elif op == 'ancestors':
+r, a = _prefetchdrevs(tree[1])
+drevs.update(r)
+ancestordrevs.update(r)
+ancestordrevs.update(a)
+else:
+for t in tree[1:]:
+r, a = _prefetchdrevs(t)
+drevs.update(r)
+ancestordrevs.update(a)
+return drevs, ancestordrevs
+
+def querydrev(repo, spec):
 """return a list of "Differential Revision" dicts
 
-params is the input of "differential.query" API, and is expected to match
-just a single Differential Revision.
+spec is a string using a simple query language, see docstring in phabread
+for details.
 
 A "Differential Revision dict" looks like:
 
@@ -525,51 +595,77 @@
 "repositoryPHID": "PHID-REPO-hub2hx62ieuqeheznasv",
 "sourcePath": null
 }
-
-If stack is True, return a list of "Differential Revision dict"s in an
-order that the latter ones depend on the former ones. Otherwise, return a
-list of a unique "Differential Revision dict".
 """
-prefetched = {} # {id or phid: drev}
 def fetch(params):
 """params -> single drev or None"""
 key = (params.get(r'ids') or params.get(r'phids') or [None])[0]
 if key in prefetched:
 return prefetched[key]
-# Otherwise, send the request. If we're fetching a stack, be smarter
-# and fetch more ids in one batch, even if it could be unnecessary.
-batchparams = params
-if stack and len(params.get(r'ids', [])) == 1:
-i = int(params[r'ids'][0])
-# developer config: phabricator.batchsize
-batchsize = repo.ui.configint('phabricator', 'batchsize', 12)
-batchparams = {'ids': range(max(1, i - batchsize), i + 1)}
-drevs = callconduit(repo, 'differential.query', batchparams)
+drevs = callconduit(repo, 'differential.query', params)
 # Fill prefetched with the result
 for drev in drevs:
 prefetched[drev[r'phid']] = drev
 prefetched[int(drev[r'id'])] = drev
 if key not in prefetched:
 raise error.Abort(_('cannot get Differential Revision %r') % 
params)
 return prefetched[key]
 
-visited = set()
-result = []
-queue = [params]
-while queue:
-params = queue.pop()
-drev = fetch(params)
- 

D125: phabricator: add a small language to query Differential Revisions

2017-08-15 Thread quark (Jun Wu)
This revision was automatically updated to reflect the committed changes.
Closed by commit rHG539541779010: phabricator: add a small language to query 
Differential Revisions (authored by quark).

REPOSITORY
  rHG Mercurial

CHANGES SINCE LAST UPDATE
  https://phab.mercurial-scm.org/D125?vs=840&id=961

REVISION DETAIL
  https://phab.mercurial-scm.org/D125

AFFECTED FILES
  contrib/phabricator.py

CHANGE DETAILS

diff --git a/contrib/phabricator.py b/contrib/phabricator.py
--- a/contrib/phabricator.py
+++ b/contrib/phabricator.py
@@ -32,7 +32,9 @@
 
 from __future__ import absolute_import
 
+import itertools
 import json
+import operator
 import re
 
 from mercurial.node import bin, nullid
@@ -44,9 +46,11 @@
 error,
 mdiff,
 obsutil,
+parser,
 patch,
 registrar,
 scmutil,
+smartset,
 tags,
 url as urlmod,
 util,
@@ -484,11 +488,77 @@
 
 return True
 
-def querydrev(repo, params, stack=False):
+# Small language to specify differential revisions. Support symbols: (), :X,
+# +, and -.
+
+_elements = {
+# token-type: binding-strength, primary, prefix, infix, suffix
+'(':  (12, None, ('group', 1, ')'), None, None),
+':':  (8, None, ('ancestors', 8), None, None),
+'&':  (5,  None, None, ('and_', 5), None),
+'+':  (4,  None, None, ('add', 4), None),
+'-':  (4,  None, None, ('sub', 4), None),
+')':  (0,  None, None, None, None),
+'symbol': (0, 'symbol', None, None, None),
+'end':(0, None, None, None, None),
+}
+
+def _tokenize(text):
+text = text.replace(' ', '') # remove space
+view = memoryview(text) # zero-copy slice
+special = '():+-&'
+pos = 0
+length = len(text)
+while pos < length:
+symbol = ''.join(itertools.takewhile(lambda ch: ch not in special,
+ view[pos:]))
+if symbol:
+yield ('symbol', symbol, pos)
+pos += len(symbol)
+else: # special char
+yield (text[pos], None, pos)
+pos += 1
+yield ('end', None, pos)
+
+def _parse(text):
+tree, pos = parser.parser(_elements).parse(_tokenize(text))
+if pos != len(text):
+raise error.ParseError('invalid token', pos)
+return tree
+
+def _parsedrev(symbol):
+"""str -> int or None, ex. 'D45' -> 45; '12' -> 12; 'x' -> None"""
+if symbol.startswith('D') and symbol[1:].isdigit():
+return int(symbol[1:])
+if symbol.isdigit():
+return int(symbol)
+
+def _prefetchdrevs(tree):
+"""return ({single-drev-id}, {ancestor-drev-id}) to prefetch"""
+drevs = set()
+ancestordrevs = set()
+op = tree[0]
+if op == 'symbol':
+r = _parsedrev(tree[1])
+if r:
+drevs.add(r)
+elif op == 'ancestors':
+r, a = _prefetchdrevs(tree[1])
+drevs.update(r)
+ancestordrevs.update(r)
+ancestordrevs.update(a)
+else:
+for t in tree[1:]:
+r, a = _prefetchdrevs(t)
+drevs.update(r)
+ancestordrevs.update(a)
+return drevs, ancestordrevs
+
+def querydrev(repo, spec):
 """return a list of "Differential Revision" dicts
 
-params is the input of "differential.query" API, and is expected to match
-just a single Differential Revision.
+spec is a string using a simple query language, see docstring in phabread
+for details.
 
 A "Differential Revision dict" looks like:
 
@@ -525,51 +595,77 @@
 "repositoryPHID": "PHID-REPO-hub2hx62ieuqeheznasv",
 "sourcePath": null
 }
-
-If stack is True, return a list of "Differential Revision dict"s in an
-order that the latter ones depend on the former ones. Otherwise, return a
-list of a unique "Differential Revision dict".
 """
-prefetched = {} # {id or phid: drev}
 def fetch(params):
 """params -> single drev or None"""
 key = (params.get(r'ids') or params.get(r'phids') or [None])[0]
 if key in prefetched:
 return prefetched[key]
-# Otherwise, send the request. If we're fetching a stack, be smarter
-# and fetch more ids in one batch, even if it could be unnecessary.
-batchparams = params
-if stack and len(params.get(r'ids', [])) == 1:
-i = int(params[r'ids'][0])
-# developer config: phabricator.batchsize
-batchsize = repo.ui.configint('phabricator', 'batchsize', 12)
-batchparams = {'ids': range(max(1, i - batchsize), i + 1)}
-drevs = callconduit(repo, 'differential.query', batchparams)
+drevs = callconduit(repo, 'differential.query', params)
 # Fill prefetched with the result
 for drev in drevs:
 prefetched[drev[r'phid']] = drev
 prefetched[int(drev[r'id'])] = drev
 if key not in prefetched:
 raise error.Abort(_('cannot get Differential Revision %r') % 
params)
 return prefetc

Re: D125: phabricator: add a small language to query Differential Revisions

2017-07-18 Thread quark (Jun Wu)
quark created this revision.
Herald added a subscriber: mercurial-devel.
Herald added a reviewer: hg-reviewers.

REVISION SUMMARY
  Previously, `phabread` can only be used to read a single patch, or a single
  stack of patches. In the future, we want to have more complex queries like
  filtering with status (open, accepted, closed, etc), or maybe more complex
  like filtering by reviewers etc. The command line flag approach won't scale
  with that.
  
  Besides, we might want to have other commands to update Differential
  Revision status in batch, like accepting a stack using a single command.
  
  Therefore, this patch adds a small language. It has basic set operations:
  `&`, `+`, `-` and an ancestor operator to support `--stack`.

TEST PLAN
  Try querying this Phabricator instance:
  
hg phabread 1+2 # 1, 2
hg phabread D2+D1 # 2, 1
hg phabread ':118-115+:2-1' # 114, 116, 117, 118, 2
hg phabread '((:118-(D115+117)))&:117' # 114, 116
hg phabread ':2&:117' --debug # differential.query is called only once
  
  Make sure the output is expected and prefetch works.

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D125

AFFECTED FILES
  contrib/phabricator.py

CHANGE DETAILS

diff --git a/contrib/phabricator.py b/contrib/phabricator.py
--- a/contrib/phabricator.py
+++ b/contrib/phabricator.py
@@ -32,7 +32,9 @@
 
 from __future__ import absolute_import
 
+import itertools
 import json
+import operator
 import re
 
 from mercurial.node import bin, nullid
@@ -44,9 +46,11 @@
 error,
 mdiff,
 obsolete,
+parser,
 patch,
 registrar,
 scmutil,
+smartset,
 tags,
 url as urlmod,
 util,
@@ -435,11 +439,77 @@
 _metanamemap = util.sortdict([(r'user', 'User'), (r'date', 'Date'),
   (r'node', 'Node ID'), (r'parent', 'Parent ')])
 
-def querydrev(repo, params, stack=False):
+# Small language to specify differential revisions. Support symbols: (), :X,
+# +, and -.
+
+_elements = {
+# token-type: binding-strength, primary, prefix, infix, suffix
+'(':  (12, None, ('group', 1, ')'), None, None),
+':':  (8, None, ('ancestors', 8), None, None),
+'&':  (5,  None, None, ('and_', 5), None),
+'+':  (4,  None, None, ('add', 4), None),
+'-':  (4,  None, None, ('sub', 4), None),
+')':  (0,  None, None, None, None),
+'symbol': (0, 'symbol', None, None, None),
+'end':(0, None, None, None, None),
+}
+
+def _tokenize(text):
+text = text.replace(' ', '') # remove space
+view = memoryview(text) # zero-copy slice
+special = '():+-&'
+pos = 0
+length = len(text)
+while pos < length:
+symbol = ''.join(itertools.takewhile(lambda ch: ch not in special,
+ view[pos:]))
+if symbol:
+yield ('symbol', symbol, pos)
+pos += len(symbol)
+else: # special char
+yield (text[pos], None, pos)
+pos += 1
+yield ('end', None, pos)
+
+def _parse(text):
+tree, pos = parser.parser(_elements).parse(_tokenize(text))
+if pos != len(text):
+raise error.ParseError('invalid token', pos)
+return tree
+
+def _parsedrev(symbol):
+"""str -> int or None, ex. 'D45' -> 45; '12' -> 12; 'x' -> None"""
+if symbol.startswith('D') and symbol[1:].isdigit():
+return int(symbol[1:])
+if symbol.isdigit():
+return int(symbol)
+
+def _prefetchdrevs(tree):
+"""return ({single-drev-id}, {ancestor-drev-id}) to prefetch"""
+drevs = set()
+ancestordrevs = set()
+op = tree[0]
+if op == 'symbol':
+r = _parsedrev(tree[1])
+if r:
+drevs.add(r)
+elif op == 'ancestors':
+r, a = _prefetchdrevs(tree[1])
+drevs.update(r)
+ancestordrevs.update(r)
+ancestordrevs.update(a)
+else:
+for t in tree[1:]:
+r, a = _prefetchdrevs(t)
+drevs.update(r)
+ancestordrevs.update(a)
+return drevs, ancestordrevs
+
+def querydrev(repo, spec):
 """return a list of "Differential Revision" dicts
 
-params is the input of "differential.query" API, and is expected to match
-just a single Differential Revision.
+spec is a string using a simple query language, see docstring in phabread
+for details.
 
 A "Differential Revision dict" looks like:
 
@@ -476,51 +546,77 @@
 "repositoryPHID": "PHID-REPO-hub2hx62ieuqeheznasv",
 "sourcePath": null
 }
-
-If stack is True, return a list of "Differential Revision dict"s in an
-order that the latter ones depend on the former ones. Otherwise, return a
-list of a unique "Differential Revision dict".
 """
-prefetched = {} # {id or phid: drev}
 def fetch(params):
 """params -> single drev or None"""
 key = (params.get(r'ids') or params.get(r'phids') or [None])[0]
 if key in prefetched: