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