##// END OF EJS Templates
exchange: drop support for lock-based unbundling (BC)...
Gregory Szorc -
r33667:fda0867c default
parent child Browse files
Show More
@@ -1,2022 +1,2009 b''
1 1 # exchange.py - utility to exchange data between repos.
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
13 13 from .i18n import _
14 14 from .node import (
15 15 hex,
16 16 nullid,
17 17 )
18 18 from . import (
19 19 bookmarks as bookmod,
20 20 bundle2,
21 21 changegroup,
22 22 discovery,
23 23 error,
24 24 lock as lockmod,
25 25 obsolete,
26 26 phases,
27 27 pushkey,
28 28 pycompat,
29 29 scmutil,
30 30 sslutil,
31 31 streamclone,
32 32 url as urlmod,
33 33 util,
34 34 )
35 35
36 36 urlerr = util.urlerr
37 37 urlreq = util.urlreq
38 38
39 39 # Maps bundle version human names to changegroup versions.
40 40 _bundlespeccgversions = {'v1': '01',
41 41 'v2': '02',
42 42 'packed1': 's1',
43 43 'bundle2': '02', #legacy
44 44 }
45 45
46 46 # Compression engines allowed in version 1. THIS SHOULD NEVER CHANGE.
47 47 _bundlespecv1compengines = {'gzip', 'bzip2', 'none'}
48 48
49 49 def parsebundlespec(repo, spec, strict=True, externalnames=False):
50 50 """Parse a bundle string specification into parts.
51 51
52 52 Bundle specifications denote a well-defined bundle/exchange format.
53 53 The content of a given specification should not change over time in
54 54 order to ensure that bundles produced by a newer version of Mercurial are
55 55 readable from an older version.
56 56
57 57 The string currently has the form:
58 58
59 59 <compression>-<type>[;<parameter0>[;<parameter1>]]
60 60
61 61 Where <compression> is one of the supported compression formats
62 62 and <type> is (currently) a version string. A ";" can follow the type and
63 63 all text afterwards is interpreted as URI encoded, ";" delimited key=value
64 64 pairs.
65 65
66 66 If ``strict`` is True (the default) <compression> is required. Otherwise,
67 67 it is optional.
68 68
69 69 If ``externalnames`` is False (the default), the human-centric names will
70 70 be converted to their internal representation.
71 71
72 72 Returns a 3-tuple of (compression, version, parameters). Compression will
73 73 be ``None`` if not in strict mode and a compression isn't defined.
74 74
75 75 An ``InvalidBundleSpecification`` is raised when the specification is
76 76 not syntactically well formed.
77 77
78 78 An ``UnsupportedBundleSpecification`` is raised when the compression or
79 79 bundle type/version is not recognized.
80 80
81 81 Note: this function will likely eventually return a more complex data
82 82 structure, including bundle2 part information.
83 83 """
84 84 def parseparams(s):
85 85 if ';' not in s:
86 86 return s, {}
87 87
88 88 params = {}
89 89 version, paramstr = s.split(';', 1)
90 90
91 91 for p in paramstr.split(';'):
92 92 if '=' not in p:
93 93 raise error.InvalidBundleSpecification(
94 94 _('invalid bundle specification: '
95 95 'missing "=" in parameter: %s') % p)
96 96
97 97 key, value = p.split('=', 1)
98 98 key = urlreq.unquote(key)
99 99 value = urlreq.unquote(value)
100 100 params[key] = value
101 101
102 102 return version, params
103 103
104 104
105 105 if strict and '-' not in spec:
106 106 raise error.InvalidBundleSpecification(
107 107 _('invalid bundle specification; '
108 108 'must be prefixed with compression: %s') % spec)
109 109
110 110 if '-' in spec:
111 111 compression, version = spec.split('-', 1)
112 112
113 113 if compression not in util.compengines.supportedbundlenames:
114 114 raise error.UnsupportedBundleSpecification(
115 115 _('%s compression is not supported') % compression)
116 116
117 117 version, params = parseparams(version)
118 118
119 119 if version not in _bundlespeccgversions:
120 120 raise error.UnsupportedBundleSpecification(
121 121 _('%s is not a recognized bundle version') % version)
122 122 else:
123 123 # Value could be just the compression or just the version, in which
124 124 # case some defaults are assumed (but only when not in strict mode).
125 125 assert not strict
126 126
127 127 spec, params = parseparams(spec)
128 128
129 129 if spec in util.compengines.supportedbundlenames:
130 130 compression = spec
131 131 version = 'v1'
132 132 # Generaldelta repos require v2.
133 133 if 'generaldelta' in repo.requirements:
134 134 version = 'v2'
135 135 # Modern compression engines require v2.
136 136 if compression not in _bundlespecv1compengines:
137 137 version = 'v2'
138 138 elif spec in _bundlespeccgversions:
139 139 if spec == 'packed1':
140 140 compression = 'none'
141 141 else:
142 142 compression = 'bzip2'
143 143 version = spec
144 144 else:
145 145 raise error.UnsupportedBundleSpecification(
146 146 _('%s is not a recognized bundle specification') % spec)
147 147
148 148 # Bundle version 1 only supports a known set of compression engines.
149 149 if version == 'v1' and compression not in _bundlespecv1compengines:
150 150 raise error.UnsupportedBundleSpecification(
151 151 _('compression engine %s is not supported on v1 bundles') %
152 152 compression)
153 153
154 154 # The specification for packed1 can optionally declare the data formats
155 155 # required to apply it. If we see this metadata, compare against what the
156 156 # repo supports and error if the bundle isn't compatible.
157 157 if version == 'packed1' and 'requirements' in params:
158 158 requirements = set(params['requirements'].split(','))
159 159 missingreqs = requirements - repo.supportedformats
160 160 if missingreqs:
161 161 raise error.UnsupportedBundleSpecification(
162 162 _('missing support for repository features: %s') %
163 163 ', '.join(sorted(missingreqs)))
164 164
165 165 if not externalnames:
166 166 engine = util.compengines.forbundlename(compression)
167 167 compression = engine.bundletype()[1]
168 168 version = _bundlespeccgversions[version]
169 169 return compression, version, params
170 170
171 171 def readbundle(ui, fh, fname, vfs=None):
172 172 header = changegroup.readexactly(fh, 4)
173 173
174 174 alg = None
175 175 if not fname:
176 176 fname = "stream"
177 177 if not header.startswith('HG') and header.startswith('\0'):
178 178 fh = changegroup.headerlessfixup(fh, header)
179 179 header = "HG10"
180 180 alg = 'UN'
181 181 elif vfs:
182 182 fname = vfs.join(fname)
183 183
184 184 magic, version = header[0:2], header[2:4]
185 185
186 186 if magic != 'HG':
187 187 raise error.Abort(_('%s: not a Mercurial bundle') % fname)
188 188 if version == '10':
189 189 if alg is None:
190 190 alg = changegroup.readexactly(fh, 2)
191 191 return changegroup.cg1unpacker(fh, alg)
192 192 elif version.startswith('2'):
193 193 return bundle2.getunbundler(ui, fh, magicstring=magic + version)
194 194 elif version == 'S1':
195 195 return streamclone.streamcloneapplier(fh)
196 196 else:
197 197 raise error.Abort(_('%s: unknown bundle version %s') % (fname, version))
198 198
199 199 def getbundlespec(ui, fh):
200 200 """Infer the bundlespec from a bundle file handle.
201 201
202 202 The input file handle is seeked and the original seek position is not
203 203 restored.
204 204 """
205 205 def speccompression(alg):
206 206 try:
207 207 return util.compengines.forbundletype(alg).bundletype()[0]
208 208 except KeyError:
209 209 return None
210 210
211 211 b = readbundle(ui, fh, None)
212 212 if isinstance(b, changegroup.cg1unpacker):
213 213 alg = b._type
214 214 if alg == '_truncatedBZ':
215 215 alg = 'BZ'
216 216 comp = speccompression(alg)
217 217 if not comp:
218 218 raise error.Abort(_('unknown compression algorithm: %s') % alg)
219 219 return '%s-v1' % comp
220 220 elif isinstance(b, bundle2.unbundle20):
221 221 if 'Compression' in b.params:
222 222 comp = speccompression(b.params['Compression'])
223 223 if not comp:
224 224 raise error.Abort(_('unknown compression algorithm: %s') % comp)
225 225 else:
226 226 comp = 'none'
227 227
228 228 version = None
229 229 for part in b.iterparts():
230 230 if part.type == 'changegroup':
231 231 version = part.params['version']
232 232 if version in ('01', '02'):
233 233 version = 'v2'
234 234 else:
235 235 raise error.Abort(_('changegroup version %s does not have '
236 236 'a known bundlespec') % version,
237 237 hint=_('try upgrading your Mercurial '
238 238 'client'))
239 239
240 240 if not version:
241 241 raise error.Abort(_('could not identify changegroup version in '
242 242 'bundle'))
243 243
244 244 return '%s-%s' % (comp, version)
245 245 elif isinstance(b, streamclone.streamcloneapplier):
246 246 requirements = streamclone.readbundle1header(fh)[2]
247 247 params = 'requirements=%s' % ','.join(sorted(requirements))
248 248 return 'none-packed1;%s' % urlreq.quote(params)
249 249 else:
250 250 raise error.Abort(_('unknown bundle type: %s') % b)
251 251
252 252 def _computeoutgoing(repo, heads, common):
253 253 """Computes which revs are outgoing given a set of common
254 254 and a set of heads.
255 255
256 256 This is a separate function so extensions can have access to
257 257 the logic.
258 258
259 259 Returns a discovery.outgoing object.
260 260 """
261 261 cl = repo.changelog
262 262 if common:
263 263 hasnode = cl.hasnode
264 264 common = [n for n in common if hasnode(n)]
265 265 else:
266 266 common = [nullid]
267 267 if not heads:
268 268 heads = cl.heads()
269 269 return discovery.outgoing(repo, common, heads)
270 270
271 271 def _forcebundle1(op):
272 272 """return true if a pull/push must use bundle1
273 273
274 274 This function is used to allow testing of the older bundle version"""
275 275 ui = op.repo.ui
276 276 forcebundle1 = False
277 277 # The goal is this config is to allow developer to choose the bundle
278 278 # version used during exchanged. This is especially handy during test.
279 279 # Value is a list of bundle version to be picked from, highest version
280 280 # should be used.
281 281 #
282 282 # developer config: devel.legacy.exchange
283 283 exchange = ui.configlist('devel', 'legacy.exchange')
284 284 forcebundle1 = 'bundle2' not in exchange and 'bundle1' in exchange
285 285 return forcebundle1 or not op.remote.capable('bundle2')
286 286
287 287 class pushoperation(object):
288 288 """A object that represent a single push operation
289 289
290 290 Its purpose is to carry push related state and very common operations.
291 291
292 292 A new pushoperation should be created at the beginning of each push and
293 293 discarded afterward.
294 294 """
295 295
296 296 def __init__(self, repo, remote, force=False, revs=None, newbranch=False,
297 297 bookmarks=()):
298 298 # repo we push from
299 299 self.repo = repo
300 300 self.ui = repo.ui
301 301 # repo we push to
302 302 self.remote = remote
303 303 # force option provided
304 304 self.force = force
305 305 # revs to be pushed (None is "all")
306 306 self.revs = revs
307 307 # bookmark explicitly pushed
308 308 self.bookmarks = bookmarks
309 309 # allow push of new branch
310 310 self.newbranch = newbranch
311 311 # did a local lock get acquired?
312 312 self.locallocked = None
313 313 # step already performed
314 314 # (used to check what steps have been already performed through bundle2)
315 315 self.stepsdone = set()
316 316 # Integer version of the changegroup push result
317 317 # - None means nothing to push
318 318 # - 0 means HTTP error
319 319 # - 1 means we pushed and remote head count is unchanged *or*
320 320 # we have outgoing changesets but refused to push
321 321 # - other values as described by addchangegroup()
322 322 self.cgresult = None
323 323 # Boolean value for the bookmark push
324 324 self.bkresult = None
325 325 # discover.outgoing object (contains common and outgoing data)
326 326 self.outgoing = None
327 327 # all remote topological heads before the push
328 328 self.remoteheads = None
329 329 # Details of the remote branch pre and post push
330 330 #
331 331 # mapping: {'branch': ([remoteheads],
332 332 # [newheads],
333 333 # [unsyncedheads],
334 334 # [discardedheads])}
335 335 # - branch: the branch name
336 336 # - remoteheads: the list of remote heads known locally
337 337 # None if the branch is new
338 338 # - newheads: the new remote heads (known locally) with outgoing pushed
339 339 # - unsyncedheads: the list of remote heads unknown locally.
340 340 # - discardedheads: the list of remote heads made obsolete by the push
341 341 self.pushbranchmap = None
342 342 # testable as a boolean indicating if any nodes are missing locally.
343 343 self.incoming = None
344 344 # phases changes that must be pushed along side the changesets
345 345 self.outdatedphases = None
346 346 # phases changes that must be pushed if changeset push fails
347 347 self.fallbackoutdatedphases = None
348 348 # outgoing obsmarkers
349 349 self.outobsmarkers = set()
350 350 # outgoing bookmarks
351 351 self.outbookmarks = []
352 352 # transaction manager
353 353 self.trmanager = None
354 354 # map { pushkey partid -> callback handling failure}
355 355 # used to handle exception from mandatory pushkey part failure
356 356 self.pkfailcb = {}
357 357
358 358 @util.propertycache
359 359 def futureheads(self):
360 360 """future remote heads if the changeset push succeeds"""
361 361 return self.outgoing.missingheads
362 362
363 363 @util.propertycache
364 364 def fallbackheads(self):
365 365 """future remote heads if the changeset push fails"""
366 366 if self.revs is None:
367 367 # not target to push, all common are relevant
368 368 return self.outgoing.commonheads
369 369 unfi = self.repo.unfiltered()
370 370 # I want cheads = heads(::missingheads and ::commonheads)
371 371 # (missingheads is revs with secret changeset filtered out)
372 372 #
373 373 # This can be expressed as:
374 374 # cheads = ( (missingheads and ::commonheads)
375 375 # + (commonheads and ::missingheads))"
376 376 # )
377 377 #
378 378 # while trying to push we already computed the following:
379 379 # common = (::commonheads)
380 380 # missing = ((commonheads::missingheads) - commonheads)
381 381 #
382 382 # We can pick:
383 383 # * missingheads part of common (::commonheads)
384 384 common = self.outgoing.common
385 385 nm = self.repo.changelog.nodemap
386 386 cheads = [node for node in self.revs if nm[node] in common]
387 387 # and
388 388 # * commonheads parents on missing
389 389 revset = unfi.set('%ln and parents(roots(%ln))',
390 390 self.outgoing.commonheads,
391 391 self.outgoing.missing)
392 392 cheads.extend(c.node() for c in revset)
393 393 return cheads
394 394
395 395 @property
396 396 def commonheads(self):
397 397 """set of all common heads after changeset bundle push"""
398 398 if self.cgresult:
399 399 return self.futureheads
400 400 else:
401 401 return self.fallbackheads
402 402
403 403 # mapping of message used when pushing bookmark
404 404 bookmsgmap = {'update': (_("updating bookmark %s\n"),
405 405 _('updating bookmark %s failed!\n')),
406 406 'export': (_("exporting bookmark %s\n"),
407 407 _('exporting bookmark %s failed!\n')),
408 408 'delete': (_("deleting remote bookmark %s\n"),
409 409 _('deleting remote bookmark %s failed!\n')),
410 410 }
411 411
412 412
413 413 def push(repo, remote, force=False, revs=None, newbranch=False, bookmarks=(),
414 414 opargs=None):
415 415 '''Push outgoing changesets (limited by revs) from a local
416 416 repository to remote. Return an integer:
417 417 - None means nothing to push
418 418 - 0 means HTTP error
419 419 - 1 means we pushed and remote head count is unchanged *or*
420 420 we have outgoing changesets but refused to push
421 421 - other values as described by addchangegroup()
422 422 '''
423 423 if opargs is None:
424 424 opargs = {}
425 425 pushop = pushoperation(repo, remote, force, revs, newbranch, bookmarks,
426 426 **opargs)
427 427 if pushop.remote.local():
428 428 missing = (set(pushop.repo.requirements)
429 429 - pushop.remote.local().supported)
430 430 if missing:
431 431 msg = _("required features are not"
432 432 " supported in the destination:"
433 433 " %s") % (', '.join(sorted(missing)))
434 434 raise error.Abort(msg)
435 435
436 # there are two ways to push to remote repo:
437 #
438 # addchangegroup assumes local user can lock remote
439 # repo (local filesystem, old ssh servers).
440 #
441 # unbundle assumes local user cannot lock remote repo (new ssh
442 # servers, http servers).
443
444 436 if not pushop.remote.canpush():
445 437 raise error.Abort(_("destination does not support push"))
438
439 if not pushop.remote.capable('unbundle'):
440 raise error.Abort(_('cannot push: destination does not support the '
441 'unbundle wire protocol command'))
442
446 443 # get local lock as we might write phase data
447 444 localwlock = locallock = None
448 445 try:
449 446 # bundle2 push may receive a reply bundle touching bookmarks or other
450 447 # things requiring the wlock. Take it now to ensure proper ordering.
451 448 maypushback = pushop.ui.configbool('experimental', 'bundle2.pushback')
452 449 if (not _forcebundle1(pushop)) and maypushback:
453 450 localwlock = pushop.repo.wlock()
454 451 locallock = pushop.repo.lock()
455 452 pushop.locallocked = True
456 453 except IOError as err:
457 454 pushop.locallocked = False
458 455 if err.errno != errno.EACCES:
459 456 raise
460 457 # source repo cannot be locked.
461 458 # We do not abort the push, but just disable the local phase
462 459 # synchronisation.
463 460 msg = 'cannot lock source repository: %s\n' % err
464 461 pushop.ui.debug(msg)
465 462 try:
466 463 if pushop.locallocked:
467 464 pushop.trmanager = transactionmanager(pushop.repo,
468 465 'push-response',
469 466 pushop.remote.url())
470 467 pushop.repo.checkpush(pushop)
471 lock = None
472 unbundle = pushop.remote.capable('unbundle')
473 if not unbundle:
474 lock = pushop.remote.lock()
475 try:
476 _pushdiscovery(pushop)
477 if not _forcebundle1(pushop):
478 _pushbundle2(pushop)
479 _pushchangeset(pushop)
480 _pushsyncphase(pushop)
481 _pushobsolete(pushop)
482 _pushbookmark(pushop)
483 finally:
484 if lock is not None:
485 lock.release()
468 _pushdiscovery(pushop)
469 if not _forcebundle1(pushop):
470 _pushbundle2(pushop)
471 _pushchangeset(pushop)
472 _pushsyncphase(pushop)
473 _pushobsolete(pushop)
474 _pushbookmark(pushop)
475
486 476 if pushop.trmanager:
487 477 pushop.trmanager.close()
488 478 finally:
489 479 if pushop.trmanager:
490 480 pushop.trmanager.release()
491 481 if locallock is not None:
492 482 locallock.release()
493 483 if localwlock is not None:
494 484 localwlock.release()
495 485
496 486 return pushop
497 487
498 488 # list of steps to perform discovery before push
499 489 pushdiscoveryorder = []
500 490
501 491 # Mapping between step name and function
502 492 #
503 493 # This exists to help extensions wrap steps if necessary
504 494 pushdiscoverymapping = {}
505 495
506 496 def pushdiscovery(stepname):
507 497 """decorator for function performing discovery before push
508 498
509 499 The function is added to the step -> function mapping and appended to the
510 500 list of steps. Beware that decorated function will be added in order (this
511 501 may matter).
512 502
513 503 You can only use this decorator for a new step, if you want to wrap a step
514 504 from an extension, change the pushdiscovery dictionary directly."""
515 505 def dec(func):
516 506 assert stepname not in pushdiscoverymapping
517 507 pushdiscoverymapping[stepname] = func
518 508 pushdiscoveryorder.append(stepname)
519 509 return func
520 510 return dec
521 511
522 512 def _pushdiscovery(pushop):
523 513 """Run all discovery steps"""
524 514 for stepname in pushdiscoveryorder:
525 515 step = pushdiscoverymapping[stepname]
526 516 step(pushop)
527 517
528 518 @pushdiscovery('changeset')
529 519 def _pushdiscoverychangeset(pushop):
530 520 """discover the changeset that need to be pushed"""
531 521 fci = discovery.findcommonincoming
532 522 commoninc = fci(pushop.repo, pushop.remote, force=pushop.force)
533 523 common, inc, remoteheads = commoninc
534 524 fco = discovery.findcommonoutgoing
535 525 outgoing = fco(pushop.repo, pushop.remote, onlyheads=pushop.revs,
536 526 commoninc=commoninc, force=pushop.force)
537 527 pushop.outgoing = outgoing
538 528 pushop.remoteheads = remoteheads
539 529 pushop.incoming = inc
540 530
541 531 @pushdiscovery('phase')
542 532 def _pushdiscoveryphase(pushop):
543 533 """discover the phase that needs to be pushed
544 534
545 535 (computed for both success and failure case for changesets push)"""
546 536 outgoing = pushop.outgoing
547 537 unfi = pushop.repo.unfiltered()
548 538 remotephases = pushop.remote.listkeys('phases')
549 539 publishing = remotephases.get('publishing', False)
550 540 if (pushop.ui.configbool('ui', '_usedassubrepo')
551 541 and remotephases # server supports phases
552 542 and not pushop.outgoing.missing # no changesets to be pushed
553 543 and publishing):
554 544 # When:
555 545 # - this is a subrepo push
556 546 # - and remote support phase
557 547 # - and no changeset are to be pushed
558 548 # - and remote is publishing
559 549 # We may be in issue 3871 case!
560 550 # We drop the possible phase synchronisation done by
561 551 # courtesy to publish changesets possibly locally draft
562 552 # on the remote.
563 553 remotephases = {'publishing': 'True'}
564 554 ana = phases.analyzeremotephases(pushop.repo,
565 555 pushop.fallbackheads,
566 556 remotephases)
567 557 pheads, droots = ana
568 558 extracond = ''
569 559 if not publishing:
570 560 extracond = ' and public()'
571 561 revset = 'heads((%%ln::%%ln) %s)' % extracond
572 562 # Get the list of all revs draft on remote by public here.
573 563 # XXX Beware that revset break if droots is not strictly
574 564 # XXX root we may want to ensure it is but it is costly
575 565 fallback = list(unfi.set(revset, droots, pushop.fallbackheads))
576 566 if not outgoing.missing:
577 567 future = fallback
578 568 else:
579 569 # adds changeset we are going to push as draft
580 570 #
581 571 # should not be necessary for publishing server, but because of an
582 572 # issue fixed in xxxxx we have to do it anyway.
583 573 fdroots = list(unfi.set('roots(%ln + %ln::)',
584 574 outgoing.missing, droots))
585 575 fdroots = [f.node() for f in fdroots]
586 576 future = list(unfi.set(revset, fdroots, pushop.futureheads))
587 577 pushop.outdatedphases = future
588 578 pushop.fallbackoutdatedphases = fallback
589 579
590 580 @pushdiscovery('obsmarker')
591 581 def _pushdiscoveryobsmarkers(pushop):
592 582 if (obsolete.isenabled(pushop.repo, obsolete.exchangeopt)
593 583 and pushop.repo.obsstore
594 584 and 'obsolete' in pushop.remote.listkeys('namespaces')):
595 585 repo = pushop.repo
596 586 # very naive computation, that can be quite expensive on big repo.
597 587 # However: evolution is currently slow on them anyway.
598 588 nodes = (c.node() for c in repo.set('::%ln', pushop.futureheads))
599 589 pushop.outobsmarkers = pushop.repo.obsstore.relevantmarkers(nodes)
600 590
601 591 @pushdiscovery('bookmarks')
602 592 def _pushdiscoverybookmarks(pushop):
603 593 ui = pushop.ui
604 594 repo = pushop.repo.unfiltered()
605 595 remote = pushop.remote
606 596 ui.debug("checking for updated bookmarks\n")
607 597 ancestors = ()
608 598 if pushop.revs:
609 599 revnums = map(repo.changelog.rev, pushop.revs)
610 600 ancestors = repo.changelog.ancestors(revnums, inclusive=True)
611 601 remotebookmark = remote.listkeys('bookmarks')
612 602
613 603 explicit = set([repo._bookmarks.expandname(bookmark)
614 604 for bookmark in pushop.bookmarks])
615 605
616 606 remotebookmark = bookmod.unhexlifybookmarks(remotebookmark)
617 607 comp = bookmod.comparebookmarks(repo, repo._bookmarks, remotebookmark)
618 608
619 609 def safehex(x):
620 610 if x is None:
621 611 return x
622 612 return hex(x)
623 613
624 614 def hexifycompbookmarks(bookmarks):
625 615 for b, scid, dcid in bookmarks:
626 616 yield b, safehex(scid), safehex(dcid)
627 617
628 618 comp = [hexifycompbookmarks(marks) for marks in comp]
629 619 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = comp
630 620
631 621 for b, scid, dcid in advsrc:
632 622 if b in explicit:
633 623 explicit.remove(b)
634 624 if not ancestors or repo[scid].rev() in ancestors:
635 625 pushop.outbookmarks.append((b, dcid, scid))
636 626 # search added bookmark
637 627 for b, scid, dcid in addsrc:
638 628 if b in explicit:
639 629 explicit.remove(b)
640 630 pushop.outbookmarks.append((b, '', scid))
641 631 # search for overwritten bookmark
642 632 for b, scid, dcid in list(advdst) + list(diverge) + list(differ):
643 633 if b in explicit:
644 634 explicit.remove(b)
645 635 pushop.outbookmarks.append((b, dcid, scid))
646 636 # search for bookmark to delete
647 637 for b, scid, dcid in adddst:
648 638 if b in explicit:
649 639 explicit.remove(b)
650 640 # treat as "deleted locally"
651 641 pushop.outbookmarks.append((b, dcid, ''))
652 642 # identical bookmarks shouldn't get reported
653 643 for b, scid, dcid in same:
654 644 if b in explicit:
655 645 explicit.remove(b)
656 646
657 647 if explicit:
658 648 explicit = sorted(explicit)
659 649 # we should probably list all of them
660 650 ui.warn(_('bookmark %s does not exist on the local '
661 651 'or remote repository!\n') % explicit[0])
662 652 pushop.bkresult = 2
663 653
664 654 pushop.outbookmarks.sort()
665 655
666 656 def _pushcheckoutgoing(pushop):
667 657 outgoing = pushop.outgoing
668 658 unfi = pushop.repo.unfiltered()
669 659 if not outgoing.missing:
670 660 # nothing to push
671 661 scmutil.nochangesfound(unfi.ui, unfi, outgoing.excluded)
672 662 return False
673 663 # something to push
674 664 if not pushop.force:
675 665 # if repo.obsstore == False --> no obsolete
676 666 # then, save the iteration
677 667 if unfi.obsstore:
678 668 # this message are here for 80 char limit reason
679 669 mso = _("push includes obsolete changeset: %s!")
680 670 mspd = _("push includes phase-divergent changeset: %s!")
681 671 mscd = _("push includes content-divergent changeset: %s!")
682 672 mst = {"orphan": _("push includes orphan changeset: %s!"),
683 673 "phase-divergent": mspd,
684 674 "content-divergent": mscd}
685 675 # If we are to push if there is at least one
686 676 # obsolete or unstable changeset in missing, at
687 677 # least one of the missinghead will be obsolete or
688 678 # unstable. So checking heads only is ok
689 679 for node in outgoing.missingheads:
690 680 ctx = unfi[node]
691 681 if ctx.obsolete():
692 682 raise error.Abort(mso % ctx)
693 683 elif ctx.troubled():
694 684 raise error.Abort(mst[ctx.troubles()[0]] % ctx)
695 685
696 686 discovery.checkheads(pushop)
697 687 return True
698 688
699 689 # List of names of steps to perform for an outgoing bundle2, order matters.
700 690 b2partsgenorder = []
701 691
702 692 # Mapping between step name and function
703 693 #
704 694 # This exists to help extensions wrap steps if necessary
705 695 b2partsgenmapping = {}
706 696
707 697 def b2partsgenerator(stepname, idx=None):
708 698 """decorator for function generating bundle2 part
709 699
710 700 The function is added to the step -> function mapping and appended to the
711 701 list of steps. Beware that decorated functions will be added in order
712 702 (this may matter).
713 703
714 704 You can only use this decorator for new steps, if you want to wrap a step
715 705 from an extension, attack the b2partsgenmapping dictionary directly."""
716 706 def dec(func):
717 707 assert stepname not in b2partsgenmapping
718 708 b2partsgenmapping[stepname] = func
719 709 if idx is None:
720 710 b2partsgenorder.append(stepname)
721 711 else:
722 712 b2partsgenorder.insert(idx, stepname)
723 713 return func
724 714 return dec
725 715
726 716 def _pushb2ctxcheckheads(pushop, bundler):
727 717 """Generate race condition checking parts
728 718
729 719 Exists as an independent function to aid extensions
730 720 """
731 721 # * 'force' do not check for push race,
732 722 # * if we don't push anything, there are nothing to check.
733 723 if not pushop.force and pushop.outgoing.missingheads:
734 724 allowunrelated = 'related' in bundler.capabilities.get('checkheads', ())
735 725 emptyremote = pushop.pushbranchmap is None
736 726 if not allowunrelated or emptyremote:
737 727 bundler.newpart('check:heads', data=iter(pushop.remoteheads))
738 728 else:
739 729 affected = set()
740 730 for branch, heads in pushop.pushbranchmap.iteritems():
741 731 remoteheads, newheads, unsyncedheads, discardedheads = heads
742 732 if remoteheads is not None:
743 733 remote = set(remoteheads)
744 734 affected |= set(discardedheads) & remote
745 735 affected |= remote - set(newheads)
746 736 if affected:
747 737 data = iter(sorted(affected))
748 738 bundler.newpart('check:updated-heads', data=data)
749 739
750 740 @b2partsgenerator('changeset')
751 741 def _pushb2ctx(pushop, bundler):
752 742 """handle changegroup push through bundle2
753 743
754 744 addchangegroup result is stored in the ``pushop.cgresult`` attribute.
755 745 """
756 746 if 'changesets' in pushop.stepsdone:
757 747 return
758 748 pushop.stepsdone.add('changesets')
759 749 # Send known heads to the server for race detection.
760 750 if not _pushcheckoutgoing(pushop):
761 751 return
762 752 pushop.repo.prepushoutgoinghooks(pushop)
763 753
764 754 _pushb2ctxcheckheads(pushop, bundler)
765 755
766 756 b2caps = bundle2.bundle2caps(pushop.remote)
767 757 version = '01'
768 758 cgversions = b2caps.get('changegroup')
769 759 if cgversions: # 3.1 and 3.2 ship with an empty value
770 760 cgversions = [v for v in cgversions
771 761 if v in changegroup.supportedoutgoingversions(
772 762 pushop.repo)]
773 763 if not cgversions:
774 764 raise ValueError(_('no common changegroup version'))
775 765 version = max(cgversions)
776 766 cg = changegroup.getlocalchangegroupraw(pushop.repo, 'push',
777 767 pushop.outgoing,
778 768 version=version)
779 769 cgpart = bundler.newpart('changegroup', data=cg)
780 770 if cgversions:
781 771 cgpart.addparam('version', version)
782 772 if 'treemanifest' in pushop.repo.requirements:
783 773 cgpart.addparam('treemanifest', '1')
784 774 def handlereply(op):
785 775 """extract addchangegroup returns from server reply"""
786 776 cgreplies = op.records.getreplies(cgpart.id)
787 777 assert len(cgreplies['changegroup']) == 1
788 778 pushop.cgresult = cgreplies['changegroup'][0]['return']
789 779 return handlereply
790 780
791 781 @b2partsgenerator('phase')
792 782 def _pushb2phases(pushop, bundler):
793 783 """handle phase push through bundle2"""
794 784 if 'phases' in pushop.stepsdone:
795 785 return
796 786 b2caps = bundle2.bundle2caps(pushop.remote)
797 787 if not 'pushkey' in b2caps:
798 788 return
799 789 pushop.stepsdone.add('phases')
800 790 part2node = []
801 791
802 792 def handlefailure(pushop, exc):
803 793 targetid = int(exc.partid)
804 794 for partid, node in part2node:
805 795 if partid == targetid:
806 796 raise error.Abort(_('updating %s to public failed') % node)
807 797
808 798 enc = pushkey.encode
809 799 for newremotehead in pushop.outdatedphases:
810 800 part = bundler.newpart('pushkey')
811 801 part.addparam('namespace', enc('phases'))
812 802 part.addparam('key', enc(newremotehead.hex()))
813 803 part.addparam('old', enc(str(phases.draft)))
814 804 part.addparam('new', enc(str(phases.public)))
815 805 part2node.append((part.id, newremotehead))
816 806 pushop.pkfailcb[part.id] = handlefailure
817 807
818 808 def handlereply(op):
819 809 for partid, node in part2node:
820 810 partrep = op.records.getreplies(partid)
821 811 results = partrep['pushkey']
822 812 assert len(results) <= 1
823 813 msg = None
824 814 if not results:
825 815 msg = _('server ignored update of %s to public!\n') % node
826 816 elif not int(results[0]['return']):
827 817 msg = _('updating %s to public failed!\n') % node
828 818 if msg is not None:
829 819 pushop.ui.warn(msg)
830 820 return handlereply
831 821
832 822 @b2partsgenerator('obsmarkers')
833 823 def _pushb2obsmarkers(pushop, bundler):
834 824 if 'obsmarkers' in pushop.stepsdone:
835 825 return
836 826 remoteversions = bundle2.obsmarkersversion(bundler.capabilities)
837 827 if obsolete.commonversion(remoteversions) is None:
838 828 return
839 829 pushop.stepsdone.add('obsmarkers')
840 830 if pushop.outobsmarkers:
841 831 markers = sorted(pushop.outobsmarkers)
842 832 bundle2.buildobsmarkerspart(bundler, markers)
843 833
844 834 @b2partsgenerator('bookmarks')
845 835 def _pushb2bookmarks(pushop, bundler):
846 836 """handle bookmark push through bundle2"""
847 837 if 'bookmarks' in pushop.stepsdone:
848 838 return
849 839 b2caps = bundle2.bundle2caps(pushop.remote)
850 840 if 'pushkey' not in b2caps:
851 841 return
852 842 pushop.stepsdone.add('bookmarks')
853 843 part2book = []
854 844 enc = pushkey.encode
855 845
856 846 def handlefailure(pushop, exc):
857 847 targetid = int(exc.partid)
858 848 for partid, book, action in part2book:
859 849 if partid == targetid:
860 850 raise error.Abort(bookmsgmap[action][1].rstrip() % book)
861 851 # we should not be called for part we did not generated
862 852 assert False
863 853
864 854 for book, old, new in pushop.outbookmarks:
865 855 part = bundler.newpart('pushkey')
866 856 part.addparam('namespace', enc('bookmarks'))
867 857 part.addparam('key', enc(book))
868 858 part.addparam('old', enc(old))
869 859 part.addparam('new', enc(new))
870 860 action = 'update'
871 861 if not old:
872 862 action = 'export'
873 863 elif not new:
874 864 action = 'delete'
875 865 part2book.append((part.id, book, action))
876 866 pushop.pkfailcb[part.id] = handlefailure
877 867
878 868 def handlereply(op):
879 869 ui = pushop.ui
880 870 for partid, book, action in part2book:
881 871 partrep = op.records.getreplies(partid)
882 872 results = partrep['pushkey']
883 873 assert len(results) <= 1
884 874 if not results:
885 875 pushop.ui.warn(_('server ignored bookmark %s update\n') % book)
886 876 else:
887 877 ret = int(results[0]['return'])
888 878 if ret:
889 879 ui.status(bookmsgmap[action][0] % book)
890 880 else:
891 881 ui.warn(bookmsgmap[action][1] % book)
892 882 if pushop.bkresult is not None:
893 883 pushop.bkresult = 1
894 884 return handlereply
895 885
896 886 @b2partsgenerator('pushvars', idx=0)
897 887 def _getbundlesendvars(pushop, bundler):
898 888 '''send shellvars via bundle2'''
899 889 if getattr(pushop.repo, '_shellvars', ()):
900 890 part = bundler.newpart('pushvars')
901 891
902 892 for key, value in pushop.repo._shellvars.iteritems():
903 893 part.addparam(key, value, mandatory=False)
904 894
905 895 def _pushbundle2(pushop):
906 896 """push data to the remote using bundle2
907 897
908 898 The only currently supported type of data is changegroup but this will
909 899 evolve in the future."""
910 900 bundler = bundle2.bundle20(pushop.ui, bundle2.bundle2caps(pushop.remote))
911 901 pushback = (pushop.trmanager
912 902 and pushop.ui.configbool('experimental', 'bundle2.pushback'))
913 903
914 904 # create reply capability
915 905 capsblob = bundle2.encodecaps(bundle2.getrepocaps(pushop.repo,
916 906 allowpushback=pushback))
917 907 bundler.newpart('replycaps', data=capsblob)
918 908 replyhandlers = []
919 909 for partgenname in b2partsgenorder:
920 910 partgen = b2partsgenmapping[partgenname]
921 911 ret = partgen(pushop, bundler)
922 912 if callable(ret):
923 913 replyhandlers.append(ret)
924 914 # do not push if nothing to push
925 915 if bundler.nbparts <= 1:
926 916 return
927 917 stream = util.chunkbuffer(bundler.getchunks())
928 918 try:
929 919 try:
930 920 reply = pushop.remote.unbundle(
931 921 stream, ['force'], pushop.remote.url())
932 922 except error.BundleValueError as exc:
933 923 raise error.Abort(_('missing support for %s') % exc)
934 924 try:
935 925 trgetter = None
936 926 if pushback:
937 927 trgetter = pushop.trmanager.transaction
938 928 op = bundle2.processbundle(pushop.repo, reply, trgetter)
939 929 except error.BundleValueError as exc:
940 930 raise error.Abort(_('missing support for %s') % exc)
941 931 except bundle2.AbortFromPart as exc:
942 932 pushop.ui.status(_('remote: %s\n') % exc)
943 933 if exc.hint is not None:
944 934 pushop.ui.status(_('remote: %s\n') % ('(%s)' % exc.hint))
945 935 raise error.Abort(_('push failed on remote'))
946 936 except error.PushkeyFailed as exc:
947 937 partid = int(exc.partid)
948 938 if partid not in pushop.pkfailcb:
949 939 raise
950 940 pushop.pkfailcb[partid](pushop, exc)
951 941 for rephand in replyhandlers:
952 942 rephand(op)
953 943
954 944 def _pushchangeset(pushop):
955 945 """Make the actual push of changeset bundle to remote repo"""
956 946 if 'changesets' in pushop.stepsdone:
957 947 return
958 948 pushop.stepsdone.add('changesets')
959 949 if not _pushcheckoutgoing(pushop):
960 950 return
951
952 # Should have verified this in push().
953 assert pushop.remote.capable('unbundle')
954
961 955 pushop.repo.prepushoutgoinghooks(pushop)
962 956 outgoing = pushop.outgoing
963 unbundle = pushop.remote.capable('unbundle')
964 957 # TODO: get bundlecaps from remote
965 958 bundlecaps = None
966 959 # create a changegroup from local
967 960 if pushop.revs is None and not (outgoing.excluded
968 961 or pushop.repo.changelog.filteredrevs):
969 962 # push everything,
970 963 # use the fast path, no race possible on push
971 964 bundler = changegroup.cg1packer(pushop.repo, bundlecaps)
972 965 cg = changegroup.getsubset(pushop.repo,
973 966 outgoing,
974 967 bundler,
975 968 'push',
976 969 fastpath=True)
977 970 else:
978 971 cg = changegroup.getchangegroup(pushop.repo, 'push', outgoing,
979 972 bundlecaps=bundlecaps)
980 973
981 974 # apply changegroup to remote
982 if unbundle:
983 # local repo finds heads on server, finds out what
984 # revs it must push. once revs transferred, if server
985 # finds it has different heads (someone else won
986 # commit/push race), server aborts.
987 if pushop.force:
988 remoteheads = ['force']
989 else:
990 remoteheads = pushop.remoteheads
991 # ssh: return remote's addchangegroup()
992 # http: return remote's addchangegroup() or 0 for error
993 pushop.cgresult = pushop.remote.unbundle(cg, remoteheads,
994 pushop.repo.url())
975 # local repo finds heads on server, finds out what
976 # revs it must push. once revs transferred, if server
977 # finds it has different heads (someone else won
978 # commit/push race), server aborts.
979 if pushop.force:
980 remoteheads = ['force']
995 981 else:
996 # we return an integer indicating remote head count
997 # change
998 pushop.cgresult = pushop.remote.addchangegroup(cg, 'push',
999 pushop.repo.url())
982 remoteheads = pushop.remoteheads
983 # ssh: return remote's addchangegroup()
984 # http: return remote's addchangegroup() or 0 for error
985 pushop.cgresult = pushop.remote.unbundle(cg, remoteheads,
986 pushop.repo.url())
1000 987
1001 988 def _pushsyncphase(pushop):
1002 989 """synchronise phase information locally and remotely"""
1003 990 cheads = pushop.commonheads
1004 991 # even when we don't push, exchanging phase data is useful
1005 992 remotephases = pushop.remote.listkeys('phases')
1006 993 if (pushop.ui.configbool('ui', '_usedassubrepo')
1007 994 and remotephases # server supports phases
1008 995 and pushop.cgresult is None # nothing was pushed
1009 996 and remotephases.get('publishing', False)):
1010 997 # When:
1011 998 # - this is a subrepo push
1012 999 # - and remote support phase
1013 1000 # - and no changeset was pushed
1014 1001 # - and remote is publishing
1015 1002 # We may be in issue 3871 case!
1016 1003 # We drop the possible phase synchronisation done by
1017 1004 # courtesy to publish changesets possibly locally draft
1018 1005 # on the remote.
1019 1006 remotephases = {'publishing': 'True'}
1020 1007 if not remotephases: # old server or public only reply from non-publishing
1021 1008 _localphasemove(pushop, cheads)
1022 1009 # don't push any phase data as there is nothing to push
1023 1010 else:
1024 1011 ana = phases.analyzeremotephases(pushop.repo, cheads,
1025 1012 remotephases)
1026 1013 pheads, droots = ana
1027 1014 ### Apply remote phase on local
1028 1015 if remotephases.get('publishing', False):
1029 1016 _localphasemove(pushop, cheads)
1030 1017 else: # publish = False
1031 1018 _localphasemove(pushop, pheads)
1032 1019 _localphasemove(pushop, cheads, phases.draft)
1033 1020 ### Apply local phase on remote
1034 1021
1035 1022 if pushop.cgresult:
1036 1023 if 'phases' in pushop.stepsdone:
1037 1024 # phases already pushed though bundle2
1038 1025 return
1039 1026 outdated = pushop.outdatedphases
1040 1027 else:
1041 1028 outdated = pushop.fallbackoutdatedphases
1042 1029
1043 1030 pushop.stepsdone.add('phases')
1044 1031
1045 1032 # filter heads already turned public by the push
1046 1033 outdated = [c for c in outdated if c.node() not in pheads]
1047 1034 # fallback to independent pushkey command
1048 1035 for newremotehead in outdated:
1049 1036 r = pushop.remote.pushkey('phases',
1050 1037 newremotehead.hex(),
1051 1038 str(phases.draft),
1052 1039 str(phases.public))
1053 1040 if not r:
1054 1041 pushop.ui.warn(_('updating %s to public failed!\n')
1055 1042 % newremotehead)
1056 1043
1057 1044 def _localphasemove(pushop, nodes, phase=phases.public):
1058 1045 """move <nodes> to <phase> in the local source repo"""
1059 1046 if pushop.trmanager:
1060 1047 phases.advanceboundary(pushop.repo,
1061 1048 pushop.trmanager.transaction(),
1062 1049 phase,
1063 1050 nodes)
1064 1051 else:
1065 1052 # repo is not locked, do not change any phases!
1066 1053 # Informs the user that phases should have been moved when
1067 1054 # applicable.
1068 1055 actualmoves = [n for n in nodes if phase < pushop.repo[n].phase()]
1069 1056 phasestr = phases.phasenames[phase]
1070 1057 if actualmoves:
1071 1058 pushop.ui.status(_('cannot lock source repo, skipping '
1072 1059 'local %s phase update\n') % phasestr)
1073 1060
1074 1061 def _pushobsolete(pushop):
1075 1062 """utility function to push obsolete markers to a remote"""
1076 1063 if 'obsmarkers' in pushop.stepsdone:
1077 1064 return
1078 1065 repo = pushop.repo
1079 1066 remote = pushop.remote
1080 1067 pushop.stepsdone.add('obsmarkers')
1081 1068 if pushop.outobsmarkers:
1082 1069 pushop.ui.debug('try to push obsolete markers to remote\n')
1083 1070 rslts = []
1084 1071 remotedata = obsolete._pushkeyescape(sorted(pushop.outobsmarkers))
1085 1072 for key in sorted(remotedata, reverse=True):
1086 1073 # reverse sort to ensure we end with dump0
1087 1074 data = remotedata[key]
1088 1075 rslts.append(remote.pushkey('obsolete', key, '', data))
1089 1076 if [r for r in rslts if not r]:
1090 1077 msg = _('failed to push some obsolete markers!\n')
1091 1078 repo.ui.warn(msg)
1092 1079
1093 1080 def _pushbookmark(pushop):
1094 1081 """Update bookmark position on remote"""
1095 1082 if pushop.cgresult == 0 or 'bookmarks' in pushop.stepsdone:
1096 1083 return
1097 1084 pushop.stepsdone.add('bookmarks')
1098 1085 ui = pushop.ui
1099 1086 remote = pushop.remote
1100 1087
1101 1088 for b, old, new in pushop.outbookmarks:
1102 1089 action = 'update'
1103 1090 if not old:
1104 1091 action = 'export'
1105 1092 elif not new:
1106 1093 action = 'delete'
1107 1094 if remote.pushkey('bookmarks', b, old, new):
1108 1095 ui.status(bookmsgmap[action][0] % b)
1109 1096 else:
1110 1097 ui.warn(bookmsgmap[action][1] % b)
1111 1098 # discovery can have set the value form invalid entry
1112 1099 if pushop.bkresult is not None:
1113 1100 pushop.bkresult = 1
1114 1101
1115 1102 class pulloperation(object):
1116 1103 """A object that represent a single pull operation
1117 1104
1118 1105 It purpose is to carry pull related state and very common operation.
1119 1106
1120 1107 A new should be created at the beginning of each pull and discarded
1121 1108 afterward.
1122 1109 """
1123 1110
1124 1111 def __init__(self, repo, remote, heads=None, force=False, bookmarks=(),
1125 1112 remotebookmarks=None, streamclonerequested=None):
1126 1113 # repo we pull into
1127 1114 self.repo = repo
1128 1115 # repo we pull from
1129 1116 self.remote = remote
1130 1117 # revision we try to pull (None is "all")
1131 1118 self.heads = heads
1132 1119 # bookmark pulled explicitly
1133 1120 self.explicitbookmarks = [repo._bookmarks.expandname(bookmark)
1134 1121 for bookmark in bookmarks]
1135 1122 # do we force pull?
1136 1123 self.force = force
1137 1124 # whether a streaming clone was requested
1138 1125 self.streamclonerequested = streamclonerequested
1139 1126 # transaction manager
1140 1127 self.trmanager = None
1141 1128 # set of common changeset between local and remote before pull
1142 1129 self.common = None
1143 1130 # set of pulled head
1144 1131 self.rheads = None
1145 1132 # list of missing changeset to fetch remotely
1146 1133 self.fetch = None
1147 1134 # remote bookmarks data
1148 1135 self.remotebookmarks = remotebookmarks
1149 1136 # result of changegroup pulling (used as return code by pull)
1150 1137 self.cgresult = None
1151 1138 # list of step already done
1152 1139 self.stepsdone = set()
1153 1140 # Whether we attempted a clone from pre-generated bundles.
1154 1141 self.clonebundleattempted = False
1155 1142
1156 1143 @util.propertycache
1157 1144 def pulledsubset(self):
1158 1145 """heads of the set of changeset target by the pull"""
1159 1146 # compute target subset
1160 1147 if self.heads is None:
1161 1148 # We pulled every thing possible
1162 1149 # sync on everything common
1163 1150 c = set(self.common)
1164 1151 ret = list(self.common)
1165 1152 for n in self.rheads:
1166 1153 if n not in c:
1167 1154 ret.append(n)
1168 1155 return ret
1169 1156 else:
1170 1157 # We pulled a specific subset
1171 1158 # sync on this subset
1172 1159 return self.heads
1173 1160
1174 1161 @util.propertycache
1175 1162 def canusebundle2(self):
1176 1163 return not _forcebundle1(self)
1177 1164
1178 1165 @util.propertycache
1179 1166 def remotebundle2caps(self):
1180 1167 return bundle2.bundle2caps(self.remote)
1181 1168
1182 1169 def gettransaction(self):
1183 1170 # deprecated; talk to trmanager directly
1184 1171 return self.trmanager.transaction()
1185 1172
1186 1173 class transactionmanager(object):
1187 1174 """An object to manage the life cycle of a transaction
1188 1175
1189 1176 It creates the transaction on demand and calls the appropriate hooks when
1190 1177 closing the transaction."""
1191 1178 def __init__(self, repo, source, url):
1192 1179 self.repo = repo
1193 1180 self.source = source
1194 1181 self.url = url
1195 1182 self._tr = None
1196 1183
1197 1184 def transaction(self):
1198 1185 """Return an open transaction object, constructing if necessary"""
1199 1186 if not self._tr:
1200 1187 trname = '%s\n%s' % (self.source, util.hidepassword(self.url))
1201 1188 self._tr = self.repo.transaction(trname)
1202 1189 self._tr.hookargs['source'] = self.source
1203 1190 self._tr.hookargs['url'] = self.url
1204 1191 return self._tr
1205 1192
1206 1193 def close(self):
1207 1194 """close transaction if created"""
1208 1195 if self._tr is not None:
1209 1196 self._tr.close()
1210 1197
1211 1198 def release(self):
1212 1199 """release transaction if created"""
1213 1200 if self._tr is not None:
1214 1201 self._tr.release()
1215 1202
1216 1203 def pull(repo, remote, heads=None, force=False, bookmarks=(), opargs=None,
1217 1204 streamclonerequested=None):
1218 1205 """Fetch repository data from a remote.
1219 1206
1220 1207 This is the main function used to retrieve data from a remote repository.
1221 1208
1222 1209 ``repo`` is the local repository to clone into.
1223 1210 ``remote`` is a peer instance.
1224 1211 ``heads`` is an iterable of revisions we want to pull. ``None`` (the
1225 1212 default) means to pull everything from the remote.
1226 1213 ``bookmarks`` is an iterable of bookmarks requesting to be pulled. By
1227 1214 default, all remote bookmarks are pulled.
1228 1215 ``opargs`` are additional keyword arguments to pass to ``pulloperation``
1229 1216 initialization.
1230 1217 ``streamclonerequested`` is a boolean indicating whether a "streaming
1231 1218 clone" is requested. A "streaming clone" is essentially a raw file copy
1232 1219 of revlogs from the server. This only works when the local repository is
1233 1220 empty. The default value of ``None`` means to respect the server
1234 1221 configuration for preferring stream clones.
1235 1222
1236 1223 Returns the ``pulloperation`` created for this pull.
1237 1224 """
1238 1225 if opargs is None:
1239 1226 opargs = {}
1240 1227 pullop = pulloperation(repo, remote, heads, force, bookmarks=bookmarks,
1241 1228 streamclonerequested=streamclonerequested, **opargs)
1242 1229 if pullop.remote.local():
1243 1230 missing = set(pullop.remote.requirements) - pullop.repo.supported
1244 1231 if missing:
1245 1232 msg = _("required features are not"
1246 1233 " supported in the destination:"
1247 1234 " %s") % (', '.join(sorted(missing)))
1248 1235 raise error.Abort(msg)
1249 1236
1250 1237 wlock = lock = None
1251 1238 try:
1252 1239 wlock = pullop.repo.wlock()
1253 1240 lock = pullop.repo.lock()
1254 1241 pullop.trmanager = transactionmanager(repo, 'pull', remote.url())
1255 1242 streamclone.maybeperformlegacystreamclone(pullop)
1256 1243 # This should ideally be in _pullbundle2(). However, it needs to run
1257 1244 # before discovery to avoid extra work.
1258 1245 _maybeapplyclonebundle(pullop)
1259 1246 _pulldiscovery(pullop)
1260 1247 if pullop.canusebundle2:
1261 1248 _pullbundle2(pullop)
1262 1249 _pullchangeset(pullop)
1263 1250 _pullphase(pullop)
1264 1251 _pullbookmarks(pullop)
1265 1252 _pullobsolete(pullop)
1266 1253 pullop.trmanager.close()
1267 1254 finally:
1268 1255 lockmod.release(pullop.trmanager, lock, wlock)
1269 1256
1270 1257 return pullop
1271 1258
1272 1259 # list of steps to perform discovery before pull
1273 1260 pulldiscoveryorder = []
1274 1261
1275 1262 # Mapping between step name and function
1276 1263 #
1277 1264 # This exists to help extensions wrap steps if necessary
1278 1265 pulldiscoverymapping = {}
1279 1266
1280 1267 def pulldiscovery(stepname):
1281 1268 """decorator for function performing discovery before pull
1282 1269
1283 1270 The function is added to the step -> function mapping and appended to the
1284 1271 list of steps. Beware that decorated function will be added in order (this
1285 1272 may matter).
1286 1273
1287 1274 You can only use this decorator for a new step, if you want to wrap a step
1288 1275 from an extension, change the pulldiscovery dictionary directly."""
1289 1276 def dec(func):
1290 1277 assert stepname not in pulldiscoverymapping
1291 1278 pulldiscoverymapping[stepname] = func
1292 1279 pulldiscoveryorder.append(stepname)
1293 1280 return func
1294 1281 return dec
1295 1282
1296 1283 def _pulldiscovery(pullop):
1297 1284 """Run all discovery steps"""
1298 1285 for stepname in pulldiscoveryorder:
1299 1286 step = pulldiscoverymapping[stepname]
1300 1287 step(pullop)
1301 1288
1302 1289 @pulldiscovery('b1:bookmarks')
1303 1290 def _pullbookmarkbundle1(pullop):
1304 1291 """fetch bookmark data in bundle1 case
1305 1292
1306 1293 If not using bundle2, we have to fetch bookmarks before changeset
1307 1294 discovery to reduce the chance and impact of race conditions."""
1308 1295 if pullop.remotebookmarks is not None:
1309 1296 return
1310 1297 if pullop.canusebundle2 and 'listkeys' in pullop.remotebundle2caps:
1311 1298 # all known bundle2 servers now support listkeys, but lets be nice with
1312 1299 # new implementation.
1313 1300 return
1314 1301 pullop.remotebookmarks = pullop.remote.listkeys('bookmarks')
1315 1302
1316 1303
1317 1304 @pulldiscovery('changegroup')
1318 1305 def _pulldiscoverychangegroup(pullop):
1319 1306 """discovery phase for the pull
1320 1307
1321 1308 Current handle changeset discovery only, will change handle all discovery
1322 1309 at some point."""
1323 1310 tmp = discovery.findcommonincoming(pullop.repo,
1324 1311 pullop.remote,
1325 1312 heads=pullop.heads,
1326 1313 force=pullop.force)
1327 1314 common, fetch, rheads = tmp
1328 1315 nm = pullop.repo.unfiltered().changelog.nodemap
1329 1316 if fetch and rheads:
1330 1317 # If a remote heads in filtered locally, lets drop it from the unknown
1331 1318 # remote heads and put in back in common.
1332 1319 #
1333 1320 # This is a hackish solution to catch most of "common but locally
1334 1321 # hidden situation". We do not performs discovery on unfiltered
1335 1322 # repository because it end up doing a pathological amount of round
1336 1323 # trip for w huge amount of changeset we do not care about.
1337 1324 #
1338 1325 # If a set of such "common but filtered" changeset exist on the server
1339 1326 # but are not including a remote heads, we'll not be able to detect it,
1340 1327 scommon = set(common)
1341 1328 filteredrheads = []
1342 1329 for n in rheads:
1343 1330 if n in nm:
1344 1331 if n not in scommon:
1345 1332 common.append(n)
1346 1333 else:
1347 1334 filteredrheads.append(n)
1348 1335 if not filteredrheads:
1349 1336 fetch = []
1350 1337 rheads = filteredrheads
1351 1338 pullop.common = common
1352 1339 pullop.fetch = fetch
1353 1340 pullop.rheads = rheads
1354 1341
1355 1342 def _pullbundle2(pullop):
1356 1343 """pull data using bundle2
1357 1344
1358 1345 For now, the only supported data are changegroup."""
1359 1346 kwargs = {'bundlecaps': caps20to10(pullop.repo)}
1360 1347
1361 1348 # At the moment we don't do stream clones over bundle2. If that is
1362 1349 # implemented then here's where the check for that will go.
1363 1350 streaming = False
1364 1351
1365 1352 # pulling changegroup
1366 1353 pullop.stepsdone.add('changegroup')
1367 1354
1368 1355 kwargs['common'] = pullop.common
1369 1356 kwargs['heads'] = pullop.heads or pullop.rheads
1370 1357 kwargs['cg'] = pullop.fetch
1371 1358 if 'listkeys' in pullop.remotebundle2caps:
1372 1359 kwargs['listkeys'] = ['phases']
1373 1360 if pullop.remotebookmarks is None:
1374 1361 # make sure to always includes bookmark data when migrating
1375 1362 # `hg incoming --bundle` to using this function.
1376 1363 kwargs['listkeys'].append('bookmarks')
1377 1364
1378 1365 # If this is a full pull / clone and the server supports the clone bundles
1379 1366 # feature, tell the server whether we attempted a clone bundle. The
1380 1367 # presence of this flag indicates the client supports clone bundles. This
1381 1368 # will enable the server to treat clients that support clone bundles
1382 1369 # differently from those that don't.
1383 1370 if (pullop.remote.capable('clonebundles')
1384 1371 and pullop.heads is None and list(pullop.common) == [nullid]):
1385 1372 kwargs['cbattempted'] = pullop.clonebundleattempted
1386 1373
1387 1374 if streaming:
1388 1375 pullop.repo.ui.status(_('streaming all changes\n'))
1389 1376 elif not pullop.fetch:
1390 1377 pullop.repo.ui.status(_("no changes found\n"))
1391 1378 pullop.cgresult = 0
1392 1379 else:
1393 1380 if pullop.heads is None and list(pullop.common) == [nullid]:
1394 1381 pullop.repo.ui.status(_("requesting all changes\n"))
1395 1382 if obsolete.isenabled(pullop.repo, obsolete.exchangeopt):
1396 1383 remoteversions = bundle2.obsmarkersversion(pullop.remotebundle2caps)
1397 1384 if obsolete.commonversion(remoteversions) is not None:
1398 1385 kwargs['obsmarkers'] = True
1399 1386 pullop.stepsdone.add('obsmarkers')
1400 1387 _pullbundle2extraprepare(pullop, kwargs)
1401 1388 bundle = pullop.remote.getbundle('pull', **pycompat.strkwargs(kwargs))
1402 1389 try:
1403 1390 op = bundle2.processbundle(pullop.repo, bundle, pullop.gettransaction)
1404 1391 except bundle2.AbortFromPart as exc:
1405 1392 pullop.repo.ui.status(_('remote: abort: %s\n') % exc)
1406 1393 raise error.Abort(_('pull failed on remote'), hint=exc.hint)
1407 1394 except error.BundleValueError as exc:
1408 1395 raise error.Abort(_('missing support for %s') % exc)
1409 1396
1410 1397 if pullop.fetch:
1411 1398 pullop.cgresult = bundle2.combinechangegroupresults(op)
1412 1399
1413 1400 # processing phases change
1414 1401 for namespace, value in op.records['listkeys']:
1415 1402 if namespace == 'phases':
1416 1403 _pullapplyphases(pullop, value)
1417 1404
1418 1405 # processing bookmark update
1419 1406 for namespace, value in op.records['listkeys']:
1420 1407 if namespace == 'bookmarks':
1421 1408 pullop.remotebookmarks = value
1422 1409
1423 1410 # bookmark data were either already there or pulled in the bundle
1424 1411 if pullop.remotebookmarks is not None:
1425 1412 _pullbookmarks(pullop)
1426 1413
1427 1414 def _pullbundle2extraprepare(pullop, kwargs):
1428 1415 """hook function so that extensions can extend the getbundle call"""
1429 1416 pass
1430 1417
1431 1418 def _pullchangeset(pullop):
1432 1419 """pull changeset from unbundle into the local repo"""
1433 1420 # We delay the open of the transaction as late as possible so we
1434 1421 # don't open transaction for nothing or you break future useful
1435 1422 # rollback call
1436 1423 if 'changegroup' in pullop.stepsdone:
1437 1424 return
1438 1425 pullop.stepsdone.add('changegroup')
1439 1426 if not pullop.fetch:
1440 1427 pullop.repo.ui.status(_("no changes found\n"))
1441 1428 pullop.cgresult = 0
1442 1429 return
1443 1430 tr = pullop.gettransaction()
1444 1431 if pullop.heads is None and list(pullop.common) == [nullid]:
1445 1432 pullop.repo.ui.status(_("requesting all changes\n"))
1446 1433 elif pullop.heads is None and pullop.remote.capable('changegroupsubset'):
1447 1434 # issue1320, avoid a race if remote changed after discovery
1448 1435 pullop.heads = pullop.rheads
1449 1436
1450 1437 if pullop.remote.capable('getbundle'):
1451 1438 # TODO: get bundlecaps from remote
1452 1439 cg = pullop.remote.getbundle('pull', common=pullop.common,
1453 1440 heads=pullop.heads or pullop.rheads)
1454 1441 elif pullop.heads is None:
1455 1442 cg = pullop.remote.changegroup(pullop.fetch, 'pull')
1456 1443 elif not pullop.remote.capable('changegroupsubset'):
1457 1444 raise error.Abort(_("partial pull cannot be done because "
1458 1445 "other repository doesn't support "
1459 1446 "changegroupsubset."))
1460 1447 else:
1461 1448 cg = pullop.remote.changegroupsubset(pullop.fetch, pullop.heads, 'pull')
1462 1449 bundleop = bundle2.applybundle(pullop.repo, cg, tr, 'pull',
1463 1450 pullop.remote.url())
1464 1451 pullop.cgresult = bundle2.combinechangegroupresults(bundleop)
1465 1452
1466 1453 def _pullphase(pullop):
1467 1454 # Get remote phases data from remote
1468 1455 if 'phases' in pullop.stepsdone:
1469 1456 return
1470 1457 remotephases = pullop.remote.listkeys('phases')
1471 1458 _pullapplyphases(pullop, remotephases)
1472 1459
1473 1460 def _pullapplyphases(pullop, remotephases):
1474 1461 """apply phase movement from observed remote state"""
1475 1462 if 'phases' in pullop.stepsdone:
1476 1463 return
1477 1464 pullop.stepsdone.add('phases')
1478 1465 publishing = bool(remotephases.get('publishing', False))
1479 1466 if remotephases and not publishing:
1480 1467 # remote is new and non-publishing
1481 1468 pheads, _dr = phases.analyzeremotephases(pullop.repo,
1482 1469 pullop.pulledsubset,
1483 1470 remotephases)
1484 1471 dheads = pullop.pulledsubset
1485 1472 else:
1486 1473 # Remote is old or publishing all common changesets
1487 1474 # should be seen as public
1488 1475 pheads = pullop.pulledsubset
1489 1476 dheads = []
1490 1477 unfi = pullop.repo.unfiltered()
1491 1478 phase = unfi._phasecache.phase
1492 1479 rev = unfi.changelog.nodemap.get
1493 1480 public = phases.public
1494 1481 draft = phases.draft
1495 1482
1496 1483 # exclude changesets already public locally and update the others
1497 1484 pheads = [pn for pn in pheads if phase(unfi, rev(pn)) > public]
1498 1485 if pheads:
1499 1486 tr = pullop.gettransaction()
1500 1487 phases.advanceboundary(pullop.repo, tr, public, pheads)
1501 1488
1502 1489 # exclude changesets already draft locally and update the others
1503 1490 dheads = [pn for pn in dheads if phase(unfi, rev(pn)) > draft]
1504 1491 if dheads:
1505 1492 tr = pullop.gettransaction()
1506 1493 phases.advanceboundary(pullop.repo, tr, draft, dheads)
1507 1494
1508 1495 def _pullbookmarks(pullop):
1509 1496 """process the remote bookmark information to update the local one"""
1510 1497 if 'bookmarks' in pullop.stepsdone:
1511 1498 return
1512 1499 pullop.stepsdone.add('bookmarks')
1513 1500 repo = pullop.repo
1514 1501 remotebookmarks = pullop.remotebookmarks
1515 1502 remotebookmarks = bookmod.unhexlifybookmarks(remotebookmarks)
1516 1503 bookmod.updatefromremote(repo.ui, repo, remotebookmarks,
1517 1504 pullop.remote.url(),
1518 1505 pullop.gettransaction,
1519 1506 explicit=pullop.explicitbookmarks)
1520 1507
1521 1508 def _pullobsolete(pullop):
1522 1509 """utility function to pull obsolete markers from a remote
1523 1510
1524 1511 The `gettransaction` is function that return the pull transaction, creating
1525 1512 one if necessary. We return the transaction to inform the calling code that
1526 1513 a new transaction have been created (when applicable).
1527 1514
1528 1515 Exists mostly to allow overriding for experimentation purpose"""
1529 1516 if 'obsmarkers' in pullop.stepsdone:
1530 1517 return
1531 1518 pullop.stepsdone.add('obsmarkers')
1532 1519 tr = None
1533 1520 if obsolete.isenabled(pullop.repo, obsolete.exchangeopt):
1534 1521 pullop.repo.ui.debug('fetching remote obsolete markers\n')
1535 1522 remoteobs = pullop.remote.listkeys('obsolete')
1536 1523 if 'dump0' in remoteobs:
1537 1524 tr = pullop.gettransaction()
1538 1525 markers = []
1539 1526 for key in sorted(remoteobs, reverse=True):
1540 1527 if key.startswith('dump'):
1541 1528 data = util.b85decode(remoteobs[key])
1542 1529 version, newmarks = obsolete._readmarkers(data)
1543 1530 markers += newmarks
1544 1531 if markers:
1545 1532 pullop.repo.obsstore.add(tr, markers)
1546 1533 pullop.repo.invalidatevolatilesets()
1547 1534 return tr
1548 1535
1549 1536 def caps20to10(repo):
1550 1537 """return a set with appropriate options to use bundle20 during getbundle"""
1551 1538 caps = {'HG20'}
1552 1539 capsblob = bundle2.encodecaps(bundle2.getrepocaps(repo))
1553 1540 caps.add('bundle2=' + urlreq.quote(capsblob))
1554 1541 return caps
1555 1542
1556 1543 # List of names of steps to perform for a bundle2 for getbundle, order matters.
1557 1544 getbundle2partsorder = []
1558 1545
1559 1546 # Mapping between step name and function
1560 1547 #
1561 1548 # This exists to help extensions wrap steps if necessary
1562 1549 getbundle2partsmapping = {}
1563 1550
1564 1551 def getbundle2partsgenerator(stepname, idx=None):
1565 1552 """decorator for function generating bundle2 part for getbundle
1566 1553
1567 1554 The function is added to the step -> function mapping and appended to the
1568 1555 list of steps. Beware that decorated functions will be added in order
1569 1556 (this may matter).
1570 1557
1571 1558 You can only use this decorator for new steps, if you want to wrap a step
1572 1559 from an extension, attack the getbundle2partsmapping dictionary directly."""
1573 1560 def dec(func):
1574 1561 assert stepname not in getbundle2partsmapping
1575 1562 getbundle2partsmapping[stepname] = func
1576 1563 if idx is None:
1577 1564 getbundle2partsorder.append(stepname)
1578 1565 else:
1579 1566 getbundle2partsorder.insert(idx, stepname)
1580 1567 return func
1581 1568 return dec
1582 1569
1583 1570 def bundle2requested(bundlecaps):
1584 1571 if bundlecaps is not None:
1585 1572 return any(cap.startswith('HG2') for cap in bundlecaps)
1586 1573 return False
1587 1574
1588 1575 def getbundlechunks(repo, source, heads=None, common=None, bundlecaps=None,
1589 1576 **kwargs):
1590 1577 """Return chunks constituting a bundle's raw data.
1591 1578
1592 1579 Could be a bundle HG10 or a bundle HG20 depending on bundlecaps
1593 1580 passed.
1594 1581
1595 1582 Returns an iterator over raw chunks (of varying sizes).
1596 1583 """
1597 1584 kwargs = pycompat.byteskwargs(kwargs)
1598 1585 usebundle2 = bundle2requested(bundlecaps)
1599 1586 # bundle10 case
1600 1587 if not usebundle2:
1601 1588 if bundlecaps and not kwargs.get('cg', True):
1602 1589 raise ValueError(_('request for bundle10 must include changegroup'))
1603 1590
1604 1591 if kwargs:
1605 1592 raise ValueError(_('unsupported getbundle arguments: %s')
1606 1593 % ', '.join(sorted(kwargs.keys())))
1607 1594 outgoing = _computeoutgoing(repo, heads, common)
1608 1595 bundler = changegroup.getbundler('01', repo, bundlecaps)
1609 1596 return changegroup.getsubsetraw(repo, outgoing, bundler, source)
1610 1597
1611 1598 # bundle20 case
1612 1599 b2caps = {}
1613 1600 for bcaps in bundlecaps:
1614 1601 if bcaps.startswith('bundle2='):
1615 1602 blob = urlreq.unquote(bcaps[len('bundle2='):])
1616 1603 b2caps.update(bundle2.decodecaps(blob))
1617 1604 bundler = bundle2.bundle20(repo.ui, b2caps)
1618 1605
1619 1606 kwargs['heads'] = heads
1620 1607 kwargs['common'] = common
1621 1608
1622 1609 for name in getbundle2partsorder:
1623 1610 func = getbundle2partsmapping[name]
1624 1611 func(bundler, repo, source, bundlecaps=bundlecaps, b2caps=b2caps,
1625 1612 **pycompat.strkwargs(kwargs))
1626 1613
1627 1614 return bundler.getchunks()
1628 1615
1629 1616 @getbundle2partsgenerator('changegroup')
1630 1617 def _getbundlechangegrouppart(bundler, repo, source, bundlecaps=None,
1631 1618 b2caps=None, heads=None, common=None, **kwargs):
1632 1619 """add a changegroup part to the requested bundle"""
1633 1620 cg = None
1634 1621 if kwargs.get('cg', True):
1635 1622 # build changegroup bundle here.
1636 1623 version = '01'
1637 1624 cgversions = b2caps.get('changegroup')
1638 1625 if cgversions: # 3.1 and 3.2 ship with an empty value
1639 1626 cgversions = [v for v in cgversions
1640 1627 if v in changegroup.supportedoutgoingversions(repo)]
1641 1628 if not cgversions:
1642 1629 raise ValueError(_('no common changegroup version'))
1643 1630 version = max(cgversions)
1644 1631 outgoing = _computeoutgoing(repo, heads, common)
1645 1632 cg = changegroup.getlocalchangegroupraw(repo, source, outgoing,
1646 1633 bundlecaps=bundlecaps,
1647 1634 version=version)
1648 1635
1649 1636 if cg:
1650 1637 part = bundler.newpart('changegroup', data=cg)
1651 1638 if cgversions:
1652 1639 part.addparam('version', version)
1653 1640 part.addparam('nbchanges', str(len(outgoing.missing)), mandatory=False)
1654 1641 if 'treemanifest' in repo.requirements:
1655 1642 part.addparam('treemanifest', '1')
1656 1643
1657 1644 @getbundle2partsgenerator('listkeys')
1658 1645 def _getbundlelistkeysparts(bundler, repo, source, bundlecaps=None,
1659 1646 b2caps=None, **kwargs):
1660 1647 """add parts containing listkeys namespaces to the requested bundle"""
1661 1648 listkeys = kwargs.get('listkeys', ())
1662 1649 for namespace in listkeys:
1663 1650 part = bundler.newpart('listkeys')
1664 1651 part.addparam('namespace', namespace)
1665 1652 keys = repo.listkeys(namespace).items()
1666 1653 part.data = pushkey.encodekeys(keys)
1667 1654
1668 1655 @getbundle2partsgenerator('obsmarkers')
1669 1656 def _getbundleobsmarkerpart(bundler, repo, source, bundlecaps=None,
1670 1657 b2caps=None, heads=None, **kwargs):
1671 1658 """add an obsolescence markers part to the requested bundle"""
1672 1659 if kwargs.get('obsmarkers', False):
1673 1660 if heads is None:
1674 1661 heads = repo.heads()
1675 1662 subset = [c.node() for c in repo.set('::%ln', heads)]
1676 1663 markers = repo.obsstore.relevantmarkers(subset)
1677 1664 markers = sorted(markers)
1678 1665 bundle2.buildobsmarkerspart(bundler, markers)
1679 1666
1680 1667 @getbundle2partsgenerator('hgtagsfnodes')
1681 1668 def _getbundletagsfnodes(bundler, repo, source, bundlecaps=None,
1682 1669 b2caps=None, heads=None, common=None,
1683 1670 **kwargs):
1684 1671 """Transfer the .hgtags filenodes mapping.
1685 1672
1686 1673 Only values for heads in this bundle will be transferred.
1687 1674
1688 1675 The part data consists of pairs of 20 byte changeset node and .hgtags
1689 1676 filenodes raw values.
1690 1677 """
1691 1678 # Don't send unless:
1692 1679 # - changeset are being exchanged,
1693 1680 # - the client supports it.
1694 1681 if not (kwargs.get('cg', True) and 'hgtagsfnodes' in b2caps):
1695 1682 return
1696 1683
1697 1684 outgoing = _computeoutgoing(repo, heads, common)
1698 1685 bundle2.addparttagsfnodescache(repo, bundler, outgoing)
1699 1686
1700 1687 def _getbookmarks(repo, **kwargs):
1701 1688 """Returns bookmark to node mapping.
1702 1689
1703 1690 This function is primarily used to generate `bookmarks` bundle2 part.
1704 1691 It is a separate function in order to make it easy to wrap it
1705 1692 in extensions. Passing `kwargs` to the function makes it easy to
1706 1693 add new parameters in extensions.
1707 1694 """
1708 1695
1709 1696 return dict(bookmod.listbinbookmarks(repo))
1710 1697
1711 1698 def check_heads(repo, their_heads, context):
1712 1699 """check if the heads of a repo have been modified
1713 1700
1714 1701 Used by peer for unbundling.
1715 1702 """
1716 1703 heads = repo.heads()
1717 1704 heads_hash = hashlib.sha1(''.join(sorted(heads))).digest()
1718 1705 if not (their_heads == ['force'] or their_heads == heads or
1719 1706 their_heads == ['hashed', heads_hash]):
1720 1707 # someone else committed/pushed/unbundled while we
1721 1708 # were transferring data
1722 1709 raise error.PushRaced('repository changed while %s - '
1723 1710 'please try again' % context)
1724 1711
1725 1712 def unbundle(repo, cg, heads, source, url):
1726 1713 """Apply a bundle to a repo.
1727 1714
1728 1715 this function makes sure the repo is locked during the application and have
1729 1716 mechanism to check that no push race occurred between the creation of the
1730 1717 bundle and its application.
1731 1718
1732 1719 If the push was raced as PushRaced exception is raised."""
1733 1720 r = 0
1734 1721 # need a transaction when processing a bundle2 stream
1735 1722 # [wlock, lock, tr] - needs to be an array so nested functions can modify it
1736 1723 lockandtr = [None, None, None]
1737 1724 recordout = None
1738 1725 # quick fix for output mismatch with bundle2 in 3.4
1739 1726 captureoutput = repo.ui.configbool('experimental', 'bundle2-output-capture')
1740 1727 if url.startswith('remote:http:') or url.startswith('remote:https:'):
1741 1728 captureoutput = True
1742 1729 try:
1743 1730 # note: outside bundle1, 'heads' is expected to be empty and this
1744 1731 # 'check_heads' call wil be a no-op
1745 1732 check_heads(repo, heads, 'uploading changes')
1746 1733 # push can proceed
1747 1734 if not isinstance(cg, bundle2.unbundle20):
1748 1735 # legacy case: bundle1 (changegroup 01)
1749 1736 txnname = "\n".join([source, util.hidepassword(url)])
1750 1737 with repo.lock(), repo.transaction(txnname) as tr:
1751 1738 op = bundle2.applybundle(repo, cg, tr, source, url)
1752 1739 r = bundle2.combinechangegroupresults(op)
1753 1740 else:
1754 1741 r = None
1755 1742 try:
1756 1743 def gettransaction():
1757 1744 if not lockandtr[2]:
1758 1745 lockandtr[0] = repo.wlock()
1759 1746 lockandtr[1] = repo.lock()
1760 1747 lockandtr[2] = repo.transaction(source)
1761 1748 lockandtr[2].hookargs['source'] = source
1762 1749 lockandtr[2].hookargs['url'] = url
1763 1750 lockandtr[2].hookargs['bundle2'] = '1'
1764 1751 return lockandtr[2]
1765 1752
1766 1753 # Do greedy locking by default until we're satisfied with lazy
1767 1754 # locking.
1768 1755 if not repo.ui.configbool('experimental', 'bundle2lazylocking'):
1769 1756 gettransaction()
1770 1757
1771 1758 op = bundle2.bundleoperation(repo, gettransaction,
1772 1759 captureoutput=captureoutput)
1773 1760 try:
1774 1761 op = bundle2.processbundle(repo, cg, op=op)
1775 1762 finally:
1776 1763 r = op.reply
1777 1764 if captureoutput and r is not None:
1778 1765 repo.ui.pushbuffer(error=True, subproc=True)
1779 1766 def recordout(output):
1780 1767 r.newpart('output', data=output, mandatory=False)
1781 1768 if lockandtr[2] is not None:
1782 1769 lockandtr[2].close()
1783 1770 except BaseException as exc:
1784 1771 exc.duringunbundle2 = True
1785 1772 if captureoutput and r is not None:
1786 1773 parts = exc._bundle2salvagedoutput = r.salvageoutput()
1787 1774 def recordout(output):
1788 1775 part = bundle2.bundlepart('output', data=output,
1789 1776 mandatory=False)
1790 1777 parts.append(part)
1791 1778 raise
1792 1779 finally:
1793 1780 lockmod.release(lockandtr[2], lockandtr[1], lockandtr[0])
1794 1781 if recordout is not None:
1795 1782 recordout(repo.ui.popbuffer())
1796 1783 return r
1797 1784
1798 1785 def _maybeapplyclonebundle(pullop):
1799 1786 """Apply a clone bundle from a remote, if possible."""
1800 1787
1801 1788 repo = pullop.repo
1802 1789 remote = pullop.remote
1803 1790
1804 1791 if not repo.ui.configbool('ui', 'clonebundles'):
1805 1792 return
1806 1793
1807 1794 # Only run if local repo is empty.
1808 1795 if len(repo):
1809 1796 return
1810 1797
1811 1798 if pullop.heads:
1812 1799 return
1813 1800
1814 1801 if not remote.capable('clonebundles'):
1815 1802 return
1816 1803
1817 1804 res = remote._call('clonebundles')
1818 1805
1819 1806 # If we call the wire protocol command, that's good enough to record the
1820 1807 # attempt.
1821 1808 pullop.clonebundleattempted = True
1822 1809
1823 1810 entries = parseclonebundlesmanifest(repo, res)
1824 1811 if not entries:
1825 1812 repo.ui.note(_('no clone bundles available on remote; '
1826 1813 'falling back to regular clone\n'))
1827 1814 return
1828 1815
1829 1816 entries = filterclonebundleentries(repo, entries)
1830 1817 if not entries:
1831 1818 # There is a thundering herd concern here. However, if a server
1832 1819 # operator doesn't advertise bundles appropriate for its clients,
1833 1820 # they deserve what's coming. Furthermore, from a client's
1834 1821 # perspective, no automatic fallback would mean not being able to
1835 1822 # clone!
1836 1823 repo.ui.warn(_('no compatible clone bundles available on server; '
1837 1824 'falling back to regular clone\n'))
1838 1825 repo.ui.warn(_('(you may want to report this to the server '
1839 1826 'operator)\n'))
1840 1827 return
1841 1828
1842 1829 entries = sortclonebundleentries(repo.ui, entries)
1843 1830
1844 1831 url = entries[0]['URL']
1845 1832 repo.ui.status(_('applying clone bundle from %s\n') % url)
1846 1833 if trypullbundlefromurl(repo.ui, repo, url):
1847 1834 repo.ui.status(_('finished applying clone bundle\n'))
1848 1835 # Bundle failed.
1849 1836 #
1850 1837 # We abort by default to avoid the thundering herd of
1851 1838 # clients flooding a server that was expecting expensive
1852 1839 # clone load to be offloaded.
1853 1840 elif repo.ui.configbool('ui', 'clonebundlefallback'):
1854 1841 repo.ui.warn(_('falling back to normal clone\n'))
1855 1842 else:
1856 1843 raise error.Abort(_('error applying bundle'),
1857 1844 hint=_('if this error persists, consider contacting '
1858 1845 'the server operator or disable clone '
1859 1846 'bundles via '
1860 1847 '"--config ui.clonebundles=false"'))
1861 1848
1862 1849 def parseclonebundlesmanifest(repo, s):
1863 1850 """Parses the raw text of a clone bundles manifest.
1864 1851
1865 1852 Returns a list of dicts. The dicts have a ``URL`` key corresponding
1866 1853 to the URL and other keys are the attributes for the entry.
1867 1854 """
1868 1855 m = []
1869 1856 for line in s.splitlines():
1870 1857 fields = line.split()
1871 1858 if not fields:
1872 1859 continue
1873 1860 attrs = {'URL': fields[0]}
1874 1861 for rawattr in fields[1:]:
1875 1862 key, value = rawattr.split('=', 1)
1876 1863 key = urlreq.unquote(key)
1877 1864 value = urlreq.unquote(value)
1878 1865 attrs[key] = value
1879 1866
1880 1867 # Parse BUNDLESPEC into components. This makes client-side
1881 1868 # preferences easier to specify since you can prefer a single
1882 1869 # component of the BUNDLESPEC.
1883 1870 if key == 'BUNDLESPEC':
1884 1871 try:
1885 1872 comp, version, params = parsebundlespec(repo, value,
1886 1873 externalnames=True)
1887 1874 attrs['COMPRESSION'] = comp
1888 1875 attrs['VERSION'] = version
1889 1876 except error.InvalidBundleSpecification:
1890 1877 pass
1891 1878 except error.UnsupportedBundleSpecification:
1892 1879 pass
1893 1880
1894 1881 m.append(attrs)
1895 1882
1896 1883 return m
1897 1884
1898 1885 def filterclonebundleentries(repo, entries):
1899 1886 """Remove incompatible clone bundle manifest entries.
1900 1887
1901 1888 Accepts a list of entries parsed with ``parseclonebundlesmanifest``
1902 1889 and returns a new list consisting of only the entries that this client
1903 1890 should be able to apply.
1904 1891
1905 1892 There is no guarantee we'll be able to apply all returned entries because
1906 1893 the metadata we use to filter on may be missing or wrong.
1907 1894 """
1908 1895 newentries = []
1909 1896 for entry in entries:
1910 1897 spec = entry.get('BUNDLESPEC')
1911 1898 if spec:
1912 1899 try:
1913 1900 parsebundlespec(repo, spec, strict=True)
1914 1901 except error.InvalidBundleSpecification as e:
1915 1902 repo.ui.debug(str(e) + '\n')
1916 1903 continue
1917 1904 except error.UnsupportedBundleSpecification as e:
1918 1905 repo.ui.debug('filtering %s because unsupported bundle '
1919 1906 'spec: %s\n' % (entry['URL'], str(e)))
1920 1907 continue
1921 1908
1922 1909 if 'REQUIRESNI' in entry and not sslutil.hassni:
1923 1910 repo.ui.debug('filtering %s because SNI not supported\n' %
1924 1911 entry['URL'])
1925 1912 continue
1926 1913
1927 1914 newentries.append(entry)
1928 1915
1929 1916 return newentries
1930 1917
1931 1918 class clonebundleentry(object):
1932 1919 """Represents an item in a clone bundles manifest.
1933 1920
1934 1921 This rich class is needed to support sorting since sorted() in Python 3
1935 1922 doesn't support ``cmp`` and our comparison is complex enough that ``key=``
1936 1923 won't work.
1937 1924 """
1938 1925
1939 1926 def __init__(self, value, prefers):
1940 1927 self.value = value
1941 1928 self.prefers = prefers
1942 1929
1943 1930 def _cmp(self, other):
1944 1931 for prefkey, prefvalue in self.prefers:
1945 1932 avalue = self.value.get(prefkey)
1946 1933 bvalue = other.value.get(prefkey)
1947 1934
1948 1935 # Special case for b missing attribute and a matches exactly.
1949 1936 if avalue is not None and bvalue is None and avalue == prefvalue:
1950 1937 return -1
1951 1938
1952 1939 # Special case for a missing attribute and b matches exactly.
1953 1940 if bvalue is not None and avalue is None and bvalue == prefvalue:
1954 1941 return 1
1955 1942
1956 1943 # We can't compare unless attribute present on both.
1957 1944 if avalue is None or bvalue is None:
1958 1945 continue
1959 1946
1960 1947 # Same values should fall back to next attribute.
1961 1948 if avalue == bvalue:
1962 1949 continue
1963 1950
1964 1951 # Exact matches come first.
1965 1952 if avalue == prefvalue:
1966 1953 return -1
1967 1954 if bvalue == prefvalue:
1968 1955 return 1
1969 1956
1970 1957 # Fall back to next attribute.
1971 1958 continue
1972 1959
1973 1960 # If we got here we couldn't sort by attributes and prefers. Fall
1974 1961 # back to index order.
1975 1962 return 0
1976 1963
1977 1964 def __lt__(self, other):
1978 1965 return self._cmp(other) < 0
1979 1966
1980 1967 def __gt__(self, other):
1981 1968 return self._cmp(other) > 0
1982 1969
1983 1970 def __eq__(self, other):
1984 1971 return self._cmp(other) == 0
1985 1972
1986 1973 def __le__(self, other):
1987 1974 return self._cmp(other) <= 0
1988 1975
1989 1976 def __ge__(self, other):
1990 1977 return self._cmp(other) >= 0
1991 1978
1992 1979 def __ne__(self, other):
1993 1980 return self._cmp(other) != 0
1994 1981
1995 1982 def sortclonebundleentries(ui, entries):
1996 1983 prefers = ui.configlist('ui', 'clonebundleprefers')
1997 1984 if not prefers:
1998 1985 return list(entries)
1999 1986
2000 1987 prefers = [p.split('=', 1) for p in prefers]
2001 1988
2002 1989 items = sorted(clonebundleentry(v, prefers) for v in entries)
2003 1990 return [i.value for i in items]
2004 1991
2005 1992 def trypullbundlefromurl(ui, repo, url):
2006 1993 """Attempt to apply a bundle from a URL."""
2007 1994 with repo.lock(), repo.transaction('bundleurl') as tr:
2008 1995 try:
2009 1996 fh = urlmod.open(ui, url)
2010 1997 cg = readbundle(ui, fh, 'stream')
2011 1998
2012 1999 if isinstance(cg, streamclone.streamcloneapplier):
2013 2000 cg.apply(repo)
2014 2001 else:
2015 2002 bundle2.applybundle(repo, cg, tr, 'clonebundles', url)
2016 2003 return True
2017 2004 except urlerr.httperror as e:
2018 2005 ui.warn(_('HTTP error fetching bundle: %s\n') % str(e))
2019 2006 except urlerr.urlerror as e:
2020 2007 ui.warn(_('error fetching bundle: %s\n') % e.reason)
2021 2008
2022 2009 return False
@@ -1,402 +1,399 b''
1 1 # httppeer.py - HTTP repository proxy classes for mercurial
2 2 #
3 3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from __future__ import absolute_import
10 10
11 11 import errno
12 12 import os
13 13 import socket
14 14 import struct
15 15 import tempfile
16 16
17 17 from .i18n import _
18 18 from .node import nullid
19 19 from . import (
20 20 bundle2,
21 21 error,
22 22 httpconnection,
23 23 pycompat,
24 24 statichttprepo,
25 25 url,
26 26 util,
27 27 wireproto,
28 28 )
29 29
30 30 httplib = util.httplib
31 31 urlerr = util.urlerr
32 32 urlreq = util.urlreq
33 33
34 34 def encodevalueinheaders(value, header, limit):
35 35 """Encode a string value into multiple HTTP headers.
36 36
37 37 ``value`` will be encoded into 1 or more HTTP headers with the names
38 38 ``header-<N>`` where ``<N>`` is an integer starting at 1. Each header
39 39 name + value will be at most ``limit`` bytes long.
40 40
41 41 Returns an iterable of 2-tuples consisting of header names and values.
42 42 """
43 43 fmt = header + '-%s'
44 44 valuelen = limit - len(fmt % '000') - len(': \r\n')
45 45 result = []
46 46
47 47 n = 0
48 48 for i in xrange(0, len(value), valuelen):
49 49 n += 1
50 50 result.append((fmt % str(n), value[i:i + valuelen]))
51 51
52 52 return result
53 53
54 54 def _wraphttpresponse(resp):
55 55 """Wrap an HTTPResponse with common error handlers.
56 56
57 57 This ensures that any I/O from any consumer raises the appropriate
58 58 error and messaging.
59 59 """
60 60 origread = resp.read
61 61
62 62 class readerproxy(resp.__class__):
63 63 def read(self, size=None):
64 64 try:
65 65 return origread(size)
66 66 except httplib.IncompleteRead as e:
67 67 # e.expected is an integer if length known or None otherwise.
68 68 if e.expected:
69 69 msg = _('HTTP request error (incomplete response; '
70 70 'expected %d bytes got %d)') % (e.expected,
71 71 len(e.partial))
72 72 else:
73 73 msg = _('HTTP request error (incomplete response)')
74 74
75 75 raise error.PeerTransportError(
76 76 msg,
77 77 hint=_('this may be an intermittent network failure; '
78 78 'if the error persists, consider contacting the '
79 79 'network or server operator'))
80 80 except httplib.HTTPException as e:
81 81 raise error.PeerTransportError(
82 82 _('HTTP request error (%s)') % e,
83 83 hint=_('this may be an intermittent network failure; '
84 84 'if the error persists, consider contacting the '
85 85 'network or server operator'))
86 86
87 87 resp.__class__ = readerproxy
88 88
89 89 class httppeer(wireproto.wirepeer):
90 90 def __init__(self, ui, path):
91 91 self.path = path
92 92 self.caps = None
93 93 self.handler = None
94 94 self.urlopener = None
95 95 self.requestbuilder = None
96 96 u = util.url(path)
97 97 if u.query or u.fragment:
98 98 raise error.Abort(_('unsupported URL component: "%s"') %
99 99 (u.query or u.fragment))
100 100
101 101 # urllib cannot handle URLs with embedded user or passwd
102 102 self._url, authinfo = u.authinfo()
103 103
104 104 self.ui = ui
105 105 self.ui.debug('using %s\n' % self._url)
106 106
107 107 self.urlopener = url.opener(ui, authinfo)
108 108 self.requestbuilder = urlreq.request
109 109
110 110 def __del__(self):
111 111 urlopener = getattr(self, 'urlopener', None)
112 112 if urlopener:
113 113 for h in urlopener.handlers:
114 114 h.close()
115 115 getattr(h, "close_all", lambda : None)()
116 116
117 117 def url(self):
118 118 return self.path
119 119
120 120 # look up capabilities only when needed
121 121
122 122 def _fetchcaps(self):
123 123 self.caps = set(self._call('capabilities').split())
124 124
125 125 def _capabilities(self):
126 126 if self.caps is None:
127 127 try:
128 128 self._fetchcaps()
129 129 except error.RepoError:
130 130 self.caps = set()
131 131 self.ui.debug('capabilities: %s\n' %
132 132 (' '.join(self.caps or ['none'])))
133 133 return self.caps
134 134
135 def lock(self):
136 raise error.Abort(_('operation not supported over http'))
137
138 135 def _callstream(self, cmd, _compressible=False, **args):
139 136 if cmd == 'pushkey':
140 137 args['data'] = ''
141 138 data = args.pop('data', None)
142 139 headers = args.pop('headers', {})
143 140
144 141 self.ui.debug("sending %s command\n" % cmd)
145 142 q = [('cmd', cmd)]
146 143 headersize = 0
147 144 varyheaders = []
148 145 # Important: don't use self.capable() here or else you end up
149 146 # with infinite recursion when trying to look up capabilities
150 147 # for the first time.
151 148 postargsok = self.caps is not None and 'httppostargs' in self.caps
152 149 # TODO: support for httppostargs when data is a file-like
153 150 # object rather than a basestring
154 151 canmungedata = not data or isinstance(data, basestring)
155 152 if postargsok and canmungedata:
156 153 strargs = urlreq.urlencode(sorted(args.items()))
157 154 if strargs:
158 155 if not data:
159 156 data = strargs
160 157 elif isinstance(data, basestring):
161 158 data = strargs + data
162 159 headers['X-HgArgs-Post'] = len(strargs)
163 160 else:
164 161 if len(args) > 0:
165 162 httpheader = self.capable('httpheader')
166 163 if httpheader:
167 164 headersize = int(httpheader.split(',', 1)[0])
168 165 if headersize > 0:
169 166 # The headers can typically carry more data than the URL.
170 167 encargs = urlreq.urlencode(sorted(args.items()))
171 168 for header, value in encodevalueinheaders(encargs, 'X-HgArg',
172 169 headersize):
173 170 headers[header] = value
174 171 varyheaders.append(header)
175 172 else:
176 173 q += sorted(args.items())
177 174 qs = '?%s' % urlreq.urlencode(q)
178 175 cu = "%s%s" % (self._url, qs)
179 176 size = 0
180 177 if util.safehasattr(data, 'length'):
181 178 size = data.length
182 179 elif data is not None:
183 180 size = len(data)
184 181 if size and self.ui.configbool('ui', 'usehttp2'):
185 182 headers['Expect'] = '100-Continue'
186 183 headers['X-HgHttp2'] = '1'
187 184 if data is not None and 'Content-Type' not in headers:
188 185 headers['Content-Type'] = 'application/mercurial-0.1'
189 186
190 187 # Tell the server we accept application/mercurial-0.2 and multiple
191 188 # compression formats if the server is capable of emitting those
192 189 # payloads.
193 190 protoparams = []
194 191
195 192 mediatypes = set()
196 193 if self.caps is not None:
197 194 mt = self.capable('httpmediatype')
198 195 if mt:
199 196 protoparams.append('0.1')
200 197 mediatypes = set(mt.split(','))
201 198
202 199 if '0.2tx' in mediatypes:
203 200 protoparams.append('0.2')
204 201
205 202 if '0.2tx' in mediatypes and self.capable('compression'):
206 203 # We /could/ compare supported compression formats and prune
207 204 # non-mutually supported or error if nothing is mutually supported.
208 205 # For now, send the full list to the server and have it error.
209 206 comps = [e.wireprotosupport().name for e in
210 207 util.compengines.supportedwireengines(util.CLIENTROLE)]
211 208 protoparams.append('comp=%s' % ','.join(comps))
212 209
213 210 if protoparams:
214 211 protoheaders = encodevalueinheaders(' '.join(protoparams),
215 212 'X-HgProto',
216 213 headersize or 1024)
217 214 for header, value in protoheaders:
218 215 headers[header] = value
219 216 varyheaders.append(header)
220 217
221 218 if varyheaders:
222 219 headers['Vary'] = ','.join(varyheaders)
223 220
224 221 req = self.requestbuilder(cu, data, headers)
225 222
226 223 if data is not None:
227 224 self.ui.debug("sending %s bytes\n" % size)
228 225 req.add_unredirected_header('Content-Length', '%d' % size)
229 226 try:
230 227 resp = self.urlopener.open(req)
231 228 except urlerr.httperror as inst:
232 229 if inst.code == 401:
233 230 raise error.Abort(_('authorization failed'))
234 231 raise
235 232 except httplib.HTTPException as inst:
236 233 self.ui.debug('http error while sending %s command\n' % cmd)
237 234 self.ui.traceback()
238 235 raise IOError(None, inst)
239 236
240 237 # Insert error handlers for common I/O failures.
241 238 _wraphttpresponse(resp)
242 239
243 240 # record the url we got redirected to
244 241 resp_url = resp.geturl()
245 242 if resp_url.endswith(qs):
246 243 resp_url = resp_url[:-len(qs)]
247 244 if self._url.rstrip('/') != resp_url.rstrip('/'):
248 245 if not self.ui.quiet:
249 246 self.ui.warn(_('real URL is %s\n') % resp_url)
250 247 self._url = resp_url
251 248 try:
252 249 proto = resp.getheader('content-type')
253 250 except AttributeError:
254 251 proto = resp.headers.get('content-type', '')
255 252
256 253 safeurl = util.hidepassword(self._url)
257 254 if proto.startswith('application/hg-error'):
258 255 raise error.OutOfBandError(resp.read())
259 256 # accept old "text/plain" and "application/hg-changegroup" for now
260 257 if not (proto.startswith('application/mercurial-') or
261 258 (proto.startswith('text/plain')
262 259 and not resp.headers.get('content-length')) or
263 260 proto.startswith('application/hg-changegroup')):
264 261 self.ui.debug("requested URL: '%s'\n" % util.hidepassword(cu))
265 262 raise error.RepoError(
266 263 _("'%s' does not appear to be an hg repository:\n"
267 264 "---%%<--- (%s)\n%s\n---%%<---\n")
268 265 % (safeurl, proto or 'no content-type', resp.read(1024)))
269 266
270 267 if proto.startswith('application/mercurial-'):
271 268 try:
272 269 version = proto.split('-', 1)[1]
273 270 version_info = tuple([int(n) for n in version.split('.')])
274 271 except ValueError:
275 272 raise error.RepoError(_("'%s' sent a broken Content-Type "
276 273 "header (%s)") % (safeurl, proto))
277 274
278 275 # TODO consider switching to a decompression reader that uses
279 276 # generators.
280 277 if version_info == (0, 1):
281 278 if _compressible:
282 279 return util.compengines['zlib'].decompressorreader(resp)
283 280 return resp
284 281 elif version_info == (0, 2):
285 282 # application/mercurial-0.2 always identifies the compression
286 283 # engine in the payload header.
287 284 elen = struct.unpack('B', resp.read(1))[0]
288 285 ename = resp.read(elen)
289 286 engine = util.compengines.forwiretype(ename)
290 287 return engine.decompressorreader(resp)
291 288 else:
292 289 raise error.RepoError(_("'%s' uses newer protocol %s") %
293 290 (safeurl, version))
294 291
295 292 if _compressible:
296 293 return util.compengines['zlib'].decompressorreader(resp)
297 294
298 295 return resp
299 296
300 297 def _call(self, cmd, **args):
301 298 fp = self._callstream(cmd, **args)
302 299 try:
303 300 return fp.read()
304 301 finally:
305 302 # if using keepalive, allow connection to be reused
306 303 fp.close()
307 304
308 305 def _callpush(self, cmd, cg, **args):
309 306 # have to stream bundle to a temp file because we do not have
310 307 # http 1.1 chunked transfer.
311 308
312 309 types = self.capable('unbundle')
313 310 try:
314 311 types = types.split(',')
315 312 except AttributeError:
316 313 # servers older than d1b16a746db6 will send 'unbundle' as a
317 314 # boolean capability. They only support headerless/uncompressed
318 315 # bundles.
319 316 types = [""]
320 317 for x in types:
321 318 if x in bundle2.bundletypes:
322 319 type = x
323 320 break
324 321
325 322 tempname = bundle2.writebundle(self.ui, cg, None, type)
326 323 fp = httpconnection.httpsendfile(self.ui, tempname, "rb")
327 324 headers = {'Content-Type': 'application/mercurial-0.1'}
328 325
329 326 try:
330 327 r = self._call(cmd, data=fp, headers=headers, **args)
331 328 vals = r.split('\n', 1)
332 329 if len(vals) < 2:
333 330 raise error.ResponseError(_("unexpected response:"), r)
334 331 return vals
335 332 except socket.error as err:
336 333 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
337 334 raise error.Abort(_('push failed: %s') % err.args[1])
338 335 raise error.Abort(err.args[1])
339 336 finally:
340 337 fp.close()
341 338 os.unlink(tempname)
342 339
343 340 def _calltwowaystream(self, cmd, fp, **args):
344 341 fh = None
345 342 fp_ = None
346 343 filename = None
347 344 try:
348 345 # dump bundle to disk
349 346 fd, filename = tempfile.mkstemp(prefix="hg-bundle-", suffix=".hg")
350 347 fh = os.fdopen(fd, pycompat.sysstr("wb"))
351 348 d = fp.read(4096)
352 349 while d:
353 350 fh.write(d)
354 351 d = fp.read(4096)
355 352 fh.close()
356 353 # start http push
357 354 fp_ = httpconnection.httpsendfile(self.ui, filename, "rb")
358 355 headers = {'Content-Type': 'application/mercurial-0.1'}
359 356 return self._callstream(cmd, data=fp_, headers=headers, **args)
360 357 finally:
361 358 if fp_ is not None:
362 359 fp_.close()
363 360 if fh is not None:
364 361 fh.close()
365 362 os.unlink(filename)
366 363
367 364 def _callcompressable(self, cmd, **args):
368 365 return self._callstream(cmd, _compressible=True, **args)
369 366
370 367 def _abort(self, exception):
371 368 raise exception
372 369
373 370 class httpspeer(httppeer):
374 371 def __init__(self, ui, path):
375 372 if not url.has_https:
376 373 raise error.Abort(_('Python support for SSL and HTTPS '
377 374 'is not installed'))
378 375 httppeer.__init__(self, ui, path)
379 376
380 377 def instance(ui, path, create):
381 378 if create:
382 379 raise error.Abort(_('cannot create new http repository'))
383 380 try:
384 381 if path.startswith('https:'):
385 382 inst = httpspeer(ui, path)
386 383 else:
387 384 inst = httppeer(ui, path)
388 385 try:
389 386 # Try to do useful work when checking compatibility.
390 387 # Usually saves a roundtrip since we want the caps anyway.
391 388 inst._fetchcaps()
392 389 except error.RepoError:
393 390 # No luck, try older compatibility check.
394 391 inst.between([(nullid, nullid)])
395 392 return inst
396 393 except error.RepoError as httpexception:
397 394 try:
398 395 r = statichttprepo.instance(ui, "static-" + path, create)
399 396 ui.note(_('(falling back to static-http)\n'))
400 397 return r
401 398 except error.RepoError:
402 399 raise httpexception # use the original http RepoError instead
@@ -1,2262 +1,2259 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 def lock(self):
241 return self._repo.lock()
242
243 240 def pushkey(self, namespace, key, old, new):
244 241 return self._repo.pushkey(namespace, key, old, new)
245 242
246 243 def listkeys(self, namespace):
247 244 return self._repo.listkeys(namespace)
248 245
249 246 def debugwireargs(self, one, two, three=None, four=None, five=None):
250 247 '''used to test argument passing over the wire'''
251 248 return "%s %s %s %s %s" % (one, two, three, four, five)
252 249
253 250 class locallegacypeer(localpeer):
254 251 '''peer extension which implements legacy methods too; used for tests with
255 252 restricted capabilities'''
256 253
257 254 def __init__(self, repo):
258 255 localpeer.__init__(self, repo, caps=legacycaps)
259 256
260 257 def branches(self, nodes):
261 258 return self._repo.branches(nodes)
262 259
263 260 def between(self, pairs):
264 261 return self._repo.between(pairs)
265 262
266 263 def changegroup(self, basenodes, source):
267 264 return changegroup.changegroup(self._repo, basenodes, source)
268 265
269 266 def changegroupsubset(self, bases, heads, source):
270 267 return changegroup.changegroupsubset(self._repo, bases, heads, source)
271 268
272 269 # Increment the sub-version when the revlog v2 format changes to lock out old
273 270 # clients.
274 271 REVLOGV2_REQUIREMENT = 'exp-revlogv2.0'
275 272
276 273 class localrepository(object):
277 274
278 275 supportedformats = {
279 276 'revlogv1',
280 277 'generaldelta',
281 278 'treemanifest',
282 279 'manifestv2',
283 280 REVLOGV2_REQUIREMENT,
284 281 }
285 282 _basesupported = supportedformats | {
286 283 'store',
287 284 'fncache',
288 285 'shared',
289 286 'relshared',
290 287 'dotencode',
291 288 'exp-sparse',
292 289 }
293 290 openerreqs = {
294 291 'revlogv1',
295 292 'generaldelta',
296 293 'treemanifest',
297 294 'manifestv2',
298 295 }
299 296
300 297 # a list of (ui, featureset) functions.
301 298 # only functions defined in module of enabled extensions are invoked
302 299 featuresetupfuncs = set()
303 300
304 301 # list of prefix for file which can be written without 'wlock'
305 302 # Extensions should extend this list when needed
306 303 _wlockfreeprefix = {
307 304 # We migh consider requiring 'wlock' for the next
308 305 # two, but pretty much all the existing code assume
309 306 # wlock is not needed so we keep them excluded for
310 307 # now.
311 308 'hgrc',
312 309 'requires',
313 310 # XXX cache is a complicatged business someone
314 311 # should investigate this in depth at some point
315 312 'cache/',
316 313 # XXX shouldn't be dirstate covered by the wlock?
317 314 'dirstate',
318 315 # XXX bisect was still a bit too messy at the time
319 316 # this changeset was introduced. Someone should fix
320 317 # the remainig bit and drop this line
321 318 'bisect.state',
322 319 }
323 320
324 321 def __init__(self, baseui, path, create=False):
325 322 self.requirements = set()
326 323 self.filtername = None
327 324 # wvfs: rooted at the repository root, used to access the working copy
328 325 self.wvfs = vfsmod.vfs(path, expandpath=True, realpath=True)
329 326 # vfs: rooted at .hg, used to access repo files outside of .hg/store
330 327 self.vfs = None
331 328 # svfs: usually rooted at .hg/store, used to access repository history
332 329 # If this is a shared repository, this vfs may point to another
333 330 # repository's .hg/store directory.
334 331 self.svfs = None
335 332 self.root = self.wvfs.base
336 333 self.path = self.wvfs.join(".hg")
337 334 self.origroot = path
338 335 # These auditor are not used by the vfs,
339 336 # only used when writing this comment: basectx.match
340 337 self.auditor = pathutil.pathauditor(self.root, self._checknested)
341 338 self.nofsauditor = pathutil.pathauditor(self.root, self._checknested,
342 339 realfs=False)
343 340 self.baseui = baseui
344 341 self.ui = baseui.copy()
345 342 self.ui.copy = baseui.copy # prevent copying repo configuration
346 343 self.vfs = vfsmod.vfs(self.path)
347 344 if (self.ui.configbool('devel', 'all-warnings') or
348 345 self.ui.configbool('devel', 'check-locks')):
349 346 self.vfs.audit = self._getvfsward(self.vfs.audit)
350 347 # A list of callback to shape the phase if no data were found.
351 348 # Callback are in the form: func(repo, roots) --> processed root.
352 349 # This list it to be filled by extension during repo setup
353 350 self._phasedefaults = []
354 351 try:
355 352 self.ui.readconfig(self.vfs.join("hgrc"), self.root)
356 353 self._loadextensions()
357 354 except IOError:
358 355 pass
359 356
360 357 if self.featuresetupfuncs:
361 358 self.supported = set(self._basesupported) # use private copy
362 359 extmods = set(m.__name__ for n, m
363 360 in extensions.extensions(self.ui))
364 361 for setupfunc in self.featuresetupfuncs:
365 362 if setupfunc.__module__ in extmods:
366 363 setupfunc(self.ui, self.supported)
367 364 else:
368 365 self.supported = self._basesupported
369 366 color.setup(self.ui)
370 367
371 368 # Add compression engines.
372 369 for name in util.compengines:
373 370 engine = util.compengines[name]
374 371 if engine.revlogheader():
375 372 self.supported.add('exp-compression-%s' % name)
376 373
377 374 if not self.vfs.isdir():
378 375 if create:
379 376 self.requirements = newreporequirements(self)
380 377
381 378 if not self.wvfs.exists():
382 379 self.wvfs.makedirs()
383 380 self.vfs.makedir(notindexed=True)
384 381
385 382 if 'store' in self.requirements:
386 383 self.vfs.mkdir("store")
387 384
388 385 # create an invalid changelog
389 386 self.vfs.append(
390 387 "00changelog.i",
391 388 '\0\0\0\2' # represents revlogv2
392 389 ' dummy changelog to prevent using the old repo layout'
393 390 )
394 391 else:
395 392 raise error.RepoError(_("repository %s not found") % path)
396 393 elif create:
397 394 raise error.RepoError(_("repository %s already exists") % path)
398 395 else:
399 396 try:
400 397 self.requirements = scmutil.readrequires(
401 398 self.vfs, self.supported)
402 399 except IOError as inst:
403 400 if inst.errno != errno.ENOENT:
404 401 raise
405 402
406 403 cachepath = self.vfs.join('cache')
407 404 self.sharedpath = self.path
408 405 try:
409 406 sharedpath = self.vfs.read("sharedpath").rstrip('\n')
410 407 if 'relshared' in self.requirements:
411 408 sharedpath = self.vfs.join(sharedpath)
412 409 vfs = vfsmod.vfs(sharedpath, realpath=True)
413 410 cachepath = vfs.join('cache')
414 411 s = vfs.base
415 412 if not vfs.exists():
416 413 raise error.RepoError(
417 414 _('.hg/sharedpath points to nonexistent directory %s') % s)
418 415 self.sharedpath = s
419 416 except IOError as inst:
420 417 if inst.errno != errno.ENOENT:
421 418 raise
422 419
423 420 if 'exp-sparse' in self.requirements and not sparse.enabled:
424 421 raise error.RepoError(_('repository is using sparse feature but '
425 422 'sparse is not enabled; enable the '
426 423 '"sparse" extensions to access'))
427 424
428 425 self.store = store.store(
429 426 self.requirements, self.sharedpath, vfsmod.vfs)
430 427 self.spath = self.store.path
431 428 self.svfs = self.store.vfs
432 429 self.sjoin = self.store.join
433 430 self.vfs.createmode = self.store.createmode
434 431 self.cachevfs = vfsmod.vfs(cachepath)
435 432 self.cachevfs.createmode = self.store.createmode
436 433 if (self.ui.configbool('devel', 'all-warnings') or
437 434 self.ui.configbool('devel', 'check-locks')):
438 435 if util.safehasattr(self.svfs, 'vfs'): # this is filtervfs
439 436 self.svfs.vfs.audit = self._getsvfsward(self.svfs.vfs.audit)
440 437 else: # standard vfs
441 438 self.svfs.audit = self._getsvfsward(self.svfs.audit)
442 439 self._applyopenerreqs()
443 440 if create:
444 441 self._writerequirements()
445 442
446 443 self._dirstatevalidatewarned = False
447 444
448 445 self._branchcaches = {}
449 446 self._revbranchcache = None
450 447 self.filterpats = {}
451 448 self._datafilters = {}
452 449 self._transref = self._lockref = self._wlockref = None
453 450
454 451 # A cache for various files under .hg/ that tracks file changes,
455 452 # (used by the filecache decorator)
456 453 #
457 454 # Maps a property name to its util.filecacheentry
458 455 self._filecache = {}
459 456
460 457 # hold sets of revision to be filtered
461 458 # should be cleared when something might have changed the filter value:
462 459 # - new changesets,
463 460 # - phase change,
464 461 # - new obsolescence marker,
465 462 # - working directory parent change,
466 463 # - bookmark changes
467 464 self.filteredrevcache = {}
468 465
469 466 # post-dirstate-status hooks
470 467 self._postdsstatus = []
471 468
472 469 # Cache of types representing filtered repos.
473 470 self._filteredrepotypes = weakref.WeakKeyDictionary()
474 471
475 472 # generic mapping between names and nodes
476 473 self.names = namespaces.namespaces()
477 474
478 475 # Key to signature value.
479 476 self._sparsesignaturecache = {}
480 477 # Signature to cached matcher instance.
481 478 self._sparsematchercache = {}
482 479
483 480 def _getvfsward(self, origfunc):
484 481 """build a ward for self.vfs"""
485 482 rref = weakref.ref(self)
486 483 def checkvfs(path, mode=None):
487 484 ret = origfunc(path, mode=mode)
488 485 repo = rref()
489 486 if (repo is None
490 487 or not util.safehasattr(repo, '_wlockref')
491 488 or not util.safehasattr(repo, '_lockref')):
492 489 return
493 490 if mode in (None, 'r', 'rb'):
494 491 return
495 492 if path.startswith(repo.path):
496 493 # truncate name relative to the repository (.hg)
497 494 path = path[len(repo.path) + 1:]
498 495 if path.startswith('cache/'):
499 496 msg = 'accessing cache with vfs instead of cachevfs: "%s"'
500 497 repo.ui.develwarn(msg % path, stacklevel=2, config="cache-vfs")
501 498 if path.startswith('journal.'):
502 499 # journal is covered by 'lock'
503 500 if repo._currentlock(repo._lockref) is None:
504 501 repo.ui.develwarn('write with no lock: "%s"' % path,
505 502 stacklevel=2, config='check-locks')
506 503 elif repo._currentlock(repo._wlockref) is None:
507 504 # rest of vfs files are covered by 'wlock'
508 505 #
509 506 # exclude special files
510 507 for prefix in self._wlockfreeprefix:
511 508 if path.startswith(prefix):
512 509 return
513 510 repo.ui.develwarn('write with no wlock: "%s"' % path,
514 511 stacklevel=2, config='check-locks')
515 512 return ret
516 513 return checkvfs
517 514
518 515 def _getsvfsward(self, origfunc):
519 516 """build a ward for self.svfs"""
520 517 rref = weakref.ref(self)
521 518 def checksvfs(path, mode=None):
522 519 ret = origfunc(path, mode=mode)
523 520 repo = rref()
524 521 if repo is None or not util.safehasattr(repo, '_lockref'):
525 522 return
526 523 if mode in (None, 'r', 'rb'):
527 524 return
528 525 if path.startswith(repo.sharedpath):
529 526 # truncate name relative to the repository (.hg)
530 527 path = path[len(repo.sharedpath) + 1:]
531 528 if repo._currentlock(repo._lockref) is None:
532 529 repo.ui.develwarn('write with no lock: "%s"' % path,
533 530 stacklevel=3)
534 531 return ret
535 532 return checksvfs
536 533
537 534 def close(self):
538 535 self._writecaches()
539 536
540 537 def _loadextensions(self):
541 538 extensions.loadall(self.ui)
542 539
543 540 def _writecaches(self):
544 541 if self._revbranchcache:
545 542 self._revbranchcache.write()
546 543
547 544 def _restrictcapabilities(self, caps):
548 545 if self.ui.configbool('experimental', 'bundle2-advertise'):
549 546 caps = set(caps)
550 547 capsblob = bundle2.encodecaps(bundle2.getrepocaps(self))
551 548 caps.add('bundle2=' + urlreq.quote(capsblob))
552 549 return caps
553 550
554 551 def _applyopenerreqs(self):
555 552 self.svfs.options = dict((r, 1) for r in self.requirements
556 553 if r in self.openerreqs)
557 554 # experimental config: format.chunkcachesize
558 555 chunkcachesize = self.ui.configint('format', 'chunkcachesize')
559 556 if chunkcachesize is not None:
560 557 self.svfs.options['chunkcachesize'] = chunkcachesize
561 558 # experimental config: format.maxchainlen
562 559 maxchainlen = self.ui.configint('format', 'maxchainlen')
563 560 if maxchainlen is not None:
564 561 self.svfs.options['maxchainlen'] = maxchainlen
565 562 # experimental config: format.manifestcachesize
566 563 manifestcachesize = self.ui.configint('format', 'manifestcachesize')
567 564 if manifestcachesize is not None:
568 565 self.svfs.options['manifestcachesize'] = manifestcachesize
569 566 # experimental config: format.aggressivemergedeltas
570 567 aggressivemergedeltas = self.ui.configbool('format',
571 568 'aggressivemergedeltas')
572 569 self.svfs.options['aggressivemergedeltas'] = aggressivemergedeltas
573 570 self.svfs.options['lazydeltabase'] = not scmutil.gddeltaconfig(self.ui)
574 571 chainspan = self.ui.configbytes('experimental', 'maxdeltachainspan', -1)
575 572 if 0 <= chainspan:
576 573 self.svfs.options['maxdeltachainspan'] = chainspan
577 574
578 575 for r in self.requirements:
579 576 if r.startswith('exp-compression-'):
580 577 self.svfs.options['compengine'] = r[len('exp-compression-'):]
581 578
582 579 # TODO move "revlogv2" to openerreqs once finalized.
583 580 if REVLOGV2_REQUIREMENT in self.requirements:
584 581 self.svfs.options['revlogv2'] = True
585 582
586 583 def _writerequirements(self):
587 584 scmutil.writerequires(self.vfs, self.requirements)
588 585
589 586 def _checknested(self, path):
590 587 """Determine if path is a legal nested repository."""
591 588 if not path.startswith(self.root):
592 589 return False
593 590 subpath = path[len(self.root) + 1:]
594 591 normsubpath = util.pconvert(subpath)
595 592
596 593 # XXX: Checking against the current working copy is wrong in
597 594 # the sense that it can reject things like
598 595 #
599 596 # $ hg cat -r 10 sub/x.txt
600 597 #
601 598 # if sub/ is no longer a subrepository in the working copy
602 599 # parent revision.
603 600 #
604 601 # However, it can of course also allow things that would have
605 602 # been rejected before, such as the above cat command if sub/
606 603 # is a subrepository now, but was a normal directory before.
607 604 # The old path auditor would have rejected by mistake since it
608 605 # panics when it sees sub/.hg/.
609 606 #
610 607 # All in all, checking against the working copy seems sensible
611 608 # since we want to prevent access to nested repositories on
612 609 # the filesystem *now*.
613 610 ctx = self[None]
614 611 parts = util.splitpath(subpath)
615 612 while parts:
616 613 prefix = '/'.join(parts)
617 614 if prefix in ctx.substate:
618 615 if prefix == normsubpath:
619 616 return True
620 617 else:
621 618 sub = ctx.sub(prefix)
622 619 return sub.checknested(subpath[len(prefix) + 1:])
623 620 else:
624 621 parts.pop()
625 622 return False
626 623
627 624 def peer(self):
628 625 return localpeer(self) # not cached to avoid reference cycle
629 626
630 627 def unfiltered(self):
631 628 """Return unfiltered version of the repository
632 629
633 630 Intended to be overwritten by filtered repo."""
634 631 return self
635 632
636 633 def filtered(self, name):
637 634 """Return a filtered version of a repository"""
638 635 # Python <3.4 easily leaks types via __mro__. See
639 636 # https://bugs.python.org/issue17950. We cache dynamically
640 637 # created types so this method doesn't leak on every
641 638 # invocation.
642 639
643 640 key = self.unfiltered().__class__
644 641 if key not in self._filteredrepotypes:
645 642 # Build a new type with the repoview mixin and the base
646 643 # class of this repo. Give it a name containing the
647 644 # filter name to aid debugging.
648 645 bases = (repoview.repoview, key)
649 646 cls = type(r'%sfilteredrepo' % name, bases, {})
650 647 self._filteredrepotypes[key] = cls
651 648
652 649 return self._filteredrepotypes[key](self, name)
653 650
654 651 @repofilecache('bookmarks', 'bookmarks.current')
655 652 def _bookmarks(self):
656 653 return bookmarks.bmstore(self)
657 654
658 655 @property
659 656 def _activebookmark(self):
660 657 return self._bookmarks.active
661 658
662 659 # _phaserevs and _phasesets depend on changelog. what we need is to
663 660 # call _phasecache.invalidate() if '00changelog.i' was changed, but it
664 661 # can't be easily expressed in filecache mechanism.
665 662 @storecache('phaseroots', '00changelog.i')
666 663 def _phasecache(self):
667 664 return phases.phasecache(self, self._phasedefaults)
668 665
669 666 @storecache('obsstore')
670 667 def obsstore(self):
671 668 return obsolete.makestore(self.ui, self)
672 669
673 670 @storecache('00changelog.i')
674 671 def changelog(self):
675 672 return changelog.changelog(self.svfs,
676 673 trypending=txnutil.mayhavepending(self.root))
677 674
678 675 def _constructmanifest(self):
679 676 # This is a temporary function while we migrate from manifest to
680 677 # manifestlog. It allows bundlerepo and unionrepo to intercept the
681 678 # manifest creation.
682 679 return manifest.manifestrevlog(self.svfs)
683 680
684 681 @storecache('00manifest.i')
685 682 def manifestlog(self):
686 683 return manifest.manifestlog(self.svfs, self)
687 684
688 685 @repofilecache('dirstate')
689 686 def dirstate(self):
690 687 sparsematchfn = lambda: sparse.matcher(self)
691 688
692 689 return dirstate.dirstate(self.vfs, self.ui, self.root,
693 690 self._dirstatevalidate, sparsematchfn)
694 691
695 692 def _dirstatevalidate(self, node):
696 693 try:
697 694 self.changelog.rev(node)
698 695 return node
699 696 except error.LookupError:
700 697 if not self._dirstatevalidatewarned:
701 698 self._dirstatevalidatewarned = True
702 699 self.ui.warn(_("warning: ignoring unknown"
703 700 " working parent %s!\n") % short(node))
704 701 return nullid
705 702
706 703 def __getitem__(self, changeid):
707 704 if changeid is None:
708 705 return context.workingctx(self)
709 706 if isinstance(changeid, slice):
710 707 # wdirrev isn't contiguous so the slice shouldn't include it
711 708 return [context.changectx(self, i)
712 709 for i in xrange(*changeid.indices(len(self)))
713 710 if i not in self.changelog.filteredrevs]
714 711 try:
715 712 return context.changectx(self, changeid)
716 713 except error.WdirUnsupported:
717 714 return context.workingctx(self)
718 715
719 716 def __contains__(self, changeid):
720 717 """True if the given changeid exists
721 718
722 719 error.LookupError is raised if an ambiguous node specified.
723 720 """
724 721 try:
725 722 self[changeid]
726 723 return True
727 724 except error.RepoLookupError:
728 725 return False
729 726
730 727 def __nonzero__(self):
731 728 return True
732 729
733 730 __bool__ = __nonzero__
734 731
735 732 def __len__(self):
736 733 return len(self.changelog)
737 734
738 735 def __iter__(self):
739 736 return iter(self.changelog)
740 737
741 738 def revs(self, expr, *args):
742 739 '''Find revisions matching a revset.
743 740
744 741 The revset is specified as a string ``expr`` that may contain
745 742 %-formatting to escape certain types. See ``revsetlang.formatspec``.
746 743
747 744 Revset aliases from the configuration are not expanded. To expand
748 745 user aliases, consider calling ``scmutil.revrange()`` or
749 746 ``repo.anyrevs([expr], user=True)``.
750 747
751 748 Returns a revset.abstractsmartset, which is a list-like interface
752 749 that contains integer revisions.
753 750 '''
754 751 expr = revsetlang.formatspec(expr, *args)
755 752 m = revset.match(None, expr)
756 753 return m(self)
757 754
758 755 def set(self, expr, *args):
759 756 '''Find revisions matching a revset and emit changectx instances.
760 757
761 758 This is a convenience wrapper around ``revs()`` that iterates the
762 759 result and is a generator of changectx instances.
763 760
764 761 Revset aliases from the configuration are not expanded. To expand
765 762 user aliases, consider calling ``scmutil.revrange()``.
766 763 '''
767 764 for r in self.revs(expr, *args):
768 765 yield self[r]
769 766
770 767 def anyrevs(self, specs, user=False, localalias=None):
771 768 '''Find revisions matching one of the given revsets.
772 769
773 770 Revset aliases from the configuration are not expanded by default. To
774 771 expand user aliases, specify ``user=True``. To provide some local
775 772 definitions overriding user aliases, set ``localalias`` to
776 773 ``{name: definitionstring}``.
777 774 '''
778 775 if user:
779 776 m = revset.matchany(self.ui, specs, repo=self,
780 777 localalias=localalias)
781 778 else:
782 779 m = revset.matchany(None, specs, localalias=localalias)
783 780 return m(self)
784 781
785 782 def url(self):
786 783 return 'file:' + self.root
787 784
788 785 def hook(self, name, throw=False, **args):
789 786 """Call a hook, passing this repo instance.
790 787
791 788 This a convenience method to aid invoking hooks. Extensions likely
792 789 won't call this unless they have registered a custom hook or are
793 790 replacing code that is expected to call a hook.
794 791 """
795 792 return hook.hook(self.ui, self, name, throw, **args)
796 793
797 794 @filteredpropertycache
798 795 def _tagscache(self):
799 796 '''Returns a tagscache object that contains various tags related
800 797 caches.'''
801 798
802 799 # This simplifies its cache management by having one decorated
803 800 # function (this one) and the rest simply fetch things from it.
804 801 class tagscache(object):
805 802 def __init__(self):
806 803 # These two define the set of tags for this repository. tags
807 804 # maps tag name to node; tagtypes maps tag name to 'global' or
808 805 # 'local'. (Global tags are defined by .hgtags across all
809 806 # heads, and local tags are defined in .hg/localtags.)
810 807 # They constitute the in-memory cache of tags.
811 808 self.tags = self.tagtypes = None
812 809
813 810 self.nodetagscache = self.tagslist = None
814 811
815 812 cache = tagscache()
816 813 cache.tags, cache.tagtypes = self._findtags()
817 814
818 815 return cache
819 816
820 817 def tags(self):
821 818 '''return a mapping of tag to node'''
822 819 t = {}
823 820 if self.changelog.filteredrevs:
824 821 tags, tt = self._findtags()
825 822 else:
826 823 tags = self._tagscache.tags
827 824 for k, v in tags.iteritems():
828 825 try:
829 826 # ignore tags to unknown nodes
830 827 self.changelog.rev(v)
831 828 t[k] = v
832 829 except (error.LookupError, ValueError):
833 830 pass
834 831 return t
835 832
836 833 def _findtags(self):
837 834 '''Do the hard work of finding tags. Return a pair of dicts
838 835 (tags, tagtypes) where tags maps tag name to node, and tagtypes
839 836 maps tag name to a string like \'global\' or \'local\'.
840 837 Subclasses or extensions are free to add their own tags, but
841 838 should be aware that the returned dicts will be retained for the
842 839 duration of the localrepo object.'''
843 840
844 841 # XXX what tagtype should subclasses/extensions use? Currently
845 842 # mq and bookmarks add tags, but do not set the tagtype at all.
846 843 # Should each extension invent its own tag type? Should there
847 844 # be one tagtype for all such "virtual" tags? Or is the status
848 845 # quo fine?
849 846
850 847
851 848 # map tag name to (node, hist)
852 849 alltags = tagsmod.findglobaltags(self.ui, self)
853 850 # map tag name to tag type
854 851 tagtypes = dict((tag, 'global') for tag in alltags)
855 852
856 853 tagsmod.readlocaltags(self.ui, self, alltags, tagtypes)
857 854
858 855 # Build the return dicts. Have to re-encode tag names because
859 856 # the tags module always uses UTF-8 (in order not to lose info
860 857 # writing to the cache), but the rest of Mercurial wants them in
861 858 # local encoding.
862 859 tags = {}
863 860 for (name, (node, hist)) in alltags.iteritems():
864 861 if node != nullid:
865 862 tags[encoding.tolocal(name)] = node
866 863 tags['tip'] = self.changelog.tip()
867 864 tagtypes = dict([(encoding.tolocal(name), value)
868 865 for (name, value) in tagtypes.iteritems()])
869 866 return (tags, tagtypes)
870 867
871 868 def tagtype(self, tagname):
872 869 '''
873 870 return the type of the given tag. result can be:
874 871
875 872 'local' : a local tag
876 873 'global' : a global tag
877 874 None : tag does not exist
878 875 '''
879 876
880 877 return self._tagscache.tagtypes.get(tagname)
881 878
882 879 def tagslist(self):
883 880 '''return a list of tags ordered by revision'''
884 881 if not self._tagscache.tagslist:
885 882 l = []
886 883 for t, n in self.tags().iteritems():
887 884 l.append((self.changelog.rev(n), t, n))
888 885 self._tagscache.tagslist = [(t, n) for r, t, n in sorted(l)]
889 886
890 887 return self._tagscache.tagslist
891 888
892 889 def nodetags(self, node):
893 890 '''return the tags associated with a node'''
894 891 if not self._tagscache.nodetagscache:
895 892 nodetagscache = {}
896 893 for t, n in self._tagscache.tags.iteritems():
897 894 nodetagscache.setdefault(n, []).append(t)
898 895 for tags in nodetagscache.itervalues():
899 896 tags.sort()
900 897 self._tagscache.nodetagscache = nodetagscache
901 898 return self._tagscache.nodetagscache.get(node, [])
902 899
903 900 def nodebookmarks(self, node):
904 901 """return the list of bookmarks pointing to the specified node"""
905 902 marks = []
906 903 for bookmark, n in self._bookmarks.iteritems():
907 904 if n == node:
908 905 marks.append(bookmark)
909 906 return sorted(marks)
910 907
911 908 def branchmap(self):
912 909 '''returns a dictionary {branch: [branchheads]} with branchheads
913 910 ordered by increasing revision number'''
914 911 branchmap.updatecache(self)
915 912 return self._branchcaches[self.filtername]
916 913
917 914 @unfilteredmethod
918 915 def revbranchcache(self):
919 916 if not self._revbranchcache:
920 917 self._revbranchcache = branchmap.revbranchcache(self.unfiltered())
921 918 return self._revbranchcache
922 919
923 920 def branchtip(self, branch, ignoremissing=False):
924 921 '''return the tip node for a given branch
925 922
926 923 If ignoremissing is True, then this method will not raise an error.
927 924 This is helpful for callers that only expect None for a missing branch
928 925 (e.g. namespace).
929 926
930 927 '''
931 928 try:
932 929 return self.branchmap().branchtip(branch)
933 930 except KeyError:
934 931 if not ignoremissing:
935 932 raise error.RepoLookupError(_("unknown branch '%s'") % branch)
936 933 else:
937 934 pass
938 935
939 936 def lookup(self, key):
940 937 return self[key].node()
941 938
942 939 def lookupbranch(self, key, remote=None):
943 940 repo = remote or self
944 941 if key in repo.branchmap():
945 942 return key
946 943
947 944 repo = (remote and remote.local()) and remote or self
948 945 return repo[key].branch()
949 946
950 947 def known(self, nodes):
951 948 cl = self.changelog
952 949 nm = cl.nodemap
953 950 filtered = cl.filteredrevs
954 951 result = []
955 952 for n in nodes:
956 953 r = nm.get(n)
957 954 resp = not (r is None or r in filtered)
958 955 result.append(resp)
959 956 return result
960 957
961 958 def local(self):
962 959 return self
963 960
964 961 def publishing(self):
965 962 # it's safe (and desirable) to trust the publish flag unconditionally
966 963 # so that we don't finalize changes shared between users via ssh or nfs
967 964 return self.ui.configbool('phases', 'publish', untrusted=True)
968 965
969 966 def cancopy(self):
970 967 # so statichttprepo's override of local() works
971 968 if not self.local():
972 969 return False
973 970 if not self.publishing():
974 971 return True
975 972 # if publishing we can't copy if there is filtered content
976 973 return not self.filtered('visible').changelog.filteredrevs
977 974
978 975 def shared(self):
979 976 '''the type of shared repository (None if not shared)'''
980 977 if self.sharedpath != self.path:
981 978 return 'store'
982 979 return None
983 980
984 981 def wjoin(self, f, *insidef):
985 982 return self.vfs.reljoin(self.root, f, *insidef)
986 983
987 984 def file(self, f):
988 985 if f[0] == '/':
989 986 f = f[1:]
990 987 return filelog.filelog(self.svfs, f)
991 988
992 989 def changectx(self, changeid):
993 990 return self[changeid]
994 991
995 992 def setparents(self, p1, p2=nullid):
996 993 with self.dirstate.parentchange():
997 994 copies = self.dirstate.setparents(p1, p2)
998 995 pctx = self[p1]
999 996 if copies:
1000 997 # Adjust copy records, the dirstate cannot do it, it
1001 998 # requires access to parents manifests. Preserve them
1002 999 # only for entries added to first parent.
1003 1000 for f in copies:
1004 1001 if f not in pctx and copies[f] in pctx:
1005 1002 self.dirstate.copy(copies[f], f)
1006 1003 if p2 == nullid:
1007 1004 for f, s in sorted(self.dirstate.copies().items()):
1008 1005 if f not in pctx and s not in pctx:
1009 1006 self.dirstate.copy(None, f)
1010 1007
1011 1008 def filectx(self, path, changeid=None, fileid=None):
1012 1009 """changeid can be a changeset revision, node, or tag.
1013 1010 fileid can be a file revision or node."""
1014 1011 return context.filectx(self, path, changeid, fileid)
1015 1012
1016 1013 def getcwd(self):
1017 1014 return self.dirstate.getcwd()
1018 1015
1019 1016 def pathto(self, f, cwd=None):
1020 1017 return self.dirstate.pathto(f, cwd)
1021 1018
1022 1019 def _loadfilter(self, filter):
1023 1020 if filter not in self.filterpats:
1024 1021 l = []
1025 1022 for pat, cmd in self.ui.configitems(filter):
1026 1023 if cmd == '!':
1027 1024 continue
1028 1025 mf = matchmod.match(self.root, '', [pat])
1029 1026 fn = None
1030 1027 params = cmd
1031 1028 for name, filterfn in self._datafilters.iteritems():
1032 1029 if cmd.startswith(name):
1033 1030 fn = filterfn
1034 1031 params = cmd[len(name):].lstrip()
1035 1032 break
1036 1033 if not fn:
1037 1034 fn = lambda s, c, **kwargs: util.filter(s, c)
1038 1035 # Wrap old filters not supporting keyword arguments
1039 1036 if not inspect.getargspec(fn)[2]:
1040 1037 oldfn = fn
1041 1038 fn = lambda s, c, **kwargs: oldfn(s, c)
1042 1039 l.append((mf, fn, params))
1043 1040 self.filterpats[filter] = l
1044 1041 return self.filterpats[filter]
1045 1042
1046 1043 def _filter(self, filterpats, filename, data):
1047 1044 for mf, fn, cmd in filterpats:
1048 1045 if mf(filename):
1049 1046 self.ui.debug("filtering %s through %s\n" % (filename, cmd))
1050 1047 data = fn(data, cmd, ui=self.ui, repo=self, filename=filename)
1051 1048 break
1052 1049
1053 1050 return data
1054 1051
1055 1052 @unfilteredpropertycache
1056 1053 def _encodefilterpats(self):
1057 1054 return self._loadfilter('encode')
1058 1055
1059 1056 @unfilteredpropertycache
1060 1057 def _decodefilterpats(self):
1061 1058 return self._loadfilter('decode')
1062 1059
1063 1060 def adddatafilter(self, name, filter):
1064 1061 self._datafilters[name] = filter
1065 1062
1066 1063 def wread(self, filename):
1067 1064 if self.wvfs.islink(filename):
1068 1065 data = self.wvfs.readlink(filename)
1069 1066 else:
1070 1067 data = self.wvfs.read(filename)
1071 1068 return self._filter(self._encodefilterpats, filename, data)
1072 1069
1073 1070 def wwrite(self, filename, data, flags, backgroundclose=False):
1074 1071 """write ``data`` into ``filename`` in the working directory
1075 1072
1076 1073 This returns length of written (maybe decoded) data.
1077 1074 """
1078 1075 data = self._filter(self._decodefilterpats, filename, data)
1079 1076 if 'l' in flags:
1080 1077 self.wvfs.symlink(data, filename)
1081 1078 else:
1082 1079 self.wvfs.write(filename, data, backgroundclose=backgroundclose)
1083 1080 if 'x' in flags:
1084 1081 self.wvfs.setflags(filename, False, True)
1085 1082 return len(data)
1086 1083
1087 1084 def wwritedata(self, filename, data):
1088 1085 return self._filter(self._decodefilterpats, filename, data)
1089 1086
1090 1087 def currenttransaction(self):
1091 1088 """return the current transaction or None if non exists"""
1092 1089 if self._transref:
1093 1090 tr = self._transref()
1094 1091 else:
1095 1092 tr = None
1096 1093
1097 1094 if tr and tr.running():
1098 1095 return tr
1099 1096 return None
1100 1097
1101 1098 def transaction(self, desc, report=None):
1102 1099 if (self.ui.configbool('devel', 'all-warnings')
1103 1100 or self.ui.configbool('devel', 'check-locks')):
1104 1101 if self._currentlock(self._lockref) is None:
1105 1102 raise error.ProgrammingError('transaction requires locking')
1106 1103 tr = self.currenttransaction()
1107 1104 if tr is not None:
1108 1105 scmutil.registersummarycallback(self, tr, desc)
1109 1106 return tr.nest()
1110 1107
1111 1108 # abort here if the journal already exists
1112 1109 if self.svfs.exists("journal"):
1113 1110 raise error.RepoError(
1114 1111 _("abandoned transaction found"),
1115 1112 hint=_("run 'hg recover' to clean up transaction"))
1116 1113
1117 1114 idbase = "%.40f#%f" % (random.random(), time.time())
1118 1115 ha = hex(hashlib.sha1(idbase).digest())
1119 1116 txnid = 'TXN:' + ha
1120 1117 self.hook('pretxnopen', throw=True, txnname=desc, txnid=txnid)
1121 1118
1122 1119 self._writejournal(desc)
1123 1120 renames = [(vfs, x, undoname(x)) for vfs, x in self._journalfiles()]
1124 1121 if report:
1125 1122 rp = report
1126 1123 else:
1127 1124 rp = self.ui.warn
1128 1125 vfsmap = {'plain': self.vfs} # root of .hg/
1129 1126 # we must avoid cyclic reference between repo and transaction.
1130 1127 reporef = weakref.ref(self)
1131 1128 # Code to track tag movement
1132 1129 #
1133 1130 # Since tags are all handled as file content, it is actually quite hard
1134 1131 # to track these movement from a code perspective. So we fallback to a
1135 1132 # tracking at the repository level. One could envision to track changes
1136 1133 # to the '.hgtags' file through changegroup apply but that fails to
1137 1134 # cope with case where transaction expose new heads without changegroup
1138 1135 # being involved (eg: phase movement).
1139 1136 #
1140 1137 # For now, We gate the feature behind a flag since this likely comes
1141 1138 # with performance impacts. The current code run more often than needed
1142 1139 # and do not use caches as much as it could. The current focus is on
1143 1140 # the behavior of the feature so we disable it by default. The flag
1144 1141 # will be removed when we are happy with the performance impact.
1145 1142 #
1146 1143 # Once this feature is no longer experimental move the following
1147 1144 # documentation to the appropriate help section:
1148 1145 #
1149 1146 # The ``HG_TAG_MOVED`` variable will be set if the transaction touched
1150 1147 # tags (new or changed or deleted tags). In addition the details of
1151 1148 # these changes are made available in a file at:
1152 1149 # ``REPOROOT/.hg/changes/tags.changes``.
1153 1150 # Make sure you check for HG_TAG_MOVED before reading that file as it
1154 1151 # might exist from a previous transaction even if no tag were touched
1155 1152 # in this one. Changes are recorded in a line base format::
1156 1153 #
1157 1154 # <action> <hex-node> <tag-name>\n
1158 1155 #
1159 1156 # Actions are defined as follow:
1160 1157 # "-R": tag is removed,
1161 1158 # "+A": tag is added,
1162 1159 # "-M": tag is moved (old value),
1163 1160 # "+M": tag is moved (new value),
1164 1161 tracktags = lambda x: None
1165 1162 # experimental config: experimental.hook-track-tags
1166 1163 shouldtracktags = self.ui.configbool('experimental', 'hook-track-tags')
1167 1164 if desc != 'strip' and shouldtracktags:
1168 1165 oldheads = self.changelog.headrevs()
1169 1166 def tracktags(tr2):
1170 1167 repo = reporef()
1171 1168 oldfnodes = tagsmod.fnoderevs(repo.ui, repo, oldheads)
1172 1169 newheads = repo.changelog.headrevs()
1173 1170 newfnodes = tagsmod.fnoderevs(repo.ui, repo, newheads)
1174 1171 # notes: we compare lists here.
1175 1172 # As we do it only once buiding set would not be cheaper
1176 1173 changes = tagsmod.difftags(repo.ui, repo, oldfnodes, newfnodes)
1177 1174 if changes:
1178 1175 tr2.hookargs['tag_moved'] = '1'
1179 1176 with repo.vfs('changes/tags.changes', 'w',
1180 1177 atomictemp=True) as changesfile:
1181 1178 # note: we do not register the file to the transaction
1182 1179 # because we needs it to still exist on the transaction
1183 1180 # is close (for txnclose hooks)
1184 1181 tagsmod.writediff(changesfile, changes)
1185 1182 def validate(tr2):
1186 1183 """will run pre-closing hooks"""
1187 1184 # XXX the transaction API is a bit lacking here so we take a hacky
1188 1185 # path for now
1189 1186 #
1190 1187 # We cannot add this as a "pending" hooks since the 'tr.hookargs'
1191 1188 # dict is copied before these run. In addition we needs the data
1192 1189 # available to in memory hooks too.
1193 1190 #
1194 1191 # Moreover, we also need to make sure this runs before txnclose
1195 1192 # hooks and there is no "pending" mechanism that would execute
1196 1193 # logic only if hooks are about to run.
1197 1194 #
1198 1195 # Fixing this limitation of the transaction is also needed to track
1199 1196 # other families of changes (bookmarks, phases, obsolescence).
1200 1197 #
1201 1198 # This will have to be fixed before we remove the experimental
1202 1199 # gating.
1203 1200 tracktags(tr2)
1204 1201 reporef().hook('pretxnclose', throw=True,
1205 1202 txnname=desc, **pycompat.strkwargs(tr.hookargs))
1206 1203 def releasefn(tr, success):
1207 1204 repo = reporef()
1208 1205 if success:
1209 1206 # this should be explicitly invoked here, because
1210 1207 # in-memory changes aren't written out at closing
1211 1208 # transaction, if tr.addfilegenerator (via
1212 1209 # dirstate.write or so) isn't invoked while
1213 1210 # transaction running
1214 1211 repo.dirstate.write(None)
1215 1212 else:
1216 1213 # discard all changes (including ones already written
1217 1214 # out) in this transaction
1218 1215 repo.dirstate.restorebackup(None, 'journal.dirstate')
1219 1216
1220 1217 repo.invalidate(clearfilecache=True)
1221 1218
1222 1219 tr = transaction.transaction(rp, self.svfs, vfsmap,
1223 1220 "journal",
1224 1221 "undo",
1225 1222 aftertrans(renames),
1226 1223 self.store.createmode,
1227 1224 validator=validate,
1228 1225 releasefn=releasefn,
1229 1226 checkambigfiles=_cachedfiles)
1230 1227 tr.changes['revs'] = set()
1231 1228 tr.changes['obsmarkers'] = set()
1232 1229 tr.changes['phases'] = {}
1233 1230 tr.changes['bookmarks'] = {}
1234 1231
1235 1232 tr.hookargs['txnid'] = txnid
1236 1233 # note: writing the fncache only during finalize mean that the file is
1237 1234 # outdated when running hooks. As fncache is used for streaming clone,
1238 1235 # this is not expected to break anything that happen during the hooks.
1239 1236 tr.addfinalize('flush-fncache', self.store.write)
1240 1237 def txnclosehook(tr2):
1241 1238 """To be run if transaction is successful, will schedule a hook run
1242 1239 """
1243 1240 # Don't reference tr2 in hook() so we don't hold a reference.
1244 1241 # This reduces memory consumption when there are multiple
1245 1242 # transactions per lock. This can likely go away if issue5045
1246 1243 # fixes the function accumulation.
1247 1244 hookargs = tr2.hookargs
1248 1245
1249 1246 def hook():
1250 1247 reporef().hook('txnclose', throw=False, txnname=desc,
1251 1248 **pycompat.strkwargs(hookargs))
1252 1249 reporef()._afterlock(hook)
1253 1250 tr.addfinalize('txnclose-hook', txnclosehook)
1254 1251 tr.addpostclose('warms-cache', self._buildcacheupdater(tr))
1255 1252 def txnaborthook(tr2):
1256 1253 """To be run if transaction is aborted
1257 1254 """
1258 1255 reporef().hook('txnabort', throw=False, txnname=desc,
1259 1256 **tr2.hookargs)
1260 1257 tr.addabort('txnabort-hook', txnaborthook)
1261 1258 # avoid eager cache invalidation. in-memory data should be identical
1262 1259 # to stored data if transaction has no error.
1263 1260 tr.addpostclose('refresh-filecachestats', self._refreshfilecachestats)
1264 1261 self._transref = weakref.ref(tr)
1265 1262 scmutil.registersummarycallback(self, tr, desc)
1266 1263 return tr
1267 1264
1268 1265 def _journalfiles(self):
1269 1266 return ((self.svfs, 'journal'),
1270 1267 (self.vfs, 'journal.dirstate'),
1271 1268 (self.vfs, 'journal.branch'),
1272 1269 (self.vfs, 'journal.desc'),
1273 1270 (self.vfs, 'journal.bookmarks'),
1274 1271 (self.svfs, 'journal.phaseroots'))
1275 1272
1276 1273 def undofiles(self):
1277 1274 return [(vfs, undoname(x)) for vfs, x in self._journalfiles()]
1278 1275
1279 1276 @unfilteredmethod
1280 1277 def _writejournal(self, desc):
1281 1278 self.dirstate.savebackup(None, 'journal.dirstate')
1282 1279 self.vfs.write("journal.branch",
1283 1280 encoding.fromlocal(self.dirstate.branch()))
1284 1281 self.vfs.write("journal.desc",
1285 1282 "%d\n%s\n" % (len(self), desc))
1286 1283 self.vfs.write("journal.bookmarks",
1287 1284 self.vfs.tryread("bookmarks"))
1288 1285 self.svfs.write("journal.phaseroots",
1289 1286 self.svfs.tryread("phaseroots"))
1290 1287
1291 1288 def recover(self):
1292 1289 with self.lock():
1293 1290 if self.svfs.exists("journal"):
1294 1291 self.ui.status(_("rolling back interrupted transaction\n"))
1295 1292 vfsmap = {'': self.svfs,
1296 1293 'plain': self.vfs,}
1297 1294 transaction.rollback(self.svfs, vfsmap, "journal",
1298 1295 self.ui.warn,
1299 1296 checkambigfiles=_cachedfiles)
1300 1297 self.invalidate()
1301 1298 return True
1302 1299 else:
1303 1300 self.ui.warn(_("no interrupted transaction available\n"))
1304 1301 return False
1305 1302
1306 1303 def rollback(self, dryrun=False, force=False):
1307 1304 wlock = lock = dsguard = None
1308 1305 try:
1309 1306 wlock = self.wlock()
1310 1307 lock = self.lock()
1311 1308 if self.svfs.exists("undo"):
1312 1309 dsguard = dirstateguard.dirstateguard(self, 'rollback')
1313 1310
1314 1311 return self._rollback(dryrun, force, dsguard)
1315 1312 else:
1316 1313 self.ui.warn(_("no rollback information available\n"))
1317 1314 return 1
1318 1315 finally:
1319 1316 release(dsguard, lock, wlock)
1320 1317
1321 1318 @unfilteredmethod # Until we get smarter cache management
1322 1319 def _rollback(self, dryrun, force, dsguard):
1323 1320 ui = self.ui
1324 1321 try:
1325 1322 args = self.vfs.read('undo.desc').splitlines()
1326 1323 (oldlen, desc, detail) = (int(args[0]), args[1], None)
1327 1324 if len(args) >= 3:
1328 1325 detail = args[2]
1329 1326 oldtip = oldlen - 1
1330 1327
1331 1328 if detail and ui.verbose:
1332 1329 msg = (_('repository tip rolled back to revision %d'
1333 1330 ' (undo %s: %s)\n')
1334 1331 % (oldtip, desc, detail))
1335 1332 else:
1336 1333 msg = (_('repository tip rolled back to revision %d'
1337 1334 ' (undo %s)\n')
1338 1335 % (oldtip, desc))
1339 1336 except IOError:
1340 1337 msg = _('rolling back unknown transaction\n')
1341 1338 desc = None
1342 1339
1343 1340 if not force and self['.'] != self['tip'] and desc == 'commit':
1344 1341 raise error.Abort(
1345 1342 _('rollback of last commit while not checked out '
1346 1343 'may lose data'), hint=_('use -f to force'))
1347 1344
1348 1345 ui.status(msg)
1349 1346 if dryrun:
1350 1347 return 0
1351 1348
1352 1349 parents = self.dirstate.parents()
1353 1350 self.destroying()
1354 1351 vfsmap = {'plain': self.vfs, '': self.svfs}
1355 1352 transaction.rollback(self.svfs, vfsmap, 'undo', ui.warn,
1356 1353 checkambigfiles=_cachedfiles)
1357 1354 if self.vfs.exists('undo.bookmarks'):
1358 1355 self.vfs.rename('undo.bookmarks', 'bookmarks', checkambig=True)
1359 1356 if self.svfs.exists('undo.phaseroots'):
1360 1357 self.svfs.rename('undo.phaseroots', 'phaseroots', checkambig=True)
1361 1358 self.invalidate()
1362 1359
1363 1360 parentgone = (parents[0] not in self.changelog.nodemap or
1364 1361 parents[1] not in self.changelog.nodemap)
1365 1362 if parentgone:
1366 1363 # prevent dirstateguard from overwriting already restored one
1367 1364 dsguard.close()
1368 1365
1369 1366 self.dirstate.restorebackup(None, 'undo.dirstate')
1370 1367 try:
1371 1368 branch = self.vfs.read('undo.branch')
1372 1369 self.dirstate.setbranch(encoding.tolocal(branch))
1373 1370 except IOError:
1374 1371 ui.warn(_('named branch could not be reset: '
1375 1372 'current branch is still \'%s\'\n')
1376 1373 % self.dirstate.branch())
1377 1374
1378 1375 parents = tuple([p.rev() for p in self[None].parents()])
1379 1376 if len(parents) > 1:
1380 1377 ui.status(_('working directory now based on '
1381 1378 'revisions %d and %d\n') % parents)
1382 1379 else:
1383 1380 ui.status(_('working directory now based on '
1384 1381 'revision %d\n') % parents)
1385 1382 mergemod.mergestate.clean(self, self['.'].node())
1386 1383
1387 1384 # TODO: if we know which new heads may result from this rollback, pass
1388 1385 # them to destroy(), which will prevent the branchhead cache from being
1389 1386 # invalidated.
1390 1387 self.destroyed()
1391 1388 return 0
1392 1389
1393 1390 def _buildcacheupdater(self, newtransaction):
1394 1391 """called during transaction to build the callback updating cache
1395 1392
1396 1393 Lives on the repository to help extension who might want to augment
1397 1394 this logic. For this purpose, the created transaction is passed to the
1398 1395 method.
1399 1396 """
1400 1397 # we must avoid cyclic reference between repo and transaction.
1401 1398 reporef = weakref.ref(self)
1402 1399 def updater(tr):
1403 1400 repo = reporef()
1404 1401 repo.updatecaches(tr)
1405 1402 return updater
1406 1403
1407 1404 @unfilteredmethod
1408 1405 def updatecaches(self, tr=None):
1409 1406 """warm appropriate caches
1410 1407
1411 1408 If this function is called after a transaction closed. The transaction
1412 1409 will be available in the 'tr' argument. This can be used to selectively
1413 1410 update caches relevant to the changes in that transaction.
1414 1411 """
1415 1412 if tr is not None and tr.hookargs.get('source') == 'strip':
1416 1413 # During strip, many caches are invalid but
1417 1414 # later call to `destroyed` will refresh them.
1418 1415 return
1419 1416
1420 1417 if tr is None or tr.changes['revs']:
1421 1418 # updating the unfiltered branchmap should refresh all the others,
1422 1419 self.ui.debug('updating the branch cache\n')
1423 1420 branchmap.updatecache(self.filtered('served'))
1424 1421
1425 1422 def invalidatecaches(self):
1426 1423
1427 1424 if '_tagscache' in vars(self):
1428 1425 # can't use delattr on proxy
1429 1426 del self.__dict__['_tagscache']
1430 1427
1431 1428 self.unfiltered()._branchcaches.clear()
1432 1429 self.invalidatevolatilesets()
1433 1430 self._sparsesignaturecache.clear()
1434 1431
1435 1432 def invalidatevolatilesets(self):
1436 1433 self.filteredrevcache.clear()
1437 1434 obsolete.clearobscaches(self)
1438 1435
1439 1436 def invalidatedirstate(self):
1440 1437 '''Invalidates the dirstate, causing the next call to dirstate
1441 1438 to check if it was modified since the last time it was read,
1442 1439 rereading it if it has.
1443 1440
1444 1441 This is different to dirstate.invalidate() that it doesn't always
1445 1442 rereads the dirstate. Use dirstate.invalidate() if you want to
1446 1443 explicitly read the dirstate again (i.e. restoring it to a previous
1447 1444 known good state).'''
1448 1445 if hasunfilteredcache(self, 'dirstate'):
1449 1446 for k in self.dirstate._filecache:
1450 1447 try:
1451 1448 delattr(self.dirstate, k)
1452 1449 except AttributeError:
1453 1450 pass
1454 1451 delattr(self.unfiltered(), 'dirstate')
1455 1452
1456 1453 def invalidate(self, clearfilecache=False):
1457 1454 '''Invalidates both store and non-store parts other than dirstate
1458 1455
1459 1456 If a transaction is running, invalidation of store is omitted,
1460 1457 because discarding in-memory changes might cause inconsistency
1461 1458 (e.g. incomplete fncache causes unintentional failure, but
1462 1459 redundant one doesn't).
1463 1460 '''
1464 1461 unfiltered = self.unfiltered() # all file caches are stored unfiltered
1465 1462 for k in list(self._filecache.keys()):
1466 1463 # dirstate is invalidated separately in invalidatedirstate()
1467 1464 if k == 'dirstate':
1468 1465 continue
1469 1466
1470 1467 if clearfilecache:
1471 1468 del self._filecache[k]
1472 1469 try:
1473 1470 delattr(unfiltered, k)
1474 1471 except AttributeError:
1475 1472 pass
1476 1473 self.invalidatecaches()
1477 1474 if not self.currenttransaction():
1478 1475 # TODO: Changing contents of store outside transaction
1479 1476 # causes inconsistency. We should make in-memory store
1480 1477 # changes detectable, and abort if changed.
1481 1478 self.store.invalidatecaches()
1482 1479
1483 1480 def invalidateall(self):
1484 1481 '''Fully invalidates both store and non-store parts, causing the
1485 1482 subsequent operation to reread any outside changes.'''
1486 1483 # extension should hook this to invalidate its caches
1487 1484 self.invalidate()
1488 1485 self.invalidatedirstate()
1489 1486
1490 1487 @unfilteredmethod
1491 1488 def _refreshfilecachestats(self, tr):
1492 1489 """Reload stats of cached files so that they are flagged as valid"""
1493 1490 for k, ce in self._filecache.items():
1494 1491 if k == 'dirstate' or k not in self.__dict__:
1495 1492 continue
1496 1493 ce.refresh()
1497 1494
1498 1495 def _lock(self, vfs, lockname, wait, releasefn, acquirefn, desc,
1499 1496 inheritchecker=None, parentenvvar=None):
1500 1497 parentlock = None
1501 1498 # the contents of parentenvvar are used by the underlying lock to
1502 1499 # determine whether it can be inherited
1503 1500 if parentenvvar is not None:
1504 1501 parentlock = encoding.environ.get(parentenvvar)
1505 1502 try:
1506 1503 l = lockmod.lock(vfs, lockname, 0, releasefn=releasefn,
1507 1504 acquirefn=acquirefn, desc=desc,
1508 1505 inheritchecker=inheritchecker,
1509 1506 parentlock=parentlock)
1510 1507 except error.LockHeld as inst:
1511 1508 if not wait:
1512 1509 raise
1513 1510 # show more details for new-style locks
1514 1511 if ':' in inst.locker:
1515 1512 host, pid = inst.locker.split(":", 1)
1516 1513 self.ui.warn(
1517 1514 _("waiting for lock on %s held by process %r "
1518 1515 "on host %r\n") % (desc, pid, host))
1519 1516 else:
1520 1517 self.ui.warn(_("waiting for lock on %s held by %r\n") %
1521 1518 (desc, inst.locker))
1522 1519 # default to 600 seconds timeout
1523 1520 l = lockmod.lock(vfs, lockname,
1524 1521 int(self.ui.config("ui", "timeout")),
1525 1522 releasefn=releasefn, acquirefn=acquirefn,
1526 1523 desc=desc)
1527 1524 self.ui.warn(_("got lock after %s seconds\n") % l.delay)
1528 1525 return l
1529 1526
1530 1527 def _afterlock(self, callback):
1531 1528 """add a callback to be run when the repository is fully unlocked
1532 1529
1533 1530 The callback will be executed when the outermost lock is released
1534 1531 (with wlock being higher level than 'lock')."""
1535 1532 for ref in (self._wlockref, self._lockref):
1536 1533 l = ref and ref()
1537 1534 if l and l.held:
1538 1535 l.postrelease.append(callback)
1539 1536 break
1540 1537 else: # no lock have been found.
1541 1538 callback()
1542 1539
1543 1540 def lock(self, wait=True):
1544 1541 '''Lock the repository store (.hg/store) and return a weak reference
1545 1542 to the lock. Use this before modifying the store (e.g. committing or
1546 1543 stripping). If you are opening a transaction, get a lock as well.)
1547 1544
1548 1545 If both 'lock' and 'wlock' must be acquired, ensure you always acquires
1549 1546 'wlock' first to avoid a dead-lock hazard.'''
1550 1547 l = self._currentlock(self._lockref)
1551 1548 if l is not None:
1552 1549 l.lock()
1553 1550 return l
1554 1551
1555 1552 l = self._lock(self.svfs, "lock", wait, None,
1556 1553 self.invalidate, _('repository %s') % self.origroot)
1557 1554 self._lockref = weakref.ref(l)
1558 1555 return l
1559 1556
1560 1557 def _wlockchecktransaction(self):
1561 1558 if self.currenttransaction() is not None:
1562 1559 raise error.LockInheritanceContractViolation(
1563 1560 'wlock cannot be inherited in the middle of a transaction')
1564 1561
1565 1562 def wlock(self, wait=True):
1566 1563 '''Lock the non-store parts of the repository (everything under
1567 1564 .hg except .hg/store) and return a weak reference to the lock.
1568 1565
1569 1566 Use this before modifying files in .hg.
1570 1567
1571 1568 If both 'lock' and 'wlock' must be acquired, ensure you always acquires
1572 1569 'wlock' first to avoid a dead-lock hazard.'''
1573 1570 l = self._wlockref and self._wlockref()
1574 1571 if l is not None and l.held:
1575 1572 l.lock()
1576 1573 return l
1577 1574
1578 1575 # We do not need to check for non-waiting lock acquisition. Such
1579 1576 # acquisition would not cause dead-lock as they would just fail.
1580 1577 if wait and (self.ui.configbool('devel', 'all-warnings')
1581 1578 or self.ui.configbool('devel', 'check-locks')):
1582 1579 if self._currentlock(self._lockref) is not None:
1583 1580 self.ui.develwarn('"wlock" acquired after "lock"')
1584 1581
1585 1582 def unlock():
1586 1583 if self.dirstate.pendingparentchange():
1587 1584 self.dirstate.invalidate()
1588 1585 else:
1589 1586 self.dirstate.write(None)
1590 1587
1591 1588 self._filecache['dirstate'].refresh()
1592 1589
1593 1590 l = self._lock(self.vfs, "wlock", wait, unlock,
1594 1591 self.invalidatedirstate, _('working directory of %s') %
1595 1592 self.origroot,
1596 1593 inheritchecker=self._wlockchecktransaction,
1597 1594 parentenvvar='HG_WLOCK_LOCKER')
1598 1595 self._wlockref = weakref.ref(l)
1599 1596 return l
1600 1597
1601 1598 def _currentlock(self, lockref):
1602 1599 """Returns the lock if it's held, or None if it's not."""
1603 1600 if lockref is None:
1604 1601 return None
1605 1602 l = lockref()
1606 1603 if l is None or not l.held:
1607 1604 return None
1608 1605 return l
1609 1606
1610 1607 def currentwlock(self):
1611 1608 """Returns the wlock if it's held, or None if it's not."""
1612 1609 return self._currentlock(self._wlockref)
1613 1610
1614 1611 def _filecommit(self, fctx, manifest1, manifest2, linkrev, tr, changelist):
1615 1612 """
1616 1613 commit an individual file as part of a larger transaction
1617 1614 """
1618 1615
1619 1616 fname = fctx.path()
1620 1617 fparent1 = manifest1.get(fname, nullid)
1621 1618 fparent2 = manifest2.get(fname, nullid)
1622 1619 if isinstance(fctx, context.filectx):
1623 1620 node = fctx.filenode()
1624 1621 if node in [fparent1, fparent2]:
1625 1622 self.ui.debug('reusing %s filelog entry\n' % fname)
1626 1623 if manifest1.flags(fname) != fctx.flags():
1627 1624 changelist.append(fname)
1628 1625 return node
1629 1626
1630 1627 flog = self.file(fname)
1631 1628 meta = {}
1632 1629 copy = fctx.renamed()
1633 1630 if copy and copy[0] != fname:
1634 1631 # Mark the new revision of this file as a copy of another
1635 1632 # file. This copy data will effectively act as a parent
1636 1633 # of this new revision. If this is a merge, the first
1637 1634 # parent will be the nullid (meaning "look up the copy data")
1638 1635 # and the second one will be the other parent. For example:
1639 1636 #
1640 1637 # 0 --- 1 --- 3 rev1 changes file foo
1641 1638 # \ / rev2 renames foo to bar and changes it
1642 1639 # \- 2 -/ rev3 should have bar with all changes and
1643 1640 # should record that bar descends from
1644 1641 # bar in rev2 and foo in rev1
1645 1642 #
1646 1643 # this allows this merge to succeed:
1647 1644 #
1648 1645 # 0 --- 1 --- 3 rev4 reverts the content change from rev2
1649 1646 # \ / merging rev3 and rev4 should use bar@rev2
1650 1647 # \- 2 --- 4 as the merge base
1651 1648 #
1652 1649
1653 1650 cfname = copy[0]
1654 1651 crev = manifest1.get(cfname)
1655 1652 newfparent = fparent2
1656 1653
1657 1654 if manifest2: # branch merge
1658 1655 if fparent2 == nullid or crev is None: # copied on remote side
1659 1656 if cfname in manifest2:
1660 1657 crev = manifest2[cfname]
1661 1658 newfparent = fparent1
1662 1659
1663 1660 # Here, we used to search backwards through history to try to find
1664 1661 # where the file copy came from if the source of a copy was not in
1665 1662 # the parent directory. However, this doesn't actually make sense to
1666 1663 # do (what does a copy from something not in your working copy even
1667 1664 # mean?) and it causes bugs (eg, issue4476). Instead, we will warn
1668 1665 # the user that copy information was dropped, so if they didn't
1669 1666 # expect this outcome it can be fixed, but this is the correct
1670 1667 # behavior in this circumstance.
1671 1668
1672 1669 if crev:
1673 1670 self.ui.debug(" %s: copy %s:%s\n" % (fname, cfname, hex(crev)))
1674 1671 meta["copy"] = cfname
1675 1672 meta["copyrev"] = hex(crev)
1676 1673 fparent1, fparent2 = nullid, newfparent
1677 1674 else:
1678 1675 self.ui.warn(_("warning: can't find ancestor for '%s' "
1679 1676 "copied from '%s'!\n") % (fname, cfname))
1680 1677
1681 1678 elif fparent1 == nullid:
1682 1679 fparent1, fparent2 = fparent2, nullid
1683 1680 elif fparent2 != nullid:
1684 1681 # is one parent an ancestor of the other?
1685 1682 fparentancestors = flog.commonancestorsheads(fparent1, fparent2)
1686 1683 if fparent1 in fparentancestors:
1687 1684 fparent1, fparent2 = fparent2, nullid
1688 1685 elif fparent2 in fparentancestors:
1689 1686 fparent2 = nullid
1690 1687
1691 1688 # is the file changed?
1692 1689 text = fctx.data()
1693 1690 if fparent2 != nullid or flog.cmp(fparent1, text) or meta:
1694 1691 changelist.append(fname)
1695 1692 return flog.add(text, meta, tr, linkrev, fparent1, fparent2)
1696 1693 # are just the flags changed during merge?
1697 1694 elif fname in manifest1 and manifest1.flags(fname) != fctx.flags():
1698 1695 changelist.append(fname)
1699 1696
1700 1697 return fparent1
1701 1698
1702 1699 def checkcommitpatterns(self, wctx, vdirs, match, status, fail):
1703 1700 """check for commit arguments that aren't committable"""
1704 1701 if match.isexact() or match.prefix():
1705 1702 matched = set(status.modified + status.added + status.removed)
1706 1703
1707 1704 for f in match.files():
1708 1705 f = self.dirstate.normalize(f)
1709 1706 if f == '.' or f in matched or f in wctx.substate:
1710 1707 continue
1711 1708 if f in status.deleted:
1712 1709 fail(f, _('file not found!'))
1713 1710 if f in vdirs: # visited directory
1714 1711 d = f + '/'
1715 1712 for mf in matched:
1716 1713 if mf.startswith(d):
1717 1714 break
1718 1715 else:
1719 1716 fail(f, _("no match under directory!"))
1720 1717 elif f not in self.dirstate:
1721 1718 fail(f, _("file not tracked!"))
1722 1719
1723 1720 @unfilteredmethod
1724 1721 def commit(self, text="", user=None, date=None, match=None, force=False,
1725 1722 editor=False, extra=None):
1726 1723 """Add a new revision to current repository.
1727 1724
1728 1725 Revision information is gathered from the working directory,
1729 1726 match can be used to filter the committed files. If editor is
1730 1727 supplied, it is called to get a commit message.
1731 1728 """
1732 1729 if extra is None:
1733 1730 extra = {}
1734 1731
1735 1732 def fail(f, msg):
1736 1733 raise error.Abort('%s: %s' % (f, msg))
1737 1734
1738 1735 if not match:
1739 1736 match = matchmod.always(self.root, '')
1740 1737
1741 1738 if not force:
1742 1739 vdirs = []
1743 1740 match.explicitdir = vdirs.append
1744 1741 match.bad = fail
1745 1742
1746 1743 wlock = lock = tr = None
1747 1744 try:
1748 1745 wlock = self.wlock()
1749 1746 lock = self.lock() # for recent changelog (see issue4368)
1750 1747
1751 1748 wctx = self[None]
1752 1749 merge = len(wctx.parents()) > 1
1753 1750
1754 1751 if not force and merge and not match.always():
1755 1752 raise error.Abort(_('cannot partially commit a merge '
1756 1753 '(do not specify files or patterns)'))
1757 1754
1758 1755 status = self.status(match=match, clean=force)
1759 1756 if force:
1760 1757 status.modified.extend(status.clean) # mq may commit clean files
1761 1758
1762 1759 # check subrepos
1763 1760 subs = []
1764 1761 commitsubs = set()
1765 1762 newstate = wctx.substate.copy()
1766 1763 # only manage subrepos and .hgsubstate if .hgsub is present
1767 1764 if '.hgsub' in wctx:
1768 1765 # we'll decide whether to track this ourselves, thanks
1769 1766 for c in status.modified, status.added, status.removed:
1770 1767 if '.hgsubstate' in c:
1771 1768 c.remove('.hgsubstate')
1772 1769
1773 1770 # compare current state to last committed state
1774 1771 # build new substate based on last committed state
1775 1772 oldstate = wctx.p1().substate
1776 1773 for s in sorted(newstate.keys()):
1777 1774 if not match(s):
1778 1775 # ignore working copy, use old state if present
1779 1776 if s in oldstate:
1780 1777 newstate[s] = oldstate[s]
1781 1778 continue
1782 1779 if not force:
1783 1780 raise error.Abort(
1784 1781 _("commit with new subrepo %s excluded") % s)
1785 1782 dirtyreason = wctx.sub(s).dirtyreason(True)
1786 1783 if dirtyreason:
1787 1784 if not self.ui.configbool('ui', 'commitsubrepos'):
1788 1785 raise error.Abort(dirtyreason,
1789 1786 hint=_("use --subrepos for recursive commit"))
1790 1787 subs.append(s)
1791 1788 commitsubs.add(s)
1792 1789 else:
1793 1790 bs = wctx.sub(s).basestate()
1794 1791 newstate[s] = (newstate[s][0], bs, newstate[s][2])
1795 1792 if oldstate.get(s, (None, None, None))[1] != bs:
1796 1793 subs.append(s)
1797 1794
1798 1795 # check for removed subrepos
1799 1796 for p in wctx.parents():
1800 1797 r = [s for s in p.substate if s not in newstate]
1801 1798 subs += [s for s in r if match(s)]
1802 1799 if subs:
1803 1800 if (not match('.hgsub') and
1804 1801 '.hgsub' in (wctx.modified() + wctx.added())):
1805 1802 raise error.Abort(
1806 1803 _("can't commit subrepos without .hgsub"))
1807 1804 status.modified.insert(0, '.hgsubstate')
1808 1805
1809 1806 elif '.hgsub' in status.removed:
1810 1807 # clean up .hgsubstate when .hgsub is removed
1811 1808 if ('.hgsubstate' in wctx and
1812 1809 '.hgsubstate' not in (status.modified + status.added +
1813 1810 status.removed)):
1814 1811 status.removed.insert(0, '.hgsubstate')
1815 1812
1816 1813 # make sure all explicit patterns are matched
1817 1814 if not force:
1818 1815 self.checkcommitpatterns(wctx, vdirs, match, status, fail)
1819 1816
1820 1817 cctx = context.workingcommitctx(self, status,
1821 1818 text, user, date, extra)
1822 1819
1823 1820 # internal config: ui.allowemptycommit
1824 1821 allowemptycommit = (wctx.branch() != wctx.p1().branch()
1825 1822 or extra.get('close') or merge or cctx.files()
1826 1823 or self.ui.configbool('ui', 'allowemptycommit'))
1827 1824 if not allowemptycommit:
1828 1825 return None
1829 1826
1830 1827 if merge and cctx.deleted():
1831 1828 raise error.Abort(_("cannot commit merge with missing files"))
1832 1829
1833 1830 ms = mergemod.mergestate.read(self)
1834 1831 mergeutil.checkunresolved(ms)
1835 1832
1836 1833 if editor:
1837 1834 cctx._text = editor(self, cctx, subs)
1838 1835 edited = (text != cctx._text)
1839 1836
1840 1837 # Save commit message in case this transaction gets rolled back
1841 1838 # (e.g. by a pretxncommit hook). Leave the content alone on
1842 1839 # the assumption that the user will use the same editor again.
1843 1840 msgfn = self.savecommitmessage(cctx._text)
1844 1841
1845 1842 # commit subs and write new state
1846 1843 if subs:
1847 1844 for s in sorted(commitsubs):
1848 1845 sub = wctx.sub(s)
1849 1846 self.ui.status(_('committing subrepository %s\n') %
1850 1847 subrepo.subrelpath(sub))
1851 1848 sr = sub.commit(cctx._text, user, date)
1852 1849 newstate[s] = (newstate[s][0], sr)
1853 1850 subrepo.writestate(self, newstate)
1854 1851
1855 1852 p1, p2 = self.dirstate.parents()
1856 1853 hookp1, hookp2 = hex(p1), (p2 != nullid and hex(p2) or '')
1857 1854 try:
1858 1855 self.hook("precommit", throw=True, parent1=hookp1,
1859 1856 parent2=hookp2)
1860 1857 tr = self.transaction('commit')
1861 1858 ret = self.commitctx(cctx, True)
1862 1859 except: # re-raises
1863 1860 if edited:
1864 1861 self.ui.write(
1865 1862 _('note: commit message saved in %s\n') % msgfn)
1866 1863 raise
1867 1864 # update bookmarks, dirstate and mergestate
1868 1865 bookmarks.update(self, [p1, p2], ret)
1869 1866 cctx.markcommitted(ret)
1870 1867 ms.reset()
1871 1868 tr.close()
1872 1869
1873 1870 finally:
1874 1871 lockmod.release(tr, lock, wlock)
1875 1872
1876 1873 def commithook(node=hex(ret), parent1=hookp1, parent2=hookp2):
1877 1874 # hack for command that use a temporary commit (eg: histedit)
1878 1875 # temporary commit got stripped before hook release
1879 1876 if self.changelog.hasnode(ret):
1880 1877 self.hook("commit", node=node, parent1=parent1,
1881 1878 parent2=parent2)
1882 1879 self._afterlock(commithook)
1883 1880 return ret
1884 1881
1885 1882 @unfilteredmethod
1886 1883 def commitctx(self, ctx, error=False):
1887 1884 """Add a new revision to current repository.
1888 1885 Revision information is passed via the context argument.
1889 1886 """
1890 1887
1891 1888 tr = None
1892 1889 p1, p2 = ctx.p1(), ctx.p2()
1893 1890 user = ctx.user()
1894 1891
1895 1892 lock = self.lock()
1896 1893 try:
1897 1894 tr = self.transaction("commit")
1898 1895 trp = weakref.proxy(tr)
1899 1896
1900 1897 if ctx.manifestnode():
1901 1898 # reuse an existing manifest revision
1902 1899 mn = ctx.manifestnode()
1903 1900 files = ctx.files()
1904 1901 elif ctx.files():
1905 1902 m1ctx = p1.manifestctx()
1906 1903 m2ctx = p2.manifestctx()
1907 1904 mctx = m1ctx.copy()
1908 1905
1909 1906 m = mctx.read()
1910 1907 m1 = m1ctx.read()
1911 1908 m2 = m2ctx.read()
1912 1909
1913 1910 # check in files
1914 1911 added = []
1915 1912 changed = []
1916 1913 removed = list(ctx.removed())
1917 1914 linkrev = len(self)
1918 1915 self.ui.note(_("committing files:\n"))
1919 1916 for f in sorted(ctx.modified() + ctx.added()):
1920 1917 self.ui.note(f + "\n")
1921 1918 try:
1922 1919 fctx = ctx[f]
1923 1920 if fctx is None:
1924 1921 removed.append(f)
1925 1922 else:
1926 1923 added.append(f)
1927 1924 m[f] = self._filecommit(fctx, m1, m2, linkrev,
1928 1925 trp, changed)
1929 1926 m.setflag(f, fctx.flags())
1930 1927 except OSError as inst:
1931 1928 self.ui.warn(_("trouble committing %s!\n") % f)
1932 1929 raise
1933 1930 except IOError as inst:
1934 1931 errcode = getattr(inst, 'errno', errno.ENOENT)
1935 1932 if error or errcode and errcode != errno.ENOENT:
1936 1933 self.ui.warn(_("trouble committing %s!\n") % f)
1937 1934 raise
1938 1935
1939 1936 # update manifest
1940 1937 self.ui.note(_("committing manifest\n"))
1941 1938 removed = [f for f in sorted(removed) if f in m1 or f in m2]
1942 1939 drop = [f for f in removed if f in m]
1943 1940 for f in drop:
1944 1941 del m[f]
1945 1942 mn = mctx.write(trp, linkrev,
1946 1943 p1.manifestnode(), p2.manifestnode(),
1947 1944 added, drop)
1948 1945 files = changed + removed
1949 1946 else:
1950 1947 mn = p1.manifestnode()
1951 1948 files = []
1952 1949
1953 1950 # update changelog
1954 1951 self.ui.note(_("committing changelog\n"))
1955 1952 self.changelog.delayupdate(tr)
1956 1953 n = self.changelog.add(mn, files, ctx.description(),
1957 1954 trp, p1.node(), p2.node(),
1958 1955 user, ctx.date(), ctx.extra().copy())
1959 1956 xp1, xp2 = p1.hex(), p2 and p2.hex() or ''
1960 1957 self.hook('pretxncommit', throw=True, node=hex(n), parent1=xp1,
1961 1958 parent2=xp2)
1962 1959 # set the new commit is proper phase
1963 1960 targetphase = subrepo.newcommitphase(self.ui, ctx)
1964 1961 if targetphase:
1965 1962 # retract boundary do not alter parent changeset.
1966 1963 # if a parent have higher the resulting phase will
1967 1964 # be compliant anyway
1968 1965 #
1969 1966 # if minimal phase was 0 we don't need to retract anything
1970 1967 phases.registernew(self, tr, targetphase, [n])
1971 1968 tr.close()
1972 1969 return n
1973 1970 finally:
1974 1971 if tr:
1975 1972 tr.release()
1976 1973 lock.release()
1977 1974
1978 1975 @unfilteredmethod
1979 1976 def destroying(self):
1980 1977 '''Inform the repository that nodes are about to be destroyed.
1981 1978 Intended for use by strip and rollback, so there's a common
1982 1979 place for anything that has to be done before destroying history.
1983 1980
1984 1981 This is mostly useful for saving state that is in memory and waiting
1985 1982 to be flushed when the current lock is released. Because a call to
1986 1983 destroyed is imminent, the repo will be invalidated causing those
1987 1984 changes to stay in memory (waiting for the next unlock), or vanish
1988 1985 completely.
1989 1986 '''
1990 1987 # When using the same lock to commit and strip, the phasecache is left
1991 1988 # dirty after committing. Then when we strip, the repo is invalidated,
1992 1989 # causing those changes to disappear.
1993 1990 if '_phasecache' in vars(self):
1994 1991 self._phasecache.write()
1995 1992
1996 1993 @unfilteredmethod
1997 1994 def destroyed(self):
1998 1995 '''Inform the repository that nodes have been destroyed.
1999 1996 Intended for use by strip and rollback, so there's a common
2000 1997 place for anything that has to be done after destroying history.
2001 1998 '''
2002 1999 # When one tries to:
2003 2000 # 1) destroy nodes thus calling this method (e.g. strip)
2004 2001 # 2) use phasecache somewhere (e.g. commit)
2005 2002 #
2006 2003 # then 2) will fail because the phasecache contains nodes that were
2007 2004 # removed. We can either remove phasecache from the filecache,
2008 2005 # causing it to reload next time it is accessed, or simply filter
2009 2006 # the removed nodes now and write the updated cache.
2010 2007 self._phasecache.filterunknown(self)
2011 2008 self._phasecache.write()
2012 2009
2013 2010 # refresh all repository caches
2014 2011 self.updatecaches()
2015 2012
2016 2013 # Ensure the persistent tag cache is updated. Doing it now
2017 2014 # means that the tag cache only has to worry about destroyed
2018 2015 # heads immediately after a strip/rollback. That in turn
2019 2016 # guarantees that "cachetip == currenttip" (comparing both rev
2020 2017 # and node) always means no nodes have been added or destroyed.
2021 2018
2022 2019 # XXX this is suboptimal when qrefresh'ing: we strip the current
2023 2020 # head, refresh the tag cache, then immediately add a new head.
2024 2021 # But I think doing it this way is necessary for the "instant
2025 2022 # tag cache retrieval" case to work.
2026 2023 self.invalidate()
2027 2024
2028 2025 def walk(self, match, node=None):
2029 2026 '''
2030 2027 walk recursively through the directory tree or a given
2031 2028 changeset, finding all files matched by the match
2032 2029 function
2033 2030 '''
2034 2031 self.ui.deprecwarn('use repo[node].walk instead of repo.walk', '4.3')
2035 2032 return self[node].walk(match)
2036 2033
2037 2034 def status(self, node1='.', node2=None, match=None,
2038 2035 ignored=False, clean=False, unknown=False,
2039 2036 listsubrepos=False):
2040 2037 '''a convenience method that calls node1.status(node2)'''
2041 2038 return self[node1].status(node2, match, ignored, clean, unknown,
2042 2039 listsubrepos)
2043 2040
2044 2041 def addpostdsstatus(self, ps):
2045 2042 """Add a callback to run within the wlock, at the point at which status
2046 2043 fixups happen.
2047 2044
2048 2045 On status completion, callback(wctx, status) will be called with the
2049 2046 wlock held, unless the dirstate has changed from underneath or the wlock
2050 2047 couldn't be grabbed.
2051 2048
2052 2049 Callbacks should not capture and use a cached copy of the dirstate --
2053 2050 it might change in the meanwhile. Instead, they should access the
2054 2051 dirstate via wctx.repo().dirstate.
2055 2052
2056 2053 This list is emptied out after each status run -- extensions should
2057 2054 make sure it adds to this list each time dirstate.status is called.
2058 2055 Extensions should also make sure they don't call this for statuses
2059 2056 that don't involve the dirstate.
2060 2057 """
2061 2058
2062 2059 # The list is located here for uniqueness reasons -- it is actually
2063 2060 # managed by the workingctx, but that isn't unique per-repo.
2064 2061 self._postdsstatus.append(ps)
2065 2062
2066 2063 def postdsstatus(self):
2067 2064 """Used by workingctx to get the list of post-dirstate-status hooks."""
2068 2065 return self._postdsstatus
2069 2066
2070 2067 def clearpostdsstatus(self):
2071 2068 """Used by workingctx to clear post-dirstate-status hooks."""
2072 2069 del self._postdsstatus[:]
2073 2070
2074 2071 def heads(self, start=None):
2075 2072 if start is None:
2076 2073 cl = self.changelog
2077 2074 headrevs = reversed(cl.headrevs())
2078 2075 return [cl.node(rev) for rev in headrevs]
2079 2076
2080 2077 heads = self.changelog.heads(start)
2081 2078 # sort the output in rev descending order
2082 2079 return sorted(heads, key=self.changelog.rev, reverse=True)
2083 2080
2084 2081 def branchheads(self, branch=None, start=None, closed=False):
2085 2082 '''return a (possibly filtered) list of heads for the given branch
2086 2083
2087 2084 Heads are returned in topological order, from newest to oldest.
2088 2085 If branch is None, use the dirstate branch.
2089 2086 If start is not None, return only heads reachable from start.
2090 2087 If closed is True, return heads that are marked as closed as well.
2091 2088 '''
2092 2089 if branch is None:
2093 2090 branch = self[None].branch()
2094 2091 branches = self.branchmap()
2095 2092 if branch not in branches:
2096 2093 return []
2097 2094 # the cache returns heads ordered lowest to highest
2098 2095 bheads = list(reversed(branches.branchheads(branch, closed=closed)))
2099 2096 if start is not None:
2100 2097 # filter out the heads that cannot be reached from startrev
2101 2098 fbheads = set(self.changelog.nodesbetween([start], bheads)[2])
2102 2099 bheads = [h for h in bheads if h in fbheads]
2103 2100 return bheads
2104 2101
2105 2102 def branches(self, nodes):
2106 2103 if not nodes:
2107 2104 nodes = [self.changelog.tip()]
2108 2105 b = []
2109 2106 for n in nodes:
2110 2107 t = n
2111 2108 while True:
2112 2109 p = self.changelog.parents(n)
2113 2110 if p[1] != nullid or p[0] == nullid:
2114 2111 b.append((t, n, p[0], p[1]))
2115 2112 break
2116 2113 n = p[0]
2117 2114 return b
2118 2115
2119 2116 def between(self, pairs):
2120 2117 r = []
2121 2118
2122 2119 for top, bottom in pairs:
2123 2120 n, l, i = top, [], 0
2124 2121 f = 1
2125 2122
2126 2123 while n != bottom and n != nullid:
2127 2124 p = self.changelog.parents(n)[0]
2128 2125 if i == f:
2129 2126 l.append(n)
2130 2127 f = f * 2
2131 2128 n = p
2132 2129 i += 1
2133 2130
2134 2131 r.append(l)
2135 2132
2136 2133 return r
2137 2134
2138 2135 def checkpush(self, pushop):
2139 2136 """Extensions can override this function if additional checks have
2140 2137 to be performed before pushing, or call it if they override push
2141 2138 command.
2142 2139 """
2143 2140 pass
2144 2141
2145 2142 @unfilteredpropertycache
2146 2143 def prepushoutgoinghooks(self):
2147 2144 """Return util.hooks consists of a pushop with repo, remote, outgoing
2148 2145 methods, which are called before pushing changesets.
2149 2146 """
2150 2147 return util.hooks()
2151 2148
2152 2149 def pushkey(self, namespace, key, old, new):
2153 2150 try:
2154 2151 tr = self.currenttransaction()
2155 2152 hookargs = {}
2156 2153 if tr is not None:
2157 2154 hookargs.update(tr.hookargs)
2158 2155 hookargs['namespace'] = namespace
2159 2156 hookargs['key'] = key
2160 2157 hookargs['old'] = old
2161 2158 hookargs['new'] = new
2162 2159 self.hook('prepushkey', throw=True, **hookargs)
2163 2160 except error.HookAbort as exc:
2164 2161 self.ui.write_err(_("pushkey-abort: %s\n") % exc)
2165 2162 if exc.hint:
2166 2163 self.ui.write_err(_("(%s)\n") % exc.hint)
2167 2164 return False
2168 2165 self.ui.debug('pushing key for "%s:%s"\n' % (namespace, key))
2169 2166 ret = pushkey.push(self, namespace, key, old, new)
2170 2167 def runhook():
2171 2168 self.hook('pushkey', namespace=namespace, key=key, old=old, new=new,
2172 2169 ret=ret)
2173 2170 self._afterlock(runhook)
2174 2171 return ret
2175 2172
2176 2173 def listkeys(self, namespace):
2177 2174 self.hook('prelistkeys', throw=True, namespace=namespace)
2178 2175 self.ui.debug('listing keys for "%s"\n' % namespace)
2179 2176 values = pushkey.list(self, namespace)
2180 2177 self.hook('listkeys', namespace=namespace, values=values)
2181 2178 return values
2182 2179
2183 2180 def debugwireargs(self, one, two, three=None, four=None, five=None):
2184 2181 '''used to test argument passing over the wire'''
2185 2182 return "%s %s %s %s %s" % (one, two, three, four, five)
2186 2183
2187 2184 def savecommitmessage(self, text):
2188 2185 fp = self.vfs('last-message.txt', 'wb')
2189 2186 try:
2190 2187 fp.write(text)
2191 2188 finally:
2192 2189 fp.close()
2193 2190 return self.pathto(fp.name[len(self.root) + 1:])
2194 2191
2195 2192 # used to avoid circular references so destructors work
2196 2193 def aftertrans(files):
2197 2194 renamefiles = [tuple(t) for t in files]
2198 2195 def a():
2199 2196 for vfs, src, dest in renamefiles:
2200 2197 # if src and dest refer to a same file, vfs.rename is a no-op,
2201 2198 # leaving both src and dest on disk. delete dest to make sure
2202 2199 # the rename couldn't be such a no-op.
2203 2200 vfs.tryunlink(dest)
2204 2201 try:
2205 2202 vfs.rename(src, dest)
2206 2203 except OSError: # journal file does not yet exist
2207 2204 pass
2208 2205 return a
2209 2206
2210 2207 def undoname(fn):
2211 2208 base, name = os.path.split(fn)
2212 2209 assert name.startswith('journal')
2213 2210 return os.path.join(base, name.replace('journal', 'undo', 1))
2214 2211
2215 2212 def instance(ui, path, create):
2216 2213 return localrepository(ui, util.urllocalpath(path), create)
2217 2214
2218 2215 def islocal(path):
2219 2216 return True
2220 2217
2221 2218 def newreporequirements(repo):
2222 2219 """Determine the set of requirements for a new local repository.
2223 2220
2224 2221 Extensions can wrap this function to specify custom requirements for
2225 2222 new repositories.
2226 2223 """
2227 2224 ui = repo.ui
2228 2225 requirements = {'revlogv1'}
2229 2226 if ui.configbool('format', 'usestore'):
2230 2227 requirements.add('store')
2231 2228 if ui.configbool('format', 'usefncache'):
2232 2229 requirements.add('fncache')
2233 2230 if ui.configbool('format', 'dotencode'):
2234 2231 requirements.add('dotencode')
2235 2232
2236 2233 compengine = ui.config('experimental', 'format.compression')
2237 2234 if compengine not in util.compengines:
2238 2235 raise error.Abort(_('compression engine %s defined by '
2239 2236 'experimental.format.compression not available') %
2240 2237 compengine,
2241 2238 hint=_('run "hg debuginstall" to list available '
2242 2239 'compression engines'))
2243 2240
2244 2241 # zlib is the historical default and doesn't need an explicit requirement.
2245 2242 if compengine != 'zlib':
2246 2243 requirements.add('exp-compression-%s' % compengine)
2247 2244
2248 2245 if scmutil.gdinitconfig(ui):
2249 2246 requirements.add('generaldelta')
2250 2247 if ui.configbool('experimental', 'treemanifest'):
2251 2248 requirements.add('treemanifest')
2252 2249 if ui.configbool('experimental', 'manifestv2'):
2253 2250 requirements.add('manifestv2')
2254 2251
2255 2252 revlogv2 = ui.config('experimental', 'revlogv2')
2256 2253 if revlogv2 == 'enable-unstable-format-and-corrupt-my-data':
2257 2254 requirements.remove('revlogv1')
2258 2255 # generaldelta is implied by revlogv2.
2259 2256 requirements.discard('generaldelta')
2260 2257 requirements.add(REVLOGV2_REQUIREMENT)
2261 2258
2262 2259 return requirements
@@ -1,369 +1,325 b''
1 1 # sshpeer.py - ssh repository proxy class for mercurial
2 2 #
3 3 # Copyright 2005, 2006 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 re
11 11
12 12 from .i18n import _
13 13 from . import (
14 14 error,
15 15 pycompat,
16 16 util,
17 17 wireproto,
18 18 )
19 19
20 class remotelock(object):
21 def __init__(self, repo):
22 self.repo = repo
23 def release(self):
24 self.repo.unlock()
25 self.repo = None
26 def __enter__(self):
27 return self
28 def __exit__(self, exc_type, exc_val, exc_tb):
29 if self.repo:
30 self.release()
31 def __del__(self):
32 if self.repo:
33 self.release()
34
35 20 def _serverquote(s):
36 21 if not s:
37 22 return s
38 23 '''quote a string for the remote shell ... which we assume is sh'''
39 24 if re.match('[a-zA-Z0-9@%_+=:,./-]*$', s):
40 25 return s
41 26 return "'%s'" % s.replace("'", "'\\''")
42 27
43 28 def _forwardoutput(ui, pipe):
44 29 """display all data currently available on pipe as remote output.
45 30
46 31 This is non blocking."""
47 32 s = util.readpipe(pipe)
48 33 if s:
49 34 for l in s.splitlines():
50 35 ui.status(_("remote: "), l, '\n')
51 36
52 37 class doublepipe(object):
53 38 """Operate a side-channel pipe in addition of a main one
54 39
55 40 The side-channel pipe contains server output to be forwarded to the user
56 41 input. The double pipe will behave as the "main" pipe, but will ensure the
57 42 content of the "side" pipe is properly processed while we wait for blocking
58 43 call on the "main" pipe.
59 44
60 45 If large amounts of data are read from "main", the forward will cease after
61 46 the first bytes start to appear. This simplifies the implementation
62 47 without affecting actual output of sshpeer too much as we rarely issue
63 48 large read for data not yet emitted by the server.
64 49
65 50 The main pipe is expected to be a 'bufferedinputpipe' from the util module
66 51 that handle all the os specific bits. This class lives in this module
67 52 because it focus on behavior specific to the ssh protocol."""
68 53
69 54 def __init__(self, ui, main, side):
70 55 self._ui = ui
71 56 self._main = main
72 57 self._side = side
73 58
74 59 def _wait(self):
75 60 """wait until some data are available on main or side
76 61
77 62 return a pair of boolean (ismainready, issideready)
78 63
79 64 (This will only wait for data if the setup is supported by `util.poll`)
80 65 """
81 66 if getattr(self._main, 'hasbuffer', False): # getattr for classic pipe
82 67 return (True, True) # main has data, assume side is worth poking at.
83 68 fds = [self._main.fileno(), self._side.fileno()]
84 69 try:
85 70 act = util.poll(fds)
86 71 except NotImplementedError:
87 72 # non supported yet case, assume all have data.
88 73 act = fds
89 74 return (self._main.fileno() in act, self._side.fileno() in act)
90 75
91 76 def write(self, data):
92 77 return self._call('write', data)
93 78
94 79 def read(self, size):
95 80 r = self._call('read', size)
96 81 if size != 0 and not r:
97 82 # We've observed a condition that indicates the
98 83 # stdout closed unexpectedly. Check stderr one
99 84 # more time and snag anything that's there before
100 85 # letting anyone know the main part of the pipe
101 86 # closed prematurely.
102 87 _forwardoutput(self._ui, self._side)
103 88 return r
104 89
105 90 def readline(self):
106 91 return self._call('readline')
107 92
108 93 def _call(self, methname, data=None):
109 94 """call <methname> on "main", forward output of "side" while blocking
110 95 """
111 96 # data can be '' or 0
112 97 if (data is not None and not data) or self._main.closed:
113 98 _forwardoutput(self._ui, self._side)
114 99 return ''
115 100 while True:
116 101 mainready, sideready = self._wait()
117 102 if sideready:
118 103 _forwardoutput(self._ui, self._side)
119 104 if mainready:
120 105 meth = getattr(self._main, methname)
121 106 if data is None:
122 107 return meth()
123 108 else:
124 109 return meth(data)
125 110
126 111 def close(self):
127 112 return self._main.close()
128 113
129 114 def flush(self):
130 115 return self._main.flush()
131 116
132 117 class sshpeer(wireproto.wirepeer):
133 118 def __init__(self, ui, path, create=False):
134 119 self._url = path
135 120 self.ui = ui
136 121 self.pipeo = self.pipei = self.pipee = None
137 122
138 123 u = util.url(path, parsequery=False, parsefragment=False)
139 124 if u.scheme != 'ssh' or not u.host or u.path is None:
140 125 self._abort(error.RepoError(_("couldn't parse location %s") % path))
141 126
142 127 self.user = u.user
143 128 if u.passwd is not None:
144 129 self._abort(error.RepoError(_("password in URL not supported")))
145 130 self.host = u.host
146 131 self.port = u.port
147 132 self.path = u.path or "."
148 133
149 134 sshcmd = self.ui.config("ui", "ssh")
150 135 remotecmd = self.ui.config("ui", "remotecmd")
151 136
152 137 args = util.sshargs(sshcmd,
153 138 _serverquote(self.host),
154 139 _serverquote(self.user),
155 140 _serverquote(self.port))
156 141
157 142 if create:
158 143 cmd = '%s %s %s' % (sshcmd, args,
159 144 util.shellquote("%s init %s" %
160 145 (_serverquote(remotecmd), _serverquote(self.path))))
161 146 ui.debug('running %s\n' % cmd)
162 147 res = ui.system(cmd, blockedtag='sshpeer')
163 148 if res != 0:
164 149 self._abort(error.RepoError(_("could not create remote repo")))
165 150
166 151 self._validaterepo(sshcmd, args, remotecmd)
167 152
168 153 def url(self):
169 154 return self._url
170 155
171 156 def _validaterepo(self, sshcmd, args, remotecmd):
172 157 # cleanup up previous run
173 158 self.cleanup()
174 159
175 160 cmd = '%s %s %s' % (sshcmd, args,
176 161 util.shellquote("%s -R %s serve --stdio" %
177 162 (_serverquote(remotecmd), _serverquote(self.path))))
178 163 self.ui.debug('running %s\n' % cmd)
179 164 cmd = util.quotecommand(cmd)
180 165
181 166 # while self.subprocess isn't used, having it allows the subprocess to
182 167 # to clean up correctly later
183 168 #
184 169 # no buffer allow the use of 'select'
185 170 # feel free to remove buffering and select usage when we ultimately
186 171 # move to threading.
187 172 sub = util.popen4(cmd, bufsize=0)
188 173 self.pipeo, self.pipei, self.pipee, self.subprocess = sub
189 174
190 175 self.pipei = util.bufferedinputpipe(self.pipei)
191 176 self.pipei = doublepipe(self.ui, self.pipei, self.pipee)
192 177 self.pipeo = doublepipe(self.ui, self.pipeo, self.pipee)
193 178
194 179 # skip any noise generated by remote shell
195 180 self._callstream("hello")
196 181 r = self._callstream("between", pairs=("%s-%s" % ("0"*40, "0"*40)))
197 182 lines = ["", "dummy"]
198 183 max_noise = 500
199 184 while lines[-1] and max_noise:
200 185 l = r.readline()
201 186 self.readerr()
202 187 if lines[-1] == "1\n" and l == "\n":
203 188 break
204 189 if l:
205 190 self.ui.debug("remote: ", l)
206 191 lines.append(l)
207 192 max_noise -= 1
208 193 else:
209 194 self._abort(error.RepoError(_('no suitable response from '
210 195 'remote hg')))
211 196
212 197 self._caps = set()
213 198 for l in reversed(lines):
214 199 if l.startswith("capabilities:"):
215 200 self._caps.update(l[:-1].split(":")[1].split())
216 201 break
217 202
218 203 def _capabilities(self):
219 204 return self._caps
220 205
221 206 def readerr(self):
222 207 _forwardoutput(self.ui, self.pipee)
223 208
224 209 def _abort(self, exception):
225 210 self.cleanup()
226 211 raise exception
227 212
228 213 def cleanup(self):
229 214 if self.pipeo is None:
230 215 return
231 216 self.pipeo.close()
232 217 self.pipei.close()
233 218 try:
234 219 # read the error descriptor until EOF
235 220 for l in self.pipee:
236 221 self.ui.status(_("remote: "), l)
237 222 except (IOError, ValueError):
238 223 pass
239 224 self.pipee.close()
240 225
241 226 __del__ = cleanup
242 227
243 228 def _submitbatch(self, req):
244 229 rsp = self._callstream("batch", cmds=wireproto.encodebatchcmds(req))
245 230 available = self._getamount()
246 231 # TODO this response parsing is probably suboptimal for large
247 232 # batches with large responses.
248 233 toread = min(available, 1024)
249 234 work = rsp.read(toread)
250 235 available -= toread
251 236 chunk = work
252 237 while chunk:
253 238 while ';' in work:
254 239 one, work = work.split(';', 1)
255 240 yield wireproto.unescapearg(one)
256 241 toread = min(available, 1024)
257 242 chunk = rsp.read(toread)
258 243 available -= toread
259 244 work += chunk
260 245 yield wireproto.unescapearg(work)
261 246
262 247 def _callstream(self, cmd, **args):
263 248 args = pycompat.byteskwargs(args)
264 249 self.ui.debug("sending %s command\n" % cmd)
265 250 self.pipeo.write("%s\n" % cmd)
266 251 _func, names = wireproto.commands[cmd]
267 252 keys = names.split()
268 253 wireargs = {}
269 254 for k in keys:
270 255 if k == '*':
271 256 wireargs['*'] = args
272 257 break
273 258 else:
274 259 wireargs[k] = args[k]
275 260 del args[k]
276 261 for k, v in sorted(wireargs.iteritems()):
277 262 self.pipeo.write("%s %d\n" % (k, len(v)))
278 263 if isinstance(v, dict):
279 264 for dk, dv in v.iteritems():
280 265 self.pipeo.write("%s %d\n" % (dk, len(dv)))
281 266 self.pipeo.write(dv)
282 267 else:
283 268 self.pipeo.write(v)
284 269 self.pipeo.flush()
285 270
286 271 return self.pipei
287 272
288 273 def _callcompressable(self, cmd, **args):
289 274 return self._callstream(cmd, **args)
290 275
291 276 def _call(self, cmd, **args):
292 277 self._callstream(cmd, **args)
293 278 return self._recv()
294 279
295 280 def _callpush(self, cmd, fp, **args):
296 281 r = self._call(cmd, **args)
297 282 if r:
298 283 return '', r
299 284 for d in iter(lambda: fp.read(4096), ''):
300 285 self._send(d)
301 286 self._send("", flush=True)
302 287 r = self._recv()
303 288 if r:
304 289 return '', r
305 290 return self._recv(), ''
306 291
307 292 def _calltwowaystream(self, cmd, fp, **args):
308 293 r = self._call(cmd, **args)
309 294 if r:
310 295 # XXX needs to be made better
311 296 raise error.Abort(_('unexpected remote reply: %s') % r)
312 297 for d in iter(lambda: fp.read(4096), ''):
313 298 self._send(d)
314 299 self._send("", flush=True)
315 300 return self.pipei
316 301
317 302 def _getamount(self):
318 303 l = self.pipei.readline()
319 304 if l == '\n':
320 305 self.readerr()
321 306 msg = _('check previous remote output')
322 307 self._abort(error.OutOfBandError(hint=msg))
323 308 self.readerr()
324 309 try:
325 310 return int(l)
326 311 except ValueError:
327 312 self._abort(error.ResponseError(_("unexpected response:"), l))
328 313
329 314 def _recv(self):
330 315 return self.pipei.read(self._getamount())
331 316
332 317 def _send(self, data, flush=False):
333 318 self.pipeo.write("%d\n" % len(data))
334 319 if data:
335 320 self.pipeo.write(data)
336 321 if flush:
337 322 self.pipeo.flush()
338 323 self.readerr()
339 324
340 def lock(self):
341 self._call("lock")
342 return remotelock(self)
343
344 def unlock(self):
345 self._call("unlock")
346
347 def addchangegroup(self, cg, source, url, lock=None):
348 '''Send a changegroup to the remote server. Return an integer
349 similar to unbundle(). DEPRECATED, since it requires locking the
350 remote.'''
351 d = self._call("addchangegroup")
352 if d:
353 self._abort(error.RepoError(_("push refused: %s") % d))
354 for d in iter(lambda: cg.read(4096), ''):
355 self.pipeo.write(d)
356 self.readerr()
357
358 self.pipeo.flush()
359
360 self.readerr()
361 r = self._recv()
362 if not r:
363 return 1
364 try:
365 return int(r)
366 except ValueError:
367 self._abort(error.ResponseError(_("unexpected response:"), r))
368
369 325 instance = sshpeer
General Comments 0
You need to be logged in to leave comments. Login now