##// END OF EJS Templates
sparse: vendor Facebook-developed extension...
Gregory Szorc -
r33289:abd7dedb default
parent child Browse files
Show More
This diff has been collapsed as it changes many lines, (1081 lines changed) Show them Hide them
@@ -0,0 +1,1081 b''
1 # sparse.py - allow sparse checkouts of the working directory
2 #
3 # Copyright 2014 Facebook, Inc.
4 #
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
7
8 """allow sparse checkouts of the working directory (EXPERIMENTAL)
9 """
10
11 from __future__ import absolute_import
12
13 import collections
14 import hashlib
15 import os
16
17 from mercurial.i18n import _
18 from mercurial.node import nullid
19 from mercurial import (
20 cmdutil,
21 commands,
22 context,
23 dirstate,
24 error,
25 extensions,
26 hg,
27 localrepo,
28 match as matchmod,
29 merge as mergemod,
30 registrar,
31 util,
32 )
33
34 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
35 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
36 # be specifying the version(s) of Mercurial they are tested with, or
37 # leave the attribute unspecified.
38 testedwith = 'ships-with-hg-core'
39
40 cmdtable = {}
41 command = registrar.command(cmdtable)
42
43 def uisetup(ui):
44 _setupupdates(ui)
45 _setupcommit(ui)
46
47 def extsetup(ui):
48 _setupclone(ui)
49 _setuplog(ui)
50 _setupadd(ui)
51 _setupdirstate(ui)
52 # if fsmonitor is enabled, tell it to use our hash function
53 try:
54 fsmonitor = extensions.find('fsmonitor')
55 def _hashignore(orig, ignore):
56 return _hashmatcher(ignore)
57 extensions.wrapfunction(fsmonitor, '_hashignore', _hashignore)
58 except KeyError:
59 pass
60 # do the same for hgwatchman, old name
61 try:
62 hgwatchman = extensions.find('hgwatchman')
63 def _hashignore(orig, ignore):
64 return _hashmatcher(ignore)
65 extensions.wrapfunction(hgwatchman, '_hashignore', _hashignore)
66 except KeyError:
67 pass
68
69 def reposetup(ui, repo):
70 if not util.safehasattr(repo, 'dirstate'):
71 return
72
73 _wraprepo(ui, repo)
74
75 def replacefilecache(cls, propname, replacement):
76 """Replace a filecache property with a new class. This allows changing the
77 cache invalidation condition."""
78 origcls = cls
79 assert callable(replacement)
80 while cls is not object:
81 if propname in cls.__dict__:
82 orig = cls.__dict__[propname]
83 setattr(cls, propname, replacement(orig))
84 break
85 cls = cls.__bases__[0]
86
87 if cls is object:
88 raise AttributeError(_("type '%s' has no property '%s'") % (origcls,
89 propname))
90
91 def _setupupdates(ui):
92 def _calculateupdates(orig, repo, wctx, mctx, ancestors, branchmerge, *arg,
93 **kwargs):
94 """Filter updates to only lay out files that match the sparse rules.
95 """
96 actions, diverge, renamedelete = orig(repo, wctx, mctx, ancestors,
97 branchmerge, *arg, **kwargs)
98
99 if not util.safehasattr(repo, 'sparsematch'):
100 return actions, diverge, renamedelete
101
102 files = set()
103 prunedactions = {}
104 oldrevs = [pctx.rev() for pctx in wctx.parents()]
105 oldsparsematch = repo.sparsematch(*oldrevs)
106
107 if branchmerge:
108 # If we're merging, use the wctx filter, since we're merging into
109 # the wctx.
110 sparsematch = repo.sparsematch(wctx.parents()[0].rev())
111 else:
112 # If we're updating, use the target context's filter, since we're
113 # moving to the target context.
114 sparsematch = repo.sparsematch(mctx.rev())
115
116 temporaryfiles = []
117 for file, action in actions.iteritems():
118 type, args, msg = action
119 files.add(file)
120 if sparsematch(file):
121 prunedactions[file] = action
122 elif type == 'm':
123 temporaryfiles.append(file)
124 prunedactions[file] = action
125 elif branchmerge:
126 if type != 'k':
127 temporaryfiles.append(file)
128 prunedactions[file] = action
129 elif type == 'f':
130 prunedactions[file] = action
131 elif file in wctx:
132 prunedactions[file] = ('r', args, msg)
133
134 if len(temporaryfiles) > 0:
135 ui.status(_("temporarily included %d file(s) in the sparse checkout"
136 " for merging\n") % len(temporaryfiles))
137 repo.addtemporaryincludes(temporaryfiles)
138
139 # Add the new files to the working copy so they can be merged, etc
140 actions = []
141 message = 'temporarily adding to sparse checkout'
142 wctxmanifest = repo[None].manifest()
143 for file in temporaryfiles:
144 if file in wctxmanifest:
145 fctx = repo[None][file]
146 actions.append((file, (fctx.flags(), False), message))
147
148 typeactions = collections.defaultdict(list)
149 typeactions['g'] = actions
150 mergemod.applyupdates(repo, typeactions, repo[None], repo['.'],
151 False)
152
153 dirstate = repo.dirstate
154 for file, flags, msg in actions:
155 dirstate.normal(file)
156
157 profiles = repo.getactiveprofiles()
158 changedprofiles = profiles & files
159 # If an active profile changed during the update, refresh the checkout.
160 # Don't do this during a branch merge, since all incoming changes should
161 # have been handled by the temporary includes above.
162 if changedprofiles and not branchmerge:
163 mf = mctx.manifest()
164 for file in mf:
165 old = oldsparsematch(file)
166 new = sparsematch(file)
167 if not old and new:
168 flags = mf.flags(file)
169 prunedactions[file] = ('g', (flags, False), '')
170 elif old and not new:
171 prunedactions[file] = ('r', [], '')
172
173 return prunedactions, diverge, renamedelete
174
175 extensions.wrapfunction(mergemod, 'calculateupdates', _calculateupdates)
176
177 def _update(orig, repo, node, branchmerge, *args, **kwargs):
178 results = orig(repo, node, branchmerge, *args, **kwargs)
179
180 # If we're updating to a location, clean up any stale temporary includes
181 # (ex: this happens during hg rebase --abort).
182 if not branchmerge and util.safehasattr(repo, 'sparsematch'):
183 repo.prunetemporaryincludes()
184 return results
185
186 extensions.wrapfunction(mergemod, 'update', _update)
187
188 def _setupcommit(ui):
189 def _refreshoncommit(orig, self, node):
190 """Refresh the checkout when commits touch .hgsparse
191 """
192 orig(self, node)
193 repo = self._repo
194 if util.safehasattr(repo, 'getsparsepatterns'):
195 ctx = repo[node]
196 _, _, profiles = repo.getsparsepatterns(ctx.rev())
197 if set(profiles) & set(ctx.files()):
198 origstatus = repo.status()
199 origsparsematch = repo.sparsematch()
200 _refresh(repo.ui, repo, origstatus, origsparsematch, True)
201
202 repo.prunetemporaryincludes()
203
204 extensions.wrapfunction(context.committablectx, 'markcommitted',
205 _refreshoncommit)
206
207 def _setuplog(ui):
208 entry = commands.table['^log|history']
209 entry[1].append(('', 'sparse', None,
210 "limit to changesets affecting the sparse checkout"))
211
212 def _logrevs(orig, repo, opts):
213 revs = orig(repo, opts)
214 if opts.get('sparse'):
215 sparsematch = repo.sparsematch()
216 def ctxmatch(rev):
217 ctx = repo[rev]
218 return any(f for f in ctx.files() if sparsematch(f))
219 revs = revs.filter(ctxmatch)
220 return revs
221 extensions.wrapfunction(cmdutil, '_logrevs', _logrevs)
222
223 def _clonesparsecmd(orig, ui, repo, *args, **opts):
224 include_pat = opts.get('include')
225 exclude_pat = opts.get('exclude')
226 enableprofile_pat = opts.get('enable_profile')
227 include = exclude = enableprofile = False
228 if include_pat:
229 pat = include_pat
230 include = True
231 if exclude_pat:
232 pat = exclude_pat
233 exclude = True
234 if enableprofile_pat:
235 pat = enableprofile_pat
236 enableprofile = True
237 if sum([include, exclude, enableprofile]) > 1:
238 raise error.Abort(_("too many flags specified."))
239 if include or exclude or enableprofile:
240 def clonesparse(orig, self, node, overwrite, *args, **kwargs):
241 _config(self.ui, self.unfiltered(), pat, {}, include=include,
242 exclude=exclude, enableprofile=enableprofile)
243 return orig(self, node, overwrite, *args, **kwargs)
244 extensions.wrapfunction(hg, 'updaterepo', clonesparse)
245 return orig(ui, repo, *args, **opts)
246
247 def _setupclone(ui):
248 entry = commands.table['^clone']
249 entry[1].append(('', 'enable-profile', [],
250 'enable a sparse profile'))
251 entry[1].append(('', 'include', [],
252 'include sparse pattern'))
253 entry[1].append(('', 'exclude', [],
254 'exclude sparse pattern'))
255 extensions.wrapcommand(commands.table, 'clone', _clonesparsecmd)
256
257 def _setupadd(ui):
258 entry = commands.table['^add']
259 entry[1].append(('s', 'sparse', None,
260 'also include directories of added files in sparse config'))
261
262 def _add(orig, ui, repo, *pats, **opts):
263 if opts.get('sparse'):
264 dirs = set()
265 for pat in pats:
266 dirname, basename = util.split(pat)
267 dirs.add(dirname)
268 _config(ui, repo, list(dirs), opts, include=True)
269 return orig(ui, repo, *pats, **opts)
270
271 extensions.wrapcommand(commands.table, 'add', _add)
272
273 def _setupdirstate(ui):
274 """Modify the dirstate to prevent stat'ing excluded files,
275 and to prevent modifications to files outside the checkout.
276 """
277
278 def _dirstate(orig, repo):
279 dirstate = orig(repo)
280 dirstate.repo = repo
281 return dirstate
282 extensions.wrapfunction(
283 localrepo.localrepository.dirstate, 'func', _dirstate)
284
285 # The atrocity below is needed to wrap dirstate._ignore. It is a cached
286 # property, which means normal function wrapping doesn't work.
287 class ignorewrapper(object):
288 def __init__(self, orig):
289 self.orig = orig
290 self.origignore = None
291 self.func = None
292 self.sparsematch = None
293
294 def __get__(self, obj, type=None):
295 repo = obj.repo
296 origignore = self.orig.__get__(obj)
297 if not util.safehasattr(repo, 'sparsematch'):
298 return origignore
299
300 sparsematch = repo.sparsematch()
301 if self.sparsematch != sparsematch or self.origignore != origignore:
302 self.func = unionmatcher([origignore,
303 negatematcher(sparsematch)])
304 self.sparsematch = sparsematch
305 self.origignore = origignore
306 return self.func
307
308 def __set__(self, obj, value):
309 return self.orig.__set__(obj, value)
310
311 def __delete__(self, obj):
312 return self.orig.__delete__(obj)
313
314 replacefilecache(dirstate.dirstate, '_ignore', ignorewrapper)
315
316 # dirstate.rebuild should not add non-matching files
317 def _rebuild(orig, self, parent, allfiles, changedfiles=None):
318 if util.safehasattr(self.repo, 'sparsematch'):
319 matcher = self.repo.sparsematch()
320 allfiles = allfiles.matches(matcher)
321 if changedfiles:
322 changedfiles = [f for f in changedfiles if matcher(f)]
323
324 if changedfiles is not None:
325 # In _rebuild, these files will be deleted from the dirstate
326 # when they are not found to be in allfiles
327 dirstatefilestoremove = set(f for f in self if not matcher(f))
328 changedfiles = dirstatefilestoremove.union(changedfiles)
329
330 return orig(self, parent, allfiles, changedfiles)
331 extensions.wrapfunction(dirstate.dirstate, 'rebuild', _rebuild)
332
333 # Prevent adding files that are outside the sparse checkout
334 editfuncs = ['normal', 'add', 'normallookup', 'copy', 'remove', 'merge']
335 hint = _('include file with `hg sparse --include <pattern>` or use ' +
336 '`hg add -s <file>` to include file directory while adding')
337 for func in editfuncs:
338 def _wrapper(orig, self, *args):
339 repo = self.repo
340 if util.safehasattr(repo, 'sparsematch'):
341 dirstate = repo.dirstate
342 sparsematch = repo.sparsematch()
343 for f in args:
344 if (f is not None and not sparsematch(f) and
345 f not in dirstate):
346 raise error.Abort(_("cannot add '%s' - it is outside "
347 "the sparse checkout") % f,
348 hint=hint)
349 return orig(self, *args)
350 extensions.wrapfunction(dirstate.dirstate, func, _wrapper)
351
352 def _wraprepo(ui, repo):
353 class SparseRepo(repo.__class__):
354 def readsparseconfig(self, raw):
355 """Takes a string sparse config and returns the includes,
356 excludes, and profiles it specified.
357 """
358 includes = set()
359 excludes = set()
360 current = includes
361 profiles = []
362 for line in raw.split('\n'):
363 line = line.strip()
364 if not line or line.startswith('#'):
365 # empty or comment line, skip
366 continue
367 elif line.startswith('%include '):
368 line = line[9:].strip()
369 if line:
370 profiles.append(line)
371 elif line == '[include]':
372 if current != includes:
373 raise error.Abort(_('.hg/sparse cannot have includes ' +
374 'after excludes'))
375 continue
376 elif line == '[exclude]':
377 current = excludes
378 elif line:
379 if line.strip().startswith('/'):
380 self.ui.warn(_('warning: sparse profile cannot use' +
381 ' paths starting with /, ignoring %s\n')
382 % line)
383 continue
384 current.add(line)
385
386 return includes, excludes, profiles
387
388 def getsparsepatterns(self, rev):
389 """Returns the include/exclude patterns specified by the
390 given rev.
391 """
392 if not self.vfs.exists('sparse'):
393 return set(), set(), []
394 if rev is None:
395 raise error.Abort(_("cannot parse sparse patterns from " +
396 "working copy"))
397
398 raw = self.vfs.read('sparse')
399 includes, excludes, profiles = self.readsparseconfig(raw)
400
401 ctx = self[rev]
402 if profiles:
403 visited = set()
404 while profiles:
405 profile = profiles.pop()
406 if profile in visited:
407 continue
408 visited.add(profile)
409
410 try:
411 raw = self.getrawprofile(profile, rev)
412 except error.ManifestLookupError:
413 msg = (
414 "warning: sparse profile '%s' not found "
415 "in rev %s - ignoring it\n" % (profile, ctx))
416 if self.ui.configbool(
417 'sparse', 'missingwarning', True):
418 self.ui.warn(msg)
419 else:
420 self.ui.debug(msg)
421 continue
422 pincludes, pexcludes, subprofs = \
423 self.readsparseconfig(raw)
424 includes.update(pincludes)
425 excludes.update(pexcludes)
426 for subprofile in subprofs:
427 profiles.append(subprofile)
428
429 profiles = visited
430
431 if includes:
432 includes.add('.hg*')
433 return includes, excludes, profiles
434
435 def getrawprofile(self, profile, changeid):
436 try:
437 simplecache = extensions.find('simplecache')
438 node = self[changeid].hex()
439 def func():
440 return self.filectx(profile, changeid=changeid).data()
441 key = 'sparseprofile:%s:%s' % (profile.replace('/', '__'), node)
442 return simplecache.memoize(func, key,
443 simplecache.stringserializer, self.ui)
444 except KeyError:
445 return self.filectx(profile, changeid=changeid).data()
446
447 def sparsechecksum(self, filepath):
448 fh = open(filepath)
449 return hashlib.sha1(fh.read()).hexdigest()
450
451 def _sparsesignature(self, includetemp=True):
452 """Returns the signature string representing the contents of the
453 current project sparse configuration. This can be used to cache the
454 sparse matcher for a given set of revs."""
455 signaturecache = self.signaturecache
456 signature = signaturecache.get('signature')
457 if includetemp:
458 tempsignature = signaturecache.get('tempsignature')
459 else:
460 tempsignature = 0
461
462 if signature is None or (includetemp and tempsignature is None):
463 signature = 0
464 try:
465 sparsepath = self.vfs.join('sparse')
466 signature = self.sparsechecksum(sparsepath)
467 except (OSError, IOError):
468 pass
469 signaturecache['signature'] = signature
470
471 tempsignature = 0
472 if includetemp:
473 try:
474 tempsparsepath = self.vfs.join('tempsparse')
475 tempsignature = self.sparsechecksum(tempsparsepath)
476 except (OSError, IOError):
477 pass
478 signaturecache['tempsignature'] = tempsignature
479 return '%s %s' % (str(signature), str(tempsignature))
480
481 def invalidatecaches(self):
482 self.invalidatesignaturecache()
483 return super(SparseRepo, self).invalidatecaches()
484
485 def invalidatesignaturecache(self):
486 self.signaturecache.clear()
487
488 def sparsematch(self, *revs, **kwargs):
489 """Returns the sparse match function for the given revs.
490
491 If multiple revs are specified, the match function is the union
492 of all the revs.
493
494 `includetemp` is used to indicate if the temporarily included file
495 should be part of the matcher.
496 """
497 if not revs or revs == (None,):
498 revs = [self.changelog.rev(node) for node in
499 self.dirstate.parents() if node != nullid]
500
501 includetemp = kwargs.get('includetemp', True)
502 signature = self._sparsesignature(includetemp=includetemp)
503
504 key = '%s %s' % (str(signature), ' '.join([str(r) for r in revs]))
505
506 result = self.sparsecache.get(key, None)
507 if result:
508 return result
509
510 matchers = []
511 for rev in revs:
512 try:
513 includes, excludes, profiles = self.getsparsepatterns(rev)
514
515 if includes or excludes:
516 # Explicitly include subdirectories of includes so
517 # status will walk them down to the actual include.
518 subdirs = set()
519 for include in includes:
520 dirname = os.path.dirname(include)
521 # basename is used to avoid issues with absolute
522 # paths (which on Windows can include the drive).
523 while os.path.basename(dirname):
524 subdirs.add(dirname)
525 dirname = os.path.dirname(dirname)
526
527 matcher = matchmod.match(self.root, '', [],
528 include=includes, exclude=excludes,
529 default='relpath')
530 if subdirs:
531 matcher = forceincludematcher(matcher, subdirs)
532 matchers.append(matcher)
533 except IOError:
534 pass
535
536 result = None
537 if not matchers:
538 result = matchmod.always(self.root, '')
539 elif len(matchers) == 1:
540 result = matchers[0]
541 else:
542 result = unionmatcher(matchers)
543
544 if kwargs.get('includetemp', True):
545 tempincludes = self.gettemporaryincludes()
546 result = forceincludematcher(result, tempincludes)
547
548 self.sparsecache[key] = result
549
550 return result
551
552 def getactiveprofiles(self):
553 revs = [self.changelog.rev(node) for node in
554 self.dirstate.parents() if node != nullid]
555
556 activeprofiles = set()
557 for rev in revs:
558 _, _, profiles = self.getsparsepatterns(rev)
559 activeprofiles.update(profiles)
560
561 return activeprofiles
562
563 def writesparseconfig(self, include, exclude, profiles):
564 raw = '%s[include]\n%s\n[exclude]\n%s\n' % (
565 ''.join(['%%include %s\n' % p for p in sorted(profiles)]),
566 '\n'.join(sorted(include)),
567 '\n'.join(sorted(exclude)))
568 self.vfs.write("sparse", raw)
569 self.invalidatesignaturecache()
570
571 def addtemporaryincludes(self, files):
572 includes = self.gettemporaryincludes()
573 for file in files:
574 includes.add(file)
575 self._writetemporaryincludes(includes)
576
577 def gettemporaryincludes(self):
578 existingtemp = set()
579 if self.vfs.exists('tempsparse'):
580 raw = self.vfs.read('tempsparse')
581 existingtemp.update(raw.split('\n'))
582 return existingtemp
583
584 def _writetemporaryincludes(self, includes):
585 raw = '\n'.join(sorted(includes))
586 self.vfs.write('tempsparse', raw)
587 self.invalidatesignaturecache()
588
589 def prunetemporaryincludes(self):
590 if repo.vfs.exists('tempsparse'):
591 origstatus = self.status()
592 modified, added, removed, deleted, a, b, c = origstatus
593 if modified or added or removed or deleted:
594 # Still have pending changes. Don't bother trying to prune.
595 return
596
597 sparsematch = self.sparsematch(includetemp=False)
598 dirstate = self.dirstate
599 actions = []
600 dropped = []
601 tempincludes = self.gettemporaryincludes()
602 for file in tempincludes:
603 if file in dirstate and not sparsematch(file):
604 message = 'dropping temporarily included sparse files'
605 actions.append((file, None, message))
606 dropped.append(file)
607
608 typeactions = collections.defaultdict(list)
609 typeactions['r'] = actions
610 mergemod.applyupdates(self, typeactions, self[None], self['.'],
611 False)
612
613 # Fix dirstate
614 for file in dropped:
615 dirstate.drop(file)
616
617 self.vfs.unlink('tempsparse')
618 self.invalidatesignaturecache()
619 msg = _("cleaned up %d temporarily added file(s) from the "
620 "sparse checkout\n")
621 ui.status(msg % len(tempincludes))
622
623 if 'dirstate' in repo._filecache:
624 repo.dirstate.repo = repo
625 repo.sparsecache = {}
626 repo.signaturecache = {}
627 repo.__class__ = SparseRepo
628
629 @command('^sparse', [
630 ('I', 'include', False, _('include files in the sparse checkout')),
631 ('X', 'exclude', False, _('exclude files in the sparse checkout')),
632 ('d', 'delete', False, _('delete an include/exclude rule')),
633 ('f', 'force', False, _('allow changing rules even with pending changes')),
634 ('', 'enable-profile', False, _('enables the specified profile')),
635 ('', 'disable-profile', False, _('disables the specified profile')),
636 ('', 'import-rules', False, _('imports rules from a file')),
637 ('', 'clear-rules', False, _('clears local include/exclude rules')),
638 ('', 'refresh', False, _('updates the working after sparseness changes')),
639 ('', 'reset', False, _('makes the repo full again')),
640 ] + commands.templateopts,
641 _('[--OPTION] PATTERN...'))
642 def sparse(ui, repo, *pats, **opts):
643 """make the current checkout sparse, or edit the existing checkout
644
645 The sparse command is used to make the current checkout sparse.
646 This means files that don't meet the sparse condition will not be
647 written to disk, or show up in any working copy operations. It does
648 not affect files in history in any way.
649
650 Passing no arguments prints the currently applied sparse rules.
651
652 --include and --exclude are used to add and remove files from the sparse
653 checkout. The effects of adding an include or exclude rule are applied
654 immediately. If applying the new rule would cause a file with pending
655 changes to be added or removed, the command will fail. Pass --force to
656 force a rule change even with pending changes (the changes on disk will
657 be preserved).
658
659 --delete removes an existing include/exclude rule. The effects are
660 immediate.
661
662 --refresh refreshes the files on disk based on the sparse rules. This is
663 only necessary if .hg/sparse was changed by hand.
664
665 --enable-profile and --disable-profile accept a path to a .hgsparse file.
666 This allows defining sparse checkouts and tracking them inside the
667 repository. This is useful for defining commonly used sparse checkouts for
668 many people to use. As the profile definition changes over time, the sparse
669 checkout will automatically be updated appropriately, depending on which
670 changeset is checked out. Changes to .hgsparse are not applied until they
671 have been committed.
672
673 --import-rules accepts a path to a file containing rules in the .hgsparse
674 format, allowing you to add --include, --exclude and --enable-profile rules
675 in bulk. Like the --include, --exclude and --enable-profile switches, the
676 changes are applied immediately.
677
678 --clear-rules removes all local include and exclude rules, while leaving
679 any enabled profiles in place.
680
681 Returns 0 if editing the sparse checkout succeeds.
682 """
683 include = opts.get('include')
684 exclude = opts.get('exclude')
685 force = opts.get('force')
686 enableprofile = opts.get('enable_profile')
687 disableprofile = opts.get('disable_profile')
688 importrules = opts.get('import_rules')
689 clearrules = opts.get('clear_rules')
690 delete = opts.get('delete')
691 refresh = opts.get('refresh')
692 reset = opts.get('reset')
693 count = sum([include, exclude, enableprofile, disableprofile, delete,
694 importrules, refresh, clearrules, reset])
695 if count > 1:
696 raise error.Abort(_("too many flags specified"))
697
698 if count == 0:
699 if repo.vfs.exists('sparse'):
700 ui.status(repo.vfs.read("sparse") + "\n")
701 temporaryincludes = repo.gettemporaryincludes()
702 if temporaryincludes:
703 ui.status(_("Temporarily Included Files (for merge/rebase):\n"))
704 ui.status(("\n".join(temporaryincludes) + "\n"))
705 else:
706 ui.status(_('repo is not sparse\n'))
707 return
708
709 if include or exclude or delete or reset or enableprofile or disableprofile:
710 _config(ui, repo, pats, opts, include=include, exclude=exclude,
711 reset=reset, delete=delete, enableprofile=enableprofile,
712 disableprofile=disableprofile, force=force)
713
714 if importrules:
715 _import(ui, repo, pats, opts, force=force)
716
717 if clearrules:
718 _clear(ui, repo, pats, force=force)
719
720 if refresh:
721 try:
722 wlock = repo.wlock()
723 fcounts = map(
724 len,
725 _refresh(ui, repo, repo.status(), repo.sparsematch(), force))
726 _verbose_output(ui, opts, 0, 0, 0, *fcounts)
727 finally:
728 wlock.release()
729
730 def _config(ui, repo, pats, opts, include=False, exclude=False, reset=False,
731 delete=False, enableprofile=False, disableprofile=False,
732 force=False):
733 """
734 Perform a sparse config update. Only one of the kwargs may be specified.
735 """
736 wlock = repo.wlock()
737 try:
738 oldsparsematch = repo.sparsematch()
739
740 if repo.vfs.exists('sparse'):
741 raw = repo.vfs.read('sparse')
742 oldinclude, oldexclude, oldprofiles = map(
743 set, repo.readsparseconfig(raw))
744 else:
745 oldinclude = set()
746 oldexclude = set()
747 oldprofiles = set()
748
749 try:
750 if reset:
751 newinclude = set()
752 newexclude = set()
753 newprofiles = set()
754 else:
755 newinclude = set(oldinclude)
756 newexclude = set(oldexclude)
757 newprofiles = set(oldprofiles)
758
759 oldstatus = repo.status()
760
761 if any(pat.startswith('/') for pat in pats):
762 ui.warn(_('warning: paths cannot start with /, ignoring: %s\n')
763 % ([pat for pat in pats if pat.startswith('/')]))
764 elif include:
765 newinclude.update(pats)
766 elif exclude:
767 newexclude.update(pats)
768 elif enableprofile:
769 newprofiles.update(pats)
770 elif disableprofile:
771 newprofiles.difference_update(pats)
772 elif delete:
773 newinclude.difference_update(pats)
774 newexclude.difference_update(pats)
775
776 repo.writesparseconfig(newinclude, newexclude, newprofiles)
777 fcounts = map(
778 len, _refresh(ui, repo, oldstatus, oldsparsematch, force))
779
780 profilecount = (len(newprofiles - oldprofiles) -
781 len(oldprofiles - newprofiles))
782 includecount = (len(newinclude - oldinclude) -
783 len(oldinclude - newinclude))
784 excludecount = (len(newexclude - oldexclude) -
785 len(oldexclude - newexclude))
786 _verbose_output(
787 ui, opts, profilecount, includecount, excludecount, *fcounts)
788 except Exception:
789 repo.writesparseconfig(oldinclude, oldexclude, oldprofiles)
790 raise
791 finally:
792 wlock.release()
793
794 def _import(ui, repo, files, opts, force=False):
795 with repo.wlock():
796 # load union of current active profile
797 revs = [repo.changelog.rev(node) for node in
798 repo.dirstate.parents() if node != nullid]
799
800 # read current configuration
801 raw = ''
802 if repo.vfs.exists('sparse'):
803 raw = repo.vfs.read('sparse')
804 oincludes, oexcludes, oprofiles = repo.readsparseconfig(raw)
805 includes, excludes, profiles = map(
806 set, (oincludes, oexcludes, oprofiles))
807
808 # all active rules
809 aincludes, aexcludes, aprofiles = set(), set(), set()
810 for rev in revs:
811 rincludes, rexcludes, rprofiles = repo.getsparsepatterns(rev)
812 aincludes.update(rincludes)
813 aexcludes.update(rexcludes)
814 aprofiles.update(rprofiles)
815
816 # import rules on top; only take in rules that are not yet
817 # part of the active rules.
818 changed = False
819 for file in files:
820 with util.posixfile(util.expandpath(file)) as importfile:
821 iincludes, iexcludes, iprofiles = repo.readsparseconfig(
822 importfile.read())
823 oldsize = len(includes) + len(excludes) + len(profiles)
824 includes.update(iincludes - aincludes)
825 excludes.update(iexcludes - aexcludes)
826 profiles.update(set(iprofiles) - aprofiles)
827 if len(includes) + len(excludes) + len(profiles) > oldsize:
828 changed = True
829
830 profilecount = includecount = excludecount = 0
831 fcounts = (0, 0, 0)
832
833 if changed:
834 profilecount = len(profiles - aprofiles)
835 includecount = len(includes - aincludes)
836 excludecount = len(excludes - aexcludes)
837
838 oldstatus = repo.status()
839 oldsparsematch = repo.sparsematch()
840 repo.writesparseconfig(includes, excludes, profiles)
841
842 try:
843 fcounts = map(
844 len, _refresh(ui, repo, oldstatus, oldsparsematch, force))
845 except Exception:
846 repo.writesparseconfig(oincludes, oexcludes, oprofiles)
847 raise
848
849 _verbose_output(ui, opts, profilecount, includecount, excludecount,
850 *fcounts)
851
852 def _clear(ui, repo, files, force=False):
853 with repo.wlock():
854 raw = ''
855 if repo.vfs.exists('sparse'):
856 raw = repo.vfs.read('sparse')
857 includes, excludes, profiles = repo.readsparseconfig(raw)
858
859 if includes or excludes:
860 oldstatus = repo.status()
861 oldsparsematch = repo.sparsematch()
862 repo.writesparseconfig(set(), set(), profiles)
863 _refresh(ui, repo, oldstatus, oldsparsematch, force)
864
865 def _refresh(ui, repo, origstatus, origsparsematch, force):
866 """Refreshes which files are on disk by comparing the old status and
867 sparsematch with the new sparsematch.
868
869 Will raise an exception if a file with pending changes is being excluded
870 or included (unless force=True).
871 """
872 modified, added, removed, deleted, unknown, ignored, clean = origstatus
873
874 # Verify there are no pending changes
875 pending = set()
876 pending.update(modified)
877 pending.update(added)
878 pending.update(removed)
879 sparsematch = repo.sparsematch()
880 abort = False
881 for file in pending:
882 if not sparsematch(file):
883 ui.warn(_("pending changes to '%s'\n") % file)
884 abort = not force
885 if abort:
886 raise error.Abort(_("could not update sparseness due to " +
887 "pending changes"))
888
889 # Calculate actions
890 dirstate = repo.dirstate
891 ctx = repo['.']
892 added = []
893 lookup = []
894 dropped = []
895 mf = ctx.manifest()
896 files = set(mf)
897
898 actions = {}
899
900 for file in files:
901 old = origsparsematch(file)
902 new = sparsematch(file)
903 # Add files that are newly included, or that don't exist in
904 # the dirstate yet.
905 if (new and not old) or (old and new and not file in dirstate):
906 fl = mf.flags(file)
907 if repo.wvfs.exists(file):
908 actions[file] = ('e', (fl,), '')
909 lookup.append(file)
910 else:
911 actions[file] = ('g', (fl, False), '')
912 added.append(file)
913 # Drop files that are newly excluded, or that still exist in
914 # the dirstate.
915 elif (old and not new) or (not old and not new and file in dirstate):
916 dropped.append(file)
917 if file not in pending:
918 actions[file] = ('r', [], '')
919
920 # Verify there are no pending changes in newly included files
921 abort = False
922 for file in lookup:
923 ui.warn(_("pending changes to '%s'\n") % file)
924 abort = not force
925 if abort:
926 raise error.Abort(_("cannot change sparseness due to " +
927 "pending changes (delete the files or use --force " +
928 "to bring them back dirty)"))
929
930 # Check for files that were only in the dirstate.
931 for file, state in dirstate.iteritems():
932 if not file in files:
933 old = origsparsematch(file)
934 new = sparsematch(file)
935 if old and not new:
936 dropped.append(file)
937
938 # Apply changes to disk
939 typeactions = dict((m, []) for m in 'a f g am cd dc r dm dg m e k'.split())
940 for f, (m, args, msg) in actions.iteritems():
941 if m not in typeactions:
942 typeactions[m] = []
943 typeactions[m].append((f, args, msg))
944 mergemod.applyupdates(repo, typeactions, repo[None], repo['.'], False)
945
946 # Fix dirstate
947 for file in added:
948 dirstate.normal(file)
949
950 for file in dropped:
951 dirstate.drop(file)
952
953 for file in lookup:
954 # File exists on disk, and we're bringing it back in an unknown state.
955 dirstate.normallookup(file)
956
957 return added, dropped, lookup
958
959 def _verbose_output(ui, opts, profilecount, includecount, excludecount, added,
960 dropped, lookup):
961 """Produce --verbose and templatable output
962
963 This specifically enables -Tjson, providing machine-readable stats on how
964 the sparse profile changed.
965
966 """
967 with ui.formatter('sparse', opts) as fm:
968 fm.startitem()
969 fm.condwrite(ui.verbose, 'profiles_added', 'Profile # change: %d\n',
970 profilecount)
971 fm.condwrite(ui.verbose, 'include_rules_added',
972 'Include rule # change: %d\n', includecount)
973 fm.condwrite(ui.verbose, 'exclude_rules_added',
974 'Exclude rule # change: %d\n', excludecount)
975 # In 'plain' verbose mode, mergemod.applyupdates already outputs what
976 # files are added or removed outside of the templating formatter
977 # framework. No point in repeating ourselves in that case.
978 if not fm.isplain():
979 fm.condwrite(ui.verbose, 'files_added', 'Files added: %d\n',
980 added)
981 fm.condwrite(ui.verbose, 'files_dropped', 'Files dropped: %d\n',
982 dropped)
983 fm.condwrite(ui.verbose, 'files_conflicting',
984 'Files conflicting: %d\n', lookup)
985
986 class forceincludematcher(object):
987 """A matcher that returns true for any of the forced includes before testing
988 against the actual matcher."""
989 def __init__(self, matcher, includes):
990 self._matcher = matcher
991 self._includes = includes
992
993 def __call__(self, value):
994 return value in self._includes or self._matcher(value)
995
996 def always(self):
997 return False
998
999 def files(self):
1000 return []
1001
1002 def isexact(self):
1003 return False
1004
1005 def anypats(self):
1006 return True
1007
1008 def prefix(self):
1009 return False
1010
1011 def hash(self):
1012 sha1 = hashlib.sha1()
1013 sha1.update(_hashmatcher(self._matcher))
1014 for include in sorted(self._includes):
1015 sha1.update(include + '\0')
1016 return sha1.hexdigest()
1017
1018 class unionmatcher(object):
1019 """A matcher that is the union of several matchers."""
1020 def __init__(self, matchers):
1021 self._matchers = matchers
1022
1023 def __call__(self, value):
1024 for match in self._matchers:
1025 if match(value):
1026 return True
1027 return False
1028
1029 def always(self):
1030 return False
1031
1032 def files(self):
1033 return []
1034
1035 def isexact(self):
1036 return False
1037
1038 def anypats(self):
1039 return True
1040
1041 def prefix(self):
1042 return False
1043
1044 def hash(self):
1045 sha1 = hashlib.sha1()
1046 for m in self._matchers:
1047 sha1.update(_hashmatcher(m))
1048 return sha1.hexdigest()
1049
1050 class negatematcher(object):
1051 def __init__(self, matcher):
1052 self._matcher = matcher
1053
1054 def __call__(self, value):
1055 return not self._matcher(value)
1056
1057 def always(self):
1058 return False
1059
1060 def files(self):
1061 return []
1062
1063 def isexact(self):
1064 return False
1065
1066 def anypats(self):
1067 return True
1068
1069 def hash(self):
1070 sha1 = hashlib.sha1()
1071 sha1.update('negate')
1072 sha1.update(_hashmatcher(self._matcher))
1073 return sha1.hexdigest()
1074
1075 def _hashmatcher(matcher):
1076 if util.safehasattr(matcher, 'hash'):
1077 return matcher.hash()
1078
1079 sha1 = hashlib.sha1()
1080 sha1.update(repr(matcher))
1081 return sha1.hexdigest()
@@ -0,0 +1,73 b''
1 test sparse
2
3 $ hg init myrepo
4 $ cd myrepo
5 $ cat >> $HGRCPATH <<EOF
6 > [extensions]
7 > sparse=
8 > purge=
9 > strip=
10 > rebase=
11 > EOF
12
13 $ echo a > index.html
14 $ echo x > data.py
15 $ echo z > readme.txt
16 $ cat > base.sparse <<EOF
17 > [include]
18 > *.sparse
19 > EOF
20 $ hg ci -Aqm 'initial'
21 $ cat > webpage.sparse <<EOF
22 > %include base.sparse
23 > [include]
24 > *.html
25 > EOF
26 $ hg ci -Aqm 'initial'
27
28 Clear rules when there are includes
29
30 $ hg sparse --include *.py
31 $ ls
32 data.py
33 $ hg sparse --clear-rules
34 $ ls
35 base.sparse
36 data.py
37 index.html
38 readme.txt
39 webpage.sparse
40
41 Clear rules when there are excludes
42
43 $ hg sparse --exclude *.sparse
44 $ ls
45 data.py
46 index.html
47 readme.txt
48 $ hg sparse --clear-rules
49 $ ls
50 base.sparse
51 data.py
52 index.html
53 readme.txt
54 webpage.sparse
55
56 Clearing rules should not alter profiles
57
58 $ hg sparse --enable-profile webpage.sparse
59 $ ls
60 base.sparse
61 index.html
62 webpage.sparse
63 $ hg sparse --include *.py
64 $ ls
65 base.sparse
66 data.py
67 index.html
68 webpage.sparse
69 $ hg sparse --clear-rules
70 $ ls
71 base.sparse
72 index.html
73 webpage.sparse
@@ -0,0 +1,72 b''
1 test sparse
2
3 $ cat >> $HGRCPATH << EOF
4 > [ui]
5 > ssh = python "$RUNTESTDIR/dummyssh"
6 > username = nobody <no.reply@fb.com>
7 > [extensions]
8 > sparse=
9 > purge=
10 > strip=
11 > rebase=
12 > EOF
13
14 $ hg init myrepo
15 $ cd myrepo
16 $ echo a > index.html
17 $ echo x > data.py
18 $ echo z > readme.txt
19 $ cat > webpage.sparse <<EOF
20 > [include]
21 > *.html
22 > EOF
23 $ cat > backend.sparse <<EOF
24 > [include]
25 > *.py
26 > EOF
27 $ hg ci -Aqm 'initial'
28 $ cd ..
29
30 Verify local clone with a sparse profile works
31
32 $ hg clone --enable-profile webpage.sparse myrepo clone1
33 updating to branch default
34 warning: sparse profile 'webpage.sparse' not found in rev 000000000000 - ignoring it
35 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
36 $ cd clone1
37 $ ls
38 index.html
39 $ cd ..
40
41 Verify local clone with include works
42
43 $ hg clone --include *.sparse myrepo clone2
44 updating to branch default
45 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
46 $ cd clone2
47 $ ls
48 backend.sparse
49 webpage.sparse
50 $ cd ..
51
52 Verify local clone with exclude works
53
54 $ hg clone --exclude data.py myrepo clone3
55 updating to branch default
56 4 files updated, 0 files merged, 0 files removed, 0 files unresolved
57 $ cd clone3
58 $ ls
59 backend.sparse
60 index.html
61 readme.txt
62 webpage.sparse
63 $ cd ..
64
65 Verify sparse clone profile over ssh works
66
67 $ hg clone -q --enable-profile webpage.sparse ssh://user@dummy/myrepo clone4
68 warning: sparse profile 'webpage.sparse' not found in rev 000000000000 - ignoring it
69 $ cd clone4
70 $ ls
71 index.html
72 $ cd ..
@@ -0,0 +1,44 b''
1 This test doesn't yet work due to the way fsmonitor is integrated with test runner
2
3 $ exit 80
4
5 test sparse interaction with other extensions
6
7 $ hg init myrepo
8 $ cd myrepo
9 $ cat > .hg/hgrc <<EOF
10 > [extensions]
11 > sparse=
12 > strip=
13 > EOF
14
15 Test fsmonitor integration (if available)
16 TODO: make fully isolated integration test a'la https://github.com/facebook/watchman/blob/master/tests/integration/WatchmanInstance.py
17 (this one is using the systemwide watchman instance)
18
19 $ touch .watchmanconfig
20 $ echo "ignoredir1/" >> .hgignore
21 $ hg commit -Am ignoredir1
22 adding .hgignore
23 $ echo "ignoredir2/" >> .hgignore
24 $ hg commit -m ignoredir2
25
26 $ hg sparse --reset
27 $ hg sparse -I ignoredir1 -I ignoredir2 -I dir1
28
29 $ mkdir ignoredir1 ignoredir2 dir1
30 $ touch ignoredir1/file ignoredir2/file dir1/file
31
32 Run status twice to compensate for a condition in fsmonitor where it will check
33 ignored files the second time it runs, regardless of previous state (ask @sid0)
34 $ hg status --config extensions.fsmonitor=
35 ? dir1/file
36 $ hg status --config extensions.fsmonitor=
37 ? dir1/file
38
39 Test that fsmonitor ignore hash check updates when .hgignore changes
40
41 $ hg up -q ".^"
42 $ hg status --config extensions.fsmonitor=
43 ? dir1/file
44 ? ignoredir2/file
@@ -0,0 +1,186 b''
1 test sparse
2
3 $ hg init myrepo
4 $ cd myrepo
5 $ cat >> $HGRCPATH <<EOF
6 > [extensions]
7 > sparse=
8 > purge=
9 > strip=
10 > rebase=
11 > EOF
12
13 $ echo a > index.html
14 $ echo x > data.py
15 $ echo z > readme.txt
16 $ cat > base.sparse <<EOF
17 > [include]
18 > *.sparse
19 > EOF
20 $ hg ci -Aqm 'initial'
21 $ cat > webpage.sparse <<EOF
22 > %include base.sparse
23 > [include]
24 > *.html
25 > EOF
26 $ hg ci -Aqm 'initial'
27
28 Import a rules file against a 'blank' sparse profile
29
30 $ cat > $TESTTMP/rules_to_import <<EOF
31 > [include]
32 > *.py
33 > EOF
34 $ hg sparse --import-rules $TESTTMP/rules_to_import
35 $ ls
36 data.py
37
38 $ hg sparse --reset
39 $ rm .hg/sparse
40
41 $ cat > $TESTTMP/rules_to_import <<EOF
42 > %include base.sparse
43 > [include]
44 > *.py
45 > EOF
46 $ hg sparse --import-rules $TESTTMP/rules_to_import
47 $ ls
48 base.sparse
49 data.py
50 webpage.sparse
51
52 $ hg sparse --reset
53 $ rm .hg/sparse
54
55 Start against an existing profile; rules *already active* should be ignored
56
57 $ hg sparse --enable-profile webpage.sparse
58 $ hg sparse --include *.py
59 $ cat > $TESTTMP/rules_to_import <<EOF
60 > %include base.sparse
61 > [include]
62 > *.html
63 > *.txt
64 > [exclude]
65 > *.py
66 > EOF
67 $ hg sparse --import-rules $TESTTMP/rules_to_import
68 $ ls
69 base.sparse
70 index.html
71 readme.txt
72 webpage.sparse
73 $ cat .hg/sparse
74 %include webpage.sparse
75 [include]
76 *.py
77 *.txt
78 [exclude]
79 *.py
80
81 $ hg sparse --reset
82 $ rm .hg/sparse
83
84 Same tests, with -Tjson enabled to output summaries
85
86 $ cat > $TESTTMP/rules_to_import <<EOF
87 > [include]
88 > *.py
89 > EOF
90 $ hg sparse --import-rules $TESTTMP/rules_to_import -Tjson
91 [
92 {
93 "exclude_rules_added": 0,
94 "files_added": 0,
95 "files_conflicting": 0,
96 "files_dropped": 4,
97 "include_rules_added": 1,
98 "profiles_added": 0
99 }
100 ]
101
102 $ hg sparse --reset
103 $ rm .hg/sparse
104
105 $ cat > $TESTTMP/rules_to_import <<EOF
106 > %include base.sparse
107 > [include]
108 > *.py
109 > EOF
110 $ hg sparse --import-rules $TESTTMP/rules_to_import -Tjson
111 [
112 {
113 "exclude_rules_added": 0,
114 "files_added": 0,
115 "files_conflicting": 0,
116 "files_dropped": 2,
117 "include_rules_added": 1,
118 "profiles_added": 1
119 }
120 ]
121
122 $ hg sparse --reset
123 $ rm .hg/sparse
124
125 $ hg sparse --enable-profile webpage.sparse
126 $ hg sparse --include *.py
127 $ cat > $TESTTMP/rules_to_import <<EOF
128 > %include base.sparse
129 > [include]
130 > *.html
131 > *.txt
132 > [exclude]
133 > *.py
134 > EOF
135 $ hg sparse --import-rules $TESTTMP/rules_to_import -Tjson
136 [
137 {
138 "exclude_rules_added": 1,
139 "files_added": 1,
140 "files_conflicting": 0,
141 "files_dropped": 1,
142 "include_rules_added": 1,
143 "profiles_added": 0
144 }
145 ]
146
147 If importing results in no new rules being added, no refresh should take place!
148
149 $ cat > $TESTTMP/trap_sparse_refresh.py <<EOF
150 > from mercurial import error, extensions
151 > def extsetup(ui):
152 > def abort_refresh(ui, *args):
153 > raise error.Abort('sparse._refresh called!')
154 > def sparseloaded(loaded):
155 > if not loaded:
156 > return
157 > sparse = extensions.find('sparse')
158 > sparse._refresh = abort_refresh
159 > extensions.afterloaded('sparse', sparseloaded)
160 > EOF
161 $ cat >> $HGRCPATH <<EOF
162 > [extensions]
163 > trap_sparse_refresh=$TESTTMP/trap_sparse_refresh.py
164 > EOF
165 $ cat > $TESTTMP/rules_to_import <<EOF
166 > [include]
167 > *.py
168 > EOF
169 $ hg sparse --import-rules $TESTTMP/rules_to_import
170
171 If an exception is raised during refresh, restore the existing rules again.
172
173 $ cat > $TESTTMP/rules_to_import <<EOF
174 > [exclude]
175 > *.html
176 > EOF
177 $ hg sparse --import-rules $TESTTMP/rules_to_import
178 abort: sparse._refresh called!
179 [255]
180 $ cat .hg/sparse
181 %include webpage.sparse
182 [include]
183 *.py
184 *.txt
185 [exclude]
186 *.py
@@ -0,0 +1,62 b''
1 test merging things outside of the sparse checkout
2
3 $ hg init myrepo
4 $ cd myrepo
5 $ cat > .hg/hgrc <<EOF
6 > [extensions]
7 > sparse=
8 > EOF
9
10 $ echo foo > foo
11 $ echo bar > bar
12 $ hg add foo bar
13 $ hg commit -m initial
14
15 $ hg branch feature
16 marked working directory as branch feature
17 (branches are permanent and global, did you want a bookmark?)
18 $ echo bar2 >> bar
19 $ hg commit -m 'feature - bar2'
20
21 $ hg update -q default
22 $ hg sparse --exclude 'bar**'
23
24 $ hg merge feature
25 temporarily included 1 file(s) in the sparse checkout for merging
26 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
27 (branch merge, don't forget to commit)
28
29 Verify bar was merged temporarily
30
31 $ ls
32 bar
33 foo
34 $ hg status
35 M bar
36
37 Verify bar disappears automatically when the working copy becomes clean
38
39 $ hg commit -m "merged"
40 cleaned up 1 temporarily added file(s) from the sparse checkout
41 $ hg status
42 $ ls
43 foo
44
45 $ hg cat -r . bar
46 bar
47 bar2
48
49 Test merging things outside of the sparse checkout that are not in the working
50 copy
51
52 $ hg strip -q -r . --config extensions.strip=
53 $ hg up -q feature
54 $ touch branchonly
55 $ hg ci -Aqm 'add branchonly'
56
57 $ hg up -q default
58 $ hg sparse -X branchonly
59 $ hg merge feature
60 temporarily included 2 file(s) in the sparse checkout for merging
61 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
62 (branch merge, don't forget to commit)
@@ -0,0 +1,272 b''
1 test sparse
2
3 $ hg init myrepo
4 $ cd myrepo
5 $ cat > .hg/hgrc <<EOF
6 > [extensions]
7 > sparse=
8 > purge=
9 > strip=
10 > rebase=
11 > EOF
12
13 $ echo a > index.html
14 $ echo x > data.py
15 $ echo z > readme.txt
16 $ cat > webpage.sparse <<EOF
17 > # frontend sparse profile
18 > [include]
19 > *.html
20 > EOF
21 $ cat > backend.sparse <<EOF
22 > # backend sparse profile
23 > [include]
24 > *.py
25 > EOF
26 $ hg ci -Aqm 'initial'
27
28 $ hg sparse --include '*.sparse'
29
30 Verify enabling a single profile works
31
32 $ hg sparse --enable-profile webpage.sparse
33 $ ls
34 backend.sparse
35 index.html
36 webpage.sparse
37
38 Verify enabling two profiles works
39
40 $ hg sparse --enable-profile backend.sparse
41 $ ls
42 backend.sparse
43 data.py
44 index.html
45 webpage.sparse
46
47 Verify disabling a profile works
48
49 $ hg sparse --disable-profile webpage.sparse
50 $ ls
51 backend.sparse
52 data.py
53 webpage.sparse
54
55 Verify that a profile is updated across multiple commits
56
57 $ cat > webpage.sparse <<EOF
58 > # frontend sparse profile
59 > [include]
60 > *.html
61 > EOF
62 $ cat > backend.sparse <<EOF
63 > # backend sparse profile
64 > [include]
65 > *.py
66 > *.txt
67 > EOF
68
69 $ echo foo >> data.py
70
71 $ hg ci -m 'edit profile'
72 $ ls
73 backend.sparse
74 data.py
75 readme.txt
76 webpage.sparse
77
78 $ hg up -q 0
79 $ ls
80 backend.sparse
81 data.py
82 webpage.sparse
83
84 $ hg up -q 1
85 $ ls
86 backend.sparse
87 data.py
88 readme.txt
89 webpage.sparse
90
91 Introduce a conflicting .hgsparse change
92
93 $ hg up -q 0
94 $ cat > backend.sparse <<EOF
95 > # Different backend sparse profile
96 > [include]
97 > *.html
98 > EOF
99 $ echo bar >> data.py
100
101 $ hg ci -qAm "edit profile other"
102 $ ls
103 backend.sparse
104 index.html
105 webpage.sparse
106
107 Verify conflicting merge pulls in the conflicting changes
108
109 $ hg merge 1
110 temporarily included 1 file(s) in the sparse checkout for merging
111 merging backend.sparse
112 merging data.py
113 warning: conflicts while merging backend.sparse! (edit, then use 'hg resolve --mark')
114 warning: conflicts while merging data.py! (edit, then use 'hg resolve --mark')
115 0 files updated, 0 files merged, 0 files removed, 2 files unresolved
116 use 'hg resolve' to retry unresolved file merges or 'hg update -C .' to abandon
117 [1]
118
119 $ rm *.orig
120 $ ls
121 backend.sparse
122 data.py
123 index.html
124 webpage.sparse
125
126 Verify resolving the merge removes the temporarily unioned files
127
128 $ cat > backend.sparse <<EOF
129 > # backend sparse profile
130 > [include]
131 > *.html
132 > *.txt
133 > EOF
134 $ hg resolve -m backend.sparse
135
136 $ cat > data.py <<EOF
137 > x
138 > foo
139 > bar
140 > EOF
141 $ hg resolve -m data.py
142 (no more unresolved files)
143
144 $ hg ci -qAm "merge profiles"
145 $ ls
146 backend.sparse
147 index.html
148 readme.txt
149 webpage.sparse
150
151 $ hg cat -r . data.py
152 x
153 foo
154 bar
155
156 Verify stripping refreshes dirstate
157
158 $ hg strip -q -r .
159 $ ls
160 backend.sparse
161 index.html
162 webpage.sparse
163
164 Verify rebase conflicts pulls in the conflicting changes
165
166 $ hg up -q 1
167 $ ls
168 backend.sparse
169 data.py
170 readme.txt
171 webpage.sparse
172
173 $ hg rebase -d 2
174 rebasing 1:a2b1de640a62 "edit profile"
175 temporarily included 1 file(s) in the sparse checkout for merging
176 merging backend.sparse
177 merging data.py
178 warning: conflicts while merging backend.sparse! (edit, then use 'hg resolve --mark')
179 warning: conflicts while merging data.py! (edit, then use 'hg resolve --mark')
180 unresolved conflicts (see hg resolve, then hg rebase --continue)
181 [1]
182 $ rm *.orig
183 $ ls
184 backend.sparse
185 data.py
186 index.html
187 webpage.sparse
188
189 Verify resolving conflict removes the temporary files
190
191 $ cat > backend.sparse <<EOF
192 > [include]
193 > *.html
194 > *.txt
195 > EOF
196 $ hg resolve -m backend.sparse
197
198 $ cat > data.py <<EOF
199 > x
200 > foo
201 > bar
202 > EOF
203 $ hg resolve -m data.py
204 (no more unresolved files)
205 continue: hg rebase --continue
206
207 $ hg rebase -q --continue
208 $ ls
209 backend.sparse
210 index.html
211 readme.txt
212 webpage.sparse
213
214 $ hg cat -r . data.py
215 x
216 foo
217 bar
218
219 Test checking out a commit that does not contain the sparse profile. The
220 warning message can be suppressed by setting missingwarning = false in
221 [sparse] section of your config:
222
223 $ hg sparse --reset
224 $ hg rm *.sparse
225 $ hg commit -m "delete profiles"
226 $ hg up -q ".^"
227 $ hg sparse --enable-profile backend.sparse
228 $ ls
229 index.html
230 readme.txt
231 $ hg up tip | grep warning
232 warning: sparse profile 'backend.sparse' not found in rev bfcb76de99cc - ignoring it
233 [1]
234 $ ls
235 data.py
236 index.html
237 readme.txt
238 $ hg sparse --disable-profile backend.sparse | grep warning
239 warning: sparse profile 'backend.sparse' not found in rev bfcb76de99cc - ignoring it
240 [1]
241 $ cat >> .hg/hgrc <<EOF
242 > [sparse]
243 > missingwarning = false
244 > EOF
245 $ hg sparse --enable-profile backend.sparse
246
247 $ cd ..
248
249 Test file permissions changing across a sparse profile change
250 $ hg init sparseperm
251 $ cd sparseperm
252 $ cat > .hg/hgrc <<EOF
253 > [extensions]
254 > sparse=
255 > EOF
256 $ touch a b
257 $ cat > .hgsparse <<EOF
258 > a
259 > EOF
260 $ hg commit -Aqm 'initial'
261 $ chmod a+x b
262 $ hg commit -qm 'make executable'
263 $ cat >> .hgsparse <<EOF
264 > b
265 > EOF
266 $ hg commit -qm 'update profile'
267 $ hg up -q 0
268 $ hg sparse --enable-profile .hgsparse
269 $ hg up -q 2
270 $ ls -l b
271 -rwxr-xr-x* b (glob)
272
@@ -0,0 +1,82 b''
1 test sparse with --verbose and -T json
2
3 $ hg init myrepo
4 $ cd myrepo
5 $ cat > .hg/hgrc <<EOF
6 > [extensions]
7 > sparse=
8 > strip=
9 > EOF
10
11 $ echo a > show
12 $ echo x > hide
13 $ hg ci -Aqm 'initial'
14
15 $ echo b > show
16 $ echo y > hide
17 $ echo aa > show2
18 $ echo xx > hide2
19 $ hg ci -Aqm 'two'
20
21 Verify basic --include and --reset
22
23 $ hg up -q 0
24 $ hg sparse --include 'hide' -Tjson
25 [
26 {
27 "exclude_rules_added": 0,
28 "files_added": 0,
29 "files_conflicting": 0,
30 "files_dropped": 1,
31 "include_rules_added": 1,
32 "profiles_added": 0
33 }
34 ]
35 $ hg sparse --clear-rules
36 $ hg sparse --include 'hide' --verbose
37 removing show
38 Profile # change: 0
39 Include rule # change: 1
40 Exclude rule # change: 0
41
42 $ hg sparse --reset -Tjson
43 [
44 {
45 "exclude_rules_added": 0,
46 "files_added": 1,
47 "files_conflicting": 0,
48 "files_dropped": 0,
49 "include_rules_added": -1,
50 "profiles_added": 0
51 }
52 ]
53 $ hg sparse --include 'hide'
54 $ hg sparse --reset --verbose
55 getting show
56 Profile # change: 0
57 Include rule # change: -1
58 Exclude rule # change: 0
59
60 Verifying that problematic files still allow us to see the deltas when forcing:
61
62 $ hg sparse --include 'show*'
63 $ touch hide
64 $ hg sparse --delete 'show*' --force -Tjson
65 pending changes to 'hide'
66 [
67 {
68 "exclude_rules_added": 0,
69 "files_added": 0,
70 "files_conflicting": 1,
71 "files_dropped": 0,
72 "include_rules_added": -1,
73 "profiles_added": 0
74 }
75 ]
76 $ hg sparse --include 'show*' --force
77 pending changes to 'hide'
78 $ hg sparse --delete 'show*' --force --verbose
79 pending changes to 'hide'
80 Profile # change: 0
81 Include rule # change: -1
82 Exclude rule # change: 0
@@ -0,0 +1,369 b''
1 test sparse
2
3 $ hg init myrepo
4 $ cd myrepo
5 $ cat > .hg/hgrc <<EOF
6 > [extensions]
7 > sparse=
8 > strip=
9 > EOF
10
11 $ echo a > show
12 $ echo x > hide
13 $ hg ci -Aqm 'initial'
14
15 $ echo b > show
16 $ echo y > hide
17 $ echo aa > show2
18 $ echo xx > hide2
19 $ hg ci -Aqm 'two'
20
21 Verify basic --include
22
23 $ hg up -q 0
24 $ hg sparse --include 'hide'
25 $ ls
26 hide
27
28 Absolute paths outside the repo should just be rejected
29
30 $ hg sparse --include /foo/bar
31 warning: paths cannot start with /, ignoring: ['/foo/bar']
32 $ hg sparse --include '$TESTTMP/myrepo/hide'
33
34 $ hg sparse --include '/root'
35 warning: paths cannot start with /, ignoring: ['/root']
36
37 Verify commiting while sparse includes other files
38
39 $ echo z > hide
40 $ hg ci -Aqm 'edit hide'
41 $ ls
42 hide
43 $ hg manifest
44 hide
45 show
46
47 Verify --reset brings files back
48
49 $ hg sparse --reset
50 $ ls
51 hide
52 show
53 $ cat hide
54 z
55 $ cat show
56 a
57
58 Verify 'hg sparse' default output
59
60 $ hg up -q null
61 $ hg sparse --include 'show*'
62
63 $ hg sparse
64 [include]
65 show*
66 [exclude]
67
68
69 Verify update only writes included files
70
71 $ hg up -q 0
72 $ ls
73 show
74
75 $ hg up -q 1
76 $ ls
77 show
78 show2
79
80 Verify status only shows included files
81
82 $ touch hide
83 $ touch hide3
84 $ echo c > show
85 $ hg status
86 M show
87
88 Adding an excluded file should fail
89
90 $ hg add hide3
91 abort: cannot add 'hide3' - it is outside the sparse checkout
92 (include file with `hg sparse --include <pattern>` or use `hg add -s <file>` to include file directory while adding)
93 [255]
94
95 Verify deleting sparseness while a file has changes fails
96
97 $ hg sparse --delete 'show*'
98 pending changes to 'hide'
99 abort: cannot change sparseness due to pending changes (delete the files or use --force to bring them back dirty)
100 [255]
101
102 Verify deleting sparseness with --force brings back files
103
104 $ hg sparse --delete -f 'show*'
105 pending changes to 'hide'
106 $ ls
107 hide
108 hide2
109 hide3
110 show
111 show2
112 $ hg st
113 M hide
114 M show
115 ? hide3
116
117 Verify editing sparseness fails if pending changes
118
119 $ hg sparse --include 'show*'
120 pending changes to 'hide'
121 abort: could not update sparseness due to pending changes
122 [255]
123
124 Verify adding sparseness hides files
125
126 $ hg sparse --exclude -f 'hide*'
127 pending changes to 'hide'
128 $ ls
129 hide
130 hide3
131 show
132 show2
133 $ hg st
134 M show
135
136 $ hg up -qC .
137 $ hg purge --all --config extensions.purge=
138 $ ls
139 show
140 show2
141
142 Verify rebase temporarily includes excluded files
143
144 $ hg rebase -d 1 -r 2 --config extensions.rebase=
145 rebasing 2:b91df4f39e75 "edit hide" (tip)
146 temporarily included 1 file(s) in the sparse checkout for merging
147 merging hide
148 warning: conflicts while merging hide! (edit, then use 'hg resolve --mark')
149 unresolved conflicts (see hg resolve, then hg rebase --continue)
150 [1]
151
152 $ hg sparse
153 [include]
154
155 [exclude]
156 hide*
157
158 Temporarily Included Files (for merge/rebase):
159 hide
160
161 $ cat hide
162 <<<<<<< dest: 39278f7c08a9 - test: two
163 y
164 =======
165 z
166 >>>>>>> source: b91df4f39e75 - test: edit hide
167
168 Verify aborting a rebase cleans up temporary files
169
170 $ hg rebase --abort --config extensions.rebase=
171 cleaned up 1 temporarily added file(s) from the sparse checkout
172 rebase aborted
173 $ rm hide.orig
174
175 $ ls
176 show
177 show2
178
179 Verify merge fails if merging excluded files
180
181 $ hg up -q 1
182 $ hg merge -r 2
183 temporarily included 1 file(s) in the sparse checkout for merging
184 merging hide
185 warning: conflicts while merging hide! (edit, then use 'hg resolve --mark')
186 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
187 use 'hg resolve' to retry unresolved file merges or 'hg update -C .' to abandon
188 [1]
189 $ hg sparse
190 [include]
191
192 [exclude]
193 hide*
194
195 Temporarily Included Files (for merge/rebase):
196 hide
197
198 $ hg up -C .
199 cleaned up 1 temporarily added file(s) from the sparse checkout
200 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
201 $ hg sparse
202 [include]
203
204 [exclude]
205 hide*
206
207
208 Verify strip -k resets dirstate correctly
209
210 $ hg status
211 $ hg sparse
212 [include]
213
214 [exclude]
215 hide*
216
217 $ hg log -r . -T '{rev}\n' --stat
218 1
219 hide | 2 +-
220 hide2 | 1 +
221 show | 2 +-
222 show2 | 1 +
223 4 files changed, 4 insertions(+), 2 deletions(-)
224
225 $ hg strip -r . -k
226 saved backup bundle to $TESTTMP/myrepo/.hg/strip-backup/39278f7c08a9-ce59e002-backup.hg (glob)
227 $ hg status
228 M show
229 ? show2
230
231 Verify rebase succeeds if all changed files are in sparse checkout
232
233 $ hg commit -Aqm "add show2"
234 $ hg rebase -d 1 --config extensions.rebase=
235 rebasing 2:bdde55290160 "add show2" (tip)
236 saved backup bundle to $TESTTMP/myrepo/.hg/strip-backup/bdde55290160-216ed9c6-backup.hg (glob)
237
238 Verify log --sparse only shows commits that affect the sparse checkout
239
240 $ hg log -T '{rev} '
241 2 1 0 (no-eol)
242 $ hg log --sparse -T '{rev} '
243 2 0 (no-eol)
244
245 Test status on a file in a subdir
246
247 $ mkdir -p dir1/dir2
248 $ touch dir1/dir2/file
249 $ hg sparse -I dir1/dir2
250 $ hg status
251 ? dir1/dir2/file
252
253 Test that add -s adds dirs to sparse profile
254
255 $ hg sparse --reset
256 $ hg sparse --include empty
257 $ hg sparse
258 [include]
259 empty
260 [exclude]
261
262
263
264 $ mkdir add
265 $ touch add/foo
266 $ touch add/bar
267 $ hg add add/foo
268 abort: cannot add 'add/foo' - it is outside the sparse checkout
269 (include file with `hg sparse --include <pattern>` or use `hg add -s <file>` to include file directory while adding)
270 [255]
271 $ hg add -s add/foo
272 $ hg st
273 A add/foo
274 ? add/bar
275 $ hg sparse
276 [include]
277 add
278 empty
279 [exclude]
280
281
282 $ hg add -s add/*
283 add/foo already tracked!
284 $ hg st
285 A add/bar
286 A add/foo
287 $ hg sparse
288 [include]
289 add
290 empty
291 [exclude]
292
293
294
295 $ cd ..
296
297 Test non-sparse repos work while sparse is loaded
298 $ hg init sparserepo
299 $ hg init nonsparserepo
300 $ cd sparserepo
301 $ cat > .hg/hgrc <<EOF
302 > [extensions]
303 > sparse=
304 > EOF
305 $ cd ../nonsparserepo
306 $ echo x > x && hg add x && hg commit -qAm x
307 $ cd ../sparserepo
308 $ hg clone ../nonsparserepo ../nonsparserepo2
309 updating to branch default
310 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
311
312 Test debugrebuilddirstate
313 $ cd ../sparserepo
314 $ touch included
315 $ touch excluded
316 $ hg add included excluded
317 $ hg commit -m 'a commit' -q
318 $ cp .hg/dirstate ../dirstateboth
319 $ hg sparse -X excluded
320 $ cp ../dirstateboth .hg/dirstate
321 $ hg debugrebuilddirstate
322 $ hg debugdirstate
323 n 0 -1 unset included
324
325 Test debugdirstate --minimal where file is in the parent manifest but not the
326 dirstate
327 $ hg sparse -X included
328 $ hg debugdirstate
329 $ cp .hg/dirstate ../dirstateallexcluded
330 $ hg sparse --reset
331 $ hg sparse -X excluded
332 $ cp ../dirstateallexcluded .hg/dirstate
333 $ touch includedadded
334 $ hg add includedadded
335 $ hg debugdirstate --nodates
336 a 0 -1 unset includedadded
337 $ hg debugrebuilddirstate --minimal
338 $ hg debugdirstate --nodates
339 n 0 -1 unset included
340 a 0 -1 * includedadded (glob)
341
342 Test debugdirstate --minimal where a file is not in parent manifest
343 but in the dirstate. This should take into account excluded files in the
344 manifest
345 $ cp ../dirstateboth .hg/dirstate
346 $ touch includedadded
347 $ hg add includedadded
348 $ touch excludednomanifest
349 $ hg add excludednomanifest
350 $ cp .hg/dirstate ../moreexcluded
351 $ hg forget excludednomanifest
352 $ rm excludednomanifest
353 $ hg sparse -X excludednomanifest
354 $ cp ../moreexcluded .hg/dirstate
355 $ hg manifest
356 excluded
357 included
358 We have files in the dirstate that are included and excluded. Some are in the
359 manifest and some are not.
360 $ hg debugdirstate --nodates
361 n 644 0 * excluded (glob)
362 a 0 -1 * excludednomanifest (glob)
363 n 644 0 * included (glob)
364 a 0 -1 * includedadded (glob)
365 $ hg debugrebuilddirstate --minimal
366 $ hg debugdirstate --nodates
367 n 644 0 * included (glob)
368 a 0 -1 * includedadded (glob)
369
General Comments 0
You need to be logged in to leave comments. Login now