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