##// END OF EJS Templates
transaction-summary: display the summary for all transactions...
Boris Feld -
r33541:b47fef6d default
parent child Browse files
Show More
@@ -1,1853 +1,1851 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
156 156 from .i18n import _
157 157 from . import (
158 158 changegroup,
159 159 error,
160 160 obsolete,
161 161 phases,
162 162 pushkey,
163 163 pycompat,
164 scmutil,
165 164 tags,
166 165 url,
167 166 util,
168 167 )
169 168
170 169 urlerr = util.urlerr
171 170 urlreq = util.urlreq
172 171
173 172 _pack = struct.pack
174 173 _unpack = struct.unpack
175 174
176 175 _fstreamparamsize = '>i'
177 176 _fpartheadersize = '>i'
178 177 _fparttypesize = '>B'
179 178 _fpartid = '>I'
180 179 _fpayloadsize = '>i'
181 180 _fpartparamcount = '>BB'
182 181
183 182 _fphasesentry = '>i20s'
184 183
185 184 preferedchunksize = 4096
186 185
187 186 _parttypeforbidden = re.compile('[^a-zA-Z0-9_:-]')
188 187
189 188 def outdebug(ui, message):
190 189 """debug regarding output stream (bundling)"""
191 190 if ui.configbool('devel', 'bundle2.debug'):
192 191 ui.debug('bundle2-output: %s\n' % message)
193 192
194 193 def indebug(ui, message):
195 194 """debug on input stream (unbundling)"""
196 195 if ui.configbool('devel', 'bundle2.debug'):
197 196 ui.debug('bundle2-input: %s\n' % message)
198 197
199 198 def validateparttype(parttype):
200 199 """raise ValueError if a parttype contains invalid character"""
201 200 if _parttypeforbidden.search(parttype):
202 201 raise ValueError(parttype)
203 202
204 203 def _makefpartparamsizes(nbparams):
205 204 """return a struct format to read part parameter sizes
206 205
207 206 The number parameters is variable so we need to build that format
208 207 dynamically.
209 208 """
210 209 return '>'+('BB'*nbparams)
211 210
212 211 parthandlermapping = {}
213 212
214 213 def parthandler(parttype, params=()):
215 214 """decorator that register a function as a bundle2 part handler
216 215
217 216 eg::
218 217
219 218 @parthandler('myparttype', ('mandatory', 'param', 'handled'))
220 219 def myparttypehandler(...):
221 220 '''process a part of type "my part".'''
222 221 ...
223 222 """
224 223 validateparttype(parttype)
225 224 def _decorator(func):
226 225 lparttype = parttype.lower() # enforce lower case matching.
227 226 assert lparttype not in parthandlermapping
228 227 parthandlermapping[lparttype] = func
229 228 func.params = frozenset(params)
230 229 return func
231 230 return _decorator
232 231
233 232 class unbundlerecords(object):
234 233 """keep record of what happens during and unbundle
235 234
236 235 New records are added using `records.add('cat', obj)`. Where 'cat' is a
237 236 category of record and obj is an arbitrary object.
238 237
239 238 `records['cat']` will return all entries of this category 'cat'.
240 239
241 240 Iterating on the object itself will yield `('category', obj)` tuples
242 241 for all entries.
243 242
244 243 All iterations happens in chronological order.
245 244 """
246 245
247 246 def __init__(self):
248 247 self._categories = {}
249 248 self._sequences = []
250 249 self._replies = {}
251 250
252 251 def add(self, category, entry, inreplyto=None):
253 252 """add a new record of a given category.
254 253
255 254 The entry can then be retrieved in the list returned by
256 255 self['category']."""
257 256 self._categories.setdefault(category, []).append(entry)
258 257 self._sequences.append((category, entry))
259 258 if inreplyto is not None:
260 259 self.getreplies(inreplyto).add(category, entry)
261 260
262 261 def getreplies(self, partid):
263 262 """get the records that are replies to a specific part"""
264 263 return self._replies.setdefault(partid, unbundlerecords())
265 264
266 265 def __getitem__(self, cat):
267 266 return tuple(self._categories.get(cat, ()))
268 267
269 268 def __iter__(self):
270 269 return iter(self._sequences)
271 270
272 271 def __len__(self):
273 272 return len(self._sequences)
274 273
275 274 def __nonzero__(self):
276 275 return bool(self._sequences)
277 276
278 277 __bool__ = __nonzero__
279 278
280 279 class bundleoperation(object):
281 280 """an object that represents a single bundling process
282 281
283 282 Its purpose is to carry unbundle-related objects and states.
284 283
285 284 A new object should be created at the beginning of each bundle processing.
286 285 The object is to be returned by the processing function.
287 286
288 287 The object has very little content now it will ultimately contain:
289 288 * an access to the repo the bundle is applied to,
290 289 * a ui object,
291 290 * a way to retrieve a transaction to add changes to the repo,
292 291 * a way to record the result of processing each part,
293 292 * a way to construct a bundle response when applicable.
294 293 """
295 294
296 295 def __init__(self, repo, transactiongetter, captureoutput=True):
297 296 self.repo = repo
298 297 self.ui = repo.ui
299 298 self.records = unbundlerecords()
300 299 self.gettransaction = transactiongetter
301 300 self.reply = None
302 301 self.captureoutput = captureoutput
303 302
304 303 class TransactionUnavailable(RuntimeError):
305 304 pass
306 305
307 306 def _notransaction():
308 307 """default method to get a transaction while processing a bundle
309 308
310 309 Raise an exception to highlight the fact that no transaction was expected
311 310 to be created"""
312 311 raise TransactionUnavailable()
313 312
314 313 def applybundle(repo, unbundler, tr, source=None, url=None, **kwargs):
315 314 # transform me into unbundler.apply() as soon as the freeze is lifted
316 315 if isinstance(unbundler, unbundle20):
317 316 tr.hookargs['bundle2'] = '1'
318 317 if source is not None and 'source' not in tr.hookargs:
319 318 tr.hookargs['source'] = source
320 319 if url is not None and 'url' not in tr.hookargs:
321 320 tr.hookargs['url'] = url
322 321 return processbundle(repo, unbundler, lambda: tr)
323 322 else:
324 323 # the transactiongetter won't be used, but we might as well set it
325 324 op = bundleoperation(repo, lambda: tr)
326 325 _processchangegroup(op, unbundler, tr, source, url, **kwargs)
327 326 return op
328 327
329 328 def processbundle(repo, unbundler, transactiongetter=None, op=None):
330 329 """This function process a bundle, apply effect to/from a repo
331 330
332 331 It iterates over each part then searches for and uses the proper handling
333 332 code to process the part. Parts are processed in order.
334 333
335 334 Unknown Mandatory part will abort the process.
336 335
337 336 It is temporarily possible to provide a prebuilt bundleoperation to the
338 337 function. This is used to ensure output is properly propagated in case of
339 338 an error during the unbundling. This output capturing part will likely be
340 339 reworked and this ability will probably go away in the process.
341 340 """
342 341 if op is None:
343 342 if transactiongetter is None:
344 343 transactiongetter = _notransaction
345 344 op = bundleoperation(repo, transactiongetter)
346 345 # todo:
347 346 # - replace this is a init function soon.
348 347 # - exception catching
349 348 unbundler.params
350 349 if repo.ui.debugflag:
351 350 msg = ['bundle2-input-bundle:']
352 351 if unbundler.params:
353 352 msg.append(' %i params' % len(unbundler.params))
354 353 if op.gettransaction is None or op.gettransaction is _notransaction:
355 354 msg.append(' no-transaction')
356 355 else:
357 356 msg.append(' with-transaction')
358 357 msg.append('\n')
359 358 repo.ui.debug(''.join(msg))
360 359 iterparts = enumerate(unbundler.iterparts())
361 360 part = None
362 361 nbpart = 0
363 362 try:
364 363 for nbpart, part in iterparts:
365 364 _processpart(op, part)
366 365 except Exception as exc:
367 366 # Any exceptions seeking to the end of the bundle at this point are
368 367 # almost certainly related to the underlying stream being bad.
369 368 # And, chances are that the exception we're handling is related to
370 369 # getting in that bad state. So, we swallow the seeking error and
371 370 # re-raise the original error.
372 371 seekerror = False
373 372 try:
374 373 for nbpart, part in iterparts:
375 374 # consume the bundle content
376 375 part.seek(0, 2)
377 376 except Exception:
378 377 seekerror = True
379 378
380 379 # Small hack to let caller code distinguish exceptions from bundle2
381 380 # processing from processing the old format. This is mostly
382 381 # needed to handle different return codes to unbundle according to the
383 382 # type of bundle. We should probably clean up or drop this return code
384 383 # craziness in a future version.
385 384 exc.duringunbundle2 = True
386 385 salvaged = []
387 386 replycaps = None
388 387 if op.reply is not None:
389 388 salvaged = op.reply.salvageoutput()
390 389 replycaps = op.reply.capabilities
391 390 exc._replycaps = replycaps
392 391 exc._bundle2salvagedoutput = salvaged
393 392
394 393 # Re-raising from a variable loses the original stack. So only use
395 394 # that form if we need to.
396 395 if seekerror:
397 396 raise exc
398 397 else:
399 398 raise
400 399 finally:
401 400 repo.ui.debug('bundle2-input-bundle: %i parts total\n' % nbpart)
402 401
403 402 return op
404 403
405 404 def _processchangegroup(op, cg, tr, source, url, **kwargs):
406 405 ret = cg.apply(op.repo, tr, source, url, **kwargs)
407 406 op.records.add('changegroup', {
408 407 'return': ret,
409 408 })
410 409 return ret
411 410
412 411 def _processpart(op, part):
413 412 """process a single part from a bundle
414 413
415 414 The part is guaranteed to have been fully consumed when the function exits
416 415 (even if an exception is raised)."""
417 416 status = 'unknown' # used by debug output
418 417 hardabort = False
419 418 try:
420 419 try:
421 420 handler = parthandlermapping.get(part.type)
422 421 if handler is None:
423 422 status = 'unsupported-type'
424 423 raise error.BundleUnknownFeatureError(parttype=part.type)
425 424 indebug(op.ui, 'found a handler for part %r' % part.type)
426 425 unknownparams = part.mandatorykeys - handler.params
427 426 if unknownparams:
428 427 unknownparams = list(unknownparams)
429 428 unknownparams.sort()
430 429 status = 'unsupported-params (%s)' % unknownparams
431 430 raise error.BundleUnknownFeatureError(parttype=part.type,
432 431 params=unknownparams)
433 432 status = 'supported'
434 433 except error.BundleUnknownFeatureError as exc:
435 434 if part.mandatory: # mandatory parts
436 435 raise
437 436 indebug(op.ui, 'ignoring unsupported advisory part %s' % exc)
438 437 return # skip to part processing
439 438 finally:
440 439 if op.ui.debugflag:
441 440 msg = ['bundle2-input-part: "%s"' % part.type]
442 441 if not part.mandatory:
443 442 msg.append(' (advisory)')
444 443 nbmp = len(part.mandatorykeys)
445 444 nbap = len(part.params) - nbmp
446 445 if nbmp or nbap:
447 446 msg.append(' (params:')
448 447 if nbmp:
449 448 msg.append(' %i mandatory' % nbmp)
450 449 if nbap:
451 450 msg.append(' %i advisory' % nbmp)
452 451 msg.append(')')
453 452 msg.append(' %s\n' % status)
454 453 op.ui.debug(''.join(msg))
455 454
456 455 # handler is called outside the above try block so that we don't
457 456 # risk catching KeyErrors from anything other than the
458 457 # parthandlermapping lookup (any KeyError raised by handler()
459 458 # itself represents a defect of a different variety).
460 459 output = None
461 460 if op.captureoutput and op.reply is not None:
462 461 op.ui.pushbuffer(error=True, subproc=True)
463 462 output = ''
464 463 try:
465 464 handler(op, part)
466 465 finally:
467 466 if output is not None:
468 467 output = op.ui.popbuffer()
469 468 if output:
470 469 outpart = op.reply.newpart('output', data=output,
471 470 mandatory=False)
472 471 outpart.addparam('in-reply-to', str(part.id), mandatory=False)
473 472 # If exiting or interrupted, do not attempt to seek the stream in the
474 473 # finally block below. This makes abort faster.
475 474 except (SystemExit, KeyboardInterrupt):
476 475 hardabort = True
477 476 raise
478 477 finally:
479 478 # consume the part content to not corrupt the stream.
480 479 if not hardabort:
481 480 part.seek(0, 2)
482 481
483 482
484 483 def decodecaps(blob):
485 484 """decode a bundle2 caps bytes blob into a dictionary
486 485
487 486 The blob is a list of capabilities (one per line)
488 487 Capabilities may have values using a line of the form::
489 488
490 489 capability=value1,value2,value3
491 490
492 491 The values are always a list."""
493 492 caps = {}
494 493 for line in blob.splitlines():
495 494 if not line:
496 495 continue
497 496 if '=' not in line:
498 497 key, vals = line, ()
499 498 else:
500 499 key, vals = line.split('=', 1)
501 500 vals = vals.split(',')
502 501 key = urlreq.unquote(key)
503 502 vals = [urlreq.unquote(v) for v in vals]
504 503 caps[key] = vals
505 504 return caps
506 505
507 506 def encodecaps(caps):
508 507 """encode a bundle2 caps dictionary into a bytes blob"""
509 508 chunks = []
510 509 for ca in sorted(caps):
511 510 vals = caps[ca]
512 511 ca = urlreq.quote(ca)
513 512 vals = [urlreq.quote(v) for v in vals]
514 513 if vals:
515 514 ca = "%s=%s" % (ca, ','.join(vals))
516 515 chunks.append(ca)
517 516 return '\n'.join(chunks)
518 517
519 518 bundletypes = {
520 519 "": ("", 'UN'), # only when using unbundle on ssh and old http servers
521 520 # since the unification ssh accepts a header but there
522 521 # is no capability signaling it.
523 522 "HG20": (), # special-cased below
524 523 "HG10UN": ("HG10UN", 'UN'),
525 524 "HG10BZ": ("HG10", 'BZ'),
526 525 "HG10GZ": ("HG10GZ", 'GZ'),
527 526 }
528 527
529 528 # hgweb uses this list to communicate its preferred type
530 529 bundlepriority = ['HG10GZ', 'HG10BZ', 'HG10UN']
531 530
532 531 class bundle20(object):
533 532 """represent an outgoing bundle2 container
534 533
535 534 Use the `addparam` method to add stream level parameter. and `newpart` to
536 535 populate it. Then call `getchunks` to retrieve all the binary chunks of
537 536 data that compose the bundle2 container."""
538 537
539 538 _magicstring = 'HG20'
540 539
541 540 def __init__(self, ui, capabilities=()):
542 541 self.ui = ui
543 542 self._params = []
544 543 self._parts = []
545 544 self.capabilities = dict(capabilities)
546 545 self._compengine = util.compengines.forbundletype('UN')
547 546 self._compopts = None
548 547
549 548 def setcompression(self, alg, compopts=None):
550 549 """setup core part compression to <alg>"""
551 550 if alg in (None, 'UN'):
552 551 return
553 552 assert not any(n.lower() == 'compression' for n, v in self._params)
554 553 self.addparam('Compression', alg)
555 554 self._compengine = util.compengines.forbundletype(alg)
556 555 self._compopts = compopts
557 556
558 557 @property
559 558 def nbparts(self):
560 559 """total number of parts added to the bundler"""
561 560 return len(self._parts)
562 561
563 562 # methods used to defines the bundle2 content
564 563 def addparam(self, name, value=None):
565 564 """add a stream level parameter"""
566 565 if not name:
567 566 raise ValueError('empty parameter name')
568 567 if name[0] not in string.letters:
569 568 raise ValueError('non letter first character: %r' % name)
570 569 self._params.append((name, value))
571 570
572 571 def addpart(self, part):
573 572 """add a new part to the bundle2 container
574 573
575 574 Parts contains the actual applicative payload."""
576 575 assert part.id is None
577 576 part.id = len(self._parts) # very cheap counter
578 577 self._parts.append(part)
579 578
580 579 def newpart(self, typeid, *args, **kwargs):
581 580 """create a new part and add it to the containers
582 581
583 582 As the part is directly added to the containers. For now, this means
584 583 that any failure to properly initialize the part after calling
585 584 ``newpart`` should result in a failure of the whole bundling process.
586 585
587 586 You can still fall back to manually create and add if you need better
588 587 control."""
589 588 part = bundlepart(typeid, *args, **kwargs)
590 589 self.addpart(part)
591 590 return part
592 591
593 592 # methods used to generate the bundle2 stream
594 593 def getchunks(self):
595 594 if self.ui.debugflag:
596 595 msg = ['bundle2-output-bundle: "%s",' % self._magicstring]
597 596 if self._params:
598 597 msg.append(' (%i params)' % len(self._params))
599 598 msg.append(' %i parts total\n' % len(self._parts))
600 599 self.ui.debug(''.join(msg))
601 600 outdebug(self.ui, 'start emission of %s stream' % self._magicstring)
602 601 yield self._magicstring
603 602 param = self._paramchunk()
604 603 outdebug(self.ui, 'bundle parameter: %s' % param)
605 604 yield _pack(_fstreamparamsize, len(param))
606 605 if param:
607 606 yield param
608 607 for chunk in self._compengine.compressstream(self._getcorechunk(),
609 608 self._compopts):
610 609 yield chunk
611 610
612 611 def _paramchunk(self):
613 612 """return a encoded version of all stream parameters"""
614 613 blocks = []
615 614 for par, value in self._params:
616 615 par = urlreq.quote(par)
617 616 if value is not None:
618 617 value = urlreq.quote(value)
619 618 par = '%s=%s' % (par, value)
620 619 blocks.append(par)
621 620 return ' '.join(blocks)
622 621
623 622 def _getcorechunk(self):
624 623 """yield chunk for the core part of the bundle
625 624
626 625 (all but headers and parameters)"""
627 626 outdebug(self.ui, 'start of parts')
628 627 for part in self._parts:
629 628 outdebug(self.ui, 'bundle part: "%s"' % part.type)
630 629 for chunk in part.getchunks(ui=self.ui):
631 630 yield chunk
632 631 outdebug(self.ui, 'end of bundle')
633 632 yield _pack(_fpartheadersize, 0)
634 633
635 634
636 635 def salvageoutput(self):
637 636 """return a list with a copy of all output parts in the bundle
638 637
639 638 This is meant to be used during error handling to make sure we preserve
640 639 server output"""
641 640 salvaged = []
642 641 for part in self._parts:
643 642 if part.type.startswith('output'):
644 643 salvaged.append(part.copy())
645 644 return salvaged
646 645
647 646
648 647 class unpackermixin(object):
649 648 """A mixin to extract bytes and struct data from a stream"""
650 649
651 650 def __init__(self, fp):
652 651 self._fp = fp
653 652
654 653 def _unpack(self, format):
655 654 """unpack this struct format from the stream
656 655
657 656 This method is meant for internal usage by the bundle2 protocol only.
658 657 They directly manipulate the low level stream including bundle2 level
659 658 instruction.
660 659
661 660 Do not use it to implement higher-level logic or methods."""
662 661 data = self._readexact(struct.calcsize(format))
663 662 return _unpack(format, data)
664 663
665 664 def _readexact(self, size):
666 665 """read exactly <size> bytes from the stream
667 666
668 667 This method is meant for internal usage by the bundle2 protocol only.
669 668 They directly manipulate the low level stream including bundle2 level
670 669 instruction.
671 670
672 671 Do not use it to implement higher-level logic or methods."""
673 672 return changegroup.readexactly(self._fp, size)
674 673
675 674 def getunbundler(ui, fp, magicstring=None):
676 675 """return a valid unbundler object for a given magicstring"""
677 676 if magicstring is None:
678 677 magicstring = changegroup.readexactly(fp, 4)
679 678 magic, version = magicstring[0:2], magicstring[2:4]
680 679 if magic != 'HG':
681 680 ui.debug(
682 681 "error: invalid magic: %r (version %r), should be 'HG'\n"
683 682 % (magic, version))
684 683 raise error.Abort(_('not a Mercurial bundle'))
685 684 unbundlerclass = formatmap.get(version)
686 685 if unbundlerclass is None:
687 686 raise error.Abort(_('unknown bundle version %s') % version)
688 687 unbundler = unbundlerclass(ui, fp)
689 688 indebug(ui, 'start processing of %s stream' % magicstring)
690 689 return unbundler
691 690
692 691 class unbundle20(unpackermixin):
693 692 """interpret a bundle2 stream
694 693
695 694 This class is fed with a binary stream and yields parts through its
696 695 `iterparts` methods."""
697 696
698 697 _magicstring = 'HG20'
699 698
700 699 def __init__(self, ui, fp):
701 700 """If header is specified, we do not read it out of the stream."""
702 701 self.ui = ui
703 702 self._compengine = util.compengines.forbundletype('UN')
704 703 self._compressed = None
705 704 super(unbundle20, self).__init__(fp)
706 705
707 706 @util.propertycache
708 707 def params(self):
709 708 """dictionary of stream level parameters"""
710 709 indebug(self.ui, 'reading bundle2 stream parameters')
711 710 params = {}
712 711 paramssize = self._unpack(_fstreamparamsize)[0]
713 712 if paramssize < 0:
714 713 raise error.BundleValueError('negative bundle param size: %i'
715 714 % paramssize)
716 715 if paramssize:
717 716 params = self._readexact(paramssize)
718 717 params = self._processallparams(params)
719 718 return params
720 719
721 720 def _processallparams(self, paramsblock):
722 721 """"""
723 722 params = util.sortdict()
724 723 for p in paramsblock.split(' '):
725 724 p = p.split('=', 1)
726 725 p = [urlreq.unquote(i) for i in p]
727 726 if len(p) < 2:
728 727 p.append(None)
729 728 self._processparam(*p)
730 729 params[p[0]] = p[1]
731 730 return params
732 731
733 732
734 733 def _processparam(self, name, value):
735 734 """process a parameter, applying its effect if needed
736 735
737 736 Parameter starting with a lower case letter are advisory and will be
738 737 ignored when unknown. Those starting with an upper case letter are
739 738 mandatory and will this function will raise a KeyError when unknown.
740 739
741 740 Note: no option are currently supported. Any input will be either
742 741 ignored or failing.
743 742 """
744 743 if not name:
745 744 raise ValueError('empty parameter name')
746 745 if name[0] not in string.letters:
747 746 raise ValueError('non letter first character: %r' % name)
748 747 try:
749 748 handler = b2streamparamsmap[name.lower()]
750 749 except KeyError:
751 750 if name[0].islower():
752 751 indebug(self.ui, "ignoring unknown parameter %r" % name)
753 752 else:
754 753 raise error.BundleUnknownFeatureError(params=(name,))
755 754 else:
756 755 handler(self, name, value)
757 756
758 757 def _forwardchunks(self):
759 758 """utility to transfer a bundle2 as binary
760 759
761 760 This is made necessary by the fact the 'getbundle' command over 'ssh'
762 761 have no way to know then the reply end, relying on the bundle to be
763 762 interpreted to know its end. This is terrible and we are sorry, but we
764 763 needed to move forward to get general delta enabled.
765 764 """
766 765 yield self._magicstring
767 766 assert 'params' not in vars(self)
768 767 paramssize = self._unpack(_fstreamparamsize)[0]
769 768 if paramssize < 0:
770 769 raise error.BundleValueError('negative bundle param size: %i'
771 770 % paramssize)
772 771 yield _pack(_fstreamparamsize, paramssize)
773 772 if paramssize:
774 773 params = self._readexact(paramssize)
775 774 self._processallparams(params)
776 775 yield params
777 776 assert self._compengine.bundletype == 'UN'
778 777 # From there, payload might need to be decompressed
779 778 self._fp = self._compengine.decompressorreader(self._fp)
780 779 emptycount = 0
781 780 while emptycount < 2:
782 781 # so we can brainlessly loop
783 782 assert _fpartheadersize == _fpayloadsize
784 783 size = self._unpack(_fpartheadersize)[0]
785 784 yield _pack(_fpartheadersize, size)
786 785 if size:
787 786 emptycount = 0
788 787 else:
789 788 emptycount += 1
790 789 continue
791 790 if size == flaginterrupt:
792 791 continue
793 792 elif size < 0:
794 793 raise error.BundleValueError('negative chunk size: %i')
795 794 yield self._readexact(size)
796 795
797 796
798 797 def iterparts(self):
799 798 """yield all parts contained in the stream"""
800 799 # make sure param have been loaded
801 800 self.params
802 801 # From there, payload need to be decompressed
803 802 self._fp = self._compengine.decompressorreader(self._fp)
804 803 indebug(self.ui, 'start extraction of bundle2 parts')
805 804 headerblock = self._readpartheader()
806 805 while headerblock is not None:
807 806 part = unbundlepart(self.ui, headerblock, self._fp)
808 807 yield part
809 808 part.seek(0, 2)
810 809 headerblock = self._readpartheader()
811 810 indebug(self.ui, 'end of bundle2 stream')
812 811
813 812 def _readpartheader(self):
814 813 """reads a part header size and return the bytes blob
815 814
816 815 returns None if empty"""
817 816 headersize = self._unpack(_fpartheadersize)[0]
818 817 if headersize < 0:
819 818 raise error.BundleValueError('negative part header size: %i'
820 819 % headersize)
821 820 indebug(self.ui, 'part header size: %i' % headersize)
822 821 if headersize:
823 822 return self._readexact(headersize)
824 823 return None
825 824
826 825 def compressed(self):
827 826 self.params # load params
828 827 return self._compressed
829 828
830 829 def close(self):
831 830 """close underlying file"""
832 831 if util.safehasattr(self._fp, 'close'):
833 832 return self._fp.close()
834 833
835 834 formatmap = {'20': unbundle20}
836 835
837 836 b2streamparamsmap = {}
838 837
839 838 def b2streamparamhandler(name):
840 839 """register a handler for a stream level parameter"""
841 840 def decorator(func):
842 841 assert name not in formatmap
843 842 b2streamparamsmap[name] = func
844 843 return func
845 844 return decorator
846 845
847 846 @b2streamparamhandler('compression')
848 847 def processcompression(unbundler, param, value):
849 848 """read compression parameter and install payload decompression"""
850 849 if value not in util.compengines.supportedbundletypes:
851 850 raise error.BundleUnknownFeatureError(params=(param,),
852 851 values=(value,))
853 852 unbundler._compengine = util.compengines.forbundletype(value)
854 853 if value is not None:
855 854 unbundler._compressed = True
856 855
857 856 class bundlepart(object):
858 857 """A bundle2 part contains application level payload
859 858
860 859 The part `type` is used to route the part to the application level
861 860 handler.
862 861
863 862 The part payload is contained in ``part.data``. It could be raw bytes or a
864 863 generator of byte chunks.
865 864
866 865 You can add parameters to the part using the ``addparam`` method.
867 866 Parameters can be either mandatory (default) or advisory. Remote side
868 867 should be able to safely ignore the advisory ones.
869 868
870 869 Both data and parameters cannot be modified after the generation has begun.
871 870 """
872 871
873 872 def __init__(self, parttype, mandatoryparams=(), advisoryparams=(),
874 873 data='', mandatory=True):
875 874 validateparttype(parttype)
876 875 self.id = None
877 876 self.type = parttype
878 877 self._data = data
879 878 self._mandatoryparams = list(mandatoryparams)
880 879 self._advisoryparams = list(advisoryparams)
881 880 # checking for duplicated entries
882 881 self._seenparams = set()
883 882 for pname, __ in self._mandatoryparams + self._advisoryparams:
884 883 if pname in self._seenparams:
885 884 raise error.ProgrammingError('duplicated params: %s' % pname)
886 885 self._seenparams.add(pname)
887 886 # status of the part's generation:
888 887 # - None: not started,
889 888 # - False: currently generated,
890 889 # - True: generation done.
891 890 self._generated = None
892 891 self.mandatory = mandatory
893 892
894 893 def __repr__(self):
895 894 cls = "%s.%s" % (self.__class__.__module__, self.__class__.__name__)
896 895 return ('<%s object at %x; id: %s; type: %s; mandatory: %s>'
897 896 % (cls, id(self), self.id, self.type, self.mandatory))
898 897
899 898 def copy(self):
900 899 """return a copy of the part
901 900
902 901 The new part have the very same content but no partid assigned yet.
903 902 Parts with generated data cannot be copied."""
904 903 assert not util.safehasattr(self.data, 'next')
905 904 return self.__class__(self.type, self._mandatoryparams,
906 905 self._advisoryparams, self._data, self.mandatory)
907 906
908 907 # methods used to defines the part content
909 908 @property
910 909 def data(self):
911 910 return self._data
912 911
913 912 @data.setter
914 913 def data(self, data):
915 914 if self._generated is not None:
916 915 raise error.ReadOnlyPartError('part is being generated')
917 916 self._data = data
918 917
919 918 @property
920 919 def mandatoryparams(self):
921 920 # make it an immutable tuple to force people through ``addparam``
922 921 return tuple(self._mandatoryparams)
923 922
924 923 @property
925 924 def advisoryparams(self):
926 925 # make it an immutable tuple to force people through ``addparam``
927 926 return tuple(self._advisoryparams)
928 927
929 928 def addparam(self, name, value='', mandatory=True):
930 929 """add a parameter to the part
931 930
932 931 If 'mandatory' is set to True, the remote handler must claim support
933 932 for this parameter or the unbundling will be aborted.
934 933
935 934 The 'name' and 'value' cannot exceed 255 bytes each.
936 935 """
937 936 if self._generated is not None:
938 937 raise error.ReadOnlyPartError('part is being generated')
939 938 if name in self._seenparams:
940 939 raise ValueError('duplicated params: %s' % name)
941 940 self._seenparams.add(name)
942 941 params = self._advisoryparams
943 942 if mandatory:
944 943 params = self._mandatoryparams
945 944 params.append((name, value))
946 945
947 946 # methods used to generates the bundle2 stream
948 947 def getchunks(self, ui):
949 948 if self._generated is not None:
950 949 raise error.ProgrammingError('part can only be consumed once')
951 950 self._generated = False
952 951
953 952 if ui.debugflag:
954 953 msg = ['bundle2-output-part: "%s"' % self.type]
955 954 if not self.mandatory:
956 955 msg.append(' (advisory)')
957 956 nbmp = len(self.mandatoryparams)
958 957 nbap = len(self.advisoryparams)
959 958 if nbmp or nbap:
960 959 msg.append(' (params:')
961 960 if nbmp:
962 961 msg.append(' %i mandatory' % nbmp)
963 962 if nbap:
964 963 msg.append(' %i advisory' % nbmp)
965 964 msg.append(')')
966 965 if not self.data:
967 966 msg.append(' empty payload')
968 967 elif util.safehasattr(self.data, 'next'):
969 968 msg.append(' streamed payload')
970 969 else:
971 970 msg.append(' %i bytes payload' % len(self.data))
972 971 msg.append('\n')
973 972 ui.debug(''.join(msg))
974 973
975 974 #### header
976 975 if self.mandatory:
977 976 parttype = self.type.upper()
978 977 else:
979 978 parttype = self.type.lower()
980 979 outdebug(ui, 'part %s: "%s"' % (self.id, parttype))
981 980 ## parttype
982 981 header = [_pack(_fparttypesize, len(parttype)),
983 982 parttype, _pack(_fpartid, self.id),
984 983 ]
985 984 ## parameters
986 985 # count
987 986 manpar = self.mandatoryparams
988 987 advpar = self.advisoryparams
989 988 header.append(_pack(_fpartparamcount, len(manpar), len(advpar)))
990 989 # size
991 990 parsizes = []
992 991 for key, value in manpar:
993 992 parsizes.append(len(key))
994 993 parsizes.append(len(value))
995 994 for key, value in advpar:
996 995 parsizes.append(len(key))
997 996 parsizes.append(len(value))
998 997 paramsizes = _pack(_makefpartparamsizes(len(parsizes) / 2), *parsizes)
999 998 header.append(paramsizes)
1000 999 # key, value
1001 1000 for key, value in manpar:
1002 1001 header.append(key)
1003 1002 header.append(value)
1004 1003 for key, value in advpar:
1005 1004 header.append(key)
1006 1005 header.append(value)
1007 1006 ## finalize header
1008 1007 headerchunk = ''.join(header)
1009 1008 outdebug(ui, 'header chunk size: %i' % len(headerchunk))
1010 1009 yield _pack(_fpartheadersize, len(headerchunk))
1011 1010 yield headerchunk
1012 1011 ## payload
1013 1012 try:
1014 1013 for chunk in self._payloadchunks():
1015 1014 outdebug(ui, 'payload chunk size: %i' % len(chunk))
1016 1015 yield _pack(_fpayloadsize, len(chunk))
1017 1016 yield chunk
1018 1017 except GeneratorExit:
1019 1018 # GeneratorExit means that nobody is listening for our
1020 1019 # results anyway, so just bail quickly rather than trying
1021 1020 # to produce an error part.
1022 1021 ui.debug('bundle2-generatorexit\n')
1023 1022 raise
1024 1023 except BaseException as exc:
1025 1024 # backup exception data for later
1026 1025 ui.debug('bundle2-input-stream-interrupt: encoding exception %s'
1027 1026 % exc)
1028 1027 tb = sys.exc_info()[2]
1029 1028 msg = 'unexpected error: %s' % exc
1030 1029 interpart = bundlepart('error:abort', [('message', msg)],
1031 1030 mandatory=False)
1032 1031 interpart.id = 0
1033 1032 yield _pack(_fpayloadsize, -1)
1034 1033 for chunk in interpart.getchunks(ui=ui):
1035 1034 yield chunk
1036 1035 outdebug(ui, 'closing payload chunk')
1037 1036 # abort current part payload
1038 1037 yield _pack(_fpayloadsize, 0)
1039 1038 pycompat.raisewithtb(exc, tb)
1040 1039 # end of payload
1041 1040 outdebug(ui, 'closing payload chunk')
1042 1041 yield _pack(_fpayloadsize, 0)
1043 1042 self._generated = True
1044 1043
1045 1044 def _payloadchunks(self):
1046 1045 """yield chunks of a the part payload
1047 1046
1048 1047 Exists to handle the different methods to provide data to a part."""
1049 1048 # we only support fixed size data now.
1050 1049 # This will be improved in the future.
1051 1050 if util.safehasattr(self.data, 'next'):
1052 1051 buff = util.chunkbuffer(self.data)
1053 1052 chunk = buff.read(preferedchunksize)
1054 1053 while chunk:
1055 1054 yield chunk
1056 1055 chunk = buff.read(preferedchunksize)
1057 1056 elif len(self.data):
1058 1057 yield self.data
1059 1058
1060 1059
1061 1060 flaginterrupt = -1
1062 1061
1063 1062 class interrupthandler(unpackermixin):
1064 1063 """read one part and process it with restricted capability
1065 1064
1066 1065 This allows to transmit exception raised on the producer size during part
1067 1066 iteration while the consumer is reading a part.
1068 1067
1069 1068 Part processed in this manner only have access to a ui object,"""
1070 1069
1071 1070 def __init__(self, ui, fp):
1072 1071 super(interrupthandler, self).__init__(fp)
1073 1072 self.ui = ui
1074 1073
1075 1074 def _readpartheader(self):
1076 1075 """reads a part header size and return the bytes blob
1077 1076
1078 1077 returns None if empty"""
1079 1078 headersize = self._unpack(_fpartheadersize)[0]
1080 1079 if headersize < 0:
1081 1080 raise error.BundleValueError('negative part header size: %i'
1082 1081 % headersize)
1083 1082 indebug(self.ui, 'part header size: %i\n' % headersize)
1084 1083 if headersize:
1085 1084 return self._readexact(headersize)
1086 1085 return None
1087 1086
1088 1087 def __call__(self):
1089 1088
1090 1089 self.ui.debug('bundle2-input-stream-interrupt:'
1091 1090 ' opening out of band context\n')
1092 1091 indebug(self.ui, 'bundle2 stream interruption, looking for a part.')
1093 1092 headerblock = self._readpartheader()
1094 1093 if headerblock is None:
1095 1094 indebug(self.ui, 'no part found during interruption.')
1096 1095 return
1097 1096 part = unbundlepart(self.ui, headerblock, self._fp)
1098 1097 op = interruptoperation(self.ui)
1099 1098 _processpart(op, part)
1100 1099 self.ui.debug('bundle2-input-stream-interrupt:'
1101 1100 ' closing out of band context\n')
1102 1101
1103 1102 class interruptoperation(object):
1104 1103 """A limited operation to be use by part handler during interruption
1105 1104
1106 1105 It only have access to an ui object.
1107 1106 """
1108 1107
1109 1108 def __init__(self, ui):
1110 1109 self.ui = ui
1111 1110 self.reply = None
1112 1111 self.captureoutput = False
1113 1112
1114 1113 @property
1115 1114 def repo(self):
1116 1115 raise error.ProgrammingError('no repo access from stream interruption')
1117 1116
1118 1117 def gettransaction(self):
1119 1118 raise TransactionUnavailable('no repo access from stream interruption')
1120 1119
1121 1120 class unbundlepart(unpackermixin):
1122 1121 """a bundle part read from a bundle"""
1123 1122
1124 1123 def __init__(self, ui, header, fp):
1125 1124 super(unbundlepart, self).__init__(fp)
1126 1125 self._seekable = (util.safehasattr(fp, 'seek') and
1127 1126 util.safehasattr(fp, 'tell'))
1128 1127 self.ui = ui
1129 1128 # unbundle state attr
1130 1129 self._headerdata = header
1131 1130 self._headeroffset = 0
1132 1131 self._initialized = False
1133 1132 self.consumed = False
1134 1133 # part data
1135 1134 self.id = None
1136 1135 self.type = None
1137 1136 self.mandatoryparams = None
1138 1137 self.advisoryparams = None
1139 1138 self.params = None
1140 1139 self.mandatorykeys = ()
1141 1140 self._payloadstream = None
1142 1141 self._readheader()
1143 1142 self._mandatory = None
1144 1143 self._chunkindex = [] #(payload, file) position tuples for chunk starts
1145 1144 self._pos = 0
1146 1145
1147 1146 def _fromheader(self, size):
1148 1147 """return the next <size> byte from the header"""
1149 1148 offset = self._headeroffset
1150 1149 data = self._headerdata[offset:(offset + size)]
1151 1150 self._headeroffset = offset + size
1152 1151 return data
1153 1152
1154 1153 def _unpackheader(self, format):
1155 1154 """read given format from header
1156 1155
1157 1156 This automatically compute the size of the format to read."""
1158 1157 data = self._fromheader(struct.calcsize(format))
1159 1158 return _unpack(format, data)
1160 1159
1161 1160 def _initparams(self, mandatoryparams, advisoryparams):
1162 1161 """internal function to setup all logic related parameters"""
1163 1162 # make it read only to prevent people touching it by mistake.
1164 1163 self.mandatoryparams = tuple(mandatoryparams)
1165 1164 self.advisoryparams = tuple(advisoryparams)
1166 1165 # user friendly UI
1167 1166 self.params = util.sortdict(self.mandatoryparams)
1168 1167 self.params.update(self.advisoryparams)
1169 1168 self.mandatorykeys = frozenset(p[0] for p in mandatoryparams)
1170 1169
1171 1170 def _payloadchunks(self, chunknum=0):
1172 1171 '''seek to specified chunk and start yielding data'''
1173 1172 if len(self._chunkindex) == 0:
1174 1173 assert chunknum == 0, 'Must start with chunk 0'
1175 1174 self._chunkindex.append((0, self._tellfp()))
1176 1175 else:
1177 1176 assert chunknum < len(self._chunkindex), \
1178 1177 'Unknown chunk %d' % chunknum
1179 1178 self._seekfp(self._chunkindex[chunknum][1])
1180 1179
1181 1180 pos = self._chunkindex[chunknum][0]
1182 1181 payloadsize = self._unpack(_fpayloadsize)[0]
1183 1182 indebug(self.ui, 'payload chunk size: %i' % payloadsize)
1184 1183 while payloadsize:
1185 1184 if payloadsize == flaginterrupt:
1186 1185 # interruption detection, the handler will now read a
1187 1186 # single part and process it.
1188 1187 interrupthandler(self.ui, self._fp)()
1189 1188 elif payloadsize < 0:
1190 1189 msg = 'negative payload chunk size: %i' % payloadsize
1191 1190 raise error.BundleValueError(msg)
1192 1191 else:
1193 1192 result = self._readexact(payloadsize)
1194 1193 chunknum += 1
1195 1194 pos += payloadsize
1196 1195 if chunknum == len(self._chunkindex):
1197 1196 self._chunkindex.append((pos, self._tellfp()))
1198 1197 yield result
1199 1198 payloadsize = self._unpack(_fpayloadsize)[0]
1200 1199 indebug(self.ui, 'payload chunk size: %i' % payloadsize)
1201 1200
1202 1201 def _findchunk(self, pos):
1203 1202 '''for a given payload position, return a chunk number and offset'''
1204 1203 for chunk, (ppos, fpos) in enumerate(self._chunkindex):
1205 1204 if ppos == pos:
1206 1205 return chunk, 0
1207 1206 elif ppos > pos:
1208 1207 return chunk - 1, pos - self._chunkindex[chunk - 1][0]
1209 1208 raise ValueError('Unknown chunk')
1210 1209
1211 1210 def _readheader(self):
1212 1211 """read the header and setup the object"""
1213 1212 typesize = self._unpackheader(_fparttypesize)[0]
1214 1213 self.type = self._fromheader(typesize)
1215 1214 indebug(self.ui, 'part type: "%s"' % self.type)
1216 1215 self.id = self._unpackheader(_fpartid)[0]
1217 1216 indebug(self.ui, 'part id: "%s"' % self.id)
1218 1217 # extract mandatory bit from type
1219 1218 self.mandatory = (self.type != self.type.lower())
1220 1219 self.type = self.type.lower()
1221 1220 ## reading parameters
1222 1221 # param count
1223 1222 mancount, advcount = self._unpackheader(_fpartparamcount)
1224 1223 indebug(self.ui, 'part parameters: %i' % (mancount + advcount))
1225 1224 # param size
1226 1225 fparamsizes = _makefpartparamsizes(mancount + advcount)
1227 1226 paramsizes = self._unpackheader(fparamsizes)
1228 1227 # make it a list of couple again
1229 1228 paramsizes = zip(paramsizes[::2], paramsizes[1::2])
1230 1229 # split mandatory from advisory
1231 1230 mansizes = paramsizes[:mancount]
1232 1231 advsizes = paramsizes[mancount:]
1233 1232 # retrieve param value
1234 1233 manparams = []
1235 1234 for key, value in mansizes:
1236 1235 manparams.append((self._fromheader(key), self._fromheader(value)))
1237 1236 advparams = []
1238 1237 for key, value in advsizes:
1239 1238 advparams.append((self._fromheader(key), self._fromheader(value)))
1240 1239 self._initparams(manparams, advparams)
1241 1240 ## part payload
1242 1241 self._payloadstream = util.chunkbuffer(self._payloadchunks())
1243 1242 # we read the data, tell it
1244 1243 self._initialized = True
1245 1244
1246 1245 def read(self, size=None):
1247 1246 """read payload data"""
1248 1247 if not self._initialized:
1249 1248 self._readheader()
1250 1249 if size is None:
1251 1250 data = self._payloadstream.read()
1252 1251 else:
1253 1252 data = self._payloadstream.read(size)
1254 1253 self._pos += len(data)
1255 1254 if size is None or len(data) < size:
1256 1255 if not self.consumed and self._pos:
1257 1256 self.ui.debug('bundle2-input-part: total payload size %i\n'
1258 1257 % self._pos)
1259 1258 self.consumed = True
1260 1259 return data
1261 1260
1262 1261 def tell(self):
1263 1262 return self._pos
1264 1263
1265 1264 def seek(self, offset, whence=0):
1266 1265 if whence == 0:
1267 1266 newpos = offset
1268 1267 elif whence == 1:
1269 1268 newpos = self._pos + offset
1270 1269 elif whence == 2:
1271 1270 if not self.consumed:
1272 1271 self.read()
1273 1272 newpos = self._chunkindex[-1][0] - offset
1274 1273 else:
1275 1274 raise ValueError('Unknown whence value: %r' % (whence,))
1276 1275
1277 1276 if newpos > self._chunkindex[-1][0] and not self.consumed:
1278 1277 self.read()
1279 1278 if not 0 <= newpos <= self._chunkindex[-1][0]:
1280 1279 raise ValueError('Offset out of range')
1281 1280
1282 1281 if self._pos != newpos:
1283 1282 chunk, internaloffset = self._findchunk(newpos)
1284 1283 self._payloadstream = util.chunkbuffer(self._payloadchunks(chunk))
1285 1284 adjust = self.read(internaloffset)
1286 1285 if len(adjust) != internaloffset:
1287 1286 raise error.Abort(_('Seek failed\n'))
1288 1287 self._pos = newpos
1289 1288
1290 1289 def _seekfp(self, offset, whence=0):
1291 1290 """move the underlying file pointer
1292 1291
1293 1292 This method is meant for internal usage by the bundle2 protocol only.
1294 1293 They directly manipulate the low level stream including bundle2 level
1295 1294 instruction.
1296 1295
1297 1296 Do not use it to implement higher-level logic or methods."""
1298 1297 if self._seekable:
1299 1298 return self._fp.seek(offset, whence)
1300 1299 else:
1301 1300 raise NotImplementedError(_('File pointer is not seekable'))
1302 1301
1303 1302 def _tellfp(self):
1304 1303 """return the file offset, or None if file is not seekable
1305 1304
1306 1305 This method is meant for internal usage by the bundle2 protocol only.
1307 1306 They directly manipulate the low level stream including bundle2 level
1308 1307 instruction.
1309 1308
1310 1309 Do not use it to implement higher-level logic or methods."""
1311 1310 if self._seekable:
1312 1311 try:
1313 1312 return self._fp.tell()
1314 1313 except IOError as e:
1315 1314 if e.errno == errno.ESPIPE:
1316 1315 self._seekable = False
1317 1316 else:
1318 1317 raise
1319 1318 return None
1320 1319
1321 1320 # These are only the static capabilities.
1322 1321 # Check the 'getrepocaps' function for the rest.
1323 1322 capabilities = {'HG20': (),
1324 1323 'error': ('abort', 'unsupportedcontent', 'pushraced',
1325 1324 'pushkey'),
1326 1325 'listkeys': (),
1327 1326 'pushkey': (),
1328 1327 'digests': tuple(sorted(util.DIGESTS.keys())),
1329 1328 'remote-changegroup': ('http', 'https'),
1330 1329 'hgtagsfnodes': (),
1331 1330 }
1332 1331
1333 1332 def getrepocaps(repo, allowpushback=False):
1334 1333 """return the bundle2 capabilities for a given repo
1335 1334
1336 1335 Exists to allow extensions (like evolution) to mutate the capabilities.
1337 1336 """
1338 1337 caps = capabilities.copy()
1339 1338 caps['changegroup'] = tuple(sorted(
1340 1339 changegroup.supportedincomingversions(repo)))
1341 1340 if obsolete.isenabled(repo, obsolete.exchangeopt):
1342 1341 supportedformat = tuple('V%i' % v for v in obsolete.formats)
1343 1342 caps['obsmarkers'] = supportedformat
1344 1343 if allowpushback:
1345 1344 caps['pushback'] = ()
1346 1345 cpmode = repo.ui.config('server', 'concurrent-push-mode')
1347 1346 if cpmode == 'check-related':
1348 1347 caps['checkheads'] = ('related',)
1349 1348 return caps
1350 1349
1351 1350 def bundle2caps(remote):
1352 1351 """return the bundle capabilities of a peer as dict"""
1353 1352 raw = remote.capable('bundle2')
1354 1353 if not raw and raw != '':
1355 1354 return {}
1356 1355 capsblob = urlreq.unquote(remote.capable('bundle2'))
1357 1356 return decodecaps(capsblob)
1358 1357
1359 1358 def obsmarkersversion(caps):
1360 1359 """extract the list of supported obsmarkers versions from a bundle2caps dict
1361 1360 """
1362 1361 obscaps = caps.get('obsmarkers', ())
1363 1362 return [int(c[1:]) for c in obscaps if c.startswith('V')]
1364 1363
1365 1364 def writenewbundle(ui, repo, source, filename, bundletype, outgoing, opts,
1366 1365 vfs=None, compression=None, compopts=None):
1367 1366 if bundletype.startswith('HG10'):
1368 1367 cg = changegroup.getchangegroup(repo, source, outgoing, version='01')
1369 1368 return writebundle(ui, cg, filename, bundletype, vfs=vfs,
1370 1369 compression=compression, compopts=compopts)
1371 1370 elif not bundletype.startswith('HG20'):
1372 1371 raise error.ProgrammingError('unknown bundle type: %s' % bundletype)
1373 1372
1374 1373 caps = {}
1375 1374 if 'obsolescence' in opts:
1376 1375 caps['obsmarkers'] = ('V1',)
1377 1376 bundle = bundle20(ui, caps)
1378 1377 bundle.setcompression(compression, compopts)
1379 1378 _addpartsfromopts(ui, repo, bundle, source, outgoing, opts)
1380 1379 chunkiter = bundle.getchunks()
1381 1380
1382 1381 return changegroup.writechunks(ui, chunkiter, filename, vfs=vfs)
1383 1382
1384 1383 def _addpartsfromopts(ui, repo, bundler, source, outgoing, opts):
1385 1384 # We should eventually reconcile this logic with the one behind
1386 1385 # 'exchange.getbundle2partsgenerator'.
1387 1386 #
1388 1387 # The type of input from 'getbundle' and 'writenewbundle' are a bit
1389 1388 # different right now. So we keep them separated for now for the sake of
1390 1389 # simplicity.
1391 1390
1392 1391 # we always want a changegroup in such bundle
1393 1392 cgversion = opts.get('cg.version')
1394 1393 if cgversion is None:
1395 1394 cgversion = changegroup.safeversion(repo)
1396 1395 cg = changegroup.getchangegroup(repo, source, outgoing,
1397 1396 version=cgversion)
1398 1397 part = bundler.newpart('changegroup', data=cg.getchunks())
1399 1398 part.addparam('version', cg.version)
1400 1399 if 'clcount' in cg.extras:
1401 1400 part.addparam('nbchanges', str(cg.extras['clcount']),
1402 1401 mandatory=False)
1403 1402 if opts.get('phases') and repo.revs('%ln and secret()',
1404 1403 outgoing.missingheads):
1405 1404 part.addparam('targetphase', '%d' % phases.secret, mandatory=False)
1406 1405
1407 1406 addparttagsfnodescache(repo, bundler, outgoing)
1408 1407
1409 1408 if opts.get('obsolescence', False):
1410 1409 obsmarkers = repo.obsstore.relevantmarkers(outgoing.missing)
1411 1410 buildobsmarkerspart(bundler, obsmarkers)
1412 1411
1413 1412 if opts.get('phases', False):
1414 1413 headsbyphase = phases.subsetphaseheads(repo, outgoing.missing)
1415 1414 phasedata = []
1416 1415 for phase in phases.allphases:
1417 1416 for head in headsbyphase[phase]:
1418 1417 phasedata.append(_pack(_fphasesentry, phase, head))
1419 1418 bundler.newpart('phase-heads', data=''.join(phasedata))
1420 1419
1421 1420 def addparttagsfnodescache(repo, bundler, outgoing):
1422 1421 # we include the tags fnode cache for the bundle changeset
1423 1422 # (as an optional parts)
1424 1423 cache = tags.hgtagsfnodescache(repo.unfiltered())
1425 1424 chunks = []
1426 1425
1427 1426 # .hgtags fnodes are only relevant for head changesets. While we could
1428 1427 # transfer values for all known nodes, there will likely be little to
1429 1428 # no benefit.
1430 1429 #
1431 1430 # We don't bother using a generator to produce output data because
1432 1431 # a) we only have 40 bytes per head and even esoteric numbers of heads
1433 1432 # consume little memory (1M heads is 40MB) b) we don't want to send the
1434 1433 # part if we don't have entries and knowing if we have entries requires
1435 1434 # cache lookups.
1436 1435 for node in outgoing.missingheads:
1437 1436 # Don't compute missing, as this may slow down serving.
1438 1437 fnode = cache.getfnode(node, computemissing=False)
1439 1438 if fnode is not None:
1440 1439 chunks.extend([node, fnode])
1441 1440
1442 1441 if chunks:
1443 1442 bundler.newpart('hgtagsfnodes', data=''.join(chunks))
1444 1443
1445 1444 def buildobsmarkerspart(bundler, markers):
1446 1445 """add an obsmarker part to the bundler with <markers>
1447 1446
1448 1447 No part is created if markers is empty.
1449 1448 Raises ValueError if the bundler doesn't support any known obsmarker format.
1450 1449 """
1451 1450 if not markers:
1452 1451 return None
1453 1452
1454 1453 remoteversions = obsmarkersversion(bundler.capabilities)
1455 1454 version = obsolete.commonversion(remoteversions)
1456 1455 if version is None:
1457 1456 raise ValueError('bundler does not support common obsmarker format')
1458 1457 stream = obsolete.encodemarkers(markers, True, version=version)
1459 1458 return bundler.newpart('obsmarkers', data=stream)
1460 1459
1461 1460 def writebundle(ui, cg, filename, bundletype, vfs=None, compression=None,
1462 1461 compopts=None):
1463 1462 """Write a bundle file and return its filename.
1464 1463
1465 1464 Existing files will not be overwritten.
1466 1465 If no filename is specified, a temporary file is created.
1467 1466 bz2 compression can be turned off.
1468 1467 The bundle file will be deleted in case of errors.
1469 1468 """
1470 1469
1471 1470 if bundletype == "HG20":
1472 1471 bundle = bundle20(ui)
1473 1472 bundle.setcompression(compression, compopts)
1474 1473 part = bundle.newpart('changegroup', data=cg.getchunks())
1475 1474 part.addparam('version', cg.version)
1476 1475 if 'clcount' in cg.extras:
1477 1476 part.addparam('nbchanges', str(cg.extras['clcount']),
1478 1477 mandatory=False)
1479 1478 chunkiter = bundle.getchunks()
1480 1479 else:
1481 1480 # compression argument is only for the bundle2 case
1482 1481 assert compression is None
1483 1482 if cg.version != '01':
1484 1483 raise error.Abort(_('old bundle types only supports v1 '
1485 1484 'changegroups'))
1486 1485 header, comp = bundletypes[bundletype]
1487 1486 if comp not in util.compengines.supportedbundletypes:
1488 1487 raise error.Abort(_('unknown stream compression type: %s')
1489 1488 % comp)
1490 1489 compengine = util.compengines.forbundletype(comp)
1491 1490 def chunkiter():
1492 1491 yield header
1493 1492 for chunk in compengine.compressstream(cg.getchunks(), compopts):
1494 1493 yield chunk
1495 1494 chunkiter = chunkiter()
1496 1495
1497 1496 # parse the changegroup data, otherwise we will block
1498 1497 # in case of sshrepo because we don't know the end of the stream
1499 1498 return changegroup.writechunks(ui, chunkiter, filename, vfs=vfs)
1500 1499
1501 1500 def combinechangegroupresults(op):
1502 1501 """logic to combine 0 or more addchangegroup results into one"""
1503 1502 results = [r.get('return', 0)
1504 1503 for r in op.records['changegroup']]
1505 1504 changedheads = 0
1506 1505 result = 1
1507 1506 for ret in results:
1508 1507 # If any changegroup result is 0, return 0
1509 1508 if ret == 0:
1510 1509 result = 0
1511 1510 break
1512 1511 if ret < -1:
1513 1512 changedheads += ret + 1
1514 1513 elif ret > 1:
1515 1514 changedheads += ret - 1
1516 1515 if changedheads > 0:
1517 1516 result = 1 + changedheads
1518 1517 elif changedheads < 0:
1519 1518 result = -1 + changedheads
1520 1519 return result
1521 1520
1522 1521 @parthandler('changegroup', ('version', 'nbchanges', 'treemanifest',
1523 1522 'targetphase'))
1524 1523 def handlechangegroup(op, inpart):
1525 1524 """apply a changegroup part on the repo
1526 1525
1527 1526 This is a very early implementation that will massive rework before being
1528 1527 inflicted to any end-user.
1529 1528 """
1530 1529 tr = op.gettransaction()
1531 1530 unpackerversion = inpart.params.get('version', '01')
1532 1531 # We should raise an appropriate exception here
1533 1532 cg = changegroup.getunbundler(unpackerversion, inpart, None)
1534 1533 # the source and url passed here are overwritten by the one contained in
1535 1534 # the transaction.hookargs argument. So 'bundle2' is a placeholder
1536 1535 nbchangesets = None
1537 1536 if 'nbchanges' in inpart.params:
1538 1537 nbchangesets = int(inpart.params.get('nbchanges'))
1539 1538 if ('treemanifest' in inpart.params and
1540 1539 'treemanifest' not in op.repo.requirements):
1541 1540 if len(op.repo.changelog) != 0:
1542 1541 raise error.Abort(_(
1543 1542 "bundle contains tree manifests, but local repo is "
1544 1543 "non-empty and does not use tree manifests"))
1545 1544 op.repo.requirements.add('treemanifest')
1546 1545 op.repo._applyopenerreqs()
1547 1546 op.repo._writerequirements()
1548 1547 extrakwargs = {}
1549 1548 targetphase = inpart.params.get('targetphase')
1550 1549 if targetphase is not None:
1551 1550 extrakwargs['targetphase'] = int(targetphase)
1552 1551 ret = _processchangegroup(op, cg, tr, 'bundle2', 'bundle2',
1553 1552 expectedtotal=nbchangesets, **extrakwargs)
1554 1553 if op.reply is not None:
1555 1554 # This is definitely not the final form of this
1556 1555 # return. But one need to start somewhere.
1557 1556 part = op.reply.newpart('reply:changegroup', mandatory=False)
1558 1557 part.addparam('in-reply-to', str(inpart.id), mandatory=False)
1559 1558 part.addparam('return', '%i' % ret, mandatory=False)
1560 1559 assert not inpart.read()
1561 1560
1562 1561 _remotechangegroupparams = tuple(['url', 'size', 'digests'] +
1563 1562 ['digest:%s' % k for k in util.DIGESTS.keys()])
1564 1563 @parthandler('remote-changegroup', _remotechangegroupparams)
1565 1564 def handleremotechangegroup(op, inpart):
1566 1565 """apply a bundle10 on the repo, given an url and validation information
1567 1566
1568 1567 All the information about the remote bundle to import are given as
1569 1568 parameters. The parameters include:
1570 1569 - url: the url to the bundle10.
1571 1570 - size: the bundle10 file size. It is used to validate what was
1572 1571 retrieved by the client matches the server knowledge about the bundle.
1573 1572 - digests: a space separated list of the digest types provided as
1574 1573 parameters.
1575 1574 - digest:<digest-type>: the hexadecimal representation of the digest with
1576 1575 that name. Like the size, it is used to validate what was retrieved by
1577 1576 the client matches what the server knows about the bundle.
1578 1577
1579 1578 When multiple digest types are given, all of them are checked.
1580 1579 """
1581 1580 try:
1582 1581 raw_url = inpart.params['url']
1583 1582 except KeyError:
1584 1583 raise error.Abort(_('remote-changegroup: missing "%s" param') % 'url')
1585 1584 parsed_url = util.url(raw_url)
1586 1585 if parsed_url.scheme not in capabilities['remote-changegroup']:
1587 1586 raise error.Abort(_('remote-changegroup does not support %s urls') %
1588 1587 parsed_url.scheme)
1589 1588
1590 1589 try:
1591 1590 size = int(inpart.params['size'])
1592 1591 except ValueError:
1593 1592 raise error.Abort(_('remote-changegroup: invalid value for param "%s"')
1594 1593 % 'size')
1595 1594 except KeyError:
1596 1595 raise error.Abort(_('remote-changegroup: missing "%s" param') % 'size')
1597 1596
1598 1597 digests = {}
1599 1598 for typ in inpart.params.get('digests', '').split():
1600 1599 param = 'digest:%s' % typ
1601 1600 try:
1602 1601 value = inpart.params[param]
1603 1602 except KeyError:
1604 1603 raise error.Abort(_('remote-changegroup: missing "%s" param') %
1605 1604 param)
1606 1605 digests[typ] = value
1607 1606
1608 1607 real_part = util.digestchecker(url.open(op.ui, raw_url), size, digests)
1609 1608
1610 1609 tr = op.gettransaction()
1611 1610 from . import exchange
1612 1611 cg = exchange.readbundle(op.repo.ui, real_part, raw_url)
1613 1612 if not isinstance(cg, changegroup.cg1unpacker):
1614 1613 raise error.Abort(_('%s: not a bundle version 1.0') %
1615 1614 util.hidepassword(raw_url))
1616 1615 ret = _processchangegroup(op, cg, tr, 'bundle2', 'bundle2')
1617 1616 if op.reply is not None:
1618 1617 # This is definitely not the final form of this
1619 1618 # return. But one need to start somewhere.
1620 1619 part = op.reply.newpart('reply:changegroup')
1621 1620 part.addparam('in-reply-to', str(inpart.id), mandatory=False)
1622 1621 part.addparam('return', '%i' % ret, mandatory=False)
1623 1622 try:
1624 1623 real_part.validate()
1625 1624 except error.Abort as e:
1626 1625 raise error.Abort(_('bundle at %s is corrupted:\n%s') %
1627 1626 (util.hidepassword(raw_url), str(e)))
1628 1627 assert not inpart.read()
1629 1628
1630 1629 @parthandler('reply:changegroup', ('return', 'in-reply-to'))
1631 1630 def handlereplychangegroup(op, inpart):
1632 1631 ret = int(inpart.params['return'])
1633 1632 replyto = int(inpart.params['in-reply-to'])
1634 1633 op.records.add('changegroup', {'return': ret}, replyto)
1635 1634
1636 1635 @parthandler('check:heads')
1637 1636 def handlecheckheads(op, inpart):
1638 1637 """check that head of the repo did not change
1639 1638
1640 1639 This is used to detect a push race when using unbundle.
1641 1640 This replaces the "heads" argument of unbundle."""
1642 1641 h = inpart.read(20)
1643 1642 heads = []
1644 1643 while len(h) == 20:
1645 1644 heads.append(h)
1646 1645 h = inpart.read(20)
1647 1646 assert not h
1648 1647 # Trigger a transaction so that we are guaranteed to have the lock now.
1649 1648 if op.ui.configbool('experimental', 'bundle2lazylocking'):
1650 1649 op.gettransaction()
1651 1650 if sorted(heads) != sorted(op.repo.heads()):
1652 1651 raise error.PushRaced('repository changed while pushing - '
1653 1652 'please try again')
1654 1653
1655 1654 @parthandler('check:updated-heads')
1656 1655 def handlecheckupdatedheads(op, inpart):
1657 1656 """check for race on the heads touched by a push
1658 1657
1659 1658 This is similar to 'check:heads' but focus on the heads actually updated
1660 1659 during the push. If other activities happen on unrelated heads, it is
1661 1660 ignored.
1662 1661
1663 1662 This allow server with high traffic to avoid push contention as long as
1664 1663 unrelated parts of the graph are involved."""
1665 1664 h = inpart.read(20)
1666 1665 heads = []
1667 1666 while len(h) == 20:
1668 1667 heads.append(h)
1669 1668 h = inpart.read(20)
1670 1669 assert not h
1671 1670 # trigger a transaction so that we are guaranteed to have the lock now.
1672 1671 if op.ui.configbool('experimental', 'bundle2lazylocking'):
1673 1672 op.gettransaction()
1674 1673
1675 1674 currentheads = set()
1676 1675 for ls in op.repo.branchmap().itervalues():
1677 1676 currentheads.update(ls)
1678 1677
1679 1678 for h in heads:
1680 1679 if h not in currentheads:
1681 1680 raise error.PushRaced('repository changed while pushing - '
1682 1681 'please try again')
1683 1682
1684 1683 @parthandler('output')
1685 1684 def handleoutput(op, inpart):
1686 1685 """forward output captured on the server to the client"""
1687 1686 for line in inpart.read().splitlines():
1688 1687 op.ui.status(_('remote: %s\n') % line)
1689 1688
1690 1689 @parthandler('replycaps')
1691 1690 def handlereplycaps(op, inpart):
1692 1691 """Notify that a reply bundle should be created
1693 1692
1694 1693 The payload contains the capabilities information for the reply"""
1695 1694 caps = decodecaps(inpart.read())
1696 1695 if op.reply is None:
1697 1696 op.reply = bundle20(op.ui, caps)
1698 1697
1699 1698 class AbortFromPart(error.Abort):
1700 1699 """Sub-class of Abort that denotes an error from a bundle2 part."""
1701 1700
1702 1701 @parthandler('error:abort', ('message', 'hint'))
1703 1702 def handleerrorabort(op, inpart):
1704 1703 """Used to transmit abort error over the wire"""
1705 1704 raise AbortFromPart(inpart.params['message'],
1706 1705 hint=inpart.params.get('hint'))
1707 1706
1708 1707 @parthandler('error:pushkey', ('namespace', 'key', 'new', 'old', 'ret',
1709 1708 'in-reply-to'))
1710 1709 def handleerrorpushkey(op, inpart):
1711 1710 """Used to transmit failure of a mandatory pushkey over the wire"""
1712 1711 kwargs = {}
1713 1712 for name in ('namespace', 'key', 'new', 'old', 'ret'):
1714 1713 value = inpart.params.get(name)
1715 1714 if value is not None:
1716 1715 kwargs[name] = value
1717 1716 raise error.PushkeyFailed(inpart.params['in-reply-to'], **kwargs)
1718 1717
1719 1718 @parthandler('error:unsupportedcontent', ('parttype', 'params'))
1720 1719 def handleerrorunsupportedcontent(op, inpart):
1721 1720 """Used to transmit unknown content error over the wire"""
1722 1721 kwargs = {}
1723 1722 parttype = inpart.params.get('parttype')
1724 1723 if parttype is not None:
1725 1724 kwargs['parttype'] = parttype
1726 1725 params = inpart.params.get('params')
1727 1726 if params is not None:
1728 1727 kwargs['params'] = params.split('\0')
1729 1728
1730 1729 raise error.BundleUnknownFeatureError(**kwargs)
1731 1730
1732 1731 @parthandler('error:pushraced', ('message',))
1733 1732 def handleerrorpushraced(op, inpart):
1734 1733 """Used to transmit push race error over the wire"""
1735 1734 raise error.ResponseError(_('push failed:'), inpart.params['message'])
1736 1735
1737 1736 @parthandler('listkeys', ('namespace',))
1738 1737 def handlelistkeys(op, inpart):
1739 1738 """retrieve pushkey namespace content stored in a bundle2"""
1740 1739 namespace = inpart.params['namespace']
1741 1740 r = pushkey.decodekeys(inpart.read())
1742 1741 op.records.add('listkeys', (namespace, r))
1743 1742
1744 1743 @parthandler('pushkey', ('namespace', 'key', 'old', 'new'))
1745 1744 def handlepushkey(op, inpart):
1746 1745 """process a pushkey request"""
1747 1746 dec = pushkey.decode
1748 1747 namespace = dec(inpart.params['namespace'])
1749 1748 key = dec(inpart.params['key'])
1750 1749 old = dec(inpart.params['old'])
1751 1750 new = dec(inpart.params['new'])
1752 1751 # Grab the transaction to ensure that we have the lock before performing the
1753 1752 # pushkey.
1754 1753 if op.ui.configbool('experimental', 'bundle2lazylocking'):
1755 1754 op.gettransaction()
1756 1755 ret = op.repo.pushkey(namespace, key, old, new)
1757 1756 record = {'namespace': namespace,
1758 1757 'key': key,
1759 1758 'old': old,
1760 1759 'new': new}
1761 1760 op.records.add('pushkey', record)
1762 1761 if op.reply is not None:
1763 1762 rpart = op.reply.newpart('reply:pushkey')
1764 1763 rpart.addparam('in-reply-to', str(inpart.id), mandatory=False)
1765 1764 rpart.addparam('return', '%i' % ret, mandatory=False)
1766 1765 if inpart.mandatory and not ret:
1767 1766 kwargs = {}
1768 1767 for key in ('namespace', 'key', 'new', 'old', 'ret'):
1769 1768 if key in inpart.params:
1770 1769 kwargs[key] = inpart.params[key]
1771 1770 raise error.PushkeyFailed(partid=str(inpart.id), **kwargs)
1772 1771
1773 1772 def _readphaseheads(inpart):
1774 1773 headsbyphase = [[] for i in phases.allphases]
1775 1774 entrysize = struct.calcsize(_fphasesentry)
1776 1775 while True:
1777 1776 entry = inpart.read(entrysize)
1778 1777 if len(entry) < entrysize:
1779 1778 if entry:
1780 1779 raise error.Abort(_('bad phase-heads bundle part'))
1781 1780 break
1782 1781 phase, node = struct.unpack(_fphasesentry, entry)
1783 1782 headsbyphase[phase].append(node)
1784 1783 return headsbyphase
1785 1784
1786 1785 @parthandler('phase-heads')
1787 1786 def handlephases(op, inpart):
1788 1787 """apply phases from bundle part to repo"""
1789 1788 headsbyphase = _readphaseheads(inpart)
1790 1789 phases.updatephases(op.repo.unfiltered(), op.gettransaction(), headsbyphase)
1791 1790
1792 1791 @parthandler('reply:pushkey', ('return', 'in-reply-to'))
1793 1792 def handlepushkeyreply(op, inpart):
1794 1793 """retrieve the result of a pushkey request"""
1795 1794 ret = int(inpart.params['return'])
1796 1795 partid = int(inpart.params['in-reply-to'])
1797 1796 op.records.add('pushkey', {'return': ret}, partid)
1798 1797
1799 1798 @parthandler('obsmarkers')
1800 1799 def handleobsmarker(op, inpart):
1801 1800 """add a stream of obsmarkers to the repo"""
1802 1801 tr = op.gettransaction()
1803 1802 markerdata = inpart.read()
1804 1803 if op.ui.config('experimental', 'obsmarkers-exchange-debug'):
1805 1804 op.ui.write(('obsmarker-exchange: %i bytes received\n')
1806 1805 % len(markerdata))
1807 1806 # The mergemarkers call will crash if marker creation is not enabled.
1808 1807 # we want to avoid this if the part is advisory.
1809 1808 if not inpart.mandatory and op.repo.obsstore.readonly:
1810 1809 op.repo.ui.debug('ignoring obsolescence markers, feature not enabled')
1811 1810 return
1812 1811 new = op.repo.obsstore.mergemarkers(tr, markerdata)
1813 1812 op.repo.invalidatevolatilesets()
1814 1813 if new:
1815 1814 op.repo.ui.status(_('%i new obsolescence markers\n') % new)
1816 1815 op.records.add('obsmarkers', {'new': new})
1817 scmutil.registersummarycallback(op.repo, tr)
1818 1816 if op.reply is not None:
1819 1817 rpart = op.reply.newpart('reply:obsmarkers')
1820 1818 rpart.addparam('in-reply-to', str(inpart.id), mandatory=False)
1821 1819 rpart.addparam('new', '%i' % new, mandatory=False)
1822 1820
1823 1821
1824 1822 @parthandler('reply:obsmarkers', ('new', 'in-reply-to'))
1825 1823 def handleobsmarkerreply(op, inpart):
1826 1824 """retrieve the result of a pushkey request"""
1827 1825 ret = int(inpart.params['new'])
1828 1826 partid = int(inpart.params['in-reply-to'])
1829 1827 op.records.add('obsmarkers', {'new': ret}, partid)
1830 1828
1831 1829 @parthandler('hgtagsfnodes')
1832 1830 def handlehgtagsfnodes(op, inpart):
1833 1831 """Applies .hgtags fnodes cache entries to the local repo.
1834 1832
1835 1833 Payload is pairs of 20 byte changeset nodes and filenodes.
1836 1834 """
1837 1835 # Grab the transaction so we ensure that we have the lock at this point.
1838 1836 if op.ui.configbool('experimental', 'bundle2lazylocking'):
1839 1837 op.gettransaction()
1840 1838 cache = tags.hgtagsfnodescache(op.repo.unfiltered())
1841 1839
1842 1840 count = 0
1843 1841 while True:
1844 1842 node = inpart.read(20)
1845 1843 fnode = inpart.read(20)
1846 1844 if len(node) < 20 or len(fnode) < 20:
1847 1845 op.ui.debug('ignoring incomplete received .hgtags fnodes data\n')
1848 1846 break
1849 1847 cache.setfnode(node, fnode)
1850 1848 count += 1
1851 1849
1852 1850 cache.write()
1853 1851 op.ui.debug('applied %i hgtags fnodes cache entries\n' % count)
@@ -1,2254 +1,2256 b''
1 1 # localrepo.py - read/write repository class for mercurial
2 2 #
3 3 # Copyright 2005-2007 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 hashlib
12 12 import inspect
13 13 import os
14 14 import random
15 15 import time
16 16 import weakref
17 17
18 18 from .i18n import _
19 19 from .node import (
20 20 hex,
21 21 nullid,
22 22 short,
23 23 )
24 24 from . import (
25 25 bookmarks,
26 26 branchmap,
27 27 bundle2,
28 28 changegroup,
29 29 changelog,
30 30 color,
31 31 context,
32 32 dirstate,
33 33 dirstateguard,
34 34 encoding,
35 35 error,
36 36 exchange,
37 37 extensions,
38 38 filelog,
39 39 hook,
40 40 lock as lockmod,
41 41 manifest,
42 42 match as matchmod,
43 43 merge as mergemod,
44 44 mergeutil,
45 45 namespaces,
46 46 obsolete,
47 47 pathutil,
48 48 peer,
49 49 phases,
50 50 pushkey,
51 51 pycompat,
52 52 repoview,
53 53 revset,
54 54 revsetlang,
55 55 scmutil,
56 56 sparse,
57 57 store,
58 58 subrepo,
59 59 tags as tagsmod,
60 60 transaction,
61 61 txnutil,
62 62 util,
63 63 vfs as vfsmod,
64 64 )
65 65
66 66 release = lockmod.release
67 67 urlerr = util.urlerr
68 68 urlreq = util.urlreq
69 69
70 70 # set of (path, vfs-location) tuples. vfs-location is:
71 71 # - 'plain for vfs relative paths
72 72 # - '' for svfs relative paths
73 73 _cachedfiles = set()
74 74
75 75 class _basefilecache(scmutil.filecache):
76 76 """All filecache usage on repo are done for logic that should be unfiltered
77 77 """
78 78 def __get__(self, repo, type=None):
79 79 if repo is None:
80 80 return self
81 81 return super(_basefilecache, self).__get__(repo.unfiltered(), type)
82 82 def __set__(self, repo, value):
83 83 return super(_basefilecache, self).__set__(repo.unfiltered(), value)
84 84 def __delete__(self, repo):
85 85 return super(_basefilecache, self).__delete__(repo.unfiltered())
86 86
87 87 class repofilecache(_basefilecache):
88 88 """filecache for files in .hg but outside of .hg/store"""
89 89 def __init__(self, *paths):
90 90 super(repofilecache, self).__init__(*paths)
91 91 for path in paths:
92 92 _cachedfiles.add((path, 'plain'))
93 93
94 94 def join(self, obj, fname):
95 95 return obj.vfs.join(fname)
96 96
97 97 class storecache(_basefilecache):
98 98 """filecache for files in the store"""
99 99 def __init__(self, *paths):
100 100 super(storecache, self).__init__(*paths)
101 101 for path in paths:
102 102 _cachedfiles.add((path, ''))
103 103
104 104 def join(self, obj, fname):
105 105 return obj.sjoin(fname)
106 106
107 107 def isfilecached(repo, name):
108 108 """check if a repo has already cached "name" filecache-ed property
109 109
110 110 This returns (cachedobj-or-None, iscached) tuple.
111 111 """
112 112 cacheentry = repo.unfiltered()._filecache.get(name, None)
113 113 if not cacheentry:
114 114 return None, False
115 115 return cacheentry.obj, True
116 116
117 117 class unfilteredpropertycache(util.propertycache):
118 118 """propertycache that apply to unfiltered repo only"""
119 119
120 120 def __get__(self, repo, type=None):
121 121 unfi = repo.unfiltered()
122 122 if unfi is repo:
123 123 return super(unfilteredpropertycache, self).__get__(unfi)
124 124 return getattr(unfi, self.name)
125 125
126 126 class filteredpropertycache(util.propertycache):
127 127 """propertycache that must take filtering in account"""
128 128
129 129 def cachevalue(self, obj, value):
130 130 object.__setattr__(obj, self.name, value)
131 131
132 132
133 133 def hasunfilteredcache(repo, name):
134 134 """check if a repo has an unfilteredpropertycache value for <name>"""
135 135 return name in vars(repo.unfiltered())
136 136
137 137 def unfilteredmethod(orig):
138 138 """decorate method that always need to be run on unfiltered version"""
139 139 def wrapper(repo, *args, **kwargs):
140 140 return orig(repo.unfiltered(), *args, **kwargs)
141 141 return wrapper
142 142
143 143 moderncaps = {'lookup', 'branchmap', 'pushkey', 'known', 'getbundle',
144 144 'unbundle'}
145 145 legacycaps = moderncaps.union({'changegroupsubset'})
146 146
147 147 class localpeer(peer.peerrepository):
148 148 '''peer for a local repo; reflects only the most recent API'''
149 149
150 150 def __init__(self, repo, caps=None):
151 151 if caps is None:
152 152 caps = moderncaps.copy()
153 153 peer.peerrepository.__init__(self)
154 154 self._repo = repo.filtered('served')
155 155 self.ui = repo.ui
156 156 self._caps = repo._restrictcapabilities(caps)
157 157 self.requirements = repo.requirements
158 158 self.supportedformats = repo.supportedformats
159 159
160 160 def close(self):
161 161 self._repo.close()
162 162
163 163 def _capabilities(self):
164 164 return self._caps
165 165
166 166 def local(self):
167 167 return self._repo
168 168
169 169 def canpush(self):
170 170 return True
171 171
172 172 def url(self):
173 173 return self._repo.url()
174 174
175 175 def lookup(self, key):
176 176 return self._repo.lookup(key)
177 177
178 178 def branchmap(self):
179 179 return self._repo.branchmap()
180 180
181 181 def heads(self):
182 182 return self._repo.heads()
183 183
184 184 def known(self, nodes):
185 185 return self._repo.known(nodes)
186 186
187 187 def getbundle(self, source, heads=None, common=None, bundlecaps=None,
188 188 **kwargs):
189 189 chunks = exchange.getbundlechunks(self._repo, source, heads=heads,
190 190 common=common, bundlecaps=bundlecaps,
191 191 **kwargs)
192 192 cb = util.chunkbuffer(chunks)
193 193
194 194 if exchange.bundle2requested(bundlecaps):
195 195 # When requesting a bundle2, getbundle returns a stream to make the
196 196 # wire level function happier. We need to build a proper object
197 197 # from it in local peer.
198 198 return bundle2.getunbundler(self.ui, cb)
199 199 else:
200 200 return changegroup.getunbundler('01', cb, None)
201 201
202 202 # TODO We might want to move the next two calls into legacypeer and add
203 203 # unbundle instead.
204 204
205 205 def unbundle(self, cg, heads, url):
206 206 """apply a bundle on a repo
207 207
208 208 This function handles the repo locking itself."""
209 209 try:
210 210 try:
211 211 cg = exchange.readbundle(self.ui, cg, None)
212 212 ret = exchange.unbundle(self._repo, cg, heads, 'push', url)
213 213 if util.safehasattr(ret, 'getchunks'):
214 214 # This is a bundle20 object, turn it into an unbundler.
215 215 # This little dance should be dropped eventually when the
216 216 # API is finally improved.
217 217 stream = util.chunkbuffer(ret.getchunks())
218 218 ret = bundle2.getunbundler(self.ui, stream)
219 219 return ret
220 220 except Exception as exc:
221 221 # If the exception contains output salvaged from a bundle2
222 222 # reply, we need to make sure it is printed before continuing
223 223 # to fail. So we build a bundle2 with such output and consume
224 224 # it directly.
225 225 #
226 226 # This is not very elegant but allows a "simple" solution for
227 227 # issue4594
228 228 output = getattr(exc, '_bundle2salvagedoutput', ())
229 229 if output:
230 230 bundler = bundle2.bundle20(self._repo.ui)
231 231 for out in output:
232 232 bundler.addpart(out)
233 233 stream = util.chunkbuffer(bundler.getchunks())
234 234 b = bundle2.getunbundler(self.ui, stream)
235 235 bundle2.processbundle(self._repo, b)
236 236 raise
237 237 except error.PushRaced as exc:
238 238 raise error.ResponseError(_('push failed:'), str(exc))
239 239
240 240 def lock(self):
241 241 return self._repo.lock()
242 242
243 243 def pushkey(self, namespace, key, old, new):
244 244 return self._repo.pushkey(namespace, key, old, new)
245 245
246 246 def listkeys(self, namespace):
247 247 return self._repo.listkeys(namespace)
248 248
249 249 def debugwireargs(self, one, two, three=None, four=None, five=None):
250 250 '''used to test argument passing over the wire'''
251 251 return "%s %s %s %s %s" % (one, two, three, four, five)
252 252
253 253 class locallegacypeer(localpeer):
254 254 '''peer extension which implements legacy methods too; used for tests with
255 255 restricted capabilities'''
256 256
257 257 def __init__(self, repo):
258 258 localpeer.__init__(self, repo, caps=legacycaps)
259 259
260 260 def branches(self, nodes):
261 261 return self._repo.branches(nodes)
262 262
263 263 def between(self, pairs):
264 264 return self._repo.between(pairs)
265 265
266 266 def changegroup(self, basenodes, source):
267 267 return changegroup.changegroup(self._repo, basenodes, source)
268 268
269 269 def changegroupsubset(self, bases, heads, source):
270 270 return changegroup.changegroupsubset(self._repo, bases, heads, source)
271 271
272 272 # Increment the sub-version when the revlog v2 format changes to lock out old
273 273 # clients.
274 274 REVLOGV2_REQUIREMENT = 'exp-revlogv2.0'
275 275
276 276 class localrepository(object):
277 277
278 278 supportedformats = {
279 279 'revlogv1',
280 280 'generaldelta',
281 281 'treemanifest',
282 282 'manifestv2',
283 283 REVLOGV2_REQUIREMENT,
284 284 }
285 285 _basesupported = supportedformats | {
286 286 'store',
287 287 'fncache',
288 288 'shared',
289 289 'relshared',
290 290 'dotencode',
291 291 }
292 292 openerreqs = {
293 293 'revlogv1',
294 294 'generaldelta',
295 295 'treemanifest',
296 296 'manifestv2',
297 297 }
298 298
299 299 # a list of (ui, featureset) functions.
300 300 # only functions defined in module of enabled extensions are invoked
301 301 featuresetupfuncs = set()
302 302
303 303 # list of prefix for file which can be written without 'wlock'
304 304 # Extensions should extend this list when needed
305 305 _wlockfreeprefix = {
306 306 # We migh consider requiring 'wlock' for the next
307 307 # two, but pretty much all the existing code assume
308 308 # wlock is not needed so we keep them excluded for
309 309 # now.
310 310 'hgrc',
311 311 'requires',
312 312 # XXX cache is a complicatged business someone
313 313 # should investigate this in depth at some point
314 314 'cache/',
315 315 # XXX shouldn't be dirstate covered by the wlock?
316 316 'dirstate',
317 317 # XXX bisect was still a bit too messy at the time
318 318 # this changeset was introduced. Someone should fix
319 319 # the remainig bit and drop this line
320 320 'bisect.state',
321 321 }
322 322
323 323 def __init__(self, baseui, path, create=False):
324 324 self.requirements = set()
325 325 self.filtername = None
326 326 # wvfs: rooted at the repository root, used to access the working copy
327 327 self.wvfs = vfsmod.vfs(path, expandpath=True, realpath=True)
328 328 # vfs: rooted at .hg, used to access repo files outside of .hg/store
329 329 self.vfs = None
330 330 # svfs: usually rooted at .hg/store, used to access repository history
331 331 # If this is a shared repository, this vfs may point to another
332 332 # repository's .hg/store directory.
333 333 self.svfs = None
334 334 self.root = self.wvfs.base
335 335 self.path = self.wvfs.join(".hg")
336 336 self.origroot = path
337 337 # These auditor are not used by the vfs,
338 338 # only used when writing this comment: basectx.match
339 339 self.auditor = pathutil.pathauditor(self.root, self._checknested)
340 340 self.nofsauditor = pathutil.pathauditor(self.root, self._checknested,
341 341 realfs=False)
342 342 self.baseui = baseui
343 343 self.ui = baseui.copy()
344 344 self.ui.copy = baseui.copy # prevent copying repo configuration
345 345 self.vfs = vfsmod.vfs(self.path)
346 346 if (self.ui.configbool('devel', 'all-warnings') or
347 347 self.ui.configbool('devel', 'check-locks')):
348 348 self.vfs.audit = self._getvfsward(self.vfs.audit)
349 349 # A list of callback to shape the phase if no data were found.
350 350 # Callback are in the form: func(repo, roots) --> processed root.
351 351 # This list it to be filled by extension during repo setup
352 352 self._phasedefaults = []
353 353 try:
354 354 self.ui.readconfig(self.vfs.join("hgrc"), self.root)
355 355 self._loadextensions()
356 356 except IOError:
357 357 pass
358 358
359 359 if self.featuresetupfuncs:
360 360 self.supported = set(self._basesupported) # use private copy
361 361 extmods = set(m.__name__ for n, m
362 362 in extensions.extensions(self.ui))
363 363 for setupfunc in self.featuresetupfuncs:
364 364 if setupfunc.__module__ in extmods:
365 365 setupfunc(self.ui, self.supported)
366 366 else:
367 367 self.supported = self._basesupported
368 368 color.setup(self.ui)
369 369
370 370 # Add compression engines.
371 371 for name in util.compengines:
372 372 engine = util.compengines[name]
373 373 if engine.revlogheader():
374 374 self.supported.add('exp-compression-%s' % name)
375 375
376 376 if not self.vfs.isdir():
377 377 if create:
378 378 self.requirements = newreporequirements(self)
379 379
380 380 if not self.wvfs.exists():
381 381 self.wvfs.makedirs()
382 382 self.vfs.makedir(notindexed=True)
383 383
384 384 if 'store' in self.requirements:
385 385 self.vfs.mkdir("store")
386 386
387 387 # create an invalid changelog
388 388 self.vfs.append(
389 389 "00changelog.i",
390 390 '\0\0\0\2' # represents revlogv2
391 391 ' dummy changelog to prevent using the old repo layout'
392 392 )
393 393 else:
394 394 raise error.RepoError(_("repository %s not found") % path)
395 395 elif create:
396 396 raise error.RepoError(_("repository %s already exists") % path)
397 397 else:
398 398 try:
399 399 self.requirements = scmutil.readrequires(
400 400 self.vfs, self.supported)
401 401 except IOError as inst:
402 402 if inst.errno != errno.ENOENT:
403 403 raise
404 404
405 405 cachepath = self.vfs.join('cache')
406 406 self.sharedpath = self.path
407 407 try:
408 408 sharedpath = self.vfs.read("sharedpath").rstrip('\n')
409 409 if 'relshared' in self.requirements:
410 410 sharedpath = self.vfs.join(sharedpath)
411 411 vfs = vfsmod.vfs(sharedpath, realpath=True)
412 412 cachepath = vfs.join('cache')
413 413 s = vfs.base
414 414 if not vfs.exists():
415 415 raise error.RepoError(
416 416 _('.hg/sharedpath points to nonexistent directory %s') % s)
417 417 self.sharedpath = s
418 418 except IOError as inst:
419 419 if inst.errno != errno.ENOENT:
420 420 raise
421 421
422 422 self.store = store.store(
423 423 self.requirements, self.sharedpath, vfsmod.vfs)
424 424 self.spath = self.store.path
425 425 self.svfs = self.store.vfs
426 426 self.sjoin = self.store.join
427 427 self.vfs.createmode = self.store.createmode
428 428 self.cachevfs = vfsmod.vfs(cachepath)
429 429 self.cachevfs.createmode = self.store.createmode
430 430 if (self.ui.configbool('devel', 'all-warnings') or
431 431 self.ui.configbool('devel', 'check-locks')):
432 432 if util.safehasattr(self.svfs, 'vfs'): # this is filtervfs
433 433 self.svfs.vfs.audit = self._getsvfsward(self.svfs.vfs.audit)
434 434 else: # standard vfs
435 435 self.svfs.audit = self._getsvfsward(self.svfs.audit)
436 436 self._applyopenerreqs()
437 437 if create:
438 438 self._writerequirements()
439 439
440 440 self._dirstatevalidatewarned = False
441 441
442 442 self._branchcaches = {}
443 443 self._revbranchcache = None
444 444 self.filterpats = {}
445 445 self._datafilters = {}
446 446 self._transref = self._lockref = self._wlockref = None
447 447
448 448 # A cache for various files under .hg/ that tracks file changes,
449 449 # (used by the filecache decorator)
450 450 #
451 451 # Maps a property name to its util.filecacheentry
452 452 self._filecache = {}
453 453
454 454 # hold sets of revision to be filtered
455 455 # should be cleared when something might have changed the filter value:
456 456 # - new changesets,
457 457 # - phase change,
458 458 # - new obsolescence marker,
459 459 # - working directory parent change,
460 460 # - bookmark changes
461 461 self.filteredrevcache = {}
462 462
463 463 # post-dirstate-status hooks
464 464 self._postdsstatus = []
465 465
466 466 # Cache of types representing filtered repos.
467 467 self._filteredrepotypes = weakref.WeakKeyDictionary()
468 468
469 469 # generic mapping between names and nodes
470 470 self.names = namespaces.namespaces()
471 471
472 472 # Key to signature value.
473 473 self._sparsesignaturecache = {}
474 474 # Signature to cached matcher instance.
475 475 self._sparsematchercache = {}
476 476
477 477 def _getvfsward(self, origfunc):
478 478 """build a ward for self.vfs"""
479 479 rref = weakref.ref(self)
480 480 def checkvfs(path, mode=None):
481 481 ret = origfunc(path, mode=mode)
482 482 repo = rref()
483 483 if (repo is None
484 484 or not util.safehasattr(repo, '_wlockref')
485 485 or not util.safehasattr(repo, '_lockref')):
486 486 return
487 487 if mode in (None, 'r', 'rb'):
488 488 return
489 489 if path.startswith(repo.path):
490 490 # truncate name relative to the repository (.hg)
491 491 path = path[len(repo.path) + 1:]
492 492 if path.startswith('cache/'):
493 493 msg = 'accessing cache with vfs instead of cachevfs: "%s"'
494 494 repo.ui.develwarn(msg % path, stacklevel=2, config="cache-vfs")
495 495 if path.startswith('journal.'):
496 496 # journal is covered by 'lock'
497 497 if repo._currentlock(repo._lockref) is None:
498 498 repo.ui.develwarn('write with no lock: "%s"' % path,
499 499 stacklevel=2, config='check-locks')
500 500 elif repo._currentlock(repo._wlockref) is None:
501 501 # rest of vfs files are covered by 'wlock'
502 502 #
503 503 # exclude special files
504 504 for prefix in self._wlockfreeprefix:
505 505 if path.startswith(prefix):
506 506 return
507 507 repo.ui.develwarn('write with no wlock: "%s"' % path,
508 508 stacklevel=2, config='check-locks')
509 509 return ret
510 510 return checkvfs
511 511
512 512 def _getsvfsward(self, origfunc):
513 513 """build a ward for self.svfs"""
514 514 rref = weakref.ref(self)
515 515 def checksvfs(path, mode=None):
516 516 ret = origfunc(path, mode=mode)
517 517 repo = rref()
518 518 if repo is None or not util.safehasattr(repo, '_lockref'):
519 519 return
520 520 if mode in (None, 'r', 'rb'):
521 521 return
522 522 if path.startswith(repo.sharedpath):
523 523 # truncate name relative to the repository (.hg)
524 524 path = path[len(repo.sharedpath) + 1:]
525 525 if repo._currentlock(repo._lockref) is None:
526 526 repo.ui.develwarn('write with no lock: "%s"' % path,
527 527 stacklevel=3)
528 528 return ret
529 529 return checksvfs
530 530
531 531 def close(self):
532 532 self._writecaches()
533 533
534 534 def _loadextensions(self):
535 535 extensions.loadall(self.ui)
536 536
537 537 def _writecaches(self):
538 538 if self._revbranchcache:
539 539 self._revbranchcache.write()
540 540
541 541 def _restrictcapabilities(self, caps):
542 542 if self.ui.configbool('experimental', 'bundle2-advertise'):
543 543 caps = set(caps)
544 544 capsblob = bundle2.encodecaps(bundle2.getrepocaps(self))
545 545 caps.add('bundle2=' + urlreq.quote(capsblob))
546 546 return caps
547 547
548 548 def _applyopenerreqs(self):
549 549 self.svfs.options = dict((r, 1) for r in self.requirements
550 550 if r in self.openerreqs)
551 551 # experimental config: format.chunkcachesize
552 552 chunkcachesize = self.ui.configint('format', 'chunkcachesize')
553 553 if chunkcachesize is not None:
554 554 self.svfs.options['chunkcachesize'] = chunkcachesize
555 555 # experimental config: format.maxchainlen
556 556 maxchainlen = self.ui.configint('format', 'maxchainlen')
557 557 if maxchainlen is not None:
558 558 self.svfs.options['maxchainlen'] = maxchainlen
559 559 # experimental config: format.manifestcachesize
560 560 manifestcachesize = self.ui.configint('format', 'manifestcachesize')
561 561 if manifestcachesize is not None:
562 562 self.svfs.options['manifestcachesize'] = manifestcachesize
563 563 # experimental config: format.aggressivemergedeltas
564 564 aggressivemergedeltas = self.ui.configbool('format',
565 565 'aggressivemergedeltas')
566 566 self.svfs.options['aggressivemergedeltas'] = aggressivemergedeltas
567 567 self.svfs.options['lazydeltabase'] = not scmutil.gddeltaconfig(self.ui)
568 568 chainspan = self.ui.configbytes('experimental', 'maxdeltachainspan', -1)
569 569 if 0 <= chainspan:
570 570 self.svfs.options['maxdeltachainspan'] = chainspan
571 571
572 572 for r in self.requirements:
573 573 if r.startswith('exp-compression-'):
574 574 self.svfs.options['compengine'] = r[len('exp-compression-'):]
575 575
576 576 # TODO move "revlogv2" to openerreqs once finalized.
577 577 if REVLOGV2_REQUIREMENT in self.requirements:
578 578 self.svfs.options['revlogv2'] = True
579 579
580 580 def _writerequirements(self):
581 581 scmutil.writerequires(self.vfs, self.requirements)
582 582
583 583 def _checknested(self, path):
584 584 """Determine if path is a legal nested repository."""
585 585 if not path.startswith(self.root):
586 586 return False
587 587 subpath = path[len(self.root) + 1:]
588 588 normsubpath = util.pconvert(subpath)
589 589
590 590 # XXX: Checking against the current working copy is wrong in
591 591 # the sense that it can reject things like
592 592 #
593 593 # $ hg cat -r 10 sub/x.txt
594 594 #
595 595 # if sub/ is no longer a subrepository in the working copy
596 596 # parent revision.
597 597 #
598 598 # However, it can of course also allow things that would have
599 599 # been rejected before, such as the above cat command if sub/
600 600 # is a subrepository now, but was a normal directory before.
601 601 # The old path auditor would have rejected by mistake since it
602 602 # panics when it sees sub/.hg/.
603 603 #
604 604 # All in all, checking against the working copy seems sensible
605 605 # since we want to prevent access to nested repositories on
606 606 # the filesystem *now*.
607 607 ctx = self[None]
608 608 parts = util.splitpath(subpath)
609 609 while parts:
610 610 prefix = '/'.join(parts)
611 611 if prefix in ctx.substate:
612 612 if prefix == normsubpath:
613 613 return True
614 614 else:
615 615 sub = ctx.sub(prefix)
616 616 return sub.checknested(subpath[len(prefix) + 1:])
617 617 else:
618 618 parts.pop()
619 619 return False
620 620
621 621 def peer(self):
622 622 return localpeer(self) # not cached to avoid reference cycle
623 623
624 624 def unfiltered(self):
625 625 """Return unfiltered version of the repository
626 626
627 627 Intended to be overwritten by filtered repo."""
628 628 return self
629 629
630 630 def filtered(self, name):
631 631 """Return a filtered version of a repository"""
632 632 # Python <3.4 easily leaks types via __mro__. See
633 633 # https://bugs.python.org/issue17950. We cache dynamically
634 634 # created types so this method doesn't leak on every
635 635 # invocation.
636 636
637 637 key = self.unfiltered().__class__
638 638 if key not in self._filteredrepotypes:
639 639 # Build a new type with the repoview mixin and the base
640 640 # class of this repo. Give it a name containing the
641 641 # filter name to aid debugging.
642 642 bases = (repoview.repoview, key)
643 643 cls = type(r'%sfilteredrepo' % name, bases, {})
644 644 self._filteredrepotypes[key] = cls
645 645
646 646 return self._filteredrepotypes[key](self, name)
647 647
648 648 @repofilecache('bookmarks', 'bookmarks.current')
649 649 def _bookmarks(self):
650 650 return bookmarks.bmstore(self)
651 651
652 652 @property
653 653 def _activebookmark(self):
654 654 return self._bookmarks.active
655 655
656 656 # _phaserevs and _phasesets depend on changelog. what we need is to
657 657 # call _phasecache.invalidate() if '00changelog.i' was changed, but it
658 658 # can't be easily expressed in filecache mechanism.
659 659 @storecache('phaseroots', '00changelog.i')
660 660 def _phasecache(self):
661 661 return phases.phasecache(self, self._phasedefaults)
662 662
663 663 @storecache('obsstore')
664 664 def obsstore(self):
665 665 return obsolete.makestore(self.ui, self)
666 666
667 667 @storecache('00changelog.i')
668 668 def changelog(self):
669 669 return changelog.changelog(self.svfs,
670 670 trypending=txnutil.mayhavepending(self.root))
671 671
672 672 def _constructmanifest(self):
673 673 # This is a temporary function while we migrate from manifest to
674 674 # manifestlog. It allows bundlerepo and unionrepo to intercept the
675 675 # manifest creation.
676 676 return manifest.manifestrevlog(self.svfs)
677 677
678 678 @storecache('00manifest.i')
679 679 def manifestlog(self):
680 680 return manifest.manifestlog(self.svfs, self)
681 681
682 682 @repofilecache('dirstate')
683 683 def dirstate(self):
684 684 sparsematchfn = lambda: sparse.matcher(self)
685 685
686 686 return dirstate.dirstate(self.vfs, self.ui, self.root,
687 687 self._dirstatevalidate, sparsematchfn)
688 688
689 689 def _dirstatevalidate(self, node):
690 690 try:
691 691 self.changelog.rev(node)
692 692 return node
693 693 except error.LookupError:
694 694 if not self._dirstatevalidatewarned:
695 695 self._dirstatevalidatewarned = True
696 696 self.ui.warn(_("warning: ignoring unknown"
697 697 " working parent %s!\n") % short(node))
698 698 return nullid
699 699
700 700 def __getitem__(self, changeid):
701 701 if changeid is None:
702 702 return context.workingctx(self)
703 703 if isinstance(changeid, slice):
704 704 # wdirrev isn't contiguous so the slice shouldn't include it
705 705 return [context.changectx(self, i)
706 706 for i in xrange(*changeid.indices(len(self)))
707 707 if i not in self.changelog.filteredrevs]
708 708 try:
709 709 return context.changectx(self, changeid)
710 710 except error.WdirUnsupported:
711 711 return context.workingctx(self)
712 712
713 713 def __contains__(self, changeid):
714 714 """True if the given changeid exists
715 715
716 716 error.LookupError is raised if an ambiguous node specified.
717 717 """
718 718 try:
719 719 self[changeid]
720 720 return True
721 721 except error.RepoLookupError:
722 722 return False
723 723
724 724 def __nonzero__(self):
725 725 return True
726 726
727 727 __bool__ = __nonzero__
728 728
729 729 def __len__(self):
730 730 return len(self.changelog)
731 731
732 732 def __iter__(self):
733 733 return iter(self.changelog)
734 734
735 735 def revs(self, expr, *args):
736 736 '''Find revisions matching a revset.
737 737
738 738 The revset is specified as a string ``expr`` that may contain
739 739 %-formatting to escape certain types. See ``revsetlang.formatspec``.
740 740
741 741 Revset aliases from the configuration are not expanded. To expand
742 742 user aliases, consider calling ``scmutil.revrange()`` or
743 743 ``repo.anyrevs([expr], user=True)``.
744 744
745 745 Returns a revset.abstractsmartset, which is a list-like interface
746 746 that contains integer revisions.
747 747 '''
748 748 expr = revsetlang.formatspec(expr, *args)
749 749 m = revset.match(None, expr)
750 750 return m(self)
751 751
752 752 def set(self, expr, *args):
753 753 '''Find revisions matching a revset and emit changectx instances.
754 754
755 755 This is a convenience wrapper around ``revs()`` that iterates the
756 756 result and is a generator of changectx instances.
757 757
758 758 Revset aliases from the configuration are not expanded. To expand
759 759 user aliases, consider calling ``scmutil.revrange()``.
760 760 '''
761 761 for r in self.revs(expr, *args):
762 762 yield self[r]
763 763
764 764 def anyrevs(self, specs, user=False, localalias=None):
765 765 '''Find revisions matching one of the given revsets.
766 766
767 767 Revset aliases from the configuration are not expanded by default. To
768 768 expand user aliases, specify ``user=True``. To provide some local
769 769 definitions overriding user aliases, set ``localalias`` to
770 770 ``{name: definitionstring}``.
771 771 '''
772 772 if user:
773 773 m = revset.matchany(self.ui, specs, repo=self,
774 774 localalias=localalias)
775 775 else:
776 776 m = revset.matchany(None, specs, localalias=localalias)
777 777 return m(self)
778 778
779 779 def url(self):
780 780 return 'file:' + self.root
781 781
782 782 def hook(self, name, throw=False, **args):
783 783 """Call a hook, passing this repo instance.
784 784
785 785 This a convenience method to aid invoking hooks. Extensions likely
786 786 won't call this unless they have registered a custom hook or are
787 787 replacing code that is expected to call a hook.
788 788 """
789 789 return hook.hook(self.ui, self, name, throw, **args)
790 790
791 791 @filteredpropertycache
792 792 def _tagscache(self):
793 793 '''Returns a tagscache object that contains various tags related
794 794 caches.'''
795 795
796 796 # This simplifies its cache management by having one decorated
797 797 # function (this one) and the rest simply fetch things from it.
798 798 class tagscache(object):
799 799 def __init__(self):
800 800 # These two define the set of tags for this repository. tags
801 801 # maps tag name to node; tagtypes maps tag name to 'global' or
802 802 # 'local'. (Global tags are defined by .hgtags across all
803 803 # heads, and local tags are defined in .hg/localtags.)
804 804 # They constitute the in-memory cache of tags.
805 805 self.tags = self.tagtypes = None
806 806
807 807 self.nodetagscache = self.tagslist = None
808 808
809 809 cache = tagscache()
810 810 cache.tags, cache.tagtypes = self._findtags()
811 811
812 812 return cache
813 813
814 814 def tags(self):
815 815 '''return a mapping of tag to node'''
816 816 t = {}
817 817 if self.changelog.filteredrevs:
818 818 tags, tt = self._findtags()
819 819 else:
820 820 tags = self._tagscache.tags
821 821 for k, v in tags.iteritems():
822 822 try:
823 823 # ignore tags to unknown nodes
824 824 self.changelog.rev(v)
825 825 t[k] = v
826 826 except (error.LookupError, ValueError):
827 827 pass
828 828 return t
829 829
830 830 def _findtags(self):
831 831 '''Do the hard work of finding tags. Return a pair of dicts
832 832 (tags, tagtypes) where tags maps tag name to node, and tagtypes
833 833 maps tag name to a string like \'global\' or \'local\'.
834 834 Subclasses or extensions are free to add their own tags, but
835 835 should be aware that the returned dicts will be retained for the
836 836 duration of the localrepo object.'''
837 837
838 838 # XXX what tagtype should subclasses/extensions use? Currently
839 839 # mq and bookmarks add tags, but do not set the tagtype at all.
840 840 # Should each extension invent its own tag type? Should there
841 841 # be one tagtype for all such "virtual" tags? Or is the status
842 842 # quo fine?
843 843
844 844
845 845 # map tag name to (node, hist)
846 846 alltags = tagsmod.findglobaltags(self.ui, self)
847 847 # map tag name to tag type
848 848 tagtypes = dict((tag, 'global') for tag in alltags)
849 849
850 850 tagsmod.readlocaltags(self.ui, self, alltags, tagtypes)
851 851
852 852 # Build the return dicts. Have to re-encode tag names because
853 853 # the tags module always uses UTF-8 (in order not to lose info
854 854 # writing to the cache), but the rest of Mercurial wants them in
855 855 # local encoding.
856 856 tags = {}
857 857 for (name, (node, hist)) in alltags.iteritems():
858 858 if node != nullid:
859 859 tags[encoding.tolocal(name)] = node
860 860 tags['tip'] = self.changelog.tip()
861 861 tagtypes = dict([(encoding.tolocal(name), value)
862 862 for (name, value) in tagtypes.iteritems()])
863 863 return (tags, tagtypes)
864 864
865 865 def tagtype(self, tagname):
866 866 '''
867 867 return the type of the given tag. result can be:
868 868
869 869 'local' : a local tag
870 870 'global' : a global tag
871 871 None : tag does not exist
872 872 '''
873 873
874 874 return self._tagscache.tagtypes.get(tagname)
875 875
876 876 def tagslist(self):
877 877 '''return a list of tags ordered by revision'''
878 878 if not self._tagscache.tagslist:
879 879 l = []
880 880 for t, n in self.tags().iteritems():
881 881 l.append((self.changelog.rev(n), t, n))
882 882 self._tagscache.tagslist = [(t, n) for r, t, n in sorted(l)]
883 883
884 884 return self._tagscache.tagslist
885 885
886 886 def nodetags(self, node):
887 887 '''return the tags associated with a node'''
888 888 if not self._tagscache.nodetagscache:
889 889 nodetagscache = {}
890 890 for t, n in self._tagscache.tags.iteritems():
891 891 nodetagscache.setdefault(n, []).append(t)
892 892 for tags in nodetagscache.itervalues():
893 893 tags.sort()
894 894 self._tagscache.nodetagscache = nodetagscache
895 895 return self._tagscache.nodetagscache.get(node, [])
896 896
897 897 def nodebookmarks(self, node):
898 898 """return the list of bookmarks pointing to the specified node"""
899 899 marks = []
900 900 for bookmark, n in self._bookmarks.iteritems():
901 901 if n == node:
902 902 marks.append(bookmark)
903 903 return sorted(marks)
904 904
905 905 def branchmap(self):
906 906 '''returns a dictionary {branch: [branchheads]} with branchheads
907 907 ordered by increasing revision number'''
908 908 branchmap.updatecache(self)
909 909 return self._branchcaches[self.filtername]
910 910
911 911 @unfilteredmethod
912 912 def revbranchcache(self):
913 913 if not self._revbranchcache:
914 914 self._revbranchcache = branchmap.revbranchcache(self.unfiltered())
915 915 return self._revbranchcache
916 916
917 917 def branchtip(self, branch, ignoremissing=False):
918 918 '''return the tip node for a given branch
919 919
920 920 If ignoremissing is True, then this method will not raise an error.
921 921 This is helpful for callers that only expect None for a missing branch
922 922 (e.g. namespace).
923 923
924 924 '''
925 925 try:
926 926 return self.branchmap().branchtip(branch)
927 927 except KeyError:
928 928 if not ignoremissing:
929 929 raise error.RepoLookupError(_("unknown branch '%s'") % branch)
930 930 else:
931 931 pass
932 932
933 933 def lookup(self, key):
934 934 return self[key].node()
935 935
936 936 def lookupbranch(self, key, remote=None):
937 937 repo = remote or self
938 938 if key in repo.branchmap():
939 939 return key
940 940
941 941 repo = (remote and remote.local()) and remote or self
942 942 return repo[key].branch()
943 943
944 944 def known(self, nodes):
945 945 cl = self.changelog
946 946 nm = cl.nodemap
947 947 filtered = cl.filteredrevs
948 948 result = []
949 949 for n in nodes:
950 950 r = nm.get(n)
951 951 resp = not (r is None or r in filtered)
952 952 result.append(resp)
953 953 return result
954 954
955 955 def local(self):
956 956 return self
957 957
958 958 def publishing(self):
959 959 # it's safe (and desirable) to trust the publish flag unconditionally
960 960 # so that we don't finalize changes shared between users via ssh or nfs
961 961 return self.ui.configbool('phases', 'publish', untrusted=True)
962 962
963 963 def cancopy(self):
964 964 # so statichttprepo's override of local() works
965 965 if not self.local():
966 966 return False
967 967 if not self.publishing():
968 968 return True
969 969 # if publishing we can't copy if there is filtered content
970 970 return not self.filtered('visible').changelog.filteredrevs
971 971
972 972 def shared(self):
973 973 '''the type of shared repository (None if not shared)'''
974 974 if self.sharedpath != self.path:
975 975 return 'store'
976 976 return None
977 977
978 978 def wjoin(self, f, *insidef):
979 979 return self.vfs.reljoin(self.root, f, *insidef)
980 980
981 981 def file(self, f):
982 982 if f[0] == '/':
983 983 f = f[1:]
984 984 return filelog.filelog(self.svfs, f)
985 985
986 986 def changectx(self, changeid):
987 987 return self[changeid]
988 988
989 989 def setparents(self, p1, p2=nullid):
990 990 with self.dirstate.parentchange():
991 991 copies = self.dirstate.setparents(p1, p2)
992 992 pctx = self[p1]
993 993 if copies:
994 994 # Adjust copy records, the dirstate cannot do it, it
995 995 # requires access to parents manifests. Preserve them
996 996 # only for entries added to first parent.
997 997 for f in copies:
998 998 if f not in pctx and copies[f] in pctx:
999 999 self.dirstate.copy(copies[f], f)
1000 1000 if p2 == nullid:
1001 1001 for f, s in sorted(self.dirstate.copies().items()):
1002 1002 if f not in pctx and s not in pctx:
1003 1003 self.dirstate.copy(None, f)
1004 1004
1005 1005 def filectx(self, path, changeid=None, fileid=None):
1006 1006 """changeid can be a changeset revision, node, or tag.
1007 1007 fileid can be a file revision or node."""
1008 1008 return context.filectx(self, path, changeid, fileid)
1009 1009
1010 1010 def getcwd(self):
1011 1011 return self.dirstate.getcwd()
1012 1012
1013 1013 def pathto(self, f, cwd=None):
1014 1014 return self.dirstate.pathto(f, cwd)
1015 1015
1016 1016 def _loadfilter(self, filter):
1017 1017 if filter not in self.filterpats:
1018 1018 l = []
1019 1019 for pat, cmd in self.ui.configitems(filter):
1020 1020 if cmd == '!':
1021 1021 continue
1022 1022 mf = matchmod.match(self.root, '', [pat])
1023 1023 fn = None
1024 1024 params = cmd
1025 1025 for name, filterfn in self._datafilters.iteritems():
1026 1026 if cmd.startswith(name):
1027 1027 fn = filterfn
1028 1028 params = cmd[len(name):].lstrip()
1029 1029 break
1030 1030 if not fn:
1031 1031 fn = lambda s, c, **kwargs: util.filter(s, c)
1032 1032 # Wrap old filters not supporting keyword arguments
1033 1033 if not inspect.getargspec(fn)[2]:
1034 1034 oldfn = fn
1035 1035 fn = lambda s, c, **kwargs: oldfn(s, c)
1036 1036 l.append((mf, fn, params))
1037 1037 self.filterpats[filter] = l
1038 1038 return self.filterpats[filter]
1039 1039
1040 1040 def _filter(self, filterpats, filename, data):
1041 1041 for mf, fn, cmd in filterpats:
1042 1042 if mf(filename):
1043 1043 self.ui.debug("filtering %s through %s\n" % (filename, cmd))
1044 1044 data = fn(data, cmd, ui=self.ui, repo=self, filename=filename)
1045 1045 break
1046 1046
1047 1047 return data
1048 1048
1049 1049 @unfilteredpropertycache
1050 1050 def _encodefilterpats(self):
1051 1051 return self._loadfilter('encode')
1052 1052
1053 1053 @unfilteredpropertycache
1054 1054 def _decodefilterpats(self):
1055 1055 return self._loadfilter('decode')
1056 1056
1057 1057 def adddatafilter(self, name, filter):
1058 1058 self._datafilters[name] = filter
1059 1059
1060 1060 def wread(self, filename):
1061 1061 if self.wvfs.islink(filename):
1062 1062 data = self.wvfs.readlink(filename)
1063 1063 else:
1064 1064 data = self.wvfs.read(filename)
1065 1065 return self._filter(self._encodefilterpats, filename, data)
1066 1066
1067 1067 def wwrite(self, filename, data, flags, backgroundclose=False):
1068 1068 """write ``data`` into ``filename`` in the working directory
1069 1069
1070 1070 This returns length of written (maybe decoded) data.
1071 1071 """
1072 1072 data = self._filter(self._decodefilterpats, filename, data)
1073 1073 if 'l' in flags:
1074 1074 self.wvfs.symlink(data, filename)
1075 1075 else:
1076 1076 self.wvfs.write(filename, data, backgroundclose=backgroundclose)
1077 1077 if 'x' in flags:
1078 1078 self.wvfs.setflags(filename, False, True)
1079 1079 return len(data)
1080 1080
1081 1081 def wwritedata(self, filename, data):
1082 1082 return self._filter(self._decodefilterpats, filename, data)
1083 1083
1084 1084 def currenttransaction(self):
1085 1085 """return the current transaction or None if non exists"""
1086 1086 if self._transref:
1087 1087 tr = self._transref()
1088 1088 else:
1089 1089 tr = None
1090 1090
1091 1091 if tr and tr.running():
1092 1092 return tr
1093 1093 return None
1094 1094
1095 1095 def transaction(self, desc, report=None):
1096 1096 if (self.ui.configbool('devel', 'all-warnings')
1097 1097 or self.ui.configbool('devel', 'check-locks')):
1098 1098 if self._currentlock(self._lockref) is None:
1099 1099 raise error.ProgrammingError('transaction requires locking')
1100 1100 tr = self.currenttransaction()
1101 1101 if tr is not None:
1102 scmutil.registersummarycallback(self, tr, desc)
1102 1103 return tr.nest()
1103 1104
1104 1105 # abort here if the journal already exists
1105 1106 if self.svfs.exists("journal"):
1106 1107 raise error.RepoError(
1107 1108 _("abandoned transaction found"),
1108 1109 hint=_("run 'hg recover' to clean up transaction"))
1109 1110
1110 1111 idbase = "%.40f#%f" % (random.random(), time.time())
1111 1112 ha = hex(hashlib.sha1(idbase).digest())
1112 1113 txnid = 'TXN:' + ha
1113 1114 self.hook('pretxnopen', throw=True, txnname=desc, txnid=txnid)
1114 1115
1115 1116 self._writejournal(desc)
1116 1117 renames = [(vfs, x, undoname(x)) for vfs, x in self._journalfiles()]
1117 1118 if report:
1118 1119 rp = report
1119 1120 else:
1120 1121 rp = self.ui.warn
1121 1122 vfsmap = {'plain': self.vfs} # root of .hg/
1122 1123 # we must avoid cyclic reference between repo and transaction.
1123 1124 reporef = weakref.ref(self)
1124 1125 # Code to track tag movement
1125 1126 #
1126 1127 # Since tags are all handled as file content, it is actually quite hard
1127 1128 # to track these movement from a code perspective. So we fallback to a
1128 1129 # tracking at the repository level. One could envision to track changes
1129 1130 # to the '.hgtags' file through changegroup apply but that fails to
1130 1131 # cope with case where transaction expose new heads without changegroup
1131 1132 # being involved (eg: phase movement).
1132 1133 #
1133 1134 # For now, We gate the feature behind a flag since this likely comes
1134 1135 # with performance impacts. The current code run more often than needed
1135 1136 # and do not use caches as much as it could. The current focus is on
1136 1137 # the behavior of the feature so we disable it by default. The flag
1137 1138 # will be removed when we are happy with the performance impact.
1138 1139 #
1139 1140 # Once this feature is no longer experimental move the following
1140 1141 # documentation to the appropriate help section:
1141 1142 #
1142 1143 # The ``HG_TAG_MOVED`` variable will be set if the transaction touched
1143 1144 # tags (new or changed or deleted tags). In addition the details of
1144 1145 # these changes are made available in a file at:
1145 1146 # ``REPOROOT/.hg/changes/tags.changes``.
1146 1147 # Make sure you check for HG_TAG_MOVED before reading that file as it
1147 1148 # might exist from a previous transaction even if no tag were touched
1148 1149 # in this one. Changes are recorded in a line base format::
1149 1150 #
1150 1151 # <action> <hex-node> <tag-name>\n
1151 1152 #
1152 1153 # Actions are defined as follow:
1153 1154 # "-R": tag is removed,
1154 1155 # "+A": tag is added,
1155 1156 # "-M": tag is moved (old value),
1156 1157 # "+M": tag is moved (new value),
1157 1158 tracktags = lambda x: None
1158 1159 # experimental config: experimental.hook-track-tags
1159 1160 shouldtracktags = self.ui.configbool('experimental', 'hook-track-tags')
1160 1161 if desc != 'strip' and shouldtracktags:
1161 1162 oldheads = self.changelog.headrevs()
1162 1163 def tracktags(tr2):
1163 1164 repo = reporef()
1164 1165 oldfnodes = tagsmod.fnoderevs(repo.ui, repo, oldheads)
1165 1166 newheads = repo.changelog.headrevs()
1166 1167 newfnodes = tagsmod.fnoderevs(repo.ui, repo, newheads)
1167 1168 # notes: we compare lists here.
1168 1169 # As we do it only once buiding set would not be cheaper
1169 1170 changes = tagsmod.difftags(repo.ui, repo, oldfnodes, newfnodes)
1170 1171 if changes:
1171 1172 tr2.hookargs['tag_moved'] = '1'
1172 1173 with repo.vfs('changes/tags.changes', 'w',
1173 1174 atomictemp=True) as changesfile:
1174 1175 # note: we do not register the file to the transaction
1175 1176 # because we needs it to still exist on the transaction
1176 1177 # is close (for txnclose hooks)
1177 1178 tagsmod.writediff(changesfile, changes)
1178 1179 def validate(tr2):
1179 1180 """will run pre-closing hooks"""
1180 1181 # XXX the transaction API is a bit lacking here so we take a hacky
1181 1182 # path for now
1182 1183 #
1183 1184 # We cannot add this as a "pending" hooks since the 'tr.hookargs'
1184 1185 # dict is copied before these run. In addition we needs the data
1185 1186 # available to in memory hooks too.
1186 1187 #
1187 1188 # Moreover, we also need to make sure this runs before txnclose
1188 1189 # hooks and there is no "pending" mechanism that would execute
1189 1190 # logic only if hooks are about to run.
1190 1191 #
1191 1192 # Fixing this limitation of the transaction is also needed to track
1192 1193 # other families of changes (bookmarks, phases, obsolescence).
1193 1194 #
1194 1195 # This will have to be fixed before we remove the experimental
1195 1196 # gating.
1196 1197 tracktags(tr2)
1197 1198 reporef().hook('pretxnclose', throw=True,
1198 1199 txnname=desc, **pycompat.strkwargs(tr.hookargs))
1199 1200 def releasefn(tr, success):
1200 1201 repo = reporef()
1201 1202 if success:
1202 1203 # this should be explicitly invoked here, because
1203 1204 # in-memory changes aren't written out at closing
1204 1205 # transaction, if tr.addfilegenerator (via
1205 1206 # dirstate.write or so) isn't invoked while
1206 1207 # transaction running
1207 1208 repo.dirstate.write(None)
1208 1209 else:
1209 1210 # discard all changes (including ones already written
1210 1211 # out) in this transaction
1211 1212 repo.dirstate.restorebackup(None, 'journal.dirstate')
1212 1213
1213 1214 repo.invalidate(clearfilecache=True)
1214 1215
1215 1216 tr = transaction.transaction(rp, self.svfs, vfsmap,
1216 1217 "journal",
1217 1218 "undo",
1218 1219 aftertrans(renames),
1219 1220 self.store.createmode,
1220 1221 validator=validate,
1221 1222 releasefn=releasefn,
1222 1223 checkambigfiles=_cachedfiles)
1223 1224 tr.changes['revs'] = set()
1224 1225 tr.changes['obsmarkers'] = set()
1225 1226 tr.changes['phases'] = {}
1226 1227 tr.changes['bookmarks'] = {}
1227 1228
1228 1229 tr.hookargs['txnid'] = txnid
1229 1230 # note: writing the fncache only during finalize mean that the file is
1230 1231 # outdated when running hooks. As fncache is used for streaming clone,
1231 1232 # this is not expected to break anything that happen during the hooks.
1232 1233 tr.addfinalize('flush-fncache', self.store.write)
1233 1234 def txnclosehook(tr2):
1234 1235 """To be run if transaction is successful, will schedule a hook run
1235 1236 """
1236 1237 # Don't reference tr2 in hook() so we don't hold a reference.
1237 1238 # This reduces memory consumption when there are multiple
1238 1239 # transactions per lock. This can likely go away if issue5045
1239 1240 # fixes the function accumulation.
1240 1241 hookargs = tr2.hookargs
1241 1242
1242 1243 def hook():
1243 1244 reporef().hook('txnclose', throw=False, txnname=desc,
1244 1245 **pycompat.strkwargs(hookargs))
1245 1246 reporef()._afterlock(hook)
1246 1247 tr.addfinalize('txnclose-hook', txnclosehook)
1247 1248 tr.addpostclose('warms-cache', self._buildcacheupdater(tr))
1248 1249 def txnaborthook(tr2):
1249 1250 """To be run if transaction is aborted
1250 1251 """
1251 1252 reporef().hook('txnabort', throw=False, txnname=desc,
1252 1253 **tr2.hookargs)
1253 1254 tr.addabort('txnabort-hook', txnaborthook)
1254 1255 # avoid eager cache invalidation. in-memory data should be identical
1255 1256 # to stored data if transaction has no error.
1256 1257 tr.addpostclose('refresh-filecachestats', self._refreshfilecachestats)
1257 1258 self._transref = weakref.ref(tr)
1259 scmutil.registersummarycallback(self, tr, desc)
1258 1260 return tr
1259 1261
1260 1262 def _journalfiles(self):
1261 1263 return ((self.svfs, 'journal'),
1262 1264 (self.vfs, 'journal.dirstate'),
1263 1265 (self.vfs, 'journal.branch'),
1264 1266 (self.vfs, 'journal.desc'),
1265 1267 (self.vfs, 'journal.bookmarks'),
1266 1268 (self.svfs, 'journal.phaseroots'))
1267 1269
1268 1270 def undofiles(self):
1269 1271 return [(vfs, undoname(x)) for vfs, x in self._journalfiles()]
1270 1272
1271 1273 @unfilteredmethod
1272 1274 def _writejournal(self, desc):
1273 1275 self.dirstate.savebackup(None, 'journal.dirstate')
1274 1276 self.vfs.write("journal.branch",
1275 1277 encoding.fromlocal(self.dirstate.branch()))
1276 1278 self.vfs.write("journal.desc",
1277 1279 "%d\n%s\n" % (len(self), desc))
1278 1280 self.vfs.write("journal.bookmarks",
1279 1281 self.vfs.tryread("bookmarks"))
1280 1282 self.svfs.write("journal.phaseroots",
1281 1283 self.svfs.tryread("phaseroots"))
1282 1284
1283 1285 def recover(self):
1284 1286 with self.lock():
1285 1287 if self.svfs.exists("journal"):
1286 1288 self.ui.status(_("rolling back interrupted transaction\n"))
1287 1289 vfsmap = {'': self.svfs,
1288 1290 'plain': self.vfs,}
1289 1291 transaction.rollback(self.svfs, vfsmap, "journal",
1290 1292 self.ui.warn,
1291 1293 checkambigfiles=_cachedfiles)
1292 1294 self.invalidate()
1293 1295 return True
1294 1296 else:
1295 1297 self.ui.warn(_("no interrupted transaction available\n"))
1296 1298 return False
1297 1299
1298 1300 def rollback(self, dryrun=False, force=False):
1299 1301 wlock = lock = dsguard = None
1300 1302 try:
1301 1303 wlock = self.wlock()
1302 1304 lock = self.lock()
1303 1305 if self.svfs.exists("undo"):
1304 1306 dsguard = dirstateguard.dirstateguard(self, 'rollback')
1305 1307
1306 1308 return self._rollback(dryrun, force, dsguard)
1307 1309 else:
1308 1310 self.ui.warn(_("no rollback information available\n"))
1309 1311 return 1
1310 1312 finally:
1311 1313 release(dsguard, lock, wlock)
1312 1314
1313 1315 @unfilteredmethod # Until we get smarter cache management
1314 1316 def _rollback(self, dryrun, force, dsguard):
1315 1317 ui = self.ui
1316 1318 try:
1317 1319 args = self.vfs.read('undo.desc').splitlines()
1318 1320 (oldlen, desc, detail) = (int(args[0]), args[1], None)
1319 1321 if len(args) >= 3:
1320 1322 detail = args[2]
1321 1323 oldtip = oldlen - 1
1322 1324
1323 1325 if detail and ui.verbose:
1324 1326 msg = (_('repository tip rolled back to revision %d'
1325 1327 ' (undo %s: %s)\n')
1326 1328 % (oldtip, desc, detail))
1327 1329 else:
1328 1330 msg = (_('repository tip rolled back to revision %d'
1329 1331 ' (undo %s)\n')
1330 1332 % (oldtip, desc))
1331 1333 except IOError:
1332 1334 msg = _('rolling back unknown transaction\n')
1333 1335 desc = None
1334 1336
1335 1337 if not force and self['.'] != self['tip'] and desc == 'commit':
1336 1338 raise error.Abort(
1337 1339 _('rollback of last commit while not checked out '
1338 1340 'may lose data'), hint=_('use -f to force'))
1339 1341
1340 1342 ui.status(msg)
1341 1343 if dryrun:
1342 1344 return 0
1343 1345
1344 1346 parents = self.dirstate.parents()
1345 1347 self.destroying()
1346 1348 vfsmap = {'plain': self.vfs, '': self.svfs}
1347 1349 transaction.rollback(self.svfs, vfsmap, 'undo', ui.warn,
1348 1350 checkambigfiles=_cachedfiles)
1349 1351 if self.vfs.exists('undo.bookmarks'):
1350 1352 self.vfs.rename('undo.bookmarks', 'bookmarks', checkambig=True)
1351 1353 if self.svfs.exists('undo.phaseroots'):
1352 1354 self.svfs.rename('undo.phaseroots', 'phaseroots', checkambig=True)
1353 1355 self.invalidate()
1354 1356
1355 1357 parentgone = (parents[0] not in self.changelog.nodemap or
1356 1358 parents[1] not in self.changelog.nodemap)
1357 1359 if parentgone:
1358 1360 # prevent dirstateguard from overwriting already restored one
1359 1361 dsguard.close()
1360 1362
1361 1363 self.dirstate.restorebackup(None, 'undo.dirstate')
1362 1364 try:
1363 1365 branch = self.vfs.read('undo.branch')
1364 1366 self.dirstate.setbranch(encoding.tolocal(branch))
1365 1367 except IOError:
1366 1368 ui.warn(_('named branch could not be reset: '
1367 1369 'current branch is still \'%s\'\n')
1368 1370 % self.dirstate.branch())
1369 1371
1370 1372 parents = tuple([p.rev() for p in self[None].parents()])
1371 1373 if len(parents) > 1:
1372 1374 ui.status(_('working directory now based on '
1373 1375 'revisions %d and %d\n') % parents)
1374 1376 else:
1375 1377 ui.status(_('working directory now based on '
1376 1378 'revision %d\n') % parents)
1377 1379 mergemod.mergestate.clean(self, self['.'].node())
1378 1380
1379 1381 # TODO: if we know which new heads may result from this rollback, pass
1380 1382 # them to destroy(), which will prevent the branchhead cache from being
1381 1383 # invalidated.
1382 1384 self.destroyed()
1383 1385 return 0
1384 1386
1385 1387 def _buildcacheupdater(self, newtransaction):
1386 1388 """called during transaction to build the callback updating cache
1387 1389
1388 1390 Lives on the repository to help extension who might want to augment
1389 1391 this logic. For this purpose, the created transaction is passed to the
1390 1392 method.
1391 1393 """
1392 1394 # we must avoid cyclic reference between repo and transaction.
1393 1395 reporef = weakref.ref(self)
1394 1396 def updater(tr):
1395 1397 repo = reporef()
1396 1398 repo.updatecaches(tr)
1397 1399 return updater
1398 1400
1399 1401 @unfilteredmethod
1400 1402 def updatecaches(self, tr=None):
1401 1403 """warm appropriate caches
1402 1404
1403 1405 If this function is called after a transaction closed. The transaction
1404 1406 will be available in the 'tr' argument. This can be used to selectively
1405 1407 update caches relevant to the changes in that transaction.
1406 1408 """
1407 1409 if tr is not None and tr.hookargs.get('source') == 'strip':
1408 1410 # During strip, many caches are invalid but
1409 1411 # later call to `destroyed` will refresh them.
1410 1412 return
1411 1413
1412 1414 if tr is None or tr.changes['revs']:
1413 1415 # updating the unfiltered branchmap should refresh all the others,
1414 1416 self.ui.debug('updating the branch cache\n')
1415 1417 branchmap.updatecache(self.filtered('served'))
1416 1418
1417 1419 def invalidatecaches(self):
1418 1420
1419 1421 if '_tagscache' in vars(self):
1420 1422 # can't use delattr on proxy
1421 1423 del self.__dict__['_tagscache']
1422 1424
1423 1425 self.unfiltered()._branchcaches.clear()
1424 1426 self.invalidatevolatilesets()
1425 1427 self._sparsesignaturecache.clear()
1426 1428
1427 1429 def invalidatevolatilesets(self):
1428 1430 self.filteredrevcache.clear()
1429 1431 obsolete.clearobscaches(self)
1430 1432
1431 1433 def invalidatedirstate(self):
1432 1434 '''Invalidates the dirstate, causing the next call to dirstate
1433 1435 to check if it was modified since the last time it was read,
1434 1436 rereading it if it has.
1435 1437
1436 1438 This is different to dirstate.invalidate() that it doesn't always
1437 1439 rereads the dirstate. Use dirstate.invalidate() if you want to
1438 1440 explicitly read the dirstate again (i.e. restoring it to a previous
1439 1441 known good state).'''
1440 1442 if hasunfilteredcache(self, 'dirstate'):
1441 1443 for k in self.dirstate._filecache:
1442 1444 try:
1443 1445 delattr(self.dirstate, k)
1444 1446 except AttributeError:
1445 1447 pass
1446 1448 delattr(self.unfiltered(), 'dirstate')
1447 1449
1448 1450 def invalidate(self, clearfilecache=False):
1449 1451 '''Invalidates both store and non-store parts other than dirstate
1450 1452
1451 1453 If a transaction is running, invalidation of store is omitted,
1452 1454 because discarding in-memory changes might cause inconsistency
1453 1455 (e.g. incomplete fncache causes unintentional failure, but
1454 1456 redundant one doesn't).
1455 1457 '''
1456 1458 unfiltered = self.unfiltered() # all file caches are stored unfiltered
1457 1459 for k in list(self._filecache.keys()):
1458 1460 # dirstate is invalidated separately in invalidatedirstate()
1459 1461 if k == 'dirstate':
1460 1462 continue
1461 1463
1462 1464 if clearfilecache:
1463 1465 del self._filecache[k]
1464 1466 try:
1465 1467 delattr(unfiltered, k)
1466 1468 except AttributeError:
1467 1469 pass
1468 1470 self.invalidatecaches()
1469 1471 if not self.currenttransaction():
1470 1472 # TODO: Changing contents of store outside transaction
1471 1473 # causes inconsistency. We should make in-memory store
1472 1474 # changes detectable, and abort if changed.
1473 1475 self.store.invalidatecaches()
1474 1476
1475 1477 def invalidateall(self):
1476 1478 '''Fully invalidates both store and non-store parts, causing the
1477 1479 subsequent operation to reread any outside changes.'''
1478 1480 # extension should hook this to invalidate its caches
1479 1481 self.invalidate()
1480 1482 self.invalidatedirstate()
1481 1483
1482 1484 @unfilteredmethod
1483 1485 def _refreshfilecachestats(self, tr):
1484 1486 """Reload stats of cached files so that they are flagged as valid"""
1485 1487 for k, ce in self._filecache.items():
1486 1488 if k == 'dirstate' or k not in self.__dict__:
1487 1489 continue
1488 1490 ce.refresh()
1489 1491
1490 1492 def _lock(self, vfs, lockname, wait, releasefn, acquirefn, desc,
1491 1493 inheritchecker=None, parentenvvar=None):
1492 1494 parentlock = None
1493 1495 # the contents of parentenvvar are used by the underlying lock to
1494 1496 # determine whether it can be inherited
1495 1497 if parentenvvar is not None:
1496 1498 parentlock = encoding.environ.get(parentenvvar)
1497 1499 try:
1498 1500 l = lockmod.lock(vfs, lockname, 0, releasefn=releasefn,
1499 1501 acquirefn=acquirefn, desc=desc,
1500 1502 inheritchecker=inheritchecker,
1501 1503 parentlock=parentlock)
1502 1504 except error.LockHeld as inst:
1503 1505 if not wait:
1504 1506 raise
1505 1507 # show more details for new-style locks
1506 1508 if ':' in inst.locker:
1507 1509 host, pid = inst.locker.split(":", 1)
1508 1510 self.ui.warn(
1509 1511 _("waiting for lock on %s held by process %r "
1510 1512 "on host %r\n") % (desc, pid, host))
1511 1513 else:
1512 1514 self.ui.warn(_("waiting for lock on %s held by %r\n") %
1513 1515 (desc, inst.locker))
1514 1516 # default to 600 seconds timeout
1515 1517 l = lockmod.lock(vfs, lockname,
1516 1518 int(self.ui.config("ui", "timeout")),
1517 1519 releasefn=releasefn, acquirefn=acquirefn,
1518 1520 desc=desc)
1519 1521 self.ui.warn(_("got lock after %s seconds\n") % l.delay)
1520 1522 return l
1521 1523
1522 1524 def _afterlock(self, callback):
1523 1525 """add a callback to be run when the repository is fully unlocked
1524 1526
1525 1527 The callback will be executed when the outermost lock is released
1526 1528 (with wlock being higher level than 'lock')."""
1527 1529 for ref in (self._wlockref, self._lockref):
1528 1530 l = ref and ref()
1529 1531 if l and l.held:
1530 1532 l.postrelease.append(callback)
1531 1533 break
1532 1534 else: # no lock have been found.
1533 1535 callback()
1534 1536
1535 1537 def lock(self, wait=True):
1536 1538 '''Lock the repository store (.hg/store) and return a weak reference
1537 1539 to the lock. Use this before modifying the store (e.g. committing or
1538 1540 stripping). If you are opening a transaction, get a lock as well.)
1539 1541
1540 1542 If both 'lock' and 'wlock' must be acquired, ensure you always acquires
1541 1543 'wlock' first to avoid a dead-lock hazard.'''
1542 1544 l = self._currentlock(self._lockref)
1543 1545 if l is not None:
1544 1546 l.lock()
1545 1547 return l
1546 1548
1547 1549 l = self._lock(self.svfs, "lock", wait, None,
1548 1550 self.invalidate, _('repository %s') % self.origroot)
1549 1551 self._lockref = weakref.ref(l)
1550 1552 return l
1551 1553
1552 1554 def _wlockchecktransaction(self):
1553 1555 if self.currenttransaction() is not None:
1554 1556 raise error.LockInheritanceContractViolation(
1555 1557 'wlock cannot be inherited in the middle of a transaction')
1556 1558
1557 1559 def wlock(self, wait=True):
1558 1560 '''Lock the non-store parts of the repository (everything under
1559 1561 .hg except .hg/store) and return a weak reference to the lock.
1560 1562
1561 1563 Use this before modifying files in .hg.
1562 1564
1563 1565 If both 'lock' and 'wlock' must be acquired, ensure you always acquires
1564 1566 'wlock' first to avoid a dead-lock hazard.'''
1565 1567 l = self._wlockref and self._wlockref()
1566 1568 if l is not None and l.held:
1567 1569 l.lock()
1568 1570 return l
1569 1571
1570 1572 # We do not need to check for non-waiting lock acquisition. Such
1571 1573 # acquisition would not cause dead-lock as they would just fail.
1572 1574 if wait and (self.ui.configbool('devel', 'all-warnings')
1573 1575 or self.ui.configbool('devel', 'check-locks')):
1574 1576 if self._currentlock(self._lockref) is not None:
1575 1577 self.ui.develwarn('"wlock" acquired after "lock"')
1576 1578
1577 1579 def unlock():
1578 1580 if self.dirstate.pendingparentchange():
1579 1581 self.dirstate.invalidate()
1580 1582 else:
1581 1583 self.dirstate.write(None)
1582 1584
1583 1585 self._filecache['dirstate'].refresh()
1584 1586
1585 1587 l = self._lock(self.vfs, "wlock", wait, unlock,
1586 1588 self.invalidatedirstate, _('working directory of %s') %
1587 1589 self.origroot,
1588 1590 inheritchecker=self._wlockchecktransaction,
1589 1591 parentenvvar='HG_WLOCK_LOCKER')
1590 1592 self._wlockref = weakref.ref(l)
1591 1593 return l
1592 1594
1593 1595 def _currentlock(self, lockref):
1594 1596 """Returns the lock if it's held, or None if it's not."""
1595 1597 if lockref is None:
1596 1598 return None
1597 1599 l = lockref()
1598 1600 if l is None or not l.held:
1599 1601 return None
1600 1602 return l
1601 1603
1602 1604 def currentwlock(self):
1603 1605 """Returns the wlock if it's held, or None if it's not."""
1604 1606 return self._currentlock(self._wlockref)
1605 1607
1606 1608 def _filecommit(self, fctx, manifest1, manifest2, linkrev, tr, changelist):
1607 1609 """
1608 1610 commit an individual file as part of a larger transaction
1609 1611 """
1610 1612
1611 1613 fname = fctx.path()
1612 1614 fparent1 = manifest1.get(fname, nullid)
1613 1615 fparent2 = manifest2.get(fname, nullid)
1614 1616 if isinstance(fctx, context.filectx):
1615 1617 node = fctx.filenode()
1616 1618 if node in [fparent1, fparent2]:
1617 1619 self.ui.debug('reusing %s filelog entry\n' % fname)
1618 1620 if manifest1.flags(fname) != fctx.flags():
1619 1621 changelist.append(fname)
1620 1622 return node
1621 1623
1622 1624 flog = self.file(fname)
1623 1625 meta = {}
1624 1626 copy = fctx.renamed()
1625 1627 if copy and copy[0] != fname:
1626 1628 # Mark the new revision of this file as a copy of another
1627 1629 # file. This copy data will effectively act as a parent
1628 1630 # of this new revision. If this is a merge, the first
1629 1631 # parent will be the nullid (meaning "look up the copy data")
1630 1632 # and the second one will be the other parent. For example:
1631 1633 #
1632 1634 # 0 --- 1 --- 3 rev1 changes file foo
1633 1635 # \ / rev2 renames foo to bar and changes it
1634 1636 # \- 2 -/ rev3 should have bar with all changes and
1635 1637 # should record that bar descends from
1636 1638 # bar in rev2 and foo in rev1
1637 1639 #
1638 1640 # this allows this merge to succeed:
1639 1641 #
1640 1642 # 0 --- 1 --- 3 rev4 reverts the content change from rev2
1641 1643 # \ / merging rev3 and rev4 should use bar@rev2
1642 1644 # \- 2 --- 4 as the merge base
1643 1645 #
1644 1646
1645 1647 cfname = copy[0]
1646 1648 crev = manifest1.get(cfname)
1647 1649 newfparent = fparent2
1648 1650
1649 1651 if manifest2: # branch merge
1650 1652 if fparent2 == nullid or crev is None: # copied on remote side
1651 1653 if cfname in manifest2:
1652 1654 crev = manifest2[cfname]
1653 1655 newfparent = fparent1
1654 1656
1655 1657 # Here, we used to search backwards through history to try to find
1656 1658 # where the file copy came from if the source of a copy was not in
1657 1659 # the parent directory. However, this doesn't actually make sense to
1658 1660 # do (what does a copy from something not in your working copy even
1659 1661 # mean?) and it causes bugs (eg, issue4476). Instead, we will warn
1660 1662 # the user that copy information was dropped, so if they didn't
1661 1663 # expect this outcome it can be fixed, but this is the correct
1662 1664 # behavior in this circumstance.
1663 1665
1664 1666 if crev:
1665 1667 self.ui.debug(" %s: copy %s:%s\n" % (fname, cfname, hex(crev)))
1666 1668 meta["copy"] = cfname
1667 1669 meta["copyrev"] = hex(crev)
1668 1670 fparent1, fparent2 = nullid, newfparent
1669 1671 else:
1670 1672 self.ui.warn(_("warning: can't find ancestor for '%s' "
1671 1673 "copied from '%s'!\n") % (fname, cfname))
1672 1674
1673 1675 elif fparent1 == nullid:
1674 1676 fparent1, fparent2 = fparent2, nullid
1675 1677 elif fparent2 != nullid:
1676 1678 # is one parent an ancestor of the other?
1677 1679 fparentancestors = flog.commonancestorsheads(fparent1, fparent2)
1678 1680 if fparent1 in fparentancestors:
1679 1681 fparent1, fparent2 = fparent2, nullid
1680 1682 elif fparent2 in fparentancestors:
1681 1683 fparent2 = nullid
1682 1684
1683 1685 # is the file changed?
1684 1686 text = fctx.data()
1685 1687 if fparent2 != nullid or flog.cmp(fparent1, text) or meta:
1686 1688 changelist.append(fname)
1687 1689 return flog.add(text, meta, tr, linkrev, fparent1, fparent2)
1688 1690 # are just the flags changed during merge?
1689 1691 elif fname in manifest1 and manifest1.flags(fname) != fctx.flags():
1690 1692 changelist.append(fname)
1691 1693
1692 1694 return fparent1
1693 1695
1694 1696 def checkcommitpatterns(self, wctx, vdirs, match, status, fail):
1695 1697 """check for commit arguments that aren't committable"""
1696 1698 if match.isexact() or match.prefix():
1697 1699 matched = set(status.modified + status.added + status.removed)
1698 1700
1699 1701 for f in match.files():
1700 1702 f = self.dirstate.normalize(f)
1701 1703 if f == '.' or f in matched or f in wctx.substate:
1702 1704 continue
1703 1705 if f in status.deleted:
1704 1706 fail(f, _('file not found!'))
1705 1707 if f in vdirs: # visited directory
1706 1708 d = f + '/'
1707 1709 for mf in matched:
1708 1710 if mf.startswith(d):
1709 1711 break
1710 1712 else:
1711 1713 fail(f, _("no match under directory!"))
1712 1714 elif f not in self.dirstate:
1713 1715 fail(f, _("file not tracked!"))
1714 1716
1715 1717 @unfilteredmethod
1716 1718 def commit(self, text="", user=None, date=None, match=None, force=False,
1717 1719 editor=False, extra=None):
1718 1720 """Add a new revision to current repository.
1719 1721
1720 1722 Revision information is gathered from the working directory,
1721 1723 match can be used to filter the committed files. If editor is
1722 1724 supplied, it is called to get a commit message.
1723 1725 """
1724 1726 if extra is None:
1725 1727 extra = {}
1726 1728
1727 1729 def fail(f, msg):
1728 1730 raise error.Abort('%s: %s' % (f, msg))
1729 1731
1730 1732 if not match:
1731 1733 match = matchmod.always(self.root, '')
1732 1734
1733 1735 if not force:
1734 1736 vdirs = []
1735 1737 match.explicitdir = vdirs.append
1736 1738 match.bad = fail
1737 1739
1738 1740 wlock = lock = tr = None
1739 1741 try:
1740 1742 wlock = self.wlock()
1741 1743 lock = self.lock() # for recent changelog (see issue4368)
1742 1744
1743 1745 wctx = self[None]
1744 1746 merge = len(wctx.parents()) > 1
1745 1747
1746 1748 if not force and merge and not match.always():
1747 1749 raise error.Abort(_('cannot partially commit a merge '
1748 1750 '(do not specify files or patterns)'))
1749 1751
1750 1752 status = self.status(match=match, clean=force)
1751 1753 if force:
1752 1754 status.modified.extend(status.clean) # mq may commit clean files
1753 1755
1754 1756 # check subrepos
1755 1757 subs = []
1756 1758 commitsubs = set()
1757 1759 newstate = wctx.substate.copy()
1758 1760 # only manage subrepos and .hgsubstate if .hgsub is present
1759 1761 if '.hgsub' in wctx:
1760 1762 # we'll decide whether to track this ourselves, thanks
1761 1763 for c in status.modified, status.added, status.removed:
1762 1764 if '.hgsubstate' in c:
1763 1765 c.remove('.hgsubstate')
1764 1766
1765 1767 # compare current state to last committed state
1766 1768 # build new substate based on last committed state
1767 1769 oldstate = wctx.p1().substate
1768 1770 for s in sorted(newstate.keys()):
1769 1771 if not match(s):
1770 1772 # ignore working copy, use old state if present
1771 1773 if s in oldstate:
1772 1774 newstate[s] = oldstate[s]
1773 1775 continue
1774 1776 if not force:
1775 1777 raise error.Abort(
1776 1778 _("commit with new subrepo %s excluded") % s)
1777 1779 dirtyreason = wctx.sub(s).dirtyreason(True)
1778 1780 if dirtyreason:
1779 1781 if not self.ui.configbool('ui', 'commitsubrepos'):
1780 1782 raise error.Abort(dirtyreason,
1781 1783 hint=_("use --subrepos for recursive commit"))
1782 1784 subs.append(s)
1783 1785 commitsubs.add(s)
1784 1786 else:
1785 1787 bs = wctx.sub(s).basestate()
1786 1788 newstate[s] = (newstate[s][0], bs, newstate[s][2])
1787 1789 if oldstate.get(s, (None, None, None))[1] != bs:
1788 1790 subs.append(s)
1789 1791
1790 1792 # check for removed subrepos
1791 1793 for p in wctx.parents():
1792 1794 r = [s for s in p.substate if s not in newstate]
1793 1795 subs += [s for s in r if match(s)]
1794 1796 if subs:
1795 1797 if (not match('.hgsub') and
1796 1798 '.hgsub' in (wctx.modified() + wctx.added())):
1797 1799 raise error.Abort(
1798 1800 _("can't commit subrepos without .hgsub"))
1799 1801 status.modified.insert(0, '.hgsubstate')
1800 1802
1801 1803 elif '.hgsub' in status.removed:
1802 1804 # clean up .hgsubstate when .hgsub is removed
1803 1805 if ('.hgsubstate' in wctx and
1804 1806 '.hgsubstate' not in (status.modified + status.added +
1805 1807 status.removed)):
1806 1808 status.removed.insert(0, '.hgsubstate')
1807 1809
1808 1810 # make sure all explicit patterns are matched
1809 1811 if not force:
1810 1812 self.checkcommitpatterns(wctx, vdirs, match, status, fail)
1811 1813
1812 1814 cctx = context.workingcommitctx(self, status,
1813 1815 text, user, date, extra)
1814 1816
1815 1817 # internal config: ui.allowemptycommit
1816 1818 allowemptycommit = (wctx.branch() != wctx.p1().branch()
1817 1819 or extra.get('close') or merge or cctx.files()
1818 1820 or self.ui.configbool('ui', 'allowemptycommit'))
1819 1821 if not allowemptycommit:
1820 1822 return None
1821 1823
1822 1824 if merge and cctx.deleted():
1823 1825 raise error.Abort(_("cannot commit merge with missing files"))
1824 1826
1825 1827 ms = mergemod.mergestate.read(self)
1826 1828 mergeutil.checkunresolved(ms)
1827 1829
1828 1830 if editor:
1829 1831 cctx._text = editor(self, cctx, subs)
1830 1832 edited = (text != cctx._text)
1831 1833
1832 1834 # Save commit message in case this transaction gets rolled back
1833 1835 # (e.g. by a pretxncommit hook). Leave the content alone on
1834 1836 # the assumption that the user will use the same editor again.
1835 1837 msgfn = self.savecommitmessage(cctx._text)
1836 1838
1837 1839 # commit subs and write new state
1838 1840 if subs:
1839 1841 for s in sorted(commitsubs):
1840 1842 sub = wctx.sub(s)
1841 1843 self.ui.status(_('committing subrepository %s\n') %
1842 1844 subrepo.subrelpath(sub))
1843 1845 sr = sub.commit(cctx._text, user, date)
1844 1846 newstate[s] = (newstate[s][0], sr)
1845 1847 subrepo.writestate(self, newstate)
1846 1848
1847 1849 p1, p2 = self.dirstate.parents()
1848 1850 hookp1, hookp2 = hex(p1), (p2 != nullid and hex(p2) or '')
1849 1851 try:
1850 1852 self.hook("precommit", throw=True, parent1=hookp1,
1851 1853 parent2=hookp2)
1852 1854 tr = self.transaction('commit')
1853 1855 ret = self.commitctx(cctx, True)
1854 1856 except: # re-raises
1855 1857 if edited:
1856 1858 self.ui.write(
1857 1859 _('note: commit message saved in %s\n') % msgfn)
1858 1860 raise
1859 1861 # update bookmarks, dirstate and mergestate
1860 1862 bookmarks.update(self, [p1, p2], ret)
1861 1863 cctx.markcommitted(ret)
1862 1864 ms.reset()
1863 1865 tr.close()
1864 1866
1865 1867 finally:
1866 1868 lockmod.release(tr, lock, wlock)
1867 1869
1868 1870 def commithook(node=hex(ret), parent1=hookp1, parent2=hookp2):
1869 1871 # hack for command that use a temporary commit (eg: histedit)
1870 1872 # temporary commit got stripped before hook release
1871 1873 if self.changelog.hasnode(ret):
1872 1874 self.hook("commit", node=node, parent1=parent1,
1873 1875 parent2=parent2)
1874 1876 self._afterlock(commithook)
1875 1877 return ret
1876 1878
1877 1879 @unfilteredmethod
1878 1880 def commitctx(self, ctx, error=False):
1879 1881 """Add a new revision to current repository.
1880 1882 Revision information is passed via the context argument.
1881 1883 """
1882 1884
1883 1885 tr = None
1884 1886 p1, p2 = ctx.p1(), ctx.p2()
1885 1887 user = ctx.user()
1886 1888
1887 1889 lock = self.lock()
1888 1890 try:
1889 1891 tr = self.transaction("commit")
1890 1892 trp = weakref.proxy(tr)
1891 1893
1892 1894 if ctx.manifestnode():
1893 1895 # reuse an existing manifest revision
1894 1896 mn = ctx.manifestnode()
1895 1897 files = ctx.files()
1896 1898 elif ctx.files():
1897 1899 m1ctx = p1.manifestctx()
1898 1900 m2ctx = p2.manifestctx()
1899 1901 mctx = m1ctx.copy()
1900 1902
1901 1903 m = mctx.read()
1902 1904 m1 = m1ctx.read()
1903 1905 m2 = m2ctx.read()
1904 1906
1905 1907 # check in files
1906 1908 added = []
1907 1909 changed = []
1908 1910 removed = list(ctx.removed())
1909 1911 linkrev = len(self)
1910 1912 self.ui.note(_("committing files:\n"))
1911 1913 for f in sorted(ctx.modified() + ctx.added()):
1912 1914 self.ui.note(f + "\n")
1913 1915 try:
1914 1916 fctx = ctx[f]
1915 1917 if fctx is None:
1916 1918 removed.append(f)
1917 1919 else:
1918 1920 added.append(f)
1919 1921 m[f] = self._filecommit(fctx, m1, m2, linkrev,
1920 1922 trp, changed)
1921 1923 m.setflag(f, fctx.flags())
1922 1924 except OSError as inst:
1923 1925 self.ui.warn(_("trouble committing %s!\n") % f)
1924 1926 raise
1925 1927 except IOError as inst:
1926 1928 errcode = getattr(inst, 'errno', errno.ENOENT)
1927 1929 if error or errcode and errcode != errno.ENOENT:
1928 1930 self.ui.warn(_("trouble committing %s!\n") % f)
1929 1931 raise
1930 1932
1931 1933 # update manifest
1932 1934 self.ui.note(_("committing manifest\n"))
1933 1935 removed = [f for f in sorted(removed) if f in m1 or f in m2]
1934 1936 drop = [f for f in removed if f in m]
1935 1937 for f in drop:
1936 1938 del m[f]
1937 1939 mn = mctx.write(trp, linkrev,
1938 1940 p1.manifestnode(), p2.manifestnode(),
1939 1941 added, drop)
1940 1942 files = changed + removed
1941 1943 else:
1942 1944 mn = p1.manifestnode()
1943 1945 files = []
1944 1946
1945 1947 # update changelog
1946 1948 self.ui.note(_("committing changelog\n"))
1947 1949 self.changelog.delayupdate(tr)
1948 1950 n = self.changelog.add(mn, files, ctx.description(),
1949 1951 trp, p1.node(), p2.node(),
1950 1952 user, ctx.date(), ctx.extra().copy())
1951 1953 xp1, xp2 = p1.hex(), p2 and p2.hex() or ''
1952 1954 self.hook('pretxncommit', throw=True, node=hex(n), parent1=xp1,
1953 1955 parent2=xp2)
1954 1956 # set the new commit is proper phase
1955 1957 targetphase = subrepo.newcommitphase(self.ui, ctx)
1956 1958 if targetphase:
1957 1959 # retract boundary do not alter parent changeset.
1958 1960 # if a parent have higher the resulting phase will
1959 1961 # be compliant anyway
1960 1962 #
1961 1963 # if minimal phase was 0 we don't need to retract anything
1962 1964 phases.registernew(self, tr, targetphase, [n])
1963 1965 tr.close()
1964 1966 return n
1965 1967 finally:
1966 1968 if tr:
1967 1969 tr.release()
1968 1970 lock.release()
1969 1971
1970 1972 @unfilteredmethod
1971 1973 def destroying(self):
1972 1974 '''Inform the repository that nodes are about to be destroyed.
1973 1975 Intended for use by strip and rollback, so there's a common
1974 1976 place for anything that has to be done before destroying history.
1975 1977
1976 1978 This is mostly useful for saving state that is in memory and waiting
1977 1979 to be flushed when the current lock is released. Because a call to
1978 1980 destroyed is imminent, the repo will be invalidated causing those
1979 1981 changes to stay in memory (waiting for the next unlock), or vanish
1980 1982 completely.
1981 1983 '''
1982 1984 # When using the same lock to commit and strip, the phasecache is left
1983 1985 # dirty after committing. Then when we strip, the repo is invalidated,
1984 1986 # causing those changes to disappear.
1985 1987 if '_phasecache' in vars(self):
1986 1988 self._phasecache.write()
1987 1989
1988 1990 @unfilteredmethod
1989 1991 def destroyed(self):
1990 1992 '''Inform the repository that nodes have been destroyed.
1991 1993 Intended for use by strip and rollback, so there's a common
1992 1994 place for anything that has to be done after destroying history.
1993 1995 '''
1994 1996 # When one tries to:
1995 1997 # 1) destroy nodes thus calling this method (e.g. strip)
1996 1998 # 2) use phasecache somewhere (e.g. commit)
1997 1999 #
1998 2000 # then 2) will fail because the phasecache contains nodes that were
1999 2001 # removed. We can either remove phasecache from the filecache,
2000 2002 # causing it to reload next time it is accessed, or simply filter
2001 2003 # the removed nodes now and write the updated cache.
2002 2004 self._phasecache.filterunknown(self)
2003 2005 self._phasecache.write()
2004 2006
2005 2007 # refresh all repository caches
2006 2008 self.updatecaches()
2007 2009
2008 2010 # Ensure the persistent tag cache is updated. Doing it now
2009 2011 # means that the tag cache only has to worry about destroyed
2010 2012 # heads immediately after a strip/rollback. That in turn
2011 2013 # guarantees that "cachetip == currenttip" (comparing both rev
2012 2014 # and node) always means no nodes have been added or destroyed.
2013 2015
2014 2016 # XXX this is suboptimal when qrefresh'ing: we strip the current
2015 2017 # head, refresh the tag cache, then immediately add a new head.
2016 2018 # But I think doing it this way is necessary for the "instant
2017 2019 # tag cache retrieval" case to work.
2018 2020 self.invalidate()
2019 2021
2020 2022 def walk(self, match, node=None):
2021 2023 '''
2022 2024 walk recursively through the directory tree or a given
2023 2025 changeset, finding all files matched by the match
2024 2026 function
2025 2027 '''
2026 2028 self.ui.deprecwarn('use repo[node].walk instead of repo.walk', '4.3')
2027 2029 return self[node].walk(match)
2028 2030
2029 2031 def status(self, node1='.', node2=None, match=None,
2030 2032 ignored=False, clean=False, unknown=False,
2031 2033 listsubrepos=False):
2032 2034 '''a convenience method that calls node1.status(node2)'''
2033 2035 return self[node1].status(node2, match, ignored, clean, unknown,
2034 2036 listsubrepos)
2035 2037
2036 2038 def addpostdsstatus(self, ps):
2037 2039 """Add a callback to run within the wlock, at the point at which status
2038 2040 fixups happen.
2039 2041
2040 2042 On status completion, callback(wctx, status) will be called with the
2041 2043 wlock held, unless the dirstate has changed from underneath or the wlock
2042 2044 couldn't be grabbed.
2043 2045
2044 2046 Callbacks should not capture and use a cached copy of the dirstate --
2045 2047 it might change in the meanwhile. Instead, they should access the
2046 2048 dirstate via wctx.repo().dirstate.
2047 2049
2048 2050 This list is emptied out after each status run -- extensions should
2049 2051 make sure it adds to this list each time dirstate.status is called.
2050 2052 Extensions should also make sure they don't call this for statuses
2051 2053 that don't involve the dirstate.
2052 2054 """
2053 2055
2054 2056 # The list is located here for uniqueness reasons -- it is actually
2055 2057 # managed by the workingctx, but that isn't unique per-repo.
2056 2058 self._postdsstatus.append(ps)
2057 2059
2058 2060 def postdsstatus(self):
2059 2061 """Used by workingctx to get the list of post-dirstate-status hooks."""
2060 2062 return self._postdsstatus
2061 2063
2062 2064 def clearpostdsstatus(self):
2063 2065 """Used by workingctx to clear post-dirstate-status hooks."""
2064 2066 del self._postdsstatus[:]
2065 2067
2066 2068 def heads(self, start=None):
2067 2069 if start is None:
2068 2070 cl = self.changelog
2069 2071 headrevs = reversed(cl.headrevs())
2070 2072 return [cl.node(rev) for rev in headrevs]
2071 2073
2072 2074 heads = self.changelog.heads(start)
2073 2075 # sort the output in rev descending order
2074 2076 return sorted(heads, key=self.changelog.rev, reverse=True)
2075 2077
2076 2078 def branchheads(self, branch=None, start=None, closed=False):
2077 2079 '''return a (possibly filtered) list of heads for the given branch
2078 2080
2079 2081 Heads are returned in topological order, from newest to oldest.
2080 2082 If branch is None, use the dirstate branch.
2081 2083 If start is not None, return only heads reachable from start.
2082 2084 If closed is True, return heads that are marked as closed as well.
2083 2085 '''
2084 2086 if branch is None:
2085 2087 branch = self[None].branch()
2086 2088 branches = self.branchmap()
2087 2089 if branch not in branches:
2088 2090 return []
2089 2091 # the cache returns heads ordered lowest to highest
2090 2092 bheads = list(reversed(branches.branchheads(branch, closed=closed)))
2091 2093 if start is not None:
2092 2094 # filter out the heads that cannot be reached from startrev
2093 2095 fbheads = set(self.changelog.nodesbetween([start], bheads)[2])
2094 2096 bheads = [h for h in bheads if h in fbheads]
2095 2097 return bheads
2096 2098
2097 2099 def branches(self, nodes):
2098 2100 if not nodes:
2099 2101 nodes = [self.changelog.tip()]
2100 2102 b = []
2101 2103 for n in nodes:
2102 2104 t = n
2103 2105 while True:
2104 2106 p = self.changelog.parents(n)
2105 2107 if p[1] != nullid or p[0] == nullid:
2106 2108 b.append((t, n, p[0], p[1]))
2107 2109 break
2108 2110 n = p[0]
2109 2111 return b
2110 2112
2111 2113 def between(self, pairs):
2112 2114 r = []
2113 2115
2114 2116 for top, bottom in pairs:
2115 2117 n, l, i = top, [], 0
2116 2118 f = 1
2117 2119
2118 2120 while n != bottom and n != nullid:
2119 2121 p = self.changelog.parents(n)[0]
2120 2122 if i == f:
2121 2123 l.append(n)
2122 2124 f = f * 2
2123 2125 n = p
2124 2126 i += 1
2125 2127
2126 2128 r.append(l)
2127 2129
2128 2130 return r
2129 2131
2130 2132 def checkpush(self, pushop):
2131 2133 """Extensions can override this function if additional checks have
2132 2134 to be performed before pushing, or call it if they override push
2133 2135 command.
2134 2136 """
2135 2137 pass
2136 2138
2137 2139 @unfilteredpropertycache
2138 2140 def prepushoutgoinghooks(self):
2139 2141 """Return util.hooks consists of a pushop with repo, remote, outgoing
2140 2142 methods, which are called before pushing changesets.
2141 2143 """
2142 2144 return util.hooks()
2143 2145
2144 2146 def pushkey(self, namespace, key, old, new):
2145 2147 try:
2146 2148 tr = self.currenttransaction()
2147 2149 hookargs = {}
2148 2150 if tr is not None:
2149 2151 hookargs.update(tr.hookargs)
2150 2152 hookargs['namespace'] = namespace
2151 2153 hookargs['key'] = key
2152 2154 hookargs['old'] = old
2153 2155 hookargs['new'] = new
2154 2156 self.hook('prepushkey', throw=True, **hookargs)
2155 2157 except error.HookAbort as exc:
2156 2158 self.ui.write_err(_("pushkey-abort: %s\n") % exc)
2157 2159 if exc.hint:
2158 2160 self.ui.write_err(_("(%s)\n") % exc.hint)
2159 2161 return False
2160 2162 self.ui.debug('pushing key for "%s:%s"\n' % (namespace, key))
2161 2163 ret = pushkey.push(self, namespace, key, old, new)
2162 2164 def runhook():
2163 2165 self.hook('pushkey', namespace=namespace, key=key, old=old, new=new,
2164 2166 ret=ret)
2165 2167 self._afterlock(runhook)
2166 2168 return ret
2167 2169
2168 2170 def listkeys(self, namespace):
2169 2171 self.hook('prelistkeys', throw=True, namespace=namespace)
2170 2172 self.ui.debug('listing keys for "%s"\n' % namespace)
2171 2173 values = pushkey.list(self, namespace)
2172 2174 self.hook('listkeys', namespace=namespace, values=values)
2173 2175 return values
2174 2176
2175 2177 def debugwireargs(self, one, two, three=None, four=None, five=None):
2176 2178 '''used to test argument passing over the wire'''
2177 2179 return "%s %s %s %s %s" % (one, two, three, four, five)
2178 2180
2179 2181 def savecommitmessage(self, text):
2180 2182 fp = self.vfs('last-message.txt', 'wb')
2181 2183 try:
2182 2184 fp.write(text)
2183 2185 finally:
2184 2186 fp.close()
2185 2187 return self.pathto(fp.name[len(self.root) + 1:])
2186 2188
2187 2189 # used to avoid circular references so destructors work
2188 2190 def aftertrans(files):
2189 2191 renamefiles = [tuple(t) for t in files]
2190 2192 def a():
2191 2193 for vfs, src, dest in renamefiles:
2192 2194 # if src and dest refer to a same file, vfs.rename is a no-op,
2193 2195 # leaving both src and dest on disk. delete dest to make sure
2194 2196 # the rename couldn't be such a no-op.
2195 2197 vfs.tryunlink(dest)
2196 2198 try:
2197 2199 vfs.rename(src, dest)
2198 2200 except OSError: # journal file does not yet exist
2199 2201 pass
2200 2202 return a
2201 2203
2202 2204 def undoname(fn):
2203 2205 base, name = os.path.split(fn)
2204 2206 assert name.startswith('journal')
2205 2207 return os.path.join(base, name.replace('journal', 'undo', 1))
2206 2208
2207 2209 def instance(ui, path, create):
2208 2210 return localrepository(ui, util.urllocalpath(path), create)
2209 2211
2210 2212 def islocal(path):
2211 2213 return True
2212 2214
2213 2215 def newreporequirements(repo):
2214 2216 """Determine the set of requirements for a new local repository.
2215 2217
2216 2218 Extensions can wrap this function to specify custom requirements for
2217 2219 new repositories.
2218 2220 """
2219 2221 ui = repo.ui
2220 2222 requirements = {'revlogv1'}
2221 2223 if ui.configbool('format', 'usestore'):
2222 2224 requirements.add('store')
2223 2225 if ui.configbool('format', 'usefncache'):
2224 2226 requirements.add('fncache')
2225 2227 if ui.configbool('format', 'dotencode'):
2226 2228 requirements.add('dotencode')
2227 2229
2228 2230 compengine = ui.config('experimental', 'format.compression')
2229 2231 if compengine not in util.compengines:
2230 2232 raise error.Abort(_('compression engine %s defined by '
2231 2233 'experimental.format.compression not available') %
2232 2234 compengine,
2233 2235 hint=_('run "hg debuginstall" to list available '
2234 2236 'compression engines'))
2235 2237
2236 2238 # zlib is the historical default and doesn't need an explicit requirement.
2237 2239 if compengine != 'zlib':
2238 2240 requirements.add('exp-compression-%s' % compengine)
2239 2241
2240 2242 if scmutil.gdinitconfig(ui):
2241 2243 requirements.add('generaldelta')
2242 2244 if ui.configbool('experimental', 'treemanifest'):
2243 2245 requirements.add('treemanifest')
2244 2246 if ui.configbool('experimental', 'manifestv2'):
2245 2247 requirements.add('manifestv2')
2246 2248
2247 2249 revlogv2 = ui.config('experimental', 'revlogv2')
2248 2250 if revlogv2 == 'enable-unstable-format-and-corrupt-my-data':
2249 2251 requirements.remove('revlogv1')
2250 2252 # generaldelta is implied by revlogv2.
2251 2253 requirements.discard('generaldelta')
2252 2254 requirements.add(REVLOGV2_REQUIREMENT)
2253 2255
2254 2256 return requirements
@@ -1,1093 +1,1104 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 hashlib
13 13 import os
14 14 import re
15 15 import socket
16 16 import weakref
17 17
18 18 from .i18n import _
19 19 from .node import (
20 20 hex,
21 21 nullid,
22 22 wdirid,
23 23 wdirrev,
24 24 )
25 25
26 26 from . import (
27 27 encoding,
28 28 error,
29 29 match as matchmod,
30 30 obsolete,
31 31 obsutil,
32 32 pathutil,
33 33 phases,
34 34 pycompat,
35 35 revsetlang,
36 36 similar,
37 37 util,
38 38 )
39 39
40 40 if pycompat.osname == 'nt':
41 41 from . import scmwindows as scmplatform
42 42 else:
43 43 from . import scmposix as scmplatform
44 44
45 45 termsize = scmplatform.termsize
46 46
47 47 class status(tuple):
48 48 '''Named tuple with a list of files per status. The 'deleted', 'unknown'
49 49 and 'ignored' properties are only relevant to the working copy.
50 50 '''
51 51
52 52 __slots__ = ()
53 53
54 54 def __new__(cls, modified, added, removed, deleted, unknown, ignored,
55 55 clean):
56 56 return tuple.__new__(cls, (modified, added, removed, deleted, unknown,
57 57 ignored, clean))
58 58
59 59 @property
60 60 def modified(self):
61 61 '''files that have been modified'''
62 62 return self[0]
63 63
64 64 @property
65 65 def added(self):
66 66 '''files that have been added'''
67 67 return self[1]
68 68
69 69 @property
70 70 def removed(self):
71 71 '''files that have been removed'''
72 72 return self[2]
73 73
74 74 @property
75 75 def deleted(self):
76 76 '''files that are in the dirstate, but have been deleted from the
77 77 working copy (aka "missing")
78 78 '''
79 79 return self[3]
80 80
81 81 @property
82 82 def unknown(self):
83 83 '''files not in the dirstate that are not ignored'''
84 84 return self[4]
85 85
86 86 @property
87 87 def ignored(self):
88 88 '''files not in the dirstate that are ignored (by _dirignore())'''
89 89 return self[5]
90 90
91 91 @property
92 92 def clean(self):
93 93 '''files that have not been modified'''
94 94 return self[6]
95 95
96 96 def __repr__(self, *args, **kwargs):
97 97 return (('<status modified=%r, added=%r, removed=%r, deleted=%r, '
98 98 'unknown=%r, ignored=%r, clean=%r>') % self)
99 99
100 100 def itersubrepos(ctx1, ctx2):
101 101 """find subrepos in ctx1 or ctx2"""
102 102 # Create a (subpath, ctx) mapping where we prefer subpaths from
103 103 # ctx1. The subpaths from ctx2 are important when the .hgsub file
104 104 # has been modified (in ctx2) but not yet committed (in ctx1).
105 105 subpaths = dict.fromkeys(ctx2.substate, ctx2)
106 106 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
107 107
108 108 missing = set()
109 109
110 110 for subpath in ctx2.substate:
111 111 if subpath not in ctx1.substate:
112 112 del subpaths[subpath]
113 113 missing.add(subpath)
114 114
115 115 for subpath, ctx in sorted(subpaths.iteritems()):
116 116 yield subpath, ctx.sub(subpath)
117 117
118 118 # Yield an empty subrepo based on ctx1 for anything only in ctx2. That way,
119 119 # status and diff will have an accurate result when it does
120 120 # 'sub.{status|diff}(rev2)'. Otherwise, the ctx2 subrepo is compared
121 121 # against itself.
122 122 for subpath in missing:
123 123 yield subpath, ctx2.nullsub(subpath, ctx1)
124 124
125 125 def nochangesfound(ui, repo, excluded=None):
126 126 '''Report no changes for push/pull, excluded is None or a list of
127 127 nodes excluded from the push/pull.
128 128 '''
129 129 secretlist = []
130 130 if excluded:
131 131 for n in excluded:
132 132 ctx = repo[n]
133 133 if ctx.phase() >= phases.secret and not ctx.extinct():
134 134 secretlist.append(n)
135 135
136 136 if secretlist:
137 137 ui.status(_("no changes found (ignored %d secret changesets)\n")
138 138 % len(secretlist))
139 139 else:
140 140 ui.status(_("no changes found\n"))
141 141
142 142 def callcatch(ui, func):
143 143 """call func() with global exception handling
144 144
145 145 return func() if no exception happens. otherwise do some error handling
146 146 and return an exit code accordingly. does not handle all exceptions.
147 147 """
148 148 try:
149 149 try:
150 150 return func()
151 151 except: # re-raises
152 152 ui.traceback()
153 153 raise
154 154 # Global exception handling, alphabetically
155 155 # Mercurial-specific first, followed by built-in and library exceptions
156 156 except error.LockHeld as inst:
157 157 if inst.errno == errno.ETIMEDOUT:
158 158 reason = _('timed out waiting for lock held by %r') % inst.locker
159 159 else:
160 160 reason = _('lock held by %r') % inst.locker
161 161 ui.warn(_("abort: %s: %s\n") % (inst.desc or inst.filename, reason))
162 162 if not inst.locker:
163 163 ui.warn(_("(lock might be very busy)\n"))
164 164 except error.LockUnavailable as inst:
165 165 ui.warn(_("abort: could not lock %s: %s\n") %
166 166 (inst.desc or inst.filename, inst.strerror))
167 167 except error.OutOfBandError as inst:
168 168 if inst.args:
169 169 msg = _("abort: remote error:\n")
170 170 else:
171 171 msg = _("abort: remote error\n")
172 172 ui.warn(msg)
173 173 if inst.args:
174 174 ui.warn(''.join(inst.args))
175 175 if inst.hint:
176 176 ui.warn('(%s)\n' % inst.hint)
177 177 except error.RepoError as inst:
178 178 ui.warn(_("abort: %s!\n") % inst)
179 179 if inst.hint:
180 180 ui.warn(_("(%s)\n") % inst.hint)
181 181 except error.ResponseError as inst:
182 182 ui.warn(_("abort: %s") % inst.args[0])
183 183 if not isinstance(inst.args[1], basestring):
184 184 ui.warn(" %r\n" % (inst.args[1],))
185 185 elif not inst.args[1]:
186 186 ui.warn(_(" empty string\n"))
187 187 else:
188 188 ui.warn("\n%r\n" % util.ellipsis(inst.args[1]))
189 189 except error.CensoredNodeError as inst:
190 190 ui.warn(_("abort: file censored %s!\n") % inst)
191 191 except error.RevlogError as inst:
192 192 ui.warn(_("abort: %s!\n") % inst)
193 193 except error.InterventionRequired as inst:
194 194 ui.warn("%s\n" % inst)
195 195 if inst.hint:
196 196 ui.warn(_("(%s)\n") % inst.hint)
197 197 return 1
198 198 except error.WdirUnsupported:
199 199 ui.warn(_("abort: working directory revision cannot be specified\n"))
200 200 except error.Abort as inst:
201 201 ui.warn(_("abort: %s\n") % inst)
202 202 if inst.hint:
203 203 ui.warn(_("(%s)\n") % inst.hint)
204 204 except ImportError as inst:
205 205 ui.warn(_("abort: %s!\n") % inst)
206 206 m = str(inst).split()[-1]
207 207 if m in "mpatch bdiff".split():
208 208 ui.warn(_("(did you forget to compile extensions?)\n"))
209 209 elif m in "zlib".split():
210 210 ui.warn(_("(is your Python install correct?)\n"))
211 211 except IOError as inst:
212 212 if util.safehasattr(inst, "code"):
213 213 ui.warn(_("abort: %s\n") % inst)
214 214 elif util.safehasattr(inst, "reason"):
215 215 try: # usually it is in the form (errno, strerror)
216 216 reason = inst.reason.args[1]
217 217 except (AttributeError, IndexError):
218 218 # it might be anything, for example a string
219 219 reason = inst.reason
220 220 if isinstance(reason, unicode):
221 221 # SSLError of Python 2.7.9 contains a unicode
222 222 reason = encoding.unitolocal(reason)
223 223 ui.warn(_("abort: error: %s\n") % reason)
224 224 elif (util.safehasattr(inst, "args")
225 225 and inst.args and inst.args[0] == errno.EPIPE):
226 226 pass
227 227 elif getattr(inst, "strerror", None):
228 228 if getattr(inst, "filename", None):
229 229 ui.warn(_("abort: %s: %s\n") % (inst.strerror, inst.filename))
230 230 else:
231 231 ui.warn(_("abort: %s\n") % inst.strerror)
232 232 else:
233 233 raise
234 234 except OSError as inst:
235 235 if getattr(inst, "filename", None) is not None:
236 236 ui.warn(_("abort: %s: '%s'\n") % (inst.strerror, inst.filename))
237 237 else:
238 238 ui.warn(_("abort: %s\n") % inst.strerror)
239 239 except MemoryError:
240 240 ui.warn(_("abort: out of memory\n"))
241 241 except SystemExit as inst:
242 242 # Commands shouldn't sys.exit directly, but give a return code.
243 243 # Just in case catch this and and pass exit code to caller.
244 244 return inst.code
245 245 except socket.error as inst:
246 246 ui.warn(_("abort: %s\n") % inst.args[-1])
247 247
248 248 return -1
249 249
250 250 def checknewlabel(repo, lbl, kind):
251 251 # Do not use the "kind" parameter in ui output.
252 252 # It makes strings difficult to translate.
253 253 if lbl in ['tip', '.', 'null']:
254 254 raise error.Abort(_("the name '%s' is reserved") % lbl)
255 255 for c in (':', '\0', '\n', '\r'):
256 256 if c in lbl:
257 257 raise error.Abort(_("%r cannot be used in a name") % c)
258 258 try:
259 259 int(lbl)
260 260 raise error.Abort(_("cannot use an integer as a name"))
261 261 except ValueError:
262 262 pass
263 263
264 264 def checkfilename(f):
265 265 '''Check that the filename f is an acceptable filename for a tracked file'''
266 266 if '\r' in f or '\n' in f:
267 267 raise error.Abort(_("'\\n' and '\\r' disallowed in filenames: %r") % f)
268 268
269 269 def checkportable(ui, f):
270 270 '''Check if filename f is portable and warn or abort depending on config'''
271 271 checkfilename(f)
272 272 abort, warn = checkportabilityalert(ui)
273 273 if abort or warn:
274 274 msg = util.checkwinfilename(f)
275 275 if msg:
276 276 msg = "%s: %r" % (msg, f)
277 277 if abort:
278 278 raise error.Abort(msg)
279 279 ui.warn(_("warning: %s\n") % msg)
280 280
281 281 def checkportabilityalert(ui):
282 282 '''check if the user's config requests nothing, a warning, or abort for
283 283 non-portable filenames'''
284 284 val = ui.config('ui', 'portablefilenames')
285 285 lval = val.lower()
286 286 bval = util.parsebool(val)
287 287 abort = pycompat.osname == 'nt' or lval == 'abort'
288 288 warn = bval or lval == 'warn'
289 289 if bval is None and not (warn or abort or lval == 'ignore'):
290 290 raise error.ConfigError(
291 291 _("ui.portablefilenames value is invalid ('%s')") % val)
292 292 return abort, warn
293 293
294 294 class casecollisionauditor(object):
295 295 def __init__(self, ui, abort, dirstate):
296 296 self._ui = ui
297 297 self._abort = abort
298 298 allfiles = '\0'.join(dirstate._map)
299 299 self._loweredfiles = set(encoding.lower(allfiles).split('\0'))
300 300 self._dirstate = dirstate
301 301 # The purpose of _newfiles is so that we don't complain about
302 302 # case collisions if someone were to call this object with the
303 303 # same filename twice.
304 304 self._newfiles = set()
305 305
306 306 def __call__(self, f):
307 307 if f in self._newfiles:
308 308 return
309 309 fl = encoding.lower(f)
310 310 if fl in self._loweredfiles and f not in self._dirstate:
311 311 msg = _('possible case-folding collision for %s') % f
312 312 if self._abort:
313 313 raise error.Abort(msg)
314 314 self._ui.warn(_("warning: %s\n") % msg)
315 315 self._loweredfiles.add(fl)
316 316 self._newfiles.add(f)
317 317
318 318 def filteredhash(repo, maxrev):
319 319 """build hash of filtered revisions in the current repoview.
320 320
321 321 Multiple caches perform up-to-date validation by checking that the
322 322 tiprev and tipnode stored in the cache file match the current repository.
323 323 However, this is not sufficient for validating repoviews because the set
324 324 of revisions in the view may change without the repository tiprev and
325 325 tipnode changing.
326 326
327 327 This function hashes all the revs filtered from the view and returns
328 328 that SHA-1 digest.
329 329 """
330 330 cl = repo.changelog
331 331 if not cl.filteredrevs:
332 332 return None
333 333 key = None
334 334 revs = sorted(r for r in cl.filteredrevs if r <= maxrev)
335 335 if revs:
336 336 s = hashlib.sha1()
337 337 for rev in revs:
338 338 s.update('%d;' % rev)
339 339 key = s.digest()
340 340 return key
341 341
342 342 def walkrepos(path, followsym=False, seen_dirs=None, recurse=False):
343 343 '''yield every hg repository under path, always recursively.
344 344 The recurse flag will only control recursion into repo working dirs'''
345 345 def errhandler(err):
346 346 if err.filename == path:
347 347 raise err
348 348 samestat = getattr(os.path, 'samestat', None)
349 349 if followsym and samestat is not None:
350 350 def adddir(dirlst, dirname):
351 351 match = False
352 352 dirstat = os.stat(dirname)
353 353 for lstdirstat in dirlst:
354 354 if samestat(dirstat, lstdirstat):
355 355 match = True
356 356 break
357 357 if not match:
358 358 dirlst.append(dirstat)
359 359 return not match
360 360 else:
361 361 followsym = False
362 362
363 363 if (seen_dirs is None) and followsym:
364 364 seen_dirs = []
365 365 adddir(seen_dirs, path)
366 366 for root, dirs, files in os.walk(path, topdown=True, onerror=errhandler):
367 367 dirs.sort()
368 368 if '.hg' in dirs:
369 369 yield root # found a repository
370 370 qroot = os.path.join(root, '.hg', 'patches')
371 371 if os.path.isdir(os.path.join(qroot, '.hg')):
372 372 yield qroot # we have a patch queue repo here
373 373 if recurse:
374 374 # avoid recursing inside the .hg directory
375 375 dirs.remove('.hg')
376 376 else:
377 377 dirs[:] = [] # don't descend further
378 378 elif followsym:
379 379 newdirs = []
380 380 for d in dirs:
381 381 fname = os.path.join(root, d)
382 382 if adddir(seen_dirs, fname):
383 383 if os.path.islink(fname):
384 384 for hgname in walkrepos(fname, True, seen_dirs):
385 385 yield hgname
386 386 else:
387 387 newdirs.append(d)
388 388 dirs[:] = newdirs
389 389
390 390 def binnode(ctx):
391 391 """Return binary node id for a given basectx"""
392 392 node = ctx.node()
393 393 if node is None:
394 394 return wdirid
395 395 return node
396 396
397 397 def intrev(ctx):
398 398 """Return integer for a given basectx that can be used in comparison or
399 399 arithmetic operation"""
400 400 rev = ctx.rev()
401 401 if rev is None:
402 402 return wdirrev
403 403 return rev
404 404
405 405 def revsingle(repo, revspec, default='.'):
406 406 if not revspec and revspec != 0:
407 407 return repo[default]
408 408
409 409 l = revrange(repo, [revspec])
410 410 if not l:
411 411 raise error.Abort(_('empty revision set'))
412 412 return repo[l.last()]
413 413
414 414 def _pairspec(revspec):
415 415 tree = revsetlang.parse(revspec)
416 416 return tree and tree[0] in ('range', 'rangepre', 'rangepost', 'rangeall')
417 417
418 418 def revpair(repo, revs):
419 419 if not revs:
420 420 return repo.dirstate.p1(), None
421 421
422 422 l = revrange(repo, revs)
423 423
424 424 if not l:
425 425 first = second = None
426 426 elif l.isascending():
427 427 first = l.min()
428 428 second = l.max()
429 429 elif l.isdescending():
430 430 first = l.max()
431 431 second = l.min()
432 432 else:
433 433 first = l.first()
434 434 second = l.last()
435 435
436 436 if first is None:
437 437 raise error.Abort(_('empty revision range'))
438 438 if (first == second and len(revs) >= 2
439 439 and not all(revrange(repo, [r]) for r in revs)):
440 440 raise error.Abort(_('empty revision on one side of range'))
441 441
442 442 # if top-level is range expression, the result must always be a pair
443 443 if first == second and len(revs) == 1 and not _pairspec(revs[0]):
444 444 return repo.lookup(first), None
445 445
446 446 return repo.lookup(first), repo.lookup(second)
447 447
448 448 def revrange(repo, specs):
449 449 """Execute 1 to many revsets and return the union.
450 450
451 451 This is the preferred mechanism for executing revsets using user-specified
452 452 config options, such as revset aliases.
453 453
454 454 The revsets specified by ``specs`` will be executed via a chained ``OR``
455 455 expression. If ``specs`` is empty, an empty result is returned.
456 456
457 457 ``specs`` can contain integers, in which case they are assumed to be
458 458 revision numbers.
459 459
460 460 It is assumed the revsets are already formatted. If you have arguments
461 461 that need to be expanded in the revset, call ``revsetlang.formatspec()``
462 462 and pass the result as an element of ``specs``.
463 463
464 464 Specifying a single revset is allowed.
465 465
466 466 Returns a ``revset.abstractsmartset`` which is a list-like interface over
467 467 integer revisions.
468 468 """
469 469 allspecs = []
470 470 for spec in specs:
471 471 if isinstance(spec, int):
472 472 spec = revsetlang.formatspec('rev(%d)', spec)
473 473 allspecs.append(spec)
474 474 return repo.anyrevs(allspecs, user=True)
475 475
476 476 def meaningfulparents(repo, ctx):
477 477 """Return list of meaningful (or all if debug) parentrevs for rev.
478 478
479 479 For merges (two non-nullrev revisions) both parents are meaningful.
480 480 Otherwise the first parent revision is considered meaningful if it
481 481 is not the preceding revision.
482 482 """
483 483 parents = ctx.parents()
484 484 if len(parents) > 1:
485 485 return parents
486 486 if repo.ui.debugflag:
487 487 return [parents[0], repo['null']]
488 488 if parents[0].rev() >= intrev(ctx) - 1:
489 489 return []
490 490 return parents
491 491
492 492 def expandpats(pats):
493 493 '''Expand bare globs when running on windows.
494 494 On posix we assume it already has already been done by sh.'''
495 495 if not util.expandglobs:
496 496 return list(pats)
497 497 ret = []
498 498 for kindpat in pats:
499 499 kind, pat = matchmod._patsplit(kindpat, None)
500 500 if kind is None:
501 501 try:
502 502 globbed = glob.glob(pat)
503 503 except re.error:
504 504 globbed = [pat]
505 505 if globbed:
506 506 ret.extend(globbed)
507 507 continue
508 508 ret.append(kindpat)
509 509 return ret
510 510
511 511 def matchandpats(ctx, pats=(), opts=None, globbed=False, default='relpath',
512 512 badfn=None):
513 513 '''Return a matcher and the patterns that were used.
514 514 The matcher will warn about bad matches, unless an alternate badfn callback
515 515 is provided.'''
516 516 if pats == ("",):
517 517 pats = []
518 518 if opts is None:
519 519 opts = {}
520 520 if not globbed and default == 'relpath':
521 521 pats = expandpats(pats or [])
522 522
523 523 def bad(f, msg):
524 524 ctx.repo().ui.warn("%s: %s\n" % (m.rel(f), msg))
525 525
526 526 if badfn is None:
527 527 badfn = bad
528 528
529 529 m = ctx.match(pats, opts.get('include'), opts.get('exclude'),
530 530 default, listsubrepos=opts.get('subrepos'), badfn=badfn)
531 531
532 532 if m.always():
533 533 pats = []
534 534 return m, pats
535 535
536 536 def match(ctx, pats=(), opts=None, globbed=False, default='relpath',
537 537 badfn=None):
538 538 '''Return a matcher that will warn about bad matches.'''
539 539 return matchandpats(ctx, pats, opts, globbed, default, badfn=badfn)[0]
540 540
541 541 def matchall(repo):
542 542 '''Return a matcher that will efficiently match everything.'''
543 543 return matchmod.always(repo.root, repo.getcwd())
544 544
545 545 def matchfiles(repo, files, badfn=None):
546 546 '''Return a matcher that will efficiently match exactly these files.'''
547 547 return matchmod.exact(repo.root, repo.getcwd(), files, badfn=badfn)
548 548
549 549 def origpath(ui, repo, filepath):
550 550 '''customize where .orig files are created
551 551
552 552 Fetch user defined path from config file: [ui] origbackuppath = <path>
553 553 Fall back to default (filepath) if not specified
554 554 '''
555 555 origbackuppath = ui.config('ui', 'origbackuppath')
556 556 if origbackuppath is None:
557 557 return filepath + ".orig"
558 558
559 559 filepathfromroot = os.path.relpath(filepath, start=repo.root)
560 560 fullorigpath = repo.wjoin(origbackuppath, filepathfromroot)
561 561
562 562 origbackupdir = repo.vfs.dirname(fullorigpath)
563 563 if not repo.vfs.exists(origbackupdir):
564 564 ui.note(_('creating directory: %s\n') % origbackupdir)
565 565 util.makedirs(origbackupdir)
566 566
567 567 return fullorigpath + ".orig"
568 568
569 569 class _containsnode(object):
570 570 """proxy __contains__(node) to container.__contains__ which accepts revs"""
571 571
572 572 def __init__(self, repo, revcontainer):
573 573 self._torev = repo.changelog.rev
574 574 self._revcontains = revcontainer.__contains__
575 575
576 576 def __contains__(self, node):
577 577 return self._revcontains(self._torev(node))
578 578
579 579 def cleanupnodes(repo, mapping, operation):
580 580 """do common cleanups when old nodes are replaced by new nodes
581 581
582 582 That includes writing obsmarkers or stripping nodes, and moving bookmarks.
583 583 (we might also want to move working directory parent in the future)
584 584
585 585 mapping is {oldnode: [newnode]} or a iterable of nodes if they do not have
586 586 replacements. operation is a string, like "rebase".
587 587 """
588 588 if not util.safehasattr(mapping, 'items'):
589 589 mapping = {n: () for n in mapping}
590 590
591 591 with repo.transaction('cleanup') as tr:
592 592 # Move bookmarks
593 593 bmarks = repo._bookmarks
594 594 bmarkchanges = []
595 595 allnewnodes = [n for ns in mapping.values() for n in ns]
596 596 for oldnode, newnodes in mapping.items():
597 597 oldbmarks = repo.nodebookmarks(oldnode)
598 598 if not oldbmarks:
599 599 continue
600 600 from . import bookmarks # avoid import cycle
601 601 if len(newnodes) > 1:
602 602 # usually a split, take the one with biggest rev number
603 603 newnode = next(repo.set('max(%ln)', newnodes)).node()
604 604 elif len(newnodes) == 0:
605 605 # move bookmark backwards
606 606 roots = list(repo.set('max((::%n) - %ln)', oldnode,
607 607 list(mapping)))
608 608 if roots:
609 609 newnode = roots[0].node()
610 610 else:
611 611 newnode = nullid
612 612 else:
613 613 newnode = newnodes[0]
614 614 repo.ui.debug('moving bookmarks %r from %s to %s\n' %
615 615 (oldbmarks, hex(oldnode), hex(newnode)))
616 616 # Delete divergent bookmarks being parents of related newnodes
617 617 deleterevs = repo.revs('parents(roots(%ln & (::%n))) - parents(%n)',
618 618 allnewnodes, newnode, oldnode)
619 619 deletenodes = _containsnode(repo, deleterevs)
620 620 for name in oldbmarks:
621 621 bmarkchanges.append((name, newnode))
622 622 for b in bookmarks.divergent2delete(repo, deletenodes, name):
623 623 bmarkchanges.append((b, None))
624 624
625 625 if bmarkchanges:
626 626 bmarks.applychanges(repo, tr, bmarkchanges)
627 627
628 628 # Obsolete or strip nodes
629 629 if obsolete.isenabled(repo, obsolete.createmarkersopt):
630 630 # If a node is already obsoleted, and we want to obsolete it
631 631 # without a successor, skip that obssolete request since it's
632 632 # unnecessary. That's the "if s or not isobs(n)" check below.
633 633 # Also sort the node in topology order, that might be useful for
634 634 # some obsstore logic.
635 635 # NOTE: the filtering and sorting might belong to createmarkers.
636 636 # Unfiltered repo is needed since nodes in mapping might be hidden.
637 637 unfi = repo.unfiltered()
638 638 isobs = unfi.obsstore.successors.__contains__
639 639 torev = unfi.changelog.rev
640 640 sortfunc = lambda ns: torev(ns[0])
641 641 rels = [(unfi[n], tuple(unfi[m] for m in s))
642 642 for n, s in sorted(mapping.items(), key=sortfunc)
643 643 if s or not isobs(n)]
644 644 obsolete.createmarkers(repo, rels, operation=operation)
645 645 else:
646 646 from . import repair # avoid import cycle
647 647 repair.delayedstrip(repo.ui, repo, list(mapping), operation)
648 648
649 649 def addremove(repo, matcher, prefix, opts=None, dry_run=None, similarity=None):
650 650 if opts is None:
651 651 opts = {}
652 652 m = matcher
653 653 if dry_run is None:
654 654 dry_run = opts.get('dry_run')
655 655 if similarity is None:
656 656 similarity = float(opts.get('similarity') or 0)
657 657
658 658 ret = 0
659 659 join = lambda f: os.path.join(prefix, f)
660 660
661 661 wctx = repo[None]
662 662 for subpath in sorted(wctx.substate):
663 663 submatch = matchmod.subdirmatcher(subpath, m)
664 664 if opts.get('subrepos') or m.exact(subpath) or any(submatch.files()):
665 665 sub = wctx.sub(subpath)
666 666 try:
667 667 if sub.addremove(submatch, prefix, opts, dry_run, similarity):
668 668 ret = 1
669 669 except error.LookupError:
670 670 repo.ui.status(_("skipping missing subrepository: %s\n")
671 671 % join(subpath))
672 672
673 673 rejected = []
674 674 def badfn(f, msg):
675 675 if f in m.files():
676 676 m.bad(f, msg)
677 677 rejected.append(f)
678 678
679 679 badmatch = matchmod.badmatch(m, badfn)
680 680 added, unknown, deleted, removed, forgotten = _interestingfiles(repo,
681 681 badmatch)
682 682
683 683 unknownset = set(unknown + forgotten)
684 684 toprint = unknownset.copy()
685 685 toprint.update(deleted)
686 686 for abs in sorted(toprint):
687 687 if repo.ui.verbose or not m.exact(abs):
688 688 if abs in unknownset:
689 689 status = _('adding %s\n') % m.uipath(abs)
690 690 else:
691 691 status = _('removing %s\n') % m.uipath(abs)
692 692 repo.ui.status(status)
693 693
694 694 renames = _findrenames(repo, m, added + unknown, removed + deleted,
695 695 similarity)
696 696
697 697 if not dry_run:
698 698 _markchanges(repo, unknown + forgotten, deleted, renames)
699 699
700 700 for f in rejected:
701 701 if f in m.files():
702 702 return 1
703 703 return ret
704 704
705 705 def marktouched(repo, files, similarity=0.0):
706 706 '''Assert that files have somehow been operated upon. files are relative to
707 707 the repo root.'''
708 708 m = matchfiles(repo, files, badfn=lambda x, y: rejected.append(x))
709 709 rejected = []
710 710
711 711 added, unknown, deleted, removed, forgotten = _interestingfiles(repo, m)
712 712
713 713 if repo.ui.verbose:
714 714 unknownset = set(unknown + forgotten)
715 715 toprint = unknownset.copy()
716 716 toprint.update(deleted)
717 717 for abs in sorted(toprint):
718 718 if abs in unknownset:
719 719 status = _('adding %s\n') % abs
720 720 else:
721 721 status = _('removing %s\n') % abs
722 722 repo.ui.status(status)
723 723
724 724 renames = _findrenames(repo, m, added + unknown, removed + deleted,
725 725 similarity)
726 726
727 727 _markchanges(repo, unknown + forgotten, deleted, renames)
728 728
729 729 for f in rejected:
730 730 if f in m.files():
731 731 return 1
732 732 return 0
733 733
734 734 def _interestingfiles(repo, matcher):
735 735 '''Walk dirstate with matcher, looking for files that addremove would care
736 736 about.
737 737
738 738 This is different from dirstate.status because it doesn't care about
739 739 whether files are modified or clean.'''
740 740 added, unknown, deleted, removed, forgotten = [], [], [], [], []
741 741 audit_path = pathutil.pathauditor(repo.root)
742 742
743 743 ctx = repo[None]
744 744 dirstate = repo.dirstate
745 745 walkresults = dirstate.walk(matcher, sorted(ctx.substate), True, False,
746 746 full=False)
747 747 for abs, st in walkresults.iteritems():
748 748 dstate = dirstate[abs]
749 749 if dstate == '?' and audit_path.check(abs):
750 750 unknown.append(abs)
751 751 elif dstate != 'r' and not st:
752 752 deleted.append(abs)
753 753 elif dstate == 'r' and st:
754 754 forgotten.append(abs)
755 755 # for finding renames
756 756 elif dstate == 'r' and not st:
757 757 removed.append(abs)
758 758 elif dstate == 'a':
759 759 added.append(abs)
760 760
761 761 return added, unknown, deleted, removed, forgotten
762 762
763 763 def _findrenames(repo, matcher, added, removed, similarity):
764 764 '''Find renames from removed files to added ones.'''
765 765 renames = {}
766 766 if similarity > 0:
767 767 for old, new, score in similar.findrenames(repo, added, removed,
768 768 similarity):
769 769 if (repo.ui.verbose or not matcher.exact(old)
770 770 or not matcher.exact(new)):
771 771 repo.ui.status(_('recording removal of %s as rename to %s '
772 772 '(%d%% similar)\n') %
773 773 (matcher.rel(old), matcher.rel(new),
774 774 score * 100))
775 775 renames[new] = old
776 776 return renames
777 777
778 778 def _markchanges(repo, unknown, deleted, renames):
779 779 '''Marks the files in unknown as added, the files in deleted as removed,
780 780 and the files in renames as copied.'''
781 781 wctx = repo[None]
782 782 with repo.wlock():
783 783 wctx.forget(deleted)
784 784 wctx.add(unknown)
785 785 for new, old in renames.iteritems():
786 786 wctx.copy(old, new)
787 787
788 788 def dirstatecopy(ui, repo, wctx, src, dst, dryrun=False, cwd=None):
789 789 """Update the dirstate to reflect the intent of copying src to dst. For
790 790 different reasons it might not end with dst being marked as copied from src.
791 791 """
792 792 origsrc = repo.dirstate.copied(src) or src
793 793 if dst == origsrc: # copying back a copy?
794 794 if repo.dirstate[dst] not in 'mn' and not dryrun:
795 795 repo.dirstate.normallookup(dst)
796 796 else:
797 797 if repo.dirstate[origsrc] == 'a' and origsrc == src:
798 798 if not ui.quiet:
799 799 ui.warn(_("%s has not been committed yet, so no copy "
800 800 "data will be stored for %s.\n")
801 801 % (repo.pathto(origsrc, cwd), repo.pathto(dst, cwd)))
802 802 if repo.dirstate[dst] in '?r' and not dryrun:
803 803 wctx.add([dst])
804 804 elif not dryrun:
805 805 wctx.copy(origsrc, dst)
806 806
807 807 def readrequires(opener, supported):
808 808 '''Reads and parses .hg/requires and checks if all entries found
809 809 are in the list of supported features.'''
810 810 requirements = set(opener.read("requires").splitlines())
811 811 missings = []
812 812 for r in requirements:
813 813 if r not in supported:
814 814 if not r or not r[0].isalnum():
815 815 raise error.RequirementError(_(".hg/requires file is corrupt"))
816 816 missings.append(r)
817 817 missings.sort()
818 818 if missings:
819 819 raise error.RequirementError(
820 820 _("repository requires features unknown to this Mercurial: %s")
821 821 % " ".join(missings),
822 822 hint=_("see https://mercurial-scm.org/wiki/MissingRequirement"
823 823 " for more information"))
824 824 return requirements
825 825
826 826 def writerequires(opener, requirements):
827 827 with opener('requires', 'w') as fp:
828 828 for r in sorted(requirements):
829 829 fp.write("%s\n" % r)
830 830
831 831 class filecachesubentry(object):
832 832 def __init__(self, path, stat):
833 833 self.path = path
834 834 self.cachestat = None
835 835 self._cacheable = None
836 836
837 837 if stat:
838 838 self.cachestat = filecachesubentry.stat(self.path)
839 839
840 840 if self.cachestat:
841 841 self._cacheable = self.cachestat.cacheable()
842 842 else:
843 843 # None means we don't know yet
844 844 self._cacheable = None
845 845
846 846 def refresh(self):
847 847 if self.cacheable():
848 848 self.cachestat = filecachesubentry.stat(self.path)
849 849
850 850 def cacheable(self):
851 851 if self._cacheable is not None:
852 852 return self._cacheable
853 853
854 854 # we don't know yet, assume it is for now
855 855 return True
856 856
857 857 def changed(self):
858 858 # no point in going further if we can't cache it
859 859 if not self.cacheable():
860 860 return True
861 861
862 862 newstat = filecachesubentry.stat(self.path)
863 863
864 864 # we may not know if it's cacheable yet, check again now
865 865 if newstat and self._cacheable is None:
866 866 self._cacheable = newstat.cacheable()
867 867
868 868 # check again
869 869 if not self._cacheable:
870 870 return True
871 871
872 872 if self.cachestat != newstat:
873 873 self.cachestat = newstat
874 874 return True
875 875 else:
876 876 return False
877 877
878 878 @staticmethod
879 879 def stat(path):
880 880 try:
881 881 return util.cachestat(path)
882 882 except OSError as e:
883 883 if e.errno != errno.ENOENT:
884 884 raise
885 885
886 886 class filecacheentry(object):
887 887 def __init__(self, paths, stat=True):
888 888 self._entries = []
889 889 for path in paths:
890 890 self._entries.append(filecachesubentry(path, stat))
891 891
892 892 def changed(self):
893 893 '''true if any entry has changed'''
894 894 for entry in self._entries:
895 895 if entry.changed():
896 896 return True
897 897 return False
898 898
899 899 def refresh(self):
900 900 for entry in self._entries:
901 901 entry.refresh()
902 902
903 903 class filecache(object):
904 904 '''A property like decorator that tracks files under .hg/ for updates.
905 905
906 906 Records stat info when called in _filecache.
907 907
908 908 On subsequent calls, compares old stat info with new info, and recreates the
909 909 object when any of the files changes, updating the new stat info in
910 910 _filecache.
911 911
912 912 Mercurial either atomic renames or appends for files under .hg,
913 913 so to ensure the cache is reliable we need the filesystem to be able
914 914 to tell us if a file has been replaced. If it can't, we fallback to
915 915 recreating the object on every call (essentially the same behavior as
916 916 propertycache).
917 917
918 918 '''
919 919 def __init__(self, *paths):
920 920 self.paths = paths
921 921
922 922 def join(self, obj, fname):
923 923 """Used to compute the runtime path of a cached file.
924 924
925 925 Users should subclass filecache and provide their own version of this
926 926 function to call the appropriate join function on 'obj' (an instance
927 927 of the class that its member function was decorated).
928 928 """
929 929 raise NotImplementedError
930 930
931 931 def __call__(self, func):
932 932 self.func = func
933 933 self.name = func.__name__.encode('ascii')
934 934 return self
935 935
936 936 def __get__(self, obj, type=None):
937 937 # if accessed on the class, return the descriptor itself.
938 938 if obj is None:
939 939 return self
940 940 # do we need to check if the file changed?
941 941 if self.name in obj.__dict__:
942 942 assert self.name in obj._filecache, self.name
943 943 return obj.__dict__[self.name]
944 944
945 945 entry = obj._filecache.get(self.name)
946 946
947 947 if entry:
948 948 if entry.changed():
949 949 entry.obj = self.func(obj)
950 950 else:
951 951 paths = [self.join(obj, path) for path in self.paths]
952 952
953 953 # We stat -before- creating the object so our cache doesn't lie if
954 954 # a writer modified between the time we read and stat
955 955 entry = filecacheentry(paths, True)
956 956 entry.obj = self.func(obj)
957 957
958 958 obj._filecache[self.name] = entry
959 959
960 960 obj.__dict__[self.name] = entry.obj
961 961 return entry.obj
962 962
963 963 def __set__(self, obj, value):
964 964 if self.name not in obj._filecache:
965 965 # we add an entry for the missing value because X in __dict__
966 966 # implies X in _filecache
967 967 paths = [self.join(obj, path) for path in self.paths]
968 968 ce = filecacheentry(paths, False)
969 969 obj._filecache[self.name] = ce
970 970 else:
971 971 ce = obj._filecache[self.name]
972 972
973 973 ce.obj = value # update cached copy
974 974 obj.__dict__[self.name] = value # update copy returned by obj.x
975 975
976 976 def __delete__(self, obj):
977 977 try:
978 978 del obj.__dict__[self.name]
979 979 except KeyError:
980 980 raise AttributeError(self.name)
981 981
982 982 def _locksub(repo, lock, envvar, cmd, environ=None, *args, **kwargs):
983 983 if lock is None:
984 984 raise error.LockInheritanceContractViolation(
985 985 'lock can only be inherited while held')
986 986 if environ is None:
987 987 environ = {}
988 988 with lock.inherit() as locker:
989 989 environ[envvar] = locker
990 990 return repo.ui.system(cmd, environ=environ, *args, **kwargs)
991 991
992 992 def wlocksub(repo, cmd, *args, **kwargs):
993 993 """run cmd as a subprocess that allows inheriting repo's wlock
994 994
995 995 This can only be called while the wlock is held. This takes all the
996 996 arguments that ui.system does, and returns the exit code of the
997 997 subprocess."""
998 998 return _locksub(repo, repo.currentwlock(), 'HG_WLOCK_LOCKER', cmd, *args,
999 999 **kwargs)
1000 1000
1001 1001 def gdinitconfig(ui):
1002 1002 """helper function to know if a repo should be created as general delta
1003 1003 """
1004 1004 # experimental config: format.generaldelta
1005 1005 return (ui.configbool('format', 'generaldelta')
1006 1006 or ui.configbool('format', 'usegeneraldelta'))
1007 1007
1008 1008 def gddeltaconfig(ui):
1009 1009 """helper function to know if incoming delta should be optimised
1010 1010 """
1011 1011 # experimental config: format.generaldelta
1012 1012 return ui.configbool('format', 'generaldelta')
1013 1013
1014 1014 class simplekeyvaluefile(object):
1015 1015 """A simple file with key=value lines
1016 1016
1017 1017 Keys must be alphanumerics and start with a letter, values must not
1018 1018 contain '\n' characters"""
1019 1019 firstlinekey = '__firstline'
1020 1020
1021 1021 def __init__(self, vfs, path, keys=None):
1022 1022 self.vfs = vfs
1023 1023 self.path = path
1024 1024
1025 1025 def read(self, firstlinenonkeyval=False):
1026 1026 """Read the contents of a simple key-value file
1027 1027
1028 1028 'firstlinenonkeyval' indicates whether the first line of file should
1029 1029 be treated as a key-value pair or reuturned fully under the
1030 1030 __firstline key."""
1031 1031 lines = self.vfs.readlines(self.path)
1032 1032 d = {}
1033 1033 if firstlinenonkeyval:
1034 1034 if not lines:
1035 1035 e = _("empty simplekeyvalue file")
1036 1036 raise error.CorruptedState(e)
1037 1037 # we don't want to include '\n' in the __firstline
1038 1038 d[self.firstlinekey] = lines[0][:-1]
1039 1039 del lines[0]
1040 1040
1041 1041 try:
1042 1042 # the 'if line.strip()' part prevents us from failing on empty
1043 1043 # lines which only contain '\n' therefore are not skipped
1044 1044 # by 'if line'
1045 1045 updatedict = dict(line[:-1].split('=', 1) for line in lines
1046 1046 if line.strip())
1047 1047 if self.firstlinekey in updatedict:
1048 1048 e = _("%r can't be used as a key")
1049 1049 raise error.CorruptedState(e % self.firstlinekey)
1050 1050 d.update(updatedict)
1051 1051 except ValueError as e:
1052 1052 raise error.CorruptedState(str(e))
1053 1053 return d
1054 1054
1055 1055 def write(self, data, firstline=None):
1056 1056 """Write key=>value mapping to a file
1057 1057 data is a dict. Keys must be alphanumerical and start with a letter.
1058 1058 Values must not contain newline characters.
1059 1059
1060 1060 If 'firstline' is not None, it is written to file before
1061 1061 everything else, as it is, not in a key=value form"""
1062 1062 lines = []
1063 1063 if firstline is not None:
1064 1064 lines.append('%s\n' % firstline)
1065 1065
1066 1066 for k, v in data.items():
1067 1067 if k == self.firstlinekey:
1068 1068 e = "key name '%s' is reserved" % self.firstlinekey
1069 1069 raise error.ProgrammingError(e)
1070 1070 if not k[0].isalpha():
1071 1071 e = "keys must start with a letter in a key-value file"
1072 1072 raise error.ProgrammingError(e)
1073 1073 if not k.isalnum():
1074 1074 e = "invalid key name in a simple key-value file"
1075 1075 raise error.ProgrammingError(e)
1076 1076 if '\n' in v:
1077 1077 e = "invalid value in a simple key-value file"
1078 1078 raise error.ProgrammingError(e)
1079 1079 lines.append("%s=%s\n" % (k, v))
1080 1080 with self.vfs(self.path, mode='wb', atomictemp=True) as fp:
1081 1081 fp.write(''.join(lines))
1082 1082
1083 def registersummarycallback(repo, otr):
1083 _reportobsoletedsource = [
1084 'pull',
1085 'push',
1086 'serve',
1087 'unbundle',
1088 ]
1089
1090 def registersummarycallback(repo, otr, txnname=''):
1084 1091 """register a callback to issue a summary after the transaction is closed
1085 1092 """
1086 reporef = weakref.ref(repo)
1087 def reportsummary(tr):
1088 """the actual callback reporting the summary"""
1089 repo = reporef()
1090 obsoleted = obsutil.getobsoleted(repo, tr)
1091 if obsoleted:
1092 repo.ui.status(_('obsoleted %i changesets\n') % len(obsoleted))
1093 otr.addpostclose('00-txnreport', reportsummary)
1093 for source in _reportobsoletedsource:
1094 if txnname.startswith(source):
1095 reporef = weakref.ref(repo)
1096 def reportsummary(tr):
1097 """the actual callback reporting the summary"""
1098 repo = reporef()
1099 obsoleted = obsutil.getobsoleted(repo, tr)
1100 if obsoleted:
1101 repo.ui.status(_('obsoleted %i changesets\n')
1102 % len(obsoleted))
1103 otr.addpostclose('00-txnreport', reportsummary)
1104 break
General Comments 0
You need to be logged in to leave comments. Login now