##// END OF EJS Templates
py3: require values in changelog extras to be bytes...
Martin von Zweigbergk -
r45095:21414275 stable
parent child Browse files
Show More
@@ -1,626 +1,623 b''
1 # changelog.py - changelog class for mercurial
1 # changelog.py - changelog class for mercurial
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
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 .node import (
11 from .node import (
12 bin,
12 bin,
13 hex,
13 hex,
14 nullid,
14 nullid,
15 )
15 )
16 from .thirdparty import attr
16 from .thirdparty import attr
17
17
18 from . import (
18 from . import (
19 copies,
19 copies,
20 encoding,
20 encoding,
21 error,
21 error,
22 pycompat,
22 pycompat,
23 revlog,
23 revlog,
24 )
24 )
25 from .utils import (
25 from .utils import (
26 dateutil,
26 dateutil,
27 stringutil,
27 stringutil,
28 )
28 )
29
29
30 from .revlogutils import sidedata as sidedatamod
30 from .revlogutils import sidedata as sidedatamod
31
31
32 _defaultextra = {b'branch': b'default'}
32 _defaultextra = {b'branch': b'default'}
33
33
34
34
35 def _string_escape(text):
35 def _string_escape(text):
36 """
36 """
37 >>> from .pycompat import bytechr as chr
37 >>> from .pycompat import bytechr as chr
38 >>> d = {b'nl': chr(10), b'bs': chr(92), b'cr': chr(13), b'nul': chr(0)}
38 >>> d = {b'nl': chr(10), b'bs': chr(92), b'cr': chr(13), b'nul': chr(0)}
39 >>> s = b"ab%(nl)scd%(bs)s%(bs)sn%(nul)s12ab%(cr)scd%(bs)s%(nl)s" % d
39 >>> s = b"ab%(nl)scd%(bs)s%(bs)sn%(nul)s12ab%(cr)scd%(bs)s%(nl)s" % d
40 >>> s
40 >>> s
41 'ab\\ncd\\\\\\\\n\\x0012ab\\rcd\\\\\\n'
41 'ab\\ncd\\\\\\\\n\\x0012ab\\rcd\\\\\\n'
42 >>> res = _string_escape(s)
42 >>> res = _string_escape(s)
43 >>> s == _string_unescape(res)
43 >>> s == _string_unescape(res)
44 True
44 True
45 """
45 """
46 # subset of the string_escape codec
46 # subset of the string_escape codec
47 text = (
47 text = (
48 text.replace(b'\\', b'\\\\')
48 text.replace(b'\\', b'\\\\')
49 .replace(b'\n', b'\\n')
49 .replace(b'\n', b'\\n')
50 .replace(b'\r', b'\\r')
50 .replace(b'\r', b'\\r')
51 )
51 )
52 return text.replace(b'\0', b'\\0')
52 return text.replace(b'\0', b'\\0')
53
53
54
54
55 def _string_unescape(text):
55 def _string_unescape(text):
56 if b'\\0' in text:
56 if b'\\0' in text:
57 # fix up \0 without getting into trouble with \\0
57 # fix up \0 without getting into trouble with \\0
58 text = text.replace(b'\\\\', b'\\\\\n')
58 text = text.replace(b'\\\\', b'\\\\\n')
59 text = text.replace(b'\\0', b'\0')
59 text = text.replace(b'\\0', b'\0')
60 text = text.replace(b'\n', b'')
60 text = text.replace(b'\n', b'')
61 return stringutil.unescapestr(text)
61 return stringutil.unescapestr(text)
62
62
63
63
64 def decodeextra(text):
64 def decodeextra(text):
65 """
65 """
66 >>> from .pycompat import bytechr as chr
66 >>> from .pycompat import bytechr as chr
67 >>> sorted(decodeextra(encodeextra({b'foo': b'bar', b'baz': chr(0) + b'2'})
67 >>> sorted(decodeextra(encodeextra({b'foo': b'bar', b'baz': chr(0) + b'2'})
68 ... ).items())
68 ... ).items())
69 [('baz', '\\x002'), ('branch', 'default'), ('foo', 'bar')]
69 [('baz', '\\x002'), ('branch', 'default'), ('foo', 'bar')]
70 >>> sorted(decodeextra(encodeextra({b'foo': b'bar',
70 >>> sorted(decodeextra(encodeextra({b'foo': b'bar',
71 ... b'baz': chr(92) + chr(0) + b'2'})
71 ... b'baz': chr(92) + chr(0) + b'2'})
72 ... ).items())
72 ... ).items())
73 [('baz', '\\\\\\x002'), ('branch', 'default'), ('foo', 'bar')]
73 [('baz', '\\\\\\x002'), ('branch', 'default'), ('foo', 'bar')]
74 """
74 """
75 extra = _defaultextra.copy()
75 extra = _defaultextra.copy()
76 for l in text.split(b'\0'):
76 for l in text.split(b'\0'):
77 if l:
77 if l:
78 k, v = _string_unescape(l).split(b':', 1)
78 k, v = _string_unescape(l).split(b':', 1)
79 extra[k] = v
79 extra[k] = v
80 return extra
80 return extra
81
81
82
82
83 def encodeextra(d):
83 def encodeextra(d):
84 # keys must be sorted to produce a deterministic changelog entry
84 # keys must be sorted to produce a deterministic changelog entry
85 items = [
85 items = [_string_escape(b'%s:%s' % (k, d[k])) for k in sorted(d)]
86 _string_escape(b'%s:%s' % (k, pycompat.bytestr(d[k])))
87 for k in sorted(d)
88 ]
89 return b"\0".join(items)
86 return b"\0".join(items)
90
87
91
88
92 def stripdesc(desc):
89 def stripdesc(desc):
93 """strip trailing whitespace and leading and trailing empty lines"""
90 """strip trailing whitespace and leading and trailing empty lines"""
94 return b'\n'.join([l.rstrip() for l in desc.splitlines()]).strip(b'\n')
91 return b'\n'.join([l.rstrip() for l in desc.splitlines()]).strip(b'\n')
95
92
96
93
97 class appender(object):
94 class appender(object):
98 '''the changelog index must be updated last on disk, so we use this class
95 '''the changelog index must be updated last on disk, so we use this class
99 to delay writes to it'''
96 to delay writes to it'''
100
97
101 def __init__(self, vfs, name, mode, buf):
98 def __init__(self, vfs, name, mode, buf):
102 self.data = buf
99 self.data = buf
103 fp = vfs(name, mode)
100 fp = vfs(name, mode)
104 self.fp = fp
101 self.fp = fp
105 self.offset = fp.tell()
102 self.offset = fp.tell()
106 self.size = vfs.fstat(fp).st_size
103 self.size = vfs.fstat(fp).st_size
107 self._end = self.size
104 self._end = self.size
108
105
109 def end(self):
106 def end(self):
110 return self._end
107 return self._end
111
108
112 def tell(self):
109 def tell(self):
113 return self.offset
110 return self.offset
114
111
115 def flush(self):
112 def flush(self):
116 pass
113 pass
117
114
118 @property
115 @property
119 def closed(self):
116 def closed(self):
120 return self.fp.closed
117 return self.fp.closed
121
118
122 def close(self):
119 def close(self):
123 self.fp.close()
120 self.fp.close()
124
121
125 def seek(self, offset, whence=0):
122 def seek(self, offset, whence=0):
126 '''virtual file offset spans real file and data'''
123 '''virtual file offset spans real file and data'''
127 if whence == 0:
124 if whence == 0:
128 self.offset = offset
125 self.offset = offset
129 elif whence == 1:
126 elif whence == 1:
130 self.offset += offset
127 self.offset += offset
131 elif whence == 2:
128 elif whence == 2:
132 self.offset = self.end() + offset
129 self.offset = self.end() + offset
133 if self.offset < self.size:
130 if self.offset < self.size:
134 self.fp.seek(self.offset)
131 self.fp.seek(self.offset)
135
132
136 def read(self, count=-1):
133 def read(self, count=-1):
137 '''only trick here is reads that span real file and data'''
134 '''only trick here is reads that span real file and data'''
138 ret = b""
135 ret = b""
139 if self.offset < self.size:
136 if self.offset < self.size:
140 s = self.fp.read(count)
137 s = self.fp.read(count)
141 ret = s
138 ret = s
142 self.offset += len(s)
139 self.offset += len(s)
143 if count > 0:
140 if count > 0:
144 count -= len(s)
141 count -= len(s)
145 if count != 0:
142 if count != 0:
146 doff = self.offset - self.size
143 doff = self.offset - self.size
147 self.data.insert(0, b"".join(self.data))
144 self.data.insert(0, b"".join(self.data))
148 del self.data[1:]
145 del self.data[1:]
149 s = self.data[0][doff : doff + count]
146 s = self.data[0][doff : doff + count]
150 self.offset += len(s)
147 self.offset += len(s)
151 ret += s
148 ret += s
152 return ret
149 return ret
153
150
154 def write(self, s):
151 def write(self, s):
155 self.data.append(bytes(s))
152 self.data.append(bytes(s))
156 self.offset += len(s)
153 self.offset += len(s)
157 self._end += len(s)
154 self._end += len(s)
158
155
159 def __enter__(self):
156 def __enter__(self):
160 self.fp.__enter__()
157 self.fp.__enter__()
161 return self
158 return self
162
159
163 def __exit__(self, *args):
160 def __exit__(self, *args):
164 return self.fp.__exit__(*args)
161 return self.fp.__exit__(*args)
165
162
166
163
167 def _divertopener(opener, target):
164 def _divertopener(opener, target):
168 """build an opener that writes in 'target.a' instead of 'target'"""
165 """build an opener that writes in 'target.a' instead of 'target'"""
169
166
170 def _divert(name, mode=b'r', checkambig=False, **kwargs):
167 def _divert(name, mode=b'r', checkambig=False, **kwargs):
171 if name != target:
168 if name != target:
172 return opener(name, mode, **kwargs)
169 return opener(name, mode, **kwargs)
173 return opener(name + b".a", mode, **kwargs)
170 return opener(name + b".a", mode, **kwargs)
174
171
175 return _divert
172 return _divert
176
173
177
174
178 def _delayopener(opener, target, buf):
175 def _delayopener(opener, target, buf):
179 """build an opener that stores chunks in 'buf' instead of 'target'"""
176 """build an opener that stores chunks in 'buf' instead of 'target'"""
180
177
181 def _delay(name, mode=b'r', checkambig=False, **kwargs):
178 def _delay(name, mode=b'r', checkambig=False, **kwargs):
182 if name != target:
179 if name != target:
183 return opener(name, mode, **kwargs)
180 return opener(name, mode, **kwargs)
184 assert not kwargs
181 assert not kwargs
185 return appender(opener, name, mode, buf)
182 return appender(opener, name, mode, buf)
186
183
187 return _delay
184 return _delay
188
185
189
186
190 @attr.s
187 @attr.s
191 class _changelogrevision(object):
188 class _changelogrevision(object):
192 # Extensions might modify _defaultextra, so let the constructor below pass
189 # Extensions might modify _defaultextra, so let the constructor below pass
193 # it in
190 # it in
194 extra = attr.ib()
191 extra = attr.ib()
195 manifest = attr.ib(default=nullid)
192 manifest = attr.ib(default=nullid)
196 user = attr.ib(default=b'')
193 user = attr.ib(default=b'')
197 date = attr.ib(default=(0, 0))
194 date = attr.ib(default=(0, 0))
198 files = attr.ib(default=attr.Factory(list))
195 files = attr.ib(default=attr.Factory(list))
199 filesadded = attr.ib(default=None)
196 filesadded = attr.ib(default=None)
200 filesremoved = attr.ib(default=None)
197 filesremoved = attr.ib(default=None)
201 p1copies = attr.ib(default=None)
198 p1copies = attr.ib(default=None)
202 p2copies = attr.ib(default=None)
199 p2copies = attr.ib(default=None)
203 description = attr.ib(default=b'')
200 description = attr.ib(default=b'')
204
201
205
202
206 class changelogrevision(object):
203 class changelogrevision(object):
207 """Holds results of a parsed changelog revision.
204 """Holds results of a parsed changelog revision.
208
205
209 Changelog revisions consist of multiple pieces of data, including
206 Changelog revisions consist of multiple pieces of data, including
210 the manifest node, user, and date. This object exposes a view into
207 the manifest node, user, and date. This object exposes a view into
211 the parsed object.
208 the parsed object.
212 """
209 """
213
210
214 __slots__ = (
211 __slots__ = (
215 '_offsets',
212 '_offsets',
216 '_text',
213 '_text',
217 '_sidedata',
214 '_sidedata',
218 '_cpsd',
215 '_cpsd',
219 )
216 )
220
217
221 def __new__(cls, text, sidedata, cpsd):
218 def __new__(cls, text, sidedata, cpsd):
222 if not text:
219 if not text:
223 return _changelogrevision(extra=_defaultextra)
220 return _changelogrevision(extra=_defaultextra)
224
221
225 self = super(changelogrevision, cls).__new__(cls)
222 self = super(changelogrevision, cls).__new__(cls)
226 # We could return here and implement the following as an __init__.
223 # We could return here and implement the following as an __init__.
227 # But doing it here is equivalent and saves an extra function call.
224 # But doing it here is equivalent and saves an extra function call.
228
225
229 # format used:
226 # format used:
230 # nodeid\n : manifest node in ascii
227 # nodeid\n : manifest node in ascii
231 # user\n : user, no \n or \r allowed
228 # user\n : user, no \n or \r allowed
232 # time tz extra\n : date (time is int or float, timezone is int)
229 # time tz extra\n : date (time is int or float, timezone is int)
233 # : extra is metadata, encoded and separated by '\0'
230 # : extra is metadata, encoded and separated by '\0'
234 # : older versions ignore it
231 # : older versions ignore it
235 # files\n\n : files modified by the cset, no \n or \r allowed
232 # files\n\n : files modified by the cset, no \n or \r allowed
236 # (.*) : comment (free text, ideally utf-8)
233 # (.*) : comment (free text, ideally utf-8)
237 #
234 #
238 # changelog v0 doesn't use extra
235 # changelog v0 doesn't use extra
239
236
240 nl1 = text.index(b'\n')
237 nl1 = text.index(b'\n')
241 nl2 = text.index(b'\n', nl1 + 1)
238 nl2 = text.index(b'\n', nl1 + 1)
242 nl3 = text.index(b'\n', nl2 + 1)
239 nl3 = text.index(b'\n', nl2 + 1)
243
240
244 # The list of files may be empty. Which means nl3 is the first of the
241 # The list of files may be empty. Which means nl3 is the first of the
245 # double newline that precedes the description.
242 # double newline that precedes the description.
246 if text[nl3 + 1 : nl3 + 2] == b'\n':
243 if text[nl3 + 1 : nl3 + 2] == b'\n':
247 doublenl = nl3
244 doublenl = nl3
248 else:
245 else:
249 doublenl = text.index(b'\n\n', nl3 + 1)
246 doublenl = text.index(b'\n\n', nl3 + 1)
250
247
251 self._offsets = (nl1, nl2, nl3, doublenl)
248 self._offsets = (nl1, nl2, nl3, doublenl)
252 self._text = text
249 self._text = text
253 self._sidedata = sidedata
250 self._sidedata = sidedata
254 self._cpsd = cpsd
251 self._cpsd = cpsd
255
252
256 return self
253 return self
257
254
258 @property
255 @property
259 def manifest(self):
256 def manifest(self):
260 return bin(self._text[0 : self._offsets[0]])
257 return bin(self._text[0 : self._offsets[0]])
261
258
262 @property
259 @property
263 def user(self):
260 def user(self):
264 off = self._offsets
261 off = self._offsets
265 return encoding.tolocal(self._text[off[0] + 1 : off[1]])
262 return encoding.tolocal(self._text[off[0] + 1 : off[1]])
266
263
267 @property
264 @property
268 def _rawdate(self):
265 def _rawdate(self):
269 off = self._offsets
266 off = self._offsets
270 dateextra = self._text[off[1] + 1 : off[2]]
267 dateextra = self._text[off[1] + 1 : off[2]]
271 return dateextra.split(b' ', 2)[0:2]
268 return dateextra.split(b' ', 2)[0:2]
272
269
273 @property
270 @property
274 def _rawextra(self):
271 def _rawextra(self):
275 off = self._offsets
272 off = self._offsets
276 dateextra = self._text[off[1] + 1 : off[2]]
273 dateextra = self._text[off[1] + 1 : off[2]]
277 fields = dateextra.split(b' ', 2)
274 fields = dateextra.split(b' ', 2)
278 if len(fields) != 3:
275 if len(fields) != 3:
279 return None
276 return None
280
277
281 return fields[2]
278 return fields[2]
282
279
283 @property
280 @property
284 def date(self):
281 def date(self):
285 raw = self._rawdate
282 raw = self._rawdate
286 time = float(raw[0])
283 time = float(raw[0])
287 # Various tools did silly things with the timezone.
284 # Various tools did silly things with the timezone.
288 try:
285 try:
289 timezone = int(raw[1])
286 timezone = int(raw[1])
290 except ValueError:
287 except ValueError:
291 timezone = 0
288 timezone = 0
292
289
293 return time, timezone
290 return time, timezone
294
291
295 @property
292 @property
296 def extra(self):
293 def extra(self):
297 raw = self._rawextra
294 raw = self._rawextra
298 if raw is None:
295 if raw is None:
299 return _defaultextra
296 return _defaultextra
300
297
301 return decodeextra(raw)
298 return decodeextra(raw)
302
299
303 @property
300 @property
304 def files(self):
301 def files(self):
305 off = self._offsets
302 off = self._offsets
306 if off[2] == off[3]:
303 if off[2] == off[3]:
307 return []
304 return []
308
305
309 return self._text[off[2] + 1 : off[3]].split(b'\n')
306 return self._text[off[2] + 1 : off[3]].split(b'\n')
310
307
311 @property
308 @property
312 def filesadded(self):
309 def filesadded(self):
313 if self._cpsd:
310 if self._cpsd:
314 rawindices = self._sidedata.get(sidedatamod.SD_FILESADDED)
311 rawindices = self._sidedata.get(sidedatamod.SD_FILESADDED)
315 if not rawindices:
312 if not rawindices:
316 return []
313 return []
317 else:
314 else:
318 rawindices = self.extra.get(b'filesadded')
315 rawindices = self.extra.get(b'filesadded')
319 if rawindices is None:
316 if rawindices is None:
320 return None
317 return None
321 return copies.decodefileindices(self.files, rawindices)
318 return copies.decodefileindices(self.files, rawindices)
322
319
323 @property
320 @property
324 def filesremoved(self):
321 def filesremoved(self):
325 if self._cpsd:
322 if self._cpsd:
326 rawindices = self._sidedata.get(sidedatamod.SD_FILESREMOVED)
323 rawindices = self._sidedata.get(sidedatamod.SD_FILESREMOVED)
327 if not rawindices:
324 if not rawindices:
328 return []
325 return []
329 else:
326 else:
330 rawindices = self.extra.get(b'filesremoved')
327 rawindices = self.extra.get(b'filesremoved')
331 if rawindices is None:
328 if rawindices is None:
332 return None
329 return None
333 return copies.decodefileindices(self.files, rawindices)
330 return copies.decodefileindices(self.files, rawindices)
334
331
335 @property
332 @property
336 def p1copies(self):
333 def p1copies(self):
337 if self._cpsd:
334 if self._cpsd:
338 rawcopies = self._sidedata.get(sidedatamod.SD_P1COPIES)
335 rawcopies = self._sidedata.get(sidedatamod.SD_P1COPIES)
339 if not rawcopies:
336 if not rawcopies:
340 return {}
337 return {}
341 else:
338 else:
342 rawcopies = self.extra.get(b'p1copies')
339 rawcopies = self.extra.get(b'p1copies')
343 if rawcopies is None:
340 if rawcopies is None:
344 return None
341 return None
345 return copies.decodecopies(self.files, rawcopies)
342 return copies.decodecopies(self.files, rawcopies)
346
343
347 @property
344 @property
348 def p2copies(self):
345 def p2copies(self):
349 if self._cpsd:
346 if self._cpsd:
350 rawcopies = self._sidedata.get(sidedatamod.SD_P2COPIES)
347 rawcopies = self._sidedata.get(sidedatamod.SD_P2COPIES)
351 if not rawcopies:
348 if not rawcopies:
352 return {}
349 return {}
353 else:
350 else:
354 rawcopies = self.extra.get(b'p2copies')
351 rawcopies = self.extra.get(b'p2copies')
355 if rawcopies is None:
352 if rawcopies is None:
356 return None
353 return None
357 return copies.decodecopies(self.files, rawcopies)
354 return copies.decodecopies(self.files, rawcopies)
358
355
359 @property
356 @property
360 def description(self):
357 def description(self):
361 return encoding.tolocal(self._text[self._offsets[3] + 2 :])
358 return encoding.tolocal(self._text[self._offsets[3] + 2 :])
362
359
363
360
364 class changelog(revlog.revlog):
361 class changelog(revlog.revlog):
365 def __init__(self, opener, trypending=False):
362 def __init__(self, opener, trypending=False):
366 """Load a changelog revlog using an opener.
363 """Load a changelog revlog using an opener.
367
364
368 If ``trypending`` is true, we attempt to load the index from a
365 If ``trypending`` is true, we attempt to load the index from a
369 ``00changelog.i.a`` file instead of the default ``00changelog.i``.
366 ``00changelog.i.a`` file instead of the default ``00changelog.i``.
370 The ``00changelog.i.a`` file contains index (and possibly inline
367 The ``00changelog.i.a`` file contains index (and possibly inline
371 revision) data for a transaction that hasn't been finalized yet.
368 revision) data for a transaction that hasn't been finalized yet.
372 It exists in a separate file to facilitate readers (such as
369 It exists in a separate file to facilitate readers (such as
373 hooks processes) accessing data before a transaction is finalized.
370 hooks processes) accessing data before a transaction is finalized.
374 """
371 """
375 if trypending and opener.exists(b'00changelog.i.a'):
372 if trypending and opener.exists(b'00changelog.i.a'):
376 indexfile = b'00changelog.i.a'
373 indexfile = b'00changelog.i.a'
377 else:
374 else:
378 indexfile = b'00changelog.i'
375 indexfile = b'00changelog.i'
379
376
380 datafile = b'00changelog.d'
377 datafile = b'00changelog.d'
381 revlog.revlog.__init__(
378 revlog.revlog.__init__(
382 self,
379 self,
383 opener,
380 opener,
384 indexfile,
381 indexfile,
385 datafile=datafile,
382 datafile=datafile,
386 checkambig=True,
383 checkambig=True,
387 mmaplargeindex=True,
384 mmaplargeindex=True,
388 )
385 )
389
386
390 if self._initempty and (self.version & 0xFFFF == revlog.REVLOGV1):
387 if self._initempty and (self.version & 0xFFFF == revlog.REVLOGV1):
391 # changelogs don't benefit from generaldelta.
388 # changelogs don't benefit from generaldelta.
392
389
393 self.version &= ~revlog.FLAG_GENERALDELTA
390 self.version &= ~revlog.FLAG_GENERALDELTA
394 self._generaldelta = False
391 self._generaldelta = False
395
392
396 # Delta chains for changelogs tend to be very small because entries
393 # Delta chains for changelogs tend to be very small because entries
397 # tend to be small and don't delta well with each. So disable delta
394 # tend to be small and don't delta well with each. So disable delta
398 # chains.
395 # chains.
399 self._storedeltachains = False
396 self._storedeltachains = False
400
397
401 self._realopener = opener
398 self._realopener = opener
402 self._delayed = False
399 self._delayed = False
403 self._delaybuf = None
400 self._delaybuf = None
404 self._divert = False
401 self._divert = False
405 self.filteredrevs = frozenset()
402 self.filteredrevs = frozenset()
406 self._copiesstorage = opener.options.get(b'copies-storage')
403 self._copiesstorage = opener.options.get(b'copies-storage')
407
404
408 def delayupdate(self, tr):
405 def delayupdate(self, tr):
409 """delay visibility of index updates to other readers"""
406 """delay visibility of index updates to other readers"""
410
407
411 if not self._delayed:
408 if not self._delayed:
412 if len(self) == 0:
409 if len(self) == 0:
413 self._divert = True
410 self._divert = True
414 if self._realopener.exists(self.indexfile + b'.a'):
411 if self._realopener.exists(self.indexfile + b'.a'):
415 self._realopener.unlink(self.indexfile + b'.a')
412 self._realopener.unlink(self.indexfile + b'.a')
416 self.opener = _divertopener(self._realopener, self.indexfile)
413 self.opener = _divertopener(self._realopener, self.indexfile)
417 else:
414 else:
418 self._delaybuf = []
415 self._delaybuf = []
419 self.opener = _delayopener(
416 self.opener = _delayopener(
420 self._realopener, self.indexfile, self._delaybuf
417 self._realopener, self.indexfile, self._delaybuf
421 )
418 )
422 self._delayed = True
419 self._delayed = True
423 tr.addpending(b'cl-%i' % id(self), self._writepending)
420 tr.addpending(b'cl-%i' % id(self), self._writepending)
424 tr.addfinalize(b'cl-%i' % id(self), self._finalize)
421 tr.addfinalize(b'cl-%i' % id(self), self._finalize)
425
422
426 def _finalize(self, tr):
423 def _finalize(self, tr):
427 """finalize index updates"""
424 """finalize index updates"""
428 self._delayed = False
425 self._delayed = False
429 self.opener = self._realopener
426 self.opener = self._realopener
430 # move redirected index data back into place
427 # move redirected index data back into place
431 if self._divert:
428 if self._divert:
432 assert not self._delaybuf
429 assert not self._delaybuf
433 tmpname = self.indexfile + b".a"
430 tmpname = self.indexfile + b".a"
434 nfile = self.opener.open(tmpname)
431 nfile = self.opener.open(tmpname)
435 nfile.close()
432 nfile.close()
436 self.opener.rename(tmpname, self.indexfile, checkambig=True)
433 self.opener.rename(tmpname, self.indexfile, checkambig=True)
437 elif self._delaybuf:
434 elif self._delaybuf:
438 fp = self.opener(self.indexfile, b'a', checkambig=True)
435 fp = self.opener(self.indexfile, b'a', checkambig=True)
439 fp.write(b"".join(self._delaybuf))
436 fp.write(b"".join(self._delaybuf))
440 fp.close()
437 fp.close()
441 self._delaybuf = None
438 self._delaybuf = None
442 self._divert = False
439 self._divert = False
443 # split when we're done
440 # split when we're done
444 self._enforceinlinesize(tr)
441 self._enforceinlinesize(tr)
445
442
446 def _writepending(self, tr):
443 def _writepending(self, tr):
447 """create a file containing the unfinalized state for
444 """create a file containing the unfinalized state for
448 pretxnchangegroup"""
445 pretxnchangegroup"""
449 if self._delaybuf:
446 if self._delaybuf:
450 # make a temporary copy of the index
447 # make a temporary copy of the index
451 fp1 = self._realopener(self.indexfile)
448 fp1 = self._realopener(self.indexfile)
452 pendingfilename = self.indexfile + b".a"
449 pendingfilename = self.indexfile + b".a"
453 # register as a temp file to ensure cleanup on failure
450 # register as a temp file to ensure cleanup on failure
454 tr.registertmp(pendingfilename)
451 tr.registertmp(pendingfilename)
455 # write existing data
452 # write existing data
456 fp2 = self._realopener(pendingfilename, b"w")
453 fp2 = self._realopener(pendingfilename, b"w")
457 fp2.write(fp1.read())
454 fp2.write(fp1.read())
458 # add pending data
455 # add pending data
459 fp2.write(b"".join(self._delaybuf))
456 fp2.write(b"".join(self._delaybuf))
460 fp2.close()
457 fp2.close()
461 # switch modes so finalize can simply rename
458 # switch modes so finalize can simply rename
462 self._delaybuf = None
459 self._delaybuf = None
463 self._divert = True
460 self._divert = True
464 self.opener = _divertopener(self._realopener, self.indexfile)
461 self.opener = _divertopener(self._realopener, self.indexfile)
465
462
466 if self._divert:
463 if self._divert:
467 return True
464 return True
468
465
469 return False
466 return False
470
467
471 def _enforceinlinesize(self, tr, fp=None):
468 def _enforceinlinesize(self, tr, fp=None):
472 if not self._delayed:
469 if not self._delayed:
473 revlog.revlog._enforceinlinesize(self, tr, fp)
470 revlog.revlog._enforceinlinesize(self, tr, fp)
474
471
475 def read(self, node):
472 def read(self, node):
476 """Obtain data from a parsed changelog revision.
473 """Obtain data from a parsed changelog revision.
477
474
478 Returns a 6-tuple of:
475 Returns a 6-tuple of:
479
476
480 - manifest node in binary
477 - manifest node in binary
481 - author/user as a localstr
478 - author/user as a localstr
482 - date as a 2-tuple of (time, timezone)
479 - date as a 2-tuple of (time, timezone)
483 - list of files
480 - list of files
484 - commit message as a localstr
481 - commit message as a localstr
485 - dict of extra metadata
482 - dict of extra metadata
486
483
487 Unless you need to access all fields, consider calling
484 Unless you need to access all fields, consider calling
488 ``changelogrevision`` instead, as it is faster for partial object
485 ``changelogrevision`` instead, as it is faster for partial object
489 access.
486 access.
490 """
487 """
491 d, s = self._revisiondata(node)
488 d, s = self._revisiondata(node)
492 c = changelogrevision(
489 c = changelogrevision(
493 d, s, self._copiesstorage == b'changeset-sidedata'
490 d, s, self._copiesstorage == b'changeset-sidedata'
494 )
491 )
495 return (c.manifest, c.user, c.date, c.files, c.description, c.extra)
492 return (c.manifest, c.user, c.date, c.files, c.description, c.extra)
496
493
497 def changelogrevision(self, nodeorrev):
494 def changelogrevision(self, nodeorrev):
498 """Obtain a ``changelogrevision`` for a node or revision."""
495 """Obtain a ``changelogrevision`` for a node or revision."""
499 text, sidedata = self._revisiondata(nodeorrev)
496 text, sidedata = self._revisiondata(nodeorrev)
500 return changelogrevision(
497 return changelogrevision(
501 text, sidedata, self._copiesstorage == b'changeset-sidedata'
498 text, sidedata, self._copiesstorage == b'changeset-sidedata'
502 )
499 )
503
500
504 def readfiles(self, node):
501 def readfiles(self, node):
505 """
502 """
506 short version of read that only returns the files modified by the cset
503 short version of read that only returns the files modified by the cset
507 """
504 """
508 text = self.revision(node)
505 text = self.revision(node)
509 if not text:
506 if not text:
510 return []
507 return []
511 last = text.index(b"\n\n")
508 last = text.index(b"\n\n")
512 l = text[:last].split(b'\n')
509 l = text[:last].split(b'\n')
513 return l[3:]
510 return l[3:]
514
511
515 def add(
512 def add(
516 self,
513 self,
517 manifest,
514 manifest,
518 files,
515 files,
519 desc,
516 desc,
520 transaction,
517 transaction,
521 p1,
518 p1,
522 p2,
519 p2,
523 user,
520 user,
524 date=None,
521 date=None,
525 extra=None,
522 extra=None,
526 p1copies=None,
523 p1copies=None,
527 p2copies=None,
524 p2copies=None,
528 filesadded=None,
525 filesadded=None,
529 filesremoved=None,
526 filesremoved=None,
530 ):
527 ):
531 # Convert to UTF-8 encoded bytestrings as the very first
528 # Convert to UTF-8 encoded bytestrings as the very first
532 # thing: calling any method on a localstr object will turn it
529 # thing: calling any method on a localstr object will turn it
533 # into a str object and the cached UTF-8 string is thus lost.
530 # into a str object and the cached UTF-8 string is thus lost.
534 user, desc = encoding.fromlocal(user), encoding.fromlocal(desc)
531 user, desc = encoding.fromlocal(user), encoding.fromlocal(desc)
535
532
536 user = user.strip()
533 user = user.strip()
537 # An empty username or a username with a "\n" will make the
534 # An empty username or a username with a "\n" will make the
538 # revision text contain two "\n\n" sequences -> corrupt
535 # revision text contain two "\n\n" sequences -> corrupt
539 # repository since read cannot unpack the revision.
536 # repository since read cannot unpack the revision.
540 if not user:
537 if not user:
541 raise error.StorageError(_(b"empty username"))
538 raise error.StorageError(_(b"empty username"))
542 if b"\n" in user:
539 if b"\n" in user:
543 raise error.StorageError(
540 raise error.StorageError(
544 _(b"username %r contains a newline") % pycompat.bytestr(user)
541 _(b"username %r contains a newline") % pycompat.bytestr(user)
545 )
542 )
546
543
547 desc = stripdesc(desc)
544 desc = stripdesc(desc)
548
545
549 if date:
546 if date:
550 parseddate = b"%d %d" % dateutil.parsedate(date)
547 parseddate = b"%d %d" % dateutil.parsedate(date)
551 else:
548 else:
552 parseddate = b"%d %d" % dateutil.makedate()
549 parseddate = b"%d %d" % dateutil.makedate()
553 if extra:
550 if extra:
554 branch = extra.get(b"branch")
551 branch = extra.get(b"branch")
555 if branch in (b"default", b""):
552 if branch in (b"default", b""):
556 del extra[b"branch"]
553 del extra[b"branch"]
557 elif branch in (b".", b"null", b"tip"):
554 elif branch in (b".", b"null", b"tip"):
558 raise error.StorageError(
555 raise error.StorageError(
559 _(b'the name \'%s\' is reserved') % branch
556 _(b'the name \'%s\' is reserved') % branch
560 )
557 )
561 sortedfiles = sorted(files)
558 sortedfiles = sorted(files)
562 sidedata = None
559 sidedata = None
563 if extra is not None:
560 if extra is not None:
564 for name in (
561 for name in (
565 b'p1copies',
562 b'p1copies',
566 b'p2copies',
563 b'p2copies',
567 b'filesadded',
564 b'filesadded',
568 b'filesremoved',
565 b'filesremoved',
569 ):
566 ):
570 extra.pop(name, None)
567 extra.pop(name, None)
571 if p1copies is not None:
568 if p1copies is not None:
572 p1copies = copies.encodecopies(sortedfiles, p1copies)
569 p1copies = copies.encodecopies(sortedfiles, p1copies)
573 if p2copies is not None:
570 if p2copies is not None:
574 p2copies = copies.encodecopies(sortedfiles, p2copies)
571 p2copies = copies.encodecopies(sortedfiles, p2copies)
575 if filesadded is not None:
572 if filesadded is not None:
576 filesadded = copies.encodefileindices(sortedfiles, filesadded)
573 filesadded = copies.encodefileindices(sortedfiles, filesadded)
577 if filesremoved is not None:
574 if filesremoved is not None:
578 filesremoved = copies.encodefileindices(sortedfiles, filesremoved)
575 filesremoved = copies.encodefileindices(sortedfiles, filesremoved)
579 if self._copiesstorage == b'extra':
576 if self._copiesstorage == b'extra':
580 extrasentries = p1copies, p2copies, filesadded, filesremoved
577 extrasentries = p1copies, p2copies, filesadded, filesremoved
581 if extra is None and any(x is not None for x in extrasentries):
578 if extra is None and any(x is not None for x in extrasentries):
582 extra = {}
579 extra = {}
583 if p1copies is not None:
580 if p1copies is not None:
584 extra[b'p1copies'] = p1copies
581 extra[b'p1copies'] = p1copies
585 if p2copies is not None:
582 if p2copies is not None:
586 extra[b'p2copies'] = p2copies
583 extra[b'p2copies'] = p2copies
587 if filesadded is not None:
584 if filesadded is not None:
588 extra[b'filesadded'] = filesadded
585 extra[b'filesadded'] = filesadded
589 if filesremoved is not None:
586 if filesremoved is not None:
590 extra[b'filesremoved'] = filesremoved
587 extra[b'filesremoved'] = filesremoved
591 elif self._copiesstorage == b'changeset-sidedata':
588 elif self._copiesstorage == b'changeset-sidedata':
592 sidedata = {}
589 sidedata = {}
593 if p1copies:
590 if p1copies:
594 sidedata[sidedatamod.SD_P1COPIES] = p1copies
591 sidedata[sidedatamod.SD_P1COPIES] = p1copies
595 if p2copies:
592 if p2copies:
596 sidedata[sidedatamod.SD_P2COPIES] = p2copies
593 sidedata[sidedatamod.SD_P2COPIES] = p2copies
597 if filesadded:
594 if filesadded:
598 sidedata[sidedatamod.SD_FILESADDED] = filesadded
595 sidedata[sidedatamod.SD_FILESADDED] = filesadded
599 if filesremoved:
596 if filesremoved:
600 sidedata[sidedatamod.SD_FILESREMOVED] = filesremoved
597 sidedata[sidedatamod.SD_FILESREMOVED] = filesremoved
601 if not sidedata:
598 if not sidedata:
602 sidedata = None
599 sidedata = None
603
600
604 if extra:
601 if extra:
605 extra = encodeextra(extra)
602 extra = encodeextra(extra)
606 parseddate = b"%s %s" % (parseddate, extra)
603 parseddate = b"%s %s" % (parseddate, extra)
607 l = [hex(manifest), user, parseddate] + sortedfiles + [b"", desc]
604 l = [hex(manifest), user, parseddate] + sortedfiles + [b"", desc]
608 text = b"\n".join(l)
605 text = b"\n".join(l)
609 return self.addrevision(
606 return self.addrevision(
610 text, transaction, len(self), p1, p2, sidedata=sidedata
607 text, transaction, len(self), p1, p2, sidedata=sidedata
611 )
608 )
612
609
613 def branchinfo(self, rev):
610 def branchinfo(self, rev):
614 """return the branch name and open/close state of a revision
611 """return the branch name and open/close state of a revision
615
612
616 This function exists because creating a changectx object
613 This function exists because creating a changectx object
617 just to access this is costly."""
614 just to access this is costly."""
618 extra = self.read(rev)[5]
615 extra = self.read(rev)[5]
619 return encoding.tolocal(extra.get(b"branch")), b'close' in extra
616 return encoding.tolocal(extra.get(b"branch")), b'close' in extra
620
617
621 def _nodeduplicatecallback(self, transaction, node):
618 def _nodeduplicatecallback(self, transaction, node):
622 # keep track of revisions that got "re-added", eg: unbunde of know rev.
619 # keep track of revisions that got "re-added", eg: unbunde of know rev.
623 #
620 #
624 # We track them in a list to preserve their order from the source bundle
621 # We track them in a list to preserve their order from the source bundle
625 duplicates = transaction.changes.setdefault(b'revduplicates', [])
622 duplicates = transaction.changes.setdefault(b'revduplicates', [])
626 duplicates.append(self.rev(node))
623 duplicates.append(self.rev(node))
General Comments 0
You need to be logged in to leave comments. Login now