##// END OF EJS Templates
py3: convert known-int values to bytes using %d...
Augie Fackler -
r36441:2831d918 default
parent child Browse files
Show More
@@ -1,2263 +1,2263 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 collections
11 11 import errno
12 12 import hashlib
13 13
14 14 from .i18n import _
15 15 from .node import (
16 16 bin,
17 17 hex,
18 18 nullid,
19 19 )
20 20 from . import (
21 21 bookmarks as bookmod,
22 22 bundle2,
23 23 changegroup,
24 24 discovery,
25 25 error,
26 26 lock as lockmod,
27 27 logexchange,
28 28 obsolete,
29 29 phases,
30 30 pushkey,
31 31 pycompat,
32 32 scmutil,
33 33 sslutil,
34 34 streamclone,
35 35 url as urlmod,
36 36 util,
37 37 )
38 38
39 39 urlerr = util.urlerr
40 40 urlreq = util.urlreq
41 41
42 42 # Maps bundle version human names to changegroup versions.
43 43 _bundlespeccgversions = {'v1': '01',
44 44 'v2': '02',
45 45 'packed1': 's1',
46 46 'bundle2': '02', #legacy
47 47 }
48 48
49 49 # Compression engines allowed in version 1. THIS SHOULD NEVER CHANGE.
50 50 _bundlespecv1compengines = {'gzip', 'bzip2', 'none'}
51 51
52 52 def parsebundlespec(repo, spec, strict=True, externalnames=False):
53 53 """Parse a bundle string specification into parts.
54 54
55 55 Bundle specifications denote a well-defined bundle/exchange format.
56 56 The content of a given specification should not change over time in
57 57 order to ensure that bundles produced by a newer version of Mercurial are
58 58 readable from an older version.
59 59
60 60 The string currently has the form:
61 61
62 62 <compression>-<type>[;<parameter0>[;<parameter1>]]
63 63
64 64 Where <compression> is one of the supported compression formats
65 65 and <type> is (currently) a version string. A ";" can follow the type and
66 66 all text afterwards is interpreted as URI encoded, ";" delimited key=value
67 67 pairs.
68 68
69 69 If ``strict`` is True (the default) <compression> is required. Otherwise,
70 70 it is optional.
71 71
72 72 If ``externalnames`` is False (the default), the human-centric names will
73 73 be converted to their internal representation.
74 74
75 75 Returns a 3-tuple of (compression, version, parameters). Compression will
76 76 be ``None`` if not in strict mode and a compression isn't defined.
77 77
78 78 An ``InvalidBundleSpecification`` is raised when the specification is
79 79 not syntactically well formed.
80 80
81 81 An ``UnsupportedBundleSpecification`` is raised when the compression or
82 82 bundle type/version is not recognized.
83 83
84 84 Note: this function will likely eventually return a more complex data
85 85 structure, including bundle2 part information.
86 86 """
87 87 def parseparams(s):
88 88 if ';' not in s:
89 89 return s, {}
90 90
91 91 params = {}
92 92 version, paramstr = s.split(';', 1)
93 93
94 94 for p in paramstr.split(';'):
95 95 if '=' not in p:
96 96 raise error.InvalidBundleSpecification(
97 97 _('invalid bundle specification: '
98 98 'missing "=" in parameter: %s') % p)
99 99
100 100 key, value = p.split('=', 1)
101 101 key = urlreq.unquote(key)
102 102 value = urlreq.unquote(value)
103 103 params[key] = value
104 104
105 105 return version, params
106 106
107 107
108 108 if strict and '-' not in spec:
109 109 raise error.InvalidBundleSpecification(
110 110 _('invalid bundle specification; '
111 111 'must be prefixed with compression: %s') % spec)
112 112
113 113 if '-' in spec:
114 114 compression, version = spec.split('-', 1)
115 115
116 116 if compression not in util.compengines.supportedbundlenames:
117 117 raise error.UnsupportedBundleSpecification(
118 118 _('%s compression is not supported') % compression)
119 119
120 120 version, params = parseparams(version)
121 121
122 122 if version not in _bundlespeccgversions:
123 123 raise error.UnsupportedBundleSpecification(
124 124 _('%s is not a recognized bundle version') % version)
125 125 else:
126 126 # Value could be just the compression or just the version, in which
127 127 # case some defaults are assumed (but only when not in strict mode).
128 128 assert not strict
129 129
130 130 spec, params = parseparams(spec)
131 131
132 132 if spec in util.compengines.supportedbundlenames:
133 133 compression = spec
134 134 version = 'v1'
135 135 # Generaldelta repos require v2.
136 136 if 'generaldelta' in repo.requirements:
137 137 version = 'v2'
138 138 # Modern compression engines require v2.
139 139 if compression not in _bundlespecv1compengines:
140 140 version = 'v2'
141 141 elif spec in _bundlespeccgversions:
142 142 if spec == 'packed1':
143 143 compression = 'none'
144 144 else:
145 145 compression = 'bzip2'
146 146 version = spec
147 147 else:
148 148 raise error.UnsupportedBundleSpecification(
149 149 _('%s is not a recognized bundle specification') % spec)
150 150
151 151 # Bundle version 1 only supports a known set of compression engines.
152 152 if version == 'v1' and compression not in _bundlespecv1compengines:
153 153 raise error.UnsupportedBundleSpecification(
154 154 _('compression engine %s is not supported on v1 bundles') %
155 155 compression)
156 156
157 157 # The specification for packed1 can optionally declare the data formats
158 158 # required to apply it. If we see this metadata, compare against what the
159 159 # repo supports and error if the bundle isn't compatible.
160 160 if version == 'packed1' and 'requirements' in params:
161 161 requirements = set(params['requirements'].split(','))
162 162 missingreqs = requirements - repo.supportedformats
163 163 if missingreqs:
164 164 raise error.UnsupportedBundleSpecification(
165 165 _('missing support for repository features: %s') %
166 166 ', '.join(sorted(missingreqs)))
167 167
168 168 if not externalnames:
169 169 engine = util.compengines.forbundlename(compression)
170 170 compression = engine.bundletype()[1]
171 171 version = _bundlespeccgversions[version]
172 172 return compression, version, params
173 173
174 174 def readbundle(ui, fh, fname, vfs=None):
175 175 header = changegroup.readexactly(fh, 4)
176 176
177 177 alg = None
178 178 if not fname:
179 179 fname = "stream"
180 180 if not header.startswith('HG') and header.startswith('\0'):
181 181 fh = changegroup.headerlessfixup(fh, header)
182 182 header = "HG10"
183 183 alg = 'UN'
184 184 elif vfs:
185 185 fname = vfs.join(fname)
186 186
187 187 magic, version = header[0:2], header[2:4]
188 188
189 189 if magic != 'HG':
190 190 raise error.Abort(_('%s: not a Mercurial bundle') % fname)
191 191 if version == '10':
192 192 if alg is None:
193 193 alg = changegroup.readexactly(fh, 2)
194 194 return changegroup.cg1unpacker(fh, alg)
195 195 elif version.startswith('2'):
196 196 return bundle2.getunbundler(ui, fh, magicstring=magic + version)
197 197 elif version == 'S1':
198 198 return streamclone.streamcloneapplier(fh)
199 199 else:
200 200 raise error.Abort(_('%s: unknown bundle version %s') % (fname, version))
201 201
202 202 def _formatrequirementsspec(requirements):
203 203 return urlreq.quote(','.join(sorted(requirements)))
204 204
205 205 def _formatrequirementsparams(requirements):
206 206 requirements = _formatrequirementsspec(requirements)
207 207 params = "%s%s" % (urlreq.quote("requirements="), requirements)
208 208 return params
209 209
210 210 def getbundlespec(ui, fh):
211 211 """Infer the bundlespec from a bundle file handle.
212 212
213 213 The input file handle is seeked and the original seek position is not
214 214 restored.
215 215 """
216 216 def speccompression(alg):
217 217 try:
218 218 return util.compengines.forbundletype(alg).bundletype()[0]
219 219 except KeyError:
220 220 return None
221 221
222 222 b = readbundle(ui, fh, None)
223 223 if isinstance(b, changegroup.cg1unpacker):
224 224 alg = b._type
225 225 if alg == '_truncatedBZ':
226 226 alg = 'BZ'
227 227 comp = speccompression(alg)
228 228 if not comp:
229 229 raise error.Abort(_('unknown compression algorithm: %s') % alg)
230 230 return '%s-v1' % comp
231 231 elif isinstance(b, bundle2.unbundle20):
232 232 if 'Compression' in b.params:
233 233 comp = speccompression(b.params['Compression'])
234 234 if not comp:
235 235 raise error.Abort(_('unknown compression algorithm: %s') % comp)
236 236 else:
237 237 comp = 'none'
238 238
239 239 version = None
240 240 for part in b.iterparts():
241 241 if part.type == 'changegroup':
242 242 version = part.params['version']
243 243 if version in ('01', '02'):
244 244 version = 'v2'
245 245 else:
246 246 raise error.Abort(_('changegroup version %s does not have '
247 247 'a known bundlespec') % version,
248 248 hint=_('try upgrading your Mercurial '
249 249 'client'))
250 250
251 251 if not version:
252 252 raise error.Abort(_('could not identify changegroup version in '
253 253 'bundle'))
254 254
255 255 return '%s-%s' % (comp, version)
256 256 elif isinstance(b, streamclone.streamcloneapplier):
257 257 requirements = streamclone.readbundle1header(fh)[2]
258 258 return 'none-packed1;%s' % _formatrequirementsparams(requirements)
259 259 else:
260 260 raise error.Abort(_('unknown bundle type: %s') % b)
261 261
262 262 def _computeoutgoing(repo, heads, common):
263 263 """Computes which revs are outgoing given a set of common
264 264 and a set of heads.
265 265
266 266 This is a separate function so extensions can have access to
267 267 the logic.
268 268
269 269 Returns a discovery.outgoing object.
270 270 """
271 271 cl = repo.changelog
272 272 if common:
273 273 hasnode = cl.hasnode
274 274 common = [n for n in common if hasnode(n)]
275 275 else:
276 276 common = [nullid]
277 277 if not heads:
278 278 heads = cl.heads()
279 279 return discovery.outgoing(repo, common, heads)
280 280
281 281 def _forcebundle1(op):
282 282 """return true if a pull/push must use bundle1
283 283
284 284 This function is used to allow testing of the older bundle version"""
285 285 ui = op.repo.ui
286 286 forcebundle1 = False
287 287 # The goal is this config is to allow developer to choose the bundle
288 288 # version used during exchanged. This is especially handy during test.
289 289 # Value is a list of bundle version to be picked from, highest version
290 290 # should be used.
291 291 #
292 292 # developer config: devel.legacy.exchange
293 293 exchange = ui.configlist('devel', 'legacy.exchange')
294 294 forcebundle1 = 'bundle2' not in exchange and 'bundle1' in exchange
295 295 return forcebundle1 or not op.remote.capable('bundle2')
296 296
297 297 class pushoperation(object):
298 298 """A object that represent a single push operation
299 299
300 300 Its purpose is to carry push related state and very common operations.
301 301
302 302 A new pushoperation should be created at the beginning of each push and
303 303 discarded afterward.
304 304 """
305 305
306 306 def __init__(self, repo, remote, force=False, revs=None, newbranch=False,
307 307 bookmarks=(), pushvars=None):
308 308 # repo we push from
309 309 self.repo = repo
310 310 self.ui = repo.ui
311 311 # repo we push to
312 312 self.remote = remote
313 313 # force option provided
314 314 self.force = force
315 315 # revs to be pushed (None is "all")
316 316 self.revs = revs
317 317 # bookmark explicitly pushed
318 318 self.bookmarks = bookmarks
319 319 # allow push of new branch
320 320 self.newbranch = newbranch
321 321 # step already performed
322 322 # (used to check what steps have been already performed through bundle2)
323 323 self.stepsdone = set()
324 324 # Integer version of the changegroup push result
325 325 # - None means nothing to push
326 326 # - 0 means HTTP error
327 327 # - 1 means we pushed and remote head count is unchanged *or*
328 328 # we have outgoing changesets but refused to push
329 329 # - other values as described by addchangegroup()
330 330 self.cgresult = None
331 331 # Boolean value for the bookmark push
332 332 self.bkresult = None
333 333 # discover.outgoing object (contains common and outgoing data)
334 334 self.outgoing = None
335 335 # all remote topological heads before the push
336 336 self.remoteheads = None
337 337 # Details of the remote branch pre and post push
338 338 #
339 339 # mapping: {'branch': ([remoteheads],
340 340 # [newheads],
341 341 # [unsyncedheads],
342 342 # [discardedheads])}
343 343 # - branch: the branch name
344 344 # - remoteheads: the list of remote heads known locally
345 345 # None if the branch is new
346 346 # - newheads: the new remote heads (known locally) with outgoing pushed
347 347 # - unsyncedheads: the list of remote heads unknown locally.
348 348 # - discardedheads: the list of remote heads made obsolete by the push
349 349 self.pushbranchmap = None
350 350 # testable as a boolean indicating if any nodes are missing locally.
351 351 self.incoming = None
352 352 # summary of the remote phase situation
353 353 self.remotephases = None
354 354 # phases changes that must be pushed along side the changesets
355 355 self.outdatedphases = None
356 356 # phases changes that must be pushed if changeset push fails
357 357 self.fallbackoutdatedphases = None
358 358 # outgoing obsmarkers
359 359 self.outobsmarkers = set()
360 360 # outgoing bookmarks
361 361 self.outbookmarks = []
362 362 # transaction manager
363 363 self.trmanager = None
364 364 # map { pushkey partid -> callback handling failure}
365 365 # used to handle exception from mandatory pushkey part failure
366 366 self.pkfailcb = {}
367 367 # an iterable of pushvars or None
368 368 self.pushvars = pushvars
369 369
370 370 @util.propertycache
371 371 def futureheads(self):
372 372 """future remote heads if the changeset push succeeds"""
373 373 return self.outgoing.missingheads
374 374
375 375 @util.propertycache
376 376 def fallbackheads(self):
377 377 """future remote heads if the changeset push fails"""
378 378 if self.revs is None:
379 379 # not target to push, all common are relevant
380 380 return self.outgoing.commonheads
381 381 unfi = self.repo.unfiltered()
382 382 # I want cheads = heads(::missingheads and ::commonheads)
383 383 # (missingheads is revs with secret changeset filtered out)
384 384 #
385 385 # This can be expressed as:
386 386 # cheads = ( (missingheads and ::commonheads)
387 387 # + (commonheads and ::missingheads))"
388 388 # )
389 389 #
390 390 # while trying to push we already computed the following:
391 391 # common = (::commonheads)
392 392 # missing = ((commonheads::missingheads) - commonheads)
393 393 #
394 394 # We can pick:
395 395 # * missingheads part of common (::commonheads)
396 396 common = self.outgoing.common
397 397 nm = self.repo.changelog.nodemap
398 398 cheads = [node for node in self.revs if nm[node] in common]
399 399 # and
400 400 # * commonheads parents on missing
401 401 revset = unfi.set('%ln and parents(roots(%ln))',
402 402 self.outgoing.commonheads,
403 403 self.outgoing.missing)
404 404 cheads.extend(c.node() for c in revset)
405 405 return cheads
406 406
407 407 @property
408 408 def commonheads(self):
409 409 """set of all common heads after changeset bundle push"""
410 410 if self.cgresult:
411 411 return self.futureheads
412 412 else:
413 413 return self.fallbackheads
414 414
415 415 # mapping of message used when pushing bookmark
416 416 bookmsgmap = {'update': (_("updating bookmark %s\n"),
417 417 _('updating bookmark %s failed!\n')),
418 418 'export': (_("exporting bookmark %s\n"),
419 419 _('exporting bookmark %s failed!\n')),
420 420 'delete': (_("deleting remote bookmark %s\n"),
421 421 _('deleting remote bookmark %s failed!\n')),
422 422 }
423 423
424 424
425 425 def push(repo, remote, force=False, revs=None, newbranch=False, bookmarks=(),
426 426 opargs=None):
427 427 '''Push outgoing changesets (limited by revs) from a local
428 428 repository to remote. Return an integer:
429 429 - None means nothing to push
430 430 - 0 means HTTP error
431 431 - 1 means we pushed and remote head count is unchanged *or*
432 432 we have outgoing changesets but refused to push
433 433 - other values as described by addchangegroup()
434 434 '''
435 435 if opargs is None:
436 436 opargs = {}
437 437 pushop = pushoperation(repo, remote, force, revs, newbranch, bookmarks,
438 438 **pycompat.strkwargs(opargs))
439 439 if pushop.remote.local():
440 440 missing = (set(pushop.repo.requirements)
441 441 - pushop.remote.local().supported)
442 442 if missing:
443 443 msg = _("required features are not"
444 444 " supported in the destination:"
445 445 " %s") % (', '.join(sorted(missing)))
446 446 raise error.Abort(msg)
447 447
448 448 if not pushop.remote.canpush():
449 449 raise error.Abort(_("destination does not support push"))
450 450
451 451 if not pushop.remote.capable('unbundle'):
452 452 raise error.Abort(_('cannot push: destination does not support the '
453 453 'unbundle wire protocol command'))
454 454
455 455 # get lock as we might write phase data
456 456 wlock = lock = None
457 457 try:
458 458 # bundle2 push may receive a reply bundle touching bookmarks or other
459 459 # things requiring the wlock. Take it now to ensure proper ordering.
460 460 maypushback = pushop.ui.configbool('experimental', 'bundle2.pushback')
461 461 if (not _forcebundle1(pushop)) and maypushback:
462 462 wlock = pushop.repo.wlock()
463 463 lock = pushop.repo.lock()
464 464 pushop.trmanager = transactionmanager(pushop.repo,
465 465 'push-response',
466 466 pushop.remote.url())
467 467 except IOError as err:
468 468 if err.errno != errno.EACCES:
469 469 raise
470 470 # source repo cannot be locked.
471 471 # We do not abort the push, but just disable the local phase
472 472 # synchronisation.
473 473 msg = 'cannot lock source repository: %s\n' % err
474 474 pushop.ui.debug(msg)
475 475
476 476 with wlock or util.nullcontextmanager(), \
477 477 lock or util.nullcontextmanager(), \
478 478 pushop.trmanager or util.nullcontextmanager():
479 479 pushop.repo.checkpush(pushop)
480 480 _pushdiscovery(pushop)
481 481 if not _forcebundle1(pushop):
482 482 _pushbundle2(pushop)
483 483 _pushchangeset(pushop)
484 484 _pushsyncphase(pushop)
485 485 _pushobsolete(pushop)
486 486 _pushbookmark(pushop)
487 487
488 488 return pushop
489 489
490 490 # list of steps to perform discovery before push
491 491 pushdiscoveryorder = []
492 492
493 493 # Mapping between step name and function
494 494 #
495 495 # This exists to help extensions wrap steps if necessary
496 496 pushdiscoverymapping = {}
497 497
498 498 def pushdiscovery(stepname):
499 499 """decorator for function performing discovery before push
500 500
501 501 The function is added to the step -> function mapping and appended to the
502 502 list of steps. Beware that decorated function will be added in order (this
503 503 may matter).
504 504
505 505 You can only use this decorator for a new step, if you want to wrap a step
506 506 from an extension, change the pushdiscovery dictionary directly."""
507 507 def dec(func):
508 508 assert stepname not in pushdiscoverymapping
509 509 pushdiscoverymapping[stepname] = func
510 510 pushdiscoveryorder.append(stepname)
511 511 return func
512 512 return dec
513 513
514 514 def _pushdiscovery(pushop):
515 515 """Run all discovery steps"""
516 516 for stepname in pushdiscoveryorder:
517 517 step = pushdiscoverymapping[stepname]
518 518 step(pushop)
519 519
520 520 @pushdiscovery('changeset')
521 521 def _pushdiscoverychangeset(pushop):
522 522 """discover the changeset that need to be pushed"""
523 523 fci = discovery.findcommonincoming
524 524 if pushop.revs:
525 525 commoninc = fci(pushop.repo, pushop.remote, force=pushop.force,
526 526 ancestorsof=pushop.revs)
527 527 else:
528 528 commoninc = fci(pushop.repo, pushop.remote, force=pushop.force)
529 529 common, inc, remoteheads = commoninc
530 530 fco = discovery.findcommonoutgoing
531 531 outgoing = fco(pushop.repo, pushop.remote, onlyheads=pushop.revs,
532 532 commoninc=commoninc, force=pushop.force)
533 533 pushop.outgoing = outgoing
534 534 pushop.remoteheads = remoteheads
535 535 pushop.incoming = inc
536 536
537 537 @pushdiscovery('phase')
538 538 def _pushdiscoveryphase(pushop):
539 539 """discover the phase that needs to be pushed
540 540
541 541 (computed for both success and failure case for changesets push)"""
542 542 outgoing = pushop.outgoing
543 543 unfi = pushop.repo.unfiltered()
544 544 remotephases = pushop.remote.listkeys('phases')
545 545 if (pushop.ui.configbool('ui', '_usedassubrepo')
546 546 and remotephases # server supports phases
547 547 and not pushop.outgoing.missing # no changesets to be pushed
548 548 and remotephases.get('publishing', False)):
549 549 # When:
550 550 # - this is a subrepo push
551 551 # - and remote support phase
552 552 # - and no changeset are to be pushed
553 553 # - and remote is publishing
554 554 # We may be in issue 3781 case!
555 555 # We drop the possible phase synchronisation done by
556 556 # courtesy to publish changesets possibly locally draft
557 557 # on the remote.
558 558 pushop.outdatedphases = []
559 559 pushop.fallbackoutdatedphases = []
560 560 return
561 561
562 562 pushop.remotephases = phases.remotephasessummary(pushop.repo,
563 563 pushop.fallbackheads,
564 564 remotephases)
565 565 droots = pushop.remotephases.draftroots
566 566
567 567 extracond = ''
568 568 if not pushop.remotephases.publishing:
569 569 extracond = ' and public()'
570 570 revset = 'heads((%%ln::%%ln) %s)' % extracond
571 571 # Get the list of all revs draft on remote by public here.
572 572 # XXX Beware that revset break if droots is not strictly
573 573 # XXX root we may want to ensure it is but it is costly
574 574 fallback = list(unfi.set(revset, droots, pushop.fallbackheads))
575 575 if not outgoing.missing:
576 576 future = fallback
577 577 else:
578 578 # adds changeset we are going to push as draft
579 579 #
580 580 # should not be necessary for publishing server, but because of an
581 581 # issue fixed in xxxxx we have to do it anyway.
582 582 fdroots = list(unfi.set('roots(%ln + %ln::)',
583 583 outgoing.missing, droots))
584 584 fdroots = [f.node() for f in fdroots]
585 585 future = list(unfi.set(revset, fdroots, pushop.futureheads))
586 586 pushop.outdatedphases = future
587 587 pushop.fallbackoutdatedphases = fallback
588 588
589 589 @pushdiscovery('obsmarker')
590 590 def _pushdiscoveryobsmarkers(pushop):
591 591 if (obsolete.isenabled(pushop.repo, obsolete.exchangeopt)
592 592 and pushop.repo.obsstore
593 593 and 'obsolete' in pushop.remote.listkeys('namespaces')):
594 594 repo = pushop.repo
595 595 # very naive computation, that can be quite expensive on big repo.
596 596 # However: evolution is currently slow on them anyway.
597 597 nodes = (c.node() for c in repo.set('::%ln', pushop.futureheads))
598 598 pushop.outobsmarkers = pushop.repo.obsstore.relevantmarkers(nodes)
599 599
600 600 @pushdiscovery('bookmarks')
601 601 def _pushdiscoverybookmarks(pushop):
602 602 ui = pushop.ui
603 603 repo = pushop.repo.unfiltered()
604 604 remote = pushop.remote
605 605 ui.debug("checking for updated bookmarks\n")
606 606 ancestors = ()
607 607 if pushop.revs:
608 608 revnums = map(repo.changelog.rev, pushop.revs)
609 609 ancestors = repo.changelog.ancestors(revnums, inclusive=True)
610 610 remotebookmark = remote.listkeys('bookmarks')
611 611
612 612 explicit = set([repo._bookmarks.expandname(bookmark)
613 613 for bookmark in pushop.bookmarks])
614 614
615 615 remotebookmark = bookmod.unhexlifybookmarks(remotebookmark)
616 616 comp = bookmod.comparebookmarks(repo, repo._bookmarks, remotebookmark)
617 617
618 618 def safehex(x):
619 619 if x is None:
620 620 return x
621 621 return hex(x)
622 622
623 623 def hexifycompbookmarks(bookmarks):
624 624 for b, scid, dcid in bookmarks:
625 625 yield b, safehex(scid), safehex(dcid)
626 626
627 627 comp = [hexifycompbookmarks(marks) for marks in comp]
628 628 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = comp
629 629
630 630 for b, scid, dcid in advsrc:
631 631 if b in explicit:
632 632 explicit.remove(b)
633 633 if not ancestors or repo[scid].rev() in ancestors:
634 634 pushop.outbookmarks.append((b, dcid, scid))
635 635 # search added bookmark
636 636 for b, scid, dcid in addsrc:
637 637 if b in explicit:
638 638 explicit.remove(b)
639 639 pushop.outbookmarks.append((b, '', scid))
640 640 # search for overwritten bookmark
641 641 for b, scid, dcid in list(advdst) + list(diverge) + list(differ):
642 642 if b in explicit:
643 643 explicit.remove(b)
644 644 pushop.outbookmarks.append((b, dcid, scid))
645 645 # search for bookmark to delete
646 646 for b, scid, dcid in adddst:
647 647 if b in explicit:
648 648 explicit.remove(b)
649 649 # treat as "deleted locally"
650 650 pushop.outbookmarks.append((b, dcid, ''))
651 651 # identical bookmarks shouldn't get reported
652 652 for b, scid, dcid in same:
653 653 if b in explicit:
654 654 explicit.remove(b)
655 655
656 656 if explicit:
657 657 explicit = sorted(explicit)
658 658 # we should probably list all of them
659 659 ui.warn(_('bookmark %s does not exist on the local '
660 660 'or remote repository!\n') % explicit[0])
661 661 pushop.bkresult = 2
662 662
663 663 pushop.outbookmarks.sort()
664 664
665 665 def _pushcheckoutgoing(pushop):
666 666 outgoing = pushop.outgoing
667 667 unfi = pushop.repo.unfiltered()
668 668 if not outgoing.missing:
669 669 # nothing to push
670 670 scmutil.nochangesfound(unfi.ui, unfi, outgoing.excluded)
671 671 return False
672 672 # something to push
673 673 if not pushop.force:
674 674 # if repo.obsstore == False --> no obsolete
675 675 # then, save the iteration
676 676 if unfi.obsstore:
677 677 # this message are here for 80 char limit reason
678 678 mso = _("push includes obsolete changeset: %s!")
679 679 mspd = _("push includes phase-divergent changeset: %s!")
680 680 mscd = _("push includes content-divergent changeset: %s!")
681 681 mst = {"orphan": _("push includes orphan changeset: %s!"),
682 682 "phase-divergent": mspd,
683 683 "content-divergent": mscd}
684 684 # If we are to push if there is at least one
685 685 # obsolete or unstable changeset in missing, at
686 686 # least one of the missinghead will be obsolete or
687 687 # unstable. So checking heads only is ok
688 688 for node in outgoing.missingheads:
689 689 ctx = unfi[node]
690 690 if ctx.obsolete():
691 691 raise error.Abort(mso % ctx)
692 692 elif ctx.isunstable():
693 693 # TODO print more than one instability in the abort
694 694 # message
695 695 raise error.Abort(mst[ctx.instabilities()[0]] % ctx)
696 696
697 697 discovery.checkheads(pushop)
698 698 return True
699 699
700 700 # List of names of steps to perform for an outgoing bundle2, order matters.
701 701 b2partsgenorder = []
702 702
703 703 # Mapping between step name and function
704 704 #
705 705 # This exists to help extensions wrap steps if necessary
706 706 b2partsgenmapping = {}
707 707
708 708 def b2partsgenerator(stepname, idx=None):
709 709 """decorator for function generating bundle2 part
710 710
711 711 The function is added to the step -> function mapping and appended to the
712 712 list of steps. Beware that decorated functions will be added in order
713 713 (this may matter).
714 714
715 715 You can only use this decorator for new steps, if you want to wrap a step
716 716 from an extension, attack the b2partsgenmapping dictionary directly."""
717 717 def dec(func):
718 718 assert stepname not in b2partsgenmapping
719 719 b2partsgenmapping[stepname] = func
720 720 if idx is None:
721 721 b2partsgenorder.append(stepname)
722 722 else:
723 723 b2partsgenorder.insert(idx, stepname)
724 724 return func
725 725 return dec
726 726
727 727 def _pushb2ctxcheckheads(pushop, bundler):
728 728 """Generate race condition checking parts
729 729
730 730 Exists as an independent function to aid extensions
731 731 """
732 732 # * 'force' do not check for push race,
733 733 # * if we don't push anything, there are nothing to check.
734 734 if not pushop.force and pushop.outgoing.missingheads:
735 735 allowunrelated = 'related' in bundler.capabilities.get('checkheads', ())
736 736 emptyremote = pushop.pushbranchmap is None
737 737 if not allowunrelated or emptyremote:
738 738 bundler.newpart('check:heads', data=iter(pushop.remoteheads))
739 739 else:
740 740 affected = set()
741 741 for branch, heads in pushop.pushbranchmap.iteritems():
742 742 remoteheads, newheads, unsyncedheads, discardedheads = heads
743 743 if remoteheads is not None:
744 744 remote = set(remoteheads)
745 745 affected |= set(discardedheads) & remote
746 746 affected |= remote - set(newheads)
747 747 if affected:
748 748 data = iter(sorted(affected))
749 749 bundler.newpart('check:updated-heads', data=data)
750 750
751 751 def _pushing(pushop):
752 752 """return True if we are pushing anything"""
753 753 return bool(pushop.outgoing.missing
754 754 or pushop.outdatedphases
755 755 or pushop.outobsmarkers
756 756 or pushop.outbookmarks)
757 757
758 758 @b2partsgenerator('check-bookmarks')
759 759 def _pushb2checkbookmarks(pushop, bundler):
760 760 """insert bookmark move checking"""
761 761 if not _pushing(pushop) or pushop.force:
762 762 return
763 763 b2caps = bundle2.bundle2caps(pushop.remote)
764 764 hasbookmarkcheck = 'bookmarks' in b2caps
765 765 if not (pushop.outbookmarks and hasbookmarkcheck):
766 766 return
767 767 data = []
768 768 for book, old, new in pushop.outbookmarks:
769 769 old = bin(old)
770 770 data.append((book, old))
771 771 checkdata = bookmod.binaryencode(data)
772 772 bundler.newpart('check:bookmarks', data=checkdata)
773 773
774 774 @b2partsgenerator('check-phases')
775 775 def _pushb2checkphases(pushop, bundler):
776 776 """insert phase move checking"""
777 777 if not _pushing(pushop) or pushop.force:
778 778 return
779 779 b2caps = bundle2.bundle2caps(pushop.remote)
780 780 hasphaseheads = 'heads' in b2caps.get('phases', ())
781 781 if pushop.remotephases is not None and hasphaseheads:
782 782 # check that the remote phase has not changed
783 783 checks = [[] for p in phases.allphases]
784 784 checks[phases.public].extend(pushop.remotephases.publicheads)
785 785 checks[phases.draft].extend(pushop.remotephases.draftroots)
786 786 if any(checks):
787 787 for nodes in checks:
788 788 nodes.sort()
789 789 checkdata = phases.binaryencode(checks)
790 790 bundler.newpart('check:phases', data=checkdata)
791 791
792 792 @b2partsgenerator('changeset')
793 793 def _pushb2ctx(pushop, bundler):
794 794 """handle changegroup push through bundle2
795 795
796 796 addchangegroup result is stored in the ``pushop.cgresult`` attribute.
797 797 """
798 798 if 'changesets' in pushop.stepsdone:
799 799 return
800 800 pushop.stepsdone.add('changesets')
801 801 # Send known heads to the server for race detection.
802 802 if not _pushcheckoutgoing(pushop):
803 803 return
804 804 pushop.repo.prepushoutgoinghooks(pushop)
805 805
806 806 _pushb2ctxcheckheads(pushop, bundler)
807 807
808 808 b2caps = bundle2.bundle2caps(pushop.remote)
809 809 version = '01'
810 810 cgversions = b2caps.get('changegroup')
811 811 if cgversions: # 3.1 and 3.2 ship with an empty value
812 812 cgversions = [v for v in cgversions
813 813 if v in changegroup.supportedoutgoingversions(
814 814 pushop.repo)]
815 815 if not cgversions:
816 816 raise ValueError(_('no common changegroup version'))
817 817 version = max(cgversions)
818 818 cgstream = changegroup.makestream(pushop.repo, pushop.outgoing, version,
819 819 'push')
820 820 cgpart = bundler.newpart('changegroup', data=cgstream)
821 821 if cgversions:
822 822 cgpart.addparam('version', version)
823 823 if 'treemanifest' in pushop.repo.requirements:
824 824 cgpart.addparam('treemanifest', '1')
825 825 def handlereply(op):
826 826 """extract addchangegroup returns from server reply"""
827 827 cgreplies = op.records.getreplies(cgpart.id)
828 828 assert len(cgreplies['changegroup']) == 1
829 829 pushop.cgresult = cgreplies['changegroup'][0]['return']
830 830 return handlereply
831 831
832 832 @b2partsgenerator('phase')
833 833 def _pushb2phases(pushop, bundler):
834 834 """handle phase push through bundle2"""
835 835 if 'phases' in pushop.stepsdone:
836 836 return
837 837 b2caps = bundle2.bundle2caps(pushop.remote)
838 838 ui = pushop.repo.ui
839 839
840 840 legacyphase = 'phases' in ui.configlist('devel', 'legacy.exchange')
841 841 haspushkey = 'pushkey' in b2caps
842 842 hasphaseheads = 'heads' in b2caps.get('phases', ())
843 843
844 844 if hasphaseheads and not legacyphase:
845 845 return _pushb2phaseheads(pushop, bundler)
846 846 elif haspushkey:
847 847 return _pushb2phasespushkey(pushop, bundler)
848 848
849 849 def _pushb2phaseheads(pushop, bundler):
850 850 """push phase information through a bundle2 - binary part"""
851 851 pushop.stepsdone.add('phases')
852 852 if pushop.outdatedphases:
853 853 updates = [[] for p in phases.allphases]
854 854 updates[0].extend(h.node() for h in pushop.outdatedphases)
855 855 phasedata = phases.binaryencode(updates)
856 856 bundler.newpart('phase-heads', data=phasedata)
857 857
858 858 def _pushb2phasespushkey(pushop, bundler):
859 859 """push phase information through a bundle2 - pushkey part"""
860 860 pushop.stepsdone.add('phases')
861 861 part2node = []
862 862
863 863 def handlefailure(pushop, exc):
864 864 targetid = int(exc.partid)
865 865 for partid, node in part2node:
866 866 if partid == targetid:
867 867 raise error.Abort(_('updating %s to public failed') % node)
868 868
869 869 enc = pushkey.encode
870 870 for newremotehead in pushop.outdatedphases:
871 871 part = bundler.newpart('pushkey')
872 872 part.addparam('namespace', enc('phases'))
873 873 part.addparam('key', enc(newremotehead.hex()))
874 874 part.addparam('old', enc('%d' % phases.draft))
875 875 part.addparam('new', enc('%d' % phases.public))
876 876 part2node.append((part.id, newremotehead))
877 877 pushop.pkfailcb[part.id] = handlefailure
878 878
879 879 def handlereply(op):
880 880 for partid, node in part2node:
881 881 partrep = op.records.getreplies(partid)
882 882 results = partrep['pushkey']
883 883 assert len(results) <= 1
884 884 msg = None
885 885 if not results:
886 886 msg = _('server ignored update of %s to public!\n') % node
887 887 elif not int(results[0]['return']):
888 888 msg = _('updating %s to public failed!\n') % node
889 889 if msg is not None:
890 890 pushop.ui.warn(msg)
891 891 return handlereply
892 892
893 893 @b2partsgenerator('obsmarkers')
894 894 def _pushb2obsmarkers(pushop, bundler):
895 895 if 'obsmarkers' in pushop.stepsdone:
896 896 return
897 897 remoteversions = bundle2.obsmarkersversion(bundler.capabilities)
898 898 if obsolete.commonversion(remoteversions) is None:
899 899 return
900 900 pushop.stepsdone.add('obsmarkers')
901 901 if pushop.outobsmarkers:
902 902 markers = sorted(pushop.outobsmarkers)
903 903 bundle2.buildobsmarkerspart(bundler, markers)
904 904
905 905 @b2partsgenerator('bookmarks')
906 906 def _pushb2bookmarks(pushop, bundler):
907 907 """handle bookmark push through bundle2"""
908 908 if 'bookmarks' in pushop.stepsdone:
909 909 return
910 910 b2caps = bundle2.bundle2caps(pushop.remote)
911 911
912 912 legacy = pushop.repo.ui.configlist('devel', 'legacy.exchange')
913 913 legacybooks = 'bookmarks' in legacy
914 914
915 915 if not legacybooks and 'bookmarks' in b2caps:
916 916 return _pushb2bookmarkspart(pushop, bundler)
917 917 elif 'pushkey' in b2caps:
918 918 return _pushb2bookmarkspushkey(pushop, bundler)
919 919
920 920 def _bmaction(old, new):
921 921 """small utility for bookmark pushing"""
922 922 if not old:
923 923 return 'export'
924 924 elif not new:
925 925 return 'delete'
926 926 return 'update'
927 927
928 928 def _pushb2bookmarkspart(pushop, bundler):
929 929 pushop.stepsdone.add('bookmarks')
930 930 if not pushop.outbookmarks:
931 931 return
932 932
933 933 allactions = []
934 934 data = []
935 935 for book, old, new in pushop.outbookmarks:
936 936 new = bin(new)
937 937 data.append((book, new))
938 938 allactions.append((book, _bmaction(old, new)))
939 939 checkdata = bookmod.binaryencode(data)
940 940 bundler.newpart('bookmarks', data=checkdata)
941 941
942 942 def handlereply(op):
943 943 ui = pushop.ui
944 944 # if success
945 945 for book, action in allactions:
946 946 ui.status(bookmsgmap[action][0] % book)
947 947
948 948 return handlereply
949 949
950 950 def _pushb2bookmarkspushkey(pushop, bundler):
951 951 pushop.stepsdone.add('bookmarks')
952 952 part2book = []
953 953 enc = pushkey.encode
954 954
955 955 def handlefailure(pushop, exc):
956 956 targetid = int(exc.partid)
957 957 for partid, book, action in part2book:
958 958 if partid == targetid:
959 959 raise error.Abort(bookmsgmap[action][1].rstrip() % book)
960 960 # we should not be called for part we did not generated
961 961 assert False
962 962
963 963 for book, old, new in pushop.outbookmarks:
964 964 part = bundler.newpart('pushkey')
965 965 part.addparam('namespace', enc('bookmarks'))
966 966 part.addparam('key', enc(book))
967 967 part.addparam('old', enc(old))
968 968 part.addparam('new', enc(new))
969 969 action = 'update'
970 970 if not old:
971 971 action = 'export'
972 972 elif not new:
973 973 action = 'delete'
974 974 part2book.append((part.id, book, action))
975 975 pushop.pkfailcb[part.id] = handlefailure
976 976
977 977 def handlereply(op):
978 978 ui = pushop.ui
979 979 for partid, book, action in part2book:
980 980 partrep = op.records.getreplies(partid)
981 981 results = partrep['pushkey']
982 982 assert len(results) <= 1
983 983 if not results:
984 984 pushop.ui.warn(_('server ignored bookmark %s update\n') % book)
985 985 else:
986 986 ret = int(results[0]['return'])
987 987 if ret:
988 988 ui.status(bookmsgmap[action][0] % book)
989 989 else:
990 990 ui.warn(bookmsgmap[action][1] % book)
991 991 if pushop.bkresult is not None:
992 992 pushop.bkresult = 1
993 993 return handlereply
994 994
995 995 @b2partsgenerator('pushvars', idx=0)
996 996 def _getbundlesendvars(pushop, bundler):
997 997 '''send shellvars via bundle2'''
998 998 pushvars = pushop.pushvars
999 999 if pushvars:
1000 1000 shellvars = {}
1001 1001 for raw in pushvars:
1002 1002 if '=' not in raw:
1003 1003 msg = ("unable to parse variable '%s', should follow "
1004 1004 "'KEY=VALUE' or 'KEY=' format")
1005 1005 raise error.Abort(msg % raw)
1006 1006 k, v = raw.split('=', 1)
1007 1007 shellvars[k] = v
1008 1008
1009 1009 part = bundler.newpart('pushvars')
1010 1010
1011 1011 for key, value in shellvars.iteritems():
1012 1012 part.addparam(key, value, mandatory=False)
1013 1013
1014 1014 def _pushbundle2(pushop):
1015 1015 """push data to the remote using bundle2
1016 1016
1017 1017 The only currently supported type of data is changegroup but this will
1018 1018 evolve in the future."""
1019 1019 bundler = bundle2.bundle20(pushop.ui, bundle2.bundle2caps(pushop.remote))
1020 1020 pushback = (pushop.trmanager
1021 1021 and pushop.ui.configbool('experimental', 'bundle2.pushback'))
1022 1022
1023 1023 # create reply capability
1024 1024 capsblob = bundle2.encodecaps(bundle2.getrepocaps(pushop.repo,
1025 1025 allowpushback=pushback,
1026 1026 role='client'))
1027 1027 bundler.newpart('replycaps', data=capsblob)
1028 1028 replyhandlers = []
1029 1029 for partgenname in b2partsgenorder:
1030 1030 partgen = b2partsgenmapping[partgenname]
1031 1031 ret = partgen(pushop, bundler)
1032 1032 if callable(ret):
1033 1033 replyhandlers.append(ret)
1034 1034 # do not push if nothing to push
1035 1035 if bundler.nbparts <= 1:
1036 1036 return
1037 1037 stream = util.chunkbuffer(bundler.getchunks())
1038 1038 try:
1039 1039 try:
1040 1040 reply = pushop.remote.unbundle(
1041 1041 stream, ['force'], pushop.remote.url())
1042 1042 except error.BundleValueError as exc:
1043 1043 raise error.Abort(_('missing support for %s') % exc)
1044 1044 try:
1045 1045 trgetter = None
1046 1046 if pushback:
1047 1047 trgetter = pushop.trmanager.transaction
1048 1048 op = bundle2.processbundle(pushop.repo, reply, trgetter)
1049 1049 except error.BundleValueError as exc:
1050 1050 raise error.Abort(_('missing support for %s') % exc)
1051 1051 except bundle2.AbortFromPart as exc:
1052 1052 pushop.ui.status(_('remote: %s\n') % exc)
1053 1053 if exc.hint is not None:
1054 1054 pushop.ui.status(_('remote: %s\n') % ('(%s)' % exc.hint))
1055 1055 raise error.Abort(_('push failed on remote'))
1056 1056 except error.PushkeyFailed as exc:
1057 1057 partid = int(exc.partid)
1058 1058 if partid not in pushop.pkfailcb:
1059 1059 raise
1060 1060 pushop.pkfailcb[partid](pushop, exc)
1061 1061 for rephand in replyhandlers:
1062 1062 rephand(op)
1063 1063
1064 1064 def _pushchangeset(pushop):
1065 1065 """Make the actual push of changeset bundle to remote repo"""
1066 1066 if 'changesets' in pushop.stepsdone:
1067 1067 return
1068 1068 pushop.stepsdone.add('changesets')
1069 1069 if not _pushcheckoutgoing(pushop):
1070 1070 return
1071 1071
1072 1072 # Should have verified this in push().
1073 1073 assert pushop.remote.capable('unbundle')
1074 1074
1075 1075 pushop.repo.prepushoutgoinghooks(pushop)
1076 1076 outgoing = pushop.outgoing
1077 1077 # TODO: get bundlecaps from remote
1078 1078 bundlecaps = None
1079 1079 # create a changegroup from local
1080 1080 if pushop.revs is None and not (outgoing.excluded
1081 1081 or pushop.repo.changelog.filteredrevs):
1082 1082 # push everything,
1083 1083 # use the fast path, no race possible on push
1084 1084 cg = changegroup.makechangegroup(pushop.repo, outgoing, '01', 'push',
1085 1085 fastpath=True, bundlecaps=bundlecaps)
1086 1086 else:
1087 1087 cg = changegroup.makechangegroup(pushop.repo, outgoing, '01',
1088 1088 'push', bundlecaps=bundlecaps)
1089 1089
1090 1090 # apply changegroup to remote
1091 1091 # local repo finds heads on server, finds out what
1092 1092 # revs it must push. once revs transferred, if server
1093 1093 # finds it has different heads (someone else won
1094 1094 # commit/push race), server aborts.
1095 1095 if pushop.force:
1096 1096 remoteheads = ['force']
1097 1097 else:
1098 1098 remoteheads = pushop.remoteheads
1099 1099 # ssh: return remote's addchangegroup()
1100 1100 # http: return remote's addchangegroup() or 0 for error
1101 1101 pushop.cgresult = pushop.remote.unbundle(cg, remoteheads,
1102 1102 pushop.repo.url())
1103 1103
1104 1104 def _pushsyncphase(pushop):
1105 1105 """synchronise phase information locally and remotely"""
1106 1106 cheads = pushop.commonheads
1107 1107 # even when we don't push, exchanging phase data is useful
1108 1108 remotephases = pushop.remote.listkeys('phases')
1109 1109 if (pushop.ui.configbool('ui', '_usedassubrepo')
1110 1110 and remotephases # server supports phases
1111 1111 and pushop.cgresult is None # nothing was pushed
1112 1112 and remotephases.get('publishing', False)):
1113 1113 # When:
1114 1114 # - this is a subrepo push
1115 1115 # - and remote support phase
1116 1116 # - and no changeset was pushed
1117 1117 # - and remote is publishing
1118 1118 # We may be in issue 3871 case!
1119 1119 # We drop the possible phase synchronisation done by
1120 1120 # courtesy to publish changesets possibly locally draft
1121 1121 # on the remote.
1122 1122 remotephases = {'publishing': 'True'}
1123 1123 if not remotephases: # old server or public only reply from non-publishing
1124 1124 _localphasemove(pushop, cheads)
1125 1125 # don't push any phase data as there is nothing to push
1126 1126 else:
1127 1127 ana = phases.analyzeremotephases(pushop.repo, cheads,
1128 1128 remotephases)
1129 1129 pheads, droots = ana
1130 1130 ### Apply remote phase on local
1131 1131 if remotephases.get('publishing', False):
1132 1132 _localphasemove(pushop, cheads)
1133 1133 else: # publish = False
1134 1134 _localphasemove(pushop, pheads)
1135 1135 _localphasemove(pushop, cheads, phases.draft)
1136 1136 ### Apply local phase on remote
1137 1137
1138 1138 if pushop.cgresult:
1139 1139 if 'phases' in pushop.stepsdone:
1140 1140 # phases already pushed though bundle2
1141 1141 return
1142 1142 outdated = pushop.outdatedphases
1143 1143 else:
1144 1144 outdated = pushop.fallbackoutdatedphases
1145 1145
1146 1146 pushop.stepsdone.add('phases')
1147 1147
1148 1148 # filter heads already turned public by the push
1149 1149 outdated = [c for c in outdated if c.node() not in pheads]
1150 1150 # fallback to independent pushkey command
1151 1151 for newremotehead in outdated:
1152 1152 r = pushop.remote.pushkey('phases',
1153 1153 newremotehead.hex(),
1154 str(phases.draft),
1155 str(phases.public))
1154 ('%d' % phases.draft),
1155 ('%d' % phases.public))
1156 1156 if not r:
1157 1157 pushop.ui.warn(_('updating %s to public failed!\n')
1158 1158 % newremotehead)
1159 1159
1160 1160 def _localphasemove(pushop, nodes, phase=phases.public):
1161 1161 """move <nodes> to <phase> in the local source repo"""
1162 1162 if pushop.trmanager:
1163 1163 phases.advanceboundary(pushop.repo,
1164 1164 pushop.trmanager.transaction(),
1165 1165 phase,
1166 1166 nodes)
1167 1167 else:
1168 1168 # repo is not locked, do not change any phases!
1169 1169 # Informs the user that phases should have been moved when
1170 1170 # applicable.
1171 1171 actualmoves = [n for n in nodes if phase < pushop.repo[n].phase()]
1172 1172 phasestr = phases.phasenames[phase]
1173 1173 if actualmoves:
1174 1174 pushop.ui.status(_('cannot lock source repo, skipping '
1175 1175 'local %s phase update\n') % phasestr)
1176 1176
1177 1177 def _pushobsolete(pushop):
1178 1178 """utility function to push obsolete markers to a remote"""
1179 1179 if 'obsmarkers' in pushop.stepsdone:
1180 1180 return
1181 1181 repo = pushop.repo
1182 1182 remote = pushop.remote
1183 1183 pushop.stepsdone.add('obsmarkers')
1184 1184 if pushop.outobsmarkers:
1185 1185 pushop.ui.debug('try to push obsolete markers to remote\n')
1186 1186 rslts = []
1187 1187 remotedata = obsolete._pushkeyescape(sorted(pushop.outobsmarkers))
1188 1188 for key in sorted(remotedata, reverse=True):
1189 1189 # reverse sort to ensure we end with dump0
1190 1190 data = remotedata[key]
1191 1191 rslts.append(remote.pushkey('obsolete', key, '', data))
1192 1192 if [r for r in rslts if not r]:
1193 1193 msg = _('failed to push some obsolete markers!\n')
1194 1194 repo.ui.warn(msg)
1195 1195
1196 1196 def _pushbookmark(pushop):
1197 1197 """Update bookmark position on remote"""
1198 1198 if pushop.cgresult == 0 or 'bookmarks' in pushop.stepsdone:
1199 1199 return
1200 1200 pushop.stepsdone.add('bookmarks')
1201 1201 ui = pushop.ui
1202 1202 remote = pushop.remote
1203 1203
1204 1204 for b, old, new in pushop.outbookmarks:
1205 1205 action = 'update'
1206 1206 if not old:
1207 1207 action = 'export'
1208 1208 elif not new:
1209 1209 action = 'delete'
1210 1210 if remote.pushkey('bookmarks', b, old, new):
1211 1211 ui.status(bookmsgmap[action][0] % b)
1212 1212 else:
1213 1213 ui.warn(bookmsgmap[action][1] % b)
1214 1214 # discovery can have set the value form invalid entry
1215 1215 if pushop.bkresult is not None:
1216 1216 pushop.bkresult = 1
1217 1217
1218 1218 class pulloperation(object):
1219 1219 """A object that represent a single pull operation
1220 1220
1221 1221 It purpose is to carry pull related state and very common operation.
1222 1222
1223 1223 A new should be created at the beginning of each pull and discarded
1224 1224 afterward.
1225 1225 """
1226 1226
1227 1227 def __init__(self, repo, remote, heads=None, force=False, bookmarks=(),
1228 1228 remotebookmarks=None, streamclonerequested=None):
1229 1229 # repo we pull into
1230 1230 self.repo = repo
1231 1231 # repo we pull from
1232 1232 self.remote = remote
1233 1233 # revision we try to pull (None is "all")
1234 1234 self.heads = heads
1235 1235 # bookmark pulled explicitly
1236 1236 self.explicitbookmarks = [repo._bookmarks.expandname(bookmark)
1237 1237 for bookmark in bookmarks]
1238 1238 # do we force pull?
1239 1239 self.force = force
1240 1240 # whether a streaming clone was requested
1241 1241 self.streamclonerequested = streamclonerequested
1242 1242 # transaction manager
1243 1243 self.trmanager = None
1244 1244 # set of common changeset between local and remote before pull
1245 1245 self.common = None
1246 1246 # set of pulled head
1247 1247 self.rheads = None
1248 1248 # list of missing changeset to fetch remotely
1249 1249 self.fetch = None
1250 1250 # remote bookmarks data
1251 1251 self.remotebookmarks = remotebookmarks
1252 1252 # result of changegroup pulling (used as return code by pull)
1253 1253 self.cgresult = None
1254 1254 # list of step already done
1255 1255 self.stepsdone = set()
1256 1256 # Whether we attempted a clone from pre-generated bundles.
1257 1257 self.clonebundleattempted = False
1258 1258
1259 1259 @util.propertycache
1260 1260 def pulledsubset(self):
1261 1261 """heads of the set of changeset target by the pull"""
1262 1262 # compute target subset
1263 1263 if self.heads is None:
1264 1264 # We pulled every thing possible
1265 1265 # sync on everything common
1266 1266 c = set(self.common)
1267 1267 ret = list(self.common)
1268 1268 for n in self.rheads:
1269 1269 if n not in c:
1270 1270 ret.append(n)
1271 1271 return ret
1272 1272 else:
1273 1273 # We pulled a specific subset
1274 1274 # sync on this subset
1275 1275 return self.heads
1276 1276
1277 1277 @util.propertycache
1278 1278 def canusebundle2(self):
1279 1279 return not _forcebundle1(self)
1280 1280
1281 1281 @util.propertycache
1282 1282 def remotebundle2caps(self):
1283 1283 return bundle2.bundle2caps(self.remote)
1284 1284
1285 1285 def gettransaction(self):
1286 1286 # deprecated; talk to trmanager directly
1287 1287 return self.trmanager.transaction()
1288 1288
1289 1289 class transactionmanager(util.transactional):
1290 1290 """An object to manage the life cycle of a transaction
1291 1291
1292 1292 It creates the transaction on demand and calls the appropriate hooks when
1293 1293 closing the transaction."""
1294 1294 def __init__(self, repo, source, url):
1295 1295 self.repo = repo
1296 1296 self.source = source
1297 1297 self.url = url
1298 1298 self._tr = None
1299 1299
1300 1300 def transaction(self):
1301 1301 """Return an open transaction object, constructing if necessary"""
1302 1302 if not self._tr:
1303 1303 trname = '%s\n%s' % (self.source, util.hidepassword(self.url))
1304 1304 self._tr = self.repo.transaction(trname)
1305 1305 self._tr.hookargs['source'] = self.source
1306 1306 self._tr.hookargs['url'] = self.url
1307 1307 return self._tr
1308 1308
1309 1309 def close(self):
1310 1310 """close transaction if created"""
1311 1311 if self._tr is not None:
1312 1312 self._tr.close()
1313 1313
1314 1314 def release(self):
1315 1315 """release transaction if created"""
1316 1316 if self._tr is not None:
1317 1317 self._tr.release()
1318 1318
1319 1319 def pull(repo, remote, heads=None, force=False, bookmarks=(), opargs=None,
1320 1320 streamclonerequested=None):
1321 1321 """Fetch repository data from a remote.
1322 1322
1323 1323 This is the main function used to retrieve data from a remote repository.
1324 1324
1325 1325 ``repo`` is the local repository to clone into.
1326 1326 ``remote`` is a peer instance.
1327 1327 ``heads`` is an iterable of revisions we want to pull. ``None`` (the
1328 1328 default) means to pull everything from the remote.
1329 1329 ``bookmarks`` is an iterable of bookmarks requesting to be pulled. By
1330 1330 default, all remote bookmarks are pulled.
1331 1331 ``opargs`` are additional keyword arguments to pass to ``pulloperation``
1332 1332 initialization.
1333 1333 ``streamclonerequested`` is a boolean indicating whether a "streaming
1334 1334 clone" is requested. A "streaming clone" is essentially a raw file copy
1335 1335 of revlogs from the server. This only works when the local repository is
1336 1336 empty. The default value of ``None`` means to respect the server
1337 1337 configuration for preferring stream clones.
1338 1338
1339 1339 Returns the ``pulloperation`` created for this pull.
1340 1340 """
1341 1341 if opargs is None:
1342 1342 opargs = {}
1343 1343 pullop = pulloperation(repo, remote, heads, force, bookmarks=bookmarks,
1344 1344 streamclonerequested=streamclonerequested,
1345 1345 **pycompat.strkwargs(opargs))
1346 1346
1347 1347 peerlocal = pullop.remote.local()
1348 1348 if peerlocal:
1349 1349 missing = set(peerlocal.requirements) - pullop.repo.supported
1350 1350 if missing:
1351 1351 msg = _("required features are not"
1352 1352 " supported in the destination:"
1353 1353 " %s") % (', '.join(sorted(missing)))
1354 1354 raise error.Abort(msg)
1355 1355
1356 1356 pullop.trmanager = transactionmanager(repo, 'pull', remote.url())
1357 1357 with repo.wlock(), repo.lock(), pullop.trmanager:
1358 1358 # This should ideally be in _pullbundle2(). However, it needs to run
1359 1359 # before discovery to avoid extra work.
1360 1360 _maybeapplyclonebundle(pullop)
1361 1361 streamclone.maybeperformlegacystreamclone(pullop)
1362 1362 _pulldiscovery(pullop)
1363 1363 if pullop.canusebundle2:
1364 1364 _pullbundle2(pullop)
1365 1365 _pullchangeset(pullop)
1366 1366 _pullphase(pullop)
1367 1367 _pullbookmarks(pullop)
1368 1368 _pullobsolete(pullop)
1369 1369
1370 1370 # storing remotenames
1371 1371 if repo.ui.configbool('experimental', 'remotenames'):
1372 1372 logexchange.pullremotenames(repo, remote)
1373 1373
1374 1374 return pullop
1375 1375
1376 1376 # list of steps to perform discovery before pull
1377 1377 pulldiscoveryorder = []
1378 1378
1379 1379 # Mapping between step name and function
1380 1380 #
1381 1381 # This exists to help extensions wrap steps if necessary
1382 1382 pulldiscoverymapping = {}
1383 1383
1384 1384 def pulldiscovery(stepname):
1385 1385 """decorator for function performing discovery before pull
1386 1386
1387 1387 The function is added to the step -> function mapping and appended to the
1388 1388 list of steps. Beware that decorated function will be added in order (this
1389 1389 may matter).
1390 1390
1391 1391 You can only use this decorator for a new step, if you want to wrap a step
1392 1392 from an extension, change the pulldiscovery dictionary directly."""
1393 1393 def dec(func):
1394 1394 assert stepname not in pulldiscoverymapping
1395 1395 pulldiscoverymapping[stepname] = func
1396 1396 pulldiscoveryorder.append(stepname)
1397 1397 return func
1398 1398 return dec
1399 1399
1400 1400 def _pulldiscovery(pullop):
1401 1401 """Run all discovery steps"""
1402 1402 for stepname in pulldiscoveryorder:
1403 1403 step = pulldiscoverymapping[stepname]
1404 1404 step(pullop)
1405 1405
1406 1406 @pulldiscovery('b1:bookmarks')
1407 1407 def _pullbookmarkbundle1(pullop):
1408 1408 """fetch bookmark data in bundle1 case
1409 1409
1410 1410 If not using bundle2, we have to fetch bookmarks before changeset
1411 1411 discovery to reduce the chance and impact of race conditions."""
1412 1412 if pullop.remotebookmarks is not None:
1413 1413 return
1414 1414 if pullop.canusebundle2 and 'listkeys' in pullop.remotebundle2caps:
1415 1415 # all known bundle2 servers now support listkeys, but lets be nice with
1416 1416 # new implementation.
1417 1417 return
1418 1418 books = pullop.remote.listkeys('bookmarks')
1419 1419 pullop.remotebookmarks = bookmod.unhexlifybookmarks(books)
1420 1420
1421 1421
1422 1422 @pulldiscovery('changegroup')
1423 1423 def _pulldiscoverychangegroup(pullop):
1424 1424 """discovery phase for the pull
1425 1425
1426 1426 Current handle changeset discovery only, will change handle all discovery
1427 1427 at some point."""
1428 1428 tmp = discovery.findcommonincoming(pullop.repo,
1429 1429 pullop.remote,
1430 1430 heads=pullop.heads,
1431 1431 force=pullop.force)
1432 1432 common, fetch, rheads = tmp
1433 1433 nm = pullop.repo.unfiltered().changelog.nodemap
1434 1434 if fetch and rheads:
1435 1435 # If a remote heads is filtered locally, put in back in common.
1436 1436 #
1437 1437 # This is a hackish solution to catch most of "common but locally
1438 1438 # hidden situation". We do not performs discovery on unfiltered
1439 1439 # repository because it end up doing a pathological amount of round
1440 1440 # trip for w huge amount of changeset we do not care about.
1441 1441 #
1442 1442 # If a set of such "common but filtered" changeset exist on the server
1443 1443 # but are not including a remote heads, we'll not be able to detect it,
1444 1444 scommon = set(common)
1445 1445 for n in rheads:
1446 1446 if n in nm:
1447 1447 if n not in scommon:
1448 1448 common.append(n)
1449 1449 if set(rheads).issubset(set(common)):
1450 1450 fetch = []
1451 1451 pullop.common = common
1452 1452 pullop.fetch = fetch
1453 1453 pullop.rheads = rheads
1454 1454
1455 1455 def _pullbundle2(pullop):
1456 1456 """pull data using bundle2
1457 1457
1458 1458 For now, the only supported data are changegroup."""
1459 1459 kwargs = {'bundlecaps': caps20to10(pullop.repo, role='client')}
1460 1460
1461 1461 # make ui easier to access
1462 1462 ui = pullop.repo.ui
1463 1463
1464 1464 # At the moment we don't do stream clones over bundle2. If that is
1465 1465 # implemented then here's where the check for that will go.
1466 1466 streaming = streamclone.canperformstreamclone(pullop, bundle2=True)[0]
1467 1467
1468 1468 # declare pull perimeters
1469 1469 kwargs['common'] = pullop.common
1470 1470 kwargs['heads'] = pullop.heads or pullop.rheads
1471 1471
1472 1472 if streaming:
1473 1473 kwargs['cg'] = False
1474 1474 kwargs['stream'] = True
1475 1475 pullop.stepsdone.add('changegroup')
1476 1476 pullop.stepsdone.add('phases')
1477 1477
1478 1478 else:
1479 1479 # pulling changegroup
1480 1480 pullop.stepsdone.add('changegroup')
1481 1481
1482 1482 kwargs['cg'] = pullop.fetch
1483 1483
1484 1484 legacyphase = 'phases' in ui.configlist('devel', 'legacy.exchange')
1485 1485 hasbinaryphase = 'heads' in pullop.remotebundle2caps.get('phases', ())
1486 1486 if (not legacyphase and hasbinaryphase):
1487 1487 kwargs['phases'] = True
1488 1488 pullop.stepsdone.add('phases')
1489 1489
1490 1490 if 'listkeys' in pullop.remotebundle2caps:
1491 1491 if 'phases' not in pullop.stepsdone:
1492 1492 kwargs['listkeys'] = ['phases']
1493 1493
1494 1494 bookmarksrequested = False
1495 1495 legacybookmark = 'bookmarks' in ui.configlist('devel', 'legacy.exchange')
1496 1496 hasbinarybook = 'bookmarks' in pullop.remotebundle2caps
1497 1497
1498 1498 if pullop.remotebookmarks is not None:
1499 1499 pullop.stepsdone.add('request-bookmarks')
1500 1500
1501 1501 if ('request-bookmarks' not in pullop.stepsdone
1502 1502 and pullop.remotebookmarks is None
1503 1503 and not legacybookmark and hasbinarybook):
1504 1504 kwargs['bookmarks'] = True
1505 1505 bookmarksrequested = True
1506 1506
1507 1507 if 'listkeys' in pullop.remotebundle2caps:
1508 1508 if 'request-bookmarks' not in pullop.stepsdone:
1509 1509 # make sure to always includes bookmark data when migrating
1510 1510 # `hg incoming --bundle` to using this function.
1511 1511 pullop.stepsdone.add('request-bookmarks')
1512 1512 kwargs.setdefault('listkeys', []).append('bookmarks')
1513 1513
1514 1514 # If this is a full pull / clone and the server supports the clone bundles
1515 1515 # feature, tell the server whether we attempted a clone bundle. The
1516 1516 # presence of this flag indicates the client supports clone bundles. This
1517 1517 # will enable the server to treat clients that support clone bundles
1518 1518 # differently from those that don't.
1519 1519 if (pullop.remote.capable('clonebundles')
1520 1520 and pullop.heads is None and list(pullop.common) == [nullid]):
1521 1521 kwargs['cbattempted'] = pullop.clonebundleattempted
1522 1522
1523 1523 if streaming:
1524 1524 pullop.repo.ui.status(_('streaming all changes\n'))
1525 1525 elif not pullop.fetch:
1526 1526 pullop.repo.ui.status(_("no changes found\n"))
1527 1527 pullop.cgresult = 0
1528 1528 else:
1529 1529 if pullop.heads is None and list(pullop.common) == [nullid]:
1530 1530 pullop.repo.ui.status(_("requesting all changes\n"))
1531 1531 if obsolete.isenabled(pullop.repo, obsolete.exchangeopt):
1532 1532 remoteversions = bundle2.obsmarkersversion(pullop.remotebundle2caps)
1533 1533 if obsolete.commonversion(remoteversions) is not None:
1534 1534 kwargs['obsmarkers'] = True
1535 1535 pullop.stepsdone.add('obsmarkers')
1536 1536 _pullbundle2extraprepare(pullop, kwargs)
1537 1537 bundle = pullop.remote.getbundle('pull', **pycompat.strkwargs(kwargs))
1538 1538 try:
1539 1539 op = bundle2.bundleoperation(pullop.repo, pullop.gettransaction)
1540 1540 op.modes['bookmarks'] = 'records'
1541 1541 bundle2.processbundle(pullop.repo, bundle, op=op)
1542 1542 except bundle2.AbortFromPart as exc:
1543 1543 pullop.repo.ui.status(_('remote: abort: %s\n') % exc)
1544 1544 raise error.Abort(_('pull failed on remote'), hint=exc.hint)
1545 1545 except error.BundleValueError as exc:
1546 1546 raise error.Abort(_('missing support for %s') % exc)
1547 1547
1548 1548 if pullop.fetch:
1549 1549 pullop.cgresult = bundle2.combinechangegroupresults(op)
1550 1550
1551 1551 # processing phases change
1552 1552 for namespace, value in op.records['listkeys']:
1553 1553 if namespace == 'phases':
1554 1554 _pullapplyphases(pullop, value)
1555 1555
1556 1556 # processing bookmark update
1557 1557 if bookmarksrequested:
1558 1558 books = {}
1559 1559 for record in op.records['bookmarks']:
1560 1560 books[record['bookmark']] = record["node"]
1561 1561 pullop.remotebookmarks = books
1562 1562 else:
1563 1563 for namespace, value in op.records['listkeys']:
1564 1564 if namespace == 'bookmarks':
1565 1565 pullop.remotebookmarks = bookmod.unhexlifybookmarks(value)
1566 1566
1567 1567 # bookmark data were either already there or pulled in the bundle
1568 1568 if pullop.remotebookmarks is not None:
1569 1569 _pullbookmarks(pullop)
1570 1570
1571 1571 def _pullbundle2extraprepare(pullop, kwargs):
1572 1572 """hook function so that extensions can extend the getbundle call"""
1573 1573
1574 1574 def _pullchangeset(pullop):
1575 1575 """pull changeset from unbundle into the local repo"""
1576 1576 # We delay the open of the transaction as late as possible so we
1577 1577 # don't open transaction for nothing or you break future useful
1578 1578 # rollback call
1579 1579 if 'changegroup' in pullop.stepsdone:
1580 1580 return
1581 1581 pullop.stepsdone.add('changegroup')
1582 1582 if not pullop.fetch:
1583 1583 pullop.repo.ui.status(_("no changes found\n"))
1584 1584 pullop.cgresult = 0
1585 1585 return
1586 1586 tr = pullop.gettransaction()
1587 1587 if pullop.heads is None and list(pullop.common) == [nullid]:
1588 1588 pullop.repo.ui.status(_("requesting all changes\n"))
1589 1589 elif pullop.heads is None and pullop.remote.capable('changegroupsubset'):
1590 1590 # issue1320, avoid a race if remote changed after discovery
1591 1591 pullop.heads = pullop.rheads
1592 1592
1593 1593 if pullop.remote.capable('getbundle'):
1594 1594 # TODO: get bundlecaps from remote
1595 1595 cg = pullop.remote.getbundle('pull', common=pullop.common,
1596 1596 heads=pullop.heads or pullop.rheads)
1597 1597 elif pullop.heads is None:
1598 1598 cg = pullop.remote.changegroup(pullop.fetch, 'pull')
1599 1599 elif not pullop.remote.capable('changegroupsubset'):
1600 1600 raise error.Abort(_("partial pull cannot be done because "
1601 1601 "other repository doesn't support "
1602 1602 "changegroupsubset."))
1603 1603 else:
1604 1604 cg = pullop.remote.changegroupsubset(pullop.fetch, pullop.heads, 'pull')
1605 1605 bundleop = bundle2.applybundle(pullop.repo, cg, tr, 'pull',
1606 1606 pullop.remote.url())
1607 1607 pullop.cgresult = bundle2.combinechangegroupresults(bundleop)
1608 1608
1609 1609 def _pullphase(pullop):
1610 1610 # Get remote phases data from remote
1611 1611 if 'phases' in pullop.stepsdone:
1612 1612 return
1613 1613 remotephases = pullop.remote.listkeys('phases')
1614 1614 _pullapplyphases(pullop, remotephases)
1615 1615
1616 1616 def _pullapplyphases(pullop, remotephases):
1617 1617 """apply phase movement from observed remote state"""
1618 1618 if 'phases' in pullop.stepsdone:
1619 1619 return
1620 1620 pullop.stepsdone.add('phases')
1621 1621 publishing = bool(remotephases.get('publishing', False))
1622 1622 if remotephases and not publishing:
1623 1623 # remote is new and non-publishing
1624 1624 pheads, _dr = phases.analyzeremotephases(pullop.repo,
1625 1625 pullop.pulledsubset,
1626 1626 remotephases)
1627 1627 dheads = pullop.pulledsubset
1628 1628 else:
1629 1629 # Remote is old or publishing all common changesets
1630 1630 # should be seen as public
1631 1631 pheads = pullop.pulledsubset
1632 1632 dheads = []
1633 1633 unfi = pullop.repo.unfiltered()
1634 1634 phase = unfi._phasecache.phase
1635 1635 rev = unfi.changelog.nodemap.get
1636 1636 public = phases.public
1637 1637 draft = phases.draft
1638 1638
1639 1639 # exclude changesets already public locally and update the others
1640 1640 pheads = [pn for pn in pheads if phase(unfi, rev(pn)) > public]
1641 1641 if pheads:
1642 1642 tr = pullop.gettransaction()
1643 1643 phases.advanceboundary(pullop.repo, tr, public, pheads)
1644 1644
1645 1645 # exclude changesets already draft locally and update the others
1646 1646 dheads = [pn for pn in dheads if phase(unfi, rev(pn)) > draft]
1647 1647 if dheads:
1648 1648 tr = pullop.gettransaction()
1649 1649 phases.advanceboundary(pullop.repo, tr, draft, dheads)
1650 1650
1651 1651 def _pullbookmarks(pullop):
1652 1652 """process the remote bookmark information to update the local one"""
1653 1653 if 'bookmarks' in pullop.stepsdone:
1654 1654 return
1655 1655 pullop.stepsdone.add('bookmarks')
1656 1656 repo = pullop.repo
1657 1657 remotebookmarks = pullop.remotebookmarks
1658 1658 bookmod.updatefromremote(repo.ui, repo, remotebookmarks,
1659 1659 pullop.remote.url(),
1660 1660 pullop.gettransaction,
1661 1661 explicit=pullop.explicitbookmarks)
1662 1662
1663 1663 def _pullobsolete(pullop):
1664 1664 """utility function to pull obsolete markers from a remote
1665 1665
1666 1666 The `gettransaction` is function that return the pull transaction, creating
1667 1667 one if necessary. We return the transaction to inform the calling code that
1668 1668 a new transaction have been created (when applicable).
1669 1669
1670 1670 Exists mostly to allow overriding for experimentation purpose"""
1671 1671 if 'obsmarkers' in pullop.stepsdone:
1672 1672 return
1673 1673 pullop.stepsdone.add('obsmarkers')
1674 1674 tr = None
1675 1675 if obsolete.isenabled(pullop.repo, obsolete.exchangeopt):
1676 1676 pullop.repo.ui.debug('fetching remote obsolete markers\n')
1677 1677 remoteobs = pullop.remote.listkeys('obsolete')
1678 1678 if 'dump0' in remoteobs:
1679 1679 tr = pullop.gettransaction()
1680 1680 markers = []
1681 1681 for key in sorted(remoteobs, reverse=True):
1682 1682 if key.startswith('dump'):
1683 1683 data = util.b85decode(remoteobs[key])
1684 1684 version, newmarks = obsolete._readmarkers(data)
1685 1685 markers += newmarks
1686 1686 if markers:
1687 1687 pullop.repo.obsstore.add(tr, markers)
1688 1688 pullop.repo.invalidatevolatilesets()
1689 1689 return tr
1690 1690
1691 1691 def caps20to10(repo, role):
1692 1692 """return a set with appropriate options to use bundle20 during getbundle"""
1693 1693 caps = {'HG20'}
1694 1694 capsblob = bundle2.encodecaps(bundle2.getrepocaps(repo, role=role))
1695 1695 caps.add('bundle2=' + urlreq.quote(capsblob))
1696 1696 return caps
1697 1697
1698 1698 # List of names of steps to perform for a bundle2 for getbundle, order matters.
1699 1699 getbundle2partsorder = []
1700 1700
1701 1701 # Mapping between step name and function
1702 1702 #
1703 1703 # This exists to help extensions wrap steps if necessary
1704 1704 getbundle2partsmapping = {}
1705 1705
1706 1706 def getbundle2partsgenerator(stepname, idx=None):
1707 1707 """decorator for function generating bundle2 part for getbundle
1708 1708
1709 1709 The function is added to the step -> function mapping and appended to the
1710 1710 list of steps. Beware that decorated functions will be added in order
1711 1711 (this may matter).
1712 1712
1713 1713 You can only use this decorator for new steps, if you want to wrap a step
1714 1714 from an extension, attack the getbundle2partsmapping dictionary directly."""
1715 1715 def dec(func):
1716 1716 assert stepname not in getbundle2partsmapping
1717 1717 getbundle2partsmapping[stepname] = func
1718 1718 if idx is None:
1719 1719 getbundle2partsorder.append(stepname)
1720 1720 else:
1721 1721 getbundle2partsorder.insert(idx, stepname)
1722 1722 return func
1723 1723 return dec
1724 1724
1725 1725 def bundle2requested(bundlecaps):
1726 1726 if bundlecaps is not None:
1727 1727 return any(cap.startswith('HG2') for cap in bundlecaps)
1728 1728 return False
1729 1729
1730 1730 def getbundlechunks(repo, source, heads=None, common=None, bundlecaps=None,
1731 1731 **kwargs):
1732 1732 """Return chunks constituting a bundle's raw data.
1733 1733
1734 1734 Could be a bundle HG10 or a bundle HG20 depending on bundlecaps
1735 1735 passed.
1736 1736
1737 1737 Returns a 2-tuple of a dict with metadata about the generated bundle
1738 1738 and an iterator over raw chunks (of varying sizes).
1739 1739 """
1740 1740 kwargs = pycompat.byteskwargs(kwargs)
1741 1741 info = {}
1742 1742 usebundle2 = bundle2requested(bundlecaps)
1743 1743 # bundle10 case
1744 1744 if not usebundle2:
1745 1745 if bundlecaps and not kwargs.get('cg', True):
1746 1746 raise ValueError(_('request for bundle10 must include changegroup'))
1747 1747
1748 1748 if kwargs:
1749 1749 raise ValueError(_('unsupported getbundle arguments: %s')
1750 1750 % ', '.join(sorted(kwargs.keys())))
1751 1751 outgoing = _computeoutgoing(repo, heads, common)
1752 1752 info['bundleversion'] = 1
1753 1753 return info, changegroup.makestream(repo, outgoing, '01', source,
1754 1754 bundlecaps=bundlecaps)
1755 1755
1756 1756 # bundle20 case
1757 1757 info['bundleversion'] = 2
1758 1758 b2caps = {}
1759 1759 for bcaps in bundlecaps:
1760 1760 if bcaps.startswith('bundle2='):
1761 1761 blob = urlreq.unquote(bcaps[len('bundle2='):])
1762 1762 b2caps.update(bundle2.decodecaps(blob))
1763 1763 bundler = bundle2.bundle20(repo.ui, b2caps)
1764 1764
1765 1765 kwargs['heads'] = heads
1766 1766 kwargs['common'] = common
1767 1767
1768 1768 for name in getbundle2partsorder:
1769 1769 func = getbundle2partsmapping[name]
1770 1770 func(bundler, repo, source, bundlecaps=bundlecaps, b2caps=b2caps,
1771 1771 **pycompat.strkwargs(kwargs))
1772 1772
1773 1773 info['prefercompressed'] = bundler.prefercompressed
1774 1774
1775 1775 return info, bundler.getchunks()
1776 1776
1777 1777 @getbundle2partsgenerator('stream2')
1778 1778 def _getbundlestream2(bundler, repo, source, bundlecaps=None,
1779 1779 b2caps=None, heads=None, common=None, **kwargs):
1780 1780 if not kwargs.get('stream', False):
1781 1781 return
1782 1782
1783 1783 if not streamclone.allowservergeneration(repo):
1784 1784 raise error.Abort(_('stream data requested but server does not allow '
1785 1785 'this feature'),
1786 1786 hint=_('well-behaved clients should not be '
1787 1787 'requesting stream data from servers not '
1788 1788 'advertising it; the client may be buggy'))
1789 1789
1790 1790 # Stream clones don't compress well. And compression undermines a
1791 1791 # goal of stream clones, which is to be fast. Communicate the desire
1792 1792 # to avoid compression to consumers of the bundle.
1793 1793 bundler.prefercompressed = False
1794 1794
1795 1795 filecount, bytecount, it = streamclone.generatev2(repo)
1796 1796 requirements = _formatrequirementsspec(repo.requirements)
1797 1797 part = bundler.newpart('stream2', data=it)
1798 1798 part.addparam('bytecount', '%d' % bytecount, mandatory=True)
1799 1799 part.addparam('filecount', '%d' % filecount, mandatory=True)
1800 1800 part.addparam('requirements', requirements, mandatory=True)
1801 1801
1802 1802 @getbundle2partsgenerator('changegroup')
1803 1803 def _getbundlechangegrouppart(bundler, repo, source, bundlecaps=None,
1804 1804 b2caps=None, heads=None, common=None, **kwargs):
1805 1805 """add a changegroup part to the requested bundle"""
1806 1806 cgstream = None
1807 1807 if kwargs.get(r'cg', True):
1808 1808 # build changegroup bundle here.
1809 1809 version = '01'
1810 1810 cgversions = b2caps.get('changegroup')
1811 1811 if cgversions: # 3.1 and 3.2 ship with an empty value
1812 1812 cgversions = [v for v in cgversions
1813 1813 if v in changegroup.supportedoutgoingversions(repo)]
1814 1814 if not cgversions:
1815 1815 raise ValueError(_('no common changegroup version'))
1816 1816 version = max(cgversions)
1817 1817 outgoing = _computeoutgoing(repo, heads, common)
1818 1818 if outgoing.missing:
1819 1819 cgstream = changegroup.makestream(repo, outgoing, version, source,
1820 1820 bundlecaps=bundlecaps)
1821 1821
1822 1822 if cgstream:
1823 1823 part = bundler.newpart('changegroup', data=cgstream)
1824 1824 if cgversions:
1825 1825 part.addparam('version', version)
1826 1826 part.addparam('nbchanges', '%d' % len(outgoing.missing),
1827 1827 mandatory=False)
1828 1828 if 'treemanifest' in repo.requirements:
1829 1829 part.addparam('treemanifest', '1')
1830 1830
1831 1831 @getbundle2partsgenerator('bookmarks')
1832 1832 def _getbundlebookmarkpart(bundler, repo, source, bundlecaps=None,
1833 1833 b2caps=None, **kwargs):
1834 1834 """add a bookmark part to the requested bundle"""
1835 1835 if not kwargs.get(r'bookmarks', False):
1836 1836 return
1837 1837 if 'bookmarks' not in b2caps:
1838 1838 raise ValueError(_('no common bookmarks exchange method'))
1839 1839 books = bookmod.listbinbookmarks(repo)
1840 1840 data = bookmod.binaryencode(books)
1841 1841 if data:
1842 1842 bundler.newpart('bookmarks', data=data)
1843 1843
1844 1844 @getbundle2partsgenerator('listkeys')
1845 1845 def _getbundlelistkeysparts(bundler, repo, source, bundlecaps=None,
1846 1846 b2caps=None, **kwargs):
1847 1847 """add parts containing listkeys namespaces to the requested bundle"""
1848 1848 listkeys = kwargs.get(r'listkeys', ())
1849 1849 for namespace in listkeys:
1850 1850 part = bundler.newpart('listkeys')
1851 1851 part.addparam('namespace', namespace)
1852 1852 keys = repo.listkeys(namespace).items()
1853 1853 part.data = pushkey.encodekeys(keys)
1854 1854
1855 1855 @getbundle2partsgenerator('obsmarkers')
1856 1856 def _getbundleobsmarkerpart(bundler, repo, source, bundlecaps=None,
1857 1857 b2caps=None, heads=None, **kwargs):
1858 1858 """add an obsolescence markers part to the requested bundle"""
1859 1859 if kwargs.get(r'obsmarkers', False):
1860 1860 if heads is None:
1861 1861 heads = repo.heads()
1862 1862 subset = [c.node() for c in repo.set('::%ln', heads)]
1863 1863 markers = repo.obsstore.relevantmarkers(subset)
1864 1864 markers = sorted(markers)
1865 1865 bundle2.buildobsmarkerspart(bundler, markers)
1866 1866
1867 1867 @getbundle2partsgenerator('phases')
1868 1868 def _getbundlephasespart(bundler, repo, source, bundlecaps=None,
1869 1869 b2caps=None, heads=None, **kwargs):
1870 1870 """add phase heads part to the requested bundle"""
1871 1871 if kwargs.get(r'phases', False):
1872 1872 if not 'heads' in b2caps.get('phases'):
1873 1873 raise ValueError(_('no common phases exchange method'))
1874 1874 if heads is None:
1875 1875 heads = repo.heads()
1876 1876
1877 1877 headsbyphase = collections.defaultdict(set)
1878 1878 if repo.publishing():
1879 1879 headsbyphase[phases.public] = heads
1880 1880 else:
1881 1881 # find the appropriate heads to move
1882 1882
1883 1883 phase = repo._phasecache.phase
1884 1884 node = repo.changelog.node
1885 1885 rev = repo.changelog.rev
1886 1886 for h in heads:
1887 1887 headsbyphase[phase(repo, rev(h))].add(h)
1888 1888 seenphases = list(headsbyphase.keys())
1889 1889
1890 1890 # We do not handle anything but public and draft phase for now)
1891 1891 if seenphases:
1892 1892 assert max(seenphases) <= phases.draft
1893 1893
1894 1894 # if client is pulling non-public changesets, we need to find
1895 1895 # intermediate public heads.
1896 1896 draftheads = headsbyphase.get(phases.draft, set())
1897 1897 if draftheads:
1898 1898 publicheads = headsbyphase.get(phases.public, set())
1899 1899
1900 1900 revset = 'heads(only(%ln, %ln) and public())'
1901 1901 extraheads = repo.revs(revset, draftheads, publicheads)
1902 1902 for r in extraheads:
1903 1903 headsbyphase[phases.public].add(node(r))
1904 1904
1905 1905 # transform data in a format used by the encoding function
1906 1906 phasemapping = []
1907 1907 for phase in phases.allphases:
1908 1908 phasemapping.append(sorted(headsbyphase[phase]))
1909 1909
1910 1910 # generate the actual part
1911 1911 phasedata = phases.binaryencode(phasemapping)
1912 1912 bundler.newpart('phase-heads', data=phasedata)
1913 1913
1914 1914 @getbundle2partsgenerator('hgtagsfnodes')
1915 1915 def _getbundletagsfnodes(bundler, repo, source, bundlecaps=None,
1916 1916 b2caps=None, heads=None, common=None,
1917 1917 **kwargs):
1918 1918 """Transfer the .hgtags filenodes mapping.
1919 1919
1920 1920 Only values for heads in this bundle will be transferred.
1921 1921
1922 1922 The part data consists of pairs of 20 byte changeset node and .hgtags
1923 1923 filenodes raw values.
1924 1924 """
1925 1925 # Don't send unless:
1926 1926 # - changeset are being exchanged,
1927 1927 # - the client supports it.
1928 1928 if not (kwargs.get(r'cg', True) and 'hgtagsfnodes' in b2caps):
1929 1929 return
1930 1930
1931 1931 outgoing = _computeoutgoing(repo, heads, common)
1932 1932 bundle2.addparttagsfnodescache(repo, bundler, outgoing)
1933 1933
1934 1934 def check_heads(repo, their_heads, context):
1935 1935 """check if the heads of a repo have been modified
1936 1936
1937 1937 Used by peer for unbundling.
1938 1938 """
1939 1939 heads = repo.heads()
1940 1940 heads_hash = hashlib.sha1(''.join(sorted(heads))).digest()
1941 1941 if not (their_heads == ['force'] or their_heads == heads or
1942 1942 their_heads == ['hashed', heads_hash]):
1943 1943 # someone else committed/pushed/unbundled while we
1944 1944 # were transferring data
1945 1945 raise error.PushRaced('repository changed while %s - '
1946 1946 'please try again' % context)
1947 1947
1948 1948 def unbundle(repo, cg, heads, source, url):
1949 1949 """Apply a bundle to a repo.
1950 1950
1951 1951 this function makes sure the repo is locked during the application and have
1952 1952 mechanism to check that no push race occurred between the creation of the
1953 1953 bundle and its application.
1954 1954
1955 1955 If the push was raced as PushRaced exception is raised."""
1956 1956 r = 0
1957 1957 # need a transaction when processing a bundle2 stream
1958 1958 # [wlock, lock, tr] - needs to be an array so nested functions can modify it
1959 1959 lockandtr = [None, None, None]
1960 1960 recordout = None
1961 1961 # quick fix for output mismatch with bundle2 in 3.4
1962 1962 captureoutput = repo.ui.configbool('experimental', 'bundle2-output-capture')
1963 1963 if url.startswith('remote:http:') or url.startswith('remote:https:'):
1964 1964 captureoutput = True
1965 1965 try:
1966 1966 # note: outside bundle1, 'heads' is expected to be empty and this
1967 1967 # 'check_heads' call wil be a no-op
1968 1968 check_heads(repo, heads, 'uploading changes')
1969 1969 # push can proceed
1970 1970 if not isinstance(cg, bundle2.unbundle20):
1971 1971 # legacy case: bundle1 (changegroup 01)
1972 1972 txnname = "\n".join([source, util.hidepassword(url)])
1973 1973 with repo.lock(), repo.transaction(txnname) as tr:
1974 1974 op = bundle2.applybundle(repo, cg, tr, source, url)
1975 1975 r = bundle2.combinechangegroupresults(op)
1976 1976 else:
1977 1977 r = None
1978 1978 try:
1979 1979 def gettransaction():
1980 1980 if not lockandtr[2]:
1981 1981 lockandtr[0] = repo.wlock()
1982 1982 lockandtr[1] = repo.lock()
1983 1983 lockandtr[2] = repo.transaction(source)
1984 1984 lockandtr[2].hookargs['source'] = source
1985 1985 lockandtr[2].hookargs['url'] = url
1986 1986 lockandtr[2].hookargs['bundle2'] = '1'
1987 1987 return lockandtr[2]
1988 1988
1989 1989 # Do greedy locking by default until we're satisfied with lazy
1990 1990 # locking.
1991 1991 if not repo.ui.configbool('experimental', 'bundle2lazylocking'):
1992 1992 gettransaction()
1993 1993
1994 1994 op = bundle2.bundleoperation(repo, gettransaction,
1995 1995 captureoutput=captureoutput)
1996 1996 try:
1997 1997 op = bundle2.processbundle(repo, cg, op=op)
1998 1998 finally:
1999 1999 r = op.reply
2000 2000 if captureoutput and r is not None:
2001 2001 repo.ui.pushbuffer(error=True, subproc=True)
2002 2002 def recordout(output):
2003 2003 r.newpart('output', data=output, mandatory=False)
2004 2004 if lockandtr[2] is not None:
2005 2005 lockandtr[2].close()
2006 2006 except BaseException as exc:
2007 2007 exc.duringunbundle2 = True
2008 2008 if captureoutput and r is not None:
2009 2009 parts = exc._bundle2salvagedoutput = r.salvageoutput()
2010 2010 def recordout(output):
2011 2011 part = bundle2.bundlepart('output', data=output,
2012 2012 mandatory=False)
2013 2013 parts.append(part)
2014 2014 raise
2015 2015 finally:
2016 2016 lockmod.release(lockandtr[2], lockandtr[1], lockandtr[0])
2017 2017 if recordout is not None:
2018 2018 recordout(repo.ui.popbuffer())
2019 2019 return r
2020 2020
2021 2021 def _maybeapplyclonebundle(pullop):
2022 2022 """Apply a clone bundle from a remote, if possible."""
2023 2023
2024 2024 repo = pullop.repo
2025 2025 remote = pullop.remote
2026 2026
2027 2027 if not repo.ui.configbool('ui', 'clonebundles'):
2028 2028 return
2029 2029
2030 2030 # Only run if local repo is empty.
2031 2031 if len(repo):
2032 2032 return
2033 2033
2034 2034 if pullop.heads:
2035 2035 return
2036 2036
2037 2037 if not remote.capable('clonebundles'):
2038 2038 return
2039 2039
2040 2040 res = remote._call('clonebundles')
2041 2041
2042 2042 # If we call the wire protocol command, that's good enough to record the
2043 2043 # attempt.
2044 2044 pullop.clonebundleattempted = True
2045 2045
2046 2046 entries = parseclonebundlesmanifest(repo, res)
2047 2047 if not entries:
2048 2048 repo.ui.note(_('no clone bundles available on remote; '
2049 2049 'falling back to regular clone\n'))
2050 2050 return
2051 2051
2052 2052 entries = filterclonebundleentries(
2053 2053 repo, entries, streamclonerequested=pullop.streamclonerequested)
2054 2054
2055 2055 if not entries:
2056 2056 # There is a thundering herd concern here. However, if a server
2057 2057 # operator doesn't advertise bundles appropriate for its clients,
2058 2058 # they deserve what's coming. Furthermore, from a client's
2059 2059 # perspective, no automatic fallback would mean not being able to
2060 2060 # clone!
2061 2061 repo.ui.warn(_('no compatible clone bundles available on server; '
2062 2062 'falling back to regular clone\n'))
2063 2063 repo.ui.warn(_('(you may want to report this to the server '
2064 2064 'operator)\n'))
2065 2065 return
2066 2066
2067 2067 entries = sortclonebundleentries(repo.ui, entries)
2068 2068
2069 2069 url = entries[0]['URL']
2070 2070 repo.ui.status(_('applying clone bundle from %s\n') % url)
2071 2071 if trypullbundlefromurl(repo.ui, repo, url):
2072 2072 repo.ui.status(_('finished applying clone bundle\n'))
2073 2073 # Bundle failed.
2074 2074 #
2075 2075 # We abort by default to avoid the thundering herd of
2076 2076 # clients flooding a server that was expecting expensive
2077 2077 # clone load to be offloaded.
2078 2078 elif repo.ui.configbool('ui', 'clonebundlefallback'):
2079 2079 repo.ui.warn(_('falling back to normal clone\n'))
2080 2080 else:
2081 2081 raise error.Abort(_('error applying bundle'),
2082 2082 hint=_('if this error persists, consider contacting '
2083 2083 'the server operator or disable clone '
2084 2084 'bundles via '
2085 2085 '"--config ui.clonebundles=false"'))
2086 2086
2087 2087 def parseclonebundlesmanifest(repo, s):
2088 2088 """Parses the raw text of a clone bundles manifest.
2089 2089
2090 2090 Returns a list of dicts. The dicts have a ``URL`` key corresponding
2091 2091 to the URL and other keys are the attributes for the entry.
2092 2092 """
2093 2093 m = []
2094 2094 for line in s.splitlines():
2095 2095 fields = line.split()
2096 2096 if not fields:
2097 2097 continue
2098 2098 attrs = {'URL': fields[0]}
2099 2099 for rawattr in fields[1:]:
2100 2100 key, value = rawattr.split('=', 1)
2101 2101 key = urlreq.unquote(key)
2102 2102 value = urlreq.unquote(value)
2103 2103 attrs[key] = value
2104 2104
2105 2105 # Parse BUNDLESPEC into components. This makes client-side
2106 2106 # preferences easier to specify since you can prefer a single
2107 2107 # component of the BUNDLESPEC.
2108 2108 if key == 'BUNDLESPEC':
2109 2109 try:
2110 2110 comp, version, params = parsebundlespec(repo, value,
2111 2111 externalnames=True)
2112 2112 attrs['COMPRESSION'] = comp
2113 2113 attrs['VERSION'] = version
2114 2114 except error.InvalidBundleSpecification:
2115 2115 pass
2116 2116 except error.UnsupportedBundleSpecification:
2117 2117 pass
2118 2118
2119 2119 m.append(attrs)
2120 2120
2121 2121 return m
2122 2122
2123 2123 def filterclonebundleentries(repo, entries, streamclonerequested=False):
2124 2124 """Remove incompatible clone bundle manifest entries.
2125 2125
2126 2126 Accepts a list of entries parsed with ``parseclonebundlesmanifest``
2127 2127 and returns a new list consisting of only the entries that this client
2128 2128 should be able to apply.
2129 2129
2130 2130 There is no guarantee we'll be able to apply all returned entries because
2131 2131 the metadata we use to filter on may be missing or wrong.
2132 2132 """
2133 2133 newentries = []
2134 2134 for entry in entries:
2135 2135 spec = entry.get('BUNDLESPEC')
2136 2136 if spec:
2137 2137 try:
2138 2138 comp, version, params = parsebundlespec(repo, spec, strict=True)
2139 2139
2140 2140 # If a stream clone was requested, filter out non-streamclone
2141 2141 # entries.
2142 2142 if streamclonerequested and (comp != 'UN' or version != 's1'):
2143 2143 repo.ui.debug('filtering %s because not a stream clone\n' %
2144 2144 entry['URL'])
2145 2145 continue
2146 2146
2147 2147 except error.InvalidBundleSpecification as e:
2148 2148 repo.ui.debug(str(e) + '\n')
2149 2149 continue
2150 2150 except error.UnsupportedBundleSpecification as e:
2151 2151 repo.ui.debug('filtering %s because unsupported bundle '
2152 2152 'spec: %s\n' % (
2153 2153 entry['URL'], util.forcebytestr(e)))
2154 2154 continue
2155 2155 # If we don't have a spec and requested a stream clone, we don't know
2156 2156 # what the entry is so don't attempt to apply it.
2157 2157 elif streamclonerequested:
2158 2158 repo.ui.debug('filtering %s because cannot determine if a stream '
2159 2159 'clone bundle\n' % entry['URL'])
2160 2160 continue
2161 2161
2162 2162 if 'REQUIRESNI' in entry and not sslutil.hassni:
2163 2163 repo.ui.debug('filtering %s because SNI not supported\n' %
2164 2164 entry['URL'])
2165 2165 continue
2166 2166
2167 2167 newentries.append(entry)
2168 2168
2169 2169 return newentries
2170 2170
2171 2171 class clonebundleentry(object):
2172 2172 """Represents an item in a clone bundles manifest.
2173 2173
2174 2174 This rich class is needed to support sorting since sorted() in Python 3
2175 2175 doesn't support ``cmp`` and our comparison is complex enough that ``key=``
2176 2176 won't work.
2177 2177 """
2178 2178
2179 2179 def __init__(self, value, prefers):
2180 2180 self.value = value
2181 2181 self.prefers = prefers
2182 2182
2183 2183 def _cmp(self, other):
2184 2184 for prefkey, prefvalue in self.prefers:
2185 2185 avalue = self.value.get(prefkey)
2186 2186 bvalue = other.value.get(prefkey)
2187 2187
2188 2188 # Special case for b missing attribute and a matches exactly.
2189 2189 if avalue is not None and bvalue is None and avalue == prefvalue:
2190 2190 return -1
2191 2191
2192 2192 # Special case for a missing attribute and b matches exactly.
2193 2193 if bvalue is not None and avalue is None and bvalue == prefvalue:
2194 2194 return 1
2195 2195
2196 2196 # We can't compare unless attribute present on both.
2197 2197 if avalue is None or bvalue is None:
2198 2198 continue
2199 2199
2200 2200 # Same values should fall back to next attribute.
2201 2201 if avalue == bvalue:
2202 2202 continue
2203 2203
2204 2204 # Exact matches come first.
2205 2205 if avalue == prefvalue:
2206 2206 return -1
2207 2207 if bvalue == prefvalue:
2208 2208 return 1
2209 2209
2210 2210 # Fall back to next attribute.
2211 2211 continue
2212 2212
2213 2213 # If we got here we couldn't sort by attributes and prefers. Fall
2214 2214 # back to index order.
2215 2215 return 0
2216 2216
2217 2217 def __lt__(self, other):
2218 2218 return self._cmp(other) < 0
2219 2219
2220 2220 def __gt__(self, other):
2221 2221 return self._cmp(other) > 0
2222 2222
2223 2223 def __eq__(self, other):
2224 2224 return self._cmp(other) == 0
2225 2225
2226 2226 def __le__(self, other):
2227 2227 return self._cmp(other) <= 0
2228 2228
2229 2229 def __ge__(self, other):
2230 2230 return self._cmp(other) >= 0
2231 2231
2232 2232 def __ne__(self, other):
2233 2233 return self._cmp(other) != 0
2234 2234
2235 2235 def sortclonebundleentries(ui, entries):
2236 2236 prefers = ui.configlist('ui', 'clonebundleprefers')
2237 2237 if not prefers:
2238 2238 return list(entries)
2239 2239
2240 2240 prefers = [p.split('=', 1) for p in prefers]
2241 2241
2242 2242 items = sorted(clonebundleentry(v, prefers) for v in entries)
2243 2243 return [i.value for i in items]
2244 2244
2245 2245 def trypullbundlefromurl(ui, repo, url):
2246 2246 """Attempt to apply a bundle from a URL."""
2247 2247 with repo.lock(), repo.transaction('bundleurl') as tr:
2248 2248 try:
2249 2249 fh = urlmod.open(ui, url)
2250 2250 cg = readbundle(ui, fh, 'stream')
2251 2251
2252 2252 if isinstance(cg, streamclone.streamcloneapplier):
2253 2253 cg.apply(repo)
2254 2254 else:
2255 2255 bundle2.applybundle(repo, cg, tr, 'clonebundles', url)
2256 2256 return True
2257 2257 except urlerr.httperror as e:
2258 2258 ui.warn(_('HTTP error fetching bundle: %s\n') %
2259 2259 util.forcebytestr(e))
2260 2260 except urlerr.urlerror as e:
2261 2261 ui.warn(_('error fetching bundle: %s\n') % e.reason)
2262 2262
2263 2263 return False
@@ -1,1041 +1,1041 b''
1 1 # obsolete.py - obsolete markers handling
2 2 #
3 3 # Copyright 2012 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
4 4 # Logilab SA <contact@logilab.fr>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 """Obsolete marker handling
10 10
11 11 An obsolete marker maps an old changeset to a list of new
12 12 changesets. If the list of new changesets is empty, the old changeset
13 13 is said to be "killed". Otherwise, the old changeset is being
14 14 "replaced" by the new changesets.
15 15
16 16 Obsolete markers can be used to record and distribute changeset graph
17 17 transformations performed by history rewrite operations, and help
18 18 building new tools to reconcile conflicting rewrite actions. To
19 19 facilitate conflict resolution, markers include various annotations
20 20 besides old and news changeset identifiers, such as creation date or
21 21 author name.
22 22
23 23 The old obsoleted changeset is called a "predecessor" and possible
24 24 replacements are called "successors". Markers that used changeset X as
25 25 a predecessor are called "successor markers of X" because they hold
26 26 information about the successors of X. Markers that use changeset Y as
27 27 a successors are call "predecessor markers of Y" because they hold
28 28 information about the predecessors of Y.
29 29
30 30 Examples:
31 31
32 32 - When changeset A is replaced by changeset A', one marker is stored:
33 33
34 34 (A, (A',))
35 35
36 36 - When changesets A and B are folded into a new changeset C, two markers are
37 37 stored:
38 38
39 39 (A, (C,)) and (B, (C,))
40 40
41 41 - When changeset A is simply "pruned" from the graph, a marker is created:
42 42
43 43 (A, ())
44 44
45 45 - When changeset A is split into B and C, a single marker is used:
46 46
47 47 (A, (B, C))
48 48
49 49 We use a single marker to distinguish the "split" case from the "divergence"
50 50 case. If two independent operations rewrite the same changeset A in to A' and
51 51 A'', we have an error case: divergent rewriting. We can detect it because
52 52 two markers will be created independently:
53 53
54 54 (A, (B,)) and (A, (C,))
55 55
56 56 Format
57 57 ------
58 58
59 59 Markers are stored in an append-only file stored in
60 60 '.hg/store/obsstore'.
61 61
62 62 The file starts with a version header:
63 63
64 64 - 1 unsigned byte: version number, starting at zero.
65 65
66 66 The header is followed by the markers. Marker format depend of the version. See
67 67 comment associated with each format for details.
68 68
69 69 """
70 70 from __future__ import absolute_import
71 71
72 72 import errno
73 73 import struct
74 74
75 75 from .i18n import _
76 76 from . import (
77 77 error,
78 78 node,
79 79 obsutil,
80 80 phases,
81 81 policy,
82 82 util,
83 83 )
84 84
85 85 parsers = policy.importmod(r'parsers')
86 86
87 87 _pack = struct.pack
88 88 _unpack = struct.unpack
89 89 _calcsize = struct.calcsize
90 90 propertycache = util.propertycache
91 91
92 92 # the obsolete feature is not mature enough to be enabled by default.
93 93 # you have to rely on third party extension extension to enable this.
94 94 _enabled = False
95 95
96 96 # Options for obsolescence
97 97 createmarkersopt = 'createmarkers'
98 98 allowunstableopt = 'allowunstable'
99 99 exchangeopt = 'exchange'
100 100
101 101 def _getoptionvalue(repo, option):
102 102 """Returns True if the given repository has the given obsolete option
103 103 enabled.
104 104 """
105 105 configkey = 'evolution.%s' % option
106 106 newconfig = repo.ui.configbool('experimental', configkey)
107 107
108 108 # Return the value only if defined
109 109 if newconfig is not None:
110 110 return newconfig
111 111
112 112 # Fallback on generic option
113 113 try:
114 114 return repo.ui.configbool('experimental', 'evolution')
115 115 except (error.ConfigError, AttributeError):
116 116 # Fallback on old-fashion config
117 117 # inconsistent config: experimental.evolution
118 118 result = set(repo.ui.configlist('experimental', 'evolution'))
119 119
120 120 if 'all' in result:
121 121 return True
122 122
123 123 # For migration purposes, temporarily return true if the config hasn't
124 124 # been set but _enabled is true.
125 125 if len(result) == 0 and _enabled:
126 126 return True
127 127
128 128 # Temporary hack for next check
129 129 newconfig = repo.ui.config('experimental', 'evolution.createmarkers')
130 130 if newconfig:
131 131 result.add('createmarkers')
132 132
133 133 return option in result
134 134
135 135 def isenabled(repo, option):
136 136 """Returns True if the given repository has the given obsolete option
137 137 enabled.
138 138 """
139 139 createmarkersvalue = _getoptionvalue(repo, createmarkersopt)
140 140 unstabluevalue = _getoptionvalue(repo, allowunstableopt)
141 141 exchangevalue = _getoptionvalue(repo, exchangeopt)
142 142
143 143 # createmarkers must be enabled if other options are enabled
144 144 if ((unstabluevalue or exchangevalue) and not createmarkersvalue):
145 145 raise error.Abort(_("'createmarkers' obsolete option must be enabled "
146 146 "if other obsolete options are enabled"))
147 147
148 148 return _getoptionvalue(repo, option)
149 149
150 150 ### obsolescence marker flag
151 151
152 152 ## bumpedfix flag
153 153 #
154 154 # When a changeset A' succeed to a changeset A which became public, we call A'
155 155 # "bumped" because it's a successors of a public changesets
156 156 #
157 157 # o A' (bumped)
158 158 # |`:
159 159 # | o A
160 160 # |/
161 161 # o Z
162 162 #
163 163 # The way to solve this situation is to create a new changeset Ad as children
164 164 # of A. This changeset have the same content than A'. So the diff from A to A'
165 165 # is the same than the diff from A to Ad. Ad is marked as a successors of A'
166 166 #
167 167 # o Ad
168 168 # |`:
169 169 # | x A'
170 170 # |'|
171 171 # o | A
172 172 # |/
173 173 # o Z
174 174 #
175 175 # But by transitivity Ad is also a successors of A. To avoid having Ad marked
176 176 # as bumped too, we add the `bumpedfix` flag to the marker. <A', (Ad,)>.
177 177 # This flag mean that the successors express the changes between the public and
178 178 # bumped version and fix the situation, breaking the transitivity of
179 179 # "bumped" here.
180 180 bumpedfix = 1
181 181 usingsha256 = 2
182 182
183 183 ## Parsing and writing of version "0"
184 184 #
185 185 # The header is followed by the markers. Each marker is made of:
186 186 #
187 187 # - 1 uint8 : number of new changesets "N", can be zero.
188 188 #
189 189 # - 1 uint32: metadata size "M" in bytes.
190 190 #
191 191 # - 1 byte: a bit field. It is reserved for flags used in common
192 192 # obsolete marker operations, to avoid repeated decoding of metadata
193 193 # entries.
194 194 #
195 195 # - 20 bytes: obsoleted changeset identifier.
196 196 #
197 197 # - N*20 bytes: new changesets identifiers.
198 198 #
199 199 # - M bytes: metadata as a sequence of nul-terminated strings. Each
200 200 # string contains a key and a value, separated by a colon ':', without
201 201 # additional encoding. Keys cannot contain '\0' or ':' and values
202 202 # cannot contain '\0'.
203 203 _fm0version = 0
204 204 _fm0fixed = '>BIB20s'
205 205 _fm0node = '20s'
206 206 _fm0fsize = _calcsize(_fm0fixed)
207 207 _fm0fnodesize = _calcsize(_fm0node)
208 208
209 209 def _fm0readmarkers(data, off, stop):
210 210 # Loop on markers
211 211 while off < stop:
212 212 # read fixed part
213 213 cur = data[off:off + _fm0fsize]
214 214 off += _fm0fsize
215 215 numsuc, mdsize, flags, pre = _unpack(_fm0fixed, cur)
216 216 # read replacement
217 217 sucs = ()
218 218 if numsuc:
219 219 s = (_fm0fnodesize * numsuc)
220 220 cur = data[off:off + s]
221 221 sucs = _unpack(_fm0node * numsuc, cur)
222 222 off += s
223 223 # read metadata
224 224 # (metadata will be decoded on demand)
225 225 metadata = data[off:off + mdsize]
226 226 if len(metadata) != mdsize:
227 227 raise error.Abort(_('parsing obsolete marker: metadata is too '
228 228 'short, %d bytes expected, got %d')
229 229 % (mdsize, len(metadata)))
230 230 off += mdsize
231 231 metadata = _fm0decodemeta(metadata)
232 232 try:
233 233 when, offset = metadata.pop('date', '0 0').split(' ')
234 234 date = float(when), int(offset)
235 235 except ValueError:
236 236 date = (0., 0)
237 237 parents = None
238 238 if 'p2' in metadata:
239 239 parents = (metadata.pop('p1', None), metadata.pop('p2', None))
240 240 elif 'p1' in metadata:
241 241 parents = (metadata.pop('p1', None),)
242 242 elif 'p0' in metadata:
243 243 parents = ()
244 244 if parents is not None:
245 245 try:
246 246 parents = tuple(node.bin(p) for p in parents)
247 247 # if parent content is not a nodeid, drop the data
248 248 for p in parents:
249 249 if len(p) != 20:
250 250 parents = None
251 251 break
252 252 except TypeError:
253 253 # if content cannot be translated to nodeid drop the data.
254 254 parents = None
255 255
256 256 metadata = tuple(sorted(metadata.iteritems()))
257 257
258 258 yield (pre, sucs, flags, metadata, date, parents)
259 259
260 260 def _fm0encodeonemarker(marker):
261 261 pre, sucs, flags, metadata, date, parents = marker
262 262 if flags & usingsha256:
263 263 raise error.Abort(_('cannot handle sha256 with old obsstore format'))
264 264 metadata = dict(metadata)
265 265 time, tz = date
266 266 metadata['date'] = '%r %i' % (time, tz)
267 267 if parents is not None:
268 268 if not parents:
269 269 # mark that we explicitly recorded no parents
270 270 metadata['p0'] = ''
271 271 for i, p in enumerate(parents, 1):
272 272 metadata['p%i' % i] = node.hex(p)
273 273 metadata = _fm0encodemeta(metadata)
274 274 numsuc = len(sucs)
275 275 format = _fm0fixed + (_fm0node * numsuc)
276 276 data = [numsuc, len(metadata), flags, pre]
277 277 data.extend(sucs)
278 278 return _pack(format, *data) + metadata
279 279
280 280 def _fm0encodemeta(meta):
281 281 """Return encoded metadata string to string mapping.
282 282
283 283 Assume no ':' in key and no '\0' in both key and value."""
284 284 for key, value in meta.iteritems():
285 285 if ':' in key or '\0' in key:
286 286 raise ValueError("':' and '\0' are forbidden in metadata key'")
287 287 if '\0' in value:
288 288 raise ValueError("':' is forbidden in metadata value'")
289 289 return '\0'.join(['%s:%s' % (k, meta[k]) for k in sorted(meta)])
290 290
291 291 def _fm0decodemeta(data):
292 292 """Return string to string dictionary from encoded version."""
293 293 d = {}
294 294 for l in data.split('\0'):
295 295 if l:
296 296 key, value = l.split(':')
297 297 d[key] = value
298 298 return d
299 299
300 300 ## Parsing and writing of version "1"
301 301 #
302 302 # The header is followed by the markers. Each marker is made of:
303 303 #
304 304 # - uint32: total size of the marker (including this field)
305 305 #
306 306 # - float64: date in seconds since epoch
307 307 #
308 308 # - int16: timezone offset in minutes
309 309 #
310 310 # - uint16: a bit field. It is reserved for flags used in common
311 311 # obsolete marker operations, to avoid repeated decoding of metadata
312 312 # entries.
313 313 #
314 314 # - uint8: number of successors "N", can be zero.
315 315 #
316 316 # - uint8: number of parents "P", can be zero.
317 317 #
318 318 # 0: parents data stored but no parent,
319 319 # 1: one parent stored,
320 320 # 2: two parents stored,
321 321 # 3: no parent data stored
322 322 #
323 323 # - uint8: number of metadata entries M
324 324 #
325 325 # - 20 or 32 bytes: predecessor changeset identifier.
326 326 #
327 327 # - N*(20 or 32) bytes: successors changesets identifiers.
328 328 #
329 329 # - P*(20 or 32) bytes: parents of the predecessors changesets.
330 330 #
331 331 # - M*(uint8, uint8): size of all metadata entries (key and value)
332 332 #
333 333 # - remaining bytes: the metadata, each (key, value) pair after the other.
334 334 _fm1version = 1
335 335 _fm1fixed = '>IdhHBBB20s'
336 336 _fm1nodesha1 = '20s'
337 337 _fm1nodesha256 = '32s'
338 338 _fm1nodesha1size = _calcsize(_fm1nodesha1)
339 339 _fm1nodesha256size = _calcsize(_fm1nodesha256)
340 340 _fm1fsize = _calcsize(_fm1fixed)
341 341 _fm1parentnone = 3
342 342 _fm1parentshift = 14
343 343 _fm1parentmask = (_fm1parentnone << _fm1parentshift)
344 344 _fm1metapair = 'BB'
345 345 _fm1metapairsize = _calcsize(_fm1metapair)
346 346
347 347 def _fm1purereadmarkers(data, off, stop):
348 348 # make some global constants local for performance
349 349 noneflag = _fm1parentnone
350 350 sha2flag = usingsha256
351 351 sha1size = _fm1nodesha1size
352 352 sha2size = _fm1nodesha256size
353 353 sha1fmt = _fm1nodesha1
354 354 sha2fmt = _fm1nodesha256
355 355 metasize = _fm1metapairsize
356 356 metafmt = _fm1metapair
357 357 fsize = _fm1fsize
358 358 unpack = _unpack
359 359
360 360 # Loop on markers
361 361 ufixed = struct.Struct(_fm1fixed).unpack
362 362
363 363 while off < stop:
364 364 # read fixed part
365 365 o1 = off + fsize
366 366 t, secs, tz, flags, numsuc, numpar, nummeta, prec = ufixed(data[off:o1])
367 367
368 368 if flags & sha2flag:
369 369 # FIXME: prec was read as a SHA1, needs to be amended
370 370
371 371 # read 0 or more successors
372 372 if numsuc == 1:
373 373 o2 = o1 + sha2size
374 374 sucs = (data[o1:o2],)
375 375 else:
376 376 o2 = o1 + sha2size * numsuc
377 377 sucs = unpack(sha2fmt * numsuc, data[o1:o2])
378 378
379 379 # read parents
380 380 if numpar == noneflag:
381 381 o3 = o2
382 382 parents = None
383 383 elif numpar == 1:
384 384 o3 = o2 + sha2size
385 385 parents = (data[o2:o3],)
386 386 else:
387 387 o3 = o2 + sha2size * numpar
388 388 parents = unpack(sha2fmt * numpar, data[o2:o3])
389 389 else:
390 390 # read 0 or more successors
391 391 if numsuc == 1:
392 392 o2 = o1 + sha1size
393 393 sucs = (data[o1:o2],)
394 394 else:
395 395 o2 = o1 + sha1size * numsuc
396 396 sucs = unpack(sha1fmt * numsuc, data[o1:o2])
397 397
398 398 # read parents
399 399 if numpar == noneflag:
400 400 o3 = o2
401 401 parents = None
402 402 elif numpar == 1:
403 403 o3 = o2 + sha1size
404 404 parents = (data[o2:o3],)
405 405 else:
406 406 o3 = o2 + sha1size * numpar
407 407 parents = unpack(sha1fmt * numpar, data[o2:o3])
408 408
409 409 # read metadata
410 410 off = o3 + metasize * nummeta
411 411 metapairsize = unpack('>' + (metafmt * nummeta), data[o3:off])
412 412 metadata = []
413 413 for idx in xrange(0, len(metapairsize), 2):
414 414 o1 = off + metapairsize[idx]
415 415 o2 = o1 + metapairsize[idx + 1]
416 416 metadata.append((data[off:o1], data[o1:o2]))
417 417 off = o2
418 418
419 419 yield (prec, sucs, flags, tuple(metadata), (secs, tz * 60), parents)
420 420
421 421 def _fm1encodeonemarker(marker):
422 422 pre, sucs, flags, metadata, date, parents = marker
423 423 # determine node size
424 424 _fm1node = _fm1nodesha1
425 425 if flags & usingsha256:
426 426 _fm1node = _fm1nodesha256
427 427 numsuc = len(sucs)
428 428 numextranodes = numsuc
429 429 if parents is None:
430 430 numpar = _fm1parentnone
431 431 else:
432 432 numpar = len(parents)
433 433 numextranodes += numpar
434 434 formatnodes = _fm1node * numextranodes
435 435 formatmeta = _fm1metapair * len(metadata)
436 436 format = _fm1fixed + formatnodes + formatmeta
437 437 # tz is stored in minutes so we divide by 60
438 438 tz = date[1]//60
439 439 data = [None, date[0], tz, flags, numsuc, numpar, len(metadata), pre]
440 440 data.extend(sucs)
441 441 if parents is not None:
442 442 data.extend(parents)
443 443 totalsize = _calcsize(format)
444 444 for key, value in metadata:
445 445 lk = len(key)
446 446 lv = len(value)
447 447 if lk > 255:
448 448 msg = ('obsstore metadata key cannot be longer than 255 bytes'
449 449 ' (key "%s" is %u bytes)') % (key, lk)
450 450 raise error.ProgrammingError(msg)
451 451 if lv > 255:
452 452 msg = ('obsstore metadata value cannot be longer than 255 bytes'
453 453 ' (value "%s" for key "%s" is %u bytes)') % (value, key, lv)
454 454 raise error.ProgrammingError(msg)
455 455 data.append(lk)
456 456 data.append(lv)
457 457 totalsize += lk + lv
458 458 data[0] = totalsize
459 459 data = [_pack(format, *data)]
460 460 for key, value in metadata:
461 461 data.append(key)
462 462 data.append(value)
463 463 return ''.join(data)
464 464
465 465 def _fm1readmarkers(data, off, stop):
466 466 native = getattr(parsers, 'fm1readmarkers', None)
467 467 if not native:
468 468 return _fm1purereadmarkers(data, off, stop)
469 469 return native(data, off, stop)
470 470
471 471 # mapping to read/write various marker formats
472 472 # <version> -> (decoder, encoder)
473 473 formats = {_fm0version: (_fm0readmarkers, _fm0encodeonemarker),
474 474 _fm1version: (_fm1readmarkers, _fm1encodeonemarker)}
475 475
476 476 def _readmarkerversion(data):
477 477 return _unpack('>B', data[0:1])[0]
478 478
479 479 @util.nogc
480 480 def _readmarkers(data, off=None, stop=None):
481 481 """Read and enumerate markers from raw data"""
482 482 diskversion = _readmarkerversion(data)
483 483 if not off:
484 484 off = 1 # skip 1 byte version number
485 485 if stop is None:
486 486 stop = len(data)
487 487 if diskversion not in formats:
488 488 msg = _('parsing obsolete marker: unknown version %r') % diskversion
489 489 raise error.UnknownVersion(msg, version=diskversion)
490 490 return diskversion, formats[diskversion][0](data, off, stop)
491 491
492 492 def encodeheader(version=_fm0version):
493 493 return _pack('>B', version)
494 494
495 495 def encodemarkers(markers, addheader=False, version=_fm0version):
496 496 # Kept separate from flushmarkers(), it will be reused for
497 497 # markers exchange.
498 498 encodeone = formats[version][1]
499 499 if addheader:
500 500 yield encodeheader(version)
501 501 for marker in markers:
502 502 yield encodeone(marker)
503 503
504 504 @util.nogc
505 505 def _addsuccessors(successors, markers):
506 506 for mark in markers:
507 507 successors.setdefault(mark[0], set()).add(mark)
508 508
509 509 @util.nogc
510 510 def _addpredecessors(predecessors, markers):
511 511 for mark in markers:
512 512 for suc in mark[1]:
513 513 predecessors.setdefault(suc, set()).add(mark)
514 514
515 515 @util.nogc
516 516 def _addchildren(children, markers):
517 517 for mark in markers:
518 518 parents = mark[5]
519 519 if parents is not None:
520 520 for p in parents:
521 521 children.setdefault(p, set()).add(mark)
522 522
523 523 def _checkinvalidmarkers(markers):
524 524 """search for marker with invalid data and raise error if needed
525 525
526 526 Exist as a separated function to allow the evolve extension for a more
527 527 subtle handling.
528 528 """
529 529 for mark in markers:
530 530 if node.nullid in mark[1]:
531 531 raise error.Abort(_('bad obsolescence marker detected: '
532 532 'invalid successors nullid'))
533 533
534 534 class obsstore(object):
535 535 """Store obsolete markers
536 536
537 537 Markers can be accessed with two mappings:
538 538 - predecessors[x] -> set(markers on predecessors edges of x)
539 539 - successors[x] -> set(markers on successors edges of x)
540 540 - children[x] -> set(markers on predecessors edges of children(x)
541 541 """
542 542
543 543 fields = ('prec', 'succs', 'flag', 'meta', 'date', 'parents')
544 544 # prec: nodeid, predecessors changesets
545 545 # succs: tuple of nodeid, successor changesets (0-N length)
546 546 # flag: integer, flag field carrying modifier for the markers (see doc)
547 547 # meta: binary blob, encoded metadata dictionary
548 548 # date: (float, int) tuple, date of marker creation
549 549 # parents: (tuple of nodeid) or None, parents of predecessors
550 550 # None is used when no data has been recorded
551 551
552 552 def __init__(self, svfs, defaultformat=_fm1version, readonly=False):
553 553 # caches for various obsolescence related cache
554 554 self.caches = {}
555 555 self.svfs = svfs
556 556 self._defaultformat = defaultformat
557 557 self._readonly = readonly
558 558
559 559 def __iter__(self):
560 560 return iter(self._all)
561 561
562 562 def __len__(self):
563 563 return len(self._all)
564 564
565 565 def __nonzero__(self):
566 566 if not self._cached(r'_all'):
567 567 try:
568 568 return self.svfs.stat('obsstore').st_size > 1
569 569 except OSError as inst:
570 570 if inst.errno != errno.ENOENT:
571 571 raise
572 572 # just build an empty _all list if no obsstore exists, which
573 573 # avoids further stat() syscalls
574 574 return bool(self._all)
575 575
576 576 __bool__ = __nonzero__
577 577
578 578 @property
579 579 def readonly(self):
580 580 """True if marker creation is disabled
581 581
582 582 Remove me in the future when obsolete marker is always on."""
583 583 return self._readonly
584 584
585 585 def create(self, transaction, prec, succs=(), flag=0, parents=None,
586 586 date=None, metadata=None, ui=None):
587 587 """obsolete: add a new obsolete marker
588 588
589 589 * ensuring it is hashable
590 590 * check mandatory metadata
591 591 * encode metadata
592 592
593 593 If you are a human writing code creating marker you want to use the
594 594 `createmarkers` function in this module instead.
595 595
596 596 return True if a new marker have been added, False if the markers
597 597 already existed (no op).
598 598 """
599 599 if metadata is None:
600 600 metadata = {}
601 601 if date is None:
602 602 if 'date' in metadata:
603 603 # as a courtesy for out-of-tree extensions
604 604 date = util.parsedate(metadata.pop('date'))
605 605 elif ui is not None:
606 606 date = ui.configdate('devel', 'default-date')
607 607 if date is None:
608 608 date = util.makedate()
609 609 else:
610 610 date = util.makedate()
611 611 if len(prec) != 20:
612 612 raise ValueError(prec)
613 613 for succ in succs:
614 614 if len(succ) != 20:
615 615 raise ValueError(succ)
616 616 if prec in succs:
617 617 raise ValueError(_('in-marker cycle with %s') % node.hex(prec))
618 618
619 619 metadata = tuple(sorted(metadata.iteritems()))
620 620
621 621 marker = (bytes(prec), tuple(succs), int(flag), metadata, date, parents)
622 622 return bool(self.add(transaction, [marker]))
623 623
624 624 def add(self, transaction, markers):
625 625 """Add new markers to the store
626 626
627 627 Take care of filtering duplicate.
628 628 Return the number of new marker."""
629 629 if self._readonly:
630 630 raise error.Abort(_('creating obsolete markers is not enabled on '
631 631 'this repo'))
632 632 known = set()
633 633 getsuccessors = self.successors.get
634 634 new = []
635 635 for m in markers:
636 636 if m not in getsuccessors(m[0], ()) and m not in known:
637 637 known.add(m)
638 638 new.append(m)
639 639 if new:
640 640 f = self.svfs('obsstore', 'ab')
641 641 try:
642 642 offset = f.tell()
643 643 transaction.add('obsstore', offset)
644 644 # offset == 0: new file - add the version header
645 645 data = b''.join(encodemarkers(new, offset == 0, self._version))
646 646 f.write(data)
647 647 finally:
648 648 # XXX: f.close() == filecache invalidation == obsstore rebuilt.
649 649 # call 'filecacheentry.refresh()' here
650 650 f.close()
651 651 addedmarkers = transaction.changes.get('obsmarkers')
652 652 if addedmarkers is not None:
653 653 addedmarkers.update(new)
654 654 self._addmarkers(new, data)
655 655 # new marker *may* have changed several set. invalidate the cache.
656 656 self.caches.clear()
657 657 # records the number of new markers for the transaction hooks
658 658 previous = int(transaction.hookargs.get('new_obsmarkers', '0'))
659 transaction.hookargs['new_obsmarkers'] = str(previous + len(new))
659 transaction.hookargs['new_obsmarkers'] = '%d' % (previous + len(new))
660 660 return len(new)
661 661
662 662 def mergemarkers(self, transaction, data):
663 663 """merge a binary stream of markers inside the obsstore
664 664
665 665 Returns the number of new markers added."""
666 666 version, markers = _readmarkers(data)
667 667 return self.add(transaction, markers)
668 668
669 669 @propertycache
670 670 def _data(self):
671 671 return self.svfs.tryread('obsstore')
672 672
673 673 @propertycache
674 674 def _version(self):
675 675 if len(self._data) >= 1:
676 676 return _readmarkerversion(self._data)
677 677 else:
678 678 return self._defaultformat
679 679
680 680 @propertycache
681 681 def _all(self):
682 682 data = self._data
683 683 if not data:
684 684 return []
685 685 self._version, markers = _readmarkers(data)
686 686 markers = list(markers)
687 687 _checkinvalidmarkers(markers)
688 688 return markers
689 689
690 690 @propertycache
691 691 def successors(self):
692 692 successors = {}
693 693 _addsuccessors(successors, self._all)
694 694 return successors
695 695
696 696 @propertycache
697 697 def predecessors(self):
698 698 predecessors = {}
699 699 _addpredecessors(predecessors, self._all)
700 700 return predecessors
701 701
702 702 @propertycache
703 703 def children(self):
704 704 children = {}
705 705 _addchildren(children, self._all)
706 706 return children
707 707
708 708 def _cached(self, attr):
709 709 return attr in self.__dict__
710 710
711 711 def _addmarkers(self, markers, rawdata):
712 712 markers = list(markers) # to allow repeated iteration
713 713 self._data = self._data + rawdata
714 714 self._all.extend(markers)
715 715 if self._cached(r'successors'):
716 716 _addsuccessors(self.successors, markers)
717 717 if self._cached(r'predecessors'):
718 718 _addpredecessors(self.predecessors, markers)
719 719 if self._cached(r'children'):
720 720 _addchildren(self.children, markers)
721 721 _checkinvalidmarkers(markers)
722 722
723 723 def relevantmarkers(self, nodes):
724 724 """return a set of all obsolescence markers relevant to a set of nodes.
725 725
726 726 "relevant" to a set of nodes mean:
727 727
728 728 - marker that use this changeset as successor
729 729 - prune marker of direct children on this changeset
730 730 - recursive application of the two rules on predecessors of these
731 731 markers
732 732
733 733 It is a set so you cannot rely on order."""
734 734
735 735 pendingnodes = set(nodes)
736 736 seenmarkers = set()
737 737 seennodes = set(pendingnodes)
738 738 precursorsmarkers = self.predecessors
739 739 succsmarkers = self.successors
740 740 children = self.children
741 741 while pendingnodes:
742 742 direct = set()
743 743 for current in pendingnodes:
744 744 direct.update(precursorsmarkers.get(current, ()))
745 745 pruned = [m for m in children.get(current, ()) if not m[1]]
746 746 direct.update(pruned)
747 747 pruned = [m for m in succsmarkers.get(current, ()) if not m[1]]
748 748 direct.update(pruned)
749 749 direct -= seenmarkers
750 750 pendingnodes = set([m[0] for m in direct])
751 751 seenmarkers |= direct
752 752 pendingnodes -= seennodes
753 753 seennodes |= pendingnodes
754 754 return seenmarkers
755 755
756 756 def makestore(ui, repo):
757 757 """Create an obsstore instance from a repo."""
758 758 # read default format for new obsstore.
759 759 # developer config: format.obsstore-version
760 760 defaultformat = ui.configint('format', 'obsstore-version')
761 761 # rely on obsstore class default when possible.
762 762 kwargs = {}
763 763 if defaultformat is not None:
764 764 kwargs[r'defaultformat'] = defaultformat
765 765 readonly = not isenabled(repo, createmarkersopt)
766 766 store = obsstore(repo.svfs, readonly=readonly, **kwargs)
767 767 if store and readonly:
768 768 ui.warn(_('obsolete feature not enabled but %i markers found!\n')
769 769 % len(list(store)))
770 770 return store
771 771
772 772 def commonversion(versions):
773 773 """Return the newest version listed in both versions and our local formats.
774 774
775 775 Returns None if no common version exists.
776 776 """
777 777 versions.sort(reverse=True)
778 778 # search for highest version known on both side
779 779 for v in versions:
780 780 if v in formats:
781 781 return v
782 782 return None
783 783
784 784 # arbitrary picked to fit into 8K limit from HTTP server
785 785 # you have to take in account:
786 786 # - the version header
787 787 # - the base85 encoding
788 788 _maxpayload = 5300
789 789
790 790 def _pushkeyescape(markers):
791 791 """encode markers into a dict suitable for pushkey exchange
792 792
793 793 - binary data is base85 encoded
794 794 - split in chunks smaller than 5300 bytes"""
795 795 keys = {}
796 796 parts = []
797 797 currentlen = _maxpayload * 2 # ensure we create a new part
798 798 for marker in markers:
799 799 nextdata = _fm0encodeonemarker(marker)
800 800 if (len(nextdata) + currentlen > _maxpayload):
801 801 currentpart = []
802 802 currentlen = 0
803 803 parts.append(currentpart)
804 804 currentpart.append(nextdata)
805 805 currentlen += len(nextdata)
806 806 for idx, part in enumerate(reversed(parts)):
807 807 data = ''.join([_pack('>B', _fm0version)] + part)
808 808 keys['dump%i' % idx] = util.b85encode(data)
809 809 return keys
810 810
811 811 def listmarkers(repo):
812 812 """List markers over pushkey"""
813 813 if not repo.obsstore:
814 814 return {}
815 815 return _pushkeyescape(sorted(repo.obsstore))
816 816
817 817 def pushmarker(repo, key, old, new):
818 818 """Push markers over pushkey"""
819 819 if not key.startswith('dump'):
820 820 repo.ui.warn(_('unknown key: %r') % key)
821 821 return False
822 822 if old:
823 823 repo.ui.warn(_('unexpected old value for %r') % key)
824 824 return False
825 825 data = util.b85decode(new)
826 826 with repo.lock(), repo.transaction('pushkey: obsolete markers') as tr:
827 827 repo.obsstore.mergemarkers(tr, data)
828 828 repo.invalidatevolatilesets()
829 829 return True
830 830
831 831 # mapping of 'set-name' -> <function to compute this set>
832 832 cachefuncs = {}
833 833 def cachefor(name):
834 834 """Decorator to register a function as computing the cache for a set"""
835 835 def decorator(func):
836 836 if name in cachefuncs:
837 837 msg = "duplicated registration for volatileset '%s' (existing: %r)"
838 838 raise error.ProgrammingError(msg % (name, cachefuncs[name]))
839 839 cachefuncs[name] = func
840 840 return func
841 841 return decorator
842 842
843 843 def getrevs(repo, name):
844 844 """Return the set of revision that belong to the <name> set
845 845
846 846 Such access may compute the set and cache it for future use"""
847 847 repo = repo.unfiltered()
848 848 if not repo.obsstore:
849 849 return frozenset()
850 850 if name not in repo.obsstore.caches:
851 851 repo.obsstore.caches[name] = cachefuncs[name](repo)
852 852 return repo.obsstore.caches[name]
853 853
854 854 # To be simple we need to invalidate obsolescence cache when:
855 855 #
856 856 # - new changeset is added:
857 857 # - public phase is changed
858 858 # - obsolescence marker are added
859 859 # - strip is used a repo
860 860 def clearobscaches(repo):
861 861 """Remove all obsolescence related cache from a repo
862 862
863 863 This remove all cache in obsstore is the obsstore already exist on the
864 864 repo.
865 865
866 866 (We could be smarter here given the exact event that trigger the cache
867 867 clearing)"""
868 868 # only clear cache is there is obsstore data in this repo
869 869 if 'obsstore' in repo._filecache:
870 870 repo.obsstore.caches.clear()
871 871
872 872 def _mutablerevs(repo):
873 873 """the set of mutable revision in the repository"""
874 874 return repo._phasecache.getrevset(repo, (phases.draft, phases.secret))
875 875
876 876 @cachefor('obsolete')
877 877 def _computeobsoleteset(repo):
878 878 """the set of obsolete revisions"""
879 879 getnode = repo.changelog.node
880 880 notpublic = _mutablerevs(repo)
881 881 isobs = repo.obsstore.successors.__contains__
882 882 obs = set(r for r in notpublic if isobs(getnode(r)))
883 883 return obs
884 884
885 885 @cachefor('orphan')
886 886 def _computeorphanset(repo):
887 887 """the set of non obsolete revisions with obsolete parents"""
888 888 pfunc = repo.changelog.parentrevs
889 889 mutable = _mutablerevs(repo)
890 890 obsolete = getrevs(repo, 'obsolete')
891 891 others = mutable - obsolete
892 892 unstable = set()
893 893 for r in sorted(others):
894 894 # A rev is unstable if one of its parent is obsolete or unstable
895 895 # this works since we traverse following growing rev order
896 896 for p in pfunc(r):
897 897 if p in obsolete or p in unstable:
898 898 unstable.add(r)
899 899 break
900 900 return unstable
901 901
902 902 @cachefor('suspended')
903 903 def _computesuspendedset(repo):
904 904 """the set of obsolete parents with non obsolete descendants"""
905 905 suspended = repo.changelog.ancestors(getrevs(repo, 'orphan'))
906 906 return set(r for r in getrevs(repo, 'obsolete') if r in suspended)
907 907
908 908 @cachefor('extinct')
909 909 def _computeextinctset(repo):
910 910 """the set of obsolete parents without non obsolete descendants"""
911 911 return getrevs(repo, 'obsolete') - getrevs(repo, 'suspended')
912 912
913 913 @cachefor('phasedivergent')
914 914 def _computephasedivergentset(repo):
915 915 """the set of revs trying to obsolete public revisions"""
916 916 bumped = set()
917 917 # util function (avoid attribute lookup in the loop)
918 918 phase = repo._phasecache.phase # would be faster to grab the full list
919 919 public = phases.public
920 920 cl = repo.changelog
921 921 torev = cl.nodemap.get
922 922 tonode = cl.node
923 923 for rev in repo.revs('(not public()) and (not obsolete())'):
924 924 # We only evaluate mutable, non-obsolete revision
925 925 node = tonode(rev)
926 926 # (future) A cache of predecessors may worth if split is very common
927 927 for pnode in obsutil.allpredecessors(repo.obsstore, [node],
928 928 ignoreflags=bumpedfix):
929 929 prev = torev(pnode) # unfiltered! but so is phasecache
930 930 if (prev is not None) and (phase(repo, prev) <= public):
931 931 # we have a public predecessor
932 932 bumped.add(rev)
933 933 break # Next draft!
934 934 return bumped
935 935
936 936 @cachefor('contentdivergent')
937 937 def _computecontentdivergentset(repo):
938 938 """the set of rev that compete to be the final successors of some revision.
939 939 """
940 940 divergent = set()
941 941 obsstore = repo.obsstore
942 942 newermap = {}
943 943 tonode = repo.changelog.node
944 944 for rev in repo.revs('(not public()) - obsolete()'):
945 945 node = tonode(rev)
946 946 mark = obsstore.predecessors.get(node, ())
947 947 toprocess = set(mark)
948 948 seen = set()
949 949 while toprocess:
950 950 prec = toprocess.pop()[0]
951 951 if prec in seen:
952 952 continue # emergency cycle hanging prevention
953 953 seen.add(prec)
954 954 if prec not in newermap:
955 955 obsutil.successorssets(repo, prec, cache=newermap)
956 956 newer = [n for n in newermap[prec] if n]
957 957 if len(newer) > 1:
958 958 divergent.add(rev)
959 959 break
960 960 toprocess.update(obsstore.predecessors.get(prec, ()))
961 961 return divergent
962 962
963 963
964 964 def createmarkers(repo, relations, flag=0, date=None, metadata=None,
965 965 operation=None):
966 966 """Add obsolete markers between changesets in a repo
967 967
968 968 <relations> must be an iterable of (<old>, (<new>, ...)[,{metadata}])
969 969 tuple. `old` and `news` are changectx. metadata is an optional dictionary
970 970 containing metadata for this marker only. It is merged with the global
971 971 metadata specified through the `metadata` argument of this function,
972 972
973 973 Trying to obsolete a public changeset will raise an exception.
974 974
975 975 Current user and date are used except if specified otherwise in the
976 976 metadata attribute.
977 977
978 978 This function operates within a transaction of its own, but does
979 979 not take any lock on the repo.
980 980 """
981 981 # prepare metadata
982 982 if metadata is None:
983 983 metadata = {}
984 984 if 'user' not in metadata:
985 985 develuser = repo.ui.config('devel', 'user.obsmarker')
986 986 if develuser:
987 987 metadata['user'] = develuser
988 988 else:
989 989 metadata['user'] = repo.ui.username()
990 990
991 991 # Operation metadata handling
992 992 useoperation = repo.ui.configbool('experimental',
993 993 'evolution.track-operation')
994 994 if useoperation and operation:
995 995 metadata['operation'] = operation
996 996
997 997 # Effect flag metadata handling
998 998 saveeffectflag = repo.ui.configbool('experimental',
999 999 'evolution.effect-flags')
1000 1000
1001 1001 with repo.transaction('add-obsolescence-marker') as tr:
1002 1002 markerargs = []
1003 1003 for rel in relations:
1004 1004 prec = rel[0]
1005 1005 sucs = rel[1]
1006 1006 localmetadata = metadata.copy()
1007 1007 if 2 < len(rel):
1008 1008 localmetadata.update(rel[2])
1009 1009
1010 1010 if not prec.mutable():
1011 1011 raise error.Abort(_("cannot obsolete public changeset: %s")
1012 1012 % prec,
1013 1013 hint="see 'hg help phases' for details")
1014 1014 nprec = prec.node()
1015 1015 nsucs = tuple(s.node() for s in sucs)
1016 1016 npare = None
1017 1017 if not nsucs:
1018 1018 npare = tuple(p.node() for p in prec.parents())
1019 1019 if nprec in nsucs:
1020 1020 raise error.Abort(_("changeset %s cannot obsolete itself")
1021 1021 % prec)
1022 1022
1023 1023 # Effect flag can be different by relation
1024 1024 if saveeffectflag:
1025 1025 # The effect flag is saved in a versioned field name for future
1026 1026 # evolution
1027 1027 effectflag = obsutil.geteffectflag(rel)
1028 1028 localmetadata[obsutil.EFFECTFLAGFIELD] = "%d" % effectflag
1029 1029
1030 1030 # Creating the marker causes the hidden cache to become invalid,
1031 1031 # which causes recomputation when we ask for prec.parents() above.
1032 1032 # Resulting in n^2 behavior. So let's prepare all of the args
1033 1033 # first, then create the markers.
1034 1034 markerargs.append((nprec, nsucs, npare, localmetadata))
1035 1035
1036 1036 for args in markerargs:
1037 1037 nprec, nsucs, npare, localmetadata = args
1038 1038 repo.obsstore.create(tr, nprec, nsucs, flag, parents=npare,
1039 1039 date=date, metadata=localmetadata,
1040 1040 ui=repo.ui)
1041 1041 repo.filteredrevcache.clear()
@@ -1,2903 +1,2903 b''
1 1 # patch.py - patch file parsing routines
2 2 #
3 3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
4 4 # Copyright 2007 Chris Mason <chris.mason@oracle.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from __future__ import absolute_import, print_function
10 10
11 11 import collections
12 12 import copy
13 13 import difflib
14 14 import email
15 15 import errno
16 16 import hashlib
17 17 import os
18 18 import posixpath
19 19 import re
20 20 import shutil
21 21 import tempfile
22 22 import zlib
23 23
24 24 from .i18n import _
25 25 from .node import (
26 26 hex,
27 27 short,
28 28 )
29 29 from . import (
30 30 copies,
31 31 encoding,
32 32 error,
33 33 mail,
34 34 mdiff,
35 35 pathutil,
36 36 policy,
37 37 pycompat,
38 38 scmutil,
39 39 similar,
40 40 util,
41 41 vfs as vfsmod,
42 42 )
43 43
44 44 diffhelpers = policy.importmod(r'diffhelpers')
45 45 stringio = util.stringio
46 46
47 47 gitre = re.compile(br'diff --git a/(.*) b/(.*)')
48 48 tabsplitter = re.compile(br'(\t+|[^\t]+)')
49 49 _nonwordre = re.compile(br'([^a-zA-Z0-9_\x80-\xff])')
50 50
51 51 PatchError = error.PatchError
52 52
53 53 # public functions
54 54
55 55 def split(stream):
56 56 '''return an iterator of individual patches from a stream'''
57 57 def isheader(line, inheader):
58 58 if inheader and line[0] in (' ', '\t'):
59 59 # continuation
60 60 return True
61 61 if line[0] in (' ', '-', '+'):
62 62 # diff line - don't check for header pattern in there
63 63 return False
64 64 l = line.split(': ', 1)
65 65 return len(l) == 2 and ' ' not in l[0]
66 66
67 67 def chunk(lines):
68 68 return stringio(''.join(lines))
69 69
70 70 def hgsplit(stream, cur):
71 71 inheader = True
72 72
73 73 for line in stream:
74 74 if not line.strip():
75 75 inheader = False
76 76 if not inheader and line.startswith('# HG changeset patch'):
77 77 yield chunk(cur)
78 78 cur = []
79 79 inheader = True
80 80
81 81 cur.append(line)
82 82
83 83 if cur:
84 84 yield chunk(cur)
85 85
86 86 def mboxsplit(stream, cur):
87 87 for line in stream:
88 88 if line.startswith('From '):
89 89 for c in split(chunk(cur[1:])):
90 90 yield c
91 91 cur = []
92 92
93 93 cur.append(line)
94 94
95 95 if cur:
96 96 for c in split(chunk(cur[1:])):
97 97 yield c
98 98
99 99 def mimesplit(stream, cur):
100 100 def msgfp(m):
101 101 fp = stringio()
102 102 g = email.Generator.Generator(fp, mangle_from_=False)
103 103 g.flatten(m)
104 104 fp.seek(0)
105 105 return fp
106 106
107 107 for line in stream:
108 108 cur.append(line)
109 109 c = chunk(cur)
110 110
111 111 m = pycompat.emailparser().parse(c)
112 112 if not m.is_multipart():
113 113 yield msgfp(m)
114 114 else:
115 115 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
116 116 for part in m.walk():
117 117 ct = part.get_content_type()
118 118 if ct not in ok_types:
119 119 continue
120 120 yield msgfp(part)
121 121
122 122 def headersplit(stream, cur):
123 123 inheader = False
124 124
125 125 for line in stream:
126 126 if not inheader and isheader(line, inheader):
127 127 yield chunk(cur)
128 128 cur = []
129 129 inheader = True
130 130 if inheader and not isheader(line, inheader):
131 131 inheader = False
132 132
133 133 cur.append(line)
134 134
135 135 if cur:
136 136 yield chunk(cur)
137 137
138 138 def remainder(cur):
139 139 yield chunk(cur)
140 140
141 141 class fiter(object):
142 142 def __init__(self, fp):
143 143 self.fp = fp
144 144
145 145 def __iter__(self):
146 146 return self
147 147
148 148 def next(self):
149 149 l = self.fp.readline()
150 150 if not l:
151 151 raise StopIteration
152 152 return l
153 153
154 154 __next__ = next
155 155
156 156 inheader = False
157 157 cur = []
158 158
159 159 mimeheaders = ['content-type']
160 160
161 161 if not util.safehasattr(stream, 'next'):
162 162 # http responses, for example, have readline but not next
163 163 stream = fiter(stream)
164 164
165 165 for line in stream:
166 166 cur.append(line)
167 167 if line.startswith('# HG changeset patch'):
168 168 return hgsplit(stream, cur)
169 169 elif line.startswith('From '):
170 170 return mboxsplit(stream, cur)
171 171 elif isheader(line, inheader):
172 172 inheader = True
173 173 if line.split(':', 1)[0].lower() in mimeheaders:
174 174 # let email parser handle this
175 175 return mimesplit(stream, cur)
176 176 elif line.startswith('--- ') and inheader:
177 177 # No evil headers seen by diff start, split by hand
178 178 return headersplit(stream, cur)
179 179 # Not enough info, keep reading
180 180
181 181 # if we are here, we have a very plain patch
182 182 return remainder(cur)
183 183
184 184 ## Some facility for extensible patch parsing:
185 185 # list of pairs ("header to match", "data key")
186 186 patchheadermap = [('Date', 'date'),
187 187 ('Branch', 'branch'),
188 188 ('Node ID', 'nodeid'),
189 189 ]
190 190
191 191 def extract(ui, fileobj):
192 192 '''extract patch from data read from fileobj.
193 193
194 194 patch can be a normal patch or contained in an email message.
195 195
196 196 return a dictionary. Standard keys are:
197 197 - filename,
198 198 - message,
199 199 - user,
200 200 - date,
201 201 - branch,
202 202 - node,
203 203 - p1,
204 204 - p2.
205 205 Any item can be missing from the dictionary. If filename is missing,
206 206 fileobj did not contain a patch. Caller must unlink filename when done.'''
207 207
208 208 # attempt to detect the start of a patch
209 209 # (this heuristic is borrowed from quilt)
210 210 diffre = re.compile(br'^(?:Index:[ \t]|diff[ \t]-|RCS file: |'
211 211 br'retrieving revision [0-9]+(\.[0-9]+)*$|'
212 212 br'---[ \t].*?^\+\+\+[ \t]|'
213 213 br'\*\*\*[ \t].*?^---[ \t])',
214 214 re.MULTILINE | re.DOTALL)
215 215
216 216 data = {}
217 217 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
218 218 tmpfp = os.fdopen(fd, pycompat.sysstr('wb'))
219 219 try:
220 220 msg = pycompat.emailparser().parse(fileobj)
221 221
222 222 subject = msg['Subject'] and mail.headdecode(msg['Subject'])
223 223 data['user'] = msg['From'] and mail.headdecode(msg['From'])
224 224 if not subject and not data['user']:
225 225 # Not an email, restore parsed headers if any
226 226 subject = '\n'.join(': '.join(h) for h in msg.items()) + '\n'
227 227
228 228 # should try to parse msg['Date']
229 229 parents = []
230 230
231 231 if subject:
232 232 if subject.startswith('[PATCH'):
233 233 pend = subject.find(']')
234 234 if pend >= 0:
235 235 subject = subject[pend + 1:].lstrip()
236 236 subject = re.sub(br'\n[ \t]+', ' ', subject)
237 237 ui.debug('Subject: %s\n' % subject)
238 238 if data['user']:
239 239 ui.debug('From: %s\n' % data['user'])
240 240 diffs_seen = 0
241 241 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
242 242 message = ''
243 243 for part in msg.walk():
244 244 content_type = pycompat.bytestr(part.get_content_type())
245 245 ui.debug('Content-Type: %s\n' % content_type)
246 246 if content_type not in ok_types:
247 247 continue
248 248 payload = part.get_payload(decode=True)
249 249 m = diffre.search(payload)
250 250 if m:
251 251 hgpatch = False
252 252 hgpatchheader = False
253 253 ignoretext = False
254 254
255 255 ui.debug('found patch at byte %d\n' % m.start(0))
256 256 diffs_seen += 1
257 257 cfp = stringio()
258 258 for line in payload[:m.start(0)].splitlines():
259 259 if line.startswith('# HG changeset patch') and not hgpatch:
260 260 ui.debug('patch generated by hg export\n')
261 261 hgpatch = True
262 262 hgpatchheader = True
263 263 # drop earlier commit message content
264 264 cfp.seek(0)
265 265 cfp.truncate()
266 266 subject = None
267 267 elif hgpatchheader:
268 268 if line.startswith('# User '):
269 269 data['user'] = line[7:]
270 270 ui.debug('From: %s\n' % data['user'])
271 271 elif line.startswith("# Parent "):
272 272 parents.append(line[9:].lstrip())
273 273 elif line.startswith("# "):
274 274 for header, key in patchheadermap:
275 275 prefix = '# %s ' % header
276 276 if line.startswith(prefix):
277 277 data[key] = line[len(prefix):]
278 278 else:
279 279 hgpatchheader = False
280 280 elif line == '---':
281 281 ignoretext = True
282 282 if not hgpatchheader and not ignoretext:
283 283 cfp.write(line)
284 284 cfp.write('\n')
285 285 message = cfp.getvalue()
286 286 if tmpfp:
287 287 tmpfp.write(payload)
288 288 if not payload.endswith('\n'):
289 289 tmpfp.write('\n')
290 290 elif not diffs_seen and message and content_type == 'text/plain':
291 291 message += '\n' + payload
292 292 except: # re-raises
293 293 tmpfp.close()
294 294 os.unlink(tmpname)
295 295 raise
296 296
297 297 if subject and not message.startswith(subject):
298 298 message = '%s\n%s' % (subject, message)
299 299 data['message'] = message
300 300 tmpfp.close()
301 301 if parents:
302 302 data['p1'] = parents.pop(0)
303 303 if parents:
304 304 data['p2'] = parents.pop(0)
305 305
306 306 if diffs_seen:
307 307 data['filename'] = tmpname
308 308 else:
309 309 os.unlink(tmpname)
310 310 return data
311 311
312 312 class patchmeta(object):
313 313 """Patched file metadata
314 314
315 315 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
316 316 or COPY. 'path' is patched file path. 'oldpath' is set to the
317 317 origin file when 'op' is either COPY or RENAME, None otherwise. If
318 318 file mode is changed, 'mode' is a tuple (islink, isexec) where
319 319 'islink' is True if the file is a symlink and 'isexec' is True if
320 320 the file is executable. Otherwise, 'mode' is None.
321 321 """
322 322 def __init__(self, path):
323 323 self.path = path
324 324 self.oldpath = None
325 325 self.mode = None
326 326 self.op = 'MODIFY'
327 327 self.binary = False
328 328
329 329 def setmode(self, mode):
330 330 islink = mode & 0o20000
331 331 isexec = mode & 0o100
332 332 self.mode = (islink, isexec)
333 333
334 334 def copy(self):
335 335 other = patchmeta(self.path)
336 336 other.oldpath = self.oldpath
337 337 other.mode = self.mode
338 338 other.op = self.op
339 339 other.binary = self.binary
340 340 return other
341 341
342 342 def _ispatchinga(self, afile):
343 343 if afile == '/dev/null':
344 344 return self.op == 'ADD'
345 345 return afile == 'a/' + (self.oldpath or self.path)
346 346
347 347 def _ispatchingb(self, bfile):
348 348 if bfile == '/dev/null':
349 349 return self.op == 'DELETE'
350 350 return bfile == 'b/' + self.path
351 351
352 352 def ispatching(self, afile, bfile):
353 353 return self._ispatchinga(afile) and self._ispatchingb(bfile)
354 354
355 355 def __repr__(self):
356 356 return "<patchmeta %s %r>" % (self.op, self.path)
357 357
358 358 def readgitpatch(lr):
359 359 """extract git-style metadata about patches from <patchname>"""
360 360
361 361 # Filter patch for git information
362 362 gp = None
363 363 gitpatches = []
364 364 for line in lr:
365 365 line = line.rstrip(' \r\n')
366 366 if line.startswith('diff --git a/'):
367 367 m = gitre.match(line)
368 368 if m:
369 369 if gp:
370 370 gitpatches.append(gp)
371 371 dst = m.group(2)
372 372 gp = patchmeta(dst)
373 373 elif gp:
374 374 if line.startswith('--- '):
375 375 gitpatches.append(gp)
376 376 gp = None
377 377 continue
378 378 if line.startswith('rename from '):
379 379 gp.op = 'RENAME'
380 380 gp.oldpath = line[12:]
381 381 elif line.startswith('rename to '):
382 382 gp.path = line[10:]
383 383 elif line.startswith('copy from '):
384 384 gp.op = 'COPY'
385 385 gp.oldpath = line[10:]
386 386 elif line.startswith('copy to '):
387 387 gp.path = line[8:]
388 388 elif line.startswith('deleted file'):
389 389 gp.op = 'DELETE'
390 390 elif line.startswith('new file mode '):
391 391 gp.op = 'ADD'
392 392 gp.setmode(int(line[-6:], 8))
393 393 elif line.startswith('new mode '):
394 394 gp.setmode(int(line[-6:], 8))
395 395 elif line.startswith('GIT binary patch'):
396 396 gp.binary = True
397 397 if gp:
398 398 gitpatches.append(gp)
399 399
400 400 return gitpatches
401 401
402 402 class linereader(object):
403 403 # simple class to allow pushing lines back into the input stream
404 404 def __init__(self, fp):
405 405 self.fp = fp
406 406 self.buf = []
407 407
408 408 def push(self, line):
409 409 if line is not None:
410 410 self.buf.append(line)
411 411
412 412 def readline(self):
413 413 if self.buf:
414 414 l = self.buf[0]
415 415 del self.buf[0]
416 416 return l
417 417 return self.fp.readline()
418 418
419 419 def __iter__(self):
420 420 return iter(self.readline, '')
421 421
422 422 class abstractbackend(object):
423 423 def __init__(self, ui):
424 424 self.ui = ui
425 425
426 426 def getfile(self, fname):
427 427 """Return target file data and flags as a (data, (islink,
428 428 isexec)) tuple. Data is None if file is missing/deleted.
429 429 """
430 430 raise NotImplementedError
431 431
432 432 def setfile(self, fname, data, mode, copysource):
433 433 """Write data to target file fname and set its mode. mode is a
434 434 (islink, isexec) tuple. If data is None, the file content should
435 435 be left unchanged. If the file is modified after being copied,
436 436 copysource is set to the original file name.
437 437 """
438 438 raise NotImplementedError
439 439
440 440 def unlink(self, fname):
441 441 """Unlink target file."""
442 442 raise NotImplementedError
443 443
444 444 def writerej(self, fname, failed, total, lines):
445 445 """Write rejected lines for fname. total is the number of hunks
446 446 which failed to apply and total the total number of hunks for this
447 447 files.
448 448 """
449 449
450 450 def exists(self, fname):
451 451 raise NotImplementedError
452 452
453 453 def close(self):
454 454 raise NotImplementedError
455 455
456 456 class fsbackend(abstractbackend):
457 457 def __init__(self, ui, basedir):
458 458 super(fsbackend, self).__init__(ui)
459 459 self.opener = vfsmod.vfs(basedir)
460 460
461 461 def getfile(self, fname):
462 462 if self.opener.islink(fname):
463 463 return (self.opener.readlink(fname), (True, False))
464 464
465 465 isexec = False
466 466 try:
467 467 isexec = self.opener.lstat(fname).st_mode & 0o100 != 0
468 468 except OSError as e:
469 469 if e.errno != errno.ENOENT:
470 470 raise
471 471 try:
472 472 return (self.opener.read(fname), (False, isexec))
473 473 except IOError as e:
474 474 if e.errno != errno.ENOENT:
475 475 raise
476 476 return None, None
477 477
478 478 def setfile(self, fname, data, mode, copysource):
479 479 islink, isexec = mode
480 480 if data is None:
481 481 self.opener.setflags(fname, islink, isexec)
482 482 return
483 483 if islink:
484 484 self.opener.symlink(data, fname)
485 485 else:
486 486 self.opener.write(fname, data)
487 487 if isexec:
488 488 self.opener.setflags(fname, False, True)
489 489
490 490 def unlink(self, fname):
491 491 self.opener.unlinkpath(fname, ignoremissing=True)
492 492
493 493 def writerej(self, fname, failed, total, lines):
494 494 fname = fname + ".rej"
495 495 self.ui.warn(
496 496 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
497 497 (failed, total, fname))
498 498 fp = self.opener(fname, 'w')
499 499 fp.writelines(lines)
500 500 fp.close()
501 501
502 502 def exists(self, fname):
503 503 return self.opener.lexists(fname)
504 504
505 505 class workingbackend(fsbackend):
506 506 def __init__(self, ui, repo, similarity):
507 507 super(workingbackend, self).__init__(ui, repo.root)
508 508 self.repo = repo
509 509 self.similarity = similarity
510 510 self.removed = set()
511 511 self.changed = set()
512 512 self.copied = []
513 513
514 514 def _checkknown(self, fname):
515 515 if self.repo.dirstate[fname] == '?' and self.exists(fname):
516 516 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
517 517
518 518 def setfile(self, fname, data, mode, copysource):
519 519 self._checkknown(fname)
520 520 super(workingbackend, self).setfile(fname, data, mode, copysource)
521 521 if copysource is not None:
522 522 self.copied.append((copysource, fname))
523 523 self.changed.add(fname)
524 524
525 525 def unlink(self, fname):
526 526 self._checkknown(fname)
527 527 super(workingbackend, self).unlink(fname)
528 528 self.removed.add(fname)
529 529 self.changed.add(fname)
530 530
531 531 def close(self):
532 532 wctx = self.repo[None]
533 533 changed = set(self.changed)
534 534 for src, dst in self.copied:
535 535 scmutil.dirstatecopy(self.ui, self.repo, wctx, src, dst)
536 536 if self.removed:
537 537 wctx.forget(sorted(self.removed))
538 538 for f in self.removed:
539 539 if f not in self.repo.dirstate:
540 540 # File was deleted and no longer belongs to the
541 541 # dirstate, it was probably marked added then
542 542 # deleted, and should not be considered by
543 543 # marktouched().
544 544 changed.discard(f)
545 545 if changed:
546 546 scmutil.marktouched(self.repo, changed, self.similarity)
547 547 return sorted(self.changed)
548 548
549 549 class filestore(object):
550 550 def __init__(self, maxsize=None):
551 551 self.opener = None
552 552 self.files = {}
553 553 self.created = 0
554 554 self.maxsize = maxsize
555 555 if self.maxsize is None:
556 556 self.maxsize = 4*(2**20)
557 557 self.size = 0
558 558 self.data = {}
559 559
560 560 def setfile(self, fname, data, mode, copied=None):
561 561 if self.maxsize < 0 or (len(data) + self.size) <= self.maxsize:
562 562 self.data[fname] = (data, mode, copied)
563 563 self.size += len(data)
564 564 else:
565 565 if self.opener is None:
566 566 root = tempfile.mkdtemp(prefix='hg-patch-')
567 567 self.opener = vfsmod.vfs(root)
568 568 # Avoid filename issues with these simple names
569 fn = str(self.created)
569 fn = '%d' % self.created
570 570 self.opener.write(fn, data)
571 571 self.created += 1
572 572 self.files[fname] = (fn, mode, copied)
573 573
574 574 def getfile(self, fname):
575 575 if fname in self.data:
576 576 return self.data[fname]
577 577 if not self.opener or fname not in self.files:
578 578 return None, None, None
579 579 fn, mode, copied = self.files[fname]
580 580 return self.opener.read(fn), mode, copied
581 581
582 582 def close(self):
583 583 if self.opener:
584 584 shutil.rmtree(self.opener.base)
585 585
586 586 class repobackend(abstractbackend):
587 587 def __init__(self, ui, repo, ctx, store):
588 588 super(repobackend, self).__init__(ui)
589 589 self.repo = repo
590 590 self.ctx = ctx
591 591 self.store = store
592 592 self.changed = set()
593 593 self.removed = set()
594 594 self.copied = {}
595 595
596 596 def _checkknown(self, fname):
597 597 if fname not in self.ctx:
598 598 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
599 599
600 600 def getfile(self, fname):
601 601 try:
602 602 fctx = self.ctx[fname]
603 603 except error.LookupError:
604 604 return None, None
605 605 flags = fctx.flags()
606 606 return fctx.data(), ('l' in flags, 'x' in flags)
607 607
608 608 def setfile(self, fname, data, mode, copysource):
609 609 if copysource:
610 610 self._checkknown(copysource)
611 611 if data is None:
612 612 data = self.ctx[fname].data()
613 613 self.store.setfile(fname, data, mode, copysource)
614 614 self.changed.add(fname)
615 615 if copysource:
616 616 self.copied[fname] = copysource
617 617
618 618 def unlink(self, fname):
619 619 self._checkknown(fname)
620 620 self.removed.add(fname)
621 621
622 622 def exists(self, fname):
623 623 return fname in self.ctx
624 624
625 625 def close(self):
626 626 return self.changed | self.removed
627 627
628 628 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
629 629 unidesc = re.compile('@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@')
630 630 contextdesc = re.compile('(?:---|\*\*\*) (\d+)(?:,(\d+))? (?:---|\*\*\*)')
631 631 eolmodes = ['strict', 'crlf', 'lf', 'auto']
632 632
633 633 class patchfile(object):
634 634 def __init__(self, ui, gp, backend, store, eolmode='strict'):
635 635 self.fname = gp.path
636 636 self.eolmode = eolmode
637 637 self.eol = None
638 638 self.backend = backend
639 639 self.ui = ui
640 640 self.lines = []
641 641 self.exists = False
642 642 self.missing = True
643 643 self.mode = gp.mode
644 644 self.copysource = gp.oldpath
645 645 self.create = gp.op in ('ADD', 'COPY', 'RENAME')
646 646 self.remove = gp.op == 'DELETE'
647 647 if self.copysource is None:
648 648 data, mode = backend.getfile(self.fname)
649 649 else:
650 650 data, mode = store.getfile(self.copysource)[:2]
651 651 if data is not None:
652 652 self.exists = self.copysource is None or backend.exists(self.fname)
653 653 self.missing = False
654 654 if data:
655 655 self.lines = mdiff.splitnewlines(data)
656 656 if self.mode is None:
657 657 self.mode = mode
658 658 if self.lines:
659 659 # Normalize line endings
660 660 if self.lines[0].endswith('\r\n'):
661 661 self.eol = '\r\n'
662 662 elif self.lines[0].endswith('\n'):
663 663 self.eol = '\n'
664 664 if eolmode != 'strict':
665 665 nlines = []
666 666 for l in self.lines:
667 667 if l.endswith('\r\n'):
668 668 l = l[:-2] + '\n'
669 669 nlines.append(l)
670 670 self.lines = nlines
671 671 else:
672 672 if self.create:
673 673 self.missing = False
674 674 if self.mode is None:
675 675 self.mode = (False, False)
676 676 if self.missing:
677 677 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
678 678 self.ui.warn(_("(use '--prefix' to apply patch relative to the "
679 679 "current directory)\n"))
680 680
681 681 self.hash = {}
682 682 self.dirty = 0
683 683 self.offset = 0
684 684 self.skew = 0
685 685 self.rej = []
686 686 self.fileprinted = False
687 687 self.printfile(False)
688 688 self.hunks = 0
689 689
690 690 def writelines(self, fname, lines, mode):
691 691 if self.eolmode == 'auto':
692 692 eol = self.eol
693 693 elif self.eolmode == 'crlf':
694 694 eol = '\r\n'
695 695 else:
696 696 eol = '\n'
697 697
698 698 if self.eolmode != 'strict' and eol and eol != '\n':
699 699 rawlines = []
700 700 for l in lines:
701 701 if l and l[-1] == '\n':
702 702 l = l[:-1] + eol
703 703 rawlines.append(l)
704 704 lines = rawlines
705 705
706 706 self.backend.setfile(fname, ''.join(lines), mode, self.copysource)
707 707
708 708 def printfile(self, warn):
709 709 if self.fileprinted:
710 710 return
711 711 if warn or self.ui.verbose:
712 712 self.fileprinted = True
713 713 s = _("patching file %s\n") % self.fname
714 714 if warn:
715 715 self.ui.warn(s)
716 716 else:
717 717 self.ui.note(s)
718 718
719 719
720 720 def findlines(self, l, linenum):
721 721 # looks through the hash and finds candidate lines. The
722 722 # result is a list of line numbers sorted based on distance
723 723 # from linenum
724 724
725 725 cand = self.hash.get(l, [])
726 726 if len(cand) > 1:
727 727 # resort our list of potentials forward then back.
728 728 cand.sort(key=lambda x: abs(x - linenum))
729 729 return cand
730 730
731 731 def write_rej(self):
732 732 # our rejects are a little different from patch(1). This always
733 733 # creates rejects in the same form as the original patch. A file
734 734 # header is inserted so that you can run the reject through patch again
735 735 # without having to type the filename.
736 736 if not self.rej:
737 737 return
738 738 base = os.path.basename(self.fname)
739 739 lines = ["--- %s\n+++ %s\n" % (base, base)]
740 740 for x in self.rej:
741 741 for l in x.hunk:
742 742 lines.append(l)
743 743 if l[-1:] != '\n':
744 744 lines.append("\n\ No newline at end of file\n")
745 745 self.backend.writerej(self.fname, len(self.rej), self.hunks, lines)
746 746
747 747 def apply(self, h):
748 748 if not h.complete():
749 749 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
750 750 (h.number, h.desc, len(h.a), h.lena, len(h.b),
751 751 h.lenb))
752 752
753 753 self.hunks += 1
754 754
755 755 if self.missing:
756 756 self.rej.append(h)
757 757 return -1
758 758
759 759 if self.exists and self.create:
760 760 if self.copysource:
761 761 self.ui.warn(_("cannot create %s: destination already "
762 762 "exists\n") % self.fname)
763 763 else:
764 764 self.ui.warn(_("file %s already exists\n") % self.fname)
765 765 self.rej.append(h)
766 766 return -1
767 767
768 768 if isinstance(h, binhunk):
769 769 if self.remove:
770 770 self.backend.unlink(self.fname)
771 771 else:
772 772 l = h.new(self.lines)
773 773 self.lines[:] = l
774 774 self.offset += len(l)
775 775 self.dirty = True
776 776 return 0
777 777
778 778 horig = h
779 779 if (self.eolmode in ('crlf', 'lf')
780 780 or self.eolmode == 'auto' and self.eol):
781 781 # If new eols are going to be normalized, then normalize
782 782 # hunk data before patching. Otherwise, preserve input
783 783 # line-endings.
784 784 h = h.getnormalized()
785 785
786 786 # fast case first, no offsets, no fuzz
787 787 old, oldstart, new, newstart = h.fuzzit(0, False)
788 788 oldstart += self.offset
789 789 orig_start = oldstart
790 790 # if there's skew we want to emit the "(offset %d lines)" even
791 791 # when the hunk cleanly applies at start + skew, so skip the
792 792 # fast case code
793 793 if (self.skew == 0 and
794 794 diffhelpers.testhunk(old, self.lines, oldstart) == 0):
795 795 if self.remove:
796 796 self.backend.unlink(self.fname)
797 797 else:
798 798 self.lines[oldstart:oldstart + len(old)] = new
799 799 self.offset += len(new) - len(old)
800 800 self.dirty = True
801 801 return 0
802 802
803 803 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
804 804 self.hash = {}
805 805 for x, s in enumerate(self.lines):
806 806 self.hash.setdefault(s, []).append(x)
807 807
808 808 for fuzzlen in xrange(self.ui.configint("patch", "fuzz") + 1):
809 809 for toponly in [True, False]:
810 810 old, oldstart, new, newstart = h.fuzzit(fuzzlen, toponly)
811 811 oldstart = oldstart + self.offset + self.skew
812 812 oldstart = min(oldstart, len(self.lines))
813 813 if old:
814 814 cand = self.findlines(old[0][1:], oldstart)
815 815 else:
816 816 # Only adding lines with no or fuzzed context, just
817 817 # take the skew in account
818 818 cand = [oldstart]
819 819
820 820 for l in cand:
821 821 if not old or diffhelpers.testhunk(old, self.lines, l) == 0:
822 822 self.lines[l : l + len(old)] = new
823 823 self.offset += len(new) - len(old)
824 824 self.skew = l - orig_start
825 825 self.dirty = True
826 826 offset = l - orig_start - fuzzlen
827 827 if fuzzlen:
828 828 msg = _("Hunk #%d succeeded at %d "
829 829 "with fuzz %d "
830 830 "(offset %d lines).\n")
831 831 self.printfile(True)
832 832 self.ui.warn(msg %
833 833 (h.number, l + 1, fuzzlen, offset))
834 834 else:
835 835 msg = _("Hunk #%d succeeded at %d "
836 836 "(offset %d lines).\n")
837 837 self.ui.note(msg % (h.number, l + 1, offset))
838 838 return fuzzlen
839 839 self.printfile(True)
840 840 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
841 841 self.rej.append(horig)
842 842 return -1
843 843
844 844 def close(self):
845 845 if self.dirty:
846 846 self.writelines(self.fname, self.lines, self.mode)
847 847 self.write_rej()
848 848 return len(self.rej)
849 849
850 850 class header(object):
851 851 """patch header
852 852 """
853 853 diffgit_re = re.compile('diff --git a/(.*) b/(.*)$')
854 854 diff_re = re.compile('diff -r .* (.*)$')
855 855 allhunks_re = re.compile('(?:index|deleted file) ')
856 856 pretty_re = re.compile('(?:new file|deleted file) ')
857 857 special_re = re.compile('(?:index|deleted|copy|rename) ')
858 858 newfile_re = re.compile('(?:new file)')
859 859
860 860 def __init__(self, header):
861 861 self.header = header
862 862 self.hunks = []
863 863
864 864 def binary(self):
865 865 return any(h.startswith('index ') for h in self.header)
866 866
867 867 def pretty(self, fp):
868 868 for h in self.header:
869 869 if h.startswith('index '):
870 870 fp.write(_('this modifies a binary file (all or nothing)\n'))
871 871 break
872 872 if self.pretty_re.match(h):
873 873 fp.write(h)
874 874 if self.binary():
875 875 fp.write(_('this is a binary file\n'))
876 876 break
877 877 if h.startswith('---'):
878 878 fp.write(_('%d hunks, %d lines changed\n') %
879 879 (len(self.hunks),
880 880 sum([max(h.added, h.removed) for h in self.hunks])))
881 881 break
882 882 fp.write(h)
883 883
884 884 def write(self, fp):
885 885 fp.write(''.join(self.header))
886 886
887 887 def allhunks(self):
888 888 return any(self.allhunks_re.match(h) for h in self.header)
889 889
890 890 def files(self):
891 891 match = self.diffgit_re.match(self.header[0])
892 892 if match:
893 893 fromfile, tofile = match.groups()
894 894 if fromfile == tofile:
895 895 return [fromfile]
896 896 return [fromfile, tofile]
897 897 else:
898 898 return self.diff_re.match(self.header[0]).groups()
899 899
900 900 def filename(self):
901 901 return self.files()[-1]
902 902
903 903 def __repr__(self):
904 904 return '<header %s>' % (' '.join(map(repr, self.files())))
905 905
906 906 def isnewfile(self):
907 907 return any(self.newfile_re.match(h) for h in self.header)
908 908
909 909 def special(self):
910 910 # Special files are shown only at the header level and not at the hunk
911 911 # level for example a file that has been deleted is a special file.
912 912 # The user cannot change the content of the operation, in the case of
913 913 # the deleted file he has to take the deletion or not take it, he
914 914 # cannot take some of it.
915 915 # Newly added files are special if they are empty, they are not special
916 916 # if they have some content as we want to be able to change it
917 917 nocontent = len(self.header) == 2
918 918 emptynewfile = self.isnewfile() and nocontent
919 919 return emptynewfile or \
920 920 any(self.special_re.match(h) for h in self.header)
921 921
922 922 class recordhunk(object):
923 923 """patch hunk
924 924
925 925 XXX shouldn't we merge this with the other hunk class?
926 926 """
927 927
928 928 def __init__(self, header, fromline, toline, proc, before, hunk, after,
929 929 maxcontext=None):
930 930 def trimcontext(lines, reverse=False):
931 931 if maxcontext is not None:
932 932 delta = len(lines) - maxcontext
933 933 if delta > 0:
934 934 if reverse:
935 935 return delta, lines[delta:]
936 936 else:
937 937 return delta, lines[:maxcontext]
938 938 return 0, lines
939 939
940 940 self.header = header
941 941 trimedbefore, self.before = trimcontext(before, True)
942 942 self.fromline = fromline + trimedbefore
943 943 self.toline = toline + trimedbefore
944 944 _trimedafter, self.after = trimcontext(after, False)
945 945 self.proc = proc
946 946 self.hunk = hunk
947 947 self.added, self.removed = self.countchanges(self.hunk)
948 948
949 949 def __eq__(self, v):
950 950 if not isinstance(v, recordhunk):
951 951 return False
952 952
953 953 return ((v.hunk == self.hunk) and
954 954 (v.proc == self.proc) and
955 955 (self.fromline == v.fromline) and
956 956 (self.header.files() == v.header.files()))
957 957
958 958 def __hash__(self):
959 959 return hash((tuple(self.hunk),
960 960 tuple(self.header.files()),
961 961 self.fromline,
962 962 self.proc))
963 963
964 964 def countchanges(self, hunk):
965 965 """hunk -> (n+,n-)"""
966 966 add = len([h for h in hunk if h.startswith('+')])
967 967 rem = len([h for h in hunk if h.startswith('-')])
968 968 return add, rem
969 969
970 970 def reversehunk(self):
971 971 """return another recordhunk which is the reverse of the hunk
972 972
973 973 If this hunk is diff(A, B), the returned hunk is diff(B, A). To do
974 974 that, swap fromline/toline and +/- signs while keep other things
975 975 unchanged.
976 976 """
977 977 m = {'+': '-', '-': '+', '\\': '\\'}
978 978 hunk = ['%s%s' % (m[l[0:1]], l[1:]) for l in self.hunk]
979 979 return recordhunk(self.header, self.toline, self.fromline, self.proc,
980 980 self.before, hunk, self.after)
981 981
982 982 def write(self, fp):
983 983 delta = len(self.before) + len(self.after)
984 984 if self.after and self.after[-1] == '\\ No newline at end of file\n':
985 985 delta -= 1
986 986 fromlen = delta + self.removed
987 987 tolen = delta + self.added
988 988 fp.write('@@ -%d,%d +%d,%d @@%s\n' %
989 989 (self.fromline, fromlen, self.toline, tolen,
990 990 self.proc and (' ' + self.proc)))
991 991 fp.write(''.join(self.before + self.hunk + self.after))
992 992
993 993 pretty = write
994 994
995 995 def filename(self):
996 996 return self.header.filename()
997 997
998 998 def __repr__(self):
999 999 return '<hunk %r@%d>' % (self.filename(), self.fromline)
1000 1000
1001 1001 def getmessages():
1002 1002 return {
1003 1003 'multiple': {
1004 1004 'apply': _("apply change %d/%d to '%s'?"),
1005 1005 'discard': _("discard change %d/%d to '%s'?"),
1006 1006 'record': _("record change %d/%d to '%s'?"),
1007 1007 },
1008 1008 'single': {
1009 1009 'apply': _("apply this change to '%s'?"),
1010 1010 'discard': _("discard this change to '%s'?"),
1011 1011 'record': _("record this change to '%s'?"),
1012 1012 },
1013 1013 'help': {
1014 1014 'apply': _('[Ynesfdaq?]'
1015 1015 '$$ &Yes, apply this change'
1016 1016 '$$ &No, skip this change'
1017 1017 '$$ &Edit this change manually'
1018 1018 '$$ &Skip remaining changes to this file'
1019 1019 '$$ Apply remaining changes to this &file'
1020 1020 '$$ &Done, skip remaining changes and files'
1021 1021 '$$ Apply &all changes to all remaining files'
1022 1022 '$$ &Quit, applying no changes'
1023 1023 '$$ &? (display help)'),
1024 1024 'discard': _('[Ynesfdaq?]'
1025 1025 '$$ &Yes, discard this change'
1026 1026 '$$ &No, skip this change'
1027 1027 '$$ &Edit this change manually'
1028 1028 '$$ &Skip remaining changes to this file'
1029 1029 '$$ Discard remaining changes to this &file'
1030 1030 '$$ &Done, skip remaining changes and files'
1031 1031 '$$ Discard &all changes to all remaining files'
1032 1032 '$$ &Quit, discarding no changes'
1033 1033 '$$ &? (display help)'),
1034 1034 'record': _('[Ynesfdaq?]'
1035 1035 '$$ &Yes, record this change'
1036 1036 '$$ &No, skip this change'
1037 1037 '$$ &Edit this change manually'
1038 1038 '$$ &Skip remaining changes to this file'
1039 1039 '$$ Record remaining changes to this &file'
1040 1040 '$$ &Done, skip remaining changes and files'
1041 1041 '$$ Record &all changes to all remaining files'
1042 1042 '$$ &Quit, recording no changes'
1043 1043 '$$ &? (display help)'),
1044 1044 }
1045 1045 }
1046 1046
1047 1047 def filterpatch(ui, headers, operation=None):
1048 1048 """Interactively filter patch chunks into applied-only chunks"""
1049 1049 messages = getmessages()
1050 1050
1051 1051 if operation is None:
1052 1052 operation = 'record'
1053 1053
1054 1054 def prompt(skipfile, skipall, query, chunk):
1055 1055 """prompt query, and process base inputs
1056 1056
1057 1057 - y/n for the rest of file
1058 1058 - y/n for the rest
1059 1059 - ? (help)
1060 1060 - q (quit)
1061 1061
1062 1062 Return True/False and possibly updated skipfile and skipall.
1063 1063 """
1064 1064 newpatches = None
1065 1065 if skipall is not None:
1066 1066 return skipall, skipfile, skipall, newpatches
1067 1067 if skipfile is not None:
1068 1068 return skipfile, skipfile, skipall, newpatches
1069 1069 while True:
1070 1070 resps = messages['help'][operation]
1071 1071 r = ui.promptchoice("%s %s" % (query, resps))
1072 1072 ui.write("\n")
1073 1073 if r == 8: # ?
1074 1074 for c, t in ui.extractchoices(resps)[1]:
1075 1075 ui.write('%s - %s\n' % (c, encoding.lower(t)))
1076 1076 continue
1077 1077 elif r == 0: # yes
1078 1078 ret = True
1079 1079 elif r == 1: # no
1080 1080 ret = False
1081 1081 elif r == 2: # Edit patch
1082 1082 if chunk is None:
1083 1083 ui.write(_('cannot edit patch for whole file'))
1084 1084 ui.write("\n")
1085 1085 continue
1086 1086 if chunk.header.binary():
1087 1087 ui.write(_('cannot edit patch for binary file'))
1088 1088 ui.write("\n")
1089 1089 continue
1090 1090 # Patch comment based on the Git one (based on comment at end of
1091 1091 # https://mercurial-scm.org/wiki/RecordExtension)
1092 1092 phelp = '---' + _("""
1093 1093 To remove '-' lines, make them ' ' lines (context).
1094 1094 To remove '+' lines, delete them.
1095 1095 Lines starting with # will be removed from the patch.
1096 1096
1097 1097 If the patch applies cleanly, the edited hunk will immediately be
1098 1098 added to the record list. If it does not apply cleanly, a rejects
1099 1099 file will be generated: you can use that when you try again. If
1100 1100 all lines of the hunk are removed, then the edit is aborted and
1101 1101 the hunk is left unchanged.
1102 1102 """)
1103 1103 (patchfd, patchfn) = tempfile.mkstemp(prefix="hg-editor-",
1104 1104 suffix=".diff", text=True)
1105 1105 ncpatchfp = None
1106 1106 try:
1107 1107 # Write the initial patch
1108 1108 f = os.fdopen(patchfd, pycompat.sysstr("w"))
1109 1109 chunk.header.write(f)
1110 1110 chunk.write(f)
1111 1111 f.write('\n'.join(['# ' + i for i in phelp.splitlines()]))
1112 1112 f.close()
1113 1113 # Start the editor and wait for it to complete
1114 1114 editor = ui.geteditor()
1115 1115 ret = ui.system("%s \"%s\"" % (editor, patchfn),
1116 1116 environ={'HGUSER': ui.username()},
1117 1117 blockedtag='filterpatch')
1118 1118 if ret != 0:
1119 1119 ui.warn(_("editor exited with exit code %d\n") % ret)
1120 1120 continue
1121 1121 # Remove comment lines
1122 1122 patchfp = open(patchfn)
1123 1123 ncpatchfp = stringio()
1124 1124 for line in util.iterfile(patchfp):
1125 1125 if not line.startswith('#'):
1126 1126 ncpatchfp.write(line)
1127 1127 patchfp.close()
1128 1128 ncpatchfp.seek(0)
1129 1129 newpatches = parsepatch(ncpatchfp)
1130 1130 finally:
1131 1131 os.unlink(patchfn)
1132 1132 del ncpatchfp
1133 1133 # Signal that the chunk shouldn't be applied as-is, but
1134 1134 # provide the new patch to be used instead.
1135 1135 ret = False
1136 1136 elif r == 3: # Skip
1137 1137 ret = skipfile = False
1138 1138 elif r == 4: # file (Record remaining)
1139 1139 ret = skipfile = True
1140 1140 elif r == 5: # done, skip remaining
1141 1141 ret = skipall = False
1142 1142 elif r == 6: # all
1143 1143 ret = skipall = True
1144 1144 elif r == 7: # quit
1145 1145 raise error.Abort(_('user quit'))
1146 1146 return ret, skipfile, skipall, newpatches
1147 1147
1148 1148 seen = set()
1149 1149 applied = {} # 'filename' -> [] of chunks
1150 1150 skipfile, skipall = None, None
1151 1151 pos, total = 1, sum(len(h.hunks) for h in headers)
1152 1152 for h in headers:
1153 1153 pos += len(h.hunks)
1154 1154 skipfile = None
1155 1155 fixoffset = 0
1156 1156 hdr = ''.join(h.header)
1157 1157 if hdr in seen:
1158 1158 continue
1159 1159 seen.add(hdr)
1160 1160 if skipall is None:
1161 1161 h.pretty(ui)
1162 1162 msg = (_('examine changes to %s?') %
1163 1163 _(' and ').join("'%s'" % f for f in h.files()))
1164 1164 r, skipfile, skipall, np = prompt(skipfile, skipall, msg, None)
1165 1165 if not r:
1166 1166 continue
1167 1167 applied[h.filename()] = [h]
1168 1168 if h.allhunks():
1169 1169 applied[h.filename()] += h.hunks
1170 1170 continue
1171 1171 for i, chunk in enumerate(h.hunks):
1172 1172 if skipfile is None and skipall is None:
1173 1173 chunk.pretty(ui)
1174 1174 if total == 1:
1175 1175 msg = messages['single'][operation] % chunk.filename()
1176 1176 else:
1177 1177 idx = pos - len(h.hunks) + i
1178 1178 msg = messages['multiple'][operation] % (idx, total,
1179 1179 chunk.filename())
1180 1180 r, skipfile, skipall, newpatches = prompt(skipfile,
1181 1181 skipall, msg, chunk)
1182 1182 if r:
1183 1183 if fixoffset:
1184 1184 chunk = copy.copy(chunk)
1185 1185 chunk.toline += fixoffset
1186 1186 applied[chunk.filename()].append(chunk)
1187 1187 elif newpatches is not None:
1188 1188 for newpatch in newpatches:
1189 1189 for newhunk in newpatch.hunks:
1190 1190 if fixoffset:
1191 1191 newhunk.toline += fixoffset
1192 1192 applied[newhunk.filename()].append(newhunk)
1193 1193 else:
1194 1194 fixoffset += chunk.removed - chunk.added
1195 1195 return (sum([h for h in applied.itervalues()
1196 1196 if h[0].special() or len(h) > 1], []), {})
1197 1197 class hunk(object):
1198 1198 def __init__(self, desc, num, lr, context):
1199 1199 self.number = num
1200 1200 self.desc = desc
1201 1201 self.hunk = [desc]
1202 1202 self.a = []
1203 1203 self.b = []
1204 1204 self.starta = self.lena = None
1205 1205 self.startb = self.lenb = None
1206 1206 if lr is not None:
1207 1207 if context:
1208 1208 self.read_context_hunk(lr)
1209 1209 else:
1210 1210 self.read_unified_hunk(lr)
1211 1211
1212 1212 def getnormalized(self):
1213 1213 """Return a copy with line endings normalized to LF."""
1214 1214
1215 1215 def normalize(lines):
1216 1216 nlines = []
1217 1217 for line in lines:
1218 1218 if line.endswith('\r\n'):
1219 1219 line = line[:-2] + '\n'
1220 1220 nlines.append(line)
1221 1221 return nlines
1222 1222
1223 1223 # Dummy object, it is rebuilt manually
1224 1224 nh = hunk(self.desc, self.number, None, None)
1225 1225 nh.number = self.number
1226 1226 nh.desc = self.desc
1227 1227 nh.hunk = self.hunk
1228 1228 nh.a = normalize(self.a)
1229 1229 nh.b = normalize(self.b)
1230 1230 nh.starta = self.starta
1231 1231 nh.startb = self.startb
1232 1232 nh.lena = self.lena
1233 1233 nh.lenb = self.lenb
1234 1234 return nh
1235 1235
1236 1236 def read_unified_hunk(self, lr):
1237 1237 m = unidesc.match(self.desc)
1238 1238 if not m:
1239 1239 raise PatchError(_("bad hunk #%d") % self.number)
1240 1240 self.starta, self.lena, self.startb, self.lenb = m.groups()
1241 1241 if self.lena is None:
1242 1242 self.lena = 1
1243 1243 else:
1244 1244 self.lena = int(self.lena)
1245 1245 if self.lenb is None:
1246 1246 self.lenb = 1
1247 1247 else:
1248 1248 self.lenb = int(self.lenb)
1249 1249 self.starta = int(self.starta)
1250 1250 self.startb = int(self.startb)
1251 1251 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a,
1252 1252 self.b)
1253 1253 # if we hit eof before finishing out the hunk, the last line will
1254 1254 # be zero length. Lets try to fix it up.
1255 1255 while len(self.hunk[-1]) == 0:
1256 1256 del self.hunk[-1]
1257 1257 del self.a[-1]
1258 1258 del self.b[-1]
1259 1259 self.lena -= 1
1260 1260 self.lenb -= 1
1261 1261 self._fixnewline(lr)
1262 1262
1263 1263 def read_context_hunk(self, lr):
1264 1264 self.desc = lr.readline()
1265 1265 m = contextdesc.match(self.desc)
1266 1266 if not m:
1267 1267 raise PatchError(_("bad hunk #%d") % self.number)
1268 1268 self.starta, aend = m.groups()
1269 1269 self.starta = int(self.starta)
1270 1270 if aend is None:
1271 1271 aend = self.starta
1272 1272 self.lena = int(aend) - self.starta
1273 1273 if self.starta:
1274 1274 self.lena += 1
1275 1275 for x in xrange(self.lena):
1276 1276 l = lr.readline()
1277 1277 if l.startswith('---'):
1278 1278 # lines addition, old block is empty
1279 1279 lr.push(l)
1280 1280 break
1281 1281 s = l[2:]
1282 1282 if l.startswith('- ') or l.startswith('! '):
1283 1283 u = '-' + s
1284 1284 elif l.startswith(' '):
1285 1285 u = ' ' + s
1286 1286 else:
1287 1287 raise PatchError(_("bad hunk #%d old text line %d") %
1288 1288 (self.number, x))
1289 1289 self.a.append(u)
1290 1290 self.hunk.append(u)
1291 1291
1292 1292 l = lr.readline()
1293 1293 if l.startswith('\ '):
1294 1294 s = self.a[-1][:-1]
1295 1295 self.a[-1] = s
1296 1296 self.hunk[-1] = s
1297 1297 l = lr.readline()
1298 1298 m = contextdesc.match(l)
1299 1299 if not m:
1300 1300 raise PatchError(_("bad hunk #%d") % self.number)
1301 1301 self.startb, bend = m.groups()
1302 1302 self.startb = int(self.startb)
1303 1303 if bend is None:
1304 1304 bend = self.startb
1305 1305 self.lenb = int(bend) - self.startb
1306 1306 if self.startb:
1307 1307 self.lenb += 1
1308 1308 hunki = 1
1309 1309 for x in xrange(self.lenb):
1310 1310 l = lr.readline()
1311 1311 if l.startswith('\ '):
1312 1312 # XXX: the only way to hit this is with an invalid line range.
1313 1313 # The no-eol marker is not counted in the line range, but I
1314 1314 # guess there are diff(1) out there which behave differently.
1315 1315 s = self.b[-1][:-1]
1316 1316 self.b[-1] = s
1317 1317 self.hunk[hunki - 1] = s
1318 1318 continue
1319 1319 if not l:
1320 1320 # line deletions, new block is empty and we hit EOF
1321 1321 lr.push(l)
1322 1322 break
1323 1323 s = l[2:]
1324 1324 if l.startswith('+ ') or l.startswith('! '):
1325 1325 u = '+' + s
1326 1326 elif l.startswith(' '):
1327 1327 u = ' ' + s
1328 1328 elif len(self.b) == 0:
1329 1329 # line deletions, new block is empty
1330 1330 lr.push(l)
1331 1331 break
1332 1332 else:
1333 1333 raise PatchError(_("bad hunk #%d old text line %d") %
1334 1334 (self.number, x))
1335 1335 self.b.append(s)
1336 1336 while True:
1337 1337 if hunki >= len(self.hunk):
1338 1338 h = ""
1339 1339 else:
1340 1340 h = self.hunk[hunki]
1341 1341 hunki += 1
1342 1342 if h == u:
1343 1343 break
1344 1344 elif h.startswith('-'):
1345 1345 continue
1346 1346 else:
1347 1347 self.hunk.insert(hunki - 1, u)
1348 1348 break
1349 1349
1350 1350 if not self.a:
1351 1351 # this happens when lines were only added to the hunk
1352 1352 for x in self.hunk:
1353 1353 if x.startswith('-') or x.startswith(' '):
1354 1354 self.a.append(x)
1355 1355 if not self.b:
1356 1356 # this happens when lines were only deleted from the hunk
1357 1357 for x in self.hunk:
1358 1358 if x.startswith('+') or x.startswith(' '):
1359 1359 self.b.append(x[1:])
1360 1360 # @@ -start,len +start,len @@
1361 1361 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
1362 1362 self.startb, self.lenb)
1363 1363 self.hunk[0] = self.desc
1364 1364 self._fixnewline(lr)
1365 1365
1366 1366 def _fixnewline(self, lr):
1367 1367 l = lr.readline()
1368 1368 if l.startswith('\ '):
1369 1369 diffhelpers.fix_newline(self.hunk, self.a, self.b)
1370 1370 else:
1371 1371 lr.push(l)
1372 1372
1373 1373 def complete(self):
1374 1374 return len(self.a) == self.lena and len(self.b) == self.lenb
1375 1375
1376 1376 def _fuzzit(self, old, new, fuzz, toponly):
1377 1377 # this removes context lines from the top and bottom of list 'l'. It
1378 1378 # checks the hunk to make sure only context lines are removed, and then
1379 1379 # returns a new shortened list of lines.
1380 1380 fuzz = min(fuzz, len(old))
1381 1381 if fuzz:
1382 1382 top = 0
1383 1383 bot = 0
1384 1384 hlen = len(self.hunk)
1385 1385 for x in xrange(hlen - 1):
1386 1386 # the hunk starts with the @@ line, so use x+1
1387 1387 if self.hunk[x + 1][0] == ' ':
1388 1388 top += 1
1389 1389 else:
1390 1390 break
1391 1391 if not toponly:
1392 1392 for x in xrange(hlen - 1):
1393 1393 if self.hunk[hlen - bot - 1][0] == ' ':
1394 1394 bot += 1
1395 1395 else:
1396 1396 break
1397 1397
1398 1398 bot = min(fuzz, bot)
1399 1399 top = min(fuzz, top)
1400 1400 return old[top:len(old) - bot], new[top:len(new) - bot], top
1401 1401 return old, new, 0
1402 1402
1403 1403 def fuzzit(self, fuzz, toponly):
1404 1404 old, new, top = self._fuzzit(self.a, self.b, fuzz, toponly)
1405 1405 oldstart = self.starta + top
1406 1406 newstart = self.startb + top
1407 1407 # zero length hunk ranges already have their start decremented
1408 1408 if self.lena and oldstart > 0:
1409 1409 oldstart -= 1
1410 1410 if self.lenb and newstart > 0:
1411 1411 newstart -= 1
1412 1412 return old, oldstart, new, newstart
1413 1413
1414 1414 class binhunk(object):
1415 1415 'A binary patch file.'
1416 1416 def __init__(self, lr, fname):
1417 1417 self.text = None
1418 1418 self.delta = False
1419 1419 self.hunk = ['GIT binary patch\n']
1420 1420 self._fname = fname
1421 1421 self._read(lr)
1422 1422
1423 1423 def complete(self):
1424 1424 return self.text is not None
1425 1425
1426 1426 def new(self, lines):
1427 1427 if self.delta:
1428 1428 return [applybindelta(self.text, ''.join(lines))]
1429 1429 return [self.text]
1430 1430
1431 1431 def _read(self, lr):
1432 1432 def getline(lr, hunk):
1433 1433 l = lr.readline()
1434 1434 hunk.append(l)
1435 1435 return l.rstrip('\r\n')
1436 1436
1437 1437 size = 0
1438 1438 while True:
1439 1439 line = getline(lr, self.hunk)
1440 1440 if not line:
1441 1441 raise PatchError(_('could not extract "%s" binary data')
1442 1442 % self._fname)
1443 1443 if line.startswith('literal '):
1444 1444 size = int(line[8:].rstrip())
1445 1445 break
1446 1446 if line.startswith('delta '):
1447 1447 size = int(line[6:].rstrip())
1448 1448 self.delta = True
1449 1449 break
1450 1450 dec = []
1451 1451 line = getline(lr, self.hunk)
1452 1452 while len(line) > 1:
1453 1453 l = line[0:1]
1454 1454 if l <= 'Z' and l >= 'A':
1455 1455 l = ord(l) - ord('A') + 1
1456 1456 else:
1457 1457 l = ord(l) - ord('a') + 27
1458 1458 try:
1459 1459 dec.append(util.b85decode(line[1:])[:l])
1460 1460 except ValueError as e:
1461 1461 raise PatchError(_('could not decode "%s" binary patch: %s')
1462 1462 % (self._fname, util.forcebytestr(e)))
1463 1463 line = getline(lr, self.hunk)
1464 1464 text = zlib.decompress(''.join(dec))
1465 1465 if len(text) != size:
1466 1466 raise PatchError(_('"%s" length is %d bytes, should be %d')
1467 1467 % (self._fname, len(text), size))
1468 1468 self.text = text
1469 1469
1470 1470 def parsefilename(str):
1471 1471 # --- filename \t|space stuff
1472 1472 s = str[4:].rstrip('\r\n')
1473 1473 i = s.find('\t')
1474 1474 if i < 0:
1475 1475 i = s.find(' ')
1476 1476 if i < 0:
1477 1477 return s
1478 1478 return s[:i]
1479 1479
1480 1480 def reversehunks(hunks):
1481 1481 '''reverse the signs in the hunks given as argument
1482 1482
1483 1483 This function operates on hunks coming out of patch.filterpatch, that is
1484 1484 a list of the form: [header1, hunk1, hunk2, header2...]. Example usage:
1485 1485
1486 1486 >>> rawpatch = b"""diff --git a/folder1/g b/folder1/g
1487 1487 ... --- a/folder1/g
1488 1488 ... +++ b/folder1/g
1489 1489 ... @@ -1,7 +1,7 @@
1490 1490 ... +firstline
1491 1491 ... c
1492 1492 ... 1
1493 1493 ... 2
1494 1494 ... + 3
1495 1495 ... -4
1496 1496 ... 5
1497 1497 ... d
1498 1498 ... +lastline"""
1499 1499 >>> hunks = parsepatch([rawpatch])
1500 1500 >>> hunkscomingfromfilterpatch = []
1501 1501 >>> for h in hunks:
1502 1502 ... hunkscomingfromfilterpatch.append(h)
1503 1503 ... hunkscomingfromfilterpatch.extend(h.hunks)
1504 1504
1505 1505 >>> reversedhunks = reversehunks(hunkscomingfromfilterpatch)
1506 1506 >>> from . import util
1507 1507 >>> fp = util.stringio()
1508 1508 >>> for c in reversedhunks:
1509 1509 ... c.write(fp)
1510 1510 >>> fp.seek(0) or None
1511 1511 >>> reversedpatch = fp.read()
1512 1512 >>> print(pycompat.sysstr(reversedpatch))
1513 1513 diff --git a/folder1/g b/folder1/g
1514 1514 --- a/folder1/g
1515 1515 +++ b/folder1/g
1516 1516 @@ -1,4 +1,3 @@
1517 1517 -firstline
1518 1518 c
1519 1519 1
1520 1520 2
1521 1521 @@ -2,6 +1,6 @@
1522 1522 c
1523 1523 1
1524 1524 2
1525 1525 - 3
1526 1526 +4
1527 1527 5
1528 1528 d
1529 1529 @@ -6,3 +5,2 @@
1530 1530 5
1531 1531 d
1532 1532 -lastline
1533 1533
1534 1534 '''
1535 1535
1536 1536 newhunks = []
1537 1537 for c in hunks:
1538 1538 if util.safehasattr(c, 'reversehunk'):
1539 1539 c = c.reversehunk()
1540 1540 newhunks.append(c)
1541 1541 return newhunks
1542 1542
1543 1543 def parsepatch(originalchunks, maxcontext=None):
1544 1544 """patch -> [] of headers -> [] of hunks
1545 1545
1546 1546 If maxcontext is not None, trim context lines if necessary.
1547 1547
1548 1548 >>> rawpatch = b'''diff --git a/folder1/g b/folder1/g
1549 1549 ... --- a/folder1/g
1550 1550 ... +++ b/folder1/g
1551 1551 ... @@ -1,8 +1,10 @@
1552 1552 ... 1
1553 1553 ... 2
1554 1554 ... -3
1555 1555 ... 4
1556 1556 ... 5
1557 1557 ... 6
1558 1558 ... +6.1
1559 1559 ... +6.2
1560 1560 ... 7
1561 1561 ... 8
1562 1562 ... +9'''
1563 1563 >>> out = util.stringio()
1564 1564 >>> headers = parsepatch([rawpatch], maxcontext=1)
1565 1565 >>> for header in headers:
1566 1566 ... header.write(out)
1567 1567 ... for hunk in header.hunks:
1568 1568 ... hunk.write(out)
1569 1569 >>> print(pycompat.sysstr(out.getvalue()))
1570 1570 diff --git a/folder1/g b/folder1/g
1571 1571 --- a/folder1/g
1572 1572 +++ b/folder1/g
1573 1573 @@ -2,3 +2,2 @@
1574 1574 2
1575 1575 -3
1576 1576 4
1577 1577 @@ -6,2 +5,4 @@
1578 1578 6
1579 1579 +6.1
1580 1580 +6.2
1581 1581 7
1582 1582 @@ -8,1 +9,2 @@
1583 1583 8
1584 1584 +9
1585 1585 """
1586 1586 class parser(object):
1587 1587 """patch parsing state machine"""
1588 1588 def __init__(self):
1589 1589 self.fromline = 0
1590 1590 self.toline = 0
1591 1591 self.proc = ''
1592 1592 self.header = None
1593 1593 self.context = []
1594 1594 self.before = []
1595 1595 self.hunk = []
1596 1596 self.headers = []
1597 1597
1598 1598 def addrange(self, limits):
1599 1599 fromstart, fromend, tostart, toend, proc = limits
1600 1600 self.fromline = int(fromstart)
1601 1601 self.toline = int(tostart)
1602 1602 self.proc = proc
1603 1603
1604 1604 def addcontext(self, context):
1605 1605 if self.hunk:
1606 1606 h = recordhunk(self.header, self.fromline, self.toline,
1607 1607 self.proc, self.before, self.hunk, context, maxcontext)
1608 1608 self.header.hunks.append(h)
1609 1609 self.fromline += len(self.before) + h.removed
1610 1610 self.toline += len(self.before) + h.added
1611 1611 self.before = []
1612 1612 self.hunk = []
1613 1613 self.context = context
1614 1614
1615 1615 def addhunk(self, hunk):
1616 1616 if self.context:
1617 1617 self.before = self.context
1618 1618 self.context = []
1619 1619 self.hunk = hunk
1620 1620
1621 1621 def newfile(self, hdr):
1622 1622 self.addcontext([])
1623 1623 h = header(hdr)
1624 1624 self.headers.append(h)
1625 1625 self.header = h
1626 1626
1627 1627 def addother(self, line):
1628 1628 pass # 'other' lines are ignored
1629 1629
1630 1630 def finished(self):
1631 1631 self.addcontext([])
1632 1632 return self.headers
1633 1633
1634 1634 transitions = {
1635 1635 'file': {'context': addcontext,
1636 1636 'file': newfile,
1637 1637 'hunk': addhunk,
1638 1638 'range': addrange},
1639 1639 'context': {'file': newfile,
1640 1640 'hunk': addhunk,
1641 1641 'range': addrange,
1642 1642 'other': addother},
1643 1643 'hunk': {'context': addcontext,
1644 1644 'file': newfile,
1645 1645 'range': addrange},
1646 1646 'range': {'context': addcontext,
1647 1647 'hunk': addhunk},
1648 1648 'other': {'other': addother},
1649 1649 }
1650 1650
1651 1651 p = parser()
1652 1652 fp = stringio()
1653 1653 fp.write(''.join(originalchunks))
1654 1654 fp.seek(0)
1655 1655
1656 1656 state = 'context'
1657 1657 for newstate, data in scanpatch(fp):
1658 1658 try:
1659 1659 p.transitions[state][newstate](p, data)
1660 1660 except KeyError:
1661 1661 raise PatchError('unhandled transition: %s -> %s' %
1662 1662 (state, newstate))
1663 1663 state = newstate
1664 1664 del fp
1665 1665 return p.finished()
1666 1666
1667 1667 def pathtransform(path, strip, prefix):
1668 1668 '''turn a path from a patch into a path suitable for the repository
1669 1669
1670 1670 prefix, if not empty, is expected to be normalized with a / at the end.
1671 1671
1672 1672 Returns (stripped components, path in repository).
1673 1673
1674 1674 >>> pathtransform(b'a/b/c', 0, b'')
1675 1675 ('', 'a/b/c')
1676 1676 >>> pathtransform(b' a/b/c ', 0, b'')
1677 1677 ('', ' a/b/c')
1678 1678 >>> pathtransform(b' a/b/c ', 2, b'')
1679 1679 ('a/b/', 'c')
1680 1680 >>> pathtransform(b'a/b/c', 0, b'd/e/')
1681 1681 ('', 'd/e/a/b/c')
1682 1682 >>> pathtransform(b' a//b/c ', 2, b'd/e/')
1683 1683 ('a//b/', 'd/e/c')
1684 1684 >>> pathtransform(b'a/b/c', 3, b'')
1685 1685 Traceback (most recent call last):
1686 1686 PatchError: unable to strip away 1 of 3 dirs from a/b/c
1687 1687 '''
1688 1688 pathlen = len(path)
1689 1689 i = 0
1690 1690 if strip == 0:
1691 1691 return '', prefix + path.rstrip()
1692 1692 count = strip
1693 1693 while count > 0:
1694 1694 i = path.find('/', i)
1695 1695 if i == -1:
1696 1696 raise PatchError(_("unable to strip away %d of %d dirs from %s") %
1697 1697 (count, strip, path))
1698 1698 i += 1
1699 1699 # consume '//' in the path
1700 1700 while i < pathlen - 1 and path[i:i + 1] == '/':
1701 1701 i += 1
1702 1702 count -= 1
1703 1703 return path[:i].lstrip(), prefix + path[i:].rstrip()
1704 1704
1705 1705 def makepatchmeta(backend, afile_orig, bfile_orig, hunk, strip, prefix):
1706 1706 nulla = afile_orig == "/dev/null"
1707 1707 nullb = bfile_orig == "/dev/null"
1708 1708 create = nulla and hunk.starta == 0 and hunk.lena == 0
1709 1709 remove = nullb and hunk.startb == 0 and hunk.lenb == 0
1710 1710 abase, afile = pathtransform(afile_orig, strip, prefix)
1711 1711 gooda = not nulla and backend.exists(afile)
1712 1712 bbase, bfile = pathtransform(bfile_orig, strip, prefix)
1713 1713 if afile == bfile:
1714 1714 goodb = gooda
1715 1715 else:
1716 1716 goodb = not nullb and backend.exists(bfile)
1717 1717 missing = not goodb and not gooda and not create
1718 1718
1719 1719 # some diff programs apparently produce patches where the afile is
1720 1720 # not /dev/null, but afile starts with bfile
1721 1721 abasedir = afile[:afile.rfind('/') + 1]
1722 1722 bbasedir = bfile[:bfile.rfind('/') + 1]
1723 1723 if (missing and abasedir == bbasedir and afile.startswith(bfile)
1724 1724 and hunk.starta == 0 and hunk.lena == 0):
1725 1725 create = True
1726 1726 missing = False
1727 1727
1728 1728 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
1729 1729 # diff is between a file and its backup. In this case, the original
1730 1730 # file should be patched (see original mpatch code).
1731 1731 isbackup = (abase == bbase and bfile.startswith(afile))
1732 1732 fname = None
1733 1733 if not missing:
1734 1734 if gooda and goodb:
1735 1735 if isbackup:
1736 1736 fname = afile
1737 1737 else:
1738 1738 fname = bfile
1739 1739 elif gooda:
1740 1740 fname = afile
1741 1741
1742 1742 if not fname:
1743 1743 if not nullb:
1744 1744 if isbackup:
1745 1745 fname = afile
1746 1746 else:
1747 1747 fname = bfile
1748 1748 elif not nulla:
1749 1749 fname = afile
1750 1750 else:
1751 1751 raise PatchError(_("undefined source and destination files"))
1752 1752
1753 1753 gp = patchmeta(fname)
1754 1754 if create:
1755 1755 gp.op = 'ADD'
1756 1756 elif remove:
1757 1757 gp.op = 'DELETE'
1758 1758 return gp
1759 1759
1760 1760 def scanpatch(fp):
1761 1761 """like patch.iterhunks, but yield different events
1762 1762
1763 1763 - ('file', [header_lines + fromfile + tofile])
1764 1764 - ('context', [context_lines])
1765 1765 - ('hunk', [hunk_lines])
1766 1766 - ('range', (-start,len, +start,len, proc))
1767 1767 """
1768 1768 lines_re = re.compile(br'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)')
1769 1769 lr = linereader(fp)
1770 1770
1771 1771 def scanwhile(first, p):
1772 1772 """scan lr while predicate holds"""
1773 1773 lines = [first]
1774 1774 for line in iter(lr.readline, ''):
1775 1775 if p(line):
1776 1776 lines.append(line)
1777 1777 else:
1778 1778 lr.push(line)
1779 1779 break
1780 1780 return lines
1781 1781
1782 1782 for line in iter(lr.readline, ''):
1783 1783 if line.startswith('diff --git a/') or line.startswith('diff -r '):
1784 1784 def notheader(line):
1785 1785 s = line.split(None, 1)
1786 1786 return not s or s[0] not in ('---', 'diff')
1787 1787 header = scanwhile(line, notheader)
1788 1788 fromfile = lr.readline()
1789 1789 if fromfile.startswith('---'):
1790 1790 tofile = lr.readline()
1791 1791 header += [fromfile, tofile]
1792 1792 else:
1793 1793 lr.push(fromfile)
1794 1794 yield 'file', header
1795 1795 elif line[0:1] == ' ':
1796 1796 yield 'context', scanwhile(line, lambda l: l[0] in ' \\')
1797 1797 elif line[0] in '-+':
1798 1798 yield 'hunk', scanwhile(line, lambda l: l[0] in '-+\\')
1799 1799 else:
1800 1800 m = lines_re.match(line)
1801 1801 if m:
1802 1802 yield 'range', m.groups()
1803 1803 else:
1804 1804 yield 'other', line
1805 1805
1806 1806 def scangitpatch(lr, firstline):
1807 1807 """
1808 1808 Git patches can emit:
1809 1809 - rename a to b
1810 1810 - change b
1811 1811 - copy a to c
1812 1812 - change c
1813 1813
1814 1814 We cannot apply this sequence as-is, the renamed 'a' could not be
1815 1815 found for it would have been renamed already. And we cannot copy
1816 1816 from 'b' instead because 'b' would have been changed already. So
1817 1817 we scan the git patch for copy and rename commands so we can
1818 1818 perform the copies ahead of time.
1819 1819 """
1820 1820 pos = 0
1821 1821 try:
1822 1822 pos = lr.fp.tell()
1823 1823 fp = lr.fp
1824 1824 except IOError:
1825 1825 fp = stringio(lr.fp.read())
1826 1826 gitlr = linereader(fp)
1827 1827 gitlr.push(firstline)
1828 1828 gitpatches = readgitpatch(gitlr)
1829 1829 fp.seek(pos)
1830 1830 return gitpatches
1831 1831
1832 1832 def iterhunks(fp):
1833 1833 """Read a patch and yield the following events:
1834 1834 - ("file", afile, bfile, firsthunk): select a new target file.
1835 1835 - ("hunk", hunk): a new hunk is ready to be applied, follows a
1836 1836 "file" event.
1837 1837 - ("git", gitchanges): current diff is in git format, gitchanges
1838 1838 maps filenames to gitpatch records. Unique event.
1839 1839 """
1840 1840 afile = ""
1841 1841 bfile = ""
1842 1842 state = None
1843 1843 hunknum = 0
1844 1844 emitfile = newfile = False
1845 1845 gitpatches = None
1846 1846
1847 1847 # our states
1848 1848 BFILE = 1
1849 1849 context = None
1850 1850 lr = linereader(fp)
1851 1851
1852 1852 for x in iter(lr.readline, ''):
1853 1853 if state == BFILE and (
1854 1854 (not context and x[0] == '@')
1855 1855 or (context is not False and x.startswith('***************'))
1856 1856 or x.startswith('GIT binary patch')):
1857 1857 gp = None
1858 1858 if (gitpatches and
1859 1859 gitpatches[-1].ispatching(afile, bfile)):
1860 1860 gp = gitpatches.pop()
1861 1861 if x.startswith('GIT binary patch'):
1862 1862 h = binhunk(lr, gp.path)
1863 1863 else:
1864 1864 if context is None and x.startswith('***************'):
1865 1865 context = True
1866 1866 h = hunk(x, hunknum + 1, lr, context)
1867 1867 hunknum += 1
1868 1868 if emitfile:
1869 1869 emitfile = False
1870 1870 yield 'file', (afile, bfile, h, gp and gp.copy() or None)
1871 1871 yield 'hunk', h
1872 1872 elif x.startswith('diff --git a/'):
1873 1873 m = gitre.match(x.rstrip(' \r\n'))
1874 1874 if not m:
1875 1875 continue
1876 1876 if gitpatches is None:
1877 1877 # scan whole input for git metadata
1878 1878 gitpatches = scangitpatch(lr, x)
1879 1879 yield 'git', [g.copy() for g in gitpatches
1880 1880 if g.op in ('COPY', 'RENAME')]
1881 1881 gitpatches.reverse()
1882 1882 afile = 'a/' + m.group(1)
1883 1883 bfile = 'b/' + m.group(2)
1884 1884 while gitpatches and not gitpatches[-1].ispatching(afile, bfile):
1885 1885 gp = gitpatches.pop()
1886 1886 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1887 1887 if not gitpatches:
1888 1888 raise PatchError(_('failed to synchronize metadata for "%s"')
1889 1889 % afile[2:])
1890 1890 gp = gitpatches[-1]
1891 1891 newfile = True
1892 1892 elif x.startswith('---'):
1893 1893 # check for a unified diff
1894 1894 l2 = lr.readline()
1895 1895 if not l2.startswith('+++'):
1896 1896 lr.push(l2)
1897 1897 continue
1898 1898 newfile = True
1899 1899 context = False
1900 1900 afile = parsefilename(x)
1901 1901 bfile = parsefilename(l2)
1902 1902 elif x.startswith('***'):
1903 1903 # check for a context diff
1904 1904 l2 = lr.readline()
1905 1905 if not l2.startswith('---'):
1906 1906 lr.push(l2)
1907 1907 continue
1908 1908 l3 = lr.readline()
1909 1909 lr.push(l3)
1910 1910 if not l3.startswith("***************"):
1911 1911 lr.push(l2)
1912 1912 continue
1913 1913 newfile = True
1914 1914 context = True
1915 1915 afile = parsefilename(x)
1916 1916 bfile = parsefilename(l2)
1917 1917
1918 1918 if newfile:
1919 1919 newfile = False
1920 1920 emitfile = True
1921 1921 state = BFILE
1922 1922 hunknum = 0
1923 1923
1924 1924 while gitpatches:
1925 1925 gp = gitpatches.pop()
1926 1926 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1927 1927
1928 1928 def applybindelta(binchunk, data):
1929 1929 """Apply a binary delta hunk
1930 1930 The algorithm used is the algorithm from git's patch-delta.c
1931 1931 """
1932 1932 def deltahead(binchunk):
1933 1933 i = 0
1934 1934 for c in binchunk:
1935 1935 i += 1
1936 1936 if not (ord(c) & 0x80):
1937 1937 return i
1938 1938 return i
1939 1939 out = ""
1940 1940 s = deltahead(binchunk)
1941 1941 binchunk = binchunk[s:]
1942 1942 s = deltahead(binchunk)
1943 1943 binchunk = binchunk[s:]
1944 1944 i = 0
1945 1945 while i < len(binchunk):
1946 1946 cmd = ord(binchunk[i])
1947 1947 i += 1
1948 1948 if (cmd & 0x80):
1949 1949 offset = 0
1950 1950 size = 0
1951 1951 if (cmd & 0x01):
1952 1952 offset = ord(binchunk[i])
1953 1953 i += 1
1954 1954 if (cmd & 0x02):
1955 1955 offset |= ord(binchunk[i]) << 8
1956 1956 i += 1
1957 1957 if (cmd & 0x04):
1958 1958 offset |= ord(binchunk[i]) << 16
1959 1959 i += 1
1960 1960 if (cmd & 0x08):
1961 1961 offset |= ord(binchunk[i]) << 24
1962 1962 i += 1
1963 1963 if (cmd & 0x10):
1964 1964 size = ord(binchunk[i])
1965 1965 i += 1
1966 1966 if (cmd & 0x20):
1967 1967 size |= ord(binchunk[i]) << 8
1968 1968 i += 1
1969 1969 if (cmd & 0x40):
1970 1970 size |= ord(binchunk[i]) << 16
1971 1971 i += 1
1972 1972 if size == 0:
1973 1973 size = 0x10000
1974 1974 offset_end = offset + size
1975 1975 out += data[offset:offset_end]
1976 1976 elif cmd != 0:
1977 1977 offset_end = i + cmd
1978 1978 out += binchunk[i:offset_end]
1979 1979 i += cmd
1980 1980 else:
1981 1981 raise PatchError(_('unexpected delta opcode 0'))
1982 1982 return out
1983 1983
1984 1984 def applydiff(ui, fp, backend, store, strip=1, prefix='', eolmode='strict'):
1985 1985 """Reads a patch from fp and tries to apply it.
1986 1986
1987 1987 Returns 0 for a clean patch, -1 if any rejects were found and 1 if
1988 1988 there was any fuzz.
1989 1989
1990 1990 If 'eolmode' is 'strict', the patch content and patched file are
1991 1991 read in binary mode. Otherwise, line endings are ignored when
1992 1992 patching then normalized according to 'eolmode'.
1993 1993 """
1994 1994 return _applydiff(ui, fp, patchfile, backend, store, strip=strip,
1995 1995 prefix=prefix, eolmode=eolmode)
1996 1996
1997 1997 def _canonprefix(repo, prefix):
1998 1998 if prefix:
1999 1999 prefix = pathutil.canonpath(repo.root, repo.getcwd(), prefix)
2000 2000 if prefix != '':
2001 2001 prefix += '/'
2002 2002 return prefix
2003 2003
2004 2004 def _applydiff(ui, fp, patcher, backend, store, strip=1, prefix='',
2005 2005 eolmode='strict'):
2006 2006 prefix = _canonprefix(backend.repo, prefix)
2007 2007 def pstrip(p):
2008 2008 return pathtransform(p, strip - 1, prefix)[1]
2009 2009
2010 2010 rejects = 0
2011 2011 err = 0
2012 2012 current_file = None
2013 2013
2014 2014 for state, values in iterhunks(fp):
2015 2015 if state == 'hunk':
2016 2016 if not current_file:
2017 2017 continue
2018 2018 ret = current_file.apply(values)
2019 2019 if ret > 0:
2020 2020 err = 1
2021 2021 elif state == 'file':
2022 2022 if current_file:
2023 2023 rejects += current_file.close()
2024 2024 current_file = None
2025 2025 afile, bfile, first_hunk, gp = values
2026 2026 if gp:
2027 2027 gp.path = pstrip(gp.path)
2028 2028 if gp.oldpath:
2029 2029 gp.oldpath = pstrip(gp.oldpath)
2030 2030 else:
2031 2031 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip,
2032 2032 prefix)
2033 2033 if gp.op == 'RENAME':
2034 2034 backend.unlink(gp.oldpath)
2035 2035 if not first_hunk:
2036 2036 if gp.op == 'DELETE':
2037 2037 backend.unlink(gp.path)
2038 2038 continue
2039 2039 data, mode = None, None
2040 2040 if gp.op in ('RENAME', 'COPY'):
2041 2041 data, mode = store.getfile(gp.oldpath)[:2]
2042 2042 if data is None:
2043 2043 # This means that the old path does not exist
2044 2044 raise PatchError(_("source file '%s' does not exist")
2045 2045 % gp.oldpath)
2046 2046 if gp.mode:
2047 2047 mode = gp.mode
2048 2048 if gp.op == 'ADD':
2049 2049 # Added files without content have no hunk and
2050 2050 # must be created
2051 2051 data = ''
2052 2052 if data or mode:
2053 2053 if (gp.op in ('ADD', 'RENAME', 'COPY')
2054 2054 and backend.exists(gp.path)):
2055 2055 raise PatchError(_("cannot create %s: destination "
2056 2056 "already exists") % gp.path)
2057 2057 backend.setfile(gp.path, data, mode, gp.oldpath)
2058 2058 continue
2059 2059 try:
2060 2060 current_file = patcher(ui, gp, backend, store,
2061 2061 eolmode=eolmode)
2062 2062 except PatchError as inst:
2063 2063 ui.warn(str(inst) + '\n')
2064 2064 current_file = None
2065 2065 rejects += 1
2066 2066 continue
2067 2067 elif state == 'git':
2068 2068 for gp in values:
2069 2069 path = pstrip(gp.oldpath)
2070 2070 data, mode = backend.getfile(path)
2071 2071 if data is None:
2072 2072 # The error ignored here will trigger a getfile()
2073 2073 # error in a place more appropriate for error
2074 2074 # handling, and will not interrupt the patching
2075 2075 # process.
2076 2076 pass
2077 2077 else:
2078 2078 store.setfile(path, data, mode)
2079 2079 else:
2080 2080 raise error.Abort(_('unsupported parser state: %s') % state)
2081 2081
2082 2082 if current_file:
2083 2083 rejects += current_file.close()
2084 2084
2085 2085 if rejects:
2086 2086 return -1
2087 2087 return err
2088 2088
2089 2089 def _externalpatch(ui, repo, patcher, patchname, strip, files,
2090 2090 similarity):
2091 2091 """use <patcher> to apply <patchname> to the working directory.
2092 2092 returns whether patch was applied with fuzz factor."""
2093 2093
2094 2094 fuzz = False
2095 2095 args = []
2096 2096 cwd = repo.root
2097 2097 if cwd:
2098 2098 args.append('-d %s' % util.shellquote(cwd))
2099 2099 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
2100 2100 util.shellquote(patchname)))
2101 2101 try:
2102 2102 for line in util.iterfile(fp):
2103 2103 line = line.rstrip()
2104 2104 ui.note(line + '\n')
2105 2105 if line.startswith('patching file '):
2106 2106 pf = util.parsepatchoutput(line)
2107 2107 printed_file = False
2108 2108 files.add(pf)
2109 2109 elif line.find('with fuzz') >= 0:
2110 2110 fuzz = True
2111 2111 if not printed_file:
2112 2112 ui.warn(pf + '\n')
2113 2113 printed_file = True
2114 2114 ui.warn(line + '\n')
2115 2115 elif line.find('saving rejects to file') >= 0:
2116 2116 ui.warn(line + '\n')
2117 2117 elif line.find('FAILED') >= 0:
2118 2118 if not printed_file:
2119 2119 ui.warn(pf + '\n')
2120 2120 printed_file = True
2121 2121 ui.warn(line + '\n')
2122 2122 finally:
2123 2123 if files:
2124 2124 scmutil.marktouched(repo, files, similarity)
2125 2125 code = fp.close()
2126 2126 if code:
2127 2127 raise PatchError(_("patch command failed: %s") %
2128 2128 util.explainexit(code)[0])
2129 2129 return fuzz
2130 2130
2131 2131 def patchbackend(ui, backend, patchobj, strip, prefix, files=None,
2132 2132 eolmode='strict'):
2133 2133 if files is None:
2134 2134 files = set()
2135 2135 if eolmode is None:
2136 2136 eolmode = ui.config('patch', 'eol')
2137 2137 if eolmode.lower() not in eolmodes:
2138 2138 raise error.Abort(_('unsupported line endings type: %s') % eolmode)
2139 2139 eolmode = eolmode.lower()
2140 2140
2141 2141 store = filestore()
2142 2142 try:
2143 2143 fp = open(patchobj, 'rb')
2144 2144 except TypeError:
2145 2145 fp = patchobj
2146 2146 try:
2147 2147 ret = applydiff(ui, fp, backend, store, strip=strip, prefix=prefix,
2148 2148 eolmode=eolmode)
2149 2149 finally:
2150 2150 if fp != patchobj:
2151 2151 fp.close()
2152 2152 files.update(backend.close())
2153 2153 store.close()
2154 2154 if ret < 0:
2155 2155 raise PatchError(_('patch failed to apply'))
2156 2156 return ret > 0
2157 2157
2158 2158 def internalpatch(ui, repo, patchobj, strip, prefix='', files=None,
2159 2159 eolmode='strict', similarity=0):
2160 2160 """use builtin patch to apply <patchobj> to the working directory.
2161 2161 returns whether patch was applied with fuzz factor."""
2162 2162 backend = workingbackend(ui, repo, similarity)
2163 2163 return patchbackend(ui, backend, patchobj, strip, prefix, files, eolmode)
2164 2164
2165 2165 def patchrepo(ui, repo, ctx, store, patchobj, strip, prefix, files=None,
2166 2166 eolmode='strict'):
2167 2167 backend = repobackend(ui, repo, ctx, store)
2168 2168 return patchbackend(ui, backend, patchobj, strip, prefix, files, eolmode)
2169 2169
2170 2170 def patch(ui, repo, patchname, strip=1, prefix='', files=None, eolmode='strict',
2171 2171 similarity=0):
2172 2172 """Apply <patchname> to the working directory.
2173 2173
2174 2174 'eolmode' specifies how end of lines should be handled. It can be:
2175 2175 - 'strict': inputs are read in binary mode, EOLs are preserved
2176 2176 - 'crlf': EOLs are ignored when patching and reset to CRLF
2177 2177 - 'lf': EOLs are ignored when patching and reset to LF
2178 2178 - None: get it from user settings, default to 'strict'
2179 2179 'eolmode' is ignored when using an external patcher program.
2180 2180
2181 2181 Returns whether patch was applied with fuzz factor.
2182 2182 """
2183 2183 patcher = ui.config('ui', 'patch')
2184 2184 if files is None:
2185 2185 files = set()
2186 2186 if patcher:
2187 2187 return _externalpatch(ui, repo, patcher, patchname, strip,
2188 2188 files, similarity)
2189 2189 return internalpatch(ui, repo, patchname, strip, prefix, files, eolmode,
2190 2190 similarity)
2191 2191
2192 2192 def changedfiles(ui, repo, patchpath, strip=1, prefix=''):
2193 2193 backend = fsbackend(ui, repo.root)
2194 2194 prefix = _canonprefix(repo, prefix)
2195 2195 with open(patchpath, 'rb') as fp:
2196 2196 changed = set()
2197 2197 for state, values in iterhunks(fp):
2198 2198 if state == 'file':
2199 2199 afile, bfile, first_hunk, gp = values
2200 2200 if gp:
2201 2201 gp.path = pathtransform(gp.path, strip - 1, prefix)[1]
2202 2202 if gp.oldpath:
2203 2203 gp.oldpath = pathtransform(gp.oldpath, strip - 1,
2204 2204 prefix)[1]
2205 2205 else:
2206 2206 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip,
2207 2207 prefix)
2208 2208 changed.add(gp.path)
2209 2209 if gp.op == 'RENAME':
2210 2210 changed.add(gp.oldpath)
2211 2211 elif state not in ('hunk', 'git'):
2212 2212 raise error.Abort(_('unsupported parser state: %s') % state)
2213 2213 return changed
2214 2214
2215 2215 class GitDiffRequired(Exception):
2216 2216 pass
2217 2217
2218 2218 def diffallopts(ui, opts=None, untrusted=False, section='diff'):
2219 2219 '''return diffopts with all features supported and parsed'''
2220 2220 return difffeatureopts(ui, opts=opts, untrusted=untrusted, section=section,
2221 2221 git=True, whitespace=True, formatchanging=True)
2222 2222
2223 2223 diffopts = diffallopts
2224 2224
2225 2225 def difffeatureopts(ui, opts=None, untrusted=False, section='diff', git=False,
2226 2226 whitespace=False, formatchanging=False):
2227 2227 '''return diffopts with only opted-in features parsed
2228 2228
2229 2229 Features:
2230 2230 - git: git-style diffs
2231 2231 - whitespace: whitespace options like ignoreblanklines and ignorews
2232 2232 - formatchanging: options that will likely break or cause correctness issues
2233 2233 with most diff parsers
2234 2234 '''
2235 2235 def get(key, name=None, getter=ui.configbool, forceplain=None):
2236 2236 if opts:
2237 2237 v = opts.get(key)
2238 2238 # diffopts flags are either None-default (which is passed
2239 2239 # through unchanged, so we can identify unset values), or
2240 2240 # some other falsey default (eg --unified, which defaults
2241 2241 # to an empty string). We only want to override the config
2242 2242 # entries from hgrc with command line values if they
2243 2243 # appear to have been set, which is any truthy value,
2244 2244 # True, or False.
2245 2245 if v or isinstance(v, bool):
2246 2246 return v
2247 2247 if forceplain is not None and ui.plain():
2248 2248 return forceplain
2249 2249 return getter(section, name or key, untrusted=untrusted)
2250 2250
2251 2251 # core options, expected to be understood by every diff parser
2252 2252 buildopts = {
2253 2253 'nodates': get('nodates'),
2254 2254 'showfunc': get('show_function', 'showfunc'),
2255 2255 'context': get('unified', getter=ui.config),
2256 2256 }
2257 2257 buildopts['worddiff'] = ui.configbool('experimental', 'worddiff')
2258 2258
2259 2259 if git:
2260 2260 buildopts['git'] = get('git')
2261 2261
2262 2262 # since this is in the experimental section, we need to call
2263 2263 # ui.configbool directory
2264 2264 buildopts['showsimilarity'] = ui.configbool('experimental',
2265 2265 'extendedheader.similarity')
2266 2266
2267 2267 # need to inspect the ui object instead of using get() since we want to
2268 2268 # test for an int
2269 2269 hconf = ui.config('experimental', 'extendedheader.index')
2270 2270 if hconf is not None:
2271 2271 hlen = None
2272 2272 try:
2273 2273 # the hash config could be an integer (for length of hash) or a
2274 2274 # word (e.g. short, full, none)
2275 2275 hlen = int(hconf)
2276 2276 if hlen < 0 or hlen > 40:
2277 2277 msg = _("invalid length for extendedheader.index: '%d'\n")
2278 2278 ui.warn(msg % hlen)
2279 2279 except ValueError:
2280 2280 # default value
2281 2281 if hconf == 'short' or hconf == '':
2282 2282 hlen = 12
2283 2283 elif hconf == 'full':
2284 2284 hlen = 40
2285 2285 elif hconf != 'none':
2286 2286 msg = _("invalid value for extendedheader.index: '%s'\n")
2287 2287 ui.warn(msg % hconf)
2288 2288 finally:
2289 2289 buildopts['index'] = hlen
2290 2290
2291 2291 if whitespace:
2292 2292 buildopts['ignorews'] = get('ignore_all_space', 'ignorews')
2293 2293 buildopts['ignorewsamount'] = get('ignore_space_change',
2294 2294 'ignorewsamount')
2295 2295 buildopts['ignoreblanklines'] = get('ignore_blank_lines',
2296 2296 'ignoreblanklines')
2297 2297 buildopts['ignorewseol'] = get('ignore_space_at_eol', 'ignorewseol')
2298 2298 if formatchanging:
2299 2299 buildopts['text'] = opts and opts.get('text')
2300 2300 binary = None if opts is None else opts.get('binary')
2301 2301 buildopts['nobinary'] = (not binary if binary is not None
2302 2302 else get('nobinary', forceplain=False))
2303 2303 buildopts['noprefix'] = get('noprefix', forceplain=False)
2304 2304
2305 2305 return mdiff.diffopts(**pycompat.strkwargs(buildopts))
2306 2306
2307 2307 def diff(repo, node1=None, node2=None, match=None, changes=None,
2308 2308 opts=None, losedatafn=None, prefix='', relroot='', copy=None,
2309 2309 hunksfilterfn=None):
2310 2310 '''yields diff of changes to files between two nodes, or node and
2311 2311 working directory.
2312 2312
2313 2313 if node1 is None, use first dirstate parent instead.
2314 2314 if node2 is None, compare node1 with working directory.
2315 2315
2316 2316 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
2317 2317 every time some change cannot be represented with the current
2318 2318 patch format. Return False to upgrade to git patch format, True to
2319 2319 accept the loss or raise an exception to abort the diff. It is
2320 2320 called with the name of current file being diffed as 'fn'. If set
2321 2321 to None, patches will always be upgraded to git format when
2322 2322 necessary.
2323 2323
2324 2324 prefix is a filename prefix that is prepended to all filenames on
2325 2325 display (used for subrepos).
2326 2326
2327 2327 relroot, if not empty, must be normalized with a trailing /. Any match
2328 2328 patterns that fall outside it will be ignored.
2329 2329
2330 2330 copy, if not empty, should contain mappings {dst@y: src@x} of copy
2331 2331 information.
2332 2332
2333 2333 hunksfilterfn, if not None, should be a function taking a filectx and
2334 2334 hunks generator that may yield filtered hunks.
2335 2335 '''
2336 2336 for fctx1, fctx2, hdr, hunks in diffhunks(
2337 2337 repo, node1=node1, node2=node2,
2338 2338 match=match, changes=changes, opts=opts,
2339 2339 losedatafn=losedatafn, prefix=prefix, relroot=relroot, copy=copy,
2340 2340 ):
2341 2341 if hunksfilterfn is not None:
2342 2342 # If the file has been removed, fctx2 is None; but this should
2343 2343 # not occur here since we catch removed files early in
2344 2344 # logcmdutil.getlinerangerevs() for 'hg log -L'.
2345 2345 assert fctx2 is not None, \
2346 2346 'fctx2 unexpectly None in diff hunks filtering'
2347 2347 hunks = hunksfilterfn(fctx2, hunks)
2348 2348 text = ''.join(sum((list(hlines) for hrange, hlines in hunks), []))
2349 2349 if hdr and (text or len(hdr) > 1):
2350 2350 yield '\n'.join(hdr) + '\n'
2351 2351 if text:
2352 2352 yield text
2353 2353
2354 2354 def diffhunks(repo, node1=None, node2=None, match=None, changes=None,
2355 2355 opts=None, losedatafn=None, prefix='', relroot='', copy=None):
2356 2356 """Yield diff of changes to files in the form of (`header`, `hunks`) tuples
2357 2357 where `header` is a list of diff headers and `hunks` is an iterable of
2358 2358 (`hunkrange`, `hunklines`) tuples.
2359 2359
2360 2360 See diff() for the meaning of parameters.
2361 2361 """
2362 2362
2363 2363 if opts is None:
2364 2364 opts = mdiff.defaultopts
2365 2365
2366 2366 if not node1 and not node2:
2367 2367 node1 = repo.dirstate.p1()
2368 2368
2369 2369 def lrugetfilectx():
2370 2370 cache = {}
2371 2371 order = collections.deque()
2372 2372 def getfilectx(f, ctx):
2373 2373 fctx = ctx.filectx(f, filelog=cache.get(f))
2374 2374 if f not in cache:
2375 2375 if len(cache) > 20:
2376 2376 del cache[order.popleft()]
2377 2377 cache[f] = fctx.filelog()
2378 2378 else:
2379 2379 order.remove(f)
2380 2380 order.append(f)
2381 2381 return fctx
2382 2382 return getfilectx
2383 2383 getfilectx = lrugetfilectx()
2384 2384
2385 2385 ctx1 = repo[node1]
2386 2386 ctx2 = repo[node2]
2387 2387
2388 2388 relfiltered = False
2389 2389 if relroot != '' and match.always():
2390 2390 # as a special case, create a new matcher with just the relroot
2391 2391 pats = [relroot]
2392 2392 match = scmutil.match(ctx2, pats, default='path')
2393 2393 relfiltered = True
2394 2394
2395 2395 if not changes:
2396 2396 changes = repo.status(ctx1, ctx2, match=match)
2397 2397 modified, added, removed = changes[:3]
2398 2398
2399 2399 if not modified and not added and not removed:
2400 2400 return []
2401 2401
2402 2402 if repo.ui.debugflag:
2403 2403 hexfunc = hex
2404 2404 else:
2405 2405 hexfunc = short
2406 2406 revs = [hexfunc(node) for node in [ctx1.node(), ctx2.node()] if node]
2407 2407
2408 2408 if copy is None:
2409 2409 copy = {}
2410 2410 if opts.git or opts.upgrade:
2411 2411 copy = copies.pathcopies(ctx1, ctx2, match=match)
2412 2412
2413 2413 if relroot is not None:
2414 2414 if not relfiltered:
2415 2415 # XXX this would ideally be done in the matcher, but that is
2416 2416 # generally meant to 'or' patterns, not 'and' them. In this case we
2417 2417 # need to 'and' all the patterns from the matcher with relroot.
2418 2418 def filterrel(l):
2419 2419 return [f for f in l if f.startswith(relroot)]
2420 2420 modified = filterrel(modified)
2421 2421 added = filterrel(added)
2422 2422 removed = filterrel(removed)
2423 2423 relfiltered = True
2424 2424 # filter out copies where either side isn't inside the relative root
2425 2425 copy = dict(((dst, src) for (dst, src) in copy.iteritems()
2426 2426 if dst.startswith(relroot)
2427 2427 and src.startswith(relroot)))
2428 2428
2429 2429 modifiedset = set(modified)
2430 2430 addedset = set(added)
2431 2431 removedset = set(removed)
2432 2432 for f in modified:
2433 2433 if f not in ctx1:
2434 2434 # Fix up added, since merged-in additions appear as
2435 2435 # modifications during merges
2436 2436 modifiedset.remove(f)
2437 2437 addedset.add(f)
2438 2438 for f in removed:
2439 2439 if f not in ctx1:
2440 2440 # Merged-in additions that are then removed are reported as removed.
2441 2441 # They are not in ctx1, so We don't want to show them in the diff.
2442 2442 removedset.remove(f)
2443 2443 modified = sorted(modifiedset)
2444 2444 added = sorted(addedset)
2445 2445 removed = sorted(removedset)
2446 2446 for dst, src in list(copy.items()):
2447 2447 if src not in ctx1:
2448 2448 # Files merged in during a merge and then copied/renamed are
2449 2449 # reported as copies. We want to show them in the diff as additions.
2450 2450 del copy[dst]
2451 2451
2452 2452 def difffn(opts, losedata):
2453 2453 return trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
2454 2454 copy, getfilectx, opts, losedata, prefix, relroot)
2455 2455 if opts.upgrade and not opts.git:
2456 2456 try:
2457 2457 def losedata(fn):
2458 2458 if not losedatafn or not losedatafn(fn=fn):
2459 2459 raise GitDiffRequired
2460 2460 # Buffer the whole output until we are sure it can be generated
2461 2461 return list(difffn(opts.copy(git=False), losedata))
2462 2462 except GitDiffRequired:
2463 2463 return difffn(opts.copy(git=True), None)
2464 2464 else:
2465 2465 return difffn(opts, None)
2466 2466
2467 2467 def difflabel(func, *args, **kw):
2468 2468 '''yields 2-tuples of (output, label) based on the output of func()'''
2469 2469 inlinecolor = False
2470 2470 if kw.get(r'opts'):
2471 2471 inlinecolor = kw[r'opts'].worddiff
2472 2472 headprefixes = [('diff', 'diff.diffline'),
2473 2473 ('copy', 'diff.extended'),
2474 2474 ('rename', 'diff.extended'),
2475 2475 ('old', 'diff.extended'),
2476 2476 ('new', 'diff.extended'),
2477 2477 ('deleted', 'diff.extended'),
2478 2478 ('index', 'diff.extended'),
2479 2479 ('similarity', 'diff.extended'),
2480 2480 ('---', 'diff.file_a'),
2481 2481 ('+++', 'diff.file_b')]
2482 2482 textprefixes = [('@', 'diff.hunk'),
2483 2483 ('-', 'diff.deleted'),
2484 2484 ('+', 'diff.inserted')]
2485 2485 head = False
2486 2486 for chunk in func(*args, **kw):
2487 2487 lines = chunk.split('\n')
2488 2488 matches = {}
2489 2489 if inlinecolor:
2490 2490 matches = _findmatches(lines)
2491 2491 for i, line in enumerate(lines):
2492 2492 if i != 0:
2493 2493 yield ('\n', '')
2494 2494 if head:
2495 2495 if line.startswith('@'):
2496 2496 head = False
2497 2497 else:
2498 2498 if line and line[0] not in ' +-@\\':
2499 2499 head = True
2500 2500 stripline = line
2501 2501 diffline = False
2502 2502 if not head and line and line[0] in '+-':
2503 2503 # highlight tabs and trailing whitespace, but only in
2504 2504 # changed lines
2505 2505 stripline = line.rstrip()
2506 2506 diffline = True
2507 2507
2508 2508 prefixes = textprefixes
2509 2509 if head:
2510 2510 prefixes = headprefixes
2511 2511 for prefix, label in prefixes:
2512 2512 if stripline.startswith(prefix):
2513 2513 if diffline:
2514 2514 if i in matches:
2515 2515 for t, l in _inlinediff(lines[i].rstrip(),
2516 2516 lines[matches[i]].rstrip(),
2517 2517 label):
2518 2518 yield (t, l)
2519 2519 else:
2520 2520 for token in tabsplitter.findall(stripline):
2521 2521 if '\t' == token[0]:
2522 2522 yield (token, 'diff.tab')
2523 2523 else:
2524 2524 yield (token, label)
2525 2525 else:
2526 2526 yield (stripline, label)
2527 2527 break
2528 2528 else:
2529 2529 yield (line, '')
2530 2530 if line != stripline:
2531 2531 yield (line[len(stripline):], 'diff.trailingwhitespace')
2532 2532
2533 2533 def _findmatches(slist):
2534 2534 '''Look for insertion matches to deletion and returns a dict of
2535 2535 correspondences.
2536 2536 '''
2537 2537 lastmatch = 0
2538 2538 matches = {}
2539 2539 for i, line in enumerate(slist):
2540 2540 if line == '':
2541 2541 continue
2542 2542 if line[0] == '-':
2543 2543 lastmatch = max(lastmatch, i)
2544 2544 newgroup = False
2545 2545 for j, newline in enumerate(slist[lastmatch + 1:]):
2546 2546 if newline == '':
2547 2547 continue
2548 2548 if newline[0] == '-' and newgroup: # too far, no match
2549 2549 break
2550 2550 if newline[0] == '+': # potential match
2551 2551 newgroup = True
2552 2552 sim = difflib.SequenceMatcher(None, line, newline).ratio()
2553 2553 if sim > 0.7:
2554 2554 lastmatch = lastmatch + 1 + j
2555 2555 matches[i] = lastmatch
2556 2556 matches[lastmatch] = i
2557 2557 break
2558 2558 return matches
2559 2559
2560 2560 def _inlinediff(s1, s2, operation):
2561 2561 '''Perform string diff to highlight specific changes.'''
2562 2562 operation_skip = '+?' if operation == 'diff.deleted' else '-?'
2563 2563 if operation == 'diff.deleted':
2564 2564 s2, s1 = s1, s2
2565 2565
2566 2566 buff = []
2567 2567 # we never want to higlight the leading +-
2568 2568 if operation == 'diff.deleted' and s2.startswith('-'):
2569 2569 label = operation
2570 2570 token = '-'
2571 2571 s2 = s2[1:]
2572 2572 s1 = s1[1:]
2573 2573 elif operation == 'diff.inserted' and s1.startswith('+'):
2574 2574 label = operation
2575 2575 token = '+'
2576 2576 s2 = s2[1:]
2577 2577 s1 = s1[1:]
2578 2578 else:
2579 2579 raise error.ProgrammingError("Case not expected, operation = %s" %
2580 2580 operation)
2581 2581
2582 2582 s = difflib.ndiff(_nonwordre.split(s2), _nonwordre.split(s1))
2583 2583 for part in s:
2584 2584 if part[0] in operation_skip or len(part) == 2:
2585 2585 continue
2586 2586 l = operation + '.highlight'
2587 2587 if part[0] in ' ':
2588 2588 l = operation
2589 2589 if part[2:] == '\t':
2590 2590 l = 'diff.tab'
2591 2591 if l == label: # contiguous token with same label
2592 2592 token += part[2:]
2593 2593 continue
2594 2594 else:
2595 2595 buff.append((token, label))
2596 2596 label = l
2597 2597 token = part[2:]
2598 2598 buff.append((token, label))
2599 2599
2600 2600 return buff
2601 2601
2602 2602 def diffui(*args, **kw):
2603 2603 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
2604 2604 return difflabel(diff, *args, **kw)
2605 2605
2606 2606 def _filepairs(modified, added, removed, copy, opts):
2607 2607 '''generates tuples (f1, f2, copyop), where f1 is the name of the file
2608 2608 before and f2 is the the name after. For added files, f1 will be None,
2609 2609 and for removed files, f2 will be None. copyop may be set to None, 'copy'
2610 2610 or 'rename' (the latter two only if opts.git is set).'''
2611 2611 gone = set()
2612 2612
2613 2613 copyto = dict([(v, k) for k, v in copy.items()])
2614 2614
2615 2615 addedset, removedset = set(added), set(removed)
2616 2616
2617 2617 for f in sorted(modified + added + removed):
2618 2618 copyop = None
2619 2619 f1, f2 = f, f
2620 2620 if f in addedset:
2621 2621 f1 = None
2622 2622 if f in copy:
2623 2623 if opts.git:
2624 2624 f1 = copy[f]
2625 2625 if f1 in removedset and f1 not in gone:
2626 2626 copyop = 'rename'
2627 2627 gone.add(f1)
2628 2628 else:
2629 2629 copyop = 'copy'
2630 2630 elif f in removedset:
2631 2631 f2 = None
2632 2632 if opts.git:
2633 2633 # have we already reported a copy above?
2634 2634 if (f in copyto and copyto[f] in addedset
2635 2635 and copy[copyto[f]] == f):
2636 2636 continue
2637 2637 yield f1, f2, copyop
2638 2638
2639 2639 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
2640 2640 copy, getfilectx, opts, losedatafn, prefix, relroot):
2641 2641 '''given input data, generate a diff and yield it in blocks
2642 2642
2643 2643 If generating a diff would lose data like flags or binary data and
2644 2644 losedatafn is not None, it will be called.
2645 2645
2646 2646 relroot is removed and prefix is added to every path in the diff output.
2647 2647
2648 2648 If relroot is not empty, this function expects every path in modified,
2649 2649 added, removed and copy to start with it.'''
2650 2650
2651 2651 def gitindex(text):
2652 2652 if not text:
2653 2653 text = ""
2654 2654 l = len(text)
2655 2655 s = hashlib.sha1('blob %d\0' % l)
2656 2656 s.update(text)
2657 2657 return hex(s.digest())
2658 2658
2659 2659 if opts.noprefix:
2660 2660 aprefix = bprefix = ''
2661 2661 else:
2662 2662 aprefix = 'a/'
2663 2663 bprefix = 'b/'
2664 2664
2665 2665 def diffline(f, revs):
2666 2666 revinfo = ' '.join(["-r %s" % rev for rev in revs])
2667 2667 return 'diff %s %s' % (revinfo, f)
2668 2668
2669 2669 def isempty(fctx):
2670 2670 return fctx is None or fctx.size() == 0
2671 2671
2672 2672 date1 = util.datestr(ctx1.date())
2673 2673 date2 = util.datestr(ctx2.date())
2674 2674
2675 2675 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
2676 2676
2677 2677 if relroot != '' and (repo.ui.configbool('devel', 'all-warnings')
2678 2678 or repo.ui.configbool('devel', 'check-relroot')):
2679 2679 for f in modified + added + removed + list(copy) + list(copy.values()):
2680 2680 if f is not None and not f.startswith(relroot):
2681 2681 raise AssertionError(
2682 2682 "file %s doesn't start with relroot %s" % (f, relroot))
2683 2683
2684 2684 for f1, f2, copyop in _filepairs(modified, added, removed, copy, opts):
2685 2685 content1 = None
2686 2686 content2 = None
2687 2687 fctx1 = None
2688 2688 fctx2 = None
2689 2689 flag1 = None
2690 2690 flag2 = None
2691 2691 if f1:
2692 2692 fctx1 = getfilectx(f1, ctx1)
2693 2693 if opts.git or losedatafn:
2694 2694 flag1 = ctx1.flags(f1)
2695 2695 if f2:
2696 2696 fctx2 = getfilectx(f2, ctx2)
2697 2697 if opts.git or losedatafn:
2698 2698 flag2 = ctx2.flags(f2)
2699 2699 # if binary is True, output "summary" or "base85", but not "text diff"
2700 2700 if opts.text:
2701 2701 binary = False
2702 2702 else:
2703 2703 binary = any(f.isbinary() for f in [fctx1, fctx2] if f is not None)
2704 2704
2705 2705 if losedatafn and not opts.git:
2706 2706 if (binary or
2707 2707 # copy/rename
2708 2708 f2 in copy or
2709 2709 # empty file creation
2710 2710 (not f1 and isempty(fctx2)) or
2711 2711 # empty file deletion
2712 2712 (isempty(fctx1) and not f2) or
2713 2713 # create with flags
2714 2714 (not f1 and flag2) or
2715 2715 # change flags
2716 2716 (f1 and f2 and flag1 != flag2)):
2717 2717 losedatafn(f2 or f1)
2718 2718
2719 2719 path1 = f1 or f2
2720 2720 path2 = f2 or f1
2721 2721 path1 = posixpath.join(prefix, path1[len(relroot):])
2722 2722 path2 = posixpath.join(prefix, path2[len(relroot):])
2723 2723 header = []
2724 2724 if opts.git:
2725 2725 header.append('diff --git %s%s %s%s' %
2726 2726 (aprefix, path1, bprefix, path2))
2727 2727 if not f1: # added
2728 2728 header.append('new file mode %s' % gitmode[flag2])
2729 2729 elif not f2: # removed
2730 2730 header.append('deleted file mode %s' % gitmode[flag1])
2731 2731 else: # modified/copied/renamed
2732 2732 mode1, mode2 = gitmode[flag1], gitmode[flag2]
2733 2733 if mode1 != mode2:
2734 2734 header.append('old mode %s' % mode1)
2735 2735 header.append('new mode %s' % mode2)
2736 2736 if copyop is not None:
2737 2737 if opts.showsimilarity:
2738 2738 sim = similar.score(ctx1[path1], ctx2[path2]) * 100
2739 2739 header.append('similarity index %d%%' % sim)
2740 2740 header.append('%s from %s' % (copyop, path1))
2741 2741 header.append('%s to %s' % (copyop, path2))
2742 2742 elif revs and not repo.ui.quiet:
2743 2743 header.append(diffline(path1, revs))
2744 2744
2745 2745 # fctx.is | diffopts | what to | is fctx.data()
2746 2746 # binary() | text nobinary git index | output? | outputted?
2747 2747 # ------------------------------------|----------------------------
2748 2748 # yes | no no no * | summary | no
2749 2749 # yes | no no yes * | base85 | yes
2750 2750 # yes | no yes no * | summary | no
2751 2751 # yes | no yes yes 0 | summary | no
2752 2752 # yes | no yes yes >0 | summary | semi [1]
2753 2753 # yes | yes * * * | text diff | yes
2754 2754 # no | * * * * | text diff | yes
2755 2755 # [1]: hash(fctx.data()) is outputted. so fctx.data() cannot be faked
2756 2756 if binary and (not opts.git or (opts.git and opts.nobinary and not
2757 2757 opts.index)):
2758 2758 # fast path: no binary content will be displayed, content1 and
2759 2759 # content2 are only used for equivalent test. cmp() could have a
2760 2760 # fast path.
2761 2761 if fctx1 is not None:
2762 2762 content1 = b'\0'
2763 2763 if fctx2 is not None:
2764 2764 if fctx1 is not None and not fctx1.cmp(fctx2):
2765 2765 content2 = b'\0' # not different
2766 2766 else:
2767 2767 content2 = b'\0\0'
2768 2768 else:
2769 2769 # normal path: load contents
2770 2770 if fctx1 is not None:
2771 2771 content1 = fctx1.data()
2772 2772 if fctx2 is not None:
2773 2773 content2 = fctx2.data()
2774 2774
2775 2775 if binary and opts.git and not opts.nobinary:
2776 2776 text = mdiff.b85diff(content1, content2)
2777 2777 if text:
2778 2778 header.append('index %s..%s' %
2779 2779 (gitindex(content1), gitindex(content2)))
2780 2780 hunks = (None, [text]),
2781 2781 else:
2782 2782 if opts.git and opts.index > 0:
2783 2783 flag = flag1
2784 2784 if flag is None:
2785 2785 flag = flag2
2786 2786 header.append('index %s..%s %s' %
2787 2787 (gitindex(content1)[0:opts.index],
2788 2788 gitindex(content2)[0:opts.index],
2789 2789 gitmode[flag]))
2790 2790
2791 2791 uheaders, hunks = mdiff.unidiff(content1, date1,
2792 2792 content2, date2,
2793 2793 path1, path2,
2794 2794 binary=binary, opts=opts)
2795 2795 header.extend(uheaders)
2796 2796 yield fctx1, fctx2, header, hunks
2797 2797
2798 2798 def diffstatsum(stats):
2799 2799 maxfile, maxtotal, addtotal, removetotal, binary = 0, 0, 0, 0, False
2800 2800 for f, a, r, b in stats:
2801 2801 maxfile = max(maxfile, encoding.colwidth(f))
2802 2802 maxtotal = max(maxtotal, a + r)
2803 2803 addtotal += a
2804 2804 removetotal += r
2805 2805 binary = binary or b
2806 2806
2807 2807 return maxfile, maxtotal, addtotal, removetotal, binary
2808 2808
2809 2809 def diffstatdata(lines):
2810 2810 diffre = re.compile('^diff .*-r [a-z0-9]+\s(.*)$')
2811 2811
2812 2812 results = []
2813 2813 filename, adds, removes, isbinary = None, 0, 0, False
2814 2814
2815 2815 def addresult():
2816 2816 if filename:
2817 2817 results.append((filename, adds, removes, isbinary))
2818 2818
2819 2819 # inheader is used to track if a line is in the
2820 2820 # header portion of the diff. This helps properly account
2821 2821 # for lines that start with '--' or '++'
2822 2822 inheader = False
2823 2823
2824 2824 for line in lines:
2825 2825 if line.startswith('diff'):
2826 2826 addresult()
2827 2827 # starting a new file diff
2828 2828 # set numbers to 0 and reset inheader
2829 2829 inheader = True
2830 2830 adds, removes, isbinary = 0, 0, False
2831 2831 if line.startswith('diff --git a/'):
2832 2832 filename = gitre.search(line).group(2)
2833 2833 elif line.startswith('diff -r'):
2834 2834 # format: "diff -r ... -r ... filename"
2835 2835 filename = diffre.search(line).group(1)
2836 2836 elif line.startswith('@@'):
2837 2837 inheader = False
2838 2838 elif line.startswith('+') and not inheader:
2839 2839 adds += 1
2840 2840 elif line.startswith('-') and not inheader:
2841 2841 removes += 1
2842 2842 elif (line.startswith('GIT binary patch') or
2843 2843 line.startswith('Binary file')):
2844 2844 isbinary = True
2845 2845 addresult()
2846 2846 return results
2847 2847
2848 2848 def diffstat(lines, width=80):
2849 2849 output = []
2850 2850 stats = diffstatdata(lines)
2851 2851 maxname, maxtotal, totaladds, totalremoves, hasbinary = diffstatsum(stats)
2852 2852
2853 2853 countwidth = len(str(maxtotal))
2854 2854 if hasbinary and countwidth < 3:
2855 2855 countwidth = 3
2856 2856 graphwidth = width - countwidth - maxname - 6
2857 2857 if graphwidth < 10:
2858 2858 graphwidth = 10
2859 2859
2860 2860 def scale(i):
2861 2861 if maxtotal <= graphwidth:
2862 2862 return i
2863 2863 # If diffstat runs out of room it doesn't print anything,
2864 2864 # which isn't very useful, so always print at least one + or -
2865 2865 # if there were at least some changes.
2866 2866 return max(i * graphwidth // maxtotal, int(bool(i)))
2867 2867
2868 2868 for filename, adds, removes, isbinary in stats:
2869 2869 if isbinary:
2870 2870 count = 'Bin'
2871 2871 else:
2872 2872 count = '%d' % (adds + removes)
2873 2873 pluses = '+' * scale(adds)
2874 2874 minuses = '-' * scale(removes)
2875 2875 output.append(' %s%s | %*s %s%s\n' %
2876 2876 (filename, ' ' * (maxname - encoding.colwidth(filename)),
2877 2877 countwidth, count, pluses, minuses))
2878 2878
2879 2879 if stats:
2880 2880 output.append(_(' %d files changed, %d insertions(+), '
2881 2881 '%d deletions(-)\n')
2882 2882 % (len(stats), totaladds, totalremoves))
2883 2883
2884 2884 return ''.join(output)
2885 2885
2886 2886 def diffstatui(*args, **kw):
2887 2887 '''like diffstat(), but yields 2-tuples of (output, label) for
2888 2888 ui.write()
2889 2889 '''
2890 2890
2891 2891 for line in diffstat(*args, **kw).splitlines():
2892 2892 if line and line[-1] in '+-':
2893 2893 name, graph = line.rsplit(' ', 1)
2894 2894 yield (name + ' ', '')
2895 2895 m = re.search(br'\++', graph)
2896 2896 if m:
2897 2897 yield (m.group(0), 'diffstat.inserted')
2898 2898 m = re.search(br'-+', graph)
2899 2899 if m:
2900 2900 yield (m.group(0), 'diffstat.deleted')
2901 2901 else:
2902 2902 yield (line, '')
2903 2903 yield ('\n', '')
@@ -1,302 +1,302 b''
1 1 # progress.py progress bars related code
2 2 #
3 3 # Copyright (C) 2010 Augie Fackler <durin42@gmail.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 threading
12 12 import time
13 13
14 14 from .i18n import _
15 15 from . import encoding
16 16
17 17 def spacejoin(*args):
18 18 return ' '.join(s for s in args if s)
19 19
20 20 def shouldprint(ui):
21 21 return not (ui.quiet or ui.plain('progress')) and (
22 22 ui._isatty(ui.ferr) or ui.configbool('progress', 'assume-tty'))
23 23
24 24 def fmtremaining(seconds):
25 25 """format a number of remaining seconds in human readable way
26 26
27 27 This will properly display seconds, minutes, hours, days if needed"""
28 28 if seconds < 60:
29 29 # i18n: format XX seconds as "XXs"
30 30 return _("%02ds") % (seconds)
31 31 minutes = seconds // 60
32 32 if minutes < 60:
33 33 seconds -= minutes * 60
34 34 # i18n: format X minutes and YY seconds as "XmYYs"
35 35 return _("%dm%02ds") % (minutes, seconds)
36 36 # we're going to ignore seconds in this case
37 37 minutes += 1
38 38 hours = minutes // 60
39 39 minutes -= hours * 60
40 40 if hours < 30:
41 41 # i18n: format X hours and YY minutes as "XhYYm"
42 42 return _("%dh%02dm") % (hours, minutes)
43 43 # we're going to ignore minutes in this case
44 44 hours += 1
45 45 days = hours // 24
46 46 hours -= days * 24
47 47 if days < 15:
48 48 # i18n: format X days and YY hours as "XdYYh"
49 49 return _("%dd%02dh") % (days, hours)
50 50 # we're going to ignore hours in this case
51 51 days += 1
52 52 weeks = days // 7
53 53 days -= weeks * 7
54 54 if weeks < 55:
55 55 # i18n: format X weeks and YY days as "XwYYd"
56 56 return _("%dw%02dd") % (weeks, days)
57 57 # we're going to ignore days and treat a year as 52 weeks
58 58 weeks += 1
59 59 years = weeks // 52
60 60 weeks -= years * 52
61 61 # i18n: format X years and YY weeks as "XyYYw"
62 62 return _("%dy%02dw") % (years, weeks)
63 63
64 64 # file_write() and file_flush() of Python 2 do not restart on EINTR if
65 65 # the file is attached to a "slow" device (e.g. a terminal) and raise
66 66 # IOError. We cannot know how many bytes would be written by file_write(),
67 67 # but a progress text is known to be short enough to be written by a
68 68 # single write() syscall, so we can just retry file_write() with the whole
69 69 # text. (issue5532)
70 70 #
71 71 # This should be a short-term workaround. We'll need to fix every occurrence
72 72 # of write() to a terminal or pipe.
73 73 def _eintrretry(func, *args):
74 74 while True:
75 75 try:
76 76 return func(*args)
77 77 except IOError as err:
78 78 if err.errno == errno.EINTR:
79 79 continue
80 80 raise
81 81
82 82 class progbar(object):
83 83 def __init__(self, ui):
84 84 self.ui = ui
85 85 self._refreshlock = threading.Lock()
86 86 self.resetstate()
87 87
88 88 def resetstate(self):
89 89 self.topics = []
90 90 self.topicstates = {}
91 91 self.starttimes = {}
92 92 self.startvals = {}
93 93 self.printed = False
94 94 self.lastprint = time.time() + float(self.ui.config(
95 95 'progress', 'delay'))
96 96 self.curtopic = None
97 97 self.lasttopic = None
98 98 self.indetcount = 0
99 99 self.refresh = float(self.ui.config(
100 100 'progress', 'refresh'))
101 101 self.changedelay = max(3 * self.refresh,
102 102 float(self.ui.config(
103 103 'progress', 'changedelay')))
104 104 self.order = self.ui.configlist('progress', 'format')
105 105 self.estimateinterval = self.ui.configwith(
106 106 float, 'progress', 'estimateinterval')
107 107
108 108 def show(self, now, topic, pos, item, unit, total):
109 109 if not shouldprint(self.ui):
110 110 return
111 111 termwidth = self.width()
112 112 self.printed = True
113 113 head = ''
114 114 needprogress = False
115 115 tail = ''
116 116 for indicator in self.order:
117 117 add = ''
118 118 if indicator == 'topic':
119 119 add = topic
120 120 elif indicator == 'number':
121 121 if total:
122 122 add = b'%*d/%d' % (len(str(total)), pos, total)
123 123 else:
124 add = str(pos)
124 add = b'%d' % pos
125 125 elif indicator.startswith('item') and item:
126 126 slice = 'end'
127 127 if '-' in indicator:
128 128 wid = int(indicator.split('-')[1])
129 129 elif '+' in indicator:
130 130 slice = 'beginning'
131 131 wid = int(indicator.split('+')[1])
132 132 else:
133 133 wid = 20
134 134 if slice == 'end':
135 135 add = encoding.trim(item, wid, leftside=True)
136 136 else:
137 137 add = encoding.trim(item, wid)
138 138 add += (wid - encoding.colwidth(add)) * ' '
139 139 elif indicator == 'bar':
140 140 add = ''
141 141 needprogress = True
142 142 elif indicator == 'unit' and unit:
143 143 add = unit
144 144 elif indicator == 'estimate':
145 145 add = self.estimate(topic, pos, total, now)
146 146 elif indicator == 'speed':
147 147 add = self.speed(topic, pos, unit, now)
148 148 if not needprogress:
149 149 head = spacejoin(head, add)
150 150 else:
151 151 tail = spacejoin(tail, add)
152 152 if needprogress:
153 153 used = 0
154 154 if head:
155 155 used += encoding.colwidth(head) + 1
156 156 if tail:
157 157 used += encoding.colwidth(tail) + 1
158 158 progwidth = termwidth - used - 3
159 159 if total and pos <= total:
160 160 amt = pos * progwidth // total
161 161 bar = '=' * (amt - 1)
162 162 if amt > 0:
163 163 bar += '>'
164 164 bar += ' ' * (progwidth - amt)
165 165 else:
166 166 progwidth -= 3
167 167 self.indetcount += 1
168 168 # mod the count by twice the width so we can make the
169 169 # cursor bounce between the right and left sides
170 170 amt = self.indetcount % (2 * progwidth)
171 171 amt -= progwidth
172 172 bar = (' ' * int(progwidth - abs(amt)) + '<=>' +
173 173 ' ' * int(abs(amt)))
174 174 prog = ''.join(('[', bar, ']'))
175 175 out = spacejoin(head, prog, tail)
176 176 else:
177 177 out = spacejoin(head, tail)
178 178 self._writeerr('\r' + encoding.trim(out, termwidth))
179 179 self.lasttopic = topic
180 180 self._flusherr()
181 181
182 182 def clear(self):
183 183 if not self.printed or not self.lastprint or not shouldprint(self.ui):
184 184 return
185 185 self._writeerr('\r%s\r' % (' ' * self.width()))
186 186 if self.printed:
187 187 # force immediate re-paint of progress bar
188 188 self.lastprint = 0
189 189
190 190 def complete(self):
191 191 if not shouldprint(self.ui):
192 192 return
193 193 if self.ui.configbool('progress', 'clear-complete'):
194 194 self.clear()
195 195 else:
196 196 self._writeerr('\n')
197 197 self._flusherr()
198 198
199 199 def _flusherr(self):
200 200 _eintrretry(self.ui.ferr.flush)
201 201
202 202 def _writeerr(self, msg):
203 203 _eintrretry(self.ui.ferr.write, msg)
204 204
205 205 def width(self):
206 206 tw = self.ui.termwidth()
207 207 return min(int(self.ui.config('progress', 'width', default=tw)), tw)
208 208
209 209 def estimate(self, topic, pos, total, now):
210 210 if total is None:
211 211 return ''
212 212 initialpos = self.startvals[topic]
213 213 target = total - initialpos
214 214 delta = pos - initialpos
215 215 if delta > 0:
216 216 elapsed = now - self.starttimes[topic]
217 217 seconds = (elapsed * (target - delta)) // delta + 1
218 218 return fmtremaining(seconds)
219 219 return ''
220 220
221 221 def speed(self, topic, pos, unit, now):
222 222 initialpos = self.startvals[topic]
223 223 delta = pos - initialpos
224 224 elapsed = now - self.starttimes[topic]
225 225 if elapsed > 0:
226 226 return _('%d %s/sec') % (delta / elapsed, unit)
227 227 return ''
228 228
229 229 def _oktoprint(self, now):
230 230 '''Check if conditions are met to print - e.g. changedelay elapsed'''
231 231 if (self.lasttopic is None # first time we printed
232 232 # not a topic change
233 233 or self.curtopic == self.lasttopic
234 234 # it's been long enough we should print anyway
235 235 or now - self.lastprint >= self.changedelay):
236 236 return True
237 237 else:
238 238 return False
239 239
240 240 def _calibrateestimate(self, topic, now, pos):
241 241 '''Adjust starttimes and startvals for topic so ETA works better
242 242
243 243 If progress is non-linear (ex. get much slower in the last minute),
244 244 it's more friendly to only use a recent time span for ETA and speed
245 245 calculation.
246 246
247 247 [======================================> ]
248 248 ^^^^^^^
249 249 estimateinterval, only use this for estimation
250 250 '''
251 251 interval = self.estimateinterval
252 252 if interval <= 0:
253 253 return
254 254 elapsed = now - self.starttimes[topic]
255 255 if elapsed > interval:
256 256 delta = pos - self.startvals[topic]
257 257 newdelta = delta * interval / elapsed
258 258 # If a stall happens temporarily, ETA could change dramatically
259 259 # frequently. This is to avoid such dramatical change and make ETA
260 260 # smoother.
261 261 if newdelta < 0.1:
262 262 return
263 263 self.startvals[topic] = pos - newdelta
264 264 self.starttimes[topic] = now - interval
265 265
266 266 def progress(self, topic, pos, item='', unit='', total=None):
267 267 now = time.time()
268 268 self._refreshlock.acquire()
269 269 try:
270 270 if pos is None:
271 271 self.starttimes.pop(topic, None)
272 272 self.startvals.pop(topic, None)
273 273 self.topicstates.pop(topic, None)
274 274 # reset the progress bar if this is the outermost topic
275 275 if self.topics and self.topics[0] == topic and self.printed:
276 276 self.complete()
277 277 self.resetstate()
278 278 # truncate the list of topics assuming all topics within
279 279 # this one are also closed
280 280 if topic in self.topics:
281 281 self.topics = self.topics[:self.topics.index(topic)]
282 282 # reset the last topic to the one we just unwound to,
283 283 # so that higher-level topics will be stickier than
284 284 # lower-level topics
285 285 if self.topics:
286 286 self.lasttopic = self.topics[-1]
287 287 else:
288 288 self.lasttopic = None
289 289 else:
290 290 if topic not in self.topics:
291 291 self.starttimes[topic] = now
292 292 self.startvals[topic] = pos
293 293 self.topics.append(topic)
294 294 self.topicstates[topic] = pos, item, unit, total
295 295 self.curtopic = topic
296 296 self._calibrateestimate(topic, now, pos)
297 297 if now - self.lastprint >= self.refresh and self.topics:
298 298 if self._oktoprint(now):
299 299 self.lastprint = now
300 300 self.show(now, topic, *self.topicstates[topic])
301 301 finally:
302 302 self._refreshlock.release()
General Comments 0
You need to be logged in to leave comments. Login now