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