def iterate(): if follow and not match.files(): ff = followfilter(onlyfirst=opts.get('follow_first')) def want(rev): return ff.match(rev) and rev in wanted else: def want(rev): return rev in wanted for i, window in increasing_windows(0, len(revs)): change = util.cachefunc(repo.changectx) nrevs = [rev for rev in revs[i:i + window] if want(rev)] for rev in sorted(nrevs): fns = fncache.get(rev) ctx = change(rev) if not fns: def fns_generator(): for f in ctx.files(): if match(f): yield f fns = fns_generator() prepare(ctx, fns) for rev in nrevs: yield change(rev)
def finddate(ui, repo, date): """Find the tipmost changeset that matches the given date spec""" df = util.matchdate(date) get = util.cachefunc(lambda r: repo.changectx(r).changeset()) changeiter, matchfn = walkchangerevs(ui, repo, [], get, {'rev': None}) results = {} for st, rev, fns in changeiter: if st == 'add': d = get(rev)[2] if df(d[0]): results[rev] = d elif st == 'iter': if rev in results: ui.status("Found revision %s from %s\n" % (rev, util.datestr(results[rev]))) return str(rev) raise util.Abort(_("revision matching date not found"))
def finddate(ui, repo, date): """Find the tipmost changeset that matches the given date spec""" df = util.matchdate(date) get = util.cachefunc(lambda r: repo.changectx(r).changeset()) changeiter, matchfn = walkchangerevs(ui, repo, [], get, {'rev':None}) results = {} for st, rev, fns in changeiter: if st == 'add': d = get(rev)[2] if df(d[0]): results[rev] = d elif st == 'iter': if rev in results: ui.status("Found revision %s from %s\n" % (rev, util.datestr(results[rev]))) return str(rev) raise util.Abort(_("revision matching date not found"))
def walkchangerevs(repo, match, opts, prepare): '''Iterate over files and the revs in which they changed. Callers most commonly need to iterate backwards over the history in which they are interested. 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 yielding contexts. Before yielding each context, the iterator will first call the prepare function on each context in the window in forward order.''' 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 follow = opts.get('follow') or opts.get('follow_first') if not len(repo): return [] if follow: defrange = '%s:0' % repo['.'].rev() else: defrange = '-1:0' revs = revrange(repo, opts['rev'] or [defrange]) wanted = set() slowpath = match.anypats() or (match.files() and opts.get('removed')) fncache = {} change = util.cachefunc(repo.changectx) if not slowpath and not match.files(): # No files, no patterns. Display all revs. wanted = set(revs) copies = [] if not slowpath: # Only files, no patterns. Check the history of each file. def filerevgen(filelog, node): cl_count = len(repo) if node is None: last = len(filelog) - 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(j), follow and filelog.renamed(n))) for rev in reversed(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 match.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_) if not len(filelog): if node is None: # A zero count may be a directory or deleted file, so # try to find matching entries on the slow path. if follow: raise util.Abort( _('cannot follow nonexistent file: "%s"') % file_) slowpath = True break else: continue for rev, copied in filerevgen(filelog, node): if rev <= maxrev: if rev < minrev: break fncache.setdefault(rev, []) fncache[rev].append(file_) wanted.add(rev) if follow and copied: copies.append(copied) if slowpath: if follow: raise util.Abort( _('can only follow copies/renames for explicit ' 'filenames')) # The slow path checks files modified in every changeset. def changerevgen(): for i, window in increasing_windows(len(repo) - 1, nullrev): for j in xrange(i - window, i + 1): yield change(j) for ctx in changerevgen(): matches = filter(match, ctx.files()) if matches: fncache[ctx.rev()] = matches wanted.add(ctx.rev()) class followfilter(object): def __init__(self, onlyfirst=False): self.startrev = nullrev self.roots = set() 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.add(self.startrev) for parent in realparents(rev): if parent in self.roots: self.roots.add(rev) return True else: # backwards: all parents if not self.roots: self.roots.update(realparents(self.startrev)) if rev in self.roots: self.roots.remove(rev) self.roots.update(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): wanted.discard(x) def iterate(): if follow and not match.files(): ff = followfilter(onlyfirst=opts.get('follow_first')) def want(rev): return ff.match(rev) and rev in wanted else: def want(rev): return rev in wanted for i, window in increasing_windows(0, len(revs)): change = util.cachefunc(repo.changectx) nrevs = [rev for rev in revs[i:i + window] if want(rev)] for rev in sorted(nrevs): fns = fncache.get(rev) ctx = change(rev) if not fns: def fns_generator(): for f in ctx.files(): if match(f): yield f fns = fns_generator() prepare(ctx, fns) for rev in nrevs: yield change(rev) return iterate()
def walkchangerevs(repo, match, opts, prepare): '''Iterate over files and the revs in which they changed. Callers most commonly need to iterate backwards over the history in which they are interested. 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 yielding contexts. Before yielding each context, the iterator will first call the prepare function on each context in the window in forward order.''' 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 follow = opts.get('follow') or opts.get('follow_first') if not len(repo): return [] if follow: defrange = '%s:0' % repo['.'].rev() else: defrange = '-1:0' revs = scmutil.revrange(repo, opts['rev'] or [defrange]) if not revs: return [] wanted = set() slowpath = match.anypats() or (match.files() and opts.get('removed')) fncache = {} change = util.cachefunc(repo.changectx) # First step is to fill wanted, the set of revisions that we want to yield. # When it does not induce extra cost, we also fill fncache for revisions in # wanted: a cache of filenames that were changed (ctx.files()) and that # match the file filtering conditions. if not slowpath and not match.files(): # No files, no patterns. Display all revs. wanted = set(revs) copies = [] if not slowpath: # We only have to read through the filelog to find wanted revisions minrev, maxrev = min(revs), max(revs) def filerevgen(filelog, last): """ Only files, no patterns. Check the history of each file. Examines filelog entries within minrev, maxrev linkrev range Returns an iterator yielding (linkrev, parentlinkrevs, copied) tuples in backwards order """ cl_count = len(repo) revs = [] for j in xrange(0, last + 1): linkrev = filelog.linkrev(j) if linkrev < minrev: continue # only yield rev for which we have the changelog, it can # happen while doing "hg log" during a pull or commit if linkrev >= cl_count: break parentlinkrevs = [] for p in filelog.parentrevs(j): if p != nullrev: parentlinkrevs.append(filelog.linkrev(p)) n = filelog.node(j) revs.append((linkrev, parentlinkrevs, follow and filelog.renamed(n))) return reversed(revs) def iterfiles(): for filename in match.files(): yield filename, None for filename_node in copies: yield filename_node for file_, node in iterfiles(): filelog = repo.file(file_) if not len(filelog): if node is None: # A zero count may be a directory or deleted file, so # try to find matching entries on the slow path. if follow: raise util.Abort( _('cannot follow nonexistent file: "%s"') % file_) slowpath = True break else: continue if node is None: last = len(filelog) - 1 else: last = filelog.rev(node) # keep track of all ancestors of the file ancestors = set([filelog.linkrev(last)]) # iterate from latest to oldest revision for rev, flparentlinkrevs, copied in filerevgen(filelog, last): if not follow: if rev > maxrev: continue else: # Note that last might not be the first interesting # rev to us: # if the file has been changed after maxrev, we'll # have linkrev(last) > maxrev, and we still need # to explore the file graph if rev not in ancestors: continue # XXX insert 1327 fix here if flparentlinkrevs: ancestors.update(flparentlinkrevs) fncache.setdefault(rev, []).append(file_) wanted.add(rev) if copied: copies.append(copied) if slowpath: # We have to read the changelog to match filenames against # changed files if follow: raise util.Abort( _('can only follow copies/renames for explicit ' 'filenames')) # The slow path checks files modified in every changeset. for i in sorted(revs): ctx = change(i) matches = filter(match, ctx.files()) if matches: fncache[i] = matches wanted.add(i) class followfilter(object): def __init__(self, onlyfirst=False): self.startrev = nullrev self.roots = set() 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.add(self.startrev) for parent in realparents(rev): if parent in self.roots: self.roots.add(rev) return True else: # backwards: all parents if not self.roots: self.roots.update(realparents(self.startrev)) if rev in self.roots: self.roots.remove(rev) self.roots.update(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): wanted.discard(x) # Now that wanted is correctly initialized, we can iterate over the # revision range, yielding only revisions in wanted. def iterate(): if follow and not match.files(): ff = followfilter(onlyfirst=opts.get('follow_first')) def want(rev): return ff.match(rev) and rev in wanted else: def want(rev): return rev in wanted for i, window in increasing_windows(0, len(revs)): nrevs = [rev for rev in revs[i:i + window] if want(rev)] for rev in sorted(nrevs): fns = fncache.get(rev) ctx = change(rev) if not fns: def fns_generator(): for f in ctx.files(): if match(f): yield f fns = fns_generator() prepare(ctx, fns) for rev in nrevs: yield change(rev) return iterate()
def walkchangerevs(repo, match, opts, prepare): '''Iterate over files and the revs in which they changed. Callers most commonly need to iterate backwards over the history in which they are interested. 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 yielding contexts. Before yielding each context, the iterator will first call the prepare function on each context in the window in forward order.''' 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 follow = opts.get('follow') or opts.get('follow_first') if not len(repo): return [] if follow: defrange = '%s:0' % repo['.'].rev() else: defrange = '-1:0' revs = revrange(repo, opts['rev'] or [defrange]) if not revs: return [] wanted = set() slowpath = match.anypats() or (match.files() and opts.get('removed')) fncache = {} change = util.cachefunc(repo.changectx) # First step is to fill wanted, the set of revisions that we want to yield. # When it does not induce extra cost, we also fill fncache for revisions in # wanted: a cache of filenames that were changed (ctx.files()) and that # match the file filtering conditions. if not slowpath and not match.files(): # No files, no patterns. Display all revs. wanted = set(revs) copies = [] if not slowpath: # We only have to read through the filelog to find wanted revisions minrev, maxrev = min(revs), max(revs) def filerevgen(filelog, last): """ Only files, no patterns. Check the history of each file. Examines filelog entries within minrev, maxrev linkrev range Returns an iterator yielding (linkrev, parentlinkrevs, copied) tuples in backwards order """ cl_count = len(repo) revs = [] for j in xrange(0, last + 1): linkrev = filelog.linkrev(j) if linkrev < minrev: continue # only yield rev for which we have the changelog, it can # happen while doing "hg log" during a pull or commit if linkrev >= cl_count: break parentlinkrevs = [] for p in filelog.parentrevs(j): if p != nullrev: parentlinkrevs.append(filelog.linkrev(p)) n = filelog.node(j) revs.append((linkrev, parentlinkrevs, follow and filelog.renamed(n))) return reversed(revs) def iterfiles(): for filename in match.files(): yield filename, None for filename_node in copies: yield filename_node for file_, node in iterfiles(): filelog = repo.file(file_) if not len(filelog): if node is None: # A zero count may be a directory or deleted file, so # try to find matching entries on the slow path. if follow: raise util.Abort( _('cannot follow nonexistent file: "%s"') % file_) slowpath = True break else: continue if node is None: last = len(filelog) - 1 else: last = filelog.rev(node) # keep track of all ancestors of the file ancestors = set([filelog.linkrev(last)]) # iterate from latest to oldest revision for rev, flparentlinkrevs, copied in filerevgen(filelog, last): if not follow: if rev > maxrev: continue else: # Note that last might not be the first interesting # rev to us: # if the file has been changed after maxrev, we'll # have linkrev(last) > maxrev, and we still need # to explore the file graph if rev not in ancestors: continue # XXX insert 1327 fix here if flparentlinkrevs: ancestors.update(flparentlinkrevs) fncache.setdefault(rev, []).append(file_) wanted.add(rev) if copied: copies.append(copied) if slowpath: # We have to read the changelog to match filenames against # changed files if follow: raise util.Abort(_('can only follow copies/renames for explicit ' 'filenames')) # The slow path checks files modified in every changeset. for i in sorted(revs): ctx = change(i) matches = filter(match, ctx.files()) if matches: fncache[i] = matches wanted.add(i) class followfilter(object): def __init__(self, onlyfirst=False): self.startrev = nullrev self.roots = set() 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.add(self.startrev) for parent in realparents(rev): if parent in self.roots: self.roots.add(rev) return True else: # backwards: all parents if not self.roots: self.roots.update(realparents(self.startrev)) if rev in self.roots: self.roots.remove(rev) self.roots.update(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): wanted.discard(x) # Now that wanted is correctly initialized, we can iterate over the # revision range, yielding only revisions in wanted. def iterate(): if follow and not match.files(): ff = followfilter(onlyfirst=opts.get('follow_first')) def want(rev): return ff.match(rev) and rev in wanted else: def want(rev): return rev in wanted for i, window in increasing_windows(0, len(revs)): nrevs = [rev for rev in revs[i:i + window] if want(rev)] for rev in sorted(nrevs): fns = fncache.get(rev) ctx = change(rev) if not fns: def fns_generator(): for f in ctx.files(): if match(f): yield f fns = fns_generator() prepare(ctx, fns) for rev in nrevs: yield change(rev) return iterate()
def annotate(self, follow=False, linenumber=None): '''returns a list of tuples of (ctx, line) for each line in the file, where ctx is the filectx of the node where that line was last changed. This returns tuples of ((ctx, linenumber), line) for each line, if "linenumber" parameter is NOT "None". In such tuples, linenumber means one at the first appearance in the managed file. To reduce annotation cost, this returns fixed value(False is used) as linenumber, if "linenumber" parameter is "False".''' def decorate_compat(text, rev): return ([rev] * len(text.splitlines()), text) def without_linenumber(text, rev): return ([(rev, False)] * len(text.splitlines()), text) def with_linenumber(text, rev): size = len(text.splitlines()) return ([(rev, i) for i in xrange(1, size + 1)], text) decorate = (((linenumber is None) and decorate_compat) or (linenumber and with_linenumber) or without_linenumber) def pair(parent, child): for a1, a2, b1, b2 in bdiff.blocks(parent[1], child[1]): child[0][b1:b2] = parent[0][a1:a2] return child getlog = util.cachefunc(lambda x: self._repo.file(x)) def getctx(path, fileid): log = path == self._path and self._filelog or getlog(path) return filectx(self._repo, path, fileid=fileid, filelog=log) getctx = util.cachefunc(getctx) def parents(f): # we want to reuse filectx objects as much as possible p = f._path if f._filerev is None: # working dir pl = [(n.path(), n.filerev()) for n in f.parents()] else: pl = [(p, n) for n in f._filelog.parentrevs(f._filerev)] if follow: r = f.renamed() if r: pl[0] = (r[0], getlog(r[0]).rev(r[1])) return [getctx(p, n) for p, n in pl if n != nullrev] # use linkrev to find the first changeset where self appeared if self.rev() != self.linkrev(): base = self.filectx(self.filerev()) else: base = self # find all ancestors needed = {base: 1} visit = [base] files = [base._path] while visit: f = visit.pop(0) for p in parents(f): if p not in needed: needed[p] = 1 visit.append(p) if p._path not in files: files.append(p._path) else: # count how many times we'll use this needed[p] += 1 # sort by revision (per file) which is a topological order visit = [] for f in files: fn = [(n.rev(), n) for n in needed.keys() if n._path == f] visit.extend(fn) visit.sort() hist = {} for r, f in visit: curr = decorate(f.data(), f) for p in parents(f): if p != nullid: curr = pair(hist[p], curr) # trim the history of unneeded revs needed[p] -= 1 if not needed[p]: del hist[p] hist[f] = curr return zip(hist[f][0], hist[f][1].splitlines(1))
def walkchangerevs(repo, match, opts, prepare): '''Iterate over files and the revs in which they changed. Callers most commonly need to iterate backwards over the history in which they are interested. 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 yielding contexts. Before yielding each context, the iterator will first call the prepare function on each context in the window in forward order.''' 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 follow = opts.get('follow') or opts.get('follow_first') if not len(repo): return [] if follow: defrange = '%s:0' % repo['.'].rev() else: defrange = '-1:0' revs = revrange(repo, opts['rev'] or [defrange]) wanted = set() slowpath = match.anypats() or (match.files() and opts.get('removed')) fncache = {} change = util.cachefunc(repo.changectx) if not slowpath and not match.files(): # No files, no patterns. Display all revs. wanted = set(revs) copies = [] if not slowpath: # Only files, no patterns. Check the history of each file. def filerevgen(filelog, node): cl_count = len(repo) if node is None: last = len(filelog) - 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(j), follow and filelog.renamed(n))) for rev in reversed(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 match.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_) if not len(filelog): if node is None: # A zero count may be a directory or deleted file, so # try to find matching entries on the slow path. if follow: raise util.Abort( _('cannot follow nonexistent file: "%s"') % file_) slowpath = True break else: continue for rev, copied in filerevgen(filelog, node): if rev <= maxrev: if rev < minrev: break fncache.setdefault(rev, []) fncache[rev].append(file_) wanted.add(rev) if follow and copied: copies.append(copied) if slowpath: if follow: raise util.Abort(_('can only follow copies/renames for explicit ' 'filenames')) # The slow path checks files modified in every changeset. def changerevgen(): for i, window in increasing_windows(len(repo) - 1, nullrev): for j in xrange(i - window, i + 1): yield change(j) for ctx in changerevgen(): matches = filter(match, ctx.files()) if matches: fncache[ctx.rev()] = matches wanted.add(ctx.rev()) class followfilter(object): def __init__(self, onlyfirst=False): self.startrev = nullrev self.roots = set() 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.add(self.startrev) for parent in realparents(rev): if parent in self.roots: self.roots.add(rev) return True else: # backwards: all parents if not self.roots: self.roots.update(realparents(self.startrev)) if rev in self.roots: self.roots.remove(rev) self.roots.update(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): wanted.discard(x) def iterate(): if follow and not match.files(): ff = followfilter(onlyfirst=opts.get('follow_first')) def want(rev): return ff.match(rev) and rev in wanted else: def want(rev): return rev in wanted for i, window in increasing_windows(0, len(revs)): change = util.cachefunc(repo.changectx) nrevs = [rev for rev in revs[i:i + window] if want(rev)] for rev in sorted(nrevs): fns = fncache.get(rev) ctx = change(rev) if not fns: def fns_generator(): for f in ctx.files(): if match(f): yield f fns = fns_generator() prepare(ctx, fns) for rev in nrevs: yield change(rev) return iterate()
def copies(repo, c1, c2, ca, checkdirs=False): """ Find moves and copies between context c1 and c2 """ # avoid silly behavior for update from empty dir if not c1 or not c2 or c1 == c2: return {}, {} # avoid silly behavior for parent -> working dir if c2.node() == None and c1.node() == repo.dirstate.parents()[0]: return repo.dirstate.copies(), {} limit = _findlimit(repo, c1.rev(), c2.rev()) m1 = c1.manifest() m2 = c2.manifest() ma = ca.manifest() def makectx(f, n): if len(n) != 20: # in a working context? if c1.rev() is None: return c1.filectx(f) return c2.filectx(f) return repo.filectx(f, fileid=n) ctx = util.cachefunc(makectx) copy = {} fullcopy = {} diverge = {} def checkcopies(f, m1, m2): '''check possible copies of f from m1 to m2''' c1 = ctx(f, m1[f]) for of in _findoldnames(c1, limit): fullcopy[f] = of # remember for dir rename detection if of in m2: # original file not in other manifest? # if the original file is unchanged on the other branch, # no merge needed if m2[of] != ma.get(of): c2 = ctx(of, m2[of]) ca = c1.ancestor(c2) # related and named changed on only one side? if ca and (ca.path() == f or ca.path() == c2.path()): if c1 != ca or c2 != ca: # merge needed? copy[f] = of elif of in ma: diverge.setdefault(of, []).append(f) repo.ui.debug(_(" searching for copies back to rev %d\n") % limit) u1 = _nonoverlap(m1, m2, ma) u2 = _nonoverlap(m2, m1, ma) if u1: repo.ui.debug(_(" unmatched files in local:\n %s\n") % "\n ".join(u1)) if u2: repo.ui.debug(_(" unmatched files in other:\n %s\n") % "\n ".join(u2)) for f in u1: checkcopies(f, m1, m2) for f in u2: checkcopies(f, m2, m1) diverge2 = {} for of, fl in diverge.items(): if len(fl) == 1: del diverge[of] # not actually divergent else: diverge2.update(dict.fromkeys(fl)) # reverse map for below if fullcopy: repo.ui.debug(_(" all copies found (* = to merge, ! = divergent):\n")) for f in fullcopy: note = "" if f in copy: note += "*" if f in diverge2: note += "!" repo.ui.debug(_(" %s -> %s %s\n") % (f, fullcopy[f], note)) del diverge2 if not fullcopy or not checkdirs: return copy, diverge repo.ui.debug(_(" checking for directory renames\n")) # generate a directory move map d1, d2 = _dirs(m1), _dirs(m2) invalid = {} dirmove = {} # examine each file copy for a potential directory move, which is # when all the files in a directory are moved to a new directory for dst, src in fullcopy.iteritems(): dsrc, ddst = _dirname(src), _dirname(dst) if dsrc in invalid: # already seen to be uninteresting continue elif dsrc in d1 and ddst in d1: # directory wasn't entirely moved locally invalid[dsrc] = True elif dsrc in d2 and ddst in d2: # directory wasn't entirely moved remotely invalid[dsrc] = True elif dsrc in dirmove and dirmove[dsrc] != ddst: # files from the same directory moved to two different places invalid[dsrc] = True else: # looks good so far dirmove[dsrc + "/"] = ddst + "/" for i in invalid: if i in dirmove: del dirmove[i] del d1, d2, invalid if not dirmove: return copy, diverge for d in dirmove: repo.ui.debug(_(" dir %s -> %s\n") % (d, dirmove[d])) # check unaccounted nonoverlapping files against directory moves for f in u1 + u2: if f not in fullcopy: for d in dirmove: if f.startswith(d): # new file added in a directory that was moved, move it df = dirmove[d] + f[len(d):] if df not in copy: copy[f] = df repo.ui.debug(_(" file %s -> %s\n") % (f, copy[f])) break return copy, diverge
def findcopies(repo, m1, m2, ma, limit): """ Find moves and copies between m1 and m2 back to limit linkrev """ def nonoverlap(d1, d2, d3): "Return list of elements in d1 not in d2 or d3" l = [d for d in d1 if d not in d3 and d not in d2] l.sort() return l def dirname(f): s = f.rfind("/") if s == -1: return "" return f[:s] def dirs(files): d = {} for f in files: f = dirname(f) while f not in d: d[f] = True f = dirname(f) return d wctx = repo.workingctx() def makectx(f, n): if len(n) == 20: return repo.filectx(f, fileid=n) return wctx.filectx(f) ctx = util.cachefunc(makectx) def findold(fctx): "find files that path was copied from, back to linkrev limit" old = {} seen = {} orig = fctx.path() visit = [fctx] while visit: fc = visit.pop() s = str(fc) if s in seen: continue seen[s] = 1 if fc.path() != orig and fc.path() not in old: old[fc.path()] = 1 if fc.rev() < limit: continue visit += fc.parents() old = old.keys() old.sort() return old copy = {} fullcopy = {} diverge = {} def checkcopies(c, man, aman): '''check possible copies for filectx c''' for of in findold(c): fullcopy[c.path()] = of # remember for dir rename detection if of not in man: # original file not in other manifest? if of in ma: diverge.setdefault(of, []).append(c.path()) continue # if the original file is unchanged on the other branch, # no merge needed if man[of] == aman.get(of): continue c2 = ctx(of, man[of]) ca = c.ancestor(c2) if not ca: # unrelated? continue # named changed on only one side? if ca.path() == c.path() or ca.path() == c2.path(): if c == ca and c2 == ca: # no merge needed, ignore copy continue copy[c.path()] = of if not repo.ui.configbool("merge", "followcopies", True): return {}, {} # avoid silly behavior for update from empty dir if not m1 or not m2 or not ma: return {}, {} repo.ui.debug(_(" searching for copies back to rev %d\n") % limit) u1 = nonoverlap(m1, m2, ma) u2 = nonoverlap(m2, m1, ma) if u1: repo.ui.debug( _(" unmatched files in local:\n %s\n") % "\n ".join(u1)) if u2: repo.ui.debug( _(" unmatched files in other:\n %s\n") % "\n ".join(u2)) for f in u1: checkcopies(ctx(f, m1[f]), m2, ma) for f in u2: checkcopies(ctx(f, m2[f]), m1, ma) d2 = {} for of, fl in diverge.items(): for f in fl: fo = list(fl) fo.remove(f) d2[f] = (of, fo) if fullcopy: repo.ui.debug(_(" all copies found (* = to merge, ! = divergent):\n")) for f in fullcopy: note = "" if f in copy: note += "*" if f in diverge: note += "!" repo.ui.debug(_(" %s -> %s %s\n") % (f, fullcopy[f], note)) if not fullcopy or not repo.ui.configbool("merge", "followdirs", True): return copy, diverge repo.ui.debug(_(" checking for directory renames\n")) # generate a directory move map d1, d2 = dirs(m1), dirs(m2) invalid = {} dirmove = {} # examine each file copy for a potential directory move, which is # when all the files in a directory are moved to a new directory for dst, src in fullcopy.items(): dsrc, ddst = dirname(src), dirname(dst) if dsrc in invalid: # already seen to be uninteresting continue elif dsrc in d1 and ddst in d1: # directory wasn't entirely moved locally invalid[dsrc] = True elif dsrc in d2 and ddst in d2: # directory wasn't entirely moved remotely invalid[dsrc] = True elif dsrc in dirmove and dirmove[dsrc] != ddst: # files from the same directory moved to two different places invalid[dsrc] = True else: # looks good so far dirmove[dsrc + "/"] = ddst + "/" for i in invalid: if i in dirmove: del dirmove[i] del d1, d2, invalid if not dirmove: return copy, diverge for d in dirmove: repo.ui.debug(_(" dir %s -> %s\n") % (d, dirmove[d])) # check unaccounted nonoverlapping files against directory moves for f in u1 + u2: if f not in fullcopy: for d in dirmove: if f.startswith(d): # new file added in a directory that was moved, move it copy[f] = dirmove[d] + f[len(d):] repo.ui.debug(_(" file %s -> %s\n") % (f, copy[f])) break return copy, diverge
def copies(repo, c1, c2, ca, checkdirs=False): """ Find moves and copies between context c1 and c2 """ # avoid silly behavior for update from empty dir if not c1 or not c2 or c1 == c2: return {}, {} limit = _findlimit(repo, c1.rev(), c2.rev()) m1 = c1.manifest() m2 = c2.manifest() ma = ca.manifest() def makectx(f, n): if len(n) != 20: # in a working context? if c1.rev() is None: return c1.filectx(f) return c2.filectx(f) return repo.filectx(f, fileid=n) ctx = util.cachefunc(makectx) copy = {} fullcopy = {} diverge = {} def checkcopies(f, m1, m2): '''check possible copies of f from m1 to m2''' c1 = ctx(f, m1[f]) for of in _findoldnames(c1, limit): fullcopy[f] = of # remember for dir rename detection if of in m2: # original file not in other manifest? # if the original file is unchanged on the other branch, # no merge needed if m2[of] != ma.get(of): c2 = ctx(of, m2[of]) ca = c1.ancestor(c2) # related and named changed on only one side? if ca and (ca.path() == f or ca.path() == c2.path()): if c1 != ca or c2 != ca: # merge needed? copy[f] = of elif of in ma: diverge.setdefault(of, []).append(f) repo.ui.debug(_(" searching for copies back to rev %d\n") % limit) u1 = _nonoverlap(m1, m2, ma) u2 = _nonoverlap(m2, m1, ma) if u1: repo.ui.debug(_(" unmatched files in local:\n %s\n") % "\n ".join(u1)) if u2: repo.ui.debug(_(" unmatched files in other:\n %s\n") % "\n ".join(u2)) for f in u1: checkcopies(f, m1, m2) for f in u2: checkcopies(f, m2, m1) diverge2 = {} for of, fl in diverge.items(): if len(fl) == 1: del diverge[of] # not actually divergent else: diverge2.update(dict.fromkeys(fl)) # reverse map for below if fullcopy: repo.ui.debug(_(" all copies found (* = to merge, ! = divergent):\n")) for f in fullcopy: note = "" if f in copy: note += "*" if f in diverge2: note += "!" repo.ui.debug(_(" %s -> %s %s\n") % (f, fullcopy[f], note)) del diverge2 if not fullcopy or not checkdirs: return copy, diverge repo.ui.debug(_(" checking for directory renames\n")) # generate a directory move map d1, d2 = _dirs(m1), _dirs(m2) invalid = {} dirmove = {} # examine each file copy for a potential directory move, which is # when all the files in a directory are moved to a new directory for dst, src in fullcopy.items(): dsrc, ddst = _dirname(src), _dirname(dst) if dsrc in invalid: # already seen to be uninteresting continue elif dsrc in d1 and ddst in d1: # directory wasn't entirely moved locally invalid[dsrc] = True elif dsrc in d2 and ddst in d2: # directory wasn't entirely moved remotely invalid[dsrc] = True elif dsrc in dirmove and dirmove[dsrc] != ddst: # files from the same directory moved to two different places invalid[dsrc] = True else: # looks good so far dirmove[dsrc + "/"] = ddst + "/" for i in invalid: if i in dirmove: del dirmove[i] del d1, d2, invalid if not dirmove: return copy, diverge for d in dirmove: repo.ui.debug(_(" dir %s -> %s\n") % (d, dirmove[d])) # check unaccounted nonoverlapping files against directory moves for f in u1 + u2: if f not in fullcopy: for d in dirmove: if f.startswith(d): # new file added in a directory that was moved, move it df = dirmove[d] + f[len(d):] if df not in copy: copy[f] = df repo.ui.debug(_(" file %s -> %s\n") % (f, copy[f])) break return copy, diverge