diff --git a/hgext3rd/sparse.py b/hgext3rd/sparse.py deleted file mode 100644 --- a/hgext3rd/sparse.py +++ /dev/null @@ -1,1215 +0,0 @@ -# sparse.py - allow sparse checkouts of the working directory -# -# Copyright 2014 Facebook, Inc. -# -# This software may be used and distributed according to the terms of the -# GNU General Public License version 2 or any later version. - -"""allow sparse checkouts of the working directory -""" - -from mercurial import util, cmdutil, extensions, context, dirstate, commands -from mercurial import localrepo, error, hg, pathutil, registrar, patch -from mercurial import match as matchmod -from mercurial import merge as mergemod -from mercurial.node import nullid -from mercurial.i18n import _ -import os, collections, hashlib - -cmdtable = {} -command = registrar.command(cmdtable) -testedwith = 'ships-with-fb-hgext' - -cwdrealtivepatkinds = ('glob', 'relpath') - -def _fbsparseexists(ui): - # internal config: extensions.fbsparse - with ui.configoverride({("devel", "all-warnings"): False}): - return not ui.config('extensions', 'fbsparse', '!').startswith('!') - -def uisetup(ui): - if _fbsparseexists(ui): - cmdtable.clear() - return - _setupupdates(ui) - _setupcommit(ui) - -def extsetup(ui): - if _fbsparseexists(ui): - cmdtable.clear() - return - _setupclone(ui) - _setuplog(ui) - _setupadd(ui) - _setupdirstate(ui) - _setupdiff(ui) - # if fsmonitor is enabled, tell it to use our hash function - try: - fsmonitor = extensions.find('fsmonitor') - def _hashignore(orig, ignore): - return _hashmatcher(ignore) - extensions.wrapfunction(fsmonitor, '_hashignore', _hashignore) - except KeyError: - pass - # do the same for hgwatchman, old name - try: - hgwatchman = extensions.find('hgwatchman') - def _hashignore(orig, ignore): - return _hashmatcher(ignore) - extensions.wrapfunction(hgwatchman, '_hashignore', _hashignore) - except KeyError: - pass - -def reposetup(ui, repo): - if _fbsparseexists(repo.ui): - return - if not util.safehasattr(repo, 'dirstate'): - return - - _wraprepo(ui, repo) - -def replacefilecache(cls, propname, replacement): - """Replace a filecache property with a new class. This allows changing the - cache invalidation condition.""" - origcls = cls - assert callable(replacement) - while cls is not object: - if propname in cls.__dict__: - orig = cls.__dict__[propname] - setattr(cls, propname, replacement(orig)) - break - cls = cls.__bases__[0] - - if cls is object: - raise AttributeError(_("type '%s' has no property '%s'") % (origcls, - propname)) - -def _setupupdates(ui): - def _calculateupdates(orig, repo, wctx, mctx, ancestors, branchmerge, *arg, - **kwargs): - """Filter updates to only lay out files that match the sparse rules. - """ - actions, diverge, renamedelete = orig(repo, wctx, mctx, ancestors, - branchmerge, *arg, **kwargs) - - if not util.safehasattr(repo, 'sparsematch'): - return actions, diverge, renamedelete - - files = set() - prunedactions = {} - oldrevs = [pctx.rev() for pctx in wctx.parents()] - oldsparsematch = repo.sparsematch(*oldrevs) - - if branchmerge: - # If we're merging, use the wctx filter, since we're merging into - # the wctx. - sparsematch = repo.sparsematch(wctx.parents()[0].rev()) - else: - # If we're updating, use the target context's filter, since we're - # moving to the target context. - sparsematch = repo.sparsematch(mctx.rev()) - - temporaryfiles = [] - for file, action in actions.iteritems(): - type, args, msg = action - files.add(file) - if sparsematch(file): - prunedactions[file] = action - elif type == 'm': - temporaryfiles.append(file) - prunedactions[file] = action - elif branchmerge: - if type != 'k': - temporaryfiles.append(file) - prunedactions[file] = action - elif type == 'f': - prunedactions[file] = action - elif file in wctx: - prunedactions[file] = ('r', args, msg) - - if len(temporaryfiles) > 0: - ui.status(_("temporarily included %d file(s) in the sparse checkout" - " for merging\n") % len(temporaryfiles)) - repo.addtemporaryincludes(temporaryfiles) - - # Add the new files to the working copy so they can be merged, etc - actions = [] - message = 'temporarily adding to sparse checkout' - wctxmanifest = repo[None].manifest() - for file in temporaryfiles: - if file in wctxmanifest: - fctx = repo[None][file] - actions.append((file, (fctx.flags(), False), message)) - - typeactions = collections.defaultdict(list) - typeactions['g'] = actions - mergemod.applyupdates(repo, typeactions, repo[None], repo['.'], - False) - - dirstate = repo.dirstate - for file, flags, msg in actions: - dirstate.normal(file) - - profiles = repo.getactiveprofiles() - changedprofiles = profiles & files - # If an active profile changed during the update, refresh the checkout. - # Don't do this during a branch merge, since all incoming changes should - # have been handled by the temporary includes above. - if changedprofiles and not branchmerge: - mf = mctx.manifest() - for file in mf: - old = oldsparsematch(file) - new = sparsematch(file) - if not old and new: - flags = mf.flags(file) - prunedactions[file] = ('g', (flags, False), '') - elif old and not new: - prunedactions[file] = ('r', [], '') - - return prunedactions, diverge, renamedelete - - extensions.wrapfunction(mergemod, 'calculateupdates', _calculateupdates) - - def _update(orig, repo, node, branchmerge, *args, **kwargs): - results = orig(repo, node, branchmerge, *args, **kwargs) - - # If we're updating to a location, clean up any stale temporary includes - # (ex: this happens during hg rebase --abort). - if not branchmerge and util.safehasattr(repo, 'sparsematch'): - repo.prunetemporaryincludes() - return results - - extensions.wrapfunction(mergemod, 'update', _update) - -def _setupcommit(ui): - def _refreshoncommit(orig, self, node): - """Refresh the checkout when commits touch .hgsparse - """ - orig(self, node) - repo = self._repo - if util.safehasattr(repo, 'getsparsepatterns'): - ctx = repo[node] - _, _, profiles = repo.getsparsepatterns(ctx.rev()) - if set(profiles) & set(ctx.files()): - origstatus = repo.status() - origsparsematch = repo.sparsematch() - _refresh(repo.ui, repo, origstatus, origsparsematch, True) - - repo.prunetemporaryincludes() - - extensions.wrapfunction(context.committablectx, 'markcommitted', - _refreshoncommit) - -def _setuplog(ui): - entry = commands.table['^log|history'] - entry[1].append(('', 'sparse', None, - "limit to changesets affecting the sparse checkout")) - - def _logrevs(orig, repo, opts): - revs = orig(repo, opts) - if opts.get('sparse'): - sparsematch = repo.sparsematch() - def ctxmatch(rev): - ctx = repo[rev] - return any(f for f in ctx.files() if sparsematch(f)) - revs = revs.filter(ctxmatch) - return revs - extensions.wrapfunction(cmdutil, '_logrevs', _logrevs) - -def _clonesparsecmd(orig, ui, repo, *args, **opts): - include_pat = opts.get('include') - exclude_pat = opts.get('exclude') - enableprofile_pat = opts.get('enable_profile') - include = exclude = enableprofile = False - if include_pat: - pat = include_pat - include = True - if exclude_pat: - pat = exclude_pat - exclude = True - if enableprofile_pat: - pat = enableprofile_pat - enableprofile = True - if sum([include, exclude, enableprofile]) > 1: - raise error.Abort(_("too many flags specified.")) - if include or exclude or enableprofile: - def clone_sparse(orig, self, node, overwrite, *args, **kwargs): - # sparse clone is a special snowflake as in that case always - # are outside of the repo's dir hierachy, yet we always want - # to name our includes/excludes/enables using repo-root - # relative paths - overrides = { - ('sparse', 'includereporootpaths'): True, - ('sparse', 'enablereporootpaths'): True, - } - with self.ui.configoverride(overrides, 'sparse'): - _config(self.ui, self.unfiltered(), pat, {}, include=include, - exclude=exclude, enableprofile=enableprofile) - return orig(self, node, overwrite, *args, **kwargs) - extensions.wrapfunction(hg, 'updaterepo', clone_sparse) - return orig(ui, repo, *args, **opts) - -def _setupclone(ui): - entry = commands.table['^clone'] - entry[1].append(('', 'enable-profile', [], - 'enable a sparse profile')) - entry[1].append(('', 'include', [], - 'include sparse pattern')) - entry[1].append(('', 'exclude', [], - 'exclude sparse pattern')) - extensions.wrapcommand(commands.table, 'clone', _clonesparsecmd) - -def _setupadd(ui): - entry = commands.table['^add'] - entry[1].append(('s', 'sparse', None, - 'also include directories of added files in sparse config')) - - def _add(orig, ui, repo, *pats, **opts): - if opts.get('sparse'): - dirs = set() - for pat in pats: - dirname, basename = util.split(pat) - dirs.add(dirname) - _config(ui, repo, list(dirs), opts, include=True) - return orig(ui, repo, *pats, **opts) - - extensions.wrapcommand(commands.table, 'add', _add) - -def _setupdirstate(ui): - """Modify the dirstate to prevent stat'ing excluded files, - and to prevent modifications to files outside the checkout. - """ - - def _dirstate(orig, repo): - dirstate = orig(repo) - dirstate.repo = repo - return dirstate - extensions.wrapfunction( - localrepo.localrepository.dirstate, 'func', _dirstate) - - # The atrocity below is needed to wrap dirstate._ignore. It is a cached - # property, which means normal function wrapping doesn't work. - class ignorewrapper(object): - def __init__(self, orig): - self.orig = orig - self.origignore = None - self.func = None - self.sparsematch = None - - def __get__(self, obj, type=None): - repo = obj.repo - origignore = self.orig.__get__(obj) - if not util.safehasattr(repo, 'sparsematch'): - return origignore - - sparsematch = repo.sparsematch() - if self.sparsematch != sparsematch or self.origignore != origignore: - self.func = unionmatcher([origignore, - negatematcher(sparsematch)]) - self.sparsematch = sparsematch - self.origignore = origignore - return self.func - - def __set__(self, obj, value): - return self.orig.__set__(obj, value) - - def __delete__(self, obj): - return self.orig.__delete__(obj) - - replacefilecache(dirstate.dirstate, '_ignore', ignorewrapper) - - # dirstate.rebuild should not add non-matching files - def _rebuild(orig, self, parent, allfiles, changedfiles=None): - if util.safehasattr(self.repo, 'sparsematch'): - matcher = self.repo.sparsematch() - allfiles = allfiles.matches(matcher) - if changedfiles: - changedfiles = [f for f in changedfiles if matcher(f)] - - if changedfiles is not None: - # In _rebuild, these files will be deleted from the dirstate - # when they are not found to be in allfiles - dirstatefilestoremove = set(f for f in self if not matcher(f)) - changedfiles = dirstatefilestoremove.union(changedfiles) - - return orig(self, parent, allfiles, changedfiles) - extensions.wrapfunction(dirstate.dirstate, 'rebuild', _rebuild) - - # Prevent adding files that are outside the sparse checkout - editfuncs = ['normal', 'add', 'normallookup', 'copy', 'remove', 'merge'] - hint = _('include file with `hg sparse --include ` or use ' + - '`hg add -s ` to include file directory while adding') - for func in editfuncs: - def _wrapper(orig, self, *args): - repo = self.repo - if util.safehasattr(repo, 'sparsematch'): - dirstate = repo.dirstate - sparsematch = repo.sparsematch() - for f in args: - if (f is not None and not sparsematch(f) and - f not in dirstate): - raise error.Abort(_("cannot add '%s' - it is outside " - "the sparse checkout") % f, - hint=hint) - return orig(self, *args) - extensions.wrapfunction(dirstate.dirstate, func, _wrapper) - -def _setupdiff(ui): - entry = commands.table['^diff'] - entry[1].append(('s', 'sparse', None, - 'only show changes in files in the sparse config')) - # wrap workingfilectx's data function to return the data for files - # outside the sparse checkout by fetching from the working copy parent. - def workingfilectxdata(orig, self): - if not util.safehasattr(self.repo(), 'sparsematch'): - return orig(self) - sparsematch = self.repo().sparsematch() - if sparsematch(self._path): - return orig(self) - else: - basectx = self._changectx._parents[0] - return basectx[self._path].data() - extensions.wrapfunction(context.workingfilectx, 'data', workingfilectxdata) - - # wrap trydiff to filter diffs if '--sparse' is set - def trydiff(orig, repo, revs, ctx1, ctx2, modified, added, removed, - copy, getfilectx, opts, losedatafn, prefix, relroot): - sparsematch = repo.sparsematch() - modified = filter(sparsematch, modified) - added = filter(sparsematch, added) - removed = filter(sparsematch, removed) - copy = dict((d, s) for d, s in copy.items() if sparsematch(s)) - return orig(repo, revs, ctx1, ctx2, modified, added, removed, - copy, getfilectx, opts, losedatafn, prefix, relroot) - def diff(orig, ui, repo, *pats, **opts): - issparse = bool(opts.get('sparse')) - if issparse: - extensions.wrapfunction(patch, 'trydiff', trydiff) - try: - orig(ui, repo, *pats, **opts) - finally: - if issparse: - extensions.unwrapfunction(patch, 'trydiff', trydiff) - extensions.wrapcommand(commands.table, 'diff', diff) - -def _wraprepo(ui, repo): - class SparseRepo(repo.__class__): - def readsparseconfig(self, raw): - """Takes a string sparse config and returns the includes, - excludes, and profiles it specified. - """ - includes = set() - excludes = set() - current = includes - profiles = [] - for line in raw.split('\n'): - line = line.strip() - if not line or line.startswith('#'): - # empty or comment line, skip - continue - elif line.startswith('%include '): - line = line[9:].strip() - if line: - profiles.append(line) - elif line == '[include]': - if current != includes: - raise error.Abort(_('.hg/sparse cannot have includes ' + - 'after excludes')) - continue - elif line == '[exclude]': - current = excludes - elif line: - if line.strip().startswith('/'): - self.ui.warn(_('warning: sparse profile cannot use' + - ' paths starting with /, ignoring %s\n') - % line) - continue - current.add(line) - - return includes, excludes, profiles - - def getsparsepatterns(self, rev): - """Returns the include/exclude patterns specified by the - given rev. - """ - if not self.vfs.exists('sparse'): - return set(), set(), [] - if rev is None: - raise error.Abort(_("cannot parse sparse patterns from " + - "working copy")) - - raw = self.vfs.read('sparse') - includes, excludes, profiles = self.readsparseconfig(raw) - - ctx = self[rev] - if profiles: - visited = set() - while profiles: - profile = profiles.pop() - if profile in visited: - continue - visited.add(profile) - - try: - raw = self.getrawprofile(profile, rev) - except error.ManifestLookupError: - msg = ( - "warning: sparse profile '%s' not found " - "in rev %s - ignoring it\n" % (profile, ctx)) - if self.ui.configbool('sparse', 'missingwarning'): - self.ui.warn(msg) - else: - self.ui.debug(msg) - continue - pincludes, pexcludes, subprofs = \ - self.readsparseconfig(raw) - includes.update(pincludes) - excludes.update(pexcludes) - for subprofile in subprofs: - profiles.append(subprofile) - - profiles = visited - - if includes: - includes.add('.hg*') - return includes, excludes, profiles - - def getrawprofile(self, profile, changeid): - try: - simplecache = extensions.find('simplecache') - node = self[changeid].hex() - def func(): - return self.filectx(profile, changeid=changeid).data() - key = 'sparseprofile:%s:%s' % (profile.replace('/', '__'), node) - return simplecache.memoize(func, key, - simplecache.stringserializer, self.ui) - except KeyError: - return self.filectx(profile, changeid=changeid).data() - - def sparsechecksum(self, filepath): - fh = open(filepath) - return hashlib.sha1(fh.read()).hexdigest() - - def _sparsesignature(self, includetemp=True): - """Returns the signature string representing the contents of the - current project sparse configuration. This can be used to cache the - sparse matcher for a given set of revs.""" - signaturecache = self.signaturecache - signature = signaturecache.get('signature') - if includetemp: - tempsignature = signaturecache.get('tempsignature') - else: - tempsignature = 0 - - if signature is None or (includetemp and tempsignature is None): - signature = 0 - try: - sparsepath = self.vfs.join('sparse') - signature = self.sparsechecksum(sparsepath) - except (OSError, IOError): - pass - signaturecache['signature'] = signature - - tempsignature = 0 - if includetemp: - try: - tempsparsepath = self.vfs.join('tempsparse') - tempsignature = self.sparsechecksum(tempsparsepath) - except (OSError, IOError): - pass - signaturecache['tempsignature'] = tempsignature - return '%s %s' % (str(signature), str(tempsignature)) - - def invalidatecaches(self): - self.invalidatesignaturecache() - return super(SparseRepo, self).invalidatecaches() - - def invalidatesignaturecache(self): - self.signaturecache.clear() - - def sparsematch(self, *revs, **kwargs): - """Returns the sparse match function for the given revs. - - If multiple revs are specified, the match function is the union - of all the revs. - - `includetemp` is used to indicate if the temporarily included file - should be part of the matcher. - """ - if not revs or revs == (None,): - revs = [self.changelog.rev(node) for node in - self.dirstate.parents() if node != nullid] - - includetemp = kwargs.get('includetemp', True) - signature = self._sparsesignature(includetemp=includetemp) - - key = '%s %s' % (str(signature), ' '.join([str(r) for r in revs])) - - result = self.sparsecache.get(key, None) - if result: - return result - - matchers = [] - for rev in revs: - try: - includes, excludes, profiles = self.getsparsepatterns(rev) - - if includes or excludes: - # Explicitly include subdirectories of includes so - # status will walk them down to the actual include. - subdirs = set() - for include in includes: - dirname = os.path.dirname(include) - # basename is used to avoid issues with absolute - # paths (which on Windows can include the drive). - while os.path.basename(dirname): - subdirs.add(dirname) - dirname = os.path.dirname(dirname) - - matcher = matchmod.match(self.root, '', [], - include=includes, exclude=excludes, - default='relpath') - if subdirs: - matcher = forceincludematcher(matcher, subdirs) - matchers.append(matcher) - except IOError: - pass - - result = None - if not matchers: - result = matchmod.always(self.root, '') - elif len(matchers) == 1: - result = matchers[0] - else: - result = unionmatcher(matchers) - - if kwargs.get('includetemp', True): - tempincludes = self.gettemporaryincludes() - result = forceincludematcher(result, tempincludes) - - self.sparsecache[key] = result - - return result - - def getactiveprofiles(self): - revs = [self.changelog.rev(node) for node in - self.dirstate.parents() if node != nullid] - - activeprofiles = set() - for rev in revs: - _, _, profiles = self.getsparsepatterns(rev) - activeprofiles.update(profiles) - - return activeprofiles - - def writesparseconfig(self, include, exclude, profiles): - raw = '%s[include]\n%s\n[exclude]\n%s\n' % ( - ''.join(['%%include %s\n' % p for p in sorted(profiles)]), - '\n'.join(sorted(include)), - '\n'.join(sorted(exclude))) - self.vfs.write("sparse", raw) - self.invalidatesignaturecache() - - def addtemporaryincludes(self, files): - includes = self.gettemporaryincludes() - for file in files: - includes.add(file) - self._writetemporaryincludes(includes) - - def gettemporaryincludes(self): - existingtemp = set() - if self.vfs.exists('tempsparse'): - raw = self.vfs.read('tempsparse') - existingtemp.update(raw.split('\n')) - return existingtemp - - def _writetemporaryincludes(self, includes): - raw = '\n'.join(sorted(includes)) - self.vfs.write('tempsparse', raw) - self.invalidatesignaturecache() - - def prunetemporaryincludes(self): - if repo.vfs.exists('tempsparse'): - origstatus = self.status() - modified, added, removed, deleted, a, b, c = origstatus - if modified or added or removed or deleted: - # Still have pending changes. Don't bother trying to prune. - return - - sparsematch = self.sparsematch(includetemp=False) - dirstate = self.dirstate - actions = [] - dropped = [] - tempincludes = self.gettemporaryincludes() - for file in tempincludes: - if file in dirstate and not sparsematch(file): - message = 'dropping temporarily included sparse files' - actions.append((file, None, message)) - dropped.append(file) - - typeactions = collections.defaultdict(list) - typeactions['r'] = actions - mergemod.applyupdates(self, typeactions, self[None], self['.'], - False) - - # Fix dirstate - for file in dropped: - dirstate.drop(file) - - self.vfs.unlink('tempsparse') - self.invalidatesignaturecache() - msg = _("cleaned up %d temporarily added file(s) from the " - "sparse checkout\n") - ui.status(msg % len(tempincludes)) - - if 'dirstate' in repo._filecache: - repo.dirstate.repo = repo - repo.sparsecache = {} - repo.signaturecache = {} - repo.__class__ = SparseRepo - -@command('^sparse', [ - ('I', 'include', False, _('include files in the sparse checkout')), - ('X', 'exclude', False, _('exclude files in the sparse checkout')), - ('d', 'delete', False, _('delete an include/exclude rule')), - ('f', 'force', False, _('allow changing rules even with pending changes')), - ('', 'enable-profile', False, _('enables the specified profile')), - ('', 'disable-profile', False, _('disables the specified profile')), - ('', 'import-rules', False, _('imports rules from a file')), - ('', 'clear-rules', False, _('clears local include/exclude rules')), - ('', 'refresh', False, _('updates the working after sparseness changes')), - ('', 'reset', False, _('makes the repo full again')), - ('', 'cwd-list', False, _('list the full contents of the current ' - 'directory')), - ] + commands.templateopts, - _('[--OPTION] PATTERN...')) -def sparse(ui, repo, *pats, **opts): - """make the current checkout sparse, or edit the existing checkout - - The sparse command is used to make the current checkout sparse. - This means files that don't meet the sparse condition will not be - written to disk, or show up in any working copy operations. It does - not affect files in history in any way. - - Passing no arguments prints the currently applied sparse rules. - - --include and --exclude are used to add and remove files from the sparse - checkout. The effects of adding an include or exclude rule are applied - immediately. If applying the new rule would cause a file with pending - changes to be added or removed, the command will fail. Pass --force to - force a rule change even with pending changes (the changes on disk will - be preserved). - - --delete removes an existing include/exclude rule. The effects are - immediate. - - --refresh refreshes the files on disk based on the sparse rules. This is - only necessary if .hg/sparse was changed by hand. - - --enable-profile and --disable-profile accept a path to a .hgsparse file. - This allows defining sparse checkouts and tracking them inside the - repository. This is useful for defining commonly used sparse checkouts for - many people to use. As the profile definition changes over time, the sparse - checkout will automatically be updated appropriately, depending on which - changeset is checked out. Changes to .hgsparse are not applied until they - have been committed. - - --import-rules accepts a path to a file containing rules in the .hgsparse - format, allowing you to add --include, --exclude and --enable-profile rules - in bulk. Like the --include, --exclude and --enable-profile switches, the - changes are applied immediately. - - --clear-rules removes all local include and exclude rules, while leaving - any enabled profiles in place. - - --cwd-list list all the contents of the current directory. The files that - are excluded by the current sparse checkout are annotated with a hyphen - ('-') before the name. - - The following config option defines whether sparse treats supplied - paths as relative to repo root or to the current working dir for - include and exclude options: - - [sparse] - includereporootpaths = off - - The following config option defines whether sparse treats supplied - paths as relative to repo root or to the current working dir for - enableprofile and disableprofile options: - - [sparse] - enablereporootpaths = on - - Returns 0 if editing the sparse checkout succeeds. - """ - include = opts.get('include') - exclude = opts.get('exclude') - force = opts.get('force') - enableprofile = opts.get('enable_profile') - disableprofile = opts.get('disable_profile') - importrules = opts.get('import_rules') - clearrules = opts.get('clear_rules') - delete = opts.get('delete') - refresh = opts.get('refresh') - reset = opts.get('reset') - cwdlist = opts.get('cwd_list') - count = sum([include, exclude, enableprofile, disableprofile, delete, - importrules, refresh, clearrules, reset, cwdlist]) - if count > 1: - raise error.Abort(_("too many flags specified")) - - if count == 0: - if repo.vfs.exists('sparse'): - ui.status(repo.vfs.read("sparse") + "\n") - temporaryincludes = repo.gettemporaryincludes() - if temporaryincludes: - ui.status(_("Temporarily Included Files (for merge/rebase):\n")) - ui.status(("\n".join(temporaryincludes) + "\n")) - else: - ui.status(_('repo is not sparse\n')) - return - - if include or exclude or delete or reset or enableprofile or disableprofile: - _config(ui, repo, pats, opts, include=include, exclude=exclude, - reset=reset, delete=delete, enableprofile=enableprofile, - disableprofile=disableprofile, force=force) - - if importrules: - _import(ui, repo, pats, opts, force=force) - - if clearrules: - _clear(ui, repo, pats, force=force) - - if refresh: - try: - wlock = repo.wlock() - fcounts = map( - len, - _refresh(ui, repo, repo.status(), repo.sparsematch(), force)) - _verbose_output(ui, opts, 0, 0, 0, *fcounts) - finally: - wlock.release() - - if cwdlist: - _cwdlist(repo) - -def _config(ui, repo, pats, opts, include=False, exclude=False, reset=False, - delete=False, enableprofile=False, disableprofile=False, - force=False): - """ - Perform a sparse config update. Only one of the kwargs may be specified. - """ - wlock = repo.wlock() - try: - oldsparsematch = repo.sparsematch() - - if repo.vfs.exists('sparse'): - raw = repo.vfs.read('sparse') - oldinclude, oldexclude, oldprofiles = map( - set, repo.readsparseconfig(raw)) - else: - oldinclude = set() - oldexclude = set() - oldprofiles = set() - - try: - if reset: - newinclude = set() - newexclude = set() - newprofiles = set() - else: - newinclude = set(oldinclude) - newexclude = set(oldexclude) - newprofiles = set(oldprofiles) - - if any(os.path.isabs(pat) for pat in pats): - err = _('paths cannot be absolute') - raise error.Abort(err) - - adjustpats = ((include or exclude or delete) and - not ui.configbool('sparse', 'includereporootpaths', False)) - adjustpats |= ((enableprofile or disableprofile) and - not ui.configbool('sparse', 'enablereporootpaths', True)) - if adjustpats: - # supplied file patterns should be treated as relative - # to current working dir, so we need to convert them first - root, cwd = repo.root, repo.getcwd() - abspats = [] - for kindpat in pats: - kind, pat = matchmod._patsplit(kindpat, None) - if kind in cwdrealtivepatkinds or kind is None: - kindpat = ((kind + ':' if kind else '') + - pathutil.canonpath(root, cwd, pat)) - abspats.append(kindpat) - pats = abspats - - oldstatus = repo.status() - if include: - newinclude.update(pats) - elif exclude: - newexclude.update(pats) - elif enableprofile: - newprofiles.update(pats) - elif disableprofile: - newprofiles.difference_update(pats) - elif delete: - newinclude.difference_update(pats) - newexclude.difference_update(pats) - - repo.writesparseconfig(newinclude, newexclude, newprofiles) - fcounts = map( - len, _refresh(ui, repo, oldstatus, oldsparsematch, force)) - - profilecount = (len(newprofiles - oldprofiles) - - len(oldprofiles - newprofiles)) - includecount = (len(newinclude - oldinclude) - - len(oldinclude - newinclude)) - excludecount = (len(newexclude - oldexclude) - - len(oldexclude - newexclude)) - _verbose_output( - ui, opts, profilecount, includecount, excludecount, *fcounts) - except Exception: - repo.writesparseconfig(oldinclude, oldexclude, oldprofiles) - raise - finally: - wlock.release() - -def _import(ui, repo, files, opts, force=False): - with repo.wlock(): - # load union of current active profile - revs = [repo.changelog.rev(node) for node in - repo.dirstate.parents() if node != nullid] - - # read current configuration - raw = '' - if repo.vfs.exists('sparse'): - raw = repo.vfs.read('sparse') - oincludes, oexcludes, oprofiles = repo.readsparseconfig(raw) - includes, excludes, profiles = map( - set, (oincludes, oexcludes, oprofiles)) - - # all active rules - aincludes, aexcludes, aprofiles = set(), set(), set() - for rev in revs: - rincludes, rexcludes, rprofiles = repo.getsparsepatterns(rev) - aincludes.update(rincludes) - aexcludes.update(rexcludes) - aprofiles.update(rprofiles) - - # import rules on top; only take in rules that are not yet - # part of the active rules. - changed = False - for file in files: - with util.posixfile(util.expandpath(file)) as importfile: - iincludes, iexcludes, iprofiles = repo.readsparseconfig( - importfile.read()) - oldsize = len(includes) + len(excludes) + len(profiles) - includes.update(iincludes - aincludes) - excludes.update(iexcludes - aexcludes) - profiles.update(set(iprofiles) - aprofiles) - if len(includes) + len(excludes) + len(profiles) > oldsize: - changed = True - - profilecount = includecount = excludecount = 0 - fcounts = (0, 0, 0) - - if changed: - profilecount = len(profiles - aprofiles) - includecount = len(includes - aincludes) - excludecount = len(excludes - aexcludes) - - oldstatus = repo.status() - oldsparsematch = repo.sparsematch() - repo.writesparseconfig(includes, excludes, profiles) - - try: - fcounts = map( - len, _refresh(ui, repo, oldstatus, oldsparsematch, force)) - except Exception: - repo.writesparseconfig(oincludes, oexcludes, oprofiles) - raise - - _verbose_output(ui, opts, profilecount, includecount, excludecount, - *fcounts) - -def _clear(ui, repo, files, force=False): - with repo.wlock(): - raw = '' - if repo.vfs.exists('sparse'): - raw = repo.vfs.read('sparse') - includes, excludes, profiles = repo.readsparseconfig(raw) - - if includes or excludes: - oldstatus = repo.status() - oldsparsematch = repo.sparsematch() - repo.writesparseconfig(set(), set(), profiles) - _refresh(ui, repo, oldstatus, oldsparsematch, force) - -def _refresh(ui, repo, origstatus, origsparsematch, force): - """Refreshes which files are on disk by comparing the old status and - sparsematch with the new sparsematch. - - Will raise an exception if a file with pending changes is being excluded - or included (unless force=True). - """ - modified, added, removed, deleted, unknown, ignored, clean = origstatus - - # Verify there are no pending changes - pending = set() - pending.update(modified) - pending.update(added) - pending.update(removed) - sparsematch = repo.sparsematch() - abort = False - for file in pending: - if not sparsematch(file): - ui.warn(_("pending changes to '%s'\n") % file) - abort = not force - if abort: - raise error.Abort(_("could not update sparseness due to " + - "pending changes")) - - # Calculate actions - dirstate = repo.dirstate - ctx = repo['.'] - added = [] - lookup = [] - dropped = [] - mf = ctx.manifest() - files = set(mf) - - actions = {} - - for file in files: - old = origsparsematch(file) - new = sparsematch(file) - # Add files that are newly included, or that don't exist in - # the dirstate yet. - if (new and not old) or (old and new and not file in dirstate): - fl = mf.flags(file) - if repo.wvfs.exists(file): - actions[file] = ('e', (fl,), '') - lookup.append(file) - else: - actions[file] = ('g', (fl, False), '') - added.append(file) - # Drop files that are newly excluded, or that still exist in - # the dirstate. - elif (old and not new) or (not old and not new and file in dirstate): - dropped.append(file) - if file not in pending: - actions[file] = ('r', [], '') - - # Verify there are no pending changes in newly included files - abort = False - for file in lookup: - ui.warn(_("pending changes to '%s'\n") % file) - abort = not force - if abort: - raise error.Abort(_("cannot change sparseness due to " + - "pending changes (delete the files or use --force " + - "to bring them back dirty)")) - - # Check for files that were only in the dirstate. - for file, state in dirstate.iteritems(): - if not file in files: - old = origsparsematch(file) - new = sparsematch(file) - if old and not new: - dropped.append(file) - - # Apply changes to disk - typeactions = dict((m, []) - for m in 'a f g am cd dc r dm dg m e k p pr'.split()) - for f, (m, args, msg) in actions.iteritems(): - if m not in typeactions: - typeactions[m] = [] - typeactions[m].append((f, args, msg)) - mergemod.applyupdates(repo, typeactions, repo[None], repo['.'], False) - - # Fix dirstate - for file in added: - dirstate.normal(file) - - for file in dropped: - dirstate.drop(file) - - for file in lookup: - # File exists on disk, and we're bringing it back in an unknown state. - dirstate.normallookup(file) - - return added, dropped, lookup - -def _verbose_output(ui, opts, profilecount, includecount, excludecount, added, - dropped, lookup): - """Produce --verbose and templatable output - - This specifically enables -Tjson, providing machine-readable stats on how - the sparse profile changed. - - """ - with ui.formatter('sparse', opts) as fm: - fm.startitem() - fm.condwrite(ui.verbose, 'profiles_added', 'Profile # change: %d\n', - profilecount) - fm.condwrite(ui.verbose, 'include_rules_added', - 'Include rule # change: %d\n', includecount) - fm.condwrite(ui.verbose, 'exclude_rules_added', - 'Exclude rule # change: %d\n', excludecount) - # In 'plain' verbose mode, mergemod.applyupdates already outputs what - # files are added or removed outside of the templating formatter - # framework. No point in repeating ourselves in that case. - if not fm.isplain(): - fm.condwrite(ui.verbose, 'files_added', 'Files added: %d\n', - added) - fm.condwrite(ui.verbose, 'files_dropped', 'Files dropped: %d\n', - dropped) - fm.condwrite(ui.verbose, 'files_conflicting', - 'Files conflicting: %d\n', lookup) - -def _cwdlist(repo): - """ List the contents in the current directory. Annotate - the files in the sparse profile. - """ - ctx = repo['.'] - mf = ctx.manifest() - cwd = util.normpath(os.getcwd()) - - # Get the root of the repo so that we remove the content of - # the root from the current working directory - root = repo.root - if cwd.startswith(root): - cwd = cwd[len(root):] - else: - raise error.Abort(_("the current working directory should begin " + - "with the root %s") % root) - - cwd = cwd.strip("/") - sparsematch = repo.sparsematch(ctx.rev()) - checkedoutentries = set() - allentries = set() - cwdlength = len(cwd) + 1 - for filepath in mf: - if filepath.startswith(cwd): - tail = filepath[cwdlength:] if cwdlength > 1 else filepath - entryname = tail.split('/', 1)[0] - - allentries.add(entryname) - if sparsematch(filepath): - checkedoutentries.add(entryname) - - ui = repo.ui - for entry in sorted(allentries): - marker = ' ' if entry in checkedoutentries else '-' - ui.status("%s %s\n" % (marker, entry)) - -class forceincludematcher(object): - """A matcher that returns true for any of the forced includes before testing - against the actual matcher.""" - def __init__(self, matcher, includes): - self._matcher = matcher - self._includes = includes - - def __call__(self, value): - return value in self._includes or self._matcher(value) - - def always(self): - return False - - def files(self): - return [] - - def isexact(self): - return False - - def anypats(self): - return True - - def prefix(self): - return False - - def visitdir(self, dir): - if any(True for path in self._includes if path.startswith(dir)): - return True - return self._matcher.visitdir(dir) - - def hash(self): - sha1 = hashlib.sha1() - sha1.update(_hashmatcher(self._matcher)) - for include in sorted(self._includes): - sha1.update(include + '\0') - return sha1.hexdigest() - -class unionmatcher(object): - """A matcher that is the union of several matchers.""" - def __init__(self, matchers): - self._matchers = matchers - - def __call__(self, value): - for match in self._matchers: - if match(value): - return True - return False - - def always(self): - return False - - def files(self): - return [] - - def isexact(self): - return False - - def anypats(self): - return True - - def prefix(self): - return False - - def visitdir(self, dir): - for match in self._matchers: - if match.visitdir(dir): - return True - return False - - def hash(self): - sha1 = hashlib.sha1() - for m in self._matchers: - sha1.update(_hashmatcher(m)) - return sha1.hexdigest() - -class negatematcher(object): - def __init__(self, matcher): - self._matcher = matcher - - def __call__(self, value): - return not self._matcher(value) - - def always(self): - return False - - def files(self): - return [] - - def isexact(self): - return False - - def anypats(self): - return True - - def visitdir(self, dir): - return True - - def hash(self): - sha1 = hashlib.sha1() - sha1.update('negate') - sha1.update(_hashmatcher(self._matcher)) - return sha1.hexdigest() - -def _hashmatcher(matcher): - if util.safehasattr(matcher, 'hash'): - return matcher.hash() - - sha1 = hashlib.sha1() - sha1.update(repr(matcher)) - return sha1.hexdigest() diff --git a/tests/test-check-py3-compat-hg.t b/tests/test-check-py3-compat-hg.t --- a/tests/test-check-py3-compat-hg.t +++ b/tests/test-check-py3-compat-hg.t @@ -61,7 +61,6 @@ hgext3rd/sampling.py not using absolute_import hgext3rd/sigtrace.py not using absolute_import hgext3rd/simplecache.py not using absolute_import - hgext3rd/sparse.py not using absolute_import hgext3rd/sshaskpass.py not using absolute_import hgext3rd/stat.py not using absolute_import hgext3rd/upgradegeneraldelta.py not using absolute_import diff --git a/tests/test-sparse-clear.t b/tests/test-sparse-clear.t deleted file mode 100644 --- a/tests/test-sparse-clear.t +++ /dev/null @@ -1,73 +0,0 @@ -test sparse - - $ hg init myrepo - $ cd myrepo - $ cat >> $HGRCPATH < [extensions] - > sparse=$TESTDIR/../hgext3rd/fbsparse.py - > purge= - > strip= - > rebase= - > EOF - - $ echo a > index.html - $ echo x > data.py - $ echo z > readme.txt - $ cat > base.sparse < [include] - > *.sparse - > EOF - $ hg ci -Aqm 'initial' - $ cat > webpage.sparse < %include base.sparse - > [include] - > *.html - > EOF - $ hg ci -Aqm 'initial' - -Clear rules when there are includes - - $ hg sparse --include *.py - $ ls - data.py - $ hg sparse --clear-rules - $ ls - base.sparse - data.py - index.html - readme.txt - webpage.sparse - -Clear rules when there are excludes - - $ hg sparse --exclude *.sparse - $ ls - data.py - index.html - readme.txt - $ hg sparse --clear-rules - $ ls - base.sparse - data.py - index.html - readme.txt - webpage.sparse - -Clearing rules should not alter profiles - - $ hg sparse --enable-profile webpage.sparse - $ ls - base.sparse - index.html - webpage.sparse - $ hg sparse --include *.py - $ ls - base.sparse - data.py - index.html - webpage.sparse - $ hg sparse --clear-rules - $ ls - base.sparse - index.html - webpage.sparse diff --git a/tests/test-sparse-clone.t b/tests/test-sparse-clone.t deleted file mode 100644 --- a/tests/test-sparse-clone.t +++ /dev/null @@ -1,72 +0,0 @@ -test sparse - - $ cat >> $HGRCPATH << EOF - > [ui] - > ssh = python "$RUNTESTDIR/dummyssh" - > username = nobody - > [extensions] - > sparse=$TESTDIR/../hgext3rd/fbsparse.py - > purge= - > strip= - > rebase= - > EOF - - $ hg init myrepo - $ cd myrepo - $ echo a > index.html - $ echo x > data.py - $ echo z > readme.txt - $ cat > webpage.sparse < [include] - > *.html - > EOF - $ cat > backend.sparse < [include] - > *.py - > EOF - $ hg ci -Aqm 'initial' - $ cd .. - -Verify local clone with a sparse profile works - - $ hg clone --enable-profile webpage.sparse myrepo clone1 - updating to branch default - warning: sparse profile 'webpage.sparse' not found in rev 000000000000 - ignoring it - 1 files updated, 0 files merged, 0 files removed, 0 files unresolved - $ cd clone1 - $ ls - index.html - $ cd .. - -Verify local clone with include works - - $ hg clone --include *.sparse myrepo clone2 - updating to branch default - 2 files updated, 0 files merged, 0 files removed, 0 files unresolved - $ cd clone2 - $ ls - backend.sparse - webpage.sparse - $ cd .. - -Verify local clone with exclude works - - $ hg clone --exclude data.py myrepo clone3 - updating to branch default - 4 files updated, 0 files merged, 0 files removed, 0 files unresolved - $ cd clone3 - $ ls - backend.sparse - index.html - readme.txt - webpage.sparse - $ cd .. - -Verify sparse clone profile over ssh works - - $ hg clone -q --enable-profile webpage.sparse ssh://user@dummy/myrepo clone4 - warning: sparse profile 'webpage.sparse' not found in rev 000000000000 - ignoring it - $ cd clone4 - $ ls - index.html - $ cd .. diff --git a/tests/test-sparse-diff.t b/tests/test-sparse-diff.t deleted file mode 100644 --- a/tests/test-sparse-diff.t +++ /dev/null @@ -1,105 +0,0 @@ - $ hg init repo - $ cd repo - $ cat > .hg/hgrc < [extensions] - > sparse=$TESTDIR/../hgext3rd/fbsparse.py - > EOF - $ mkdir show hide - $ echo show-modify-1 > show/modify - $ echo show-remove-1 > show/remove - $ echo hide-modify-1 > hide/modify - $ echo hide-remove-1 > hide/remove - $ echo show-moveout > show/moveout - $ echo show-movein > hide/movein - $ hg add show/modify show/remove hide/modify hide/remove show/moveout hide/movein - $ hg commit -m "first revision" - $ echo show-modify-2 > show/modify - $ echo show-add-2 > show/add ; hg add show/add - $ hg rm show/remove - $ echo hide-modify-2 > hide/modify - $ echo hide-add-2 > hide/add ; hg add hide/add - $ hg rm hide/remove - $ hg mv hide/movein show/movein - $ hg mv show/moveout hide/moveout - $ hg commit -m "second revision" - $ hg sparse --exclude hide - -Run diff. This should still show the file contents of excluded files (and should not crash). - - $ hg diff -r ".^" --git - diff --git a/hide/add b/hide/add - new file mode 100644 - --- /dev/null - +++ b/hide/add - @@ -0,0 +1,1 @@ - +hide-add-2 - diff --git a/hide/modify b/hide/modify - --- a/hide/modify - +++ b/hide/modify - @@ -1,1 +1,1 @@ - -hide-modify-1 - +hide-modify-2 - diff --git a/show/moveout b/hide/moveout - rename from show/moveout - rename to hide/moveout - diff --git a/hide/remove b/hide/remove - deleted file mode 100644 - --- a/hide/remove - +++ /dev/null - @@ -1,1 +0,0 @@ - -hide-remove-1 - diff --git a/show/add b/show/add - new file mode 100644 - --- /dev/null - +++ b/show/add - @@ -0,0 +1,1 @@ - +show-add-2 - diff --git a/show/modify b/show/modify - --- a/show/modify - +++ b/show/modify - @@ -1,1 +1,1 @@ - -show-modify-1 - +show-modify-2 - diff --git a/hide/movein b/show/movein - rename from hide/movein - rename to show/movein - diff --git a/show/remove b/show/remove - deleted file mode 100644 - --- a/show/remove - +++ /dev/null - @@ -1,1 +0,0 @@ - -show-remove-1 - -Run diff --sparse. This should only show files within the sparse profile. - - $ hg diff --sparse --git -r ".^" - diff --git a/show/add b/show/add - new file mode 100644 - --- /dev/null - +++ b/show/add - @@ -0,0 +1,1 @@ - +show-add-2 - diff --git a/show/modify b/show/modify - --- a/show/modify - +++ b/show/modify - @@ -1,1 +1,1 @@ - -show-modify-1 - +show-modify-2 - diff --git a/show/movein b/show/movein - new file mode 100644 - --- /dev/null - +++ b/show/movein - @@ -0,0 +1,1 @@ - +show-movein - diff --git a/show/moveout b/show/moveout - deleted file mode 100644 - --- a/show/moveout - +++ /dev/null - @@ -1,1 +0,0 @@ - -show-moveout - diff --git a/show/remove b/show/remove - deleted file mode 100644 - --- a/show/remove - +++ /dev/null - @@ -1,1 +0,0 @@ - -show-remove-1 diff --git a/tests/test-sparse-extensions.t b/tests/test-sparse-extensions.t deleted file mode 100644 --- a/tests/test-sparse-extensions.t +++ /dev/null @@ -1,56 +0,0 @@ -test sparse interaction with other extensions - - $ hg init myrepo - $ cd myrepo - $ cat > .hg/hgrc < [extensions] - > sparse=$TESTDIR/../hgext3rd/fbsparse.py - > strip= - > [simplecache] - > cachedir=$TESTTMP/hgsimplecache - > EOF - -Test integration with simplecache for profile reads - - $ $PYTHON -c 'import hgext3rd.simplecache' || exit 80 - $ printf "[include]\nfoo\n" > .hgsparse - $ hg add .hgsparse - $ hg commit -qm 'Add profile' - $ hg sparse --enable-profile .hgsparse - $ hg status --debug --config extensions.simplecache= - falling back for value sparseprofile:.hgsparse:52fe6c0958d7d08df53bdf7ee62a261abb7f599e:v1 - set value for key sparseprofile:.hgsparse:52fe6c0958d7d08df53bdf7ee62a261abb7f599e:v1 to local - $ hg status --debug --config extensions.simplecache= - got value for key sparseprofile:.hgsparse:52fe6c0958d7d08df53bdf7ee62a261abb7f599e:v1 from local - -Test fsmonitor integration (if available) -TODO: make fully isolated integration test a'la https://github.com/facebook/watchman/blob/master/tests/integration/WatchmanInstance.py -(this one is using the systemwide watchman instance) - - $ touch .watchmanconfig - $ $PYTHON -c 'import hgext.fsmonitor' || exit 80 - $ echo "ignoredir1/" >> .hgignore - $ hg commit -Am ignoredir1 - adding .hgignore - $ echo "ignoredir2/" >> .hgignore - $ hg commit -m ignoredir2 - - $ hg sparse --reset - $ hg sparse -I ignoredir1 -I ignoredir2 -I dir1 - - $ mkdir ignoredir1 ignoredir2 dir1 - $ touch ignoredir1/file ignoredir2/file dir1/file - -Run status twice to compensate for a condition in fsmonitor where it will check -ignored files the second time it runs, regardless of previous state (ask @sid0) - $ hg status --config extensions.fsmonitor= - ? dir1/file - $ hg status --config extensions.fsmonitor= - ? dir1/file - -Test that fsmonitor ignore hash check updates when .hgignore changes - - $ hg up -q ".^" - $ hg status --config extensions.fsmonitor= - ? dir1/file - ? ignoredir2/file diff --git a/tests/test-sparse-import.t b/tests/test-sparse-import.t deleted file mode 100644 --- a/tests/test-sparse-import.t +++ /dev/null @@ -1,186 +0,0 @@ -test sparse - - $ hg init myrepo - $ cd myrepo - $ cat >> $HGRCPATH < [extensions] - > sparse=$TESTDIR/../hgext3rd/fbsparse.py - > purge= - > strip= - > rebase= - > EOF - - $ echo a > index.html - $ echo x > data.py - $ echo z > readme.txt - $ cat > base.sparse < [include] - > *.sparse - > EOF - $ hg ci -Aqm 'initial' - $ cat > webpage.sparse < %include base.sparse - > [include] - > *.html - > EOF - $ hg ci -Aqm 'initial' - -Import a rules file against a 'blank' sparse profile - - $ cat > $TESTTMP/rules_to_import < [include] - > *.py - > EOF - $ hg sparse --import-rules $TESTTMP/rules_to_import - $ ls - data.py - - $ hg sparse --reset - $ rm .hg/sparse - - $ cat > $TESTTMP/rules_to_import < %include base.sparse - > [include] - > *.py - > EOF - $ hg sparse --import-rules $TESTTMP/rules_to_import - $ ls - base.sparse - data.py - webpage.sparse - - $ hg sparse --reset - $ rm .hg/sparse - -Start against an existing profile; rules *already active* should be ignored - - $ hg sparse --enable-profile webpage.sparse - $ hg sparse --include *.py - $ cat > $TESTTMP/rules_to_import < %include base.sparse - > [include] - > *.html - > *.txt - > [exclude] - > *.py - > EOF - $ hg sparse --import-rules $TESTTMP/rules_to_import - $ ls - base.sparse - index.html - readme.txt - webpage.sparse - $ cat .hg/sparse - %include webpage.sparse - [include] - *.py - *.txt - [exclude] - *.py - - $ hg sparse --reset - $ rm .hg/sparse - -Same tests, with -Tjson enabled to output summaries - - $ cat > $TESTTMP/rules_to_import < [include] - > *.py - > EOF - $ hg sparse --import-rules $TESTTMP/rules_to_import -Tjson - [ - { - "exclude_rules_added": 0, - "files_added": 0, - "files_conflicting": 0, - "files_dropped": 4, - "include_rules_added": 1, - "profiles_added": 0 - } - ] - - $ hg sparse --reset - $ rm .hg/sparse - - $ cat > $TESTTMP/rules_to_import < %include base.sparse - > [include] - > *.py - > EOF - $ hg sparse --import-rules $TESTTMP/rules_to_import -Tjson - [ - { - "exclude_rules_added": 0, - "files_added": 0, - "files_conflicting": 0, - "files_dropped": 2, - "include_rules_added": 1, - "profiles_added": 1 - } - ] - - $ hg sparse --reset - $ rm .hg/sparse - - $ hg sparse --enable-profile webpage.sparse - $ hg sparse --include *.py - $ cat > $TESTTMP/rules_to_import < %include base.sparse - > [include] - > *.html - > *.txt - > [exclude] - > *.py - > EOF - $ hg sparse --import-rules $TESTTMP/rules_to_import -Tjson - [ - { - "exclude_rules_added": 1, - "files_added": 1, - "files_conflicting": 0, - "files_dropped": 1, - "include_rules_added": 1, - "profiles_added": 0 - } - ] - -If importing results in no new rules being added, no refresh should take place! - - $ cat > $TESTTMP/trap_sparse_refresh.py < from mercurial import error, extensions - > def extsetup(ui): - > def abort_refresh(ui, *args): - > raise error.Abort('sparse._refresh called!') - > def sparseloaded(loaded): - > if not loaded: - > return - > sparse = extensions.find('sparse') - > sparse._refresh = abort_refresh - > extensions.afterloaded('sparse', sparseloaded) - > EOF - $ cat >> $HGRCPATH < [extensions] - > trap_sparse_refresh=$TESTTMP/trap_sparse_refresh.py - > EOF - $ cat > $TESTTMP/rules_to_import < [include] - > *.py - > EOF - $ hg sparse --import-rules $TESTTMP/rules_to_import - -If an exception is raised during refresh, restore the existing rules again. - - $ cat > $TESTTMP/rules_to_import < [exclude] - > *.html - > EOF - $ hg sparse --import-rules $TESTTMP/rules_to_import - abort: sparse._refresh called! - [255] - $ cat .hg/sparse - %include webpage.sparse - [include] - *.py - *.txt - [exclude] - *.py diff --git a/tests/test-sparse-merges.t b/tests/test-sparse-merges.t deleted file mode 100644 --- a/tests/test-sparse-merges.t +++ /dev/null @@ -1,62 +0,0 @@ -test merging things outside of the sparse checkout - - $ hg init myrepo - $ cd myrepo - $ cat > .hg/hgrc < [extensions] - > sparse=$TESTDIR/../hgext3rd/fbsparse.py - > EOF - - $ echo foo > foo - $ echo bar > bar - $ hg add foo bar - $ hg commit -m initial - - $ hg branch feature - marked working directory as branch feature - (branches are permanent and global, did you want a bookmark?) - $ echo bar2 >> bar - $ hg commit -m 'feature - bar2' - - $ hg update -q default - $ hg sparse --exclude 'bar**' - - $ hg merge feature - temporarily included 1 file(s) in the sparse checkout for merging - 1 files updated, 0 files merged, 0 files removed, 0 files unresolved - (branch merge, don't forget to commit) - -Verify bar was merged temporarily - - $ ls - bar - foo - $ hg status - M bar - -Verify bar disappears automatically when the working copy becomes clean - - $ hg commit -m "merged" - cleaned up 1 temporarily added file(s) from the sparse checkout - $ hg status - $ ls - foo - - $ hg cat -r . bar - bar - bar2 - -Test merging things outside of the sparse checkout that are not in the working -copy - - $ hg strip -q -r . --config extensions.strip= - $ hg up -q feature - $ touch branchonly - $ hg ci -Aqm 'add branchonly' - - $ hg up -q default - $ hg sparse -X branchonly - $ hg merge feature - temporarily included 2 file(s) in the sparse checkout for merging - 2 files updated, 0 files merged, 0 files removed, 0 files unresolved - (branch merge, don't forget to commit) diff --git a/tests/test-sparse-notsparse.t b/tests/test-sparse-notsparse.t deleted file mode 100644 --- a/tests/test-sparse-notsparse.t +++ /dev/null @@ -1,41 +0,0 @@ -Make sure the sparse extension does not break functionality when it gets -loaded in a non-sparse repository. - -First create a base repository with sparse enabled. - - $ hg init base - $ cd base - $ cat > .hg/hgrc < [extensions] - > sparse=$TESTDIR/../hgext3rd/fbsparse.py - > journal= - > EOF - - $ echo a > file1 - $ echo x > file2 - $ hg ci -Aqm 'initial' - $ cd .. - -Now create a shared working copy that is not sparse. - - $ hg --config extensions.share= share base shared - updating working directory - 2 files updated, 0 files merged, 0 files removed, 0 files unresolved - $ cd shared - $ cat > .hg/hgrc < [extensions] - > share= - > sparse=! - > journal= - > EOF - -Make sure "hg diff" works in the non-sparse working directory. - - $ echo z >> file1 - $ hg diff - diff -r 1f02e070b36e file1 - --- a/file1 Thu Jan 01 00:00:00 1970 +0000 - +++ b/file1 Thu Jan 01 00:00:00 1970 +0000 - @@ -1,1 +1,2 @@ - a - +z diff --git a/tests/test-sparse-profiles.t b/tests/test-sparse-profiles.t deleted file mode 100644 --- a/tests/test-sparse-profiles.t +++ /dev/null @@ -1,351 +0,0 @@ -test sparse - - $ hg init myrepo - $ cd myrepo - $ cat > .hg/hgrc < [extensions] - > sparse=$TESTDIR/../hgext3rd/fbsparse.py - > purge= - > strip= - > rebase= - > EOF - - $ echo a > index.html - $ echo x > data.py - $ echo z > readme.txt - $ cat > webpage.sparse < # frontend sparse profile - > [include] - > *.html - > EOF - $ cat > backend.sparse < # backend sparse profile - > [include] - > *.py - > EOF - $ hg ci -Aqm 'initial' - - $ hg sparse --include '*.sparse' - -Verify enabling a single profile works - - $ hg sparse --enable-profile webpage.sparse - $ ls - backend.sparse - index.html - webpage.sparse - -Verify enabling two profiles works - - $ hg sparse --enable-profile backend.sparse - $ ls - backend.sparse - data.py - index.html - webpage.sparse - -Verify disabling a profile works - - $ hg sparse --disable-profile webpage.sparse - $ ls - backend.sparse - data.py - webpage.sparse - - -Verify that a profile is updated across multiple commits - - $ cat > webpage.sparse < # frontend sparse profile - > [include] - > *.html - > EOF - $ cat > backend.sparse < # backend sparse profile - > [include] - > *.py - > *.txt - > EOF - - $ echo foo >> data.py - - $ hg ci -m 'edit profile' - $ ls - backend.sparse - data.py - readme.txt - webpage.sparse - - $ hg up -q 0 - $ ls - backend.sparse - data.py - webpage.sparse - - $ hg up -q 1 - $ ls - backend.sparse - data.py - readme.txt - webpage.sparse - -Introduce a conflicting .hgsparse change - - $ hg up -q 0 - $ cat > backend.sparse < # Different backend sparse profile - > [include] - > *.html - > EOF - $ echo bar >> data.py - - $ hg ci -qAm "edit profile other" - $ ls - backend.sparse - index.html - webpage.sparse - -Verify conflicting merge pulls in the conflicting changes - - $ hg merge 1 - temporarily included 1 file(s) in the sparse checkout for merging - merging backend.sparse - merging data.py - warning: conflicts while merging backend.sparse! (edit, then use 'hg resolve --mark') - warning: conflicts while merging data.py! (edit, then use 'hg resolve --mark') - 0 files updated, 0 files merged, 0 files removed, 2 files unresolved - use 'hg resolve' to retry unresolved file merges or 'hg update -C .' to abandon - [1] - - $ rm *.orig - $ ls - backend.sparse - data.py - index.html - webpage.sparse - -Verify resolving the merge removes the temporarily unioned files - - $ cat > backend.sparse < # backend sparse profile - > [include] - > *.html - > *.txt - > EOF - $ hg resolve -m backend.sparse - - $ cat > data.py < x - > foo - > bar - > EOF - $ hg resolve -m data.py - (no more unresolved files) - - $ hg ci -qAm "merge profiles" - $ ls - backend.sparse - index.html - readme.txt - webpage.sparse - - $ hg cat -r . data.py - x - foo - bar - -Verify stripping refreshes dirstate - - $ hg strip -q -r . - $ ls - backend.sparse - index.html - webpage.sparse - -Verify rebase conflicts pulls in the conflicting changes - - $ hg up -q 1 - $ ls - backend.sparse - data.py - readme.txt - webpage.sparse - - $ hg rebase -d 2 - rebasing 1:a2b1de640a62 "edit profile" - temporarily included 1 file(s) in the sparse checkout for merging - merging backend.sparse - merging data.py - warning: conflicts while merging backend.sparse! (edit, then use 'hg resolve --mark') - warning: conflicts while merging data.py! (edit, then use 'hg resolve --mark') - unresolved conflicts (see hg resolve, then hg rebase --continue) - [1] - $ rm *.orig - $ ls - backend.sparse - data.py - index.html - webpage.sparse - -Verify resolving conflict removes the temporary files - - $ cat > backend.sparse < [include] - > *.html - > *.txt - > EOF - $ hg resolve -m backend.sparse - - $ cat > data.py < x - > foo - > bar - > EOF - $ hg resolve -m data.py - (no more unresolved files) - continue: hg rebase --continue - - $ hg rebase -q --continue - $ ls - backend.sparse - index.html - readme.txt - webpage.sparse - - $ hg cat -r . data.py - x - foo - bar - -Test checking out a commit that does not contain the sparse profile. The -warning message can be suppressed by setting missingwarning = false in -[sparse] section of your config: - - $ hg sparse --reset - $ hg rm *.sparse - $ hg commit -m "delete profiles" - $ hg up -q ".^" - $ hg sparse --enable-profile backend.sparse - $ ls - index.html - readme.txt - $ hg up tip | grep warning - warning: sparse profile 'backend.sparse' not found in rev bfcb76de99cc - ignoring it - [1] - $ ls - data.py - index.html - readme.txt - $ hg sparse --disable-profile backend.sparse | grep warning - warning: sparse profile 'backend.sparse' not found in rev bfcb76de99cc - ignoring it - [1] - $ cat >> .hg/hgrc < [sparse] - > missingwarning = false - > EOF - $ hg sparse --enable-profile backend.sparse - - $ cd .. - -Test file permissions changing across a sparse profile change - $ hg init sparseperm - $ cd sparseperm - $ cat > .hg/hgrc < [extensions] - > sparse=$TESTDIR/../hgext3rd/fbsparse.py - > EOF - $ touch a b - $ cat > .hgsparse < a - > EOF - $ hg commit -Aqm 'initial' - $ chmod a+x b - $ hg commit -qm 'make executable' - $ cat >> .hgsparse < b - > EOF - $ hg commit -qm 'update profile' - $ hg up -q 0 - $ hg sparse --enable-profile .hgsparse - $ hg up -q 2 - $ ls -l b - -rwxr-xr-x* b (glob) - - $ cd .. - -Test profile discovery - $ hg init sparseprofiles - $ cd sparseprofiles - $ cat > .hg/hgrc < [extensions] - > sparse=$TESTDIR/../hgext3rd/fbsparse.py - > EOF - $ mkdir -p profiles/foo profiles/bar - $ touch profiles/README.txt - $ cat > profiles/foo/spam < %include profiles/bar/eggs - > EOF - $ cat > profiles/bar/eggs < [include] - > profiles - > EOF - $ touch profiles/foo/monty - $ touch profiles/bar/python - $ hg add -q profiles - $ hg commit -qm 'created profiles' - $ hg sparse --enable-profile profiles/foo/spam - $ hg sparse --list-profiles - symbols: * = active profile, ~ = transitively included - ~ profiles/bar/eggs - * profiles/foo/spam - $ hg sparse --list-profiles -T json - [ - { - "active": "included", - "path": "profiles/bar/eggs" - }, - { - "active": "active", - "path": "profiles/foo/spam" - } - ] - $ cat >> .hg/hgrc < [sparse] - > profile_directory = profiles/ - > EOF - $ hg sparse --list-profiles - symbols: * = active profile, ~ = transitively included - ~ profiles/bar/eggs - profiles/bar/python - profiles/foo/monty - * profiles/foo/spam - $ hg sparse --list-profiles -T json - [ - { - "active": "included", - "path": "profiles/bar/eggs" - }, - { - "active": "inactive", - "path": "profiles/bar/python" - }, - { - "active": "inactive", - "path": "profiles/foo/monty" - }, - { - "active": "active", - "path": "profiles/foo/spam" - } - ] - -Profiles are loaded from the manifest, so excluding a profile directory should -not hamper listing. - - $ hg sparse --exclude profiles/bar - $ hg sparse --list-profiles - symbols: * = active profile, ~ = transitively included - ~ profiles/bar/eggs - profiles/bar/python - profiles/foo/monty - * profiles/foo/spam diff --git a/tests/test-sparse-rebase.t b/tests/test-sparse-rebase.t deleted file mode 100644 --- a/tests/test-sparse-rebase.t +++ /dev/null @@ -1,29 +0,0 @@ - $ hg init repo - $ cd repo - $ cat > .hg/hgrc < [extensions] - > sparse=$TESTDIR/../hgext3rd/fbsparse.py - > dragdag=$RUNTESTDIR/drawdag.py - > rebase= - > EOF - - $ hg debugdrawdag <<'EOS' - > D - > | - > B C - > |/ - > A - > EOS - - $ hg sparse --exclude A B C D E - $ hg update A -q - $ printf D > D - $ echo 2 > E - $ hg rebase -s C -d B - rebasing 2:dc0947a82db8 "C" (C) - temporarily included 1 file(s) in the sparse checkout for merging - cleaned up 1 temporarily added file(s) from the sparse checkout - rebasing 3:e7b3f00ed42e "D" (D tip) - temporarily included 1 file(s) in the sparse checkout for merging - cleaned up 1 temporarily added file(s) from the sparse checkout - saved backup bundle to $TESTTMP/repo/.hg/strip-backup/dc0947a82db8-b8481714-rebase.hg (glob) diff --git a/tests/test-sparse-verbose-json.t b/tests/test-sparse-verbose-json.t deleted file mode 100644 --- a/tests/test-sparse-verbose-json.t +++ /dev/null @@ -1,82 +0,0 @@ -test sparse with --verbose and -T json - - $ hg init myrepo - $ cd myrepo - $ cat > .hg/hgrc < [extensions] - > sparse=$TESTDIR/../hgext3rd/fbsparse.py - > strip= - > EOF - - $ echo a > show - $ echo x > hide - $ hg ci -Aqm 'initial' - - $ echo b > show - $ echo y > hide - $ echo aa > show2 - $ echo xx > hide2 - $ hg ci -Aqm 'two' - -Verify basic --include and --reset - - $ hg up -q 0 - $ hg sparse --include 'hide' -Tjson - [ - { - "exclude_rules_added": 0, - "files_added": 0, - "files_conflicting": 0, - "files_dropped": 1, - "include_rules_added": 1, - "profiles_added": 0 - } - ] - $ hg sparse --clear-rules - $ hg sparse --include 'hide' --verbose - removing show - Profile # change: 0 - Include rule # change: 1 - Exclude rule # change: 0 - - $ hg sparse --reset -Tjson - [ - { - "exclude_rules_added": 0, - "files_added": 1, - "files_conflicting": 0, - "files_dropped": 0, - "include_rules_added": -1, - "profiles_added": 0 - } - ] - $ hg sparse --include 'hide' - $ hg sparse --reset --verbose - getting show - Profile # change: 0 - Include rule # change: -1 - Exclude rule # change: 0 - -Verifying that problematic files still allow us to see the deltas when forcing: - - $ hg sparse --include 'show*' - $ touch hide - $ hg sparse --delete 'show*' --force -Tjson - pending changes to 'hide' - [ - { - "exclude_rules_added": 0, - "files_added": 0, - "files_conflicting": 1, - "files_dropped": 0, - "include_rules_added": -1, - "profiles_added": 0 - } - ] - $ hg sparse --include 'show*' --force - pending changes to 'hide' - $ hg sparse --delete 'show*' --force --verbose - pending changes to 'hide' - Profile # change: 0 - Include rule # change: -1 - Exclude rule # change: 0 diff --git a/tests/test-sparse.t b/tests/test-sparse.t deleted file mode 100644 --- a/tests/test-sparse.t +++ /dev/null @@ -1,433 +0,0 @@ -test sparse - - $ hg init myrepo - $ cd myrepo - $ cat > .hg/hgrc < [extensions] - > sparse=$TESTDIR/../hgext3rd/fbsparse.py - > strip= - > EOF - - $ echo a > show - $ echo x > hide - $ hg ci -Aqm 'initial' - - $ echo b > show - $ echo y > hide - $ echo aa > show2 - $ echo xx > hide2 - $ hg ci -Aqm 'two' - -Verify basic --include - - $ hg up -q 0 - $ hg sparse --include 'hide' - $ ls - hide - -Absolute paths outside the repo should just be rejected - - $ hg sparse --include /foo/bar - abort: paths cannot be absolute - [255] - $ hg sparse --include '$TESTTMP/myrepo/hide' - - $ hg sparse --include '/root' - abort: paths cannot be absolute - [255] - -Repo root-relaive vs. cwd-relative includes - $ mkdir subdir - $ cd subdir - $ hg sparse --config sparse.includereporootpaths=on --include notinsubdir/path - $ hg sparse --config sparse.includereporootpaths=off --include **/path - $ hg sparse --config sparse.includereporootpaths=off --include path:abspath - $ hg sparse - [include] - $TESTTMP/myrepo/hide - hide - notinsubdir/path - path:abspath - subdir/**/path - [exclude] - - - $ cd .. - $ rm -rf subdir - -Verify deleting uses relative paths - $ mkdir subdir && echo foo > subdir/foo - $ hg sparse - [include] - $TESTTMP/myrepo/hide - hide - notinsubdir/path - path:abspath - subdir/**/path - [exclude] - - - $ cd subdir - $ hg sparse --delete **/path - $ hg sparse - [include] - $TESTTMP/myrepo/hide - hide - notinsubdir/path - path:abspath - [exclude] - - - $ cd .. - $ rm -rf subdir - -Verify commiting while sparse includes other files - - $ echo z > hide - $ hg ci -Aqm 'edit hide' - $ ls - hide - $ hg manifest - hide - show - -Verify --reset brings files back - - $ hg sparse --reset - $ ls - hide - show - $ cat hide - z - $ cat show - a - -Verify 'hg sparse' default output - - $ hg up -q null - $ hg sparse --include 'show*' - - $ hg sparse - [include] - show* - [exclude] - - -Verify update only writes included files - - $ hg up -q 0 - $ ls - show - - $ hg up -q 1 - $ ls - show - show2 - -Verify status only shows included files - - $ touch hide - $ touch hide3 - $ echo c > show - $ hg status - M show - -Adding an excluded file should fail - - $ hg add hide3 - abort: cannot add 'hide3' - it is outside the sparse checkout - (include file with `hg sparse --include ` or use `hg add -s ` to include file directory while adding) - [255] - -Verify deleting sparseness while a file has changes fails - - $ hg sparse --delete 'show*' - pending changes to 'hide' - abort: cannot change sparseness due to pending changes (delete the files or use --force to bring them back dirty) - [255] - -Verify deleting sparseness with --force brings back files - - $ hg sparse --delete -f 'show*' - pending changes to 'hide' - $ ls - hide - hide2 - hide3 - show - show2 - $ hg st - M hide - M show - ? hide3 - -Verify editing sparseness fails if pending changes - - $ hg sparse --include 'show*' - pending changes to 'hide' - abort: could not update sparseness due to pending changes - [255] - -Verify adding sparseness hides files - - $ hg sparse --exclude -f 'hide*' - pending changes to 'hide' - $ ls - hide - hide3 - show - show2 - $ hg st - M show - - $ hg up -qC . - $ hg purge --all --config extensions.purge= - $ ls - show - show2 - -Verify rebase temporarily includes excluded files - - $ hg rebase -d 1 -r 2 --config extensions.rebase= - rebasing 2:b91df4f39e75 "edit hide" (tip) - temporarily included 1 file(s) in the sparse checkout for merging - merging hide - warning: conflicts while merging hide! (edit, then use 'hg resolve --mark') - unresolved conflicts (see hg resolve, then hg rebase --continue) - [1] - - $ hg sparse - [include] - - [exclude] - hide* - - Temporarily Included Files (for merge/rebase): - hide - - $ cat hide - <<<<<<< dest: 39278f7c08a9 - test: two - y - ======= - z - >>>>>>> source: b91df4f39e75 - test: edit hide - -Verify aborting a rebase cleans up temporary files - - $ hg rebase --abort --config extensions.rebase= - cleaned up 1 temporarily added file(s) from the sparse checkout - rebase aborted - $ rm hide.orig - - $ ls - show - show2 - -Verify merge fails if merging excluded files - - $ hg up -q 1 - $ hg merge -r 2 - temporarily included 1 file(s) in the sparse checkout for merging - merging hide - warning: conflicts while merging hide! (edit, then use 'hg resolve --mark') - 0 files updated, 0 files merged, 0 files removed, 1 files unresolved - use 'hg resolve' to retry unresolved file merges or 'hg update -C .' to abandon - [1] - $ hg sparse - [include] - - [exclude] - hide* - - Temporarily Included Files (for merge/rebase): - hide - - $ hg up -C . - cleaned up 1 temporarily added file(s) from the sparse checkout - 1 files updated, 0 files merged, 0 files removed, 0 files unresolved - $ hg sparse - [include] - - [exclude] - hide* - - -Verify strip -k resets dirstate correctly - - $ hg status - $ hg sparse - [include] - - [exclude] - hide* - - $ hg log -r . -T '{rev}\n' --stat - 1 - hide | 2 +- - hide2 | 1 + - show | 2 +- - show2 | 1 + - 4 files changed, 4 insertions(+), 2 deletions(-) - - $ hg strip -r . -k - saved backup bundle to $TESTTMP/myrepo/.hg/strip-backup/39278f7c08a9-ce59e002-backup.hg (glob) - $ hg status - M show - ? show2 - -Verify rebase succeeds if all changed files are in sparse checkout - - $ hg commit -Aqm "add show2" - $ hg rebase -d 1 --config extensions.rebase= - rebasing 2:bdde55290160 "add show2" (tip) - saved backup bundle to $TESTTMP/myrepo/.hg/strip-backup/bdde55290160-216ed9c6-rebase.hg (glob) - -Verify log --sparse only shows commits that affect the sparse checkout - - $ hg log -T '{rev} ' - 2 1 0 (no-eol) - $ hg log --sparse -T '{rev} ' - 2 0 (no-eol) - -Test status on a file in a subdir - - $ mkdir -p dir1/dir2 - $ touch dir1/dir2/file - $ hg sparse -I dir1/dir2 - $ hg status - ? dir1/dir2/file - -Test that add -s adds dirs to sparse profile - - $ hg sparse --reset - $ hg sparse --include empty - $ hg sparse - [include] - empty - [exclude] - - - - $ mkdir add - $ touch add/foo - $ touch add/bar - $ hg add add/foo - abort: cannot add 'add/foo' - it is outside the sparse checkout - (include file with `hg sparse --include ` or use `hg add -s ` to include file directory while adding) - [255] - $ hg add -s add/foo - $ hg st - A add/foo - ? add/bar - $ hg sparse - [include] - add - empty - [exclude] - - - $ hg add -s add/* - add/foo already tracked! - $ hg st - A add/bar - A add/foo - $ hg sparse - [include] - add - empty - [exclude] - - -Test --cwd-list - $ hg commit -m 'commit' - $ hg sparse --cwd-list - add - - hide - - show - - show2 - $ cd add - $ hg sparse --cwd-list - bar - foo - $ hg sparse -I foo - $ hg sparse --delete . - $ hg sparse --cwd-list - - bar - foo - $ cd .. - - $ cd .. - -Test non-sparse repos work while sparse is loaded - $ hg init sparserepo - $ hg init nonsparserepo - $ cd sparserepo - $ cat > .hg/hgrc < [extensions] - > sparse=$TESTDIR/../hgext3rd/fbsparse.py - > EOF - $ cd ../nonsparserepo - $ echo x > x && hg add x && hg commit -qAm x - $ cd ../sparserepo - $ hg clone ../nonsparserepo ../nonsparserepo2 - updating to branch default - 1 files updated, 0 files merged, 0 files removed, 0 files unresolved - -Test debugrebuilddirstate - $ cd ../sparserepo - $ touch included - $ touch excluded - $ hg add included excluded - $ hg commit -m 'a commit' -q - $ cp .hg/dirstate ../dirstateboth - $ hg sparse -X excluded - $ cp ../dirstateboth .hg/dirstate - $ hg debugrebuilddirstate - $ hg debugdirstate - n 0 -1 unset included - -Test debugdirstate --minimal where file is in the parent manifest but not the -dirstate - $ hg sparse -X included - $ hg debugdirstate - $ cp .hg/dirstate ../dirstateallexcluded - $ hg sparse --reset - $ hg sparse -X excluded - $ cp ../dirstateallexcluded .hg/dirstate - $ touch includedadded - $ hg add includedadded - $ hg debugdirstate --nodates - a 0 -1 unset includedadded - $ hg debugrebuilddirstate --minimal - $ hg debugdirstate --nodates - n 0 -1 unset included - a 0 -1 * includedadded (glob) - -Test debugdirstate --minimal where a file is not in parent manifest -but in the dirstate. This should take into account excluded files in the -manifest - $ cp ../dirstateboth .hg/dirstate - $ touch includedadded - $ hg add includedadded - $ touch excludednomanifest - $ hg add excludednomanifest - $ cp .hg/dirstate ../moreexcluded - $ hg forget excludednomanifest - $ rm excludednomanifest - $ hg sparse -X excludednomanifest - $ cp ../moreexcluded .hg/dirstate - $ hg manifest - excluded - included -We have files in the dirstate that are included and excluded. Some are in the -manifest and some are not. - $ hg debugdirstate --nodates - n 644 0 * excluded (glob) - a 0 -1 * excludednomanifest (glob) - n 644 0 * included (glob) - a 0 -1 * includedadded (glob) - $ hg debugrebuilddirstate --minimal - $ hg debugdirstate --nodates - n 644 0 * included (glob) - a 0 -1 * includedadded (glob) -