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