diff --git a/mercurial/revlog.py b/mercurial/revlog.py --- a/mercurial/revlog.py +++ b/mercurial/revlog.py @@ -1973,7 +1973,7 @@ _(b"%s not found in the transaction") % self._indexfile ) trindex = 0 - tr.add(self._datafile, 0) + trdataoffset = 0 if fp: fp.flush() @@ -1983,10 +1983,22 @@ self._writinghandles = None with self._indexfp(b'r') as ifh, self._datafp(b'w') as dfh: + if dfh.tell() != 0: + # Should never happen, but may reduce confusion if something + # went wrong in a rollback or manual fix. + raise error.RevlogError( + _(b"%s is an inline revlog but %s is nonempty") + % (self._indexfile, self._datafile) + ) + # write this after the dfh.tell check, otherwise the rollback caused + # by a failure of the check would truncate whatever is the datafile + tr.add(self._datafile, 0) for r in self: dfh.write(self._getsegmentforrevs(r, r, df=ifh)[1]) if troffset <= self.start(r) + r * self.index.entry_size: + # trindex is the first revision to be excluded on rollback trindex = r + trdataoffset = self.start(r) with self._indexfp(b'w') as fp: self._format_flags &= ~FLAG_INLINE_DATA @@ -1999,9 +2011,24 @@ e = header + e fp.write(e) - # the temp file replace the real index when we exit the context - # manager - + # The temp file replace the real index when we exit the context + # manager. When that happens, transaction rollback is temporarily + # broken, because the rollback journal entry for the old .i is + # larger than the size of the new .i (because they include the same + # revisions, but offset for the old .i accounts for the inline + # data. Except when the data is of size 0, but that should be both + # impossible and harmless). This gets corrected very immediately by + # the tr.replace below. Note that we *must* write the offset for the + # (new) datafile before the offset for the new .i. In this order, if + # we get interrupted after the first tr.replace, the rollback + # fails. In the other order, if we got interrupted after the + # tr.replace for the index, rollback would succeed but leave the + # repository broken (non-inline index + datafile of size 0). One + # problem we still have is that since this change is done outside + # the transaction, the fncache should get updated even on + # transaction rollback, but doesn't. + + tr.replace(self._datafile, trdataoffset) tr.replace(self._indexfile, trindex * self.index.entry_size) nodemaputil.setup_persistent_nodemap(tr, self) self._chunkclear() diff --git a/mercurial/transaction.py b/mercurial/transaction.py --- a/mercurial/transaction.py +++ b/mercurial/transaction.py @@ -46,6 +46,26 @@ return _active +def _entries_to_playback(entries): + # On revlog split, we end up with two journal entries for the .i + # and .d. In these cases we want the second entries to override + # the first ones. This filters out the first entries. + usable_entries = [] + seen_f = {} + for f, o in reversed(entries): + if f in seen_f: + if seen_f[f] != 1: + raise error.Abort( + _(b"unexpectedly many rollback journal entries for %s") % f + ) + seen_f[f] = 2 + else: + usable_entries.append((f, o)) + seen_f[f] = 1 + usable_entries.reverse() + return usable_entries + + def _playback( journal, report, @@ -56,7 +76,7 @@ unlink=True, checkambigfiles=None, ): - for f, o in entries: + for f, o in _entries_to_playback(entries): if o or not unlink: checkambig = checkambigfiles and (f, b'') in checkambigfiles try: diff --git a/tests/test-transaction-rollback-on-revlog-split.t b/tests/test-transaction-rollback-on-revlog-split.t --- a/tests/test-transaction-rollback-on-revlog-split.t +++ b/tests/test-transaction-rollback-on-revlog-split.t @@ -23,19 +23,18 @@ $ hg commit -m_ transaction abort! - rollback failed - please run hg recover - (failure reason: attempted to truncate data/file.i to 1065 bytes, but it was already 128 bytes) + rollback completed abort: pretxncommit hook exited with status 1 [40] - $ cat .hg/store/journal | tr -s '\000' ' ' - data/file.i 1065 - data/file.d 0 - data/file.i 64 - 00manifest.i 111 - 00changelog.i 122 +Verify that rollback worked properly (the fncache being stale is a +bug): - $ hg recover - rolling back interrupted transaction - abort: attempted to truncate data/file.i to 1065 bytes, but it was already 128 bytes - [255] + $ hg verify -q + warning: revlog 'data/file.d' not in fncache! + 1 warnings encountered! + hint: run "hg debugrebuildfncache" to recover from corrupt fncache + $ hg debugrebuildfncache + adding data/file.d + 1 items added, 0 removed from fncache + $ hg verify -q