/mercurial/hbisect.py
Python | 260 lines | 178 code | 28 blank | 54 comment | 52 complexity | b872535ad747df0774e09d0836bc58e4 MD5 | raw file
Possible License(s): GPL-2.0
- # changelog bisection for mercurial
- #
- # Copyright 2007 Matt Mackall
- # Copyright 2005, 2006 Benoit Boissinot <benoit.boissinot@ens-lyon.org>
- #
- # Inspired by git bisect, extension skeleton taken from mq.py.
- #
- # This software may be used and distributed according to the terms of the
- # GNU General Public License version 2 or any later version.
- import os
- import error
- from i18n import _
- from node import short, hex
- import util
- def bisect(changelog, state):
- """find the next node (if any) for testing during a bisect search.
- returns a (nodes, number, good) tuple.
- 'nodes' is the final result of the bisect if 'number' is 0.
- Otherwise 'number' indicates the remaining possible candidates for
- the search and 'nodes' contains the next bisect target.
- 'good' is True if bisect is searching for a first good changeset, False
- if searching for a first bad one.
- """
- clparents = changelog.parentrevs
- skip = set([changelog.rev(n) for n in state['skip']])
- def buildancestors(bad, good):
- # only the earliest bad revision matters
- badrev = min([changelog.rev(n) for n in bad])
- goodrevs = [changelog.rev(n) for n in good]
- goodrev = min(goodrevs)
- # build visit array
- ancestors = [None] * (len(changelog) + 1) # an extra for [-1]
- # set nodes descended from goodrevs
- for rev in goodrevs:
- ancestors[rev] = []
- for rev in changelog.revs(goodrev + 1):
- for prev in clparents(rev):
- if ancestors[prev] == []:
- ancestors[rev] = []
- # clear good revs from array
- for rev in goodrevs:
- ancestors[rev] = None
- for rev in changelog.revs(len(changelog), goodrev):
- if ancestors[rev] is None:
- for prev in clparents(rev):
- ancestors[prev] = None
- if ancestors[badrev] is None:
- return badrev, None
- return badrev, ancestors
- good = False
- badrev, ancestors = buildancestors(state['bad'], state['good'])
- if not ancestors: # looking for bad to good transition?
- good = True
- badrev, ancestors = buildancestors(state['good'], state['bad'])
- bad = changelog.node(badrev)
- if not ancestors: # now we're confused
- if (len(state['bad']) == 1 and len(state['good']) == 1 and
- state['bad'] != state['good']):
- raise util.Abort(_("starting revisions are not directly related"))
- raise util.Abort(_("inconsistent state, %s:%s is good and bad")
- % (badrev, short(bad)))
- # build children dict
- children = {}
- visit = util.deque([badrev])
- candidates = []
- while visit:
- rev = visit.popleft()
- if ancestors[rev] == []:
- candidates.append(rev)
- for prev in clparents(rev):
- if prev != -1:
- if prev in children:
- children[prev].append(rev)
- else:
- children[prev] = [rev]
- visit.append(prev)
- candidates.sort()
- # have we narrowed it down to one entry?
- # or have all other possible candidates besides 'bad' have been skipped?
- tot = len(candidates)
- unskipped = [c for c in candidates if (c not in skip) and (c != badrev)]
- if tot == 1 or not unskipped:
- return ([changelog.node(rev) for rev in candidates], 0, good)
- perfect = tot // 2
- # find the best node to test
- best_rev = None
- best_len = -1
- poison = set()
- for rev in candidates:
- if rev in poison:
- # poison children
- poison.update(children.get(rev, []))
- continue
- a = ancestors[rev] or [rev]
- ancestors[rev] = None
- x = len(a) # number of ancestors
- y = tot - x # number of non-ancestors
- value = min(x, y) # how good is this test?
- if value > best_len and rev not in skip:
- best_len = value
- best_rev = rev
- if value == perfect: # found a perfect candidate? quit early
- break
- if y < perfect and rev not in skip: # all downhill from here?
- # poison children
- poison.update(children.get(rev, []))
- continue
- for c in children.get(rev, []):
- if ancestors[c]:
- ancestors[c] = list(set(ancestors[c] + a))
- else:
- ancestors[c] = a + [c]
- assert best_rev is not None
- best_node = changelog.node(best_rev)
- return ([best_node], tot, good)
- def load_state(repo):
- state = {'current': [], 'good': [], 'bad': [], 'skip': []}
- if os.path.exists(repo.join("bisect.state")):
- for l in repo.opener("bisect.state"):
- kind, node = l[:-1].split()
- node = repo.lookup(node)
- if kind not in state:
- raise util.Abort(_("unknown bisect kind %s") % kind)
- state[kind].append(node)
- return state
- def save_state(repo, state):
- f = repo.opener("bisect.state", "w", atomictemp=True)
- wlock = repo.wlock()
- try:
- for kind in sorted(state):
- for node in state[kind]:
- f.write("%s %s\n" % (kind, hex(node)))
- f.close()
- finally:
- wlock.release()
- def get(repo, status):
- """
- Return a list of revision(s) that match the given status:
- - ``good``, ``bad``, ``skip``: csets explicitly marked as good/bad/skip
- - ``goods``, ``bads`` : csets topologically good/bad
- - ``range`` : csets taking part in the bisection
- - ``pruned`` : csets that are goods, bads or skipped
- - ``untested`` : csets whose fate is yet unknown
- - ``ignored`` : csets ignored due to DAG topology
- - ``current`` : the cset currently being bisected
- """
- state = load_state(repo)
- if status in ('good', 'bad', 'skip', 'current'):
- return map(repo.changelog.rev, state[status])
- else:
- # In the following sets, we do *not* call 'bisect()' with more
- # than one level of recursion, because that can be very, very
- # time consuming. Instead, we always develop the expression as
- # much as possible.
- # 'range' is all csets that make the bisection:
- # - have a good ancestor and a bad descendant, or conversely
- # that's because the bisection can go either way
- range = '( bisect(bad)::bisect(good) | bisect(good)::bisect(bad) )'
- _t = repo.revs('bisect(good)::bisect(bad)')
- # The sets of topologically good or bad csets
- if len(_t) == 0:
- # Goods are topologically after bads
- goods = 'bisect(good)::' # Pruned good csets
- bads = '::bisect(bad)' # Pruned bad csets
- else:
- # Goods are topologically before bads
- goods = '::bisect(good)' # Pruned good csets
- bads = 'bisect(bad)::' # Pruned bad csets
- # 'pruned' is all csets whose fate is already known: good, bad, skip
- skips = 'bisect(skip)' # Pruned skipped csets
- pruned = '( (%s) | (%s) | (%s) )' % (goods, bads, skips)
- # 'untested' is all cset that are- in 'range', but not in 'pruned'
- untested = '( (%s) - (%s) )' % (range, pruned)
- # 'ignored' is all csets that were not used during the bisection
- # due to DAG topology, but may however have had an impact.
- # E.g., a branch merged between bads and goods, but whose branch-
- # point is out-side of the range.
- iba = '::bisect(bad) - ::bisect(good)' # Ignored bads' ancestors
- iga = '::bisect(good) - ::bisect(bad)' # Ignored goods' ancestors
- ignored = '( ( (%s) | (%s) ) - (%s) )' % (iba, iga, range)
- if status == 'range':
- return repo.revs(range)
- elif status == 'pruned':
- return repo.revs(pruned)
- elif status == 'untested':
- return repo.revs(untested)
- elif status == 'ignored':
- return repo.revs(ignored)
- elif status == "goods":
- return repo.revs(goods)
- elif status == "bads":
- return repo.revs(bads)
- else:
- raise error.ParseError(_('invalid bisect state'))
- def label(repo, node):
- rev = repo.changelog.rev(node)
- # Try explicit sets
- if rev in get(repo, 'good'):
- # i18n: bisect changeset status
- return _('good')
- if rev in get(repo, 'bad'):
- # i18n: bisect changeset status
- return _('bad')
- if rev in get(repo, 'skip'):
- # i18n: bisect changeset status
- return _('skipped')
- if rev in get(repo, 'untested') or rev in get(repo, 'current'):
- # i18n: bisect changeset status
- return _('untested')
- if rev in get(repo, 'ignored'):
- # i18n: bisect changeset status
- return _('ignored')
- # Try implicit sets
- if rev in get(repo, 'goods'):
- # i18n: bisect changeset status
- return _('good (implicit)')
- if rev in get(repo, 'bads'):
- # i18n: bisect changeset status
- return _('bad (implicit)')
- return None
- def shortlabel(label):
- if label:
- return label[0].upper()
- return None