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