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