##// END OF EJS Templates
changelog-delay: move the delay/divert logic inside the (inner) revlog...
marmoute -
r51999:d83d7885 default
parent child Browse files
Show More
@@ -1,579 +1,500 b''
1 1 # changelog.py - changelog class for mercurial
2 2 #
3 3 # Copyright 2005-2007 Olivia Mackall <olivia@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
9 9 from .i18n import _
10 10 from .node import (
11 11 bin,
12 12 hex,
13 13 )
14 14 from .thirdparty import attr
15 15
16 16 from . import (
17 17 encoding,
18 18 error,
19 19 metadata,
20 20 pycompat,
21 21 revlog,
22 22 )
23 23 from .utils import (
24 24 dateutil,
25 25 stringutil,
26 26 )
27 27 from .revlogutils import (
28 28 constants as revlog_constants,
29 29 flagutil,
30 randomaccessfile,
31 30 )
32 31
33 32 _defaultextra = {b'branch': b'default'}
34 33
35 34
36 35 def _string_escape(text):
37 36 """
38 37 >>> from .pycompat import bytechr as chr
39 38 >>> d = {b'nl': chr(10), b'bs': chr(92), b'cr': chr(13), b'nul': chr(0)}
40 39 >>> s = b"ab%(nl)scd%(bs)s%(bs)sn%(nul)s12ab%(cr)scd%(bs)s%(nl)s" % d
41 40 >>> s
42 41 'ab\\ncd\\\\\\\\n\\x0012ab\\rcd\\\\\\n'
43 42 >>> res = _string_escape(s)
44 43 >>> s == _string_unescape(res)
45 44 True
46 45 """
47 46 # subset of the string_escape codec
48 47 text = (
49 48 text.replace(b'\\', b'\\\\')
50 49 .replace(b'\n', b'\\n')
51 50 .replace(b'\r', b'\\r')
52 51 )
53 52 return text.replace(b'\0', b'\\0')
54 53
55 54
56 55 def _string_unescape(text):
57 56 if b'\\0' in text:
58 57 # fix up \0 without getting into trouble with \\0
59 58 text = text.replace(b'\\\\', b'\\\\\n')
60 59 text = text.replace(b'\\0', b'\0')
61 60 text = text.replace(b'\n', b'')
62 61 return stringutil.unescapestr(text)
63 62
64 63
65 64 def decodeextra(text):
66 65 """
67 66 >>> from .pycompat import bytechr as chr
68 67 >>> sorted(decodeextra(encodeextra({b'foo': b'bar', b'baz': chr(0) + b'2'})
69 68 ... ).items())
70 69 [('baz', '\\x002'), ('branch', 'default'), ('foo', 'bar')]
71 70 >>> sorted(decodeextra(encodeextra({b'foo': b'bar',
72 71 ... b'baz': chr(92) + chr(0) + b'2'})
73 72 ... ).items())
74 73 [('baz', '\\\\\\x002'), ('branch', 'default'), ('foo', 'bar')]
75 74 """
76 75 extra = _defaultextra.copy()
77 76 for l in text.split(b'\0'):
78 77 if l:
79 78 k, v = _string_unescape(l).split(b':', 1)
80 79 extra[k] = v
81 80 return extra
82 81
83 82
84 83 def encodeextra(d):
85 84 # keys must be sorted to produce a deterministic changelog entry
86 85 items = [_string_escape(b'%s:%s' % (k, d[k])) for k in sorted(d)]
87 86 return b"\0".join(items)
88 87
89 88
90 89 def stripdesc(desc):
91 90 """strip trailing whitespace and leading and trailing empty lines"""
92 91 return b'\n'.join([l.rstrip() for l in desc.splitlines()]).strip(b'\n')
93 92
94 93
95 class _divertopener:
96 def __init__(self, opener, target):
97 self._opener = opener
98 self._target = target
99
100 def __call__(self, name, mode=b'r', checkambig=False, **kwargs):
101 if name != self._target:
102 return self._opener(name, mode, **kwargs)
103 return self._opener(name + b".a", mode, **kwargs)
104
105 def __getattr__(self, attr):
106 return getattr(self._opener, attr)
107
108
109 class _delayopener:
110 """build an opener that stores chunks in 'buf' instead of 'target'"""
111
112 def __init__(self, opener, target, buf):
113 self._opener = opener
114 self._target = target
115 self._buf = buf
116
117 def __call__(self, name, mode=b'r', checkambig=False, **kwargs):
118 if name != self._target:
119 return self._opener(name, mode, **kwargs)
120 assert not kwargs
121 return randomaccessfile.appender(self._opener, name, mode, self._buf)
122
123 def __getattr__(self, attr):
124 return getattr(self._opener, attr)
125
126
127 94 @attr.s
128 95 class _changelogrevision:
129 96 # Extensions might modify _defaultextra, so let the constructor below pass
130 97 # it in
131 98 extra = attr.ib()
132 99 manifest = attr.ib()
133 100 user = attr.ib(default=b'')
134 101 date = attr.ib(default=(0, 0))
135 102 files = attr.ib(default=attr.Factory(list))
136 103 filesadded = attr.ib(default=None)
137 104 filesremoved = attr.ib(default=None)
138 105 p1copies = attr.ib(default=None)
139 106 p2copies = attr.ib(default=None)
140 107 description = attr.ib(default=b'')
141 108 branchinfo = attr.ib(default=(_defaultextra[b'branch'], False))
142 109
143 110
144 111 class changelogrevision:
145 112 """Holds results of a parsed changelog revision.
146 113
147 114 Changelog revisions consist of multiple pieces of data, including
148 115 the manifest node, user, and date. This object exposes a view into
149 116 the parsed object.
150 117 """
151 118
152 119 __slots__ = (
153 120 '_offsets',
154 121 '_text',
155 122 '_sidedata',
156 123 '_cpsd',
157 124 '_changes',
158 125 )
159 126
160 127 def __new__(cls, cl, text, sidedata, cpsd):
161 128 if not text:
162 129 return _changelogrevision(extra=_defaultextra, manifest=cl.nullid)
163 130
164 131 self = super(changelogrevision, cls).__new__(cls)
165 132 # We could return here and implement the following as an __init__.
166 133 # But doing it here is equivalent and saves an extra function call.
167 134
168 135 # format used:
169 136 # nodeid\n : manifest node in ascii
170 137 # user\n : user, no \n or \r allowed
171 138 # time tz extra\n : date (time is int or float, timezone is int)
172 139 # : extra is metadata, encoded and separated by '\0'
173 140 # : older versions ignore it
174 141 # files\n\n : files modified by the cset, no \n or \r allowed
175 142 # (.*) : comment (free text, ideally utf-8)
176 143 #
177 144 # changelog v0 doesn't use extra
178 145
179 146 nl1 = text.index(b'\n')
180 147 nl2 = text.index(b'\n', nl1 + 1)
181 148 nl3 = text.index(b'\n', nl2 + 1)
182 149
183 150 # The list of files may be empty. Which means nl3 is the first of the
184 151 # double newline that precedes the description.
185 152 if text[nl3 + 1 : nl3 + 2] == b'\n':
186 153 doublenl = nl3
187 154 else:
188 155 doublenl = text.index(b'\n\n', nl3 + 1)
189 156
190 157 self._offsets = (nl1, nl2, nl3, doublenl)
191 158 self._text = text
192 159 self._sidedata = sidedata
193 160 self._cpsd = cpsd
194 161 self._changes = None
195 162
196 163 return self
197 164
198 165 @property
199 166 def manifest(self):
200 167 return bin(self._text[0 : self._offsets[0]])
201 168
202 169 @property
203 170 def user(self):
204 171 off = self._offsets
205 172 return encoding.tolocal(self._text[off[0] + 1 : off[1]])
206 173
207 174 @property
208 175 def _rawdate(self):
209 176 off = self._offsets
210 177 dateextra = self._text[off[1] + 1 : off[2]]
211 178 return dateextra.split(b' ', 2)[0:2]
212 179
213 180 @property
214 181 def _rawextra(self):
215 182 off = self._offsets
216 183 dateextra = self._text[off[1] + 1 : off[2]]
217 184 fields = dateextra.split(b' ', 2)
218 185 if len(fields) != 3:
219 186 return None
220 187
221 188 return fields[2]
222 189
223 190 @property
224 191 def date(self):
225 192 raw = self._rawdate
226 193 time = float(raw[0])
227 194 # Various tools did silly things with the timezone.
228 195 try:
229 196 timezone = int(raw[1])
230 197 except ValueError:
231 198 timezone = 0
232 199
233 200 return time, timezone
234 201
235 202 @property
236 203 def extra(self):
237 204 raw = self._rawextra
238 205 if raw is None:
239 206 return _defaultextra
240 207
241 208 return decodeextra(raw)
242 209
243 210 @property
244 211 def changes(self):
245 212 if self._changes is not None:
246 213 return self._changes
247 214 if self._cpsd:
248 215 changes = metadata.decode_files_sidedata(self._sidedata)
249 216 else:
250 217 changes = metadata.ChangingFiles(
251 218 touched=self.files or (),
252 219 added=self.filesadded or (),
253 220 removed=self.filesremoved or (),
254 221 p1_copies=self.p1copies or {},
255 222 p2_copies=self.p2copies or {},
256 223 )
257 224 self._changes = changes
258 225 return changes
259 226
260 227 @property
261 228 def files(self):
262 229 if self._cpsd:
263 230 return sorted(self.changes.touched)
264 231 off = self._offsets
265 232 if off[2] == off[3]:
266 233 return []
267 234
268 235 return self._text[off[2] + 1 : off[3]].split(b'\n')
269 236
270 237 @property
271 238 def filesadded(self):
272 239 if self._cpsd:
273 240 return self.changes.added
274 241 else:
275 242 rawindices = self.extra.get(b'filesadded')
276 243 if rawindices is None:
277 244 return None
278 245 return metadata.decodefileindices(self.files, rawindices)
279 246
280 247 @property
281 248 def filesremoved(self):
282 249 if self._cpsd:
283 250 return self.changes.removed
284 251 else:
285 252 rawindices = self.extra.get(b'filesremoved')
286 253 if rawindices is None:
287 254 return None
288 255 return metadata.decodefileindices(self.files, rawindices)
289 256
290 257 @property
291 258 def p1copies(self):
292 259 if self._cpsd:
293 260 return self.changes.copied_from_p1
294 261 else:
295 262 rawcopies = self.extra.get(b'p1copies')
296 263 if rawcopies is None:
297 264 return None
298 265 return metadata.decodecopies(self.files, rawcopies)
299 266
300 267 @property
301 268 def p2copies(self):
302 269 if self._cpsd:
303 270 return self.changes.copied_from_p2
304 271 else:
305 272 rawcopies = self.extra.get(b'p2copies')
306 273 if rawcopies is None:
307 274 return None
308 275 return metadata.decodecopies(self.files, rawcopies)
309 276
310 277 @property
311 278 def description(self):
312 279 return encoding.tolocal(self._text[self._offsets[3] + 2 :])
313 280
314 281 @property
315 282 def branchinfo(self):
316 283 extra = self.extra
317 284 return encoding.tolocal(extra.get(b"branch")), b'close' in extra
318 285
319 286
320 287 class changelog(revlog.revlog):
321 288 def __init__(self, opener, trypending=False, concurrencychecker=None):
322 289 """Load a changelog revlog using an opener.
323 290
324 291 If ``trypending`` is true, we attempt to load the index from a
325 292 ``00changelog.i.a`` file instead of the default ``00changelog.i``.
326 293 The ``00changelog.i.a`` file contains index (and possibly inline
327 294 revision) data for a transaction that hasn't been finalized yet.
328 295 It exists in a separate file to facilitate readers (such as
329 296 hooks processes) accessing data before a transaction is finalized.
330 297
331 298 ``concurrencychecker`` will be passed to the revlog init function, see
332 299 the documentation there.
333 300 """
334 301 revlog.revlog.__init__(
335 302 self,
336 303 opener,
337 304 target=(revlog_constants.KIND_CHANGELOG, None),
338 305 radix=b'00changelog',
339 306 checkambig=True,
340 307 mmaplargeindex=True,
341 308 persistentnodemap=opener.options.get(b'persistent-nodemap', False),
342 309 concurrencychecker=concurrencychecker,
343 310 trypending=trypending,
344 311 )
345 312
346 313 if self._initempty and (self._format_version == revlog.REVLOGV1):
347 314 # changelogs don't benefit from generaldelta.
348 315
349 316 self._format_flags &= ~revlog.FLAG_GENERALDELTA
350 317 self.delta_config.general_delta = False
351 318
352 319 # Delta chains for changelogs tend to be very small because entries
353 320 # tend to be small and don't delta well with each. So disable delta
354 321 # chains.
355 322 self._storedeltachains = False
356 323
357 self._realopener = opener
358 self._delayed = False
359 self._delaybuf = None
360 self._divert = False
324 self._v2_delayed = False
361 325 self._filteredrevs = frozenset()
362 326 self._filteredrevs_hashcache = {}
363 327 self._copiesstorage = opener.options.get(b'copies-storage')
364 328
365 329 @property
366 330 def filteredrevs(self):
367 331 return self._filteredrevs
368 332
369 333 @filteredrevs.setter
370 334 def filteredrevs(self, val):
371 335 # Ensure all updates go through this function
372 336 assert isinstance(val, frozenset)
373 337 self._filteredrevs = val
374 338 self._filteredrevs_hashcache = {}
375 339
376 340 def _write_docket(self, tr):
377 if not self.is_delaying:
341 if not self._v2_delayed:
378 342 super(changelog, self)._write_docket(tr)
379 343
380 @property
381 def is_delaying(self):
382 return self._delayed
383
384 344 def delayupdate(self, tr):
385 345 """delay visibility of index updates to other readers"""
386 346 assert not self._inner.is_open
387 if self._docket is None and not self.is_delaying:
388 if len(self) == 0:
389 self._divert = True
390 if self._realopener.exists(self._indexfile + b'.a'):
391 self._realopener.unlink(self._indexfile + b'.a')
392 self.opener = _divertopener(self._realopener, self._indexfile)
393 else:
394 self._delaybuf = []
395 self.opener = _delayopener(
396 self._realopener, self._indexfile, self._delaybuf
397 )
398 self._inner.opener = self.opener
399 self._inner._segmentfile.opener = self.opener
400 self._inner._segmentfile_sidedata.opener = self.opener
401 self._delayed = True
347 if self._docket is not None:
348 self._v2_delayed = True
349 else:
350 new_index = self._inner.delay()
351 if new_index is not None:
352 self._indexfile = new_index
353 tr.registertmp(new_index)
402 354 tr.addpending(b'cl-%i' % id(self), self._writepending)
403 355 tr.addfinalize(b'cl-%i' % id(self), self._finalize)
404 356
405 357 def _finalize(self, tr):
406 358 """finalize index updates"""
407 359 assert not self._inner.is_open
408 self._delayed = False
409 self.opener = self._realopener
410 self._inner.opener = self.opener
411 self._inner._segmentfile.opener = self.opener
412 self._inner._segmentfile_sidedata.opener = self.opener
413 # move redirected index data back into place
414 360 if self._docket is not None:
415 self._write_docket(tr)
416 elif self._divert:
417 assert not self._delaybuf
418 tmpname = self._indexfile + b".a"
419 nfile = self.opener.open(tmpname)
420 nfile.close()
421 self.opener.rename(tmpname, self._indexfile, checkambig=True)
422 elif self._delaybuf:
423 fp = self.opener(self._indexfile, b'a', checkambig=True)
424 fp.write(b"".join(self._delaybuf))
425 fp.close()
426 self._delaybuf = None
427 self._divert = False
428 # split when we're done
429 self._enforceinlinesize(tr, side_write=False)
361 self._docket.write(tr)
362 self._v2_delayed = False
363 else:
364 new_index_file = self._inner.finalize_pending()
365 self._indexfile = new_index_file
366 # split when we're done
367 self._enforceinlinesize(tr, side_write=False)
430 368
431 369 def _writepending(self, tr):
432 370 """create a file containing the unfinalized state for
433 371 pretxnchangegroup"""
434 372 assert not self._inner.is_open
435 373 if self._docket:
436 return self._docket.write(tr, pending=True)
437 if self._delaybuf:
438 # make a temporary copy of the index
439 fp1 = self._realopener(self._indexfile)
440 pendingfilename = self._indexfile + b".a"
441 # register as a temp file to ensure cleanup on failure
442 tr.registertmp(pendingfilename)
443 # write existing data
444 fp2 = self._realopener(pendingfilename, b"w")
445 fp2.write(fp1.read())
446 # add pending data
447 fp2.write(b"".join(self._delaybuf))
448 fp2.close()
449 # switch modes so finalize can simply rename
450 self._delaybuf = None
451 self._divert = True
452 self.opener = _divertopener(self._realopener, self._indexfile)
453 self._inner.opener = self.opener
454 self._inner._segmentfile.opener = self.opener
455 self._inner._segmentfile_sidedata.opener = self.opener
456
457 if self._divert:
458 return True
459
460 return False
374 any_pending = self._docket.write(tr, pending=True)
375 self._v2_delayed = False
376 else:
377 new_index, any_pending = self._inner.write_pending()
378 if new_index is not None:
379 self._indexfile = new_index
380 tr.registertmp(new_index)
381 return any_pending
461 382
462 383 def _enforceinlinesize(self, tr, side_write=True):
463 384 if not self.is_delaying:
464 385 revlog.revlog._enforceinlinesize(self, tr, side_write=side_write)
465 386
466 387 def read(self, nodeorrev):
467 388 """Obtain data from a parsed changelog revision.
468 389
469 390 Returns a 6-tuple of:
470 391
471 392 - manifest node in binary
472 393 - author/user as a localstr
473 394 - date as a 2-tuple of (time, timezone)
474 395 - list of files
475 396 - commit message as a localstr
476 397 - dict of extra metadata
477 398
478 399 Unless you need to access all fields, consider calling
479 400 ``changelogrevision`` instead, as it is faster for partial object
480 401 access.
481 402 """
482 403 d = self._revisiondata(nodeorrev)
483 404 sidedata = self.sidedata(nodeorrev)
484 405 copy_sd = self._copiesstorage == b'changeset-sidedata'
485 406 c = changelogrevision(self, d, sidedata, copy_sd)
486 407 return (c.manifest, c.user, c.date, c.files, c.description, c.extra)
487 408
488 409 def changelogrevision(self, nodeorrev):
489 410 """Obtain a ``changelogrevision`` for a node or revision."""
490 411 text = self._revisiondata(nodeorrev)
491 412 sidedata = self.sidedata(nodeorrev)
492 413 return changelogrevision(
493 414 self, text, sidedata, self._copiesstorage == b'changeset-sidedata'
494 415 )
495 416
496 417 def readfiles(self, nodeorrev):
497 418 """
498 419 short version of read that only returns the files modified by the cset
499 420 """
500 421 text = self.revision(nodeorrev)
501 422 if not text:
502 423 return []
503 424 last = text.index(b"\n\n")
504 425 l = text[:last].split(b'\n')
505 426 return l[3:]
506 427
507 428 def add(
508 429 self,
509 430 manifest,
510 431 files,
511 432 desc,
512 433 transaction,
513 434 p1,
514 435 p2,
515 436 user,
516 437 date=None,
517 438 extra=None,
518 439 ):
519 440 # Convert to UTF-8 encoded bytestrings as the very first
520 441 # thing: calling any method on a localstr object will turn it
521 442 # into a str object and the cached UTF-8 string is thus lost.
522 443 user, desc = encoding.fromlocal(user), encoding.fromlocal(desc)
523 444
524 445 user = user.strip()
525 446 # An empty username or a username with a "\n" will make the
526 447 # revision text contain two "\n\n" sequences -> corrupt
527 448 # repository since read cannot unpack the revision.
528 449 if not user:
529 450 raise error.StorageError(_(b"empty username"))
530 451 if b"\n" in user:
531 452 raise error.StorageError(
532 453 _(b"username %r contains a newline") % pycompat.bytestr(user)
533 454 )
534 455
535 456 desc = stripdesc(desc)
536 457
537 458 if date:
538 459 parseddate = b"%d %d" % dateutil.parsedate(date)
539 460 else:
540 461 parseddate = b"%d %d" % dateutil.makedate()
541 462 if extra:
542 463 branch = extra.get(b"branch")
543 464 if branch in (b"default", b""):
544 465 del extra[b"branch"]
545 466 elif branch in (b".", b"null", b"tip"):
546 467 raise error.StorageError(
547 468 _(b'the name \'%s\' is reserved') % branch
548 469 )
549 470 sortedfiles = sorted(files.touched)
550 471 flags = 0
551 472 sidedata = None
552 473 if self._copiesstorage == b'changeset-sidedata':
553 474 if files.has_copies_info:
554 475 flags |= flagutil.REVIDX_HASCOPIESINFO
555 476 sidedata = metadata.encode_files_sidedata(files)
556 477
557 478 if extra:
558 479 extra = encodeextra(extra)
559 480 parseddate = b"%s %s" % (parseddate, extra)
560 481 l = [hex(manifest), user, parseddate] + sortedfiles + [b"", desc]
561 482 text = b"\n".join(l)
562 483 rev = self.addrevision(
563 484 text, transaction, len(self), p1, p2, sidedata=sidedata, flags=flags
564 485 )
565 486 return self.node(rev)
566 487
567 488 def branchinfo(self, rev):
568 489 """return the branch name and open/close state of a revision
569 490
570 491 This function exists because creating a changectx object
571 492 just to access this is costly."""
572 493 return self.changelogrevision(rev).branchinfo
573 494
574 495 def _nodeduplicatecallback(self, transaction, rev):
575 496 # keep track of revisions that got "re-added", eg: unbunde of know rev.
576 497 #
577 498 # We track them in a list to preserve their order from the source bundle
578 499 duplicates = transaction.changes.setdefault(b'revduplicates', [])
579 500 duplicates.append(rev)
@@ -1,138 +1,138 b''
1 1 # repocache.py - in-memory repository cache for long-running services
2 2 #
3 3 # Copyright 2018 Yuya Nishihara <yuya@tcha.org>
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
9 9 import collections
10 10 import gc
11 11 import threading
12 12
13 13 from . import (
14 14 error,
15 15 hg,
16 16 obsolete,
17 17 scmutil,
18 18 util,
19 19 )
20 20
21 21
22 22 class repoloader:
23 23 """Load repositories in background thread
24 24
25 25 This is designed for a forking server. A cached repo cannot be obtained
26 26 until the server fork()s a worker and the loader thread stops.
27 27 """
28 28
29 29 def __init__(self, ui, maxlen):
30 30 self._ui = ui.copy()
31 31 self._cache = util.lrucachedict(max=maxlen)
32 32 # use deque and Event instead of Queue since deque can discard
33 33 # old items to keep at most maxlen items.
34 34 self._inqueue = collections.deque(maxlen=maxlen)
35 35 self._accepting = False
36 36 self._newentry = threading.Event()
37 37 self._thread = None
38 38
39 39 def start(self):
40 40 assert not self._thread
41 41 if self._inqueue.maxlen == 0:
42 42 # no need to spawn loader thread as the cache is disabled
43 43 return
44 44 self._accepting = True
45 45 self._thread = threading.Thread(target=self._mainloop)
46 46 self._thread.start()
47 47
48 48 def stop(self):
49 49 if not self._thread:
50 50 return
51 51 self._accepting = False
52 52 self._newentry.set()
53 53 self._thread.join()
54 54 self._thread = None
55 55 self._cache.clear()
56 56 self._inqueue.clear()
57 57
58 58 def load(self, path):
59 59 """Request to load the specified repository in background"""
60 60 self._inqueue.append(path)
61 61 self._newentry.set()
62 62
63 63 def get(self, path):
64 64 """Return a cached repo if available
65 65
66 66 This function must be called after fork(), where the loader thread
67 67 is stopped. Otherwise, the returned repo might be updated by the
68 68 loader thread.
69 69 """
70 70 if self._thread and self._thread.is_alive():
71 71 raise error.ProgrammingError(
72 72 b'cannot obtain cached repo while loader is active'
73 73 )
74 74 return self._cache.peek(path, None)
75 75
76 76 def _mainloop(self):
77 77 while self._accepting:
78 78 # Avoid heavy GC after fork(), which would cancel the benefit of
79 79 # COW. We assume that GIL is acquired while GC is underway in the
80 80 # loader thread. If that isn't true, we might have to move
81 81 # gc.collect() to the main thread so that fork() would never stop
82 82 # the thread where GC is in progress.
83 83 gc.collect()
84 84
85 85 self._newentry.wait()
86 86 while self._accepting:
87 87 self._newentry.clear()
88 88 try:
89 89 path = self._inqueue.popleft()
90 90 except IndexError:
91 91 break
92 92 scmutil.callcatch(self._ui, lambda: self._load(path))
93 93
94 94 def _load(self, path):
95 95 start = util.timer()
96 96 # TODO: repo should be recreated if storage configuration changed
97 97 try:
98 98 # pop before loading so inconsistent state wouldn't be exposed
99 99 repo = self._cache.pop(path)
100 100 except KeyError:
101 101 repo = hg.repository(self._ui, path).unfiltered()
102 102 _warmupcache(repo)
103 103 repo.ui.log(
104 104 b'repocache',
105 105 b'loaded repo into cache: %s (in %.3fs)\n',
106 106 path,
107 107 util.timer() - start,
108 108 )
109 109 self._cache.insert(path, repo)
110 110
111 111
112 112 # TODO: think about proper API of preloading cache
113 113 def _warmupcache(repo):
114 114 repo.invalidateall()
115 115 repo.changelog
116 116 repo.obsstore._all
117 117 repo.obsstore.successors
118 118 repo.obsstore.predecessors
119 119 repo.obsstore.children
120 120 for name in obsolete.cachefuncs:
121 121 obsolete.getrevs(repo, name)
122 122 repo._phasecache.loadphaserevs(repo)
123 123
124 124
125 125 # TODO: think about proper API of attaching preloaded attributes
126 126 def copycache(srcrepo, destrepo):
127 127 """Copy cached attributes from srcrepo to destrepo"""
128 128 destfilecache = destrepo._filecache
129 129 srcfilecache = srcrepo._filecache
130 130 if b'changelog' in srcfilecache:
131 131 destfilecache[b'changelog'] = ce = srcfilecache[b'changelog']
132 ce.obj.opener = ce.obj._realopener = destrepo.svfs
132 ce.obj.opener = ce.obj._inner.opener = destrepo.svfs
133 133 if b'obsstore' in srcfilecache:
134 134 destfilecache[b'obsstore'] = ce = srcfilecache[b'obsstore']
135 135 ce.obj.svfs = destrepo.svfs
136 136 if b'_phasecache' in srcfilecache:
137 137 destfilecache[b'_phasecache'] = ce = srcfilecache[b'_phasecache']
138 138 ce.obj.opener = destrepo.svfs
@@ -1,4053 +1,4170 b''
1 1 # revlog.py - storage back-end for mercurial
2 2 # coding: utf8
3 3 #
4 4 # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 """Storage back-end for Mercurial.
10 10
11 11 This provides efficient delta storage with O(1) retrieve and append
12 12 and O(changes) merge between branches.
13 13 """
14 14
15 15
16 16 import binascii
17 17 import collections
18 18 import contextlib
19 19 import io
20 20 import os
21 21 import struct
22 22 import weakref
23 23 import zlib
24 24
25 25 # import stuff from node for others to import from revlog
26 26 from .node import (
27 27 bin,
28 28 hex,
29 29 nullrev,
30 30 sha1nodeconstants,
31 31 short,
32 32 wdirrev,
33 33 )
34 34 from .i18n import _
35 35 from .revlogutils.constants import (
36 36 ALL_KINDS,
37 37 CHANGELOGV2,
38 38 COMP_MODE_DEFAULT,
39 39 COMP_MODE_INLINE,
40 40 COMP_MODE_PLAIN,
41 41 DELTA_BASE_REUSE_NO,
42 42 DELTA_BASE_REUSE_TRY,
43 43 ENTRY_RANK,
44 44 FEATURES_BY_VERSION,
45 45 FLAG_GENERALDELTA,
46 46 FLAG_INLINE_DATA,
47 47 INDEX_HEADER,
48 48 KIND_CHANGELOG,
49 49 KIND_FILELOG,
50 50 RANK_UNKNOWN,
51 51 REVLOGV0,
52 52 REVLOGV1,
53 53 REVLOGV1_FLAGS,
54 54 REVLOGV2,
55 55 REVLOGV2_FLAGS,
56 56 REVLOG_DEFAULT_FLAGS,
57 57 REVLOG_DEFAULT_FORMAT,
58 58 REVLOG_DEFAULT_VERSION,
59 59 SUPPORTED_FLAGS,
60 60 )
61 61 from .revlogutils.flagutil import (
62 62 REVIDX_DEFAULT_FLAGS,
63 63 REVIDX_ELLIPSIS,
64 64 REVIDX_EXTSTORED,
65 65 REVIDX_FLAGS_ORDER,
66 66 REVIDX_HASCOPIESINFO,
67 67 REVIDX_ISCENSORED,
68 68 REVIDX_RAWTEXT_CHANGING_FLAGS,
69 69 )
70 70 from .thirdparty import attr
71 71 from . import (
72 72 ancestor,
73 73 dagop,
74 74 error,
75 75 mdiff,
76 76 policy,
77 77 pycompat,
78 78 revlogutils,
79 79 templatefilters,
80 80 util,
81 81 )
82 82 from .interfaces import (
83 83 repository,
84 84 util as interfaceutil,
85 85 )
86 86 from .revlogutils import (
87 87 deltas as deltautil,
88 88 docket as docketutil,
89 89 flagutil,
90 90 nodemap as nodemaputil,
91 91 randomaccessfile,
92 92 revlogv0,
93 93 rewrite,
94 94 sidedata as sidedatautil,
95 95 )
96 96 from .utils import (
97 97 storageutil,
98 98 stringutil,
99 99 )
100 100
101 101 # blanked usage of all the name to prevent pyflakes constraints
102 102 # We need these name available in the module for extensions.
103 103
104 104 REVLOGV0
105 105 REVLOGV1
106 106 REVLOGV2
107 107 CHANGELOGV2
108 108 FLAG_INLINE_DATA
109 109 FLAG_GENERALDELTA
110 110 REVLOG_DEFAULT_FLAGS
111 111 REVLOG_DEFAULT_FORMAT
112 112 REVLOG_DEFAULT_VERSION
113 113 REVLOGV1_FLAGS
114 114 REVLOGV2_FLAGS
115 115 REVIDX_ISCENSORED
116 116 REVIDX_ELLIPSIS
117 117 REVIDX_HASCOPIESINFO
118 118 REVIDX_EXTSTORED
119 119 REVIDX_DEFAULT_FLAGS
120 120 REVIDX_FLAGS_ORDER
121 121 REVIDX_RAWTEXT_CHANGING_FLAGS
122 122
123 123 parsers = policy.importmod('parsers')
124 124 rustancestor = policy.importrust('ancestor')
125 125 rustdagop = policy.importrust('dagop')
126 126 rustrevlog = policy.importrust('revlog')
127 127
128 128 # Aliased for performance.
129 129 _zlibdecompress = zlib.decompress
130 130
131 131 # max size of inline data embedded into a revlog
132 132 _maxinline = 131072
133 133
134 134 # Flag processors for REVIDX_ELLIPSIS.
135 135 def ellipsisreadprocessor(rl, text):
136 136 return text, False
137 137
138 138
139 139 def ellipsiswriteprocessor(rl, text):
140 140 return text, False
141 141
142 142
143 143 def ellipsisrawprocessor(rl, text):
144 144 return False
145 145
146 146
147 147 ellipsisprocessor = (
148 148 ellipsisreadprocessor,
149 149 ellipsiswriteprocessor,
150 150 ellipsisrawprocessor,
151 151 )
152 152
153 153
154 154 def _verify_revision(rl, skipflags, state, node):
155 155 """Verify the integrity of the given revlog ``node`` while providing a hook
156 156 point for extensions to influence the operation."""
157 157 if skipflags:
158 158 state[b'skipread'].add(node)
159 159 else:
160 160 # Side-effect: read content and verify hash.
161 161 rl.revision(node)
162 162
163 163
164 164 # True if a fast implementation for persistent-nodemap is available
165 165 #
166 166 # We also consider we have a "fast" implementation in "pure" python because
167 167 # people using pure don't really have performance consideration (and a
168 168 # wheelbarrow of other slowness source)
169 169 HAS_FAST_PERSISTENT_NODEMAP = rustrevlog is not None or hasattr(
170 170 parsers, 'BaseIndexObject'
171 171 )
172 172
173 173
174 174 @interfaceutil.implementer(repository.irevisiondelta)
175 175 @attr.s(slots=True)
176 176 class revlogrevisiondelta:
177 177 node = attr.ib()
178 178 p1node = attr.ib()
179 179 p2node = attr.ib()
180 180 basenode = attr.ib()
181 181 flags = attr.ib()
182 182 baserevisionsize = attr.ib()
183 183 revision = attr.ib()
184 184 delta = attr.ib()
185 185 sidedata = attr.ib()
186 186 protocol_flags = attr.ib()
187 187 linknode = attr.ib(default=None)
188 188
189 189
190 190 @interfaceutil.implementer(repository.iverifyproblem)
191 191 @attr.s(frozen=True)
192 192 class revlogproblem:
193 193 warning = attr.ib(default=None)
194 194 error = attr.ib(default=None)
195 195 node = attr.ib(default=None)
196 196
197 197
198 198 def parse_index_v1(data, inline):
199 199 # call the C implementation to parse the index data
200 200 index, cache = parsers.parse_index2(data, inline)
201 201 return index, cache
202 202
203 203
204 204 def parse_index_v2(data, inline):
205 205 # call the C implementation to parse the index data
206 206 index, cache = parsers.parse_index2(data, inline, format=REVLOGV2)
207 207 return index, cache
208 208
209 209
210 210 def parse_index_cl_v2(data, inline):
211 211 # call the C implementation to parse the index data
212 212 index, cache = parsers.parse_index2(data, inline, format=CHANGELOGV2)
213 213 return index, cache
214 214
215 215
216 216 if hasattr(parsers, 'parse_index_devel_nodemap'):
217 217
218 218 def parse_index_v1_nodemap(data, inline):
219 219 index, cache = parsers.parse_index_devel_nodemap(data, inline)
220 220 return index, cache
221 221
222 222
223 223 else:
224 224 parse_index_v1_nodemap = None
225 225
226 226
227 227 def parse_index_v1_mixed(data, inline):
228 228 index, cache = parse_index_v1(data, inline)
229 229 return rustrevlog.MixedIndex(index), cache
230 230
231 231
232 232 # corresponds to uncompressed length of indexformatng (2 gigs, 4-byte
233 233 # signed integer)
234 234 _maxentrysize = 0x7FFFFFFF
235 235
236 236 FILE_TOO_SHORT_MSG = _(
237 237 b'cannot read from revlog %s;'
238 238 b' expected %d bytes from offset %d, data size is %d'
239 239 )
240 240
241 241 hexdigits = b'0123456789abcdefABCDEF'
242 242
243 243
244 244 class _Config:
245 245 def copy(self):
246 246 return self.__class__(**self.__dict__)
247 247
248 248
249 249 @attr.s()
250 250 class FeatureConfig(_Config):
251 251 """Hold configuration values about the available revlog features"""
252 252
253 253 # the default compression engine
254 254 compression_engine = attr.ib(default=b'zlib')
255 255 # compression engines options
256 256 compression_engine_options = attr.ib(default=attr.Factory(dict))
257 257
258 258 # can we use censor on this revlog
259 259 censorable = attr.ib(default=False)
260 260 # does this revlog use the "side data" feature
261 261 has_side_data = attr.ib(default=False)
262 262 # might remove rank configuration once the computation has no impact
263 263 compute_rank = attr.ib(default=False)
264 264 # parent order is supposed to be semantically irrelevant, so we
265 265 # normally resort parents to ensure that the first parent is non-null,
266 266 # if there is a non-null parent at all.
267 267 # filelog abuses the parent order as flag to mark some instances of
268 268 # meta-encoded files, so allow it to disable this behavior.
269 269 canonical_parent_order = attr.ib(default=False)
270 270 # can ellipsis commit be used
271 271 enable_ellipsis = attr.ib(default=False)
272 272
273 273 def copy(self):
274 274 new = super().copy()
275 275 new.compression_engine_options = self.compression_engine_options.copy()
276 276 return new
277 277
278 278
279 279 @attr.s()
280 280 class DataConfig(_Config):
281 281 """Hold configuration value about how the revlog data are read"""
282 282
283 283 # should we try to open the "pending" version of the revlog
284 284 try_pending = attr.ib(default=False)
285 285 # should we try to open the "splitted" version of the revlog
286 286 try_split = attr.ib(default=False)
287 287 # When True, indexfile should be opened with checkambig=True at writing,
288 288 # to avoid file stat ambiguity.
289 289 check_ambig = attr.ib(default=False)
290 290
291 291 # If true, use mmap instead of reading to deal with large index
292 292 mmap_large_index = attr.ib(default=False)
293 293 # how much data is large
294 294 mmap_index_threshold = attr.ib(default=None)
295 295 # How much data to read and cache into the raw revlog data cache.
296 296 chunk_cache_size = attr.ib(default=65536)
297 297
298 298 # Allow sparse reading of the revlog data
299 299 with_sparse_read = attr.ib(default=False)
300 300 # minimal density of a sparse read chunk
301 301 sr_density_threshold = attr.ib(default=0.50)
302 302 # minimal size of data we skip when performing sparse read
303 303 sr_min_gap_size = attr.ib(default=262144)
304 304
305 305 # are delta encoded against arbitrary bases.
306 306 generaldelta = attr.ib(default=False)
307 307
308 308
309 309 @attr.s()
310 310 class DeltaConfig(_Config):
311 311 """Hold configuration value about how new delta are computed
312 312
313 313 Some attributes are duplicated from DataConfig to help havign each object
314 314 self contained.
315 315 """
316 316
317 317 # can delta be encoded against arbitrary bases.
318 318 general_delta = attr.ib(default=False)
319 319 # Allow sparse writing of the revlog data
320 320 sparse_revlog = attr.ib(default=False)
321 321 # maximum length of a delta chain
322 322 max_chain_len = attr.ib(default=None)
323 323 # Maximum distance between delta chain base start and end
324 324 max_deltachain_span = attr.ib(default=-1)
325 325 # If `upper_bound_comp` is not None, this is the expected maximal gain from
326 326 # compression for the data content.
327 327 upper_bound_comp = attr.ib(default=None)
328 328 # Should we try a delta against both parent
329 329 delta_both_parents = attr.ib(default=True)
330 330 # Test delta base candidate group by chunk of this maximal size.
331 331 candidate_group_chunk_size = attr.ib(default=0)
332 332 # Should we display debug information about delta computation
333 333 debug_delta = attr.ib(default=False)
334 334 # trust incoming delta by default
335 335 lazy_delta = attr.ib(default=True)
336 336 # trust the base of incoming delta by default
337 337 lazy_delta_base = attr.ib(default=False)
338 338
339 339
340 340 class _InnerRevlog:
341 341 """An inner layer of the revlog object
342 342
343 343 That layer exist to be able to delegate some operation to Rust, its
344 344 boundaries are arbitrary and based on what we can delegate to Rust.
345 345 """
346 346
347 347 def __init__(
348 348 self,
349 349 opener,
350 350 index,
351 351 index_file,
352 352 data_file,
353 353 sidedata_file,
354 354 inline,
355 355 data_config,
356 356 delta_config,
357 357 feature_config,
358 358 chunk_cache,
359 359 default_compression_header,
360 360 ):
361 361 self.opener = opener
362 362 self.index = index
363 363
364 364 self.__index_file = index_file
365 365 self.data_file = data_file
366 366 self.sidedata_file = sidedata_file
367 367 self.inline = inline
368 368 self.data_config = data_config
369 369 self.delta_config = delta_config
370 370 self.feature_config = feature_config
371 371
372 # used during diverted write.
373 self._orig_index_file = None
374
372 375 self._default_compression_header = default_compression_header
373 376
374 377 # index
375 378
376 379 # 3-tuple of file handles being used for active writing.
377 380 self._writinghandles = None
378 381
379 382 self._segmentfile = randomaccessfile.randomaccessfile(
380 383 self.opener,
381 384 (self.index_file if self.inline else self.data_file),
382 385 self.data_config.chunk_cache_size,
383 386 chunk_cache,
384 387 )
385 388 self._segmentfile_sidedata = randomaccessfile.randomaccessfile(
386 389 self.opener,
387 390 self.sidedata_file,
388 391 self.data_config.chunk_cache_size,
389 392 )
390 393
391 394 # revlog header -> revlog compressor
392 395 self._decompressors = {}
393 396 # 3-tuple of (node, rev, text) for a raw revision.
394 397 self._revisioncache = None
395 398
399 self._delay_buffer = None
400
396 401 @property
397 402 def index_file(self):
398 403 return self.__index_file
399 404
400 405 @index_file.setter
401 406 def index_file(self, new_index_file):
402 407 self.__index_file = new_index_file
403 408 if self.inline:
404 409 self._segmentfile.filename = new_index_file
405 410
406 411 def __len__(self):
407 412 return len(self.index)
408 413
409 414 def clear_cache(self):
415 assert not self.is_delaying
410 416 self._revisioncache = None
411 417 self._segmentfile.clear_cache()
412 418 self._segmentfile_sidedata.clear_cache()
413 419
414 420 @property
415 421 def canonical_index_file(self):
422 if self._orig_index_file is not None:
423 return self._orig_index_file
416 424 return self.index_file
417 425
426 @property
427 def is_delaying(self):
428 """is the revlog is currently delaying the visibility of written data?
429
430 The delaying mechanism can be either in-memory or written on disk in a
431 side-file."""
432 return (self._delay_buffer is not None) or (
433 self._orig_index_file is not None
434 )
435
418 436 # Derived from index values.
419 437
420 438 def start(self, rev):
421 439 """the offset of the data chunk for this revision"""
422 440 return int(self.index[rev][0] >> 16)
423 441
424 442 def length(self, rev):
425 443 """the length of the data chunk for this revision"""
426 444 return self.index[rev][1]
427 445
428 446 def end(self, rev):
429 447 """the end of the data chunk for this revision"""
430 448 return self.start(rev) + self.length(rev)
431 449
432 450 def deltaparent(self, rev):
433 451 """return deltaparent of the given revision"""
434 452 base = self.index[rev][3]
435 453 if base == rev:
436 454 return nullrev
437 455 elif self.delta_config.general_delta:
438 456 return base
439 457 else:
440 458 return rev - 1
441 459
442 460 def issnapshot(self, rev):
443 461 """tells whether rev is a snapshot"""
444 462 if not self.delta_config.sparse_revlog:
445 463 return self.deltaparent(rev) == nullrev
446 464 elif hasattr(self.index, 'issnapshot'):
447 465 # directly assign the method to cache the testing and access
448 466 self.issnapshot = self.index.issnapshot
449 467 return self.issnapshot(rev)
450 468 if rev == nullrev:
451 469 return True
452 470 entry = self.index[rev]
453 471 base = entry[3]
454 472 if base == rev:
455 473 return True
456 474 if base == nullrev:
457 475 return True
458 476 p1 = entry[5]
459 477 while self.length(p1) == 0:
460 478 b = self.deltaparent(p1)
461 479 if b == p1:
462 480 break
463 481 p1 = b
464 482 p2 = entry[6]
465 483 while self.length(p2) == 0:
466 484 b = self.deltaparent(p2)
467 485 if b == p2:
468 486 break
469 487 p2 = b
470 488 if base == p1 or base == p2:
471 489 return False
472 490 return self.issnapshot(base)
473 491
474 492 def _deltachain(self, rev, stoprev=None):
475 493 """Obtain the delta chain for a revision.
476 494
477 495 ``stoprev`` specifies a revision to stop at. If not specified, we
478 496 stop at the base of the chain.
479 497
480 498 Returns a 2-tuple of (chain, stopped) where ``chain`` is a list of
481 499 revs in ascending order and ``stopped`` is a bool indicating whether
482 500 ``stoprev`` was hit.
483 501 """
484 502 generaldelta = self.delta_config.general_delta
485 503 # Try C implementation.
486 504 try:
487 505 return self.index.deltachain(rev, stoprev, generaldelta)
488 506 except AttributeError:
489 507 pass
490 508
491 509 chain = []
492 510
493 511 # Alias to prevent attribute lookup in tight loop.
494 512 index = self.index
495 513
496 514 iterrev = rev
497 515 e = index[iterrev]
498 516 while iterrev != e[3] and iterrev != stoprev:
499 517 chain.append(iterrev)
500 518 if generaldelta:
501 519 iterrev = e[3]
502 520 else:
503 521 iterrev -= 1
504 522 e = index[iterrev]
505 523
506 524 if iterrev == stoprev:
507 525 stopped = True
508 526 else:
509 527 chain.append(iterrev)
510 528 stopped = False
511 529
512 530 chain.reverse()
513 531 return chain, stopped
514 532
515 533 @util.propertycache
516 534 def _compressor(self):
517 535 engine = util.compengines[self.feature_config.compression_engine]
518 536 return engine.revlogcompressor(
519 537 self.feature_config.compression_engine_options
520 538 )
521 539
522 540 @util.propertycache
523 541 def _decompressor(self):
524 542 """the default decompressor"""
525 543 if self._default_compression_header is None:
526 544 return None
527 545 t = self._default_compression_header
528 546 c = self._get_decompressor(t)
529 547 return c.decompress
530 548
531 549 def _get_decompressor(self, t):
532 550 try:
533 551 compressor = self._decompressors[t]
534 552 except KeyError:
535 553 try:
536 554 engine = util.compengines.forrevlogheader(t)
537 555 compressor = engine.revlogcompressor(
538 556 self.feature_config.compression_engine_options
539 557 )
540 558 self._decompressors[t] = compressor
541 559 except KeyError:
542 560 raise error.RevlogError(
543 561 _(b'unknown compression type %s') % binascii.hexlify(t)
544 562 )
545 563 return compressor
546 564
547 565 def compress(self, data):
548 566 """Generate a possibly-compressed representation of data."""
549 567 if not data:
550 568 return b'', data
551 569
552 570 compressed = self._compressor.compress(data)
553 571
554 572 if compressed:
555 573 # The revlog compressor added the header in the returned data.
556 574 return b'', compressed
557 575
558 576 if data[0:1] == b'\0':
559 577 return b'', data
560 578 return b'u', data
561 579
562 580 def decompress(self, data):
563 581 """Decompress a revlog chunk.
564 582
565 583 The chunk is expected to begin with a header identifying the
566 584 format type so it can be routed to an appropriate decompressor.
567 585 """
568 586 if not data:
569 587 return data
570 588
571 589 # Revlogs are read much more frequently than they are written and many
572 590 # chunks only take microseconds to decompress, so performance is
573 591 # important here.
574 592 #
575 593 # We can make a few assumptions about revlogs:
576 594 #
577 595 # 1) the majority of chunks will be compressed (as opposed to inline
578 596 # raw data).
579 597 # 2) decompressing *any* data will likely by at least 10x slower than
580 598 # returning raw inline data.
581 599 # 3) we want to prioritize common and officially supported compression
582 600 # engines
583 601 #
584 602 # It follows that we want to optimize for "decompress compressed data
585 603 # when encoded with common and officially supported compression engines"
586 604 # case over "raw data" and "data encoded by less common or non-official
587 605 # compression engines." That is why we have the inline lookup first
588 606 # followed by the compengines lookup.
589 607 #
590 608 # According to `hg perfrevlogchunks`, this is ~0.5% faster for zlib
591 609 # compressed chunks. And this matters for changelog and manifest reads.
592 610 t = data[0:1]
593 611
594 612 if t == b'x':
595 613 try:
596 614 return _zlibdecompress(data)
597 615 except zlib.error as e:
598 616 raise error.RevlogError(
599 617 _(b'revlog decompress error: %s')
600 618 % stringutil.forcebytestr(e)
601 619 )
602 620 # '\0' is more common than 'u' so it goes first.
603 621 elif t == b'\0':
604 622 return data
605 623 elif t == b'u':
606 624 return util.buffer(data, 1)
607 625
608 626 compressor = self._get_decompressor(t)
609 627
610 628 return compressor.decompress(data)
611 629
612 630 @contextlib.contextmanager
613 631 def reading(self):
614 632 """Context manager that keeps data and sidedata files open for reading"""
615 633 if len(self.index) == 0:
616 634 yield # nothing to be read
617 635 else:
618 636 with self._segmentfile.reading():
619 637 with self._segmentfile_sidedata.reading():
620 638 yield
621 639
622 640 @property
623 641 def is_writing(self):
624 642 """True is a writing context is open"""
625 643 return self._writinghandles is not None
626 644
627 645 @property
628 646 def is_open(self):
629 647 """True if any file handle is being held
630 648
631 649 Used for assert and debug in the python code"""
632 650 return self._segmentfile.is_open or self._segmentfile_sidedata.is_open
633 651
634 652 @contextlib.contextmanager
635 653 def writing(self, transaction, data_end=None, sidedata_end=None):
636 654 """Open the revlog files for writing
637 655
638 656 Add content to a revlog should be done within such context.
639 657 """
640 658 if self.is_writing:
641 659 yield
642 660 else:
643 661 ifh = dfh = sdfh = None
644 662 try:
645 663 r = len(self.index)
646 664 # opening the data file.
647 665 dsize = 0
648 666 if r:
649 667 dsize = self.end(r - 1)
650 668 dfh = None
651 669 if not self.inline:
652 670 try:
653 671 dfh = self.opener(self.data_file, mode=b"r+")
654 672 if data_end is None:
655 673 dfh.seek(0, os.SEEK_END)
656 674 else:
657 675 dfh.seek(data_end, os.SEEK_SET)
658 676 except FileNotFoundError:
659 677 dfh = self.opener(self.data_file, mode=b"w+")
660 678 transaction.add(self.data_file, dsize)
661 679 if self.sidedata_file is not None:
662 680 assert sidedata_end is not None
663 681 # revlog-v2 does not inline, help Pytype
664 682 assert dfh is not None
665 683 try:
666 684 sdfh = self.opener(self.sidedata_file, mode=b"r+")
667 685 dfh.seek(sidedata_end, os.SEEK_SET)
668 686 except FileNotFoundError:
669 687 sdfh = self.opener(self.sidedata_file, mode=b"w+")
670 688 transaction.add(self.sidedata_file, sidedata_end)
671 689
672 690 # opening the index file.
673 691 isize = r * self.index.entry_size
674 692 ifh = self.__index_write_fp()
675 693 if self.inline:
676 694 transaction.add(self.index_file, dsize + isize)
677 695 else:
678 696 transaction.add(self.index_file, isize)
679 697 # exposing all file handle for writing.
680 698 self._writinghandles = (ifh, dfh, sdfh)
681 699 self._segmentfile.writing_handle = ifh if self.inline else dfh
682 700 self._segmentfile_sidedata.writing_handle = sdfh
683 701 yield
684 702 finally:
685 703 self._writinghandles = None
686 704 self._segmentfile.writing_handle = None
687 705 self._segmentfile_sidedata.writing_handle = None
688 706 if dfh is not None:
689 707 dfh.close()
690 708 if sdfh is not None:
691 709 sdfh.close()
692 710 # closing the index file last to avoid exposing referent to
693 711 # potential unflushed data content.
694 712 if ifh is not None:
695 713 ifh.close()
696 714
697 715 def __index_write_fp(self, index_end=None):
698 716 """internal method to open the index file for writing
699 717
700 718 You should not use this directly and use `_writing` instead
701 719 """
702 720 try:
703 f = self.opener(
704 self.index_file,
705 mode=b"r+",
706 checkambig=self.data_config.check_ambig,
707 )
721 if self._delay_buffer is None:
722 f = self.opener(
723 self.index_file,
724 mode=b"r+",
725 checkambig=self.data_config.check_ambig,
726 )
727 else:
728 # check_ambig affect we way we open file for writing, however
729 # here, we do not actually open a file for writting as write
730 # will appened to a delay_buffer. So check_ambig is not
731 # meaningful and unneeded here.
732 f = randomaccessfile.appender(
733 self.opener, self.index_file, b"r+", self._delay_buffer
734 )
708 735 if index_end is None:
709 736 f.seek(0, os.SEEK_END)
710 737 else:
711 738 f.seek(index_end, os.SEEK_SET)
712 739 return f
713 740 except FileNotFoundError:
714 return self.opener(
715 self.index_file,
716 mode=b"w+",
717 checkambig=self.data_config.check_ambig,
718 )
741 if self._delay_buffer is None:
742 return self.opener(
743 self.index_file,
744 mode=b"w+",
745 checkambig=self.data_config.check_ambig,
746 )
747 else:
748 return randomaccessfile.appender(
749 self.opener, self.index_file, b"w+", self._delay_buffer
750 )
719 751
720 752 def __index_new_fp(self):
721 753 """internal method to create a new index file for writing
722 754
723 755 You should not use this unless you are upgrading from inline revlog
724 756 """
725 757 return self.opener(
726 758 self.index_file,
727 759 mode=b"w",
728 760 checkambig=self.data_config.check_ambig,
729 761 atomictemp=True,
730 762 )
731 763
732 764 def split_inline(self, tr, header, new_index_file_path=None):
733 765 """split the data of an inline revlog into an index and a data file"""
734 766 existing_handles = False
735 767 if self._writinghandles is not None:
736 768 existing_handles = True
737 769 fp = self._writinghandles[0]
738 770 fp.flush()
739 771 fp.close()
740 772 # We can't use the cached file handle after close(). So prevent
741 773 # its usage.
742 774 self._writinghandles = None
743 775 self._segmentfile.writing_handle = None
744 776 # No need to deal with sidedata writing handle as it is only
745 777 # relevant with revlog-v2 which is never inline, not reaching
746 778 # this code
747 779
748 780 new_dfh = self.opener(self.data_file, mode=b"w+")
749 781 new_dfh.truncate(0) # drop any potentially existing data
750 782 try:
751 783 with self.reading():
752 784 for r in range(len(self.index)):
753 785 new_dfh.write(self.get_segment_for_revs(r, r)[1])
754 786 new_dfh.flush()
755 787
756 788 if new_index_file_path is not None:
757 789 self.index_file = new_index_file_path
758 790 with self.__index_new_fp() as fp:
759 791 self.inline = False
760 792 for i in range(len(self.index)):
761 793 e = self.index.entry_binary(i)
762 794 if i == 0:
763 795 packed_header = self.index.pack_header(header)
764 796 e = packed_header + e
765 797 fp.write(e)
766 798
767 799 # If we don't use side-write, the temp file replace the real
768 800 # index when we exit the context manager
769 801
770 802 self._segmentfile = randomaccessfile.randomaccessfile(
771 803 self.opener,
772 804 self.data_file,
773 805 self.data_config.chunk_cache_size,
774 806 )
775 807
776 808 if existing_handles:
777 809 # switched from inline to conventional reopen the index
778 810 ifh = self.__index_write_fp()
779 811 self._writinghandles = (ifh, new_dfh, None)
780 812 self._segmentfile.writing_handle = new_dfh
781 813 new_dfh = None
782 814 # No need to deal with sidedata writing handle as it is only
783 815 # relevant with revlog-v2 which is never inline, not reaching
784 816 # this code
785 817 finally:
786 818 if new_dfh is not None:
787 819 new_dfh.close()
788 820 return self.index_file
789 821
790 822 def get_segment_for_revs(self, startrev, endrev):
791 823 """Obtain a segment of raw data corresponding to a range of revisions.
792 824
793 825 Accepts the start and end revisions and an optional already-open
794 826 file handle to be used for reading. If the file handle is read, its
795 827 seek position will not be preserved.
796 828
797 829 Requests for data may be satisfied by a cache.
798 830
799 831 Returns a 2-tuple of (offset, data) for the requested range of
800 832 revisions. Offset is the integer offset from the beginning of the
801 833 revlog and data is a str or buffer of the raw byte data.
802 834
803 835 Callers will need to call ``self.start(rev)`` and ``self.length(rev)``
804 836 to determine where each revision's data begins and ends.
805 837
806 838 API: we should consider making this a private part of the InnerRevlog
807 839 at some point.
808 840 """
809 841 # Inlined self.start(startrev) & self.end(endrev) for perf reasons
810 842 # (functions are expensive).
811 843 index = self.index
812 844 istart = index[startrev]
813 845 start = int(istart[0] >> 16)
814 846 if startrev == endrev:
815 847 end = start + istart[1]
816 848 else:
817 849 iend = index[endrev]
818 850 end = int(iend[0] >> 16) + iend[1]
819 851
820 852 if self.inline:
821 853 start += (startrev + 1) * self.index.entry_size
822 854 end += (endrev + 1) * self.index.entry_size
823 855 length = end - start
824 856
825 857 return start, self._segmentfile.read_chunk(start, length)
826 858
827 859 def _chunk(self, rev):
828 860 """Obtain a single decompressed chunk for a revision.
829 861
830 862 Accepts an integer revision and an optional already-open file handle
831 863 to be used for reading. If used, the seek position of the file will not
832 864 be preserved.
833 865
834 866 Returns a str holding uncompressed data for the requested revision.
835 867 """
836 868 compression_mode = self.index[rev][10]
837 869 data = self.get_segment_for_revs(rev, rev)[1]
838 870 if compression_mode == COMP_MODE_PLAIN:
839 871 return data
840 872 elif compression_mode == COMP_MODE_DEFAULT:
841 873 return self._decompressor(data)
842 874 elif compression_mode == COMP_MODE_INLINE:
843 875 return self.decompress(data)
844 876 else:
845 877 msg = b'unknown compression mode %d'
846 878 msg %= compression_mode
847 879 raise error.RevlogError(msg)
848 880
849 881 def _chunks(self, revs, targetsize=None):
850 882 """Obtain decompressed chunks for the specified revisions.
851 883
852 884 Accepts an iterable of numeric revisions that are assumed to be in
853 885 ascending order. Also accepts an optional already-open file handle
854 886 to be used for reading. If used, the seek position of the file will
855 887 not be preserved.
856 888
857 889 This function is similar to calling ``self._chunk()`` multiple times,
858 890 but is faster.
859 891
860 892 Returns a list with decompressed data for each requested revision.
861 893 """
862 894 if not revs:
863 895 return []
864 896 start = self.start
865 897 length = self.length
866 898 inline = self.inline
867 899 iosize = self.index.entry_size
868 900 buffer = util.buffer
869 901
870 902 l = []
871 903 ladd = l.append
872 904
873 905 if not self.data_config.with_sparse_read:
874 906 slicedchunks = (revs,)
875 907 else:
876 908 slicedchunks = deltautil.slicechunk(
877 909 self,
878 910 revs,
879 911 targetsize=targetsize,
880 912 )
881 913
882 914 for revschunk in slicedchunks:
883 915 firstrev = revschunk[0]
884 916 # Skip trailing revisions with empty diff
885 917 for lastrev in revschunk[::-1]:
886 918 if length(lastrev) != 0:
887 919 break
888 920
889 921 try:
890 922 offset, data = self.get_segment_for_revs(firstrev, lastrev)
891 923 except OverflowError:
892 924 # issue4215 - we can't cache a run of chunks greater than
893 925 # 2G on Windows
894 926 return [self._chunk(rev) for rev in revschunk]
895 927
896 928 decomp = self.decompress
897 929 # self._decompressor might be None, but will not be used in that case
898 930 def_decomp = self._decompressor
899 931 for rev in revschunk:
900 932 chunkstart = start(rev)
901 933 if inline:
902 934 chunkstart += (rev + 1) * iosize
903 935 chunklength = length(rev)
904 936 comp_mode = self.index[rev][10]
905 937 c = buffer(data, chunkstart - offset, chunklength)
906 938 if comp_mode == COMP_MODE_PLAIN:
907 939 ladd(c)
908 940 elif comp_mode == COMP_MODE_INLINE:
909 941 ladd(decomp(c))
910 942 elif comp_mode == COMP_MODE_DEFAULT:
911 943 ladd(def_decomp(c))
912 944 else:
913 945 msg = b'unknown compression mode %d'
914 946 msg %= comp_mode
915 947 raise error.RevlogError(msg)
916 948
917 949 return l
918 950
919 951 def raw_text(self, node, rev):
920 952 """return the possibly unvalidated rawtext for a revision
921 953
922 954 returns (rev, rawtext, validated)
923 955 """
924 956
925 957 # revision in the cache (could be useful to apply delta)
926 958 cachedrev = None
927 959 # An intermediate text to apply deltas to
928 960 basetext = None
929 961
930 962 # Check if we have the entry in cache
931 963 # The cache entry looks like (node, rev, rawtext)
932 964 if self._revisioncache:
933 965 cachedrev = self._revisioncache[1]
934 966
935 967 chain, stopped = self._deltachain(rev, stoprev=cachedrev)
936 968 if stopped:
937 969 basetext = self._revisioncache[2]
938 970
939 971 # drop cache to save memory, the caller is expected to
940 972 # update self._inner._revisioncache after validating the text
941 973 self._revisioncache = None
942 974
943 975 targetsize = None
944 976 rawsize = self.index[rev][2]
945 977 if 0 <= rawsize:
946 978 targetsize = 4 * rawsize
947 979
948 980 bins = self._chunks(chain, targetsize=targetsize)
949 981 if basetext is None:
950 982 basetext = bytes(bins[0])
951 983 bins = bins[1:]
952 984
953 985 rawtext = mdiff.patches(basetext, bins)
954 986 del basetext # let us have a chance to free memory early
955 987 return (rev, rawtext, False)
956 988
957 989 def sidedata(self, rev, sidedata_end):
958 990 """Return the sidedata for a given revision number."""
959 991 index_entry = self.index[rev]
960 992 sidedata_offset = index_entry[8]
961 993 sidedata_size = index_entry[9]
962 994
963 995 if self.inline:
964 996 sidedata_offset += self.index.entry_size * (1 + rev)
965 997 if sidedata_size == 0:
966 998 return {}
967 999
968 1000 if sidedata_end < sidedata_offset + sidedata_size:
969 1001 filename = self.sidedata_file
970 1002 end = sidedata_end
971 1003 offset = sidedata_offset
972 1004 length = sidedata_size
973 1005 m = FILE_TOO_SHORT_MSG % (filename, length, offset, end)
974 1006 raise error.RevlogError(m)
975 1007
976 1008 comp_segment = self._segmentfile_sidedata.read_chunk(
977 1009 sidedata_offset, sidedata_size
978 1010 )
979 1011
980 1012 comp = self.index[rev][11]
981 1013 if comp == COMP_MODE_PLAIN:
982 1014 segment = comp_segment
983 1015 elif comp == COMP_MODE_DEFAULT:
984 1016 segment = self._decompressor(comp_segment)
985 1017 elif comp == COMP_MODE_INLINE:
986 1018 segment = self.decompress(comp_segment)
987 1019 else:
988 1020 msg = b'unknown compression mode %d'
989 1021 msg %= comp
990 1022 raise error.RevlogError(msg)
991 1023
992 1024 sidedata = sidedatautil.deserialize_sidedata(segment)
993 1025 return sidedata
994 1026
995 1027 def write_entry(
996 1028 self,
997 1029 transaction,
998 1030 entry,
999 1031 data,
1000 1032 link,
1001 1033 offset,
1002 1034 sidedata,
1003 1035 sidedata_offset,
1004 1036 index_end,
1005 1037 data_end,
1006 1038 sidedata_end,
1007 1039 ):
1008 1040 # Files opened in a+ mode have inconsistent behavior on various
1009 1041 # platforms. Windows requires that a file positioning call be made
1010 1042 # when the file handle transitions between reads and writes. See
1011 1043 # 3686fa2b8eee and the mixedfilemodewrapper in windows.py. On other
1012 1044 # platforms, Python or the platform itself can be buggy. Some versions
1013 1045 # of Solaris have been observed to not append at the end of the file
1014 1046 # if the file was seeked to before the end. See issue4943 for more.
1015 1047 #
1016 1048 # We work around this issue by inserting a seek() before writing.
1017 1049 # Note: This is likely not necessary on Python 3. However, because
1018 1050 # the file handle is reused for reads and may be seeked there, we need
1019 1051 # to be careful before changing this.
1020 1052 if self._writinghandles is None:
1021 1053 msg = b'adding revision outside `revlog._writing` context'
1022 1054 raise error.ProgrammingError(msg)
1023 1055 ifh, dfh, sdfh = self._writinghandles
1024 1056 if index_end is None:
1025 1057 ifh.seek(0, os.SEEK_END)
1026 1058 else:
1027 1059 ifh.seek(index_end, os.SEEK_SET)
1028 1060 if dfh:
1029 1061 if data_end is None:
1030 1062 dfh.seek(0, os.SEEK_END)
1031 1063 else:
1032 1064 dfh.seek(data_end, os.SEEK_SET)
1033 1065 if sdfh:
1034 1066 sdfh.seek(sidedata_end, os.SEEK_SET)
1035 1067
1036 1068 curr = len(self.index) - 1
1037 1069 if not self.inline:
1038 1070 transaction.add(self.data_file, offset)
1039 1071 if self.sidedata_file:
1040 1072 transaction.add(self.sidedata_file, sidedata_offset)
1041 1073 transaction.add(self.canonical_index_file, curr * len(entry))
1042 1074 if data[0]:
1043 1075 dfh.write(data[0])
1044 1076 dfh.write(data[1])
1045 1077 if sidedata:
1046 1078 sdfh.write(sidedata)
1047 ifh.write(entry)
1079 if self._delay_buffer is None:
1080 ifh.write(entry)
1081 else:
1082 self._delay_buffer.append(entry)
1048 1083 else:
1049 1084 offset += curr * self.index.entry_size
1050 1085 transaction.add(self.canonical_index_file, offset)
1051 ifh.write(entry)
1052 ifh.write(data[0])
1053 ifh.write(data[1])
1054 1086 assert not sidedata
1087 if self._delay_buffer is None:
1088 ifh.write(entry)
1089 ifh.write(data[0])
1090 ifh.write(data[1])
1091 else:
1092 self._delay_buffer.append(entry)
1093 self._delay_buffer.append(data[0])
1094 self._delay_buffer.append(data[1])
1055 1095 return (
1056 1096 ifh.tell(),
1057 1097 dfh.tell() if dfh else None,
1058 1098 sdfh.tell() if sdfh else None,
1059 1099 )
1060 1100
1101 def _divert_index(self):
1102 return self.index_file + b'.a'
1103
1104 def delay(self):
1105 assert not self.is_open
1106 if self._delay_buffer is not None or self._orig_index_file is not None:
1107 # delay or divert already in place
1108 return None
1109 elif len(self.index) == 0:
1110 self._orig_index_file = self.index_file
1111 self.index_file = self._divert_index()
1112 self._segmentfile.filename = self.index_file
1113 assert self._orig_index_file is not None
1114 assert self.index_file is not None
1115 if self.opener.exists(self.index_file):
1116 self.opener.unlink(self.index_file)
1117 return self.index_file
1118 else:
1119 self._segmentfile._delay_buffer = self._delay_buffer = []
1120 return None
1121
1122 def write_pending(self):
1123 assert not self.is_open
1124 if self._orig_index_file is not None:
1125 return None, True
1126 any_pending = False
1127 pending_index_file = self._divert_index()
1128 if self.opener.exists(pending_index_file):
1129 self.opener.unlink(pending_index_file)
1130 util.copyfile(
1131 self.opener.join(self.index_file),
1132 self.opener.join(pending_index_file),
1133 )
1134 if self._delay_buffer:
1135 with self.opener(pending_index_file, b'r+') as ifh:
1136 ifh.seek(0, os.SEEK_END)
1137 ifh.write(b"".join(self._delay_buffer))
1138 any_pending = True
1139 self._segmentfile._delay_buffer = self._delay_buffer = None
1140 self._orig_index_file = self.index_file
1141 self.index_file = pending_index_file
1142 self._segmentfile.filename = self.index_file
1143 return self.index_file, any_pending
1144
1145 def finalize_pending(self):
1146 assert not self.is_open
1147
1148 delay = self._delay_buffer is not None
1149 divert = self._orig_index_file is not None
1150
1151 if delay and divert:
1152 assert False, "unreachable"
1153 elif delay:
1154 if self._delay_buffer:
1155 with self.opener(self.index_file, b'r+') as ifh:
1156 ifh.seek(0, os.SEEK_END)
1157 ifh.write(b"".join(self._delay_buffer))
1158 self._segmentfile._delay_buffer = self._delay_buffer = None
1159 elif divert:
1160 if self.opener.exists(self.index_file):
1161 self.opener.rename(
1162 self.index_file,
1163 self._orig_index_file,
1164 checkambig=True,
1165 )
1166 self.index_file = self._orig_index_file
1167 self._orig_index_file = None
1168 self._segmentfile.filename = self.index_file
1169 else:
1170 msg = b"not delay or divert found on this revlog"
1171 raise error.ProgrammingError(msg)
1172 return self.canonical_index_file
1173
1061 1174
1062 1175 class revlog:
1063 1176 """
1064 1177 the underlying revision storage object
1065 1178
1066 1179 A revlog consists of two parts, an index and the revision data.
1067 1180
1068 1181 The index is a file with a fixed record size containing
1069 1182 information on each revision, including its nodeid (hash), the
1070 1183 nodeids of its parents, the position and offset of its data within
1071 1184 the data file, and the revision it's based on. Finally, each entry
1072 1185 contains a linkrev entry that can serve as a pointer to external
1073 1186 data.
1074 1187
1075 1188 The revision data itself is a linear collection of data chunks.
1076 1189 Each chunk represents a revision and is usually represented as a
1077 1190 delta against the previous chunk. To bound lookup time, runs of
1078 1191 deltas are limited to about 2 times the length of the original
1079 1192 version data. This makes retrieval of a version proportional to
1080 1193 its size, or O(1) relative to the number of revisions.
1081 1194
1082 1195 Both pieces of the revlog are written to in an append-only
1083 1196 fashion, which means we never need to rewrite a file to insert or
1084 1197 remove data, and can use some simple techniques to avoid the need
1085 1198 for locking while reading.
1086 1199
1087 1200 If checkambig, indexfile is opened with checkambig=True at
1088 1201 writing, to avoid file stat ambiguity.
1089 1202
1090 1203 If mmaplargeindex is True, and an mmapindexthreshold is set, the
1091 1204 index will be mmapped rather than read if it is larger than the
1092 1205 configured threshold.
1093 1206
1094 1207 If censorable is True, the revlog can have censored revisions.
1095 1208
1096 1209 If `upperboundcomp` is not None, this is the expected maximal gain from
1097 1210 compression for the data content.
1098 1211
1099 1212 `concurrencychecker` is an optional function that receives 3 arguments: a
1100 1213 file handle, a filename, and an expected position. It should check whether
1101 1214 the current position in the file handle is valid, and log/warn/fail (by
1102 1215 raising).
1103 1216
1104 1217 See mercurial/revlogutils/contants.py for details about the content of an
1105 1218 index entry.
1106 1219 """
1107 1220
1108 1221 _flagserrorclass = error.RevlogError
1109 1222
1110 1223 @staticmethod
1111 1224 def is_inline_index(header_bytes):
1112 1225 """Determine if a revlog is inline from the initial bytes of the index"""
1113 1226 header = INDEX_HEADER.unpack(header_bytes)[0]
1114 1227
1115 1228 _format_flags = header & ~0xFFFF
1116 1229 _format_version = header & 0xFFFF
1117 1230
1118 1231 features = FEATURES_BY_VERSION[_format_version]
1119 1232 return features[b'inline'](_format_flags)
1120 1233
1121 1234 def __init__(
1122 1235 self,
1123 1236 opener,
1124 1237 target,
1125 1238 radix,
1126 1239 postfix=None, # only exist for `tmpcensored` now
1127 1240 checkambig=False,
1128 1241 mmaplargeindex=False,
1129 1242 censorable=False,
1130 1243 upperboundcomp=None,
1131 1244 persistentnodemap=False,
1132 1245 concurrencychecker=None,
1133 1246 trypending=False,
1134 1247 try_split=False,
1135 1248 canonical_parent_order=True,
1136 1249 ):
1137 1250 """
1138 1251 create a revlog object
1139 1252
1140 1253 opener is a function that abstracts the file opening operation
1141 1254 and can be used to implement COW semantics or the like.
1142 1255
1143 1256 `target`: a (KIND, ID) tuple that identify the content stored in
1144 1257 this revlog. It help the rest of the code to understand what the revlog
1145 1258 is about without having to resort to heuristic and index filename
1146 1259 analysis. Note: that this must be reliably be set by normal code, but
1147 1260 that test, debug, or performance measurement code might not set this to
1148 1261 accurate value.
1149 1262 """
1150 1263
1151 1264 self.radix = radix
1152 1265
1153 1266 self._docket_file = None
1154 1267 self._indexfile = None
1155 1268 self._datafile = None
1156 1269 self._sidedatafile = None
1157 1270 self._nodemap_file = None
1158 1271 self.postfix = postfix
1159 1272 self._trypending = trypending
1160 1273 self._try_split = try_split
1161 1274 self.opener = opener
1162 1275 if persistentnodemap:
1163 1276 self._nodemap_file = nodemaputil.get_nodemap_file(self)
1164 1277
1165 1278 assert target[0] in ALL_KINDS
1166 1279 assert len(target) == 2
1167 1280 self.target = target
1168 1281 if b'feature-config' in self.opener.options:
1169 1282 self.feature_config = self.opener.options[b'feature-config'].copy()
1170 1283 else:
1171 1284 self.feature_config = FeatureConfig()
1172 1285 self.feature_config.censorable = censorable
1173 1286 self.feature_config.canonical_parent_order = canonical_parent_order
1174 1287 if b'data-config' in self.opener.options:
1175 1288 self.data_config = self.opener.options[b'data-config'].copy()
1176 1289 else:
1177 1290 self.data_config = DataConfig()
1178 1291 self.data_config.check_ambig = checkambig
1179 1292 self.data_config.mmap_large_index = mmaplargeindex
1180 1293 if b'delta-config' in self.opener.options:
1181 1294 self.delta_config = self.opener.options[b'delta-config'].copy()
1182 1295 else:
1183 1296 self.delta_config = DeltaConfig()
1184 1297 self.delta_config.upper_bound_comp = upperboundcomp
1185 1298
1186 1299 # Maps rev to chain base rev.
1187 1300 self._chainbasecache = util.lrucachedict(100)
1188 1301
1189 1302 self.index = None
1190 1303 self._docket = None
1191 1304 self._nodemap_docket = None
1192 1305 # Mapping of partial identifiers to full nodes.
1193 1306 self._pcache = {}
1194 1307
1195 1308 # other optionnals features
1196 1309
1197 1310 # Make copy of flag processors so each revlog instance can support
1198 1311 # custom flags.
1199 1312 self._flagprocessors = dict(flagutil.flagprocessors)
1200 1313 # prevent nesting of addgroup
1201 1314 self._adding_group = None
1202 1315
1203 1316 chunk_cache = self._loadindex()
1204 1317 self._load_inner(chunk_cache)
1205 1318 self._concurrencychecker = concurrencychecker
1206 1319
1207 1320 @property
1208 1321 def _generaldelta(self):
1209 1322 """temporary compatibility proxy"""
1210 1323 util.nouideprecwarn(
1211 1324 b"use revlog.delta_config.general_delta", b"6.6", stacklevel=2
1212 1325 )
1213 1326 return self.delta_config.general_delta
1214 1327
1215 1328 @property
1216 1329 def _checkambig(self):
1217 1330 """temporary compatibility proxy"""
1218 1331 util.nouideprecwarn(
1219 1332 b"use revlog.data_config.checkambig", b"6.6", stacklevel=2
1220 1333 )
1221 1334 return self.data_config.check_ambig
1222 1335
1223 1336 @property
1224 1337 def _mmaplargeindex(self):
1225 1338 """temporary compatibility proxy"""
1226 1339 util.nouideprecwarn(
1227 1340 b"use revlog.data_config.mmap_large_index", b"6.6", stacklevel=2
1228 1341 )
1229 1342 return self.data_config.mmap_large_index
1230 1343
1231 1344 @property
1232 1345 def _censorable(self):
1233 1346 """temporary compatibility proxy"""
1234 1347 util.nouideprecwarn(
1235 1348 b"use revlog.feature_config.censorable", b"6.6", stacklevel=2
1236 1349 )
1237 1350 return self.feature_config.censorable
1238 1351
1239 1352 @property
1240 1353 def _chunkcachesize(self):
1241 1354 """temporary compatibility proxy"""
1242 1355 util.nouideprecwarn(
1243 1356 b"use revlog.data_config.chunk_cache_size", b"6.6", stacklevel=2
1244 1357 )
1245 1358 return self.data_config.chunk_cache_size
1246 1359
1247 1360 @property
1248 1361 def _maxchainlen(self):
1249 1362 """temporary compatibility proxy"""
1250 1363 util.nouideprecwarn(
1251 1364 b"use revlog.delta_config.max_chain_len", b"6.6", stacklevel=2
1252 1365 )
1253 1366 return self.delta_config.max_chain_len
1254 1367
1255 1368 @property
1256 1369 def _deltabothparents(self):
1257 1370 """temporary compatibility proxy"""
1258 1371 util.nouideprecwarn(
1259 1372 b"use revlog.delta_config.delta_both_parents", b"6.6", stacklevel=2
1260 1373 )
1261 1374 return self.delta_config.delta_both_parents
1262 1375
1263 1376 @property
1264 1377 def _candidate_group_chunk_size(self):
1265 1378 """temporary compatibility proxy"""
1266 1379 util.nouideprecwarn(
1267 1380 b"use revlog.delta_config.candidate_group_chunk_size",
1268 1381 b"6.6",
1269 1382 stacklevel=2,
1270 1383 )
1271 1384 return self.delta_config.candidate_group_chunk_size
1272 1385
1273 1386 @property
1274 1387 def _debug_delta(self):
1275 1388 """temporary compatibility proxy"""
1276 1389 util.nouideprecwarn(
1277 1390 b"use revlog.delta_config.debug_delta", b"6.6", stacklevel=2
1278 1391 )
1279 1392 return self.delta_config.debug_delta
1280 1393
1281 1394 @property
1282 1395 def _compengine(self):
1283 1396 """temporary compatibility proxy"""
1284 1397 util.nouideprecwarn(
1285 1398 b"use revlog.feature_config.compression_engine",
1286 1399 b"6.6",
1287 1400 stacklevel=2,
1288 1401 )
1289 1402 return self.feature_config.compression_engine
1290 1403
1291 1404 @property
1292 1405 def upperboundcomp(self):
1293 1406 """temporary compatibility proxy"""
1294 1407 util.nouideprecwarn(
1295 1408 b"use revlog.delta_config.upper_bound_comp",
1296 1409 b"6.6",
1297 1410 stacklevel=2,
1298 1411 )
1299 1412 return self.delta_config.upper_bound_comp
1300 1413
1301 1414 @property
1302 1415 def _compengineopts(self):
1303 1416 """temporary compatibility proxy"""
1304 1417 util.nouideprecwarn(
1305 1418 b"use revlog.feature_config.compression_engine_options",
1306 1419 b"6.6",
1307 1420 stacklevel=2,
1308 1421 )
1309 1422 return self.feature_config.compression_engine_options
1310 1423
1311 1424 @property
1312 1425 def _maxdeltachainspan(self):
1313 1426 """temporary compatibility proxy"""
1314 1427 util.nouideprecwarn(
1315 1428 b"use revlog.delta_config.max_deltachain_span", b"6.6", stacklevel=2
1316 1429 )
1317 1430 return self.delta_config.max_deltachain_span
1318 1431
1319 1432 @property
1320 1433 def _withsparseread(self):
1321 1434 """temporary compatibility proxy"""
1322 1435 util.nouideprecwarn(
1323 1436 b"use revlog.data_config.with_sparse_read", b"6.6", stacklevel=2
1324 1437 )
1325 1438 return self.data_config.with_sparse_read
1326 1439
1327 1440 @property
1328 1441 def _sparserevlog(self):
1329 1442 """temporary compatibility proxy"""
1330 1443 util.nouideprecwarn(
1331 1444 b"use revlog.delta_config.sparse_revlog", b"6.6", stacklevel=2
1332 1445 )
1333 1446 return self.delta_config.sparse_revlog
1334 1447
1335 1448 @property
1336 1449 def hassidedata(self):
1337 1450 """temporary compatibility proxy"""
1338 1451 util.nouideprecwarn(
1339 1452 b"use revlog.feature_config.has_side_data", b"6.6", stacklevel=2
1340 1453 )
1341 1454 return self.feature_config.has_side_data
1342 1455
1343 1456 @property
1344 1457 def _srdensitythreshold(self):
1345 1458 """temporary compatibility proxy"""
1346 1459 util.nouideprecwarn(
1347 1460 b"use revlog.data_config.sr_density_threshold",
1348 1461 b"6.6",
1349 1462 stacklevel=2,
1350 1463 )
1351 1464 return self.data_config.sr_density_threshold
1352 1465
1353 1466 @property
1354 1467 def _srmingapsize(self):
1355 1468 """temporary compatibility proxy"""
1356 1469 util.nouideprecwarn(
1357 1470 b"use revlog.data_config.sr_min_gap_size", b"6.6", stacklevel=2
1358 1471 )
1359 1472 return self.data_config.sr_min_gap_size
1360 1473
1361 1474 @property
1362 1475 def _compute_rank(self):
1363 1476 """temporary compatibility proxy"""
1364 1477 util.nouideprecwarn(
1365 1478 b"use revlog.feature_config.compute_rank", b"6.6", stacklevel=2
1366 1479 )
1367 1480 return self.feature_config.compute_rank
1368 1481
1369 1482 @property
1370 1483 def canonical_parent_order(self):
1371 1484 """temporary compatibility proxy"""
1372 1485 util.nouideprecwarn(
1373 1486 b"use revlog.feature_config.canonical_parent_order",
1374 1487 b"6.6",
1375 1488 stacklevel=2,
1376 1489 )
1377 1490 return self.feature_config.canonical_parent_order
1378 1491
1379 1492 @property
1380 1493 def _lazydelta(self):
1381 1494 """temporary compatibility proxy"""
1382 1495 util.nouideprecwarn(
1383 1496 b"use revlog.delta_config.lazy_delta", b"6.6", stacklevel=2
1384 1497 )
1385 1498 return self.delta_config.lazy_delta
1386 1499
1387 1500 @property
1388 1501 def _lazydeltabase(self):
1389 1502 """temporary compatibility proxy"""
1390 1503 util.nouideprecwarn(
1391 1504 b"use revlog.delta_config.lazy_delta_base", b"6.6", stacklevel=2
1392 1505 )
1393 1506 return self.delta_config.lazy_delta_base
1394 1507
1395 1508 def _init_opts(self):
1396 1509 """process options (from above/config) to setup associated default revlog mode
1397 1510
1398 1511 These values might be affected when actually reading on disk information.
1399 1512
1400 1513 The relevant values are returned for use in _loadindex().
1401 1514
1402 1515 * newversionflags:
1403 1516 version header to use if we need to create a new revlog
1404 1517
1405 1518 * mmapindexthreshold:
1406 1519 minimal index size for start to use mmap
1407 1520
1408 1521 * force_nodemap:
1409 1522 force the usage of a "development" version of the nodemap code
1410 1523 """
1411 1524 opts = self.opener.options
1412 1525
1413 1526 if b'changelogv2' in opts and self.revlog_kind == KIND_CHANGELOG:
1414 1527 new_header = CHANGELOGV2
1415 1528 compute_rank = opts.get(b'changelogv2.compute-rank', True)
1416 1529 self.feature_config.compute_rank = compute_rank
1417 1530 elif b'revlogv2' in opts:
1418 1531 new_header = REVLOGV2
1419 1532 elif b'revlogv1' in opts:
1420 1533 new_header = REVLOGV1 | FLAG_INLINE_DATA
1421 1534 if b'generaldelta' in opts:
1422 1535 new_header |= FLAG_GENERALDELTA
1423 1536 elif b'revlogv0' in self.opener.options:
1424 1537 new_header = REVLOGV0
1425 1538 else:
1426 1539 new_header = REVLOG_DEFAULT_VERSION
1427 1540
1428 1541 mmapindexthreshold = None
1429 1542 if self.data_config.mmap_large_index:
1430 1543 mmapindexthreshold = self.data_config.mmap_index_threshold
1431 1544 if self.feature_config.enable_ellipsis:
1432 1545 self._flagprocessors[REVIDX_ELLIPSIS] = ellipsisprocessor
1433 1546
1434 1547 # revlog v0 doesn't have flag processors
1435 1548 for flag, processor in opts.get(b'flagprocessors', {}).items():
1436 1549 flagutil.insertflagprocessor(flag, processor, self._flagprocessors)
1437 1550
1438 1551 chunk_cache_size = self.data_config.chunk_cache_size
1439 1552 if chunk_cache_size <= 0:
1440 1553 raise error.RevlogError(
1441 1554 _(b'revlog chunk cache size %r is not greater than 0')
1442 1555 % chunk_cache_size
1443 1556 )
1444 1557 elif chunk_cache_size & (chunk_cache_size - 1):
1445 1558 raise error.RevlogError(
1446 1559 _(b'revlog chunk cache size %r is not a power of 2')
1447 1560 % chunk_cache_size
1448 1561 )
1449 1562 force_nodemap = opts.get(b'devel-force-nodemap', False)
1450 1563 return new_header, mmapindexthreshold, force_nodemap
1451 1564
1452 1565 def _get_data(self, filepath, mmap_threshold, size=None):
1453 1566 """return a file content with or without mmap
1454 1567
1455 1568 If the file is missing return the empty string"""
1456 1569 try:
1457 1570 with self.opener(filepath) as fp:
1458 1571 if mmap_threshold is not None:
1459 1572 file_size = self.opener.fstat(fp).st_size
1460 1573 if file_size >= mmap_threshold:
1461 1574 if size is not None:
1462 1575 # avoid potentiel mmap crash
1463 1576 size = min(file_size, size)
1464 1577 # TODO: should .close() to release resources without
1465 1578 # relying on Python GC
1466 1579 if size is None:
1467 1580 return util.buffer(util.mmapread(fp))
1468 1581 else:
1469 1582 return util.buffer(util.mmapread(fp, size))
1470 1583 if size is None:
1471 1584 return fp.read()
1472 1585 else:
1473 1586 return fp.read(size)
1474 1587 except FileNotFoundError:
1475 1588 return b''
1476 1589
1477 1590 def get_streams(self, max_linkrev, force_inline=False):
1478 1591 """return a list of streams that represent this revlog
1479 1592
1480 1593 This is used by stream-clone to do bytes to bytes copies of a repository.
1481 1594
1482 1595 This streams data for all revisions that refer to a changelog revision up
1483 1596 to `max_linkrev`.
1484 1597
1485 1598 If `force_inline` is set, it enforces that the stream will represent an inline revlog.
1486 1599
1487 1600 It returns is a list of three-tuple:
1488 1601
1489 1602 [
1490 1603 (filename, bytes_stream, stream_size),
1491 1604 …
1492 1605 ]
1493 1606 """
1494 1607 n = len(self)
1495 1608 index = self.index
1496 1609 while n > 0:
1497 1610 linkrev = index[n - 1][4]
1498 1611 if linkrev < max_linkrev:
1499 1612 break
1500 1613 # note: this loop will rarely go through multiple iterations, since
1501 1614 # it only traverses commits created during the current streaming
1502 1615 # pull operation.
1503 1616 #
1504 1617 # If this become a problem, using a binary search should cap the
1505 1618 # runtime of this.
1506 1619 n = n - 1
1507 1620 if n == 0:
1508 1621 # no data to send
1509 1622 return []
1510 1623 index_size = n * index.entry_size
1511 1624 data_size = self.end(n - 1)
1512 1625
1513 1626 # XXX we might have been split (or stripped) since the object
1514 1627 # initialization, We need to close this race too, but having a way to
1515 1628 # pre-open the file we feed to the revlog and never closing them before
1516 1629 # we are done streaming.
1517 1630
1518 1631 if self._inline:
1519 1632
1520 1633 def get_stream():
1521 1634 with self.opener(self._indexfile, mode=b"r") as fp:
1522 1635 yield None
1523 1636 size = index_size + data_size
1524 1637 if size <= 65536:
1525 1638 yield fp.read(size)
1526 1639 else:
1527 1640 yield from util.filechunkiter(fp, limit=size)
1528 1641
1529 1642 inline_stream = get_stream()
1530 1643 next(inline_stream)
1531 1644 return [
1532 1645 (self._indexfile, inline_stream, index_size + data_size),
1533 1646 ]
1534 1647 elif force_inline:
1535 1648
1536 1649 def get_stream():
1537 1650 with self.reading():
1538 1651 yield None
1539 1652
1540 1653 for rev in range(n):
1541 1654 idx = self.index.entry_binary(rev)
1542 1655 if rev == 0 and self._docket is None:
1543 1656 # re-inject the inline flag
1544 1657 header = self._format_flags
1545 1658 header |= self._format_version
1546 1659 header |= FLAG_INLINE_DATA
1547 1660 header = self.index.pack_header(header)
1548 1661 idx = header + idx
1549 1662 yield idx
1550 1663 yield self._inner.get_segment_for_revs(rev, rev)[1]
1551 1664
1552 1665 inline_stream = get_stream()
1553 1666 next(inline_stream)
1554 1667 return [
1555 1668 (self._indexfile, inline_stream, index_size + data_size),
1556 1669 ]
1557 1670 else:
1558 1671
1559 1672 def get_index_stream():
1560 1673 with self.opener(self._indexfile, mode=b"r") as fp:
1561 1674 yield None
1562 1675 if index_size <= 65536:
1563 1676 yield fp.read(index_size)
1564 1677 else:
1565 1678 yield from util.filechunkiter(fp, limit=index_size)
1566 1679
1567 1680 def get_data_stream():
1568 1681 with self._datafp() as fp:
1569 1682 yield None
1570 1683 if data_size <= 65536:
1571 1684 yield fp.read(data_size)
1572 1685 else:
1573 1686 yield from util.filechunkiter(fp, limit=data_size)
1574 1687
1575 1688 index_stream = get_index_stream()
1576 1689 next(index_stream)
1577 1690 data_stream = get_data_stream()
1578 1691 next(data_stream)
1579 1692 return [
1580 1693 (self._datafile, data_stream, data_size),
1581 1694 (self._indexfile, index_stream, index_size),
1582 1695 ]
1583 1696
1584 1697 def _loadindex(self, docket=None):
1585 1698
1586 1699 new_header, mmapindexthreshold, force_nodemap = self._init_opts()
1587 1700
1588 1701 if self.postfix is not None:
1589 1702 entry_point = b'%s.i.%s' % (self.radix, self.postfix)
1590 1703 elif self._trypending and self.opener.exists(b'%s.i.a' % self.radix):
1591 1704 entry_point = b'%s.i.a' % self.radix
1592 1705 elif self._try_split and self.opener.exists(self._split_index_file):
1593 1706 entry_point = self._split_index_file
1594 1707 else:
1595 1708 entry_point = b'%s.i' % self.radix
1596 1709
1597 1710 if docket is not None:
1598 1711 self._docket = docket
1599 1712 self._docket_file = entry_point
1600 1713 else:
1601 1714 self._initempty = True
1602 1715 entry_data = self._get_data(entry_point, mmapindexthreshold)
1603 1716 if len(entry_data) > 0:
1604 1717 header = INDEX_HEADER.unpack(entry_data[:4])[0]
1605 1718 self._initempty = False
1606 1719 else:
1607 1720 header = new_header
1608 1721
1609 1722 self._format_flags = header & ~0xFFFF
1610 1723 self._format_version = header & 0xFFFF
1611 1724
1612 1725 supported_flags = SUPPORTED_FLAGS.get(self._format_version)
1613 1726 if supported_flags is None:
1614 1727 msg = _(b'unknown version (%d) in revlog %s')
1615 1728 msg %= (self._format_version, self.display_id)
1616 1729 raise error.RevlogError(msg)
1617 1730 elif self._format_flags & ~supported_flags:
1618 1731 msg = _(b'unknown flags (%#04x) in version %d revlog %s')
1619 1732 display_flag = self._format_flags >> 16
1620 1733 msg %= (display_flag, self._format_version, self.display_id)
1621 1734 raise error.RevlogError(msg)
1622 1735
1623 1736 features = FEATURES_BY_VERSION[self._format_version]
1624 1737 self._inline = features[b'inline'](self._format_flags)
1625 1738 self.delta_config.general_delta = features[b'generaldelta'](
1626 1739 self._format_flags
1627 1740 )
1628 1741 self.feature_config.has_side_data = features[b'sidedata']
1629 1742
1630 1743 if not features[b'docket']:
1631 1744 self._indexfile = entry_point
1632 1745 index_data = entry_data
1633 1746 else:
1634 1747 self._docket_file = entry_point
1635 1748 if self._initempty:
1636 1749 self._docket = docketutil.default_docket(self, header)
1637 1750 else:
1638 1751 self._docket = docketutil.parse_docket(
1639 1752 self, entry_data, use_pending=self._trypending
1640 1753 )
1641 1754
1642 1755 if self._docket is not None:
1643 1756 self._indexfile = self._docket.index_filepath()
1644 1757 index_data = b''
1645 1758 index_size = self._docket.index_end
1646 1759 if index_size > 0:
1647 1760 index_data = self._get_data(
1648 1761 self._indexfile, mmapindexthreshold, size=index_size
1649 1762 )
1650 1763 if len(index_data) < index_size:
1651 1764 msg = _(b'too few index data for %s: got %d, expected %d')
1652 1765 msg %= (self.display_id, len(index_data), index_size)
1653 1766 raise error.RevlogError(msg)
1654 1767
1655 1768 self._inline = False
1656 1769 # generaldelta implied by version 2 revlogs.
1657 1770 self.delta_config.general_delta = True
1658 1771 # the logic for persistent nodemap will be dealt with within the
1659 1772 # main docket, so disable it for now.
1660 1773 self._nodemap_file = None
1661 1774
1662 1775 if self._docket is not None:
1663 1776 self._datafile = self._docket.data_filepath()
1664 1777 self._sidedatafile = self._docket.sidedata_filepath()
1665 1778 elif self.postfix is None:
1666 1779 self._datafile = b'%s.d' % self.radix
1667 1780 else:
1668 1781 self._datafile = b'%s.d.%s' % (self.radix, self.postfix)
1669 1782
1670 1783 self.nodeconstants = sha1nodeconstants
1671 1784 self.nullid = self.nodeconstants.nullid
1672 1785
1673 1786 # sparse-revlog can't be on without general-delta (issue6056)
1674 1787 if not self.delta_config.general_delta:
1675 1788 self.delta_config.sparse_revlog = False
1676 1789
1677 1790 self._storedeltachains = True
1678 1791
1679 1792 devel_nodemap = (
1680 1793 self._nodemap_file
1681 1794 and force_nodemap
1682 1795 and parse_index_v1_nodemap is not None
1683 1796 )
1684 1797
1685 1798 use_rust_index = False
1686 1799 if rustrevlog is not None:
1687 1800 if self._nodemap_file is not None:
1688 1801 use_rust_index = True
1689 1802 else:
1690 1803 use_rust_index = self.opener.options.get(b'rust.index')
1691 1804
1692 1805 self._parse_index = parse_index_v1
1693 1806 if self._format_version == REVLOGV0:
1694 1807 self._parse_index = revlogv0.parse_index_v0
1695 1808 elif self._format_version == REVLOGV2:
1696 1809 self._parse_index = parse_index_v2
1697 1810 elif self._format_version == CHANGELOGV2:
1698 1811 self._parse_index = parse_index_cl_v2
1699 1812 elif devel_nodemap:
1700 1813 self._parse_index = parse_index_v1_nodemap
1701 1814 elif use_rust_index:
1702 1815 self._parse_index = parse_index_v1_mixed
1703 1816 try:
1704 1817 d = self._parse_index(index_data, self._inline)
1705 1818 index, chunkcache = d
1706 1819 use_nodemap = (
1707 1820 not self._inline
1708 1821 and self._nodemap_file is not None
1709 1822 and hasattr(index, 'update_nodemap_data')
1710 1823 )
1711 1824 if use_nodemap:
1712 1825 nodemap_data = nodemaputil.persisted_data(self)
1713 1826 if nodemap_data is not None:
1714 1827 docket = nodemap_data[0]
1715 1828 if (
1716 1829 len(d[0]) > docket.tip_rev
1717 1830 and d[0][docket.tip_rev][7] == docket.tip_node
1718 1831 ):
1719 1832 # no changelog tampering
1720 1833 self._nodemap_docket = docket
1721 1834 index.update_nodemap_data(*nodemap_data)
1722 1835 except (ValueError, IndexError):
1723 1836 raise error.RevlogError(
1724 1837 _(b"index %s is corrupted") % self.display_id
1725 1838 )
1726 1839 self.index = index
1727 1840 # revnum -> (chain-length, sum-delta-length)
1728 1841 self._chaininfocache = util.lrucachedict(500)
1729 1842
1730 1843 return chunkcache
1731 1844
1732 1845 def _load_inner(self, chunk_cache):
1733 1846 if self._docket is None:
1734 1847 default_compression_header = None
1735 1848 else:
1736 1849 default_compression_header = self._docket.default_compression_header
1737 1850
1738 1851 self._inner = _InnerRevlog(
1739 1852 opener=self.opener,
1740 1853 index=self.index,
1741 1854 index_file=self._indexfile,
1742 1855 data_file=self._datafile,
1743 1856 sidedata_file=self._sidedatafile,
1744 1857 inline=self._inline,
1745 1858 data_config=self.data_config,
1746 1859 delta_config=self.delta_config,
1747 1860 feature_config=self.feature_config,
1748 1861 chunk_cache=chunk_cache,
1749 1862 default_compression_header=default_compression_header,
1750 1863 )
1751 1864
1752 1865 def get_revlog(self):
1753 1866 """simple function to mirror API of other not-really-revlog API"""
1754 1867 return self
1755 1868
1756 1869 @util.propertycache
1757 1870 def revlog_kind(self):
1758 1871 return self.target[0]
1759 1872
1760 1873 @util.propertycache
1761 1874 def display_id(self):
1762 1875 """The public facing "ID" of the revlog that we use in message"""
1763 1876 if self.revlog_kind == KIND_FILELOG:
1764 1877 # Reference the file without the "data/" prefix, so it is familiar
1765 1878 # to the user.
1766 1879 return self.target[1]
1767 1880 else:
1768 1881 return self.radix
1769 1882
1770 1883 def _datafp(self, mode=b'r'):
1771 1884 """file object for the revlog's data file"""
1772 1885 return self.opener(self._datafile, mode=mode)
1773 1886
1774 1887 def tiprev(self):
1775 1888 return len(self.index) - 1
1776 1889
1777 1890 def tip(self):
1778 1891 return self.node(self.tiprev())
1779 1892
1780 1893 def __contains__(self, rev):
1781 1894 return 0 <= rev < len(self)
1782 1895
1783 1896 def __len__(self):
1784 1897 return len(self.index)
1785 1898
1786 1899 def __iter__(self):
1787 1900 return iter(range(len(self)))
1788 1901
1789 1902 def revs(self, start=0, stop=None):
1790 1903 """iterate over all rev in this revlog (from start to stop)"""
1791 1904 return storageutil.iterrevs(len(self), start=start, stop=stop)
1792 1905
1793 1906 def hasnode(self, node):
1794 1907 try:
1795 1908 self.rev(node)
1796 1909 return True
1797 1910 except KeyError:
1798 1911 return False
1799 1912
1800 1913 def _candelta(self, baserev, rev):
1801 1914 """whether two revisions (baserev, rev) can be delta-ed or not"""
1802 1915 # Disable delta if either rev requires a content-changing flag
1803 1916 # processor (ex. LFS). This is because such flag processor can alter
1804 1917 # the rawtext content that the delta will be based on, and two clients
1805 1918 # could have a same revlog node with different flags (i.e. different
1806 1919 # rawtext contents) and the delta could be incompatible.
1807 1920 if (self.flags(baserev) & REVIDX_RAWTEXT_CHANGING_FLAGS) or (
1808 1921 self.flags(rev) & REVIDX_RAWTEXT_CHANGING_FLAGS
1809 1922 ):
1810 1923 return False
1811 1924 return True
1812 1925
1813 1926 def update_caches(self, transaction):
1814 1927 """update on disk cache
1815 1928
1816 1929 If a transaction is passed, the update may be delayed to transaction
1817 1930 commit."""
1818 1931 if self._nodemap_file is not None:
1819 1932 if transaction is None:
1820 1933 nodemaputil.update_persistent_nodemap(self)
1821 1934 else:
1822 1935 nodemaputil.setup_persistent_nodemap(transaction, self)
1823 1936
1824 1937 def clearcaches(self):
1825 1938 """Clear in-memory caches"""
1826 1939 self._chainbasecache.clear()
1827 1940 self._inner.clear_cache()
1828 1941 self._pcache = {}
1829 1942 self._nodemap_docket = None
1830 1943 self.index.clearcaches()
1831 1944 # The python code is the one responsible for validating the docket, we
1832 1945 # end up having to refresh it here.
1833 1946 use_nodemap = (
1834 1947 not self._inline
1835 1948 and self._nodemap_file is not None
1836 1949 and hasattr(self.index, 'update_nodemap_data')
1837 1950 )
1838 1951 if use_nodemap:
1839 1952 nodemap_data = nodemaputil.persisted_data(self)
1840 1953 if nodemap_data is not None:
1841 1954 self._nodemap_docket = nodemap_data[0]
1842 1955 self.index.update_nodemap_data(*nodemap_data)
1843 1956
1844 1957 def rev(self, node):
1845 1958 """return the revision number associated with a <nodeid>"""
1846 1959 try:
1847 1960 return self.index.rev(node)
1848 1961 except TypeError:
1849 1962 raise
1850 1963 except error.RevlogError:
1851 1964 # parsers.c radix tree lookup failed
1852 1965 if (
1853 1966 node == self.nodeconstants.wdirid
1854 1967 or node in self.nodeconstants.wdirfilenodeids
1855 1968 ):
1856 1969 raise error.WdirUnsupported
1857 1970 raise error.LookupError(node, self.display_id, _(b'no node'))
1858 1971
1859 1972 # Accessors for index entries.
1860 1973
1861 1974 # First tuple entry is 8 bytes. First 6 bytes are offset. Last 2 bytes
1862 1975 # are flags.
1863 1976 def start(self, rev):
1864 1977 return int(self.index[rev][0] >> 16)
1865 1978
1866 1979 def sidedata_cut_off(self, rev):
1867 1980 sd_cut_off = self.index[rev][8]
1868 1981 if sd_cut_off != 0:
1869 1982 return sd_cut_off
1870 1983 # This is some annoying dance, because entries without sidedata
1871 1984 # currently use 0 as their ofsset. (instead of previous-offset +
1872 1985 # previous-size)
1873 1986 #
1874 1987 # We should reconsider this sidedata β†’ 0 sidata_offset policy.
1875 1988 # In the meantime, we need this.
1876 1989 while 0 <= rev:
1877 1990 e = self.index[rev]
1878 1991 if e[9] != 0:
1879 1992 return e[8] + e[9]
1880 1993 rev -= 1
1881 1994 return 0
1882 1995
1883 1996 def flags(self, rev):
1884 1997 return self.index[rev][0] & 0xFFFF
1885 1998
1886 1999 def length(self, rev):
1887 2000 return self.index[rev][1]
1888 2001
1889 2002 def sidedata_length(self, rev):
1890 2003 if not self.feature_config.has_side_data:
1891 2004 return 0
1892 2005 return self.index[rev][9]
1893 2006
1894 2007 def rawsize(self, rev):
1895 2008 """return the length of the uncompressed text for a given revision"""
1896 2009 l = self.index[rev][2]
1897 2010 if l >= 0:
1898 2011 return l
1899 2012
1900 2013 t = self.rawdata(rev)
1901 2014 return len(t)
1902 2015
1903 2016 def size(self, rev):
1904 2017 """length of non-raw text (processed by a "read" flag processor)"""
1905 2018 # fast path: if no "read" flag processor could change the content,
1906 2019 # size is rawsize. note: ELLIPSIS is known to not change the content.
1907 2020 flags = self.flags(rev)
1908 2021 if flags & (flagutil.REVIDX_KNOWN_FLAGS ^ REVIDX_ELLIPSIS) == 0:
1909 2022 return self.rawsize(rev)
1910 2023
1911 2024 return len(self.revision(rev))
1912 2025
1913 2026 def fast_rank(self, rev):
1914 2027 """Return the rank of a revision if already known, or None otherwise.
1915 2028
1916 2029 The rank of a revision is the size of the sub-graph it defines as a
1917 2030 head. Equivalently, the rank of a revision `r` is the size of the set
1918 2031 `ancestors(r)`, `r` included.
1919 2032
1920 2033 This method returns the rank retrieved from the revlog in constant
1921 2034 time. It makes no attempt at computing unknown values for versions of
1922 2035 the revlog which do not persist the rank.
1923 2036 """
1924 2037 rank = self.index[rev][ENTRY_RANK]
1925 2038 if self._format_version != CHANGELOGV2 or rank == RANK_UNKNOWN:
1926 2039 return None
1927 2040 if rev == nullrev:
1928 2041 return 0 # convention
1929 2042 return rank
1930 2043
1931 2044 def chainbase(self, rev):
1932 2045 base = self._chainbasecache.get(rev)
1933 2046 if base is not None:
1934 2047 return base
1935 2048
1936 2049 index = self.index
1937 2050 iterrev = rev
1938 2051 base = index[iterrev][3]
1939 2052 while base != iterrev:
1940 2053 iterrev = base
1941 2054 base = index[iterrev][3]
1942 2055
1943 2056 self._chainbasecache[rev] = base
1944 2057 return base
1945 2058
1946 2059 def linkrev(self, rev):
1947 2060 return self.index[rev][4]
1948 2061
1949 2062 def parentrevs(self, rev):
1950 2063 try:
1951 2064 entry = self.index[rev]
1952 2065 except IndexError:
1953 2066 if rev == wdirrev:
1954 2067 raise error.WdirUnsupported
1955 2068 raise
1956 2069
1957 2070 if self.feature_config.canonical_parent_order and entry[5] == nullrev:
1958 2071 return entry[6], entry[5]
1959 2072 else:
1960 2073 return entry[5], entry[6]
1961 2074
1962 2075 # fast parentrevs(rev) where rev isn't filtered
1963 2076 _uncheckedparentrevs = parentrevs
1964 2077
1965 2078 def node(self, rev):
1966 2079 try:
1967 2080 return self.index[rev][7]
1968 2081 except IndexError:
1969 2082 if rev == wdirrev:
1970 2083 raise error.WdirUnsupported
1971 2084 raise
1972 2085
1973 2086 # Derived from index values.
1974 2087
1975 2088 def end(self, rev):
1976 2089 return self.start(rev) + self.length(rev)
1977 2090
1978 2091 def parents(self, node):
1979 2092 i = self.index
1980 2093 d = i[self.rev(node)]
1981 2094 # inline node() to avoid function call overhead
1982 2095 if self.feature_config.canonical_parent_order and d[5] == self.nullid:
1983 2096 return i[d[6]][7], i[d[5]][7]
1984 2097 else:
1985 2098 return i[d[5]][7], i[d[6]][7]
1986 2099
1987 2100 def chainlen(self, rev):
1988 2101 return self._chaininfo(rev)[0]
1989 2102
1990 2103 def _chaininfo(self, rev):
1991 2104 chaininfocache = self._chaininfocache
1992 2105 if rev in chaininfocache:
1993 2106 return chaininfocache[rev]
1994 2107 index = self.index
1995 2108 generaldelta = self.delta_config.general_delta
1996 2109 iterrev = rev
1997 2110 e = index[iterrev]
1998 2111 clen = 0
1999 2112 compresseddeltalen = 0
2000 2113 while iterrev != e[3]:
2001 2114 clen += 1
2002 2115 compresseddeltalen += e[1]
2003 2116 if generaldelta:
2004 2117 iterrev = e[3]
2005 2118 else:
2006 2119 iterrev -= 1
2007 2120 if iterrev in chaininfocache:
2008 2121 t = chaininfocache[iterrev]
2009 2122 clen += t[0]
2010 2123 compresseddeltalen += t[1]
2011 2124 break
2012 2125 e = index[iterrev]
2013 2126 else:
2014 2127 # Add text length of base since decompressing that also takes
2015 2128 # work. For cache hits the length is already included.
2016 2129 compresseddeltalen += e[1]
2017 2130 r = (clen, compresseddeltalen)
2018 2131 chaininfocache[rev] = r
2019 2132 return r
2020 2133
2021 2134 def _deltachain(self, rev, stoprev=None):
2022 2135 return self._inner._deltachain(rev, stoprev=stoprev)
2023 2136
2024 2137 def ancestors(self, revs, stoprev=0, inclusive=False):
2025 2138 """Generate the ancestors of 'revs' in reverse revision order.
2026 2139 Does not generate revs lower than stoprev.
2027 2140
2028 2141 See the documentation for ancestor.lazyancestors for more details."""
2029 2142
2030 2143 # first, make sure start revisions aren't filtered
2031 2144 revs = list(revs)
2032 2145 checkrev = self.node
2033 2146 for r in revs:
2034 2147 checkrev(r)
2035 2148 # and we're sure ancestors aren't filtered as well
2036 2149
2037 2150 if rustancestor is not None and self.index.rust_ext_compat:
2038 2151 lazyancestors = rustancestor.LazyAncestors
2039 2152 arg = self.index
2040 2153 else:
2041 2154 lazyancestors = ancestor.lazyancestors
2042 2155 arg = self._uncheckedparentrevs
2043 2156 return lazyancestors(arg, revs, stoprev=stoprev, inclusive=inclusive)
2044 2157
2045 2158 def descendants(self, revs):
2046 2159 return dagop.descendantrevs(revs, self.revs, self.parentrevs)
2047 2160
2048 2161 def findcommonmissing(self, common=None, heads=None):
2049 2162 """Return a tuple of the ancestors of common and the ancestors of heads
2050 2163 that are not ancestors of common. In revset terminology, we return the
2051 2164 tuple:
2052 2165
2053 2166 ::common, (::heads) - (::common)
2054 2167
2055 2168 The list is sorted by revision number, meaning it is
2056 2169 topologically sorted.
2057 2170
2058 2171 'heads' and 'common' are both lists of node IDs. If heads is
2059 2172 not supplied, uses all of the revlog's heads. If common is not
2060 2173 supplied, uses nullid."""
2061 2174 if common is None:
2062 2175 common = [self.nullid]
2063 2176 if heads is None:
2064 2177 heads = self.heads()
2065 2178
2066 2179 common = [self.rev(n) for n in common]
2067 2180 heads = [self.rev(n) for n in heads]
2068 2181
2069 2182 # we want the ancestors, but inclusive
2070 2183 class lazyset:
2071 2184 def __init__(self, lazyvalues):
2072 2185 self.addedvalues = set()
2073 2186 self.lazyvalues = lazyvalues
2074 2187
2075 2188 def __contains__(self, value):
2076 2189 return value in self.addedvalues or value in self.lazyvalues
2077 2190
2078 2191 def __iter__(self):
2079 2192 added = self.addedvalues
2080 2193 for r in added:
2081 2194 yield r
2082 2195 for r in self.lazyvalues:
2083 2196 if not r in added:
2084 2197 yield r
2085 2198
2086 2199 def add(self, value):
2087 2200 self.addedvalues.add(value)
2088 2201
2089 2202 def update(self, values):
2090 2203 self.addedvalues.update(values)
2091 2204
2092 2205 has = lazyset(self.ancestors(common))
2093 2206 has.add(nullrev)
2094 2207 has.update(common)
2095 2208
2096 2209 # take all ancestors from heads that aren't in has
2097 2210 missing = set()
2098 2211 visit = collections.deque(r for r in heads if r not in has)
2099 2212 while visit:
2100 2213 r = visit.popleft()
2101 2214 if r in missing:
2102 2215 continue
2103 2216 else:
2104 2217 missing.add(r)
2105 2218 for p in self.parentrevs(r):
2106 2219 if p not in has:
2107 2220 visit.append(p)
2108 2221 missing = list(missing)
2109 2222 missing.sort()
2110 2223 return has, [self.node(miss) for miss in missing]
2111 2224
2112 2225 def incrementalmissingrevs(self, common=None):
2113 2226 """Return an object that can be used to incrementally compute the
2114 2227 revision numbers of the ancestors of arbitrary sets that are not
2115 2228 ancestors of common. This is an ancestor.incrementalmissingancestors
2116 2229 object.
2117 2230
2118 2231 'common' is a list of revision numbers. If common is not supplied, uses
2119 2232 nullrev.
2120 2233 """
2121 2234 if common is None:
2122 2235 common = [nullrev]
2123 2236
2124 2237 if rustancestor is not None and self.index.rust_ext_compat:
2125 2238 return rustancestor.MissingAncestors(self.index, common)
2126 2239 return ancestor.incrementalmissingancestors(self.parentrevs, common)
2127 2240
2128 2241 def findmissingrevs(self, common=None, heads=None):
2129 2242 """Return the revision numbers of the ancestors of heads that
2130 2243 are not ancestors of common.
2131 2244
2132 2245 More specifically, return a list of revision numbers corresponding to
2133 2246 nodes N such that every N satisfies the following constraints:
2134 2247
2135 2248 1. N is an ancestor of some node in 'heads'
2136 2249 2. N is not an ancestor of any node in 'common'
2137 2250
2138 2251 The list is sorted by revision number, meaning it is
2139 2252 topologically sorted.
2140 2253
2141 2254 'heads' and 'common' are both lists of revision numbers. If heads is
2142 2255 not supplied, uses all of the revlog's heads. If common is not
2143 2256 supplied, uses nullid."""
2144 2257 if common is None:
2145 2258 common = [nullrev]
2146 2259 if heads is None:
2147 2260 heads = self.headrevs()
2148 2261
2149 2262 inc = self.incrementalmissingrevs(common=common)
2150 2263 return inc.missingancestors(heads)
2151 2264
2152 2265 def findmissing(self, common=None, heads=None):
2153 2266 """Return the ancestors of heads that are not ancestors of common.
2154 2267
2155 2268 More specifically, return a list of nodes N such that every N
2156 2269 satisfies the following constraints:
2157 2270
2158 2271 1. N is an ancestor of some node in 'heads'
2159 2272 2. N is not an ancestor of any node in 'common'
2160 2273
2161 2274 The list is sorted by revision number, meaning it is
2162 2275 topologically sorted.
2163 2276
2164 2277 'heads' and 'common' are both lists of node IDs. If heads is
2165 2278 not supplied, uses all of the revlog's heads. If common is not
2166 2279 supplied, uses nullid."""
2167 2280 if common is None:
2168 2281 common = [self.nullid]
2169 2282 if heads is None:
2170 2283 heads = self.heads()
2171 2284
2172 2285 common = [self.rev(n) for n in common]
2173 2286 heads = [self.rev(n) for n in heads]
2174 2287
2175 2288 inc = self.incrementalmissingrevs(common=common)
2176 2289 return [self.node(r) for r in inc.missingancestors(heads)]
2177 2290
2178 2291 def nodesbetween(self, roots=None, heads=None):
2179 2292 """Return a topological path from 'roots' to 'heads'.
2180 2293
2181 2294 Return a tuple (nodes, outroots, outheads) where 'nodes' is a
2182 2295 topologically sorted list of all nodes N that satisfy both of
2183 2296 these constraints:
2184 2297
2185 2298 1. N is a descendant of some node in 'roots'
2186 2299 2. N is an ancestor of some node in 'heads'
2187 2300
2188 2301 Every node is considered to be both a descendant and an ancestor
2189 2302 of itself, so every reachable node in 'roots' and 'heads' will be
2190 2303 included in 'nodes'.
2191 2304
2192 2305 'outroots' is the list of reachable nodes in 'roots', i.e., the
2193 2306 subset of 'roots' that is returned in 'nodes'. Likewise,
2194 2307 'outheads' is the subset of 'heads' that is also in 'nodes'.
2195 2308
2196 2309 'roots' and 'heads' are both lists of node IDs. If 'roots' is
2197 2310 unspecified, uses nullid as the only root. If 'heads' is
2198 2311 unspecified, uses list of all of the revlog's heads."""
2199 2312 nonodes = ([], [], [])
2200 2313 if roots is not None:
2201 2314 roots = list(roots)
2202 2315 if not roots:
2203 2316 return nonodes
2204 2317 lowestrev = min([self.rev(n) for n in roots])
2205 2318 else:
2206 2319 roots = [self.nullid] # Everybody's a descendant of nullid
2207 2320 lowestrev = nullrev
2208 2321 if (lowestrev == nullrev) and (heads is None):
2209 2322 # We want _all_ the nodes!
2210 2323 return (
2211 2324 [self.node(r) for r in self],
2212 2325 [self.nullid],
2213 2326 list(self.heads()),
2214 2327 )
2215 2328 if heads is None:
2216 2329 # All nodes are ancestors, so the latest ancestor is the last
2217 2330 # node.
2218 2331 highestrev = len(self) - 1
2219 2332 # Set ancestors to None to signal that every node is an ancestor.
2220 2333 ancestors = None
2221 2334 # Set heads to an empty dictionary for later discovery of heads
2222 2335 heads = {}
2223 2336 else:
2224 2337 heads = list(heads)
2225 2338 if not heads:
2226 2339 return nonodes
2227 2340 ancestors = set()
2228 2341 # Turn heads into a dictionary so we can remove 'fake' heads.
2229 2342 # Also, later we will be using it to filter out the heads we can't
2230 2343 # find from roots.
2231 2344 heads = dict.fromkeys(heads, False)
2232 2345 # Start at the top and keep marking parents until we're done.
2233 2346 nodestotag = set(heads)
2234 2347 # Remember where the top was so we can use it as a limit later.
2235 2348 highestrev = max([self.rev(n) for n in nodestotag])
2236 2349 while nodestotag:
2237 2350 # grab a node to tag
2238 2351 n = nodestotag.pop()
2239 2352 # Never tag nullid
2240 2353 if n == self.nullid:
2241 2354 continue
2242 2355 # A node's revision number represents its place in a
2243 2356 # topologically sorted list of nodes.
2244 2357 r = self.rev(n)
2245 2358 if r >= lowestrev:
2246 2359 if n not in ancestors:
2247 2360 # If we are possibly a descendant of one of the roots
2248 2361 # and we haven't already been marked as an ancestor
2249 2362 ancestors.add(n) # Mark as ancestor
2250 2363 # Add non-nullid parents to list of nodes to tag.
2251 2364 nodestotag.update(
2252 2365 [p for p in self.parents(n) if p != self.nullid]
2253 2366 )
2254 2367 elif n in heads: # We've seen it before, is it a fake head?
2255 2368 # So it is, real heads should not be the ancestors of
2256 2369 # any other heads.
2257 2370 heads.pop(n)
2258 2371 if not ancestors:
2259 2372 return nonodes
2260 2373 # Now that we have our set of ancestors, we want to remove any
2261 2374 # roots that are not ancestors.
2262 2375
2263 2376 # If one of the roots was nullid, everything is included anyway.
2264 2377 if lowestrev > nullrev:
2265 2378 # But, since we weren't, let's recompute the lowest rev to not
2266 2379 # include roots that aren't ancestors.
2267 2380
2268 2381 # Filter out roots that aren't ancestors of heads
2269 2382 roots = [root for root in roots if root in ancestors]
2270 2383 # Recompute the lowest revision
2271 2384 if roots:
2272 2385 lowestrev = min([self.rev(root) for root in roots])
2273 2386 else:
2274 2387 # No more roots? Return empty list
2275 2388 return nonodes
2276 2389 else:
2277 2390 # We are descending from nullid, and don't need to care about
2278 2391 # any other roots.
2279 2392 lowestrev = nullrev
2280 2393 roots = [self.nullid]
2281 2394 # Transform our roots list into a set.
2282 2395 descendants = set(roots)
2283 2396 # Also, keep the original roots so we can filter out roots that aren't
2284 2397 # 'real' roots (i.e. are descended from other roots).
2285 2398 roots = descendants.copy()
2286 2399 # Our topologically sorted list of output nodes.
2287 2400 orderedout = []
2288 2401 # Don't start at nullid since we don't want nullid in our output list,
2289 2402 # and if nullid shows up in descendants, empty parents will look like
2290 2403 # they're descendants.
2291 2404 for r in self.revs(start=max(lowestrev, 0), stop=highestrev + 1):
2292 2405 n = self.node(r)
2293 2406 isdescendant = False
2294 2407 if lowestrev == nullrev: # Everybody is a descendant of nullid
2295 2408 isdescendant = True
2296 2409 elif n in descendants:
2297 2410 # n is already a descendant
2298 2411 isdescendant = True
2299 2412 # This check only needs to be done here because all the roots
2300 2413 # will start being marked is descendants before the loop.
2301 2414 if n in roots:
2302 2415 # If n was a root, check if it's a 'real' root.
2303 2416 p = tuple(self.parents(n))
2304 2417 # If any of its parents are descendants, it's not a root.
2305 2418 if (p[0] in descendants) or (p[1] in descendants):
2306 2419 roots.remove(n)
2307 2420 else:
2308 2421 p = tuple(self.parents(n))
2309 2422 # A node is a descendant if either of its parents are
2310 2423 # descendants. (We seeded the dependents list with the roots
2311 2424 # up there, remember?)
2312 2425 if (p[0] in descendants) or (p[1] in descendants):
2313 2426 descendants.add(n)
2314 2427 isdescendant = True
2315 2428 if isdescendant and ((ancestors is None) or (n in ancestors)):
2316 2429 # Only include nodes that are both descendants and ancestors.
2317 2430 orderedout.append(n)
2318 2431 if (ancestors is not None) and (n in heads):
2319 2432 # We're trying to figure out which heads are reachable
2320 2433 # from roots.
2321 2434 # Mark this head as having been reached
2322 2435 heads[n] = True
2323 2436 elif ancestors is None:
2324 2437 # Otherwise, we're trying to discover the heads.
2325 2438 # Assume this is a head because if it isn't, the next step
2326 2439 # will eventually remove it.
2327 2440 heads[n] = True
2328 2441 # But, obviously its parents aren't.
2329 2442 for p in self.parents(n):
2330 2443 heads.pop(p, None)
2331 2444 heads = [head for head, flag in heads.items() if flag]
2332 2445 roots = list(roots)
2333 2446 assert orderedout
2334 2447 assert roots
2335 2448 assert heads
2336 2449 return (orderedout, roots, heads)
2337 2450
2338 2451 def headrevs(self, revs=None):
2339 2452 if revs is None:
2340 2453 try:
2341 2454 return self.index.headrevs()
2342 2455 except AttributeError:
2343 2456 return self._headrevs()
2344 2457 if rustdagop is not None and self.index.rust_ext_compat:
2345 2458 return rustdagop.headrevs(self.index, revs)
2346 2459 return dagop.headrevs(revs, self._uncheckedparentrevs)
2347 2460
2348 2461 def computephases(self, roots):
2349 2462 return self.index.computephasesmapsets(roots)
2350 2463
2351 2464 def _headrevs(self):
2352 2465 count = len(self)
2353 2466 if not count:
2354 2467 return [nullrev]
2355 2468 # we won't iter over filtered rev so nobody is a head at start
2356 2469 ishead = [0] * (count + 1)
2357 2470 index = self.index
2358 2471 for r in self:
2359 2472 ishead[r] = 1 # I may be an head
2360 2473 e = index[r]
2361 2474 ishead[e[5]] = ishead[e[6]] = 0 # my parent are not
2362 2475 return [r for r, val in enumerate(ishead) if val]
2363 2476
2364 2477 def heads(self, start=None, stop=None):
2365 2478 """return the list of all nodes that have no children
2366 2479
2367 2480 if start is specified, only heads that are descendants of
2368 2481 start will be returned
2369 2482 if stop is specified, it will consider all the revs from stop
2370 2483 as if they had no children
2371 2484 """
2372 2485 if start is None and stop is None:
2373 2486 if not len(self):
2374 2487 return [self.nullid]
2375 2488 return [self.node(r) for r in self.headrevs()]
2376 2489
2377 2490 if start is None:
2378 2491 start = nullrev
2379 2492 else:
2380 2493 start = self.rev(start)
2381 2494
2382 2495 stoprevs = {self.rev(n) for n in stop or []}
2383 2496
2384 2497 revs = dagop.headrevssubset(
2385 2498 self.revs, self.parentrevs, startrev=start, stoprevs=stoprevs
2386 2499 )
2387 2500
2388 2501 return [self.node(rev) for rev in revs]
2389 2502
2390 2503 def children(self, node):
2391 2504 """find the children of a given node"""
2392 2505 c = []
2393 2506 p = self.rev(node)
2394 2507 for r in self.revs(start=p + 1):
2395 2508 prevs = [pr for pr in self.parentrevs(r) if pr != nullrev]
2396 2509 if prevs:
2397 2510 for pr in prevs:
2398 2511 if pr == p:
2399 2512 c.append(self.node(r))
2400 2513 elif p == nullrev:
2401 2514 c.append(self.node(r))
2402 2515 return c
2403 2516
2404 2517 def commonancestorsheads(self, a, b):
2405 2518 """calculate all the heads of the common ancestors of nodes a and b"""
2406 2519 a, b = self.rev(a), self.rev(b)
2407 2520 ancs = self._commonancestorsheads(a, b)
2408 2521 return pycompat.maplist(self.node, ancs)
2409 2522
2410 2523 def _commonancestorsheads(self, *revs):
2411 2524 """calculate all the heads of the common ancestors of revs"""
2412 2525 try:
2413 2526 ancs = self.index.commonancestorsheads(*revs)
2414 2527 except (AttributeError, OverflowError): # C implementation failed
2415 2528 ancs = ancestor.commonancestorsheads(self.parentrevs, *revs)
2416 2529 return ancs
2417 2530
2418 2531 def isancestor(self, a, b):
2419 2532 """return True if node a is an ancestor of node b
2420 2533
2421 2534 A revision is considered an ancestor of itself."""
2422 2535 a, b = self.rev(a), self.rev(b)
2423 2536 return self.isancestorrev(a, b)
2424 2537
2425 2538 def isancestorrev(self, a, b):
2426 2539 """return True if revision a is an ancestor of revision b
2427 2540
2428 2541 A revision is considered an ancestor of itself.
2429 2542
2430 2543 The implementation of this is trivial but the use of
2431 2544 reachableroots is not."""
2432 2545 if a == nullrev:
2433 2546 return True
2434 2547 elif a == b:
2435 2548 return True
2436 2549 elif a > b:
2437 2550 return False
2438 2551 return bool(self.reachableroots(a, [b], [a], includepath=False))
2439 2552
2440 2553 def reachableroots(self, minroot, heads, roots, includepath=False):
2441 2554 """return (heads(::(<roots> and <roots>::<heads>)))
2442 2555
2443 2556 If includepath is True, return (<roots>::<heads>)."""
2444 2557 try:
2445 2558 return self.index.reachableroots2(
2446 2559 minroot, heads, roots, includepath
2447 2560 )
2448 2561 except AttributeError:
2449 2562 return dagop._reachablerootspure(
2450 2563 self.parentrevs, minroot, roots, heads, includepath
2451 2564 )
2452 2565
2453 2566 def ancestor(self, a, b):
2454 2567 """calculate the "best" common ancestor of nodes a and b"""
2455 2568
2456 2569 a, b = self.rev(a), self.rev(b)
2457 2570 try:
2458 2571 ancs = self.index.ancestors(a, b)
2459 2572 except (AttributeError, OverflowError):
2460 2573 ancs = ancestor.ancestors(self.parentrevs, a, b)
2461 2574 if ancs:
2462 2575 # choose a consistent winner when there's a tie
2463 2576 return min(map(self.node, ancs))
2464 2577 return self.nullid
2465 2578
2466 2579 def _match(self, id):
2467 2580 if isinstance(id, int):
2468 2581 # rev
2469 2582 return self.node(id)
2470 2583 if len(id) == self.nodeconstants.nodelen:
2471 2584 # possibly a binary node
2472 2585 # odds of a binary node being all hex in ASCII are 1 in 10**25
2473 2586 try:
2474 2587 node = id
2475 2588 self.rev(node) # quick search the index
2476 2589 return node
2477 2590 except error.LookupError:
2478 2591 pass # may be partial hex id
2479 2592 try:
2480 2593 # str(rev)
2481 2594 rev = int(id)
2482 2595 if b"%d" % rev != id:
2483 2596 raise ValueError
2484 2597 if rev < 0:
2485 2598 rev = len(self) + rev
2486 2599 if rev < 0 or rev >= len(self):
2487 2600 raise ValueError
2488 2601 return self.node(rev)
2489 2602 except (ValueError, OverflowError):
2490 2603 pass
2491 2604 if len(id) == 2 * self.nodeconstants.nodelen:
2492 2605 try:
2493 2606 # a full hex nodeid?
2494 2607 node = bin(id)
2495 2608 self.rev(node)
2496 2609 return node
2497 2610 except (binascii.Error, error.LookupError):
2498 2611 pass
2499 2612
2500 2613 def _partialmatch(self, id):
2501 2614 # we don't care wdirfilenodeids as they should be always full hash
2502 2615 maybewdir = self.nodeconstants.wdirhex.startswith(id)
2503 2616 ambiguous = False
2504 2617 try:
2505 2618 partial = self.index.partialmatch(id)
2506 2619 if partial and self.hasnode(partial):
2507 2620 if maybewdir:
2508 2621 # single 'ff...' match in radix tree, ambiguous with wdir
2509 2622 ambiguous = True
2510 2623 else:
2511 2624 return partial
2512 2625 elif maybewdir:
2513 2626 # no 'ff...' match in radix tree, wdir identified
2514 2627 raise error.WdirUnsupported
2515 2628 else:
2516 2629 return None
2517 2630 except error.RevlogError:
2518 2631 # parsers.c radix tree lookup gave multiple matches
2519 2632 # fast path: for unfiltered changelog, radix tree is accurate
2520 2633 if not getattr(self, 'filteredrevs', None):
2521 2634 ambiguous = True
2522 2635 # fall through to slow path that filters hidden revisions
2523 2636 except (AttributeError, ValueError):
2524 2637 # we are pure python, or key is not hex
2525 2638 pass
2526 2639 if ambiguous:
2527 2640 raise error.AmbiguousPrefixLookupError(
2528 2641 id, self.display_id, _(b'ambiguous identifier')
2529 2642 )
2530 2643
2531 2644 if id in self._pcache:
2532 2645 return self._pcache[id]
2533 2646
2534 2647 if len(id) <= 40:
2535 2648 # hex(node)[:...]
2536 2649 l = len(id) // 2 * 2 # grab an even number of digits
2537 2650 try:
2538 2651 # we're dropping the last digit, so let's check that it's hex,
2539 2652 # to avoid the expensive computation below if it's not
2540 2653 if len(id) % 2 > 0:
2541 2654 if not (id[-1] in hexdigits):
2542 2655 return None
2543 2656 prefix = bin(id[:l])
2544 2657 except binascii.Error:
2545 2658 pass
2546 2659 else:
2547 2660 nl = [e[7] for e in self.index if e[7].startswith(prefix)]
2548 2661 nl = [
2549 2662 n for n in nl if hex(n).startswith(id) and self.hasnode(n)
2550 2663 ]
2551 2664 if self.nodeconstants.nullhex.startswith(id):
2552 2665 nl.append(self.nullid)
2553 2666 if len(nl) > 0:
2554 2667 if len(nl) == 1 and not maybewdir:
2555 2668 self._pcache[id] = nl[0]
2556 2669 return nl[0]
2557 2670 raise error.AmbiguousPrefixLookupError(
2558 2671 id, self.display_id, _(b'ambiguous identifier')
2559 2672 )
2560 2673 if maybewdir:
2561 2674 raise error.WdirUnsupported
2562 2675 return None
2563 2676
2564 2677 def lookup(self, id):
2565 2678 """locate a node based on:
2566 2679 - revision number or str(revision number)
2567 2680 - nodeid or subset of hex nodeid
2568 2681 """
2569 2682 n = self._match(id)
2570 2683 if n is not None:
2571 2684 return n
2572 2685 n = self._partialmatch(id)
2573 2686 if n:
2574 2687 return n
2575 2688
2576 2689 raise error.LookupError(id, self.display_id, _(b'no match found'))
2577 2690
2578 2691 def shortest(self, node, minlength=1):
2579 2692 """Find the shortest unambiguous prefix that matches node."""
2580 2693
2581 2694 def isvalid(prefix):
2582 2695 try:
2583 2696 matchednode = self._partialmatch(prefix)
2584 2697 except error.AmbiguousPrefixLookupError:
2585 2698 return False
2586 2699 except error.WdirUnsupported:
2587 2700 # single 'ff...' match
2588 2701 return True
2589 2702 if matchednode is None:
2590 2703 raise error.LookupError(node, self.display_id, _(b'no node'))
2591 2704 return True
2592 2705
2593 2706 def maybewdir(prefix):
2594 2707 return all(c == b'f' for c in pycompat.iterbytestr(prefix))
2595 2708
2596 2709 hexnode = hex(node)
2597 2710
2598 2711 def disambiguate(hexnode, minlength):
2599 2712 """Disambiguate against wdirid."""
2600 2713 for length in range(minlength, len(hexnode) + 1):
2601 2714 prefix = hexnode[:length]
2602 2715 if not maybewdir(prefix):
2603 2716 return prefix
2604 2717
2605 2718 if not getattr(self, 'filteredrevs', None):
2606 2719 try:
2607 2720 length = max(self.index.shortest(node), minlength)
2608 2721 return disambiguate(hexnode, length)
2609 2722 except error.RevlogError:
2610 2723 if node != self.nodeconstants.wdirid:
2611 2724 raise error.LookupError(
2612 2725 node, self.display_id, _(b'no node')
2613 2726 )
2614 2727 except AttributeError:
2615 2728 # Fall through to pure code
2616 2729 pass
2617 2730
2618 2731 if node == self.nodeconstants.wdirid:
2619 2732 for length in range(minlength, len(hexnode) + 1):
2620 2733 prefix = hexnode[:length]
2621 2734 if isvalid(prefix):
2622 2735 return prefix
2623 2736
2624 2737 for length in range(minlength, len(hexnode) + 1):
2625 2738 prefix = hexnode[:length]
2626 2739 if isvalid(prefix):
2627 2740 return disambiguate(hexnode, length)
2628 2741
2629 2742 def cmp(self, node, text):
2630 2743 """compare text with a given file revision
2631 2744
2632 2745 returns True if text is different than what is stored.
2633 2746 """
2634 2747 p1, p2 = self.parents(node)
2635 2748 return storageutil.hashrevisionsha1(text, p1, p2) != node
2636 2749
2637 2750 def deltaparent(self, rev):
2638 2751 """return deltaparent of the given revision"""
2639 2752 base = self.index[rev][3]
2640 2753 if base == rev:
2641 2754 return nullrev
2642 2755 elif self.delta_config.general_delta:
2643 2756 return base
2644 2757 else:
2645 2758 return rev - 1
2646 2759
2647 2760 def issnapshot(self, rev):
2648 2761 """tells whether rev is a snapshot"""
2649 2762 ret = self._inner.issnapshot(rev)
2650 2763 self.issnapshot = self._inner.issnapshot
2651 2764 return ret
2652 2765
2653 2766 def snapshotdepth(self, rev):
2654 2767 """number of snapshot in the chain before this one"""
2655 2768 if not self.issnapshot(rev):
2656 2769 raise error.ProgrammingError(b'revision %d not a snapshot')
2657 2770 return len(self._inner._deltachain(rev)[0]) - 1
2658 2771
2659 2772 def revdiff(self, rev1, rev2):
2660 2773 """return or calculate a delta between two revisions
2661 2774
2662 2775 The delta calculated is in binary form and is intended to be written to
2663 2776 revlog data directly. So this function needs raw revision data.
2664 2777 """
2665 2778 if rev1 != nullrev and self.deltaparent(rev2) == rev1:
2666 2779 return bytes(self._inner._chunk(rev2))
2667 2780
2668 2781 return mdiff.textdiff(self.rawdata(rev1), self.rawdata(rev2))
2669 2782
2670 2783 def revision(self, nodeorrev):
2671 2784 """return an uncompressed revision of a given node or revision
2672 2785 number.
2673 2786 """
2674 2787 return self._revisiondata(nodeorrev)
2675 2788
2676 2789 def sidedata(self, nodeorrev):
2677 2790 """a map of extra data related to the changeset but not part of the hash
2678 2791
2679 2792 This function currently return a dictionary. However, more advanced
2680 2793 mapping object will likely be used in the future for a more
2681 2794 efficient/lazy code.
2682 2795 """
2683 2796 # deal with <nodeorrev> argument type
2684 2797 if isinstance(nodeorrev, int):
2685 2798 rev = nodeorrev
2686 2799 else:
2687 2800 rev = self.rev(nodeorrev)
2688 2801 return self._sidedata(rev)
2689 2802
2690 2803 def _rawtext(self, node, rev):
2691 2804 """return the possibly unvalidated rawtext for a revision
2692 2805
2693 2806 returns (rev, rawtext, validated)
2694 2807 """
2695 2808 # Check if we have the entry in cache
2696 2809 # The cache entry looks like (node, rev, rawtext)
2697 2810 if self._inner._revisioncache:
2698 2811 if self._inner._revisioncache[0] == node:
2699 2812 return (rev, self._inner._revisioncache[2], True)
2700 2813
2701 2814 if rev is None:
2702 2815 rev = self.rev(node)
2703 2816
2704 2817 return self._inner.raw_text(node, rev)
2705 2818
2706 2819 def _revisiondata(self, nodeorrev, raw=False):
2707 2820 # deal with <nodeorrev> argument type
2708 2821 if isinstance(nodeorrev, int):
2709 2822 rev = nodeorrev
2710 2823 node = self.node(rev)
2711 2824 else:
2712 2825 node = nodeorrev
2713 2826 rev = None
2714 2827
2715 2828 # fast path the special `nullid` rev
2716 2829 if node == self.nullid:
2717 2830 return b""
2718 2831
2719 2832 # ``rawtext`` is the text as stored inside the revlog. Might be the
2720 2833 # revision or might need to be processed to retrieve the revision.
2721 2834 rev, rawtext, validated = self._rawtext(node, rev)
2722 2835
2723 2836 if raw and validated:
2724 2837 # if we don't want to process the raw text and that raw
2725 2838 # text is cached, we can exit early.
2726 2839 return rawtext
2727 2840 if rev is None:
2728 2841 rev = self.rev(node)
2729 2842 # the revlog's flag for this revision
2730 2843 # (usually alter its state or content)
2731 2844 flags = self.flags(rev)
2732 2845
2733 2846 if validated and flags == REVIDX_DEFAULT_FLAGS:
2734 2847 # no extra flags set, no flag processor runs, text = rawtext
2735 2848 return rawtext
2736 2849
2737 2850 if raw:
2738 2851 validatehash = flagutil.processflagsraw(self, rawtext, flags)
2739 2852 text = rawtext
2740 2853 else:
2741 2854 r = flagutil.processflagsread(self, rawtext, flags)
2742 2855 text, validatehash = r
2743 2856 if validatehash:
2744 2857 self.checkhash(text, node, rev=rev)
2745 2858 if not validated:
2746 2859 self._inner._revisioncache = (node, rev, rawtext)
2747 2860
2748 2861 return text
2749 2862
2750 2863 def _sidedata(self, rev):
2751 2864 """Return the sidedata for a given revision number."""
2752 2865 sidedata_end = None
2753 2866 if self._docket is not None:
2754 2867 sidedata_end = self._docket.sidedata_end
2755 2868 return self._inner.sidedata(rev, sidedata_end)
2756 2869
2757 2870 def rawdata(self, nodeorrev):
2758 2871 """return an uncompressed raw data of a given node or revision number."""
2759 2872 return self._revisiondata(nodeorrev, raw=True)
2760 2873
2761 2874 def hash(self, text, p1, p2):
2762 2875 """Compute a node hash.
2763 2876
2764 2877 Available as a function so that subclasses can replace the hash
2765 2878 as needed.
2766 2879 """
2767 2880 return storageutil.hashrevisionsha1(text, p1, p2)
2768 2881
2769 2882 def checkhash(self, text, node, p1=None, p2=None, rev=None):
2770 2883 """Check node hash integrity.
2771 2884
2772 2885 Available as a function so that subclasses can extend hash mismatch
2773 2886 behaviors as needed.
2774 2887 """
2775 2888 try:
2776 2889 if p1 is None and p2 is None:
2777 2890 p1, p2 = self.parents(node)
2778 2891 if node != self.hash(text, p1, p2):
2779 2892 # Clear the revision cache on hash failure. The revision cache
2780 2893 # only stores the raw revision and clearing the cache does have
2781 2894 # the side-effect that we won't have a cache hit when the raw
2782 2895 # revision data is accessed. But this case should be rare and
2783 2896 # it is extra work to teach the cache about the hash
2784 2897 # verification state.
2785 2898 if (
2786 2899 self._inner._revisioncache
2787 2900 and self._inner._revisioncache[0] == node
2788 2901 ):
2789 2902 self._inner._revisioncache = None
2790 2903
2791 2904 revornode = rev
2792 2905 if revornode is None:
2793 2906 revornode = templatefilters.short(hex(node))
2794 2907 raise error.RevlogError(
2795 2908 _(b"integrity check failed on %s:%s")
2796 2909 % (self.display_id, pycompat.bytestr(revornode))
2797 2910 )
2798 2911 except error.RevlogError:
2799 2912 if self.feature_config.censorable and storageutil.iscensoredtext(
2800 2913 text
2801 2914 ):
2802 2915 raise error.CensoredNodeError(self.display_id, node, text)
2803 2916 raise
2804 2917
2805 2918 @property
2806 2919 def _split_index_file(self):
2807 2920 """the path where to expect the index of an ongoing splitting operation
2808 2921
2809 2922 The file will only exist if a splitting operation is in progress, but
2810 2923 it is always expected at the same location."""
2811 2924 parts = self.radix.split(b'/')
2812 2925 if len(parts) > 1:
2813 2926 # adds a '-s' prefix to the ``data/` or `meta/` base
2814 2927 head = parts[0] + b'-s'
2815 2928 mids = parts[1:-1]
2816 2929 tail = parts[-1] + b'.i'
2817 2930 pieces = [head] + mids + [tail]
2818 2931 return b'/'.join(pieces)
2819 2932 else:
2820 2933 # the revlog is stored at the root of the store (changelog or
2821 2934 # manifest), no risk of collision.
2822 2935 return self.radix + b'.i.s'
2823 2936
2824 2937 def _enforceinlinesize(self, tr, side_write=True):
2825 2938 """Check if the revlog is too big for inline and convert if so.
2826 2939
2827 2940 This should be called after revisions are added to the revlog. If the
2828 2941 revlog has grown too large to be an inline revlog, it will convert it
2829 2942 to use multiple index and data files.
2830 2943 """
2831 2944 tiprev = len(self) - 1
2832 2945 total_size = self.start(tiprev) + self.length(tiprev)
2833 2946 if not self._inline or total_size < _maxinline:
2834 2947 return
2835 2948
2836 2949 if self._docket is not None:
2837 2950 msg = b"inline revlog should not have a docket"
2838 2951 raise error.ProgrammingError(msg)
2839 2952
2840 2953 troffset = tr.findoffset(self._inner.canonical_index_file)
2841 2954 if troffset is None:
2842 2955 raise error.RevlogError(
2843 2956 _(b"%s not found in the transaction") % self._indexfile
2844 2957 )
2845 2958 if troffset:
2846 2959 tr.addbackup(self._inner.canonical_index_file, for_offset=True)
2847 2960 tr.add(self._datafile, 0)
2848 2961
2849 2962 new_index_file_path = None
2850 2963 if side_write:
2851 2964 old_index_file_path = self._indexfile
2852 2965 new_index_file_path = self._split_index_file
2853 2966 opener = self.opener
2854 2967 weak_self = weakref.ref(self)
2855 2968
2856 2969 # the "split" index replace the real index when the transaction is
2857 2970 # finalized
2858 2971 def finalize_callback(tr):
2859 2972 opener.rename(
2860 2973 new_index_file_path,
2861 2974 old_index_file_path,
2862 2975 checkambig=True,
2863 2976 )
2864 2977 maybe_self = weak_self()
2865 2978 if maybe_self is not None:
2866 2979 maybe_self._indexfile = old_index_file_path
2867 2980 maybe_self._inner.index_file = maybe_self._indexfile
2868 2981
2869 2982 def abort_callback(tr):
2870 2983 maybe_self = weak_self()
2871 2984 if maybe_self is not None:
2872 2985 maybe_self._indexfile = old_index_file_path
2873 2986 maybe_self._inner.inline = True
2874 2987 maybe_self._inner.index_file = old_index_file_path
2875 2988
2876 2989 tr.registertmp(new_index_file_path)
2877 2990 if self.target[1] is not None:
2878 2991 callback_id = b'000-revlog-split-%d-%s' % self.target
2879 2992 else:
2880 2993 callback_id = b'000-revlog-split-%d' % self.target[0]
2881 2994 tr.addfinalize(callback_id, finalize_callback)
2882 2995 tr.addabort(callback_id, abort_callback)
2883 2996
2884 2997 self._format_flags &= ~FLAG_INLINE_DATA
2885 2998 self._inner.split_inline(
2886 2999 tr,
2887 3000 self._format_flags | self._format_version,
2888 3001 new_index_file_path=new_index_file_path,
2889 3002 )
2890 3003
2891 3004 self._inline = False
2892 3005 if new_index_file_path is not None:
2893 3006 self._indexfile = new_index_file_path
2894 3007
2895 3008 nodemaputil.setup_persistent_nodemap(tr, self)
2896 3009
2897 3010 def _nodeduplicatecallback(self, transaction, node):
2898 3011 """called when trying to add a node already stored."""
2899 3012
2900 3013 @contextlib.contextmanager
2901 3014 def reading(self):
2902 3015 with self._inner.reading():
2903 3016 yield
2904 3017
2905 3018 @contextlib.contextmanager
2906 3019 def _writing(self, transaction):
2907 3020 if self._trypending:
2908 3021 msg = b'try to write in a `trypending` revlog: %s'
2909 3022 msg %= self.display_id
2910 3023 raise error.ProgrammingError(msg)
2911 3024 if self._inner.is_writing:
2912 3025 yield
2913 3026 else:
2914 3027 data_end = None
2915 3028 sidedata_end = None
2916 3029 if self._docket is not None:
2917 3030 data_end = self._docket.data_end
2918 3031 sidedata_end = self._docket.sidedata_end
2919 3032 with self._inner.writing(
2920 3033 transaction,
2921 3034 data_end=data_end,
2922 3035 sidedata_end=sidedata_end,
2923 3036 ):
2924 3037 yield
2925 3038 if self._docket is not None:
2926 3039 self._write_docket(transaction)
2927 3040
3041 @property
3042 def is_delaying(self):
3043 return self._inner.is_delaying
3044
2928 3045 def _write_docket(self, transaction):
2929 3046 """write the current docket on disk
2930 3047
2931 3048 Exist as a method to help changelog to implement transaction logic
2932 3049
2933 3050 We could also imagine using the same transaction logic for all revlog
2934 3051 since docket are cheap."""
2935 3052 self._docket.write(transaction)
2936 3053
2937 3054 def addrevision(
2938 3055 self,
2939 3056 text,
2940 3057 transaction,
2941 3058 link,
2942 3059 p1,
2943 3060 p2,
2944 3061 cachedelta=None,
2945 3062 node=None,
2946 3063 flags=REVIDX_DEFAULT_FLAGS,
2947 3064 deltacomputer=None,
2948 3065 sidedata=None,
2949 3066 ):
2950 3067 """add a revision to the log
2951 3068
2952 3069 text - the revision data to add
2953 3070 transaction - the transaction object used for rollback
2954 3071 link - the linkrev data to add
2955 3072 p1, p2 - the parent nodeids of the revision
2956 3073 cachedelta - an optional precomputed delta
2957 3074 node - nodeid of revision; typically node is not specified, and it is
2958 3075 computed by default as hash(text, p1, p2), however subclasses might
2959 3076 use different hashing method (and override checkhash() in such case)
2960 3077 flags - the known flags to set on the revision
2961 3078 deltacomputer - an optional deltacomputer instance shared between
2962 3079 multiple calls
2963 3080 """
2964 3081 if link == nullrev:
2965 3082 raise error.RevlogError(
2966 3083 _(b"attempted to add linkrev -1 to %s") % self.display_id
2967 3084 )
2968 3085
2969 3086 if sidedata is None:
2970 3087 sidedata = {}
2971 3088 elif sidedata and not self.feature_config.has_side_data:
2972 3089 raise error.ProgrammingError(
2973 3090 _(b"trying to add sidedata to a revlog who don't support them")
2974 3091 )
2975 3092
2976 3093 if flags:
2977 3094 node = node or self.hash(text, p1, p2)
2978 3095
2979 3096 rawtext, validatehash = flagutil.processflagswrite(self, text, flags)
2980 3097
2981 3098 # If the flag processor modifies the revision data, ignore any provided
2982 3099 # cachedelta.
2983 3100 if rawtext != text:
2984 3101 cachedelta = None
2985 3102
2986 3103 if len(rawtext) > _maxentrysize:
2987 3104 raise error.RevlogError(
2988 3105 _(
2989 3106 b"%s: size of %d bytes exceeds maximum revlog storage of 2GiB"
2990 3107 )
2991 3108 % (self.display_id, len(rawtext))
2992 3109 )
2993 3110
2994 3111 node = node or self.hash(rawtext, p1, p2)
2995 3112 rev = self.index.get_rev(node)
2996 3113 if rev is not None:
2997 3114 return rev
2998 3115
2999 3116 if validatehash:
3000 3117 self.checkhash(rawtext, node, p1=p1, p2=p2)
3001 3118
3002 3119 return self.addrawrevision(
3003 3120 rawtext,
3004 3121 transaction,
3005 3122 link,
3006 3123 p1,
3007 3124 p2,
3008 3125 node,
3009 3126 flags,
3010 3127 cachedelta=cachedelta,
3011 3128 deltacomputer=deltacomputer,
3012 3129 sidedata=sidedata,
3013 3130 )
3014 3131
3015 3132 def addrawrevision(
3016 3133 self,
3017 3134 rawtext,
3018 3135 transaction,
3019 3136 link,
3020 3137 p1,
3021 3138 p2,
3022 3139 node,
3023 3140 flags,
3024 3141 cachedelta=None,
3025 3142 deltacomputer=None,
3026 3143 sidedata=None,
3027 3144 ):
3028 3145 """add a raw revision with known flags, node and parents
3029 3146 useful when reusing a revision not stored in this revlog (ex: received
3030 3147 over wire, or read from an external bundle).
3031 3148 """
3032 3149 with self._writing(transaction):
3033 3150 return self._addrevision(
3034 3151 node,
3035 3152 rawtext,
3036 3153 transaction,
3037 3154 link,
3038 3155 p1,
3039 3156 p2,
3040 3157 flags,
3041 3158 cachedelta,
3042 3159 deltacomputer=deltacomputer,
3043 3160 sidedata=sidedata,
3044 3161 )
3045 3162
3046 3163 def compress(self, data):
3047 3164 return self._inner.compress(data)
3048 3165
3049 3166 def decompress(self, data):
3050 3167 return self._inner.decompress(data)
3051 3168
3052 3169 def _addrevision(
3053 3170 self,
3054 3171 node,
3055 3172 rawtext,
3056 3173 transaction,
3057 3174 link,
3058 3175 p1,
3059 3176 p2,
3060 3177 flags,
3061 3178 cachedelta,
3062 3179 alwayscache=False,
3063 3180 deltacomputer=None,
3064 3181 sidedata=None,
3065 3182 ):
3066 3183 """internal function to add revisions to the log
3067 3184
3068 3185 see addrevision for argument descriptions.
3069 3186
3070 3187 note: "addrevision" takes non-raw text, "_addrevision" takes raw text.
3071 3188
3072 3189 if "deltacomputer" is not provided or None, a defaultdeltacomputer will
3073 3190 be used.
3074 3191
3075 3192 invariants:
3076 3193 - rawtext is optional (can be None); if not set, cachedelta must be set.
3077 3194 if both are set, they must correspond to each other.
3078 3195 """
3079 3196 if node == self.nullid:
3080 3197 raise error.RevlogError(
3081 3198 _(b"%s: attempt to add null revision") % self.display_id
3082 3199 )
3083 3200 if (
3084 3201 node == self.nodeconstants.wdirid
3085 3202 or node in self.nodeconstants.wdirfilenodeids
3086 3203 ):
3087 3204 raise error.RevlogError(
3088 3205 _(b"%s: attempt to add wdir revision") % self.display_id
3089 3206 )
3090 3207 if self._inner._writinghandles is None:
3091 3208 msg = b'adding revision outside `revlog._writing` context'
3092 3209 raise error.ProgrammingError(msg)
3093 3210
3094 3211 btext = [rawtext]
3095 3212
3096 3213 curr = len(self)
3097 3214 prev = curr - 1
3098 3215
3099 3216 offset = self._get_data_offset(prev)
3100 3217
3101 3218 if self._concurrencychecker:
3102 3219 ifh, dfh, sdfh = self._inner._writinghandles
3103 3220 # XXX no checking for the sidedata file
3104 3221 if self._inline:
3105 3222 # offset is "as if" it were in the .d file, so we need to add on
3106 3223 # the size of the entry metadata.
3107 3224 self._concurrencychecker(
3108 3225 ifh, self._indexfile, offset + curr * self.index.entry_size
3109 3226 )
3110 3227 else:
3111 3228 # Entries in the .i are a consistent size.
3112 3229 self._concurrencychecker(
3113 3230 ifh, self._indexfile, curr * self.index.entry_size
3114 3231 )
3115 3232 self._concurrencychecker(dfh, self._datafile, offset)
3116 3233
3117 3234 p1r, p2r = self.rev(p1), self.rev(p2)
3118 3235
3119 3236 # full versions are inserted when the needed deltas
3120 3237 # become comparable to the uncompressed text
3121 3238 if rawtext is None:
3122 3239 # need rawtext size, before changed by flag processors, which is
3123 3240 # the non-raw size. use revlog explicitly to avoid filelog's extra
3124 3241 # logic that might remove metadata size.
3125 3242 textlen = mdiff.patchedsize(
3126 3243 revlog.size(self, cachedelta[0]), cachedelta[1]
3127 3244 )
3128 3245 else:
3129 3246 textlen = len(rawtext)
3130 3247
3131 3248 if deltacomputer is None:
3132 3249 write_debug = None
3133 3250 if self.delta_config.debug_delta:
3134 3251 write_debug = transaction._report
3135 3252 deltacomputer = deltautil.deltacomputer(
3136 3253 self, write_debug=write_debug
3137 3254 )
3138 3255
3139 3256 if cachedelta is not None and len(cachedelta) == 2:
3140 3257 # If the cached delta has no information about how it should be
3141 3258 # reused, add the default reuse instruction according to the
3142 3259 # revlog's configuration.
3143 3260 if (
3144 3261 self.delta_config.general_delta
3145 3262 and self.delta_config.lazy_delta_base
3146 3263 ):
3147 3264 delta_base_reuse = DELTA_BASE_REUSE_TRY
3148 3265 else:
3149 3266 delta_base_reuse = DELTA_BASE_REUSE_NO
3150 3267 cachedelta = (cachedelta[0], cachedelta[1], delta_base_reuse)
3151 3268
3152 3269 revinfo = revlogutils.revisioninfo(
3153 3270 node,
3154 3271 p1,
3155 3272 p2,
3156 3273 btext,
3157 3274 textlen,
3158 3275 cachedelta,
3159 3276 flags,
3160 3277 )
3161 3278
3162 3279 deltainfo = deltacomputer.finddeltainfo(revinfo)
3163 3280
3164 3281 compression_mode = COMP_MODE_INLINE
3165 3282 if self._docket is not None:
3166 3283 default_comp = self._docket.default_compression_header
3167 3284 r = deltautil.delta_compression(default_comp, deltainfo)
3168 3285 compression_mode, deltainfo = r
3169 3286
3170 3287 sidedata_compression_mode = COMP_MODE_INLINE
3171 3288 if sidedata and self.feature_config.has_side_data:
3172 3289 sidedata_compression_mode = COMP_MODE_PLAIN
3173 3290 serialized_sidedata = sidedatautil.serialize_sidedata(sidedata)
3174 3291 sidedata_offset = self._docket.sidedata_end
3175 3292 h, comp_sidedata = self._inner.compress(serialized_sidedata)
3176 3293 if (
3177 3294 h != b'u'
3178 3295 and comp_sidedata[0:1] != b'\0'
3179 3296 and len(comp_sidedata) < len(serialized_sidedata)
3180 3297 ):
3181 3298 assert not h
3182 3299 if (
3183 3300 comp_sidedata[0:1]
3184 3301 == self._docket.default_compression_header
3185 3302 ):
3186 3303 sidedata_compression_mode = COMP_MODE_DEFAULT
3187 3304 serialized_sidedata = comp_sidedata
3188 3305 else:
3189 3306 sidedata_compression_mode = COMP_MODE_INLINE
3190 3307 serialized_sidedata = comp_sidedata
3191 3308 else:
3192 3309 serialized_sidedata = b""
3193 3310 # Don't store the offset if the sidedata is empty, that way
3194 3311 # we can easily detect empty sidedata and they will be no different
3195 3312 # than ones we manually add.
3196 3313 sidedata_offset = 0
3197 3314
3198 3315 rank = RANK_UNKNOWN
3199 3316 if self.feature_config.compute_rank:
3200 3317 if (p1r, p2r) == (nullrev, nullrev):
3201 3318 rank = 1
3202 3319 elif p1r != nullrev and p2r == nullrev:
3203 3320 rank = 1 + self.fast_rank(p1r)
3204 3321 elif p1r == nullrev and p2r != nullrev:
3205 3322 rank = 1 + self.fast_rank(p2r)
3206 3323 else: # merge node
3207 3324 if rustdagop is not None and self.index.rust_ext_compat:
3208 3325 rank = rustdagop.rank(self.index, p1r, p2r)
3209 3326 else:
3210 3327 pmin, pmax = sorted((p1r, p2r))
3211 3328 rank = 1 + self.fast_rank(pmax)
3212 3329 rank += sum(1 for _ in self.findmissingrevs([pmax], [pmin]))
3213 3330
3214 3331 e = revlogutils.entry(
3215 3332 flags=flags,
3216 3333 data_offset=offset,
3217 3334 data_compressed_length=deltainfo.deltalen,
3218 3335 data_uncompressed_length=textlen,
3219 3336 data_compression_mode=compression_mode,
3220 3337 data_delta_base=deltainfo.base,
3221 3338 link_rev=link,
3222 3339 parent_rev_1=p1r,
3223 3340 parent_rev_2=p2r,
3224 3341 node_id=node,
3225 3342 sidedata_offset=sidedata_offset,
3226 3343 sidedata_compressed_length=len(serialized_sidedata),
3227 3344 sidedata_compression_mode=sidedata_compression_mode,
3228 3345 rank=rank,
3229 3346 )
3230 3347
3231 3348 self.index.append(e)
3232 3349 entry = self.index.entry_binary(curr)
3233 3350 if curr == 0 and self._docket is None:
3234 3351 header = self._format_flags | self._format_version
3235 3352 header = self.index.pack_header(header)
3236 3353 entry = header + entry
3237 3354 self._writeentry(
3238 3355 transaction,
3239 3356 entry,
3240 3357 deltainfo.data,
3241 3358 link,
3242 3359 offset,
3243 3360 serialized_sidedata,
3244 3361 sidedata_offset,
3245 3362 )
3246 3363
3247 3364 rawtext = btext[0]
3248 3365
3249 3366 if alwayscache and rawtext is None:
3250 3367 rawtext = deltacomputer.buildtext(revinfo)
3251 3368
3252 3369 if type(rawtext) == bytes: # only accept immutable objects
3253 3370 self._inner._revisioncache = (node, curr, rawtext)
3254 3371 self._chainbasecache[curr] = deltainfo.chainbase
3255 3372 return curr
3256 3373
3257 3374 def _get_data_offset(self, prev):
3258 3375 """Returns the current offset in the (in-transaction) data file.
3259 3376 Versions < 2 of the revlog can get this 0(1), revlog v2 needs a docket
3260 3377 file to store that information: since sidedata can be rewritten to the
3261 3378 end of the data file within a transaction, you can have cases where, for
3262 3379 example, rev `n` does not have sidedata while rev `n - 1` does, leading
3263 3380 to `n - 1`'s sidedata being written after `n`'s data.
3264 3381
3265 3382 TODO cache this in a docket file before getting out of experimental."""
3266 3383 if self._docket is None:
3267 3384 return self.end(prev)
3268 3385 else:
3269 3386 return self._docket.data_end
3270 3387
3271 3388 def _writeentry(
3272 3389 self,
3273 3390 transaction,
3274 3391 entry,
3275 3392 data,
3276 3393 link,
3277 3394 offset,
3278 3395 sidedata,
3279 3396 sidedata_offset,
3280 3397 ):
3281 3398 # Files opened in a+ mode have inconsistent behavior on various
3282 3399 # platforms. Windows requires that a file positioning call be made
3283 3400 # when the file handle transitions between reads and writes. See
3284 3401 # 3686fa2b8eee and the mixedfilemodewrapper in windows.py. On other
3285 3402 # platforms, Python or the platform itself can be buggy. Some versions
3286 3403 # of Solaris have been observed to not append at the end of the file
3287 3404 # if the file was seeked to before the end. See issue4943 for more.
3288 3405 #
3289 3406 # We work around this issue by inserting a seek() before writing.
3290 3407 # Note: This is likely not necessary on Python 3. However, because
3291 3408 # the file handle is reused for reads and may be seeked there, we need
3292 3409 # to be careful before changing this.
3293 3410 index_end = data_end = sidedata_end = None
3294 3411 if self._docket is not None:
3295 3412 index_end = self._docket.index_end
3296 3413 data_end = self._docket.data_end
3297 3414 sidedata_end = self._docket.sidedata_end
3298 3415
3299 3416 files_end = self._inner.write_entry(
3300 3417 transaction,
3301 3418 entry,
3302 3419 data,
3303 3420 link,
3304 3421 offset,
3305 3422 sidedata,
3306 3423 sidedata_offset,
3307 3424 index_end,
3308 3425 data_end,
3309 3426 sidedata_end,
3310 3427 )
3311 3428 self._enforceinlinesize(transaction)
3312 3429 if self._docket is not None:
3313 3430 self._docket.index_end = files_end[0]
3314 3431 self._docket.data_end = files_end[1]
3315 3432 self._docket.sidedata_end = files_end[2]
3316 3433
3317 3434 nodemaputil.setup_persistent_nodemap(transaction, self)
3318 3435
3319 3436 def addgroup(
3320 3437 self,
3321 3438 deltas,
3322 3439 linkmapper,
3323 3440 transaction,
3324 3441 alwayscache=False,
3325 3442 addrevisioncb=None,
3326 3443 duplicaterevisioncb=None,
3327 3444 debug_info=None,
3328 3445 delta_base_reuse_policy=None,
3329 3446 ):
3330 3447 """
3331 3448 add a delta group
3332 3449
3333 3450 given a set of deltas, add them to the revision log. the
3334 3451 first delta is against its parent, which should be in our
3335 3452 log, the rest are against the previous delta.
3336 3453
3337 3454 If ``addrevisioncb`` is defined, it will be called with arguments of
3338 3455 this revlog and the node that was added.
3339 3456 """
3340 3457
3341 3458 if self._adding_group:
3342 3459 raise error.ProgrammingError(b'cannot nest addgroup() calls')
3343 3460
3344 3461 # read the default delta-base reuse policy from revlog config if the
3345 3462 # group did not specify one.
3346 3463 if delta_base_reuse_policy is None:
3347 3464 if (
3348 3465 self.delta_config.general_delta
3349 3466 and self.delta_config.lazy_delta_base
3350 3467 ):
3351 3468 delta_base_reuse_policy = DELTA_BASE_REUSE_TRY
3352 3469 else:
3353 3470 delta_base_reuse_policy = DELTA_BASE_REUSE_NO
3354 3471
3355 3472 self._adding_group = True
3356 3473 empty = True
3357 3474 try:
3358 3475 with self._writing(transaction):
3359 3476 write_debug = None
3360 3477 if self.delta_config.debug_delta:
3361 3478 write_debug = transaction._report
3362 3479 deltacomputer = deltautil.deltacomputer(
3363 3480 self,
3364 3481 write_debug=write_debug,
3365 3482 debug_info=debug_info,
3366 3483 )
3367 3484 # loop through our set of deltas
3368 3485 for data in deltas:
3369 3486 (
3370 3487 node,
3371 3488 p1,
3372 3489 p2,
3373 3490 linknode,
3374 3491 deltabase,
3375 3492 delta,
3376 3493 flags,
3377 3494 sidedata,
3378 3495 ) = data
3379 3496 link = linkmapper(linknode)
3380 3497 flags = flags or REVIDX_DEFAULT_FLAGS
3381 3498
3382 3499 rev = self.index.get_rev(node)
3383 3500 if rev is not None:
3384 3501 # this can happen if two branches make the same change
3385 3502 self._nodeduplicatecallback(transaction, rev)
3386 3503 if duplicaterevisioncb:
3387 3504 duplicaterevisioncb(self, rev)
3388 3505 empty = False
3389 3506 continue
3390 3507
3391 3508 for p in (p1, p2):
3392 3509 if not self.index.has_node(p):
3393 3510 raise error.LookupError(
3394 3511 p, self.radix, _(b'unknown parent')
3395 3512 )
3396 3513
3397 3514 if not self.index.has_node(deltabase):
3398 3515 raise error.LookupError(
3399 3516 deltabase, self.display_id, _(b'unknown delta base')
3400 3517 )
3401 3518
3402 3519 baserev = self.rev(deltabase)
3403 3520
3404 3521 if baserev != nullrev and self.iscensored(baserev):
3405 3522 # if base is censored, delta must be full replacement in a
3406 3523 # single patch operation
3407 3524 hlen = struct.calcsize(b">lll")
3408 3525 oldlen = self.rawsize(baserev)
3409 3526 newlen = len(delta) - hlen
3410 3527 if delta[:hlen] != mdiff.replacediffheader(
3411 3528 oldlen, newlen
3412 3529 ):
3413 3530 raise error.CensoredBaseError(
3414 3531 self.display_id, self.node(baserev)
3415 3532 )
3416 3533
3417 3534 if not flags and self._peek_iscensored(baserev, delta):
3418 3535 flags |= REVIDX_ISCENSORED
3419 3536
3420 3537 # We assume consumers of addrevisioncb will want to retrieve
3421 3538 # the added revision, which will require a call to
3422 3539 # revision(). revision() will fast path if there is a cache
3423 3540 # hit. So, we tell _addrevision() to always cache in this case.
3424 3541 # We're only using addgroup() in the context of changegroup
3425 3542 # generation so the revision data can always be handled as raw
3426 3543 # by the flagprocessor.
3427 3544 rev = self._addrevision(
3428 3545 node,
3429 3546 None,
3430 3547 transaction,
3431 3548 link,
3432 3549 p1,
3433 3550 p2,
3434 3551 flags,
3435 3552 (baserev, delta, delta_base_reuse_policy),
3436 3553 alwayscache=alwayscache,
3437 3554 deltacomputer=deltacomputer,
3438 3555 sidedata=sidedata,
3439 3556 )
3440 3557
3441 3558 if addrevisioncb:
3442 3559 addrevisioncb(self, rev)
3443 3560 empty = False
3444 3561 finally:
3445 3562 self._adding_group = False
3446 3563 return not empty
3447 3564
3448 3565 def iscensored(self, rev):
3449 3566 """Check if a file revision is censored."""
3450 3567 if not self.feature_config.censorable:
3451 3568 return False
3452 3569
3453 3570 return self.flags(rev) & REVIDX_ISCENSORED
3454 3571
3455 3572 def _peek_iscensored(self, baserev, delta):
3456 3573 """Quickly check if a delta produces a censored revision."""
3457 3574 if not self.feature_config.censorable:
3458 3575 return False
3459 3576
3460 3577 return storageutil.deltaiscensored(delta, baserev, self.rawsize)
3461 3578
3462 3579 def getstrippoint(self, minlink):
3463 3580 """find the minimum rev that must be stripped to strip the linkrev
3464 3581
3465 3582 Returns a tuple containing the minimum rev and a set of all revs that
3466 3583 have linkrevs that will be broken by this strip.
3467 3584 """
3468 3585 return storageutil.resolvestripinfo(
3469 3586 minlink,
3470 3587 len(self) - 1,
3471 3588 self.headrevs(),
3472 3589 self.linkrev,
3473 3590 self.parentrevs,
3474 3591 )
3475 3592
3476 3593 def strip(self, minlink, transaction):
3477 3594 """truncate the revlog on the first revision with a linkrev >= minlink
3478 3595
3479 3596 This function is called when we're stripping revision minlink and
3480 3597 its descendants from the repository.
3481 3598
3482 3599 We have to remove all revisions with linkrev >= minlink, because
3483 3600 the equivalent changelog revisions will be renumbered after the
3484 3601 strip.
3485 3602
3486 3603 So we truncate the revlog on the first of these revisions, and
3487 3604 trust that the caller has saved the revisions that shouldn't be
3488 3605 removed and that it'll re-add them after this truncation.
3489 3606 """
3490 3607 if len(self) == 0:
3491 3608 return
3492 3609
3493 3610 rev, _ = self.getstrippoint(minlink)
3494 3611 if rev == len(self):
3495 3612 return
3496 3613
3497 3614 # first truncate the files on disk
3498 3615 data_end = self.start(rev)
3499 3616 if not self._inline:
3500 3617 transaction.add(self._datafile, data_end)
3501 3618 end = rev * self.index.entry_size
3502 3619 else:
3503 3620 end = data_end + (rev * self.index.entry_size)
3504 3621
3505 3622 if self._sidedatafile:
3506 3623 sidedata_end = self.sidedata_cut_off(rev)
3507 3624 transaction.add(self._sidedatafile, sidedata_end)
3508 3625
3509 3626 transaction.add(self._indexfile, end)
3510 3627 if self._docket is not None:
3511 3628 # XXX we could, leverage the docket while stripping. However it is
3512 3629 # not powerfull enough at the time of this comment
3513 3630 self._docket.index_end = end
3514 3631 self._docket.data_end = data_end
3515 3632 self._docket.sidedata_end = sidedata_end
3516 3633 self._docket.write(transaction, stripping=True)
3517 3634
3518 3635 # then reset internal state in memory to forget those revisions
3519 3636 self._chaininfocache = util.lrucachedict(500)
3520 3637 self._inner.clear_cache()
3521 3638
3522 3639 del self.index[rev:-1]
3523 3640
3524 3641 def checksize(self):
3525 3642 """Check size of index and data files
3526 3643
3527 3644 return a (dd, di) tuple.
3528 3645 - dd: extra bytes for the "data" file
3529 3646 - di: extra bytes for the "index" file
3530 3647
3531 3648 A healthy revlog will return (0, 0).
3532 3649 """
3533 3650 expected = 0
3534 3651 if len(self):
3535 3652 expected = max(0, self.end(len(self) - 1))
3536 3653
3537 3654 try:
3538 3655 with self._datafp() as f:
3539 3656 f.seek(0, io.SEEK_END)
3540 3657 actual = f.tell()
3541 3658 dd = actual - expected
3542 3659 except FileNotFoundError:
3543 3660 dd = 0
3544 3661
3545 3662 try:
3546 3663 f = self.opener(self._indexfile)
3547 3664 f.seek(0, io.SEEK_END)
3548 3665 actual = f.tell()
3549 3666 f.close()
3550 3667 s = self.index.entry_size
3551 3668 i = max(0, actual // s)
3552 3669 di = actual - (i * s)
3553 3670 if self._inline:
3554 3671 databytes = 0
3555 3672 for r in self:
3556 3673 databytes += max(0, self.length(r))
3557 3674 dd = 0
3558 3675 di = actual - len(self) * s - databytes
3559 3676 except FileNotFoundError:
3560 3677 di = 0
3561 3678
3562 3679 return (dd, di)
3563 3680
3564 3681 def files(self):
3565 3682 """return list of files that compose this revlog"""
3566 3683 res = [self._indexfile]
3567 3684 if self._docket_file is None:
3568 3685 if not self._inline:
3569 3686 res.append(self._datafile)
3570 3687 else:
3571 3688 res.append(self._docket_file)
3572 3689 res.extend(self._docket.old_index_filepaths(include_empty=False))
3573 3690 if self._docket.data_end:
3574 3691 res.append(self._datafile)
3575 3692 res.extend(self._docket.old_data_filepaths(include_empty=False))
3576 3693 if self._docket.sidedata_end:
3577 3694 res.append(self._sidedatafile)
3578 3695 res.extend(self._docket.old_sidedata_filepaths(include_empty=False))
3579 3696 return res
3580 3697
3581 3698 def emitrevisions(
3582 3699 self,
3583 3700 nodes,
3584 3701 nodesorder=None,
3585 3702 revisiondata=False,
3586 3703 assumehaveparentrevisions=False,
3587 3704 deltamode=repository.CG_DELTAMODE_STD,
3588 3705 sidedata_helpers=None,
3589 3706 debug_info=None,
3590 3707 ):
3591 3708 if nodesorder not in (b'nodes', b'storage', b'linear', None):
3592 3709 raise error.ProgrammingError(
3593 3710 b'unhandled value for nodesorder: %s' % nodesorder
3594 3711 )
3595 3712
3596 3713 if nodesorder is None and not self.delta_config.general_delta:
3597 3714 nodesorder = b'storage'
3598 3715
3599 3716 if (
3600 3717 not self._storedeltachains
3601 3718 and deltamode != repository.CG_DELTAMODE_PREV
3602 3719 ):
3603 3720 deltamode = repository.CG_DELTAMODE_FULL
3604 3721
3605 3722 return storageutil.emitrevisions(
3606 3723 self,
3607 3724 nodes,
3608 3725 nodesorder,
3609 3726 revlogrevisiondelta,
3610 3727 deltaparentfn=self.deltaparent,
3611 3728 candeltafn=self._candelta,
3612 3729 rawsizefn=self.rawsize,
3613 3730 revdifffn=self.revdiff,
3614 3731 flagsfn=self.flags,
3615 3732 deltamode=deltamode,
3616 3733 revisiondata=revisiondata,
3617 3734 assumehaveparentrevisions=assumehaveparentrevisions,
3618 3735 sidedata_helpers=sidedata_helpers,
3619 3736 debug_info=debug_info,
3620 3737 )
3621 3738
3622 3739 DELTAREUSEALWAYS = b'always'
3623 3740 DELTAREUSESAMEREVS = b'samerevs'
3624 3741 DELTAREUSENEVER = b'never'
3625 3742
3626 3743 DELTAREUSEFULLADD = b'fulladd'
3627 3744
3628 3745 DELTAREUSEALL = {b'always', b'samerevs', b'never', b'fulladd'}
3629 3746
3630 3747 def clone(
3631 3748 self,
3632 3749 tr,
3633 3750 destrevlog,
3634 3751 addrevisioncb=None,
3635 3752 deltareuse=DELTAREUSESAMEREVS,
3636 3753 forcedeltabothparents=None,
3637 3754 sidedata_helpers=None,
3638 3755 ):
3639 3756 """Copy this revlog to another, possibly with format changes.
3640 3757
3641 3758 The destination revlog will contain the same revisions and nodes.
3642 3759 However, it may not be bit-for-bit identical due to e.g. delta encoding
3643 3760 differences.
3644 3761
3645 3762 The ``deltareuse`` argument control how deltas from the existing revlog
3646 3763 are preserved in the destination revlog. The argument can have the
3647 3764 following values:
3648 3765
3649 3766 DELTAREUSEALWAYS
3650 3767 Deltas will always be reused (if possible), even if the destination
3651 3768 revlog would not select the same revisions for the delta. This is the
3652 3769 fastest mode of operation.
3653 3770 DELTAREUSESAMEREVS
3654 3771 Deltas will be reused if the destination revlog would pick the same
3655 3772 revisions for the delta. This mode strikes a balance between speed
3656 3773 and optimization.
3657 3774 DELTAREUSENEVER
3658 3775 Deltas will never be reused. This is the slowest mode of execution.
3659 3776 This mode can be used to recompute deltas (e.g. if the diff/delta
3660 3777 algorithm changes).
3661 3778 DELTAREUSEFULLADD
3662 3779 Revision will be re-added as if their were new content. This is
3663 3780 slower than DELTAREUSEALWAYS but allow more mechanism to kicks in.
3664 3781 eg: large file detection and handling.
3665 3782
3666 3783 Delta computation can be slow, so the choice of delta reuse policy can
3667 3784 significantly affect run time.
3668 3785
3669 3786 The default policy (``DELTAREUSESAMEREVS``) strikes a balance between
3670 3787 two extremes. Deltas will be reused if they are appropriate. But if the
3671 3788 delta could choose a better revision, it will do so. This means if you
3672 3789 are converting a non-generaldelta revlog to a generaldelta revlog,
3673 3790 deltas will be recomputed if the delta's parent isn't a parent of the
3674 3791 revision.
3675 3792
3676 3793 In addition to the delta policy, the ``forcedeltabothparents``
3677 3794 argument controls whether to force compute deltas against both parents
3678 3795 for merges. By default, the current default is used.
3679 3796
3680 3797 See `revlogutil.sidedata.get_sidedata_helpers` for the doc on
3681 3798 `sidedata_helpers`.
3682 3799 """
3683 3800 if deltareuse not in self.DELTAREUSEALL:
3684 3801 raise ValueError(
3685 3802 _(b'value for deltareuse invalid: %s') % deltareuse
3686 3803 )
3687 3804
3688 3805 if len(destrevlog):
3689 3806 raise ValueError(_(b'destination revlog is not empty'))
3690 3807
3691 3808 if getattr(self, 'filteredrevs', None):
3692 3809 raise ValueError(_(b'source revlog has filtered revisions'))
3693 3810 if getattr(destrevlog, 'filteredrevs', None):
3694 3811 raise ValueError(_(b'destination revlog has filtered revisions'))
3695 3812
3696 3813 # lazydelta and lazydeltabase controls whether to reuse a cached delta,
3697 3814 # if possible.
3698 3815 old_delta_config = destrevlog.delta_config
3699 3816 destrevlog.delta_config = destrevlog.delta_config.copy()
3700 3817
3701 3818 try:
3702 3819 if deltareuse == self.DELTAREUSEALWAYS:
3703 3820 destrevlog.delta_config.lazy_delta_base = True
3704 3821 destrevlog.delta_config.lazy_delta = True
3705 3822 elif deltareuse == self.DELTAREUSESAMEREVS:
3706 3823 destrevlog.delta_config.lazy_delta_base = False
3707 3824 destrevlog.delta_config.lazy_delta = True
3708 3825 elif deltareuse == self.DELTAREUSENEVER:
3709 3826 destrevlog.delta_config.lazy_delta_base = False
3710 3827 destrevlog.delta_config.lazy_delta = False
3711 3828
3712 3829 delta_both_parents = (
3713 3830 forcedeltabothparents or old_delta_config.delta_both_parents
3714 3831 )
3715 3832 destrevlog.delta_config.delta_both_parents = delta_both_parents
3716 3833
3717 3834 with self.reading(), destrevlog._writing(tr):
3718 3835 self._clone(
3719 3836 tr,
3720 3837 destrevlog,
3721 3838 addrevisioncb,
3722 3839 deltareuse,
3723 3840 forcedeltabothparents,
3724 3841 sidedata_helpers,
3725 3842 )
3726 3843
3727 3844 finally:
3728 3845 destrevlog.delta_config = old_delta_config
3729 3846
3730 3847 def _clone(
3731 3848 self,
3732 3849 tr,
3733 3850 destrevlog,
3734 3851 addrevisioncb,
3735 3852 deltareuse,
3736 3853 forcedeltabothparents,
3737 3854 sidedata_helpers,
3738 3855 ):
3739 3856 """perform the core duty of `revlog.clone` after parameter processing"""
3740 3857 write_debug = None
3741 3858 if self.delta_config.debug_delta:
3742 3859 write_debug = tr._report
3743 3860 deltacomputer = deltautil.deltacomputer(
3744 3861 destrevlog,
3745 3862 write_debug=write_debug,
3746 3863 )
3747 3864 index = self.index
3748 3865 for rev in self:
3749 3866 entry = index[rev]
3750 3867
3751 3868 # Some classes override linkrev to take filtered revs into
3752 3869 # account. Use raw entry from index.
3753 3870 flags = entry[0] & 0xFFFF
3754 3871 linkrev = entry[4]
3755 3872 p1 = index[entry[5]][7]
3756 3873 p2 = index[entry[6]][7]
3757 3874 node = entry[7]
3758 3875
3759 3876 # (Possibly) reuse the delta from the revlog if allowed and
3760 3877 # the revlog chunk is a delta.
3761 3878 cachedelta = None
3762 3879 rawtext = None
3763 3880 if deltareuse == self.DELTAREUSEFULLADD:
3764 3881 text = self._revisiondata(rev)
3765 3882 sidedata = self.sidedata(rev)
3766 3883
3767 3884 if sidedata_helpers is not None:
3768 3885 (sidedata, new_flags) = sidedatautil.run_sidedata_helpers(
3769 3886 self, sidedata_helpers, sidedata, rev
3770 3887 )
3771 3888 flags = flags | new_flags[0] & ~new_flags[1]
3772 3889
3773 3890 destrevlog.addrevision(
3774 3891 text,
3775 3892 tr,
3776 3893 linkrev,
3777 3894 p1,
3778 3895 p2,
3779 3896 cachedelta=cachedelta,
3780 3897 node=node,
3781 3898 flags=flags,
3782 3899 deltacomputer=deltacomputer,
3783 3900 sidedata=sidedata,
3784 3901 )
3785 3902 else:
3786 3903 if destrevlog.delta_config.lazy_delta:
3787 3904 dp = self.deltaparent(rev)
3788 3905 if dp != nullrev:
3789 3906 cachedelta = (dp, bytes(self._inner._chunk(rev)))
3790 3907
3791 3908 sidedata = None
3792 3909 if not cachedelta:
3793 3910 try:
3794 3911 rawtext = self._revisiondata(rev)
3795 3912 except error.CensoredNodeError as censored:
3796 3913 assert flags & REVIDX_ISCENSORED
3797 3914 rawtext = censored.tombstone
3798 3915 sidedata = self.sidedata(rev)
3799 3916 if sidedata is None:
3800 3917 sidedata = self.sidedata(rev)
3801 3918
3802 3919 if sidedata_helpers is not None:
3803 3920 (sidedata, new_flags) = sidedatautil.run_sidedata_helpers(
3804 3921 self, sidedata_helpers, sidedata, rev
3805 3922 )
3806 3923 flags = flags | new_flags[0] & ~new_flags[1]
3807 3924
3808 3925 destrevlog._addrevision(
3809 3926 node,
3810 3927 rawtext,
3811 3928 tr,
3812 3929 linkrev,
3813 3930 p1,
3814 3931 p2,
3815 3932 flags,
3816 3933 cachedelta,
3817 3934 deltacomputer=deltacomputer,
3818 3935 sidedata=sidedata,
3819 3936 )
3820 3937
3821 3938 if addrevisioncb:
3822 3939 addrevisioncb(self, rev, node)
3823 3940
3824 3941 def censorrevision(self, tr, censornode, tombstone=b''):
3825 3942 if self._format_version == REVLOGV0:
3826 3943 raise error.RevlogError(
3827 3944 _(b'cannot censor with version %d revlogs')
3828 3945 % self._format_version
3829 3946 )
3830 3947 elif self._format_version == REVLOGV1:
3831 3948 rewrite.v1_censor(self, tr, censornode, tombstone)
3832 3949 else:
3833 3950 rewrite.v2_censor(self, tr, censornode, tombstone)
3834 3951
3835 3952 def verifyintegrity(self, state):
3836 3953 """Verifies the integrity of the revlog.
3837 3954
3838 3955 Yields ``revlogproblem`` instances describing problems that are
3839 3956 found.
3840 3957 """
3841 3958 dd, di = self.checksize()
3842 3959 if dd:
3843 3960 yield revlogproblem(error=_(b'data length off by %d bytes') % dd)
3844 3961 if di:
3845 3962 yield revlogproblem(error=_(b'index contains %d extra bytes') % di)
3846 3963
3847 3964 version = self._format_version
3848 3965
3849 3966 # The verifier tells us what version revlog we should be.
3850 3967 if version != state[b'expectedversion']:
3851 3968 yield revlogproblem(
3852 3969 warning=_(b"warning: '%s' uses revlog format %d; expected %d")
3853 3970 % (self.display_id, version, state[b'expectedversion'])
3854 3971 )
3855 3972
3856 3973 state[b'skipread'] = set()
3857 3974 state[b'safe_renamed'] = set()
3858 3975
3859 3976 for rev in self:
3860 3977 node = self.node(rev)
3861 3978
3862 3979 # Verify contents. 4 cases to care about:
3863 3980 #
3864 3981 # common: the most common case
3865 3982 # rename: with a rename
3866 3983 # meta: file content starts with b'\1\n', the metadata
3867 3984 # header defined in filelog.py, but without a rename
3868 3985 # ext: content stored externally
3869 3986 #
3870 3987 # More formally, their differences are shown below:
3871 3988 #
3872 3989 # | common | rename | meta | ext
3873 3990 # -------------------------------------------------------
3874 3991 # flags() | 0 | 0 | 0 | not 0
3875 3992 # renamed() | False | True | False | ?
3876 3993 # rawtext[0:2]=='\1\n'| False | True | True | ?
3877 3994 #
3878 3995 # "rawtext" means the raw text stored in revlog data, which
3879 3996 # could be retrieved by "rawdata(rev)". "text"
3880 3997 # mentioned below is "revision(rev)".
3881 3998 #
3882 3999 # There are 3 different lengths stored physically:
3883 4000 # 1. L1: rawsize, stored in revlog index
3884 4001 # 2. L2: len(rawtext), stored in revlog data
3885 4002 # 3. L3: len(text), stored in revlog data if flags==0, or
3886 4003 # possibly somewhere else if flags!=0
3887 4004 #
3888 4005 # L1 should be equal to L2. L3 could be different from them.
3889 4006 # "text" may or may not affect commit hash depending on flag
3890 4007 # processors (see flagutil.addflagprocessor).
3891 4008 #
3892 4009 # | common | rename | meta | ext
3893 4010 # -------------------------------------------------
3894 4011 # rawsize() | L1 | L1 | L1 | L1
3895 4012 # size() | L1 | L2-LM | L1(*) | L1 (?)
3896 4013 # len(rawtext) | L2 | L2 | L2 | L2
3897 4014 # len(text) | L2 | L2 | L2 | L3
3898 4015 # len(read()) | L2 | L2-LM | L2-LM | L3 (?)
3899 4016 #
3900 4017 # LM: length of metadata, depending on rawtext
3901 4018 # (*): not ideal, see comment in filelog.size
3902 4019 # (?): could be "- len(meta)" if the resolved content has
3903 4020 # rename metadata
3904 4021 #
3905 4022 # Checks needed to be done:
3906 4023 # 1. length check: L1 == L2, in all cases.
3907 4024 # 2. hash check: depending on flag processor, we may need to
3908 4025 # use either "text" (external), or "rawtext" (in revlog).
3909 4026
3910 4027 try:
3911 4028 skipflags = state.get(b'skipflags', 0)
3912 4029 if skipflags:
3913 4030 skipflags &= self.flags(rev)
3914 4031
3915 4032 _verify_revision(self, skipflags, state, node)
3916 4033
3917 4034 l1 = self.rawsize(rev)
3918 4035 l2 = len(self.rawdata(node))
3919 4036
3920 4037 if l1 != l2:
3921 4038 yield revlogproblem(
3922 4039 error=_(b'unpacked size is %d, %d expected') % (l2, l1),
3923 4040 node=node,
3924 4041 )
3925 4042
3926 4043 except error.CensoredNodeError:
3927 4044 if state[b'erroroncensored']:
3928 4045 yield revlogproblem(
3929 4046 error=_(b'censored file data'), node=node
3930 4047 )
3931 4048 state[b'skipread'].add(node)
3932 4049 except Exception as e:
3933 4050 yield revlogproblem(
3934 4051 error=_(b'unpacking %s: %s')
3935 4052 % (short(node), stringutil.forcebytestr(e)),
3936 4053 node=node,
3937 4054 )
3938 4055 state[b'skipread'].add(node)
3939 4056
3940 4057 def storageinfo(
3941 4058 self,
3942 4059 exclusivefiles=False,
3943 4060 sharedfiles=False,
3944 4061 revisionscount=False,
3945 4062 trackedsize=False,
3946 4063 storedsize=False,
3947 4064 ):
3948 4065 d = {}
3949 4066
3950 4067 if exclusivefiles:
3951 4068 d[b'exclusivefiles'] = [(self.opener, self._indexfile)]
3952 4069 if not self._inline:
3953 4070 d[b'exclusivefiles'].append((self.opener, self._datafile))
3954 4071
3955 4072 if sharedfiles:
3956 4073 d[b'sharedfiles'] = []
3957 4074
3958 4075 if revisionscount:
3959 4076 d[b'revisionscount'] = len(self)
3960 4077
3961 4078 if trackedsize:
3962 4079 d[b'trackedsize'] = sum(map(self.rawsize, iter(self)))
3963 4080
3964 4081 if storedsize:
3965 4082 d[b'storedsize'] = sum(
3966 4083 self.opener.stat(path).st_size for path in self.files()
3967 4084 )
3968 4085
3969 4086 return d
3970 4087
3971 4088 def rewrite_sidedata(self, transaction, helpers, startrev, endrev):
3972 4089 if not self.feature_config.has_side_data:
3973 4090 return
3974 4091 # revlog formats with sidedata support does not support inline
3975 4092 assert not self._inline
3976 4093 if not helpers[1] and not helpers[2]:
3977 4094 # Nothing to generate or remove
3978 4095 return
3979 4096
3980 4097 new_entries = []
3981 4098 # append the new sidedata
3982 4099 with self._writing(transaction):
3983 4100 ifh, dfh, sdfh = self._inner._writinghandles
3984 4101 dfh.seek(self._docket.sidedata_end, os.SEEK_SET)
3985 4102
3986 4103 current_offset = sdfh.tell()
3987 4104 for rev in range(startrev, endrev + 1):
3988 4105 entry = self.index[rev]
3989 4106 new_sidedata, flags = sidedatautil.run_sidedata_helpers(
3990 4107 store=self,
3991 4108 sidedata_helpers=helpers,
3992 4109 sidedata={},
3993 4110 rev=rev,
3994 4111 )
3995 4112
3996 4113 serialized_sidedata = sidedatautil.serialize_sidedata(
3997 4114 new_sidedata
3998 4115 )
3999 4116
4000 4117 sidedata_compression_mode = COMP_MODE_INLINE
4001 4118 if serialized_sidedata and self.feature_config.has_side_data:
4002 4119 sidedata_compression_mode = COMP_MODE_PLAIN
4003 4120 h, comp_sidedata = self._inner.compress(serialized_sidedata)
4004 4121 if (
4005 4122 h != b'u'
4006 4123 and comp_sidedata[0] != b'\0'
4007 4124 and len(comp_sidedata) < len(serialized_sidedata)
4008 4125 ):
4009 4126 assert not h
4010 4127 if (
4011 4128 comp_sidedata[0]
4012 4129 == self._docket.default_compression_header
4013 4130 ):
4014 4131 sidedata_compression_mode = COMP_MODE_DEFAULT
4015 4132 serialized_sidedata = comp_sidedata
4016 4133 else:
4017 4134 sidedata_compression_mode = COMP_MODE_INLINE
4018 4135 serialized_sidedata = comp_sidedata
4019 4136 if entry[8] != 0 or entry[9] != 0:
4020 4137 # rewriting entries that already have sidedata is not
4021 4138 # supported yet, because it introduces garbage data in the
4022 4139 # revlog.
4023 4140 msg = b"rewriting existing sidedata is not supported yet"
4024 4141 raise error.Abort(msg)
4025 4142
4026 4143 # Apply (potential) flags to add and to remove after running
4027 4144 # the sidedata helpers
4028 4145 new_offset_flags = entry[0] | flags[0] & ~flags[1]
4029 4146 entry_update = (
4030 4147 current_offset,
4031 4148 len(serialized_sidedata),
4032 4149 new_offset_flags,
4033 4150 sidedata_compression_mode,
4034 4151 )
4035 4152
4036 4153 # the sidedata computation might have move the file cursors around
4037 4154 sdfh.seek(current_offset, os.SEEK_SET)
4038 4155 sdfh.write(serialized_sidedata)
4039 4156 new_entries.append(entry_update)
4040 4157 current_offset += len(serialized_sidedata)
4041 4158 self._docket.sidedata_end = sdfh.tell()
4042 4159
4043 4160 # rewrite the new index entries
4044 4161 ifh.seek(startrev * self.index.entry_size)
4045 4162 for i, e in enumerate(new_entries):
4046 4163 rev = startrev + i
4047 4164 self.index.replace_sidedata_info(rev, *e)
4048 4165 packed = self.index.entry_binary(rev)
4049 4166 if rev == 0 and self._docket is None:
4050 4167 header = self._format_flags | self._format_version
4051 4168 header = self.index.pack_header(header)
4052 4169 packed = header + packed
4053 4170 ifh.write(packed)
@@ -1,234 +1,241 b''
1 1 # Copyright Mercurial Contributors
2 2 #
3 3 # This software may be used and distributed according to the terms of the
4 4 # GNU General Public License version 2 or any later version.
5 5
6 6 import contextlib
7 7
8 8 from ..i18n import _
9 9 from .. import (
10 10 error,
11 11 util,
12 12 )
13 13
14 14
15 15 _MAX_CACHED_CHUNK_SIZE = 1048576 # 1 MiB
16 16
17 17 PARTIAL_READ_MSG = _(
18 18 b'partial read of revlog %s; expected %d bytes from offset %d, got %d'
19 19 )
20 20
21 21
22 22 def _is_power_of_two(n):
23 23 return (n & (n - 1) == 0) and n != 0
24 24
25 25
26 26 class appender:
27 27 """the changelog index must be updated last on disk, so we use this class
28 28 to delay writes to it"""
29 29
30 30 def __init__(self, vfs, name, mode, buf):
31 31 self.data = buf
32 32 fp = vfs(name, mode)
33 33 self.fp = fp
34 34 self.offset = fp.tell()
35 35 self.size = vfs.fstat(fp).st_size
36 36 self._end = self.size
37 37
38 38 def end(self):
39 39 return self._end
40 40
41 41 def tell(self):
42 42 return self.offset
43 43
44 44 def flush(self):
45 45 pass
46 46
47 47 @property
48 48 def closed(self):
49 49 return self.fp.closed
50 50
51 51 def close(self):
52 52 self.fp.close()
53 53
54 54 def seek(self, offset, whence=0):
55 55 '''virtual file offset spans real file and data'''
56 56 if whence == 0:
57 57 self.offset = offset
58 58 elif whence == 1:
59 59 self.offset += offset
60 60 elif whence == 2:
61 61 self.offset = self.end() + offset
62 62 if self.offset < self.size:
63 63 self.fp.seek(self.offset)
64 64
65 65 def read(self, count=-1):
66 66 '''only trick here is reads that span real file and data'''
67 67 ret = b""
68 68 if self.offset < self.size:
69 69 s = self.fp.read(count)
70 70 ret = s
71 71 self.offset += len(s)
72 72 if count > 0:
73 73 count -= len(s)
74 74 if count != 0:
75 75 doff = self.offset - self.size
76 76 self.data.insert(0, b"".join(self.data))
77 77 del self.data[1:]
78 78 s = self.data[0][doff : doff + count]
79 79 self.offset += len(s)
80 80 ret += s
81 81 return ret
82 82
83 83 def write(self, s):
84 84 self.data.append(bytes(s))
85 85 self.offset += len(s)
86 86 self._end += len(s)
87 87
88 88 def __enter__(self):
89 89 self.fp.__enter__()
90 90 return self
91 91
92 92 def __exit__(self, *args):
93 93 return self.fp.__exit__(*args)
94 94
95 95
96 96 class randomaccessfile:
97 97 """Accessing arbitrary chuncks of data within a file, with some caching"""
98 98
99 99 def __init__(
100 100 self,
101 101 opener,
102 102 filename,
103 103 default_cached_chunk_size,
104 104 initial_cache=None,
105 105 ):
106 106 # Required by bitwise manipulation below
107 107 assert _is_power_of_two(default_cached_chunk_size)
108 108
109 109 self.opener = opener
110 110 self.filename = filename
111 111 self.default_cached_chunk_size = default_cached_chunk_size
112 112 self.writing_handle = None # This is set from revlog.py
113 113 self.reading_handle = None
114 114 self._cached_chunk = b''
115 115 self._cached_chunk_position = 0 # Offset from the start of the file
116 116 if initial_cache:
117 117 self._cached_chunk_position, self._cached_chunk = initial_cache
118 118
119 self._delay_buffer = None
120
119 121 def clear_cache(self):
120 122 self._cached_chunk = b''
121 123 self._cached_chunk_position = 0
122 124
123 125 @property
124 126 def is_open(self):
125 127 """True if any file handle is being held
126 128
127 129 Used for assert and debug in the python code"""
128 130 return (
129 131 self.reading_handle is not None or self.writing_handle is not None
130 132 )
131 133
132 134 def _open(self, mode=b'r'):
133 135 """Return a file object"""
134 return self.opener(self.filename, mode=mode)
136 if self._delay_buffer is None:
137 return self.opener(self.filename, mode=mode)
138 else:
139 return appender(
140 self.opener, self.filename, mode, self._delay_buffer
141 )
135 142
136 143 @contextlib.contextmanager
137 144 def _read_handle(self):
138 145 """File object suitable for reading data"""
139 146 # Use a file handle being actively used for writes, if available.
140 147 # There is some danger to doing this because reads will seek the
141 148 # file. However, revlog._writeentry performs a SEEK_END before all
142 149 # writes, so we should be safe.
143 150 if self.writing_handle:
144 151 yield self.writing_handle
145 152
146 153 elif self.reading_handle:
147 154 yield self.reading_handle
148 155
149 156 # Otherwise open a new file handle.
150 157 else:
151 158 with self._open() as fp:
152 159 yield fp
153 160
154 161 @contextlib.contextmanager
155 162 def reading(self):
156 163 """Context manager that keeps the file open for reading"""
157 164 if (
158 165 self.reading_handle is None
159 166 and self.writing_handle is None
160 167 and self.filename is not None
161 168 ):
162 169 with self._open() as fp:
163 170 self.reading_handle = fp
164 171 try:
165 172 yield
166 173 finally:
167 174 self.reading_handle = None
168 175 else:
169 176 yield
170 177
171 178 def read_chunk(self, offset, length):
172 179 """Read a chunk of bytes from the file.
173 180
174 181 Accepts an absolute offset, length to read, and an optional existing
175 182 file handle to read from.
176 183
177 184 If an existing file handle is passed, it will be seeked and the
178 185 original seek position will NOT be restored.
179 186
180 187 Returns a str or buffer of raw byte data.
181 188
182 189 Raises if the requested number of bytes could not be read.
183 190 """
184 191 end = offset + length
185 192 cache_start = self._cached_chunk_position
186 193 cache_end = cache_start + len(self._cached_chunk)
187 194 # Is the requested chunk within the cache?
188 195 if cache_start <= offset and end <= cache_end:
189 196 if cache_start == offset and end == cache_end:
190 197 return self._cached_chunk # avoid a copy
191 198 relative_start = offset - cache_start
192 199 return util.buffer(self._cached_chunk, relative_start, length)
193 200
194 201 return self._read_and_update_cache(offset, length)
195 202
196 203 def _read_and_update_cache(self, offset, length):
197 204 # Cache data both forward and backward around the requested
198 205 # data, in a fixed size window. This helps speed up operations
199 206 # involving reading the revlog backwards.
200 207 real_offset = offset & ~(self.default_cached_chunk_size - 1)
201 208 real_length = (
202 209 (offset + length + self.default_cached_chunk_size)
203 210 & ~(self.default_cached_chunk_size - 1)
204 211 ) - real_offset
205 212 with self._read_handle() as file_obj:
206 213 file_obj.seek(real_offset)
207 214 data = file_obj.read(real_length)
208 215
209 216 self._add_cached_chunk(real_offset, data)
210 217
211 218 relative_offset = offset - real_offset
212 219 got = len(data) - relative_offset
213 220 if got < length:
214 221 message = PARTIAL_READ_MSG % (self.filename, length, offset, got)
215 222 raise error.RevlogError(message)
216 223
217 224 if offset != real_offset or real_length != length:
218 225 return util.buffer(data, relative_offset, length)
219 226 return data
220 227
221 228 def _add_cached_chunk(self, offset, data):
222 229 """Add to or replace the cached data chunk.
223 230
224 231 Accepts an absolute offset and the data that is at that location.
225 232 """
226 233 if (
227 234 self._cached_chunk_position + len(self._cached_chunk) == offset
228 235 and len(self._cached_chunk) + len(data) < _MAX_CACHED_CHUNK_SIZE
229 236 ):
230 237 # add to existing cache
231 238 self._cached_chunk += data
232 239 else:
233 240 self._cached_chunk = data
234 241 self._cached_chunk_position = offset
@@ -1,1136 +1,1134 b''
1 1 Test exchange of common information using bundle2
2 2
3 3
4 4 $ getmainid() {
5 5 > hg -R main log --template '{node}\n' --rev "$1"
6 6 > }
7 7
8 8 enable obsolescence
9 9
10 10 $ cp $HGRCPATH $TESTTMP/hgrc.orig
11 11 $ cat > $TESTTMP/bundle2-pushkey-hook.sh << EOF
12 12 > echo pushkey: lock state after \"\$HG_NAMESPACE\"
13 13 > hg debuglock
14 14 > EOF
15 15
16 16 $ cat >> $HGRCPATH << EOF
17 17 > [experimental]
18 18 > evolution.createmarkers=True
19 19 > evolution.exchange=True
20 20 > bundle2-output-capture=True
21 21 > [command-templates]
22 22 > log={rev}:{node|short} {phase} {author} {bookmarks} {desc|firstline}
23 23 > [web]
24 24 > push_ssl = false
25 25 > allow_push = *
26 26 > [phases]
27 27 > publish=False
28 28 > [hooks]
29 29 > pretxnclose.tip = hg log -r tip -T "pre-close-tip:{node|short} {phase} {bookmarks}\n"
30 30 > txnclose.tip = hg log -r tip -T "postclose-tip:{node|short} {phase} {bookmarks}\n"
31 31 > txnclose.env = sh -c "HG_LOCAL= printenv.py txnclose"
32 32 > pushkey= sh "$TESTTMP/bundle2-pushkey-hook.sh"
33 33 > EOF
34 34
35 35 The extension requires a repo (currently unused)
36 36
37 37 $ hg init main
38 38 $ cd main
39 39 $ touch a
40 40 $ hg add a
41 41 $ hg commit -m 'a'
42 42 pre-close-tip:3903775176ed draft
43 43 postclose-tip:3903775176ed draft
44 44 txnclose hook: HG_HOOKNAME=txnclose.env HG_HOOKTYPE=txnclose HG_PHASES_MOVED=1 HG_TXNID=TXN:$ID$ HG_TXNNAME=commit
45 45
46 46 $ hg unbundle $TESTDIR/bundles/rebase.hg
47 47 adding changesets
48 48 adding manifests
49 49 adding file changes
50 50 pre-close-tip:02de42196ebe draft
51 51 added 8 changesets with 7 changes to 7 files (+3 heads)
52 52 new changesets cd010b8cd998:02de42196ebe (8 drafts)
53 53 postclose-tip:02de42196ebe draft
54 54 txnclose hook: HG_HOOKNAME=txnclose.env HG_HOOKTYPE=txnclose HG_NODE=cd010b8cd998f3981a5a8115f94f8da4ab506089 HG_NODE_LAST=02de42196ebee42ef284b6780a87cdc96e8eaab6 HG_PHASES_MOVED=1 HG_SOURCE=unbundle HG_TXNID=TXN:$ID$ HG_TXNNAME=unbundle
55 55 bundle:*/tests/bundles/rebase.hg HG_URL=bundle:*/tests/bundles/rebase.hg (glob)
56 56 (run 'hg heads' to see heads, 'hg merge' to merge)
57 57
58 58 $ cd ..
59 59
60 60 Real world exchange
61 61 =====================
62 62
63 63 Add more obsolescence information
64 64
65 65 $ hg -R main debugobsolete -d '0 0' 1111111111111111111111111111111111111111 `getmainid 9520eea781bc`
66 66 pre-close-tip:02de42196ebe draft
67 67 1 new obsolescence markers
68 68 postclose-tip:02de42196ebe draft
69 69 txnclose hook: HG_HOOKNAME=txnclose.env HG_HOOKTYPE=txnclose HG_NEW_OBSMARKERS=1 HG_TXNID=TXN:$ID$ HG_TXNNAME=debugobsolete
70 70 $ hg -R main debugobsolete -d '0 0' 2222222222222222222222222222222222222222 `getmainid 24b6387c8c8c`
71 71 pre-close-tip:02de42196ebe draft
72 72 1 new obsolescence markers
73 73 postclose-tip:02de42196ebe draft
74 74 txnclose hook: HG_HOOKNAME=txnclose.env HG_HOOKTYPE=txnclose HG_NEW_OBSMARKERS=1 HG_TXNID=TXN:$ID$ HG_TXNNAME=debugobsolete
75 75
76 76 clone --pull
77 77
78 78 $ hg -R main phase --public cd010b8cd998
79 79 pre-close-tip:02de42196ebe draft
80 80 postclose-tip:02de42196ebe draft
81 81 txnclose hook: HG_HOOKNAME=txnclose.env HG_HOOKTYPE=txnclose HG_PHASES_MOVED=1 HG_TXNID=TXN:$ID$ HG_TXNNAME=phase
82 82 $ hg clone main other --pull --rev 9520eea781bc
83 83 adding changesets
84 84 adding manifests
85 85 adding file changes
86 86 pre-close-tip:9520eea781bc draft
87 87 added 2 changesets with 2 changes to 2 files
88 88 1 new obsolescence markers
89 89 new changesets cd010b8cd998:9520eea781bc (1 drafts)
90 90 postclose-tip:9520eea781bc draft
91 91 txnclose hook: HG_HOOKNAME=txnclose.env HG_HOOKTYPE=txnclose HG_NEW_OBSMARKERS=1 HG_NODE=cd010b8cd998f3981a5a8115f94f8da4ab506089 HG_NODE_LAST=9520eea781bcca16c1e15acc0ba14335a0e8e5ba HG_PHASES_MOVED=1 HG_SOURCE=pull HG_TXNID=TXN:$ID$ HG_TXNNAME=pull
92 92 file:/*/$TESTTMP/main HG_URL=file:$TESTTMP/main (glob)
93 93 updating to branch default
94 94 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
95 95 $ hg -R other log -G
96 96 @ 1:9520eea781bc draft Nicolas Dumazet <nicdumz.commits@gmail.com> E
97 97 |
98 98 o 0:cd010b8cd998 public Nicolas Dumazet <nicdumz.commits@gmail.com> A
99 99
100 100 $ hg -R other debugobsolete
101 101 1111111111111111111111111111111111111111 9520eea781bcca16c1e15acc0ba14335a0e8e5ba 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
102 102
103 103 pull
104 104
105 105 $ hg -R main phase --public 9520eea781bc
106 106 pre-close-tip:02de42196ebe draft
107 107 postclose-tip:02de42196ebe draft
108 108 txnclose hook: HG_HOOKNAME=txnclose.env HG_HOOKTYPE=txnclose HG_PHASES_MOVED=1 HG_TXNID=TXN:$ID$ HG_TXNNAME=phase
109 109 $ hg -R other pull -r 24b6387c8c8c
110 110 pulling from $TESTTMP/main
111 111 searching for changes
112 112 adding changesets
113 113 adding manifests
114 114 adding file changes
115 115 pre-close-tip:24b6387c8c8c draft
116 116 added 1 changesets with 1 changes to 1 files (+1 heads)
117 117 1 new obsolescence markers
118 118 new changesets 24b6387c8c8c (1 drafts)
119 119 postclose-tip:24b6387c8c8c draft
120 120 txnclose hook: HG_HOOKNAME=txnclose.env HG_HOOKTYPE=txnclose HG_NEW_OBSMARKERS=1 HG_NODE=24b6387c8c8cae37178880f3fa95ded3cb1cf785 HG_NODE_LAST=24b6387c8c8cae37178880f3fa95ded3cb1cf785 HG_PHASES_MOVED=1 HG_SOURCE=pull HG_TXNID=TXN:$ID$ HG_TXNNAME=pull
121 121 file:/*/$TESTTMP/main HG_URL=file:$TESTTMP/main (glob)
122 122 (run 'hg heads' to see heads, 'hg merge' to merge)
123 123 $ hg -R other log -G
124 124 o 2:24b6387c8c8c draft Nicolas Dumazet <nicdumz.commits@gmail.com> F
125 125 |
126 126 | @ 1:9520eea781bc draft Nicolas Dumazet <nicdumz.commits@gmail.com> E
127 127 |/
128 128 o 0:cd010b8cd998 public Nicolas Dumazet <nicdumz.commits@gmail.com> A
129 129
130 130 $ hg -R other debugobsolete
131 131 1111111111111111111111111111111111111111 9520eea781bcca16c1e15acc0ba14335a0e8e5ba 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
132 132 2222222222222222222222222222222222222222 24b6387c8c8cae37178880f3fa95ded3cb1cf785 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
133 133
134 134 pull empty (with phase movement)
135 135
136 136 $ hg -R main phase --public 24b6387c8c8c
137 137 pre-close-tip:02de42196ebe draft
138 138 postclose-tip:02de42196ebe draft
139 139 txnclose hook: HG_HOOKNAME=txnclose.env HG_HOOKTYPE=txnclose HG_PHASES_MOVED=1 HG_TXNID=TXN:$ID$ HG_TXNNAME=phase
140 140 $ hg -R other pull -r 24b6387c8c8c
141 141 pulling from $TESTTMP/main
142 142 no changes found
143 143 pre-close-tip:24b6387c8c8c public
144 144 1 local changesets published
145 145 postclose-tip:24b6387c8c8c public
146 146 txnclose hook: HG_HOOKNAME=txnclose.env HG_HOOKTYPE=txnclose HG_NEW_OBSMARKERS=0 HG_PHASES_MOVED=1 HG_SOURCE=pull HG_TXNID=TXN:$ID$ HG_TXNNAME=pull
147 147 file:/*/$TESTTMP/main HG_URL=file:$TESTTMP/main (glob)
148 148 $ hg -R other log -G
149 149 o 2:24b6387c8c8c public Nicolas Dumazet <nicdumz.commits@gmail.com> F
150 150 |
151 151 | @ 1:9520eea781bc draft Nicolas Dumazet <nicdumz.commits@gmail.com> E
152 152 |/
153 153 o 0:cd010b8cd998 public Nicolas Dumazet <nicdumz.commits@gmail.com> A
154 154
155 155 $ hg -R other debugobsolete
156 156 1111111111111111111111111111111111111111 9520eea781bcca16c1e15acc0ba14335a0e8e5ba 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
157 157 2222222222222222222222222222222222222222 24b6387c8c8cae37178880f3fa95ded3cb1cf785 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
158 158
159 159 pull empty
160 160
161 161 $ hg -R other pull -r 24b6387c8c8c
162 162 pulling from $TESTTMP/main
163 163 no changes found
164 164 pre-close-tip:24b6387c8c8c public
165 165 postclose-tip:24b6387c8c8c public
166 166 txnclose hook: HG_HOOKNAME=txnclose.env HG_HOOKTYPE=txnclose HG_NEW_OBSMARKERS=0 HG_SOURCE=pull HG_TXNID=TXN:$ID$ HG_TXNNAME=pull
167 167 file:/*/$TESTTMP/main HG_URL=file:$TESTTMP/main (glob)
168 168 $ hg -R other log -G
169 169 o 2:24b6387c8c8c public Nicolas Dumazet <nicdumz.commits@gmail.com> F
170 170 |
171 171 | @ 1:9520eea781bc draft Nicolas Dumazet <nicdumz.commits@gmail.com> E
172 172 |/
173 173 o 0:cd010b8cd998 public Nicolas Dumazet <nicdumz.commits@gmail.com> A
174 174
175 175 $ hg -R other debugobsolete
176 176 1111111111111111111111111111111111111111 9520eea781bcca16c1e15acc0ba14335a0e8e5ba 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
177 177 2222222222222222222222222222222222222222 24b6387c8c8cae37178880f3fa95ded3cb1cf785 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
178 178
179 179 add extra data to test their exchange during push
180 180
181 181 $ hg -R main bookmark --rev eea13746799a book_eea1
182 182 pre-close-tip:02de42196ebe draft
183 183 postclose-tip:02de42196ebe draft
184 184 txnclose hook: HG_BOOKMARK_MOVED=1 HG_HOOKNAME=txnclose.env HG_HOOKTYPE=txnclose HG_TXNID=TXN:$ID$ HG_TXNNAME=bookmark
185 185 $ hg -R main debugobsolete -d '0 0' 3333333333333333333333333333333333333333 `getmainid eea13746799a`
186 186 pre-close-tip:02de42196ebe draft
187 187 1 new obsolescence markers
188 188 postclose-tip:02de42196ebe draft
189 189 txnclose hook: HG_HOOKNAME=txnclose.env HG_HOOKTYPE=txnclose HG_NEW_OBSMARKERS=1 HG_TXNID=TXN:$ID$ HG_TXNNAME=debugobsolete
190 190 $ hg -R main bookmark --rev 02de42196ebe book_02de
191 191 pre-close-tip:02de42196ebe draft book_02de
192 192 postclose-tip:02de42196ebe draft book_02de
193 193 txnclose hook: HG_BOOKMARK_MOVED=1 HG_HOOKNAME=txnclose.env HG_HOOKTYPE=txnclose HG_TXNID=TXN:$ID$ HG_TXNNAME=bookmark
194 194 $ hg -R main debugobsolete -d '0 0' 4444444444444444444444444444444444444444 `getmainid 02de42196ebe`
195 195 pre-close-tip:02de42196ebe draft book_02de
196 196 1 new obsolescence markers
197 197 postclose-tip:02de42196ebe draft book_02de
198 198 txnclose hook: HG_HOOKNAME=txnclose.env HG_HOOKTYPE=txnclose HG_NEW_OBSMARKERS=1 HG_TXNID=TXN:$ID$ HG_TXNNAME=debugobsolete
199 199 $ hg -R main bookmark --rev 42ccdea3bb16 book_42cc
200 200 pre-close-tip:02de42196ebe draft book_02de
201 201 postclose-tip:02de42196ebe draft book_02de
202 202 txnclose hook: HG_BOOKMARK_MOVED=1 HG_HOOKNAME=txnclose.env HG_HOOKTYPE=txnclose HG_TXNID=TXN:$ID$ HG_TXNNAME=bookmark
203 203 $ hg -R main debugobsolete -d '0 0' 5555555555555555555555555555555555555555 `getmainid 42ccdea3bb16`
204 204 pre-close-tip:02de42196ebe draft book_02de
205 205 1 new obsolescence markers
206 206 postclose-tip:02de42196ebe draft book_02de
207 207 txnclose hook: HG_HOOKNAME=txnclose.env HG_HOOKTYPE=txnclose HG_NEW_OBSMARKERS=1 HG_TXNID=TXN:$ID$ HG_TXNNAME=debugobsolete
208 208 $ hg -R main bookmark --rev 5fddd98957c8 book_5fdd
209 209 pre-close-tip:02de42196ebe draft book_02de
210 210 postclose-tip:02de42196ebe draft book_02de
211 211 txnclose hook: HG_BOOKMARK_MOVED=1 HG_HOOKNAME=txnclose.env HG_HOOKTYPE=txnclose HG_TXNID=TXN:$ID$ HG_TXNNAME=bookmark
212 212 $ hg -R main debugobsolete -d '0 0' 6666666666666666666666666666666666666666 `getmainid 5fddd98957c8`
213 213 pre-close-tip:02de42196ebe draft book_02de
214 214 1 new obsolescence markers
215 215 postclose-tip:02de42196ebe draft book_02de
216 216 txnclose hook: HG_HOOKNAME=txnclose.env HG_HOOKTYPE=txnclose HG_NEW_OBSMARKERS=1 HG_TXNID=TXN:$ID$ HG_TXNNAME=debugobsolete
217 217 $ hg -R main bookmark --rev 32af7686d403 book_32af
218 218 pre-close-tip:02de42196ebe draft book_02de
219 219 postclose-tip:02de42196ebe draft book_02de
220 220 txnclose hook: HG_BOOKMARK_MOVED=1 HG_HOOKNAME=txnclose.env HG_HOOKTYPE=txnclose HG_TXNID=TXN:$ID$ HG_TXNNAME=bookmark
221 221 $ hg -R main debugobsolete -d '0 0' 7777777777777777777777777777777777777777 `getmainid 32af7686d403`
222 222 pre-close-tip:02de42196ebe draft book_02de
223 223 1 new obsolescence markers
224 224 postclose-tip:02de42196ebe draft book_02de
225 225 txnclose hook: HG_HOOKNAME=txnclose.env HG_HOOKTYPE=txnclose HG_NEW_OBSMARKERS=1 HG_TXNID=TXN:$ID$ HG_TXNNAME=debugobsolete
226 226
227 227 $ hg -R other bookmark --rev cd010b8cd998 book_eea1
228 228 pre-close-tip:24b6387c8c8c public
229 229 postclose-tip:24b6387c8c8c public
230 230 txnclose hook: HG_BOOKMARK_MOVED=1 HG_HOOKNAME=txnclose.env HG_HOOKTYPE=txnclose HG_TXNID=TXN:$ID$ HG_TXNNAME=bookmark
231 231 $ hg -R other bookmark --rev cd010b8cd998 book_02de
232 232 pre-close-tip:24b6387c8c8c public
233 233 postclose-tip:24b6387c8c8c public
234 234 txnclose hook: HG_BOOKMARK_MOVED=1 HG_HOOKNAME=txnclose.env HG_HOOKTYPE=txnclose HG_TXNID=TXN:$ID$ HG_TXNNAME=bookmark
235 235 $ hg -R other bookmark --rev cd010b8cd998 book_42cc
236 236 pre-close-tip:24b6387c8c8c public
237 237 postclose-tip:24b6387c8c8c public
238 238 txnclose hook: HG_BOOKMARK_MOVED=1 HG_HOOKNAME=txnclose.env HG_HOOKTYPE=txnclose HG_TXNID=TXN:$ID$ HG_TXNNAME=bookmark
239 239 $ hg -R other bookmark --rev cd010b8cd998 book_5fdd
240 240 pre-close-tip:24b6387c8c8c public
241 241 postclose-tip:24b6387c8c8c public
242 242 txnclose hook: HG_BOOKMARK_MOVED=1 HG_HOOKNAME=txnclose.env HG_HOOKTYPE=txnclose HG_TXNID=TXN:$ID$ HG_TXNNAME=bookmark
243 243 $ hg -R other bookmark --rev cd010b8cd998 book_32af
244 244 pre-close-tip:24b6387c8c8c public
245 245 postclose-tip:24b6387c8c8c public
246 246 txnclose hook: HG_BOOKMARK_MOVED=1 HG_HOOKNAME=txnclose.env HG_HOOKTYPE=txnclose HG_TXNID=TXN:$ID$ HG_TXNNAME=bookmark
247 247
248 248 $ hg -R main phase --public eea13746799a
249 249 pre-close-tip:02de42196ebe draft book_02de
250 250 postclose-tip:02de42196ebe draft book_02de
251 251 txnclose hook: HG_HOOKNAME=txnclose.env HG_HOOKTYPE=txnclose HG_PHASES_MOVED=1 HG_TXNID=TXN:$ID$ HG_TXNNAME=phase
252 252
253 253 push
254 254 $ hg -R main push other --rev eea13746799a --bookmark book_eea1
255 255 pushing to other
256 256 searching for changes
257 257 remote: adding changesets
258 258 remote: adding manifests
259 259 remote: adding file changes
260 260 remote: pre-close-tip:eea13746799a public book_eea1
261 261 remote: added 1 changesets with 0 changes to 0 files (-1 heads)
262 262 remote: 1 new obsolescence markers
263 263 remote: pushkey: lock state after "bookmarks"
264 264 remote: lock: free
265 265 remote: wlock: free
266 266 remote: postclose-tip:eea13746799a public book_eea1
267 267 remote: txnclose hook: HG_BOOKMARK_MOVED=1 HG_BUNDLE2=1 HG_HOOKNAME=txnclose.env HG_HOOKTYPE=txnclose HG_NEW_OBSMARKERS=1 HG_NODE=eea13746799a9e0bfd88f29d3c2e9dc9389f524f HG_NODE_LAST=eea13746799a9e0bfd88f29d3c2e9dc9389f524f HG_PHASES_MOVED=1 HG_SOURCE=push HG_TXNID=TXN:$ID$ HG_TXNNAME=push HG_URL=file:$TESTTMP/other
268 268 updating bookmark book_eea1
269 269 pre-close-tip:02de42196ebe draft book_02de
270 270 postclose-tip:02de42196ebe draft book_02de
271 271 txnclose hook: HG_HOOKNAME=txnclose.env HG_HOOKTYPE=txnclose HG_SOURCE=push-response HG_TXNID=TXN:$ID$ HG_TXNNAME=push-response
272 272 file:/*/$TESTTMP/other HG_URL=file:$TESTTMP/other (glob)
273 273 $ hg -R other log -G
274 274 o 3:eea13746799a public Nicolas Dumazet <nicdumz.commits@gmail.com> book_eea1 G
275 275 |\
276 276 | o 2:24b6387c8c8c public Nicolas Dumazet <nicdumz.commits@gmail.com> F
277 277 | |
278 278 @ | 1:9520eea781bc public Nicolas Dumazet <nicdumz.commits@gmail.com> E
279 279 |/
280 280 o 0:cd010b8cd998 public Nicolas Dumazet <nicdumz.commits@gmail.com> book_02de book_32af book_42cc book_5fdd A
281 281
282 282 $ hg -R other debugobsolete
283 283 1111111111111111111111111111111111111111 9520eea781bcca16c1e15acc0ba14335a0e8e5ba 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
284 284 2222222222222222222222222222222222222222 24b6387c8c8cae37178880f3fa95ded3cb1cf785 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
285 285 3333333333333333333333333333333333333333 eea13746799a9e0bfd88f29d3c2e9dc9389f524f 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
286 286
287 287 pull over ssh
288 288
289 289 $ hg -R other pull ssh://user@dummy/main -r 02de42196ebe --bookmark book_02de
290 290 pulling from ssh://user@dummy/main
291 291 searching for changes
292 292 adding changesets
293 293 adding manifests
294 294 adding file changes
295 295 updating bookmark book_02de
296 296 pre-close-tip:02de42196ebe draft book_02de
297 297 added 1 changesets with 1 changes to 1 files (+1 heads)
298 298 1 new obsolescence markers
299 299 new changesets 02de42196ebe (1 drafts)
300 300 postclose-tip:02de42196ebe draft book_02de
301 301 txnclose hook: HG_BOOKMARK_MOVED=1 HG_HOOKNAME=txnclose.env HG_HOOKTYPE=txnclose HG_NEW_OBSMARKERS=1 HG_NODE=02de42196ebee42ef284b6780a87cdc96e8eaab6 HG_NODE_LAST=02de42196ebee42ef284b6780a87cdc96e8eaab6 HG_PHASES_MOVED=1 HG_SOURCE=pull HG_TXNID=TXN:$ID$ HG_TXNNAME=pull
302 302 ssh://user@dummy/main HG_URL=ssh://user@dummy/main
303 303 (run 'hg heads' to see heads, 'hg merge' to merge)
304 304 $ hg -R other debugobsolete
305 305 1111111111111111111111111111111111111111 9520eea781bcca16c1e15acc0ba14335a0e8e5ba 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
306 306 2222222222222222222222222222222222222222 24b6387c8c8cae37178880f3fa95ded3cb1cf785 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
307 307 3333333333333333333333333333333333333333 eea13746799a9e0bfd88f29d3c2e9dc9389f524f 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
308 308 4444444444444444444444444444444444444444 02de42196ebee42ef284b6780a87cdc96e8eaab6 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
309 309
310 310 pull over http
311 311
312 312 $ hg serve -R main -p $HGPORT -d --pid-file=main.pid -E main-error.log
313 313 $ cat main.pid >> $DAEMON_PIDS
314 314
315 315 $ hg -R other pull http://localhost:$HGPORT/ -r 42ccdea3bb16 --bookmark book_42cc
316 316 pulling from http://localhost:$HGPORT/
317 317 searching for changes
318 318 adding changesets
319 319 adding manifests
320 320 adding file changes
321 321 updating bookmark book_42cc
322 322 pre-close-tip:42ccdea3bb16 draft book_42cc
323 323 added 1 changesets with 1 changes to 1 files (+1 heads)
324 324 1 new obsolescence markers
325 325 new changesets 42ccdea3bb16 (1 drafts)
326 326 postclose-tip:42ccdea3bb16 draft book_42cc
327 327 txnclose hook: HG_BOOKMARK_MOVED=1 HG_HOOKNAME=txnclose.env HG_HOOKTYPE=txnclose HG_NEW_OBSMARKERS=1 HG_NODE=42ccdea3bb16d28e1848c95fe2e44c000f3f21b1 HG_NODE_LAST=42ccdea3bb16d28e1848c95fe2e44c000f3f21b1 HG_PHASES_MOVED=1 HG_SOURCE=pull HG_TXNID=TXN:$ID$ HG_TXNNAME=pull
328 328 http://localhost:$HGPORT/ HG_URL=http://localhost:$HGPORT/
329 329 (run 'hg heads .' to see heads, 'hg merge' to merge)
330 330 $ cat main-error.log
331 331 $ hg -R other debugobsolete
332 332 1111111111111111111111111111111111111111 9520eea781bcca16c1e15acc0ba14335a0e8e5ba 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
333 333 2222222222222222222222222222222222222222 24b6387c8c8cae37178880f3fa95ded3cb1cf785 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
334 334 3333333333333333333333333333333333333333 eea13746799a9e0bfd88f29d3c2e9dc9389f524f 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
335 335 4444444444444444444444444444444444444444 02de42196ebee42ef284b6780a87cdc96e8eaab6 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
336 336 5555555555555555555555555555555555555555 42ccdea3bb16d28e1848c95fe2e44c000f3f21b1 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
337 337
338 338 push over ssh
339 339
340 340 $ hg -R main push ssh://user@dummy/other -r 5fddd98957c8 --bookmark book_5fdd
341 341 pushing to ssh://user@dummy/other
342 342 searching for changes
343 343 remote: adding changesets
344 344 remote: adding manifests
345 345 remote: adding file changes
346 346 remote: pre-close-tip:5fddd98957c8 draft book_5fdd
347 347 remote: added 1 changesets with 1 changes to 1 files
348 348 remote: 1 new obsolescence markers
349 349 remote: pushkey: lock state after "bookmarks"
350 350 remote: lock: free
351 351 remote: wlock: free
352 352 remote: postclose-tip:5fddd98957c8 draft book_5fdd
353 353 remote: txnclose hook: HG_BOOKMARK_MOVED=1 HG_BUNDLE2=1 HG_HOOKNAME=txnclose.env HG_HOOKTYPE=txnclose HG_NEW_OBSMARKERS=1 HG_NODE=5fddd98957c8a54a4d436dfe1da9d87f21a1b97b HG_NODE_LAST=5fddd98957c8a54a4d436dfe1da9d87f21a1b97b HG_SOURCE=serve HG_TXNID=TXN:$ID$ HG_TXNNAME=serve HG_URL=remote:ssh:$LOCALIP
354 354 updating bookmark book_5fdd
355 355 pre-close-tip:02de42196ebe draft book_02de
356 356 postclose-tip:02de42196ebe draft book_02de
357 357 txnclose hook: HG_HOOKNAME=txnclose.env HG_HOOKTYPE=txnclose HG_SOURCE=push-response HG_TXNID=TXN:$ID$ HG_TXNNAME=push-response
358 358 ssh://user@dummy/other HG_URL=ssh://user@dummy/other
359 359 $ hg -R other log -G
360 360 o 6:5fddd98957c8 draft Nicolas Dumazet <nicdumz.commits@gmail.com> book_5fdd C
361 361 |
362 362 o 5:42ccdea3bb16 draft Nicolas Dumazet <nicdumz.commits@gmail.com> book_42cc B
363 363 |
364 364 | o 4:02de42196ebe draft Nicolas Dumazet <nicdumz.commits@gmail.com> book_02de H
365 365 | |
366 366 | | o 3:eea13746799a public Nicolas Dumazet <nicdumz.commits@gmail.com> book_eea1 G
367 367 | |/|
368 368 | o | 2:24b6387c8c8c public Nicolas Dumazet <nicdumz.commits@gmail.com> F
369 369 |/ /
370 370 | @ 1:9520eea781bc public Nicolas Dumazet <nicdumz.commits@gmail.com> E
371 371 |/
372 372 o 0:cd010b8cd998 public Nicolas Dumazet <nicdumz.commits@gmail.com> book_32af A
373 373
374 374 $ hg -R other debugobsolete
375 375 1111111111111111111111111111111111111111 9520eea781bcca16c1e15acc0ba14335a0e8e5ba 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
376 376 2222222222222222222222222222222222222222 24b6387c8c8cae37178880f3fa95ded3cb1cf785 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
377 377 3333333333333333333333333333333333333333 eea13746799a9e0bfd88f29d3c2e9dc9389f524f 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
378 378 4444444444444444444444444444444444444444 02de42196ebee42ef284b6780a87cdc96e8eaab6 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
379 379 5555555555555555555555555555555555555555 42ccdea3bb16d28e1848c95fe2e44c000f3f21b1 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
380 380 6666666666666666666666666666666666666666 5fddd98957c8a54a4d436dfe1da9d87f21a1b97b 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
381 381
382 382 push over http
383 383
384 384 $ hg serve -R other -p $HGPORT2 -d --pid-file=other.pid -E other-error.log
385 385 $ cat other.pid >> $DAEMON_PIDS
386 386
387 387 $ hg -R main phase --public 32af7686d403
388 388 pre-close-tip:02de42196ebe draft book_02de
389 389 postclose-tip:02de42196ebe draft book_02de
390 390 txnclose hook: HG_HOOKNAME=txnclose.env HG_HOOKTYPE=txnclose HG_PHASES_MOVED=1 HG_TXNID=TXN:$ID$ HG_TXNNAME=phase
391 391 $ hg -R main push http://localhost:$HGPORT2/ -r 32af7686d403 --bookmark book_32af
392 392 pushing to http://localhost:$HGPORT2/
393 393 searching for changes
394 394 remote: adding changesets
395 395 remote: adding manifests
396 396 remote: adding file changes
397 397 remote: pre-close-tip:32af7686d403 public book_32af
398 398 remote: added 1 changesets with 1 changes to 1 files
399 399 remote: 1 new obsolescence markers
400 400 remote: pushkey: lock state after "bookmarks"
401 401 remote: lock: free
402 402 remote: wlock: free
403 403 remote: postclose-tip:32af7686d403 public book_32af
404 404 remote: txnclose hook: HG_BOOKMARK_MOVED=1 HG_BUNDLE2=1 HG_HOOKNAME=txnclose.env HG_HOOKTYPE=txnclose HG_NEW_OBSMARKERS=1 HG_NODE=32af7686d403cf45b5d95f2d70cebea587ac806a HG_NODE_LAST=32af7686d403cf45b5d95f2d70cebea587ac806a HG_PHASES_MOVED=1 HG_SOURCE=serve HG_TXNID=TXN:$ID$ HG_TXNNAME=serve HG_URL=remote:http:$LOCALIP: (glob)
405 405 updating bookmark book_32af
406 406 pre-close-tip:02de42196ebe draft book_02de
407 407 postclose-tip:02de42196ebe draft book_02de
408 408 txnclose hook: HG_HOOKNAME=txnclose.env HG_HOOKTYPE=txnclose HG_SOURCE=push-response HG_TXNID=TXN:$ID$ HG_TXNNAME=push-response
409 409 http://localhost:$HGPORT2/ HG_URL=http://localhost:$HGPORT2/
410 410 $ cat other-error.log
411 411
412 412 Check final content.
413 413
414 414 $ hg -R other log -G
415 415 o 7:32af7686d403 public Nicolas Dumazet <nicdumz.commits@gmail.com> book_32af D
416 416 |
417 417 o 6:5fddd98957c8 public Nicolas Dumazet <nicdumz.commits@gmail.com> book_5fdd C
418 418 |
419 419 o 5:42ccdea3bb16 public Nicolas Dumazet <nicdumz.commits@gmail.com> book_42cc B
420 420 |
421 421 | o 4:02de42196ebe draft Nicolas Dumazet <nicdumz.commits@gmail.com> book_02de H
422 422 | |
423 423 | | o 3:eea13746799a public Nicolas Dumazet <nicdumz.commits@gmail.com> book_eea1 G
424 424 | |/|
425 425 | o | 2:24b6387c8c8c public Nicolas Dumazet <nicdumz.commits@gmail.com> F
426 426 |/ /
427 427 | @ 1:9520eea781bc public Nicolas Dumazet <nicdumz.commits@gmail.com> E
428 428 |/
429 429 o 0:cd010b8cd998 public Nicolas Dumazet <nicdumz.commits@gmail.com> A
430 430
431 431 $ hg -R other debugobsolete
432 432 1111111111111111111111111111111111111111 9520eea781bcca16c1e15acc0ba14335a0e8e5ba 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
433 433 2222222222222222222222222222222222222222 24b6387c8c8cae37178880f3fa95ded3cb1cf785 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
434 434 3333333333333333333333333333333333333333 eea13746799a9e0bfd88f29d3c2e9dc9389f524f 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
435 435 4444444444444444444444444444444444444444 02de42196ebee42ef284b6780a87cdc96e8eaab6 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
436 436 5555555555555555555555555555555555555555 42ccdea3bb16d28e1848c95fe2e44c000f3f21b1 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
437 437 6666666666666666666666666666666666666666 5fddd98957c8a54a4d436dfe1da9d87f21a1b97b 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
438 438 7777777777777777777777777777777777777777 32af7686d403cf45b5d95f2d70cebea587ac806a 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
439 439
440 440 (check that no 'pending' files remain)
441 441
442 442 $ ls -1 other/.hg/bookmarks*
443 443 other/.hg/bookmarks
444 444 $ ls -1 other/.hg/store/phaseroots*
445 445 other/.hg/store/phaseroots
446 446 $ ls -1 other/.hg/store/00changelog.i*
447 447 other/.hg/store/00changelog.i
448 448
449 449 Error Handling
450 450 ==============
451 451
452 452 Check that errors are properly returned to the client during push.
453 453
454 454 Setting up
455 455
456 456 $ cat > failpush.py << EOF
457 457 > """A small extension that makes push fails when using bundle2
458 458 >
459 459 > used to test error handling in bundle2
460 460 > """
461 461 >
462 462 > from mercurial import error
463 463 > from mercurial import bundle2
464 464 > from mercurial import exchange
465 465 > from mercurial import extensions
466 466 > from mercurial import registrar
467 467 > cmdtable = {}
468 468 > command = registrar.command(cmdtable)
469 469 >
470 470 > configtable = {}
471 471 > configitem = registrar.configitem(configtable)
472 472 > configitem(b'failpush', b'reason',
473 473 > default=None,
474 474 > )
475 475 >
476 476 > def _pushbundle2failpart(pushop, bundler):
477 477 > reason = pushop.ui.config(b'failpush', b'reason')
478 478 > part = None
479 479 > if reason == b'abort':
480 480 > bundler.newpart(b'test:abort')
481 481 > if reason == b'unknown':
482 482 > bundler.newpart(b'test:unknown')
483 483 > if reason == b'race':
484 484 > # 20 Bytes of crap
485 485 > bundler.newpart(b'check:heads', data=b'01234567890123456789')
486 486 >
487 487 > @bundle2.parthandler(b"test:abort")
488 488 > def handleabort(op, part):
489 489 > raise error.Abort(b'Abandon ship!', hint=b"don't panic")
490 490 >
491 491 > def uisetup(ui):
492 492 > exchange.b2partsgenmapping[b'failpart'] = _pushbundle2failpart
493 493 > exchange.b2partsgenorder.insert(0, b'failpart')
494 494 >
495 495 > EOF
496 496
497 497 $ cd main
498 498 $ hg up tip
499 499 3 files updated, 0 files merged, 1 files removed, 0 files unresolved
500 500 $ echo 'I' > I
501 501 $ hg add I
502 502 $ hg ci -m 'I'
503 503 pre-close-tip:e7ec4e813ba6 draft
504 504 postclose-tip:e7ec4e813ba6 draft
505 505 txnclose hook: HG_HOOKNAME=txnclose.env HG_HOOKTYPE=txnclose HG_TXNID=TXN:$ID$ HG_TXNNAME=commit
506 506 $ hg id
507 507 e7ec4e813ba6 tip
508 508 $ cd ..
509 509
510 510 $ cat << EOF >> $HGRCPATH
511 511 > [extensions]
512 512 > failpush=$TESTTMP/failpush.py
513 513 > EOF
514 514
515 515 $ killdaemons.py
516 516 $ hg serve -R other -p $HGPORT2 -d --pid-file=other.pid -E other-error.log
517 517 $ cat other.pid >> $DAEMON_PIDS
518 518
519 519 Doing the actual push: Abort error
520 520
521 521 $ cat << EOF >> $HGRCPATH
522 522 > [failpush]
523 523 > reason = abort
524 524 > EOF
525 525
526 526 $ hg -R main push other -r e7ec4e813ba6
527 527 pushing to other
528 528 searching for changes
529 529 abort: Abandon ship!
530 530 (don't panic)
531 531 [255]
532 532
533 533 $ hg -R main push ssh://user@dummy/other -r e7ec4e813ba6
534 534 pushing to ssh://user@dummy/other
535 535 searching for changes
536 536 remote: Abandon ship!
537 537 remote: (don't panic)
538 538 abort: push failed on remote
539 539 [100]
540 540
541 541 $ hg -R main push http://localhost:$HGPORT2/ -r e7ec4e813ba6
542 542 pushing to http://localhost:$HGPORT2/
543 543 searching for changes
544 544 remote: Abandon ship!
545 545 remote: (don't panic)
546 546 abort: push failed on remote
547 547 [100]
548 548
549 549
550 550 Doing the actual push: unknown mandatory parts
551 551
552 552 $ cat << EOF >> $HGRCPATH
553 553 > [failpush]
554 554 > reason = unknown
555 555 > EOF
556 556
557 557 $ hg -R main push other -r e7ec4e813ba6
558 558 pushing to other
559 559 searching for changes
560 560 abort: missing support for test:unknown
561 561 [100]
562 562
563 563 $ hg -R main push ssh://user@dummy/other -r e7ec4e813ba6
564 564 pushing to ssh://user@dummy/other
565 565 searching for changes
566 566 abort: missing support for test:unknown
567 567 [100]
568 568
569 569 $ hg -R main push http://localhost:$HGPORT2/ -r e7ec4e813ba6
570 570 pushing to http://localhost:$HGPORT2/
571 571 searching for changes
572 572 abort: missing support for test:unknown
573 573 [100]
574 574
575 575 Doing the actual push: race
576 576
577 577 $ cat << EOF >> $HGRCPATH
578 578 > [failpush]
579 579 > reason = race
580 580 > EOF
581 581
582 582 $ hg -R main push other -r e7ec4e813ba6
583 583 pushing to other
584 584 searching for changes
585 585 abort: push failed:
586 586 'remote repository changed while pushing - please try again'
587 587 [255]
588 588
589 589 $ hg -R main push ssh://user@dummy/other -r e7ec4e813ba6
590 590 pushing to ssh://user@dummy/other
591 591 searching for changes
592 592 abort: push failed:
593 593 'remote repository changed while pushing - please try again'
594 594 [255]
595 595
596 596 $ hg -R main push http://localhost:$HGPORT2/ -r e7ec4e813ba6
597 597 pushing to http://localhost:$HGPORT2/
598 598 searching for changes
599 599 abort: push failed:
600 600 'remote repository changed while pushing - please try again'
601 601 [255]
602 602
603 603 Doing the actual push: hook abort
604 604
605 605 $ cat << EOF >> $HGRCPATH
606 606 > [failpush]
607 607 > reason =
608 608 > [hooks]
609 609 > pretxnclose.failpush = sh -c "echo 'You shall not pass!'; false"
610 610 > txnabort.failpush = sh -c "echo 'Cleaning up the mess...'"
611 611 > EOF
612 612
613 613 $ killdaemons.py
614 614 $ hg serve -R other -p $HGPORT2 -d --pid-file=other.pid -E other-error.log
615 615 $ cat other.pid >> $DAEMON_PIDS
616 616
617 617 $ hg -R main push other -r e7ec4e813ba6
618 618 pushing to other
619 619 searching for changes
620 620 remote: adding changesets
621 621 remote: adding manifests
622 622 remote: adding file changes
623 623 remote: pre-close-tip:e7ec4e813ba6 draft
624 624 remote: You shall not pass!
625 625 remote: transaction abort!
626 626 remote: Cleaning up the mess...
627 627 remote: rollback completed
628 628 abort: pretxnclose.failpush hook exited with status 1
629 629 [40]
630 630
631 631 $ hg -R main push ssh://user@dummy/other -r e7ec4e813ba6
632 632 pushing to ssh://user@dummy/other
633 633 searching for changes
634 634 remote: adding changesets
635 635 remote: adding manifests
636 636 remote: adding file changes
637 637 remote: pre-close-tip:e7ec4e813ba6 draft
638 638 remote: You shall not pass!
639 639 remote: transaction abort!
640 640 remote: Cleaning up the mess...
641 641 remote: rollback completed
642 642 remote: pretxnclose.failpush hook exited with status 1
643 643 abort: push failed on remote
644 644 [100]
645 645
646 646 $ hg -R main push http://localhost:$HGPORT2/ -r e7ec4e813ba6
647 647 pushing to http://localhost:$HGPORT2/
648 648 searching for changes
649 649 remote: adding changesets
650 650 remote: adding manifests
651 651 remote: adding file changes
652 652 remote: pre-close-tip:e7ec4e813ba6 draft
653 653 remote: You shall not pass!
654 654 remote: transaction abort!
655 655 remote: Cleaning up the mess...
656 656 remote: rollback completed
657 657 remote: pretxnclose.failpush hook exited with status 1
658 658 abort: push failed on remote
659 659 [100]
660 660
661 661 (check that no 'pending' files remain)
662 662
663 663 $ ls -1 other/.hg/bookmarks*
664 664 other/.hg/bookmarks
665 665 $ ls -1 other/.hg/store/phaseroots*
666 666 other/.hg/store/phaseroots
667 667 $ ls -1 other/.hg/store/00changelog.i*
668 668 other/.hg/store/00changelog.i
669 669
670 670 Check error from hook during the unbundling process itself
671 671
672 672 $ cat << EOF >> $HGRCPATH
673 673 > pretxnchangegroup = sh -c "echo 'Fail early!'; false"
674 674 > EOF
675 675 $ killdaemons.py # reload http config
676 676 $ hg serve -R other -p $HGPORT2 -d --pid-file=other.pid -E other-error.log
677 677 $ cat other.pid >> $DAEMON_PIDS
678 678
679 679 $ hg -R main push other -r e7ec4e813ba6
680 680 pushing to other
681 681 searching for changes
682 682 remote: adding changesets
683 683 remote: adding manifests
684 684 remote: adding file changes
685 685 remote: Fail early!
686 686 remote: transaction abort!
687 687 remote: Cleaning up the mess...
688 688 remote: rollback completed
689 689 abort: pretxnchangegroup hook exited with status 1
690 690 [40]
691 691 $ hg -R main push ssh://user@dummy/other -r e7ec4e813ba6
692 692 pushing to ssh://user@dummy/other
693 693 searching for changes
694 694 remote: adding changesets
695 695 remote: adding manifests
696 696 remote: adding file changes
697 697 remote: Fail early!
698 698 remote: transaction abort!
699 699 remote: Cleaning up the mess...
700 700 remote: rollback completed
701 701 remote: pretxnchangegroup hook exited with status 1
702 702 abort: push failed on remote
703 703 [100]
704 704 $ hg -R main push http://localhost:$HGPORT2/ -r e7ec4e813ba6
705 705 pushing to http://localhost:$HGPORT2/
706 706 searching for changes
707 707 remote: adding changesets
708 708 remote: adding manifests
709 709 remote: adding file changes
710 710 remote: Fail early!
711 711 remote: transaction abort!
712 712 remote: Cleaning up the mess...
713 713 remote: rollback completed
714 714 remote: pretxnchangegroup hook exited with status 1
715 715 abort: push failed on remote
716 716 [100]
717 717
718 718 Check output capture control.
719 719
720 720 (should be still forced for http, disabled for local and ssh)
721 721
722 722 $ cat >> $HGRCPATH << EOF
723 723 > [experimental]
724 724 > bundle2-output-capture=False
725 725 > EOF
726 726
727 727 $ hg -R main push other -r e7ec4e813ba6
728 728 pushing to other
729 729 searching for changes
730 730 adding changesets
731 731 adding manifests
732 732 adding file changes
733 733 Fail early!
734 734 transaction abort!
735 735 Cleaning up the mess...
736 736 rollback completed
737 737 abort: pretxnchangegroup hook exited with status 1
738 738 [40]
739 739 $ hg -R main push ssh://user@dummy/other -r e7ec4e813ba6
740 740 pushing to ssh://user@dummy/other
741 741 searching for changes
742 742 remote: adding changesets
743 743 remote: adding manifests
744 744 remote: adding file changes
745 745 remote: Fail early!
746 746 remote: transaction abort!
747 747 remote: Cleaning up the mess...
748 748 remote: rollback completed
749 749 remote: pretxnchangegroup hook exited with status 1
750 750 abort: push failed on remote
751 751 [100]
752 752 $ hg -R main push http://localhost:$HGPORT2/ -r e7ec4e813ba6
753 753 pushing to http://localhost:$HGPORT2/
754 754 searching for changes
755 755 remote: adding changesets
756 756 remote: adding manifests
757 757 remote: adding file changes
758 758 remote: Fail early!
759 759 remote: transaction abort!
760 760 remote: Cleaning up the mess...
761 761 remote: rollback completed
762 762 remote: pretxnchangegroup hook exited with status 1
763 763 abort: push failed on remote
764 764 [100]
765 765
766 766 Check abort from mandatory pushkey
767 767
768 768 $ cat > mandatorypart.py << EOF
769 769 > from mercurial import exchange
770 770 > from mercurial import pushkey
771 771 > from mercurial import node
772 772 > from mercurial import error
773 773 > @exchange.b2partsgenerator(b'failingpuskey')
774 774 > def addfailingpushey(pushop, bundler):
775 775 > enc = pushkey.encode
776 776 > part = bundler.newpart(b'pushkey')
777 777 > part.addparam(b'namespace', enc(b'phases'))
778 778 > part.addparam(b'key', enc(b'cd010b8cd998f3981a5a8115f94f8da4ab506089'))
779 779 > part.addparam(b'old', enc(b'0')) # successful update
780 780 > part.addparam(b'new', enc(b'0'))
781 781 > def fail(pushop, exc):
782 782 > raise error.Abort(b'Correct phase push failed (because hooks)')
783 783 > pushop.pkfailcb[part.id] = fail
784 784 > EOF
785 785 $ cat >> $HGRCPATH << EOF
786 786 > [hooks]
787 787 > pretxnchangegroup=
788 788 > pretxnclose.failpush=
789 789 > prepushkey.failpush = sh -c "echo 'do not push the key !'; false"
790 790 > [extensions]
791 791 > mandatorypart=$TESTTMP/mandatorypart.py
792 792 > EOF
793 793 $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS # reload http config
794 794 $ hg serve -R other -p $HGPORT2 -d --pid-file=other.pid -E other-error.log
795 795 $ cat other.pid >> $DAEMON_PIDS
796 796
797 797 (Failure from a hook)
798 798
799 799 $ hg -R main push other -r e7ec4e813ba6
800 800 pushing to other
801 801 searching for changes
802 802 adding changesets
803 803 adding manifests
804 804 adding file changes
805 805 do not push the key !
806 806 pushkey-abort: prepushkey.failpush hook exited with status 1
807 807 transaction abort!
808 808 Cleaning up the mess...
809 809 rollback completed
810 810 abort: Correct phase push failed (because hooks)
811 811 [255]
812 812 $ hg -R main push ssh://user@dummy/other -r e7ec4e813ba6
813 813 pushing to ssh://user@dummy/other
814 814 searching for changes
815 815 remote: adding changesets
816 816 remote: adding manifests
817 817 remote: adding file changes
818 818 remote: do not push the key !
819 819 remote: pushkey-abort: prepushkey.failpush hook exited with status 1
820 820 remote: transaction abort!
821 821 remote: Cleaning up the mess...
822 822 remote: rollback completed
823 823 abort: Correct phase push failed (because hooks)
824 824 [255]
825 825 $ hg -R main push http://localhost:$HGPORT2/ -r e7ec4e813ba6
826 826 pushing to http://localhost:$HGPORT2/
827 827 searching for changes
828 828 remote: adding changesets
829 829 remote: adding manifests
830 830 remote: adding file changes
831 831 remote: do not push the key !
832 832 remote: pushkey-abort: prepushkey.failpush hook exited with status 1
833 833 remote: transaction abort!
834 834 remote: Cleaning up the mess...
835 835 remote: rollback completed
836 836 abort: Correct phase push failed (because hooks)
837 837 [255]
838 838
839 839 (Failure from a the pushkey)
840 840
841 841 $ cat > mandatorypart.py << EOF
842 842 > from mercurial import exchange
843 843 > from mercurial import pushkey
844 844 > from mercurial import node
845 845 > from mercurial import error
846 846 > @exchange.b2partsgenerator(b'failingpuskey')
847 847 > def addfailingpushey(pushop, bundler):
848 848 > enc = pushkey.encode
849 849 > part = bundler.newpart(b'pushkey')
850 850 > part.addparam(b'namespace', enc(b'phases'))
851 851 > part.addparam(b'key', enc(b'cd010b8cd998f3981a5a8115f94f8da4ab506089'))
852 852 > part.addparam(b'old', enc(b'4')) # will fail
853 853 > part.addparam(b'new', enc(b'3'))
854 854 > def fail(pushop, exc):
855 855 > raise error.Abort(b'Clown phase push failed')
856 856 > pushop.pkfailcb[part.id] = fail
857 857 > EOF
858 858 $ cat >> $HGRCPATH << EOF
859 859 > [hooks]
860 860 > prepushkey.failpush =
861 861 > EOF
862 862 $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS # reload http config
863 863 $ hg serve -R other -p $HGPORT2 -d --pid-file=other.pid -E other-error.log
864 864 $ cat other.pid >> $DAEMON_PIDS
865 865
866 866 $ hg -R main push other -r e7ec4e813ba6
867 867 pushing to other
868 868 searching for changes
869 869 adding changesets
870 870 adding manifests
871 871 adding file changes
872 872 transaction abort!
873 873 Cleaning up the mess...
874 874 rollback completed
875 875 pushkey: lock state after "phases"
876 876 lock: free
877 877 wlock: free
878 878 abort: Clown phase push failed
879 879 [255]
880 880 $ hg -R main push ssh://user@dummy/other -r e7ec4e813ba6
881 881 pushing to ssh://user@dummy/other
882 882 searching for changes
883 883 remote: adding changesets
884 884 remote: adding manifests
885 885 remote: adding file changes
886 886 remote: transaction abort!
887 887 remote: Cleaning up the mess...
888 888 remote: rollback completed
889 889 remote: pushkey: lock state after "phases"
890 890 remote: lock: free
891 891 remote: wlock: free
892 892 abort: Clown phase push failed
893 893 [255]
894 894 $ hg -R main push http://localhost:$HGPORT2/ -r e7ec4e813ba6
895 895 pushing to http://localhost:$HGPORT2/
896 896 searching for changes
897 897 remote: adding changesets
898 898 remote: adding manifests
899 899 remote: adding file changes
900 900 remote: transaction abort!
901 901 remote: Cleaning up the mess...
902 902 remote: rollback completed
903 903 remote: pushkey: lock state after "phases"
904 904 remote: lock: free
905 905 remote: wlock: free
906 906 abort: Clown phase push failed
907 907 [255]
908 908
909 909 Test lazily acquiring the lock during unbundle
910 910 $ cp $TESTTMP/hgrc.orig $HGRCPATH
911 911
912 912 $ cat >> $TESTTMP/locktester.py <<EOF
913 913 > import os
914 914 > from mercurial import bundle2, error, extensions
915 915 > def checklock(orig, repo, *args, **kwargs):
916 916 > if repo.svfs.lexists(b"lock"):
917 917 > raise error.Abort(b"Lock should not be taken")
918 918 > return orig(repo, *args, **kwargs)
919 919 > def extsetup(ui):
920 920 > extensions.wrapfunction(bundle2, 'processbundle', checklock)
921 921 > EOF
922 922
923 923 $ hg init lazylock
924 924 $ cat >> lazylock/.hg/hgrc <<EOF
925 925 > [extensions]
926 926 > locktester=$TESTTMP/locktester.py
927 927 > EOF
928 928
929 929 $ hg clone -q ssh://user@dummy/lazylock lazylockclient
930 930 $ cd lazylockclient
931 931 $ touch a && hg ci -Aqm a
932 932 $ hg push
933 933 pushing to ssh://user@dummy/lazylock
934 934 searching for changes
935 935 remote: Lock should not be taken
936 936 abort: push failed on remote
937 937 [100]
938 938
939 939 $ cat >> ../lazylock/.hg/hgrc <<EOF
940 940 > [experimental]
941 941 > bundle2lazylocking=True
942 942 > EOF
943 943 $ hg push
944 944 pushing to ssh://user@dummy/lazylock
945 945 searching for changes
946 946 remote: adding changesets
947 947 remote: adding manifests
948 948 remote: adding file changes
949 949 remote: added 1 changesets with 1 changes to 1 files
950 950
951 951 $ cd ..
952 952
953 953 Servers can disable bundle1 for clone/pull operations
954 954
955 955 $ killdaemons.py
956 956 $ hg init bundle2onlyserver
957 957 $ cd bundle2onlyserver
958 958 $ cat > .hg/hgrc << EOF
959 959 > [server]
960 960 > bundle1.pull = false
961 961 > EOF
962 962
963 963 $ touch foo
964 964 $ hg -q commit -A -m initial
965 965
966 966 $ hg serve -p $HGPORT -d --pid-file=hg.pid
967 967 $ cat hg.pid >> $DAEMON_PIDS
968 968
969 969 $ hg --config devel.legacy.exchange=bundle1 clone http://localhost:$HGPORT/ not-bundle2
970 970 requesting all changes
971 971 abort: remote error:
972 972 incompatible Mercurial client; bundle2 required
973 973 (see https://www.mercurial-scm.org/wiki/IncompatibleClient)
974 974 [100]
975 975 $ killdaemons.py
976 976 $ cd ..
977 977
978 978 bundle1 can still pull non-generaldelta repos when generaldelta bundle1 disabled
979 979
980 980 $ hg --config format.usegeneraldelta=false init notgdserver
981 981 $ cd notgdserver
982 982 $ cat > .hg/hgrc << EOF
983 983 > [server]
984 984 > bundle1gd.pull = false
985 985 > EOF
986 986
987 987 $ touch foo
988 988 $ hg -q commit -A -m initial
989 989 $ hg serve -p $HGPORT -d --pid-file=hg.pid
990 990 $ cat hg.pid >> $DAEMON_PIDS
991 991
992 992 $ hg --config devel.legacy.exchange=bundle1 clone http://localhost:$HGPORT/ not-bundle2-1
993 993 requesting all changes
994 994 adding changesets
995 995 adding manifests
996 996 adding file changes
997 997 added 1 changesets with 1 changes to 1 files
998 998 new changesets 96ee1d7354c4
999 999 updating to branch default
1000 1000 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1001 1001
1002 1002 $ killdaemons.py
1003 1003 $ cd ../bundle2onlyserver
1004 1004
1005 1005 bundle1 pull can be disabled for generaldelta repos only
1006 1006
1007 1007 $ cat > .hg/hgrc << EOF
1008 1008 > [server]
1009 1009 > bundle1gd.pull = false
1010 1010 > EOF
1011 1011
1012 1012 $ hg serve -p $HGPORT -d --pid-file=hg.pid
1013 1013 $ cat hg.pid >> $DAEMON_PIDS
1014 1014 $ hg --config devel.legacy.exchange=bundle1 clone http://localhost:$HGPORT/ not-bundle2
1015 1015 requesting all changes
1016 1016 abort: remote error:
1017 1017 incompatible Mercurial client; bundle2 required
1018 1018 (see https://www.mercurial-scm.org/wiki/IncompatibleClient)
1019 1019 [100]
1020 1020
1021 1021 $ killdaemons.py
1022 1022
1023 1023 Verify the global server.bundle1 option works
1024 1024
1025 1025 $ cd ..
1026 1026 $ cat > bundle2onlyserver/.hg/hgrc << EOF
1027 1027 > [server]
1028 1028 > bundle1 = false
1029 1029 > EOF
1030 1030 $ hg serve -R bundle2onlyserver -p $HGPORT -d --pid-file=hg.pid
1031 1031 $ cat hg.pid >> $DAEMON_PIDS
1032 1032 $ hg --config devel.legacy.exchange=bundle1 clone http://localhost:$HGPORT not-bundle2
1033 1033 requesting all changes
1034 1034 abort: remote error:
1035 1035 incompatible Mercurial client; bundle2 required
1036 1036 (see https://www.mercurial-scm.org/wiki/IncompatibleClient)
1037 1037 [100]
1038 1038 $ killdaemons.py
1039 1039
1040 1040 $ hg --config devel.legacy.exchange=bundle1 clone ssh://user@dummy/bundle2onlyserver not-bundle2-ssh
1041 1041 requesting all changes
1042 1042 adding changesets
1043 1043 remote: abort: incompatible Mercurial client; bundle2 required
1044 1044 remote: (see https://www.mercurial-scm.org/wiki/IncompatibleClient)
1045 transaction abort!
1046 rollback completed
1047 1045 abort: stream ended unexpectedly (got 0 bytes, expected 4)
1048 1046 [255]
1049 1047
1050 1048 $ cat > bundle2onlyserver/.hg/hgrc << EOF
1051 1049 > [server]
1052 1050 > bundle1gd = false
1053 1051 > EOF
1054 1052 $ hg serve -R bundle2onlyserver -p $HGPORT -d --pid-file=hg.pid
1055 1053 $ cat hg.pid >> $DAEMON_PIDS
1056 1054
1057 1055 $ hg --config devel.legacy.exchange=bundle1 clone http://localhost:$HGPORT/ not-bundle2
1058 1056 requesting all changes
1059 1057 abort: remote error:
1060 1058 incompatible Mercurial client; bundle2 required
1061 1059 (see https://www.mercurial-scm.org/wiki/IncompatibleClient)
1062 1060 [100]
1063 1061
1064 1062 $ killdaemons.py
1065 1063
1066 1064 $ cd notgdserver
1067 1065 $ cat > .hg/hgrc << EOF
1068 1066 > [server]
1069 1067 > bundle1gd = false
1070 1068 > EOF
1071 1069 $ hg serve -p $HGPORT -d --pid-file=hg.pid
1072 1070 $ cat hg.pid >> $DAEMON_PIDS
1073 1071
1074 1072 $ hg --config devel.legacy.exchange=bundle1 clone http://localhost:$HGPORT/ not-bundle2-2
1075 1073 requesting all changes
1076 1074 adding changesets
1077 1075 adding manifests
1078 1076 adding file changes
1079 1077 added 1 changesets with 1 changes to 1 files
1080 1078 new changesets 96ee1d7354c4
1081 1079 updating to branch default
1082 1080 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1083 1081
1084 1082 $ killdaemons.py
1085 1083 $ cd ../bundle2onlyserver
1086 1084
1087 1085 Verify bundle1 pushes can be disabled
1088 1086
1089 1087 $ cat > .hg/hgrc << EOF
1090 1088 > [server]
1091 1089 > bundle1.push = false
1092 1090 > [web]
1093 1091 > allow_push = *
1094 1092 > push_ssl = false
1095 1093 > EOF
1096 1094
1097 1095 $ hg serve -p $HGPORT -d --pid-file=hg.pid -E error.log
1098 1096 $ cat hg.pid >> $DAEMON_PIDS
1099 1097 $ cd ..
1100 1098
1101 1099 $ hg clone http://localhost:$HGPORT bundle2-only
1102 1100 requesting all changes
1103 1101 adding changesets
1104 1102 adding manifests
1105 1103 adding file changes
1106 1104 added 1 changesets with 1 changes to 1 files
1107 1105 new changesets 96ee1d7354c4
1108 1106 updating to branch default
1109 1107 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1110 1108 $ cd bundle2-only
1111 1109 $ echo commit > foo
1112 1110 $ hg commit -m commit
1113 1111 $ hg --config devel.legacy.exchange=bundle1 push
1114 1112 pushing to http://localhost:$HGPORT/
1115 1113 searching for changes
1116 1114 abort: remote error:
1117 1115 incompatible Mercurial client; bundle2 required
1118 1116 (see https://www.mercurial-scm.org/wiki/IncompatibleClient)
1119 1117 [100]
1120 1118
1121 1119 (also check with ssh)
1122 1120
1123 1121 $ hg --config devel.legacy.exchange=bundle1 push ssh://user@dummy/bundle2onlyserver
1124 1122 pushing to ssh://user@dummy/bundle2onlyserver
1125 1123 searching for changes
1126 1124 remote: abort: incompatible Mercurial client; bundle2 required
1127 1125 remote: (see https://www.mercurial-scm.org/wiki/IncompatibleClient)
1128 1126 [1]
1129 1127
1130 1128 $ hg push
1131 1129 pushing to http://localhost:$HGPORT/
1132 1130 searching for changes
1133 1131 remote: adding changesets
1134 1132 remote: adding manifests
1135 1133 remote: adding file changes
1136 1134 remote: added 1 changesets with 1 changes to 1 files
@@ -1,952 +1,946 b''
1 1 #require serve zstd
2 2
3 3 Client version is embedded in HTTP request and is effectively dynamic. Pin the
4 4 version so behavior is deterministic.
5 5
6 6 $ cat > fakeversion.py << EOF
7 7 > from mercurial import util
8 8 > util.version = lambda: b'4.2'
9 9 > EOF
10 10
11 11 $ cat >> $HGRCPATH << EOF
12 12 > [extensions]
13 13 > fakeversion = `pwd`/fakeversion.py
14 14 > [format]
15 15 > sparse-revlog = no
16 16 > use-persistent-nodemap = no
17 17 > [devel]
18 18 > legacy.exchange = phases
19 19 > [server]
20 20 > concurrent-push-mode = strict
21 21 > EOF
22 22
23 23 $ hg init server0
24 24 $ cd server0
25 25 $ touch foo
26 26 $ hg -q commit -A -m initial
27 27
28 28 Also disable compression because zstd is optional and causes output to vary
29 29 and because debugging partial responses is hard when compression is involved
30 30
31 31 $ cat > .hg/hgrc << EOF
32 32 > [extensions]
33 33 > badserver = $TESTDIR/testlib/badserverext.py
34 34 > [server]
35 35 > compressionengines = none
36 36 > EOF
37 37
38 38 Failure to accept() socket should result in connection related error message
39 39 ----------------------------------------------------------------------------
40 40
41 41 $ hg serve --config badserver.close-before-accept=true -p $HGPORT -d --pid-file=hg.pid
42 42 $ cat hg.pid > $DAEMON_PIDS
43 43
44 44 $ hg clone http://localhost:$HGPORT/ clone
45 45 abort: error: (\$ECONNRESET\$|\$EADDRNOTAVAIL\$) (re)
46 46 [100]
47 47
48 48 (The server exits on its own, but there is a race between that and starting a new server.
49 49 So ensure the process is dead.)
50 50
51 51 $ killdaemons.py $DAEMON_PIDS
52 52
53 53 Failure immediately after accept() should yield connection related error message
54 54 --------------------------------------------------------------------------------
55 55
56 56 $ hg serve --config badserver.close-after-accept=true -p $HGPORT -d --pid-file=hg.pid
57 57 $ cat hg.pid > $DAEMON_PIDS
58 58
59 59 TODO: this usually outputs good results, but sometimes emits abort:
60 60 error: '' on FreeBSD and OS X.
61 61 What we ideally want are:
62 62
63 63 abort: error: $ECONNRESET$
64 64
65 65 The flakiness in this output was observable easily with
66 66 --runs-per-test=20 on macOS 10.12 during the freeze for 4.2.
67 67 $ hg clone http://localhost:$HGPORT/ clone
68 68 abort: error: * (glob)
69 69 [100]
70 70
71 71 $ killdaemons.py $DAEMON_PIDS
72 72
73 73 Failure to read all bytes in initial HTTP request should yield connection related error message
74 74 -----------------------------------------------------------------------------------------------
75 75
76 76 $ hg serve --config badserver.close-after-recv-bytes=1 -p $HGPORT -d --pid-file=hg.pid -E error.log
77 77 $ cat hg.pid > $DAEMON_PIDS
78 78
79 79 $ hg clone http://localhost:$HGPORT/ clone
80 80 abort: error: bad HTTP status line: * (glob)
81 81 [100]
82 82
83 83 $ killdaemons.py $DAEMON_PIDS
84 84
85 85 $ cat error.log
86 86 readline(1 from ~) -> (1) G
87 87 read limit reached; closing socket
88 88
89 89 $ rm -f error.log
90 90
91 91 Same failure, but server reads full HTTP request line
92 92 -----------------------------------------------------
93 93
94 94 $ hg serve \
95 95 > --config badserver.close-after-recv-patterns="GET /\?cmd=capabilities" \
96 96 > --config badserver.close-after-recv-bytes=7 \
97 97 > -p $HGPORT -d --pid-file=hg.pid -E error.log
98 98 $ cat hg.pid > $DAEMON_PIDS
99 99 $ hg clone http://localhost:$HGPORT/ clone
100 100 abort: error: bad HTTP status line: * (glob)
101 101 [100]
102 102
103 103 $ killdaemons.py $DAEMON_PIDS
104 104
105 105 $ cat error.log
106 106 readline(~) -> (33) GET /?cmd=capabilities HTTP/1.1\r\n
107 107 readline(7 from *) -> (7) Accept- (glob)
108 108 read limit reached; closing socket
109 109
110 110 $ rm -f error.log
111 111
112 112 Failure on subsequent HTTP request on the same socket (cmd?batch)
113 113 -----------------------------------------------------------------
114 114
115 115 $ hg serve \
116 116 > --config badserver.close-after-recv-patterns="GET /\?cmd=batch,GET /\?cmd=batch" \
117 117 > --config badserver.close-after-recv-bytes=15,197 \
118 118 > -p $HGPORT -d --pid-file=hg.pid -E error.log
119 119 $ cat hg.pid > $DAEMON_PIDS
120 120 $ hg clone http://localhost:$HGPORT/ clone
121 121 abort: error: bad HTTP status line: * (glob)
122 122 [100]
123 123
124 124 $ killdaemons.py $DAEMON_PIDS
125 125
126 126 $ cat error.log
127 127 readline(~) -> (33) GET /?cmd=capabilities HTTP/1.1\r\n
128 128 readline(*) -> (27) Accept-Encoding: identity\r\n (glob)
129 129 readline(*) -> (35) accept: application/mercurial-0.1\r\n (glob)
130 130 readline(*) -> (*) host: localhost:$HGPORT\r\n (glob)
131 131 readline(*) -> (49) user-agent: mercurial/proto-1.0 (Mercurial 4.2)\r\n (glob)
132 132 readline(*) -> (2) \r\n (glob)
133 133 sendall(160) -> HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercurial-0.1\r\nContent-Length: *\r\n\r\n (glob)
134 134 sendall(*) -> batch branchmap $USUAL_BUNDLE2_CAPS_NO_PHASES$ changegroupsubset compression=none getbundle httpheader=1024 httpmediatype=0.1rx,0.1tx,0.2tx known lookup pushkey streamreqs=* unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash (glob)
135 135 readline(~) -> (26) GET /?cmd=batch HTTP/1.1\r\n (glob)
136 136 readline(*) -> (1?) Accept-Encoding* (glob)
137 137 read limit reached; closing socket
138 138 readline(~) -> (26) GET /?cmd=batch HTTP/1.1\r\n
139 139 readline(*) -> (27) Accept-Encoding: identity\r\n (glob)
140 140 readline(*) -> (29) vary: X-HgArg-1,X-HgProto-1\r\n (glob)
141 141 readline(*) -> (41) x-hgarg-1: cmds=heads+%3Bknown+nodes%3D\r\n (glob)
142 142 readline(*) -> (61) x-hgproto-1: 0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull\r\n (glob)
143 143 readline(*) -> (35) accept: application/mercurial-0.1\r\n (glob)
144 144 readline(4 from *) -> (4) host (glob)
145 145 read limit reached; closing socket
146 146
147 147 $ rm -f error.log
148 148
149 149 Failure to read getbundle HTTP request
150 150 --------------------------------------
151 151
152 152 $ hg serve \
153 153 > --config badserver.close-after-recv-patterns="GET /\?cmd=batch,user-agent: mercurial/proto-1.0,GET /\?cmd=getbundle" \
154 154 > --config badserver.close-after-recv-bytes=110,26,281 \
155 155 > -p $HGPORT -d --pid-file=hg.pid -E error.log
156 156 $ cat hg.pid > $DAEMON_PIDS
157 157 $ hg clone http://localhost:$HGPORT/ clone
158 158 requesting all changes
159 159 abort: error: bad HTTP status line: * (glob)
160 160 [100]
161 161
162 162 $ killdaemons.py $DAEMON_PIDS
163 163
164 164 $ cat error.log
165 165 readline(1 from -1) -> (1) x (?)
166 166 readline(1 from -1) -> (1) x (?)
167 167 readline(~) -> (33) GET /?cmd=capabilities HTTP/1.1\r\n
168 168 readline(*) -> (27) Accept-Encoding: identity\r\n (glob)
169 169 readline(*) -> (35) accept: application/mercurial-0.1\r\n (glob)
170 170 readline(*) -> (*) host: localhost:$HGPORT\r\n (glob)
171 171 readline(*) -> (49) user-agent: mercurial/proto-1.0 (Mercurial 4.2)\r\n (glob)
172 172 readline(*) -> (2) \r\n (glob)
173 173 sendall(160) -> HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercurial-0.1\r\nContent-Length: *\r\n\r\n (glob)
174 174 sendall(*) -> batch branchmap $USUAL_BUNDLE2_CAPS_NO_PHASES$ changegroupsubset compression=none getbundle httpheader=1024 httpmediatype=0.1rx,0.1tx,0.2tx known lookup pushkey streamreqs=* unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash (glob)
175 175 readline(~) -> (26) GET /?cmd=batch HTTP/1.1\r\n (glob)
176 176 readline(*) -> (27) Accept-Encoding: identity\r\n (glob)
177 177 readline(*) -> (29) vary: X-HgArg-1,X-HgProto-1\r\n (glob)
178 178 readline(*) -> (41) x-hgarg-1: cmds=heads+%3Bknown+nodes%3D\r\n (glob)
179 179 readline(*) -> (1?) x-hgproto-1:* (glob)
180 180 read limit reached; closing socket
181 181 readline(~) -> (26) GET /?cmd=batch HTTP/1.1\r\n
182 182 readline(*) -> (27) Accept-Encoding: identity\r\n (glob)
183 183 readline(*) -> (29) vary: X-HgArg-1,X-HgProto-1\r\n (glob)
184 184 readline(*) -> (41) x-hgarg-1: cmds=heads+%3Bknown+nodes%3D\r\n (glob)
185 185 readline(*) -> (61) x-hgproto-1: 0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull\r\n (glob)
186 186 readline(*) -> (35) accept: application/mercurial-0.1\r\n (glob)
187 187 readline(*) -> (*) host: localhost:$HGPORT\r\n (glob)
188 188 readline(*) -> (49) user-agent: mercurial/proto-1.0 (Mercurial 4.2)\r\n (glob)
189 189 readline(*) -> (2) \r\n (glob)
190 190 sendall(159) -> HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercurial-0.1\r\nContent-Length: 42\r\n\r\n
191 191 sendall(42) -> 96ee1d7354c4ad7372047672c36a1f561e3a6a4c\n;
192 192 readline(24 from ~) -> (*) GET /?cmd=getbundle HTTP* (glob)
193 193 read limit reached; closing socket
194 194 readline(~) -> (30) GET /?cmd=getbundle HTTP/1.1\r\n
195 195 readline(281 from *) -> (27) Accept-Encoding: identity\r\n (glob)
196 196 readline(254 from *) -> (29) vary: X-HgArg-1,X-HgProto-1\r\n (glob)
197 197 readline(225 from *) -> (225) x-hgarg-1: bookmarks=1&bundlecaps=HG20%2Cbundle2%3DHG20%250Abookmarks%250Achangegroup%253D01%252C02%252C03%250Adigests%253Dmd5%252Csha1%252Csha512%250Aerror%253Dabort%252Cunsupportedcontent%252Cpushraced%252Cpushkey%250Ahgtag (glob)
198 198 read limit reached; closing socket
199 199
200 200 $ rm -f error.log
201 201
202 202 Now do a variation using POST to send arguments
203 203 ===============================================
204 204
205 205 $ hg serve \
206 206 > --config badserver.close-after-recv-patterns="x-hgargs-post:,user-agent: mercurial/proto-1.0" \
207 207 > --config badserver.close-after-recv-bytes="14,26" \
208 208 > --config experimental.httppostargs=true \
209 209 > -p $HGPORT -d --pid-file=hg.pid -E error.log
210 210 $ cat hg.pid > $DAEMON_PIDS
211 211
212 212 $ hg clone http://localhost:$HGPORT/ clone
213 213 abort: error: bad HTTP status line: * (glob)
214 214 [100]
215 215
216 216 $ killdaemons.py $DAEMON_PIDS
217 217
218 218 $ cat error.log | "$PYTHON" $TESTDIR/filtertraceback.py
219 219 readline(~) -> (33) GET /?cmd=capabilities HTTP/1.1\r\n
220 220 readline(*) -> (27) Accept-Encoding: identity\r\n (glob)
221 221 readline(*) -> (35) accept: application/mercurial-0.1\r\n (glob)
222 222 readline(*) -> (2?) host: localhost:$HGPORT\r\n (glob)
223 223 readline(*) -> (49) user-agent: mercurial/proto-1.0 (Mercurial 4.2)\r\n (glob)
224 224 readline(*) -> (2) \r\n (glob)
225 225 sendall(160) -> HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercurial-0.1\r\nContent-Length: *\r\n\r\n (glob)
226 226 sendall(*) -> batch branchmap $USUAL_BUNDLE2_CAPS_NO_PHASES$ changegroupsubset compression=none getbundle httpheader=1024 httpmediatype=0.1rx,0.1tx,0.2tx httppostargs known lookup pushkey streamreqs=* unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash (glob)
227 227 readline(~) -> (27) POST /?cmd=batch HTTP/1.1\r\n (glob)
228 228 readline(*) -> (27) Accept-Encoding: identity\r\n (glob)
229 229 readline(*) -> (41) content-type: application/mercurial-0.1\r\n (glob)
230 230 readline(*) -> (33) vary: X-HgArgs-Post,X-HgProto-1\r\n (glob)
231 231 readline(*) -> (19) x-hgargs-post: 28\r\n (glob)
232 232 readline(*) -> (1?) x-hgproto-1: * (glob)
233 233 read limit reached; closing socket
234 234 readline(~) -> (27) POST /?cmd=batch HTTP/1.1\r\n
235 235 readline(*) -> (27) Accept-Encoding: identity\r\n (glob)
236 236 readline(*) -> (41) content-type: application/mercurial-0.1\r\n (glob)
237 237 readline(*) -> (33) vary: X-HgArgs-Post,X-HgProto-1\r\n (glob)
238 238 readline(*) -> (19) x-hgargs-post: 28\r\n (glob)
239 239 readline(*) -> (61) x-hgproto-1: 0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull\r\n (glob)
240 240 readline(*) -> (35) accept: application/mercurial-0.1\r\n (glob)
241 241 readline(*) -> (20) content-length: 28\r\n (glob)
242 242 readline(*) -> (*) host: localhost:$HGPORT\r\n (glob)
243 243 readline(*) -> (49) user-agent: mercurial/proto-1.0 (Mercurial 4.2)\r\n (glob)
244 244 readline(*) -> (2) \r\n (glob)
245 245 read(24 from 28) -> (*) cmds=* (glob)
246 246 read limit reached; closing socket
247 247 $LOCALIP - - [$ERRDATE$] Exception happened during processing request '/?cmd=batch': (glob)
248 248 Traceback (most recent call last):
249 249 Exception: connection closed after receiving N bytes
250 250
251 251
252 252 $ rm -f error.log
253 253
254 254 Now move on to partial server responses
255 255 =======================================
256 256
257 257 Server sends a single character from the HTTP response line
258 258 -----------------------------------------------------------
259 259
260 260 $ hg serve --config badserver.close-after-send-bytes=1 -p $HGPORT -d --pid-file=hg.pid -E error.log
261 261 $ cat hg.pid > $DAEMON_PIDS
262 262
263 263 $ hg clone http://localhost:$HGPORT/ clone
264 264 abort: error: bad HTTP status line: H
265 265 [100]
266 266
267 267 $ killdaemons.py $DAEMON_PIDS
268 268
269 269 $ cat error.log | "$PYTHON" $TESTDIR/filtertraceback.py
270 270 readline(~) -> (33) GET /?cmd=capabilities HTTP/1.1\r\n
271 271 readline(*) -> (27) Accept-Encoding: identity\r\n (glob)
272 272 readline(*) -> (35) accept: application/mercurial-0.1\r\n (glob)
273 273 readline(*) -> (2?) host: localhost:$HGPORT\r\n (glob)
274 274 readline(*) -> (49) user-agent: mercurial/proto-1.0 (Mercurial 4.2)\r\n (glob)
275 275 readline(*) -> (2) \r\n (glob)
276 276 sendall(1 from 160) -> (0) H
277 277 write limit reached; closing socket
278 278 $LOCALIP - - [$ERRDATE$] Exception happened during processing request '/?cmd=capabilities': (glob)
279 279 Traceback (most recent call last):
280 280 Exception: connection closed after sending N bytes
281 281
282 282
283 283 $ rm -f error.log
284 284
285 285 Server sends an incomplete capabilities response body
286 286 -----------------------------------------------------
287 287
288 288 $ hg serve \
289 289 > --config badserver.close-after-send-patterns='batch branchmap bund' \
290 290 > -p $HGPORT -d --pid-file=hg.pid -E error.log
291 291 $ cat hg.pid > $DAEMON_PIDS
292 292
293 293 $ hg clone http://localhost:$HGPORT/ clone
294 294 abort: HTTP request error (incomplete response; expected * bytes got 20) (glob)
295 295 (this may be an intermittent network failure; if the error persists, consider contacting the network or server operator)
296 296 [255]
297 297
298 298 $ killdaemons.py $DAEMON_PIDS
299 299
300 300 $ cat error.log | "$PYTHON" $TESTDIR/filtertraceback.py
301 301 readline(~) -> (33) GET /?cmd=capabilities HTTP/1.1\r\n
302 302 readline(*) -> (27) Accept-Encoding: identity\r\n (glob)
303 303 readline(*) -> (35) accept: application/mercurial-0.1\r\n (glob)
304 304 readline(*) -> (2?) host: localhost:$HGPORT\r\n (glob)
305 305 readline(*) -> (49) user-agent: mercurial/proto-1.0 (Mercurial 4.2)\r\n (glob)
306 306 readline(*) -> (2) \r\n (glob)
307 307 sendall(160) -> HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercurial-0.1\r\nContent-Length: *\r\n\r\n (glob)
308 308 sendall(20 from *) -> (0) batch branchmap bund (glob)
309 309 write limit reached; closing socket
310 310 $LOCALIP - - [$ERRDATE$] Exception happened during processing request '/?cmd=capabilities': (glob)
311 311 Traceback (most recent call last):
312 312 Exception: connection closed after sending N bytes
313 313
314 314
315 315 $ rm -f error.log
316 316
317 317 Server sends incomplete headers for batch request
318 318 -------------------------------------------------
319 319
320 320 $ hg serve \
321 321 > --config badserver.close-after-send-patterns='(.*Content-Type: applicat){2}' \
322 322 > -p $HGPORT -d --pid-file=hg.pid -E error.log
323 323 $ cat hg.pid > $DAEMON_PIDS
324 324
325 325 TODO this output is horrible
326 326
327 327 $ hg clone http://localhost:$HGPORT/ clone
328 328 abort: 'http://localhost:$HGPORT/' does not appear to be an hg repository:
329 329 ---%<--- (applicat)
330 330
331 331 ---%<---
332 332
333 333 [255]
334 334
335 335 $ killdaemons.py $DAEMON_PIDS
336 336
337 337 $ cat error.log | "$PYTHON" $TESTDIR/filtertraceback.py
338 338 readline(~) -> (33) GET /?cmd=capabilities HTTP/1.1\r\n
339 339 readline(*) -> (27) Accept-Encoding: identity\r\n (glob)
340 340 readline(*) -> (35) accept: application/mercurial-0.1\r\n (glob)
341 341 readline(*) -> (2?) host: localhost:$HGPORT\r\n (glob)
342 342 readline(*) -> (49) user-agent: mercurial/proto-1.0 (Mercurial 4.2)\r\n (glob)
343 343 readline(*) -> (2) \r\n (glob)
344 344 sendall(160) -> HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercurial-0.1\r\nContent-Length: *\r\n\r\n (glob)
345 345 sendall(*) -> batch branchmap $USUAL_BUNDLE2_CAPS_NO_PHASES$ changegroupsubset compression=none getbundle httpheader=1024 httpmediatype=0.1rx,0.1tx,0.2tx known lookup pushkey streamreqs=* unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash (glob)
346 346 readline(~) -> (26) GET /?cmd=batch HTTP/1.1\r\n
347 347 readline(*) -> (27) Accept-Encoding: identity\r\n (glob)
348 348 readline(*) -> (29) vary: X-HgArg-1,X-HgProto-1\r\n (glob)
349 349 readline(*) -> (41) x-hgarg-1: cmds=heads+%3Bknown+nodes%3D\r\n (glob)
350 350 readline(*) -> (61) x-hgproto-1: 0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull\r\n (glob)
351 351 readline(*) -> (35) accept: application/mercurial-0.1\r\n (glob)
352 352 readline(*) -> (2?) host: localhost:$HGPORT\r\n (glob)
353 353 readline(*) -> (49) user-agent: mercurial/proto-1.0 (Mercurial 4.2)\r\n (glob)
354 354 readline(*) -> (2) \r\n (glob)
355 355 sendall(118 from 159) -> (0) HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: applicat
356 356 write limit reached; closing socket
357 357 $LOCALIP - - [$ERRDATE$] Exception happened during processing request '/?cmd=batch': (glob)
358 358 Traceback (most recent call last):
359 359 Exception: connection closed after sending N bytes
360 360
361 361
362 362 $ rm -f error.log
363 363
364 364 Server sends an incomplete HTTP response body to batch request
365 365 --------------------------------------------------------------
366 366
367 367 $ hg serve \
368 368 > --config badserver.close-after-send-patterns=96ee1d7354c4ad7372047672 \
369 369 > -p $HGPORT -d --pid-file=hg.pid -E error.log
370 370 $ cat hg.pid > $DAEMON_PIDS
371 371
372 372 $ hg clone http://localhost:$HGPORT/ clone
373 373 abort: unexpected response:
374 374 '96ee1d7354c4ad7372047672'
375 375 [255]
376 376
377 377 $ killdaemons.py $DAEMON_PIDS
378 378
379 379 $ cat error.log | "$PYTHON" $TESTDIR/filtertraceback.py
380 380 readline(~) -> (33) GET /?cmd=capabilities HTTP/1.1\r\n
381 381 readline(*) -> (27) Accept-Encoding: identity\r\n (glob)
382 382 readline(*) -> (35) accept: application/mercurial-0.1\r\n (glob)
383 383 readline(*) -> (2?) host: localhost:$HGPORT\r\n (glob)
384 384 readline(*) -> (49) user-agent: mercurial/proto-1.0 (Mercurial 4.2)\r\n (glob)
385 385 readline(*) -> (2) \r\n (glob)
386 386 sendall(160) -> HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercurial-0.1\r\nContent-Length: *\r\n\r\n (glob)
387 387 sendall(*) -> batch branchmap $USUAL_BUNDLE2_CAPS_NO_PHASES$ changegroupsubset compression=none getbundle httpheader=1024 httpmediatype=0.1rx,0.1tx,0.2tx known lookup pushkey streamreqs=* unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash (glob)
388 388 readline(~) -> (26) GET /?cmd=batch HTTP/1.1\r\n
389 389 readline(*) -> (27) Accept-Encoding: identity\r\n (glob)
390 390 readline(*) -> (29) vary: X-HgArg-1,X-HgProto-1\r\n (glob)
391 391 readline(*) -> (41) x-hgarg-1: cmds=heads+%3Bknown+nodes%3D\r\n (glob)
392 392 readline(*) -> (61) x-hgproto-1: 0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull\r\n (glob)
393 393 readline(*) -> (35) accept: application/mercurial-0.1\r\n (glob)
394 394 readline(*) -> (2?) host: localhost:$HGPORT\r\n (glob)
395 395 readline(*) -> (49) user-agent: mercurial/proto-1.0 (Mercurial 4.2)\r\n (glob)
396 396 readline(*) -> (2) \r\n (glob)
397 397 sendall(159) -> HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercurial-0.1\r\nContent-Length: 42\r\n\r\n
398 398 sendall(24 from 42) -> (0) 96ee1d7354c4ad7372047672
399 399 write limit reached; closing socket
400 400 $LOCALIP - - [$ERRDATE$] Exception happened during processing request '/?cmd=batch': (glob)
401 401 Traceback (most recent call last):
402 402 Exception: connection closed after sending N bytes
403 403
404 404
405 405 $ rm -f error.log
406 406
407 407 Server sends incomplete headers for getbundle response
408 408 ------------------------------------------------------
409 409
410 410 $ hg serve \
411 411 > --config badserver.close-after-send-patterns='(.*Content-Type: application/mercuri){3}' \
412 412 > -p $HGPORT -d --pid-file=hg.pid -E error.log
413 413 $ cat hg.pid > $DAEMON_PIDS
414 414
415 415 TODO this output is terrible
416 416
417 417 $ hg clone http://localhost:$HGPORT/ clone
418 418 requesting all changes
419 419 abort: 'http://localhost:$HGPORT/' does not appear to be an hg repository:
420 420 ---%<--- (application/mercuri)
421 421
422 422 ---%<---
423 423
424 424 [255]
425 425
426 426 $ killdaemons.py $DAEMON_PIDS
427 427
428 428 $ cat error.log | "$PYTHON" $TESTDIR/filtertraceback.py
429 429 readline(~) -> (33) GET /?cmd=capabilities HTTP/1.1\r\n
430 430 readline(*) -> (27) Accept-Encoding: identity\r\n (glob)
431 431 readline(*) -> (35) accept: application/mercurial-0.1\r\n (glob)
432 432 readline(*) -> (2?) host: localhost:$HGPORT\r\n (glob)
433 433 readline(*) -> (49) user-agent: mercurial/proto-1.0 (Mercurial 4.2)\r\n (glob)
434 434 readline(*) -> (2) \r\n (glob)
435 435 sendall(160) -> HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercurial-0.1\r\nContent-Length: *\r\n\r\n (glob)
436 436 sendall(*) -> batch branchmap $USUAL_BUNDLE2_CAPS_NO_PHASES$ changegroupsubset compression=none getbundle httpheader=1024 httpmediatype=0.1rx,0.1tx,0.2tx known lookup pushkey streamreqs=* unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash (glob)
437 437 readline(~) -> (26) GET /?cmd=batch HTTP/1.1\r\n
438 438 readline(*) -> (27) Accept-Encoding: identity\r\n (glob)
439 439 readline(*) -> (29) vary: X-HgArg-1,X-HgProto-1\r\n (glob)
440 440 readline(*) -> (41) x-hgarg-1: cmds=heads+%3Bknown+nodes%3D\r\n (glob)
441 441 readline(*) -> (61) x-hgproto-1: 0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull\r\n (glob)
442 442 readline(*) -> (35) accept: application/mercurial-0.1\r\n (glob)
443 443 readline(*) -> (2?) host: localhost:$HGPORT\r\n (glob)
444 444 readline(*) -> (49) user-agent: mercurial/proto-1.0 (Mercurial 4.2)\r\n (glob)
445 445 readline(*) -> (2) \r\n (glob)
446 446 sendall(159) -> HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercurial-0.1\r\nContent-Length: 42\r\n\r\n
447 447 sendall(42) -> 96ee1d7354c4ad7372047672c36a1f561e3a6a4c\n;
448 448 readline(~) -> (30) GET /?cmd=getbundle HTTP/1.1\r\n
449 449 readline(*) -> (27) Accept-Encoding: identity\r\n (glob)
450 450 readline(*) -> (29) vary: X-HgArg-1,X-HgProto-1\r\n (glob)
451 451 readline(*) -> (447) x-hgarg-1: bookmarks=1&bundlecaps=HG20%2Cbundle2%3DHG20%250Abookmarks%250Achangegroup%253D01%252C02%252C03%250Adigests%253Dmd5%252Csha1%252Csha512%250Aerror%253Dabort%252Cunsupportedcontent%252Cpushraced%252Cpushkey%250Ahgtagsfnodes%250Alistkeys%250Apushkey%250Aremote-changegroup%253Dhttp%252Chttps%250Astream%253Dv2&cg=1&common=0000000000000000000000000000000000000000&heads=96ee1d7354c4ad7372047672c36a1f561e3a6a4c&listkeys=phases%2Cbookmarks\r\n (glob)
452 452 readline(*) -> (61) x-hgproto-1: 0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull\r\n (glob)
453 453 readline(*) -> (35) accept: application/mercurial-0.1\r\n (glob)
454 454 readline(*) -> (2?) host: localhost:$HGPORT\r\n (glob)
455 455 readline(*) -> (49) user-agent: mercurial/proto-1.0 (Mercurial 4.2)\r\n (glob)
456 456 readline(*) -> (2) \r\n (glob)
457 457 sendall(129 from 167) -> (0) HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercuri
458 458 write limit reached; closing socket
459 459 $LOCALIP - - [$ERRDATE$] Exception happened during processing request '/?cmd=getbundle': (glob)
460 460 Traceback (most recent call last):
461 461 Exception: connection closed after sending N bytes
462 462
463 463
464 464 $ rm -f error.log
465 465
466 466 Server stops before it sends transfer encoding
467 467 ----------------------------------------------
468 468
469 469 $ hg serve \
470 470 > --config badserver.close-after-send-patterns="Transfer-Encoding: chunke" \
471 471 > -p $HGPORT -d --pid-file=hg.pid -E error.log
472 472 $ cat hg.pid > $DAEMON_PIDS
473 473
474 474 $ hg clone http://localhost:$HGPORT/ clone
475 475 requesting all changes
476 476 abort: stream ended unexpectedly (got 0 bytes, expected 1)
477 477 [255]
478 478
479 479 $ killdaemons.py $DAEMON_PIDS
480 480
481 481 $ "$PYTHON" $TESTDIR/filtertraceback.py < error.log | tail -6
482 482 sendall(162 from 167) -> (0) HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercurial-0.2\r\nTransfer-Encoding: chunke
483 483 write limit reached; closing socket
484 484 $LOCALIP - - [$ERRDATE$] Exception happened during processing request '/?cmd=getbundle': (glob)
485 485 Traceback (most recent call last):
486 486 Exception: connection closed after sending N bytes
487 487
488 488 $ rm -f error.log
489 489
490 490 Server sends empty HTTP body for getbundle
491 491 ------------------------------------------
492 492
493 493 $ hg serve \
494 494 > --config badserver.close-after-send-patterns='Transfer-Encoding: chunked\r\n\r\n' \
495 495 > -p $HGPORT -d --pid-file=hg.pid -E error.log
496 496 $ cat hg.pid > $DAEMON_PIDS
497 497
498 498 $ hg clone http://localhost:$HGPORT/ clone
499 499 requesting all changes
500 500 abort: HTTP request error (incomplete response)
501 501 (this may be an intermittent network failure; if the error persists, consider contacting the network or server operator)
502 502 [255]
503 503
504 504 $ killdaemons.py $DAEMON_PIDS
505 505
506 506 $ cat error.log | "$PYTHON" $TESTDIR/filtertraceback.py
507 507 readline(~) -> (33) GET /?cmd=capabilities HTTP/1.1\r\n
508 508 readline(*) -> (27) Accept-Encoding: identity\r\n (glob)
509 509 readline(*) -> (35) accept: application/mercurial-0.1\r\n (glob)
510 510 readline(*) -> (2?) host: localhost:$HGPORT\r\n (glob)
511 511 readline(*) -> (49) user-agent: mercurial/proto-1.0 (Mercurial 4.2)\r\n (glob)
512 512 readline(*) -> (2) \r\n (glob)
513 513 sendall(160) -> HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercurial-0.1\r\nContent-Length: *\r\n\r\n (glob)
514 514 sendall(*) -> batch branchmap $USUAL_BUNDLE2_CAPS_NO_PHASES$ changegroupsubset compression=none getbundle httpheader=1024 httpmediatype=0.1rx,0.1tx,0.2tx known lookup pushkey streamreqs=* unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash (glob)
515 515 readline(~) -> (26) GET /?cmd=batch HTTP/1.1\r\n
516 516 readline(*) -> (27) Accept-Encoding: identity\r\n (glob)
517 517 readline(*) -> (29) vary: X-HgArg-1,X-HgProto-1\r\n (glob)
518 518 readline(*) -> (41) x-hgarg-1: cmds=heads+%3Bknown+nodes%3D\r\n (glob)
519 519 readline(*) -> (61) x-hgproto-1: 0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull\r\n (glob)
520 520 readline(*) -> (35) accept: application/mercurial-0.1\r\n (glob)
521 521 readline(*) -> (2?) host: localhost:$HGPORT\r\n (glob)
522 522 readline(*) -> (49) user-agent: mercurial/proto-1.0 (Mercurial 4.2)\r\n (glob)
523 523 readline(*) -> (2) \r\n (glob)
524 524 sendall(159) -> HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercurial-0.1\r\nContent-Length: 42\r\n\r\n
525 525 sendall(42) -> 96ee1d7354c4ad7372047672c36a1f561e3a6a4c\n;
526 526 readline(~) -> (30) GET /?cmd=getbundle HTTP/1.1\r\n
527 527 readline(*) -> (27) Accept-Encoding: identity\r\n (glob)
528 528 readline(*) -> (29) vary: X-HgArg-1,X-HgProto-1\r\n (glob)
529 529 readline(*) -> (447) x-hgarg-1: bookmarks=1&bundlecaps=HG20%2Cbundle2%3DHG20%250Abookmarks%250Achangegroup%253D01%252C02%252C03%250Adigests%253Dmd5%252Csha1%252Csha512%250Aerror%253Dabort%252Cunsupportedcontent%252Cpushraced%252Cpushkey%250Ahgtagsfnodes%250Alistkeys%250Apushkey%250Aremote-changegroup%253Dhttp%252Chttps%250Astream%253Dv2&cg=1&common=0000000000000000000000000000000000000000&heads=96ee1d7354c4ad7372047672c36a1f561e3a6a4c&listkeys=phases%2Cbookmarks\r\n (glob)
530 530 readline(*) -> (61) x-hgproto-1: 0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull\r\n (glob)
531 531 readline(*) -> (35) accept: application/mercurial-0.1\r\n (glob)
532 532 readline(*) -> (2?) host: localhost:$HGPORT\r\n (glob)
533 533 readline(*) -> (49) user-agent: mercurial/proto-1.0 (Mercurial 4.2)\r\n (glob)
534 534 readline(*) -> (2) \r\n (glob)
535 535 sendall(167 from 167) -> (0) HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercurial-0.2\r\nTransfer-Encoding: chunked\r\n\r\n
536 536 write limit reached; closing socket
537 537 $LOCALIP - - [$ERRDATE$] Exception happened during processing request '/?cmd=getbundle': (glob)
538 538 Traceback (most recent call last):
539 539 Exception: connection closed after sending N bytes
540 540
541 541
542 542 $ rm -f error.log
543 543
544 544 Server sends partial compression string
545 545 ---------------------------------------
546 546
547 547 $ hg serve \
548 548 > --config badserver.close-after-send-patterns='4\r\nHG20\r\n' \
549 549 > -p $HGPORT -d --pid-file=hg.pid -E error.log
550 550 $ cat hg.pid > $DAEMON_PIDS
551 551
552 552 $ hg clone http://localhost:$HGPORT/ clone
553 553 requesting all changes
554 554 abort: HTTP request error (incomplete response)
555 555 (this may be an intermittent network failure; if the error persists, consider contacting the network or server operator)
556 556 [255]
557 557
558 558 $ killdaemons.py $DAEMON_PIDS
559 559
560 560 $ cat error.log | "$PYTHON" $TESTDIR/filtertraceback.py
561 561 readline(~) -> (33) GET /?cmd=capabilities HTTP/1.1\r\n
562 562 readline(*) -> (27) Accept-Encoding: identity\r\n (glob)
563 563 readline(*) -> (35) accept: application/mercurial-0.1\r\n (glob)
564 564 readline(*) -> (2?) host: localhost:$HGPORT\r\n (glob)
565 565 readline(*) -> (49) user-agent: mercurial/proto-1.0 (Mercurial 4.2)\r\n (glob)
566 566 readline(*) -> (2) \r\n (glob)
567 567 sendall(160) -> HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercurial-0.1\r\nContent-Length: *\r\n\r\n (glob)
568 568 sendall(*) -> batch branchmap $USUAL_BUNDLE2_CAPS_NO_PHASES$ changegroupsubset compression=none getbundle httpheader=1024 httpmediatype=0.1rx,0.1tx,0.2tx known lookup pushkey streamreqs=* unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash (glob)
569 569 readline(~) -> (26) GET /?cmd=batch HTTP/1.1\r\n
570 570 readline(*) -> (27) Accept-Encoding: identity\r\n (glob)
571 571 readline(*) -> (29) vary: X-HgArg-1,X-HgProto-1\r\n (glob)
572 572 readline(*) -> (41) x-hgarg-1: cmds=heads+%3Bknown+nodes%3D\r\n (glob)
573 573 readline(*) -> (61) x-hgproto-1: 0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull\r\n (glob)
574 574 readline(*) -> (35) accept: application/mercurial-0.1\r\n (glob)
575 575 readline(*) -> (2?) host: localhost:$HGPORT\r\n (glob)
576 576 readline(*) -> (49) user-agent: mercurial/proto-1.0 (Mercurial 4.2)\r\n (glob)
577 577 readline(*) -> (2) \r\n (glob)
578 578 sendall(159) -> HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercurial-0.1\r\nContent-Length: 42\r\n\r\n
579 579 sendall(42) -> 96ee1d7354c4ad7372047672c36a1f561e3a6a4c\n;
580 580 readline(~) -> (30) GET /?cmd=getbundle HTTP/1.1\r\n
581 581 readline(*) -> (27) Accept-Encoding: identity\r\n (glob)
582 582 readline(*) -> (29) vary: X-HgArg-1,X-HgProto-1\r\n (glob)
583 583 readline(*) -> (447) x-hgarg-1: bookmarks=1&bundlecaps=HG20%2Cbundle2%3DHG20%250Abookmarks%250Achangegroup%253D01%252C02%252C03%250Adigests%253Dmd5%252Csha1%252Csha512%250Aerror%253Dabort%252Cunsupportedcontent%252Cpushraced%252Cpushkey%250Ahgtagsfnodes%250Alistkeys%250Apushkey%250Aremote-changegroup%253Dhttp%252Chttps%250Astream%253Dv2&cg=1&common=0000000000000000000000000000000000000000&heads=96ee1d7354c4ad7372047672c36a1f561e3a6a4c&listkeys=phases%2Cbookmarks\r\n (glob)
584 584 readline(*) -> (61) x-hgproto-1: 0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull\r\n (glob)
585 585 readline(*) -> (35) accept: application/mercurial-0.1\r\n (glob)
586 586 readline(*) -> (2?) host: localhost:$HGPORT\r\n (glob)
587 587 readline(*) -> (49) user-agent: mercurial/proto-1.0 (Mercurial 4.2)\r\n (glob)
588 588 readline(*) -> (2) \r\n (glob)
589 589 sendall(167) -> HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercurial-0.2\r\nTransfer-Encoding: chunked\r\n\r\n
590 590 sendall(6) -> 1\\r\\n\x04\\r\\n (esc)
591 591 sendall(9) -> 4\r\nnone\r\n
592 592 sendall(9 from 9) -> (0) 4\r\nHG20\r\n
593 593 write limit reached; closing socket
594 594 $LOCALIP - - [$ERRDATE$] Exception happened during processing request '/?cmd=getbundle': (glob)
595 595 Traceback (most recent call last):
596 596 Exception: connection closed after sending N bytes
597 597
598 598
599 599 $ rm -f error.log
600 600
601 601 Server sends partial bundle2 header magic
602 602 -----------------------------------------
603 603
604 604 $ hg serve \
605 605 > --config badserver.close-after-send-patterns='4\r\nHG2' \
606 606 > -p $HGPORT -d --pid-file=hg.pid -E error.log
607 607 $ cat hg.pid > $DAEMON_PIDS
608 608
609 609 $ hg clone http://localhost:$HGPORT/ clone
610 610 requesting all changes
611 611 abort: HTTP request error (incomplete response*) (glob)
612 612 (this may be an intermittent network failure; if the error persists, consider contacting the network or server operator)
613 613 [255]
614 614
615 615 $ killdaemons.py $DAEMON_PIDS
616 616
617 617 $ "$PYTHON" $TESTDIR/filtertraceback.py < error.log | tail -9
618 618 sendall(167) -> HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercurial-0.2\r\nTransfer-Encoding: chunked\r\n\r\n
619 619 sendall(6) -> 1\\r\\n\x04\\r\\n (esc)
620 620 sendall(9) -> 4\r\nnone\r\n
621 621 sendall(6 from 9) -> (0) 4\r\nHG2
622 622 write limit reached; closing socket
623 623 $LOCALIP - - [$ERRDATE$] Exception happened during processing request '/?cmd=getbundle': (glob)
624 624 Traceback (most recent call last):
625 625 Exception: connection closed after sending N bytes
626 626
627 627 $ rm -f error.log
628 628
629 629 Server sends incomplete bundle2 stream params length
630 630 ----------------------------------------------------
631 631
632 632 $ hg serve \
633 633 > --config badserver.close-after-send-patterns='4\r\n\0\0\0' \
634 634 > -p $HGPORT -d --pid-file=hg.pid -E error.log
635 635 $ cat hg.pid > $DAEMON_PIDS
636 636
637 637 $ hg clone http://localhost:$HGPORT/ clone
638 638 requesting all changes
639 639 abort: HTTP request error (incomplete response*) (glob)
640 640 (this may be an intermittent network failure; if the error persists, consider contacting the network or server operator)
641 641 [255]
642 642
643 643 $ killdaemons.py $DAEMON_PIDS
644 644
645 645 $ "$PYTHON" $TESTDIR/filtertraceback.py < error.log | tail -10
646 646 sendall(167) -> HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercurial-0.2\r\nTransfer-Encoding: chunked\r\n\r\n
647 647 sendall(6) -> 1\\r\\n\x04\\r\\n (esc)
648 648 sendall(9) -> 4\r\nnone\r\n
649 649 sendall(9) -> 4\r\nHG20\r\n
650 650 sendall(6 from 9) -> (0) 4\\r\\n\x00\x00\x00 (esc)
651 651 write limit reached; closing socket
652 652 $LOCALIP - - [$ERRDATE$] Exception happened during processing request '/?cmd=getbundle': (glob)
653 653 Traceback (most recent call last):
654 654 Exception: connection closed after sending N bytes
655 655
656 656 $ rm -f error.log
657 657
658 658 Servers stops after bundle2 stream params header
659 659 ------------------------------------------------
660 660
661 661 $ hg serve \
662 662 > --config badserver.close-after-send-patterns='4\r\n\0\0\0\0\r\n' \
663 663 > -p $HGPORT -d --pid-file=hg.pid -E error.log
664 664 $ cat hg.pid > $DAEMON_PIDS
665 665
666 666 $ hg clone http://localhost:$HGPORT/ clone
667 667 requesting all changes
668 668 abort: HTTP request error (incomplete response)
669 669 (this may be an intermittent network failure; if the error persists, consider contacting the network or server operator)
670 670 [255]
671 671
672 672 $ killdaemons.py $DAEMON_PIDS
673 673
674 674 $ "$PYTHON" $TESTDIR/filtertraceback.py < error.log | tail -10
675 675 sendall(167) -> HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercurial-0.2\r\nTransfer-Encoding: chunked\r\n\r\n
676 676 sendall(6) -> 1\\r\\n\x04\\r\\n (esc)
677 677 sendall(9) -> 4\r\nnone\r\n
678 678 sendall(9) -> 4\r\nHG20\r\n
679 679 sendall(9 from 9) -> (0) 4\\r\\n\x00\x00\x00\x00\\r\\n (esc)
680 680 write limit reached; closing socket
681 681 $LOCALIP - - [$ERRDATE$] Exception happened during processing request '/?cmd=getbundle': (glob)
682 682 Traceback (most recent call last):
683 683 Exception: connection closed after sending N bytes
684 684
685 685 $ rm -f error.log
686 686
687 687 Server stops sending after bundle2 part header length
688 688 -----------------------------------------------------
689 689
690 690 $ hg serve \
691 691 > --config badserver.close-after-send-patterns='4\r\n\0\0\0\)\r\n' \
692 692 > -p $HGPORT -d --pid-file=hg.pid -E error.log
693 693 $ cat hg.pid > $DAEMON_PIDS
694 694
695 695 $ hg clone http://localhost:$HGPORT/ clone
696 696 requesting all changes
697 697 abort: HTTP request error (incomplete response)
698 698 (this may be an intermittent network failure; if the error persists, consider contacting the network or server operator)
699 699 [255]
700 700
701 701 $ killdaemons.py $DAEMON_PIDS
702 702
703 703 $ "$PYTHON" $TESTDIR/filtertraceback.py < error.log | tail -11
704 704 sendall(167) -> HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercurial-0.2\r\nTransfer-Encoding: chunked\r\n\r\n
705 705 sendall(6) -> 1\\r\\n\x04\\r\\n (esc)
706 706 sendall(9) -> 4\r\nnone\r\n
707 707 sendall(9) -> 4\r\nHG20\r\n
708 708 sendall(9) -> 4\\r\\n\x00\x00\x00\x00\\r\\n (esc)
709 709 sendall(9 from 9) -> (0) 4\\r\\n\x00\x00\x00)\\r\\n (esc)
710 710 write limit reached; closing socket
711 711 $LOCALIP - - [$ERRDATE$] Exception happened during processing request '/?cmd=getbundle': (glob)
712 712 Traceback (most recent call last):
713 713 Exception: connection closed after sending N bytes
714 714
715 715 $ rm -f error.log
716 716
717 717 Server stops sending after bundle2 part header
718 718 ----------------------------------------------
719 719
720 720 $ hg serve \
721 721 > --config badserver.close-after-send-patterns="version03nbchanges1\\r\\n" \
722 722 > -p $HGPORT -d --pid-file=hg.pid -E error.log
723 723 $ cat hg.pid > $DAEMON_PIDS
724 724
725 725 $ hg clone http://localhost:$HGPORT/ clone
726 726 requesting all changes
727 727 adding changesets
728 transaction abort!
729 rollback completed
730 728 abort: HTTP request error (incomplete response)
731 729 (this may be an intermittent network failure; if the error persists, consider contacting the network or server operator)
732 730 [255]
733 731
734 732 $ killdaemons.py $DAEMON_PIDS
735 733
736 734 $ "$PYTHON" $TESTDIR/filtertraceback.py < error.log | tail -12
737 735 sendall(167) -> HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercurial-0.2\r\nTransfer-Encoding: chunked\r\n\r\n
738 736 sendall(6) -> 1\\r\\n\x04\\r\\n (esc)
739 737 sendall(9) -> 4\r\nnone\r\n
740 738 sendall(9) -> 4\r\nHG20\r\n
741 739 sendall(9) -> 4\\r\\n\x00\x00\x00\x00\\r\\n (esc)
742 740 sendall(9) -> 4\\r\\n\x00\x00\x00)\\r\\n (esc)
743 741 sendall(47 from 47) -> (0) 29\\r\\n\x0bCHANGEGROUP\x00\x00\x00\x00\x01\x01\x07\x02 \x01version03nbchanges1\\r\\n (esc)
744 742 write limit reached; closing socket
745 743 $LOCALIP - - [$ERRDATE$] Exception happened during processing request '/?cmd=getbundle': (glob)
746 744 Traceback (most recent call last):
747 745 Exception: connection closed after sending N bytes
748 746
749 747 $ rm -f error.log
750 748
751 749 Server stops after bundle2 part payload chunk size
752 750 --------------------------------------------------
753 751
754 752 $ hg serve \
755 753 > --config badserver.close-after-send-patterns='1dc\r\n.......' \
756 754 > -p $HGPORT -d --pid-file=hg.pid -E error.log
757 755 $ cat hg.pid > $DAEMON_PIDS
758 756
759 757 $ hg clone http://localhost:$HGPORT/ clone
760 758 requesting all changes
761 759 adding changesets
762 transaction abort!
763 rollback completed
764 760 abort: HTTP request error (incomplete response*) (glob)
765 761 (this may be an intermittent network failure; if the error persists, consider contacting the network or server operator)
766 762 [255]
767 763
768 764 $ killdaemons.py $DAEMON_PIDS
769 765
770 766 $ "$PYTHON" $TESTDIR/filtertraceback.py < error.log | tail -14
771 767 sendall(167) -> HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercurial-0.2\r\nTransfer-Encoding: chunked\r\n\r\n
772 768 sendall(6) -> 1\\r\\n\x04\\r\\n (esc)
773 769 sendall(9) -> 4\r\nnone\r\n
774 770 sendall(9) -> 4\r\nHG20\r\n
775 771 sendall(9) -> 4\\r\\n\x00\x00\x00\x00\\r\\n (esc)
776 772 sendall(9) -> 4\\r\\n\x00\x00\x00)\\r\\n (esc)
777 773 sendall(47) -> 29\\r\\n\x0bCHANGEGROUP\x00\x00\x00\x00\x01\x01\x07\x02 \x01version03nbchanges1\\r\\n (esc)
778 774 sendall(9) -> 4\\r\\n\x00\x00\x01\xdc\\r\\n (esc)
779 775 sendall(12 from 483) -> (0) 1dc\\r\\n\x00\x00\x00\xb4\x96\xee\x1d (esc)
780 776 write limit reached; closing socket
781 777 $LOCALIP - - [$ERRDATE$] Exception happened during processing request '/?cmd=getbundle': (glob)
782 778 Traceback (most recent call last):
783 779 Exception: connection closed after sending N bytes
784 780
785 781 $ rm -f error.log
786 782
787 783 Server stops sending in middle of bundle2 payload chunk
788 784 -------------------------------------------------------
789 785
790 786 $ hg serve \
791 787 > --config badserver.close-after-send-patterns=':jL\0\0\x00\0\0\0\0\0\0\0\r\n' \
792 788 > -p $HGPORT -d --pid-file=hg.pid -E error.log
793 789 $ cat hg.pid > $DAEMON_PIDS
794 790
795 791 $ hg clone http://localhost:$HGPORT/ clone
796 792 requesting all changes
797 793 adding changesets
798 transaction abort!
799 rollback completed
800 794 abort: HTTP request error (incomplete response)
801 795 (this may be an intermittent network failure; if the error persists, consider contacting the network or server operator)
802 796 [255]
803 797
804 798 $ killdaemons.py $DAEMON_PIDS
805 799
806 800 $ "$PYTHON" $TESTDIR/filtertraceback.py < error.log | tail -14
807 801 sendall(167) -> HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercurial-0.2\r\nTransfer-Encoding: chunked\r\n\r\n
808 802 sendall(6) -> 1\\r\\n\x04\\r\\n (esc)
809 803 sendall(9) -> 4\r\nnone\r\n
810 804 sendall(9) -> 4\r\nHG20\r\n
811 805 sendall(9) -> 4\\r\\n\x00\x00\x00\x00\\r\\n (esc)
812 806 sendall(9) -> 4\\r\\n\x00\x00\x00)\\r\\n (esc)
813 807 sendall(47) -> 29\\r\\n\x0bCHANGEGROUP\x00\x00\x00\x00\x01\x01\x07\x02 \x01version03nbchanges1\\r\\n (esc)
814 808 sendall(9) -> 4\\r\\n\x00\x00\x01\xdc\\r\\n (esc)
815 809 sendall(483 from 483) -> (0) 1dc\\r\\n\x00\x00\x00\xb4\x96\xee\x1dsT\xc4\xadsr\x04vr\xc3j\x1fV\x1e:jL\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x96\xee\x1dsT\xc4\xadsr\x04vr\xc3j\x1fV\x1e:jL\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>6a3df4de388f3c4f8e28f4f9a814299a3cbb5f50\\ntest\\n0 0\\nfoo\\n\\ninitial\x00\x00\x00\x00\x00\x00\x00\xa3j=\xf4\xde8\x8f<O\x8e(\xf4\xf9\xa8\x14)\x9a<\xbb_P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x96\xee\x1dsT\xc4\xadsr\x04vr\xc3j\x1fV\x1e:jL\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00-foo\x00b80de5d138758541c5f05265ad144ab9fa86d1db\\n\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07foo\x00\x00\x00j\xb8\\r\xe5\xd18u\x85A\xc5\xf0Re\xad\x14J\xb9\xfa\x86\xd1\xdb\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x96\xee\x1dsT\xc4\xadsr\x04vr\xc3j\x1fV\x1e:jL\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\\r\\n (esc)
816 810 write limit reached; closing socket
817 811 $LOCALIP - - [$ERRDATE$] Exception happened during processing request '/?cmd=getbundle': (glob)
818 812 Traceback (most recent call last):
819 813 Exception: connection closed after sending N bytes
820 814
821 815 $ rm -f error.log
822 816
823 817 Server stops sending after 0 length payload chunk size
824 818 ------------------------------------------------------
825 819
826 820 $ hg serve \
827 821 > --config badserver.close-after-send-patterns=LISTKEYS \
828 822 > -p $HGPORT -d --pid-file=hg.pid -E error.log
829 823 $ cat hg.pid > $DAEMON_PIDS
830 824
831 825 $ hg clone http://localhost:$HGPORT/ clone
832 826 requesting all changes
833 827 adding changesets
834 828 adding manifests
835 829 adding file changes
836 830 transaction abort!
837 831 rollback completed
838 832 abort: HTTP request error (incomplete response*) (glob)
839 833 (this may be an intermittent network failure; if the error persists, consider contacting the network or server operator)
840 834 [255]
841 835
842 836 $ killdaemons.py $DAEMON_PIDS
843 837
844 838 $ "$PYTHON" $TESTDIR/filtertraceback.py < error.log | tail -16
845 839 sendall(6) -> 1\\r\\n\x04\\r\\n (esc)
846 840 sendall(9) -> 4\r\nnone\r\n
847 841 sendall(9) -> 4\r\nHG20\r\n
848 842 sendall(9) -> 4\\r\\n\x00\x00\x00\x00\\r\\n (esc)
849 843 sendall(9) -> 4\\r\\n\x00\x00\x00)\\r\\n (esc)
850 844 sendall(47) -> 29\\r\\n\x0bCHANGEGROUP\x00\x00\x00\x00\x01\x01\x07\x02 \x01version03nbchanges1\\r\\n (esc)
851 845 sendall(9) -> 4\\r\\n\x00\x00\x01\xdc\\r\\n (esc)
852 846 sendall(483) -> 1dc\\r\\n\x00\x00\x00\xb4\x96\xee\x1dsT\xc4\xadsr\x04vr\xc3j\x1fV\x1e:jL\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x96\xee\x1dsT\xc4\xadsr\x04vr\xc3j\x1fV\x1e:jL\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>6a3df4de388f3c4f8e28f4f9a814299a3cbb5f50\\ntest\\n0 0\\nfoo\\n\\ninitial\x00\x00\x00\x00\x00\x00\x00\xa3j=\xf4\xde8\x8f<O\x8e(\xf4\xf9\xa8\x14)\x9a<\xbb_P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x96\xee\x1dsT\xc4\xadsr\x04vr\xc3j\x1fV\x1e:jL\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00-foo\x00b80de5d138758541c5f05265ad144ab9fa86d1db\\n\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07foo\x00\x00\x00j\xb8\\r\xe5\xd18u\x85A\xc5\xf0Re\xad\x14J\xb9\xfa\x86\xd1\xdb\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x96\xee\x1dsT\xc4\xadsr\x04vr\xc3j\x1fV\x1e:jL\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\\r\\n (esc)
853 847 sendall(9) -> 4\\r\\n\x00\x00\x00\x00\\r\\n (esc)
854 848 sendall(9) -> 4\\r\\n\x00\x00\x00 \\r\\n (esc)
855 849 sendall(13 from 38) -> (0) 20\\r\\n\x08LISTKEYS (esc)
856 850 write limit reached; closing socket
857 851 $LOCALIP - - [$ERRDATE$] Exception happened during processing request '/?cmd=getbundle': (glob)
858 852 Traceback (most recent call last):
859 853 Exception: connection closed after sending N bytes
860 854
861 855 $ rm -f error.log
862 856
863 857 Server stops sending after 0 part bundle part header (indicating end of bundle2 payload)
864 858 ----------------------------------------------------------------------------------------
865 859
866 860 This is before the 0 size chunked transfer part that signals end of HTTP response.
867 861
868 862 $ hg serve \
869 863 > --config badserver.close-after-send-patterns='(.*4\r\n\0\0\0\0\r\n){5}' \
870 864 > -p $HGPORT -d --pid-file=hg.pid -E error.log
871 865 $ cat hg.pid > $DAEMON_PIDS
872 866
873 867 $ hg clone http://localhost:$HGPORT/ clone
874 868 requesting all changes
875 869 adding changesets
876 870 adding manifests
877 871 adding file changes
878 872 added 1 changesets with 1 changes to 1 files
879 873 new changesets 96ee1d7354c4
880 874 updating to branch default
881 875 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
882 876
883 877 $ killdaemons.py $DAEMON_PIDS
884 878
885 879 $ "$PYTHON" $TESTDIR/filtertraceback.py < error.log | tail -20
886 880 sendall(9) -> 4\\r\\n\x00\x00\x00\x00\\r\\n (esc)
887 881 sendall(9) -> 4\\r\\n\x00\x00\x00)\\r\\n (esc)
888 882 sendall(47) -> 29\\r\\n\x0bCHANGEGROUP\x00\x00\x00\x00\x01\x01\x07\x02 \x01version03nbchanges1\\r\\n (esc)
889 883 sendall(9) -> 4\\r\\n\x00\x00\x01\xdc\\r\\n (esc)
890 884 sendall(483) -> 1dc\\r\\n\x00\x00\x00\xb4\x96\xee\x1dsT\xc4\xadsr\x04vr\xc3j\x1fV\x1e:jL\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x96\xee\x1dsT\xc4\xadsr\x04vr\xc3j\x1fV\x1e:jL\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>6a3df4de388f3c4f8e28f4f9a814299a3cbb5f50\\ntest\\n0 0\\nfoo\\n\\ninitial\x00\x00\x00\x00\x00\x00\x00\xa3j=\xf4\xde8\x8f<O\x8e(\xf4\xf9\xa8\x14)\x9a<\xbb_P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x96\xee\x1dsT\xc4\xadsr\x04vr\xc3j\x1fV\x1e:jL\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00-foo\x00b80de5d138758541c5f05265ad144ab9fa86d1db\\n\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07foo\x00\x00\x00j\xb8\\r\xe5\xd18u\x85A\xc5\xf0Re\xad\x14J\xb9\xfa\x86\xd1\xdb\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x96\xee\x1dsT\xc4\xadsr\x04vr\xc3j\x1fV\x1e:jL\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\\r\\n (esc)
891 885 sendall(9) -> 4\\r\\n\x00\x00\x00\x00\\r\\n (esc)
892 886 sendall(9) -> 4\\r\\n\x00\x00\x00 \\r\\n (esc)
893 887 sendall(38) -> 20\\r\\n\x08LISTKEYS\x00\x00\x00\x01\x01\x00 \x06namespacephases\\r\\n (esc)
894 888 sendall(9) -> 4\\r\\n\x00\x00\x00:\\r\\n (esc)
895 889 sendall(64) -> 3a\r\n96ee1d7354c4ad7372047672c36a1f561e3a6a4c 1\npublishing True\r\n
896 890 sendall(9) -> 4\\r\\n\x00\x00\x00\x00\\r\\n (esc)
897 891 sendall(9) -> 4\\r\\n\x00\x00\x00#\\r\\n (esc)
898 892 sendall(41) -> 23\\r\\n\x08LISTKEYS\x00\x00\x00\x02\x01\x00 namespacebookmarks\\r\\n (esc)
899 893 sendall(9) -> 4\\r\\n\x00\x00\x00\x00\\r\\n (esc)
900 894 sendall(9 from 9) -> (0) 4\\r\\n\x00\x00\x00\x00\\r\\n (esc)
901 895 write limit reached; closing socket
902 896 $LOCALIP - - [$ERRDATE$] Exception happened during processing request '/?cmd=getbundle': (glob)
903 897 Traceback (most recent call last):
904 898 Exception: connection closed after sending N bytes
905 899
906 900 $ rm -f error.log
907 901 $ rm -rf clone
908 902
909 903 Server sends a size 0 chunked-transfer size without terminating \r\n
910 904 --------------------------------------------------------------------
911 905
912 906 $ hg serve \
913 907 > --config badserver.close-after-send-patterns="(.*4\\r\\n\0\0\0\0\\r\\n0\r\n)" \
914 908 > -p $HGPORT -d --pid-file=hg.pid -E error.log
915 909 $ cat hg.pid > $DAEMON_PIDS
916 910
917 911 $ hg clone http://localhost:$HGPORT/ clone
918 912 requesting all changes
919 913 adding changesets
920 914 adding manifests
921 915 adding file changes
922 916 added 1 changesets with 1 changes to 1 files
923 917 new changesets 96ee1d7354c4
924 918 updating to branch default
925 919 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
926 920
927 921 $ killdaemons.py $DAEMON_PIDS
928 922
929 923 $ "$PYTHON" $TESTDIR/filtertraceback.py < error.log | tail -21
930 924 sendall(9) -> 4\\r\\n\x00\x00\x00\x00\\r\\n (esc)
931 925 sendall(9) -> 4\\r\\n\x00\x00\x00)\\r\\n (esc)
932 926 sendall(47) -> 29\\r\\n\x0bCHANGEGROUP\x00\x00\x00\x00\x01\x01\x07\x02 \x01version03nbchanges1\\r\\n (esc)
933 927 sendall(9) -> 4\\r\\n\x00\x00\x01\xdc\\r\\n (esc)
934 928 sendall(483) -> 1dc\\r\\n\x00\x00\x00\xb4\x96\xee\x1dsT\xc4\xadsr\x04vr\xc3j\x1fV\x1e:jL\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x96\xee\x1dsT\xc4\xadsr\x04vr\xc3j\x1fV\x1e:jL\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>6a3df4de388f3c4f8e28f4f9a814299a3cbb5f50\\ntest\\n0 0\\nfoo\\n\\ninitial\x00\x00\x00\x00\x00\x00\x00\xa3j=\xf4\xde8\x8f<O\x8e(\xf4\xf9\xa8\x14)\x9a<\xbb_P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x96\xee\x1dsT\xc4\xadsr\x04vr\xc3j\x1fV\x1e:jL\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00-foo\x00b80de5d138758541c5f05265ad144ab9fa86d1db\\n\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07foo\x00\x00\x00j\xb8\\r\xe5\xd18u\x85A\xc5\xf0Re\xad\x14J\xb9\xfa\x86\xd1\xdb\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x96\xee\x1dsT\xc4\xadsr\x04vr\xc3j\x1fV\x1e:jL\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\\r\\n (esc)
935 929 sendall(9) -> 4\\r\\n\x00\x00\x00\x00\\r\\n (esc)
936 930 sendall(9) -> 4\\r\\n\x00\x00\x00 \\r\\n (esc)
937 931 sendall(38) -> 20\\r\\n\x08LISTKEYS\x00\x00\x00\x01\x01\x00 \x06namespacephases\\r\\n (esc)
938 932 sendall(9) -> 4\\r\\n\x00\x00\x00:\\r\\n (esc)
939 933 sendall(64) -> 3a\r\n96ee1d7354c4ad7372047672c36a1f561e3a6a4c 1\npublishing True\r\n
940 934 sendall(9) -> 4\\r\\n\x00\x00\x00\x00\\r\\n (esc)
941 935 sendall(9) -> 4\\r\\n\x00\x00\x00#\\r\\n (esc)
942 936 sendall(41) -> 23\\r\\n\x08LISTKEYS\x00\x00\x00\x02\x01\x00 namespacebookmarks\\r\\n (esc)
943 937 sendall(9) -> 4\\r\\n\x00\x00\x00\x00\\r\\n (esc)
944 938 sendall(9) -> 4\\r\\n\x00\x00\x00\x00\\r\\n (esc)
945 939 sendall(3 from 5) -> (0) 0\r\n
946 940 write limit reached; closing socket
947 941 $LOCALIP - - [$ERRDATE$] Exception happened during processing request '/?cmd=getbundle': (glob)
948 942 Traceback (most recent call last):
949 943 Exception: connection closed after sending N bytes
950 944
951 945 $ rm -f error.log
952 946 $ rm -rf clone
General Comments 0
You need to be logged in to leave comments. Login now