mercurial/revlogutils/docket.py
author Pierre-Yves David <pierre-yves.david@octobus.net>
Mon, 03 May 2021 12:35:25 +0200
changeset 47241 2219853a1503
parent 47238 6597255a4f94
child 47242 4abd474a10af
permissions -rw-r--r--
revlogv2: track pending write in the docket and expose it to hooks The docket is now able to write pending data. We could have used a distinct intermediate files, however keeping everything in the same file will make it simpler to keep track of the various involved files if necessary. However it might prove more complicated for streaming clone. This will be dealt with later. Note that we lifted the stderr redirection in the test since we no longer suffer from "unkown working directory parent" message. Differential Revision: https://phab.mercurial-scm.org/D10631

# docket - code related to revlog "docket"
#
# Copyright 2021 Pierre-Yves David <pierre-yves.david@octobus.net>
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.

### Revlog docket file
#
# The revlog is stored on disk using multiple files:
#
# * a small docket file, containing metadata and a pointer,
#
# * an index file, containing fixed width information about revisions,
#
# * a data file, containing variable width data for these revisions,

from __future__ import absolute_import

import struct

from .. import (
    error,
)

from . import (
    constants,
)

# Docket format
#
# * 4 bytes: revlog version
#          |   This is mandatory as docket must be compatible with the previous
#          |   revlog index header.
# * 8 bytes: size of index data
# * 8 bytes: pending size of index data
S_HEADER = struct.Struct(constants.INDEX_HEADER.format + 'LL')


class RevlogDocket(object):
    """metadata associated with revlog"""

    def __init__(
        self,
        revlog,
        use_pending=False,
        version_header=None,
        index_end=0,
        pending_index_end=0,
    ):
        self._version_header = version_header
        self._read_only = bool(use_pending)
        self._dirty = False
        self._radix = revlog.radix
        self._path = revlog._docket_file
        self._opener = revlog.opener
        # this assert should be True as long as we have a single index filename
        assert index_end <= pending_index_end
        self._initial_index_end = index_end
        self._pending_index_end = pending_index_end
        if use_pending:
            self._index_end = self._pending_index_end
        else:
            self._index_end = self._initial_index_end

    def index_filepath(self):
        """file path to the current index file associated to this docket"""
        # very simplistic version at first
        return b"%s.idx" % self._radix

    @property
    def index_end(self):
        return self._index_end

    @index_end.setter
    def index_end(self, new_size):
        if new_size != self._index_end:
            self._index_end = new_size
            self._dirty = True

    def write(self, transaction, pending=False, stripping=False):
        """write the modification of disk if any

        This make the new content visible to all process"""
        if not self._dirty:
            return False
        else:
            if self._read_only:
                msg = b'writing read-only docket: %s'
                msg %= self._path
                raise error.ProgrammingError(msg)
            if not stripping:
                # XXX we could, leverage the docket while stripping. However it
                # is not powerfull enough at the time of this comment
                transaction.addbackup(self._path, location=b'store')
            with self._opener(self._path, mode=b'w', atomictemp=True) as f:
                f.write(self._serialize(pending=pending))
            # if pending we still need to the write final data eventually
            self._dirty = pending
            return True

    def _serialize(self, pending=False):
        if pending:
            official_index_end = self._initial_index_end
        else:
            official_index_end = self._index_end

        # this assert should be True as long as we have a single index filename
        assert official_index_end <= self._index_end
        data = (
            self._version_header,
            official_index_end,
            self._index_end,
        )
        return S_HEADER.pack(*data)


def default_docket(revlog, version_header):
    """given a revlog version a new docket object for the given revlog"""
    if (version_header & 0xFFFF) != constants.REVLOGV2:
        return None
    docket = RevlogDocket(revlog, version_header=version_header)
    docket._dirty = True
    return docket


def parse_docket(revlog, data, use_pending=False):
    """given some docket data return a docket object for the given revlog"""
    header = S_HEADER.unpack(data[: S_HEADER.size])
    version_header, index_size, pending_index_size = header
    docket = RevlogDocket(
        revlog,
        use_pending=use_pending,
        version_header=version_header,
        index_end=index_size,
        pending_index_end=pending_index_size,
    )
    return docket