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