hgext/eol.py
changeset 43077 687b865b95ad
parent 43076 2372284d9457
child 43105 649d3ac37a12
equal deleted inserted replaced
43076:2372284d9457 43077:687b865b95ad
   110 
   110 
   111 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
   111 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
   112 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
   112 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
   113 # be specifying the version(s) of Mercurial they are tested with, or
   113 # be specifying the version(s) of Mercurial they are tested with, or
   114 # leave the attribute unspecified.
   114 # leave the attribute unspecified.
   115 testedwith = 'ships-with-hg-core'
   115 testedwith = b'ships-with-hg-core'
   116 
   116 
   117 configtable = {}
   117 configtable = {}
   118 configitem = registrar.configitem(configtable)
   118 configitem = registrar.configitem(configtable)
   119 
   119 
   120 configitem(
   120 configitem(
   121     'eol', 'fix-trailing-newline', default=False,
   121     b'eol', b'fix-trailing-newline', default=False,
   122 )
   122 )
   123 configitem(
   123 configitem(
   124     'eol', 'native', default=pycompat.oslinesep,
   124     b'eol', b'native', default=pycompat.oslinesep,
   125 )
   125 )
   126 configitem(
   126 configitem(
   127     'eol', 'only-consistent', default=True,
   127     b'eol', b'only-consistent', default=True,
   128 )
   128 )
   129 
   129 
   130 # Matches a lone LF, i.e., one that is not part of CRLF.
   130 # Matches a lone LF, i.e., one that is not part of CRLF.
   131 singlelf = re.compile('(^|[^\r])\n')
   131 singlelf = re.compile(b'(^|[^\r])\n')
   132 
   132 
   133 
   133 
   134 def inconsistenteol(data):
   134 def inconsistenteol(data):
   135     return '\r\n' in data and singlelf.search(data)
   135     return b'\r\n' in data and singlelf.search(data)
   136 
   136 
   137 
   137 
   138 def tolf(s, params, ui, **kwargs):
   138 def tolf(s, params, ui, **kwargs):
   139     """Filter to convert to LF EOLs."""
   139     """Filter to convert to LF EOLs."""
   140     if stringutil.binary(s):
   140     if stringutil.binary(s):
   141         return s
   141         return s
   142     if ui.configbool('eol', 'only-consistent') and inconsistenteol(s):
   142     if ui.configbool(b'eol', b'only-consistent') and inconsistenteol(s):
   143         return s
   143         return s
   144     if (
   144     if (
   145         ui.configbool('eol', 'fix-trailing-newline')
   145         ui.configbool(b'eol', b'fix-trailing-newline')
   146         and s
   146         and s
   147         and not s.endswith('\n')
   147         and not s.endswith(b'\n')
   148     ):
   148     ):
   149         s = s + '\n'
   149         s = s + b'\n'
   150     return util.tolf(s)
   150     return util.tolf(s)
   151 
   151 
   152 
   152 
   153 def tocrlf(s, params, ui, **kwargs):
   153 def tocrlf(s, params, ui, **kwargs):
   154     """Filter to convert to CRLF EOLs."""
   154     """Filter to convert to CRLF EOLs."""
   155     if stringutil.binary(s):
   155     if stringutil.binary(s):
   156         return s
   156         return s
   157     if ui.configbool('eol', 'only-consistent') and inconsistenteol(s):
   157     if ui.configbool(b'eol', b'only-consistent') and inconsistenteol(s):
   158         return s
   158         return s
   159     if (
   159     if (
   160         ui.configbool('eol', 'fix-trailing-newline')
   160         ui.configbool(b'eol', b'fix-trailing-newline')
   161         and s
   161         and s
   162         and not s.endswith('\n')
   162         and not s.endswith(b'\n')
   163     ):
   163     ):
   164         s = s + '\n'
   164         s = s + b'\n'
   165     return util.tocrlf(s)
   165     return util.tocrlf(s)
   166 
   166 
   167 
   167 
   168 def isbinary(s, params):
   168 def isbinary(s, params):
   169     """Filter to do nothing with the file."""
   169     """Filter to do nothing with the file."""
   170     return s
   170     return s
   171 
   171 
   172 
   172 
   173 filters = {
   173 filters = {
   174     'to-lf': tolf,
   174     b'to-lf': tolf,
   175     'to-crlf': tocrlf,
   175     b'to-crlf': tocrlf,
   176     'is-binary': isbinary,
   176     b'is-binary': isbinary,
   177     # The following provide backwards compatibility with win32text
   177     # The following provide backwards compatibility with win32text
   178     'cleverencode:': tolf,
   178     b'cleverencode:': tolf,
   179     'cleverdecode:': tocrlf,
   179     b'cleverdecode:': tocrlf,
   180 }
   180 }
   181 
   181 
   182 
   182 
   183 class eolfile(object):
   183 class eolfile(object):
   184     def __init__(self, ui, root, data):
   184     def __init__(self, ui, root, data):
   185         self._decode = {'LF': 'to-lf', 'CRLF': 'to-crlf', 'BIN': 'is-binary'}
   185         self._decode = {
   186         self._encode = {'LF': 'to-lf', 'CRLF': 'to-crlf', 'BIN': 'is-binary'}
   186             b'LF': b'to-lf',
       
   187             b'CRLF': b'to-crlf',
       
   188             b'BIN': b'is-binary',
       
   189         }
       
   190         self._encode = {
       
   191             b'LF': b'to-lf',
       
   192             b'CRLF': b'to-crlf',
       
   193             b'BIN': b'is-binary',
       
   194         }
   187 
   195 
   188         self.cfg = config.config()
   196         self.cfg = config.config()
   189         # Our files should not be touched. The pattern must be
   197         # Our files should not be touched. The pattern must be
   190         # inserted first override a '** = native' pattern.
   198         # inserted first override a '** = native' pattern.
   191         self.cfg.set('patterns', '.hg*', 'BIN', 'eol')
   199         self.cfg.set(b'patterns', b'.hg*', b'BIN', b'eol')
   192         # We can then parse the user's patterns.
   200         # We can then parse the user's patterns.
   193         self.cfg.parse('.hgeol', data)
   201         self.cfg.parse(b'.hgeol', data)
   194 
   202 
   195         isrepolf = self.cfg.get('repository', 'native') != 'CRLF'
   203         isrepolf = self.cfg.get(b'repository', b'native') != b'CRLF'
   196         self._encode['NATIVE'] = isrepolf and 'to-lf' or 'to-crlf'
   204         self._encode[b'NATIVE'] = isrepolf and b'to-lf' or b'to-crlf'
   197         iswdlf = ui.config('eol', 'native') in ('LF', '\n')
   205         iswdlf = ui.config(b'eol', b'native') in (b'LF', b'\n')
   198         self._decode['NATIVE'] = iswdlf and 'to-lf' or 'to-crlf'
   206         self._decode[b'NATIVE'] = iswdlf and b'to-lf' or b'to-crlf'
   199 
   207 
   200         include = []
   208         include = []
   201         exclude = []
   209         exclude = []
   202         self.patterns = []
   210         self.patterns = []
   203         for pattern, style in self.cfg.items('patterns'):
   211         for pattern, style in self.cfg.items(b'patterns'):
   204             key = style.upper()
   212             key = style.upper()
   205             if key == 'BIN':
   213             if key == b'BIN':
   206                 exclude.append(pattern)
   214                 exclude.append(pattern)
   207             else:
   215             else:
   208                 include.append(pattern)
   216                 include.append(pattern)
   209             m = match.match(root, '', [pattern])
   217             m = match.match(root, b'', [pattern])
   210             self.patterns.append((pattern, key, m))
   218             self.patterns.append((pattern, key, m))
   211         # This will match the files for which we need to care
   219         # This will match the files for which we need to care
   212         # about inconsistent newlines.
   220         # about inconsistent newlines.
   213         self.match = match.match(root, '', [], include, exclude)
   221         self.match = match.match(root, b'', [], include, exclude)
   214 
   222 
   215     def copytoui(self, ui):
   223     def copytoui(self, ui):
   216         for pattern, key, m in self.patterns:
   224         for pattern, key, m in self.patterns:
   217             try:
   225             try:
   218                 ui.setconfig('decode', pattern, self._decode[key], 'eol')
   226                 ui.setconfig(b'decode', pattern, self._decode[key], b'eol')
   219                 ui.setconfig('encode', pattern, self._encode[key], 'eol')
   227                 ui.setconfig(b'encode', pattern, self._encode[key], b'eol')
   220             except KeyError:
   228             except KeyError:
   221                 ui.warn(
   229                 ui.warn(
   222                     _("ignoring unknown EOL style '%s' from %s\n")
   230                     _(b"ignoring unknown EOL style '%s' from %s\n")
   223                     % (key, self.cfg.source('patterns', pattern))
   231                     % (key, self.cfg.source(b'patterns', pattern))
   224                 )
   232                 )
   225         # eol.only-consistent can be specified in ~/.hgrc or .hgeol
   233         # eol.only-consistent can be specified in ~/.hgrc or .hgeol
   226         for k, v in self.cfg.items('eol'):
   234         for k, v in self.cfg.items(b'eol'):
   227             ui.setconfig('eol', k, v, 'eol')
   235             ui.setconfig(b'eol', k, v, b'eol')
   228 
   236 
   229     def checkrev(self, repo, ctx, files):
   237     def checkrev(self, repo, ctx, files):
   230         failed = []
   238         failed = []
   231         for f in files or ctx.files():
   239         for f in files or ctx.files():
   232             if f not in ctx:
   240             if f not in ctx:
   235                 if not m(f):
   243                 if not m(f):
   236                     continue
   244                     continue
   237                 target = self._encode[key]
   245                 target = self._encode[key]
   238                 data = ctx[f].data()
   246                 data = ctx[f].data()
   239                 if (
   247                 if (
   240                     target == "to-lf"
   248                     target == b"to-lf"
   241                     and "\r\n" in data
   249                     and b"\r\n" in data
   242                     or target == "to-crlf"
   250                     or target == b"to-crlf"
   243                     and singlelf.search(data)
   251                     and singlelf.search(data)
   244                 ):
   252                 ):
   245                     failed.append((f, target, bytes(ctx)))
   253                     failed.append((f, target, bytes(ctx)))
   246                 break
   254                 break
   247         return failed
   255         return failed
   252         for node in nodes:
   260         for node in nodes:
   253             try:
   261             try:
   254                 if node is None:
   262                 if node is None:
   255                     # Cannot use workingctx.data() since it would load
   263                     # Cannot use workingctx.data() since it would load
   256                     # and cache the filters before we configure them.
   264                     # and cache the filters before we configure them.
   257                     data = repo.wvfs('.hgeol').read()
   265                     data = repo.wvfs(b'.hgeol').read()
   258                 else:
   266                 else:
   259                     data = repo[node]['.hgeol'].data()
   267                     data = repo[node][b'.hgeol'].data()
   260                 return eolfile(ui, repo.root, data)
   268                 return eolfile(ui, repo.root, data)
   261             except (IOError, LookupError):
   269             except (IOError, LookupError):
   262                 pass
   270                 pass
   263     except errormod.ParseError as inst:
   271     except errormod.ParseError as inst:
   264         ui.warn(
   272         ui.warn(
   265             _("warning: ignoring .hgeol file due to parse error " "at %s: %s\n")
   273             _(
       
   274                 b"warning: ignoring .hgeol file due to parse error "
       
   275                 b"at %s: %s\n"
       
   276             )
   266             % (inst.args[1], inst.args[0])
   277             % (inst.args[1], inst.args[0])
   267         )
   278         )
   268     return None
   279     return None
   269 
   280 
   270 
   281 
   274     When eol is used through hooks, the extension is never formally loaded and
   285     When eol is used through hooks, the extension is never formally loaded and
   275     enabled. This has some side effect, for example the config declaration is
   286     enabled. This has some side effect, for example the config declaration is
   276     never loaded. This function ensure the extension is enabled when running
   287     never loaded. This function ensure the extension is enabled when running
   277     hooks.
   288     hooks.
   278     """
   289     """
   279     if 'eol' in ui._knownconfig:
   290     if b'eol' in ui._knownconfig:
   280         return
   291         return
   281     ui.setconfig('extensions', 'eol', '', source='internal')
   292     ui.setconfig(b'extensions', b'eol', b'', source=b'internal')
   282     extensions.loadall(ui, ['eol'])
   293     extensions.loadall(ui, [b'eol'])
   283 
   294 
   284 
   295 
   285 def _checkhook(ui, repo, node, headsonly):
   296 def _checkhook(ui, repo, node, headsonly):
   286     # Get revisions to check and touched files at the same time
   297     # Get revisions to check and touched files at the same time
   287     ensureenabled(ui)
   298     ensureenabled(ui)
   300         eol = parseeol(ui, repo, [ctx.node()])
   311         eol = parseeol(ui, repo, [ctx.node()])
   301         if eol:
   312         if eol:
   302             failed.extend(eol.checkrev(repo, ctx, files))
   313             failed.extend(eol.checkrev(repo, ctx, files))
   303 
   314 
   304     if failed:
   315     if failed:
   305         eols = {'to-lf': 'CRLF', 'to-crlf': 'LF'}
   316         eols = {b'to-lf': b'CRLF', b'to-crlf': b'LF'}
   306         msgs = []
   317         msgs = []
   307         for f, target, node in sorted(failed):
   318         for f, target, node in sorted(failed):
   308             msgs.append(
   319             msgs.append(
   309                 _("  %s in %s should not have %s line endings")
   320                 _(b"  %s in %s should not have %s line endings")
   310                 % (f, node, eols[target])
   321                 % (f, node, eols[target])
   311             )
   322             )
   312         raise errormod.Abort(_("end-of-line check failed:\n") + "\n".join(msgs))
   323         raise errormod.Abort(
       
   324             _(b"end-of-line check failed:\n") + b"\n".join(msgs)
       
   325         )
   313 
   326 
   314 
   327 
   315 def checkallhook(ui, repo, node, hooktype, **kwargs):
   328 def checkallhook(ui, repo, node, hooktype, **kwargs):
   316     """verify that files have expected EOLs"""
   329     """verify that files have expected EOLs"""
   317     _checkhook(ui, repo, node, False)
   330     _checkhook(ui, repo, node, False)
   331     repo.loadeol([p1node])
   344     repo.loadeol([p1node])
   332     return False
   345     return False
   333 
   346 
   334 
   347 
   335 def uisetup(ui):
   348 def uisetup(ui):
   336     ui.setconfig('hooks', 'preupdate.eol', preupdate, 'eol')
   349     ui.setconfig(b'hooks', b'preupdate.eol', preupdate, b'eol')
   337 
   350 
   338 
   351 
   339 def extsetup(ui):
   352 def extsetup(ui):
   340     try:
   353     try:
   341         extensions.find('win32text')
   354         extensions.find(b'win32text')
   342         ui.warn(
   355         ui.warn(
   343             _(
   356             _(
   344                 "the eol extension is incompatible with the "
   357                 b"the eol extension is incompatible with the "
   345                 "win32text extension\n"
   358                 b"win32text extension\n"
   346             )
   359             )
   347         )
   360         )
   348     except KeyError:
   361     except KeyError:
   349         pass
   362         pass
   350 
   363 
   355     if not repo.local():
   368     if not repo.local():
   356         return
   369         return
   357     for name, fn in filters.iteritems():
   370     for name, fn in filters.iteritems():
   358         repo.adddatafilter(name, fn)
   371         repo.adddatafilter(name, fn)
   359 
   372 
   360     ui.setconfig('patch', 'eol', 'auto', 'eol')
   373     ui.setconfig(b'patch', b'eol', b'auto', b'eol')
   361 
   374 
   362     class eolrepo(repo.__class__):
   375     class eolrepo(repo.__class__):
   363         def loadeol(self, nodes):
   376         def loadeol(self, nodes):
   364             eol = parseeol(self.ui, self, nodes)
   377             eol = parseeol(self.ui, self, nodes)
   365             if eol is None:
   378             if eol is None:
   366                 return None
   379                 return None
   367             eol.copytoui(self.ui)
   380             eol.copytoui(self.ui)
   368             return eol.match
   381             return eol.match
   369 
   382 
   370         def _hgcleardirstate(self):
   383         def _hgcleardirstate(self):
   371             self._eolmatch = self.loadeol([None, 'tip'])
   384             self._eolmatch = self.loadeol([None, b'tip'])
   372             if not self._eolmatch:
   385             if not self._eolmatch:
   373                 self._eolmatch = util.never
   386                 self._eolmatch = util.never
   374                 return
   387                 return
   375 
   388 
   376             oldeol = None
   389             oldeol = None
   377             try:
   390             try:
   378                 cachemtime = os.path.getmtime(self.vfs.join("eol.cache"))
   391                 cachemtime = os.path.getmtime(self.vfs.join(b"eol.cache"))
   379             except OSError:
   392             except OSError:
   380                 cachemtime = 0
   393                 cachemtime = 0
   381             else:
   394             else:
   382                 olddata = self.vfs.read("eol.cache")
   395                 olddata = self.vfs.read(b"eol.cache")
   383                 if olddata:
   396                 if olddata:
   384                     oldeol = eolfile(self.ui, self.root, olddata)
   397                     oldeol = eolfile(self.ui, self.root, olddata)
   385 
   398 
   386             try:
   399             try:
   387                 eolmtime = os.path.getmtime(self.wjoin(".hgeol"))
   400                 eolmtime = os.path.getmtime(self.wjoin(b".hgeol"))
   388             except OSError:
   401             except OSError:
   389                 eolmtime = 0
   402                 eolmtime = 0
   390 
   403 
   391             if eolmtime > cachemtime:
   404             if eolmtime > cachemtime:
   392                 self.ui.debug("eol: detected change in .hgeol\n")
   405                 self.ui.debug(b"eol: detected change in .hgeol\n")
   393 
   406 
   394                 hgeoldata = self.wvfs.read('.hgeol')
   407                 hgeoldata = self.wvfs.read(b'.hgeol')
   395                 neweol = eolfile(self.ui, self.root, hgeoldata)
   408                 neweol = eolfile(self.ui, self.root, hgeoldata)
   396 
   409 
   397                 wlock = None
   410                 wlock = None
   398                 try:
   411                 try:
   399                     wlock = self.wlock()
   412                     wlock = self.wlock()
   400                     for f in self.dirstate:
   413                     for f in self.dirstate:
   401                         if self.dirstate[f] != 'n':
   414                         if self.dirstate[f] != b'n':
   402                             continue
   415                             continue
   403                         if oldeol is not None:
   416                         if oldeol is not None:
   404                             if not oldeol.match(f) and not neweol.match(f):
   417                             if not oldeol.match(f) and not neweol.match(f):
   405                                 continue
   418                                 continue
   406                             oldkey = None
   419                             oldkey = None
   417                                 continue
   430                                 continue
   418                         # all normal files need to be looked at again since
   431                         # all normal files need to be looked at again since
   419                         # the new .hgeol file specify a different filter
   432                         # the new .hgeol file specify a different filter
   420                         self.dirstate.normallookup(f)
   433                         self.dirstate.normallookup(f)
   421                     # Write the cache to update mtime and cache .hgeol
   434                     # Write the cache to update mtime and cache .hgeol
   422                     with self.vfs("eol.cache", "w") as f:
   435                     with self.vfs(b"eol.cache", b"w") as f:
   423                         f.write(hgeoldata)
   436                         f.write(hgeoldata)
   424                 except errormod.LockUnavailable:
   437                 except errormod.LockUnavailable:
   425                     # If we cannot lock the repository and clear the
   438                     # If we cannot lock the repository and clear the
   426                     # dirstate, then a commit might not see all files
   439                     # dirstate, then a commit might not see all files
   427                     # as modified. But if we cannot lock the
   440                     # as modified. But if we cannot lock the
   445                     # be able to say "** = native" to automatically
   458                     # be able to say "** = native" to automatically
   446                     # have all non-binary files taken care of.
   459                     # have all non-binary files taken care of.
   447                     continue
   460                     continue
   448                 if inconsistenteol(data):
   461                 if inconsistenteol(data):
   449                     raise errormod.Abort(
   462                     raise errormod.Abort(
   450                         _("inconsistent newline style " "in %s\n") % f
   463                         _(b"inconsistent newline style " b"in %s\n") % f
   451                     )
   464                     )
   452             return super(eolrepo, self).commitctx(ctx, error, origctx)
   465             return super(eolrepo, self).commitctx(ctx, error, origctx)
   453 
   466 
   454     repo.__class__ = eolrepo
   467     repo.__class__ = eolrepo
   455     repo._hgcleardirstate()
   468     repo._hgcleardirstate()