##// END OF EJS Templates
revlog: move nodemap update within the index code...
marmoute -
r43931:dcf9826c default
parent child Browse files
Show More
@@ -1,203 +1,205 b''
1 1 # parsers.py - Python implementation of parsers.c
2 2 #
3 3 # Copyright 2009 Matt Mackall <mpm@selenic.com> and others
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import struct
11 11 import zlib
12 12
13 13 from ..node import nullid, nullrev
14 14 from .. import (
15 15 pycompat,
16 16 revlogutils,
17 17 util,
18 18 )
19 19
20 20 stringio = pycompat.bytesio
21 21
22 22
23 23 _pack = struct.pack
24 24 _unpack = struct.unpack
25 25 _compress = zlib.compress
26 26 _decompress = zlib.decompress
27 27
28 28 # Some code below makes tuples directly because it's more convenient. However,
29 29 # code outside this module should always use dirstatetuple.
30 30 def dirstatetuple(*x):
31 31 # x is a tuple
32 32 return x
33 33
34 34
35 35 indexformatng = b">Qiiiiii20s12x"
36 36 indexfirst = struct.calcsize(b'Q')
37 37 sizeint = struct.calcsize(b'i')
38 38 indexsize = struct.calcsize(indexformatng)
39 39
40 40
41 41 def gettype(q):
42 42 return int(q & 0xFFFF)
43 43
44 44
45 45 def offset_type(offset, type):
46 46 return int(int(offset) << 16 | type)
47 47
48 48
49 49 class BaseIndexObject(object):
50 50 @util.propertycache
51 51 def nodemap(self):
52 52 nodemap = revlogutils.NodeMap({nullid: nullrev})
53 53 for r in range(0, len(self)):
54 54 n = self[r][7]
55 55 nodemap[n] = r
56 56 return nodemap
57 57
58 58 def clearcaches(self):
59 59 self.__dict__.pop('nodemap', None)
60 60
61 61 def __len__(self):
62 62 return self._lgt + len(self._extra)
63 63
64 64 def append(self, tup):
65 if 'nodemap' in vars(self):
66 self.nodemap[tup[7]] = len(self)
65 67 self._extra.append(tup)
66 68
67 69 def _check_index(self, i):
68 70 if not isinstance(i, int):
69 71 raise TypeError(b"expecting int indexes")
70 72 if i < 0 or i >= len(self):
71 73 raise IndexError
72 74
73 75 def __getitem__(self, i):
74 76 if i == -1:
75 77 return (0, 0, 0, -1, -1, -1, -1, nullid)
76 78 self._check_index(i)
77 79 if i >= self._lgt:
78 80 return self._extra[i - self._lgt]
79 81 index = self._calculate_index(i)
80 82 r = struct.unpack(indexformatng, self._data[index : index + indexsize])
81 83 if i == 0:
82 84 e = list(r)
83 85 type = gettype(e[0])
84 86 e[0] = offset_type(0, type)
85 87 return tuple(e)
86 88 return r
87 89
88 90
89 91 class IndexObject(BaseIndexObject):
90 92 def __init__(self, data):
91 93 assert len(data) % indexsize == 0
92 94 self._data = data
93 95 self._lgt = len(data) // indexsize
94 96 self._extra = []
95 97
96 98 def _calculate_index(self, i):
97 99 return i * indexsize
98 100
99 101 def __delitem__(self, i):
100 102 if not isinstance(i, slice) or not i.stop == -1 or i.step is not None:
101 103 raise ValueError(b"deleting slices only supports a:-1 with step 1")
102 104 i = i.start
103 105 self._check_index(i)
104 106 if i < self._lgt:
105 107 self._data = self._data[: i * indexsize]
106 108 self._lgt = i
107 109 self._extra = []
108 110 else:
109 111 self._extra = self._extra[: i - self._lgt]
110 112
111 113
112 114 class InlinedIndexObject(BaseIndexObject):
113 115 def __init__(self, data, inline=0):
114 116 self._data = data
115 117 self._lgt = self._inline_scan(None)
116 118 self._inline_scan(self._lgt)
117 119 self._extra = []
118 120
119 121 def _inline_scan(self, lgt):
120 122 off = 0
121 123 if lgt is not None:
122 124 self._offsets = [0] * lgt
123 125 count = 0
124 126 while off <= len(self._data) - indexsize:
125 127 (s,) = struct.unpack(
126 128 b'>i', self._data[off + indexfirst : off + sizeint + indexfirst]
127 129 )
128 130 if lgt is not None:
129 131 self._offsets[count] = off
130 132 count += 1
131 133 off += indexsize + s
132 134 if off != len(self._data):
133 135 raise ValueError(b"corrupted data")
134 136 return count
135 137
136 138 def __delitem__(self, i):
137 139 if not isinstance(i, slice) or not i.stop == -1 or i.step is not None:
138 140 raise ValueError(b"deleting slices only supports a:-1 with step 1")
139 141 i = i.start
140 142 self._check_index(i)
141 143 if i < self._lgt:
142 144 self._offsets = self._offsets[:i]
143 145 self._lgt = i
144 146 self._extra = []
145 147 else:
146 148 self._extra = self._extra[: i - self._lgt]
147 149
148 150 def _calculate_index(self, i):
149 151 return self._offsets[i]
150 152
151 153
152 154 def parse_index2(data, inline):
153 155 if not inline:
154 156 return IndexObject(data), None
155 157 return InlinedIndexObject(data, inline), (0, data)
156 158
157 159
158 160 def parse_dirstate(dmap, copymap, st):
159 161 parents = [st[:20], st[20:40]]
160 162 # dereference fields so they will be local in loop
161 163 format = b">cllll"
162 164 e_size = struct.calcsize(format)
163 165 pos1 = 40
164 166 l = len(st)
165 167
166 168 # the inner loop
167 169 while pos1 < l:
168 170 pos2 = pos1 + e_size
169 171 e = _unpack(b">cllll", st[pos1:pos2]) # a literal here is faster
170 172 pos1 = pos2 + e[4]
171 173 f = st[pos2:pos1]
172 174 if b'\0' in f:
173 175 f, c = f.split(b'\0')
174 176 copymap[f] = c
175 177 dmap[f] = e[:4]
176 178 return parents
177 179
178 180
179 181 def pack_dirstate(dmap, copymap, pl, now):
180 182 now = int(now)
181 183 cs = stringio()
182 184 write = cs.write
183 185 write(b"".join(pl))
184 186 for f, e in pycompat.iteritems(dmap):
185 187 if e[0] == b'n' and e[3] == now:
186 188 # The file was last modified "simultaneously" with the current
187 189 # write to dirstate (i.e. within the same second for file-
188 190 # systems with a granularity of 1 sec). This commonly happens
189 191 # for at least a couple of files on 'update'.
190 192 # The user could change the file without changing its size
191 193 # within the same second. Invalidate the file's mtime in
192 194 # dirstate, forcing future 'status' calls to compare the
193 195 # contents of the file if the size is the same. This prevents
194 196 # mistakenly treating such files as clean.
195 197 e = dirstatetuple(e[0], e[1], e[2], -1)
196 198 dmap[f] = e
197 199
198 200 if f in copymap:
199 201 f = b"%s\0%s" % (f, copymap[f])
200 202 e = _pack(b">cllll", e[0], e[1], e[2], e[3], len(f))
201 203 write(e)
202 204 write(f)
203 205 return cs.getvalue()
@@ -1,2952 +1,2955 b''
1 1 # revlog.py - storage back-end for mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 """Storage back-end for Mercurial.
9 9
10 10 This provides efficient delta storage with O(1) retrieve and append
11 11 and O(changes) merge between branches.
12 12 """
13 13
14 14 from __future__ import absolute_import
15 15
16 16 import collections
17 17 import contextlib
18 18 import errno
19 19 import io
20 20 import os
21 21 import struct
22 22 import zlib
23 23
24 24 # import stuff from node for others to import from revlog
25 25 from .node import (
26 26 bin,
27 27 hex,
28 28 nullhex,
29 29 nullid,
30 30 nullrev,
31 31 short,
32 32 wdirfilenodeids,
33 33 wdirhex,
34 34 wdirid,
35 35 wdirrev,
36 36 )
37 37 from .i18n import _
38 38 from .pycompat import getattr
39 39 from .revlogutils.constants import (
40 40 FLAG_GENERALDELTA,
41 41 FLAG_INLINE_DATA,
42 42 REVLOGV0,
43 43 REVLOGV1,
44 44 REVLOGV1_FLAGS,
45 45 REVLOGV2,
46 46 REVLOGV2_FLAGS,
47 47 REVLOG_DEFAULT_FLAGS,
48 48 REVLOG_DEFAULT_FORMAT,
49 49 REVLOG_DEFAULT_VERSION,
50 50 )
51 51 from .revlogutils.flagutil import (
52 52 REVIDX_DEFAULT_FLAGS,
53 53 REVIDX_ELLIPSIS,
54 54 REVIDX_EXTSTORED,
55 55 REVIDX_FLAGS_ORDER,
56 56 REVIDX_ISCENSORED,
57 57 REVIDX_RAWTEXT_CHANGING_FLAGS,
58 58 REVIDX_SIDEDATA,
59 59 )
60 60 from .thirdparty import attr
61 61 from . import (
62 62 ancestor,
63 63 dagop,
64 64 error,
65 65 mdiff,
66 66 policy,
67 67 pycompat,
68 68 revlogutils,
69 69 templatefilters,
70 70 util,
71 71 )
72 72 from .interfaces import (
73 73 repository,
74 74 util as interfaceutil,
75 75 )
76 76 from .revlogutils import (
77 77 deltas as deltautil,
78 78 flagutil,
79 79 sidedata as sidedatautil,
80 80 )
81 81 from .utils import (
82 82 storageutil,
83 83 stringutil,
84 84 )
85 85
86 86 # blanked usage of all the name to prevent pyflakes constraints
87 87 # We need these name available in the module for extensions.
88 88 REVLOGV0
89 89 REVLOGV1
90 90 REVLOGV2
91 91 FLAG_INLINE_DATA
92 92 FLAG_GENERALDELTA
93 93 REVLOG_DEFAULT_FLAGS
94 94 REVLOG_DEFAULT_FORMAT
95 95 REVLOG_DEFAULT_VERSION
96 96 REVLOGV1_FLAGS
97 97 REVLOGV2_FLAGS
98 98 REVIDX_ISCENSORED
99 99 REVIDX_ELLIPSIS
100 100 REVIDX_SIDEDATA
101 101 REVIDX_EXTSTORED
102 102 REVIDX_DEFAULT_FLAGS
103 103 REVIDX_FLAGS_ORDER
104 104 REVIDX_RAWTEXT_CHANGING_FLAGS
105 105
106 106 parsers = policy.importmod('parsers')
107 107 rustancestor = policy.importrust('ancestor')
108 108 rustdagop = policy.importrust('dagop')
109 109
110 110 # Aliased for performance.
111 111 _zlibdecompress = zlib.decompress
112 112
113 113 # max size of revlog with inline data
114 114 _maxinline = 131072
115 115 _chunksize = 1048576
116 116
117 117 # Flag processors for REVIDX_ELLIPSIS.
118 118 def ellipsisreadprocessor(rl, text):
119 119 return text, False, {}
120 120
121 121
122 122 def ellipsiswriteprocessor(rl, text, sidedata):
123 123 return text, False
124 124
125 125
126 126 def ellipsisrawprocessor(rl, text):
127 127 return False
128 128
129 129
130 130 ellipsisprocessor = (
131 131 ellipsisreadprocessor,
132 132 ellipsiswriteprocessor,
133 133 ellipsisrawprocessor,
134 134 )
135 135
136 136
137 137 def getoffset(q):
138 138 return int(q >> 16)
139 139
140 140
141 141 def gettype(q):
142 142 return int(q & 0xFFFF)
143 143
144 144
145 145 def offset_type(offset, type):
146 146 if (type & ~flagutil.REVIDX_KNOWN_FLAGS) != 0:
147 147 raise ValueError(b'unknown revlog index flags')
148 148 return int(int(offset) << 16 | type)
149 149
150 150
151 151 @attr.s(slots=True, frozen=True)
152 152 class _revisioninfo(object):
153 153 """Information about a revision that allows building its fulltext
154 154 node: expected hash of the revision
155 155 p1, p2: parent revs of the revision
156 156 btext: built text cache consisting of a one-element list
157 157 cachedelta: (baserev, uncompressed_delta) or None
158 158 flags: flags associated to the revision storage
159 159
160 160 One of btext[0] or cachedelta must be set.
161 161 """
162 162
163 163 node = attr.ib()
164 164 p1 = attr.ib()
165 165 p2 = attr.ib()
166 166 btext = attr.ib()
167 167 textlen = attr.ib()
168 168 cachedelta = attr.ib()
169 169 flags = attr.ib()
170 170
171 171
172 172 @interfaceutil.implementer(repository.irevisiondelta)
173 173 @attr.s(slots=True)
174 174 class revlogrevisiondelta(object):
175 175 node = attr.ib()
176 176 p1node = attr.ib()
177 177 p2node = attr.ib()
178 178 basenode = attr.ib()
179 179 flags = attr.ib()
180 180 baserevisionsize = attr.ib()
181 181 revision = attr.ib()
182 182 delta = attr.ib()
183 183 linknode = attr.ib(default=None)
184 184
185 185
186 186 @interfaceutil.implementer(repository.iverifyproblem)
187 187 @attr.s(frozen=True)
188 188 class revlogproblem(object):
189 189 warning = attr.ib(default=None)
190 190 error = attr.ib(default=None)
191 191 node = attr.ib(default=None)
192 192
193 193
194 194 # index v0:
195 195 # 4 bytes: offset
196 196 # 4 bytes: compressed length
197 197 # 4 bytes: base rev
198 198 # 4 bytes: link rev
199 199 # 20 bytes: parent 1 nodeid
200 200 # 20 bytes: parent 2 nodeid
201 201 # 20 bytes: nodeid
202 202 indexformatv0 = struct.Struct(b">4l20s20s20s")
203 203 indexformatv0_pack = indexformatv0.pack
204 204 indexformatv0_unpack = indexformatv0.unpack
205 205
206 206
207 207 class revlogoldindex(list):
208 208 @util.propertycache
209 209 def nodemap(self):
210 210 nodemap = revlogutils.NodeMap({nullid: nullrev})
211 211 for r in range(0, len(self)):
212 212 n = self[r][7]
213 213 nodemap[n] = r
214 214 return nodemap
215 215
216 def append(self, tup):
217 self.nodemap[tup[7]] = len(self)
218 super(revlogoldindex, self).append(tup)
219
216 220 def clearcaches(self):
217 221 self.__dict__.pop('nodemap', None)
218 222
219 223 def __getitem__(self, i):
220 224 if i == -1:
221 225 return (0, 0, 0, -1, -1, -1, -1, nullid)
222 226 return list.__getitem__(self, i)
223 227
224 228
225 229 class revlogoldio(object):
226 230 def __init__(self):
227 231 self.size = indexformatv0.size
228 232
229 233 def parseindex(self, data, inline):
230 234 s = self.size
231 235 index = []
232 236 nodemap = revlogutils.NodeMap({nullid: nullrev})
233 237 n = off = 0
234 238 l = len(data)
235 239 while off + s <= l:
236 240 cur = data[off : off + s]
237 241 off += s
238 242 e = indexformatv0_unpack(cur)
239 243 # transform to revlogv1 format
240 244 e2 = (
241 245 offset_type(e[0], 0),
242 246 e[1],
243 247 -1,
244 248 e[2],
245 249 e[3],
246 250 nodemap.get(e[4], nullrev),
247 251 nodemap.get(e[5], nullrev),
248 252 e[6],
249 253 )
250 254 index.append(e2)
251 255 nodemap[e[6]] = n
252 256 n += 1
253 257
254 258 index = revlogoldindex(index)
255 259 return index, None
256 260
257 261 def packentry(self, entry, node, version, rev):
258 262 if gettype(entry[0]):
259 263 raise error.RevlogError(
260 264 _(b'index entry flags need revlog version 1')
261 265 )
262 266 e2 = (
263 267 getoffset(entry[0]),
264 268 entry[1],
265 269 entry[3],
266 270 entry[4],
267 271 node(entry[5]),
268 272 node(entry[6]),
269 273 entry[7],
270 274 )
271 275 return indexformatv0_pack(*e2)
272 276
273 277
274 278 # index ng:
275 279 # 6 bytes: offset
276 280 # 2 bytes: flags
277 281 # 4 bytes: compressed length
278 282 # 4 bytes: uncompressed length
279 283 # 4 bytes: base rev
280 284 # 4 bytes: link rev
281 285 # 4 bytes: parent 1 rev
282 286 # 4 bytes: parent 2 rev
283 287 # 32 bytes: nodeid
284 288 indexformatng = struct.Struct(b">Qiiiiii20s12x")
285 289 indexformatng_pack = indexformatng.pack
286 290 versionformat = struct.Struct(b">I")
287 291 versionformat_pack = versionformat.pack
288 292 versionformat_unpack = versionformat.unpack
289 293
290 294 # corresponds to uncompressed length of indexformatng (2 gigs, 4-byte
291 295 # signed integer)
292 296 _maxentrysize = 0x7FFFFFFF
293 297
294 298
295 299 class revlogio(object):
296 300 def __init__(self):
297 301 self.size = indexformatng.size
298 302
299 303 def parseindex(self, data, inline):
300 304 # call the C implementation to parse the index data
301 305 index, cache = parsers.parse_index2(data, inline)
302 306 return index, cache
303 307
304 308 def packentry(self, entry, node, version, rev):
305 309 p = indexformatng_pack(*entry)
306 310 if rev == 0:
307 311 p = versionformat_pack(version) + p[4:]
308 312 return p
309 313
310 314
311 315 class revlog(object):
312 316 """
313 317 the underlying revision storage object
314 318
315 319 A revlog consists of two parts, an index and the revision data.
316 320
317 321 The index is a file with a fixed record size containing
318 322 information on each revision, including its nodeid (hash), the
319 323 nodeids of its parents, the position and offset of its data within
320 324 the data file, and the revision it's based on. Finally, each entry
321 325 contains a linkrev entry that can serve as a pointer to external
322 326 data.
323 327
324 328 The revision data itself is a linear collection of data chunks.
325 329 Each chunk represents a revision and is usually represented as a
326 330 delta against the previous chunk. To bound lookup time, runs of
327 331 deltas are limited to about 2 times the length of the original
328 332 version data. This makes retrieval of a version proportional to
329 333 its size, or O(1) relative to the number of revisions.
330 334
331 335 Both pieces of the revlog are written to in an append-only
332 336 fashion, which means we never need to rewrite a file to insert or
333 337 remove data, and can use some simple techniques to avoid the need
334 338 for locking while reading.
335 339
336 340 If checkambig, indexfile is opened with checkambig=True at
337 341 writing, to avoid file stat ambiguity.
338 342
339 343 If mmaplargeindex is True, and an mmapindexthreshold is set, the
340 344 index will be mmapped rather than read if it is larger than the
341 345 configured threshold.
342 346
343 347 If censorable is True, the revlog can have censored revisions.
344 348
345 349 If `upperboundcomp` is not None, this is the expected maximal gain from
346 350 compression for the data content.
347 351 """
348 352
349 353 _flagserrorclass = error.RevlogError
350 354
351 355 def __init__(
352 356 self,
353 357 opener,
354 358 indexfile,
355 359 datafile=None,
356 360 checkambig=False,
357 361 mmaplargeindex=False,
358 362 censorable=False,
359 363 upperboundcomp=None,
360 364 ):
361 365 """
362 366 create a revlog object
363 367
364 368 opener is a function that abstracts the file opening operation
365 369 and can be used to implement COW semantics or the like.
366 370
367 371 """
368 372 self.upperboundcomp = upperboundcomp
369 373 self.indexfile = indexfile
370 374 self.datafile = datafile or (indexfile[:-2] + b".d")
371 375 self.opener = opener
372 376 # When True, indexfile is opened with checkambig=True at writing, to
373 377 # avoid file stat ambiguity.
374 378 self._checkambig = checkambig
375 379 self._mmaplargeindex = mmaplargeindex
376 380 self._censorable = censorable
377 381 # 3-tuple of (node, rev, text) for a raw revision.
378 382 self._revisioncache = None
379 383 # Maps rev to chain base rev.
380 384 self._chainbasecache = util.lrucachedict(100)
381 385 # 2-tuple of (offset, data) of raw data from the revlog at an offset.
382 386 self._chunkcache = (0, b'')
383 387 # How much data to read and cache into the raw revlog data cache.
384 388 self._chunkcachesize = 65536
385 389 self._maxchainlen = None
386 390 self._deltabothparents = True
387 391 self.index = None
388 392 # Mapping of partial identifiers to full nodes.
389 393 self._pcache = {}
390 394 # Mapping of revision integer to full node.
391 395 self._nodepos = None
392 396 self._compengine = b'zlib'
393 397 self._compengineopts = {}
394 398 self._maxdeltachainspan = -1
395 399 self._withsparseread = False
396 400 self._sparserevlog = False
397 401 self._srdensitythreshold = 0.50
398 402 self._srmingapsize = 262144
399 403
400 404 # Make copy of flag processors so each revlog instance can support
401 405 # custom flags.
402 406 self._flagprocessors = dict(flagutil.flagprocessors)
403 407
404 408 # 2-tuple of file handles being used for active writing.
405 409 self._writinghandles = None
406 410
407 411 self._loadindex()
408 412
409 413 def _loadindex(self):
410 414 mmapindexthreshold = None
411 415 opts = self.opener.options
412 416
413 417 if b'revlogv2' in opts:
414 418 newversionflags = REVLOGV2 | FLAG_INLINE_DATA
415 419 elif b'revlogv1' in opts:
416 420 newversionflags = REVLOGV1 | FLAG_INLINE_DATA
417 421 if b'generaldelta' in opts:
418 422 newversionflags |= FLAG_GENERALDELTA
419 423 elif b'revlogv0' in self.opener.options:
420 424 newversionflags = REVLOGV0
421 425 else:
422 426 newversionflags = REVLOG_DEFAULT_VERSION
423 427
424 428 if b'chunkcachesize' in opts:
425 429 self._chunkcachesize = opts[b'chunkcachesize']
426 430 if b'maxchainlen' in opts:
427 431 self._maxchainlen = opts[b'maxchainlen']
428 432 if b'deltabothparents' in opts:
429 433 self._deltabothparents = opts[b'deltabothparents']
430 434 self._lazydelta = bool(opts.get(b'lazydelta', True))
431 435 self._lazydeltabase = False
432 436 if self._lazydelta:
433 437 self._lazydeltabase = bool(opts.get(b'lazydeltabase', False))
434 438 if b'compengine' in opts:
435 439 self._compengine = opts[b'compengine']
436 440 if b'zlib.level' in opts:
437 441 self._compengineopts[b'zlib.level'] = opts[b'zlib.level']
438 442 if b'zstd.level' in opts:
439 443 self._compengineopts[b'zstd.level'] = opts[b'zstd.level']
440 444 if b'maxdeltachainspan' in opts:
441 445 self._maxdeltachainspan = opts[b'maxdeltachainspan']
442 446 if self._mmaplargeindex and b'mmapindexthreshold' in opts:
443 447 mmapindexthreshold = opts[b'mmapindexthreshold']
444 448 self.hassidedata = bool(opts.get(b'side-data', False))
445 449 if self.hassidedata:
446 450 self._flagprocessors[REVIDX_SIDEDATA] = sidedatautil.processors
447 451 self._sparserevlog = bool(opts.get(b'sparse-revlog', False))
448 452 withsparseread = bool(opts.get(b'with-sparse-read', False))
449 453 # sparse-revlog forces sparse-read
450 454 self._withsparseread = self._sparserevlog or withsparseread
451 455 if b'sparse-read-density-threshold' in opts:
452 456 self._srdensitythreshold = opts[b'sparse-read-density-threshold']
453 457 if b'sparse-read-min-gap-size' in opts:
454 458 self._srmingapsize = opts[b'sparse-read-min-gap-size']
455 459 if opts.get(b'enableellipsis'):
456 460 self._flagprocessors[REVIDX_ELLIPSIS] = ellipsisprocessor
457 461
458 462 # revlog v0 doesn't have flag processors
459 463 for flag, processor in pycompat.iteritems(
460 464 opts.get(b'flagprocessors', {})
461 465 ):
462 466 flagutil.insertflagprocessor(flag, processor, self._flagprocessors)
463 467
464 468 if self._chunkcachesize <= 0:
465 469 raise error.RevlogError(
466 470 _(b'revlog chunk cache size %r is not greater than 0')
467 471 % self._chunkcachesize
468 472 )
469 473 elif self._chunkcachesize & (self._chunkcachesize - 1):
470 474 raise error.RevlogError(
471 475 _(b'revlog chunk cache size %r is not a power of 2')
472 476 % self._chunkcachesize
473 477 )
474 478
475 479 indexdata = b''
476 480 self._initempty = True
477 481 try:
478 482 with self._indexfp() as f:
479 483 if (
480 484 mmapindexthreshold is not None
481 485 and self.opener.fstat(f).st_size >= mmapindexthreshold
482 486 ):
483 487 # TODO: should .close() to release resources without
484 488 # relying on Python GC
485 489 indexdata = util.buffer(util.mmapread(f))
486 490 else:
487 491 indexdata = f.read()
488 492 if len(indexdata) > 0:
489 493 versionflags = versionformat_unpack(indexdata[:4])[0]
490 494 self._initempty = False
491 495 else:
492 496 versionflags = newversionflags
493 497 except IOError as inst:
494 498 if inst.errno != errno.ENOENT:
495 499 raise
496 500
497 501 versionflags = newversionflags
498 502
499 503 self.version = versionflags
500 504
501 505 flags = versionflags & ~0xFFFF
502 506 fmt = versionflags & 0xFFFF
503 507
504 508 if fmt == REVLOGV0:
505 509 if flags:
506 510 raise error.RevlogError(
507 511 _(b'unknown flags (%#04x) in version %d revlog %s')
508 512 % (flags >> 16, fmt, self.indexfile)
509 513 )
510 514
511 515 self._inline = False
512 516 self._generaldelta = False
513 517
514 518 elif fmt == REVLOGV1:
515 519 if flags & ~REVLOGV1_FLAGS:
516 520 raise error.RevlogError(
517 521 _(b'unknown flags (%#04x) in version %d revlog %s')
518 522 % (flags >> 16, fmt, self.indexfile)
519 523 )
520 524
521 525 self._inline = versionflags & FLAG_INLINE_DATA
522 526 self._generaldelta = versionflags & FLAG_GENERALDELTA
523 527
524 528 elif fmt == REVLOGV2:
525 529 if flags & ~REVLOGV2_FLAGS:
526 530 raise error.RevlogError(
527 531 _(b'unknown flags (%#04x) in version %d revlog %s')
528 532 % (flags >> 16, fmt, self.indexfile)
529 533 )
530 534
531 535 self._inline = versionflags & FLAG_INLINE_DATA
532 536 # generaldelta implied by version 2 revlogs.
533 537 self._generaldelta = True
534 538
535 539 else:
536 540 raise error.RevlogError(
537 541 _(b'unknown version (%d) in revlog %s') % (fmt, self.indexfile)
538 542 )
539 543 # sparse-revlog can't be on without general-delta (issue6056)
540 544 if not self._generaldelta:
541 545 self._sparserevlog = False
542 546
543 547 self._storedeltachains = True
544 548
545 549 self._io = revlogio()
546 550 if self.version == REVLOGV0:
547 551 self._io = revlogoldio()
548 552 try:
549 553 d = self._io.parseindex(indexdata, self._inline)
550 554 except (ValueError, IndexError):
551 555 raise error.RevlogError(
552 556 _(b"index %s is corrupted") % self.indexfile
553 557 )
554 558 self.index, self._chunkcache = d
555 559 self.nodemap = self.index.nodemap
556 560 if not self._chunkcache:
557 561 self._chunkclear()
558 562 # revnum -> (chain-length, sum-delta-length)
559 563 self._chaininfocache = {}
560 564 # revlog header -> revlog compressor
561 565 self._decompressors = {}
562 566
563 567 @util.propertycache
564 568 def _compressor(self):
565 569 engine = util.compengines[self._compengine]
566 570 return engine.revlogcompressor(self._compengineopts)
567 571
568 572 def _indexfp(self, mode=b'r'):
569 573 """file object for the revlog's index file"""
570 574 args = {'mode': mode}
571 575 if mode != b'r':
572 576 args['checkambig'] = self._checkambig
573 577 if mode == b'w':
574 578 args['atomictemp'] = True
575 579 return self.opener(self.indexfile, **args)
576 580
577 581 def _datafp(self, mode=b'r'):
578 582 """file object for the revlog's data file"""
579 583 return self.opener(self.datafile, mode=mode)
580 584
581 585 @contextlib.contextmanager
582 586 def _datareadfp(self, existingfp=None):
583 587 """file object suitable to read data"""
584 588 # Use explicit file handle, if given.
585 589 if existingfp is not None:
586 590 yield existingfp
587 591
588 592 # Use a file handle being actively used for writes, if available.
589 593 # There is some danger to doing this because reads will seek the
590 594 # file. However, _writeentry() performs a SEEK_END before all writes,
591 595 # so we should be safe.
592 596 elif self._writinghandles:
593 597 if self._inline:
594 598 yield self._writinghandles[0]
595 599 else:
596 600 yield self._writinghandles[1]
597 601
598 602 # Otherwise open a new file handle.
599 603 else:
600 604 if self._inline:
601 605 func = self._indexfp
602 606 else:
603 607 func = self._datafp
604 608 with func() as fp:
605 609 yield fp
606 610
607 611 def tiprev(self):
608 612 return len(self.index) - 1
609 613
610 614 def tip(self):
611 615 return self.node(self.tiprev())
612 616
613 617 def __contains__(self, rev):
614 618 return 0 <= rev < len(self)
615 619
616 620 def __len__(self):
617 621 return len(self.index)
618 622
619 623 def __iter__(self):
620 624 return iter(pycompat.xrange(len(self)))
621 625
622 626 def revs(self, start=0, stop=None):
623 627 """iterate over all rev in this revlog (from start to stop)"""
624 628 return storageutil.iterrevs(len(self), start=start, stop=stop)
625 629
626 630 @util.propertycache
627 631 def nodemap(self):
628 632 if self.index:
629 633 # populate mapping down to the initial node
630 634 node0 = self.index[0][7] # get around changelog filtering
631 635 self.rev(node0)
632 636 return self.index.nodemap
633 637
634 638 @property
635 639 def _nodecache(self):
636 640 msg = "revlog._nodecache is deprecated, use revlog.index.nodemap"
637 641 util.nouideprecwarn(msg, b'5.3', stacklevel=2)
638 642 return self.index.nodemap
639 643
640 644 def hasnode(self, node):
641 645 try:
642 646 self.rev(node)
643 647 return True
644 648 except KeyError:
645 649 return False
646 650
647 651 def candelta(self, baserev, rev):
648 652 """whether two revisions (baserev, rev) can be delta-ed or not"""
649 653 # Disable delta if either rev requires a content-changing flag
650 654 # processor (ex. LFS). This is because such flag processor can alter
651 655 # the rawtext content that the delta will be based on, and two clients
652 656 # could have a same revlog node with different flags (i.e. different
653 657 # rawtext contents) and the delta could be incompatible.
654 658 if (self.flags(baserev) & REVIDX_RAWTEXT_CHANGING_FLAGS) or (
655 659 self.flags(rev) & REVIDX_RAWTEXT_CHANGING_FLAGS
656 660 ):
657 661 return False
658 662 return True
659 663
660 664 def clearcaches(self):
661 665 self._revisioncache = None
662 666 self._chainbasecache.clear()
663 667 self._chunkcache = (0, b'')
664 668 self._pcache = {}
665 669 self.index.clearcaches()
666 670
667 671 def rev(self, node):
668 672 try:
669 673 return self.index.nodemap[node]
670 674 except TypeError:
671 675 raise
672 676 except error.RevlogError:
673 677 # parsers.c radix tree lookup failed
674 678 if node == wdirid or node in wdirfilenodeids:
675 679 raise error.WdirUnsupported
676 680 raise error.LookupError(node, self.indexfile, _(b'no node'))
677 681
678 682 # Accessors for index entries.
679 683
680 684 # First tuple entry is 8 bytes. First 6 bytes are offset. Last 2 bytes
681 685 # are flags.
682 686 def start(self, rev):
683 687 return int(self.index[rev][0] >> 16)
684 688
685 689 def flags(self, rev):
686 690 return self.index[rev][0] & 0xFFFF
687 691
688 692 def length(self, rev):
689 693 return self.index[rev][1]
690 694
691 695 def rawsize(self, rev):
692 696 """return the length of the uncompressed text for a given revision"""
693 697 l = self.index[rev][2]
694 698 if l >= 0:
695 699 return l
696 700
697 701 t = self.rawdata(rev)
698 702 return len(t)
699 703
700 704 def size(self, rev):
701 705 """length of non-raw text (processed by a "read" flag processor)"""
702 706 # fast path: if no "read" flag processor could change the content,
703 707 # size is rawsize. note: ELLIPSIS is known to not change the content.
704 708 flags = self.flags(rev)
705 709 if flags & (flagutil.REVIDX_KNOWN_FLAGS ^ REVIDX_ELLIPSIS) == 0:
706 710 return self.rawsize(rev)
707 711
708 712 return len(self.revision(rev, raw=False))
709 713
710 714 def chainbase(self, rev):
711 715 base = self._chainbasecache.get(rev)
712 716 if base is not None:
713 717 return base
714 718
715 719 index = self.index
716 720 iterrev = rev
717 721 base = index[iterrev][3]
718 722 while base != iterrev:
719 723 iterrev = base
720 724 base = index[iterrev][3]
721 725
722 726 self._chainbasecache[rev] = base
723 727 return base
724 728
725 729 def linkrev(self, rev):
726 730 return self.index[rev][4]
727 731
728 732 def parentrevs(self, rev):
729 733 try:
730 734 entry = self.index[rev]
731 735 except IndexError:
732 736 if rev == wdirrev:
733 737 raise error.WdirUnsupported
734 738 raise
735 739
736 740 return entry[5], entry[6]
737 741
738 742 # fast parentrevs(rev) where rev isn't filtered
739 743 _uncheckedparentrevs = parentrevs
740 744
741 745 def node(self, rev):
742 746 try:
743 747 return self.index[rev][7]
744 748 except IndexError:
745 749 if rev == wdirrev:
746 750 raise error.WdirUnsupported
747 751 raise
748 752
749 753 # Derived from index values.
750 754
751 755 def end(self, rev):
752 756 return self.start(rev) + self.length(rev)
753 757
754 758 def parents(self, node):
755 759 i = self.index
756 760 d = i[self.rev(node)]
757 761 return i[d[5]][7], i[d[6]][7] # map revisions to nodes inline
758 762
759 763 def chainlen(self, rev):
760 764 return self._chaininfo(rev)[0]
761 765
762 766 def _chaininfo(self, rev):
763 767 chaininfocache = self._chaininfocache
764 768 if rev in chaininfocache:
765 769 return chaininfocache[rev]
766 770 index = self.index
767 771 generaldelta = self._generaldelta
768 772 iterrev = rev
769 773 e = index[iterrev]
770 774 clen = 0
771 775 compresseddeltalen = 0
772 776 while iterrev != e[3]:
773 777 clen += 1
774 778 compresseddeltalen += e[1]
775 779 if generaldelta:
776 780 iterrev = e[3]
777 781 else:
778 782 iterrev -= 1
779 783 if iterrev in chaininfocache:
780 784 t = chaininfocache[iterrev]
781 785 clen += t[0]
782 786 compresseddeltalen += t[1]
783 787 break
784 788 e = index[iterrev]
785 789 else:
786 790 # Add text length of base since decompressing that also takes
787 791 # work. For cache hits the length is already included.
788 792 compresseddeltalen += e[1]
789 793 r = (clen, compresseddeltalen)
790 794 chaininfocache[rev] = r
791 795 return r
792 796
793 797 def _deltachain(self, rev, stoprev=None):
794 798 """Obtain the delta chain for a revision.
795 799
796 800 ``stoprev`` specifies a revision to stop at. If not specified, we
797 801 stop at the base of the chain.
798 802
799 803 Returns a 2-tuple of (chain, stopped) where ``chain`` is a list of
800 804 revs in ascending order and ``stopped`` is a bool indicating whether
801 805 ``stoprev`` was hit.
802 806 """
803 807 # Try C implementation.
804 808 try:
805 809 return self.index.deltachain(rev, stoprev, self._generaldelta)
806 810 except AttributeError:
807 811 pass
808 812
809 813 chain = []
810 814
811 815 # Alias to prevent attribute lookup in tight loop.
812 816 index = self.index
813 817 generaldelta = self._generaldelta
814 818
815 819 iterrev = rev
816 820 e = index[iterrev]
817 821 while iterrev != e[3] and iterrev != stoprev:
818 822 chain.append(iterrev)
819 823 if generaldelta:
820 824 iterrev = e[3]
821 825 else:
822 826 iterrev -= 1
823 827 e = index[iterrev]
824 828
825 829 if iterrev == stoprev:
826 830 stopped = True
827 831 else:
828 832 chain.append(iterrev)
829 833 stopped = False
830 834
831 835 chain.reverse()
832 836 return chain, stopped
833 837
834 838 def ancestors(self, revs, stoprev=0, inclusive=False):
835 839 """Generate the ancestors of 'revs' in reverse revision order.
836 840 Does not generate revs lower than stoprev.
837 841
838 842 See the documentation for ancestor.lazyancestors for more details."""
839 843
840 844 # first, make sure start revisions aren't filtered
841 845 revs = list(revs)
842 846 checkrev = self.node
843 847 for r in revs:
844 848 checkrev(r)
845 849 # and we're sure ancestors aren't filtered as well
846 850
847 851 if rustancestor is not None:
848 852 lazyancestors = rustancestor.LazyAncestors
849 853 arg = self.index
850 854 elif util.safehasattr(parsers, b'rustlazyancestors'):
851 855 lazyancestors = ancestor.rustlazyancestors
852 856 arg = self.index
853 857 else:
854 858 lazyancestors = ancestor.lazyancestors
855 859 arg = self._uncheckedparentrevs
856 860 return lazyancestors(arg, revs, stoprev=stoprev, inclusive=inclusive)
857 861
858 862 def descendants(self, revs):
859 863 return dagop.descendantrevs(revs, self.revs, self.parentrevs)
860 864
861 865 def findcommonmissing(self, common=None, heads=None):
862 866 """Return a tuple of the ancestors of common and the ancestors of heads
863 867 that are not ancestors of common. In revset terminology, we return the
864 868 tuple:
865 869
866 870 ::common, (::heads) - (::common)
867 871
868 872 The list is sorted by revision number, meaning it is
869 873 topologically sorted.
870 874
871 875 'heads' and 'common' are both lists of node IDs. If heads is
872 876 not supplied, uses all of the revlog's heads. If common is not
873 877 supplied, uses nullid."""
874 878 if common is None:
875 879 common = [nullid]
876 880 if heads is None:
877 881 heads = self.heads()
878 882
879 883 common = [self.rev(n) for n in common]
880 884 heads = [self.rev(n) for n in heads]
881 885
882 886 # we want the ancestors, but inclusive
883 887 class lazyset(object):
884 888 def __init__(self, lazyvalues):
885 889 self.addedvalues = set()
886 890 self.lazyvalues = lazyvalues
887 891
888 892 def __contains__(self, value):
889 893 return value in self.addedvalues or value in self.lazyvalues
890 894
891 895 def __iter__(self):
892 896 added = self.addedvalues
893 897 for r in added:
894 898 yield r
895 899 for r in self.lazyvalues:
896 900 if not r in added:
897 901 yield r
898 902
899 903 def add(self, value):
900 904 self.addedvalues.add(value)
901 905
902 906 def update(self, values):
903 907 self.addedvalues.update(values)
904 908
905 909 has = lazyset(self.ancestors(common))
906 910 has.add(nullrev)
907 911 has.update(common)
908 912
909 913 # take all ancestors from heads that aren't in has
910 914 missing = set()
911 915 visit = collections.deque(r for r in heads if r not in has)
912 916 while visit:
913 917 r = visit.popleft()
914 918 if r in missing:
915 919 continue
916 920 else:
917 921 missing.add(r)
918 922 for p in self.parentrevs(r):
919 923 if p not in has:
920 924 visit.append(p)
921 925 missing = list(missing)
922 926 missing.sort()
923 927 return has, [self.node(miss) for miss in missing]
924 928
925 929 def incrementalmissingrevs(self, common=None):
926 930 """Return an object that can be used to incrementally compute the
927 931 revision numbers of the ancestors of arbitrary sets that are not
928 932 ancestors of common. This is an ancestor.incrementalmissingancestors
929 933 object.
930 934
931 935 'common' is a list of revision numbers. If common is not supplied, uses
932 936 nullrev.
933 937 """
934 938 if common is None:
935 939 common = [nullrev]
936 940
937 941 if rustancestor is not None:
938 942 return rustancestor.MissingAncestors(self.index, common)
939 943 return ancestor.incrementalmissingancestors(self.parentrevs, common)
940 944
941 945 def findmissingrevs(self, common=None, heads=None):
942 946 """Return the revision numbers of the ancestors of heads that
943 947 are not ancestors of common.
944 948
945 949 More specifically, return a list of revision numbers corresponding to
946 950 nodes N such that every N satisfies the following constraints:
947 951
948 952 1. N is an ancestor of some node in 'heads'
949 953 2. N is not an ancestor of any node in 'common'
950 954
951 955 The list is sorted by revision number, meaning it is
952 956 topologically sorted.
953 957
954 958 'heads' and 'common' are both lists of revision numbers. If heads is
955 959 not supplied, uses all of the revlog's heads. If common is not
956 960 supplied, uses nullid."""
957 961 if common is None:
958 962 common = [nullrev]
959 963 if heads is None:
960 964 heads = self.headrevs()
961 965
962 966 inc = self.incrementalmissingrevs(common=common)
963 967 return inc.missingancestors(heads)
964 968
965 969 def findmissing(self, common=None, heads=None):
966 970 """Return the ancestors of heads that are not ancestors of common.
967 971
968 972 More specifically, return a list of nodes N such that every N
969 973 satisfies the following constraints:
970 974
971 975 1. N is an ancestor of some node in 'heads'
972 976 2. N is not an ancestor of any node in 'common'
973 977
974 978 The list is sorted by revision number, meaning it is
975 979 topologically sorted.
976 980
977 981 'heads' and 'common' are both lists of node IDs. If heads is
978 982 not supplied, uses all of the revlog's heads. If common is not
979 983 supplied, uses nullid."""
980 984 if common is None:
981 985 common = [nullid]
982 986 if heads is None:
983 987 heads = self.heads()
984 988
985 989 common = [self.rev(n) for n in common]
986 990 heads = [self.rev(n) for n in heads]
987 991
988 992 inc = self.incrementalmissingrevs(common=common)
989 993 return [self.node(r) for r in inc.missingancestors(heads)]
990 994
991 995 def nodesbetween(self, roots=None, heads=None):
992 996 """Return a topological path from 'roots' to 'heads'.
993 997
994 998 Return a tuple (nodes, outroots, outheads) where 'nodes' is a
995 999 topologically sorted list of all nodes N that satisfy both of
996 1000 these constraints:
997 1001
998 1002 1. N is a descendant of some node in 'roots'
999 1003 2. N is an ancestor of some node in 'heads'
1000 1004
1001 1005 Every node is considered to be both a descendant and an ancestor
1002 1006 of itself, so every reachable node in 'roots' and 'heads' will be
1003 1007 included in 'nodes'.
1004 1008
1005 1009 'outroots' is the list of reachable nodes in 'roots', i.e., the
1006 1010 subset of 'roots' that is returned in 'nodes'. Likewise,
1007 1011 'outheads' is the subset of 'heads' that is also in 'nodes'.
1008 1012
1009 1013 'roots' and 'heads' are both lists of node IDs. If 'roots' is
1010 1014 unspecified, uses nullid as the only root. If 'heads' is
1011 1015 unspecified, uses list of all of the revlog's heads."""
1012 1016 nonodes = ([], [], [])
1013 1017 if roots is not None:
1014 1018 roots = list(roots)
1015 1019 if not roots:
1016 1020 return nonodes
1017 1021 lowestrev = min([self.rev(n) for n in roots])
1018 1022 else:
1019 1023 roots = [nullid] # Everybody's a descendant of nullid
1020 1024 lowestrev = nullrev
1021 1025 if (lowestrev == nullrev) and (heads is None):
1022 1026 # We want _all_ the nodes!
1023 1027 return ([self.node(r) for r in self], [nullid], list(self.heads()))
1024 1028 if heads is None:
1025 1029 # All nodes are ancestors, so the latest ancestor is the last
1026 1030 # node.
1027 1031 highestrev = len(self) - 1
1028 1032 # Set ancestors to None to signal that every node is an ancestor.
1029 1033 ancestors = None
1030 1034 # Set heads to an empty dictionary for later discovery of heads
1031 1035 heads = {}
1032 1036 else:
1033 1037 heads = list(heads)
1034 1038 if not heads:
1035 1039 return nonodes
1036 1040 ancestors = set()
1037 1041 # Turn heads into a dictionary so we can remove 'fake' heads.
1038 1042 # Also, later we will be using it to filter out the heads we can't
1039 1043 # find from roots.
1040 1044 heads = dict.fromkeys(heads, False)
1041 1045 # Start at the top and keep marking parents until we're done.
1042 1046 nodestotag = set(heads)
1043 1047 # Remember where the top was so we can use it as a limit later.
1044 1048 highestrev = max([self.rev(n) for n in nodestotag])
1045 1049 while nodestotag:
1046 1050 # grab a node to tag
1047 1051 n = nodestotag.pop()
1048 1052 # Never tag nullid
1049 1053 if n == nullid:
1050 1054 continue
1051 1055 # A node's revision number represents its place in a
1052 1056 # topologically sorted list of nodes.
1053 1057 r = self.rev(n)
1054 1058 if r >= lowestrev:
1055 1059 if n not in ancestors:
1056 1060 # If we are possibly a descendant of one of the roots
1057 1061 # and we haven't already been marked as an ancestor
1058 1062 ancestors.add(n) # Mark as ancestor
1059 1063 # Add non-nullid parents to list of nodes to tag.
1060 1064 nodestotag.update(
1061 1065 [p for p in self.parents(n) if p != nullid]
1062 1066 )
1063 1067 elif n in heads: # We've seen it before, is it a fake head?
1064 1068 # So it is, real heads should not be the ancestors of
1065 1069 # any other heads.
1066 1070 heads.pop(n)
1067 1071 if not ancestors:
1068 1072 return nonodes
1069 1073 # Now that we have our set of ancestors, we want to remove any
1070 1074 # roots that are not ancestors.
1071 1075
1072 1076 # If one of the roots was nullid, everything is included anyway.
1073 1077 if lowestrev > nullrev:
1074 1078 # But, since we weren't, let's recompute the lowest rev to not
1075 1079 # include roots that aren't ancestors.
1076 1080
1077 1081 # Filter out roots that aren't ancestors of heads
1078 1082 roots = [root for root in roots if root in ancestors]
1079 1083 # Recompute the lowest revision
1080 1084 if roots:
1081 1085 lowestrev = min([self.rev(root) for root in roots])
1082 1086 else:
1083 1087 # No more roots? Return empty list
1084 1088 return nonodes
1085 1089 else:
1086 1090 # We are descending from nullid, and don't need to care about
1087 1091 # any other roots.
1088 1092 lowestrev = nullrev
1089 1093 roots = [nullid]
1090 1094 # Transform our roots list into a set.
1091 1095 descendants = set(roots)
1092 1096 # Also, keep the original roots so we can filter out roots that aren't
1093 1097 # 'real' roots (i.e. are descended from other roots).
1094 1098 roots = descendants.copy()
1095 1099 # Our topologically sorted list of output nodes.
1096 1100 orderedout = []
1097 1101 # Don't start at nullid since we don't want nullid in our output list,
1098 1102 # and if nullid shows up in descendants, empty parents will look like
1099 1103 # they're descendants.
1100 1104 for r in self.revs(start=max(lowestrev, 0), stop=highestrev + 1):
1101 1105 n = self.node(r)
1102 1106 isdescendant = False
1103 1107 if lowestrev == nullrev: # Everybody is a descendant of nullid
1104 1108 isdescendant = True
1105 1109 elif n in descendants:
1106 1110 # n is already a descendant
1107 1111 isdescendant = True
1108 1112 # This check only needs to be done here because all the roots
1109 1113 # will start being marked is descendants before the loop.
1110 1114 if n in roots:
1111 1115 # If n was a root, check if it's a 'real' root.
1112 1116 p = tuple(self.parents(n))
1113 1117 # If any of its parents are descendants, it's not a root.
1114 1118 if (p[0] in descendants) or (p[1] in descendants):
1115 1119 roots.remove(n)
1116 1120 else:
1117 1121 p = tuple(self.parents(n))
1118 1122 # A node is a descendant if either of its parents are
1119 1123 # descendants. (We seeded the dependents list with the roots
1120 1124 # up there, remember?)
1121 1125 if (p[0] in descendants) or (p[1] in descendants):
1122 1126 descendants.add(n)
1123 1127 isdescendant = True
1124 1128 if isdescendant and ((ancestors is None) or (n in ancestors)):
1125 1129 # Only include nodes that are both descendants and ancestors.
1126 1130 orderedout.append(n)
1127 1131 if (ancestors is not None) and (n in heads):
1128 1132 # We're trying to figure out which heads are reachable
1129 1133 # from roots.
1130 1134 # Mark this head as having been reached
1131 1135 heads[n] = True
1132 1136 elif ancestors is None:
1133 1137 # Otherwise, we're trying to discover the heads.
1134 1138 # Assume this is a head because if it isn't, the next step
1135 1139 # will eventually remove it.
1136 1140 heads[n] = True
1137 1141 # But, obviously its parents aren't.
1138 1142 for p in self.parents(n):
1139 1143 heads.pop(p, None)
1140 1144 heads = [head for head, flag in pycompat.iteritems(heads) if flag]
1141 1145 roots = list(roots)
1142 1146 assert orderedout
1143 1147 assert roots
1144 1148 assert heads
1145 1149 return (orderedout, roots, heads)
1146 1150
1147 1151 def headrevs(self, revs=None):
1148 1152 if revs is None:
1149 1153 try:
1150 1154 return self.index.headrevs()
1151 1155 except AttributeError:
1152 1156 return self._headrevs()
1153 1157 if rustdagop is not None:
1154 1158 return rustdagop.headrevs(self.index, revs)
1155 1159 return dagop.headrevs(revs, self._uncheckedparentrevs)
1156 1160
1157 1161 def computephases(self, roots):
1158 1162 return self.index.computephasesmapsets(roots)
1159 1163
1160 1164 def _headrevs(self):
1161 1165 count = len(self)
1162 1166 if not count:
1163 1167 return [nullrev]
1164 1168 # we won't iter over filtered rev so nobody is a head at start
1165 1169 ishead = [0] * (count + 1)
1166 1170 index = self.index
1167 1171 for r in self:
1168 1172 ishead[r] = 1 # I may be an head
1169 1173 e = index[r]
1170 1174 ishead[e[5]] = ishead[e[6]] = 0 # my parent are not
1171 1175 return [r for r, val in enumerate(ishead) if val]
1172 1176
1173 1177 def heads(self, start=None, stop=None):
1174 1178 """return the list of all nodes that have no children
1175 1179
1176 1180 if start is specified, only heads that are descendants of
1177 1181 start will be returned
1178 1182 if stop is specified, it will consider all the revs from stop
1179 1183 as if they had no children
1180 1184 """
1181 1185 if start is None and stop is None:
1182 1186 if not len(self):
1183 1187 return [nullid]
1184 1188 return [self.node(r) for r in self.headrevs()]
1185 1189
1186 1190 if start is None:
1187 1191 start = nullrev
1188 1192 else:
1189 1193 start = self.rev(start)
1190 1194
1191 1195 stoprevs = set(self.rev(n) for n in stop or [])
1192 1196
1193 1197 revs = dagop.headrevssubset(
1194 1198 self.revs, self.parentrevs, startrev=start, stoprevs=stoprevs
1195 1199 )
1196 1200
1197 1201 return [self.node(rev) for rev in revs]
1198 1202
1199 1203 def children(self, node):
1200 1204 """find the children of a given node"""
1201 1205 c = []
1202 1206 p = self.rev(node)
1203 1207 for r in self.revs(start=p + 1):
1204 1208 prevs = [pr for pr in self.parentrevs(r) if pr != nullrev]
1205 1209 if prevs:
1206 1210 for pr in prevs:
1207 1211 if pr == p:
1208 1212 c.append(self.node(r))
1209 1213 elif p == nullrev:
1210 1214 c.append(self.node(r))
1211 1215 return c
1212 1216
1213 1217 def commonancestorsheads(self, a, b):
1214 1218 """calculate all the heads of the common ancestors of nodes a and b"""
1215 1219 a, b = self.rev(a), self.rev(b)
1216 1220 ancs = self._commonancestorsheads(a, b)
1217 1221 return pycompat.maplist(self.node, ancs)
1218 1222
1219 1223 def _commonancestorsheads(self, *revs):
1220 1224 """calculate all the heads of the common ancestors of revs"""
1221 1225 try:
1222 1226 ancs = self.index.commonancestorsheads(*revs)
1223 1227 except (AttributeError, OverflowError): # C implementation failed
1224 1228 ancs = ancestor.commonancestorsheads(self.parentrevs, *revs)
1225 1229 return ancs
1226 1230
1227 1231 def isancestor(self, a, b):
1228 1232 """return True if node a is an ancestor of node b
1229 1233
1230 1234 A revision is considered an ancestor of itself."""
1231 1235 a, b = self.rev(a), self.rev(b)
1232 1236 return self.isancestorrev(a, b)
1233 1237
1234 1238 def isancestorrev(self, a, b):
1235 1239 """return True if revision a is an ancestor of revision b
1236 1240
1237 1241 A revision is considered an ancestor of itself.
1238 1242
1239 1243 The implementation of this is trivial but the use of
1240 1244 reachableroots is not."""
1241 1245 if a == nullrev:
1242 1246 return True
1243 1247 elif a == b:
1244 1248 return True
1245 1249 elif a > b:
1246 1250 return False
1247 1251 return bool(self.reachableroots(a, [b], [a], includepath=False))
1248 1252
1249 1253 def reachableroots(self, minroot, heads, roots, includepath=False):
1250 1254 """return (heads(::<roots> and <roots>::<heads>))
1251 1255
1252 1256 If includepath is True, return (<roots>::<heads>)."""
1253 1257 try:
1254 1258 return self.index.reachableroots2(
1255 1259 minroot, heads, roots, includepath
1256 1260 )
1257 1261 except AttributeError:
1258 1262 return dagop._reachablerootspure(
1259 1263 self.parentrevs, minroot, roots, heads, includepath
1260 1264 )
1261 1265
1262 1266 def ancestor(self, a, b):
1263 1267 """calculate the "best" common ancestor of nodes a and b"""
1264 1268
1265 1269 a, b = self.rev(a), self.rev(b)
1266 1270 try:
1267 1271 ancs = self.index.ancestors(a, b)
1268 1272 except (AttributeError, OverflowError):
1269 1273 ancs = ancestor.ancestors(self.parentrevs, a, b)
1270 1274 if ancs:
1271 1275 # choose a consistent winner when there's a tie
1272 1276 return min(map(self.node, ancs))
1273 1277 return nullid
1274 1278
1275 1279 def _match(self, id):
1276 1280 if isinstance(id, int):
1277 1281 # rev
1278 1282 return self.node(id)
1279 1283 if len(id) == 20:
1280 1284 # possibly a binary node
1281 1285 # odds of a binary node being all hex in ASCII are 1 in 10**25
1282 1286 try:
1283 1287 node = id
1284 1288 self.rev(node) # quick search the index
1285 1289 return node
1286 1290 except error.LookupError:
1287 1291 pass # may be partial hex id
1288 1292 try:
1289 1293 # str(rev)
1290 1294 rev = int(id)
1291 1295 if b"%d" % rev != id:
1292 1296 raise ValueError
1293 1297 if rev < 0:
1294 1298 rev = len(self) + rev
1295 1299 if rev < 0 or rev >= len(self):
1296 1300 raise ValueError
1297 1301 return self.node(rev)
1298 1302 except (ValueError, OverflowError):
1299 1303 pass
1300 1304 if len(id) == 40:
1301 1305 try:
1302 1306 # a full hex nodeid?
1303 1307 node = bin(id)
1304 1308 self.rev(node)
1305 1309 return node
1306 1310 except (TypeError, error.LookupError):
1307 1311 pass
1308 1312
1309 1313 def _partialmatch(self, id):
1310 1314 # we don't care wdirfilenodeids as they should be always full hash
1311 1315 maybewdir = wdirhex.startswith(id)
1312 1316 try:
1313 1317 partial = self.index.partialmatch(id)
1314 1318 if partial and self.hasnode(partial):
1315 1319 if maybewdir:
1316 1320 # single 'ff...' match in radix tree, ambiguous with wdir
1317 1321 raise error.RevlogError
1318 1322 return partial
1319 1323 if maybewdir:
1320 1324 # no 'ff...' match in radix tree, wdir identified
1321 1325 raise error.WdirUnsupported
1322 1326 return None
1323 1327 except error.RevlogError:
1324 1328 # parsers.c radix tree lookup gave multiple matches
1325 1329 # fast path: for unfiltered changelog, radix tree is accurate
1326 1330 if not getattr(self, 'filteredrevs', None):
1327 1331 raise error.AmbiguousPrefixLookupError(
1328 1332 id, self.indexfile, _(b'ambiguous identifier')
1329 1333 )
1330 1334 # fall through to slow path that filters hidden revisions
1331 1335 except (AttributeError, ValueError):
1332 1336 # we are pure python, or key was too short to search radix tree
1333 1337 pass
1334 1338
1335 1339 if id in self._pcache:
1336 1340 return self._pcache[id]
1337 1341
1338 1342 if len(id) <= 40:
1339 1343 try:
1340 1344 # hex(node)[:...]
1341 1345 l = len(id) // 2 # grab an even number of digits
1342 1346 prefix = bin(id[: l * 2])
1343 1347 nl = [e[7] for e in self.index if e[7].startswith(prefix)]
1344 1348 nl = [
1345 1349 n for n in nl if hex(n).startswith(id) and self.hasnode(n)
1346 1350 ]
1347 1351 if nullhex.startswith(id):
1348 1352 nl.append(nullid)
1349 1353 if len(nl) > 0:
1350 1354 if len(nl) == 1 and not maybewdir:
1351 1355 self._pcache[id] = nl[0]
1352 1356 return nl[0]
1353 1357 raise error.AmbiguousPrefixLookupError(
1354 1358 id, self.indexfile, _(b'ambiguous identifier')
1355 1359 )
1356 1360 if maybewdir:
1357 1361 raise error.WdirUnsupported
1358 1362 return None
1359 1363 except TypeError:
1360 1364 pass
1361 1365
1362 1366 def lookup(self, id):
1363 1367 """locate a node based on:
1364 1368 - revision number or str(revision number)
1365 1369 - nodeid or subset of hex nodeid
1366 1370 """
1367 1371 n = self._match(id)
1368 1372 if n is not None:
1369 1373 return n
1370 1374 n = self._partialmatch(id)
1371 1375 if n:
1372 1376 return n
1373 1377
1374 1378 raise error.LookupError(id, self.indexfile, _(b'no match found'))
1375 1379
1376 1380 def shortest(self, node, minlength=1):
1377 1381 """Find the shortest unambiguous prefix that matches node."""
1378 1382
1379 1383 def isvalid(prefix):
1380 1384 try:
1381 1385 matchednode = self._partialmatch(prefix)
1382 1386 except error.AmbiguousPrefixLookupError:
1383 1387 return False
1384 1388 except error.WdirUnsupported:
1385 1389 # single 'ff...' match
1386 1390 return True
1387 1391 if matchednode is None:
1388 1392 raise error.LookupError(node, self.indexfile, _(b'no node'))
1389 1393 return True
1390 1394
1391 1395 def maybewdir(prefix):
1392 1396 return all(c == b'f' for c in pycompat.iterbytestr(prefix))
1393 1397
1394 1398 hexnode = hex(node)
1395 1399
1396 1400 def disambiguate(hexnode, minlength):
1397 1401 """Disambiguate against wdirid."""
1398 1402 for length in range(minlength, 41):
1399 1403 prefix = hexnode[:length]
1400 1404 if not maybewdir(prefix):
1401 1405 return prefix
1402 1406
1403 1407 if not getattr(self, 'filteredrevs', None):
1404 1408 try:
1405 1409 length = max(self.index.shortest(node), minlength)
1406 1410 return disambiguate(hexnode, length)
1407 1411 except error.RevlogError:
1408 1412 if node != wdirid:
1409 1413 raise error.LookupError(node, self.indexfile, _(b'no node'))
1410 1414 except AttributeError:
1411 1415 # Fall through to pure code
1412 1416 pass
1413 1417
1414 1418 if node == wdirid:
1415 1419 for length in range(minlength, 41):
1416 1420 prefix = hexnode[:length]
1417 1421 if isvalid(prefix):
1418 1422 return prefix
1419 1423
1420 1424 for length in range(minlength, 41):
1421 1425 prefix = hexnode[:length]
1422 1426 if isvalid(prefix):
1423 1427 return disambiguate(hexnode, length)
1424 1428
1425 1429 def cmp(self, node, text):
1426 1430 """compare text with a given file revision
1427 1431
1428 1432 returns True if text is different than what is stored.
1429 1433 """
1430 1434 p1, p2 = self.parents(node)
1431 1435 return storageutil.hashrevisionsha1(text, p1, p2) != node
1432 1436
1433 1437 def _cachesegment(self, offset, data):
1434 1438 """Add a segment to the revlog cache.
1435 1439
1436 1440 Accepts an absolute offset and the data that is at that location.
1437 1441 """
1438 1442 o, d = self._chunkcache
1439 1443 # try to add to existing cache
1440 1444 if o + len(d) == offset and len(d) + len(data) < _chunksize:
1441 1445 self._chunkcache = o, d + data
1442 1446 else:
1443 1447 self._chunkcache = offset, data
1444 1448
1445 1449 def _readsegment(self, offset, length, df=None):
1446 1450 """Load a segment of raw data from the revlog.
1447 1451
1448 1452 Accepts an absolute offset, length to read, and an optional existing
1449 1453 file handle to read from.
1450 1454
1451 1455 If an existing file handle is passed, it will be seeked and the
1452 1456 original seek position will NOT be restored.
1453 1457
1454 1458 Returns a str or buffer of raw byte data.
1455 1459
1456 1460 Raises if the requested number of bytes could not be read.
1457 1461 """
1458 1462 # Cache data both forward and backward around the requested
1459 1463 # data, in a fixed size window. This helps speed up operations
1460 1464 # involving reading the revlog backwards.
1461 1465 cachesize = self._chunkcachesize
1462 1466 realoffset = offset & ~(cachesize - 1)
1463 1467 reallength = (
1464 1468 (offset + length + cachesize) & ~(cachesize - 1)
1465 1469 ) - realoffset
1466 1470 with self._datareadfp(df) as df:
1467 1471 df.seek(realoffset)
1468 1472 d = df.read(reallength)
1469 1473
1470 1474 self._cachesegment(realoffset, d)
1471 1475 if offset != realoffset or reallength != length:
1472 1476 startoffset = offset - realoffset
1473 1477 if len(d) - startoffset < length:
1474 1478 raise error.RevlogError(
1475 1479 _(
1476 1480 b'partial read of revlog %s; expected %d bytes from '
1477 1481 b'offset %d, got %d'
1478 1482 )
1479 1483 % (
1480 1484 self.indexfile if self._inline else self.datafile,
1481 1485 length,
1482 1486 realoffset,
1483 1487 len(d) - startoffset,
1484 1488 )
1485 1489 )
1486 1490
1487 1491 return util.buffer(d, startoffset, length)
1488 1492
1489 1493 if len(d) < length:
1490 1494 raise error.RevlogError(
1491 1495 _(
1492 1496 b'partial read of revlog %s; expected %d bytes from offset '
1493 1497 b'%d, got %d'
1494 1498 )
1495 1499 % (
1496 1500 self.indexfile if self._inline else self.datafile,
1497 1501 length,
1498 1502 offset,
1499 1503 len(d),
1500 1504 )
1501 1505 )
1502 1506
1503 1507 return d
1504 1508
1505 1509 def _getsegment(self, offset, length, df=None):
1506 1510 """Obtain a segment of raw data from the revlog.
1507 1511
1508 1512 Accepts an absolute offset, length of bytes to obtain, and an
1509 1513 optional file handle to the already-opened revlog. If the file
1510 1514 handle is used, it's original seek position will not be preserved.
1511 1515
1512 1516 Requests for data may be returned from a cache.
1513 1517
1514 1518 Returns a str or a buffer instance of raw byte data.
1515 1519 """
1516 1520 o, d = self._chunkcache
1517 1521 l = len(d)
1518 1522
1519 1523 # is it in the cache?
1520 1524 cachestart = offset - o
1521 1525 cacheend = cachestart + length
1522 1526 if cachestart >= 0 and cacheend <= l:
1523 1527 if cachestart == 0 and cacheend == l:
1524 1528 return d # avoid a copy
1525 1529 return util.buffer(d, cachestart, cacheend - cachestart)
1526 1530
1527 1531 return self._readsegment(offset, length, df=df)
1528 1532
1529 1533 def _getsegmentforrevs(self, startrev, endrev, df=None):
1530 1534 """Obtain a segment of raw data corresponding to a range of revisions.
1531 1535
1532 1536 Accepts the start and end revisions and an optional already-open
1533 1537 file handle to be used for reading. If the file handle is read, its
1534 1538 seek position will not be preserved.
1535 1539
1536 1540 Requests for data may be satisfied by a cache.
1537 1541
1538 1542 Returns a 2-tuple of (offset, data) for the requested range of
1539 1543 revisions. Offset is the integer offset from the beginning of the
1540 1544 revlog and data is a str or buffer of the raw byte data.
1541 1545
1542 1546 Callers will need to call ``self.start(rev)`` and ``self.length(rev)``
1543 1547 to determine where each revision's data begins and ends.
1544 1548 """
1545 1549 # Inlined self.start(startrev) & self.end(endrev) for perf reasons
1546 1550 # (functions are expensive).
1547 1551 index = self.index
1548 1552 istart = index[startrev]
1549 1553 start = int(istart[0] >> 16)
1550 1554 if startrev == endrev:
1551 1555 end = start + istart[1]
1552 1556 else:
1553 1557 iend = index[endrev]
1554 1558 end = int(iend[0] >> 16) + iend[1]
1555 1559
1556 1560 if self._inline:
1557 1561 start += (startrev + 1) * self._io.size
1558 1562 end += (endrev + 1) * self._io.size
1559 1563 length = end - start
1560 1564
1561 1565 return start, self._getsegment(start, length, df=df)
1562 1566
1563 1567 def _chunk(self, rev, df=None):
1564 1568 """Obtain a single decompressed chunk for a revision.
1565 1569
1566 1570 Accepts an integer revision and an optional already-open file handle
1567 1571 to be used for reading. If used, the seek position of the file will not
1568 1572 be preserved.
1569 1573
1570 1574 Returns a str holding uncompressed data for the requested revision.
1571 1575 """
1572 1576 return self.decompress(self._getsegmentforrevs(rev, rev, df=df)[1])
1573 1577
1574 1578 def _chunks(self, revs, df=None, targetsize=None):
1575 1579 """Obtain decompressed chunks for the specified revisions.
1576 1580
1577 1581 Accepts an iterable of numeric revisions that are assumed to be in
1578 1582 ascending order. Also accepts an optional already-open file handle
1579 1583 to be used for reading. If used, the seek position of the file will
1580 1584 not be preserved.
1581 1585
1582 1586 This function is similar to calling ``self._chunk()`` multiple times,
1583 1587 but is faster.
1584 1588
1585 1589 Returns a list with decompressed data for each requested revision.
1586 1590 """
1587 1591 if not revs:
1588 1592 return []
1589 1593 start = self.start
1590 1594 length = self.length
1591 1595 inline = self._inline
1592 1596 iosize = self._io.size
1593 1597 buffer = util.buffer
1594 1598
1595 1599 l = []
1596 1600 ladd = l.append
1597 1601
1598 1602 if not self._withsparseread:
1599 1603 slicedchunks = (revs,)
1600 1604 else:
1601 1605 slicedchunks = deltautil.slicechunk(
1602 1606 self, revs, targetsize=targetsize
1603 1607 )
1604 1608
1605 1609 for revschunk in slicedchunks:
1606 1610 firstrev = revschunk[0]
1607 1611 # Skip trailing revisions with empty diff
1608 1612 for lastrev in revschunk[::-1]:
1609 1613 if length(lastrev) != 0:
1610 1614 break
1611 1615
1612 1616 try:
1613 1617 offset, data = self._getsegmentforrevs(firstrev, lastrev, df=df)
1614 1618 except OverflowError:
1615 1619 # issue4215 - we can't cache a run of chunks greater than
1616 1620 # 2G on Windows
1617 1621 return [self._chunk(rev, df=df) for rev in revschunk]
1618 1622
1619 1623 decomp = self.decompress
1620 1624 for rev in revschunk:
1621 1625 chunkstart = start(rev)
1622 1626 if inline:
1623 1627 chunkstart += (rev + 1) * iosize
1624 1628 chunklength = length(rev)
1625 1629 ladd(decomp(buffer(data, chunkstart - offset, chunklength)))
1626 1630
1627 1631 return l
1628 1632
1629 1633 def _chunkclear(self):
1630 1634 """Clear the raw chunk cache."""
1631 1635 self._chunkcache = (0, b'')
1632 1636
1633 1637 def deltaparent(self, rev):
1634 1638 """return deltaparent of the given revision"""
1635 1639 base = self.index[rev][3]
1636 1640 if base == rev:
1637 1641 return nullrev
1638 1642 elif self._generaldelta:
1639 1643 return base
1640 1644 else:
1641 1645 return rev - 1
1642 1646
1643 1647 def issnapshot(self, rev):
1644 1648 """tells whether rev is a snapshot
1645 1649 """
1646 1650 if not self._sparserevlog:
1647 1651 return self.deltaparent(rev) == nullrev
1648 1652 elif util.safehasattr(self.index, b'issnapshot'):
1649 1653 # directly assign the method to cache the testing and access
1650 1654 self.issnapshot = self.index.issnapshot
1651 1655 return self.issnapshot(rev)
1652 1656 if rev == nullrev:
1653 1657 return True
1654 1658 entry = self.index[rev]
1655 1659 base = entry[3]
1656 1660 if base == rev:
1657 1661 return True
1658 1662 if base == nullrev:
1659 1663 return True
1660 1664 p1 = entry[5]
1661 1665 p2 = entry[6]
1662 1666 if base == p1 or base == p2:
1663 1667 return False
1664 1668 return self.issnapshot(base)
1665 1669
1666 1670 def snapshotdepth(self, rev):
1667 1671 """number of snapshot in the chain before this one"""
1668 1672 if not self.issnapshot(rev):
1669 1673 raise error.ProgrammingError(b'revision %d not a snapshot')
1670 1674 return len(self._deltachain(rev)[0]) - 1
1671 1675
1672 1676 def revdiff(self, rev1, rev2):
1673 1677 """return or calculate a delta between two revisions
1674 1678
1675 1679 The delta calculated is in binary form and is intended to be written to
1676 1680 revlog data directly. So this function needs raw revision data.
1677 1681 """
1678 1682 if rev1 != nullrev and self.deltaparent(rev2) == rev1:
1679 1683 return bytes(self._chunk(rev2))
1680 1684
1681 1685 return mdiff.textdiff(self.rawdata(rev1), self.rawdata(rev2))
1682 1686
1683 1687 def _processflags(self, text, flags, operation, raw=False):
1684 1688 """deprecated entry point to access flag processors"""
1685 1689 msg = b'_processflag(...) use the specialized variant'
1686 1690 util.nouideprecwarn(msg, b'5.2', stacklevel=2)
1687 1691 if raw:
1688 1692 return text, flagutil.processflagsraw(self, text, flags)
1689 1693 elif operation == b'read':
1690 1694 return flagutil.processflagsread(self, text, flags)
1691 1695 else: # write operation
1692 1696 return flagutil.processflagswrite(self, text, flags)
1693 1697
1694 1698 def revision(self, nodeorrev, _df=None, raw=False):
1695 1699 """return an uncompressed revision of a given node or revision
1696 1700 number.
1697 1701
1698 1702 _df - an existing file handle to read from. (internal-only)
1699 1703 raw - an optional argument specifying if the revision data is to be
1700 1704 treated as raw data when applying flag transforms. 'raw' should be set
1701 1705 to True when generating changegroups or in debug commands.
1702 1706 """
1703 1707 if raw:
1704 1708 msg = (
1705 1709 b'revlog.revision(..., raw=True) is deprecated, '
1706 1710 b'use revlog.rawdata(...)'
1707 1711 )
1708 1712 util.nouideprecwarn(msg, b'5.2', stacklevel=2)
1709 1713 return self._revisiondata(nodeorrev, _df, raw=raw)[0]
1710 1714
1711 1715 def sidedata(self, nodeorrev, _df=None):
1712 1716 """a map of extra data related to the changeset but not part of the hash
1713 1717
1714 1718 This function currently return a dictionary. However, more advanced
1715 1719 mapping object will likely be used in the future for a more
1716 1720 efficient/lazy code.
1717 1721 """
1718 1722 return self._revisiondata(nodeorrev, _df)[1]
1719 1723
1720 1724 def _revisiondata(self, nodeorrev, _df=None, raw=False):
1721 1725 # deal with <nodeorrev> argument type
1722 1726 if isinstance(nodeorrev, int):
1723 1727 rev = nodeorrev
1724 1728 node = self.node(rev)
1725 1729 else:
1726 1730 node = nodeorrev
1727 1731 rev = None
1728 1732
1729 1733 # fast path the special `nullid` rev
1730 1734 if node == nullid:
1731 1735 return b"", {}
1732 1736
1733 1737 # The text as stored inside the revlog. Might be the revision or might
1734 1738 # need to be processed to retrieve the revision.
1735 1739 rawtext = None
1736 1740
1737 1741 rev, rawtext, validated = self._rawtext(node, rev, _df=_df)
1738 1742
1739 1743 if raw and validated:
1740 1744 # if we don't want to process the raw text and that raw
1741 1745 # text is cached, we can exit early.
1742 1746 return rawtext, {}
1743 1747 if rev is None:
1744 1748 rev = self.rev(node)
1745 1749 # the revlog's flag for this revision
1746 1750 # (usually alter its state or content)
1747 1751 flags = self.flags(rev)
1748 1752
1749 1753 if validated and flags == REVIDX_DEFAULT_FLAGS:
1750 1754 # no extra flags set, no flag processor runs, text = rawtext
1751 1755 return rawtext, {}
1752 1756
1753 1757 sidedata = {}
1754 1758 if raw:
1755 1759 validatehash = flagutil.processflagsraw(self, rawtext, flags)
1756 1760 text = rawtext
1757 1761 else:
1758 1762 try:
1759 1763 r = flagutil.processflagsread(self, rawtext, flags)
1760 1764 except error.SidedataHashError as exc:
1761 1765 msg = _(b"integrity check failed on %s:%s sidedata key %d")
1762 1766 msg %= (self.indexfile, pycompat.bytestr(rev), exc.sidedatakey)
1763 1767 raise error.RevlogError(msg)
1764 1768 text, validatehash, sidedata = r
1765 1769 if validatehash:
1766 1770 self.checkhash(text, node, rev=rev)
1767 1771 if not validated:
1768 1772 self._revisioncache = (node, rev, rawtext)
1769 1773
1770 1774 return text, sidedata
1771 1775
1772 1776 def _rawtext(self, node, rev, _df=None):
1773 1777 """return the possibly unvalidated rawtext for a revision
1774 1778
1775 1779 returns (rev, rawtext, validated)
1776 1780 """
1777 1781
1778 1782 # revision in the cache (could be useful to apply delta)
1779 1783 cachedrev = None
1780 1784 # An intermediate text to apply deltas to
1781 1785 basetext = None
1782 1786
1783 1787 # Check if we have the entry in cache
1784 1788 # The cache entry looks like (node, rev, rawtext)
1785 1789 if self._revisioncache:
1786 1790 if self._revisioncache[0] == node:
1787 1791 return (rev, self._revisioncache[2], True)
1788 1792 cachedrev = self._revisioncache[1]
1789 1793
1790 1794 if rev is None:
1791 1795 rev = self.rev(node)
1792 1796
1793 1797 chain, stopped = self._deltachain(rev, stoprev=cachedrev)
1794 1798 if stopped:
1795 1799 basetext = self._revisioncache[2]
1796 1800
1797 1801 # drop cache to save memory, the caller is expected to
1798 1802 # update self._revisioncache after validating the text
1799 1803 self._revisioncache = None
1800 1804
1801 1805 targetsize = None
1802 1806 rawsize = self.index[rev][2]
1803 1807 if 0 <= rawsize:
1804 1808 targetsize = 4 * rawsize
1805 1809
1806 1810 bins = self._chunks(chain, df=_df, targetsize=targetsize)
1807 1811 if basetext is None:
1808 1812 basetext = bytes(bins[0])
1809 1813 bins = bins[1:]
1810 1814
1811 1815 rawtext = mdiff.patches(basetext, bins)
1812 1816 del basetext # let us have a chance to free memory early
1813 1817 return (rev, rawtext, False)
1814 1818
1815 1819 def rawdata(self, nodeorrev, _df=None):
1816 1820 """return an uncompressed raw data of a given node or revision number.
1817 1821
1818 1822 _df - an existing file handle to read from. (internal-only)
1819 1823 """
1820 1824 return self._revisiondata(nodeorrev, _df, raw=True)[0]
1821 1825
1822 1826 def hash(self, text, p1, p2):
1823 1827 """Compute a node hash.
1824 1828
1825 1829 Available as a function so that subclasses can replace the hash
1826 1830 as needed.
1827 1831 """
1828 1832 return storageutil.hashrevisionsha1(text, p1, p2)
1829 1833
1830 1834 def checkhash(self, text, node, p1=None, p2=None, rev=None):
1831 1835 """Check node hash integrity.
1832 1836
1833 1837 Available as a function so that subclasses can extend hash mismatch
1834 1838 behaviors as needed.
1835 1839 """
1836 1840 try:
1837 1841 if p1 is None and p2 is None:
1838 1842 p1, p2 = self.parents(node)
1839 1843 if node != self.hash(text, p1, p2):
1840 1844 # Clear the revision cache on hash failure. The revision cache
1841 1845 # only stores the raw revision and clearing the cache does have
1842 1846 # the side-effect that we won't have a cache hit when the raw
1843 1847 # revision data is accessed. But this case should be rare and
1844 1848 # it is extra work to teach the cache about the hash
1845 1849 # verification state.
1846 1850 if self._revisioncache and self._revisioncache[0] == node:
1847 1851 self._revisioncache = None
1848 1852
1849 1853 revornode = rev
1850 1854 if revornode is None:
1851 1855 revornode = templatefilters.short(hex(node))
1852 1856 raise error.RevlogError(
1853 1857 _(b"integrity check failed on %s:%s")
1854 1858 % (self.indexfile, pycompat.bytestr(revornode))
1855 1859 )
1856 1860 except error.RevlogError:
1857 1861 if self._censorable and storageutil.iscensoredtext(text):
1858 1862 raise error.CensoredNodeError(self.indexfile, node, text)
1859 1863 raise
1860 1864
1861 1865 def _enforceinlinesize(self, tr, fp=None):
1862 1866 """Check if the revlog is too big for inline and convert if so.
1863 1867
1864 1868 This should be called after revisions are added to the revlog. If the
1865 1869 revlog has grown too large to be an inline revlog, it will convert it
1866 1870 to use multiple index and data files.
1867 1871 """
1868 1872 tiprev = len(self) - 1
1869 1873 if (
1870 1874 not self._inline
1871 1875 or (self.start(tiprev) + self.length(tiprev)) < _maxinline
1872 1876 ):
1873 1877 return
1874 1878
1875 1879 trinfo = tr.find(self.indexfile)
1876 1880 if trinfo is None:
1877 1881 raise error.RevlogError(
1878 1882 _(b"%s not found in the transaction") % self.indexfile
1879 1883 )
1880 1884
1881 1885 trindex = trinfo[2]
1882 1886 if trindex is not None:
1883 1887 dataoff = self.start(trindex)
1884 1888 else:
1885 1889 # revlog was stripped at start of transaction, use all leftover data
1886 1890 trindex = len(self) - 1
1887 1891 dataoff = self.end(tiprev)
1888 1892
1889 1893 tr.add(self.datafile, dataoff)
1890 1894
1891 1895 if fp:
1892 1896 fp.flush()
1893 1897 fp.close()
1894 1898 # We can't use the cached file handle after close(). So prevent
1895 1899 # its usage.
1896 1900 self._writinghandles = None
1897 1901
1898 1902 with self._indexfp(b'r') as ifh, self._datafp(b'w') as dfh:
1899 1903 for r in self:
1900 1904 dfh.write(self._getsegmentforrevs(r, r, df=ifh)[1])
1901 1905
1902 1906 with self._indexfp(b'w') as fp:
1903 1907 self.version &= ~FLAG_INLINE_DATA
1904 1908 self._inline = False
1905 1909 io = self._io
1906 1910 for i in self:
1907 1911 e = io.packentry(self.index[i], self.node, self.version, i)
1908 1912 fp.write(e)
1909 1913
1910 1914 # the temp file replace the real index when we exit the context
1911 1915 # manager
1912 1916
1913 1917 tr.replace(self.indexfile, trindex * self._io.size)
1914 1918 self._chunkclear()
1915 1919
1916 1920 def _nodeduplicatecallback(self, transaction, node):
1917 1921 """called when trying to add a node already stored.
1918 1922 """
1919 1923
1920 1924 def addrevision(
1921 1925 self,
1922 1926 text,
1923 1927 transaction,
1924 1928 link,
1925 1929 p1,
1926 1930 p2,
1927 1931 cachedelta=None,
1928 1932 node=None,
1929 1933 flags=REVIDX_DEFAULT_FLAGS,
1930 1934 deltacomputer=None,
1931 1935 sidedata=None,
1932 1936 ):
1933 1937 """add a revision to the log
1934 1938
1935 1939 text - the revision data to add
1936 1940 transaction - the transaction object used for rollback
1937 1941 link - the linkrev data to add
1938 1942 p1, p2 - the parent nodeids of the revision
1939 1943 cachedelta - an optional precomputed delta
1940 1944 node - nodeid of revision; typically node is not specified, and it is
1941 1945 computed by default as hash(text, p1, p2), however subclasses might
1942 1946 use different hashing method (and override checkhash() in such case)
1943 1947 flags - the known flags to set on the revision
1944 1948 deltacomputer - an optional deltacomputer instance shared between
1945 1949 multiple calls
1946 1950 """
1947 1951 if link == nullrev:
1948 1952 raise error.RevlogError(
1949 1953 _(b"attempted to add linkrev -1 to %s") % self.indexfile
1950 1954 )
1951 1955
1952 1956 if sidedata is None:
1953 1957 sidedata = {}
1954 1958 flags = flags & ~REVIDX_SIDEDATA
1955 1959 elif not self.hassidedata:
1956 1960 raise error.ProgrammingError(
1957 1961 _(b"trying to add sidedata to a revlog who don't support them")
1958 1962 )
1959 1963 else:
1960 1964 flags |= REVIDX_SIDEDATA
1961 1965
1962 1966 if flags:
1963 1967 node = node or self.hash(text, p1, p2)
1964 1968
1965 1969 rawtext, validatehash = flagutil.processflagswrite(
1966 1970 self, text, flags, sidedata=sidedata
1967 1971 )
1968 1972
1969 1973 # If the flag processor modifies the revision data, ignore any provided
1970 1974 # cachedelta.
1971 1975 if rawtext != text:
1972 1976 cachedelta = None
1973 1977
1974 1978 if len(rawtext) > _maxentrysize:
1975 1979 raise error.RevlogError(
1976 1980 _(
1977 1981 b"%s: size of %d bytes exceeds maximum revlog storage of 2GiB"
1978 1982 )
1979 1983 % (self.indexfile, len(rawtext))
1980 1984 )
1981 1985
1982 1986 node = node or self.hash(rawtext, p1, p2)
1983 1987 if node in self.nodemap:
1984 1988 return node
1985 1989
1986 1990 if validatehash:
1987 1991 self.checkhash(rawtext, node, p1=p1, p2=p2)
1988 1992
1989 1993 return self.addrawrevision(
1990 1994 rawtext,
1991 1995 transaction,
1992 1996 link,
1993 1997 p1,
1994 1998 p2,
1995 1999 node,
1996 2000 flags,
1997 2001 cachedelta=cachedelta,
1998 2002 deltacomputer=deltacomputer,
1999 2003 )
2000 2004
2001 2005 def addrawrevision(
2002 2006 self,
2003 2007 rawtext,
2004 2008 transaction,
2005 2009 link,
2006 2010 p1,
2007 2011 p2,
2008 2012 node,
2009 2013 flags,
2010 2014 cachedelta=None,
2011 2015 deltacomputer=None,
2012 2016 ):
2013 2017 """add a raw revision with known flags, node and parents
2014 2018 useful when reusing a revision not stored in this revlog (ex: received
2015 2019 over wire, or read from an external bundle).
2016 2020 """
2017 2021 dfh = None
2018 2022 if not self._inline:
2019 2023 dfh = self._datafp(b"a+")
2020 2024 ifh = self._indexfp(b"a+")
2021 2025 try:
2022 2026 return self._addrevision(
2023 2027 node,
2024 2028 rawtext,
2025 2029 transaction,
2026 2030 link,
2027 2031 p1,
2028 2032 p2,
2029 2033 flags,
2030 2034 cachedelta,
2031 2035 ifh,
2032 2036 dfh,
2033 2037 deltacomputer=deltacomputer,
2034 2038 )
2035 2039 finally:
2036 2040 if dfh:
2037 2041 dfh.close()
2038 2042 ifh.close()
2039 2043
2040 2044 def compress(self, data):
2041 2045 """Generate a possibly-compressed representation of data."""
2042 2046 if not data:
2043 2047 return b'', data
2044 2048
2045 2049 compressed = self._compressor.compress(data)
2046 2050
2047 2051 if compressed:
2048 2052 # The revlog compressor added the header in the returned data.
2049 2053 return b'', compressed
2050 2054
2051 2055 if data[0:1] == b'\0':
2052 2056 return b'', data
2053 2057 return b'u', data
2054 2058
2055 2059 def decompress(self, data):
2056 2060 """Decompress a revlog chunk.
2057 2061
2058 2062 The chunk is expected to begin with a header identifying the
2059 2063 format type so it can be routed to an appropriate decompressor.
2060 2064 """
2061 2065 if not data:
2062 2066 return data
2063 2067
2064 2068 # Revlogs are read much more frequently than they are written and many
2065 2069 # chunks only take microseconds to decompress, so performance is
2066 2070 # important here.
2067 2071 #
2068 2072 # We can make a few assumptions about revlogs:
2069 2073 #
2070 2074 # 1) the majority of chunks will be compressed (as opposed to inline
2071 2075 # raw data).
2072 2076 # 2) decompressing *any* data will likely by at least 10x slower than
2073 2077 # returning raw inline data.
2074 2078 # 3) we want to prioritize common and officially supported compression
2075 2079 # engines
2076 2080 #
2077 2081 # It follows that we want to optimize for "decompress compressed data
2078 2082 # when encoded with common and officially supported compression engines"
2079 2083 # case over "raw data" and "data encoded by less common or non-official
2080 2084 # compression engines." That is why we have the inline lookup first
2081 2085 # followed by the compengines lookup.
2082 2086 #
2083 2087 # According to `hg perfrevlogchunks`, this is ~0.5% faster for zlib
2084 2088 # compressed chunks. And this matters for changelog and manifest reads.
2085 2089 t = data[0:1]
2086 2090
2087 2091 if t == b'x':
2088 2092 try:
2089 2093 return _zlibdecompress(data)
2090 2094 except zlib.error as e:
2091 2095 raise error.RevlogError(
2092 2096 _(b'revlog decompress error: %s')
2093 2097 % stringutil.forcebytestr(e)
2094 2098 )
2095 2099 # '\0' is more common than 'u' so it goes first.
2096 2100 elif t == b'\0':
2097 2101 return data
2098 2102 elif t == b'u':
2099 2103 return util.buffer(data, 1)
2100 2104
2101 2105 try:
2102 2106 compressor = self._decompressors[t]
2103 2107 except KeyError:
2104 2108 try:
2105 2109 engine = util.compengines.forrevlogheader(t)
2106 2110 compressor = engine.revlogcompressor(self._compengineopts)
2107 2111 self._decompressors[t] = compressor
2108 2112 except KeyError:
2109 2113 raise error.RevlogError(_(b'unknown compression type %r') % t)
2110 2114
2111 2115 return compressor.decompress(data)
2112 2116
2113 2117 def _addrevision(
2114 2118 self,
2115 2119 node,
2116 2120 rawtext,
2117 2121 transaction,
2118 2122 link,
2119 2123 p1,
2120 2124 p2,
2121 2125 flags,
2122 2126 cachedelta,
2123 2127 ifh,
2124 2128 dfh,
2125 2129 alwayscache=False,
2126 2130 deltacomputer=None,
2127 2131 ):
2128 2132 """internal function to add revisions to the log
2129 2133
2130 2134 see addrevision for argument descriptions.
2131 2135
2132 2136 note: "addrevision" takes non-raw text, "_addrevision" takes raw text.
2133 2137
2134 2138 if "deltacomputer" is not provided or None, a defaultdeltacomputer will
2135 2139 be used.
2136 2140
2137 2141 invariants:
2138 2142 - rawtext is optional (can be None); if not set, cachedelta must be set.
2139 2143 if both are set, they must correspond to each other.
2140 2144 """
2141 2145 if node == nullid:
2142 2146 raise error.RevlogError(
2143 2147 _(b"%s: attempt to add null revision") % self.indexfile
2144 2148 )
2145 2149 if node == wdirid or node in wdirfilenodeids:
2146 2150 raise error.RevlogError(
2147 2151 _(b"%s: attempt to add wdir revision") % self.indexfile
2148 2152 )
2149 2153
2150 2154 if self._inline:
2151 2155 fh = ifh
2152 2156 else:
2153 2157 fh = dfh
2154 2158
2155 2159 btext = [rawtext]
2156 2160
2157 2161 curr = len(self)
2158 2162 prev = curr - 1
2159 2163 offset = self.end(prev)
2160 2164 p1r, p2r = self.rev(p1), self.rev(p2)
2161 2165
2162 2166 # full versions are inserted when the needed deltas
2163 2167 # become comparable to the uncompressed text
2164 2168 if rawtext is None:
2165 2169 # need rawtext size, before changed by flag processors, which is
2166 2170 # the non-raw size. use revlog explicitly to avoid filelog's extra
2167 2171 # logic that might remove metadata size.
2168 2172 textlen = mdiff.patchedsize(
2169 2173 revlog.size(self, cachedelta[0]), cachedelta[1]
2170 2174 )
2171 2175 else:
2172 2176 textlen = len(rawtext)
2173 2177
2174 2178 if deltacomputer is None:
2175 2179 deltacomputer = deltautil.deltacomputer(self)
2176 2180
2177 2181 revinfo = _revisioninfo(node, p1, p2, btext, textlen, cachedelta, flags)
2178 2182
2179 2183 deltainfo = deltacomputer.finddeltainfo(revinfo, fh)
2180 2184
2181 2185 e = (
2182 2186 offset_type(offset, flags),
2183 2187 deltainfo.deltalen,
2184 2188 textlen,
2185 2189 deltainfo.base,
2186 2190 link,
2187 2191 p1r,
2188 2192 p2r,
2189 2193 node,
2190 2194 )
2191 2195 self.index.append(e)
2192 self.nodemap[node] = curr
2193 2196
2194 2197 # Reset the pure node cache start lookup offset to account for new
2195 2198 # revision.
2196 2199 if self._nodepos is not None:
2197 2200 self._nodepos = curr
2198 2201
2199 2202 entry = self._io.packentry(e, self.node, self.version, curr)
2200 2203 self._writeentry(
2201 2204 transaction, ifh, dfh, entry, deltainfo.data, link, offset
2202 2205 )
2203 2206
2204 2207 rawtext = btext[0]
2205 2208
2206 2209 if alwayscache and rawtext is None:
2207 2210 rawtext = deltacomputer.buildtext(revinfo, fh)
2208 2211
2209 2212 if type(rawtext) == bytes: # only accept immutable objects
2210 2213 self._revisioncache = (node, curr, rawtext)
2211 2214 self._chainbasecache[curr] = deltainfo.chainbase
2212 2215 return node
2213 2216
2214 2217 def _writeentry(self, transaction, ifh, dfh, entry, data, link, offset):
2215 2218 # Files opened in a+ mode have inconsistent behavior on various
2216 2219 # platforms. Windows requires that a file positioning call be made
2217 2220 # when the file handle transitions between reads and writes. See
2218 2221 # 3686fa2b8eee and the mixedfilemodewrapper in windows.py. On other
2219 2222 # platforms, Python or the platform itself can be buggy. Some versions
2220 2223 # of Solaris have been observed to not append at the end of the file
2221 2224 # if the file was seeked to before the end. See issue4943 for more.
2222 2225 #
2223 2226 # We work around this issue by inserting a seek() before writing.
2224 2227 # Note: This is likely not necessary on Python 3. However, because
2225 2228 # the file handle is reused for reads and may be seeked there, we need
2226 2229 # to be careful before changing this.
2227 2230 ifh.seek(0, os.SEEK_END)
2228 2231 if dfh:
2229 2232 dfh.seek(0, os.SEEK_END)
2230 2233
2231 2234 curr = len(self) - 1
2232 2235 if not self._inline:
2233 2236 transaction.add(self.datafile, offset)
2234 2237 transaction.add(self.indexfile, curr * len(entry))
2235 2238 if data[0]:
2236 2239 dfh.write(data[0])
2237 2240 dfh.write(data[1])
2238 2241 ifh.write(entry)
2239 2242 else:
2240 2243 offset += curr * self._io.size
2241 2244 transaction.add(self.indexfile, offset, curr)
2242 2245 ifh.write(entry)
2243 2246 ifh.write(data[0])
2244 2247 ifh.write(data[1])
2245 2248 self._enforceinlinesize(transaction, ifh)
2246 2249
2247 2250 def addgroup(self, deltas, linkmapper, transaction, addrevisioncb=None):
2248 2251 """
2249 2252 add a delta group
2250 2253
2251 2254 given a set of deltas, add them to the revision log. the
2252 2255 first delta is against its parent, which should be in our
2253 2256 log, the rest are against the previous delta.
2254 2257
2255 2258 If ``addrevisioncb`` is defined, it will be called with arguments of
2256 2259 this revlog and the node that was added.
2257 2260 """
2258 2261
2259 2262 if self._writinghandles:
2260 2263 raise error.ProgrammingError(b'cannot nest addgroup() calls')
2261 2264
2262 2265 nodes = []
2263 2266
2264 2267 r = len(self)
2265 2268 end = 0
2266 2269 if r:
2267 2270 end = self.end(r - 1)
2268 2271 ifh = self._indexfp(b"a+")
2269 2272 isize = r * self._io.size
2270 2273 if self._inline:
2271 2274 transaction.add(self.indexfile, end + isize, r)
2272 2275 dfh = None
2273 2276 else:
2274 2277 transaction.add(self.indexfile, isize, r)
2275 2278 transaction.add(self.datafile, end)
2276 2279 dfh = self._datafp(b"a+")
2277 2280
2278 2281 def flush():
2279 2282 if dfh:
2280 2283 dfh.flush()
2281 2284 ifh.flush()
2282 2285
2283 2286 self._writinghandles = (ifh, dfh)
2284 2287
2285 2288 try:
2286 2289 deltacomputer = deltautil.deltacomputer(self)
2287 2290 # loop through our set of deltas
2288 2291 for data in deltas:
2289 2292 node, p1, p2, linknode, deltabase, delta, flags = data
2290 2293 link = linkmapper(linknode)
2291 2294 flags = flags or REVIDX_DEFAULT_FLAGS
2292 2295
2293 2296 nodes.append(node)
2294 2297
2295 2298 if node in self.nodemap:
2296 2299 self._nodeduplicatecallback(transaction, node)
2297 2300 # this can happen if two branches make the same change
2298 2301 continue
2299 2302
2300 2303 for p in (p1, p2):
2301 2304 if p not in self.nodemap:
2302 2305 raise error.LookupError(
2303 2306 p, self.indexfile, _(b'unknown parent')
2304 2307 )
2305 2308
2306 2309 if deltabase not in self.nodemap:
2307 2310 raise error.LookupError(
2308 2311 deltabase, self.indexfile, _(b'unknown delta base')
2309 2312 )
2310 2313
2311 2314 baserev = self.rev(deltabase)
2312 2315
2313 2316 if baserev != nullrev and self.iscensored(baserev):
2314 2317 # if base is censored, delta must be full replacement in a
2315 2318 # single patch operation
2316 2319 hlen = struct.calcsize(b">lll")
2317 2320 oldlen = self.rawsize(baserev)
2318 2321 newlen = len(delta) - hlen
2319 2322 if delta[:hlen] != mdiff.replacediffheader(oldlen, newlen):
2320 2323 raise error.CensoredBaseError(
2321 2324 self.indexfile, self.node(baserev)
2322 2325 )
2323 2326
2324 2327 if not flags and self._peek_iscensored(baserev, delta, flush):
2325 2328 flags |= REVIDX_ISCENSORED
2326 2329
2327 2330 # We assume consumers of addrevisioncb will want to retrieve
2328 2331 # the added revision, which will require a call to
2329 2332 # revision(). revision() will fast path if there is a cache
2330 2333 # hit. So, we tell _addrevision() to always cache in this case.
2331 2334 # We're only using addgroup() in the context of changegroup
2332 2335 # generation so the revision data can always be handled as raw
2333 2336 # by the flagprocessor.
2334 2337 self._addrevision(
2335 2338 node,
2336 2339 None,
2337 2340 transaction,
2338 2341 link,
2339 2342 p1,
2340 2343 p2,
2341 2344 flags,
2342 2345 (baserev, delta),
2343 2346 ifh,
2344 2347 dfh,
2345 2348 alwayscache=bool(addrevisioncb),
2346 2349 deltacomputer=deltacomputer,
2347 2350 )
2348 2351
2349 2352 if addrevisioncb:
2350 2353 addrevisioncb(self, node)
2351 2354
2352 2355 if not dfh and not self._inline:
2353 2356 # addrevision switched from inline to conventional
2354 2357 # reopen the index
2355 2358 ifh.close()
2356 2359 dfh = self._datafp(b"a+")
2357 2360 ifh = self._indexfp(b"a+")
2358 2361 self._writinghandles = (ifh, dfh)
2359 2362 finally:
2360 2363 self._writinghandles = None
2361 2364
2362 2365 if dfh:
2363 2366 dfh.close()
2364 2367 ifh.close()
2365 2368
2366 2369 return nodes
2367 2370
2368 2371 def iscensored(self, rev):
2369 2372 """Check if a file revision is censored."""
2370 2373 if not self._censorable:
2371 2374 return False
2372 2375
2373 2376 return self.flags(rev) & REVIDX_ISCENSORED
2374 2377
2375 2378 def _peek_iscensored(self, baserev, delta, flush):
2376 2379 """Quickly check if a delta produces a censored revision."""
2377 2380 if not self._censorable:
2378 2381 return False
2379 2382
2380 2383 return storageutil.deltaiscensored(delta, baserev, self.rawsize)
2381 2384
2382 2385 def getstrippoint(self, minlink):
2383 2386 """find the minimum rev that must be stripped to strip the linkrev
2384 2387
2385 2388 Returns a tuple containing the minimum rev and a set of all revs that
2386 2389 have linkrevs that will be broken by this strip.
2387 2390 """
2388 2391 return storageutil.resolvestripinfo(
2389 2392 minlink,
2390 2393 len(self) - 1,
2391 2394 self.headrevs(),
2392 2395 self.linkrev,
2393 2396 self.parentrevs,
2394 2397 )
2395 2398
2396 2399 def strip(self, minlink, transaction):
2397 2400 """truncate the revlog on the first revision with a linkrev >= minlink
2398 2401
2399 2402 This function is called when we're stripping revision minlink and
2400 2403 its descendants from the repository.
2401 2404
2402 2405 We have to remove all revisions with linkrev >= minlink, because
2403 2406 the equivalent changelog revisions will be renumbered after the
2404 2407 strip.
2405 2408
2406 2409 So we truncate the revlog on the first of these revisions, and
2407 2410 trust that the caller has saved the revisions that shouldn't be
2408 2411 removed and that it'll re-add them after this truncation.
2409 2412 """
2410 2413 if len(self) == 0:
2411 2414 return
2412 2415
2413 2416 rev, _ = self.getstrippoint(minlink)
2414 2417 if rev == len(self):
2415 2418 return
2416 2419
2417 2420 # first truncate the files on disk
2418 2421 end = self.start(rev)
2419 2422 if not self._inline:
2420 2423 transaction.add(self.datafile, end)
2421 2424 end = rev * self._io.size
2422 2425 else:
2423 2426 end += rev * self._io.size
2424 2427
2425 2428 transaction.add(self.indexfile, end)
2426 2429
2427 2430 # then reset internal state in memory to forget those revisions
2428 2431 self._revisioncache = None
2429 2432 self._chaininfocache = {}
2430 2433 self._chunkclear()
2431 2434 for x in pycompat.xrange(rev, len(self)):
2432 2435 del self.nodemap[self.node(x)]
2433 2436
2434 2437 del self.index[rev:-1]
2435 2438 self._nodepos = None
2436 2439
2437 2440 def checksize(self):
2438 2441 """Check size of index and data files
2439 2442
2440 2443 return a (dd, di) tuple.
2441 2444 - dd: extra bytes for the "data" file
2442 2445 - di: extra bytes for the "index" file
2443 2446
2444 2447 A healthy revlog will return (0, 0).
2445 2448 """
2446 2449 expected = 0
2447 2450 if len(self):
2448 2451 expected = max(0, self.end(len(self) - 1))
2449 2452
2450 2453 try:
2451 2454 with self._datafp() as f:
2452 2455 f.seek(0, io.SEEK_END)
2453 2456 actual = f.tell()
2454 2457 dd = actual - expected
2455 2458 except IOError as inst:
2456 2459 if inst.errno != errno.ENOENT:
2457 2460 raise
2458 2461 dd = 0
2459 2462
2460 2463 try:
2461 2464 f = self.opener(self.indexfile)
2462 2465 f.seek(0, io.SEEK_END)
2463 2466 actual = f.tell()
2464 2467 f.close()
2465 2468 s = self._io.size
2466 2469 i = max(0, actual // s)
2467 2470 di = actual - (i * s)
2468 2471 if self._inline:
2469 2472 databytes = 0
2470 2473 for r in self:
2471 2474 databytes += max(0, self.length(r))
2472 2475 dd = 0
2473 2476 di = actual - len(self) * s - databytes
2474 2477 except IOError as inst:
2475 2478 if inst.errno != errno.ENOENT:
2476 2479 raise
2477 2480 di = 0
2478 2481
2479 2482 return (dd, di)
2480 2483
2481 2484 def files(self):
2482 2485 res = [self.indexfile]
2483 2486 if not self._inline:
2484 2487 res.append(self.datafile)
2485 2488 return res
2486 2489
2487 2490 def emitrevisions(
2488 2491 self,
2489 2492 nodes,
2490 2493 nodesorder=None,
2491 2494 revisiondata=False,
2492 2495 assumehaveparentrevisions=False,
2493 2496 deltamode=repository.CG_DELTAMODE_STD,
2494 2497 ):
2495 2498 if nodesorder not in (b'nodes', b'storage', b'linear', None):
2496 2499 raise error.ProgrammingError(
2497 2500 b'unhandled value for nodesorder: %s' % nodesorder
2498 2501 )
2499 2502
2500 2503 if nodesorder is None and not self._generaldelta:
2501 2504 nodesorder = b'storage'
2502 2505
2503 2506 if (
2504 2507 not self._storedeltachains
2505 2508 and deltamode != repository.CG_DELTAMODE_PREV
2506 2509 ):
2507 2510 deltamode = repository.CG_DELTAMODE_FULL
2508 2511
2509 2512 return storageutil.emitrevisions(
2510 2513 self,
2511 2514 nodes,
2512 2515 nodesorder,
2513 2516 revlogrevisiondelta,
2514 2517 deltaparentfn=self.deltaparent,
2515 2518 candeltafn=self.candelta,
2516 2519 rawsizefn=self.rawsize,
2517 2520 revdifffn=self.revdiff,
2518 2521 flagsfn=self.flags,
2519 2522 deltamode=deltamode,
2520 2523 revisiondata=revisiondata,
2521 2524 assumehaveparentrevisions=assumehaveparentrevisions,
2522 2525 )
2523 2526
2524 2527 DELTAREUSEALWAYS = b'always'
2525 2528 DELTAREUSESAMEREVS = b'samerevs'
2526 2529 DELTAREUSENEVER = b'never'
2527 2530
2528 2531 DELTAREUSEFULLADD = b'fulladd'
2529 2532
2530 2533 DELTAREUSEALL = {b'always', b'samerevs', b'never', b'fulladd'}
2531 2534
2532 2535 def clone(
2533 2536 self,
2534 2537 tr,
2535 2538 destrevlog,
2536 2539 addrevisioncb=None,
2537 2540 deltareuse=DELTAREUSESAMEREVS,
2538 2541 forcedeltabothparents=None,
2539 2542 sidedatacompanion=None,
2540 2543 ):
2541 2544 """Copy this revlog to another, possibly with format changes.
2542 2545
2543 2546 The destination revlog will contain the same revisions and nodes.
2544 2547 However, it may not be bit-for-bit identical due to e.g. delta encoding
2545 2548 differences.
2546 2549
2547 2550 The ``deltareuse`` argument control how deltas from the existing revlog
2548 2551 are preserved in the destination revlog. The argument can have the
2549 2552 following values:
2550 2553
2551 2554 DELTAREUSEALWAYS
2552 2555 Deltas will always be reused (if possible), even if the destination
2553 2556 revlog would not select the same revisions for the delta. This is the
2554 2557 fastest mode of operation.
2555 2558 DELTAREUSESAMEREVS
2556 2559 Deltas will be reused if the destination revlog would pick the same
2557 2560 revisions for the delta. This mode strikes a balance between speed
2558 2561 and optimization.
2559 2562 DELTAREUSENEVER
2560 2563 Deltas will never be reused. This is the slowest mode of execution.
2561 2564 This mode can be used to recompute deltas (e.g. if the diff/delta
2562 2565 algorithm changes).
2563 2566 DELTAREUSEFULLADD
2564 2567 Revision will be re-added as if their were new content. This is
2565 2568 slower than DELTAREUSEALWAYS but allow more mechanism to kicks in.
2566 2569 eg: large file detection and handling.
2567 2570
2568 2571 Delta computation can be slow, so the choice of delta reuse policy can
2569 2572 significantly affect run time.
2570 2573
2571 2574 The default policy (``DELTAREUSESAMEREVS``) strikes a balance between
2572 2575 two extremes. Deltas will be reused if they are appropriate. But if the
2573 2576 delta could choose a better revision, it will do so. This means if you
2574 2577 are converting a non-generaldelta revlog to a generaldelta revlog,
2575 2578 deltas will be recomputed if the delta's parent isn't a parent of the
2576 2579 revision.
2577 2580
2578 2581 In addition to the delta policy, the ``forcedeltabothparents``
2579 2582 argument controls whether to force compute deltas against both parents
2580 2583 for merges. By default, the current default is used.
2581 2584
2582 2585 If not None, the `sidedatacompanion` is callable that accept two
2583 2586 arguments:
2584 2587
2585 2588 (srcrevlog, rev)
2586 2589
2587 2590 and return a triplet that control changes to sidedata content from the
2588 2591 old revision to the new clone result:
2589 2592
2590 2593 (dropall, filterout, update)
2591 2594
2592 2595 * if `dropall` is True, all sidedata should be dropped
2593 2596 * `filterout` is a set of sidedata keys that should be dropped
2594 2597 * `update` is a mapping of additionnal/new key -> value
2595 2598 """
2596 2599 if deltareuse not in self.DELTAREUSEALL:
2597 2600 raise ValueError(
2598 2601 _(b'value for deltareuse invalid: %s') % deltareuse
2599 2602 )
2600 2603
2601 2604 if len(destrevlog):
2602 2605 raise ValueError(_(b'destination revlog is not empty'))
2603 2606
2604 2607 if getattr(self, 'filteredrevs', None):
2605 2608 raise ValueError(_(b'source revlog has filtered revisions'))
2606 2609 if getattr(destrevlog, 'filteredrevs', None):
2607 2610 raise ValueError(_(b'destination revlog has filtered revisions'))
2608 2611
2609 2612 # lazydelta and lazydeltabase controls whether to reuse a cached delta,
2610 2613 # if possible.
2611 2614 oldlazydelta = destrevlog._lazydelta
2612 2615 oldlazydeltabase = destrevlog._lazydeltabase
2613 2616 oldamd = destrevlog._deltabothparents
2614 2617
2615 2618 try:
2616 2619 if deltareuse == self.DELTAREUSEALWAYS:
2617 2620 destrevlog._lazydeltabase = True
2618 2621 destrevlog._lazydelta = True
2619 2622 elif deltareuse == self.DELTAREUSESAMEREVS:
2620 2623 destrevlog._lazydeltabase = False
2621 2624 destrevlog._lazydelta = True
2622 2625 elif deltareuse == self.DELTAREUSENEVER:
2623 2626 destrevlog._lazydeltabase = False
2624 2627 destrevlog._lazydelta = False
2625 2628
2626 2629 destrevlog._deltabothparents = forcedeltabothparents or oldamd
2627 2630
2628 2631 self._clone(
2629 2632 tr,
2630 2633 destrevlog,
2631 2634 addrevisioncb,
2632 2635 deltareuse,
2633 2636 forcedeltabothparents,
2634 2637 sidedatacompanion,
2635 2638 )
2636 2639
2637 2640 finally:
2638 2641 destrevlog._lazydelta = oldlazydelta
2639 2642 destrevlog._lazydeltabase = oldlazydeltabase
2640 2643 destrevlog._deltabothparents = oldamd
2641 2644
2642 2645 def _clone(
2643 2646 self,
2644 2647 tr,
2645 2648 destrevlog,
2646 2649 addrevisioncb,
2647 2650 deltareuse,
2648 2651 forcedeltabothparents,
2649 2652 sidedatacompanion,
2650 2653 ):
2651 2654 """perform the core duty of `revlog.clone` after parameter processing"""
2652 2655 deltacomputer = deltautil.deltacomputer(destrevlog)
2653 2656 index = self.index
2654 2657 for rev in self:
2655 2658 entry = index[rev]
2656 2659
2657 2660 # Some classes override linkrev to take filtered revs into
2658 2661 # account. Use raw entry from index.
2659 2662 flags = entry[0] & 0xFFFF
2660 2663 linkrev = entry[4]
2661 2664 p1 = index[entry[5]][7]
2662 2665 p2 = index[entry[6]][7]
2663 2666 node = entry[7]
2664 2667
2665 2668 sidedataactions = (False, [], {})
2666 2669 if sidedatacompanion is not None:
2667 2670 sidedataactions = sidedatacompanion(self, rev)
2668 2671
2669 2672 # (Possibly) reuse the delta from the revlog if allowed and
2670 2673 # the revlog chunk is a delta.
2671 2674 cachedelta = None
2672 2675 rawtext = None
2673 2676 if any(sidedataactions) or deltareuse == self.DELTAREUSEFULLADD:
2674 2677 dropall, filterout, update = sidedataactions
2675 2678 text, sidedata = self._revisiondata(rev)
2676 2679 if dropall:
2677 2680 sidedata = {}
2678 2681 for key in filterout:
2679 2682 sidedata.pop(key, None)
2680 2683 sidedata.update(update)
2681 2684 if not sidedata:
2682 2685 sidedata = None
2683 2686 destrevlog.addrevision(
2684 2687 text,
2685 2688 tr,
2686 2689 linkrev,
2687 2690 p1,
2688 2691 p2,
2689 2692 cachedelta=cachedelta,
2690 2693 node=node,
2691 2694 flags=flags,
2692 2695 deltacomputer=deltacomputer,
2693 2696 sidedata=sidedata,
2694 2697 )
2695 2698 else:
2696 2699 if destrevlog._lazydelta:
2697 2700 dp = self.deltaparent(rev)
2698 2701 if dp != nullrev:
2699 2702 cachedelta = (dp, bytes(self._chunk(rev)))
2700 2703
2701 2704 if not cachedelta:
2702 2705 rawtext = self.rawdata(rev)
2703 2706
2704 2707 ifh = destrevlog.opener(
2705 2708 destrevlog.indexfile, b'a+', checkambig=False
2706 2709 )
2707 2710 dfh = None
2708 2711 if not destrevlog._inline:
2709 2712 dfh = destrevlog.opener(destrevlog.datafile, b'a+')
2710 2713 try:
2711 2714 destrevlog._addrevision(
2712 2715 node,
2713 2716 rawtext,
2714 2717 tr,
2715 2718 linkrev,
2716 2719 p1,
2717 2720 p2,
2718 2721 flags,
2719 2722 cachedelta,
2720 2723 ifh,
2721 2724 dfh,
2722 2725 deltacomputer=deltacomputer,
2723 2726 )
2724 2727 finally:
2725 2728 if dfh:
2726 2729 dfh.close()
2727 2730 ifh.close()
2728 2731
2729 2732 if addrevisioncb:
2730 2733 addrevisioncb(self, rev, node)
2731 2734
2732 2735 def censorrevision(self, tr, censornode, tombstone=b''):
2733 2736 if (self.version & 0xFFFF) == REVLOGV0:
2734 2737 raise error.RevlogError(
2735 2738 _(b'cannot censor with version %d revlogs') % self.version
2736 2739 )
2737 2740
2738 2741 censorrev = self.rev(censornode)
2739 2742 tombstone = storageutil.packmeta({b'censored': tombstone}, b'')
2740 2743
2741 2744 if len(tombstone) > self.rawsize(censorrev):
2742 2745 raise error.Abort(
2743 2746 _(b'censor tombstone must be no longer than censored data')
2744 2747 )
2745 2748
2746 2749 # Rewriting the revlog in place is hard. Our strategy for censoring is
2747 2750 # to create a new revlog, copy all revisions to it, then replace the
2748 2751 # revlogs on transaction close.
2749 2752
2750 2753 newindexfile = self.indexfile + b'.tmpcensored'
2751 2754 newdatafile = self.datafile + b'.tmpcensored'
2752 2755
2753 2756 # This is a bit dangerous. We could easily have a mismatch of state.
2754 2757 newrl = revlog(self.opener, newindexfile, newdatafile, censorable=True)
2755 2758 newrl.version = self.version
2756 2759 newrl._generaldelta = self._generaldelta
2757 2760 newrl._io = self._io
2758 2761
2759 2762 for rev in self.revs():
2760 2763 node = self.node(rev)
2761 2764 p1, p2 = self.parents(node)
2762 2765
2763 2766 if rev == censorrev:
2764 2767 newrl.addrawrevision(
2765 2768 tombstone,
2766 2769 tr,
2767 2770 self.linkrev(censorrev),
2768 2771 p1,
2769 2772 p2,
2770 2773 censornode,
2771 2774 REVIDX_ISCENSORED,
2772 2775 )
2773 2776
2774 2777 if newrl.deltaparent(rev) != nullrev:
2775 2778 raise error.Abort(
2776 2779 _(
2777 2780 b'censored revision stored as delta; '
2778 2781 b'cannot censor'
2779 2782 ),
2780 2783 hint=_(
2781 2784 b'censoring of revlogs is not '
2782 2785 b'fully implemented; please report '
2783 2786 b'this bug'
2784 2787 ),
2785 2788 )
2786 2789 continue
2787 2790
2788 2791 if self.iscensored(rev):
2789 2792 if self.deltaparent(rev) != nullrev:
2790 2793 raise error.Abort(
2791 2794 _(
2792 2795 b'cannot censor due to censored '
2793 2796 b'revision having delta stored'
2794 2797 )
2795 2798 )
2796 2799 rawtext = self._chunk(rev)
2797 2800 else:
2798 2801 rawtext = self.rawdata(rev)
2799 2802
2800 2803 newrl.addrawrevision(
2801 2804 rawtext, tr, self.linkrev(rev), p1, p2, node, self.flags(rev)
2802 2805 )
2803 2806
2804 2807 tr.addbackup(self.indexfile, location=b'store')
2805 2808 if not self._inline:
2806 2809 tr.addbackup(self.datafile, location=b'store')
2807 2810
2808 2811 self.opener.rename(newrl.indexfile, self.indexfile)
2809 2812 if not self._inline:
2810 2813 self.opener.rename(newrl.datafile, self.datafile)
2811 2814
2812 2815 self.clearcaches()
2813 2816 self._loadindex()
2814 2817
2815 2818 def verifyintegrity(self, state):
2816 2819 """Verifies the integrity of the revlog.
2817 2820
2818 2821 Yields ``revlogproblem`` instances describing problems that are
2819 2822 found.
2820 2823 """
2821 2824 dd, di = self.checksize()
2822 2825 if dd:
2823 2826 yield revlogproblem(error=_(b'data length off by %d bytes') % dd)
2824 2827 if di:
2825 2828 yield revlogproblem(error=_(b'index contains %d extra bytes') % di)
2826 2829
2827 2830 version = self.version & 0xFFFF
2828 2831
2829 2832 # The verifier tells us what version revlog we should be.
2830 2833 if version != state[b'expectedversion']:
2831 2834 yield revlogproblem(
2832 2835 warning=_(b"warning: '%s' uses revlog format %d; expected %d")
2833 2836 % (self.indexfile, version, state[b'expectedversion'])
2834 2837 )
2835 2838
2836 2839 state[b'skipread'] = set()
2837 2840
2838 2841 for rev in self:
2839 2842 node = self.node(rev)
2840 2843
2841 2844 # Verify contents. 4 cases to care about:
2842 2845 #
2843 2846 # common: the most common case
2844 2847 # rename: with a rename
2845 2848 # meta: file content starts with b'\1\n', the metadata
2846 2849 # header defined in filelog.py, but without a rename
2847 2850 # ext: content stored externally
2848 2851 #
2849 2852 # More formally, their differences are shown below:
2850 2853 #
2851 2854 # | common | rename | meta | ext
2852 2855 # -------------------------------------------------------
2853 2856 # flags() | 0 | 0 | 0 | not 0
2854 2857 # renamed() | False | True | False | ?
2855 2858 # rawtext[0:2]=='\1\n'| False | True | True | ?
2856 2859 #
2857 2860 # "rawtext" means the raw text stored in revlog data, which
2858 2861 # could be retrieved by "rawdata(rev)". "text"
2859 2862 # mentioned below is "revision(rev)".
2860 2863 #
2861 2864 # There are 3 different lengths stored physically:
2862 2865 # 1. L1: rawsize, stored in revlog index
2863 2866 # 2. L2: len(rawtext), stored in revlog data
2864 2867 # 3. L3: len(text), stored in revlog data if flags==0, or
2865 2868 # possibly somewhere else if flags!=0
2866 2869 #
2867 2870 # L1 should be equal to L2. L3 could be different from them.
2868 2871 # "text" may or may not affect commit hash depending on flag
2869 2872 # processors (see flagutil.addflagprocessor).
2870 2873 #
2871 2874 # | common | rename | meta | ext
2872 2875 # -------------------------------------------------
2873 2876 # rawsize() | L1 | L1 | L1 | L1
2874 2877 # size() | L1 | L2-LM | L1(*) | L1 (?)
2875 2878 # len(rawtext) | L2 | L2 | L2 | L2
2876 2879 # len(text) | L2 | L2 | L2 | L3
2877 2880 # len(read()) | L2 | L2-LM | L2-LM | L3 (?)
2878 2881 #
2879 2882 # LM: length of metadata, depending on rawtext
2880 2883 # (*): not ideal, see comment in filelog.size
2881 2884 # (?): could be "- len(meta)" if the resolved content has
2882 2885 # rename metadata
2883 2886 #
2884 2887 # Checks needed to be done:
2885 2888 # 1. length check: L1 == L2, in all cases.
2886 2889 # 2. hash check: depending on flag processor, we may need to
2887 2890 # use either "text" (external), or "rawtext" (in revlog).
2888 2891
2889 2892 try:
2890 2893 skipflags = state.get(b'skipflags', 0)
2891 2894 if skipflags:
2892 2895 skipflags &= self.flags(rev)
2893 2896
2894 2897 if skipflags:
2895 2898 state[b'skipread'].add(node)
2896 2899 else:
2897 2900 # Side-effect: read content and verify hash.
2898 2901 self.revision(node)
2899 2902
2900 2903 l1 = self.rawsize(rev)
2901 2904 l2 = len(self.rawdata(node))
2902 2905
2903 2906 if l1 != l2:
2904 2907 yield revlogproblem(
2905 2908 error=_(b'unpacked size is %d, %d expected') % (l2, l1),
2906 2909 node=node,
2907 2910 )
2908 2911
2909 2912 except error.CensoredNodeError:
2910 2913 if state[b'erroroncensored']:
2911 2914 yield revlogproblem(
2912 2915 error=_(b'censored file data'), node=node
2913 2916 )
2914 2917 state[b'skipread'].add(node)
2915 2918 except Exception as e:
2916 2919 yield revlogproblem(
2917 2920 error=_(b'unpacking %s: %s')
2918 2921 % (short(node), stringutil.forcebytestr(e)),
2919 2922 node=node,
2920 2923 )
2921 2924 state[b'skipread'].add(node)
2922 2925
2923 2926 def storageinfo(
2924 2927 self,
2925 2928 exclusivefiles=False,
2926 2929 sharedfiles=False,
2927 2930 revisionscount=False,
2928 2931 trackedsize=False,
2929 2932 storedsize=False,
2930 2933 ):
2931 2934 d = {}
2932 2935
2933 2936 if exclusivefiles:
2934 2937 d[b'exclusivefiles'] = [(self.opener, self.indexfile)]
2935 2938 if not self._inline:
2936 2939 d[b'exclusivefiles'].append((self.opener, self.datafile))
2937 2940
2938 2941 if sharedfiles:
2939 2942 d[b'sharedfiles'] = []
2940 2943
2941 2944 if revisionscount:
2942 2945 d[b'revisionscount'] = len(self)
2943 2946
2944 2947 if trackedsize:
2945 2948 d[b'trackedsize'] = sum(map(self.rawsize, iter(self)))
2946 2949
2947 2950 if storedsize:
2948 2951 d[b'storedsize'] = sum(
2949 2952 self.opener.stat(path).st_size for path in self.files()
2950 2953 )
2951 2954
2952 2955 return d
General Comments 0
You need to be logged in to leave comments. Login now