##// END OF EJS Templates
sparse: treat paths as cwd-relative...
Kostia Balytskyi -
r33648:e1c56486 default
parent child Browse files
Show More
@@ -1,336 +1,337 b''
1 1 # sparse.py - allow sparse checkouts of the working directory
2 2 #
3 3 # Copyright 2014 Facebook, Inc.
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 """allow sparse checkouts of the working directory (EXPERIMENTAL)
9 9
10 10 (This extension is not yet protected by backwards compatibility
11 11 guarantees. Any aspect may break in future releases until this
12 12 notice is removed.)
13 13
14 14 This extension allows the working directory to only consist of a
15 15 subset of files for the revision. This allows specific files or
16 16 directories to be explicitly included or excluded. Many repository
17 17 operations have performance proportional to the number of files in
18 18 the working directory. So only realizing a subset of files in the
19 19 working directory can improve performance.
20 20
21 21 Sparse Config Files
22 22 -------------------
23 23
24 24 The set of files that are part of a sparse checkout are defined by
25 25 a sparse config file. The file defines 3 things: includes (files to
26 26 include in the sparse checkout), excludes (files to exclude from the
27 27 sparse checkout), and profiles (links to other config files).
28 28
29 29 The file format is newline delimited. Empty lines and lines beginning
30 30 with ``#`` are ignored.
31 31
32 32 Lines beginning with ``%include `` denote another sparse config file
33 33 to include. e.g. ``%include tests.sparse``. The filename is relative
34 34 to the repository root.
35 35
36 36 The special lines ``[include]`` and ``[exclude]`` denote the section
37 37 for includes and excludes that follow, respectively. It is illegal to
38 38 have ``[include]`` after ``[exclude]``.
39 39
40 40 Non-special lines resemble file patterns to be added to either includes
41 41 or excludes. The syntax of these lines is documented by :hg:`help patterns`.
42 42 Patterns are interpreted as ``glob:`` by default and match against the
43 43 root of the repository.
44 44
45 45 Exclusion patterns take precedence over inclusion patterns. So even
46 46 if a file is explicitly included, an ``[exclude]`` entry can remove it.
47 47
48 48 For example, say you have a repository with 3 directories, ``frontend/``,
49 49 ``backend/``, and ``tools/``. ``frontend/`` and ``backend/`` correspond
50 50 to different projects and it is uncommon for someone working on one
51 51 to need the files for the other. But ``tools/`` contains files shared
52 52 between both projects. Your sparse config files may resemble::
53 53
54 54 # frontend.sparse
55 55 frontend/**
56 56 tools/**
57 57
58 58 # backend.sparse
59 59 backend/**
60 60 tools/**
61 61
62 62 Say the backend grows in size. Or there's a directory with thousands
63 63 of files you wish to exclude. You can modify the profile to exclude
64 64 certain files::
65 65
66 66 [include]
67 67 backend/**
68 68 tools/**
69 69
70 70 [exclude]
71 71 tools/tests/**
72 72 """
73 73
74 74 from __future__ import absolute_import
75 75
76 76 from mercurial.i18n import _
77 77 from mercurial import (
78 78 cmdutil,
79 79 commands,
80 80 dirstate,
81 81 error,
82 82 extensions,
83 83 hg,
84 84 match as matchmod,
85 85 registrar,
86 86 sparse,
87 87 util,
88 88 )
89 89
90 90 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
91 91 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
92 92 # be specifying the version(s) of Mercurial they are tested with, or
93 93 # leave the attribute unspecified.
94 94 testedwith = 'ships-with-hg-core'
95 95
96 96 cmdtable = {}
97 97 command = registrar.command(cmdtable)
98 98
99 99 def extsetup(ui):
100 100 sparse.enabled = True
101 101
102 102 _setupclone(ui)
103 103 _setuplog(ui)
104 104 _setupadd(ui)
105 105 _setupdirstate(ui)
106 106
107 107 def replacefilecache(cls, propname, replacement):
108 108 """Replace a filecache property with a new class. This allows changing the
109 109 cache invalidation condition."""
110 110 origcls = cls
111 111 assert callable(replacement)
112 112 while cls is not object:
113 113 if propname in cls.__dict__:
114 114 orig = cls.__dict__[propname]
115 115 setattr(cls, propname, replacement(orig))
116 116 break
117 117 cls = cls.__bases__[0]
118 118
119 119 if cls is object:
120 120 raise AttributeError(_("type '%s' has no property '%s'") % (origcls,
121 121 propname))
122 122
123 123 def _setuplog(ui):
124 124 entry = commands.table['^log|history']
125 125 entry[1].append(('', 'sparse', None,
126 126 "limit to changesets affecting the sparse checkout"))
127 127
128 128 def _logrevs(orig, repo, opts):
129 129 revs = orig(repo, opts)
130 130 if opts.get('sparse'):
131 131 sparsematch = sparse.matcher(repo)
132 132 def ctxmatch(rev):
133 133 ctx = repo[rev]
134 134 return any(f for f in ctx.files() if sparsematch(f))
135 135 revs = revs.filter(ctxmatch)
136 136 return revs
137 137 extensions.wrapfunction(cmdutil, '_logrevs', _logrevs)
138 138
139 139 def _clonesparsecmd(orig, ui, repo, *args, **opts):
140 140 include_pat = opts.get('include')
141 141 exclude_pat = opts.get('exclude')
142 142 enableprofile_pat = opts.get('enable_profile')
143 143 include = exclude = enableprofile = False
144 144 if include_pat:
145 145 pat = include_pat
146 146 include = True
147 147 if exclude_pat:
148 148 pat = exclude_pat
149 149 exclude = True
150 150 if enableprofile_pat:
151 151 pat = enableprofile_pat
152 152 enableprofile = True
153 153 if sum([include, exclude, enableprofile]) > 1:
154 154 raise error.Abort(_("too many flags specified."))
155 155 if include or exclude or enableprofile:
156 156 def clonesparse(orig, self, node, overwrite, *args, **kwargs):
157 157 sparse.updateconfig(self.unfiltered(), pat, {}, include=include,
158 exclude=exclude, enableprofile=enableprofile)
158 exclude=exclude, enableprofile=enableprofile,
159 usereporootpaths=True)
159 160 return orig(self, node, overwrite, *args, **kwargs)
160 161 extensions.wrapfunction(hg, 'updaterepo', clonesparse)
161 162 return orig(ui, repo, *args, **opts)
162 163
163 164 def _setupclone(ui):
164 165 entry = commands.table['^clone']
165 166 entry[1].append(('', 'enable-profile', [],
166 167 'enable a sparse profile'))
167 168 entry[1].append(('', 'include', [],
168 169 'include sparse pattern'))
169 170 entry[1].append(('', 'exclude', [],
170 171 'exclude sparse pattern'))
171 172 extensions.wrapcommand(commands.table, 'clone', _clonesparsecmd)
172 173
173 174 def _setupadd(ui):
174 175 entry = commands.table['^add']
175 176 entry[1].append(('s', 'sparse', None,
176 177 'also include directories of added files in sparse config'))
177 178
178 179 def _add(orig, ui, repo, *pats, **opts):
179 180 if opts.get('sparse'):
180 181 dirs = set()
181 182 for pat in pats:
182 183 dirname, basename = util.split(pat)
183 184 dirs.add(dirname)
184 185 sparse.updateconfig(repo, list(dirs), opts, include=True)
185 186 return orig(ui, repo, *pats, **opts)
186 187
187 188 extensions.wrapcommand(commands.table, 'add', _add)
188 189
189 190 def _setupdirstate(ui):
190 191 """Modify the dirstate to prevent stat'ing excluded files,
191 192 and to prevent modifications to files outside the checkout.
192 193 """
193 194
194 195 def walk(orig, self, match, subrepos, unknown, ignored, full=True):
195 196 match = matchmod.intersectmatchers(match, self._sparsematcher)
196 197 return orig(self, match, subrepos, unknown, ignored, full)
197 198
198 199 extensions.wrapfunction(dirstate.dirstate, 'walk', walk)
199 200
200 201 # dirstate.rebuild should not add non-matching files
201 202 def _rebuild(orig, self, parent, allfiles, changedfiles=None):
202 203 matcher = self._sparsematcher
203 204 if not matcher.always():
204 205 allfiles = allfiles.matches(matcher)
205 206 if changedfiles:
206 207 changedfiles = [f for f in changedfiles if matcher(f)]
207 208
208 209 if changedfiles is not None:
209 210 # In _rebuild, these files will be deleted from the dirstate
210 211 # when they are not found to be in allfiles
211 212 dirstatefilestoremove = set(f for f in self if not matcher(f))
212 213 changedfiles = dirstatefilestoremove.union(changedfiles)
213 214
214 215 return orig(self, parent, allfiles, changedfiles)
215 216 extensions.wrapfunction(dirstate.dirstate, 'rebuild', _rebuild)
216 217
217 218 # Prevent adding files that are outside the sparse checkout
218 219 editfuncs = ['normal', 'add', 'normallookup', 'copy', 'remove', 'merge']
219 220 hint = _('include file with `hg debugsparse --include <pattern>` or use ' +
220 221 '`hg add -s <file>` to include file directory while adding')
221 222 for func in editfuncs:
222 223 def _wrapper(orig, self, *args):
223 224 sparsematch = self._sparsematcher
224 225 if not sparsematch.always():
225 226 for f in args:
226 227 if (f is not None and not sparsematch(f) and
227 228 f not in self):
228 229 raise error.Abort(_("cannot add '%s' - it is outside "
229 230 "the sparse checkout") % f,
230 231 hint=hint)
231 232 return orig(self, *args)
232 233 extensions.wrapfunction(dirstate.dirstate, func, _wrapper)
233 234
234 235 @command('^debugsparse', [
235 236 ('I', 'include', False, _('include files in the sparse checkout')),
236 237 ('X', 'exclude', False, _('exclude files in the sparse checkout')),
237 238 ('d', 'delete', False, _('delete an include/exclude rule')),
238 239 ('f', 'force', False, _('allow changing rules even with pending changes')),
239 240 ('', 'enable-profile', False, _('enables the specified profile')),
240 241 ('', 'disable-profile', False, _('disables the specified profile')),
241 242 ('', 'import-rules', False, _('imports rules from a file')),
242 243 ('', 'clear-rules', False, _('clears local include/exclude rules')),
243 244 ('', 'refresh', False, _('updates the working after sparseness changes')),
244 245 ('', 'reset', False, _('makes the repo full again')),
245 246 ] + commands.templateopts,
246 247 _('[--OPTION] PATTERN...'))
247 248 def debugsparse(ui, repo, *pats, **opts):
248 249 """make the current checkout sparse, or edit the existing checkout
249 250
250 251 The sparse command is used to make the current checkout sparse.
251 252 This means files that don't meet the sparse condition will not be
252 253 written to disk, or show up in any working copy operations. It does
253 254 not affect files in history in any way.
254 255
255 256 Passing no arguments prints the currently applied sparse rules.
256 257
257 258 --include and --exclude are used to add and remove files from the sparse
258 259 checkout. The effects of adding an include or exclude rule are applied
259 260 immediately. If applying the new rule would cause a file with pending
260 261 changes to be added or removed, the command will fail. Pass --force to
261 262 force a rule change even with pending changes (the changes on disk will
262 263 be preserved).
263 264
264 265 --delete removes an existing include/exclude rule. The effects are
265 266 immediate.
266 267
267 268 --refresh refreshes the files on disk based on the sparse rules. This is
268 269 only necessary if .hg/sparse was changed by hand.
269 270
270 271 --enable-profile and --disable-profile accept a path to a .hgsparse file.
271 272 This allows defining sparse checkouts and tracking them inside the
272 273 repository. This is useful for defining commonly used sparse checkouts for
273 274 many people to use. As the profile definition changes over time, the sparse
274 275 checkout will automatically be updated appropriately, depending on which
275 276 changeset is checked out. Changes to .hgsparse are not applied until they
276 277 have been committed.
277 278
278 279 --import-rules accepts a path to a file containing rules in the .hgsparse
279 280 format, allowing you to add --include, --exclude and --enable-profile rules
280 281 in bulk. Like the --include, --exclude and --enable-profile switches, the
281 282 changes are applied immediately.
282 283
283 284 --clear-rules removes all local include and exclude rules, while leaving
284 285 any enabled profiles in place.
285 286
286 287 Returns 0 if editing the sparse checkout succeeds.
287 288 """
288 289 include = opts.get('include')
289 290 exclude = opts.get('exclude')
290 291 force = opts.get('force')
291 292 enableprofile = opts.get('enable_profile')
292 293 disableprofile = opts.get('disable_profile')
293 294 importrules = opts.get('import_rules')
294 295 clearrules = opts.get('clear_rules')
295 296 delete = opts.get('delete')
296 297 refresh = opts.get('refresh')
297 298 reset = opts.get('reset')
298 299 count = sum([include, exclude, enableprofile, disableprofile, delete,
299 300 importrules, refresh, clearrules, reset])
300 301 if count > 1:
301 302 raise error.Abort(_("too many flags specified"))
302 303
303 304 if count == 0:
304 305 if repo.vfs.exists('sparse'):
305 306 ui.status(repo.vfs.read("sparse") + "\n")
306 307 temporaryincludes = sparse.readtemporaryincludes(repo)
307 308 if temporaryincludes:
308 309 ui.status(_("Temporarily Included Files (for merge/rebase):\n"))
309 310 ui.status(("\n".join(temporaryincludes) + "\n"))
310 311 else:
311 312 ui.status(_('repo is not sparse\n'))
312 313 return
313 314
314 315 if include or exclude or delete or reset or enableprofile or disableprofile:
315 316 sparse.updateconfig(repo, pats, opts, include=include, exclude=exclude,
316 317 reset=reset, delete=delete,
317 318 enableprofile=enableprofile,
318 319 disableprofile=disableprofile, force=force)
319 320
320 321 if importrules:
321 322 sparse.importfromfiles(repo, opts, pats, force=force)
322 323
323 324 if clearrules:
324 325 sparse.clearrules(repo, force=force)
325 326
326 327 if refresh:
327 328 try:
328 329 wlock = repo.wlock()
329 330 fcounts = map(
330 331 len,
331 332 sparse.refreshwdir(repo, repo.status(), sparse.matcher(repo),
332 333 force=force))
333 334 sparse.printchanges(ui, opts, added=fcounts[0], dropped=fcounts[1],
334 335 conflicting=fcounts[2])
335 336 finally:
336 337 wlock.release()
@@ -1,689 +1,704 b''
1 1 # sparse.py - functionality for sparse checkouts
2 2 #
3 3 # Copyright 2014 Facebook, Inc.
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import collections
11 11 import hashlib
12 12 import os
13 13
14 14 from .i18n import _
15 15 from .node import nullid
16 16 from . import (
17 17 error,
18 18 match as matchmod,
19 19 merge as mergemod,
20 pathutil,
20 21 pycompat,
21 22 scmutil,
22 23 util,
23 24 )
24 25
25 26 # Whether sparse features are enabled. This variable is intended to be
26 27 # temporary to facilitate porting sparse to core. It should eventually be
27 28 # a per-repo option, possibly a repo requirement.
28 29 enabled = False
29 30
30 31 def parseconfig(ui, raw):
31 32 """Parse sparse config file content.
32 33
33 34 Returns a tuple of includes, excludes, and profiles.
34 35 """
35 36 includes = set()
36 37 excludes = set()
37 38 profiles = set()
38 39 current = None
39 40 havesection = False
40 41
41 42 for line in raw.split('\n'):
42 43 line = line.strip()
43 44 if not line or line.startswith('#'):
44 45 # empty or comment line, skip
45 46 continue
46 47 elif line.startswith('%include '):
47 48 line = line[9:].strip()
48 49 if line:
49 50 profiles.add(line)
50 51 elif line == '[include]':
51 52 if havesection and current != includes:
52 53 # TODO pass filename into this API so we can report it.
53 54 raise error.Abort(_('sparse config cannot have includes ' +
54 55 'after excludes'))
55 56 havesection = True
56 57 current = includes
57 58 continue
58 59 elif line == '[exclude]':
59 60 havesection = True
60 61 current = excludes
61 62 elif line:
62 63 if current is None:
63 64 raise error.Abort(_('sparse config entry outside of '
64 65 'section: %s') % line,
65 66 hint=_('add an [include] or [exclude] line '
66 67 'to declare the entry type'))
67 68
68 69 if line.strip().startswith('/'):
69 70 ui.warn(_('warning: sparse profile cannot use' +
70 71 ' paths starting with /, ignoring %s\n') % line)
71 72 continue
72 73 current.add(line)
73 74
74 75 return includes, excludes, profiles
75 76
76 77 # Exists as separate function to facilitate monkeypatching.
77 78 def readprofile(repo, profile, changeid):
78 79 """Resolve the raw content of a sparse profile file."""
79 80 # TODO add some kind of cache here because this incurs a manifest
80 81 # resolve and can be slow.
81 82 return repo.filectx(profile, changeid=changeid).data()
82 83
83 84 def patternsforrev(repo, rev):
84 85 """Obtain sparse checkout patterns for the given rev.
85 86
86 87 Returns a tuple of iterables representing includes, excludes, and
87 88 patterns.
88 89 """
89 90 # Feature isn't enabled. No-op.
90 91 if not enabled:
91 92 return set(), set(), set()
92 93
93 94 raw = repo.vfs.tryread('sparse')
94 95 if not raw:
95 96 return set(), set(), set()
96 97
97 98 if rev is None:
98 99 raise error.Abort(_('cannot parse sparse patterns from working '
99 100 'directory'))
100 101
101 102 includes, excludes, profiles = parseconfig(repo.ui, raw)
102 103 ctx = repo[rev]
103 104
104 105 if profiles:
105 106 visited = set()
106 107 while profiles:
107 108 profile = profiles.pop()
108 109 if profile in visited:
109 110 continue
110 111
111 112 visited.add(profile)
112 113
113 114 try:
114 115 raw = readprofile(repo, profile, rev)
115 116 except error.ManifestLookupError:
116 117 msg = (
117 118 "warning: sparse profile '%s' not found "
118 119 "in rev %s - ignoring it\n" % (profile, ctx))
119 120 # experimental config: sparse.missingwarning
120 121 if repo.ui.configbool(
121 122 'sparse', 'missingwarning'):
122 123 repo.ui.warn(msg)
123 124 else:
124 125 repo.ui.debug(msg)
125 126 continue
126 127
127 128 pincludes, pexcludes, subprofs = parseconfig(repo.ui, raw)
128 129 includes.update(pincludes)
129 130 excludes.update(pexcludes)
130 131 profiles.update(subprofs)
131 132
132 133 profiles = visited
133 134
134 135 if includes:
135 136 includes.add('.hg*')
136 137
137 138 return includes, excludes, profiles
138 139
139 140 def activeconfig(repo):
140 141 """Determine the active sparse config rules.
141 142
142 143 Rules are constructed by reading the current sparse config and bringing in
143 144 referenced profiles from parents of the working directory.
144 145 """
145 146 revs = [repo.changelog.rev(node) for node in
146 147 repo.dirstate.parents() if node != nullid]
147 148
148 149 allincludes = set()
149 150 allexcludes = set()
150 151 allprofiles = set()
151 152
152 153 for rev in revs:
153 154 includes, excludes, profiles = patternsforrev(repo, rev)
154 155 allincludes |= includes
155 156 allexcludes |= excludes
156 157 allprofiles |= profiles
157 158
158 159 return allincludes, allexcludes, allprofiles
159 160
160 161 def configsignature(repo, includetemp=True):
161 162 """Obtain the signature string for the current sparse configuration.
162 163
163 164 This is used to construct a cache key for matchers.
164 165 """
165 166 cache = repo._sparsesignaturecache
166 167
167 168 signature = cache.get('signature')
168 169
169 170 if includetemp:
170 171 tempsignature = cache.get('tempsignature')
171 172 else:
172 173 tempsignature = '0'
173 174
174 175 if signature is None or (includetemp and tempsignature is None):
175 176 signature = hashlib.sha1(repo.vfs.tryread('sparse')).hexdigest()
176 177 cache['signature'] = signature
177 178
178 179 if includetemp:
179 180 raw = repo.vfs.tryread('tempsparse')
180 181 tempsignature = hashlib.sha1(raw).hexdigest()
181 182 cache['tempsignature'] = tempsignature
182 183
183 184 return '%s %s' % (signature, tempsignature)
184 185
185 186 def writeconfig(repo, includes, excludes, profiles):
186 187 """Write the sparse config file given a sparse configuration."""
187 188 with repo.vfs('sparse', 'wb') as fh:
188 189 for p in sorted(profiles):
189 190 fh.write('%%include %s\n' % p)
190 191
191 192 if includes:
192 193 fh.write('[include]\n')
193 194 for i in sorted(includes):
194 195 fh.write(i)
195 196 fh.write('\n')
196 197
197 198 if excludes:
198 199 fh.write('[exclude]\n')
199 200 for e in sorted(excludes):
200 201 fh.write(e)
201 202 fh.write('\n')
202 203
203 204 repo._sparsesignaturecache.clear()
204 205
205 206 def readtemporaryincludes(repo):
206 207 raw = repo.vfs.tryread('tempsparse')
207 208 if not raw:
208 209 return set()
209 210
210 211 return set(raw.split('\n'))
211 212
212 213 def writetemporaryincludes(repo, includes):
213 214 repo.vfs.write('tempsparse', '\n'.join(sorted(includes)))
214 215 repo._sparsesignaturecache.clear()
215 216
216 217 def addtemporaryincludes(repo, additional):
217 218 includes = readtemporaryincludes(repo)
218 219 for i in additional:
219 220 includes.add(i)
220 221 writetemporaryincludes(repo, includes)
221 222
222 223 def prunetemporaryincludes(repo):
223 224 if not enabled or not repo.vfs.exists('tempsparse'):
224 225 return
225 226
226 227 s = repo.status()
227 228 if s.modified or s.added or s.removed or s.deleted:
228 229 # Still have pending changes. Don't bother trying to prune.
229 230 return
230 231
231 232 sparsematch = matcher(repo, includetemp=False)
232 233 dirstate = repo.dirstate
233 234 actions = []
234 235 dropped = []
235 236 tempincludes = readtemporaryincludes(repo)
236 237 for file in tempincludes:
237 238 if file in dirstate and not sparsematch(file):
238 239 message = _('dropping temporarily included sparse files')
239 240 actions.append((file, None, message))
240 241 dropped.append(file)
241 242
242 243 typeactions = collections.defaultdict(list)
243 244 typeactions['r'] = actions
244 245 mergemod.applyupdates(repo, typeactions, repo[None], repo['.'], False)
245 246
246 247 # Fix dirstate
247 248 for file in dropped:
248 249 dirstate.drop(file)
249 250
250 251 repo.vfs.unlink('tempsparse')
251 252 repo._sparsesignaturecache.clear()
252 253 msg = _('cleaned up %d temporarily added file(s) from the '
253 254 'sparse checkout\n')
254 255 repo.ui.status(msg % len(tempincludes))
255 256
256 257 def forceincludematcher(matcher, includes):
257 258 """Returns a matcher that returns true for any of the forced includes
258 259 before testing against the actual matcher."""
259 260 kindpats = [('path', include, '') for include in includes]
260 261 includematcher = matchmod.includematcher('', '', kindpats)
261 262 return matchmod.unionmatcher([includematcher, matcher])
262 263
263 264 def matcher(repo, revs=None, includetemp=True):
264 265 """Obtain a matcher for sparse working directories for the given revs.
265 266
266 267 If multiple revisions are specified, the matcher is the union of all
267 268 revs.
268 269
269 270 ``includetemp`` indicates whether to use the temporary sparse profile.
270 271 """
271 272 # If sparse isn't enabled, sparse matcher matches everything.
272 273 if not enabled:
273 274 return matchmod.always(repo.root, '')
274 275
275 276 if not revs or revs == [None]:
276 277 revs = [repo.changelog.rev(node)
277 278 for node in repo.dirstate.parents() if node != nullid]
278 279
279 280 signature = configsignature(repo, includetemp=includetemp)
280 281
281 282 key = '%s %s' % (signature, ' '.join(map(pycompat.bytestr, revs)))
282 283
283 284 result = repo._sparsematchercache.get(key)
284 285 if result:
285 286 return result
286 287
287 288 matchers = []
288 289 for rev in revs:
289 290 try:
290 291 includes, excludes, profiles = patternsforrev(repo, rev)
291 292
292 293 if includes or excludes:
293 294 # Explicitly include subdirectories of includes so
294 295 # status will walk them down to the actual include.
295 296 subdirs = set()
296 297 for include in includes:
297 298 # TODO consider using posix path functions here so Windows
298 299 # \ directory separators don't come into play.
299 300 dirname = os.path.dirname(include)
300 301 # basename is used to avoid issues with absolute
301 302 # paths (which on Windows can include the drive).
302 303 while os.path.basename(dirname):
303 304 subdirs.add(dirname)
304 305 dirname = os.path.dirname(dirname)
305 306
306 307 matcher = matchmod.match(repo.root, '', [],
307 308 include=includes, exclude=excludes,
308 309 default='relpath')
309 310 if subdirs:
310 311 matcher = forceincludematcher(matcher, subdirs)
311 312 matchers.append(matcher)
312 313 except IOError:
313 314 pass
314 315
315 316 if not matchers:
316 317 result = matchmod.always(repo.root, '')
317 318 elif len(matchers) == 1:
318 319 result = matchers[0]
319 320 else:
320 321 result = matchmod.unionmatcher(matchers)
321 322
322 323 if includetemp:
323 324 tempincludes = readtemporaryincludes(repo)
324 325 result = forceincludematcher(result, tempincludes)
325 326
326 327 repo._sparsematchercache[key] = result
327 328
328 329 return result
329 330
330 331 def filterupdatesactions(repo, wctx, mctx, branchmerge, actions):
331 332 """Filter updates to only lay out files that match the sparse rules."""
332 333 if not enabled:
333 334 return actions
334 335
335 336 oldrevs = [pctx.rev() for pctx in wctx.parents()]
336 337 oldsparsematch = matcher(repo, oldrevs)
337 338
338 339 if oldsparsematch.always():
339 340 return actions
340 341
341 342 files = set()
342 343 prunedactions = {}
343 344
344 345 if branchmerge:
345 346 # If we're merging, use the wctx filter, since we're merging into
346 347 # the wctx.
347 348 sparsematch = matcher(repo, [wctx.parents()[0].rev()])
348 349 else:
349 350 # If we're updating, use the target context's filter, since we're
350 351 # moving to the target context.
351 352 sparsematch = matcher(repo, [mctx.rev()])
352 353
353 354 temporaryfiles = []
354 355 for file, action in actions.iteritems():
355 356 type, args, msg = action
356 357 files.add(file)
357 358 if sparsematch(file):
358 359 prunedactions[file] = action
359 360 elif type == 'm':
360 361 temporaryfiles.append(file)
361 362 prunedactions[file] = action
362 363 elif branchmerge:
363 364 if type != 'k':
364 365 temporaryfiles.append(file)
365 366 prunedactions[file] = action
366 367 elif type == 'f':
367 368 prunedactions[file] = action
368 369 elif file in wctx:
369 370 prunedactions[file] = ('r', args, msg)
370 371
371 372 if len(temporaryfiles) > 0:
372 373 repo.ui.status(_('temporarily included %d file(s) in the sparse '
373 374 'checkout for merging\n') % len(temporaryfiles))
374 375 addtemporaryincludes(repo, temporaryfiles)
375 376
376 377 # Add the new files to the working copy so they can be merged, etc
377 378 actions = []
378 379 message = 'temporarily adding to sparse checkout'
379 380 wctxmanifest = repo[None].manifest()
380 381 for file in temporaryfiles:
381 382 if file in wctxmanifest:
382 383 fctx = repo[None][file]
383 384 actions.append((file, (fctx.flags(), False), message))
384 385
385 386 typeactions = collections.defaultdict(list)
386 387 typeactions['g'] = actions
387 388 mergemod.applyupdates(repo, typeactions, repo[None], repo['.'],
388 389 False)
389 390
390 391 dirstate = repo.dirstate
391 392 for file, flags, msg in actions:
392 393 dirstate.normal(file)
393 394
394 395 profiles = activeconfig(repo)[2]
395 396 changedprofiles = profiles & files
396 397 # If an active profile changed during the update, refresh the checkout.
397 398 # Don't do this during a branch merge, since all incoming changes should
398 399 # have been handled by the temporary includes above.
399 400 if changedprofiles and not branchmerge:
400 401 mf = mctx.manifest()
401 402 for file in mf:
402 403 old = oldsparsematch(file)
403 404 new = sparsematch(file)
404 405 if not old and new:
405 406 flags = mf.flags(file)
406 407 prunedactions[file] = ('g', (flags, False), '')
407 408 elif old and not new:
408 409 prunedactions[file] = ('r', [], '')
409 410
410 411 return prunedactions
411 412
412 413 def refreshwdir(repo, origstatus, origsparsematch, force=False):
413 414 """Refreshes working directory by taking sparse config into account.
414 415
415 416 The old status and sparse matcher is compared against the current sparse
416 417 matcher.
417 418
418 419 Will abort if a file with pending changes is being excluded or included
419 420 unless ``force`` is True.
420 421 """
421 422 # Verify there are no pending changes
422 423 pending = set()
423 424 pending.update(origstatus.modified)
424 425 pending.update(origstatus.added)
425 426 pending.update(origstatus.removed)
426 427 sparsematch = matcher(repo)
427 428 abort = False
428 429
429 430 for f in pending:
430 431 if not sparsematch(f):
431 432 repo.ui.warn(_("pending changes to '%s'\n") % f)
432 433 abort = not force
433 434
434 435 if abort:
435 436 raise error.Abort(_('could not update sparseness due to pending '
436 437 'changes'))
437 438
438 439 # Calculate actions
439 440 dirstate = repo.dirstate
440 441 ctx = repo['.']
441 442 added = []
442 443 lookup = []
443 444 dropped = []
444 445 mf = ctx.manifest()
445 446 files = set(mf)
446 447
447 448 actions = {}
448 449
449 450 for file in files:
450 451 old = origsparsematch(file)
451 452 new = sparsematch(file)
452 453 # Add files that are newly included, or that don't exist in
453 454 # the dirstate yet.
454 455 if (new and not old) or (old and new and not file in dirstate):
455 456 fl = mf.flags(file)
456 457 if repo.wvfs.exists(file):
457 458 actions[file] = ('e', (fl,), '')
458 459 lookup.append(file)
459 460 else:
460 461 actions[file] = ('g', (fl, False), '')
461 462 added.append(file)
462 463 # Drop files that are newly excluded, or that still exist in
463 464 # the dirstate.
464 465 elif (old and not new) or (not old and not new and file in dirstate):
465 466 dropped.append(file)
466 467 if file not in pending:
467 468 actions[file] = ('r', [], '')
468 469
469 470 # Verify there are no pending changes in newly included files
470 471 abort = False
471 472 for file in lookup:
472 473 repo.ui.warn(_("pending changes to '%s'\n") % file)
473 474 abort = not force
474 475 if abort:
475 476 raise error.Abort(_('cannot change sparseness due to pending '
476 477 'changes (delete the files or use '
477 478 '--force to bring them back dirty)'))
478 479
479 480 # Check for files that were only in the dirstate.
480 481 for file, state in dirstate.iteritems():
481 482 if not file in files:
482 483 old = origsparsematch(file)
483 484 new = sparsematch(file)
484 485 if old and not new:
485 486 dropped.append(file)
486 487
487 488 # Apply changes to disk
488 489 typeactions = dict((m, []) for m in 'a f g am cd dc r dm dg m e k'.split())
489 490 for f, (m, args, msg) in actions.iteritems():
490 491 if m not in typeactions:
491 492 typeactions[m] = []
492 493 typeactions[m].append((f, args, msg))
493 494
494 495 mergemod.applyupdates(repo, typeactions, repo[None], repo['.'], False)
495 496
496 497 # Fix dirstate
497 498 for file in added:
498 499 dirstate.normal(file)
499 500
500 501 for file in dropped:
501 502 dirstate.drop(file)
502 503
503 504 for file in lookup:
504 505 # File exists on disk, and we're bringing it back in an unknown state.
505 506 dirstate.normallookup(file)
506 507
507 508 return added, dropped, lookup
508 509
509 510 def aftercommit(repo, node):
510 511 """Perform actions after a working directory commit."""
511 512 # This function is called unconditionally, even if sparse isn't
512 513 # enabled.
513 514 ctx = repo[node]
514 515
515 516 profiles = patternsforrev(repo, ctx.rev())[2]
516 517
517 518 # profiles will only have data if sparse is enabled.
518 519 if profiles & set(ctx.files()):
519 520 origstatus = repo.status()
520 521 origsparsematch = matcher(repo)
521 522 refreshwdir(repo, origstatus, origsparsematch, force=True)
522 523
523 524 prunetemporaryincludes(repo)
524 525
525 526 def _updateconfigandrefreshwdir(repo, includes, excludes, profiles,
526 527 force=False, removing=False):
527 528 """Update the sparse config and working directory state."""
528 529 raw = repo.vfs.tryread('sparse')
529 530 oldincludes, oldexcludes, oldprofiles = parseconfig(repo.ui, raw)
530 531
531 532 oldstatus = repo.status()
532 533 oldmatch = matcher(repo)
533 534 oldrequires = set(repo.requirements)
534 535
535 536 # TODO remove this try..except once the matcher integrates better
536 537 # with dirstate. We currently have to write the updated config
537 538 # because that will invalidate the matcher cache and force a
538 539 # re-read. We ideally want to update the cached matcher on the
539 540 # repo instance then flush the new config to disk once wdir is
540 541 # updated. But this requires massive rework to matcher() and its
541 542 # consumers.
542 543
543 544 if 'exp-sparse' in oldrequires and removing:
544 545 repo.requirements.discard('exp-sparse')
545 546 scmutil.writerequires(repo.vfs, repo.requirements)
546 547 elif 'exp-sparse' not in oldrequires:
547 548 repo.requirements.add('exp-sparse')
548 549 scmutil.writerequires(repo.vfs, repo.requirements)
549 550
550 551 try:
551 552 writeconfig(repo, includes, excludes, profiles)
552 553 return refreshwdir(repo, oldstatus, oldmatch, force=force)
553 554 except Exception:
554 555 if repo.requirements != oldrequires:
555 556 repo.requirements.clear()
556 557 repo.requirements |= oldrequires
557 558 scmutil.writerequires(repo.vfs, repo.requirements)
558 559 writeconfig(repo, oldincludes, oldexcludes, oldprofiles)
559 560 raise
560 561
561 562 def clearrules(repo, force=False):
562 563 """Clears include/exclude rules from the sparse config.
563 564
564 565 The remaining sparse config only has profiles, if defined. The working
565 566 directory is refreshed, as needed.
566 567 """
567 568 with repo.wlock():
568 569 raw = repo.vfs.tryread('sparse')
569 570 includes, excludes, profiles = parseconfig(repo.ui, raw)
570 571
571 572 if not includes and not excludes:
572 573 return
573 574
574 575 _updateconfigandrefreshwdir(repo, set(), set(), profiles, force=force)
575 576
576 577 def importfromfiles(repo, opts, paths, force=False):
577 578 """Import sparse config rules from files.
578 579
579 580 The updated sparse config is written out and the working directory
580 581 is refreshed, as needed.
581 582 """
582 583 with repo.wlock():
583 584 # read current configuration
584 585 raw = repo.vfs.tryread('sparse')
585 586 includes, excludes, profiles = parseconfig(repo.ui, raw)
586 587 aincludes, aexcludes, aprofiles = activeconfig(repo)
587 588
588 589 # Import rules on top; only take in rules that are not yet
589 590 # part of the active rules.
590 591 changed = False
591 592 for p in paths:
592 593 with util.posixfile(util.expandpath(p)) as fh:
593 594 raw = fh.read()
594 595
595 596 iincludes, iexcludes, iprofiles = parseconfig(repo.ui, raw)
596 597 oldsize = len(includes) + len(excludes) + len(profiles)
597 598 includes.update(iincludes - aincludes)
598 599 excludes.update(iexcludes - aexcludes)
599 600 profiles.update(iprofiles - aprofiles)
600 601 if len(includes) + len(excludes) + len(profiles) > oldsize:
601 602 changed = True
602 603
603 604 profilecount = includecount = excludecount = 0
604 605 fcounts = (0, 0, 0)
605 606
606 607 if changed:
607 608 profilecount = len(profiles - aprofiles)
608 609 includecount = len(includes - aincludes)
609 610 excludecount = len(excludes - aexcludes)
610 611
611 612 fcounts = map(len, _updateconfigandrefreshwdir(
612 613 repo, includes, excludes, profiles, force=force))
613 614
614 615 printchanges(repo.ui, opts, profilecount, includecount, excludecount,
615 616 *fcounts)
616 617
617 618 def updateconfig(repo, pats, opts, include=False, exclude=False, reset=False,
618 619 delete=False, enableprofile=False, disableprofile=False,
619 force=False):
620 force=False, usereporootpaths=False):
620 621 """Perform a sparse config update.
621 622
622 623 Only one of the actions may be performed.
623 624
624 625 The new config is written out and a working directory refresh is performed.
625 626 """
626 627 with repo.wlock():
627 628 raw = repo.vfs.tryread('sparse')
628 629 oldinclude, oldexclude, oldprofiles = parseconfig(repo.ui, raw)
629 630
630 631 if reset:
631 632 newinclude = set()
632 633 newexclude = set()
633 634 newprofiles = set()
634 635 else:
635 636 newinclude = set(oldinclude)
636 637 newexclude = set(oldexclude)
637 638 newprofiles = set(oldprofiles)
638 639
639 640 if any(os.path.isabs(pat) for pat in pats):
640 641 raise error.Abort(_('paths cannot be absolute'))
641 642
643 if not usereporootpaths:
644 # let's treat paths as relative to cwd
645 root, cwd = repo.root, repo.getcwd()
646 abspats = []
647 for kindpat in pats:
648 kind, pat = matchmod._patsplit(kindpat, None)
649 if kind in matchmod.cwdrelativepatternkinds or kind is None:
650 ap = (kind + ':' if kind else '') +\
651 pathutil.canonpath(root, cwd, pat)
652 abspats.append(ap)
653 else:
654 abspats.append(kindpat)
655 pats = abspats
656
642 657 if include:
643 658 newinclude.update(pats)
644 659 elif exclude:
645 660 newexclude.update(pats)
646 661 elif enableprofile:
647 662 newprofiles.update(pats)
648 663 elif disableprofile:
649 664 newprofiles.difference_update(pats)
650 665 elif delete:
651 666 newinclude.difference_update(pats)
652 667 newexclude.difference_update(pats)
653 668
654 669 profilecount = (len(newprofiles - oldprofiles) -
655 670 len(oldprofiles - newprofiles))
656 671 includecount = (len(newinclude - oldinclude) -
657 672 len(oldinclude - newinclude))
658 673 excludecount = (len(newexclude - oldexclude) -
659 674 len(oldexclude - newexclude))
660 675
661 676 fcounts = map(len, _updateconfigandrefreshwdir(
662 677 repo, newinclude, newexclude, newprofiles, force=force,
663 678 removing=reset))
664 679
665 680 printchanges(repo.ui, opts, profilecount, includecount,
666 681 excludecount, *fcounts)
667 682
668 683 def printchanges(ui, opts, profilecount=0, includecount=0, excludecount=0,
669 684 added=0, dropped=0, conflicting=0):
670 685 """Print output summarizing sparse config changes."""
671 686 with ui.formatter('sparse', opts) as fm:
672 687 fm.startitem()
673 688 fm.condwrite(ui.verbose, 'profiles_added', _('Profiles changed: %d\n'),
674 689 profilecount)
675 690 fm.condwrite(ui.verbose, 'include_rules_added',
676 691 _('Include rules changed: %d\n'), includecount)
677 692 fm.condwrite(ui.verbose, 'exclude_rules_added',
678 693 _('Exclude rules changed: %d\n'), excludecount)
679 694
680 695 # In 'plain' verbose mode, mergemod.applyupdates already outputs what
681 696 # files are added or removed outside of the templating formatter
682 697 # framework. No point in repeating ourselves in that case.
683 698 if not fm.isplain():
684 699 fm.condwrite(ui.verbose, 'files_added', _('Files added: %d\n'),
685 700 added)
686 701 fm.condwrite(ui.verbose, 'files_dropped', _('Files dropped: %d\n'),
687 702 dropped)
688 703 fm.condwrite(ui.verbose, 'files_conflicting',
689 704 _('Files conflicting: %d\n'), conflicting)
@@ -1,372 +1,397 b''
1 1 test sparse
2 2
3 3 $ hg init myrepo
4 4 $ cd myrepo
5 5 $ cat > .hg/hgrc <<EOF
6 6 > [extensions]
7 7 > sparse=
8 8 > strip=
9 9 > EOF
10 10
11 11 $ echo a > show
12 12 $ echo x > hide
13 13 $ hg ci -Aqm 'initial'
14 14
15 15 $ echo b > show
16 16 $ echo y > hide
17 17 $ echo aa > show2
18 18 $ echo xx > hide2
19 19 $ hg ci -Aqm 'two'
20 20
21 21 Verify basic --include
22 22
23 23 $ hg up -q 0
24 24 $ hg debugsparse --include 'hide'
25 25 $ ls
26 26 hide
27 27
28 28 Absolute paths outside the repo should just be rejected
29 29
30 30 #if no-windows
31 31 $ hg debugsparse --include /foo/bar
32 32 abort: paths cannot be absolute
33 33 [255]
34 34 $ hg debugsparse --include '$TESTTMP/myrepo/hide'
35 35
36 36 $ hg debugsparse --include '/root'
37 37 abort: paths cannot be absolute
38 38 [255]
39 39 #else
40 40 TODO: See if this can be made to fail the same way as on Unix
41 41 $ hg debugsparse --include /c/foo/bar
42 42 abort: paths cannot be absolute
43 43 [255]
44 44 $ hg debugsparse --include '$TESTTMP/myrepo/hide'
45 45
46 46 $ hg debugsparse --include '/c/root'
47 47 abort: paths cannot be absolute
48 48 [255]
49 49 #endif
50 50
51 Paths should be treated as cwd-relative, not repo-root-relative
52 $ mkdir subdir && cd subdir
53 $ hg debugsparse --include path
54 $ hg debugsparse
55 [include]
56 $TESTTMP/myrepo/hide
57 hide
58 subdir/path (glob)
59
60 $ cd ..
61 $ echo hello > subdir/file2.ext
62 $ cd subdir
63 $ hg debugsparse --include '**.ext' # let us test globs
64 $ hg debugsparse --include 'path:abspath' # and a path: pattern
65 $ cd ..
66 $ hg debugsparse
67 [include]
68 $TESTTMP/myrepo/hide
69 hide
70 path:abspath
71 subdir/**.ext
72 subdir/path (glob)
73
74 $ rm -rf subdir
75
51 76 Verify commiting while sparse includes other files
52 77
53 78 $ echo z > hide
54 79 $ hg ci -Aqm 'edit hide'
55 80 $ ls
56 81 hide
57 82 $ hg manifest
58 83 hide
59 84 show
60 85
61 86 Verify --reset brings files back
62 87
63 88 $ hg debugsparse --reset
64 89 $ ls
65 90 hide
66 91 show
67 92 $ cat hide
68 93 z
69 94 $ cat show
70 95 a
71 96
72 97 Verify 'hg debugsparse' default output
73 98
74 99 $ hg up -q null
75 100 $ hg debugsparse --include 'show*'
76 101
77 102 $ hg debugsparse
78 103 [include]
79 104 show*
80 105
81 106 Verify update only writes included files
82 107
83 108 $ hg up -q 0
84 109 $ ls
85 110 show
86 111
87 112 $ hg up -q 1
88 113 $ ls
89 114 show
90 115 show2
91 116
92 117 Verify status only shows included files
93 118
94 119 $ touch hide
95 120 $ touch hide3
96 121 $ echo c > show
97 122 $ hg status
98 123 M show
99 124
100 125 Adding an excluded file should fail
101 126
102 127 $ hg add hide3
103 128 abort: cannot add 'hide3' - it is outside the sparse checkout
104 129 (include file with `hg debugsparse --include <pattern>` or use `hg add -s <file>` to include file directory while adding)
105 130 [255]
106 131
107 132 Verify deleting sparseness while a file has changes fails
108 133
109 134 $ hg debugsparse --delete 'show*'
110 135 pending changes to 'hide'
111 136 abort: cannot change sparseness due to pending changes (delete the files or use --force to bring them back dirty)
112 137 [255]
113 138
114 139 Verify deleting sparseness with --force brings back files
115 140
116 141 $ hg debugsparse --delete -f 'show*'
117 142 pending changes to 'hide'
118 143 $ ls
119 144 hide
120 145 hide2
121 146 hide3
122 147 show
123 148 show2
124 149 $ hg st
125 150 M hide
126 151 M show
127 152 ? hide3
128 153
129 154 Verify editing sparseness fails if pending changes
130 155
131 156 $ hg debugsparse --include 'show*'
132 157 pending changes to 'hide'
133 158 abort: could not update sparseness due to pending changes
134 159 [255]
135 160
136 161 Verify adding sparseness hides files
137 162
138 163 $ hg debugsparse --exclude -f 'hide*'
139 164 pending changes to 'hide'
140 165 $ ls
141 166 hide
142 167 hide3
143 168 show
144 169 show2
145 170 $ hg st
146 171 M show
147 172
148 173 $ hg up -qC .
149 174 TODO: add an option to purge to also purge files outside the sparse config?
150 175 $ hg purge --all --config extensions.purge=
151 176 $ ls
152 177 hide
153 178 hide3
154 179 show
155 180 show2
156 181 For now, manually remove the files
157 182 $ rm hide hide3
158 183
159 184 Verify rebase temporarily includes excluded files
160 185
161 186 $ hg rebase -d 1 -r 2 --config extensions.rebase=
162 187 rebasing 2:b91df4f39e75 "edit hide" (tip)
163 188 temporarily included 1 file(s) in the sparse checkout for merging
164 189 merging hide
165 190 warning: conflicts while merging hide! (edit, then use 'hg resolve --mark')
166 191 unresolved conflicts (see hg resolve, then hg rebase --continue)
167 192 [1]
168 193
169 194 $ hg debugsparse
170 195 [exclude]
171 196 hide*
172 197
173 198 Temporarily Included Files (for merge/rebase):
174 199 hide
175 200
176 201 $ cat hide
177 202 <<<<<<< dest: 39278f7c08a9 - test: two
178 203 y
179 204 =======
180 205 z
181 206 >>>>>>> source: b91df4f39e75 - test: edit hide
182 207
183 208 Verify aborting a rebase cleans up temporary files
184 209
185 210 $ hg rebase --abort --config extensions.rebase=
186 211 cleaned up 1 temporarily added file(s) from the sparse checkout
187 212 rebase aborted
188 213 $ rm hide.orig
189 214
190 215 $ ls
191 216 show
192 217 show2
193 218
194 219 Verify merge fails if merging excluded files
195 220
196 221 $ hg up -q 1
197 222 $ hg merge -r 2
198 223 temporarily included 1 file(s) in the sparse checkout for merging
199 224 merging hide
200 225 warning: conflicts while merging hide! (edit, then use 'hg resolve --mark')
201 226 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
202 227 use 'hg resolve' to retry unresolved file merges or 'hg update -C .' to abandon
203 228 [1]
204 229 $ hg debugsparse
205 230 [exclude]
206 231 hide*
207 232
208 233 Temporarily Included Files (for merge/rebase):
209 234 hide
210 235
211 236 $ hg up -C .
212 237 cleaned up 1 temporarily added file(s) from the sparse checkout
213 238 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
214 239 $ hg debugsparse
215 240 [exclude]
216 241 hide*
217 242
218 243
219 244 Verify strip -k resets dirstate correctly
220 245
221 246 $ hg status
222 247 $ hg debugsparse
223 248 [exclude]
224 249 hide*
225 250
226 251 $ hg log -r . -T '{rev}\n' --stat
227 252 1
228 253 hide | 2 +-
229 254 hide2 | 1 +
230 255 show | 2 +-
231 256 show2 | 1 +
232 257 4 files changed, 4 insertions(+), 2 deletions(-)
233 258
234 259 $ hg strip -r . -k
235 260 saved backup bundle to $TESTTMP/myrepo/.hg/strip-backup/39278f7c08a9-ce59e002-backup.hg (glob)
236 261 $ hg status
237 262 M show
238 263 ? show2
239 264
240 265 Verify rebase succeeds if all changed files are in sparse checkout
241 266
242 267 $ hg commit -Aqm "add show2"
243 268 $ hg rebase -d 1 --config extensions.rebase=
244 269 rebasing 2:bdde55290160 "add show2" (tip)
245 270 saved backup bundle to $TESTTMP/myrepo/.hg/strip-backup/bdde55290160-216ed9c6-rebase.hg (glob)
246 271
247 272 Verify log --sparse only shows commits that affect the sparse checkout
248 273
249 274 $ hg log -T '{rev} '
250 275 2 1 0 (no-eol)
251 276 $ hg log --sparse -T '{rev} '
252 277 2 0 (no-eol)
253 278
254 279 Test status on a file in a subdir
255 280
256 281 $ mkdir -p dir1/dir2
257 282 $ touch dir1/dir2/file
258 283 $ hg debugsparse -I dir1/dir2
259 284 $ hg status
260 285 ? dir1/dir2/file
261 286
262 287 Test that add -s adds dirs to sparse profile
263 288
264 289 $ hg debugsparse --reset
265 290 $ hg debugsparse --include empty
266 291 $ hg debugsparse
267 292 [include]
268 293 empty
269 294
270 295
271 296 $ mkdir add
272 297 $ touch add/foo
273 298 $ touch add/bar
274 299 $ hg add add/foo
275 300 abort: cannot add 'add/foo' - it is outside the sparse checkout
276 301 (include file with `hg debugsparse --include <pattern>` or use `hg add -s <file>` to include file directory while adding)
277 302 [255]
278 303 $ hg add -s add/foo
279 304 $ hg st
280 305 A add/foo
281 306 ? add/bar
282 307 $ hg debugsparse
283 308 [include]
284 309 add
285 310 empty
286 311
287 312 $ hg add -s add/*
288 313 add/foo already tracked!
289 314 $ hg st
290 315 A add/bar
291 316 A add/foo
292 317 $ hg debugsparse
293 318 [include]
294 319 add
295 320 empty
296 321
297 322
298 323 $ cd ..
299 324
300 325 Test non-sparse repos work while sparse is loaded
301 326 $ hg init sparserepo
302 327 $ hg init nonsparserepo
303 328 $ cd sparserepo
304 329 $ cat > .hg/hgrc <<EOF
305 330 > [extensions]
306 331 > sparse=
307 332 > EOF
308 333 $ cd ../nonsparserepo
309 334 $ echo x > x && hg add x && hg commit -qAm x
310 335 $ cd ../sparserepo
311 336 $ hg clone ../nonsparserepo ../nonsparserepo2
312 337 updating to branch default
313 338 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
314 339
315 340 Test debugrebuilddirstate
316 341 $ cd ../sparserepo
317 342 $ touch included
318 343 $ touch excluded
319 344 $ hg add included excluded
320 345 $ hg commit -m 'a commit' -q
321 346 $ cp .hg/dirstate ../dirstateboth
322 347 $ hg debugsparse -X excluded
323 348 $ cp ../dirstateboth .hg/dirstate
324 349 $ hg debugrebuilddirstate
325 350 $ hg debugdirstate
326 351 n 0 -1 unset included
327 352
328 353 Test debugdirstate --minimal where file is in the parent manifest but not the
329 354 dirstate
330 355 $ hg debugsparse -X included
331 356 $ hg debugdirstate
332 357 $ cp .hg/dirstate ../dirstateallexcluded
333 358 $ hg debugsparse --reset
334 359 $ hg debugsparse -X excluded
335 360 $ cp ../dirstateallexcluded .hg/dirstate
336 361 $ touch includedadded
337 362 $ hg add includedadded
338 363 $ hg debugdirstate --nodates
339 364 a 0 -1 unset includedadded
340 365 $ hg debugrebuilddirstate --minimal
341 366 $ hg debugdirstate --nodates
342 367 n 0 -1 unset included
343 368 a 0 -1 * includedadded (glob)
344 369
345 370 Test debugdirstate --minimal where a file is not in parent manifest
346 371 but in the dirstate. This should take into account excluded files in the
347 372 manifest
348 373 $ cp ../dirstateboth .hg/dirstate
349 374 $ touch includedadded
350 375 $ hg add includedadded
351 376 $ touch excludednomanifest
352 377 $ hg add excludednomanifest
353 378 $ cp .hg/dirstate ../moreexcluded
354 379 $ hg forget excludednomanifest
355 380 $ rm excludednomanifest
356 381 $ hg debugsparse -X excludednomanifest
357 382 $ cp ../moreexcluded .hg/dirstate
358 383 $ hg manifest
359 384 excluded
360 385 included
361 386 We have files in the dirstate that are included and excluded. Some are in the
362 387 manifest and some are not.
363 388 $ hg debugdirstate --nodates
364 389 n 644 0 * excluded (glob)
365 390 a 0 -1 * excludednomanifest (glob)
366 391 n 644 0 * included (glob)
367 392 a 0 -1 * includedadded (glob)
368 393 $ hg debugrebuilddirstate --minimal
369 394 $ hg debugdirstate --nodates
370 395 n 644 0 * included (glob)
371 396 a 0 -1 * includedadded (glob)
372 397
General Comments 0
You need to be logged in to leave comments. Login now