##// END OF EJS Templates
sparse: move code for clearing rules to core...
Gregory Szorc -
r33354:4695f182 default
parent child Browse files
Show More
@@ -1,542 +1,531 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]``. If no sections are defined,
39 entries are assumed to be in the ``[include]`` section.
39 entries are assumed to be in the ``[include]`` section.
40
40
41 Non-special lines resemble file patterns to be added to either includes
41 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`.
42 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
43 Patterns are interpreted as ``glob:`` by default and match against the
44 root of the repository.
44 root of the repository.
45
45
46 Exclusion patterns take precedence over inclusion patterns. So even
46 Exclusion patterns take precedence over inclusion patterns. So even
47 if a file is explicitly included, an ``[exclude]`` entry can remove it.
47 if a file is explicitly included, an ``[exclude]`` entry can remove it.
48
48
49 For example, say you have a repository with 3 directories, ``frontend/``,
49 For example, say you have a repository with 3 directories, ``frontend/``,
50 ``backend/``, and ``tools/``. ``frontend/`` and ``backend/`` correspond
50 ``backend/``, and ``tools/``. ``frontend/`` and ``backend/`` correspond
51 to different projects and it is uncommon for someone working on one
51 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
52 to need the files for the other. But ``tools/`` contains files shared
53 between both projects. Your sparse config files may resemble::
53 between both projects. Your sparse config files may resemble::
54
54
55 # frontend.sparse
55 # frontend.sparse
56 frontend/**
56 frontend/**
57 tools/**
57 tools/**
58
58
59 # backend.sparse
59 # backend.sparse
60 backend/**
60 backend/**
61 tools/**
61 tools/**
62
62
63 Say the backend grows in size. Or there's a directory with thousands
63 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
64 of files you wish to exclude. You can modify the profile to exclude
65 certain files::
65 certain files::
66
66
67 [include]
67 [include]
68 backend/**
68 backend/**
69 tools/**
69 tools/**
70
70
71 [exclude]
71 [exclude]
72 tools/tests/**
72 tools/tests/**
73 """
73 """
74
74
75 from __future__ import absolute_import
75 from __future__ import absolute_import
76
76
77 from mercurial.i18n import _
77 from mercurial.i18n import _
78 from mercurial.node import nullid
78 from mercurial.node import nullid
79 from mercurial import (
79 from mercurial import (
80 cmdutil,
80 cmdutil,
81 commands,
81 commands,
82 dirstate,
82 dirstate,
83 error,
83 error,
84 extensions,
84 extensions,
85 hg,
85 hg,
86 localrepo,
86 localrepo,
87 match as matchmod,
87 match as matchmod,
88 registrar,
88 registrar,
89 sparse,
89 sparse,
90 util,
90 util,
91 )
91 )
92
92
93 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
93 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
94 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
94 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
95 # be specifying the version(s) of Mercurial they are tested with, or
95 # be specifying the version(s) of Mercurial they are tested with, or
96 # leave the attribute unspecified.
96 # leave the attribute unspecified.
97 testedwith = 'ships-with-hg-core'
97 testedwith = 'ships-with-hg-core'
98
98
99 cmdtable = {}
99 cmdtable = {}
100 command = registrar.command(cmdtable)
100 command = registrar.command(cmdtable)
101
101
102 def extsetup(ui):
102 def extsetup(ui):
103 sparse.enabled = True
103 sparse.enabled = True
104
104
105 _setupclone(ui)
105 _setupclone(ui)
106 _setuplog(ui)
106 _setuplog(ui)
107 _setupadd(ui)
107 _setupadd(ui)
108 _setupdirstate(ui)
108 _setupdirstate(ui)
109
109
110 def reposetup(ui, repo):
110 def reposetup(ui, repo):
111 if not util.safehasattr(repo, 'dirstate'):
111 if not util.safehasattr(repo, 'dirstate'):
112 return
112 return
113
113
114 if 'dirstate' in repo._filecache:
114 if 'dirstate' in repo._filecache:
115 repo.dirstate.repo = repo
115 repo.dirstate.repo = repo
116
116
117 def replacefilecache(cls, propname, replacement):
117 def replacefilecache(cls, propname, replacement):
118 """Replace a filecache property with a new class. This allows changing the
118 """Replace a filecache property with a new class. This allows changing the
119 cache invalidation condition."""
119 cache invalidation condition."""
120 origcls = cls
120 origcls = cls
121 assert callable(replacement)
121 assert callable(replacement)
122 while cls is not object:
122 while cls is not object:
123 if propname in cls.__dict__:
123 if propname in cls.__dict__:
124 orig = cls.__dict__[propname]
124 orig = cls.__dict__[propname]
125 setattr(cls, propname, replacement(orig))
125 setattr(cls, propname, replacement(orig))
126 break
126 break
127 cls = cls.__bases__[0]
127 cls = cls.__bases__[0]
128
128
129 if cls is object:
129 if cls is object:
130 raise AttributeError(_("type '%s' has no property '%s'") % (origcls,
130 raise AttributeError(_("type '%s' has no property '%s'") % (origcls,
131 propname))
131 propname))
132
132
133 def _setuplog(ui):
133 def _setuplog(ui):
134 entry = commands.table['^log|history']
134 entry = commands.table['^log|history']
135 entry[1].append(('', 'sparse', None,
135 entry[1].append(('', 'sparse', None,
136 "limit to changesets affecting the sparse checkout"))
136 "limit to changesets affecting the sparse checkout"))
137
137
138 def _logrevs(orig, repo, opts):
138 def _logrevs(orig, repo, opts):
139 revs = orig(repo, opts)
139 revs = orig(repo, opts)
140 if opts.get('sparse'):
140 if opts.get('sparse'):
141 sparsematch = sparse.matcher(repo)
141 sparsematch = sparse.matcher(repo)
142 def ctxmatch(rev):
142 def ctxmatch(rev):
143 ctx = repo[rev]
143 ctx = repo[rev]
144 return any(f for f in ctx.files() if sparsematch(f))
144 return any(f for f in ctx.files() if sparsematch(f))
145 revs = revs.filter(ctxmatch)
145 revs = revs.filter(ctxmatch)
146 return revs
146 return revs
147 extensions.wrapfunction(cmdutil, '_logrevs', _logrevs)
147 extensions.wrapfunction(cmdutil, '_logrevs', _logrevs)
148
148
149 def _clonesparsecmd(orig, ui, repo, *args, **opts):
149 def _clonesparsecmd(orig, ui, repo, *args, **opts):
150 include_pat = opts.get('include')
150 include_pat = opts.get('include')
151 exclude_pat = opts.get('exclude')
151 exclude_pat = opts.get('exclude')
152 enableprofile_pat = opts.get('enable_profile')
152 enableprofile_pat = opts.get('enable_profile')
153 include = exclude = enableprofile = False
153 include = exclude = enableprofile = False
154 if include_pat:
154 if include_pat:
155 pat = include_pat
155 pat = include_pat
156 include = True
156 include = True
157 if exclude_pat:
157 if exclude_pat:
158 pat = exclude_pat
158 pat = exclude_pat
159 exclude = True
159 exclude = True
160 if enableprofile_pat:
160 if enableprofile_pat:
161 pat = enableprofile_pat
161 pat = enableprofile_pat
162 enableprofile = True
162 enableprofile = True
163 if sum([include, exclude, enableprofile]) > 1:
163 if sum([include, exclude, enableprofile]) > 1:
164 raise error.Abort(_("too many flags specified."))
164 raise error.Abort(_("too many flags specified."))
165 if include or exclude or enableprofile:
165 if include or exclude or enableprofile:
166 def clonesparse(orig, self, node, overwrite, *args, **kwargs):
166 def clonesparse(orig, self, node, overwrite, *args, **kwargs):
167 _config(self.ui, self.unfiltered(), pat, {}, include=include,
167 _config(self.ui, self.unfiltered(), pat, {}, include=include,
168 exclude=exclude, enableprofile=enableprofile)
168 exclude=exclude, enableprofile=enableprofile)
169 return orig(self, node, overwrite, *args, **kwargs)
169 return orig(self, node, overwrite, *args, **kwargs)
170 extensions.wrapfunction(hg, 'updaterepo', clonesparse)
170 extensions.wrapfunction(hg, 'updaterepo', clonesparse)
171 return orig(ui, repo, *args, **opts)
171 return orig(ui, repo, *args, **opts)
172
172
173 def _setupclone(ui):
173 def _setupclone(ui):
174 entry = commands.table['^clone']
174 entry = commands.table['^clone']
175 entry[1].append(('', 'enable-profile', [],
175 entry[1].append(('', 'enable-profile', [],
176 'enable a sparse profile'))
176 'enable a sparse profile'))
177 entry[1].append(('', 'include', [],
177 entry[1].append(('', 'include', [],
178 'include sparse pattern'))
178 'include sparse pattern'))
179 entry[1].append(('', 'exclude', [],
179 entry[1].append(('', 'exclude', [],
180 'exclude sparse pattern'))
180 'exclude sparse pattern'))
181 extensions.wrapcommand(commands.table, 'clone', _clonesparsecmd)
181 extensions.wrapcommand(commands.table, 'clone', _clonesparsecmd)
182
182
183 def _setupadd(ui):
183 def _setupadd(ui):
184 entry = commands.table['^add']
184 entry = commands.table['^add']
185 entry[1].append(('s', 'sparse', None,
185 entry[1].append(('s', 'sparse', None,
186 'also include directories of added files in sparse config'))
186 'also include directories of added files in sparse config'))
187
187
188 def _add(orig, ui, repo, *pats, **opts):
188 def _add(orig, ui, repo, *pats, **opts):
189 if opts.get('sparse'):
189 if opts.get('sparse'):
190 dirs = set()
190 dirs = set()
191 for pat in pats:
191 for pat in pats:
192 dirname, basename = util.split(pat)
192 dirname, basename = util.split(pat)
193 dirs.add(dirname)
193 dirs.add(dirname)
194 _config(ui, repo, list(dirs), opts, include=True)
194 _config(ui, repo, list(dirs), opts, include=True)
195 return orig(ui, repo, *pats, **opts)
195 return orig(ui, repo, *pats, **opts)
196
196
197 extensions.wrapcommand(commands.table, 'add', _add)
197 extensions.wrapcommand(commands.table, 'add', _add)
198
198
199 def _setupdirstate(ui):
199 def _setupdirstate(ui):
200 """Modify the dirstate to prevent stat'ing excluded files,
200 """Modify the dirstate to prevent stat'ing excluded files,
201 and to prevent modifications to files outside the checkout.
201 and to prevent modifications to files outside the checkout.
202 """
202 """
203
203
204 def _dirstate(orig, repo):
204 def _dirstate(orig, repo):
205 dirstate = orig(repo)
205 dirstate = orig(repo)
206 dirstate.repo = repo
206 dirstate.repo = repo
207 return dirstate
207 return dirstate
208 extensions.wrapfunction(
208 extensions.wrapfunction(
209 localrepo.localrepository.dirstate, 'func', _dirstate)
209 localrepo.localrepository.dirstate, 'func', _dirstate)
210
210
211 # The atrocity below is needed to wrap dirstate._ignore. It is a cached
211 # The atrocity below is needed to wrap dirstate._ignore. It is a cached
212 # property, which means normal function wrapping doesn't work.
212 # property, which means normal function wrapping doesn't work.
213 class ignorewrapper(object):
213 class ignorewrapper(object):
214 def __init__(self, orig):
214 def __init__(self, orig):
215 self.orig = orig
215 self.orig = orig
216 self.origignore = None
216 self.origignore = None
217 self.func = None
217 self.func = None
218 self.sparsematch = None
218 self.sparsematch = None
219
219
220 def __get__(self, obj, type=None):
220 def __get__(self, obj, type=None):
221 repo = obj.repo
221 repo = obj.repo
222 origignore = self.orig.__get__(obj)
222 origignore = self.orig.__get__(obj)
223
223
224 sparsematch = sparse.matcher(repo)
224 sparsematch = sparse.matcher(repo)
225 if sparsematch.always():
225 if sparsematch.always():
226 return origignore
226 return origignore
227
227
228 if self.sparsematch != sparsematch or self.origignore != origignore:
228 if self.sparsematch != sparsematch or self.origignore != origignore:
229 self.func = matchmod.unionmatcher([
229 self.func = matchmod.unionmatcher([
230 origignore, matchmod.negatematcher(sparsematch)])
230 origignore, matchmod.negatematcher(sparsematch)])
231 self.sparsematch = sparsematch
231 self.sparsematch = sparsematch
232 self.origignore = origignore
232 self.origignore = origignore
233 return self.func
233 return self.func
234
234
235 def __set__(self, obj, value):
235 def __set__(self, obj, value):
236 return self.orig.__set__(obj, value)
236 return self.orig.__set__(obj, value)
237
237
238 def __delete__(self, obj):
238 def __delete__(self, obj):
239 return self.orig.__delete__(obj)
239 return self.orig.__delete__(obj)
240
240
241 replacefilecache(dirstate.dirstate, '_ignore', ignorewrapper)
241 replacefilecache(dirstate.dirstate, '_ignore', ignorewrapper)
242
242
243 # dirstate.rebuild should not add non-matching files
243 # dirstate.rebuild should not add non-matching files
244 def _rebuild(orig, self, parent, allfiles, changedfiles=None):
244 def _rebuild(orig, self, parent, allfiles, changedfiles=None):
245 matcher = sparse.matcher(self.repo)
245 matcher = sparse.matcher(self.repo)
246 if not matcher.always():
246 if not matcher.always():
247 allfiles = allfiles.matches(matcher)
247 allfiles = allfiles.matches(matcher)
248 if changedfiles:
248 if changedfiles:
249 changedfiles = [f for f in changedfiles if matcher(f)]
249 changedfiles = [f for f in changedfiles if matcher(f)]
250
250
251 if changedfiles is not None:
251 if changedfiles is not None:
252 # In _rebuild, these files will be deleted from the dirstate
252 # In _rebuild, these files will be deleted from the dirstate
253 # when they are not found to be in allfiles
253 # when they are not found to be in allfiles
254 dirstatefilestoremove = set(f for f in self if not matcher(f))
254 dirstatefilestoremove = set(f for f in self if not matcher(f))
255 changedfiles = dirstatefilestoremove.union(changedfiles)
255 changedfiles = dirstatefilestoremove.union(changedfiles)
256
256
257 return orig(self, parent, allfiles, changedfiles)
257 return orig(self, parent, allfiles, changedfiles)
258 extensions.wrapfunction(dirstate.dirstate, 'rebuild', _rebuild)
258 extensions.wrapfunction(dirstate.dirstate, 'rebuild', _rebuild)
259
259
260 # Prevent adding files that are outside the sparse checkout
260 # Prevent adding files that are outside the sparse checkout
261 editfuncs = ['normal', 'add', 'normallookup', 'copy', 'remove', 'merge']
261 editfuncs = ['normal', 'add', 'normallookup', 'copy', 'remove', 'merge']
262 hint = _('include file with `hg debugsparse --include <pattern>` or use ' +
262 hint = _('include file with `hg debugsparse --include <pattern>` or use ' +
263 '`hg add -s <file>` to include file directory while adding')
263 '`hg add -s <file>` to include file directory while adding')
264 for func in editfuncs:
264 for func in editfuncs:
265 def _wrapper(orig, self, *args):
265 def _wrapper(orig, self, *args):
266 repo = self.repo
266 repo = self.repo
267 sparsematch = sparse.matcher(repo)
267 sparsematch = sparse.matcher(repo)
268 if not sparsematch.always():
268 if not sparsematch.always():
269 dirstate = repo.dirstate
269 dirstate = repo.dirstate
270 for f in args:
270 for f in args:
271 if (f is not None and not sparsematch(f) and
271 if (f is not None and not sparsematch(f) and
272 f not in dirstate):
272 f not in dirstate):
273 raise error.Abort(_("cannot add '%s' - it is outside "
273 raise error.Abort(_("cannot add '%s' - it is outside "
274 "the sparse checkout") % f,
274 "the sparse checkout") % f,
275 hint=hint)
275 hint=hint)
276 return orig(self, *args)
276 return orig(self, *args)
277 extensions.wrapfunction(dirstate.dirstate, func, _wrapper)
277 extensions.wrapfunction(dirstate.dirstate, func, _wrapper)
278
278
279 @command('^debugsparse', [
279 @command('^debugsparse', [
280 ('I', 'include', False, _('include files in the sparse checkout')),
280 ('I', 'include', False, _('include files in the sparse checkout')),
281 ('X', 'exclude', False, _('exclude files in the sparse checkout')),
281 ('X', 'exclude', False, _('exclude files in the sparse checkout')),
282 ('d', 'delete', False, _('delete an include/exclude rule')),
282 ('d', 'delete', False, _('delete an include/exclude rule')),
283 ('f', 'force', False, _('allow changing rules even with pending changes')),
283 ('f', 'force', False, _('allow changing rules even with pending changes')),
284 ('', 'enable-profile', False, _('enables the specified profile')),
284 ('', 'enable-profile', False, _('enables the specified profile')),
285 ('', 'disable-profile', False, _('disables the specified profile')),
285 ('', 'disable-profile', False, _('disables the specified profile')),
286 ('', 'import-rules', False, _('imports rules from a file')),
286 ('', 'import-rules', False, _('imports rules from a file')),
287 ('', 'clear-rules', False, _('clears local include/exclude rules')),
287 ('', 'clear-rules', False, _('clears local include/exclude rules')),
288 ('', 'refresh', False, _('updates the working after sparseness changes')),
288 ('', 'refresh', False, _('updates the working after sparseness changes')),
289 ('', 'reset', False, _('makes the repo full again')),
289 ('', 'reset', False, _('makes the repo full again')),
290 ] + commands.templateopts,
290 ] + commands.templateopts,
291 _('[--OPTION] PATTERN...'))
291 _('[--OPTION] PATTERN...'))
292 def debugsparse(ui, repo, *pats, **opts):
292 def debugsparse(ui, repo, *pats, **opts):
293 """make the current checkout sparse, or edit the existing checkout
293 """make the current checkout sparse, or edit the existing checkout
294
294
295 The sparse command is used to make the current checkout sparse.
295 The sparse command is used to make the current checkout sparse.
296 This means files that don't meet the sparse condition will not be
296 This means files that don't meet the sparse condition will not be
297 written to disk, or show up in any working copy operations. It does
297 written to disk, or show up in any working copy operations. It does
298 not affect files in history in any way.
298 not affect files in history in any way.
299
299
300 Passing no arguments prints the currently applied sparse rules.
300 Passing no arguments prints the currently applied sparse rules.
301
301
302 --include and --exclude are used to add and remove files from the sparse
302 --include and --exclude are used to add and remove files from the sparse
303 checkout. The effects of adding an include or exclude rule are applied
303 checkout. The effects of adding an include or exclude rule are applied
304 immediately. If applying the new rule would cause a file with pending
304 immediately. If applying the new rule would cause a file with pending
305 changes to be added or removed, the command will fail. Pass --force to
305 changes to be added or removed, the command will fail. Pass --force to
306 force a rule change even with pending changes (the changes on disk will
306 force a rule change even with pending changes (the changes on disk will
307 be preserved).
307 be preserved).
308
308
309 --delete removes an existing include/exclude rule. The effects are
309 --delete removes an existing include/exclude rule. The effects are
310 immediate.
310 immediate.
311
311
312 --refresh refreshes the files on disk based on the sparse rules. This is
312 --refresh refreshes the files on disk based on the sparse rules. This is
313 only necessary if .hg/sparse was changed by hand.
313 only necessary if .hg/sparse was changed by hand.
314
314
315 --enable-profile and --disable-profile accept a path to a .hgsparse file.
315 --enable-profile and --disable-profile accept a path to a .hgsparse file.
316 This allows defining sparse checkouts and tracking them inside the
316 This allows defining sparse checkouts and tracking them inside the
317 repository. This is useful for defining commonly used sparse checkouts for
317 repository. This is useful for defining commonly used sparse checkouts for
318 many people to use. As the profile definition changes over time, the sparse
318 many people to use. As the profile definition changes over time, the sparse
319 checkout will automatically be updated appropriately, depending on which
319 checkout will automatically be updated appropriately, depending on which
320 changeset is checked out. Changes to .hgsparse are not applied until they
320 changeset is checked out. Changes to .hgsparse are not applied until they
321 have been committed.
321 have been committed.
322
322
323 --import-rules accepts a path to a file containing rules in the .hgsparse
323 --import-rules accepts a path to a file containing rules in the .hgsparse
324 format, allowing you to add --include, --exclude and --enable-profile rules
324 format, allowing you to add --include, --exclude and --enable-profile rules
325 in bulk. Like the --include, --exclude and --enable-profile switches, the
325 in bulk. Like the --include, --exclude and --enable-profile switches, the
326 changes are applied immediately.
326 changes are applied immediately.
327
327
328 --clear-rules removes all local include and exclude rules, while leaving
328 --clear-rules removes all local include and exclude rules, while leaving
329 any enabled profiles in place.
329 any enabled profiles in place.
330
330
331 Returns 0 if editing the sparse checkout succeeds.
331 Returns 0 if editing the sparse checkout succeeds.
332 """
332 """
333 include = opts.get('include')
333 include = opts.get('include')
334 exclude = opts.get('exclude')
334 exclude = opts.get('exclude')
335 force = opts.get('force')
335 force = opts.get('force')
336 enableprofile = opts.get('enable_profile')
336 enableprofile = opts.get('enable_profile')
337 disableprofile = opts.get('disable_profile')
337 disableprofile = opts.get('disable_profile')
338 importrules = opts.get('import_rules')
338 importrules = opts.get('import_rules')
339 clearrules = opts.get('clear_rules')
339 clearrules = opts.get('clear_rules')
340 delete = opts.get('delete')
340 delete = opts.get('delete')
341 refresh = opts.get('refresh')
341 refresh = opts.get('refresh')
342 reset = opts.get('reset')
342 reset = opts.get('reset')
343 count = sum([include, exclude, enableprofile, disableprofile, delete,
343 count = sum([include, exclude, enableprofile, disableprofile, delete,
344 importrules, refresh, clearrules, reset])
344 importrules, refresh, clearrules, reset])
345 if count > 1:
345 if count > 1:
346 raise error.Abort(_("too many flags specified"))
346 raise error.Abort(_("too many flags specified"))
347
347
348 if count == 0:
348 if count == 0:
349 if repo.vfs.exists('sparse'):
349 if repo.vfs.exists('sparse'):
350 ui.status(repo.vfs.read("sparse") + "\n")
350 ui.status(repo.vfs.read("sparse") + "\n")
351 temporaryincludes = sparse.readtemporaryincludes(repo)
351 temporaryincludes = sparse.readtemporaryincludes(repo)
352 if temporaryincludes:
352 if temporaryincludes:
353 ui.status(_("Temporarily Included Files (for merge/rebase):\n"))
353 ui.status(_("Temporarily Included Files (for merge/rebase):\n"))
354 ui.status(("\n".join(temporaryincludes) + "\n"))
354 ui.status(("\n".join(temporaryincludes) + "\n"))
355 else:
355 else:
356 ui.status(_('repo is not sparse\n'))
356 ui.status(_('repo is not sparse\n'))
357 return
357 return
358
358
359 if include or exclude or delete or reset or enableprofile or disableprofile:
359 if include or exclude or delete or reset or enableprofile or disableprofile:
360 _config(ui, repo, pats, opts, include=include, exclude=exclude,
360 _config(ui, repo, pats, opts, include=include, exclude=exclude,
361 reset=reset, delete=delete, enableprofile=enableprofile,
361 reset=reset, delete=delete, enableprofile=enableprofile,
362 disableprofile=disableprofile, force=force)
362 disableprofile=disableprofile, force=force)
363
363
364 if importrules:
364 if importrules:
365 _import(ui, repo, pats, opts, force=force)
365 _import(ui, repo, pats, opts, force=force)
366
366
367 if clearrules:
367 if clearrules:
368 _clear(ui, repo, pats, force=force)
368 sparse.clearrules(repo, force=force)
369
369
370 if refresh:
370 if refresh:
371 try:
371 try:
372 wlock = repo.wlock()
372 wlock = repo.wlock()
373 fcounts = map(
373 fcounts = map(
374 len,
374 len,
375 sparse.refreshwdir(repo, repo.status(), sparse.matcher(repo),
375 sparse.refreshwdir(repo, repo.status(), sparse.matcher(repo),
376 force=force))
376 force=force))
377 _verbose_output(ui, opts, 0, 0, 0, *fcounts)
377 _verbose_output(ui, opts, 0, 0, 0, *fcounts)
378 finally:
378 finally:
379 wlock.release()
379 wlock.release()
380
380
381 def _config(ui, repo, pats, opts, include=False, exclude=False, reset=False,
381 def _config(ui, repo, pats, opts, include=False, exclude=False, reset=False,
382 delete=False, enableprofile=False, disableprofile=False,
382 delete=False, enableprofile=False, disableprofile=False,
383 force=False):
383 force=False):
384 """
384 """
385 Perform a sparse config update. Only one of the kwargs may be specified.
385 Perform a sparse config update. Only one of the kwargs may be specified.
386 """
386 """
387 wlock = repo.wlock()
387 wlock = repo.wlock()
388 try:
388 try:
389 oldsparsematch = sparse.matcher(repo)
389 oldsparsematch = sparse.matcher(repo)
390
390
391 raw = repo.vfs.tryread('sparse')
391 raw = repo.vfs.tryread('sparse')
392 if raw:
392 if raw:
393 oldinclude, oldexclude, oldprofiles = map(
393 oldinclude, oldexclude, oldprofiles = map(
394 set, sparse.parseconfig(ui, raw))
394 set, sparse.parseconfig(ui, raw))
395 else:
395 else:
396 oldinclude = set()
396 oldinclude = set()
397 oldexclude = set()
397 oldexclude = set()
398 oldprofiles = set()
398 oldprofiles = set()
399
399
400 try:
400 try:
401 if reset:
401 if reset:
402 newinclude = set()
402 newinclude = set()
403 newexclude = set()
403 newexclude = set()
404 newprofiles = set()
404 newprofiles = set()
405 else:
405 else:
406 newinclude = set(oldinclude)
406 newinclude = set(oldinclude)
407 newexclude = set(oldexclude)
407 newexclude = set(oldexclude)
408 newprofiles = set(oldprofiles)
408 newprofiles = set(oldprofiles)
409
409
410 oldstatus = repo.status()
410 oldstatus = repo.status()
411
411
412 if any(pat.startswith('/') for pat in pats):
412 if any(pat.startswith('/') for pat in pats):
413 ui.warn(_('warning: paths cannot start with /, ignoring: %s\n')
413 ui.warn(_('warning: paths cannot start with /, ignoring: %s\n')
414 % ([pat for pat in pats if pat.startswith('/')]))
414 % ([pat for pat in pats if pat.startswith('/')]))
415 elif include:
415 elif include:
416 newinclude.update(pats)
416 newinclude.update(pats)
417 elif exclude:
417 elif exclude:
418 newexclude.update(pats)
418 newexclude.update(pats)
419 elif enableprofile:
419 elif enableprofile:
420 newprofiles.update(pats)
420 newprofiles.update(pats)
421 elif disableprofile:
421 elif disableprofile:
422 newprofiles.difference_update(pats)
422 newprofiles.difference_update(pats)
423 elif delete:
423 elif delete:
424 newinclude.difference_update(pats)
424 newinclude.difference_update(pats)
425 newexclude.difference_update(pats)
425 newexclude.difference_update(pats)
426
426
427 sparse.writeconfig(repo, newinclude, newexclude, newprofiles)
427 sparse.writeconfig(repo, newinclude, newexclude, newprofiles)
428
428
429 fcounts = map(
429 fcounts = map(
430 len,
430 len,
431 sparse.refreshwdir(repo, oldstatus, oldsparsematch,
431 sparse.refreshwdir(repo, oldstatus, oldsparsematch,
432 force=force))
432 force=force))
433
433
434 profilecount = (len(newprofiles - oldprofiles) -
434 profilecount = (len(newprofiles - oldprofiles) -
435 len(oldprofiles - newprofiles))
435 len(oldprofiles - newprofiles))
436 includecount = (len(newinclude - oldinclude) -
436 includecount = (len(newinclude - oldinclude) -
437 len(oldinclude - newinclude))
437 len(oldinclude - newinclude))
438 excludecount = (len(newexclude - oldexclude) -
438 excludecount = (len(newexclude - oldexclude) -
439 len(oldexclude - newexclude))
439 len(oldexclude - newexclude))
440 _verbose_output(
440 _verbose_output(
441 ui, opts, profilecount, includecount, excludecount, *fcounts)
441 ui, opts, profilecount, includecount, excludecount, *fcounts)
442 except Exception:
442 except Exception:
443 sparse.writeconfig(repo, oldinclude, oldexclude, oldprofiles)
443 sparse.writeconfig(repo, oldinclude, oldexclude, oldprofiles)
444 raise
444 raise
445 finally:
445 finally:
446 wlock.release()
446 wlock.release()
447
447
448 def _import(ui, repo, files, opts, force=False):
448 def _import(ui, repo, files, opts, force=False):
449 with repo.wlock():
449 with repo.wlock():
450 # load union of current active profile
450 # load union of current active profile
451 revs = [repo.changelog.rev(node) for node in
451 revs = [repo.changelog.rev(node) for node in
452 repo.dirstate.parents() if node != nullid]
452 repo.dirstate.parents() if node != nullid]
453
453
454 # read current configuration
454 # read current configuration
455 raw = repo.vfs.tryread('sparse')
455 raw = repo.vfs.tryread('sparse')
456 oincludes, oexcludes, oprofiles = sparse.parseconfig(ui, raw)
456 oincludes, oexcludes, oprofiles = sparse.parseconfig(ui, raw)
457 includes, excludes, profiles = map(
457 includes, excludes, profiles = map(
458 set, (oincludes, oexcludes, oprofiles))
458 set, (oincludes, oexcludes, oprofiles))
459
459
460 # all active rules
460 # all active rules
461 aincludes, aexcludes, aprofiles = set(), set(), set()
461 aincludes, aexcludes, aprofiles = set(), set(), set()
462 for rev in revs:
462 for rev in revs:
463 rincludes, rexcludes, rprofiles = sparse.patternsforrev(repo, rev)
463 rincludes, rexcludes, rprofiles = sparse.patternsforrev(repo, rev)
464 aincludes.update(rincludes)
464 aincludes.update(rincludes)
465 aexcludes.update(rexcludes)
465 aexcludes.update(rexcludes)
466 aprofiles.update(rprofiles)
466 aprofiles.update(rprofiles)
467
467
468 # import rules on top; only take in rules that are not yet
468 # import rules on top; only take in rules that are not yet
469 # part of the active rules.
469 # part of the active rules.
470 changed = False
470 changed = False
471 for file in files:
471 for file in files:
472 with util.posixfile(util.expandpath(file)) as importfile:
472 with util.posixfile(util.expandpath(file)) as importfile:
473 iincludes, iexcludes, iprofiles = sparse.parseconfig(
473 iincludes, iexcludes, iprofiles = sparse.parseconfig(
474 ui, importfile.read())
474 ui, importfile.read())
475 oldsize = len(includes) + len(excludes) + len(profiles)
475 oldsize = len(includes) + len(excludes) + len(profiles)
476 includes.update(iincludes - aincludes)
476 includes.update(iincludes - aincludes)
477 excludes.update(iexcludes - aexcludes)
477 excludes.update(iexcludes - aexcludes)
478 profiles.update(set(iprofiles) - aprofiles)
478 profiles.update(set(iprofiles) - aprofiles)
479 if len(includes) + len(excludes) + len(profiles) > oldsize:
479 if len(includes) + len(excludes) + len(profiles) > oldsize:
480 changed = True
480 changed = True
481
481
482 profilecount = includecount = excludecount = 0
482 profilecount = includecount = excludecount = 0
483 fcounts = (0, 0, 0)
483 fcounts = (0, 0, 0)
484
484
485 if changed:
485 if changed:
486 profilecount = len(profiles - aprofiles)
486 profilecount = len(profiles - aprofiles)
487 includecount = len(includes - aincludes)
487 includecount = len(includes - aincludes)
488 excludecount = len(excludes - aexcludes)
488 excludecount = len(excludes - aexcludes)
489
489
490 oldstatus = repo.status()
490 oldstatus = repo.status()
491 oldsparsematch = sparse.matcher(repo)
491 oldsparsematch = sparse.matcher(repo)
492 sparse.writeconfig(repo, includes, excludes, profiles)
492 sparse.writeconfig(repo, includes, excludes, profiles)
493
493
494 try:
494 try:
495 fcounts = map(
495 fcounts = map(
496 len,
496 len,
497 sparse.refreshwdir(repo, oldstatus, oldsparsematch,
497 sparse.refreshwdir(repo, oldstatus, oldsparsematch,
498 force=force))
498 force=force))
499 except Exception:
499 except Exception:
500 sparse.writeconfig(repo, oincludes, oexcludes, oprofiles)
500 sparse.writeconfig(repo, oincludes, oexcludes, oprofiles)
501 raise
501 raise
502
502
503 _verbose_output(ui, opts, profilecount, includecount, excludecount,
503 _verbose_output(ui, opts, profilecount, includecount, excludecount,
504 *fcounts)
504 *fcounts)
505
505
506 def _clear(ui, repo, files, force=False):
507 with repo.wlock():
508 raw = repo.vfs.tryread('sparse')
509 includes, excludes, profiles = sparse.parseconfig(ui, raw)
510
511 if includes or excludes:
512 oldstatus = repo.status()
513 oldsparsematch = sparse.matcher(repo)
514 sparse.writeconfig(repo, set(), set(), profiles)
515 sparse.refreshwdir(repo, oldstatus, oldsparsematch, force)
516
517 def _verbose_output(ui, opts, profilecount, includecount, excludecount, added,
506 def _verbose_output(ui, opts, profilecount, includecount, excludecount, added,
518 dropped, lookup):
507 dropped, lookup):
519 """Produce --verbose and templatable output
508 """Produce --verbose and templatable output
520
509
521 This specifically enables -Tjson, providing machine-readable stats on how
510 This specifically enables -Tjson, providing machine-readable stats on how
522 the sparse profile changed.
511 the sparse profile changed.
523
512
524 """
513 """
525 with ui.formatter('sparse', opts) as fm:
514 with ui.formatter('sparse', opts) as fm:
526 fm.startitem()
515 fm.startitem()
527 fm.condwrite(ui.verbose, 'profiles_added', 'Profile # change: %d\n',
516 fm.condwrite(ui.verbose, 'profiles_added', 'Profile # change: %d\n',
528 profilecount)
517 profilecount)
529 fm.condwrite(ui.verbose, 'include_rules_added',
518 fm.condwrite(ui.verbose, 'include_rules_added',
530 'Include rule # change: %d\n', includecount)
519 'Include rule # change: %d\n', includecount)
531 fm.condwrite(ui.verbose, 'exclude_rules_added',
520 fm.condwrite(ui.verbose, 'exclude_rules_added',
532 'Exclude rule # change: %d\n', excludecount)
521 'Exclude rule # change: %d\n', excludecount)
533 # In 'plain' verbose mode, mergemod.applyupdates already outputs what
522 # In 'plain' verbose mode, mergemod.applyupdates already outputs what
534 # files are added or removed outside of the templating formatter
523 # files are added or removed outside of the templating formatter
535 # framework. No point in repeating ourselves in that case.
524 # framework. No point in repeating ourselves in that case.
536 if not fm.isplain():
525 if not fm.isplain():
537 fm.condwrite(ui.verbose, 'files_added', 'Files added: %d\n',
526 fm.condwrite(ui.verbose, 'files_added', 'Files added: %d\n',
538 added)
527 added)
539 fm.condwrite(ui.verbose, 'files_dropped', 'Files dropped: %d\n',
528 fm.condwrite(ui.verbose, 'files_dropped', 'Files dropped: %d\n',
540 dropped)
529 dropped)
541 fm.condwrite(ui.verbose, 'files_conflicting',
530 fm.condwrite(ui.verbose, 'files_conflicting',
542 'Files conflicting: %d\n', lookup)
531 'Files conflicting: %d\n', lookup)
@@ -1,496 +1,514 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 )
21 )
22
22
23 # Whether sparse features are enabled. This variable is intended to be
23 # Whether sparse features are enabled. This variable is intended to be
24 # temporary to facilitate porting sparse to core. It should eventually be
24 # temporary to facilitate porting sparse to core. It should eventually be
25 # a per-repo option, possibly a repo requirement.
25 # a per-repo option, possibly a repo requirement.
26 enabled = False
26 enabled = False
27
27
28 def parseconfig(ui, raw):
28 def parseconfig(ui, raw):
29 """Parse sparse config file content.
29 """Parse sparse config file content.
30
30
31 Returns a tuple of includes, excludes, and profiles.
31 Returns a tuple of includes, excludes, and profiles.
32 """
32 """
33 includes = set()
33 includes = set()
34 excludes = set()
34 excludes = set()
35 current = includes
35 current = includes
36 profiles = []
36 profiles = []
37 for line in raw.split('\n'):
37 for line in raw.split('\n'):
38 line = line.strip()
38 line = line.strip()
39 if not line or line.startswith('#'):
39 if not line or line.startswith('#'):
40 # empty or comment line, skip
40 # empty or comment line, skip
41 continue
41 continue
42 elif line.startswith('%include '):
42 elif line.startswith('%include '):
43 line = line[9:].strip()
43 line = line[9:].strip()
44 if line:
44 if line:
45 profiles.append(line)
45 profiles.append(line)
46 elif line == '[include]':
46 elif line == '[include]':
47 if current != includes:
47 if current != includes:
48 # TODO pass filename into this API so we can report it.
48 # TODO pass filename into this API so we can report it.
49 raise error.Abort(_('sparse config cannot have includes ' +
49 raise error.Abort(_('sparse config cannot have includes ' +
50 'after excludes'))
50 'after excludes'))
51 continue
51 continue
52 elif line == '[exclude]':
52 elif line == '[exclude]':
53 current = excludes
53 current = excludes
54 elif line:
54 elif line:
55 if line.strip().startswith('/'):
55 if line.strip().startswith('/'):
56 ui.warn(_('warning: sparse profile cannot use' +
56 ui.warn(_('warning: sparse profile cannot use' +
57 ' paths starting with /, ignoring %s\n') % line)
57 ' paths starting with /, ignoring %s\n') % line)
58 continue
58 continue
59 current.add(line)
59 current.add(line)
60
60
61 return includes, excludes, profiles
61 return includes, excludes, profiles
62
62
63 # Exists as separate function to facilitate monkeypatching.
63 # Exists as separate function to facilitate monkeypatching.
64 def readprofile(repo, profile, changeid):
64 def readprofile(repo, profile, changeid):
65 """Resolve the raw content of a sparse profile file."""
65 """Resolve the raw content of a sparse profile file."""
66 # TODO add some kind of cache here because this incurs a manifest
66 # TODO add some kind of cache here because this incurs a manifest
67 # resolve and can be slow.
67 # resolve and can be slow.
68 return repo.filectx(profile, changeid=changeid).data()
68 return repo.filectx(profile, changeid=changeid).data()
69
69
70 def patternsforrev(repo, rev):
70 def patternsforrev(repo, rev):
71 """Obtain sparse checkout patterns for the given rev.
71 """Obtain sparse checkout patterns for the given rev.
72
72
73 Returns a tuple of iterables representing includes, excludes, and
73 Returns a tuple of iterables representing includes, excludes, and
74 patterns.
74 patterns.
75 """
75 """
76 # Feature isn't enabled. No-op.
76 # Feature isn't enabled. No-op.
77 if not enabled:
77 if not enabled:
78 return set(), set(), []
78 return set(), set(), []
79
79
80 raw = repo.vfs.tryread('sparse')
80 raw = repo.vfs.tryread('sparse')
81 if not raw:
81 if not raw:
82 return set(), set(), []
82 return set(), set(), []
83
83
84 if rev is None:
84 if rev is None:
85 raise error.Abort(_('cannot parse sparse patterns from working '
85 raise error.Abort(_('cannot parse sparse patterns from working '
86 'directory'))
86 'directory'))
87
87
88 includes, excludes, profiles = parseconfig(repo.ui, raw)
88 includes, excludes, profiles = parseconfig(repo.ui, raw)
89 ctx = repo[rev]
89 ctx = repo[rev]
90
90
91 if profiles:
91 if profiles:
92 visited = set()
92 visited = set()
93 while profiles:
93 while profiles:
94 profile = profiles.pop()
94 profile = profiles.pop()
95 if profile in visited:
95 if profile in visited:
96 continue
96 continue
97
97
98 visited.add(profile)
98 visited.add(profile)
99
99
100 try:
100 try:
101 raw = readprofile(repo, profile, rev)
101 raw = readprofile(repo, profile, rev)
102 except error.ManifestLookupError:
102 except error.ManifestLookupError:
103 msg = (
103 msg = (
104 "warning: sparse profile '%s' not found "
104 "warning: sparse profile '%s' not found "
105 "in rev %s - ignoring it\n" % (profile, ctx))
105 "in rev %s - ignoring it\n" % (profile, ctx))
106 # experimental config: sparse.missingwarning
106 # experimental config: sparse.missingwarning
107 if repo.ui.configbool(
107 if repo.ui.configbool(
108 'sparse', 'missingwarning', True):
108 'sparse', 'missingwarning', True):
109 repo.ui.warn(msg)
109 repo.ui.warn(msg)
110 else:
110 else:
111 repo.ui.debug(msg)
111 repo.ui.debug(msg)
112 continue
112 continue
113
113
114 pincludes, pexcludes, subprofs = parseconfig(repo.ui, raw)
114 pincludes, pexcludes, subprofs = parseconfig(repo.ui, raw)
115 includes.update(pincludes)
115 includes.update(pincludes)
116 excludes.update(pexcludes)
116 excludes.update(pexcludes)
117 for subprofile in subprofs:
117 for subprofile in subprofs:
118 profiles.append(subprofile)
118 profiles.append(subprofile)
119
119
120 profiles = visited
120 profiles = visited
121
121
122 if includes:
122 if includes:
123 includes.add('.hg*')
123 includes.add('.hg*')
124
124
125 return includes, excludes, profiles
125 return includes, excludes, profiles
126
126
127 def activeprofiles(repo):
127 def activeprofiles(repo):
128 revs = [repo.changelog.rev(node) for node in
128 revs = [repo.changelog.rev(node) for node in
129 repo.dirstate.parents() if node != nullid]
129 repo.dirstate.parents() if node != nullid]
130
130
131 profiles = set()
131 profiles = set()
132 for rev in revs:
132 for rev in revs:
133 profiles.update(patternsforrev(repo, rev)[2])
133 profiles.update(patternsforrev(repo, rev)[2])
134
134
135 return profiles
135 return profiles
136
136
137 def configsignature(repo, includetemp=True):
137 def configsignature(repo, includetemp=True):
138 """Obtain the signature string for the current sparse configuration.
138 """Obtain the signature string for the current sparse configuration.
139
139
140 This is used to construct a cache key for matchers.
140 This is used to construct a cache key for matchers.
141 """
141 """
142 cache = repo._sparsesignaturecache
142 cache = repo._sparsesignaturecache
143
143
144 signature = cache.get('signature')
144 signature = cache.get('signature')
145
145
146 if includetemp:
146 if includetemp:
147 tempsignature = cache.get('tempsignature')
147 tempsignature = cache.get('tempsignature')
148 else:
148 else:
149 tempsignature = '0'
149 tempsignature = '0'
150
150
151 if signature is None or (includetemp and tempsignature is None):
151 if signature is None or (includetemp and tempsignature is None):
152 signature = hashlib.sha1(repo.vfs.tryread('sparse')).hexdigest()
152 signature = hashlib.sha1(repo.vfs.tryread('sparse')).hexdigest()
153 cache['signature'] = signature
153 cache['signature'] = signature
154
154
155 if includetemp:
155 if includetemp:
156 raw = repo.vfs.tryread('tempsparse')
156 raw = repo.vfs.tryread('tempsparse')
157 tempsignature = hashlib.sha1(raw).hexdigest()
157 tempsignature = hashlib.sha1(raw).hexdigest()
158 cache['tempsignature'] = tempsignature
158 cache['tempsignature'] = tempsignature
159
159
160 return '%s %s' % (signature, tempsignature)
160 return '%s %s' % (signature, tempsignature)
161
161
162 def writeconfig(repo, includes, excludes, profiles):
162 def writeconfig(repo, includes, excludes, profiles):
163 """Write the sparse config file given a sparse configuration."""
163 """Write the sparse config file given a sparse configuration."""
164 with repo.vfs('sparse', 'wb') as fh:
164 with repo.vfs('sparse', 'wb') as fh:
165 for p in sorted(profiles):
165 for p in sorted(profiles):
166 fh.write('%%include %s\n' % p)
166 fh.write('%%include %s\n' % p)
167
167
168 if includes:
168 if includes:
169 fh.write('[include]\n')
169 fh.write('[include]\n')
170 for i in sorted(includes):
170 for i in sorted(includes):
171 fh.write(i)
171 fh.write(i)
172 fh.write('\n')
172 fh.write('\n')
173
173
174 if excludes:
174 if excludes:
175 fh.write('[exclude]\n')
175 fh.write('[exclude]\n')
176 for e in sorted(excludes):
176 for e in sorted(excludes):
177 fh.write(e)
177 fh.write(e)
178 fh.write('\n')
178 fh.write('\n')
179
179
180 repo._sparsesignaturecache.clear()
180 repo._sparsesignaturecache.clear()
181
181
182 def readtemporaryincludes(repo):
182 def readtemporaryincludes(repo):
183 raw = repo.vfs.tryread('tempsparse')
183 raw = repo.vfs.tryread('tempsparse')
184 if not raw:
184 if not raw:
185 return set()
185 return set()
186
186
187 return set(raw.split('\n'))
187 return set(raw.split('\n'))
188
188
189 def writetemporaryincludes(repo, includes):
189 def writetemporaryincludes(repo, includes):
190 repo.vfs.write('tempsparse', '\n'.join(sorted(includes)))
190 repo.vfs.write('tempsparse', '\n'.join(sorted(includes)))
191 repo._sparsesignaturecache.clear()
191 repo._sparsesignaturecache.clear()
192
192
193 def addtemporaryincludes(repo, additional):
193 def addtemporaryincludes(repo, additional):
194 includes = readtemporaryincludes(repo)
194 includes = readtemporaryincludes(repo)
195 for i in additional:
195 for i in additional:
196 includes.add(i)
196 includes.add(i)
197 writetemporaryincludes(repo, includes)
197 writetemporaryincludes(repo, includes)
198
198
199 def prunetemporaryincludes(repo):
199 def prunetemporaryincludes(repo):
200 if not enabled or not repo.vfs.exists('tempsparse'):
200 if not enabled or not repo.vfs.exists('tempsparse'):
201 return
201 return
202
202
203 origstatus = repo.status()
203 origstatus = repo.status()
204 modified, added, removed, deleted, a, b, c = origstatus
204 modified, added, removed, deleted, a, b, c = origstatus
205 if modified or added or removed or deleted:
205 if modified or added or removed or deleted:
206 # Still have pending changes. Don't bother trying to prune.
206 # Still have pending changes. Don't bother trying to prune.
207 return
207 return
208
208
209 sparsematch = matcher(repo, includetemp=False)
209 sparsematch = matcher(repo, includetemp=False)
210 dirstate = repo.dirstate
210 dirstate = repo.dirstate
211 actions = []
211 actions = []
212 dropped = []
212 dropped = []
213 tempincludes = readtemporaryincludes(repo)
213 tempincludes = readtemporaryincludes(repo)
214 for file in tempincludes:
214 for file in tempincludes:
215 if file in dirstate and not sparsematch(file):
215 if file in dirstate and not sparsematch(file):
216 message = _('dropping temporarily included sparse files')
216 message = _('dropping temporarily included sparse files')
217 actions.append((file, None, message))
217 actions.append((file, None, message))
218 dropped.append(file)
218 dropped.append(file)
219
219
220 typeactions = collections.defaultdict(list)
220 typeactions = collections.defaultdict(list)
221 typeactions['r'] = actions
221 typeactions['r'] = actions
222 mergemod.applyupdates(repo, typeactions, repo[None], repo['.'], False)
222 mergemod.applyupdates(repo, typeactions, repo[None], repo['.'], False)
223
223
224 # Fix dirstate
224 # Fix dirstate
225 for file in dropped:
225 for file in dropped:
226 dirstate.drop(file)
226 dirstate.drop(file)
227
227
228 repo.vfs.unlink('tempsparse')
228 repo.vfs.unlink('tempsparse')
229 repo._sparsesignaturecache.clear()
229 repo._sparsesignaturecache.clear()
230 msg = _('cleaned up %d temporarily added file(s) from the '
230 msg = _('cleaned up %d temporarily added file(s) from the '
231 'sparse checkout\n')
231 'sparse checkout\n')
232 repo.ui.status(msg % len(tempincludes))
232 repo.ui.status(msg % len(tempincludes))
233
233
234 def matcher(repo, revs=None, includetemp=True):
234 def matcher(repo, revs=None, includetemp=True):
235 """Obtain a matcher for sparse working directories for the given revs.
235 """Obtain a matcher for sparse working directories for the given revs.
236
236
237 If multiple revisions are specified, the matcher is the union of all
237 If multiple revisions are specified, the matcher is the union of all
238 revs.
238 revs.
239
239
240 ``includetemp`` indicates whether to use the temporary sparse profile.
240 ``includetemp`` indicates whether to use the temporary sparse profile.
241 """
241 """
242 # If sparse isn't enabled, sparse matcher matches everything.
242 # If sparse isn't enabled, sparse matcher matches everything.
243 if not enabled:
243 if not enabled:
244 return matchmod.always(repo.root, '')
244 return matchmod.always(repo.root, '')
245
245
246 if not revs or revs == [None]:
246 if not revs or revs == [None]:
247 revs = [repo.changelog.rev(node)
247 revs = [repo.changelog.rev(node)
248 for node in repo.dirstate.parents() if node != nullid]
248 for node in repo.dirstate.parents() if node != nullid]
249
249
250 signature = configsignature(repo, includetemp=includetemp)
250 signature = configsignature(repo, includetemp=includetemp)
251
251
252 key = '%s %s' % (signature, ' '.join(map(pycompat.bytestr, revs)))
252 key = '%s %s' % (signature, ' '.join(map(pycompat.bytestr, revs)))
253
253
254 result = repo._sparsematchercache.get(key)
254 result = repo._sparsematchercache.get(key)
255 if result:
255 if result:
256 return result
256 return result
257
257
258 matchers = []
258 matchers = []
259 for rev in revs:
259 for rev in revs:
260 try:
260 try:
261 includes, excludes, profiles = patternsforrev(repo, rev)
261 includes, excludes, profiles = patternsforrev(repo, rev)
262
262
263 if includes or excludes:
263 if includes or excludes:
264 # Explicitly include subdirectories of includes so
264 # Explicitly include subdirectories of includes so
265 # status will walk them down to the actual include.
265 # status will walk them down to the actual include.
266 subdirs = set()
266 subdirs = set()
267 for include in includes:
267 for include in includes:
268 # TODO consider using posix path functions here so Windows
268 # TODO consider using posix path functions here so Windows
269 # \ directory separators don't come into play.
269 # \ directory separators don't come into play.
270 dirname = os.path.dirname(include)
270 dirname = os.path.dirname(include)
271 # basename is used to avoid issues with absolute
271 # basename is used to avoid issues with absolute
272 # paths (which on Windows can include the drive).
272 # paths (which on Windows can include the drive).
273 while os.path.basename(dirname):
273 while os.path.basename(dirname):
274 subdirs.add(dirname)
274 subdirs.add(dirname)
275 dirname = os.path.dirname(dirname)
275 dirname = os.path.dirname(dirname)
276
276
277 matcher = matchmod.match(repo.root, '', [],
277 matcher = matchmod.match(repo.root, '', [],
278 include=includes, exclude=excludes,
278 include=includes, exclude=excludes,
279 default='relpath')
279 default='relpath')
280 if subdirs:
280 if subdirs:
281 matcher = matchmod.forceincludematcher(matcher, subdirs)
281 matcher = matchmod.forceincludematcher(matcher, subdirs)
282 matchers.append(matcher)
282 matchers.append(matcher)
283 except IOError:
283 except IOError:
284 pass
284 pass
285
285
286 if not matchers:
286 if not matchers:
287 result = matchmod.always(repo.root, '')
287 result = matchmod.always(repo.root, '')
288 elif len(matchers) == 1:
288 elif len(matchers) == 1:
289 result = matchers[0]
289 result = matchers[0]
290 else:
290 else:
291 result = matchmod.unionmatcher(matchers)
291 result = matchmod.unionmatcher(matchers)
292
292
293 if includetemp:
293 if includetemp:
294 tempincludes = readtemporaryincludes(repo)
294 tempincludes = readtemporaryincludes(repo)
295 result = matchmod.forceincludematcher(result, tempincludes)
295 result = matchmod.forceincludematcher(result, tempincludes)
296
296
297 repo._sparsematchercache[key] = result
297 repo._sparsematchercache[key] = result
298
298
299 return result
299 return result
300
300
301 def filterupdatesactions(repo, wctx, mctx, branchmerge, actions):
301 def filterupdatesactions(repo, wctx, mctx, branchmerge, actions):
302 """Filter updates to only lay out files that match the sparse rules."""
302 """Filter updates to only lay out files that match the sparse rules."""
303 if not enabled:
303 if not enabled:
304 return actions
304 return actions
305
305
306 oldrevs = [pctx.rev() for pctx in wctx.parents()]
306 oldrevs = [pctx.rev() for pctx in wctx.parents()]
307 oldsparsematch = matcher(repo, oldrevs)
307 oldsparsematch = matcher(repo, oldrevs)
308
308
309 if oldsparsematch.always():
309 if oldsparsematch.always():
310 return actions
310 return actions
311
311
312 files = set()
312 files = set()
313 prunedactions = {}
313 prunedactions = {}
314
314
315 if branchmerge:
315 if branchmerge:
316 # If we're merging, use the wctx filter, since we're merging into
316 # If we're merging, use the wctx filter, since we're merging into
317 # the wctx.
317 # the wctx.
318 sparsematch = matcher(repo, [wctx.parents()[0].rev()])
318 sparsematch = matcher(repo, [wctx.parents()[0].rev()])
319 else:
319 else:
320 # If we're updating, use the target context's filter, since we're
320 # If we're updating, use the target context's filter, since we're
321 # moving to the target context.
321 # moving to the target context.
322 sparsematch = matcher(repo, [mctx.rev()])
322 sparsematch = matcher(repo, [mctx.rev()])
323
323
324 temporaryfiles = []
324 temporaryfiles = []
325 for file, action in actions.iteritems():
325 for file, action in actions.iteritems():
326 type, args, msg = action
326 type, args, msg = action
327 files.add(file)
327 files.add(file)
328 if sparsematch(file):
328 if sparsematch(file):
329 prunedactions[file] = action
329 prunedactions[file] = action
330 elif type == 'm':
330 elif type == 'm':
331 temporaryfiles.append(file)
331 temporaryfiles.append(file)
332 prunedactions[file] = action
332 prunedactions[file] = action
333 elif branchmerge:
333 elif branchmerge:
334 if type != 'k':
334 if type != 'k':
335 temporaryfiles.append(file)
335 temporaryfiles.append(file)
336 prunedactions[file] = action
336 prunedactions[file] = action
337 elif type == 'f':
337 elif type == 'f':
338 prunedactions[file] = action
338 prunedactions[file] = action
339 elif file in wctx:
339 elif file in wctx:
340 prunedactions[file] = ('r', args, msg)
340 prunedactions[file] = ('r', args, msg)
341
341
342 if len(temporaryfiles) > 0:
342 if len(temporaryfiles) > 0:
343 repo.ui.status(_('temporarily included %d file(s) in the sparse '
343 repo.ui.status(_('temporarily included %d file(s) in the sparse '
344 'checkout for merging\n') % len(temporaryfiles))
344 'checkout for merging\n') % len(temporaryfiles))
345 addtemporaryincludes(repo, temporaryfiles)
345 addtemporaryincludes(repo, temporaryfiles)
346
346
347 # Add the new files to the working copy so they can be merged, etc
347 # Add the new files to the working copy so they can be merged, etc
348 actions = []
348 actions = []
349 message = 'temporarily adding to sparse checkout'
349 message = 'temporarily adding to sparse checkout'
350 wctxmanifest = repo[None].manifest()
350 wctxmanifest = repo[None].manifest()
351 for file in temporaryfiles:
351 for file in temporaryfiles:
352 if file in wctxmanifest:
352 if file in wctxmanifest:
353 fctx = repo[None][file]
353 fctx = repo[None][file]
354 actions.append((file, (fctx.flags(), False), message))
354 actions.append((file, (fctx.flags(), False), message))
355
355
356 typeactions = collections.defaultdict(list)
356 typeactions = collections.defaultdict(list)
357 typeactions['g'] = actions
357 typeactions['g'] = actions
358 mergemod.applyupdates(repo, typeactions, repo[None], repo['.'],
358 mergemod.applyupdates(repo, typeactions, repo[None], repo['.'],
359 False)
359 False)
360
360
361 dirstate = repo.dirstate
361 dirstate = repo.dirstate
362 for file, flags, msg in actions:
362 for file, flags, msg in actions:
363 dirstate.normal(file)
363 dirstate.normal(file)
364
364
365 profiles = activeprofiles(repo)
365 profiles = activeprofiles(repo)
366 changedprofiles = profiles & files
366 changedprofiles = profiles & files
367 # If an active profile changed during the update, refresh the checkout.
367 # If an active profile changed during the update, refresh the checkout.
368 # Don't do this during a branch merge, since all incoming changes should
368 # Don't do this during a branch merge, since all incoming changes should
369 # have been handled by the temporary includes above.
369 # have been handled by the temporary includes above.
370 if changedprofiles and not branchmerge:
370 if changedprofiles and not branchmerge:
371 mf = mctx.manifest()
371 mf = mctx.manifest()
372 for file in mf:
372 for file in mf:
373 old = oldsparsematch(file)
373 old = oldsparsematch(file)
374 new = sparsematch(file)
374 new = sparsematch(file)
375 if not old and new:
375 if not old and new:
376 flags = mf.flags(file)
376 flags = mf.flags(file)
377 prunedactions[file] = ('g', (flags, False), '')
377 prunedactions[file] = ('g', (flags, False), '')
378 elif old and not new:
378 elif old and not new:
379 prunedactions[file] = ('r', [], '')
379 prunedactions[file] = ('r', [], '')
380
380
381 return prunedactions
381 return prunedactions
382
382
383 def refreshwdir(repo, origstatus, origsparsematch, force=False):
383 def refreshwdir(repo, origstatus, origsparsematch, force=False):
384 """Refreshes working directory by taking sparse config into account.
384 """Refreshes working directory by taking sparse config into account.
385
385
386 The old status and sparse matcher is compared against the current sparse
386 The old status and sparse matcher is compared against the current sparse
387 matcher.
387 matcher.
388
388
389 Will abort if a file with pending changes is being excluded or included
389 Will abort if a file with pending changes is being excluded or included
390 unless ``force`` is True.
390 unless ``force`` is True.
391 """
391 """
392 modified, added, removed, deleted, unknown, ignored, clean = origstatus
392 modified, added, removed, deleted, unknown, ignored, clean = origstatus
393
393
394 # Verify there are no pending changes
394 # Verify there are no pending changes
395 pending = set()
395 pending = set()
396 pending.update(modified)
396 pending.update(modified)
397 pending.update(added)
397 pending.update(added)
398 pending.update(removed)
398 pending.update(removed)
399 sparsematch = matcher(repo)
399 sparsematch = matcher(repo)
400 abort = False
400 abort = False
401
401
402 for f in pending:
402 for f in pending:
403 if not sparsematch(f):
403 if not sparsematch(f):
404 repo.ui.warn(_("pending changes to '%s'\n") % f)
404 repo.ui.warn(_("pending changes to '%s'\n") % f)
405 abort = not force
405 abort = not force
406
406
407 if abort:
407 if abort:
408 raise error.Abort(_('could not update sparseness due to pending '
408 raise error.Abort(_('could not update sparseness due to pending '
409 'changes'))
409 'changes'))
410
410
411 # Calculate actions
411 # Calculate actions
412 dirstate = repo.dirstate
412 dirstate = repo.dirstate
413 ctx = repo['.']
413 ctx = repo['.']
414 added = []
414 added = []
415 lookup = []
415 lookup = []
416 dropped = []
416 dropped = []
417 mf = ctx.manifest()
417 mf = ctx.manifest()
418 files = set(mf)
418 files = set(mf)
419
419
420 actions = {}
420 actions = {}
421
421
422 for file in files:
422 for file in files:
423 old = origsparsematch(file)
423 old = origsparsematch(file)
424 new = sparsematch(file)
424 new = sparsematch(file)
425 # Add files that are newly included, or that don't exist in
425 # Add files that are newly included, or that don't exist in
426 # the dirstate yet.
426 # the dirstate yet.
427 if (new and not old) or (old and new and not file in dirstate):
427 if (new and not old) or (old and new and not file in dirstate):
428 fl = mf.flags(file)
428 fl = mf.flags(file)
429 if repo.wvfs.exists(file):
429 if repo.wvfs.exists(file):
430 actions[file] = ('e', (fl,), '')
430 actions[file] = ('e', (fl,), '')
431 lookup.append(file)
431 lookup.append(file)
432 else:
432 else:
433 actions[file] = ('g', (fl, False), '')
433 actions[file] = ('g', (fl, False), '')
434 added.append(file)
434 added.append(file)
435 # Drop files that are newly excluded, or that still exist in
435 # Drop files that are newly excluded, or that still exist in
436 # the dirstate.
436 # the dirstate.
437 elif (old and not new) or (not old and not new and file in dirstate):
437 elif (old and not new) or (not old and not new and file in dirstate):
438 dropped.append(file)
438 dropped.append(file)
439 if file not in pending:
439 if file not in pending:
440 actions[file] = ('r', [], '')
440 actions[file] = ('r', [], '')
441
441
442 # Verify there are no pending changes in newly included files
442 # Verify there are no pending changes in newly included files
443 abort = False
443 abort = False
444 for file in lookup:
444 for file in lookup:
445 repo.ui.warn(_("pending changes to '%s'\n") % file)
445 repo.ui.warn(_("pending changes to '%s'\n") % file)
446 abort = not force
446 abort = not force
447 if abort:
447 if abort:
448 raise error.Abort(_('cannot change sparseness due to pending '
448 raise error.Abort(_('cannot change sparseness due to pending '
449 'changes (delete the files or use '
449 'changes (delete the files or use '
450 '--force to bring them back dirty)'))
450 '--force to bring them back dirty)'))
451
451
452 # Check for files that were only in the dirstate.
452 # Check for files that were only in the dirstate.
453 for file, state in dirstate.iteritems():
453 for file, state in dirstate.iteritems():
454 if not file in files:
454 if not file in files:
455 old = origsparsematch(file)
455 old = origsparsematch(file)
456 new = sparsematch(file)
456 new = sparsematch(file)
457 if old and not new:
457 if old and not new:
458 dropped.append(file)
458 dropped.append(file)
459
459
460 # Apply changes to disk
460 # Apply changes to disk
461 typeactions = dict((m, []) for m in 'a f g am cd dc r dm dg m e k'.split())
461 typeactions = dict((m, []) for m in 'a f g am cd dc r dm dg m e k'.split())
462 for f, (m, args, msg) in actions.iteritems():
462 for f, (m, args, msg) in actions.iteritems():
463 if m not in typeactions:
463 if m not in typeactions:
464 typeactions[m] = []
464 typeactions[m] = []
465 typeactions[m].append((f, args, msg))
465 typeactions[m].append((f, args, msg))
466
466
467 mergemod.applyupdates(repo, typeactions, repo[None], repo['.'], False)
467 mergemod.applyupdates(repo, typeactions, repo[None], repo['.'], False)
468
468
469 # Fix dirstate
469 # Fix dirstate
470 for file in added:
470 for file in added:
471 dirstate.normal(file)
471 dirstate.normal(file)
472
472
473 for file in dropped:
473 for file in dropped:
474 dirstate.drop(file)
474 dirstate.drop(file)
475
475
476 for file in lookup:
476 for file in lookup:
477 # File exists on disk, and we're bringing it back in an unknown state.
477 # File exists on disk, and we're bringing it back in an unknown state.
478 dirstate.normallookup(file)
478 dirstate.normallookup(file)
479
479
480 return added, dropped, lookup
480 return added, dropped, lookup
481
481
482 def aftercommit(repo, node):
482 def aftercommit(repo, node):
483 """Perform actions after a working directory commit."""
483 """Perform actions after a working directory commit."""
484 # This function is called unconditionally, even if sparse isn't
484 # This function is called unconditionally, even if sparse isn't
485 # enabled.
485 # enabled.
486 ctx = repo[node]
486 ctx = repo[node]
487
487
488 profiles = patternsforrev(repo, ctx.rev())[2]
488 profiles = patternsforrev(repo, ctx.rev())[2]
489
489
490 # profiles will only have data if sparse is enabled.
490 # profiles will only have data if sparse is enabled.
491 if set(profiles) & set(ctx.files()):
491 if set(profiles) & set(ctx.files()):
492 origstatus = repo.status()
492 origstatus = repo.status()
493 origsparsematch = matcher(repo)
493 origsparsematch = matcher(repo)
494 refreshwdir(repo, origstatus, origsparsematch, force=True)
494 refreshwdir(repo, origstatus, origsparsematch, force=True)
495
495
496 prunetemporaryincludes(repo)
496 prunetemporaryincludes(repo)
497
498 def clearrules(repo, force=False):
499 """Clears include/exclude rules from the sparse config.
500
501 The remaining sparse config only has profiles, if defined. The working
502 directory is refreshed, as needed.
503 """
504 with repo.wlock():
505 raw = repo.vfs.tryread('sparse')
506 includes, excludes, profiles = parseconfig(repo.ui, raw)
507
508 if not includes and not excludes:
509 return
510
511 oldstatus = repo.status()
512 oldmatch = matcher(repo)
513 writeconfig(repo, set(), set(), profiles)
514 refreshwdir(repo, oldstatus, oldmatch, force=force)
General Comments 0
You need to be logged in to leave comments. Login now