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