##// END OF EJS Templates
obsolete: invalidate "volatile" set cache after merging marker...
marmoute -
r32314:99515353 stable
parent child Browse files
Show More
@@ -1,1672 +1,1673 b''
1 # bundle2.py - generic container format to transmit arbitrary data.
1 # bundle2.py - generic container format to transmit arbitrary data.
2 #
2 #
3 # Copyright 2013 Facebook, Inc.
3 # Copyright 2013 Facebook, Inc.
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7 """Handling of the new bundle2 format
7 """Handling of the new bundle2 format
8
8
9 The goal of bundle2 is to act as an atomically packet to transmit a set of
9 The goal of bundle2 is to act as an atomically packet to transmit a set of
10 payloads in an application agnostic way. It consist in a sequence of "parts"
10 payloads in an application agnostic way. It consist in a sequence of "parts"
11 that will be handed to and processed by the application layer.
11 that will be handed to and processed by the application layer.
12
12
13
13
14 General format architecture
14 General format architecture
15 ===========================
15 ===========================
16
16
17 The format is architectured as follow
17 The format is architectured as follow
18
18
19 - magic string
19 - magic string
20 - stream level parameters
20 - stream level parameters
21 - payload parts (any number)
21 - payload parts (any number)
22 - end of stream marker.
22 - end of stream marker.
23
23
24 the Binary format
24 the Binary format
25 ============================
25 ============================
26
26
27 All numbers are unsigned and big-endian.
27 All numbers are unsigned and big-endian.
28
28
29 stream level parameters
29 stream level parameters
30 ------------------------
30 ------------------------
31
31
32 Binary format is as follow
32 Binary format is as follow
33
33
34 :params size: int32
34 :params size: int32
35
35
36 The total number of Bytes used by the parameters
36 The total number of Bytes used by the parameters
37
37
38 :params value: arbitrary number of Bytes
38 :params value: arbitrary number of Bytes
39
39
40 A blob of `params size` containing the serialized version of all stream level
40 A blob of `params size` containing the serialized version of all stream level
41 parameters.
41 parameters.
42
42
43 The blob contains a space separated list of parameters. Parameters with value
43 The blob contains a space separated list of parameters. Parameters with value
44 are stored in the form `<name>=<value>`. Both name and value are urlquoted.
44 are stored in the form `<name>=<value>`. Both name and value are urlquoted.
45
45
46 Empty name are obviously forbidden.
46 Empty name are obviously forbidden.
47
47
48 Name MUST start with a letter. If this first letter is lower case, the
48 Name MUST start with a letter. If this first letter is lower case, the
49 parameter is advisory and can be safely ignored. However when the first
49 parameter is advisory and can be safely ignored. However when the first
50 letter is capital, the parameter is mandatory and the bundling process MUST
50 letter is capital, the parameter is mandatory and the bundling process MUST
51 stop if he is not able to proceed it.
51 stop if he is not able to proceed it.
52
52
53 Stream parameters use a simple textual format for two main reasons:
53 Stream parameters use a simple textual format for two main reasons:
54
54
55 - Stream level parameters should remain simple and we want to discourage any
55 - Stream level parameters should remain simple and we want to discourage any
56 crazy usage.
56 crazy usage.
57 - Textual data allow easy human inspection of a bundle2 header in case of
57 - Textual data allow easy human inspection of a bundle2 header in case of
58 troubles.
58 troubles.
59
59
60 Any Applicative level options MUST go into a bundle2 part instead.
60 Any Applicative level options MUST go into a bundle2 part instead.
61
61
62 Payload part
62 Payload part
63 ------------------------
63 ------------------------
64
64
65 Binary format is as follow
65 Binary format is as follow
66
66
67 :header size: int32
67 :header size: int32
68
68
69 The total number of Bytes used by the part header. When the header is empty
69 The total number of Bytes used by the part header. When the header is empty
70 (size = 0) this is interpreted as the end of stream marker.
70 (size = 0) this is interpreted as the end of stream marker.
71
71
72 :header:
72 :header:
73
73
74 The header defines how to interpret the part. It contains two piece of
74 The header defines how to interpret the part. It contains two piece of
75 data: the part type, and the part parameters.
75 data: the part type, and the part parameters.
76
76
77 The part type is used to route an application level handler, that can
77 The part type is used to route an application level handler, that can
78 interpret payload.
78 interpret payload.
79
79
80 Part parameters are passed to the application level handler. They are
80 Part parameters are passed to the application level handler. They are
81 meant to convey information that will help the application level object to
81 meant to convey information that will help the application level object to
82 interpret the part payload.
82 interpret the part payload.
83
83
84 The binary format of the header is has follow
84 The binary format of the header is has follow
85
85
86 :typesize: (one byte)
86 :typesize: (one byte)
87
87
88 :parttype: alphanumerical part name (restricted to [a-zA-Z0-9_:-]*)
88 :parttype: alphanumerical part name (restricted to [a-zA-Z0-9_:-]*)
89
89
90 :partid: A 32bits integer (unique in the bundle) that can be used to refer
90 :partid: A 32bits integer (unique in the bundle) that can be used to refer
91 to this part.
91 to this part.
92
92
93 :parameters:
93 :parameters:
94
94
95 Part's parameter may have arbitrary content, the binary structure is::
95 Part's parameter may have arbitrary content, the binary structure is::
96
96
97 <mandatory-count><advisory-count><param-sizes><param-data>
97 <mandatory-count><advisory-count><param-sizes><param-data>
98
98
99 :mandatory-count: 1 byte, number of mandatory parameters
99 :mandatory-count: 1 byte, number of mandatory parameters
100
100
101 :advisory-count: 1 byte, number of advisory parameters
101 :advisory-count: 1 byte, number of advisory parameters
102
102
103 :param-sizes:
103 :param-sizes:
104
104
105 N couple of bytes, where N is the total number of parameters. Each
105 N couple of bytes, where N is the total number of parameters. Each
106 couple contains (<size-of-key>, <size-of-value) for one parameter.
106 couple contains (<size-of-key>, <size-of-value) for one parameter.
107
107
108 :param-data:
108 :param-data:
109
109
110 A blob of bytes from which each parameter key and value can be
110 A blob of bytes from which each parameter key and value can be
111 retrieved using the list of size couples stored in the previous
111 retrieved using the list of size couples stored in the previous
112 field.
112 field.
113
113
114 Mandatory parameters comes first, then the advisory ones.
114 Mandatory parameters comes first, then the advisory ones.
115
115
116 Each parameter's key MUST be unique within the part.
116 Each parameter's key MUST be unique within the part.
117
117
118 :payload:
118 :payload:
119
119
120 payload is a series of `<chunksize><chunkdata>`.
120 payload is a series of `<chunksize><chunkdata>`.
121
121
122 `chunksize` is an int32, `chunkdata` are plain bytes (as much as
122 `chunksize` is an int32, `chunkdata` are plain bytes (as much as
123 `chunksize` says)` The payload part is concluded by a zero size chunk.
123 `chunksize` says)` The payload part is concluded by a zero size chunk.
124
124
125 The current implementation always produces either zero or one chunk.
125 The current implementation always produces either zero or one chunk.
126 This is an implementation limitation that will ultimately be lifted.
126 This is an implementation limitation that will ultimately be lifted.
127
127
128 `chunksize` can be negative to trigger special case processing. No such
128 `chunksize` can be negative to trigger special case processing. No such
129 processing is in place yet.
129 processing is in place yet.
130
130
131 Bundle processing
131 Bundle processing
132 ============================
132 ============================
133
133
134 Each part is processed in order using a "part handler". Handler are registered
134 Each part is processed in order using a "part handler". Handler are registered
135 for a certain part type.
135 for a certain part type.
136
136
137 The matching of a part to its handler is case insensitive. The case of the
137 The matching of a part to its handler is case insensitive. The case of the
138 part type is used to know if a part is mandatory or advisory. If the Part type
138 part type is used to know if a part is mandatory or advisory. If the Part type
139 contains any uppercase char it is considered mandatory. When no handler is
139 contains any uppercase char it is considered mandatory. When no handler is
140 known for a Mandatory part, the process is aborted and an exception is raised.
140 known for a Mandatory part, the process is aborted and an exception is raised.
141 If the part is advisory and no handler is known, the part is ignored. When the
141 If the part is advisory and no handler is known, the part is ignored. When the
142 process is aborted, the full bundle is still read from the stream to keep the
142 process is aborted, the full bundle is still read from the stream to keep the
143 channel usable. But none of the part read from an abort are processed. In the
143 channel usable. But none of the part read from an abort are processed. In the
144 future, dropping the stream may become an option for channel we do not care to
144 future, dropping the stream may become an option for channel we do not care to
145 preserve.
145 preserve.
146 """
146 """
147
147
148 from __future__ import absolute_import
148 from __future__ import absolute_import
149
149
150 import errno
150 import errno
151 import re
151 import re
152 import string
152 import string
153 import struct
153 import struct
154 import sys
154 import sys
155
155
156 from .i18n import _
156 from .i18n import _
157 from . import (
157 from . import (
158 changegroup,
158 changegroup,
159 error,
159 error,
160 obsolete,
160 obsolete,
161 pushkey,
161 pushkey,
162 pycompat,
162 pycompat,
163 tags,
163 tags,
164 url,
164 url,
165 util,
165 util,
166 )
166 )
167
167
168 urlerr = util.urlerr
168 urlerr = util.urlerr
169 urlreq = util.urlreq
169 urlreq = util.urlreq
170
170
171 _pack = struct.pack
171 _pack = struct.pack
172 _unpack = struct.unpack
172 _unpack = struct.unpack
173
173
174 _fstreamparamsize = '>i'
174 _fstreamparamsize = '>i'
175 _fpartheadersize = '>i'
175 _fpartheadersize = '>i'
176 _fparttypesize = '>B'
176 _fparttypesize = '>B'
177 _fpartid = '>I'
177 _fpartid = '>I'
178 _fpayloadsize = '>i'
178 _fpayloadsize = '>i'
179 _fpartparamcount = '>BB'
179 _fpartparamcount = '>BB'
180
180
181 preferedchunksize = 4096
181 preferedchunksize = 4096
182
182
183 _parttypeforbidden = re.compile('[^a-zA-Z0-9_:-]')
183 _parttypeforbidden = re.compile('[^a-zA-Z0-9_:-]')
184
184
185 def outdebug(ui, message):
185 def outdebug(ui, message):
186 """debug regarding output stream (bundling)"""
186 """debug regarding output stream (bundling)"""
187 if ui.configbool('devel', 'bundle2.debug', False):
187 if ui.configbool('devel', 'bundle2.debug', False):
188 ui.debug('bundle2-output: %s\n' % message)
188 ui.debug('bundle2-output: %s\n' % message)
189
189
190 def indebug(ui, message):
190 def indebug(ui, message):
191 """debug on input stream (unbundling)"""
191 """debug on input stream (unbundling)"""
192 if ui.configbool('devel', 'bundle2.debug', False):
192 if ui.configbool('devel', 'bundle2.debug', False):
193 ui.debug('bundle2-input: %s\n' % message)
193 ui.debug('bundle2-input: %s\n' % message)
194
194
195 def validateparttype(parttype):
195 def validateparttype(parttype):
196 """raise ValueError if a parttype contains invalid character"""
196 """raise ValueError if a parttype contains invalid character"""
197 if _parttypeforbidden.search(parttype):
197 if _parttypeforbidden.search(parttype):
198 raise ValueError(parttype)
198 raise ValueError(parttype)
199
199
200 def _makefpartparamsizes(nbparams):
200 def _makefpartparamsizes(nbparams):
201 """return a struct format to read part parameter sizes
201 """return a struct format to read part parameter sizes
202
202
203 The number parameters is variable so we need to build that format
203 The number parameters is variable so we need to build that format
204 dynamically.
204 dynamically.
205 """
205 """
206 return '>'+('BB'*nbparams)
206 return '>'+('BB'*nbparams)
207
207
208 parthandlermapping = {}
208 parthandlermapping = {}
209
209
210 def parthandler(parttype, params=()):
210 def parthandler(parttype, params=()):
211 """decorator that register a function as a bundle2 part handler
211 """decorator that register a function as a bundle2 part handler
212
212
213 eg::
213 eg::
214
214
215 @parthandler('myparttype', ('mandatory', 'param', 'handled'))
215 @parthandler('myparttype', ('mandatory', 'param', 'handled'))
216 def myparttypehandler(...):
216 def myparttypehandler(...):
217 '''process a part of type "my part".'''
217 '''process a part of type "my part".'''
218 ...
218 ...
219 """
219 """
220 validateparttype(parttype)
220 validateparttype(parttype)
221 def _decorator(func):
221 def _decorator(func):
222 lparttype = parttype.lower() # enforce lower case matching.
222 lparttype = parttype.lower() # enforce lower case matching.
223 assert lparttype not in parthandlermapping
223 assert lparttype not in parthandlermapping
224 parthandlermapping[lparttype] = func
224 parthandlermapping[lparttype] = func
225 func.params = frozenset(params)
225 func.params = frozenset(params)
226 return func
226 return func
227 return _decorator
227 return _decorator
228
228
229 class unbundlerecords(object):
229 class unbundlerecords(object):
230 """keep record of what happens during and unbundle
230 """keep record of what happens during and unbundle
231
231
232 New records are added using `records.add('cat', obj)`. Where 'cat' is a
232 New records are added using `records.add('cat', obj)`. Where 'cat' is a
233 category of record and obj is an arbitrary object.
233 category of record and obj is an arbitrary object.
234
234
235 `records['cat']` will return all entries of this category 'cat'.
235 `records['cat']` will return all entries of this category 'cat'.
236
236
237 Iterating on the object itself will yield `('category', obj)` tuples
237 Iterating on the object itself will yield `('category', obj)` tuples
238 for all entries.
238 for all entries.
239
239
240 All iterations happens in chronological order.
240 All iterations happens in chronological order.
241 """
241 """
242
242
243 def __init__(self):
243 def __init__(self):
244 self._categories = {}
244 self._categories = {}
245 self._sequences = []
245 self._sequences = []
246 self._replies = {}
246 self._replies = {}
247
247
248 def add(self, category, entry, inreplyto=None):
248 def add(self, category, entry, inreplyto=None):
249 """add a new record of a given category.
249 """add a new record of a given category.
250
250
251 The entry can then be retrieved in the list returned by
251 The entry can then be retrieved in the list returned by
252 self['category']."""
252 self['category']."""
253 self._categories.setdefault(category, []).append(entry)
253 self._categories.setdefault(category, []).append(entry)
254 self._sequences.append((category, entry))
254 self._sequences.append((category, entry))
255 if inreplyto is not None:
255 if inreplyto is not None:
256 self.getreplies(inreplyto).add(category, entry)
256 self.getreplies(inreplyto).add(category, entry)
257
257
258 def getreplies(self, partid):
258 def getreplies(self, partid):
259 """get the records that are replies to a specific part"""
259 """get the records that are replies to a specific part"""
260 return self._replies.setdefault(partid, unbundlerecords())
260 return self._replies.setdefault(partid, unbundlerecords())
261
261
262 def __getitem__(self, cat):
262 def __getitem__(self, cat):
263 return tuple(self._categories.get(cat, ()))
263 return tuple(self._categories.get(cat, ()))
264
264
265 def __iter__(self):
265 def __iter__(self):
266 return iter(self._sequences)
266 return iter(self._sequences)
267
267
268 def __len__(self):
268 def __len__(self):
269 return len(self._sequences)
269 return len(self._sequences)
270
270
271 def __nonzero__(self):
271 def __nonzero__(self):
272 return bool(self._sequences)
272 return bool(self._sequences)
273
273
274 __bool__ = __nonzero__
274 __bool__ = __nonzero__
275
275
276 class bundleoperation(object):
276 class bundleoperation(object):
277 """an object that represents a single bundling process
277 """an object that represents a single bundling process
278
278
279 Its purpose is to carry unbundle-related objects and states.
279 Its purpose is to carry unbundle-related objects and states.
280
280
281 A new object should be created at the beginning of each bundle processing.
281 A new object should be created at the beginning of each bundle processing.
282 The object is to be returned by the processing function.
282 The object is to be returned by the processing function.
283
283
284 The object has very little content now it will ultimately contain:
284 The object has very little content now it will ultimately contain:
285 * an access to the repo the bundle is applied to,
285 * an access to the repo the bundle is applied to,
286 * a ui object,
286 * a ui object,
287 * a way to retrieve a transaction to add changes to the repo,
287 * a way to retrieve a transaction to add changes to the repo,
288 * a way to record the result of processing each part,
288 * a way to record the result of processing each part,
289 * a way to construct a bundle response when applicable.
289 * a way to construct a bundle response when applicable.
290 """
290 """
291
291
292 def __init__(self, repo, transactiongetter, captureoutput=True):
292 def __init__(self, repo, transactiongetter, captureoutput=True):
293 self.repo = repo
293 self.repo = repo
294 self.ui = repo.ui
294 self.ui = repo.ui
295 self.records = unbundlerecords()
295 self.records = unbundlerecords()
296 self.gettransaction = transactiongetter
296 self.gettransaction = transactiongetter
297 self.reply = None
297 self.reply = None
298 self.captureoutput = captureoutput
298 self.captureoutput = captureoutput
299
299
300 class TransactionUnavailable(RuntimeError):
300 class TransactionUnavailable(RuntimeError):
301 pass
301 pass
302
302
303 def _notransaction():
303 def _notransaction():
304 """default method to get a transaction while processing a bundle
304 """default method to get a transaction while processing a bundle
305
305
306 Raise an exception to highlight the fact that no transaction was expected
306 Raise an exception to highlight the fact that no transaction was expected
307 to be created"""
307 to be created"""
308 raise TransactionUnavailable()
308 raise TransactionUnavailable()
309
309
310 def applybundle(repo, unbundler, tr, source=None, url=None, op=None):
310 def applybundle(repo, unbundler, tr, source=None, url=None, op=None):
311 # transform me into unbundler.apply() as soon as the freeze is lifted
311 # transform me into unbundler.apply() as soon as the freeze is lifted
312 tr.hookargs['bundle2'] = '1'
312 tr.hookargs['bundle2'] = '1'
313 if source is not None and 'source' not in tr.hookargs:
313 if source is not None and 'source' not in tr.hookargs:
314 tr.hookargs['source'] = source
314 tr.hookargs['source'] = source
315 if url is not None and 'url' not in tr.hookargs:
315 if url is not None and 'url' not in tr.hookargs:
316 tr.hookargs['url'] = url
316 tr.hookargs['url'] = url
317 return processbundle(repo, unbundler, lambda: tr, op=op)
317 return processbundle(repo, unbundler, lambda: tr, op=op)
318
318
319 def processbundle(repo, unbundler, transactiongetter=None, op=None):
319 def processbundle(repo, unbundler, transactiongetter=None, op=None):
320 """This function process a bundle, apply effect to/from a repo
320 """This function process a bundle, apply effect to/from a repo
321
321
322 It iterates over each part then searches for and uses the proper handling
322 It iterates over each part then searches for and uses the proper handling
323 code to process the part. Parts are processed in order.
323 code to process the part. Parts are processed in order.
324
324
325 Unknown Mandatory part will abort the process.
325 Unknown Mandatory part will abort the process.
326
326
327 It is temporarily possible to provide a prebuilt bundleoperation to the
327 It is temporarily possible to provide a prebuilt bundleoperation to the
328 function. This is used to ensure output is properly propagated in case of
328 function. This is used to ensure output is properly propagated in case of
329 an error during the unbundling. This output capturing part will likely be
329 an error during the unbundling. This output capturing part will likely be
330 reworked and this ability will probably go away in the process.
330 reworked and this ability will probably go away in the process.
331 """
331 """
332 if op is None:
332 if op is None:
333 if transactiongetter is None:
333 if transactiongetter is None:
334 transactiongetter = _notransaction
334 transactiongetter = _notransaction
335 op = bundleoperation(repo, transactiongetter)
335 op = bundleoperation(repo, transactiongetter)
336 # todo:
336 # todo:
337 # - replace this is a init function soon.
337 # - replace this is a init function soon.
338 # - exception catching
338 # - exception catching
339 unbundler.params
339 unbundler.params
340 if repo.ui.debugflag:
340 if repo.ui.debugflag:
341 msg = ['bundle2-input-bundle:']
341 msg = ['bundle2-input-bundle:']
342 if unbundler.params:
342 if unbundler.params:
343 msg.append(' %i params')
343 msg.append(' %i params')
344 if op.gettransaction is None:
344 if op.gettransaction is None:
345 msg.append(' no-transaction')
345 msg.append(' no-transaction')
346 else:
346 else:
347 msg.append(' with-transaction')
347 msg.append(' with-transaction')
348 msg.append('\n')
348 msg.append('\n')
349 repo.ui.debug(''.join(msg))
349 repo.ui.debug(''.join(msg))
350 iterparts = enumerate(unbundler.iterparts())
350 iterparts = enumerate(unbundler.iterparts())
351 part = None
351 part = None
352 nbpart = 0
352 nbpart = 0
353 try:
353 try:
354 for nbpart, part in iterparts:
354 for nbpart, part in iterparts:
355 _processpart(op, part)
355 _processpart(op, part)
356 except Exception as exc:
356 except Exception as exc:
357 # Any exceptions seeking to the end of the bundle at this point are
357 # Any exceptions seeking to the end of the bundle at this point are
358 # almost certainly related to the underlying stream being bad.
358 # almost certainly related to the underlying stream being bad.
359 # And, chances are that the exception we're handling is related to
359 # And, chances are that the exception we're handling is related to
360 # getting in that bad state. So, we swallow the seeking error and
360 # getting in that bad state. So, we swallow the seeking error and
361 # re-raise the original error.
361 # re-raise the original error.
362 seekerror = False
362 seekerror = False
363 try:
363 try:
364 for nbpart, part in iterparts:
364 for nbpart, part in iterparts:
365 # consume the bundle content
365 # consume the bundle content
366 part.seek(0, 2)
366 part.seek(0, 2)
367 except Exception:
367 except Exception:
368 seekerror = True
368 seekerror = True
369
369
370 # Small hack to let caller code distinguish exceptions from bundle2
370 # Small hack to let caller code distinguish exceptions from bundle2
371 # processing from processing the old format. This is mostly
371 # processing from processing the old format. This is mostly
372 # needed to handle different return codes to unbundle according to the
372 # needed to handle different return codes to unbundle according to the
373 # type of bundle. We should probably clean up or drop this return code
373 # type of bundle. We should probably clean up or drop this return code
374 # craziness in a future version.
374 # craziness in a future version.
375 exc.duringunbundle2 = True
375 exc.duringunbundle2 = True
376 salvaged = []
376 salvaged = []
377 replycaps = None
377 replycaps = None
378 if op.reply is not None:
378 if op.reply is not None:
379 salvaged = op.reply.salvageoutput()
379 salvaged = op.reply.salvageoutput()
380 replycaps = op.reply.capabilities
380 replycaps = op.reply.capabilities
381 exc._replycaps = replycaps
381 exc._replycaps = replycaps
382 exc._bundle2salvagedoutput = salvaged
382 exc._bundle2salvagedoutput = salvaged
383
383
384 # Re-raising from a variable loses the original stack. So only use
384 # Re-raising from a variable loses the original stack. So only use
385 # that form if we need to.
385 # that form if we need to.
386 if seekerror:
386 if seekerror:
387 raise exc
387 raise exc
388 else:
388 else:
389 raise
389 raise
390 finally:
390 finally:
391 repo.ui.debug('bundle2-input-bundle: %i parts total\n' % nbpart)
391 repo.ui.debug('bundle2-input-bundle: %i parts total\n' % nbpart)
392
392
393 return op
393 return op
394
394
395 def _processpart(op, part):
395 def _processpart(op, part):
396 """process a single part from a bundle
396 """process a single part from a bundle
397
397
398 The part is guaranteed to have been fully consumed when the function exits
398 The part is guaranteed to have been fully consumed when the function exits
399 (even if an exception is raised)."""
399 (even if an exception is raised)."""
400 status = 'unknown' # used by debug output
400 status = 'unknown' # used by debug output
401 hardabort = False
401 hardabort = False
402 try:
402 try:
403 try:
403 try:
404 handler = parthandlermapping.get(part.type)
404 handler = parthandlermapping.get(part.type)
405 if handler is None:
405 if handler is None:
406 status = 'unsupported-type'
406 status = 'unsupported-type'
407 raise error.BundleUnknownFeatureError(parttype=part.type)
407 raise error.BundleUnknownFeatureError(parttype=part.type)
408 indebug(op.ui, 'found a handler for part %r' % part.type)
408 indebug(op.ui, 'found a handler for part %r' % part.type)
409 unknownparams = part.mandatorykeys - handler.params
409 unknownparams = part.mandatorykeys - handler.params
410 if unknownparams:
410 if unknownparams:
411 unknownparams = list(unknownparams)
411 unknownparams = list(unknownparams)
412 unknownparams.sort()
412 unknownparams.sort()
413 status = 'unsupported-params (%s)' % unknownparams
413 status = 'unsupported-params (%s)' % unknownparams
414 raise error.BundleUnknownFeatureError(parttype=part.type,
414 raise error.BundleUnknownFeatureError(parttype=part.type,
415 params=unknownparams)
415 params=unknownparams)
416 status = 'supported'
416 status = 'supported'
417 except error.BundleUnknownFeatureError as exc:
417 except error.BundleUnknownFeatureError as exc:
418 if part.mandatory: # mandatory parts
418 if part.mandatory: # mandatory parts
419 raise
419 raise
420 indebug(op.ui, 'ignoring unsupported advisory part %s' % exc)
420 indebug(op.ui, 'ignoring unsupported advisory part %s' % exc)
421 return # skip to part processing
421 return # skip to part processing
422 finally:
422 finally:
423 if op.ui.debugflag:
423 if op.ui.debugflag:
424 msg = ['bundle2-input-part: "%s"' % part.type]
424 msg = ['bundle2-input-part: "%s"' % part.type]
425 if not part.mandatory:
425 if not part.mandatory:
426 msg.append(' (advisory)')
426 msg.append(' (advisory)')
427 nbmp = len(part.mandatorykeys)
427 nbmp = len(part.mandatorykeys)
428 nbap = len(part.params) - nbmp
428 nbap = len(part.params) - nbmp
429 if nbmp or nbap:
429 if nbmp or nbap:
430 msg.append(' (params:')
430 msg.append(' (params:')
431 if nbmp:
431 if nbmp:
432 msg.append(' %i mandatory' % nbmp)
432 msg.append(' %i mandatory' % nbmp)
433 if nbap:
433 if nbap:
434 msg.append(' %i advisory' % nbmp)
434 msg.append(' %i advisory' % nbmp)
435 msg.append(')')
435 msg.append(')')
436 msg.append(' %s\n' % status)
436 msg.append(' %s\n' % status)
437 op.ui.debug(''.join(msg))
437 op.ui.debug(''.join(msg))
438
438
439 # handler is called outside the above try block so that we don't
439 # handler is called outside the above try block so that we don't
440 # risk catching KeyErrors from anything other than the
440 # risk catching KeyErrors from anything other than the
441 # parthandlermapping lookup (any KeyError raised by handler()
441 # parthandlermapping lookup (any KeyError raised by handler()
442 # itself represents a defect of a different variety).
442 # itself represents a defect of a different variety).
443 output = None
443 output = None
444 if op.captureoutput and op.reply is not None:
444 if op.captureoutput and op.reply is not None:
445 op.ui.pushbuffer(error=True, subproc=True)
445 op.ui.pushbuffer(error=True, subproc=True)
446 output = ''
446 output = ''
447 try:
447 try:
448 handler(op, part)
448 handler(op, part)
449 finally:
449 finally:
450 if output is not None:
450 if output is not None:
451 output = op.ui.popbuffer()
451 output = op.ui.popbuffer()
452 if output:
452 if output:
453 outpart = op.reply.newpart('output', data=output,
453 outpart = op.reply.newpart('output', data=output,
454 mandatory=False)
454 mandatory=False)
455 outpart.addparam('in-reply-to', str(part.id), mandatory=False)
455 outpart.addparam('in-reply-to', str(part.id), mandatory=False)
456 # If exiting or interrupted, do not attempt to seek the stream in the
456 # If exiting or interrupted, do not attempt to seek the stream in the
457 # finally block below. This makes abort faster.
457 # finally block below. This makes abort faster.
458 except (SystemExit, KeyboardInterrupt):
458 except (SystemExit, KeyboardInterrupt):
459 hardabort = True
459 hardabort = True
460 raise
460 raise
461 finally:
461 finally:
462 # consume the part content to not corrupt the stream.
462 # consume the part content to not corrupt the stream.
463 if not hardabort:
463 if not hardabort:
464 part.seek(0, 2)
464 part.seek(0, 2)
465
465
466
466
467 def decodecaps(blob):
467 def decodecaps(blob):
468 """decode a bundle2 caps bytes blob into a dictionary
468 """decode a bundle2 caps bytes blob into a dictionary
469
469
470 The blob is a list of capabilities (one per line)
470 The blob is a list of capabilities (one per line)
471 Capabilities may have values using a line of the form::
471 Capabilities may have values using a line of the form::
472
472
473 capability=value1,value2,value3
473 capability=value1,value2,value3
474
474
475 The values are always a list."""
475 The values are always a list."""
476 caps = {}
476 caps = {}
477 for line in blob.splitlines():
477 for line in blob.splitlines():
478 if not line:
478 if not line:
479 continue
479 continue
480 if '=' not in line:
480 if '=' not in line:
481 key, vals = line, ()
481 key, vals = line, ()
482 else:
482 else:
483 key, vals = line.split('=', 1)
483 key, vals = line.split('=', 1)
484 vals = vals.split(',')
484 vals = vals.split(',')
485 key = urlreq.unquote(key)
485 key = urlreq.unquote(key)
486 vals = [urlreq.unquote(v) for v in vals]
486 vals = [urlreq.unquote(v) for v in vals]
487 caps[key] = vals
487 caps[key] = vals
488 return caps
488 return caps
489
489
490 def encodecaps(caps):
490 def encodecaps(caps):
491 """encode a bundle2 caps dictionary into a bytes blob"""
491 """encode a bundle2 caps dictionary into a bytes blob"""
492 chunks = []
492 chunks = []
493 for ca in sorted(caps):
493 for ca in sorted(caps):
494 vals = caps[ca]
494 vals = caps[ca]
495 ca = urlreq.quote(ca)
495 ca = urlreq.quote(ca)
496 vals = [urlreq.quote(v) for v in vals]
496 vals = [urlreq.quote(v) for v in vals]
497 if vals:
497 if vals:
498 ca = "%s=%s" % (ca, ','.join(vals))
498 ca = "%s=%s" % (ca, ','.join(vals))
499 chunks.append(ca)
499 chunks.append(ca)
500 return '\n'.join(chunks)
500 return '\n'.join(chunks)
501
501
502 bundletypes = {
502 bundletypes = {
503 "": ("", 'UN'), # only when using unbundle on ssh and old http servers
503 "": ("", 'UN'), # only when using unbundle on ssh and old http servers
504 # since the unification ssh accepts a header but there
504 # since the unification ssh accepts a header but there
505 # is no capability signaling it.
505 # is no capability signaling it.
506 "HG20": (), # special-cased below
506 "HG20": (), # special-cased below
507 "HG10UN": ("HG10UN", 'UN'),
507 "HG10UN": ("HG10UN", 'UN'),
508 "HG10BZ": ("HG10", 'BZ'),
508 "HG10BZ": ("HG10", 'BZ'),
509 "HG10GZ": ("HG10GZ", 'GZ'),
509 "HG10GZ": ("HG10GZ", 'GZ'),
510 }
510 }
511
511
512 # hgweb uses this list to communicate its preferred type
512 # hgweb uses this list to communicate its preferred type
513 bundlepriority = ['HG10GZ', 'HG10BZ', 'HG10UN']
513 bundlepriority = ['HG10GZ', 'HG10BZ', 'HG10UN']
514
514
515 class bundle20(object):
515 class bundle20(object):
516 """represent an outgoing bundle2 container
516 """represent an outgoing bundle2 container
517
517
518 Use the `addparam` method to add stream level parameter. and `newpart` to
518 Use the `addparam` method to add stream level parameter. and `newpart` to
519 populate it. Then call `getchunks` to retrieve all the binary chunks of
519 populate it. Then call `getchunks` to retrieve all the binary chunks of
520 data that compose the bundle2 container."""
520 data that compose the bundle2 container."""
521
521
522 _magicstring = 'HG20'
522 _magicstring = 'HG20'
523
523
524 def __init__(self, ui, capabilities=()):
524 def __init__(self, ui, capabilities=()):
525 self.ui = ui
525 self.ui = ui
526 self._params = []
526 self._params = []
527 self._parts = []
527 self._parts = []
528 self.capabilities = dict(capabilities)
528 self.capabilities = dict(capabilities)
529 self._compengine = util.compengines.forbundletype('UN')
529 self._compengine = util.compengines.forbundletype('UN')
530 self._compopts = None
530 self._compopts = None
531
531
532 def setcompression(self, alg, compopts=None):
532 def setcompression(self, alg, compopts=None):
533 """setup core part compression to <alg>"""
533 """setup core part compression to <alg>"""
534 if alg in (None, 'UN'):
534 if alg in (None, 'UN'):
535 return
535 return
536 assert not any(n.lower() == 'compression' for n, v in self._params)
536 assert not any(n.lower() == 'compression' for n, v in self._params)
537 self.addparam('Compression', alg)
537 self.addparam('Compression', alg)
538 self._compengine = util.compengines.forbundletype(alg)
538 self._compengine = util.compengines.forbundletype(alg)
539 self._compopts = compopts
539 self._compopts = compopts
540
540
541 @property
541 @property
542 def nbparts(self):
542 def nbparts(self):
543 """total number of parts added to the bundler"""
543 """total number of parts added to the bundler"""
544 return len(self._parts)
544 return len(self._parts)
545
545
546 # methods used to defines the bundle2 content
546 # methods used to defines the bundle2 content
547 def addparam(self, name, value=None):
547 def addparam(self, name, value=None):
548 """add a stream level parameter"""
548 """add a stream level parameter"""
549 if not name:
549 if not name:
550 raise ValueError('empty parameter name')
550 raise ValueError('empty parameter name')
551 if name[0] not in string.letters:
551 if name[0] not in string.letters:
552 raise ValueError('non letter first character: %r' % name)
552 raise ValueError('non letter first character: %r' % name)
553 self._params.append((name, value))
553 self._params.append((name, value))
554
554
555 def addpart(self, part):
555 def addpart(self, part):
556 """add a new part to the bundle2 container
556 """add a new part to the bundle2 container
557
557
558 Parts contains the actual applicative payload."""
558 Parts contains the actual applicative payload."""
559 assert part.id is None
559 assert part.id is None
560 part.id = len(self._parts) # very cheap counter
560 part.id = len(self._parts) # very cheap counter
561 self._parts.append(part)
561 self._parts.append(part)
562
562
563 def newpart(self, typeid, *args, **kwargs):
563 def newpart(self, typeid, *args, **kwargs):
564 """create a new part and add it to the containers
564 """create a new part and add it to the containers
565
565
566 As the part is directly added to the containers. For now, this means
566 As the part is directly added to the containers. For now, this means
567 that any failure to properly initialize the part after calling
567 that any failure to properly initialize the part after calling
568 ``newpart`` should result in a failure of the whole bundling process.
568 ``newpart`` should result in a failure of the whole bundling process.
569
569
570 You can still fall back to manually create and add if you need better
570 You can still fall back to manually create and add if you need better
571 control."""
571 control."""
572 part = bundlepart(typeid, *args, **kwargs)
572 part = bundlepart(typeid, *args, **kwargs)
573 self.addpart(part)
573 self.addpart(part)
574 return part
574 return part
575
575
576 # methods used to generate the bundle2 stream
576 # methods used to generate the bundle2 stream
577 def getchunks(self):
577 def getchunks(self):
578 if self.ui.debugflag:
578 if self.ui.debugflag:
579 msg = ['bundle2-output-bundle: "%s",' % self._magicstring]
579 msg = ['bundle2-output-bundle: "%s",' % self._magicstring]
580 if self._params:
580 if self._params:
581 msg.append(' (%i params)' % len(self._params))
581 msg.append(' (%i params)' % len(self._params))
582 msg.append(' %i parts total\n' % len(self._parts))
582 msg.append(' %i parts total\n' % len(self._parts))
583 self.ui.debug(''.join(msg))
583 self.ui.debug(''.join(msg))
584 outdebug(self.ui, 'start emission of %s stream' % self._magicstring)
584 outdebug(self.ui, 'start emission of %s stream' % self._magicstring)
585 yield self._magicstring
585 yield self._magicstring
586 param = self._paramchunk()
586 param = self._paramchunk()
587 outdebug(self.ui, 'bundle parameter: %s' % param)
587 outdebug(self.ui, 'bundle parameter: %s' % param)
588 yield _pack(_fstreamparamsize, len(param))
588 yield _pack(_fstreamparamsize, len(param))
589 if param:
589 if param:
590 yield param
590 yield param
591 for chunk in self._compengine.compressstream(self._getcorechunk(),
591 for chunk in self._compengine.compressstream(self._getcorechunk(),
592 self._compopts):
592 self._compopts):
593 yield chunk
593 yield chunk
594
594
595 def _paramchunk(self):
595 def _paramchunk(self):
596 """return a encoded version of all stream parameters"""
596 """return a encoded version of all stream parameters"""
597 blocks = []
597 blocks = []
598 for par, value in self._params:
598 for par, value in self._params:
599 par = urlreq.quote(par)
599 par = urlreq.quote(par)
600 if value is not None:
600 if value is not None:
601 value = urlreq.quote(value)
601 value = urlreq.quote(value)
602 par = '%s=%s' % (par, value)
602 par = '%s=%s' % (par, value)
603 blocks.append(par)
603 blocks.append(par)
604 return ' '.join(blocks)
604 return ' '.join(blocks)
605
605
606 def _getcorechunk(self):
606 def _getcorechunk(self):
607 """yield chunk for the core part of the bundle
607 """yield chunk for the core part of the bundle
608
608
609 (all but headers and parameters)"""
609 (all but headers and parameters)"""
610 outdebug(self.ui, 'start of parts')
610 outdebug(self.ui, 'start of parts')
611 for part in self._parts:
611 for part in self._parts:
612 outdebug(self.ui, 'bundle part: "%s"' % part.type)
612 outdebug(self.ui, 'bundle part: "%s"' % part.type)
613 for chunk in part.getchunks(ui=self.ui):
613 for chunk in part.getchunks(ui=self.ui):
614 yield chunk
614 yield chunk
615 outdebug(self.ui, 'end of bundle')
615 outdebug(self.ui, 'end of bundle')
616 yield _pack(_fpartheadersize, 0)
616 yield _pack(_fpartheadersize, 0)
617
617
618
618
619 def salvageoutput(self):
619 def salvageoutput(self):
620 """return a list with a copy of all output parts in the bundle
620 """return a list with a copy of all output parts in the bundle
621
621
622 This is meant to be used during error handling to make sure we preserve
622 This is meant to be used during error handling to make sure we preserve
623 server output"""
623 server output"""
624 salvaged = []
624 salvaged = []
625 for part in self._parts:
625 for part in self._parts:
626 if part.type.startswith('output'):
626 if part.type.startswith('output'):
627 salvaged.append(part.copy())
627 salvaged.append(part.copy())
628 return salvaged
628 return salvaged
629
629
630
630
631 class unpackermixin(object):
631 class unpackermixin(object):
632 """A mixin to extract bytes and struct data from a stream"""
632 """A mixin to extract bytes and struct data from a stream"""
633
633
634 def __init__(self, fp):
634 def __init__(self, fp):
635 self._fp = fp
635 self._fp = fp
636
636
637 def _unpack(self, format):
637 def _unpack(self, format):
638 """unpack this struct format from the stream
638 """unpack this struct format from the stream
639
639
640 This method is meant for internal usage by the bundle2 protocol only.
640 This method is meant for internal usage by the bundle2 protocol only.
641 They directly manipulate the low level stream including bundle2 level
641 They directly manipulate the low level stream including bundle2 level
642 instruction.
642 instruction.
643
643
644 Do not use it to implement higher-level logic or methods."""
644 Do not use it to implement higher-level logic or methods."""
645 data = self._readexact(struct.calcsize(format))
645 data = self._readexact(struct.calcsize(format))
646 return _unpack(format, data)
646 return _unpack(format, data)
647
647
648 def _readexact(self, size):
648 def _readexact(self, size):
649 """read exactly <size> bytes from the stream
649 """read exactly <size> bytes from the stream
650
650
651 This method is meant for internal usage by the bundle2 protocol only.
651 This method is meant for internal usage by the bundle2 protocol only.
652 They directly manipulate the low level stream including bundle2 level
652 They directly manipulate the low level stream including bundle2 level
653 instruction.
653 instruction.
654
654
655 Do not use it to implement higher-level logic or methods."""
655 Do not use it to implement higher-level logic or methods."""
656 return changegroup.readexactly(self._fp, size)
656 return changegroup.readexactly(self._fp, size)
657
657
658 def getunbundler(ui, fp, magicstring=None):
658 def getunbundler(ui, fp, magicstring=None):
659 """return a valid unbundler object for a given magicstring"""
659 """return a valid unbundler object for a given magicstring"""
660 if magicstring is None:
660 if magicstring is None:
661 magicstring = changegroup.readexactly(fp, 4)
661 magicstring = changegroup.readexactly(fp, 4)
662 magic, version = magicstring[0:2], magicstring[2:4]
662 magic, version = magicstring[0:2], magicstring[2:4]
663 if magic != 'HG':
663 if magic != 'HG':
664 raise error.Abort(_('not a Mercurial bundle'))
664 raise error.Abort(_('not a Mercurial bundle'))
665 unbundlerclass = formatmap.get(version)
665 unbundlerclass = formatmap.get(version)
666 if unbundlerclass is None:
666 if unbundlerclass is None:
667 raise error.Abort(_('unknown bundle version %s') % version)
667 raise error.Abort(_('unknown bundle version %s') % version)
668 unbundler = unbundlerclass(ui, fp)
668 unbundler = unbundlerclass(ui, fp)
669 indebug(ui, 'start processing of %s stream' % magicstring)
669 indebug(ui, 'start processing of %s stream' % magicstring)
670 return unbundler
670 return unbundler
671
671
672 class unbundle20(unpackermixin):
672 class unbundle20(unpackermixin):
673 """interpret a bundle2 stream
673 """interpret a bundle2 stream
674
674
675 This class is fed with a binary stream and yields parts through its
675 This class is fed with a binary stream and yields parts through its
676 `iterparts` methods."""
676 `iterparts` methods."""
677
677
678 _magicstring = 'HG20'
678 _magicstring = 'HG20'
679
679
680 def __init__(self, ui, fp):
680 def __init__(self, ui, fp):
681 """If header is specified, we do not read it out of the stream."""
681 """If header is specified, we do not read it out of the stream."""
682 self.ui = ui
682 self.ui = ui
683 self._compengine = util.compengines.forbundletype('UN')
683 self._compengine = util.compengines.forbundletype('UN')
684 self._compressed = None
684 self._compressed = None
685 super(unbundle20, self).__init__(fp)
685 super(unbundle20, self).__init__(fp)
686
686
687 @util.propertycache
687 @util.propertycache
688 def params(self):
688 def params(self):
689 """dictionary of stream level parameters"""
689 """dictionary of stream level parameters"""
690 indebug(self.ui, 'reading bundle2 stream parameters')
690 indebug(self.ui, 'reading bundle2 stream parameters')
691 params = {}
691 params = {}
692 paramssize = self._unpack(_fstreamparamsize)[0]
692 paramssize = self._unpack(_fstreamparamsize)[0]
693 if paramssize < 0:
693 if paramssize < 0:
694 raise error.BundleValueError('negative bundle param size: %i'
694 raise error.BundleValueError('negative bundle param size: %i'
695 % paramssize)
695 % paramssize)
696 if paramssize:
696 if paramssize:
697 params = self._readexact(paramssize)
697 params = self._readexact(paramssize)
698 params = self._processallparams(params)
698 params = self._processallparams(params)
699 return params
699 return params
700
700
701 def _processallparams(self, paramsblock):
701 def _processallparams(self, paramsblock):
702 """"""
702 """"""
703 params = util.sortdict()
703 params = util.sortdict()
704 for p in paramsblock.split(' '):
704 for p in paramsblock.split(' '):
705 p = p.split('=', 1)
705 p = p.split('=', 1)
706 p = [urlreq.unquote(i) for i in p]
706 p = [urlreq.unquote(i) for i in p]
707 if len(p) < 2:
707 if len(p) < 2:
708 p.append(None)
708 p.append(None)
709 self._processparam(*p)
709 self._processparam(*p)
710 params[p[0]] = p[1]
710 params[p[0]] = p[1]
711 return params
711 return params
712
712
713
713
714 def _processparam(self, name, value):
714 def _processparam(self, name, value):
715 """process a parameter, applying its effect if needed
715 """process a parameter, applying its effect if needed
716
716
717 Parameter starting with a lower case letter are advisory and will be
717 Parameter starting with a lower case letter are advisory and will be
718 ignored when unknown. Those starting with an upper case letter are
718 ignored when unknown. Those starting with an upper case letter are
719 mandatory and will this function will raise a KeyError when unknown.
719 mandatory and will this function will raise a KeyError when unknown.
720
720
721 Note: no option are currently supported. Any input will be either
721 Note: no option are currently supported. Any input will be either
722 ignored or failing.
722 ignored or failing.
723 """
723 """
724 if not name:
724 if not name:
725 raise ValueError('empty parameter name')
725 raise ValueError('empty parameter name')
726 if name[0] not in string.letters:
726 if name[0] not in string.letters:
727 raise ValueError('non letter first character: %r' % name)
727 raise ValueError('non letter first character: %r' % name)
728 try:
728 try:
729 handler = b2streamparamsmap[name.lower()]
729 handler = b2streamparamsmap[name.lower()]
730 except KeyError:
730 except KeyError:
731 if name[0].islower():
731 if name[0].islower():
732 indebug(self.ui, "ignoring unknown parameter %r" % name)
732 indebug(self.ui, "ignoring unknown parameter %r" % name)
733 else:
733 else:
734 raise error.BundleUnknownFeatureError(params=(name,))
734 raise error.BundleUnknownFeatureError(params=(name,))
735 else:
735 else:
736 handler(self, name, value)
736 handler(self, name, value)
737
737
738 def _forwardchunks(self):
738 def _forwardchunks(self):
739 """utility to transfer a bundle2 as binary
739 """utility to transfer a bundle2 as binary
740
740
741 This is made necessary by the fact the 'getbundle' command over 'ssh'
741 This is made necessary by the fact the 'getbundle' command over 'ssh'
742 have no way to know then the reply end, relying on the bundle to be
742 have no way to know then the reply end, relying on the bundle to be
743 interpreted to know its end. This is terrible and we are sorry, but we
743 interpreted to know its end. This is terrible and we are sorry, but we
744 needed to move forward to get general delta enabled.
744 needed to move forward to get general delta enabled.
745 """
745 """
746 yield self._magicstring
746 yield self._magicstring
747 assert 'params' not in vars(self)
747 assert 'params' not in vars(self)
748 paramssize = self._unpack(_fstreamparamsize)[0]
748 paramssize = self._unpack(_fstreamparamsize)[0]
749 if paramssize < 0:
749 if paramssize < 0:
750 raise error.BundleValueError('negative bundle param size: %i'
750 raise error.BundleValueError('negative bundle param size: %i'
751 % paramssize)
751 % paramssize)
752 yield _pack(_fstreamparamsize, paramssize)
752 yield _pack(_fstreamparamsize, paramssize)
753 if paramssize:
753 if paramssize:
754 params = self._readexact(paramssize)
754 params = self._readexact(paramssize)
755 self._processallparams(params)
755 self._processallparams(params)
756 yield params
756 yield params
757 assert self._compengine.bundletype == 'UN'
757 assert self._compengine.bundletype == 'UN'
758 # From there, payload might need to be decompressed
758 # From there, payload might need to be decompressed
759 self._fp = self._compengine.decompressorreader(self._fp)
759 self._fp = self._compengine.decompressorreader(self._fp)
760 emptycount = 0
760 emptycount = 0
761 while emptycount < 2:
761 while emptycount < 2:
762 # so we can brainlessly loop
762 # so we can brainlessly loop
763 assert _fpartheadersize == _fpayloadsize
763 assert _fpartheadersize == _fpayloadsize
764 size = self._unpack(_fpartheadersize)[0]
764 size = self._unpack(_fpartheadersize)[0]
765 yield _pack(_fpartheadersize, size)
765 yield _pack(_fpartheadersize, size)
766 if size:
766 if size:
767 emptycount = 0
767 emptycount = 0
768 else:
768 else:
769 emptycount += 1
769 emptycount += 1
770 continue
770 continue
771 if size == flaginterrupt:
771 if size == flaginterrupt:
772 continue
772 continue
773 elif size < 0:
773 elif size < 0:
774 raise error.BundleValueError('negative chunk size: %i')
774 raise error.BundleValueError('negative chunk size: %i')
775 yield self._readexact(size)
775 yield self._readexact(size)
776
776
777
777
778 def iterparts(self):
778 def iterparts(self):
779 """yield all parts contained in the stream"""
779 """yield all parts contained in the stream"""
780 # make sure param have been loaded
780 # make sure param have been loaded
781 self.params
781 self.params
782 # From there, payload need to be decompressed
782 # From there, payload need to be decompressed
783 self._fp = self._compengine.decompressorreader(self._fp)
783 self._fp = self._compengine.decompressorreader(self._fp)
784 indebug(self.ui, 'start extraction of bundle2 parts')
784 indebug(self.ui, 'start extraction of bundle2 parts')
785 headerblock = self._readpartheader()
785 headerblock = self._readpartheader()
786 while headerblock is not None:
786 while headerblock is not None:
787 part = unbundlepart(self.ui, headerblock, self._fp)
787 part = unbundlepart(self.ui, headerblock, self._fp)
788 yield part
788 yield part
789 part.seek(0, 2)
789 part.seek(0, 2)
790 headerblock = self._readpartheader()
790 headerblock = self._readpartheader()
791 indebug(self.ui, 'end of bundle2 stream')
791 indebug(self.ui, 'end of bundle2 stream')
792
792
793 def _readpartheader(self):
793 def _readpartheader(self):
794 """reads a part header size and return the bytes blob
794 """reads a part header size and return the bytes blob
795
795
796 returns None if empty"""
796 returns None if empty"""
797 headersize = self._unpack(_fpartheadersize)[0]
797 headersize = self._unpack(_fpartheadersize)[0]
798 if headersize < 0:
798 if headersize < 0:
799 raise error.BundleValueError('negative part header size: %i'
799 raise error.BundleValueError('negative part header size: %i'
800 % headersize)
800 % headersize)
801 indebug(self.ui, 'part header size: %i' % headersize)
801 indebug(self.ui, 'part header size: %i' % headersize)
802 if headersize:
802 if headersize:
803 return self._readexact(headersize)
803 return self._readexact(headersize)
804 return None
804 return None
805
805
806 def compressed(self):
806 def compressed(self):
807 self.params # load params
807 self.params # load params
808 return self._compressed
808 return self._compressed
809
809
810 def close(self):
810 def close(self):
811 """close underlying file"""
811 """close underlying file"""
812 if util.safehasattr(self._fp, 'close'):
812 if util.safehasattr(self._fp, 'close'):
813 return self._fp.close()
813 return self._fp.close()
814
814
815 formatmap = {'20': unbundle20}
815 formatmap = {'20': unbundle20}
816
816
817 b2streamparamsmap = {}
817 b2streamparamsmap = {}
818
818
819 def b2streamparamhandler(name):
819 def b2streamparamhandler(name):
820 """register a handler for a stream level parameter"""
820 """register a handler for a stream level parameter"""
821 def decorator(func):
821 def decorator(func):
822 assert name not in formatmap
822 assert name not in formatmap
823 b2streamparamsmap[name] = func
823 b2streamparamsmap[name] = func
824 return func
824 return func
825 return decorator
825 return decorator
826
826
827 @b2streamparamhandler('compression')
827 @b2streamparamhandler('compression')
828 def processcompression(unbundler, param, value):
828 def processcompression(unbundler, param, value):
829 """read compression parameter and install payload decompression"""
829 """read compression parameter and install payload decompression"""
830 if value not in util.compengines.supportedbundletypes:
830 if value not in util.compengines.supportedbundletypes:
831 raise error.BundleUnknownFeatureError(params=(param,),
831 raise error.BundleUnknownFeatureError(params=(param,),
832 values=(value,))
832 values=(value,))
833 unbundler._compengine = util.compengines.forbundletype(value)
833 unbundler._compengine = util.compengines.forbundletype(value)
834 if value is not None:
834 if value is not None:
835 unbundler._compressed = True
835 unbundler._compressed = True
836
836
837 class bundlepart(object):
837 class bundlepart(object):
838 """A bundle2 part contains application level payload
838 """A bundle2 part contains application level payload
839
839
840 The part `type` is used to route the part to the application level
840 The part `type` is used to route the part to the application level
841 handler.
841 handler.
842
842
843 The part payload is contained in ``part.data``. It could be raw bytes or a
843 The part payload is contained in ``part.data``. It could be raw bytes or a
844 generator of byte chunks.
844 generator of byte chunks.
845
845
846 You can add parameters to the part using the ``addparam`` method.
846 You can add parameters to the part using the ``addparam`` method.
847 Parameters can be either mandatory (default) or advisory. Remote side
847 Parameters can be either mandatory (default) or advisory. Remote side
848 should be able to safely ignore the advisory ones.
848 should be able to safely ignore the advisory ones.
849
849
850 Both data and parameters cannot be modified after the generation has begun.
850 Both data and parameters cannot be modified after the generation has begun.
851 """
851 """
852
852
853 def __init__(self, parttype, mandatoryparams=(), advisoryparams=(),
853 def __init__(self, parttype, mandatoryparams=(), advisoryparams=(),
854 data='', mandatory=True):
854 data='', mandatory=True):
855 validateparttype(parttype)
855 validateparttype(parttype)
856 self.id = None
856 self.id = None
857 self.type = parttype
857 self.type = parttype
858 self._data = data
858 self._data = data
859 self._mandatoryparams = list(mandatoryparams)
859 self._mandatoryparams = list(mandatoryparams)
860 self._advisoryparams = list(advisoryparams)
860 self._advisoryparams = list(advisoryparams)
861 # checking for duplicated entries
861 # checking for duplicated entries
862 self._seenparams = set()
862 self._seenparams = set()
863 for pname, __ in self._mandatoryparams + self._advisoryparams:
863 for pname, __ in self._mandatoryparams + self._advisoryparams:
864 if pname in self._seenparams:
864 if pname in self._seenparams:
865 raise error.ProgrammingError('duplicated params: %s' % pname)
865 raise error.ProgrammingError('duplicated params: %s' % pname)
866 self._seenparams.add(pname)
866 self._seenparams.add(pname)
867 # status of the part's generation:
867 # status of the part's generation:
868 # - None: not started,
868 # - None: not started,
869 # - False: currently generated,
869 # - False: currently generated,
870 # - True: generation done.
870 # - True: generation done.
871 self._generated = None
871 self._generated = None
872 self.mandatory = mandatory
872 self.mandatory = mandatory
873
873
874 def __repr__(self):
874 def __repr__(self):
875 cls = "%s.%s" % (self.__class__.__module__, self.__class__.__name__)
875 cls = "%s.%s" % (self.__class__.__module__, self.__class__.__name__)
876 return ('<%s object at %x; id: %s; type: %s; mandatory: %s>'
876 return ('<%s object at %x; id: %s; type: %s; mandatory: %s>'
877 % (cls, id(self), self.id, self.type, self.mandatory))
877 % (cls, id(self), self.id, self.type, self.mandatory))
878
878
879 def copy(self):
879 def copy(self):
880 """return a copy of the part
880 """return a copy of the part
881
881
882 The new part have the very same content but no partid assigned yet.
882 The new part have the very same content but no partid assigned yet.
883 Parts with generated data cannot be copied."""
883 Parts with generated data cannot be copied."""
884 assert not util.safehasattr(self.data, 'next')
884 assert not util.safehasattr(self.data, 'next')
885 return self.__class__(self.type, self._mandatoryparams,
885 return self.__class__(self.type, self._mandatoryparams,
886 self._advisoryparams, self._data, self.mandatory)
886 self._advisoryparams, self._data, self.mandatory)
887
887
888 # methods used to defines the part content
888 # methods used to defines the part content
889 @property
889 @property
890 def data(self):
890 def data(self):
891 return self._data
891 return self._data
892
892
893 @data.setter
893 @data.setter
894 def data(self, data):
894 def data(self, data):
895 if self._generated is not None:
895 if self._generated is not None:
896 raise error.ReadOnlyPartError('part is being generated')
896 raise error.ReadOnlyPartError('part is being generated')
897 self._data = data
897 self._data = data
898
898
899 @property
899 @property
900 def mandatoryparams(self):
900 def mandatoryparams(self):
901 # make it an immutable tuple to force people through ``addparam``
901 # make it an immutable tuple to force people through ``addparam``
902 return tuple(self._mandatoryparams)
902 return tuple(self._mandatoryparams)
903
903
904 @property
904 @property
905 def advisoryparams(self):
905 def advisoryparams(self):
906 # make it an immutable tuple to force people through ``addparam``
906 # make it an immutable tuple to force people through ``addparam``
907 return tuple(self._advisoryparams)
907 return tuple(self._advisoryparams)
908
908
909 def addparam(self, name, value='', mandatory=True):
909 def addparam(self, name, value='', mandatory=True):
910 """add a parameter to the part
910 """add a parameter to the part
911
911
912 If 'mandatory' is set to True, the remote handler must claim support
912 If 'mandatory' is set to True, the remote handler must claim support
913 for this parameter or the unbundling will be aborted.
913 for this parameter or the unbundling will be aborted.
914
914
915 The 'name' and 'value' cannot exceed 255 bytes each.
915 The 'name' and 'value' cannot exceed 255 bytes each.
916 """
916 """
917 if self._generated is not None:
917 if self._generated is not None:
918 raise error.ReadOnlyPartError('part is being generated')
918 raise error.ReadOnlyPartError('part is being generated')
919 if name in self._seenparams:
919 if name in self._seenparams:
920 raise ValueError('duplicated params: %s' % name)
920 raise ValueError('duplicated params: %s' % name)
921 self._seenparams.add(name)
921 self._seenparams.add(name)
922 params = self._advisoryparams
922 params = self._advisoryparams
923 if mandatory:
923 if mandatory:
924 params = self._mandatoryparams
924 params = self._mandatoryparams
925 params.append((name, value))
925 params.append((name, value))
926
926
927 # methods used to generates the bundle2 stream
927 # methods used to generates the bundle2 stream
928 def getchunks(self, ui):
928 def getchunks(self, ui):
929 if self._generated is not None:
929 if self._generated is not None:
930 raise error.ProgrammingError('part can only be consumed once')
930 raise error.ProgrammingError('part can only be consumed once')
931 self._generated = False
931 self._generated = False
932
932
933 if ui.debugflag:
933 if ui.debugflag:
934 msg = ['bundle2-output-part: "%s"' % self.type]
934 msg = ['bundle2-output-part: "%s"' % self.type]
935 if not self.mandatory:
935 if not self.mandatory:
936 msg.append(' (advisory)')
936 msg.append(' (advisory)')
937 nbmp = len(self.mandatoryparams)
937 nbmp = len(self.mandatoryparams)
938 nbap = len(self.advisoryparams)
938 nbap = len(self.advisoryparams)
939 if nbmp or nbap:
939 if nbmp or nbap:
940 msg.append(' (params:')
940 msg.append(' (params:')
941 if nbmp:
941 if nbmp:
942 msg.append(' %i mandatory' % nbmp)
942 msg.append(' %i mandatory' % nbmp)
943 if nbap:
943 if nbap:
944 msg.append(' %i advisory' % nbmp)
944 msg.append(' %i advisory' % nbmp)
945 msg.append(')')
945 msg.append(')')
946 if not self.data:
946 if not self.data:
947 msg.append(' empty payload')
947 msg.append(' empty payload')
948 elif util.safehasattr(self.data, 'next'):
948 elif util.safehasattr(self.data, 'next'):
949 msg.append(' streamed payload')
949 msg.append(' streamed payload')
950 else:
950 else:
951 msg.append(' %i bytes payload' % len(self.data))
951 msg.append(' %i bytes payload' % len(self.data))
952 msg.append('\n')
952 msg.append('\n')
953 ui.debug(''.join(msg))
953 ui.debug(''.join(msg))
954
954
955 #### header
955 #### header
956 if self.mandatory:
956 if self.mandatory:
957 parttype = self.type.upper()
957 parttype = self.type.upper()
958 else:
958 else:
959 parttype = self.type.lower()
959 parttype = self.type.lower()
960 outdebug(ui, 'part %s: "%s"' % (self.id, parttype))
960 outdebug(ui, 'part %s: "%s"' % (self.id, parttype))
961 ## parttype
961 ## parttype
962 header = [_pack(_fparttypesize, len(parttype)),
962 header = [_pack(_fparttypesize, len(parttype)),
963 parttype, _pack(_fpartid, self.id),
963 parttype, _pack(_fpartid, self.id),
964 ]
964 ]
965 ## parameters
965 ## parameters
966 # count
966 # count
967 manpar = self.mandatoryparams
967 manpar = self.mandatoryparams
968 advpar = self.advisoryparams
968 advpar = self.advisoryparams
969 header.append(_pack(_fpartparamcount, len(manpar), len(advpar)))
969 header.append(_pack(_fpartparamcount, len(manpar), len(advpar)))
970 # size
970 # size
971 parsizes = []
971 parsizes = []
972 for key, value in manpar:
972 for key, value in manpar:
973 parsizes.append(len(key))
973 parsizes.append(len(key))
974 parsizes.append(len(value))
974 parsizes.append(len(value))
975 for key, value in advpar:
975 for key, value in advpar:
976 parsizes.append(len(key))
976 parsizes.append(len(key))
977 parsizes.append(len(value))
977 parsizes.append(len(value))
978 paramsizes = _pack(_makefpartparamsizes(len(parsizes) / 2), *parsizes)
978 paramsizes = _pack(_makefpartparamsizes(len(parsizes) / 2), *parsizes)
979 header.append(paramsizes)
979 header.append(paramsizes)
980 # key, value
980 # key, value
981 for key, value in manpar:
981 for key, value in manpar:
982 header.append(key)
982 header.append(key)
983 header.append(value)
983 header.append(value)
984 for key, value in advpar:
984 for key, value in advpar:
985 header.append(key)
985 header.append(key)
986 header.append(value)
986 header.append(value)
987 ## finalize header
987 ## finalize header
988 headerchunk = ''.join(header)
988 headerchunk = ''.join(header)
989 outdebug(ui, 'header chunk size: %i' % len(headerchunk))
989 outdebug(ui, 'header chunk size: %i' % len(headerchunk))
990 yield _pack(_fpartheadersize, len(headerchunk))
990 yield _pack(_fpartheadersize, len(headerchunk))
991 yield headerchunk
991 yield headerchunk
992 ## payload
992 ## payload
993 try:
993 try:
994 for chunk in self._payloadchunks():
994 for chunk in self._payloadchunks():
995 outdebug(ui, 'payload chunk size: %i' % len(chunk))
995 outdebug(ui, 'payload chunk size: %i' % len(chunk))
996 yield _pack(_fpayloadsize, len(chunk))
996 yield _pack(_fpayloadsize, len(chunk))
997 yield chunk
997 yield chunk
998 except GeneratorExit:
998 except GeneratorExit:
999 # GeneratorExit means that nobody is listening for our
999 # GeneratorExit means that nobody is listening for our
1000 # results anyway, so just bail quickly rather than trying
1000 # results anyway, so just bail quickly rather than trying
1001 # to produce an error part.
1001 # to produce an error part.
1002 ui.debug('bundle2-generatorexit\n')
1002 ui.debug('bundle2-generatorexit\n')
1003 raise
1003 raise
1004 except BaseException as exc:
1004 except BaseException as exc:
1005 # backup exception data for later
1005 # backup exception data for later
1006 ui.debug('bundle2-input-stream-interrupt: encoding exception %s'
1006 ui.debug('bundle2-input-stream-interrupt: encoding exception %s'
1007 % exc)
1007 % exc)
1008 exc_info = sys.exc_info()
1008 exc_info = sys.exc_info()
1009 msg = 'unexpected error: %s' % exc
1009 msg = 'unexpected error: %s' % exc
1010 interpart = bundlepart('error:abort', [('message', msg)],
1010 interpart = bundlepart('error:abort', [('message', msg)],
1011 mandatory=False)
1011 mandatory=False)
1012 interpart.id = 0
1012 interpart.id = 0
1013 yield _pack(_fpayloadsize, -1)
1013 yield _pack(_fpayloadsize, -1)
1014 for chunk in interpart.getchunks(ui=ui):
1014 for chunk in interpart.getchunks(ui=ui):
1015 yield chunk
1015 yield chunk
1016 outdebug(ui, 'closing payload chunk')
1016 outdebug(ui, 'closing payload chunk')
1017 # abort current part payload
1017 # abort current part payload
1018 yield _pack(_fpayloadsize, 0)
1018 yield _pack(_fpayloadsize, 0)
1019 if pycompat.ispy3:
1019 if pycompat.ispy3:
1020 raise exc_info[0](exc_info[1]).with_traceback(exc_info[2])
1020 raise exc_info[0](exc_info[1]).with_traceback(exc_info[2])
1021 else:
1021 else:
1022 exec("""raise exc_info[0], exc_info[1], exc_info[2]""")
1022 exec("""raise exc_info[0], exc_info[1], exc_info[2]""")
1023 # end of payload
1023 # end of payload
1024 outdebug(ui, 'closing payload chunk')
1024 outdebug(ui, 'closing payload chunk')
1025 yield _pack(_fpayloadsize, 0)
1025 yield _pack(_fpayloadsize, 0)
1026 self._generated = True
1026 self._generated = True
1027
1027
1028 def _payloadchunks(self):
1028 def _payloadchunks(self):
1029 """yield chunks of a the part payload
1029 """yield chunks of a the part payload
1030
1030
1031 Exists to handle the different methods to provide data to a part."""
1031 Exists to handle the different methods to provide data to a part."""
1032 # we only support fixed size data now.
1032 # we only support fixed size data now.
1033 # This will be improved in the future.
1033 # This will be improved in the future.
1034 if util.safehasattr(self.data, 'next'):
1034 if util.safehasattr(self.data, 'next'):
1035 buff = util.chunkbuffer(self.data)
1035 buff = util.chunkbuffer(self.data)
1036 chunk = buff.read(preferedchunksize)
1036 chunk = buff.read(preferedchunksize)
1037 while chunk:
1037 while chunk:
1038 yield chunk
1038 yield chunk
1039 chunk = buff.read(preferedchunksize)
1039 chunk = buff.read(preferedchunksize)
1040 elif len(self.data):
1040 elif len(self.data):
1041 yield self.data
1041 yield self.data
1042
1042
1043
1043
1044 flaginterrupt = -1
1044 flaginterrupt = -1
1045
1045
1046 class interrupthandler(unpackermixin):
1046 class interrupthandler(unpackermixin):
1047 """read one part and process it with restricted capability
1047 """read one part and process it with restricted capability
1048
1048
1049 This allows to transmit exception raised on the producer size during part
1049 This allows to transmit exception raised on the producer size during part
1050 iteration while the consumer is reading a part.
1050 iteration while the consumer is reading a part.
1051
1051
1052 Part processed in this manner only have access to a ui object,"""
1052 Part processed in this manner only have access to a ui object,"""
1053
1053
1054 def __init__(self, ui, fp):
1054 def __init__(self, ui, fp):
1055 super(interrupthandler, self).__init__(fp)
1055 super(interrupthandler, self).__init__(fp)
1056 self.ui = ui
1056 self.ui = ui
1057
1057
1058 def _readpartheader(self):
1058 def _readpartheader(self):
1059 """reads a part header size and return the bytes blob
1059 """reads a part header size and return the bytes blob
1060
1060
1061 returns None if empty"""
1061 returns None if empty"""
1062 headersize = self._unpack(_fpartheadersize)[0]
1062 headersize = self._unpack(_fpartheadersize)[0]
1063 if headersize < 0:
1063 if headersize < 0:
1064 raise error.BundleValueError('negative part header size: %i'
1064 raise error.BundleValueError('negative part header size: %i'
1065 % headersize)
1065 % headersize)
1066 indebug(self.ui, 'part header size: %i\n' % headersize)
1066 indebug(self.ui, 'part header size: %i\n' % headersize)
1067 if headersize:
1067 if headersize:
1068 return self._readexact(headersize)
1068 return self._readexact(headersize)
1069 return None
1069 return None
1070
1070
1071 def __call__(self):
1071 def __call__(self):
1072
1072
1073 self.ui.debug('bundle2-input-stream-interrupt:'
1073 self.ui.debug('bundle2-input-stream-interrupt:'
1074 ' opening out of band context\n')
1074 ' opening out of band context\n')
1075 indebug(self.ui, 'bundle2 stream interruption, looking for a part.')
1075 indebug(self.ui, 'bundle2 stream interruption, looking for a part.')
1076 headerblock = self._readpartheader()
1076 headerblock = self._readpartheader()
1077 if headerblock is None:
1077 if headerblock is None:
1078 indebug(self.ui, 'no part found during interruption.')
1078 indebug(self.ui, 'no part found during interruption.')
1079 return
1079 return
1080 part = unbundlepart(self.ui, headerblock, self._fp)
1080 part = unbundlepart(self.ui, headerblock, self._fp)
1081 op = interruptoperation(self.ui)
1081 op = interruptoperation(self.ui)
1082 _processpart(op, part)
1082 _processpart(op, part)
1083 self.ui.debug('bundle2-input-stream-interrupt:'
1083 self.ui.debug('bundle2-input-stream-interrupt:'
1084 ' closing out of band context\n')
1084 ' closing out of band context\n')
1085
1085
1086 class interruptoperation(object):
1086 class interruptoperation(object):
1087 """A limited operation to be use by part handler during interruption
1087 """A limited operation to be use by part handler during interruption
1088
1088
1089 It only have access to an ui object.
1089 It only have access to an ui object.
1090 """
1090 """
1091
1091
1092 def __init__(self, ui):
1092 def __init__(self, ui):
1093 self.ui = ui
1093 self.ui = ui
1094 self.reply = None
1094 self.reply = None
1095 self.captureoutput = False
1095 self.captureoutput = False
1096
1096
1097 @property
1097 @property
1098 def repo(self):
1098 def repo(self):
1099 raise error.ProgrammingError('no repo access from stream interruption')
1099 raise error.ProgrammingError('no repo access from stream interruption')
1100
1100
1101 def gettransaction(self):
1101 def gettransaction(self):
1102 raise TransactionUnavailable('no repo access from stream interruption')
1102 raise TransactionUnavailable('no repo access from stream interruption')
1103
1103
1104 class unbundlepart(unpackermixin):
1104 class unbundlepart(unpackermixin):
1105 """a bundle part read from a bundle"""
1105 """a bundle part read from a bundle"""
1106
1106
1107 def __init__(self, ui, header, fp):
1107 def __init__(self, ui, header, fp):
1108 super(unbundlepart, self).__init__(fp)
1108 super(unbundlepart, self).__init__(fp)
1109 self._seekable = (util.safehasattr(fp, 'seek') and
1109 self._seekable = (util.safehasattr(fp, 'seek') and
1110 util.safehasattr(fp, 'tell'))
1110 util.safehasattr(fp, 'tell'))
1111 self.ui = ui
1111 self.ui = ui
1112 # unbundle state attr
1112 # unbundle state attr
1113 self._headerdata = header
1113 self._headerdata = header
1114 self._headeroffset = 0
1114 self._headeroffset = 0
1115 self._initialized = False
1115 self._initialized = False
1116 self.consumed = False
1116 self.consumed = False
1117 # part data
1117 # part data
1118 self.id = None
1118 self.id = None
1119 self.type = None
1119 self.type = None
1120 self.mandatoryparams = None
1120 self.mandatoryparams = None
1121 self.advisoryparams = None
1121 self.advisoryparams = None
1122 self.params = None
1122 self.params = None
1123 self.mandatorykeys = ()
1123 self.mandatorykeys = ()
1124 self._payloadstream = None
1124 self._payloadstream = None
1125 self._readheader()
1125 self._readheader()
1126 self._mandatory = None
1126 self._mandatory = None
1127 self._chunkindex = [] #(payload, file) position tuples for chunk starts
1127 self._chunkindex = [] #(payload, file) position tuples for chunk starts
1128 self._pos = 0
1128 self._pos = 0
1129
1129
1130 def _fromheader(self, size):
1130 def _fromheader(self, size):
1131 """return the next <size> byte from the header"""
1131 """return the next <size> byte from the header"""
1132 offset = self._headeroffset
1132 offset = self._headeroffset
1133 data = self._headerdata[offset:(offset + size)]
1133 data = self._headerdata[offset:(offset + size)]
1134 self._headeroffset = offset + size
1134 self._headeroffset = offset + size
1135 return data
1135 return data
1136
1136
1137 def _unpackheader(self, format):
1137 def _unpackheader(self, format):
1138 """read given format from header
1138 """read given format from header
1139
1139
1140 This automatically compute the size of the format to read."""
1140 This automatically compute the size of the format to read."""
1141 data = self._fromheader(struct.calcsize(format))
1141 data = self._fromheader(struct.calcsize(format))
1142 return _unpack(format, data)
1142 return _unpack(format, data)
1143
1143
1144 def _initparams(self, mandatoryparams, advisoryparams):
1144 def _initparams(self, mandatoryparams, advisoryparams):
1145 """internal function to setup all logic related parameters"""
1145 """internal function to setup all logic related parameters"""
1146 # make it read only to prevent people touching it by mistake.
1146 # make it read only to prevent people touching it by mistake.
1147 self.mandatoryparams = tuple(mandatoryparams)
1147 self.mandatoryparams = tuple(mandatoryparams)
1148 self.advisoryparams = tuple(advisoryparams)
1148 self.advisoryparams = tuple(advisoryparams)
1149 # user friendly UI
1149 # user friendly UI
1150 self.params = util.sortdict(self.mandatoryparams)
1150 self.params = util.sortdict(self.mandatoryparams)
1151 self.params.update(self.advisoryparams)
1151 self.params.update(self.advisoryparams)
1152 self.mandatorykeys = frozenset(p[0] for p in mandatoryparams)
1152 self.mandatorykeys = frozenset(p[0] for p in mandatoryparams)
1153
1153
1154 def _payloadchunks(self, chunknum=0):
1154 def _payloadchunks(self, chunknum=0):
1155 '''seek to specified chunk and start yielding data'''
1155 '''seek to specified chunk and start yielding data'''
1156 if len(self._chunkindex) == 0:
1156 if len(self._chunkindex) == 0:
1157 assert chunknum == 0, 'Must start with chunk 0'
1157 assert chunknum == 0, 'Must start with chunk 0'
1158 self._chunkindex.append((0, self._tellfp()))
1158 self._chunkindex.append((0, self._tellfp()))
1159 else:
1159 else:
1160 assert chunknum < len(self._chunkindex), \
1160 assert chunknum < len(self._chunkindex), \
1161 'Unknown chunk %d' % chunknum
1161 'Unknown chunk %d' % chunknum
1162 self._seekfp(self._chunkindex[chunknum][1])
1162 self._seekfp(self._chunkindex[chunknum][1])
1163
1163
1164 pos = self._chunkindex[chunknum][0]
1164 pos = self._chunkindex[chunknum][0]
1165 payloadsize = self._unpack(_fpayloadsize)[0]
1165 payloadsize = self._unpack(_fpayloadsize)[0]
1166 indebug(self.ui, 'payload chunk size: %i' % payloadsize)
1166 indebug(self.ui, 'payload chunk size: %i' % payloadsize)
1167 while payloadsize:
1167 while payloadsize:
1168 if payloadsize == flaginterrupt:
1168 if payloadsize == flaginterrupt:
1169 # interruption detection, the handler will now read a
1169 # interruption detection, the handler will now read a
1170 # single part and process it.
1170 # single part and process it.
1171 interrupthandler(self.ui, self._fp)()
1171 interrupthandler(self.ui, self._fp)()
1172 elif payloadsize < 0:
1172 elif payloadsize < 0:
1173 msg = 'negative payload chunk size: %i' % payloadsize
1173 msg = 'negative payload chunk size: %i' % payloadsize
1174 raise error.BundleValueError(msg)
1174 raise error.BundleValueError(msg)
1175 else:
1175 else:
1176 result = self._readexact(payloadsize)
1176 result = self._readexact(payloadsize)
1177 chunknum += 1
1177 chunknum += 1
1178 pos += payloadsize
1178 pos += payloadsize
1179 if chunknum == len(self._chunkindex):
1179 if chunknum == len(self._chunkindex):
1180 self._chunkindex.append((pos, self._tellfp()))
1180 self._chunkindex.append((pos, self._tellfp()))
1181 yield result
1181 yield result
1182 payloadsize = self._unpack(_fpayloadsize)[0]
1182 payloadsize = self._unpack(_fpayloadsize)[0]
1183 indebug(self.ui, 'payload chunk size: %i' % payloadsize)
1183 indebug(self.ui, 'payload chunk size: %i' % payloadsize)
1184
1184
1185 def _findchunk(self, pos):
1185 def _findchunk(self, pos):
1186 '''for a given payload position, return a chunk number and offset'''
1186 '''for a given payload position, return a chunk number and offset'''
1187 for chunk, (ppos, fpos) in enumerate(self._chunkindex):
1187 for chunk, (ppos, fpos) in enumerate(self._chunkindex):
1188 if ppos == pos:
1188 if ppos == pos:
1189 return chunk, 0
1189 return chunk, 0
1190 elif ppos > pos:
1190 elif ppos > pos:
1191 return chunk - 1, pos - self._chunkindex[chunk - 1][0]
1191 return chunk - 1, pos - self._chunkindex[chunk - 1][0]
1192 raise ValueError('Unknown chunk')
1192 raise ValueError('Unknown chunk')
1193
1193
1194 def _readheader(self):
1194 def _readheader(self):
1195 """read the header and setup the object"""
1195 """read the header and setup the object"""
1196 typesize = self._unpackheader(_fparttypesize)[0]
1196 typesize = self._unpackheader(_fparttypesize)[0]
1197 self.type = self._fromheader(typesize)
1197 self.type = self._fromheader(typesize)
1198 indebug(self.ui, 'part type: "%s"' % self.type)
1198 indebug(self.ui, 'part type: "%s"' % self.type)
1199 self.id = self._unpackheader(_fpartid)[0]
1199 self.id = self._unpackheader(_fpartid)[0]
1200 indebug(self.ui, 'part id: "%s"' % self.id)
1200 indebug(self.ui, 'part id: "%s"' % self.id)
1201 # extract mandatory bit from type
1201 # extract mandatory bit from type
1202 self.mandatory = (self.type != self.type.lower())
1202 self.mandatory = (self.type != self.type.lower())
1203 self.type = self.type.lower()
1203 self.type = self.type.lower()
1204 ## reading parameters
1204 ## reading parameters
1205 # param count
1205 # param count
1206 mancount, advcount = self._unpackheader(_fpartparamcount)
1206 mancount, advcount = self._unpackheader(_fpartparamcount)
1207 indebug(self.ui, 'part parameters: %i' % (mancount + advcount))
1207 indebug(self.ui, 'part parameters: %i' % (mancount + advcount))
1208 # param size
1208 # param size
1209 fparamsizes = _makefpartparamsizes(mancount + advcount)
1209 fparamsizes = _makefpartparamsizes(mancount + advcount)
1210 paramsizes = self._unpackheader(fparamsizes)
1210 paramsizes = self._unpackheader(fparamsizes)
1211 # make it a list of couple again
1211 # make it a list of couple again
1212 paramsizes = zip(paramsizes[::2], paramsizes[1::2])
1212 paramsizes = zip(paramsizes[::2], paramsizes[1::2])
1213 # split mandatory from advisory
1213 # split mandatory from advisory
1214 mansizes = paramsizes[:mancount]
1214 mansizes = paramsizes[:mancount]
1215 advsizes = paramsizes[mancount:]
1215 advsizes = paramsizes[mancount:]
1216 # retrieve param value
1216 # retrieve param value
1217 manparams = []
1217 manparams = []
1218 for key, value in mansizes:
1218 for key, value in mansizes:
1219 manparams.append((self._fromheader(key), self._fromheader(value)))
1219 manparams.append((self._fromheader(key), self._fromheader(value)))
1220 advparams = []
1220 advparams = []
1221 for key, value in advsizes:
1221 for key, value in advsizes:
1222 advparams.append((self._fromheader(key), self._fromheader(value)))
1222 advparams.append((self._fromheader(key), self._fromheader(value)))
1223 self._initparams(manparams, advparams)
1223 self._initparams(manparams, advparams)
1224 ## part payload
1224 ## part payload
1225 self._payloadstream = util.chunkbuffer(self._payloadchunks())
1225 self._payloadstream = util.chunkbuffer(self._payloadchunks())
1226 # we read the data, tell it
1226 # we read the data, tell it
1227 self._initialized = True
1227 self._initialized = True
1228
1228
1229 def read(self, size=None):
1229 def read(self, size=None):
1230 """read payload data"""
1230 """read payload data"""
1231 if not self._initialized:
1231 if not self._initialized:
1232 self._readheader()
1232 self._readheader()
1233 if size is None:
1233 if size is None:
1234 data = self._payloadstream.read()
1234 data = self._payloadstream.read()
1235 else:
1235 else:
1236 data = self._payloadstream.read(size)
1236 data = self._payloadstream.read(size)
1237 self._pos += len(data)
1237 self._pos += len(data)
1238 if size is None or len(data) < size:
1238 if size is None or len(data) < size:
1239 if not self.consumed and self._pos:
1239 if not self.consumed and self._pos:
1240 self.ui.debug('bundle2-input-part: total payload size %i\n'
1240 self.ui.debug('bundle2-input-part: total payload size %i\n'
1241 % self._pos)
1241 % self._pos)
1242 self.consumed = True
1242 self.consumed = True
1243 return data
1243 return data
1244
1244
1245 def tell(self):
1245 def tell(self):
1246 return self._pos
1246 return self._pos
1247
1247
1248 def seek(self, offset, whence=0):
1248 def seek(self, offset, whence=0):
1249 if whence == 0:
1249 if whence == 0:
1250 newpos = offset
1250 newpos = offset
1251 elif whence == 1:
1251 elif whence == 1:
1252 newpos = self._pos + offset
1252 newpos = self._pos + offset
1253 elif whence == 2:
1253 elif whence == 2:
1254 if not self.consumed:
1254 if not self.consumed:
1255 self.read()
1255 self.read()
1256 newpos = self._chunkindex[-1][0] - offset
1256 newpos = self._chunkindex[-1][0] - offset
1257 else:
1257 else:
1258 raise ValueError('Unknown whence value: %r' % (whence,))
1258 raise ValueError('Unknown whence value: %r' % (whence,))
1259
1259
1260 if newpos > self._chunkindex[-1][0] and not self.consumed:
1260 if newpos > self._chunkindex[-1][0] and not self.consumed:
1261 self.read()
1261 self.read()
1262 if not 0 <= newpos <= self._chunkindex[-1][0]:
1262 if not 0 <= newpos <= self._chunkindex[-1][0]:
1263 raise ValueError('Offset out of range')
1263 raise ValueError('Offset out of range')
1264
1264
1265 if self._pos != newpos:
1265 if self._pos != newpos:
1266 chunk, internaloffset = self._findchunk(newpos)
1266 chunk, internaloffset = self._findchunk(newpos)
1267 self._payloadstream = util.chunkbuffer(self._payloadchunks(chunk))
1267 self._payloadstream = util.chunkbuffer(self._payloadchunks(chunk))
1268 adjust = self.read(internaloffset)
1268 adjust = self.read(internaloffset)
1269 if len(adjust) != internaloffset:
1269 if len(adjust) != internaloffset:
1270 raise error.Abort(_('Seek failed\n'))
1270 raise error.Abort(_('Seek failed\n'))
1271 self._pos = newpos
1271 self._pos = newpos
1272
1272
1273 def _seekfp(self, offset, whence=0):
1273 def _seekfp(self, offset, whence=0):
1274 """move the underlying file pointer
1274 """move the underlying file pointer
1275
1275
1276 This method is meant for internal usage by the bundle2 protocol only.
1276 This method is meant for internal usage by the bundle2 protocol only.
1277 They directly manipulate the low level stream including bundle2 level
1277 They directly manipulate the low level stream including bundle2 level
1278 instruction.
1278 instruction.
1279
1279
1280 Do not use it to implement higher-level logic or methods."""
1280 Do not use it to implement higher-level logic or methods."""
1281 if self._seekable:
1281 if self._seekable:
1282 return self._fp.seek(offset, whence)
1282 return self._fp.seek(offset, whence)
1283 else:
1283 else:
1284 raise NotImplementedError(_('File pointer is not seekable'))
1284 raise NotImplementedError(_('File pointer is not seekable'))
1285
1285
1286 def _tellfp(self):
1286 def _tellfp(self):
1287 """return the file offset, or None if file is not seekable
1287 """return the file offset, or None if file is not seekable
1288
1288
1289 This method is meant for internal usage by the bundle2 protocol only.
1289 This method is meant for internal usage by the bundle2 protocol only.
1290 They directly manipulate the low level stream including bundle2 level
1290 They directly manipulate the low level stream including bundle2 level
1291 instruction.
1291 instruction.
1292
1292
1293 Do not use it to implement higher-level logic or methods."""
1293 Do not use it to implement higher-level logic or methods."""
1294 if self._seekable:
1294 if self._seekable:
1295 try:
1295 try:
1296 return self._fp.tell()
1296 return self._fp.tell()
1297 except IOError as e:
1297 except IOError as e:
1298 if e.errno == errno.ESPIPE:
1298 if e.errno == errno.ESPIPE:
1299 self._seekable = False
1299 self._seekable = False
1300 else:
1300 else:
1301 raise
1301 raise
1302 return None
1302 return None
1303
1303
1304 # These are only the static capabilities.
1304 # These are only the static capabilities.
1305 # Check the 'getrepocaps' function for the rest.
1305 # Check the 'getrepocaps' function for the rest.
1306 capabilities = {'HG20': (),
1306 capabilities = {'HG20': (),
1307 'error': ('abort', 'unsupportedcontent', 'pushraced',
1307 'error': ('abort', 'unsupportedcontent', 'pushraced',
1308 'pushkey'),
1308 'pushkey'),
1309 'listkeys': (),
1309 'listkeys': (),
1310 'pushkey': (),
1310 'pushkey': (),
1311 'digests': tuple(sorted(util.DIGESTS.keys())),
1311 'digests': tuple(sorted(util.DIGESTS.keys())),
1312 'remote-changegroup': ('http', 'https'),
1312 'remote-changegroup': ('http', 'https'),
1313 'hgtagsfnodes': (),
1313 'hgtagsfnodes': (),
1314 }
1314 }
1315
1315
1316 def getrepocaps(repo, allowpushback=False):
1316 def getrepocaps(repo, allowpushback=False):
1317 """return the bundle2 capabilities for a given repo
1317 """return the bundle2 capabilities for a given repo
1318
1318
1319 Exists to allow extensions (like evolution) to mutate the capabilities.
1319 Exists to allow extensions (like evolution) to mutate the capabilities.
1320 """
1320 """
1321 caps = capabilities.copy()
1321 caps = capabilities.copy()
1322 caps['changegroup'] = tuple(sorted(
1322 caps['changegroup'] = tuple(sorted(
1323 changegroup.supportedincomingversions(repo)))
1323 changegroup.supportedincomingversions(repo)))
1324 if obsolete.isenabled(repo, obsolete.exchangeopt):
1324 if obsolete.isenabled(repo, obsolete.exchangeopt):
1325 supportedformat = tuple('V%i' % v for v in obsolete.formats)
1325 supportedformat = tuple('V%i' % v for v in obsolete.formats)
1326 caps['obsmarkers'] = supportedformat
1326 caps['obsmarkers'] = supportedformat
1327 if allowpushback:
1327 if allowpushback:
1328 caps['pushback'] = ()
1328 caps['pushback'] = ()
1329 return caps
1329 return caps
1330
1330
1331 def bundle2caps(remote):
1331 def bundle2caps(remote):
1332 """return the bundle capabilities of a peer as dict"""
1332 """return the bundle capabilities of a peer as dict"""
1333 raw = remote.capable('bundle2')
1333 raw = remote.capable('bundle2')
1334 if not raw and raw != '':
1334 if not raw and raw != '':
1335 return {}
1335 return {}
1336 capsblob = urlreq.unquote(remote.capable('bundle2'))
1336 capsblob = urlreq.unquote(remote.capable('bundle2'))
1337 return decodecaps(capsblob)
1337 return decodecaps(capsblob)
1338
1338
1339 def obsmarkersversion(caps):
1339 def obsmarkersversion(caps):
1340 """extract the list of supported obsmarkers versions from a bundle2caps dict
1340 """extract the list of supported obsmarkers versions from a bundle2caps dict
1341 """
1341 """
1342 obscaps = caps.get('obsmarkers', ())
1342 obscaps = caps.get('obsmarkers', ())
1343 return [int(c[1:]) for c in obscaps if c.startswith('V')]
1343 return [int(c[1:]) for c in obscaps if c.startswith('V')]
1344
1344
1345 def writebundle(ui, cg, filename, bundletype, vfs=None, compression=None,
1345 def writebundle(ui, cg, filename, bundletype, vfs=None, compression=None,
1346 compopts=None):
1346 compopts=None):
1347 """Write a bundle file and return its filename.
1347 """Write a bundle file and return its filename.
1348
1348
1349 Existing files will not be overwritten.
1349 Existing files will not be overwritten.
1350 If no filename is specified, a temporary file is created.
1350 If no filename is specified, a temporary file is created.
1351 bz2 compression can be turned off.
1351 bz2 compression can be turned off.
1352 The bundle file will be deleted in case of errors.
1352 The bundle file will be deleted in case of errors.
1353 """
1353 """
1354
1354
1355 if bundletype == "HG20":
1355 if bundletype == "HG20":
1356 bundle = bundle20(ui)
1356 bundle = bundle20(ui)
1357 bundle.setcompression(compression, compopts)
1357 bundle.setcompression(compression, compopts)
1358 part = bundle.newpart('changegroup', data=cg.getchunks())
1358 part = bundle.newpart('changegroup', data=cg.getchunks())
1359 part.addparam('version', cg.version)
1359 part.addparam('version', cg.version)
1360 if 'clcount' in cg.extras:
1360 if 'clcount' in cg.extras:
1361 part.addparam('nbchanges', str(cg.extras['clcount']),
1361 part.addparam('nbchanges', str(cg.extras['clcount']),
1362 mandatory=False)
1362 mandatory=False)
1363 chunkiter = bundle.getchunks()
1363 chunkiter = bundle.getchunks()
1364 else:
1364 else:
1365 # compression argument is only for the bundle2 case
1365 # compression argument is only for the bundle2 case
1366 assert compression is None
1366 assert compression is None
1367 if cg.version != '01':
1367 if cg.version != '01':
1368 raise error.Abort(_('old bundle types only supports v1 '
1368 raise error.Abort(_('old bundle types only supports v1 '
1369 'changegroups'))
1369 'changegroups'))
1370 header, comp = bundletypes[bundletype]
1370 header, comp = bundletypes[bundletype]
1371 if comp not in util.compengines.supportedbundletypes:
1371 if comp not in util.compengines.supportedbundletypes:
1372 raise error.Abort(_('unknown stream compression type: %s')
1372 raise error.Abort(_('unknown stream compression type: %s')
1373 % comp)
1373 % comp)
1374 compengine = util.compengines.forbundletype(comp)
1374 compengine = util.compengines.forbundletype(comp)
1375 def chunkiter():
1375 def chunkiter():
1376 yield header
1376 yield header
1377 for chunk in compengine.compressstream(cg.getchunks(), compopts):
1377 for chunk in compengine.compressstream(cg.getchunks(), compopts):
1378 yield chunk
1378 yield chunk
1379 chunkiter = chunkiter()
1379 chunkiter = chunkiter()
1380
1380
1381 # parse the changegroup data, otherwise we will block
1381 # parse the changegroup data, otherwise we will block
1382 # in case of sshrepo because we don't know the end of the stream
1382 # in case of sshrepo because we don't know the end of the stream
1383 return changegroup.writechunks(ui, chunkiter, filename, vfs=vfs)
1383 return changegroup.writechunks(ui, chunkiter, filename, vfs=vfs)
1384
1384
1385 @parthandler('changegroup', ('version', 'nbchanges', 'treemanifest'))
1385 @parthandler('changegroup', ('version', 'nbchanges', 'treemanifest'))
1386 def handlechangegroup(op, inpart):
1386 def handlechangegroup(op, inpart):
1387 """apply a changegroup part on the repo
1387 """apply a changegroup part on the repo
1388
1388
1389 This is a very early implementation that will massive rework before being
1389 This is a very early implementation that will massive rework before being
1390 inflicted to any end-user.
1390 inflicted to any end-user.
1391 """
1391 """
1392 # Make sure we trigger a transaction creation
1392 # Make sure we trigger a transaction creation
1393 #
1393 #
1394 # The addchangegroup function will get a transaction object by itself, but
1394 # The addchangegroup function will get a transaction object by itself, but
1395 # we need to make sure we trigger the creation of a transaction object used
1395 # we need to make sure we trigger the creation of a transaction object used
1396 # for the whole processing scope.
1396 # for the whole processing scope.
1397 op.gettransaction()
1397 op.gettransaction()
1398 unpackerversion = inpart.params.get('version', '01')
1398 unpackerversion = inpart.params.get('version', '01')
1399 # We should raise an appropriate exception here
1399 # We should raise an appropriate exception here
1400 cg = changegroup.getunbundler(unpackerversion, inpart, None)
1400 cg = changegroup.getunbundler(unpackerversion, inpart, None)
1401 # the source and url passed here are overwritten by the one contained in
1401 # the source and url passed here are overwritten by the one contained in
1402 # the transaction.hookargs argument. So 'bundle2' is a placeholder
1402 # the transaction.hookargs argument. So 'bundle2' is a placeholder
1403 nbchangesets = None
1403 nbchangesets = None
1404 if 'nbchanges' in inpart.params:
1404 if 'nbchanges' in inpart.params:
1405 nbchangesets = int(inpart.params.get('nbchanges'))
1405 nbchangesets = int(inpart.params.get('nbchanges'))
1406 if ('treemanifest' in inpart.params and
1406 if ('treemanifest' in inpart.params and
1407 'treemanifest' not in op.repo.requirements):
1407 'treemanifest' not in op.repo.requirements):
1408 if len(op.repo.changelog) != 0:
1408 if len(op.repo.changelog) != 0:
1409 raise error.Abort(_(
1409 raise error.Abort(_(
1410 "bundle contains tree manifests, but local repo is "
1410 "bundle contains tree manifests, but local repo is "
1411 "non-empty and does not use tree manifests"))
1411 "non-empty and does not use tree manifests"))
1412 op.repo.requirements.add('treemanifest')
1412 op.repo.requirements.add('treemanifest')
1413 op.repo._applyopenerreqs()
1413 op.repo._applyopenerreqs()
1414 op.repo._writerequirements()
1414 op.repo._writerequirements()
1415 ret = cg.apply(op.repo, 'bundle2', 'bundle2', expectedtotal=nbchangesets)
1415 ret = cg.apply(op.repo, 'bundle2', 'bundle2', expectedtotal=nbchangesets)
1416 op.records.add('changegroup', {'return': ret})
1416 op.records.add('changegroup', {'return': ret})
1417 if op.reply is not None:
1417 if op.reply is not None:
1418 # This is definitely not the final form of this
1418 # This is definitely not the final form of this
1419 # return. But one need to start somewhere.
1419 # return. But one need to start somewhere.
1420 part = op.reply.newpart('reply:changegroup', mandatory=False)
1420 part = op.reply.newpart('reply:changegroup', mandatory=False)
1421 part.addparam('in-reply-to', str(inpart.id), mandatory=False)
1421 part.addparam('in-reply-to', str(inpart.id), mandatory=False)
1422 part.addparam('return', '%i' % ret, mandatory=False)
1422 part.addparam('return', '%i' % ret, mandatory=False)
1423 assert not inpart.read()
1423 assert not inpart.read()
1424
1424
1425 _remotechangegroupparams = tuple(['url', 'size', 'digests'] +
1425 _remotechangegroupparams = tuple(['url', 'size', 'digests'] +
1426 ['digest:%s' % k for k in util.DIGESTS.keys()])
1426 ['digest:%s' % k for k in util.DIGESTS.keys()])
1427 @parthandler('remote-changegroup', _remotechangegroupparams)
1427 @parthandler('remote-changegroup', _remotechangegroupparams)
1428 def handleremotechangegroup(op, inpart):
1428 def handleremotechangegroup(op, inpart):
1429 """apply a bundle10 on the repo, given an url and validation information
1429 """apply a bundle10 on the repo, given an url and validation information
1430
1430
1431 All the information about the remote bundle to import are given as
1431 All the information about the remote bundle to import are given as
1432 parameters. The parameters include:
1432 parameters. The parameters include:
1433 - url: the url to the bundle10.
1433 - url: the url to the bundle10.
1434 - size: the bundle10 file size. It is used to validate what was
1434 - size: the bundle10 file size. It is used to validate what was
1435 retrieved by the client matches the server knowledge about the bundle.
1435 retrieved by the client matches the server knowledge about the bundle.
1436 - digests: a space separated list of the digest types provided as
1436 - digests: a space separated list of the digest types provided as
1437 parameters.
1437 parameters.
1438 - digest:<digest-type>: the hexadecimal representation of the digest with
1438 - digest:<digest-type>: the hexadecimal representation of the digest with
1439 that name. Like the size, it is used to validate what was retrieved by
1439 that name. Like the size, it is used to validate what was retrieved by
1440 the client matches what the server knows about the bundle.
1440 the client matches what the server knows about the bundle.
1441
1441
1442 When multiple digest types are given, all of them are checked.
1442 When multiple digest types are given, all of them are checked.
1443 """
1443 """
1444 try:
1444 try:
1445 raw_url = inpart.params['url']
1445 raw_url = inpart.params['url']
1446 except KeyError:
1446 except KeyError:
1447 raise error.Abort(_('remote-changegroup: missing "%s" param') % 'url')
1447 raise error.Abort(_('remote-changegroup: missing "%s" param') % 'url')
1448 parsed_url = util.url(raw_url)
1448 parsed_url = util.url(raw_url)
1449 if parsed_url.scheme not in capabilities['remote-changegroup']:
1449 if parsed_url.scheme not in capabilities['remote-changegroup']:
1450 raise error.Abort(_('remote-changegroup does not support %s urls') %
1450 raise error.Abort(_('remote-changegroup does not support %s urls') %
1451 parsed_url.scheme)
1451 parsed_url.scheme)
1452
1452
1453 try:
1453 try:
1454 size = int(inpart.params['size'])
1454 size = int(inpart.params['size'])
1455 except ValueError:
1455 except ValueError:
1456 raise error.Abort(_('remote-changegroup: invalid value for param "%s"')
1456 raise error.Abort(_('remote-changegroup: invalid value for param "%s"')
1457 % 'size')
1457 % 'size')
1458 except KeyError:
1458 except KeyError:
1459 raise error.Abort(_('remote-changegroup: missing "%s" param') % 'size')
1459 raise error.Abort(_('remote-changegroup: missing "%s" param') % 'size')
1460
1460
1461 digests = {}
1461 digests = {}
1462 for typ in inpart.params.get('digests', '').split():
1462 for typ in inpart.params.get('digests', '').split():
1463 param = 'digest:%s' % typ
1463 param = 'digest:%s' % typ
1464 try:
1464 try:
1465 value = inpart.params[param]
1465 value = inpart.params[param]
1466 except KeyError:
1466 except KeyError:
1467 raise error.Abort(_('remote-changegroup: missing "%s" param') %
1467 raise error.Abort(_('remote-changegroup: missing "%s" param') %
1468 param)
1468 param)
1469 digests[typ] = value
1469 digests[typ] = value
1470
1470
1471 real_part = util.digestchecker(url.open(op.ui, raw_url), size, digests)
1471 real_part = util.digestchecker(url.open(op.ui, raw_url), size, digests)
1472
1472
1473 # Make sure we trigger a transaction creation
1473 # Make sure we trigger a transaction creation
1474 #
1474 #
1475 # The addchangegroup function will get a transaction object by itself, but
1475 # The addchangegroup function will get a transaction object by itself, but
1476 # we need to make sure we trigger the creation of a transaction object used
1476 # we need to make sure we trigger the creation of a transaction object used
1477 # for the whole processing scope.
1477 # for the whole processing scope.
1478 op.gettransaction()
1478 op.gettransaction()
1479 from . import exchange
1479 from . import exchange
1480 cg = exchange.readbundle(op.repo.ui, real_part, raw_url)
1480 cg = exchange.readbundle(op.repo.ui, real_part, raw_url)
1481 if not isinstance(cg, changegroup.cg1unpacker):
1481 if not isinstance(cg, changegroup.cg1unpacker):
1482 raise error.Abort(_('%s: not a bundle version 1.0') %
1482 raise error.Abort(_('%s: not a bundle version 1.0') %
1483 util.hidepassword(raw_url))
1483 util.hidepassword(raw_url))
1484 ret = cg.apply(op.repo, 'bundle2', 'bundle2')
1484 ret = cg.apply(op.repo, 'bundle2', 'bundle2')
1485 op.records.add('changegroup', {'return': ret})
1485 op.records.add('changegroup', {'return': ret})
1486 if op.reply is not None:
1486 if op.reply is not None:
1487 # This is definitely not the final form of this
1487 # This is definitely not the final form of this
1488 # return. But one need to start somewhere.
1488 # return. But one need to start somewhere.
1489 part = op.reply.newpart('reply:changegroup')
1489 part = op.reply.newpart('reply:changegroup')
1490 part.addparam('in-reply-to', str(inpart.id), mandatory=False)
1490 part.addparam('in-reply-to', str(inpart.id), mandatory=False)
1491 part.addparam('return', '%i' % ret, mandatory=False)
1491 part.addparam('return', '%i' % ret, mandatory=False)
1492 try:
1492 try:
1493 real_part.validate()
1493 real_part.validate()
1494 except error.Abort as e:
1494 except error.Abort as e:
1495 raise error.Abort(_('bundle at %s is corrupted:\n%s') %
1495 raise error.Abort(_('bundle at %s is corrupted:\n%s') %
1496 (util.hidepassword(raw_url), str(e)))
1496 (util.hidepassword(raw_url), str(e)))
1497 assert not inpart.read()
1497 assert not inpart.read()
1498
1498
1499 @parthandler('reply:changegroup', ('return', 'in-reply-to'))
1499 @parthandler('reply:changegroup', ('return', 'in-reply-to'))
1500 def handlereplychangegroup(op, inpart):
1500 def handlereplychangegroup(op, inpart):
1501 ret = int(inpart.params['return'])
1501 ret = int(inpart.params['return'])
1502 replyto = int(inpart.params['in-reply-to'])
1502 replyto = int(inpart.params['in-reply-to'])
1503 op.records.add('changegroup', {'return': ret}, replyto)
1503 op.records.add('changegroup', {'return': ret}, replyto)
1504
1504
1505 @parthandler('check:heads')
1505 @parthandler('check:heads')
1506 def handlecheckheads(op, inpart):
1506 def handlecheckheads(op, inpart):
1507 """check that head of the repo did not change
1507 """check that head of the repo did not change
1508
1508
1509 This is used to detect a push race when using unbundle.
1509 This is used to detect a push race when using unbundle.
1510 This replaces the "heads" argument of unbundle."""
1510 This replaces the "heads" argument of unbundle."""
1511 h = inpart.read(20)
1511 h = inpart.read(20)
1512 heads = []
1512 heads = []
1513 while len(h) == 20:
1513 while len(h) == 20:
1514 heads.append(h)
1514 heads.append(h)
1515 h = inpart.read(20)
1515 h = inpart.read(20)
1516 assert not h
1516 assert not h
1517 # Trigger a transaction so that we are guaranteed to have the lock now.
1517 # Trigger a transaction so that we are guaranteed to have the lock now.
1518 if op.ui.configbool('experimental', 'bundle2lazylocking'):
1518 if op.ui.configbool('experimental', 'bundle2lazylocking'):
1519 op.gettransaction()
1519 op.gettransaction()
1520 if sorted(heads) != sorted(op.repo.heads()):
1520 if sorted(heads) != sorted(op.repo.heads()):
1521 raise error.PushRaced('repository changed while pushing - '
1521 raise error.PushRaced('repository changed while pushing - '
1522 'please try again')
1522 'please try again')
1523
1523
1524 @parthandler('output')
1524 @parthandler('output')
1525 def handleoutput(op, inpart):
1525 def handleoutput(op, inpart):
1526 """forward output captured on the server to the client"""
1526 """forward output captured on the server to the client"""
1527 for line in inpart.read().splitlines():
1527 for line in inpart.read().splitlines():
1528 op.ui.status(_('remote: %s\n') % line)
1528 op.ui.status(_('remote: %s\n') % line)
1529
1529
1530 @parthandler('replycaps')
1530 @parthandler('replycaps')
1531 def handlereplycaps(op, inpart):
1531 def handlereplycaps(op, inpart):
1532 """Notify that a reply bundle should be created
1532 """Notify that a reply bundle should be created
1533
1533
1534 The payload contains the capabilities information for the reply"""
1534 The payload contains the capabilities information for the reply"""
1535 caps = decodecaps(inpart.read())
1535 caps = decodecaps(inpart.read())
1536 if op.reply is None:
1536 if op.reply is None:
1537 op.reply = bundle20(op.ui, caps)
1537 op.reply = bundle20(op.ui, caps)
1538
1538
1539 class AbortFromPart(error.Abort):
1539 class AbortFromPart(error.Abort):
1540 """Sub-class of Abort that denotes an error from a bundle2 part."""
1540 """Sub-class of Abort that denotes an error from a bundle2 part."""
1541
1541
1542 @parthandler('error:abort', ('message', 'hint'))
1542 @parthandler('error:abort', ('message', 'hint'))
1543 def handleerrorabort(op, inpart):
1543 def handleerrorabort(op, inpart):
1544 """Used to transmit abort error over the wire"""
1544 """Used to transmit abort error over the wire"""
1545 raise AbortFromPart(inpart.params['message'],
1545 raise AbortFromPart(inpart.params['message'],
1546 hint=inpart.params.get('hint'))
1546 hint=inpart.params.get('hint'))
1547
1547
1548 @parthandler('error:pushkey', ('namespace', 'key', 'new', 'old', 'ret',
1548 @parthandler('error:pushkey', ('namespace', 'key', 'new', 'old', 'ret',
1549 'in-reply-to'))
1549 'in-reply-to'))
1550 def handleerrorpushkey(op, inpart):
1550 def handleerrorpushkey(op, inpart):
1551 """Used to transmit failure of a mandatory pushkey over the wire"""
1551 """Used to transmit failure of a mandatory pushkey over the wire"""
1552 kwargs = {}
1552 kwargs = {}
1553 for name in ('namespace', 'key', 'new', 'old', 'ret'):
1553 for name in ('namespace', 'key', 'new', 'old', 'ret'):
1554 value = inpart.params.get(name)
1554 value = inpart.params.get(name)
1555 if value is not None:
1555 if value is not None:
1556 kwargs[name] = value
1556 kwargs[name] = value
1557 raise error.PushkeyFailed(inpart.params['in-reply-to'], **kwargs)
1557 raise error.PushkeyFailed(inpart.params['in-reply-to'], **kwargs)
1558
1558
1559 @parthandler('error:unsupportedcontent', ('parttype', 'params'))
1559 @parthandler('error:unsupportedcontent', ('parttype', 'params'))
1560 def handleerrorunsupportedcontent(op, inpart):
1560 def handleerrorunsupportedcontent(op, inpart):
1561 """Used to transmit unknown content error over the wire"""
1561 """Used to transmit unknown content error over the wire"""
1562 kwargs = {}
1562 kwargs = {}
1563 parttype = inpart.params.get('parttype')
1563 parttype = inpart.params.get('parttype')
1564 if parttype is not None:
1564 if parttype is not None:
1565 kwargs['parttype'] = parttype
1565 kwargs['parttype'] = parttype
1566 params = inpart.params.get('params')
1566 params = inpart.params.get('params')
1567 if params is not None:
1567 if params is not None:
1568 kwargs['params'] = params.split('\0')
1568 kwargs['params'] = params.split('\0')
1569
1569
1570 raise error.BundleUnknownFeatureError(**kwargs)
1570 raise error.BundleUnknownFeatureError(**kwargs)
1571
1571
1572 @parthandler('error:pushraced', ('message',))
1572 @parthandler('error:pushraced', ('message',))
1573 def handleerrorpushraced(op, inpart):
1573 def handleerrorpushraced(op, inpart):
1574 """Used to transmit push race error over the wire"""
1574 """Used to transmit push race error over the wire"""
1575 raise error.ResponseError(_('push failed:'), inpart.params['message'])
1575 raise error.ResponseError(_('push failed:'), inpart.params['message'])
1576
1576
1577 @parthandler('listkeys', ('namespace',))
1577 @parthandler('listkeys', ('namespace',))
1578 def handlelistkeys(op, inpart):
1578 def handlelistkeys(op, inpart):
1579 """retrieve pushkey namespace content stored in a bundle2"""
1579 """retrieve pushkey namespace content stored in a bundle2"""
1580 namespace = inpart.params['namespace']
1580 namespace = inpart.params['namespace']
1581 r = pushkey.decodekeys(inpart.read())
1581 r = pushkey.decodekeys(inpart.read())
1582 op.records.add('listkeys', (namespace, r))
1582 op.records.add('listkeys', (namespace, r))
1583
1583
1584 @parthandler('pushkey', ('namespace', 'key', 'old', 'new'))
1584 @parthandler('pushkey', ('namespace', 'key', 'old', 'new'))
1585 def handlepushkey(op, inpart):
1585 def handlepushkey(op, inpart):
1586 """process a pushkey request"""
1586 """process a pushkey request"""
1587 dec = pushkey.decode
1587 dec = pushkey.decode
1588 namespace = dec(inpart.params['namespace'])
1588 namespace = dec(inpart.params['namespace'])
1589 key = dec(inpart.params['key'])
1589 key = dec(inpart.params['key'])
1590 old = dec(inpart.params['old'])
1590 old = dec(inpart.params['old'])
1591 new = dec(inpart.params['new'])
1591 new = dec(inpart.params['new'])
1592 # Grab the transaction to ensure that we have the lock before performing the
1592 # Grab the transaction to ensure that we have the lock before performing the
1593 # pushkey.
1593 # pushkey.
1594 if op.ui.configbool('experimental', 'bundle2lazylocking'):
1594 if op.ui.configbool('experimental', 'bundle2lazylocking'):
1595 op.gettransaction()
1595 op.gettransaction()
1596 ret = op.repo.pushkey(namespace, key, old, new)
1596 ret = op.repo.pushkey(namespace, key, old, new)
1597 record = {'namespace': namespace,
1597 record = {'namespace': namespace,
1598 'key': key,
1598 'key': key,
1599 'old': old,
1599 'old': old,
1600 'new': new}
1600 'new': new}
1601 op.records.add('pushkey', record)
1601 op.records.add('pushkey', record)
1602 if op.reply is not None:
1602 if op.reply is not None:
1603 rpart = op.reply.newpart('reply:pushkey')
1603 rpart = op.reply.newpart('reply:pushkey')
1604 rpart.addparam('in-reply-to', str(inpart.id), mandatory=False)
1604 rpart.addparam('in-reply-to', str(inpart.id), mandatory=False)
1605 rpart.addparam('return', '%i' % ret, mandatory=False)
1605 rpart.addparam('return', '%i' % ret, mandatory=False)
1606 if inpart.mandatory and not ret:
1606 if inpart.mandatory and not ret:
1607 kwargs = {}
1607 kwargs = {}
1608 for key in ('namespace', 'key', 'new', 'old', 'ret'):
1608 for key in ('namespace', 'key', 'new', 'old', 'ret'):
1609 if key in inpart.params:
1609 if key in inpart.params:
1610 kwargs[key] = inpart.params[key]
1610 kwargs[key] = inpart.params[key]
1611 raise error.PushkeyFailed(partid=str(inpart.id), **kwargs)
1611 raise error.PushkeyFailed(partid=str(inpart.id), **kwargs)
1612
1612
1613 @parthandler('reply:pushkey', ('return', 'in-reply-to'))
1613 @parthandler('reply:pushkey', ('return', 'in-reply-to'))
1614 def handlepushkeyreply(op, inpart):
1614 def handlepushkeyreply(op, inpart):
1615 """retrieve the result of a pushkey request"""
1615 """retrieve the result of a pushkey request"""
1616 ret = int(inpart.params['return'])
1616 ret = int(inpart.params['return'])
1617 partid = int(inpart.params['in-reply-to'])
1617 partid = int(inpart.params['in-reply-to'])
1618 op.records.add('pushkey', {'return': ret}, partid)
1618 op.records.add('pushkey', {'return': ret}, partid)
1619
1619
1620 @parthandler('obsmarkers')
1620 @parthandler('obsmarkers')
1621 def handleobsmarker(op, inpart):
1621 def handleobsmarker(op, inpart):
1622 """add a stream of obsmarkers to the repo"""
1622 """add a stream of obsmarkers to the repo"""
1623 tr = op.gettransaction()
1623 tr = op.gettransaction()
1624 markerdata = inpart.read()
1624 markerdata = inpart.read()
1625 if op.ui.config('experimental', 'obsmarkers-exchange-debug', False):
1625 if op.ui.config('experimental', 'obsmarkers-exchange-debug', False):
1626 op.ui.write(('obsmarker-exchange: %i bytes received\n')
1626 op.ui.write(('obsmarker-exchange: %i bytes received\n')
1627 % len(markerdata))
1627 % len(markerdata))
1628 # The mergemarkers call will crash if marker creation is not enabled.
1628 # The mergemarkers call will crash if marker creation is not enabled.
1629 # we want to avoid this if the part is advisory.
1629 # we want to avoid this if the part is advisory.
1630 if not inpart.mandatory and op.repo.obsstore.readonly:
1630 if not inpart.mandatory and op.repo.obsstore.readonly:
1631 op.repo.ui.debug('ignoring obsolescence markers, feature not enabled')
1631 op.repo.ui.debug('ignoring obsolescence markers, feature not enabled')
1632 return
1632 return
1633 new = op.repo.obsstore.mergemarkers(tr, markerdata)
1633 new = op.repo.obsstore.mergemarkers(tr, markerdata)
1634 op.repo.invalidatevolatilesets()
1634 if new:
1635 if new:
1635 op.repo.ui.status(_('%i new obsolescence markers\n') % new)
1636 op.repo.ui.status(_('%i new obsolescence markers\n') % new)
1636 op.records.add('obsmarkers', {'new': new})
1637 op.records.add('obsmarkers', {'new': new})
1637 if op.reply is not None:
1638 if op.reply is not None:
1638 rpart = op.reply.newpart('reply:obsmarkers')
1639 rpart = op.reply.newpart('reply:obsmarkers')
1639 rpart.addparam('in-reply-to', str(inpart.id), mandatory=False)
1640 rpart.addparam('in-reply-to', str(inpart.id), mandatory=False)
1640 rpart.addparam('new', '%i' % new, mandatory=False)
1641 rpart.addparam('new', '%i' % new, mandatory=False)
1641
1642
1642
1643
1643 @parthandler('reply:obsmarkers', ('new', 'in-reply-to'))
1644 @parthandler('reply:obsmarkers', ('new', 'in-reply-to'))
1644 def handleobsmarkerreply(op, inpart):
1645 def handleobsmarkerreply(op, inpart):
1645 """retrieve the result of a pushkey request"""
1646 """retrieve the result of a pushkey request"""
1646 ret = int(inpart.params['new'])
1647 ret = int(inpart.params['new'])
1647 partid = int(inpart.params['in-reply-to'])
1648 partid = int(inpart.params['in-reply-to'])
1648 op.records.add('obsmarkers', {'new': ret}, partid)
1649 op.records.add('obsmarkers', {'new': ret}, partid)
1649
1650
1650 @parthandler('hgtagsfnodes')
1651 @parthandler('hgtagsfnodes')
1651 def handlehgtagsfnodes(op, inpart):
1652 def handlehgtagsfnodes(op, inpart):
1652 """Applies .hgtags fnodes cache entries to the local repo.
1653 """Applies .hgtags fnodes cache entries to the local repo.
1653
1654
1654 Payload is pairs of 20 byte changeset nodes and filenodes.
1655 Payload is pairs of 20 byte changeset nodes and filenodes.
1655 """
1656 """
1656 # Grab the transaction so we ensure that we have the lock at this point.
1657 # Grab the transaction so we ensure that we have the lock at this point.
1657 if op.ui.configbool('experimental', 'bundle2lazylocking'):
1658 if op.ui.configbool('experimental', 'bundle2lazylocking'):
1658 op.gettransaction()
1659 op.gettransaction()
1659 cache = tags.hgtagsfnodescache(op.repo.unfiltered())
1660 cache = tags.hgtagsfnodescache(op.repo.unfiltered())
1660
1661
1661 count = 0
1662 count = 0
1662 while True:
1663 while True:
1663 node = inpart.read(20)
1664 node = inpart.read(20)
1664 fnode = inpart.read(20)
1665 fnode = inpart.read(20)
1665 if len(node) < 20 or len(fnode) < 20:
1666 if len(node) < 20 or len(fnode) < 20:
1666 op.ui.debug('ignoring incomplete received .hgtags fnodes data\n')
1667 op.ui.debug('ignoring incomplete received .hgtags fnodes data\n')
1667 break
1668 break
1668 cache.setfnode(node, fnode)
1669 cache.setfnode(node, fnode)
1669 count += 1
1670 count += 1
1670
1671
1671 cache.write()
1672 cache.write()
1672 op.ui.debug('applied %i hgtags fnodes cache entries\n' % count)
1673 op.ui.debug('applied %i hgtags fnodes cache entries\n' % count)
@@ -1,1285 +1,1286 b''
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 is used:
45 - When changeset A is split into B and C, a single marker is used:
46
46
47 (A, (B, C))
47 (A, (B, 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 from __future__ import absolute_import
70 from __future__ import absolute_import
71
71
72 import errno
72 import errno
73 import struct
73 import struct
74
74
75 from .i18n import _
75 from .i18n import _
76 from . import (
76 from . import (
77 base85,
77 base85,
78 error,
78 error,
79 node,
79 node,
80 parsers,
80 parsers,
81 phases,
81 phases,
82 util,
82 util,
83 )
83 )
84
84
85 _pack = struct.pack
85 _pack = struct.pack
86 _unpack = struct.unpack
86 _unpack = struct.unpack
87 _calcsize = struct.calcsize
87 _calcsize = struct.calcsize
88 propertycache = util.propertycache
88 propertycache = util.propertycache
89
89
90 # the obsolete feature is not mature enough to be enabled by default.
90 # the obsolete feature is not mature enough to be enabled by default.
91 # you have to rely on third party extension extension to enable this.
91 # you have to rely on third party extension extension to enable this.
92 _enabled = False
92 _enabled = False
93
93
94 # Options for obsolescence
94 # Options for obsolescence
95 createmarkersopt = 'createmarkers'
95 createmarkersopt = 'createmarkers'
96 allowunstableopt = 'allowunstable'
96 allowunstableopt = 'allowunstable'
97 exchangeopt = 'exchange'
97 exchangeopt = 'exchange'
98
98
99 ### obsolescence marker flag
99 ### obsolescence marker flag
100
100
101 ## bumpedfix flag
101 ## bumpedfix flag
102 #
102 #
103 # When a changeset A' succeed to a changeset A which became public, we call A'
103 # When a changeset A' succeed to a changeset A which became public, we call A'
104 # "bumped" because it's a successors of a public changesets
104 # "bumped" because it's a successors of a public changesets
105 #
105 #
106 # o A' (bumped)
106 # o A' (bumped)
107 # |`:
107 # |`:
108 # | o A
108 # | o A
109 # |/
109 # |/
110 # o Z
110 # o Z
111 #
111 #
112 # The way to solve this situation is to create a new changeset Ad as children
112 # The way to solve this situation is to create a new changeset Ad as children
113 # of A. This changeset have the same content than A'. So the diff from A to A'
113 # of A. This changeset have the same content than A'. So the diff from A to A'
114 # is the same than the diff from A to Ad. Ad is marked as a successors of A'
114 # is the same than the diff from A to Ad. Ad is marked as a successors of A'
115 #
115 #
116 # o Ad
116 # o Ad
117 # |`:
117 # |`:
118 # | x A'
118 # | x A'
119 # |'|
119 # |'|
120 # o | A
120 # o | A
121 # |/
121 # |/
122 # o Z
122 # o Z
123 #
123 #
124 # But by transitivity Ad is also a successors of A. To avoid having Ad marked
124 # But by transitivity Ad is also a successors of A. To avoid having Ad marked
125 # as bumped too, we add the `bumpedfix` flag to the marker. <A', (Ad,)>.
125 # as bumped too, we add the `bumpedfix` flag to the marker. <A', (Ad,)>.
126 # This flag mean that the successors express the changes between the public and
126 # This flag mean that the successors express the changes between the public and
127 # bumped version and fix the situation, breaking the transitivity of
127 # bumped version and fix the situation, breaking the transitivity of
128 # "bumped" here.
128 # "bumped" here.
129 bumpedfix = 1
129 bumpedfix = 1
130 usingsha256 = 2
130 usingsha256 = 2
131
131
132 ## Parsing and writing of version "0"
132 ## Parsing and writing of version "0"
133 #
133 #
134 # The header is followed by the markers. Each marker is made of:
134 # The header is followed by the markers. Each marker is made of:
135 #
135 #
136 # - 1 uint8 : number of new changesets "N", can be zero.
136 # - 1 uint8 : number of new changesets "N", can be zero.
137 #
137 #
138 # - 1 uint32: metadata size "M" in bytes.
138 # - 1 uint32: metadata size "M" in bytes.
139 #
139 #
140 # - 1 byte: a bit field. It is reserved for flags used in common
140 # - 1 byte: a bit field. It is reserved for flags used in common
141 # obsolete marker operations, to avoid repeated decoding of metadata
141 # obsolete marker operations, to avoid repeated decoding of metadata
142 # entries.
142 # entries.
143 #
143 #
144 # - 20 bytes: obsoleted changeset identifier.
144 # - 20 bytes: obsoleted changeset identifier.
145 #
145 #
146 # - N*20 bytes: new changesets identifiers.
146 # - N*20 bytes: new changesets identifiers.
147 #
147 #
148 # - M bytes: metadata as a sequence of nul-terminated strings. Each
148 # - M bytes: metadata as a sequence of nul-terminated strings. Each
149 # string contains a key and a value, separated by a colon ':', without
149 # string contains a key and a value, separated by a colon ':', without
150 # additional encoding. Keys cannot contain '\0' or ':' and values
150 # additional encoding. Keys cannot contain '\0' or ':' and values
151 # cannot contain '\0'.
151 # cannot contain '\0'.
152 _fm0version = 0
152 _fm0version = 0
153 _fm0fixed = '>BIB20s'
153 _fm0fixed = '>BIB20s'
154 _fm0node = '20s'
154 _fm0node = '20s'
155 _fm0fsize = _calcsize(_fm0fixed)
155 _fm0fsize = _calcsize(_fm0fixed)
156 _fm0fnodesize = _calcsize(_fm0node)
156 _fm0fnodesize = _calcsize(_fm0node)
157
157
158 def _fm0readmarkers(data, off):
158 def _fm0readmarkers(data, off):
159 # Loop on markers
159 # Loop on markers
160 l = len(data)
160 l = len(data)
161 while off + _fm0fsize <= l:
161 while off + _fm0fsize <= l:
162 # read fixed part
162 # read fixed part
163 cur = data[off:off + _fm0fsize]
163 cur = data[off:off + _fm0fsize]
164 off += _fm0fsize
164 off += _fm0fsize
165 numsuc, mdsize, flags, pre = _unpack(_fm0fixed, cur)
165 numsuc, mdsize, flags, pre = _unpack(_fm0fixed, cur)
166 # read replacement
166 # read replacement
167 sucs = ()
167 sucs = ()
168 if numsuc:
168 if numsuc:
169 s = (_fm0fnodesize * numsuc)
169 s = (_fm0fnodesize * numsuc)
170 cur = data[off:off + s]
170 cur = data[off:off + s]
171 sucs = _unpack(_fm0node * numsuc, cur)
171 sucs = _unpack(_fm0node * numsuc, cur)
172 off += s
172 off += s
173 # read metadata
173 # read metadata
174 # (metadata will be decoded on demand)
174 # (metadata will be decoded on demand)
175 metadata = data[off:off + mdsize]
175 metadata = data[off:off + mdsize]
176 if len(metadata) != mdsize:
176 if len(metadata) != mdsize:
177 raise error.Abort(_('parsing obsolete marker: metadata is too '
177 raise error.Abort(_('parsing obsolete marker: metadata is too '
178 'short, %d bytes expected, got %d')
178 'short, %d bytes expected, got %d')
179 % (mdsize, len(metadata)))
179 % (mdsize, len(metadata)))
180 off += mdsize
180 off += mdsize
181 metadata = _fm0decodemeta(metadata)
181 metadata = _fm0decodemeta(metadata)
182 try:
182 try:
183 when, offset = metadata.pop('date', '0 0').split(' ')
183 when, offset = metadata.pop('date', '0 0').split(' ')
184 date = float(when), int(offset)
184 date = float(when), int(offset)
185 except ValueError:
185 except ValueError:
186 date = (0., 0)
186 date = (0., 0)
187 parents = None
187 parents = None
188 if 'p2' in metadata:
188 if 'p2' in metadata:
189 parents = (metadata.pop('p1', None), metadata.pop('p2', None))
189 parents = (metadata.pop('p1', None), metadata.pop('p2', None))
190 elif 'p1' in metadata:
190 elif 'p1' in metadata:
191 parents = (metadata.pop('p1', None),)
191 parents = (metadata.pop('p1', None),)
192 elif 'p0' in metadata:
192 elif 'p0' in metadata:
193 parents = ()
193 parents = ()
194 if parents is not None:
194 if parents is not None:
195 try:
195 try:
196 parents = tuple(node.bin(p) for p in parents)
196 parents = tuple(node.bin(p) for p in parents)
197 # if parent content is not a nodeid, drop the data
197 # if parent content is not a nodeid, drop the data
198 for p in parents:
198 for p in parents:
199 if len(p) != 20:
199 if len(p) != 20:
200 parents = None
200 parents = None
201 break
201 break
202 except TypeError:
202 except TypeError:
203 # if content cannot be translated to nodeid drop the data.
203 # if content cannot be translated to nodeid drop the data.
204 parents = None
204 parents = None
205
205
206 metadata = tuple(sorted(metadata.iteritems()))
206 metadata = tuple(sorted(metadata.iteritems()))
207
207
208 yield (pre, sucs, flags, metadata, date, parents)
208 yield (pre, sucs, flags, metadata, date, parents)
209
209
210 def _fm0encodeonemarker(marker):
210 def _fm0encodeonemarker(marker):
211 pre, sucs, flags, metadata, date, parents = marker
211 pre, sucs, flags, metadata, date, parents = marker
212 if flags & usingsha256:
212 if flags & usingsha256:
213 raise error.Abort(_('cannot handle sha256 with old obsstore format'))
213 raise error.Abort(_('cannot handle sha256 with old obsstore format'))
214 metadata = dict(metadata)
214 metadata = dict(metadata)
215 time, tz = date
215 time, tz = date
216 metadata['date'] = '%r %i' % (time, tz)
216 metadata['date'] = '%r %i' % (time, tz)
217 if parents is not None:
217 if parents is not None:
218 if not parents:
218 if not parents:
219 # mark that we explicitly recorded no parents
219 # mark that we explicitly recorded no parents
220 metadata['p0'] = ''
220 metadata['p0'] = ''
221 for i, p in enumerate(parents):
221 for i, p in enumerate(parents):
222 metadata['p%i' % (i + 1)] = node.hex(p)
222 metadata['p%i' % (i + 1)] = node.hex(p)
223 metadata = _fm0encodemeta(metadata)
223 metadata = _fm0encodemeta(metadata)
224 numsuc = len(sucs)
224 numsuc = len(sucs)
225 format = _fm0fixed + (_fm0node * numsuc)
225 format = _fm0fixed + (_fm0node * numsuc)
226 data = [numsuc, len(metadata), flags, pre]
226 data = [numsuc, len(metadata), flags, pre]
227 data.extend(sucs)
227 data.extend(sucs)
228 return _pack(format, *data) + metadata
228 return _pack(format, *data) + metadata
229
229
230 def _fm0encodemeta(meta):
230 def _fm0encodemeta(meta):
231 """Return encoded metadata string to string mapping.
231 """Return encoded metadata string to string mapping.
232
232
233 Assume no ':' in key and no '\0' in both key and value."""
233 Assume no ':' in key and no '\0' in both key and value."""
234 for key, value in meta.iteritems():
234 for key, value in meta.iteritems():
235 if ':' in key or '\0' in key:
235 if ':' in key or '\0' in key:
236 raise ValueError("':' and '\0' are forbidden in metadata key'")
236 raise ValueError("':' and '\0' are forbidden in metadata key'")
237 if '\0' in value:
237 if '\0' in value:
238 raise ValueError("':' is forbidden in metadata value'")
238 raise ValueError("':' is forbidden in metadata value'")
239 return '\0'.join(['%s:%s' % (k, meta[k]) for k in sorted(meta)])
239 return '\0'.join(['%s:%s' % (k, meta[k]) for k in sorted(meta)])
240
240
241 def _fm0decodemeta(data):
241 def _fm0decodemeta(data):
242 """Return string to string dictionary from encoded version."""
242 """Return string to string dictionary from encoded version."""
243 d = {}
243 d = {}
244 for l in data.split('\0'):
244 for l in data.split('\0'):
245 if l:
245 if l:
246 key, value = l.split(':')
246 key, value = l.split(':')
247 d[key] = value
247 d[key] = value
248 return d
248 return d
249
249
250 ## Parsing and writing of version "1"
250 ## Parsing and writing of version "1"
251 #
251 #
252 # The header is followed by the markers. Each marker is made of:
252 # The header is followed by the markers. Each marker is made of:
253 #
253 #
254 # - uint32: total size of the marker (including this field)
254 # - uint32: total size of the marker (including this field)
255 #
255 #
256 # - float64: date in seconds since epoch
256 # - float64: date in seconds since epoch
257 #
257 #
258 # - int16: timezone offset in minutes
258 # - int16: timezone offset in minutes
259 #
259 #
260 # - uint16: a bit field. It is reserved for flags used in common
260 # - uint16: a bit field. It is reserved for flags used in common
261 # obsolete marker operations, to avoid repeated decoding of metadata
261 # obsolete marker operations, to avoid repeated decoding of metadata
262 # entries.
262 # entries.
263 #
263 #
264 # - uint8: number of successors "N", can be zero.
264 # - uint8: number of successors "N", can be zero.
265 #
265 #
266 # - uint8: number of parents "P", can be zero.
266 # - uint8: number of parents "P", can be zero.
267 #
267 #
268 # 0: parents data stored but no parent,
268 # 0: parents data stored but no parent,
269 # 1: one parent stored,
269 # 1: one parent stored,
270 # 2: two parents stored,
270 # 2: two parents stored,
271 # 3: no parent data stored
271 # 3: no parent data stored
272 #
272 #
273 # - uint8: number of metadata entries M
273 # - uint8: number of metadata entries M
274 #
274 #
275 # - 20 or 32 bytes: precursor changeset identifier.
275 # - 20 or 32 bytes: precursor changeset identifier.
276 #
276 #
277 # - N*(20 or 32) bytes: successors changesets identifiers.
277 # - N*(20 or 32) bytes: successors changesets identifiers.
278 #
278 #
279 # - P*(20 or 32) bytes: parents of the precursors changesets.
279 # - P*(20 or 32) bytes: parents of the precursors changesets.
280 #
280 #
281 # - M*(uint8, uint8): size of all metadata entries (key and value)
281 # - M*(uint8, uint8): size of all metadata entries (key and value)
282 #
282 #
283 # - remaining bytes: the metadata, each (key, value) pair after the other.
283 # - remaining bytes: the metadata, each (key, value) pair after the other.
284 _fm1version = 1
284 _fm1version = 1
285 _fm1fixed = '>IdhHBBB20s'
285 _fm1fixed = '>IdhHBBB20s'
286 _fm1nodesha1 = '20s'
286 _fm1nodesha1 = '20s'
287 _fm1nodesha256 = '32s'
287 _fm1nodesha256 = '32s'
288 _fm1nodesha1size = _calcsize(_fm1nodesha1)
288 _fm1nodesha1size = _calcsize(_fm1nodesha1)
289 _fm1nodesha256size = _calcsize(_fm1nodesha256)
289 _fm1nodesha256size = _calcsize(_fm1nodesha256)
290 _fm1fsize = _calcsize(_fm1fixed)
290 _fm1fsize = _calcsize(_fm1fixed)
291 _fm1parentnone = 3
291 _fm1parentnone = 3
292 _fm1parentshift = 14
292 _fm1parentshift = 14
293 _fm1parentmask = (_fm1parentnone << _fm1parentshift)
293 _fm1parentmask = (_fm1parentnone << _fm1parentshift)
294 _fm1metapair = 'BB'
294 _fm1metapair = 'BB'
295 _fm1metapairsize = _calcsize('BB')
295 _fm1metapairsize = _calcsize('BB')
296
296
297 def _fm1purereadmarkers(data, off):
297 def _fm1purereadmarkers(data, off):
298 # make some global constants local for performance
298 # make some global constants local for performance
299 noneflag = _fm1parentnone
299 noneflag = _fm1parentnone
300 sha2flag = usingsha256
300 sha2flag = usingsha256
301 sha1size = _fm1nodesha1size
301 sha1size = _fm1nodesha1size
302 sha2size = _fm1nodesha256size
302 sha2size = _fm1nodesha256size
303 sha1fmt = _fm1nodesha1
303 sha1fmt = _fm1nodesha1
304 sha2fmt = _fm1nodesha256
304 sha2fmt = _fm1nodesha256
305 metasize = _fm1metapairsize
305 metasize = _fm1metapairsize
306 metafmt = _fm1metapair
306 metafmt = _fm1metapair
307 fsize = _fm1fsize
307 fsize = _fm1fsize
308 unpack = _unpack
308 unpack = _unpack
309
309
310 # Loop on markers
310 # Loop on markers
311 stop = len(data) - _fm1fsize
311 stop = len(data) - _fm1fsize
312 ufixed = struct.Struct(_fm1fixed).unpack
312 ufixed = struct.Struct(_fm1fixed).unpack
313
313
314 while off <= stop:
314 while off <= stop:
315 # read fixed part
315 # read fixed part
316 o1 = off + fsize
316 o1 = off + fsize
317 t, secs, tz, flags, numsuc, numpar, nummeta, prec = ufixed(data[off:o1])
317 t, secs, tz, flags, numsuc, numpar, nummeta, prec = ufixed(data[off:o1])
318
318
319 if flags & sha2flag:
319 if flags & sha2flag:
320 # FIXME: prec was read as a SHA1, needs to be amended
320 # FIXME: prec was read as a SHA1, needs to be amended
321
321
322 # read 0 or more successors
322 # read 0 or more successors
323 if numsuc == 1:
323 if numsuc == 1:
324 o2 = o1 + sha2size
324 o2 = o1 + sha2size
325 sucs = (data[o1:o2],)
325 sucs = (data[o1:o2],)
326 else:
326 else:
327 o2 = o1 + sha2size * numsuc
327 o2 = o1 + sha2size * numsuc
328 sucs = unpack(sha2fmt * numsuc, data[o1:o2])
328 sucs = unpack(sha2fmt * numsuc, data[o1:o2])
329
329
330 # read parents
330 # read parents
331 if numpar == noneflag:
331 if numpar == noneflag:
332 o3 = o2
332 o3 = o2
333 parents = None
333 parents = None
334 elif numpar == 1:
334 elif numpar == 1:
335 o3 = o2 + sha2size
335 o3 = o2 + sha2size
336 parents = (data[o2:o3],)
336 parents = (data[o2:o3],)
337 else:
337 else:
338 o3 = o2 + sha2size * numpar
338 o3 = o2 + sha2size * numpar
339 parents = unpack(sha2fmt * numpar, data[o2:o3])
339 parents = unpack(sha2fmt * numpar, data[o2:o3])
340 else:
340 else:
341 # read 0 or more successors
341 # read 0 or more successors
342 if numsuc == 1:
342 if numsuc == 1:
343 o2 = o1 + sha1size
343 o2 = o1 + sha1size
344 sucs = (data[o1:o2],)
344 sucs = (data[o1:o2],)
345 else:
345 else:
346 o2 = o1 + sha1size * numsuc
346 o2 = o1 + sha1size * numsuc
347 sucs = unpack(sha1fmt * numsuc, data[o1:o2])
347 sucs = unpack(sha1fmt * numsuc, data[o1:o2])
348
348
349 # read parents
349 # read parents
350 if numpar == noneflag:
350 if numpar == noneflag:
351 o3 = o2
351 o3 = o2
352 parents = None
352 parents = None
353 elif numpar == 1:
353 elif numpar == 1:
354 o3 = o2 + sha1size
354 o3 = o2 + sha1size
355 parents = (data[o2:o3],)
355 parents = (data[o2:o3],)
356 else:
356 else:
357 o3 = o2 + sha1size * numpar
357 o3 = o2 + sha1size * numpar
358 parents = unpack(sha1fmt * numpar, data[o2:o3])
358 parents = unpack(sha1fmt * numpar, data[o2:o3])
359
359
360 # read metadata
360 # read metadata
361 off = o3 + metasize * nummeta
361 off = o3 + metasize * nummeta
362 metapairsize = unpack('>' + (metafmt * nummeta), data[o3:off])
362 metapairsize = unpack('>' + (metafmt * nummeta), data[o3:off])
363 metadata = []
363 metadata = []
364 for idx in xrange(0, len(metapairsize), 2):
364 for idx in xrange(0, len(metapairsize), 2):
365 o1 = off + metapairsize[idx]
365 o1 = off + metapairsize[idx]
366 o2 = o1 + metapairsize[idx + 1]
366 o2 = o1 + metapairsize[idx + 1]
367 metadata.append((data[off:o1], data[o1:o2]))
367 metadata.append((data[off:o1], data[o1:o2]))
368 off = o2
368 off = o2
369
369
370 yield (prec, sucs, flags, tuple(metadata), (secs, tz * 60), parents)
370 yield (prec, sucs, flags, tuple(metadata), (secs, tz * 60), parents)
371
371
372 def _fm1encodeonemarker(marker):
372 def _fm1encodeonemarker(marker):
373 pre, sucs, flags, metadata, date, parents = marker
373 pre, sucs, flags, metadata, date, parents = marker
374 # determine node size
374 # determine node size
375 _fm1node = _fm1nodesha1
375 _fm1node = _fm1nodesha1
376 if flags & usingsha256:
376 if flags & usingsha256:
377 _fm1node = _fm1nodesha256
377 _fm1node = _fm1nodesha256
378 numsuc = len(sucs)
378 numsuc = len(sucs)
379 numextranodes = numsuc
379 numextranodes = numsuc
380 if parents is None:
380 if parents is None:
381 numpar = _fm1parentnone
381 numpar = _fm1parentnone
382 else:
382 else:
383 numpar = len(parents)
383 numpar = len(parents)
384 numextranodes += numpar
384 numextranodes += numpar
385 formatnodes = _fm1node * numextranodes
385 formatnodes = _fm1node * numextranodes
386 formatmeta = _fm1metapair * len(metadata)
386 formatmeta = _fm1metapair * len(metadata)
387 format = _fm1fixed + formatnodes + formatmeta
387 format = _fm1fixed + formatnodes + formatmeta
388 # tz is stored in minutes so we divide by 60
388 # tz is stored in minutes so we divide by 60
389 tz = date[1]//60
389 tz = date[1]//60
390 data = [None, date[0], tz, flags, numsuc, numpar, len(metadata), pre]
390 data = [None, date[0], tz, flags, numsuc, numpar, len(metadata), pre]
391 data.extend(sucs)
391 data.extend(sucs)
392 if parents is not None:
392 if parents is not None:
393 data.extend(parents)
393 data.extend(parents)
394 totalsize = _calcsize(format)
394 totalsize = _calcsize(format)
395 for key, value in metadata:
395 for key, value in metadata:
396 lk = len(key)
396 lk = len(key)
397 lv = len(value)
397 lv = len(value)
398 data.append(lk)
398 data.append(lk)
399 data.append(lv)
399 data.append(lv)
400 totalsize += lk + lv
400 totalsize += lk + lv
401 data[0] = totalsize
401 data[0] = totalsize
402 data = [_pack(format, *data)]
402 data = [_pack(format, *data)]
403 for key, value in metadata:
403 for key, value in metadata:
404 data.append(key)
404 data.append(key)
405 data.append(value)
405 data.append(value)
406 return ''.join(data)
406 return ''.join(data)
407
407
408 def _fm1readmarkers(data, off):
408 def _fm1readmarkers(data, off):
409 native = getattr(parsers, 'fm1readmarkers', None)
409 native = getattr(parsers, 'fm1readmarkers', None)
410 if not native:
410 if not native:
411 return _fm1purereadmarkers(data, off)
411 return _fm1purereadmarkers(data, off)
412 stop = len(data) - _fm1fsize
412 stop = len(data) - _fm1fsize
413 return native(data, off, stop)
413 return native(data, off, stop)
414
414
415 # mapping to read/write various marker formats
415 # mapping to read/write various marker formats
416 # <version> -> (decoder, encoder)
416 # <version> -> (decoder, encoder)
417 formats = {_fm0version: (_fm0readmarkers, _fm0encodeonemarker),
417 formats = {_fm0version: (_fm0readmarkers, _fm0encodeonemarker),
418 _fm1version: (_fm1readmarkers, _fm1encodeonemarker)}
418 _fm1version: (_fm1readmarkers, _fm1encodeonemarker)}
419
419
420 @util.nogc
420 @util.nogc
421 def _readmarkers(data):
421 def _readmarkers(data):
422 """Read and enumerate markers from raw data"""
422 """Read and enumerate markers from raw data"""
423 off = 0
423 off = 0
424 diskversion = _unpack('>B', data[off:off + 1])[0]
424 diskversion = _unpack('>B', data[off:off + 1])[0]
425 off += 1
425 off += 1
426 if diskversion not in formats:
426 if diskversion not in formats:
427 raise error.Abort(_('parsing obsolete marker: unknown version %r')
427 raise error.Abort(_('parsing obsolete marker: unknown version %r')
428 % diskversion)
428 % diskversion)
429 return diskversion, formats[diskversion][0](data, off)
429 return diskversion, formats[diskversion][0](data, off)
430
430
431 def encodemarkers(markers, addheader=False, version=_fm0version):
431 def encodemarkers(markers, addheader=False, version=_fm0version):
432 # Kept separate from flushmarkers(), it will be reused for
432 # Kept separate from flushmarkers(), it will be reused for
433 # markers exchange.
433 # markers exchange.
434 encodeone = formats[version][1]
434 encodeone = formats[version][1]
435 if addheader:
435 if addheader:
436 yield _pack('>B', version)
436 yield _pack('>B', version)
437 for marker in markers:
437 for marker in markers:
438 yield encodeone(marker)
438 yield encodeone(marker)
439
439
440
440
441 class marker(object):
441 class marker(object):
442 """Wrap obsolete marker raw data"""
442 """Wrap obsolete marker raw data"""
443
443
444 def __init__(self, repo, data):
444 def __init__(self, repo, data):
445 # the repo argument will be used to create changectx in later version
445 # the repo argument will be used to create changectx in later version
446 self._repo = repo
446 self._repo = repo
447 self._data = data
447 self._data = data
448 self._decodedmeta = None
448 self._decodedmeta = None
449
449
450 def __hash__(self):
450 def __hash__(self):
451 return hash(self._data)
451 return hash(self._data)
452
452
453 def __eq__(self, other):
453 def __eq__(self, other):
454 if type(other) != type(self):
454 if type(other) != type(self):
455 return False
455 return False
456 return self._data == other._data
456 return self._data == other._data
457
457
458 def precnode(self):
458 def precnode(self):
459 """Precursor changeset node identifier"""
459 """Precursor changeset node identifier"""
460 return self._data[0]
460 return self._data[0]
461
461
462 def succnodes(self):
462 def succnodes(self):
463 """List of successor changesets node identifiers"""
463 """List of successor changesets node identifiers"""
464 return self._data[1]
464 return self._data[1]
465
465
466 def parentnodes(self):
466 def parentnodes(self):
467 """Parents of the precursors (None if not recorded)"""
467 """Parents of the precursors (None if not recorded)"""
468 return self._data[5]
468 return self._data[5]
469
469
470 def metadata(self):
470 def metadata(self):
471 """Decoded metadata dictionary"""
471 """Decoded metadata dictionary"""
472 return dict(self._data[3])
472 return dict(self._data[3])
473
473
474 def date(self):
474 def date(self):
475 """Creation date as (unixtime, offset)"""
475 """Creation date as (unixtime, offset)"""
476 return self._data[4]
476 return self._data[4]
477
477
478 def flags(self):
478 def flags(self):
479 """The flags field of the marker"""
479 """The flags field of the marker"""
480 return self._data[2]
480 return self._data[2]
481
481
482 @util.nogc
482 @util.nogc
483 def _addsuccessors(successors, markers):
483 def _addsuccessors(successors, markers):
484 for mark in markers:
484 for mark in markers:
485 successors.setdefault(mark[0], set()).add(mark)
485 successors.setdefault(mark[0], set()).add(mark)
486
486
487 @util.nogc
487 @util.nogc
488 def _addprecursors(precursors, markers):
488 def _addprecursors(precursors, markers):
489 for mark in markers:
489 for mark in markers:
490 for suc in mark[1]:
490 for suc in mark[1]:
491 precursors.setdefault(suc, set()).add(mark)
491 precursors.setdefault(suc, set()).add(mark)
492
492
493 @util.nogc
493 @util.nogc
494 def _addchildren(children, markers):
494 def _addchildren(children, markers):
495 for mark in markers:
495 for mark in markers:
496 parents = mark[5]
496 parents = mark[5]
497 if parents is not None:
497 if parents is not None:
498 for p in parents:
498 for p in parents:
499 children.setdefault(p, set()).add(mark)
499 children.setdefault(p, set()).add(mark)
500
500
501 def _checkinvalidmarkers(markers):
501 def _checkinvalidmarkers(markers):
502 """search for marker with invalid data and raise error if needed
502 """search for marker with invalid data and raise error if needed
503
503
504 Exist as a separated function to allow the evolve extension for a more
504 Exist as a separated function to allow the evolve extension for a more
505 subtle handling.
505 subtle handling.
506 """
506 """
507 for mark in markers:
507 for mark in markers:
508 if node.nullid in mark[1]:
508 if node.nullid in mark[1]:
509 raise error.Abort(_('bad obsolescence marker detected: '
509 raise error.Abort(_('bad obsolescence marker detected: '
510 'invalid successors nullid'))
510 'invalid successors nullid'))
511
511
512 class obsstore(object):
512 class obsstore(object):
513 """Store obsolete markers
513 """Store obsolete markers
514
514
515 Markers can be accessed with two mappings:
515 Markers can be accessed with two mappings:
516 - precursors[x] -> set(markers on precursors edges of x)
516 - precursors[x] -> set(markers on precursors edges of x)
517 - successors[x] -> set(markers on successors edges of x)
517 - successors[x] -> set(markers on successors edges of x)
518 - children[x] -> set(markers on precursors edges of children(x)
518 - children[x] -> set(markers on precursors edges of children(x)
519 """
519 """
520
520
521 fields = ('prec', 'succs', 'flag', 'meta', 'date', 'parents')
521 fields = ('prec', 'succs', 'flag', 'meta', 'date', 'parents')
522 # prec: nodeid, precursor changesets
522 # prec: nodeid, precursor changesets
523 # succs: tuple of nodeid, successor changesets (0-N length)
523 # succs: tuple of nodeid, successor changesets (0-N length)
524 # flag: integer, flag field carrying modifier for the markers (see doc)
524 # flag: integer, flag field carrying modifier for the markers (see doc)
525 # meta: binary blob, encoded metadata dictionary
525 # meta: binary blob, encoded metadata dictionary
526 # date: (float, int) tuple, date of marker creation
526 # date: (float, int) tuple, date of marker creation
527 # parents: (tuple of nodeid) or None, parents of precursors
527 # parents: (tuple of nodeid) or None, parents of precursors
528 # None is used when no data has been recorded
528 # None is used when no data has been recorded
529
529
530 def __init__(self, svfs, defaultformat=_fm1version, readonly=False):
530 def __init__(self, svfs, defaultformat=_fm1version, readonly=False):
531 # caches for various obsolescence related cache
531 # caches for various obsolescence related cache
532 self.caches = {}
532 self.caches = {}
533 self.svfs = svfs
533 self.svfs = svfs
534 self._version = defaultformat
534 self._version = defaultformat
535 self._readonly = readonly
535 self._readonly = readonly
536
536
537 def __iter__(self):
537 def __iter__(self):
538 return iter(self._all)
538 return iter(self._all)
539
539
540 def __len__(self):
540 def __len__(self):
541 return len(self._all)
541 return len(self._all)
542
542
543 def __nonzero__(self):
543 def __nonzero__(self):
544 if not self._cached('_all'):
544 if not self._cached('_all'):
545 try:
545 try:
546 return self.svfs.stat('obsstore').st_size > 1
546 return self.svfs.stat('obsstore').st_size > 1
547 except OSError as inst:
547 except OSError as inst:
548 if inst.errno != errno.ENOENT:
548 if inst.errno != errno.ENOENT:
549 raise
549 raise
550 # just build an empty _all list if no obsstore exists, which
550 # just build an empty _all list if no obsstore exists, which
551 # avoids further stat() syscalls
551 # avoids further stat() syscalls
552 pass
552 pass
553 return bool(self._all)
553 return bool(self._all)
554
554
555 __bool__ = __nonzero__
555 __bool__ = __nonzero__
556
556
557 @property
557 @property
558 def readonly(self):
558 def readonly(self):
559 """True if marker creation is disabled
559 """True if marker creation is disabled
560
560
561 Remove me in the future when obsolete marker is always on."""
561 Remove me in the future when obsolete marker is always on."""
562 return self._readonly
562 return self._readonly
563
563
564 def create(self, transaction, prec, succs=(), flag=0, parents=None,
564 def create(self, transaction, prec, succs=(), flag=0, parents=None,
565 date=None, metadata=None):
565 date=None, metadata=None):
566 """obsolete: add a new obsolete marker
566 """obsolete: add a new obsolete marker
567
567
568 * ensuring it is hashable
568 * ensuring it is hashable
569 * check mandatory metadata
569 * check mandatory metadata
570 * encode metadata
570 * encode metadata
571
571
572 If you are a human writing code creating marker you want to use the
572 If you are a human writing code creating marker you want to use the
573 `createmarkers` function in this module instead.
573 `createmarkers` function in this module instead.
574
574
575 return True if a new marker have been added, False if the markers
575 return True if a new marker have been added, False if the markers
576 already existed (no op).
576 already existed (no op).
577 """
577 """
578 if metadata is None:
578 if metadata is None:
579 metadata = {}
579 metadata = {}
580 if date is None:
580 if date is None:
581 if 'date' in metadata:
581 if 'date' in metadata:
582 # as a courtesy for out-of-tree extensions
582 # as a courtesy for out-of-tree extensions
583 date = util.parsedate(metadata.pop('date'))
583 date = util.parsedate(metadata.pop('date'))
584 else:
584 else:
585 date = util.makedate()
585 date = util.makedate()
586 if len(prec) != 20:
586 if len(prec) != 20:
587 raise ValueError(prec)
587 raise ValueError(prec)
588 for succ in succs:
588 for succ in succs:
589 if len(succ) != 20:
589 if len(succ) != 20:
590 raise ValueError(succ)
590 raise ValueError(succ)
591 if prec in succs:
591 if prec in succs:
592 raise ValueError(_('in-marker cycle with %s') % node.hex(prec))
592 raise ValueError(_('in-marker cycle with %s') % node.hex(prec))
593
593
594 metadata = tuple(sorted(metadata.iteritems()))
594 metadata = tuple(sorted(metadata.iteritems()))
595
595
596 marker = (str(prec), tuple(succs), int(flag), metadata, date, parents)
596 marker = (str(prec), tuple(succs), int(flag), metadata, date, parents)
597 return bool(self.add(transaction, [marker]))
597 return bool(self.add(transaction, [marker]))
598
598
599 def add(self, transaction, markers):
599 def add(self, transaction, markers):
600 """Add new markers to the store
600 """Add new markers to the store
601
601
602 Take care of filtering duplicate.
602 Take care of filtering duplicate.
603 Return the number of new marker."""
603 Return the number of new marker."""
604 if self._readonly:
604 if self._readonly:
605 raise error.Abort(_('creating obsolete markers is not enabled on '
605 raise error.Abort(_('creating obsolete markers is not enabled on '
606 'this repo'))
606 'this repo'))
607 known = set(self._all)
607 known = set(self._all)
608 new = []
608 new = []
609 for m in markers:
609 for m in markers:
610 if m not in known:
610 if m not in known:
611 known.add(m)
611 known.add(m)
612 new.append(m)
612 new.append(m)
613 if new:
613 if new:
614 f = self.svfs('obsstore', 'ab')
614 f = self.svfs('obsstore', 'ab')
615 try:
615 try:
616 offset = f.tell()
616 offset = f.tell()
617 transaction.add('obsstore', offset)
617 transaction.add('obsstore', offset)
618 # offset == 0: new file - add the version header
618 # offset == 0: new file - add the version header
619 for bytes in encodemarkers(new, offset == 0, self._version):
619 for bytes in encodemarkers(new, offset == 0, self._version):
620 f.write(bytes)
620 f.write(bytes)
621 finally:
621 finally:
622 # XXX: f.close() == filecache invalidation == obsstore rebuilt.
622 # XXX: f.close() == filecache invalidation == obsstore rebuilt.
623 # call 'filecacheentry.refresh()' here
623 # call 'filecacheentry.refresh()' here
624 f.close()
624 f.close()
625 self._addmarkers(new)
625 self._addmarkers(new)
626 # new marker *may* have changed several set. invalidate the cache.
626 # new marker *may* have changed several set. invalidate the cache.
627 self.caches.clear()
627 self.caches.clear()
628 # records the number of new markers for the transaction hooks
628 # records the number of new markers for the transaction hooks
629 previous = int(transaction.hookargs.get('new_obsmarkers', '0'))
629 previous = int(transaction.hookargs.get('new_obsmarkers', '0'))
630 transaction.hookargs['new_obsmarkers'] = str(previous + len(new))
630 transaction.hookargs['new_obsmarkers'] = str(previous + len(new))
631 return len(new)
631 return len(new)
632
632
633 def mergemarkers(self, transaction, data):
633 def mergemarkers(self, transaction, data):
634 """merge a binary stream of markers inside the obsstore
634 """merge a binary stream of markers inside the obsstore
635
635
636 Returns the number of new markers added."""
636 Returns the number of new markers added."""
637 version, markers = _readmarkers(data)
637 version, markers = _readmarkers(data)
638 return self.add(transaction, markers)
638 return self.add(transaction, markers)
639
639
640 @propertycache
640 @propertycache
641 def _all(self):
641 def _all(self):
642 data = self.svfs.tryread('obsstore')
642 data = self.svfs.tryread('obsstore')
643 if not data:
643 if not data:
644 return []
644 return []
645 self._version, markers = _readmarkers(data)
645 self._version, markers = _readmarkers(data)
646 markers = list(markers)
646 markers = list(markers)
647 _checkinvalidmarkers(markers)
647 _checkinvalidmarkers(markers)
648 return markers
648 return markers
649
649
650 @propertycache
650 @propertycache
651 def successors(self):
651 def successors(self):
652 successors = {}
652 successors = {}
653 _addsuccessors(successors, self._all)
653 _addsuccessors(successors, self._all)
654 return successors
654 return successors
655
655
656 @propertycache
656 @propertycache
657 def precursors(self):
657 def precursors(self):
658 precursors = {}
658 precursors = {}
659 _addprecursors(precursors, self._all)
659 _addprecursors(precursors, self._all)
660 return precursors
660 return precursors
661
661
662 @propertycache
662 @propertycache
663 def children(self):
663 def children(self):
664 children = {}
664 children = {}
665 _addchildren(children, self._all)
665 _addchildren(children, self._all)
666 return children
666 return children
667
667
668 def _cached(self, attr):
668 def _cached(self, attr):
669 return attr in self.__dict__
669 return attr in self.__dict__
670
670
671 def _addmarkers(self, markers):
671 def _addmarkers(self, markers):
672 markers = list(markers) # to allow repeated iteration
672 markers = list(markers) # to allow repeated iteration
673 self._all.extend(markers)
673 self._all.extend(markers)
674 if self._cached('successors'):
674 if self._cached('successors'):
675 _addsuccessors(self.successors, markers)
675 _addsuccessors(self.successors, markers)
676 if self._cached('precursors'):
676 if self._cached('precursors'):
677 _addprecursors(self.precursors, markers)
677 _addprecursors(self.precursors, markers)
678 if self._cached('children'):
678 if self._cached('children'):
679 _addchildren(self.children, markers)
679 _addchildren(self.children, markers)
680 _checkinvalidmarkers(markers)
680 _checkinvalidmarkers(markers)
681
681
682 def relevantmarkers(self, nodes):
682 def relevantmarkers(self, nodes):
683 """return a set of all obsolescence markers relevant to a set of nodes.
683 """return a set of all obsolescence markers relevant to a set of nodes.
684
684
685 "relevant" to a set of nodes mean:
685 "relevant" to a set of nodes mean:
686
686
687 - marker that use this changeset as successor
687 - marker that use this changeset as successor
688 - prune marker of direct children on this changeset
688 - prune marker of direct children on this changeset
689 - recursive application of the two rules on precursors of these markers
689 - recursive application of the two rules on precursors of these markers
690
690
691 It is a set so you cannot rely on order."""
691 It is a set so you cannot rely on order."""
692
692
693 pendingnodes = set(nodes)
693 pendingnodes = set(nodes)
694 seenmarkers = set()
694 seenmarkers = set()
695 seennodes = set(pendingnodes)
695 seennodes = set(pendingnodes)
696 precursorsmarkers = self.precursors
696 precursorsmarkers = self.precursors
697 children = self.children
697 children = self.children
698 while pendingnodes:
698 while pendingnodes:
699 direct = set()
699 direct = set()
700 for current in pendingnodes:
700 for current in pendingnodes:
701 direct.update(precursorsmarkers.get(current, ()))
701 direct.update(precursorsmarkers.get(current, ()))
702 pruned = [m for m in children.get(current, ()) if not m[1]]
702 pruned = [m for m in children.get(current, ()) if not m[1]]
703 direct.update(pruned)
703 direct.update(pruned)
704 direct -= seenmarkers
704 direct -= seenmarkers
705 pendingnodes = set([m[0] for m in direct])
705 pendingnodes = set([m[0] for m in direct])
706 seenmarkers |= direct
706 seenmarkers |= direct
707 pendingnodes -= seennodes
707 pendingnodes -= seennodes
708 seennodes |= pendingnodes
708 seennodes |= pendingnodes
709 return seenmarkers
709 return seenmarkers
710
710
711 def commonversion(versions):
711 def commonversion(versions):
712 """Return the newest version listed in both versions and our local formats.
712 """Return the newest version listed in both versions and our local formats.
713
713
714 Returns None if no common version exists.
714 Returns None if no common version exists.
715 """
715 """
716 versions.sort(reverse=True)
716 versions.sort(reverse=True)
717 # search for highest version known on both side
717 # search for highest version known on both side
718 for v in versions:
718 for v in versions:
719 if v in formats:
719 if v in formats:
720 return v
720 return v
721 return None
721 return None
722
722
723 # arbitrary picked to fit into 8K limit from HTTP server
723 # arbitrary picked to fit into 8K limit from HTTP server
724 # you have to take in account:
724 # you have to take in account:
725 # - the version header
725 # - the version header
726 # - the base85 encoding
726 # - the base85 encoding
727 _maxpayload = 5300
727 _maxpayload = 5300
728
728
729 def _pushkeyescape(markers):
729 def _pushkeyescape(markers):
730 """encode markers into a dict suitable for pushkey exchange
730 """encode markers into a dict suitable for pushkey exchange
731
731
732 - binary data is base85 encoded
732 - binary data is base85 encoded
733 - split in chunks smaller than 5300 bytes"""
733 - split in chunks smaller than 5300 bytes"""
734 keys = {}
734 keys = {}
735 parts = []
735 parts = []
736 currentlen = _maxpayload * 2 # ensure we create a new part
736 currentlen = _maxpayload * 2 # ensure we create a new part
737 for marker in markers:
737 for marker in markers:
738 nextdata = _fm0encodeonemarker(marker)
738 nextdata = _fm0encodeonemarker(marker)
739 if (len(nextdata) + currentlen > _maxpayload):
739 if (len(nextdata) + currentlen > _maxpayload):
740 currentpart = []
740 currentpart = []
741 currentlen = 0
741 currentlen = 0
742 parts.append(currentpart)
742 parts.append(currentpart)
743 currentpart.append(nextdata)
743 currentpart.append(nextdata)
744 currentlen += len(nextdata)
744 currentlen += len(nextdata)
745 for idx, part in enumerate(reversed(parts)):
745 for idx, part in enumerate(reversed(parts)):
746 data = ''.join([_pack('>B', _fm0version)] + part)
746 data = ''.join([_pack('>B', _fm0version)] + part)
747 keys['dump%i' % idx] = base85.b85encode(data)
747 keys['dump%i' % idx] = base85.b85encode(data)
748 return keys
748 return keys
749
749
750 def listmarkers(repo):
750 def listmarkers(repo):
751 """List markers over pushkey"""
751 """List markers over pushkey"""
752 if not repo.obsstore:
752 if not repo.obsstore:
753 return {}
753 return {}
754 return _pushkeyescape(sorted(repo.obsstore))
754 return _pushkeyescape(sorted(repo.obsstore))
755
755
756 def pushmarker(repo, key, old, new):
756 def pushmarker(repo, key, old, new):
757 """Push markers over pushkey"""
757 """Push markers over pushkey"""
758 if not key.startswith('dump'):
758 if not key.startswith('dump'):
759 repo.ui.warn(_('unknown key: %r') % key)
759 repo.ui.warn(_('unknown key: %r') % key)
760 return 0
760 return 0
761 if old:
761 if old:
762 repo.ui.warn(_('unexpected old value for %r') % key)
762 repo.ui.warn(_('unexpected old value for %r') % key)
763 return 0
763 return 0
764 data = base85.b85decode(new)
764 data = base85.b85decode(new)
765 lock = repo.lock()
765 lock = repo.lock()
766 try:
766 try:
767 tr = repo.transaction('pushkey: obsolete markers')
767 tr = repo.transaction('pushkey: obsolete markers')
768 try:
768 try:
769 repo.obsstore.mergemarkers(tr, data)
769 repo.obsstore.mergemarkers(tr, data)
770 repo.invalidatevolatilesets()
770 tr.close()
771 tr.close()
771 return 1
772 return 1
772 finally:
773 finally:
773 tr.release()
774 tr.release()
774 finally:
775 finally:
775 lock.release()
776 lock.release()
776
777
777 def getmarkers(repo, nodes=None):
778 def getmarkers(repo, nodes=None):
778 """returns markers known in a repository
779 """returns markers known in a repository
779
780
780 If <nodes> is specified, only markers "relevant" to those nodes are are
781 If <nodes> is specified, only markers "relevant" to those nodes are are
781 returned"""
782 returned"""
782 if nodes is None:
783 if nodes is None:
783 rawmarkers = repo.obsstore
784 rawmarkers = repo.obsstore
784 else:
785 else:
785 rawmarkers = repo.obsstore.relevantmarkers(nodes)
786 rawmarkers = repo.obsstore.relevantmarkers(nodes)
786
787
787 for markerdata in rawmarkers:
788 for markerdata in rawmarkers:
788 yield marker(repo, markerdata)
789 yield marker(repo, markerdata)
789
790
790 def relevantmarkers(repo, node):
791 def relevantmarkers(repo, node):
791 """all obsolete markers relevant to some revision"""
792 """all obsolete markers relevant to some revision"""
792 for markerdata in repo.obsstore.relevantmarkers(node):
793 for markerdata in repo.obsstore.relevantmarkers(node):
793 yield marker(repo, markerdata)
794 yield marker(repo, markerdata)
794
795
795
796
796 def precursormarkers(ctx):
797 def precursormarkers(ctx):
797 """obsolete marker marking this changeset as a successors"""
798 """obsolete marker marking this changeset as a successors"""
798 for data in ctx.repo().obsstore.precursors.get(ctx.node(), ()):
799 for data in ctx.repo().obsstore.precursors.get(ctx.node(), ()):
799 yield marker(ctx.repo(), data)
800 yield marker(ctx.repo(), data)
800
801
801 def successormarkers(ctx):
802 def successormarkers(ctx):
802 """obsolete marker making this changeset obsolete"""
803 """obsolete marker making this changeset obsolete"""
803 for data in ctx.repo().obsstore.successors.get(ctx.node(), ()):
804 for data in ctx.repo().obsstore.successors.get(ctx.node(), ()):
804 yield marker(ctx.repo(), data)
805 yield marker(ctx.repo(), data)
805
806
806 def allsuccessors(obsstore, nodes, ignoreflags=0):
807 def allsuccessors(obsstore, nodes, ignoreflags=0):
807 """Yield node for every successor of <nodes>.
808 """Yield node for every successor of <nodes>.
808
809
809 Some successors may be unknown locally.
810 Some successors may be unknown locally.
810
811
811 This is a linear yield unsuited to detecting split changesets. It includes
812 This is a linear yield unsuited to detecting split changesets. It includes
812 initial nodes too."""
813 initial nodes too."""
813 remaining = set(nodes)
814 remaining = set(nodes)
814 seen = set(remaining)
815 seen = set(remaining)
815 while remaining:
816 while remaining:
816 current = remaining.pop()
817 current = remaining.pop()
817 yield current
818 yield current
818 for mark in obsstore.successors.get(current, ()):
819 for mark in obsstore.successors.get(current, ()):
819 # ignore marker flagged with specified flag
820 # ignore marker flagged with specified flag
820 if mark[2] & ignoreflags:
821 if mark[2] & ignoreflags:
821 continue
822 continue
822 for suc in mark[1]:
823 for suc in mark[1]:
823 if suc not in seen:
824 if suc not in seen:
824 seen.add(suc)
825 seen.add(suc)
825 remaining.add(suc)
826 remaining.add(suc)
826
827
827 def allprecursors(obsstore, nodes, ignoreflags=0):
828 def allprecursors(obsstore, nodes, ignoreflags=0):
828 """Yield node for every precursors of <nodes>.
829 """Yield node for every precursors of <nodes>.
829
830
830 Some precursors may be unknown locally.
831 Some precursors may be unknown locally.
831
832
832 This is a linear yield unsuited to detecting folded changesets. It includes
833 This is a linear yield unsuited to detecting folded changesets. It includes
833 initial nodes too."""
834 initial nodes too."""
834
835
835 remaining = set(nodes)
836 remaining = set(nodes)
836 seen = set(remaining)
837 seen = set(remaining)
837 while remaining:
838 while remaining:
838 current = remaining.pop()
839 current = remaining.pop()
839 yield current
840 yield current
840 for mark in obsstore.precursors.get(current, ()):
841 for mark in obsstore.precursors.get(current, ()):
841 # ignore marker flagged with specified flag
842 # ignore marker flagged with specified flag
842 if mark[2] & ignoreflags:
843 if mark[2] & ignoreflags:
843 continue
844 continue
844 suc = mark[0]
845 suc = mark[0]
845 if suc not in seen:
846 if suc not in seen:
846 seen.add(suc)
847 seen.add(suc)
847 remaining.add(suc)
848 remaining.add(suc)
848
849
849 def foreground(repo, nodes):
850 def foreground(repo, nodes):
850 """return all nodes in the "foreground" of other node
851 """return all nodes in the "foreground" of other node
851
852
852 The foreground of a revision is anything reachable using parent -> children
853 The foreground of a revision is anything reachable using parent -> children
853 or precursor -> successor relation. It is very similar to "descendant" but
854 or precursor -> successor relation. It is very similar to "descendant" but
854 augmented with obsolescence information.
855 augmented with obsolescence information.
855
856
856 Beware that possible obsolescence cycle may result if complex situation.
857 Beware that possible obsolescence cycle may result if complex situation.
857 """
858 """
858 repo = repo.unfiltered()
859 repo = repo.unfiltered()
859 foreground = set(repo.set('%ln::', nodes))
860 foreground = set(repo.set('%ln::', nodes))
860 if repo.obsstore:
861 if repo.obsstore:
861 # We only need this complicated logic if there is obsolescence
862 # We only need this complicated logic if there is obsolescence
862 # XXX will probably deserve an optimised revset.
863 # XXX will probably deserve an optimised revset.
863 nm = repo.changelog.nodemap
864 nm = repo.changelog.nodemap
864 plen = -1
865 plen = -1
865 # compute the whole set of successors or descendants
866 # compute the whole set of successors or descendants
866 while len(foreground) != plen:
867 while len(foreground) != plen:
867 plen = len(foreground)
868 plen = len(foreground)
868 succs = set(c.node() for c in foreground)
869 succs = set(c.node() for c in foreground)
869 mutable = [c.node() for c in foreground if c.mutable()]
870 mutable = [c.node() for c in foreground if c.mutable()]
870 succs.update(allsuccessors(repo.obsstore, mutable))
871 succs.update(allsuccessors(repo.obsstore, mutable))
871 known = (n for n in succs if n in nm)
872 known = (n for n in succs if n in nm)
872 foreground = set(repo.set('%ln::', known))
873 foreground = set(repo.set('%ln::', known))
873 return set(c.node() for c in foreground)
874 return set(c.node() for c in foreground)
874
875
875
876
876 def successorssets(repo, initialnode, cache=None):
877 def successorssets(repo, initialnode, cache=None):
877 """Return set of all latest successors of initial nodes
878 """Return set of all latest successors of initial nodes
878
879
879 The successors set of a changeset A are the group of revisions that succeed
880 The successors set of a changeset A are the group of revisions that succeed
880 A. It succeeds A as a consistent whole, each revision being only a partial
881 A. It succeeds A as a consistent whole, each revision being only a partial
881 replacement. The successors set contains non-obsolete changesets only.
882 replacement. The successors set contains non-obsolete changesets only.
882
883
883 This function returns the full list of successor sets which is why it
884 This function returns the full list of successor sets which is why it
884 returns a list of tuples and not just a single tuple. Each tuple is a valid
885 returns a list of tuples and not just a single tuple. Each tuple is a valid
885 successors set. Note that (A,) may be a valid successors set for changeset A
886 successors set. Note that (A,) may be a valid successors set for changeset A
886 (see below).
887 (see below).
887
888
888 In most cases, a changeset A will have a single element (e.g. the changeset
889 In most cases, a changeset A will have a single element (e.g. the changeset
889 A is replaced by A') in its successors set. Though, it is also common for a
890 A is replaced by A') in its successors set. Though, it is also common for a
890 changeset A to have no elements in its successor set (e.g. the changeset
891 changeset A to have no elements in its successor set (e.g. the changeset
891 has been pruned). Therefore, the returned list of successors sets will be
892 has been pruned). Therefore, the returned list of successors sets will be
892 [(A',)] or [], respectively.
893 [(A',)] or [], respectively.
893
894
894 When a changeset A is split into A' and B', however, it will result in a
895 When a changeset A is split into A' and B', however, it will result in a
895 successors set containing more than a single element, i.e. [(A',B')].
896 successors set containing more than a single element, i.e. [(A',B')].
896 Divergent changesets will result in multiple successors sets, i.e. [(A',),
897 Divergent changesets will result in multiple successors sets, i.e. [(A',),
897 (A'')].
898 (A'')].
898
899
899 If a changeset A is not obsolete, then it will conceptually have no
900 If a changeset A is not obsolete, then it will conceptually have no
900 successors set. To distinguish this from a pruned changeset, the successor
901 successors set. To distinguish this from a pruned changeset, the successor
901 set will contain itself only, i.e. [(A,)].
902 set will contain itself only, i.e. [(A,)].
902
903
903 Finally, successors unknown locally are considered to be pruned (obsoleted
904 Finally, successors unknown locally are considered to be pruned (obsoleted
904 without any successors).
905 without any successors).
905
906
906 The optional `cache` parameter is a dictionary that may contain precomputed
907 The optional `cache` parameter is a dictionary that may contain precomputed
907 successors sets. It is meant to reuse the computation of a previous call to
908 successors sets. It is meant to reuse the computation of a previous call to
908 `successorssets` when multiple calls are made at the same time. The cache
909 `successorssets` when multiple calls are made at the same time. The cache
909 dictionary is updated in place. The caller is responsible for its life
910 dictionary is updated in place. The caller is responsible for its life
910 span. Code that makes multiple calls to `successorssets` *must* use this
911 span. Code that makes multiple calls to `successorssets` *must* use this
911 cache mechanism or suffer terrible performance.
912 cache mechanism or suffer terrible performance.
912 """
913 """
913
914
914 succmarkers = repo.obsstore.successors
915 succmarkers = repo.obsstore.successors
915
916
916 # Stack of nodes we search successors sets for
917 # Stack of nodes we search successors sets for
917 toproceed = [initialnode]
918 toproceed = [initialnode]
918 # set version of above list for fast loop detection
919 # set version of above list for fast loop detection
919 # element added to "toproceed" must be added here
920 # element added to "toproceed" must be added here
920 stackedset = set(toproceed)
921 stackedset = set(toproceed)
921 if cache is None:
922 if cache is None:
922 cache = {}
923 cache = {}
923
924
924 # This while loop is the flattened version of a recursive search for
925 # This while loop is the flattened version of a recursive search for
925 # successors sets
926 # successors sets
926 #
927 #
927 # def successorssets(x):
928 # def successorssets(x):
928 # successors = directsuccessors(x)
929 # successors = directsuccessors(x)
929 # ss = [[]]
930 # ss = [[]]
930 # for succ in directsuccessors(x):
931 # for succ in directsuccessors(x):
931 # # product as in itertools cartesian product
932 # # product as in itertools cartesian product
932 # ss = product(ss, successorssets(succ))
933 # ss = product(ss, successorssets(succ))
933 # return ss
934 # return ss
934 #
935 #
935 # But we can not use plain recursive calls here:
936 # But we can not use plain recursive calls here:
936 # - that would blow the python call stack
937 # - that would blow the python call stack
937 # - obsolescence markers may have cycles, we need to handle them.
938 # - obsolescence markers may have cycles, we need to handle them.
938 #
939 #
939 # The `toproceed` list act as our call stack. Every node we search
940 # The `toproceed` list act as our call stack. Every node we search
940 # successors set for are stacked there.
941 # successors set for are stacked there.
941 #
942 #
942 # The `stackedset` is set version of this stack used to check if a node is
943 # The `stackedset` is set version of this stack used to check if a node is
943 # already stacked. This check is used to detect cycles and prevent infinite
944 # already stacked. This check is used to detect cycles and prevent infinite
944 # loop.
945 # loop.
945 #
946 #
946 # successors set of all nodes are stored in the `cache` dictionary.
947 # successors set of all nodes are stored in the `cache` dictionary.
947 #
948 #
948 # After this while loop ends we use the cache to return the successors sets
949 # After this while loop ends we use the cache to return the successors sets
949 # for the node requested by the caller.
950 # for the node requested by the caller.
950 while toproceed:
951 while toproceed:
951 # Every iteration tries to compute the successors sets of the topmost
952 # Every iteration tries to compute the successors sets of the topmost
952 # node of the stack: CURRENT.
953 # node of the stack: CURRENT.
953 #
954 #
954 # There are four possible outcomes:
955 # There are four possible outcomes:
955 #
956 #
956 # 1) We already know the successors sets of CURRENT:
957 # 1) We already know the successors sets of CURRENT:
957 # -> mission accomplished, pop it from the stack.
958 # -> mission accomplished, pop it from the stack.
958 # 2) Node is not obsolete:
959 # 2) Node is not obsolete:
959 # -> the node is its own successors sets. Add it to the cache.
960 # -> the node is its own successors sets. Add it to the cache.
960 # 3) We do not know successors set of direct successors of CURRENT:
961 # 3) We do not know successors set of direct successors of CURRENT:
961 # -> We add those successors to the stack.
962 # -> We add those successors to the stack.
962 # 4) We know successors sets of all direct successors of CURRENT:
963 # 4) We know successors sets of all direct successors of CURRENT:
963 # -> We can compute CURRENT successors set and add it to the
964 # -> We can compute CURRENT successors set and add it to the
964 # cache.
965 # cache.
965 #
966 #
966 current = toproceed[-1]
967 current = toproceed[-1]
967 if current in cache:
968 if current in cache:
968 # case (1): We already know the successors sets
969 # case (1): We already know the successors sets
969 stackedset.remove(toproceed.pop())
970 stackedset.remove(toproceed.pop())
970 elif current not in succmarkers:
971 elif current not in succmarkers:
971 # case (2): The node is not obsolete.
972 # case (2): The node is not obsolete.
972 if current in repo:
973 if current in repo:
973 # We have a valid last successors.
974 # We have a valid last successors.
974 cache[current] = [(current,)]
975 cache[current] = [(current,)]
975 else:
976 else:
976 # Final obsolete version is unknown locally.
977 # Final obsolete version is unknown locally.
977 # Do not count that as a valid successors
978 # Do not count that as a valid successors
978 cache[current] = []
979 cache[current] = []
979 else:
980 else:
980 # cases (3) and (4)
981 # cases (3) and (4)
981 #
982 #
982 # We proceed in two phases. Phase 1 aims to distinguish case (3)
983 # We proceed in two phases. Phase 1 aims to distinguish case (3)
983 # from case (4):
984 # from case (4):
984 #
985 #
985 # For each direct successors of CURRENT, we check whether its
986 # For each direct successors of CURRENT, we check whether its
986 # successors sets are known. If they are not, we stack the
987 # successors sets are known. If they are not, we stack the
987 # unknown node and proceed to the next iteration of the while
988 # unknown node and proceed to the next iteration of the while
988 # loop. (case 3)
989 # loop. (case 3)
989 #
990 #
990 # During this step, we may detect obsolescence cycles: a node
991 # During this step, we may detect obsolescence cycles: a node
991 # with unknown successors sets but already in the call stack.
992 # with unknown successors sets but already in the call stack.
992 # In such a situation, we arbitrary set the successors sets of
993 # In such a situation, we arbitrary set the successors sets of
993 # the node to nothing (node pruned) to break the cycle.
994 # the node to nothing (node pruned) to break the cycle.
994 #
995 #
995 # If no break was encountered we proceed to phase 2.
996 # If no break was encountered we proceed to phase 2.
996 #
997 #
997 # Phase 2 computes successors sets of CURRENT (case 4); see details
998 # Phase 2 computes successors sets of CURRENT (case 4); see details
998 # in phase 2 itself.
999 # in phase 2 itself.
999 #
1000 #
1000 # Note the two levels of iteration in each phase.
1001 # Note the two levels of iteration in each phase.
1001 # - The first one handles obsolescence markers using CURRENT as
1002 # - The first one handles obsolescence markers using CURRENT as
1002 # precursor (successors markers of CURRENT).
1003 # precursor (successors markers of CURRENT).
1003 #
1004 #
1004 # Having multiple entry here means divergence.
1005 # Having multiple entry here means divergence.
1005 #
1006 #
1006 # - The second one handles successors defined in each marker.
1007 # - The second one handles successors defined in each marker.
1007 #
1008 #
1008 # Having none means pruned node, multiple successors means split,
1009 # Having none means pruned node, multiple successors means split,
1009 # single successors are standard replacement.
1010 # single successors are standard replacement.
1010 #
1011 #
1011 for mark in sorted(succmarkers[current]):
1012 for mark in sorted(succmarkers[current]):
1012 for suc in mark[1]:
1013 for suc in mark[1]:
1013 if suc not in cache:
1014 if suc not in cache:
1014 if suc in stackedset:
1015 if suc in stackedset:
1015 # cycle breaking
1016 # cycle breaking
1016 cache[suc] = []
1017 cache[suc] = []
1017 else:
1018 else:
1018 # case (3) If we have not computed successors sets
1019 # case (3) If we have not computed successors sets
1019 # of one of those successors we add it to the
1020 # of one of those successors we add it to the
1020 # `toproceed` stack and stop all work for this
1021 # `toproceed` stack and stop all work for this
1021 # iteration.
1022 # iteration.
1022 toproceed.append(suc)
1023 toproceed.append(suc)
1023 stackedset.add(suc)
1024 stackedset.add(suc)
1024 break
1025 break
1025 else:
1026 else:
1026 continue
1027 continue
1027 break
1028 break
1028 else:
1029 else:
1029 # case (4): we know all successors sets of all direct
1030 # case (4): we know all successors sets of all direct
1030 # successors
1031 # successors
1031 #
1032 #
1032 # Successors set contributed by each marker depends on the
1033 # Successors set contributed by each marker depends on the
1033 # successors sets of all its "successors" node.
1034 # successors sets of all its "successors" node.
1034 #
1035 #
1035 # Each different marker is a divergence in the obsolescence
1036 # Each different marker is a divergence in the obsolescence
1036 # history. It contributes successors sets distinct from other
1037 # history. It contributes successors sets distinct from other
1037 # markers.
1038 # markers.
1038 #
1039 #
1039 # Within a marker, a successor may have divergent successors
1040 # Within a marker, a successor may have divergent successors
1040 # sets. In such a case, the marker will contribute multiple
1041 # sets. In such a case, the marker will contribute multiple
1041 # divergent successors sets. If multiple successors have
1042 # divergent successors sets. If multiple successors have
1042 # divergent successors sets, a Cartesian product is used.
1043 # divergent successors sets, a Cartesian product is used.
1043 #
1044 #
1044 # At the end we post-process successors sets to remove
1045 # At the end we post-process successors sets to remove
1045 # duplicated entry and successors set that are strict subset of
1046 # duplicated entry and successors set that are strict subset of
1046 # another one.
1047 # another one.
1047 succssets = []
1048 succssets = []
1048 for mark in sorted(succmarkers[current]):
1049 for mark in sorted(succmarkers[current]):
1049 # successors sets contributed by this marker
1050 # successors sets contributed by this marker
1050 markss = [[]]
1051 markss = [[]]
1051 for suc in mark[1]:
1052 for suc in mark[1]:
1052 # cardinal product with previous successors
1053 # cardinal product with previous successors
1053 productresult = []
1054 productresult = []
1054 for prefix in markss:
1055 for prefix in markss:
1055 for suffix in cache[suc]:
1056 for suffix in cache[suc]:
1056 newss = list(prefix)
1057 newss = list(prefix)
1057 for part in suffix:
1058 for part in suffix:
1058 # do not duplicated entry in successors set
1059 # do not duplicated entry in successors set
1059 # first entry wins.
1060 # first entry wins.
1060 if part not in newss:
1061 if part not in newss:
1061 newss.append(part)
1062 newss.append(part)
1062 productresult.append(newss)
1063 productresult.append(newss)
1063 markss = productresult
1064 markss = productresult
1064 succssets.extend(markss)
1065 succssets.extend(markss)
1065 # remove duplicated and subset
1066 # remove duplicated and subset
1066 seen = []
1067 seen = []
1067 final = []
1068 final = []
1068 candidate = sorted(((set(s), s) for s in succssets if s),
1069 candidate = sorted(((set(s), s) for s in succssets if s),
1069 key=lambda x: len(x[1]), reverse=True)
1070 key=lambda x: len(x[1]), reverse=True)
1070 for setversion, listversion in candidate:
1071 for setversion, listversion in candidate:
1071 for seenset in seen:
1072 for seenset in seen:
1072 if setversion.issubset(seenset):
1073 if setversion.issubset(seenset):
1073 break
1074 break
1074 else:
1075 else:
1075 final.append(listversion)
1076 final.append(listversion)
1076 seen.append(setversion)
1077 seen.append(setversion)
1077 final.reverse() # put small successors set first
1078 final.reverse() # put small successors set first
1078 cache[current] = final
1079 cache[current] = final
1079 return cache[initialnode]
1080 return cache[initialnode]
1080
1081
1081 # mapping of 'set-name' -> <function to compute this set>
1082 # mapping of 'set-name' -> <function to compute this set>
1082 cachefuncs = {}
1083 cachefuncs = {}
1083 def cachefor(name):
1084 def cachefor(name):
1084 """Decorator to register a function as computing the cache for a set"""
1085 """Decorator to register a function as computing the cache for a set"""
1085 def decorator(func):
1086 def decorator(func):
1086 assert name not in cachefuncs
1087 assert name not in cachefuncs
1087 cachefuncs[name] = func
1088 cachefuncs[name] = func
1088 return func
1089 return func
1089 return decorator
1090 return decorator
1090
1091
1091 def getrevs(repo, name):
1092 def getrevs(repo, name):
1092 """Return the set of revision that belong to the <name> set
1093 """Return the set of revision that belong to the <name> set
1093
1094
1094 Such access may compute the set and cache it for future use"""
1095 Such access may compute the set and cache it for future use"""
1095 repo = repo.unfiltered()
1096 repo = repo.unfiltered()
1096 if not repo.obsstore:
1097 if not repo.obsstore:
1097 return frozenset()
1098 return frozenset()
1098 if name not in repo.obsstore.caches:
1099 if name not in repo.obsstore.caches:
1099 repo.obsstore.caches[name] = cachefuncs[name](repo)
1100 repo.obsstore.caches[name] = cachefuncs[name](repo)
1100 return repo.obsstore.caches[name]
1101 return repo.obsstore.caches[name]
1101
1102
1102 # To be simple we need to invalidate obsolescence cache when:
1103 # To be simple we need to invalidate obsolescence cache when:
1103 #
1104 #
1104 # - new changeset is added:
1105 # - new changeset is added:
1105 # - public phase is changed
1106 # - public phase is changed
1106 # - obsolescence marker are added
1107 # - obsolescence marker are added
1107 # - strip is used a repo
1108 # - strip is used a repo
1108 def clearobscaches(repo):
1109 def clearobscaches(repo):
1109 """Remove all obsolescence related cache from a repo
1110 """Remove all obsolescence related cache from a repo
1110
1111
1111 This remove all cache in obsstore is the obsstore already exist on the
1112 This remove all cache in obsstore is the obsstore already exist on the
1112 repo.
1113 repo.
1113
1114
1114 (We could be smarter here given the exact event that trigger the cache
1115 (We could be smarter here given the exact event that trigger the cache
1115 clearing)"""
1116 clearing)"""
1116 # only clear cache is there is obsstore data in this repo
1117 # only clear cache is there is obsstore data in this repo
1117 if 'obsstore' in repo._filecache:
1118 if 'obsstore' in repo._filecache:
1118 repo.obsstore.caches.clear()
1119 repo.obsstore.caches.clear()
1119
1120
1120 @cachefor('obsolete')
1121 @cachefor('obsolete')
1121 def _computeobsoleteset(repo):
1122 def _computeobsoleteset(repo):
1122 """the set of obsolete revisions"""
1123 """the set of obsolete revisions"""
1123 obs = set()
1124 obs = set()
1124 getnode = repo.changelog.node
1125 getnode = repo.changelog.node
1125 notpublic = repo._phasecache.getrevset(repo, (phases.draft, phases.secret))
1126 notpublic = repo._phasecache.getrevset(repo, (phases.draft, phases.secret))
1126 for r in notpublic:
1127 for r in notpublic:
1127 if getnode(r) in repo.obsstore.successors:
1128 if getnode(r) in repo.obsstore.successors:
1128 obs.add(r)
1129 obs.add(r)
1129 return obs
1130 return obs
1130
1131
1131 @cachefor('unstable')
1132 @cachefor('unstable')
1132 def _computeunstableset(repo):
1133 def _computeunstableset(repo):
1133 """the set of non obsolete revisions with obsolete parents"""
1134 """the set of non obsolete revisions with obsolete parents"""
1134 revs = [(ctx.rev(), ctx) for ctx in
1135 revs = [(ctx.rev(), ctx) for ctx in
1135 repo.set('(not public()) and (not obsolete())')]
1136 repo.set('(not public()) and (not obsolete())')]
1136 revs.sort(key=lambda x:x[0])
1137 revs.sort(key=lambda x:x[0])
1137 unstable = set()
1138 unstable = set()
1138 for rev, ctx in revs:
1139 for rev, ctx in revs:
1139 # A rev is unstable if one of its parent is obsolete or unstable
1140 # A rev is unstable if one of its parent is obsolete or unstable
1140 # this works since we traverse following growing rev order
1141 # this works since we traverse following growing rev order
1141 if any((x.obsolete() or (x.rev() in unstable))
1142 if any((x.obsolete() or (x.rev() in unstable))
1142 for x in ctx.parents()):
1143 for x in ctx.parents()):
1143 unstable.add(rev)
1144 unstable.add(rev)
1144 return unstable
1145 return unstable
1145
1146
1146 @cachefor('suspended')
1147 @cachefor('suspended')
1147 def _computesuspendedset(repo):
1148 def _computesuspendedset(repo):
1148 """the set of obsolete parents with non obsolete descendants"""
1149 """the set of obsolete parents with non obsolete descendants"""
1149 suspended = repo.changelog.ancestors(getrevs(repo, 'unstable'))
1150 suspended = repo.changelog.ancestors(getrevs(repo, 'unstable'))
1150 return set(r for r in getrevs(repo, 'obsolete') if r in suspended)
1151 return set(r for r in getrevs(repo, 'obsolete') if r in suspended)
1151
1152
1152 @cachefor('extinct')
1153 @cachefor('extinct')
1153 def _computeextinctset(repo):
1154 def _computeextinctset(repo):
1154 """the set of obsolete parents without non obsolete descendants"""
1155 """the set of obsolete parents without non obsolete descendants"""
1155 return getrevs(repo, 'obsolete') - getrevs(repo, 'suspended')
1156 return getrevs(repo, 'obsolete') - getrevs(repo, 'suspended')
1156
1157
1157
1158
1158 @cachefor('bumped')
1159 @cachefor('bumped')
1159 def _computebumpedset(repo):
1160 def _computebumpedset(repo):
1160 """the set of revs trying to obsolete public revisions"""
1161 """the set of revs trying to obsolete public revisions"""
1161 bumped = set()
1162 bumped = set()
1162 # util function (avoid attribute lookup in the loop)
1163 # util function (avoid attribute lookup in the loop)
1163 phase = repo._phasecache.phase # would be faster to grab the full list
1164 phase = repo._phasecache.phase # would be faster to grab the full list
1164 public = phases.public
1165 public = phases.public
1165 cl = repo.changelog
1166 cl = repo.changelog
1166 torev = cl.nodemap.get
1167 torev = cl.nodemap.get
1167 for ctx in repo.set('(not public()) and (not obsolete())'):
1168 for ctx in repo.set('(not public()) and (not obsolete())'):
1168 rev = ctx.rev()
1169 rev = ctx.rev()
1169 # We only evaluate mutable, non-obsolete revision
1170 # We only evaluate mutable, non-obsolete revision
1170 node = ctx.node()
1171 node = ctx.node()
1171 # (future) A cache of precursors may worth if split is very common
1172 # (future) A cache of precursors may worth if split is very common
1172 for pnode in allprecursors(repo.obsstore, [node],
1173 for pnode in allprecursors(repo.obsstore, [node],
1173 ignoreflags=bumpedfix):
1174 ignoreflags=bumpedfix):
1174 prev = torev(pnode) # unfiltered! but so is phasecache
1175 prev = torev(pnode) # unfiltered! but so is phasecache
1175 if (prev is not None) and (phase(repo, prev) <= public):
1176 if (prev is not None) and (phase(repo, prev) <= public):
1176 # we have a public precursor
1177 # we have a public precursor
1177 bumped.add(rev)
1178 bumped.add(rev)
1178 break # Next draft!
1179 break # Next draft!
1179 return bumped
1180 return bumped
1180
1181
1181 @cachefor('divergent')
1182 @cachefor('divergent')
1182 def _computedivergentset(repo):
1183 def _computedivergentset(repo):
1183 """the set of rev that compete to be the final successors of some revision.
1184 """the set of rev that compete to be the final successors of some revision.
1184 """
1185 """
1185 divergent = set()
1186 divergent = set()
1186 obsstore = repo.obsstore
1187 obsstore = repo.obsstore
1187 newermap = {}
1188 newermap = {}
1188 for ctx in repo.set('(not public()) - obsolete()'):
1189 for ctx in repo.set('(not public()) - obsolete()'):
1189 mark = obsstore.precursors.get(ctx.node(), ())
1190 mark = obsstore.precursors.get(ctx.node(), ())
1190 toprocess = set(mark)
1191 toprocess = set(mark)
1191 seen = set()
1192 seen = set()
1192 while toprocess:
1193 while toprocess:
1193 prec = toprocess.pop()[0]
1194 prec = toprocess.pop()[0]
1194 if prec in seen:
1195 if prec in seen:
1195 continue # emergency cycle hanging prevention
1196 continue # emergency cycle hanging prevention
1196 seen.add(prec)
1197 seen.add(prec)
1197 if prec not in newermap:
1198 if prec not in newermap:
1198 successorssets(repo, prec, newermap)
1199 successorssets(repo, prec, newermap)
1199 newer = [n for n in newermap[prec] if n]
1200 newer = [n for n in newermap[prec] if n]
1200 if len(newer) > 1:
1201 if len(newer) > 1:
1201 divergent.add(ctx.rev())
1202 divergent.add(ctx.rev())
1202 break
1203 break
1203 toprocess.update(obsstore.precursors.get(prec, ()))
1204 toprocess.update(obsstore.precursors.get(prec, ()))
1204 return divergent
1205 return divergent
1205
1206
1206
1207
1207 def createmarkers(repo, relations, flag=0, date=None, metadata=None):
1208 def createmarkers(repo, relations, flag=0, date=None, metadata=None):
1208 """Add obsolete markers between changesets in a repo
1209 """Add obsolete markers between changesets in a repo
1209
1210
1210 <relations> must be an iterable of (<old>, (<new>, ...)[,{metadata}])
1211 <relations> must be an iterable of (<old>, (<new>, ...)[,{metadata}])
1211 tuple. `old` and `news` are changectx. metadata is an optional dictionary
1212 tuple. `old` and `news` are changectx. metadata is an optional dictionary
1212 containing metadata for this marker only. It is merged with the global
1213 containing metadata for this marker only. It is merged with the global
1213 metadata specified through the `metadata` argument of this function,
1214 metadata specified through the `metadata` argument of this function,
1214
1215
1215 Trying to obsolete a public changeset will raise an exception.
1216 Trying to obsolete a public changeset will raise an exception.
1216
1217
1217 Current user and date are used except if specified otherwise in the
1218 Current user and date are used except if specified otherwise in the
1218 metadata attribute.
1219 metadata attribute.
1219
1220
1220 This function operates within a transaction of its own, but does
1221 This function operates within a transaction of its own, but does
1221 not take any lock on the repo.
1222 not take any lock on the repo.
1222 """
1223 """
1223 # prepare metadata
1224 # prepare metadata
1224 if metadata is None:
1225 if metadata is None:
1225 metadata = {}
1226 metadata = {}
1226 if 'user' not in metadata:
1227 if 'user' not in metadata:
1227 metadata['user'] = repo.ui.username()
1228 metadata['user'] = repo.ui.username()
1228 tr = repo.transaction('add-obsolescence-marker')
1229 tr = repo.transaction('add-obsolescence-marker')
1229 try:
1230 try:
1230 markerargs = []
1231 markerargs = []
1231 for rel in relations:
1232 for rel in relations:
1232 prec = rel[0]
1233 prec = rel[0]
1233 sucs = rel[1]
1234 sucs = rel[1]
1234 localmetadata = metadata.copy()
1235 localmetadata = metadata.copy()
1235 if 2 < len(rel):
1236 if 2 < len(rel):
1236 localmetadata.update(rel[2])
1237 localmetadata.update(rel[2])
1237
1238
1238 if not prec.mutable():
1239 if not prec.mutable():
1239 raise error.Abort(_("cannot obsolete public changeset: %s")
1240 raise error.Abort(_("cannot obsolete public changeset: %s")
1240 % prec,
1241 % prec,
1241 hint="see 'hg help phases' for details")
1242 hint="see 'hg help phases' for details")
1242 nprec = prec.node()
1243 nprec = prec.node()
1243 nsucs = tuple(s.node() for s in sucs)
1244 nsucs = tuple(s.node() for s in sucs)
1244 npare = None
1245 npare = None
1245 if not nsucs:
1246 if not nsucs:
1246 npare = tuple(p.node() for p in prec.parents())
1247 npare = tuple(p.node() for p in prec.parents())
1247 if nprec in nsucs:
1248 if nprec in nsucs:
1248 raise error.Abort(_("changeset %s cannot obsolete itself")
1249 raise error.Abort(_("changeset %s cannot obsolete itself")
1249 % prec)
1250 % prec)
1250
1251
1251 # Creating the marker causes the hidden cache to become invalid,
1252 # Creating the marker causes the hidden cache to become invalid,
1252 # which causes recomputation when we ask for prec.parents() above.
1253 # which causes recomputation when we ask for prec.parents() above.
1253 # Resulting in n^2 behavior. So let's prepare all of the args
1254 # Resulting in n^2 behavior. So let's prepare all of the args
1254 # first, then create the markers.
1255 # first, then create the markers.
1255 markerargs.append((nprec, nsucs, npare, localmetadata))
1256 markerargs.append((nprec, nsucs, npare, localmetadata))
1256
1257
1257 for args in markerargs:
1258 for args in markerargs:
1258 nprec, nsucs, npare, localmetadata = args
1259 nprec, nsucs, npare, localmetadata = args
1259 repo.obsstore.create(tr, nprec, nsucs, flag, parents=npare,
1260 repo.obsstore.create(tr, nprec, nsucs, flag, parents=npare,
1260 date=date, metadata=localmetadata)
1261 date=date, metadata=localmetadata)
1261 repo.filteredrevcache.clear()
1262 repo.filteredrevcache.clear()
1262 tr.close()
1263 tr.close()
1263 finally:
1264 finally:
1264 tr.release()
1265 tr.release()
1265
1266
1266 def isenabled(repo, option):
1267 def isenabled(repo, option):
1267 """Returns True if the given repository has the given obsolete option
1268 """Returns True if the given repository has the given obsolete option
1268 enabled.
1269 enabled.
1269 """
1270 """
1270 result = set(repo.ui.configlist('experimental', 'evolution'))
1271 result = set(repo.ui.configlist('experimental', 'evolution'))
1271 if 'all' in result:
1272 if 'all' in result:
1272 return True
1273 return True
1273
1274
1274 # For migration purposes, temporarily return true if the config hasn't been
1275 # For migration purposes, temporarily return true if the config hasn't been
1275 # set but _enabled is true.
1276 # set but _enabled is true.
1276 if len(result) == 0 and _enabled:
1277 if len(result) == 0 and _enabled:
1277 return True
1278 return True
1278
1279
1279 # createmarkers must be enabled if other options are enabled
1280 # createmarkers must be enabled if other options are enabled
1280 if ((allowunstableopt in result or exchangeopt in result) and
1281 if ((allowunstableopt in result or exchangeopt in result) and
1281 not createmarkersopt in result):
1282 not createmarkersopt in result):
1282 raise error.Abort(_("'createmarkers' obsolete option must be enabled "
1283 raise error.Abort(_("'createmarkers' obsolete option must be enabled "
1283 "if other obsolete options are enabled"))
1284 "if other obsolete options are enabled"))
1284
1285
1285 return option in result
1286 return option in result
General Comments 0
You need to be logged in to leave comments. Login now