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