Mercurial > hg
view mercurial/sparse.py @ 46582:b0a3ca02d17a
copies-rust: implement PartialEqual manually
Now that we know that each (dest, rev) pair has at most a unique CopySource, we
can simplify comparison a lot.
This "simple" step buy a good share of the previous slowdown back in some case:
Repo Case Source-Rev Dest-Rev # of revisions old time new time Difference Factor time per rev
---------------------------------------------------------------------------------------------------------------------------------------------------------------
mozilla-try x00000_revs_x00000_added_x000_copies 9b2a99adc05e 8e29777b48e6 : 382065 revs, 43.304637 s, 34.443661 s, -8.860976 s, × 0.7954, 90 µs/rev
Full benchmark:
Repo Case Source-Rev Dest-Rev # of revisions old time new time Difference Factor time per rev
---------------------------------------------------------------------------------------------------------------------------------------------------------------
mercurial x_revs_x_added_0_copies ad6b123de1c7 39cfcef4f463 : 1 revs, 0.000043 s, 0.000043 s, +0.000000 s, × 1.0000, 43 µs/rev
mercurial x_revs_x_added_x_copies 2b1c78674230 0c1d10351869 : 6 revs, 0.000114 s, 0.000117 s, +0.000003 s, × 1.0263, 19 µs/rev
mercurial x000_revs_x000_added_x_copies 81f8ff2a9bf2 dd3267698d84 : 1032 revs, 0.004937 s, 0.004892 s, -0.000045 s, × 0.9909, 4 µs/rev
pypy x_revs_x_added_0_copies aed021ee8ae8 099ed31b181b : 9 revs, 0.000339 s, 0.000196 s, -0.000143 s, × 0.5782, 21 µs/rev
pypy x_revs_x000_added_0_copies 4aa4e1f8e19a 359343b9ac0e : 1 revs, 0.000049 s, 0.000050 s, +0.000001 s, × 1.0204, 50 µs/rev
pypy x_revs_x_added_x_copies ac52eb7bbbb0 72e022663155 : 7 revs, 0.000202 s, 0.000117 s, -0.000085 s, × 0.5792, 16 µs/rev
pypy x_revs_x00_added_x_copies c3b14617fbd7 ace7255d9a26 : 1 revs, 0.000409 s, 0.6f1f4a s, -0.000087 s, × 0.7873, 322 µs/rev
pypy x_revs_x000_added_x000_copies df6f7a526b60 a83dc6a2d56f : 6 revs, 0.011984 s, 0.011949 s, -0.000035 s, × 0.9971, 1991 µs/rev
pypy x000_revs_xx00_added_0_copies 89a76aede314 2f22446ff07e : 4785 revs, 0.050820 s, 0.050802 s, -0.000018 s, × 0.9996, 10 µs/rev
pypy x000_revs_x000_added_x_copies 8a3b5bfd266e 2c68e87c3efe : 6780 revs, 0.087953 s, 0.088090 s, +0.000137 s, × 1.0016, 12 µs/rev
pypy x000_revs_x000_added_x000_copies 89a76aede314 7b3dda341c84 : 5441 revs, 0.062902 s, 0.062079 s, -0.000823 s, × 0.9869, 11 µs/rev
pypy x0000_revs_x_added_0_copies d1defd0dc478 c9cb1334cc78 : 43645 revs, 0.679234 s, 0.635337 s, -0.043897 s, × 0.9354, 14 µs/rev
pypy x0000_revs_xx000_added_0_copies bf2c629d0071 4ffed77c095c : 2 revs, 0.013095 s, 0.013262 s, +0.000167 s, × 1.0128, 6631 µs/rev
pypy x0000_revs_xx000_added_x000_copies 08ea3258278e d9fa043f30c0 : 11316 revs, 0.120910 s, 0.120085 s, -0.000825 s, × 0.9932, 10 µs/rev
netbeans x_revs_x_added_0_copies fb0955ffcbcd a01e9239f9e7 : 2 revs, 0.000087 s, 0.000085 s, -0.000002 s, × 0.9770, 42 µs/rev
netbeans x_revs_x000_added_0_copies 6f360122949f 20eb231cc7d0 : 2 revs, 0.000107 s, 0.000110 s, +0.000003 s, × 1.0280, 55 µs/rev
netbeans x_revs_x_added_x_copies 1ada3faf6fb6 5a39d12eecf4 : 3 revs, 0.000186 s, 0.000177 s, -0.000009 s, × 0.9516, 59 µs/rev
netbeans x_revs_x00_added_x_copies 35be93ba1e2c 9eec5e90c05f : 9 revs, 0.000754 s, 0.000743 s, -0.000011 s, × 0.9854, 82 µs/rev
netbeans x000_revs_xx00_added_0_copies eac3045b4fdd 51d4ae7f1290 : 1421 revs, 0.010443 s, 0.010168 s, -0.000275 s, × 0.9737, 7 µs/rev
netbeans x000_revs_x000_added_x_copies e2063d266acd 6081d72689dc : 1533 revs, 0.015697 s, 0.015946 s, +0.000249 s, × 1.0159, 10 µs/rev
netbeans x000_revs_x000_added_x000_copies ff453e9fee32 411350406ec2 : 5750 revs, 0.063528 s, 0.062712 s, -0.000816 s, × 0.9872, 10 µs/rev
netbeans x0000_revs_xx000_added_x000_copies 588c2d1ced70 1aad62e59ddd : 66949 revs, 0.545515 s, 0.523832 s, -0.021683 s, × 0.9603, 7 µs/rev
mozilla-central x_revs_x_added_0_copies 3697f962bb7b 7015fcdd43a2 : 2 revs, 0.000089 s, 0.000090 s, +0.000001 s, × 1.0112, 45 µs/rev
mozilla-central x_revs_x000_added_0_copies dd390860c6c9 40d0c5bed75d : 8 revs, 0.000265 s, 0.000264 s, -0.000001 s, × 0.9962, 33 µs/rev
mozilla-central x_revs_x_added_x_copies 8d198483ae3b 14207ffc2b2f : 9 revs, 0.000381 s, 0.000187 s, -0.000194 s, × 0.4908, 20 µs/rev
mozilla-central x_revs_x00_added_x_copies 98cbc58cc6bc 446a150332c3 : 7 revs, 0.000672 s, 0.000665 s, -0.000007 s, × 0.9896, 95 µs/rev
mozilla-central x_revs_x000_added_x000_copies 3c684b4b8f68 0a5e72d1b479 : 3 revs, 0.003497 s, 0.003556 s, +0.000059 s, × 1.0169, 1185 µs/rev
mozilla-central x_revs_x0000_added_x0000_copies effb563bb7e5 c07a39dc4e80 : 6 revs, 0.073204 s, 0.071345 s, -0.001859 s, × 0.9746, 11890 µs/rev
mozilla-central x000_revs_xx00_added_0_copies 6100d773079a 04a55431795e : 1593 revs, 0.006482 s, 0.006551 s, +0.000069 s, × 1.0106, 4 µs/rev
mozilla-central x000_revs_x000_added_x_copies 9f17a6fc04f9 2d37b966abed : 41 revs, 0.005066 s, 0.005078 s, +0.000012 s, × 1.0024, 123 µs/rev
mozilla-central x000_revs_x000_added_x000_copies 7c97034feb78 4407bd0c6330 : 7839 revs, 0.065707 s, 0.065823 s, +0.000116 s, × 1.0018, 8 µs/rev
mozilla-central x0000_revs_xx000_added_0_copies 9eec5917337d 67118cc6dcad : 615 revs, 0.026800 s, 0.027050 s, +0.000250 s, × 1.0093, 43 µs/rev
mozilla-central x0000_revs_xx000_added_x000_copies f78c615a656c 96a38b690156 : 30263 revs, 0.203856 s, 0.202443 s, -0.001413 s, × 0.9931, 6 µs/rev
mozilla-central x00000_revs_x0000_added_x0000_copies 6832ae71433c 4c222a1d9a00 : 153721 revs, 1.293394 s, 1.261583 s, -0.031811 s, × 0.9754, 8 µs/rev
mozilla-central x00000_revs_x00000_added_x000_copies 76caed42cf7c 1daa622bbe42 : 204976 revs, 1.698239 s, 1.643869 s, -0.054370 s, × 0.9680, 8 µs/rev
mozilla-try x_revs_x_added_0_copies aaf6dde0deb8 9790f499805a : 2 revs, 0.000875 s, 0.000868 s, -0.000007 s, × 0.9920, 434 µs/rev
mozilla-try x_revs_x000_added_0_copies d8d0222927b4 5bb8ce8c7450 : 2 revs, 0.000891 s, 0.000887 s, -0.000004 s, × 0.9955, 443 µs/rev
mozilla-try x_revs_x_added_x_copies 092fcca11bdb 936255a0384a : 4 revs, 0.000292 s, 0.000168 s, -0.000124 s, × 0.5753, 42 µs/rev
mozilla-try x_revs_x00_added_x_copies b53d2fadbdb5 017afae788ec : 2 revs, 0.003939 s, 0.001160 s, -0.002779 s, × 0.2945, 580 µs/rev
mozilla-try x_revs_x000_added_x000_copies 20408ad61ce5 6f0ee96e21ad : 1 revs, 0.033027 s, 0.033016 s, -0.000011 s, × 0.9997, 33016 µs/rev
mozilla-try x_revs_x0000_added_x0000_copies effb563bb7e5 c07a39dc4e80 : 6 revs, 0.073703 s, 0.073312 s, -0.39ae31 s, × 0.9947, 12218 µs/rev
mozilla-try x000_revs_xx00_added_0_copies 6100d773079a 04a55431795e : 1593 revs, 0.006469 s, 0.006485 s, +0.000016 s, × 1.0025, 4 µs/rev
mozilla-try x000_revs_x000_added_x_copies 9f17a6fc04f9 2d37b966abed : 41 revs, 0.005278 s, 0.005494 s, +0.000216 s, × 1.0409, 134 µs/rev
mozilla-try x000_revs_x000_added_x000_copies 1346fd0130e4 4c65cbdabc1f : 6657 revs, 0.064995 s, 0.064879 s, -0.000116 s, × 0.9982, 9 µs/rev
mozilla-try x0000_revs_x_added_0_copies 63519bfd42ee a36a2a865d92 : 40314 revs, 0.301041 s, 0.301469 s, +0.000428 s, × 1.0014, 7 µs/rev
mozilla-try x0000_revs_x_added_x_copies 9fe69ff0762d bcabf2a78927 : 38690 revs, 0.285575 s, 0.297113 s, +0.011538 s, × 1.0404, 7 µs/rev
mozilla-try x0000_revs_xx000_added_x_copies 156f6e2674f2 4d0f2c178e66 : 8598 revs, 0.085597 s, 0.085890 s, +0.000293 s, × 1.0034, 9 µs/rev
mozilla-try x0000_revs_xx000_added_0_copies 9eec5917337d 67118cc6dcad : 615 revs, 0.027118 s, 0.027718 s, +0.000600 s, × 1.0221, 45 µs/rev
mozilla-try x0000_revs_xx000_added_x000_copies 89294cd501d9 7ccb2fc7ccb5 : 97052 revs, 2.119204 s, 2.048949 s, -0.070255 s, × 0.9668, 21 µs/rev
mozilla-try x0000_revs_x0000_added_x0000_copies e928c65095ed e951f4ad123a : 52031 revs, 0.701479 s, 0.685924 s, -0.015555 s, × 0.9778, 13 µs/rev
mozilla-try x00000_revs_x_added_0_copies 6a320851d377 1ebb79acd503 : 363753 revs, 4.482399 s, 4.482891 s, +0.000492 s, × 1.0001, 12 µs/rev
mozilla-try x00000_revs_x00000_added_0_copies dc8a3ca7010e d16fde900c9c : 34414 revs, 0.574082 s, 0.577633 s, +0.003551 s, × 1.0062, 16 µs/rev
mozilla-try x00000_revs_x_added_x_copies 5173c4b6f97c 95d83ee7242d : 362229 revs, 4.480366 s, 4.397816 s, -0.082550 s, × 0.9816, 12 µs/rev
mozilla-try x00000_revs_x000_added_x_copies 9126823d0e9c ca82787bb23c : 359344 revs, 4.369070 s, 4.370538 s, +0.001468 s, × 1.0003, 12 µs/rev
mozilla-try x00000_revs_x0000_added_x0000_copies 8d3fafa80d4b eb884023b810 : 192665 revs, 1.592506 s, 1.570439 s, -0.022067 s, × 0.9861, 8 µs/rev
mozilla-try x00000_revs_x00000_added_x0000_copies 1b661134e2ca 1ae03d022d6d : 228985 revs, 87.824489 s, 88.388512 s, +0.564023 s, × 1.0064, 386 µs/rev
mozilla-try x00000_revs_x00000_added_x000_copies 9b2a99adc05e 8e29777b48e6 : 382065 revs, 43.304637 s, 34.443661 s, -8.860976 s, × 0.7954, 90 µs/rev
private : 459513 revs, 33.853687 s, 27.370148 s, -6.483539 s, × 0.8085, 59 µs/rev
Differential Revision: https://phab.mercurial-scm.org/D9653
author | Pierre-Yves David <pierre-yves.david@octobus.net> |
---|---|
date | Wed, 16 Dec 2020 11:11:05 +0100 |
parents | 590a840fa367 |
children | d55b71393907 |
line wrap: on
line source
# sparse.py - functionality for sparse checkouts # # Copyright 2014 Facebook, Inc. # # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. from __future__ import absolute_import import os from .i18n import _ from .node import ( hex, nullid, ) from . import ( error, match as matchmod, merge as mergemod, mergestate as mergestatemod, pathutil, pycompat, requirements, scmutil, util, ) from .utils import hashutil # Whether sparse features are enabled. This variable is intended to be # temporary to facilitate porting sparse to core. It should eventually be # a per-repo option, possibly a repo requirement. enabled = False def parseconfig(ui, raw, action): """Parse sparse config file content. action is the command which is trigerring this read, can be narrow, sparse Returns a tuple of includes, excludes, and profiles. """ includes = set() excludes = set() profiles = set() current = None havesection = False for line in raw.split(b'\n'): line = line.strip() if not line or line.startswith(b'#'): # empty or comment line, skip continue elif line.startswith(b'%include '): line = line[9:].strip() if line: profiles.add(line) elif line == b'[include]': if havesection and current != includes: # TODO pass filename into this API so we can report it. raise error.Abort( _( b'%(action)s config cannot have includes ' b'after excludes' ) % {b'action': action} ) havesection = True current = includes continue elif line == b'[exclude]': havesection = True current = excludes elif line: if current is None: raise error.Abort( _( b'%(action)s config entry outside of ' b'section: %(line)s' ) % {b'action': action, b'line': line}, hint=_( b'add an [include] or [exclude] line ' b'to declare the entry type' ), ) if line.strip().startswith(b'/'): ui.warn( _( b'warning: %(action)s profile cannot use' b' paths starting with /, ignoring %(line)s\n' ) % {b'action': action, b'line': line} ) continue current.add(line) return includes, excludes, profiles # Exists as separate function to facilitate monkeypatching. def readprofile(repo, profile, changeid): """Resolve the raw content of a sparse profile file.""" # TODO add some kind of cache here because this incurs a manifest # resolve and can be slow. return repo.filectx(profile, changeid=changeid).data() def patternsforrev(repo, rev): """Obtain sparse checkout patterns for the given rev. Returns a tuple of iterables representing includes, excludes, and patterns. """ # Feature isn't enabled. No-op. if not enabled: return set(), set(), set() raw = repo.vfs.tryread(b'sparse') if not raw: return set(), set(), set() if rev is None: raise error.Abort( _(b'cannot parse sparse patterns from working directory') ) includes, excludes, profiles = parseconfig(repo.ui, raw, b'sparse') ctx = repo[rev] if profiles: visited = set() while profiles: profile = profiles.pop() if profile in visited: continue visited.add(profile) try: raw = readprofile(repo, profile, rev) except error.ManifestLookupError: msg = ( b"warning: sparse profile '%s' not found " b"in rev %s - ignoring it\n" % (profile, ctx) ) # experimental config: sparse.missingwarning if repo.ui.configbool(b'sparse', b'missingwarning'): repo.ui.warn(msg) else: repo.ui.debug(msg) continue pincludes, pexcludes, subprofs = parseconfig( repo.ui, raw, b'sparse' ) includes.update(pincludes) excludes.update(pexcludes) profiles.update(subprofs) profiles = visited if includes: includes.add(b'.hg*') return includes, excludes, profiles def activeconfig(repo): """Determine the active sparse config rules. Rules are constructed by reading the current sparse config and bringing in referenced profiles from parents of the working directory. """ revs = [ repo.changelog.rev(node) for node in repo.dirstate.parents() if node != nullid ] allincludes = set() allexcludes = set() allprofiles = set() for rev in revs: includes, excludes, profiles = patternsforrev(repo, rev) allincludes |= includes allexcludes |= excludes allprofiles |= profiles return allincludes, allexcludes, allprofiles def configsignature(repo, includetemp=True): """Obtain the signature string for the current sparse configuration. This is used to construct a cache key for matchers. """ cache = repo._sparsesignaturecache signature = cache.get(b'signature') if includetemp: tempsignature = cache.get(b'tempsignature') else: tempsignature = b'0' if signature is None or (includetemp and tempsignature is None): signature = hex(hashutil.sha1(repo.vfs.tryread(b'sparse')).digest()) cache[b'signature'] = signature if includetemp: raw = repo.vfs.tryread(b'tempsparse') tempsignature = hex(hashutil.sha1(raw).digest()) cache[b'tempsignature'] = tempsignature return b'%s %s' % (signature, tempsignature) def writeconfig(repo, includes, excludes, profiles): """Write the sparse config file given a sparse configuration.""" with repo.vfs(b'sparse', b'wb') as fh: for p in sorted(profiles): fh.write(b'%%include %s\n' % p) if includes: fh.write(b'[include]\n') for i in sorted(includes): fh.write(i) fh.write(b'\n') if excludes: fh.write(b'[exclude]\n') for e in sorted(excludes): fh.write(e) fh.write(b'\n') repo._sparsesignaturecache.clear() def readtemporaryincludes(repo): raw = repo.vfs.tryread(b'tempsparse') if not raw: return set() return set(raw.split(b'\n')) def writetemporaryincludes(repo, includes): repo.vfs.write(b'tempsparse', b'\n'.join(sorted(includes))) repo._sparsesignaturecache.clear() def addtemporaryincludes(repo, additional): includes = readtemporaryincludes(repo) for i in additional: includes.add(i) writetemporaryincludes(repo, includes) def prunetemporaryincludes(repo): if not enabled or not repo.vfs.exists(b'tempsparse'): return s = repo.status() if s.modified or s.added or s.removed or s.deleted: # Still have pending changes. Don't bother trying to prune. return sparsematch = matcher(repo, includetemp=False) dirstate = repo.dirstate mresult = mergemod.mergeresult() dropped = [] tempincludes = readtemporaryincludes(repo) for file in tempincludes: if file in dirstate and not sparsematch(file): message = _(b'dropping temporarily included sparse files') mresult.addfile(file, mergestatemod.ACTION_REMOVE, None, message) dropped.append(file) mergemod.applyupdates( repo, mresult, repo[None], repo[b'.'], False, wantfiledata=False ) # Fix dirstate for file in dropped: dirstate.drop(file) repo.vfs.unlink(b'tempsparse') repo._sparsesignaturecache.clear() msg = _( b'cleaned up %d temporarily added file(s) from the ' b'sparse checkout\n' ) repo.ui.status(msg % len(tempincludes)) def forceincludematcher(matcher, includes): """Returns a matcher that returns true for any of the forced includes before testing against the actual matcher.""" kindpats = [(b'path', include, b'') for include in includes] includematcher = matchmod.includematcher(b'', kindpats) return matchmod.unionmatcher([includematcher, matcher]) def matcher(repo, revs=None, includetemp=True): """Obtain a matcher for sparse working directories for the given revs. If multiple revisions are specified, the matcher is the union of all revs. ``includetemp`` indicates whether to use the temporary sparse profile. """ # If sparse isn't enabled, sparse matcher matches everything. if not enabled: return matchmod.always() if not revs or revs == [None]: revs = [ repo.changelog.rev(node) for node in repo.dirstate.parents() if node != nullid ] signature = configsignature(repo, includetemp=includetemp) key = b'%s %s' % (signature, b' '.join(map(pycompat.bytestr, revs))) result = repo._sparsematchercache.get(key) if result: return result matchers = [] for rev in revs: try: includes, excludes, profiles = patternsforrev(repo, rev) if includes or excludes: matcher = matchmod.match( repo.root, b'', [], include=includes, exclude=excludes, default=b'relpath', ) matchers.append(matcher) except IOError: pass if not matchers: result = matchmod.always() elif len(matchers) == 1: result = matchers[0] else: result = matchmod.unionmatcher(matchers) if includetemp: tempincludes = readtemporaryincludes(repo) result = forceincludematcher(result, tempincludes) repo._sparsematchercache[key] = result return result def filterupdatesactions(repo, wctx, mctx, branchmerge, mresult): """Filter updates to only lay out files that match the sparse rules.""" if not enabled: return oldrevs = [pctx.rev() for pctx in wctx.parents()] oldsparsematch = matcher(repo, oldrevs) if oldsparsematch.always(): return files = set() prunedactions = {} if branchmerge: # If we're merging, use the wctx filter, since we're merging into # the wctx. sparsematch = matcher(repo, [wctx.p1().rev()]) else: # If we're updating, use the target context's filter, since we're # moving to the target context. sparsematch = matcher(repo, [mctx.rev()]) temporaryfiles = [] for file, action in mresult.filemap(): type, args, msg = action files.add(file) if sparsematch(file): prunedactions[file] = action elif type == mergestatemod.ACTION_MERGE: temporaryfiles.append(file) prunedactions[file] = action elif branchmerge: if type not in mergestatemod.NO_OP_ACTIONS: temporaryfiles.append(file) prunedactions[file] = action elif type == mergestatemod.ACTION_FORGET: prunedactions[file] = action elif file in wctx: prunedactions[file] = (mergestatemod.ACTION_REMOVE, args, msg) # in case or rename on one side, it is possible that f1 might not # be present in sparse checkout we should include it # TODO: should we do the same for f2? # exists as a separate check because file can be in sparse and hence # if we try to club this condition in above `elif type == ACTION_MERGE` # it won't be triggered if branchmerge and type == mergestatemod.ACTION_MERGE: f1, f2, fa, move, anc = args if not sparsematch(f1): temporaryfiles.append(f1) if len(temporaryfiles) > 0: repo.ui.status( _( b'temporarily included %d file(s) in the sparse ' b'checkout for merging\n' ) % len(temporaryfiles) ) addtemporaryincludes(repo, temporaryfiles) # Add the new files to the working copy so they can be merged, etc tmresult = mergemod.mergeresult() message = b'temporarily adding to sparse checkout' wctxmanifest = repo[None].manifest() for file in temporaryfiles: if file in wctxmanifest: fctx = repo[None][file] tmresult.addfile( file, mergestatemod.ACTION_GET, (fctx.flags(), False), message, ) mergemod.applyupdates( repo, tmresult, repo[None], repo[b'.'], False, wantfiledata=False ) dirstate = repo.dirstate for file, flags, msg in tmresult.getactions([mergestatemod.ACTION_GET]): dirstate.normal(file) profiles = activeconfig(repo)[2] changedprofiles = profiles & files # If an active profile changed during the update, refresh the checkout. # Don't do this during a branch merge, since all incoming changes should # have been handled by the temporary includes above. if changedprofiles and not branchmerge: mf = mctx.manifest() for file in mf: old = oldsparsematch(file) new = sparsematch(file) if not old and new: flags = mf.flags(file) prunedactions[file] = ( mergestatemod.ACTION_GET, (flags, False), b'', ) elif old and not new: prunedactions[file] = (mergestatemod.ACTION_REMOVE, [], b'') mresult.setactions(prunedactions) def refreshwdir(repo, origstatus, origsparsematch, force=False): """Refreshes working directory by taking sparse config into account. The old status and sparse matcher is compared against the current sparse matcher. Will abort if a file with pending changes is being excluded or included unless ``force`` is True. """ # Verify there are no pending changes pending = set() pending.update(origstatus.modified) pending.update(origstatus.added) pending.update(origstatus.removed) sparsematch = matcher(repo) abort = False for f in pending: if not sparsematch(f): repo.ui.warn(_(b"pending changes to '%s'\n") % f) abort = not force if abort: raise error.Abort( _(b'could not update sparseness due to pending changes') ) # Calculate merge result dirstate = repo.dirstate ctx = repo[b'.'] added = [] lookup = [] dropped = [] mf = ctx.manifest() files = set(mf) mresult = mergemod.mergeresult() for file in files: old = origsparsematch(file) new = sparsematch(file) # Add files that are newly included, or that don't exist in # the dirstate yet. if (new and not old) or (old and new and not file in dirstate): fl = mf.flags(file) if repo.wvfs.exists(file): mresult.addfile(file, mergestatemod.ACTION_EXEC, (fl,), b'') lookup.append(file) else: mresult.addfile( file, mergestatemod.ACTION_GET, (fl, False), b'' ) added.append(file) # Drop files that are newly excluded, or that still exist in # the dirstate. elif (old and not new) or (not old and not new and file in dirstate): dropped.append(file) if file not in pending: mresult.addfile(file, mergestatemod.ACTION_REMOVE, [], b'') # Verify there are no pending changes in newly included files abort = False for file in lookup: repo.ui.warn(_(b"pending changes to '%s'\n") % file) abort = not force if abort: raise error.Abort( _( b'cannot change sparseness due to pending ' b'changes (delete the files or use ' b'--force to bring them back dirty)' ) ) # Check for files that were only in the dirstate. for file, state in pycompat.iteritems(dirstate): if not file in files: old = origsparsematch(file) new = sparsematch(file) if old and not new: dropped.append(file) mergemod.applyupdates( repo, mresult, repo[None], repo[b'.'], False, wantfiledata=False ) # Fix dirstate for file in added: dirstate.normal(file) for file in dropped: dirstate.drop(file) for file in lookup: # File exists on disk, and we're bringing it back in an unknown state. dirstate.normallookup(file) return added, dropped, lookup def aftercommit(repo, node): """Perform actions after a working directory commit.""" # This function is called unconditionally, even if sparse isn't # enabled. ctx = repo[node] profiles = patternsforrev(repo, ctx.rev())[2] # profiles will only have data if sparse is enabled. if profiles & set(ctx.files()): origstatus = repo.status() origsparsematch = matcher(repo) refreshwdir(repo, origstatus, origsparsematch, force=True) prunetemporaryincludes(repo) def _updateconfigandrefreshwdir( repo, includes, excludes, profiles, force=False, removing=False ): """Update the sparse config and working directory state.""" raw = repo.vfs.tryread(b'sparse') oldincludes, oldexcludes, oldprofiles = parseconfig(repo.ui, raw, b'sparse') oldstatus = repo.status() oldmatch = matcher(repo) oldrequires = set(repo.requirements) # TODO remove this try..except once the matcher integrates better # with dirstate. We currently have to write the updated config # because that will invalidate the matcher cache and force a # re-read. We ideally want to update the cached matcher on the # repo instance then flush the new config to disk once wdir is # updated. But this requires massive rework to matcher() and its # consumers. if requirements.SPARSE_REQUIREMENT in oldrequires and removing: repo.requirements.discard(requirements.SPARSE_REQUIREMENT) scmutil.writereporequirements(repo) elif requirements.SPARSE_REQUIREMENT not in oldrequires: repo.requirements.add(requirements.SPARSE_REQUIREMENT) scmutil.writereporequirements(repo) try: writeconfig(repo, includes, excludes, profiles) return refreshwdir(repo, oldstatus, oldmatch, force=force) except Exception: if repo.requirements != oldrequires: repo.requirements.clear() repo.requirements |= oldrequires scmutil.writereporequirements(repo) writeconfig(repo, oldincludes, oldexcludes, oldprofiles) raise def clearrules(repo, force=False): """Clears include/exclude rules from the sparse config. The remaining sparse config only has profiles, if defined. The working directory is refreshed, as needed. """ with repo.wlock(): raw = repo.vfs.tryread(b'sparse') includes, excludes, profiles = parseconfig(repo.ui, raw, b'sparse') if not includes and not excludes: return _updateconfigandrefreshwdir(repo, set(), set(), profiles, force=force) def importfromfiles(repo, opts, paths, force=False): """Import sparse config rules from files. The updated sparse config is written out and the working directory is refreshed, as needed. """ with repo.wlock(): # read current configuration raw = repo.vfs.tryread(b'sparse') includes, excludes, profiles = parseconfig(repo.ui, raw, b'sparse') aincludes, aexcludes, aprofiles = activeconfig(repo) # Import rules on top; only take in rules that are not yet # part of the active rules. changed = False for p in paths: with util.posixfile(util.expandpath(p), mode=b'rb') as fh: raw = fh.read() iincludes, iexcludes, iprofiles = parseconfig( repo.ui, raw, b'sparse' ) oldsize = len(includes) + len(excludes) + len(profiles) includes.update(iincludes - aincludes) excludes.update(iexcludes - aexcludes) profiles.update(iprofiles - aprofiles) if len(includes) + len(excludes) + len(profiles) > oldsize: changed = True profilecount = includecount = excludecount = 0 fcounts = (0, 0, 0) if changed: profilecount = len(profiles - aprofiles) includecount = len(includes - aincludes) excludecount = len(excludes - aexcludes) fcounts = map( len, _updateconfigandrefreshwdir( repo, includes, excludes, profiles, force=force ), ) printchanges( repo.ui, opts, profilecount, includecount, excludecount, *fcounts ) def updateconfig( repo, pats, opts, include=False, exclude=False, reset=False, delete=False, enableprofile=False, disableprofile=False, force=False, usereporootpaths=False, ): """Perform a sparse config update. Only one of the actions may be performed. The new config is written out and a working directory refresh is performed. """ with repo.wlock(): raw = repo.vfs.tryread(b'sparse') oldinclude, oldexclude, oldprofiles = parseconfig( repo.ui, raw, b'sparse' ) if reset: newinclude = set() newexclude = set() newprofiles = set() else: newinclude = set(oldinclude) newexclude = set(oldexclude) newprofiles = set(oldprofiles) if any(os.path.isabs(pat) for pat in pats): raise error.Abort(_(b'paths cannot be absolute')) if not usereporootpaths: # let's treat paths as relative to cwd root, cwd = repo.root, repo.getcwd() abspats = [] for kindpat in pats: kind, pat = matchmod._patsplit(kindpat, None) if kind in matchmod.cwdrelativepatternkinds or kind is None: ap = (kind + b':' if kind else b'') + pathutil.canonpath( root, cwd, pat ) abspats.append(ap) else: abspats.append(kindpat) pats = abspats if include: newinclude.update(pats) elif exclude: newexclude.update(pats) elif enableprofile: newprofiles.update(pats) elif disableprofile: newprofiles.difference_update(pats) elif delete: newinclude.difference_update(pats) newexclude.difference_update(pats) profilecount = len(newprofiles - oldprofiles) - len( oldprofiles - newprofiles ) includecount = len(newinclude - oldinclude) - len( oldinclude - newinclude ) excludecount = len(newexclude - oldexclude) - len( oldexclude - newexclude ) fcounts = map( len, _updateconfigandrefreshwdir( repo, newinclude, newexclude, newprofiles, force=force, removing=reset, ), ) printchanges( repo.ui, opts, profilecount, includecount, excludecount, *fcounts ) def printchanges( ui, opts, profilecount=0, includecount=0, excludecount=0, added=0, dropped=0, conflicting=0, ): """Print output summarizing sparse config changes.""" with ui.formatter(b'sparse', opts) as fm: fm.startitem() fm.condwrite( ui.verbose, b'profiles_added', _(b'Profiles changed: %d\n'), profilecount, ) fm.condwrite( ui.verbose, b'include_rules_added', _(b'Include rules changed: %d\n'), includecount, ) fm.condwrite( ui.verbose, b'exclude_rules_added', _(b'Exclude rules changed: %d\n'), excludecount, ) # In 'plain' verbose mode, mergemod.applyupdates already outputs what # files are added or removed outside of the templating formatter # framework. No point in repeating ourselves in that case. if not fm.isplain(): fm.condwrite( ui.verbose, b'files_added', _(b'Files added: %d\n'), added ) fm.condwrite( ui.verbose, b'files_dropped', _(b'Files dropped: %d\n'), dropped ) fm.condwrite( ui.verbose, b'files_conflicting', _(b'Files conflicting: %d\n'), conflicting, )