comparison mercurial/obsolete.py @ 17070:ad0d6c2b3279

obsolete: introduction of obsolete markers Markers are stored as binary records in a log structured file in .hg/store/obsstore.
author Pierre-Yves.David@ens-lyon.org
date Thu, 07 Jun 2012 19:07:39 +0200
parents
children 11f26e2669aa
comparison
equal deleted inserted replaced
17069:2b1c78674230 17070:ad0d6c2b3279
1 # obsolete.py - obsolete markers handling
2 #
3 # Copyright 2012 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
4 # Logilab SA <contact@logilab.fr>
5 #
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
8
9 """Obsolete markers handling
10
11 An obsolete marker maps an old changeset to a list of new
12 changesets. If the list of new changesets is empty, the old changeset
13 is said to be "killed". Otherwise, the old changeset is being
14 "replaced" by the new changesets.
15
16 Obsolete markers can be used to record and distribute changeset graph
17 transformations performed by history rewriting operations, and help
18 building new tools to reconciliate conflicting rewriting actions. To
19 facilitate conflicts resolution, markers include various annotations
20 besides old and news changeset identifiers, such as creation date or
21 author name.
22
23
24 Format
25 ------
26
27 Markers are stored in an append-only file stored in
28 '.hg/store/obsstore'.
29
30 The file starts with a version header:
31
32 - 1 unsigned byte: version number, starting at zero.
33
34
35 The header is followed by the markers. Each marker is made of:
36
37 - 1 unsigned byte: number of new changesets "R", could be zero.
38
39 - 1 unsigned 32-bits integer: metadata size "M" in bytes.
40
41 - 1 byte: a bit field. It is reserved for flags used in obsolete
42 markers common operations, to avoid repeated decoding of metadata
43 entries.
44
45 - 20 bytes: obsoleted changeset identifier.
46
47 - N*20 bytes: new changesets identifiers.
48
49 - M bytes: metadata as a sequence of nul-terminated strings. Each
50 string contains a key and a value, separated by a color ':', without
51 additional encoding. Keys cannot contain '\0' or ':' and values
52 cannot contain '\0'.
53 """
54 import struct
55 from mercurial import util
56 from i18n import _
57
58 _pack = struct.pack
59 _unpack = struct.unpack
60
61
62
63 # data used for parsing and writing
64 _fmversion = 0
65 _fmfixed = '>BIB20s'
66 _fmnode = '20s'
67 _fmfsize = struct.calcsize(_fmfixed)
68 _fnodesize = struct.calcsize(_fmnode)
69
70 def _readmarkers(data):
71 """Read and enumerate markers from raw data"""
72 off = 0
73 diskversion = _unpack('>B', data[off:off + 1])[0]
74 off += 1
75 if diskversion != _fmversion:
76 raise util.Abort(_('parsing obsolete marker: unknown version %r')
77 % diskversion)
78
79 # Loop on markers
80 l = len(data)
81 while off + _fmfsize <= l:
82 # read fixed part
83 cur = data[off:off + _fmfsize]
84 off += _fmfsize
85 nbsuc, mdsize, flags, pre = _unpack(_fmfixed, cur)
86 # read replacement
87 sucs = ()
88 if nbsuc:
89 s = (_fnodesize * nbsuc)
90 cur = data[off:off + s]
91 sucs = _unpack(_fmnode * nbsuc, cur)
92 off += s
93 # read metadata
94 # (metadata will be decoded on demand)
95 metadata = data[off:off + mdsize]
96 if len(metadata) != mdsize:
97 raise util.Abort(_('parsing obsolete marker: metadata is too '
98 'short, %d bytes expected, got %d')
99 % (len(metadata), mdsize))
100 off += mdsize
101 yield (pre, sucs, flags, metadata)
102
103 def encodemeta(meta):
104 """Return encoded metadata string to string mapping.
105
106 Assume no ':' in key and no '\0' in both key and value."""
107 for key, value in meta.iteritems():
108 if ':' in key or '\0' in key:
109 raise ValueError("':' and '\0' are forbidden in metadata key'")
110 if '\0' in value:
111 raise ValueError("':' are forbidden in metadata value'")
112 return '\0'.join(['%s:%s' % (k, meta[k]) for k in sorted(meta)])
113
114 def decodemeta(data):
115 """Return string to string dictionary from encoded version."""
116 d = {}
117 for l in data.split('\0'):
118 if l:
119 key, value = l.split(':')
120 d[key] = value
121 return d
122
123 class obsstore(object):
124 """Store obsolete markers
125
126 Markers can be accessed with two mappings:
127 - precursors: old -> set(new)
128 - successors: new -> set(old)
129 """
130
131 def __init__(self):
132 self._all = []
133 # new markers to serialize
134 self._new = []
135 self.precursors = {}
136 self.successors = {}
137
138 def add(self, marker):
139 """Add a new marker to the store
140
141 This marker still needs to be written to disk"""
142 self._new.append(marker)
143 self._load(marker)
144
145 def loadmarkers(self, data):
146 """Load all markers in data, mark them as known."""
147 for marker in _readmarkers(data):
148 self._load(marker)
149
150 def flushmarkers(self, stream):
151 """Write all markers to a stream
152
153 After this operation, "new" markers are considered "known"."""
154 self._writemarkers(stream)
155 self._new[:] = []
156
157 def _load(self, marker):
158 self._all.append(marker)
159 pre, sucs = marker[:2]
160 self.precursors.setdefault(pre, set()).add(marker)
161 for suc in sucs:
162 self.successors.setdefault(suc, set()).add(marker)
163
164 def _writemarkers(self, stream):
165 # Kept separate from flushmarkers(), it will be reused for
166 # markers exchange.
167 stream.write(_pack('>B', _fmversion))
168 for marker in self._all:
169 pre, sucs, flags, metadata = marker
170 nbsuc = len(sucs)
171 format = _fmfixed + (_fmnode * nbsuc)
172 data = [nbsuc, len(metadata), flags, pre]
173 data.extend(sucs)
174 stream.write(_pack(format, *data))
175 stream.write(metadata)