diff --git a/hgext/narrow/__init__.py b/hgext/narrow/__init__.py new file mode 100644 --- /dev/null +++ b/hgext/narrow/__init__.py @@ -0,0 +1,111 @@ +# __init__.py - narrowhg extension +# +# Copyright 2017 Google, Inc. +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. +'''create clones which fetch history data for subset of files (EXPERIMENTAL)''' + +from __future__ import absolute_import + +from mercurial import __version__ +if __version__.version < '3.7': + raise ImportError( + 'narrowhg requires mercurial 3.7 or newer') + +try: + from .__versionnum__ import version + __version__ = version +except ImportError: + pass + +from mercurial import ( + extensions, + hg, + localrepo, + registrar, + util, + verify as verifymod, +) + +from . import ( + narrowbundle2, + narrowchangegroup, + narrowcommands, + narrowcopies, + narrowdirstate, + narrowmerge, + narrowpatch, + narrowrepo, + narrowrevlog, + narrowtemplates, + narrowwirepeer, +) + +configtable = {} +configitem = registrar.configitem(configtable) +# Narrowhg *has* support for serving ellipsis nodes (which are used at +# least by Google's internal server), but that support is pretty +# fragile and has a lot of problems on real-world repositories that +# have complex graph topologies. This could probably be corrected, but +# absent someone needing the full support for ellipsis nodes in +# repositories with merges, it's unlikely this work will get done. As +# of this writining in late 2017, all repositories large enough for +# ellipsis nodes to be a hard requirement also enforce strictly linear +# history for other scaling reasons. +configitem('experimental', 'narrowservebrokenellipses', + default=False, + alias=[('narrow', 'serveellipses')], +) + +# Export the commands table for Mercurial to see. +cmdtable = narrowcommands.table + +localrepo.localrepository._basesupported.add(narrowrepo.requirement) + +def uisetup(ui): + """Wraps user-facing mercurial commands with narrow-aware versions.""" + narrowrevlog.setup() + narrowbundle2.setup() + narrowmerge.setup() + narrowtemplates.setup() + narrowcommands.setup() + narrowchangegroup.setup() + narrowwirepeer.uisetup() + +def reposetup(ui, repo): + """Wraps local repositories with narrow repo support.""" + if not isinstance(repo, localrepo.localrepository): + return + + if narrowrepo.requirement in repo.requirements: + narrowrepo.wraprepo(repo, True) + narrowcopies.setup(repo) + narrowdirstate.setup(repo) + narrowpatch.setup(repo) + narrowwirepeer.reposetup(repo) + +def _narrowvalidpath(orig, repo, path): + matcher = getattr(repo, 'narrowmatch', None) + if matcher is None: + return orig(repo, path) + matcher = matcher() + if matcher.visitdir(path) or matcher(path): + return orig(repo, path) + return False + +def _verifierinit(orig, self, repo, matcher=None): + # The verifier's matcher argument was desgined for narrowhg, so it should + # be None from core. If another extension passes a matcher (unlikely), + # we'll have to fail until matchers can be composed more easily. + assert matcher is None + matcher = getattr(repo, 'narrowmatch', lambda: None)() + orig(self, repo, matcher) + +def extsetup(ui): + if util.safehasattr(verifymod, '_validpath'): + extensions.wrapfunction(verifymod, '_validpath', _narrowvalidpath) + else: + extensions.wrapfunction(verifymod.verifier, '__init__', _verifierinit) + extensions.wrapfunction(hg, 'postshare', narrowrepo.wrappostshare) + extensions.wrapfunction(hg, 'copystore', narrowrepo.unsharenarrowspec) diff --git a/hgext/narrow/narrowbundle2.py b/hgext/narrow/narrowbundle2.py new file mode 100644 --- /dev/null +++ b/hgext/narrow/narrowbundle2.py @@ -0,0 +1,503 @@ +# narrowbundle2.py - bundle2 extensions for narrow repository support +# +# Copyright 2017 Google, Inc. +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +from __future__ import absolute_import + +import collections +import errno +import struct + +from mercurial.i18n import _ +from mercurial.node import ( + bin, + nullid, + nullrev, +) +from mercurial import ( + bundle2, + changegroup, + dagutil, + error, + exchange, + extensions, + repair, + util, + wireproto, +) + +from . import ( + narrowrepo, + narrowspec, +) + +narrowcap = 'narrow' +narrowacl_section = 'narrowhgacl' +changespecpart = narrowcap + ':changespec' +specpart = narrowcap + ':spec' +specpart_include = 'include' +specpart_exclude = 'exclude' +killnodesignal = 'KILL' +donesignal = 'DONE' +elidedcsheader = '>20s20s20sl' # cset id, p1, p2, len(text) +elidedmfheader = '>20s20s20s20sl' # manifest id, p1, p2, link id, len(text) +csheadersize = struct.calcsize(elidedcsheader) +mfheadersize = struct.calcsize(elidedmfheader) + +# When advertising capabilities, always include narrow clone support. +def getrepocaps_narrow(orig, repo, **kwargs): + caps = orig(repo, **kwargs) + caps[narrowcap] = ['v0'] + return caps + +def _computeellipsis(repo, common, heads, known, match, depth=None): + """Compute the shape of a narrowed DAG. + + Args: + repo: The repository we're transferring. + common: The roots of the DAG range we're transferring. + May be just [nullid], which means all ancestors of heads. + heads: The heads of the DAG range we're transferring. + match: The narrowmatcher that allows us to identify relevant changes. + depth: If not None, only consider nodes to be full nodes if they are at + most depth changesets away from one of heads. + + Returns: + A tuple of (visitnodes, relevant_nodes, ellipsisroots) where: + + visitnodes: The list of nodes (either full or ellipsis) which + need to be sent to the client. + relevant_nodes: The set of changelog nodes which change a file inside + the narrowspec. The client needs these as non-ellipsis nodes. + ellipsisroots: A dict of {rev: parents} that is used in + narrowchangegroup to produce ellipsis nodes with the + correct parents. + """ + cl = repo.changelog + mfl = repo.manifestlog + + cldag = dagutil.revlogdag(cl) + # dagutil does not like nullid/nullrev + commonrevs = cldag.internalizeall(common - set([nullid])) | set([nullrev]) + headsrevs = cldag.internalizeall(heads) + if depth: + revdepth = {h: 0 for h in headsrevs} + + ellipsisheads = collections.defaultdict(set) + ellipsisroots = collections.defaultdict(set) + + def addroot(head, curchange): + """Add a root to an ellipsis head, splitting heads with 3 roots.""" + ellipsisroots[head].add(curchange) + # Recursively split ellipsis heads with 3 roots by finding the + # roots' youngest common descendant which is an elided merge commit. + # That descendant takes 2 of the 3 roots as its own, and becomes a + # root of the head. + while len(ellipsisroots[head]) > 2: + child, roots = splithead(head) + splitroots(head, child, roots) + head = child # Recurse in case we just added a 3rd root + + def splitroots(head, child, roots): + ellipsisroots[head].difference_update(roots) + ellipsisroots[head].add(child) + ellipsisroots[child].update(roots) + ellipsisroots[child].discard(child) + + def splithead(head): + r1, r2, r3 = sorted(ellipsisroots[head]) + for nr1, nr2 in ((r2, r3), (r1, r3), (r1, r2)): + mid = repo.revs('sort(merge() & %d::%d & %d::%d, -rev)', + nr1, head, nr2, head) + for j in mid: + if j == nr2: + return nr2, (nr1, nr2) + if j not in ellipsisroots or len(ellipsisroots[j]) < 2: + return j, (nr1, nr2) + raise error.Abort('Failed to split up ellipsis node! head: %d, ' + 'roots: %d %d %d' % (head, r1, r2, r3)) + + missing = list(cl.findmissingrevs(common=commonrevs, heads=headsrevs)) + visit = reversed(missing) + relevant_nodes = set() + visitnodes = map(cl.node, missing) + required = set(headsrevs) | known + for rev in visit: + clrev = cl.changelogrevision(rev) + ps = cldag.parents(rev) + if depth is not None: + curdepth = revdepth[rev] + for p in ps: + revdepth[p] = min(curdepth + 1, revdepth.get(p, depth + 1)) + needed = False + shallow_enough = depth is None or revdepth[rev] <= depth + if shallow_enough: + curmf = mfl[clrev.manifest].read() + if ps: + # We choose to not trust the changed files list in + # changesets because it's not always correct. TODO: could + # we trust it for the non-merge case? + p1mf = mfl[cl.changelogrevision(ps[0]).manifest].read() + needed = any(match(f) for f in curmf.diff(p1mf).iterkeys()) + if not needed and len(ps) > 1: + # For merge changes, the list of changed files is not + # helpful, since we need to emit the merge if a file + # in the narrow spec has changed on either side of the + # merge. As a result, we do a manifest diff to check. + p2mf = mfl[cl.changelogrevision(ps[1]).manifest].read() + needed = any(match(f) for f in curmf.diff(p2mf).iterkeys()) + else: + # For a root node, we need to include the node if any + # files in the node match the narrowspec. + needed = any(match(f) for f in curmf) + + if needed: + for head in ellipsisheads[rev]: + addroot(head, rev) + for p in ps: + required.add(p) + relevant_nodes.add(cl.node(rev)) + else: + if not ps: + ps = [nullrev] + if rev in required: + for head in ellipsisheads[rev]: + addroot(head, rev) + for p in ps: + ellipsisheads[p].add(rev) + else: + for p in ps: + ellipsisheads[p] |= ellipsisheads[rev] + + # add common changesets as roots of their reachable ellipsis heads + for c in commonrevs: + for head in ellipsisheads[c]: + addroot(head, c) + return visitnodes, relevant_nodes, ellipsisroots + +def _packellipsischangegroup(repo, common, match, relevant_nodes, + ellipsisroots, visitnodes, depth, source, version): + if version in ('01', '02'): + raise error.Abort( + 'ellipsis nodes require at least cg3 on client and server, ' + 'but negotiated version %s' % version) + # We wrap cg1packer.revchunk, using a side channel to pass + # relevant_nodes into that area. Then if linknode isn't in the + # set, we know we have an ellipsis node and we should defer + # sending that node's data. We override close() to detect + # pending ellipsis nodes and flush them. + packer = changegroup.getbundler(version, repo) + # Let the packer have access to the narrow matcher so it can + # omit filelogs and dirlogs as needed + packer._narrow_matcher = lambda : match + # Give the packer the list of nodes which should not be + # ellipsis nodes. We store this rather than the set of nodes + # that should be an ellipsis because for very large histories + # we expect this to be significantly smaller. + packer.full_nodes = relevant_nodes + # Maps ellipsis revs to their roots at the changelog level. + packer.precomputed_ellipsis = ellipsisroots + # Maps CL revs to per-revlog revisions. Cleared in close() at + # the end of each group. + packer.clrev_to_localrev = {} + packer.next_clrev_to_localrev = {} + # Maps changelog nodes to changelog revs. Filled in once + # during changelog stage and then left unmodified. + packer.clnode_to_rev = {} + packer.changelog_done = False + # If true, informs the packer that it is serving shallow content and might + # need to pack file contents not introduced by the changes being packed. + packer.is_shallow = depth is not None + + return packer.generate(common, visitnodes, False, source) + +# Serve a changegroup for a client with a narrow clone. +def getbundlechangegrouppart_narrow(bundler, repo, source, + bundlecaps=None, b2caps=None, heads=None, + common=None, **kwargs): + cgversions = b2caps.get('changegroup') + getcgkwargs = {} + if cgversions: # 3.1 and 3.2 ship with an empty value + cgversions = [v for v in cgversions + if v in changegroup.supportedoutgoingversions(repo)] + if not cgversions: + raise ValueError(_('no common changegroup version')) + version = getcgkwargs['version'] = max(cgversions) + else: + raise ValueError(_("server does not advertise changegroup version," + " can't negotiate support for ellipsis nodes")) + + include = sorted(filter(bool, kwargs.get('includepats', []))) + exclude = sorted(filter(bool, kwargs.get('excludepats', []))) + newmatch = narrowspec.match(repo.root, include=include, exclude=exclude) + if not repo.ui.configbool("experimental", "narrowservebrokenellipses"): + outgoing = exchange._computeoutgoing(repo, heads, common) + if not outgoing.missing: + return + if util.safehasattr(changegroup, 'getsubsetraw'): + # getsubsetraw was replaced with makestream in hg in 92f1e2be8ab6 + # (2017/09/10). + packer = changegroup.getbundler(version, repo) + packer._narrow_matcher = lambda : newmatch + cg = changegroup.getsubsetraw(repo, outgoing, packer, source) + else: + def wrappedgetbundler(orig, *args, **kwargs): + bundler = orig(*args, **kwargs) + bundler._narrow_matcher = lambda : newmatch + return bundler + with extensions.wrappedfunction(changegroup, 'getbundler', + wrappedgetbundler): + cg = changegroup.makestream(repo, outgoing, version, source) + part = bundler.newpart('changegroup', data=cg) + part.addparam('version', version) + if 'treemanifest' in repo.requirements: + part.addparam('treemanifest', '1') + + if include or exclude: + narrowspecpart = bundler.newpart(specpart) + if include: + narrowspecpart.addparam( + specpart_include, '\n'.join(include), mandatory=True) + if exclude: + narrowspecpart.addparam( + specpart_exclude, '\n'.join(exclude), mandatory=True) + + return + + depth = kwargs.get('depth', None) + if depth is not None: + depth = int(depth) + if depth < 1: + raise error.Abort(_('depth must be positive, got %d') % depth) + + heads = set(heads or repo.heads()) + common = set(common or [nullid]) + oldinclude = sorted(filter(bool, kwargs.get('oldincludepats', []))) + oldexclude = sorted(filter(bool, kwargs.get('oldexcludepats', []))) + known = {bin(n) for n in kwargs.get('known', [])} + if known and (oldinclude != include or oldexclude != exclude): + # Steps: + # 1. Send kill for "$known & ::common" + # + # 2. Send changegroup for ::common + # + # 3. Proceed. + # + # In the future, we can send kills for only the specific + # nodes we know should go away or change shape, and then + # send a data stream that tells the client something like this: + # + # a) apply this changegroup + # b) apply nodes XXX, YYY, ZZZ that you already have + # c) goto a + # + # until they've built up the full new state. + # Convert to revnums and intersect with "common". The client should + # have made it a subset of "common" already, but let's be safe. + known = set(repo.revs("%ln & ::%ln", known, common)) + # TODO: we could send only roots() of this set, and the + # list of nodes in common, and the client could work out + # what to strip, instead of us explicitly sending every + # single node. + deadrevs = known + def genkills(): + for r in deadrevs: + yield killnodesignal + yield repo.changelog.node(r) + yield donesignal + bundler.newpart(changespecpart, data=genkills()) + newvisit, newfull, newellipsis = _computeellipsis( + repo, set(), common, known, newmatch) + if newvisit: + cg = _packellipsischangegroup( + repo, common, newmatch, newfull, newellipsis, + newvisit, depth, source, version) + part = bundler.newpart('changegroup', data=cg) + part.addparam('version', version) + if 'treemanifest' in repo.requirements: + part.addparam('treemanifest', '1') + + visitnodes, relevant_nodes, ellipsisroots = _computeellipsis( + repo, common, heads, set(), newmatch, depth=depth) + + repo.ui.debug('Found %d relevant revs\n' % len(relevant_nodes)) + if visitnodes: + cg = _packellipsischangegroup( + repo, common, newmatch, relevant_nodes, ellipsisroots, + visitnodes, depth, source, version) + part = bundler.newpart('changegroup', data=cg) + part.addparam('version', version) + if 'treemanifest' in repo.requirements: + part.addparam('treemanifest', '1') + +def applyacl_narrow(repo, kwargs): + username = repo.ui.shortuser(repo.ui.username()) + user_includes = repo.ui.configlist( + narrowacl_section, username + '.includes', + repo.ui.configlist(narrowacl_section, 'default.includes')) + user_excludes = repo.ui.configlist( + narrowacl_section, username + '.excludes', + repo.ui.configlist(narrowacl_section, 'default.excludes')) + if not user_includes: + raise error.Abort(_("{} configuration for user {} is empty") + .format(narrowacl_section, username)) + + user_includes = [ + 'path:.' if p == '*' else 'path:' + p for p in user_includes] + user_excludes = [ + 'path:.' if p == '*' else 'path:' + p for p in user_excludes] + + req_includes = set(kwargs.get('includepats', [])) + req_excludes = set(kwargs.get('excludepats', [])) + + invalid_includes = [] + req_includes, req_excludes = narrowspec.restrictpatterns( + req_includes, req_excludes, + user_includes, user_excludes, invalid_includes) + + if invalid_includes: + raise error.Abort( + _("The following includes are not accessible for {}: {}") + .format(username, invalid_includes)) + + new_args = {} + new_args.update(kwargs) + new_args['includepats'] = req_includes + if req_excludes: + new_args['excludepats'] = req_excludes + return new_args + +@bundle2.parthandler(specpart, (specpart_include, specpart_exclude)) +def _handlechangespec_2(op, inpart): + includepats = set(inpart.params.get(specpart_include, '').splitlines()) + excludepats = set(inpart.params.get(specpart_exclude, '').splitlines()) + narrowspec.save(op.repo, includepats, excludepats) + if not narrowrepo.requirement in op.repo.requirements: + op.repo.requirements.add(narrowrepo.requirement) + op.repo._writerequirements() + op.repo.invalidate(clearfilecache=True) + +@bundle2.parthandler(changespecpart) +def _handlechangespec(op, inpart): + repo = op.repo + cl = repo.changelog + + # changesets which need to be stripped entirely. either they're no longer + # needed in the new narrow spec, or the server is sending a replacement + # in the changegroup part. + clkills = set() + + # A changespec part contains all the updates to ellipsis nodes + # that will happen as a result of widening or narrowing a + # repo. All the changes that this block encounters are ellipsis + # nodes or flags to kill an existing ellipsis. + chunksignal = changegroup.readexactly(inpart, 4) + while chunksignal != donesignal: + if chunksignal == killnodesignal: + # a node used to be an ellipsis but isn't anymore + ck = changegroup.readexactly(inpart, 20) + if cl.hasnode(ck): + clkills.add(ck) + else: + raise error.Abort( + _('unexpected changespec node chunk type: %s') % chunksignal) + chunksignal = changegroup.readexactly(inpart, 4) + + if clkills: + # preserve bookmarks that repair.strip() would otherwise strip + bmstore = repo._bookmarks + class dummybmstore(dict): + def applychanges(self, repo, tr, changes): + pass + def recordchange(self, tr): # legacy version + pass + repo._bookmarks = dummybmstore() + chgrpfile = repair.strip(op.ui, repo, list(clkills), backup=True, + topic='widen') + repo._bookmarks = bmstore + if chgrpfile: + # presence of _widen_bundle attribute activates widen handler later + op._widen_bundle = chgrpfile + # Set the new narrowspec if we're widening. The setnewnarrowpats() method + # will currently always be there when using the core+narrowhg server, but + # other servers may include a changespec part even when not widening (e.g. + # because we're deepening a shallow repo). + if util.safehasattr(repo, 'setnewnarrowpats'): + repo.setnewnarrowpats() + +def handlechangegroup_widen(op, inpart): + """Changegroup exchange handler which restores temporarily-stripped nodes""" + # We saved a bundle with stripped node data we must now restore. + # This approach is based on mercurial/repair.py@6ee26a53c111. + repo = op.repo + ui = op.ui + + chgrpfile = op._widen_bundle + del op._widen_bundle + vfs = repo.vfs + + ui.note(_("adding branch\n")) + f = vfs.open(chgrpfile, "rb") + try: + gen = exchange.readbundle(ui, f, chgrpfile, vfs) + if not ui.verbose: + # silence internal shuffling chatter + ui.pushbuffer() + if isinstance(gen, bundle2.unbundle20): + with repo.transaction('strip') as tr: + bundle2.processbundle(repo, gen, lambda: tr) + else: + gen.apply(repo, 'strip', 'bundle:' + vfs.join(chgrpfile), True) + if not ui.verbose: + ui.popbuffer() + finally: + f.close() + + # remove undo files + for undovfs, undofile in repo.undofiles(): + try: + undovfs.unlink(undofile) + except OSError as e: + if e.errno != errno.ENOENT: + ui.warn(_('error removing %s: %s\n') % + (undovfs.join(undofile), str(e))) + + # Remove partial backup only if there were no exceptions + vfs.unlink(chgrpfile) + +def setup(): + """Enable narrow repo support in bundle2-related extension points.""" + extensions.wrapfunction(bundle2, 'getrepocaps', getrepocaps_narrow) + + wireproto.gboptsmap['narrow'] = 'boolean' + wireproto.gboptsmap['depth'] = 'plain' + wireproto.gboptsmap['oldincludepats'] = 'csv' + wireproto.gboptsmap['oldexcludepats'] = 'csv' + wireproto.gboptsmap['includepats'] = 'csv' + wireproto.gboptsmap['excludepats'] = 'csv' + wireproto.gboptsmap['known'] = 'csv' + + # Extend changegroup serving to handle requests from narrow clients. + origcgfn = exchange.getbundle2partsmapping['changegroup'] + def wrappedcgfn(*args, **kwargs): + repo = args[1] + if repo.ui.has_section(narrowacl_section): + getbundlechangegrouppart_narrow( + *args, **applyacl_narrow(repo, kwargs)) + elif kwargs.get('narrow', False): + getbundlechangegrouppart_narrow(*args, **kwargs) + else: + origcgfn(*args, **kwargs) + exchange.getbundle2partsmapping['changegroup'] = wrappedcgfn + + # Extend changegroup receiver so client can fixup after widen requests. + origcghandler = bundle2.parthandlermapping['changegroup'] + def wrappedcghandler(op, inpart): + origcghandler(op, inpart) + if util.safehasattr(op, '_widen_bundle'): + handlechangegroup_widen(op, inpart) + wrappedcghandler.params = origcghandler.params + bundle2.parthandlermapping['changegroup'] = wrappedcghandler diff --git a/hgext/narrow/narrowchangegroup.py b/hgext/narrow/narrowchangegroup.py new file mode 100644 --- /dev/null +++ b/hgext/narrow/narrowchangegroup.py @@ -0,0 +1,385 @@ +# narrowchangegroup.py - narrow clone changegroup creation and consumption +# +# Copyright 2017 Google, Inc. +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +from __future__ import absolute_import + +from mercurial.i18n import _ +from mercurial import ( + changegroup, + error, + extensions, + manifest, + mdiff, + node, + util, +) + +from . import ( + narrowrepo, + narrowrevlog, +) + +def setup(): + + def supportedoutgoingversions(orig, repo): + versions = orig(repo) + if narrowrepo.requirement in repo.requirements: + versions.discard('01') + versions.discard('02') + return versions + + extensions.wrapfunction(changegroup, 'supportedoutgoingversions', + supportedoutgoingversions) + + def prune(orig, self, revlog, missing, commonrevs): + if isinstance(revlog, manifest.manifestrevlog): + matcher = getattr(self._repo, 'narrowmatch', + getattr(self, '_narrow_matcher', None)) + if (matcher is not None and + not matcher().visitdir(revlog._dir[:-1] or '.')): + return [] + return orig(self, revlog, missing, commonrevs) + + extensions.wrapfunction(changegroup.cg1packer, 'prune', prune) + + def generatefiles(orig, self, changedfiles, linknodes, commonrevs, + source): + matcher = getattr(self._repo, 'narrowmatch', + getattr(self, '_narrow_matcher', None)) + if matcher is not None: + narrowmatch = matcher() + changedfiles = filter(narrowmatch, changedfiles) + if getattr(self, 'is_shallow', False): + # See comment in generate() for why this sadness is a thing. + mfdicts = self._mfdicts + del self._mfdicts + # In a shallow clone, the linknodes callback needs to also include + # those file nodes that are in the manifests we sent but weren't + # introduced by those manifests. + commonctxs = [self._repo[c] for c in commonrevs] + oldlinknodes = linknodes + clrev = self._repo.changelog.rev + def linknodes(flog, fname): + for c in commonctxs: + try: + fnode = c.filenode(fname) + self.clrev_to_localrev[c.rev()] = flog.rev(fnode) + except error.ManifestLookupError: + pass + links = oldlinknodes(flog, fname) + if len(links) != len(mfdicts): + for mf, lr in mfdicts: + fnode = mf.get(fname, None) + if fnode in links: + links[fnode] = min(links[fnode], lr, key=clrev) + elif fnode: + links[fnode] = lr + return links + return orig(self, changedfiles, linknodes, commonrevs, source) + extensions.wrapfunction( + changegroup.cg1packer, 'generatefiles', generatefiles) + + def ellipsisdata(packer, rev, revlog, p1, p2, data, linknode): + n = revlog.node(rev) + p1n, p2n = revlog.node(p1), revlog.node(p2) + flags = revlog.flags(rev) + flags |= narrowrevlog.ELLIPSIS_NODE_FLAG + meta = packer.builddeltaheader( + n, p1n, p2n, node.nullid, linknode, flags) + # TODO: try and actually send deltas for ellipsis data blocks + diffheader = mdiff.trivialdiffheader(len(data)) + l = len(meta) + len(diffheader) + len(data) + return ''.join((changegroup.chunkheader(l), + meta, + diffheader, + data)) + + def close(orig, self): + getattr(self, 'clrev_to_localrev', {}).clear() + if getattr(self, 'next_clrev_to_localrev', {}): + self.clrev_to_localrev = self.next_clrev_to_localrev + del self.next_clrev_to_localrev + self.changelog_done = True + return orig(self) + extensions.wrapfunction(changegroup.cg1packer, 'close', close) + + # In a perfect world, we'd generate better ellipsis-ified graphs + # for non-changelog revlogs. In practice, we haven't started doing + # that yet, so the resulting DAGs for the manifestlog and filelogs + # are actually full of bogus parentage on all the ellipsis + # nodes. This has the side effect that, while the contents are + # correct, the individual DAGs might be completely out of whack in + # a case like 882681bc3166 and its ancestors (back about 10 + # revisions or so) in the main hg repo. + # + # The one invariant we *know* holds is that the new (potentially + # bogus) DAG shape will be valid if we order the nodes in the + # order that they're introduced in dramatis personae by the + # changelog, so what we do is we sort the non-changelog histories + # by the order in which they are used by the changelog. + def _sortgroup(orig, self, revlog, nodelist, lookup): + if not util.safehasattr(self, 'full_nodes') or not self.clnode_to_rev: + return orig(self, revlog, nodelist, lookup) + key = lambda n: self.clnode_to_rev[lookup(n)] + return [revlog.rev(n) for n in sorted(nodelist, key=key)] + + extensions.wrapfunction(changegroup.cg1packer, '_sortgroup', _sortgroup) + + def generate(orig, self, commonrevs, clnodes, fastpathlinkrev, source): + '''yield a sequence of changegroup chunks (strings)''' + # Note: other than delegating to orig, the only deviation in + # logic from normal hg's generate is marked with BEGIN/END + # NARROW HACK. + if not util.safehasattr(self, 'full_nodes'): + # not sending a narrow bundle + for x in orig(self, commonrevs, clnodes, fastpathlinkrev, source): + yield x + return + + repo = self._repo + cl = repo.changelog + mfl = repo.manifestlog + mfrevlog = mfl._revlog + + clrevorder = {} + mfs = {} # needed manifests + fnodes = {} # needed file nodes + changedfiles = set() + + # Callback for the changelog, used to collect changed files and manifest + # nodes. + # Returns the linkrev node (identity in the changelog case). + def lookupcl(x): + c = cl.read(x) + clrevorder[x] = len(clrevorder) + # BEGIN NARROW HACK + # + # Only update mfs if x is going to be sent. Otherwise we + # end up with bogus linkrevs specified for manifests and + # we skip some manifest nodes that we should otherwise + # have sent. + if x in self.full_nodes or cl.rev(x) in self.precomputed_ellipsis: + n = c[0] + # record the first changeset introducing this manifest version + mfs.setdefault(n, x) + # Set this narrow-specific dict so we have the lowest manifest + # revnum to look up for this cl revnum. (Part of mapping + # changelog ellipsis parents to manifest ellipsis parents) + self.next_clrev_to_localrev.setdefault(cl.rev(x), + mfrevlog.rev(n)) + # We can't trust the changed files list in the changeset if the + # client requested a shallow clone. + if self.is_shallow: + changedfiles.update(mfl[c[0]].read().keys()) + else: + changedfiles.update(c[3]) + # END NARROW HACK + # Record a complete list of potentially-changed files in + # this manifest. + return x + + self._verbosenote(_('uncompressed size of bundle content:\n')) + size = 0 + for chunk in self.group(clnodes, cl, lookupcl, units=_('changesets')): + size += len(chunk) + yield chunk + self._verbosenote(_('%8.i (changelog)\n') % size) + + # We need to make sure that the linkrev in the changegroup refers to + # the first changeset that introduced the manifest or file revision. + # The fastpath is usually safer than the slowpath, because the filelogs + # are walked in revlog order. + # + # When taking the slowpath with reorder=None and the manifest revlog + # uses generaldelta, the manifest may be walked in the "wrong" order. + # Without 'clrevorder', we would get an incorrect linkrev (see fix in + # cc0ff93d0c0c). + # + # When taking the fastpath, we are only vulnerable to reordering + # of the changelog itself. The changelog never uses generaldelta, so + # it is only reordered when reorder=True. To handle this case, we + # simply take the slowpath, which already has the 'clrevorder' logic. + # This was also fixed in cc0ff93d0c0c. + fastpathlinkrev = fastpathlinkrev and not self._reorder + # Treemanifests don't work correctly with fastpathlinkrev + # either, because we don't discover which directory nodes to + # send along with files. This could probably be fixed. + fastpathlinkrev = fastpathlinkrev and ( + 'treemanifest' not in repo.requirements) + # Shallow clones also don't work correctly with fastpathlinkrev + # because file nodes may need to be sent for a manifest even if they + # weren't introduced by that manifest. + fastpathlinkrev = fastpathlinkrev and not self.is_shallow + + moreargs = [] + if self.generatemanifests.func_code.co_argcount == 7: + # The source argument was added to generatemanifests in hg in + # 75cc1f1e11f2 (2017/09/11). + moreargs.append(source) + for chunk in self.generatemanifests(commonrevs, clrevorder, + fastpathlinkrev, mfs, fnodes, *moreargs): + yield chunk + # BEGIN NARROW HACK + mfdicts = None + if self.is_shallow: + mfdicts = [(self._repo.manifestlog[n].read(), lr) + for (n, lr) in mfs.iteritems()] + # END NARROW HACK + mfs.clear() + clrevs = set(cl.rev(x) for x in clnodes) + + if not fastpathlinkrev: + def linknodes(unused, fname): + return fnodes.get(fname, {}) + else: + cln = cl.node + def linknodes(filerevlog, fname): + llr = filerevlog.linkrev + fln = filerevlog.node + revs = ((r, llr(r)) for r in filerevlog) + return dict((fln(r), cln(lr)) for r, lr in revs if lr in clrevs) + + # BEGIN NARROW HACK + # + # We need to pass the mfdicts variable down into + # generatefiles(), but more than one command might have + # wrapped generatefiles so we can't modify the function + # signature. Instead, we pass the data to ourselves using an + # instance attribute. I'm sorry. + self._mfdicts = mfdicts + # END NARROW HACK + for chunk in self.generatefiles(changedfiles, linknodes, commonrevs, + source): + yield chunk + + yield self.close() + + if clnodes: + repo.hook('outgoing', node=node.hex(clnodes[0]), source=source) + extensions.wrapfunction(changegroup.cg1packer, 'generate', generate) + + def revchunk(orig, self, revlog, rev, prev, linknode): + if not util.safehasattr(self, 'full_nodes'): + # not sending a narrow changegroup + for x in orig(self, revlog, rev, prev, linknode): + yield x + return + # build up some mapping information that's useful later. See + # the local() nested function below. + if not self.changelog_done: + self.clnode_to_rev[linknode] = rev + linkrev = rev + self.clrev_to_localrev[linkrev] = rev + else: + linkrev = self.clnode_to_rev[linknode] + self.clrev_to_localrev[linkrev] = rev + # This is a node to send in full, because the changeset it + # corresponds to was a full changeset. + if linknode in self.full_nodes: + for x in orig(self, revlog, rev, prev, linknode): + yield x + return + # At this point, a node can either be one we should skip or an + # ellipsis. If it's not an ellipsis, bail immediately. + if linkrev not in self.precomputed_ellipsis: + return + linkparents = self.precomputed_ellipsis[linkrev] + def local(clrev): + """Turn a changelog revnum into a local revnum. + + The ellipsis dag is stored as revnums on the changelog, + but when we're producing ellipsis entries for + non-changelog revlogs, we need to turn those numbers into + something local. This does that for us, and during the + changelog sending phase will also expand the stored + mappings as needed. + """ + if clrev == node.nullrev: + return node.nullrev + if not self.changelog_done: + # If we're doing the changelog, it's possible that we + # have a parent that is already on the client, and we + # need to store some extra mapping information so that + # our contained ellipsis nodes will be able to resolve + # their parents. + if clrev not in self.clrev_to_localrev: + clnode = revlog.node(clrev) + self.clnode_to_rev[clnode] = clrev + return clrev + # Walk the ellipsis-ized changelog breadth-first looking for a + # change that has been linked from the current revlog. + # + # For a flat manifest revlog only a single step should be necessary + # as all relevant changelog entries are relevant to the flat + # manifest. + # + # For a filelog or tree manifest dirlog however not every changelog + # entry will have been relevant, so we need to skip some changelog + # nodes even after ellipsis-izing. + walk = [clrev] + while walk: + p = walk[0] + walk = walk[1:] + if p in self.clrev_to_localrev: + return self.clrev_to_localrev[p] + elif p in self.full_nodes: + walk.extend([pp for pp in self._repo.changelog.parentrevs(p) + if pp != node.nullrev]) + elif p in self.precomputed_ellipsis: + walk.extend([pp for pp in self.precomputed_ellipsis[p] + if pp != node.nullrev]) + else: + # In this case, we've got an ellipsis with parents + # outside the current bundle (likely an + # incremental pull). We "know" that we can use the + # value of this same revlog at whatever revision + # is pointed to by linknode. "Know" is in scare + # quotes because I haven't done enough examination + # of edge cases to convince myself this is really + # a fact - it works for all the (admittedly + # thorough) cases in our testsuite, but I would be + # somewhat unsurprised to find a case in the wild + # where this breaks down a bit. That said, I don't + # know if it would hurt anything. + for i in xrange(rev, 0, -1): + if revlog.linkrev(i) == clrev: + return i + # We failed to resolve a parent for this node, so + # we crash the changegroup construction. + raise error.Abort( + 'unable to resolve parent while packing %r %r' + ' for changeset %r' % (revlog.indexfile, rev, clrev)) + return node.nullrev + + if not linkparents or ( + revlog.parentrevs(rev) == (node.nullrev, node.nullrev)): + p1, p2 = node.nullrev, node.nullrev + elif len(linkparents) == 1: + p1, = sorted(local(p) for p in linkparents) + p2 = node.nullrev + else: + p1, p2 = sorted(local(p) for p in linkparents) + yield ellipsisdata( + self, rev, revlog, p1, p2, revlog.revision(rev), linknode) + extensions.wrapfunction(changegroup.cg1packer, 'revchunk', revchunk) + + def deltaparent(orig, self, revlog, rev, p1, p2, prev): + if util.safehasattr(self, 'full_nodes'): + # TODO: send better deltas when in narrow mode. + # + # changegroup.group() loops over revisions to send, + # including revisions we'll skip. What this means is that + # `prev` will be a potentially useless delta base for all + # ellipsis nodes, as the client likely won't have it. In + # the future we should do bookkeeping about which nodes + # have been sent to the client, and try to be + # significantly smarter about delta bases. This is + # slightly tricky because this same code has to work for + # all revlogs, and we don't have the linkrev/linknode here. + return p1 + return orig(self, revlog, rev, p1, p2, prev) + extensions.wrapfunction(changegroup.cg2packer, 'deltaparent', deltaparent) diff --git a/hgext/narrow/narrowcommands.py b/hgext/narrow/narrowcommands.py new file mode 100644 --- /dev/null +++ b/hgext/narrow/narrowcommands.py @@ -0,0 +1,402 @@ +# narrowcommands.py - command modifications for narrowhg extension +# +# Copyright 2017 Google, Inc. +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. +from __future__ import absolute_import + +import itertools + +from mercurial.i18n import _ +from mercurial import ( + cmdutil, + commands, + discovery, + error, + exchange, + extensions, + hg, + merge, + node, + registrar, + repair, + repoview, + util, +) + +from . import ( + narrowbundle2, + narrowrepo, + narrowspec, +) + +table = {} +command = registrar.command(table) + +def setup(): + """Wraps user-facing mercurial commands with narrow-aware versions.""" + + entry = extensions.wrapcommand(commands.table, 'clone', clonenarrowcmd) + entry[1].append(('', 'narrow', None, + _("create a narrow clone of select files"))) + entry[1].append(('', 'depth', '', + _("limit the history fetched by distance from heads"))) + # TODO(durin42): unify sparse/narrow --include/--exclude logic a bit + if 'sparse' not in extensions.enabled(): + entry[1].append(('', 'include', [], + _("specifically fetch this file/directory"))) + entry[1].append( + ('', 'exclude', [], + _("do not fetch this file/directory, even if included"))) + + entry = extensions.wrapcommand(commands.table, 'pull', pullnarrowcmd) + entry[1].append(('', 'depth', '', + _("limit the history fetched by distance from heads"))) + + extensions.wrapcommand(commands.table, 'archive', archivenarrowcmd) + +def expandpull(pullop, includepats, excludepats): + if not narrowspec.needsexpansion(includepats): + return includepats, excludepats + + heads = pullop.heads or pullop.rheads + includepats, excludepats = pullop.remote.expandnarrow( + includepats, excludepats, heads) + pullop.repo.ui.debug('Expanded narrowspec to inc=%s, exc=%s\n' % ( + includepats, excludepats)) + return set(includepats), set(excludepats) + +def clonenarrowcmd(orig, ui, repo, *args, **opts): + """Wraps clone command, so 'hg clone' first wraps localrepo.clone().""" + wrappedextraprepare = util.nullcontextmanager() + opts_narrow = opts['narrow'] + if opts_narrow: + def pullbundle2extraprepare_widen(orig, pullop, kwargs): + # Create narrow spec patterns from clone flags + includepats = narrowspec.parsepatterns(opts['include']) + excludepats = narrowspec.parsepatterns(opts['exclude']) + + # If necessary, ask the server to expand the narrowspec. + includepats, excludepats = expandpull( + pullop, includepats, excludepats) + + if not includepats and excludepats: + # If nothing was included, we assume the user meant to include + # everything, except what they asked to exclude. + includepats = {'path:.'} + + narrowspec.save(pullop.repo, includepats, excludepats) + + # This will populate 'includepats' etc with the values from the + # narrowspec we just saved. + orig(pullop, kwargs) + + if opts.get('depth'): + kwargs['depth'] = opts['depth'] + wrappedextraprepare = extensions.wrappedfunction(exchange, + '_pullbundle2extraprepare', pullbundle2extraprepare_widen) + + def pullnarrow(orig, repo, *args, **kwargs): + narrowrepo.wraprepo(repo.unfiltered(), opts_narrow) + if isinstance(repo, repoview.repoview): + repo.__class__.__bases__ = (repo.__class__.__bases__[0], + repo.unfiltered().__class__) + if opts_narrow: + repo.requirements.add(narrowrepo.requirement) + repo._writerequirements() + + return orig(repo, *args, **kwargs) + + wrappedpull = extensions.wrappedfunction(exchange, 'pull', pullnarrow) + + with wrappedextraprepare, wrappedpull: + return orig(ui, repo, *args, **opts) + +def pullnarrowcmd(orig, ui, repo, *args, **opts): + """Wraps pull command to allow modifying narrow spec.""" + wrappedextraprepare = util.nullcontextmanager() + if narrowrepo.requirement in repo.requirements: + + def pullbundle2extraprepare_widen(orig, pullop, kwargs): + orig(pullop, kwargs) + if opts.get('depth'): + kwargs['depth'] = opts['depth'] + wrappedextraprepare = extensions.wrappedfunction(exchange, + '_pullbundle2extraprepare', pullbundle2extraprepare_widen) + + with wrappedextraprepare: + return orig(ui, repo, *args, **opts) + +def archivenarrowcmd(orig, ui, repo, *args, **opts): + """Wraps archive command to narrow the default includes.""" + if narrowrepo.requirement in repo.requirements: + repo_includes, repo_excludes = repo.narrowpats + includes = set(opts.get('include', [])) + excludes = set(opts.get('exclude', [])) + includes, excludes = narrowspec.restrictpatterns( + includes, excludes, repo_includes, repo_excludes) + if includes: + opts['include'] = includes + if excludes: + opts['exclude'] = excludes + return orig(ui, repo, *args, **opts) + +def pullbundle2extraprepare(orig, pullop, kwargs): + repo = pullop.repo + if narrowrepo.requirement not in repo.requirements: + return orig(pullop, kwargs) + + if narrowbundle2.narrowcap not in pullop.remotebundle2caps: + raise error.Abort(_("server doesn't support narrow clones")) + orig(pullop, kwargs) + kwargs['narrow'] = True + include, exclude = repo.narrowpats + kwargs['oldincludepats'] = include + kwargs['oldexcludepats'] = exclude + kwargs['includepats'] = include + kwargs['excludepats'] = exclude + kwargs['known'] = [node.hex(ctx.node()) for ctx in + repo.set('::%ln', pullop.common) + if ctx.node() != node.nullid] + if not kwargs['known']: + # Mercurial serialized an empty list as '' and deserializes it as + # [''], so delete it instead to avoid handling the empty string on the + # server. + del kwargs['known'] + +extensions.wrapfunction(exchange,'_pullbundle2extraprepare', + pullbundle2extraprepare) + +def _narrow(ui, repo, remote, commoninc, oldincludes, oldexcludes, + newincludes, newexcludes, force): + oldmatch = narrowspec.match(repo.root, oldincludes, oldexcludes) + newmatch = narrowspec.match(repo.root, newincludes, newexcludes) + + # This is essentially doing "hg outgoing" to find all local-only + # commits. We will then check that the local-only commits don't + # have any changes to files that will be untracked. + unfi = repo.unfiltered() + outgoing = discovery.findcommonoutgoing(unfi, remote, + commoninc=commoninc) + ui.status(_('looking for local changes to affected paths\n')) + localnodes = [] + for n in itertools.chain(outgoing.missing, outgoing.excluded): + if any(oldmatch(f) and not newmatch(f) for f in unfi[n].files()): + localnodes.append(n) + revstostrip = unfi.revs('descendants(%ln)', localnodes) + hiddenrevs = repoview.filterrevs(repo, 'visible') + visibletostrip = list(repo.changelog.node(r) + for r in (revstostrip - hiddenrevs)) + if visibletostrip: + ui.status(_('The following changeset(s) or their ancestors have ' + 'local changes not on the remote:\n')) + maxnodes = 10 + if ui.verbose or len(visibletostrip) <= maxnodes: + for n in visibletostrip: + ui.status('%s\n' % node.short(n)) + else: + for n in visibletostrip[:maxnodes]: + ui.status('%s\n' % node.short(n)) + ui.status(_('...and %d more, use --verbose to list all\n') % + (len(visibletostrip) - maxnodes)) + if not force: + raise error.Abort(_('local changes found'), + hint=_('use --force-delete-local-changes to ' + 'ignore')) + + if revstostrip: + tostrip = [unfi.changelog.node(r) for r in revstostrip] + if repo['.'].node() in tostrip: + # stripping working copy, so move to a different commit first + urev = max(repo.revs('(::%n) - %ln + null', + repo['.'].node(), visibletostrip)) + hg.clean(repo, urev) + repair.strip(ui, unfi, tostrip, topic='narrow') + + todelete = [] + for f, f2, size in repo.store.datafiles(): + if f.startswith('data/'): + file = f[5:-2] + if not newmatch(file): + todelete.append(f) + elif f.startswith('meta/'): + dir = f[5:-13] + dirs = ['.'] + sorted(util.dirs({dir})) + [dir] + include = True + for d in dirs: + visit = newmatch.visitdir(d) + if not visit: + include = False + break + if visit == 'all': + break + if not include: + todelete.append(f) + + repo.destroying() + + with repo.transaction("narrowing"): + for f in todelete: + ui.status(_('deleting %s\n') % f) + util.unlinkpath(repo.svfs.join(f)) + repo.store.markremoved(f) + + for f in repo.dirstate: + if not newmatch(f): + repo.dirstate.drop(f) + repo.wvfs.unlinkpath(f) + repo.setnarrowpats(newincludes, newexcludes) + + repo.destroyed() + +def _widen(ui, repo, remote, commoninc, newincludes, newexcludes): + newmatch = narrowspec.match(repo.root, newincludes, newexcludes) + + # TODO(martinvonz): Get expansion working with widening/narrowing. + if narrowspec.needsexpansion(newincludes): + raise error.Abort('Expansion not yet supported on pull') + + def pullbundle2extraprepare_widen(orig, pullop, kwargs): + orig(pullop, kwargs) + # The old{in,ex}cludepats have already been set by orig() + kwargs['includepats'] = newincludes + kwargs['excludepats'] = newexcludes + wrappedextraprepare = extensions.wrappedfunction(exchange, + '_pullbundle2extraprepare', pullbundle2extraprepare_widen) + + # define a function that narrowbundle2 can call after creating the + # backup bundle, but before applying the bundle from the server + def setnewnarrowpats(): + repo.setnarrowpats(newincludes, newexcludes) + repo.setnewnarrowpats = setnewnarrowpats + + ds = repo.dirstate + p1, p2 = ds.p1(), ds.p2() + with ds.parentchange(): + ds.setparents(node.nullid, node.nullid) + common = commoninc[0] + with wrappedextraprepare: + exchange.pull(repo, remote, heads=common) + with ds.parentchange(): + ds.setparents(p1, p2) + + actions = {k: [] for k in 'a am f g cd dc r dm dg m e k p pr'.split()} + addgaction = actions['g'].append + + mf = repo['.'].manifest().matches(newmatch) + for f, fn in mf.iteritems(): + if f not in repo.dirstate: + addgaction((f, (mf.flags(f), False), + "add from widened narrow clone")) + + merge.applyupdates(repo, actions, wctx=repo[None], + mctx=repo['.'], overwrite=False) + merge.recordupdates(repo, actions, branchmerge=False) + +# TODO(rdamazio): Make new matcher format and update description +@command('tracked', + [('', 'addinclude', [], _('new paths to include')), + ('', 'removeinclude', [], _('old paths to no longer include')), + ('', 'addexclude', [], _('new paths to exclude')), + ('', 'removeexclude', [], _('old paths to no longer exclude')), + ('', 'clear', False, _('whether to replace the existing narrowspec')), + ('', 'force-delete-local-changes', False, + _('forces deletion of local changes when narrowing')), + ] + commands.remoteopts, + _('[OPTIONS]... [REMOTE]'), + inferrepo=True) +def trackedcmd(ui, repo, remotepath=None, *pats, **opts): + """show or change the current narrowspec + + With no argument, shows the current narrowspec entries, one per line. Each + line will be prefixed with 'I' or 'X' for included or excluded patterns, + respectively. + + The narrowspec is comprised of expressions to match remote files and/or + directories that should be pulled into your client. + The narrowspec has *include* and *exclude* expressions, with excludes always + trumping includes: that is, if a file matches an exclude expression, it will + be excluded even if it also matches an include expression. + Excluding files that were never included has no effect. + + Each included or excluded entry is in the format described by + 'hg help patterns'. + + The options allow you to add or remove included and excluded expressions. + + If --clear is specified, then all previous includes and excludes are DROPPED + and replaced by the new ones specified to --addinclude and --addexclude. + If --clear is specified without any further options, the narrowspec will be + empty and will not match any files. + """ + if narrowrepo.requirement not in repo.requirements: + ui.warn(_('The narrow command is only supported on respositories cloned' + ' with --narrow.\n')) + return 1 + + # Before supporting, decide whether it "hg tracked --clear" should mean + # tracking no paths or all paths. + if opts['clear']: + ui.warn(_('The --clear option is not yet supported.\n')) + return 1 + + if narrowspec.needsexpansion(opts['addinclude'] + opts['addexclude']): + raise error.Abort('Expansion not yet supported on widen/narrow') + + addedincludes = narrowspec.parsepatterns(opts['addinclude']) + removedincludes = narrowspec.parsepatterns(opts['removeinclude']) + addedexcludes = narrowspec.parsepatterns(opts['addexclude']) + removedexcludes = narrowspec.parsepatterns(opts['removeexclude']) + widening = addedincludes or removedexcludes + narrowing = removedincludes or addedexcludes + only_show = not widening and not narrowing + + # Only print the current narrowspec. + if only_show: + include, exclude = repo.narrowpats + + ui.pager('tracked') + fm = ui.formatter('narrow', opts) + for i in sorted(include): + fm.startitem() + fm.write('status', '%s ', 'I', label='narrow.included') + fm.write('pat', '%s\n', i, label='narrow.included') + for i in sorted(exclude): + fm.startitem() + fm.write('status', '%s ', 'X', label='narrow.excluded') + fm.write('pat', '%s\n', i, label='narrow.excluded') + fm.end() + return 0 + + with repo.wlock(), repo.lock(): + cmdutil.bailifchanged(repo) + + # Find the revisions we have in common with the remote. These will + # be used for finding local-only changes for narrowing. They will + # also define the set of revisions to update for widening. + remotepath = ui.expandpath(remotepath or 'default') + url, branches = hg.parseurl(remotepath) + ui.status(_('comparing with %s\n') % util.hidepassword(url)) + remote = hg.peer(repo, opts, url) + commoninc = discovery.findcommonincoming(repo, remote) + + oldincludes, oldexcludes = repo.narrowpats + if narrowing: + newincludes = oldincludes - removedincludes + newexcludes = oldexcludes | addedexcludes + _narrow(ui, repo, remote, commoninc, oldincludes, oldexcludes, + newincludes, newexcludes, + opts['force_delete_local_changes']) + # _narrow() updated the narrowspec and _widen() below needs to + # use the updated values as its base (otherwise removed includes + # and addedexcludes will be lost in the resulting narrowspec) + oldincludes = newincludes + oldexcludes = newexcludes + + if widening: + newincludes = oldincludes | addedincludes + newexcludes = oldexcludes - removedexcludes + _widen(ui, repo, remote, commoninc, newincludes, newexcludes) + + return 0 diff --git a/hgext/narrow/narrowcopies.py b/hgext/narrow/narrowcopies.py new file mode 100644 --- /dev/null +++ b/hgext/narrow/narrowcopies.py @@ -0,0 +1,35 @@ +# narrowcopies.py - extensions to mercurial copies module to support narrow +# clones +# +# Copyright 2017 Google, Inc. +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +from __future__ import absolute_import + +from mercurial import ( + copies, + extensions, + util, +) + +def setup(repo): + def _computeforwardmissing(orig, a, b, match=None): + missing = orig(a, b, match) + if util.safehasattr(repo, 'narrowmatch'): + narrowmatch = repo.narrowmatch() + missing = filter(narrowmatch, missing) + return missing + + def _checkcopies(orig, srcctx, dstctx, f, base, tca, remotebase, limit, + data): + if util.safehasattr(repo, 'narrowmatch'): + narrowmatch = repo.narrowmatch() + if not narrowmatch(f): + return + orig(srcctx, dstctx, f, base, tca, remotebase, limit, data) + + extensions.wrapfunction(copies, '_computeforwardmissing', + _computeforwardmissing) + extensions.wrapfunction(copies, '_checkcopies', _checkcopies) diff --git a/hgext/narrow/narrowdirstate.py b/hgext/narrow/narrowdirstate.py new file mode 100644 --- /dev/null +++ b/hgext/narrow/narrowdirstate.py @@ -0,0 +1,80 @@ +# narrowdirstate.py - extensions to mercurial dirstate to support narrow clones +# +# Copyright 2017 Google, Inc. +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +from __future__ import absolute_import + +from mercurial.i18n import _ +from mercurial import ( + dirstate, + error, + extensions, + match as matchmod, + util as hgutil, +) + +from . import narrowspec + +def setup(repo): + """Add narrow spec dirstate ignore, block changes outside narrow spec.""" + + def walk(orig, self, match, subrepos, unknown, ignored, full=True, + narrowonly=True): + if narrowonly: + narrowmatch = repo.narrowmatch() + match = matchmod.intersectmatchers(match, narrowmatch) + return orig(self, match, subrepos, unknown, ignored, full) + + extensions.wrapfunction(dirstate.dirstate, 'walk', walk) + + # Prevent adding files that are outside the sparse checkout + editfuncs = ['normal', 'add', 'normallookup', 'copy', 'remove', 'merge'] + for func in editfuncs: + def _wrapper(orig, self, *args): + dirstate = repo.dirstate + narrowmatch = repo.narrowmatch() + for f in args: + if f is not None and not narrowmatch(f) and f not in dirstate: + raise error.Abort(_("cannot track '%s' - it is outside " + + "the narrow clone") % f) + return orig(self, *args) + extensions.wrapfunction(dirstate.dirstate, func, _wrapper) + + def filterrebuild(orig, self, parent, allfiles, changedfiles=None): + if changedfiles is None: + # Rebuilding entire dirstate, let's filter allfiles to match the + # narrowspec. + allfiles = [f for f in allfiles if repo.narrowmatch()(f)] + orig(self, parent, allfiles, changedfiles) + + extensions.wrapfunction(dirstate.dirstate, 'rebuild', filterrebuild) + + def _narrowbackupname(backupname): + assert 'dirstate' in backupname + return backupname.replace('dirstate', narrowspec.FILENAME) + + def restorebackup(orig, self, tr, backupname): + self._opener.rename(_narrowbackupname(backupname), narrowspec.FILENAME, + checkambig=True) + orig(self, tr, backupname) + + extensions.wrapfunction(dirstate.dirstate, 'restorebackup', restorebackup) + + def savebackup(orig, self, tr, backupname): + orig(self, tr, backupname) + + narrowbackupname = _narrowbackupname(backupname) + self._opener.tryunlink(narrowbackupname) + hgutil.copyfile(self._opener.join(narrowspec.FILENAME), + self._opener.join(narrowbackupname), hardlink=True) + + extensions.wrapfunction(dirstate.dirstate, 'savebackup', savebackup) + + def clearbackup(orig, self, tr, backupname): + orig(self, tr, backupname) + self._opener.unlink(_narrowbackupname(backupname)) + + extensions.wrapfunction(dirstate.dirstate, 'clearbackup', clearbackup) diff --git a/hgext/narrow/narrowmerge.py b/hgext/narrow/narrowmerge.py new file mode 100644 --- /dev/null +++ b/hgext/narrow/narrowmerge.py @@ -0,0 +1,76 @@ +# narrowmerge.py - extensions to mercurial merge module to support narrow clones +# +# Copyright 2017 Google, Inc. +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +from __future__ import absolute_import + +from mercurial.i18n import _ +from mercurial import ( + copies, + error, + extensions, + merge, + util, +) + +def setup(): + def _manifestmerge(orig, repo, wctx, p2, pa, branchmerge, *args, **kwargs): + """Filter updates to only lay out files that match the narrow spec.""" + actions, diverge, renamedelete = orig( + repo, wctx, p2, pa, branchmerge, *args, **kwargs) + + if not util.safehasattr(repo, 'narrowmatch'): + return actions, diverge, renamedelete + + nooptypes = set(['k']) # TODO: handle with nonconflicttypes + nonconflicttypes = set('a am c cm f g r e'.split()) + narrowmatch = repo.narrowmatch() + for f, action in actions.items(): + if narrowmatch(f): + pass + elif not branchmerge: + del actions[f] # just updating, ignore changes outside clone + elif action[0] in nooptypes: + del actions[f] # merge does not affect file + elif action[0] in nonconflicttypes: + raise error.Abort(_('merge affects file \'%s\' outside narrow, ' + 'which is not yet supported') % f, + hint=_('merging in the other direction ' + 'may work')) + else: + raise error.Abort(_('conflict in file \'%s\' is outside ' + 'narrow clone') % f) + + return actions, diverge, renamedelete + + extensions.wrapfunction(merge, 'manifestmerge', _manifestmerge) + + def _checkcollision(orig, repo, wmf, actions): + if util.safehasattr(repo, 'narrowmatch'): + narrowmatch = repo.narrowmatch() + wmf = wmf.matches(narrowmatch) + if actions: + narrowactions = {} + for m, actionsfortype in actions.iteritems(): + narrowactions[m] = [] + for (f, args, msg) in actionsfortype: + if narrowmatch(f): + narrowactions[m].append((f, args, msg)) + actions = narrowactions + return orig(repo, wmf, actions) + + extensions.wrapfunction(merge, '_checkcollision', _checkcollision) + + def _computenonoverlap(orig, repo, *args, **kwargs): + u1, u2 = orig(repo, *args, **kwargs) + if not util.safehasattr(repo, 'narrowmatch'): + return u1, u2 + + narrowmatch = repo.narrowmatch() + u1 = [f for f in u1 if narrowmatch(f)] + u2 = [f for f in u2 if narrowmatch(f)] + return u1, u2 + extensions.wrapfunction(copies, '_computenonoverlap', _computenonoverlap) diff --git a/hgext/narrow/narrowpatch.py b/hgext/narrow/narrowpatch.py new file mode 100644 --- /dev/null +++ b/hgext/narrow/narrowpatch.py @@ -0,0 +1,42 @@ +# narrowpatch.py - extensions to mercurial patch module to support narrow clones +# +# Copyright 2017 Google, Inc. +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +from __future__ import absolute_import + +from mercurial import ( + extensions, + patch, + util, +) + +def setup(repo): + def _filepairs(orig, *args): + """Only includes files within the narrow spec in the diff.""" + if util.safehasattr(repo, 'narrowmatch'): + narrowmatch = repo.narrowmatch() + for x in orig(*args): + f1, f2, copyop = x + if ((not f1 or narrowmatch(f1)) and + (not f2 or narrowmatch(f2))): + yield x + else: + for x in orig(*args): + yield x + + def trydiff(orig, repo, revs, ctx1, ctx2, modified, added, removed, + copy, getfilectx, *args, **kwargs): + if util.safehasattr(repo, 'narrowmatch'): + narrowmatch = repo.narrowmatch() + modified = filter(narrowmatch, modified) + added = filter(narrowmatch, added) + removed = filter(narrowmatch, removed) + copy = {k: v for k, v in copy.iteritems() if narrowmatch(k)} + return orig(repo, revs, ctx1, ctx2, modified, added, removed, copy, + getfilectx, *args, **kwargs) + + extensions.wrapfunction(patch, '_filepairs', _filepairs) + extensions.wrapfunction(patch, 'trydiff', trydiff) diff --git a/hgext/narrow/narrowrepo.py b/hgext/narrow/narrowrepo.py new file mode 100644 --- /dev/null +++ b/hgext/narrow/narrowrepo.py @@ -0,0 +1,110 @@ +# narrowrepo.py - repository which supports narrow revlogs, lazy loading +# +# Copyright 2017 Google, Inc. +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +from __future__ import absolute_import + +from mercurial import ( + bundlerepo, + localrepo, + match as matchmod, + scmutil, +) + +from .. import ( + share, +) + +from . import ( + narrowrevlog, + narrowspec, +) + +requirement = 'narrowhg' + +def wrappostshare(orig, sourcerepo, destrepo, **kwargs): + orig(sourcerepo, destrepo, **kwargs) + if requirement in sourcerepo.requirements: + with destrepo.wlock(): + with destrepo.vfs('shared', 'a') as fp: + fp.write(narrowspec.FILENAME + '\n') + +def unsharenarrowspec(orig, ui, repo, repopath): + if (requirement in repo.requirements + and repo.path == repopath and repo.shared()): + srcrepo = share._getsrcrepo(repo) + with srcrepo.vfs(narrowspec.FILENAME) as f: + spec = f.read() + with repo.vfs(narrowspec.FILENAME, 'w') as f: + f.write(spec) + return orig(ui, repo, repopath) + +def wraprepo(repo, opts_narrow): + """Enables narrow clone functionality on a single local repository.""" + + cacheprop = localrepo.storecache + if isinstance(repo, bundlerepo.bundlerepository): + # We have to use a different caching property decorator for + # bundlerepo because storecache blows up in strange ways on a + # bundlerepo. Fortunately, there's no risk of data changing in + # a bundlerepo. + cacheprop = lambda name: localrepo.unfilteredpropertycache + + class narrowrepository(repo.__class__): + + def _constructmanifest(self): + manifest = super(narrowrepository, self)._constructmanifest() + narrowrevlog.makenarrowmanifestrevlog(manifest, repo) + return manifest + + @cacheprop('00manifest.i') + def manifestlog(self): + mfl = super(narrowrepository, self).manifestlog + narrowrevlog.makenarrowmanifestlog(mfl, self) + return mfl + + def file(self, f): + fl = super(narrowrepository, self).file(f) + narrowrevlog.makenarrowfilelog(fl, self.narrowmatch()) + return fl + + @localrepo.repofilecache(narrowspec.FILENAME) + def narrowpats(self): + return narrowspec.load(self) + + @localrepo.repofilecache(narrowspec.FILENAME) + def _narrowmatch(self): + include, exclude = self.narrowpats + if not opts_narrow and not include and not exclude: + return matchmod.always(self.root, '') + return narrowspec.match(self.root, include=include, exclude=exclude) + + # TODO(martinvonz): make this property-like instead? + def narrowmatch(self): + return self._narrowmatch + + def setnarrowpats(self, newincludes, newexcludes): + narrowspec.save(self, newincludes, newexcludes) + self.invalidate(clearfilecache=True) + + # I'm not sure this is the right place to do this filter. + # context._manifestmatches() would probably be better, or perhaps + # move it to a later place, in case some of the callers do want to know + # which directories changed. This seems to work for now, though. + def status(self, *args, **kwargs): + s = super(narrowrepository, self).status(*args, **kwargs) + narrowmatch = self.narrowmatch() + modified = filter(narrowmatch, s.modified) + added = filter(narrowmatch, s.added) + removed = filter(narrowmatch, s.removed) + deleted = filter(narrowmatch, s.deleted) + unknown = filter(narrowmatch, s.unknown) + ignored = filter(narrowmatch, s.ignored) + clean = filter(narrowmatch, s.clean) + return scmutil.status(modified, added, removed, deleted, unknown, + ignored, clean) + + repo.__class__ = narrowrepository diff --git a/hgext/narrow/narrowrevlog.py b/hgext/narrow/narrowrevlog.py new file mode 100644 --- /dev/null +++ b/hgext/narrow/narrowrevlog.py @@ -0,0 +1,163 @@ +# narrowrevlog.py - revlog storing irrelevant nodes as "ellipsis" nodes +# +# Copyright 2017 Google, Inc. +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +from __future__ import absolute_import + +from mercurial import ( + manifest, + revlog, + util, +) + +ELLIPSIS_NODE_FLAG = 1 << 14 +revlog.REVIDX_KNOWN_FLAGS |= ELLIPSIS_NODE_FLAG +if (util.safehasattr(revlog, 'REVIDX_FLAGS_ORDER') and + ELLIPSIS_NODE_FLAG not in revlog.REVIDX_FLAGS_ORDER): + revlog.REVIDX_FLAGS_ORDER.append(ELLIPSIS_NODE_FLAG) + +def readtransform(self, text): + return text, False + +def writetransform(self, text): + return text, False + +def rawtransform(self, text): + return False + +if util.safehasattr(revlog, 'addflagprocessor'): + revlog.addflagprocessor(ELLIPSIS_NODE_FLAG, + (readtransform, writetransform, rawtransform)) + +def setup(): + # We just wanted to add the flag processor, which is done at module + # load time. + pass + +class excludeddir(manifest.treemanifest): + def __init__(self, dir, node): + super(excludeddir, self).__init__(dir) + self._node = node + # Add an empty file, which will be included by iterators and such, + # appearing as the directory itself (i.e. something like "dir/") + self._files[''] = node + self._flags[''] = 't' + + # Manifests outside the narrowspec should never be modified, so avoid + # copying. This makes a noticeable difference when there are very many + # directories outside the narrowspec. Also, it makes sense for the copy to + # be of the same type as the original, which would not happen with the + # super type's copy(). + def copy(self): + return self + +class excludeddirmanifestctx(manifest.treemanifestctx): + def __init__(self, dir, node): + self._dir = dir + self._node = node + + def read(self): + return excludeddir(self._dir, self._node) + + def write(self, *args): + raise AssertionError('Attempt to write manifest from excluded dir %s' % + self._dir) + +class excludedmanifestrevlog(manifest.manifestrevlog): + def __init__(self, dir): + self._dir = dir + + def __len__(self): + raise AssertionError('Attempt to get length of excluded dir %s' % + self._dir) + + def rev(self, node): + raise AssertionError('Attempt to get rev from excluded dir %s' % + self._dir) + + def linkrev(self, node): + raise AssertionError('Attempt to get linkrev from excluded dir %s' % + self._dir) + + def node(self, rev): + raise AssertionError('Attempt to get node from excluded dir %s' % + self._dir) + + def add(self, *args, **kwargs): + # We should never write entries in dirlogs outside the narrow clone. + # However, the method still gets called from writesubtree() in + # _addtree(), so we need to handle it. We should possibly make that + # avoid calling add() with a clean manifest (_dirty is always False + # in excludeddir instances). + pass + +def makenarrowmanifestrevlog(mfrevlog, repo): + if util.safehasattr(mfrevlog, '_narrowed'): + return + + class narrowmanifestrevlog(mfrevlog.__class__): + # This function is called via debug{revlog,index,data}, but also during + # at least some push operations. This will be used to wrap/exclude the + # child directories when using treemanifests. + def dirlog(self, dir): + if dir and not dir.endswith('/'): + dir = dir + '/' + if not repo.narrowmatch().visitdir(dir[:-1] or '.'): + return excludedmanifestrevlog(dir) + result = super(narrowmanifestrevlog, self).dirlog(dir) + makenarrowmanifestrevlog(result, repo) + return result + + mfrevlog.__class__ = narrowmanifestrevlog + mfrevlog._narrowed = True + +def makenarrowmanifestlog(mfl, repo): + class narrowmanifestlog(mfl.__class__): + def get(self, dir, node, verify=True): + if not repo.narrowmatch().visitdir(dir[:-1] or '.'): + return excludeddirmanifestctx(dir, node) + return super(narrowmanifestlog, self).get(dir, node, verify=verify) + mfl.__class__ = narrowmanifestlog + +def makenarrowfilelog(fl, narrowmatch): + class narrowfilelog(fl.__class__): + def renamed(self, node): + m = super(narrowfilelog, self).renamed(node) + if m and not narrowmatch(m[0]): + return None + return m + + def size(self, rev): + # We take advantage of the fact that remotefilelog + # lacks a node() method to just skip the + # rename-checking logic when on remotefilelog. This + # might be incorrect on other non-revlog-based storage + # engines, but for now this seems to be fine. + if util.safehasattr(self, 'node'): + node = self.node(rev) + # Because renamed() is overridden above to + # sometimes return None even if there is metadata + # in the revlog, size can be incorrect for + # copies/renames, so we need to make sure we call + # the super class's implementation of renamed() + # for the purpose of size calculation. + if super(narrowfilelog, self).renamed(node): + return len(self.read(node)) + return super(narrowfilelog, self).size(rev) + + def cmp(self, node, text): + different = super(narrowfilelog, self).cmp(node, text) + if different: + # Similar to size() above, if the file was copied from + # a file outside the narrowspec, the super class's + # would have returned True because we tricked it into + # thinking that the file was not renamed. + if super(narrowfilelog, self).renamed(node): + t2 = self.read(node) + return t2 != text + return different + + fl.__class__ = narrowfilelog diff --git a/hgext/narrow/narrowspec.py b/hgext/narrow/narrowspec.py new file mode 100644 --- /dev/null +++ b/hgext/narrow/narrowspec.py @@ -0,0 +1,204 @@ +# narrowspec.py - methods for working with a narrow view of a repository +# +# Copyright 2017 Google, Inc. +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +from __future__ import absolute_import + +import errno + +from mercurial.i18n import _ +from mercurial import ( + error, + match as matchmod, + util, +) + +from .. import ( + share, +) + +FILENAME = 'narrowspec' + +def _parsestoredpatterns(text): + """Parses the narrowspec format that's stored on disk.""" + patlist = None + includepats = [] + excludepats = [] + for l in text.splitlines(): + if l == '[includes]': + if patlist is None: + patlist = includepats + else: + raise error.Abort(_('narrowspec includes section must appear ' + 'at most once, before excludes')) + elif l == '[excludes]': + if patlist is not excludepats: + patlist = excludepats + else: + raise error.Abort(_('narrowspec excludes section must appear ' + 'at most once')) + else: + patlist.append(l) + + return set(includepats), set(excludepats) + +def parseserverpatterns(text): + """Parses the narrowspec format that's returned by the server.""" + includepats = set() + excludepats = set() + + # We get one entry per line, in the format " ". + # It's OK for value to contain other spaces. + for kp in (l.split(' ', 1) for l in text.splitlines()): + if len(kp) != 2: + raise error.Abort(_('Invalid narrowspec pattern line: "%s"') % kp) + key = kp[0] + pat = kp[1] + if key == 'include': + includepats.add(pat) + elif key == 'exclude': + excludepats.add(pat) + else: + raise error.Abort(_('Invalid key "%s" in server response') % key) + + return includepats, excludepats + +def normalizesplitpattern(kind, pat): + """Returns the normalized version of a pattern and kind. + + Returns a tuple with the normalized kind and normalized pattern. + """ + pat = pat.rstrip('/') + _validatepattern(pat) + return kind, pat + +def _numlines(s): + """Returns the number of lines in s, including ending empty lines.""" + # We use splitlines because it is Unicode-friendly and thus Python 3 + # compatible. However, it does not count empty lines at the end, so trick + # it by adding a character at the end. + return len((s + 'x').splitlines()) + +def _validatepattern(pat): + """Validates the pattern and aborts if it is invalid.""" + + # We use newlines as separators in the narrowspec file, so don't allow them + # in patterns. + if _numlines(pat) > 1: + raise error.Abort('newlines are not allowed in narrowspec paths') + + components = pat.split('/') + if '.' in components or '..' in components: + raise error.Abort(_('"." and ".." are not allowed in narrowspec paths')) + +def normalizepattern(pattern, defaultkind='path'): + """Returns the normalized version of a text-format pattern. + + If the pattern has no kind, the default will be added. + """ + kind, pat = matchmod._patsplit(pattern, defaultkind) + return '%s:%s' % normalizesplitpattern(kind, pat) + +def parsepatterns(pats): + """Parses a list of patterns into a typed pattern set.""" + return set(normalizepattern(p) for p in pats) + +def format(includes, excludes): + output = '[includes]\n' + for i in sorted(includes - excludes): + output += i + '\n' + output += '[excludes]\n' + for e in sorted(excludes): + output += e + '\n' + return output + +def match(root, include=None, exclude=None): + if not include: + # Passing empty include and empty exclude to matchmod.match() + # gives a matcher that matches everything, so explicitly use + # the nevermatcher. + return matchmod.never(root, '') + return matchmod.match(root, '', [], include=include or [], + exclude=exclude or []) + +def needsexpansion(includes): + return [i for i in includes if i.startswith('include:')] + +def load(repo): + if repo.shared(): + repo = share._getsrcrepo(repo) + try: + spec = repo.vfs.read(FILENAME) + except IOError as e: + # Treat "narrowspec does not exist" the same as "narrowspec file exists + # and is empty". + if e.errno == errno.ENOENT: + # Without this the next call to load will use the cached + # non-existence of the file, which can cause some odd issues. + repo.invalidate(clearfilecache=True) + return set(), set() + raise + return _parsestoredpatterns(spec) + +def save(repo, includepats, excludepats): + spec = format(includepats, excludepats) + if repo.shared(): + repo = share._getsrcrepo(repo) + repo.vfs.write(FILENAME, spec) + +def restrictpatterns(req_includes, req_excludes, repo_includes, repo_excludes, + invalid_includes=None): + r""" Restricts the patterns according to repo settings, + results in a logical AND operation + + :param req_includes: requested includes + :param req_excludes: requested excludes + :param repo_includes: repo includes + :param repo_excludes: repo excludes + :param invalid_includes: an array to collect invalid includes + :return: include and exclude patterns + + >>> restrictpatterns({'f1','f2'}, {}, ['f1'], []) + (set(['f1']), {}) + >>> restrictpatterns({'f1'}, {}, ['f1','f2'], []) + (set(['f1']), {}) + >>> restrictpatterns({'f1/fc1', 'f3/fc3'}, {}, ['f1','f2'], []) + (set(['f1/fc1']), {}) + >>> restrictpatterns({'f1_fc1'}, {}, ['f1','f2'], []) + ([], set(['path:.'])) + >>> restrictpatterns({'f1/../f2/fc2'}, {}, ['f1','f2'], []) + (set(['f2/fc2']), {}) + >>> restrictpatterns({'f1/../f3/fc3'}, {}, ['f1','f2'], []) + ([], set(['path:.'])) + >>> restrictpatterns({'f1/$non_exitent_var'}, {}, ['f1','f2'], []) + (set(['f1/$non_exitent_var']), {}) + """ + res_excludes = req_excludes.copy() + res_excludes.update(repo_excludes) + if not req_includes: + res_includes = set(repo_includes) + elif 'path:.' not in repo_includes: + res_includes = [] + for req_include in req_includes: + req_include = util.expandpath(util.normpath(req_include)) + if req_include in repo_includes: + res_includes.append(req_include) + continue + valid = False + for repo_include in repo_includes: + if req_include.startswith(repo_include + '/'): + valid = True + res_includes.append(req_include) + break + if not valid and invalid_includes is not None: + invalid_includes.append(req_include) + if len(res_includes) == 0: + res_excludes = {'path:.'} + else: + res_includes = set(res_includes) + else: + res_includes = set(req_includes) + return res_includes, res_excludes diff --git a/hgext/narrow/narrowtemplates.py b/hgext/narrow/narrowtemplates.py new file mode 100644 --- /dev/null +++ b/hgext/narrow/narrowtemplates.py @@ -0,0 +1,50 @@ +# narrowtemplates.py - added template keywords for narrow clones +# +# Copyright 2017 Google, Inc. +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +from __future__ import absolute_import + +from mercurial import ( + revset, + templatekw, + util, +) + +from . import narrowrevlog + +def _isellipsis(repo, rev): + if repo.changelog.flags(rev) & narrowrevlog.ELLIPSIS_NODE_FLAG: + return True + return False + +def ellipsis(repo, ctx, templ, **args): + """:ellipsis: String. 'ellipsis' if the change is an ellipsis node, + else ''.""" + if _isellipsis(repo, ctx.rev()): + return 'ellipsis' + return '' + +def outsidenarrow(repo, ctx, templ, **args): + """:outsidenarrow: String. 'outsidenarrow' if the change affects no + tracked files, else ''.""" + if util.safehasattr(repo, 'narrowmatch'): + m = repo.narrowmatch() + if not any(m(f) for f in ctx.files()): + return 'outsidenarrow' + return '' + +def ellipsisrevset(repo, subset, x): + """``ellipsis()`` + Changesets that are ellipsis nodes. + """ + return subset.filter(lambda r: _isellipsis(repo, r)) + +def setup(): + templatekw.keywords['ellipsis'] = ellipsis + templatekw.keywords['outsidenarrow'] = outsidenarrow + + revset.symbols['ellipsis'] = ellipsisrevset + revset.safesymbols.add('ellipsis') diff --git a/hgext/narrow/narrowwirepeer.py b/hgext/narrow/narrowwirepeer.py new file mode 100644 --- /dev/null +++ b/hgext/narrow/narrowwirepeer.py @@ -0,0 +1,51 @@ +# narrowwirepeer.py - passes narrow spec with unbundle command +# +# Copyright 2017 Google, Inc. +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +from __future__ import absolute_import + +from mercurial.i18n import _ +from mercurial import ( + error, + extensions, + hg, + node, +) + +from . import narrowspec + +def uisetup(): + def peersetup(ui, peer): + # We must set up the expansion before reposetup below, since it's used + # at clone time before we have a repo. + class expandingpeer(peer.__class__): + def expandnarrow(self, narrow_include, narrow_exclude, nodes): + ui.status(_("expanding narrowspec\n")) + if not self.capable('expandnarrow'): + raise error.Abort( + 'peer does not support expanding narrowspecs') + + hex_nodes = (node.hex(n) for n in nodes) + new_narrowspec = self._call( + 'expandnarrow', + includepats=','.join(narrow_include), + excludepats=','.join(narrow_exclude), + nodes=','.join(hex_nodes)) + + return narrowspec.parseserverpatterns(new_narrowspec) + peer.__class__ = expandingpeer + hg.wirepeersetupfuncs.append(peersetup) + +def reposetup(repo): + def wirereposetup(ui, peer): + def wrapped(orig, cmd, *args, **kwargs): + if cmd == 'unbundle': + include, exclude = repo.narrowpats + kwargs["includepats"] = ','.join(include) + kwargs["excludepats"] = ','.join(exclude) + return orig(cmd, *args, **kwargs) + extensions.wrapfunction(peer, '_calltwowaystream', wrapped) + hg.wirepeersetupfuncs.append(wirereposetup) diff --git a/setup.py b/setup.py --- a/setup.py +++ b/setup.py @@ -792,7 +792,8 @@ packages = ['mercurial', 'mercurial.thirdparty.attr', 'hgext', 'hgext.convert', 'hgext.fsmonitor', 'hgext.fsmonitor.pywatchman', 'hgext.highlight', - 'hgext.largefiles', 'hgext.lfs', 'hgext.zeroconf', 'hgext3rd', + 'hgext.largefiles', 'hgext.lfs', 'hgext.narrow', + 'hgext.zeroconf', 'hgext3rd', 'hgdemandimport'] common_depends = ['mercurial/bitmanipulation.h', diff --git a/tests/narrow-library.sh b/tests/narrow-library.sh new file mode 100644 --- /dev/null +++ b/tests/narrow-library.sh @@ -0,0 +1,9 @@ +cat >> $HGRCPATH < echo $x > "f$x" + > hg add "f$x" + > hg commit -m "Add $x" + > done + $ cat >> .hg/hgrc << EOF + > [narrowhgacl] + > default.includes=f1 f2 + > EOF + $ hg serve -a localhost -p $HGPORT1 -d --pid-file=hg.pid + $ cat hg.pid >> "$DAEMON_PIDS" + + $ cd .. + $ hg clone http://localhost:$HGPORT1 narrowclone1 + requesting all changes + adding changesets + adding manifests + adding file changes + added 3 changesets with 2 changes to 2 files + new changesets * (glob) + updating to branch default + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + +The clone directory should only contain f1 and f2 + $ ls -1 narrowclone1 | sort + f1 + f2 + +Requirements should contain narrowhg + $ cat narrowclone1/.hg/requires | grep narrowhg + narrowhg + +NarrowHG should track f1 and f2 + $ hg -R narrowclone1 tracked + I path:f1 + I path:f2 diff --git a/tests/test-narrow-archive.t b/tests/test-narrow-archive.t new file mode 100644 --- /dev/null +++ b/tests/test-narrow-archive.t @@ -0,0 +1,32 @@ +Make a narrow clone then archive it + $ . "$TESTDIR/narrow-library.sh" + + $ hg init master + $ cd master + + $ for x in `$TESTDIR/seq.py 3`; do + > echo $x > "f$x" + > hg add "f$x" + > hg commit -m "Add $x" + > done + + $ hg serve -a localhost -p $HGPORT1 -d --pid-file=hg.pid + $ cat hg.pid >> "$DAEMON_PIDS" + + $ cd .. + $ hg clone --narrow --include f1 --include f2 http://localhost:$HGPORT1/ narrowclone1 + requesting all changes + adding changesets + adding manifests + adding file changes + added 3 changesets with 2 changes to 2 files + new changesets * (glob) + updating to branch default + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + +The tar should only contain f1 and f2 + $ cd narrowclone1 + $ hg archive -t tgz repo.tgz + $ tar tfz repo.tgz + repo/f1 + repo/f2 diff --git a/tests/test-narrow-clone-no-ellipsis.t b/tests/test-narrow-clone-no-ellipsis.t new file mode 100644 --- /dev/null +++ b/tests/test-narrow-clone-no-ellipsis.t @@ -0,0 +1,130 @@ + $ . "$TESTDIR/narrow-library.sh" + + $ hg init master + $ cd master + $ mkdir dir + $ mkdir dir/src + $ cd dir/src + $ for x in `$TESTDIR/seq.py 20`; do echo $x > "f$x"; hg add "f$x"; hg commit -m "Commit src $x"; done + $ cd .. + $ mkdir tests + $ cd tests + $ for x in `$TESTDIR/seq.py 20`; do echo $x > "t$x"; hg add "t$x"; hg commit -m "Commit test $x"; done + $ cd ../../.. + +narrow clone a file, f10 + + $ hg clone --narrow ssh://user@dummy/master narrow --noupdate --include "dir/src/f10" + requesting all changes + adding changesets + adding manifests + adding file changes + added 40 changesets with 1 changes to 1 files + new changesets *:* (glob) + $ cd narrow + $ cat .hg/requires | grep -v generaldelta + dotencode + fncache + narrowhg + revlogv1 + store + + $ cat .hg/narrowspec + [includes] + path:dir/src/f10 + [excludes] + $ hg update + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ find * | sort + dir + dir/src + dir/src/f10 + $ cat dir/src/f10 + 10 + + $ cd .. + +narrow clone a directory, tests/, except tests/t19 + + $ hg clone --narrow ssh://user@dummy/master narrowdir --noupdate --include "dir/tests/" --exclude "dir/tests/t19" + requesting all changes + adding changesets + adding manifests + adding file changes + added 40 changesets with 19 changes to 19 files + new changesets *:* (glob) + $ cd narrowdir + $ cat .hg/narrowspec + [includes] + path:dir/tests + [excludes] + path:dir/tests/t19 + $ hg update + 19 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ find * | sort + dir + dir/tests + dir/tests/t1 + dir/tests/t10 + dir/tests/t11 + dir/tests/t12 + dir/tests/t13 + dir/tests/t14 + dir/tests/t15 + dir/tests/t16 + dir/tests/t17 + dir/tests/t18 + dir/tests/t2 + dir/tests/t20 + dir/tests/t3 + dir/tests/t4 + dir/tests/t5 + dir/tests/t6 + dir/tests/t7 + dir/tests/t8 + dir/tests/t9 + + $ cd .. + +narrow clone everything but a directory (tests/) + + $ hg clone --narrow ssh://user@dummy/master narrowroot --noupdate --exclude "dir/tests" + requesting all changes + adding changesets + adding manifests + adding file changes + added 40 changesets with 20 changes to 20 files + new changesets *:* (glob) + $ cd narrowroot + $ cat .hg/narrowspec + [includes] + path:. + [excludes] + path:dir/tests + $ hg update + 20 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ find * | sort + dir + dir/src + dir/src/f1 + dir/src/f10 + dir/src/f11 + dir/src/f12 + dir/src/f13 + dir/src/f14 + dir/src/f15 + dir/src/f16 + dir/src/f17 + dir/src/f18 + dir/src/f19 + dir/src/f2 + dir/src/f20 + dir/src/f3 + dir/src/f4 + dir/src/f5 + dir/src/f6 + dir/src/f7 + dir/src/f8 + dir/src/f9 + + $ cd .. diff --git a/tests/test-narrow-clone-non-narrow-server.t b/tests/test-narrow-clone-non-narrow-server.t new file mode 100644 --- /dev/null +++ b/tests/test-narrow-clone-non-narrow-server.t @@ -0,0 +1,53 @@ +Test attempting a narrow clone against a server that doesn't support narrowhg. + + $ . "$TESTDIR/narrow-library.sh" + + $ hg init master + $ cd master + + $ for x in `$TESTDIR/seq.py 10`; do + > echo $x > "f$x" + > hg add "f$x" + > hg commit -m "Add $x" + > done + + $ hg serve -a localhost -p $HGPORT1 --config extensions.narrow=! -d \ + > --pid-file=hg.pid + $ cat hg.pid >> "$DAEMON_PIDS" + $ hg serve -a localhost -p $HGPORT2 -d --pid-file=hg.pid + $ cat hg.pid >> "$DAEMON_PIDS" + +Verify that narrow is advertised in the bundle2 capabilities: + $ echo capabilities | hg -R . serve --stdio | \ + > python -c "import sys, urllib; print urllib.unquote_plus(list(sys.stdin)[1])" | grep narrow + narrow=v0 + + $ cd .. + + $ hg clone --narrow --include f1 http://localhost:$HGPORT1/ narrowclone + requesting all changes + abort: server doesn't support narrow clones + [255] + +Make a narrow clone (via HGPORT2), then try to narrow and widen +into it (from HGPORT1) to prove that narrowing is fine and widening fails +gracefully: + $ hg clone -r 0 --narrow --include f1 http://localhost:$HGPORT2/ narrowclone + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files + new changesets * (glob) + updating to branch default + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd narrowclone + $ hg tracked --addexclude f2 http://localhost:$HGPORT1/ + comparing with http://localhost:$HGPORT1/ + searching for changes + looking for local changes to affected paths + $ hg tracked --addinclude f1 http://localhost:$HGPORT1/ + comparing with http://localhost:$HGPORT1/ + searching for changes + no changes found + abort: server doesn't support narrow clones + [255] diff --git a/tests/test-narrow-clone-nonlinear.t b/tests/test-narrow-clone-nonlinear.t new file mode 100644 --- /dev/null +++ b/tests/test-narrow-clone-nonlinear.t @@ -0,0 +1,148 @@ +Testing narrow clones when changesets modifying a matching file exist on +multiple branches + + $ . "$TESTDIR/narrow-library.sh" + + $ hg init master + $ cd master + $ cat >> .hg/hgrc < [narrow] + > serveellipses=True + > EOF + + $ hg branch default + marked working directory as branch default + (branches are permanent and global, did you want a bookmark?) + $ for x in `$TESTDIR/seq.py 10`; do + > echo $x > "f$x" + > hg add "f$x" + > hg commit -m "Add $x" + > done + + $ hg branch release-v1 + marked working directory as branch release-v1 + (branches are permanent and global, did you want a bookmark?) + $ hg commit -m "Start release for v1" + + $ hg update default + 0 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ for x in `$TESTDIR/seq.py 10`; do + > echo "$x v2" > "f$x" + > hg commit -m "Update $x to v2" + > done + + $ hg update release-v1 + 10 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg branch release-v1 + marked working directory as branch release-v1 + $ for x in `$TESTDIR/seq.py 1 5`; do + > echo "$x v1 hotfix" > "f$x" + > hg commit -m "Hotfix $x in v1" + > done + + $ hg update default + 10 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg branch release-v2 + marked working directory as branch release-v2 + $ hg commit -m "Start release for v2" + + $ hg update default + 0 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg branch default + marked working directory as branch default + $ for x in `$TESTDIR/seq.py 10`; do + > echo "$x v3" > "f$x" + > hg commit -m "Update $x to v3" + > done + + $ hg update release-v2 + 10 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg branch release-v2 + marked working directory as branch release-v2 + $ for x in `$TESTDIR/seq.py 4 9`; do + > echo "$x v2 hotfix" > "f$x" + > hg commit -m "Hotfix $x in v2" + > done + + $ hg heads -T '{rev} <- {p1rev} ({branch}): {desc}\n' + 42 <- 41 (release-v2): Hotfix 9 in v2 + 36 <- 35 (default): Update 10 to v3 + 25 <- 24 (release-v1): Hotfix 5 in v1 + + $ cd .. + +We now have 3 branches: default, which has v3 of all files, release-v1 which +has v1 of all files, and release-v2 with v2 of all files. + +Narrow clone which should get all branches + + $ hg clone --narrow ssh://user@dummy/master narrow --include "f5" + requesting all changes + adding changesets + adding manifests + adding file changes + added 12 changesets with 5 changes to 1 files (+2 heads) + new changesets *:* (glob) + updating to branch default + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd narrow + $ hg log -G -T "{if(ellipsis, '...')}{node|short} ({branch}): {desc}\n" + o ...031f516143fe (release-v2): Hotfix 9 in v2 + | + o 9cd7f7bb9ca1 (release-v2): Hotfix 5 in v2 + | + o ...37bbc88f3ef0 (release-v2): Hotfix 4 in v2 + | + | @ ...dae2f368ca07 (default): Update 10 to v3 + | | + | o 9c224e89cb31 (default): Update 5 to v3 + | | + | o ...04fb59c7c9dc (default): Update 4 to v3 + |/ + | o b2253e82401f (release-v1): Hotfix 5 in v1 + | | + | o ...960ac37d74fd (release-v1): Hotfix 4 in v1 + | | + o | 986298e3f347 (default): Update 5 to v2 + | | + o | ...75d539c667ec (default): Update 4 to v2 + |/ + o 04c71bd5707f (default): Add 5 + | + o ...881b3891d041 (default): Add 4 + + +Narrow clone the first file, hitting edge condition where unaligned +changeset and manifest revnums cross branches. + + $ hg clone --narrow ssh://user@dummy/master narrow --include "f1" + requesting all changes + adding changesets + adding manifests + adding file changes + added 10 changesets with 4 changes to 1 files (+2 heads) + new changesets *:* (glob) + updating to branch default + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd narrow + $ hg log -G -T "{if(ellipsis, '...')}{node|short} ({branch}): {desc}\n" + o ...031f516143fe (release-v2): Hotfix 9 in v2 + | + | @ ...dae2f368ca07 (default): Update 10 to v3 + | | + | o 1f5d184b8e96 (default): Update 1 to v3 + |/ + | o ...b2253e82401f (release-v1): Hotfix 5 in v1 + | | + | o 133502f6b7e5 (release-v1): Hotfix 1 in v1 + | | + o | ...79165c83d644 (default): Update 10 to v2 + | | + o | c7b7a5f2f088 (default): Update 1 to v2 + | | + | o ...f0531a3db7a9 (release-v1): Start release for v1 + |/ + o ...6a3f0f0abef3 (default): Add 10 + | + o e012ac15eaaa (default): Add 1 + diff --git a/tests/test-narrow-clone.t b/tests/test-narrow-clone.t new file mode 100644 --- /dev/null +++ b/tests/test-narrow-clone.t @@ -0,0 +1,225 @@ + $ . "$TESTDIR/narrow-library.sh" + + $ hg init master + $ cd master + $ cat >> .hg/hgrc < [narrow] + > serveellipses=True + > EOF + $ mkdir dir + $ mkdir dir/src + $ cd dir/src + $ for x in `$TESTDIR/seq.py 20`; do echo $x > "f$x"; hg add "f$x"; hg commit -m "Commit src $x"; done + $ cd .. + $ mkdir tests + $ cd tests + $ for x in `$TESTDIR/seq.py 20`; do echo $x > "t$x"; hg add "t$x"; hg commit -m "Commit test $x"; done + $ cd ../../.. + +narrow clone a file, f10 + + $ hg clone --narrow ssh://user@dummy/master narrow --noupdate --include "dir/src/f10" + requesting all changes + adding changesets + adding manifests + adding file changes + added 3 changesets with 1 changes to 1 files + new changesets *:* (glob) + $ cd narrow + $ cat .hg/requires | grep -v generaldelta + dotencode + fncache + narrowhg + revlogv1 + store + + $ cat .hg/narrowspec + [includes] + path:dir/src/f10 + [excludes] + $ hg tracked + I path:dir/src/f10 + $ hg update + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ find * | sort + dir + dir/src + dir/src/f10 + $ cat dir/src/f10 + 10 + + $ cd .. + +narrow clone with a newline should fail + + $ hg clone --narrow ssh://user@dummy/master narrow_fail --noupdate --include 'dir/src/f10 + > ' + requesting all changes + abort: newlines are not allowed in narrowspec paths + [255] + +narrow clone a directory, tests/, except tests/t19 + + $ hg clone --narrow ssh://user@dummy/master narrowdir --noupdate --include "dir/tests/" --exclude "dir/tests/t19" + requesting all changes + adding changesets + adding manifests + adding file changes + added 21 changesets with 19 changes to 19 files + new changesets *:* (glob) + $ cd narrowdir + $ cat .hg/narrowspec + [includes] + path:dir/tests + [excludes] + path:dir/tests/t19 + $ hg tracked + I path:dir/tests + X path:dir/tests/t19 + $ hg update + 19 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ find * | sort + dir + dir/tests + dir/tests/t1 + dir/tests/t10 + dir/tests/t11 + dir/tests/t12 + dir/tests/t13 + dir/tests/t14 + dir/tests/t15 + dir/tests/t16 + dir/tests/t17 + dir/tests/t18 + dir/tests/t2 + dir/tests/t20 + dir/tests/t3 + dir/tests/t4 + dir/tests/t5 + dir/tests/t6 + dir/tests/t7 + dir/tests/t8 + dir/tests/t9 + + $ cd .. + +narrow clone everything but a directory (tests/) + + $ hg clone --narrow ssh://user@dummy/master narrowroot --noupdate --exclude "dir/tests" + requesting all changes + adding changesets + adding manifests + adding file changes + added 21 changesets with 20 changes to 20 files + new changesets *:* (glob) + $ cd narrowroot + $ cat .hg/narrowspec + [includes] + path:. + [excludes] + path:dir/tests + $ hg tracked + I path:. + X path:dir/tests + $ hg update + 20 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ find * | sort + dir + dir/src + dir/src/f1 + dir/src/f10 + dir/src/f11 + dir/src/f12 + dir/src/f13 + dir/src/f14 + dir/src/f15 + dir/src/f16 + dir/src/f17 + dir/src/f18 + dir/src/f19 + dir/src/f2 + dir/src/f20 + dir/src/f3 + dir/src/f4 + dir/src/f5 + dir/src/f6 + dir/src/f7 + dir/src/f8 + dir/src/f9 + + $ cd .. + +narrow clone no paths at all + + $ hg clone --narrow ssh://user@dummy/master narrowempty --noupdate + requesting all changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 0 changes to 0 files + new changesets * (glob) + $ cd narrowempty + $ hg tracked + $ hg update + 0 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ ls + + $ cd .. + +simple clone + $ hg clone ssh://user@dummy/master simpleclone + requesting all changes + adding changesets + adding manifests + adding file changes + added 40 changesets with 40 changes to 40 files + new changesets * (glob) + updating to branch default + 40 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd simpleclone + $ find * | sort + dir + dir/src + dir/src/f1 + dir/src/f10 + dir/src/f11 + dir/src/f12 + dir/src/f13 + dir/src/f14 + dir/src/f15 + dir/src/f16 + dir/src/f17 + dir/src/f18 + dir/src/f19 + dir/src/f2 + dir/src/f20 + dir/src/f3 + dir/src/f4 + dir/src/f5 + dir/src/f6 + dir/src/f7 + dir/src/f8 + dir/src/f9 + dir/tests + dir/tests/t1 + dir/tests/t10 + dir/tests/t11 + dir/tests/t12 + dir/tests/t13 + dir/tests/t14 + dir/tests/t15 + dir/tests/t16 + dir/tests/t17 + dir/tests/t18 + dir/tests/t19 + dir/tests/t2 + dir/tests/t20 + dir/tests/t3 + dir/tests/t4 + dir/tests/t5 + dir/tests/t6 + dir/tests/t7 + dir/tests/t8 + dir/tests/t9 + + $ cd .. diff --git a/tests/test-narrow-commit-tree.t b/tests/test-narrow-commit-tree.t new file mode 100644 --- /dev/null +++ b/tests/test-narrow-commit-tree.t @@ -0,0 +1,21 @@ + $ cd $TESTDIR && python $RUNTESTDIR/run-tests.py \ + > --extra-config-opt experimental.treemanifest=1 test-narrow-commit.t 2>&1 | \ + > grep -v 'unexpected mercurial lib' | egrep -v '\(expected' + + --- */tests/test-narrow-commit.t (glob) + +++ */tests/test-narrow-commit.t.err (glob) + @@ -\d+,\d+ \+\d+,\d+ @@ (re) + created new head + $ hg files -r . + inside/f1 + - outside/f1 + + outside/ + Some filesystems (notably FAT/exFAT only store timestamps with 2 + seconds of precision, so by sleeping for 3 seconds, we can ensure that + the timestamps of files stored by dirstate will appear older than the + + ERROR: test-narrow-commit.t output changed + ! + Failed test-narrow-commit.t: output changed + # Ran 1 tests, 0 skipped, 1 failed. + python hash seed: * (glob) diff --git a/tests/test-narrow-commit.t b/tests/test-narrow-commit.t new file mode 100644 --- /dev/null +++ b/tests/test-narrow-commit.t @@ -0,0 +1,79 @@ + $ . "$TESTDIR/narrow-library.sh" + +create full repo + + $ hg init master + $ cd master + + $ mkdir inside + $ echo inside > inside/f1 + $ mkdir outside + $ echo outside > outside/f1 + $ hg ci -Aqm 'initial' + + $ echo modified > inside/f1 + $ hg ci -qm 'modify inside' + + $ echo modified > outside/f1 + $ hg ci -qm 'modify outside' + + $ cd .. + + $ hg clone --narrow ssh://user@dummy/master narrow --include inside + requesting all changes + adding changesets + adding manifests + adding file changes + added 3 changesets with 2 changes to 1 files + new changesets *:* (glob) + updating to branch default + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd narrow + + $ hg update -q 0 + +Can not modify dirstate outside + + $ mkdir outside + $ touch outside/f1 + $ hg debugwalk -I 'relglob:f1' + matcher: + f inside/f1 inside/f1 + $ hg add outside/f1 + abort: cannot track 'outside/f1' - it is outside the narrow clone + [255] + $ touch outside/f3 + $ hg add outside/f3 + abort: cannot track 'outside/f3' - it is outside the narrow clone + [255] + $ rm -r outside + +Can modify dirstate inside + + $ echo modified > inside/f1 + $ touch inside/f3 + $ hg add inside/f3 + $ hg status + M inside/f1 + A inside/f3 + $ hg revert -qC . + $ rm inside/f3 + +Can commit changes inside. Leaves outside unchanged. + + $ hg update -q 'desc("initial")' + $ echo modified2 > inside/f1 + $ hg commit -m 'modify inside/f1' + created new head + $ hg files -r . + inside/f1 + outside/f1 +Some filesystems (notably FAT/exFAT only store timestamps with 2 +seconds of precision, so by sleeping for 3 seconds, we can ensure that +the timestamps of files stored by dirstate will appear older than the +dirstate file, and therefore we'll be able to get stable output from +debugdirstate. If we don't do this, the test can be slightly flaky. + $ sleep 3 + $ hg status + $ hg debugdirstate --nodates + n 644 10 set inside/f1 diff --git a/tests/test-narrow-copies.t b/tests/test-narrow-copies.t new file mode 100644 --- /dev/null +++ b/tests/test-narrow-copies.t @@ -0,0 +1,57 @@ + + $ . "$TESTDIR/narrow-library.sh" + +create full repo + + $ hg init master + $ cd master + + $ mkdir inside + $ echo inside > inside/f1 + $ mkdir outside + $ echo outside > outside/f2 + $ hg ci -Aqm 'initial' + + $ hg mv outside/f2 inside/f2 + $ hg ci -qm 'move f2 from outside' + + $ echo modified > inside/f2 + $ hg ci -qm 'modify inside/f2' + + $ cd .. + + $ hg clone --narrow ssh://user@dummy/master narrow --include inside + requesting all changes + adding changesets + adding manifests + adding file changes + added 3 changesets with 3 changes to 2 files + new changesets *:* (glob) + updating to branch default + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd narrow + + $ hg co 'desc("move f2")' + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg status + $ hg diff + $ hg diff --change . --git + diff --git a/inside/f2 b/inside/f2 + new file mode 100644 + --- /dev/null + +++ b/inside/f2 + @@ -0,0 +1,1 @@ + +outside + + $ hg log --follow inside/f2 -r tip + changeset: 2:bcfb756e0ca9 + tag: tip + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: modify inside/f2 + + changeset: 1:5a016133b2bb + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: move f2 from outside + diff --git a/tests/test-narrow-debugcommands.t b/tests/test-narrow-debugcommands.t new file mode 100644 --- /dev/null +++ b/tests/test-narrow-debugcommands.t @@ -0,0 +1,43 @@ + $ . "$TESTDIR/narrow-library.sh" + $ hg init repo + $ cd repo + $ cat << EOF > .hg/narrowspec + > [includes] + > path:foo + > [excludes] + > EOF + $ echo treemanifest >> .hg/requires + $ echo narrowhg >> .hg/requires + $ mkdir -p foo/bar + $ echo b > foo/f + $ echo c > foo/bar/f + $ hg commit -Am hi + adding foo/bar/f + adding foo/f + $ hg debugindex -m + rev offset length delta linkrev nodeid p1 p2 + 0 0 47 -1 0 14a5d056d75a 000000000000 000000000000 + $ hg debugindex --dir foo + rev offset length delta linkrev nodeid p1 p2 + 0 0 77 -1 0 e635c7857aef 000000000000 000000000000 + $ hg debugindex --dir foo/ + rev offset length delta linkrev nodeid p1 p2 + 0 0 77 -1 0 e635c7857aef 000000000000 000000000000 + $ hg debugindex --dir foo/bar + rev offset length delta linkrev nodeid p1 p2 + 0 0 44 -1 0 e091d4224761 000000000000 000000000000 + $ hg debugindex --dir foo/bar/ + rev offset length delta linkrev nodeid p1 p2 + 0 0 44 -1 0 e091d4224761 000000000000 000000000000 + $ hg debugdata -m 0 + foo\x00e635c7857aef92ac761ce5741a99da159abbbb24t (esc) + $ hg debugdata --dir foo 0 + bar\x00e091d42247613adff5d41b67f15fe7189ee97b39t (esc) + f\x001e88685f5ddec574a34c70af492f95b6debc8741 (esc) + $ hg debugdata --dir foo/ 0 + bar\x00e091d42247613adff5d41b67f15fe7189ee97b39t (esc) + f\x001e88685f5ddec574a34c70af492f95b6debc8741 (esc) + $ hg debugdata --dir foo/bar 0 + f\x00149da44f2a4e14f488b7bd4157945a9837408c00 (esc) + $ hg debugdata --dir foo/bar/ 0 + f\x00149da44f2a4e14f488b7bd4157945a9837408c00 (esc) diff --git a/tests/test-narrow-debugrebuilddirstate.t b/tests/test-narrow-debugrebuilddirstate.t new file mode 100644 --- /dev/null +++ b/tests/test-narrow-debugrebuilddirstate.t @@ -0,0 +1,31 @@ + $ . "$TESTDIR/narrow-library.sh" + $ hg init master + $ cd master + $ echo treemanifest >> .hg/requires + $ echo 'contents of file' > file + $ mkdir foo + $ echo 'contents of foo/bar' > foo/bar + $ hg ci -Am 'some change' + adding file + adding foo/bar + + $ cd .. + $ hg clone --narrow ssh://user@dummy/master copy --include=foo + requesting all changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files + new changesets * (glob) + updating to branch default + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd copy + + $ hg debugdirstate + n * 20 unset foo/bar (glob) + $ mv .hg/dirstate .hg/old_dirstate + $ dd bs=40 count=1 if=.hg/old_dirstate of=.hg/dirstate 2>/dev/null + $ hg debugdirstate + $ hg debugrebuilddirstate + $ hg debugdirstate + n * * unset foo/bar (glob) diff --git a/tests/test-narrow-exchange-merges.t b/tests/test-narrow-exchange-merges.t new file mode 100644 --- /dev/null +++ b/tests/test-narrow-exchange-merges.t @@ -0,0 +1,207 @@ + + $ . "$TESTDIR/narrow-library.sh" + +create full repo + + $ hg init master + $ cd master + $ cat >> .hg/hgrc < [narrow] + > serveellipses=True + > EOF + + $ mkdir inside + $ echo 1 > inside/f + $ hg commit -Aqm 'initial inside' + + $ mkdir outside + $ echo 1 > outside/f + $ hg commit -Aqm 'initial outside' + + $ echo 2a > outside/f + $ hg commit -Aqm 'outside 2a' + $ echo 3 > inside/f + $ hg commit -Aqm 'inside 3' + $ echo 4a > outside/f + $ hg commit -Aqm 'outside 4a' + $ hg update '.~3' + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + + $ echo 2b > outside/f + $ hg commit -Aqm 'outside 2b' + $ echo 3 > inside/f + $ hg commit -Aqm 'inside 3' + $ echo 4b > outside/f + $ hg commit -Aqm 'outside 4b' + $ hg update '.~3' + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + + $ echo 2c > outside/f + $ hg commit -Aqm 'outside 2c' + $ echo 3 > inside/f + $ hg commit -Aqm 'inside 3' + $ echo 4c > outside/f + $ hg commit -Aqm 'outside 4c' + $ hg update '.~3' + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + + $ echo 2d > outside/f + $ hg commit -Aqm 'outside 2d' + $ echo 3 > inside/f + $ hg commit -Aqm 'inside 3' + $ echo 4d > outside/f + $ hg commit -Aqm 'outside 4d' + + $ hg update -r 'desc("outside 4a")' + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg merge -r 'desc("outside 4b")' 2>&1 | egrep -v '(warning:|incomplete!)' + merging outside/f + 0 files updated, 0 files merged, 0 files removed, 1 files unresolved + use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon + $ echo 5 > outside/f + $ rm outside/f.orig + $ hg resolve --mark outside/f + (no more unresolved files) + $ hg commit -m 'merge a/b 5' + $ echo 6 > outside/f + $ hg commit -Aqm 'outside 6' + + $ hg merge -r 'desc("outside 4c")' 2>&1 | egrep -v '(warning:|incomplete!)' + merging outside/f + 0 files updated, 0 files merged, 0 files removed, 1 files unresolved + use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon + $ echo 7 > outside/f + $ rm outside/f.orig + $ hg resolve --mark outside/f + (no more unresolved files) + $ hg commit -Aqm 'merge a/b/c 7' + $ echo 8 > outside/f + $ hg commit -Aqm 'outside 8' + + $ hg merge -r 'desc("outside 4d")' 2>&1 | egrep -v '(warning:|incomplete!)' + merging outside/f + 0 files updated, 0 files merged, 0 files removed, 1 files unresolved + use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon + $ echo 9 > outside/f + $ rm outside/f.orig + $ hg resolve --mark outside/f + (no more unresolved files) + $ hg commit -Aqm 'merge a/b/c/d 9' + $ echo 10 > outside/f + $ hg commit -Aqm 'outside 10' + + $ echo 11 > inside/f + $ hg commit -Aqm 'inside 11' + $ echo 12 > outside/f + $ hg commit -Aqm 'outside 12' + + $ hg log -G -T '{rev} {node|short} {desc}\n' + @ 21 8d874d57adea outside 12 + | + o 20 7ef88b4dd4fa inside 11 + | + o 19 2a20009de83e outside 10 + | + o 18 3ac1f5779de3 merge a/b/c/d 9 + |\ + | o 17 38a9c2f7e546 outside 8 + | | + | o 16 094aa62fc898 merge a/b/c 7 + | |\ + | | o 15 f29d083d32e4 outside 6 + | | | + | | o 14 2dc11382541d merge a/b 5 + | | |\ + o | | | 13 27d07ef97221 outside 4d + | | | | + o | | | 12 465567bdfb2d inside 3 + | | | | + o | | | 11 d1c61993ec83 outside 2d + | | | | + | o | | 10 56859a8e33b9 outside 4c + | | | | + | o | | 9 bb96a08b062a inside 3 + | | | | + | o | | 8 b844052e7b3b outside 2c + |/ / / + | | o 7 9db2d8fcc2a6 outside 4b + | | | + | | o 6 6418167787a6 inside 3 + | | | + +---o 5 77344f344d83 outside 2b + | | + | o 4 9cadde08dc9f outside 4a + | | + | o 3 019ef06f125b inside 3 + | | + | o 2 75e40c075a19 outside 2a + |/ + o 1 906d6c682641 initial outside + | + o 0 9f8e82b51004 initial inside + + +Now narrow clone this and get a hopefully correct graph + + $ cd .. + $ hg clone --narrow ssh://user@dummy/master narrow --include inside + requesting all changes + adding changesets + adding manifests + adding file changes + added 14 changesets with 3 changes to 1 files + new changesets *:* (glob) + updating to branch default + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd narrow + +To make updating the tests easier, we print the emitted nodes +sorted. This makes it easier to identify when the same node structure +has been emitted, just in a different order. + + $ hg log -T '{if(ellipsis,"...")}{node|short} {p1node|short} {p2node|short} {desc}\n' | sort + ...094aa62fc898 6418167787a6 bb96a08b062a merge a/b/c 7 + ...2a20009de83e 019ef06f125b 3ac1f5779de3 outside 10 + ...3ac1f5779de3 465567bdfb2d 094aa62fc898 merge a/b/c/d 9 + ...75e40c075a19 9f8e82b51004 000000000000 outside 2a + ...77344f344d83 9f8e82b51004 000000000000 outside 2b + ...8d874d57adea 7ef88b4dd4fa 000000000000 outside 12 + ...b844052e7b3b 9f8e82b51004 000000000000 outside 2c + ...d1c61993ec83 9f8e82b51004 000000000000 outside 2d + 019ef06f125b 75e40c075a19 000000000000 inside 3 + 465567bdfb2d d1c61993ec83 000000000000 inside 3 + 6418167787a6 77344f344d83 000000000000 inside 3 + 7ef88b4dd4fa 2a20009de83e 000000000000 inside 11 + 9f8e82b51004 000000000000 000000000000 initial inside + bb96a08b062a b844052e7b3b 000000000000 inside 3 + +But seeing the graph is also nice: + $ hg log -G -T '{if(ellipsis,"...")}{node|short} {desc}\n' + @ ...8d874d57adea outside 12 + | + o 7ef88b4dd4fa inside 11 + | + o ...2a20009de83e outside 10 + |\ + | o ...3ac1f5779de3 merge a/b/c/d 9 + | |\ + | | o ...094aa62fc898 merge a/b/c 7 + | | |\ + | o | | 465567bdfb2d inside 3 + | | | | + | o | | ...d1c61993ec83 outside 2d + | | | | + | | | o bb96a08b062a inside 3 + | | | | + | +---o ...b844052e7b3b outside 2c + | | | + | | o 6418167787a6 inside 3 + | | | + | | o ...77344f344d83 outside 2b + | |/ + o | 019ef06f125b inside 3 + | | + o | ...75e40c075a19 outside 2a + |/ + o 9f8e82b51004 initial inside + diff --git a/tests/test-narrow-exchange.t b/tests/test-narrow-exchange.t new file mode 100644 --- /dev/null +++ b/tests/test-narrow-exchange.t @@ -0,0 +1,210 @@ + + $ . "$TESTDIR/narrow-library.sh" + +create full repo + + $ hg init master + $ cd master + $ cat >> .hg/hgrc < [narrow] + > serveellipses=True + > EOF + + $ mkdir inside + $ echo 1 > inside/f + $ mkdir inside2 + $ echo 1 > inside2/f + $ mkdir outside + $ echo 1 > outside/f + $ hg ci -Aqm 'initial' + + $ echo 2 > inside/f + $ hg ci -qm 'inside 2' + + $ echo 2 > inside2/f + $ hg ci -qm 'inside2 2' + + $ echo 2 > outside/f + $ hg ci -qm 'outside 2' + + $ cd .. + + $ hg clone --narrow ssh://user@dummy/master narrow --include inside + requesting all changes + adding changesets + adding manifests + adding file changes + added 3 changesets with 2 changes to 1 files + new changesets *:* (glob) + updating to branch default + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + + $ hg clone --narrow ssh://user@dummy/master narrow2 --include inside --include inside2 + requesting all changes + adding changesets + adding manifests + adding file changes + added 4 changesets with 4 changes to 2 files + new changesets *:* (glob) + updating to branch default + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + +Can push to wider repo if change does not affect paths in wider repo that are +not also in narrower repo + + $ cd narrow + $ echo 3 > inside/f + $ hg ci -m 'inside 3' + $ hg push ssh://user@dummy/narrow2 + pushing to ssh://user@dummy/narrow2 + searching for changes + remote: adding changesets + remote: adding manifests + remote: adding file changes + remote: added 1 changesets with 1 changes to 1 files + +Can push to narrower repo if change affects only paths within remote's +narrow spec + + $ cd ../narrow2 + $ cat >> .hg/hgrc < [narrow] + > serveellipses=True + > EOF + $ hg co -r 'desc("inside 3")' + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ echo 4 > inside/f + $ hg ci -m 'inside 4' + $ hg push ssh://user@dummy/narrow + pushing to ssh://user@dummy/narrow + searching for changes + remote: adding changesets + remote: adding manifests + remote: adding file changes + remote: added 1 changesets with 1 changes to 1 files + +Can push to narrow repo if change affects only paths outside remote's +narrow spec + + $ echo 3 > inside2/f + $ hg ci -m 'inside2 3' +TODO: this should be successful + $ hg push ssh://user@dummy/narrow + pushing to ssh://user@dummy/narrow + searching for changes + remote: adding changesets + remote: adding manifests + remote: adding file changes + remote: transaction abort! + remote: rollback completed + remote: abort: data/inside2/f.i@4a1aa07735e6: unknown parent! + abort: stream ended unexpectedly (got 0 bytes, expected 4) + [255] + +Can pull from wider repo if change affects only paths outside remote's +narrow spec + $ echo 4 > inside2/f + $ hg ci -m 'inside2 4' + $ hg log -G -T '{rev} {node|short} {files}\n' + @ 7 d78a96df731d inside2/f + | + o 6 8c26f5218962 inside2/f + | + o 5 ba3480e2f9de inside/f + | + o 4 4e5edd526618 inside/f + | + o 3 81e7e07b7ab0 outside/f + | + o 2 f3993b8c0c2b inside2/f + | + o 1 8cd66ca966b4 inside/f + | + o 0 c8057d6f53ab inside/f inside2/f outside/f + + $ cd ../narrow + $ hg log -G -T '{rev} {node|short} {files}\n' + o 4 ba3480e2f9de inside/f + | + @ 3 4e5edd526618 inside/f + | + o 2 81e7e07b7ab0 outside/f + | + o 1 8cd66ca966b4 inside/f + | + o 0 c8057d6f53ab inside/f inside2/f outside/f + + $ hg pull ssh://user@dummy/narrow2 + pulling from ssh://user@dummy/narrow2 + searching for changes + remote: abort: unable to resolve parent while packing 'data/inside2/f.i' 3 for changeset 5 (?) + adding changesets + remote: abort: unexpected error: unable to resolve parent while packing 'data/inside2/f.i' 3 for changeset 5 + transaction abort! + rollback completed + abort: pull failed on remote + [255] + +Check that the resulting history is valid in the full repo + + $ cd ../narrow2 + $ hg push ssh://user@dummy/master + pushing to ssh://user@dummy/master + searching for changes + remote: adding changesets + remote: adding manifests + remote: adding file changes + remote: added 4 changesets with 4 changes to 2 files + $ cd ../master + $ hg verify + checking changesets + checking manifests + crosschecking files in changesets and manifests + checking files + 3 files, 8 changesets, 10 total revisions + +Can not push to wider repo if change affects paths in wider repo that are +not also in narrower repo + $ cd ../master + $ hg co -r 'desc("inside2 4")' + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ echo 5 > inside2/f + $ hg ci -m 'inside2 5' + $ hg log -G -T '{rev} {node|short} {files}\n' + @ 8 5970befb64ba inside2/f + | + o 7 d78a96df731d inside2/f + | + o 6 8c26f5218962 inside2/f + | + o 5 ba3480e2f9de inside/f + | + o 4 4e5edd526618 inside/f + | + o 3 81e7e07b7ab0 outside/f + | + o 2 f3993b8c0c2b inside2/f + | + o 1 8cd66ca966b4 inside/f + | + o 0 c8057d6f53ab inside/f inside2/f outside/f + + $ cd ../narrow + $ hg pull + pulling from ssh://user@dummy/master + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 0 changes to 0 files + new changesets * (glob) + (run 'hg update' to get a working copy) +TODO: this should tell the user that their narrow clone does not have the +necessary content to be able to push to the target + $ hg push ssh://user@dummy/narrow2 + pushing to ssh://user@dummy/narrow2 + searching for changes + remote has heads on branch 'default' that are not known locally: d78a96df731d + abort: push creates new remote head 5970befb64ba! + (pull and merge or see 'hg help push' for details about pushing new heads) + [255] diff --git a/tests/test-narrow-expanddirstate.t b/tests/test-narrow-expanddirstate.t new file mode 100644 --- /dev/null +++ b/tests/test-narrow-expanddirstate.t @@ -0,0 +1,170 @@ + $ . "$TESTDIR/narrow-library.sh" + + $ hg init master + $ cd master + + $ mkdir inside + $ echo inside > inside/f1 + $ mkdir outside + $ echo outside > outside/f2 + $ mkdir patchdir + $ echo patch_this > patchdir/f3 + $ hg ci -Aqm 'initial' + + $ cd .. + + $ hg clone --narrow ssh://user@dummy/master narrow --include inside + requesting all changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files + new changesets dff6a2a6d433 + updating to branch default + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + + $ cd narrow + + $ mkdir outside + $ echo other_contents > outside/f2 + $ grep outside .hg/narrowspec + [1] + $ grep outside .hg/dirstate + [1] + $ hg status + +`hg status` did not add outside. + $ grep outside .hg/narrowspec + [1] + $ grep outside .hg/dirstate + [1] + +Unfortunately this is not really a candidate for adding to narrowhg proper, +since it depends on some other source for providing the manifests (when using +treemanifests) and file contents. Something like a virtual filesystem and/or +remotefilelog. We want to be useful when not using those systems, so we do not +have this method available in narrowhg proper at the moment. + $ cat > "$TESTTMP/expand_extension.py" < import os + > import sys + > + > from mercurial import extensions + > from mercurial import localrepo + > from mercurial import match as matchmod + > from mercurial import patch + > from mercurial import util as hgutil + > + > def expandnarrowspec(ui, repo, newincludes=None): + > if not newincludes: + > return + > import sys + > newincludes = set([newincludes]) + > narrowhg = extensions.find('narrow') + > includes, excludes = repo.narrowpats + > currentmatcher = narrowhg.narrowspec.match(repo.root, includes, excludes) + > includes = includes | newincludes + > if not repo.currenttransaction(): + > ui.develwarn('expandnarrowspec called outside of transaction!') + > repo.setnarrowpats(includes, excludes) + > newmatcher = narrowhg.narrowspec.match(repo.root, includes, excludes) + > added = matchmod.differencematcher(newmatcher, currentmatcher) + > for f in repo['.'].manifest().walk(added): + > repo.dirstate.normallookup(f) + > + > def makeds(ui, repo): + > def wrapds(orig, self): + > ds = orig(self) + > class expandingdirstate(ds.__class__): + > # Mercurial 4.4 uses this version. + > @hgutil.propertycache + > def _map(self): + > ret = super(expandingdirstate, self)._map + > with repo.wlock(), repo.lock(), repo.transaction( + > 'expandnarrowspec'): + > expandnarrowspec(ui, repo, os.environ.get('DIRSTATEINCLUDES')) + > return ret + > # Mercurial 4.3.3 and earlier uses this version. It seems that + > # narrowhg does not currently support this version, but we include + > # it just in case backwards compatibility is restored. + > def _read(self): + > ret = super(expandingdirstate, self)._read() + > with repo.wlock(), repo.lock(), repo.transaction( + > 'expandnarrowspec'): + > expandnarrowspec(ui, repo, os.environ.get('DIRSTATEINCLUDES')) + > return ret + > ds.__class__ = expandingdirstate + > return ds + > return wrapds + > + > def reposetup(ui, repo): + > extensions.wrapfilecache(localrepo.localrepository, 'dirstate', + > makeds(ui, repo)) + > def overridepatch(orig, *args, **kwargs): + > with repo.wlock(): + > expandnarrowspec(ui, repo, os.environ.get('PATCHINCLUDES')) + > return orig(*args, **kwargs) + > + > extensions.wrapfunction(patch, 'patch', overridepatch) + > EOF + $ cat >> ".hg/hgrc" < [extensions] + > expand_extension = $TESTTMP/expand_extension.py + > EOF + +Since we do not have the ability to rely on a virtual filesystem or +remotefilelog in the test, we just fake it by copying the data from the 'master' +repo. + $ cp -a ../master/.hg/store/data/* .hg/store/data +Do that for patchdir as well. + $ cp -a ../master/patchdir . + +`hg status` will now add outside, but not patchdir. + $ DIRSTATEINCLUDES=path:outside hg status + M outside/f2 + $ grep outside .hg/narrowspec + path:outside + $ grep outside .hg/dirstate > /dev/null + $ grep patchdir .hg/narrowspec + [1] + $ grep patchdir .hg/dirstate + [1] + +Get rid of the modification to outside/f2. + $ hg update -C . + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + +This patch will not apply cleanly at the moment, so `hg import` will break + $ cat > "$TESTTMP/foo.patch" < --- patchdir/f3 + > +++ patchdir/f3 + > @@ -1,1 +1,1 @@ + > -this should be "patch_this", but its not, so patch fails + > +this text is irrelevant + > EOF + $ PATCHINCLUDES=path:patchdir hg import -p0 -e "$TESTTMP/foo.patch" -m ignored + applying $TESTTMP/foo.patch + patching file patchdir/f3 + Hunk #1 FAILED at 0 + 1 out of 1 hunks FAILED -- saving rejects to file patchdir/f3.rej + abort: patch failed to apply + [255] + $ grep patchdir .hg/narrowspec + [1] + $ grep patchdir .hg/dirstate > /dev/null + [1] + +Let's make it apply cleanly and see that it *did* expand properly + $ cat > "$TESTTMP/foo.patch" < --- patchdir/f3 + > +++ patchdir/f3 + > @@ -1,1 +1,1 @@ + > -patch_this + > +patched_this + > EOF + $ PATCHINCLUDES=path:patchdir hg import -p0 -e "$TESTTMP/foo.patch" -m message + applying $TESTTMP/foo.patch + $ cat patchdir/f3 + patched_this + $ grep patchdir .hg/narrowspec + path:patchdir + $ grep patchdir .hg/dirstate > /dev/null diff --git a/tests/test-narrow-merge-tree.t b/tests/test-narrow-merge-tree.t new file mode 100644 --- /dev/null +++ b/tests/test-narrow-merge-tree.t @@ -0,0 +1,28 @@ + $ cd $TESTDIR && python $RUNTESTDIR/run-tests.py \ + > --extra-config-opt experimental.treemanifest=1 test-narrow-merge.t 2>&1 | \ + > grep -v 'unexpected mercurial lib' | egrep -v '\(expected' + + --- */tests/test-narrow-merge.t (glob) + +++ */tests/test-narrow-merge.t.err (glob) + @@ -\d+,\d+ \+\d+,\d+ @@ (re) + + $ hg update -q 'desc("modify inside/f1")' + $ hg merge 'desc("modify outside/f1")' + - abort: merge affects file 'outside/f1' outside narrow, which is not yet supported + + abort: merge affects file 'outside/' outside narrow, which is not yet supported + (merging in the other direction may work) + [255] + + @@ -\d+,\d+ \+\d+,\d+ @@ (re) + + $ hg update -q 'desc("modify outside/f1")' + $ hg merge 'desc("conflicting outside/f1")' + - abort: conflict in file 'outside/f1' is outside narrow clone + + abort: conflict in file 'outside/' is outside narrow clone + [255] + + ERROR: test-narrow-merge.t output changed + ! + Failed test-narrow-merge.t: output changed + # Ran 1 tests, 0 skipped, 1 failed. + python hash seed: * (glob) diff --git a/tests/test-narrow-merge.t b/tests/test-narrow-merge.t new file mode 100644 --- /dev/null +++ b/tests/test-narrow-merge.t @@ -0,0 +1,94 @@ + + $ . "$TESTDIR/narrow-library.sh" + +create full repo + + $ hg init master + $ cd master + $ cat >> .hg/hgrc < [narrow] + > serveellipses=True + > EOF + + $ mkdir inside + $ echo inside1 > inside/f1 + $ echo inside2 > inside/f2 + $ mkdir outside + $ echo outside1 > outside/f1 + $ echo outside2 > outside/f2 + $ hg ci -Aqm 'initial' + + $ echo modified > inside/f1 + $ hg ci -qm 'modify inside/f1' + + $ hg update -q 0 + $ echo modified > inside/f2 + $ hg ci -qm 'modify inside/f2' + + $ hg update -q 0 + $ echo modified2 > inside/f1 + $ hg ci -qm 'conflicting inside/f1' + + $ hg update -q 0 + $ echo modified > outside/f1 + $ hg ci -qm 'modify outside/f1' + + $ hg update -q 0 + $ echo modified2 > outside/f1 + $ hg ci -qm 'conflicting outside/f1' + + $ cd .. + + $ hg clone --narrow ssh://user@dummy/master narrow --include inside + requesting all changes + adding changesets + adding manifests + adding file changes + added 6 changesets with 5 changes to 2 files (+4 heads) + new changesets *:* (glob) + updating to branch default + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd narrow + + $ hg update -q 0 + +Can merge in when no files outside narrow spec are involved + + $ hg update -q 'desc("modify inside/f1")' + $ hg merge 'desc("modify inside/f2")' + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + (branch merge, don't forget to commit) + $ hg commit -m 'merge inside changes' + +Can merge conflicting changes inside narrow spec + + $ hg update -q 'desc("modify inside/f1")' + $ hg merge 'desc("conflicting inside/f1")' 2>&1 | egrep -v '(warning:|incomplete!)' + merging inside/f1 + 0 files updated, 0 files merged, 0 files removed, 1 files unresolved + use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon + $ echo modified3 > inside/f1 + $ hg resolve -m + (no more unresolved files) + $ hg commit -m 'merge inside/f1' + +TODO: Can merge non-conflicting changes outside narrow spec + + $ hg update -q 'desc("modify inside/f1")' + $ hg merge 'desc("modify outside/f1")' + abort: merge affects file 'outside/f1' outside narrow, which is not yet supported + (merging in the other direction may work) + [255] + + $ hg update -q 'desc("modify outside/f1")' + $ hg merge 'desc("modify inside/f1")' + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + (branch merge, don't forget to commit) + $ hg ci -m 'merge from inside to outside' + +Refuses merge of conflicting outside changes + + $ hg update -q 'desc("modify outside/f1")' + $ hg merge 'desc("conflicting outside/f1")' + abort: conflict in file 'outside/f1' is outside narrow clone + [255] diff --git a/tests/test-narrow-patch-tree.t b/tests/test-narrow-patch-tree.t new file mode 100644 --- /dev/null +++ b/tests/test-narrow-patch-tree.t @@ -0,0 +1,5 @@ + $ cd $TESTDIR && python $RUNTESTDIR/run-tests.py \ + > --extra-config-opt experimental.treemanifest=1 test-patch.t 2>&1 | \ + > grep -v 'unexpected mercurial lib' | egrep -v '\(expected' + . + # Ran 1 tests, 0 skipped, 0 failed. diff --git a/tests/test-narrow-patch.t b/tests/test-narrow-patch.t new file mode 100644 --- /dev/null +++ b/tests/test-narrow-patch.t @@ -0,0 +1,76 @@ + + $ . "$TESTDIR/narrow-library.sh" + +create full repo + + $ hg init master + $ cd master + + $ mkdir inside + $ echo inside > inside/f1 + $ mkdir outside + $ echo outside > outside/f1 + $ hg ci -Aqm 'initial' + + $ echo modified > inside/f1 + $ hg ci -qm 'modify inside' + + $ echo modified > outside/f1 + $ hg ci -qm 'modify outside' + + $ cd .. + + $ hg clone --narrow ssh://user@dummy/master narrow --include inside + requesting all changes + adding changesets + adding manifests + adding file changes + added 3 changesets with 2 changes to 1 files + new changesets *:* (glob) + updating to branch default + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd narrow + +Can show patch touching paths outside + + $ hg log -p + changeset: 2:* (glob) + tag: tip + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: modify outside + + + changeset: 1:* (glob) + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: modify inside + + diff -r * -r * inside/f1 (glob) + --- a/inside/f1 Thu Jan 01 00:00:00 1970 +0000 + +++ b/inside/f1 Thu Jan 01 00:00:00 1970 +0000 + @@ -1,1 +1,1 @@ + -inside + +modified + + changeset: 0:* (glob) + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: initial + + diff -r 000000000000 -r * inside/f1 (glob) + --- /dev/null Thu Jan 01 00:00:00 1970 +0000 + +++ b/inside/f1 Thu Jan 01 00:00:00 1970 +0000 + @@ -0,0 +1,1 @@ + +inside + + + $ hg status --rev 1 --rev 2 + +Can show copies inside the narrow clone + + $ hg cp inside/f1 inside/f2 + $ hg diff --git + diff --git a/inside/f1 b/inside/f2 + copy from inside/f1 + copy to inside/f2 diff --git a/tests/test-narrow-patterns.t b/tests/test-narrow-patterns.t new file mode 100644 --- /dev/null +++ b/tests/test-narrow-patterns.t @@ -0,0 +1,418 @@ + $ . "$TESTDIR/narrow-library.sh" + +initialize nested directories to validate complex include/exclude patterns + + $ hg init master + $ cd master + $ cat >> .hg/hgrc < [narrow] + > serveellipses=True + > EOF + + $ echo root > root + $ hg add root + $ hg commit -m 'add root' + + $ for d in dir1 dir2 dir1/dirA dir1/dirB dir2/dirA dir2/dirB + > do + > mkdir -p $d + > echo $d/foo > $d/foo + > hg add $d/foo + > hg commit -m "add $d/foo" + > echo $d/bar > $d/bar + > hg add $d/bar + > hg commit -m "add $d/bar" + > done + $ chmod +x dir1/dirA/foo + $ hg commit -m "make dir1/dirA/foo executable" + $ hg log -G -T '{rev} {node|short} {files}\n' + @ 13 c87ca422d521 dir1/dirA/foo + | + o 12 951b8a83924e dir2/dirB/bar + | + o 11 01ae5a51b563 dir2/dirB/foo + | + o 10 5eababdf0ac5 dir2/dirA/bar + | + o 9 99d690663739 dir2/dirA/foo + | + o 8 8e80155d5445 dir1/dirB/bar + | + o 7 406760310428 dir1/dirB/foo + | + o 6 623466a5f475 dir1/dirA/bar + | + o 5 06ff3a5be997 dir1/dirA/foo + | + o 4 33227af02764 dir2/bar + | + o 3 5e1f9d8d7c69 dir2/foo + | + o 2 594bc4b13d4a dir1/bar + | + o 1 47f480a08324 dir1/foo + | + o 0 2a4f0c3b67da root + + $ cd .. + +clone a narrow portion of the master, such that we can widen it later + + $ hg clone --narrow ssh://user@dummy/master narrow \ + > --include dir1 \ + > --include dir2 \ + > --exclude dir1/dirA \ + > --exclude dir1/dirB \ + > --exclude dir2/dirA \ + > --exclude dir2/dirB + requesting all changes + adding changesets + adding manifests + adding file changes + added 6 changesets with 4 changes to 4 files + new changesets *:* (glob) + updating to branch default + 4 files updated, 0 files merged, 0 files removed, 0 files unresolved + + $ cd narrow + $ cat .hg/narrowspec + [includes] + path:dir1 + path:dir2 + [excludes] + path:dir1/dirA + path:dir1/dirB + path:dir2/dirA + path:dir2/dirB + $ hg manifest -r tip + dir1/bar + dir1/dirA/bar + dir1/dirA/foo + dir1/dirB/bar + dir1/dirB/foo + dir1/foo + dir2/bar + dir2/dirA/bar + dir2/dirA/foo + dir2/dirB/bar + dir2/dirB/foo + dir2/foo + root + $ find * | sort + dir1 + dir1/bar + dir1/foo + dir2 + dir2/bar + dir2/foo + $ hg log -G -T '{rev} {node|short}{if(ellipsis, "...")} {files}\n' + @ 5 c87ca422d521... dir1/dirA/foo + | + o 4 33227af02764 dir2/bar + | + o 3 5e1f9d8d7c69 dir2/foo + | + o 2 594bc4b13d4a dir1/bar + | + o 1 47f480a08324 dir1/foo + | + o 0 2a4f0c3b67da... root + + +widen the narrow checkout + + $ hg tracked --removeexclude dir1/dirA + comparing with ssh://user@dummy/master + searching for changes + no changes found + saved backup bundle to $TESTTMP/narrow/.hg/strip-backup/*-widen.hg (glob) + adding changesets + adding manifests + adding file changes + added 9 changesets with 6 changes to 6 files + new changesets *:* (glob) + $ cat .hg/narrowspec + [includes] + path:dir1 + path:dir2 + [excludes] + path:dir1/dirB + path:dir2/dirA + path:dir2/dirB + $ find * | sort + dir1 + dir1/bar + dir1/dirA + dir1/dirA/bar + dir1/dirA/foo + dir1/foo + dir2 + dir2/bar + dir2/foo + $ test -x dir1/dirA/foo && echo executable + executable + $ test -x dir1/dirA/bar || echo not executable + not executable + $ hg log -G -T '{rev} {node|short}{if(ellipsis, "...")} {files}\n' + @ 8 c87ca422d521 dir1/dirA/foo + | + o 7 951b8a83924e... dir2/dirB/bar + | + o 6 623466a5f475 dir1/dirA/bar + | + o 5 06ff3a5be997 dir1/dirA/foo + | + o 4 33227af02764 dir2/bar + | + o 3 5e1f9d8d7c69 dir2/foo + | + o 2 594bc4b13d4a dir1/bar + | + o 1 47f480a08324 dir1/foo + | + o 0 2a4f0c3b67da... root + + +widen narrow spec again, but exclude a file in previously included spec + + $ hg tracked --removeexclude dir2/dirB --addexclude dir1/dirA/bar + comparing with ssh://user@dummy/master + searching for changes + looking for local changes to affected paths + deleting data/dir1/dirA/bar.i + no changes found + saved backup bundle to $TESTTMP/narrow/.hg/strip-backup/*-widen.hg (glob) + adding changesets + adding manifests + adding file changes + added 11 changesets with 7 changes to 7 files + new changesets *:* (glob) + $ cat .hg/narrowspec + [includes] + path:dir1 + path:dir2 + [excludes] + path:dir1/dirA/bar + path:dir1/dirB + path:dir2/dirA + $ find * | sort + dir1 + dir1/bar + dir1/dirA + dir1/dirA/foo + dir1/foo + dir2 + dir2/bar + dir2/dirB + dir2/dirB/bar + dir2/dirB/foo + dir2/foo + $ hg log -G -T '{rev} {node|short}{if(ellipsis, "...")} {files}\n' + @ 10 c87ca422d521 dir1/dirA/foo + | + o 9 951b8a83924e dir2/dirB/bar + | + o 8 01ae5a51b563 dir2/dirB/foo + | + o 7 5eababdf0ac5... dir2/dirA/bar + | + o 6 623466a5f475... dir1/dirA/bar + | + o 5 06ff3a5be997 dir1/dirA/foo + | + o 4 33227af02764 dir2/bar + | + o 3 5e1f9d8d7c69 dir2/foo + | + o 2 594bc4b13d4a dir1/bar + | + o 1 47f480a08324 dir1/foo + | + o 0 2a4f0c3b67da... root + + +widen narrow spec yet again, excluding a directory in previous spec + + $ hg tracked --removeexclude dir2/dirA --addexclude dir1/dirA + comparing with ssh://user@dummy/master + searching for changes + looking for local changes to affected paths + deleting data/dir1/dirA/foo.i + no changes found + saved backup bundle to $TESTTMP/narrow/.hg/strip-backup/*-widen.hg (glob) + adding changesets + adding manifests + adding file changes + added 13 changesets with 8 changes to 8 files + new changesets *:* (glob) + $ cat .hg/narrowspec + [includes] + path:dir1 + path:dir2 + [excludes] + path:dir1/dirA + path:dir1/dirA/bar + path:dir1/dirB + $ find * | sort + dir1 + dir1/bar + dir1/foo + dir2 + dir2/bar + dir2/dirA + dir2/dirA/bar + dir2/dirA/foo + dir2/dirB + dir2/dirB/bar + dir2/dirB/foo + dir2/foo + $ hg log -G -T '{rev} {node|short}{if(ellipsis, "...")} {files}\n' + @ 12 c87ca422d521... dir1/dirA/foo + | + o 11 951b8a83924e dir2/dirB/bar + | + o 10 01ae5a51b563 dir2/dirB/foo + | + o 9 5eababdf0ac5 dir2/dirA/bar + | + o 8 99d690663739 dir2/dirA/foo + | + o 7 8e80155d5445... dir1/dirB/bar + | + o 6 623466a5f475... dir1/dirA/bar + | + o 5 06ff3a5be997... dir1/dirA/foo + | + o 4 33227af02764 dir2/bar + | + o 3 5e1f9d8d7c69 dir2/foo + | + o 2 594bc4b13d4a dir1/bar + | + o 1 47f480a08324 dir1/foo + | + o 0 2a4f0c3b67da... root + + +include a directory that was previously explicitly excluded + + $ hg tracked --removeexclude dir1/dirA + comparing with ssh://user@dummy/master + searching for changes + no changes found + saved backup bundle to $TESTTMP/narrow/.hg/strip-backup/*-widen.hg (glob) + adding changesets + adding manifests + adding file changes + added 13 changesets with 9 changes to 9 files + new changesets *:* (glob) + $ cat .hg/narrowspec + [includes] + path:dir1 + path:dir2 + [excludes] + path:dir1/dirA/bar + path:dir1/dirB + $ find * | sort + dir1 + dir1/bar + dir1/dirA + dir1/dirA/foo + dir1/foo + dir2 + dir2/bar + dir2/dirA + dir2/dirA/bar + dir2/dirA/foo + dir2/dirB + dir2/dirB/bar + dir2/dirB/foo + dir2/foo + $ hg log -G -T '{rev} {node|short}{if(ellipsis, "...")} {files}\n' + @ 12 c87ca422d521 dir1/dirA/foo + | + o 11 951b8a83924e dir2/dirB/bar + | + o 10 01ae5a51b563 dir2/dirB/foo + | + o 9 5eababdf0ac5 dir2/dirA/bar + | + o 8 99d690663739 dir2/dirA/foo + | + o 7 8e80155d5445... dir1/dirB/bar + | + o 6 623466a5f475... dir1/dirA/bar + | + o 5 06ff3a5be997 dir1/dirA/foo + | + o 4 33227af02764 dir2/bar + | + o 3 5e1f9d8d7c69 dir2/foo + | + o 2 594bc4b13d4a dir1/bar + | + o 1 47f480a08324 dir1/foo + | + o 0 2a4f0c3b67da... root + + + $ cd .. + +clone a narrow portion of the master, such that we can widen it later + + $ hg clone --narrow ssh://user@dummy/master narrow2 --include dir1/dirA + requesting all changes + adding changesets + adding manifests + adding file changes + added 5 changesets with 2 changes to 2 files + new changesets *:* (glob) + updating to branch default + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd narrow2 + $ find * | sort + dir1 + dir1/dirA + dir1/dirA/bar + dir1/dirA/foo + $ hg tracked --addinclude dir1 + comparing with ssh://user@dummy/master + searching for changes + no changes found + saved backup bundle to $TESTTMP/narrow2/.hg/strip-backup/*-widen.hg (glob) + adding changesets + adding manifests + adding file changes + added 10 changesets with 6 changes to 6 files + new changesets *:* (glob) + $ find * | sort + dir1 + dir1/bar + dir1/dirA + dir1/dirA/bar + dir1/dirA/foo + dir1/dirB + dir1/dirB/bar + dir1/dirB/foo + dir1/foo + $ hg log -G -T '{rev} {node|short}{if(ellipsis, "...")} {files}\n' + @ 9 c87ca422d521 dir1/dirA/foo + | + o 8 951b8a83924e... dir2/dirB/bar + | + o 7 8e80155d5445 dir1/dirB/bar + | + o 6 406760310428 dir1/dirB/foo + | + o 5 623466a5f475 dir1/dirA/bar + | + o 4 06ff3a5be997 dir1/dirA/foo + | + o 3 33227af02764... dir2/bar + | + o 2 594bc4b13d4a dir1/bar + | + o 1 47f480a08324 dir1/foo + | + o 0 2a4f0c3b67da... root + diff --git a/tests/test-narrow-pull.t b/tests/test-narrow-pull.t new file mode 100644 --- /dev/null +++ b/tests/test-narrow-pull.t @@ -0,0 +1,175 @@ + $ . "$TESTDIR/narrow-library.sh" + + $ hg init master + $ cd master + $ cat >> .hg/hgrc < [narrow] + > serveellipses=True + > EOF + $ for x in `$TESTDIR/seq.py 10` + > do + > echo $x > "f$x" + > hg add "f$x" + > hg commit -m "Commit f$x" + > done + $ cd .. + +narrow clone a couple files, f2 and f8 + + $ hg clone --narrow ssh://user@dummy/master narrow --include "f2" --include "f8" + requesting all changes + adding changesets + adding manifests + adding file changes + added 5 changesets with 2 changes to 2 files + new changesets *:* (glob) + updating to branch default + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd narrow + $ ls + f2 + f8 + $ cat f2 f8 + 2 + 8 + + $ cd .. + +change every upstream file twice + + $ cd master + $ for x in `$TESTDIR/seq.py 10` + > do + > echo "update#1 $x" >> "f$x" + > hg commit -m "Update#1 to f$x" "f$x" + > done + $ for x in `$TESTDIR/seq.py 10` + > do + > echo "update#2 $x" >> "f$x" + > hg commit -m "Update#2 to f$x" "f$x" + > done + $ cd .. + +look for incoming changes + + $ cd narrow + $ hg incoming --limit 3 + comparing with ssh://user@dummy/master + searching for changes + changeset: 5:ddc055582556 + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: Update#1 to f1 + + changeset: 6:f66eb5ad621d + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: Update#1 to f2 + + changeset: 7:c42ecff04e99 + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: Update#1 to f3 + + +Interrupting the pull is safe + $ hg --config hooks.pretxnchangegroup.bad=false pull -q + transaction abort! + rollback completed + abort: pretxnchangegroup.bad hook exited with status 1 + [255] + $ hg id + 223311e70a6f tip + +pull new changes down to the narrow clone. Should get 8 new changesets: 4 +relevant to the narrow spec, and 4 ellipsis nodes gluing them all together. + + $ hg pull + pulling from ssh://user@dummy/master + searching for changes + adding changesets + adding manifests + adding file changes + added 9 changesets with 4 changes to 2 files + new changesets *:* (glob) + (run 'hg update' to get a working copy) + $ hg log -T '{rev}: {desc}\n' + 13: Update#2 to f10 + 12: Update#2 to f8 + 11: Update#2 to f7 + 10: Update#2 to f2 + 9: Update#2 to f1 + 8: Update#1 to f8 + 7: Update#1 to f7 + 6: Update#1 to f2 + 5: Update#1 to f1 + 4: Commit f10 + 3: Commit f8 + 2: Commit f7 + 1: Commit f2 + 0: Commit f1 + $ hg update tip + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + +add a change and push it + + $ echo "update#3 2" >> f2 + $ hg commit -m "Update#3 to f2" f2 + $ hg log f2 -T '{rev}: {desc}\n' + 14: Update#3 to f2 + 10: Update#2 to f2 + 6: Update#1 to f2 + 1: Commit f2 + $ hg push + pushing to ssh://user@dummy/master + searching for changes + remote: adding changesets + remote: adding manifests + remote: adding file changes + remote: added 1 changesets with 1 changes to 1 files + $ cd .. + + $ cd master + $ hg log f2 -T '{rev}: {desc}\n' + 30: Update#3 to f2 + 21: Update#2 to f2 + 11: Update#1 to f2 + 1: Commit f2 + $ hg log -l 3 -T '{rev}: {desc}\n' + 30: Update#3 to f2 + 29: Update#2 to f10 + 28: Update#2 to f9 + +Can pull into repo with a single commit + + $ cd .. + $ hg clone -q --narrow ssh://user@dummy/master narrow2 --include "f1" -r 0 + $ cd narrow2 + $ hg pull -q -r 1 + transaction abort! + rollback completed + abort: pull failed on remote + [255] + +Can use 'hg share': + $ cat >> $HGRCPATH < [extensions] + > share= + > EOF + + $ cd .. + $ hg share narrow2 narrow2-share + updating working directory + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd narrow2-share + $ hg status + +We should also be able to unshare without breaking everything: + $ hg unshare + devel-warn: write with no wlock: "narrowspec" at: */hgext/narrow/narrowrepo.py:41 (unsharenarrowspec) (glob) + $ hg verify + checking changesets + checking manifests + crosschecking files in changesets and manifests + checking files + 1 files, 1 changesets, 1 total revisions diff --git a/tests/test-narrow-rebase.t b/tests/test-narrow-rebase.t new file mode 100644 --- /dev/null +++ b/tests/test-narrow-rebase.t @@ -0,0 +1,93 @@ + + $ . "$TESTDIR/narrow-library.sh" + +create full repo + + $ hg init master + $ cd master + + $ mkdir inside + $ echo inside1 > inside/f1 + $ echo inside2 > inside/f2 + $ mkdir outside + $ echo outside1 > outside/f1 + $ echo outside2 > outside/f2 + $ hg ci -Aqm 'initial' + + $ echo modified > inside/f1 + $ hg ci -qm 'modify inside/f1' + + $ hg update -q 0 + $ echo modified2 > inside/f2 + $ hg ci -qm 'modify inside/f2' + + $ hg update -q 0 + $ echo modified > outside/f1 + $ hg ci -qm 'modify outside/f1' + + $ hg update -q 0 + $ echo modified2 > outside/f1 + $ hg ci -qm 'conflicting outside/f1' + + $ cd .. + + $ hg clone --narrow ssh://user@dummy/master narrow --include inside + requesting all changes + adding changesets + adding manifests + adding file changes + added 5 changesets with 4 changes to 2 files (+3 heads) + new changesets *:* (glob) + updating to branch default + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd narrow + $ cat >> $HGRCPATH < [extensions] + > rebase= + > EOF + + $ hg update -q 0 + +Can rebase onto commit where no files outside narrow spec are involved + + $ hg update -q 0 + $ echo modified > inside/f2 + $ hg ci -qm 'modify inside/f2' + $ hg rebase -d 'desc("modify inside/f1")' + rebasing 5:c2f36d04e05d "modify inside/f2" (tip) + saved backup bundle to $TESTTMP/narrow/.hg/strip-backup/*-rebase.hg (glob) + +Can rebase onto conflicting changes inside narrow spec + + $ hg update -q 0 + $ echo conflicting > inside/f1 + $ hg ci -qm 'conflicting inside/f1' + $ hg rebase -d 'desc("modify inside/f1")' 2>&1 | egrep -v '(warning:|incomplete!)' + rebasing 6:cdce97fbf653 "conflicting inside/f1" (tip) + merging inside/f1 + unresolved conflicts (see hg resolve, then hg rebase --continue) + $ echo modified3 > inside/f1 + $ hg resolve -m 2>&1 | grep -v continue: + (no more unresolved files) + $ hg rebase --continue + rebasing 6:cdce97fbf653 "conflicting inside/f1" (tip) + saved backup bundle to $TESTTMP/narrow/.hg/strip-backup/*-rebase.hg (glob) + +Can rebase onto non-conflicting changes outside narrow spec + + $ hg update -q 0 + $ echo modified > inside/f2 + $ hg ci -qm 'modify inside/f2' + $ hg rebase -d 'desc("modify outside/f1")' + rebasing 7:c2f36d04e05d "modify inside/f2" (tip) + saved backup bundle to $TESTTMP/narrow/.hg/strip-backup/*-rebase.hg (glob) + +Rebase interrupts on conflicting changes outside narrow spec + + $ hg update -q 'desc("conflicting outside/f1")' + $ hg phase -f -d . + no phases changed + $ hg rebase -d 'desc("modify outside/f1")' + rebasing 4:707c035aadb6 "conflicting outside/f1" + abort: conflict in file 'outside/f1' is outside narrow clone + [255] diff --git a/tests/test-narrow-shallow-merges.t b/tests/test-narrow-shallow-merges.t new file mode 100644 --- /dev/null +++ b/tests/test-narrow-shallow-merges.t @@ -0,0 +1,345 @@ + $ . "$TESTDIR/narrow-library.sh" + +create full repo + + $ hg init master + $ cd master + $ cat >> .hg/hgrc < [narrow] + > serveellipses=True + > EOF + + $ mkdir inside + $ echo 1 > inside/f + $ hg commit -Aqm 'initial inside' + + $ mkdir outside + $ echo 1 > outside/f + $ hg commit -Aqm 'initial outside' + + $ echo 2a > outside/f + $ hg commit -Aqm 'outside 2a' + $ echo 3 > inside/f + $ hg commit -Aqm 'inside 3' + $ echo 4a > outside/f + $ hg commit -Aqm 'outside 4a' + $ hg update '.~3' + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + + $ echo 2b > outside/f + $ hg commit -Aqm 'outside 2b' + $ echo 3 > inside/f + $ hg commit -Aqm 'inside 3' + $ echo 4b > outside/f + $ hg commit -Aqm 'outside 4b' + $ hg update '.~3' + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + + $ echo 2c > outside/f + $ hg commit -Aqm 'outside 2c' + $ echo 3 > inside/f + $ hg commit -Aqm 'inside 3' + $ echo 4c > outside/f + $ hg commit -Aqm 'outside 4c' + $ hg update '.~3' + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + + $ echo 2d > outside/f + $ hg commit -Aqm 'outside 2d' + $ echo 3 > inside/f + $ hg commit -Aqm 'inside 3' + $ echo 4d > outside/f + $ hg commit -Aqm 'outside 4d' + + $ hg update -r 'desc("outside 4a")' + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg merge -r 'desc("outside 4b")' 2>&1 | egrep -v '(warning:|incomplete!)' + merging outside/f + 0 files updated, 0 files merged, 0 files removed, 1 files unresolved + use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon + $ echo 5 > outside/f + $ rm outside/f.orig + $ hg resolve --mark outside/f + (no more unresolved files) + $ hg commit -m 'merge a/b 5' + $ echo 6 > outside/f + $ hg commit -Aqm 'outside 6' + + $ hg merge -r 'desc("outside 4c")' 2>&1 | egrep -v '(warning:|incomplete!)' + merging outside/f + 0 files updated, 0 files merged, 0 files removed, 1 files unresolved + use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon + $ echo 7 > outside/f + $ rm outside/f.orig + $ hg resolve --mark outside/f + (no more unresolved files) + $ hg commit -Aqm 'merge a/b/c 7' + $ echo 8 > outside/f + $ hg commit -Aqm 'outside 8' + + $ hg merge -r 'desc("outside 4d")' 2>&1 | egrep -v '(warning:|incomplete!)' + merging outside/f + 0 files updated, 0 files merged, 0 files removed, 1 files unresolved + use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon + $ echo 9 > outside/f + $ rm outside/f.orig + $ hg resolve --mark outside/f + (no more unresolved files) + $ hg commit -Aqm 'merge a/b/c/d 9' + $ echo 10 > outside/f + $ hg commit -Aqm 'outside 10' + + $ echo 11 > inside/f + $ hg commit -Aqm 'inside 11' + $ echo 12 > outside/f + $ hg commit -Aqm 'outside 12' + + $ hg log -G -T '{rev} {node|short} {desc}\n' + @ 21 8d874d57adea outside 12 + | + o 20 7ef88b4dd4fa inside 11 + | + o 19 2a20009de83e outside 10 + | + o 18 3ac1f5779de3 merge a/b/c/d 9 + |\ + | o 17 38a9c2f7e546 outside 8 + | | + | o 16 094aa62fc898 merge a/b/c 7 + | |\ + | | o 15 f29d083d32e4 outside 6 + | | | + | | o 14 2dc11382541d merge a/b 5 + | | |\ + o | | | 13 27d07ef97221 outside 4d + | | | | + o | | | 12 465567bdfb2d inside 3 + | | | | + o | | | 11 d1c61993ec83 outside 2d + | | | | + | o | | 10 56859a8e33b9 outside 4c + | | | | + | o | | 9 bb96a08b062a inside 3 + | | | | + | o | | 8 b844052e7b3b outside 2c + |/ / / + | | o 7 9db2d8fcc2a6 outside 4b + | | | + | | o 6 6418167787a6 inside 3 + | | | + +---o 5 77344f344d83 outside 2b + | | + | o 4 9cadde08dc9f outside 4a + | | + | o 3 019ef06f125b inside 3 + | | + | o 2 75e40c075a19 outside 2a + |/ + o 1 906d6c682641 initial outside + | + o 0 9f8e82b51004 initial inside + + +Now narrow and shallow clone this and get a hopefully correct graph + + $ cd .. + $ hg clone --narrow ssh://user@dummy/master narrow --include inside --depth 7 + requesting all changes + adding changesets + adding manifests + adding file changes + added 8 changesets with 3 changes to 1 files + new changesets *:* (glob) + updating to branch default + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd narrow + +To make updating the tests easier, we print the emitted nodes +sorted. This makes it easier to identify when the same node structure +has been emitted, just in a different order. + + $ hg log -G -T '{rev} {node|short}{if(ellipsis,"...")} {desc}\n' + @ 7 8d874d57adea... outside 12 + | + o 6 7ef88b4dd4fa inside 11 + | + o 5 2a20009de83e... outside 10 + | + o 4 3ac1f5779de3... merge a/b/c/d 9 + |\ + | o 3 465567bdfb2d inside 3 + | | + | o 2 d1c61993ec83... outside 2d + | + o 1 bb96a08b062a inside 3 + | + o 0 b844052e7b3b... outside 2c + + + $ hg log -T '{if(ellipsis,"...")}{node|short} {p1node|short} {p2node|short} {desc}\n' | sort + ...2a20009de83e 000000000000 3ac1f5779de3 outside 10 + ...3ac1f5779de3 bb96a08b062a 465567bdfb2d merge a/b/c/d 9 + ...8d874d57adea 7ef88b4dd4fa 000000000000 outside 12 + ...b844052e7b3b 000000000000 000000000000 outside 2c + ...d1c61993ec83 000000000000 000000000000 outside 2d + 465567bdfb2d d1c61993ec83 000000000000 inside 3 + 7ef88b4dd4fa 2a20009de83e 000000000000 inside 11 + bb96a08b062a b844052e7b3b 000000000000 inside 3 + + $ cd .. + +Incremental test case: show a pull can pull in a conflicted merge even if elided + + $ hg init pullmaster + $ cd pullmaster + $ cat >> .hg/hgrc < [narrow] + > serveellipses=True + > EOF + $ mkdir inside outside + $ echo v1 > inside/f + $ echo v1 > outside/f + $ hg add inside/f outside/f + $ hg commit -m init + + $ for line in a b c d + > do + > hg update -r 0 + > echo v2$line > outside/f + > hg commit -m "outside 2$line" + > echo v2$line > inside/f + > hg commit -m "inside 2$line" + > echo v3$line > outside/f + > hg commit -m "outside 3$line" + > echo v4$line > outside/f + > hg commit -m "outside 4$line" + > done + 0 files updated, 0 files merged, 0 files removed, 0 files unresolved + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + created new head + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + created new head + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + created new head + + $ cd .. + $ hg clone --narrow ssh://user@dummy/pullmaster pullshallow \ + > --include inside --depth 3 + requesting all changes + adding changesets + adding manifests + adding file changes + added 12 changesets with 5 changes to 1 files (+3 heads) + new changesets *:* (glob) + updating to branch default + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd pullshallow + + $ hg log -G -T '{rev} {node|short}{if(ellipsis,"...")} {desc}\n' + @ 11 0ebbd712a0c8... outside 4d + | + o 10 0d4c867aeb23 inside 2d + | + o 9 e932969c3961... outside 2d + + o 8 33d530345455... outside 4c + | + o 7 0ce6481bfe07 inside 2c + | + o 6 caa65c940632... outside 2c + + o 5 3df233defecc... outside 4b + | + o 4 7162cc6d11a4 inside 2b + | + o 3 f2a632f0082d... outside 2b + + o 2 b8a3da16ba49... outside 4a + | + o 1 53f543eb8e45 inside 2a + | + o 0 1be3e5221c6a... outside 2a + + $ hg log -T '{if(ellipsis,"...")}{node|short} {p1node|short} {p2node|short} {desc}\n' | sort + ...0ebbd712a0c8 0d4c867aeb23 000000000000 outside 4d + ...1be3e5221c6a 000000000000 000000000000 outside 2a + ...33d530345455 0ce6481bfe07 000000000000 outside 4c + ...3df233defecc 7162cc6d11a4 000000000000 outside 4b + ...b8a3da16ba49 53f543eb8e45 000000000000 outside 4a + ...caa65c940632 000000000000 000000000000 outside 2c + ...e932969c3961 000000000000 000000000000 outside 2d + ...f2a632f0082d 000000000000 000000000000 outside 2b + 0ce6481bfe07 caa65c940632 000000000000 inside 2c + 0d4c867aeb23 e932969c3961 000000000000 inside 2d + 53f543eb8e45 1be3e5221c6a 000000000000 inside 2a + 7162cc6d11a4 f2a632f0082d 000000000000 inside 2b + + $ cd ../pullmaster + $ hg update -r 'desc("outside 4a")' + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg merge -r 'desc("outside 4b")' 2>&1 | egrep -v '(warning:|incomplete!)' + merging inside/f + merging outside/f + 0 files updated, 0 files merged, 0 files removed, 2 files unresolved + use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon + $ echo 3 > inside/f + $ echo 5 > outside/f + $ rm -f {in,out}side/f.orig + $ hg resolve --mark inside/f outside/f + (no more unresolved files) + $ hg commit -m 'merge a/b 5' + + $ hg update -r 'desc("outside 4c")' + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg merge -r 'desc("outside 4d")' 2>&1 | egrep -v '(warning:|incomplete!)' + merging inside/f + merging outside/f + 0 files updated, 0 files merged, 0 files removed, 2 files unresolved + use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon + $ echo 3 > inside/f + $ echo 5 > outside/f + $ rm -f {in,out}side/f.orig + $ hg resolve --mark inside/f outside/f + (no more unresolved files) + $ hg commit -m 'merge c/d 5' + + $ hg update -r 'desc("merge a/b 5")' + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg merge -r 'desc("merge c/d 5")' + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + (branch merge, don't forget to commit) + $ echo 6 > outside/f + $ hg commit -m 'outside 6' + $ echo 7 > outside/f + $ hg commit -m 'outside 7' + $ echo 8 > outside/f + $ hg commit -m 'outside 8' + + $ cd ../pullshallow + $ hg pull --depth 3 + pulling from ssh://user@dummy/pullmaster + searching for changes + adding changesets + adding manifests + adding file changes + added 4 changesets with 3 changes to 1 files (-3 heads) + new changesets *:* (glob) + (run 'hg update' to get a working copy) + + $ hg log -T '{if(ellipsis,"...")}{node|short} {p1node|short} {p2node|short} {desc}\n' | sort + ...0ebbd712a0c8 0d4c867aeb23 000000000000 outside 4d + ...1be3e5221c6a 000000000000 000000000000 outside 2a + ...33d530345455 0ce6481bfe07 000000000000 outside 4c + ...3df233defecc 7162cc6d11a4 000000000000 outside 4b + ...b8a3da16ba49 53f543eb8e45 000000000000 outside 4a + ...bf545653453e 968003d40c60 000000000000 outside 8 + ...caa65c940632 000000000000 000000000000 outside 2c + ...e932969c3961 000000000000 000000000000 outside 2d + ...f2a632f0082d 000000000000 000000000000 outside 2b + 0ce6481bfe07 caa65c940632 000000000000 inside 2c + 0d4c867aeb23 e932969c3961 000000000000 inside 2d + 53f543eb8e45 1be3e5221c6a 000000000000 inside 2a + 67d49c0bdbda b8a3da16ba49 3df233defecc merge a/b 5 + 7162cc6d11a4 f2a632f0082d 000000000000 inside 2b + 968003d40c60 67d49c0bdbda e867021d52c2 outside 6 + e867021d52c2 33d530345455 0ebbd712a0c8 merge c/d 5 diff --git a/tests/test-narrow-shallow.t b/tests/test-narrow-shallow.t new file mode 100644 --- /dev/null +++ b/tests/test-narrow-shallow.t @@ -0,0 +1,122 @@ + $ . "$TESTDIR/narrow-library.sh" + + $ hg init master + $ cd master + $ cat >> .hg/hgrc < [narrow] + > serveellipses=True + > EOF + $ for x in `$TESTDIR/seq.py 10` + > do + > echo $x > "f$x" + > hg add "f$x" + > done + $ hg commit -m "Add root files" + $ mkdir d1 d2 + $ for x in `$TESTDIR/seq.py 10` + > do + > echo d1/$x > "d1/f$x" + > hg add "d1/f$x" + > echo d2/$x > "d2/f$x" + > hg add "d2/f$x" + > done + $ hg commit -m "Add d1 and d2" + $ for x in `$TESTDIR/seq.py 10` + > do + > echo f$x rev2 > "f$x" + > echo d1/f$x rev2 > "d1/f$x" + > echo d2/f$x rev2 > "d2/f$x" + > hg commit -m "Commit rev2 of f$x, d1/f$x, d2/f$x" + > done + $ cd .. + +narrow and shallow clone the d2 directory + + $ hg clone --narrow ssh://user@dummy/master shallow --include "d2" --depth 2 + requesting all changes + adding changesets + adding manifests + adding file changes + added 4 changesets with 13 changes to 10 files + new changesets *:* (glob) + updating to branch default + 10 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd shallow + $ hg log -T '{rev}{if(ellipsis,"...")}: {desc}\n' + 3: Commit rev2 of f10, d1/f10, d2/f10 + 2: Commit rev2 of f9, d1/f9, d2/f9 + 1: Commit rev2 of f8, d1/f8, d2/f8 + 0...: Commit rev2 of f7, d1/f7, d2/f7 + $ hg update 0 + 3 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cat d2/f7 d2/f8 + d2/f7 rev2 + d2/8 + + $ cd .. + +change every upstream file once + + $ cd master + $ for x in `$TESTDIR/seq.py 10` + > do + > echo f$x rev3 > "f$x" + > echo d1/f$x rev3 > "d1/f$x" + > echo d2/f$x rev3 > "d2/f$x" + > hg commit -m "Commit rev3 of f$x, d1/f$x, d2/f$x" + > done + $ cd .. + +pull new changes with --depth specified. There were 10 changes to the d2 +directory but the shallow pull should only fetch 3. + + $ cd shallow + $ hg pull --depth 2 + pulling from ssh://user@dummy/master + searching for changes + adding changesets + adding manifests + adding file changes + added 4 changesets with 10 changes to 10 files + new changesets *:* (glob) + (run 'hg update' to get a working copy) + $ hg log -T '{rev}{if(ellipsis,"...")}: {desc}\n' + 7: Commit rev3 of f10, d1/f10, d2/f10 + 6: Commit rev3 of f9, d1/f9, d2/f9 + 5: Commit rev3 of f8, d1/f8, d2/f8 + 4...: Commit rev3 of f7, d1/f7, d2/f7 + 3: Commit rev2 of f10, d1/f10, d2/f10 + 2: Commit rev2 of f9, d1/f9, d2/f9 + 1: Commit rev2 of f8, d1/f8, d2/f8 + 0...: Commit rev2 of f7, d1/f7, d2/f7 + $ hg update 4 + merging d2/f1 + merging d2/f2 + merging d2/f3 + merging d2/f4 + merging d2/f5 + merging d2/f6 + merging d2/f7 + 3 files updated, 7 files merged, 0 files removed, 0 files unresolved + $ cat d2/f7 d2/f8 + d2/f7 rev3 + d2/f8 rev2 + $ hg update 7 + 3 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cat d2/f10 + d2/f10 rev3 + + $ cd .. + +cannot clone with zero or negative depth + + $ hg clone --narrow ssh://user@dummy/master bad --include "d2" --depth 0 + requesting all changes + remote: abort: depth must be positive, got 0 + abort: pull failed on remote + [255] + $ hg clone --narrow ssh://user@dummy/master bad --include "d2" --depth -1 + requesting all changes + remote: abort: depth must be positive, got -1 + abort: pull failed on remote + [255] diff --git a/tests/test-narrow-strip-tree.t b/tests/test-narrow-strip-tree.t new file mode 100644 --- /dev/null +++ b/tests/test-narrow-strip-tree.t @@ -0,0 +1,52 @@ + $ cd $TESTDIR && python $RUNTESTDIR/run-tests.py \ + > --extra-config-opt experimental.treemanifest=1 test-narrow-strip.t 2>&1 | \ + > grep -v 'unexpected mercurial lib' | egrep -v '\(expected' + + --- */test-narrow-strip.t (glob) + +++ */test-narrow-strip.t.err (glob) + @@ -\d+,\d+ \+\d+,\d+ @@ (re) + o 0 initial + + $ hg debugdata -m 1 + - inside/f1\x004d6a634d5ba06331a60c29ee0db8412490a54fcd (esc) + - outside/f1\x0084ba604d54dee1f13310ce3d4ac2e8a36636691a (esc) + + inside\x006a8bc41df94075d501f9740587a0c0e13c170dc5t (esc) + + outside\x00255c2627ebdd3c7dcaa6945246f9b9f02bd45a09t (esc) + + $ rm -f $TESTTMP/narrow/.hg/strip-backup/*-backup.hg + $ hg strip . + @@ -\d+,\d+ \+\d+,\d+ @@ (re) + + Check that hash of file outside narrow spec got restored + $ hg debugdata -m 2 + - inside/f1\x004d6a634d5ba06331a60c29ee0db8412490a54fcd (esc) + - outside/f1\x0084ba604d54dee1f13310ce3d4ac2e8a36636691a (esc) + + inside\x006a8bc41df94075d501f9740587a0c0e13c170dc5t (esc) + + outside\x00255c2627ebdd3c7dcaa6945246f9b9f02bd45a09t (esc) + + Also verify we can apply the bundle with 'hg pull': + $ hg co -r 'desc("modify inside")' + @@ -\d+,\d+ \+\d+,\d+ @@ (re) + date: Thu Jan 01 00:00:00 1970 +0000 + summary: initial + + - changeset: 1:9e48d953700d + + changeset: 1:3888164bccf0 + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: modify outside again + + - changeset: 2:f505d5e96aa8 + + changeset: 2:40b66f95a209 + tag: tip + - parent: 0:a99f4d53924d + + parent: 0:c2a5fabcca3c + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: modify inside + + ERROR: test-narrow-strip.t output changed + ! + Failed test-narrow-strip.t: output changed + # Ran 1 tests, 0 skipped, 1 failed. + python hash seed: * (glob) diff --git a/tests/test-narrow-strip.t b/tests/test-narrow-strip.t new file mode 100644 --- /dev/null +++ b/tests/test-narrow-strip.t @@ -0,0 +1,148 @@ + + $ . "$TESTDIR/narrow-library.sh" + +create full repo + + $ hg init master + $ cd master + $ cat >> .hg/hgrc < [narrow] + > serveellipses=True + > EOF + + $ mkdir inside + $ echo inside > inside/f1 + $ mkdir outside + $ echo outside > outside/f1 + $ hg ci -Aqm 'initial' + + $ echo modified > inside/f1 + $ hg ci -qm 'modify inside' + + $ hg co -q 0 + $ echo modified > outside/f1 + $ hg ci -qm 'modify outside' + + $ echo modified again >> outside/f1 + $ hg ci -qm 'modify outside again' + + $ cd .. + + $ hg clone --narrow ssh://user@dummy/master narrow --include inside + requesting all changes + adding changesets + adding manifests + adding file changes + added 3 changesets with 2 changes to 1 files (+1 heads) + new changesets *:* (glob) + updating to branch default + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd narrow + $ cat >> $HGRCPATH < [extensions] + > strip= + > EOF + +Can strip and recover changesets affecting only files within narrow spec + + $ hg co -r 'desc("modify inside")' + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ rm -f $TESTTMP/narrow/.hg/strip-backup/*-backup.hg + $ hg strip . + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + saved backup bundle to $TESTTMP/narrow/.hg/strip-backup/*-backup.hg (glob) + $ hg unbundle .hg/strip-backup/*-backup.hg + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files (+1 heads) + new changesets * (glob) + (run 'hg heads' to see heads, 'hg merge' to merge) + +Can strip and recover changesets affecting files outside of narrow spec + + $ hg co -r 'desc("modify outside")' + 0 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg log -G -T '{rev} {desc}\n' + o 2 modify inside + | + | @ 1 modify outside again + |/ + o 0 initial + + $ hg debugdata -m 1 + inside/f1\x004d6a634d5ba06331a60c29ee0db8412490a54fcd (esc) + outside/f1\x0084ba604d54dee1f13310ce3d4ac2e8a36636691a (esc) + + $ rm -f $TESTTMP/narrow/.hg/strip-backup/*-backup.hg + $ hg strip . + 0 files updated, 0 files merged, 0 files removed, 0 files unresolved + saved backup bundle to $TESTTMP/narrow/.hg/strip-backup/*-backup.hg (glob) + $ hg unbundle .hg/strip-backup/*-backup.hg + adding changesets + adding manifests + adding file changes + added 1 changesets with 0 changes to 0 files (+1 heads) + new changesets * (glob) + (run 'hg heads' to see heads, 'hg merge' to merge) + $ hg log -G -T '{rev} {desc}\n' + o 2 modify outside again + | + | o 1 modify inside + |/ + @ 0 initial + +Check that hash of file outside narrow spec got restored + $ hg debugdata -m 2 + inside/f1\x004d6a634d5ba06331a60c29ee0db8412490a54fcd (esc) + outside/f1\x0084ba604d54dee1f13310ce3d4ac2e8a36636691a (esc) + +Also verify we can apply the bundle with 'hg pull': + $ hg co -r 'desc("modify inside")' + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ rm .hg/strip-backup/*-backup.hg + $ hg strip . + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + saved backup bundle to $TESTTMP/narrow/.hg/strip-backup/*-backup.hg (glob) + $ hg pull .hg/strip-backup/*-backup.hg + pulling from .hg/strip-backup/*-backup.hg (glob) + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files (+1 heads) + new changesets * (glob) + (run 'hg heads' to see heads, 'hg merge' to merge) + + $ rm .hg/strip-backup/*-backup.hg + $ hg strip 0 + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + saved backup bundle to $TESTTMP/narrow/.hg/strip-backup/*-backup.hg (glob) + $ hg incoming .hg/strip-backup/*-backup.hg + comparing with .hg/strip-backup/*-backup.hg (glob) + changeset: 0:* (glob) + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: initial + + changeset: 1:9e48d953700d + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: modify outside again + + changeset: 2:f505d5e96aa8 + tag: tip + parent: 0:a99f4d53924d + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: modify inside + + $ hg pull .hg/strip-backup/*-backup.hg + pulling from .hg/strip-backup/*-backup.hg (glob) + requesting all changes + adding changesets + adding manifests + adding file changes + added 3 changesets with 2 changes to 1 files (+1 heads) + new changesets *:* (glob) + (run 'hg heads' to see heads, 'hg merge' to merge) diff --git a/tests/test-narrow-tree.t b/tests/test-narrow-tree.t new file mode 100644 --- /dev/null +++ b/tests/test-narrow-tree.t @@ -0,0 +1,68 @@ + $ cd $TESTDIR && python $RUNTESTDIR/run-tests.py \ + > --extra-config-opt experimental.treemanifest=1 test-narrow-narrow.t 2>&1 | \ + > grep -v 'unexpected mercurial lib' | egrep -v '\(expected' + + --- /*/tests/test-narrow-narrow.t (glob) + +++ /*/tests/test-narrow-narrow.t.err (glob) + @@ -\d+,\d+ \+\d+,\d+ @@ (re) + * (glob) + * (glob) + deleting data/d0/f.i + + deleting meta/d0/00manifest.i + $ hg log -T "{node|short}: {desc} {outsidenarrow}\n" + *: local change to d3 (glob) + *: add d10/f outsidenarrow (glob) + @@ -\d+,\d+ \+\d+,\d+ @@ (re) + looking for local changes to affected paths + saved backup bundle to $TESTTMP/narrow-local-changes/.hg/strip-backup/*-narrow.hg (glob) + deleting data/d0/f.i + + deleting meta/d0/00manifest.i + Updates off of stripped commit if necessary + $ hg co -r 'desc("local change to d3")' -q + $ echo local change >> d6/f + @@ -\d+,\d+ \+\d+,\d+ @@ (re) + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + saved backup bundle to $TESTTMP/narrow-local-changes/.hg/strip-backup/*-narrow.hg (glob) + deleting data/d3/f.i + + deleting meta/d3/00manifest.i + $ hg log -T '{desc}\n' -r . + add d10/f + Updates to nullid if necessary + @@ -\d+,\d+ \+\d+,\d+ @@ (re) + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + saved backup bundle to $TESTTMP/narrow-local-changes/.hg/strip-backup/*-narrow.hg (glob) + deleting data/d3/f.i + + deleting meta/d3/00manifest.i + $ hg id + 000000000000 + $ cd .. + @@ -\d+,\d+ \+\d+,\d+ @@ (re) + searching for changes + looking for local changes to affected paths + deleting data/d0/f.i + + deleting meta/d0/00manifest.i + $ hg tracked + $ hg files + [1] + @@ -\d+,\d+ \+\d+,\d+ @@ (re) + searching for changes + looking for local changes to affected paths + deleting data/d6/f.i + + deleting meta/d6/00manifest.i + $ hg tracked + I path:d0 + I path:d3 + @@ -\d+,\d+ \+\d+,\d+ @@ (re) + searching for changes + looking for local changes to affected paths + deleting data/d0/f.i + + deleting meta/d0/00manifest.i + $ hg tracked + I path:d3 + I path:d9 + + ERROR: test-narrow-narrow.t output changed + ! + Failed test-narrow-narrow.t: output changed + # Ran 1 tests, 0 skipped, 1 failed. + python hash seed: * (glob) diff --git a/tests/test-narrow-update.t b/tests/test-narrow-update.t new file mode 100644 --- /dev/null +++ b/tests/test-narrow-update.t @@ -0,0 +1,76 @@ + + $ . "$TESTDIR/narrow-library.sh" + +create full repo + + $ hg init master + $ cd master + $ echo init > init + $ hg ci -Aqm 'initial' + + $ mkdir inside + $ echo inside > inside/f1 + $ mkdir outside + $ echo outside > outside/f1 + $ hg ci -Aqm 'add inside and outside' + + $ echo modified > inside/f1 + $ hg ci -qm 'modify inside' + + $ echo modified > outside/f1 + $ hg ci -qm 'modify outside' + + $ cd .. + + $ hg clone --narrow ssh://user@dummy/master narrow --include inside + requesting all changes + adding changesets + adding manifests + adding file changes + added 4 changesets with 2 changes to 1 files + new changesets *:* (glob) + updating to branch default + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd narrow + $ hg debugindex -c + rev offset length base linkrev nodeid p1 p2 + 0 0 64 0 0 9958b1af2add 000000000000 000000000000 + 1 64 81 1 1 2db4ce2a3bfe 9958b1af2add 000000000000 + 2 145 75 2 2 0980ee31a742 2db4ce2a3bfe 000000000000 + 3 220 (76|77) 3 3 4410145019b7 0980ee31a742 000000000000 (re) + + $ hg update -q 0 + +Can update to revision with changes inside + + $ hg update -q 'desc("add inside and outside")' + $ hg update -q 'desc("modify inside")' + $ find * + inside + inside/f1 (glob) + $ cat inside/f1 + modified + +Can update to revision with changes outside + + $ hg update -q 'desc("modify outside")' + $ find * + inside + inside/f1 (glob) + $ cat inside/f1 + modified + +Can update with a deleted file inside + + $ hg rm inside/f1 + $ hg update -q 'desc("modify inside")' + $ hg update -q 'desc("modify outside")' + $ hg update -q 'desc("initial")' + $ hg update -q 'desc("modify inside")' + +Can update with a moved file inside + + $ hg mv inside/f1 inside/f2 + $ hg update -q 'desc("modify outside")' + $ hg update -q 'desc("initial")' + $ hg update -q 'desc("modify inside")' diff --git a/tests/test-narrow-widen-tree.t b/tests/test-narrow-widen-tree.t new file mode 100644 --- /dev/null +++ b/tests/test-narrow-widen-tree.t @@ -0,0 +1,28 @@ + $ cd $TESTDIR && python $RUNTESTDIR/run-tests.py \ + > --extra-config-opt experimental.treemanifest=1 test-narrow-widen.t 2>&1 | \ + > grep -v 'unexpected mercurial lib' | egrep -v '\(expected' + + --- */test-narrow-widen.t (glob) + +++ */test-narrow-widen.t.err (glob) + @@ -\d+,\d+ \+\d+,\d+ @@ (re) + $ hg verify + checking changesets + checking manifests + + checking directory manifests + crosschecking files in changesets and manifests + checking files + 4 files, 8 changesets, 4 total revisions + @@ -\d+,\d+ \+\d+,\d+ @@ (re) + $ hg verify + checking changesets + checking manifests + + checking directory manifests + crosschecking files in changesets and manifests + checking files + 5 files, 9 changesets, 5 total revisions + + ERROR: test-narrow-widen.t output changed + ! + Failed test-narrow-widen.t: output changed + # Ran 1 tests, 0 skipped, 1 failed. + python hash seed: * (glob) diff --git a/tests/test-narrow-widen.t b/tests/test-narrow-widen.t new file mode 100644 --- /dev/null +++ b/tests/test-narrow-widen.t @@ -0,0 +1,355 @@ + $ . "$TESTDIR/narrow-library.sh" + + $ hg init master + $ cd master + $ cat >> .hg/hgrc < [narrow] + > serveellipses=True + > EOF + + $ mkdir inside + $ echo 'inside' > inside/f + $ hg add inside/f + $ hg commit -m 'add inside' + + $ mkdir widest + $ echo 'widest' > widest/f + $ hg add widest/f + $ hg commit -m 'add widest' + + $ mkdir outside + $ echo 'outside' > outside/f + $ hg add outside/f + $ hg commit -m 'add outside' + + $ cd .. + +narrow clone the inside file + + $ hg clone --narrow ssh://user@dummy/master narrow --include inside + requesting all changes + adding changesets + adding manifests + adding file changes + added 2 changesets with 1 changes to 1 files + new changesets *:* (glob) + updating to branch default + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd narrow + $ hg tracked + I path:inside + $ ls + inside + $ cat inside/f + inside + $ cd .. + +add more upstream files which we will include in a wider narrow spec + + $ cd master + + $ mkdir wider + $ echo 'wider' > wider/f + $ hg add wider/f + $ echo 'widest v2' > widest/f + $ hg commit -m 'add wider, update widest' + + $ echo 'widest v3' > widest/f + $ hg commit -m 'update widest v3' + + $ echo 'inside v2' > inside/f + $ hg commit -m 'update inside' + + $ mkdir outside2 + $ echo 'outside2' > outside2/f + $ hg add outside2/f + $ hg commit -m 'add outside2' + + $ echo 'widest v4' > widest/f + $ hg commit -m 'update widest v4' + + $ hg log -T "{if(ellipsis, '...')}{node|short}: {desc}\n" + *: update widest v4 (glob) + *: add outside2 (glob) + *: update inside (glob) + *: update widest v3 (glob) + *: add wider, update widest (glob) + *: add outside (glob) + *: add widest (glob) + *: add inside (glob) + + $ cd .. + +Widen the narrow spec to see the wider file. This should not get the newly +added upstream revisions. + + $ cd narrow + $ hg tracked --addinclude wider/f + comparing with ssh://user@dummy/master + searching for changes + no changes found + saved backup bundle to $TESTTMP/narrow/.hg/strip-backup/*-widen.hg (glob) + adding changesets + adding manifests + adding file changes + added 2 changesets with 1 changes to 1 files + new changesets *:* (glob) + $ hg tracked + I path:inside + I path:wider/f + +Pull down the newly added upstream revision. + + $ hg pull + pulling from ssh://user@dummy/master + searching for changes + adding changesets + adding manifests + adding file changes + added 4 changesets with 2 changes to 2 files + new changesets *:* (glob) + (run 'hg update' to get a working copy) + $ hg update -r 'desc("add wider")' + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cat wider/f + wider + + $ hg update -r 'desc("update inside")' + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cat wider/f + wider + $ cat inside/f + inside v2 + + $ hg log -T "{if(ellipsis, '...')}{node|short}: {desc}\n" + ...*: update widest v4 (glob) + *: update inside (glob) + ...*: update widest v3 (glob) + *: add wider, update widest (glob) + ...*: add outside (glob) + *: add inside (glob) + +Check that widening with a newline fails + + $ hg tracked --addinclude 'widest + > ' + abort: newlines are not allowed in narrowspec paths + [255] + +widen the narrow spec to include the widest file + + $ hg tracked --addinclude widest + comparing with ssh://user@dummy/master + searching for changes + no changes found + saved backup bundle to $TESTTMP/narrow/.hg/strip-backup/*-widen.hg (glob) + adding changesets + adding manifests + adding file changes + added 8 changesets with 7 changes to 3 files + new changesets *:* (glob) + $ hg tracked + I path:inside + I path:wider/f + I path:widest + $ hg update 'desc("add widest")' + 2 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ cat widest/f + widest + $ hg update 'desc("add wider, update widest")' + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cat wider/f + wider + $ cat widest/f + widest v2 + $ hg update 'desc("update widest v3")' + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cat widest/f + widest v3 + $ hg update 'desc("update widest v4")' + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cat widest/f + widest v4 + + $ hg log -T "{if(ellipsis, '...')}{node|short}: {desc}\n" + *: update widest v4 (glob) + ...*: add outside2 (glob) + *: update inside (glob) + *: update widest v3 (glob) + *: add wider, update widest (glob) + ...*: add outside (glob) + *: add widest (glob) + *: add inside (glob) + +separate suite of tests: files from 0-10 modified in changes 0-10. This allows +more obvious precise tests tickling particular corner cases. + + $ cd .. + $ hg init upstream + $ cd upstream + $ cat >> .hg/hgrc < [narrow] + > serveellipses=True + > EOF + $ for x in `$TESTDIR/seq.py 0 10` + > do + > mkdir d$x + > echo $x > d$x/f + > hg add d$x/f + > hg commit -m "add d$x/f" + > done + $ hg log -T "{node|short}: {desc}\n" + *: add d10/f (glob) + *: add d9/f (glob) + *: add d8/f (glob) + *: add d7/f (glob) + *: add d6/f (glob) + *: add d5/f (glob) + *: add d4/f (glob) + *: add d3/f (glob) + *: add d2/f (glob) + *: add d1/f (glob) + *: add d0/f (glob) + +make narrow clone with every third node. + + $ cd .. + $ hg clone --narrow ssh://user@dummy/upstream narrow2 --include d0 --include d3 --include d6 --include d9 + requesting all changes + adding changesets + adding manifests + adding file changes + added 8 changesets with 4 changes to 4 files + new changesets *:* (glob) + updating to branch default + 4 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd narrow2 + $ hg tracked + I path:d0 + I path:d3 + I path:d6 + I path:d9 + $ hg verify + checking changesets + checking manifests + crosschecking files in changesets and manifests + checking files + 4 files, 8 changesets, 4 total revisions + $ hg log -T "{if(ellipsis, '...')}{node|short}: {desc}\n" + ...*: add d10/f (glob) + *: add d9/f (glob) + ...*: add d8/f (glob) + *: add d6/f (glob) + ...*: add d5/f (glob) + *: add d3/f (glob) + ...*: add d2/f (glob) + *: add d0/f (glob) + $ hg tracked --addinclude d1 + comparing with ssh://user@dummy/upstream + searching for changes + no changes found + saved backup bundle to $TESTTMP/narrow2/.hg/strip-backup/*-widen.hg (glob) + adding changesets + adding manifests + adding file changes + added 9 changesets with 5 changes to 5 files + new changesets *:* (glob) + $ hg tracked + I path:d0 + I path:d1 + I path:d3 + I path:d6 + I path:d9 + $ hg log -T "{if(ellipsis, '...')}{node|short}: {desc}\n" + ...*: add d10/f (glob) + *: add d9/f (glob) + ...*: add d8/f (glob) + *: add d6/f (glob) + ...*: add d5/f (glob) + *: add d3/f (glob) + ...*: add d2/f (glob) + *: add d1/f (glob) + *: add d0/f (glob) + +Verify shouldn't claim the repo is corrupt after a widen. + + $ hg verify + checking changesets + checking manifests + crosschecking files in changesets and manifests + checking files + 5 files, 9 changesets, 5 total revisions + +Widening preserves parent of local commit + + $ cd .. + $ hg clone -q --narrow ssh://user@dummy/upstream narrow3 --include d2 -r 2 + $ cd narrow3 + $ hg log -T "{if(ellipsis, '...')}{node|short}: {desc}\n" + *: add d2/f (glob) + ...*: add d1/f (glob) + $ hg pull -q -r 3 + $ hg co -q tip + $ hg pull -q -r 4 + $ echo local > d2/f + $ hg ci -m local + created new head + $ hg tracked -q --addinclude d0 --addinclude d9 + +Widening preserves bookmarks + + $ cd .. + $ hg clone -q --narrow ssh://user@dummy/upstream narrow-bookmarks --include d4 + $ cd narrow-bookmarks + $ echo local > d4/f + $ hg ci -m local + $ hg bookmarks bookmark + $ hg bookmarks + * bookmark 3:* (glob) + $ hg -q tracked --addinclude d2 + $ hg bookmarks + * bookmark 5:* (glob) + $ hg log -r bookmark -T '{desc}\n' + local + +Widening that fails can be recovered from + + $ cd .. + $ hg clone -q --narrow ssh://user@dummy/upstream interrupted --include d0 + $ cd interrupted + $ echo local > d0/f + $ hg ci -m local + $ hg log -T "{if(ellipsis, '...')}{rev}: {desc}\n" + 2: local + ...1: add d10/f + 0: add d0/f + $ hg bookmarks bookmark + $ hg --config hooks.pretxnchangegroup.bad=false tracked --addinclude d1 + comparing with ssh://user@dummy/upstream + searching for changes + no changes found + saved backup bundle to $TESTTMP/interrupted/.hg/strip-backup/*-widen.hg (glob) + adding changesets + adding manifests + adding file changes + added 3 changesets with 2 changes to 2 files + transaction abort! + rollback completed + abort: pretxnchangegroup.bad hook exited with status 1 + [255] + $ hg log -T "{if(ellipsis, '...')}{rev}: {desc}\n" + $ hg bookmarks + no bookmarks set + $ hg unbundle .hg/strip-backup/*-widen.hg + adding changesets + adding manifests + adding file changes + added 3 changesets with 2 changes to 1 files + new changesets *:* (glob) + (run 'hg update' to get a working copy) + $ hg log -T "{if(ellipsis, '...')}{rev}: {desc}\n" + 2: local + ...1: add d10/f + 0: add d0/f + $ hg bookmarks + * bookmark 2:* (glob) diff --git a/tests/test-narrow.t b/tests/test-narrow.t new file mode 100644 --- /dev/null +++ b/tests/test-narrow.t @@ -0,0 +1,358 @@ + $ . "$TESTDIR/narrow-library.sh" + + $ hg init master + $ cd master + $ cat >> .hg/hgrc < [narrow] + > serveellipses=True + > EOF + $ for x in `$TESTDIR/seq.py 0 10` + > do + > mkdir d$x + > echo $x > d$x/f + > hg add d$x/f + > hg commit -m "add d$x/f" + > done + $ hg log -T "{node|short}: {desc}\n" + *: add d10/f (glob) + *: add d9/f (glob) + *: add d8/f (glob) + *: add d7/f (glob) + *: add d6/f (glob) + *: add d5/f (glob) + *: add d4/f (glob) + *: add d3/f (glob) + *: add d2/f (glob) + *: add d1/f (glob) + *: add d0/f (glob) + $ cd .. + +Error if '.' or '..' are in the directory to track. + $ hg clone --narrow ssh://user@dummy/master foo --include ./asdf + requesting all changes + abort: "." and ".." are not allowed in narrowspec paths + [255] + $ hg clone --narrow ssh://user@dummy/master foo --include asdf/.. + requesting all changes + abort: "." and ".." are not allowed in narrowspec paths + [255] + $ hg clone --narrow ssh://user@dummy/master foo --include a/./c + requesting all changes + abort: "." and ".." are not allowed in narrowspec paths + [255] + +Names with '.' in them are OK. + $ hg clone --narrow ssh://user@dummy/master $RANDOM --include a/.b/c + requesting all changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 0 changes to 0 files + new changesets * (glob) + updating to branch default + 0 files updated, 0 files merged, 0 files removed, 0 files unresolved + +Test repo with local changes + $ hg clone --narrow ssh://user@dummy/master narrow-local-changes --include d0 --include d3 --include d6 + requesting all changes + adding changesets + adding manifests + adding file changes + added 6 changesets with 3 changes to 3 files + new changesets *:* (glob) + updating to branch default + 3 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd narrow-local-changes + $ cat >> $HGRCPATH << EOF + > [experimental] + > evolution=createmarkers + > EOF + $ echo local change >> d0/f + $ hg ci -m 'local change to d0' + $ hg co '.^' + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ echo local change >> d3/f + $ hg ci -m 'local hidden change to d3' + created new head + $ hg ci --amend -m 'local change to d3' + $ hg tracked --removeinclude d0 + comparing with ssh://user@dummy/master + searching for changes + looking for local changes to affected paths + The following changeset(s) or their ancestors have local changes not on the remote: + * (glob) + abort: local changes found + (use --force-delete-local-changes to ignore) + [255] +Check that nothing was removed by the failed attempts + $ hg tracked + I path:d0 + I path:d3 + I path:d6 + $ hg files + d0/f + d3/f + d6/f + $ find * + d0 + d0/f + d3 + d3/f + d6 + d6/f + $ hg verify -q +Force deletion of local changes + $ hg log -T "{node|short}: {desc} {outsidenarrow}\n" + *: local change to d3 (glob) + *: local change to d0 (glob) + *: add d10/f outsidenarrow (glob) + *: add d6/f (glob) + *: add d5/f outsidenarrow (glob) + *: add d3/f (glob) + *: add d2/f outsidenarrow (glob) + *: add d0/f (glob) + $ hg tracked --removeinclude d0 --force-delete-local-changes + comparing with ssh://user@dummy/master + searching for changes + looking for local changes to affected paths + The following changeset(s) or their ancestors have local changes not on the remote: + * (glob) + saved backup bundle to $TESTTMP/narrow-local-changes/.hg/strip-backup/*-narrow.hg (glob) + deleting data/d0/f.i + $ hg log -T "{node|short}: {desc} {outsidenarrow}\n" + *: local change to d3 (glob) + *: add d10/f outsidenarrow (glob) + *: add d6/f (glob) + *: add d5/f outsidenarrow (glob) + *: add d3/f (glob) + *: add d2/f outsidenarrow (glob) + *: add d0/f outsidenarrow (glob) +Can restore stripped local changes after widening + $ hg tracked --addinclude d0 -q + $ hg unbundle .hg/strip-backup/*-narrow.hg -q + $ hg --hidden co -r 'desc("local change to d0")' -q + $ cat d0/f + 0 + local change +Pruned commits affecting removed paths should not prevent narrowing + $ hg co '.^' + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg debugobsolete `hg log -T '{node}' -r 'desc("local change to d0")'` + obsoleted 1 changesets + $ hg tracked --removeinclude d0 + comparing with ssh://user@dummy/master + searching for changes + looking for local changes to affected paths + saved backup bundle to $TESTTMP/narrow-local-changes/.hg/strip-backup/*-narrow.hg (glob) + deleting data/d0/f.i +Updates off of stripped commit if necessary + $ hg co -r 'desc("local change to d3")' -q + $ echo local change >> d6/f + $ hg ci -m 'local change to d6' + $ hg tracked --removeinclude d3 --force-delete-local-changes + comparing with ssh://user@dummy/master + searching for changes + looking for local changes to affected paths + The following changeset(s) or their ancestors have local changes not on the remote: + * (glob) + * (glob) + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + saved backup bundle to $TESTTMP/narrow-local-changes/.hg/strip-backup/*-narrow.hg (glob) + deleting data/d3/f.i + $ hg log -T '{desc}\n' -r . + add d10/f +Updates to nullid if necessary + $ hg tracked --addinclude d3 -q + $ hg co null -q + $ mkdir d3 + $ echo local change > d3/f + $ hg add d3/f + $ hg ci -m 'local change to d3' + created new head + $ hg tracked --removeinclude d3 --force-delete-local-changes + comparing with ssh://user@dummy/master + searching for changes + looking for local changes to affected paths + The following changeset(s) or their ancestors have local changes not on the remote: + * (glob) + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + saved backup bundle to $TESTTMP/narrow-local-changes/.hg/strip-backup/*-narrow.hg (glob) + deleting data/d3/f.i + $ hg id + 000000000000 + $ cd .. + +Can remove last include, making repo empty + $ hg clone --narrow ssh://user@dummy/master narrow-empty --include d0 -r 5 + adding changesets + adding manifests + adding file changes + added 2 changesets with 1 changes to 1 files + new changesets *:* (glob) + updating to branch default + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd narrow-empty + $ hg tracked --removeinclude d0 + comparing with ssh://user@dummy/master + searching for changes + looking for local changes to affected paths + deleting data/d0/f.i + $ hg tracked + $ hg files + [1] + $ test -d d0 + [1] +Do some work in the empty clone + $ hg diff --change . + $ hg branch foo + marked working directory as branch foo + (branches are permanent and global, did you want a bookmark?) + $ hg ci -m empty + $ hg pull -q +Can widen the empty clone + $ hg tracked --addinclude d0 + comparing with ssh://user@dummy/master + searching for changes + no changes found + saved backup bundle to $TESTTMP/narrow-empty/.hg/strip-backup/*-widen.hg (glob) + adding changesets + adding manifests + adding file changes + added 3 changesets with 1 changes to 1 files + new changesets *:* (glob) + $ hg tracked + I path:d0 + $ hg files + d0/f + $ find * + d0 + d0/f + $ cd .. + +TODO(martinvonz): test including e.g. d3/g and then removing it once +https://bitbucket.org/Google/narrowhg/issues/6 is fixed + + $ hg clone --narrow ssh://user@dummy/master narrow --include d0 --include d3 --include d6 --include d9 + requesting all changes + adding changesets + adding manifests + adding file changes + added 8 changesets with 4 changes to 4 files + new changesets *:* (glob) + updating to branch default + 4 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd narrow + $ hg tracked + I path:d0 + I path:d3 + I path:d6 + I path:d9 + $ hg tracked --removeinclude d6 + comparing with ssh://user@dummy/master + searching for changes + looking for local changes to affected paths + deleting data/d6/f.i + $ hg tracked + I path:d0 + I path:d3 + I path:d9 + $ hg debugrebuildfncache + fncache already up to date + $ find * + d0 + d0/f + d3 + d3/f + d9 + d9/f + $ hg verify -q + $ hg tracked --addexclude d3/f + comparing with ssh://user@dummy/master + searching for changes + looking for local changes to affected paths + deleting data/d3/f.i + $ hg tracked + I path:d0 + I path:d3 + I path:d9 + X path:d3/f + $ hg debugrebuildfncache + fncache already up to date + $ find * + d0 + d0/f + d9 + d9/f + $ hg verify -q + $ hg tracked --addexclude d0 + comparing with ssh://user@dummy/master + searching for changes + looking for local changes to affected paths + deleting data/d0/f.i + $ hg tracked + I path:d3 + I path:d9 + X path:d0 + X path:d3/f + $ hg debugrebuildfncache + fncache already up to date + $ find * + d9 + d9/f + +Make a 15 of changes to d9 to test the path without --verbose +(Note: using regexes instead of "* (glob)" because if the test fails, it +produces more sensible diffs) + $ hg tracked + I path:d3 + I path:d9 + X path:d0 + X path:d3/f + $ for x in `$TESTDIR/seq.py 1 15` + > do + > echo local change >> d9/f + > hg commit -m "change $x to d9/f" + > done + $ hg tracked --removeinclude d9 + comparing with ssh://user@dummy/master + searching for changes + looking for local changes to affected paths + The following changeset(s) or their ancestors have local changes not on the remote: + ^[0-9a-f]{12}$ (re) + ^[0-9a-f]{12}$ (re) + ^[0-9a-f]{12}$ (re) + ^[0-9a-f]{12}$ (re) + ^[0-9a-f]{12}$ (re) + ^[0-9a-f]{12}$ (re) + ^[0-9a-f]{12}$ (re) + ^[0-9a-f]{12}$ (re) + ^[0-9a-f]{12}$ (re) + ^[0-9a-f]{12}$ (re) + ...and 5 more, use --verbose to list all + abort: local changes found + (use --force-delete-local-changes to ignore) + [255] +Now test it *with* verbose. + $ hg tracked --removeinclude d9 --verbose + comparing with ssh://user@dummy/master + searching for changes + looking for local changes to affected paths + The following changeset(s) or their ancestors have local changes not on the remote: + ^[0-9a-f]{12}$ (re) + ^[0-9a-f]{12}$ (re) + ^[0-9a-f]{12}$ (re) + ^[0-9a-f]{12}$ (re) + ^[0-9a-f]{12}$ (re) + ^[0-9a-f]{12}$ (re) + ^[0-9a-f]{12}$ (re) + ^[0-9a-f]{12}$ (re) + ^[0-9a-f]{12}$ (re) + ^[0-9a-f]{12}$ (re) + ^[0-9a-f]{12}$ (re) + ^[0-9a-f]{12}$ (re) + ^[0-9a-f]{12}$ (re) + ^[0-9a-f]{12}$ (re) + ^[0-9a-f]{12}$ (re) + abort: local changes found + (use --force-delete-local-changes to ignore) + [255]