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