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