##// END OF EJS Templates
dirstate: explicitly write the dirstate after `eol` dirstate manipulation...
marmoute -
r50892:981d2f13 default
parent child Browse files
Show More
@@ -1,479 +1,480 b''
1 1 """automatically manage newlines in repository files
2 2
3 3 This extension allows you to manage the type of line endings (CRLF or
4 4 LF) that are used in the repository and in the local working
5 5 directory. That way you can get CRLF line endings on Windows and LF on
6 6 Unix/Mac, thereby letting everybody use their OS native line endings.
7 7
8 8 The extension reads its configuration from a versioned ``.hgeol``
9 9 configuration file found in the root of the working directory. The
10 10 ``.hgeol`` file use the same syntax as all other Mercurial
11 11 configuration files. It uses two sections, ``[patterns]`` and
12 12 ``[repository]``.
13 13
14 14 The ``[patterns]`` section specifies how line endings should be
15 15 converted between the working directory and the repository. The format is
16 16 specified by a file pattern. The first match is used, so put more
17 17 specific patterns first. The available line endings are ``LF``,
18 18 ``CRLF``, and ``BIN``.
19 19
20 20 Files with the declared format of ``CRLF`` or ``LF`` are always
21 21 checked out and stored in the repository in that format and files
22 22 declared to be binary (``BIN``) are left unchanged. Additionally,
23 23 ``native`` is an alias for checking out in the platform's default line
24 24 ending: ``LF`` on Unix (including Mac OS X) and ``CRLF`` on
25 25 Windows. Note that ``BIN`` (do nothing to line endings) is Mercurial's
26 26 default behavior; it is only needed if you need to override a later,
27 27 more general pattern.
28 28
29 29 The optional ``[repository]`` section specifies the line endings to
30 30 use for files stored in the repository. It has a single setting,
31 31 ``native``, which determines the storage line endings for files
32 32 declared as ``native`` in the ``[patterns]`` section. It can be set to
33 33 ``LF`` or ``CRLF``. The default is ``LF``. For example, this means
34 34 that on Windows, files configured as ``native`` (``CRLF`` by default)
35 35 will be converted to ``LF`` when stored in the repository. Files
36 36 declared as ``LF``, ``CRLF``, or ``BIN`` in the ``[patterns]`` section
37 37 are always stored as-is in the repository.
38 38
39 39 Example versioned ``.hgeol`` file::
40 40
41 41 [patterns]
42 42 **.py = native
43 43 **.vcproj = CRLF
44 44 **.txt = native
45 45 Makefile = LF
46 46 **.jpg = BIN
47 47
48 48 [repository]
49 49 native = LF
50 50
51 51 .. note::
52 52
53 53 The rules will first apply when files are touched in the working
54 54 directory, e.g. by updating to null and back to tip to touch all files.
55 55
56 56 The extension uses an optional ``[eol]`` section read from both the
57 57 normal Mercurial configuration files and the ``.hgeol`` file, with the
58 58 latter overriding the former. You can use that section to control the
59 59 overall behavior. There are three settings:
60 60
61 61 - ``eol.native`` (default ``os.linesep``) can be set to ``LF`` or
62 62 ``CRLF`` to override the default interpretation of ``native`` for
63 63 checkout. This can be used with :hg:`archive` on Unix, say, to
64 64 generate an archive where files have line endings for Windows.
65 65
66 66 - ``eol.only-consistent`` (default True) can be set to False to make
67 67 the extension convert files with inconsistent EOLs. Inconsistent
68 68 means that there is both ``CRLF`` and ``LF`` present in the file.
69 69 Such files are normally not touched under the assumption that they
70 70 have mixed EOLs on purpose.
71 71
72 72 - ``eol.fix-trailing-newline`` (default False) can be set to True to
73 73 ensure that converted files end with a EOL character (either ``\\n``
74 74 or ``\\r\\n`` as per the configured patterns).
75 75
76 76 The extension provides ``cleverencode:`` and ``cleverdecode:`` filters
77 77 like the deprecated win32text extension does. This means that you can
78 78 disable win32text and enable eol and your filters will still work. You
79 79 only need to these filters until you have prepared a ``.hgeol`` file.
80 80
81 81 The ``win32text.forbid*`` hooks provided by the win32text extension
82 82 have been unified into a single hook named ``eol.checkheadshook``. The
83 83 hook will lookup the expected line endings from the ``.hgeol`` file,
84 84 which means you must migrate to a ``.hgeol`` file first before using
85 85 the hook. ``eol.checkheadshook`` only checks heads, intermediate
86 86 invalid revisions will be pushed. To forbid them completely, use the
87 87 ``eol.checkallhook`` hook. These hooks are best used as
88 88 ``pretxnchangegroup`` hooks.
89 89
90 90 See :hg:`help patterns` for more information about the glob patterns
91 91 used.
92 92 """
93 93
94 94
95 95 import os
96 96 import re
97 97 from mercurial.i18n import _
98 98 from mercurial import (
99 99 config,
100 100 error as errormod,
101 101 extensions,
102 102 match,
103 103 pycompat,
104 104 registrar,
105 105 scmutil,
106 106 util,
107 107 )
108 108 from mercurial.utils import stringutil
109 109
110 110 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
111 111 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
112 112 # be specifying the version(s) of Mercurial they are tested with, or
113 113 # leave the attribute unspecified.
114 114 testedwith = b'ships-with-hg-core'
115 115
116 116 configtable = {}
117 117 configitem = registrar.configitem(configtable)
118 118
119 119 configitem(
120 120 b'eol',
121 121 b'fix-trailing-newline',
122 122 default=False,
123 123 )
124 124 configitem(
125 125 b'eol',
126 126 b'native',
127 127 default=pycompat.oslinesep,
128 128 )
129 129 configitem(
130 130 b'eol',
131 131 b'only-consistent',
132 132 default=True,
133 133 )
134 134
135 135 # Matches a lone LF, i.e., one that is not part of CRLF.
136 136 singlelf = re.compile(b'(^|[^\r])\n')
137 137
138 138
139 139 def inconsistenteol(data):
140 140 return b'\r\n' in data and singlelf.search(data)
141 141
142 142
143 143 def tolf(s, params, ui, **kwargs):
144 144 """Filter to convert to LF EOLs."""
145 145 if stringutil.binary(s):
146 146 return s
147 147 if ui.configbool(b'eol', b'only-consistent') and inconsistenteol(s):
148 148 return s
149 149 if (
150 150 ui.configbool(b'eol', b'fix-trailing-newline')
151 151 and s
152 152 and not s.endswith(b'\n')
153 153 ):
154 154 s = s + b'\n'
155 155 return util.tolf(s)
156 156
157 157
158 158 def tocrlf(s, params, ui, **kwargs):
159 159 """Filter to convert to CRLF EOLs."""
160 160 if stringutil.binary(s):
161 161 return s
162 162 if ui.configbool(b'eol', b'only-consistent') and inconsistenteol(s):
163 163 return s
164 164 if (
165 165 ui.configbool(b'eol', b'fix-trailing-newline')
166 166 and s
167 167 and not s.endswith(b'\n')
168 168 ):
169 169 s = s + b'\n'
170 170 return util.tocrlf(s)
171 171
172 172
173 173 def isbinary(s, params, ui, **kwargs):
174 174 """Filter to do nothing with the file."""
175 175 return s
176 176
177 177
178 178 filters = {
179 179 b'to-lf': tolf,
180 180 b'to-crlf': tocrlf,
181 181 b'is-binary': isbinary,
182 182 # The following provide backwards compatibility with win32text
183 183 b'cleverencode:': tolf,
184 184 b'cleverdecode:': tocrlf,
185 185 }
186 186
187 187
188 188 class eolfile:
189 189 def __init__(self, ui, root, data):
190 190 self._decode = {
191 191 b'LF': b'to-lf',
192 192 b'CRLF': b'to-crlf',
193 193 b'BIN': b'is-binary',
194 194 }
195 195 self._encode = {
196 196 b'LF': b'to-lf',
197 197 b'CRLF': b'to-crlf',
198 198 b'BIN': b'is-binary',
199 199 }
200 200
201 201 self.cfg = config.config()
202 202 # Our files should not be touched. The pattern must be
203 203 # inserted first override a '** = native' pattern.
204 204 self.cfg.set(b'patterns', b'.hg*', b'BIN', b'eol')
205 205 # We can then parse the user's patterns.
206 206 self.cfg.parse(b'.hgeol', data)
207 207
208 208 isrepolf = self.cfg.get(b'repository', b'native') != b'CRLF'
209 209 self._encode[b'NATIVE'] = isrepolf and b'to-lf' or b'to-crlf'
210 210 iswdlf = ui.config(b'eol', b'native') in (b'LF', b'\n')
211 211 self._decode[b'NATIVE'] = iswdlf and b'to-lf' or b'to-crlf'
212 212
213 213 include = []
214 214 exclude = []
215 215 self.patterns = []
216 216 for pattern, style in self.cfg.items(b'patterns'):
217 217 key = style.upper()
218 218 if key == b'BIN':
219 219 exclude.append(pattern)
220 220 else:
221 221 include.append(pattern)
222 222 m = match.match(root, b'', [pattern])
223 223 self.patterns.append((pattern, key, m))
224 224 # This will match the files for which we need to care
225 225 # about inconsistent newlines.
226 226 self.match = match.match(root, b'', [], include, exclude)
227 227
228 228 def copytoui(self, ui):
229 229 newpatterns = {pattern for pattern, key, m in self.patterns}
230 230 for section in (b'decode', b'encode'):
231 231 for oldpattern, _filter in ui.configitems(section):
232 232 if oldpattern not in newpatterns:
233 233 if ui.configsource(section, oldpattern) == b'eol':
234 234 ui.setconfig(section, oldpattern, b'!', b'eol')
235 235 for pattern, key, m in self.patterns:
236 236 try:
237 237 ui.setconfig(b'decode', pattern, self._decode[key], b'eol')
238 238 ui.setconfig(b'encode', pattern, self._encode[key], b'eol')
239 239 except KeyError:
240 240 ui.warn(
241 241 _(b"ignoring unknown EOL style '%s' from %s\n")
242 242 % (key, self.cfg.source(b'patterns', pattern))
243 243 )
244 244 # eol.only-consistent can be specified in ~/.hgrc or .hgeol
245 245 for k, v in self.cfg.items(b'eol'):
246 246 ui.setconfig(b'eol', k, v, b'eol')
247 247
248 248 def checkrev(self, repo, ctx, files):
249 249 failed = []
250 250 for f in files or ctx.files():
251 251 if f not in ctx:
252 252 continue
253 253 for pattern, key, m in self.patterns:
254 254 if not m(f):
255 255 continue
256 256 target = self._encode[key]
257 257 data = ctx[f].data()
258 258 if (
259 259 target == b"to-lf"
260 260 and b"\r\n" in data
261 261 or target == b"to-crlf"
262 262 and singlelf.search(data)
263 263 ):
264 264 failed.append((f, target, bytes(ctx)))
265 265 break
266 266 return failed
267 267
268 268
269 269 def parseeol(ui, repo, nodes):
270 270 try:
271 271 for node in nodes:
272 272 try:
273 273 if node is None:
274 274 # Cannot use workingctx.data() since it would load
275 275 # and cache the filters before we configure them.
276 276 data = repo.wvfs(b'.hgeol').read()
277 277 else:
278 278 data = repo[node][b'.hgeol'].data()
279 279 return eolfile(ui, repo.root, data)
280 280 except (IOError, LookupError):
281 281 pass
282 282 except errormod.ConfigError as inst:
283 283 ui.warn(
284 284 _(
285 285 b"warning: ignoring .hgeol file due to parse error "
286 286 b"at %s: %s\n"
287 287 )
288 288 % (inst.location, inst.message)
289 289 )
290 290 return None
291 291
292 292
293 293 def ensureenabled(ui):
294 294 """make sure the extension is enabled when used as hook
295 295
296 296 When eol is used through hooks, the extension is never formally loaded and
297 297 enabled. This has some side effect, for example the config declaration is
298 298 never loaded. This function ensure the extension is enabled when running
299 299 hooks.
300 300 """
301 301 if b'eol' in ui._knownconfig:
302 302 return
303 303 ui.setconfig(b'extensions', b'eol', b'', source=b'internal')
304 304 extensions.loadall(ui, [b'eol'])
305 305
306 306
307 307 def _checkhook(ui, repo, node, headsonly):
308 308 # Get revisions to check and touched files at the same time
309 309 ensureenabled(ui)
310 310 files = set()
311 311 revs = set()
312 312 for rev in range(repo[node].rev(), len(repo)):
313 313 revs.add(rev)
314 314 if headsonly:
315 315 ctx = repo[rev]
316 316 files.update(ctx.files())
317 317 for pctx in ctx.parents():
318 318 revs.discard(pctx.rev())
319 319 failed = []
320 320 for rev in revs:
321 321 ctx = repo[rev]
322 322 eol = parseeol(ui, repo, [ctx.node()])
323 323 if eol:
324 324 failed.extend(eol.checkrev(repo, ctx, files))
325 325
326 326 if failed:
327 327 eols = {b'to-lf': b'CRLF', b'to-crlf': b'LF'}
328 328 msgs = []
329 329 for f, target, node in sorted(failed):
330 330 msgs.append(
331 331 _(b" %s in %s should not have %s line endings")
332 332 % (f, node, eols[target])
333 333 )
334 334 raise errormod.Abort(
335 335 _(b"end-of-line check failed:\n") + b"\n".join(msgs)
336 336 )
337 337
338 338
339 339 def checkallhook(ui, repo, node, hooktype, **kwargs):
340 340 """verify that files have expected EOLs"""
341 341 _checkhook(ui, repo, node, False)
342 342
343 343
344 344 def checkheadshook(ui, repo, node, hooktype, **kwargs):
345 345 """verify that files have expected EOLs"""
346 346 _checkhook(ui, repo, node, True)
347 347
348 348
349 349 # "checkheadshook" used to be called "hook"
350 350 hook = checkheadshook
351 351
352 352
353 353 def preupdate(ui, repo, hooktype, parent1, parent2):
354 354 p1node = scmutil.resolvehexnodeidprefix(repo, parent1)
355 355 repo.loadeol([p1node])
356 356 return False
357 357
358 358
359 359 def uisetup(ui):
360 360 ui.setconfig(b'hooks', b'preupdate.eol', preupdate, b'eol')
361 361
362 362
363 363 def extsetup(ui):
364 364 try:
365 365 extensions.find(b'win32text')
366 366 ui.warn(
367 367 _(
368 368 b"the eol extension is incompatible with the "
369 369 b"win32text extension\n"
370 370 )
371 371 )
372 372 except KeyError:
373 373 pass
374 374
375 375
376 376 def reposetup(ui, repo):
377 377 uisetup(repo.ui)
378 378
379 379 if not repo.local():
380 380 return
381 381 for name, fn in filters.items():
382 382 repo.adddatafilter(name, fn)
383 383
384 384 ui.setconfig(b'patch', b'eol', b'auto', b'eol')
385 385
386 386 class eolrepo(repo.__class__):
387 387 def loadeol(self, nodes):
388 388 eol = parseeol(self.ui, self, nodes)
389 389 if eol is None:
390 390 return None
391 391 eol.copytoui(self.ui)
392 392 return eol.match
393 393
394 394 def _hgcleardirstate(self):
395 395 self._eolmatch = self.loadeol([None])
396 396 if not self._eolmatch:
397 397 self._eolmatch = util.never
398 398 return
399 399
400 400 oldeol = None
401 401 try:
402 402 cachemtime = os.path.getmtime(self.vfs.join(b"eol.cache"))
403 403 except OSError:
404 404 cachemtime = 0
405 405 else:
406 406 olddata = self.vfs.read(b"eol.cache")
407 407 if olddata:
408 408 oldeol = eolfile(self.ui, self.root, olddata)
409 409
410 410 try:
411 411 eolmtime = os.path.getmtime(self.wjoin(b".hgeol"))
412 412 except OSError:
413 413 eolmtime = 0
414 414
415 415 if eolmtime >= cachemtime and eolmtime > 0:
416 416 self.ui.debug(b"eol: detected change in .hgeol\n")
417 417
418 418 hgeoldata = self.wvfs.read(b'.hgeol')
419 419 neweol = eolfile(self.ui, self.root, hgeoldata)
420 420
421 421 wlock = None
422 422 try:
423 423 wlock = self.wlock()
424 424 for f in self.dirstate:
425 425 if not self.dirstate.get_entry(f).maybe_clean:
426 426 continue
427 427 if oldeol is not None:
428 428 if not oldeol.match(f) and not neweol.match(f):
429 429 continue
430 430 oldkey = None
431 431 for pattern, key, m in oldeol.patterns:
432 432 if m(f):
433 433 oldkey = key
434 434 break
435 435 newkey = None
436 436 for pattern, key, m in neweol.patterns:
437 437 if m(f):
438 438 newkey = key
439 439 break
440 440 if oldkey == newkey:
441 441 continue
442 442 # all normal files need to be looked at again since
443 443 # the new .hgeol file specify a different filter
444 444 self.dirstate.set_possibly_dirty(f)
445 445 # Write the cache to update mtime and cache .hgeol
446 446 with self.vfs(b"eol.cache", b"w") as f:
447 447 f.write(hgeoldata)
448 self.dirstate.write(self.currenttransaction())
448 449 except errormod.LockUnavailable:
449 450 # If we cannot lock the repository and clear the
450 451 # dirstate, then a commit might not see all files
451 452 # as modified. But if we cannot lock the
452 453 # repository, then we can also not make a commit,
453 454 # so ignore the error.
454 455 pass
455 456 finally:
456 457 if wlock is not None:
457 458 wlock.release()
458 459
459 460 def commitctx(self, ctx, error=False, origctx=None):
460 461 for f in sorted(ctx.added() + ctx.modified()):
461 462 if not self._eolmatch(f):
462 463 continue
463 464 fctx = ctx[f]
464 465 if fctx is None:
465 466 continue
466 467 data = fctx.data()
467 468 if stringutil.binary(data):
468 469 # We should not abort here, since the user should
469 470 # be able to say "** = native" to automatically
470 471 # have all non-binary files taken care of.
471 472 continue
472 473 if inconsistenteol(data):
473 474 raise errormod.Abort(
474 475 _(b"inconsistent newline style in %s\n") % f
475 476 )
476 477 return super(eolrepo, self).commitctx(ctx, error, origctx)
477 478
478 479 repo.__class__ = eolrepo
479 480 repo._hgcleardirstate()
General Comments 0
You need to be logged in to leave comments. Login now