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