mercurial/sparse.py
changeset 43076 2372284d9457
parent 42456 87a34c767384
child 43077 687b865b95ad
equal deleted inserted replaced
43075:57875cf423c9 43076:2372284d9457
    28 # Whether sparse features are enabled. This variable is intended to be
    28 # Whether sparse features are enabled. This variable is intended to be
    29 # temporary to facilitate porting sparse to core. It should eventually be
    29 # temporary to facilitate porting sparse to core. It should eventually be
    30 # a per-repo option, possibly a repo requirement.
    30 # a per-repo option, possibly a repo requirement.
    31 enabled = False
    31 enabled = False
    32 
    32 
       
    33 
    33 def parseconfig(ui, raw, action):
    34 def parseconfig(ui, raw, action):
    34     """Parse sparse config file content.
    35     """Parse sparse config file content.
    35 
    36 
    36     action is the command which is trigerring this read, can be narrow, sparse
    37     action is the command which is trigerring this read, can be narrow, sparse
    37 
    38 
    53             if line:
    54             if line:
    54                 profiles.add(line)
    55                 profiles.add(line)
    55         elif line == '[include]':
    56         elif line == '[include]':
    56             if havesection and current != includes:
    57             if havesection and current != includes:
    57                 # TODO pass filename into this API so we can report it.
    58                 # TODO pass filename into this API so we can report it.
    58                 raise error.Abort(_('%(action)s config cannot have includes '
    59                 raise error.Abort(
    59                                     'after excludes') % {'action': action})
    60                     _(
       
    61                         '%(action)s config cannot have includes '
       
    62                         'after excludes'
       
    63                     )
       
    64                     % {'action': action}
       
    65                 )
    60             havesection = True
    66             havesection = True
    61             current = includes
    67             current = includes
    62             continue
    68             continue
    63         elif line == '[exclude]':
    69         elif line == '[exclude]':
    64             havesection = True
    70             havesection = True
    65             current = excludes
    71             current = excludes
    66         elif line:
    72         elif line:
    67             if current is None:
    73             if current is None:
    68                 raise error.Abort(_('%(action)s config entry outside of '
    74                 raise error.Abort(
    69                                     'section: %(line)s')
    75                     _('%(action)s config entry outside of ' 'section: %(line)s')
    70                                   % {'action': action, 'line': line},
    76                     % {'action': action, 'line': line},
    71                                   hint=_('add an [include] or [exclude] line '
    77                     hint=_(
    72                                          'to declare the entry type'))
    78                         'add an [include] or [exclude] line '
       
    79                         'to declare the entry type'
       
    80                     ),
       
    81                 )
    73 
    82 
    74             if line.strip().startswith('/'):
    83             if line.strip().startswith('/'):
    75                 ui.warn(_('warning: %(action)s profile cannot use'
    84                 ui.warn(
    76                           ' paths starting with /, ignoring %(line)s\n')
    85                     _(
    77                         % {'action': action, 'line': line})
    86                         'warning: %(action)s profile cannot use'
       
    87                         ' paths starting with /, ignoring %(line)s\n'
       
    88                     )
       
    89                     % {'action': action, 'line': line}
       
    90                 )
    78                 continue
    91                 continue
    79             current.add(line)
    92             current.add(line)
    80 
    93 
    81     return includes, excludes, profiles
    94     return includes, excludes, profiles
       
    95 
    82 
    96 
    83 # Exists as separate function to facilitate monkeypatching.
    97 # Exists as separate function to facilitate monkeypatching.
    84 def readprofile(repo, profile, changeid):
    98 def readprofile(repo, profile, changeid):
    85     """Resolve the raw content of a sparse profile file."""
    99     """Resolve the raw content of a sparse profile file."""
    86     # TODO add some kind of cache here because this incurs a manifest
   100     # TODO add some kind of cache here because this incurs a manifest
    87     # resolve and can be slow.
   101     # resolve and can be slow.
    88     return repo.filectx(profile, changeid=changeid).data()
   102     return repo.filectx(profile, changeid=changeid).data()
    89 
   103 
       
   104 
    90 def patternsforrev(repo, rev):
   105 def patternsforrev(repo, rev):
    91     """Obtain sparse checkout patterns for the given rev.
   106     """Obtain sparse checkout patterns for the given rev.
    92 
   107 
    93     Returns a tuple of iterables representing includes, excludes, and
   108     Returns a tuple of iterables representing includes, excludes, and
    94     patterns.
   109     patterns.
   100     raw = repo.vfs.tryread('sparse')
   115     raw = repo.vfs.tryread('sparse')
   101     if not raw:
   116     if not raw:
   102         return set(), set(), set()
   117         return set(), set(), set()
   103 
   118 
   104     if rev is None:
   119     if rev is None:
   105         raise error.Abort(_('cannot parse sparse patterns from working '
   120         raise error.Abort(
   106                             'directory'))
   121             _('cannot parse sparse patterns from working ' 'directory')
       
   122         )
   107 
   123 
   108     includes, excludes, profiles = parseconfig(repo.ui, raw, 'sparse')
   124     includes, excludes, profiles = parseconfig(repo.ui, raw, 'sparse')
   109     ctx = repo[rev]
   125     ctx = repo[rev]
   110 
   126 
   111     if profiles:
   127     if profiles:
   120             try:
   136             try:
   121                 raw = readprofile(repo, profile, rev)
   137                 raw = readprofile(repo, profile, rev)
   122             except error.ManifestLookupError:
   138             except error.ManifestLookupError:
   123                 msg = (
   139                 msg = (
   124                     "warning: sparse profile '%s' not found "
   140                     "warning: sparse profile '%s' not found "
   125                     "in rev %s - ignoring it\n" % (profile, ctx))
   141                     "in rev %s - ignoring it\n" % (profile, ctx)
       
   142                 )
   126                 # experimental config: sparse.missingwarning
   143                 # experimental config: sparse.missingwarning
   127                 if repo.ui.configbool(
   144                 if repo.ui.configbool('sparse', 'missingwarning'):
   128                         'sparse', 'missingwarning'):
       
   129                     repo.ui.warn(msg)
   145                     repo.ui.warn(msg)
   130                 else:
   146                 else:
   131                     repo.ui.debug(msg)
   147                     repo.ui.debug(msg)
   132                 continue
   148                 continue
   133 
   149 
   141     if includes:
   157     if includes:
   142         includes.add('.hg*')
   158         includes.add('.hg*')
   143 
   159 
   144     return includes, excludes, profiles
   160     return includes, excludes, profiles
   145 
   161 
       
   162 
   146 def activeconfig(repo):
   163 def activeconfig(repo):
   147     """Determine the active sparse config rules.
   164     """Determine the active sparse config rules.
   148 
   165 
   149     Rules are constructed by reading the current sparse config and bringing in
   166     Rules are constructed by reading the current sparse config and bringing in
   150     referenced profiles from parents of the working directory.
   167     referenced profiles from parents of the working directory.
   151     """
   168     """
   152     revs = [repo.changelog.rev(node) for node in
   169     revs = [
   153             repo.dirstate.parents() if node != nullid]
   170         repo.changelog.rev(node)
       
   171         for node in repo.dirstate.parents()
       
   172         if node != nullid
       
   173     ]
   154 
   174 
   155     allincludes = set()
   175     allincludes = set()
   156     allexcludes = set()
   176     allexcludes = set()
   157     allprofiles = set()
   177     allprofiles = set()
   158 
   178 
   162         allexcludes |= excludes
   182         allexcludes |= excludes
   163         allprofiles |= profiles
   183         allprofiles |= profiles
   164 
   184 
   165     return allincludes, allexcludes, allprofiles
   185     return allincludes, allexcludes, allprofiles
   166 
   186 
       
   187 
   167 def configsignature(repo, includetemp=True):
   188 def configsignature(repo, includetemp=True):
   168     """Obtain the signature string for the current sparse configuration.
   189     """Obtain the signature string for the current sparse configuration.
   169 
   190 
   170     This is used to construct a cache key for matchers.
   191     This is used to construct a cache key for matchers.
   171     """
   192     """
   186             raw = repo.vfs.tryread('tempsparse')
   207             raw = repo.vfs.tryread('tempsparse')
   187             tempsignature = hex(hashlib.sha1(raw).digest())
   208             tempsignature = hex(hashlib.sha1(raw).digest())
   188             cache['tempsignature'] = tempsignature
   209             cache['tempsignature'] = tempsignature
   189 
   210 
   190     return '%s %s' % (signature, tempsignature)
   211     return '%s %s' % (signature, tempsignature)
       
   212 
   191 
   213 
   192 def writeconfig(repo, includes, excludes, profiles):
   214 def writeconfig(repo, includes, excludes, profiles):
   193     """Write the sparse config file given a sparse configuration."""
   215     """Write the sparse config file given a sparse configuration."""
   194     with repo.vfs('sparse', 'wb') as fh:
   216     with repo.vfs('sparse', 'wb') as fh:
   195         for p in sorted(profiles):
   217         for p in sorted(profiles):
   207                 fh.write(e)
   229                 fh.write(e)
   208                 fh.write('\n')
   230                 fh.write('\n')
   209 
   231 
   210     repo._sparsesignaturecache.clear()
   232     repo._sparsesignaturecache.clear()
   211 
   233 
       
   234 
   212 def readtemporaryincludes(repo):
   235 def readtemporaryincludes(repo):
   213     raw = repo.vfs.tryread('tempsparse')
   236     raw = repo.vfs.tryread('tempsparse')
   214     if not raw:
   237     if not raw:
   215         return set()
   238         return set()
   216 
   239 
   217     return set(raw.split('\n'))
   240     return set(raw.split('\n'))
   218 
   241 
       
   242 
   219 def writetemporaryincludes(repo, includes):
   243 def writetemporaryincludes(repo, includes):
   220     repo.vfs.write('tempsparse', '\n'.join(sorted(includes)))
   244     repo.vfs.write('tempsparse', '\n'.join(sorted(includes)))
   221     repo._sparsesignaturecache.clear()
   245     repo._sparsesignaturecache.clear()
       
   246 
   222 
   247 
   223 def addtemporaryincludes(repo, additional):
   248 def addtemporaryincludes(repo, additional):
   224     includes = readtemporaryincludes(repo)
   249     includes = readtemporaryincludes(repo)
   225     for i in additional:
   250     for i in additional:
   226         includes.add(i)
   251         includes.add(i)
   227     writetemporaryincludes(repo, includes)
   252     writetemporaryincludes(repo, includes)
       
   253 
   228 
   254 
   229 def prunetemporaryincludes(repo):
   255 def prunetemporaryincludes(repo):
   230     if not enabled or not repo.vfs.exists('tempsparse'):
   256     if not enabled or not repo.vfs.exists('tempsparse'):
   231         return
   257         return
   232 
   258 
   246             actions.append((file, None, message))
   272             actions.append((file, None, message))
   247             dropped.append(file)
   273             dropped.append(file)
   248 
   274 
   249     typeactions = mergemod.emptyactions()
   275     typeactions = mergemod.emptyactions()
   250     typeactions['r'] = actions
   276     typeactions['r'] = actions
   251     mergemod.applyupdates(repo, typeactions, repo[None], repo['.'], False,
   277     mergemod.applyupdates(
   252                           wantfiledata=False)
   278         repo, typeactions, repo[None], repo['.'], False, wantfiledata=False
       
   279     )
   253 
   280 
   254     # Fix dirstate
   281     # Fix dirstate
   255     for file in dropped:
   282     for file in dropped:
   256         dirstate.drop(file)
   283         dirstate.drop(file)
   257 
   284 
   258     repo.vfs.unlink('tempsparse')
   285     repo.vfs.unlink('tempsparse')
   259     repo._sparsesignaturecache.clear()
   286     repo._sparsesignaturecache.clear()
   260     msg = _('cleaned up %d temporarily added file(s) from the '
   287     msg = _(
   261             'sparse checkout\n')
   288         'cleaned up %d temporarily added file(s) from the ' 'sparse checkout\n'
       
   289     )
   262     repo.ui.status(msg % len(tempincludes))
   290     repo.ui.status(msg % len(tempincludes))
       
   291 
   263 
   292 
   264 def forceincludematcher(matcher, includes):
   293 def forceincludematcher(matcher, includes):
   265     """Returns a matcher that returns true for any of the forced includes
   294     """Returns a matcher that returns true for any of the forced includes
   266     before testing against the actual matcher."""
   295     before testing against the actual matcher."""
   267     kindpats = [('path', include, '') for include in includes]
   296     kindpats = [('path', include, '') for include in includes]
   268     includematcher = matchmod.includematcher('', kindpats)
   297     includematcher = matchmod.includematcher('', kindpats)
   269     return matchmod.unionmatcher([includematcher, matcher])
   298     return matchmod.unionmatcher([includematcher, matcher])
   270 
   299 
       
   300 
   271 def matcher(repo, revs=None, includetemp=True):
   301 def matcher(repo, revs=None, includetemp=True):
   272     """Obtain a matcher for sparse working directories for the given revs.
   302     """Obtain a matcher for sparse working directories for the given revs.
   273 
   303 
   274     If multiple revisions are specified, the matcher is the union of all
   304     If multiple revisions are specified, the matcher is the union of all
   275     revs.
   305     revs.
   279     # If sparse isn't enabled, sparse matcher matches everything.
   309     # If sparse isn't enabled, sparse matcher matches everything.
   280     if not enabled:
   310     if not enabled:
   281         return matchmod.always()
   311         return matchmod.always()
   282 
   312 
   283     if not revs or revs == [None]:
   313     if not revs or revs == [None]:
   284         revs = [repo.changelog.rev(node)
   314         revs = [
   285                 for node in repo.dirstate.parents() if node != nullid]
   315             repo.changelog.rev(node)
       
   316             for node in repo.dirstate.parents()
       
   317             if node != nullid
       
   318         ]
   286 
   319 
   287     signature = configsignature(repo, includetemp=includetemp)
   320     signature = configsignature(repo, includetemp=includetemp)
   288 
   321 
   289     key = '%s %s' % (signature, ' '.join(map(pycompat.bytestr, revs)))
   322     key = '%s %s' % (signature, ' '.join(map(pycompat.bytestr, revs)))
   290 
   323 
   296     for rev in revs:
   329     for rev in revs:
   297         try:
   330         try:
   298             includes, excludes, profiles = patternsforrev(repo, rev)
   331             includes, excludes, profiles = patternsforrev(repo, rev)
   299 
   332 
   300             if includes or excludes:
   333             if includes or excludes:
   301                 matcher = matchmod.match(repo.root, '', [],
   334                 matcher = matchmod.match(
   302                                          include=includes, exclude=excludes,
   335                     repo.root,
   303                                          default='relpath')
   336                     '',
       
   337                     [],
       
   338                     include=includes,
       
   339                     exclude=excludes,
       
   340                     default='relpath',
       
   341                 )
   304                 matchers.append(matcher)
   342                 matchers.append(matcher)
   305         except IOError:
   343         except IOError:
   306             pass
   344             pass
   307 
   345 
   308     if not matchers:
   346     if not matchers:
   317         result = forceincludematcher(result, tempincludes)
   355         result = forceincludematcher(result, tempincludes)
   318 
   356 
   319     repo._sparsematchercache[key] = result
   357     repo._sparsematchercache[key] = result
   320 
   358 
   321     return result
   359     return result
       
   360 
   322 
   361 
   323 def filterupdatesactions(repo, wctx, mctx, branchmerge, actions):
   362 def filterupdatesactions(repo, wctx, mctx, branchmerge, actions):
   324     """Filter updates to only lay out files that match the sparse rules."""
   363     """Filter updates to only lay out files that match the sparse rules."""
   325     if not enabled:
   364     if not enabled:
   326         return actions
   365         return actions
   365             f1, f2, fa, move, anc = args
   404             f1, f2, fa, move, anc = args
   366             if not sparsematch(f1):
   405             if not sparsematch(f1):
   367                 temporaryfiles.append(f1)
   406                 temporaryfiles.append(f1)
   368 
   407 
   369     if len(temporaryfiles) > 0:
   408     if len(temporaryfiles) > 0:
   370         repo.ui.status(_('temporarily included %d file(s) in the sparse '
   409         repo.ui.status(
   371                          'checkout for merging\n') % len(temporaryfiles))
   410             _(
       
   411                 'temporarily included %d file(s) in the sparse '
       
   412                 'checkout for merging\n'
       
   413             )
       
   414             % len(temporaryfiles)
       
   415         )
   372         addtemporaryincludes(repo, temporaryfiles)
   416         addtemporaryincludes(repo, temporaryfiles)
   373 
   417 
   374         # Add the new files to the working copy so they can be merged, etc
   418         # Add the new files to the working copy so they can be merged, etc
   375         actions = []
   419         actions = []
   376         message = 'temporarily adding to sparse checkout'
   420         message = 'temporarily adding to sparse checkout'
   380                 fctx = repo[None][file]
   424                 fctx = repo[None][file]
   381                 actions.append((file, (fctx.flags(), False), message))
   425                 actions.append((file, (fctx.flags(), False), message))
   382 
   426 
   383         typeactions = mergemod.emptyactions()
   427         typeactions = mergemod.emptyactions()
   384         typeactions['g'] = actions
   428         typeactions['g'] = actions
   385         mergemod.applyupdates(repo, typeactions, repo[None], repo['.'],
   429         mergemod.applyupdates(
   386                               False, wantfiledata=False)
   430             repo, typeactions, repo[None], repo['.'], False, wantfiledata=False
       
   431         )
   387 
   432 
   388         dirstate = repo.dirstate
   433         dirstate = repo.dirstate
   389         for file, flags, msg in actions:
   434         for file, flags, msg in actions:
   390             dirstate.normal(file)
   435             dirstate.normal(file)
   391 
   436 
   405             elif old and not new:
   450             elif old and not new:
   406                 prunedactions[file] = ('r', [], '')
   451                 prunedactions[file] = ('r', [], '')
   407 
   452 
   408     return prunedactions
   453     return prunedactions
   409 
   454 
       
   455 
   410 def refreshwdir(repo, origstatus, origsparsematch, force=False):
   456 def refreshwdir(repo, origstatus, origsparsematch, force=False):
   411     """Refreshes working directory by taking sparse config into account.
   457     """Refreshes working directory by taking sparse config into account.
   412 
   458 
   413     The old status and sparse matcher is compared against the current sparse
   459     The old status and sparse matcher is compared against the current sparse
   414     matcher.
   460     matcher.
   428         if not sparsematch(f):
   474         if not sparsematch(f):
   429             repo.ui.warn(_("pending changes to '%s'\n") % f)
   475             repo.ui.warn(_("pending changes to '%s'\n") % f)
   430             abort = not force
   476             abort = not force
   431 
   477 
   432     if abort:
   478     if abort:
   433         raise error.Abort(_('could not update sparseness due to pending '
   479         raise error.Abort(
   434                             'changes'))
   480             _('could not update sparseness due to pending ' 'changes')
       
   481         )
   435 
   482 
   436     # Calculate actions
   483     # Calculate actions
   437     dirstate = repo.dirstate
   484     dirstate = repo.dirstate
   438     ctx = repo['.']
   485     ctx = repo['.']
   439     added = []
   486     added = []
   468     abort = False
   515     abort = False
   469     for file in lookup:
   516     for file in lookup:
   470         repo.ui.warn(_("pending changes to '%s'\n") % file)
   517         repo.ui.warn(_("pending changes to '%s'\n") % file)
   471         abort = not force
   518         abort = not force
   472     if abort:
   519     if abort:
   473         raise error.Abort(_('cannot change sparseness due to pending '
   520         raise error.Abort(
   474                             'changes (delete the files or use '
   521             _(
   475                             '--force to bring them back dirty)'))
   522                 'cannot change sparseness due to pending '
       
   523                 'changes (delete the files or use '
       
   524                 '--force to bring them back dirty)'
       
   525             )
       
   526         )
   476 
   527 
   477     # Check for files that were only in the dirstate.
   528     # Check for files that were only in the dirstate.
   478     for file, state in dirstate.iteritems():
   529     for file, state in dirstate.iteritems():
   479         if not file in files:
   530         if not file in files:
   480             old = origsparsematch(file)
   531             old = origsparsematch(file)
   485     # Apply changes to disk
   536     # Apply changes to disk
   486     typeactions = mergemod.emptyactions()
   537     typeactions = mergemod.emptyactions()
   487     for f, (m, args, msg) in actions.iteritems():
   538     for f, (m, args, msg) in actions.iteritems():
   488         typeactions[m].append((f, args, msg))
   539         typeactions[m].append((f, args, msg))
   489 
   540 
   490     mergemod.applyupdates(repo, typeactions, repo[None], repo['.'], False,
   541     mergemod.applyupdates(
   491                           wantfiledata=False)
   542         repo, typeactions, repo[None], repo['.'], False, wantfiledata=False
       
   543     )
   492 
   544 
   493     # Fix dirstate
   545     # Fix dirstate
   494     for file in added:
   546     for file in added:
   495         dirstate.normal(file)
   547         dirstate.normal(file)
   496 
   548 
   500     for file in lookup:
   552     for file in lookup:
   501         # File exists on disk, and we're bringing it back in an unknown state.
   553         # File exists on disk, and we're bringing it back in an unknown state.
   502         dirstate.normallookup(file)
   554         dirstate.normallookup(file)
   503 
   555 
   504     return added, dropped, lookup
   556     return added, dropped, lookup
       
   557 
   505 
   558 
   506 def aftercommit(repo, node):
   559 def aftercommit(repo, node):
   507     """Perform actions after a working directory commit."""
   560     """Perform actions after a working directory commit."""
   508     # This function is called unconditionally, even if sparse isn't
   561     # This function is called unconditionally, even if sparse isn't
   509     # enabled.
   562     # enabled.
   517         origsparsematch = matcher(repo)
   570         origsparsematch = matcher(repo)
   518         refreshwdir(repo, origstatus, origsparsematch, force=True)
   571         refreshwdir(repo, origstatus, origsparsematch, force=True)
   519 
   572 
   520     prunetemporaryincludes(repo)
   573     prunetemporaryincludes(repo)
   521 
   574 
   522 def _updateconfigandrefreshwdir(repo, includes, excludes, profiles,
   575 
   523                                 force=False, removing=False):
   576 def _updateconfigandrefreshwdir(
       
   577     repo, includes, excludes, profiles, force=False, removing=False
       
   578 ):
   524     """Update the sparse config and working directory state."""
   579     """Update the sparse config and working directory state."""
   525     raw = repo.vfs.tryread('sparse')
   580     raw = repo.vfs.tryread('sparse')
   526     oldincludes, oldexcludes, oldprofiles = parseconfig(repo.ui, raw, 'sparse')
   581     oldincludes, oldexcludes, oldprofiles = parseconfig(repo.ui, raw, 'sparse')
   527 
   582 
   528     oldstatus = repo.status()
   583     oldstatus = repo.status()
   553             repo.requirements |= oldrequires
   608             repo.requirements |= oldrequires
   554             scmutil.writerequires(repo.vfs, repo.requirements)
   609             scmutil.writerequires(repo.vfs, repo.requirements)
   555         writeconfig(repo, oldincludes, oldexcludes, oldprofiles)
   610         writeconfig(repo, oldincludes, oldexcludes, oldprofiles)
   556         raise
   611         raise
   557 
   612 
       
   613 
   558 def clearrules(repo, force=False):
   614 def clearrules(repo, force=False):
   559     """Clears include/exclude rules from the sparse config.
   615     """Clears include/exclude rules from the sparse config.
   560 
   616 
   561     The remaining sparse config only has profiles, if defined. The working
   617     The remaining sparse config only has profiles, if defined. The working
   562     directory is refreshed, as needed.
   618     directory is refreshed, as needed.
   567 
   623 
   568         if not includes and not excludes:
   624         if not includes and not excludes:
   569             return
   625             return
   570 
   626 
   571         _updateconfigandrefreshwdir(repo, set(), set(), profiles, force=force)
   627         _updateconfigandrefreshwdir(repo, set(), set(), profiles, force=force)
       
   628 
   572 
   629 
   573 def importfromfiles(repo, opts, paths, force=False):
   630 def importfromfiles(repo, opts, paths, force=False):
   574     """Import sparse config rules from files.
   631     """Import sparse config rules from files.
   575 
   632 
   576     The updated sparse config is written out and the working directory
   633     The updated sparse config is written out and the working directory
   587         changed = False
   644         changed = False
   588         for p in paths:
   645         for p in paths:
   589             with util.posixfile(util.expandpath(p), mode='rb') as fh:
   646             with util.posixfile(util.expandpath(p), mode='rb') as fh:
   590                 raw = fh.read()
   647                 raw = fh.read()
   591 
   648 
   592             iincludes, iexcludes, iprofiles = parseconfig(repo.ui, raw,
   649             iincludes, iexcludes, iprofiles = parseconfig(
   593                                                           'sparse')
   650                 repo.ui, raw, 'sparse'
       
   651             )
   594             oldsize = len(includes) + len(excludes) + len(profiles)
   652             oldsize = len(includes) + len(excludes) + len(profiles)
   595             includes.update(iincludes - aincludes)
   653             includes.update(iincludes - aincludes)
   596             excludes.update(iexcludes - aexcludes)
   654             excludes.update(iexcludes - aexcludes)
   597             profiles.update(iprofiles - aprofiles)
   655             profiles.update(iprofiles - aprofiles)
   598             if len(includes) + len(excludes) + len(profiles) > oldsize:
   656             if len(includes) + len(excludes) + len(profiles) > oldsize:
   604         if changed:
   662         if changed:
   605             profilecount = len(profiles - aprofiles)
   663             profilecount = len(profiles - aprofiles)
   606             includecount = len(includes - aincludes)
   664             includecount = len(includes - aincludes)
   607             excludecount = len(excludes - aexcludes)
   665             excludecount = len(excludes - aexcludes)
   608 
   666 
   609             fcounts = map(len, _updateconfigandrefreshwdir(
   667             fcounts = map(
   610                 repo, includes, excludes, profiles, force=force))
   668                 len,
   611 
   669                 _updateconfigandrefreshwdir(
   612         printchanges(repo.ui, opts, profilecount, includecount, excludecount,
   670                     repo, includes, excludes, profiles, force=force
   613                      *fcounts)
   671                 ),
   614 
   672             )
   615 def updateconfig(repo, pats, opts, include=False, exclude=False, reset=False,
   673 
   616                  delete=False, enableprofile=False, disableprofile=False,
   674         printchanges(
   617                  force=False, usereporootpaths=False):
   675             repo.ui, opts, profilecount, includecount, excludecount, *fcounts
       
   676         )
       
   677 
       
   678 
       
   679 def updateconfig(
       
   680     repo,
       
   681     pats,
       
   682     opts,
       
   683     include=False,
       
   684     exclude=False,
       
   685     reset=False,
       
   686     delete=False,
       
   687     enableprofile=False,
       
   688     disableprofile=False,
       
   689     force=False,
       
   690     usereporootpaths=False,
       
   691 ):
   618     """Perform a sparse config update.
   692     """Perform a sparse config update.
   619 
   693 
   620     Only one of the actions may be performed.
   694     Only one of the actions may be performed.
   621 
   695 
   622     The new config is written out and a working directory refresh is performed.
   696     The new config is written out and a working directory refresh is performed.
   623     """
   697     """
   624     with repo.wlock():
   698     with repo.wlock():
   625         raw = repo.vfs.tryread('sparse')
   699         raw = repo.vfs.tryread('sparse')
   626         oldinclude, oldexclude, oldprofiles = parseconfig(repo.ui, raw,
   700         oldinclude, oldexclude, oldprofiles = parseconfig(
   627                                                           'sparse')
   701             repo.ui, raw, 'sparse'
       
   702         )
   628 
   703 
   629         if reset:
   704         if reset:
   630             newinclude = set()
   705             newinclude = set()
   631             newexclude = set()
   706             newexclude = set()
   632             newprofiles = set()
   707             newprofiles = set()
   643             root, cwd = repo.root, repo.getcwd()
   718             root, cwd = repo.root, repo.getcwd()
   644             abspats = []
   719             abspats = []
   645             for kindpat in pats:
   720             for kindpat in pats:
   646                 kind, pat = matchmod._patsplit(kindpat, None)
   721                 kind, pat = matchmod._patsplit(kindpat, None)
   647                 if kind in matchmod.cwdrelativepatternkinds or kind is None:
   722                 if kind in matchmod.cwdrelativepatternkinds or kind is None:
   648                     ap = ((kind + ':' if kind else '') +
   723                     ap = (kind + ':' if kind else '') + pathutil.canonpath(
   649                           pathutil.canonpath(root, cwd, pat))
   724                         root, cwd, pat
       
   725                     )
   650                     abspats.append(ap)
   726                     abspats.append(ap)
   651                 else:
   727                 else:
   652                     abspats.append(kindpat)
   728                     abspats.append(kindpat)
   653             pats = abspats
   729             pats = abspats
   654 
   730 
   662             newprofiles.difference_update(pats)
   738             newprofiles.difference_update(pats)
   663         elif delete:
   739         elif delete:
   664             newinclude.difference_update(pats)
   740             newinclude.difference_update(pats)
   665             newexclude.difference_update(pats)
   741             newexclude.difference_update(pats)
   666 
   742 
   667         profilecount = (len(newprofiles - oldprofiles) -
   743         profilecount = len(newprofiles - oldprofiles) - len(
   668                         len(oldprofiles - newprofiles))
   744             oldprofiles - newprofiles
   669         includecount = (len(newinclude - oldinclude) -
   745         )
   670                         len(oldinclude - newinclude))
   746         includecount = len(newinclude - oldinclude) - len(
   671         excludecount = (len(newexclude - oldexclude) -
   747             oldinclude - newinclude
   672                         len(oldexclude - newexclude))
   748         )
   673 
   749         excludecount = len(newexclude - oldexclude) - len(
   674         fcounts = map(len, _updateconfigandrefreshwdir(
   750             oldexclude - newexclude
   675             repo, newinclude, newexclude, newprofiles, force=force,
   751         )
   676             removing=reset))
   752 
   677 
   753         fcounts = map(
   678         printchanges(repo.ui, opts, profilecount, includecount,
   754             len,
   679                      excludecount, *fcounts)
   755             _updateconfigandrefreshwdir(
   680 
   756                 repo,
   681 def printchanges(ui, opts, profilecount=0, includecount=0, excludecount=0,
   757                 newinclude,
   682                  added=0, dropped=0, conflicting=0):
   758                 newexclude,
       
   759                 newprofiles,
       
   760                 force=force,
       
   761                 removing=reset,
       
   762             ),
       
   763         )
       
   764 
       
   765         printchanges(
       
   766             repo.ui, opts, profilecount, includecount, excludecount, *fcounts
       
   767         )
       
   768 
       
   769 
       
   770 def printchanges(
       
   771     ui,
       
   772     opts,
       
   773     profilecount=0,
       
   774     includecount=0,
       
   775     excludecount=0,
       
   776     added=0,
       
   777     dropped=0,
       
   778     conflicting=0,
       
   779 ):
   683     """Print output summarizing sparse config changes."""
   780     """Print output summarizing sparse config changes."""
   684     with ui.formatter('sparse', opts) as fm:
   781     with ui.formatter('sparse', opts) as fm:
   685         fm.startitem()
   782         fm.startitem()
   686         fm.condwrite(ui.verbose, 'profiles_added', _('Profiles changed: %d\n'),
   783         fm.condwrite(
   687                      profilecount)
   784             ui.verbose,
   688         fm.condwrite(ui.verbose, 'include_rules_added',
   785             'profiles_added',
   689                      _('Include rules changed: %d\n'), includecount)
   786             _('Profiles changed: %d\n'),
   690         fm.condwrite(ui.verbose, 'exclude_rules_added',
   787             profilecount,
   691                      _('Exclude rules changed: %d\n'), excludecount)
   788         )
       
   789         fm.condwrite(
       
   790             ui.verbose,
       
   791             'include_rules_added',
       
   792             _('Include rules changed: %d\n'),
       
   793             includecount,
       
   794         )
       
   795         fm.condwrite(
       
   796             ui.verbose,
       
   797             'exclude_rules_added',
       
   798             _('Exclude rules changed: %d\n'),
       
   799             excludecount,
       
   800         )
   692 
   801 
   693         # In 'plain' verbose mode, mergemod.applyupdates already outputs what
   802         # In 'plain' verbose mode, mergemod.applyupdates already outputs what
   694         # files are added or removed outside of the templating formatter
   803         # files are added or removed outside of the templating formatter
   695         # framework. No point in repeating ourselves in that case.
   804         # framework. No point in repeating ourselves in that case.
   696         if not fm.isplain():
   805         if not fm.isplain():
   697             fm.condwrite(ui.verbose, 'files_added', _('Files added: %d\n'),
   806             fm.condwrite(
   698                          added)
   807                 ui.verbose, 'files_added', _('Files added: %d\n'), added
   699             fm.condwrite(ui.verbose, 'files_dropped', _('Files dropped: %d\n'),
   808             )
   700                          dropped)
   809             fm.condwrite(
   701             fm.condwrite(ui.verbose, 'files_conflicting',
   810                 ui.verbose, 'files_dropped', _('Files dropped: %d\n'), dropped
   702                          _('Files conflicting: %d\n'), conflicting)
   811             )
       
   812             fm.condwrite(
       
   813                 ui.verbose,
       
   814                 'files_conflicting',
       
   815                 _('Files conflicting: %d\n'),
       
   816                 conflicting,
       
   817             )