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