##// END OF EJS Templates
sparse: remove custom hash matcher...
Gregory Szorc -
r33316:310f7bca default
parent child Browse files
Show More
@@ -1,989 +1,976
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 import collections
77 import collections
78 import hashlib
78 import hashlib
79 import os
79 import os
80
80
81 from mercurial.i18n import _
81 from mercurial.i18n import _
82 from mercurial.node import nullid
82 from mercurial.node import nullid
83 from mercurial import (
83 from mercurial import (
84 cmdutil,
84 cmdutil,
85 commands,
85 commands,
86 context,
86 context,
87 dirstate,
87 dirstate,
88 error,
88 error,
89 extensions,
89 extensions,
90 hg,
90 hg,
91 localrepo,
91 localrepo,
92 match as matchmod,
92 match as matchmod,
93 merge as mergemod,
93 merge as mergemod,
94 registrar,
94 registrar,
95 sparse,
95 sparse,
96 util,
96 util,
97 )
97 )
98
98
99 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
99 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
100 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
100 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
101 # be specifying the version(s) of Mercurial they are tested with, or
101 # be specifying the version(s) of Mercurial they are tested with, or
102 # leave the attribute unspecified.
102 # leave the attribute unspecified.
103 testedwith = 'ships-with-hg-core'
103 testedwith = 'ships-with-hg-core'
104
104
105 cmdtable = {}
105 cmdtable = {}
106 command = registrar.command(cmdtable)
106 command = registrar.command(cmdtable)
107
107
108 def uisetup(ui):
108 def uisetup(ui):
109 _setupupdates(ui)
109 _setupupdates(ui)
110 _setupcommit(ui)
110 _setupcommit(ui)
111
111
112 def extsetup(ui):
112 def extsetup(ui):
113 sparse.enabled = True
113 sparse.enabled = True
114
114
115 _setupclone(ui)
115 _setupclone(ui)
116 _setuplog(ui)
116 _setuplog(ui)
117 _setupadd(ui)
117 _setupadd(ui)
118 _setupdirstate(ui)
118 _setupdirstate(ui)
119 # if fsmonitor is enabled, tell it to use our hash function
120 try:
121 fsmonitor = extensions.find('fsmonitor')
122 def _hashignore(orig, ignore):
123 return _hashmatcher(ignore)
124 extensions.wrapfunction(fsmonitor, '_hashignore', _hashignore)
125 except KeyError:
126 pass
127
119
128 def reposetup(ui, repo):
120 def reposetup(ui, repo):
129 if not util.safehasattr(repo, 'dirstate'):
121 if not util.safehasattr(repo, 'dirstate'):
130 return
122 return
131
123
132 _wraprepo(ui, repo)
124 _wraprepo(ui, repo)
133
125
134 def replacefilecache(cls, propname, replacement):
126 def replacefilecache(cls, propname, replacement):
135 """Replace a filecache property with a new class. This allows changing the
127 """Replace a filecache property with a new class. This allows changing the
136 cache invalidation condition."""
128 cache invalidation condition."""
137 origcls = cls
129 origcls = cls
138 assert callable(replacement)
130 assert callable(replacement)
139 while cls is not object:
131 while cls is not object:
140 if propname in cls.__dict__:
132 if propname in cls.__dict__:
141 orig = cls.__dict__[propname]
133 orig = cls.__dict__[propname]
142 setattr(cls, propname, replacement(orig))
134 setattr(cls, propname, replacement(orig))
143 break
135 break
144 cls = cls.__bases__[0]
136 cls = cls.__bases__[0]
145
137
146 if cls is object:
138 if cls is object:
147 raise AttributeError(_("type '%s' has no property '%s'") % (origcls,
139 raise AttributeError(_("type '%s' has no property '%s'") % (origcls,
148 propname))
140 propname))
149
141
150 def _setupupdates(ui):
142 def _setupupdates(ui):
151 def _calculateupdates(orig, repo, wctx, mctx, ancestors, branchmerge, *arg,
143 def _calculateupdates(orig, repo, wctx, mctx, ancestors, branchmerge, *arg,
152 **kwargs):
144 **kwargs):
153 """Filter updates to only lay out files that match the sparse rules.
145 """Filter updates to only lay out files that match the sparse rules.
154 """
146 """
155 actions, diverge, renamedelete = orig(repo, wctx, mctx, ancestors,
147 actions, diverge, renamedelete = orig(repo, wctx, mctx, ancestors,
156 branchmerge, *arg, **kwargs)
148 branchmerge, *arg, **kwargs)
157
149
158 if not util.safehasattr(repo, 'sparsematch'):
150 if not util.safehasattr(repo, 'sparsematch'):
159 return actions, diverge, renamedelete
151 return actions, diverge, renamedelete
160
152
161 files = set()
153 files = set()
162 prunedactions = {}
154 prunedactions = {}
163 oldrevs = [pctx.rev() for pctx in wctx.parents()]
155 oldrevs = [pctx.rev() for pctx in wctx.parents()]
164 oldsparsematch = repo.sparsematch(*oldrevs)
156 oldsparsematch = repo.sparsematch(*oldrevs)
165
157
166 if branchmerge:
158 if branchmerge:
167 # If we're merging, use the wctx filter, since we're merging into
159 # If we're merging, use the wctx filter, since we're merging into
168 # the wctx.
160 # the wctx.
169 sparsematch = repo.sparsematch(wctx.parents()[0].rev())
161 sparsematch = repo.sparsematch(wctx.parents()[0].rev())
170 else:
162 else:
171 # If we're updating, use the target context's filter, since we're
163 # If we're updating, use the target context's filter, since we're
172 # moving to the target context.
164 # moving to the target context.
173 sparsematch = repo.sparsematch(mctx.rev())
165 sparsematch = repo.sparsematch(mctx.rev())
174
166
175 temporaryfiles = []
167 temporaryfiles = []
176 for file, action in actions.iteritems():
168 for file, action in actions.iteritems():
177 type, args, msg = action
169 type, args, msg = action
178 files.add(file)
170 files.add(file)
179 if sparsematch(file):
171 if sparsematch(file):
180 prunedactions[file] = action
172 prunedactions[file] = action
181 elif type == 'm':
173 elif type == 'm':
182 temporaryfiles.append(file)
174 temporaryfiles.append(file)
183 prunedactions[file] = action
175 prunedactions[file] = action
184 elif branchmerge:
176 elif branchmerge:
185 if type != 'k':
177 if type != 'k':
186 temporaryfiles.append(file)
178 temporaryfiles.append(file)
187 prunedactions[file] = action
179 prunedactions[file] = action
188 elif type == 'f':
180 elif type == 'f':
189 prunedactions[file] = action
181 prunedactions[file] = action
190 elif file in wctx:
182 elif file in wctx:
191 prunedactions[file] = ('r', args, msg)
183 prunedactions[file] = ('r', args, msg)
192
184
193 if len(temporaryfiles) > 0:
185 if len(temporaryfiles) > 0:
194 ui.status(_("temporarily included %d file(s) in the sparse checkout"
186 ui.status(_("temporarily included %d file(s) in the sparse checkout"
195 " for merging\n") % len(temporaryfiles))
187 " for merging\n") % len(temporaryfiles))
196 sparse.addtemporaryincludes(repo, temporaryfiles)
188 sparse.addtemporaryincludes(repo, temporaryfiles)
197
189
198 # Add the new files to the working copy so they can be merged, etc
190 # Add the new files to the working copy so they can be merged, etc
199 actions = []
191 actions = []
200 message = 'temporarily adding to sparse checkout'
192 message = 'temporarily adding to sparse checkout'
201 wctxmanifest = repo[None].manifest()
193 wctxmanifest = repo[None].manifest()
202 for file in temporaryfiles:
194 for file in temporaryfiles:
203 if file in wctxmanifest:
195 if file in wctxmanifest:
204 fctx = repo[None][file]
196 fctx = repo[None][file]
205 actions.append((file, (fctx.flags(), False), message))
197 actions.append((file, (fctx.flags(), False), message))
206
198
207 typeactions = collections.defaultdict(list)
199 typeactions = collections.defaultdict(list)
208 typeactions['g'] = actions
200 typeactions['g'] = actions
209 mergemod.applyupdates(repo, typeactions, repo[None], repo['.'],
201 mergemod.applyupdates(repo, typeactions, repo[None], repo['.'],
210 False)
202 False)
211
203
212 dirstate = repo.dirstate
204 dirstate = repo.dirstate
213 for file, flags, msg in actions:
205 for file, flags, msg in actions:
214 dirstate.normal(file)
206 dirstate.normal(file)
215
207
216 profiles = sparse.activeprofiles(repo)
208 profiles = sparse.activeprofiles(repo)
217 changedprofiles = profiles & files
209 changedprofiles = profiles & files
218 # If an active profile changed during the update, refresh the checkout.
210 # If an active profile changed during the update, refresh the checkout.
219 # Don't do this during a branch merge, since all incoming changes should
211 # Don't do this during a branch merge, since all incoming changes should
220 # have been handled by the temporary includes above.
212 # have been handled by the temporary includes above.
221 if changedprofiles and not branchmerge:
213 if changedprofiles and not branchmerge:
222 mf = mctx.manifest()
214 mf = mctx.manifest()
223 for file in mf:
215 for file in mf:
224 old = oldsparsematch(file)
216 old = oldsparsematch(file)
225 new = sparsematch(file)
217 new = sparsematch(file)
226 if not old and new:
218 if not old and new:
227 flags = mf.flags(file)
219 flags = mf.flags(file)
228 prunedactions[file] = ('g', (flags, False), '')
220 prunedactions[file] = ('g', (flags, False), '')
229 elif old and not new:
221 elif old and not new:
230 prunedactions[file] = ('r', [], '')
222 prunedactions[file] = ('r', [], '')
231
223
232 return prunedactions, diverge, renamedelete
224 return prunedactions, diverge, renamedelete
233
225
234 extensions.wrapfunction(mergemod, 'calculateupdates', _calculateupdates)
226 extensions.wrapfunction(mergemod, 'calculateupdates', _calculateupdates)
235
227
236 def _update(orig, repo, node, branchmerge, *args, **kwargs):
228 def _update(orig, repo, node, branchmerge, *args, **kwargs):
237 results = orig(repo, node, branchmerge, *args, **kwargs)
229 results = orig(repo, node, branchmerge, *args, **kwargs)
238
230
239 # If we're updating to a location, clean up any stale temporary includes
231 # If we're updating to a location, clean up any stale temporary includes
240 # (ex: this happens during hg rebase --abort).
232 # (ex: this happens during hg rebase --abort).
241 if not branchmerge and util.safehasattr(repo, 'sparsematch'):
233 if not branchmerge and util.safehasattr(repo, 'sparsematch'):
242 repo.prunetemporaryincludes()
234 repo.prunetemporaryincludes()
243 return results
235 return results
244
236
245 extensions.wrapfunction(mergemod, 'update', _update)
237 extensions.wrapfunction(mergemod, 'update', _update)
246
238
247 def _setupcommit(ui):
239 def _setupcommit(ui):
248 def _refreshoncommit(orig, self, node):
240 def _refreshoncommit(orig, self, node):
249 """Refresh the checkout when commits touch .hgsparse
241 """Refresh the checkout when commits touch .hgsparse
250 """
242 """
251 orig(self, node)
243 orig(self, node)
252 repo = self._repo
244 repo = self._repo
253
245
254 ctx = repo[node]
246 ctx = repo[node]
255 profiles = sparse.patternsforrev(repo, ctx.rev())[2]
247 profiles = sparse.patternsforrev(repo, ctx.rev())[2]
256
248
257 # profiles will only have data if sparse is enabled.
249 # profiles will only have data if sparse is enabled.
258 if set(profiles) & set(ctx.files()):
250 if set(profiles) & set(ctx.files()):
259 origstatus = repo.status()
251 origstatus = repo.status()
260 origsparsematch = repo.sparsematch()
252 origsparsematch = repo.sparsematch()
261 _refresh(repo.ui, repo, origstatus, origsparsematch, True)
253 _refresh(repo.ui, repo, origstatus, origsparsematch, True)
262
254
263 if util.safehasattr(repo, 'prunetemporaryincludes'):
255 if util.safehasattr(repo, 'prunetemporaryincludes'):
264 repo.prunetemporaryincludes()
256 repo.prunetemporaryincludes()
265
257
266 extensions.wrapfunction(context.committablectx, 'markcommitted',
258 extensions.wrapfunction(context.committablectx, 'markcommitted',
267 _refreshoncommit)
259 _refreshoncommit)
268
260
269 def _setuplog(ui):
261 def _setuplog(ui):
270 entry = commands.table['^log|history']
262 entry = commands.table['^log|history']
271 entry[1].append(('', 'sparse', None,
263 entry[1].append(('', 'sparse', None,
272 "limit to changesets affecting the sparse checkout"))
264 "limit to changesets affecting the sparse checkout"))
273
265
274 def _logrevs(orig, repo, opts):
266 def _logrevs(orig, repo, opts):
275 revs = orig(repo, opts)
267 revs = orig(repo, opts)
276 if opts.get('sparse'):
268 if opts.get('sparse'):
277 sparsematch = repo.sparsematch()
269 sparsematch = repo.sparsematch()
278 def ctxmatch(rev):
270 def ctxmatch(rev):
279 ctx = repo[rev]
271 ctx = repo[rev]
280 return any(f for f in ctx.files() if sparsematch(f))
272 return any(f for f in ctx.files() if sparsematch(f))
281 revs = revs.filter(ctxmatch)
273 revs = revs.filter(ctxmatch)
282 return revs
274 return revs
283 extensions.wrapfunction(cmdutil, '_logrevs', _logrevs)
275 extensions.wrapfunction(cmdutil, '_logrevs', _logrevs)
284
276
285 def _clonesparsecmd(orig, ui, repo, *args, **opts):
277 def _clonesparsecmd(orig, ui, repo, *args, **opts):
286 include_pat = opts.get('include')
278 include_pat = opts.get('include')
287 exclude_pat = opts.get('exclude')
279 exclude_pat = opts.get('exclude')
288 enableprofile_pat = opts.get('enable_profile')
280 enableprofile_pat = opts.get('enable_profile')
289 include = exclude = enableprofile = False
281 include = exclude = enableprofile = False
290 if include_pat:
282 if include_pat:
291 pat = include_pat
283 pat = include_pat
292 include = True
284 include = True
293 if exclude_pat:
285 if exclude_pat:
294 pat = exclude_pat
286 pat = exclude_pat
295 exclude = True
287 exclude = True
296 if enableprofile_pat:
288 if enableprofile_pat:
297 pat = enableprofile_pat
289 pat = enableprofile_pat
298 enableprofile = True
290 enableprofile = True
299 if sum([include, exclude, enableprofile]) > 1:
291 if sum([include, exclude, enableprofile]) > 1:
300 raise error.Abort(_("too many flags specified."))
292 raise error.Abort(_("too many flags specified."))
301 if include or exclude or enableprofile:
293 if include or exclude or enableprofile:
302 def clonesparse(orig, self, node, overwrite, *args, **kwargs):
294 def clonesparse(orig, self, node, overwrite, *args, **kwargs):
303 _config(self.ui, self.unfiltered(), pat, {}, include=include,
295 _config(self.ui, self.unfiltered(), pat, {}, include=include,
304 exclude=exclude, enableprofile=enableprofile)
296 exclude=exclude, enableprofile=enableprofile)
305 return orig(self, node, overwrite, *args, **kwargs)
297 return orig(self, node, overwrite, *args, **kwargs)
306 extensions.wrapfunction(hg, 'updaterepo', clonesparse)
298 extensions.wrapfunction(hg, 'updaterepo', clonesparse)
307 return orig(ui, repo, *args, **opts)
299 return orig(ui, repo, *args, **opts)
308
300
309 def _setupclone(ui):
301 def _setupclone(ui):
310 entry = commands.table['^clone']
302 entry = commands.table['^clone']
311 entry[1].append(('', 'enable-profile', [],
303 entry[1].append(('', 'enable-profile', [],
312 'enable a sparse profile'))
304 'enable a sparse profile'))
313 entry[1].append(('', 'include', [],
305 entry[1].append(('', 'include', [],
314 'include sparse pattern'))
306 'include sparse pattern'))
315 entry[1].append(('', 'exclude', [],
307 entry[1].append(('', 'exclude', [],
316 'exclude sparse pattern'))
308 'exclude sparse pattern'))
317 extensions.wrapcommand(commands.table, 'clone', _clonesparsecmd)
309 extensions.wrapcommand(commands.table, 'clone', _clonesparsecmd)
318
310
319 def _setupadd(ui):
311 def _setupadd(ui):
320 entry = commands.table['^add']
312 entry = commands.table['^add']
321 entry[1].append(('s', 'sparse', None,
313 entry[1].append(('s', 'sparse', None,
322 'also include directories of added files in sparse config'))
314 'also include directories of added files in sparse config'))
323
315
324 def _add(orig, ui, repo, *pats, **opts):
316 def _add(orig, ui, repo, *pats, **opts):
325 if opts.get('sparse'):
317 if opts.get('sparse'):
326 dirs = set()
318 dirs = set()
327 for pat in pats:
319 for pat in pats:
328 dirname, basename = util.split(pat)
320 dirname, basename = util.split(pat)
329 dirs.add(dirname)
321 dirs.add(dirname)
330 _config(ui, repo, list(dirs), opts, include=True)
322 _config(ui, repo, list(dirs), opts, include=True)
331 return orig(ui, repo, *pats, **opts)
323 return orig(ui, repo, *pats, **opts)
332
324
333 extensions.wrapcommand(commands.table, 'add', _add)
325 extensions.wrapcommand(commands.table, 'add', _add)
334
326
335 def _setupdirstate(ui):
327 def _setupdirstate(ui):
336 """Modify the dirstate to prevent stat'ing excluded files,
328 """Modify the dirstate to prevent stat'ing excluded files,
337 and to prevent modifications to files outside the checkout.
329 and to prevent modifications to files outside the checkout.
338 """
330 """
339
331
340 def _dirstate(orig, repo):
332 def _dirstate(orig, repo):
341 dirstate = orig(repo)
333 dirstate = orig(repo)
342 dirstate.repo = repo
334 dirstate.repo = repo
343 return dirstate
335 return dirstate
344 extensions.wrapfunction(
336 extensions.wrapfunction(
345 localrepo.localrepository.dirstate, 'func', _dirstate)
337 localrepo.localrepository.dirstate, 'func', _dirstate)
346
338
347 # The atrocity below is needed to wrap dirstate._ignore. It is a cached
339 # The atrocity below is needed to wrap dirstate._ignore. It is a cached
348 # property, which means normal function wrapping doesn't work.
340 # property, which means normal function wrapping doesn't work.
349 class ignorewrapper(object):
341 class ignorewrapper(object):
350 def __init__(self, orig):
342 def __init__(self, orig):
351 self.orig = orig
343 self.orig = orig
352 self.origignore = None
344 self.origignore = None
353 self.func = None
345 self.func = None
354 self.sparsematch = None
346 self.sparsematch = None
355
347
356 def __get__(self, obj, type=None):
348 def __get__(self, obj, type=None):
357 repo = obj.repo
349 repo = obj.repo
358 origignore = self.orig.__get__(obj)
350 origignore = self.orig.__get__(obj)
359 if not util.safehasattr(repo, 'sparsematch'):
351 if not util.safehasattr(repo, 'sparsematch'):
360 return origignore
352 return origignore
361
353
362 sparsematch = repo.sparsematch()
354 sparsematch = repo.sparsematch()
363 if self.sparsematch != sparsematch or self.origignore != origignore:
355 if self.sparsematch != sparsematch or self.origignore != origignore:
364 self.func = unionmatcher([origignore,
356 self.func = unionmatcher([origignore,
365 negatematcher(sparsematch)])
357 negatematcher(sparsematch)])
366 self.sparsematch = sparsematch
358 self.sparsematch = sparsematch
367 self.origignore = origignore
359 self.origignore = origignore
368 return self.func
360 return self.func
369
361
370 def __set__(self, obj, value):
362 def __set__(self, obj, value):
371 return self.orig.__set__(obj, value)
363 return self.orig.__set__(obj, value)
372
364
373 def __delete__(self, obj):
365 def __delete__(self, obj):
374 return self.orig.__delete__(obj)
366 return self.orig.__delete__(obj)
375
367
376 replacefilecache(dirstate.dirstate, '_ignore', ignorewrapper)
368 replacefilecache(dirstate.dirstate, '_ignore', ignorewrapper)
377
369
378 # dirstate.rebuild should not add non-matching files
370 # dirstate.rebuild should not add non-matching files
379 def _rebuild(orig, self, parent, allfiles, changedfiles=None):
371 def _rebuild(orig, self, parent, allfiles, changedfiles=None):
380 if util.safehasattr(self.repo, 'sparsematch'):
372 if util.safehasattr(self.repo, 'sparsematch'):
381 matcher = self.repo.sparsematch()
373 matcher = self.repo.sparsematch()
382 allfiles = allfiles.matches(matcher)
374 allfiles = allfiles.matches(matcher)
383 if changedfiles:
375 if changedfiles:
384 changedfiles = [f for f in changedfiles if matcher(f)]
376 changedfiles = [f for f in changedfiles if matcher(f)]
385
377
386 if changedfiles is not None:
378 if changedfiles is not None:
387 # In _rebuild, these files will be deleted from the dirstate
379 # In _rebuild, these files will be deleted from the dirstate
388 # when they are not found to be in allfiles
380 # when they are not found to be in allfiles
389 dirstatefilestoremove = set(f for f in self if not matcher(f))
381 dirstatefilestoremove = set(f for f in self if not matcher(f))
390 changedfiles = dirstatefilestoremove.union(changedfiles)
382 changedfiles = dirstatefilestoremove.union(changedfiles)
391
383
392 return orig(self, parent, allfiles, changedfiles)
384 return orig(self, parent, allfiles, changedfiles)
393 extensions.wrapfunction(dirstate.dirstate, 'rebuild', _rebuild)
385 extensions.wrapfunction(dirstate.dirstate, 'rebuild', _rebuild)
394
386
395 # Prevent adding files that are outside the sparse checkout
387 # Prevent adding files that are outside the sparse checkout
396 editfuncs = ['normal', 'add', 'normallookup', 'copy', 'remove', 'merge']
388 editfuncs = ['normal', 'add', 'normallookup', 'copy', 'remove', 'merge']
397 hint = _('include file with `hg debugsparse --include <pattern>` or use ' +
389 hint = _('include file with `hg debugsparse --include <pattern>` or use ' +
398 '`hg add -s <file>` to include file directory while adding')
390 '`hg add -s <file>` to include file directory while adding')
399 for func in editfuncs:
391 for func in editfuncs:
400 def _wrapper(orig, self, *args):
392 def _wrapper(orig, self, *args):
401 repo = self.repo
393 repo = self.repo
402 if util.safehasattr(repo, 'sparsematch'):
394 if util.safehasattr(repo, 'sparsematch'):
403 dirstate = repo.dirstate
395 dirstate = repo.dirstate
404 sparsematch = repo.sparsematch()
396 sparsematch = repo.sparsematch()
405 for f in args:
397 for f in args:
406 if (f is not None and not sparsematch(f) and
398 if (f is not None and not sparsematch(f) and
407 f not in dirstate):
399 f not in dirstate):
408 raise error.Abort(_("cannot add '%s' - it is outside "
400 raise error.Abort(_("cannot add '%s' - it is outside "
409 "the sparse checkout") % f,
401 "the sparse checkout") % f,
410 hint=hint)
402 hint=hint)
411 return orig(self, *args)
403 return orig(self, *args)
412 extensions.wrapfunction(dirstate.dirstate, func, _wrapper)
404 extensions.wrapfunction(dirstate.dirstate, func, _wrapper)
413
405
414 def _wraprepo(ui, repo):
406 def _wraprepo(ui, repo):
415 class SparseRepo(repo.__class__):
407 class SparseRepo(repo.__class__):
416 def _sparsechecksum(self, path):
408 def _sparsechecksum(self, path):
417 data = self.vfs.read(path)
409 data = self.vfs.read(path)
418 return hashlib.sha1(data).hexdigest()
410 return hashlib.sha1(data).hexdigest()
419
411
420 def _sparsesignature(self, includetemp=True):
412 def _sparsesignature(self, includetemp=True):
421 """Returns the signature string representing the contents of the
413 """Returns the signature string representing the contents of the
422 current project sparse configuration. This can be used to cache the
414 current project sparse configuration. This can be used to cache the
423 sparse matcher for a given set of revs."""
415 sparse matcher for a given set of revs."""
424 signaturecache = self._sparsesignaturecache
416 signaturecache = self._sparsesignaturecache
425 signature = signaturecache.get('signature')
417 signature = signaturecache.get('signature')
426 if includetemp:
418 if includetemp:
427 tempsignature = signaturecache.get('tempsignature')
419 tempsignature = signaturecache.get('tempsignature')
428 else:
420 else:
429 tempsignature = 0
421 tempsignature = 0
430
422
431 if signature is None or (includetemp and tempsignature is None):
423 if signature is None or (includetemp and tempsignature is None):
432 signature = 0
424 signature = 0
433 try:
425 try:
434 signature = self._sparsechecksum('sparse')
426 signature = self._sparsechecksum('sparse')
435 except (OSError, IOError):
427 except (OSError, IOError):
436 pass
428 pass
437 signaturecache['signature'] = signature
429 signaturecache['signature'] = signature
438
430
439 tempsignature = 0
431 tempsignature = 0
440 if includetemp:
432 if includetemp:
441 try:
433 try:
442 tempsignature = self._sparsechecksum('tempsparse')
434 tempsignature = self._sparsechecksum('tempsparse')
443 except (OSError, IOError):
435 except (OSError, IOError):
444 pass
436 pass
445 signaturecache['tempsignature'] = tempsignature
437 signaturecache['tempsignature'] = tempsignature
446 return '%s %s' % (str(signature), str(tempsignature))
438 return '%s %s' % (str(signature), str(tempsignature))
447
439
448 def sparsematch(self, *revs, **kwargs):
440 def sparsematch(self, *revs, **kwargs):
449 """Returns the sparse match function for the given revs.
441 """Returns the sparse match function for the given revs.
450
442
451 If multiple revs are specified, the match function is the union
443 If multiple revs are specified, the match function is the union
452 of all the revs.
444 of all the revs.
453
445
454 `includetemp` is used to indicate if the temporarily included file
446 `includetemp` is used to indicate if the temporarily included file
455 should be part of the matcher.
447 should be part of the matcher.
456 """
448 """
457 if not revs or revs == (None,):
449 if not revs or revs == (None,):
458 revs = [self.changelog.rev(node) for node in
450 revs = [self.changelog.rev(node) for node in
459 self.dirstate.parents() if node != nullid]
451 self.dirstate.parents() if node != nullid]
460
452
461 includetemp = kwargs.get('includetemp', True)
453 includetemp = kwargs.get('includetemp', True)
462 signature = self._sparsesignature(includetemp=includetemp)
454 signature = self._sparsesignature(includetemp=includetemp)
463
455
464 key = '%s %s' % (str(signature), ' '.join([str(r) for r in revs]))
456 key = '%s %s' % (str(signature), ' '.join([str(r) for r in revs]))
465
457
466 result = self._sparsematchercache.get(key, None)
458 result = self._sparsematchercache.get(key, None)
467 if result:
459 if result:
468 return result
460 return result
469
461
470 matchers = []
462 matchers = []
471 for rev in revs:
463 for rev in revs:
472 try:
464 try:
473 includes, excludes, profiles = sparse.patternsforrev(
465 includes, excludes, profiles = sparse.patternsforrev(
474 self, rev)
466 self, rev)
475
467
476 if includes or excludes:
468 if includes or excludes:
477 # Explicitly include subdirectories of includes so
469 # Explicitly include subdirectories of includes so
478 # status will walk them down to the actual include.
470 # status will walk them down to the actual include.
479 subdirs = set()
471 subdirs = set()
480 for include in includes:
472 for include in includes:
481 dirname = os.path.dirname(include)
473 dirname = os.path.dirname(include)
482 # basename is used to avoid issues with absolute
474 # basename is used to avoid issues with absolute
483 # paths (which on Windows can include the drive).
475 # paths (which on Windows can include the drive).
484 while os.path.basename(dirname):
476 while os.path.basename(dirname):
485 subdirs.add(dirname)
477 subdirs.add(dirname)
486 dirname = os.path.dirname(dirname)
478 dirname = os.path.dirname(dirname)
487
479
488 matcher = matchmod.match(self.root, '', [],
480 matcher = matchmod.match(self.root, '', [],
489 include=includes, exclude=excludes,
481 include=includes, exclude=excludes,
490 default='relpath')
482 default='relpath')
491 if subdirs:
483 if subdirs:
492 matcher = forceincludematcher(matcher, subdirs)
484 matcher = forceincludematcher(matcher, subdirs)
493 matchers.append(matcher)
485 matchers.append(matcher)
494 except IOError:
486 except IOError:
495 pass
487 pass
496
488
497 result = None
489 result = None
498 if not matchers:
490 if not matchers:
499 result = matchmod.always(self.root, '')
491 result = matchmod.always(self.root, '')
500 elif len(matchers) == 1:
492 elif len(matchers) == 1:
501 result = matchers[0]
493 result = matchers[0]
502 else:
494 else:
503 result = unionmatcher(matchers)
495 result = unionmatcher(matchers)
504
496
505 if kwargs.get('includetemp', True):
497 if kwargs.get('includetemp', True):
506 tempincludes = sparse.readtemporaryincludes(self)
498 tempincludes = sparse.readtemporaryincludes(self)
507 result = forceincludematcher(result, tempincludes)
499 result = forceincludematcher(result, tempincludes)
508
500
509 self._sparsematchercache[key] = result
501 self._sparsematchercache[key] = result
510
502
511 return result
503 return result
512
504
513 def prunetemporaryincludes(self):
505 def prunetemporaryincludes(self):
514 if repo.vfs.exists('tempsparse'):
506 if repo.vfs.exists('tempsparse'):
515 origstatus = self.status()
507 origstatus = self.status()
516 modified, added, removed, deleted, a, b, c = origstatus
508 modified, added, removed, deleted, a, b, c = origstatus
517 if modified or added or removed or deleted:
509 if modified or added or removed or deleted:
518 # Still have pending changes. Don't bother trying to prune.
510 # Still have pending changes. Don't bother trying to prune.
519 return
511 return
520
512
521 sparsematch = self.sparsematch(includetemp=False)
513 sparsematch = self.sparsematch(includetemp=False)
522 dirstate = self.dirstate
514 dirstate = self.dirstate
523 actions = []
515 actions = []
524 dropped = []
516 dropped = []
525 tempincludes = sparse.readtemporaryincludes(self)
517 tempincludes = sparse.readtemporaryincludes(self)
526 for file in tempincludes:
518 for file in tempincludes:
527 if file in dirstate and not sparsematch(file):
519 if file in dirstate and not sparsematch(file):
528 message = 'dropping temporarily included sparse files'
520 message = 'dropping temporarily included sparse files'
529 actions.append((file, None, message))
521 actions.append((file, None, message))
530 dropped.append(file)
522 dropped.append(file)
531
523
532 typeactions = collections.defaultdict(list)
524 typeactions = collections.defaultdict(list)
533 typeactions['r'] = actions
525 typeactions['r'] = actions
534 mergemod.applyupdates(self, typeactions, self[None], self['.'],
526 mergemod.applyupdates(self, typeactions, self[None], self['.'],
535 False)
527 False)
536
528
537 # Fix dirstate
529 # Fix dirstate
538 for file in dropped:
530 for file in dropped:
539 dirstate.drop(file)
531 dirstate.drop(file)
540
532
541 self.vfs.unlink('tempsparse')
533 self.vfs.unlink('tempsparse')
542 sparse.invalidatesignaturecache(self)
534 sparse.invalidatesignaturecache(self)
543 msg = _("cleaned up %d temporarily added file(s) from the "
535 msg = _("cleaned up %d temporarily added file(s) from the "
544 "sparse checkout\n")
536 "sparse checkout\n")
545 ui.status(msg % len(tempincludes))
537 ui.status(msg % len(tempincludes))
546
538
547 if 'dirstate' in repo._filecache:
539 if 'dirstate' in repo._filecache:
548 repo.dirstate.repo = repo
540 repo.dirstate.repo = repo
549
541
550 repo.__class__ = SparseRepo
542 repo.__class__ = SparseRepo
551
543
552 @command('^debugsparse', [
544 @command('^debugsparse', [
553 ('I', 'include', False, _('include files in the sparse checkout')),
545 ('I', 'include', False, _('include files in the sparse checkout')),
554 ('X', 'exclude', False, _('exclude files in the sparse checkout')),
546 ('X', 'exclude', False, _('exclude files in the sparse checkout')),
555 ('d', 'delete', False, _('delete an include/exclude rule')),
547 ('d', 'delete', False, _('delete an include/exclude rule')),
556 ('f', 'force', False, _('allow changing rules even with pending changes')),
548 ('f', 'force', False, _('allow changing rules even with pending changes')),
557 ('', 'enable-profile', False, _('enables the specified profile')),
549 ('', 'enable-profile', False, _('enables the specified profile')),
558 ('', 'disable-profile', False, _('disables the specified profile')),
550 ('', 'disable-profile', False, _('disables the specified profile')),
559 ('', 'import-rules', False, _('imports rules from a file')),
551 ('', 'import-rules', False, _('imports rules from a file')),
560 ('', 'clear-rules', False, _('clears local include/exclude rules')),
552 ('', 'clear-rules', False, _('clears local include/exclude rules')),
561 ('', 'refresh', False, _('updates the working after sparseness changes')),
553 ('', 'refresh', False, _('updates the working after sparseness changes')),
562 ('', 'reset', False, _('makes the repo full again')),
554 ('', 'reset', False, _('makes the repo full again')),
563 ] + commands.templateopts,
555 ] + commands.templateopts,
564 _('[--OPTION] PATTERN...'))
556 _('[--OPTION] PATTERN...'))
565 def debugsparse(ui, repo, *pats, **opts):
557 def debugsparse(ui, repo, *pats, **opts):
566 """make the current checkout sparse, or edit the existing checkout
558 """make the current checkout sparse, or edit the existing checkout
567
559
568 The sparse command is used to make the current checkout sparse.
560 The sparse command is used to make the current checkout sparse.
569 This means files that don't meet the sparse condition will not be
561 This means files that don't meet the sparse condition will not be
570 written to disk, or show up in any working copy operations. It does
562 written to disk, or show up in any working copy operations. It does
571 not affect files in history in any way.
563 not affect files in history in any way.
572
564
573 Passing no arguments prints the currently applied sparse rules.
565 Passing no arguments prints the currently applied sparse rules.
574
566
575 --include and --exclude are used to add and remove files from the sparse
567 --include and --exclude are used to add and remove files from the sparse
576 checkout. The effects of adding an include or exclude rule are applied
568 checkout. The effects of adding an include or exclude rule are applied
577 immediately. If applying the new rule would cause a file with pending
569 immediately. If applying the new rule would cause a file with pending
578 changes to be added or removed, the command will fail. Pass --force to
570 changes to be added or removed, the command will fail. Pass --force to
579 force a rule change even with pending changes (the changes on disk will
571 force a rule change even with pending changes (the changes on disk will
580 be preserved).
572 be preserved).
581
573
582 --delete removes an existing include/exclude rule. The effects are
574 --delete removes an existing include/exclude rule. The effects are
583 immediate.
575 immediate.
584
576
585 --refresh refreshes the files on disk based on the sparse rules. This is
577 --refresh refreshes the files on disk based on the sparse rules. This is
586 only necessary if .hg/sparse was changed by hand.
578 only necessary if .hg/sparse was changed by hand.
587
579
588 --enable-profile and --disable-profile accept a path to a .hgsparse file.
580 --enable-profile and --disable-profile accept a path to a .hgsparse file.
589 This allows defining sparse checkouts and tracking them inside the
581 This allows defining sparse checkouts and tracking them inside the
590 repository. This is useful for defining commonly used sparse checkouts for
582 repository. This is useful for defining commonly used sparse checkouts for
591 many people to use. As the profile definition changes over time, the sparse
583 many people to use. As the profile definition changes over time, the sparse
592 checkout will automatically be updated appropriately, depending on which
584 checkout will automatically be updated appropriately, depending on which
593 changeset is checked out. Changes to .hgsparse are not applied until they
585 changeset is checked out. Changes to .hgsparse are not applied until they
594 have been committed.
586 have been committed.
595
587
596 --import-rules accepts a path to a file containing rules in the .hgsparse
588 --import-rules accepts a path to a file containing rules in the .hgsparse
597 format, allowing you to add --include, --exclude and --enable-profile rules
589 format, allowing you to add --include, --exclude and --enable-profile rules
598 in bulk. Like the --include, --exclude and --enable-profile switches, the
590 in bulk. Like the --include, --exclude and --enable-profile switches, the
599 changes are applied immediately.
591 changes are applied immediately.
600
592
601 --clear-rules removes all local include and exclude rules, while leaving
593 --clear-rules removes all local include and exclude rules, while leaving
602 any enabled profiles in place.
594 any enabled profiles in place.
603
595
604 Returns 0 if editing the sparse checkout succeeds.
596 Returns 0 if editing the sparse checkout succeeds.
605 """
597 """
606 include = opts.get('include')
598 include = opts.get('include')
607 exclude = opts.get('exclude')
599 exclude = opts.get('exclude')
608 force = opts.get('force')
600 force = opts.get('force')
609 enableprofile = opts.get('enable_profile')
601 enableprofile = opts.get('enable_profile')
610 disableprofile = opts.get('disable_profile')
602 disableprofile = opts.get('disable_profile')
611 importrules = opts.get('import_rules')
603 importrules = opts.get('import_rules')
612 clearrules = opts.get('clear_rules')
604 clearrules = opts.get('clear_rules')
613 delete = opts.get('delete')
605 delete = opts.get('delete')
614 refresh = opts.get('refresh')
606 refresh = opts.get('refresh')
615 reset = opts.get('reset')
607 reset = opts.get('reset')
616 count = sum([include, exclude, enableprofile, disableprofile, delete,
608 count = sum([include, exclude, enableprofile, disableprofile, delete,
617 importrules, refresh, clearrules, reset])
609 importrules, refresh, clearrules, reset])
618 if count > 1:
610 if count > 1:
619 raise error.Abort(_("too many flags specified"))
611 raise error.Abort(_("too many flags specified"))
620
612
621 if count == 0:
613 if count == 0:
622 if repo.vfs.exists('sparse'):
614 if repo.vfs.exists('sparse'):
623 ui.status(repo.vfs.read("sparse") + "\n")
615 ui.status(repo.vfs.read("sparse") + "\n")
624 temporaryincludes = sparse.readtemporaryincludes(repo)
616 temporaryincludes = sparse.readtemporaryincludes(repo)
625 if temporaryincludes:
617 if temporaryincludes:
626 ui.status(_("Temporarily Included Files (for merge/rebase):\n"))
618 ui.status(_("Temporarily Included Files (for merge/rebase):\n"))
627 ui.status(("\n".join(temporaryincludes) + "\n"))
619 ui.status(("\n".join(temporaryincludes) + "\n"))
628 else:
620 else:
629 ui.status(_('repo is not sparse\n'))
621 ui.status(_('repo is not sparse\n'))
630 return
622 return
631
623
632 if include or exclude or delete or reset or enableprofile or disableprofile:
624 if include or exclude or delete or reset or enableprofile or disableprofile:
633 _config(ui, repo, pats, opts, include=include, exclude=exclude,
625 _config(ui, repo, pats, opts, include=include, exclude=exclude,
634 reset=reset, delete=delete, enableprofile=enableprofile,
626 reset=reset, delete=delete, enableprofile=enableprofile,
635 disableprofile=disableprofile, force=force)
627 disableprofile=disableprofile, force=force)
636
628
637 if importrules:
629 if importrules:
638 _import(ui, repo, pats, opts, force=force)
630 _import(ui, repo, pats, opts, force=force)
639
631
640 if clearrules:
632 if clearrules:
641 _clear(ui, repo, pats, force=force)
633 _clear(ui, repo, pats, force=force)
642
634
643 if refresh:
635 if refresh:
644 try:
636 try:
645 wlock = repo.wlock()
637 wlock = repo.wlock()
646 fcounts = map(
638 fcounts = map(
647 len,
639 len,
648 _refresh(ui, repo, repo.status(), repo.sparsematch(), force))
640 _refresh(ui, repo, repo.status(), repo.sparsematch(), force))
649 _verbose_output(ui, opts, 0, 0, 0, *fcounts)
641 _verbose_output(ui, opts, 0, 0, 0, *fcounts)
650 finally:
642 finally:
651 wlock.release()
643 wlock.release()
652
644
653 def _config(ui, repo, pats, opts, include=False, exclude=False, reset=False,
645 def _config(ui, repo, pats, opts, include=False, exclude=False, reset=False,
654 delete=False, enableprofile=False, disableprofile=False,
646 delete=False, enableprofile=False, disableprofile=False,
655 force=False):
647 force=False):
656 """
648 """
657 Perform a sparse config update. Only one of the kwargs may be specified.
649 Perform a sparse config update. Only one of the kwargs may be specified.
658 """
650 """
659 wlock = repo.wlock()
651 wlock = repo.wlock()
660 try:
652 try:
661 oldsparsematch = repo.sparsematch()
653 oldsparsematch = repo.sparsematch()
662
654
663 raw = repo.vfs.tryread('sparse')
655 raw = repo.vfs.tryread('sparse')
664 if raw:
656 if raw:
665 oldinclude, oldexclude, oldprofiles = map(
657 oldinclude, oldexclude, oldprofiles = map(
666 set, sparse.parseconfig(ui, raw))
658 set, sparse.parseconfig(ui, raw))
667 else:
659 else:
668 oldinclude = set()
660 oldinclude = set()
669 oldexclude = set()
661 oldexclude = set()
670 oldprofiles = set()
662 oldprofiles = set()
671
663
672 try:
664 try:
673 if reset:
665 if reset:
674 newinclude = set()
666 newinclude = set()
675 newexclude = set()
667 newexclude = set()
676 newprofiles = set()
668 newprofiles = set()
677 else:
669 else:
678 newinclude = set(oldinclude)
670 newinclude = set(oldinclude)
679 newexclude = set(oldexclude)
671 newexclude = set(oldexclude)
680 newprofiles = set(oldprofiles)
672 newprofiles = set(oldprofiles)
681
673
682 oldstatus = repo.status()
674 oldstatus = repo.status()
683
675
684 if any(pat.startswith('/') for pat in pats):
676 if any(pat.startswith('/') for pat in pats):
685 ui.warn(_('warning: paths cannot start with /, ignoring: %s\n')
677 ui.warn(_('warning: paths cannot start with /, ignoring: %s\n')
686 % ([pat for pat in pats if pat.startswith('/')]))
678 % ([pat for pat in pats if pat.startswith('/')]))
687 elif include:
679 elif include:
688 newinclude.update(pats)
680 newinclude.update(pats)
689 elif exclude:
681 elif exclude:
690 newexclude.update(pats)
682 newexclude.update(pats)
691 elif enableprofile:
683 elif enableprofile:
692 newprofiles.update(pats)
684 newprofiles.update(pats)
693 elif disableprofile:
685 elif disableprofile:
694 newprofiles.difference_update(pats)
686 newprofiles.difference_update(pats)
695 elif delete:
687 elif delete:
696 newinclude.difference_update(pats)
688 newinclude.difference_update(pats)
697 newexclude.difference_update(pats)
689 newexclude.difference_update(pats)
698
690
699 sparse.writeconfig(repo, newinclude, newexclude, newprofiles)
691 sparse.writeconfig(repo, newinclude, newexclude, newprofiles)
700
692
701 fcounts = map(
693 fcounts = map(
702 len, _refresh(ui, repo, oldstatus, oldsparsematch, force))
694 len, _refresh(ui, repo, oldstatus, oldsparsematch, force))
703
695
704 profilecount = (len(newprofiles - oldprofiles) -
696 profilecount = (len(newprofiles - oldprofiles) -
705 len(oldprofiles - newprofiles))
697 len(oldprofiles - newprofiles))
706 includecount = (len(newinclude - oldinclude) -
698 includecount = (len(newinclude - oldinclude) -
707 len(oldinclude - newinclude))
699 len(oldinclude - newinclude))
708 excludecount = (len(newexclude - oldexclude) -
700 excludecount = (len(newexclude - oldexclude) -
709 len(oldexclude - newexclude))
701 len(oldexclude - newexclude))
710 _verbose_output(
702 _verbose_output(
711 ui, opts, profilecount, includecount, excludecount, *fcounts)
703 ui, opts, profilecount, includecount, excludecount, *fcounts)
712 except Exception:
704 except Exception:
713 sparse.writeconfig(repo, oldinclude, oldexclude, oldprofiles)
705 sparse.writeconfig(repo, oldinclude, oldexclude, oldprofiles)
714 raise
706 raise
715 finally:
707 finally:
716 wlock.release()
708 wlock.release()
717
709
718 def _import(ui, repo, files, opts, force=False):
710 def _import(ui, repo, files, opts, force=False):
719 with repo.wlock():
711 with repo.wlock():
720 # load union of current active profile
712 # load union of current active profile
721 revs = [repo.changelog.rev(node) for node in
713 revs = [repo.changelog.rev(node) for node in
722 repo.dirstate.parents() if node != nullid]
714 repo.dirstate.parents() if node != nullid]
723
715
724 # read current configuration
716 # read current configuration
725 raw = repo.vfs.tryread('sparse')
717 raw = repo.vfs.tryread('sparse')
726 oincludes, oexcludes, oprofiles = sparse.parseconfig(ui, raw)
718 oincludes, oexcludes, oprofiles = sparse.parseconfig(ui, raw)
727 includes, excludes, profiles = map(
719 includes, excludes, profiles = map(
728 set, (oincludes, oexcludes, oprofiles))
720 set, (oincludes, oexcludes, oprofiles))
729
721
730 # all active rules
722 # all active rules
731 aincludes, aexcludes, aprofiles = set(), set(), set()
723 aincludes, aexcludes, aprofiles = set(), set(), set()
732 for rev in revs:
724 for rev in revs:
733 rincludes, rexcludes, rprofiles = sparse.patternsforrev(repo, rev)
725 rincludes, rexcludes, rprofiles = sparse.patternsforrev(repo, rev)
734 aincludes.update(rincludes)
726 aincludes.update(rincludes)
735 aexcludes.update(rexcludes)
727 aexcludes.update(rexcludes)
736 aprofiles.update(rprofiles)
728 aprofiles.update(rprofiles)
737
729
738 # import rules on top; only take in rules that are not yet
730 # import rules on top; only take in rules that are not yet
739 # part of the active rules.
731 # part of the active rules.
740 changed = False
732 changed = False
741 for file in files:
733 for file in files:
742 with util.posixfile(util.expandpath(file)) as importfile:
734 with util.posixfile(util.expandpath(file)) as importfile:
743 iincludes, iexcludes, iprofiles = sparse.parseconfig(
735 iincludes, iexcludes, iprofiles = sparse.parseconfig(
744 ui, importfile.read())
736 ui, importfile.read())
745 oldsize = len(includes) + len(excludes) + len(profiles)
737 oldsize = len(includes) + len(excludes) + len(profiles)
746 includes.update(iincludes - aincludes)
738 includes.update(iincludes - aincludes)
747 excludes.update(iexcludes - aexcludes)
739 excludes.update(iexcludes - aexcludes)
748 profiles.update(set(iprofiles) - aprofiles)
740 profiles.update(set(iprofiles) - aprofiles)
749 if len(includes) + len(excludes) + len(profiles) > oldsize:
741 if len(includes) + len(excludes) + len(profiles) > oldsize:
750 changed = True
742 changed = True
751
743
752 profilecount = includecount = excludecount = 0
744 profilecount = includecount = excludecount = 0
753 fcounts = (0, 0, 0)
745 fcounts = (0, 0, 0)
754
746
755 if changed:
747 if changed:
756 profilecount = len(profiles - aprofiles)
748 profilecount = len(profiles - aprofiles)
757 includecount = len(includes - aincludes)
749 includecount = len(includes - aincludes)
758 excludecount = len(excludes - aexcludes)
750 excludecount = len(excludes - aexcludes)
759
751
760 oldstatus = repo.status()
752 oldstatus = repo.status()
761 oldsparsematch = repo.sparsematch()
753 oldsparsematch = repo.sparsematch()
762 sparse.writeconfig(repo, includes, excludes, profiles)
754 sparse.writeconfig(repo, includes, excludes, profiles)
763
755
764 try:
756 try:
765 fcounts = map(
757 fcounts = map(
766 len, _refresh(ui, repo, oldstatus, oldsparsematch, force))
758 len, _refresh(ui, repo, oldstatus, oldsparsematch, force))
767 except Exception:
759 except Exception:
768 sparse.writeconfig(repo, oincludes, oexcludes, oprofiles)
760 sparse.writeconfig(repo, oincludes, oexcludes, oprofiles)
769 raise
761 raise
770
762
771 _verbose_output(ui, opts, profilecount, includecount, excludecount,
763 _verbose_output(ui, opts, profilecount, includecount, excludecount,
772 *fcounts)
764 *fcounts)
773
765
774 def _clear(ui, repo, files, force=False):
766 def _clear(ui, repo, files, force=False):
775 with repo.wlock():
767 with repo.wlock():
776 raw = repo.vfs.tryread('sparse')
768 raw = repo.vfs.tryread('sparse')
777 includes, excludes, profiles = sparse.parseconfig(ui, raw)
769 includes, excludes, profiles = sparse.parseconfig(ui, raw)
778
770
779 if includes or excludes:
771 if includes or excludes:
780 oldstatus = repo.status()
772 oldstatus = repo.status()
781 oldsparsematch = repo.sparsematch()
773 oldsparsematch = repo.sparsematch()
782 sparse.writeconfig(repo, set(), set(), profiles)
774 sparse.writeconfig(repo, set(), set(), profiles)
783 _refresh(ui, repo, oldstatus, oldsparsematch, force)
775 _refresh(ui, repo, oldstatus, oldsparsematch, force)
784
776
785 def _refresh(ui, repo, origstatus, origsparsematch, force):
777 def _refresh(ui, repo, origstatus, origsparsematch, force):
786 """Refreshes which files are on disk by comparing the old status and
778 """Refreshes which files are on disk by comparing the old status and
787 sparsematch with the new sparsematch.
779 sparsematch with the new sparsematch.
788
780
789 Will raise an exception if a file with pending changes is being excluded
781 Will raise an exception if a file with pending changes is being excluded
790 or included (unless force=True).
782 or included (unless force=True).
791 """
783 """
792 modified, added, removed, deleted, unknown, ignored, clean = origstatus
784 modified, added, removed, deleted, unknown, ignored, clean = origstatus
793
785
794 # Verify there are no pending changes
786 # Verify there are no pending changes
795 pending = set()
787 pending = set()
796 pending.update(modified)
788 pending.update(modified)
797 pending.update(added)
789 pending.update(added)
798 pending.update(removed)
790 pending.update(removed)
799 sparsematch = repo.sparsematch()
791 sparsematch = repo.sparsematch()
800 abort = False
792 abort = False
801 for file in pending:
793 for file in pending:
802 if not sparsematch(file):
794 if not sparsematch(file):
803 ui.warn(_("pending changes to '%s'\n") % file)
795 ui.warn(_("pending changes to '%s'\n") % file)
804 abort = not force
796 abort = not force
805 if abort:
797 if abort:
806 raise error.Abort(_("could not update sparseness due to " +
798 raise error.Abort(_("could not update sparseness due to " +
807 "pending changes"))
799 "pending changes"))
808
800
809 # Calculate actions
801 # Calculate actions
810 dirstate = repo.dirstate
802 dirstate = repo.dirstate
811 ctx = repo['.']
803 ctx = repo['.']
812 added = []
804 added = []
813 lookup = []
805 lookup = []
814 dropped = []
806 dropped = []
815 mf = ctx.manifest()
807 mf = ctx.manifest()
816 files = set(mf)
808 files = set(mf)
817
809
818 actions = {}
810 actions = {}
819
811
820 for file in files:
812 for file in files:
821 old = origsparsematch(file)
813 old = origsparsematch(file)
822 new = sparsematch(file)
814 new = sparsematch(file)
823 # Add files that are newly included, or that don't exist in
815 # Add files that are newly included, or that don't exist in
824 # the dirstate yet.
816 # the dirstate yet.
825 if (new and not old) or (old and new and not file in dirstate):
817 if (new and not old) or (old and new and not file in dirstate):
826 fl = mf.flags(file)
818 fl = mf.flags(file)
827 if repo.wvfs.exists(file):
819 if repo.wvfs.exists(file):
828 actions[file] = ('e', (fl,), '')
820 actions[file] = ('e', (fl,), '')
829 lookup.append(file)
821 lookup.append(file)
830 else:
822 else:
831 actions[file] = ('g', (fl, False), '')
823 actions[file] = ('g', (fl, False), '')
832 added.append(file)
824 added.append(file)
833 # Drop files that are newly excluded, or that still exist in
825 # Drop files that are newly excluded, or that still exist in
834 # the dirstate.
826 # the dirstate.
835 elif (old and not new) or (not old and not new and file in dirstate):
827 elif (old and not new) or (not old and not new and file in dirstate):
836 dropped.append(file)
828 dropped.append(file)
837 if file not in pending:
829 if file not in pending:
838 actions[file] = ('r', [], '')
830 actions[file] = ('r', [], '')
839
831
840 # Verify there are no pending changes in newly included files
832 # Verify there are no pending changes in newly included files
841 abort = False
833 abort = False
842 for file in lookup:
834 for file in lookup:
843 ui.warn(_("pending changes to '%s'\n") % file)
835 ui.warn(_("pending changes to '%s'\n") % file)
844 abort = not force
836 abort = not force
845 if abort:
837 if abort:
846 raise error.Abort(_("cannot change sparseness due to " +
838 raise error.Abort(_("cannot change sparseness due to " +
847 "pending changes (delete the files or use --force " +
839 "pending changes (delete the files or use --force " +
848 "to bring them back dirty)"))
840 "to bring them back dirty)"))
849
841
850 # Check for files that were only in the dirstate.
842 # Check for files that were only in the dirstate.
851 for file, state in dirstate.iteritems():
843 for file, state in dirstate.iteritems():
852 if not file in files:
844 if not file in files:
853 old = origsparsematch(file)
845 old = origsparsematch(file)
854 new = sparsematch(file)
846 new = sparsematch(file)
855 if old and not new:
847 if old and not new:
856 dropped.append(file)
848 dropped.append(file)
857
849
858 # Apply changes to disk
850 # Apply changes to disk
859 typeactions = dict((m, []) for m in 'a f g am cd dc r dm dg m e k'.split())
851 typeactions = dict((m, []) for m in 'a f g am cd dc r dm dg m e k'.split())
860 for f, (m, args, msg) in actions.iteritems():
852 for f, (m, args, msg) in actions.iteritems():
861 if m not in typeactions:
853 if m not in typeactions:
862 typeactions[m] = []
854 typeactions[m] = []
863 typeactions[m].append((f, args, msg))
855 typeactions[m].append((f, args, msg))
864 mergemod.applyupdates(repo, typeactions, repo[None], repo['.'], False)
856 mergemod.applyupdates(repo, typeactions, repo[None], repo['.'], False)
865
857
866 # Fix dirstate
858 # Fix dirstate
867 for file in added:
859 for file in added:
868 dirstate.normal(file)
860 dirstate.normal(file)
869
861
870 for file in dropped:
862 for file in dropped:
871 dirstate.drop(file)
863 dirstate.drop(file)
872
864
873 for file in lookup:
865 for file in lookup:
874 # File exists on disk, and we're bringing it back in an unknown state.
866 # File exists on disk, and we're bringing it back in an unknown state.
875 dirstate.normallookup(file)
867 dirstate.normallookup(file)
876
868
877 return added, dropped, lookup
869 return added, dropped, lookup
878
870
879 def _verbose_output(ui, opts, profilecount, includecount, excludecount, added,
871 def _verbose_output(ui, opts, profilecount, includecount, excludecount, added,
880 dropped, lookup):
872 dropped, lookup):
881 """Produce --verbose and templatable output
873 """Produce --verbose and templatable output
882
874
883 This specifically enables -Tjson, providing machine-readable stats on how
875 This specifically enables -Tjson, providing machine-readable stats on how
884 the sparse profile changed.
876 the sparse profile changed.
885
877
886 """
878 """
887 with ui.formatter('sparse', opts) as fm:
879 with ui.formatter('sparse', opts) as fm:
888 fm.startitem()
880 fm.startitem()
889 fm.condwrite(ui.verbose, 'profiles_added', 'Profile # change: %d\n',
881 fm.condwrite(ui.verbose, 'profiles_added', 'Profile # change: %d\n',
890 profilecount)
882 profilecount)
891 fm.condwrite(ui.verbose, 'include_rules_added',
883 fm.condwrite(ui.verbose, 'include_rules_added',
892 'Include rule # change: %d\n', includecount)
884 'Include rule # change: %d\n', includecount)
893 fm.condwrite(ui.verbose, 'exclude_rules_added',
885 fm.condwrite(ui.verbose, 'exclude_rules_added',
894 'Exclude rule # change: %d\n', excludecount)
886 'Exclude rule # change: %d\n', excludecount)
895 # In 'plain' verbose mode, mergemod.applyupdates already outputs what
887 # In 'plain' verbose mode, mergemod.applyupdates already outputs what
896 # files are added or removed outside of the templating formatter
888 # files are added or removed outside of the templating formatter
897 # framework. No point in repeating ourselves in that case.
889 # framework. No point in repeating ourselves in that case.
898 if not fm.isplain():
890 if not fm.isplain():
899 fm.condwrite(ui.verbose, 'files_added', 'Files added: %d\n',
891 fm.condwrite(ui.verbose, 'files_added', 'Files added: %d\n',
900 added)
892 added)
901 fm.condwrite(ui.verbose, 'files_dropped', 'Files dropped: %d\n',
893 fm.condwrite(ui.verbose, 'files_dropped', 'Files dropped: %d\n',
902 dropped)
894 dropped)
903 fm.condwrite(ui.verbose, 'files_conflicting',
895 fm.condwrite(ui.verbose, 'files_conflicting',
904 'Files conflicting: %d\n', lookup)
896 'Files conflicting: %d\n', lookup)
905
897
906 class forceincludematcher(object):
898 class forceincludematcher(object):
907 """A matcher that returns true for any of the forced includes before testing
899 """A matcher that returns true for any of the forced includes before testing
908 against the actual matcher."""
900 against the actual matcher."""
909 def __init__(self, matcher, includes):
901 def __init__(self, matcher, includes):
910 self._matcher = matcher
902 self._matcher = matcher
911 self._includes = includes
903 self._includes = includes
912
904
913 def __call__(self, value):
905 def __call__(self, value):
914 return value in self._includes or self._matcher(value)
906 return value in self._includes or self._matcher(value)
915
907
916 def always(self):
908 def always(self):
917 return False
909 return False
918
910
919 def files(self):
911 def files(self):
920 return []
912 return []
921
913
922 def isexact(self):
914 def isexact(self):
923 return False
915 return False
924
916
925 def anypats(self):
917 def anypats(self):
926 return True
918 return True
927
919
928 def prefix(self):
920 def prefix(self):
929 return False
921 return False
930
922
931 def __repr__(self):
923 def __repr__(self):
932 return ('<forceincludematcher matcher=%r, includes=%r>' %
924 return ('<forceincludematcher matcher=%r, includes=%r>' %
933 (self._matcher, sorted(self._includes)))
925 (self._matcher, sorted(self._includes)))
934
926
935 class unionmatcher(object):
927 class unionmatcher(object):
936 """A matcher that is the union of several matchers."""
928 """A matcher that is the union of several matchers."""
937 def __init__(self, matchers):
929 def __init__(self, matchers):
938 self._matchers = matchers
930 self._matchers = matchers
939
931
940 def __call__(self, value):
932 def __call__(self, value):
941 for match in self._matchers:
933 for match in self._matchers:
942 if match(value):
934 if match(value):
943 return True
935 return True
944 return False
936 return False
945
937
946 def always(self):
938 def always(self):
947 return False
939 return False
948
940
949 def files(self):
941 def files(self):
950 return []
942 return []
951
943
952 def isexact(self):
944 def isexact(self):
953 return False
945 return False
954
946
955 def anypats(self):
947 def anypats(self):
956 return True
948 return True
957
949
958 def prefix(self):
950 def prefix(self):
959 return False
951 return False
960
952
961 def __repr__(self):
953 def __repr__(self):
962 return ('<unionmatcher matchers=%r>' % self._matchers)
954 return ('<unionmatcher matchers=%r>' % self._matchers)
963
955
964 class negatematcher(object):
956 class negatematcher(object):
965 def __init__(self, matcher):
957 def __init__(self, matcher):
966 self._matcher = matcher
958 self._matcher = matcher
967
959
968 def __call__(self, value):
960 def __call__(self, value):
969 return not self._matcher(value)
961 return not self._matcher(value)
970
962
971 def always(self):
963 def always(self):
972 return False
964 return False
973
965
974 def files(self):
966 def files(self):
975 return []
967 return []
976
968
977 def isexact(self):
969 def isexact(self):
978 return False
970 return False
979
971
980 def anypats(self):
972 def anypats(self):
981 return True
973 return True
982
974
983 def __repr__(self):
975 def __repr__(self):
984 return ('<negatematcher matcher=%r>' % self._matcher)
976 return ('<negatematcher matcher=%r>' % self._matcher)
985
986 def _hashmatcher(matcher):
987 sha1 = hashlib.sha1()
988 sha1.update(repr(matcher))
989 return sha1.hexdigest()
General Comments 0
You need to be logged in to leave comments. Login now