##// END OF EJS Templates
templater: add subsetparents(rev, revset) function...
Yuya Nishihara -
r45083:7cd5c096 default
parent child Browse files
Show More
@@ -0,0 +1,337 b''
1 Test graph-related template functions
2 =====================================
3
4 $ cat <<'EOF' >> $HGRCPATH
5 > [extensions]
6 > drawdag = $RUNTESTDIR/drawdag.py
7 > EOF
8
9 $ hg init a
10 $ cd a
11
12 $ hg debugdrawdag <<'EOF'
13 > l
14 > / \
15 > | k
16 > | |\
17 > | | j
18 > | | |
19 > i | |
20 > |\ | |
21 > h | | |
22 > | | | |
23 > | g | |
24 > | | | |
25 > f | | |
26 > | |/ /
27 > | e |
28 > | |/
29 > | d
30 > |/|
31 > c |
32 > | |
33 > b |
34 > |
35 > a
36 > EOF
37
38 $ hg log -Gq -T'{rev} {tags}\n'
39 o 11 l tip
40 |\
41 | o 10 i
42 | |\
43 o \ \ 9 k
44 |\ \ \
45 +-----o 8 g
46 | | |
47 | o | 7 j
48 | | |
49 | | o 6 h
50 | | |
51 o | | 5 e
52 |/ /
53 | o 4 f
54 | |
55 o | 3 d
56 |\|
57 | o 2 c
58 | |
59 | o 1 b
60 |
61 o 0 a
62
63
64 $ cd ..
65
66 subsetparents
67 -------------
68
69 $ cd a
70
71 $ hg log -Gq -T '{rev} {tags}: {subsetparents(rev, revset("c+i"))}\n' -r 'c+i'
72 o 10 i: 2
73 :
74 o 2 c:
75 |
76 ~
77
78 $ hg log -Gq -T '{rev} {tags}: {subsetparents(rev, revset("c+h+i"))}\n' -r 'c+h+i'
79 o 10 i: 6
80 |\
81 o : 6 h: 2
82 :/
83 o 2 c:
84 |
85 ~
86
87 $ hg log -Gq -T '{rev} {tags}: {subsetparents(rev, revset("c+h+l"))}\n' -r 'c+h+l'
88 o 11 l tip: 6
89 :\
90 : o 6 h: 2
91 :/
92 o 2 c:
93 |
94 ~
95
96 $ hg log -Gq -T '{rev} {tags}: {subsetparents(rev, revset("c+f+l"))}\n' -r 'c+f+l'
97 o 11 l tip: 4
98 :\
99 : o 4 f: 2
100 :/
101 o 2 c:
102 |
103 ~
104
105 $ hg log -Gq -T '{rev} {tags}: {subsetparents(rev, revset("c+h+i+k"))}\n' -r 'c+h+i+k'
106 o 10 i: 6
107 |\
108 | : o 9 k: 2
109 | :/
110 o : 6 h: 2
111 :/
112 o 2 c:
113 |
114 ~
115
116 $ hg log -Gq -T '{rev} {tags}: {subsetparents(rev, revset("c+d+h+i+k"))}\n' -r 'c+d+h+i+k'
117 o 10 i: 6 3
118 |\
119 | : o 9 k: 3
120 | :/
121 o : 6 h: 2
122 : :
123 : o 3 d: 2
124 :/|
125 : ~
126 o 2 c:
127 |
128 ~
129
130 $ hg log -Gq -T '{rev} {tags}: {subsetparents(rev, revset("c+j+k+i"))}\n' -r 'c+j+k+i'
131 o 10 i: 2
132 :
133 : o 9 k: 7
134 :/|
135 : o 7 j: 2
136 :/
137 o 2 c:
138 |
139 ~
140
141 $ hg log -Gq -T '{rev} {tags}: {subsetparents(rev, revset("c+e+f+j"))}\n' -r 'c+e+f+j'
142 o 7 j: 2
143 :
144 : o 5 e: 2
145 :/
146 : o 4 f: 2
147 :/
148 o 2 c:
149 |
150 ~
151
152 $ hg log -Gq -T '{rev} {tags}: {subsetparents(rev, revset("b+e+f+j"))}\n' -r 'b+e+f+j'
153 o 7 j: 1
154 :
155 : o 5 e: 1
156 :/
157 : o 4 f: 1
158 :/
159 o 1 b:
160
161
162 $ hg log -Gq -T '{rev} {tags}: {subsetparents(rev, revset("a+c+f+g+j+l"))}\n' -r 'a+c+f+g+j+l'
163 o 11 l tip: 4 8 7
164 :\
165 : \
166 : :\
167 : : \
168 : : :\
169 : : : \
170 : : : :\
171 : o---+ : 8 g: 0 2
172 : :/ / /
173 : +---o 7 j: 0 2
174 : : :/
175 o---+ 4 f: 2
176 / /
177 : o 2 c:
178 : |
179 : ~
180 o 0 a:
181
182
183 $ hg log -Gq -T '{rev} {tags}: {subsetparents(rev, revset("b+i+l"))}\n' -r 'b+i+l'
184 o 11 l tip: 10
185 |\
186 o : 10 i: 1
187 :/
188 o 1 b:
189
190
191 $ hg log -Gq -T '{rev} {tags}: {subsetparents(rev, revset("b+i+j+l"))}\n' -r 'b+i+j+l'
192 o 11 l tip: 10 7
193 |\
194 | \
195 | :\
196 o : : 10 i: 1
197 :/ /
198 : o 7 j: 1
199 :/
200 o 1 b:
201
202
203 null in subset:
204
205 $ hg log -Gq -T '{rev} {tags}: {subsetparents(rev, revset("null+a+c+f"))}\n' -r 'null+a+c+f'
206 o 4 f: 2
207 |
208 o 2 c: -1
209 :
210 : o 0 a: -1
211 :/
212 @ -1 : -1
213
214
215 $ hg log -Gq -T '{rev} {tags}: {subsetparents(rev, revset("null+a+b+c+f"))}\n' -r 'null+a+b+c+f'
216 o 4 f: 2
217 |
218 o 2 c: 1
219 |
220 o 1 b: -1
221 |
222 | o 0 a: -1
223 |/
224 @ -1 : -1
225
226
227 wdir in subset:
228
229 $ hg update -qC i
230
231 $ hg log -Gq -T '{rev} {tags}: {subsetparents(rev, revset("f+k+wdir()"))}\n' -r 'f+k+wdir()'
232 o 2147483647 : 4
233 :
234 : o 9 k:
235 : |\
236 : ~ ~
237 o 4 f:
238 |
239 ~
240
241 $ hg update -qC null
242
243 Revisions not in subset:
244
245 $ hg log -T '{rev} {tags}: {subsetparents(rev, revset("a+c+f+g+j+l"))}\n'
246 11 l tip: 4 8 7
247 10 i:
248 9 k:
249 8 g: 0 2
250 7 j: 0 2
251 6 h:
252 5 e:
253 4 f: 2
254 3 d:
255 2 c:
256 1 b:
257 0 a:
258
259 $ hg log -T '{rev} {tags}: {subsetparents(rev, revset("b+c"))}\n'
260 11 l tip:
261 10 i:
262 9 k:
263 8 g:
264 7 j:
265 6 h:
266 5 e:
267 4 f:
268 3 d:
269 2 c: 1
270 1 b:
271 0 a:
272
273 $ hg log -T '{rev} {tags}: {subsetparents(rev, revset("b+c"))}\n' -r'reverse(null:2)'
274 2 c: 1
275 1 b:
276 0 a:
277 -1 :
278
279 Nothing excluded:
280
281 $ hg log -T '{rev} {tags}: {subsetparents(rev, revset("null:wdir()"))}\n' -r'reverse(null:wdir())'
282 2147483647 : -1
283 11 l tip: 10 9
284 10 i: 6 8
285 9 k: 5 7
286 8 g: 5
287 7 j: 3
288 6 h: 4
289 5 e: 3
290 4 f: 2
291 3 d: 0 2
292 2 c: 1
293 1 b: -1
294 0 a: -1
295 -1 : -1
296
297 Uncachable query:
298
299 $ hg log -Gq -T '{rev} {tags}: {subsetparents(rev, revset("%d:%d", rev, rev - 1))}\n'
300 o 11 l tip: 10
301 |\
302 | o 10 i:
303 | |\
304 o \ \ 9 k:
305 |\ \ \
306 +-----o 8 g:
307 | | |
308 | o | 7 j:
309 | | |
310 | | o 6 h:
311 | | |
312 o | | 5 e:
313 |/ /
314 | o 4 f:
315 | |
316 o | 3 d: 2
317 |\|
318 | o 2 c: 1
319 | |
320 | o 1 b:
321 |
322 o 0 a: -1
323
324
325 Invalid arguments:
326
327 $ hg log -T '{subsetparents()}\n'
328 hg: parse error: subsetparents expects two arguments
329 [255]
330 $ hg log -T '{subsetparents("a")}\n'
331 hg: parse error: subsetparents expects two arguments
332 [255]
333 $ hg log -T '{subsetparents(rev, extras)}\n'
334 hg: parse error: subsetparents expects a queried revset
335 [255]
336
337 $ cd ..
@@ -274,6 +274,238 b' def descendantrevs(revs, revsfn, parentr'
274 break
274 break
275
275
276
276
277 class subsetparentswalker(object):
278 r"""Scan adjacent ancestors in the graph given by the subset
279
280 This computes parent-child relations in the sub graph filtered by
281 a revset. Primary use case is to draw a revisions graph.
282
283 In the following example, we consider that the node 'f' has edges to all
284 ancestor nodes, but redundant paths are eliminated. The edge 'f'->'b'
285 is eliminated because there is a path 'f'->'c'->'b' for example.
286
287 - d - e -
288 / \
289 a - b - c - f
290
291 If the node 'c' is filtered out, the edge 'f'->'b' is activated.
292
293 - d - e -
294 / \
295 a - b -(c)- f
296
297 Likewise, if 'd' and 'e' are filtered out, this edge is fully eliminated
298 since there is a path 'f'->'c'->'b'->'a' for 'f'->'a'.
299
300 (d) (e)
301
302 a - b - c - f
303
304 Implementation-wise, 'f' is passed down to 'a' as unresolved through the
305 'f'->'e'->'d'->'a' path, whereas we do also remember that 'f' has already
306 been resolved while walking down the 'f'->'c'->'b'->'a' path. When
307 processing the node 'a', the unresolved 'f'->'a' path is eliminated as
308 the 'f' end is marked as resolved.
309
310 Ancestors are searched from the tipmost revision in the subset so the
311 results can be cached. You should specify startrev to narrow the search
312 space to ':startrev'.
313 """
314
315 def __init__(self, repo, subset, startrev=None):
316 if startrev is not None:
317 subset = repo.revs(b'%d:null', startrev) & subset
318
319 # equivalent to 'subset = subset.sorted(reverse=True)', but there's
320 # no such function.
321 fastdesc = subset.fastdesc
322 if fastdesc:
323 desciter = fastdesc()
324 else:
325 if not subset.isdescending() and not subset.istopo():
326 subset = smartset.baseset(subset)
327 subset.sort(reverse=True)
328 desciter = iter(subset)
329
330 self._repo = repo
331 self._changelog = repo.changelog
332 self._subset = subset
333
334 # scanning state (see _scanparents):
335 self._tovisit = []
336 self._pendingcnt = {}
337 self._pointers = {}
338 self._parents = {}
339 self._inputhead = nullrev # reassigned by self._advanceinput()
340 self._inputtail = desciter
341 self._bottomrev = nullrev
342 self._advanceinput()
343
344 def parentsset(self, rev):
345 """Look up parents of the given revision in the subset, and returns
346 as a smartset"""
347 return smartset.baseset(self.parents(rev))
348
349 def parents(self, rev):
350 """Look up parents of the given revision in the subset
351
352 The returned revisions are sorted by parent index (p1/p2).
353 """
354 self._scanparents(rev)
355 return [r for _c, r in sorted(self._parents.get(rev, []))]
356
357 def _parentrevs(self, rev):
358 try:
359 revs = self._changelog.parentrevs(rev)
360 if revs[-1] == nullrev:
361 return revs[:-1]
362 return revs
363 except error.WdirUnsupported:
364 return tuple(pctx.rev() for pctx in self._repo[None].parents())
365
366 def _advanceinput(self):
367 """Advance the input iterator and set the next revision to _inputhead"""
368 if self._inputhead < nullrev:
369 return
370 try:
371 self._inputhead = next(self._inputtail)
372 except StopIteration:
373 self._bottomrev = self._inputhead
374 self._inputhead = nullrev - 1
375
376 def _scanparents(self, stoprev):
377 """Scan ancestors until the parents of the specified stoprev are
378 resolved"""
379
380 # 'tovisit' is the queue of the input revisions and their ancestors.
381 # It will be populated incrementally to minimize the initial cost
382 # of computing the given subset.
383 #
384 # For to-visit revisions, we keep track of
385 # - the number of the unresolved paths: pendingcnt[rev],
386 # - dict of the unresolved descendants and chains: pointers[rev][0],
387 # - set of the already resolved descendants: pointers[rev][1].
388 #
389 # When a revision is visited, 'pointers[rev]' should be popped and
390 # propagated to its parents accordingly.
391 #
392 # Once all pending paths have been resolved, 'pendingcnt[rev]' becomes
393 # 0 and 'parents[rev]' contains the unsorted list of parent revisions
394 # and p1/p2 chains (excluding linear paths.)
395
396 subset = self._subset
397 tovisit = self._tovisit # heap queue of [-rev]
398 pendingcnt = self._pendingcnt # {rev: count} for visited revisions
399 pointers = self._pointers # {rev: [{unresolved_rev: chain}, resolved]}
400 parents = self._parents # {rev: [(chain, rev)]}
401
402 while tovisit or self._inputhead >= nullrev:
403 if pendingcnt.get(stoprev) == 0:
404 return
405
406 # feed greater revisions from input set to queue
407 if not tovisit:
408 heapq.heappush(tovisit, -self._inputhead)
409 self._advanceinput()
410 while self._inputhead >= -tovisit[0]:
411 heapq.heappush(tovisit, -self._inputhead)
412 self._advanceinput()
413
414 rev = -heapq.heappop(tovisit)
415 if rev < self._bottomrev:
416 return
417 if rev in pendingcnt and rev not in pointers:
418 continue # already visited
419
420 curactive = rev in subset
421 pendingcnt.setdefault(rev, 0) # mark as visited
422 if curactive:
423 assert rev not in parents
424 parents[rev] = []
425 unresolved, resolved = pointers.pop(rev, ({}, set()))
426
427 if curactive:
428 # reached to active rev, resolve pending descendants' parents
429 for r, c in unresolved.items():
430 pendingcnt[r] -= 1
431 assert pendingcnt[r] >= 0
432 if r in resolved:
433 continue # eliminate redundant path
434 parents[r].append((c, rev))
435 # mark the descendant 'r' as resolved through this path if
436 # there are still pending pointers. the 'resolved' set may
437 # be concatenated later at a fork revision.
438 if pendingcnt[r] > 0:
439 resolved.add(r)
440 unresolved.clear()
441 # occasionally clean resolved markers. otherwise the set
442 # would grow indefinitely.
443 resolved = {r for r in resolved if pendingcnt[r] > 0}
444
445 parentrevs = self._parentrevs(rev)
446 bothparentsactive = all(p in subset for p in parentrevs)
447
448 # set up or propagate tracking pointers if
449 # - one of the parents is not active,
450 # - or descendants' parents are unresolved.
451 if not bothparentsactive or unresolved or resolved:
452 if len(parentrevs) > 1:
453 # 'rev' is a merge revision. increment the pending count
454 # as the 'unresolved' dict will be duplicated.
455 for r in unresolved:
456 pendingcnt[r] += 1
457 reusable = True # can we avoid copying the tracking pointer?
458 for i, p in enumerate(parentrevs):
459 assert p < rev
460 heapq.heappush(tovisit, -p)
461 if p in pointers:
462 # 'p' is a fork revision. concatenate tracking pointers
463 # and decrement the pending count accordingly.
464 knownunresolved, knownresolved = pointers[p]
465 for r, c in unresolved.items():
466 c += [b'1', b'2'][i]
467 if r in knownunresolved:
468 # unresolved at both paths
469 pendingcnt[r] -= 1
470 assert pendingcnt[r] > 0
471 # take shorter chain
472 knownunresolved[r] = min(c, knownunresolved[r])
473 else:
474 knownunresolved[r] = c
475 # simply propagate the 'resolved' set as deduplicating
476 # 'unresolved' here would be slightly complicated.
477 knownresolved.update(resolved)
478 elif reusable:
479 pointers[p] = (unresolved, resolved)
480 reusable = False
481 else:
482 pointers[p] = (unresolved.copy(), resolved.copy())
483
484 # then, populate the active parents directly and add the current
485 # 'rev' to the tracking pointers of the inactive parents.
486 # 'pointers[p]' may be optimized out if both parents are active.
487 if curactive and bothparentsactive:
488 for i, p in enumerate(parentrevs):
489 c = [b'1', b'2'][i]
490 parents[rev].append((c, p))
491 # no need to mark 'rev' as resolved since the 'rev' should
492 # be fully resolved (i.e. pendingcnt[rev] == 0)
493 assert pendingcnt[rev] == 0
494 elif curactive:
495 for i, p in enumerate(parentrevs):
496 unresolved, resolved = pointers[p]
497 assert rev not in unresolved
498 c = [b'1', b'2'][i]
499 if p in subset:
500 parents[rev].append((c, p))
501 # mark 'rev' as resolved through this path
502 resolved.add(rev)
503 else:
504 pendingcnt[rev] += 1
505 unresolved[rev] = c
506 assert 0 < pendingcnt[rev] <= 2
507
508
277 def _reachablerootspure(pfunc, minroot, roots, heads, includepath):
509 def _reachablerootspure(pfunc, minroot, roots, heads, includepath):
278 """See revlog.reachableroots"""
510 """See revlog.reachableroots"""
279 if not roots:
511 if not roots:
@@ -16,6 +16,7 b' from .node import ('
16 )
16 )
17 from . import (
17 from . import (
18 color,
18 color,
19 dagop,
19 diffutil,
20 diffutil,
20 encoding,
21 encoding,
21 error,
22 error,
@@ -842,6 +843,45 b' def startswith(context, mapping, args):'
842 return b''
843 return b''
843
844
844
845
846 @templatefunc(
847 b'subsetparents(rev, revset)',
848 argspec=b'rev revset',
849 requires={b'repo', b'cache'},
850 )
851 def subsetparents(context, mapping, args):
852 """Look up parents of the rev in the sub graph given by the revset."""
853 if b'rev' not in args or b'revset' not in args:
854 # i18n: "subsetparents" is a keyword
855 raise error.ParseError(_(b"subsetparents expects two arguments"))
856
857 repo = context.resource(mapping, b'repo')
858
859 rev = templateutil.evalinteger(context, mapping, args[b'rev'])
860
861 # TODO: maybe subsetparents(rev) should be allowed. the default revset
862 # will be the revisions specified by -rREV argument.
863 q = templateutil.evalwrapped(context, mapping, args[b'revset'])
864 if not isinstance(q, templateutil.revslist):
865 # i18n: "subsetparents" is a keyword
866 raise error.ParseError(_(b"subsetparents expects a queried revset"))
867 subset = q.tovalue(context, mapping)
868 key = q.cachekey
869
870 if key:
871 # cache only if revset query isn't dynamic
872 cache = context.resource(mapping, b'cache')
873 walkercache = cache.setdefault(b'subsetparentswalker', {})
874 if key in walkercache:
875 walker = walkercache[key]
876 else:
877 walker = dagop.subsetparentswalker(repo, subset)
878 walkercache[key] = walker
879 else:
880 # for one-shot use, specify startrev to limit the search space
881 walker = dagop.subsetparentswalker(repo, subset, startrev=rev)
882 return templateutil.revslist(repo, walker.parentsset(rev))
883
884
845 @templatefunc(b'word(number, text[, separator])')
885 @templatefunc(b'word(number, text[, separator])')
846 def word(context, mapping, args):
886 def word(context, mapping, args):
847 """Return the nth word from a string."""
887 """Return the nth word from a string."""
General Comments 0
You need to be logged in to leave comments. Login now