# HG changeset patch # User Matt Mackall # Date 1163627518 21600 # Node ID 731e739b8659268b5e7b77707412e27deaf6dc59 # Parent e50891e461e4947d186941c22f4713245602bf05 move walkchangerevs to cmdutils diff --git a/mercurial/cmdutil.py b/mercurial/cmdutil.py --- a/mercurial/cmdutil.py +++ b/mercurial/cmdutil.py @@ -592,3 +592,196 @@ def show_changeset(ui, repo, opts, buffe return t return changeset_printer(ui, repo, patch, br, buffered) +def walkchangerevs(ui, repo, pats, change, opts): + '''Iterate over files and the revs they changed in. + + Callers most commonly need to iterate backwards over the history + it is interested in. Doing so has awful (quadratic-looking) + performance, so we use iterators in a "windowed" way. + + We walk a window of revisions in the desired order. Within the + window, we first walk forwards to gather data, then in the desired + order (usually backwards) to display it. + + This function returns an (iterator, matchfn) tuple. The iterator + yields 3-tuples. They will be of one of the following forms: + + "window", incrementing, lastrev: stepping through a window, + positive if walking forwards through revs, last rev in the + sequence iterated over - use to reset state for the current window + + "add", rev, fns: out-of-order traversal of the given file names + fns, which changed during revision rev - use to gather data for + possible display + + "iter", rev, None: in-order traversal of the revs earlier iterated + over with "add" - use to display data''' + + def increasing_windows(start, end, windowsize=8, sizelimit=512): + if start < end: + while start < end: + yield start, min(windowsize, end-start) + start += windowsize + if windowsize < sizelimit: + windowsize *= 2 + else: + while start > end: + yield start, min(windowsize, start-end-1) + start -= windowsize + if windowsize < sizelimit: + windowsize *= 2 + + files, matchfn, anypats = matchpats(repo, pats, opts) + follow = opts.get('follow') or opts.get('follow_first') + + if repo.changelog.count() == 0: + return [], matchfn + + if follow: + defrange = '%s:0' % repo.changectx().rev() + else: + defrange = 'tip:0' + revs = revrange(ui, repo, opts['rev'] or [defrange]) + wanted = {} + slowpath = anypats + fncache = {} + + if not slowpath and not files: + # No files, no patterns. Display all revs. + wanted = dict.fromkeys(revs) + copies = [] + if not slowpath: + # Only files, no patterns. Check the history of each file. + def filerevgen(filelog, node): + cl_count = repo.changelog.count() + if node is None: + last = filelog.count() - 1 + else: + last = filelog.rev(node) + for i, window in increasing_windows(last, nullrev): + revs = [] + for j in xrange(i - window, i + 1): + n = filelog.node(j) + revs.append((filelog.linkrev(n), + follow and filelog.renamed(n))) + revs.reverse() + for rev in revs: + # only yield rev for which we have the changelog, it can + # happen while doing "hg log" during a pull or commit + if rev[0] < cl_count: + yield rev + def iterfiles(): + for filename in files: + yield filename, None + for filename_node in copies: + yield filename_node + minrev, maxrev = min(revs), max(revs) + for file_, node in iterfiles(): + filelog = repo.file(file_) + # A zero count may be a directory or deleted file, so + # try to find matching entries on the slow path. + if filelog.count() == 0: + slowpath = True + break + for rev, copied in filerevgen(filelog, node): + if rev <= maxrev: + if rev < minrev: + break + fncache.setdefault(rev, []) + fncache[rev].append(file_) + wanted[rev] = 1 + if follow and copied: + copies.append(copied) + if slowpath: + if follow: + raise util.Abort(_('can only follow copies/renames for explicit ' + 'file names')) + + # The slow path checks files modified in every changeset. + def changerevgen(): + for i, window in increasing_windows(repo.changelog.count()-1, + nullrev): + for j in xrange(i - window, i + 1): + yield j, change(j)[3] + + for rev, changefiles in changerevgen(): + matches = filter(matchfn, changefiles) + if matches: + fncache[rev] = matches + wanted[rev] = 1 + + class followfilter: + def __init__(self, onlyfirst=False): + self.startrev = nullrev + self.roots = [] + self.onlyfirst = onlyfirst + + def match(self, rev): + def realparents(rev): + if self.onlyfirst: + return repo.changelog.parentrevs(rev)[0:1] + else: + return filter(lambda x: x != nullrev, + repo.changelog.parentrevs(rev)) + + if self.startrev == nullrev: + self.startrev = rev + return True + + if rev > self.startrev: + # forward: all descendants + if not self.roots: + self.roots.append(self.startrev) + for parent in realparents(rev): + if parent in self.roots: + self.roots.append(rev) + return True + else: + # backwards: all parents + if not self.roots: + self.roots.extend(realparents(self.startrev)) + if rev in self.roots: + self.roots.remove(rev) + self.roots.extend(realparents(rev)) + return True + + return False + + # it might be worthwhile to do this in the iterator if the rev range + # is descending and the prune args are all within that range + for rev in opts.get('prune', ()): + rev = repo.changelog.rev(repo.lookup(rev)) + ff = followfilter() + stop = min(revs[0], revs[-1]) + for x in xrange(rev, stop-1, -1): + if ff.match(x) and x in wanted: + del wanted[x] + + def iterate(): + if follow and not files: + ff = followfilter(onlyfirst=opts.get('follow_first')) + def want(rev): + if ff.match(rev) and rev in wanted: + return True + return False + else: + def want(rev): + return rev in wanted + + for i, window in increasing_windows(0, len(revs)): + yield 'window', revs[0] < revs[-1], revs[-1] + nrevs = [rev for rev in revs[i:i+window] if want(rev)] + srevs = list(nrevs) + srevs.sort() + for rev in srevs: + fns = fncache.get(rev) + if not fns: + def fns_generator(): + for f in change(rev)[3]: + if matchfn(f): + yield f + fns = fns_generator() + yield 'add', rev, fns + for rev in nrevs: + yield 'iter', rev, None + return iterate(), matchfn diff --git a/mercurial/commands.py b/mercurial/commands.py --- a/mercurial/commands.py +++ b/mercurial/commands.py @@ -49,200 +49,6 @@ def logmessage(opts): (logfile, inst.strerror)) return message -def walkchangerevs(ui, repo, pats, change, opts): - '''Iterate over files and the revs they changed in. - - Callers most commonly need to iterate backwards over the history - it is interested in. Doing so has awful (quadratic-looking) - performance, so we use iterators in a "windowed" way. - - We walk a window of revisions in the desired order. Within the - window, we first walk forwards to gather data, then in the desired - order (usually backwards) to display it. - - This function returns an (iterator, matchfn) tuple. The iterator - yields 3-tuples. They will be of one of the following forms: - - "window", incrementing, lastrev: stepping through a window, - positive if walking forwards through revs, last rev in the - sequence iterated over - use to reset state for the current window - - "add", rev, fns: out-of-order traversal of the given file names - fns, which changed during revision rev - use to gather data for - possible display - - "iter", rev, None: in-order traversal of the revs earlier iterated - over with "add" - use to display data''' - - def increasing_windows(start, end, windowsize=8, sizelimit=512): - if start < end: - while start < end: - yield start, min(windowsize, end-start) - start += windowsize - if windowsize < sizelimit: - windowsize *= 2 - else: - while start > end: - yield start, min(windowsize, start-end-1) - start -= windowsize - if windowsize < sizelimit: - windowsize *= 2 - - files, matchfn, anypats = cmdutil.matchpats(repo, pats, opts) - follow = opts.get('follow') or opts.get('follow_first') - - if repo.changelog.count() == 0: - return [], matchfn - - if follow: - defrange = '%s:0' % repo.changectx().rev() - else: - defrange = 'tip:0' - revs = cmdutil.revrange(ui, repo, opts['rev'] or [defrange]) - wanted = {} - slowpath = anypats - fncache = {} - - if not slowpath and not files: - # No files, no patterns. Display all revs. - wanted = dict.fromkeys(revs) - copies = [] - if not slowpath: - # Only files, no patterns. Check the history of each file. - def filerevgen(filelog, node): - cl_count = repo.changelog.count() - if node is None: - last = filelog.count() - 1 - else: - last = filelog.rev(node) - for i, window in increasing_windows(last, nullrev): - revs = [] - for j in xrange(i - window, i + 1): - n = filelog.node(j) - revs.append((filelog.linkrev(n), - follow and filelog.renamed(n))) - revs.reverse() - for rev in revs: - # only yield rev for which we have the changelog, it can - # happen while doing "hg log" during a pull or commit - if rev[0] < cl_count: - yield rev - def iterfiles(): - for filename in files: - yield filename, None - for filename_node in copies: - yield filename_node - minrev, maxrev = min(revs), max(revs) - for file_, node in iterfiles(): - filelog = repo.file(file_) - # A zero count may be a directory or deleted file, so - # try to find matching entries on the slow path. - if filelog.count() == 0: - slowpath = True - break - for rev, copied in filerevgen(filelog, node): - if rev <= maxrev: - if rev < minrev: - break - fncache.setdefault(rev, []) - fncache[rev].append(file_) - wanted[rev] = 1 - if follow and copied: - copies.append(copied) - if slowpath: - if follow: - raise util.Abort(_('can only follow copies/renames for explicit ' - 'file names')) - - # The slow path checks files modified in every changeset. - def changerevgen(): - for i, window in increasing_windows(repo.changelog.count()-1, - nullrev): - for j in xrange(i - window, i + 1): - yield j, change(j)[3] - - for rev, changefiles in changerevgen(): - matches = filter(matchfn, changefiles) - if matches: - fncache[rev] = matches - wanted[rev] = 1 - - class followfilter: - def __init__(self, onlyfirst=False): - self.startrev = nullrev - self.roots = [] - self.onlyfirst = onlyfirst - - def match(self, rev): - def realparents(rev): - if self.onlyfirst: - return repo.changelog.parentrevs(rev)[0:1] - else: - return filter(lambda x: x != nullrev, - repo.changelog.parentrevs(rev)) - - if self.startrev == nullrev: - self.startrev = rev - return True - - if rev > self.startrev: - # forward: all descendants - if not self.roots: - self.roots.append(self.startrev) - for parent in realparents(rev): - if parent in self.roots: - self.roots.append(rev) - return True - else: - # backwards: all parents - if not self.roots: - self.roots.extend(realparents(self.startrev)) - if rev in self.roots: - self.roots.remove(rev) - self.roots.extend(realparents(rev)) - return True - - return False - - # it might be worthwhile to do this in the iterator if the rev range - # is descending and the prune args are all within that range - for rev in opts.get('prune', ()): - rev = repo.changelog.rev(repo.lookup(rev)) - ff = followfilter() - stop = min(revs[0], revs[-1]) - for x in xrange(rev, stop-1, -1): - if ff.match(x) and x in wanted: - del wanted[x] - - def iterate(): - if follow and not files: - ff = followfilter(onlyfirst=opts.get('follow_first')) - def want(rev): - if ff.match(rev) and rev in wanted: - return True - return False - else: - def want(rev): - return rev in wanted - - for i, window in increasing_windows(0, len(revs)): - yield 'window', revs[0] < revs[-1], revs[-1] - nrevs = [rev for rev in revs[i:i+window] if want(rev)] - srevs = list(nrevs) - srevs.sort() - for rev in srevs: - fns = fncache.get(rev) - if not fns: - def fns_generator(): - for f in change(rev)[3]: - if matchfn(f): - yield f - fns = fns_generator() - yield 'add', rev, fns - for rev in nrevs: - yield 'iter', rev, None - return iterate(), matchfn - def write_bundle(cg, filename=None, compress=True): """Write a bundle file and return its filename. @@ -1352,7 +1158,7 @@ def grep(ui, repo, pattern, *pats, **opt if opts['all']: cols.append(change) if opts['user']: - cols.append(ui.shortuser(getchange(r)[1])) + cols.append(ui.shortuser(get(r)[1])) if opts['files_with_matches']: c = (fn, r) if c in filerevmatches: @@ -1366,8 +1172,8 @@ def grep(ui, repo, pattern, *pats, **opt fstate = {} skip = {} - getchange = util.cachefunc(lambda r:repo.changectx(r).changeset()) - changeiter, matchfn = walkchangerevs(ui, repo, pats, getchange, opts) + get = util.cachefunc(lambda r:repo.changectx(r).changeset()) + changeiter, matchfn = cmdutil.walkchangerevs(ui, repo, pats, get, opts) count = 0 incrementing = False follow = opts.get('follow') @@ -1664,8 +1470,8 @@ def log(ui, repo, *pats, **opts): files and full commit message is shown. """ - getchange = util.cachefunc(lambda r:repo.changectx(r).changeset()) - changeiter, matchfn = walkchangerevs(ui, repo, pats, getchange, opts) + get = util.cachefunc(lambda r:repo.changectx(r).changeset()) + changeiter, matchfn = cmdutil.walkchangerevs(ui, repo, pats, get, opts) if opts['limit']: try: @@ -1725,7 +1531,7 @@ def log(ui, repo, *pats, **opts): continue if opts['keyword']: - changes = getchange(rev) + changes = get(rev) miss = 0 for k in [kw.lower() for kw in opts['keyword']]: if not (k in changes[1].lower() or @@ -1738,8 +1544,8 @@ def log(ui, repo, *pats, **opts): copies = [] if opts.get('copies') and rev: - mf = getchange(rev)[0] - for fn in getchange(rev)[3]: + mf = get(rev)[0] + for fn in get(rev)[3]: rename = getrenamed(fn, rev, mf) if rename: copies.append((fn, rename[0]))