##// END OF EJS Templates
sparse: use `update_file` instead of `drop`...
marmoute -
r48548:000ea893 default
parent child Browse files
Show More
@@ -1,840 +1,840 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 dirstate.drop(file)
286 dirstate.update_file(file, p1_tracked=False, wc_tracked=False)
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 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.update_file(file, p1_tracked=True, wc_tracked=True)
569 569
570 570 for file in dropped:
571 571 dirstate.update_file(file, p1_tracked=False, wc_tracked=False)
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.update_file(
576 576 file, p1_tracked=True, wc_tracked=True, possibly_dirty=True
577 577 )
578 578
579 579 return added, dropped, lookup
580 580
581 581
582 582 def aftercommit(repo, node):
583 583 """Perform actions after a working directory commit."""
584 584 # This function is called unconditionally, even if sparse isn't
585 585 # enabled.
586 586 ctx = repo[node]
587 587
588 588 profiles = patternsforrev(repo, ctx.rev())[2]
589 589
590 590 # profiles will only have data if sparse is enabled.
591 591 if profiles & set(ctx.files()):
592 592 origstatus = repo.status()
593 593 origsparsematch = matcher(repo)
594 594 refreshwdir(repo, origstatus, origsparsematch, force=True)
595 595
596 596 prunetemporaryincludes(repo)
597 597
598 598
599 599 def _updateconfigandrefreshwdir(
600 600 repo, includes, excludes, profiles, force=False, removing=False
601 601 ):
602 602 """Update the sparse config and working directory state."""
603 603 raw = repo.vfs.tryread(b'sparse')
604 604 oldincludes, oldexcludes, oldprofiles = parseconfig(repo.ui, raw, b'sparse')
605 605
606 606 oldstatus = repo.status()
607 607 oldmatch = matcher(repo)
608 608 oldrequires = set(repo.requirements)
609 609
610 610 # TODO remove this try..except once the matcher integrates better
611 611 # with dirstate. We currently have to write the updated config
612 612 # because that will invalidate the matcher cache and force a
613 613 # re-read. We ideally want to update the cached matcher on the
614 614 # repo instance then flush the new config to disk once wdir is
615 615 # updated. But this requires massive rework to matcher() and its
616 616 # consumers.
617 617
618 618 if requirements.SPARSE_REQUIREMENT in oldrequires and removing:
619 619 repo.requirements.discard(requirements.SPARSE_REQUIREMENT)
620 620 scmutil.writereporequirements(repo)
621 621 elif requirements.SPARSE_REQUIREMENT not in oldrequires:
622 622 repo.requirements.add(requirements.SPARSE_REQUIREMENT)
623 623 scmutil.writereporequirements(repo)
624 624
625 625 try:
626 626 writeconfig(repo, includes, excludes, profiles)
627 627 return refreshwdir(repo, oldstatus, oldmatch, force=force)
628 628 except Exception:
629 629 if repo.requirements != oldrequires:
630 630 repo.requirements.clear()
631 631 repo.requirements |= oldrequires
632 632 scmutil.writereporequirements(repo)
633 633 writeconfig(repo, oldincludes, oldexcludes, oldprofiles)
634 634 raise
635 635
636 636
637 637 def clearrules(repo, force=False):
638 638 """Clears include/exclude rules from the sparse config.
639 639
640 640 The remaining sparse config only has profiles, if defined. The working
641 641 directory is refreshed, as needed.
642 642 """
643 643 with repo.wlock(), repo.dirstate.parentchange():
644 644 raw = repo.vfs.tryread(b'sparse')
645 645 includes, excludes, profiles = parseconfig(repo.ui, raw, b'sparse')
646 646
647 647 if not includes and not excludes:
648 648 return
649 649
650 650 _updateconfigandrefreshwdir(repo, set(), set(), profiles, force=force)
651 651
652 652
653 653 def importfromfiles(repo, opts, paths, force=False):
654 654 """Import sparse config rules from files.
655 655
656 656 The updated sparse config is written out and the working directory
657 657 is refreshed, as needed.
658 658 """
659 659 with repo.wlock(), repo.dirstate.parentchange():
660 660 # read current configuration
661 661 raw = repo.vfs.tryread(b'sparse')
662 662 includes, excludes, profiles = parseconfig(repo.ui, raw, b'sparse')
663 663 aincludes, aexcludes, aprofiles = activeconfig(repo)
664 664
665 665 # Import rules on top; only take in rules that are not yet
666 666 # part of the active rules.
667 667 changed = False
668 668 for p in paths:
669 669 with util.posixfile(util.expandpath(p), mode=b'rb') as fh:
670 670 raw = fh.read()
671 671
672 672 iincludes, iexcludes, iprofiles = parseconfig(
673 673 repo.ui, raw, b'sparse'
674 674 )
675 675 oldsize = len(includes) + len(excludes) + len(profiles)
676 676 includes.update(iincludes - aincludes)
677 677 excludes.update(iexcludes - aexcludes)
678 678 profiles.update(iprofiles - aprofiles)
679 679 if len(includes) + len(excludes) + len(profiles) > oldsize:
680 680 changed = True
681 681
682 682 profilecount = includecount = excludecount = 0
683 683 fcounts = (0, 0, 0)
684 684
685 685 if changed:
686 686 profilecount = len(profiles - aprofiles)
687 687 includecount = len(includes - aincludes)
688 688 excludecount = len(excludes - aexcludes)
689 689
690 690 fcounts = map(
691 691 len,
692 692 _updateconfigandrefreshwdir(
693 693 repo, includes, excludes, profiles, force=force
694 694 ),
695 695 )
696 696
697 697 printchanges(
698 698 repo.ui, opts, profilecount, includecount, excludecount, *fcounts
699 699 )
700 700
701 701
702 702 def updateconfig(
703 703 repo,
704 704 pats,
705 705 opts,
706 706 include=False,
707 707 exclude=False,
708 708 reset=False,
709 709 delete=False,
710 710 enableprofile=False,
711 711 disableprofile=False,
712 712 force=False,
713 713 usereporootpaths=False,
714 714 ):
715 715 """Perform a sparse config update.
716 716
717 717 Only one of the actions may be performed.
718 718
719 719 The new config is written out and a working directory refresh is performed.
720 720 """
721 721 with repo.wlock(), repo.dirstate.parentchange():
722 722 raw = repo.vfs.tryread(b'sparse')
723 723 oldinclude, oldexclude, oldprofiles = parseconfig(
724 724 repo.ui, raw, b'sparse'
725 725 )
726 726
727 727 if reset:
728 728 newinclude = set()
729 729 newexclude = set()
730 730 newprofiles = set()
731 731 else:
732 732 newinclude = set(oldinclude)
733 733 newexclude = set(oldexclude)
734 734 newprofiles = set(oldprofiles)
735 735
736 736 if any(os.path.isabs(pat) for pat in pats):
737 737 raise error.Abort(_(b'paths cannot be absolute'))
738 738
739 739 if not usereporootpaths:
740 740 # let's treat paths as relative to cwd
741 741 root, cwd = repo.root, repo.getcwd()
742 742 abspats = []
743 743 for kindpat in pats:
744 744 kind, pat = matchmod._patsplit(kindpat, None)
745 745 if kind in matchmod.cwdrelativepatternkinds or kind is None:
746 746 ap = (kind + b':' if kind else b'') + pathutil.canonpath(
747 747 root, cwd, pat
748 748 )
749 749 abspats.append(ap)
750 750 else:
751 751 abspats.append(kindpat)
752 752 pats = abspats
753 753
754 754 if include:
755 755 newinclude.update(pats)
756 756 elif exclude:
757 757 newexclude.update(pats)
758 758 elif enableprofile:
759 759 newprofiles.update(pats)
760 760 elif disableprofile:
761 761 newprofiles.difference_update(pats)
762 762 elif delete:
763 763 newinclude.difference_update(pats)
764 764 newexclude.difference_update(pats)
765 765
766 766 profilecount = len(newprofiles - oldprofiles) - len(
767 767 oldprofiles - newprofiles
768 768 )
769 769 includecount = len(newinclude - oldinclude) - len(
770 770 oldinclude - newinclude
771 771 )
772 772 excludecount = len(newexclude - oldexclude) - len(
773 773 oldexclude - newexclude
774 774 )
775 775
776 776 fcounts = map(
777 777 len,
778 778 _updateconfigandrefreshwdir(
779 779 repo,
780 780 newinclude,
781 781 newexclude,
782 782 newprofiles,
783 783 force=force,
784 784 removing=reset,
785 785 ),
786 786 )
787 787
788 788 printchanges(
789 789 repo.ui, opts, profilecount, includecount, excludecount, *fcounts
790 790 )
791 791
792 792
793 793 def printchanges(
794 794 ui,
795 795 opts,
796 796 profilecount=0,
797 797 includecount=0,
798 798 excludecount=0,
799 799 added=0,
800 800 dropped=0,
801 801 conflicting=0,
802 802 ):
803 803 """Print output summarizing sparse config changes."""
804 804 with ui.formatter(b'sparse', opts) as fm:
805 805 fm.startitem()
806 806 fm.condwrite(
807 807 ui.verbose,
808 808 b'profiles_added',
809 809 _(b'Profiles changed: %d\n'),
810 810 profilecount,
811 811 )
812 812 fm.condwrite(
813 813 ui.verbose,
814 814 b'include_rules_added',
815 815 _(b'Include rules changed: %d\n'),
816 816 includecount,
817 817 )
818 818 fm.condwrite(
819 819 ui.verbose,
820 820 b'exclude_rules_added',
821 821 _(b'Exclude rules changed: %d\n'),
822 822 excludecount,
823 823 )
824 824
825 825 # In 'plain' verbose mode, mergemod.applyupdates already outputs what
826 826 # files are added or removed outside of the templating formatter
827 827 # framework. No point in repeating ourselves in that case.
828 828 if not fm.isplain():
829 829 fm.condwrite(
830 830 ui.verbose, b'files_added', _(b'Files added: %d\n'), added
831 831 )
832 832 fm.condwrite(
833 833 ui.verbose, b'files_dropped', _(b'Files dropped: %d\n'), dropped
834 834 )
835 835 fm.condwrite(
836 836 ui.verbose,
837 837 b'files_conflicting',
838 838 _(b'Files conflicting: %d\n'),
839 839 conflicting,
840 840 )
General Comments 0
You need to be logged in to leave comments. Login now