##// END OF EJS Templates
sparse: --include 'dir1/dir2' should not include 'dir1/*'...
Hollis Blanchard -
r35760:7a1806e0 default
parent child Browse files
Show More
@@ -1,708 +1,693 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 import collections
11 11 import hashlib
12 12 import os
13 13
14 14 from .i18n import _
15 15 from .node import (
16 16 hex,
17 17 nullid,
18 18 )
19 19 from . import (
20 20 error,
21 21 match as matchmod,
22 22 merge as mergemod,
23 23 pathutil,
24 24 pycompat,
25 25 scmutil,
26 26 util,
27 27 )
28 28
29 29 # Whether sparse features are enabled. This variable is intended to be
30 30 # temporary to facilitate porting sparse to core. It should eventually be
31 31 # a per-repo option, possibly a repo requirement.
32 32 enabled = False
33 33
34 34 def parseconfig(ui, raw):
35 35 """Parse sparse config file content.
36 36
37 37 Returns a tuple of includes, excludes, and profiles.
38 38 """
39 39 includes = set()
40 40 excludes = set()
41 41 profiles = set()
42 42 current = None
43 43 havesection = False
44 44
45 45 for line in raw.split('\n'):
46 46 line = line.strip()
47 47 if not line or line.startswith('#'):
48 48 # empty or comment line, skip
49 49 continue
50 50 elif line.startswith('%include '):
51 51 line = line[9:].strip()
52 52 if line:
53 53 profiles.add(line)
54 54 elif line == '[include]':
55 55 if havesection and current != includes:
56 56 # TODO pass filename into this API so we can report it.
57 57 raise error.Abort(_('sparse config cannot have includes ' +
58 58 'after excludes'))
59 59 havesection = True
60 60 current = includes
61 61 continue
62 62 elif line == '[exclude]':
63 63 havesection = True
64 64 current = excludes
65 65 elif line:
66 66 if current is None:
67 67 raise error.Abort(_('sparse config entry outside of '
68 68 'section: %s') % line,
69 69 hint=_('add an [include] or [exclude] line '
70 70 'to declare the entry type'))
71 71
72 72 if line.strip().startswith('/'):
73 73 ui.warn(_('warning: sparse profile cannot use' +
74 74 ' paths starting with /, ignoring %s\n') % line)
75 75 continue
76 76 current.add(line)
77 77
78 78 return includes, excludes, profiles
79 79
80 80 # Exists as separate function to facilitate monkeypatching.
81 81 def readprofile(repo, profile, changeid):
82 82 """Resolve the raw content of a sparse profile file."""
83 83 # TODO add some kind of cache here because this incurs a manifest
84 84 # resolve and can be slow.
85 85 return repo.filectx(profile, changeid=changeid).data()
86 86
87 87 def patternsforrev(repo, rev):
88 88 """Obtain sparse checkout patterns for the given rev.
89 89
90 90 Returns a tuple of iterables representing includes, excludes, and
91 91 patterns.
92 92 """
93 93 # Feature isn't enabled. No-op.
94 94 if not enabled:
95 95 return set(), set(), set()
96 96
97 97 raw = repo.vfs.tryread('sparse')
98 98 if not raw:
99 99 return set(), set(), set()
100 100
101 101 if rev is None:
102 102 raise error.Abort(_('cannot parse sparse patterns from working '
103 103 'directory'))
104 104
105 105 includes, excludes, profiles = parseconfig(repo.ui, raw)
106 106 ctx = repo[rev]
107 107
108 108 if profiles:
109 109 visited = set()
110 110 while profiles:
111 111 profile = profiles.pop()
112 112 if profile in visited:
113 113 continue
114 114
115 115 visited.add(profile)
116 116
117 117 try:
118 118 raw = readprofile(repo, profile, rev)
119 119 except error.ManifestLookupError:
120 120 msg = (
121 121 "warning: sparse profile '%s' not found "
122 122 "in rev %s - ignoring it\n" % (profile, ctx))
123 123 # experimental config: sparse.missingwarning
124 124 if repo.ui.configbool(
125 125 'sparse', 'missingwarning'):
126 126 repo.ui.warn(msg)
127 127 else:
128 128 repo.ui.debug(msg)
129 129 continue
130 130
131 131 pincludes, pexcludes, subprofs = parseconfig(repo.ui, raw)
132 132 includes.update(pincludes)
133 133 excludes.update(pexcludes)
134 134 profiles.update(subprofs)
135 135
136 136 profiles = visited
137 137
138 138 if includes:
139 139 includes.add('.hg*')
140 140
141 141 return includes, excludes, profiles
142 142
143 143 def activeconfig(repo):
144 144 """Determine the active sparse config rules.
145 145
146 146 Rules are constructed by reading the current sparse config and bringing in
147 147 referenced profiles from parents of the working directory.
148 148 """
149 149 revs = [repo.changelog.rev(node) for node in
150 150 repo.dirstate.parents() if node != nullid]
151 151
152 152 allincludes = set()
153 153 allexcludes = set()
154 154 allprofiles = set()
155 155
156 156 for rev in revs:
157 157 includes, excludes, profiles = patternsforrev(repo, rev)
158 158 allincludes |= includes
159 159 allexcludes |= excludes
160 160 allprofiles |= profiles
161 161
162 162 return allincludes, allexcludes, allprofiles
163 163
164 164 def configsignature(repo, includetemp=True):
165 165 """Obtain the signature string for the current sparse configuration.
166 166
167 167 This is used to construct a cache key for matchers.
168 168 """
169 169 cache = repo._sparsesignaturecache
170 170
171 171 signature = cache.get('signature')
172 172
173 173 if includetemp:
174 174 tempsignature = cache.get('tempsignature')
175 175 else:
176 176 tempsignature = '0'
177 177
178 178 if signature is None or (includetemp and tempsignature is None):
179 179 signature = hex(hashlib.sha1(repo.vfs.tryread('sparse')).digest())
180 180 cache['signature'] = signature
181 181
182 182 if includetemp:
183 183 raw = repo.vfs.tryread('tempsparse')
184 184 tempsignature = hex(hashlib.sha1(raw).digest())
185 185 cache['tempsignature'] = tempsignature
186 186
187 187 return '%s %s' % (signature, tempsignature)
188 188
189 189 def writeconfig(repo, includes, excludes, profiles):
190 190 """Write the sparse config file given a sparse configuration."""
191 191 with repo.vfs('sparse', 'wb') as fh:
192 192 for p in sorted(profiles):
193 193 fh.write('%%include %s\n' % p)
194 194
195 195 if includes:
196 196 fh.write('[include]\n')
197 197 for i in sorted(includes):
198 198 fh.write(i)
199 199 fh.write('\n')
200 200
201 201 if excludes:
202 202 fh.write('[exclude]\n')
203 203 for e in sorted(excludes):
204 204 fh.write(e)
205 205 fh.write('\n')
206 206
207 207 repo._sparsesignaturecache.clear()
208 208
209 209 def readtemporaryincludes(repo):
210 210 raw = repo.vfs.tryread('tempsparse')
211 211 if not raw:
212 212 return set()
213 213
214 214 return set(raw.split('\n'))
215 215
216 216 def writetemporaryincludes(repo, includes):
217 217 repo.vfs.write('tempsparse', '\n'.join(sorted(includes)))
218 218 repo._sparsesignaturecache.clear()
219 219
220 220 def addtemporaryincludes(repo, additional):
221 221 includes = readtemporaryincludes(repo)
222 222 for i in additional:
223 223 includes.add(i)
224 224 writetemporaryincludes(repo, includes)
225 225
226 226 def prunetemporaryincludes(repo):
227 227 if not enabled or not repo.vfs.exists('tempsparse'):
228 228 return
229 229
230 230 s = repo.status()
231 231 if s.modified or s.added or s.removed or s.deleted:
232 232 # Still have pending changes. Don't bother trying to prune.
233 233 return
234 234
235 235 sparsematch = matcher(repo, includetemp=False)
236 236 dirstate = repo.dirstate
237 237 actions = []
238 238 dropped = []
239 239 tempincludes = readtemporaryincludes(repo)
240 240 for file in tempincludes:
241 241 if file in dirstate and not sparsematch(file):
242 242 message = _('dropping temporarily included sparse files')
243 243 actions.append((file, None, message))
244 244 dropped.append(file)
245 245
246 246 typeactions = collections.defaultdict(list)
247 247 typeactions['r'] = actions
248 248 mergemod.applyupdates(repo, typeactions, repo[None], repo['.'], False)
249 249
250 250 # Fix dirstate
251 251 for file in dropped:
252 252 dirstate.drop(file)
253 253
254 254 repo.vfs.unlink('tempsparse')
255 255 repo._sparsesignaturecache.clear()
256 256 msg = _('cleaned up %d temporarily added file(s) from the '
257 257 'sparse checkout\n')
258 258 repo.ui.status(msg % len(tempincludes))
259 259
260 260 def forceincludematcher(matcher, includes):
261 261 """Returns a matcher that returns true for any of the forced includes
262 262 before testing against the actual matcher."""
263 263 kindpats = [('path', include, '') for include in includes]
264 264 includematcher = matchmod.includematcher('', '', kindpats)
265 265 return matchmod.unionmatcher([includematcher, matcher])
266 266
267 267 def matcher(repo, revs=None, includetemp=True):
268 268 """Obtain a matcher for sparse working directories for the given revs.
269 269
270 270 If multiple revisions are specified, the matcher is the union of all
271 271 revs.
272 272
273 273 ``includetemp`` indicates whether to use the temporary sparse profile.
274 274 """
275 275 # If sparse isn't enabled, sparse matcher matches everything.
276 276 if not enabled:
277 277 return matchmod.always(repo.root, '')
278 278
279 279 if not revs or revs == [None]:
280 280 revs = [repo.changelog.rev(node)
281 281 for node in repo.dirstate.parents() if node != nullid]
282 282
283 283 signature = configsignature(repo, includetemp=includetemp)
284 284
285 285 key = '%s %s' % (signature, ' '.join(map(pycompat.bytestr, revs)))
286 286
287 287 result = repo._sparsematchercache.get(key)
288 288 if result:
289 289 return result
290 290
291 291 matchers = []
292 292 for rev in revs:
293 293 try:
294 294 includes, excludes, profiles = patternsforrev(repo, rev)
295 295
296 296 if includes or excludes:
297 # Explicitly include subdirectories of includes so
298 # status will walk them down to the actual include.
299 subdirs = set()
300 for include in includes:
301 # TODO consider using posix path functions here so Windows
302 # \ directory separators don't come into play.
303 dirname = os.path.dirname(include)
304 # basename is used to avoid issues with absolute
305 # paths (which on Windows can include the drive).
306 while os.path.basename(dirname):
307 subdirs.add(dirname)
308 dirname = os.path.dirname(dirname)
309
310 297 matcher = matchmod.match(repo.root, '', [],
311 298 include=includes, exclude=excludes,
312 299 default='relpath')
313 if subdirs:
314 matcher = forceincludematcher(matcher, subdirs)
315 300 matchers.append(matcher)
316 301 except IOError:
317 302 pass
318 303
319 304 if not matchers:
320 305 result = matchmod.always(repo.root, '')
321 306 elif len(matchers) == 1:
322 307 result = matchers[0]
323 308 else:
324 309 result = matchmod.unionmatcher(matchers)
325 310
326 311 if includetemp:
327 312 tempincludes = readtemporaryincludes(repo)
328 313 result = forceincludematcher(result, tempincludes)
329 314
330 315 repo._sparsematchercache[key] = result
331 316
332 317 return result
333 318
334 319 def filterupdatesactions(repo, wctx, mctx, branchmerge, actions):
335 320 """Filter updates to only lay out files that match the sparse rules."""
336 321 if not enabled:
337 322 return actions
338 323
339 324 oldrevs = [pctx.rev() for pctx in wctx.parents()]
340 325 oldsparsematch = matcher(repo, oldrevs)
341 326
342 327 if oldsparsematch.always():
343 328 return actions
344 329
345 330 files = set()
346 331 prunedactions = {}
347 332
348 333 if branchmerge:
349 334 # If we're merging, use the wctx filter, since we're merging into
350 335 # the wctx.
351 336 sparsematch = matcher(repo, [wctx.parents()[0].rev()])
352 337 else:
353 338 # If we're updating, use the target context's filter, since we're
354 339 # moving to the target context.
355 340 sparsematch = matcher(repo, [mctx.rev()])
356 341
357 342 temporaryfiles = []
358 343 for file, action in actions.iteritems():
359 344 type, args, msg = action
360 345 files.add(file)
361 346 if sparsematch(file):
362 347 prunedactions[file] = action
363 348 elif type == 'm':
364 349 temporaryfiles.append(file)
365 350 prunedactions[file] = action
366 351 elif branchmerge:
367 352 if type != 'k':
368 353 temporaryfiles.append(file)
369 354 prunedactions[file] = action
370 355 elif type == 'f':
371 356 prunedactions[file] = action
372 357 elif file in wctx:
373 358 prunedactions[file] = ('r', args, msg)
374 359
375 360 if len(temporaryfiles) > 0:
376 361 repo.ui.status(_('temporarily included %d file(s) in the sparse '
377 362 'checkout for merging\n') % len(temporaryfiles))
378 363 addtemporaryincludes(repo, temporaryfiles)
379 364
380 365 # Add the new files to the working copy so they can be merged, etc
381 366 actions = []
382 367 message = 'temporarily adding to sparse checkout'
383 368 wctxmanifest = repo[None].manifest()
384 369 for file in temporaryfiles:
385 370 if file in wctxmanifest:
386 371 fctx = repo[None][file]
387 372 actions.append((file, (fctx.flags(), False), message))
388 373
389 374 typeactions = collections.defaultdict(list)
390 375 typeactions['g'] = actions
391 376 mergemod.applyupdates(repo, typeactions, repo[None], repo['.'],
392 377 False)
393 378
394 379 dirstate = repo.dirstate
395 380 for file, flags, msg in actions:
396 381 dirstate.normal(file)
397 382
398 383 profiles = activeconfig(repo)[2]
399 384 changedprofiles = profiles & files
400 385 # If an active profile changed during the update, refresh the checkout.
401 386 # Don't do this during a branch merge, since all incoming changes should
402 387 # have been handled by the temporary includes above.
403 388 if changedprofiles and not branchmerge:
404 389 mf = mctx.manifest()
405 390 for file in mf:
406 391 old = oldsparsematch(file)
407 392 new = sparsematch(file)
408 393 if not old and new:
409 394 flags = mf.flags(file)
410 395 prunedactions[file] = ('g', (flags, False), '')
411 396 elif old and not new:
412 397 prunedactions[file] = ('r', [], '')
413 398
414 399 return prunedactions
415 400
416 401 def refreshwdir(repo, origstatus, origsparsematch, force=False):
417 402 """Refreshes working directory by taking sparse config into account.
418 403
419 404 The old status and sparse matcher is compared against the current sparse
420 405 matcher.
421 406
422 407 Will abort if a file with pending changes is being excluded or included
423 408 unless ``force`` is True.
424 409 """
425 410 # Verify there are no pending changes
426 411 pending = set()
427 412 pending.update(origstatus.modified)
428 413 pending.update(origstatus.added)
429 414 pending.update(origstatus.removed)
430 415 sparsematch = matcher(repo)
431 416 abort = False
432 417
433 418 for f in pending:
434 419 if not sparsematch(f):
435 420 repo.ui.warn(_("pending changes to '%s'\n") % f)
436 421 abort = not force
437 422
438 423 if abort:
439 424 raise error.Abort(_('could not update sparseness due to pending '
440 425 'changes'))
441 426
442 427 # Calculate actions
443 428 dirstate = repo.dirstate
444 429 ctx = repo['.']
445 430 added = []
446 431 lookup = []
447 432 dropped = []
448 433 mf = ctx.manifest()
449 434 files = set(mf)
450 435
451 436 actions = {}
452 437
453 438 for file in files:
454 439 old = origsparsematch(file)
455 440 new = sparsematch(file)
456 441 # Add files that are newly included, or that don't exist in
457 442 # the dirstate yet.
458 443 if (new and not old) or (old and new and not file in dirstate):
459 444 fl = mf.flags(file)
460 445 if repo.wvfs.exists(file):
461 446 actions[file] = ('e', (fl,), '')
462 447 lookup.append(file)
463 448 else:
464 449 actions[file] = ('g', (fl, False), '')
465 450 added.append(file)
466 451 # Drop files that are newly excluded, or that still exist in
467 452 # the dirstate.
468 453 elif (old and not new) or (not old and not new and file in dirstate):
469 454 dropped.append(file)
470 455 if file not in pending:
471 456 actions[file] = ('r', [], '')
472 457
473 458 # Verify there are no pending changes in newly included files
474 459 abort = False
475 460 for file in lookup:
476 461 repo.ui.warn(_("pending changes to '%s'\n") % file)
477 462 abort = not force
478 463 if abort:
479 464 raise error.Abort(_('cannot change sparseness due to pending '
480 465 'changes (delete the files or use '
481 466 '--force to bring them back dirty)'))
482 467
483 468 # Check for files that were only in the dirstate.
484 469 for file, state in dirstate.iteritems():
485 470 if not file in files:
486 471 old = origsparsematch(file)
487 472 new = sparsematch(file)
488 473 if old and not new:
489 474 dropped.append(file)
490 475
491 476 # Apply changes to disk
492 477 typeactions = dict((m, [])
493 478 for m in 'a f g am cd dc r dm dg m e k p pr'.split())
494 479 for f, (m, args, msg) in actions.iteritems():
495 480 if m not in typeactions:
496 481 typeactions[m] = []
497 482 typeactions[m].append((f, args, msg))
498 483
499 484 mergemod.applyupdates(repo, typeactions, repo[None], repo['.'], False)
500 485
501 486 # Fix dirstate
502 487 for file in added:
503 488 dirstate.normal(file)
504 489
505 490 for file in dropped:
506 491 dirstate.drop(file)
507 492
508 493 for file in lookup:
509 494 # File exists on disk, and we're bringing it back in an unknown state.
510 495 dirstate.normallookup(file)
511 496
512 497 return added, dropped, lookup
513 498
514 499 def aftercommit(repo, node):
515 500 """Perform actions after a working directory commit."""
516 501 # This function is called unconditionally, even if sparse isn't
517 502 # enabled.
518 503 ctx = repo[node]
519 504
520 505 profiles = patternsforrev(repo, ctx.rev())[2]
521 506
522 507 # profiles will only have data if sparse is enabled.
523 508 if profiles & set(ctx.files()):
524 509 origstatus = repo.status()
525 510 origsparsematch = matcher(repo)
526 511 refreshwdir(repo, origstatus, origsparsematch, force=True)
527 512
528 513 prunetemporaryincludes(repo)
529 514
530 515 def _updateconfigandrefreshwdir(repo, includes, excludes, profiles,
531 516 force=False, removing=False):
532 517 """Update the sparse config and working directory state."""
533 518 raw = repo.vfs.tryread('sparse')
534 519 oldincludes, oldexcludes, oldprofiles = parseconfig(repo.ui, raw)
535 520
536 521 oldstatus = repo.status()
537 522 oldmatch = matcher(repo)
538 523 oldrequires = set(repo.requirements)
539 524
540 525 # TODO remove this try..except once the matcher integrates better
541 526 # with dirstate. We currently have to write the updated config
542 527 # because that will invalidate the matcher cache and force a
543 528 # re-read. We ideally want to update the cached matcher on the
544 529 # repo instance then flush the new config to disk once wdir is
545 530 # updated. But this requires massive rework to matcher() and its
546 531 # consumers.
547 532
548 533 if 'exp-sparse' in oldrequires and removing:
549 534 repo.requirements.discard('exp-sparse')
550 535 scmutil.writerequires(repo.vfs, repo.requirements)
551 536 elif 'exp-sparse' not in oldrequires:
552 537 repo.requirements.add('exp-sparse')
553 538 scmutil.writerequires(repo.vfs, repo.requirements)
554 539
555 540 try:
556 541 writeconfig(repo, includes, excludes, profiles)
557 542 return refreshwdir(repo, oldstatus, oldmatch, force=force)
558 543 except Exception:
559 544 if repo.requirements != oldrequires:
560 545 repo.requirements.clear()
561 546 repo.requirements |= oldrequires
562 547 scmutil.writerequires(repo.vfs, repo.requirements)
563 548 writeconfig(repo, oldincludes, oldexcludes, oldprofiles)
564 549 raise
565 550
566 551 def clearrules(repo, force=False):
567 552 """Clears include/exclude rules from the sparse config.
568 553
569 554 The remaining sparse config only has profiles, if defined. The working
570 555 directory is refreshed, as needed.
571 556 """
572 557 with repo.wlock():
573 558 raw = repo.vfs.tryread('sparse')
574 559 includes, excludes, profiles = parseconfig(repo.ui, raw)
575 560
576 561 if not includes and not excludes:
577 562 return
578 563
579 564 _updateconfigandrefreshwdir(repo, set(), set(), profiles, force=force)
580 565
581 566 def importfromfiles(repo, opts, paths, force=False):
582 567 """Import sparse config rules from files.
583 568
584 569 The updated sparse config is written out and the working directory
585 570 is refreshed, as needed.
586 571 """
587 572 with repo.wlock():
588 573 # read current configuration
589 574 raw = repo.vfs.tryread('sparse')
590 575 includes, excludes, profiles = parseconfig(repo.ui, raw)
591 576 aincludes, aexcludes, aprofiles = activeconfig(repo)
592 577
593 578 # Import rules on top; only take in rules that are not yet
594 579 # part of the active rules.
595 580 changed = False
596 581 for p in paths:
597 582 with util.posixfile(util.expandpath(p)) as fh:
598 583 raw = fh.read()
599 584
600 585 iincludes, iexcludes, iprofiles = parseconfig(repo.ui, raw)
601 586 oldsize = len(includes) + len(excludes) + len(profiles)
602 587 includes.update(iincludes - aincludes)
603 588 excludes.update(iexcludes - aexcludes)
604 589 profiles.update(iprofiles - aprofiles)
605 590 if len(includes) + len(excludes) + len(profiles) > oldsize:
606 591 changed = True
607 592
608 593 profilecount = includecount = excludecount = 0
609 594 fcounts = (0, 0, 0)
610 595
611 596 if changed:
612 597 profilecount = len(profiles - aprofiles)
613 598 includecount = len(includes - aincludes)
614 599 excludecount = len(excludes - aexcludes)
615 600
616 601 fcounts = map(len, _updateconfigandrefreshwdir(
617 602 repo, includes, excludes, profiles, force=force))
618 603
619 604 printchanges(repo.ui, opts, profilecount, includecount, excludecount,
620 605 *fcounts)
621 606
622 607 def updateconfig(repo, pats, opts, include=False, exclude=False, reset=False,
623 608 delete=False, enableprofile=False, disableprofile=False,
624 609 force=False, usereporootpaths=False):
625 610 """Perform a sparse config update.
626 611
627 612 Only one of the actions may be performed.
628 613
629 614 The new config is written out and a working directory refresh is performed.
630 615 """
631 616 with repo.wlock():
632 617 raw = repo.vfs.tryread('sparse')
633 618 oldinclude, oldexclude, oldprofiles = parseconfig(repo.ui, raw)
634 619
635 620 if reset:
636 621 newinclude = set()
637 622 newexclude = set()
638 623 newprofiles = set()
639 624 else:
640 625 newinclude = set(oldinclude)
641 626 newexclude = set(oldexclude)
642 627 newprofiles = set(oldprofiles)
643 628
644 629 if any(os.path.isabs(pat) for pat in pats):
645 630 raise error.Abort(_('paths cannot be absolute'))
646 631
647 632 if not usereporootpaths:
648 633 # let's treat paths as relative to cwd
649 634 root, cwd = repo.root, repo.getcwd()
650 635 abspats = []
651 636 for kindpat in pats:
652 637 kind, pat = matchmod._patsplit(kindpat, None)
653 638 if kind in matchmod.cwdrelativepatternkinds or kind is None:
654 639 ap = (kind + ':' if kind else '') +\
655 640 pathutil.canonpath(root, cwd, pat)
656 641 abspats.append(ap)
657 642 else:
658 643 abspats.append(kindpat)
659 644 pats = abspats
660 645
661 646 if include:
662 647 newinclude.update(pats)
663 648 elif exclude:
664 649 newexclude.update(pats)
665 650 elif enableprofile:
666 651 newprofiles.update(pats)
667 652 elif disableprofile:
668 653 newprofiles.difference_update(pats)
669 654 elif delete:
670 655 newinclude.difference_update(pats)
671 656 newexclude.difference_update(pats)
672 657
673 658 profilecount = (len(newprofiles - oldprofiles) -
674 659 len(oldprofiles - newprofiles))
675 660 includecount = (len(newinclude - oldinclude) -
676 661 len(oldinclude - newinclude))
677 662 excludecount = (len(newexclude - oldexclude) -
678 663 len(oldexclude - newexclude))
679 664
680 665 fcounts = map(len, _updateconfigandrefreshwdir(
681 666 repo, newinclude, newexclude, newprofiles, force=force,
682 667 removing=reset))
683 668
684 669 printchanges(repo.ui, opts, profilecount, includecount,
685 670 excludecount, *fcounts)
686 671
687 672 def printchanges(ui, opts, profilecount=0, includecount=0, excludecount=0,
688 673 added=0, dropped=0, conflicting=0):
689 674 """Print output summarizing sparse config changes."""
690 675 with ui.formatter('sparse', opts) as fm:
691 676 fm.startitem()
692 677 fm.condwrite(ui.verbose, 'profiles_added', _('Profiles changed: %d\n'),
693 678 profilecount)
694 679 fm.condwrite(ui.verbose, 'include_rules_added',
695 680 _('Include rules changed: %d\n'), includecount)
696 681 fm.condwrite(ui.verbose, 'exclude_rules_added',
697 682 _('Exclude rules changed: %d\n'), excludecount)
698 683
699 684 # In 'plain' verbose mode, mergemod.applyupdates already outputs what
700 685 # files are added or removed outside of the templating formatter
701 686 # framework. No point in repeating ourselves in that case.
702 687 if not fm.isplain():
703 688 fm.condwrite(ui.verbose, 'files_added', _('Files added: %d\n'),
704 689 added)
705 690 fm.condwrite(ui.verbose, 'files_dropped', _('Files dropped: %d\n'),
706 691 dropped)
707 692 fm.condwrite(ui.verbose, 'files_conflicting',
708 693 _('Files conflicting: %d\n'), conflicting)
@@ -1,397 +1,418 b''
1 1 test sparse
2 2
3 3 $ hg init myrepo
4 4 $ cd myrepo
5 5 $ cat > .hg/hgrc <<EOF
6 6 > [extensions]
7 7 > sparse=
8 8 > strip=
9 9 > EOF
10 10
11 11 $ echo a > show
12 12 $ echo x > hide
13 13 $ hg ci -Aqm 'initial'
14 14
15 15 $ echo b > show
16 16 $ echo y > hide
17 17 $ echo aa > show2
18 18 $ echo xx > hide2
19 19 $ hg ci -Aqm 'two'
20 20
21 21 Verify basic --include
22 22
23 23 $ hg up -q 0
24 24 $ hg debugsparse --include 'hide'
25 25 $ ls
26 26 hide
27 27
28 28 Absolute paths outside the repo should just be rejected
29 29
30 30 #if no-windows
31 31 $ hg debugsparse --include /foo/bar
32 32 abort: paths cannot be absolute
33 33 [255]
34 34 $ hg debugsparse --include '$TESTTMP/myrepo/hide'
35 35
36 36 $ hg debugsparse --include '/root'
37 37 abort: paths cannot be absolute
38 38 [255]
39 39 #else
40 40 TODO: See if this can be made to fail the same way as on Unix
41 41 $ hg debugsparse --include /c/foo/bar
42 42 abort: paths cannot be absolute
43 43 [255]
44 44 $ hg debugsparse --include '$TESTTMP/myrepo/hide'
45 45
46 46 $ hg debugsparse --include '/c/root'
47 47 abort: paths cannot be absolute
48 48 [255]
49 49 #endif
50 50
51 51 Paths should be treated as cwd-relative, not repo-root-relative
52 52 $ mkdir subdir && cd subdir
53 53 $ hg debugsparse --include path
54 54 $ hg debugsparse
55 55 [include]
56 56 $TESTTMP/myrepo/hide
57 57 hide
58 58 subdir/path
59 59
60 60 $ cd ..
61 61 $ echo hello > subdir/file2.ext
62 62 $ cd subdir
63 63 $ hg debugsparse --include '**.ext' # let us test globs
64 64 $ hg debugsparse --include 'path:abspath' # and a path: pattern
65 65 $ cd ..
66 66 $ hg debugsparse
67 67 [include]
68 68 $TESTTMP/myrepo/hide
69 69 hide
70 70 path:abspath
71 71 subdir/**.ext
72 72 subdir/path
73 73
74 74 $ rm -rf subdir
75 75
76 76 Verify commiting while sparse includes other files
77 77
78 78 $ echo z > hide
79 79 $ hg ci -Aqm 'edit hide'
80 80 $ ls
81 81 hide
82 82 $ hg manifest
83 83 hide
84 84 show
85 85
86 86 Verify --reset brings files back
87 87
88 88 $ hg debugsparse --reset
89 89 $ ls
90 90 hide
91 91 show
92 92 $ cat hide
93 93 z
94 94 $ cat show
95 95 a
96 96
97 97 Verify 'hg debugsparse' default output
98 98
99 99 $ hg up -q null
100 100 $ hg debugsparse --include 'show*'
101 101
102 102 $ hg debugsparse
103 103 [include]
104 104 show*
105 105
106 106 Verify update only writes included files
107 107
108 108 $ hg up -q 0
109 109 $ ls
110 110 show
111 111
112 112 $ hg up -q 1
113 113 $ ls
114 114 show
115 115 show2
116 116
117 117 Verify status only shows included files
118 118
119 119 $ touch hide
120 120 $ touch hide3
121 121 $ echo c > show
122 122 $ hg status
123 123 M show
124 124
125 125 Adding an excluded file should fail
126 126
127 127 $ hg add hide3
128 128 abort: cannot add 'hide3' - it is outside the sparse checkout
129 129 (include file with `hg debugsparse --include <pattern>` or use `hg add -s <file>` to include file directory while adding)
130 130 [255]
131 131
132 132 Verify deleting sparseness while a file has changes fails
133 133
134 134 $ hg debugsparse --delete 'show*'
135 135 pending changes to 'hide'
136 136 abort: cannot change sparseness due to pending changes (delete the files or use --force to bring them back dirty)
137 137 [255]
138 138
139 139 Verify deleting sparseness with --force brings back files
140 140
141 141 $ hg debugsparse --delete -f 'show*'
142 142 pending changes to 'hide'
143 143 $ ls
144 144 hide
145 145 hide2
146 146 hide3
147 147 show
148 148 show2
149 149 $ hg st
150 150 M hide
151 151 M show
152 152 ? hide3
153 153
154 154 Verify editing sparseness fails if pending changes
155 155
156 156 $ hg debugsparse --include 'show*'
157 157 pending changes to 'hide'
158 158 abort: could not update sparseness due to pending changes
159 159 [255]
160 160
161 161 Verify adding sparseness hides files
162 162
163 163 $ hg debugsparse --exclude -f 'hide*'
164 164 pending changes to 'hide'
165 165 $ ls
166 166 hide
167 167 hide3
168 168 show
169 169 show2
170 170 $ hg st
171 171 M show
172 172
173 173 $ hg up -qC .
174 174 TODO: add an option to purge to also purge files outside the sparse config?
175 175 $ hg purge --all --config extensions.purge=
176 176 $ ls
177 177 hide
178 178 hide3
179 179 show
180 180 show2
181 181 For now, manually remove the files
182 182 $ rm hide hide3
183 183
184 184 Verify rebase temporarily includes excluded files
185 185
186 186 $ hg rebase -d 1 -r 2 --config extensions.rebase=
187 187 rebasing 2:b91df4f39e75 "edit hide" (tip)
188 188 temporarily included 1 file(s) in the sparse checkout for merging
189 189 merging hide
190 190 warning: conflicts while merging hide! (edit, then use 'hg resolve --mark')
191 191 unresolved conflicts (see hg resolve, then hg rebase --continue)
192 192 [1]
193 193
194 194 $ hg debugsparse
195 195 [exclude]
196 196 hide*
197 197
198 198 Temporarily Included Files (for merge/rebase):
199 199 hide
200 200
201 201 $ cat hide
202 202 <<<<<<< dest: 39278f7c08a9 - test: two
203 203 y
204 204 =======
205 205 z
206 206 >>>>>>> source: b91df4f39e75 - test: edit hide
207 207
208 208 Verify aborting a rebase cleans up temporary files
209 209
210 210 $ hg rebase --abort --config extensions.rebase=
211 211 cleaned up 1 temporarily added file(s) from the sparse checkout
212 212 rebase aborted
213 213 $ rm hide.orig
214 214
215 215 $ ls
216 216 show
217 217 show2
218 218
219 219 Verify merge fails if merging excluded files
220 220
221 221 $ hg up -q 1
222 222 $ hg merge -r 2
223 223 temporarily included 1 file(s) in the sparse checkout for merging
224 224 merging hide
225 225 warning: conflicts while merging hide! (edit, then use 'hg resolve --mark')
226 226 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
227 227 use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
228 228 [1]
229 229 $ hg debugsparse
230 230 [exclude]
231 231 hide*
232 232
233 233 Temporarily Included Files (for merge/rebase):
234 234 hide
235 235
236 236 $ hg up -C .
237 237 cleaned up 1 temporarily added file(s) from the sparse checkout
238 238 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
239 239 $ hg debugsparse
240 240 [exclude]
241 241 hide*
242 242
243 243
244 244 Verify strip -k resets dirstate correctly
245 245
246 246 $ hg status
247 247 $ hg debugsparse
248 248 [exclude]
249 249 hide*
250 250
251 251 $ hg log -r . -T '{rev}\n' --stat
252 252 1
253 253 hide | 2 +-
254 254 hide2 | 1 +
255 255 show | 2 +-
256 256 show2 | 1 +
257 257 4 files changed, 4 insertions(+), 2 deletions(-)
258 258
259 259 $ hg strip -r . -k
260 260 saved backup bundle to $TESTTMP/myrepo/.hg/strip-backup/39278f7c08a9-ce59e002-backup.hg
261 261 $ hg status
262 262 M show
263 263 ? show2
264 264
265 265 Verify rebase succeeds if all changed files are in sparse checkout
266 266
267 267 $ hg commit -Aqm "add show2"
268 268 $ hg rebase -d 1 --config extensions.rebase=
269 269 rebasing 2:bdde55290160 "add show2" (tip)
270 270 saved backup bundle to $TESTTMP/myrepo/.hg/strip-backup/bdde55290160-216ed9c6-rebase.hg
271 271
272 272 Verify log --sparse only shows commits that affect the sparse checkout
273 273
274 274 $ hg log -T '{rev} '
275 275 2 1 0 (no-eol)
276 276 $ hg log --sparse -T '{rev} '
277 277 2 0 (no-eol)
278 278
279 279 Test status on a file in a subdir
280 280
281 281 $ mkdir -p dir1/dir2
282 282 $ touch dir1/dir2/file
283 283 $ hg debugsparse -I dir1/dir2
284 284 $ hg status
285 285 ? dir1/dir2/file
286 286
287 Mix files and subdirectories, both "glob:" and unprefixed
288
289 $ hg debugsparse --reset
290 $ touch dir1/notshown
291 $ hg commit -A dir1/notshown -m "notshown"
292 $ hg debugsparse --include 'dir1/dir2'
293 $ $PYTHON $TESTDIR/list-tree.py . | grep -v ./.hg
294 ./
295 ./dir1/
296 ./dir1/dir2/
297 ./dir1/dir2/file
298 ./hide.orig
299 $ hg debugsparse --delete 'dir1/dir2'
300 $ hg debugsparse --include 'glob:dir1/dir2'
301 $ $PYTHON $TESTDIR/list-tree.py . | grep -v ./.hg
302 ./
303 ./dir1/
304 ./dir1/dir2/
305 ./dir1/dir2/file
306 ./hide.orig
307
287 308 Test that add -s adds dirs to sparse profile
288 309
289 310 $ hg debugsparse --reset
290 311 $ hg debugsparse --include empty
291 312 $ hg debugsparse
292 313 [include]
293 314 empty
294 315
295 316
296 317 $ mkdir add
297 318 $ touch add/foo
298 319 $ touch add/bar
299 320 $ hg add add/foo
300 321 abort: cannot add 'add/foo' - it is outside the sparse checkout
301 322 (include file with `hg debugsparse --include <pattern>` or use `hg add -s <file>` to include file directory while adding)
302 323 [255]
303 324 $ hg add -s add/foo
304 325 $ hg st
305 326 A add/foo
306 327 ? add/bar
307 328 $ hg debugsparse
308 329 [include]
309 330 add
310 331 empty
311 332
312 333 $ hg add -s add/*
313 334 add/foo already tracked!
314 335 $ hg st
315 336 A add/bar
316 337 A add/foo
317 338 $ hg debugsparse
318 339 [include]
319 340 add
320 341 empty
321 342
322 343
323 344 $ cd ..
324 345
325 346 Test non-sparse repos work while sparse is loaded
326 347 $ hg init sparserepo
327 348 $ hg init nonsparserepo
328 349 $ cd sparserepo
329 350 $ cat > .hg/hgrc <<EOF
330 351 > [extensions]
331 352 > sparse=
332 353 > EOF
333 354 $ cd ../nonsparserepo
334 355 $ echo x > x && hg add x && hg commit -qAm x
335 356 $ cd ../sparserepo
336 357 $ hg clone ../nonsparserepo ../nonsparserepo2
337 358 updating to branch default
338 359 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
339 360
340 361 Test debugrebuilddirstate
341 362 $ cd ../sparserepo
342 363 $ touch included
343 364 $ touch excluded
344 365 $ hg add included excluded
345 366 $ hg commit -m 'a commit' -q
346 367 $ cp .hg/dirstate ../dirstateboth
347 368 $ hg debugsparse -X excluded
348 369 $ cp ../dirstateboth .hg/dirstate
349 370 $ hg debugrebuilddirstate
350 371 $ hg debugdirstate
351 372 n 0 -1 unset included
352 373
353 374 Test debugdirstate --minimal where file is in the parent manifest but not the
354 375 dirstate
355 376 $ hg debugsparse -X included
356 377 $ hg debugdirstate
357 378 $ cp .hg/dirstate ../dirstateallexcluded
358 379 $ hg debugsparse --reset
359 380 $ hg debugsparse -X excluded
360 381 $ cp ../dirstateallexcluded .hg/dirstate
361 382 $ touch includedadded
362 383 $ hg add includedadded
363 384 $ hg debugdirstate --nodates
364 385 a 0 -1 unset includedadded
365 386 $ hg debugrebuilddirstate --minimal
366 387 $ hg debugdirstate --nodates
367 388 n 0 -1 unset included
368 389 a 0 -1 * includedadded (glob)
369 390
370 391 Test debugdirstate --minimal where a file is not in parent manifest
371 392 but in the dirstate. This should take into account excluded files in the
372 393 manifest
373 394 $ cp ../dirstateboth .hg/dirstate
374 395 $ touch includedadded
375 396 $ hg add includedadded
376 397 $ touch excludednomanifest
377 398 $ hg add excludednomanifest
378 399 $ cp .hg/dirstate ../moreexcluded
379 400 $ hg forget excludednomanifest
380 401 $ rm excludednomanifest
381 402 $ hg debugsparse -X excludednomanifest
382 403 $ cp ../moreexcluded .hg/dirstate
383 404 $ hg manifest
384 405 excluded
385 406 included
386 407 We have files in the dirstate that are included and excluded. Some are in the
387 408 manifest and some are not.
388 409 $ hg debugdirstate --nodates
389 410 n 644 0 * excluded (glob)
390 411 a 0 -1 * excludednomanifest (glob)
391 412 n 644 0 * included (glob)
392 413 a 0 -1 * includedadded (glob)
393 414 $ hg debugrebuilddirstate --minimal
394 415 $ hg debugdirstate --nodates
395 416 n 644 0 * included (glob)
396 417 a 0 -1 * includedadded (glob)
397 418
General Comments 0
You need to be logged in to leave comments. Login now