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