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