mercurial/narrowspec.py
changeset 43076 2372284d9457
parent 42813 268662aac075
child 43077 687b865b95ad
equal deleted inserted replaced
43075:57875cf423c9 43076:2372284d9457
     6 # GNU General Public License version 2 or any later version.
     6 # GNU General Public License version 2 or any later version.
     7 
     7 
     8 from __future__ import absolute_import
     8 from __future__ import absolute_import
     9 
     9 
    10 from .i18n import _
    10 from .i18n import _
    11 from .interfaces import (
    11 from .interfaces import repository
    12     repository,
       
    13 )
       
    14 from . import (
    12 from . import (
    15     error,
    13     error,
    16     match as matchmod,
    14     match as matchmod,
    17     merge,
    15     merge,
    18     scmutil,
    16     scmutil,
    34 VALID_PREFIXES = (
    32 VALID_PREFIXES = (
    35     b'path:',
    33     b'path:',
    36     b'rootfilesin:',
    34     b'rootfilesin:',
    37 )
    35 )
    38 
    36 
       
    37 
    39 def normalizesplitpattern(kind, pat):
    38 def normalizesplitpattern(kind, pat):
    40     """Returns the normalized version of a pattern and kind.
    39     """Returns the normalized version of a pattern and kind.
    41 
    40 
    42     Returns a tuple with the normalized kind and normalized pattern.
    41     Returns a tuple with the normalized kind and normalized pattern.
    43     """
    42     """
    44     pat = pat.rstrip('/')
    43     pat = pat.rstrip('/')
    45     _validatepattern(pat)
    44     _validatepattern(pat)
    46     return kind, pat
    45     return kind, pat
       
    46 
    47 
    47 
    48 def _numlines(s):
    48 def _numlines(s):
    49     """Returns the number of lines in s, including ending empty lines."""
    49     """Returns the number of lines in s, including ending empty lines."""
    50     # We use splitlines because it is Unicode-friendly and thus Python 3
    50     # We use splitlines because it is Unicode-friendly and thus Python 3
    51     # compatible. However, it does not count empty lines at the end, so trick
    51     # compatible. However, it does not count empty lines at the end, so trick
    52     # it by adding a character at the end.
    52     # it by adding a character at the end.
    53     return len((s + 'x').splitlines())
    53     return len((s + 'x').splitlines())
    54 
    54 
       
    55 
    55 def _validatepattern(pat):
    56 def _validatepattern(pat):
    56     """Validates the pattern and aborts if it is invalid.
    57     """Validates the pattern and aborts if it is invalid.
    57 
    58 
    58     Patterns are stored in the narrowspec as newline-separated
    59     Patterns are stored in the narrowspec as newline-separated
    59     POSIX-style bytestring paths. There's no escaping.
    60     POSIX-style bytestring paths. There's no escaping.
    66 
    67 
    67     components = pat.split('/')
    68     components = pat.split('/')
    68     if '.' in components or '..' in components:
    69     if '.' in components or '..' in components:
    69         raise error.Abort(_('"." and ".." are not allowed in narrowspec paths'))
    70         raise error.Abort(_('"." and ".." are not allowed in narrowspec paths'))
    70 
    71 
       
    72 
    71 def normalizepattern(pattern, defaultkind='path'):
    73 def normalizepattern(pattern, defaultkind='path'):
    72     """Returns the normalized version of a text-format pattern.
    74     """Returns the normalized version of a text-format pattern.
    73 
    75 
    74     If the pattern has no kind, the default will be added.
    76     If the pattern has no kind, the default will be added.
    75     """
    77     """
    76     kind, pat = matchmod._patsplit(pattern, defaultkind)
    78     kind, pat = matchmod._patsplit(pattern, defaultkind)
    77     return '%s:%s' % normalizesplitpattern(kind, pat)
    79     return '%s:%s' % normalizesplitpattern(kind, pat)
       
    80 
    78 
    81 
    79 def parsepatterns(pats):
    82 def parsepatterns(pats):
    80     """Parses an iterable of patterns into a typed pattern set.
    83     """Parses an iterable of patterns into a typed pattern set.
    81 
    84 
    82     Patterns are assumed to be ``path:`` if no prefix is present.
    85     Patterns are assumed to be ``path:`` if no prefix is present.
    89     """
    92     """
    90     res = {normalizepattern(orig) for orig in pats}
    93     res = {normalizepattern(orig) for orig in pats}
    91     validatepatterns(res)
    94     validatepatterns(res)
    92     return res
    95     return res
    93 
    96 
       
    97 
    94 def validatepatterns(pats):
    98 def validatepatterns(pats):
    95     """Validate that patterns are in the expected data structure and format.
    99     """Validate that patterns are in the expected data structure and format.
    96 
   100 
    97     And that is a set of normalized patterns beginning with ``path:`` or
   101     And that is a set of normalized patterns beginning with ``path:`` or
    98     ``rootfilesin:``.
   102     ``rootfilesin:``.
   100     This function should be used to validate internal data structures
   104     This function should be used to validate internal data structures
   101     and patterns that are loaded from sources that use the internal,
   105     and patterns that are loaded from sources that use the internal,
   102     prefixed pattern representation (but can't necessarily be fully trusted).
   106     prefixed pattern representation (but can't necessarily be fully trusted).
   103     """
   107     """
   104     if not isinstance(pats, set):
   108     if not isinstance(pats, set):
   105         raise error.ProgrammingError('narrow patterns should be a set; '
   109         raise error.ProgrammingError(
   106                                      'got %r' % pats)
   110             'narrow patterns should be a set; ' 'got %r' % pats
       
   111         )
   107 
   112 
   108     for pat in pats:
   113     for pat in pats:
   109         if not pat.startswith(VALID_PREFIXES):
   114         if not pat.startswith(VALID_PREFIXES):
   110             # Use a Mercurial exception because this can happen due to user
   115             # Use a Mercurial exception because this can happen due to user
   111             # bugs (e.g. manually updating spec file).
   116             # bugs (e.g. manually updating spec file).
   112             raise error.Abort(_('invalid prefix on narrow pattern: %s') % pat,
   117             raise error.Abort(
   113                               hint=_('narrow patterns must begin with one of '
   118                 _('invalid prefix on narrow pattern: %s') % pat,
   114                                      'the following: %s') %
   119                 hint=_(
   115                                    ', '.join(VALID_PREFIXES))
   120                     'narrow patterns must begin with one of '
       
   121                     'the following: %s'
       
   122                 )
       
   123                 % ', '.join(VALID_PREFIXES),
       
   124             )
       
   125 
   116 
   126 
   117 def format(includes, excludes):
   127 def format(includes, excludes):
   118     output = '[include]\n'
   128     output = '[include]\n'
   119     for i in sorted(includes - excludes):
   129     for i in sorted(includes - excludes):
   120         output += i + '\n'
   130         output += i + '\n'
   121     output += '[exclude]\n'
   131     output += '[exclude]\n'
   122     for e in sorted(excludes):
   132     for e in sorted(excludes):
   123         output += e + '\n'
   133         output += e + '\n'
   124     return output
   134     return output
   125 
   135 
       
   136 
   126 def match(root, include=None, exclude=None):
   137 def match(root, include=None, exclude=None):
   127     if not include:
   138     if not include:
   128         # Passing empty include and empty exclude to matchmod.match()
   139         # Passing empty include and empty exclude to matchmod.match()
   129         # gives a matcher that matches everything, so explicitly use
   140         # gives a matcher that matches everything, so explicitly use
   130         # the nevermatcher.
   141         # the nevermatcher.
   131         return matchmod.never()
   142         return matchmod.never()
   132     return matchmod.match(root, '', [], include=include or [],
   143     return matchmod.match(
   133                           exclude=exclude or [])
   144         root, '', [], include=include or [], exclude=exclude or []
       
   145     )
       
   146 
   134 
   147 
   135 def parseconfig(ui, spec):
   148 def parseconfig(ui, spec):
   136     # maybe we should care about the profiles returned too
   149     # maybe we should care about the profiles returned too
   137     includepats, excludepats, profiles = sparse.parseconfig(ui, spec, 'narrow')
   150     includepats, excludepats, profiles = sparse.parseconfig(ui, spec, 'narrow')
   138     if profiles:
   151     if profiles:
   139         raise error.Abort(_("including other spec files using '%include' is not"
   152         raise error.Abort(
   140                             " supported in narrowspec"))
   153             _(
       
   154                 "including other spec files using '%include' is not"
       
   155                 " supported in narrowspec"
       
   156             )
       
   157         )
   141 
   158 
   142     validatepatterns(includepats)
   159     validatepatterns(includepats)
   143     validatepatterns(excludepats)
   160     validatepatterns(excludepats)
   144 
   161 
   145     return includepats, excludepats
   162     return includepats, excludepats
       
   163 
   146 
   164 
   147 def load(repo):
   165 def load(repo):
   148     # Treat "narrowspec does not exist" the same as "narrowspec file exists
   166     # Treat "narrowspec does not exist" the same as "narrowspec file exists
   149     # and is empty".
   167     # and is empty".
   150     spec = repo.svfs.tryread(FILENAME)
   168     spec = repo.svfs.tryread(FILENAME)
   151     return parseconfig(repo.ui, spec)
   169     return parseconfig(repo.ui, spec)
   152 
   170 
       
   171 
   153 def save(repo, includepats, excludepats):
   172 def save(repo, includepats, excludepats):
   154     validatepatterns(includepats)
   173     validatepatterns(includepats)
   155     validatepatterns(excludepats)
   174     validatepatterns(excludepats)
   156     spec = format(includepats, excludepats)
   175     spec = format(includepats, excludepats)
   157     repo.svfs.write(FILENAME, spec)
   176     repo.svfs.write(FILENAME, spec)
   158 
   177 
       
   178 
   159 def copytoworkingcopy(repo):
   179 def copytoworkingcopy(repo):
   160     spec = repo.svfs.read(FILENAME)
   180     spec = repo.svfs.read(FILENAME)
   161     repo.vfs.write(DIRSTATE_FILENAME, spec)
   181     repo.vfs.write(DIRSTATE_FILENAME, spec)
   162 
   182 
       
   183 
   163 def savebackup(repo, backupname):
   184 def savebackup(repo, backupname):
   164     if repository.NARROW_REQUIREMENT not in repo.requirements:
   185     if repository.NARROW_REQUIREMENT not in repo.requirements:
   165         return
   186         return
   166     svfs = repo.svfs
   187     svfs = repo.svfs
   167     svfs.tryunlink(backupname)
   188     svfs.tryunlink(backupname)
   168     util.copyfile(svfs.join(FILENAME), svfs.join(backupname), hardlink=True)
   189     util.copyfile(svfs.join(FILENAME), svfs.join(backupname), hardlink=True)
   169 
   190 
       
   191 
   170 def restorebackup(repo, backupname):
   192 def restorebackup(repo, backupname):
   171     if repository.NARROW_REQUIREMENT not in repo.requirements:
   193     if repository.NARROW_REQUIREMENT not in repo.requirements:
   172         return
   194         return
   173     util.rename(repo.svfs.join(backupname), repo.svfs.join(FILENAME))
   195     util.rename(repo.svfs.join(backupname), repo.svfs.join(FILENAME))
       
   196 
   174 
   197 
   175 def savewcbackup(repo, backupname):
   198 def savewcbackup(repo, backupname):
   176     if repository.NARROW_REQUIREMENT not in repo.requirements:
   199     if repository.NARROW_REQUIREMENT not in repo.requirements:
   177         return
   200         return
   178     vfs = repo.vfs
   201     vfs = repo.vfs
   179     vfs.tryunlink(backupname)
   202     vfs.tryunlink(backupname)
   180     # It may not exist in old repos
   203     # It may not exist in old repos
   181     if vfs.exists(DIRSTATE_FILENAME):
   204     if vfs.exists(DIRSTATE_FILENAME):
   182         util.copyfile(vfs.join(DIRSTATE_FILENAME), vfs.join(backupname),
   205         util.copyfile(
   183                       hardlink=True)
   206             vfs.join(DIRSTATE_FILENAME), vfs.join(backupname), hardlink=True
       
   207         )
       
   208 
   184 
   209 
   185 def restorewcbackup(repo, backupname):
   210 def restorewcbackup(repo, backupname):
   186     if repository.NARROW_REQUIREMENT not in repo.requirements:
   211     if repository.NARROW_REQUIREMENT not in repo.requirements:
   187         return
   212         return
   188     # It may not exist in old repos
   213     # It may not exist in old repos
   189     if repo.vfs.exists(backupname):
   214     if repo.vfs.exists(backupname):
   190         util.rename(repo.vfs.join(backupname), repo.vfs.join(DIRSTATE_FILENAME))
   215         util.rename(repo.vfs.join(backupname), repo.vfs.join(DIRSTATE_FILENAME))
   191 
   216 
       
   217 
   192 def clearwcbackup(repo, backupname):
   218 def clearwcbackup(repo, backupname):
   193     if repository.NARROW_REQUIREMENT not in repo.requirements:
   219     if repository.NARROW_REQUIREMENT not in repo.requirements:
   194         return
   220         return
   195     repo.vfs.tryunlink(backupname)
   221     repo.vfs.tryunlink(backupname)
       
   222 
   196 
   223 
   197 def restrictpatterns(req_includes, req_excludes, repo_includes, repo_excludes):
   224 def restrictpatterns(req_includes, req_excludes, repo_includes, repo_excludes):
   198     r""" Restricts the patterns according to repo settings,
   225     r""" Restricts the patterns according to repo settings,
   199     results in a logical AND operation
   226     results in a logical AND operation
   200 
   227 
   245             res_includes = set(res_includes)
   272             res_includes = set(res_includes)
   246     else:
   273     else:
   247         res_includes = set(req_includes)
   274         res_includes = set(req_includes)
   248     return res_includes, res_excludes, invalid_includes
   275     return res_includes, res_excludes, invalid_includes
   249 
   276 
       
   277 
   250 # These two are extracted for extensions (specifically for Google's CitC file
   278 # These two are extracted for extensions (specifically for Google's CitC file
   251 # system)
   279 # system)
   252 def _deletecleanfiles(repo, files):
   280 def _deletecleanfiles(repo, files):
   253     for f in files:
   281     for f in files:
   254         repo.wvfs.unlinkpath(f)
   282         repo.wvfs.unlinkpath(f)
       
   283 
   255 
   284 
   256 def _writeaddedfiles(repo, pctx, files):
   285 def _writeaddedfiles(repo, pctx, files):
   257     actions = merge.emptyactions()
   286     actions = merge.emptyactions()
   258     addgaction = actions[merge.ACTION_GET].append
   287     addgaction = actions[merge.ACTION_GET].append
   259     mf = repo['.'].manifest()
   288     mf = repo['.'].manifest()
   260     for f in files:
   289     for f in files:
   261         if not repo.wvfs.exists(f):
   290         if not repo.wvfs.exists(f):
   262             addgaction((f, (mf.flags(f), False), "narrowspec updated"))
   291             addgaction((f, (mf.flags(f), False), "narrowspec updated"))
   263     merge.applyupdates(repo, actions, wctx=repo[None],
   292     merge.applyupdates(
   264                        mctx=repo['.'], overwrite=False, wantfiledata=False)
   293         repo,
       
   294         actions,
       
   295         wctx=repo[None],
       
   296         mctx=repo['.'],
       
   297         overwrite=False,
       
   298         wantfiledata=False,
       
   299     )
       
   300 
   265 
   301 
   266 def checkworkingcopynarrowspec(repo):
   302 def checkworkingcopynarrowspec(repo):
   267     # Avoid infinite recursion when updating the working copy
   303     # Avoid infinite recursion when updating the working copy
   268     if getattr(repo, '_updatingnarrowspec', False):
   304     if getattr(repo, '_updatingnarrowspec', False):
   269         return
   305         return
   270     storespec = repo.svfs.tryread(FILENAME)
   306     storespec = repo.svfs.tryread(FILENAME)
   271     wcspec = repo.vfs.tryread(DIRSTATE_FILENAME)
   307     wcspec = repo.vfs.tryread(DIRSTATE_FILENAME)
   272     if wcspec != storespec:
   308     if wcspec != storespec:
   273         raise error.Abort(_("working copy's narrowspec is stale"),
   309         raise error.Abort(
   274                           hint=_("run 'hg tracked --update-working-copy'"))
   310             _("working copy's narrowspec is stale"),
       
   311             hint=_("run 'hg tracked --update-working-copy'"),
       
   312         )
       
   313 
   275 
   314 
   276 def updateworkingcopy(repo, assumeclean=False):
   315 def updateworkingcopy(repo, assumeclean=False):
   277     """updates the working copy and dirstate from the store narrowspec
   316     """updates the working copy and dirstate from the store narrowspec
   278 
   317 
   279     When assumeclean=True, files that are not known to be clean will also
   318     When assumeclean=True, files that are not known to be clean will also
   289     newmatch = match(repo.root, include=newincludes, exclude=newexcludes)
   328     newmatch = match(repo.root, include=newincludes, exclude=newexcludes)
   290     addedmatch = matchmod.differencematcher(newmatch, oldmatch)
   329     addedmatch = matchmod.differencematcher(newmatch, oldmatch)
   291     removedmatch = matchmod.differencematcher(oldmatch, newmatch)
   330     removedmatch = matchmod.differencematcher(oldmatch, newmatch)
   292 
   331 
   293     ds = repo.dirstate
   332     ds = repo.dirstate
   294     lookup, status = ds.status(removedmatch, subrepos=[], ignored=True,
   333     lookup, status = ds.status(
   295                                clean=True, unknown=True)
   334         removedmatch, subrepos=[], ignored=True, clean=True, unknown=True
       
   335     )
   296     trackeddirty = status.modified + status.added
   336     trackeddirty = status.modified + status.added
   297     clean = status.clean
   337     clean = status.clean
   298     if assumeclean:
   338     if assumeclean:
   299         assert not trackeddirty
   339         assert not trackeddirty
   300         clean.extend(lookup)
   340         clean.extend(lookup)