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