##// END OF EJS Templates
typing: minor tweaks to allow updating to pytype 2022.11.18
Matt Harbison -
r50543:9be765b8 default
parent child Browse files
Show More
@@ -1,2589 +1,2594 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
148
149 import collections
149 import collections
150 import errno
150 import errno
151 import os
151 import os
152 import re
152 import re
153 import string
153 import string
154 import struct
154 import struct
155 import sys
155 import sys
156
156
157 from .i18n import _
157 from .i18n import _
158 from .node import (
158 from .node import (
159 hex,
159 hex,
160 short,
160 short,
161 )
161 )
162 from . import (
162 from . import (
163 bookmarks,
163 bookmarks,
164 changegroup,
164 changegroup,
165 encoding,
165 encoding,
166 error,
166 error,
167 obsolete,
167 obsolete,
168 phases,
168 phases,
169 pushkey,
169 pushkey,
170 pycompat,
170 pycompat,
171 requirements,
171 requirements,
172 scmutil,
172 scmutil,
173 streamclone,
173 streamclone,
174 tags,
174 tags,
175 url,
175 url,
176 util,
176 util,
177 )
177 )
178 from .utils import (
178 from .utils import (
179 stringutil,
179 stringutil,
180 urlutil,
180 urlutil,
181 )
181 )
182 from .interfaces import repository
182 from .interfaces import repository
183
183
184 urlerr = util.urlerr
184 urlerr = util.urlerr
185 urlreq = util.urlreq
185 urlreq = util.urlreq
186
186
187 _pack = struct.pack
187 _pack = struct.pack
188 _unpack = struct.unpack
188 _unpack = struct.unpack
189
189
190 _fstreamparamsize = b'>i'
190 _fstreamparamsize = b'>i'
191 _fpartheadersize = b'>i'
191 _fpartheadersize = b'>i'
192 _fparttypesize = b'>B'
192 _fparttypesize = b'>B'
193 _fpartid = b'>I'
193 _fpartid = b'>I'
194 _fpayloadsize = b'>i'
194 _fpayloadsize = b'>i'
195 _fpartparamcount = b'>BB'
195 _fpartparamcount = b'>BB'
196
196
197 preferedchunksize = 32768
197 preferedchunksize = 32768
198
198
199 _parttypeforbidden = re.compile(b'[^a-zA-Z0-9_:-]')
199 _parttypeforbidden = re.compile(b'[^a-zA-Z0-9_:-]')
200
200
201
201
202 def outdebug(ui, message):
202 def outdebug(ui, message):
203 """debug regarding output stream (bundling)"""
203 """debug regarding output stream (bundling)"""
204 if ui.configbool(b'devel', b'bundle2.debug'):
204 if ui.configbool(b'devel', b'bundle2.debug'):
205 ui.debug(b'bundle2-output: %s\n' % message)
205 ui.debug(b'bundle2-output: %s\n' % message)
206
206
207
207
208 def indebug(ui, message):
208 def indebug(ui, message):
209 """debug on input stream (unbundling)"""
209 """debug on input stream (unbundling)"""
210 if ui.configbool(b'devel', b'bundle2.debug'):
210 if ui.configbool(b'devel', b'bundle2.debug'):
211 ui.debug(b'bundle2-input: %s\n' % message)
211 ui.debug(b'bundle2-input: %s\n' % message)
212
212
213
213
214 def validateparttype(parttype):
214 def validateparttype(parttype):
215 """raise ValueError if a parttype contains invalid character"""
215 """raise ValueError if a parttype contains invalid character"""
216 if _parttypeforbidden.search(parttype):
216 if _parttypeforbidden.search(parttype):
217 raise ValueError(parttype)
217 raise ValueError(parttype)
218
218
219
219
220 def _makefpartparamsizes(nbparams):
220 def _makefpartparamsizes(nbparams):
221 """return a struct format to read part parameter sizes
221 """return a struct format to read part parameter sizes
222
222
223 The number parameters is variable so we need to build that format
223 The number parameters is variable so we need to build that format
224 dynamically.
224 dynamically.
225 """
225 """
226 return b'>' + (b'BB' * nbparams)
226 return b'>' + (b'BB' * nbparams)
227
227
228
228
229 parthandlermapping = {}
229 parthandlermapping = {}
230
230
231
231
232 def parthandler(parttype, params=()):
232 def parthandler(parttype, params=()):
233 """decorator that register a function as a bundle2 part handler
233 """decorator that register a function as a bundle2 part handler
234
234
235 eg::
235 eg::
236
236
237 @parthandler('myparttype', ('mandatory', 'param', 'handled'))
237 @parthandler('myparttype', ('mandatory', 'param', 'handled'))
238 def myparttypehandler(...):
238 def myparttypehandler(...):
239 '''process a part of type "my part".'''
239 '''process a part of type "my part".'''
240 ...
240 ...
241 """
241 """
242 validateparttype(parttype)
242 validateparttype(parttype)
243
243
244 def _decorator(func):
244 def _decorator(func):
245 lparttype = parttype.lower() # enforce lower case matching.
245 lparttype = parttype.lower() # enforce lower case matching.
246 assert lparttype not in parthandlermapping
246 assert lparttype not in parthandlermapping
247 parthandlermapping[lparttype] = func
247 parthandlermapping[lparttype] = func
248 func.params = frozenset(params)
248 func.params = frozenset(params)
249 return func
249 return func
250
250
251 return _decorator
251 return _decorator
252
252
253
253
254 class unbundlerecords:
254 class unbundlerecords:
255 """keep record of what happens during and unbundle
255 """keep record of what happens during and unbundle
256
256
257 New records are added using `records.add('cat', obj)`. Where 'cat' is a
257 New records are added using `records.add('cat', obj)`. Where 'cat' is a
258 category of record and obj is an arbitrary object.
258 category of record and obj is an arbitrary object.
259
259
260 `records['cat']` will return all entries of this category 'cat'.
260 `records['cat']` will return all entries of this category 'cat'.
261
261
262 Iterating on the object itself will yield `('category', obj)` tuples
262 Iterating on the object itself will yield `('category', obj)` tuples
263 for all entries.
263 for all entries.
264
264
265 All iterations happens in chronological order.
265 All iterations happens in chronological order.
266 """
266 """
267
267
268 def __init__(self):
268 def __init__(self):
269 self._categories = {}
269 self._categories = {}
270 self._sequences = []
270 self._sequences = []
271 self._replies = {}
271 self._replies = {}
272
272
273 def add(self, category, entry, inreplyto=None):
273 def add(self, category, entry, inreplyto=None):
274 """add a new record of a given category.
274 """add a new record of a given category.
275
275
276 The entry can then be retrieved in the list returned by
276 The entry can then be retrieved in the list returned by
277 self['category']."""
277 self['category']."""
278 self._categories.setdefault(category, []).append(entry)
278 self._categories.setdefault(category, []).append(entry)
279 self._sequences.append((category, entry))
279 self._sequences.append((category, entry))
280 if inreplyto is not None:
280 if inreplyto is not None:
281 self.getreplies(inreplyto).add(category, entry)
281 self.getreplies(inreplyto).add(category, entry)
282
282
283 def getreplies(self, partid):
283 def getreplies(self, partid):
284 """get the records that are replies to a specific part"""
284 """get the records that are replies to a specific part"""
285 return self._replies.setdefault(partid, unbundlerecords())
285 return self._replies.setdefault(partid, unbundlerecords())
286
286
287 def __getitem__(self, cat):
287 def __getitem__(self, cat):
288 return tuple(self._categories.get(cat, ()))
288 return tuple(self._categories.get(cat, ()))
289
289
290 def __iter__(self):
290 def __iter__(self):
291 return iter(self._sequences)
291 return iter(self._sequences)
292
292
293 def __len__(self):
293 def __len__(self):
294 return len(self._sequences)
294 return len(self._sequences)
295
295
296 def __nonzero__(self):
296 def __nonzero__(self):
297 return bool(self._sequences)
297 return bool(self._sequences)
298
298
299 __bool__ = __nonzero__
299 __bool__ = __nonzero__
300
300
301
301
302 class bundleoperation:
302 class bundleoperation:
303 """an object that represents a single bundling process
303 """an object that represents a single bundling process
304
304
305 Its purpose is to carry unbundle-related objects and states.
305 Its purpose is to carry unbundle-related objects and states.
306
306
307 A new object should be created at the beginning of each bundle processing.
307 A new object should be created at the beginning of each bundle processing.
308 The object is to be returned by the processing function.
308 The object is to be returned by the processing function.
309
309
310 The object has very little content now it will ultimately contain:
310 The object has very little content now it will ultimately contain:
311 * an access to the repo the bundle is applied to,
311 * an access to the repo the bundle is applied to,
312 * a ui object,
312 * a ui object,
313 * a way to retrieve a transaction to add changes to the repo,
313 * a way to retrieve a transaction to add changes to the repo,
314 * a way to record the result of processing each part,
314 * a way to record the result of processing each part,
315 * a way to construct a bundle response when applicable.
315 * a way to construct a bundle response when applicable.
316 """
316 """
317
317
318 def __init__(self, repo, transactiongetter, captureoutput=True, source=b''):
318 def __init__(self, repo, transactiongetter, captureoutput=True, source=b''):
319 self.repo = repo
319 self.repo = repo
320 self.ui = repo.ui
320 self.ui = repo.ui
321 self.records = unbundlerecords()
321 self.records = unbundlerecords()
322 self.reply = None
322 self.reply = None
323 self.captureoutput = captureoutput
323 self.captureoutput = captureoutput
324 self.hookargs = {}
324 self.hookargs = {}
325 self._gettransaction = transactiongetter
325 self._gettransaction = transactiongetter
326 # carries value that can modify part behavior
326 # carries value that can modify part behavior
327 self.modes = {}
327 self.modes = {}
328 self.source = source
328 self.source = source
329
329
330 def gettransaction(self):
330 def gettransaction(self):
331 transaction = self._gettransaction()
331 transaction = self._gettransaction()
332
332
333 if self.hookargs:
333 if self.hookargs:
334 # the ones added to the transaction supercede those added
334 # the ones added to the transaction supercede those added
335 # to the operation.
335 # to the operation.
336 self.hookargs.update(transaction.hookargs)
336 self.hookargs.update(transaction.hookargs)
337 transaction.hookargs = self.hookargs
337 transaction.hookargs = self.hookargs
338
338
339 # mark the hookargs as flushed. further attempts to add to
339 # mark the hookargs as flushed. further attempts to add to
340 # hookargs will result in an abort.
340 # hookargs will result in an abort.
341 self.hookargs = None
341 self.hookargs = None
342
342
343 return transaction
343 return transaction
344
344
345 def addhookargs(self, hookargs):
345 def addhookargs(self, hookargs):
346 if self.hookargs is None:
346 if self.hookargs is None:
347 raise error.ProgrammingError(
347 raise error.ProgrammingError(
348 b'attempted to add hookargs to '
348 b'attempted to add hookargs to '
349 b'operation after transaction started'
349 b'operation after transaction started'
350 )
350 )
351 self.hookargs.update(hookargs)
351 self.hookargs.update(hookargs)
352
352
353
353
354 class TransactionUnavailable(RuntimeError):
354 class TransactionUnavailable(RuntimeError):
355 pass
355 pass
356
356
357
357
358 def _notransaction():
358 def _notransaction():
359 """default method to get a transaction while processing a bundle
359 """default method to get a transaction while processing a bundle
360
360
361 Raise an exception to highlight the fact that no transaction was expected
361 Raise an exception to highlight the fact that no transaction was expected
362 to be created"""
362 to be created"""
363 raise TransactionUnavailable()
363 raise TransactionUnavailable()
364
364
365
365
366 def applybundle(repo, unbundler, tr, source, url=None, **kwargs):
366 def applybundle(repo, unbundler, tr, source, url=None, **kwargs):
367 # transform me into unbundler.apply() as soon as the freeze is lifted
367 # transform me into unbundler.apply() as soon as the freeze is lifted
368 if isinstance(unbundler, unbundle20):
368 if isinstance(unbundler, unbundle20):
369 tr.hookargs[b'bundle2'] = b'1'
369 tr.hookargs[b'bundle2'] = b'1'
370 if source is not None and b'source' not in tr.hookargs:
370 if source is not None and b'source' not in tr.hookargs:
371 tr.hookargs[b'source'] = source
371 tr.hookargs[b'source'] = source
372 if url is not None and b'url' not in tr.hookargs:
372 if url is not None and b'url' not in tr.hookargs:
373 tr.hookargs[b'url'] = url
373 tr.hookargs[b'url'] = url
374 return processbundle(repo, unbundler, lambda: tr, source=source)
374 return processbundle(repo, unbundler, lambda: tr, source=source)
375 else:
375 else:
376 # the transactiongetter won't be used, but we might as well set it
376 # the transactiongetter won't be used, but we might as well set it
377 op = bundleoperation(repo, lambda: tr, source=source)
377 op = bundleoperation(repo, lambda: tr, source=source)
378 _processchangegroup(op, unbundler, tr, source, url, **kwargs)
378 _processchangegroup(op, unbundler, tr, source, url, **kwargs)
379 return op
379 return op
380
380
381
381
382 class partiterator:
382 class partiterator:
383 def __init__(self, repo, op, unbundler):
383 def __init__(self, repo, op, unbundler):
384 self.repo = repo
384 self.repo = repo
385 self.op = op
385 self.op = op
386 self.unbundler = unbundler
386 self.unbundler = unbundler
387 self.iterator = None
387 self.iterator = None
388 self.count = 0
388 self.count = 0
389 self.current = None
389 self.current = None
390
390
391 def __enter__(self):
391 def __enter__(self):
392 def func():
392 def func():
393 itr = enumerate(self.unbundler.iterparts(), 1)
393 itr = enumerate(self.unbundler.iterparts(), 1)
394 for count, p in itr:
394 for count, p in itr:
395 self.count = count
395 self.count = count
396 self.current = p
396 self.current = p
397 yield p
397 yield p
398 p.consume()
398 p.consume()
399 self.current = None
399 self.current = None
400
400
401 self.iterator = func()
401 self.iterator = func()
402 return self.iterator
402 return self.iterator
403
403
404 def __exit__(self, type, exc, tb):
404 def __exit__(self, type, exc, tb):
405 if not self.iterator:
405 if not self.iterator:
406 return
406 return
407
407
408 # Only gracefully abort in a normal exception situation. User aborts
408 # Only gracefully abort in a normal exception situation. User aborts
409 # like Ctrl+C throw a KeyboardInterrupt which is not a base Exception,
409 # like Ctrl+C throw a KeyboardInterrupt which is not a base Exception,
410 # and should not gracefully cleanup.
410 # and should not gracefully cleanup.
411 if isinstance(exc, Exception):
411 if isinstance(exc, Exception):
412 # Any exceptions seeking to the end of the bundle at this point are
412 # Any exceptions seeking to the end of the bundle at this point are
413 # almost certainly related to the underlying stream being bad.
413 # almost certainly related to the underlying stream being bad.
414 # And, chances are that the exception we're handling is related to
414 # And, chances are that the exception we're handling is related to
415 # getting in that bad state. So, we swallow the seeking error and
415 # getting in that bad state. So, we swallow the seeking error and
416 # re-raise the original error.
416 # re-raise the original error.
417 seekerror = False
417 seekerror = False
418 try:
418 try:
419 if self.current:
419 if self.current:
420 # consume the part content to not corrupt the stream.
420 # consume the part content to not corrupt the stream.
421 self.current.consume()
421 self.current.consume()
422
422
423 for part in self.iterator:
423 for part in self.iterator:
424 # consume the bundle content
424 # consume the bundle content
425 part.consume()
425 part.consume()
426 except Exception:
426 except Exception:
427 seekerror = True
427 seekerror = True
428
428
429 # Small hack to let caller code distinguish exceptions from bundle2
429 # Small hack to let caller code distinguish exceptions from bundle2
430 # processing from processing the old format. This is mostly needed
430 # processing from processing the old format. This is mostly needed
431 # to handle different return codes to unbundle according to the type
431 # to handle different return codes to unbundle according to the type
432 # of bundle. We should probably clean up or drop this return code
432 # of bundle. We should probably clean up or drop this return code
433 # craziness in a future version.
433 # craziness in a future version.
434 exc.duringunbundle2 = True
434 exc.duringunbundle2 = True
435 salvaged = []
435 salvaged = []
436 replycaps = None
436 replycaps = None
437 if self.op.reply is not None:
437 if self.op.reply is not None:
438 salvaged = self.op.reply.salvageoutput()
438 salvaged = self.op.reply.salvageoutput()
439 replycaps = self.op.reply.capabilities
439 replycaps = self.op.reply.capabilities
440 exc._replycaps = replycaps
440 exc._replycaps = replycaps
441 exc._bundle2salvagedoutput = salvaged
441 exc._bundle2salvagedoutput = salvaged
442
442
443 # Re-raising from a variable loses the original stack. So only use
443 # Re-raising from a variable loses the original stack. So only use
444 # that form if we need to.
444 # that form if we need to.
445 if seekerror:
445 if seekerror:
446 raise exc
446 raise exc
447
447
448 self.repo.ui.debug(
448 self.repo.ui.debug(
449 b'bundle2-input-bundle: %i parts total\n' % self.count
449 b'bundle2-input-bundle: %i parts total\n' % self.count
450 )
450 )
451
451
452
452
453 def processbundle(repo, unbundler, transactiongetter=None, op=None, source=b''):
453 def processbundle(repo, unbundler, transactiongetter=None, op=None, source=b''):
454 """This function process a bundle, apply effect to/from a repo
454 """This function process a bundle, apply effect to/from a repo
455
455
456 It iterates over each part then searches for and uses the proper handling
456 It iterates over each part then searches for and uses the proper handling
457 code to process the part. Parts are processed in order.
457 code to process the part. Parts are processed in order.
458
458
459 Unknown Mandatory part will abort the process.
459 Unknown Mandatory part will abort the process.
460
460
461 It is temporarily possible to provide a prebuilt bundleoperation to the
461 It is temporarily possible to provide a prebuilt bundleoperation to the
462 function. This is used to ensure output is properly propagated in case of
462 function. This is used to ensure output is properly propagated in case of
463 an error during the unbundling. This output capturing part will likely be
463 an error during the unbundling. This output capturing part will likely be
464 reworked and this ability will probably go away in the process.
464 reworked and this ability will probably go away in the process.
465 """
465 """
466 if op is None:
466 if op is None:
467 if transactiongetter is None:
467 if transactiongetter is None:
468 transactiongetter = _notransaction
468 transactiongetter = _notransaction
469 op = bundleoperation(repo, transactiongetter, source=source)
469 op = bundleoperation(repo, transactiongetter, source=source)
470 # todo:
470 # todo:
471 # - replace this is a init function soon.
471 # - replace this is a init function soon.
472 # - exception catching
472 # - exception catching
473 unbundler.params
473 unbundler.params
474 if repo.ui.debugflag:
474 if repo.ui.debugflag:
475 msg = [b'bundle2-input-bundle:']
475 msg = [b'bundle2-input-bundle:']
476 if unbundler.params:
476 if unbundler.params:
477 msg.append(b' %i params' % len(unbundler.params))
477 msg.append(b' %i params' % len(unbundler.params))
478 if op._gettransaction is None or op._gettransaction is _notransaction:
478 if op._gettransaction is None or op._gettransaction is _notransaction:
479 msg.append(b' no-transaction')
479 msg.append(b' no-transaction')
480 else:
480 else:
481 msg.append(b' with-transaction')
481 msg.append(b' with-transaction')
482 msg.append(b'\n')
482 msg.append(b'\n')
483 repo.ui.debug(b''.join(msg))
483 repo.ui.debug(b''.join(msg))
484
484
485 processparts(repo, op, unbundler)
485 processparts(repo, op, unbundler)
486
486
487 return op
487 return op
488
488
489
489
490 def processparts(repo, op, unbundler):
490 def processparts(repo, op, unbundler):
491 with partiterator(repo, op, unbundler) as parts:
491 with partiterator(repo, op, unbundler) as parts:
492 for part in parts:
492 for part in parts:
493 _processpart(op, part)
493 _processpart(op, part)
494
494
495
495
496 def _processchangegroup(op, cg, tr, source, url, **kwargs):
496 def _processchangegroup(op, cg, tr, source, url, **kwargs):
497 ret = cg.apply(op.repo, tr, source, url, **kwargs)
497 ret = cg.apply(op.repo, tr, source, url, **kwargs)
498 op.records.add(
498 op.records.add(
499 b'changegroup',
499 b'changegroup',
500 {
500 {
501 b'return': ret,
501 b'return': ret,
502 },
502 },
503 )
503 )
504 return ret
504 return ret
505
505
506
506
507 def _gethandler(op, part):
507 def _gethandler(op, part):
508 status = b'unknown' # used by debug output
508 status = b'unknown' # used by debug output
509 try:
509 try:
510 handler = parthandlermapping.get(part.type)
510 handler = parthandlermapping.get(part.type)
511 if handler is None:
511 if handler is None:
512 status = b'unsupported-type'
512 status = b'unsupported-type'
513 raise error.BundleUnknownFeatureError(parttype=part.type)
513 raise error.BundleUnknownFeatureError(parttype=part.type)
514 indebug(op.ui, b'found a handler for part %s' % part.type)
514 indebug(op.ui, b'found a handler for part %s' % part.type)
515 unknownparams = part.mandatorykeys - handler.params
515 unknownparams = part.mandatorykeys - handler.params
516 if unknownparams:
516 if unknownparams:
517 unknownparams = list(unknownparams)
517 unknownparams = list(unknownparams)
518 unknownparams.sort()
518 unknownparams.sort()
519 status = b'unsupported-params (%s)' % b', '.join(unknownparams)
519 status = b'unsupported-params (%s)' % b', '.join(unknownparams)
520 raise error.BundleUnknownFeatureError(
520 raise error.BundleUnknownFeatureError(
521 parttype=part.type, params=unknownparams
521 parttype=part.type, params=unknownparams
522 )
522 )
523 status = b'supported'
523 status = b'supported'
524 except error.BundleUnknownFeatureError as exc:
524 except error.BundleUnknownFeatureError as exc:
525 if part.mandatory: # mandatory parts
525 if part.mandatory: # mandatory parts
526 raise
526 raise
527 indebug(op.ui, b'ignoring unsupported advisory part %s' % exc)
527 indebug(op.ui, b'ignoring unsupported advisory part %s' % exc)
528 return # skip to part processing
528 return # skip to part processing
529 finally:
529 finally:
530 if op.ui.debugflag:
530 if op.ui.debugflag:
531 msg = [b'bundle2-input-part: "%s"' % part.type]
531 msg = [b'bundle2-input-part: "%s"' % part.type]
532 if not part.mandatory:
532 if not part.mandatory:
533 msg.append(b' (advisory)')
533 msg.append(b' (advisory)')
534 nbmp = len(part.mandatorykeys)
534 nbmp = len(part.mandatorykeys)
535 nbap = len(part.params) - nbmp
535 nbap = len(part.params) - nbmp
536 if nbmp or nbap:
536 if nbmp or nbap:
537 msg.append(b' (params:')
537 msg.append(b' (params:')
538 if nbmp:
538 if nbmp:
539 msg.append(b' %i mandatory' % nbmp)
539 msg.append(b' %i mandatory' % nbmp)
540 if nbap:
540 if nbap:
541 msg.append(b' %i advisory' % nbmp)
541 msg.append(b' %i advisory' % nbmp)
542 msg.append(b')')
542 msg.append(b')')
543 msg.append(b' %s\n' % status)
543 msg.append(b' %s\n' % status)
544 op.ui.debug(b''.join(msg))
544 op.ui.debug(b''.join(msg))
545
545
546 return handler
546 return handler
547
547
548
548
549 def _processpart(op, part):
549 def _processpart(op, part):
550 """process a single part from a bundle
550 """process a single part from a bundle
551
551
552 The part is guaranteed to have been fully consumed when the function exits
552 The part is guaranteed to have been fully consumed when the function exits
553 (even if an exception is raised)."""
553 (even if an exception is raised)."""
554 handler = _gethandler(op, part)
554 handler = _gethandler(op, part)
555 if handler is None:
555 if handler is None:
556 return
556 return
557
557
558 # handler is called outside the above try block so that we don't
558 # handler is called outside the above try block so that we don't
559 # risk catching KeyErrors from anything other than the
559 # risk catching KeyErrors from anything other than the
560 # parthandlermapping lookup (any KeyError raised by handler()
560 # parthandlermapping lookup (any KeyError raised by handler()
561 # itself represents a defect of a different variety).
561 # itself represents a defect of a different variety).
562 output = None
562 output = None
563 if op.captureoutput and op.reply is not None:
563 if op.captureoutput and op.reply is not None:
564 op.ui.pushbuffer(error=True, subproc=True)
564 op.ui.pushbuffer(error=True, subproc=True)
565 output = b''
565 output = b''
566 try:
566 try:
567 handler(op, part)
567 handler(op, part)
568 finally:
568 finally:
569 if output is not None:
569 if output is not None:
570 output = op.ui.popbuffer()
570 output = op.ui.popbuffer()
571 if output:
571 if output:
572 outpart = op.reply.newpart(b'output', data=output, mandatory=False)
572 outpart = op.reply.newpart(b'output', data=output, mandatory=False)
573 outpart.addparam(
573 outpart.addparam(
574 b'in-reply-to', pycompat.bytestr(part.id), mandatory=False
574 b'in-reply-to', pycompat.bytestr(part.id), mandatory=False
575 )
575 )
576
576
577
577
578 def decodecaps(blob):
578 def decodecaps(blob):
579 """decode a bundle2 caps bytes blob into a dictionary
579 """decode a bundle2 caps bytes blob into a dictionary
580
580
581 The blob is a list of capabilities (one per line)
581 The blob is a list of capabilities (one per line)
582 Capabilities may have values using a line of the form::
582 Capabilities may have values using a line of the form::
583
583
584 capability=value1,value2,value3
584 capability=value1,value2,value3
585
585
586 The values are always a list."""
586 The values are always a list."""
587 caps = {}
587 caps = {}
588 for line in blob.splitlines():
588 for line in blob.splitlines():
589 if not line:
589 if not line:
590 continue
590 continue
591 if b'=' not in line:
591 if b'=' not in line:
592 key, vals = line, ()
592 key, vals = line, ()
593 else:
593 else:
594 key, vals = line.split(b'=', 1)
594 key, vals = line.split(b'=', 1)
595 vals = vals.split(b',')
595 vals = vals.split(b',')
596 key = urlreq.unquote(key)
596 key = urlreq.unquote(key)
597 vals = [urlreq.unquote(v) for v in vals]
597 vals = [urlreq.unquote(v) for v in vals]
598 caps[key] = vals
598 caps[key] = vals
599 return caps
599 return caps
600
600
601
601
602 def encodecaps(caps):
602 def encodecaps(caps):
603 """encode a bundle2 caps dictionary into a bytes blob"""
603 """encode a bundle2 caps dictionary into a bytes blob"""
604 chunks = []
604 chunks = []
605 for ca in sorted(caps):
605 for ca in sorted(caps):
606 vals = caps[ca]
606 vals = caps[ca]
607 ca = urlreq.quote(ca)
607 ca = urlreq.quote(ca)
608 vals = [urlreq.quote(v) for v in vals]
608 vals = [urlreq.quote(v) for v in vals]
609 if vals:
609 if vals:
610 ca = b"%s=%s" % (ca, b','.join(vals))
610 ca = b"%s=%s" % (ca, b','.join(vals))
611 chunks.append(ca)
611 chunks.append(ca)
612 return b'\n'.join(chunks)
612 return b'\n'.join(chunks)
613
613
614
614
615 bundletypes = {
615 bundletypes = {
616 b"": (b"", b'UN'), # only when using unbundle on ssh and old http servers
616 b"": (b"", b'UN'), # only when using unbundle on ssh and old http servers
617 # since the unification ssh accepts a header but there
617 # since the unification ssh accepts a header but there
618 # is no capability signaling it.
618 # is no capability signaling it.
619 b"HG20": (), # special-cased below
619 b"HG20": (), # special-cased below
620 b"HG10UN": (b"HG10UN", b'UN'),
620 b"HG10UN": (b"HG10UN", b'UN'),
621 b"HG10BZ": (b"HG10", b'BZ'),
621 b"HG10BZ": (b"HG10", b'BZ'),
622 b"HG10GZ": (b"HG10GZ", b'GZ'),
622 b"HG10GZ": (b"HG10GZ", b'GZ'),
623 }
623 }
624
624
625 # hgweb uses this list to communicate its preferred type
625 # hgweb uses this list to communicate its preferred type
626 bundlepriority = [b'HG10GZ', b'HG10BZ', b'HG10UN']
626 bundlepriority = [b'HG10GZ', b'HG10BZ', b'HG10UN']
627
627
628
628
629 class bundle20:
629 class bundle20:
630 """represent an outgoing bundle2 container
630 """represent an outgoing bundle2 container
631
631
632 Use the `addparam` method to add stream level parameter. and `newpart` to
632 Use the `addparam` method to add stream level parameter. and `newpart` to
633 populate it. Then call `getchunks` to retrieve all the binary chunks of
633 populate it. Then call `getchunks` to retrieve all the binary chunks of
634 data that compose the bundle2 container."""
634 data that compose the bundle2 container."""
635
635
636 _magicstring = b'HG20'
636 _magicstring = b'HG20'
637
637
638 def __init__(self, ui, capabilities=()):
638 def __init__(self, ui, capabilities=()):
639 self.ui = ui
639 self.ui = ui
640 self._params = []
640 self._params = []
641 self._parts = []
641 self._parts = []
642 self.capabilities = dict(capabilities)
642 self.capabilities = dict(capabilities)
643 self._compengine = util.compengines.forbundletype(b'UN')
643 self._compengine = util.compengines.forbundletype(b'UN')
644 self._compopts = None
644 self._compopts = None
645 # If compression is being handled by a consumer of the raw
645 # If compression is being handled by a consumer of the raw
646 # data (e.g. the wire protocol), unsetting this flag tells
646 # data (e.g. the wire protocol), unsetting this flag tells
647 # consumers that the bundle is best left uncompressed.
647 # consumers that the bundle is best left uncompressed.
648 self.prefercompressed = True
648 self.prefercompressed = True
649
649
650 def setcompression(self, alg, compopts=None):
650 def setcompression(self, alg, compopts=None):
651 """setup core part compression to <alg>"""
651 """setup core part compression to <alg>"""
652 if alg in (None, b'UN'):
652 if alg in (None, b'UN'):
653 return
653 return
654 assert not any(n.lower() == b'compression' for n, v in self._params)
654 assert not any(n.lower() == b'compression' for n, v in self._params)
655 self.addparam(b'Compression', alg)
655 self.addparam(b'Compression', alg)
656 self._compengine = util.compengines.forbundletype(alg)
656 self._compengine = util.compengines.forbundletype(alg)
657 self._compopts = compopts
657 self._compopts = compopts
658
658
659 @property
659 @property
660 def nbparts(self):
660 def nbparts(self):
661 """total number of parts added to the bundler"""
661 """total number of parts added to the bundler"""
662 return len(self._parts)
662 return len(self._parts)
663
663
664 # methods used to defines the bundle2 content
664 # methods used to defines the bundle2 content
665 def addparam(self, name, value=None):
665 def addparam(self, name, value=None):
666 """add a stream level parameter"""
666 """add a stream level parameter"""
667 if not name:
667 if not name:
668 raise error.ProgrammingError(b'empty parameter name')
668 raise error.ProgrammingError(b'empty parameter name')
669 if name[0:1] not in pycompat.bytestr(
669 if name[0:1] not in pycompat.bytestr(
670 string.ascii_letters # pytype: disable=wrong-arg-types
670 string.ascii_letters # pytype: disable=wrong-arg-types
671 ):
671 ):
672 raise error.ProgrammingError(
672 raise error.ProgrammingError(
673 b'non letter first character: %s' % name
673 b'non letter first character: %s' % name
674 )
674 )
675 self._params.append((name, value))
675 self._params.append((name, value))
676
676
677 def addpart(self, part):
677 def addpart(self, part):
678 """add a new part to the bundle2 container
678 """add a new part to the bundle2 container
679
679
680 Parts contains the actual applicative payload."""
680 Parts contains the actual applicative payload."""
681 assert part.id is None
681 assert part.id is None
682 part.id = len(self._parts) # very cheap counter
682 part.id = len(self._parts) # very cheap counter
683 self._parts.append(part)
683 self._parts.append(part)
684
684
685 def newpart(self, typeid, *args, **kwargs):
685 def newpart(self, typeid, *args, **kwargs):
686 """create a new part and add it to the containers
686 """create a new part and add it to the containers
687
687
688 As the part is directly added to the containers. For now, this means
688 As the part is directly added to the containers. For now, this means
689 that any failure to properly initialize the part after calling
689 that any failure to properly initialize the part after calling
690 ``newpart`` should result in a failure of the whole bundling process.
690 ``newpart`` should result in a failure of the whole bundling process.
691
691
692 You can still fall back to manually create and add if you need better
692 You can still fall back to manually create and add if you need better
693 control."""
693 control."""
694 part = bundlepart(typeid, *args, **kwargs)
694 part = bundlepart(typeid, *args, **kwargs)
695 self.addpart(part)
695 self.addpart(part)
696 return part
696 return part
697
697
698 # methods used to generate the bundle2 stream
698 # methods used to generate the bundle2 stream
699 def getchunks(self):
699 def getchunks(self):
700 if self.ui.debugflag:
700 if self.ui.debugflag:
701 msg = [b'bundle2-output-bundle: "%s",' % self._magicstring]
701 msg = [b'bundle2-output-bundle: "%s",' % self._magicstring]
702 if self._params:
702 if self._params:
703 msg.append(b' (%i params)' % len(self._params))
703 msg.append(b' (%i params)' % len(self._params))
704 msg.append(b' %i parts total\n' % len(self._parts))
704 msg.append(b' %i parts total\n' % len(self._parts))
705 self.ui.debug(b''.join(msg))
705 self.ui.debug(b''.join(msg))
706 outdebug(self.ui, b'start emission of %s stream' % self._magicstring)
706 outdebug(self.ui, b'start emission of %s stream' % self._magicstring)
707 yield self._magicstring
707 yield self._magicstring
708 param = self._paramchunk()
708 param = self._paramchunk()
709 outdebug(self.ui, b'bundle parameter: %s' % param)
709 outdebug(self.ui, b'bundle parameter: %s' % param)
710 yield _pack(_fstreamparamsize, len(param))
710 yield _pack(_fstreamparamsize, len(param))
711 if param:
711 if param:
712 yield param
712 yield param
713 for chunk in self._compengine.compressstream(
713 for chunk in self._compengine.compressstream(
714 self._getcorechunk(), self._compopts
714 self._getcorechunk(), self._compopts
715 ):
715 ):
716 yield chunk
716 yield chunk
717
717
718 def _paramchunk(self):
718 def _paramchunk(self):
719 """return a encoded version of all stream parameters"""
719 """return a encoded version of all stream parameters"""
720 blocks = []
720 blocks = []
721 for par, value in self._params:
721 for par, value in self._params:
722 par = urlreq.quote(par)
722 par = urlreq.quote(par)
723 if value is not None:
723 if value is not None:
724 value = urlreq.quote(value)
724 value = urlreq.quote(value)
725 par = b'%s=%s' % (par, value)
725 par = b'%s=%s' % (par, value)
726 blocks.append(par)
726 blocks.append(par)
727 return b' '.join(blocks)
727 return b' '.join(blocks)
728
728
729 def _getcorechunk(self):
729 def _getcorechunk(self):
730 """yield chunk for the core part of the bundle
730 """yield chunk for the core part of the bundle
731
731
732 (all but headers and parameters)"""
732 (all but headers and parameters)"""
733 outdebug(self.ui, b'start of parts')
733 outdebug(self.ui, b'start of parts')
734 for part in self._parts:
734 for part in self._parts:
735 outdebug(self.ui, b'bundle part: "%s"' % part.type)
735 outdebug(self.ui, b'bundle part: "%s"' % part.type)
736 for chunk in part.getchunks(ui=self.ui):
736 for chunk in part.getchunks(ui=self.ui):
737 yield chunk
737 yield chunk
738 outdebug(self.ui, b'end of bundle')
738 outdebug(self.ui, b'end of bundle')
739 yield _pack(_fpartheadersize, 0)
739 yield _pack(_fpartheadersize, 0)
740
740
741 def salvageoutput(self):
741 def salvageoutput(self):
742 """return a list with a copy of all output parts in the bundle
742 """return a list with a copy of all output parts in the bundle
743
743
744 This is meant to be used during error handling to make sure we preserve
744 This is meant to be used during error handling to make sure we preserve
745 server output"""
745 server output"""
746 salvaged = []
746 salvaged = []
747 for part in self._parts:
747 for part in self._parts:
748 if part.type.startswith(b'output'):
748 if part.type.startswith(b'output'):
749 salvaged.append(part.copy())
749 salvaged.append(part.copy())
750 return salvaged
750 return salvaged
751
751
752
752
753 class unpackermixin:
753 class unpackermixin:
754 """A mixin to extract bytes and struct data from a stream"""
754 """A mixin to extract bytes and struct data from a stream"""
755
755
756 def __init__(self, fp):
756 def __init__(self, fp):
757 self._fp = fp
757 self._fp = fp
758
758
759 def _unpack(self, format):
759 def _unpack(self, format):
760 """unpack this struct format from the stream
760 """unpack this struct format from the stream
761
761
762 This method is meant for internal usage by the bundle2 protocol only.
762 This method is meant for internal usage by the bundle2 protocol only.
763 They directly manipulate the low level stream including bundle2 level
763 They directly manipulate the low level stream including bundle2 level
764 instruction.
764 instruction.
765
765
766 Do not use it to implement higher-level logic or methods."""
766 Do not use it to implement higher-level logic or methods."""
767 data = self._readexact(struct.calcsize(format))
767 data = self._readexact(struct.calcsize(format))
768 return _unpack(format, data)
768 return _unpack(format, data)
769
769
770 def _readexact(self, size):
770 def _readexact(self, size):
771 """read exactly <size> bytes from the stream
771 """read exactly <size> bytes from the stream
772
772
773 This method is meant for internal usage by the bundle2 protocol only.
773 This method is meant for internal usage by the bundle2 protocol only.
774 They directly manipulate the low level stream including bundle2 level
774 They directly manipulate the low level stream including bundle2 level
775 instruction.
775 instruction.
776
776
777 Do not use it to implement higher-level logic or methods."""
777 Do not use it to implement higher-level logic or methods."""
778 return changegroup.readexactly(self._fp, size)
778 return changegroup.readexactly(self._fp, size)
779
779
780
780
781 def getunbundler(ui, fp, magicstring=None):
781 def getunbundler(ui, fp, magicstring=None):
782 """return a valid unbundler object for a given magicstring"""
782 """return a valid unbundler object for a given magicstring"""
783 if magicstring is None:
783 if magicstring is None:
784 magicstring = changegroup.readexactly(fp, 4)
784 magicstring = changegroup.readexactly(fp, 4)
785 magic, version = magicstring[0:2], magicstring[2:4]
785 magic, version = magicstring[0:2], magicstring[2:4]
786 if magic != b'HG':
786 if magic != b'HG':
787 ui.debug(
787 ui.debug(
788 b"error: invalid magic: %r (version %r), should be 'HG'\n"
788 b"error: invalid magic: %r (version %r), should be 'HG'\n"
789 % (magic, version)
789 % (magic, version)
790 )
790 )
791 raise error.Abort(_(b'not a Mercurial bundle'))
791 raise error.Abort(_(b'not a Mercurial bundle'))
792 unbundlerclass = formatmap.get(version)
792 unbundlerclass = formatmap.get(version)
793 if unbundlerclass is None:
793 if unbundlerclass is None:
794 raise error.Abort(_(b'unknown bundle version %s') % version)
794 raise error.Abort(_(b'unknown bundle version %s') % version)
795 unbundler = unbundlerclass(ui, fp)
795 unbundler = unbundlerclass(ui, fp)
796 indebug(ui, b'start processing of %s stream' % magicstring)
796 indebug(ui, b'start processing of %s stream' % magicstring)
797 return unbundler
797 return unbundler
798
798
799
799
800 class unbundle20(unpackermixin):
800 class unbundle20(unpackermixin):
801 """interpret a bundle2 stream
801 """interpret a bundle2 stream
802
802
803 This class is fed with a binary stream and yields parts through its
803 This class is fed with a binary stream and yields parts through its
804 `iterparts` methods."""
804 `iterparts` methods."""
805
805
806 _magicstring = b'HG20'
806 _magicstring = b'HG20'
807
807
808 def __init__(self, ui, fp):
808 def __init__(self, ui, fp):
809 """If header is specified, we do not read it out of the stream."""
809 """If header is specified, we do not read it out of the stream."""
810 self.ui = ui
810 self.ui = ui
811 self._compengine = util.compengines.forbundletype(b'UN')
811 self._compengine = util.compengines.forbundletype(b'UN')
812 self._compressed = None
812 self._compressed = None
813 super(unbundle20, self).__init__(fp)
813 super(unbundle20, self).__init__(fp)
814
814
815 @util.propertycache
815 @util.propertycache
816 def params(self):
816 def params(self):
817 """dictionary of stream level parameters"""
817 """dictionary of stream level parameters"""
818 indebug(self.ui, b'reading bundle2 stream parameters')
818 indebug(self.ui, b'reading bundle2 stream parameters')
819 params = {}
819 params = {}
820 paramssize = self._unpack(_fstreamparamsize)[0]
820 paramssize = self._unpack(_fstreamparamsize)[0]
821 if paramssize < 0:
821 if paramssize < 0:
822 raise error.BundleValueError(
822 raise error.BundleValueError(
823 b'negative bundle param size: %i' % paramssize
823 b'negative bundle param size: %i' % paramssize
824 )
824 )
825 if paramssize:
825 if paramssize:
826 params = self._readexact(paramssize)
826 params = self._readexact(paramssize)
827 params = self._processallparams(params)
827 params = self._processallparams(params)
828 return params
828 return params
829
829
830 def _processallparams(self, paramsblock):
830 def _processallparams(self, paramsblock):
831 """ """
831 """ """
832 params = util.sortdict()
832 params = util.sortdict()
833 for p in paramsblock.split(b' '):
833 for p in paramsblock.split(b' '):
834 p = p.split(b'=', 1)
834 p = p.split(b'=', 1)
835 p = [urlreq.unquote(i) for i in p]
835 p = [urlreq.unquote(i) for i in p]
836 if len(p) < 2:
836 if len(p) < 2:
837 p.append(None)
837 p.append(None)
838 self._processparam(*p)
838 self._processparam(*p)
839 params[p[0]] = p[1]
839 params[p[0]] = p[1]
840 return params
840 return params
841
841
842 def _processparam(self, name, value):
842 def _processparam(self, name, value):
843 """process a parameter, applying its effect if needed
843 """process a parameter, applying its effect if needed
844
844
845 Parameter starting with a lower case letter are advisory and will be
845 Parameter starting with a lower case letter are advisory and will be
846 ignored when unknown. Those starting with an upper case letter are
846 ignored when unknown. Those starting with an upper case letter are
847 mandatory and will this function will raise a KeyError when unknown.
847 mandatory and will this function will raise a KeyError when unknown.
848
848
849 Note: no option are currently supported. Any input will be either
849 Note: no option are currently supported. Any input will be either
850 ignored or failing.
850 ignored or failing.
851 """
851 """
852 if not name:
852 if not name:
853 raise ValueError('empty parameter name')
853 raise ValueError('empty parameter name')
854 if name[0:1] not in pycompat.bytestr(
854 if name[0:1] not in pycompat.bytestr(
855 string.ascii_letters # pytype: disable=wrong-arg-types
855 string.ascii_letters # pytype: disable=wrong-arg-types
856 ):
856 ):
857 raise ValueError('non letter first character: %s' % name)
857 raise ValueError('non letter first character: %s' % name)
858 try:
858 try:
859 handler = b2streamparamsmap[name.lower()]
859 handler = b2streamparamsmap[name.lower()]
860 except KeyError:
860 except KeyError:
861 if name[0:1].islower():
861 if name[0:1].islower():
862 indebug(self.ui, b"ignoring unknown parameter %s" % name)
862 indebug(self.ui, b"ignoring unknown parameter %s" % name)
863 else:
863 else:
864 raise error.BundleUnknownFeatureError(params=(name,))
864 raise error.BundleUnknownFeatureError(params=(name,))
865 else:
865 else:
866 handler(self, name, value)
866 handler(self, name, value)
867
867
868 def _forwardchunks(self):
868 def _forwardchunks(self):
869 """utility to transfer a bundle2 as binary
869 """utility to transfer a bundle2 as binary
870
870
871 This is made necessary by the fact the 'getbundle' command over 'ssh'
871 This is made necessary by the fact the 'getbundle' command over 'ssh'
872 have no way to know then the reply end, relying on the bundle to be
872 have no way to know then the reply end, relying on the bundle to be
873 interpreted to know its end. This is terrible and we are sorry, but we
873 interpreted to know its end. This is terrible and we are sorry, but we
874 needed to move forward to get general delta enabled.
874 needed to move forward to get general delta enabled.
875 """
875 """
876 yield self._magicstring
876 yield self._magicstring
877 assert 'params' not in vars(self)
877 assert 'params' not in vars(self)
878 paramssize = self._unpack(_fstreamparamsize)[0]
878 paramssize = self._unpack(_fstreamparamsize)[0]
879 if paramssize < 0:
879 if paramssize < 0:
880 raise error.BundleValueError(
880 raise error.BundleValueError(
881 b'negative bundle param size: %i' % paramssize
881 b'negative bundle param size: %i' % paramssize
882 )
882 )
883 if paramssize:
883 if paramssize:
884 params = self._readexact(paramssize)
884 params = self._readexact(paramssize)
885 self._processallparams(params)
885 self._processallparams(params)
886 # The payload itself is decompressed below, so drop
886 # The payload itself is decompressed below, so drop
887 # the compression parameter passed down to compensate.
887 # the compression parameter passed down to compensate.
888 outparams = []
888 outparams = []
889 for p in params.split(b' '):
889 for p in params.split(b' '):
890 k, v = p.split(b'=', 1)
890 k, v = p.split(b'=', 1)
891 if k.lower() != b'compression':
891 if k.lower() != b'compression':
892 outparams.append(p)
892 outparams.append(p)
893 outparams = b' '.join(outparams)
893 outparams = b' '.join(outparams)
894 yield _pack(_fstreamparamsize, len(outparams))
894 yield _pack(_fstreamparamsize, len(outparams))
895 yield outparams
895 yield outparams
896 else:
896 else:
897 yield _pack(_fstreamparamsize, paramssize)
897 yield _pack(_fstreamparamsize, paramssize)
898 # From there, payload might need to be decompressed
898 # From there, payload might need to be decompressed
899 self._fp = self._compengine.decompressorreader(self._fp)
899 self._fp = self._compengine.decompressorreader(self._fp)
900 emptycount = 0
900 emptycount = 0
901 while emptycount < 2:
901 while emptycount < 2:
902 # so we can brainlessly loop
902 # so we can brainlessly loop
903 assert _fpartheadersize == _fpayloadsize
903 assert _fpartheadersize == _fpayloadsize
904 size = self._unpack(_fpartheadersize)[0]
904 size = self._unpack(_fpartheadersize)[0]
905 yield _pack(_fpartheadersize, size)
905 yield _pack(_fpartheadersize, size)
906 if size:
906 if size:
907 emptycount = 0
907 emptycount = 0
908 else:
908 else:
909 emptycount += 1
909 emptycount += 1
910 continue
910 continue
911 if size == flaginterrupt:
911 if size == flaginterrupt:
912 continue
912 continue
913 elif size < 0:
913 elif size < 0:
914 raise error.BundleValueError(b'negative chunk size: %i')
914 raise error.BundleValueError(b'negative chunk size: %i')
915 yield self._readexact(size)
915 yield self._readexact(size)
916
916
917 def iterparts(self, seekable=False):
917 def iterparts(self, seekable=False):
918 """yield all parts contained in the stream"""
918 """yield all parts contained in the stream"""
919 cls = seekableunbundlepart if seekable else unbundlepart
919 cls = seekableunbundlepart if seekable else unbundlepart
920 # make sure param have been loaded
920 # make sure param have been loaded
921 self.params
921 self.params
922 # From there, payload need to be decompressed
922 # From there, payload need to be decompressed
923 self._fp = self._compengine.decompressorreader(self._fp)
923 self._fp = self._compengine.decompressorreader(self._fp)
924 indebug(self.ui, b'start extraction of bundle2 parts')
924 indebug(self.ui, b'start extraction of bundle2 parts')
925 headerblock = self._readpartheader()
925 headerblock = self._readpartheader()
926 while headerblock is not None:
926 while headerblock is not None:
927 part = cls(self.ui, headerblock, self._fp)
927 part = cls(self.ui, headerblock, self._fp)
928 yield part
928 yield part
929 # Ensure part is fully consumed so we can start reading the next
929 # Ensure part is fully consumed so we can start reading the next
930 # part.
930 # part.
931 part.consume()
931 part.consume()
932
932
933 headerblock = self._readpartheader()
933 headerblock = self._readpartheader()
934 indebug(self.ui, b'end of bundle2 stream')
934 indebug(self.ui, b'end of bundle2 stream')
935
935
936 def _readpartheader(self):
936 def _readpartheader(self):
937 """reads a part header size and return the bytes blob
937 """reads a part header size and return the bytes blob
938
938
939 returns None if empty"""
939 returns None if empty"""
940 headersize = self._unpack(_fpartheadersize)[0]
940 headersize = self._unpack(_fpartheadersize)[0]
941 if headersize < 0:
941 if headersize < 0:
942 raise error.BundleValueError(
942 raise error.BundleValueError(
943 b'negative part header size: %i' % headersize
943 b'negative part header size: %i' % headersize
944 )
944 )
945 indebug(self.ui, b'part header size: %i' % headersize)
945 indebug(self.ui, b'part header size: %i' % headersize)
946 if headersize:
946 if headersize:
947 return self._readexact(headersize)
947 return self._readexact(headersize)
948 return None
948 return None
949
949
950 def compressed(self):
950 def compressed(self):
951 self.params # load params
951 self.params # load params
952 return self._compressed
952 return self._compressed
953
953
954 def close(self):
954 def close(self):
955 """close underlying file"""
955 """close underlying file"""
956 if util.safehasattr(self._fp, 'close'):
956 if util.safehasattr(self._fp, 'close'):
957 return self._fp.close()
957 return self._fp.close()
958
958
959
959
960 formatmap = {b'20': unbundle20}
960 formatmap = {b'20': unbundle20}
961
961
962 b2streamparamsmap = {}
962 b2streamparamsmap = {}
963
963
964
964
965 def b2streamparamhandler(name):
965 def b2streamparamhandler(name):
966 """register a handler for a stream level parameter"""
966 """register a handler for a stream level parameter"""
967
967
968 def decorator(func):
968 def decorator(func):
969 assert name not in formatmap
969 assert name not in formatmap
970 b2streamparamsmap[name] = func
970 b2streamparamsmap[name] = func
971 return func
971 return func
972
972
973 return decorator
973 return decorator
974
974
975
975
976 @b2streamparamhandler(b'compression')
976 @b2streamparamhandler(b'compression')
977 def processcompression(unbundler, param, value):
977 def processcompression(unbundler, param, value):
978 """read compression parameter and install payload decompression"""
978 """read compression parameter and install payload decompression"""
979 if value not in util.compengines.supportedbundletypes:
979 if value not in util.compengines.supportedbundletypes:
980 raise error.BundleUnknownFeatureError(params=(param,), values=(value,))
980 raise error.BundleUnknownFeatureError(params=(param,), values=(value,))
981 unbundler._compengine = util.compengines.forbundletype(value)
981 unbundler._compengine = util.compengines.forbundletype(value)
982 if value is not None:
982 if value is not None:
983 unbundler._compressed = True
983 unbundler._compressed = True
984
984
985
985
986 class bundlepart:
986 class bundlepart:
987 """A bundle2 part contains application level payload
987 """A bundle2 part contains application level payload
988
988
989 The part `type` is used to route the part to the application level
989 The part `type` is used to route the part to the application level
990 handler.
990 handler.
991
991
992 The part payload is contained in ``part.data``. It could be raw bytes or a
992 The part payload is contained in ``part.data``. It could be raw bytes or a
993 generator of byte chunks.
993 generator of byte chunks.
994
994
995 You can add parameters to the part using the ``addparam`` method.
995 You can add parameters to the part using the ``addparam`` method.
996 Parameters can be either mandatory (default) or advisory. Remote side
996 Parameters can be either mandatory (default) or advisory. Remote side
997 should be able to safely ignore the advisory ones.
997 should be able to safely ignore the advisory ones.
998
998
999 Both data and parameters cannot be modified after the generation has begun.
999 Both data and parameters cannot be modified after the generation has begun.
1000 """
1000 """
1001
1001
1002 def __init__(
1002 def __init__(
1003 self,
1003 self,
1004 parttype,
1004 parttype,
1005 mandatoryparams=(),
1005 mandatoryparams=(),
1006 advisoryparams=(),
1006 advisoryparams=(),
1007 data=b'',
1007 data=b'',
1008 mandatory=True,
1008 mandatory=True,
1009 ):
1009 ):
1010 validateparttype(parttype)
1010 validateparttype(parttype)
1011 self.id = None
1011 self.id = None
1012 self.type = parttype
1012 self.type = parttype
1013 self._data = data
1013 self._data = data
1014 self._mandatoryparams = list(mandatoryparams)
1014 self._mandatoryparams = list(mandatoryparams)
1015 self._advisoryparams = list(advisoryparams)
1015 self._advisoryparams = list(advisoryparams)
1016 # checking for duplicated entries
1016 # checking for duplicated entries
1017 self._seenparams = set()
1017 self._seenparams = set()
1018 for pname, __ in self._mandatoryparams + self._advisoryparams:
1018 for pname, __ in self._mandatoryparams + self._advisoryparams:
1019 if pname in self._seenparams:
1019 if pname in self._seenparams:
1020 raise error.ProgrammingError(b'duplicated params: %s' % pname)
1020 raise error.ProgrammingError(b'duplicated params: %s' % pname)
1021 self._seenparams.add(pname)
1021 self._seenparams.add(pname)
1022 # status of the part's generation:
1022 # status of the part's generation:
1023 # - None: not started,
1023 # - None: not started,
1024 # - False: currently generated,
1024 # - False: currently generated,
1025 # - True: generation done.
1025 # - True: generation done.
1026 self._generated = None
1026 self._generated = None
1027 self.mandatory = mandatory
1027 self.mandatory = mandatory
1028
1028
1029 def __repr__(self):
1029 def __repr__(self):
1030 cls = "%s.%s" % (self.__class__.__module__, self.__class__.__name__)
1030 cls = "%s.%s" % (self.__class__.__module__, self.__class__.__name__)
1031 return '<%s object at %x; id: %s; type: %s; mandatory: %s>' % (
1031 return '<%s object at %x; id: %s; type: %s; mandatory: %s>' % (
1032 cls,
1032 cls,
1033 id(self),
1033 id(self),
1034 self.id,
1034 self.id,
1035 self.type,
1035 self.type,
1036 self.mandatory,
1036 self.mandatory,
1037 )
1037 )
1038
1038
1039 def copy(self):
1039 def copy(self):
1040 """return a copy of the part
1040 """return a copy of the part
1041
1041
1042 The new part have the very same content but no partid assigned yet.
1042 The new part have the very same content but no partid assigned yet.
1043 Parts with generated data cannot be copied."""
1043 Parts with generated data cannot be copied."""
1044 assert not util.safehasattr(self.data, 'next')
1044 assert not util.safehasattr(self.data, 'next')
1045 return self.__class__(
1045 return self.__class__(
1046 self.type,
1046 self.type,
1047 self._mandatoryparams,
1047 self._mandatoryparams,
1048 self._advisoryparams,
1048 self._advisoryparams,
1049 self._data,
1049 self._data,
1050 self.mandatory,
1050 self.mandatory,
1051 )
1051 )
1052
1052
1053 # methods used to defines the part content
1053 # methods used to defines the part content
1054 @property
1054 @property
1055 def data(self):
1055 def data(self):
1056 return self._data
1056 return self._data
1057
1057
1058 @data.setter
1058 @data.setter
1059 def data(self, data):
1059 def data(self, data):
1060 if self._generated is not None:
1060 if self._generated is not None:
1061 raise error.ReadOnlyPartError(b'part is being generated')
1061 raise error.ReadOnlyPartError(b'part is being generated')
1062 self._data = data
1062 self._data = data
1063
1063
1064 @property
1064 @property
1065 def mandatoryparams(self):
1065 def mandatoryparams(self):
1066 # make it an immutable tuple to force people through ``addparam``
1066 # make it an immutable tuple to force people through ``addparam``
1067 return tuple(self._mandatoryparams)
1067 return tuple(self._mandatoryparams)
1068
1068
1069 @property
1069 @property
1070 def advisoryparams(self):
1070 def advisoryparams(self):
1071 # make it an immutable tuple to force people through ``addparam``
1071 # make it an immutable tuple to force people through ``addparam``
1072 return tuple(self._advisoryparams)
1072 return tuple(self._advisoryparams)
1073
1073
1074 def addparam(self, name, value=b'', mandatory=True):
1074 def addparam(self, name, value=b'', mandatory=True):
1075 """add a parameter to the part
1075 """add a parameter to the part
1076
1076
1077 If 'mandatory' is set to True, the remote handler must claim support
1077 If 'mandatory' is set to True, the remote handler must claim support
1078 for this parameter or the unbundling will be aborted.
1078 for this parameter or the unbundling will be aborted.
1079
1079
1080 The 'name' and 'value' cannot exceed 255 bytes each.
1080 The 'name' and 'value' cannot exceed 255 bytes each.
1081 """
1081 """
1082 if self._generated is not None:
1082 if self._generated is not None:
1083 raise error.ReadOnlyPartError(b'part is being generated')
1083 raise error.ReadOnlyPartError(b'part is being generated')
1084 if name in self._seenparams:
1084 if name in self._seenparams:
1085 raise ValueError(b'duplicated params: %s' % name)
1085 raise ValueError(b'duplicated params: %s' % name)
1086 self._seenparams.add(name)
1086 self._seenparams.add(name)
1087 params = self._advisoryparams
1087 params = self._advisoryparams
1088 if mandatory:
1088 if mandatory:
1089 params = self._mandatoryparams
1089 params = self._mandatoryparams
1090 params.append((name, value))
1090 params.append((name, value))
1091
1091
1092 # methods used to generates the bundle2 stream
1092 # methods used to generates the bundle2 stream
1093 def getchunks(self, ui):
1093 def getchunks(self, ui):
1094 if self._generated is not None:
1094 if self._generated is not None:
1095 raise error.ProgrammingError(b'part can only be consumed once')
1095 raise error.ProgrammingError(b'part can only be consumed once')
1096 self._generated = False
1096 self._generated = False
1097
1097
1098 if ui.debugflag:
1098 if ui.debugflag:
1099 msg = [b'bundle2-output-part: "%s"' % self.type]
1099 msg = [b'bundle2-output-part: "%s"' % self.type]
1100 if not self.mandatory:
1100 if not self.mandatory:
1101 msg.append(b' (advisory)')
1101 msg.append(b' (advisory)')
1102 nbmp = len(self.mandatoryparams)
1102 nbmp = len(self.mandatoryparams)
1103 nbap = len(self.advisoryparams)
1103 nbap = len(self.advisoryparams)
1104 if nbmp or nbap:
1104 if nbmp or nbap:
1105 msg.append(b' (params:')
1105 msg.append(b' (params:')
1106 if nbmp:
1106 if nbmp:
1107 msg.append(b' %i mandatory' % nbmp)
1107 msg.append(b' %i mandatory' % nbmp)
1108 if nbap:
1108 if nbap:
1109 msg.append(b' %i advisory' % nbmp)
1109 msg.append(b' %i advisory' % nbmp)
1110 msg.append(b')')
1110 msg.append(b')')
1111 if not self.data:
1111 if not self.data:
1112 msg.append(b' empty payload')
1112 msg.append(b' empty payload')
1113 elif util.safehasattr(self.data, 'next') or util.safehasattr(
1113 elif util.safehasattr(self.data, 'next') or util.safehasattr(
1114 self.data, b'__next__'
1114 self.data, b'__next__'
1115 ):
1115 ):
1116 msg.append(b' streamed payload')
1116 msg.append(b' streamed payload')
1117 else:
1117 else:
1118 msg.append(b' %i bytes payload' % len(self.data))
1118 msg.append(b' %i bytes payload' % len(self.data))
1119 msg.append(b'\n')
1119 msg.append(b'\n')
1120 ui.debug(b''.join(msg))
1120 ui.debug(b''.join(msg))
1121
1121
1122 #### header
1122 #### header
1123 if self.mandatory:
1123 if self.mandatory:
1124 parttype = self.type.upper()
1124 parttype = self.type.upper()
1125 else:
1125 else:
1126 parttype = self.type.lower()
1126 parttype = self.type.lower()
1127 outdebug(ui, b'part %s: "%s"' % (pycompat.bytestr(self.id), parttype))
1127 outdebug(ui, b'part %s: "%s"' % (pycompat.bytestr(self.id), parttype))
1128 ## parttype
1128 ## parttype
1129 header = [
1129 header = [
1130 _pack(_fparttypesize, len(parttype)),
1130 _pack(_fparttypesize, len(parttype)),
1131 parttype,
1131 parttype,
1132 _pack(_fpartid, self.id),
1132 _pack(_fpartid, self.id),
1133 ]
1133 ]
1134 ## parameters
1134 ## parameters
1135 # count
1135 # count
1136 manpar = self.mandatoryparams
1136 manpar = self.mandatoryparams
1137 advpar = self.advisoryparams
1137 advpar = self.advisoryparams
1138 header.append(_pack(_fpartparamcount, len(manpar), len(advpar)))
1138 header.append(_pack(_fpartparamcount, len(manpar), len(advpar)))
1139 # size
1139 # size
1140 parsizes = []
1140 parsizes = []
1141 for key, value in manpar:
1141 for key, value in manpar:
1142 parsizes.append(len(key))
1142 parsizes.append(len(key))
1143 parsizes.append(len(value))
1143 parsizes.append(len(value))
1144 for key, value in advpar:
1144 for key, value in advpar:
1145 parsizes.append(len(key))
1145 parsizes.append(len(key))
1146 parsizes.append(len(value))
1146 parsizes.append(len(value))
1147 paramsizes = _pack(_makefpartparamsizes(len(parsizes) // 2), *parsizes)
1147 paramsizes = _pack(_makefpartparamsizes(len(parsizes) // 2), *parsizes)
1148 header.append(paramsizes)
1148 header.append(paramsizes)
1149 # key, value
1149 # key, value
1150 for key, value in manpar:
1150 for key, value in manpar:
1151 header.append(key)
1151 header.append(key)
1152 header.append(value)
1152 header.append(value)
1153 for key, value in advpar:
1153 for key, value in advpar:
1154 header.append(key)
1154 header.append(key)
1155 header.append(value)
1155 header.append(value)
1156 ## finalize header
1156 ## finalize header
1157 try:
1157 try:
1158 headerchunk = b''.join(header)
1158 headerchunk = b''.join(header)
1159 except TypeError:
1159 except TypeError:
1160 raise TypeError(
1160 raise TypeError(
1161 'Found a non-bytes trying to '
1161 'Found a non-bytes trying to '
1162 'build bundle part header: %r' % header
1162 'build bundle part header: %r' % header
1163 )
1163 )
1164 outdebug(ui, b'header chunk size: %i' % len(headerchunk))
1164 outdebug(ui, b'header chunk size: %i' % len(headerchunk))
1165 yield _pack(_fpartheadersize, len(headerchunk))
1165 yield _pack(_fpartheadersize, len(headerchunk))
1166 yield headerchunk
1166 yield headerchunk
1167 ## payload
1167 ## payload
1168 try:
1168 try:
1169 for chunk in self._payloadchunks():
1169 for chunk in self._payloadchunks():
1170 outdebug(ui, b'payload chunk size: %i' % len(chunk))
1170 outdebug(ui, b'payload chunk size: %i' % len(chunk))
1171 yield _pack(_fpayloadsize, len(chunk))
1171 yield _pack(_fpayloadsize, len(chunk))
1172 yield chunk
1172 yield chunk
1173 except GeneratorExit:
1173 except GeneratorExit:
1174 # GeneratorExit means that nobody is listening for our
1174 # GeneratorExit means that nobody is listening for our
1175 # results anyway, so just bail quickly rather than trying
1175 # results anyway, so just bail quickly rather than trying
1176 # to produce an error part.
1176 # to produce an error part.
1177 ui.debug(b'bundle2-generatorexit\n')
1177 ui.debug(b'bundle2-generatorexit\n')
1178 raise
1178 raise
1179 except BaseException as exc:
1179 except BaseException as exc:
1180 bexc = stringutil.forcebytestr(exc)
1180 bexc = stringutil.forcebytestr(exc)
1181 # backup exception data for later
1181 # backup exception data for later
1182 ui.debug(
1182 ui.debug(
1183 b'bundle2-input-stream-interrupt: encoding exception %s' % bexc
1183 b'bundle2-input-stream-interrupt: encoding exception %s' % bexc
1184 )
1184 )
1185 tb = sys.exc_info()[2]
1185 tb = sys.exc_info()[2]
1186 msg = b'unexpected error: %s' % bexc
1186 msg = b'unexpected error: %s' % bexc
1187 interpart = bundlepart(
1187 interpart = bundlepart(
1188 b'error:abort', [(b'message', msg)], mandatory=False
1188 b'error:abort', [(b'message', msg)], mandatory=False
1189 )
1189 )
1190 interpart.id = 0
1190 interpart.id = 0
1191 yield _pack(_fpayloadsize, -1)
1191 yield _pack(_fpayloadsize, -1)
1192 for chunk in interpart.getchunks(ui=ui):
1192 for chunk in interpart.getchunks(ui=ui):
1193 yield chunk
1193 yield chunk
1194 outdebug(ui, b'closing payload chunk')
1194 outdebug(ui, b'closing payload chunk')
1195 # abort current part payload
1195 # abort current part payload
1196 yield _pack(_fpayloadsize, 0)
1196 yield _pack(_fpayloadsize, 0)
1197 pycompat.raisewithtb(exc, tb)
1197 pycompat.raisewithtb(exc, tb)
1198 # end of payload
1198 # end of payload
1199 outdebug(ui, b'closing payload chunk')
1199 outdebug(ui, b'closing payload chunk')
1200 yield _pack(_fpayloadsize, 0)
1200 yield _pack(_fpayloadsize, 0)
1201 self._generated = True
1201 self._generated = True
1202
1202
1203 def _payloadchunks(self):
1203 def _payloadchunks(self):
1204 """yield chunks of a the part payload
1204 """yield chunks of a the part payload
1205
1205
1206 Exists to handle the different methods to provide data to a part."""
1206 Exists to handle the different methods to provide data to a part."""
1207 # we only support fixed size data now.
1207 # we only support fixed size data now.
1208 # This will be improved in the future.
1208 # This will be improved in the future.
1209 if util.safehasattr(self.data, 'next') or util.safehasattr(
1209 if util.safehasattr(self.data, 'next') or util.safehasattr(
1210 self.data, b'__next__'
1210 self.data, b'__next__'
1211 ):
1211 ):
1212 buff = util.chunkbuffer(self.data)
1212 buff = util.chunkbuffer(self.data)
1213 chunk = buff.read(preferedchunksize)
1213 chunk = buff.read(preferedchunksize)
1214 while chunk:
1214 while chunk:
1215 yield chunk
1215 yield chunk
1216 chunk = buff.read(preferedchunksize)
1216 chunk = buff.read(preferedchunksize)
1217 elif len(self.data):
1217 elif len(self.data):
1218 yield self.data
1218 yield self.data
1219
1219
1220
1220
1221 flaginterrupt = -1
1221 flaginterrupt = -1
1222
1222
1223
1223
1224 class interrupthandler(unpackermixin):
1224 class interrupthandler(unpackermixin):
1225 """read one part and process it with restricted capability
1225 """read one part and process it with restricted capability
1226
1226
1227 This allows to transmit exception raised on the producer size during part
1227 This allows to transmit exception raised on the producer size during part
1228 iteration while the consumer is reading a part.
1228 iteration while the consumer is reading a part.
1229
1229
1230 Part processed in this manner only have access to a ui object,"""
1230 Part processed in this manner only have access to a ui object,"""
1231
1231
1232 def __init__(self, ui, fp):
1232 def __init__(self, ui, fp):
1233 super(interrupthandler, self).__init__(fp)
1233 super(interrupthandler, self).__init__(fp)
1234 self.ui = ui
1234 self.ui = ui
1235
1235
1236 def _readpartheader(self):
1236 def _readpartheader(self):
1237 """reads a part header size and return the bytes blob
1237 """reads a part header size and return the bytes blob
1238
1238
1239 returns None if empty"""
1239 returns None if empty"""
1240 headersize = self._unpack(_fpartheadersize)[0]
1240 headersize = self._unpack(_fpartheadersize)[0]
1241 if headersize < 0:
1241 if headersize < 0:
1242 raise error.BundleValueError(
1242 raise error.BundleValueError(
1243 b'negative part header size: %i' % headersize
1243 b'negative part header size: %i' % headersize
1244 )
1244 )
1245 indebug(self.ui, b'part header size: %i\n' % headersize)
1245 indebug(self.ui, b'part header size: %i\n' % headersize)
1246 if headersize:
1246 if headersize:
1247 return self._readexact(headersize)
1247 return self._readexact(headersize)
1248 return None
1248 return None
1249
1249
1250 def __call__(self):
1250 def __call__(self):
1251
1251
1252 self.ui.debug(
1252 self.ui.debug(
1253 b'bundle2-input-stream-interrupt: opening out of band context\n'
1253 b'bundle2-input-stream-interrupt: opening out of band context\n'
1254 )
1254 )
1255 indebug(self.ui, b'bundle2 stream interruption, looking for a part.')
1255 indebug(self.ui, b'bundle2 stream interruption, looking for a part.')
1256 headerblock = self._readpartheader()
1256 headerblock = self._readpartheader()
1257 if headerblock is None:
1257 if headerblock is None:
1258 indebug(self.ui, b'no part found during interruption.')
1258 indebug(self.ui, b'no part found during interruption.')
1259 return
1259 return
1260 part = unbundlepart(self.ui, headerblock, self._fp)
1260 part = unbundlepart(self.ui, headerblock, self._fp)
1261 op = interruptoperation(self.ui)
1261 op = interruptoperation(self.ui)
1262 hardabort = False
1262 hardabort = False
1263 try:
1263 try:
1264 _processpart(op, part)
1264 _processpart(op, part)
1265 except (SystemExit, KeyboardInterrupt):
1265 except (SystemExit, KeyboardInterrupt):
1266 hardabort = True
1266 hardabort = True
1267 raise
1267 raise
1268 finally:
1268 finally:
1269 if not hardabort:
1269 if not hardabort:
1270 part.consume()
1270 part.consume()
1271 self.ui.debug(
1271 self.ui.debug(
1272 b'bundle2-input-stream-interrupt: closing out of band context\n'
1272 b'bundle2-input-stream-interrupt: closing out of band context\n'
1273 )
1273 )
1274
1274
1275
1275
1276 class interruptoperation:
1276 class interruptoperation:
1277 """A limited operation to be use by part handler during interruption
1277 """A limited operation to be use by part handler during interruption
1278
1278
1279 It only have access to an ui object.
1279 It only have access to an ui object.
1280 """
1280 """
1281
1281
1282 def __init__(self, ui):
1282 def __init__(self, ui):
1283 self.ui = ui
1283 self.ui = ui
1284 self.reply = None
1284 self.reply = None
1285 self.captureoutput = False
1285 self.captureoutput = False
1286
1286
1287 @property
1287 @property
1288 def repo(self):
1288 def repo(self):
1289 raise error.ProgrammingError(b'no repo access from stream interruption')
1289 raise error.ProgrammingError(b'no repo access from stream interruption')
1290
1290
1291 def gettransaction(self):
1291 def gettransaction(self):
1292 raise TransactionUnavailable(b'no repo access from stream interruption')
1292 raise TransactionUnavailable(b'no repo access from stream interruption')
1293
1293
1294
1294
1295 def decodepayloadchunks(ui, fh):
1295 def decodepayloadchunks(ui, fh):
1296 """Reads bundle2 part payload data into chunks.
1296 """Reads bundle2 part payload data into chunks.
1297
1297
1298 Part payload data consists of framed chunks. This function takes
1298 Part payload data consists of framed chunks. This function takes
1299 a file handle and emits those chunks.
1299 a file handle and emits those chunks.
1300 """
1300 """
1301 dolog = ui.configbool(b'devel', b'bundle2.debug')
1301 dolog = ui.configbool(b'devel', b'bundle2.debug')
1302 debug = ui.debug
1302 debug = ui.debug
1303
1303
1304 headerstruct = struct.Struct(_fpayloadsize)
1304 headerstruct = struct.Struct(_fpayloadsize)
1305 headersize = headerstruct.size
1305 headersize = headerstruct.size
1306 unpack = headerstruct.unpack
1306 unpack = headerstruct.unpack
1307
1307
1308 readexactly = changegroup.readexactly
1308 readexactly = changegroup.readexactly
1309 read = fh.read
1309 read = fh.read
1310
1310
1311 chunksize = unpack(readexactly(fh, headersize))[0]
1311 chunksize = unpack(readexactly(fh, headersize))[0]
1312 indebug(ui, b'payload chunk size: %i' % chunksize)
1312 indebug(ui, b'payload chunk size: %i' % chunksize)
1313
1313
1314 # changegroup.readexactly() is inlined below for performance.
1314 # changegroup.readexactly() is inlined below for performance.
1315 while chunksize:
1315 while chunksize:
1316 if chunksize >= 0:
1316 if chunksize >= 0:
1317 s = read(chunksize)
1317 s = read(chunksize)
1318 if len(s) < chunksize:
1318 if len(s) < chunksize:
1319 raise error.Abort(
1319 raise error.Abort(
1320 _(
1320 _(
1321 b'stream ended unexpectedly '
1321 b'stream ended unexpectedly '
1322 b' (got %d bytes, expected %d)'
1322 b' (got %d bytes, expected %d)'
1323 )
1323 )
1324 % (len(s), chunksize)
1324 % (len(s), chunksize)
1325 )
1325 )
1326
1326
1327 yield s
1327 yield s
1328 elif chunksize == flaginterrupt:
1328 elif chunksize == flaginterrupt:
1329 # Interrupt "signal" detected. The regular stream is interrupted
1329 # Interrupt "signal" detected. The regular stream is interrupted
1330 # and a bundle2 part follows. Consume it.
1330 # and a bundle2 part follows. Consume it.
1331 interrupthandler(ui, fh)()
1331 interrupthandler(ui, fh)()
1332 else:
1332 else:
1333 raise error.BundleValueError(
1333 raise error.BundleValueError(
1334 b'negative payload chunk size: %s' % chunksize
1334 b'negative payload chunk size: %s' % chunksize
1335 )
1335 )
1336
1336
1337 s = read(headersize)
1337 s = read(headersize)
1338 if len(s) < headersize:
1338 if len(s) < headersize:
1339 raise error.Abort(
1339 raise error.Abort(
1340 _(b'stream ended unexpectedly (got %d bytes, expected %d)')
1340 _(b'stream ended unexpectedly (got %d bytes, expected %d)')
1341 % (len(s), chunksize)
1341 % (len(s), chunksize)
1342 )
1342 )
1343
1343
1344 chunksize = unpack(s)[0]
1344 chunksize = unpack(s)[0]
1345
1345
1346 # indebug() inlined for performance.
1346 # indebug() inlined for performance.
1347 if dolog:
1347 if dolog:
1348 debug(b'bundle2-input: payload chunk size: %i\n' % chunksize)
1348 debug(b'bundle2-input: payload chunk size: %i\n' % chunksize)
1349
1349
1350
1350
1351 class unbundlepart(unpackermixin):
1351 class unbundlepart(unpackermixin):
1352 """a bundle part read from a bundle"""
1352 """a bundle part read from a bundle"""
1353
1353
1354 def __init__(self, ui, header, fp):
1354 def __init__(self, ui, header, fp):
1355 super(unbundlepart, self).__init__(fp)
1355 super(unbundlepart, self).__init__(fp)
1356 self._seekable = util.safehasattr(fp, 'seek') and util.safehasattr(
1356 self._seekable = util.safehasattr(fp, 'seek') and util.safehasattr(
1357 fp, b'tell'
1357 fp, b'tell'
1358 )
1358 )
1359 self.ui = ui
1359 self.ui = ui
1360 # unbundle state attr
1360 # unbundle state attr
1361 self._headerdata = header
1361 self._headerdata = header
1362 self._headeroffset = 0
1362 self._headeroffset = 0
1363 self._initialized = False
1363 self._initialized = False
1364 self.consumed = False
1364 self.consumed = False
1365 # part data
1365 # part data
1366 self.id = None
1366 self.id = None
1367 self.type = None
1367 self.type = None
1368 self.mandatoryparams = None
1368 self.mandatoryparams = None
1369 self.advisoryparams = None
1369 self.advisoryparams = None
1370 self.params = None
1370 self.params = None
1371 self.mandatorykeys = ()
1371 self.mandatorykeys = ()
1372 self._readheader()
1372 self._readheader()
1373 self._mandatory = None
1373 self._mandatory = None
1374 self._pos = 0
1374 self._pos = 0
1375
1375
1376 def _fromheader(self, size):
1376 def _fromheader(self, size):
1377 """return the next <size> byte from the header"""
1377 """return the next <size> byte from the header"""
1378 offset = self._headeroffset
1378 offset = self._headeroffset
1379 data = self._headerdata[offset : (offset + size)]
1379 data = self._headerdata[offset : (offset + size)]
1380 self._headeroffset = offset + size
1380 self._headeroffset = offset + size
1381 return data
1381 return data
1382
1382
1383 def _unpackheader(self, format):
1383 def _unpackheader(self, format):
1384 """read given format from header
1384 """read given format from header
1385
1385
1386 This automatically compute the size of the format to read."""
1386 This automatically compute the size of the format to read."""
1387 data = self._fromheader(struct.calcsize(format))
1387 data = self._fromheader(struct.calcsize(format))
1388 return _unpack(format, data)
1388 return _unpack(format, data)
1389
1389
1390 def _initparams(self, mandatoryparams, advisoryparams):
1390 def _initparams(self, mandatoryparams, advisoryparams):
1391 """internal function to setup all logic related parameters"""
1391 """internal function to setup all logic related parameters"""
1392 # make it read only to prevent people touching it by mistake.
1392 # make it read only to prevent people touching it by mistake.
1393 self.mandatoryparams = tuple(mandatoryparams)
1393 self.mandatoryparams = tuple(mandatoryparams)
1394 self.advisoryparams = tuple(advisoryparams)
1394 self.advisoryparams = tuple(advisoryparams)
1395 # user friendly UI
1395 # user friendly UI
1396 self.params = util.sortdict(self.mandatoryparams)
1396 self.params = util.sortdict(self.mandatoryparams)
1397 self.params.update(self.advisoryparams)
1397 self.params.update(self.advisoryparams)
1398 self.mandatorykeys = frozenset(p[0] for p in mandatoryparams)
1398 self.mandatorykeys = frozenset(p[0] for p in mandatoryparams)
1399
1399
1400 def _readheader(self):
1400 def _readheader(self):
1401 """read the header and setup the object"""
1401 """read the header and setup the object"""
1402 typesize = self._unpackheader(_fparttypesize)[0]
1402 typesize = self._unpackheader(_fparttypesize)[0]
1403 self.type = self._fromheader(typesize)
1403 self.type = self._fromheader(typesize)
1404 indebug(self.ui, b'part type: "%s"' % self.type)
1404 indebug(self.ui, b'part type: "%s"' % self.type)
1405 self.id = self._unpackheader(_fpartid)[0]
1405 self.id = self._unpackheader(_fpartid)[0]
1406 indebug(self.ui, b'part id: "%s"' % pycompat.bytestr(self.id))
1406 indebug(self.ui, b'part id: "%s"' % pycompat.bytestr(self.id))
1407 # extract mandatory bit from type
1407 # extract mandatory bit from type
1408 self.mandatory = self.type != self.type.lower()
1408 self.mandatory = self.type != self.type.lower()
1409 self.type = self.type.lower()
1409 self.type = self.type.lower()
1410 ## reading parameters
1410 ## reading parameters
1411 # param count
1411 # param count
1412 mancount, advcount = self._unpackheader(_fpartparamcount)
1412 mancount, advcount = self._unpackheader(_fpartparamcount)
1413 indebug(self.ui, b'part parameters: %i' % (mancount + advcount))
1413 indebug(self.ui, b'part parameters: %i' % (mancount + advcount))
1414 # param size
1414 # param size
1415 fparamsizes = _makefpartparamsizes(mancount + advcount)
1415 fparamsizes = _makefpartparamsizes(mancount + advcount)
1416 paramsizes = self._unpackheader(fparamsizes)
1416 paramsizes = self._unpackheader(fparamsizes)
1417 # make it a list of couple again
1417 # make it a list of couple again
1418 paramsizes = list(zip(paramsizes[::2], paramsizes[1::2]))
1418 paramsizes = list(zip(paramsizes[::2], paramsizes[1::2]))
1419 # split mandatory from advisory
1419 # split mandatory from advisory
1420 mansizes = paramsizes[:mancount]
1420 mansizes = paramsizes[:mancount]
1421 advsizes = paramsizes[mancount:]
1421 advsizes = paramsizes[mancount:]
1422 # retrieve param value
1422 # retrieve param value
1423 manparams = []
1423 manparams = []
1424 for key, value in mansizes:
1424 for key, value in mansizes:
1425 manparams.append((self._fromheader(key), self._fromheader(value)))
1425 manparams.append((self._fromheader(key), self._fromheader(value)))
1426 advparams = []
1426 advparams = []
1427 for key, value in advsizes:
1427 for key, value in advsizes:
1428 advparams.append((self._fromheader(key), self._fromheader(value)))
1428 advparams.append((self._fromheader(key), self._fromheader(value)))
1429 self._initparams(manparams, advparams)
1429 self._initparams(manparams, advparams)
1430 ## part payload
1430 ## part payload
1431 self._payloadstream = util.chunkbuffer(self._payloadchunks())
1431 self._payloadstream = util.chunkbuffer(self._payloadchunks())
1432 # we read the data, tell it
1432 # we read the data, tell it
1433 self._initialized = True
1433 self._initialized = True
1434
1434
1435 def _payloadchunks(self):
1435 def _payloadchunks(self):
1436 """Generator of decoded chunks in the payload."""
1436 """Generator of decoded chunks in the payload."""
1437 return decodepayloadchunks(self.ui, self._fp)
1437 return decodepayloadchunks(self.ui, self._fp)
1438
1438
1439 def consume(self):
1439 def consume(self):
1440 """Read the part payload until completion.
1440 """Read the part payload until completion.
1441
1441
1442 By consuming the part data, the underlying stream read offset will
1442 By consuming the part data, the underlying stream read offset will
1443 be advanced to the next part (or end of stream).
1443 be advanced to the next part (or end of stream).
1444 """
1444 """
1445 if self.consumed:
1445 if self.consumed:
1446 return
1446 return
1447
1447
1448 chunk = self.read(32768)
1448 chunk = self.read(32768)
1449 while chunk:
1449 while chunk:
1450 self._pos += len(chunk)
1450 self._pos += len(chunk)
1451 chunk = self.read(32768)
1451 chunk = self.read(32768)
1452
1452
1453 def read(self, size=None):
1453 def read(self, size=None):
1454 """read payload data"""
1454 """read payload data"""
1455 if not self._initialized:
1455 if not self._initialized:
1456 self._readheader()
1456 self._readheader()
1457 if size is None:
1457 if size is None:
1458 data = self._payloadstream.read()
1458 data = self._payloadstream.read()
1459 else:
1459 else:
1460 data = self._payloadstream.read(size)
1460 data = self._payloadstream.read(size)
1461 self._pos += len(data)
1461 self._pos += len(data)
1462 if size is None or len(data) < size:
1462 if size is None or len(data) < size:
1463 if not self.consumed and self._pos:
1463 if not self.consumed and self._pos:
1464 self.ui.debug(
1464 self.ui.debug(
1465 b'bundle2-input-part: total payload size %i\n' % self._pos
1465 b'bundle2-input-part: total payload size %i\n' % self._pos
1466 )
1466 )
1467 self.consumed = True
1467 self.consumed = True
1468 return data
1468 return data
1469
1469
1470
1470
1471 class seekableunbundlepart(unbundlepart):
1471 class seekableunbundlepart(unbundlepart):
1472 """A bundle2 part in a bundle that is seekable.
1472 """A bundle2 part in a bundle that is seekable.
1473
1473
1474 Regular ``unbundlepart`` instances can only be read once. This class
1474 Regular ``unbundlepart`` instances can only be read once. This class
1475 extends ``unbundlepart`` to enable bi-directional seeking within the
1475 extends ``unbundlepart`` to enable bi-directional seeking within the
1476 part.
1476 part.
1477
1477
1478 Bundle2 part data consists of framed chunks. Offsets when seeking
1478 Bundle2 part data consists of framed chunks. Offsets when seeking
1479 refer to the decoded data, not the offsets in the underlying bundle2
1479 refer to the decoded data, not the offsets in the underlying bundle2
1480 stream.
1480 stream.
1481
1481
1482 To facilitate quickly seeking within the decoded data, instances of this
1482 To facilitate quickly seeking within the decoded data, instances of this
1483 class maintain a mapping between offsets in the underlying stream and
1483 class maintain a mapping between offsets in the underlying stream and
1484 the decoded payload. This mapping will consume memory in proportion
1484 the decoded payload. This mapping will consume memory in proportion
1485 to the number of chunks within the payload (which almost certainly
1485 to the number of chunks within the payload (which almost certainly
1486 increases in proportion with the size of the part).
1486 increases in proportion with the size of the part).
1487 """
1487 """
1488
1488
1489 def __init__(self, ui, header, fp):
1489 def __init__(self, ui, header, fp):
1490 # (payload, file) offsets for chunk starts.
1490 # (payload, file) offsets for chunk starts.
1491 self._chunkindex = []
1491 self._chunkindex = []
1492
1492
1493 super(seekableunbundlepart, self).__init__(ui, header, fp)
1493 super(seekableunbundlepart, self).__init__(ui, header, fp)
1494
1494
1495 def _payloadchunks(self, chunknum=0):
1495 def _payloadchunks(self, chunknum=0):
1496 '''seek to specified chunk and start yielding data'''
1496 '''seek to specified chunk and start yielding data'''
1497 if len(self._chunkindex) == 0:
1497 if len(self._chunkindex) == 0:
1498 assert chunknum == 0, b'Must start with chunk 0'
1498 assert chunknum == 0, b'Must start with chunk 0'
1499 self._chunkindex.append((0, self._tellfp()))
1499 self._chunkindex.append((0, self._tellfp()))
1500 else:
1500 else:
1501 assert chunknum < len(self._chunkindex), (
1501 assert chunknum < len(self._chunkindex), (
1502 b'Unknown chunk %d' % chunknum
1502 b'Unknown chunk %d' % chunknum
1503 )
1503 )
1504 self._seekfp(self._chunkindex[chunknum][1])
1504 self._seekfp(self._chunkindex[chunknum][1])
1505
1505
1506 pos = self._chunkindex[chunknum][0]
1506 pos = self._chunkindex[chunknum][0]
1507
1507
1508 for chunk in decodepayloadchunks(self.ui, self._fp):
1508 for chunk in decodepayloadchunks(self.ui, self._fp):
1509 chunknum += 1
1509 chunknum += 1
1510 pos += len(chunk)
1510 pos += len(chunk)
1511 if chunknum == len(self._chunkindex):
1511 if chunknum == len(self._chunkindex):
1512 self._chunkindex.append((pos, self._tellfp()))
1512 self._chunkindex.append((pos, self._tellfp()))
1513
1513
1514 yield chunk
1514 yield chunk
1515
1515
1516 def _findchunk(self, pos):
1516 def _findchunk(self, pos):
1517 '''for a given payload position, return a chunk number and offset'''
1517 '''for a given payload position, return a chunk number and offset'''
1518 for chunk, (ppos, fpos) in enumerate(self._chunkindex):
1518 for chunk, (ppos, fpos) in enumerate(self._chunkindex):
1519 if ppos == pos:
1519 if ppos == pos:
1520 return chunk, 0
1520 return chunk, 0
1521 elif ppos > pos:
1521 elif ppos > pos:
1522 return chunk - 1, pos - self._chunkindex[chunk - 1][0]
1522 return chunk - 1, pos - self._chunkindex[chunk - 1][0]
1523 raise ValueError(b'Unknown chunk')
1523 raise ValueError(b'Unknown chunk')
1524
1524
1525 def tell(self):
1525 def tell(self):
1526 return self._pos
1526 return self._pos
1527
1527
1528 def seek(self, offset, whence=os.SEEK_SET):
1528 def seek(self, offset, whence=os.SEEK_SET):
1529 if whence == os.SEEK_SET:
1529 if whence == os.SEEK_SET:
1530 newpos = offset
1530 newpos = offset
1531 elif whence == os.SEEK_CUR:
1531 elif whence == os.SEEK_CUR:
1532 newpos = self._pos + offset
1532 newpos = self._pos + offset
1533 elif whence == os.SEEK_END:
1533 elif whence == os.SEEK_END:
1534 if not self.consumed:
1534 if not self.consumed:
1535 # Can't use self.consume() here because it advances self._pos.
1535 # Can't use self.consume() here because it advances self._pos.
1536 chunk = self.read(32768)
1536 chunk = self.read(32768)
1537 while chunk:
1537 while chunk:
1538 chunk = self.read(32768)
1538 chunk = self.read(32768)
1539 newpos = self._chunkindex[-1][0] - offset
1539 newpos = self._chunkindex[-1][0] - offset
1540 else:
1540 else:
1541 raise ValueError(b'Unknown whence value: %r' % (whence,))
1541 raise ValueError(b'Unknown whence value: %r' % (whence,))
1542
1542
1543 if newpos > self._chunkindex[-1][0] and not self.consumed:
1543 if newpos > self._chunkindex[-1][0] and not self.consumed:
1544 # Can't use self.consume() here because it advances self._pos.
1544 # Can't use self.consume() here because it advances self._pos.
1545 chunk = self.read(32768)
1545 chunk = self.read(32768)
1546 while chunk:
1546 while chunk:
1547 chunk = self.read(32668)
1547 chunk = self.read(32668)
1548
1548
1549 if not 0 <= newpos <= self._chunkindex[-1][0]:
1549 if not 0 <= newpos <= self._chunkindex[-1][0]:
1550 raise ValueError(b'Offset out of range')
1550 raise ValueError(b'Offset out of range')
1551
1551
1552 if self._pos != newpos:
1552 if self._pos != newpos:
1553 chunk, internaloffset = self._findchunk(newpos)
1553 chunk, internaloffset = self._findchunk(newpos)
1554 self._payloadstream = util.chunkbuffer(self._payloadchunks(chunk))
1554 self._payloadstream = util.chunkbuffer(self._payloadchunks(chunk))
1555 adjust = self.read(internaloffset)
1555 adjust = self.read(internaloffset)
1556 if len(adjust) != internaloffset:
1556 if len(adjust) != internaloffset:
1557 raise error.Abort(_(b'Seek failed\n'))
1557 raise error.Abort(_(b'Seek failed\n'))
1558 self._pos = newpos
1558 self._pos = newpos
1559
1559
1560 def _seekfp(self, offset, whence=0):
1560 def _seekfp(self, offset, whence=0):
1561 """move the underlying file pointer
1561 """move the underlying file pointer
1562
1562
1563 This method is meant for internal usage by the bundle2 protocol only.
1563 This method is meant for internal usage by the bundle2 protocol only.
1564 They directly manipulate the low level stream including bundle2 level
1564 They directly manipulate the low level stream including bundle2 level
1565 instruction.
1565 instruction.
1566
1566
1567 Do not use it to implement higher-level logic or methods."""
1567 Do not use it to implement higher-level logic or methods."""
1568 if self._seekable:
1568 if self._seekable:
1569 return self._fp.seek(offset, whence)
1569 return self._fp.seek(offset, whence)
1570 else:
1570 else:
1571 raise NotImplementedError(_(b'File pointer is not seekable'))
1571 raise NotImplementedError(_(b'File pointer is not seekable'))
1572
1572
1573 def _tellfp(self):
1573 def _tellfp(self):
1574 """return the file offset, or None if file is not seekable
1574 """return the file offset, or None if file is not seekable
1575
1575
1576 This method is meant for internal usage by the bundle2 protocol only.
1576 This method is meant for internal usage by the bundle2 protocol only.
1577 They directly manipulate the low level stream including bundle2 level
1577 They directly manipulate the low level stream including bundle2 level
1578 instruction.
1578 instruction.
1579
1579
1580 Do not use it to implement higher-level logic or methods."""
1580 Do not use it to implement higher-level logic or methods."""
1581 if self._seekable:
1581 if self._seekable:
1582 try:
1582 try:
1583 return self._fp.tell()
1583 return self._fp.tell()
1584 except IOError as e:
1584 except IOError as e:
1585 if e.errno == errno.ESPIPE:
1585 if e.errno == errno.ESPIPE:
1586 self._seekable = False
1586 self._seekable = False
1587 else:
1587 else:
1588 raise
1588 raise
1589 return None
1589 return None
1590
1590
1591
1591
1592 # These are only the static capabilities.
1592 # These are only the static capabilities.
1593 # Check the 'getrepocaps' function for the rest.
1593 # Check the 'getrepocaps' function for the rest.
1594 capabilities = {
1594 capabilities = {
1595 b'HG20': (),
1595 b'HG20': (),
1596 b'bookmarks': (),
1596 b'bookmarks': (),
1597 b'error': (b'abort', b'unsupportedcontent', b'pushraced', b'pushkey'),
1597 b'error': (b'abort', b'unsupportedcontent', b'pushraced', b'pushkey'),
1598 b'listkeys': (),
1598 b'listkeys': (),
1599 b'pushkey': (),
1599 b'pushkey': (),
1600 b'digests': tuple(sorted(util.DIGESTS.keys())),
1600 b'digests': tuple(sorted(util.DIGESTS.keys())),
1601 b'remote-changegroup': (b'http', b'https'),
1601 b'remote-changegroup': (b'http', b'https'),
1602 b'hgtagsfnodes': (),
1602 b'hgtagsfnodes': (),
1603 b'phases': (b'heads',),
1603 b'phases': (b'heads',),
1604 b'stream': (b'v2',),
1604 b'stream': (b'v2',),
1605 }
1605 }
1606
1606
1607
1607
1608 def getrepocaps(repo, allowpushback=False, role=None):
1608 def getrepocaps(repo, allowpushback=False, role=None):
1609 """return the bundle2 capabilities for a given repo
1609 """return the bundle2 capabilities for a given repo
1610
1610
1611 Exists to allow extensions (like evolution) to mutate the capabilities.
1611 Exists to allow extensions (like evolution) to mutate the capabilities.
1612
1612
1613 The returned value is used for servers advertising their capabilities as
1613 The returned value is used for servers advertising their capabilities as
1614 well as clients advertising their capabilities to servers as part of
1614 well as clients advertising their capabilities to servers as part of
1615 bundle2 requests. The ``role`` argument specifies which is which.
1615 bundle2 requests. The ``role`` argument specifies which is which.
1616 """
1616 """
1617 if role not in (b'client', b'server'):
1617 if role not in (b'client', b'server'):
1618 raise error.ProgrammingError(b'role argument must be client or server')
1618 raise error.ProgrammingError(b'role argument must be client or server')
1619
1619
1620 caps = capabilities.copy()
1620 caps = capabilities.copy()
1621 caps[b'changegroup'] = tuple(
1621 caps[b'changegroup'] = tuple(
1622 sorted(changegroup.supportedincomingversions(repo))
1622 sorted(changegroup.supportedincomingversions(repo))
1623 )
1623 )
1624 if obsolete.isenabled(repo, obsolete.exchangeopt):
1624 if obsolete.isenabled(repo, obsolete.exchangeopt):
1625 supportedformat = tuple(b'V%i' % v for v in obsolete.formats)
1625 supportedformat = tuple(b'V%i' % v for v in obsolete.formats)
1626 caps[b'obsmarkers'] = supportedformat
1626 caps[b'obsmarkers'] = supportedformat
1627 if allowpushback:
1627 if allowpushback:
1628 caps[b'pushback'] = ()
1628 caps[b'pushback'] = ()
1629 cpmode = repo.ui.config(b'server', b'concurrent-push-mode')
1629 cpmode = repo.ui.config(b'server', b'concurrent-push-mode')
1630 if cpmode == b'check-related':
1630 if cpmode == b'check-related':
1631 caps[b'checkheads'] = (b'related',)
1631 caps[b'checkheads'] = (b'related',)
1632 if b'phases' in repo.ui.configlist(b'devel', b'legacy.exchange'):
1632 if b'phases' in repo.ui.configlist(b'devel', b'legacy.exchange'):
1633 caps.pop(b'phases')
1633 caps.pop(b'phases')
1634
1634
1635 # Don't advertise stream clone support in server mode if not configured.
1635 # Don't advertise stream clone support in server mode if not configured.
1636 if role == b'server':
1636 if role == b'server':
1637 streamsupported = repo.ui.configbool(
1637 streamsupported = repo.ui.configbool(
1638 b'server', b'uncompressed', untrusted=True
1638 b'server', b'uncompressed', untrusted=True
1639 )
1639 )
1640 featuresupported = repo.ui.configbool(b'server', b'bundle2.stream')
1640 featuresupported = repo.ui.configbool(b'server', b'bundle2.stream')
1641
1641
1642 if not streamsupported or not featuresupported:
1642 if not streamsupported or not featuresupported:
1643 caps.pop(b'stream')
1643 caps.pop(b'stream')
1644 # Else always advertise support on client, because payload support
1644 # Else always advertise support on client, because payload support
1645 # should always be advertised.
1645 # should always be advertised.
1646
1646
1647 # b'rev-branch-cache is no longer advertised, but still supported
1647 # b'rev-branch-cache is no longer advertised, but still supported
1648 # for legacy clients.
1648 # for legacy clients.
1649
1649
1650 return caps
1650 return caps
1651
1651
1652
1652
1653 def bundle2caps(remote):
1653 def bundle2caps(remote):
1654 """return the bundle capabilities of a peer as dict"""
1654 """return the bundle capabilities of a peer as dict"""
1655 raw = remote.capable(b'bundle2')
1655 raw = remote.capable(b'bundle2')
1656 if not raw and raw != b'':
1656 if not raw and raw != b'':
1657 return {}
1657 return {}
1658 capsblob = urlreq.unquote(remote.capable(b'bundle2'))
1658 capsblob = urlreq.unquote(remote.capable(b'bundle2'))
1659 return decodecaps(capsblob)
1659 return decodecaps(capsblob)
1660
1660
1661
1661
1662 def obsmarkersversion(caps):
1662 def obsmarkersversion(caps):
1663 """extract the list of supported obsmarkers versions from a bundle2caps dict"""
1663 """extract the list of supported obsmarkers versions from a bundle2caps dict"""
1664 obscaps = caps.get(b'obsmarkers', ())
1664 obscaps = caps.get(b'obsmarkers', ())
1665 return [int(c[1:]) for c in obscaps if c.startswith(b'V')]
1665 return [int(c[1:]) for c in obscaps if c.startswith(b'V')]
1666
1666
1667
1667
1668 def writenewbundle(
1668 def writenewbundle(
1669 ui,
1669 ui,
1670 repo,
1670 repo,
1671 source,
1671 source,
1672 filename,
1672 filename,
1673 bundletype,
1673 bundletype,
1674 outgoing,
1674 outgoing,
1675 opts,
1675 opts,
1676 vfs=None,
1676 vfs=None,
1677 compression=None,
1677 compression=None,
1678 compopts=None,
1678 compopts=None,
1679 ):
1679 ):
1680 if bundletype.startswith(b'HG10'):
1680 if bundletype.startswith(b'HG10'):
1681 cg = changegroup.makechangegroup(repo, outgoing, b'01', source)
1681 cg = changegroup.makechangegroup(repo, outgoing, b'01', source)
1682 return writebundle(
1682 return writebundle(
1683 ui,
1683 ui,
1684 cg,
1684 cg,
1685 filename,
1685 filename,
1686 bundletype,
1686 bundletype,
1687 vfs=vfs,
1687 vfs=vfs,
1688 compression=compression,
1688 compression=compression,
1689 compopts=compopts,
1689 compopts=compopts,
1690 )
1690 )
1691 elif not bundletype.startswith(b'HG20'):
1691 elif not bundletype.startswith(b'HG20'):
1692 raise error.ProgrammingError(b'unknown bundle type: %s' % bundletype)
1692 raise error.ProgrammingError(b'unknown bundle type: %s' % bundletype)
1693
1693
1694 caps = {}
1694 caps = {}
1695 if opts.get(b'obsolescence', False):
1695 if opts.get(b'obsolescence', False):
1696 caps[b'obsmarkers'] = (b'V1',)
1696 caps[b'obsmarkers'] = (b'V1',)
1697 bundle = bundle20(ui, caps)
1697 bundle = bundle20(ui, caps)
1698 bundle.setcompression(compression, compopts)
1698 bundle.setcompression(compression, compopts)
1699 _addpartsfromopts(ui, repo, bundle, source, outgoing, opts)
1699 _addpartsfromopts(ui, repo, bundle, source, outgoing, opts)
1700 chunkiter = bundle.getchunks()
1700 chunkiter = bundle.getchunks()
1701
1701
1702 return changegroup.writechunks(ui, chunkiter, filename, vfs=vfs)
1702 return changegroup.writechunks(ui, chunkiter, filename, vfs=vfs)
1703
1703
1704
1704
1705 def _addpartsfromopts(ui, repo, bundler, source, outgoing, opts):
1705 def _addpartsfromopts(ui, repo, bundler, source, outgoing, opts):
1706 # We should eventually reconcile this logic with the one behind
1706 # We should eventually reconcile this logic with the one behind
1707 # 'exchange.getbundle2partsgenerator'.
1707 # 'exchange.getbundle2partsgenerator'.
1708 #
1708 #
1709 # The type of input from 'getbundle' and 'writenewbundle' are a bit
1709 # The type of input from 'getbundle' and 'writenewbundle' are a bit
1710 # different right now. So we keep them separated for now for the sake of
1710 # different right now. So we keep them separated for now for the sake of
1711 # simplicity.
1711 # simplicity.
1712
1712
1713 # we might not always want a changegroup in such bundle, for example in
1713 # we might not always want a changegroup in such bundle, for example in
1714 # stream bundles
1714 # stream bundles
1715 if opts.get(b'changegroup', True):
1715 if opts.get(b'changegroup', True):
1716 cgversion = opts.get(b'cg.version')
1716 cgversion = opts.get(b'cg.version')
1717 if cgversion is None:
1717 if cgversion is None:
1718 cgversion = changegroup.safeversion(repo)
1718 cgversion = changegroup.safeversion(repo)
1719 cg = changegroup.makechangegroup(repo, outgoing, cgversion, source)
1719 cg = changegroup.makechangegroup(repo, outgoing, cgversion, source)
1720 part = bundler.newpart(b'changegroup', data=cg.getchunks())
1720 part = bundler.newpart(b'changegroup', data=cg.getchunks())
1721 part.addparam(b'version', cg.version)
1721 part.addparam(b'version', cg.version)
1722 if b'clcount' in cg.extras:
1722 if b'clcount' in cg.extras:
1723 part.addparam(
1723 part.addparam(
1724 b'nbchanges', b'%d' % cg.extras[b'clcount'], mandatory=False
1724 b'nbchanges', b'%d' % cg.extras[b'clcount'], mandatory=False
1725 )
1725 )
1726 if opts.get(b'phases') and repo.revs(
1726 if opts.get(b'phases') and repo.revs(
1727 b'%ln and secret()', outgoing.ancestorsof
1727 b'%ln and secret()', outgoing.ancestorsof
1728 ):
1728 ):
1729 part.addparam(
1729 part.addparam(
1730 b'targetphase', b'%d' % phases.secret, mandatory=False
1730 b'targetphase', b'%d' % phases.secret, mandatory=False
1731 )
1731 )
1732 if repository.REPO_FEATURE_SIDE_DATA in repo.features:
1732 if repository.REPO_FEATURE_SIDE_DATA in repo.features:
1733 part.addparam(b'exp-sidedata', b'1')
1733 part.addparam(b'exp-sidedata', b'1')
1734
1734
1735 if opts.get(b'streamv2', False):
1735 if opts.get(b'streamv2', False):
1736 addpartbundlestream2(bundler, repo, stream=True)
1736 addpartbundlestream2(bundler, repo, stream=True)
1737
1737
1738 if opts.get(b'tagsfnodescache', True):
1738 if opts.get(b'tagsfnodescache', True):
1739 addparttagsfnodescache(repo, bundler, outgoing)
1739 addparttagsfnodescache(repo, bundler, outgoing)
1740
1740
1741 if opts.get(b'revbranchcache', True):
1741 if opts.get(b'revbranchcache', True):
1742 addpartrevbranchcache(repo, bundler, outgoing)
1742 addpartrevbranchcache(repo, bundler, outgoing)
1743
1743
1744 if opts.get(b'obsolescence', False):
1744 if opts.get(b'obsolescence', False):
1745 obsmarkers = repo.obsstore.relevantmarkers(outgoing.missing)
1745 obsmarkers = repo.obsstore.relevantmarkers(outgoing.missing)
1746 buildobsmarkerspart(
1746 buildobsmarkerspart(
1747 bundler,
1747 bundler,
1748 obsmarkers,
1748 obsmarkers,
1749 mandatory=opts.get(b'obsolescence-mandatory', True),
1749 mandatory=opts.get(b'obsolescence-mandatory', True),
1750 )
1750 )
1751
1751
1752 if opts.get(b'phases', False):
1752 if opts.get(b'phases', False):
1753 headsbyphase = phases.subsetphaseheads(repo, outgoing.missing)
1753 headsbyphase = phases.subsetphaseheads(repo, outgoing.missing)
1754 phasedata = phases.binaryencode(headsbyphase)
1754 phasedata = phases.binaryencode(headsbyphase)
1755 bundler.newpart(b'phase-heads', data=phasedata)
1755 bundler.newpart(b'phase-heads', data=phasedata)
1756
1756
1757
1757
1758 def addparttagsfnodescache(repo, bundler, outgoing):
1758 def addparttagsfnodescache(repo, bundler, outgoing):
1759 # we include the tags fnode cache for the bundle changeset
1759 # we include the tags fnode cache for the bundle changeset
1760 # (as an optional parts)
1760 # (as an optional parts)
1761 cache = tags.hgtagsfnodescache(repo.unfiltered())
1761 cache = tags.hgtagsfnodescache(repo.unfiltered())
1762 chunks = []
1762 chunks = []
1763
1763
1764 # .hgtags fnodes are only relevant for head changesets. While we could
1764 # .hgtags fnodes are only relevant for head changesets. While we could
1765 # transfer values for all known nodes, there will likely be little to
1765 # transfer values for all known nodes, there will likely be little to
1766 # no benefit.
1766 # no benefit.
1767 #
1767 #
1768 # We don't bother using a generator to produce output data because
1768 # We don't bother using a generator to produce output data because
1769 # a) we only have 40 bytes per head and even esoteric numbers of heads
1769 # a) we only have 40 bytes per head and even esoteric numbers of heads
1770 # consume little memory (1M heads is 40MB) b) we don't want to send the
1770 # consume little memory (1M heads is 40MB) b) we don't want to send the
1771 # part if we don't have entries and knowing if we have entries requires
1771 # part if we don't have entries and knowing if we have entries requires
1772 # cache lookups.
1772 # cache lookups.
1773 for node in outgoing.ancestorsof:
1773 for node in outgoing.ancestorsof:
1774 # Don't compute missing, as this may slow down serving.
1774 # Don't compute missing, as this may slow down serving.
1775 fnode = cache.getfnode(node, computemissing=False)
1775 fnode = cache.getfnode(node, computemissing=False)
1776 if fnode:
1776 if fnode:
1777 chunks.extend([node, fnode])
1777 chunks.extend([node, fnode])
1778
1778
1779 if chunks:
1779 if chunks:
1780 bundler.newpart(b'hgtagsfnodes', data=b''.join(chunks))
1780 bundler.newpart(b'hgtagsfnodes', data=b''.join(chunks))
1781
1781
1782
1782
1783 def addpartrevbranchcache(repo, bundler, outgoing):
1783 def addpartrevbranchcache(repo, bundler, outgoing):
1784 # we include the rev branch cache for the bundle changeset
1784 # we include the rev branch cache for the bundle changeset
1785 # (as an optional parts)
1785 # (as an optional parts)
1786 cache = repo.revbranchcache()
1786 cache = repo.revbranchcache()
1787 cl = repo.unfiltered().changelog
1787 cl = repo.unfiltered().changelog
1788 branchesdata = collections.defaultdict(lambda: (set(), set()))
1788 branchesdata = collections.defaultdict(lambda: (set(), set()))
1789 for node in outgoing.missing:
1789 for node in outgoing.missing:
1790 branch, close = cache.branchinfo(cl.rev(node))
1790 branch, close = cache.branchinfo(cl.rev(node))
1791 branchesdata[branch][close].add(node)
1791 branchesdata[branch][close].add(node)
1792
1792
1793 def generate():
1793 def generate():
1794 for branch, (nodes, closed) in sorted(branchesdata.items()):
1794 for branch, (nodes, closed) in sorted(branchesdata.items()):
1795 utf8branch = encoding.fromlocal(branch)
1795 utf8branch = encoding.fromlocal(branch)
1796 yield rbcstruct.pack(len(utf8branch), len(nodes), len(closed))
1796 yield rbcstruct.pack(len(utf8branch), len(nodes), len(closed))
1797 yield utf8branch
1797 yield utf8branch
1798 for n in sorted(nodes):
1798 for n in sorted(nodes):
1799 yield n
1799 yield n
1800 for n in sorted(closed):
1800 for n in sorted(closed):
1801 yield n
1801 yield n
1802
1802
1803 bundler.newpart(b'cache:rev-branch-cache', data=generate(), mandatory=False)
1803 bundler.newpart(b'cache:rev-branch-cache', data=generate(), mandatory=False)
1804
1804
1805
1805
1806 def _formatrequirementsspec(requirements):
1806 def _formatrequirementsspec(requirements):
1807 requirements = [req for req in requirements if req != b"shared"]
1807 requirements = [req for req in requirements if req != b"shared"]
1808 return urlreq.quote(b','.join(sorted(requirements)))
1808 return urlreq.quote(b','.join(sorted(requirements)))
1809
1809
1810
1810
1811 def _formatrequirementsparams(requirements):
1811 def _formatrequirementsparams(requirements):
1812 requirements = _formatrequirementsspec(requirements)
1812 requirements = _formatrequirementsspec(requirements)
1813 params = b"%s%s" % (urlreq.quote(b"requirements="), requirements)
1813 params = b"%s%s" % (urlreq.quote(b"requirements="), requirements)
1814 return params
1814 return params
1815
1815
1816
1816
1817 def format_remote_wanted_sidedata(repo):
1817 def format_remote_wanted_sidedata(repo):
1818 """Formats a repo's wanted sidedata categories into a bytestring for
1818 """Formats a repo's wanted sidedata categories into a bytestring for
1819 capabilities exchange."""
1819 capabilities exchange."""
1820 wanted = b""
1820 wanted = b""
1821 if repo._wanted_sidedata:
1821 if repo._wanted_sidedata:
1822 wanted = b','.join(
1822 wanted = b','.join(
1823 pycompat.bytestr(c) for c in sorted(repo._wanted_sidedata)
1823 pycompat.bytestr(c) for c in sorted(repo._wanted_sidedata)
1824 )
1824 )
1825 return wanted
1825 return wanted
1826
1826
1827
1827
1828 def read_remote_wanted_sidedata(remote):
1828 def read_remote_wanted_sidedata(remote):
1829 sidedata_categories = remote.capable(b'exp-wanted-sidedata')
1829 sidedata_categories = remote.capable(b'exp-wanted-sidedata')
1830 return read_wanted_sidedata(sidedata_categories)
1830 return read_wanted_sidedata(sidedata_categories)
1831
1831
1832
1832
1833 def read_wanted_sidedata(formatted):
1833 def read_wanted_sidedata(formatted):
1834 if formatted:
1834 if formatted:
1835 return set(formatted.split(b','))
1835 return set(formatted.split(b','))
1836 return set()
1836 return set()
1837
1837
1838
1838
1839 def addpartbundlestream2(bundler, repo, **kwargs):
1839 def addpartbundlestream2(bundler, repo, **kwargs):
1840 if not kwargs.get('stream', False):
1840 if not kwargs.get('stream', False):
1841 return
1841 return
1842
1842
1843 if not streamclone.allowservergeneration(repo):
1843 if not streamclone.allowservergeneration(repo):
1844 raise error.Abort(
1844 raise error.Abort(
1845 _(
1845 _(
1846 b'stream data requested but server does not allow '
1846 b'stream data requested but server does not allow '
1847 b'this feature'
1847 b'this feature'
1848 ),
1848 ),
1849 hint=_(
1849 hint=_(
1850 b'well-behaved clients should not be '
1850 b'well-behaved clients should not be '
1851 b'requesting stream data from servers not '
1851 b'requesting stream data from servers not '
1852 b'advertising it; the client may be buggy'
1852 b'advertising it; the client may be buggy'
1853 ),
1853 ),
1854 )
1854 )
1855
1855
1856 # Stream clones don't compress well. And compression undermines a
1856 # Stream clones don't compress well. And compression undermines a
1857 # goal of stream clones, which is to be fast. Communicate the desire
1857 # goal of stream clones, which is to be fast. Communicate the desire
1858 # to avoid compression to consumers of the bundle.
1858 # to avoid compression to consumers of the bundle.
1859 bundler.prefercompressed = False
1859 bundler.prefercompressed = False
1860
1860
1861 # get the includes and excludes
1861 # get the includes and excludes
1862 includepats = kwargs.get('includepats')
1862 includepats = kwargs.get('includepats')
1863 excludepats = kwargs.get('excludepats')
1863 excludepats = kwargs.get('excludepats')
1864
1864
1865 narrowstream = repo.ui.configbool(
1865 narrowstream = repo.ui.configbool(
1866 b'experimental', b'server.stream-narrow-clones'
1866 b'experimental', b'server.stream-narrow-clones'
1867 )
1867 )
1868
1868
1869 if (includepats or excludepats) and not narrowstream:
1869 if (includepats or excludepats) and not narrowstream:
1870 raise error.Abort(_(b'server does not support narrow stream clones'))
1870 raise error.Abort(_(b'server does not support narrow stream clones'))
1871
1871
1872 includeobsmarkers = False
1872 includeobsmarkers = False
1873 if repo.obsstore:
1873 if repo.obsstore:
1874 remoteversions = obsmarkersversion(bundler.capabilities)
1874 remoteversions = obsmarkersversion(bundler.capabilities)
1875 if not remoteversions:
1875 if not remoteversions:
1876 raise error.Abort(
1876 raise error.Abort(
1877 _(
1877 _(
1878 b'server has obsolescence markers, but client '
1878 b'server has obsolescence markers, but client '
1879 b'cannot receive them via stream clone'
1879 b'cannot receive them via stream clone'
1880 )
1880 )
1881 )
1881 )
1882 elif repo.obsstore._version in remoteversions:
1882 elif repo.obsstore._version in remoteversions:
1883 includeobsmarkers = True
1883 includeobsmarkers = True
1884
1884
1885 filecount, bytecount, it = streamclone.generatev2(
1885 filecount, bytecount, it = streamclone.generatev2(
1886 repo, includepats, excludepats, includeobsmarkers
1886 repo, includepats, excludepats, includeobsmarkers
1887 )
1887 )
1888 requirements = streamclone.streamed_requirements(repo)
1888 requirements = streamclone.streamed_requirements(repo)
1889 requirements = _formatrequirementsspec(requirements)
1889 requirements = _formatrequirementsspec(requirements)
1890 part = bundler.newpart(b'stream2', data=it)
1890 part = bundler.newpart(b'stream2', data=it)
1891 part.addparam(b'bytecount', b'%d' % bytecount, mandatory=True)
1891 part.addparam(b'bytecount', b'%d' % bytecount, mandatory=True)
1892 part.addparam(b'filecount', b'%d' % filecount, mandatory=True)
1892 part.addparam(b'filecount', b'%d' % filecount, mandatory=True)
1893 part.addparam(b'requirements', requirements, mandatory=True)
1893 part.addparam(b'requirements', requirements, mandatory=True)
1894
1894
1895
1895
1896 def buildobsmarkerspart(bundler, markers, mandatory=True):
1896 def buildobsmarkerspart(bundler, markers, mandatory=True):
1897 """add an obsmarker part to the bundler with <markers>
1897 """add an obsmarker part to the bundler with <markers>
1898
1898
1899 No part is created if markers is empty.
1899 No part is created if markers is empty.
1900 Raises ValueError if the bundler doesn't support any known obsmarker format.
1900 Raises ValueError if the bundler doesn't support any known obsmarker format.
1901 """
1901 """
1902 if not markers:
1902 if not markers:
1903 return None
1903 return None
1904
1904
1905 remoteversions = obsmarkersversion(bundler.capabilities)
1905 remoteversions = obsmarkersversion(bundler.capabilities)
1906 version = obsolete.commonversion(remoteversions)
1906 version = obsolete.commonversion(remoteversions)
1907 if version is None:
1907 if version is None:
1908 raise ValueError(b'bundler does not support common obsmarker format')
1908 raise ValueError(b'bundler does not support common obsmarker format')
1909 stream = obsolete.encodemarkers(markers, True, version=version)
1909 stream = obsolete.encodemarkers(markers, True, version=version)
1910 return bundler.newpart(b'obsmarkers', data=stream, mandatory=mandatory)
1910 return bundler.newpart(b'obsmarkers', data=stream, mandatory=mandatory)
1911
1911
1912
1912
1913 def writebundle(
1913 def writebundle(
1914 ui, cg, filename, bundletype, vfs=None, compression=None, compopts=None
1914 ui, cg, filename, bundletype, vfs=None, compression=None, compopts=None
1915 ):
1915 ):
1916 """Write a bundle file and return its filename.
1916 """Write a bundle file and return its filename.
1917
1917
1918 Existing files will not be overwritten.
1918 Existing files will not be overwritten.
1919 If no filename is specified, a temporary file is created.
1919 If no filename is specified, a temporary file is created.
1920 bz2 compression can be turned off.
1920 bz2 compression can be turned off.
1921 The bundle file will be deleted in case of errors.
1921 The bundle file will be deleted in case of errors.
1922 """
1922 """
1923
1923
1924 if bundletype == b"HG20":
1924 if bundletype == b"HG20":
1925 bundle = bundle20(ui)
1925 bundle = bundle20(ui)
1926 bundle.setcompression(compression, compopts)
1926 bundle.setcompression(compression, compopts)
1927 part = bundle.newpart(b'changegroup', data=cg.getchunks())
1927 part = bundle.newpart(b'changegroup', data=cg.getchunks())
1928 part.addparam(b'version', cg.version)
1928 part.addparam(b'version', cg.version)
1929 if b'clcount' in cg.extras:
1929 if b'clcount' in cg.extras:
1930 part.addparam(
1930 part.addparam(
1931 b'nbchanges', b'%d' % cg.extras[b'clcount'], mandatory=False
1931 b'nbchanges', b'%d' % cg.extras[b'clcount'], mandatory=False
1932 )
1932 )
1933 chunkiter = bundle.getchunks()
1933 chunkiter = bundle.getchunks()
1934 else:
1934 else:
1935 # compression argument is only for the bundle2 case
1935 # compression argument is only for the bundle2 case
1936 assert compression is None
1936 assert compression is None
1937 if cg.version != b'01':
1937 if cg.version != b'01':
1938 raise error.Abort(
1938 raise error.Abort(
1939 _(b'old bundle types only supports v1 changegroups')
1939 _(b'old bundle types only supports v1 changegroups')
1940 )
1940 )
1941
1942 # HG20 is the case without 2 values to unpack, but is handled above.
1943 # pytype: disable=bad-unpacking
1941 header, comp = bundletypes[bundletype]
1944 header, comp = bundletypes[bundletype]
1945 # pytype: enable=bad-unpacking
1946
1942 if comp not in util.compengines.supportedbundletypes:
1947 if comp not in util.compengines.supportedbundletypes:
1943 raise error.Abort(_(b'unknown stream compression type: %s') % comp)
1948 raise error.Abort(_(b'unknown stream compression type: %s') % comp)
1944 compengine = util.compengines.forbundletype(comp)
1949 compengine = util.compengines.forbundletype(comp)
1945
1950
1946 def chunkiter():
1951 def chunkiter():
1947 yield header
1952 yield header
1948 for chunk in compengine.compressstream(cg.getchunks(), compopts):
1953 for chunk in compengine.compressstream(cg.getchunks(), compopts):
1949 yield chunk
1954 yield chunk
1950
1955
1951 chunkiter = chunkiter()
1956 chunkiter = chunkiter()
1952
1957
1953 # parse the changegroup data, otherwise we will block
1958 # parse the changegroup data, otherwise we will block
1954 # in case of sshrepo because we don't know the end of the stream
1959 # in case of sshrepo because we don't know the end of the stream
1955 return changegroup.writechunks(ui, chunkiter, filename, vfs=vfs)
1960 return changegroup.writechunks(ui, chunkiter, filename, vfs=vfs)
1956
1961
1957
1962
1958 def combinechangegroupresults(op):
1963 def combinechangegroupresults(op):
1959 """logic to combine 0 or more addchangegroup results into one"""
1964 """logic to combine 0 or more addchangegroup results into one"""
1960 results = [r.get(b'return', 0) for r in op.records[b'changegroup']]
1965 results = [r.get(b'return', 0) for r in op.records[b'changegroup']]
1961 changedheads = 0
1966 changedheads = 0
1962 result = 1
1967 result = 1
1963 for ret in results:
1968 for ret in results:
1964 # If any changegroup result is 0, return 0
1969 # If any changegroup result is 0, return 0
1965 if ret == 0:
1970 if ret == 0:
1966 result = 0
1971 result = 0
1967 break
1972 break
1968 if ret < -1:
1973 if ret < -1:
1969 changedheads += ret + 1
1974 changedheads += ret + 1
1970 elif ret > 1:
1975 elif ret > 1:
1971 changedheads += ret - 1
1976 changedheads += ret - 1
1972 if changedheads > 0:
1977 if changedheads > 0:
1973 result = 1 + changedheads
1978 result = 1 + changedheads
1974 elif changedheads < 0:
1979 elif changedheads < 0:
1975 result = -1 + changedheads
1980 result = -1 + changedheads
1976 return result
1981 return result
1977
1982
1978
1983
1979 @parthandler(
1984 @parthandler(
1980 b'changegroup',
1985 b'changegroup',
1981 (
1986 (
1982 b'version',
1987 b'version',
1983 b'nbchanges',
1988 b'nbchanges',
1984 b'exp-sidedata',
1989 b'exp-sidedata',
1985 b'exp-wanted-sidedata',
1990 b'exp-wanted-sidedata',
1986 b'treemanifest',
1991 b'treemanifest',
1987 b'targetphase',
1992 b'targetphase',
1988 ),
1993 ),
1989 )
1994 )
1990 def handlechangegroup(op, inpart):
1995 def handlechangegroup(op, inpart):
1991 """apply a changegroup part on the repo"""
1996 """apply a changegroup part on the repo"""
1992 from . import localrepo
1997 from . import localrepo
1993
1998
1994 tr = op.gettransaction()
1999 tr = op.gettransaction()
1995 unpackerversion = inpart.params.get(b'version', b'01')
2000 unpackerversion = inpart.params.get(b'version', b'01')
1996 # We should raise an appropriate exception here
2001 # We should raise an appropriate exception here
1997 cg = changegroup.getunbundler(unpackerversion, inpart, None)
2002 cg = changegroup.getunbundler(unpackerversion, inpart, None)
1998 # the source and url passed here are overwritten by the one contained in
2003 # the source and url passed here are overwritten by the one contained in
1999 # the transaction.hookargs argument. So 'bundle2' is a placeholder
2004 # the transaction.hookargs argument. So 'bundle2' is a placeholder
2000 nbchangesets = None
2005 nbchangesets = None
2001 if b'nbchanges' in inpart.params:
2006 if b'nbchanges' in inpart.params:
2002 nbchangesets = int(inpart.params.get(b'nbchanges'))
2007 nbchangesets = int(inpart.params.get(b'nbchanges'))
2003 if b'treemanifest' in inpart.params and not scmutil.istreemanifest(op.repo):
2008 if b'treemanifest' in inpart.params and not scmutil.istreemanifest(op.repo):
2004 if len(op.repo.changelog) != 0:
2009 if len(op.repo.changelog) != 0:
2005 raise error.Abort(
2010 raise error.Abort(
2006 _(
2011 _(
2007 b"bundle contains tree manifests, but local repo is "
2012 b"bundle contains tree manifests, but local repo is "
2008 b"non-empty and does not use tree manifests"
2013 b"non-empty and does not use tree manifests"
2009 )
2014 )
2010 )
2015 )
2011 op.repo.requirements.add(requirements.TREEMANIFEST_REQUIREMENT)
2016 op.repo.requirements.add(requirements.TREEMANIFEST_REQUIREMENT)
2012 op.repo.svfs.options = localrepo.resolvestorevfsoptions(
2017 op.repo.svfs.options = localrepo.resolvestorevfsoptions(
2013 op.repo.ui, op.repo.requirements, op.repo.features
2018 op.repo.ui, op.repo.requirements, op.repo.features
2014 )
2019 )
2015 scmutil.writereporequirements(op.repo)
2020 scmutil.writereporequirements(op.repo)
2016
2021
2017 extrakwargs = {}
2022 extrakwargs = {}
2018 targetphase = inpart.params.get(b'targetphase')
2023 targetphase = inpart.params.get(b'targetphase')
2019 if targetphase is not None:
2024 if targetphase is not None:
2020 extrakwargs['targetphase'] = int(targetphase)
2025 extrakwargs['targetphase'] = int(targetphase)
2021
2026
2022 remote_sidedata = inpart.params.get(b'exp-wanted-sidedata')
2027 remote_sidedata = inpart.params.get(b'exp-wanted-sidedata')
2023 extrakwargs['sidedata_categories'] = read_wanted_sidedata(remote_sidedata)
2028 extrakwargs['sidedata_categories'] = read_wanted_sidedata(remote_sidedata)
2024
2029
2025 ret = _processchangegroup(
2030 ret = _processchangegroup(
2026 op,
2031 op,
2027 cg,
2032 cg,
2028 tr,
2033 tr,
2029 op.source,
2034 op.source,
2030 b'bundle2',
2035 b'bundle2',
2031 expectedtotal=nbchangesets,
2036 expectedtotal=nbchangesets,
2032 **extrakwargs
2037 **extrakwargs
2033 )
2038 )
2034 if op.reply is not None:
2039 if op.reply is not None:
2035 # This is definitely not the final form of this
2040 # This is definitely not the final form of this
2036 # return. But one need to start somewhere.
2041 # return. But one need to start somewhere.
2037 part = op.reply.newpart(b'reply:changegroup', mandatory=False)
2042 part = op.reply.newpart(b'reply:changegroup', mandatory=False)
2038 part.addparam(
2043 part.addparam(
2039 b'in-reply-to', pycompat.bytestr(inpart.id), mandatory=False
2044 b'in-reply-to', pycompat.bytestr(inpart.id), mandatory=False
2040 )
2045 )
2041 part.addparam(b'return', b'%i' % ret, mandatory=False)
2046 part.addparam(b'return', b'%i' % ret, mandatory=False)
2042 assert not inpart.read()
2047 assert not inpart.read()
2043
2048
2044
2049
2045 _remotechangegroupparams = tuple(
2050 _remotechangegroupparams = tuple(
2046 [b'url', b'size', b'digests']
2051 [b'url', b'size', b'digests']
2047 + [b'digest:%s' % k for k in util.DIGESTS.keys()]
2052 + [b'digest:%s' % k for k in util.DIGESTS.keys()]
2048 )
2053 )
2049
2054
2050
2055
2051 @parthandler(b'remote-changegroup', _remotechangegroupparams)
2056 @parthandler(b'remote-changegroup', _remotechangegroupparams)
2052 def handleremotechangegroup(op, inpart):
2057 def handleremotechangegroup(op, inpart):
2053 """apply a bundle10 on the repo, given an url and validation information
2058 """apply a bundle10 on the repo, given an url and validation information
2054
2059
2055 All the information about the remote bundle to import are given as
2060 All the information about the remote bundle to import are given as
2056 parameters. The parameters include:
2061 parameters. The parameters include:
2057 - url: the url to the bundle10.
2062 - url: the url to the bundle10.
2058 - size: the bundle10 file size. It is used to validate what was
2063 - size: the bundle10 file size. It is used to validate what was
2059 retrieved by the client matches the server knowledge about the bundle.
2064 retrieved by the client matches the server knowledge about the bundle.
2060 - digests: a space separated list of the digest types provided as
2065 - digests: a space separated list of the digest types provided as
2061 parameters.
2066 parameters.
2062 - digest:<digest-type>: the hexadecimal representation of the digest with
2067 - digest:<digest-type>: the hexadecimal representation of the digest with
2063 that name. Like the size, it is used to validate what was retrieved by
2068 that name. Like the size, it is used to validate what was retrieved by
2064 the client matches what the server knows about the bundle.
2069 the client matches what the server knows about the bundle.
2065
2070
2066 When multiple digest types are given, all of them are checked.
2071 When multiple digest types are given, all of them are checked.
2067 """
2072 """
2068 try:
2073 try:
2069 raw_url = inpart.params[b'url']
2074 raw_url = inpart.params[b'url']
2070 except KeyError:
2075 except KeyError:
2071 raise error.Abort(_(b'remote-changegroup: missing "%s" param') % b'url')
2076 raise error.Abort(_(b'remote-changegroup: missing "%s" param') % b'url')
2072 parsed_url = urlutil.url(raw_url)
2077 parsed_url = urlutil.url(raw_url)
2073 if parsed_url.scheme not in capabilities[b'remote-changegroup']:
2078 if parsed_url.scheme not in capabilities[b'remote-changegroup']:
2074 raise error.Abort(
2079 raise error.Abort(
2075 _(b'remote-changegroup does not support %s urls')
2080 _(b'remote-changegroup does not support %s urls')
2076 % parsed_url.scheme
2081 % parsed_url.scheme
2077 )
2082 )
2078
2083
2079 try:
2084 try:
2080 size = int(inpart.params[b'size'])
2085 size = int(inpart.params[b'size'])
2081 except ValueError:
2086 except ValueError:
2082 raise error.Abort(
2087 raise error.Abort(
2083 _(b'remote-changegroup: invalid value for param "%s"') % b'size'
2088 _(b'remote-changegroup: invalid value for param "%s"') % b'size'
2084 )
2089 )
2085 except KeyError:
2090 except KeyError:
2086 raise error.Abort(
2091 raise error.Abort(
2087 _(b'remote-changegroup: missing "%s" param') % b'size'
2092 _(b'remote-changegroup: missing "%s" param') % b'size'
2088 )
2093 )
2089
2094
2090 digests = {}
2095 digests = {}
2091 for typ in inpart.params.get(b'digests', b'').split():
2096 for typ in inpart.params.get(b'digests', b'').split():
2092 param = b'digest:%s' % typ
2097 param = b'digest:%s' % typ
2093 try:
2098 try:
2094 value = inpart.params[param]
2099 value = inpart.params[param]
2095 except KeyError:
2100 except KeyError:
2096 raise error.Abort(
2101 raise error.Abort(
2097 _(b'remote-changegroup: missing "%s" param') % param
2102 _(b'remote-changegroup: missing "%s" param') % param
2098 )
2103 )
2099 digests[typ] = value
2104 digests[typ] = value
2100
2105
2101 real_part = util.digestchecker(url.open(op.ui, raw_url), size, digests)
2106 real_part = util.digestchecker(url.open(op.ui, raw_url), size, digests)
2102
2107
2103 tr = op.gettransaction()
2108 tr = op.gettransaction()
2104 from . import exchange
2109 from . import exchange
2105
2110
2106 cg = exchange.readbundle(op.repo.ui, real_part, raw_url)
2111 cg = exchange.readbundle(op.repo.ui, real_part, raw_url)
2107 if not isinstance(cg, changegroup.cg1unpacker):
2112 if not isinstance(cg, changegroup.cg1unpacker):
2108 raise error.Abort(
2113 raise error.Abort(
2109 _(b'%s: not a bundle version 1.0') % urlutil.hidepassword(raw_url)
2114 _(b'%s: not a bundle version 1.0') % urlutil.hidepassword(raw_url)
2110 )
2115 )
2111 ret = _processchangegroup(op, cg, tr, op.source, b'bundle2')
2116 ret = _processchangegroup(op, cg, tr, op.source, b'bundle2')
2112 if op.reply is not None:
2117 if op.reply is not None:
2113 # This is definitely not the final form of this
2118 # This is definitely not the final form of this
2114 # return. But one need to start somewhere.
2119 # return. But one need to start somewhere.
2115 part = op.reply.newpart(b'reply:changegroup')
2120 part = op.reply.newpart(b'reply:changegroup')
2116 part.addparam(
2121 part.addparam(
2117 b'in-reply-to', pycompat.bytestr(inpart.id), mandatory=False
2122 b'in-reply-to', pycompat.bytestr(inpart.id), mandatory=False
2118 )
2123 )
2119 part.addparam(b'return', b'%i' % ret, mandatory=False)
2124 part.addparam(b'return', b'%i' % ret, mandatory=False)
2120 try:
2125 try:
2121 real_part.validate()
2126 real_part.validate()
2122 except error.Abort as e:
2127 except error.Abort as e:
2123 raise error.Abort(
2128 raise error.Abort(
2124 _(b'bundle at %s is corrupted:\n%s')
2129 _(b'bundle at %s is corrupted:\n%s')
2125 % (urlutil.hidepassword(raw_url), e.message)
2130 % (urlutil.hidepassword(raw_url), e.message)
2126 )
2131 )
2127 assert not inpart.read()
2132 assert not inpart.read()
2128
2133
2129
2134
2130 @parthandler(b'reply:changegroup', (b'return', b'in-reply-to'))
2135 @parthandler(b'reply:changegroup', (b'return', b'in-reply-to'))
2131 def handlereplychangegroup(op, inpart):
2136 def handlereplychangegroup(op, inpart):
2132 ret = int(inpart.params[b'return'])
2137 ret = int(inpart.params[b'return'])
2133 replyto = int(inpart.params[b'in-reply-to'])
2138 replyto = int(inpart.params[b'in-reply-to'])
2134 op.records.add(b'changegroup', {b'return': ret}, replyto)
2139 op.records.add(b'changegroup', {b'return': ret}, replyto)
2135
2140
2136
2141
2137 @parthandler(b'check:bookmarks')
2142 @parthandler(b'check:bookmarks')
2138 def handlecheckbookmarks(op, inpart):
2143 def handlecheckbookmarks(op, inpart):
2139 """check location of bookmarks
2144 """check location of bookmarks
2140
2145
2141 This part is to be used to detect push race regarding bookmark, it
2146 This part is to be used to detect push race regarding bookmark, it
2142 contains binary encoded (bookmark, node) tuple. If the local state does
2147 contains binary encoded (bookmark, node) tuple. If the local state does
2143 not marks the one in the part, a PushRaced exception is raised
2148 not marks the one in the part, a PushRaced exception is raised
2144 """
2149 """
2145 bookdata = bookmarks.binarydecode(op.repo, inpart)
2150 bookdata = bookmarks.binarydecode(op.repo, inpart)
2146
2151
2147 msgstandard = (
2152 msgstandard = (
2148 b'remote repository changed while pushing - please try again '
2153 b'remote repository changed while pushing - please try again '
2149 b'(bookmark "%s" move from %s to %s)'
2154 b'(bookmark "%s" move from %s to %s)'
2150 )
2155 )
2151 msgmissing = (
2156 msgmissing = (
2152 b'remote repository changed while pushing - please try again '
2157 b'remote repository changed while pushing - please try again '
2153 b'(bookmark "%s" is missing, expected %s)'
2158 b'(bookmark "%s" is missing, expected %s)'
2154 )
2159 )
2155 msgexist = (
2160 msgexist = (
2156 b'remote repository changed while pushing - please try again '
2161 b'remote repository changed while pushing - please try again '
2157 b'(bookmark "%s" set on %s, expected missing)'
2162 b'(bookmark "%s" set on %s, expected missing)'
2158 )
2163 )
2159 for book, node in bookdata:
2164 for book, node in bookdata:
2160 currentnode = op.repo._bookmarks.get(book)
2165 currentnode = op.repo._bookmarks.get(book)
2161 if currentnode != node:
2166 if currentnode != node:
2162 if node is None:
2167 if node is None:
2163 finalmsg = msgexist % (book, short(currentnode))
2168 finalmsg = msgexist % (book, short(currentnode))
2164 elif currentnode is None:
2169 elif currentnode is None:
2165 finalmsg = msgmissing % (book, short(node))
2170 finalmsg = msgmissing % (book, short(node))
2166 else:
2171 else:
2167 finalmsg = msgstandard % (
2172 finalmsg = msgstandard % (
2168 book,
2173 book,
2169 short(node),
2174 short(node),
2170 short(currentnode),
2175 short(currentnode),
2171 )
2176 )
2172 raise error.PushRaced(finalmsg)
2177 raise error.PushRaced(finalmsg)
2173
2178
2174
2179
2175 @parthandler(b'check:heads')
2180 @parthandler(b'check:heads')
2176 def handlecheckheads(op, inpart):
2181 def handlecheckheads(op, inpart):
2177 """check that head of the repo did not change
2182 """check that head of the repo did not change
2178
2183
2179 This is used to detect a push race when using unbundle.
2184 This is used to detect a push race when using unbundle.
2180 This replaces the "heads" argument of unbundle."""
2185 This replaces the "heads" argument of unbundle."""
2181 h = inpart.read(20)
2186 h = inpart.read(20)
2182 heads = []
2187 heads = []
2183 while len(h) == 20:
2188 while len(h) == 20:
2184 heads.append(h)
2189 heads.append(h)
2185 h = inpart.read(20)
2190 h = inpart.read(20)
2186 assert not h
2191 assert not h
2187 # Trigger a transaction so that we are guaranteed to have the lock now.
2192 # Trigger a transaction so that we are guaranteed to have the lock now.
2188 if op.ui.configbool(b'experimental', b'bundle2lazylocking'):
2193 if op.ui.configbool(b'experimental', b'bundle2lazylocking'):
2189 op.gettransaction()
2194 op.gettransaction()
2190 if sorted(heads) != sorted(op.repo.heads()):
2195 if sorted(heads) != sorted(op.repo.heads()):
2191 raise error.PushRaced(
2196 raise error.PushRaced(
2192 b'remote repository changed while pushing - please try again'
2197 b'remote repository changed while pushing - please try again'
2193 )
2198 )
2194
2199
2195
2200
2196 @parthandler(b'check:updated-heads')
2201 @parthandler(b'check:updated-heads')
2197 def handlecheckupdatedheads(op, inpart):
2202 def handlecheckupdatedheads(op, inpart):
2198 """check for race on the heads touched by a push
2203 """check for race on the heads touched by a push
2199
2204
2200 This is similar to 'check:heads' but focus on the heads actually updated
2205 This is similar to 'check:heads' but focus on the heads actually updated
2201 during the push. If other activities happen on unrelated heads, it is
2206 during the push. If other activities happen on unrelated heads, it is
2202 ignored.
2207 ignored.
2203
2208
2204 This allow server with high traffic to avoid push contention as long as
2209 This allow server with high traffic to avoid push contention as long as
2205 unrelated parts of the graph are involved."""
2210 unrelated parts of the graph are involved."""
2206 h = inpart.read(20)
2211 h = inpart.read(20)
2207 heads = []
2212 heads = []
2208 while len(h) == 20:
2213 while len(h) == 20:
2209 heads.append(h)
2214 heads.append(h)
2210 h = inpart.read(20)
2215 h = inpart.read(20)
2211 assert not h
2216 assert not h
2212 # trigger a transaction so that we are guaranteed to have the lock now.
2217 # trigger a transaction so that we are guaranteed to have the lock now.
2213 if op.ui.configbool(b'experimental', b'bundle2lazylocking'):
2218 if op.ui.configbool(b'experimental', b'bundle2lazylocking'):
2214 op.gettransaction()
2219 op.gettransaction()
2215
2220
2216 currentheads = set()
2221 currentheads = set()
2217 for ls in op.repo.branchmap().iterheads():
2222 for ls in op.repo.branchmap().iterheads():
2218 currentheads.update(ls)
2223 currentheads.update(ls)
2219
2224
2220 for h in heads:
2225 for h in heads:
2221 if h not in currentheads:
2226 if h not in currentheads:
2222 raise error.PushRaced(
2227 raise error.PushRaced(
2223 b'remote repository changed while pushing - '
2228 b'remote repository changed while pushing - '
2224 b'please try again'
2229 b'please try again'
2225 )
2230 )
2226
2231
2227
2232
2228 @parthandler(b'check:phases')
2233 @parthandler(b'check:phases')
2229 def handlecheckphases(op, inpart):
2234 def handlecheckphases(op, inpart):
2230 """check that phase boundaries of the repository did not change
2235 """check that phase boundaries of the repository did not change
2231
2236
2232 This is used to detect a push race.
2237 This is used to detect a push race.
2233 """
2238 """
2234 phasetonodes = phases.binarydecode(inpart)
2239 phasetonodes = phases.binarydecode(inpart)
2235 unfi = op.repo.unfiltered()
2240 unfi = op.repo.unfiltered()
2236 cl = unfi.changelog
2241 cl = unfi.changelog
2237 phasecache = unfi._phasecache
2242 phasecache = unfi._phasecache
2238 msg = (
2243 msg = (
2239 b'remote repository changed while pushing - please try again '
2244 b'remote repository changed while pushing - please try again '
2240 b'(%s is %s expected %s)'
2245 b'(%s is %s expected %s)'
2241 )
2246 )
2242 for expectedphase, nodes in phasetonodes.items():
2247 for expectedphase, nodes in phasetonodes.items():
2243 for n in nodes:
2248 for n in nodes:
2244 actualphase = phasecache.phase(unfi, cl.rev(n))
2249 actualphase = phasecache.phase(unfi, cl.rev(n))
2245 if actualphase != expectedphase:
2250 if actualphase != expectedphase:
2246 finalmsg = msg % (
2251 finalmsg = msg % (
2247 short(n),
2252 short(n),
2248 phases.phasenames[actualphase],
2253 phases.phasenames[actualphase],
2249 phases.phasenames[expectedphase],
2254 phases.phasenames[expectedphase],
2250 )
2255 )
2251 raise error.PushRaced(finalmsg)
2256 raise error.PushRaced(finalmsg)
2252
2257
2253
2258
2254 @parthandler(b'output')
2259 @parthandler(b'output')
2255 def handleoutput(op, inpart):
2260 def handleoutput(op, inpart):
2256 """forward output captured on the server to the client"""
2261 """forward output captured on the server to the client"""
2257 for line in inpart.read().splitlines():
2262 for line in inpart.read().splitlines():
2258 op.ui.status(_(b'remote: %s\n') % line)
2263 op.ui.status(_(b'remote: %s\n') % line)
2259
2264
2260
2265
2261 @parthandler(b'replycaps')
2266 @parthandler(b'replycaps')
2262 def handlereplycaps(op, inpart):
2267 def handlereplycaps(op, inpart):
2263 """Notify that a reply bundle should be created
2268 """Notify that a reply bundle should be created
2264
2269
2265 The payload contains the capabilities information for the reply"""
2270 The payload contains the capabilities information for the reply"""
2266 caps = decodecaps(inpart.read())
2271 caps = decodecaps(inpart.read())
2267 if op.reply is None:
2272 if op.reply is None:
2268 op.reply = bundle20(op.ui, caps)
2273 op.reply = bundle20(op.ui, caps)
2269
2274
2270
2275
2271 class AbortFromPart(error.Abort):
2276 class AbortFromPart(error.Abort):
2272 """Sub-class of Abort that denotes an error from a bundle2 part."""
2277 """Sub-class of Abort that denotes an error from a bundle2 part."""
2273
2278
2274
2279
2275 @parthandler(b'error:abort', (b'message', b'hint'))
2280 @parthandler(b'error:abort', (b'message', b'hint'))
2276 def handleerrorabort(op, inpart):
2281 def handleerrorabort(op, inpart):
2277 """Used to transmit abort error over the wire"""
2282 """Used to transmit abort error over the wire"""
2278 raise AbortFromPart(
2283 raise AbortFromPart(
2279 inpart.params[b'message'], hint=inpart.params.get(b'hint')
2284 inpart.params[b'message'], hint=inpart.params.get(b'hint')
2280 )
2285 )
2281
2286
2282
2287
2283 @parthandler(
2288 @parthandler(
2284 b'error:pushkey',
2289 b'error:pushkey',
2285 (b'namespace', b'key', b'new', b'old', b'ret', b'in-reply-to'),
2290 (b'namespace', b'key', b'new', b'old', b'ret', b'in-reply-to'),
2286 )
2291 )
2287 def handleerrorpushkey(op, inpart):
2292 def handleerrorpushkey(op, inpart):
2288 """Used to transmit failure of a mandatory pushkey over the wire"""
2293 """Used to transmit failure of a mandatory pushkey over the wire"""
2289 kwargs = {}
2294 kwargs = {}
2290 for name in (b'namespace', b'key', b'new', b'old', b'ret'):
2295 for name in (b'namespace', b'key', b'new', b'old', b'ret'):
2291 value = inpart.params.get(name)
2296 value = inpart.params.get(name)
2292 if value is not None:
2297 if value is not None:
2293 kwargs[name] = value
2298 kwargs[name] = value
2294 raise error.PushkeyFailed(
2299 raise error.PushkeyFailed(
2295 inpart.params[b'in-reply-to'], **pycompat.strkwargs(kwargs)
2300 inpart.params[b'in-reply-to'], **pycompat.strkwargs(kwargs)
2296 )
2301 )
2297
2302
2298
2303
2299 @parthandler(b'error:unsupportedcontent', (b'parttype', b'params'))
2304 @parthandler(b'error:unsupportedcontent', (b'parttype', b'params'))
2300 def handleerrorunsupportedcontent(op, inpart):
2305 def handleerrorunsupportedcontent(op, inpart):
2301 """Used to transmit unknown content error over the wire"""
2306 """Used to transmit unknown content error over the wire"""
2302 kwargs = {}
2307 kwargs = {}
2303 parttype = inpart.params.get(b'parttype')
2308 parttype = inpart.params.get(b'parttype')
2304 if parttype is not None:
2309 if parttype is not None:
2305 kwargs[b'parttype'] = parttype
2310 kwargs[b'parttype'] = parttype
2306 params = inpart.params.get(b'params')
2311 params = inpart.params.get(b'params')
2307 if params is not None:
2312 if params is not None:
2308 kwargs[b'params'] = params.split(b'\0')
2313 kwargs[b'params'] = params.split(b'\0')
2309
2314
2310 raise error.BundleUnknownFeatureError(**pycompat.strkwargs(kwargs))
2315 raise error.BundleUnknownFeatureError(**pycompat.strkwargs(kwargs))
2311
2316
2312
2317
2313 @parthandler(b'error:pushraced', (b'message',))
2318 @parthandler(b'error:pushraced', (b'message',))
2314 def handleerrorpushraced(op, inpart):
2319 def handleerrorpushraced(op, inpart):
2315 """Used to transmit push race error over the wire"""
2320 """Used to transmit push race error over the wire"""
2316 raise error.ResponseError(_(b'push failed:'), inpart.params[b'message'])
2321 raise error.ResponseError(_(b'push failed:'), inpart.params[b'message'])
2317
2322
2318
2323
2319 @parthandler(b'listkeys', (b'namespace',))
2324 @parthandler(b'listkeys', (b'namespace',))
2320 def handlelistkeys(op, inpart):
2325 def handlelistkeys(op, inpart):
2321 """retrieve pushkey namespace content stored in a bundle2"""
2326 """retrieve pushkey namespace content stored in a bundle2"""
2322 namespace = inpart.params[b'namespace']
2327 namespace = inpart.params[b'namespace']
2323 r = pushkey.decodekeys(inpart.read())
2328 r = pushkey.decodekeys(inpart.read())
2324 op.records.add(b'listkeys', (namespace, r))
2329 op.records.add(b'listkeys', (namespace, r))
2325
2330
2326
2331
2327 @parthandler(b'pushkey', (b'namespace', b'key', b'old', b'new'))
2332 @parthandler(b'pushkey', (b'namespace', b'key', b'old', b'new'))
2328 def handlepushkey(op, inpart):
2333 def handlepushkey(op, inpart):
2329 """process a pushkey request"""
2334 """process a pushkey request"""
2330 dec = pushkey.decode
2335 dec = pushkey.decode
2331 namespace = dec(inpart.params[b'namespace'])
2336 namespace = dec(inpart.params[b'namespace'])
2332 key = dec(inpart.params[b'key'])
2337 key = dec(inpart.params[b'key'])
2333 old = dec(inpart.params[b'old'])
2338 old = dec(inpart.params[b'old'])
2334 new = dec(inpart.params[b'new'])
2339 new = dec(inpart.params[b'new'])
2335 # Grab the transaction to ensure that we have the lock before performing the
2340 # Grab the transaction to ensure that we have the lock before performing the
2336 # pushkey.
2341 # pushkey.
2337 if op.ui.configbool(b'experimental', b'bundle2lazylocking'):
2342 if op.ui.configbool(b'experimental', b'bundle2lazylocking'):
2338 op.gettransaction()
2343 op.gettransaction()
2339 ret = op.repo.pushkey(namespace, key, old, new)
2344 ret = op.repo.pushkey(namespace, key, old, new)
2340 record = {b'namespace': namespace, b'key': key, b'old': old, b'new': new}
2345 record = {b'namespace': namespace, b'key': key, b'old': old, b'new': new}
2341 op.records.add(b'pushkey', record)
2346 op.records.add(b'pushkey', record)
2342 if op.reply is not None:
2347 if op.reply is not None:
2343 rpart = op.reply.newpart(b'reply:pushkey')
2348 rpart = op.reply.newpart(b'reply:pushkey')
2344 rpart.addparam(
2349 rpart.addparam(
2345 b'in-reply-to', pycompat.bytestr(inpart.id), mandatory=False
2350 b'in-reply-to', pycompat.bytestr(inpart.id), mandatory=False
2346 )
2351 )
2347 rpart.addparam(b'return', b'%i' % ret, mandatory=False)
2352 rpart.addparam(b'return', b'%i' % ret, mandatory=False)
2348 if inpart.mandatory and not ret:
2353 if inpart.mandatory and not ret:
2349 kwargs = {}
2354 kwargs = {}
2350 for key in (b'namespace', b'key', b'new', b'old', b'ret'):
2355 for key in (b'namespace', b'key', b'new', b'old', b'ret'):
2351 if key in inpart.params:
2356 if key in inpart.params:
2352 kwargs[key] = inpart.params[key]
2357 kwargs[key] = inpart.params[key]
2353 raise error.PushkeyFailed(
2358 raise error.PushkeyFailed(
2354 partid=b'%d' % inpart.id, **pycompat.strkwargs(kwargs)
2359 partid=b'%d' % inpart.id, **pycompat.strkwargs(kwargs)
2355 )
2360 )
2356
2361
2357
2362
2358 @parthandler(b'bookmarks')
2363 @parthandler(b'bookmarks')
2359 def handlebookmark(op, inpart):
2364 def handlebookmark(op, inpart):
2360 """transmit bookmark information
2365 """transmit bookmark information
2361
2366
2362 The part contains binary encoded bookmark information.
2367 The part contains binary encoded bookmark information.
2363
2368
2364 The exact behavior of this part can be controlled by the 'bookmarks' mode
2369 The exact behavior of this part can be controlled by the 'bookmarks' mode
2365 on the bundle operation.
2370 on the bundle operation.
2366
2371
2367 When mode is 'apply' (the default) the bookmark information is applied as
2372 When mode is 'apply' (the default) the bookmark information is applied as
2368 is to the unbundling repository. Make sure a 'check:bookmarks' part is
2373 is to the unbundling repository. Make sure a 'check:bookmarks' part is
2369 issued earlier to check for push races in such update. This behavior is
2374 issued earlier to check for push races in such update. This behavior is
2370 suitable for pushing.
2375 suitable for pushing.
2371
2376
2372 When mode is 'records', the information is recorded into the 'bookmarks'
2377 When mode is 'records', the information is recorded into the 'bookmarks'
2373 records of the bundle operation. This behavior is suitable for pulling.
2378 records of the bundle operation. This behavior is suitable for pulling.
2374 """
2379 """
2375 changes = bookmarks.binarydecode(op.repo, inpart)
2380 changes = bookmarks.binarydecode(op.repo, inpart)
2376
2381
2377 pushkeycompat = op.repo.ui.configbool(
2382 pushkeycompat = op.repo.ui.configbool(
2378 b'server', b'bookmarks-pushkey-compat'
2383 b'server', b'bookmarks-pushkey-compat'
2379 )
2384 )
2380 bookmarksmode = op.modes.get(b'bookmarks', b'apply')
2385 bookmarksmode = op.modes.get(b'bookmarks', b'apply')
2381
2386
2382 if bookmarksmode == b'apply':
2387 if bookmarksmode == b'apply':
2383 tr = op.gettransaction()
2388 tr = op.gettransaction()
2384 bookstore = op.repo._bookmarks
2389 bookstore = op.repo._bookmarks
2385 if pushkeycompat:
2390 if pushkeycompat:
2386 allhooks = []
2391 allhooks = []
2387 for book, node in changes:
2392 for book, node in changes:
2388 hookargs = tr.hookargs.copy()
2393 hookargs = tr.hookargs.copy()
2389 hookargs[b'pushkeycompat'] = b'1'
2394 hookargs[b'pushkeycompat'] = b'1'
2390 hookargs[b'namespace'] = b'bookmarks'
2395 hookargs[b'namespace'] = b'bookmarks'
2391 hookargs[b'key'] = book
2396 hookargs[b'key'] = book
2392 hookargs[b'old'] = hex(bookstore.get(book, b''))
2397 hookargs[b'old'] = hex(bookstore.get(book, b''))
2393 hookargs[b'new'] = hex(node if node is not None else b'')
2398 hookargs[b'new'] = hex(node if node is not None else b'')
2394 allhooks.append(hookargs)
2399 allhooks.append(hookargs)
2395
2400
2396 for hookargs in allhooks:
2401 for hookargs in allhooks:
2397 op.repo.hook(
2402 op.repo.hook(
2398 b'prepushkey', throw=True, **pycompat.strkwargs(hookargs)
2403 b'prepushkey', throw=True, **pycompat.strkwargs(hookargs)
2399 )
2404 )
2400
2405
2401 for book, node in changes:
2406 for book, node in changes:
2402 if bookmarks.isdivergent(book):
2407 if bookmarks.isdivergent(book):
2403 msg = _(b'cannot accept divergent bookmark %s!') % book
2408 msg = _(b'cannot accept divergent bookmark %s!') % book
2404 raise error.Abort(msg)
2409 raise error.Abort(msg)
2405
2410
2406 bookstore.applychanges(op.repo, op.gettransaction(), changes)
2411 bookstore.applychanges(op.repo, op.gettransaction(), changes)
2407
2412
2408 if pushkeycompat:
2413 if pushkeycompat:
2409
2414
2410 def runhook(unused_success):
2415 def runhook(unused_success):
2411 for hookargs in allhooks:
2416 for hookargs in allhooks:
2412 op.repo.hook(b'pushkey', **pycompat.strkwargs(hookargs))
2417 op.repo.hook(b'pushkey', **pycompat.strkwargs(hookargs))
2413
2418
2414 op.repo._afterlock(runhook)
2419 op.repo._afterlock(runhook)
2415
2420
2416 elif bookmarksmode == b'records':
2421 elif bookmarksmode == b'records':
2417 for book, node in changes:
2422 for book, node in changes:
2418 record = {b'bookmark': book, b'node': node}
2423 record = {b'bookmark': book, b'node': node}
2419 op.records.add(b'bookmarks', record)
2424 op.records.add(b'bookmarks', record)
2420 else:
2425 else:
2421 raise error.ProgrammingError(
2426 raise error.ProgrammingError(
2422 b'unknown bookmark mode: %s' % bookmarksmode
2427 b'unknown bookmark mode: %s' % bookmarksmode
2423 )
2428 )
2424
2429
2425
2430
2426 @parthandler(b'phase-heads')
2431 @parthandler(b'phase-heads')
2427 def handlephases(op, inpart):
2432 def handlephases(op, inpart):
2428 """apply phases from bundle part to repo"""
2433 """apply phases from bundle part to repo"""
2429 headsbyphase = phases.binarydecode(inpart)
2434 headsbyphase = phases.binarydecode(inpart)
2430 phases.updatephases(op.repo.unfiltered(), op.gettransaction, headsbyphase)
2435 phases.updatephases(op.repo.unfiltered(), op.gettransaction, headsbyphase)
2431
2436
2432
2437
2433 @parthandler(b'reply:pushkey', (b'return', b'in-reply-to'))
2438 @parthandler(b'reply:pushkey', (b'return', b'in-reply-to'))
2434 def handlepushkeyreply(op, inpart):
2439 def handlepushkeyreply(op, inpart):
2435 """retrieve the result of a pushkey request"""
2440 """retrieve the result of a pushkey request"""
2436 ret = int(inpart.params[b'return'])
2441 ret = int(inpart.params[b'return'])
2437 partid = int(inpart.params[b'in-reply-to'])
2442 partid = int(inpart.params[b'in-reply-to'])
2438 op.records.add(b'pushkey', {b'return': ret}, partid)
2443 op.records.add(b'pushkey', {b'return': ret}, partid)
2439
2444
2440
2445
2441 @parthandler(b'obsmarkers')
2446 @parthandler(b'obsmarkers')
2442 def handleobsmarker(op, inpart):
2447 def handleobsmarker(op, inpart):
2443 """add a stream of obsmarkers to the repo"""
2448 """add a stream of obsmarkers to the repo"""
2444 tr = op.gettransaction()
2449 tr = op.gettransaction()
2445 markerdata = inpart.read()
2450 markerdata = inpart.read()
2446 if op.ui.config(b'experimental', b'obsmarkers-exchange-debug'):
2451 if op.ui.config(b'experimental', b'obsmarkers-exchange-debug'):
2447 op.ui.writenoi18n(
2452 op.ui.writenoi18n(
2448 b'obsmarker-exchange: %i bytes received\n' % len(markerdata)
2453 b'obsmarker-exchange: %i bytes received\n' % len(markerdata)
2449 )
2454 )
2450 # The mergemarkers call will crash if marker creation is not enabled.
2455 # The mergemarkers call will crash if marker creation is not enabled.
2451 # we want to avoid this if the part is advisory.
2456 # we want to avoid this if the part is advisory.
2452 if not inpart.mandatory and op.repo.obsstore.readonly:
2457 if not inpart.mandatory and op.repo.obsstore.readonly:
2453 op.repo.ui.debug(
2458 op.repo.ui.debug(
2454 b'ignoring obsolescence markers, feature not enabled\n'
2459 b'ignoring obsolescence markers, feature not enabled\n'
2455 )
2460 )
2456 return
2461 return
2457 new = op.repo.obsstore.mergemarkers(tr, markerdata)
2462 new = op.repo.obsstore.mergemarkers(tr, markerdata)
2458 op.repo.invalidatevolatilesets()
2463 op.repo.invalidatevolatilesets()
2459 op.records.add(b'obsmarkers', {b'new': new})
2464 op.records.add(b'obsmarkers', {b'new': new})
2460 if op.reply is not None:
2465 if op.reply is not None:
2461 rpart = op.reply.newpart(b'reply:obsmarkers')
2466 rpart = op.reply.newpart(b'reply:obsmarkers')
2462 rpart.addparam(
2467 rpart.addparam(
2463 b'in-reply-to', pycompat.bytestr(inpart.id), mandatory=False
2468 b'in-reply-to', pycompat.bytestr(inpart.id), mandatory=False
2464 )
2469 )
2465 rpart.addparam(b'new', b'%i' % new, mandatory=False)
2470 rpart.addparam(b'new', b'%i' % new, mandatory=False)
2466
2471
2467
2472
2468 @parthandler(b'reply:obsmarkers', (b'new', b'in-reply-to'))
2473 @parthandler(b'reply:obsmarkers', (b'new', b'in-reply-to'))
2469 def handleobsmarkerreply(op, inpart):
2474 def handleobsmarkerreply(op, inpart):
2470 """retrieve the result of a pushkey request"""
2475 """retrieve the result of a pushkey request"""
2471 ret = int(inpart.params[b'new'])
2476 ret = int(inpart.params[b'new'])
2472 partid = int(inpart.params[b'in-reply-to'])
2477 partid = int(inpart.params[b'in-reply-to'])
2473 op.records.add(b'obsmarkers', {b'new': ret}, partid)
2478 op.records.add(b'obsmarkers', {b'new': ret}, partid)
2474
2479
2475
2480
2476 @parthandler(b'hgtagsfnodes')
2481 @parthandler(b'hgtagsfnodes')
2477 def handlehgtagsfnodes(op, inpart):
2482 def handlehgtagsfnodes(op, inpart):
2478 """Applies .hgtags fnodes cache entries to the local repo.
2483 """Applies .hgtags fnodes cache entries to the local repo.
2479
2484
2480 Payload is pairs of 20 byte changeset nodes and filenodes.
2485 Payload is pairs of 20 byte changeset nodes and filenodes.
2481 """
2486 """
2482 # Grab the transaction so we ensure that we have the lock at this point.
2487 # Grab the transaction so we ensure that we have the lock at this point.
2483 if op.ui.configbool(b'experimental', b'bundle2lazylocking'):
2488 if op.ui.configbool(b'experimental', b'bundle2lazylocking'):
2484 op.gettransaction()
2489 op.gettransaction()
2485 cache = tags.hgtagsfnodescache(op.repo.unfiltered())
2490 cache = tags.hgtagsfnodescache(op.repo.unfiltered())
2486
2491
2487 count = 0
2492 count = 0
2488 while True:
2493 while True:
2489 node = inpart.read(20)
2494 node = inpart.read(20)
2490 fnode = inpart.read(20)
2495 fnode = inpart.read(20)
2491 if len(node) < 20 or len(fnode) < 20:
2496 if len(node) < 20 or len(fnode) < 20:
2492 op.ui.debug(b'ignoring incomplete received .hgtags fnodes data\n')
2497 op.ui.debug(b'ignoring incomplete received .hgtags fnodes data\n')
2493 break
2498 break
2494 cache.setfnode(node, fnode)
2499 cache.setfnode(node, fnode)
2495 count += 1
2500 count += 1
2496
2501
2497 cache.write()
2502 cache.write()
2498 op.ui.debug(b'applied %i hgtags fnodes cache entries\n' % count)
2503 op.ui.debug(b'applied %i hgtags fnodes cache entries\n' % count)
2499
2504
2500
2505
2501 rbcstruct = struct.Struct(b'>III')
2506 rbcstruct = struct.Struct(b'>III')
2502
2507
2503
2508
2504 @parthandler(b'cache:rev-branch-cache')
2509 @parthandler(b'cache:rev-branch-cache')
2505 def handlerbc(op, inpart):
2510 def handlerbc(op, inpart):
2506 """Legacy part, ignored for compatibility with bundles from or
2511 """Legacy part, ignored for compatibility with bundles from or
2507 for Mercurial before 5.7. Newer Mercurial computes the cache
2512 for Mercurial before 5.7. Newer Mercurial computes the cache
2508 efficiently enough during unbundling that the additional transfer
2513 efficiently enough during unbundling that the additional transfer
2509 is unnecessary."""
2514 is unnecessary."""
2510
2515
2511
2516
2512 @parthandler(b'pushvars')
2517 @parthandler(b'pushvars')
2513 def bundle2getvars(op, part):
2518 def bundle2getvars(op, part):
2514 '''unbundle a bundle2 containing shellvars on the server'''
2519 '''unbundle a bundle2 containing shellvars on the server'''
2515 # An option to disable unbundling on server-side for security reasons
2520 # An option to disable unbundling on server-side for security reasons
2516 if op.ui.configbool(b'push', b'pushvars.server'):
2521 if op.ui.configbool(b'push', b'pushvars.server'):
2517 hookargs = {}
2522 hookargs = {}
2518 for key, value in part.advisoryparams:
2523 for key, value in part.advisoryparams:
2519 key = key.upper()
2524 key = key.upper()
2520 # We want pushed variables to have USERVAR_ prepended so we know
2525 # We want pushed variables to have USERVAR_ prepended so we know
2521 # they came from the --pushvar flag.
2526 # they came from the --pushvar flag.
2522 key = b"USERVAR_" + key
2527 key = b"USERVAR_" + key
2523 hookargs[key] = value
2528 hookargs[key] = value
2524 op.addhookargs(hookargs)
2529 op.addhookargs(hookargs)
2525
2530
2526
2531
2527 @parthandler(b'stream2', (b'requirements', b'filecount', b'bytecount'))
2532 @parthandler(b'stream2', (b'requirements', b'filecount', b'bytecount'))
2528 def handlestreamv2bundle(op, part):
2533 def handlestreamv2bundle(op, part):
2529
2534
2530 requirements = urlreq.unquote(part.params[b'requirements'])
2535 requirements = urlreq.unquote(part.params[b'requirements'])
2531 requirements = requirements.split(b',') if requirements else []
2536 requirements = requirements.split(b',') if requirements else []
2532 filecount = int(part.params[b'filecount'])
2537 filecount = int(part.params[b'filecount'])
2533 bytecount = int(part.params[b'bytecount'])
2538 bytecount = int(part.params[b'bytecount'])
2534
2539
2535 repo = op.repo
2540 repo = op.repo
2536 if len(repo):
2541 if len(repo):
2537 msg = _(b'cannot apply stream clone to non empty repository')
2542 msg = _(b'cannot apply stream clone to non empty repository')
2538 raise error.Abort(msg)
2543 raise error.Abort(msg)
2539
2544
2540 repo.ui.debug(b'applying stream bundle\n')
2545 repo.ui.debug(b'applying stream bundle\n')
2541 streamclone.applybundlev2(repo, part, filecount, bytecount, requirements)
2546 streamclone.applybundlev2(repo, part, filecount, bytecount, requirements)
2542
2547
2543
2548
2544 def widen_bundle(
2549 def widen_bundle(
2545 bundler, repo, oldmatcher, newmatcher, common, known, cgversion, ellipses
2550 bundler, repo, oldmatcher, newmatcher, common, known, cgversion, ellipses
2546 ):
2551 ):
2547 """generates bundle2 for widening a narrow clone
2552 """generates bundle2 for widening a narrow clone
2548
2553
2549 bundler is the bundle to which data should be added
2554 bundler is the bundle to which data should be added
2550 repo is the localrepository instance
2555 repo is the localrepository instance
2551 oldmatcher matches what the client already has
2556 oldmatcher matches what the client already has
2552 newmatcher matches what the client needs (including what it already has)
2557 newmatcher matches what the client needs (including what it already has)
2553 common is set of common heads between server and client
2558 common is set of common heads between server and client
2554 known is a set of revs known on the client side (used in ellipses)
2559 known is a set of revs known on the client side (used in ellipses)
2555 cgversion is the changegroup version to send
2560 cgversion is the changegroup version to send
2556 ellipses is boolean value telling whether to send ellipses data or not
2561 ellipses is boolean value telling whether to send ellipses data or not
2557
2562
2558 returns bundle2 of the data required for extending
2563 returns bundle2 of the data required for extending
2559 """
2564 """
2560 commonnodes = set()
2565 commonnodes = set()
2561 cl = repo.changelog
2566 cl = repo.changelog
2562 for r in repo.revs(b"::%ln", common):
2567 for r in repo.revs(b"::%ln", common):
2563 commonnodes.add(cl.node(r))
2568 commonnodes.add(cl.node(r))
2564 if commonnodes:
2569 if commonnodes:
2565 packer = changegroup.getbundler(
2570 packer = changegroup.getbundler(
2566 cgversion,
2571 cgversion,
2567 repo,
2572 repo,
2568 oldmatcher=oldmatcher,
2573 oldmatcher=oldmatcher,
2569 matcher=newmatcher,
2574 matcher=newmatcher,
2570 fullnodes=commonnodes,
2575 fullnodes=commonnodes,
2571 )
2576 )
2572 cgdata = packer.generate(
2577 cgdata = packer.generate(
2573 {repo.nullid},
2578 {repo.nullid},
2574 list(commonnodes),
2579 list(commonnodes),
2575 False,
2580 False,
2576 b'narrow_widen',
2581 b'narrow_widen',
2577 changelog=False,
2582 changelog=False,
2578 )
2583 )
2579
2584
2580 part = bundler.newpart(b'changegroup', data=cgdata)
2585 part = bundler.newpart(b'changegroup', data=cgdata)
2581 part.addparam(b'version', cgversion)
2586 part.addparam(b'version', cgversion)
2582 if scmutil.istreemanifest(repo):
2587 if scmutil.istreemanifest(repo):
2583 part.addparam(b'treemanifest', b'1')
2588 part.addparam(b'treemanifest', b'1')
2584 if repository.REPO_FEATURE_SIDE_DATA in repo.features:
2589 if repository.REPO_FEATURE_SIDE_DATA in repo.features:
2585 part.addparam(b'exp-sidedata', b'1')
2590 part.addparam(b'exp-sidedata', b'1')
2586 wanted = format_remote_wanted_sidedata(repo)
2591 wanted = format_remote_wanted_sidedata(repo)
2587 part.addparam(b'exp-wanted-sidedata', wanted)
2592 part.addparam(b'exp-wanted-sidedata', wanted)
2588
2593
2589 return bundler
2594 return bundler
@@ -1,489 +1,493 b''
1 # bundlecaches.py - utility to deal with pre-computed bundle for servers
1 # bundlecaches.py - utility to deal with pre-computed bundle for servers
2 #
2 #
3 # This software may be used and distributed according to the terms of the
3 # This software may be used and distributed according to the terms of the
4 # GNU General Public License version 2 or any later version.
4 # GNU General Public License version 2 or any later version.
5
5
6 import collections
6 import collections
7
7
8 from typing import (
9 cast,
10 )
11
8 from .i18n import _
12 from .i18n import _
9
13
10 from .thirdparty import attr
14 from .thirdparty import attr
11
15
12 from . import (
16 from . import (
13 error,
17 error,
14 requirements as requirementsmod,
18 requirements as requirementsmod,
15 sslutil,
19 sslutil,
16 util,
20 util,
17 )
21 )
18 from .utils import stringutil
22 from .utils import stringutil
19
23
20 urlreq = util.urlreq
24 urlreq = util.urlreq
21
25
22 CB_MANIFEST_FILE = b'clonebundles.manifest'
26 CB_MANIFEST_FILE = b'clonebundles.manifest'
23
27
24
28
25 @attr.s
29 @attr.s
26 class bundlespec:
30 class bundlespec:
27 compression = attr.ib()
31 compression = attr.ib()
28 wirecompression = attr.ib()
32 wirecompression = attr.ib()
29 version = attr.ib()
33 version = attr.ib()
30 wireversion = attr.ib()
34 wireversion = attr.ib()
31 # parameters explicitly overwritten by the config or the specification
35 # parameters explicitly overwritten by the config or the specification
32 _explicit_params = attr.ib()
36 _explicit_params = attr.ib()
33 # default parameter for the version
37 # default parameter for the version
34 #
38 #
35 # Keeping it separated is useful to check what was actually overwritten.
39 # Keeping it separated is useful to check what was actually overwritten.
36 _default_opts = attr.ib()
40 _default_opts = attr.ib()
37
41
38 @property
42 @property
39 def params(self):
43 def params(self):
40 return collections.ChainMap(self._explicit_params, self._default_opts)
44 return collections.ChainMap(self._explicit_params, self._default_opts)
41
45
42 @property
46 @property
43 def contentopts(self):
47 def contentopts(self):
44 # kept for Backward Compatibility concerns.
48 # kept for Backward Compatibility concerns.
45 return self.params
49 return self.params
46
50
47 def set_param(self, key, value, overwrite=True):
51 def set_param(self, key, value, overwrite=True):
48 """Set a bundle parameter value.
52 """Set a bundle parameter value.
49
53
50 Will only overwrite if overwrite is true"""
54 Will only overwrite if overwrite is true"""
51 if overwrite or key not in self._explicit_params:
55 if overwrite or key not in self._explicit_params:
52 self._explicit_params[key] = value
56 self._explicit_params[key] = value
53
57
54
58
55 # Maps bundle version human names to changegroup versions.
59 # Maps bundle version human names to changegroup versions.
56 _bundlespeccgversions = {
60 _bundlespeccgversions = {
57 b'v1': b'01',
61 b'v1': b'01',
58 b'v2': b'02',
62 b'v2': b'02',
59 b'packed1': b's1',
63 b'packed1': b's1',
60 b'bundle2': b'02', # legacy
64 b'bundle2': b'02', # legacy
61 }
65 }
62
66
63 # Maps bundle version with content opts to choose which part to bundle
67 # Maps bundle version with content opts to choose which part to bundle
64 _bundlespeccontentopts = {
68 _bundlespeccontentopts = {
65 b'v1': {
69 b'v1': {
66 b'changegroup': True,
70 b'changegroup': True,
67 b'cg.version': b'01',
71 b'cg.version': b'01',
68 b'obsolescence': False,
72 b'obsolescence': False,
69 b'phases': False,
73 b'phases': False,
70 b'tagsfnodescache': False,
74 b'tagsfnodescache': False,
71 b'revbranchcache': False,
75 b'revbranchcache': False,
72 },
76 },
73 b'v2': {
77 b'v2': {
74 b'changegroup': True,
78 b'changegroup': True,
75 b'cg.version': b'02',
79 b'cg.version': b'02',
76 b'obsolescence': False,
80 b'obsolescence': False,
77 b'phases': False,
81 b'phases': False,
78 b'tagsfnodescache': True,
82 b'tagsfnodescache': True,
79 b'revbranchcache': True,
83 b'revbranchcache': True,
80 },
84 },
81 b'streamv2': {
85 b'streamv2': {
82 b'changegroup': False,
86 b'changegroup': False,
83 b'cg.version': b'02',
87 b'cg.version': b'02',
84 b'obsolescence': False,
88 b'obsolescence': False,
85 b'phases': False,
89 b'phases': False,
86 b"streamv2": True,
90 b"streamv2": True,
87 b'tagsfnodescache': False,
91 b'tagsfnodescache': False,
88 b'revbranchcache': False,
92 b'revbranchcache': False,
89 },
93 },
90 b'packed1': {
94 b'packed1': {
91 b'cg.version': b's1',
95 b'cg.version': b's1',
92 },
96 },
93 b'bundle2': { # legacy
97 b'bundle2': { # legacy
94 b'cg.version': b'02',
98 b'cg.version': b'02',
95 },
99 },
96 }
100 }
97 _bundlespeccontentopts[b'bundle2'] = _bundlespeccontentopts[b'v2']
101 _bundlespeccontentopts[b'bundle2'] = _bundlespeccontentopts[b'v2']
98
102
99 _bundlespecvariants = {b"streamv2": {}}
103 _bundlespecvariants = {b"streamv2": {}}
100
104
101 # Compression engines allowed in version 1. THIS SHOULD NEVER CHANGE.
105 # Compression engines allowed in version 1. THIS SHOULD NEVER CHANGE.
102 _bundlespecv1compengines = {b'gzip', b'bzip2', b'none'}
106 _bundlespecv1compengines = {b'gzip', b'bzip2', b'none'}
103
107
104
108
105 def param_bool(key, value):
109 def param_bool(key, value):
106 """make a boolean out of a parameter value"""
110 """make a boolean out of a parameter value"""
107 b = stringutil.parsebool(value)
111 b = stringutil.parsebool(value)
108 if b is None:
112 if b is None:
109 msg = _(b"parameter %s should be a boolean ('%s')")
113 msg = _(b"parameter %s should be a boolean ('%s')")
110 msg %= (key, value)
114 msg %= (key, value)
111 raise error.InvalidBundleSpecification(msg)
115 raise error.InvalidBundleSpecification(msg)
112 return b
116 return b
113
117
114
118
115 # mapping of known parameter name need their value processed
119 # mapping of known parameter name need their value processed
116 bundle_spec_param_processing = {
120 bundle_spec_param_processing = {
117 b"obsolescence": param_bool,
121 b"obsolescence": param_bool,
118 b"obsolescence-mandatory": param_bool,
122 b"obsolescence-mandatory": param_bool,
119 b"phases": param_bool,
123 b"phases": param_bool,
120 }
124 }
121
125
122
126
123 def _parseparams(s):
127 def _parseparams(s):
124 """parse bundlespec parameter section
128 """parse bundlespec parameter section
125
129
126 input: "comp-version;params" string
130 input: "comp-version;params" string
127
131
128 return: (spec; {param_key: param_value})
132 return: (spec; {param_key: param_value})
129 """
133 """
130 if b';' not in s:
134 if b';' not in s:
131 return s, {}
135 return s, {}
132
136
133 params = {}
137 params = {}
134 version, paramstr = s.split(b';', 1)
138 version, paramstr = s.split(b';', 1)
135
139
136 err = _(b'invalid bundle specification: missing "=" in parameter: %s')
140 err = _(b'invalid bundle specification: missing "=" in parameter: %s')
137 for p in paramstr.split(b';'):
141 for p in paramstr.split(b';'):
138 if b'=' not in p:
142 if b'=' not in p:
139 msg = err % p
143 msg = err % p
140 raise error.InvalidBundleSpecification(msg)
144 raise error.InvalidBundleSpecification(msg)
141
145
142 key, value = p.split(b'=', 1)
146 key, value = p.split(b'=', 1)
143 key = urlreq.unquote(key)
147 key = urlreq.unquote(key)
144 value = urlreq.unquote(value)
148 value = urlreq.unquote(value)
145 process = bundle_spec_param_processing.get(key)
149 process = bundle_spec_param_processing.get(key)
146 if process is not None:
150 if process is not None:
147 value = process(key, value)
151 value = process(key, value)
148 params[key] = value
152 params[key] = value
149
153
150 return version, params
154 return version, params
151
155
152
156
153 def parsebundlespec(repo, spec, strict=True):
157 def parsebundlespec(repo, spec, strict=True):
154 """Parse a bundle string specification into parts.
158 """Parse a bundle string specification into parts.
155
159
156 Bundle specifications denote a well-defined bundle/exchange format.
160 Bundle specifications denote a well-defined bundle/exchange format.
157 The content of a given specification should not change over time in
161 The content of a given specification should not change over time in
158 order to ensure that bundles produced by a newer version of Mercurial are
162 order to ensure that bundles produced by a newer version of Mercurial are
159 readable from an older version.
163 readable from an older version.
160
164
161 The string currently has the form:
165 The string currently has the form:
162
166
163 <compression>-<type>[;<parameter0>[;<parameter1>]]
167 <compression>-<type>[;<parameter0>[;<parameter1>]]
164
168
165 Where <compression> is one of the supported compression formats
169 Where <compression> is one of the supported compression formats
166 and <type> is (currently) a version string. A ";" can follow the type and
170 and <type> is (currently) a version string. A ";" can follow the type and
167 all text afterwards is interpreted as URI encoded, ";" delimited key=value
171 all text afterwards is interpreted as URI encoded, ";" delimited key=value
168 pairs.
172 pairs.
169
173
170 If ``strict`` is True (the default) <compression> is required. Otherwise,
174 If ``strict`` is True (the default) <compression> is required. Otherwise,
171 it is optional.
175 it is optional.
172
176
173 Returns a bundlespec object of (compression, version, parameters).
177 Returns a bundlespec object of (compression, version, parameters).
174 Compression will be ``None`` if not in strict mode and a compression isn't
178 Compression will be ``None`` if not in strict mode and a compression isn't
175 defined.
179 defined.
176
180
177 An ``InvalidBundleSpecification`` is raised when the specification is
181 An ``InvalidBundleSpecification`` is raised when the specification is
178 not syntactically well formed.
182 not syntactically well formed.
179
183
180 An ``UnsupportedBundleSpecification`` is raised when the compression or
184 An ``UnsupportedBundleSpecification`` is raised when the compression or
181 bundle type/version is not recognized.
185 bundle type/version is not recognized.
182
186
183 Note: this function will likely eventually return a more complex data
187 Note: this function will likely eventually return a more complex data
184 structure, including bundle2 part information.
188 structure, including bundle2 part information.
185 """
189 """
186 if strict and b'-' not in spec:
190 if strict and b'-' not in spec:
187 raise error.InvalidBundleSpecification(
191 raise error.InvalidBundleSpecification(
188 _(
192 _(
189 b'invalid bundle specification; '
193 b'invalid bundle specification; '
190 b'must be prefixed with compression: %s'
194 b'must be prefixed with compression: %s'
191 )
195 )
192 % spec
196 % spec
193 )
197 )
194
198
195 pre_args = spec.split(b';', 1)[0]
199 pre_args = spec.split(b';', 1)[0]
196 if b'-' in pre_args:
200 if b'-' in pre_args:
197 compression, version = spec.split(b'-', 1)
201 compression, version = spec.split(b'-', 1)
198
202
199 if compression not in util.compengines.supportedbundlenames:
203 if compression not in util.compengines.supportedbundlenames:
200 raise error.UnsupportedBundleSpecification(
204 raise error.UnsupportedBundleSpecification(
201 _(b'%s compression is not supported') % compression
205 _(b'%s compression is not supported') % compression
202 )
206 )
203
207
204 version, params = _parseparams(version)
208 version, params = _parseparams(version)
205
209
206 if version not in _bundlespeccontentopts:
210 if version not in _bundlespeccontentopts:
207 raise error.UnsupportedBundleSpecification(
211 raise error.UnsupportedBundleSpecification(
208 _(b'%s is not a recognized bundle version') % version
212 _(b'%s is not a recognized bundle version') % version
209 )
213 )
210 else:
214 else:
211 # Value could be just the compression or just the version, in which
215 # Value could be just the compression or just the version, in which
212 # case some defaults are assumed (but only when not in strict mode).
216 # case some defaults are assumed (but only when not in strict mode).
213 assert not strict
217 assert not strict
214
218
215 spec, params = _parseparams(spec)
219 spec, params = _parseparams(spec)
216
220
217 if spec in util.compengines.supportedbundlenames:
221 if spec in util.compengines.supportedbundlenames:
218 compression = spec
222 compression = spec
219 version = b'v1'
223 version = b'v1'
220 # Generaldelta repos require v2.
224 # Generaldelta repos require v2.
221 if requirementsmod.GENERALDELTA_REQUIREMENT in repo.requirements:
225 if requirementsmod.GENERALDELTA_REQUIREMENT in repo.requirements:
222 version = b'v2'
226 version = b'v2'
223 elif requirementsmod.REVLOGV2_REQUIREMENT in repo.requirements:
227 elif requirementsmod.REVLOGV2_REQUIREMENT in repo.requirements:
224 version = b'v2'
228 version = b'v2'
225 # Modern compression engines require v2.
229 # Modern compression engines require v2.
226 if compression not in _bundlespecv1compengines:
230 if compression not in _bundlespecv1compengines:
227 version = b'v2'
231 version = b'v2'
228 elif spec in _bundlespeccontentopts:
232 elif spec in _bundlespeccontentopts:
229 if spec == b'packed1':
233 if spec == b'packed1':
230 compression = b'none'
234 compression = b'none'
231 else:
235 else:
232 compression = b'bzip2'
236 compression = b'bzip2'
233 version = spec
237 version = spec
234 else:
238 else:
235 raise error.UnsupportedBundleSpecification(
239 raise error.UnsupportedBundleSpecification(
236 _(b'%s is not a recognized bundle specification') % spec
240 _(b'%s is not a recognized bundle specification') % spec
237 )
241 )
238
242
239 # Bundle version 1 only supports a known set of compression engines.
243 # Bundle version 1 only supports a known set of compression engines.
240 if version == b'v1' and compression not in _bundlespecv1compengines:
244 if version == b'v1' and compression not in _bundlespecv1compengines:
241 raise error.UnsupportedBundleSpecification(
245 raise error.UnsupportedBundleSpecification(
242 _(b'compression engine %s is not supported on v1 bundles')
246 _(b'compression engine %s is not supported on v1 bundles')
243 % compression
247 % compression
244 )
248 )
245
249
246 # The specification for packed1 can optionally declare the data formats
250 # The specification for packed1 can optionally declare the data formats
247 # required to apply it. If we see this metadata, compare against what the
251 # required to apply it. If we see this metadata, compare against what the
248 # repo supports and error if the bundle isn't compatible.
252 # repo supports and error if the bundle isn't compatible.
249 if version == b'packed1' and b'requirements' in params:
253 if version == b'packed1' and b'requirements' in params:
250 requirements = set(params[b'requirements'].split(b','))
254 requirements = set(cast(bytes, params[b'requirements']).split(b','))
251 missingreqs = requirements - requirementsmod.STREAM_FIXED_REQUIREMENTS
255 missingreqs = requirements - requirementsmod.STREAM_FIXED_REQUIREMENTS
252 if missingreqs:
256 if missingreqs:
253 raise error.UnsupportedBundleSpecification(
257 raise error.UnsupportedBundleSpecification(
254 _(b'missing support for repository features: %s')
258 _(b'missing support for repository features: %s')
255 % b', '.join(sorted(missingreqs))
259 % b', '.join(sorted(missingreqs))
256 )
260 )
257
261
258 # Compute contentopts based on the version
262 # Compute contentopts based on the version
259 if b"stream" in params and params[b"stream"] == b"v2":
263 if b"stream" in params and params[b"stream"] == b"v2":
260 # That case is fishy as this mostly derails the version selection
264 # That case is fishy as this mostly derails the version selection
261 # mechanism. `stream` bundles are quite specific and used differently
265 # mechanism. `stream` bundles are quite specific and used differently
262 # as "normal" bundles.
266 # as "normal" bundles.
263 #
267 #
264 # So we are pinning this to "v2", as this will likely be
268 # So we are pinning this to "v2", as this will likely be
265 # compatible forever. (see the next conditional).
269 # compatible forever. (see the next conditional).
266 #
270 #
267 # (we should probably define a cleaner way to do this and raise a
271 # (we should probably define a cleaner way to do this and raise a
268 # warning when the old way is encounter)
272 # warning when the old way is encounter)
269 version = b"streamv2"
273 version = b"streamv2"
270 contentopts = _bundlespeccontentopts.get(version, {}).copy()
274 contentopts = _bundlespeccontentopts.get(version, {}).copy()
271 if version == b"streamv2":
275 if version == b"streamv2":
272 # streamv2 have been reported as "v2" for a while.
276 # streamv2 have been reported as "v2" for a while.
273 version = b"v2"
277 version = b"v2"
274
278
275 engine = util.compengines.forbundlename(compression)
279 engine = util.compengines.forbundlename(compression)
276 compression, wirecompression = engine.bundletype()
280 compression, wirecompression = engine.bundletype()
277 wireversion = _bundlespeccontentopts[version][b'cg.version']
281 wireversion = _bundlespeccontentopts[version][b'cg.version']
278
282
279 return bundlespec(
283 return bundlespec(
280 compression, wirecompression, version, wireversion, params, contentopts
284 compression, wirecompression, version, wireversion, params, contentopts
281 )
285 )
282
286
283
287
284 def parseclonebundlesmanifest(repo, s):
288 def parseclonebundlesmanifest(repo, s):
285 """Parses the raw text of a clone bundles manifest.
289 """Parses the raw text of a clone bundles manifest.
286
290
287 Returns a list of dicts. The dicts have a ``URL`` key corresponding
291 Returns a list of dicts. The dicts have a ``URL`` key corresponding
288 to the URL and other keys are the attributes for the entry.
292 to the URL and other keys are the attributes for the entry.
289 """
293 """
290 m = []
294 m = []
291 for line in s.splitlines():
295 for line in s.splitlines():
292 fields = line.split()
296 fields = line.split()
293 if not fields:
297 if not fields:
294 continue
298 continue
295 attrs = {b'URL': fields[0]}
299 attrs = {b'URL': fields[0]}
296 for rawattr in fields[1:]:
300 for rawattr in fields[1:]:
297 key, value = rawattr.split(b'=', 1)
301 key, value = rawattr.split(b'=', 1)
298 key = util.urlreq.unquote(key)
302 key = util.urlreq.unquote(key)
299 value = util.urlreq.unquote(value)
303 value = util.urlreq.unquote(value)
300 attrs[key] = value
304 attrs[key] = value
301
305
302 # Parse BUNDLESPEC into components. This makes client-side
306 # Parse BUNDLESPEC into components. This makes client-side
303 # preferences easier to specify since you can prefer a single
307 # preferences easier to specify since you can prefer a single
304 # component of the BUNDLESPEC.
308 # component of the BUNDLESPEC.
305 if key == b'BUNDLESPEC':
309 if key == b'BUNDLESPEC':
306 try:
310 try:
307 bundlespec = parsebundlespec(repo, value)
311 bundlespec = parsebundlespec(repo, value)
308 attrs[b'COMPRESSION'] = bundlespec.compression
312 attrs[b'COMPRESSION'] = bundlespec.compression
309 attrs[b'VERSION'] = bundlespec.version
313 attrs[b'VERSION'] = bundlespec.version
310 except error.InvalidBundleSpecification:
314 except error.InvalidBundleSpecification:
311 pass
315 pass
312 except error.UnsupportedBundleSpecification:
316 except error.UnsupportedBundleSpecification:
313 pass
317 pass
314
318
315 m.append(attrs)
319 m.append(attrs)
316
320
317 return m
321 return m
318
322
319
323
320 def isstreamclonespec(bundlespec):
324 def isstreamclonespec(bundlespec):
321 # Stream clone v1
325 # Stream clone v1
322 if bundlespec.wirecompression == b'UN' and bundlespec.wireversion == b's1':
326 if bundlespec.wirecompression == b'UN' and bundlespec.wireversion == b's1':
323 return True
327 return True
324
328
325 # Stream clone v2
329 # Stream clone v2
326 if (
330 if (
327 bundlespec.wirecompression == b'UN'
331 bundlespec.wirecompression == b'UN'
328 and bundlespec.wireversion == b'02'
332 and bundlespec.wireversion == b'02'
329 and bundlespec.contentopts.get(b'streamv2')
333 and bundlespec.contentopts.get(b'streamv2')
330 ):
334 ):
331 return True
335 return True
332
336
333 return False
337 return False
334
338
335
339
336 def filterclonebundleentries(repo, entries, streamclonerequested=False):
340 def filterclonebundleentries(repo, entries, streamclonerequested=False):
337 """Remove incompatible clone bundle manifest entries.
341 """Remove incompatible clone bundle manifest entries.
338
342
339 Accepts a list of entries parsed with ``parseclonebundlesmanifest``
343 Accepts a list of entries parsed with ``parseclonebundlesmanifest``
340 and returns a new list consisting of only the entries that this client
344 and returns a new list consisting of only the entries that this client
341 should be able to apply.
345 should be able to apply.
342
346
343 There is no guarantee we'll be able to apply all returned entries because
347 There is no guarantee we'll be able to apply all returned entries because
344 the metadata we use to filter on may be missing or wrong.
348 the metadata we use to filter on may be missing or wrong.
345 """
349 """
346 newentries = []
350 newentries = []
347 for entry in entries:
351 for entry in entries:
348 spec = entry.get(b'BUNDLESPEC')
352 spec = entry.get(b'BUNDLESPEC')
349 if spec:
353 if spec:
350 try:
354 try:
351 bundlespec = parsebundlespec(repo, spec, strict=True)
355 bundlespec = parsebundlespec(repo, spec, strict=True)
352
356
353 # If a stream clone was requested, filter out non-streamclone
357 # If a stream clone was requested, filter out non-streamclone
354 # entries.
358 # entries.
355 if streamclonerequested and not isstreamclonespec(bundlespec):
359 if streamclonerequested and not isstreamclonespec(bundlespec):
356 repo.ui.debug(
360 repo.ui.debug(
357 b'filtering %s because not a stream clone\n'
361 b'filtering %s because not a stream clone\n'
358 % entry[b'URL']
362 % entry[b'URL']
359 )
363 )
360 continue
364 continue
361
365
362 except error.InvalidBundleSpecification as e:
366 except error.InvalidBundleSpecification as e:
363 repo.ui.debug(stringutil.forcebytestr(e) + b'\n')
367 repo.ui.debug(stringutil.forcebytestr(e) + b'\n')
364 continue
368 continue
365 except error.UnsupportedBundleSpecification as e:
369 except error.UnsupportedBundleSpecification as e:
366 repo.ui.debug(
370 repo.ui.debug(
367 b'filtering %s because unsupported bundle '
371 b'filtering %s because unsupported bundle '
368 b'spec: %s\n' % (entry[b'URL'], stringutil.forcebytestr(e))
372 b'spec: %s\n' % (entry[b'URL'], stringutil.forcebytestr(e))
369 )
373 )
370 continue
374 continue
371 # If we don't have a spec and requested a stream clone, we don't know
375 # If we don't have a spec and requested a stream clone, we don't know
372 # what the entry is so don't attempt to apply it.
376 # what the entry is so don't attempt to apply it.
373 elif streamclonerequested:
377 elif streamclonerequested:
374 repo.ui.debug(
378 repo.ui.debug(
375 b'filtering %s because cannot determine if a stream '
379 b'filtering %s because cannot determine if a stream '
376 b'clone bundle\n' % entry[b'URL']
380 b'clone bundle\n' % entry[b'URL']
377 )
381 )
378 continue
382 continue
379
383
380 if b'REQUIRESNI' in entry and not sslutil.hassni:
384 if b'REQUIRESNI' in entry and not sslutil.hassni:
381 repo.ui.debug(
385 repo.ui.debug(
382 b'filtering %s because SNI not supported\n' % entry[b'URL']
386 b'filtering %s because SNI not supported\n' % entry[b'URL']
383 )
387 )
384 continue
388 continue
385
389
386 if b'REQUIREDRAM' in entry:
390 if b'REQUIREDRAM' in entry:
387 try:
391 try:
388 requiredram = util.sizetoint(entry[b'REQUIREDRAM'])
392 requiredram = util.sizetoint(entry[b'REQUIREDRAM'])
389 except error.ParseError:
393 except error.ParseError:
390 repo.ui.debug(
394 repo.ui.debug(
391 b'filtering %s due to a bad REQUIREDRAM attribute\n'
395 b'filtering %s due to a bad REQUIREDRAM attribute\n'
392 % entry[b'URL']
396 % entry[b'URL']
393 )
397 )
394 continue
398 continue
395 actualram = repo.ui.estimatememory()
399 actualram = repo.ui.estimatememory()
396 if actualram is not None and actualram * 0.66 < requiredram:
400 if actualram is not None and actualram * 0.66 < requiredram:
397 repo.ui.debug(
401 repo.ui.debug(
398 b'filtering %s as it needs more than 2/3 of system memory\n'
402 b'filtering %s as it needs more than 2/3 of system memory\n'
399 % entry[b'URL']
403 % entry[b'URL']
400 )
404 )
401 continue
405 continue
402
406
403 newentries.append(entry)
407 newentries.append(entry)
404
408
405 return newentries
409 return newentries
406
410
407
411
408 class clonebundleentry:
412 class clonebundleentry:
409 """Represents an item in a clone bundles manifest.
413 """Represents an item in a clone bundles manifest.
410
414
411 This rich class is needed to support sorting since sorted() in Python 3
415 This rich class is needed to support sorting since sorted() in Python 3
412 doesn't support ``cmp`` and our comparison is complex enough that ``key=``
416 doesn't support ``cmp`` and our comparison is complex enough that ``key=``
413 won't work.
417 won't work.
414 """
418 """
415
419
416 def __init__(self, value, prefers):
420 def __init__(self, value, prefers):
417 self.value = value
421 self.value = value
418 self.prefers = prefers
422 self.prefers = prefers
419
423
420 def _cmp(self, other):
424 def _cmp(self, other):
421 for prefkey, prefvalue in self.prefers:
425 for prefkey, prefvalue in self.prefers:
422 avalue = self.value.get(prefkey)
426 avalue = self.value.get(prefkey)
423 bvalue = other.value.get(prefkey)
427 bvalue = other.value.get(prefkey)
424
428
425 # Special case for b missing attribute and a matches exactly.
429 # Special case for b missing attribute and a matches exactly.
426 if avalue is not None and bvalue is None and avalue == prefvalue:
430 if avalue is not None and bvalue is None and avalue == prefvalue:
427 return -1
431 return -1
428
432
429 # Special case for a missing attribute and b matches exactly.
433 # Special case for a missing attribute and b matches exactly.
430 if bvalue is not None and avalue is None and bvalue == prefvalue:
434 if bvalue is not None and avalue is None and bvalue == prefvalue:
431 return 1
435 return 1
432
436
433 # We can't compare unless attribute present on both.
437 # We can't compare unless attribute present on both.
434 if avalue is None or bvalue is None:
438 if avalue is None or bvalue is None:
435 continue
439 continue
436
440
437 # Same values should fall back to next attribute.
441 # Same values should fall back to next attribute.
438 if avalue == bvalue:
442 if avalue == bvalue:
439 continue
443 continue
440
444
441 # Exact matches come first.
445 # Exact matches come first.
442 if avalue == prefvalue:
446 if avalue == prefvalue:
443 return -1
447 return -1
444 if bvalue == prefvalue:
448 if bvalue == prefvalue:
445 return 1
449 return 1
446
450
447 # Fall back to next attribute.
451 # Fall back to next attribute.
448 continue
452 continue
449
453
450 # If we got here we couldn't sort by attributes and prefers. Fall
454 # If we got here we couldn't sort by attributes and prefers. Fall
451 # back to index order.
455 # back to index order.
452 return 0
456 return 0
453
457
454 def __lt__(self, other):
458 def __lt__(self, other):
455 return self._cmp(other) < 0
459 return self._cmp(other) < 0
456
460
457 def __gt__(self, other):
461 def __gt__(self, other):
458 return self._cmp(other) > 0
462 return self._cmp(other) > 0
459
463
460 def __eq__(self, other):
464 def __eq__(self, other):
461 return self._cmp(other) == 0
465 return self._cmp(other) == 0
462
466
463 def __le__(self, other):
467 def __le__(self, other):
464 return self._cmp(other) <= 0
468 return self._cmp(other) <= 0
465
469
466 def __ge__(self, other):
470 def __ge__(self, other):
467 return self._cmp(other) >= 0
471 return self._cmp(other) >= 0
468
472
469 def __ne__(self, other):
473 def __ne__(self, other):
470 return self._cmp(other) != 0
474 return self._cmp(other) != 0
471
475
472
476
473 def sortclonebundleentries(ui, entries):
477 def sortclonebundleentries(ui, entries):
474 prefers = ui.configlist(b'ui', b'clonebundleprefers')
478 prefers = ui.configlist(b'ui', b'clonebundleprefers')
475 if not prefers:
479 if not prefers:
476 return list(entries)
480 return list(entries)
477
481
478 def _split(p):
482 def _split(p):
479 if b'=' not in p:
483 if b'=' not in p:
480 hint = _(b"each comma separated item should be key=value pairs")
484 hint = _(b"each comma separated item should be key=value pairs")
481 raise error.Abort(
485 raise error.Abort(
482 _(b"invalid ui.clonebundleprefers item: %s") % p, hint=hint
486 _(b"invalid ui.clonebundleprefers item: %s") % p, hint=hint
483 )
487 )
484 return p.split(b'=', 1)
488 return p.split(b'=', 1)
485
489
486 prefers = [_split(p) for p in prefers]
490 prefers = [_split(p) for p in prefers]
487
491
488 items = sorted(clonebundleentry(v, prefers) for v in entries)
492 items = sorted(clonebundleentry(v, prefers) for v in entries)
489 return [i.value for i in items]
493 return [i.value for i in items]
@@ -1,998 +1,1002 b''
1 # stringutil.py - utility for generic string formatting, parsing, etc.
1 # stringutil.py - utility for generic string formatting, parsing, etc.
2 #
2 #
3 # Copyright 2005 K. Thananchayan <thananck@yahoo.com>
3 # Copyright 2005 K. Thananchayan <thananck@yahoo.com>
4 # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
4 # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 #
6 #
7 # This software may be used and distributed according to the terms of the
7 # This software may be used and distributed according to the terms of the
8 # GNU General Public License version 2 or any later version.
8 # GNU General Public License version 2 or any later version.
9
9
10
10
11 import ast
11 import ast
12 import codecs
12 import codecs
13 import re as remod
13 import re as remod
14 import textwrap
14 import textwrap
15 import types
15 import types
16
16
17 from typing import (
17 from typing import (
18 Optional,
18 Optional,
19 overload,
19 overload,
20 )
20 )
21
21
22 from ..i18n import _
22 from ..i18n import _
23 from ..thirdparty import attr
23 from ..thirdparty import attr
24
24
25 from .. import (
25 from .. import (
26 encoding,
26 encoding,
27 error,
27 error,
28 pycompat,
28 pycompat,
29 )
29 )
30
30
31 # regex special chars pulled from https://bugs.python.org/issue29995
31 # regex special chars pulled from https://bugs.python.org/issue29995
32 # which was part of Python 3.7.
32 # which was part of Python 3.7.
33 _respecial = pycompat.bytestr(b'()[]{}?*+-|^$\\.&~# \t\n\r\v\f')
33 _respecial = pycompat.bytestr(b'()[]{}?*+-|^$\\.&~# \t\n\r\v\f')
34 _regexescapemap = {ord(i): (b'\\' + i).decode('latin1') for i in _respecial}
34 _regexescapemap = {ord(i): (b'\\' + i).decode('latin1') for i in _respecial}
35 regexbytesescapemap = {i: (b'\\' + i) for i in _respecial}
35 regexbytesescapemap = {i: (b'\\' + i) for i in _respecial}
36
36
37
37
38 @overload
38 @overload
39 def reescape(pat: bytes) -> bytes:
39 def reescape(pat: bytes) -> bytes:
40 ...
40 ...
41
41
42
42
43 @overload
43 @overload
44 def reescape(pat: str) -> str:
44 def reescape(pat: str) -> str:
45 ...
45 ...
46
46
47
47
48 def reescape(pat):
48 def reescape(pat):
49 """Drop-in replacement for re.escape."""
49 """Drop-in replacement for re.escape."""
50 # NOTE: it is intentional that this works on unicodes and not
50 # NOTE: it is intentional that this works on unicodes and not
51 # bytes, as it's only possible to do the escaping with
51 # bytes, as it's only possible to do the escaping with
52 # unicode.translate, not bytes.translate. Sigh.
52 # unicode.translate, not bytes.translate. Sigh.
53 wantuni = True
53 wantuni = True
54 if isinstance(pat, bytes):
54 if isinstance(pat, bytes):
55 wantuni = False
55 wantuni = False
56 pat = pat.decode('latin1')
56 pat = pat.decode('latin1')
57 pat = pat.translate(_regexescapemap)
57 pat = pat.translate(_regexescapemap)
58 if wantuni:
58 if wantuni:
59 return pat
59 return pat
60 return pat.encode('latin1')
60 return pat.encode('latin1')
61
61
62
62
63 def pprint(o, bprefix: bool = False, indent: int = 0, level: int = 0) -> bytes:
63 def pprint(o, bprefix: bool = False, indent: int = 0, level: int = 0) -> bytes:
64 """Pretty print an object."""
64 """Pretty print an object."""
65 return b''.join(pprintgen(o, bprefix=bprefix, indent=indent, level=level))
65 return b''.join(pprintgen(o, bprefix=bprefix, indent=indent, level=level))
66
66
67
67
68 def pprintgen(o, bprefix: bool = False, indent: int = 0, level: int = 0):
68 def pprintgen(o, bprefix: bool = False, indent: int = 0, level: int = 0):
69 """Pretty print an object to a generator of atoms.
69 """Pretty print an object to a generator of atoms.
70
70
71 ``bprefix`` is a flag influencing whether bytestrings are preferred with
71 ``bprefix`` is a flag influencing whether bytestrings are preferred with
72 a ``b''`` prefix.
72 a ``b''`` prefix.
73
73
74 ``indent`` controls whether collections and nested data structures
74 ``indent`` controls whether collections and nested data structures
75 span multiple lines via the indentation amount in spaces. By default,
75 span multiple lines via the indentation amount in spaces. By default,
76 no newlines are emitted.
76 no newlines are emitted.
77
77
78 ``level`` specifies the initial indent level. Used if ``indent > 0``.
78 ``level`` specifies the initial indent level. Used if ``indent > 0``.
79 """
79 """
80
80
81 if isinstance(o, bytes):
81 if isinstance(o, bytes):
82 if bprefix:
82 if bprefix:
83 yield b"b'%s'" % escapestr(o)
83 yield b"b'%s'" % escapestr(o)
84 else:
84 else:
85 yield b"'%s'" % escapestr(o)
85 yield b"'%s'" % escapestr(o)
86 elif isinstance(o, bytearray):
86 elif isinstance(o, bytearray):
87 # codecs.escape_encode() can't handle bytearray, so escapestr fails
87 # codecs.escape_encode() can't handle bytearray, so escapestr fails
88 # without coercion.
88 # without coercion.
89 yield b"bytearray['%s']" % escapestr(bytes(o))
89 yield b"bytearray['%s']" % escapestr(bytes(o))
90 elif isinstance(o, list):
90 elif isinstance(o, list):
91 if not o:
91 if not o:
92 yield b'[]'
92 yield b'[]'
93 return
93 return
94
94
95 yield b'['
95 yield b'['
96
96
97 if indent:
97 if indent:
98 level += 1
98 level += 1
99 yield b'\n'
99 yield b'\n'
100 yield b' ' * (level * indent)
100 yield b' ' * (level * indent)
101
101
102 for i, a in enumerate(o):
102 for i, a in enumerate(o):
103 for chunk in pprintgen(
103 for chunk in pprintgen(
104 a, bprefix=bprefix, indent=indent, level=level
104 a, bprefix=bprefix, indent=indent, level=level
105 ):
105 ):
106 yield chunk
106 yield chunk
107
107
108 if i + 1 < len(o):
108 if i + 1 < len(o):
109 if indent:
109 if indent:
110 yield b',\n'
110 yield b',\n'
111 yield b' ' * (level * indent)
111 yield b' ' * (level * indent)
112 else:
112 else:
113 yield b', '
113 yield b', '
114
114
115 if indent:
115 if indent:
116 level -= 1
116 level -= 1
117 yield b'\n'
117 yield b'\n'
118 yield b' ' * (level * indent)
118 yield b' ' * (level * indent)
119
119
120 yield b']'
120 yield b']'
121 elif isinstance(o, dict):
121 elif isinstance(o, dict):
122 if not o:
122 if not o:
123 yield b'{}'
123 yield b'{}'
124 return
124 return
125
125
126 yield b'{'
126 yield b'{'
127
127
128 if indent:
128 if indent:
129 level += 1
129 level += 1
130 yield b'\n'
130 yield b'\n'
131 yield b' ' * (level * indent)
131 yield b' ' * (level * indent)
132
132
133 for i, (k, v) in enumerate(sorted(o.items())):
133 for i, (k, v) in enumerate(sorted(o.items())):
134 for chunk in pprintgen(
134 for chunk in pprintgen(
135 k, bprefix=bprefix, indent=indent, level=level
135 k, bprefix=bprefix, indent=indent, level=level
136 ):
136 ):
137 yield chunk
137 yield chunk
138
138
139 yield b': '
139 yield b': '
140
140
141 for chunk in pprintgen(
141 for chunk in pprintgen(
142 v, bprefix=bprefix, indent=indent, level=level
142 v, bprefix=bprefix, indent=indent, level=level
143 ):
143 ):
144 yield chunk
144 yield chunk
145
145
146 if i + 1 < len(o):
146 if i + 1 < len(o):
147 if indent:
147 if indent:
148 yield b',\n'
148 yield b',\n'
149 yield b' ' * (level * indent)
149 yield b' ' * (level * indent)
150 else:
150 else:
151 yield b', '
151 yield b', '
152
152
153 if indent:
153 if indent:
154 level -= 1
154 level -= 1
155 yield b'\n'
155 yield b'\n'
156 yield b' ' * (level * indent)
156 yield b' ' * (level * indent)
157
157
158 yield b'}'
158 yield b'}'
159 elif isinstance(o, set):
159 elif isinstance(o, set):
160 if not o:
160 if not o:
161 yield b'set([])'
161 yield b'set([])'
162 return
162 return
163
163
164 yield b'set(['
164 yield b'set(['
165
165
166 if indent:
166 if indent:
167 level += 1
167 level += 1
168 yield b'\n'
168 yield b'\n'
169 yield b' ' * (level * indent)
169 yield b' ' * (level * indent)
170
170
171 for i, k in enumerate(sorted(o)):
171 for i, k in enumerate(sorted(o)):
172 for chunk in pprintgen(
172 for chunk in pprintgen(
173 k, bprefix=bprefix, indent=indent, level=level
173 k, bprefix=bprefix, indent=indent, level=level
174 ):
174 ):
175 yield chunk
175 yield chunk
176
176
177 if i + 1 < len(o):
177 if i + 1 < len(o):
178 if indent:
178 if indent:
179 yield b',\n'
179 yield b',\n'
180 yield b' ' * (level * indent)
180 yield b' ' * (level * indent)
181 else:
181 else:
182 yield b', '
182 yield b', '
183
183
184 if indent:
184 if indent:
185 level -= 1
185 level -= 1
186 yield b'\n'
186 yield b'\n'
187 yield b' ' * (level * indent)
187 yield b' ' * (level * indent)
188
188
189 yield b'])'
189 yield b'])'
190 elif isinstance(o, tuple):
190 elif isinstance(o, tuple):
191 if not o:
191 if not o:
192 yield b'()'
192 yield b'()'
193 return
193 return
194
194
195 yield b'('
195 yield b'('
196
196
197 if indent:
197 if indent:
198 level += 1
198 level += 1
199 yield b'\n'
199 yield b'\n'
200 yield b' ' * (level * indent)
200 yield b' ' * (level * indent)
201
201
202 for i, a in enumerate(o):
202 for i, a in enumerate(o):
203 for chunk in pprintgen(
203 for chunk in pprintgen(
204 a, bprefix=bprefix, indent=indent, level=level
204 a, bprefix=bprefix, indent=indent, level=level
205 ):
205 ):
206 yield chunk
206 yield chunk
207
207
208 if i + 1 < len(o):
208 if i + 1 < len(o):
209 if indent:
209 if indent:
210 yield b',\n'
210 yield b',\n'
211 yield b' ' * (level * indent)
211 yield b' ' * (level * indent)
212 else:
212 else:
213 yield b', '
213 yield b', '
214
214
215 if indent:
215 if indent:
216 level -= 1
216 level -= 1
217 yield b'\n'
217 yield b'\n'
218 yield b' ' * (level * indent)
218 yield b' ' * (level * indent)
219
219
220 yield b')'
220 yield b')'
221 elif isinstance(o, types.GeneratorType):
221 elif isinstance(o, types.GeneratorType):
222 # Special case of empty generator.
222 # Special case of empty generator.
223 try:
223 try:
224 nextitem = next(o)
224 nextitem = next(o)
225 except StopIteration:
225 except StopIteration:
226 yield b'gen[]'
226 yield b'gen[]'
227 return
227 return
228
228
229 yield b'gen['
229 yield b'gen['
230
230
231 if indent:
231 if indent:
232 level += 1
232 level += 1
233 yield b'\n'
233 yield b'\n'
234 yield b' ' * (level * indent)
234 yield b' ' * (level * indent)
235
235
236 last = False
236 last = False
237
237
238 while not last:
238 while not last:
239 current = nextitem
239 current = nextitem
240
240
241 try:
241 try:
242 nextitem = next(o)
242 nextitem = next(o)
243 except StopIteration:
243 except StopIteration:
244 last = True
244 last = True
245
245
246 for chunk in pprintgen(
246 for chunk in pprintgen(
247 current, bprefix=bprefix, indent=indent, level=level
247 current, bprefix=bprefix, indent=indent, level=level
248 ):
248 ):
249 yield chunk
249 yield chunk
250
250
251 if not last:
251 if not last:
252 if indent:
252 if indent:
253 yield b',\n'
253 yield b',\n'
254 yield b' ' * (level * indent)
254 yield b' ' * (level * indent)
255 else:
255 else:
256 yield b', '
256 yield b', '
257
257
258 if indent:
258 if indent:
259 level -= 1
259 level -= 1
260 yield b'\n'
260 yield b'\n'
261 yield b' ' * (level * indent)
261 yield b' ' * (level * indent)
262
262
263 yield b']'
263 yield b']'
264 else:
264 else:
265 yield pycompat.byterepr(o)
265 yield pycompat.byterepr(o)
266
266
267
267
268 def prettyrepr(o) -> bytes:
268 def prettyrepr(o) -> bytes:
269 """Pretty print a representation of a possibly-nested object"""
269 """Pretty print a representation of a possibly-nested object"""
270 lines = []
270 lines = []
271 rs = pycompat.byterepr(o)
271 rs = pycompat.byterepr(o)
272 p0 = p1 = 0
272 p0 = p1 = 0
273 while p0 < len(rs):
273 while p0 < len(rs):
274 # '... field=<type ... field=<type ...'
274 # '... field=<type ... field=<type ...'
275 # ~~~~~~~~~~~~~~~~
275 # ~~~~~~~~~~~~~~~~
276 # p0 p1 q0 q1
276 # p0 p1 q0 q1
277 q0 = -1
277 q0 = -1
278 q1 = rs.find(b'<', p1 + 1)
278 q1 = rs.find(b'<', p1 + 1)
279 if q1 < 0:
279 if q1 < 0:
280 q1 = len(rs)
280 q1 = len(rs)
281 # pytype: disable=wrong-arg-count
281 # pytype: disable=wrong-arg-count
282 # TODO: figure out why pytype doesn't recognize the optional start
282 # TODO: figure out why pytype doesn't recognize the optional start
283 # arg
283 # arg
284 elif q1 > p1 + 1 and rs.startswith(b'=', q1 - 1):
284 elif q1 > p1 + 1 and rs.startswith(b'=', q1 - 1):
285 # pytype: enable=wrong-arg-count
285 # pytype: enable=wrong-arg-count
286 # backtrack for ' field=<'
286 # backtrack for ' field=<'
287 q0 = rs.rfind(b' ', p1 + 1, q1 - 1)
287 q0 = rs.rfind(b' ', p1 + 1, q1 - 1)
288 if q0 < 0:
288 if q0 < 0:
289 q0 = q1
289 q0 = q1
290 else:
290 else:
291 q0 += 1 # skip ' '
291 q0 += 1 # skip ' '
292 l = rs.count(b'<', 0, p0) - rs.count(b'>', 0, p0)
292 l = rs.count(b'<', 0, p0) - rs.count(b'>', 0, p0)
293 assert l >= 0
293 assert l >= 0
294 lines.append((l, rs[p0:q0].rstrip()))
294 lines.append((l, rs[p0:q0].rstrip()))
295 p0, p1 = q0, q1
295 p0, p1 = q0, q1
296 return b'\n'.join(b' ' * l + s for l, s in lines)
296 return b'\n'.join(b' ' * l + s for l, s in lines)
297
297
298
298
299 def buildrepr(r) -> bytes:
299 def buildrepr(r) -> bytes:
300 """Format an optional printable representation from unexpanded bits
300 """Format an optional printable representation from unexpanded bits
301
301
302 ======== =================================
302 ======== =================================
303 type(r) example
303 type(r) example
304 ======== =================================
304 ======== =================================
305 tuple ('<not %r>', other)
305 tuple ('<not %r>', other)
306 bytes '<branch closed>'
306 bytes '<branch closed>'
307 callable lambda: '<branch %r>' % sorted(b)
307 callable lambda: '<branch %r>' % sorted(b)
308 object other
308 object other
309 ======== =================================
309 ======== =================================
310 """
310 """
311 if r is None:
311 if r is None:
312 return b''
312 return b''
313 elif isinstance(r, tuple):
313 elif isinstance(r, tuple):
314 return r[0] % pycompat.rapply(pycompat.maybebytestr, r[1:])
314 return r[0] % pycompat.rapply(pycompat.maybebytestr, r[1:])
315 elif isinstance(r, bytes):
315 elif isinstance(r, bytes):
316 return r
316 return r
317 elif callable(r):
317 elif callable(r):
318 return r()
318 return r()
319 else:
319 else:
320 return pprint(r)
320 return pprint(r)
321
321
322
322
323 def binary(s: bytes) -> bool:
323 def binary(s: bytes) -> bool:
324 """return true if a string is binary data"""
324 """return true if a string is binary data"""
325 return bool(s and b'\0' in s)
325 return bool(s and b'\0' in s)
326
326
327
327
328 def _splitpattern(pattern: bytes):
328 def _splitpattern(pattern: bytes):
329 if pattern.startswith(b're:'):
329 if pattern.startswith(b're:'):
330 return b're', pattern[3:]
330 return b're', pattern[3:]
331 elif pattern.startswith(b'literal:'):
331 elif pattern.startswith(b'literal:'):
332 return b'literal', pattern[8:]
332 return b'literal', pattern[8:]
333 return b'literal', pattern
333 return b'literal', pattern
334
334
335
335
336 def stringmatcher(pattern: bytes, casesensitive: bool = True):
336 def stringmatcher(pattern: bytes, casesensitive: bool = True):
337 """
337 """
338 accepts a string, possibly starting with 're:' or 'literal:' prefix.
338 accepts a string, possibly starting with 're:' or 'literal:' prefix.
339 returns the matcher name, pattern, and matcher function.
339 returns the matcher name, pattern, and matcher function.
340 missing or unknown prefixes are treated as literal matches.
340 missing or unknown prefixes are treated as literal matches.
341
341
342 helper for tests:
342 helper for tests:
343 >>> def test(pattern, *tests):
343 >>> def test(pattern, *tests):
344 ... kind, pattern, matcher = stringmatcher(pattern)
344 ... kind, pattern, matcher = stringmatcher(pattern)
345 ... return (kind, pattern, [bool(matcher(t)) for t in tests])
345 ... return (kind, pattern, [bool(matcher(t)) for t in tests])
346 >>> def itest(pattern, *tests):
346 >>> def itest(pattern, *tests):
347 ... kind, pattern, matcher = stringmatcher(pattern, casesensitive=False)
347 ... kind, pattern, matcher = stringmatcher(pattern, casesensitive=False)
348 ... return (kind, pattern, [bool(matcher(t)) for t in tests])
348 ... return (kind, pattern, [bool(matcher(t)) for t in tests])
349
349
350 exact matching (no prefix):
350 exact matching (no prefix):
351 >>> test(b'abcdefg', b'abc', b'def', b'abcdefg')
351 >>> test(b'abcdefg', b'abc', b'def', b'abcdefg')
352 ('literal', 'abcdefg', [False, False, True])
352 ('literal', 'abcdefg', [False, False, True])
353
353
354 regex matching ('re:' prefix)
354 regex matching ('re:' prefix)
355 >>> test(b're:a.+b', b'nomatch', b'fooadef', b'fooadefbar')
355 >>> test(b're:a.+b', b'nomatch', b'fooadef', b'fooadefbar')
356 ('re', 'a.+b', [False, False, True])
356 ('re', 'a.+b', [False, False, True])
357
357
358 force exact matches ('literal:' prefix)
358 force exact matches ('literal:' prefix)
359 >>> test(b'literal:re:foobar', b'foobar', b're:foobar')
359 >>> test(b'literal:re:foobar', b'foobar', b're:foobar')
360 ('literal', 're:foobar', [False, True])
360 ('literal', 're:foobar', [False, True])
361
361
362 unknown prefixes are ignored and treated as literals
362 unknown prefixes are ignored and treated as literals
363 >>> test(b'foo:bar', b'foo', b'bar', b'foo:bar')
363 >>> test(b'foo:bar', b'foo', b'bar', b'foo:bar')
364 ('literal', 'foo:bar', [False, False, True])
364 ('literal', 'foo:bar', [False, False, True])
365
365
366 case insensitive regex matches
366 case insensitive regex matches
367 >>> itest(b're:A.+b', b'nomatch', b'fooadef', b'fooadefBar')
367 >>> itest(b're:A.+b', b'nomatch', b'fooadef', b'fooadefBar')
368 ('re', 'A.+b', [False, False, True])
368 ('re', 'A.+b', [False, False, True])
369
369
370 case insensitive literal matches
370 case insensitive literal matches
371 >>> itest(b'ABCDEFG', b'abc', b'def', b'abcdefg')
371 >>> itest(b'ABCDEFG', b'abc', b'def', b'abcdefg')
372 ('literal', 'ABCDEFG', [False, False, True])
372 ('literal', 'ABCDEFG', [False, False, True])
373 """
373 """
374 kind, pattern = _splitpattern(pattern)
374 kind, pattern = _splitpattern(pattern)
375 if kind == b're':
375 if kind == b're':
376 try:
376 try:
377 flags = 0
377 flags = 0
378 if not casesensitive:
378 if not casesensitive:
379 flags = remod.I
379 flags = remod.I
380 regex = remod.compile(pattern, flags)
380 regex = remod.compile(pattern, flags)
381 except remod.error as e:
381 except remod.error as e:
382 raise error.ParseError(
382 raise error.ParseError(
383 _(b'invalid regular expression: %s') % forcebytestr(e)
383 _(b'invalid regular expression: %s') % forcebytestr(e)
384 )
384 )
385 return kind, pattern, regex.search
385 return kind, pattern, regex.search
386 elif kind == b'literal':
386 elif kind == b'literal':
387 if casesensitive:
387 if casesensitive:
388 match = pattern.__eq__
388 match = pattern.__eq__
389 else:
389 else:
390 ipat = encoding.lower(pattern)
390 ipat = encoding.lower(pattern)
391 match = lambda s: ipat == encoding.lower(s)
391 match = lambda s: ipat == encoding.lower(s)
392 return kind, pattern, match
392 return kind, pattern, match
393
393
394 raise error.ProgrammingError(b'unhandled pattern kind: %s' % kind)
394 raise error.ProgrammingError(b'unhandled pattern kind: %s' % kind)
395
395
396
396
397 def substringregexp(pattern: bytes, flags: int = 0):
397 def substringregexp(pattern: bytes, flags: int = 0):
398 """Build a regexp object from a string pattern possibly starting with
398 """Build a regexp object from a string pattern possibly starting with
399 're:' or 'literal:' prefix.
399 're:' or 'literal:' prefix.
400
400
401 helper for tests:
401 helper for tests:
402 >>> def test(pattern, *tests):
402 >>> def test(pattern, *tests):
403 ... regexp = substringregexp(pattern)
403 ... regexp = substringregexp(pattern)
404 ... return [bool(regexp.search(t)) for t in tests]
404 ... return [bool(regexp.search(t)) for t in tests]
405 >>> def itest(pattern, *tests):
405 >>> def itest(pattern, *tests):
406 ... regexp = substringregexp(pattern, remod.I)
406 ... regexp = substringregexp(pattern, remod.I)
407 ... return [bool(regexp.search(t)) for t in tests]
407 ... return [bool(regexp.search(t)) for t in tests]
408
408
409 substring matching (no prefix):
409 substring matching (no prefix):
410 >>> test(b'bcde', b'abc', b'def', b'abcdefg')
410 >>> test(b'bcde', b'abc', b'def', b'abcdefg')
411 [False, False, True]
411 [False, False, True]
412
412
413 substring pattern should be escaped:
413 substring pattern should be escaped:
414 >>> substringregexp(b'.bc').pattern
414 >>> substringregexp(b'.bc').pattern
415 '\\\\.bc'
415 '\\\\.bc'
416 >>> test(b'.bc', b'abc', b'def', b'abcdefg')
416 >>> test(b'.bc', b'abc', b'def', b'abcdefg')
417 [False, False, False]
417 [False, False, False]
418
418
419 regex matching ('re:' prefix)
419 regex matching ('re:' prefix)
420 >>> test(b're:a.+b', b'nomatch', b'fooadef', b'fooadefbar')
420 >>> test(b're:a.+b', b'nomatch', b'fooadef', b'fooadefbar')
421 [False, False, True]
421 [False, False, True]
422
422
423 force substring matches ('literal:' prefix)
423 force substring matches ('literal:' prefix)
424 >>> test(b'literal:re:foobar', b'foobar', b're:foobar')
424 >>> test(b'literal:re:foobar', b'foobar', b're:foobar')
425 [False, True]
425 [False, True]
426
426
427 case insensitive literal matches
427 case insensitive literal matches
428 >>> itest(b'BCDE', b'abc', b'def', b'abcdefg')
428 >>> itest(b'BCDE', b'abc', b'def', b'abcdefg')
429 [False, False, True]
429 [False, False, True]
430
430
431 case insensitive regex matches
431 case insensitive regex matches
432 >>> itest(b're:A.+b', b'nomatch', b'fooadef', b'fooadefBar')
432 >>> itest(b're:A.+b', b'nomatch', b'fooadef', b'fooadefBar')
433 [False, False, True]
433 [False, False, True]
434 """
434 """
435 kind, pattern = _splitpattern(pattern)
435 kind, pattern = _splitpattern(pattern)
436 if kind == b're':
436 if kind == b're':
437 try:
437 try:
438 return remod.compile(pattern, flags)
438 return remod.compile(pattern, flags)
439 except remod.error as e:
439 except remod.error as e:
440 raise error.ParseError(
440 raise error.ParseError(
441 _(b'invalid regular expression: %s') % forcebytestr(e)
441 _(b'invalid regular expression: %s') % forcebytestr(e)
442 )
442 )
443 elif kind == b'literal':
443 elif kind == b'literal':
444 return remod.compile(remod.escape(pattern), flags)
444 return remod.compile(remod.escape(pattern), flags)
445
445
446 raise error.ProgrammingError(b'unhandled pattern kind: %s' % kind)
446 raise error.ProgrammingError(b'unhandled pattern kind: %s' % kind)
447
447
448
448
449 def shortuser(user: bytes) -> bytes:
449 def shortuser(user: bytes) -> bytes:
450 """Return a short representation of a user name or email address."""
450 """Return a short representation of a user name or email address."""
451 f = user.find(b'@')
451 f = user.find(b'@')
452 if f >= 0:
452 if f >= 0:
453 user = user[:f]
453 user = user[:f]
454 f = user.find(b'<')
454 f = user.find(b'<')
455 if f >= 0:
455 if f >= 0:
456 user = user[f + 1 :]
456 user = user[f + 1 :]
457 f = user.find(b' ')
457 f = user.find(b' ')
458 if f >= 0:
458 if f >= 0:
459 user = user[:f]
459 user = user[:f]
460 f = user.find(b'.')
460 f = user.find(b'.')
461 if f >= 0:
461 if f >= 0:
462 user = user[:f]
462 user = user[:f]
463 return user
463 return user
464
464
465
465
466 def emailuser(user: bytes) -> bytes:
466 def emailuser(user: bytes) -> bytes:
467 """Return the user portion of an email address."""
467 """Return the user portion of an email address."""
468 f = user.find(b'@')
468 f = user.find(b'@')
469 if f >= 0:
469 if f >= 0:
470 user = user[:f]
470 user = user[:f]
471 f = user.find(b'<')
471 f = user.find(b'<')
472 if f >= 0:
472 if f >= 0:
473 user = user[f + 1 :]
473 user = user[f + 1 :]
474 return user
474 return user
475
475
476
476
477 def email(author: bytes) -> bytes:
477 def email(author: bytes) -> bytes:
478 '''get email of author.'''
478 '''get email of author.'''
479 r = author.find(b'>')
479 r = author.find(b'>')
480 if r == -1:
480 if r == -1:
481 r = None
481 r = None
482 return author[author.find(b'<') + 1 : r]
482 return author[author.find(b'<') + 1 : r]
483
483
484
484
485 def person(author: bytes) -> bytes:
485 def person(author: bytes) -> bytes:
486 """Returns the name before an email address,
486 """Returns the name before an email address,
487 interpreting it as per RFC 5322
487 interpreting it as per RFC 5322
488
488
489 >>> person(b'foo@bar')
489 >>> person(b'foo@bar')
490 'foo'
490 'foo'
491 >>> person(b'Foo Bar <foo@bar>')
491 >>> person(b'Foo Bar <foo@bar>')
492 'Foo Bar'
492 'Foo Bar'
493 >>> person(b'"Foo Bar" <foo@bar>')
493 >>> person(b'"Foo Bar" <foo@bar>')
494 'Foo Bar'
494 'Foo Bar'
495 >>> person(b'"Foo \"buz\" Bar" <foo@bar>')
495 >>> person(b'"Foo \"buz\" Bar" <foo@bar>')
496 'Foo "buz" Bar'
496 'Foo "buz" Bar'
497 >>> # The following are invalid, but do exist in real-life
497 >>> # The following are invalid, but do exist in real-life
498 ...
498 ...
499 >>> person(b'Foo "buz" Bar <foo@bar>')
499 >>> person(b'Foo "buz" Bar <foo@bar>')
500 'Foo "buz" Bar'
500 'Foo "buz" Bar'
501 >>> person(b'"Foo Bar <foo@bar>')
501 >>> person(b'"Foo Bar <foo@bar>')
502 'Foo Bar'
502 'Foo Bar'
503 """
503 """
504 if b'@' not in author:
504 if b'@' not in author:
505 return author
505 return author
506 f = author.find(b'<')
506 f = author.find(b'<')
507 if f != -1:
507 if f != -1:
508 return author[:f].strip(b' "').replace(b'\\"', b'"')
508 return author[:f].strip(b' "').replace(b'\\"', b'"')
509 f = author.find(b'@')
509 f = author.find(b'@')
510 return author[:f].replace(b'.', b' ')
510 return author[:f].replace(b'.', b' ')
511
511
512
512
513 @attr.s(hash=True)
513 @attr.s(hash=True)
514 class mailmapping:
514 class mailmapping:
515 """Represents a username/email key or value in
515 """Represents a username/email key or value in
516 a mailmap file"""
516 a mailmap file"""
517
517
518 email = attr.ib()
518 email = attr.ib()
519 name = attr.ib(default=None)
519 name = attr.ib(default=None)
520
520
521
521
522 def _ismailmaplineinvalid(names, emails):
522 def _ismailmaplineinvalid(names, emails):
523 """Returns True if the parsed names and emails
523 """Returns True if the parsed names and emails
524 in a mailmap entry are invalid.
524 in a mailmap entry are invalid.
525
525
526 >>> # No names or emails fails
526 >>> # No names or emails fails
527 >>> names, emails = [], []
527 >>> names, emails = [], []
528 >>> _ismailmaplineinvalid(names, emails)
528 >>> _ismailmaplineinvalid(names, emails)
529 True
529 True
530 >>> # Only one email fails
530 >>> # Only one email fails
531 >>> emails = [b'email@email.com']
531 >>> emails = [b'email@email.com']
532 >>> _ismailmaplineinvalid(names, emails)
532 >>> _ismailmaplineinvalid(names, emails)
533 True
533 True
534 >>> # One email and one name passes
534 >>> # One email and one name passes
535 >>> names = [b'Test Name']
535 >>> names = [b'Test Name']
536 >>> _ismailmaplineinvalid(names, emails)
536 >>> _ismailmaplineinvalid(names, emails)
537 False
537 False
538 >>> # No names but two emails passes
538 >>> # No names but two emails passes
539 >>> names = []
539 >>> names = []
540 >>> emails = [b'proper@email.com', b'commit@email.com']
540 >>> emails = [b'proper@email.com', b'commit@email.com']
541 >>> _ismailmaplineinvalid(names, emails)
541 >>> _ismailmaplineinvalid(names, emails)
542 False
542 False
543 """
543 """
544 return not emails or not names and len(emails) < 2
544 return not emails or not names and len(emails) < 2
545
545
546
546
547 def parsemailmap(mailmapcontent):
547 def parsemailmap(mailmapcontent):
548 """Parses data in the .mailmap format
548 """Parses data in the .mailmap format
549
549
550 >>> mmdata = b"\\n".join([
550 >>> mmdata = b"\\n".join([
551 ... b'# Comment',
551 ... b'# Comment',
552 ... b'Name <commit1@email.xx>',
552 ... b'Name <commit1@email.xx>',
553 ... b'<name@email.xx> <commit2@email.xx>',
553 ... b'<name@email.xx> <commit2@email.xx>',
554 ... b'Name <proper@email.xx> <commit3@email.xx>',
554 ... b'Name <proper@email.xx> <commit3@email.xx>',
555 ... b'Name <proper@email.xx> Commit <commit4@email.xx>',
555 ... b'Name <proper@email.xx> Commit <commit4@email.xx>',
556 ... ])
556 ... ])
557 >>> mm = parsemailmap(mmdata)
557 >>> mm = parsemailmap(mmdata)
558 >>> for key in sorted(mm.keys()):
558 >>> for key in sorted(mm.keys()):
559 ... print(key)
559 ... print(key)
560 mailmapping(email='commit1@email.xx', name=None)
560 mailmapping(email='commit1@email.xx', name=None)
561 mailmapping(email='commit2@email.xx', name=None)
561 mailmapping(email='commit2@email.xx', name=None)
562 mailmapping(email='commit3@email.xx', name=None)
562 mailmapping(email='commit3@email.xx', name=None)
563 mailmapping(email='commit4@email.xx', name='Commit')
563 mailmapping(email='commit4@email.xx', name='Commit')
564 >>> for val in sorted(mm.values()):
564 >>> for val in sorted(mm.values()):
565 ... print(val)
565 ... print(val)
566 mailmapping(email='commit1@email.xx', name='Name')
566 mailmapping(email='commit1@email.xx', name='Name')
567 mailmapping(email='name@email.xx', name=None)
567 mailmapping(email='name@email.xx', name=None)
568 mailmapping(email='proper@email.xx', name='Name')
568 mailmapping(email='proper@email.xx', name='Name')
569 mailmapping(email='proper@email.xx', name='Name')
569 mailmapping(email='proper@email.xx', name='Name')
570 """
570 """
571 mailmap = {}
571 mailmap = {}
572
572
573 if mailmapcontent is None:
573 if mailmapcontent is None:
574 return mailmap
574 return mailmap
575
575
576 for line in mailmapcontent.splitlines():
576 for line in mailmapcontent.splitlines():
577
577
578 # Don't bother checking the line if it is a comment or
578 # Don't bother checking the line if it is a comment or
579 # is an improperly formed author field
579 # is an improperly formed author field
580 if line.lstrip().startswith(b'#'):
580 if line.lstrip().startswith(b'#'):
581 continue
581 continue
582
582
583 # names, emails hold the parsed emails and names for each line
583 # names, emails hold the parsed emails and names for each line
584 # name_builder holds the words in a persons name
584 # name_builder holds the words in a persons name
585 names, emails = [], []
585 names, emails = [], []
586 namebuilder = []
586 namebuilder = []
587
587
588 for element in line.split():
588 for element in line.split():
589 if element.startswith(b'#'):
589 if element.startswith(b'#'):
590 # If we reach a comment in the mailmap file, move on
590 # If we reach a comment in the mailmap file, move on
591 break
591 break
592
592
593 elif element.startswith(b'<') and element.endswith(b'>'):
593 elif element.startswith(b'<') and element.endswith(b'>'):
594 # We have found an email.
594 # We have found an email.
595 # Parse it, and finalize any names from earlier
595 # Parse it, and finalize any names from earlier
596 emails.append(element[1:-1]) # Slice off the "<>"
596 emails.append(element[1:-1]) # Slice off the "<>"
597
597
598 if namebuilder:
598 if namebuilder:
599 names.append(b' '.join(namebuilder))
599 names.append(b' '.join(namebuilder))
600 namebuilder = []
600 namebuilder = []
601
601
602 # Break if we have found a second email, any other
602 # Break if we have found a second email, any other
603 # data does not fit the spec for .mailmap
603 # data does not fit the spec for .mailmap
604 if len(emails) > 1:
604 if len(emails) > 1:
605 break
605 break
606
606
607 else:
607 else:
608 # We have found another word in the committers name
608 # We have found another word in the committers name
609 namebuilder.append(element)
609 namebuilder.append(element)
610
610
611 # Check to see if we have parsed the line into a valid form
611 # Check to see if we have parsed the line into a valid form
612 # We require at least one email, and either at least one
612 # We require at least one email, and either at least one
613 # name or a second email
613 # name or a second email
614 if _ismailmaplineinvalid(names, emails):
614 if _ismailmaplineinvalid(names, emails):
615 continue
615 continue
616
616
617 mailmapkey = mailmapping(
617 mailmapkey = mailmapping(
618 email=emails[-1],
618 email=emails[-1],
619 name=names[-1] if len(names) == 2 else None,
619 name=names[-1] if len(names) == 2 else None,
620 )
620 )
621
621
622 mailmap[mailmapkey] = mailmapping(
622 mailmap[mailmapkey] = mailmapping(
623 email=emails[0],
623 email=emails[0],
624 name=names[0] if names else None,
624 name=names[0] if names else None,
625 )
625 )
626
626
627 return mailmap
627 return mailmap
628
628
629
629
630 def mapname(mailmap, author: bytes) -> bytes:
630 def mapname(mailmap, author: bytes) -> bytes:
631 """Returns the author field according to the mailmap cache, or
631 """Returns the author field according to the mailmap cache, or
632 the original author field.
632 the original author field.
633
633
634 >>> mmdata = b"\\n".join([
634 >>> mmdata = b"\\n".join([
635 ... b'# Comment',
635 ... b'# Comment',
636 ... b'Name <commit1@email.xx>',
636 ... b'Name <commit1@email.xx>',
637 ... b'<name@email.xx> <commit2@email.xx>',
637 ... b'<name@email.xx> <commit2@email.xx>',
638 ... b'Name <proper@email.xx> <commit3@email.xx>',
638 ... b'Name <proper@email.xx> <commit3@email.xx>',
639 ... b'Name <proper@email.xx> Commit <commit4@email.xx>',
639 ... b'Name <proper@email.xx> Commit <commit4@email.xx>',
640 ... ])
640 ... ])
641 >>> m = parsemailmap(mmdata)
641 >>> m = parsemailmap(mmdata)
642 >>> mapname(m, b'Commit <commit1@email.xx>')
642 >>> mapname(m, b'Commit <commit1@email.xx>')
643 'Name <commit1@email.xx>'
643 'Name <commit1@email.xx>'
644 >>> mapname(m, b'Name <commit2@email.xx>')
644 >>> mapname(m, b'Name <commit2@email.xx>')
645 'Name <name@email.xx>'
645 'Name <name@email.xx>'
646 >>> mapname(m, b'Commit <commit3@email.xx>')
646 >>> mapname(m, b'Commit <commit3@email.xx>')
647 'Name <proper@email.xx>'
647 'Name <proper@email.xx>'
648 >>> mapname(m, b'Commit <commit4@email.xx>')
648 >>> mapname(m, b'Commit <commit4@email.xx>')
649 'Name <proper@email.xx>'
649 'Name <proper@email.xx>'
650 >>> mapname(m, b'Unknown Name <unknown@email.com>')
650 >>> mapname(m, b'Unknown Name <unknown@email.com>')
651 'Unknown Name <unknown@email.com>'
651 'Unknown Name <unknown@email.com>'
652 """
652 """
653 # If the author field coming in isn't in the correct format,
653 # If the author field coming in isn't in the correct format,
654 # or the mailmap is empty just return the original author field
654 # or the mailmap is empty just return the original author field
655 if not isauthorwellformed(author) or not mailmap:
655 if not isauthorwellformed(author) or not mailmap:
656 return author
656 return author
657
657
658 # Turn the user name into a mailmapping
658 # Turn the user name into a mailmapping
659 commit = mailmapping(name=person(author), email=email(author))
659 commit = mailmapping(name=person(author), email=email(author))
660
660
661 try:
661 try:
662 # Try and use both the commit email and name as the key
662 # Try and use both the commit email and name as the key
663 proper = mailmap[commit]
663 proper = mailmap[commit]
664
664
665 except KeyError:
665 except KeyError:
666 # If the lookup fails, use just the email as the key instead
666 # If the lookup fails, use just the email as the key instead
667 # We call this commit2 as not to erase original commit fields
667 # We call this commit2 as not to erase original commit fields
668 commit2 = mailmapping(email=commit.email)
668 commit2 = mailmapping(email=commit.email)
669 proper = mailmap.get(commit2, mailmapping(None, None))
669 proper = mailmap.get(commit2, mailmapping(None, None))
670
670
671 # Return the author field with proper values filled in
671 # Return the author field with proper values filled in
672 return b'%s <%s>' % (
672 return b'%s <%s>' % (
673 proper.name if proper.name else commit.name,
673 proper.name if proper.name else commit.name,
674 proper.email if proper.email else commit.email,
674 proper.email if proper.email else commit.email,
675 )
675 )
676
676
677
677
678 _correctauthorformat = remod.compile(br'^[^<]+\s<[^<>]+@[^<>]+>$')
678 _correctauthorformat = remod.compile(br'^[^<]+\s<[^<>]+@[^<>]+>$')
679
679
680
680
681 def isauthorwellformed(author: bytes) -> bool:
681 def isauthorwellformed(author: bytes) -> bool:
682 """Return True if the author field is well formed
682 """Return True if the author field is well formed
683 (ie "Contributor Name <contrib@email.dom>")
683 (ie "Contributor Name <contrib@email.dom>")
684
684
685 >>> isauthorwellformed(b'Good Author <good@author.com>')
685 >>> isauthorwellformed(b'Good Author <good@author.com>')
686 True
686 True
687 >>> isauthorwellformed(b'Author <good@author.com>')
687 >>> isauthorwellformed(b'Author <good@author.com>')
688 True
688 True
689 >>> isauthorwellformed(b'Bad Author')
689 >>> isauthorwellformed(b'Bad Author')
690 False
690 False
691 >>> isauthorwellformed(b'Bad Author <author@author.com')
691 >>> isauthorwellformed(b'Bad Author <author@author.com')
692 False
692 False
693 >>> isauthorwellformed(b'Bad Author author@author.com')
693 >>> isauthorwellformed(b'Bad Author author@author.com')
694 False
694 False
695 >>> isauthorwellformed(b'<author@author.com>')
695 >>> isauthorwellformed(b'<author@author.com>')
696 False
696 False
697 >>> isauthorwellformed(b'Bad Author <author>')
697 >>> isauthorwellformed(b'Bad Author <author>')
698 False
698 False
699 """
699 """
700 return _correctauthorformat.match(author) is not None
700 return _correctauthorformat.match(author) is not None
701
701
702
702
703 def firstline(text: bytes) -> bytes:
703 def firstline(text: bytes) -> bytes:
704 """Return the first line of the input"""
704 """Return the first line of the input"""
705 # Try to avoid running splitlines() on the whole string
705 # Try to avoid running splitlines() on the whole string
706 i = text.find(b'\n')
706 i = text.find(b'\n')
707 if i != -1:
707 if i != -1:
708 text = text[:i]
708 text = text[:i]
709 try:
709 try:
710 return text.splitlines()[0]
710 return text.splitlines()[0]
711 except IndexError:
711 except IndexError:
712 return b''
712 return b''
713
713
714
714
715 def ellipsis(text: bytes, maxlength: int = 400) -> bytes:
715 def ellipsis(text: bytes, maxlength: int = 400) -> bytes:
716 """Trim string to at most maxlength (default: 400) columns in display."""
716 """Trim string to at most maxlength (default: 400) columns in display."""
717 return encoding.trim(text, maxlength, ellipsis=b'...')
717 return encoding.trim(text, maxlength, ellipsis=b'...')
718
718
719
719
720 def escapestr(s: bytes) -> bytes:
720 def escapestr(s: bytes) -> bytes:
721 # "bytes" is also a typing shortcut for bytes, bytearray, and memoryview
721 # "bytes" is also a typing shortcut for bytes, bytearray, and memoryview
722 if isinstance(s, memoryview):
722 if isinstance(s, memoryview):
723 s = bytes(s)
723 s = bytes(s)
724 # call underlying function of s.encode('string_escape') directly for
724 # call underlying function of s.encode('string_escape') directly for
725 # Python 3 compatibility
725 # Python 3 compatibility
726 # pytype: disable=bad-return-type
726 return codecs.escape_encode(s)[0] # pytype: disable=module-attr
727 return codecs.escape_encode(s)[0] # pytype: disable=module-attr
728 # pytype: enable=bad-return-type
727
729
728
730
729 def unescapestr(s: bytes) -> bytes:
731 def unescapestr(s: bytes) -> bytes:
732 # pytype: disable=bad-return-type
730 return codecs.escape_decode(s)[0] # pytype: disable=module-attr
733 return codecs.escape_decode(s)[0] # pytype: disable=module-attr
734 # pytype: enable=bad-return-type
731
735
732
736
733 def forcebytestr(obj):
737 def forcebytestr(obj):
734 """Portably format an arbitrary object (e.g. exception) into a byte
738 """Portably format an arbitrary object (e.g. exception) into a byte
735 string."""
739 string."""
736 try:
740 try:
737 return pycompat.bytestr(obj)
741 return pycompat.bytestr(obj)
738 except UnicodeEncodeError:
742 except UnicodeEncodeError:
739 # non-ascii string, may be lossy
743 # non-ascii string, may be lossy
740 return pycompat.bytestr(encoding.strtolocal(str(obj)))
744 return pycompat.bytestr(encoding.strtolocal(str(obj)))
741
745
742
746
743 def uirepr(s: bytes) -> bytes:
747 def uirepr(s: bytes) -> bytes:
744 # Avoid double backslash in Windows path repr()
748 # Avoid double backslash in Windows path repr()
745 return pycompat.byterepr(pycompat.bytestr(s)).replace(b'\\\\', b'\\')
749 return pycompat.byterepr(pycompat.bytestr(s)).replace(b'\\\\', b'\\')
746
750
747
751
748 # delay import of textwrap
752 # delay import of textwrap
749 def _MBTextWrapper(**kwargs):
753 def _MBTextWrapper(**kwargs):
750 class tw(textwrap.TextWrapper):
754 class tw(textwrap.TextWrapper):
751 """
755 """
752 Extend TextWrapper for width-awareness.
756 Extend TextWrapper for width-awareness.
753
757
754 Neither number of 'bytes' in any encoding nor 'characters' is
758 Neither number of 'bytes' in any encoding nor 'characters' is
755 appropriate to calculate terminal columns for specified string.
759 appropriate to calculate terminal columns for specified string.
756
760
757 Original TextWrapper implementation uses built-in 'len()' directly,
761 Original TextWrapper implementation uses built-in 'len()' directly,
758 so overriding is needed to use width information of each characters.
762 so overriding is needed to use width information of each characters.
759
763
760 In addition, characters classified into 'ambiguous' width are
764 In addition, characters classified into 'ambiguous' width are
761 treated as wide in East Asian area, but as narrow in other.
765 treated as wide in East Asian area, but as narrow in other.
762
766
763 This requires use decision to determine width of such characters.
767 This requires use decision to determine width of such characters.
764 """
768 """
765
769
766 def _cutdown(self, ucstr, space_left):
770 def _cutdown(self, ucstr, space_left):
767 l = 0
771 l = 0
768 colwidth = encoding.ucolwidth
772 colwidth = encoding.ucolwidth
769 for i in range(len(ucstr)):
773 for i in range(len(ucstr)):
770 l += colwidth(ucstr[i])
774 l += colwidth(ucstr[i])
771 if space_left < l:
775 if space_left < l:
772 return (ucstr[:i], ucstr[i:])
776 return (ucstr[:i], ucstr[i:])
773 return ucstr, b''
777 return ucstr, b''
774
778
775 # overriding of base class
779 # overriding of base class
776 def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width):
780 def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width):
777 space_left = max(width - cur_len, 1)
781 space_left = max(width - cur_len, 1)
778
782
779 if self.break_long_words:
783 if self.break_long_words:
780 cut, res = self._cutdown(reversed_chunks[-1], space_left)
784 cut, res = self._cutdown(reversed_chunks[-1], space_left)
781 cur_line.append(cut)
785 cur_line.append(cut)
782 reversed_chunks[-1] = res
786 reversed_chunks[-1] = res
783 elif not cur_line:
787 elif not cur_line:
784 cur_line.append(reversed_chunks.pop())
788 cur_line.append(reversed_chunks.pop())
785
789
786 # this overriding code is imported from TextWrapper of Python 2.6
790 # this overriding code is imported from TextWrapper of Python 2.6
787 # to calculate columns of string by 'encoding.ucolwidth()'
791 # to calculate columns of string by 'encoding.ucolwidth()'
788 def _wrap_chunks(self, chunks):
792 def _wrap_chunks(self, chunks):
789 colwidth = encoding.ucolwidth
793 colwidth = encoding.ucolwidth
790
794
791 lines = []
795 lines = []
792 if self.width <= 0:
796 if self.width <= 0:
793 raise ValueError(b"invalid width %r (must be > 0)" % self.width)
797 raise ValueError(b"invalid width %r (must be > 0)" % self.width)
794
798
795 # Arrange in reverse order so items can be efficiently popped
799 # Arrange in reverse order so items can be efficiently popped
796 # from a stack of chucks.
800 # from a stack of chucks.
797 chunks.reverse()
801 chunks.reverse()
798
802
799 while chunks:
803 while chunks:
800
804
801 # Start the list of chunks that will make up the current line.
805 # Start the list of chunks that will make up the current line.
802 # cur_len is just the length of all the chunks in cur_line.
806 # cur_len is just the length of all the chunks in cur_line.
803 cur_line = []
807 cur_line = []
804 cur_len = 0
808 cur_len = 0
805
809
806 # Figure out which static string will prefix this line.
810 # Figure out which static string will prefix this line.
807 if lines:
811 if lines:
808 indent = self.subsequent_indent
812 indent = self.subsequent_indent
809 else:
813 else:
810 indent = self.initial_indent
814 indent = self.initial_indent
811
815
812 # Maximum width for this line.
816 # Maximum width for this line.
813 width = self.width - len(indent)
817 width = self.width - len(indent)
814
818
815 # First chunk on line is whitespace -- drop it, unless this
819 # First chunk on line is whitespace -- drop it, unless this
816 # is the very beginning of the text (i.e. no lines started yet).
820 # is the very beginning of the text (i.e. no lines started yet).
817 if self.drop_whitespace and chunks[-1].strip() == '' and lines:
821 if self.drop_whitespace and chunks[-1].strip() == '' and lines:
818 del chunks[-1]
822 del chunks[-1]
819
823
820 while chunks:
824 while chunks:
821 l = colwidth(chunks[-1])
825 l = colwidth(chunks[-1])
822
826
823 # Can at least squeeze this chunk onto the current line.
827 # Can at least squeeze this chunk onto the current line.
824 if cur_len + l <= width:
828 if cur_len + l <= width:
825 cur_line.append(chunks.pop())
829 cur_line.append(chunks.pop())
826 cur_len += l
830 cur_len += l
827
831
828 # Nope, this line is full.
832 # Nope, this line is full.
829 else:
833 else:
830 break
834 break
831
835
832 # The current line is full, and the next chunk is too big to
836 # The current line is full, and the next chunk is too big to
833 # fit on *any* line (not just this one).
837 # fit on *any* line (not just this one).
834 if chunks and colwidth(chunks[-1]) > width:
838 if chunks and colwidth(chunks[-1]) > width:
835 self._handle_long_word(chunks, cur_line, cur_len, width)
839 self._handle_long_word(chunks, cur_line, cur_len, width)
836
840
837 # If the last chunk on this line is all whitespace, drop it.
841 # If the last chunk on this line is all whitespace, drop it.
838 if (
842 if (
839 self.drop_whitespace
843 self.drop_whitespace
840 and cur_line
844 and cur_line
841 and cur_line[-1].strip() == r''
845 and cur_line[-1].strip() == r''
842 ):
846 ):
843 del cur_line[-1]
847 del cur_line[-1]
844
848
845 # Convert current line back to a string and store it in list
849 # Convert current line back to a string and store it in list
846 # of all lines (return value).
850 # of all lines (return value).
847 if cur_line:
851 if cur_line:
848 lines.append(indent + ''.join(cur_line))
852 lines.append(indent + ''.join(cur_line))
849
853
850 return lines
854 return lines
851
855
852 global _MBTextWrapper
856 global _MBTextWrapper
853 _MBTextWrapper = tw
857 _MBTextWrapper = tw
854 return tw(**kwargs)
858 return tw(**kwargs)
855
859
856
860
857 def wrap(
861 def wrap(
858 line: bytes, width: int, initindent: bytes = b'', hangindent: bytes = b''
862 line: bytes, width: int, initindent: bytes = b'', hangindent: bytes = b''
859 ) -> bytes:
863 ) -> bytes:
860 maxindent = max(len(hangindent), len(initindent))
864 maxindent = max(len(hangindent), len(initindent))
861 if width <= maxindent:
865 if width <= maxindent:
862 # adjust for weird terminal size
866 # adjust for weird terminal size
863 width = max(78, maxindent + 1)
867 width = max(78, maxindent + 1)
864 line = line.decode(
868 line = line.decode(
865 pycompat.sysstr(encoding.encoding),
869 pycompat.sysstr(encoding.encoding),
866 pycompat.sysstr(encoding.encodingmode),
870 pycompat.sysstr(encoding.encodingmode),
867 )
871 )
868 initindent = initindent.decode(
872 initindent = initindent.decode(
869 pycompat.sysstr(encoding.encoding),
873 pycompat.sysstr(encoding.encoding),
870 pycompat.sysstr(encoding.encodingmode),
874 pycompat.sysstr(encoding.encodingmode),
871 )
875 )
872 hangindent = hangindent.decode(
876 hangindent = hangindent.decode(
873 pycompat.sysstr(encoding.encoding),
877 pycompat.sysstr(encoding.encoding),
874 pycompat.sysstr(encoding.encodingmode),
878 pycompat.sysstr(encoding.encodingmode),
875 )
879 )
876 wrapper = _MBTextWrapper(
880 wrapper = _MBTextWrapper(
877 width=width, initial_indent=initindent, subsequent_indent=hangindent
881 width=width, initial_indent=initindent, subsequent_indent=hangindent
878 )
882 )
879 return wrapper.fill(line).encode(pycompat.sysstr(encoding.encoding))
883 return wrapper.fill(line).encode(pycompat.sysstr(encoding.encoding))
880
884
881
885
882 _booleans = {
886 _booleans = {
883 b'1': True,
887 b'1': True,
884 b'yes': True,
888 b'yes': True,
885 b'true': True,
889 b'true': True,
886 b'on': True,
890 b'on': True,
887 b'always': True,
891 b'always': True,
888 b'0': False,
892 b'0': False,
889 b'no': False,
893 b'no': False,
890 b'false': False,
894 b'false': False,
891 b'off': False,
895 b'off': False,
892 b'never': False,
896 b'never': False,
893 }
897 }
894
898
895
899
896 def parsebool(s: bytes) -> Optional[bool]:
900 def parsebool(s: bytes) -> Optional[bool]:
897 """Parse s into a boolean.
901 """Parse s into a boolean.
898
902
899 If s is not a valid boolean, returns None.
903 If s is not a valid boolean, returns None.
900 """
904 """
901 return _booleans.get(s.lower(), None)
905 return _booleans.get(s.lower(), None)
902
906
903
907
904 # TODO: make arg mandatory (and fix code below?)
908 # TODO: make arg mandatory (and fix code below?)
905 def parselist(value: Optional[bytes]):
909 def parselist(value: Optional[bytes]):
906 """parse a configuration value as a list of comma/space separated strings
910 """parse a configuration value as a list of comma/space separated strings
907
911
908 >>> parselist(b'this,is "a small" ,test')
912 >>> parselist(b'this,is "a small" ,test')
909 ['this', 'is', 'a small', 'test']
913 ['this', 'is', 'a small', 'test']
910 """
914 """
911
915
912 def _parse_plain(parts, s, offset):
916 def _parse_plain(parts, s, offset):
913 whitespace = False
917 whitespace = False
914 while offset < len(s) and (
918 while offset < len(s) and (
915 s[offset : offset + 1].isspace() or s[offset : offset + 1] == b','
919 s[offset : offset + 1].isspace() or s[offset : offset + 1] == b','
916 ):
920 ):
917 whitespace = True
921 whitespace = True
918 offset += 1
922 offset += 1
919 if offset >= len(s):
923 if offset >= len(s):
920 return None, parts, offset
924 return None, parts, offset
921 if whitespace:
925 if whitespace:
922 parts.append(b'')
926 parts.append(b'')
923 if s[offset : offset + 1] == b'"' and not parts[-1]:
927 if s[offset : offset + 1] == b'"' and not parts[-1]:
924 return _parse_quote, parts, offset + 1
928 return _parse_quote, parts, offset + 1
925 elif s[offset : offset + 1] == b'"' and parts[-1][-1:] == b'\\':
929 elif s[offset : offset + 1] == b'"' and parts[-1][-1:] == b'\\':
926 parts[-1] = parts[-1][:-1] + s[offset : offset + 1]
930 parts[-1] = parts[-1][:-1] + s[offset : offset + 1]
927 return _parse_plain, parts, offset + 1
931 return _parse_plain, parts, offset + 1
928 parts[-1] += s[offset : offset + 1]
932 parts[-1] += s[offset : offset + 1]
929 return _parse_plain, parts, offset + 1
933 return _parse_plain, parts, offset + 1
930
934
931 def _parse_quote(parts, s, offset):
935 def _parse_quote(parts, s, offset):
932 if offset < len(s) and s[offset : offset + 1] == b'"': # ""
936 if offset < len(s) and s[offset : offset + 1] == b'"': # ""
933 parts.append(b'')
937 parts.append(b'')
934 offset += 1
938 offset += 1
935 while offset < len(s) and (
939 while offset < len(s) and (
936 s[offset : offset + 1].isspace()
940 s[offset : offset + 1].isspace()
937 or s[offset : offset + 1] == b','
941 or s[offset : offset + 1] == b','
938 ):
942 ):
939 offset += 1
943 offset += 1
940 return _parse_plain, parts, offset
944 return _parse_plain, parts, offset
941
945
942 while offset < len(s) and s[offset : offset + 1] != b'"':
946 while offset < len(s) and s[offset : offset + 1] != b'"':
943 if (
947 if (
944 s[offset : offset + 1] == b'\\'
948 s[offset : offset + 1] == b'\\'
945 and offset + 1 < len(s)
949 and offset + 1 < len(s)
946 and s[offset + 1 : offset + 2] == b'"'
950 and s[offset + 1 : offset + 2] == b'"'
947 ):
951 ):
948 offset += 1
952 offset += 1
949 parts[-1] += b'"'
953 parts[-1] += b'"'
950 else:
954 else:
951 parts[-1] += s[offset : offset + 1]
955 parts[-1] += s[offset : offset + 1]
952 offset += 1
956 offset += 1
953
957
954 if offset >= len(s):
958 if offset >= len(s):
955 real_parts = _configlist(parts[-1])
959 real_parts = _configlist(parts[-1])
956 if not real_parts:
960 if not real_parts:
957 parts[-1] = b'"'
961 parts[-1] = b'"'
958 else:
962 else:
959 real_parts[0] = b'"' + real_parts[0]
963 real_parts[0] = b'"' + real_parts[0]
960 parts = parts[:-1]
964 parts = parts[:-1]
961 parts.extend(real_parts)
965 parts.extend(real_parts)
962 return None, parts, offset
966 return None, parts, offset
963
967
964 offset += 1
968 offset += 1
965 while offset < len(s) and s[offset : offset + 1] in [b' ', b',']:
969 while offset < len(s) and s[offset : offset + 1] in [b' ', b',']:
966 offset += 1
970 offset += 1
967
971
968 if offset < len(s):
972 if offset < len(s):
969 if offset + 1 == len(s) and s[offset : offset + 1] == b'"':
973 if offset + 1 == len(s) and s[offset : offset + 1] == b'"':
970 parts[-1] += b'"'
974 parts[-1] += b'"'
971 offset += 1
975 offset += 1
972 else:
976 else:
973 parts.append(b'')
977 parts.append(b'')
974 else:
978 else:
975 return None, parts, offset
979 return None, parts, offset
976
980
977 return _parse_plain, parts, offset
981 return _parse_plain, parts, offset
978
982
979 def _configlist(s):
983 def _configlist(s):
980 s = s.rstrip(b' ,')
984 s = s.rstrip(b' ,')
981 if not s:
985 if not s:
982 return []
986 return []
983 parser, parts, offset = _parse_plain, [b''], 0
987 parser, parts, offset = _parse_plain, [b''], 0
984 while parser:
988 while parser:
985 parser, parts, offset = parser(parts, s, offset)
989 parser, parts, offset = parser(parts, s, offset)
986 return parts
990 return parts
987
991
988 if value is not None and isinstance(value, bytes):
992 if value is not None and isinstance(value, bytes):
989 result = _configlist(value.lstrip(b' ,\n'))
993 result = _configlist(value.lstrip(b' ,\n'))
990 else:
994 else:
991 result = value
995 result = value
992 return result or []
996 return result or []
993
997
994
998
995 def evalpythonliteral(s: bytes):
999 def evalpythonliteral(s: bytes):
996 """Evaluate a string containing a Python literal expression"""
1000 """Evaluate a string containing a Python literal expression"""
997 # We could backport our tokenizer hack to rewrite '' to u'' if we want
1001 # We could backport our tokenizer hack to rewrite '' to u'' if we want
998 return ast.literal_eval(s.decode('latin1'))
1002 return ast.literal_eval(s.decode('latin1'))
General Comments 0
You need to be logged in to leave comments. Login now