##// END OF EJS Templates
sparse: clear rules in the context of a `parentchanges` context...
marmoute -
r48406:26bf0b9f default
parent child Browse files
Show More
@@ -1,830 +1,830 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 442 mergemod.applyupdates(
443 443 repo, tmresult, repo[None], repo[b'.'], False, wantfiledata=False
444 444 )
445 445
446 446 dirstate = repo.dirstate
447 447 for file, flags, msg in tmresult.getactions([mergestatemod.ACTION_GET]):
448 448 dirstate.normal(file)
449 449
450 450 profiles = activeconfig(repo)[2]
451 451 changedprofiles = profiles & files
452 452 # If an active profile changed during the update, refresh the checkout.
453 453 # Don't do this during a branch merge, since all incoming changes should
454 454 # have been handled by the temporary includes above.
455 455 if changedprofiles and not branchmerge:
456 456 mf = mctx.manifest()
457 457 for file in mf:
458 458 old = oldsparsematch(file)
459 459 new = sparsematch(file)
460 460 if not old and new:
461 461 flags = mf.flags(file)
462 462 prunedactions[file] = (
463 463 mergestatemod.ACTION_GET,
464 464 (flags, False),
465 465 b'',
466 466 )
467 467 elif old and not new:
468 468 prunedactions[file] = (mergestatemod.ACTION_REMOVE, [], b'')
469 469
470 470 mresult.setactions(prunedactions)
471 471
472 472
473 473 def refreshwdir(repo, origstatus, origsparsematch, force=False):
474 474 """Refreshes working directory by taking sparse config into account.
475 475
476 476 The old status and sparse matcher is compared against the current sparse
477 477 matcher.
478 478
479 479 Will abort if a file with pending changes is being excluded or included
480 480 unless ``force`` is True.
481 481 """
482 482 # Verify there are no pending changes
483 483 pending = set()
484 484 pending.update(origstatus.modified)
485 485 pending.update(origstatus.added)
486 486 pending.update(origstatus.removed)
487 487 sparsematch = matcher(repo)
488 488 abort = False
489 489
490 490 for f in pending:
491 491 if not sparsematch(f):
492 492 repo.ui.warn(_(b"pending changes to '%s'\n") % f)
493 493 abort = not force
494 494
495 495 if abort:
496 496 raise error.Abort(
497 497 _(b'could not update sparseness due to pending changes')
498 498 )
499 499
500 500 # Calculate merge result
501 501 dirstate = repo.dirstate
502 502 ctx = repo[b'.']
503 503 added = []
504 504 lookup = []
505 505 dropped = []
506 506 mf = ctx.manifest()
507 507 files = set(mf)
508 508 mresult = mergemod.mergeresult()
509 509
510 510 for file in files:
511 511 old = origsparsematch(file)
512 512 new = sparsematch(file)
513 513 # Add files that are newly included, or that don't exist in
514 514 # the dirstate yet.
515 515 if (new and not old) or (old and new and not file in dirstate):
516 516 fl = mf.flags(file)
517 517 if repo.wvfs.exists(file):
518 518 mresult.addfile(file, mergestatemod.ACTION_EXEC, (fl,), b'')
519 519 lookup.append(file)
520 520 else:
521 521 mresult.addfile(
522 522 file, mergestatemod.ACTION_GET, (fl, False), b''
523 523 )
524 524 added.append(file)
525 525 # Drop files that are newly excluded, or that still exist in
526 526 # the dirstate.
527 527 elif (old and not new) or (not old and not new and file in dirstate):
528 528 dropped.append(file)
529 529 if file not in pending:
530 530 mresult.addfile(file, mergestatemod.ACTION_REMOVE, [], b'')
531 531
532 532 # Verify there are no pending changes in newly included files
533 533 abort = False
534 534 for file in lookup:
535 535 repo.ui.warn(_(b"pending changes to '%s'\n") % file)
536 536 abort = not force
537 537 if abort:
538 538 raise error.Abort(
539 539 _(
540 540 b'cannot change sparseness due to pending '
541 541 b'changes (delete the files or use '
542 542 b'--force to bring them back dirty)'
543 543 )
544 544 )
545 545
546 546 # Check for files that were only in the dirstate.
547 547 for file, state in pycompat.iteritems(dirstate):
548 548 if not file in files:
549 549 old = origsparsematch(file)
550 550 new = sparsematch(file)
551 551 if old and not new:
552 552 dropped.append(file)
553 553
554 554 mergemod.applyupdates(
555 555 repo, mresult, repo[None], repo[b'.'], False, wantfiledata=False
556 556 )
557 557
558 558 # Fix dirstate
559 559 for file in added:
560 560 dirstate.normal(file)
561 561
562 562 for file in dropped:
563 563 dirstate.drop(file)
564 564
565 565 for file in lookup:
566 566 # File exists on disk, and we're bringing it back in an unknown state.
567 567 dirstate.normallookup(file)
568 568
569 569 return added, dropped, lookup
570 570
571 571
572 572 def aftercommit(repo, node):
573 573 """Perform actions after a working directory commit."""
574 574 # This function is called unconditionally, even if sparse isn't
575 575 # enabled.
576 576 ctx = repo[node]
577 577
578 578 profiles = patternsforrev(repo, ctx.rev())[2]
579 579
580 580 # profiles will only have data if sparse is enabled.
581 581 if profiles & set(ctx.files()):
582 582 origstatus = repo.status()
583 583 origsparsematch = matcher(repo)
584 584 refreshwdir(repo, origstatus, origsparsematch, force=True)
585 585
586 586 prunetemporaryincludes(repo)
587 587
588 588
589 589 def _updateconfigandrefreshwdir(
590 590 repo, includes, excludes, profiles, force=False, removing=False
591 591 ):
592 592 """Update the sparse config and working directory state."""
593 593 raw = repo.vfs.tryread(b'sparse')
594 594 oldincludes, oldexcludes, oldprofiles = parseconfig(repo.ui, raw, b'sparse')
595 595
596 596 oldstatus = repo.status()
597 597 oldmatch = matcher(repo)
598 598 oldrequires = set(repo.requirements)
599 599
600 600 # TODO remove this try..except once the matcher integrates better
601 601 # with dirstate. We currently have to write the updated config
602 602 # because that will invalidate the matcher cache and force a
603 603 # re-read. We ideally want to update the cached matcher on the
604 604 # repo instance then flush the new config to disk once wdir is
605 605 # updated. But this requires massive rework to matcher() and its
606 606 # consumers.
607 607
608 608 if requirements.SPARSE_REQUIREMENT in oldrequires and removing:
609 609 repo.requirements.discard(requirements.SPARSE_REQUIREMENT)
610 610 scmutil.writereporequirements(repo)
611 611 elif requirements.SPARSE_REQUIREMENT not in oldrequires:
612 612 repo.requirements.add(requirements.SPARSE_REQUIREMENT)
613 613 scmutil.writereporequirements(repo)
614 614
615 615 try:
616 616 writeconfig(repo, includes, excludes, profiles)
617 617 return refreshwdir(repo, oldstatus, oldmatch, force=force)
618 618 except Exception:
619 619 if repo.requirements != oldrequires:
620 620 repo.requirements.clear()
621 621 repo.requirements |= oldrequires
622 622 scmutil.writereporequirements(repo)
623 623 writeconfig(repo, oldincludes, oldexcludes, oldprofiles)
624 624 raise
625 625
626 626
627 627 def clearrules(repo, force=False):
628 628 """Clears include/exclude rules from the sparse config.
629 629
630 630 The remaining sparse config only has profiles, if defined. The working
631 631 directory is refreshed, as needed.
632 632 """
633 with repo.wlock():
633 with repo.wlock(), repo.dirstate.parentchange():
634 634 raw = repo.vfs.tryread(b'sparse')
635 635 includes, excludes, profiles = parseconfig(repo.ui, raw, b'sparse')
636 636
637 637 if not includes and not excludes:
638 638 return
639 639
640 640 _updateconfigandrefreshwdir(repo, set(), set(), profiles, force=force)
641 641
642 642
643 643 def importfromfiles(repo, opts, paths, force=False):
644 644 """Import sparse config rules from files.
645 645
646 646 The updated sparse config is written out and the working directory
647 647 is refreshed, as needed.
648 648 """
649 649 with repo.wlock(), repo.dirstate.parentchange():
650 650 # read current configuration
651 651 raw = repo.vfs.tryread(b'sparse')
652 652 includes, excludes, profiles = parseconfig(repo.ui, raw, b'sparse')
653 653 aincludes, aexcludes, aprofiles = activeconfig(repo)
654 654
655 655 # Import rules on top; only take in rules that are not yet
656 656 # part of the active rules.
657 657 changed = False
658 658 for p in paths:
659 659 with util.posixfile(util.expandpath(p), mode=b'rb') as fh:
660 660 raw = fh.read()
661 661
662 662 iincludes, iexcludes, iprofiles = parseconfig(
663 663 repo.ui, raw, b'sparse'
664 664 )
665 665 oldsize = len(includes) + len(excludes) + len(profiles)
666 666 includes.update(iincludes - aincludes)
667 667 excludes.update(iexcludes - aexcludes)
668 668 profiles.update(iprofiles - aprofiles)
669 669 if len(includes) + len(excludes) + len(profiles) > oldsize:
670 670 changed = True
671 671
672 672 profilecount = includecount = excludecount = 0
673 673 fcounts = (0, 0, 0)
674 674
675 675 if changed:
676 676 profilecount = len(profiles - aprofiles)
677 677 includecount = len(includes - aincludes)
678 678 excludecount = len(excludes - aexcludes)
679 679
680 680 fcounts = map(
681 681 len,
682 682 _updateconfigandrefreshwdir(
683 683 repo, includes, excludes, profiles, force=force
684 684 ),
685 685 )
686 686
687 687 printchanges(
688 688 repo.ui, opts, profilecount, includecount, excludecount, *fcounts
689 689 )
690 690
691 691
692 692 def updateconfig(
693 693 repo,
694 694 pats,
695 695 opts,
696 696 include=False,
697 697 exclude=False,
698 698 reset=False,
699 699 delete=False,
700 700 enableprofile=False,
701 701 disableprofile=False,
702 702 force=False,
703 703 usereporootpaths=False,
704 704 ):
705 705 """Perform a sparse config update.
706 706
707 707 Only one of the actions may be performed.
708 708
709 709 The new config is written out and a working directory refresh is performed.
710 710 """
711 with repo.wlock():
711 with repo.wlock(), repo.dirstate.parentchange():
712 712 raw = repo.vfs.tryread(b'sparse')
713 713 oldinclude, oldexclude, oldprofiles = parseconfig(
714 714 repo.ui, raw, b'sparse'
715 715 )
716 716
717 717 if reset:
718 718 newinclude = set()
719 719 newexclude = set()
720 720 newprofiles = set()
721 721 else:
722 722 newinclude = set(oldinclude)
723 723 newexclude = set(oldexclude)
724 724 newprofiles = set(oldprofiles)
725 725
726 726 if any(os.path.isabs(pat) for pat in pats):
727 727 raise error.Abort(_(b'paths cannot be absolute'))
728 728
729 729 if not usereporootpaths:
730 730 # let's treat paths as relative to cwd
731 731 root, cwd = repo.root, repo.getcwd()
732 732 abspats = []
733 733 for kindpat in pats:
734 734 kind, pat = matchmod._patsplit(kindpat, None)
735 735 if kind in matchmod.cwdrelativepatternkinds or kind is None:
736 736 ap = (kind + b':' if kind else b'') + pathutil.canonpath(
737 737 root, cwd, pat
738 738 )
739 739 abspats.append(ap)
740 740 else:
741 741 abspats.append(kindpat)
742 742 pats = abspats
743 743
744 744 if include:
745 745 newinclude.update(pats)
746 746 elif exclude:
747 747 newexclude.update(pats)
748 748 elif enableprofile:
749 749 newprofiles.update(pats)
750 750 elif disableprofile:
751 751 newprofiles.difference_update(pats)
752 752 elif delete:
753 753 newinclude.difference_update(pats)
754 754 newexclude.difference_update(pats)
755 755
756 756 profilecount = len(newprofiles - oldprofiles) - len(
757 757 oldprofiles - newprofiles
758 758 )
759 759 includecount = len(newinclude - oldinclude) - len(
760 760 oldinclude - newinclude
761 761 )
762 762 excludecount = len(newexclude - oldexclude) - len(
763 763 oldexclude - newexclude
764 764 )
765 765
766 766 fcounts = map(
767 767 len,
768 768 _updateconfigandrefreshwdir(
769 769 repo,
770 770 newinclude,
771 771 newexclude,
772 772 newprofiles,
773 773 force=force,
774 774 removing=reset,
775 775 ),
776 776 )
777 777
778 778 printchanges(
779 779 repo.ui, opts, profilecount, includecount, excludecount, *fcounts
780 780 )
781 781
782 782
783 783 def printchanges(
784 784 ui,
785 785 opts,
786 786 profilecount=0,
787 787 includecount=0,
788 788 excludecount=0,
789 789 added=0,
790 790 dropped=0,
791 791 conflicting=0,
792 792 ):
793 793 """Print output summarizing sparse config changes."""
794 794 with ui.formatter(b'sparse', opts) as fm:
795 795 fm.startitem()
796 796 fm.condwrite(
797 797 ui.verbose,
798 798 b'profiles_added',
799 799 _(b'Profiles changed: %d\n'),
800 800 profilecount,
801 801 )
802 802 fm.condwrite(
803 803 ui.verbose,
804 804 b'include_rules_added',
805 805 _(b'Include rules changed: %d\n'),
806 806 includecount,
807 807 )
808 808 fm.condwrite(
809 809 ui.verbose,
810 810 b'exclude_rules_added',
811 811 _(b'Exclude rules changed: %d\n'),
812 812 excludecount,
813 813 )
814 814
815 815 # In 'plain' verbose mode, mergemod.applyupdates already outputs what
816 816 # files are added or removed outside of the templating formatter
817 817 # framework. No point in repeating ourselves in that case.
818 818 if not fm.isplain():
819 819 fm.condwrite(
820 820 ui.verbose, b'files_added', _(b'Files added: %d\n'), added
821 821 )
822 822 fm.condwrite(
823 823 ui.verbose, b'files_dropped', _(b'Files dropped: %d\n'), dropped
824 824 )
825 825 fm.condwrite(
826 826 ui.verbose,
827 827 b'files_conflicting',
828 828 _(b'Files conflicting: %d\n'),
829 829 conflicting,
830 830 )
General Comments 0
You need to be logged in to leave comments. Login now