##// END OF EJS Templates
cleanup: use modern @property/@foo.setter property specification...
Augie Fackler -
r27879:52a4ad62 default
parent child Browse files
Show More
@@ -1,1552 +1,1554 b''
1 1 # bundle2.py - generic container format to transmit arbitrary data.
2 2 #
3 3 # Copyright 2013 Facebook, Inc.
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7 """Handling of the new bundle2 format
8 8
9 9 The goal of bundle2 is to act as an atomically packet to transmit a set of
10 10 payloads in an application agnostic way. It consist in a sequence of "parts"
11 11 that will be handed to and processed by the application layer.
12 12
13 13
14 14 General format architecture
15 15 ===========================
16 16
17 17 The format is architectured as follow
18 18
19 19 - magic string
20 20 - stream level parameters
21 21 - payload parts (any number)
22 22 - end of stream marker.
23 23
24 24 the Binary format
25 25 ============================
26 26
27 27 All numbers are unsigned and big-endian.
28 28
29 29 stream level parameters
30 30 ------------------------
31 31
32 32 Binary format is as follow
33 33
34 34 :params size: int32
35 35
36 36 The total number of Bytes used by the parameters
37 37
38 38 :params value: arbitrary number of Bytes
39 39
40 40 A blob of `params size` containing the serialized version of all stream level
41 41 parameters.
42 42
43 43 The blob contains a space separated list of parameters. Parameters with value
44 44 are stored in the form `<name>=<value>`. Both name and value are urlquoted.
45 45
46 46 Empty name are obviously forbidden.
47 47
48 48 Name MUST start with a letter. If this first letter is lower case, the
49 49 parameter is advisory and can be safely ignored. However when the first
50 50 letter is capital, the parameter is mandatory and the bundling process MUST
51 51 stop if he is not able to proceed it.
52 52
53 53 Stream parameters use a simple textual format for two main reasons:
54 54
55 55 - Stream level parameters should remain simple and we want to discourage any
56 56 crazy usage.
57 57 - Textual data allow easy human inspection of a bundle2 header in case of
58 58 troubles.
59 59
60 60 Any Applicative level options MUST go into a bundle2 part instead.
61 61
62 62 Payload part
63 63 ------------------------
64 64
65 65 Binary format is as follow
66 66
67 67 :header size: int32
68 68
69 69 The total number of Bytes used by the part header. When the header is empty
70 70 (size = 0) this is interpreted as the end of stream marker.
71 71
72 72 :header:
73 73
74 74 The header defines how to interpret the part. It contains two piece of
75 75 data: the part type, and the part parameters.
76 76
77 77 The part type is used to route an application level handler, that can
78 78 interpret payload.
79 79
80 80 Part parameters are passed to the application level handler. They are
81 81 meant to convey information that will help the application level object to
82 82 interpret the part payload.
83 83
84 84 The binary format of the header is has follow
85 85
86 86 :typesize: (one byte)
87 87
88 88 :parttype: alphanumerical part name (restricted to [a-zA-Z0-9_:-]*)
89 89
90 90 :partid: A 32bits integer (unique in the bundle) that can be used to refer
91 91 to this part.
92 92
93 93 :parameters:
94 94
95 95 Part's parameter may have arbitrary content, the binary structure is::
96 96
97 97 <mandatory-count><advisory-count><param-sizes><param-data>
98 98
99 99 :mandatory-count: 1 byte, number of mandatory parameters
100 100
101 101 :advisory-count: 1 byte, number of advisory parameters
102 102
103 103 :param-sizes:
104 104
105 105 N couple of bytes, where N is the total number of parameters. Each
106 106 couple contains (<size-of-key>, <size-of-value) for one parameter.
107 107
108 108 :param-data:
109 109
110 110 A blob of bytes from which each parameter key and value can be
111 111 retrieved using the list of size couples stored in the previous
112 112 field.
113 113
114 114 Mandatory parameters comes first, then the advisory ones.
115 115
116 116 Each parameter's key MUST be unique within the part.
117 117
118 118 :payload:
119 119
120 120 payload is a series of `<chunksize><chunkdata>`.
121 121
122 122 `chunksize` is an int32, `chunkdata` are plain bytes (as much as
123 123 `chunksize` says)` The payload part is concluded by a zero size chunk.
124 124
125 125 The current implementation always produces either zero or one chunk.
126 126 This is an implementation limitation that will ultimately be lifted.
127 127
128 128 `chunksize` can be negative to trigger special case processing. No such
129 129 processing is in place yet.
130 130
131 131 Bundle processing
132 132 ============================
133 133
134 134 Each part is processed in order using a "part handler". Handler are registered
135 135 for a certain part type.
136 136
137 137 The matching of a part to its handler is case insensitive. The case of the
138 138 part type is used to know if a part is mandatory or advisory. If the Part type
139 139 contains any uppercase char it is considered mandatory. When no handler is
140 140 known for a Mandatory part, the process is aborted and an exception is raised.
141 141 If the part is advisory and no handler is known, the part is ignored. When the
142 142 process is aborted, the full bundle is still read from the stream to keep the
143 143 channel usable. But none of the part read from an abort are processed. In the
144 144 future, dropping the stream may become an option for channel we do not care to
145 145 preserve.
146 146 """
147 147
148 148 from __future__ import absolute_import
149 149
150 150 import errno
151 151 import re
152 152 import string
153 153 import struct
154 154 import sys
155 155 import urllib
156 156
157 157 from .i18n import _
158 158 from . import (
159 159 changegroup,
160 160 error,
161 161 obsolete,
162 162 pushkey,
163 163 tags,
164 164 url,
165 165 util,
166 166 )
167 167
168 168 _pack = struct.pack
169 169 _unpack = struct.unpack
170 170
171 171 _fstreamparamsize = '>i'
172 172 _fpartheadersize = '>i'
173 173 _fparttypesize = '>B'
174 174 _fpartid = '>I'
175 175 _fpayloadsize = '>i'
176 176 _fpartparamcount = '>BB'
177 177
178 178 preferedchunksize = 4096
179 179
180 180 _parttypeforbidden = re.compile('[^a-zA-Z0-9_:-]')
181 181
182 182 def outdebug(ui, message):
183 183 """debug regarding output stream (bundling)"""
184 184 if ui.configbool('devel', 'bundle2.debug', False):
185 185 ui.debug('bundle2-output: %s\n' % message)
186 186
187 187 def indebug(ui, message):
188 188 """debug on input stream (unbundling)"""
189 189 if ui.configbool('devel', 'bundle2.debug', False):
190 190 ui.debug('bundle2-input: %s\n' % message)
191 191
192 192 def validateparttype(parttype):
193 193 """raise ValueError if a parttype contains invalid character"""
194 194 if _parttypeforbidden.search(parttype):
195 195 raise ValueError(parttype)
196 196
197 197 def _makefpartparamsizes(nbparams):
198 198 """return a struct format to read part parameter sizes
199 199
200 200 The number parameters is variable so we need to build that format
201 201 dynamically.
202 202 """
203 203 return '>'+('BB'*nbparams)
204 204
205 205 parthandlermapping = {}
206 206
207 207 def parthandler(parttype, params=()):
208 208 """decorator that register a function as a bundle2 part handler
209 209
210 210 eg::
211 211
212 212 @parthandler('myparttype', ('mandatory', 'param', 'handled'))
213 213 def myparttypehandler(...):
214 214 '''process a part of type "my part".'''
215 215 ...
216 216 """
217 217 validateparttype(parttype)
218 218 def _decorator(func):
219 219 lparttype = parttype.lower() # enforce lower case matching.
220 220 assert lparttype not in parthandlermapping
221 221 parthandlermapping[lparttype] = func
222 222 func.params = frozenset(params)
223 223 return func
224 224 return _decorator
225 225
226 226 class unbundlerecords(object):
227 227 """keep record of what happens during and unbundle
228 228
229 229 New records are added using `records.add('cat', obj)`. Where 'cat' is a
230 230 category of record and obj is an arbitrary object.
231 231
232 232 `records['cat']` will return all entries of this category 'cat'.
233 233
234 234 Iterating on the object itself will yield `('category', obj)` tuples
235 235 for all entries.
236 236
237 237 All iterations happens in chronological order.
238 238 """
239 239
240 240 def __init__(self):
241 241 self._categories = {}
242 242 self._sequences = []
243 243 self._replies = {}
244 244
245 245 def add(self, category, entry, inreplyto=None):
246 246 """add a new record of a given category.
247 247
248 248 The entry can then be retrieved in the list returned by
249 249 self['category']."""
250 250 self._categories.setdefault(category, []).append(entry)
251 251 self._sequences.append((category, entry))
252 252 if inreplyto is not None:
253 253 self.getreplies(inreplyto).add(category, entry)
254 254
255 255 def getreplies(self, partid):
256 256 """get the records that are replies to a specific part"""
257 257 return self._replies.setdefault(partid, unbundlerecords())
258 258
259 259 def __getitem__(self, cat):
260 260 return tuple(self._categories.get(cat, ()))
261 261
262 262 def __iter__(self):
263 263 return iter(self._sequences)
264 264
265 265 def __len__(self):
266 266 return len(self._sequences)
267 267
268 268 def __nonzero__(self):
269 269 return bool(self._sequences)
270 270
271 271 class bundleoperation(object):
272 272 """an object that represents a single bundling process
273 273
274 274 Its purpose is to carry unbundle-related objects and states.
275 275
276 276 A new object should be created at the beginning of each bundle processing.
277 277 The object is to be returned by the processing function.
278 278
279 279 The object has very little content now it will ultimately contain:
280 280 * an access to the repo the bundle is applied to,
281 281 * a ui object,
282 282 * a way to retrieve a transaction to add changes to the repo,
283 283 * a way to record the result of processing each part,
284 284 * a way to construct a bundle response when applicable.
285 285 """
286 286
287 287 def __init__(self, repo, transactiongetter, captureoutput=True):
288 288 self.repo = repo
289 289 self.ui = repo.ui
290 290 self.records = unbundlerecords()
291 291 self.gettransaction = transactiongetter
292 292 self.reply = None
293 293 self.captureoutput = captureoutput
294 294
295 295 class TransactionUnavailable(RuntimeError):
296 296 pass
297 297
298 298 def _notransaction():
299 299 """default method to get a transaction while processing a bundle
300 300
301 301 Raise an exception to highlight the fact that no transaction was expected
302 302 to be created"""
303 303 raise TransactionUnavailable()
304 304
305 305 def applybundle(repo, unbundler, tr, source=None, url=None, op=None):
306 306 # transform me into unbundler.apply() as soon as the freeze is lifted
307 307 tr.hookargs['bundle2'] = '1'
308 308 if source is not None and 'source' not in tr.hookargs:
309 309 tr.hookargs['source'] = source
310 310 if url is not None and 'url' not in tr.hookargs:
311 311 tr.hookargs['url'] = url
312 312 return processbundle(repo, unbundler, lambda: tr, op=op)
313 313
314 314 def processbundle(repo, unbundler, transactiongetter=None, op=None):
315 315 """This function process a bundle, apply effect to/from a repo
316 316
317 317 It iterates over each part then searches for and uses the proper handling
318 318 code to process the part. Parts are processed in order.
319 319
320 320 This is very early version of this function that will be strongly reworked
321 321 before final usage.
322 322
323 323 Unknown Mandatory part will abort the process.
324 324
325 325 It is temporarily possible to provide a prebuilt bundleoperation to the
326 326 function. This is used to ensure output is properly propagated in case of
327 327 an error during the unbundling. This output capturing part will likely be
328 328 reworked and this ability will probably go away in the process.
329 329 """
330 330 if op is None:
331 331 if transactiongetter is None:
332 332 transactiongetter = _notransaction
333 333 op = bundleoperation(repo, transactiongetter)
334 334 # todo:
335 335 # - replace this is a init function soon.
336 336 # - exception catching
337 337 unbundler.params
338 338 if repo.ui.debugflag:
339 339 msg = ['bundle2-input-bundle:']
340 340 if unbundler.params:
341 341 msg.append(' %i params')
342 342 if op.gettransaction is None:
343 343 msg.append(' no-transaction')
344 344 else:
345 345 msg.append(' with-transaction')
346 346 msg.append('\n')
347 347 repo.ui.debug(''.join(msg))
348 348 iterparts = enumerate(unbundler.iterparts())
349 349 part = None
350 350 nbpart = 0
351 351 try:
352 352 for nbpart, part in iterparts:
353 353 _processpart(op, part)
354 354 except BaseException as exc:
355 355 for nbpart, part in iterparts:
356 356 # consume the bundle content
357 357 part.seek(0, 2)
358 358 # Small hack to let caller code distinguish exceptions from bundle2
359 359 # processing from processing the old format. This is mostly
360 360 # needed to handle different return codes to unbundle according to the
361 361 # type of bundle. We should probably clean up or drop this return code
362 362 # craziness in a future version.
363 363 exc.duringunbundle2 = True
364 364 salvaged = []
365 365 replycaps = None
366 366 if op.reply is not None:
367 367 salvaged = op.reply.salvageoutput()
368 368 replycaps = op.reply.capabilities
369 369 exc._replycaps = replycaps
370 370 exc._bundle2salvagedoutput = salvaged
371 371 raise
372 372 finally:
373 373 repo.ui.debug('bundle2-input-bundle: %i parts total\n' % nbpart)
374 374
375 375 return op
376 376
377 377 def _processpart(op, part):
378 378 """process a single part from a bundle
379 379
380 380 The part is guaranteed to have been fully consumed when the function exits
381 381 (even if an exception is raised)."""
382 382 status = 'unknown' # used by debug output
383 383 try:
384 384 try:
385 385 handler = parthandlermapping.get(part.type)
386 386 if handler is None:
387 387 status = 'unsupported-type'
388 388 raise error.BundleUnknownFeatureError(parttype=part.type)
389 389 indebug(op.ui, 'found a handler for part %r' % part.type)
390 390 unknownparams = part.mandatorykeys - handler.params
391 391 if unknownparams:
392 392 unknownparams = list(unknownparams)
393 393 unknownparams.sort()
394 394 status = 'unsupported-params (%s)' % unknownparams
395 395 raise error.BundleUnknownFeatureError(parttype=part.type,
396 396 params=unknownparams)
397 397 status = 'supported'
398 398 except error.BundleUnknownFeatureError as exc:
399 399 if part.mandatory: # mandatory parts
400 400 raise
401 401 indebug(op.ui, 'ignoring unsupported advisory part %s' % exc)
402 402 return # skip to part processing
403 403 finally:
404 404 if op.ui.debugflag:
405 405 msg = ['bundle2-input-part: "%s"' % part.type]
406 406 if not part.mandatory:
407 407 msg.append(' (advisory)')
408 408 nbmp = len(part.mandatorykeys)
409 409 nbap = len(part.params) - nbmp
410 410 if nbmp or nbap:
411 411 msg.append(' (params:')
412 412 if nbmp:
413 413 msg.append(' %i mandatory' % nbmp)
414 414 if nbap:
415 415 msg.append(' %i advisory' % nbmp)
416 416 msg.append(')')
417 417 msg.append(' %s\n' % status)
418 418 op.ui.debug(''.join(msg))
419 419
420 420 # handler is called outside the above try block so that we don't
421 421 # risk catching KeyErrors from anything other than the
422 422 # parthandlermapping lookup (any KeyError raised by handler()
423 423 # itself represents a defect of a different variety).
424 424 output = None
425 425 if op.captureoutput and op.reply is not None:
426 426 op.ui.pushbuffer(error=True, subproc=True)
427 427 output = ''
428 428 try:
429 429 handler(op, part)
430 430 finally:
431 431 if output is not None:
432 432 output = op.ui.popbuffer()
433 433 if output:
434 434 outpart = op.reply.newpart('output', data=output,
435 435 mandatory=False)
436 436 outpart.addparam('in-reply-to', str(part.id), mandatory=False)
437 437 finally:
438 438 # consume the part content to not corrupt the stream.
439 439 part.seek(0, 2)
440 440
441 441
442 442 def decodecaps(blob):
443 443 """decode a bundle2 caps bytes blob into a dictionary
444 444
445 445 The blob is a list of capabilities (one per line)
446 446 Capabilities may have values using a line of the form::
447 447
448 448 capability=value1,value2,value3
449 449
450 450 The values are always a list."""
451 451 caps = {}
452 452 for line in blob.splitlines():
453 453 if not line:
454 454 continue
455 455 if '=' not in line:
456 456 key, vals = line, ()
457 457 else:
458 458 key, vals = line.split('=', 1)
459 459 vals = vals.split(',')
460 460 key = urllib.unquote(key)
461 461 vals = [urllib.unquote(v) for v in vals]
462 462 caps[key] = vals
463 463 return caps
464 464
465 465 def encodecaps(caps):
466 466 """encode a bundle2 caps dictionary into a bytes blob"""
467 467 chunks = []
468 468 for ca in sorted(caps):
469 469 vals = caps[ca]
470 470 ca = urllib.quote(ca)
471 471 vals = [urllib.quote(v) for v in vals]
472 472 if vals:
473 473 ca = "%s=%s" % (ca, ','.join(vals))
474 474 chunks.append(ca)
475 475 return '\n'.join(chunks)
476 476
477 477 class bundle20(object):
478 478 """represent an outgoing bundle2 container
479 479
480 480 Use the `addparam` method to add stream level parameter. and `newpart` to
481 481 populate it. Then call `getchunks` to retrieve all the binary chunks of
482 482 data that compose the bundle2 container."""
483 483
484 484 _magicstring = 'HG20'
485 485
486 486 def __init__(self, ui, capabilities=()):
487 487 self.ui = ui
488 488 self._params = []
489 489 self._parts = []
490 490 self.capabilities = dict(capabilities)
491 491 self._compressor = util.compressors[None]()
492 492
493 493 def setcompression(self, alg):
494 494 """setup core part compression to <alg>"""
495 495 if alg is None:
496 496 return
497 497 assert not any(n.lower() == 'Compression' for n, v in self._params)
498 498 self.addparam('Compression', alg)
499 499 self._compressor = util.compressors[alg]()
500 500
501 501 @property
502 502 def nbparts(self):
503 503 """total number of parts added to the bundler"""
504 504 return len(self._parts)
505 505
506 506 # methods used to defines the bundle2 content
507 507 def addparam(self, name, value=None):
508 508 """add a stream level parameter"""
509 509 if not name:
510 510 raise ValueError('empty parameter name')
511 511 if name[0] not in string.letters:
512 512 raise ValueError('non letter first character: %r' % name)
513 513 self._params.append((name, value))
514 514
515 515 def addpart(self, part):
516 516 """add a new part to the bundle2 container
517 517
518 518 Parts contains the actual applicative payload."""
519 519 assert part.id is None
520 520 part.id = len(self._parts) # very cheap counter
521 521 self._parts.append(part)
522 522
523 523 def newpart(self, typeid, *args, **kwargs):
524 524 """create a new part and add it to the containers
525 525
526 526 As the part is directly added to the containers. For now, this means
527 527 that any failure to properly initialize the part after calling
528 528 ``newpart`` should result in a failure of the whole bundling process.
529 529
530 530 You can still fall back to manually create and add if you need better
531 531 control."""
532 532 part = bundlepart(typeid, *args, **kwargs)
533 533 self.addpart(part)
534 534 return part
535 535
536 536 # methods used to generate the bundle2 stream
537 537 def getchunks(self):
538 538 if self.ui.debugflag:
539 539 msg = ['bundle2-output-bundle: "%s",' % self._magicstring]
540 540 if self._params:
541 541 msg.append(' (%i params)' % len(self._params))
542 542 msg.append(' %i parts total\n' % len(self._parts))
543 543 self.ui.debug(''.join(msg))
544 544 outdebug(self.ui, 'start emission of %s stream' % self._magicstring)
545 545 yield self._magicstring
546 546 param = self._paramchunk()
547 547 outdebug(self.ui, 'bundle parameter: %s' % param)
548 548 yield _pack(_fstreamparamsize, len(param))
549 549 if param:
550 550 yield param
551 551 # starting compression
552 552 for chunk in self._getcorechunk():
553 553 yield self._compressor.compress(chunk)
554 554 yield self._compressor.flush()
555 555
556 556 def _paramchunk(self):
557 557 """return a encoded version of all stream parameters"""
558 558 blocks = []
559 559 for par, value in self._params:
560 560 par = urllib.quote(par)
561 561 if value is not None:
562 562 value = urllib.quote(value)
563 563 par = '%s=%s' % (par, value)
564 564 blocks.append(par)
565 565 return ' '.join(blocks)
566 566
567 567 def _getcorechunk(self):
568 568 """yield chunk for the core part of the bundle
569 569
570 570 (all but headers and parameters)"""
571 571 outdebug(self.ui, 'start of parts')
572 572 for part in self._parts:
573 573 outdebug(self.ui, 'bundle part: "%s"' % part.type)
574 574 for chunk in part.getchunks(ui=self.ui):
575 575 yield chunk
576 576 outdebug(self.ui, 'end of bundle')
577 577 yield _pack(_fpartheadersize, 0)
578 578
579 579
580 580 def salvageoutput(self):
581 581 """return a list with a copy of all output parts in the bundle
582 582
583 583 This is meant to be used during error handling to make sure we preserve
584 584 server output"""
585 585 salvaged = []
586 586 for part in self._parts:
587 587 if part.type.startswith('output'):
588 588 salvaged.append(part.copy())
589 589 return salvaged
590 590
591 591
592 592 class unpackermixin(object):
593 593 """A mixin to extract bytes and struct data from a stream"""
594 594
595 595 def __init__(self, fp):
596 596 self._fp = fp
597 597 self._seekable = (util.safehasattr(fp, 'seek') and
598 598 util.safehasattr(fp, 'tell'))
599 599
600 600 def _unpack(self, format):
601 601 """unpack this struct format from the stream"""
602 602 data = self._readexact(struct.calcsize(format))
603 603 return _unpack(format, data)
604 604
605 605 def _readexact(self, size):
606 606 """read exactly <size> bytes from the stream"""
607 607 return changegroup.readexactly(self._fp, size)
608 608
609 609 def seek(self, offset, whence=0):
610 610 """move the underlying file pointer"""
611 611 if self._seekable:
612 612 return self._fp.seek(offset, whence)
613 613 else:
614 614 raise NotImplementedError(_('File pointer is not seekable'))
615 615
616 616 def tell(self):
617 617 """return the file offset, or None if file is not seekable"""
618 618 if self._seekable:
619 619 try:
620 620 return self._fp.tell()
621 621 except IOError as e:
622 622 if e.errno == errno.ESPIPE:
623 623 self._seekable = False
624 624 else:
625 625 raise
626 626 return None
627 627
628 628 def close(self):
629 629 """close underlying file"""
630 630 if util.safehasattr(self._fp, 'close'):
631 631 return self._fp.close()
632 632
633 633 def getunbundler(ui, fp, magicstring=None):
634 634 """return a valid unbundler object for a given magicstring"""
635 635 if magicstring is None:
636 636 magicstring = changegroup.readexactly(fp, 4)
637 637 magic, version = magicstring[0:2], magicstring[2:4]
638 638 if magic != 'HG':
639 639 raise error.Abort(_('not a Mercurial bundle'))
640 640 unbundlerclass = formatmap.get(version)
641 641 if unbundlerclass is None:
642 642 raise error.Abort(_('unknown bundle version %s') % version)
643 643 unbundler = unbundlerclass(ui, fp)
644 644 indebug(ui, 'start processing of %s stream' % magicstring)
645 645 return unbundler
646 646
647 647 class unbundle20(unpackermixin):
648 648 """interpret a bundle2 stream
649 649
650 650 This class is fed with a binary stream and yields parts through its
651 651 `iterparts` methods."""
652 652
653 653 _magicstring = 'HG20'
654 654
655 655 def __init__(self, ui, fp):
656 656 """If header is specified, we do not read it out of the stream."""
657 657 self.ui = ui
658 658 self._decompressor = util.decompressors[None]
659 659 self._compressed = None
660 660 super(unbundle20, self).__init__(fp)
661 661
662 662 @util.propertycache
663 663 def params(self):
664 664 """dictionary of stream level parameters"""
665 665 indebug(self.ui, 'reading bundle2 stream parameters')
666 666 params = {}
667 667 paramssize = self._unpack(_fstreamparamsize)[0]
668 668 if paramssize < 0:
669 669 raise error.BundleValueError('negative bundle param size: %i'
670 670 % paramssize)
671 671 if paramssize:
672 672 params = self._readexact(paramssize)
673 673 params = self._processallparams(params)
674 674 return params
675 675
676 676 def _processallparams(self, paramsblock):
677 677 """"""
678 678 params = {}
679 679 for p in paramsblock.split(' '):
680 680 p = p.split('=', 1)
681 681 p = [urllib.unquote(i) for i in p]
682 682 if len(p) < 2:
683 683 p.append(None)
684 684 self._processparam(*p)
685 685 params[p[0]] = p[1]
686 686 return params
687 687
688 688
689 689 def _processparam(self, name, value):
690 690 """process a parameter, applying its effect if needed
691 691
692 692 Parameter starting with a lower case letter are advisory and will be
693 693 ignored when unknown. Those starting with an upper case letter are
694 694 mandatory and will this function will raise a KeyError when unknown.
695 695
696 696 Note: no option are currently supported. Any input will be either
697 697 ignored or failing.
698 698 """
699 699 if not name:
700 700 raise ValueError('empty parameter name')
701 701 if name[0] not in string.letters:
702 702 raise ValueError('non letter first character: %r' % name)
703 703 try:
704 704 handler = b2streamparamsmap[name.lower()]
705 705 except KeyError:
706 706 if name[0].islower():
707 707 indebug(self.ui, "ignoring unknown parameter %r" % name)
708 708 else:
709 709 raise error.BundleUnknownFeatureError(params=(name,))
710 710 else:
711 711 handler(self, name, value)
712 712
713 713 def _forwardchunks(self):
714 714 """utility to transfer a bundle2 as binary
715 715
716 716 This is made necessary by the fact the 'getbundle' command over 'ssh'
717 717 have no way to know then the reply end, relying on the bundle to be
718 718 interpreted to know its end. This is terrible and we are sorry, but we
719 719 needed to move forward to get general delta enabled.
720 720 """
721 721 yield self._magicstring
722 722 assert 'params' not in vars(self)
723 723 paramssize = self._unpack(_fstreamparamsize)[0]
724 724 if paramssize < 0:
725 725 raise error.BundleValueError('negative bundle param size: %i'
726 726 % paramssize)
727 727 yield _pack(_fstreamparamsize, paramssize)
728 728 if paramssize:
729 729 params = self._readexact(paramssize)
730 730 self._processallparams(params)
731 731 yield params
732 732 assert self._decompressor is util.decompressors[None]
733 733 # From there, payload might need to be decompressed
734 734 self._fp = self._decompressor(self._fp)
735 735 emptycount = 0
736 736 while emptycount < 2:
737 737 # so we can brainlessly loop
738 738 assert _fpartheadersize == _fpayloadsize
739 739 size = self._unpack(_fpartheadersize)[0]
740 740 yield _pack(_fpartheadersize, size)
741 741 if size:
742 742 emptycount = 0
743 743 else:
744 744 emptycount += 1
745 745 continue
746 746 if size == flaginterrupt:
747 747 continue
748 748 elif size < 0:
749 749 raise error.BundleValueError('negative chunk size: %i')
750 750 yield self._readexact(size)
751 751
752 752
753 753 def iterparts(self):
754 754 """yield all parts contained in the stream"""
755 755 # make sure param have been loaded
756 756 self.params
757 757 # From there, payload need to be decompressed
758 758 self._fp = self._decompressor(self._fp)
759 759 indebug(self.ui, 'start extraction of bundle2 parts')
760 760 headerblock = self._readpartheader()
761 761 while headerblock is not None:
762 762 part = unbundlepart(self.ui, headerblock, self._fp)
763 763 yield part
764 764 part.seek(0, 2)
765 765 headerblock = self._readpartheader()
766 766 indebug(self.ui, 'end of bundle2 stream')
767 767
768 768 def _readpartheader(self):
769 769 """reads a part header size and return the bytes blob
770 770
771 771 returns None if empty"""
772 772 headersize = self._unpack(_fpartheadersize)[0]
773 773 if headersize < 0:
774 774 raise error.BundleValueError('negative part header size: %i'
775 775 % headersize)
776 776 indebug(self.ui, 'part header size: %i' % headersize)
777 777 if headersize:
778 778 return self._readexact(headersize)
779 779 return None
780 780
781 781 def compressed(self):
782 782 self.params # load params
783 783 return self._compressed
784 784
785 785 formatmap = {'20': unbundle20}
786 786
787 787 b2streamparamsmap = {}
788 788
789 789 def b2streamparamhandler(name):
790 790 """register a handler for a stream level parameter"""
791 791 def decorator(func):
792 792 assert name not in formatmap
793 793 b2streamparamsmap[name] = func
794 794 return func
795 795 return decorator
796 796
797 797 @b2streamparamhandler('compression')
798 798 def processcompression(unbundler, param, value):
799 799 """read compression parameter and install payload decompression"""
800 800 if value not in util.decompressors:
801 801 raise error.BundleUnknownFeatureError(params=(param,),
802 802 values=(value,))
803 803 unbundler._decompressor = util.decompressors[value]
804 804 if value is not None:
805 805 unbundler._compressed = True
806 806
807 807 class bundlepart(object):
808 808 """A bundle2 part contains application level payload
809 809
810 810 The part `type` is used to route the part to the application level
811 811 handler.
812 812
813 813 The part payload is contained in ``part.data``. It could be raw bytes or a
814 814 generator of byte chunks.
815 815
816 816 You can add parameters to the part using the ``addparam`` method.
817 817 Parameters can be either mandatory (default) or advisory. Remote side
818 818 should be able to safely ignore the advisory ones.
819 819
820 820 Both data and parameters cannot be modified after the generation has begun.
821 821 """
822 822
823 823 def __init__(self, parttype, mandatoryparams=(), advisoryparams=(),
824 824 data='', mandatory=True):
825 825 validateparttype(parttype)
826 826 self.id = None
827 827 self.type = parttype
828 828 self._data = data
829 829 self._mandatoryparams = list(mandatoryparams)
830 830 self._advisoryparams = list(advisoryparams)
831 831 # checking for duplicated entries
832 832 self._seenparams = set()
833 833 for pname, __ in self._mandatoryparams + self._advisoryparams:
834 834 if pname in self._seenparams:
835 835 raise RuntimeError('duplicated params: %s' % pname)
836 836 self._seenparams.add(pname)
837 837 # status of the part's generation:
838 838 # - None: not started,
839 839 # - False: currently generated,
840 840 # - True: generation done.
841 841 self._generated = None
842 842 self.mandatory = mandatory
843 843
844 844 def copy(self):
845 845 """return a copy of the part
846 846
847 847 The new part have the very same content but no partid assigned yet.
848 848 Parts with generated data cannot be copied."""
849 849 assert not util.safehasattr(self.data, 'next')
850 850 return self.__class__(self.type, self._mandatoryparams,
851 851 self._advisoryparams, self._data, self.mandatory)
852 852
853 853 # methods used to defines the part content
854 def __setdata(self, data):
854 @property
855 def data(self):
856 return self._data
857
858 @data.setter
859 def data(self, data):
855 860 if self._generated is not None:
856 861 raise error.ReadOnlyPartError('part is being generated')
857 862 self._data = data
858 def __getdata(self):
859 return self._data
860 data = property(__getdata, __setdata)
861 863
862 864 @property
863 865 def mandatoryparams(self):
864 866 # make it an immutable tuple to force people through ``addparam``
865 867 return tuple(self._mandatoryparams)
866 868
867 869 @property
868 870 def advisoryparams(self):
869 871 # make it an immutable tuple to force people through ``addparam``
870 872 return tuple(self._advisoryparams)
871 873
872 874 def addparam(self, name, value='', mandatory=True):
873 875 if self._generated is not None:
874 876 raise error.ReadOnlyPartError('part is being generated')
875 877 if name in self._seenparams:
876 878 raise ValueError('duplicated params: %s' % name)
877 879 self._seenparams.add(name)
878 880 params = self._advisoryparams
879 881 if mandatory:
880 882 params = self._mandatoryparams
881 883 params.append((name, value))
882 884
883 885 # methods used to generates the bundle2 stream
884 886 def getchunks(self, ui):
885 887 if self._generated is not None:
886 888 raise RuntimeError('part can only be consumed once')
887 889 self._generated = False
888 890
889 891 if ui.debugflag:
890 892 msg = ['bundle2-output-part: "%s"' % self.type]
891 893 if not self.mandatory:
892 894 msg.append(' (advisory)')
893 895 nbmp = len(self.mandatoryparams)
894 896 nbap = len(self.advisoryparams)
895 897 if nbmp or nbap:
896 898 msg.append(' (params:')
897 899 if nbmp:
898 900 msg.append(' %i mandatory' % nbmp)
899 901 if nbap:
900 902 msg.append(' %i advisory' % nbmp)
901 903 msg.append(')')
902 904 if not self.data:
903 905 msg.append(' empty payload')
904 906 elif util.safehasattr(self.data, 'next'):
905 907 msg.append(' streamed payload')
906 908 else:
907 909 msg.append(' %i bytes payload' % len(self.data))
908 910 msg.append('\n')
909 911 ui.debug(''.join(msg))
910 912
911 913 #### header
912 914 if self.mandatory:
913 915 parttype = self.type.upper()
914 916 else:
915 917 parttype = self.type.lower()
916 918 outdebug(ui, 'part %s: "%s"' % (self.id, parttype))
917 919 ## parttype
918 920 header = [_pack(_fparttypesize, len(parttype)),
919 921 parttype, _pack(_fpartid, self.id),
920 922 ]
921 923 ## parameters
922 924 # count
923 925 manpar = self.mandatoryparams
924 926 advpar = self.advisoryparams
925 927 header.append(_pack(_fpartparamcount, len(manpar), len(advpar)))
926 928 # size
927 929 parsizes = []
928 930 for key, value in manpar:
929 931 parsizes.append(len(key))
930 932 parsizes.append(len(value))
931 933 for key, value in advpar:
932 934 parsizes.append(len(key))
933 935 parsizes.append(len(value))
934 936 paramsizes = _pack(_makefpartparamsizes(len(parsizes) / 2), *parsizes)
935 937 header.append(paramsizes)
936 938 # key, value
937 939 for key, value in manpar:
938 940 header.append(key)
939 941 header.append(value)
940 942 for key, value in advpar:
941 943 header.append(key)
942 944 header.append(value)
943 945 ## finalize header
944 946 headerchunk = ''.join(header)
945 947 outdebug(ui, 'header chunk size: %i' % len(headerchunk))
946 948 yield _pack(_fpartheadersize, len(headerchunk))
947 949 yield headerchunk
948 950 ## payload
949 951 try:
950 952 for chunk in self._payloadchunks():
951 953 outdebug(ui, 'payload chunk size: %i' % len(chunk))
952 954 yield _pack(_fpayloadsize, len(chunk))
953 955 yield chunk
954 956 except GeneratorExit:
955 957 # GeneratorExit means that nobody is listening for our
956 958 # results anyway, so just bail quickly rather than trying
957 959 # to produce an error part.
958 960 ui.debug('bundle2-generatorexit\n')
959 961 raise
960 962 except BaseException as exc:
961 963 # backup exception data for later
962 964 ui.debug('bundle2-input-stream-interrupt: encoding exception %s'
963 965 % exc)
964 966 exc_info = sys.exc_info()
965 967 msg = 'unexpected error: %s' % exc
966 968 interpart = bundlepart('error:abort', [('message', msg)],
967 969 mandatory=False)
968 970 interpart.id = 0
969 971 yield _pack(_fpayloadsize, -1)
970 972 for chunk in interpart.getchunks(ui=ui):
971 973 yield chunk
972 974 outdebug(ui, 'closing payload chunk')
973 975 # abort current part payload
974 976 yield _pack(_fpayloadsize, 0)
975 977 raise exc_info[0], exc_info[1], exc_info[2]
976 978 # end of payload
977 979 outdebug(ui, 'closing payload chunk')
978 980 yield _pack(_fpayloadsize, 0)
979 981 self._generated = True
980 982
981 983 def _payloadchunks(self):
982 984 """yield chunks of a the part payload
983 985
984 986 Exists to handle the different methods to provide data to a part."""
985 987 # we only support fixed size data now.
986 988 # This will be improved in the future.
987 989 if util.safehasattr(self.data, 'next'):
988 990 buff = util.chunkbuffer(self.data)
989 991 chunk = buff.read(preferedchunksize)
990 992 while chunk:
991 993 yield chunk
992 994 chunk = buff.read(preferedchunksize)
993 995 elif len(self.data):
994 996 yield self.data
995 997
996 998
997 999 flaginterrupt = -1
998 1000
999 1001 class interrupthandler(unpackermixin):
1000 1002 """read one part and process it with restricted capability
1001 1003
1002 1004 This allows to transmit exception raised on the producer size during part
1003 1005 iteration while the consumer is reading a part.
1004 1006
1005 1007 Part processed in this manner only have access to a ui object,"""
1006 1008
1007 1009 def __init__(self, ui, fp):
1008 1010 super(interrupthandler, self).__init__(fp)
1009 1011 self.ui = ui
1010 1012
1011 1013 def _readpartheader(self):
1012 1014 """reads a part header size and return the bytes blob
1013 1015
1014 1016 returns None if empty"""
1015 1017 headersize = self._unpack(_fpartheadersize)[0]
1016 1018 if headersize < 0:
1017 1019 raise error.BundleValueError('negative part header size: %i'
1018 1020 % headersize)
1019 1021 indebug(self.ui, 'part header size: %i\n' % headersize)
1020 1022 if headersize:
1021 1023 return self._readexact(headersize)
1022 1024 return None
1023 1025
1024 1026 def __call__(self):
1025 1027
1026 1028 self.ui.debug('bundle2-input-stream-interrupt:'
1027 1029 ' opening out of band context\n')
1028 1030 indebug(self.ui, 'bundle2 stream interruption, looking for a part.')
1029 1031 headerblock = self._readpartheader()
1030 1032 if headerblock is None:
1031 1033 indebug(self.ui, 'no part found during interruption.')
1032 1034 return
1033 1035 part = unbundlepart(self.ui, headerblock, self._fp)
1034 1036 op = interruptoperation(self.ui)
1035 1037 _processpart(op, part)
1036 1038 self.ui.debug('bundle2-input-stream-interrupt:'
1037 1039 ' closing out of band context\n')
1038 1040
1039 1041 class interruptoperation(object):
1040 1042 """A limited operation to be use by part handler during interruption
1041 1043
1042 1044 It only have access to an ui object.
1043 1045 """
1044 1046
1045 1047 def __init__(self, ui):
1046 1048 self.ui = ui
1047 1049 self.reply = None
1048 1050 self.captureoutput = False
1049 1051
1050 1052 @property
1051 1053 def repo(self):
1052 1054 raise RuntimeError('no repo access from stream interruption')
1053 1055
1054 1056 def gettransaction(self):
1055 1057 raise TransactionUnavailable('no repo access from stream interruption')
1056 1058
1057 1059 class unbundlepart(unpackermixin):
1058 1060 """a bundle part read from a bundle"""
1059 1061
1060 1062 def __init__(self, ui, header, fp):
1061 1063 super(unbundlepart, self).__init__(fp)
1062 1064 self.ui = ui
1063 1065 # unbundle state attr
1064 1066 self._headerdata = header
1065 1067 self._headeroffset = 0
1066 1068 self._initialized = False
1067 1069 self.consumed = False
1068 1070 # part data
1069 1071 self.id = None
1070 1072 self.type = None
1071 1073 self.mandatoryparams = None
1072 1074 self.advisoryparams = None
1073 1075 self.params = None
1074 1076 self.mandatorykeys = ()
1075 1077 self._payloadstream = None
1076 1078 self._readheader()
1077 1079 self._mandatory = None
1078 1080 self._chunkindex = [] #(payload, file) position tuples for chunk starts
1079 1081 self._pos = 0
1080 1082
1081 1083 def _fromheader(self, size):
1082 1084 """return the next <size> byte from the header"""
1083 1085 offset = self._headeroffset
1084 1086 data = self._headerdata[offset:(offset + size)]
1085 1087 self._headeroffset = offset + size
1086 1088 return data
1087 1089
1088 1090 def _unpackheader(self, format):
1089 1091 """read given format from header
1090 1092
1091 1093 This automatically compute the size of the format to read."""
1092 1094 data = self._fromheader(struct.calcsize(format))
1093 1095 return _unpack(format, data)
1094 1096
1095 1097 def _initparams(self, mandatoryparams, advisoryparams):
1096 1098 """internal function to setup all logic related parameters"""
1097 1099 # make it read only to prevent people touching it by mistake.
1098 1100 self.mandatoryparams = tuple(mandatoryparams)
1099 1101 self.advisoryparams = tuple(advisoryparams)
1100 1102 # user friendly UI
1101 1103 self.params = dict(self.mandatoryparams)
1102 1104 self.params.update(dict(self.advisoryparams))
1103 1105 self.mandatorykeys = frozenset(p[0] for p in mandatoryparams)
1104 1106
1105 1107 def _payloadchunks(self, chunknum=0):
1106 1108 '''seek to specified chunk and start yielding data'''
1107 1109 if len(self._chunkindex) == 0:
1108 1110 assert chunknum == 0, 'Must start with chunk 0'
1109 1111 self._chunkindex.append((0, super(unbundlepart, self).tell()))
1110 1112 else:
1111 1113 assert chunknum < len(self._chunkindex), \
1112 1114 'Unknown chunk %d' % chunknum
1113 1115 super(unbundlepart, self).seek(self._chunkindex[chunknum][1])
1114 1116
1115 1117 pos = self._chunkindex[chunknum][0]
1116 1118 payloadsize = self._unpack(_fpayloadsize)[0]
1117 1119 indebug(self.ui, 'payload chunk size: %i' % payloadsize)
1118 1120 while payloadsize:
1119 1121 if payloadsize == flaginterrupt:
1120 1122 # interruption detection, the handler will now read a
1121 1123 # single part and process it.
1122 1124 interrupthandler(self.ui, self._fp)()
1123 1125 elif payloadsize < 0:
1124 1126 msg = 'negative payload chunk size: %i' % payloadsize
1125 1127 raise error.BundleValueError(msg)
1126 1128 else:
1127 1129 result = self._readexact(payloadsize)
1128 1130 chunknum += 1
1129 1131 pos += payloadsize
1130 1132 if chunknum == len(self._chunkindex):
1131 1133 self._chunkindex.append((pos,
1132 1134 super(unbundlepart, self).tell()))
1133 1135 yield result
1134 1136 payloadsize = self._unpack(_fpayloadsize)[0]
1135 1137 indebug(self.ui, 'payload chunk size: %i' % payloadsize)
1136 1138
1137 1139 def _findchunk(self, pos):
1138 1140 '''for a given payload position, return a chunk number and offset'''
1139 1141 for chunk, (ppos, fpos) in enumerate(self._chunkindex):
1140 1142 if ppos == pos:
1141 1143 return chunk, 0
1142 1144 elif ppos > pos:
1143 1145 return chunk - 1, pos - self._chunkindex[chunk - 1][0]
1144 1146 raise ValueError('Unknown chunk')
1145 1147
1146 1148 def _readheader(self):
1147 1149 """read the header and setup the object"""
1148 1150 typesize = self._unpackheader(_fparttypesize)[0]
1149 1151 self.type = self._fromheader(typesize)
1150 1152 indebug(self.ui, 'part type: "%s"' % self.type)
1151 1153 self.id = self._unpackheader(_fpartid)[0]
1152 1154 indebug(self.ui, 'part id: "%s"' % self.id)
1153 1155 # extract mandatory bit from type
1154 1156 self.mandatory = (self.type != self.type.lower())
1155 1157 self.type = self.type.lower()
1156 1158 ## reading parameters
1157 1159 # param count
1158 1160 mancount, advcount = self._unpackheader(_fpartparamcount)
1159 1161 indebug(self.ui, 'part parameters: %i' % (mancount + advcount))
1160 1162 # param size
1161 1163 fparamsizes = _makefpartparamsizes(mancount + advcount)
1162 1164 paramsizes = self._unpackheader(fparamsizes)
1163 1165 # make it a list of couple again
1164 1166 paramsizes = zip(paramsizes[::2], paramsizes[1::2])
1165 1167 # split mandatory from advisory
1166 1168 mansizes = paramsizes[:mancount]
1167 1169 advsizes = paramsizes[mancount:]
1168 1170 # retrieve param value
1169 1171 manparams = []
1170 1172 for key, value in mansizes:
1171 1173 manparams.append((self._fromheader(key), self._fromheader(value)))
1172 1174 advparams = []
1173 1175 for key, value in advsizes:
1174 1176 advparams.append((self._fromheader(key), self._fromheader(value)))
1175 1177 self._initparams(manparams, advparams)
1176 1178 ## part payload
1177 1179 self._payloadstream = util.chunkbuffer(self._payloadchunks())
1178 1180 # we read the data, tell it
1179 1181 self._initialized = True
1180 1182
1181 1183 def read(self, size=None):
1182 1184 """read payload data"""
1183 1185 if not self._initialized:
1184 1186 self._readheader()
1185 1187 if size is None:
1186 1188 data = self._payloadstream.read()
1187 1189 else:
1188 1190 data = self._payloadstream.read(size)
1189 1191 self._pos += len(data)
1190 1192 if size is None or len(data) < size:
1191 1193 if not self.consumed and self._pos:
1192 1194 self.ui.debug('bundle2-input-part: total payload size %i\n'
1193 1195 % self._pos)
1194 1196 self.consumed = True
1195 1197 return data
1196 1198
1197 1199 def tell(self):
1198 1200 return self._pos
1199 1201
1200 1202 def seek(self, offset, whence=0):
1201 1203 if whence == 0:
1202 1204 newpos = offset
1203 1205 elif whence == 1:
1204 1206 newpos = self._pos + offset
1205 1207 elif whence == 2:
1206 1208 if not self.consumed:
1207 1209 self.read()
1208 1210 newpos = self._chunkindex[-1][0] - offset
1209 1211 else:
1210 1212 raise ValueError('Unknown whence value: %r' % (whence,))
1211 1213
1212 1214 if newpos > self._chunkindex[-1][0] and not self.consumed:
1213 1215 self.read()
1214 1216 if not 0 <= newpos <= self._chunkindex[-1][0]:
1215 1217 raise ValueError('Offset out of range')
1216 1218
1217 1219 if self._pos != newpos:
1218 1220 chunk, internaloffset = self._findchunk(newpos)
1219 1221 self._payloadstream = util.chunkbuffer(self._payloadchunks(chunk))
1220 1222 adjust = self.read(internaloffset)
1221 1223 if len(adjust) != internaloffset:
1222 1224 raise error.Abort(_('Seek failed\n'))
1223 1225 self._pos = newpos
1224 1226
1225 1227 # These are only the static capabilities.
1226 1228 # Check the 'getrepocaps' function for the rest.
1227 1229 capabilities = {'HG20': (),
1228 1230 'error': ('abort', 'unsupportedcontent', 'pushraced',
1229 1231 'pushkey'),
1230 1232 'listkeys': (),
1231 1233 'pushkey': (),
1232 1234 'digests': tuple(sorted(util.DIGESTS.keys())),
1233 1235 'remote-changegroup': ('http', 'https'),
1234 1236 'hgtagsfnodes': (),
1235 1237 }
1236 1238
1237 1239 def getrepocaps(repo, allowpushback=False):
1238 1240 """return the bundle2 capabilities for a given repo
1239 1241
1240 1242 Exists to allow extensions (like evolution) to mutate the capabilities.
1241 1243 """
1242 1244 caps = capabilities.copy()
1243 1245 caps['changegroup'] = tuple(sorted(changegroup.supportedversions(repo)))
1244 1246 if obsolete.isenabled(repo, obsolete.exchangeopt):
1245 1247 supportedformat = tuple('V%i' % v for v in obsolete.formats)
1246 1248 caps['obsmarkers'] = supportedformat
1247 1249 if allowpushback:
1248 1250 caps['pushback'] = ()
1249 1251 return caps
1250 1252
1251 1253 def bundle2caps(remote):
1252 1254 """return the bundle capabilities of a peer as dict"""
1253 1255 raw = remote.capable('bundle2')
1254 1256 if not raw and raw != '':
1255 1257 return {}
1256 1258 capsblob = urllib.unquote(remote.capable('bundle2'))
1257 1259 return decodecaps(capsblob)
1258 1260
1259 1261 def obsmarkersversion(caps):
1260 1262 """extract the list of supported obsmarkers versions from a bundle2caps dict
1261 1263 """
1262 1264 obscaps = caps.get('obsmarkers', ())
1263 1265 return [int(c[1:]) for c in obscaps if c.startswith('V')]
1264 1266
1265 1267 @parthandler('changegroup', ('version', 'nbchanges', 'treemanifest'))
1266 1268 def handlechangegroup(op, inpart):
1267 1269 """apply a changegroup part on the repo
1268 1270
1269 1271 This is a very early implementation that will massive rework before being
1270 1272 inflicted to any end-user.
1271 1273 """
1272 1274 # Make sure we trigger a transaction creation
1273 1275 #
1274 1276 # The addchangegroup function will get a transaction object by itself, but
1275 1277 # we need to make sure we trigger the creation of a transaction object used
1276 1278 # for the whole processing scope.
1277 1279 op.gettransaction()
1278 1280 unpackerversion = inpart.params.get('version', '01')
1279 1281 # We should raise an appropriate exception here
1280 1282 cg = changegroup.getunbundler(unpackerversion, inpart, None)
1281 1283 # the source and url passed here are overwritten by the one contained in
1282 1284 # the transaction.hookargs argument. So 'bundle2' is a placeholder
1283 1285 nbchangesets = None
1284 1286 if 'nbchanges' in inpart.params:
1285 1287 nbchangesets = int(inpart.params.get('nbchanges'))
1286 1288 if ('treemanifest' in inpart.params and
1287 1289 'treemanifest' not in op.repo.requirements):
1288 1290 if len(op.repo.changelog) != 0:
1289 1291 raise error.Abort(_(
1290 1292 "bundle contains tree manifests, but local repo is "
1291 1293 "non-empty and does not use tree manifests"))
1292 1294 op.repo.requirements.add('treemanifest')
1293 1295 op.repo._applyopenerreqs()
1294 1296 op.repo._writerequirements()
1295 1297 ret = cg.apply(op.repo, 'bundle2', 'bundle2', expectedtotal=nbchangesets)
1296 1298 op.records.add('changegroup', {'return': ret})
1297 1299 if op.reply is not None:
1298 1300 # This is definitely not the final form of this
1299 1301 # return. But one need to start somewhere.
1300 1302 part = op.reply.newpart('reply:changegroup', mandatory=False)
1301 1303 part.addparam('in-reply-to', str(inpart.id), mandatory=False)
1302 1304 part.addparam('return', '%i' % ret, mandatory=False)
1303 1305 assert not inpart.read()
1304 1306
1305 1307 _remotechangegroupparams = tuple(['url', 'size', 'digests'] +
1306 1308 ['digest:%s' % k for k in util.DIGESTS.keys()])
1307 1309 @parthandler('remote-changegroup', _remotechangegroupparams)
1308 1310 def handleremotechangegroup(op, inpart):
1309 1311 """apply a bundle10 on the repo, given an url and validation information
1310 1312
1311 1313 All the information about the remote bundle to import are given as
1312 1314 parameters. The parameters include:
1313 1315 - url: the url to the bundle10.
1314 1316 - size: the bundle10 file size. It is used to validate what was
1315 1317 retrieved by the client matches the server knowledge about the bundle.
1316 1318 - digests: a space separated list of the digest types provided as
1317 1319 parameters.
1318 1320 - digest:<digest-type>: the hexadecimal representation of the digest with
1319 1321 that name. Like the size, it is used to validate what was retrieved by
1320 1322 the client matches what the server knows about the bundle.
1321 1323
1322 1324 When multiple digest types are given, all of them are checked.
1323 1325 """
1324 1326 try:
1325 1327 raw_url = inpart.params['url']
1326 1328 except KeyError:
1327 1329 raise error.Abort(_('remote-changegroup: missing "%s" param') % 'url')
1328 1330 parsed_url = util.url(raw_url)
1329 1331 if parsed_url.scheme not in capabilities['remote-changegroup']:
1330 1332 raise error.Abort(_('remote-changegroup does not support %s urls') %
1331 1333 parsed_url.scheme)
1332 1334
1333 1335 try:
1334 1336 size = int(inpart.params['size'])
1335 1337 except ValueError:
1336 1338 raise error.Abort(_('remote-changegroup: invalid value for param "%s"')
1337 1339 % 'size')
1338 1340 except KeyError:
1339 1341 raise error.Abort(_('remote-changegroup: missing "%s" param') % 'size')
1340 1342
1341 1343 digests = {}
1342 1344 for typ in inpart.params.get('digests', '').split():
1343 1345 param = 'digest:%s' % typ
1344 1346 try:
1345 1347 value = inpart.params[param]
1346 1348 except KeyError:
1347 1349 raise error.Abort(_('remote-changegroup: missing "%s" param') %
1348 1350 param)
1349 1351 digests[typ] = value
1350 1352
1351 1353 real_part = util.digestchecker(url.open(op.ui, raw_url), size, digests)
1352 1354
1353 1355 # Make sure we trigger a transaction creation
1354 1356 #
1355 1357 # The addchangegroup function will get a transaction object by itself, but
1356 1358 # we need to make sure we trigger the creation of a transaction object used
1357 1359 # for the whole processing scope.
1358 1360 op.gettransaction()
1359 1361 from . import exchange
1360 1362 cg = exchange.readbundle(op.repo.ui, real_part, raw_url)
1361 1363 if not isinstance(cg, changegroup.cg1unpacker):
1362 1364 raise error.Abort(_('%s: not a bundle version 1.0') %
1363 1365 util.hidepassword(raw_url))
1364 1366 ret = cg.apply(op.repo, 'bundle2', 'bundle2')
1365 1367 op.records.add('changegroup', {'return': ret})
1366 1368 if op.reply is not None:
1367 1369 # This is definitely not the final form of this
1368 1370 # return. But one need to start somewhere.
1369 1371 part = op.reply.newpart('reply:changegroup')
1370 1372 part.addparam('in-reply-to', str(inpart.id), mandatory=False)
1371 1373 part.addparam('return', '%i' % ret, mandatory=False)
1372 1374 try:
1373 1375 real_part.validate()
1374 1376 except error.Abort as e:
1375 1377 raise error.Abort(_('bundle at %s is corrupted:\n%s') %
1376 1378 (util.hidepassword(raw_url), str(e)))
1377 1379 assert not inpart.read()
1378 1380
1379 1381 @parthandler('reply:changegroup', ('return', 'in-reply-to'))
1380 1382 def handlereplychangegroup(op, inpart):
1381 1383 ret = int(inpart.params['return'])
1382 1384 replyto = int(inpart.params['in-reply-to'])
1383 1385 op.records.add('changegroup', {'return': ret}, replyto)
1384 1386
1385 1387 @parthandler('check:heads')
1386 1388 def handlecheckheads(op, inpart):
1387 1389 """check that head of the repo did not change
1388 1390
1389 1391 This is used to detect a push race when using unbundle.
1390 1392 This replaces the "heads" argument of unbundle."""
1391 1393 h = inpart.read(20)
1392 1394 heads = []
1393 1395 while len(h) == 20:
1394 1396 heads.append(h)
1395 1397 h = inpart.read(20)
1396 1398 assert not h
1397 1399 # Trigger a transaction so that we are guaranteed to have the lock now.
1398 1400 if op.ui.configbool('experimental', 'bundle2lazylocking'):
1399 1401 op.gettransaction()
1400 1402 if heads != op.repo.heads():
1401 1403 raise error.PushRaced('repository changed while pushing - '
1402 1404 'please try again')
1403 1405
1404 1406 @parthandler('output')
1405 1407 def handleoutput(op, inpart):
1406 1408 """forward output captured on the server to the client"""
1407 1409 for line in inpart.read().splitlines():
1408 1410 op.ui.status(('remote: %s\n' % line))
1409 1411
1410 1412 @parthandler('replycaps')
1411 1413 def handlereplycaps(op, inpart):
1412 1414 """Notify that a reply bundle should be created
1413 1415
1414 1416 The payload contains the capabilities information for the reply"""
1415 1417 caps = decodecaps(inpart.read())
1416 1418 if op.reply is None:
1417 1419 op.reply = bundle20(op.ui, caps)
1418 1420
1419 1421 class AbortFromPart(error.Abort):
1420 1422 """Sub-class of Abort that denotes an error from a bundle2 part."""
1421 1423
1422 1424 @parthandler('error:abort', ('message', 'hint'))
1423 1425 def handleerrorabort(op, inpart):
1424 1426 """Used to transmit abort error over the wire"""
1425 1427 raise AbortFromPart(inpart.params['message'],
1426 1428 hint=inpart.params.get('hint'))
1427 1429
1428 1430 @parthandler('error:pushkey', ('namespace', 'key', 'new', 'old', 'ret',
1429 1431 'in-reply-to'))
1430 1432 def handleerrorpushkey(op, inpart):
1431 1433 """Used to transmit failure of a mandatory pushkey over the wire"""
1432 1434 kwargs = {}
1433 1435 for name in ('namespace', 'key', 'new', 'old', 'ret'):
1434 1436 value = inpart.params.get(name)
1435 1437 if value is not None:
1436 1438 kwargs[name] = value
1437 1439 raise error.PushkeyFailed(inpart.params['in-reply-to'], **kwargs)
1438 1440
1439 1441 @parthandler('error:unsupportedcontent', ('parttype', 'params'))
1440 1442 def handleerrorunsupportedcontent(op, inpart):
1441 1443 """Used to transmit unknown content error over the wire"""
1442 1444 kwargs = {}
1443 1445 parttype = inpart.params.get('parttype')
1444 1446 if parttype is not None:
1445 1447 kwargs['parttype'] = parttype
1446 1448 params = inpart.params.get('params')
1447 1449 if params is not None:
1448 1450 kwargs['params'] = params.split('\0')
1449 1451
1450 1452 raise error.BundleUnknownFeatureError(**kwargs)
1451 1453
1452 1454 @parthandler('error:pushraced', ('message',))
1453 1455 def handleerrorpushraced(op, inpart):
1454 1456 """Used to transmit push race error over the wire"""
1455 1457 raise error.ResponseError(_('push failed:'), inpart.params['message'])
1456 1458
1457 1459 @parthandler('listkeys', ('namespace',))
1458 1460 def handlelistkeys(op, inpart):
1459 1461 """retrieve pushkey namespace content stored in a bundle2"""
1460 1462 namespace = inpart.params['namespace']
1461 1463 r = pushkey.decodekeys(inpart.read())
1462 1464 op.records.add('listkeys', (namespace, r))
1463 1465
1464 1466 @parthandler('pushkey', ('namespace', 'key', 'old', 'new'))
1465 1467 def handlepushkey(op, inpart):
1466 1468 """process a pushkey request"""
1467 1469 dec = pushkey.decode
1468 1470 namespace = dec(inpart.params['namespace'])
1469 1471 key = dec(inpart.params['key'])
1470 1472 old = dec(inpart.params['old'])
1471 1473 new = dec(inpart.params['new'])
1472 1474 # Grab the transaction to ensure that we have the lock before performing the
1473 1475 # pushkey.
1474 1476 if op.ui.configbool('experimental', 'bundle2lazylocking'):
1475 1477 op.gettransaction()
1476 1478 ret = op.repo.pushkey(namespace, key, old, new)
1477 1479 record = {'namespace': namespace,
1478 1480 'key': key,
1479 1481 'old': old,
1480 1482 'new': new}
1481 1483 op.records.add('pushkey', record)
1482 1484 if op.reply is not None:
1483 1485 rpart = op.reply.newpart('reply:pushkey')
1484 1486 rpart.addparam('in-reply-to', str(inpart.id), mandatory=False)
1485 1487 rpart.addparam('return', '%i' % ret, mandatory=False)
1486 1488 if inpart.mandatory and not ret:
1487 1489 kwargs = {}
1488 1490 for key in ('namespace', 'key', 'new', 'old', 'ret'):
1489 1491 if key in inpart.params:
1490 1492 kwargs[key] = inpart.params[key]
1491 1493 raise error.PushkeyFailed(partid=str(inpart.id), **kwargs)
1492 1494
1493 1495 @parthandler('reply:pushkey', ('return', 'in-reply-to'))
1494 1496 def handlepushkeyreply(op, inpart):
1495 1497 """retrieve the result of a pushkey request"""
1496 1498 ret = int(inpart.params['return'])
1497 1499 partid = int(inpart.params['in-reply-to'])
1498 1500 op.records.add('pushkey', {'return': ret}, partid)
1499 1501
1500 1502 @parthandler('obsmarkers')
1501 1503 def handleobsmarker(op, inpart):
1502 1504 """add a stream of obsmarkers to the repo"""
1503 1505 tr = op.gettransaction()
1504 1506 markerdata = inpart.read()
1505 1507 if op.ui.config('experimental', 'obsmarkers-exchange-debug', False):
1506 1508 op.ui.write(('obsmarker-exchange: %i bytes received\n')
1507 1509 % len(markerdata))
1508 1510 # The mergemarkers call will crash if marker creation is not enabled.
1509 1511 # we want to avoid this if the part is advisory.
1510 1512 if not inpart.mandatory and op.repo.obsstore.readonly:
1511 1513 op.repo.ui.debug('ignoring obsolescence markers, feature not enabled')
1512 1514 return
1513 1515 new = op.repo.obsstore.mergemarkers(tr, markerdata)
1514 1516 if new:
1515 1517 op.repo.ui.status(_('%i new obsolescence markers\n') % new)
1516 1518 op.records.add('obsmarkers', {'new': new})
1517 1519 if op.reply is not None:
1518 1520 rpart = op.reply.newpart('reply:obsmarkers')
1519 1521 rpart.addparam('in-reply-to', str(inpart.id), mandatory=False)
1520 1522 rpart.addparam('new', '%i' % new, mandatory=False)
1521 1523
1522 1524
1523 1525 @parthandler('reply:obsmarkers', ('new', 'in-reply-to'))
1524 1526 def handleobsmarkerreply(op, inpart):
1525 1527 """retrieve the result of a pushkey request"""
1526 1528 ret = int(inpart.params['new'])
1527 1529 partid = int(inpart.params['in-reply-to'])
1528 1530 op.records.add('obsmarkers', {'new': ret}, partid)
1529 1531
1530 1532 @parthandler('hgtagsfnodes')
1531 1533 def handlehgtagsfnodes(op, inpart):
1532 1534 """Applies .hgtags fnodes cache entries to the local repo.
1533 1535
1534 1536 Payload is pairs of 20 byte changeset nodes and filenodes.
1535 1537 """
1536 1538 # Grab the transaction so we ensure that we have the lock at this point.
1537 1539 if op.ui.configbool('experimental', 'bundle2lazylocking'):
1538 1540 op.gettransaction()
1539 1541 cache = tags.hgtagsfnodescache(op.repo.unfiltered())
1540 1542
1541 1543 count = 0
1542 1544 while True:
1543 1545 node = inpart.read(20)
1544 1546 fnode = inpart.read(20)
1545 1547 if len(node) < 20 or len(fnode) < 20:
1546 1548 op.ui.debug('ignoring incomplete received .hgtags fnodes data\n')
1547 1549 break
1548 1550 cache.setfnode(node, fnode)
1549 1551 count += 1
1550 1552
1551 1553 cache.write()
1552 1554 op.ui.debug('applied %i hgtags fnodes cache entries\n' % count)
@@ -1,1213 +1,1213 b''
1 1 # scmutil.py - Mercurial core utility functions
2 2 #
3 3 # Copyright Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import errno
11 11 import glob
12 12 import os
13 13 import re
14 14 import shutil
15 15 import stat
16 16 import tempfile
17 17
18 18 from .i18n import _
19 19 from .node import wdirrev
20 20 from . import (
21 21 encoding,
22 22 error,
23 23 match as matchmod,
24 24 osutil,
25 25 pathutil,
26 26 phases,
27 27 revset,
28 28 similar,
29 29 util,
30 30 )
31 31
32 32 if os.name == 'nt':
33 33 from . import scmwindows as scmplatform
34 34 else:
35 35 from . import scmposix as scmplatform
36 36
37 37 systemrcpath = scmplatform.systemrcpath
38 38 userrcpath = scmplatform.userrcpath
39 39
40 40 class status(tuple):
41 41 '''Named tuple with a list of files per status. The 'deleted', 'unknown'
42 42 and 'ignored' properties are only relevant to the working copy.
43 43 '''
44 44
45 45 __slots__ = ()
46 46
47 47 def __new__(cls, modified, added, removed, deleted, unknown, ignored,
48 48 clean):
49 49 return tuple.__new__(cls, (modified, added, removed, deleted, unknown,
50 50 ignored, clean))
51 51
52 52 @property
53 53 def modified(self):
54 54 '''files that have been modified'''
55 55 return self[0]
56 56
57 57 @property
58 58 def added(self):
59 59 '''files that have been added'''
60 60 return self[1]
61 61
62 62 @property
63 63 def removed(self):
64 64 '''files that have been removed'''
65 65 return self[2]
66 66
67 67 @property
68 68 def deleted(self):
69 69 '''files that are in the dirstate, but have been deleted from the
70 70 working copy (aka "missing")
71 71 '''
72 72 return self[3]
73 73
74 74 @property
75 75 def unknown(self):
76 76 '''files not in the dirstate that are not ignored'''
77 77 return self[4]
78 78
79 79 @property
80 80 def ignored(self):
81 81 '''files not in the dirstate that are ignored (by _dirignore())'''
82 82 return self[5]
83 83
84 84 @property
85 85 def clean(self):
86 86 '''files that have not been modified'''
87 87 return self[6]
88 88
89 89 def __repr__(self, *args, **kwargs):
90 90 return (('<status modified=%r, added=%r, removed=%r, deleted=%r, '
91 91 'unknown=%r, ignored=%r, clean=%r>') % self)
92 92
93 93 def itersubrepos(ctx1, ctx2):
94 94 """find subrepos in ctx1 or ctx2"""
95 95 # Create a (subpath, ctx) mapping where we prefer subpaths from
96 96 # ctx1. The subpaths from ctx2 are important when the .hgsub file
97 97 # has been modified (in ctx2) but not yet committed (in ctx1).
98 98 subpaths = dict.fromkeys(ctx2.substate, ctx2)
99 99 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
100 100
101 101 missing = set()
102 102
103 103 for subpath in ctx2.substate:
104 104 if subpath not in ctx1.substate:
105 105 del subpaths[subpath]
106 106 missing.add(subpath)
107 107
108 108 for subpath, ctx in sorted(subpaths.iteritems()):
109 109 yield subpath, ctx.sub(subpath)
110 110
111 111 # Yield an empty subrepo based on ctx1 for anything only in ctx2. That way,
112 112 # status and diff will have an accurate result when it does
113 113 # 'sub.{status|diff}(rev2)'. Otherwise, the ctx2 subrepo is compared
114 114 # against itself.
115 115 for subpath in missing:
116 116 yield subpath, ctx2.nullsub(subpath, ctx1)
117 117
118 118 def nochangesfound(ui, repo, excluded=None):
119 119 '''Report no changes for push/pull, excluded is None or a list of
120 120 nodes excluded from the push/pull.
121 121 '''
122 122 secretlist = []
123 123 if excluded:
124 124 for n in excluded:
125 125 if n not in repo:
126 126 # discovery should not have included the filtered revision,
127 127 # we have to explicitly exclude it until discovery is cleanup.
128 128 continue
129 129 ctx = repo[n]
130 130 if ctx.phase() >= phases.secret and not ctx.extinct():
131 131 secretlist.append(n)
132 132
133 133 if secretlist:
134 134 ui.status(_("no changes found (ignored %d secret changesets)\n")
135 135 % len(secretlist))
136 136 else:
137 137 ui.status(_("no changes found\n"))
138 138
139 139 def checknewlabel(repo, lbl, kind):
140 140 # Do not use the "kind" parameter in ui output.
141 141 # It makes strings difficult to translate.
142 142 if lbl in ['tip', '.', 'null']:
143 143 raise error.Abort(_("the name '%s' is reserved") % lbl)
144 144 for c in (':', '\0', '\n', '\r'):
145 145 if c in lbl:
146 146 raise error.Abort(_("%r cannot be used in a name") % c)
147 147 try:
148 148 int(lbl)
149 149 raise error.Abort(_("cannot use an integer as a name"))
150 150 except ValueError:
151 151 pass
152 152
153 153 def checkfilename(f):
154 154 '''Check that the filename f is an acceptable filename for a tracked file'''
155 155 if '\r' in f or '\n' in f:
156 156 raise error.Abort(_("'\\n' and '\\r' disallowed in filenames: %r") % f)
157 157
158 158 def checkportable(ui, f):
159 159 '''Check if filename f is portable and warn or abort depending on config'''
160 160 checkfilename(f)
161 161 abort, warn = checkportabilityalert(ui)
162 162 if abort or warn:
163 163 msg = util.checkwinfilename(f)
164 164 if msg:
165 165 msg = "%s: %r" % (msg, f)
166 166 if abort:
167 167 raise error.Abort(msg)
168 168 ui.warn(_("warning: %s\n") % msg)
169 169
170 170 def checkportabilityalert(ui):
171 171 '''check if the user's config requests nothing, a warning, or abort for
172 172 non-portable filenames'''
173 173 val = ui.config('ui', 'portablefilenames', 'warn')
174 174 lval = val.lower()
175 175 bval = util.parsebool(val)
176 176 abort = os.name == 'nt' or lval == 'abort'
177 177 warn = bval or lval == 'warn'
178 178 if bval is None and not (warn or abort or lval == 'ignore'):
179 179 raise error.ConfigError(
180 180 _("ui.portablefilenames value is invalid ('%s')") % val)
181 181 return abort, warn
182 182
183 183 class casecollisionauditor(object):
184 184 def __init__(self, ui, abort, dirstate):
185 185 self._ui = ui
186 186 self._abort = abort
187 187 allfiles = '\0'.join(dirstate._map)
188 188 self._loweredfiles = set(encoding.lower(allfiles).split('\0'))
189 189 self._dirstate = dirstate
190 190 # The purpose of _newfiles is so that we don't complain about
191 191 # case collisions if someone were to call this object with the
192 192 # same filename twice.
193 193 self._newfiles = set()
194 194
195 195 def __call__(self, f):
196 196 if f in self._newfiles:
197 197 return
198 198 fl = encoding.lower(f)
199 199 if fl in self._loweredfiles and f not in self._dirstate:
200 200 msg = _('possible case-folding collision for %s') % f
201 201 if self._abort:
202 202 raise error.Abort(msg)
203 203 self._ui.warn(_("warning: %s\n") % msg)
204 204 self._loweredfiles.add(fl)
205 205 self._newfiles.add(f)
206 206
207 207 def filteredhash(repo, maxrev):
208 208 """build hash of filtered revisions in the current repoview.
209 209
210 210 Multiple caches perform up-to-date validation by checking that the
211 211 tiprev and tipnode stored in the cache file match the current repository.
212 212 However, this is not sufficient for validating repoviews because the set
213 213 of revisions in the view may change without the repository tiprev and
214 214 tipnode changing.
215 215
216 216 This function hashes all the revs filtered from the view and returns
217 217 that SHA-1 digest.
218 218 """
219 219 cl = repo.changelog
220 220 if not cl.filteredrevs:
221 221 return None
222 222 key = None
223 223 revs = sorted(r for r in cl.filteredrevs if r <= maxrev)
224 224 if revs:
225 225 s = util.sha1()
226 226 for rev in revs:
227 227 s.update('%s;' % rev)
228 228 key = s.digest()
229 229 return key
230 230
231 231 class abstractvfs(object):
232 232 """Abstract base class; cannot be instantiated"""
233 233
234 234 def __init__(self, *args, **kwargs):
235 235 '''Prevent instantiation; don't call this from subclasses.'''
236 236 raise NotImplementedError('attempted instantiating ' + str(type(self)))
237 237
238 238 def tryread(self, path):
239 239 '''gracefully return an empty string for missing files'''
240 240 try:
241 241 return self.read(path)
242 242 except IOError as inst:
243 243 if inst.errno != errno.ENOENT:
244 244 raise
245 245 return ""
246 246
247 247 def tryreadlines(self, path, mode='rb'):
248 248 '''gracefully return an empty array for missing files'''
249 249 try:
250 250 return self.readlines(path, mode=mode)
251 251 except IOError as inst:
252 252 if inst.errno != errno.ENOENT:
253 253 raise
254 254 return []
255 255
256 256 def open(self, path, mode="r", text=False, atomictemp=False,
257 257 notindexed=False):
258 258 '''Open ``path`` file, which is relative to vfs root.
259 259
260 260 Newly created directories are marked as "not to be indexed by
261 261 the content indexing service", if ``notindexed`` is specified
262 262 for "write" mode access.
263 263 '''
264 264 self.open = self.__call__
265 265 return self.__call__(path, mode, text, atomictemp, notindexed)
266 266
267 267 def read(self, path):
268 268 with self(path, 'rb') as fp:
269 269 return fp.read()
270 270
271 271 def readlines(self, path, mode='rb'):
272 272 with self(path, mode=mode) as fp:
273 273 return fp.readlines()
274 274
275 275 def write(self, path, data):
276 276 with self(path, 'wb') as fp:
277 277 return fp.write(data)
278 278
279 279 def writelines(self, path, data, mode='wb', notindexed=False):
280 280 with self(path, mode=mode, notindexed=notindexed) as fp:
281 281 return fp.writelines(data)
282 282
283 283 def append(self, path, data):
284 284 with self(path, 'ab') as fp:
285 285 return fp.write(data)
286 286
287 287 def basename(self, path):
288 288 """return base element of a path (as os.path.basename would do)
289 289
290 290 This exists to allow handling of strange encoding if needed."""
291 291 return os.path.basename(path)
292 292
293 293 def chmod(self, path, mode):
294 294 return os.chmod(self.join(path), mode)
295 295
296 296 def dirname(self, path):
297 297 """return dirname element of a path (as os.path.dirname would do)
298 298
299 299 This exists to allow handling of strange encoding if needed."""
300 300 return os.path.dirname(path)
301 301
302 302 def exists(self, path=None):
303 303 return os.path.exists(self.join(path))
304 304
305 305 def fstat(self, fp):
306 306 return util.fstat(fp)
307 307
308 308 def isdir(self, path=None):
309 309 return os.path.isdir(self.join(path))
310 310
311 311 def isfile(self, path=None):
312 312 return os.path.isfile(self.join(path))
313 313
314 314 def islink(self, path=None):
315 315 return os.path.islink(self.join(path))
316 316
317 317 def isfileorlink(self, path=None):
318 318 '''return whether path is a regular file or a symlink
319 319
320 320 Unlike isfile, this doesn't follow symlinks.'''
321 321 try:
322 322 st = self.lstat(path)
323 323 except OSError:
324 324 return False
325 325 mode = st.st_mode
326 326 return stat.S_ISREG(mode) or stat.S_ISLNK(mode)
327 327
328 328 def reljoin(self, *paths):
329 329 """join various elements of a path together (as os.path.join would do)
330 330
331 331 The vfs base is not injected so that path stay relative. This exists
332 332 to allow handling of strange encoding if needed."""
333 333 return os.path.join(*paths)
334 334
335 335 def split(self, path):
336 336 """split top-most element of a path (as os.path.split would do)
337 337
338 338 This exists to allow handling of strange encoding if needed."""
339 339 return os.path.split(path)
340 340
341 341 def lexists(self, path=None):
342 342 return os.path.lexists(self.join(path))
343 343
344 344 def lstat(self, path=None):
345 345 return os.lstat(self.join(path))
346 346
347 347 def listdir(self, path=None):
348 348 return os.listdir(self.join(path))
349 349
350 350 def makedir(self, path=None, notindexed=True):
351 351 return util.makedir(self.join(path), notindexed)
352 352
353 353 def makedirs(self, path=None, mode=None):
354 354 return util.makedirs(self.join(path), mode)
355 355
356 356 def makelock(self, info, path):
357 357 return util.makelock(info, self.join(path))
358 358
359 359 def mkdir(self, path=None):
360 360 return os.mkdir(self.join(path))
361 361
362 362 def mkstemp(self, suffix='', prefix='tmp', dir=None, text=False):
363 363 fd, name = tempfile.mkstemp(suffix=suffix, prefix=prefix,
364 364 dir=self.join(dir), text=text)
365 365 dname, fname = util.split(name)
366 366 if dir:
367 367 return fd, os.path.join(dir, fname)
368 368 else:
369 369 return fd, fname
370 370
371 371 def readdir(self, path=None, stat=None, skip=None):
372 372 return osutil.listdir(self.join(path), stat, skip)
373 373
374 374 def readlock(self, path):
375 375 return util.readlock(self.join(path))
376 376
377 377 def rename(self, src, dst):
378 378 return util.rename(self.join(src), self.join(dst))
379 379
380 380 def readlink(self, path):
381 381 return os.readlink(self.join(path))
382 382
383 383 def removedirs(self, path=None):
384 384 """Remove a leaf directory and all empty intermediate ones
385 385 """
386 386 return util.removedirs(self.join(path))
387 387
388 388 def rmtree(self, path=None, ignore_errors=False, forcibly=False):
389 389 """Remove a directory tree recursively
390 390
391 391 If ``forcibly``, this tries to remove READ-ONLY files, too.
392 392 """
393 393 if forcibly:
394 394 def onerror(function, path, excinfo):
395 395 if function is not os.remove:
396 396 raise
397 397 # read-only files cannot be unlinked under Windows
398 398 s = os.stat(path)
399 399 if (s.st_mode & stat.S_IWRITE) != 0:
400 400 raise
401 401 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
402 402 os.remove(path)
403 403 else:
404 404 onerror = None
405 405 return shutil.rmtree(self.join(path),
406 406 ignore_errors=ignore_errors, onerror=onerror)
407 407
408 408 def setflags(self, path, l, x):
409 409 return util.setflags(self.join(path), l, x)
410 410
411 411 def stat(self, path=None):
412 412 return os.stat(self.join(path))
413 413
414 414 def unlink(self, path=None):
415 415 return util.unlink(self.join(path))
416 416
417 417 def unlinkpath(self, path=None, ignoremissing=False):
418 418 return util.unlinkpath(self.join(path), ignoremissing)
419 419
420 420 def utime(self, path=None, t=None):
421 421 return os.utime(self.join(path), t)
422 422
423 423 def walk(self, path=None, onerror=None):
424 424 """Yield (dirpath, dirs, files) tuple for each directories under path
425 425
426 426 ``dirpath`` is relative one from the root of this vfs. This
427 427 uses ``os.sep`` as path separator, even you specify POSIX
428 428 style ``path``.
429 429
430 430 "The root of this vfs" is represented as empty ``dirpath``.
431 431 """
432 432 root = os.path.normpath(self.join(None))
433 433 # when dirpath == root, dirpath[prefixlen:] becomes empty
434 434 # because len(dirpath) < prefixlen.
435 435 prefixlen = len(pathutil.normasprefix(root))
436 436 for dirpath, dirs, files in os.walk(self.join(path), onerror=onerror):
437 437 yield (dirpath[prefixlen:], dirs, files)
438 438
439 439 class vfs(abstractvfs):
440 440 '''Operate files relative to a base directory
441 441
442 442 This class is used to hide the details of COW semantics and
443 443 remote file access from higher level code.
444 444 '''
445 445 def __init__(self, base, audit=True, expandpath=False, realpath=False):
446 446 if expandpath:
447 447 base = util.expandpath(base)
448 448 if realpath:
449 449 base = os.path.realpath(base)
450 450 self.base = base
451 self._setmustaudit(audit)
451 self.mustaudit = audit
452 452 self.createmode = None
453 453 self._trustnlink = None
454 454
455 def _getmustaudit(self):
455 @property
456 def mustaudit(self):
456 457 return self._audit
457 458
458 def _setmustaudit(self, onoff):
459 @mustaudit.setter
460 def mustaudit(self, onoff):
459 461 self._audit = onoff
460 462 if onoff:
461 463 self.audit = pathutil.pathauditor(self.base)
462 464 else:
463 465 self.audit = util.always
464 466
465 mustaudit = property(_getmustaudit, _setmustaudit)
466
467 467 @util.propertycache
468 468 def _cansymlink(self):
469 469 return util.checklink(self.base)
470 470
471 471 @util.propertycache
472 472 def _chmod(self):
473 473 return util.checkexec(self.base)
474 474
475 475 def _fixfilemode(self, name):
476 476 if self.createmode is None or not self._chmod:
477 477 return
478 478 os.chmod(name, self.createmode & 0o666)
479 479
480 480 def __call__(self, path, mode="r", text=False, atomictemp=False,
481 481 notindexed=False):
482 482 '''Open ``path`` file, which is relative to vfs root.
483 483
484 484 Newly created directories are marked as "not to be indexed by
485 485 the content indexing service", if ``notindexed`` is specified
486 486 for "write" mode access.
487 487 '''
488 488 if self._audit:
489 489 r = util.checkosfilename(path)
490 490 if r:
491 491 raise error.Abort("%s: %r" % (r, path))
492 492 self.audit(path)
493 493 f = self.join(path)
494 494
495 495 if not text and "b" not in mode:
496 496 mode += "b" # for that other OS
497 497
498 498 nlink = -1
499 499 if mode not in ('r', 'rb'):
500 500 dirname, basename = util.split(f)
501 501 # If basename is empty, then the path is malformed because it points
502 502 # to a directory. Let the posixfile() call below raise IOError.
503 503 if basename:
504 504 if atomictemp:
505 505 util.ensuredirs(dirname, self.createmode, notindexed)
506 506 return util.atomictempfile(f, mode, self.createmode)
507 507 try:
508 508 if 'w' in mode:
509 509 util.unlink(f)
510 510 nlink = 0
511 511 else:
512 512 # nlinks() may behave differently for files on Windows
513 513 # shares if the file is open.
514 514 with util.posixfile(f):
515 515 nlink = util.nlinks(f)
516 516 if nlink < 1:
517 517 nlink = 2 # force mktempcopy (issue1922)
518 518 except (OSError, IOError) as e:
519 519 if e.errno != errno.ENOENT:
520 520 raise
521 521 nlink = 0
522 522 util.ensuredirs(dirname, self.createmode, notindexed)
523 523 if nlink > 0:
524 524 if self._trustnlink is None:
525 525 self._trustnlink = nlink > 1 or util.checknlink(f)
526 526 if nlink > 1 or not self._trustnlink:
527 527 util.rename(util.mktempcopy(f), f)
528 528 fp = util.posixfile(f, mode)
529 529 if nlink == 0:
530 530 self._fixfilemode(f)
531 531 return fp
532 532
533 533 def symlink(self, src, dst):
534 534 self.audit(dst)
535 535 linkname = self.join(dst)
536 536 try:
537 537 os.unlink(linkname)
538 538 except OSError:
539 539 pass
540 540
541 541 util.ensuredirs(os.path.dirname(linkname), self.createmode)
542 542
543 543 if self._cansymlink:
544 544 try:
545 545 os.symlink(src, linkname)
546 546 except OSError as err:
547 547 raise OSError(err.errno, _('could not symlink to %r: %s') %
548 548 (src, err.strerror), linkname)
549 549 else:
550 550 self.write(dst, src)
551 551
552 552 def join(self, path, *insidef):
553 553 if path:
554 554 return os.path.join(self.base, path, *insidef)
555 555 else:
556 556 return self.base
557 557
558 558 opener = vfs
559 559
560 560 class auditvfs(object):
561 561 def __init__(self, vfs):
562 562 self.vfs = vfs
563 563
564 def _getmustaudit(self):
564 @property
565 def mustaudit(self):
565 566 return self.vfs.mustaudit
566 567
567 def _setmustaudit(self, onoff):
568 @mustaudit.setter
569 def mustaudit(self, onoff):
568 570 self.vfs.mustaudit = onoff
569 571
570 mustaudit = property(_getmustaudit, _setmustaudit)
571
572 572 class filtervfs(abstractvfs, auditvfs):
573 573 '''Wrapper vfs for filtering filenames with a function.'''
574 574
575 575 def __init__(self, vfs, filter):
576 576 auditvfs.__init__(self, vfs)
577 577 self._filter = filter
578 578
579 579 def __call__(self, path, *args, **kwargs):
580 580 return self.vfs(self._filter(path), *args, **kwargs)
581 581
582 582 def join(self, path, *insidef):
583 583 if path:
584 584 return self.vfs.join(self._filter(self.vfs.reljoin(path, *insidef)))
585 585 else:
586 586 return self.vfs.join(path)
587 587
588 588 filteropener = filtervfs
589 589
590 590 class readonlyvfs(abstractvfs, auditvfs):
591 591 '''Wrapper vfs preventing any writing.'''
592 592
593 593 def __init__(self, vfs):
594 594 auditvfs.__init__(self, vfs)
595 595
596 596 def __call__(self, path, mode='r', *args, **kw):
597 597 if mode not in ('r', 'rb'):
598 598 raise error.Abort('this vfs is read only')
599 599 return self.vfs(path, mode, *args, **kw)
600 600
601 601 def join(self, path, *insidef):
602 602 return self.vfs.join(path, *insidef)
603 603
604 604 def walkrepos(path, followsym=False, seen_dirs=None, recurse=False):
605 605 '''yield every hg repository under path, always recursively.
606 606 The recurse flag will only control recursion into repo working dirs'''
607 607 def errhandler(err):
608 608 if err.filename == path:
609 609 raise err
610 610 samestat = getattr(os.path, 'samestat', None)
611 611 if followsym and samestat is not None:
612 612 def adddir(dirlst, dirname):
613 613 match = False
614 614 dirstat = os.stat(dirname)
615 615 for lstdirstat in dirlst:
616 616 if samestat(dirstat, lstdirstat):
617 617 match = True
618 618 break
619 619 if not match:
620 620 dirlst.append(dirstat)
621 621 return not match
622 622 else:
623 623 followsym = False
624 624
625 625 if (seen_dirs is None) and followsym:
626 626 seen_dirs = []
627 627 adddir(seen_dirs, path)
628 628 for root, dirs, files in os.walk(path, topdown=True, onerror=errhandler):
629 629 dirs.sort()
630 630 if '.hg' in dirs:
631 631 yield root # found a repository
632 632 qroot = os.path.join(root, '.hg', 'patches')
633 633 if os.path.isdir(os.path.join(qroot, '.hg')):
634 634 yield qroot # we have a patch queue repo here
635 635 if recurse:
636 636 # avoid recursing inside the .hg directory
637 637 dirs.remove('.hg')
638 638 else:
639 639 dirs[:] = [] # don't descend further
640 640 elif followsym:
641 641 newdirs = []
642 642 for d in dirs:
643 643 fname = os.path.join(root, d)
644 644 if adddir(seen_dirs, fname):
645 645 if os.path.islink(fname):
646 646 for hgname in walkrepos(fname, True, seen_dirs):
647 647 yield hgname
648 648 else:
649 649 newdirs.append(d)
650 650 dirs[:] = newdirs
651 651
652 652 def osrcpath():
653 653 '''return default os-specific hgrc search path'''
654 654 path = []
655 655 defaultpath = os.path.join(util.datapath, 'default.d')
656 656 if os.path.isdir(defaultpath):
657 657 for f, kind in osutil.listdir(defaultpath):
658 658 if f.endswith('.rc'):
659 659 path.append(os.path.join(defaultpath, f))
660 660 path.extend(systemrcpath())
661 661 path.extend(userrcpath())
662 662 path = [os.path.normpath(f) for f in path]
663 663 return path
664 664
665 665 _rcpath = None
666 666
667 667 def rcpath():
668 668 '''return hgrc search path. if env var HGRCPATH is set, use it.
669 669 for each item in path, if directory, use files ending in .rc,
670 670 else use item.
671 671 make HGRCPATH empty to only look in .hg/hgrc of current repo.
672 672 if no HGRCPATH, use default os-specific path.'''
673 673 global _rcpath
674 674 if _rcpath is None:
675 675 if 'HGRCPATH' in os.environ:
676 676 _rcpath = []
677 677 for p in os.environ['HGRCPATH'].split(os.pathsep):
678 678 if not p:
679 679 continue
680 680 p = util.expandpath(p)
681 681 if os.path.isdir(p):
682 682 for f, kind in osutil.listdir(p):
683 683 if f.endswith('.rc'):
684 684 _rcpath.append(os.path.join(p, f))
685 685 else:
686 686 _rcpath.append(p)
687 687 else:
688 688 _rcpath = osrcpath()
689 689 return _rcpath
690 690
691 691 def intrev(rev):
692 692 """Return integer for a given revision that can be used in comparison or
693 693 arithmetic operation"""
694 694 if rev is None:
695 695 return wdirrev
696 696 return rev
697 697
698 698 def revsingle(repo, revspec, default='.'):
699 699 if not revspec and revspec != 0:
700 700 return repo[default]
701 701
702 702 l = revrange(repo, [revspec])
703 703 if not l:
704 704 raise error.Abort(_('empty revision set'))
705 705 return repo[l.last()]
706 706
707 707 def _pairspec(revspec):
708 708 tree = revset.parse(revspec)
709 709 tree = revset.optimize(tree, True)[1] # fix up "x^:y" -> "(x^):y"
710 710 return tree and tree[0] in ('range', 'rangepre', 'rangepost', 'rangeall')
711 711
712 712 def revpair(repo, revs):
713 713 if not revs:
714 714 return repo.dirstate.p1(), None
715 715
716 716 l = revrange(repo, revs)
717 717
718 718 if not l:
719 719 first = second = None
720 720 elif l.isascending():
721 721 first = l.min()
722 722 second = l.max()
723 723 elif l.isdescending():
724 724 first = l.max()
725 725 second = l.min()
726 726 else:
727 727 first = l.first()
728 728 second = l.last()
729 729
730 730 if first is None:
731 731 raise error.Abort(_('empty revision range'))
732 732 if (first == second and len(revs) >= 2
733 733 and not all(revrange(repo, [r]) for r in revs)):
734 734 raise error.Abort(_('empty revision on one side of range'))
735 735
736 736 # if top-level is range expression, the result must always be a pair
737 737 if first == second and len(revs) == 1 and not _pairspec(revs[0]):
738 738 return repo.lookup(first), None
739 739
740 740 return repo.lookup(first), repo.lookup(second)
741 741
742 742 def revrange(repo, revs):
743 743 """Yield revision as strings from a list of revision specifications."""
744 744 allspecs = []
745 745 for spec in revs:
746 746 if isinstance(spec, int):
747 747 spec = revset.formatspec('rev(%d)', spec)
748 748 allspecs.append(spec)
749 749 m = revset.matchany(repo.ui, allspecs, repo)
750 750 return m(repo)
751 751
752 752 def meaningfulparents(repo, ctx):
753 753 """Return list of meaningful (or all if debug) parentrevs for rev.
754 754
755 755 For merges (two non-nullrev revisions) both parents are meaningful.
756 756 Otherwise the first parent revision is considered meaningful if it
757 757 is not the preceding revision.
758 758 """
759 759 parents = ctx.parents()
760 760 if len(parents) > 1:
761 761 return parents
762 762 if repo.ui.debugflag:
763 763 return [parents[0], repo['null']]
764 764 if parents[0].rev() >= intrev(ctx.rev()) - 1:
765 765 return []
766 766 return parents
767 767
768 768 def expandpats(pats):
769 769 '''Expand bare globs when running on windows.
770 770 On posix we assume it already has already been done by sh.'''
771 771 if not util.expandglobs:
772 772 return list(pats)
773 773 ret = []
774 774 for kindpat in pats:
775 775 kind, pat = matchmod._patsplit(kindpat, None)
776 776 if kind is None:
777 777 try:
778 778 globbed = glob.glob(pat)
779 779 except re.error:
780 780 globbed = [pat]
781 781 if globbed:
782 782 ret.extend(globbed)
783 783 continue
784 784 ret.append(kindpat)
785 785 return ret
786 786
787 787 def matchandpats(ctx, pats=(), opts=None, globbed=False, default='relpath',
788 788 badfn=None):
789 789 '''Return a matcher and the patterns that were used.
790 790 The matcher will warn about bad matches, unless an alternate badfn callback
791 791 is provided.'''
792 792 if pats == ("",):
793 793 pats = []
794 794 if opts is None:
795 795 opts = {}
796 796 if not globbed and default == 'relpath':
797 797 pats = expandpats(pats or [])
798 798
799 799 def bad(f, msg):
800 800 ctx.repo().ui.warn("%s: %s\n" % (m.rel(f), msg))
801 801
802 802 if badfn is None:
803 803 badfn = bad
804 804
805 805 m = ctx.match(pats, opts.get('include'), opts.get('exclude'),
806 806 default, listsubrepos=opts.get('subrepos'), badfn=badfn)
807 807
808 808 if m.always():
809 809 pats = []
810 810 return m, pats
811 811
812 812 def match(ctx, pats=(), opts=None, globbed=False, default='relpath',
813 813 badfn=None):
814 814 '''Return a matcher that will warn about bad matches.'''
815 815 return matchandpats(ctx, pats, opts, globbed, default, badfn=badfn)[0]
816 816
817 817 def matchall(repo):
818 818 '''Return a matcher that will efficiently match everything.'''
819 819 return matchmod.always(repo.root, repo.getcwd())
820 820
821 821 def matchfiles(repo, files, badfn=None):
822 822 '''Return a matcher that will efficiently match exactly these files.'''
823 823 return matchmod.exact(repo.root, repo.getcwd(), files, badfn=badfn)
824 824
825 825 def origpath(ui, repo, filepath):
826 826 '''customize where .orig files are created
827 827
828 828 Fetch user defined path from config file: [ui] origbackuppath = <path>
829 829 Fall back to default (filepath) if not specified
830 830 '''
831 831 origbackuppath = ui.config('ui', 'origbackuppath', None)
832 832 if origbackuppath is None:
833 833 return filepath + ".orig"
834 834
835 835 filepathfromroot = os.path.relpath(filepath, start=repo.root)
836 836 fullorigpath = repo.wjoin(origbackuppath, filepathfromroot)
837 837
838 838 origbackupdir = repo.vfs.dirname(fullorigpath)
839 839 if not repo.vfs.exists(origbackupdir):
840 840 ui.note(_('creating directory: %s\n') % origbackupdir)
841 841 util.makedirs(origbackupdir)
842 842
843 843 return fullorigpath + ".orig"
844 844
845 845 def addremove(repo, matcher, prefix, opts=None, dry_run=None, similarity=None):
846 846 if opts is None:
847 847 opts = {}
848 848 m = matcher
849 849 if dry_run is None:
850 850 dry_run = opts.get('dry_run')
851 851 if similarity is None:
852 852 similarity = float(opts.get('similarity') or 0)
853 853
854 854 ret = 0
855 855 join = lambda f: os.path.join(prefix, f)
856 856
857 857 def matchessubrepo(matcher, subpath):
858 858 if matcher.exact(subpath):
859 859 return True
860 860 for f in matcher.files():
861 861 if f.startswith(subpath):
862 862 return True
863 863 return False
864 864
865 865 wctx = repo[None]
866 866 for subpath in sorted(wctx.substate):
867 867 if opts.get('subrepos') or matchessubrepo(m, subpath):
868 868 sub = wctx.sub(subpath)
869 869 try:
870 870 submatch = matchmod.narrowmatcher(subpath, m)
871 871 if sub.addremove(submatch, prefix, opts, dry_run, similarity):
872 872 ret = 1
873 873 except error.LookupError:
874 874 repo.ui.status(_("skipping missing subrepository: %s\n")
875 875 % join(subpath))
876 876
877 877 rejected = []
878 878 def badfn(f, msg):
879 879 if f in m.files():
880 880 m.bad(f, msg)
881 881 rejected.append(f)
882 882
883 883 badmatch = matchmod.badmatch(m, badfn)
884 884 added, unknown, deleted, removed, forgotten = _interestingfiles(repo,
885 885 badmatch)
886 886
887 887 unknownset = set(unknown + forgotten)
888 888 toprint = unknownset.copy()
889 889 toprint.update(deleted)
890 890 for abs in sorted(toprint):
891 891 if repo.ui.verbose or not m.exact(abs):
892 892 if abs in unknownset:
893 893 status = _('adding %s\n') % m.uipath(abs)
894 894 else:
895 895 status = _('removing %s\n') % m.uipath(abs)
896 896 repo.ui.status(status)
897 897
898 898 renames = _findrenames(repo, m, added + unknown, removed + deleted,
899 899 similarity)
900 900
901 901 if not dry_run:
902 902 _markchanges(repo, unknown + forgotten, deleted, renames)
903 903
904 904 for f in rejected:
905 905 if f in m.files():
906 906 return 1
907 907 return ret
908 908
909 909 def marktouched(repo, files, similarity=0.0):
910 910 '''Assert that files have somehow been operated upon. files are relative to
911 911 the repo root.'''
912 912 m = matchfiles(repo, files, badfn=lambda x, y: rejected.append(x))
913 913 rejected = []
914 914
915 915 added, unknown, deleted, removed, forgotten = _interestingfiles(repo, m)
916 916
917 917 if repo.ui.verbose:
918 918 unknownset = set(unknown + forgotten)
919 919 toprint = unknownset.copy()
920 920 toprint.update(deleted)
921 921 for abs in sorted(toprint):
922 922 if abs in unknownset:
923 923 status = _('adding %s\n') % abs
924 924 else:
925 925 status = _('removing %s\n') % abs
926 926 repo.ui.status(status)
927 927
928 928 renames = _findrenames(repo, m, added + unknown, removed + deleted,
929 929 similarity)
930 930
931 931 _markchanges(repo, unknown + forgotten, deleted, renames)
932 932
933 933 for f in rejected:
934 934 if f in m.files():
935 935 return 1
936 936 return 0
937 937
938 938 def _interestingfiles(repo, matcher):
939 939 '''Walk dirstate with matcher, looking for files that addremove would care
940 940 about.
941 941
942 942 This is different from dirstate.status because it doesn't care about
943 943 whether files are modified or clean.'''
944 944 added, unknown, deleted, removed, forgotten = [], [], [], [], []
945 945 audit_path = pathutil.pathauditor(repo.root)
946 946
947 947 ctx = repo[None]
948 948 dirstate = repo.dirstate
949 949 walkresults = dirstate.walk(matcher, sorted(ctx.substate), True, False,
950 950 full=False)
951 951 for abs, st in walkresults.iteritems():
952 952 dstate = dirstate[abs]
953 953 if dstate == '?' and audit_path.check(abs):
954 954 unknown.append(abs)
955 955 elif dstate != 'r' and not st:
956 956 deleted.append(abs)
957 957 elif dstate == 'r' and st:
958 958 forgotten.append(abs)
959 959 # for finding renames
960 960 elif dstate == 'r' and not st:
961 961 removed.append(abs)
962 962 elif dstate == 'a':
963 963 added.append(abs)
964 964
965 965 return added, unknown, deleted, removed, forgotten
966 966
967 967 def _findrenames(repo, matcher, added, removed, similarity):
968 968 '''Find renames from removed files to added ones.'''
969 969 renames = {}
970 970 if similarity > 0:
971 971 for old, new, score in similar.findrenames(repo, added, removed,
972 972 similarity):
973 973 if (repo.ui.verbose or not matcher.exact(old)
974 974 or not matcher.exact(new)):
975 975 repo.ui.status(_('recording removal of %s as rename to %s '
976 976 '(%d%% similar)\n') %
977 977 (matcher.rel(old), matcher.rel(new),
978 978 score * 100))
979 979 renames[new] = old
980 980 return renames
981 981
982 982 def _markchanges(repo, unknown, deleted, renames):
983 983 '''Marks the files in unknown as added, the files in deleted as removed,
984 984 and the files in renames as copied.'''
985 985 wctx = repo[None]
986 986 with repo.wlock():
987 987 wctx.forget(deleted)
988 988 wctx.add(unknown)
989 989 for new, old in renames.iteritems():
990 990 wctx.copy(old, new)
991 991
992 992 def dirstatecopy(ui, repo, wctx, src, dst, dryrun=False, cwd=None):
993 993 """Update the dirstate to reflect the intent of copying src to dst. For
994 994 different reasons it might not end with dst being marked as copied from src.
995 995 """
996 996 origsrc = repo.dirstate.copied(src) or src
997 997 if dst == origsrc: # copying back a copy?
998 998 if repo.dirstate[dst] not in 'mn' and not dryrun:
999 999 repo.dirstate.normallookup(dst)
1000 1000 else:
1001 1001 if repo.dirstate[origsrc] == 'a' and origsrc == src:
1002 1002 if not ui.quiet:
1003 1003 ui.warn(_("%s has not been committed yet, so no copy "
1004 1004 "data will be stored for %s.\n")
1005 1005 % (repo.pathto(origsrc, cwd), repo.pathto(dst, cwd)))
1006 1006 if repo.dirstate[dst] in '?r' and not dryrun:
1007 1007 wctx.add([dst])
1008 1008 elif not dryrun:
1009 1009 wctx.copy(origsrc, dst)
1010 1010
1011 1011 def readrequires(opener, supported):
1012 1012 '''Reads and parses .hg/requires and checks if all entries found
1013 1013 are in the list of supported features.'''
1014 1014 requirements = set(opener.read("requires").splitlines())
1015 1015 missings = []
1016 1016 for r in requirements:
1017 1017 if r not in supported:
1018 1018 if not r or not r[0].isalnum():
1019 1019 raise error.RequirementError(_(".hg/requires file is corrupt"))
1020 1020 missings.append(r)
1021 1021 missings.sort()
1022 1022 if missings:
1023 1023 raise error.RequirementError(
1024 1024 _("repository requires features unknown to this Mercurial: %s")
1025 1025 % " ".join(missings),
1026 1026 hint=_("see https://mercurial-scm.org/wiki/MissingRequirement"
1027 1027 " for more information"))
1028 1028 return requirements
1029 1029
1030 1030 def writerequires(opener, requirements):
1031 1031 with opener('requires', 'w') as fp:
1032 1032 for r in sorted(requirements):
1033 1033 fp.write("%s\n" % r)
1034 1034
1035 1035 class filecachesubentry(object):
1036 1036 def __init__(self, path, stat):
1037 1037 self.path = path
1038 1038 self.cachestat = None
1039 1039 self._cacheable = None
1040 1040
1041 1041 if stat:
1042 1042 self.cachestat = filecachesubentry.stat(self.path)
1043 1043
1044 1044 if self.cachestat:
1045 1045 self._cacheable = self.cachestat.cacheable()
1046 1046 else:
1047 1047 # None means we don't know yet
1048 1048 self._cacheable = None
1049 1049
1050 1050 def refresh(self):
1051 1051 if self.cacheable():
1052 1052 self.cachestat = filecachesubentry.stat(self.path)
1053 1053
1054 1054 def cacheable(self):
1055 1055 if self._cacheable is not None:
1056 1056 return self._cacheable
1057 1057
1058 1058 # we don't know yet, assume it is for now
1059 1059 return True
1060 1060
1061 1061 def changed(self):
1062 1062 # no point in going further if we can't cache it
1063 1063 if not self.cacheable():
1064 1064 return True
1065 1065
1066 1066 newstat = filecachesubentry.stat(self.path)
1067 1067
1068 1068 # we may not know if it's cacheable yet, check again now
1069 1069 if newstat and self._cacheable is None:
1070 1070 self._cacheable = newstat.cacheable()
1071 1071
1072 1072 # check again
1073 1073 if not self._cacheable:
1074 1074 return True
1075 1075
1076 1076 if self.cachestat != newstat:
1077 1077 self.cachestat = newstat
1078 1078 return True
1079 1079 else:
1080 1080 return False
1081 1081
1082 1082 @staticmethod
1083 1083 def stat(path):
1084 1084 try:
1085 1085 return util.cachestat(path)
1086 1086 except OSError as e:
1087 1087 if e.errno != errno.ENOENT:
1088 1088 raise
1089 1089
1090 1090 class filecacheentry(object):
1091 1091 def __init__(self, paths, stat=True):
1092 1092 self._entries = []
1093 1093 for path in paths:
1094 1094 self._entries.append(filecachesubentry(path, stat))
1095 1095
1096 1096 def changed(self):
1097 1097 '''true if any entry has changed'''
1098 1098 for entry in self._entries:
1099 1099 if entry.changed():
1100 1100 return True
1101 1101 return False
1102 1102
1103 1103 def refresh(self):
1104 1104 for entry in self._entries:
1105 1105 entry.refresh()
1106 1106
1107 1107 class filecache(object):
1108 1108 '''A property like decorator that tracks files under .hg/ for updates.
1109 1109
1110 1110 Records stat info when called in _filecache.
1111 1111
1112 1112 On subsequent calls, compares old stat info with new info, and recreates the
1113 1113 object when any of the files changes, updating the new stat info in
1114 1114 _filecache.
1115 1115
1116 1116 Mercurial either atomic renames or appends for files under .hg,
1117 1117 so to ensure the cache is reliable we need the filesystem to be able
1118 1118 to tell us if a file has been replaced. If it can't, we fallback to
1119 1119 recreating the object on every call (essentially the same behavior as
1120 1120 propertycache).
1121 1121
1122 1122 '''
1123 1123 def __init__(self, *paths):
1124 1124 self.paths = paths
1125 1125
1126 1126 def join(self, obj, fname):
1127 1127 """Used to compute the runtime path of a cached file.
1128 1128
1129 1129 Users should subclass filecache and provide their own version of this
1130 1130 function to call the appropriate join function on 'obj' (an instance
1131 1131 of the class that its member function was decorated).
1132 1132 """
1133 1133 return obj.join(fname)
1134 1134
1135 1135 def __call__(self, func):
1136 1136 self.func = func
1137 1137 self.name = func.__name__
1138 1138 return self
1139 1139
1140 1140 def __get__(self, obj, type=None):
1141 1141 # do we need to check if the file changed?
1142 1142 if self.name in obj.__dict__:
1143 1143 assert self.name in obj._filecache, self.name
1144 1144 return obj.__dict__[self.name]
1145 1145
1146 1146 entry = obj._filecache.get(self.name)
1147 1147
1148 1148 if entry:
1149 1149 if entry.changed():
1150 1150 entry.obj = self.func(obj)
1151 1151 else:
1152 1152 paths = [self.join(obj, path) for path in self.paths]
1153 1153
1154 1154 # We stat -before- creating the object so our cache doesn't lie if
1155 1155 # a writer modified between the time we read and stat
1156 1156 entry = filecacheentry(paths, True)
1157 1157 entry.obj = self.func(obj)
1158 1158
1159 1159 obj._filecache[self.name] = entry
1160 1160
1161 1161 obj.__dict__[self.name] = entry.obj
1162 1162 return entry.obj
1163 1163
1164 1164 def __set__(self, obj, value):
1165 1165 if self.name not in obj._filecache:
1166 1166 # we add an entry for the missing value because X in __dict__
1167 1167 # implies X in _filecache
1168 1168 paths = [self.join(obj, path) for path in self.paths]
1169 1169 ce = filecacheentry(paths, False)
1170 1170 obj._filecache[self.name] = ce
1171 1171 else:
1172 1172 ce = obj._filecache[self.name]
1173 1173
1174 1174 ce.obj = value # update cached copy
1175 1175 obj.__dict__[self.name] = value # update copy returned by obj.x
1176 1176
1177 1177 def __delete__(self, obj):
1178 1178 try:
1179 1179 del obj.__dict__[self.name]
1180 1180 except KeyError:
1181 1181 raise AttributeError(self.name)
1182 1182
1183 1183 def _locksub(repo, lock, envvar, cmd, environ=None, *args, **kwargs):
1184 1184 if lock is None:
1185 1185 raise error.LockInheritanceContractViolation(
1186 1186 'lock can only be inherited while held')
1187 1187 if environ is None:
1188 1188 environ = {}
1189 1189 with lock.inherit() as locker:
1190 1190 environ[envvar] = locker
1191 1191 return repo.ui.system(cmd, environ=environ, *args, **kwargs)
1192 1192
1193 1193 def wlocksub(repo, cmd, *args, **kwargs):
1194 1194 """run cmd as a subprocess that allows inheriting repo's wlock
1195 1195
1196 1196 This can only be called while the wlock is held. This takes all the
1197 1197 arguments that ui.system does, and returns the exit code of the
1198 1198 subprocess."""
1199 1199 return _locksub(repo, repo.currentwlock(), 'HG_WLOCK_LOCKER', cmd, *args,
1200 1200 **kwargs)
1201 1201
1202 1202 def gdinitconfig(ui):
1203 1203 """helper function to know if a repo should be created as general delta
1204 1204 """
1205 1205 # experimental config: format.generaldelta
1206 1206 return (ui.configbool('format', 'generaldelta', False)
1207 1207 or ui.configbool('format', 'usegeneraldelta', True))
1208 1208
1209 1209 def gddeltaconfig(ui):
1210 1210 """helper function to know if incoming delta should be optimised
1211 1211 """
1212 1212 # experimental config: format.generaldelta
1213 1213 return ui.configbool('format', 'generaldelta', False)
General Comments 0
You need to be logged in to leave comments. Login now