Show More
@@ -0,0 +1,175 b'' | |||||
|
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) |
@@ -7,7 +7,7 b'' | |||||
7 |
|
7 | |||
8 | from node import bin, hex, nullid, nullrev, short |
|
8 | from node import bin, hex, nullid, nullrev, short | |
9 | from i18n import _ |
|
9 | from i18n import _ | |
10 | import repo, changegroup, subrepo, discovery, pushkey |
|
10 | import repo, changegroup, subrepo, discovery, pushkey, obsolete | |
11 | import changelog, dirstate, filelog, manifest, context, bookmarks, phases |
|
11 | import changelog, dirstate, filelog, manifest, context, bookmarks, phases | |
12 | import lock, transaction, store, encoding |
|
12 | import lock, transaction, store, encoding | |
13 | import scmutil, util, extensions, hook, error, revset |
|
13 | import scmutil, util, extensions, hook, error, revset | |
@@ -192,6 +192,14 b' class localrepository(repo.repository):' | |||||
192 | def _phasecache(self): |
|
192 | def _phasecache(self): | |
193 | return phases.phasecache(self, self._phasedefaults) |
|
193 | return phases.phasecache(self, self._phasedefaults) | |
194 |
|
194 | |||
|
195 | @storecache('obsstore') | |||
|
196 | def obsstore(self): | |||
|
197 | store = obsolete.obsstore() | |||
|
198 | data = self.sopener.tryread('obsstore') | |||
|
199 | if data: | |||
|
200 | store.loadmarkers(data) | |||
|
201 | return store | |||
|
202 | ||||
195 | @storecache('00changelog.i') |
|
203 | @storecache('00changelog.i') | |
196 | def changelog(self): |
|
204 | def changelog(self): | |
197 | c = changelog.changelog(self.sopener) |
|
205 | c = changelog.changelog(self.sopener) | |
@@ -983,6 +991,16 b' class localrepository(repo.repository):' | |||||
983 | self.store.write() |
|
991 | self.store.write() | |
984 | if '_phasecache' in vars(self): |
|
992 | if '_phasecache' in vars(self): | |
985 | self._phasecache.write() |
|
993 | self._phasecache.write() | |
|
994 | if 'obsstore' in vars(self) and self.obsstore._new: | |||
|
995 | # XXX: transaction logic should be used here. But for | |||
|
996 | # now rewriting the whole file is good enough. | |||
|
997 | f = self.sopener('obsstore', 'wb', atomictemp=True) | |||
|
998 | try: | |||
|
999 | self.obsstore.flushmarkers(f) | |||
|
1000 | f.close() | |||
|
1001 | except: # re-raises | |||
|
1002 | f.discard() | |||
|
1003 | raise | |||
986 | for k, ce in self._filecache.items(): |
|
1004 | for k, ce in self._filecache.items(): | |
987 | if k == 'dirstate': |
|
1005 | if k == 'dirstate': | |
988 | continue |
|
1006 | continue |
General Comments 0
You need to be logged in to leave comments.
Login now