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