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