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