##// END OF EJS Templates
sparse: start moving away from the global variable for detection of usage...
marmoute -
r50249:216f273b default
parent child Browse files
Show More
@@ -1,454 +1,459 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 38 have ``[include]`` after ``[exclude]``.
39 39
40 40 Non-special lines resemble file patterns to be added to either includes
41 41 or excludes. The syntax of these lines is documented by :hg:`help patterns`.
42 42 Patterns are interpreted as ``glob:`` by default and match against the
43 43 root of the repository.
44 44
45 45 Exclusion patterns take precedence over inclusion patterns. So even
46 46 if a file is explicitly included, an ``[exclude]`` entry can remove it.
47 47
48 48 For example, say you have a repository with 3 directories, ``frontend/``,
49 49 ``backend/``, and ``tools/``. ``frontend/`` and ``backend/`` correspond
50 50 to different projects and it is uncommon for someone working on one
51 51 to need the files for the other. But ``tools/`` contains files shared
52 52 between both projects. Your sparse config files may resemble::
53 53
54 54 # frontend.sparse
55 55 frontend/**
56 56 tools/**
57 57
58 58 # backend.sparse
59 59 backend/**
60 60 tools/**
61 61
62 62 Say the backend grows in size. Or there's a directory with thousands
63 63 of files you wish to exclude. You can modify the profile to exclude
64 64 certain files::
65 65
66 66 [include]
67 67 backend/**
68 68 tools/**
69 69
70 70 [exclude]
71 71 tools/tests/**
72 72 """
73 73
74 74
75 75 from mercurial.i18n import _
76 76 from mercurial.pycompat import setattr
77 77 from mercurial import (
78 78 cmdutil,
79 79 commands,
80 80 dirstate,
81 81 error,
82 82 extensions,
83 83 logcmdutil,
84 84 match as matchmod,
85 85 merge as mergemod,
86 86 pycompat,
87 87 registrar,
88 88 sparse,
89 89 util,
90 90 )
91 91
92 92 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
93 93 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
94 94 # be specifying the version(s) of Mercurial they are tested with, or
95 95 # leave the attribute unspecified.
96 96 testedwith = b'ships-with-hg-core'
97 97
98 98 cmdtable = {}
99 99 command = registrar.command(cmdtable)
100 100
101 101
102 102 def extsetup(ui):
103 103 sparse.enabled = True
104 104
105 105 _setupclone(ui)
106 106 _setuplog(ui)
107 107 _setupadd(ui)
108 108 _setupdirstate(ui)
109 109
110 110
111 111 def replacefilecache(cls, propname, replacement):
112 112 """Replace a filecache property with a new class. This allows changing the
113 113 cache invalidation condition."""
114 114 origcls = cls
115 115 assert callable(replacement)
116 116 while cls is not object:
117 117 if propname in cls.__dict__:
118 118 orig = cls.__dict__[propname]
119 119 setattr(cls, propname, replacement(orig))
120 120 break
121 121 cls = cls.__bases__[0]
122 122
123 123 if cls is object:
124 124 raise AttributeError(
125 125 _(b"type '%s' has no property '%s'") % (origcls, propname)
126 126 )
127 127
128 128
129 129 def _setuplog(ui):
130 130 entry = commands.table[b'log|history']
131 131 entry[1].append(
132 132 (
133 133 b'',
134 134 b'sparse',
135 135 None,
136 136 b"limit to changesets affecting the sparse checkout",
137 137 )
138 138 )
139 139
140 140 def _initialrevs(orig, repo, wopts):
141 141 revs = orig(repo, wopts)
142 142 if wopts.opts.get(b'sparse'):
143 143 sparsematch = sparse.matcher(repo)
144 144
145 145 def ctxmatch(rev):
146 146 ctx = repo[rev]
147 147 return any(f for f in ctx.files() if sparsematch(f))
148 148
149 149 revs = revs.filter(ctxmatch)
150 150 return revs
151 151
152 152 extensions.wrapfunction(logcmdutil, b'_initialrevs', _initialrevs)
153 153
154 154
155 155 def _clonesparsecmd(orig, ui, repo, *args, **opts):
156 156 include = opts.get('include')
157 157 exclude = opts.get('exclude')
158 158 enableprofile = opts.get('enable_profile')
159 159 narrow_pat = opts.get('narrow')
160 160
161 161 # if --narrow is passed, it means they are includes and excludes for narrow
162 162 # clone
163 163 if not narrow_pat and (include or exclude or enableprofile):
164 164
165 165 def clonesparse(orig, ctx, *args, **kwargs):
166 166 sparse.updateconfig(
167 167 ctx.repo().unfiltered(),
168 168 {},
169 169 include=include,
170 170 exclude=exclude,
171 171 enableprofile=enableprofile,
172 172 usereporootpaths=True,
173 173 )
174 174 return orig(ctx, *args, **kwargs)
175 175
176 176 extensions.wrapfunction(mergemod, b'update', clonesparse)
177 177 return orig(ui, repo, *args, **opts)
178 178
179 179
180 180 def _setupclone(ui):
181 181 entry = commands.table[b'clone']
182 182 entry[1].append((b'', b'enable-profile', [], b'enable a sparse profile'))
183 183 entry[1].append((b'', b'include', [], b'include sparse pattern'))
184 184 entry[1].append((b'', b'exclude', [], b'exclude sparse pattern'))
185 185 extensions.wrapcommand(commands.table, b'clone', _clonesparsecmd)
186 186
187 187
188 188 def _setupadd(ui):
189 189 entry = commands.table[b'add']
190 190 entry[1].append(
191 191 (
192 192 b's',
193 193 b'sparse',
194 194 None,
195 195 b'also include directories of added files in sparse config',
196 196 )
197 197 )
198 198
199 199 def _add(orig, ui, repo, *pats, **opts):
200 200 if opts.get('sparse'):
201 201 dirs = set()
202 202 for pat in pats:
203 203 dirname, basename = util.split(pat)
204 204 dirs.add(dirname)
205 205 sparse.updateconfig(repo, opts, include=list(dirs))
206 206 return orig(ui, repo, *pats, **opts)
207 207
208 208 extensions.wrapcommand(commands.table, b'add', _add)
209 209
210 210
211 211 def _setupdirstate(ui):
212 212 """Modify the dirstate to prevent stat'ing excluded files,
213 213 and to prevent modifications to files outside the checkout.
214 214 """
215 215
216 216 def walk(orig, self, match, subrepos, unknown, ignored, full=True):
217 217 # hack to not exclude explicitly-specified paths so that they can
218 218 # be warned later on e.g. dirstate.add()
219 219 em = matchmod.exact(match.files())
220 220 sm = matchmod.unionmatcher([self._sparsematcher, em])
221 221 match = matchmod.intersectmatchers(match, sm)
222 222 return orig(self, match, subrepos, unknown, ignored, full)
223 223
224 224 extensions.wrapfunction(dirstate.dirstate, b'walk', walk)
225 225
226 226 # dirstate.rebuild should not add non-matching files
227 227 def _rebuild(orig, self, parent, allfiles, changedfiles=None):
228 228 matcher = self._sparsematcher
229 229 if not matcher.always():
230 230 allfiles = [f for f in allfiles if matcher(f)]
231 231 if changedfiles:
232 232 changedfiles = [f for f in changedfiles if matcher(f)]
233 233
234 234 if changedfiles is not None:
235 235 # In _rebuild, these files will be deleted from the dirstate
236 236 # when they are not found to be in allfiles
237 237 dirstatefilestoremove = {f for f in self if not matcher(f)}
238 238 changedfiles = dirstatefilestoremove.union(changedfiles)
239 239
240 240 return orig(self, parent, allfiles, changedfiles)
241 241
242 242 extensions.wrapfunction(dirstate.dirstate, b'rebuild', _rebuild)
243 243
244 244 # Prevent adding files that are outside the sparse checkout
245 245 editfuncs = [
246 246 b'set_tracked',
247 247 b'set_untracked',
248 248 b'copy',
249 249 ]
250 250 hint = _(
251 251 b'include file with `hg debugsparse --include <pattern>` or use '
252 252 + b'`hg add -s <file>` to include file directory while adding'
253 253 )
254 254 for func in editfuncs:
255 255
256 256 def _wrapper(orig, self, *args, **kwargs):
257 257 sparsematch = self._sparsematcher
258 258 if not sparsematch.always():
259 259 for f in args:
260 260 if f is not None and not sparsematch(f) and f not in self:
261 261 raise error.Abort(
262 262 _(
263 263 b"cannot add '%s' - it is outside "
264 264 b"the sparse checkout"
265 265 )
266 266 % f,
267 267 hint=hint,
268 268 )
269 269 return orig(self, *args, **kwargs)
270 270
271 271 extensions.wrapfunction(dirstate.dirstate, func, _wrapper)
272 272
273 273
274 274 @command(
275 275 b'debugsparse',
276 276 [
277 277 (
278 278 b'I',
279 279 b'include',
280 280 [],
281 281 _(b'include files in the sparse checkout'),
282 282 _(b'PATTERN'),
283 283 ),
284 284 (
285 285 b'X',
286 286 b'exclude',
287 287 [],
288 288 _(b'exclude files in the sparse checkout'),
289 289 _(b'PATTERN'),
290 290 ),
291 291 (
292 292 b'd',
293 293 b'delete',
294 294 [],
295 295 _(b'delete an include/exclude rule'),
296 296 _(b'PATTERN'),
297 297 ),
298 298 (
299 299 b'f',
300 300 b'force',
301 301 False,
302 302 _(b'allow changing rules even with pending changes'),
303 303 ),
304 304 (
305 305 b'',
306 306 b'enable-profile',
307 307 [],
308 308 _(b'enables the specified profile'),
309 309 _(b'PATTERN'),
310 310 ),
311 311 (
312 312 b'',
313 313 b'disable-profile',
314 314 [],
315 315 _(b'disables the specified profile'),
316 316 _(b'PATTERN'),
317 317 ),
318 318 (
319 319 b'',
320 320 b'import-rules',
321 321 [],
322 322 _(b'imports rules from a file'),
323 323 _(b'PATTERN'),
324 324 ),
325 325 (b'', b'clear-rules', False, _(b'clears local include/exclude rules')),
326 326 (
327 327 b'',
328 328 b'refresh',
329 329 False,
330 330 _(b'updates the working after sparseness changes'),
331 331 ),
332 332 (b'', b'reset', False, _(b'makes the repo full again')),
333 333 ]
334 334 + commands.templateopts,
335 335 _(b'[--OPTION]'),
336 336 helpbasic=True,
337 337 )
338 338 def debugsparse(ui, repo, **opts):
339 339 """make the current checkout sparse, or edit the existing checkout
340 340
341 341 The sparse command is used to make the current checkout sparse.
342 342 This means files that don't meet the sparse condition will not be
343 343 written to disk, or show up in any working copy operations. It does
344 344 not affect files in history in any way.
345 345
346 346 Passing no arguments prints the currently applied sparse rules.
347 347
348 348 --include and --exclude are used to add and remove files from the sparse
349 349 checkout. The effects of adding an include or exclude rule are applied
350 350 immediately. If applying the new rule would cause a file with pending
351 351 changes to be added or removed, the command will fail. Pass --force to
352 352 force a rule change even with pending changes (the changes on disk will
353 353 be preserved).
354 354
355 355 --delete removes an existing include/exclude rule. The effects are
356 356 immediate.
357 357
358 358 --refresh refreshes the files on disk based on the sparse rules. This is
359 359 only necessary if .hg/sparse was changed by hand.
360 360
361 361 --enable-profile and --disable-profile accept a path to a .hgsparse file.
362 362 This allows defining sparse checkouts and tracking them inside the
363 363 repository. This is useful for defining commonly used sparse checkouts for
364 364 many people to use. As the profile definition changes over time, the sparse
365 365 checkout will automatically be updated appropriately, depending on which
366 366 changeset is checked out. Changes to .hgsparse are not applied until they
367 367 have been committed.
368 368
369 369 --import-rules accepts a path to a file containing rules in the .hgsparse
370 370 format, allowing you to add --include, --exclude and --enable-profile rules
371 371 in bulk. Like the --include, --exclude and --enable-profile switches, the
372 372 changes are applied immediately.
373 373
374 374 --clear-rules removes all local include and exclude rules, while leaving
375 375 any enabled profiles in place.
376 376
377 377 Returns 0 if editing the sparse checkout succeeds.
378 378 """
379 379 opts = pycompat.byteskwargs(opts)
380 380 include = opts.get(b'include')
381 381 exclude = opts.get(b'exclude')
382 382 force = opts.get(b'force')
383 383 enableprofile = opts.get(b'enable_profile')
384 384 disableprofile = opts.get(b'disable_profile')
385 385 importrules = opts.get(b'import_rules')
386 386 clearrules = opts.get(b'clear_rules')
387 387 delete = opts.get(b'delete')
388 388 refresh = opts.get(b'refresh')
389 389 reset = opts.get(b'reset')
390 390 action = cmdutil.check_at_most_one_arg(
391 391 opts, b'import_rules', b'clear_rules', b'refresh'
392 392 )
393 393 updateconfig = bool(
394 394 include or exclude or delete or reset or enableprofile or disableprofile
395 395 )
396 396 count = sum([updateconfig, bool(action)])
397 397 if count > 1:
398 398 raise error.Abort(_(b"too many flags specified"))
399 399
400 # enable sparse on repo even if the requirements is missing.
401 repo._has_sparse = True
402
400 403 if count == 0:
401 404 if repo.vfs.exists(b'sparse'):
402 405 ui.status(repo.vfs.read(b"sparse") + b"\n")
403 406 temporaryincludes = sparse.readtemporaryincludes(repo)
404 407 if temporaryincludes:
405 408 ui.status(
406 409 _(b"Temporarily Included Files (for merge/rebase):\n")
407 410 )
408 411 ui.status((b"\n".join(temporaryincludes) + b"\n"))
409 412 return
410 413 else:
411 414 raise error.Abort(
412 415 _(
413 416 b'the debugsparse command is only supported on'
414 417 b' sparse repositories'
415 418 )
416 419 )
417 420
418 421 if updateconfig:
419 422 sparse.updateconfig(
420 423 repo,
421 424 opts,
422 425 include=include,
423 426 exclude=exclude,
424 427 reset=reset,
425 428 delete=delete,
426 429 enableprofile=enableprofile,
427 430 disableprofile=disableprofile,
428 431 force=force,
429 432 )
430 433
431 434 if importrules:
432 435 sparse.importfromfiles(repo, opts, importrules, force=force)
433 436
434 437 if clearrules:
435 438 sparse.clearrules(repo, force=force)
436 439
437 440 if refresh:
438 441 try:
439 442 wlock = repo.wlock()
440 443 fcounts = map(
441 444 len,
442 445 sparse.refreshwdir(
443 446 repo, repo.status(), sparse.matcher(repo), force=force
444 447 ),
445 448 )
446 449 sparse.printchanges(
447 450 ui,
448 451 opts,
449 452 added=fcounts[0],
450 453 dropped=fcounts[1],
451 454 conflicting=fcounts[2],
452 455 )
453 456 finally:
454 457 wlock.release()
458
459 del repo._has_sparse
@@ -1,846 +1,856 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
9 9 import os
10 10
11 11 from .i18n import _
12 12 from .node import hex
13 13 from . import (
14 14 error,
15 15 match as matchmod,
16 16 merge as mergemod,
17 17 mergestate as mergestatemod,
18 18 pathutil,
19 19 pycompat,
20 20 requirements,
21 21 scmutil,
22 22 util,
23 23 )
24 24 from .utils import hashutil
25 25
26 26
27 27 # Whether sparse features are enabled. This variable is intended to be
28 28 # temporary to facilitate porting sparse to core. It should eventually be
29 29 # a per-repo option, possibly a repo requirement.
30 30 enabled = False
31 31
32 32
33 def use_sparse(repo):
34 if getattr(repo, "_has_sparse", False):
35 # When enabling sparse the first time we need it to be enabled before
36 # actually enabling it. This hack could be avoided if the code was
37 # improved further, however this is an improvement over the previously
38 # existing global variable.
39 return True
40 return requirements.SPARSE_REQUIREMENT in repo.requirements
41
42
33 43 def parseconfig(ui, raw, action):
34 44 """Parse sparse config file content.
35 45
36 46 action is the command which is trigerring this read, can be narrow, sparse
37 47
38 48 Returns a tuple of includes, excludes, and profiles.
39 49 """
40 50 with util.timedcm(
41 51 'sparse.parseconfig(ui, %d bytes, action=%s)', len(raw), action
42 52 ):
43 53 includes = set()
44 54 excludes = set()
45 55 profiles = set()
46 56 current = None
47 57 havesection = False
48 58
49 59 for line in raw.split(b'\n'):
50 60 line = line.strip()
51 61 if not line or line.startswith(b'#'):
52 62 # empty or comment line, skip
53 63 continue
54 64 elif line.startswith(b'%include '):
55 65 line = line[9:].strip()
56 66 if line:
57 67 profiles.add(line)
58 68 elif line == b'[include]':
59 69 if havesection and current != includes:
60 70 # TODO pass filename into this API so we can report it.
61 71 raise error.Abort(
62 72 _(
63 73 b'%(action)s config cannot have includes '
64 74 b'after excludes'
65 75 )
66 76 % {b'action': action}
67 77 )
68 78 havesection = True
69 79 current = includes
70 80 continue
71 81 elif line == b'[exclude]':
72 82 havesection = True
73 83 current = excludes
74 84 elif line:
75 85 if current is None:
76 86 raise error.Abort(
77 87 _(
78 88 b'%(action)s config entry outside of '
79 89 b'section: %(line)s'
80 90 )
81 91 % {b'action': action, b'line': line},
82 92 hint=_(
83 93 b'add an [include] or [exclude] line '
84 94 b'to declare the entry type'
85 95 ),
86 96 )
87 97
88 98 if line.strip().startswith(b'/'):
89 99 ui.warn(
90 100 _(
91 101 b'warning: %(action)s profile cannot use'
92 102 b' paths starting with /, ignoring %(line)s\n'
93 103 )
94 104 % {b'action': action, b'line': line}
95 105 )
96 106 continue
97 107 current.add(line)
98 108
99 109 return includes, excludes, profiles
100 110
101 111
102 112 # Exists as separate function to facilitate monkeypatching.
103 113 def readprofile(repo, profile, changeid):
104 114 """Resolve the raw content of a sparse profile file."""
105 115 # TODO add some kind of cache here because this incurs a manifest
106 116 # resolve and can be slow.
107 117 return repo.filectx(profile, changeid=changeid).data()
108 118
109 119
110 120 def patternsforrev(repo, rev):
111 121 """Obtain sparse checkout patterns for the given rev.
112 122
113 123 Returns a tuple of iterables representing includes, excludes, and
114 124 patterns.
115 125 """
116 126 # Feature isn't enabled. No-op.
117 if not enabled:
127 if not use_sparse(repo):
118 128 return set(), set(), set()
119 129
120 130 raw = repo.vfs.tryread(b'sparse')
121 131 if not raw:
122 132 return set(), set(), set()
123 133
124 134 if rev is None:
125 135 raise error.Abort(
126 136 _(b'cannot parse sparse patterns from working directory')
127 137 )
128 138
129 139 includes, excludes, profiles = parseconfig(repo.ui, raw, b'sparse')
130 140 ctx = repo[rev]
131 141
132 142 if profiles:
133 143 visited = set()
134 144 while profiles:
135 145 profile = profiles.pop()
136 146 if profile in visited:
137 147 continue
138 148
139 149 visited.add(profile)
140 150
141 151 try:
142 152 raw = readprofile(repo, profile, rev)
143 153 except error.ManifestLookupError:
144 154 msg = (
145 155 b"warning: sparse profile '%s' not found "
146 156 b"in rev %s - ignoring it\n" % (profile, ctx)
147 157 )
148 158 # experimental config: sparse.missingwarning
149 159 if repo.ui.configbool(b'sparse', b'missingwarning'):
150 160 repo.ui.warn(msg)
151 161 else:
152 162 repo.ui.debug(msg)
153 163 continue
154 164
155 165 pincludes, pexcludes, subprofs = parseconfig(
156 166 repo.ui, raw, b'sparse'
157 167 )
158 168 includes.update(pincludes)
159 169 excludes.update(pexcludes)
160 170 profiles.update(subprofs)
161 171
162 172 profiles = visited
163 173
164 174 if includes:
165 175 includes.add(b'.hg*')
166 176
167 177 return includes, excludes, profiles
168 178
169 179
170 180 def activeconfig(repo):
171 181 """Determine the active sparse config rules.
172 182
173 183 Rules are constructed by reading the current sparse config and bringing in
174 184 referenced profiles from parents of the working directory.
175 185 """
176 186 revs = [
177 187 repo.changelog.rev(node)
178 188 for node in repo.dirstate.parents()
179 189 if node != repo.nullid
180 190 ]
181 191
182 192 allincludes = set()
183 193 allexcludes = set()
184 194 allprofiles = set()
185 195
186 196 for rev in revs:
187 197 includes, excludes, profiles = patternsforrev(repo, rev)
188 198 allincludes |= includes
189 199 allexcludes |= excludes
190 200 allprofiles |= profiles
191 201
192 202 return allincludes, allexcludes, allprofiles
193 203
194 204
195 205 def configsignature(repo, includetemp=True):
196 206 """Obtain the signature string for the current sparse configuration.
197 207
198 208 This is used to construct a cache key for matchers.
199 209 """
200 210 cache = repo._sparsesignaturecache
201 211
202 212 signature = cache.get(b'signature')
203 213
204 214 if includetemp:
205 215 tempsignature = cache.get(b'tempsignature')
206 216 else:
207 217 tempsignature = b'0'
208 218
209 219 if signature is None or (includetemp and tempsignature is None):
210 220 signature = hex(hashutil.sha1(repo.vfs.tryread(b'sparse')).digest())
211 221 cache[b'signature'] = signature
212 222
213 223 if includetemp:
214 224 raw = repo.vfs.tryread(b'tempsparse')
215 225 tempsignature = hex(hashutil.sha1(raw).digest())
216 226 cache[b'tempsignature'] = tempsignature
217 227
218 228 return b'%s %s' % (signature, tempsignature)
219 229
220 230
221 231 def writeconfig(repo, includes, excludes, profiles):
222 232 """Write the sparse config file given a sparse configuration."""
223 233 with repo.vfs(b'sparse', b'wb') as fh:
224 234 for p in sorted(profiles):
225 235 fh.write(b'%%include %s\n' % p)
226 236
227 237 if includes:
228 238 fh.write(b'[include]\n')
229 239 for i in sorted(includes):
230 240 fh.write(i)
231 241 fh.write(b'\n')
232 242
233 243 if excludes:
234 244 fh.write(b'[exclude]\n')
235 245 for e in sorted(excludes):
236 246 fh.write(e)
237 247 fh.write(b'\n')
238 248
239 249 repo._sparsesignaturecache.clear()
240 250
241 251
242 252 def readtemporaryincludes(repo):
243 253 raw = repo.vfs.tryread(b'tempsparse')
244 254 if not raw:
245 255 return set()
246 256
247 257 return set(raw.split(b'\n'))
248 258
249 259
250 260 def writetemporaryincludes(repo, includes):
251 261 repo.vfs.write(b'tempsparse', b'\n'.join(sorted(includes)))
252 262 repo._sparsesignaturecache.clear()
253 263
254 264
255 265 def addtemporaryincludes(repo, additional):
256 266 includes = readtemporaryincludes(repo)
257 267 for i in additional:
258 268 includes.add(i)
259 269 writetemporaryincludes(repo, includes)
260 270
261 271
262 272 def prunetemporaryincludes(repo):
263 if not enabled or not repo.vfs.exists(b'tempsparse'):
273 if not use_sparse(repo) or not repo.vfs.exists(b'tempsparse'):
264 274 return
265 275
266 276 s = repo.status()
267 277 if s.modified or s.added or s.removed or s.deleted:
268 278 # Still have pending changes. Don't bother trying to prune.
269 279 return
270 280
271 281 sparsematch = matcher(repo, includetemp=False)
272 282 dirstate = repo.dirstate
273 283 mresult = mergemod.mergeresult()
274 284 dropped = []
275 285 tempincludes = readtemporaryincludes(repo)
276 286 for file in tempincludes:
277 287 if file in dirstate and not sparsematch(file):
278 288 message = _(b'dropping temporarily included sparse files')
279 289 mresult.addfile(file, mergestatemod.ACTION_REMOVE, None, message)
280 290 dropped.append(file)
281 291
282 292 mergemod.applyupdates(
283 293 repo, mresult, repo[None], repo[b'.'], False, wantfiledata=False
284 294 )
285 295
286 296 # Fix dirstate
287 297 for file in dropped:
288 298 dirstate.update_file(file, p1_tracked=False, wc_tracked=False)
289 299
290 300 repo.vfs.unlink(b'tempsparse')
291 301 repo._sparsesignaturecache.clear()
292 302 msg = _(
293 303 b'cleaned up %d temporarily added file(s) from the '
294 304 b'sparse checkout\n'
295 305 )
296 306 repo.ui.status(msg % len(tempincludes))
297 307
298 308
299 309 def forceincludematcher(matcher, includes):
300 310 """Returns a matcher that returns true for any of the forced includes
301 311 before testing against the actual matcher."""
302 312 kindpats = [(b'path', include, b'') for include in includes]
303 313 includematcher = matchmod.includematcher(b'', kindpats)
304 314 return matchmod.unionmatcher([includematcher, matcher])
305 315
306 316
307 317 def matcher(repo, revs=None, includetemp=True):
308 318 """Obtain a matcher for sparse working directories for the given revs.
309 319
310 320 If multiple revisions are specified, the matcher is the union of all
311 321 revs.
312 322
313 323 ``includetemp`` indicates whether to use the temporary sparse profile.
314 324 """
315 325 # If sparse isn't enabled, sparse matcher matches everything.
316 if not enabled:
326 if not use_sparse(repo):
317 327 return matchmod.always()
318 328
319 329 if not revs or revs == [None]:
320 330 revs = [
321 331 repo.changelog.rev(node)
322 332 for node in repo.dirstate.parents()
323 333 if node != repo.nullid
324 334 ]
325 335
326 336 signature = configsignature(repo, includetemp=includetemp)
327 337
328 338 key = b'%s %s' % (signature, b' '.join(map(pycompat.bytestr, revs)))
329 339
330 340 result = repo._sparsematchercache.get(key)
331 341 if result:
332 342 return result
333 343
334 344 matchers = []
335 345 for rev in revs:
336 346 try:
337 347 includes, excludes, profiles = patternsforrev(repo, rev)
338 348
339 349 if includes or excludes:
340 350 matcher = matchmod.match(
341 351 repo.root,
342 352 b'',
343 353 [],
344 354 include=includes,
345 355 exclude=excludes,
346 356 default=b'relpath',
347 357 )
348 358 matchers.append(matcher)
349 359 except IOError:
350 360 pass
351 361
352 362 if not matchers:
353 363 result = matchmod.always()
354 364 elif len(matchers) == 1:
355 365 result = matchers[0]
356 366 else:
357 367 result = matchmod.unionmatcher(matchers)
358 368
359 369 if includetemp:
360 370 tempincludes = readtemporaryincludes(repo)
361 371 result = forceincludematcher(result, tempincludes)
362 372
363 373 repo._sparsematchercache[key] = result
364 374
365 375 return result
366 376
367 377
368 378 def filterupdatesactions(repo, wctx, mctx, branchmerge, mresult):
369 379 """Filter updates to only lay out files that match the sparse rules."""
370 if not enabled:
380 if not use_sparse(repo):
371 381 return
372 382
373 383 oldrevs = [pctx.rev() for pctx in wctx.parents()]
374 384 oldsparsematch = matcher(repo, oldrevs)
375 385
376 386 if oldsparsematch.always():
377 387 return
378 388
379 389 files = set()
380 390 prunedactions = {}
381 391
382 392 if branchmerge:
383 393 # If we're merging, use the wctx filter, since we're merging into
384 394 # the wctx.
385 395 sparsematch = matcher(repo, [wctx.p1().rev()])
386 396 else:
387 397 # If we're updating, use the target context's filter, since we're
388 398 # moving to the target context.
389 399 sparsematch = matcher(repo, [mctx.rev()])
390 400
391 401 temporaryfiles = []
392 402 for file, action in mresult.filemap():
393 403 type, args, msg = action
394 404 files.add(file)
395 405 if sparsematch(file):
396 406 prunedactions[file] = action
397 407 elif type == mergestatemod.ACTION_MERGE:
398 408 temporaryfiles.append(file)
399 409 prunedactions[file] = action
400 410 elif branchmerge:
401 411 if not type.no_op:
402 412 temporaryfiles.append(file)
403 413 prunedactions[file] = action
404 414 elif type == mergestatemod.ACTION_FORGET:
405 415 prunedactions[file] = action
406 416 elif file in wctx:
407 417 prunedactions[file] = (mergestatemod.ACTION_REMOVE, args, msg)
408 418
409 419 # in case or rename on one side, it is possible that f1 might not
410 420 # be present in sparse checkout we should include it
411 421 # TODO: should we do the same for f2?
412 422 # exists as a separate check because file can be in sparse and hence
413 423 # if we try to club this condition in above `elif type == ACTION_MERGE`
414 424 # it won't be triggered
415 425 if branchmerge and type == mergestatemod.ACTION_MERGE:
416 426 f1, f2, fa, move, anc = args
417 427 if not sparsematch(f1):
418 428 temporaryfiles.append(f1)
419 429
420 430 if len(temporaryfiles) > 0:
421 431 repo.ui.status(
422 432 _(
423 433 b'temporarily included %d file(s) in the sparse '
424 434 b'checkout for merging\n'
425 435 )
426 436 % len(temporaryfiles)
427 437 )
428 438 addtemporaryincludes(repo, temporaryfiles)
429 439
430 440 # Add the new files to the working copy so they can be merged, etc
431 441 tmresult = mergemod.mergeresult()
432 442 message = b'temporarily adding to sparse checkout'
433 443 wctxmanifest = repo[None].manifest()
434 444 for file in temporaryfiles:
435 445 if file in wctxmanifest:
436 446 fctx = repo[None][file]
437 447 tmresult.addfile(
438 448 file,
439 449 mergestatemod.ACTION_GET,
440 450 (fctx.flags(), False),
441 451 message,
442 452 )
443 453
444 454 with repo.dirstate.parentchange():
445 455 mergemod.applyupdates(
446 456 repo,
447 457 tmresult,
448 458 repo[None],
449 459 repo[b'.'],
450 460 False,
451 461 wantfiledata=False,
452 462 )
453 463
454 464 dirstate = repo.dirstate
455 465 for file, flags, msg in tmresult.getactions(
456 466 [mergestatemod.ACTION_GET]
457 467 ):
458 468 dirstate.update_file(file, p1_tracked=True, wc_tracked=True)
459 469
460 470 profiles = activeconfig(repo)[2]
461 471 changedprofiles = profiles & files
462 472 # If an active profile changed during the update, refresh the checkout.
463 473 # Don't do this during a branch merge, since all incoming changes should
464 474 # have been handled by the temporary includes above.
465 475 if changedprofiles and not branchmerge:
466 476 mf = mctx.manifest()
467 477 for file in mf:
468 478 old = oldsparsematch(file)
469 479 new = sparsematch(file)
470 480 if not old and new:
471 481 flags = mf.flags(file)
472 482 prunedactions[file] = (
473 483 mergestatemod.ACTION_GET,
474 484 (flags, False),
475 485 b'',
476 486 )
477 487 elif old and not new:
478 488 prunedactions[file] = (mergestatemod.ACTION_REMOVE, [], b'')
479 489
480 490 mresult.setactions(prunedactions)
481 491
482 492
483 493 def refreshwdir(repo, origstatus, origsparsematch, force=False):
484 494 """Refreshes working directory by taking sparse config into account.
485 495
486 496 The old status and sparse matcher is compared against the current sparse
487 497 matcher.
488 498
489 499 Will abort if a file with pending changes is being excluded or included
490 500 unless ``force`` is True.
491 501 """
492 502 # Verify there are no pending changes
493 503 pending = set()
494 504 pending.update(origstatus.modified)
495 505 pending.update(origstatus.added)
496 506 pending.update(origstatus.removed)
497 507 sparsematch = matcher(repo)
498 508 abort = False
499 509
500 510 for f in pending:
501 511 if not sparsematch(f):
502 512 repo.ui.warn(_(b"pending changes to '%s'\n") % f)
503 513 abort = not force
504 514
505 515 if abort:
506 516 raise error.Abort(
507 517 _(b'could not update sparseness due to pending changes')
508 518 )
509 519
510 520 # Calculate merge result
511 521 dirstate = repo.dirstate
512 522 ctx = repo[b'.']
513 523 added = []
514 524 lookup = []
515 525 dropped = []
516 526 mf = ctx.manifest()
517 527 files = set(mf)
518 528 mresult = mergemod.mergeresult()
519 529
520 530 for file in files:
521 531 old = origsparsematch(file)
522 532 new = sparsematch(file)
523 533 # Add files that are newly included, or that don't exist in
524 534 # the dirstate yet.
525 535 if (new and not old) or (old and new and not file in dirstate):
526 536 fl = mf.flags(file)
527 537 if repo.wvfs.exists(file):
528 538 mresult.addfile(file, mergestatemod.ACTION_EXEC, (fl,), b'')
529 539 lookup.append(file)
530 540 else:
531 541 mresult.addfile(
532 542 file, mergestatemod.ACTION_GET, (fl, False), b''
533 543 )
534 544 added.append(file)
535 545 # Drop files that are newly excluded, or that still exist in
536 546 # the dirstate.
537 547 elif (old and not new) or (not old and not new and file in dirstate):
538 548 dropped.append(file)
539 549 if file not in pending:
540 550 mresult.addfile(file, mergestatemod.ACTION_REMOVE, [], b'')
541 551
542 552 # Verify there are no pending changes in newly included files
543 553 abort = False
544 554 for file in lookup:
545 555 repo.ui.warn(_(b"pending changes to '%s'\n") % file)
546 556 abort = not force
547 557 if abort:
548 558 raise error.Abort(
549 559 _(
550 560 b'cannot change sparseness due to pending '
551 561 b'changes (delete the files or use '
552 562 b'--force to bring them back dirty)'
553 563 )
554 564 )
555 565
556 566 # Check for files that were only in the dirstate.
557 567 for file, state in dirstate.items():
558 568 if not file in files:
559 569 old = origsparsematch(file)
560 570 new = sparsematch(file)
561 571 if old and not new:
562 572 dropped.append(file)
563 573
564 574 mergemod.applyupdates(
565 575 repo, mresult, repo[None], repo[b'.'], False, wantfiledata=False
566 576 )
567 577
568 578 # Fix dirstate
569 579 for file in added:
570 580 dirstate.update_file(file, p1_tracked=True, wc_tracked=True)
571 581
572 582 for file in dropped:
573 583 dirstate.update_file(file, p1_tracked=False, wc_tracked=False)
574 584
575 585 for file in lookup:
576 586 # File exists on disk, and we're bringing it back in an unknown state.
577 587 dirstate.update_file(
578 588 file, p1_tracked=True, wc_tracked=True, possibly_dirty=True
579 589 )
580 590
581 591 return added, dropped, lookup
582 592
583 593
584 594 def aftercommit(repo, node):
585 595 """Perform actions after a working directory commit."""
586 596 # This function is called unconditionally, even if sparse isn't
587 597 # enabled.
588 598 ctx = repo[node]
589 599
590 600 profiles = patternsforrev(repo, ctx.rev())[2]
591 601
592 602 # profiles will only have data if sparse is enabled.
593 603 if profiles & set(ctx.files()):
594 604 origstatus = repo.status()
595 605 origsparsematch = matcher(repo)
596 606 refreshwdir(repo, origstatus, origsparsematch, force=True)
597 607
598 608 prunetemporaryincludes(repo)
599 609
600 610
601 611 def _updateconfigandrefreshwdir(
602 612 repo, includes, excludes, profiles, force=False, removing=False
603 613 ):
604 614 """Update the sparse config and working directory state."""
605 615 with repo.lock():
606 616 raw = repo.vfs.tryread(b'sparse')
607 617 oldincludes, oldexcludes, oldprofiles = parseconfig(
608 618 repo.ui, raw, b'sparse'
609 619 )
610 620
611 621 oldstatus = repo.status()
612 622 oldmatch = matcher(repo)
613 623 oldrequires = set(repo.requirements)
614 624
615 625 # TODO remove this try..except once the matcher integrates better
616 626 # with dirstate. We currently have to write the updated config
617 627 # because that will invalidate the matcher cache and force a
618 628 # re-read. We ideally want to update the cached matcher on the
619 629 # repo instance then flush the new config to disk once wdir is
620 630 # updated. But this requires massive rework to matcher() and its
621 631 # consumers.
622 632
623 633 if requirements.SPARSE_REQUIREMENT in oldrequires and removing:
624 634 repo.requirements.discard(requirements.SPARSE_REQUIREMENT)
625 635 scmutil.writereporequirements(repo)
626 636 elif requirements.SPARSE_REQUIREMENT not in oldrequires:
627 637 repo.requirements.add(requirements.SPARSE_REQUIREMENT)
628 638 scmutil.writereporequirements(repo)
629 639
630 640 try:
631 641 writeconfig(repo, includes, excludes, profiles)
632 642 return refreshwdir(repo, oldstatus, oldmatch, force=force)
633 643 except Exception:
634 644 if repo.requirements != oldrequires:
635 645 repo.requirements.clear()
636 646 repo.requirements |= oldrequires
637 647 scmutil.writereporequirements(repo)
638 648 writeconfig(repo, oldincludes, oldexcludes, oldprofiles)
639 649 raise
640 650
641 651
642 652 def clearrules(repo, force=False):
643 653 """Clears include/exclude rules from the sparse config.
644 654
645 655 The remaining sparse config only has profiles, if defined. The working
646 656 directory is refreshed, as needed.
647 657 """
648 658 with repo.wlock(), repo.dirstate.parentchange():
649 659 raw = repo.vfs.tryread(b'sparse')
650 660 includes, excludes, profiles = parseconfig(repo.ui, raw, b'sparse')
651 661
652 662 if not includes and not excludes:
653 663 return
654 664
655 665 _updateconfigandrefreshwdir(repo, set(), set(), profiles, force=force)
656 666
657 667
658 668 def importfromfiles(repo, opts, paths, force=False):
659 669 """Import sparse config rules from files.
660 670
661 671 The updated sparse config is written out and the working directory
662 672 is refreshed, as needed.
663 673 """
664 674 with repo.wlock(), repo.dirstate.parentchange():
665 675 # read current configuration
666 676 raw = repo.vfs.tryread(b'sparse')
667 677 includes, excludes, profiles = parseconfig(repo.ui, raw, b'sparse')
668 678 aincludes, aexcludes, aprofiles = activeconfig(repo)
669 679
670 680 # Import rules on top; only take in rules that are not yet
671 681 # part of the active rules.
672 682 changed = False
673 683 for p in paths:
674 684 with util.posixfile(util.expandpath(p), mode=b'rb') as fh:
675 685 raw = fh.read()
676 686
677 687 iincludes, iexcludes, iprofiles = parseconfig(
678 688 repo.ui, raw, b'sparse'
679 689 )
680 690 oldsize = len(includes) + len(excludes) + len(profiles)
681 691 includes.update(iincludes - aincludes)
682 692 excludes.update(iexcludes - aexcludes)
683 693 profiles.update(iprofiles - aprofiles)
684 694 if len(includes) + len(excludes) + len(profiles) > oldsize:
685 695 changed = True
686 696
687 697 profilecount = includecount = excludecount = 0
688 698 fcounts = (0, 0, 0)
689 699
690 700 if changed:
691 701 profilecount = len(profiles - aprofiles)
692 702 includecount = len(includes - aincludes)
693 703 excludecount = len(excludes - aexcludes)
694 704
695 705 fcounts = map(
696 706 len,
697 707 _updateconfigandrefreshwdir(
698 708 repo, includes, excludes, profiles, force=force
699 709 ),
700 710 )
701 711
702 712 printchanges(
703 713 repo.ui, opts, profilecount, includecount, excludecount, *fcounts
704 714 )
705 715
706 716
707 717 def updateconfig(
708 718 repo,
709 719 opts,
710 720 include=(),
711 721 exclude=(),
712 722 reset=False,
713 723 delete=(),
714 724 enableprofile=(),
715 725 disableprofile=(),
716 726 force=False,
717 727 usereporootpaths=False,
718 728 ):
719 729 """Perform a sparse config update.
720 730
721 731 The new config is written out and a working directory refresh is performed.
722 732 """
723 733 with repo.wlock(), repo.lock(), repo.dirstate.parentchange():
724 734 raw = repo.vfs.tryread(b'sparse')
725 735 oldinclude, oldexclude, oldprofiles = parseconfig(
726 736 repo.ui, raw, b'sparse'
727 737 )
728 738
729 739 if reset:
730 740 newinclude = set()
731 741 newexclude = set()
732 742 newprofiles = set()
733 743 else:
734 744 newinclude = set(oldinclude)
735 745 newexclude = set(oldexclude)
736 746 newprofiles = set(oldprofiles)
737 747
738 748 def normalize_pats(pats):
739 749 if any(os.path.isabs(pat) for pat in pats):
740 750 raise error.Abort(_(b'paths cannot be absolute'))
741 751
742 752 if usereporootpaths:
743 753 return pats
744 754
745 755 # let's treat paths as relative to cwd
746 756 root, cwd = repo.root, repo.getcwd()
747 757 abspats = []
748 758 for kindpat in pats:
749 759 kind, pat = matchmod._patsplit(kindpat, None)
750 760 if kind in matchmod.cwdrelativepatternkinds or kind is None:
751 761 ap = (kind + b':' if kind else b'') + pathutil.canonpath(
752 762 root, cwd, pat
753 763 )
754 764 abspats.append(ap)
755 765 else:
756 766 abspats.append(kindpat)
757 767 return abspats
758 768
759 769 include = normalize_pats(include)
760 770 exclude = normalize_pats(exclude)
761 771 delete = normalize_pats(delete)
762 772 disableprofile = normalize_pats(disableprofile)
763 773 enableprofile = normalize_pats(enableprofile)
764 774
765 775 newinclude.difference_update(delete)
766 776 newexclude.difference_update(delete)
767 777 newprofiles.difference_update(disableprofile)
768 778 newinclude.update(include)
769 779 newprofiles.update(enableprofile)
770 780 newexclude.update(exclude)
771 781
772 782 profilecount = len(newprofiles - oldprofiles) - len(
773 783 oldprofiles - newprofiles
774 784 )
775 785 includecount = len(newinclude - oldinclude) - len(
776 786 oldinclude - newinclude
777 787 )
778 788 excludecount = len(newexclude - oldexclude) - len(
779 789 oldexclude - newexclude
780 790 )
781 791
782 792 fcounts = map(
783 793 len,
784 794 _updateconfigandrefreshwdir(
785 795 repo,
786 796 newinclude,
787 797 newexclude,
788 798 newprofiles,
789 799 force=force,
790 800 removing=reset,
791 801 ),
792 802 )
793 803
794 804 printchanges(
795 805 repo.ui, opts, profilecount, includecount, excludecount, *fcounts
796 806 )
797 807
798 808
799 809 def printchanges(
800 810 ui,
801 811 opts,
802 812 profilecount=0,
803 813 includecount=0,
804 814 excludecount=0,
805 815 added=0,
806 816 dropped=0,
807 817 conflicting=0,
808 818 ):
809 819 """Print output summarizing sparse config changes."""
810 820 with ui.formatter(b'sparse', opts) as fm:
811 821 fm.startitem()
812 822 fm.condwrite(
813 823 ui.verbose,
814 824 b'profiles_added',
815 825 _(b'Profiles changed: %d\n'),
816 826 profilecount,
817 827 )
818 828 fm.condwrite(
819 829 ui.verbose,
820 830 b'include_rules_added',
821 831 _(b'Include rules changed: %d\n'),
822 832 includecount,
823 833 )
824 834 fm.condwrite(
825 835 ui.verbose,
826 836 b'exclude_rules_added',
827 837 _(b'Exclude rules changed: %d\n'),
828 838 excludecount,
829 839 )
830 840
831 841 # In 'plain' verbose mode, mergemod.applyupdates already outputs what
832 842 # files are added or removed outside of the templating formatter
833 843 # framework. No point in repeating ourselves in that case.
834 844 if not fm.isplain():
835 845 fm.condwrite(
836 846 ui.verbose, b'files_added', _(b'Files added: %d\n'), added
837 847 )
838 848 fm.condwrite(
839 849 ui.verbose, b'files_dropped', _(b'Files dropped: %d\n'), dropped
840 850 )
841 851 fm.condwrite(
842 852 ui.verbose,
843 853 b'files_conflicting',
844 854 _(b'Files conflicting: %d\n'),
845 855 conflicting,
846 856 )
General Comments 0
You need to be logged in to leave comments. Login now