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