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