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