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