##// END OF EJS Templates
sparse: require [section] in sparse config files (BC)...
Gregory Szorc -
r33551:1d177973 default
parent child Browse files
Show More
@@ -1,337 +1,336 b''
1 1 # sparse.py - allow sparse checkouts of the working directory
2 2 #
3 3 # Copyright 2014 Facebook, Inc.
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 """allow sparse checkouts of the working directory (EXPERIMENTAL)
9 9
10 10 (This extension is not yet protected by backwards compatibility
11 11 guarantees. Any aspect may break in future releases until this
12 12 notice is removed.)
13 13
14 14 This extension allows the working directory to only consist of a
15 15 subset of files for the revision. This allows specific files or
16 16 directories to be explicitly included or excluded. Many repository
17 17 operations have performance proportional to the number of files in
18 18 the working directory. So only realizing a subset of files in the
19 19 working directory can improve performance.
20 20
21 21 Sparse Config Files
22 22 -------------------
23 23
24 24 The set of files that are part of a sparse checkout are defined by
25 25 a sparse config file. The file defines 3 things: includes (files to
26 26 include in the sparse checkout), excludes (files to exclude from the
27 27 sparse checkout), and profiles (links to other config files).
28 28
29 29 The file format is newline delimited. Empty lines and lines beginning
30 30 with ``#`` are ignored.
31 31
32 32 Lines beginning with ``%include `` denote another sparse config file
33 33 to include. e.g. ``%include tests.sparse``. The filename is relative
34 34 to the repository root.
35 35
36 36 The special lines ``[include]`` and ``[exclude]`` denote the section
37 37 for includes and excludes that follow, respectively. It is illegal to
38 have ``[include]`` after ``[exclude]``. If no sections are defined,
39 entries are assumed to be in the ``[include]`` section.
38 have ``[include]`` after ``[exclude]``.
40 39
41 40 Non-special lines resemble file patterns to be added to either includes
42 41 or excludes. The syntax of these lines is documented by :hg:`help patterns`.
43 42 Patterns are interpreted as ``glob:`` by default and match against the
44 43 root of the repository.
45 44
46 45 Exclusion patterns take precedence over inclusion patterns. So even
47 46 if a file is explicitly included, an ``[exclude]`` entry can remove it.
48 47
49 48 For example, say you have a repository with 3 directories, ``frontend/``,
50 49 ``backend/``, and ``tools/``. ``frontend/`` and ``backend/`` correspond
51 50 to different projects and it is uncommon for someone working on one
52 51 to need the files for the other. But ``tools/`` contains files shared
53 52 between both projects. Your sparse config files may resemble::
54 53
55 54 # frontend.sparse
56 55 frontend/**
57 56 tools/**
58 57
59 58 # backend.sparse
60 59 backend/**
61 60 tools/**
62 61
63 62 Say the backend grows in size. Or there's a directory with thousands
64 63 of files you wish to exclude. You can modify the profile to exclude
65 64 certain files::
66 65
67 66 [include]
68 67 backend/**
69 68 tools/**
70 69
71 70 [exclude]
72 71 tools/tests/**
73 72 """
74 73
75 74 from __future__ import absolute_import
76 75
77 76 from mercurial.i18n import _
78 77 from mercurial import (
79 78 cmdutil,
80 79 commands,
81 80 dirstate,
82 81 error,
83 82 extensions,
84 83 hg,
85 84 match as matchmod,
86 85 registrar,
87 86 sparse,
88 87 util,
89 88 )
90 89
91 90 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
92 91 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
93 92 # be specifying the version(s) of Mercurial they are tested with, or
94 93 # leave the attribute unspecified.
95 94 testedwith = 'ships-with-hg-core'
96 95
97 96 cmdtable = {}
98 97 command = registrar.command(cmdtable)
99 98
100 99 def extsetup(ui):
101 100 sparse.enabled = True
102 101
103 102 _setupclone(ui)
104 103 _setuplog(ui)
105 104 _setupadd(ui)
106 105 _setupdirstate(ui)
107 106
108 107 def replacefilecache(cls, propname, replacement):
109 108 """Replace a filecache property with a new class. This allows changing the
110 109 cache invalidation condition."""
111 110 origcls = cls
112 111 assert callable(replacement)
113 112 while cls is not object:
114 113 if propname in cls.__dict__:
115 114 orig = cls.__dict__[propname]
116 115 setattr(cls, propname, replacement(orig))
117 116 break
118 117 cls = cls.__bases__[0]
119 118
120 119 if cls is object:
121 120 raise AttributeError(_("type '%s' has no property '%s'") % (origcls,
122 121 propname))
123 122
124 123 def _setuplog(ui):
125 124 entry = commands.table['^log|history']
126 125 entry[1].append(('', 'sparse', None,
127 126 "limit to changesets affecting the sparse checkout"))
128 127
129 128 def _logrevs(orig, repo, opts):
130 129 revs = orig(repo, opts)
131 130 if opts.get('sparse'):
132 131 sparsematch = sparse.matcher(repo)
133 132 def ctxmatch(rev):
134 133 ctx = repo[rev]
135 134 return any(f for f in ctx.files() if sparsematch(f))
136 135 revs = revs.filter(ctxmatch)
137 136 return revs
138 137 extensions.wrapfunction(cmdutil, '_logrevs', _logrevs)
139 138
140 139 def _clonesparsecmd(orig, ui, repo, *args, **opts):
141 140 include_pat = opts.get('include')
142 141 exclude_pat = opts.get('exclude')
143 142 enableprofile_pat = opts.get('enable_profile')
144 143 include = exclude = enableprofile = False
145 144 if include_pat:
146 145 pat = include_pat
147 146 include = True
148 147 if exclude_pat:
149 148 pat = exclude_pat
150 149 exclude = True
151 150 if enableprofile_pat:
152 151 pat = enableprofile_pat
153 152 enableprofile = True
154 153 if sum([include, exclude, enableprofile]) > 1:
155 154 raise error.Abort(_("too many flags specified."))
156 155 if include or exclude or enableprofile:
157 156 def clonesparse(orig, self, node, overwrite, *args, **kwargs):
158 157 sparse.updateconfig(self.unfiltered(), pat, {}, include=include,
159 158 exclude=exclude, enableprofile=enableprofile)
160 159 return orig(self, node, overwrite, *args, **kwargs)
161 160 extensions.wrapfunction(hg, 'updaterepo', clonesparse)
162 161 return orig(ui, repo, *args, **opts)
163 162
164 163 def _setupclone(ui):
165 164 entry = commands.table['^clone']
166 165 entry[1].append(('', 'enable-profile', [],
167 166 'enable a sparse profile'))
168 167 entry[1].append(('', 'include', [],
169 168 'include sparse pattern'))
170 169 entry[1].append(('', 'exclude', [],
171 170 'exclude sparse pattern'))
172 171 extensions.wrapcommand(commands.table, 'clone', _clonesparsecmd)
173 172
174 173 def _setupadd(ui):
175 174 entry = commands.table['^add']
176 175 entry[1].append(('s', 'sparse', None,
177 176 'also include directories of added files in sparse config'))
178 177
179 178 def _add(orig, ui, repo, *pats, **opts):
180 179 if opts.get('sparse'):
181 180 dirs = set()
182 181 for pat in pats:
183 182 dirname, basename = util.split(pat)
184 183 dirs.add(dirname)
185 184 sparse.updateconfig(repo, list(dirs), opts, include=True)
186 185 return orig(ui, repo, *pats, **opts)
187 186
188 187 extensions.wrapcommand(commands.table, 'add', _add)
189 188
190 189 def _setupdirstate(ui):
191 190 """Modify the dirstate to prevent stat'ing excluded files,
192 191 and to prevent modifications to files outside the checkout.
193 192 """
194 193
195 194 def walk(orig, self, match, subrepos, unknown, ignored, full=True):
196 195 match = matchmod.intersectmatchers(match, self._sparsematcher)
197 196 return orig(self, match, subrepos, unknown, ignored, full)
198 197
199 198 extensions.wrapfunction(dirstate.dirstate, 'walk', walk)
200 199
201 200 # dirstate.rebuild should not add non-matching files
202 201 def _rebuild(orig, self, parent, allfiles, changedfiles=None):
203 202 matcher = self._sparsematcher
204 203 if not matcher.always():
205 204 allfiles = allfiles.matches(matcher)
206 205 if changedfiles:
207 206 changedfiles = [f for f in changedfiles if matcher(f)]
208 207
209 208 if changedfiles is not None:
210 209 # In _rebuild, these files will be deleted from the dirstate
211 210 # when they are not found to be in allfiles
212 211 dirstatefilestoremove = set(f for f in self if not matcher(f))
213 212 changedfiles = dirstatefilestoremove.union(changedfiles)
214 213
215 214 return orig(self, parent, allfiles, changedfiles)
216 215 extensions.wrapfunction(dirstate.dirstate, 'rebuild', _rebuild)
217 216
218 217 # Prevent adding files that are outside the sparse checkout
219 218 editfuncs = ['normal', 'add', 'normallookup', 'copy', 'remove', 'merge']
220 219 hint = _('include file with `hg debugsparse --include <pattern>` or use ' +
221 220 '`hg add -s <file>` to include file directory while adding')
222 221 for func in editfuncs:
223 222 def _wrapper(orig, self, *args):
224 223 sparsematch = self._sparsematcher
225 224 if not sparsematch.always():
226 225 for f in args:
227 226 if (f is not None and not sparsematch(f) and
228 227 f not in self):
229 228 raise error.Abort(_("cannot add '%s' - it is outside "
230 229 "the sparse checkout") % f,
231 230 hint=hint)
232 231 return orig(self, *args)
233 232 extensions.wrapfunction(dirstate.dirstate, func, _wrapper)
234 233
235 234 @command('^debugsparse', [
236 235 ('I', 'include', False, _('include files in the sparse checkout')),
237 236 ('X', 'exclude', False, _('exclude files in the sparse checkout')),
238 237 ('d', 'delete', False, _('delete an include/exclude rule')),
239 238 ('f', 'force', False, _('allow changing rules even with pending changes')),
240 239 ('', 'enable-profile', False, _('enables the specified profile')),
241 240 ('', 'disable-profile', False, _('disables the specified profile')),
242 241 ('', 'import-rules', False, _('imports rules from a file')),
243 242 ('', 'clear-rules', False, _('clears local include/exclude rules')),
244 243 ('', 'refresh', False, _('updates the working after sparseness changes')),
245 244 ('', 'reset', False, _('makes the repo full again')),
246 245 ] + commands.templateopts,
247 246 _('[--OPTION] PATTERN...'))
248 247 def debugsparse(ui, repo, *pats, **opts):
249 248 """make the current checkout sparse, or edit the existing checkout
250 249
251 250 The sparse command is used to make the current checkout sparse.
252 251 This means files that don't meet the sparse condition will not be
253 252 written to disk, or show up in any working copy operations. It does
254 253 not affect files in history in any way.
255 254
256 255 Passing no arguments prints the currently applied sparse rules.
257 256
258 257 --include and --exclude are used to add and remove files from the sparse
259 258 checkout. The effects of adding an include or exclude rule are applied
260 259 immediately. If applying the new rule would cause a file with pending
261 260 changes to be added or removed, the command will fail. Pass --force to
262 261 force a rule change even with pending changes (the changes on disk will
263 262 be preserved).
264 263
265 264 --delete removes an existing include/exclude rule. The effects are
266 265 immediate.
267 266
268 267 --refresh refreshes the files on disk based on the sparse rules. This is
269 268 only necessary if .hg/sparse was changed by hand.
270 269
271 270 --enable-profile and --disable-profile accept a path to a .hgsparse file.
272 271 This allows defining sparse checkouts and tracking them inside the
273 272 repository. This is useful for defining commonly used sparse checkouts for
274 273 many people to use. As the profile definition changes over time, the sparse
275 274 checkout will automatically be updated appropriately, depending on which
276 275 changeset is checked out. Changes to .hgsparse are not applied until they
277 276 have been committed.
278 277
279 278 --import-rules accepts a path to a file containing rules in the .hgsparse
280 279 format, allowing you to add --include, --exclude and --enable-profile rules
281 280 in bulk. Like the --include, --exclude and --enable-profile switches, the
282 281 changes are applied immediately.
283 282
284 283 --clear-rules removes all local include and exclude rules, while leaving
285 284 any enabled profiles in place.
286 285
287 286 Returns 0 if editing the sparse checkout succeeds.
288 287 """
289 288 include = opts.get('include')
290 289 exclude = opts.get('exclude')
291 290 force = opts.get('force')
292 291 enableprofile = opts.get('enable_profile')
293 292 disableprofile = opts.get('disable_profile')
294 293 importrules = opts.get('import_rules')
295 294 clearrules = opts.get('clear_rules')
296 295 delete = opts.get('delete')
297 296 refresh = opts.get('refresh')
298 297 reset = opts.get('reset')
299 298 count = sum([include, exclude, enableprofile, disableprofile, delete,
300 299 importrules, refresh, clearrules, reset])
301 300 if count > 1:
302 301 raise error.Abort(_("too many flags specified"))
303 302
304 303 if count == 0:
305 304 if repo.vfs.exists('sparse'):
306 305 ui.status(repo.vfs.read("sparse") + "\n")
307 306 temporaryincludes = sparse.readtemporaryincludes(repo)
308 307 if temporaryincludes:
309 308 ui.status(_("Temporarily Included Files (for merge/rebase):\n"))
310 309 ui.status(("\n".join(temporaryincludes) + "\n"))
311 310 else:
312 311 ui.status(_('repo is not sparse\n'))
313 312 return
314 313
315 314 if include or exclude or delete or reset or enableprofile or disableprofile:
316 315 sparse.updateconfig(repo, pats, opts, include=include, exclude=exclude,
317 316 reset=reset, delete=delete,
318 317 enableprofile=enableprofile,
319 318 disableprofile=disableprofile, force=force)
320 319
321 320 if importrules:
322 321 sparse.importfromfiles(repo, opts, pats, force=force)
323 322
324 323 if clearrules:
325 324 sparse.clearrules(repo, force=force)
326 325
327 326 if refresh:
328 327 try:
329 328 wlock = repo.wlock()
330 329 fcounts = map(
331 330 len,
332 331 sparse.refreshwdir(repo, repo.status(), sparse.matcher(repo),
333 332 force=force))
334 333 sparse.printchanges(ui, opts, added=fcounts[0], dropped=fcounts[1],
335 334 conflicting=fcounts[2])
336 335 finally:
337 336 wlock.release()
@@ -1,676 +1,687 b''
1 1 # sparse.py - functionality for sparse checkouts
2 2 #
3 3 # Copyright 2014 Facebook, Inc.
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import collections
11 11 import hashlib
12 12 import os
13 13
14 14 from .i18n import _
15 15 from .node import nullid
16 16 from . import (
17 17 error,
18 18 match as matchmod,
19 19 merge as mergemod,
20 20 pycompat,
21 21 util,
22 22 )
23 23
24 24 # Whether sparse features are enabled. This variable is intended to be
25 25 # temporary to facilitate porting sparse to core. It should eventually be
26 26 # a per-repo option, possibly a repo requirement.
27 27 enabled = False
28 28
29 29 def parseconfig(ui, raw):
30 30 """Parse sparse config file content.
31 31
32 32 Returns a tuple of includes, excludes, and profiles.
33 33 """
34 34 includes = set()
35 35 excludes = set()
36 current = includes
37 36 profiles = set()
37 current = None
38 havesection = False
39
38 40 for line in raw.split('\n'):
39 41 line = line.strip()
40 42 if not line or line.startswith('#'):
41 43 # empty or comment line, skip
42 44 continue
43 45 elif line.startswith('%include '):
44 46 line = line[9:].strip()
45 47 if line:
46 48 profiles.add(line)
47 49 elif line == '[include]':
48 if current != includes:
50 if havesection and current != includes:
49 51 # TODO pass filename into this API so we can report it.
50 52 raise error.Abort(_('sparse config cannot have includes ' +
51 53 'after excludes'))
54 havesection = True
55 current = includes
52 56 continue
53 57 elif line == '[exclude]':
58 havesection = True
54 59 current = excludes
55 60 elif line:
61 if current is None:
62 raise error.Abort(_('sparse config entry outside of '
63 'section: %s') % line,
64 hint=_('add an [include] or [exclude] line '
65 'to declare the entry type'))
66
56 67 if line.strip().startswith('/'):
57 68 ui.warn(_('warning: sparse profile cannot use' +
58 69 ' paths starting with /, ignoring %s\n') % line)
59 70 continue
60 71 current.add(line)
61 72
62 73 return includes, excludes, profiles
63 74
64 75 # Exists as separate function to facilitate monkeypatching.
65 76 def readprofile(repo, profile, changeid):
66 77 """Resolve the raw content of a sparse profile file."""
67 78 # TODO add some kind of cache here because this incurs a manifest
68 79 # resolve and can be slow.
69 80 return repo.filectx(profile, changeid=changeid).data()
70 81
71 82 def patternsforrev(repo, rev):
72 83 """Obtain sparse checkout patterns for the given rev.
73 84
74 85 Returns a tuple of iterables representing includes, excludes, and
75 86 patterns.
76 87 """
77 88 # Feature isn't enabled. No-op.
78 89 if not enabled:
79 90 return set(), set(), set()
80 91
81 92 raw = repo.vfs.tryread('sparse')
82 93 if not raw:
83 94 return set(), set(), set()
84 95
85 96 if rev is None:
86 97 raise error.Abort(_('cannot parse sparse patterns from working '
87 98 'directory'))
88 99
89 100 includes, excludes, profiles = parseconfig(repo.ui, raw)
90 101 ctx = repo[rev]
91 102
92 103 if profiles:
93 104 visited = set()
94 105 while profiles:
95 106 profile = profiles.pop()
96 107 if profile in visited:
97 108 continue
98 109
99 110 visited.add(profile)
100 111
101 112 try:
102 113 raw = readprofile(repo, profile, rev)
103 114 except error.ManifestLookupError:
104 115 msg = (
105 116 "warning: sparse profile '%s' not found "
106 117 "in rev %s - ignoring it\n" % (profile, ctx))
107 118 # experimental config: sparse.missingwarning
108 119 if repo.ui.configbool(
109 120 'sparse', 'missingwarning'):
110 121 repo.ui.warn(msg)
111 122 else:
112 123 repo.ui.debug(msg)
113 124 continue
114 125
115 126 pincludes, pexcludes, subprofs = parseconfig(repo.ui, raw)
116 127 includes.update(pincludes)
117 128 excludes.update(pexcludes)
118 129 profiles.update(subprofs)
119 130
120 131 profiles = visited
121 132
122 133 if includes:
123 134 includes.add('.hg*')
124 135
125 136 return includes, excludes, profiles
126 137
127 138 def activeconfig(repo):
128 139 """Determine the active sparse config rules.
129 140
130 141 Rules are constructed by reading the current sparse config and bringing in
131 142 referenced profiles from parents of the working directory.
132 143 """
133 144 revs = [repo.changelog.rev(node) for node in
134 145 repo.dirstate.parents() if node != nullid]
135 146
136 147 allincludes = set()
137 148 allexcludes = set()
138 149 allprofiles = set()
139 150
140 151 for rev in revs:
141 152 includes, excludes, profiles = patternsforrev(repo, rev)
142 153 allincludes |= includes
143 154 allexcludes |= excludes
144 155 allprofiles |= profiles
145 156
146 157 return allincludes, allexcludes, allprofiles
147 158
148 159 def configsignature(repo, includetemp=True):
149 160 """Obtain the signature string for the current sparse configuration.
150 161
151 162 This is used to construct a cache key for matchers.
152 163 """
153 164 cache = repo._sparsesignaturecache
154 165
155 166 signature = cache.get('signature')
156 167
157 168 if includetemp:
158 169 tempsignature = cache.get('tempsignature')
159 170 else:
160 171 tempsignature = '0'
161 172
162 173 if signature is None or (includetemp and tempsignature is None):
163 174 signature = hashlib.sha1(repo.vfs.tryread('sparse')).hexdigest()
164 175 cache['signature'] = signature
165 176
166 177 if includetemp:
167 178 raw = repo.vfs.tryread('tempsparse')
168 179 tempsignature = hashlib.sha1(raw).hexdigest()
169 180 cache['tempsignature'] = tempsignature
170 181
171 182 return '%s %s' % (signature, tempsignature)
172 183
173 184 def writeconfig(repo, includes, excludes, profiles):
174 185 """Write the sparse config file given a sparse configuration."""
175 186 with repo.vfs('sparse', 'wb') as fh:
176 187 for p in sorted(profiles):
177 188 fh.write('%%include %s\n' % p)
178 189
179 190 if includes:
180 191 fh.write('[include]\n')
181 192 for i in sorted(includes):
182 193 fh.write(i)
183 194 fh.write('\n')
184 195
185 196 if excludes:
186 197 fh.write('[exclude]\n')
187 198 for e in sorted(excludes):
188 199 fh.write(e)
189 200 fh.write('\n')
190 201
191 202 repo._sparsesignaturecache.clear()
192 203
193 204 def readtemporaryincludes(repo):
194 205 raw = repo.vfs.tryread('tempsparse')
195 206 if not raw:
196 207 return set()
197 208
198 209 return set(raw.split('\n'))
199 210
200 211 def writetemporaryincludes(repo, includes):
201 212 repo.vfs.write('tempsparse', '\n'.join(sorted(includes)))
202 213 repo._sparsesignaturecache.clear()
203 214
204 215 def addtemporaryincludes(repo, additional):
205 216 includes = readtemporaryincludes(repo)
206 217 for i in additional:
207 218 includes.add(i)
208 219 writetemporaryincludes(repo, includes)
209 220
210 221 def prunetemporaryincludes(repo):
211 222 if not enabled or not repo.vfs.exists('tempsparse'):
212 223 return
213 224
214 225 s = repo.status()
215 226 if s.modified or s.added or s.removed or s.deleted:
216 227 # Still have pending changes. Don't bother trying to prune.
217 228 return
218 229
219 230 sparsematch = matcher(repo, includetemp=False)
220 231 dirstate = repo.dirstate
221 232 actions = []
222 233 dropped = []
223 234 tempincludes = readtemporaryincludes(repo)
224 235 for file in tempincludes:
225 236 if file in dirstate and not sparsematch(file):
226 237 message = _('dropping temporarily included sparse files')
227 238 actions.append((file, None, message))
228 239 dropped.append(file)
229 240
230 241 typeactions = collections.defaultdict(list)
231 242 typeactions['r'] = actions
232 243 mergemod.applyupdates(repo, typeactions, repo[None], repo['.'], False)
233 244
234 245 # Fix dirstate
235 246 for file in dropped:
236 247 dirstate.drop(file)
237 248
238 249 repo.vfs.unlink('tempsparse')
239 250 repo._sparsesignaturecache.clear()
240 251 msg = _('cleaned up %d temporarily added file(s) from the '
241 252 'sparse checkout\n')
242 253 repo.ui.status(msg % len(tempincludes))
243 254
244 255 def forceincludematcher(matcher, includes):
245 256 """Returns a matcher that returns true for any of the forced includes
246 257 before testing against the actual matcher."""
247 258 kindpats = [('path', include, '') for include in includes]
248 259 includematcher = matchmod.includematcher('', '', kindpats)
249 260 return matchmod.unionmatcher([includematcher, matcher])
250 261
251 262 def matcher(repo, revs=None, includetemp=True):
252 263 """Obtain a matcher for sparse working directories for the given revs.
253 264
254 265 If multiple revisions are specified, the matcher is the union of all
255 266 revs.
256 267
257 268 ``includetemp`` indicates whether to use the temporary sparse profile.
258 269 """
259 270 # If sparse isn't enabled, sparse matcher matches everything.
260 271 if not enabled:
261 272 return matchmod.always(repo.root, '')
262 273
263 274 if not revs or revs == [None]:
264 275 revs = [repo.changelog.rev(node)
265 276 for node in repo.dirstate.parents() if node != nullid]
266 277
267 278 signature = configsignature(repo, includetemp=includetemp)
268 279
269 280 key = '%s %s' % (signature, ' '.join(map(pycompat.bytestr, revs)))
270 281
271 282 result = repo._sparsematchercache.get(key)
272 283 if result:
273 284 return result
274 285
275 286 matchers = []
276 287 for rev in revs:
277 288 try:
278 289 includes, excludes, profiles = patternsforrev(repo, rev)
279 290
280 291 if includes or excludes:
281 292 # Explicitly include subdirectories of includes so
282 293 # status will walk them down to the actual include.
283 294 subdirs = set()
284 295 for include in includes:
285 296 # TODO consider using posix path functions here so Windows
286 297 # \ directory separators don't come into play.
287 298 dirname = os.path.dirname(include)
288 299 # basename is used to avoid issues with absolute
289 300 # paths (which on Windows can include the drive).
290 301 while os.path.basename(dirname):
291 302 subdirs.add(dirname)
292 303 dirname = os.path.dirname(dirname)
293 304
294 305 matcher = matchmod.match(repo.root, '', [],
295 306 include=includes, exclude=excludes,
296 307 default='relpath')
297 308 if subdirs:
298 309 matcher = forceincludematcher(matcher, subdirs)
299 310 matchers.append(matcher)
300 311 except IOError:
301 312 pass
302 313
303 314 if not matchers:
304 315 result = matchmod.always(repo.root, '')
305 316 elif len(matchers) == 1:
306 317 result = matchers[0]
307 318 else:
308 319 result = matchmod.unionmatcher(matchers)
309 320
310 321 if includetemp:
311 322 tempincludes = readtemporaryincludes(repo)
312 323 result = forceincludematcher(result, tempincludes)
313 324
314 325 repo._sparsematchercache[key] = result
315 326
316 327 return result
317 328
318 329 def filterupdatesactions(repo, wctx, mctx, branchmerge, actions):
319 330 """Filter updates to only lay out files that match the sparse rules."""
320 331 if not enabled:
321 332 return actions
322 333
323 334 oldrevs = [pctx.rev() for pctx in wctx.parents()]
324 335 oldsparsematch = matcher(repo, oldrevs)
325 336
326 337 if oldsparsematch.always():
327 338 return actions
328 339
329 340 files = set()
330 341 prunedactions = {}
331 342
332 343 if branchmerge:
333 344 # If we're merging, use the wctx filter, since we're merging into
334 345 # the wctx.
335 346 sparsematch = matcher(repo, [wctx.parents()[0].rev()])
336 347 else:
337 348 # If we're updating, use the target context's filter, since we're
338 349 # moving to the target context.
339 350 sparsematch = matcher(repo, [mctx.rev()])
340 351
341 352 temporaryfiles = []
342 353 for file, action in actions.iteritems():
343 354 type, args, msg = action
344 355 files.add(file)
345 356 if sparsematch(file):
346 357 prunedactions[file] = action
347 358 elif type == 'm':
348 359 temporaryfiles.append(file)
349 360 prunedactions[file] = action
350 361 elif branchmerge:
351 362 if type != 'k':
352 363 temporaryfiles.append(file)
353 364 prunedactions[file] = action
354 365 elif type == 'f':
355 366 prunedactions[file] = action
356 367 elif file in wctx:
357 368 prunedactions[file] = ('r', args, msg)
358 369
359 370 if len(temporaryfiles) > 0:
360 371 repo.ui.status(_('temporarily included %d file(s) in the sparse '
361 372 'checkout for merging\n') % len(temporaryfiles))
362 373 addtemporaryincludes(repo, temporaryfiles)
363 374
364 375 # Add the new files to the working copy so they can be merged, etc
365 376 actions = []
366 377 message = 'temporarily adding to sparse checkout'
367 378 wctxmanifest = repo[None].manifest()
368 379 for file in temporaryfiles:
369 380 if file in wctxmanifest:
370 381 fctx = repo[None][file]
371 382 actions.append((file, (fctx.flags(), False), message))
372 383
373 384 typeactions = collections.defaultdict(list)
374 385 typeactions['g'] = actions
375 386 mergemod.applyupdates(repo, typeactions, repo[None], repo['.'],
376 387 False)
377 388
378 389 dirstate = repo.dirstate
379 390 for file, flags, msg in actions:
380 391 dirstate.normal(file)
381 392
382 393 profiles = activeconfig(repo)[2]
383 394 changedprofiles = profiles & files
384 395 # If an active profile changed during the update, refresh the checkout.
385 396 # Don't do this during a branch merge, since all incoming changes should
386 397 # have been handled by the temporary includes above.
387 398 if changedprofiles and not branchmerge:
388 399 mf = mctx.manifest()
389 400 for file in mf:
390 401 old = oldsparsematch(file)
391 402 new = sparsematch(file)
392 403 if not old and new:
393 404 flags = mf.flags(file)
394 405 prunedactions[file] = ('g', (flags, False), '')
395 406 elif old and not new:
396 407 prunedactions[file] = ('r', [], '')
397 408
398 409 return prunedactions
399 410
400 411 def refreshwdir(repo, origstatus, origsparsematch, force=False):
401 412 """Refreshes working directory by taking sparse config into account.
402 413
403 414 The old status and sparse matcher is compared against the current sparse
404 415 matcher.
405 416
406 417 Will abort if a file with pending changes is being excluded or included
407 418 unless ``force`` is True.
408 419 """
409 420 # Verify there are no pending changes
410 421 pending = set()
411 422 pending.update(origstatus.modified)
412 423 pending.update(origstatus.added)
413 424 pending.update(origstatus.removed)
414 425 sparsematch = matcher(repo)
415 426 abort = False
416 427
417 428 for f in pending:
418 429 if not sparsematch(f):
419 430 repo.ui.warn(_("pending changes to '%s'\n") % f)
420 431 abort = not force
421 432
422 433 if abort:
423 434 raise error.Abort(_('could not update sparseness due to pending '
424 435 'changes'))
425 436
426 437 # Calculate actions
427 438 dirstate = repo.dirstate
428 439 ctx = repo['.']
429 440 added = []
430 441 lookup = []
431 442 dropped = []
432 443 mf = ctx.manifest()
433 444 files = set(mf)
434 445
435 446 actions = {}
436 447
437 448 for file in files:
438 449 old = origsparsematch(file)
439 450 new = sparsematch(file)
440 451 # Add files that are newly included, or that don't exist in
441 452 # the dirstate yet.
442 453 if (new and not old) or (old and new and not file in dirstate):
443 454 fl = mf.flags(file)
444 455 if repo.wvfs.exists(file):
445 456 actions[file] = ('e', (fl,), '')
446 457 lookup.append(file)
447 458 else:
448 459 actions[file] = ('g', (fl, False), '')
449 460 added.append(file)
450 461 # Drop files that are newly excluded, or that still exist in
451 462 # the dirstate.
452 463 elif (old and not new) or (not old and not new and file in dirstate):
453 464 dropped.append(file)
454 465 if file not in pending:
455 466 actions[file] = ('r', [], '')
456 467
457 468 # Verify there are no pending changes in newly included files
458 469 abort = False
459 470 for file in lookup:
460 471 repo.ui.warn(_("pending changes to '%s'\n") % file)
461 472 abort = not force
462 473 if abort:
463 474 raise error.Abort(_('cannot change sparseness due to pending '
464 475 'changes (delete the files or use '
465 476 '--force to bring them back dirty)'))
466 477
467 478 # Check for files that were only in the dirstate.
468 479 for file, state in dirstate.iteritems():
469 480 if not file in files:
470 481 old = origsparsematch(file)
471 482 new = sparsematch(file)
472 483 if old and not new:
473 484 dropped.append(file)
474 485
475 486 # Apply changes to disk
476 487 typeactions = dict((m, []) for m in 'a f g am cd dc r dm dg m e k'.split())
477 488 for f, (m, args, msg) in actions.iteritems():
478 489 if m not in typeactions:
479 490 typeactions[m] = []
480 491 typeactions[m].append((f, args, msg))
481 492
482 493 mergemod.applyupdates(repo, typeactions, repo[None], repo['.'], False)
483 494
484 495 # Fix dirstate
485 496 for file in added:
486 497 dirstate.normal(file)
487 498
488 499 for file in dropped:
489 500 dirstate.drop(file)
490 501
491 502 for file in lookup:
492 503 # File exists on disk, and we're bringing it back in an unknown state.
493 504 dirstate.normallookup(file)
494 505
495 506 return added, dropped, lookup
496 507
497 508 def aftercommit(repo, node):
498 509 """Perform actions after a working directory commit."""
499 510 # This function is called unconditionally, even if sparse isn't
500 511 # enabled.
501 512 ctx = repo[node]
502 513
503 514 profiles = patternsforrev(repo, ctx.rev())[2]
504 515
505 516 # profiles will only have data if sparse is enabled.
506 517 if profiles & set(ctx.files()):
507 518 origstatus = repo.status()
508 519 origsparsematch = matcher(repo)
509 520 refreshwdir(repo, origstatus, origsparsematch, force=True)
510 521
511 522 prunetemporaryincludes(repo)
512 523
513 524 def clearrules(repo, force=False):
514 525 """Clears include/exclude rules from the sparse config.
515 526
516 527 The remaining sparse config only has profiles, if defined. The working
517 528 directory is refreshed, as needed.
518 529 """
519 530 with repo.wlock():
520 531 raw = repo.vfs.tryread('sparse')
521 532 includes, excludes, profiles = parseconfig(repo.ui, raw)
522 533
523 534 if not includes and not excludes:
524 535 return
525 536
526 537 oldstatus = repo.status()
527 538 oldmatch = matcher(repo)
528 539 writeconfig(repo, set(), set(), profiles)
529 540 refreshwdir(repo, oldstatus, oldmatch, force=force)
530 541
531 542 def importfromfiles(repo, opts, paths, force=False):
532 543 """Import sparse config rules from files.
533 544
534 545 The updated sparse config is written out and the working directory
535 546 is refreshed, as needed.
536 547 """
537 548 with repo.wlock():
538 549 # read current configuration
539 550 raw = repo.vfs.tryread('sparse')
540 551 oincludes, oexcludes, oprofiles = parseconfig(repo.ui, raw)
541 552 includes, excludes, profiles = map(
542 553 set, (oincludes, oexcludes, oprofiles))
543 554
544 555 aincludes, aexcludes, aprofiles = activeconfig(repo)
545 556
546 557 # Import rules on top; only take in rules that are not yet
547 558 # part of the active rules.
548 559 changed = False
549 560 for p in paths:
550 561 with util.posixfile(util.expandpath(p)) as fh:
551 562 raw = fh.read()
552 563
553 564 iincludes, iexcludes, iprofiles = parseconfig(repo.ui, raw)
554 565 oldsize = len(includes) + len(excludes) + len(profiles)
555 566 includes.update(iincludes - aincludes)
556 567 excludes.update(iexcludes - aexcludes)
557 568 profiles.update(iprofiles - aprofiles)
558 569 if len(includes) + len(excludes) + len(profiles) > oldsize:
559 570 changed = True
560 571
561 572 profilecount = includecount = excludecount = 0
562 573 fcounts = (0, 0, 0)
563 574
564 575 if changed:
565 576 profilecount = len(profiles - aprofiles)
566 577 includecount = len(includes - aincludes)
567 578 excludecount = len(excludes - aexcludes)
568 579
569 580 oldstatus = repo.status()
570 581 oldsparsematch = matcher(repo)
571 582
572 583 # TODO remove this try..except once the matcher integrates better
573 584 # with dirstate. We currently have to write the updated config
574 585 # because that will invalidate the matcher cache and force a
575 586 # re-read. We ideally want to update the cached matcher on the
576 587 # repo instance then flush the new config to disk once wdir is
577 588 # updated. But this requires massive rework to matcher() and its
578 589 # consumers.
579 590 writeconfig(repo, includes, excludes, profiles)
580 591
581 592 try:
582 593 fcounts = map(
583 594 len,
584 595 refreshwdir(repo, oldstatus, oldsparsematch, force=force))
585 596 except Exception:
586 597 writeconfig(repo, oincludes, oexcludes, oprofiles)
587 598 raise
588 599
589 600 printchanges(repo.ui, opts, profilecount, includecount, excludecount,
590 601 *fcounts)
591 602
592 603 def updateconfig(repo, pats, opts, include=False, exclude=False, reset=False,
593 604 delete=False, enableprofile=False, disableprofile=False,
594 605 force=False):
595 606 """Perform a sparse config update.
596 607
597 608 Only one of the actions may be performed.
598 609
599 610 The new config is written out and a working directory refresh is performed.
600 611 """
601 612 with repo.wlock():
602 613 oldmatcher = matcher(repo)
603 614
604 615 raw = repo.vfs.tryread('sparse')
605 616 oldinclude, oldexclude, oldprofiles = parseconfig(repo.ui, raw)
606 617
607 618 if reset:
608 619 newinclude = set()
609 620 newexclude = set()
610 621 newprofiles = set()
611 622 else:
612 623 newinclude = set(oldinclude)
613 624 newexclude = set(oldexclude)
614 625 newprofiles = set(oldprofiles)
615 626
616 627 oldstatus = repo.status()
617 628
618 629 if any(pat.startswith('/') for pat in pats):
619 630 repo.ui.warn(_('warning: paths cannot start with /, ignoring: %s\n')
620 631 % ([pat for pat in pats if pat.startswith('/')]))
621 632 elif include:
622 633 newinclude.update(pats)
623 634 elif exclude:
624 635 newexclude.update(pats)
625 636 elif enableprofile:
626 637 newprofiles.update(pats)
627 638 elif disableprofile:
628 639 newprofiles.difference_update(pats)
629 640 elif delete:
630 641 newinclude.difference_update(pats)
631 642 newexclude.difference_update(pats)
632 643
633 644 profilecount = (len(newprofiles - oldprofiles) -
634 645 len(oldprofiles - newprofiles))
635 646 includecount = (len(newinclude - oldinclude) -
636 647 len(oldinclude - newinclude))
637 648 excludecount = (len(newexclude - oldexclude) -
638 649 len(oldexclude - newexclude))
639 650
640 651 # TODO clean up this writeconfig() + try..except pattern once we can.
641 652 # See comment in importfromfiles() explaining it.
642 653 writeconfig(repo, newinclude, newexclude, newprofiles)
643 654
644 655 try:
645 656 fcounts = map(
646 657 len,
647 658 refreshwdir(repo, oldstatus, oldmatcher, force=force))
648 659
649 660 printchanges(repo.ui, opts, profilecount, includecount,
650 661 excludecount, *fcounts)
651 662 except Exception:
652 663 writeconfig(repo, oldinclude, oldexclude, oldprofiles)
653 664 raise
654 665
655 666 def printchanges(ui, opts, profilecount=0, includecount=0, excludecount=0,
656 667 added=0, dropped=0, conflicting=0):
657 668 """Print output summarizing sparse config changes."""
658 669 with ui.formatter('sparse', opts) as fm:
659 670 fm.startitem()
660 671 fm.condwrite(ui.verbose, 'profiles_added', _('Profiles changed: %d\n'),
661 672 profilecount)
662 673 fm.condwrite(ui.verbose, 'include_rules_added',
663 674 _('Include rules changed: %d\n'), includecount)
664 675 fm.condwrite(ui.verbose, 'exclude_rules_added',
665 676 _('Exclude rules changed: %d\n'), excludecount)
666 677
667 678 # In 'plain' verbose mode, mergemod.applyupdates already outputs what
668 679 # files are added or removed outside of the templating formatter
669 680 # framework. No point in repeating ourselves in that case.
670 681 if not fm.isplain():
671 682 fm.condwrite(ui.verbose, 'files_added', _('Files added: %d\n'),
672 683 added)
673 684 fm.condwrite(ui.verbose, 'files_dropped', _('Files dropped: %d\n'),
674 685 dropped)
675 686 fm.condwrite(ui.verbose, 'files_conflicting',
676 687 _('Files conflicting: %d\n'), conflicting)
@@ -1,275 +1,288 b''
1 1 test sparse
2 2
3 3 $ hg init myrepo
4 4 $ cd myrepo
5 5 $ cat > .hg/hgrc <<EOF
6 6 > [extensions]
7 7 > sparse=
8 8 > purge=
9 9 > strip=
10 10 > rebase=
11 11 > EOF
12 12
13 Config file without [section] is rejected
14
15 $ cat > bad.sparse <<EOF
16 > *.html
17 > EOF
18
19 $ hg debugsparse --import-rules bad.sparse
20 abort: sparse config entry outside of section: *.html
21 (add an [include] or [exclude] line to declare the entry type)
22 [255]
23 $ rm bad.sparse
24
13 25 $ echo a > index.html
14 26 $ echo x > data.py
15 27 $ echo z > readme.txt
16 28 $ cat > webpage.sparse <<EOF
17 29 > # frontend sparse profile
18 30 > [include]
19 31 > *.html
20 32 > EOF
21 33 $ cat > backend.sparse <<EOF
22 34 > # backend sparse profile
23 35 > [include]
24 36 > *.py
25 37 > EOF
26 38 $ hg ci -Aqm 'initial'
27 39
28 40 $ hg debugsparse --include '*.sparse'
29 41
30 42 Verify enabling a single profile works
31 43
32 44 $ hg debugsparse --enable-profile webpage.sparse
33 45 $ ls
34 46 backend.sparse
35 47 index.html
36 48 webpage.sparse
37 49
38 50 Verify enabling two profiles works
39 51
40 52 $ hg debugsparse --enable-profile backend.sparse
41 53 $ ls
42 54 backend.sparse
43 55 data.py
44 56 index.html
45 57 webpage.sparse
46 58
47 59 Verify disabling a profile works
48 60
49 61 $ hg debugsparse --disable-profile webpage.sparse
50 62 $ ls
51 63 backend.sparse
52 64 data.py
53 65 webpage.sparse
54 66
55 67 Verify that a profile is updated across multiple commits
56 68
57 69 $ cat > webpage.sparse <<EOF
58 70 > # frontend sparse profile
59 71 > [include]
60 72 > *.html
61 73 > EOF
62 74 $ cat > backend.sparse <<EOF
63 75 > # backend sparse profile
64 76 > [include]
65 77 > *.py
66 78 > *.txt
67 79 > EOF
68 80
69 81 $ echo foo >> data.py
70 82
71 83 $ hg ci -m 'edit profile'
72 84 $ ls
73 85 backend.sparse
74 86 data.py
75 87 readme.txt
76 88 webpage.sparse
77 89
78 90 $ hg up -q 0
79 91 $ ls
80 92 backend.sparse
81 93 data.py
82 94 webpage.sparse
83 95
84 96 $ hg up -q 1
85 97 $ ls
86 98 backend.sparse
87 99 data.py
88 100 readme.txt
89 101 webpage.sparse
90 102
91 103 Introduce a conflicting .hgsparse change
92 104
93 105 $ hg up -q 0
94 106 $ cat > backend.sparse <<EOF
95 107 > # Different backend sparse profile
96 108 > [include]
97 109 > *.html
98 110 > EOF
99 111 $ echo bar >> data.py
100 112
101 113 $ hg ci -qAm "edit profile other"
102 114 $ ls
103 115 backend.sparse
104 116 index.html
105 117 webpage.sparse
106 118
107 119 Verify conflicting merge pulls in the conflicting changes
108 120
109 121 $ hg merge 1
110 122 temporarily included 1 file(s) in the sparse checkout for merging
111 123 merging backend.sparse
112 124 merging data.py
113 125 warning: conflicts while merging backend.sparse! (edit, then use 'hg resolve --mark')
114 126 warning: conflicts while merging data.py! (edit, then use 'hg resolve --mark')
115 127 0 files updated, 0 files merged, 0 files removed, 2 files unresolved
116 128 use 'hg resolve' to retry unresolved file merges or 'hg update -C .' to abandon
117 129 [1]
118 130
119 131 $ rm *.orig
120 132 $ ls
121 133 backend.sparse
122 134 data.py
123 135 index.html
124 136 webpage.sparse
125 137
126 138 Verify resolving the merge removes the temporarily unioned files
127 139
128 140 $ cat > backend.sparse <<EOF
129 141 > # backend sparse profile
130 142 > [include]
131 143 > *.html
132 144 > *.txt
133 145 > EOF
134 146 $ hg resolve -m backend.sparse
135 147
136 148 $ cat > data.py <<EOF
137 149 > x
138 150 > foo
139 151 > bar
140 152 > EOF
141 153 $ hg resolve -m data.py
142 154 (no more unresolved files)
143 155
144 156 $ hg ci -qAm "merge profiles"
145 157 $ ls
146 158 backend.sparse
147 159 index.html
148 160 readme.txt
149 161 webpage.sparse
150 162
151 163 $ hg cat -r . data.py
152 164 x
153 165 foo
154 166 bar
155 167
156 168 Verify stripping refreshes dirstate
157 169
158 170 $ hg strip -q -r .
159 171 $ ls
160 172 backend.sparse
161 173 index.html
162 174 webpage.sparse
163 175
164 176 Verify rebase conflicts pulls in the conflicting changes
165 177
166 178 $ hg up -q 1
167 179 $ ls
168 180 backend.sparse
169 181 data.py
170 182 readme.txt
171 183 webpage.sparse
172 184
173 185 $ hg rebase -d 2
174 186 rebasing 1:a2b1de640a62 "edit profile"
175 187 temporarily included 1 file(s) in the sparse checkout for merging
176 188 merging backend.sparse
177 189 merging data.py
178 190 warning: conflicts while merging backend.sparse! (edit, then use 'hg resolve --mark')
179 191 warning: conflicts while merging data.py! (edit, then use 'hg resolve --mark')
180 192 unresolved conflicts (see hg resolve, then hg rebase --continue)
181 193 [1]
182 194 $ rm *.orig
183 195 $ ls
184 196 backend.sparse
185 197 data.py
186 198 index.html
187 199 webpage.sparse
188 200
189 201 Verify resolving conflict removes the temporary files
190 202
191 203 $ cat > backend.sparse <<EOF
192 204 > [include]
193 205 > *.html
194 206 > *.txt
195 207 > EOF
196 208 $ hg resolve -m backend.sparse
197 209
198 210 $ cat > data.py <<EOF
199 211 > x
200 212 > foo
201 213 > bar
202 214 > EOF
203 215 $ hg resolve -m data.py
204 216 (no more unresolved files)
205 217 continue: hg rebase --continue
206 218
207 219 $ hg rebase -q --continue
208 220 $ ls
209 221 backend.sparse
210 222 index.html
211 223 readme.txt
212 224 webpage.sparse
213 225
214 226 $ hg cat -r . data.py
215 227 x
216 228 foo
217 229 bar
218 230
219 231 Test checking out a commit that does not contain the sparse profile. The
220 232 warning message can be suppressed by setting missingwarning = false in
221 233 [sparse] section of your config:
222 234
223 235 $ hg debugsparse --reset
224 236 $ hg rm *.sparse
225 237 $ hg commit -m "delete profiles"
226 238 $ hg up -q ".^"
227 239 $ hg debugsparse --enable-profile backend.sparse
228 240 $ ls
229 241 index.html
230 242 readme.txt
231 243 $ hg up tip | grep warning
232 244 warning: sparse profile 'backend.sparse' not found in rev bfcb76de99cc - ignoring it
233 245 [1]
234 246 $ ls
235 247 data.py
236 248 index.html
237 249 readme.txt
238 250 $ hg debugsparse --disable-profile backend.sparse | grep warning
239 251 warning: sparse profile 'backend.sparse' not found in rev bfcb76de99cc - ignoring it
240 252 [1]
241 253 $ cat >> .hg/hgrc <<EOF
242 254 > [sparse]
243 255 > missingwarning = false
244 256 > EOF
245 257 $ hg debugsparse --enable-profile backend.sparse
246 258
247 259 $ cd ..
248 260
249 261 #if unix-permissions
250 262
251 263 Test file permissions changing across a sparse profile change
252 264 $ hg init sparseperm
253 265 $ cd sparseperm
254 266 $ cat > .hg/hgrc <<EOF
255 267 > [extensions]
256 268 > sparse=
257 269 > EOF
258 270 $ touch a b
259 271 $ cat > .hgsparse <<EOF
272 > [include]
260 273 > a
261 274 > EOF
262 275 $ hg commit -Aqm 'initial'
263 276 $ chmod a+x b
264 277 $ hg commit -qm 'make executable'
265 278 $ cat >> .hgsparse <<EOF
266 279 > b
267 280 > EOF
268 281 $ hg commit -qm 'update profile'
269 282 $ hg up -q 0
270 283 $ hg debugsparse --enable-profile .hgsparse
271 284 $ hg up -q 2
272 285 $ ls -l b
273 286 -rwxr-xr-x* b (glob)
274 287
275 288 #endif
General Comments 0
You need to be logged in to leave comments. Login now