##// END OF EJS Templates
obsolete: gather _fm0 meta encoding with other _fm0 code
Pierre-Yves David -
r22848:72100c1d default
parent child Browse files
Show More
@@ -1,1007 +1,1008
1 # obsolete.py - obsolete markers handling
1 # obsolete.py - obsolete markers handling
2 #
2 #
3 # Copyright 2012 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
3 # Copyright 2012 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
4 # Logilab SA <contact@logilab.fr>
4 # Logilab SA <contact@logilab.fr>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
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.
7 # GNU General Public License version 2 or any later version.
8
8
9 """Obsolete marker handling
9 """Obsolete marker handling
10
10
11 An obsolete marker maps an old changeset to a list of new
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
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
13 is said to be "killed". Otherwise, the old changeset is being
14 "replaced" by the new changesets.
14 "replaced" by the new changesets.
15
15
16 Obsolete markers can be used to record and distribute changeset graph
16 Obsolete markers can be used to record and distribute changeset graph
17 transformations performed by history rewrite operations, and help
17 transformations performed by history rewrite operations, and help
18 building new tools to reconcile conflicting rewrite actions. To
18 building new tools to reconcile conflicting rewrite actions. To
19 facilitate conflict resolution, markers include various annotations
19 facilitate conflict resolution, markers include various annotations
20 besides old and news changeset identifiers, such as creation date or
20 besides old and news changeset identifiers, such as creation date or
21 author name.
21 author name.
22
22
23 The old obsoleted changeset is called a "precursor" and possible
23 The old obsoleted changeset is called a "precursor" and possible
24 replacements are called "successors". Markers that used changeset X as
24 replacements are called "successors". Markers that used changeset X as
25 a precursor are called "successor markers of X" because they hold
25 a precursor are called "successor markers of X" because they hold
26 information about the successors of X. Markers that use changeset Y as
26 information about the successors of X. Markers that use changeset Y as
27 a successors are call "precursor markers of Y" because they hold
27 a successors are call "precursor markers of Y" because they hold
28 information about the precursors of Y.
28 information about the precursors of Y.
29
29
30 Examples:
30 Examples:
31
31
32 - When changeset A is replaced by changeset A', one marker is stored:
32 - When changeset A is replaced by changeset A', one marker is stored:
33
33
34 (A, (A',))
34 (A, (A',))
35
35
36 - When changesets A and B are folded into a new changeset C, two markers are
36 - When changesets A and B are folded into a new changeset C, two markers are
37 stored:
37 stored:
38
38
39 (A, (C,)) and (B, (C,))
39 (A, (C,)) and (B, (C,))
40
40
41 - When changeset A is simply "pruned" from the graph, a marker is created:
41 - When changeset A is simply "pruned" from the graph, a marker is created:
42
42
43 (A, ())
43 (A, ())
44
44
45 - When changeset A is split into B and C, a single marker are used:
45 - When changeset A is split into B and C, a single marker are used:
46
46
47 (A, (C, C))
47 (A, (C, C))
48
48
49 We use a single marker to distinguish the "split" case from the "divergence"
49 We use a single marker to distinguish the "split" case from the "divergence"
50 case. If two independent operations rewrite the same changeset A in to A' and
50 case. If two independent operations rewrite the same changeset A in to A' and
51 A'', we have an error case: divergent rewriting. We can detect it because
51 A'', we have an error case: divergent rewriting. We can detect it because
52 two markers will be created independently:
52 two markers will be created independently:
53
53
54 (A, (B,)) and (A, (C,))
54 (A, (B,)) and (A, (C,))
55
55
56 Format
56 Format
57 ------
57 ------
58
58
59 Markers are stored in an append-only file stored in
59 Markers are stored in an append-only file stored in
60 '.hg/store/obsstore'.
60 '.hg/store/obsstore'.
61
61
62 The file starts with a version header:
62 The file starts with a version header:
63
63
64 - 1 unsigned byte: version number, starting at zero.
64 - 1 unsigned byte: version number, starting at zero.
65
65
66 The header is followed by the markers. Marker format depend of the version. See
66 The header is followed by the markers. Marker format depend of the version. See
67 comment associated with each format for details.
67 comment associated with each format for details.
68
68
69 """
69 """
70 import struct
70 import struct
71 import util, base85, node
71 import util, base85, node
72 import phases
72 import phases
73 from i18n import _
73 from i18n import _
74
74
75 _pack = struct.pack
75 _pack = struct.pack
76 _unpack = struct.unpack
76 _unpack = struct.unpack
77
77
78 _SEEK_END = 2 # os.SEEK_END was introduced in Python 2.5
78 _SEEK_END = 2 # os.SEEK_END was introduced in Python 2.5
79
79
80 # the obsolete feature is not mature enough to be enabled by default.
80 # the obsolete feature is not mature enough to be enabled by default.
81 # you have to rely on third party extension extension to enable this.
81 # you have to rely on third party extension extension to enable this.
82 _enabled = False
82 _enabled = False
83
83
84 ### obsolescence marker flag
84 ### obsolescence marker flag
85
85
86 ## bumpedfix flag
86 ## bumpedfix flag
87 #
87 #
88 # When a changeset A' succeed to a changeset A which became public, we call A'
88 # When a changeset A' succeed to a changeset A which became public, we call A'
89 # "bumped" because it's a successors of a public changesets
89 # "bumped" because it's a successors of a public changesets
90 #
90 #
91 # o A' (bumped)
91 # o A' (bumped)
92 # |`:
92 # |`:
93 # | o A
93 # | o A
94 # |/
94 # |/
95 # o Z
95 # o Z
96 #
96 #
97 # The way to solve this situation is to create a new changeset Ad as children
97 # The way to solve this situation is to create a new changeset Ad as children
98 # of A. This changeset have the same content than A'. So the diff from A to A'
98 # of A. This changeset have the same content than A'. So the diff from A to A'
99 # is the same than the diff from A to Ad. Ad is marked as a successors of A'
99 # is the same than the diff from A to Ad. Ad is marked as a successors of A'
100 #
100 #
101 # o Ad
101 # o Ad
102 # |`:
102 # |`:
103 # | x A'
103 # | x A'
104 # |'|
104 # |'|
105 # o | A
105 # o | A
106 # |/
106 # |/
107 # o Z
107 # o Z
108 #
108 #
109 # But by transitivity Ad is also a successors of A. To avoid having Ad marked
109 # But by transitivity Ad is also a successors of A. To avoid having Ad marked
110 # as bumped too, we add the `bumpedfix` flag to the marker. <A', (Ad,)>.
110 # as bumped too, we add the `bumpedfix` flag to the marker. <A', (Ad,)>.
111 # This flag mean that the successors express the changes between the public and
111 # This flag mean that the successors express the changes between the public and
112 # bumped version and fix the situation, breaking the transitivity of
112 # bumped version and fix the situation, breaking the transitivity of
113 # "bumped" here.
113 # "bumped" here.
114 bumpedfix = 1
114 bumpedfix = 1
115
115
116 ## Parsing and writing of version "0"
116 ## Parsing and writing of version "0"
117 #
117 #
118 # The header is followed by the markers. Each marker is made of:
118 # The header is followed by the markers. Each marker is made of:
119 #
119 #
120 # - 1 unsigned byte: number of new changesets "N", can be zero.
120 # - 1 unsigned byte: number of new changesets "N", can be zero.
121 #
121 #
122 # - 1 unsigned 32-bits integer: metadata size "M" in bytes.
122 # - 1 unsigned 32-bits integer: metadata size "M" in bytes.
123 #
123 #
124 # - 1 byte: a bit field. It is reserved for flags used in common
124 # - 1 byte: a bit field. It is reserved for flags used in common
125 # obsolete marker operations, to avoid repeated decoding of metadata
125 # obsolete marker operations, to avoid repeated decoding of metadata
126 # entries.
126 # entries.
127 #
127 #
128 # - 20 bytes: obsoleted changeset identifier.
128 # - 20 bytes: obsoleted changeset identifier.
129 #
129 #
130 # - N*20 bytes: new changesets identifiers.
130 # - N*20 bytes: new changesets identifiers.
131 #
131 #
132 # - M bytes: metadata as a sequence of nul-terminated strings. Each
132 # - M bytes: metadata as a sequence of nul-terminated strings. Each
133 # string contains a key and a value, separated by a colon ':', without
133 # string contains a key and a value, separated by a colon ':', without
134 # additional encoding. Keys cannot contain '\0' or ':' and values
134 # additional encoding. Keys cannot contain '\0' or ':' and values
135 # cannot contain '\0'.
135 # cannot contain '\0'.
136 _fm0version = 0
136 _fm0version = 0
137 _fm0fixed = '>BIB20s'
137 _fm0fixed = '>BIB20s'
138 _fm0node = '20s'
138 _fm0node = '20s'
139 _fm0fsize = struct.calcsize(_fm0fixed)
139 _fm0fsize = struct.calcsize(_fm0fixed)
140 _fm0fnodesize = struct.calcsize(_fm0node)
140 _fm0fnodesize = struct.calcsize(_fm0node)
141
141
142 def _fm0readmarkers(data, off=0):
142 def _fm0readmarkers(data, off=0):
143 # Loop on markers
143 # Loop on markers
144 l = len(data)
144 l = len(data)
145 while off + _fm0fsize <= l:
145 while off + _fm0fsize <= l:
146 # read fixed part
146 # read fixed part
147 cur = data[off:off + _fm0fsize]
147 cur = data[off:off + _fm0fsize]
148 off += _fm0fsize
148 off += _fm0fsize
149 numsuc, mdsize, flags, pre = _unpack(_fm0fixed, cur)
149 numsuc, mdsize, flags, pre = _unpack(_fm0fixed, cur)
150 # read replacement
150 # read replacement
151 sucs = ()
151 sucs = ()
152 if numsuc:
152 if numsuc:
153 s = (_fm0fnodesize * numsuc)
153 s = (_fm0fnodesize * numsuc)
154 cur = data[off:off + s]
154 cur = data[off:off + s]
155 sucs = _unpack(_fm0node * numsuc, cur)
155 sucs = _unpack(_fm0node * numsuc, cur)
156 off += s
156 off += s
157 # read metadata
157 # read metadata
158 # (metadata will be decoded on demand)
158 # (metadata will be decoded on demand)
159 metadata = data[off:off + mdsize]
159 metadata = data[off:off + mdsize]
160 if len(metadata) != mdsize:
160 if len(metadata) != mdsize:
161 raise util.Abort(_('parsing obsolete marker: metadata is too '
161 raise util.Abort(_('parsing obsolete marker: metadata is too '
162 'short, %d bytes expected, got %d')
162 'short, %d bytes expected, got %d')
163 % (mdsize, len(metadata)))
163 % (mdsize, len(metadata)))
164 off += mdsize
164 off += mdsize
165 metadata = _fm0decodemeta(metadata)
165 metadata = _fm0decodemeta(metadata)
166 try:
166 try:
167 when, offset = metadata.pop('date', '0 0').split(' ')
167 when, offset = metadata.pop('date', '0 0').split(' ')
168 date = float(when), int(offset)
168 date = float(when), int(offset)
169 except ValueError:
169 except ValueError:
170 date = (0., 0)
170 date = (0., 0)
171 parents = None
171 parents = None
172 if 'p2' in metadata:
172 if 'p2' in metadata:
173 parents = (metadata.pop('p1', None), metadata.pop('p2', None))
173 parents = (metadata.pop('p1', None), metadata.pop('p2', None))
174 elif 'p1' in metadata:
174 elif 'p1' in metadata:
175 parents = (metadata.pop('p1', None),)
175 parents = (metadata.pop('p1', None),)
176 elif 'p0' in metadata:
176 elif 'p0' in metadata:
177 parents = ()
177 parents = ()
178 if parents is not None:
178 if parents is not None:
179 try:
179 try:
180 parents = tuple(node.bin(p) for p in parents)
180 parents = tuple(node.bin(p) for p in parents)
181 # if parent content is not a nodeid, drop the data
181 # if parent content is not a nodeid, drop the data
182 for p in parents:
182 for p in parents:
183 if len(p) != 20:
183 if len(p) != 20:
184 parents = None
184 parents = None
185 break
185 break
186 except TypeError:
186 except TypeError:
187 # if content cannot be translated to nodeid drop the data.
187 # if content cannot be translated to nodeid drop the data.
188 parents = None
188 parents = None
189
189
190 metadata = tuple(sorted(metadata.iteritems()))
190 metadata = tuple(sorted(metadata.iteritems()))
191
191
192 yield (pre, sucs, flags, metadata, date, parents)
192 yield (pre, sucs, flags, metadata, date, parents)
193
193
194 def _fm0encodeonemarker(marker):
194 def _fm0encodeonemarker(marker):
195 pre, sucs, flags, metadata, date, parents = marker
195 pre, sucs, flags, metadata, date, parents = marker
196 metadata = dict(metadata)
196 metadata = dict(metadata)
197 metadata['date'] = '%d %i' % date
197 metadata['date'] = '%d %i' % date
198 if parents is not None:
198 if parents is not None:
199 if not parents:
199 if not parents:
200 # mark that we explicitly recorded no parents
200 # mark that we explicitly recorded no parents
201 metadata['p0'] = ''
201 metadata['p0'] = ''
202 for i, p in enumerate(parents):
202 for i, p in enumerate(parents):
203 metadata['p%i' % (i + 1)] = node.hex(p)
203 metadata['p%i' % (i + 1)] = node.hex(p)
204 metadata = _fm0encodemeta(metadata)
204 metadata = _fm0encodemeta(metadata)
205 numsuc = len(sucs)
205 numsuc = len(sucs)
206 format = _fm0fixed + (_fm0node * numsuc)
206 format = _fm0fixed + (_fm0node * numsuc)
207 data = [numsuc, len(metadata), flags, pre]
207 data = [numsuc, len(metadata), flags, pre]
208 data.extend(sucs)
208 data.extend(sucs)
209 return _pack(format, *data) + metadata
209 return _pack(format, *data) + metadata
210
210
211 def _fm0encodemeta(meta):
212 """Return encoded metadata string to string mapping.
213
214 Assume no ':' in key and no '\0' in both key and value."""
215 for key, value in meta.iteritems():
216 if ':' in key or '\0' in key:
217 raise ValueError("':' and '\0' are forbidden in metadata key'")
218 if '\0' in value:
219 raise ValueError("':' is forbidden in metadata value'")
220 return '\0'.join(['%s:%s' % (k, meta[k]) for k in sorted(meta)])
221
222 def _fm0decodemeta(data):
223 """Return string to string dictionary from encoded version."""
224 d = {}
225 for l in data.split('\0'):
226 if l:
227 key, value = l.split(':')
228 d[key] = value
229 return d
230
231
211 # mapping to read/write various marker formats
232 # mapping to read/write various marker formats
212 # <version> -> (decoder, encoder)
233 # <version> -> (decoder, encoder)
213 formats = {_fm0version: (_fm0readmarkers, _fm0encodeonemarker)}
234 formats = {_fm0version: (_fm0readmarkers, _fm0encodeonemarker)}
214
235
215 def _readmarkers(data):
236 def _readmarkers(data):
216 """Read and enumerate markers from raw data"""
237 """Read and enumerate markers from raw data"""
217 off = 0
238 off = 0
218 diskversion = _unpack('>B', data[off:off + 1])[0]
239 diskversion = _unpack('>B', data[off:off + 1])[0]
219 off += 1
240 off += 1
220 if diskversion not in formats:
241 if diskversion not in formats:
221 raise util.Abort(_('parsing obsolete marker: unknown version %r')
242 raise util.Abort(_('parsing obsolete marker: unknown version %r')
222 % diskversion)
243 % diskversion)
223 return diskversion, formats[diskversion][0](data, off)
244 return diskversion, formats[diskversion][0](data, off)
224
245
225 def encodemarkers(markers, addheader=False, version=_fm0version):
246 def encodemarkers(markers, addheader=False, version=_fm0version):
226 # Kept separate from flushmarkers(), it will be reused for
247 # Kept separate from flushmarkers(), it will be reused for
227 # markers exchange.
248 # markers exchange.
228 encodeone = formats[version][1]
249 encodeone = formats[version][1]
229 if addheader:
250 if addheader:
230 yield _pack('>B', version)
251 yield _pack('>B', version)
231 for marker in markers:
252 for marker in markers:
232 yield encodeone(marker)
253 yield encodeone(marker)
233
254
234
255
235 def _fm0encodemeta(meta):
236 """Return encoded metadata string to string mapping.
237
238 Assume no ':' in key and no '\0' in both key and value."""
239 for key, value in meta.iteritems():
240 if ':' in key or '\0' in key:
241 raise ValueError("':' and '\0' are forbidden in metadata key'")
242 if '\0' in value:
243 raise ValueError("':' is forbidden in metadata value'")
244 return '\0'.join(['%s:%s' % (k, meta[k]) for k in sorted(meta)])
245
246 def _fm0decodemeta(data):
247 """Return string to string dictionary from encoded version."""
248 d = {}
249 for l in data.split('\0'):
250 if l:
251 key, value = l.split(':')
252 d[key] = value
253 return d
254
255 class marker(object):
256 class marker(object):
256 """Wrap obsolete marker raw data"""
257 """Wrap obsolete marker raw data"""
257
258
258 def __init__(self, repo, data):
259 def __init__(self, repo, data):
259 # the repo argument will be used to create changectx in later version
260 # the repo argument will be used to create changectx in later version
260 self._repo = repo
261 self._repo = repo
261 self._data = data
262 self._data = data
262 self._decodedmeta = None
263 self._decodedmeta = None
263
264
264 def __hash__(self):
265 def __hash__(self):
265 return hash(self._data)
266 return hash(self._data)
266
267
267 def __eq__(self, other):
268 def __eq__(self, other):
268 if type(other) != type(self):
269 if type(other) != type(self):
269 return False
270 return False
270 return self._data == other._data
271 return self._data == other._data
271
272
272 def precnode(self):
273 def precnode(self):
273 """Precursor changeset node identifier"""
274 """Precursor changeset node identifier"""
274 return self._data[0]
275 return self._data[0]
275
276
276 def succnodes(self):
277 def succnodes(self):
277 """List of successor changesets node identifiers"""
278 """List of successor changesets node identifiers"""
278 return self._data[1]
279 return self._data[1]
279
280
280 def parentnodes(self):
281 def parentnodes(self):
281 """Parents of the precursors (None if not recorded)"""
282 """Parents of the precursors (None if not recorded)"""
282 return self._data[5]
283 return self._data[5]
283
284
284 def metadata(self):
285 def metadata(self):
285 """Decoded metadata dictionary"""
286 """Decoded metadata dictionary"""
286 return dict(self._data[3])
287 return dict(self._data[3])
287
288
288 def date(self):
289 def date(self):
289 """Creation date as (unixtime, offset)"""
290 """Creation date as (unixtime, offset)"""
290 return self._data[4]
291 return self._data[4]
291
292
292 def flags(self):
293 def flags(self):
293 """The flags field of the marker"""
294 """The flags field of the marker"""
294 return self._data[2]
295 return self._data[2]
295
296
296 class obsstore(object):
297 class obsstore(object):
297 """Store obsolete markers
298 """Store obsolete markers
298
299
299 Markers can be accessed with two mappings:
300 Markers can be accessed with two mappings:
300 - precursors[x] -> set(markers on precursors edges of x)
301 - precursors[x] -> set(markers on precursors edges of x)
301 - successors[x] -> set(markers on successors edges of x)
302 - successors[x] -> set(markers on successors edges of x)
302 - children[x] -> set(markers on precursors edges of children(x)
303 - children[x] -> set(markers on precursors edges of children(x)
303 """
304 """
304
305
305 fields = ('prec', 'succs', 'flag', 'meta', 'date', 'parents')
306 fields = ('prec', 'succs', 'flag', 'meta', 'date', 'parents')
306 # prec: nodeid, precursor changesets
307 # prec: nodeid, precursor changesets
307 # succs: tuple of nodeid, successor changesets (0-N length)
308 # succs: tuple of nodeid, successor changesets (0-N length)
308 # flag: integer, flag field carrying modifier for the markers (see doc)
309 # flag: integer, flag field carrying modifier for the markers (see doc)
309 # meta: binary blob, encoded metadata dictionary
310 # meta: binary blob, encoded metadata dictionary
310 # date: (float, int) tuple, date of marker creation
311 # date: (float, int) tuple, date of marker creation
311 # parents: (tuple of nodeid) or None, parents of precursors
312 # parents: (tuple of nodeid) or None, parents of precursors
312 # None is used when no data has been recorded
313 # None is used when no data has been recorded
313
314
314 def __init__(self, sopener):
315 def __init__(self, sopener):
315 # caches for various obsolescence related cache
316 # caches for various obsolescence related cache
316 self.caches = {}
317 self.caches = {}
317 self._all = []
318 self._all = []
318 self.precursors = {}
319 self.precursors = {}
319 self.successors = {}
320 self.successors = {}
320 self.children = {}
321 self.children = {}
321 self.sopener = sopener
322 self.sopener = sopener
322 data = sopener.tryread('obsstore')
323 data = sopener.tryread('obsstore')
323 self._version = _fm0version
324 self._version = _fm0version
324 if data:
325 if data:
325 self._version, markers = _readmarkers(data)
326 self._version, markers = _readmarkers(data)
326 self._load(markers)
327 self._load(markers)
327
328
328 def __iter__(self):
329 def __iter__(self):
329 return iter(self._all)
330 return iter(self._all)
330
331
331 def __len__(self):
332 def __len__(self):
332 return len(self._all)
333 return len(self._all)
333
334
334 def __nonzero__(self):
335 def __nonzero__(self):
335 return bool(self._all)
336 return bool(self._all)
336
337
337 def create(self, transaction, prec, succs=(), flag=0, parents=None,
338 def create(self, transaction, prec, succs=(), flag=0, parents=None,
338 date=None, metadata=None):
339 date=None, metadata=None):
339 """obsolete: add a new obsolete marker
340 """obsolete: add a new obsolete marker
340
341
341 * ensuring it is hashable
342 * ensuring it is hashable
342 * check mandatory metadata
343 * check mandatory metadata
343 * encode metadata
344 * encode metadata
344
345
345 If you are a human writing code creating marker you want to use the
346 If you are a human writing code creating marker you want to use the
346 `createmarkers` function in this module instead.
347 `createmarkers` function in this module instead.
347
348
348 return True if a new marker have been added, False if the markers
349 return True if a new marker have been added, False if the markers
349 already existed (no op).
350 already existed (no op).
350 """
351 """
351 if metadata is None:
352 if metadata is None:
352 metadata = {}
353 metadata = {}
353 if date is None:
354 if date is None:
354 if 'date' in metadata:
355 if 'date' in metadata:
355 # as a courtesy for out-of-tree extensions
356 # as a courtesy for out-of-tree extensions
356 date = util.parsedate(metadata.pop('date'))
357 date = util.parsedate(metadata.pop('date'))
357 else:
358 else:
358 date = util.makedate()
359 date = util.makedate()
359 if len(prec) != 20:
360 if len(prec) != 20:
360 raise ValueError(prec)
361 raise ValueError(prec)
361 for succ in succs:
362 for succ in succs:
362 if len(succ) != 20:
363 if len(succ) != 20:
363 raise ValueError(succ)
364 raise ValueError(succ)
364 if prec in succs:
365 if prec in succs:
365 raise ValueError(_('in-marker cycle with %s') % node.hex(prec))
366 raise ValueError(_('in-marker cycle with %s') % node.hex(prec))
366
367
367 metadata = tuple(sorted(metadata.iteritems()))
368 metadata = tuple(sorted(metadata.iteritems()))
368
369
369 marker = (str(prec), tuple(succs), int(flag), metadata, date, parents)
370 marker = (str(prec), tuple(succs), int(flag), metadata, date, parents)
370 return bool(self.add(transaction, [marker]))
371 return bool(self.add(transaction, [marker]))
371
372
372 def add(self, transaction, markers):
373 def add(self, transaction, markers):
373 """Add new markers to the store
374 """Add new markers to the store
374
375
375 Take care of filtering duplicate.
376 Take care of filtering duplicate.
376 Return the number of new marker."""
377 Return the number of new marker."""
377 if not _enabled:
378 if not _enabled:
378 raise util.Abort('obsolete feature is not enabled on this repo')
379 raise util.Abort('obsolete feature is not enabled on this repo')
379 known = set(self._all)
380 known = set(self._all)
380 new = []
381 new = []
381 for m in markers:
382 for m in markers:
382 if m not in known:
383 if m not in known:
383 known.add(m)
384 known.add(m)
384 new.append(m)
385 new.append(m)
385 if new:
386 if new:
386 f = self.sopener('obsstore', 'ab')
387 f = self.sopener('obsstore', 'ab')
387 try:
388 try:
388 # Whether the file's current position is at the begin or at
389 # Whether the file's current position is at the begin or at
389 # the end after opening a file for appending is implementation
390 # the end after opening a file for appending is implementation
390 # defined. So we must seek to the end before calling tell(),
391 # defined. So we must seek to the end before calling tell(),
391 # or we may get a zero offset for non-zero sized files on
392 # or we may get a zero offset for non-zero sized files on
392 # some platforms (issue3543).
393 # some platforms (issue3543).
393 f.seek(0, _SEEK_END)
394 f.seek(0, _SEEK_END)
394 offset = f.tell()
395 offset = f.tell()
395 transaction.add('obsstore', offset)
396 transaction.add('obsstore', offset)
396 # offset == 0: new file - add the version header
397 # offset == 0: new file - add the version header
397 for bytes in encodemarkers(new, offset == 0, self._version):
398 for bytes in encodemarkers(new, offset == 0, self._version):
398 f.write(bytes)
399 f.write(bytes)
399 finally:
400 finally:
400 # XXX: f.close() == filecache invalidation == obsstore rebuilt.
401 # XXX: f.close() == filecache invalidation == obsstore rebuilt.
401 # call 'filecacheentry.refresh()' here
402 # call 'filecacheentry.refresh()' here
402 f.close()
403 f.close()
403 self._load(new)
404 self._load(new)
404 # new marker *may* have changed several set. invalidate the cache.
405 # new marker *may* have changed several set. invalidate the cache.
405 self.caches.clear()
406 self.caches.clear()
406 # records the number of new markers for the transaction hooks
407 # records the number of new markers for the transaction hooks
407 previous = int(transaction.hookargs.get('new_obsmarkers', '0'))
408 previous = int(transaction.hookargs.get('new_obsmarkers', '0'))
408 transaction.hookargs['new_obsmarkers'] = str(previous + len(new))
409 transaction.hookargs['new_obsmarkers'] = str(previous + len(new))
409 return len(new)
410 return len(new)
410
411
411 def mergemarkers(self, transaction, data):
412 def mergemarkers(self, transaction, data):
412 """merge a binary stream of markers inside the obsstore
413 """merge a binary stream of markers inside the obsstore
413
414
414 Returns the number of new markers added."""
415 Returns the number of new markers added."""
415 version, markers = _readmarkers(data)
416 version, markers = _readmarkers(data)
416 return self.add(transaction, markers)
417 return self.add(transaction, markers)
417
418
418 def _load(self, markers):
419 def _load(self, markers):
419 for mark in markers:
420 for mark in markers:
420 self._all.append(mark)
421 self._all.append(mark)
421 pre, sucs = mark[:2]
422 pre, sucs = mark[:2]
422 self.successors.setdefault(pre, set()).add(mark)
423 self.successors.setdefault(pre, set()).add(mark)
423 for suc in sucs:
424 for suc in sucs:
424 self.precursors.setdefault(suc, set()).add(mark)
425 self.precursors.setdefault(suc, set()).add(mark)
425 parents = mark[5]
426 parents = mark[5]
426 if parents is not None:
427 if parents is not None:
427 for p in parents:
428 for p in parents:
428 self.children.setdefault(p, set()).add(mark)
429 self.children.setdefault(p, set()).add(mark)
429 if node.nullid in self.precursors:
430 if node.nullid in self.precursors:
430 raise util.Abort(_('bad obsolescence marker detected: '
431 raise util.Abort(_('bad obsolescence marker detected: '
431 'invalid successors nullid'))
432 'invalid successors nullid'))
432 def relevantmarkers(self, nodes):
433 def relevantmarkers(self, nodes):
433 """return a set of all obsolescence markers relevant to a set of nodes.
434 """return a set of all obsolescence markers relevant to a set of nodes.
434
435
435 "relevant" to a set of nodes mean:
436 "relevant" to a set of nodes mean:
436
437
437 - marker that use this changeset as successor
438 - marker that use this changeset as successor
438 - prune marker of direct children on this changeset
439 - prune marker of direct children on this changeset
439 - recursive application of the two rules on precursors of these markers
440 - recursive application of the two rules on precursors of these markers
440
441
441 It is a set so you cannot rely on order."""
442 It is a set so you cannot rely on order."""
442
443
443 pendingnodes = set(nodes)
444 pendingnodes = set(nodes)
444 seenmarkers = set()
445 seenmarkers = set()
445 seennodes = set(pendingnodes)
446 seennodes = set(pendingnodes)
446 precursorsmarkers = self.precursors
447 precursorsmarkers = self.precursors
447 children = self.children
448 children = self.children
448 while pendingnodes:
449 while pendingnodes:
449 direct = set()
450 direct = set()
450 for current in pendingnodes:
451 for current in pendingnodes:
451 direct.update(precursorsmarkers.get(current, ()))
452 direct.update(precursorsmarkers.get(current, ()))
452 pruned = [m for m in children.get(current, ()) if not m[1]]
453 pruned = [m for m in children.get(current, ()) if not m[1]]
453 direct.update(pruned)
454 direct.update(pruned)
454 direct -= seenmarkers
455 direct -= seenmarkers
455 pendingnodes = set([m[0] for m in direct])
456 pendingnodes = set([m[0] for m in direct])
456 seenmarkers |= direct
457 seenmarkers |= direct
457 pendingnodes -= seennodes
458 pendingnodes -= seennodes
458 seennodes |= pendingnodes
459 seennodes |= pendingnodes
459 return seenmarkers
460 return seenmarkers
460
461
461 def commonversion(versions):
462 def commonversion(versions):
462 """Return the newest version listed in both versions and our local formats.
463 """Return the newest version listed in both versions and our local formats.
463
464
464 Returns None if no common version exists.
465 Returns None if no common version exists.
465 """
466 """
466 versions.sort(reverse=True)
467 versions.sort(reverse=True)
467 # search for highest version known on both side
468 # search for highest version known on both side
468 for v in versions:
469 for v in versions:
469 if v in formats:
470 if v in formats:
470 return v
471 return v
471 return None
472 return None
472
473
473 # arbitrary picked to fit into 8K limit from HTTP server
474 # arbitrary picked to fit into 8K limit from HTTP server
474 # you have to take in account:
475 # you have to take in account:
475 # - the version header
476 # - the version header
476 # - the base85 encoding
477 # - the base85 encoding
477 _maxpayload = 5300
478 _maxpayload = 5300
478
479
479 def _pushkeyescape(markers):
480 def _pushkeyescape(markers):
480 """encode markers into a dict suitable for pushkey exchange
481 """encode markers into a dict suitable for pushkey exchange
481
482
482 - binary data is base85 encoded
483 - binary data is base85 encoded
483 - split in chunks smaller than 5300 bytes"""
484 - split in chunks smaller than 5300 bytes"""
484 keys = {}
485 keys = {}
485 parts = []
486 parts = []
486 currentlen = _maxpayload * 2 # ensure we create a new part
487 currentlen = _maxpayload * 2 # ensure we create a new part
487 for marker in markers:
488 for marker in markers:
488 nextdata = _fm0encodeonemarker(marker)
489 nextdata = _fm0encodeonemarker(marker)
489 if (len(nextdata) + currentlen > _maxpayload):
490 if (len(nextdata) + currentlen > _maxpayload):
490 currentpart = []
491 currentpart = []
491 currentlen = 0
492 currentlen = 0
492 parts.append(currentpart)
493 parts.append(currentpart)
493 currentpart.append(nextdata)
494 currentpart.append(nextdata)
494 currentlen += len(nextdata)
495 currentlen += len(nextdata)
495 for idx, part in enumerate(reversed(parts)):
496 for idx, part in enumerate(reversed(parts)):
496 data = ''.join([_pack('>B', _fm0version)] + part)
497 data = ''.join([_pack('>B', _fm0version)] + part)
497 keys['dump%i' % idx] = base85.b85encode(data)
498 keys['dump%i' % idx] = base85.b85encode(data)
498 return keys
499 return keys
499
500
500 def listmarkers(repo):
501 def listmarkers(repo):
501 """List markers over pushkey"""
502 """List markers over pushkey"""
502 if not repo.obsstore:
503 if not repo.obsstore:
503 return {}
504 return {}
504 return _pushkeyescape(repo.obsstore)
505 return _pushkeyescape(repo.obsstore)
505
506
506 def pushmarker(repo, key, old, new):
507 def pushmarker(repo, key, old, new):
507 """Push markers over pushkey"""
508 """Push markers over pushkey"""
508 if not key.startswith('dump'):
509 if not key.startswith('dump'):
509 repo.ui.warn(_('unknown key: %r') % key)
510 repo.ui.warn(_('unknown key: %r') % key)
510 return 0
511 return 0
511 if old:
512 if old:
512 repo.ui.warn(_('unexpected old value for %r') % key)
513 repo.ui.warn(_('unexpected old value for %r') % key)
513 return 0
514 return 0
514 data = base85.b85decode(new)
515 data = base85.b85decode(new)
515 lock = repo.lock()
516 lock = repo.lock()
516 try:
517 try:
517 tr = repo.transaction('pushkey: obsolete markers')
518 tr = repo.transaction('pushkey: obsolete markers')
518 try:
519 try:
519 repo.obsstore.mergemarkers(tr, data)
520 repo.obsstore.mergemarkers(tr, data)
520 tr.close()
521 tr.close()
521 return 1
522 return 1
522 finally:
523 finally:
523 tr.release()
524 tr.release()
524 finally:
525 finally:
525 lock.release()
526 lock.release()
526
527
527 def getmarkers(repo, nodes=None):
528 def getmarkers(repo, nodes=None):
528 """returns markers known in a repository
529 """returns markers known in a repository
529
530
530 If <nodes> is specified, only markers "relevant" to those nodes are are
531 If <nodes> is specified, only markers "relevant" to those nodes are are
531 returned"""
532 returned"""
532 if nodes is None:
533 if nodes is None:
533 rawmarkers = repo.obsstore
534 rawmarkers = repo.obsstore
534 else:
535 else:
535 rawmarkers = repo.obsstore.relevantmarkers(nodes)
536 rawmarkers = repo.obsstore.relevantmarkers(nodes)
536
537
537 for markerdata in rawmarkers:
538 for markerdata in rawmarkers:
538 yield marker(repo, markerdata)
539 yield marker(repo, markerdata)
539
540
540 def relevantmarkers(repo, node):
541 def relevantmarkers(repo, node):
541 """all obsolete markers relevant to some revision"""
542 """all obsolete markers relevant to some revision"""
542 for markerdata in repo.obsstore.relevantmarkers(node):
543 for markerdata in repo.obsstore.relevantmarkers(node):
543 yield marker(repo, markerdata)
544 yield marker(repo, markerdata)
544
545
545
546
546 def precursormarkers(ctx):
547 def precursormarkers(ctx):
547 """obsolete marker marking this changeset as a successors"""
548 """obsolete marker marking this changeset as a successors"""
548 for data in ctx._repo.obsstore.precursors.get(ctx.node(), ()):
549 for data in ctx._repo.obsstore.precursors.get(ctx.node(), ()):
549 yield marker(ctx._repo, data)
550 yield marker(ctx._repo, data)
550
551
551 def successormarkers(ctx):
552 def successormarkers(ctx):
552 """obsolete marker making this changeset obsolete"""
553 """obsolete marker making this changeset obsolete"""
553 for data in ctx._repo.obsstore.successors.get(ctx.node(), ()):
554 for data in ctx._repo.obsstore.successors.get(ctx.node(), ()):
554 yield marker(ctx._repo, data)
555 yield marker(ctx._repo, data)
555
556
556 def allsuccessors(obsstore, nodes, ignoreflags=0):
557 def allsuccessors(obsstore, nodes, ignoreflags=0):
557 """Yield node for every successor of <nodes>.
558 """Yield node for every successor of <nodes>.
558
559
559 Some successors may be unknown locally.
560 Some successors may be unknown locally.
560
561
561 This is a linear yield unsuited to detecting split changesets. It includes
562 This is a linear yield unsuited to detecting split changesets. It includes
562 initial nodes too."""
563 initial nodes too."""
563 remaining = set(nodes)
564 remaining = set(nodes)
564 seen = set(remaining)
565 seen = set(remaining)
565 while remaining:
566 while remaining:
566 current = remaining.pop()
567 current = remaining.pop()
567 yield current
568 yield current
568 for mark in obsstore.successors.get(current, ()):
569 for mark in obsstore.successors.get(current, ()):
569 # ignore marker flagged with specified flag
570 # ignore marker flagged with specified flag
570 if mark[2] & ignoreflags:
571 if mark[2] & ignoreflags:
571 continue
572 continue
572 for suc in mark[1]:
573 for suc in mark[1]:
573 if suc not in seen:
574 if suc not in seen:
574 seen.add(suc)
575 seen.add(suc)
575 remaining.add(suc)
576 remaining.add(suc)
576
577
577 def allprecursors(obsstore, nodes, ignoreflags=0):
578 def allprecursors(obsstore, nodes, ignoreflags=0):
578 """Yield node for every precursors of <nodes>.
579 """Yield node for every precursors of <nodes>.
579
580
580 Some precursors may be unknown locally.
581 Some precursors may be unknown locally.
581
582
582 This is a linear yield unsuited to detecting folded changesets. It includes
583 This is a linear yield unsuited to detecting folded changesets. It includes
583 initial nodes too."""
584 initial nodes too."""
584
585
585 remaining = set(nodes)
586 remaining = set(nodes)
586 seen = set(remaining)
587 seen = set(remaining)
587 while remaining:
588 while remaining:
588 current = remaining.pop()
589 current = remaining.pop()
589 yield current
590 yield current
590 for mark in obsstore.precursors.get(current, ()):
591 for mark in obsstore.precursors.get(current, ()):
591 # ignore marker flagged with specified flag
592 # ignore marker flagged with specified flag
592 if mark[2] & ignoreflags:
593 if mark[2] & ignoreflags:
593 continue
594 continue
594 suc = mark[0]
595 suc = mark[0]
595 if suc not in seen:
596 if suc not in seen:
596 seen.add(suc)
597 seen.add(suc)
597 remaining.add(suc)
598 remaining.add(suc)
598
599
599 def foreground(repo, nodes):
600 def foreground(repo, nodes):
600 """return all nodes in the "foreground" of other node
601 """return all nodes in the "foreground" of other node
601
602
602 The foreground of a revision is anything reachable using parent -> children
603 The foreground of a revision is anything reachable using parent -> children
603 or precursor -> successor relation. It is very similar to "descendant" but
604 or precursor -> successor relation. It is very similar to "descendant" but
604 augmented with obsolescence information.
605 augmented with obsolescence information.
605
606
606 Beware that possible obsolescence cycle may result if complex situation.
607 Beware that possible obsolescence cycle may result if complex situation.
607 """
608 """
608 repo = repo.unfiltered()
609 repo = repo.unfiltered()
609 foreground = set(repo.set('%ln::', nodes))
610 foreground = set(repo.set('%ln::', nodes))
610 if repo.obsstore:
611 if repo.obsstore:
611 # We only need this complicated logic if there is obsolescence
612 # We only need this complicated logic if there is obsolescence
612 # XXX will probably deserve an optimised revset.
613 # XXX will probably deserve an optimised revset.
613 nm = repo.changelog.nodemap
614 nm = repo.changelog.nodemap
614 plen = -1
615 plen = -1
615 # compute the whole set of successors or descendants
616 # compute the whole set of successors or descendants
616 while len(foreground) != plen:
617 while len(foreground) != plen:
617 plen = len(foreground)
618 plen = len(foreground)
618 succs = set(c.node() for c in foreground)
619 succs = set(c.node() for c in foreground)
619 mutable = [c.node() for c in foreground if c.mutable()]
620 mutable = [c.node() for c in foreground if c.mutable()]
620 succs.update(allsuccessors(repo.obsstore, mutable))
621 succs.update(allsuccessors(repo.obsstore, mutable))
621 known = (n for n in succs if n in nm)
622 known = (n for n in succs if n in nm)
622 foreground = set(repo.set('%ln::', known))
623 foreground = set(repo.set('%ln::', known))
623 return set(c.node() for c in foreground)
624 return set(c.node() for c in foreground)
624
625
625
626
626 def successorssets(repo, initialnode, cache=None):
627 def successorssets(repo, initialnode, cache=None):
627 """Return all set of successors of initial nodes
628 """Return all set of successors of initial nodes
628
629
629 The successors set of a changeset A are a group of revisions that succeed
630 The successors set of a changeset A are a group of revisions that succeed
630 A. It succeeds A as a consistent whole, each revision being only a partial
631 A. It succeeds A as a consistent whole, each revision being only a partial
631 replacement. The successors set contains non-obsolete changesets only.
632 replacement. The successors set contains non-obsolete changesets only.
632
633
633 This function returns the full list of successor sets which is why it
634 This function returns the full list of successor sets which is why it
634 returns a list of tuples and not just a single tuple. Each tuple is a valid
635 returns a list of tuples and not just a single tuple. Each tuple is a valid
635 successors set. Not that (A,) may be a valid successors set for changeset A
636 successors set. Not that (A,) may be a valid successors set for changeset A
636 (see below).
637 (see below).
637
638
638 In most cases, a changeset A will have a single element (e.g. the changeset
639 In most cases, a changeset A will have a single element (e.g. the changeset
639 A is replaced by A') in its successors set. Though, it is also common for a
640 A is replaced by A') in its successors set. Though, it is also common for a
640 changeset A to have no elements in its successor set (e.g. the changeset
641 changeset A to have no elements in its successor set (e.g. the changeset
641 has been pruned). Therefore, the returned list of successors sets will be
642 has been pruned). Therefore, the returned list of successors sets will be
642 [(A',)] or [], respectively.
643 [(A',)] or [], respectively.
643
644
644 When a changeset A is split into A' and B', however, it will result in a
645 When a changeset A is split into A' and B', however, it will result in a
645 successors set containing more than a single element, i.e. [(A',B')].
646 successors set containing more than a single element, i.e. [(A',B')].
646 Divergent changesets will result in multiple successors sets, i.e. [(A',),
647 Divergent changesets will result in multiple successors sets, i.e. [(A',),
647 (A'')].
648 (A'')].
648
649
649 If a changeset A is not obsolete, then it will conceptually have no
650 If a changeset A is not obsolete, then it will conceptually have no
650 successors set. To distinguish this from a pruned changeset, the successor
651 successors set. To distinguish this from a pruned changeset, the successor
651 set will only contain itself, i.e. [(A,)].
652 set will only contain itself, i.e. [(A,)].
652
653
653 Finally, successors unknown locally are considered to be pruned (obsoleted
654 Finally, successors unknown locally are considered to be pruned (obsoleted
654 without any successors).
655 without any successors).
655
656
656 The optional `cache` parameter is a dictionary that may contain precomputed
657 The optional `cache` parameter is a dictionary that may contain precomputed
657 successors sets. It is meant to reuse the computation of a previous call to
658 successors sets. It is meant to reuse the computation of a previous call to
658 `successorssets` when multiple calls are made at the same time. The cache
659 `successorssets` when multiple calls are made at the same time. The cache
659 dictionary is updated in place. The caller is responsible for its live
660 dictionary is updated in place. The caller is responsible for its live
660 spawn. Code that makes multiple calls to `successorssets` *must* use this
661 spawn. Code that makes multiple calls to `successorssets` *must* use this
661 cache mechanism or suffer terrible performances.
662 cache mechanism or suffer terrible performances.
662
663
663 """
664 """
664
665
665 succmarkers = repo.obsstore.successors
666 succmarkers = repo.obsstore.successors
666
667
667 # Stack of nodes we search successors sets for
668 # Stack of nodes we search successors sets for
668 toproceed = [initialnode]
669 toproceed = [initialnode]
669 # set version of above list for fast loop detection
670 # set version of above list for fast loop detection
670 # element added to "toproceed" must be added here
671 # element added to "toproceed" must be added here
671 stackedset = set(toproceed)
672 stackedset = set(toproceed)
672 if cache is None:
673 if cache is None:
673 cache = {}
674 cache = {}
674
675
675 # This while loop is the flattened version of a recursive search for
676 # This while loop is the flattened version of a recursive search for
676 # successors sets
677 # successors sets
677 #
678 #
678 # def successorssets(x):
679 # def successorssets(x):
679 # successors = directsuccessors(x)
680 # successors = directsuccessors(x)
680 # ss = [[]]
681 # ss = [[]]
681 # for succ in directsuccessors(x):
682 # for succ in directsuccessors(x):
682 # # product as in itertools cartesian product
683 # # product as in itertools cartesian product
683 # ss = product(ss, successorssets(succ))
684 # ss = product(ss, successorssets(succ))
684 # return ss
685 # return ss
685 #
686 #
686 # But we can not use plain recursive calls here:
687 # But we can not use plain recursive calls here:
687 # - that would blow the python call stack
688 # - that would blow the python call stack
688 # - obsolescence markers may have cycles, we need to handle them.
689 # - obsolescence markers may have cycles, we need to handle them.
689 #
690 #
690 # The `toproceed` list act as our call stack. Every node we search
691 # The `toproceed` list act as our call stack. Every node we search
691 # successors set for are stacked there.
692 # successors set for are stacked there.
692 #
693 #
693 # The `stackedset` is set version of this stack used to check if a node is
694 # The `stackedset` is set version of this stack used to check if a node is
694 # already stacked. This check is used to detect cycles and prevent infinite
695 # already stacked. This check is used to detect cycles and prevent infinite
695 # loop.
696 # loop.
696 #
697 #
697 # successors set of all nodes are stored in the `cache` dictionary.
698 # successors set of all nodes are stored in the `cache` dictionary.
698 #
699 #
699 # After this while loop ends we use the cache to return the successors sets
700 # After this while loop ends we use the cache to return the successors sets
700 # for the node requested by the caller.
701 # for the node requested by the caller.
701 while toproceed:
702 while toproceed:
702 # Every iteration tries to compute the successors sets of the topmost
703 # Every iteration tries to compute the successors sets of the topmost
703 # node of the stack: CURRENT.
704 # node of the stack: CURRENT.
704 #
705 #
705 # There are four possible outcomes:
706 # There are four possible outcomes:
706 #
707 #
707 # 1) We already know the successors sets of CURRENT:
708 # 1) We already know the successors sets of CURRENT:
708 # -> mission accomplished, pop it from the stack.
709 # -> mission accomplished, pop it from the stack.
709 # 2) Node is not obsolete:
710 # 2) Node is not obsolete:
710 # -> the node is its own successors sets. Add it to the cache.
711 # -> the node is its own successors sets. Add it to the cache.
711 # 3) We do not know successors set of direct successors of CURRENT:
712 # 3) We do not know successors set of direct successors of CURRENT:
712 # -> We add those successors to the stack.
713 # -> We add those successors to the stack.
713 # 4) We know successors sets of all direct successors of CURRENT:
714 # 4) We know successors sets of all direct successors of CURRENT:
714 # -> We can compute CURRENT successors set and add it to the
715 # -> We can compute CURRENT successors set and add it to the
715 # cache.
716 # cache.
716 #
717 #
717 current = toproceed[-1]
718 current = toproceed[-1]
718 if current in cache:
719 if current in cache:
719 # case (1): We already know the successors sets
720 # case (1): We already know the successors sets
720 stackedset.remove(toproceed.pop())
721 stackedset.remove(toproceed.pop())
721 elif current not in succmarkers:
722 elif current not in succmarkers:
722 # case (2): The node is not obsolete.
723 # case (2): The node is not obsolete.
723 if current in repo:
724 if current in repo:
724 # We have a valid last successors.
725 # We have a valid last successors.
725 cache[current] = [(current,)]
726 cache[current] = [(current,)]
726 else:
727 else:
727 # Final obsolete version is unknown locally.
728 # Final obsolete version is unknown locally.
728 # Do not count that as a valid successors
729 # Do not count that as a valid successors
729 cache[current] = []
730 cache[current] = []
730 else:
731 else:
731 # cases (3) and (4)
732 # cases (3) and (4)
732 #
733 #
733 # We proceed in two phases. Phase 1 aims to distinguish case (3)
734 # We proceed in two phases. Phase 1 aims to distinguish case (3)
734 # from case (4):
735 # from case (4):
735 #
736 #
736 # For each direct successors of CURRENT, we check whether its
737 # For each direct successors of CURRENT, we check whether its
737 # successors sets are known. If they are not, we stack the
738 # successors sets are known. If they are not, we stack the
738 # unknown node and proceed to the next iteration of the while
739 # unknown node and proceed to the next iteration of the while
739 # loop. (case 3)
740 # loop. (case 3)
740 #
741 #
741 # During this step, we may detect obsolescence cycles: a node
742 # During this step, we may detect obsolescence cycles: a node
742 # with unknown successors sets but already in the call stack.
743 # with unknown successors sets but already in the call stack.
743 # In such a situation, we arbitrary set the successors sets of
744 # In such a situation, we arbitrary set the successors sets of
744 # the node to nothing (node pruned) to break the cycle.
745 # the node to nothing (node pruned) to break the cycle.
745 #
746 #
746 # If no break was encountered we proceed to phase 2.
747 # If no break was encountered we proceed to phase 2.
747 #
748 #
748 # Phase 2 computes successors sets of CURRENT (case 4); see details
749 # Phase 2 computes successors sets of CURRENT (case 4); see details
749 # in phase 2 itself.
750 # in phase 2 itself.
750 #
751 #
751 # Note the two levels of iteration in each phase.
752 # Note the two levels of iteration in each phase.
752 # - The first one handles obsolescence markers using CURRENT as
753 # - The first one handles obsolescence markers using CURRENT as
753 # precursor (successors markers of CURRENT).
754 # precursor (successors markers of CURRENT).
754 #
755 #
755 # Having multiple entry here means divergence.
756 # Having multiple entry here means divergence.
756 #
757 #
757 # - The second one handles successors defined in each marker.
758 # - The second one handles successors defined in each marker.
758 #
759 #
759 # Having none means pruned node, multiple successors means split,
760 # Having none means pruned node, multiple successors means split,
760 # single successors are standard replacement.
761 # single successors are standard replacement.
761 #
762 #
762 for mark in sorted(succmarkers[current]):
763 for mark in sorted(succmarkers[current]):
763 for suc in mark[1]:
764 for suc in mark[1]:
764 if suc not in cache:
765 if suc not in cache:
765 if suc in stackedset:
766 if suc in stackedset:
766 # cycle breaking
767 # cycle breaking
767 cache[suc] = []
768 cache[suc] = []
768 else:
769 else:
769 # case (3) If we have not computed successors sets
770 # case (3) If we have not computed successors sets
770 # of one of those successors we add it to the
771 # of one of those successors we add it to the
771 # `toproceed` stack and stop all work for this
772 # `toproceed` stack and stop all work for this
772 # iteration.
773 # iteration.
773 toproceed.append(suc)
774 toproceed.append(suc)
774 stackedset.add(suc)
775 stackedset.add(suc)
775 break
776 break
776 else:
777 else:
777 continue
778 continue
778 break
779 break
779 else:
780 else:
780 # case (4): we know all successors sets of all direct
781 # case (4): we know all successors sets of all direct
781 # successors
782 # successors
782 #
783 #
783 # Successors set contributed by each marker depends on the
784 # Successors set contributed by each marker depends on the
784 # successors sets of all its "successors" node.
785 # successors sets of all its "successors" node.
785 #
786 #
786 # Each different marker is a divergence in the obsolescence
787 # Each different marker is a divergence in the obsolescence
787 # history. It contributes successors sets distinct from other
788 # history. It contributes successors sets distinct from other
788 # markers.
789 # markers.
789 #
790 #
790 # Within a marker, a successor may have divergent successors
791 # Within a marker, a successor may have divergent successors
791 # sets. In such a case, the marker will contribute multiple
792 # sets. In such a case, the marker will contribute multiple
792 # divergent successors sets. If multiple successors have
793 # divergent successors sets. If multiple successors have
793 # divergent successors sets, a Cartesian product is used.
794 # divergent successors sets, a Cartesian product is used.
794 #
795 #
795 # At the end we post-process successors sets to remove
796 # At the end we post-process successors sets to remove
796 # duplicated entry and successors set that are strict subset of
797 # duplicated entry and successors set that are strict subset of
797 # another one.
798 # another one.
798 succssets = []
799 succssets = []
799 for mark in sorted(succmarkers[current]):
800 for mark in sorted(succmarkers[current]):
800 # successors sets contributed by this marker
801 # successors sets contributed by this marker
801 markss = [[]]
802 markss = [[]]
802 for suc in mark[1]:
803 for suc in mark[1]:
803 # cardinal product with previous successors
804 # cardinal product with previous successors
804 productresult = []
805 productresult = []
805 for prefix in markss:
806 for prefix in markss:
806 for suffix in cache[suc]:
807 for suffix in cache[suc]:
807 newss = list(prefix)
808 newss = list(prefix)
808 for part in suffix:
809 for part in suffix:
809 # do not duplicated entry in successors set
810 # do not duplicated entry in successors set
810 # first entry wins.
811 # first entry wins.
811 if part not in newss:
812 if part not in newss:
812 newss.append(part)
813 newss.append(part)
813 productresult.append(newss)
814 productresult.append(newss)
814 markss = productresult
815 markss = productresult
815 succssets.extend(markss)
816 succssets.extend(markss)
816 # remove duplicated and subset
817 # remove duplicated and subset
817 seen = []
818 seen = []
818 final = []
819 final = []
819 candidate = sorted(((set(s), s) for s in succssets if s),
820 candidate = sorted(((set(s), s) for s in succssets if s),
820 key=lambda x: len(x[1]), reverse=True)
821 key=lambda x: len(x[1]), reverse=True)
821 for setversion, listversion in candidate:
822 for setversion, listversion in candidate:
822 for seenset in seen:
823 for seenset in seen:
823 if setversion.issubset(seenset):
824 if setversion.issubset(seenset):
824 break
825 break
825 else:
826 else:
826 final.append(listversion)
827 final.append(listversion)
827 seen.append(setversion)
828 seen.append(setversion)
828 final.reverse() # put small successors set first
829 final.reverse() # put small successors set first
829 cache[current] = final
830 cache[current] = final
830 return cache[initialnode]
831 return cache[initialnode]
831
832
832 def _knownrevs(repo, nodes):
833 def _knownrevs(repo, nodes):
833 """yield revision numbers of known nodes passed in parameters
834 """yield revision numbers of known nodes passed in parameters
834
835
835 Unknown revisions are silently ignored."""
836 Unknown revisions are silently ignored."""
836 torev = repo.changelog.nodemap.get
837 torev = repo.changelog.nodemap.get
837 for n in nodes:
838 for n in nodes:
838 rev = torev(n)
839 rev = torev(n)
839 if rev is not None:
840 if rev is not None:
840 yield rev
841 yield rev
841
842
842 # mapping of 'set-name' -> <function to compute this set>
843 # mapping of 'set-name' -> <function to compute this set>
843 cachefuncs = {}
844 cachefuncs = {}
844 def cachefor(name):
845 def cachefor(name):
845 """Decorator to register a function as computing the cache for a set"""
846 """Decorator to register a function as computing the cache for a set"""
846 def decorator(func):
847 def decorator(func):
847 assert name not in cachefuncs
848 assert name not in cachefuncs
848 cachefuncs[name] = func
849 cachefuncs[name] = func
849 return func
850 return func
850 return decorator
851 return decorator
851
852
852 def getrevs(repo, name):
853 def getrevs(repo, name):
853 """Return the set of revision that belong to the <name> set
854 """Return the set of revision that belong to the <name> set
854
855
855 Such access may compute the set and cache it for future use"""
856 Such access may compute the set and cache it for future use"""
856 repo = repo.unfiltered()
857 repo = repo.unfiltered()
857 if not repo.obsstore:
858 if not repo.obsstore:
858 return frozenset()
859 return frozenset()
859 if name not in repo.obsstore.caches:
860 if name not in repo.obsstore.caches:
860 repo.obsstore.caches[name] = cachefuncs[name](repo)
861 repo.obsstore.caches[name] = cachefuncs[name](repo)
861 return repo.obsstore.caches[name]
862 return repo.obsstore.caches[name]
862
863
863 # To be simple we need to invalidate obsolescence cache when:
864 # To be simple we need to invalidate obsolescence cache when:
864 #
865 #
865 # - new changeset is added:
866 # - new changeset is added:
866 # - public phase is changed
867 # - public phase is changed
867 # - obsolescence marker are added
868 # - obsolescence marker are added
868 # - strip is used a repo
869 # - strip is used a repo
869 def clearobscaches(repo):
870 def clearobscaches(repo):
870 """Remove all obsolescence related cache from a repo
871 """Remove all obsolescence related cache from a repo
871
872
872 This remove all cache in obsstore is the obsstore already exist on the
873 This remove all cache in obsstore is the obsstore already exist on the
873 repo.
874 repo.
874
875
875 (We could be smarter here given the exact event that trigger the cache
876 (We could be smarter here given the exact event that trigger the cache
876 clearing)"""
877 clearing)"""
877 # only clear cache is there is obsstore data in this repo
878 # only clear cache is there is obsstore data in this repo
878 if 'obsstore' in repo._filecache:
879 if 'obsstore' in repo._filecache:
879 repo.obsstore.caches.clear()
880 repo.obsstore.caches.clear()
880
881
881 @cachefor('obsolete')
882 @cachefor('obsolete')
882 def _computeobsoleteset(repo):
883 def _computeobsoleteset(repo):
883 """the set of obsolete revisions"""
884 """the set of obsolete revisions"""
884 obs = set()
885 obs = set()
885 getrev = repo.changelog.nodemap.get
886 getrev = repo.changelog.nodemap.get
886 getphase = repo._phasecache.phase
887 getphase = repo._phasecache.phase
887 for n in repo.obsstore.successors:
888 for n in repo.obsstore.successors:
888 rev = getrev(n)
889 rev = getrev(n)
889 if rev is not None and getphase(repo, rev):
890 if rev is not None and getphase(repo, rev):
890 obs.add(rev)
891 obs.add(rev)
891 return obs
892 return obs
892
893
893 @cachefor('unstable')
894 @cachefor('unstable')
894 def _computeunstableset(repo):
895 def _computeunstableset(repo):
895 """the set of non obsolete revisions with obsolete parents"""
896 """the set of non obsolete revisions with obsolete parents"""
896 # revset is not efficient enough here
897 # revset is not efficient enough here
897 # we do (obsolete()::) - obsolete() by hand
898 # we do (obsolete()::) - obsolete() by hand
898 obs = getrevs(repo, 'obsolete')
899 obs = getrevs(repo, 'obsolete')
899 if not obs:
900 if not obs:
900 return set()
901 return set()
901 cl = repo.changelog
902 cl = repo.changelog
902 return set(r for r in cl.descendants(obs) if r not in obs)
903 return set(r for r in cl.descendants(obs) if r not in obs)
903
904
904 @cachefor('suspended')
905 @cachefor('suspended')
905 def _computesuspendedset(repo):
906 def _computesuspendedset(repo):
906 """the set of obsolete parents with non obsolete descendants"""
907 """the set of obsolete parents with non obsolete descendants"""
907 suspended = repo.changelog.ancestors(getrevs(repo, 'unstable'))
908 suspended = repo.changelog.ancestors(getrevs(repo, 'unstable'))
908 return set(r for r in getrevs(repo, 'obsolete') if r in suspended)
909 return set(r for r in getrevs(repo, 'obsolete') if r in suspended)
909
910
910 @cachefor('extinct')
911 @cachefor('extinct')
911 def _computeextinctset(repo):
912 def _computeextinctset(repo):
912 """the set of obsolete parents without non obsolete descendants"""
913 """the set of obsolete parents without non obsolete descendants"""
913 return getrevs(repo, 'obsolete') - getrevs(repo, 'suspended')
914 return getrevs(repo, 'obsolete') - getrevs(repo, 'suspended')
914
915
915
916
916 @cachefor('bumped')
917 @cachefor('bumped')
917 def _computebumpedset(repo):
918 def _computebumpedset(repo):
918 """the set of revs trying to obsolete public revisions"""
919 """the set of revs trying to obsolete public revisions"""
919 bumped = set()
920 bumped = set()
920 # util function (avoid attribute lookup in the loop)
921 # util function (avoid attribute lookup in the loop)
921 phase = repo._phasecache.phase # would be faster to grab the full list
922 phase = repo._phasecache.phase # would be faster to grab the full list
922 public = phases.public
923 public = phases.public
923 cl = repo.changelog
924 cl = repo.changelog
924 torev = cl.nodemap.get
925 torev = cl.nodemap.get
925 obs = getrevs(repo, 'obsolete')
926 obs = getrevs(repo, 'obsolete')
926 for rev in repo:
927 for rev in repo:
927 # We only evaluate mutable, non-obsolete revision
928 # We only evaluate mutable, non-obsolete revision
928 if (public < phase(repo, rev)) and (rev not in obs):
929 if (public < phase(repo, rev)) and (rev not in obs):
929 node = cl.node(rev)
930 node = cl.node(rev)
930 # (future) A cache of precursors may worth if split is very common
931 # (future) A cache of precursors may worth if split is very common
931 for pnode in allprecursors(repo.obsstore, [node],
932 for pnode in allprecursors(repo.obsstore, [node],
932 ignoreflags=bumpedfix):
933 ignoreflags=bumpedfix):
933 prev = torev(pnode) # unfiltered! but so is phasecache
934 prev = torev(pnode) # unfiltered! but so is phasecache
934 if (prev is not None) and (phase(repo, prev) <= public):
935 if (prev is not None) and (phase(repo, prev) <= public):
935 # we have a public precursors
936 # we have a public precursors
936 bumped.add(rev)
937 bumped.add(rev)
937 break # Next draft!
938 break # Next draft!
938 return bumped
939 return bumped
939
940
940 @cachefor('divergent')
941 @cachefor('divergent')
941 def _computedivergentset(repo):
942 def _computedivergentset(repo):
942 """the set of rev that compete to be the final successors of some revision.
943 """the set of rev that compete to be the final successors of some revision.
943 """
944 """
944 divergent = set()
945 divergent = set()
945 obsstore = repo.obsstore
946 obsstore = repo.obsstore
946 newermap = {}
947 newermap = {}
947 for ctx in repo.set('(not public()) - obsolete()'):
948 for ctx in repo.set('(not public()) - obsolete()'):
948 mark = obsstore.precursors.get(ctx.node(), ())
949 mark = obsstore.precursors.get(ctx.node(), ())
949 toprocess = set(mark)
950 toprocess = set(mark)
950 while toprocess:
951 while toprocess:
951 prec = toprocess.pop()[0]
952 prec = toprocess.pop()[0]
952 if prec not in newermap:
953 if prec not in newermap:
953 successorssets(repo, prec, newermap)
954 successorssets(repo, prec, newermap)
954 newer = [n for n in newermap[prec] if n]
955 newer = [n for n in newermap[prec] if n]
955 if len(newer) > 1:
956 if len(newer) > 1:
956 divergent.add(ctx.rev())
957 divergent.add(ctx.rev())
957 break
958 break
958 toprocess.update(obsstore.precursors.get(prec, ()))
959 toprocess.update(obsstore.precursors.get(prec, ()))
959 return divergent
960 return divergent
960
961
961
962
962 def createmarkers(repo, relations, flag=0, date=None, metadata=None):
963 def createmarkers(repo, relations, flag=0, date=None, metadata=None):
963 """Add obsolete markers between changesets in a repo
964 """Add obsolete markers between changesets in a repo
964
965
965 <relations> must be an iterable of (<old>, (<new>, ...)[,{metadata}])
966 <relations> must be an iterable of (<old>, (<new>, ...)[,{metadata}])
966 tuple. `old` and `news` are changectx. metadata is an optional dictionary
967 tuple. `old` and `news` are changectx. metadata is an optional dictionary
967 containing metadata for this marker only. It is merged with the global
968 containing metadata for this marker only. It is merged with the global
968 metadata specified through the `metadata` argument of this function,
969 metadata specified through the `metadata` argument of this function,
969
970
970 Trying to obsolete a public changeset will raise an exception.
971 Trying to obsolete a public changeset will raise an exception.
971
972
972 Current user and date are used except if specified otherwise in the
973 Current user and date are used except if specified otherwise in the
973 metadata attribute.
974 metadata attribute.
974
975
975 This function operates within a transaction of its own, but does
976 This function operates within a transaction of its own, but does
976 not take any lock on the repo.
977 not take any lock on the repo.
977 """
978 """
978 # prepare metadata
979 # prepare metadata
979 if metadata is None:
980 if metadata is None:
980 metadata = {}
981 metadata = {}
981 if 'user' not in metadata:
982 if 'user' not in metadata:
982 metadata['user'] = repo.ui.username()
983 metadata['user'] = repo.ui.username()
983 tr = repo.transaction('add-obsolescence-marker')
984 tr = repo.transaction('add-obsolescence-marker')
984 try:
985 try:
985 for rel in relations:
986 for rel in relations:
986 prec = rel[0]
987 prec = rel[0]
987 sucs = rel[1]
988 sucs = rel[1]
988 localmetadata = metadata.copy()
989 localmetadata = metadata.copy()
989 if 2 < len(rel):
990 if 2 < len(rel):
990 localmetadata.update(rel[2])
991 localmetadata.update(rel[2])
991
992
992 if not prec.mutable():
993 if not prec.mutable():
993 raise util.Abort("cannot obsolete immutable changeset: %s"
994 raise util.Abort("cannot obsolete immutable changeset: %s"
994 % prec)
995 % prec)
995 nprec = prec.node()
996 nprec = prec.node()
996 nsucs = tuple(s.node() for s in sucs)
997 nsucs = tuple(s.node() for s in sucs)
997 npare = None
998 npare = None
998 if not nsucs:
999 if not nsucs:
999 npare = tuple(p.node() for p in prec.parents())
1000 npare = tuple(p.node() for p in prec.parents())
1000 if nprec in nsucs:
1001 if nprec in nsucs:
1001 raise util.Abort("changeset %s cannot obsolete itself" % prec)
1002 raise util.Abort("changeset %s cannot obsolete itself" % prec)
1002 repo.obsstore.create(tr, nprec, nsucs, flag, parents=npare,
1003 repo.obsstore.create(tr, nprec, nsucs, flag, parents=npare,
1003 date=date, metadata=localmetadata)
1004 date=date, metadata=localmetadata)
1004 repo.filteredrevcache.clear()
1005 repo.filteredrevcache.clear()
1005 tr.close()
1006 tr.close()
1006 finally:
1007 finally:
1007 tr.release()
1008 tr.release()
General Comments 0
You need to be logged in to leave comments. Login now