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