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