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