##// END OF EJS Templates
sparse: override __repr__ in matchers...
Martin von Zweigbergk -
r33315:d2d4b210 default
parent child Browse files
Show More
@@ -1,1001 +1,989
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 sparse.addtemporaryincludes(repo, 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 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._sparsesignaturecache
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 sparsematch(self, *revs, **kwargs):
449 449 """Returns the sparse match function for the given revs.
450 450
451 451 If multiple revs are specified, the match function is the union
452 452 of all the revs.
453 453
454 454 `includetemp` is used to indicate if the temporarily included file
455 455 should be part of the matcher.
456 456 """
457 457 if not revs or revs == (None,):
458 458 revs = [self.changelog.rev(node) for node in
459 459 self.dirstate.parents() if node != nullid]
460 460
461 461 includetemp = kwargs.get('includetemp', True)
462 462 signature = self._sparsesignature(includetemp=includetemp)
463 463
464 464 key = '%s %s' % (str(signature), ' '.join([str(r) for r in revs]))
465 465
466 466 result = self._sparsematchercache.get(key, None)
467 467 if result:
468 468 return result
469 469
470 470 matchers = []
471 471 for rev in revs:
472 472 try:
473 473 includes, excludes, profiles = sparse.patternsforrev(
474 474 self, rev)
475 475
476 476 if includes or excludes:
477 477 # Explicitly include subdirectories of includes so
478 478 # status will walk them down to the actual include.
479 479 subdirs = set()
480 480 for include in includes:
481 481 dirname = os.path.dirname(include)
482 482 # basename is used to avoid issues with absolute
483 483 # paths (which on Windows can include the drive).
484 484 while os.path.basename(dirname):
485 485 subdirs.add(dirname)
486 486 dirname = os.path.dirname(dirname)
487 487
488 488 matcher = matchmod.match(self.root, '', [],
489 489 include=includes, exclude=excludes,
490 490 default='relpath')
491 491 if subdirs:
492 492 matcher = forceincludematcher(matcher, subdirs)
493 493 matchers.append(matcher)
494 494 except IOError:
495 495 pass
496 496
497 497 result = None
498 498 if not matchers:
499 499 result = matchmod.always(self.root, '')
500 500 elif len(matchers) == 1:
501 501 result = matchers[0]
502 502 else:
503 503 result = unionmatcher(matchers)
504 504
505 505 if kwargs.get('includetemp', True):
506 506 tempincludes = sparse.readtemporaryincludes(self)
507 507 result = forceincludematcher(result, tempincludes)
508 508
509 509 self._sparsematchercache[key] = result
510 510
511 511 return result
512 512
513 513 def prunetemporaryincludes(self):
514 514 if repo.vfs.exists('tempsparse'):
515 515 origstatus = self.status()
516 516 modified, added, removed, deleted, a, b, c = origstatus
517 517 if modified or added or removed or deleted:
518 518 # Still have pending changes. Don't bother trying to prune.
519 519 return
520 520
521 521 sparsematch = self.sparsematch(includetemp=False)
522 522 dirstate = self.dirstate
523 523 actions = []
524 524 dropped = []
525 525 tempincludes = sparse.readtemporaryincludes(self)
526 526 for file in tempincludes:
527 527 if file in dirstate and not sparsematch(file):
528 528 message = 'dropping temporarily included sparse files'
529 529 actions.append((file, None, message))
530 530 dropped.append(file)
531 531
532 532 typeactions = collections.defaultdict(list)
533 533 typeactions['r'] = actions
534 534 mergemod.applyupdates(self, typeactions, self[None], self['.'],
535 535 False)
536 536
537 537 # Fix dirstate
538 538 for file in dropped:
539 539 dirstate.drop(file)
540 540
541 541 self.vfs.unlink('tempsparse')
542 542 sparse.invalidatesignaturecache(self)
543 543 msg = _("cleaned up %d temporarily added file(s) from the "
544 544 "sparse checkout\n")
545 545 ui.status(msg % len(tempincludes))
546 546
547 547 if 'dirstate' in repo._filecache:
548 548 repo.dirstate.repo = repo
549 549
550 550 repo.__class__ = SparseRepo
551 551
552 552 @command('^debugsparse', [
553 553 ('I', 'include', False, _('include files in the sparse checkout')),
554 554 ('X', 'exclude', False, _('exclude files in the sparse checkout')),
555 555 ('d', 'delete', False, _('delete an include/exclude rule')),
556 556 ('f', 'force', False, _('allow changing rules even with pending changes')),
557 557 ('', 'enable-profile', False, _('enables the specified profile')),
558 558 ('', 'disable-profile', False, _('disables the specified profile')),
559 559 ('', 'import-rules', False, _('imports rules from a file')),
560 560 ('', 'clear-rules', False, _('clears local include/exclude rules')),
561 561 ('', 'refresh', False, _('updates the working after sparseness changes')),
562 562 ('', 'reset', False, _('makes the repo full again')),
563 563 ] + commands.templateopts,
564 564 _('[--OPTION] PATTERN...'))
565 565 def debugsparse(ui, repo, *pats, **opts):
566 566 """make the current checkout sparse, or edit the existing checkout
567 567
568 568 The sparse command is used to make the current checkout sparse.
569 569 This means files that don't meet the sparse condition will not be
570 570 written to disk, or show up in any working copy operations. It does
571 571 not affect files in history in any way.
572 572
573 573 Passing no arguments prints the currently applied sparse rules.
574 574
575 575 --include and --exclude are used to add and remove files from the sparse
576 576 checkout. The effects of adding an include or exclude rule are applied
577 577 immediately. If applying the new rule would cause a file with pending
578 578 changes to be added or removed, the command will fail. Pass --force to
579 579 force a rule change even with pending changes (the changes on disk will
580 580 be preserved).
581 581
582 582 --delete removes an existing include/exclude rule. The effects are
583 583 immediate.
584 584
585 585 --refresh refreshes the files on disk based on the sparse rules. This is
586 586 only necessary if .hg/sparse was changed by hand.
587 587
588 588 --enable-profile and --disable-profile accept a path to a .hgsparse file.
589 589 This allows defining sparse checkouts and tracking them inside the
590 590 repository. This is useful for defining commonly used sparse checkouts for
591 591 many people to use. As the profile definition changes over time, the sparse
592 592 checkout will automatically be updated appropriately, depending on which
593 593 changeset is checked out. Changes to .hgsparse are not applied until they
594 594 have been committed.
595 595
596 596 --import-rules accepts a path to a file containing rules in the .hgsparse
597 597 format, allowing you to add --include, --exclude and --enable-profile rules
598 598 in bulk. Like the --include, --exclude and --enable-profile switches, the
599 599 changes are applied immediately.
600 600
601 601 --clear-rules removes all local include and exclude rules, while leaving
602 602 any enabled profiles in place.
603 603
604 604 Returns 0 if editing the sparse checkout succeeds.
605 605 """
606 606 include = opts.get('include')
607 607 exclude = opts.get('exclude')
608 608 force = opts.get('force')
609 609 enableprofile = opts.get('enable_profile')
610 610 disableprofile = opts.get('disable_profile')
611 611 importrules = opts.get('import_rules')
612 612 clearrules = opts.get('clear_rules')
613 613 delete = opts.get('delete')
614 614 refresh = opts.get('refresh')
615 615 reset = opts.get('reset')
616 616 count = sum([include, exclude, enableprofile, disableprofile, delete,
617 617 importrules, refresh, clearrules, reset])
618 618 if count > 1:
619 619 raise error.Abort(_("too many flags specified"))
620 620
621 621 if count == 0:
622 622 if repo.vfs.exists('sparse'):
623 623 ui.status(repo.vfs.read("sparse") + "\n")
624 624 temporaryincludes = sparse.readtemporaryincludes(repo)
625 625 if temporaryincludes:
626 626 ui.status(_("Temporarily Included Files (for merge/rebase):\n"))
627 627 ui.status(("\n".join(temporaryincludes) + "\n"))
628 628 else:
629 629 ui.status(_('repo is not sparse\n'))
630 630 return
631 631
632 632 if include or exclude or delete or reset or enableprofile or disableprofile:
633 633 _config(ui, repo, pats, opts, include=include, exclude=exclude,
634 634 reset=reset, delete=delete, enableprofile=enableprofile,
635 635 disableprofile=disableprofile, force=force)
636 636
637 637 if importrules:
638 638 _import(ui, repo, pats, opts, force=force)
639 639
640 640 if clearrules:
641 641 _clear(ui, repo, pats, force=force)
642 642
643 643 if refresh:
644 644 try:
645 645 wlock = repo.wlock()
646 646 fcounts = map(
647 647 len,
648 648 _refresh(ui, repo, repo.status(), repo.sparsematch(), force))
649 649 _verbose_output(ui, opts, 0, 0, 0, *fcounts)
650 650 finally:
651 651 wlock.release()
652 652
653 653 def _config(ui, repo, pats, opts, include=False, exclude=False, reset=False,
654 654 delete=False, enableprofile=False, disableprofile=False,
655 655 force=False):
656 656 """
657 657 Perform a sparse config update. Only one of the kwargs may be specified.
658 658 """
659 659 wlock = repo.wlock()
660 660 try:
661 661 oldsparsematch = repo.sparsematch()
662 662
663 663 raw = repo.vfs.tryread('sparse')
664 664 if raw:
665 665 oldinclude, oldexclude, oldprofiles = map(
666 666 set, sparse.parseconfig(ui, raw))
667 667 else:
668 668 oldinclude = set()
669 669 oldexclude = set()
670 670 oldprofiles = set()
671 671
672 672 try:
673 673 if reset:
674 674 newinclude = set()
675 675 newexclude = set()
676 676 newprofiles = set()
677 677 else:
678 678 newinclude = set(oldinclude)
679 679 newexclude = set(oldexclude)
680 680 newprofiles = set(oldprofiles)
681 681
682 682 oldstatus = repo.status()
683 683
684 684 if any(pat.startswith('/') for pat in pats):
685 685 ui.warn(_('warning: paths cannot start with /, ignoring: %s\n')
686 686 % ([pat for pat in pats if pat.startswith('/')]))
687 687 elif include:
688 688 newinclude.update(pats)
689 689 elif exclude:
690 690 newexclude.update(pats)
691 691 elif enableprofile:
692 692 newprofiles.update(pats)
693 693 elif disableprofile:
694 694 newprofiles.difference_update(pats)
695 695 elif delete:
696 696 newinclude.difference_update(pats)
697 697 newexclude.difference_update(pats)
698 698
699 699 sparse.writeconfig(repo, newinclude, newexclude, newprofiles)
700 700
701 701 fcounts = map(
702 702 len, _refresh(ui, repo, oldstatus, oldsparsematch, force))
703 703
704 704 profilecount = (len(newprofiles - oldprofiles) -
705 705 len(oldprofiles - newprofiles))
706 706 includecount = (len(newinclude - oldinclude) -
707 707 len(oldinclude - newinclude))
708 708 excludecount = (len(newexclude - oldexclude) -
709 709 len(oldexclude - newexclude))
710 710 _verbose_output(
711 711 ui, opts, profilecount, includecount, excludecount, *fcounts)
712 712 except Exception:
713 713 sparse.writeconfig(repo, oldinclude, oldexclude, oldprofiles)
714 714 raise
715 715 finally:
716 716 wlock.release()
717 717
718 718 def _import(ui, repo, files, opts, force=False):
719 719 with repo.wlock():
720 720 # load union of current active profile
721 721 revs = [repo.changelog.rev(node) for node in
722 722 repo.dirstate.parents() if node != nullid]
723 723
724 724 # read current configuration
725 725 raw = repo.vfs.tryread('sparse')
726 726 oincludes, oexcludes, oprofiles = sparse.parseconfig(ui, raw)
727 727 includes, excludes, profiles = map(
728 728 set, (oincludes, oexcludes, oprofiles))
729 729
730 730 # all active rules
731 731 aincludes, aexcludes, aprofiles = set(), set(), set()
732 732 for rev in revs:
733 733 rincludes, rexcludes, rprofiles = sparse.patternsforrev(repo, rev)
734 734 aincludes.update(rincludes)
735 735 aexcludes.update(rexcludes)
736 736 aprofiles.update(rprofiles)
737 737
738 738 # import rules on top; only take in rules that are not yet
739 739 # part of the active rules.
740 740 changed = False
741 741 for file in files:
742 742 with util.posixfile(util.expandpath(file)) as importfile:
743 743 iincludes, iexcludes, iprofiles = sparse.parseconfig(
744 744 ui, importfile.read())
745 745 oldsize = len(includes) + len(excludes) + len(profiles)
746 746 includes.update(iincludes - aincludes)
747 747 excludes.update(iexcludes - aexcludes)
748 748 profiles.update(set(iprofiles) - aprofiles)
749 749 if len(includes) + len(excludes) + len(profiles) > oldsize:
750 750 changed = True
751 751
752 752 profilecount = includecount = excludecount = 0
753 753 fcounts = (0, 0, 0)
754 754
755 755 if changed:
756 756 profilecount = len(profiles - aprofiles)
757 757 includecount = len(includes - aincludes)
758 758 excludecount = len(excludes - aexcludes)
759 759
760 760 oldstatus = repo.status()
761 761 oldsparsematch = repo.sparsematch()
762 762 sparse.writeconfig(repo, includes, excludes, profiles)
763 763
764 764 try:
765 765 fcounts = map(
766 766 len, _refresh(ui, repo, oldstatus, oldsparsematch, force))
767 767 except Exception:
768 768 sparse.writeconfig(repo, oincludes, oexcludes, oprofiles)
769 769 raise
770 770
771 771 _verbose_output(ui, opts, profilecount, includecount, excludecount,
772 772 *fcounts)
773 773
774 774 def _clear(ui, repo, files, force=False):
775 775 with repo.wlock():
776 776 raw = repo.vfs.tryread('sparse')
777 777 includes, excludes, profiles = sparse.parseconfig(ui, raw)
778 778
779 779 if includes or excludes:
780 780 oldstatus = repo.status()
781 781 oldsparsematch = repo.sparsematch()
782 782 sparse.writeconfig(repo, set(), set(), profiles)
783 783 _refresh(ui, repo, oldstatus, oldsparsematch, force)
784 784
785 785 def _refresh(ui, repo, origstatus, origsparsematch, force):
786 786 """Refreshes which files are on disk by comparing the old status and
787 787 sparsematch with the new sparsematch.
788 788
789 789 Will raise an exception if a file with pending changes is being excluded
790 790 or included (unless force=True).
791 791 """
792 792 modified, added, removed, deleted, unknown, ignored, clean = origstatus
793 793
794 794 # Verify there are no pending changes
795 795 pending = set()
796 796 pending.update(modified)
797 797 pending.update(added)
798 798 pending.update(removed)
799 799 sparsematch = repo.sparsematch()
800 800 abort = False
801 801 for file in pending:
802 802 if not sparsematch(file):
803 803 ui.warn(_("pending changes to '%s'\n") % file)
804 804 abort = not force
805 805 if abort:
806 806 raise error.Abort(_("could not update sparseness due to " +
807 807 "pending changes"))
808 808
809 809 # Calculate actions
810 810 dirstate = repo.dirstate
811 811 ctx = repo['.']
812 812 added = []
813 813 lookup = []
814 814 dropped = []
815 815 mf = ctx.manifest()
816 816 files = set(mf)
817 817
818 818 actions = {}
819 819
820 820 for file in files:
821 821 old = origsparsematch(file)
822 822 new = sparsematch(file)
823 823 # Add files that are newly included, or that don't exist in
824 824 # the dirstate yet.
825 825 if (new and not old) or (old and new and not file in dirstate):
826 826 fl = mf.flags(file)
827 827 if repo.wvfs.exists(file):
828 828 actions[file] = ('e', (fl,), '')
829 829 lookup.append(file)
830 830 else:
831 831 actions[file] = ('g', (fl, False), '')
832 832 added.append(file)
833 833 # Drop files that are newly excluded, or that still exist in
834 834 # the dirstate.
835 835 elif (old and not new) or (not old and not new and file in dirstate):
836 836 dropped.append(file)
837 837 if file not in pending:
838 838 actions[file] = ('r', [], '')
839 839
840 840 # Verify there are no pending changes in newly included files
841 841 abort = False
842 842 for file in lookup:
843 843 ui.warn(_("pending changes to '%s'\n") % file)
844 844 abort = not force
845 845 if abort:
846 846 raise error.Abort(_("cannot change sparseness due to " +
847 847 "pending changes (delete the files or use --force " +
848 848 "to bring them back dirty)"))
849 849
850 850 # Check for files that were only in the dirstate.
851 851 for file, state in dirstate.iteritems():
852 852 if not file in files:
853 853 old = origsparsematch(file)
854 854 new = sparsematch(file)
855 855 if old and not new:
856 856 dropped.append(file)
857 857
858 858 # Apply changes to disk
859 859 typeactions = dict((m, []) for m in 'a f g am cd dc r dm dg m e k'.split())
860 860 for f, (m, args, msg) in actions.iteritems():
861 861 if m not in typeactions:
862 862 typeactions[m] = []
863 863 typeactions[m].append((f, args, msg))
864 864 mergemod.applyupdates(repo, typeactions, repo[None], repo['.'], False)
865 865
866 866 # Fix dirstate
867 867 for file in added:
868 868 dirstate.normal(file)
869 869
870 870 for file in dropped:
871 871 dirstate.drop(file)
872 872
873 873 for file in lookup:
874 874 # File exists on disk, and we're bringing it back in an unknown state.
875 875 dirstate.normallookup(file)
876 876
877 877 return added, dropped, lookup
878 878
879 879 def _verbose_output(ui, opts, profilecount, includecount, excludecount, added,
880 880 dropped, lookup):
881 881 """Produce --verbose and templatable output
882 882
883 883 This specifically enables -Tjson, providing machine-readable stats on how
884 884 the sparse profile changed.
885 885
886 886 """
887 887 with ui.formatter('sparse', opts) as fm:
888 888 fm.startitem()
889 889 fm.condwrite(ui.verbose, 'profiles_added', 'Profile # change: %d\n',
890 890 profilecount)
891 891 fm.condwrite(ui.verbose, 'include_rules_added',
892 892 'Include rule # change: %d\n', includecount)
893 893 fm.condwrite(ui.verbose, 'exclude_rules_added',
894 894 'Exclude rule # change: %d\n', excludecount)
895 895 # In 'plain' verbose mode, mergemod.applyupdates already outputs what
896 896 # files are added or removed outside of the templating formatter
897 897 # framework. No point in repeating ourselves in that case.
898 898 if not fm.isplain():
899 899 fm.condwrite(ui.verbose, 'files_added', 'Files added: %d\n',
900 900 added)
901 901 fm.condwrite(ui.verbose, 'files_dropped', 'Files dropped: %d\n',
902 902 dropped)
903 903 fm.condwrite(ui.verbose, 'files_conflicting',
904 904 'Files conflicting: %d\n', lookup)
905 905
906 906 class forceincludematcher(object):
907 907 """A matcher that returns true for any of the forced includes before testing
908 908 against the actual matcher."""
909 909 def __init__(self, matcher, includes):
910 910 self._matcher = matcher
911 911 self._includes = includes
912 912
913 913 def __call__(self, value):
914 914 return value in self._includes or self._matcher(value)
915 915
916 916 def always(self):
917 917 return False
918 918
919 919 def files(self):
920 920 return []
921 921
922 922 def isexact(self):
923 923 return False
924 924
925 925 def anypats(self):
926 926 return True
927 927
928 928 def prefix(self):
929 929 return False
930 930
931 def hash(self):
932 sha1 = hashlib.sha1()
933 sha1.update(_hashmatcher(self._matcher))
934 for include in sorted(self._includes):
935 sha1.update(include + '\0')
936 return sha1.hexdigest()
931 def __repr__(self):
932 return ('<forceincludematcher matcher=%r, includes=%r>' %
933 (self._matcher, sorted(self._includes)))
937 934
938 935 class unionmatcher(object):
939 936 """A matcher that is the union of several matchers."""
940 937 def __init__(self, matchers):
941 938 self._matchers = matchers
942 939
943 940 def __call__(self, value):
944 941 for match in self._matchers:
945 942 if match(value):
946 943 return True
947 944 return False
948 945
949 946 def always(self):
950 947 return False
951 948
952 949 def files(self):
953 950 return []
954 951
955 952 def isexact(self):
956 953 return False
957 954
958 955 def anypats(self):
959 956 return True
960 957
961 958 def prefix(self):
962 959 return False
963 960
964 def hash(self):
965 sha1 = hashlib.sha1()
966 for m in self._matchers:
967 sha1.update(_hashmatcher(m))
968 return sha1.hexdigest()
961 def __repr__(self):
962 return ('<unionmatcher matchers=%r>' % self._matchers)
969 963
970 964 class negatematcher(object):
971 965 def __init__(self, matcher):
972 966 self._matcher = matcher
973 967
974 968 def __call__(self, value):
975 969 return not self._matcher(value)
976 970
977 971 def always(self):
978 972 return False
979 973
980 974 def files(self):
981 975 return []
982 976
983 977 def isexact(self):
984 978 return False
985 979
986 980 def anypats(self):
987 981 return True
988 982
989 def hash(self):
990 sha1 = hashlib.sha1()
991 sha1.update('negate')
992 sha1.update(_hashmatcher(self._matcher))
993 return sha1.hexdigest()
983 def __repr__(self):
984 return ('<negatematcher matcher=%r>' % self._matcher)
994 985
995 986 def _hashmatcher(matcher):
996 if util.safehasattr(matcher, 'hash'):
997 return matcher.hash()
998
999 987 sha1 = hashlib.sha1()
1000 988 sha1.update(repr(matcher))
1001 989 return sha1.hexdigest()
General Comments 0
You need to be logged in to leave comments. Login now