##// END OF EJS Templates
dagop: make walk direction switchable so it can track descendants...
Yuya Nishihara -
r33079:550c390c default
parent child Browse files
Show More
@@ -1,463 +1,468 b''
1 1 # dagop.py - graph ancestry and topology algorithm for revset
2 2 #
3 3 # Copyright 2010 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import heapq
11 11
12 12 from . import (
13 13 error,
14 14 mdiff,
15 15 node,
16 16 patch,
17 17 smartset,
18 18 )
19 19
20 20 baseset = smartset.baseset
21 21 generatorset = smartset.generatorset
22 22
23 23 # possible maximum depth between null and wdir()
24 24 _maxlogdepth = 0x80000000
25 25
26 def _walkrevtree(pfunc, revs, startdepth, stopdepth):
26 def _walkrevtree(pfunc, revs, startdepth, stopdepth, reverse):
27 27 """Walk DAG using 'pfunc' from the given 'revs' nodes
28 28
29 'pfunc(rev)' should return the parent revisions of the given 'rev'.
29 'pfunc(rev)' should return the parent/child revisions of the given 'rev'
30 if 'reverse' is True/False respectively.
30 31
31 32 Scan ends at the stopdepth (exlusive) if specified. Revisions found
32 33 earlier than the startdepth are omitted.
33 34 """
34 35 if startdepth is None:
35 36 startdepth = 0
36 37 if stopdepth is None:
37 38 stopdepth = _maxlogdepth
38 39 if stopdepth == 0:
39 40 return
40 41 if stopdepth < 0:
41 42 raise error.ProgrammingError('negative stopdepth')
43 if reverse:
44 heapsign = -1 # max heap
45 else:
46 heapsign = +1 # min heap
42 47
43 48 # load input revs lazily to heap so earlier revisions can be yielded
44 49 # without fully computing the input revs
45 revs.sort(reverse=True)
50 revs.sort(reverse)
46 51 irevs = iter(revs)
47 pendingheap = [] # [(-rev, depth), ...] (i.e. lower depth first)
52 pendingheap = [] # [(heapsign * rev, depth), ...] (i.e. lower depth first)
48 53
49 54 inputrev = next(irevs, None)
50 55 if inputrev is not None:
51 heapq.heappush(pendingheap, (-inputrev, 0))
56 heapq.heappush(pendingheap, (heapsign * inputrev, 0))
52 57
53 58 lastrev = None
54 59 while pendingheap:
55 60 currev, curdepth = heapq.heappop(pendingheap)
56 currev = -currev
61 currev = heapsign * currev
57 62 if currev == inputrev:
58 63 inputrev = next(irevs, None)
59 64 if inputrev is not None:
60 heapq.heappush(pendingheap, (-inputrev, 0))
65 heapq.heappush(pendingheap, (heapsign * inputrev, 0))
61 66 # rescan parents until curdepth >= startdepth because queued entries
62 67 # of the same revision are iterated from the lowest depth
63 68 foundnew = (currev != lastrev)
64 69 if foundnew and curdepth >= startdepth:
65 70 lastrev = currev
66 71 yield currev
67 72 pdepth = curdepth + 1
68 73 if foundnew and pdepth < stopdepth:
69 74 for prev in pfunc(currev):
70 75 if prev != node.nullrev:
71 heapq.heappush(pendingheap, (-prev, pdepth))
76 heapq.heappush(pendingheap, (heapsign * prev, pdepth))
72 77
73 78 def _genrevancestors(repo, revs, followfirst, startdepth, stopdepth):
74 79 if followfirst:
75 80 cut = 1
76 81 else:
77 82 cut = None
78 83 cl = repo.changelog
79 84 def pfunc(rev):
80 85 try:
81 86 return cl.parentrevs(rev)[:cut]
82 87 except error.WdirUnsupported:
83 88 return (pctx.rev() for pctx in repo[rev].parents()[:cut])
84 return _walkrevtree(pfunc, revs, startdepth, stopdepth)
89 return _walkrevtree(pfunc, revs, startdepth, stopdepth, reverse=True)
85 90
86 91 def revancestors(repo, revs, followfirst, startdepth=None, stopdepth=None):
87 92 """Like revlog.ancestors(), but supports additional options, includes
88 93 the given revs themselves, and returns a smartset
89 94
90 95 Scan ends at the stopdepth (exlusive) if specified. Revisions found
91 96 earlier than the startdepth are omitted.
92 97 """
93 98 gen = _genrevancestors(repo, revs, followfirst, startdepth, stopdepth)
94 99 return generatorset(gen, iterasc=False)
95 100
96 101 def _genrevdescendants(repo, revs, followfirst):
97 102 if followfirst:
98 103 cut = 1
99 104 else:
100 105 cut = None
101 106
102 107 cl = repo.changelog
103 108 first = revs.min()
104 109 nullrev = node.nullrev
105 110 if first == nullrev:
106 111 # Are there nodes with a null first parent and a non-null
107 112 # second one? Maybe. Do we care? Probably not.
108 113 yield first
109 114 for i in cl:
110 115 yield i
111 116 else:
112 117 seen = set(revs)
113 118 for i in cl.revs(first):
114 119 if i in seen:
115 120 yield i
116 121 continue
117 122 for x in cl.parentrevs(i)[:cut]:
118 123 if x != nullrev and x in seen:
119 124 seen.add(i)
120 125 yield i
121 126 break
122 127
123 128 def revdescendants(repo, revs, followfirst):
124 129 """Like revlog.descendants() but supports additional options, includes
125 130 the given revs themselves, and returns a smartset"""
126 131 gen = _genrevdescendants(repo, revs, followfirst)
127 132 return generatorset(gen, iterasc=True)
128 133
129 134 def _reachablerootspure(repo, minroot, roots, heads, includepath):
130 135 """return (heads(::<roots> and ::<heads>))
131 136
132 137 If includepath is True, return (<roots>::<heads>)."""
133 138 if not roots:
134 139 return []
135 140 parentrevs = repo.changelog.parentrevs
136 141 roots = set(roots)
137 142 visit = list(heads)
138 143 reachable = set()
139 144 seen = {}
140 145 # prefetch all the things! (because python is slow)
141 146 reached = reachable.add
142 147 dovisit = visit.append
143 148 nextvisit = visit.pop
144 149 # open-code the post-order traversal due to the tiny size of
145 150 # sys.getrecursionlimit()
146 151 while visit:
147 152 rev = nextvisit()
148 153 if rev in roots:
149 154 reached(rev)
150 155 if not includepath:
151 156 continue
152 157 parents = parentrevs(rev)
153 158 seen[rev] = parents
154 159 for parent in parents:
155 160 if parent >= minroot and parent not in seen:
156 161 dovisit(parent)
157 162 if not reachable:
158 163 return baseset()
159 164 if not includepath:
160 165 return reachable
161 166 for rev in sorted(seen):
162 167 for parent in seen[rev]:
163 168 if parent in reachable:
164 169 reached(rev)
165 170 return reachable
166 171
167 172 def reachableroots(repo, roots, heads, includepath=False):
168 173 """return (heads(::<roots> and ::<heads>))
169 174
170 175 If includepath is True, return (<roots>::<heads>)."""
171 176 if not roots:
172 177 return baseset()
173 178 minroot = roots.min()
174 179 roots = list(roots)
175 180 heads = list(heads)
176 181 try:
177 182 revs = repo.changelog.reachableroots(minroot, heads, roots, includepath)
178 183 except AttributeError:
179 184 revs = _reachablerootspure(repo, minroot, roots, heads, includepath)
180 185 revs = baseset(revs)
181 186 revs.sort()
182 187 return revs
183 188
184 189 def _changesrange(fctx1, fctx2, linerange2, diffopts):
185 190 """Return `(diffinrange, linerange1)` where `diffinrange` is True
186 191 if diff from fctx2 to fctx1 has changes in linerange2 and
187 192 `linerange1` is the new line range for fctx1.
188 193 """
189 194 blocks = mdiff.allblocks(fctx1.data(), fctx2.data(), diffopts)
190 195 filteredblocks, linerange1 = mdiff.blocksinrange(blocks, linerange2)
191 196 diffinrange = any(stype == '!' for _, stype in filteredblocks)
192 197 return diffinrange, linerange1
193 198
194 199 def blockancestors(fctx, fromline, toline, followfirst=False):
195 200 """Yield ancestors of `fctx` with respect to the block of lines within
196 201 `fromline`-`toline` range.
197 202 """
198 203 diffopts = patch.diffopts(fctx._repo.ui)
199 204 introrev = fctx.introrev()
200 205 if fctx.rev() != introrev:
201 206 fctx = fctx.filectx(fctx.filenode(), changeid=introrev)
202 207 visit = {(fctx.linkrev(), fctx.filenode()): (fctx, (fromline, toline))}
203 208 while visit:
204 209 c, linerange2 = visit.pop(max(visit))
205 210 pl = c.parents()
206 211 if followfirst:
207 212 pl = pl[:1]
208 213 if not pl:
209 214 # The block originates from the initial revision.
210 215 yield c, linerange2
211 216 continue
212 217 inrange = False
213 218 for p in pl:
214 219 inrangep, linerange1 = _changesrange(p, c, linerange2, diffopts)
215 220 inrange = inrange or inrangep
216 221 if linerange1[0] == linerange1[1]:
217 222 # Parent's linerange is empty, meaning that the block got
218 223 # introduced in this revision; no need to go futher in this
219 224 # branch.
220 225 continue
221 226 # Set _descendantrev with 'c' (a known descendant) so that, when
222 227 # _adjustlinkrev is called for 'p', it receives this descendant
223 228 # (as srcrev) instead possibly topmost introrev.
224 229 p._descendantrev = c.rev()
225 230 visit[p.linkrev(), p.filenode()] = p, linerange1
226 231 if inrange:
227 232 yield c, linerange2
228 233
229 234 def blockdescendants(fctx, fromline, toline):
230 235 """Yield descendants of `fctx` with respect to the block of lines within
231 236 `fromline`-`toline` range.
232 237 """
233 238 # First possibly yield 'fctx' if it has changes in range with respect to
234 239 # its parents.
235 240 try:
236 241 c, linerange1 = next(blockancestors(fctx, fromline, toline))
237 242 except StopIteration:
238 243 pass
239 244 else:
240 245 if c == fctx:
241 246 yield c, linerange1
242 247
243 248 diffopts = patch.diffopts(fctx._repo.ui)
244 249 fl = fctx.filelog()
245 250 seen = {fctx.filerev(): (fctx, (fromline, toline))}
246 251 for i in fl.descendants([fctx.filerev()]):
247 252 c = fctx.filectx(i)
248 253 inrange = False
249 254 for x in fl.parentrevs(i):
250 255 try:
251 256 p, linerange2 = seen[x]
252 257 except KeyError:
253 258 # nullrev or other branch
254 259 continue
255 260 inrangep, linerange1 = _changesrange(c, p, linerange2, diffopts)
256 261 inrange = inrange or inrangep
257 262 # If revision 'i' has been seen (it's a merge), we assume that its
258 263 # line range is the same independently of which parents was used
259 264 # to compute it.
260 265 assert i not in seen or seen[i][1] == linerange1, (
261 266 'computed line range for %s is not consistent between '
262 267 'ancestor branches' % c)
263 268 seen[i] = c, linerange1
264 269 if inrange:
265 270 yield c, linerange1
266 271
267 272 def toposort(revs, parentsfunc, firstbranch=()):
268 273 """Yield revisions from heads to roots one (topo) branch at a time.
269 274
270 275 This function aims to be used by a graph generator that wishes to minimize
271 276 the number of parallel branches and their interleaving.
272 277
273 278 Example iteration order (numbers show the "true" order in a changelog):
274 279
275 280 o 4
276 281 |
277 282 o 1
278 283 |
279 284 | o 3
280 285 | |
281 286 | o 2
282 287 |/
283 288 o 0
284 289
285 290 Note that the ancestors of merges are understood by the current
286 291 algorithm to be on the same branch. This means no reordering will
287 292 occur behind a merge.
288 293 """
289 294
290 295 ### Quick summary of the algorithm
291 296 #
292 297 # This function is based around a "retention" principle. We keep revisions
293 298 # in memory until we are ready to emit a whole branch that immediately
294 299 # "merges" into an existing one. This reduces the number of parallel
295 300 # branches with interleaved revisions.
296 301 #
297 302 # During iteration revs are split into two groups:
298 303 # A) revision already emitted
299 304 # B) revision in "retention". They are stored as different subgroups.
300 305 #
301 306 # for each REV, we do the following logic:
302 307 #
303 308 # 1) if REV is a parent of (A), we will emit it. If there is a
304 309 # retention group ((B) above) that is blocked on REV being
305 310 # available, we emit all the revisions out of that retention
306 311 # group first.
307 312 #
308 313 # 2) else, we'll search for a subgroup in (B) awaiting for REV to be
309 314 # available, if such subgroup exist, we add REV to it and the subgroup is
310 315 # now awaiting for REV.parents() to be available.
311 316 #
312 317 # 3) finally if no such group existed in (B), we create a new subgroup.
313 318 #
314 319 #
315 320 # To bootstrap the algorithm, we emit the tipmost revision (which
316 321 # puts it in group (A) from above).
317 322
318 323 revs.sort(reverse=True)
319 324
320 325 # Set of parents of revision that have been emitted. They can be considered
321 326 # unblocked as the graph generator is already aware of them so there is no
322 327 # need to delay the revisions that reference them.
323 328 #
324 329 # If someone wants to prioritize a branch over the others, pre-filling this
325 330 # set will force all other branches to wait until this branch is ready to be
326 331 # emitted.
327 332 unblocked = set(firstbranch)
328 333
329 334 # list of groups waiting to be displayed, each group is defined by:
330 335 #
331 336 # (revs: lists of revs waiting to be displayed,
332 337 # blocked: set of that cannot be displayed before those in 'revs')
333 338 #
334 339 # The second value ('blocked') correspond to parents of any revision in the
335 340 # group ('revs') that is not itself contained in the group. The main idea
336 341 # of this algorithm is to delay as much as possible the emission of any
337 342 # revision. This means waiting for the moment we are about to display
338 343 # these parents to display the revs in a group.
339 344 #
340 345 # This first implementation is smart until it encounters a merge: it will
341 346 # emit revs as soon as any parent is about to be emitted and can grow an
342 347 # arbitrary number of revs in 'blocked'. In practice this mean we properly
343 348 # retains new branches but gives up on any special ordering for ancestors
344 349 # of merges. The implementation can be improved to handle this better.
345 350 #
346 351 # The first subgroup is special. It corresponds to all the revision that
347 352 # were already emitted. The 'revs' lists is expected to be empty and the
348 353 # 'blocked' set contains the parents revisions of already emitted revision.
349 354 #
350 355 # You could pre-seed the <parents> set of groups[0] to a specific
351 356 # changesets to select what the first emitted branch should be.
352 357 groups = [([], unblocked)]
353 358 pendingheap = []
354 359 pendingset = set()
355 360
356 361 heapq.heapify(pendingheap)
357 362 heappop = heapq.heappop
358 363 heappush = heapq.heappush
359 364 for currentrev in revs:
360 365 # Heap works with smallest element, we want highest so we invert
361 366 if currentrev not in pendingset:
362 367 heappush(pendingheap, -currentrev)
363 368 pendingset.add(currentrev)
364 369 # iterates on pending rev until after the current rev have been
365 370 # processed.
366 371 rev = None
367 372 while rev != currentrev:
368 373 rev = -heappop(pendingheap)
369 374 pendingset.remove(rev)
370 375
371 376 # Seek for a subgroup blocked, waiting for the current revision.
372 377 matching = [i for i, g in enumerate(groups) if rev in g[1]]
373 378
374 379 if matching:
375 380 # The main idea is to gather together all sets that are blocked
376 381 # on the same revision.
377 382 #
378 383 # Groups are merged when a common blocking ancestor is
379 384 # observed. For example, given two groups:
380 385 #
381 386 # revs [5, 4] waiting for 1
382 387 # revs [3, 2] waiting for 1
383 388 #
384 389 # These two groups will be merged when we process
385 390 # 1. In theory, we could have merged the groups when
386 391 # we added 2 to the group it is now in (we could have
387 392 # noticed the groups were both blocked on 1 then), but
388 393 # the way it works now makes the algorithm simpler.
389 394 #
390 395 # We also always keep the oldest subgroup first. We can
391 396 # probably improve the behavior by having the longest set
392 397 # first. That way, graph algorithms could minimise the length
393 398 # of parallel lines their drawing. This is currently not done.
394 399 targetidx = matching.pop(0)
395 400 trevs, tparents = groups[targetidx]
396 401 for i in matching:
397 402 gr = groups[i]
398 403 trevs.extend(gr[0])
399 404 tparents |= gr[1]
400 405 # delete all merged subgroups (except the one we kept)
401 406 # (starting from the last subgroup for performance and
402 407 # sanity reasons)
403 408 for i in reversed(matching):
404 409 del groups[i]
405 410 else:
406 411 # This is a new head. We create a new subgroup for it.
407 412 targetidx = len(groups)
408 413 groups.append(([], {rev}))
409 414
410 415 gr = groups[targetidx]
411 416
412 417 # We now add the current nodes to this subgroups. This is done
413 418 # after the subgroup merging because all elements from a subgroup
414 419 # that relied on this rev must precede it.
415 420 #
416 421 # we also update the <parents> set to include the parents of the
417 422 # new nodes.
418 423 if rev == currentrev: # only display stuff in rev
419 424 gr[0].append(rev)
420 425 gr[1].remove(rev)
421 426 parents = [p for p in parentsfunc(rev) if p > node.nullrev]
422 427 gr[1].update(parents)
423 428 for p in parents:
424 429 if p not in pendingset:
425 430 pendingset.add(p)
426 431 heappush(pendingheap, -p)
427 432
428 433 # Look for a subgroup to display
429 434 #
430 435 # When unblocked is empty (if clause), we were not waiting for any
431 436 # revisions during the first iteration (if no priority was given) or
432 437 # if we emitted a whole disconnected set of the graph (reached a
433 438 # root). In that case we arbitrarily take the oldest known
434 439 # subgroup. The heuristic could probably be better.
435 440 #
436 441 # Otherwise (elif clause) if the subgroup is blocked on
437 442 # a revision we just emitted, we can safely emit it as
438 443 # well.
439 444 if not unblocked:
440 445 if len(groups) > 1: # display other subset
441 446 targetidx = 1
442 447 gr = groups[1]
443 448 elif not gr[1] & unblocked:
444 449 gr = None
445 450
446 451 if gr is not None:
447 452 # update the set of awaited revisions with the one from the
448 453 # subgroup
449 454 unblocked |= gr[1]
450 455 # output all revisions in the subgroup
451 456 for r in gr[0]:
452 457 yield r
453 458 # delete the subgroup that you just output
454 459 # unless it is groups[0] in which case you just empty it.
455 460 if targetidx:
456 461 del groups[targetidx]
457 462 else:
458 463 gr[0][:] = []
459 464 # Check if we have some subgroup waiting for revisions we are not going to
460 465 # iterate over
461 466 for g in groups:
462 467 for r in g[0]:
463 468 yield r
General Comments 0
You need to be logged in to leave comments. Login now