##// END OF EJS Templates
py3: convert the mode argument of os.fdopen to unicodes (1 of 2)...
Pulkit Goyal -
r30924:48dea083 default
parent child Browse files
Show More
@@ -1,557 +1,557 b''
1 1 # bundlerepo.py - repository class for viewing uncompressed bundles
2 2 #
3 3 # Copyright 2006, 2007 Benoit Boissinot <bboissin@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 """Repository class for viewing uncompressed bundles.
9 9
10 10 This provides a read-only repository interface to bundles as if they
11 11 were part of the actual repository.
12 12 """
13 13
14 14 from __future__ import absolute_import
15 15
16 16 import os
17 17 import shutil
18 18 import tempfile
19 19
20 20 from .i18n import _
21 21 from .node import nullid
22 22
23 23 from . import (
24 24 bundle2,
25 25 changegroup,
26 26 changelog,
27 27 cmdutil,
28 28 discovery,
29 29 error,
30 30 exchange,
31 31 filelog,
32 32 localrepo,
33 33 manifest,
34 34 mdiff,
35 35 node as nodemod,
36 36 pathutil,
37 37 phases,
38 38 pycompat,
39 39 revlog,
40 40 scmutil,
41 41 util,
42 42 )
43 43
44 44 class bundlerevlog(revlog.revlog):
45 45 def __init__(self, opener, indexfile, bundle, linkmapper):
46 46 # How it works:
47 47 # To retrieve a revision, we need to know the offset of the revision in
48 48 # the bundle (an unbundle object). We store this offset in the index
49 49 # (start). The base of the delta is stored in the base field.
50 50 #
51 51 # To differentiate a rev in the bundle from a rev in the revlog, we
52 52 # check revision against repotiprev.
53 53 opener = scmutil.readonlyvfs(opener)
54 54 revlog.revlog.__init__(self, opener, indexfile)
55 55 self.bundle = bundle
56 56 n = len(self)
57 57 self.repotiprev = n - 1
58 58 chain = None
59 59 self.bundlerevs = set() # used by 'bundle()' revset expression
60 60 getchunk = lambda: bundle.deltachunk(chain)
61 61 for chunkdata in iter(getchunk, {}):
62 62 node = chunkdata['node']
63 63 p1 = chunkdata['p1']
64 64 p2 = chunkdata['p2']
65 65 cs = chunkdata['cs']
66 66 deltabase = chunkdata['deltabase']
67 67 delta = chunkdata['delta']
68 68
69 69 size = len(delta)
70 70 start = bundle.tell() - size
71 71
72 72 link = linkmapper(cs)
73 73 if node in self.nodemap:
74 74 # this can happen if two branches make the same change
75 75 chain = node
76 76 self.bundlerevs.add(self.nodemap[node])
77 77 continue
78 78
79 79 for p in (p1, p2):
80 80 if p not in self.nodemap:
81 81 raise error.LookupError(p, self.indexfile,
82 82 _("unknown parent"))
83 83
84 84 if deltabase not in self.nodemap:
85 85 raise LookupError(deltabase, self.indexfile,
86 86 _('unknown delta base'))
87 87
88 88 baserev = self.rev(deltabase)
89 89 # start, size, full unc. size, base (unused), link, p1, p2, node
90 90 e = (revlog.offset_type(start, 0), size, -1, baserev, link,
91 91 self.rev(p1), self.rev(p2), node)
92 92 self.index.insert(-1, e)
93 93 self.nodemap[node] = n
94 94 self.bundlerevs.add(n)
95 95 chain = node
96 96 n += 1
97 97
98 98 def _chunk(self, rev):
99 99 # Warning: in case of bundle, the diff is against what we stored as
100 100 # delta base, not against rev - 1
101 101 # XXX: could use some caching
102 102 if rev <= self.repotiprev:
103 103 return revlog.revlog._chunk(self, rev)
104 104 self.bundle.seek(self.start(rev))
105 105 return self.bundle.read(self.length(rev))
106 106
107 107 def revdiff(self, rev1, rev2):
108 108 """return or calculate a delta between two revisions"""
109 109 if rev1 > self.repotiprev and rev2 > self.repotiprev:
110 110 # hot path for bundle
111 111 revb = self.index[rev2][3]
112 112 if revb == rev1:
113 113 return self._chunk(rev2)
114 114 elif rev1 <= self.repotiprev and rev2 <= self.repotiprev:
115 115 return revlog.revlog.revdiff(self, rev1, rev2)
116 116
117 117 return mdiff.textdiff(self.revision(self.node(rev1)),
118 118 self.revision(self.node(rev2)))
119 119
120 120 def revision(self, nodeorrev, raw=False):
121 121 """return an uncompressed revision of a given node or revision
122 122 number.
123 123 """
124 124 if isinstance(nodeorrev, int):
125 125 rev = nodeorrev
126 126 node = self.node(rev)
127 127 else:
128 128 node = nodeorrev
129 129 rev = self.rev(node)
130 130
131 131 if node == nullid:
132 132 return ""
133 133
134 134 text = None
135 135 chain = []
136 136 iterrev = rev
137 137 # reconstruct the revision if it is from a changegroup
138 138 while iterrev > self.repotiprev:
139 139 if self._cache and self._cache[1] == iterrev:
140 140 text = self._cache[2]
141 141 break
142 142 chain.append(iterrev)
143 143 iterrev = self.index[iterrev][3]
144 144 if text is None:
145 145 text = self.baserevision(iterrev)
146 146
147 147 while chain:
148 148 delta = self._chunk(chain.pop())
149 149 text = mdiff.patches(text, [delta])
150 150
151 151 text, validatehash = self._processflags(text, self.flags(rev),
152 152 'read', raw=raw)
153 153 if validatehash:
154 154 self.checkhash(text, node, rev=rev)
155 155 self._cache = (node, rev, text)
156 156 return text
157 157
158 158 def baserevision(self, nodeorrev):
159 159 # Revlog subclasses may override 'revision' method to modify format of
160 160 # content retrieved from revlog. To use bundlerevlog with such class one
161 161 # needs to override 'baserevision' and make more specific call here.
162 162 return revlog.revlog.revision(self, nodeorrev)
163 163
164 164 def addrevision(self, text, transaction, link, p1=None, p2=None, d=None):
165 165 raise NotImplementedError
166 166 def addgroup(self, revs, linkmapper, transaction):
167 167 raise NotImplementedError
168 168 def strip(self, rev, minlink):
169 169 raise NotImplementedError
170 170 def checksize(self):
171 171 raise NotImplementedError
172 172
173 173 class bundlechangelog(bundlerevlog, changelog.changelog):
174 174 def __init__(self, opener, bundle):
175 175 changelog.changelog.__init__(self, opener)
176 176 linkmapper = lambda x: x
177 177 bundlerevlog.__init__(self, opener, self.indexfile, bundle,
178 178 linkmapper)
179 179
180 180 def baserevision(self, nodeorrev):
181 181 # Although changelog doesn't override 'revision' method, some extensions
182 182 # may replace this class with another that does. Same story with
183 183 # manifest and filelog classes.
184 184
185 185 # This bypasses filtering on changelog.node() and rev() because we need
186 186 # revision text of the bundle base even if it is hidden.
187 187 oldfilter = self.filteredrevs
188 188 try:
189 189 self.filteredrevs = ()
190 190 return changelog.changelog.revision(self, nodeorrev)
191 191 finally:
192 192 self.filteredrevs = oldfilter
193 193
194 194 class bundlemanifest(bundlerevlog, manifest.manifestrevlog):
195 195 def __init__(self, opener, bundle, linkmapper, dirlogstarts=None, dir=''):
196 196 manifest.manifestrevlog.__init__(self, opener, dir=dir)
197 197 bundlerevlog.__init__(self, opener, self.indexfile, bundle,
198 198 linkmapper)
199 199 if dirlogstarts is None:
200 200 dirlogstarts = {}
201 201 if self.bundle.version == "03":
202 202 dirlogstarts = _getfilestarts(self.bundle)
203 203 self._dirlogstarts = dirlogstarts
204 204 self._linkmapper = linkmapper
205 205
206 206 def baserevision(self, nodeorrev):
207 207 node = nodeorrev
208 208 if isinstance(node, int):
209 209 node = self.node(node)
210 210
211 211 if node in self.fulltextcache:
212 212 result = self.fulltextcache[node].tostring()
213 213 else:
214 214 result = manifest.manifestrevlog.revision(self, nodeorrev)
215 215 return result
216 216
217 217 def dirlog(self, d):
218 218 if d in self._dirlogstarts:
219 219 self.bundle.seek(self._dirlogstarts[d])
220 220 return bundlemanifest(
221 221 self.opener, self.bundle, self._linkmapper,
222 222 self._dirlogstarts, dir=d)
223 223 return super(bundlemanifest, self).dirlog(d)
224 224
225 225 class bundlefilelog(bundlerevlog, filelog.filelog):
226 226 def __init__(self, opener, path, bundle, linkmapper):
227 227 filelog.filelog.__init__(self, opener, path)
228 228 bundlerevlog.__init__(self, opener, self.indexfile, bundle,
229 229 linkmapper)
230 230
231 231 def baserevision(self, nodeorrev):
232 232 return filelog.filelog.revision(self, nodeorrev)
233 233
234 234 class bundlepeer(localrepo.localpeer):
235 235 def canpush(self):
236 236 return False
237 237
238 238 class bundlephasecache(phases.phasecache):
239 239 def __init__(self, *args, **kwargs):
240 240 super(bundlephasecache, self).__init__(*args, **kwargs)
241 241 if util.safehasattr(self, 'opener'):
242 242 self.opener = scmutil.readonlyvfs(self.opener)
243 243
244 244 def write(self):
245 245 raise NotImplementedError
246 246
247 247 def _write(self, fp):
248 248 raise NotImplementedError
249 249
250 250 def _updateroots(self, phase, newroots, tr):
251 251 self.phaseroots[phase] = newroots
252 252 self.invalidate()
253 253 self.dirty = True
254 254
255 255 def _getfilestarts(bundle):
256 256 bundlefilespos = {}
257 257 for chunkdata in iter(bundle.filelogheader, {}):
258 258 fname = chunkdata['filename']
259 259 bundlefilespos[fname] = bundle.tell()
260 260 for chunk in iter(lambda: bundle.deltachunk(None), {}):
261 261 pass
262 262 return bundlefilespos
263 263
264 264 class bundlerepository(localrepo.localrepository):
265 265 def __init__(self, ui, path, bundlename):
266 266 def _writetempbundle(read, suffix, header=''):
267 267 """Write a temporary file to disk
268 268
269 269 This is closure because we need to make sure this tracked by
270 270 self.tempfile for cleanup purposes."""
271 271 fdtemp, temp = self.vfs.mkstemp(prefix="hg-bundle-",
272 272 suffix=".hg10un")
273 273 self.tempfile = temp
274 274
275 with os.fdopen(fdtemp, 'wb') as fptemp:
275 with os.fdopen(fdtemp, pycompat.sysstr('wb')) as fptemp:
276 276 fptemp.write(header)
277 277 while True:
278 278 chunk = read(2**18)
279 279 if not chunk:
280 280 break
281 281 fptemp.write(chunk)
282 282
283 283 return self.vfs.open(self.tempfile, mode="rb")
284 284 self._tempparent = None
285 285 try:
286 286 localrepo.localrepository.__init__(self, ui, path)
287 287 except error.RepoError:
288 288 self._tempparent = tempfile.mkdtemp()
289 289 localrepo.instance(ui, self._tempparent, 1)
290 290 localrepo.localrepository.__init__(self, ui, self._tempparent)
291 291 self.ui.setconfig('phases', 'publish', False, 'bundlerepo')
292 292
293 293 if path:
294 294 self._url = 'bundle:' + util.expandpath(path) + '+' + bundlename
295 295 else:
296 296 self._url = 'bundle:' + bundlename
297 297
298 298 self.tempfile = None
299 299 f = util.posixfile(bundlename, "rb")
300 300 self.bundlefile = self.bundle = exchange.readbundle(ui, f, bundlename)
301 301
302 302 if isinstance(self.bundle, bundle2.unbundle20):
303 303 cgstream = None
304 304 for part in self.bundle.iterparts():
305 305 if part.type == 'changegroup':
306 306 if cgstream is not None:
307 307 raise NotImplementedError("can't process "
308 308 "multiple changegroups")
309 309 cgstream = part
310 310 version = part.params.get('version', '01')
311 311 legalcgvers = changegroup.supportedincomingversions(self)
312 312 if version not in legalcgvers:
313 313 msg = _('Unsupported changegroup version: %s')
314 314 raise error.Abort(msg % version)
315 315 if self.bundle.compressed():
316 316 cgstream = _writetempbundle(part.read,
317 317 ".cg%sun" % version)
318 318
319 319 if cgstream is None:
320 320 raise error.Abort(_('No changegroups found'))
321 321 cgstream.seek(0)
322 322
323 323 self.bundle = changegroup.getunbundler(version, cgstream, 'UN')
324 324
325 325 elif self.bundle.compressed():
326 326 f = _writetempbundle(self.bundle.read, '.hg10un', header='HG10UN')
327 327 self.bundlefile = self.bundle = exchange.readbundle(ui, f,
328 328 bundlename,
329 329 self.vfs)
330 330
331 331 # dict with the mapping 'filename' -> position in the bundle
332 332 self.bundlefilespos = {}
333 333
334 334 self.firstnewrev = self.changelog.repotiprev + 1
335 335 phases.retractboundary(self, None, phases.draft,
336 336 [ctx.node() for ctx in self[self.firstnewrev:]])
337 337
338 338 @localrepo.unfilteredpropertycache
339 339 def _phasecache(self):
340 340 return bundlephasecache(self, self._phasedefaults)
341 341
342 342 @localrepo.unfilteredpropertycache
343 343 def changelog(self):
344 344 # consume the header if it exists
345 345 self.bundle.changelogheader()
346 346 c = bundlechangelog(self.svfs, self.bundle)
347 347 self.manstart = self.bundle.tell()
348 348 return c
349 349
350 350 def _constructmanifest(self):
351 351 self.bundle.seek(self.manstart)
352 352 # consume the header if it exists
353 353 self.bundle.manifestheader()
354 354 linkmapper = self.unfiltered().changelog.rev
355 355 m = bundlemanifest(self.svfs, self.bundle, linkmapper)
356 356 self.filestart = self.bundle.tell()
357 357 return m
358 358
359 359 @localrepo.unfilteredpropertycache
360 360 def manstart(self):
361 361 self.changelog
362 362 return self.manstart
363 363
364 364 @localrepo.unfilteredpropertycache
365 365 def filestart(self):
366 366 self.manifestlog
367 367 return self.filestart
368 368
369 369 def url(self):
370 370 return self._url
371 371
372 372 def file(self, f):
373 373 if not self.bundlefilespos:
374 374 self.bundle.seek(self.filestart)
375 375 self.bundlefilespos = _getfilestarts(self.bundle)
376 376
377 377 if f in self.bundlefilespos:
378 378 self.bundle.seek(self.bundlefilespos[f])
379 379 linkmapper = self.unfiltered().changelog.rev
380 380 return bundlefilelog(self.svfs, f, self.bundle, linkmapper)
381 381 else:
382 382 return filelog.filelog(self.svfs, f)
383 383
384 384 def close(self):
385 385 """Close assigned bundle file immediately."""
386 386 self.bundlefile.close()
387 387 if self.tempfile is not None:
388 388 self.vfs.unlink(self.tempfile)
389 389 if self._tempparent:
390 390 shutil.rmtree(self._tempparent, True)
391 391
392 392 def cancopy(self):
393 393 return False
394 394
395 395 def peer(self):
396 396 return bundlepeer(self)
397 397
398 398 def getcwd(self):
399 399 return pycompat.getcwd() # always outside the repo
400 400
401 401 # Check if parents exist in localrepo before setting
402 402 def setparents(self, p1, p2=nullid):
403 403 p1rev = self.changelog.rev(p1)
404 404 p2rev = self.changelog.rev(p2)
405 405 msg = _("setting parent to node %s that only exists in the bundle\n")
406 406 if self.changelog.repotiprev < p1rev:
407 407 self.ui.warn(msg % nodemod.hex(p1))
408 408 if self.changelog.repotiprev < p2rev:
409 409 self.ui.warn(msg % nodemod.hex(p2))
410 410 return super(bundlerepository, self).setparents(p1, p2)
411 411
412 412 def instance(ui, path, create):
413 413 if create:
414 414 raise error.Abort(_('cannot create new bundle repository'))
415 415 # internal config: bundle.mainreporoot
416 416 parentpath = ui.config("bundle", "mainreporoot", "")
417 417 if not parentpath:
418 418 # try to find the correct path to the working directory repo
419 419 parentpath = cmdutil.findrepo(pycompat.getcwd())
420 420 if parentpath is None:
421 421 parentpath = ''
422 422 if parentpath:
423 423 # Try to make the full path relative so we get a nice, short URL.
424 424 # In particular, we don't want temp dir names in test outputs.
425 425 cwd = pycompat.getcwd()
426 426 if parentpath == cwd:
427 427 parentpath = ''
428 428 else:
429 429 cwd = pathutil.normasprefix(cwd)
430 430 if parentpath.startswith(cwd):
431 431 parentpath = parentpath[len(cwd):]
432 432 u = util.url(path)
433 433 path = u.localpath()
434 434 if u.scheme == 'bundle':
435 435 s = path.split("+", 1)
436 436 if len(s) == 1:
437 437 repopath, bundlename = parentpath, s[0]
438 438 else:
439 439 repopath, bundlename = s
440 440 else:
441 441 repopath, bundlename = parentpath, path
442 442 return bundlerepository(ui, repopath, bundlename)
443 443
444 444 class bundletransactionmanager(object):
445 445 def transaction(self):
446 446 return None
447 447
448 448 def close(self):
449 449 raise NotImplementedError
450 450
451 451 def release(self):
452 452 raise NotImplementedError
453 453
454 454 def getremotechanges(ui, repo, other, onlyheads=None, bundlename=None,
455 455 force=False):
456 456 '''obtains a bundle of changes incoming from other
457 457
458 458 "onlyheads" restricts the returned changes to those reachable from the
459 459 specified heads.
460 460 "bundlename", if given, stores the bundle to this file path permanently;
461 461 otherwise it's stored to a temp file and gets deleted again when you call
462 462 the returned "cleanupfn".
463 463 "force" indicates whether to proceed on unrelated repos.
464 464
465 465 Returns a tuple (local, csets, cleanupfn):
466 466
467 467 "local" is a local repo from which to obtain the actual incoming
468 468 changesets; it is a bundlerepo for the obtained bundle when the
469 469 original "other" is remote.
470 470 "csets" lists the incoming changeset node ids.
471 471 "cleanupfn" must be called without arguments when you're done processing
472 472 the changes; it closes both the original "other" and the one returned
473 473 here.
474 474 '''
475 475 tmp = discovery.findcommonincoming(repo, other, heads=onlyheads,
476 476 force=force)
477 477 common, incoming, rheads = tmp
478 478 if not incoming:
479 479 try:
480 480 if bundlename:
481 481 os.unlink(bundlename)
482 482 except OSError:
483 483 pass
484 484 return repo, [], other.close
485 485
486 486 commonset = set(common)
487 487 rheads = [x for x in rheads if x not in commonset]
488 488
489 489 bundle = None
490 490 bundlerepo = None
491 491 localrepo = other.local()
492 492 if bundlename or not localrepo:
493 493 # create a bundle (uncompressed if other repo is not local)
494 494
495 495 # developer config: devel.legacy.exchange
496 496 legexc = ui.configlist('devel', 'legacy.exchange')
497 497 forcebundle1 = 'bundle2' not in legexc and 'bundle1' in legexc
498 498 canbundle2 = (not forcebundle1
499 499 and other.capable('getbundle')
500 500 and other.capable('bundle2'))
501 501 if canbundle2:
502 502 kwargs = {}
503 503 kwargs['common'] = common
504 504 kwargs['heads'] = rheads
505 505 kwargs['bundlecaps'] = exchange.caps20to10(repo)
506 506 kwargs['cg'] = True
507 507 b2 = other.getbundle('incoming', **kwargs)
508 508 fname = bundle = changegroup.writechunks(ui, b2._forwardchunks(),
509 509 bundlename)
510 510 else:
511 511 if other.capable('getbundle'):
512 512 cg = other.getbundle('incoming', common=common, heads=rheads)
513 513 elif onlyheads is None and not other.capable('changegroupsubset'):
514 514 # compat with older servers when pulling all remote heads
515 515 cg = other.changegroup(incoming, "incoming")
516 516 rheads = None
517 517 else:
518 518 cg = other.changegroupsubset(incoming, rheads, 'incoming')
519 519 if localrepo:
520 520 bundletype = "HG10BZ"
521 521 else:
522 522 bundletype = "HG10UN"
523 523 fname = bundle = bundle2.writebundle(ui, cg, bundlename,
524 524 bundletype)
525 525 # keep written bundle?
526 526 if bundlename:
527 527 bundle = None
528 528 if not localrepo:
529 529 # use the created uncompressed bundlerepo
530 530 localrepo = bundlerepo = bundlerepository(repo.baseui, repo.root,
531 531 fname)
532 532 # this repo contains local and other now, so filter out local again
533 533 common = repo.heads()
534 534 if localrepo:
535 535 # Part of common may be remotely filtered
536 536 # So use an unfiltered version
537 537 # The discovery process probably need cleanup to avoid that
538 538 localrepo = localrepo.unfiltered()
539 539
540 540 csets = localrepo.changelog.findmissing(common, rheads)
541 541
542 542 if bundlerepo:
543 543 reponodes = [ctx.node() for ctx in bundlerepo[bundlerepo.firstnewrev:]]
544 544 remotephases = other.listkeys('phases')
545 545
546 546 pullop = exchange.pulloperation(bundlerepo, other, heads=reponodes)
547 547 pullop.trmanager = bundletransactionmanager()
548 548 exchange._pullapplyphases(pullop, remotephases)
549 549
550 550 def cleanup():
551 551 if bundlerepo:
552 552 bundlerepo.close()
553 553 if bundle:
554 554 os.unlink(bundle)
555 555 other.close()
556 556
557 557 return (localrepo, csets, cleanup)
@@ -1,588 +1,588 b''
1 1 # chgserver.py - command server extension for cHg
2 2 #
3 3 # Copyright 2011 Yuya Nishihara <yuya@tcha.org>
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 """command server extension for cHg
9 9
10 10 'S' channel (read/write)
11 11 propagate ui.system() request to client
12 12
13 13 'attachio' command
14 14 attach client's stdio passed by sendmsg()
15 15
16 16 'chdir' command
17 17 change current directory
18 18
19 19 'setenv' command
20 20 replace os.environ completely
21 21
22 22 'setumask' command
23 23 set umask
24 24
25 25 'validate' command
26 26 reload the config and check if the server is up to date
27 27
28 28 Config
29 29 ------
30 30
31 31 ::
32 32
33 33 [chgserver]
34 34 idletimeout = 3600 # seconds, after which an idle server will exit
35 35 skiphash = False # whether to skip config or env change checks
36 36 """
37 37
38 38 from __future__ import absolute_import
39 39
40 40 import errno
41 41 import hashlib
42 42 import inspect
43 43 import os
44 44 import re
45 45 import struct
46 46 import time
47 47
48 48 from .i18n import _
49 49
50 50 from . import (
51 51 commandserver,
52 52 encoding,
53 53 error,
54 54 extensions,
55 55 osutil,
56 56 pycompat,
57 57 util,
58 58 )
59 59
60 60 _log = commandserver.log
61 61
62 62 def _hashlist(items):
63 63 """return sha1 hexdigest for a list"""
64 64 return hashlib.sha1(str(items)).hexdigest()
65 65
66 66 # sensitive config sections affecting confighash
67 67 _configsections = [
68 68 'alias', # affects global state commands.table
69 69 'extdiff', # uisetup will register new commands
70 70 'extensions',
71 71 ]
72 72
73 73 # sensitive environment variables affecting confighash
74 74 _envre = re.compile(r'''\A(?:
75 75 CHGHG
76 76 |HG(?:[A-Z].*)?
77 77 |LANG(?:UAGE)?
78 78 |LC_.*
79 79 |LD_.*
80 80 |PATH
81 81 |PYTHON.*
82 82 |TERM(?:INFO)?
83 83 |TZ
84 84 )\Z''', re.X)
85 85
86 86 def _confighash(ui):
87 87 """return a quick hash for detecting config/env changes
88 88
89 89 confighash is the hash of sensitive config items and environment variables.
90 90
91 91 for chgserver, it is designed that once confighash changes, the server is
92 92 not qualified to serve its client and should redirect the client to a new
93 93 server. different from mtimehash, confighash change will not mark the
94 94 server outdated and exit since the user can have different configs at the
95 95 same time.
96 96 """
97 97 sectionitems = []
98 98 for section in _configsections:
99 99 sectionitems.append(ui.configitems(section))
100 100 sectionhash = _hashlist(sectionitems)
101 101 envitems = [(k, v) for k, v in encoding.environ.iteritems()
102 102 if _envre.match(k)]
103 103 envhash = _hashlist(sorted(envitems))
104 104 return sectionhash[:6] + envhash[:6]
105 105
106 106 def _getmtimepaths(ui):
107 107 """get a list of paths that should be checked to detect change
108 108
109 109 The list will include:
110 110 - extensions (will not cover all files for complex extensions)
111 111 - mercurial/__version__.py
112 112 - python binary
113 113 """
114 114 modules = [m for n, m in extensions.extensions(ui)]
115 115 try:
116 116 from . import __version__
117 117 modules.append(__version__)
118 118 except ImportError:
119 119 pass
120 120 files = [pycompat.sysexecutable]
121 121 for m in modules:
122 122 try:
123 123 files.append(inspect.getabsfile(m))
124 124 except TypeError:
125 125 pass
126 126 return sorted(set(files))
127 127
128 128 def _mtimehash(paths):
129 129 """return a quick hash for detecting file changes
130 130
131 131 mtimehash calls stat on given paths and calculate a hash based on size and
132 132 mtime of each file. mtimehash does not read file content because reading is
133 133 expensive. therefore it's not 100% reliable for detecting content changes.
134 134 it's possible to return different hashes for same file contents.
135 135 it's also possible to return a same hash for different file contents for
136 136 some carefully crafted situation.
137 137
138 138 for chgserver, it is designed that once mtimehash changes, the server is
139 139 considered outdated immediately and should no longer provide service.
140 140
141 141 mtimehash is not included in confighash because we only know the paths of
142 142 extensions after importing them (there is imp.find_module but that faces
143 143 race conditions). We need to calculate confighash without importing.
144 144 """
145 145 def trystat(path):
146 146 try:
147 147 st = os.stat(path)
148 148 return (st.st_mtime, st.st_size)
149 149 except OSError:
150 150 # could be ENOENT, EPERM etc. not fatal in any case
151 151 pass
152 152 return _hashlist(map(trystat, paths))[:12]
153 153
154 154 class hashstate(object):
155 155 """a structure storing confighash, mtimehash, paths used for mtimehash"""
156 156 def __init__(self, confighash, mtimehash, mtimepaths):
157 157 self.confighash = confighash
158 158 self.mtimehash = mtimehash
159 159 self.mtimepaths = mtimepaths
160 160
161 161 @staticmethod
162 162 def fromui(ui, mtimepaths=None):
163 163 if mtimepaths is None:
164 164 mtimepaths = _getmtimepaths(ui)
165 165 confighash = _confighash(ui)
166 166 mtimehash = _mtimehash(mtimepaths)
167 167 _log('confighash = %s mtimehash = %s\n' % (confighash, mtimehash))
168 168 return hashstate(confighash, mtimehash, mtimepaths)
169 169
170 170 def _newchgui(srcui, csystem, attachio):
171 171 class chgui(srcui.__class__):
172 172 def __init__(self, src=None):
173 173 super(chgui, self).__init__(src)
174 174 if src:
175 175 self._csystem = getattr(src, '_csystem', csystem)
176 176 else:
177 177 self._csystem = csystem
178 178
179 179 def system(self, cmd, environ=None, cwd=None, onerr=None,
180 180 errprefix=None):
181 181 # fallback to the original system method if the output needs to be
182 182 # captured (to self._buffers), or the output stream is not stdout
183 183 # (e.g. stderr, cStringIO), because the chg client is not aware of
184 184 # these situations and will behave differently (write to stdout).
185 185 if (any(s[1] for s in self._bufferstates)
186 186 or not util.safehasattr(self.fout, 'fileno')
187 187 or self.fout.fileno() != util.stdout.fileno()):
188 188 return super(chgui, self).system(cmd, environ, cwd, onerr,
189 189 errprefix)
190 190 self.flush()
191 191 rc = self._csystem(cmd, util.shellenviron(environ), cwd)
192 192 if rc and onerr:
193 193 errmsg = '%s %s' % (os.path.basename(cmd.split(None, 1)[0]),
194 194 util.explainexit(rc)[0])
195 195 if errprefix:
196 196 errmsg = '%s: %s' % (errprefix, errmsg)
197 197 raise onerr(errmsg)
198 198 return rc
199 199
200 200 def _runpager(self, cmd):
201 201 self._csystem(cmd, util.shellenviron(), type='pager',
202 202 cmdtable={'attachio': attachio})
203 203
204 204 return chgui(srcui)
205 205
206 206 def _loadnewui(srcui, args):
207 207 from . import dispatch # avoid cycle
208 208
209 209 newui = srcui.__class__.load()
210 210 for a in ['fin', 'fout', 'ferr', 'environ']:
211 211 setattr(newui, a, getattr(srcui, a))
212 212 if util.safehasattr(srcui, '_csystem'):
213 213 newui._csystem = srcui._csystem
214 214
215 215 # command line args
216 216 args = args[:]
217 217 dispatch._parseconfig(newui, dispatch._earlygetopt(['--config'], args))
218 218
219 219 # stolen from tortoisehg.util.copydynamicconfig()
220 220 for section, name, value in srcui.walkconfig():
221 221 source = srcui.configsource(section, name)
222 222 if ':' in source or source == '--config':
223 223 # path:line or command line
224 224 continue
225 225 newui.setconfig(section, name, value, source)
226 226
227 227 # load wd and repo config, copied from dispatch.py
228 228 cwds = dispatch._earlygetopt(['--cwd'], args)
229 229 cwd = cwds and os.path.realpath(cwds[-1]) or None
230 230 rpath = dispatch._earlygetopt(["-R", "--repository", "--repo"], args)
231 231 path, newlui = dispatch._getlocal(newui, rpath, wd=cwd)
232 232
233 233 return (newui, newlui)
234 234
235 235 class channeledsystem(object):
236 236 """Propagate ui.system() request in the following format:
237 237
238 238 payload length (unsigned int),
239 239 type, '\0',
240 240 cmd, '\0',
241 241 cwd, '\0',
242 242 envkey, '=', val, '\0',
243 243 ...
244 244 envkey, '=', val
245 245
246 246 if type == 'system', waits for:
247 247
248 248 exitcode length (unsigned int),
249 249 exitcode (int)
250 250
251 251 if type == 'pager', repetitively waits for a command name ending with '\n'
252 252 and executes it defined by cmdtable, or exits the loop if the command name
253 253 is empty.
254 254 """
255 255 def __init__(self, in_, out, channel):
256 256 self.in_ = in_
257 257 self.out = out
258 258 self.channel = channel
259 259
260 260 def __call__(self, cmd, environ, cwd=None, type='system', cmdtable=None):
261 261 args = [type, util.quotecommand(cmd), os.path.abspath(cwd or '.')]
262 262 args.extend('%s=%s' % (k, v) for k, v in environ.iteritems())
263 263 data = '\0'.join(args)
264 264 self.out.write(struct.pack('>cI', self.channel, len(data)))
265 265 self.out.write(data)
266 266 self.out.flush()
267 267
268 268 if type == 'system':
269 269 length = self.in_.read(4)
270 270 length, = struct.unpack('>I', length)
271 271 if length != 4:
272 272 raise error.Abort(_('invalid response'))
273 273 rc, = struct.unpack('>i', self.in_.read(4))
274 274 return rc
275 275 elif type == 'pager':
276 276 while True:
277 277 cmd = self.in_.readline()[:-1]
278 278 if not cmd:
279 279 break
280 280 if cmdtable and cmd in cmdtable:
281 281 _log('pager subcommand: %s' % cmd)
282 282 cmdtable[cmd]()
283 283 else:
284 284 raise error.Abort(_('unexpected command: %s') % cmd)
285 285 else:
286 286 raise error.ProgrammingError('invalid S channel type: %s' % type)
287 287
288 288 _iochannels = [
289 289 # server.ch, ui.fp, mode
290 ('cin', 'fin', 'rb'),
291 ('cout', 'fout', 'wb'),
292 ('cerr', 'ferr', 'wb'),
290 ('cin', 'fin', pycompat.sysstr('rb')),
291 ('cout', 'fout', pycompat.sysstr('wb')),
292 ('cerr', 'ferr', pycompat.sysstr('wb')),
293 293 ]
294 294
295 295 class chgcmdserver(commandserver.server):
296 296 def __init__(self, ui, repo, fin, fout, sock, hashstate, baseaddress):
297 297 super(chgcmdserver, self).__init__(
298 298 _newchgui(ui, channeledsystem(fin, fout, 'S'), self.attachio),
299 299 repo, fin, fout)
300 300 self.clientsock = sock
301 301 self._oldios = [] # original (self.ch, ui.fp, fd) before "attachio"
302 302 self.hashstate = hashstate
303 303 self.baseaddress = baseaddress
304 304 if hashstate is not None:
305 305 self.capabilities = self.capabilities.copy()
306 306 self.capabilities['validate'] = chgcmdserver.validate
307 307
308 308 def cleanup(self):
309 309 super(chgcmdserver, self).cleanup()
310 310 # dispatch._runcatch() does not flush outputs if exception is not
311 311 # handled by dispatch._dispatch()
312 312 self.ui.flush()
313 313 self._restoreio()
314 314
315 315 def attachio(self):
316 316 """Attach to client's stdio passed via unix domain socket; all
317 317 channels except cresult will no longer be used
318 318 """
319 319 # tell client to sendmsg() with 1-byte payload, which makes it
320 320 # distinctive from "attachio\n" command consumed by client.read()
321 321 self.clientsock.sendall(struct.pack('>cI', 'I', 1))
322 322 clientfds = osutil.recvfds(self.clientsock.fileno())
323 323 _log('received fds: %r\n' % clientfds)
324 324
325 325 ui = self.ui
326 326 ui.flush()
327 327 first = self._saveio()
328 328 for fd, (cn, fn, mode) in zip(clientfds, _iochannels):
329 329 assert fd > 0
330 330 fp = getattr(ui, fn)
331 331 os.dup2(fd, fp.fileno())
332 332 os.close(fd)
333 333 if not first:
334 334 continue
335 335 # reset buffering mode when client is first attached. as we want
336 336 # to see output immediately on pager, the mode stays unchanged
337 337 # when client re-attached. ferr is unchanged because it should
338 338 # be unbuffered no matter if it is a tty or not.
339 339 if fn == 'ferr':
340 340 newfp = fp
341 341 else:
342 342 # make it line buffered explicitly because the default is
343 343 # decided on first write(), where fout could be a pager.
344 344 if fp.isatty():
345 345 bufsize = 1 # line buffered
346 346 else:
347 347 bufsize = -1 # system default
348 348 newfp = os.fdopen(fp.fileno(), mode, bufsize)
349 349 setattr(ui, fn, newfp)
350 350 setattr(self, cn, newfp)
351 351
352 352 self.cresult.write(struct.pack('>i', len(clientfds)))
353 353
354 354 def _saveio(self):
355 355 if self._oldios:
356 356 return False
357 357 ui = self.ui
358 358 for cn, fn, _mode in _iochannels:
359 359 ch = getattr(self, cn)
360 360 fp = getattr(ui, fn)
361 361 fd = os.dup(fp.fileno())
362 362 self._oldios.append((ch, fp, fd))
363 363 return True
364 364
365 365 def _restoreio(self):
366 366 ui = self.ui
367 367 for (ch, fp, fd), (cn, fn, _mode) in zip(self._oldios, _iochannels):
368 368 newfp = getattr(ui, fn)
369 369 # close newfp while it's associated with client; otherwise it
370 370 # would be closed when newfp is deleted
371 371 if newfp is not fp:
372 372 newfp.close()
373 373 # restore original fd: fp is open again
374 374 os.dup2(fd, fp.fileno())
375 375 os.close(fd)
376 376 setattr(self, cn, ch)
377 377 setattr(ui, fn, fp)
378 378 del self._oldios[:]
379 379
380 380 def validate(self):
381 381 """Reload the config and check if the server is up to date
382 382
383 383 Read a list of '\0' separated arguments.
384 384 Write a non-empty list of '\0' separated instruction strings or '\0'
385 385 if the list is empty.
386 386 An instruction string could be either:
387 387 - "unlink $path", the client should unlink the path to stop the
388 388 outdated server.
389 389 - "redirect $path", the client should attempt to connect to $path
390 390 first. If it does not work, start a new server. It implies
391 391 "reconnect".
392 392 - "exit $n", the client should exit directly with code n.
393 393 This may happen if we cannot parse the config.
394 394 - "reconnect", the client should close the connection and
395 395 reconnect.
396 396 If neither "reconnect" nor "redirect" is included in the instruction
397 397 list, the client can continue with this server after completing all
398 398 the instructions.
399 399 """
400 400 from . import dispatch # avoid cycle
401 401
402 402 args = self._readlist()
403 403 try:
404 404 self.ui, lui = _loadnewui(self.ui, args)
405 405 except error.ParseError as inst:
406 406 dispatch._formatparse(self.ui.warn, inst)
407 407 self.ui.flush()
408 408 self.cresult.write('exit 255')
409 409 return
410 410 newhash = hashstate.fromui(lui, self.hashstate.mtimepaths)
411 411 insts = []
412 412 if newhash.mtimehash != self.hashstate.mtimehash:
413 413 addr = _hashaddress(self.baseaddress, self.hashstate.confighash)
414 414 insts.append('unlink %s' % addr)
415 415 # mtimehash is empty if one or more extensions fail to load.
416 416 # to be compatible with hg, still serve the client this time.
417 417 if self.hashstate.mtimehash:
418 418 insts.append('reconnect')
419 419 if newhash.confighash != self.hashstate.confighash:
420 420 addr = _hashaddress(self.baseaddress, newhash.confighash)
421 421 insts.append('redirect %s' % addr)
422 422 _log('validate: %s\n' % insts)
423 423 self.cresult.write('\0'.join(insts) or '\0')
424 424
425 425 def chdir(self):
426 426 """Change current directory
427 427
428 428 Note that the behavior of --cwd option is bit different from this.
429 429 It does not affect --config parameter.
430 430 """
431 431 path = self._readstr()
432 432 if not path:
433 433 return
434 434 _log('chdir to %r\n' % path)
435 435 os.chdir(path)
436 436
437 437 def setumask(self):
438 438 """Change umask"""
439 439 mask = struct.unpack('>I', self._read(4))[0]
440 440 _log('setumask %r\n' % mask)
441 441 os.umask(mask)
442 442
443 443 def runcommand(self):
444 444 return super(chgcmdserver, self).runcommand()
445 445
446 446 def setenv(self):
447 447 """Clear and update os.environ
448 448
449 449 Note that not all variables can make an effect on the running process.
450 450 """
451 451 l = self._readlist()
452 452 try:
453 453 newenv = dict(s.split('=', 1) for s in l)
454 454 except ValueError:
455 455 raise ValueError('unexpected value in setenv request')
456 456 _log('setenv: %r\n' % sorted(newenv.keys()))
457 457 encoding.environ.clear()
458 458 encoding.environ.update(newenv)
459 459
460 460 capabilities = commandserver.server.capabilities.copy()
461 461 capabilities.update({'attachio': attachio,
462 462 'chdir': chdir,
463 463 'runcommand': runcommand,
464 464 'setenv': setenv,
465 465 'setumask': setumask})
466 466
467 467 if util.safehasattr(osutil, 'setprocname'):
468 468 def setprocname(self):
469 469 """Change process title"""
470 470 name = self._readstr()
471 471 _log('setprocname: %r\n' % name)
472 472 osutil.setprocname(name)
473 473 capabilities['setprocname'] = setprocname
474 474
475 475 def _tempaddress(address):
476 476 return '%s.%d.tmp' % (address, os.getpid())
477 477
478 478 def _hashaddress(address, hashstr):
479 479 # if the basename of address contains '.', use only the left part. this
480 480 # makes it possible for the client to pass 'server.tmp$PID' and follow by
481 481 # an atomic rename to avoid locking when spawning new servers.
482 482 dirname, basename = os.path.split(address)
483 483 basename = basename.split('.', 1)[0]
484 484 return '%s-%s' % (os.path.join(dirname, basename), hashstr)
485 485
486 486 class chgunixservicehandler(object):
487 487 """Set of operations for chg services"""
488 488
489 489 pollinterval = 1 # [sec]
490 490
491 491 def __init__(self, ui):
492 492 self.ui = ui
493 493 self._idletimeout = ui.configint('chgserver', 'idletimeout', 3600)
494 494 self._lastactive = time.time()
495 495
496 496 def bindsocket(self, sock, address):
497 497 self._inithashstate(address)
498 498 self._checkextensions()
499 499 self._bind(sock)
500 500 self._createsymlink()
501 501
502 502 def _inithashstate(self, address):
503 503 self._baseaddress = address
504 504 if self.ui.configbool('chgserver', 'skiphash', False):
505 505 self._hashstate = None
506 506 self._realaddress = address
507 507 return
508 508 self._hashstate = hashstate.fromui(self.ui)
509 509 self._realaddress = _hashaddress(address, self._hashstate.confighash)
510 510
511 511 def _checkextensions(self):
512 512 if not self._hashstate:
513 513 return
514 514 if extensions.notloaded():
515 515 # one or more extensions failed to load. mtimehash becomes
516 516 # meaningless because we do not know the paths of those extensions.
517 517 # set mtimehash to an illegal hash value to invalidate the server.
518 518 self._hashstate.mtimehash = ''
519 519
520 520 def _bind(self, sock):
521 521 # use a unique temp address so we can stat the file and do ownership
522 522 # check later
523 523 tempaddress = _tempaddress(self._realaddress)
524 524 util.bindunixsocket(sock, tempaddress)
525 525 self._socketstat = os.stat(tempaddress)
526 526 # rename will replace the old socket file if exists atomically. the
527 527 # old server will detect ownership change and exit.
528 528 util.rename(tempaddress, self._realaddress)
529 529
530 530 def _createsymlink(self):
531 531 if self._baseaddress == self._realaddress:
532 532 return
533 533 tempaddress = _tempaddress(self._baseaddress)
534 534 os.symlink(os.path.basename(self._realaddress), tempaddress)
535 535 util.rename(tempaddress, self._baseaddress)
536 536
537 537 def _issocketowner(self):
538 538 try:
539 539 stat = os.stat(self._realaddress)
540 540 return (stat.st_ino == self._socketstat.st_ino and
541 541 stat.st_mtime == self._socketstat.st_mtime)
542 542 except OSError:
543 543 return False
544 544
545 545 def unlinksocket(self, address):
546 546 if not self._issocketowner():
547 547 return
548 548 # it is possible to have a race condition here that we may
549 549 # remove another server's socket file. but that's okay
550 550 # since that server will detect and exit automatically and
551 551 # the client will start a new server on demand.
552 552 try:
553 553 os.unlink(self._realaddress)
554 554 except OSError as exc:
555 555 if exc.errno != errno.ENOENT:
556 556 raise
557 557
558 558 def printbanner(self, address):
559 559 # no "listening at" message should be printed to simulate hg behavior
560 560 pass
561 561
562 562 def shouldexit(self):
563 563 if not self._issocketowner():
564 564 self.ui.debug('%s is not owned, exiting.\n' % self._realaddress)
565 565 return True
566 566 if time.time() - self._lastactive > self._idletimeout:
567 567 self.ui.debug('being idle too long. exiting.\n')
568 568 return True
569 569 return False
570 570
571 571 def newconnection(self):
572 572 self._lastactive = time.time()
573 573
574 574 def createcmdserver(self, repo, conn, fin, fout):
575 575 return chgcmdserver(self.ui, repo, fin, fout, conn,
576 576 self._hashstate, self._baseaddress)
577 577
578 578 def chgunixservice(ui, repo, opts):
579 579 # CHGINTERNALMARK is temporarily set by chg client to detect if chg will
580 580 # start another chg. drop it to avoid possible side effects.
581 581 if 'CHGINTERNALMARK' in encoding.environ:
582 582 del encoding.environ['CHGINTERNALMARK']
583 583
584 584 if repo:
585 585 # one chgserver can serve multiple repos. drop repo information
586 586 ui.setconfig('bundle', 'mainreporoot', '', 'repo')
587 587 h = chgunixservicehandler(ui)
588 588 return commandserver.unixforkingservice(ui, repo=None, opts=opts, handler=h)
@@ -1,551 +1,551 b''
1 1 # commandserver.py - communicate with Mercurial's API over a pipe
2 2 #
3 3 # Copyright Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import errno
11 11 import gc
12 12 import os
13 13 import random
14 14 import select
15 15 import signal
16 16 import socket
17 17 import struct
18 18 import traceback
19 19
20 20 from .i18n import _
21 21 from . import (
22 22 encoding,
23 23 error,
24 24 pycompat,
25 25 util,
26 26 )
27 27
28 28 logfile = None
29 29
30 30 def log(*args):
31 31 if not logfile:
32 32 return
33 33
34 34 for a in args:
35 35 logfile.write(str(a))
36 36
37 37 logfile.flush()
38 38
39 39 class channeledoutput(object):
40 40 """
41 41 Write data to out in the following format:
42 42
43 43 data length (unsigned int),
44 44 data
45 45 """
46 46 def __init__(self, out, channel):
47 47 self.out = out
48 48 self.channel = channel
49 49
50 50 @property
51 51 def name(self):
52 52 return '<%c-channel>' % self.channel
53 53
54 54 def write(self, data):
55 55 if not data:
56 56 return
57 57 # single write() to guarantee the same atomicity as the underlying file
58 58 self.out.write(struct.pack('>cI', self.channel, len(data)) + data)
59 59 self.out.flush()
60 60
61 61 def __getattr__(self, attr):
62 62 if attr in ('isatty', 'fileno', 'tell', 'seek'):
63 63 raise AttributeError(attr)
64 64 return getattr(self.out, attr)
65 65
66 66 class channeledinput(object):
67 67 """
68 68 Read data from in_.
69 69
70 70 Requests for input are written to out in the following format:
71 71 channel identifier - 'I' for plain input, 'L' line based (1 byte)
72 72 how many bytes to send at most (unsigned int),
73 73
74 74 The client replies with:
75 75 data length (unsigned int), 0 meaning EOF
76 76 data
77 77 """
78 78
79 79 maxchunksize = 4 * 1024
80 80
81 81 def __init__(self, in_, out, channel):
82 82 self.in_ = in_
83 83 self.out = out
84 84 self.channel = channel
85 85
86 86 @property
87 87 def name(self):
88 88 return '<%c-channel>' % self.channel
89 89
90 90 def read(self, size=-1):
91 91 if size < 0:
92 92 # if we need to consume all the clients input, ask for 4k chunks
93 93 # so the pipe doesn't fill up risking a deadlock
94 94 size = self.maxchunksize
95 95 s = self._read(size, self.channel)
96 96 buf = s
97 97 while s:
98 98 s = self._read(size, self.channel)
99 99 buf += s
100 100
101 101 return buf
102 102 else:
103 103 return self._read(size, self.channel)
104 104
105 105 def _read(self, size, channel):
106 106 if not size:
107 107 return ''
108 108 assert size > 0
109 109
110 110 # tell the client we need at most size bytes
111 111 self.out.write(struct.pack('>cI', channel, size))
112 112 self.out.flush()
113 113
114 114 length = self.in_.read(4)
115 115 length = struct.unpack('>I', length)[0]
116 116 if not length:
117 117 return ''
118 118 else:
119 119 return self.in_.read(length)
120 120
121 121 def readline(self, size=-1):
122 122 if size < 0:
123 123 size = self.maxchunksize
124 124 s = self._read(size, 'L')
125 125 buf = s
126 126 # keep asking for more until there's either no more or
127 127 # we got a full line
128 128 while s and s[-1] != '\n':
129 129 s = self._read(size, 'L')
130 130 buf += s
131 131
132 132 return buf
133 133 else:
134 134 return self._read(size, 'L')
135 135
136 136 def __iter__(self):
137 137 return self
138 138
139 139 def next(self):
140 140 l = self.readline()
141 141 if not l:
142 142 raise StopIteration
143 143 return l
144 144
145 145 def __getattr__(self, attr):
146 146 if attr in ('isatty', 'fileno', 'tell', 'seek'):
147 147 raise AttributeError(attr)
148 148 return getattr(self.in_, attr)
149 149
150 150 class server(object):
151 151 """
152 152 Listens for commands on fin, runs them and writes the output on a channel
153 153 based stream to fout.
154 154 """
155 155 def __init__(self, ui, repo, fin, fout):
156 156 self.cwd = pycompat.getcwd()
157 157
158 158 # developer config: cmdserver.log
159 159 logpath = ui.config("cmdserver", "log", None)
160 160 if logpath:
161 161 global logfile
162 162 if logpath == '-':
163 163 # write log on a special 'd' (debug) channel
164 164 logfile = channeledoutput(fout, 'd')
165 165 else:
166 166 logfile = open(logpath, 'a')
167 167
168 168 if repo:
169 169 # the ui here is really the repo ui so take its baseui so we don't
170 170 # end up with its local configuration
171 171 self.ui = repo.baseui
172 172 self.repo = repo
173 173 self.repoui = repo.ui
174 174 else:
175 175 self.ui = ui
176 176 self.repo = self.repoui = None
177 177
178 178 self.cerr = channeledoutput(fout, 'e')
179 179 self.cout = channeledoutput(fout, 'o')
180 180 self.cin = channeledinput(fin, fout, 'I')
181 181 self.cresult = channeledoutput(fout, 'r')
182 182
183 183 self.client = fin
184 184
185 185 def cleanup(self):
186 186 """release and restore resources taken during server session"""
187 187 pass
188 188
189 189 def _read(self, size):
190 190 if not size:
191 191 return ''
192 192
193 193 data = self.client.read(size)
194 194
195 195 # is the other end closed?
196 196 if not data:
197 197 raise EOFError
198 198
199 199 return data
200 200
201 201 def _readstr(self):
202 202 """read a string from the channel
203 203
204 204 format:
205 205 data length (uint32), data
206 206 """
207 207 length = struct.unpack('>I', self._read(4))[0]
208 208 if not length:
209 209 return ''
210 210 return self._read(length)
211 211
212 212 def _readlist(self):
213 213 """read a list of NULL separated strings from the channel"""
214 214 s = self._readstr()
215 215 if s:
216 216 return s.split('\0')
217 217 else:
218 218 return []
219 219
220 220 def runcommand(self):
221 221 """ reads a list of \0 terminated arguments, executes
222 222 and writes the return code to the result channel """
223 223 from . import dispatch # avoid cycle
224 224
225 225 args = self._readlist()
226 226
227 227 # copy the uis so changes (e.g. --config or --verbose) don't
228 228 # persist between requests
229 229 copiedui = self.ui.copy()
230 230 uis = [copiedui]
231 231 if self.repo:
232 232 self.repo.baseui = copiedui
233 233 # clone ui without using ui.copy because this is protected
234 234 repoui = self.repoui.__class__(self.repoui)
235 235 repoui.copy = copiedui.copy # redo copy protection
236 236 uis.append(repoui)
237 237 self.repo.ui = self.repo.dirstate._ui = repoui
238 238 self.repo.invalidateall()
239 239
240 240 for ui in uis:
241 241 ui.resetstate()
242 242 # any kind of interaction must use server channels, but chg may
243 243 # replace channels by fully functional tty files. so nontty is
244 244 # enforced only if cin is a channel.
245 245 if not util.safehasattr(self.cin, 'fileno'):
246 246 ui.setconfig('ui', 'nontty', 'true', 'commandserver')
247 247
248 248 req = dispatch.request(args[:], copiedui, self.repo, self.cin,
249 249 self.cout, self.cerr)
250 250
251 251 ret = (dispatch.dispatch(req) or 0) & 255 # might return None
252 252
253 253 # restore old cwd
254 254 if '--cwd' in args:
255 255 os.chdir(self.cwd)
256 256
257 257 self.cresult.write(struct.pack('>i', int(ret)))
258 258
259 259 def getencoding(self):
260 260 """ writes the current encoding to the result channel """
261 261 self.cresult.write(encoding.encoding)
262 262
263 263 def serveone(self):
264 264 cmd = self.client.readline()[:-1]
265 265 if cmd:
266 266 handler = self.capabilities.get(cmd)
267 267 if handler:
268 268 handler(self)
269 269 else:
270 270 # clients are expected to check what commands are supported by
271 271 # looking at the servers capabilities
272 272 raise error.Abort(_('unknown command %s') % cmd)
273 273
274 274 return cmd != ''
275 275
276 276 capabilities = {'runcommand' : runcommand,
277 277 'getencoding' : getencoding}
278 278
279 279 def serve(self):
280 280 hellomsg = 'capabilities: ' + ' '.join(sorted(self.capabilities))
281 281 hellomsg += '\n'
282 282 hellomsg += 'encoding: ' + encoding.encoding
283 283 hellomsg += '\n'
284 284 hellomsg += 'pid: %d' % util.getpid()
285 285 if util.safehasattr(os, 'getpgid'):
286 286 hellomsg += '\n'
287 287 hellomsg += 'pgid: %d' % os.getpgid(0)
288 288
289 289 # write the hello msg in -one- chunk
290 290 self.cout.write(hellomsg)
291 291
292 292 try:
293 293 while self.serveone():
294 294 pass
295 295 except EOFError:
296 296 # we'll get here if the client disconnected while we were reading
297 297 # its request
298 298 return 1
299 299
300 300 return 0
301 301
302 302 def _protectio(ui):
303 303 """ duplicates streams and redirect original to null if ui uses stdio """
304 304 ui.flush()
305 305 newfiles = []
306 306 nullfd = os.open(os.devnull, os.O_RDWR)
307 for f, sysf, mode in [(ui.fin, util.stdin, 'rb'),
308 (ui.fout, util.stdout, 'wb')]:
307 for f, sysf, mode in [(ui.fin, util.stdin, pycompat.sysstr('rb')),
308 (ui.fout, util.stdout, pycompat.sysstr('wb'))]:
309 309 if f is sysf:
310 310 newfd = os.dup(f.fileno())
311 311 os.dup2(nullfd, f.fileno())
312 312 f = os.fdopen(newfd, mode)
313 313 newfiles.append(f)
314 314 os.close(nullfd)
315 315 return tuple(newfiles)
316 316
317 317 def _restoreio(ui, fin, fout):
318 318 """ restores streams from duplicated ones """
319 319 ui.flush()
320 320 for f, uif in [(fin, ui.fin), (fout, ui.fout)]:
321 321 if f is not uif:
322 322 os.dup2(f.fileno(), uif.fileno())
323 323 f.close()
324 324
325 325 class pipeservice(object):
326 326 def __init__(self, ui, repo, opts):
327 327 self.ui = ui
328 328 self.repo = repo
329 329
330 330 def init(self):
331 331 pass
332 332
333 333 def run(self):
334 334 ui = self.ui
335 335 # redirect stdio to null device so that broken extensions or in-process
336 336 # hooks will never cause corruption of channel protocol.
337 337 fin, fout = _protectio(ui)
338 338 try:
339 339 sv = server(ui, self.repo, fin, fout)
340 340 return sv.serve()
341 341 finally:
342 342 sv.cleanup()
343 343 _restoreio(ui, fin, fout)
344 344
345 345 def _initworkerprocess():
346 346 # use a different process group from the master process, in order to:
347 347 # 1. make the current process group no longer "orphaned" (because the
348 348 # parent of this process is in a different process group while
349 349 # remains in a same session)
350 350 # according to POSIX 2.2.2.52, orphaned process group will ignore
351 351 # terminal-generated stop signals like SIGTSTP (Ctrl+Z), which will
352 352 # cause trouble for things like ncurses.
353 353 # 2. the client can use kill(-pgid, sig) to simulate terminal-generated
354 354 # SIGINT (Ctrl+C) and process-exit-generated SIGHUP. our child
355 355 # processes like ssh will be killed properly, without affecting
356 356 # unrelated processes.
357 357 os.setpgid(0, 0)
358 358 # change random state otherwise forked request handlers would have a
359 359 # same state inherited from parent.
360 360 random.seed()
361 361
362 362 def _serverequest(ui, repo, conn, createcmdserver):
363 363 fin = conn.makefile('rb')
364 364 fout = conn.makefile('wb')
365 365 sv = None
366 366 try:
367 367 sv = createcmdserver(repo, conn, fin, fout)
368 368 try:
369 369 sv.serve()
370 370 # handle exceptions that may be raised by command server. most of
371 371 # known exceptions are caught by dispatch.
372 372 except error.Abort as inst:
373 373 ui.warn(_('abort: %s\n') % inst)
374 374 except IOError as inst:
375 375 if inst.errno != errno.EPIPE:
376 376 raise
377 377 except KeyboardInterrupt:
378 378 pass
379 379 finally:
380 380 sv.cleanup()
381 381 except: # re-raises
382 382 # also write traceback to error channel. otherwise client cannot
383 383 # see it because it is written to server's stderr by default.
384 384 if sv:
385 385 cerr = sv.cerr
386 386 else:
387 387 cerr = channeledoutput(fout, 'e')
388 388 traceback.print_exc(file=cerr)
389 389 raise
390 390 finally:
391 391 fin.close()
392 392 try:
393 393 fout.close() # implicit flush() may cause another EPIPE
394 394 except IOError as inst:
395 395 if inst.errno != errno.EPIPE:
396 396 raise
397 397
398 398 class unixservicehandler(object):
399 399 """Set of pluggable operations for unix-mode services
400 400
401 401 Almost all methods except for createcmdserver() are called in the main
402 402 process. You can't pass mutable resource back from createcmdserver().
403 403 """
404 404
405 405 pollinterval = None
406 406
407 407 def __init__(self, ui):
408 408 self.ui = ui
409 409
410 410 def bindsocket(self, sock, address):
411 411 util.bindunixsocket(sock, address)
412 412
413 413 def unlinksocket(self, address):
414 414 os.unlink(address)
415 415
416 416 def printbanner(self, address):
417 417 self.ui.status(_('listening at %s\n') % address)
418 418 self.ui.flush() # avoid buffering of status message
419 419
420 420 def shouldexit(self):
421 421 """True if server should shut down; checked per pollinterval"""
422 422 return False
423 423
424 424 def newconnection(self):
425 425 """Called when main process notices new connection"""
426 426 pass
427 427
428 428 def createcmdserver(self, repo, conn, fin, fout):
429 429 """Create new command server instance; called in the process that
430 430 serves for the current connection"""
431 431 return server(self.ui, repo, fin, fout)
432 432
433 433 class unixforkingservice(object):
434 434 """
435 435 Listens on unix domain socket and forks server per connection
436 436 """
437 437
438 438 def __init__(self, ui, repo, opts, handler=None):
439 439 self.ui = ui
440 440 self.repo = repo
441 441 self.address = opts['address']
442 442 if not util.safehasattr(socket, 'AF_UNIX'):
443 443 raise error.Abort(_('unsupported platform'))
444 444 if not self.address:
445 445 raise error.Abort(_('no socket path specified with --address'))
446 446 self._servicehandler = handler or unixservicehandler(ui)
447 447 self._sock = None
448 448 self._oldsigchldhandler = None
449 449 self._workerpids = set() # updated by signal handler; do not iterate
450 450 self._socketunlinked = None
451 451
452 452 def init(self):
453 453 self._sock = socket.socket(socket.AF_UNIX)
454 454 self._servicehandler.bindsocket(self._sock, self.address)
455 455 self._sock.listen(socket.SOMAXCONN)
456 456 o = signal.signal(signal.SIGCHLD, self._sigchldhandler)
457 457 self._oldsigchldhandler = o
458 458 self._servicehandler.printbanner(self.address)
459 459 self._socketunlinked = False
460 460
461 461 def _unlinksocket(self):
462 462 if not self._socketunlinked:
463 463 self._servicehandler.unlinksocket(self.address)
464 464 self._socketunlinked = True
465 465
466 466 def _cleanup(self):
467 467 signal.signal(signal.SIGCHLD, self._oldsigchldhandler)
468 468 self._sock.close()
469 469 self._unlinksocket()
470 470 # don't kill child processes as they have active clients, just wait
471 471 self._reapworkers(0)
472 472
473 473 def run(self):
474 474 try:
475 475 self._mainloop()
476 476 finally:
477 477 self._cleanup()
478 478
479 479 def _mainloop(self):
480 480 exiting = False
481 481 h = self._servicehandler
482 482 while True:
483 483 if not exiting and h.shouldexit():
484 484 # clients can no longer connect() to the domain socket, so
485 485 # we stop queuing new requests.
486 486 # for requests that are queued (connect()-ed, but haven't been
487 487 # accept()-ed), handle them before exit. otherwise, clients
488 488 # waiting for recv() will receive ECONNRESET.
489 489 self._unlinksocket()
490 490 exiting = True
491 491 try:
492 492 ready = select.select([self._sock], [], [], h.pollinterval)[0]
493 493 if not ready:
494 494 # only exit if we completed all queued requests
495 495 if exiting:
496 496 break
497 497 continue
498 498 conn, _addr = self._sock.accept()
499 499 except (select.error, socket.error) as inst:
500 500 if inst.args[0] == errno.EINTR:
501 501 continue
502 502 raise
503 503
504 504 pid = os.fork()
505 505 if pid:
506 506 try:
507 507 self.ui.debug('forked worker process (pid=%d)\n' % pid)
508 508 self._workerpids.add(pid)
509 509 h.newconnection()
510 510 finally:
511 511 conn.close() # release handle in parent process
512 512 else:
513 513 try:
514 514 self._runworker(conn)
515 515 conn.close()
516 516 os._exit(0)
517 517 except: # never return, hence no re-raises
518 518 try:
519 519 self.ui.traceback(force=True)
520 520 finally:
521 521 os._exit(255)
522 522
523 523 def _sigchldhandler(self, signal, frame):
524 524 self._reapworkers(os.WNOHANG)
525 525
526 526 def _reapworkers(self, options):
527 527 while self._workerpids:
528 528 try:
529 529 pid, _status = os.waitpid(-1, options)
530 530 except OSError as inst:
531 531 if inst.errno == errno.EINTR:
532 532 continue
533 533 if inst.errno != errno.ECHILD:
534 534 raise
535 535 # no child processes at all (reaped by other waitpid()?)
536 536 self._workerpids.clear()
537 537 return
538 538 if pid == 0:
539 539 # no waitable child processes
540 540 return
541 541 self.ui.debug('worker process exited (pid=%d)\n' % pid)
542 542 self._workerpids.discard(pid)
543 543
544 544 def _runworker(self, conn):
545 545 signal.signal(signal.SIGCHLD, self._oldsigchldhandler)
546 546 _initworkerprocess()
547 547 h = self._servicehandler
548 548 try:
549 549 _serverequest(self.ui, self.repo, conn, h.createcmdserver)
550 550 finally:
551 551 gc.collect() # trigger __del__ since worker process uses os._exit
@@ -1,718 +1,718 b''
1 1 # filemerge.py - file-level merge handling for Mercurial
2 2 #
3 3 # Copyright 2006, 2007, 2008 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 filecmp
11 11 import os
12 12 import re
13 13 import tempfile
14 14
15 15 from .i18n import _
16 16 from .node import nullid, short
17 17
18 18 from . import (
19 19 encoding,
20 20 error,
21 21 formatter,
22 22 match,
23 23 pycompat,
24 24 scmutil,
25 25 simplemerge,
26 26 tagmerge,
27 27 templatekw,
28 28 templater,
29 29 util,
30 30 )
31 31
32 32 def _toolstr(ui, tool, part, default=""):
33 33 return ui.config("merge-tools", tool + "." + part, default)
34 34
35 35 def _toolbool(ui, tool, part, default=False):
36 36 return ui.configbool("merge-tools", tool + "." + part, default)
37 37
38 38 def _toollist(ui, tool, part, default=[]):
39 39 return ui.configlist("merge-tools", tool + "." + part, default)
40 40
41 41 internals = {}
42 42 # Merge tools to document.
43 43 internalsdoc = {}
44 44
45 45 # internal tool merge types
46 46 nomerge = None
47 47 mergeonly = 'mergeonly' # just the full merge, no premerge
48 48 fullmerge = 'fullmerge' # both premerge and merge
49 49
50 50 class absentfilectx(object):
51 51 """Represents a file that's ostensibly in a context but is actually not
52 52 present in it.
53 53
54 54 This is here because it's very specific to the filemerge code for now --
55 55 other code is likely going to break with the values this returns."""
56 56 def __init__(self, ctx, f):
57 57 self._ctx = ctx
58 58 self._f = f
59 59
60 60 def path(self):
61 61 return self._f
62 62
63 63 def size(self):
64 64 return None
65 65
66 66 def data(self):
67 67 return None
68 68
69 69 def filenode(self):
70 70 return nullid
71 71
72 72 _customcmp = True
73 73 def cmp(self, fctx):
74 74 """compare with other file context
75 75
76 76 returns True if different from fctx.
77 77 """
78 78 return not (fctx.isabsent() and
79 79 fctx.ctx() == self.ctx() and
80 80 fctx.path() == self.path())
81 81
82 82 def flags(self):
83 83 return ''
84 84
85 85 def changectx(self):
86 86 return self._ctx
87 87
88 88 def isbinary(self):
89 89 return False
90 90
91 91 def isabsent(self):
92 92 return True
93 93
94 94 def internaltool(name, mergetype, onfailure=None, precheck=None):
95 95 '''return a decorator for populating internal merge tool table'''
96 96 def decorator(func):
97 97 fullname = ':' + name
98 98 func.__doc__ = (pycompat.sysstr("``%s``\n" % fullname)
99 99 + func.__doc__.strip())
100 100 internals[fullname] = func
101 101 internals['internal:' + name] = func
102 102 internalsdoc[fullname] = func
103 103 func.mergetype = mergetype
104 104 func.onfailure = onfailure
105 105 func.precheck = precheck
106 106 return func
107 107 return decorator
108 108
109 109 def _findtool(ui, tool):
110 110 if tool in internals:
111 111 return tool
112 112 return findexternaltool(ui, tool)
113 113
114 114 def findexternaltool(ui, tool):
115 115 for kn in ("regkey", "regkeyalt"):
116 116 k = _toolstr(ui, tool, kn)
117 117 if not k:
118 118 continue
119 119 p = util.lookupreg(k, _toolstr(ui, tool, "regname"))
120 120 if p:
121 121 p = util.findexe(p + _toolstr(ui, tool, "regappend"))
122 122 if p:
123 123 return p
124 124 exe = _toolstr(ui, tool, "executable", tool)
125 125 return util.findexe(util.expandpath(exe))
126 126
127 127 def _picktool(repo, ui, path, binary, symlink, changedelete):
128 128 def supportscd(tool):
129 129 return tool in internals and internals[tool].mergetype == nomerge
130 130
131 131 def check(tool, pat, symlink, binary, changedelete):
132 132 tmsg = tool
133 133 if pat:
134 134 tmsg += " specified for " + pat
135 135 if not _findtool(ui, tool):
136 136 if pat: # explicitly requested tool deserves a warning
137 137 ui.warn(_("couldn't find merge tool %s\n") % tmsg)
138 138 else: # configured but non-existing tools are more silent
139 139 ui.note(_("couldn't find merge tool %s\n") % tmsg)
140 140 elif symlink and not _toolbool(ui, tool, "symlink"):
141 141 ui.warn(_("tool %s can't handle symlinks\n") % tmsg)
142 142 elif binary and not _toolbool(ui, tool, "binary"):
143 143 ui.warn(_("tool %s can't handle binary\n") % tmsg)
144 144 elif changedelete and not supportscd(tool):
145 145 # the nomerge tools are the only tools that support change/delete
146 146 # conflicts
147 147 pass
148 148 elif not util.gui() and _toolbool(ui, tool, "gui"):
149 149 ui.warn(_("tool %s requires a GUI\n") % tmsg)
150 150 else:
151 151 return True
152 152 return False
153 153
154 154 # internal config: ui.forcemerge
155 155 # forcemerge comes from command line arguments, highest priority
156 156 force = ui.config('ui', 'forcemerge')
157 157 if force:
158 158 toolpath = _findtool(ui, force)
159 159 if changedelete and not supportscd(toolpath):
160 160 return ":prompt", None
161 161 else:
162 162 if toolpath:
163 163 return (force, util.shellquote(toolpath))
164 164 else:
165 165 # mimic HGMERGE if given tool not found
166 166 return (force, force)
167 167
168 168 # HGMERGE takes next precedence
169 169 hgmerge = encoding.environ.get("HGMERGE")
170 170 if hgmerge:
171 171 if changedelete and not supportscd(hgmerge):
172 172 return ":prompt", None
173 173 else:
174 174 return (hgmerge, hgmerge)
175 175
176 176 # then patterns
177 177 for pat, tool in ui.configitems("merge-patterns"):
178 178 mf = match.match(repo.root, '', [pat])
179 179 if mf(path) and check(tool, pat, symlink, False, changedelete):
180 180 toolpath = _findtool(ui, tool)
181 181 return (tool, util.shellquote(toolpath))
182 182
183 183 # then merge tools
184 184 tools = {}
185 185 disabled = set()
186 186 for k, v in ui.configitems("merge-tools"):
187 187 t = k.split('.')[0]
188 188 if t not in tools:
189 189 tools[t] = int(_toolstr(ui, t, "priority", "0"))
190 190 if _toolbool(ui, t, "disabled", False):
191 191 disabled.add(t)
192 192 names = tools.keys()
193 193 tools = sorted([(-p, tool) for tool, p in tools.items()
194 194 if tool not in disabled])
195 195 uimerge = ui.config("ui", "merge")
196 196 if uimerge:
197 197 # external tools defined in uimerge won't be able to handle
198 198 # change/delete conflicts
199 199 if uimerge not in names and not changedelete:
200 200 return (uimerge, uimerge)
201 201 tools.insert(0, (None, uimerge)) # highest priority
202 202 tools.append((None, "hgmerge")) # the old default, if found
203 203 for p, t in tools:
204 204 if check(t, None, symlink, binary, changedelete):
205 205 toolpath = _findtool(ui, t)
206 206 return (t, util.shellquote(toolpath))
207 207
208 208 # internal merge or prompt as last resort
209 209 if symlink or binary or changedelete:
210 210 return ":prompt", None
211 211 return ":merge", None
212 212
213 213 def _eoltype(data):
214 214 "Guess the EOL type of a file"
215 215 if '\0' in data: # binary
216 216 return None
217 217 if '\r\n' in data: # Windows
218 218 return '\r\n'
219 219 if '\r' in data: # Old Mac
220 220 return '\r'
221 221 if '\n' in data: # UNIX
222 222 return '\n'
223 223 return None # unknown
224 224
225 225 def _matcheol(file, origfile):
226 226 "Convert EOL markers in a file to match origfile"
227 227 tostyle = _eoltype(util.readfile(origfile))
228 228 if tostyle:
229 229 data = util.readfile(file)
230 230 style = _eoltype(data)
231 231 if style:
232 232 newdata = data.replace(style, tostyle)
233 233 if newdata != data:
234 234 util.writefile(file, newdata)
235 235
236 236 @internaltool('prompt', nomerge)
237 237 def _iprompt(repo, mynode, orig, fcd, fco, fca, toolconf, labels=None):
238 238 """Asks the user which of the local `p1()` or the other `p2()` version to
239 239 keep as the merged version."""
240 240 ui = repo.ui
241 241 fd = fcd.path()
242 242
243 243 prompts = partextras(labels)
244 244 prompts['fd'] = fd
245 245 try:
246 246 if fco.isabsent():
247 247 index = ui.promptchoice(
248 248 _("local%(l)s changed %(fd)s which other%(o)s deleted\n"
249 249 "use (c)hanged version, (d)elete, or leave (u)nresolved?"
250 250 "$$ &Changed $$ &Delete $$ &Unresolved") % prompts, 2)
251 251 choice = ['local', 'other', 'unresolved'][index]
252 252 elif fcd.isabsent():
253 253 index = ui.promptchoice(
254 254 _("other%(o)s changed %(fd)s which local%(l)s deleted\n"
255 255 "use (c)hanged version, leave (d)eleted, or "
256 256 "leave (u)nresolved?"
257 257 "$$ &Changed $$ &Deleted $$ &Unresolved") % prompts, 2)
258 258 choice = ['other', 'local', 'unresolved'][index]
259 259 else:
260 260 index = ui.promptchoice(
261 261 _("no tool found to merge %(fd)s\n"
262 262 "keep (l)ocal%(l)s, take (o)ther%(o)s, or leave (u)nresolved?"
263 263 "$$ &Local $$ &Other $$ &Unresolved") % prompts, 2)
264 264 choice = ['local', 'other', 'unresolved'][index]
265 265
266 266 if choice == 'other':
267 267 return _iother(repo, mynode, orig, fcd, fco, fca, toolconf,
268 268 labels)
269 269 elif choice == 'local':
270 270 return _ilocal(repo, mynode, orig, fcd, fco, fca, toolconf,
271 271 labels)
272 272 elif choice == 'unresolved':
273 273 return _ifail(repo, mynode, orig, fcd, fco, fca, toolconf,
274 274 labels)
275 275 except error.ResponseExpected:
276 276 ui.write("\n")
277 277 return _ifail(repo, mynode, orig, fcd, fco, fca, toolconf,
278 278 labels)
279 279
280 280 @internaltool('local', nomerge)
281 281 def _ilocal(repo, mynode, orig, fcd, fco, fca, toolconf, labels=None):
282 282 """Uses the local `p1()` version of files as the merged version."""
283 283 return 0, fcd.isabsent()
284 284
285 285 @internaltool('other', nomerge)
286 286 def _iother(repo, mynode, orig, fcd, fco, fca, toolconf, labels=None):
287 287 """Uses the other `p2()` version of files as the merged version."""
288 288 if fco.isabsent():
289 289 # local changed, remote deleted -- 'deleted' picked
290 290 repo.wvfs.unlinkpath(fcd.path())
291 291 deleted = True
292 292 else:
293 293 repo.wwrite(fcd.path(), fco.data(), fco.flags())
294 294 deleted = False
295 295 return 0, deleted
296 296
297 297 @internaltool('fail', nomerge)
298 298 def _ifail(repo, mynode, orig, fcd, fco, fca, toolconf, labels=None):
299 299 """
300 300 Rather than attempting to merge files that were modified on both
301 301 branches, it marks them as unresolved. The resolve command must be
302 302 used to resolve these conflicts."""
303 303 # for change/delete conflicts write out the changed version, then fail
304 304 if fcd.isabsent():
305 305 repo.wwrite(fcd.path(), fco.data(), fco.flags())
306 306 return 1, False
307 307
308 308 def _premerge(repo, fcd, fco, fca, toolconf, files, labels=None):
309 309 tool, toolpath, binary, symlink = toolconf
310 310 if symlink or fcd.isabsent() or fco.isabsent():
311 311 return 1
312 312 a, b, c, back = files
313 313
314 314 ui = repo.ui
315 315
316 316 validkeep = ['keep', 'keep-merge3']
317 317
318 318 # do we attempt to simplemerge first?
319 319 try:
320 320 premerge = _toolbool(ui, tool, "premerge", not binary)
321 321 except error.ConfigError:
322 322 premerge = _toolstr(ui, tool, "premerge").lower()
323 323 if premerge not in validkeep:
324 324 _valid = ', '.join(["'" + v + "'" for v in validkeep])
325 325 raise error.ConfigError(_("%s.premerge not valid "
326 326 "('%s' is neither boolean nor %s)") %
327 327 (tool, premerge, _valid))
328 328
329 329 if premerge:
330 330 if premerge == 'keep-merge3':
331 331 if not labels:
332 332 labels = _defaultconflictlabels
333 333 if len(labels) < 3:
334 334 labels.append('base')
335 335 r = simplemerge.simplemerge(ui, a, b, c, quiet=True, label=labels)
336 336 if not r:
337 337 ui.debug(" premerge successful\n")
338 338 return 0
339 339 if premerge not in validkeep:
340 340 util.copyfile(back, a) # restore from backup and try again
341 341 return 1 # continue merging
342 342
343 343 def _mergecheck(repo, mynode, orig, fcd, fco, fca, toolconf):
344 344 tool, toolpath, binary, symlink = toolconf
345 345 if symlink:
346 346 repo.ui.warn(_('warning: internal %s cannot merge symlinks '
347 347 'for %s\n') % (tool, fcd.path()))
348 348 return False
349 349 if fcd.isabsent() or fco.isabsent():
350 350 repo.ui.warn(_('warning: internal %s cannot merge change/delete '
351 351 'conflict for %s\n') % (tool, fcd.path()))
352 352 return False
353 353 return True
354 354
355 355 def _merge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels, mode):
356 356 """
357 357 Uses the internal non-interactive simple merge algorithm for merging
358 358 files. It will fail if there are any conflicts and leave markers in
359 359 the partially merged file. Markers will have two sections, one for each side
360 360 of merge, unless mode equals 'union' which suppresses the markers."""
361 361 a, b, c, back = files
362 362
363 363 ui = repo.ui
364 364
365 365 r = simplemerge.simplemerge(ui, a, b, c, label=labels, mode=mode)
366 366 return True, r, False
367 367
368 368 @internaltool('union', fullmerge,
369 369 _("warning: conflicts while merging %s! "
370 370 "(edit, then use 'hg resolve --mark')\n"),
371 371 precheck=_mergecheck)
372 372 def _iunion(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
373 373 """
374 374 Uses the internal non-interactive simple merge algorithm for merging
375 375 files. It will use both left and right sides for conflict regions.
376 376 No markers are inserted."""
377 377 return _merge(repo, mynode, orig, fcd, fco, fca, toolconf,
378 378 files, labels, 'union')
379 379
380 380 @internaltool('merge', fullmerge,
381 381 _("warning: conflicts while merging %s! "
382 382 "(edit, then use 'hg resolve --mark')\n"),
383 383 precheck=_mergecheck)
384 384 def _imerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
385 385 """
386 386 Uses the internal non-interactive simple merge algorithm for merging
387 387 files. It will fail if there are any conflicts and leave markers in
388 388 the partially merged file. Markers will have two sections, one for each side
389 389 of merge."""
390 390 return _merge(repo, mynode, orig, fcd, fco, fca, toolconf,
391 391 files, labels, 'merge')
392 392
393 393 @internaltool('merge3', fullmerge,
394 394 _("warning: conflicts while merging %s! "
395 395 "(edit, then use 'hg resolve --mark')\n"),
396 396 precheck=_mergecheck)
397 397 def _imerge3(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
398 398 """
399 399 Uses the internal non-interactive simple merge algorithm for merging
400 400 files. It will fail if there are any conflicts and leave markers in
401 401 the partially merged file. Marker will have three sections, one from each
402 402 side of the merge and one for the base content."""
403 403 if not labels:
404 404 labels = _defaultconflictlabels
405 405 if len(labels) < 3:
406 406 labels.append('base')
407 407 return _imerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels)
408 408
409 409 def _imergeauto(repo, mynode, orig, fcd, fco, fca, toolconf, files,
410 410 labels=None, localorother=None):
411 411 """
412 412 Generic driver for _imergelocal and _imergeother
413 413 """
414 414 assert localorother is not None
415 415 tool, toolpath, binary, symlink = toolconf
416 416 a, b, c, back = files
417 417 r = simplemerge.simplemerge(repo.ui, a, b, c, label=labels,
418 418 localorother=localorother)
419 419 return True, r
420 420
421 421 @internaltool('merge-local', mergeonly, precheck=_mergecheck)
422 422 def _imergelocal(*args, **kwargs):
423 423 """
424 424 Like :merge, but resolve all conflicts non-interactively in favor
425 425 of the local `p1()` changes."""
426 426 success, status = _imergeauto(localorother='local', *args, **kwargs)
427 427 return success, status, False
428 428
429 429 @internaltool('merge-other', mergeonly, precheck=_mergecheck)
430 430 def _imergeother(*args, **kwargs):
431 431 """
432 432 Like :merge, but resolve all conflicts non-interactively in favor
433 433 of the other `p2()` changes."""
434 434 success, status = _imergeauto(localorother='other', *args, **kwargs)
435 435 return success, status, False
436 436
437 437 @internaltool('tagmerge', mergeonly,
438 438 _("automatic tag merging of %s failed! "
439 439 "(use 'hg resolve --tool :merge' or another merge "
440 440 "tool of your choice)\n"))
441 441 def _itagmerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
442 442 """
443 443 Uses the internal tag merge algorithm (experimental).
444 444 """
445 445 success, status = tagmerge.merge(repo, fcd, fco, fca)
446 446 return success, status, False
447 447
448 448 @internaltool('dump', fullmerge)
449 449 def _idump(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
450 450 """
451 451 Creates three versions of the files to merge, containing the
452 452 contents of local, other and base. These files can then be used to
453 453 perform a merge manually. If the file to be merged is named
454 454 ``a.txt``, these files will accordingly be named ``a.txt.local``,
455 455 ``a.txt.other`` and ``a.txt.base`` and they will be placed in the
456 456 same directory as ``a.txt``."""
457 457 a, b, c, back = files
458 458
459 459 fd = fcd.path()
460 460
461 461 util.copyfile(a, a + ".local")
462 462 repo.wwrite(fd + ".other", fco.data(), fco.flags())
463 463 repo.wwrite(fd + ".base", fca.data(), fca.flags())
464 464 return False, 1, False
465 465
466 466 def _xmerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
467 467 tool, toolpath, binary, symlink = toolconf
468 468 if fcd.isabsent() or fco.isabsent():
469 469 repo.ui.warn(_('warning: %s cannot merge change/delete conflict '
470 470 'for %s\n') % (tool, fcd.path()))
471 471 return False, 1, None
472 472 a, b, c, back = files
473 473 out = ""
474 474 env = {'HG_FILE': fcd.path(),
475 475 'HG_MY_NODE': short(mynode),
476 476 'HG_OTHER_NODE': str(fco.changectx()),
477 477 'HG_BASE_NODE': str(fca.changectx()),
478 478 'HG_MY_ISLINK': 'l' in fcd.flags(),
479 479 'HG_OTHER_ISLINK': 'l' in fco.flags(),
480 480 'HG_BASE_ISLINK': 'l' in fca.flags(),
481 481 }
482 482
483 483 ui = repo.ui
484 484
485 485 args = _toolstr(ui, tool, "args", '$local $base $other')
486 486 if "$output" in args:
487 487 out, a = a, back # read input from backup, write to original
488 488 replace = {'local': a, 'base': b, 'other': c, 'output': out}
489 489 args = util.interpolate(r'\$', replace, args,
490 490 lambda s: util.shellquote(util.localpath(s)))
491 491 cmd = toolpath + ' ' + args
492 492 if _toolbool(ui, tool, "gui"):
493 493 repo.ui.status(_('running merge tool %s for file %s\n') %
494 494 (tool, fcd.path()))
495 495 repo.ui.debug('launching merge tool: %s\n' % cmd)
496 496 r = ui.system(cmd, cwd=repo.root, environ=env)
497 497 repo.ui.debug('merge tool returned: %s\n' % r)
498 498 return True, r, False
499 499
500 500 def _formatconflictmarker(repo, ctx, template, label, pad):
501 501 """Applies the given template to the ctx, prefixed by the label.
502 502
503 503 Pad is the minimum width of the label prefix, so that multiple markers
504 504 can have aligned templated parts.
505 505 """
506 506 if ctx.node() is None:
507 507 ctx = ctx.p1()
508 508
509 509 props = templatekw.keywords.copy()
510 510 props['templ'] = template
511 511 props['ctx'] = ctx
512 512 props['repo'] = repo
513 513 templateresult = template('conflictmarker', **props)
514 514
515 515 label = ('%s:' % label).ljust(pad + 1)
516 516 mark = '%s %s' % (label, templater.stringify(templateresult))
517 517
518 518 if mark:
519 519 mark = mark.splitlines()[0] # split for safety
520 520
521 521 # 8 for the prefix of conflict marker lines (e.g. '<<<<<<< ')
522 522 return util.ellipsis(mark, 80 - 8)
523 523
524 524 _defaultconflictmarker = ('{node|short} '
525 525 '{ifeq(tags, "tip", "", '
526 526 'ifeq(tags, "", "", "{tags} "))}'
527 527 '{if(bookmarks, "{bookmarks} ")}'
528 528 '{ifeq(branch, "default", "", "{branch} ")}'
529 529 '- {author|user}: {desc|firstline}')
530 530
531 531 _defaultconflictlabels = ['local', 'other']
532 532
533 533 def _formatlabels(repo, fcd, fco, fca, labels):
534 534 """Formats the given labels using the conflict marker template.
535 535
536 536 Returns a list of formatted labels.
537 537 """
538 538 cd = fcd.changectx()
539 539 co = fco.changectx()
540 540 ca = fca.changectx()
541 541
542 542 ui = repo.ui
543 543 template = ui.config('ui', 'mergemarkertemplate', _defaultconflictmarker)
544 544 tmpl = formatter.maketemplater(ui, 'conflictmarker', template)
545 545
546 546 pad = max(len(l) for l in labels)
547 547
548 548 newlabels = [_formatconflictmarker(repo, cd, tmpl, labels[0], pad),
549 549 _formatconflictmarker(repo, co, tmpl, labels[1], pad)]
550 550 if len(labels) > 2:
551 551 newlabels.append(_formatconflictmarker(repo, ca, tmpl, labels[2], pad))
552 552 return newlabels
553 553
554 554 def partextras(labels):
555 555 """Return a dictionary of extra labels for use in prompts to the user
556 556
557 557 Intended use is in strings of the form "(l)ocal%(l)s".
558 558 """
559 559 if labels is None:
560 560 return {
561 561 "l": "",
562 562 "o": "",
563 563 }
564 564
565 565 return {
566 566 "l": " [%s]" % labels[0],
567 567 "o": " [%s]" % labels[1],
568 568 }
569 569
570 570 def _filemerge(premerge, repo, mynode, orig, fcd, fco, fca, labels=None):
571 571 """perform a 3-way merge in the working directory
572 572
573 573 premerge = whether this is a premerge
574 574 mynode = parent node before merge
575 575 orig = original local filename before merge
576 576 fco = other file context
577 577 fca = ancestor file context
578 578 fcd = local file context for current/destination file
579 579
580 580 Returns whether the merge is complete, the return value of the merge, and
581 581 a boolean indicating whether the file was deleted from disk."""
582 582
583 583 def temp(prefix, ctx):
584 584 fullbase, ext = os.path.splitext(ctx.path())
585 585 pre = "%s~%s." % (os.path.basename(fullbase), prefix)
586 586 (fd, name) = tempfile.mkstemp(prefix=pre, suffix=ext)
587 587 data = repo.wwritedata(ctx.path(), ctx.data())
588 f = os.fdopen(fd, "wb")
588 f = os.fdopen(fd, pycompat.sysstr("wb"))
589 589 f.write(data)
590 590 f.close()
591 591 return name
592 592
593 593 if not fco.cmp(fcd): # files identical?
594 594 return True, None, False
595 595
596 596 ui = repo.ui
597 597 fd = fcd.path()
598 598 binary = fcd.isbinary() or fco.isbinary() or fca.isbinary()
599 599 symlink = 'l' in fcd.flags() + fco.flags()
600 600 changedelete = fcd.isabsent() or fco.isabsent()
601 601 tool, toolpath = _picktool(repo, ui, fd, binary, symlink, changedelete)
602 602 if tool in internals and tool.startswith('internal:'):
603 603 # normalize to new-style names (':merge' etc)
604 604 tool = tool[len('internal'):]
605 605 ui.debug("picked tool '%s' for %s (binary %s symlink %s changedelete %s)\n"
606 606 % (tool, fd, binary, symlink, changedelete))
607 607
608 608 if tool in internals:
609 609 func = internals[tool]
610 610 mergetype = func.mergetype
611 611 onfailure = func.onfailure
612 612 precheck = func.precheck
613 613 else:
614 614 func = _xmerge
615 615 mergetype = fullmerge
616 616 onfailure = _("merging %s failed!\n")
617 617 precheck = None
618 618
619 619 toolconf = tool, toolpath, binary, symlink
620 620
621 621 if mergetype == nomerge:
622 622 r, deleted = func(repo, mynode, orig, fcd, fco, fca, toolconf, labels)
623 623 return True, r, deleted
624 624
625 625 if premerge:
626 626 if orig != fco.path():
627 627 ui.status(_("merging %s and %s to %s\n") % (orig, fco.path(), fd))
628 628 else:
629 629 ui.status(_("merging %s\n") % fd)
630 630
631 631 ui.debug("my %s other %s ancestor %s\n" % (fcd, fco, fca))
632 632
633 633 if precheck and not precheck(repo, mynode, orig, fcd, fco, fca,
634 634 toolconf):
635 635 if onfailure:
636 636 ui.warn(onfailure % fd)
637 637 return True, 1, False
638 638
639 639 a = repo.wjoin(fd)
640 640 b = temp("base", fca)
641 641 c = temp("other", fco)
642 642 if not fcd.isabsent():
643 643 back = scmutil.origpath(ui, repo, a)
644 644 if premerge:
645 645 util.copyfile(a, back)
646 646 else:
647 647 back = None
648 648 files = (a, b, c, back)
649 649
650 650 r = 1
651 651 try:
652 652 markerstyle = ui.config('ui', 'mergemarkers', 'basic')
653 653 if not labels:
654 654 labels = _defaultconflictlabels
655 655 if markerstyle != 'basic':
656 656 labels = _formatlabels(repo, fcd, fco, fca, labels)
657 657
658 658 if premerge and mergetype == fullmerge:
659 659 r = _premerge(repo, fcd, fco, fca, toolconf, files, labels=labels)
660 660 # complete if premerge successful (r is 0)
661 661 return not r, r, False
662 662
663 663 needcheck, r, deleted = func(repo, mynode, orig, fcd, fco, fca,
664 664 toolconf, files, labels=labels)
665 665
666 666 if needcheck:
667 667 r = _check(r, ui, tool, fcd, files)
668 668
669 669 if r:
670 670 if onfailure:
671 671 ui.warn(onfailure % fd)
672 672
673 673 return True, r, deleted
674 674 finally:
675 675 if not r and back is not None:
676 676 util.unlink(back)
677 677 util.unlink(b)
678 678 util.unlink(c)
679 679
680 680 def _check(r, ui, tool, fcd, files):
681 681 fd = fcd.path()
682 682 a, b, c, back = files
683 683
684 684 if not r and (_toolbool(ui, tool, "checkconflicts") or
685 685 'conflicts' in _toollist(ui, tool, "check")):
686 686 if re.search("^(<<<<<<< .*|=======|>>>>>>> .*)$", fcd.data(),
687 687 re.MULTILINE):
688 688 r = 1
689 689
690 690 checked = False
691 691 if 'prompt' in _toollist(ui, tool, "check"):
692 692 checked = True
693 693 if ui.promptchoice(_("was merge of '%s' successful (yn)?"
694 694 "$$ &Yes $$ &No") % fd, 1):
695 695 r = 1
696 696
697 697 if not r and not checked and (_toolbool(ui, tool, "checkchanged") or
698 698 'changed' in
699 699 _toollist(ui, tool, "check")):
700 700 if back is not None and filecmp.cmp(a, back):
701 701 if ui.promptchoice(_(" output file %s appears unchanged\n"
702 702 "was merge successful (yn)?"
703 703 "$$ &Yes $$ &No") % fd, 1):
704 704 r = 1
705 705
706 706 if back is not None and _toolbool(ui, tool, "fixeol"):
707 707 _matcheol(a, back)
708 708
709 709 return r
710 710
711 711 def premerge(repo, mynode, orig, fcd, fco, fca, labels=None):
712 712 return _filemerge(True, repo, mynode, orig, fcd, fco, fca, labels=labels)
713 713
714 714 def filemerge(repo, mynode, orig, fcd, fco, fca, labels=None):
715 715 return _filemerge(False, repo, mynode, orig, fcd, fco, fca, labels=labels)
716 716
717 717 # tell hggettext to extract docstrings from these functions:
718 718 i18nfunctions = internals.values()
@@ -1,382 +1,383 b''
1 1 # httppeer.py - HTTP repository proxy classes for mercurial
2 2 #
3 3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from __future__ import absolute_import
10 10
11 11 import errno
12 12 import os
13 13 import socket
14 14 import struct
15 15 import tempfile
16 16
17 17 from .i18n import _
18 18 from .node import nullid
19 19 from . import (
20 20 bundle2,
21 21 error,
22 22 httpconnection,
23 pycompat,
23 24 statichttprepo,
24 25 url,
25 26 util,
26 27 wireproto,
27 28 )
28 29
29 30 httplib = util.httplib
30 31 urlerr = util.urlerr
31 32 urlreq = util.urlreq
32 33
33 34 # FUTURE: consider refactoring this API to use generators. This will
34 35 # require a compression engine API to emit generators.
35 36 def decompressresponse(response, engine):
36 37 try:
37 38 reader = engine.decompressorreader(response)
38 39 except httplib.HTTPException:
39 40 raise IOError(None, _('connection ended unexpectedly'))
40 41
41 42 # We need to wrap reader.read() so HTTPException on subsequent
42 43 # reads is also converted.
43 44 # Ideally we'd use super() here. However, if ``reader`` isn't a new-style
44 45 # class, this can raise:
45 46 # TypeError: super() argument 1 must be type, not classobj
46 47 origread = reader.read
47 48 class readerproxy(reader.__class__):
48 49 def read(self, *args, **kwargs):
49 50 try:
50 51 return origread(*args, **kwargs)
51 52 except httplib.HTTPException:
52 53 raise IOError(None, _('connection ended unexpectedly'))
53 54
54 55 reader.__class__ = readerproxy
55 56 return reader
56 57
57 58 def encodevalueinheaders(value, header, limit):
58 59 """Encode a string value into multiple HTTP headers.
59 60
60 61 ``value`` will be encoded into 1 or more HTTP headers with the names
61 62 ``header-<N>`` where ``<N>`` is an integer starting at 1. Each header
62 63 name + value will be at most ``limit`` bytes long.
63 64
64 65 Returns an iterable of 2-tuples consisting of header names and values.
65 66 """
66 67 fmt = header + '-%s'
67 68 valuelen = limit - len(fmt % '000') - len(': \r\n')
68 69 result = []
69 70
70 71 n = 0
71 72 for i in xrange(0, len(value), valuelen):
72 73 n += 1
73 74 result.append((fmt % str(n), value[i:i + valuelen]))
74 75
75 76 return result
76 77
77 78 class httppeer(wireproto.wirepeer):
78 79 def __init__(self, ui, path):
79 80 self.path = path
80 81 self.caps = None
81 82 self.handler = None
82 83 self.urlopener = None
83 84 self.requestbuilder = None
84 85 u = util.url(path)
85 86 if u.query or u.fragment:
86 87 raise error.Abort(_('unsupported URL component: "%s"') %
87 88 (u.query or u.fragment))
88 89
89 90 # urllib cannot handle URLs with embedded user or passwd
90 91 self._url, authinfo = u.authinfo()
91 92
92 93 self.ui = ui
93 94 self.ui.debug('using %s\n' % self._url)
94 95
95 96 self.urlopener = url.opener(ui, authinfo)
96 97 self.requestbuilder = urlreq.request
97 98
98 99 def __del__(self):
99 100 urlopener = getattr(self, 'urlopener', None)
100 101 if urlopener:
101 102 for h in urlopener.handlers:
102 103 h.close()
103 104 getattr(h, "close_all", lambda : None)()
104 105
105 106 def url(self):
106 107 return self.path
107 108
108 109 # look up capabilities only when needed
109 110
110 111 def _fetchcaps(self):
111 112 self.caps = set(self._call('capabilities').split())
112 113
113 114 def _capabilities(self):
114 115 if self.caps is None:
115 116 try:
116 117 self._fetchcaps()
117 118 except error.RepoError:
118 119 self.caps = set()
119 120 self.ui.debug('capabilities: %s\n' %
120 121 (' '.join(self.caps or ['none'])))
121 122 return self.caps
122 123
123 124 def lock(self):
124 125 raise error.Abort(_('operation not supported over http'))
125 126
126 127 def _callstream(self, cmd, _compressible=False, **args):
127 128 if cmd == 'pushkey':
128 129 args['data'] = ''
129 130 data = args.pop('data', None)
130 131 headers = args.pop('headers', {})
131 132
132 133 self.ui.debug("sending %s command\n" % cmd)
133 134 q = [('cmd', cmd)]
134 135 headersize = 0
135 136 varyheaders = []
136 137 # Important: don't use self.capable() here or else you end up
137 138 # with infinite recursion when trying to look up capabilities
138 139 # for the first time.
139 140 postargsok = self.caps is not None and 'httppostargs' in self.caps
140 141 # TODO: support for httppostargs when data is a file-like
141 142 # object rather than a basestring
142 143 canmungedata = not data or isinstance(data, basestring)
143 144 if postargsok and canmungedata:
144 145 strargs = urlreq.urlencode(sorted(args.items()))
145 146 if strargs:
146 147 if not data:
147 148 data = strargs
148 149 elif isinstance(data, basestring):
149 150 data = strargs + data
150 151 headers['X-HgArgs-Post'] = len(strargs)
151 152 else:
152 153 if len(args) > 0:
153 154 httpheader = self.capable('httpheader')
154 155 if httpheader:
155 156 headersize = int(httpheader.split(',', 1)[0])
156 157 if headersize > 0:
157 158 # The headers can typically carry more data than the URL.
158 159 encargs = urlreq.urlencode(sorted(args.items()))
159 160 for header, value in encodevalueinheaders(encargs, 'X-HgArg',
160 161 headersize):
161 162 headers[header] = value
162 163 varyheaders.append(header)
163 164 else:
164 165 q += sorted(args.items())
165 166 qs = '?%s' % urlreq.urlencode(q)
166 167 cu = "%s%s" % (self._url, qs)
167 168 size = 0
168 169 if util.safehasattr(data, 'length'):
169 170 size = data.length
170 171 elif data is not None:
171 172 size = len(data)
172 173 if size and self.ui.configbool('ui', 'usehttp2', False):
173 174 headers['Expect'] = '100-Continue'
174 175 headers['X-HgHttp2'] = '1'
175 176 if data is not None and 'Content-Type' not in headers:
176 177 headers['Content-Type'] = 'application/mercurial-0.1'
177 178
178 179 # Tell the server we accept application/mercurial-0.2 and multiple
179 180 # compression formats if the server is capable of emitting those
180 181 # payloads.
181 182 protoparams = []
182 183
183 184 mediatypes = set()
184 185 if self.caps is not None:
185 186 mt = self.capable('httpmediatype')
186 187 if mt:
187 188 protoparams.append('0.1')
188 189 mediatypes = set(mt.split(','))
189 190
190 191 if '0.2tx' in mediatypes:
191 192 protoparams.append('0.2')
192 193
193 194 if '0.2tx' in mediatypes and self.capable('compression'):
194 195 # We /could/ compare supported compression formats and prune
195 196 # non-mutually supported or error if nothing is mutually supported.
196 197 # For now, send the full list to the server and have it error.
197 198 comps = [e.wireprotosupport().name for e in
198 199 util.compengines.supportedwireengines(util.CLIENTROLE)]
199 200 protoparams.append('comp=%s' % ','.join(comps))
200 201
201 202 if protoparams:
202 203 protoheaders = encodevalueinheaders(' '.join(protoparams),
203 204 'X-HgProto',
204 205 headersize or 1024)
205 206 for header, value in protoheaders:
206 207 headers[header] = value
207 208 varyheaders.append(header)
208 209
209 210 headers['Vary'] = ','.join(varyheaders)
210 211 req = self.requestbuilder(cu, data, headers)
211 212
212 213 if data is not None:
213 214 self.ui.debug("sending %s bytes\n" % size)
214 215 req.add_unredirected_header('Content-Length', '%d' % size)
215 216 try:
216 217 resp = self.urlopener.open(req)
217 218 except urlerr.httperror as inst:
218 219 if inst.code == 401:
219 220 raise error.Abort(_('authorization failed'))
220 221 raise
221 222 except httplib.HTTPException as inst:
222 223 self.ui.debug('http error while sending %s command\n' % cmd)
223 224 self.ui.traceback()
224 225 raise IOError(None, inst)
225 226 # record the url we got redirected to
226 227 resp_url = resp.geturl()
227 228 if resp_url.endswith(qs):
228 229 resp_url = resp_url[:-len(qs)]
229 230 if self._url.rstrip('/') != resp_url.rstrip('/'):
230 231 if not self.ui.quiet:
231 232 self.ui.warn(_('real URL is %s\n') % resp_url)
232 233 self._url = resp_url
233 234 try:
234 235 proto = resp.getheader('content-type')
235 236 except AttributeError:
236 237 proto = resp.headers.get('content-type', '')
237 238
238 239 safeurl = util.hidepassword(self._url)
239 240 if proto.startswith('application/hg-error'):
240 241 raise error.OutOfBandError(resp.read())
241 242 # accept old "text/plain" and "application/hg-changegroup" for now
242 243 if not (proto.startswith('application/mercurial-') or
243 244 (proto.startswith('text/plain')
244 245 and not resp.headers.get('content-length')) or
245 246 proto.startswith('application/hg-changegroup')):
246 247 self.ui.debug("requested URL: '%s'\n" % util.hidepassword(cu))
247 248 raise error.RepoError(
248 249 _("'%s' does not appear to be an hg repository:\n"
249 250 "---%%<--- (%s)\n%s\n---%%<---\n")
250 251 % (safeurl, proto or 'no content-type', resp.read(1024)))
251 252
252 253 if proto.startswith('application/mercurial-'):
253 254 try:
254 255 version = proto.split('-', 1)[1]
255 256 version_info = tuple([int(n) for n in version.split('.')])
256 257 except ValueError:
257 258 raise error.RepoError(_("'%s' sent a broken Content-Type "
258 259 "header (%s)") % (safeurl, proto))
259 260
260 261 if version_info == (0, 1):
261 262 if _compressible:
262 263 return decompressresponse(resp, util.compengines['zlib'])
263 264 return resp
264 265 elif version_info == (0, 2):
265 266 # application/mercurial-0.2 always identifies the compression
266 267 # engine in the payload header.
267 268 elen = struct.unpack('B', resp.read(1))[0]
268 269 ename = resp.read(elen)
269 270 engine = util.compengines.forwiretype(ename)
270 271 return decompressresponse(resp, engine)
271 272 else:
272 273 raise error.RepoError(_("'%s' uses newer protocol %s") %
273 274 (safeurl, version))
274 275
275 276 if _compressible:
276 277 return decompressresponse(resp, util.compengines['zlib'])
277 278
278 279 return resp
279 280
280 281 def _call(self, cmd, **args):
281 282 fp = self._callstream(cmd, **args)
282 283 try:
283 284 return fp.read()
284 285 finally:
285 286 # if using keepalive, allow connection to be reused
286 287 fp.close()
287 288
288 289 def _callpush(self, cmd, cg, **args):
289 290 # have to stream bundle to a temp file because we do not have
290 291 # http 1.1 chunked transfer.
291 292
292 293 types = self.capable('unbundle')
293 294 try:
294 295 types = types.split(',')
295 296 except AttributeError:
296 297 # servers older than d1b16a746db6 will send 'unbundle' as a
297 298 # boolean capability. They only support headerless/uncompressed
298 299 # bundles.
299 300 types = [""]
300 301 for x in types:
301 302 if x in bundle2.bundletypes:
302 303 type = x
303 304 break
304 305
305 306 tempname = bundle2.writebundle(self.ui, cg, None, type)
306 307 fp = httpconnection.httpsendfile(self.ui, tempname, "rb")
307 308 headers = {'Content-Type': 'application/mercurial-0.1'}
308 309
309 310 try:
310 311 r = self._call(cmd, data=fp, headers=headers, **args)
311 312 vals = r.split('\n', 1)
312 313 if len(vals) < 2:
313 314 raise error.ResponseError(_("unexpected response:"), r)
314 315 return vals
315 316 except socket.error as err:
316 317 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
317 318 raise error.Abort(_('push failed: %s') % err.args[1])
318 319 raise error.Abort(err.args[1])
319 320 finally:
320 321 fp.close()
321 322 os.unlink(tempname)
322 323
323 324 def _calltwowaystream(self, cmd, fp, **args):
324 325 fh = None
325 326 fp_ = None
326 327 filename = None
327 328 try:
328 329 # dump bundle to disk
329 330 fd, filename = tempfile.mkstemp(prefix="hg-bundle-", suffix=".hg")
330 fh = os.fdopen(fd, "wb")
331 fh = os.fdopen(fd, pycompat.sysstr("wb"))
331 332 d = fp.read(4096)
332 333 while d:
333 334 fh.write(d)
334 335 d = fp.read(4096)
335 336 fh.close()
336 337 # start http push
337 338 fp_ = httpconnection.httpsendfile(self.ui, filename, "rb")
338 339 headers = {'Content-Type': 'application/mercurial-0.1'}
339 340 return self._callstream(cmd, data=fp_, headers=headers, **args)
340 341 finally:
341 342 if fp_ is not None:
342 343 fp_.close()
343 344 if fh is not None:
344 345 fh.close()
345 346 os.unlink(filename)
346 347
347 348 def _callcompressable(self, cmd, **args):
348 349 return self._callstream(cmd, _compressible=True, **args)
349 350
350 351 def _abort(self, exception):
351 352 raise exception
352 353
353 354 class httpspeer(httppeer):
354 355 def __init__(self, ui, path):
355 356 if not url.has_https:
356 357 raise error.Abort(_('Python support for SSL and HTTPS '
357 358 'is not installed'))
358 359 httppeer.__init__(self, ui, path)
359 360
360 361 def instance(ui, path, create):
361 362 if create:
362 363 raise error.Abort(_('cannot create new http repository'))
363 364 try:
364 365 if path.startswith('https:'):
365 366 inst = httpspeer(ui, path)
366 367 else:
367 368 inst = httppeer(ui, path)
368 369 try:
369 370 # Try to do useful work when checking compatibility.
370 371 # Usually saves a roundtrip since we want the caps anyway.
371 372 inst._fetchcaps()
372 373 except error.RepoError:
373 374 # No luck, try older compatibility check.
374 375 inst.between([(nullid, nullid)])
375 376 return inst
376 377 except error.RepoError as httpexception:
377 378 try:
378 379 r = statichttprepo.instance(ui, "static-" + path, create)
379 380 ui.note(_('(falling back to static-http)\n'))
380 381 return r
381 382 except error.RepoError:
382 383 raise httpexception # use the original http RepoError instead
@@ -1,2653 +1,2654 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
10 10
11 11 import collections
12 12 import copy
13 13 import email
14 14 import errno
15 15 import hashlib
16 16 import os
17 17 import posixpath
18 18 import re
19 19 import shutil
20 20 import tempfile
21 21 import zlib
22 22
23 23 from .i18n import _
24 24 from .node import (
25 25 hex,
26 26 short,
27 27 )
28 28 from . import (
29 29 base85,
30 30 copies,
31 31 diffhelpers,
32 32 encoding,
33 33 error,
34 34 mail,
35 35 mdiff,
36 36 pathutil,
37 pycompat,
37 38 scmutil,
38 39 similar,
39 40 util,
40 41 )
41 42 stringio = util.stringio
42 43
43 44 gitre = re.compile('diff --git a/(.*) b/(.*)')
44 45 tabsplitter = re.compile(r'(\t+|[^\t]+)')
45 46
46 47 class PatchError(Exception):
47 48 pass
48 49
49 50
50 51 # public functions
51 52
52 53 def split(stream):
53 54 '''return an iterator of individual patches from a stream'''
54 55 def isheader(line, inheader):
55 56 if inheader and line[0] in (' ', '\t'):
56 57 # continuation
57 58 return True
58 59 if line[0] in (' ', '-', '+'):
59 60 # diff line - don't check for header pattern in there
60 61 return False
61 62 l = line.split(': ', 1)
62 63 return len(l) == 2 and ' ' not in l[0]
63 64
64 65 def chunk(lines):
65 66 return stringio(''.join(lines))
66 67
67 68 def hgsplit(stream, cur):
68 69 inheader = True
69 70
70 71 for line in stream:
71 72 if not line.strip():
72 73 inheader = False
73 74 if not inheader and line.startswith('# HG changeset patch'):
74 75 yield chunk(cur)
75 76 cur = []
76 77 inheader = True
77 78
78 79 cur.append(line)
79 80
80 81 if cur:
81 82 yield chunk(cur)
82 83
83 84 def mboxsplit(stream, cur):
84 85 for line in stream:
85 86 if line.startswith('From '):
86 87 for c in split(chunk(cur[1:])):
87 88 yield c
88 89 cur = []
89 90
90 91 cur.append(line)
91 92
92 93 if cur:
93 94 for c in split(chunk(cur[1:])):
94 95 yield c
95 96
96 97 def mimesplit(stream, cur):
97 98 def msgfp(m):
98 99 fp = stringio()
99 100 g = email.Generator.Generator(fp, mangle_from_=False)
100 101 g.flatten(m)
101 102 fp.seek(0)
102 103 return fp
103 104
104 105 for line in stream:
105 106 cur.append(line)
106 107 c = chunk(cur)
107 108
108 109 m = email.Parser.Parser().parse(c)
109 110 if not m.is_multipart():
110 111 yield msgfp(m)
111 112 else:
112 113 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
113 114 for part in m.walk():
114 115 ct = part.get_content_type()
115 116 if ct not in ok_types:
116 117 continue
117 118 yield msgfp(part)
118 119
119 120 def headersplit(stream, cur):
120 121 inheader = False
121 122
122 123 for line in stream:
123 124 if not inheader and isheader(line, inheader):
124 125 yield chunk(cur)
125 126 cur = []
126 127 inheader = True
127 128 if inheader and not isheader(line, inheader):
128 129 inheader = False
129 130
130 131 cur.append(line)
131 132
132 133 if cur:
133 134 yield chunk(cur)
134 135
135 136 def remainder(cur):
136 137 yield chunk(cur)
137 138
138 139 class fiter(object):
139 140 def __init__(self, fp):
140 141 self.fp = fp
141 142
142 143 def __iter__(self):
143 144 return self
144 145
145 146 def next(self):
146 147 l = self.fp.readline()
147 148 if not l:
148 149 raise StopIteration
149 150 return l
150 151
151 152 inheader = False
152 153 cur = []
153 154
154 155 mimeheaders = ['content-type']
155 156
156 157 if not util.safehasattr(stream, 'next'):
157 158 # http responses, for example, have readline but not next
158 159 stream = fiter(stream)
159 160
160 161 for line in stream:
161 162 cur.append(line)
162 163 if line.startswith('# HG changeset patch'):
163 164 return hgsplit(stream, cur)
164 165 elif line.startswith('From '):
165 166 return mboxsplit(stream, cur)
166 167 elif isheader(line, inheader):
167 168 inheader = True
168 169 if line.split(':', 1)[0].lower() in mimeheaders:
169 170 # let email parser handle this
170 171 return mimesplit(stream, cur)
171 172 elif line.startswith('--- ') and inheader:
172 173 # No evil headers seen by diff start, split by hand
173 174 return headersplit(stream, cur)
174 175 # Not enough info, keep reading
175 176
176 177 # if we are here, we have a very plain patch
177 178 return remainder(cur)
178 179
179 180 ## Some facility for extensible patch parsing:
180 181 # list of pairs ("header to match", "data key")
181 182 patchheadermap = [('Date', 'date'),
182 183 ('Branch', 'branch'),
183 184 ('Node ID', 'nodeid'),
184 185 ]
185 186
186 187 def extract(ui, fileobj):
187 188 '''extract patch from data read from fileobj.
188 189
189 190 patch can be a normal patch or contained in an email message.
190 191
191 192 return a dictionary. Standard keys are:
192 193 - filename,
193 194 - message,
194 195 - user,
195 196 - date,
196 197 - branch,
197 198 - node,
198 199 - p1,
199 200 - p2.
200 201 Any item can be missing from the dictionary. If filename is missing,
201 202 fileobj did not contain a patch. Caller must unlink filename when done.'''
202 203
203 204 # attempt to detect the start of a patch
204 205 # (this heuristic is borrowed from quilt)
205 206 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |'
206 207 r'retrieving revision [0-9]+(\.[0-9]+)*$|'
207 208 r'---[ \t].*?^\+\+\+[ \t]|'
208 209 r'\*\*\*[ \t].*?^---[ \t])', re.MULTILINE|re.DOTALL)
209 210
210 211 data = {}
211 212 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
212 tmpfp = os.fdopen(fd, 'w')
213 tmpfp = os.fdopen(fd, pycompat.sysstr('w'))
213 214 try:
214 215 msg = email.Parser.Parser().parse(fileobj)
215 216
216 217 subject = msg['Subject'] and mail.headdecode(msg['Subject'])
217 218 data['user'] = msg['From'] and mail.headdecode(msg['From'])
218 219 if not subject and not data['user']:
219 220 # Not an email, restore parsed headers if any
220 221 subject = '\n'.join(': '.join(h) for h in msg.items()) + '\n'
221 222
222 223 # should try to parse msg['Date']
223 224 parents = []
224 225
225 226 if subject:
226 227 if subject.startswith('[PATCH'):
227 228 pend = subject.find(']')
228 229 if pend >= 0:
229 230 subject = subject[pend + 1:].lstrip()
230 231 subject = re.sub(r'\n[ \t]+', ' ', subject)
231 232 ui.debug('Subject: %s\n' % subject)
232 233 if data['user']:
233 234 ui.debug('From: %s\n' % data['user'])
234 235 diffs_seen = 0
235 236 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
236 237 message = ''
237 238 for part in msg.walk():
238 239 content_type = part.get_content_type()
239 240 ui.debug('Content-Type: %s\n' % content_type)
240 241 if content_type not in ok_types:
241 242 continue
242 243 payload = part.get_payload(decode=True)
243 244 m = diffre.search(payload)
244 245 if m:
245 246 hgpatch = False
246 247 hgpatchheader = False
247 248 ignoretext = False
248 249
249 250 ui.debug('found patch at byte %d\n' % m.start(0))
250 251 diffs_seen += 1
251 252 cfp = stringio()
252 253 for line in payload[:m.start(0)].splitlines():
253 254 if line.startswith('# HG changeset patch') and not hgpatch:
254 255 ui.debug('patch generated by hg export\n')
255 256 hgpatch = True
256 257 hgpatchheader = True
257 258 # drop earlier commit message content
258 259 cfp.seek(0)
259 260 cfp.truncate()
260 261 subject = None
261 262 elif hgpatchheader:
262 263 if line.startswith('# User '):
263 264 data['user'] = line[7:]
264 265 ui.debug('From: %s\n' % data['user'])
265 266 elif line.startswith("# Parent "):
266 267 parents.append(line[9:].lstrip())
267 268 elif line.startswith("# "):
268 269 for header, key in patchheadermap:
269 270 prefix = '# %s ' % header
270 271 if line.startswith(prefix):
271 272 data[key] = line[len(prefix):]
272 273 else:
273 274 hgpatchheader = False
274 275 elif line == '---':
275 276 ignoretext = True
276 277 if not hgpatchheader and not ignoretext:
277 278 cfp.write(line)
278 279 cfp.write('\n')
279 280 message = cfp.getvalue()
280 281 if tmpfp:
281 282 tmpfp.write(payload)
282 283 if not payload.endswith('\n'):
283 284 tmpfp.write('\n')
284 285 elif not diffs_seen and message and content_type == 'text/plain':
285 286 message += '\n' + payload
286 287 except: # re-raises
287 288 tmpfp.close()
288 289 os.unlink(tmpname)
289 290 raise
290 291
291 292 if subject and not message.startswith(subject):
292 293 message = '%s\n%s' % (subject, message)
293 294 data['message'] = message
294 295 tmpfp.close()
295 296 if parents:
296 297 data['p1'] = parents.pop(0)
297 298 if parents:
298 299 data['p2'] = parents.pop(0)
299 300
300 301 if diffs_seen:
301 302 data['filename'] = tmpname
302 303 else:
303 304 os.unlink(tmpname)
304 305 return data
305 306
306 307 class patchmeta(object):
307 308 """Patched file metadata
308 309
309 310 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
310 311 or COPY. 'path' is patched file path. 'oldpath' is set to the
311 312 origin file when 'op' is either COPY or RENAME, None otherwise. If
312 313 file mode is changed, 'mode' is a tuple (islink, isexec) where
313 314 'islink' is True if the file is a symlink and 'isexec' is True if
314 315 the file is executable. Otherwise, 'mode' is None.
315 316 """
316 317 def __init__(self, path):
317 318 self.path = path
318 319 self.oldpath = None
319 320 self.mode = None
320 321 self.op = 'MODIFY'
321 322 self.binary = False
322 323
323 324 def setmode(self, mode):
324 325 islink = mode & 0o20000
325 326 isexec = mode & 0o100
326 327 self.mode = (islink, isexec)
327 328
328 329 def copy(self):
329 330 other = patchmeta(self.path)
330 331 other.oldpath = self.oldpath
331 332 other.mode = self.mode
332 333 other.op = self.op
333 334 other.binary = self.binary
334 335 return other
335 336
336 337 def _ispatchinga(self, afile):
337 338 if afile == '/dev/null':
338 339 return self.op == 'ADD'
339 340 return afile == 'a/' + (self.oldpath or self.path)
340 341
341 342 def _ispatchingb(self, bfile):
342 343 if bfile == '/dev/null':
343 344 return self.op == 'DELETE'
344 345 return bfile == 'b/' + self.path
345 346
346 347 def ispatching(self, afile, bfile):
347 348 return self._ispatchinga(afile) and self._ispatchingb(bfile)
348 349
349 350 def __repr__(self):
350 351 return "<patchmeta %s %r>" % (self.op, self.path)
351 352
352 353 def readgitpatch(lr):
353 354 """extract git-style metadata about patches from <patchname>"""
354 355
355 356 # Filter patch for git information
356 357 gp = None
357 358 gitpatches = []
358 359 for line in lr:
359 360 line = line.rstrip(' \r\n')
360 361 if line.startswith('diff --git a/'):
361 362 m = gitre.match(line)
362 363 if m:
363 364 if gp:
364 365 gitpatches.append(gp)
365 366 dst = m.group(2)
366 367 gp = patchmeta(dst)
367 368 elif gp:
368 369 if line.startswith('--- '):
369 370 gitpatches.append(gp)
370 371 gp = None
371 372 continue
372 373 if line.startswith('rename from '):
373 374 gp.op = 'RENAME'
374 375 gp.oldpath = line[12:]
375 376 elif line.startswith('rename to '):
376 377 gp.path = line[10:]
377 378 elif line.startswith('copy from '):
378 379 gp.op = 'COPY'
379 380 gp.oldpath = line[10:]
380 381 elif line.startswith('copy to '):
381 382 gp.path = line[8:]
382 383 elif line.startswith('deleted file'):
383 384 gp.op = 'DELETE'
384 385 elif line.startswith('new file mode '):
385 386 gp.op = 'ADD'
386 387 gp.setmode(int(line[-6:], 8))
387 388 elif line.startswith('new mode '):
388 389 gp.setmode(int(line[-6:], 8))
389 390 elif line.startswith('GIT binary patch'):
390 391 gp.binary = True
391 392 if gp:
392 393 gitpatches.append(gp)
393 394
394 395 return gitpatches
395 396
396 397 class linereader(object):
397 398 # simple class to allow pushing lines back into the input stream
398 399 def __init__(self, fp):
399 400 self.fp = fp
400 401 self.buf = []
401 402
402 403 def push(self, line):
403 404 if line is not None:
404 405 self.buf.append(line)
405 406
406 407 def readline(self):
407 408 if self.buf:
408 409 l = self.buf[0]
409 410 del self.buf[0]
410 411 return l
411 412 return self.fp.readline()
412 413
413 414 def __iter__(self):
414 415 return iter(self.readline, '')
415 416
416 417 class abstractbackend(object):
417 418 def __init__(self, ui):
418 419 self.ui = ui
419 420
420 421 def getfile(self, fname):
421 422 """Return target file data and flags as a (data, (islink,
422 423 isexec)) tuple. Data is None if file is missing/deleted.
423 424 """
424 425 raise NotImplementedError
425 426
426 427 def setfile(self, fname, data, mode, copysource):
427 428 """Write data to target file fname and set its mode. mode is a
428 429 (islink, isexec) tuple. If data is None, the file content should
429 430 be left unchanged. If the file is modified after being copied,
430 431 copysource is set to the original file name.
431 432 """
432 433 raise NotImplementedError
433 434
434 435 def unlink(self, fname):
435 436 """Unlink target file."""
436 437 raise NotImplementedError
437 438
438 439 def writerej(self, fname, failed, total, lines):
439 440 """Write rejected lines for fname. total is the number of hunks
440 441 which failed to apply and total the total number of hunks for this
441 442 files.
442 443 """
443 444 pass
444 445
445 446 def exists(self, fname):
446 447 raise NotImplementedError
447 448
448 449 class fsbackend(abstractbackend):
449 450 def __init__(self, ui, basedir):
450 451 super(fsbackend, self).__init__(ui)
451 452 self.opener = scmutil.opener(basedir)
452 453
453 454 def _join(self, f):
454 455 return os.path.join(self.opener.base, f)
455 456
456 457 def getfile(self, fname):
457 458 if self.opener.islink(fname):
458 459 return (self.opener.readlink(fname), (True, False))
459 460
460 461 isexec = False
461 462 try:
462 463 isexec = self.opener.lstat(fname).st_mode & 0o100 != 0
463 464 except OSError as e:
464 465 if e.errno != errno.ENOENT:
465 466 raise
466 467 try:
467 468 return (self.opener.read(fname), (False, isexec))
468 469 except IOError as e:
469 470 if e.errno != errno.ENOENT:
470 471 raise
471 472 return None, None
472 473
473 474 def setfile(self, fname, data, mode, copysource):
474 475 islink, isexec = mode
475 476 if data is None:
476 477 self.opener.setflags(fname, islink, isexec)
477 478 return
478 479 if islink:
479 480 self.opener.symlink(data, fname)
480 481 else:
481 482 self.opener.write(fname, data)
482 483 if isexec:
483 484 self.opener.setflags(fname, False, True)
484 485
485 486 def unlink(self, fname):
486 487 self.opener.unlinkpath(fname, ignoremissing=True)
487 488
488 489 def writerej(self, fname, failed, total, lines):
489 490 fname = fname + ".rej"
490 491 self.ui.warn(
491 492 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
492 493 (failed, total, fname))
493 494 fp = self.opener(fname, 'w')
494 495 fp.writelines(lines)
495 496 fp.close()
496 497
497 498 def exists(self, fname):
498 499 return self.opener.lexists(fname)
499 500
500 501 class workingbackend(fsbackend):
501 502 def __init__(self, ui, repo, similarity):
502 503 super(workingbackend, self).__init__(ui, repo.root)
503 504 self.repo = repo
504 505 self.similarity = similarity
505 506 self.removed = set()
506 507 self.changed = set()
507 508 self.copied = []
508 509
509 510 def _checkknown(self, fname):
510 511 if self.repo.dirstate[fname] == '?' and self.exists(fname):
511 512 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
512 513
513 514 def setfile(self, fname, data, mode, copysource):
514 515 self._checkknown(fname)
515 516 super(workingbackend, self).setfile(fname, data, mode, copysource)
516 517 if copysource is not None:
517 518 self.copied.append((copysource, fname))
518 519 self.changed.add(fname)
519 520
520 521 def unlink(self, fname):
521 522 self._checkknown(fname)
522 523 super(workingbackend, self).unlink(fname)
523 524 self.removed.add(fname)
524 525 self.changed.add(fname)
525 526
526 527 def close(self):
527 528 wctx = self.repo[None]
528 529 changed = set(self.changed)
529 530 for src, dst in self.copied:
530 531 scmutil.dirstatecopy(self.ui, self.repo, wctx, src, dst)
531 532 if self.removed:
532 533 wctx.forget(sorted(self.removed))
533 534 for f in self.removed:
534 535 if f not in self.repo.dirstate:
535 536 # File was deleted and no longer belongs to the
536 537 # dirstate, it was probably marked added then
537 538 # deleted, and should not be considered by
538 539 # marktouched().
539 540 changed.discard(f)
540 541 if changed:
541 542 scmutil.marktouched(self.repo, changed, self.similarity)
542 543 return sorted(self.changed)
543 544
544 545 class filestore(object):
545 546 def __init__(self, maxsize=None):
546 547 self.opener = None
547 548 self.files = {}
548 549 self.created = 0
549 550 self.maxsize = maxsize
550 551 if self.maxsize is None:
551 552 self.maxsize = 4*(2**20)
552 553 self.size = 0
553 554 self.data = {}
554 555
555 556 def setfile(self, fname, data, mode, copied=None):
556 557 if self.maxsize < 0 or (len(data) + self.size) <= self.maxsize:
557 558 self.data[fname] = (data, mode, copied)
558 559 self.size += len(data)
559 560 else:
560 561 if self.opener is None:
561 562 root = tempfile.mkdtemp(prefix='hg-patch-')
562 563 self.opener = scmutil.opener(root)
563 564 # Avoid filename issues with these simple names
564 565 fn = str(self.created)
565 566 self.opener.write(fn, data)
566 567 self.created += 1
567 568 self.files[fname] = (fn, mode, copied)
568 569
569 570 def getfile(self, fname):
570 571 if fname in self.data:
571 572 return self.data[fname]
572 573 if not self.opener or fname not in self.files:
573 574 return None, None, None
574 575 fn, mode, copied = self.files[fname]
575 576 return self.opener.read(fn), mode, copied
576 577
577 578 def close(self):
578 579 if self.opener:
579 580 shutil.rmtree(self.opener.base)
580 581
581 582 class repobackend(abstractbackend):
582 583 def __init__(self, ui, repo, ctx, store):
583 584 super(repobackend, self).__init__(ui)
584 585 self.repo = repo
585 586 self.ctx = ctx
586 587 self.store = store
587 588 self.changed = set()
588 589 self.removed = set()
589 590 self.copied = {}
590 591
591 592 def _checkknown(self, fname):
592 593 if fname not in self.ctx:
593 594 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
594 595
595 596 def getfile(self, fname):
596 597 try:
597 598 fctx = self.ctx[fname]
598 599 except error.LookupError:
599 600 return None, None
600 601 flags = fctx.flags()
601 602 return fctx.data(), ('l' in flags, 'x' in flags)
602 603
603 604 def setfile(self, fname, data, mode, copysource):
604 605 if copysource:
605 606 self._checkknown(copysource)
606 607 if data is None:
607 608 data = self.ctx[fname].data()
608 609 self.store.setfile(fname, data, mode, copysource)
609 610 self.changed.add(fname)
610 611 if copysource:
611 612 self.copied[fname] = copysource
612 613
613 614 def unlink(self, fname):
614 615 self._checkknown(fname)
615 616 self.removed.add(fname)
616 617
617 618 def exists(self, fname):
618 619 return fname in self.ctx
619 620
620 621 def close(self):
621 622 return self.changed | self.removed
622 623
623 624 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
624 625 unidesc = re.compile('@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@')
625 626 contextdesc = re.compile('(?:---|\*\*\*) (\d+)(?:,(\d+))? (?:---|\*\*\*)')
626 627 eolmodes = ['strict', 'crlf', 'lf', 'auto']
627 628
628 629 class patchfile(object):
629 630 def __init__(self, ui, gp, backend, store, eolmode='strict'):
630 631 self.fname = gp.path
631 632 self.eolmode = eolmode
632 633 self.eol = None
633 634 self.backend = backend
634 635 self.ui = ui
635 636 self.lines = []
636 637 self.exists = False
637 638 self.missing = True
638 639 self.mode = gp.mode
639 640 self.copysource = gp.oldpath
640 641 self.create = gp.op in ('ADD', 'COPY', 'RENAME')
641 642 self.remove = gp.op == 'DELETE'
642 643 if self.copysource is None:
643 644 data, mode = backend.getfile(self.fname)
644 645 else:
645 646 data, mode = store.getfile(self.copysource)[:2]
646 647 if data is not None:
647 648 self.exists = self.copysource is None or backend.exists(self.fname)
648 649 self.missing = False
649 650 if data:
650 651 self.lines = mdiff.splitnewlines(data)
651 652 if self.mode is None:
652 653 self.mode = mode
653 654 if self.lines:
654 655 # Normalize line endings
655 656 if self.lines[0].endswith('\r\n'):
656 657 self.eol = '\r\n'
657 658 elif self.lines[0].endswith('\n'):
658 659 self.eol = '\n'
659 660 if eolmode != 'strict':
660 661 nlines = []
661 662 for l in self.lines:
662 663 if l.endswith('\r\n'):
663 664 l = l[:-2] + '\n'
664 665 nlines.append(l)
665 666 self.lines = nlines
666 667 else:
667 668 if self.create:
668 669 self.missing = False
669 670 if self.mode is None:
670 671 self.mode = (False, False)
671 672 if self.missing:
672 673 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
673 674 self.ui.warn(_("(use '--prefix' to apply patch relative to the "
674 675 "current directory)\n"))
675 676
676 677 self.hash = {}
677 678 self.dirty = 0
678 679 self.offset = 0
679 680 self.skew = 0
680 681 self.rej = []
681 682 self.fileprinted = False
682 683 self.printfile(False)
683 684 self.hunks = 0
684 685
685 686 def writelines(self, fname, lines, mode):
686 687 if self.eolmode == 'auto':
687 688 eol = self.eol
688 689 elif self.eolmode == 'crlf':
689 690 eol = '\r\n'
690 691 else:
691 692 eol = '\n'
692 693
693 694 if self.eolmode != 'strict' and eol and eol != '\n':
694 695 rawlines = []
695 696 for l in lines:
696 697 if l and l[-1] == '\n':
697 698 l = l[:-1] + eol
698 699 rawlines.append(l)
699 700 lines = rawlines
700 701
701 702 self.backend.setfile(fname, ''.join(lines), mode, self.copysource)
702 703
703 704 def printfile(self, warn):
704 705 if self.fileprinted:
705 706 return
706 707 if warn or self.ui.verbose:
707 708 self.fileprinted = True
708 709 s = _("patching file %s\n") % self.fname
709 710 if warn:
710 711 self.ui.warn(s)
711 712 else:
712 713 self.ui.note(s)
713 714
714 715
715 716 def findlines(self, l, linenum):
716 717 # looks through the hash and finds candidate lines. The
717 718 # result is a list of line numbers sorted based on distance
718 719 # from linenum
719 720
720 721 cand = self.hash.get(l, [])
721 722 if len(cand) > 1:
722 723 # resort our list of potentials forward then back.
723 724 cand.sort(key=lambda x: abs(x - linenum))
724 725 return cand
725 726
726 727 def write_rej(self):
727 728 # our rejects are a little different from patch(1). This always
728 729 # creates rejects in the same form as the original patch. A file
729 730 # header is inserted so that you can run the reject through patch again
730 731 # without having to type the filename.
731 732 if not self.rej:
732 733 return
733 734 base = os.path.basename(self.fname)
734 735 lines = ["--- %s\n+++ %s\n" % (base, base)]
735 736 for x in self.rej:
736 737 for l in x.hunk:
737 738 lines.append(l)
738 739 if l[-1] != '\n':
739 740 lines.append("\n\ No newline at end of file\n")
740 741 self.backend.writerej(self.fname, len(self.rej), self.hunks, lines)
741 742
742 743 def apply(self, h):
743 744 if not h.complete():
744 745 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
745 746 (h.number, h.desc, len(h.a), h.lena, len(h.b),
746 747 h.lenb))
747 748
748 749 self.hunks += 1
749 750
750 751 if self.missing:
751 752 self.rej.append(h)
752 753 return -1
753 754
754 755 if self.exists and self.create:
755 756 if self.copysource:
756 757 self.ui.warn(_("cannot create %s: destination already "
757 758 "exists\n") % self.fname)
758 759 else:
759 760 self.ui.warn(_("file %s already exists\n") % self.fname)
760 761 self.rej.append(h)
761 762 return -1
762 763
763 764 if isinstance(h, binhunk):
764 765 if self.remove:
765 766 self.backend.unlink(self.fname)
766 767 else:
767 768 l = h.new(self.lines)
768 769 self.lines[:] = l
769 770 self.offset += len(l)
770 771 self.dirty = True
771 772 return 0
772 773
773 774 horig = h
774 775 if (self.eolmode in ('crlf', 'lf')
775 776 or self.eolmode == 'auto' and self.eol):
776 777 # If new eols are going to be normalized, then normalize
777 778 # hunk data before patching. Otherwise, preserve input
778 779 # line-endings.
779 780 h = h.getnormalized()
780 781
781 782 # fast case first, no offsets, no fuzz
782 783 old, oldstart, new, newstart = h.fuzzit(0, False)
783 784 oldstart += self.offset
784 785 orig_start = oldstart
785 786 # if there's skew we want to emit the "(offset %d lines)" even
786 787 # when the hunk cleanly applies at start + skew, so skip the
787 788 # fast case code
788 789 if (self.skew == 0 and
789 790 diffhelpers.testhunk(old, self.lines, oldstart) == 0):
790 791 if self.remove:
791 792 self.backend.unlink(self.fname)
792 793 else:
793 794 self.lines[oldstart:oldstart + len(old)] = new
794 795 self.offset += len(new) - len(old)
795 796 self.dirty = True
796 797 return 0
797 798
798 799 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
799 800 self.hash = {}
800 801 for x, s in enumerate(self.lines):
801 802 self.hash.setdefault(s, []).append(x)
802 803
803 804 for fuzzlen in xrange(self.ui.configint("patch", "fuzz", 2) + 1):
804 805 for toponly in [True, False]:
805 806 old, oldstart, new, newstart = h.fuzzit(fuzzlen, toponly)
806 807 oldstart = oldstart + self.offset + self.skew
807 808 oldstart = min(oldstart, len(self.lines))
808 809 if old:
809 810 cand = self.findlines(old[0][1:], oldstart)
810 811 else:
811 812 # Only adding lines with no or fuzzed context, just
812 813 # take the skew in account
813 814 cand = [oldstart]
814 815
815 816 for l in cand:
816 817 if not old or diffhelpers.testhunk(old, self.lines, l) == 0:
817 818 self.lines[l : l + len(old)] = new
818 819 self.offset += len(new) - len(old)
819 820 self.skew = l - orig_start
820 821 self.dirty = True
821 822 offset = l - orig_start - fuzzlen
822 823 if fuzzlen:
823 824 msg = _("Hunk #%d succeeded at %d "
824 825 "with fuzz %d "
825 826 "(offset %d lines).\n")
826 827 self.printfile(True)
827 828 self.ui.warn(msg %
828 829 (h.number, l + 1, fuzzlen, offset))
829 830 else:
830 831 msg = _("Hunk #%d succeeded at %d "
831 832 "(offset %d lines).\n")
832 833 self.ui.note(msg % (h.number, l + 1, offset))
833 834 return fuzzlen
834 835 self.printfile(True)
835 836 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
836 837 self.rej.append(horig)
837 838 return -1
838 839
839 840 def close(self):
840 841 if self.dirty:
841 842 self.writelines(self.fname, self.lines, self.mode)
842 843 self.write_rej()
843 844 return len(self.rej)
844 845
845 846 class header(object):
846 847 """patch header
847 848 """
848 849 diffgit_re = re.compile('diff --git a/(.*) b/(.*)$')
849 850 diff_re = re.compile('diff -r .* (.*)$')
850 851 allhunks_re = re.compile('(?:index|deleted file) ')
851 852 pretty_re = re.compile('(?:new file|deleted file) ')
852 853 special_re = re.compile('(?:index|deleted|copy|rename) ')
853 854 newfile_re = re.compile('(?:new file)')
854 855
855 856 def __init__(self, header):
856 857 self.header = header
857 858 self.hunks = []
858 859
859 860 def binary(self):
860 861 return any(h.startswith('index ') for h in self.header)
861 862
862 863 def pretty(self, fp):
863 864 for h in self.header:
864 865 if h.startswith('index '):
865 866 fp.write(_('this modifies a binary file (all or nothing)\n'))
866 867 break
867 868 if self.pretty_re.match(h):
868 869 fp.write(h)
869 870 if self.binary():
870 871 fp.write(_('this is a binary file\n'))
871 872 break
872 873 if h.startswith('---'):
873 874 fp.write(_('%d hunks, %d lines changed\n') %
874 875 (len(self.hunks),
875 876 sum([max(h.added, h.removed) for h in self.hunks])))
876 877 break
877 878 fp.write(h)
878 879
879 880 def write(self, fp):
880 881 fp.write(''.join(self.header))
881 882
882 883 def allhunks(self):
883 884 return any(self.allhunks_re.match(h) for h in self.header)
884 885
885 886 def files(self):
886 887 match = self.diffgit_re.match(self.header[0])
887 888 if match:
888 889 fromfile, tofile = match.groups()
889 890 if fromfile == tofile:
890 891 return [fromfile]
891 892 return [fromfile, tofile]
892 893 else:
893 894 return self.diff_re.match(self.header[0]).groups()
894 895
895 896 def filename(self):
896 897 return self.files()[-1]
897 898
898 899 def __repr__(self):
899 900 return '<header %s>' % (' '.join(map(repr, self.files())))
900 901
901 902 def isnewfile(self):
902 903 return any(self.newfile_re.match(h) for h in self.header)
903 904
904 905 def special(self):
905 906 # Special files are shown only at the header level and not at the hunk
906 907 # level for example a file that has been deleted is a special file.
907 908 # The user cannot change the content of the operation, in the case of
908 909 # the deleted file he has to take the deletion or not take it, he
909 910 # cannot take some of it.
910 911 # Newly added files are special if they are empty, they are not special
911 912 # if they have some content as we want to be able to change it
912 913 nocontent = len(self.header) == 2
913 914 emptynewfile = self.isnewfile() and nocontent
914 915 return emptynewfile or \
915 916 any(self.special_re.match(h) for h in self.header)
916 917
917 918 class recordhunk(object):
918 919 """patch hunk
919 920
920 921 XXX shouldn't we merge this with the other hunk class?
921 922 """
922 923 maxcontext = 3
923 924
924 925 def __init__(self, header, fromline, toline, proc, before, hunk, after):
925 926 def trimcontext(number, lines):
926 927 delta = len(lines) - self.maxcontext
927 928 if False and delta > 0:
928 929 return number + delta, lines[:self.maxcontext]
929 930 return number, lines
930 931
931 932 self.header = header
932 933 self.fromline, self.before = trimcontext(fromline, before)
933 934 self.toline, self.after = trimcontext(toline, after)
934 935 self.proc = proc
935 936 self.hunk = hunk
936 937 self.added, self.removed = self.countchanges(self.hunk)
937 938
938 939 def __eq__(self, v):
939 940 if not isinstance(v, recordhunk):
940 941 return False
941 942
942 943 return ((v.hunk == self.hunk) and
943 944 (v.proc == self.proc) and
944 945 (self.fromline == v.fromline) and
945 946 (self.header.files() == v.header.files()))
946 947
947 948 def __hash__(self):
948 949 return hash((tuple(self.hunk),
949 950 tuple(self.header.files()),
950 951 self.fromline,
951 952 self.proc))
952 953
953 954 def countchanges(self, hunk):
954 955 """hunk -> (n+,n-)"""
955 956 add = len([h for h in hunk if h[0] == '+'])
956 957 rem = len([h for h in hunk if h[0] == '-'])
957 958 return add, rem
958 959
959 960 def write(self, fp):
960 961 delta = len(self.before) + len(self.after)
961 962 if self.after and self.after[-1] == '\\ No newline at end of file\n':
962 963 delta -= 1
963 964 fromlen = delta + self.removed
964 965 tolen = delta + self.added
965 966 fp.write('@@ -%d,%d +%d,%d @@%s\n' %
966 967 (self.fromline, fromlen, self.toline, tolen,
967 968 self.proc and (' ' + self.proc)))
968 969 fp.write(''.join(self.before + self.hunk + self.after))
969 970
970 971 pretty = write
971 972
972 973 def filename(self):
973 974 return self.header.filename()
974 975
975 976 def __repr__(self):
976 977 return '<hunk %r@%d>' % (self.filename(), self.fromline)
977 978
978 979 def filterpatch(ui, headers, operation=None):
979 980 """Interactively filter patch chunks into applied-only chunks"""
980 981 if operation is None:
981 982 operation = 'record'
982 983 messages = {
983 984 'multiple': {
984 985 'discard': _("discard change %d/%d to '%s'?"),
985 986 'record': _("record change %d/%d to '%s'?"),
986 987 'revert': _("revert change %d/%d to '%s'?"),
987 988 }[operation],
988 989 'single': {
989 990 'discard': _("discard this change to '%s'?"),
990 991 'record': _("record this change to '%s'?"),
991 992 'revert': _("revert this change to '%s'?"),
992 993 }[operation],
993 994 }
994 995
995 996 def prompt(skipfile, skipall, query, chunk):
996 997 """prompt query, and process base inputs
997 998
998 999 - y/n for the rest of file
999 1000 - y/n for the rest
1000 1001 - ? (help)
1001 1002 - q (quit)
1002 1003
1003 1004 Return True/False and possibly updated skipfile and skipall.
1004 1005 """
1005 1006 newpatches = None
1006 1007 if skipall is not None:
1007 1008 return skipall, skipfile, skipall, newpatches
1008 1009 if skipfile is not None:
1009 1010 return skipfile, skipfile, skipall, newpatches
1010 1011 while True:
1011 1012 resps = _('[Ynesfdaq?]'
1012 1013 '$$ &Yes, record this change'
1013 1014 '$$ &No, skip this change'
1014 1015 '$$ &Edit this change manually'
1015 1016 '$$ &Skip remaining changes to this file'
1016 1017 '$$ Record remaining changes to this &file'
1017 1018 '$$ &Done, skip remaining changes and files'
1018 1019 '$$ Record &all changes to all remaining files'
1019 1020 '$$ &Quit, recording no changes'
1020 1021 '$$ &? (display help)')
1021 1022 r = ui.promptchoice("%s %s" % (query, resps))
1022 1023 ui.write("\n")
1023 1024 if r == 8: # ?
1024 1025 for c, t in ui.extractchoices(resps)[1]:
1025 1026 ui.write('%s - %s\n' % (c, encoding.lower(t)))
1026 1027 continue
1027 1028 elif r == 0: # yes
1028 1029 ret = True
1029 1030 elif r == 1: # no
1030 1031 ret = False
1031 1032 elif r == 2: # Edit patch
1032 1033 if chunk is None:
1033 1034 ui.write(_('cannot edit patch for whole file'))
1034 1035 ui.write("\n")
1035 1036 continue
1036 1037 if chunk.header.binary():
1037 1038 ui.write(_('cannot edit patch for binary file'))
1038 1039 ui.write("\n")
1039 1040 continue
1040 1041 # Patch comment based on the Git one (based on comment at end of
1041 1042 # https://mercurial-scm.org/wiki/RecordExtension)
1042 1043 phelp = '---' + _("""
1043 1044 To remove '-' lines, make them ' ' lines (context).
1044 1045 To remove '+' lines, delete them.
1045 1046 Lines starting with # will be removed from the patch.
1046 1047
1047 1048 If the patch applies cleanly, the edited hunk will immediately be
1048 1049 added to the record list. If it does not apply cleanly, a rejects
1049 1050 file will be generated: you can use that when you try again. If
1050 1051 all lines of the hunk are removed, then the edit is aborted and
1051 1052 the hunk is left unchanged.
1052 1053 """)
1053 1054 (patchfd, patchfn) = tempfile.mkstemp(prefix="hg-editor-",
1054 1055 suffix=".diff", text=True)
1055 1056 ncpatchfp = None
1056 1057 try:
1057 1058 # Write the initial patch
1058 f = os.fdopen(patchfd, "w")
1059 f = os.fdopen(patchfd, pycompat.sysstr("w"))
1059 1060 chunk.header.write(f)
1060 1061 chunk.write(f)
1061 1062 f.write('\n'.join(['# ' + i for i in phelp.splitlines()]))
1062 1063 f.close()
1063 1064 # Start the editor and wait for it to complete
1064 1065 editor = ui.geteditor()
1065 1066 ret = ui.system("%s \"%s\"" % (editor, patchfn),
1066 1067 environ={'HGUSER': ui.username()})
1067 1068 if ret != 0:
1068 1069 ui.warn(_("editor exited with exit code %d\n") % ret)
1069 1070 continue
1070 1071 # Remove comment lines
1071 1072 patchfp = open(patchfn)
1072 1073 ncpatchfp = stringio()
1073 1074 for line in util.iterfile(patchfp):
1074 1075 if not line.startswith('#'):
1075 1076 ncpatchfp.write(line)
1076 1077 patchfp.close()
1077 1078 ncpatchfp.seek(0)
1078 1079 newpatches = parsepatch(ncpatchfp)
1079 1080 finally:
1080 1081 os.unlink(patchfn)
1081 1082 del ncpatchfp
1082 1083 # Signal that the chunk shouldn't be applied as-is, but
1083 1084 # provide the new patch to be used instead.
1084 1085 ret = False
1085 1086 elif r == 3: # Skip
1086 1087 ret = skipfile = False
1087 1088 elif r == 4: # file (Record remaining)
1088 1089 ret = skipfile = True
1089 1090 elif r == 5: # done, skip remaining
1090 1091 ret = skipall = False
1091 1092 elif r == 6: # all
1092 1093 ret = skipall = True
1093 1094 elif r == 7: # quit
1094 1095 raise error.Abort(_('user quit'))
1095 1096 return ret, skipfile, skipall, newpatches
1096 1097
1097 1098 seen = set()
1098 1099 applied = {} # 'filename' -> [] of chunks
1099 1100 skipfile, skipall = None, None
1100 1101 pos, total = 1, sum(len(h.hunks) for h in headers)
1101 1102 for h in headers:
1102 1103 pos += len(h.hunks)
1103 1104 skipfile = None
1104 1105 fixoffset = 0
1105 1106 hdr = ''.join(h.header)
1106 1107 if hdr in seen:
1107 1108 continue
1108 1109 seen.add(hdr)
1109 1110 if skipall is None:
1110 1111 h.pretty(ui)
1111 1112 msg = (_('examine changes to %s?') %
1112 1113 _(' and ').join("'%s'" % f for f in h.files()))
1113 1114 r, skipfile, skipall, np = prompt(skipfile, skipall, msg, None)
1114 1115 if not r:
1115 1116 continue
1116 1117 applied[h.filename()] = [h]
1117 1118 if h.allhunks():
1118 1119 applied[h.filename()] += h.hunks
1119 1120 continue
1120 1121 for i, chunk in enumerate(h.hunks):
1121 1122 if skipfile is None and skipall is None:
1122 1123 chunk.pretty(ui)
1123 1124 if total == 1:
1124 1125 msg = messages['single'] % chunk.filename()
1125 1126 else:
1126 1127 idx = pos - len(h.hunks) + i
1127 1128 msg = messages['multiple'] % (idx, total, chunk.filename())
1128 1129 r, skipfile, skipall, newpatches = prompt(skipfile,
1129 1130 skipall, msg, chunk)
1130 1131 if r:
1131 1132 if fixoffset:
1132 1133 chunk = copy.copy(chunk)
1133 1134 chunk.toline += fixoffset
1134 1135 applied[chunk.filename()].append(chunk)
1135 1136 elif newpatches is not None:
1136 1137 for newpatch in newpatches:
1137 1138 for newhunk in newpatch.hunks:
1138 1139 if fixoffset:
1139 1140 newhunk.toline += fixoffset
1140 1141 applied[newhunk.filename()].append(newhunk)
1141 1142 else:
1142 1143 fixoffset += chunk.removed - chunk.added
1143 1144 return (sum([h for h in applied.itervalues()
1144 1145 if h[0].special() or len(h) > 1], []), {})
1145 1146 class hunk(object):
1146 1147 def __init__(self, desc, num, lr, context):
1147 1148 self.number = num
1148 1149 self.desc = desc
1149 1150 self.hunk = [desc]
1150 1151 self.a = []
1151 1152 self.b = []
1152 1153 self.starta = self.lena = None
1153 1154 self.startb = self.lenb = None
1154 1155 if lr is not None:
1155 1156 if context:
1156 1157 self.read_context_hunk(lr)
1157 1158 else:
1158 1159 self.read_unified_hunk(lr)
1159 1160
1160 1161 def getnormalized(self):
1161 1162 """Return a copy with line endings normalized to LF."""
1162 1163
1163 1164 def normalize(lines):
1164 1165 nlines = []
1165 1166 for line in lines:
1166 1167 if line.endswith('\r\n'):
1167 1168 line = line[:-2] + '\n'
1168 1169 nlines.append(line)
1169 1170 return nlines
1170 1171
1171 1172 # Dummy object, it is rebuilt manually
1172 1173 nh = hunk(self.desc, self.number, None, None)
1173 1174 nh.number = self.number
1174 1175 nh.desc = self.desc
1175 1176 nh.hunk = self.hunk
1176 1177 nh.a = normalize(self.a)
1177 1178 nh.b = normalize(self.b)
1178 1179 nh.starta = self.starta
1179 1180 nh.startb = self.startb
1180 1181 nh.lena = self.lena
1181 1182 nh.lenb = self.lenb
1182 1183 return nh
1183 1184
1184 1185 def read_unified_hunk(self, lr):
1185 1186 m = unidesc.match(self.desc)
1186 1187 if not m:
1187 1188 raise PatchError(_("bad hunk #%d") % self.number)
1188 1189 self.starta, self.lena, self.startb, self.lenb = m.groups()
1189 1190 if self.lena is None:
1190 1191 self.lena = 1
1191 1192 else:
1192 1193 self.lena = int(self.lena)
1193 1194 if self.lenb is None:
1194 1195 self.lenb = 1
1195 1196 else:
1196 1197 self.lenb = int(self.lenb)
1197 1198 self.starta = int(self.starta)
1198 1199 self.startb = int(self.startb)
1199 1200 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a,
1200 1201 self.b)
1201 1202 # if we hit eof before finishing out the hunk, the last line will
1202 1203 # be zero length. Lets try to fix it up.
1203 1204 while len(self.hunk[-1]) == 0:
1204 1205 del self.hunk[-1]
1205 1206 del self.a[-1]
1206 1207 del self.b[-1]
1207 1208 self.lena -= 1
1208 1209 self.lenb -= 1
1209 1210 self._fixnewline(lr)
1210 1211
1211 1212 def read_context_hunk(self, lr):
1212 1213 self.desc = lr.readline()
1213 1214 m = contextdesc.match(self.desc)
1214 1215 if not m:
1215 1216 raise PatchError(_("bad hunk #%d") % self.number)
1216 1217 self.starta, aend = m.groups()
1217 1218 self.starta = int(self.starta)
1218 1219 if aend is None:
1219 1220 aend = self.starta
1220 1221 self.lena = int(aend) - self.starta
1221 1222 if self.starta:
1222 1223 self.lena += 1
1223 1224 for x in xrange(self.lena):
1224 1225 l = lr.readline()
1225 1226 if l.startswith('---'):
1226 1227 # lines addition, old block is empty
1227 1228 lr.push(l)
1228 1229 break
1229 1230 s = l[2:]
1230 1231 if l.startswith('- ') or l.startswith('! '):
1231 1232 u = '-' + s
1232 1233 elif l.startswith(' '):
1233 1234 u = ' ' + s
1234 1235 else:
1235 1236 raise PatchError(_("bad hunk #%d old text line %d") %
1236 1237 (self.number, x))
1237 1238 self.a.append(u)
1238 1239 self.hunk.append(u)
1239 1240
1240 1241 l = lr.readline()
1241 1242 if l.startswith('\ '):
1242 1243 s = self.a[-1][:-1]
1243 1244 self.a[-1] = s
1244 1245 self.hunk[-1] = s
1245 1246 l = lr.readline()
1246 1247 m = contextdesc.match(l)
1247 1248 if not m:
1248 1249 raise PatchError(_("bad hunk #%d") % self.number)
1249 1250 self.startb, bend = m.groups()
1250 1251 self.startb = int(self.startb)
1251 1252 if bend is None:
1252 1253 bend = self.startb
1253 1254 self.lenb = int(bend) - self.startb
1254 1255 if self.startb:
1255 1256 self.lenb += 1
1256 1257 hunki = 1
1257 1258 for x in xrange(self.lenb):
1258 1259 l = lr.readline()
1259 1260 if l.startswith('\ '):
1260 1261 # XXX: the only way to hit this is with an invalid line range.
1261 1262 # The no-eol marker is not counted in the line range, but I
1262 1263 # guess there are diff(1) out there which behave differently.
1263 1264 s = self.b[-1][:-1]
1264 1265 self.b[-1] = s
1265 1266 self.hunk[hunki - 1] = s
1266 1267 continue
1267 1268 if not l:
1268 1269 # line deletions, new block is empty and we hit EOF
1269 1270 lr.push(l)
1270 1271 break
1271 1272 s = l[2:]
1272 1273 if l.startswith('+ ') or l.startswith('! '):
1273 1274 u = '+' + s
1274 1275 elif l.startswith(' '):
1275 1276 u = ' ' + s
1276 1277 elif len(self.b) == 0:
1277 1278 # line deletions, new block is empty
1278 1279 lr.push(l)
1279 1280 break
1280 1281 else:
1281 1282 raise PatchError(_("bad hunk #%d old text line %d") %
1282 1283 (self.number, x))
1283 1284 self.b.append(s)
1284 1285 while True:
1285 1286 if hunki >= len(self.hunk):
1286 1287 h = ""
1287 1288 else:
1288 1289 h = self.hunk[hunki]
1289 1290 hunki += 1
1290 1291 if h == u:
1291 1292 break
1292 1293 elif h.startswith('-'):
1293 1294 continue
1294 1295 else:
1295 1296 self.hunk.insert(hunki - 1, u)
1296 1297 break
1297 1298
1298 1299 if not self.a:
1299 1300 # this happens when lines were only added to the hunk
1300 1301 for x in self.hunk:
1301 1302 if x.startswith('-') or x.startswith(' '):
1302 1303 self.a.append(x)
1303 1304 if not self.b:
1304 1305 # this happens when lines were only deleted from the hunk
1305 1306 for x in self.hunk:
1306 1307 if x.startswith('+') or x.startswith(' '):
1307 1308 self.b.append(x[1:])
1308 1309 # @@ -start,len +start,len @@
1309 1310 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
1310 1311 self.startb, self.lenb)
1311 1312 self.hunk[0] = self.desc
1312 1313 self._fixnewline(lr)
1313 1314
1314 1315 def _fixnewline(self, lr):
1315 1316 l = lr.readline()
1316 1317 if l.startswith('\ '):
1317 1318 diffhelpers.fix_newline(self.hunk, self.a, self.b)
1318 1319 else:
1319 1320 lr.push(l)
1320 1321
1321 1322 def complete(self):
1322 1323 return len(self.a) == self.lena and len(self.b) == self.lenb
1323 1324
1324 1325 def _fuzzit(self, old, new, fuzz, toponly):
1325 1326 # this removes context lines from the top and bottom of list 'l'. It
1326 1327 # checks the hunk to make sure only context lines are removed, and then
1327 1328 # returns a new shortened list of lines.
1328 1329 fuzz = min(fuzz, len(old))
1329 1330 if fuzz:
1330 1331 top = 0
1331 1332 bot = 0
1332 1333 hlen = len(self.hunk)
1333 1334 for x in xrange(hlen - 1):
1334 1335 # the hunk starts with the @@ line, so use x+1
1335 1336 if self.hunk[x + 1][0] == ' ':
1336 1337 top += 1
1337 1338 else:
1338 1339 break
1339 1340 if not toponly:
1340 1341 for x in xrange(hlen - 1):
1341 1342 if self.hunk[hlen - bot - 1][0] == ' ':
1342 1343 bot += 1
1343 1344 else:
1344 1345 break
1345 1346
1346 1347 bot = min(fuzz, bot)
1347 1348 top = min(fuzz, top)
1348 1349 return old[top:len(old) - bot], new[top:len(new) - bot], top
1349 1350 return old, new, 0
1350 1351
1351 1352 def fuzzit(self, fuzz, toponly):
1352 1353 old, new, top = self._fuzzit(self.a, self.b, fuzz, toponly)
1353 1354 oldstart = self.starta + top
1354 1355 newstart = self.startb + top
1355 1356 # zero length hunk ranges already have their start decremented
1356 1357 if self.lena and oldstart > 0:
1357 1358 oldstart -= 1
1358 1359 if self.lenb and newstart > 0:
1359 1360 newstart -= 1
1360 1361 return old, oldstart, new, newstart
1361 1362
1362 1363 class binhunk(object):
1363 1364 'A binary patch file.'
1364 1365 def __init__(self, lr, fname):
1365 1366 self.text = None
1366 1367 self.delta = False
1367 1368 self.hunk = ['GIT binary patch\n']
1368 1369 self._fname = fname
1369 1370 self._read(lr)
1370 1371
1371 1372 def complete(self):
1372 1373 return self.text is not None
1373 1374
1374 1375 def new(self, lines):
1375 1376 if self.delta:
1376 1377 return [applybindelta(self.text, ''.join(lines))]
1377 1378 return [self.text]
1378 1379
1379 1380 def _read(self, lr):
1380 1381 def getline(lr, hunk):
1381 1382 l = lr.readline()
1382 1383 hunk.append(l)
1383 1384 return l.rstrip('\r\n')
1384 1385
1385 1386 size = 0
1386 1387 while True:
1387 1388 line = getline(lr, self.hunk)
1388 1389 if not line:
1389 1390 raise PatchError(_('could not extract "%s" binary data')
1390 1391 % self._fname)
1391 1392 if line.startswith('literal '):
1392 1393 size = int(line[8:].rstrip())
1393 1394 break
1394 1395 if line.startswith('delta '):
1395 1396 size = int(line[6:].rstrip())
1396 1397 self.delta = True
1397 1398 break
1398 1399 dec = []
1399 1400 line = getline(lr, self.hunk)
1400 1401 while len(line) > 1:
1401 1402 l = line[0]
1402 1403 if l <= 'Z' and l >= 'A':
1403 1404 l = ord(l) - ord('A') + 1
1404 1405 else:
1405 1406 l = ord(l) - ord('a') + 27
1406 1407 try:
1407 1408 dec.append(base85.b85decode(line[1:])[:l])
1408 1409 except ValueError as e:
1409 1410 raise PatchError(_('could not decode "%s" binary patch: %s')
1410 1411 % (self._fname, str(e)))
1411 1412 line = getline(lr, self.hunk)
1412 1413 text = zlib.decompress(''.join(dec))
1413 1414 if len(text) != size:
1414 1415 raise PatchError(_('"%s" length is %d bytes, should be %d')
1415 1416 % (self._fname, len(text), size))
1416 1417 self.text = text
1417 1418
1418 1419 def parsefilename(str):
1419 1420 # --- filename \t|space stuff
1420 1421 s = str[4:].rstrip('\r\n')
1421 1422 i = s.find('\t')
1422 1423 if i < 0:
1423 1424 i = s.find(' ')
1424 1425 if i < 0:
1425 1426 return s
1426 1427 return s[:i]
1427 1428
1428 1429 def reversehunks(hunks):
1429 1430 '''reverse the signs in the hunks given as argument
1430 1431
1431 1432 This function operates on hunks coming out of patch.filterpatch, that is
1432 1433 a list of the form: [header1, hunk1, hunk2, header2...]. Example usage:
1433 1434
1434 1435 >>> rawpatch = """diff --git a/folder1/g b/folder1/g
1435 1436 ... --- a/folder1/g
1436 1437 ... +++ b/folder1/g
1437 1438 ... @@ -1,7 +1,7 @@
1438 1439 ... +firstline
1439 1440 ... c
1440 1441 ... 1
1441 1442 ... 2
1442 1443 ... + 3
1443 1444 ... -4
1444 1445 ... 5
1445 1446 ... d
1446 1447 ... +lastline"""
1447 1448 >>> hunks = parsepatch(rawpatch)
1448 1449 >>> hunkscomingfromfilterpatch = []
1449 1450 >>> for h in hunks:
1450 1451 ... hunkscomingfromfilterpatch.append(h)
1451 1452 ... hunkscomingfromfilterpatch.extend(h.hunks)
1452 1453
1453 1454 >>> reversedhunks = reversehunks(hunkscomingfromfilterpatch)
1454 1455 >>> from . import util
1455 1456 >>> fp = util.stringio()
1456 1457 >>> for c in reversedhunks:
1457 1458 ... c.write(fp)
1458 1459 >>> fp.seek(0)
1459 1460 >>> reversedpatch = fp.read()
1460 1461 >>> print reversedpatch
1461 1462 diff --git a/folder1/g b/folder1/g
1462 1463 --- a/folder1/g
1463 1464 +++ b/folder1/g
1464 1465 @@ -1,4 +1,3 @@
1465 1466 -firstline
1466 1467 c
1467 1468 1
1468 1469 2
1469 1470 @@ -1,6 +2,6 @@
1470 1471 c
1471 1472 1
1472 1473 2
1473 1474 - 3
1474 1475 +4
1475 1476 5
1476 1477 d
1477 1478 @@ -5,3 +6,2 @@
1478 1479 5
1479 1480 d
1480 1481 -lastline
1481 1482
1482 1483 '''
1483 1484
1484 1485 from . import crecord as crecordmod
1485 1486 newhunks = []
1486 1487 for c in hunks:
1487 1488 if isinstance(c, crecordmod.uihunk):
1488 1489 # curses hunks encapsulate the record hunk in _hunk
1489 1490 c = c._hunk
1490 1491 if isinstance(c, recordhunk):
1491 1492 for j, line in enumerate(c.hunk):
1492 1493 if line.startswith("-"):
1493 1494 c.hunk[j] = "+" + c.hunk[j][1:]
1494 1495 elif line.startswith("+"):
1495 1496 c.hunk[j] = "-" + c.hunk[j][1:]
1496 1497 c.added, c.removed = c.removed, c.added
1497 1498 newhunks.append(c)
1498 1499 return newhunks
1499 1500
1500 1501 def parsepatch(originalchunks):
1501 1502 """patch -> [] of headers -> [] of hunks """
1502 1503 class parser(object):
1503 1504 """patch parsing state machine"""
1504 1505 def __init__(self):
1505 1506 self.fromline = 0
1506 1507 self.toline = 0
1507 1508 self.proc = ''
1508 1509 self.header = None
1509 1510 self.context = []
1510 1511 self.before = []
1511 1512 self.hunk = []
1512 1513 self.headers = []
1513 1514
1514 1515 def addrange(self, limits):
1515 1516 fromstart, fromend, tostart, toend, proc = limits
1516 1517 self.fromline = int(fromstart)
1517 1518 self.toline = int(tostart)
1518 1519 self.proc = proc
1519 1520
1520 1521 def addcontext(self, context):
1521 1522 if self.hunk:
1522 1523 h = recordhunk(self.header, self.fromline, self.toline,
1523 1524 self.proc, self.before, self.hunk, context)
1524 1525 self.header.hunks.append(h)
1525 1526 self.fromline += len(self.before) + h.removed
1526 1527 self.toline += len(self.before) + h.added
1527 1528 self.before = []
1528 1529 self.hunk = []
1529 1530 self.context = context
1530 1531
1531 1532 def addhunk(self, hunk):
1532 1533 if self.context:
1533 1534 self.before = self.context
1534 1535 self.context = []
1535 1536 self.hunk = hunk
1536 1537
1537 1538 def newfile(self, hdr):
1538 1539 self.addcontext([])
1539 1540 h = header(hdr)
1540 1541 self.headers.append(h)
1541 1542 self.header = h
1542 1543
1543 1544 def addother(self, line):
1544 1545 pass # 'other' lines are ignored
1545 1546
1546 1547 def finished(self):
1547 1548 self.addcontext([])
1548 1549 return self.headers
1549 1550
1550 1551 transitions = {
1551 1552 'file': {'context': addcontext,
1552 1553 'file': newfile,
1553 1554 'hunk': addhunk,
1554 1555 'range': addrange},
1555 1556 'context': {'file': newfile,
1556 1557 'hunk': addhunk,
1557 1558 'range': addrange,
1558 1559 'other': addother},
1559 1560 'hunk': {'context': addcontext,
1560 1561 'file': newfile,
1561 1562 'range': addrange},
1562 1563 'range': {'context': addcontext,
1563 1564 'hunk': addhunk},
1564 1565 'other': {'other': addother},
1565 1566 }
1566 1567
1567 1568 p = parser()
1568 1569 fp = stringio()
1569 1570 fp.write(''.join(originalchunks))
1570 1571 fp.seek(0)
1571 1572
1572 1573 state = 'context'
1573 1574 for newstate, data in scanpatch(fp):
1574 1575 try:
1575 1576 p.transitions[state][newstate](p, data)
1576 1577 except KeyError:
1577 1578 raise PatchError('unhandled transition: %s -> %s' %
1578 1579 (state, newstate))
1579 1580 state = newstate
1580 1581 del fp
1581 1582 return p.finished()
1582 1583
1583 1584 def pathtransform(path, strip, prefix):
1584 1585 '''turn a path from a patch into a path suitable for the repository
1585 1586
1586 1587 prefix, if not empty, is expected to be normalized with a / at the end.
1587 1588
1588 1589 Returns (stripped components, path in repository).
1589 1590
1590 1591 >>> pathtransform('a/b/c', 0, '')
1591 1592 ('', 'a/b/c')
1592 1593 >>> pathtransform(' a/b/c ', 0, '')
1593 1594 ('', ' a/b/c')
1594 1595 >>> pathtransform(' a/b/c ', 2, '')
1595 1596 ('a/b/', 'c')
1596 1597 >>> pathtransform('a/b/c', 0, 'd/e/')
1597 1598 ('', 'd/e/a/b/c')
1598 1599 >>> pathtransform(' a//b/c ', 2, 'd/e/')
1599 1600 ('a//b/', 'd/e/c')
1600 1601 >>> pathtransform('a/b/c', 3, '')
1601 1602 Traceback (most recent call last):
1602 1603 PatchError: unable to strip away 1 of 3 dirs from a/b/c
1603 1604 '''
1604 1605 pathlen = len(path)
1605 1606 i = 0
1606 1607 if strip == 0:
1607 1608 return '', prefix + path.rstrip()
1608 1609 count = strip
1609 1610 while count > 0:
1610 1611 i = path.find('/', i)
1611 1612 if i == -1:
1612 1613 raise PatchError(_("unable to strip away %d of %d dirs from %s") %
1613 1614 (count, strip, path))
1614 1615 i += 1
1615 1616 # consume '//' in the path
1616 1617 while i < pathlen - 1 and path[i] == '/':
1617 1618 i += 1
1618 1619 count -= 1
1619 1620 return path[:i].lstrip(), prefix + path[i:].rstrip()
1620 1621
1621 1622 def makepatchmeta(backend, afile_orig, bfile_orig, hunk, strip, prefix):
1622 1623 nulla = afile_orig == "/dev/null"
1623 1624 nullb = bfile_orig == "/dev/null"
1624 1625 create = nulla and hunk.starta == 0 and hunk.lena == 0
1625 1626 remove = nullb and hunk.startb == 0 and hunk.lenb == 0
1626 1627 abase, afile = pathtransform(afile_orig, strip, prefix)
1627 1628 gooda = not nulla and backend.exists(afile)
1628 1629 bbase, bfile = pathtransform(bfile_orig, strip, prefix)
1629 1630 if afile == bfile:
1630 1631 goodb = gooda
1631 1632 else:
1632 1633 goodb = not nullb and backend.exists(bfile)
1633 1634 missing = not goodb and not gooda and not create
1634 1635
1635 1636 # some diff programs apparently produce patches where the afile is
1636 1637 # not /dev/null, but afile starts with bfile
1637 1638 abasedir = afile[:afile.rfind('/') + 1]
1638 1639 bbasedir = bfile[:bfile.rfind('/') + 1]
1639 1640 if (missing and abasedir == bbasedir and afile.startswith(bfile)
1640 1641 and hunk.starta == 0 and hunk.lena == 0):
1641 1642 create = True
1642 1643 missing = False
1643 1644
1644 1645 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
1645 1646 # diff is between a file and its backup. In this case, the original
1646 1647 # file should be patched (see original mpatch code).
1647 1648 isbackup = (abase == bbase and bfile.startswith(afile))
1648 1649 fname = None
1649 1650 if not missing:
1650 1651 if gooda and goodb:
1651 1652 if isbackup:
1652 1653 fname = afile
1653 1654 else:
1654 1655 fname = bfile
1655 1656 elif gooda:
1656 1657 fname = afile
1657 1658
1658 1659 if not fname:
1659 1660 if not nullb:
1660 1661 if isbackup:
1661 1662 fname = afile
1662 1663 else:
1663 1664 fname = bfile
1664 1665 elif not nulla:
1665 1666 fname = afile
1666 1667 else:
1667 1668 raise PatchError(_("undefined source and destination files"))
1668 1669
1669 1670 gp = patchmeta(fname)
1670 1671 if create:
1671 1672 gp.op = 'ADD'
1672 1673 elif remove:
1673 1674 gp.op = 'DELETE'
1674 1675 return gp
1675 1676
1676 1677 def scanpatch(fp):
1677 1678 """like patch.iterhunks, but yield different events
1678 1679
1679 1680 - ('file', [header_lines + fromfile + tofile])
1680 1681 - ('context', [context_lines])
1681 1682 - ('hunk', [hunk_lines])
1682 1683 - ('range', (-start,len, +start,len, proc))
1683 1684 """
1684 1685 lines_re = re.compile(r'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)')
1685 1686 lr = linereader(fp)
1686 1687
1687 1688 def scanwhile(first, p):
1688 1689 """scan lr while predicate holds"""
1689 1690 lines = [first]
1690 1691 for line in iter(lr.readline, ''):
1691 1692 if p(line):
1692 1693 lines.append(line)
1693 1694 else:
1694 1695 lr.push(line)
1695 1696 break
1696 1697 return lines
1697 1698
1698 1699 for line in iter(lr.readline, ''):
1699 1700 if line.startswith('diff --git a/') or line.startswith('diff -r '):
1700 1701 def notheader(line):
1701 1702 s = line.split(None, 1)
1702 1703 return not s or s[0] not in ('---', 'diff')
1703 1704 header = scanwhile(line, notheader)
1704 1705 fromfile = lr.readline()
1705 1706 if fromfile.startswith('---'):
1706 1707 tofile = lr.readline()
1707 1708 header += [fromfile, tofile]
1708 1709 else:
1709 1710 lr.push(fromfile)
1710 1711 yield 'file', header
1711 1712 elif line[0] == ' ':
1712 1713 yield 'context', scanwhile(line, lambda l: l[0] in ' \\')
1713 1714 elif line[0] in '-+':
1714 1715 yield 'hunk', scanwhile(line, lambda l: l[0] in '-+\\')
1715 1716 else:
1716 1717 m = lines_re.match(line)
1717 1718 if m:
1718 1719 yield 'range', m.groups()
1719 1720 else:
1720 1721 yield 'other', line
1721 1722
1722 1723 def scangitpatch(lr, firstline):
1723 1724 """
1724 1725 Git patches can emit:
1725 1726 - rename a to b
1726 1727 - change b
1727 1728 - copy a to c
1728 1729 - change c
1729 1730
1730 1731 We cannot apply this sequence as-is, the renamed 'a' could not be
1731 1732 found for it would have been renamed already. And we cannot copy
1732 1733 from 'b' instead because 'b' would have been changed already. So
1733 1734 we scan the git patch for copy and rename commands so we can
1734 1735 perform the copies ahead of time.
1735 1736 """
1736 1737 pos = 0
1737 1738 try:
1738 1739 pos = lr.fp.tell()
1739 1740 fp = lr.fp
1740 1741 except IOError:
1741 1742 fp = stringio(lr.fp.read())
1742 1743 gitlr = linereader(fp)
1743 1744 gitlr.push(firstline)
1744 1745 gitpatches = readgitpatch(gitlr)
1745 1746 fp.seek(pos)
1746 1747 return gitpatches
1747 1748
1748 1749 def iterhunks(fp):
1749 1750 """Read a patch and yield the following events:
1750 1751 - ("file", afile, bfile, firsthunk): select a new target file.
1751 1752 - ("hunk", hunk): a new hunk is ready to be applied, follows a
1752 1753 "file" event.
1753 1754 - ("git", gitchanges): current diff is in git format, gitchanges
1754 1755 maps filenames to gitpatch records. Unique event.
1755 1756 """
1756 1757 afile = ""
1757 1758 bfile = ""
1758 1759 state = None
1759 1760 hunknum = 0
1760 1761 emitfile = newfile = False
1761 1762 gitpatches = None
1762 1763
1763 1764 # our states
1764 1765 BFILE = 1
1765 1766 context = None
1766 1767 lr = linereader(fp)
1767 1768
1768 1769 for x in iter(lr.readline, ''):
1769 1770 if state == BFILE and (
1770 1771 (not context and x[0] == '@')
1771 1772 or (context is not False and x.startswith('***************'))
1772 1773 or x.startswith('GIT binary patch')):
1773 1774 gp = None
1774 1775 if (gitpatches and
1775 1776 gitpatches[-1].ispatching(afile, bfile)):
1776 1777 gp = gitpatches.pop()
1777 1778 if x.startswith('GIT binary patch'):
1778 1779 h = binhunk(lr, gp.path)
1779 1780 else:
1780 1781 if context is None and x.startswith('***************'):
1781 1782 context = True
1782 1783 h = hunk(x, hunknum + 1, lr, context)
1783 1784 hunknum += 1
1784 1785 if emitfile:
1785 1786 emitfile = False
1786 1787 yield 'file', (afile, bfile, h, gp and gp.copy() or None)
1787 1788 yield 'hunk', h
1788 1789 elif x.startswith('diff --git a/'):
1789 1790 m = gitre.match(x.rstrip(' \r\n'))
1790 1791 if not m:
1791 1792 continue
1792 1793 if gitpatches is None:
1793 1794 # scan whole input for git metadata
1794 1795 gitpatches = scangitpatch(lr, x)
1795 1796 yield 'git', [g.copy() for g in gitpatches
1796 1797 if g.op in ('COPY', 'RENAME')]
1797 1798 gitpatches.reverse()
1798 1799 afile = 'a/' + m.group(1)
1799 1800 bfile = 'b/' + m.group(2)
1800 1801 while gitpatches and not gitpatches[-1].ispatching(afile, bfile):
1801 1802 gp = gitpatches.pop()
1802 1803 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1803 1804 if not gitpatches:
1804 1805 raise PatchError(_('failed to synchronize metadata for "%s"')
1805 1806 % afile[2:])
1806 1807 gp = gitpatches[-1]
1807 1808 newfile = True
1808 1809 elif x.startswith('---'):
1809 1810 # check for a unified diff
1810 1811 l2 = lr.readline()
1811 1812 if not l2.startswith('+++'):
1812 1813 lr.push(l2)
1813 1814 continue
1814 1815 newfile = True
1815 1816 context = False
1816 1817 afile = parsefilename(x)
1817 1818 bfile = parsefilename(l2)
1818 1819 elif x.startswith('***'):
1819 1820 # check for a context diff
1820 1821 l2 = lr.readline()
1821 1822 if not l2.startswith('---'):
1822 1823 lr.push(l2)
1823 1824 continue
1824 1825 l3 = lr.readline()
1825 1826 lr.push(l3)
1826 1827 if not l3.startswith("***************"):
1827 1828 lr.push(l2)
1828 1829 continue
1829 1830 newfile = True
1830 1831 context = True
1831 1832 afile = parsefilename(x)
1832 1833 bfile = parsefilename(l2)
1833 1834
1834 1835 if newfile:
1835 1836 newfile = False
1836 1837 emitfile = True
1837 1838 state = BFILE
1838 1839 hunknum = 0
1839 1840
1840 1841 while gitpatches:
1841 1842 gp = gitpatches.pop()
1842 1843 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1843 1844
1844 1845 def applybindelta(binchunk, data):
1845 1846 """Apply a binary delta hunk
1846 1847 The algorithm used is the algorithm from git's patch-delta.c
1847 1848 """
1848 1849 def deltahead(binchunk):
1849 1850 i = 0
1850 1851 for c in binchunk:
1851 1852 i += 1
1852 1853 if not (ord(c) & 0x80):
1853 1854 return i
1854 1855 return i
1855 1856 out = ""
1856 1857 s = deltahead(binchunk)
1857 1858 binchunk = binchunk[s:]
1858 1859 s = deltahead(binchunk)
1859 1860 binchunk = binchunk[s:]
1860 1861 i = 0
1861 1862 while i < len(binchunk):
1862 1863 cmd = ord(binchunk[i])
1863 1864 i += 1
1864 1865 if (cmd & 0x80):
1865 1866 offset = 0
1866 1867 size = 0
1867 1868 if (cmd & 0x01):
1868 1869 offset = ord(binchunk[i])
1869 1870 i += 1
1870 1871 if (cmd & 0x02):
1871 1872 offset |= ord(binchunk[i]) << 8
1872 1873 i += 1
1873 1874 if (cmd & 0x04):
1874 1875 offset |= ord(binchunk[i]) << 16
1875 1876 i += 1
1876 1877 if (cmd & 0x08):
1877 1878 offset |= ord(binchunk[i]) << 24
1878 1879 i += 1
1879 1880 if (cmd & 0x10):
1880 1881 size = ord(binchunk[i])
1881 1882 i += 1
1882 1883 if (cmd & 0x20):
1883 1884 size |= ord(binchunk[i]) << 8
1884 1885 i += 1
1885 1886 if (cmd & 0x40):
1886 1887 size |= ord(binchunk[i]) << 16
1887 1888 i += 1
1888 1889 if size == 0:
1889 1890 size = 0x10000
1890 1891 offset_end = offset + size
1891 1892 out += data[offset:offset_end]
1892 1893 elif cmd != 0:
1893 1894 offset_end = i + cmd
1894 1895 out += binchunk[i:offset_end]
1895 1896 i += cmd
1896 1897 else:
1897 1898 raise PatchError(_('unexpected delta opcode 0'))
1898 1899 return out
1899 1900
1900 1901 def applydiff(ui, fp, backend, store, strip=1, prefix='', eolmode='strict'):
1901 1902 """Reads a patch from fp and tries to apply it.
1902 1903
1903 1904 Returns 0 for a clean patch, -1 if any rejects were found and 1 if
1904 1905 there was any fuzz.
1905 1906
1906 1907 If 'eolmode' is 'strict', the patch content and patched file are
1907 1908 read in binary mode. Otherwise, line endings are ignored when
1908 1909 patching then normalized according to 'eolmode'.
1909 1910 """
1910 1911 return _applydiff(ui, fp, patchfile, backend, store, strip=strip,
1911 1912 prefix=prefix, eolmode=eolmode)
1912 1913
1913 1914 def _applydiff(ui, fp, patcher, backend, store, strip=1, prefix='',
1914 1915 eolmode='strict'):
1915 1916
1916 1917 if prefix:
1917 1918 prefix = pathutil.canonpath(backend.repo.root, backend.repo.getcwd(),
1918 1919 prefix)
1919 1920 if prefix != '':
1920 1921 prefix += '/'
1921 1922 def pstrip(p):
1922 1923 return pathtransform(p, strip - 1, prefix)[1]
1923 1924
1924 1925 rejects = 0
1925 1926 err = 0
1926 1927 current_file = None
1927 1928
1928 1929 for state, values in iterhunks(fp):
1929 1930 if state == 'hunk':
1930 1931 if not current_file:
1931 1932 continue
1932 1933 ret = current_file.apply(values)
1933 1934 if ret > 0:
1934 1935 err = 1
1935 1936 elif state == 'file':
1936 1937 if current_file:
1937 1938 rejects += current_file.close()
1938 1939 current_file = None
1939 1940 afile, bfile, first_hunk, gp = values
1940 1941 if gp:
1941 1942 gp.path = pstrip(gp.path)
1942 1943 if gp.oldpath:
1943 1944 gp.oldpath = pstrip(gp.oldpath)
1944 1945 else:
1945 1946 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip,
1946 1947 prefix)
1947 1948 if gp.op == 'RENAME':
1948 1949 backend.unlink(gp.oldpath)
1949 1950 if not first_hunk:
1950 1951 if gp.op == 'DELETE':
1951 1952 backend.unlink(gp.path)
1952 1953 continue
1953 1954 data, mode = None, None
1954 1955 if gp.op in ('RENAME', 'COPY'):
1955 1956 data, mode = store.getfile(gp.oldpath)[:2]
1956 1957 if data is None:
1957 1958 # This means that the old path does not exist
1958 1959 raise PatchError(_("source file '%s' does not exist")
1959 1960 % gp.oldpath)
1960 1961 if gp.mode:
1961 1962 mode = gp.mode
1962 1963 if gp.op == 'ADD':
1963 1964 # Added files without content have no hunk and
1964 1965 # must be created
1965 1966 data = ''
1966 1967 if data or mode:
1967 1968 if (gp.op in ('ADD', 'RENAME', 'COPY')
1968 1969 and backend.exists(gp.path)):
1969 1970 raise PatchError(_("cannot create %s: destination "
1970 1971 "already exists") % gp.path)
1971 1972 backend.setfile(gp.path, data, mode, gp.oldpath)
1972 1973 continue
1973 1974 try:
1974 1975 current_file = patcher(ui, gp, backend, store,
1975 1976 eolmode=eolmode)
1976 1977 except PatchError as inst:
1977 1978 ui.warn(str(inst) + '\n')
1978 1979 current_file = None
1979 1980 rejects += 1
1980 1981 continue
1981 1982 elif state == 'git':
1982 1983 for gp in values:
1983 1984 path = pstrip(gp.oldpath)
1984 1985 data, mode = backend.getfile(path)
1985 1986 if data is None:
1986 1987 # The error ignored here will trigger a getfile()
1987 1988 # error in a place more appropriate for error
1988 1989 # handling, and will not interrupt the patching
1989 1990 # process.
1990 1991 pass
1991 1992 else:
1992 1993 store.setfile(path, data, mode)
1993 1994 else:
1994 1995 raise error.Abort(_('unsupported parser state: %s') % state)
1995 1996
1996 1997 if current_file:
1997 1998 rejects += current_file.close()
1998 1999
1999 2000 if rejects:
2000 2001 return -1
2001 2002 return err
2002 2003
2003 2004 def _externalpatch(ui, repo, patcher, patchname, strip, files,
2004 2005 similarity):
2005 2006 """use <patcher> to apply <patchname> to the working directory.
2006 2007 returns whether patch was applied with fuzz factor."""
2007 2008
2008 2009 fuzz = False
2009 2010 args = []
2010 2011 cwd = repo.root
2011 2012 if cwd:
2012 2013 args.append('-d %s' % util.shellquote(cwd))
2013 2014 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
2014 2015 util.shellquote(patchname)))
2015 2016 try:
2016 2017 for line in util.iterfile(fp):
2017 2018 line = line.rstrip()
2018 2019 ui.note(line + '\n')
2019 2020 if line.startswith('patching file '):
2020 2021 pf = util.parsepatchoutput(line)
2021 2022 printed_file = False
2022 2023 files.add(pf)
2023 2024 elif line.find('with fuzz') >= 0:
2024 2025 fuzz = True
2025 2026 if not printed_file:
2026 2027 ui.warn(pf + '\n')
2027 2028 printed_file = True
2028 2029 ui.warn(line + '\n')
2029 2030 elif line.find('saving rejects to file') >= 0:
2030 2031 ui.warn(line + '\n')
2031 2032 elif line.find('FAILED') >= 0:
2032 2033 if not printed_file:
2033 2034 ui.warn(pf + '\n')
2034 2035 printed_file = True
2035 2036 ui.warn(line + '\n')
2036 2037 finally:
2037 2038 if files:
2038 2039 scmutil.marktouched(repo, files, similarity)
2039 2040 code = fp.close()
2040 2041 if code:
2041 2042 raise PatchError(_("patch command failed: %s") %
2042 2043 util.explainexit(code)[0])
2043 2044 return fuzz
2044 2045
2045 2046 def patchbackend(ui, backend, patchobj, strip, prefix, files=None,
2046 2047 eolmode='strict'):
2047 2048 if files is None:
2048 2049 files = set()
2049 2050 if eolmode is None:
2050 2051 eolmode = ui.config('patch', 'eol', 'strict')
2051 2052 if eolmode.lower() not in eolmodes:
2052 2053 raise error.Abort(_('unsupported line endings type: %s') % eolmode)
2053 2054 eolmode = eolmode.lower()
2054 2055
2055 2056 store = filestore()
2056 2057 try:
2057 2058 fp = open(patchobj, 'rb')
2058 2059 except TypeError:
2059 2060 fp = patchobj
2060 2061 try:
2061 2062 ret = applydiff(ui, fp, backend, store, strip=strip, prefix=prefix,
2062 2063 eolmode=eolmode)
2063 2064 finally:
2064 2065 if fp != patchobj:
2065 2066 fp.close()
2066 2067 files.update(backend.close())
2067 2068 store.close()
2068 2069 if ret < 0:
2069 2070 raise PatchError(_('patch failed to apply'))
2070 2071 return ret > 0
2071 2072
2072 2073 def internalpatch(ui, repo, patchobj, strip, prefix='', files=None,
2073 2074 eolmode='strict', similarity=0):
2074 2075 """use builtin patch to apply <patchobj> to the working directory.
2075 2076 returns whether patch was applied with fuzz factor."""
2076 2077 backend = workingbackend(ui, repo, similarity)
2077 2078 return patchbackend(ui, backend, patchobj, strip, prefix, files, eolmode)
2078 2079
2079 2080 def patchrepo(ui, repo, ctx, store, patchobj, strip, prefix, files=None,
2080 2081 eolmode='strict'):
2081 2082 backend = repobackend(ui, repo, ctx, store)
2082 2083 return patchbackend(ui, backend, patchobj, strip, prefix, files, eolmode)
2083 2084
2084 2085 def patch(ui, repo, patchname, strip=1, prefix='', files=None, eolmode='strict',
2085 2086 similarity=0):
2086 2087 """Apply <patchname> to the working directory.
2087 2088
2088 2089 'eolmode' specifies how end of lines should be handled. It can be:
2089 2090 - 'strict': inputs are read in binary mode, EOLs are preserved
2090 2091 - 'crlf': EOLs are ignored when patching and reset to CRLF
2091 2092 - 'lf': EOLs are ignored when patching and reset to LF
2092 2093 - None: get it from user settings, default to 'strict'
2093 2094 'eolmode' is ignored when using an external patcher program.
2094 2095
2095 2096 Returns whether patch was applied with fuzz factor.
2096 2097 """
2097 2098 patcher = ui.config('ui', 'patch')
2098 2099 if files is None:
2099 2100 files = set()
2100 2101 if patcher:
2101 2102 return _externalpatch(ui, repo, patcher, patchname, strip,
2102 2103 files, similarity)
2103 2104 return internalpatch(ui, repo, patchname, strip, prefix, files, eolmode,
2104 2105 similarity)
2105 2106
2106 2107 def changedfiles(ui, repo, patchpath, strip=1):
2107 2108 backend = fsbackend(ui, repo.root)
2108 2109 with open(patchpath, 'rb') as fp:
2109 2110 changed = set()
2110 2111 for state, values in iterhunks(fp):
2111 2112 if state == 'file':
2112 2113 afile, bfile, first_hunk, gp = values
2113 2114 if gp:
2114 2115 gp.path = pathtransform(gp.path, strip - 1, '')[1]
2115 2116 if gp.oldpath:
2116 2117 gp.oldpath = pathtransform(gp.oldpath, strip - 1, '')[1]
2117 2118 else:
2118 2119 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip,
2119 2120 '')
2120 2121 changed.add(gp.path)
2121 2122 if gp.op == 'RENAME':
2122 2123 changed.add(gp.oldpath)
2123 2124 elif state not in ('hunk', 'git'):
2124 2125 raise error.Abort(_('unsupported parser state: %s') % state)
2125 2126 return changed
2126 2127
2127 2128 class GitDiffRequired(Exception):
2128 2129 pass
2129 2130
2130 2131 def diffallopts(ui, opts=None, untrusted=False, section='diff'):
2131 2132 '''return diffopts with all features supported and parsed'''
2132 2133 return difffeatureopts(ui, opts=opts, untrusted=untrusted, section=section,
2133 2134 git=True, whitespace=True, formatchanging=True)
2134 2135
2135 2136 diffopts = diffallopts
2136 2137
2137 2138 def difffeatureopts(ui, opts=None, untrusted=False, section='diff', git=False,
2138 2139 whitespace=False, formatchanging=False):
2139 2140 '''return diffopts with only opted-in features parsed
2140 2141
2141 2142 Features:
2142 2143 - git: git-style diffs
2143 2144 - whitespace: whitespace options like ignoreblanklines and ignorews
2144 2145 - formatchanging: options that will likely break or cause correctness issues
2145 2146 with most diff parsers
2146 2147 '''
2147 2148 def get(key, name=None, getter=ui.configbool, forceplain=None):
2148 2149 if opts:
2149 2150 v = opts.get(key)
2150 2151 # diffopts flags are either None-default (which is passed
2151 2152 # through unchanged, so we can identify unset values), or
2152 2153 # some other falsey default (eg --unified, which defaults
2153 2154 # to an empty string). We only want to override the config
2154 2155 # entries from hgrc with command line values if they
2155 2156 # appear to have been set, which is any truthy value,
2156 2157 # True, or False.
2157 2158 if v or isinstance(v, bool):
2158 2159 return v
2159 2160 if forceplain is not None and ui.plain():
2160 2161 return forceplain
2161 2162 return getter(section, name or key, None, untrusted=untrusted)
2162 2163
2163 2164 # core options, expected to be understood by every diff parser
2164 2165 buildopts = {
2165 2166 'nodates': get('nodates'),
2166 2167 'showfunc': get('show_function', 'showfunc'),
2167 2168 'context': get('unified', getter=ui.config),
2168 2169 }
2169 2170
2170 2171 if git:
2171 2172 buildopts['git'] = get('git')
2172 2173
2173 2174 # since this is in the experimental section, we need to call
2174 2175 # ui.configbool directory
2175 2176 buildopts['showsimilarity'] = ui.configbool('experimental',
2176 2177 'extendedheader.similarity')
2177 2178
2178 2179 # need to inspect the ui object instead of using get() since we want to
2179 2180 # test for an int
2180 2181 hconf = ui.config('experimental', 'extendedheader.index')
2181 2182 if hconf is not None:
2182 2183 hlen = None
2183 2184 try:
2184 2185 # the hash config could be an integer (for length of hash) or a
2185 2186 # word (e.g. short, full, none)
2186 2187 hlen = int(hconf)
2187 2188 if hlen < 0 or hlen > 40:
2188 2189 msg = _("invalid length for extendedheader.index: '%d'\n")
2189 2190 ui.warn(msg % hlen)
2190 2191 except ValueError:
2191 2192 # default value
2192 2193 if hconf == 'short' or hconf == '':
2193 2194 hlen = 12
2194 2195 elif hconf == 'full':
2195 2196 hlen = 40
2196 2197 elif hconf != 'none':
2197 2198 msg = _("invalid value for extendedheader.index: '%s'\n")
2198 2199 ui.warn(msg % hconf)
2199 2200 finally:
2200 2201 buildopts['index'] = hlen
2201 2202
2202 2203 if whitespace:
2203 2204 buildopts['ignorews'] = get('ignore_all_space', 'ignorews')
2204 2205 buildopts['ignorewsamount'] = get('ignore_space_change',
2205 2206 'ignorewsamount')
2206 2207 buildopts['ignoreblanklines'] = get('ignore_blank_lines',
2207 2208 'ignoreblanklines')
2208 2209 if formatchanging:
2209 2210 buildopts['text'] = opts and opts.get('text')
2210 2211 buildopts['nobinary'] = get('nobinary', forceplain=False)
2211 2212 buildopts['noprefix'] = get('noprefix', forceplain=False)
2212 2213
2213 2214 return mdiff.diffopts(**buildopts)
2214 2215
2215 2216 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
2216 2217 losedatafn=None, prefix='', relroot='', copy=None):
2217 2218 '''yields diff of changes to files between two nodes, or node and
2218 2219 working directory.
2219 2220
2220 2221 if node1 is None, use first dirstate parent instead.
2221 2222 if node2 is None, compare node1 with working directory.
2222 2223
2223 2224 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
2224 2225 every time some change cannot be represented with the current
2225 2226 patch format. Return False to upgrade to git patch format, True to
2226 2227 accept the loss or raise an exception to abort the diff. It is
2227 2228 called with the name of current file being diffed as 'fn'. If set
2228 2229 to None, patches will always be upgraded to git format when
2229 2230 necessary.
2230 2231
2231 2232 prefix is a filename prefix that is prepended to all filenames on
2232 2233 display (used for subrepos).
2233 2234
2234 2235 relroot, if not empty, must be normalized with a trailing /. Any match
2235 2236 patterns that fall outside it will be ignored.
2236 2237
2237 2238 copy, if not empty, should contain mappings {dst@y: src@x} of copy
2238 2239 information.'''
2239 2240
2240 2241 if opts is None:
2241 2242 opts = mdiff.defaultopts
2242 2243
2243 2244 if not node1 and not node2:
2244 2245 node1 = repo.dirstate.p1()
2245 2246
2246 2247 def lrugetfilectx():
2247 2248 cache = {}
2248 2249 order = collections.deque()
2249 2250 def getfilectx(f, ctx):
2250 2251 fctx = ctx.filectx(f, filelog=cache.get(f))
2251 2252 if f not in cache:
2252 2253 if len(cache) > 20:
2253 2254 del cache[order.popleft()]
2254 2255 cache[f] = fctx.filelog()
2255 2256 else:
2256 2257 order.remove(f)
2257 2258 order.append(f)
2258 2259 return fctx
2259 2260 return getfilectx
2260 2261 getfilectx = lrugetfilectx()
2261 2262
2262 2263 ctx1 = repo[node1]
2263 2264 ctx2 = repo[node2]
2264 2265
2265 2266 relfiltered = False
2266 2267 if relroot != '' and match.always():
2267 2268 # as a special case, create a new matcher with just the relroot
2268 2269 pats = [relroot]
2269 2270 match = scmutil.match(ctx2, pats, default='path')
2270 2271 relfiltered = True
2271 2272
2272 2273 if not changes:
2273 2274 changes = repo.status(ctx1, ctx2, match=match)
2274 2275 modified, added, removed = changes[:3]
2275 2276
2276 2277 if not modified and not added and not removed:
2277 2278 return []
2278 2279
2279 2280 if repo.ui.debugflag:
2280 2281 hexfunc = hex
2281 2282 else:
2282 2283 hexfunc = short
2283 2284 revs = [hexfunc(node) for node in [ctx1.node(), ctx2.node()] if node]
2284 2285
2285 2286 if copy is None:
2286 2287 copy = {}
2287 2288 if opts.git or opts.upgrade:
2288 2289 copy = copies.pathcopies(ctx1, ctx2, match=match)
2289 2290
2290 2291 if relroot is not None:
2291 2292 if not relfiltered:
2292 2293 # XXX this would ideally be done in the matcher, but that is
2293 2294 # generally meant to 'or' patterns, not 'and' them. In this case we
2294 2295 # need to 'and' all the patterns from the matcher with relroot.
2295 2296 def filterrel(l):
2296 2297 return [f for f in l if f.startswith(relroot)]
2297 2298 modified = filterrel(modified)
2298 2299 added = filterrel(added)
2299 2300 removed = filterrel(removed)
2300 2301 relfiltered = True
2301 2302 # filter out copies where either side isn't inside the relative root
2302 2303 copy = dict(((dst, src) for (dst, src) in copy.iteritems()
2303 2304 if dst.startswith(relroot)
2304 2305 and src.startswith(relroot)))
2305 2306
2306 2307 modifiedset = set(modified)
2307 2308 addedset = set(added)
2308 2309 removedset = set(removed)
2309 2310 for f in modified:
2310 2311 if f not in ctx1:
2311 2312 # Fix up added, since merged-in additions appear as
2312 2313 # modifications during merges
2313 2314 modifiedset.remove(f)
2314 2315 addedset.add(f)
2315 2316 for f in removed:
2316 2317 if f not in ctx1:
2317 2318 # Merged-in additions that are then removed are reported as removed.
2318 2319 # They are not in ctx1, so We don't want to show them in the diff.
2319 2320 removedset.remove(f)
2320 2321 modified = sorted(modifiedset)
2321 2322 added = sorted(addedset)
2322 2323 removed = sorted(removedset)
2323 2324 for dst, src in copy.items():
2324 2325 if src not in ctx1:
2325 2326 # Files merged in during a merge and then copied/renamed are
2326 2327 # reported as copies. We want to show them in the diff as additions.
2327 2328 del copy[dst]
2328 2329
2329 2330 def difffn(opts, losedata):
2330 2331 return trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
2331 2332 copy, getfilectx, opts, losedata, prefix, relroot)
2332 2333 if opts.upgrade and not opts.git:
2333 2334 try:
2334 2335 def losedata(fn):
2335 2336 if not losedatafn or not losedatafn(fn=fn):
2336 2337 raise GitDiffRequired
2337 2338 # Buffer the whole output until we are sure it can be generated
2338 2339 return list(difffn(opts.copy(git=False), losedata))
2339 2340 except GitDiffRequired:
2340 2341 return difffn(opts.copy(git=True), None)
2341 2342 else:
2342 2343 return difffn(opts, None)
2343 2344
2344 2345 def difflabel(func, *args, **kw):
2345 2346 '''yields 2-tuples of (output, label) based on the output of func()'''
2346 2347 headprefixes = [('diff', 'diff.diffline'),
2347 2348 ('copy', 'diff.extended'),
2348 2349 ('rename', 'diff.extended'),
2349 2350 ('old', 'diff.extended'),
2350 2351 ('new', 'diff.extended'),
2351 2352 ('deleted', 'diff.extended'),
2352 2353 ('index', 'diff.extended'),
2353 2354 ('similarity', 'diff.extended'),
2354 2355 ('---', 'diff.file_a'),
2355 2356 ('+++', 'diff.file_b')]
2356 2357 textprefixes = [('@', 'diff.hunk'),
2357 2358 ('-', 'diff.deleted'),
2358 2359 ('+', 'diff.inserted')]
2359 2360 head = False
2360 2361 for chunk in func(*args, **kw):
2361 2362 lines = chunk.split('\n')
2362 2363 for i, line in enumerate(lines):
2363 2364 if i != 0:
2364 2365 yield ('\n', '')
2365 2366 if head:
2366 2367 if line.startswith('@'):
2367 2368 head = False
2368 2369 else:
2369 2370 if line and line[0] not in ' +-@\\':
2370 2371 head = True
2371 2372 stripline = line
2372 2373 diffline = False
2373 2374 if not head and line and line[0] in '+-':
2374 2375 # highlight tabs and trailing whitespace, but only in
2375 2376 # changed lines
2376 2377 stripline = line.rstrip()
2377 2378 diffline = True
2378 2379
2379 2380 prefixes = textprefixes
2380 2381 if head:
2381 2382 prefixes = headprefixes
2382 2383 for prefix, label in prefixes:
2383 2384 if stripline.startswith(prefix):
2384 2385 if diffline:
2385 2386 for token in tabsplitter.findall(stripline):
2386 2387 if '\t' == token[0]:
2387 2388 yield (token, 'diff.tab')
2388 2389 else:
2389 2390 yield (token, label)
2390 2391 else:
2391 2392 yield (stripline, label)
2392 2393 break
2393 2394 else:
2394 2395 yield (line, '')
2395 2396 if line != stripline:
2396 2397 yield (line[len(stripline):], 'diff.trailingwhitespace')
2397 2398
2398 2399 def diffui(*args, **kw):
2399 2400 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
2400 2401 return difflabel(diff, *args, **kw)
2401 2402
2402 2403 def _filepairs(modified, added, removed, copy, opts):
2403 2404 '''generates tuples (f1, f2, copyop), where f1 is the name of the file
2404 2405 before and f2 is the the name after. For added files, f1 will be None,
2405 2406 and for removed files, f2 will be None. copyop may be set to None, 'copy'
2406 2407 or 'rename' (the latter two only if opts.git is set).'''
2407 2408 gone = set()
2408 2409
2409 2410 copyto = dict([(v, k) for k, v in copy.items()])
2410 2411
2411 2412 addedset, removedset = set(added), set(removed)
2412 2413
2413 2414 for f in sorted(modified + added + removed):
2414 2415 copyop = None
2415 2416 f1, f2 = f, f
2416 2417 if f in addedset:
2417 2418 f1 = None
2418 2419 if f in copy:
2419 2420 if opts.git:
2420 2421 f1 = copy[f]
2421 2422 if f1 in removedset and f1 not in gone:
2422 2423 copyop = 'rename'
2423 2424 gone.add(f1)
2424 2425 else:
2425 2426 copyop = 'copy'
2426 2427 elif f in removedset:
2427 2428 f2 = None
2428 2429 if opts.git:
2429 2430 # have we already reported a copy above?
2430 2431 if (f in copyto and copyto[f] in addedset
2431 2432 and copy[copyto[f]] == f):
2432 2433 continue
2433 2434 yield f1, f2, copyop
2434 2435
2435 2436 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
2436 2437 copy, getfilectx, opts, losedatafn, prefix, relroot):
2437 2438 '''given input data, generate a diff and yield it in blocks
2438 2439
2439 2440 If generating a diff would lose data like flags or binary data and
2440 2441 losedatafn is not None, it will be called.
2441 2442
2442 2443 relroot is removed and prefix is added to every path in the diff output.
2443 2444
2444 2445 If relroot is not empty, this function expects every path in modified,
2445 2446 added, removed and copy to start with it.'''
2446 2447
2447 2448 def gitindex(text):
2448 2449 if not text:
2449 2450 text = ""
2450 2451 l = len(text)
2451 2452 s = hashlib.sha1('blob %d\0' % l)
2452 2453 s.update(text)
2453 2454 return s.hexdigest()
2454 2455
2455 2456 if opts.noprefix:
2456 2457 aprefix = bprefix = ''
2457 2458 else:
2458 2459 aprefix = 'a/'
2459 2460 bprefix = 'b/'
2460 2461
2461 2462 def diffline(f, revs):
2462 2463 revinfo = ' '.join(["-r %s" % rev for rev in revs])
2463 2464 return 'diff %s %s' % (revinfo, f)
2464 2465
2465 2466 date1 = util.datestr(ctx1.date())
2466 2467 date2 = util.datestr(ctx2.date())
2467 2468
2468 2469 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
2469 2470
2470 2471 if relroot != '' and (repo.ui.configbool('devel', 'all')
2471 2472 or repo.ui.configbool('devel', 'check-relroot')):
2472 2473 for f in modified + added + removed + copy.keys() + copy.values():
2473 2474 if f is not None and not f.startswith(relroot):
2474 2475 raise AssertionError(
2475 2476 "file %s doesn't start with relroot %s" % (f, relroot))
2476 2477
2477 2478 for f1, f2, copyop in _filepairs(modified, added, removed, copy, opts):
2478 2479 content1 = None
2479 2480 content2 = None
2480 2481 flag1 = None
2481 2482 flag2 = None
2482 2483 if f1:
2483 2484 content1 = getfilectx(f1, ctx1).data()
2484 2485 if opts.git or losedatafn:
2485 2486 flag1 = ctx1.flags(f1)
2486 2487 if f2:
2487 2488 content2 = getfilectx(f2, ctx2).data()
2488 2489 if opts.git or losedatafn:
2489 2490 flag2 = ctx2.flags(f2)
2490 2491 binary = False
2491 2492 if opts.git or losedatafn:
2492 2493 binary = util.binary(content1) or util.binary(content2)
2493 2494
2494 2495 if losedatafn and not opts.git:
2495 2496 if (binary or
2496 2497 # copy/rename
2497 2498 f2 in copy or
2498 2499 # empty file creation
2499 2500 (not f1 and not content2) or
2500 2501 # empty file deletion
2501 2502 (not content1 and not f2) or
2502 2503 # create with flags
2503 2504 (not f1 and flag2) or
2504 2505 # change flags
2505 2506 (f1 and f2 and flag1 != flag2)):
2506 2507 losedatafn(f2 or f1)
2507 2508
2508 2509 path1 = f1 or f2
2509 2510 path2 = f2 or f1
2510 2511 path1 = posixpath.join(prefix, path1[len(relroot):])
2511 2512 path2 = posixpath.join(prefix, path2[len(relroot):])
2512 2513 header = []
2513 2514 if opts.git:
2514 2515 header.append('diff --git %s%s %s%s' %
2515 2516 (aprefix, path1, bprefix, path2))
2516 2517 if not f1: # added
2517 2518 header.append('new file mode %s' % gitmode[flag2])
2518 2519 elif not f2: # removed
2519 2520 header.append('deleted file mode %s' % gitmode[flag1])
2520 2521 else: # modified/copied/renamed
2521 2522 mode1, mode2 = gitmode[flag1], gitmode[flag2]
2522 2523 if mode1 != mode2:
2523 2524 header.append('old mode %s' % mode1)
2524 2525 header.append('new mode %s' % mode2)
2525 2526 if copyop is not None:
2526 2527 if opts.showsimilarity:
2527 2528 sim = similar.score(ctx1[path1], ctx2[path2]) * 100
2528 2529 header.append('similarity index %d%%' % sim)
2529 2530 header.append('%s from %s' % (copyop, path1))
2530 2531 header.append('%s to %s' % (copyop, path2))
2531 2532 elif revs and not repo.ui.quiet:
2532 2533 header.append(diffline(path1, revs))
2533 2534
2534 2535 if binary and opts.git and not opts.nobinary:
2535 2536 text = mdiff.b85diff(content1, content2)
2536 2537 if text:
2537 2538 header.append('index %s..%s' %
2538 2539 (gitindex(content1), gitindex(content2)))
2539 2540 else:
2540 2541 if opts.git and opts.index > 0:
2541 2542 flag = flag1
2542 2543 if flag is None:
2543 2544 flag = flag2
2544 2545 header.append('index %s..%s %s' %
2545 2546 (gitindex(content1)[0:opts.index],
2546 2547 gitindex(content2)[0:opts.index],
2547 2548 gitmode[flag]))
2548 2549
2549 2550 text = mdiff.unidiff(content1, date1,
2550 2551 content2, date2,
2551 2552 path1, path2, opts=opts)
2552 2553 if header and (text or len(header) > 1):
2553 2554 yield '\n'.join(header) + '\n'
2554 2555 if text:
2555 2556 yield text
2556 2557
2557 2558 def diffstatsum(stats):
2558 2559 maxfile, maxtotal, addtotal, removetotal, binary = 0, 0, 0, 0, False
2559 2560 for f, a, r, b in stats:
2560 2561 maxfile = max(maxfile, encoding.colwidth(f))
2561 2562 maxtotal = max(maxtotal, a + r)
2562 2563 addtotal += a
2563 2564 removetotal += r
2564 2565 binary = binary or b
2565 2566
2566 2567 return maxfile, maxtotal, addtotal, removetotal, binary
2567 2568
2568 2569 def diffstatdata(lines):
2569 2570 diffre = re.compile('^diff .*-r [a-z0-9]+\s(.*)$')
2570 2571
2571 2572 results = []
2572 2573 filename, adds, removes, isbinary = None, 0, 0, False
2573 2574
2574 2575 def addresult():
2575 2576 if filename:
2576 2577 results.append((filename, adds, removes, isbinary))
2577 2578
2578 2579 for line in lines:
2579 2580 if line.startswith('diff'):
2580 2581 addresult()
2581 2582 # set numbers to 0 anyway when starting new file
2582 2583 adds, removes, isbinary = 0, 0, False
2583 2584 if line.startswith('diff --git a/'):
2584 2585 filename = gitre.search(line).group(2)
2585 2586 elif line.startswith('diff -r'):
2586 2587 # format: "diff -r ... -r ... filename"
2587 2588 filename = diffre.search(line).group(1)
2588 2589 elif line.startswith('+') and not line.startswith('+++ '):
2589 2590 adds += 1
2590 2591 elif line.startswith('-') and not line.startswith('--- '):
2591 2592 removes += 1
2592 2593 elif (line.startswith('GIT binary patch') or
2593 2594 line.startswith('Binary file')):
2594 2595 isbinary = True
2595 2596 addresult()
2596 2597 return results
2597 2598
2598 2599 def diffstat(lines, width=80):
2599 2600 output = []
2600 2601 stats = diffstatdata(lines)
2601 2602 maxname, maxtotal, totaladds, totalremoves, hasbinary = diffstatsum(stats)
2602 2603
2603 2604 countwidth = len(str(maxtotal))
2604 2605 if hasbinary and countwidth < 3:
2605 2606 countwidth = 3
2606 2607 graphwidth = width - countwidth - maxname - 6
2607 2608 if graphwidth < 10:
2608 2609 graphwidth = 10
2609 2610
2610 2611 def scale(i):
2611 2612 if maxtotal <= graphwidth:
2612 2613 return i
2613 2614 # If diffstat runs out of room it doesn't print anything,
2614 2615 # which isn't very useful, so always print at least one + or -
2615 2616 # if there were at least some changes.
2616 2617 return max(i * graphwidth // maxtotal, int(bool(i)))
2617 2618
2618 2619 for filename, adds, removes, isbinary in stats:
2619 2620 if isbinary:
2620 2621 count = 'Bin'
2621 2622 else:
2622 2623 count = adds + removes
2623 2624 pluses = '+' * scale(adds)
2624 2625 minuses = '-' * scale(removes)
2625 2626 output.append(' %s%s | %*s %s%s\n' %
2626 2627 (filename, ' ' * (maxname - encoding.colwidth(filename)),
2627 2628 countwidth, count, pluses, minuses))
2628 2629
2629 2630 if stats:
2630 2631 output.append(_(' %d files changed, %d insertions(+), '
2631 2632 '%d deletions(-)\n')
2632 2633 % (len(stats), totaladds, totalremoves))
2633 2634
2634 2635 return ''.join(output)
2635 2636
2636 2637 def diffstatui(*args, **kw):
2637 2638 '''like diffstat(), but yields 2-tuples of (output, label) for
2638 2639 ui.write()
2639 2640 '''
2640 2641
2641 2642 for line in diffstat(*args, **kw).splitlines():
2642 2643 if line and line[-1] in '+-':
2643 2644 name, graph = line.rsplit(' ', 1)
2644 2645 yield (name + ' ', '')
2645 2646 m = re.search(r'\++', graph)
2646 2647 if m:
2647 2648 yield (m.group(0), 'diffstat.inserted')
2648 2649 m = re.search(r'-+', graph)
2649 2650 if m:
2650 2651 yield (m.group(0), 'diffstat.deleted')
2651 2652 else:
2652 2653 yield (line, '')
2653 2654 yield ('\n', '')
@@ -1,1050 +1,1051 b''
1 1 # wireproto.py - generic wire protocol support functions
2 2 #
3 3 # Copyright 2005-2010 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 hashlib
11 11 import itertools
12 12 import os
13 13 import tempfile
14 14
15 15 from .i18n import _
16 16 from .node import (
17 17 bin,
18 18 hex,
19 19 )
20 20
21 21 from . import (
22 22 bundle2,
23 23 changegroup as changegroupmod,
24 24 encoding,
25 25 error,
26 26 exchange,
27 27 peer,
28 28 pushkey as pushkeymod,
29 pycompat,
29 30 streamclone,
30 31 util,
31 32 )
32 33
33 34 urlerr = util.urlerr
34 35 urlreq = util.urlreq
35 36
36 37 bundle2requiredmain = _('incompatible Mercurial client; bundle2 required')
37 38 bundle2requiredhint = _('see https://www.mercurial-scm.org/wiki/'
38 39 'IncompatibleClient')
39 40 bundle2required = '%s\n(%s)\n' % (bundle2requiredmain, bundle2requiredhint)
40 41
41 42 class abstractserverproto(object):
42 43 """abstract class that summarizes the protocol API
43 44
44 45 Used as reference and documentation.
45 46 """
46 47
47 48 def getargs(self, args):
48 49 """return the value for arguments in <args>
49 50
50 51 returns a list of values (same order as <args>)"""
51 52 raise NotImplementedError()
52 53
53 54 def getfile(self, fp):
54 55 """write the whole content of a file into a file like object
55 56
56 57 The file is in the form::
57 58
58 59 (<chunk-size>\n<chunk>)+0\n
59 60
60 61 chunk size is the ascii version of the int.
61 62 """
62 63 raise NotImplementedError()
63 64
64 65 def redirect(self):
65 66 """may setup interception for stdout and stderr
66 67
67 68 See also the `restore` method."""
68 69 raise NotImplementedError()
69 70
70 71 # If the `redirect` function does install interception, the `restore`
71 72 # function MUST be defined. If interception is not used, this function
72 73 # MUST NOT be defined.
73 74 #
74 75 # left commented here on purpose
75 76 #
76 77 #def restore(self):
77 78 # """reinstall previous stdout and stderr and return intercepted stdout
78 79 # """
79 80 # raise NotImplementedError()
80 81
81 82 class remotebatch(peer.batcher):
82 83 '''batches the queued calls; uses as few roundtrips as possible'''
83 84 def __init__(self, remote):
84 85 '''remote must support _submitbatch(encbatch) and
85 86 _submitone(op, encargs)'''
86 87 peer.batcher.__init__(self)
87 88 self.remote = remote
88 89 def submit(self):
89 90 req, rsp = [], []
90 91 for name, args, opts, resref in self.calls:
91 92 mtd = getattr(self.remote, name)
92 93 batchablefn = getattr(mtd, 'batchable', None)
93 94 if batchablefn is not None:
94 95 batchable = batchablefn(mtd.im_self, *args, **opts)
95 96 encargsorres, encresref = next(batchable)
96 97 if encresref:
97 98 req.append((name, encargsorres,))
98 99 rsp.append((batchable, encresref, resref,))
99 100 else:
100 101 resref.set(encargsorres)
101 102 else:
102 103 if req:
103 104 self._submitreq(req, rsp)
104 105 req, rsp = [], []
105 106 resref.set(mtd(*args, **opts))
106 107 if req:
107 108 self._submitreq(req, rsp)
108 109 def _submitreq(self, req, rsp):
109 110 encresults = self.remote._submitbatch(req)
110 111 for encres, r in zip(encresults, rsp):
111 112 batchable, encresref, resref = r
112 113 encresref.set(encres)
113 114 resref.set(next(batchable))
114 115
115 116 class remoteiterbatcher(peer.iterbatcher):
116 117 def __init__(self, remote):
117 118 super(remoteiterbatcher, self).__init__()
118 119 self._remote = remote
119 120
120 121 def __getattr__(self, name):
121 122 if not getattr(self._remote, name, False):
122 123 raise AttributeError(
123 124 'Attempted to iterbatch non-batchable call to %r' % name)
124 125 return super(remoteiterbatcher, self).__getattr__(name)
125 126
126 127 def submit(self):
127 128 """Break the batch request into many patch calls and pipeline them.
128 129
129 130 This is mostly valuable over http where request sizes can be
130 131 limited, but can be used in other places as well.
131 132 """
132 133 req, rsp = [], []
133 134 for name, args, opts, resref in self.calls:
134 135 mtd = getattr(self._remote, name)
135 136 batchable = mtd.batchable(mtd.im_self, *args, **opts)
136 137 encargsorres, encresref = next(batchable)
137 138 assert encresref
138 139 req.append((name, encargsorres))
139 140 rsp.append((batchable, encresref))
140 141 if req:
141 142 self._resultiter = self._remote._submitbatch(req)
142 143 self._rsp = rsp
143 144
144 145 def results(self):
145 146 for (batchable, encresref), encres in itertools.izip(
146 147 self._rsp, self._resultiter):
147 148 encresref.set(encres)
148 149 yield next(batchable)
149 150
150 151 # Forward a couple of names from peer to make wireproto interactions
151 152 # slightly more sensible.
152 153 batchable = peer.batchable
153 154 future = peer.future
154 155
155 156 # list of nodes encoding / decoding
156 157
157 158 def decodelist(l, sep=' '):
158 159 if l:
159 160 return map(bin, l.split(sep))
160 161 return []
161 162
162 163 def encodelist(l, sep=' '):
163 164 try:
164 165 return sep.join(map(hex, l))
165 166 except TypeError:
166 167 raise
167 168
168 169 # batched call argument encoding
169 170
170 171 def escapearg(plain):
171 172 return (plain
172 173 .replace(':', ':c')
173 174 .replace(',', ':o')
174 175 .replace(';', ':s')
175 176 .replace('=', ':e'))
176 177
177 178 def unescapearg(escaped):
178 179 return (escaped
179 180 .replace(':e', '=')
180 181 .replace(':s', ';')
181 182 .replace(':o', ',')
182 183 .replace(':c', ':'))
183 184
184 185 def encodebatchcmds(req):
185 186 """Return a ``cmds`` argument value for the ``batch`` command."""
186 187 cmds = []
187 188 for op, argsdict in req:
188 189 # Old servers didn't properly unescape argument names. So prevent
189 190 # the sending of argument names that may not be decoded properly by
190 191 # servers.
191 192 assert all(escapearg(k) == k for k in argsdict)
192 193
193 194 args = ','.join('%s=%s' % (escapearg(k), escapearg(v))
194 195 for k, v in argsdict.iteritems())
195 196 cmds.append('%s %s' % (op, args))
196 197
197 198 return ';'.join(cmds)
198 199
199 200 # mapping of options accepted by getbundle and their types
200 201 #
201 202 # Meant to be extended by extensions. It is extensions responsibility to ensure
202 203 # such options are properly processed in exchange.getbundle.
203 204 #
204 205 # supported types are:
205 206 #
206 207 # :nodes: list of binary nodes
207 208 # :csv: list of comma-separated values
208 209 # :scsv: list of comma-separated values return as set
209 210 # :plain: string with no transformation needed.
210 211 gboptsmap = {'heads': 'nodes',
211 212 'common': 'nodes',
212 213 'obsmarkers': 'boolean',
213 214 'bundlecaps': 'scsv',
214 215 'listkeys': 'csv',
215 216 'cg': 'boolean',
216 217 'cbattempted': 'boolean'}
217 218
218 219 # client side
219 220
220 221 class wirepeer(peer.peerrepository):
221 222 """Client-side interface for communicating with a peer repository.
222 223
223 224 Methods commonly call wire protocol commands of the same name.
224 225
225 226 See also httppeer.py and sshpeer.py for protocol-specific
226 227 implementations of this interface.
227 228 """
228 229 def batch(self):
229 230 if self.capable('batch'):
230 231 return remotebatch(self)
231 232 else:
232 233 return peer.localbatch(self)
233 234 def _submitbatch(self, req):
234 235 """run batch request <req> on the server
235 236
236 237 Returns an iterator of the raw responses from the server.
237 238 """
238 239 rsp = self._callstream("batch", cmds=encodebatchcmds(req))
239 240 chunk = rsp.read(1024)
240 241 work = [chunk]
241 242 while chunk:
242 243 while ';' not in chunk and chunk:
243 244 chunk = rsp.read(1024)
244 245 work.append(chunk)
245 246 merged = ''.join(work)
246 247 while ';' in merged:
247 248 one, merged = merged.split(';', 1)
248 249 yield unescapearg(one)
249 250 chunk = rsp.read(1024)
250 251 work = [merged, chunk]
251 252 yield unescapearg(''.join(work))
252 253
253 254 def _submitone(self, op, args):
254 255 return self._call(op, **args)
255 256
256 257 def iterbatch(self):
257 258 return remoteiterbatcher(self)
258 259
259 260 @batchable
260 261 def lookup(self, key):
261 262 self.requirecap('lookup', _('look up remote revision'))
262 263 f = future()
263 264 yield {'key': encoding.fromlocal(key)}, f
264 265 d = f.value
265 266 success, data = d[:-1].split(" ", 1)
266 267 if int(success):
267 268 yield bin(data)
268 269 self._abort(error.RepoError(data))
269 270
270 271 @batchable
271 272 def heads(self):
272 273 f = future()
273 274 yield {}, f
274 275 d = f.value
275 276 try:
276 277 yield decodelist(d[:-1])
277 278 except ValueError:
278 279 self._abort(error.ResponseError(_("unexpected response:"), d))
279 280
280 281 @batchable
281 282 def known(self, nodes):
282 283 f = future()
283 284 yield {'nodes': encodelist(nodes)}, f
284 285 d = f.value
285 286 try:
286 287 yield [bool(int(b)) for b in d]
287 288 except ValueError:
288 289 self._abort(error.ResponseError(_("unexpected response:"), d))
289 290
290 291 @batchable
291 292 def branchmap(self):
292 293 f = future()
293 294 yield {}, f
294 295 d = f.value
295 296 try:
296 297 branchmap = {}
297 298 for branchpart in d.splitlines():
298 299 branchname, branchheads = branchpart.split(' ', 1)
299 300 branchname = encoding.tolocal(urlreq.unquote(branchname))
300 301 branchheads = decodelist(branchheads)
301 302 branchmap[branchname] = branchheads
302 303 yield branchmap
303 304 except TypeError:
304 305 self._abort(error.ResponseError(_("unexpected response:"), d))
305 306
306 307 def branches(self, nodes):
307 308 n = encodelist(nodes)
308 309 d = self._call("branches", nodes=n)
309 310 try:
310 311 br = [tuple(decodelist(b)) for b in d.splitlines()]
311 312 return br
312 313 except ValueError:
313 314 self._abort(error.ResponseError(_("unexpected response:"), d))
314 315
315 316 def between(self, pairs):
316 317 batch = 8 # avoid giant requests
317 318 r = []
318 319 for i in xrange(0, len(pairs), batch):
319 320 n = " ".join([encodelist(p, '-') for p in pairs[i:i + batch]])
320 321 d = self._call("between", pairs=n)
321 322 try:
322 323 r.extend(l and decodelist(l) or [] for l in d.splitlines())
323 324 except ValueError:
324 325 self._abort(error.ResponseError(_("unexpected response:"), d))
325 326 return r
326 327
327 328 @batchable
328 329 def pushkey(self, namespace, key, old, new):
329 330 if not self.capable('pushkey'):
330 331 yield False, None
331 332 f = future()
332 333 self.ui.debug('preparing pushkey for "%s:%s"\n' % (namespace, key))
333 334 yield {'namespace': encoding.fromlocal(namespace),
334 335 'key': encoding.fromlocal(key),
335 336 'old': encoding.fromlocal(old),
336 337 'new': encoding.fromlocal(new)}, f
337 338 d = f.value
338 339 d, output = d.split('\n', 1)
339 340 try:
340 341 d = bool(int(d))
341 342 except ValueError:
342 343 raise error.ResponseError(
343 344 _('push failed (unexpected response):'), d)
344 345 for l in output.splitlines(True):
345 346 self.ui.status(_('remote: '), l)
346 347 yield d
347 348
348 349 @batchable
349 350 def listkeys(self, namespace):
350 351 if not self.capable('pushkey'):
351 352 yield {}, None
352 353 f = future()
353 354 self.ui.debug('preparing listkeys for "%s"\n' % namespace)
354 355 yield {'namespace': encoding.fromlocal(namespace)}, f
355 356 d = f.value
356 357 self.ui.debug('received listkey for "%s": %i bytes\n'
357 358 % (namespace, len(d)))
358 359 yield pushkeymod.decodekeys(d)
359 360
360 361 def stream_out(self):
361 362 return self._callstream('stream_out')
362 363
363 364 def changegroup(self, nodes, kind):
364 365 n = encodelist(nodes)
365 366 f = self._callcompressable("changegroup", roots=n)
366 367 return changegroupmod.cg1unpacker(f, 'UN')
367 368
368 369 def changegroupsubset(self, bases, heads, kind):
369 370 self.requirecap('changegroupsubset', _('look up remote changes'))
370 371 bases = encodelist(bases)
371 372 heads = encodelist(heads)
372 373 f = self._callcompressable("changegroupsubset",
373 374 bases=bases, heads=heads)
374 375 return changegroupmod.cg1unpacker(f, 'UN')
375 376
376 377 def getbundle(self, source, **kwargs):
377 378 self.requirecap('getbundle', _('look up remote changes'))
378 379 opts = {}
379 380 bundlecaps = kwargs.get('bundlecaps')
380 381 if bundlecaps is not None:
381 382 kwargs['bundlecaps'] = sorted(bundlecaps)
382 383 else:
383 384 bundlecaps = () # kwargs could have it to None
384 385 for key, value in kwargs.iteritems():
385 386 if value is None:
386 387 continue
387 388 keytype = gboptsmap.get(key)
388 389 if keytype is None:
389 390 assert False, 'unexpected'
390 391 elif keytype == 'nodes':
391 392 value = encodelist(value)
392 393 elif keytype in ('csv', 'scsv'):
393 394 value = ','.join(value)
394 395 elif keytype == 'boolean':
395 396 value = '%i' % bool(value)
396 397 elif keytype != 'plain':
397 398 raise KeyError('unknown getbundle option type %s'
398 399 % keytype)
399 400 opts[key] = value
400 401 f = self._callcompressable("getbundle", **opts)
401 402 if any((cap.startswith('HG2') for cap in bundlecaps)):
402 403 return bundle2.getunbundler(self.ui, f)
403 404 else:
404 405 return changegroupmod.cg1unpacker(f, 'UN')
405 406
406 407 def unbundle(self, cg, heads, url):
407 408 '''Send cg (a readable file-like object representing the
408 409 changegroup to push, typically a chunkbuffer object) to the
409 410 remote server as a bundle.
410 411
411 412 When pushing a bundle10 stream, return an integer indicating the
412 413 result of the push (see localrepository.addchangegroup()).
413 414
414 415 When pushing a bundle20 stream, return a bundle20 stream.
415 416
416 417 `url` is the url the client thinks it's pushing to, which is
417 418 visible to hooks.
418 419 '''
419 420
420 421 if heads != ['force'] and self.capable('unbundlehash'):
421 422 heads = encodelist(['hashed',
422 423 hashlib.sha1(''.join(sorted(heads))).digest()])
423 424 else:
424 425 heads = encodelist(heads)
425 426
426 427 if util.safehasattr(cg, 'deltaheader'):
427 428 # this a bundle10, do the old style call sequence
428 429 ret, output = self._callpush("unbundle", cg, heads=heads)
429 430 if ret == "":
430 431 raise error.ResponseError(
431 432 _('push failed:'), output)
432 433 try:
433 434 ret = int(ret)
434 435 except ValueError:
435 436 raise error.ResponseError(
436 437 _('push failed (unexpected response):'), ret)
437 438
438 439 for l in output.splitlines(True):
439 440 self.ui.status(_('remote: '), l)
440 441 else:
441 442 # bundle2 push. Send a stream, fetch a stream.
442 443 stream = self._calltwowaystream('unbundle', cg, heads=heads)
443 444 ret = bundle2.getunbundler(self.ui, stream)
444 445 return ret
445 446
446 447 def debugwireargs(self, one, two, three=None, four=None, five=None):
447 448 # don't pass optional arguments left at their default value
448 449 opts = {}
449 450 if three is not None:
450 451 opts['three'] = three
451 452 if four is not None:
452 453 opts['four'] = four
453 454 return self._call('debugwireargs', one=one, two=two, **opts)
454 455
455 456 def _call(self, cmd, **args):
456 457 """execute <cmd> on the server
457 458
458 459 The command is expected to return a simple string.
459 460
460 461 returns the server reply as a string."""
461 462 raise NotImplementedError()
462 463
463 464 def _callstream(self, cmd, **args):
464 465 """execute <cmd> on the server
465 466
466 467 The command is expected to return a stream. Note that if the
467 468 command doesn't return a stream, _callstream behaves
468 469 differently for ssh and http peers.
469 470
470 471 returns the server reply as a file like object.
471 472 """
472 473 raise NotImplementedError()
473 474
474 475 def _callcompressable(self, cmd, **args):
475 476 """execute <cmd> on the server
476 477
477 478 The command is expected to return a stream.
478 479
479 480 The stream may have been compressed in some implementations. This
480 481 function takes care of the decompression. This is the only difference
481 482 with _callstream.
482 483
483 484 returns the server reply as a file like object.
484 485 """
485 486 raise NotImplementedError()
486 487
487 488 def _callpush(self, cmd, fp, **args):
488 489 """execute a <cmd> on server
489 490
490 491 The command is expected to be related to a push. Push has a special
491 492 return method.
492 493
493 494 returns the server reply as a (ret, output) tuple. ret is either
494 495 empty (error) or a stringified int.
495 496 """
496 497 raise NotImplementedError()
497 498
498 499 def _calltwowaystream(self, cmd, fp, **args):
499 500 """execute <cmd> on server
500 501
501 502 The command will send a stream to the server and get a stream in reply.
502 503 """
503 504 raise NotImplementedError()
504 505
505 506 def _abort(self, exception):
506 507 """clearly abort the wire protocol connection and raise the exception
507 508 """
508 509 raise NotImplementedError()
509 510
510 511 # server side
511 512
512 513 # wire protocol command can either return a string or one of these classes.
513 514 class streamres(object):
514 515 """wireproto reply: binary stream
515 516
516 517 The call was successful and the result is a stream.
517 518
518 519 Accepts either a generator or an object with a ``read(size)`` method.
519 520
520 521 ``v1compressible`` indicates whether this data can be compressed to
521 522 "version 1" clients (technically: HTTP peers using
522 523 application/mercurial-0.1 media type). This flag should NOT be used on
523 524 new commands because new clients should support a more modern compression
524 525 mechanism.
525 526 """
526 527 def __init__(self, gen=None, reader=None, v1compressible=False):
527 528 self.gen = gen
528 529 self.reader = reader
529 530 self.v1compressible = v1compressible
530 531
531 532 class pushres(object):
532 533 """wireproto reply: success with simple integer return
533 534
534 535 The call was successful and returned an integer contained in `self.res`.
535 536 """
536 537 def __init__(self, res):
537 538 self.res = res
538 539
539 540 class pusherr(object):
540 541 """wireproto reply: failure
541 542
542 543 The call failed. The `self.res` attribute contains the error message.
543 544 """
544 545 def __init__(self, res):
545 546 self.res = res
546 547
547 548 class ooberror(object):
548 549 """wireproto reply: failure of a batch of operation
549 550
550 551 Something failed during a batch call. The error message is stored in
551 552 `self.message`.
552 553 """
553 554 def __init__(self, message):
554 555 self.message = message
555 556
556 557 def getdispatchrepo(repo, proto, command):
557 558 """Obtain the repo used for processing wire protocol commands.
558 559
559 560 The intent of this function is to serve as a monkeypatch point for
560 561 extensions that need commands to operate on different repo views under
561 562 specialized circumstances.
562 563 """
563 564 return repo.filtered('served')
564 565
565 566 def dispatch(repo, proto, command):
566 567 repo = getdispatchrepo(repo, proto, command)
567 568 func, spec = commands[command]
568 569 args = proto.getargs(spec)
569 570 return func(repo, proto, *args)
570 571
571 572 def options(cmd, keys, others):
572 573 opts = {}
573 574 for k in keys:
574 575 if k in others:
575 576 opts[k] = others[k]
576 577 del others[k]
577 578 if others:
578 579 util.stderr.write("warning: %s ignored unexpected arguments %s\n"
579 580 % (cmd, ",".join(others)))
580 581 return opts
581 582
582 583 def bundle1allowed(repo, action):
583 584 """Whether a bundle1 operation is allowed from the server.
584 585
585 586 Priority is:
586 587
587 588 1. server.bundle1gd.<action> (if generaldelta active)
588 589 2. server.bundle1.<action>
589 590 3. server.bundle1gd (if generaldelta active)
590 591 4. server.bundle1
591 592 """
592 593 ui = repo.ui
593 594 gd = 'generaldelta' in repo.requirements
594 595
595 596 if gd:
596 597 v = ui.configbool('server', 'bundle1gd.%s' % action, None)
597 598 if v is not None:
598 599 return v
599 600
600 601 v = ui.configbool('server', 'bundle1.%s' % action, None)
601 602 if v is not None:
602 603 return v
603 604
604 605 if gd:
605 606 v = ui.configbool('server', 'bundle1gd', None)
606 607 if v is not None:
607 608 return v
608 609
609 610 return ui.configbool('server', 'bundle1', True)
610 611
611 612 def supportedcompengines(ui, proto, role):
612 613 """Obtain the list of supported compression engines for a request."""
613 614 assert role in (util.CLIENTROLE, util.SERVERROLE)
614 615
615 616 compengines = util.compengines.supportedwireengines(role)
616 617
617 618 # Allow config to override default list and ordering.
618 619 if role == util.SERVERROLE:
619 620 configengines = ui.configlist('server', 'compressionengines')
620 621 config = 'server.compressionengines'
621 622 else:
622 623 # This is currently implemented mainly to facilitate testing. In most
623 624 # cases, the server should be in charge of choosing a compression engine
624 625 # because a server has the most to lose from a sub-optimal choice. (e.g.
625 626 # CPU DoS due to an expensive engine or a network DoS due to poor
626 627 # compression ratio).
627 628 configengines = ui.configlist('experimental',
628 629 'clientcompressionengines')
629 630 config = 'experimental.clientcompressionengines'
630 631
631 632 # No explicit config. Filter out the ones that aren't supposed to be
632 633 # advertised and return default ordering.
633 634 if not configengines:
634 635 attr = 'serverpriority' if role == util.SERVERROLE else 'clientpriority'
635 636 return [e for e in compengines
636 637 if getattr(e.wireprotosupport(), attr) > 0]
637 638
638 639 # If compression engines are listed in the config, assume there is a good
639 640 # reason for it (like server operators wanting to achieve specific
640 641 # performance characteristics). So fail fast if the config references
641 642 # unusable compression engines.
642 643 validnames = set(e.name() for e in compengines)
643 644 invalidnames = set(e for e in configengines if e not in validnames)
644 645 if invalidnames:
645 646 raise error.Abort(_('invalid compression engine defined in %s: %s') %
646 647 (config, ', '.join(sorted(invalidnames))))
647 648
648 649 compengines = [e for e in compengines if e.name() in configengines]
649 650 compengines = sorted(compengines,
650 651 key=lambda e: configengines.index(e.name()))
651 652
652 653 if not compengines:
653 654 raise error.Abort(_('%s config option does not specify any known '
654 655 'compression engines') % config,
655 656 hint=_('usable compression engines: %s') %
656 657 ', '.sorted(validnames))
657 658
658 659 return compengines
659 660
660 661 # list of commands
661 662 commands = {}
662 663
663 664 def wireprotocommand(name, args=''):
664 665 """decorator for wire protocol command"""
665 666 def register(func):
666 667 commands[name] = (func, args)
667 668 return func
668 669 return register
669 670
670 671 @wireprotocommand('batch', 'cmds *')
671 672 def batch(repo, proto, cmds, others):
672 673 repo = repo.filtered("served")
673 674 res = []
674 675 for pair in cmds.split(';'):
675 676 op, args = pair.split(' ', 1)
676 677 vals = {}
677 678 for a in args.split(','):
678 679 if a:
679 680 n, v = a.split('=')
680 681 vals[unescapearg(n)] = unescapearg(v)
681 682 func, spec = commands[op]
682 683 if spec:
683 684 keys = spec.split()
684 685 data = {}
685 686 for k in keys:
686 687 if k == '*':
687 688 star = {}
688 689 for key in vals.keys():
689 690 if key not in keys:
690 691 star[key] = vals[key]
691 692 data['*'] = star
692 693 else:
693 694 data[k] = vals[k]
694 695 result = func(repo, proto, *[data[k] for k in keys])
695 696 else:
696 697 result = func(repo, proto)
697 698 if isinstance(result, ooberror):
698 699 return result
699 700 res.append(escapearg(result))
700 701 return ';'.join(res)
701 702
702 703 @wireprotocommand('between', 'pairs')
703 704 def between(repo, proto, pairs):
704 705 pairs = [decodelist(p, '-') for p in pairs.split(" ")]
705 706 r = []
706 707 for b in repo.between(pairs):
707 708 r.append(encodelist(b) + "\n")
708 709 return "".join(r)
709 710
710 711 @wireprotocommand('branchmap')
711 712 def branchmap(repo, proto):
712 713 branchmap = repo.branchmap()
713 714 heads = []
714 715 for branch, nodes in branchmap.iteritems():
715 716 branchname = urlreq.quote(encoding.fromlocal(branch))
716 717 branchnodes = encodelist(nodes)
717 718 heads.append('%s %s' % (branchname, branchnodes))
718 719 return '\n'.join(heads)
719 720
720 721 @wireprotocommand('branches', 'nodes')
721 722 def branches(repo, proto, nodes):
722 723 nodes = decodelist(nodes)
723 724 r = []
724 725 for b in repo.branches(nodes):
725 726 r.append(encodelist(b) + "\n")
726 727 return "".join(r)
727 728
728 729 @wireprotocommand('clonebundles', '')
729 730 def clonebundles(repo, proto):
730 731 """Server command for returning info for available bundles to seed clones.
731 732
732 733 Clients will parse this response and determine what bundle to fetch.
733 734
734 735 Extensions may wrap this command to filter or dynamically emit data
735 736 depending on the request. e.g. you could advertise URLs for the closest
736 737 data center given the client's IP address.
737 738 """
738 739 return repo.opener.tryread('clonebundles.manifest')
739 740
740 741 wireprotocaps = ['lookup', 'changegroupsubset', 'branchmap', 'pushkey',
741 742 'known', 'getbundle', 'unbundlehash', 'batch']
742 743
743 744 def _capabilities(repo, proto):
744 745 """return a list of capabilities for a repo
745 746
746 747 This function exists to allow extensions to easily wrap capabilities
747 748 computation
748 749
749 750 - returns a lists: easy to alter
750 751 - change done here will be propagated to both `capabilities` and `hello`
751 752 command without any other action needed.
752 753 """
753 754 # copy to prevent modification of the global list
754 755 caps = list(wireprotocaps)
755 756 if streamclone.allowservergeneration(repo.ui):
756 757 if repo.ui.configbool('server', 'preferuncompressed', False):
757 758 caps.append('stream-preferred')
758 759 requiredformats = repo.requirements & repo.supportedformats
759 760 # if our local revlogs are just revlogv1, add 'stream' cap
760 761 if not requiredformats - set(('revlogv1',)):
761 762 caps.append('stream')
762 763 # otherwise, add 'streamreqs' detailing our local revlog format
763 764 else:
764 765 caps.append('streamreqs=%s' % ','.join(sorted(requiredformats)))
765 766 if repo.ui.configbool('experimental', 'bundle2-advertise', True):
766 767 capsblob = bundle2.encodecaps(bundle2.getrepocaps(repo))
767 768 caps.append('bundle2=' + urlreq.quote(capsblob))
768 769 caps.append('unbundle=%s' % ','.join(bundle2.bundlepriority))
769 770
770 771 if proto.name == 'http':
771 772 caps.append('httpheader=%d' %
772 773 repo.ui.configint('server', 'maxhttpheaderlen', 1024))
773 774 if repo.ui.configbool('experimental', 'httppostargs', False):
774 775 caps.append('httppostargs')
775 776
776 777 # FUTURE advertise 0.2rx once support is implemented
777 778 # FUTURE advertise minrx and mintx after consulting config option
778 779 caps.append('httpmediatype=0.1rx,0.1tx,0.2tx')
779 780
780 781 compengines = supportedcompengines(repo.ui, proto, util.SERVERROLE)
781 782 if compengines:
782 783 comptypes = ','.join(urlreq.quote(e.wireprotosupport().name)
783 784 for e in compengines)
784 785 caps.append('compression=%s' % comptypes)
785 786
786 787 return caps
787 788
788 789 # If you are writing an extension and consider wrapping this function. Wrap
789 790 # `_capabilities` instead.
790 791 @wireprotocommand('capabilities')
791 792 def capabilities(repo, proto):
792 793 return ' '.join(_capabilities(repo, proto))
793 794
794 795 @wireprotocommand('changegroup', 'roots')
795 796 def changegroup(repo, proto, roots):
796 797 nodes = decodelist(roots)
797 798 cg = changegroupmod.changegroup(repo, nodes, 'serve')
798 799 return streamres(reader=cg, v1compressible=True)
799 800
800 801 @wireprotocommand('changegroupsubset', 'bases heads')
801 802 def changegroupsubset(repo, proto, bases, heads):
802 803 bases = decodelist(bases)
803 804 heads = decodelist(heads)
804 805 cg = changegroupmod.changegroupsubset(repo, bases, heads, 'serve')
805 806 return streamres(reader=cg, v1compressible=True)
806 807
807 808 @wireprotocommand('debugwireargs', 'one two *')
808 809 def debugwireargs(repo, proto, one, two, others):
809 810 # only accept optional args from the known set
810 811 opts = options('debugwireargs', ['three', 'four'], others)
811 812 return repo.debugwireargs(one, two, **opts)
812 813
813 814 @wireprotocommand('getbundle', '*')
814 815 def getbundle(repo, proto, others):
815 816 opts = options('getbundle', gboptsmap.keys(), others)
816 817 for k, v in opts.iteritems():
817 818 keytype = gboptsmap[k]
818 819 if keytype == 'nodes':
819 820 opts[k] = decodelist(v)
820 821 elif keytype == 'csv':
821 822 opts[k] = list(v.split(','))
822 823 elif keytype == 'scsv':
823 824 opts[k] = set(v.split(','))
824 825 elif keytype == 'boolean':
825 826 # Client should serialize False as '0', which is a non-empty string
826 827 # so it evaluates as a True bool.
827 828 if v == '0':
828 829 opts[k] = False
829 830 else:
830 831 opts[k] = bool(v)
831 832 elif keytype != 'plain':
832 833 raise KeyError('unknown getbundle option type %s'
833 834 % keytype)
834 835
835 836 if not bundle1allowed(repo, 'pull'):
836 837 if not exchange.bundle2requested(opts.get('bundlecaps')):
837 838 if proto.name == 'http':
838 839 return ooberror(bundle2required)
839 840 raise error.Abort(bundle2requiredmain,
840 841 hint=bundle2requiredhint)
841 842
842 843 #chunks = exchange.getbundlechunks(repo, 'serve', **opts)
843 844 try:
844 845 chunks = exchange.getbundlechunks(repo, 'serve', **opts)
845 846 except error.Abort as exc:
846 847 # cleanly forward Abort error to the client
847 848 if not exchange.bundle2requested(opts.get('bundlecaps')):
848 849 if proto.name == 'http':
849 850 return ooberror(str(exc) + '\n')
850 851 raise # cannot do better for bundle1 + ssh
851 852 # bundle2 request expect a bundle2 reply
852 853 bundler = bundle2.bundle20(repo.ui)
853 854 manargs = [('message', str(exc))]
854 855 advargs = []
855 856 if exc.hint is not None:
856 857 advargs.append(('hint', exc.hint))
857 858 bundler.addpart(bundle2.bundlepart('error:abort',
858 859 manargs, advargs))
859 860 return streamres(gen=bundler.getchunks(), v1compressible=True)
860 861 return streamres(gen=chunks, v1compressible=True)
861 862
862 863 @wireprotocommand('heads')
863 864 def heads(repo, proto):
864 865 h = repo.heads()
865 866 return encodelist(h) + "\n"
866 867
867 868 @wireprotocommand('hello')
868 869 def hello(repo, proto):
869 870 '''the hello command returns a set of lines describing various
870 871 interesting things about the server, in an RFC822-like format.
871 872 Currently the only one defined is "capabilities", which
872 873 consists of a line in the form:
873 874
874 875 capabilities: space separated list of tokens
875 876 '''
876 877 return "capabilities: %s\n" % (capabilities(repo, proto))
877 878
878 879 @wireprotocommand('listkeys', 'namespace')
879 880 def listkeys(repo, proto, namespace):
880 881 d = repo.listkeys(encoding.tolocal(namespace)).items()
881 882 return pushkeymod.encodekeys(d)
882 883
883 884 @wireprotocommand('lookup', 'key')
884 885 def lookup(repo, proto, key):
885 886 try:
886 887 k = encoding.tolocal(key)
887 888 c = repo[k]
888 889 r = c.hex()
889 890 success = 1
890 891 except Exception as inst:
891 892 r = str(inst)
892 893 success = 0
893 894 return "%s %s\n" % (success, r)
894 895
895 896 @wireprotocommand('known', 'nodes *')
896 897 def known(repo, proto, nodes, others):
897 898 return ''.join(b and "1" or "0" for b in repo.known(decodelist(nodes)))
898 899
899 900 @wireprotocommand('pushkey', 'namespace key old new')
900 901 def pushkey(repo, proto, namespace, key, old, new):
901 902 # compatibility with pre-1.8 clients which were accidentally
902 903 # sending raw binary nodes rather than utf-8-encoded hex
903 904 if len(new) == 20 and new.encode('string-escape') != new:
904 905 # looks like it could be a binary node
905 906 try:
906 907 new.decode('utf-8')
907 908 new = encoding.tolocal(new) # but cleanly decodes as UTF-8
908 909 except UnicodeDecodeError:
909 910 pass # binary, leave unmodified
910 911 else:
911 912 new = encoding.tolocal(new) # normal path
912 913
913 914 if util.safehasattr(proto, 'restore'):
914 915
915 916 proto.redirect()
916 917
917 918 try:
918 919 r = repo.pushkey(encoding.tolocal(namespace), encoding.tolocal(key),
919 920 encoding.tolocal(old), new) or False
920 921 except error.Abort:
921 922 r = False
922 923
923 924 output = proto.restore()
924 925
925 926 return '%s\n%s' % (int(r), output)
926 927
927 928 r = repo.pushkey(encoding.tolocal(namespace), encoding.tolocal(key),
928 929 encoding.tolocal(old), new)
929 930 return '%s\n' % int(r)
930 931
931 932 @wireprotocommand('stream_out')
932 933 def stream(repo, proto):
933 934 '''If the server supports streaming clone, it advertises the "stream"
934 935 capability with a value representing the version and flags of the repo
935 936 it is serving. Client checks to see if it understands the format.
936 937 '''
937 938 if not streamclone.allowservergeneration(repo.ui):
938 939 return '1\n'
939 940
940 941 def getstream(it):
941 942 yield '0\n'
942 943 for chunk in it:
943 944 yield chunk
944 945
945 946 try:
946 947 # LockError may be raised before the first result is yielded. Don't
947 948 # emit output until we're sure we got the lock successfully.
948 949 it = streamclone.generatev1wireproto(repo)
949 950 return streamres(gen=getstream(it))
950 951 except error.LockError:
951 952 return '2\n'
952 953
953 954 @wireprotocommand('unbundle', 'heads')
954 955 def unbundle(repo, proto, heads):
955 956 their_heads = decodelist(heads)
956 957
957 958 try:
958 959 proto.redirect()
959 960
960 961 exchange.check_heads(repo, their_heads, 'preparing changes')
961 962
962 963 # write bundle data to temporary file because it can be big
963 964 fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-')
964 fp = os.fdopen(fd, 'wb+')
965 fp = os.fdopen(fd, pycompat.sysstr('wb+'))
965 966 r = 0
966 967 try:
967 968 proto.getfile(fp)
968 969 fp.seek(0)
969 970 gen = exchange.readbundle(repo.ui, fp, None)
970 971 if (isinstance(gen, changegroupmod.cg1unpacker)
971 972 and not bundle1allowed(repo, 'push')):
972 973 if proto.name == 'http':
973 974 # need to special case http because stderr do not get to
974 975 # the http client on failed push so we need to abuse some
975 976 # other error type to make sure the message get to the
976 977 # user.
977 978 return ooberror(bundle2required)
978 979 raise error.Abort(bundle2requiredmain,
979 980 hint=bundle2requiredhint)
980 981
981 982 r = exchange.unbundle(repo, gen, their_heads, 'serve',
982 983 proto._client())
983 984 if util.safehasattr(r, 'addpart'):
984 985 # The return looks streamable, we are in the bundle2 case and
985 986 # should return a stream.
986 987 return streamres(gen=r.getchunks())
987 988 return pushres(r)
988 989
989 990 finally:
990 991 fp.close()
991 992 os.unlink(tempname)
992 993
993 994 except (error.BundleValueError, error.Abort, error.PushRaced) as exc:
994 995 # handle non-bundle2 case first
995 996 if not getattr(exc, 'duringunbundle2', False):
996 997 try:
997 998 raise
998 999 except error.Abort:
999 1000 # The old code we moved used util.stderr directly.
1000 1001 # We did not change it to minimise code change.
1001 1002 # This need to be moved to something proper.
1002 1003 # Feel free to do it.
1003 1004 util.stderr.write("abort: %s\n" % exc)
1004 1005 if exc.hint is not None:
1005 1006 util.stderr.write("(%s)\n" % exc.hint)
1006 1007 return pushres(0)
1007 1008 except error.PushRaced:
1008 1009 return pusherr(str(exc))
1009 1010
1010 1011 bundler = bundle2.bundle20(repo.ui)
1011 1012 for out in getattr(exc, '_bundle2salvagedoutput', ()):
1012 1013 bundler.addpart(out)
1013 1014 try:
1014 1015 try:
1015 1016 raise
1016 1017 except error.PushkeyFailed as exc:
1017 1018 # check client caps
1018 1019 remotecaps = getattr(exc, '_replycaps', None)
1019 1020 if (remotecaps is not None
1020 1021 and 'pushkey' not in remotecaps.get('error', ())):
1021 1022 # no support remote side, fallback to Abort handler.
1022 1023 raise
1023 1024 part = bundler.newpart('error:pushkey')
1024 1025 part.addparam('in-reply-to', exc.partid)
1025 1026 if exc.namespace is not None:
1026 1027 part.addparam('namespace', exc.namespace, mandatory=False)
1027 1028 if exc.key is not None:
1028 1029 part.addparam('key', exc.key, mandatory=False)
1029 1030 if exc.new is not None:
1030 1031 part.addparam('new', exc.new, mandatory=False)
1031 1032 if exc.old is not None:
1032 1033 part.addparam('old', exc.old, mandatory=False)
1033 1034 if exc.ret is not None:
1034 1035 part.addparam('ret', exc.ret, mandatory=False)
1035 1036 except error.BundleValueError as exc:
1036 1037 errpart = bundler.newpart('error:unsupportedcontent')
1037 1038 if exc.parttype is not None:
1038 1039 errpart.addparam('parttype', exc.parttype)
1039 1040 if exc.params:
1040 1041 errpart.addparam('params', '\0'.join(exc.params))
1041 1042 except error.Abort as exc:
1042 1043 manargs = [('message', str(exc))]
1043 1044 advargs = []
1044 1045 if exc.hint is not None:
1045 1046 advargs.append(('hint', exc.hint))
1046 1047 bundler.addpart(bundle2.bundlepart('error:abort',
1047 1048 manargs, advargs))
1048 1049 except error.PushRaced as exc:
1049 1050 bundler.newpart('error:pushraced', [('message', str(exc))])
1050 1051 return streamres(gen=bundler.getchunks())
@@ -1,224 +1,224 b''
1 1 # worker.py - master-slave parallelism support
2 2 #
3 3 # Copyright 2013 Facebook, Inc.
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import errno
11 11 import os
12 12 import signal
13 13 import sys
14 14
15 15 from .i18n import _
16 16 from . import (
17 17 encoding,
18 18 error,
19 19 pycompat,
20 20 scmutil,
21 21 util,
22 22 )
23 23
24 24 def countcpus():
25 25 '''try to count the number of CPUs on the system'''
26 26
27 27 # posix
28 28 try:
29 29 n = int(os.sysconf('SC_NPROCESSORS_ONLN'))
30 30 if n > 0:
31 31 return n
32 32 except (AttributeError, ValueError):
33 33 pass
34 34
35 35 # windows
36 36 try:
37 37 n = int(encoding.environ['NUMBER_OF_PROCESSORS'])
38 38 if n > 0:
39 39 return n
40 40 except (KeyError, ValueError):
41 41 pass
42 42
43 43 return 1
44 44
45 45 def _numworkers(ui):
46 46 s = ui.config('worker', 'numcpus')
47 47 if s:
48 48 try:
49 49 n = int(s)
50 50 if n >= 1:
51 51 return n
52 52 except ValueError:
53 53 raise error.Abort(_('number of cpus must be an integer'))
54 54 return min(max(countcpus(), 4), 32)
55 55
56 56 if pycompat.osname == 'posix':
57 57 _startupcost = 0.01
58 58 else:
59 59 _startupcost = 1e30
60 60
61 61 def worthwhile(ui, costperop, nops):
62 62 '''try to determine whether the benefit of multiple processes can
63 63 outweigh the cost of starting them'''
64 64 linear = costperop * nops
65 65 workers = _numworkers(ui)
66 66 benefit = linear - (_startupcost * workers + linear / workers)
67 67 return benefit >= 0.15
68 68
69 69 def worker(ui, costperarg, func, staticargs, args):
70 70 '''run a function, possibly in parallel in multiple worker
71 71 processes.
72 72
73 73 returns a progress iterator
74 74
75 75 costperarg - cost of a single task
76 76
77 77 func - function to run
78 78
79 79 staticargs - arguments to pass to every invocation of the function
80 80
81 81 args - arguments to split into chunks, to pass to individual
82 82 workers
83 83 '''
84 84 if worthwhile(ui, costperarg, len(args)):
85 85 return _platformworker(ui, func, staticargs, args)
86 86 return func(*staticargs + (args,))
87 87
88 88 def _posixworker(ui, func, staticargs, args):
89 89 rfd, wfd = os.pipe()
90 90 workers = _numworkers(ui)
91 91 oldhandler = signal.getsignal(signal.SIGINT)
92 92 signal.signal(signal.SIGINT, signal.SIG_IGN)
93 93 pids, problem = set(), [0]
94 94 def killworkers():
95 95 # unregister SIGCHLD handler as all children will be killed. This
96 96 # function shouldn't be interrupted by another SIGCHLD; otherwise pids
97 97 # could be updated while iterating, which would cause inconsistency.
98 98 signal.signal(signal.SIGCHLD, oldchldhandler)
99 99 # if one worker bails, there's no good reason to wait for the rest
100 100 for p in pids:
101 101 try:
102 102 os.kill(p, signal.SIGTERM)
103 103 except OSError as err:
104 104 if err.errno != errno.ESRCH:
105 105 raise
106 106 def waitforworkers(blocking=True):
107 107 for pid in pids.copy():
108 108 p = st = 0
109 109 while True:
110 110 try:
111 111 p, st = os.waitpid(pid, (0 if blocking else os.WNOHANG))
112 112 break
113 113 except OSError as e:
114 114 if e.errno == errno.EINTR:
115 115 continue
116 116 elif e.errno == errno.ECHILD:
117 117 # child would already be reaped, but pids yet been
118 118 # updated (maybe interrupted just after waitpid)
119 119 pids.discard(pid)
120 120 break
121 121 else:
122 122 raise
123 123 if p:
124 124 pids.discard(p)
125 125 st = _exitstatus(st)
126 126 if st and not problem[0]:
127 127 problem[0] = st
128 128 def sigchldhandler(signum, frame):
129 129 waitforworkers(blocking=False)
130 130 if problem[0]:
131 131 killworkers()
132 132 oldchldhandler = signal.signal(signal.SIGCHLD, sigchldhandler)
133 133 for pargs in partition(args, workers):
134 134 pid = os.fork()
135 135 if pid == 0:
136 136 signal.signal(signal.SIGINT, oldhandler)
137 137 signal.signal(signal.SIGCHLD, oldchldhandler)
138 138
139 139 def workerfunc():
140 140 os.close(rfd)
141 141 for i, item in func(*(staticargs + (pargs,))):
142 142 os.write(wfd, '%d %s\n' % (i, item))
143 143
144 144 # make sure we use os._exit in all code paths. otherwise the worker
145 145 # may do some clean-ups which could cause surprises like deadlock.
146 146 # see sshpeer.cleanup for example.
147 147 try:
148 148 scmutil.callcatch(ui, workerfunc)
149 149 except KeyboardInterrupt:
150 150 os._exit(255)
151 151 except: # never return, therefore no re-raises
152 152 try:
153 153 ui.traceback()
154 154 finally:
155 155 os._exit(255)
156 156 else:
157 157 os._exit(0)
158 158 pids.add(pid)
159 159 os.close(wfd)
160 fp = os.fdopen(rfd, 'rb', 0)
160 fp = os.fdopen(rfd, pycompat.sysstr('rb'), 0)
161 161 def cleanup():
162 162 signal.signal(signal.SIGINT, oldhandler)
163 163 waitforworkers()
164 164 signal.signal(signal.SIGCHLD, oldchldhandler)
165 165 status = problem[0]
166 166 if status:
167 167 if status < 0:
168 168 os.kill(os.getpid(), -status)
169 169 sys.exit(status)
170 170 try:
171 171 for line in util.iterfile(fp):
172 172 l = line.split(' ', 1)
173 173 yield int(l[0]), l[1][:-1]
174 174 except: # re-raises
175 175 killworkers()
176 176 cleanup()
177 177 raise
178 178 cleanup()
179 179
180 180 def _posixexitstatus(code):
181 181 '''convert a posix exit status into the same form returned by
182 182 os.spawnv
183 183
184 184 returns None if the process was stopped instead of exiting'''
185 185 if os.WIFEXITED(code):
186 186 return os.WEXITSTATUS(code)
187 187 elif os.WIFSIGNALED(code):
188 188 return -os.WTERMSIG(code)
189 189
190 190 if pycompat.osname != 'nt':
191 191 _platformworker = _posixworker
192 192 _exitstatus = _posixexitstatus
193 193
194 194 def partition(lst, nslices):
195 195 '''partition a list into N slices of roughly equal size
196 196
197 197 The current strategy takes every Nth element from the input. If
198 198 we ever write workers that need to preserve grouping in input
199 199 we should consider allowing callers to specify a partition strategy.
200 200
201 201 mpm is not a fan of this partitioning strategy when files are involved.
202 202 In his words:
203 203
204 204 Single-threaded Mercurial makes a point of creating and visiting
205 205 files in a fixed order (alphabetical). When creating files in order,
206 206 a typical filesystem is likely to allocate them on nearby regions on
207 207 disk. Thus, when revisiting in the same order, locality is maximized
208 208 and various forms of OS and disk-level caching and read-ahead get a
209 209 chance to work.
210 210
211 211 This effect can be quite significant on spinning disks. I discovered it
212 212 circa Mercurial v0.4 when revlogs were named by hashes of filenames.
213 213 Tarring a repo and copying it to another disk effectively randomized
214 214 the revlog ordering on disk by sorting the revlogs by hash and suddenly
215 215 performance of my kernel checkout benchmark dropped by ~10x because the
216 216 "working set" of sectors visited no longer fit in the drive's cache and
217 217 the workload switched from streaming to random I/O.
218 218
219 219 What we should really be doing is have workers read filenames from a
220 220 ordered queue. This preserves locality and also keeps any worker from
221 221 getting more than one file out of balance.
222 222 '''
223 223 for i in range(nslices):
224 224 yield lst[i::nslices]
General Comments 0
You need to be logged in to leave comments. Login now