##// END OF EJS Templates
wireproto: rename HTTPV2 so it less like HTTP/2...
Gregory Szorc -
r37662:77c9ee77 default
parent child Browse files
Show More
@@ -1,3101 +1,3101 b''
1 1 # debugcommands.py - command processing for debug* commands
2 2 #
3 3 # Copyright 2005-2016 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 codecs
11 11 import collections
12 12 import difflib
13 13 import errno
14 14 import operator
15 15 import os
16 16 import random
17 17 import re
18 18 import socket
19 19 import ssl
20 20 import stat
21 21 import string
22 22 import subprocess
23 23 import sys
24 24 import tempfile
25 25 import time
26 26
27 27 from .i18n import _
28 28 from .node import (
29 29 bin,
30 30 hex,
31 31 nullhex,
32 32 nullid,
33 33 nullrev,
34 34 short,
35 35 )
36 36 from .thirdparty import (
37 37 cbor,
38 38 )
39 39 from . import (
40 40 bundle2,
41 41 changegroup,
42 42 cmdutil,
43 43 color,
44 44 context,
45 45 dagparser,
46 46 dagutil,
47 47 encoding,
48 48 error,
49 49 exchange,
50 50 extensions,
51 51 filemerge,
52 52 fileset,
53 53 formatter,
54 54 hg,
55 55 httppeer,
56 56 localrepo,
57 57 lock as lockmod,
58 58 logcmdutil,
59 59 merge as mergemod,
60 60 obsolete,
61 61 obsutil,
62 62 phases,
63 63 policy,
64 64 pvec,
65 65 pycompat,
66 66 registrar,
67 67 repair,
68 68 revlog,
69 69 revset,
70 70 revsetlang,
71 71 scmutil,
72 72 setdiscovery,
73 73 simplemerge,
74 74 smartset,
75 75 sshpeer,
76 76 sslutil,
77 77 streamclone,
78 78 templater,
79 79 treediscovery,
80 80 upgrade,
81 81 url as urlmod,
82 82 util,
83 83 vfs as vfsmod,
84 84 wireprotoframing,
85 85 wireprotoserver,
86 86 wireprototypes,
87 87 )
88 88 from .utils import (
89 89 dateutil,
90 90 procutil,
91 91 stringutil,
92 92 )
93 93
94 94 release = lockmod.release
95 95
96 96 command = registrar.command()
97 97
98 98 @command('debugancestor', [], _('[INDEX] REV1 REV2'), optionalrepo=True)
99 99 def debugancestor(ui, repo, *args):
100 100 """find the ancestor revision of two revisions in a given index"""
101 101 if len(args) == 3:
102 102 index, rev1, rev2 = args
103 103 r = revlog.revlog(vfsmod.vfs(pycompat.getcwd(), audit=False), index)
104 104 lookup = r.lookup
105 105 elif len(args) == 2:
106 106 if not repo:
107 107 raise error.Abort(_('there is no Mercurial repository here '
108 108 '(.hg not found)'))
109 109 rev1, rev2 = args
110 110 r = repo.changelog
111 111 lookup = repo.lookup
112 112 else:
113 113 raise error.Abort(_('either two or three arguments required'))
114 114 a = r.ancestor(lookup(rev1), lookup(rev2))
115 115 ui.write('%d:%s\n' % (r.rev(a), hex(a)))
116 116
117 117 @command('debugapplystreamclonebundle', [], 'FILE')
118 118 def debugapplystreamclonebundle(ui, repo, fname):
119 119 """apply a stream clone bundle file"""
120 120 f = hg.openpath(ui, fname)
121 121 gen = exchange.readbundle(ui, f, fname)
122 122 gen.apply(repo)
123 123
124 124 @command('debugbuilddag',
125 125 [('m', 'mergeable-file', None, _('add single file mergeable changes')),
126 126 ('o', 'overwritten-file', None, _('add single file all revs overwrite')),
127 127 ('n', 'new-file', None, _('add new file at each rev'))],
128 128 _('[OPTION]... [TEXT]'))
129 129 def debugbuilddag(ui, repo, text=None,
130 130 mergeable_file=False,
131 131 overwritten_file=False,
132 132 new_file=False):
133 133 """builds a repo with a given DAG from scratch in the current empty repo
134 134
135 135 The description of the DAG is read from stdin if not given on the
136 136 command line.
137 137
138 138 Elements:
139 139
140 140 - "+n" is a linear run of n nodes based on the current default parent
141 141 - "." is a single node based on the current default parent
142 142 - "$" resets the default parent to null (implied at the start);
143 143 otherwise the default parent is always the last node created
144 144 - "<p" sets the default parent to the backref p
145 145 - "*p" is a fork at parent p, which is a backref
146 146 - "*p1/p2" is a merge of parents p1 and p2, which are backrefs
147 147 - "/p2" is a merge of the preceding node and p2
148 148 - ":tag" defines a local tag for the preceding node
149 149 - "@branch" sets the named branch for subsequent nodes
150 150 - "#...\\n" is a comment up to the end of the line
151 151
152 152 Whitespace between the above elements is ignored.
153 153
154 154 A backref is either
155 155
156 156 - a number n, which references the node curr-n, where curr is the current
157 157 node, or
158 158 - the name of a local tag you placed earlier using ":tag", or
159 159 - empty to denote the default parent.
160 160
161 161 All string valued-elements are either strictly alphanumeric, or must
162 162 be enclosed in double quotes ("..."), with "\\" as escape character.
163 163 """
164 164
165 165 if text is None:
166 166 ui.status(_("reading DAG from stdin\n"))
167 167 text = ui.fin.read()
168 168
169 169 cl = repo.changelog
170 170 if len(cl) > 0:
171 171 raise error.Abort(_('repository is not empty'))
172 172
173 173 # determine number of revs in DAG
174 174 total = 0
175 175 for type, data in dagparser.parsedag(text):
176 176 if type == 'n':
177 177 total += 1
178 178
179 179 if mergeable_file:
180 180 linesperrev = 2
181 181 # make a file with k lines per rev
182 182 initialmergedlines = ['%d' % i for i in xrange(0, total * linesperrev)]
183 183 initialmergedlines.append("")
184 184
185 185 tags = []
186 186
187 187 wlock = lock = tr = None
188 188 try:
189 189 wlock = repo.wlock()
190 190 lock = repo.lock()
191 191 tr = repo.transaction("builddag")
192 192
193 193 at = -1
194 194 atbranch = 'default'
195 195 nodeids = []
196 196 id = 0
197 197 ui.progress(_('building'), id, unit=_('revisions'), total=total)
198 198 for type, data in dagparser.parsedag(text):
199 199 if type == 'n':
200 200 ui.note(('node %s\n' % pycompat.bytestr(data)))
201 201 id, ps = data
202 202
203 203 files = []
204 204 filecontent = {}
205 205
206 206 p2 = None
207 207 if mergeable_file:
208 208 fn = "mf"
209 209 p1 = repo[ps[0]]
210 210 if len(ps) > 1:
211 211 p2 = repo[ps[1]]
212 212 pa = p1.ancestor(p2)
213 213 base, local, other = [x[fn].data() for x in (pa, p1,
214 214 p2)]
215 215 m3 = simplemerge.Merge3Text(base, local, other)
216 216 ml = [l.strip() for l in m3.merge_lines()]
217 217 ml.append("")
218 218 elif at > 0:
219 219 ml = p1[fn].data().split("\n")
220 220 else:
221 221 ml = initialmergedlines
222 222 ml[id * linesperrev] += " r%i" % id
223 223 mergedtext = "\n".join(ml)
224 224 files.append(fn)
225 225 filecontent[fn] = mergedtext
226 226
227 227 if overwritten_file:
228 228 fn = "of"
229 229 files.append(fn)
230 230 filecontent[fn] = "r%i\n" % id
231 231
232 232 if new_file:
233 233 fn = "nf%i" % id
234 234 files.append(fn)
235 235 filecontent[fn] = "r%i\n" % id
236 236 if len(ps) > 1:
237 237 if not p2:
238 238 p2 = repo[ps[1]]
239 239 for fn in p2:
240 240 if fn.startswith("nf"):
241 241 files.append(fn)
242 242 filecontent[fn] = p2[fn].data()
243 243
244 244 def fctxfn(repo, cx, path):
245 245 if path in filecontent:
246 246 return context.memfilectx(repo, cx, path,
247 247 filecontent[path])
248 248 return None
249 249
250 250 if len(ps) == 0 or ps[0] < 0:
251 251 pars = [None, None]
252 252 elif len(ps) == 1:
253 253 pars = [nodeids[ps[0]], None]
254 254 else:
255 255 pars = [nodeids[p] for p in ps]
256 256 cx = context.memctx(repo, pars, "r%i" % id, files, fctxfn,
257 257 date=(id, 0),
258 258 user="debugbuilddag",
259 259 extra={'branch': atbranch})
260 260 nodeid = repo.commitctx(cx)
261 261 nodeids.append(nodeid)
262 262 at = id
263 263 elif type == 'l':
264 264 id, name = data
265 265 ui.note(('tag %s\n' % name))
266 266 tags.append("%s %s\n" % (hex(repo.changelog.node(id)), name))
267 267 elif type == 'a':
268 268 ui.note(('branch %s\n' % data))
269 269 atbranch = data
270 270 ui.progress(_('building'), id, unit=_('revisions'), total=total)
271 271 tr.close()
272 272
273 273 if tags:
274 274 repo.vfs.write("localtags", "".join(tags))
275 275 finally:
276 276 ui.progress(_('building'), None)
277 277 release(tr, lock, wlock)
278 278
279 279 def _debugchangegroup(ui, gen, all=None, indent=0, **opts):
280 280 indent_string = ' ' * indent
281 281 if all:
282 282 ui.write(("%sformat: id, p1, p2, cset, delta base, len(delta)\n")
283 283 % indent_string)
284 284
285 285 def showchunks(named):
286 286 ui.write("\n%s%s\n" % (indent_string, named))
287 287 for deltadata in gen.deltaiter():
288 288 node, p1, p2, cs, deltabase, delta, flags = deltadata
289 289 ui.write("%s%s %s %s %s %s %d\n" %
290 290 (indent_string, hex(node), hex(p1), hex(p2),
291 291 hex(cs), hex(deltabase), len(delta)))
292 292
293 293 chunkdata = gen.changelogheader()
294 294 showchunks("changelog")
295 295 chunkdata = gen.manifestheader()
296 296 showchunks("manifest")
297 297 for chunkdata in iter(gen.filelogheader, {}):
298 298 fname = chunkdata['filename']
299 299 showchunks(fname)
300 300 else:
301 301 if isinstance(gen, bundle2.unbundle20):
302 302 raise error.Abort(_('use debugbundle2 for this file'))
303 303 chunkdata = gen.changelogheader()
304 304 for deltadata in gen.deltaiter():
305 305 node, p1, p2, cs, deltabase, delta, flags = deltadata
306 306 ui.write("%s%s\n" % (indent_string, hex(node)))
307 307
308 308 def _debugobsmarkers(ui, part, indent=0, **opts):
309 309 """display version and markers contained in 'data'"""
310 310 opts = pycompat.byteskwargs(opts)
311 311 data = part.read()
312 312 indent_string = ' ' * indent
313 313 try:
314 314 version, markers = obsolete._readmarkers(data)
315 315 except error.UnknownVersion as exc:
316 316 msg = "%sunsupported version: %s (%d bytes)\n"
317 317 msg %= indent_string, exc.version, len(data)
318 318 ui.write(msg)
319 319 else:
320 320 msg = "%sversion: %d (%d bytes)\n"
321 321 msg %= indent_string, version, len(data)
322 322 ui.write(msg)
323 323 fm = ui.formatter('debugobsolete', opts)
324 324 for rawmarker in sorted(markers):
325 325 m = obsutil.marker(None, rawmarker)
326 326 fm.startitem()
327 327 fm.plain(indent_string)
328 328 cmdutil.showmarker(fm, m)
329 329 fm.end()
330 330
331 331 def _debugphaseheads(ui, data, indent=0):
332 332 """display version and markers contained in 'data'"""
333 333 indent_string = ' ' * indent
334 334 headsbyphase = phases.binarydecode(data)
335 335 for phase in phases.allphases:
336 336 for head in headsbyphase[phase]:
337 337 ui.write(indent_string)
338 338 ui.write('%s %s\n' % (hex(head), phases.phasenames[phase]))
339 339
340 340 def _quasirepr(thing):
341 341 if isinstance(thing, (dict, util.sortdict, collections.OrderedDict)):
342 342 return '{%s}' % (
343 343 b', '.join(b'%s: %s' % (k, thing[k]) for k in sorted(thing)))
344 344 return pycompat.bytestr(repr(thing))
345 345
346 346 def _debugbundle2(ui, gen, all=None, **opts):
347 347 """lists the contents of a bundle2"""
348 348 if not isinstance(gen, bundle2.unbundle20):
349 349 raise error.Abort(_('not a bundle2 file'))
350 350 ui.write(('Stream params: %s\n' % _quasirepr(gen.params)))
351 351 parttypes = opts.get(r'part_type', [])
352 352 for part in gen.iterparts():
353 353 if parttypes and part.type not in parttypes:
354 354 continue
355 355 ui.write('%s -- %s\n' % (part.type, _quasirepr(part.params)))
356 356 if part.type == 'changegroup':
357 357 version = part.params.get('version', '01')
358 358 cg = changegroup.getunbundler(version, part, 'UN')
359 359 if not ui.quiet:
360 360 _debugchangegroup(ui, cg, all=all, indent=4, **opts)
361 361 if part.type == 'obsmarkers':
362 362 if not ui.quiet:
363 363 _debugobsmarkers(ui, part, indent=4, **opts)
364 364 if part.type == 'phase-heads':
365 365 if not ui.quiet:
366 366 _debugphaseheads(ui, part, indent=4)
367 367
368 368 @command('debugbundle',
369 369 [('a', 'all', None, _('show all details')),
370 370 ('', 'part-type', [], _('show only the named part type')),
371 371 ('', 'spec', None, _('print the bundlespec of the bundle'))],
372 372 _('FILE'),
373 373 norepo=True)
374 374 def debugbundle(ui, bundlepath, all=None, spec=None, **opts):
375 375 """lists the contents of a bundle"""
376 376 with hg.openpath(ui, bundlepath) as f:
377 377 if spec:
378 378 spec = exchange.getbundlespec(ui, f)
379 379 ui.write('%s\n' % spec)
380 380 return
381 381
382 382 gen = exchange.readbundle(ui, f, bundlepath)
383 383 if isinstance(gen, bundle2.unbundle20):
384 384 return _debugbundle2(ui, gen, all=all, **opts)
385 385 _debugchangegroup(ui, gen, all=all, **opts)
386 386
387 387 @command('debugcapabilities',
388 388 [], _('PATH'),
389 389 norepo=True)
390 390 def debugcapabilities(ui, path, **opts):
391 391 """lists the capabilities of a remote peer"""
392 392 opts = pycompat.byteskwargs(opts)
393 393 peer = hg.peer(ui, opts, path)
394 394 caps = peer.capabilities()
395 395 ui.write(('Main capabilities:\n'))
396 396 for c in sorted(caps):
397 397 ui.write((' %s\n') % c)
398 398 b2caps = bundle2.bundle2caps(peer)
399 399 if b2caps:
400 400 ui.write(('Bundle2 capabilities:\n'))
401 401 for key, values in sorted(b2caps.iteritems()):
402 402 ui.write((' %s\n') % key)
403 403 for v in values:
404 404 ui.write((' %s\n') % v)
405 405
406 406 @command('debugcheckstate', [], '')
407 407 def debugcheckstate(ui, repo):
408 408 """validate the correctness of the current dirstate"""
409 409 parent1, parent2 = repo.dirstate.parents()
410 410 m1 = repo[parent1].manifest()
411 411 m2 = repo[parent2].manifest()
412 412 errors = 0
413 413 for f in repo.dirstate:
414 414 state = repo.dirstate[f]
415 415 if state in "nr" and f not in m1:
416 416 ui.warn(_("%s in state %s, but not in manifest1\n") % (f, state))
417 417 errors += 1
418 418 if state in "a" and f in m1:
419 419 ui.warn(_("%s in state %s, but also in manifest1\n") % (f, state))
420 420 errors += 1
421 421 if state in "m" and f not in m1 and f not in m2:
422 422 ui.warn(_("%s in state %s, but not in either manifest\n") %
423 423 (f, state))
424 424 errors += 1
425 425 for f in m1:
426 426 state = repo.dirstate[f]
427 427 if state not in "nrm":
428 428 ui.warn(_("%s in manifest1, but listed as state %s") % (f, state))
429 429 errors += 1
430 430 if errors:
431 431 error = _(".hg/dirstate inconsistent with current parent's manifest")
432 432 raise error.Abort(error)
433 433
434 434 @command('debugcolor',
435 435 [('', 'style', None, _('show all configured styles'))],
436 436 'hg debugcolor')
437 437 def debugcolor(ui, repo, **opts):
438 438 """show available color, effects or style"""
439 439 ui.write(('color mode: %s\n') % ui._colormode)
440 440 if opts.get(r'style'):
441 441 return _debugdisplaystyle(ui)
442 442 else:
443 443 return _debugdisplaycolor(ui)
444 444
445 445 def _debugdisplaycolor(ui):
446 446 ui = ui.copy()
447 447 ui._styles.clear()
448 448 for effect in color._activeeffects(ui).keys():
449 449 ui._styles[effect] = effect
450 450 if ui._terminfoparams:
451 451 for k, v in ui.configitems('color'):
452 452 if k.startswith('color.'):
453 453 ui._styles[k] = k[6:]
454 454 elif k.startswith('terminfo.'):
455 455 ui._styles[k] = k[9:]
456 456 ui.write(_('available colors:\n'))
457 457 # sort label with a '_' after the other to group '_background' entry.
458 458 items = sorted(ui._styles.items(),
459 459 key=lambda i: ('_' in i[0], i[0], i[1]))
460 460 for colorname, label in items:
461 461 ui.write(('%s\n') % colorname, label=label)
462 462
463 463 def _debugdisplaystyle(ui):
464 464 ui.write(_('available style:\n'))
465 465 width = max(len(s) for s in ui._styles)
466 466 for label, effects in sorted(ui._styles.items()):
467 467 ui.write('%s' % label, label=label)
468 468 if effects:
469 469 # 50
470 470 ui.write(': ')
471 471 ui.write(' ' * (max(0, width - len(label))))
472 472 ui.write(', '.join(ui.label(e, e) for e in effects.split()))
473 473 ui.write('\n')
474 474
475 475 @command('debugcreatestreamclonebundle', [], 'FILE')
476 476 def debugcreatestreamclonebundle(ui, repo, fname):
477 477 """create a stream clone bundle file
478 478
479 479 Stream bundles are special bundles that are essentially archives of
480 480 revlog files. They are commonly used for cloning very quickly.
481 481 """
482 482 # TODO we may want to turn this into an abort when this functionality
483 483 # is moved into `hg bundle`.
484 484 if phases.hassecret(repo):
485 485 ui.warn(_('(warning: stream clone bundle will contain secret '
486 486 'revisions)\n'))
487 487
488 488 requirements, gen = streamclone.generatebundlev1(repo)
489 489 changegroup.writechunks(ui, gen, fname)
490 490
491 491 ui.write(_('bundle requirements: %s\n') % ', '.join(sorted(requirements)))
492 492
493 493 @command('debugdag',
494 494 [('t', 'tags', None, _('use tags as labels')),
495 495 ('b', 'branches', None, _('annotate with branch names')),
496 496 ('', 'dots', None, _('use dots for runs')),
497 497 ('s', 'spaces', None, _('separate elements by spaces'))],
498 498 _('[OPTION]... [FILE [REV]...]'),
499 499 optionalrepo=True)
500 500 def debugdag(ui, repo, file_=None, *revs, **opts):
501 501 """format the changelog or an index DAG as a concise textual description
502 502
503 503 If you pass a revlog index, the revlog's DAG is emitted. If you list
504 504 revision numbers, they get labeled in the output as rN.
505 505
506 506 Otherwise, the changelog DAG of the current repo is emitted.
507 507 """
508 508 spaces = opts.get(r'spaces')
509 509 dots = opts.get(r'dots')
510 510 if file_:
511 511 rlog = revlog.revlog(vfsmod.vfs(pycompat.getcwd(), audit=False),
512 512 file_)
513 513 revs = set((int(r) for r in revs))
514 514 def events():
515 515 for r in rlog:
516 516 yield 'n', (r, list(p for p in rlog.parentrevs(r)
517 517 if p != -1))
518 518 if r in revs:
519 519 yield 'l', (r, "r%i" % r)
520 520 elif repo:
521 521 cl = repo.changelog
522 522 tags = opts.get(r'tags')
523 523 branches = opts.get(r'branches')
524 524 if tags:
525 525 labels = {}
526 526 for l, n in repo.tags().items():
527 527 labels.setdefault(cl.rev(n), []).append(l)
528 528 def events():
529 529 b = "default"
530 530 for r in cl:
531 531 if branches:
532 532 newb = cl.read(cl.node(r))[5]['branch']
533 533 if newb != b:
534 534 yield 'a', newb
535 535 b = newb
536 536 yield 'n', (r, list(p for p in cl.parentrevs(r)
537 537 if p != -1))
538 538 if tags:
539 539 ls = labels.get(r)
540 540 if ls:
541 541 for l in ls:
542 542 yield 'l', (r, l)
543 543 else:
544 544 raise error.Abort(_('need repo for changelog dag'))
545 545
546 546 for line in dagparser.dagtextlines(events(),
547 547 addspaces=spaces,
548 548 wraplabels=True,
549 549 wrapannotations=True,
550 550 wrapnonlinear=dots,
551 551 usedots=dots,
552 552 maxlinewidth=70):
553 553 ui.write(line)
554 554 ui.write("\n")
555 555
556 556 @command('debugdata', cmdutil.debugrevlogopts, _('-c|-m|FILE REV'))
557 557 def debugdata(ui, repo, file_, rev=None, **opts):
558 558 """dump the contents of a data file revision"""
559 559 opts = pycompat.byteskwargs(opts)
560 560 if opts.get('changelog') or opts.get('manifest') or opts.get('dir'):
561 561 if rev is not None:
562 562 raise error.CommandError('debugdata', _('invalid arguments'))
563 563 file_, rev = None, file_
564 564 elif rev is None:
565 565 raise error.CommandError('debugdata', _('invalid arguments'))
566 566 r = cmdutil.openrevlog(repo, 'debugdata', file_, opts)
567 567 try:
568 568 ui.write(r.revision(r.lookup(rev), raw=True))
569 569 except KeyError:
570 570 raise error.Abort(_('invalid revision identifier %s') % rev)
571 571
572 572 @command('debugdate',
573 573 [('e', 'extended', None, _('try extended date formats'))],
574 574 _('[-e] DATE [RANGE]'),
575 575 norepo=True, optionalrepo=True)
576 576 def debugdate(ui, date, range=None, **opts):
577 577 """parse and display a date"""
578 578 if opts[r"extended"]:
579 579 d = dateutil.parsedate(date, util.extendeddateformats)
580 580 else:
581 581 d = dateutil.parsedate(date)
582 582 ui.write(("internal: %d %d\n") % d)
583 583 ui.write(("standard: %s\n") % dateutil.datestr(d))
584 584 if range:
585 585 m = dateutil.matchdate(range)
586 586 ui.write(("match: %s\n") % m(d[0]))
587 587
588 588 @command('debugdeltachain',
589 589 cmdutil.debugrevlogopts + cmdutil.formatteropts,
590 590 _('-c|-m|FILE'),
591 591 optionalrepo=True)
592 592 def debugdeltachain(ui, repo, file_=None, **opts):
593 593 """dump information about delta chains in a revlog
594 594
595 595 Output can be templatized. Available template keywords are:
596 596
597 597 :``rev``: revision number
598 598 :``chainid``: delta chain identifier (numbered by unique base)
599 599 :``chainlen``: delta chain length to this revision
600 600 :``prevrev``: previous revision in delta chain
601 601 :``deltatype``: role of delta / how it was computed
602 602 :``compsize``: compressed size of revision
603 603 :``uncompsize``: uncompressed size of revision
604 604 :``chainsize``: total size of compressed revisions in chain
605 605 :``chainratio``: total chain size divided by uncompressed revision size
606 606 (new delta chains typically start at ratio 2.00)
607 607 :``lindist``: linear distance from base revision in delta chain to end
608 608 of this revision
609 609 :``extradist``: total size of revisions not part of this delta chain from
610 610 base of delta chain to end of this revision; a measurement
611 611 of how much extra data we need to read/seek across to read
612 612 the delta chain for this revision
613 613 :``extraratio``: extradist divided by chainsize; another representation of
614 614 how much unrelated data is needed to load this delta chain
615 615
616 616 If the repository is configured to use the sparse read, additional keywords
617 617 are available:
618 618
619 619 :``readsize``: total size of data read from the disk for a revision
620 620 (sum of the sizes of all the blocks)
621 621 :``largestblock``: size of the largest block of data read from the disk
622 622 :``readdensity``: density of useful bytes in the data read from the disk
623 623 :``srchunks``: in how many data hunks the whole revision would be read
624 624
625 625 The sparse read can be enabled with experimental.sparse-read = True
626 626 """
627 627 opts = pycompat.byteskwargs(opts)
628 628 r = cmdutil.openrevlog(repo, 'debugdeltachain', file_, opts)
629 629 index = r.index
630 630 generaldelta = r.version & revlog.FLAG_GENERALDELTA
631 631 withsparseread = getattr(r, '_withsparseread', False)
632 632
633 633 def revinfo(rev):
634 634 e = index[rev]
635 635 compsize = e[1]
636 636 uncompsize = e[2]
637 637 chainsize = 0
638 638
639 639 if generaldelta:
640 640 if e[3] == e[5]:
641 641 deltatype = 'p1'
642 642 elif e[3] == e[6]:
643 643 deltatype = 'p2'
644 644 elif e[3] == rev - 1:
645 645 deltatype = 'prev'
646 646 elif e[3] == rev:
647 647 deltatype = 'base'
648 648 else:
649 649 deltatype = 'other'
650 650 else:
651 651 if e[3] == rev:
652 652 deltatype = 'base'
653 653 else:
654 654 deltatype = 'prev'
655 655
656 656 chain = r._deltachain(rev)[0]
657 657 for iterrev in chain:
658 658 e = index[iterrev]
659 659 chainsize += e[1]
660 660
661 661 return compsize, uncompsize, deltatype, chain, chainsize
662 662
663 663 fm = ui.formatter('debugdeltachain', opts)
664 664
665 665 fm.plain(' rev chain# chainlen prev delta '
666 666 'size rawsize chainsize ratio lindist extradist '
667 667 'extraratio')
668 668 if withsparseread:
669 669 fm.plain(' readsize largestblk rddensity srchunks')
670 670 fm.plain('\n')
671 671
672 672 chainbases = {}
673 673 for rev in r:
674 674 comp, uncomp, deltatype, chain, chainsize = revinfo(rev)
675 675 chainbase = chain[0]
676 676 chainid = chainbases.setdefault(chainbase, len(chainbases) + 1)
677 677 start = r.start
678 678 length = r.length
679 679 basestart = start(chainbase)
680 680 revstart = start(rev)
681 681 lineardist = revstart + comp - basestart
682 682 extradist = lineardist - chainsize
683 683 try:
684 684 prevrev = chain[-2]
685 685 except IndexError:
686 686 prevrev = -1
687 687
688 688 chainratio = float(chainsize) / float(uncomp)
689 689 extraratio = float(extradist) / float(chainsize)
690 690
691 691 fm.startitem()
692 692 fm.write('rev chainid chainlen prevrev deltatype compsize '
693 693 'uncompsize chainsize chainratio lindist extradist '
694 694 'extraratio',
695 695 '%7d %7d %8d %8d %7s %10d %10d %10d %9.5f %9d %9d %10.5f',
696 696 rev, chainid, len(chain), prevrev, deltatype, comp,
697 697 uncomp, chainsize, chainratio, lineardist, extradist,
698 698 extraratio,
699 699 rev=rev, chainid=chainid, chainlen=len(chain),
700 700 prevrev=prevrev, deltatype=deltatype, compsize=comp,
701 701 uncompsize=uncomp, chainsize=chainsize,
702 702 chainratio=chainratio, lindist=lineardist,
703 703 extradist=extradist, extraratio=extraratio)
704 704 if withsparseread:
705 705 readsize = 0
706 706 largestblock = 0
707 707 srchunks = 0
708 708
709 709 for revschunk in revlog._slicechunk(r, chain):
710 710 srchunks += 1
711 711 blkend = start(revschunk[-1]) + length(revschunk[-1])
712 712 blksize = blkend - start(revschunk[0])
713 713
714 714 readsize += blksize
715 715 if largestblock < blksize:
716 716 largestblock = blksize
717 717
718 718 readdensity = float(chainsize) / float(readsize)
719 719
720 720 fm.write('readsize largestblock readdensity srchunks',
721 721 ' %10d %10d %9.5f %8d',
722 722 readsize, largestblock, readdensity, srchunks,
723 723 readsize=readsize, largestblock=largestblock,
724 724 readdensity=readdensity, srchunks=srchunks)
725 725
726 726 fm.plain('\n')
727 727
728 728 fm.end()
729 729
730 730 @command('debugdirstate|debugstate',
731 731 [('', 'nodates', None, _('do not display the saved mtime')),
732 732 ('', 'datesort', None, _('sort by saved mtime'))],
733 733 _('[OPTION]...'))
734 734 def debugstate(ui, repo, **opts):
735 735 """show the contents of the current dirstate"""
736 736
737 737 nodates = opts.get(r'nodates')
738 738 datesort = opts.get(r'datesort')
739 739
740 740 timestr = ""
741 741 if datesort:
742 742 keyfunc = lambda x: (x[1][3], x[0]) # sort by mtime, then by filename
743 743 else:
744 744 keyfunc = None # sort by filename
745 745 for file_, ent in sorted(repo.dirstate._map.iteritems(), key=keyfunc):
746 746 if ent[3] == -1:
747 747 timestr = 'unset '
748 748 elif nodates:
749 749 timestr = 'set '
750 750 else:
751 751 timestr = time.strftime(r"%Y-%m-%d %H:%M:%S ",
752 752 time.localtime(ent[3]))
753 753 timestr = encoding.strtolocal(timestr)
754 754 if ent[1] & 0o20000:
755 755 mode = 'lnk'
756 756 else:
757 757 mode = '%3o' % (ent[1] & 0o777 & ~util.umask)
758 758 ui.write("%c %s %10d %s%s\n" % (ent[0], mode, ent[2], timestr, file_))
759 759 for f in repo.dirstate.copies():
760 760 ui.write(_("copy: %s -> %s\n") % (repo.dirstate.copied(f), f))
761 761
762 762 @command('debugdiscovery',
763 763 [('', 'old', None, _('use old-style discovery')),
764 764 ('', 'nonheads', None,
765 765 _('use old-style discovery with non-heads included')),
766 766 ('', 'rev', [], 'restrict discovery to this set of revs'),
767 767 ] + cmdutil.remoteopts,
768 768 _('[--rev REV] [OTHER]'))
769 769 def debugdiscovery(ui, repo, remoteurl="default", **opts):
770 770 """runs the changeset discovery protocol in isolation"""
771 771 opts = pycompat.byteskwargs(opts)
772 772 remoteurl, branches = hg.parseurl(ui.expandpath(remoteurl))
773 773 remote = hg.peer(repo, opts, remoteurl)
774 774 ui.status(_('comparing with %s\n') % util.hidepassword(remoteurl))
775 775
776 776 # make sure tests are repeatable
777 777 random.seed(12323)
778 778
779 779 def doit(pushedrevs, remoteheads, remote=remote):
780 780 if opts.get('old'):
781 781 if not util.safehasattr(remote, 'branches'):
782 782 # enable in-client legacy support
783 783 remote = localrepo.locallegacypeer(remote.local())
784 784 common, _in, hds = treediscovery.findcommonincoming(repo, remote,
785 785 force=True)
786 786 common = set(common)
787 787 if not opts.get('nonheads'):
788 788 ui.write(("unpruned common: %s\n") %
789 789 " ".join(sorted(short(n) for n in common)))
790 790 dag = dagutil.revlogdag(repo.changelog)
791 791 all = dag.ancestorset(dag.internalizeall(common))
792 792 common = dag.externalizeall(dag.headsetofconnecteds(all))
793 793 else:
794 794 nodes = None
795 795 if pushedrevs:
796 796 revs = scmutil.revrange(repo, pushedrevs)
797 797 nodes = [repo[r].node() for r in revs]
798 798 common, any, hds = setdiscovery.findcommonheads(ui, repo, remote,
799 799 ancestorsof=nodes)
800 800 common = set(common)
801 801 rheads = set(hds)
802 802 lheads = set(repo.heads())
803 803 ui.write(("common heads: %s\n") %
804 804 " ".join(sorted(short(n) for n in common)))
805 805 if lheads <= common:
806 806 ui.write(("local is subset\n"))
807 807 elif rheads <= common:
808 808 ui.write(("remote is subset\n"))
809 809
810 810 remoterevs, _checkout = hg.addbranchrevs(repo, remote, branches, revs=None)
811 811 localrevs = opts['rev']
812 812 doit(localrevs, remoterevs)
813 813
814 814 _chunksize = 4 << 10
815 815
816 816 @command('debugdownload',
817 817 [
818 818 ('o', 'output', '', _('path')),
819 819 ],
820 820 optionalrepo=True)
821 821 def debugdownload(ui, repo, url, output=None, **opts):
822 822 """download a resource using Mercurial logic and config
823 823 """
824 824 fh = urlmod.open(ui, url, output)
825 825
826 826 dest = ui
827 827 if output:
828 828 dest = open(output, "wb", _chunksize)
829 829 try:
830 830 data = fh.read(_chunksize)
831 831 while data:
832 832 dest.write(data)
833 833 data = fh.read(_chunksize)
834 834 finally:
835 835 if output:
836 836 dest.close()
837 837
838 838 @command('debugextensions', cmdutil.formatteropts, [], norepo=True)
839 839 def debugextensions(ui, **opts):
840 840 '''show information about active extensions'''
841 841 opts = pycompat.byteskwargs(opts)
842 842 exts = extensions.extensions(ui)
843 843 hgver = util.version()
844 844 fm = ui.formatter('debugextensions', opts)
845 845 for extname, extmod in sorted(exts, key=operator.itemgetter(0)):
846 846 isinternal = extensions.ismoduleinternal(extmod)
847 847 extsource = pycompat.fsencode(extmod.__file__)
848 848 if isinternal:
849 849 exttestedwith = [] # never expose magic string to users
850 850 else:
851 851 exttestedwith = getattr(extmod, 'testedwith', '').split()
852 852 extbuglink = getattr(extmod, 'buglink', None)
853 853
854 854 fm.startitem()
855 855
856 856 if ui.quiet or ui.verbose:
857 857 fm.write('name', '%s\n', extname)
858 858 else:
859 859 fm.write('name', '%s', extname)
860 860 if isinternal or hgver in exttestedwith:
861 861 fm.plain('\n')
862 862 elif not exttestedwith:
863 863 fm.plain(_(' (untested!)\n'))
864 864 else:
865 865 lasttestedversion = exttestedwith[-1]
866 866 fm.plain(' (%s!)\n' % lasttestedversion)
867 867
868 868 fm.condwrite(ui.verbose and extsource, 'source',
869 869 _(' location: %s\n'), extsource or "")
870 870
871 871 if ui.verbose:
872 872 fm.plain(_(' bundled: %s\n') % ['no', 'yes'][isinternal])
873 873 fm.data(bundled=isinternal)
874 874
875 875 fm.condwrite(ui.verbose and exttestedwith, 'testedwith',
876 876 _(' tested with: %s\n'),
877 877 fm.formatlist(exttestedwith, name='ver'))
878 878
879 879 fm.condwrite(ui.verbose and extbuglink, 'buglink',
880 880 _(' bug reporting: %s\n'), extbuglink or "")
881 881
882 882 fm.end()
883 883
884 884 @command('debugfileset',
885 885 [('r', 'rev', '', _('apply the filespec on this revision'), _('REV'))],
886 886 _('[-r REV] FILESPEC'))
887 887 def debugfileset(ui, repo, expr, **opts):
888 888 '''parse and apply a fileset specification'''
889 889 ctx = scmutil.revsingle(repo, opts.get(r'rev'), None)
890 890 if ui.verbose:
891 891 tree = fileset.parse(expr)
892 892 ui.note(fileset.prettyformat(tree), "\n")
893 893
894 894 for f in ctx.getfileset(expr):
895 895 ui.write("%s\n" % f)
896 896
897 897 @command('debugformat',
898 898 [] + cmdutil.formatteropts,
899 899 _(''))
900 900 def debugformat(ui, repo, **opts):
901 901 """display format information about the current repository
902 902
903 903 Use --verbose to get extra information about current config value and
904 904 Mercurial default."""
905 905 opts = pycompat.byteskwargs(opts)
906 906 maxvariantlength = max(len(fv.name) for fv in upgrade.allformatvariant)
907 907 maxvariantlength = max(len('format-variant'), maxvariantlength)
908 908
909 909 def makeformatname(name):
910 910 return '%s:' + (' ' * (maxvariantlength - len(name)))
911 911
912 912 fm = ui.formatter('debugformat', opts)
913 913 if fm.isplain():
914 914 def formatvalue(value):
915 915 if util.safehasattr(value, 'startswith'):
916 916 return value
917 917 if value:
918 918 return 'yes'
919 919 else:
920 920 return 'no'
921 921 else:
922 922 formatvalue = pycompat.identity
923 923
924 924 fm.plain('format-variant')
925 925 fm.plain(' ' * (maxvariantlength - len('format-variant')))
926 926 fm.plain(' repo')
927 927 if ui.verbose:
928 928 fm.plain(' config default')
929 929 fm.plain('\n')
930 930 for fv in upgrade.allformatvariant:
931 931 fm.startitem()
932 932 repovalue = fv.fromrepo(repo)
933 933 configvalue = fv.fromconfig(repo)
934 934
935 935 if repovalue != configvalue:
936 936 namelabel = 'formatvariant.name.mismatchconfig'
937 937 repolabel = 'formatvariant.repo.mismatchconfig'
938 938 elif repovalue != fv.default:
939 939 namelabel = 'formatvariant.name.mismatchdefault'
940 940 repolabel = 'formatvariant.repo.mismatchdefault'
941 941 else:
942 942 namelabel = 'formatvariant.name.uptodate'
943 943 repolabel = 'formatvariant.repo.uptodate'
944 944
945 945 fm.write('name', makeformatname(fv.name), fv.name,
946 946 label=namelabel)
947 947 fm.write('repo', ' %3s', formatvalue(repovalue),
948 948 label=repolabel)
949 949 if fv.default != configvalue:
950 950 configlabel = 'formatvariant.config.special'
951 951 else:
952 952 configlabel = 'formatvariant.config.default'
953 953 fm.condwrite(ui.verbose, 'config', ' %6s', formatvalue(configvalue),
954 954 label=configlabel)
955 955 fm.condwrite(ui.verbose, 'default', ' %7s', formatvalue(fv.default),
956 956 label='formatvariant.default')
957 957 fm.plain('\n')
958 958 fm.end()
959 959
960 960 @command('debugfsinfo', [], _('[PATH]'), norepo=True)
961 961 def debugfsinfo(ui, path="."):
962 962 """show information detected about current filesystem"""
963 963 ui.write(('path: %s\n') % path)
964 964 ui.write(('mounted on: %s\n') % (util.getfsmountpoint(path) or '(unknown)'))
965 965 ui.write(('exec: %s\n') % (util.checkexec(path) and 'yes' or 'no'))
966 966 ui.write(('fstype: %s\n') % (util.getfstype(path) or '(unknown)'))
967 967 ui.write(('symlink: %s\n') % (util.checklink(path) and 'yes' or 'no'))
968 968 ui.write(('hardlink: %s\n') % (util.checknlink(path) and 'yes' or 'no'))
969 969 casesensitive = '(unknown)'
970 970 try:
971 971 with tempfile.NamedTemporaryFile(prefix='.debugfsinfo', dir=path) as f:
972 972 casesensitive = util.fscasesensitive(f.name) and 'yes' or 'no'
973 973 except OSError:
974 974 pass
975 975 ui.write(('case-sensitive: %s\n') % casesensitive)
976 976
977 977 @command('debuggetbundle',
978 978 [('H', 'head', [], _('id of head node'), _('ID')),
979 979 ('C', 'common', [], _('id of common node'), _('ID')),
980 980 ('t', 'type', 'bzip2', _('bundle compression type to use'), _('TYPE'))],
981 981 _('REPO FILE [-H|-C ID]...'),
982 982 norepo=True)
983 983 def debuggetbundle(ui, repopath, bundlepath, head=None, common=None, **opts):
984 984 """retrieves a bundle from a repo
985 985
986 986 Every ID must be a full-length hex node id string. Saves the bundle to the
987 987 given file.
988 988 """
989 989 opts = pycompat.byteskwargs(opts)
990 990 repo = hg.peer(ui, opts, repopath)
991 991 if not repo.capable('getbundle'):
992 992 raise error.Abort("getbundle() not supported by target repository")
993 993 args = {}
994 994 if common:
995 995 args[r'common'] = [bin(s) for s in common]
996 996 if head:
997 997 args[r'heads'] = [bin(s) for s in head]
998 998 # TODO: get desired bundlecaps from command line.
999 999 args[r'bundlecaps'] = None
1000 1000 bundle = repo.getbundle('debug', **args)
1001 1001
1002 1002 bundletype = opts.get('type', 'bzip2').lower()
1003 1003 btypes = {'none': 'HG10UN',
1004 1004 'bzip2': 'HG10BZ',
1005 1005 'gzip': 'HG10GZ',
1006 1006 'bundle2': 'HG20'}
1007 1007 bundletype = btypes.get(bundletype)
1008 1008 if bundletype not in bundle2.bundletypes:
1009 1009 raise error.Abort(_('unknown bundle type specified with --type'))
1010 1010 bundle2.writebundle(ui, bundle, bundlepath, bundletype)
1011 1011
1012 1012 @command('debugignore', [], '[FILE]')
1013 1013 def debugignore(ui, repo, *files, **opts):
1014 1014 """display the combined ignore pattern and information about ignored files
1015 1015
1016 1016 With no argument display the combined ignore pattern.
1017 1017
1018 1018 Given space separated file names, shows if the given file is ignored and
1019 1019 if so, show the ignore rule (file and line number) that matched it.
1020 1020 """
1021 1021 ignore = repo.dirstate._ignore
1022 1022 if not files:
1023 1023 # Show all the patterns
1024 1024 ui.write("%s\n" % pycompat.byterepr(ignore))
1025 1025 else:
1026 1026 m = scmutil.match(repo[None], pats=files)
1027 1027 for f in m.files():
1028 1028 nf = util.normpath(f)
1029 1029 ignored = None
1030 1030 ignoredata = None
1031 1031 if nf != '.':
1032 1032 if ignore(nf):
1033 1033 ignored = nf
1034 1034 ignoredata = repo.dirstate._ignorefileandline(nf)
1035 1035 else:
1036 1036 for p in util.finddirs(nf):
1037 1037 if ignore(p):
1038 1038 ignored = p
1039 1039 ignoredata = repo.dirstate._ignorefileandline(p)
1040 1040 break
1041 1041 if ignored:
1042 1042 if ignored == nf:
1043 1043 ui.write(_("%s is ignored\n") % m.uipath(f))
1044 1044 else:
1045 1045 ui.write(_("%s is ignored because of "
1046 1046 "containing folder %s\n")
1047 1047 % (m.uipath(f), ignored))
1048 1048 ignorefile, lineno, line = ignoredata
1049 1049 ui.write(_("(ignore rule in %s, line %d: '%s')\n")
1050 1050 % (ignorefile, lineno, line))
1051 1051 else:
1052 1052 ui.write(_("%s is not ignored\n") % m.uipath(f))
1053 1053
1054 1054 @command('debugindex', cmdutil.debugrevlogopts +
1055 1055 [('f', 'format', 0, _('revlog format'), _('FORMAT'))],
1056 1056 _('[-f FORMAT] -c|-m|FILE'),
1057 1057 optionalrepo=True)
1058 1058 def debugindex(ui, repo, file_=None, **opts):
1059 1059 """dump the contents of an index file"""
1060 1060 opts = pycompat.byteskwargs(opts)
1061 1061 r = cmdutil.openrevlog(repo, 'debugindex', file_, opts)
1062 1062 format = opts.get('format', 0)
1063 1063 if format not in (0, 1):
1064 1064 raise error.Abort(_("unknown format %d") % format)
1065 1065
1066 1066 if ui.debugflag:
1067 1067 shortfn = hex
1068 1068 else:
1069 1069 shortfn = short
1070 1070
1071 1071 # There might not be anything in r, so have a sane default
1072 1072 idlen = 12
1073 1073 for i in r:
1074 1074 idlen = len(shortfn(r.node(i)))
1075 1075 break
1076 1076
1077 1077 if format == 0:
1078 1078 if ui.verbose:
1079 1079 ui.write((" rev offset length linkrev"
1080 1080 " %s %s p2\n") % ("nodeid".ljust(idlen),
1081 1081 "p1".ljust(idlen)))
1082 1082 else:
1083 1083 ui.write((" rev linkrev %s %s p2\n") % (
1084 1084 "nodeid".ljust(idlen), "p1".ljust(idlen)))
1085 1085 elif format == 1:
1086 1086 if ui.verbose:
1087 1087 ui.write((" rev flag offset length size link p1"
1088 1088 " p2 %s\n") % "nodeid".rjust(idlen))
1089 1089 else:
1090 1090 ui.write((" rev flag size link p1 p2 %s\n") %
1091 1091 "nodeid".rjust(idlen))
1092 1092
1093 1093 for i in r:
1094 1094 node = r.node(i)
1095 1095 if format == 0:
1096 1096 try:
1097 1097 pp = r.parents(node)
1098 1098 except Exception:
1099 1099 pp = [nullid, nullid]
1100 1100 if ui.verbose:
1101 1101 ui.write("% 6d % 9d % 7d % 7d %s %s %s\n" % (
1102 1102 i, r.start(i), r.length(i), r.linkrev(i),
1103 1103 shortfn(node), shortfn(pp[0]), shortfn(pp[1])))
1104 1104 else:
1105 1105 ui.write("% 6d % 7d %s %s %s\n" % (
1106 1106 i, r.linkrev(i), shortfn(node), shortfn(pp[0]),
1107 1107 shortfn(pp[1])))
1108 1108 elif format == 1:
1109 1109 pr = r.parentrevs(i)
1110 1110 if ui.verbose:
1111 1111 ui.write("% 6d %04x % 8d % 8d % 8d % 6d % 6d % 6d %s\n" % (
1112 1112 i, r.flags(i), r.start(i), r.length(i), r.rawsize(i),
1113 1113 r.linkrev(i), pr[0], pr[1], shortfn(node)))
1114 1114 else:
1115 1115 ui.write("% 6d %04x % 8d % 6d % 6d % 6d %s\n" % (
1116 1116 i, r.flags(i), r.rawsize(i), r.linkrev(i), pr[0], pr[1],
1117 1117 shortfn(node)))
1118 1118
1119 1119 @command('debugindexdot', cmdutil.debugrevlogopts,
1120 1120 _('-c|-m|FILE'), optionalrepo=True)
1121 1121 def debugindexdot(ui, repo, file_=None, **opts):
1122 1122 """dump an index DAG as a graphviz dot file"""
1123 1123 opts = pycompat.byteskwargs(opts)
1124 1124 r = cmdutil.openrevlog(repo, 'debugindexdot', file_, opts)
1125 1125 ui.write(("digraph G {\n"))
1126 1126 for i in r:
1127 1127 node = r.node(i)
1128 1128 pp = r.parents(node)
1129 1129 ui.write("\t%d -> %d\n" % (r.rev(pp[0]), i))
1130 1130 if pp[1] != nullid:
1131 1131 ui.write("\t%d -> %d\n" % (r.rev(pp[1]), i))
1132 1132 ui.write("}\n")
1133 1133
1134 1134 @command('debuginstall', [] + cmdutil.formatteropts, '', norepo=True)
1135 1135 def debuginstall(ui, **opts):
1136 1136 '''test Mercurial installation
1137 1137
1138 1138 Returns 0 on success.
1139 1139 '''
1140 1140 opts = pycompat.byteskwargs(opts)
1141 1141
1142 1142 def writetemp(contents):
1143 1143 (fd, name) = tempfile.mkstemp(prefix="hg-debuginstall-")
1144 1144 f = os.fdopen(fd, r"wb")
1145 1145 f.write(contents)
1146 1146 f.close()
1147 1147 return name
1148 1148
1149 1149 problems = 0
1150 1150
1151 1151 fm = ui.formatter('debuginstall', opts)
1152 1152 fm.startitem()
1153 1153
1154 1154 # encoding
1155 1155 fm.write('encoding', _("checking encoding (%s)...\n"), encoding.encoding)
1156 1156 err = None
1157 1157 try:
1158 1158 codecs.lookup(pycompat.sysstr(encoding.encoding))
1159 1159 except LookupError as inst:
1160 1160 err = stringutil.forcebytestr(inst)
1161 1161 problems += 1
1162 1162 fm.condwrite(err, 'encodingerror', _(" %s\n"
1163 1163 " (check that your locale is properly set)\n"), err)
1164 1164
1165 1165 # Python
1166 1166 fm.write('pythonexe', _("checking Python executable (%s)\n"),
1167 1167 pycompat.sysexecutable)
1168 1168 fm.write('pythonver', _("checking Python version (%s)\n"),
1169 1169 ("%d.%d.%d" % sys.version_info[:3]))
1170 1170 fm.write('pythonlib', _("checking Python lib (%s)...\n"),
1171 1171 os.path.dirname(pycompat.fsencode(os.__file__)))
1172 1172
1173 1173 security = set(sslutil.supportedprotocols)
1174 1174 if sslutil.hassni:
1175 1175 security.add('sni')
1176 1176
1177 1177 fm.write('pythonsecurity', _("checking Python security support (%s)\n"),
1178 1178 fm.formatlist(sorted(security), name='protocol',
1179 1179 fmt='%s', sep=','))
1180 1180
1181 1181 # These are warnings, not errors. So don't increment problem count. This
1182 1182 # may change in the future.
1183 1183 if 'tls1.2' not in security:
1184 1184 fm.plain(_(' TLS 1.2 not supported by Python install; '
1185 1185 'network connections lack modern security\n'))
1186 1186 if 'sni' not in security:
1187 1187 fm.plain(_(' SNI not supported by Python install; may have '
1188 1188 'connectivity issues with some servers\n'))
1189 1189
1190 1190 # TODO print CA cert info
1191 1191
1192 1192 # hg version
1193 1193 hgver = util.version()
1194 1194 fm.write('hgver', _("checking Mercurial version (%s)\n"),
1195 1195 hgver.split('+')[0])
1196 1196 fm.write('hgverextra', _("checking Mercurial custom build (%s)\n"),
1197 1197 '+'.join(hgver.split('+')[1:]))
1198 1198
1199 1199 # compiled modules
1200 1200 fm.write('hgmodulepolicy', _("checking module policy (%s)\n"),
1201 1201 policy.policy)
1202 1202 fm.write('hgmodules', _("checking installed modules (%s)...\n"),
1203 1203 os.path.dirname(pycompat.fsencode(__file__)))
1204 1204
1205 1205 if policy.policy in ('c', 'allow'):
1206 1206 err = None
1207 1207 try:
1208 1208 from .cext import (
1209 1209 base85,
1210 1210 bdiff,
1211 1211 mpatch,
1212 1212 osutil,
1213 1213 )
1214 1214 dir(bdiff), dir(mpatch), dir(base85), dir(osutil) # quiet pyflakes
1215 1215 except Exception as inst:
1216 1216 err = stringutil.forcebytestr(inst)
1217 1217 problems += 1
1218 1218 fm.condwrite(err, 'extensionserror', " %s\n", err)
1219 1219
1220 1220 compengines = util.compengines._engines.values()
1221 1221 fm.write('compengines', _('checking registered compression engines (%s)\n'),
1222 1222 fm.formatlist(sorted(e.name() for e in compengines),
1223 1223 name='compengine', fmt='%s', sep=', '))
1224 1224 fm.write('compenginesavail', _('checking available compression engines '
1225 1225 '(%s)\n'),
1226 1226 fm.formatlist(sorted(e.name() for e in compengines
1227 1227 if e.available()),
1228 1228 name='compengine', fmt='%s', sep=', '))
1229 1229 wirecompengines = util.compengines.supportedwireengines(util.SERVERROLE)
1230 1230 fm.write('compenginesserver', _('checking available compression engines '
1231 1231 'for wire protocol (%s)\n'),
1232 1232 fm.formatlist([e.name() for e in wirecompengines
1233 1233 if e.wireprotosupport()],
1234 1234 name='compengine', fmt='%s', sep=', '))
1235 1235 re2 = 'missing'
1236 1236 if util._re2:
1237 1237 re2 = 'available'
1238 1238 fm.plain(_('checking "re2" regexp engine (%s)\n') % re2)
1239 1239 fm.data(re2=bool(util._re2))
1240 1240
1241 1241 # templates
1242 1242 p = templater.templatepaths()
1243 1243 fm.write('templatedirs', 'checking templates (%s)...\n', ' '.join(p))
1244 1244 fm.condwrite(not p, '', _(" no template directories found\n"))
1245 1245 if p:
1246 1246 m = templater.templatepath("map-cmdline.default")
1247 1247 if m:
1248 1248 # template found, check if it is working
1249 1249 err = None
1250 1250 try:
1251 1251 templater.templater.frommapfile(m)
1252 1252 except Exception as inst:
1253 1253 err = stringutil.forcebytestr(inst)
1254 1254 p = None
1255 1255 fm.condwrite(err, 'defaulttemplateerror', " %s\n", err)
1256 1256 else:
1257 1257 p = None
1258 1258 fm.condwrite(p, 'defaulttemplate',
1259 1259 _("checking default template (%s)\n"), m)
1260 1260 fm.condwrite(not m, 'defaulttemplatenotfound',
1261 1261 _(" template '%s' not found\n"), "default")
1262 1262 if not p:
1263 1263 problems += 1
1264 1264 fm.condwrite(not p, '',
1265 1265 _(" (templates seem to have been installed incorrectly)\n"))
1266 1266
1267 1267 # editor
1268 1268 editor = ui.geteditor()
1269 1269 editor = util.expandpath(editor)
1270 1270 editorbin = procutil.shellsplit(editor)[0]
1271 1271 fm.write('editor', _("checking commit editor... (%s)\n"), editorbin)
1272 1272 cmdpath = procutil.findexe(editorbin)
1273 1273 fm.condwrite(not cmdpath and editor == 'vi', 'vinotfound',
1274 1274 _(" No commit editor set and can't find %s in PATH\n"
1275 1275 " (specify a commit editor in your configuration"
1276 1276 " file)\n"), not cmdpath and editor == 'vi' and editorbin)
1277 1277 fm.condwrite(not cmdpath and editor != 'vi', 'editornotfound',
1278 1278 _(" Can't find editor '%s' in PATH\n"
1279 1279 " (specify a commit editor in your configuration"
1280 1280 " file)\n"), not cmdpath and editorbin)
1281 1281 if not cmdpath and editor != 'vi':
1282 1282 problems += 1
1283 1283
1284 1284 # check username
1285 1285 username = None
1286 1286 err = None
1287 1287 try:
1288 1288 username = ui.username()
1289 1289 except error.Abort as e:
1290 1290 err = stringutil.forcebytestr(e)
1291 1291 problems += 1
1292 1292
1293 1293 fm.condwrite(username, 'username', _("checking username (%s)\n"), username)
1294 1294 fm.condwrite(err, 'usernameerror', _("checking username...\n %s\n"
1295 1295 " (specify a username in your configuration file)\n"), err)
1296 1296
1297 1297 fm.condwrite(not problems, '',
1298 1298 _("no problems detected\n"))
1299 1299 if not problems:
1300 1300 fm.data(problems=problems)
1301 1301 fm.condwrite(problems, 'problems',
1302 1302 _("%d problems detected,"
1303 1303 " please check your install!\n"), problems)
1304 1304 fm.end()
1305 1305
1306 1306 return problems
1307 1307
1308 1308 @command('debugknown', [], _('REPO ID...'), norepo=True)
1309 1309 def debugknown(ui, repopath, *ids, **opts):
1310 1310 """test whether node ids are known to a repo
1311 1311
1312 1312 Every ID must be a full-length hex node id string. Returns a list of 0s
1313 1313 and 1s indicating unknown/known.
1314 1314 """
1315 1315 opts = pycompat.byteskwargs(opts)
1316 1316 repo = hg.peer(ui, opts, repopath)
1317 1317 if not repo.capable('known'):
1318 1318 raise error.Abort("known() not supported by target repository")
1319 1319 flags = repo.known([bin(s) for s in ids])
1320 1320 ui.write("%s\n" % ("".join([f and "1" or "0" for f in flags])))
1321 1321
1322 1322 @command('debuglabelcomplete', [], _('LABEL...'))
1323 1323 def debuglabelcomplete(ui, repo, *args):
1324 1324 '''backwards compatibility with old bash completion scripts (DEPRECATED)'''
1325 1325 debugnamecomplete(ui, repo, *args)
1326 1326
1327 1327 @command('debuglocks',
1328 1328 [('L', 'force-lock', None, _('free the store lock (DANGEROUS)')),
1329 1329 ('W', 'force-wlock', None,
1330 1330 _('free the working state lock (DANGEROUS)')),
1331 1331 ('s', 'set-lock', None, _('set the store lock until stopped')),
1332 1332 ('S', 'set-wlock', None,
1333 1333 _('set the working state lock until stopped'))],
1334 1334 _('[OPTION]...'))
1335 1335 def debuglocks(ui, repo, **opts):
1336 1336 """show or modify state of locks
1337 1337
1338 1338 By default, this command will show which locks are held. This
1339 1339 includes the user and process holding the lock, the amount of time
1340 1340 the lock has been held, and the machine name where the process is
1341 1341 running if it's not local.
1342 1342
1343 1343 Locks protect the integrity of Mercurial's data, so should be
1344 1344 treated with care. System crashes or other interruptions may cause
1345 1345 locks to not be properly released, though Mercurial will usually
1346 1346 detect and remove such stale locks automatically.
1347 1347
1348 1348 However, detecting stale locks may not always be possible (for
1349 1349 instance, on a shared filesystem). Removing locks may also be
1350 1350 blocked by filesystem permissions.
1351 1351
1352 1352 Setting a lock will prevent other commands from changing the data.
1353 1353 The command will wait until an interruption (SIGINT, SIGTERM, ...) occurs.
1354 1354 The set locks are removed when the command exits.
1355 1355
1356 1356 Returns 0 if no locks are held.
1357 1357
1358 1358 """
1359 1359
1360 1360 if opts.get(r'force_lock'):
1361 1361 repo.svfs.unlink('lock')
1362 1362 if opts.get(r'force_wlock'):
1363 1363 repo.vfs.unlink('wlock')
1364 1364 if opts.get(r'force_lock') or opts.get(r'force_wlock'):
1365 1365 return 0
1366 1366
1367 1367 locks = []
1368 1368 try:
1369 1369 if opts.get(r'set_wlock'):
1370 1370 try:
1371 1371 locks.append(repo.wlock(False))
1372 1372 except error.LockHeld:
1373 1373 raise error.Abort(_('wlock is already held'))
1374 1374 if opts.get(r'set_lock'):
1375 1375 try:
1376 1376 locks.append(repo.lock(False))
1377 1377 except error.LockHeld:
1378 1378 raise error.Abort(_('lock is already held'))
1379 1379 if len(locks):
1380 1380 ui.promptchoice(_("ready to release the lock (y)? $$ &Yes"))
1381 1381 return 0
1382 1382 finally:
1383 1383 release(*locks)
1384 1384
1385 1385 now = time.time()
1386 1386 held = 0
1387 1387
1388 1388 def report(vfs, name, method):
1389 1389 # this causes stale locks to get reaped for more accurate reporting
1390 1390 try:
1391 1391 l = method(False)
1392 1392 except error.LockHeld:
1393 1393 l = None
1394 1394
1395 1395 if l:
1396 1396 l.release()
1397 1397 else:
1398 1398 try:
1399 1399 st = vfs.lstat(name)
1400 1400 age = now - st[stat.ST_MTIME]
1401 1401 user = util.username(st.st_uid)
1402 1402 locker = vfs.readlock(name)
1403 1403 if ":" in locker:
1404 1404 host, pid = locker.split(':')
1405 1405 if host == socket.gethostname():
1406 1406 locker = 'user %s, process %s' % (user, pid)
1407 1407 else:
1408 1408 locker = 'user %s, process %s, host %s' \
1409 1409 % (user, pid, host)
1410 1410 ui.write(("%-6s %s (%ds)\n") % (name + ":", locker, age))
1411 1411 return 1
1412 1412 except OSError as e:
1413 1413 if e.errno != errno.ENOENT:
1414 1414 raise
1415 1415
1416 1416 ui.write(("%-6s free\n") % (name + ":"))
1417 1417 return 0
1418 1418
1419 1419 held += report(repo.svfs, "lock", repo.lock)
1420 1420 held += report(repo.vfs, "wlock", repo.wlock)
1421 1421
1422 1422 return held
1423 1423
1424 1424 @command('debugmergestate', [], '')
1425 1425 def debugmergestate(ui, repo, *args):
1426 1426 """print merge state
1427 1427
1428 1428 Use --verbose to print out information about whether v1 or v2 merge state
1429 1429 was chosen."""
1430 1430 def _hashornull(h):
1431 1431 if h == nullhex:
1432 1432 return 'null'
1433 1433 else:
1434 1434 return h
1435 1435
1436 1436 def printrecords(version):
1437 1437 ui.write(('* version %d records\n') % version)
1438 1438 if version == 1:
1439 1439 records = v1records
1440 1440 else:
1441 1441 records = v2records
1442 1442
1443 1443 for rtype, record in records:
1444 1444 # pretty print some record types
1445 1445 if rtype == 'L':
1446 1446 ui.write(('local: %s\n') % record)
1447 1447 elif rtype == 'O':
1448 1448 ui.write(('other: %s\n') % record)
1449 1449 elif rtype == 'm':
1450 1450 driver, mdstate = record.split('\0', 1)
1451 1451 ui.write(('merge driver: %s (state "%s")\n')
1452 1452 % (driver, mdstate))
1453 1453 elif rtype in 'FDC':
1454 1454 r = record.split('\0')
1455 1455 f, state, hash, lfile, afile, anode, ofile = r[0:7]
1456 1456 if version == 1:
1457 1457 onode = 'not stored in v1 format'
1458 1458 flags = r[7]
1459 1459 else:
1460 1460 onode, flags = r[7:9]
1461 1461 ui.write(('file: %s (record type "%s", state "%s", hash %s)\n')
1462 1462 % (f, rtype, state, _hashornull(hash)))
1463 1463 ui.write((' local path: %s (flags "%s")\n') % (lfile, flags))
1464 1464 ui.write((' ancestor path: %s (node %s)\n')
1465 1465 % (afile, _hashornull(anode)))
1466 1466 ui.write((' other path: %s (node %s)\n')
1467 1467 % (ofile, _hashornull(onode)))
1468 1468 elif rtype == 'f':
1469 1469 filename, rawextras = record.split('\0', 1)
1470 1470 extras = rawextras.split('\0')
1471 1471 i = 0
1472 1472 extrastrings = []
1473 1473 while i < len(extras):
1474 1474 extrastrings.append('%s = %s' % (extras[i], extras[i + 1]))
1475 1475 i += 2
1476 1476
1477 1477 ui.write(('file extras: %s (%s)\n')
1478 1478 % (filename, ', '.join(extrastrings)))
1479 1479 elif rtype == 'l':
1480 1480 labels = record.split('\0', 2)
1481 1481 labels = [l for l in labels if len(l) > 0]
1482 1482 ui.write(('labels:\n'))
1483 1483 ui.write((' local: %s\n' % labels[0]))
1484 1484 ui.write((' other: %s\n' % labels[1]))
1485 1485 if len(labels) > 2:
1486 1486 ui.write((' base: %s\n' % labels[2]))
1487 1487 else:
1488 1488 ui.write(('unrecognized entry: %s\t%s\n')
1489 1489 % (rtype, record.replace('\0', '\t')))
1490 1490
1491 1491 # Avoid mergestate.read() since it may raise an exception for unsupported
1492 1492 # merge state records. We shouldn't be doing this, but this is OK since this
1493 1493 # command is pretty low-level.
1494 1494 ms = mergemod.mergestate(repo)
1495 1495
1496 1496 # sort so that reasonable information is on top
1497 1497 v1records = ms._readrecordsv1()
1498 1498 v2records = ms._readrecordsv2()
1499 1499 order = 'LOml'
1500 1500 def key(r):
1501 1501 idx = order.find(r[0])
1502 1502 if idx == -1:
1503 1503 return (1, r[1])
1504 1504 else:
1505 1505 return (0, idx)
1506 1506 v1records.sort(key=key)
1507 1507 v2records.sort(key=key)
1508 1508
1509 1509 if not v1records and not v2records:
1510 1510 ui.write(('no merge state found\n'))
1511 1511 elif not v2records:
1512 1512 ui.note(('no version 2 merge state\n'))
1513 1513 printrecords(1)
1514 1514 elif ms._v1v2match(v1records, v2records):
1515 1515 ui.note(('v1 and v2 states match: using v2\n'))
1516 1516 printrecords(2)
1517 1517 else:
1518 1518 ui.note(('v1 and v2 states mismatch: using v1\n'))
1519 1519 printrecords(1)
1520 1520 if ui.verbose:
1521 1521 printrecords(2)
1522 1522
1523 1523 @command('debugnamecomplete', [], _('NAME...'))
1524 1524 def debugnamecomplete(ui, repo, *args):
1525 1525 '''complete "names" - tags, open branch names, bookmark names'''
1526 1526
1527 1527 names = set()
1528 1528 # since we previously only listed open branches, we will handle that
1529 1529 # specially (after this for loop)
1530 1530 for name, ns in repo.names.iteritems():
1531 1531 if name != 'branches':
1532 1532 names.update(ns.listnames(repo))
1533 1533 names.update(tag for (tag, heads, tip, closed)
1534 1534 in repo.branchmap().iterbranches() if not closed)
1535 1535 completions = set()
1536 1536 if not args:
1537 1537 args = ['']
1538 1538 for a in args:
1539 1539 completions.update(n for n in names if n.startswith(a))
1540 1540 ui.write('\n'.join(sorted(completions)))
1541 1541 ui.write('\n')
1542 1542
1543 1543 @command('debugobsolete',
1544 1544 [('', 'flags', 0, _('markers flag')),
1545 1545 ('', 'record-parents', False,
1546 1546 _('record parent information for the precursor')),
1547 1547 ('r', 'rev', [], _('display markers relevant to REV')),
1548 1548 ('', 'exclusive', False, _('restrict display to markers only '
1549 1549 'relevant to REV')),
1550 1550 ('', 'index', False, _('display index of the marker')),
1551 1551 ('', 'delete', [], _('delete markers specified by indices')),
1552 1552 ] + cmdutil.commitopts2 + cmdutil.formatteropts,
1553 1553 _('[OBSOLETED [REPLACEMENT ...]]'))
1554 1554 def debugobsolete(ui, repo, precursor=None, *successors, **opts):
1555 1555 """create arbitrary obsolete marker
1556 1556
1557 1557 With no arguments, displays the list of obsolescence markers."""
1558 1558
1559 1559 opts = pycompat.byteskwargs(opts)
1560 1560
1561 1561 def parsenodeid(s):
1562 1562 try:
1563 1563 # We do not use revsingle/revrange functions here to accept
1564 1564 # arbitrary node identifiers, possibly not present in the
1565 1565 # local repository.
1566 1566 n = bin(s)
1567 1567 if len(n) != len(nullid):
1568 1568 raise TypeError()
1569 1569 return n
1570 1570 except TypeError:
1571 1571 raise error.Abort('changeset references must be full hexadecimal '
1572 1572 'node identifiers')
1573 1573
1574 1574 if opts.get('delete'):
1575 1575 indices = []
1576 1576 for v in opts.get('delete'):
1577 1577 try:
1578 1578 indices.append(int(v))
1579 1579 except ValueError:
1580 1580 raise error.Abort(_('invalid index value: %r') % v,
1581 1581 hint=_('use integers for indices'))
1582 1582
1583 1583 if repo.currenttransaction():
1584 1584 raise error.Abort(_('cannot delete obsmarkers in the middle '
1585 1585 'of transaction.'))
1586 1586
1587 1587 with repo.lock():
1588 1588 n = repair.deleteobsmarkers(repo.obsstore, indices)
1589 1589 ui.write(_('deleted %i obsolescence markers\n') % n)
1590 1590
1591 1591 return
1592 1592
1593 1593 if precursor is not None:
1594 1594 if opts['rev']:
1595 1595 raise error.Abort('cannot select revision when creating marker')
1596 1596 metadata = {}
1597 1597 metadata['user'] = opts['user'] or ui.username()
1598 1598 succs = tuple(parsenodeid(succ) for succ in successors)
1599 1599 l = repo.lock()
1600 1600 try:
1601 1601 tr = repo.transaction('debugobsolete')
1602 1602 try:
1603 1603 date = opts.get('date')
1604 1604 if date:
1605 1605 date = dateutil.parsedate(date)
1606 1606 else:
1607 1607 date = None
1608 1608 prec = parsenodeid(precursor)
1609 1609 parents = None
1610 1610 if opts['record_parents']:
1611 1611 if prec not in repo.unfiltered():
1612 1612 raise error.Abort('cannot used --record-parents on '
1613 1613 'unknown changesets')
1614 1614 parents = repo.unfiltered()[prec].parents()
1615 1615 parents = tuple(p.node() for p in parents)
1616 1616 repo.obsstore.create(tr, prec, succs, opts['flags'],
1617 1617 parents=parents, date=date,
1618 1618 metadata=metadata, ui=ui)
1619 1619 tr.close()
1620 1620 except ValueError as exc:
1621 1621 raise error.Abort(_('bad obsmarker input: %s') %
1622 1622 pycompat.bytestr(exc))
1623 1623 finally:
1624 1624 tr.release()
1625 1625 finally:
1626 1626 l.release()
1627 1627 else:
1628 1628 if opts['rev']:
1629 1629 revs = scmutil.revrange(repo, opts['rev'])
1630 1630 nodes = [repo[r].node() for r in revs]
1631 1631 markers = list(obsutil.getmarkers(repo, nodes=nodes,
1632 1632 exclusive=opts['exclusive']))
1633 1633 markers.sort(key=lambda x: x._data)
1634 1634 else:
1635 1635 markers = obsutil.getmarkers(repo)
1636 1636
1637 1637 markerstoiter = markers
1638 1638 isrelevant = lambda m: True
1639 1639 if opts.get('rev') and opts.get('index'):
1640 1640 markerstoiter = obsutil.getmarkers(repo)
1641 1641 markerset = set(markers)
1642 1642 isrelevant = lambda m: m in markerset
1643 1643
1644 1644 fm = ui.formatter('debugobsolete', opts)
1645 1645 for i, m in enumerate(markerstoiter):
1646 1646 if not isrelevant(m):
1647 1647 # marker can be irrelevant when we're iterating over a set
1648 1648 # of markers (markerstoiter) which is bigger than the set
1649 1649 # of markers we want to display (markers)
1650 1650 # this can happen if both --index and --rev options are
1651 1651 # provided and thus we need to iterate over all of the markers
1652 1652 # to get the correct indices, but only display the ones that
1653 1653 # are relevant to --rev value
1654 1654 continue
1655 1655 fm.startitem()
1656 1656 ind = i if opts.get('index') else None
1657 1657 cmdutil.showmarker(fm, m, index=ind)
1658 1658 fm.end()
1659 1659
1660 1660 @command('debugpathcomplete',
1661 1661 [('f', 'full', None, _('complete an entire path')),
1662 1662 ('n', 'normal', None, _('show only normal files')),
1663 1663 ('a', 'added', None, _('show only added files')),
1664 1664 ('r', 'removed', None, _('show only removed files'))],
1665 1665 _('FILESPEC...'))
1666 1666 def debugpathcomplete(ui, repo, *specs, **opts):
1667 1667 '''complete part or all of a tracked path
1668 1668
1669 1669 This command supports shells that offer path name completion. It
1670 1670 currently completes only files already known to the dirstate.
1671 1671
1672 1672 Completion extends only to the next path segment unless
1673 1673 --full is specified, in which case entire paths are used.'''
1674 1674
1675 1675 def complete(path, acceptable):
1676 1676 dirstate = repo.dirstate
1677 1677 spec = os.path.normpath(os.path.join(pycompat.getcwd(), path))
1678 1678 rootdir = repo.root + pycompat.ossep
1679 1679 if spec != repo.root and not spec.startswith(rootdir):
1680 1680 return [], []
1681 1681 if os.path.isdir(spec):
1682 1682 spec += '/'
1683 1683 spec = spec[len(rootdir):]
1684 1684 fixpaths = pycompat.ossep != '/'
1685 1685 if fixpaths:
1686 1686 spec = spec.replace(pycompat.ossep, '/')
1687 1687 speclen = len(spec)
1688 1688 fullpaths = opts[r'full']
1689 1689 files, dirs = set(), set()
1690 1690 adddir, addfile = dirs.add, files.add
1691 1691 for f, st in dirstate.iteritems():
1692 1692 if f.startswith(spec) and st[0] in acceptable:
1693 1693 if fixpaths:
1694 1694 f = f.replace('/', pycompat.ossep)
1695 1695 if fullpaths:
1696 1696 addfile(f)
1697 1697 continue
1698 1698 s = f.find(pycompat.ossep, speclen)
1699 1699 if s >= 0:
1700 1700 adddir(f[:s])
1701 1701 else:
1702 1702 addfile(f)
1703 1703 return files, dirs
1704 1704
1705 1705 acceptable = ''
1706 1706 if opts[r'normal']:
1707 1707 acceptable += 'nm'
1708 1708 if opts[r'added']:
1709 1709 acceptable += 'a'
1710 1710 if opts[r'removed']:
1711 1711 acceptable += 'r'
1712 1712 cwd = repo.getcwd()
1713 1713 if not specs:
1714 1714 specs = ['.']
1715 1715
1716 1716 files, dirs = set(), set()
1717 1717 for spec in specs:
1718 1718 f, d = complete(spec, acceptable or 'nmar')
1719 1719 files.update(f)
1720 1720 dirs.update(d)
1721 1721 files.update(dirs)
1722 1722 ui.write('\n'.join(repo.pathto(p, cwd) for p in sorted(files)))
1723 1723 ui.write('\n')
1724 1724
1725 1725 @command('debugpeer', [], _('PATH'), norepo=True)
1726 1726 def debugpeer(ui, path):
1727 1727 """establish a connection to a peer repository"""
1728 1728 # Always enable peer request logging. Requires --debug to display
1729 1729 # though.
1730 1730 overrides = {
1731 1731 ('devel', 'debug.peer-request'): True,
1732 1732 }
1733 1733
1734 1734 with ui.configoverride(overrides):
1735 1735 peer = hg.peer(ui, {}, path)
1736 1736
1737 1737 local = peer.local() is not None
1738 1738 canpush = peer.canpush()
1739 1739
1740 1740 ui.write(_('url: %s\n') % peer.url())
1741 1741 ui.write(_('local: %s\n') % (_('yes') if local else _('no')))
1742 1742 ui.write(_('pushable: %s\n') % (_('yes') if canpush else _('no')))
1743 1743
1744 1744 @command('debugpickmergetool',
1745 1745 [('r', 'rev', '', _('check for files in this revision'), _('REV')),
1746 1746 ('', 'changedelete', None, _('emulate merging change and delete')),
1747 1747 ] + cmdutil.walkopts + cmdutil.mergetoolopts,
1748 1748 _('[PATTERN]...'),
1749 1749 inferrepo=True)
1750 1750 def debugpickmergetool(ui, repo, *pats, **opts):
1751 1751 """examine which merge tool is chosen for specified file
1752 1752
1753 1753 As described in :hg:`help merge-tools`, Mercurial examines
1754 1754 configurations below in this order to decide which merge tool is
1755 1755 chosen for specified file.
1756 1756
1757 1757 1. ``--tool`` option
1758 1758 2. ``HGMERGE`` environment variable
1759 1759 3. configurations in ``merge-patterns`` section
1760 1760 4. configuration of ``ui.merge``
1761 1761 5. configurations in ``merge-tools`` section
1762 1762 6. ``hgmerge`` tool (for historical reason only)
1763 1763 7. default tool for fallback (``:merge`` or ``:prompt``)
1764 1764
1765 1765 This command writes out examination result in the style below::
1766 1766
1767 1767 FILE = MERGETOOL
1768 1768
1769 1769 By default, all files known in the first parent context of the
1770 1770 working directory are examined. Use file patterns and/or -I/-X
1771 1771 options to limit target files. -r/--rev is also useful to examine
1772 1772 files in another context without actual updating to it.
1773 1773
1774 1774 With --debug, this command shows warning messages while matching
1775 1775 against ``merge-patterns`` and so on, too. It is recommended to
1776 1776 use this option with explicit file patterns and/or -I/-X options,
1777 1777 because this option increases amount of output per file according
1778 1778 to configurations in hgrc.
1779 1779
1780 1780 With -v/--verbose, this command shows configurations below at
1781 1781 first (only if specified).
1782 1782
1783 1783 - ``--tool`` option
1784 1784 - ``HGMERGE`` environment variable
1785 1785 - configuration of ``ui.merge``
1786 1786
1787 1787 If merge tool is chosen before matching against
1788 1788 ``merge-patterns``, this command can't show any helpful
1789 1789 information, even with --debug. In such case, information above is
1790 1790 useful to know why a merge tool is chosen.
1791 1791 """
1792 1792 opts = pycompat.byteskwargs(opts)
1793 1793 overrides = {}
1794 1794 if opts['tool']:
1795 1795 overrides[('ui', 'forcemerge')] = opts['tool']
1796 1796 ui.note(('with --tool %r\n') % (pycompat.bytestr(opts['tool'])))
1797 1797
1798 1798 with ui.configoverride(overrides, 'debugmergepatterns'):
1799 1799 hgmerge = encoding.environ.get("HGMERGE")
1800 1800 if hgmerge is not None:
1801 1801 ui.note(('with HGMERGE=%r\n') % (pycompat.bytestr(hgmerge)))
1802 1802 uimerge = ui.config("ui", "merge")
1803 1803 if uimerge:
1804 1804 ui.note(('with ui.merge=%r\n') % (pycompat.bytestr(uimerge)))
1805 1805
1806 1806 ctx = scmutil.revsingle(repo, opts.get('rev'))
1807 1807 m = scmutil.match(ctx, pats, opts)
1808 1808 changedelete = opts['changedelete']
1809 1809 for path in ctx.walk(m):
1810 1810 fctx = ctx[path]
1811 1811 try:
1812 1812 if not ui.debugflag:
1813 1813 ui.pushbuffer(error=True)
1814 1814 tool, toolpath = filemerge._picktool(repo, ui, path,
1815 1815 fctx.isbinary(),
1816 1816 'l' in fctx.flags(),
1817 1817 changedelete)
1818 1818 finally:
1819 1819 if not ui.debugflag:
1820 1820 ui.popbuffer()
1821 1821 ui.write(('%s = %s\n') % (path, tool))
1822 1822
1823 1823 @command('debugpushkey', [], _('REPO NAMESPACE [KEY OLD NEW]'), norepo=True)
1824 1824 def debugpushkey(ui, repopath, namespace, *keyinfo, **opts):
1825 1825 '''access the pushkey key/value protocol
1826 1826
1827 1827 With two args, list the keys in the given namespace.
1828 1828
1829 1829 With five args, set a key to new if it currently is set to old.
1830 1830 Reports success or failure.
1831 1831 '''
1832 1832
1833 1833 target = hg.peer(ui, {}, repopath)
1834 1834 if keyinfo:
1835 1835 key, old, new = keyinfo
1836 1836 r = target.pushkey(namespace, key, old, new)
1837 1837 ui.status(pycompat.bytestr(r) + '\n')
1838 1838 return not r
1839 1839 else:
1840 1840 for k, v in sorted(target.listkeys(namespace).iteritems()):
1841 1841 ui.write("%s\t%s\n" % (stringutil.escapestr(k),
1842 1842 stringutil.escapestr(v)))
1843 1843
1844 1844 @command('debugpvec', [], _('A B'))
1845 1845 def debugpvec(ui, repo, a, b=None):
1846 1846 ca = scmutil.revsingle(repo, a)
1847 1847 cb = scmutil.revsingle(repo, b)
1848 1848 pa = pvec.ctxpvec(ca)
1849 1849 pb = pvec.ctxpvec(cb)
1850 1850 if pa == pb:
1851 1851 rel = "="
1852 1852 elif pa > pb:
1853 1853 rel = ">"
1854 1854 elif pa < pb:
1855 1855 rel = "<"
1856 1856 elif pa | pb:
1857 1857 rel = "|"
1858 1858 ui.write(_("a: %s\n") % pa)
1859 1859 ui.write(_("b: %s\n") % pb)
1860 1860 ui.write(_("depth(a): %d depth(b): %d\n") % (pa._depth, pb._depth))
1861 1861 ui.write(_("delta: %d hdist: %d distance: %d relation: %s\n") %
1862 1862 (abs(pa._depth - pb._depth), pvec._hamming(pa._vec, pb._vec),
1863 1863 pa.distance(pb), rel))
1864 1864
1865 1865 @command('debugrebuilddirstate|debugrebuildstate',
1866 1866 [('r', 'rev', '', _('revision to rebuild to'), _('REV')),
1867 1867 ('', 'minimal', None, _('only rebuild files that are inconsistent with '
1868 1868 'the working copy parent')),
1869 1869 ],
1870 1870 _('[-r REV]'))
1871 1871 def debugrebuilddirstate(ui, repo, rev, **opts):
1872 1872 """rebuild the dirstate as it would look like for the given revision
1873 1873
1874 1874 If no revision is specified the first current parent will be used.
1875 1875
1876 1876 The dirstate will be set to the files of the given revision.
1877 1877 The actual working directory content or existing dirstate
1878 1878 information such as adds or removes is not considered.
1879 1879
1880 1880 ``minimal`` will only rebuild the dirstate status for files that claim to be
1881 1881 tracked but are not in the parent manifest, or that exist in the parent
1882 1882 manifest but are not in the dirstate. It will not change adds, removes, or
1883 1883 modified files that are in the working copy parent.
1884 1884
1885 1885 One use of this command is to make the next :hg:`status` invocation
1886 1886 check the actual file content.
1887 1887 """
1888 1888 ctx = scmutil.revsingle(repo, rev)
1889 1889 with repo.wlock():
1890 1890 dirstate = repo.dirstate
1891 1891 changedfiles = None
1892 1892 # See command doc for what minimal does.
1893 1893 if opts.get(r'minimal'):
1894 1894 manifestfiles = set(ctx.manifest().keys())
1895 1895 dirstatefiles = set(dirstate)
1896 1896 manifestonly = manifestfiles - dirstatefiles
1897 1897 dsonly = dirstatefiles - manifestfiles
1898 1898 dsnotadded = set(f for f in dsonly if dirstate[f] != 'a')
1899 1899 changedfiles = manifestonly | dsnotadded
1900 1900
1901 1901 dirstate.rebuild(ctx.node(), ctx.manifest(), changedfiles)
1902 1902
1903 1903 @command('debugrebuildfncache', [], '')
1904 1904 def debugrebuildfncache(ui, repo):
1905 1905 """rebuild the fncache file"""
1906 1906 repair.rebuildfncache(ui, repo)
1907 1907
1908 1908 @command('debugrename',
1909 1909 [('r', 'rev', '', _('revision to debug'), _('REV'))],
1910 1910 _('[-r REV] FILE'))
1911 1911 def debugrename(ui, repo, file1, *pats, **opts):
1912 1912 """dump rename information"""
1913 1913
1914 1914 opts = pycompat.byteskwargs(opts)
1915 1915 ctx = scmutil.revsingle(repo, opts.get('rev'))
1916 1916 m = scmutil.match(ctx, (file1,) + pats, opts)
1917 1917 for abs in ctx.walk(m):
1918 1918 fctx = ctx[abs]
1919 1919 o = fctx.filelog().renamed(fctx.filenode())
1920 1920 rel = m.rel(abs)
1921 1921 if o:
1922 1922 ui.write(_("%s renamed from %s:%s\n") % (rel, o[0], hex(o[1])))
1923 1923 else:
1924 1924 ui.write(_("%s not renamed\n") % rel)
1925 1925
1926 1926 @command('debugrevlog', cmdutil.debugrevlogopts +
1927 1927 [('d', 'dump', False, _('dump index data'))],
1928 1928 _('-c|-m|FILE'),
1929 1929 optionalrepo=True)
1930 1930 def debugrevlog(ui, repo, file_=None, **opts):
1931 1931 """show data and statistics about a revlog"""
1932 1932 opts = pycompat.byteskwargs(opts)
1933 1933 r = cmdutil.openrevlog(repo, 'debugrevlog', file_, opts)
1934 1934
1935 1935 if opts.get("dump"):
1936 1936 numrevs = len(r)
1937 1937 ui.write(("# rev p1rev p2rev start end deltastart base p1 p2"
1938 1938 " rawsize totalsize compression heads chainlen\n"))
1939 1939 ts = 0
1940 1940 heads = set()
1941 1941
1942 1942 for rev in xrange(numrevs):
1943 1943 dbase = r.deltaparent(rev)
1944 1944 if dbase == -1:
1945 1945 dbase = rev
1946 1946 cbase = r.chainbase(rev)
1947 1947 clen = r.chainlen(rev)
1948 1948 p1, p2 = r.parentrevs(rev)
1949 1949 rs = r.rawsize(rev)
1950 1950 ts = ts + rs
1951 1951 heads -= set(r.parentrevs(rev))
1952 1952 heads.add(rev)
1953 1953 try:
1954 1954 compression = ts / r.end(rev)
1955 1955 except ZeroDivisionError:
1956 1956 compression = 0
1957 1957 ui.write("%5d %5d %5d %5d %5d %10d %4d %4d %4d %7d %9d "
1958 1958 "%11d %5d %8d\n" %
1959 1959 (rev, p1, p2, r.start(rev), r.end(rev),
1960 1960 r.start(dbase), r.start(cbase),
1961 1961 r.start(p1), r.start(p2),
1962 1962 rs, ts, compression, len(heads), clen))
1963 1963 return 0
1964 1964
1965 1965 v = r.version
1966 1966 format = v & 0xFFFF
1967 1967 flags = []
1968 1968 gdelta = False
1969 1969 if v & revlog.FLAG_INLINE_DATA:
1970 1970 flags.append('inline')
1971 1971 if v & revlog.FLAG_GENERALDELTA:
1972 1972 gdelta = True
1973 1973 flags.append('generaldelta')
1974 1974 if not flags:
1975 1975 flags = ['(none)']
1976 1976
1977 1977 nummerges = 0
1978 1978 numfull = 0
1979 1979 numprev = 0
1980 1980 nump1 = 0
1981 1981 nump2 = 0
1982 1982 numother = 0
1983 1983 nump1prev = 0
1984 1984 nump2prev = 0
1985 1985 chainlengths = []
1986 1986 chainbases = []
1987 1987 chainspans = []
1988 1988
1989 1989 datasize = [None, 0, 0]
1990 1990 fullsize = [None, 0, 0]
1991 1991 deltasize = [None, 0, 0]
1992 1992 chunktypecounts = {}
1993 1993 chunktypesizes = {}
1994 1994
1995 1995 def addsize(size, l):
1996 1996 if l[0] is None or size < l[0]:
1997 1997 l[0] = size
1998 1998 if size > l[1]:
1999 1999 l[1] = size
2000 2000 l[2] += size
2001 2001
2002 2002 numrevs = len(r)
2003 2003 for rev in xrange(numrevs):
2004 2004 p1, p2 = r.parentrevs(rev)
2005 2005 delta = r.deltaparent(rev)
2006 2006 if format > 0:
2007 2007 addsize(r.rawsize(rev), datasize)
2008 2008 if p2 != nullrev:
2009 2009 nummerges += 1
2010 2010 size = r.length(rev)
2011 2011 if delta == nullrev:
2012 2012 chainlengths.append(0)
2013 2013 chainbases.append(r.start(rev))
2014 2014 chainspans.append(size)
2015 2015 numfull += 1
2016 2016 addsize(size, fullsize)
2017 2017 else:
2018 2018 chainlengths.append(chainlengths[delta] + 1)
2019 2019 baseaddr = chainbases[delta]
2020 2020 revaddr = r.start(rev)
2021 2021 chainbases.append(baseaddr)
2022 2022 chainspans.append((revaddr - baseaddr) + size)
2023 2023 addsize(size, deltasize)
2024 2024 if delta == rev - 1:
2025 2025 numprev += 1
2026 2026 if delta == p1:
2027 2027 nump1prev += 1
2028 2028 elif delta == p2:
2029 2029 nump2prev += 1
2030 2030 elif delta == p1:
2031 2031 nump1 += 1
2032 2032 elif delta == p2:
2033 2033 nump2 += 1
2034 2034 elif delta != nullrev:
2035 2035 numother += 1
2036 2036
2037 2037 # Obtain data on the raw chunks in the revlog.
2038 2038 segment = r._getsegmentforrevs(rev, rev)[1]
2039 2039 if segment:
2040 2040 chunktype = bytes(segment[0:1])
2041 2041 else:
2042 2042 chunktype = 'empty'
2043 2043
2044 2044 if chunktype not in chunktypecounts:
2045 2045 chunktypecounts[chunktype] = 0
2046 2046 chunktypesizes[chunktype] = 0
2047 2047
2048 2048 chunktypecounts[chunktype] += 1
2049 2049 chunktypesizes[chunktype] += size
2050 2050
2051 2051 # Adjust size min value for empty cases
2052 2052 for size in (datasize, fullsize, deltasize):
2053 2053 if size[0] is None:
2054 2054 size[0] = 0
2055 2055
2056 2056 numdeltas = numrevs - numfull
2057 2057 numoprev = numprev - nump1prev - nump2prev
2058 2058 totalrawsize = datasize[2]
2059 2059 datasize[2] /= numrevs
2060 2060 fulltotal = fullsize[2]
2061 2061 fullsize[2] /= numfull
2062 2062 deltatotal = deltasize[2]
2063 2063 if numrevs - numfull > 0:
2064 2064 deltasize[2] /= numrevs - numfull
2065 2065 totalsize = fulltotal + deltatotal
2066 2066 avgchainlen = sum(chainlengths) / numrevs
2067 2067 maxchainlen = max(chainlengths)
2068 2068 maxchainspan = max(chainspans)
2069 2069 compratio = 1
2070 2070 if totalsize:
2071 2071 compratio = totalrawsize / totalsize
2072 2072
2073 2073 basedfmtstr = '%%%dd\n'
2074 2074 basepcfmtstr = '%%%dd %s(%%5.2f%%%%)\n'
2075 2075
2076 2076 def dfmtstr(max):
2077 2077 return basedfmtstr % len(str(max))
2078 2078 def pcfmtstr(max, padding=0):
2079 2079 return basepcfmtstr % (len(str(max)), ' ' * padding)
2080 2080
2081 2081 def pcfmt(value, total):
2082 2082 if total:
2083 2083 return (value, 100 * float(value) / total)
2084 2084 else:
2085 2085 return value, 100.0
2086 2086
2087 2087 ui.write(('format : %d\n') % format)
2088 2088 ui.write(('flags : %s\n') % ', '.join(flags))
2089 2089
2090 2090 ui.write('\n')
2091 2091 fmt = pcfmtstr(totalsize)
2092 2092 fmt2 = dfmtstr(totalsize)
2093 2093 ui.write(('revisions : ') + fmt2 % numrevs)
2094 2094 ui.write((' merges : ') + fmt % pcfmt(nummerges, numrevs))
2095 2095 ui.write((' normal : ') + fmt % pcfmt(numrevs - nummerges, numrevs))
2096 2096 ui.write(('revisions : ') + fmt2 % numrevs)
2097 2097 ui.write((' full : ') + fmt % pcfmt(numfull, numrevs))
2098 2098 ui.write((' deltas : ') + fmt % pcfmt(numdeltas, numrevs))
2099 2099 ui.write(('revision size : ') + fmt2 % totalsize)
2100 2100 ui.write((' full : ') + fmt % pcfmt(fulltotal, totalsize))
2101 2101 ui.write((' deltas : ') + fmt % pcfmt(deltatotal, totalsize))
2102 2102
2103 2103 def fmtchunktype(chunktype):
2104 2104 if chunktype == 'empty':
2105 2105 return ' %s : ' % chunktype
2106 2106 elif chunktype in pycompat.bytestr(string.ascii_letters):
2107 2107 return ' 0x%s (%s) : ' % (hex(chunktype), chunktype)
2108 2108 else:
2109 2109 return ' 0x%s : ' % hex(chunktype)
2110 2110
2111 2111 ui.write('\n')
2112 2112 ui.write(('chunks : ') + fmt2 % numrevs)
2113 2113 for chunktype in sorted(chunktypecounts):
2114 2114 ui.write(fmtchunktype(chunktype))
2115 2115 ui.write(fmt % pcfmt(chunktypecounts[chunktype], numrevs))
2116 2116 ui.write(('chunks size : ') + fmt2 % totalsize)
2117 2117 for chunktype in sorted(chunktypecounts):
2118 2118 ui.write(fmtchunktype(chunktype))
2119 2119 ui.write(fmt % pcfmt(chunktypesizes[chunktype], totalsize))
2120 2120
2121 2121 ui.write('\n')
2122 2122 fmt = dfmtstr(max(avgchainlen, maxchainlen, maxchainspan, compratio))
2123 2123 ui.write(('avg chain length : ') + fmt % avgchainlen)
2124 2124 ui.write(('max chain length : ') + fmt % maxchainlen)
2125 2125 ui.write(('max chain reach : ') + fmt % maxchainspan)
2126 2126 ui.write(('compression ratio : ') + fmt % compratio)
2127 2127
2128 2128 if format > 0:
2129 2129 ui.write('\n')
2130 2130 ui.write(('uncompressed data size (min/max/avg) : %d / %d / %d\n')
2131 2131 % tuple(datasize))
2132 2132 ui.write(('full revision size (min/max/avg) : %d / %d / %d\n')
2133 2133 % tuple(fullsize))
2134 2134 ui.write(('delta size (min/max/avg) : %d / %d / %d\n')
2135 2135 % tuple(deltasize))
2136 2136
2137 2137 if numdeltas > 0:
2138 2138 ui.write('\n')
2139 2139 fmt = pcfmtstr(numdeltas)
2140 2140 fmt2 = pcfmtstr(numdeltas, 4)
2141 2141 ui.write(('deltas against prev : ') + fmt % pcfmt(numprev, numdeltas))
2142 2142 if numprev > 0:
2143 2143 ui.write((' where prev = p1 : ') + fmt2 % pcfmt(nump1prev,
2144 2144 numprev))
2145 2145 ui.write((' where prev = p2 : ') + fmt2 % pcfmt(nump2prev,
2146 2146 numprev))
2147 2147 ui.write((' other : ') + fmt2 % pcfmt(numoprev,
2148 2148 numprev))
2149 2149 if gdelta:
2150 2150 ui.write(('deltas against p1 : ')
2151 2151 + fmt % pcfmt(nump1, numdeltas))
2152 2152 ui.write(('deltas against p2 : ')
2153 2153 + fmt % pcfmt(nump2, numdeltas))
2154 2154 ui.write(('deltas against other : ') + fmt % pcfmt(numother,
2155 2155 numdeltas))
2156 2156
2157 2157 @command('debugrevspec',
2158 2158 [('', 'optimize', None,
2159 2159 _('print parsed tree after optimizing (DEPRECATED)')),
2160 2160 ('', 'show-revs', True, _('print list of result revisions (default)')),
2161 2161 ('s', 'show-set', None, _('print internal representation of result set')),
2162 2162 ('p', 'show-stage', [],
2163 2163 _('print parsed tree at the given stage'), _('NAME')),
2164 2164 ('', 'no-optimized', False, _('evaluate tree without optimization')),
2165 2165 ('', 'verify-optimized', False, _('verify optimized result')),
2166 2166 ],
2167 2167 ('REVSPEC'))
2168 2168 def debugrevspec(ui, repo, expr, **opts):
2169 2169 """parse and apply a revision specification
2170 2170
2171 2171 Use -p/--show-stage option to print the parsed tree at the given stages.
2172 2172 Use -p all to print tree at every stage.
2173 2173
2174 2174 Use --no-show-revs option with -s or -p to print only the set
2175 2175 representation or the parsed tree respectively.
2176 2176
2177 2177 Use --verify-optimized to compare the optimized result with the unoptimized
2178 2178 one. Returns 1 if the optimized result differs.
2179 2179 """
2180 2180 opts = pycompat.byteskwargs(opts)
2181 2181 aliases = ui.configitems('revsetalias')
2182 2182 stages = [
2183 2183 ('parsed', lambda tree: tree),
2184 2184 ('expanded', lambda tree: revsetlang.expandaliases(tree, aliases,
2185 2185 ui.warn)),
2186 2186 ('concatenated', revsetlang.foldconcat),
2187 2187 ('analyzed', revsetlang.analyze),
2188 2188 ('optimized', revsetlang.optimize),
2189 2189 ]
2190 2190 if opts['no_optimized']:
2191 2191 stages = stages[:-1]
2192 2192 if opts['verify_optimized'] and opts['no_optimized']:
2193 2193 raise error.Abort(_('cannot use --verify-optimized with '
2194 2194 '--no-optimized'))
2195 2195 stagenames = set(n for n, f in stages)
2196 2196
2197 2197 showalways = set()
2198 2198 showchanged = set()
2199 2199 if ui.verbose and not opts['show_stage']:
2200 2200 # show parsed tree by --verbose (deprecated)
2201 2201 showalways.add('parsed')
2202 2202 showchanged.update(['expanded', 'concatenated'])
2203 2203 if opts['optimize']:
2204 2204 showalways.add('optimized')
2205 2205 if opts['show_stage'] and opts['optimize']:
2206 2206 raise error.Abort(_('cannot use --optimize with --show-stage'))
2207 2207 if opts['show_stage'] == ['all']:
2208 2208 showalways.update(stagenames)
2209 2209 else:
2210 2210 for n in opts['show_stage']:
2211 2211 if n not in stagenames:
2212 2212 raise error.Abort(_('invalid stage name: %s') % n)
2213 2213 showalways.update(opts['show_stage'])
2214 2214
2215 2215 treebystage = {}
2216 2216 printedtree = None
2217 2217 tree = revsetlang.parse(expr, lookup=revset.lookupfn(repo))
2218 2218 for n, f in stages:
2219 2219 treebystage[n] = tree = f(tree)
2220 2220 if n in showalways or (n in showchanged and tree != printedtree):
2221 2221 if opts['show_stage'] or n != 'parsed':
2222 2222 ui.write(("* %s:\n") % n)
2223 2223 ui.write(revsetlang.prettyformat(tree), "\n")
2224 2224 printedtree = tree
2225 2225
2226 2226 if opts['verify_optimized']:
2227 2227 arevs = revset.makematcher(treebystage['analyzed'])(repo)
2228 2228 brevs = revset.makematcher(treebystage['optimized'])(repo)
2229 2229 if opts['show_set'] or (opts['show_set'] is None and ui.verbose):
2230 2230 ui.write(("* analyzed set:\n"), smartset.prettyformat(arevs), "\n")
2231 2231 ui.write(("* optimized set:\n"), smartset.prettyformat(brevs), "\n")
2232 2232 arevs = list(arevs)
2233 2233 brevs = list(brevs)
2234 2234 if arevs == brevs:
2235 2235 return 0
2236 2236 ui.write(('--- analyzed\n'), label='diff.file_a')
2237 2237 ui.write(('+++ optimized\n'), label='diff.file_b')
2238 2238 sm = difflib.SequenceMatcher(None, arevs, brevs)
2239 2239 for tag, alo, ahi, blo, bhi in sm.get_opcodes():
2240 2240 if tag in ('delete', 'replace'):
2241 2241 for c in arevs[alo:ahi]:
2242 2242 ui.write('-%s\n' % c, label='diff.deleted')
2243 2243 if tag in ('insert', 'replace'):
2244 2244 for c in brevs[blo:bhi]:
2245 2245 ui.write('+%s\n' % c, label='diff.inserted')
2246 2246 if tag == 'equal':
2247 2247 for c in arevs[alo:ahi]:
2248 2248 ui.write(' %s\n' % c)
2249 2249 return 1
2250 2250
2251 2251 func = revset.makematcher(tree)
2252 2252 revs = func(repo)
2253 2253 if opts['show_set'] or (opts['show_set'] is None and ui.verbose):
2254 2254 ui.write(("* set:\n"), smartset.prettyformat(revs), "\n")
2255 2255 if not opts['show_revs']:
2256 2256 return
2257 2257 for c in revs:
2258 2258 ui.write("%d\n" % c)
2259 2259
2260 2260 @command('debugserve', [
2261 2261 ('', 'sshstdio', False, _('run an SSH server bound to process handles')),
2262 2262 ('', 'logiofd', '', _('file descriptor to log server I/O to')),
2263 2263 ('', 'logiofile', '', _('file to log server I/O to')),
2264 2264 ], '')
2265 2265 def debugserve(ui, repo, **opts):
2266 2266 """run a server with advanced settings
2267 2267
2268 2268 This command is similar to :hg:`serve`. It exists partially as a
2269 2269 workaround to the fact that ``hg serve --stdio`` must have specific
2270 2270 arguments for security reasons.
2271 2271 """
2272 2272 opts = pycompat.byteskwargs(opts)
2273 2273
2274 2274 if not opts['sshstdio']:
2275 2275 raise error.Abort(_('only --sshstdio is currently supported'))
2276 2276
2277 2277 logfh = None
2278 2278
2279 2279 if opts['logiofd'] and opts['logiofile']:
2280 2280 raise error.Abort(_('cannot use both --logiofd and --logiofile'))
2281 2281
2282 2282 if opts['logiofd']:
2283 2283 # Line buffered because output is line based.
2284 2284 logfh = os.fdopen(int(opts['logiofd']), r'ab', 1)
2285 2285 elif opts['logiofile']:
2286 2286 logfh = open(opts['logiofile'], 'ab', 1)
2287 2287
2288 2288 s = wireprotoserver.sshserver(ui, repo, logfh=logfh)
2289 2289 s.serve_forever()
2290 2290
2291 2291 @command('debugsetparents', [], _('REV1 [REV2]'))
2292 2292 def debugsetparents(ui, repo, rev1, rev2=None):
2293 2293 """manually set the parents of the current working directory
2294 2294
2295 2295 This is useful for writing repository conversion tools, but should
2296 2296 be used with care. For example, neither the working directory nor the
2297 2297 dirstate is updated, so file status may be incorrect after running this
2298 2298 command.
2299 2299
2300 2300 Returns 0 on success.
2301 2301 """
2302 2302
2303 2303 node1 = scmutil.revsingle(repo, rev1).node()
2304 2304 node2 = scmutil.revsingle(repo, rev2, 'null').node()
2305 2305
2306 2306 with repo.wlock():
2307 2307 repo.setparents(node1, node2)
2308 2308
2309 2309 @command('debugssl', [], '[SOURCE]', optionalrepo=True)
2310 2310 def debugssl(ui, repo, source=None, **opts):
2311 2311 '''test a secure connection to a server
2312 2312
2313 2313 This builds the certificate chain for the server on Windows, installing the
2314 2314 missing intermediates and trusted root via Windows Update if necessary. It
2315 2315 does nothing on other platforms.
2316 2316
2317 2317 If SOURCE is omitted, the 'default' path will be used. If a URL is given,
2318 2318 that server is used. See :hg:`help urls` for more information.
2319 2319
2320 2320 If the update succeeds, retry the original operation. Otherwise, the cause
2321 2321 of the SSL error is likely another issue.
2322 2322 '''
2323 2323 if not pycompat.iswindows:
2324 2324 raise error.Abort(_('certificate chain building is only possible on '
2325 2325 'Windows'))
2326 2326
2327 2327 if not source:
2328 2328 if not repo:
2329 2329 raise error.Abort(_("there is no Mercurial repository here, and no "
2330 2330 "server specified"))
2331 2331 source = "default"
2332 2332
2333 2333 source, branches = hg.parseurl(ui.expandpath(source))
2334 2334 url = util.url(source)
2335 2335 addr = None
2336 2336
2337 2337 defaultport = {'https': 443, 'ssh': 22}
2338 2338 if url.scheme in defaultport:
2339 2339 try:
2340 2340 addr = (url.host, int(url.port or defaultport[url.scheme]))
2341 2341 except ValueError:
2342 2342 raise error.Abort(_("malformed port number in URL"))
2343 2343 else:
2344 2344 raise error.Abort(_("only https and ssh connections are supported"))
2345 2345
2346 2346 from . import win32
2347 2347
2348 2348 s = ssl.wrap_socket(socket.socket(), ssl_version=ssl.PROTOCOL_TLS,
2349 2349 cert_reqs=ssl.CERT_NONE, ca_certs=None)
2350 2350
2351 2351 try:
2352 2352 s.connect(addr)
2353 2353 cert = s.getpeercert(True)
2354 2354
2355 2355 ui.status(_('checking the certificate chain for %s\n') % url.host)
2356 2356
2357 2357 complete = win32.checkcertificatechain(cert, build=False)
2358 2358
2359 2359 if not complete:
2360 2360 ui.status(_('certificate chain is incomplete, updating... '))
2361 2361
2362 2362 if not win32.checkcertificatechain(cert):
2363 2363 ui.status(_('failed.\n'))
2364 2364 else:
2365 2365 ui.status(_('done.\n'))
2366 2366 else:
2367 2367 ui.status(_('full certificate chain is available\n'))
2368 2368 finally:
2369 2369 s.close()
2370 2370
2371 2371 @command('debugsub',
2372 2372 [('r', 'rev', '',
2373 2373 _('revision to check'), _('REV'))],
2374 2374 _('[-r REV] [REV]'))
2375 2375 def debugsub(ui, repo, rev=None):
2376 2376 ctx = scmutil.revsingle(repo, rev, None)
2377 2377 for k, v in sorted(ctx.substate.items()):
2378 2378 ui.write(('path %s\n') % k)
2379 2379 ui.write((' source %s\n') % v[0])
2380 2380 ui.write((' revision %s\n') % v[1])
2381 2381
2382 2382 @command('debugsuccessorssets',
2383 2383 [('', 'closest', False, _('return closest successors sets only'))],
2384 2384 _('[REV]'))
2385 2385 def debugsuccessorssets(ui, repo, *revs, **opts):
2386 2386 """show set of successors for revision
2387 2387
2388 2388 A successors set of changeset A is a consistent group of revisions that
2389 2389 succeed A. It contains non-obsolete changesets only unless closests
2390 2390 successors set is set.
2391 2391
2392 2392 In most cases a changeset A has a single successors set containing a single
2393 2393 successor (changeset A replaced by A').
2394 2394
2395 2395 A changeset that is made obsolete with no successors are called "pruned".
2396 2396 Such changesets have no successors sets at all.
2397 2397
2398 2398 A changeset that has been "split" will have a successors set containing
2399 2399 more than one successor.
2400 2400
2401 2401 A changeset that has been rewritten in multiple different ways is called
2402 2402 "divergent". Such changesets have multiple successor sets (each of which
2403 2403 may also be split, i.e. have multiple successors).
2404 2404
2405 2405 Results are displayed as follows::
2406 2406
2407 2407 <rev1>
2408 2408 <successors-1A>
2409 2409 <rev2>
2410 2410 <successors-2A>
2411 2411 <successors-2B1> <successors-2B2> <successors-2B3>
2412 2412
2413 2413 Here rev2 has two possible (i.e. divergent) successors sets. The first
2414 2414 holds one element, whereas the second holds three (i.e. the changeset has
2415 2415 been split).
2416 2416 """
2417 2417 # passed to successorssets caching computation from one call to another
2418 2418 cache = {}
2419 2419 ctx2str = bytes
2420 2420 node2str = short
2421 2421 for rev in scmutil.revrange(repo, revs):
2422 2422 ctx = repo[rev]
2423 2423 ui.write('%s\n'% ctx2str(ctx))
2424 2424 for succsset in obsutil.successorssets(repo, ctx.node(),
2425 2425 closest=opts[r'closest'],
2426 2426 cache=cache):
2427 2427 if succsset:
2428 2428 ui.write(' ')
2429 2429 ui.write(node2str(succsset[0]))
2430 2430 for node in succsset[1:]:
2431 2431 ui.write(' ')
2432 2432 ui.write(node2str(node))
2433 2433 ui.write('\n')
2434 2434
2435 2435 @command('debugtemplate',
2436 2436 [('r', 'rev', [], _('apply template on changesets'), _('REV')),
2437 2437 ('D', 'define', [], _('define template keyword'), _('KEY=VALUE'))],
2438 2438 _('[-r REV]... [-D KEY=VALUE]... TEMPLATE'),
2439 2439 optionalrepo=True)
2440 2440 def debugtemplate(ui, repo, tmpl, **opts):
2441 2441 """parse and apply a template
2442 2442
2443 2443 If -r/--rev is given, the template is processed as a log template and
2444 2444 applied to the given changesets. Otherwise, it is processed as a generic
2445 2445 template.
2446 2446
2447 2447 Use --verbose to print the parsed tree.
2448 2448 """
2449 2449 revs = None
2450 2450 if opts[r'rev']:
2451 2451 if repo is None:
2452 2452 raise error.RepoError(_('there is no Mercurial repository here '
2453 2453 '(.hg not found)'))
2454 2454 revs = scmutil.revrange(repo, opts[r'rev'])
2455 2455
2456 2456 props = {}
2457 2457 for d in opts[r'define']:
2458 2458 try:
2459 2459 k, v = (e.strip() for e in d.split('=', 1))
2460 2460 if not k or k == 'ui':
2461 2461 raise ValueError
2462 2462 props[k] = v
2463 2463 except ValueError:
2464 2464 raise error.Abort(_('malformed keyword definition: %s') % d)
2465 2465
2466 2466 if ui.verbose:
2467 2467 aliases = ui.configitems('templatealias')
2468 2468 tree = templater.parse(tmpl)
2469 2469 ui.note(templater.prettyformat(tree), '\n')
2470 2470 newtree = templater.expandaliases(tree, aliases)
2471 2471 if newtree != tree:
2472 2472 ui.note(("* expanded:\n"), templater.prettyformat(newtree), '\n')
2473 2473
2474 2474 if revs is None:
2475 2475 tres = formatter.templateresources(ui, repo)
2476 2476 t = formatter.maketemplater(ui, tmpl, resources=tres)
2477 2477 ui.write(t.renderdefault(props))
2478 2478 else:
2479 2479 displayer = logcmdutil.maketemplater(ui, repo, tmpl)
2480 2480 for r in revs:
2481 2481 displayer.show(repo[r], **pycompat.strkwargs(props))
2482 2482 displayer.close()
2483 2483
2484 2484 @command('debuguigetpass', [
2485 2485 ('p', 'prompt', '', _('prompt text'), _('TEXT')),
2486 2486 ], _('[-p TEXT]'), norepo=True)
2487 2487 def debuguigetpass(ui, prompt=''):
2488 2488 """show prompt to type password"""
2489 2489 r = ui.getpass(prompt)
2490 2490 ui.write(('respose: %s\n') % r)
2491 2491
2492 2492 @command('debuguiprompt', [
2493 2493 ('p', 'prompt', '', _('prompt text'), _('TEXT')),
2494 2494 ], _('[-p TEXT]'), norepo=True)
2495 2495 def debuguiprompt(ui, prompt=''):
2496 2496 """show plain prompt"""
2497 2497 r = ui.prompt(prompt)
2498 2498 ui.write(('response: %s\n') % r)
2499 2499
2500 2500 @command('debugupdatecaches', [])
2501 2501 def debugupdatecaches(ui, repo, *pats, **opts):
2502 2502 """warm all known caches in the repository"""
2503 2503 with repo.wlock(), repo.lock():
2504 2504 repo.updatecaches(full=True)
2505 2505
2506 2506 @command('debugupgraderepo', [
2507 2507 ('o', 'optimize', [], _('extra optimization to perform'), _('NAME')),
2508 2508 ('', 'run', False, _('performs an upgrade')),
2509 2509 ])
2510 2510 def debugupgraderepo(ui, repo, run=False, optimize=None):
2511 2511 """upgrade a repository to use different features
2512 2512
2513 2513 If no arguments are specified, the repository is evaluated for upgrade
2514 2514 and a list of problems and potential optimizations is printed.
2515 2515
2516 2516 With ``--run``, a repository upgrade is performed. Behavior of the upgrade
2517 2517 can be influenced via additional arguments. More details will be provided
2518 2518 by the command output when run without ``--run``.
2519 2519
2520 2520 During the upgrade, the repository will be locked and no writes will be
2521 2521 allowed.
2522 2522
2523 2523 At the end of the upgrade, the repository may not be readable while new
2524 2524 repository data is swapped in. This window will be as long as it takes to
2525 2525 rename some directories inside the ``.hg`` directory. On most machines, this
2526 2526 should complete almost instantaneously and the chances of a consumer being
2527 2527 unable to access the repository should be low.
2528 2528 """
2529 2529 return upgrade.upgraderepo(ui, repo, run=run, optimize=optimize)
2530 2530
2531 2531 @command('debugwalk', cmdutil.walkopts, _('[OPTION]... [FILE]...'),
2532 2532 inferrepo=True)
2533 2533 def debugwalk(ui, repo, *pats, **opts):
2534 2534 """show how files match on given patterns"""
2535 2535 opts = pycompat.byteskwargs(opts)
2536 2536 m = scmutil.match(repo[None], pats, opts)
2537 2537 ui.write(('matcher: %r\n' % m))
2538 2538 items = list(repo[None].walk(m))
2539 2539 if not items:
2540 2540 return
2541 2541 f = lambda fn: fn
2542 2542 if ui.configbool('ui', 'slash') and pycompat.ossep != '/':
2543 2543 f = lambda fn: util.normpath(fn)
2544 2544 fmt = 'f %%-%ds %%-%ds %%s' % (
2545 2545 max([len(abs) for abs in items]),
2546 2546 max([len(m.rel(abs)) for abs in items]))
2547 2547 for abs in items:
2548 2548 line = fmt % (abs, f(m.rel(abs)), m.exact(abs) and 'exact' or '')
2549 2549 ui.write("%s\n" % line.rstrip())
2550 2550
2551 2551 @command('debugwhyunstable', [], _('REV'))
2552 2552 def debugwhyunstable(ui, repo, rev):
2553 2553 """explain instabilities of a changeset"""
2554 2554 for entry in obsutil.whyunstable(repo, scmutil.revsingle(repo, rev)):
2555 2555 dnodes = ''
2556 2556 if entry.get('divergentnodes'):
2557 2557 dnodes = ' '.join('%s (%s)' % (ctx.hex(), ctx.phasestr())
2558 2558 for ctx in entry['divergentnodes']) + ' '
2559 2559 ui.write('%s: %s%s %s\n' % (entry['instability'], dnodes,
2560 2560 entry['reason'], entry['node']))
2561 2561
2562 2562 @command('debugwireargs',
2563 2563 [('', 'three', '', 'three'),
2564 2564 ('', 'four', '', 'four'),
2565 2565 ('', 'five', '', 'five'),
2566 2566 ] + cmdutil.remoteopts,
2567 2567 _('REPO [OPTIONS]... [ONE [TWO]]'),
2568 2568 norepo=True)
2569 2569 def debugwireargs(ui, repopath, *vals, **opts):
2570 2570 opts = pycompat.byteskwargs(opts)
2571 2571 repo = hg.peer(ui, opts, repopath)
2572 2572 for opt in cmdutil.remoteopts:
2573 2573 del opts[opt[1]]
2574 2574 args = {}
2575 2575 for k, v in opts.iteritems():
2576 2576 if v:
2577 2577 args[k] = v
2578 2578 args = pycompat.strkwargs(args)
2579 2579 # run twice to check that we don't mess up the stream for the next command
2580 2580 res1 = repo.debugwireargs(*vals, **args)
2581 2581 res2 = repo.debugwireargs(*vals, **args)
2582 2582 ui.write("%s\n" % res1)
2583 2583 if res1 != res2:
2584 2584 ui.warn("%s\n" % res2)
2585 2585
2586 2586 def _parsewirelangblocks(fh):
2587 2587 activeaction = None
2588 2588 blocklines = []
2589 2589
2590 2590 for line in fh:
2591 2591 line = line.rstrip()
2592 2592 if not line:
2593 2593 continue
2594 2594
2595 2595 if line.startswith(b'#'):
2596 2596 continue
2597 2597
2598 2598 if not line.startswith(' '):
2599 2599 # New block. Flush previous one.
2600 2600 if activeaction:
2601 2601 yield activeaction, blocklines
2602 2602
2603 2603 activeaction = line
2604 2604 blocklines = []
2605 2605 continue
2606 2606
2607 2607 # Else we start with an indent.
2608 2608
2609 2609 if not activeaction:
2610 2610 raise error.Abort(_('indented line outside of block'))
2611 2611
2612 2612 blocklines.append(line)
2613 2613
2614 2614 # Flush last block.
2615 2615 if activeaction:
2616 2616 yield activeaction, blocklines
2617 2617
2618 2618 @command('debugwireproto',
2619 2619 [
2620 2620 ('', 'localssh', False, _('start an SSH server for this repo')),
2621 2621 ('', 'peer', '', _('construct a specific version of the peer')),
2622 2622 ('', 'noreadstderr', False, _('do not read from stderr of the remote')),
2623 2623 ] + cmdutil.remoteopts,
2624 2624 _('[PATH]'),
2625 2625 optionalrepo=True)
2626 2626 def debugwireproto(ui, repo, path=None, **opts):
2627 2627 """send wire protocol commands to a server
2628 2628
2629 2629 This command can be used to issue wire protocol commands to remote
2630 2630 peers and to debug the raw data being exchanged.
2631 2631
2632 2632 ``--localssh`` will start an SSH server against the current repository
2633 2633 and connect to that. By default, the connection will perform a handshake
2634 2634 and establish an appropriate peer instance.
2635 2635
2636 2636 ``--peer`` can be used to bypass the handshake protocol and construct a
2637 2637 peer instance using the specified class type. Valid values are ``raw``,
2638 2638 ``http2``, ``ssh1``, and ``ssh2``. ``raw`` instances only allow sending
2639 2639 raw data payloads and don't support higher-level command actions.
2640 2640
2641 2641 ``--noreadstderr`` can be used to disable automatic reading from stderr
2642 2642 of the peer (for SSH connections only). Disabling automatic reading of
2643 2643 stderr is useful for making output more deterministic.
2644 2644
2645 2645 Commands are issued via a mini language which is specified via stdin.
2646 2646 The language consists of individual actions to perform. An action is
2647 2647 defined by a block. A block is defined as a line with no leading
2648 2648 space followed by 0 or more lines with leading space. Blocks are
2649 2649 effectively a high-level command with additional metadata.
2650 2650
2651 2651 Lines beginning with ``#`` are ignored.
2652 2652
2653 2653 The following sections denote available actions.
2654 2654
2655 2655 raw
2656 2656 ---
2657 2657
2658 2658 Send raw data to the server.
2659 2659
2660 2660 The block payload contains the raw data to send as one atomic send
2661 2661 operation. The data may not actually be delivered in a single system
2662 2662 call: it depends on the abilities of the transport being used.
2663 2663
2664 2664 Each line in the block is de-indented and concatenated. Then, that
2665 2665 value is evaluated as a Python b'' literal. This allows the use of
2666 2666 backslash escaping, etc.
2667 2667
2668 2668 raw+
2669 2669 ----
2670 2670
2671 2671 Behaves like ``raw`` except flushes output afterwards.
2672 2672
2673 2673 command <X>
2674 2674 -----------
2675 2675
2676 2676 Send a request to run a named command, whose name follows the ``command``
2677 2677 string.
2678 2678
2679 2679 Arguments to the command are defined as lines in this block. The format of
2680 2680 each line is ``<key> <value>``. e.g.::
2681 2681
2682 2682 command listkeys
2683 2683 namespace bookmarks
2684 2684
2685 2685 If the value begins with ``eval:``, it will be interpreted as a Python
2686 2686 literal expression. Otherwise values are interpreted as Python b'' literals.
2687 2687 This allows sending complex types and encoding special byte sequences via
2688 2688 backslash escaping.
2689 2689
2690 2690 The following arguments have special meaning:
2691 2691
2692 2692 ``PUSHFILE``
2693 2693 When defined, the *push* mechanism of the peer will be used instead
2694 2694 of the static request-response mechanism and the content of the
2695 2695 file specified in the value of this argument will be sent as the
2696 2696 command payload.
2697 2697
2698 2698 This can be used to submit a local bundle file to the remote.
2699 2699
2700 2700 batchbegin
2701 2701 ----------
2702 2702
2703 2703 Instruct the peer to begin a batched send.
2704 2704
2705 2705 All ``command`` blocks are queued for execution until the next
2706 2706 ``batchsubmit`` block.
2707 2707
2708 2708 batchsubmit
2709 2709 -----------
2710 2710
2711 2711 Submit previously queued ``command`` blocks as a batch request.
2712 2712
2713 2713 This action MUST be paired with a ``batchbegin`` action.
2714 2714
2715 2715 httprequest <method> <path>
2716 2716 ---------------------------
2717 2717
2718 2718 (HTTP peer only)
2719 2719
2720 2720 Send an HTTP request to the peer.
2721 2721
2722 2722 The HTTP request line follows the ``httprequest`` action. e.g. ``GET /foo``.
2723 2723
2724 2724 Arguments of the form ``<key>: <value>`` are interpreted as HTTP request
2725 2725 headers to add to the request. e.g. ``Accept: foo``.
2726 2726
2727 2727 The following arguments are special:
2728 2728
2729 2729 ``BODYFILE``
2730 2730 The content of the file defined as the value to this argument will be
2731 2731 transferred verbatim as the HTTP request body.
2732 2732
2733 2733 ``frame <type> <flags> <payload>``
2734 2734 Send a unified protocol frame as part of the request body.
2735 2735
2736 2736 All frames will be collected and sent as the body to the HTTP
2737 2737 request.
2738 2738
2739 2739 close
2740 2740 -----
2741 2741
2742 2742 Close the connection to the server.
2743 2743
2744 2744 flush
2745 2745 -----
2746 2746
2747 2747 Flush data written to the server.
2748 2748
2749 2749 readavailable
2750 2750 -------------
2751 2751
2752 2752 Close the write end of the connection and read all available data from
2753 2753 the server.
2754 2754
2755 2755 If the connection to the server encompasses multiple pipes, we poll both
2756 2756 pipes and read available data.
2757 2757
2758 2758 readline
2759 2759 --------
2760 2760
2761 2761 Read a line of output from the server. If there are multiple output
2762 2762 pipes, reads only the main pipe.
2763 2763
2764 2764 ereadline
2765 2765 ---------
2766 2766
2767 2767 Like ``readline``, but read from the stderr pipe, if available.
2768 2768
2769 2769 read <X>
2770 2770 --------
2771 2771
2772 2772 ``read()`` N bytes from the server's main output pipe.
2773 2773
2774 2774 eread <X>
2775 2775 ---------
2776 2776
2777 2777 ``read()`` N bytes from the server's stderr pipe, if available.
2778 2778
2779 2779 Specifying Unified Frame-Based Protocol Frames
2780 2780 ----------------------------------------------
2781 2781
2782 2782 It is possible to emit a *Unified Frame-Based Protocol* by using special
2783 2783 syntax.
2784 2784
2785 2785 A frame is composed as a type, flags, and payload. These can be parsed
2786 2786 from a string of the form:
2787 2787
2788 2788 <request-id> <stream-id> <stream-flags> <type> <flags> <payload>
2789 2789
2790 2790 ``request-id`` and ``stream-id`` are integers defining the request and
2791 2791 stream identifiers.
2792 2792
2793 2793 ``type`` can be an integer value for the frame type or the string name
2794 2794 of the type. The strings are defined in ``wireprotoframing.py``. e.g.
2795 2795 ``command-name``.
2796 2796
2797 2797 ``stream-flags`` and ``flags`` are a ``|`` delimited list of flag
2798 2798 components. Each component (and there can be just one) can be an integer
2799 2799 or a flag name for stream flags or frame flags, respectively. Values are
2800 2800 resolved to integers and then bitwise OR'd together.
2801 2801
2802 2802 ``payload`` represents the raw frame payload. If it begins with
2803 2803 ``cbor:``, the following string is evaluated as Python code and the
2804 2804 resulting object is fed into a CBOR encoder. Otherwise it is interpreted
2805 2805 as a Python byte string literal.
2806 2806 """
2807 2807 opts = pycompat.byteskwargs(opts)
2808 2808
2809 2809 if opts['localssh'] and not repo:
2810 2810 raise error.Abort(_('--localssh requires a repository'))
2811 2811
2812 2812 if opts['peer'] and opts['peer'] not in ('raw', 'http2', 'ssh1', 'ssh2'):
2813 2813 raise error.Abort(_('invalid value for --peer'),
2814 2814 hint=_('valid values are "raw", "ssh1", and "ssh2"'))
2815 2815
2816 2816 if path and opts['localssh']:
2817 2817 raise error.Abort(_('cannot specify --localssh with an explicit '
2818 2818 'path'))
2819 2819
2820 2820 if ui.interactive():
2821 2821 ui.write(_('(waiting for commands on stdin)\n'))
2822 2822
2823 2823 blocks = list(_parsewirelangblocks(ui.fin))
2824 2824
2825 2825 proc = None
2826 2826 stdin = None
2827 2827 stdout = None
2828 2828 stderr = None
2829 2829 opener = None
2830 2830
2831 2831 if opts['localssh']:
2832 2832 # We start the SSH server in its own process so there is process
2833 2833 # separation. This prevents a whole class of potential bugs around
2834 2834 # shared state from interfering with server operation.
2835 2835 args = procutil.hgcmd() + [
2836 2836 '-R', repo.root,
2837 2837 'debugserve', '--sshstdio',
2838 2838 ]
2839 2839 proc = subprocess.Popen(args, stdin=subprocess.PIPE,
2840 2840 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
2841 2841 bufsize=0)
2842 2842
2843 2843 stdin = proc.stdin
2844 2844 stdout = proc.stdout
2845 2845 stderr = proc.stderr
2846 2846
2847 2847 # We turn the pipes into observers so we can log I/O.
2848 2848 if ui.verbose or opts['peer'] == 'raw':
2849 2849 stdin = util.makeloggingfileobject(ui, proc.stdin, b'i',
2850 2850 logdata=True)
2851 2851 stdout = util.makeloggingfileobject(ui, proc.stdout, b'o',
2852 2852 logdata=True)
2853 2853 stderr = util.makeloggingfileobject(ui, proc.stderr, b'e',
2854 2854 logdata=True)
2855 2855
2856 2856 # --localssh also implies the peer connection settings.
2857 2857
2858 2858 url = 'ssh://localserver'
2859 2859 autoreadstderr = not opts['noreadstderr']
2860 2860
2861 2861 if opts['peer'] == 'ssh1':
2862 2862 ui.write(_('creating ssh peer for wire protocol version 1\n'))
2863 2863 peer = sshpeer.sshv1peer(ui, url, proc, stdin, stdout, stderr,
2864 2864 None, autoreadstderr=autoreadstderr)
2865 2865 elif opts['peer'] == 'ssh2':
2866 2866 ui.write(_('creating ssh peer for wire protocol version 2\n'))
2867 2867 peer = sshpeer.sshv2peer(ui, url, proc, stdin, stdout, stderr,
2868 2868 None, autoreadstderr=autoreadstderr)
2869 2869 elif opts['peer'] == 'raw':
2870 2870 ui.write(_('using raw connection to peer\n'))
2871 2871 peer = None
2872 2872 else:
2873 2873 ui.write(_('creating ssh peer from handshake results\n'))
2874 2874 peer = sshpeer.makepeer(ui, url, proc, stdin, stdout, stderr,
2875 2875 autoreadstderr=autoreadstderr)
2876 2876
2877 2877 elif path:
2878 2878 # We bypass hg.peer() so we can proxy the sockets.
2879 2879 # TODO consider not doing this because we skip
2880 2880 # ``hg.wirepeersetupfuncs`` and potentially other useful functionality.
2881 2881 u = util.url(path)
2882 2882 if u.scheme != 'http':
2883 2883 raise error.Abort(_('only http:// paths are currently supported'))
2884 2884
2885 2885 url, authinfo = u.authinfo()
2886 2886 openerargs = {
2887 2887 r'useragent': b'Mercurial debugwireproto',
2888 2888 }
2889 2889
2890 2890 # Turn pipes/sockets into observers so we can log I/O.
2891 2891 if ui.verbose:
2892 2892 openerargs.update({
2893 2893 r'loggingfh': ui,
2894 2894 r'loggingname': b's',
2895 2895 r'loggingopts': {
2896 2896 r'logdata': True,
2897 2897 r'logdataapis': False,
2898 2898 },
2899 2899 })
2900 2900
2901 2901 if ui.debugflag:
2902 2902 openerargs[r'loggingopts'][r'logdataapis'] = True
2903 2903
2904 2904 # Don't send default headers when in raw mode. This allows us to
2905 2905 # bypass most of the behavior of our URL handling code so we can
2906 2906 # have near complete control over what's sent on the wire.
2907 2907 if opts['peer'] == 'raw':
2908 2908 openerargs[r'sendaccept'] = False
2909 2909
2910 2910 opener = urlmod.opener(ui, authinfo, **openerargs)
2911 2911
2912 2912 if opts['peer'] == 'http2':
2913 2913 ui.write(_('creating http peer for wire protocol version 2\n'))
2914 2914 peer = httppeer.httpv2peer(
2915 ui, path, 'api/%s' % wireprototypes.HTTPV2,
2915 ui, path, 'api/%s' % wireprototypes.HTTP_WIREPROTO_V2,
2916 2916 opener, httppeer.urlreq.request, {})
2917 2917 elif opts['peer'] == 'raw':
2918 2918 ui.write(_('using raw connection to peer\n'))
2919 2919 peer = None
2920 2920 elif opts['peer']:
2921 2921 raise error.Abort(_('--peer %s not supported with HTTP peers') %
2922 2922 opts['peer'])
2923 2923 else:
2924 2924 peer = httppeer.makepeer(ui, path, opener=opener)
2925 2925
2926 2926 # We /could/ populate stdin/stdout with sock.makefile()...
2927 2927 else:
2928 2928 raise error.Abort(_('unsupported connection configuration'))
2929 2929
2930 2930 batchedcommands = None
2931 2931
2932 2932 # Now perform actions based on the parsed wire language instructions.
2933 2933 for action, lines in blocks:
2934 2934 if action in ('raw', 'raw+'):
2935 2935 if not stdin:
2936 2936 raise error.Abort(_('cannot call raw/raw+ on this peer'))
2937 2937
2938 2938 # Concatenate the data together.
2939 2939 data = ''.join(l.lstrip() for l in lines)
2940 2940 data = stringutil.unescapestr(data)
2941 2941 stdin.write(data)
2942 2942
2943 2943 if action == 'raw+':
2944 2944 stdin.flush()
2945 2945 elif action == 'flush':
2946 2946 if not stdin:
2947 2947 raise error.Abort(_('cannot call flush on this peer'))
2948 2948 stdin.flush()
2949 2949 elif action.startswith('command'):
2950 2950 if not peer:
2951 2951 raise error.Abort(_('cannot send commands unless peer instance '
2952 2952 'is available'))
2953 2953
2954 2954 command = action.split(' ', 1)[1]
2955 2955
2956 2956 args = {}
2957 2957 for line in lines:
2958 2958 # We need to allow empty values.
2959 2959 fields = line.lstrip().split(' ', 1)
2960 2960 if len(fields) == 1:
2961 2961 key = fields[0]
2962 2962 value = ''
2963 2963 else:
2964 2964 key, value = fields
2965 2965
2966 2966 if value.startswith('eval:'):
2967 2967 value = stringutil.evalpythonliteral(value[5:])
2968 2968 else:
2969 2969 value = stringutil.unescapestr(value)
2970 2970
2971 2971 args[key] = value
2972 2972
2973 2973 if batchedcommands is not None:
2974 2974 batchedcommands.append((command, args))
2975 2975 continue
2976 2976
2977 2977 ui.status(_('sending %s command\n') % command)
2978 2978
2979 2979 if 'PUSHFILE' in args:
2980 2980 with open(args['PUSHFILE'], r'rb') as fh:
2981 2981 del args['PUSHFILE']
2982 2982 res, output = peer._callpush(command, fh,
2983 2983 **pycompat.strkwargs(args))
2984 2984 ui.status(_('result: %s\n') % stringutil.escapestr(res))
2985 2985 ui.status(_('remote output: %s\n') %
2986 2986 stringutil.escapestr(output))
2987 2987 else:
2988 2988 res = peer._call(command, **pycompat.strkwargs(args))
2989 2989 ui.status(_('response: %s\n') % stringutil.pprint(res))
2990 2990
2991 2991 elif action == 'batchbegin':
2992 2992 if batchedcommands is not None:
2993 2993 raise error.Abort(_('nested batchbegin not allowed'))
2994 2994
2995 2995 batchedcommands = []
2996 2996 elif action == 'batchsubmit':
2997 2997 # There is a batching API we could go through. But it would be
2998 2998 # difficult to normalize requests into function calls. It is easier
2999 2999 # to bypass this layer and normalize to commands + args.
3000 3000 ui.status(_('sending batch with %d sub-commands\n') %
3001 3001 len(batchedcommands))
3002 3002 for i, chunk in enumerate(peer._submitbatch(batchedcommands)):
3003 3003 ui.status(_('response #%d: %s\n') %
3004 3004 (i, stringutil.escapestr(chunk)))
3005 3005
3006 3006 batchedcommands = None
3007 3007
3008 3008 elif action.startswith('httprequest '):
3009 3009 if not opener:
3010 3010 raise error.Abort(_('cannot use httprequest without an HTTP '
3011 3011 'peer'))
3012 3012
3013 3013 request = action.split(' ', 2)
3014 3014 if len(request) != 3:
3015 3015 raise error.Abort(_('invalid httprequest: expected format is '
3016 3016 '"httprequest <method> <path>'))
3017 3017
3018 3018 method, httppath = request[1:]
3019 3019 headers = {}
3020 3020 body = None
3021 3021 frames = []
3022 3022 for line in lines:
3023 3023 line = line.lstrip()
3024 3024 m = re.match(b'^([a-zA-Z0-9_-]+): (.*)$', line)
3025 3025 if m:
3026 3026 headers[m.group(1)] = m.group(2)
3027 3027 continue
3028 3028
3029 3029 if line.startswith(b'BODYFILE '):
3030 3030 with open(line.split(b' ', 1), 'rb') as fh:
3031 3031 body = fh.read()
3032 3032 elif line.startswith(b'frame '):
3033 3033 frame = wireprotoframing.makeframefromhumanstring(
3034 3034 line[len(b'frame '):])
3035 3035
3036 3036 frames.append(frame)
3037 3037 else:
3038 3038 raise error.Abort(_('unknown argument to httprequest: %s') %
3039 3039 line)
3040 3040
3041 3041 url = path + httppath
3042 3042
3043 3043 if frames:
3044 3044 body = b''.join(bytes(f) for f in frames)
3045 3045
3046 3046 req = urlmod.urlreq.request(pycompat.strurl(url), body, headers)
3047 3047
3048 3048 # urllib.Request insists on using has_data() as a proxy for
3049 3049 # determining the request method. Override that to use our
3050 3050 # explicitly requested method.
3051 3051 req.get_method = lambda: method
3052 3052
3053 3053 try:
3054 3054 res = opener.open(req)
3055 3055 body = res.read()
3056 3056 except util.urlerr.urlerror as e:
3057 3057 e.read()
3058 3058 continue
3059 3059
3060 3060 if res.headers.get('Content-Type') == 'application/mercurial-cbor':
3061 3061 ui.write(_('cbor> %s\n') % stringutil.pprint(cbor.loads(body)))
3062 3062
3063 3063 elif action == 'close':
3064 3064 peer.close()
3065 3065 elif action == 'readavailable':
3066 3066 if not stdout or not stderr:
3067 3067 raise error.Abort(_('readavailable not available on this peer'))
3068 3068
3069 3069 stdin.close()
3070 3070 stdout.read()
3071 3071 stderr.read()
3072 3072
3073 3073 elif action == 'readline':
3074 3074 if not stdout:
3075 3075 raise error.Abort(_('readline not available on this peer'))
3076 3076 stdout.readline()
3077 3077 elif action == 'ereadline':
3078 3078 if not stderr:
3079 3079 raise error.Abort(_('ereadline not available on this peer'))
3080 3080 stderr.readline()
3081 3081 elif action.startswith('read '):
3082 3082 count = int(action.split(' ', 1)[1])
3083 3083 if not stdout:
3084 3084 raise error.Abort(_('read not available on this peer'))
3085 3085 stdout.read(count)
3086 3086 elif action.startswith('eread '):
3087 3087 count = int(action.split(' ', 1)[1])
3088 3088 if not stderr:
3089 3089 raise error.Abort(_('eread not available on this peer'))
3090 3090 stderr.read(count)
3091 3091 else:
3092 3092 raise error.Abort(_('unknown action: %s') % action)
3093 3093
3094 3094 if batchedcommands is not None:
3095 3095 raise error.Abort(_('unclosed "batchbegin" request'))
3096 3096
3097 3097 if peer:
3098 3098 peer.close()
3099 3099
3100 3100 if proc:
3101 3101 proc.kill()
@@ -1,817 +1,817 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 io
13 13 import os
14 14 import socket
15 15 import struct
16 16 import tempfile
17 17
18 18 from .i18n import _
19 19 from .thirdparty import (
20 20 cbor,
21 21 )
22 22 from .thirdparty.zope import (
23 23 interface as zi,
24 24 )
25 25 from . import (
26 26 bundle2,
27 27 error,
28 28 httpconnection,
29 29 pycompat,
30 30 repository,
31 31 statichttprepo,
32 32 url as urlmod,
33 33 util,
34 34 wireproto,
35 35 wireprotoframing,
36 36 wireprototypes,
37 37 wireprotov1peer,
38 38 wireprotov2server,
39 39 )
40 40
41 41 httplib = util.httplib
42 42 urlerr = util.urlerr
43 43 urlreq = util.urlreq
44 44
45 45 def encodevalueinheaders(value, header, limit):
46 46 """Encode a string value into multiple HTTP headers.
47 47
48 48 ``value`` will be encoded into 1 or more HTTP headers with the names
49 49 ``header-<N>`` where ``<N>`` is an integer starting at 1. Each header
50 50 name + value will be at most ``limit`` bytes long.
51 51
52 52 Returns an iterable of 2-tuples consisting of header names and
53 53 values as native strings.
54 54 """
55 55 # HTTP Headers are ASCII. Python 3 requires them to be unicodes,
56 56 # not bytes. This function always takes bytes in as arguments.
57 57 fmt = pycompat.strurl(header) + r'-%s'
58 58 # Note: it is *NOT* a bug that the last bit here is a bytestring
59 59 # and not a unicode: we're just getting the encoded length anyway,
60 60 # and using an r-string to make it portable between Python 2 and 3
61 61 # doesn't work because then the \r is a literal backslash-r
62 62 # instead of a carriage return.
63 63 valuelen = limit - len(fmt % r'000') - len(': \r\n')
64 64 result = []
65 65
66 66 n = 0
67 67 for i in xrange(0, len(value), valuelen):
68 68 n += 1
69 69 result.append((fmt % str(n), pycompat.strurl(value[i:i + valuelen])))
70 70
71 71 return result
72 72
73 73 def _wraphttpresponse(resp):
74 74 """Wrap an HTTPResponse with common error handlers.
75 75
76 76 This ensures that any I/O from any consumer raises the appropriate
77 77 error and messaging.
78 78 """
79 79 origread = resp.read
80 80
81 81 class readerproxy(resp.__class__):
82 82 def read(self, size=None):
83 83 try:
84 84 return origread(size)
85 85 except httplib.IncompleteRead as e:
86 86 # e.expected is an integer if length known or None otherwise.
87 87 if e.expected:
88 88 msg = _('HTTP request error (incomplete response; '
89 89 'expected %d bytes got %d)') % (e.expected,
90 90 len(e.partial))
91 91 else:
92 92 msg = _('HTTP request error (incomplete response)')
93 93
94 94 raise error.PeerTransportError(
95 95 msg,
96 96 hint=_('this may be an intermittent network failure; '
97 97 'if the error persists, consider contacting the '
98 98 'network or server operator'))
99 99 except httplib.HTTPException as e:
100 100 raise error.PeerTransportError(
101 101 _('HTTP request error (%s)') % e,
102 102 hint=_('this may be an intermittent network failure; '
103 103 'if the error persists, consider contacting the '
104 104 'network or server operator'))
105 105
106 106 resp.__class__ = readerproxy
107 107
108 108 class _multifile(object):
109 109 def __init__(self, *fileobjs):
110 110 for f in fileobjs:
111 111 if not util.safehasattr(f, 'length'):
112 112 raise ValueError(
113 113 '_multifile only supports file objects that '
114 114 'have a length but this one does not:', type(f), f)
115 115 self._fileobjs = fileobjs
116 116 self._index = 0
117 117
118 118 @property
119 119 def length(self):
120 120 return sum(f.length for f in self._fileobjs)
121 121
122 122 def read(self, amt=None):
123 123 if amt <= 0:
124 124 return ''.join(f.read() for f in self._fileobjs)
125 125 parts = []
126 126 while amt and self._index < len(self._fileobjs):
127 127 parts.append(self._fileobjs[self._index].read(amt))
128 128 got = len(parts[-1])
129 129 if got < amt:
130 130 self._index += 1
131 131 amt -= got
132 132 return ''.join(parts)
133 133
134 134 def seek(self, offset, whence=os.SEEK_SET):
135 135 if whence != os.SEEK_SET:
136 136 raise NotImplementedError(
137 137 '_multifile does not support anything other'
138 138 ' than os.SEEK_SET for whence on seek()')
139 139 if offset != 0:
140 140 raise NotImplementedError(
141 141 '_multifile only supports seeking to start, but that '
142 142 'could be fixed if you need it')
143 143 for f in self._fileobjs:
144 144 f.seek(0)
145 145 self._index = 0
146 146
147 147 def makev1commandrequest(ui, requestbuilder, caps, capablefn,
148 148 repobaseurl, cmd, args):
149 149 """Make an HTTP request to run a command for a version 1 client.
150 150
151 151 ``caps`` is a set of known server capabilities. The value may be
152 152 None if capabilities are not yet known.
153 153
154 154 ``capablefn`` is a function to evaluate a capability.
155 155
156 156 ``cmd``, ``args``, and ``data`` define the command, its arguments, and
157 157 raw data to pass to it.
158 158 """
159 159 if cmd == 'pushkey':
160 160 args['data'] = ''
161 161 data = args.pop('data', None)
162 162 headers = args.pop('headers', {})
163 163
164 164 ui.debug("sending %s command\n" % cmd)
165 165 q = [('cmd', cmd)]
166 166 headersize = 0
167 167 # Important: don't use self.capable() here or else you end up
168 168 # with infinite recursion when trying to look up capabilities
169 169 # for the first time.
170 170 postargsok = caps is not None and 'httppostargs' in caps
171 171
172 172 # Send arguments via POST.
173 173 if postargsok and args:
174 174 strargs = urlreq.urlencode(sorted(args.items()))
175 175 if not data:
176 176 data = strargs
177 177 else:
178 178 if isinstance(data, bytes):
179 179 i = io.BytesIO(data)
180 180 i.length = len(data)
181 181 data = i
182 182 argsio = io.BytesIO(strargs)
183 183 argsio.length = len(strargs)
184 184 data = _multifile(argsio, data)
185 185 headers[r'X-HgArgs-Post'] = len(strargs)
186 186 elif args:
187 187 # Calling self.capable() can infinite loop if we are calling
188 188 # "capabilities". But that command should never accept wire
189 189 # protocol arguments. So this should never happen.
190 190 assert cmd != 'capabilities'
191 191 httpheader = capablefn('httpheader')
192 192 if httpheader:
193 193 headersize = int(httpheader.split(',', 1)[0])
194 194
195 195 # Send arguments via HTTP headers.
196 196 if headersize > 0:
197 197 # The headers can typically carry more data than the URL.
198 198 encargs = urlreq.urlencode(sorted(args.items()))
199 199 for header, value in encodevalueinheaders(encargs, 'X-HgArg',
200 200 headersize):
201 201 headers[header] = value
202 202 # Send arguments via query string (Mercurial <1.9).
203 203 else:
204 204 q += sorted(args.items())
205 205
206 206 qs = '?%s' % urlreq.urlencode(q)
207 207 cu = "%s%s" % (repobaseurl, qs)
208 208 size = 0
209 209 if util.safehasattr(data, 'length'):
210 210 size = data.length
211 211 elif data is not None:
212 212 size = len(data)
213 213 if data is not None and r'Content-Type' not in headers:
214 214 headers[r'Content-Type'] = r'application/mercurial-0.1'
215 215
216 216 # Tell the server we accept application/mercurial-0.2 and multiple
217 217 # compression formats if the server is capable of emitting those
218 218 # payloads.
219 219 # Note: Keep this set empty by default, as client advertisement of
220 220 # protocol parameters should only occur after the handshake.
221 221 protoparams = set()
222 222
223 223 mediatypes = set()
224 224 if caps is not None:
225 225 mt = capablefn('httpmediatype')
226 226 if mt:
227 227 protoparams.add('0.1')
228 228 mediatypes = set(mt.split(','))
229 229
230 230 protoparams.add('partial-pull')
231 231
232 232 if '0.2tx' in mediatypes:
233 233 protoparams.add('0.2')
234 234
235 235 if '0.2tx' in mediatypes and capablefn('compression'):
236 236 # We /could/ compare supported compression formats and prune
237 237 # non-mutually supported or error if nothing is mutually supported.
238 238 # For now, send the full list to the server and have it error.
239 239 comps = [e.wireprotosupport().name for e in
240 240 util.compengines.supportedwireengines(util.CLIENTROLE)]
241 241 protoparams.add('comp=%s' % ','.join(comps))
242 242
243 243 if protoparams:
244 244 protoheaders = encodevalueinheaders(' '.join(sorted(protoparams)),
245 245 'X-HgProto',
246 246 headersize or 1024)
247 247 for header, value in protoheaders:
248 248 headers[header] = value
249 249
250 250 varyheaders = []
251 251 for header in headers:
252 252 if header.lower().startswith(r'x-hg'):
253 253 varyheaders.append(header)
254 254
255 255 if varyheaders:
256 256 headers[r'Vary'] = r','.join(sorted(varyheaders))
257 257
258 258 req = requestbuilder(pycompat.strurl(cu), data, headers)
259 259
260 260 if data is not None:
261 261 ui.debug("sending %d bytes\n" % size)
262 262 req.add_unredirected_header(r'Content-Length', r'%d' % size)
263 263
264 264 return req, cu, qs
265 265
266 266 def sendrequest(ui, opener, req):
267 267 """Send a prepared HTTP request.
268 268
269 269 Returns the response object.
270 270 """
271 271 if (ui.debugflag
272 272 and ui.configbool('devel', 'debug.peer-request')):
273 273 dbg = ui.debug
274 274 line = 'devel-peer-request: %s\n'
275 275 dbg(line % '%s %s' % (req.get_method(), req.get_full_url()))
276 276 hgargssize = None
277 277
278 278 for header, value in sorted(req.header_items()):
279 279 if header.startswith('X-hgarg-'):
280 280 if hgargssize is None:
281 281 hgargssize = 0
282 282 hgargssize += len(value)
283 283 else:
284 284 dbg(line % ' %s %s' % (header, value))
285 285
286 286 if hgargssize is not None:
287 287 dbg(line % ' %d bytes of commands arguments in headers'
288 288 % hgargssize)
289 289
290 290 if req.has_data():
291 291 data = req.get_data()
292 292 length = getattr(data, 'length', None)
293 293 if length is None:
294 294 length = len(data)
295 295 dbg(line % ' %d bytes of data' % length)
296 296
297 297 start = util.timer()
298 298
299 299 try:
300 300 res = opener.open(req)
301 301 except urlerr.httperror as inst:
302 302 if inst.code == 401:
303 303 raise error.Abort(_('authorization failed'))
304 304 raise
305 305 except httplib.HTTPException as inst:
306 306 ui.debug('http error requesting %s\n' %
307 307 util.hidepassword(req.get_full_url()))
308 308 ui.traceback()
309 309 raise IOError(None, inst)
310 310 finally:
311 311 if ui.configbool('devel', 'debug.peer-request'):
312 312 dbg(line % ' finished in %.4f seconds (%s)'
313 313 % (util.timer() - start, res.code))
314 314
315 315 # Insert error handlers for common I/O failures.
316 316 _wraphttpresponse(res)
317 317
318 318 return res
319 319
320 320 def parsev1commandresponse(ui, baseurl, requrl, qs, resp, compressible,
321 321 allowcbor=False):
322 322 # record the url we got redirected to
323 323 respurl = pycompat.bytesurl(resp.geturl())
324 324 if respurl.endswith(qs):
325 325 respurl = respurl[:-len(qs)]
326 326 if baseurl.rstrip('/') != respurl.rstrip('/'):
327 327 if not ui.quiet:
328 328 ui.warn(_('real URL is %s\n') % respurl)
329 329
330 330 try:
331 331 proto = pycompat.bytesurl(resp.getheader(r'content-type', r''))
332 332 except AttributeError:
333 333 proto = pycompat.bytesurl(resp.headers.get(r'content-type', r''))
334 334
335 335 safeurl = util.hidepassword(baseurl)
336 336 if proto.startswith('application/hg-error'):
337 337 raise error.OutOfBandError(resp.read())
338 338
339 339 # Pre 1.0 versions of Mercurial used text/plain and
340 340 # application/hg-changegroup. We don't support such old servers.
341 341 if not proto.startswith('application/mercurial-'):
342 342 ui.debug("requested URL: '%s'\n" % util.hidepassword(requrl))
343 343 raise error.RepoError(
344 344 _("'%s' does not appear to be an hg repository:\n"
345 345 "---%%<--- (%s)\n%s\n---%%<---\n")
346 346 % (safeurl, proto or 'no content-type', resp.read(1024)))
347 347
348 348 try:
349 349 subtype = proto.split('-', 1)[1]
350 350
351 351 # Unless we end up supporting CBOR in the legacy wire protocol,
352 352 # this should ONLY be encountered for the initial capabilities
353 353 # request during handshake.
354 354 if subtype == 'cbor':
355 355 if allowcbor:
356 356 return respurl, proto, resp
357 357 else:
358 358 raise error.RepoError(_('unexpected CBOR response from '
359 359 'server'))
360 360
361 361 version_info = tuple([int(n) for n in subtype.split('.')])
362 362 except ValueError:
363 363 raise error.RepoError(_("'%s' sent a broken Content-Type "
364 364 "header (%s)") % (safeurl, proto))
365 365
366 366 # TODO consider switching to a decompression reader that uses
367 367 # generators.
368 368 if version_info == (0, 1):
369 369 if compressible:
370 370 resp = util.compengines['zlib'].decompressorreader(resp)
371 371
372 372 elif version_info == (0, 2):
373 373 # application/mercurial-0.2 always identifies the compression
374 374 # engine in the payload header.
375 375 elen = struct.unpack('B', resp.read(1))[0]
376 376 ename = resp.read(elen)
377 377 engine = util.compengines.forwiretype(ename)
378 378
379 379 resp = engine.decompressorreader(resp)
380 380 else:
381 381 raise error.RepoError(_("'%s' uses newer protocol %s") %
382 382 (safeurl, subtype))
383 383
384 384 return respurl, proto, resp
385 385
386 386 class httppeer(wireprotov1peer.wirepeer):
387 387 def __init__(self, ui, path, url, opener, requestbuilder, caps):
388 388 self.ui = ui
389 389 self._path = path
390 390 self._url = url
391 391 self._caps = caps
392 392 self._urlopener = opener
393 393 self._requestbuilder = requestbuilder
394 394
395 395 def __del__(self):
396 396 for h in self._urlopener.handlers:
397 397 h.close()
398 398 getattr(h, "close_all", lambda: None)()
399 399
400 400 # Begin of ipeerconnection interface.
401 401
402 402 def url(self):
403 403 return self._path
404 404
405 405 def local(self):
406 406 return None
407 407
408 408 def peer(self):
409 409 return self
410 410
411 411 def canpush(self):
412 412 return True
413 413
414 414 def close(self):
415 415 pass
416 416
417 417 # End of ipeerconnection interface.
418 418
419 419 # Begin of ipeercommands interface.
420 420
421 421 def capabilities(self):
422 422 return self._caps
423 423
424 424 # End of ipeercommands interface.
425 425
426 426 # look up capabilities only when needed
427 427
428 428 def _callstream(self, cmd, _compressible=False, **args):
429 429 args = pycompat.byteskwargs(args)
430 430
431 431 req, cu, qs = makev1commandrequest(self.ui, self._requestbuilder,
432 432 self._caps, self.capable,
433 433 self._url, cmd, args)
434 434
435 435 resp = sendrequest(self.ui, self._urlopener, req)
436 436
437 437 self._url, ct, resp = parsev1commandresponse(self.ui, self._url, cu, qs,
438 438 resp, _compressible)
439 439
440 440 return resp
441 441
442 442 def _call(self, cmd, **args):
443 443 fp = self._callstream(cmd, **args)
444 444 try:
445 445 return fp.read()
446 446 finally:
447 447 # if using keepalive, allow connection to be reused
448 448 fp.close()
449 449
450 450 def _callpush(self, cmd, cg, **args):
451 451 # have to stream bundle to a temp file because we do not have
452 452 # http 1.1 chunked transfer.
453 453
454 454 types = self.capable('unbundle')
455 455 try:
456 456 types = types.split(',')
457 457 except AttributeError:
458 458 # servers older than d1b16a746db6 will send 'unbundle' as a
459 459 # boolean capability. They only support headerless/uncompressed
460 460 # bundles.
461 461 types = [""]
462 462 for x in types:
463 463 if x in bundle2.bundletypes:
464 464 type = x
465 465 break
466 466
467 467 tempname = bundle2.writebundle(self.ui, cg, None, type)
468 468 fp = httpconnection.httpsendfile(self.ui, tempname, "rb")
469 469 headers = {r'Content-Type': r'application/mercurial-0.1'}
470 470
471 471 try:
472 472 r = self._call(cmd, data=fp, headers=headers, **args)
473 473 vals = r.split('\n', 1)
474 474 if len(vals) < 2:
475 475 raise error.ResponseError(_("unexpected response:"), r)
476 476 return vals
477 477 except urlerr.httperror:
478 478 # Catch and re-raise these so we don't try and treat them
479 479 # like generic socket errors. They lack any values in
480 480 # .args on Python 3 which breaks our socket.error block.
481 481 raise
482 482 except socket.error as err:
483 483 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
484 484 raise error.Abort(_('push failed: %s') % err.args[1])
485 485 raise error.Abort(err.args[1])
486 486 finally:
487 487 fp.close()
488 488 os.unlink(tempname)
489 489
490 490 def _calltwowaystream(self, cmd, fp, **args):
491 491 fh = None
492 492 fp_ = None
493 493 filename = None
494 494 try:
495 495 # dump bundle to disk
496 496 fd, filename = tempfile.mkstemp(prefix="hg-bundle-", suffix=".hg")
497 497 fh = os.fdopen(fd, r"wb")
498 498 d = fp.read(4096)
499 499 while d:
500 500 fh.write(d)
501 501 d = fp.read(4096)
502 502 fh.close()
503 503 # start http push
504 504 fp_ = httpconnection.httpsendfile(self.ui, filename, "rb")
505 505 headers = {r'Content-Type': r'application/mercurial-0.1'}
506 506 return self._callstream(cmd, data=fp_, headers=headers, **args)
507 507 finally:
508 508 if fp_ is not None:
509 509 fp_.close()
510 510 if fh is not None:
511 511 fh.close()
512 512 os.unlink(filename)
513 513
514 514 def _callcompressable(self, cmd, **args):
515 515 return self._callstream(cmd, _compressible=True, **args)
516 516
517 517 def _abort(self, exception):
518 518 raise exception
519 519
520 520 # TODO implement interface for version 2 peers
521 521 @zi.implementer(repository.ipeerconnection, repository.ipeercapabilities)
522 522 class httpv2peer(object):
523 523 def __init__(self, ui, repourl, apipath, opener, requestbuilder,
524 524 apidescriptor):
525 525 self.ui = ui
526 526
527 527 if repourl.endswith('/'):
528 528 repourl = repourl[:-1]
529 529
530 530 self._url = repourl
531 531 self._apipath = apipath
532 532 self._opener = opener
533 533 self._requestbuilder = requestbuilder
534 534 self._descriptor = apidescriptor
535 535
536 536 # Start of ipeerconnection.
537 537
538 538 def url(self):
539 539 return self._url
540 540
541 541 def local(self):
542 542 return None
543 543
544 544 def peer(self):
545 545 return self
546 546
547 547 def canpush(self):
548 548 # TODO change once implemented.
549 549 return False
550 550
551 551 def close(self):
552 552 pass
553 553
554 554 # End of ipeerconnection.
555 555
556 556 # Start of ipeercapabilities.
557 557
558 558 def capable(self, name):
559 559 # The capabilities used internally historically map to capabilities
560 560 # advertised from the "capabilities" wire protocol command. However,
561 561 # version 2 of that command works differently.
562 562
563 563 # Maps to commands that are available.
564 564 if name in ('branchmap', 'getbundle', 'known', 'lookup', 'pushkey'):
565 565 return True
566 566
567 567 # Other concepts.
568 568 if name in ('bundle2',):
569 569 return True
570 570
571 571 return False
572 572
573 573 def requirecap(self, name, purpose):
574 574 if self.capable(name):
575 575 return
576 576
577 577 raise error.CapabilityError(
578 578 _('cannot %s; client or remote repository does not support the %r '
579 579 'capability') % (purpose, name))
580 580
581 581 # End of ipeercapabilities.
582 582
583 583 # TODO require to be part of a batched primitive, use futures.
584 584 def _call(self, name, **args):
585 585 """Call a wire protocol command with arguments."""
586 586
587 587 # Having this early has a side-effect of importing wireprotov2server,
588 588 # which has the side-effect of ensuring commands are registered.
589 589
590 590 # TODO modify user-agent to reflect v2.
591 591 headers = {
592 592 r'Accept': wireprotov2server.FRAMINGTYPE,
593 593 r'Content-Type': wireprotov2server.FRAMINGTYPE,
594 594 }
595 595
596 596 # TODO permissions should come from capabilities results.
597 597 permission = wireproto.commandsv2[name].permission
598 598 if permission not in ('push', 'pull'):
599 599 raise error.ProgrammingError('unknown permission type: %s' %
600 600 permission)
601 601
602 602 permission = {
603 603 'push': 'rw',
604 604 'pull': 'ro',
605 605 }[permission]
606 606
607 607 url = '%s/%s/%s/%s' % (self._url, self._apipath, permission, name)
608 608
609 609 # TODO this should be part of a generic peer for the frame-based
610 610 # protocol.
611 611 reactor = wireprotoframing.clientreactor(hasmultiplesend=False,
612 612 buffersends=True)
613 613
614 614 request, action, meta = reactor.callcommand(name, args)
615 615 assert action == 'noop'
616 616
617 617 action, meta = reactor.flushcommands()
618 618 assert action == 'sendframes'
619 619
620 620 body = b''.join(map(bytes, meta['framegen']))
621 621 req = self._requestbuilder(pycompat.strurl(url), body, headers)
622 622 req.add_unredirected_header(r'Content-Length', r'%d' % len(body))
623 623
624 624 # TODO unify this code with httppeer.
625 625 try:
626 626 res = self._opener.open(req)
627 627 except urlerr.httperror as e:
628 628 if e.code == 401:
629 629 raise error.Abort(_('authorization failed'))
630 630
631 631 raise
632 632 except httplib.HTTPException as e:
633 633 self.ui.traceback()
634 634 raise IOError(None, e)
635 635
636 636 # TODO validate response type, wrap response to handle I/O errors.
637 637 # TODO more robust frame receiver.
638 638 results = []
639 639
640 640 while True:
641 641 frame = wireprotoframing.readframe(res)
642 642 if frame is None:
643 643 break
644 644
645 645 self.ui.note(_('received %r\n') % frame)
646 646
647 647 action, meta = reactor.onframerecv(frame)
648 648
649 649 if action == 'responsedata':
650 650 if meta['cbor']:
651 651 payload = util.bytesio(meta['data'])
652 652
653 653 decoder = cbor.CBORDecoder(payload)
654 654 while payload.tell() + 1 < len(meta['data']):
655 655 results.append(decoder.decode())
656 656 else:
657 657 results.append(meta['data'])
658 658 else:
659 659 error.ProgrammingError('unhandled action: %s' % action)
660 660
661 661 return results
662 662
663 663 # Registry of API service names to metadata about peers that handle it.
664 664 #
665 665 # The following keys are meaningful:
666 666 #
667 667 # init
668 668 # Callable receiving (ui, repourl, servicepath, opener, requestbuilder,
669 669 # apidescriptor) to create a peer.
670 670 #
671 671 # priority
672 672 # Integer priority for the service. If we could choose from multiple
673 673 # services, we choose the one with the highest priority.
674 674 API_PEERS = {
675 wireprototypes.HTTPV2: {
675 wireprototypes.HTTP_WIREPROTO_V2: {
676 676 'init': httpv2peer,
677 677 'priority': 50,
678 678 },
679 679 }
680 680
681 681 def performhandshake(ui, url, opener, requestbuilder):
682 682 # The handshake is a request to the capabilities command.
683 683
684 684 caps = None
685 685 def capable(x):
686 686 raise error.ProgrammingError('should not be called')
687 687
688 688 args = {}
689 689
690 690 # The client advertises support for newer protocols by adding an
691 691 # X-HgUpgrade-* header with a list of supported APIs and an
692 692 # X-HgProto-* header advertising which serializing formats it supports.
693 693 # We only support the HTTP version 2 transport and CBOR responses for
694 694 # now.
695 695 advertisev2 = ui.configbool('experimental', 'httppeer.advertise-v2')
696 696
697 697 if advertisev2:
698 698 args['headers'] = {
699 699 r'X-HgProto-1': r'cbor',
700 700 }
701 701
702 702 args['headers'].update(
703 703 encodevalueinheaders(' '.join(sorted(API_PEERS)),
704 704 'X-HgUpgrade',
705 705 # We don't know the header limit this early.
706 706 # So make it small.
707 707 1024))
708 708
709 709 req, requrl, qs = makev1commandrequest(ui, requestbuilder, caps,
710 710 capable, url, 'capabilities',
711 711 args)
712 712
713 713 resp = sendrequest(ui, opener, req)
714 714
715 715 respurl, ct, resp = parsev1commandresponse(ui, url, requrl, qs, resp,
716 716 compressible=False,
717 717 allowcbor=advertisev2)
718 718
719 719 try:
720 720 rawdata = resp.read()
721 721 finally:
722 722 resp.close()
723 723
724 724 if not ct.startswith('application/mercurial-'):
725 725 raise error.ProgrammingError('unexpected content-type: %s' % ct)
726 726
727 727 if advertisev2:
728 728 if ct == 'application/mercurial-cbor':
729 729 try:
730 730 info = cbor.loads(rawdata)
731 731 except cbor.CBORDecodeError:
732 732 raise error.Abort(_('error decoding CBOR from remote server'),
733 733 hint=_('try again and consider contacting '
734 734 'the server operator'))
735 735
736 736 # We got a legacy response. That's fine.
737 737 elif ct in ('application/mercurial-0.1', 'application/mercurial-0.2'):
738 738 info = {
739 739 'v1capabilities': set(rawdata.split())
740 740 }
741 741
742 742 else:
743 743 raise error.RepoError(
744 744 _('unexpected response type from server: %s') % ct)
745 745 else:
746 746 info = {
747 747 'v1capabilities': set(rawdata.split())
748 748 }
749 749
750 750 return respurl, info
751 751
752 752 def makepeer(ui, path, opener=None, requestbuilder=urlreq.request):
753 753 """Construct an appropriate HTTP peer instance.
754 754
755 755 ``opener`` is an ``url.opener`` that should be used to establish
756 756 connections, perform HTTP requests.
757 757
758 758 ``requestbuilder`` is the type used for constructing HTTP requests.
759 759 It exists as an argument so extensions can override the default.
760 760 """
761 761 u = util.url(path)
762 762 if u.query or u.fragment:
763 763 raise error.Abort(_('unsupported URL component: "%s"') %
764 764 (u.query or u.fragment))
765 765
766 766 # urllib cannot handle URLs with embedded user or passwd.
767 767 url, authinfo = u.authinfo()
768 768 ui.debug('using %s\n' % url)
769 769
770 770 opener = opener or urlmod.opener(ui, authinfo)
771 771
772 772 respurl, info = performhandshake(ui, url, opener, requestbuilder)
773 773
774 774 # Given the intersection of APIs that both we and the server support,
775 775 # sort by their advertised priority and pick the first one.
776 776 #
777 777 # TODO consider making this request-based and interface driven. For
778 778 # example, the caller could say "I want a peer that does X." It's quite
779 779 # possible that not all peers would do that. Since we know the service
780 780 # capabilities, we could filter out services not meeting the
781 781 # requirements. Possibly by consulting the interfaces defined by the
782 782 # peer type.
783 783 apipeerchoices = set(info.get('apis', {}).keys()) & set(API_PEERS.keys())
784 784
785 785 preferredchoices = sorted(apipeerchoices,
786 786 key=lambda x: API_PEERS[x]['priority'],
787 787 reverse=True)
788 788
789 789 for service in preferredchoices:
790 790 apipath = '%s/%s' % (info['apibase'].rstrip('/'), service)
791 791
792 792 return API_PEERS[service]['init'](ui, respurl, apipath, opener,
793 793 requestbuilder,
794 794 info['apis'][service])
795 795
796 796 # Failed to construct an API peer. Fall back to legacy.
797 797 return httppeer(ui, path, respurl, opener, requestbuilder,
798 798 info['v1capabilities'])
799 799
800 800 def instance(ui, path, create):
801 801 if create:
802 802 raise error.Abort(_('cannot create new http repository'))
803 803 try:
804 804 if path.startswith('https:') and not urlmod.has_https:
805 805 raise error.Abort(_('Python support for SSL and HTTPS '
806 806 'is not installed'))
807 807
808 808 inst = makepeer(ui, path)
809 809
810 810 return inst
811 811 except error.RepoError as httpexception:
812 812 try:
813 813 r = statichttprepo.instance(ui, "static-" + path, create)
814 814 ui.note(_('(falling back to static-http)\n'))
815 815 return r
816 816 except error.RepoError:
817 817 raise httpexception # use the original http RepoError instead
@@ -1,811 +1,811 b''
1 1 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
2 2 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 3 #
4 4 # This software may be used and distributed according to the terms of the
5 5 # GNU General Public License version 2 or any later version.
6 6
7 7 from __future__ import absolute_import
8 8
9 9 import contextlib
10 10 import struct
11 11 import sys
12 12 import threading
13 13
14 14 from .i18n import _
15 15 from .thirdparty import (
16 16 cbor,
17 17 )
18 18 from .thirdparty.zope import (
19 19 interface as zi,
20 20 )
21 21 from . import (
22 22 encoding,
23 23 error,
24 24 hook,
25 25 pycompat,
26 26 util,
27 27 wireproto,
28 28 wireprototypes,
29 29 wireprotov2server,
30 30 )
31 31 from .utils import (
32 32 procutil,
33 33 )
34 34
35 35 stringio = util.stringio
36 36
37 37 urlerr = util.urlerr
38 38 urlreq = util.urlreq
39 39
40 40 HTTP_OK = 200
41 41
42 42 HGTYPE = 'application/mercurial-0.1'
43 43 HGTYPE2 = 'application/mercurial-0.2'
44 44 HGERRTYPE = 'application/hg-error'
45 45
46 46 SSHV1 = wireprototypes.SSHV1
47 47 SSHV2 = wireprototypes.SSHV2
48 48
49 49 def decodevaluefromheaders(req, headerprefix):
50 50 """Decode a long value from multiple HTTP request headers.
51 51
52 52 Returns the value as a bytes, not a str.
53 53 """
54 54 chunks = []
55 55 i = 1
56 56 while True:
57 57 v = req.headers.get(b'%s-%d' % (headerprefix, i))
58 58 if v is None:
59 59 break
60 60 chunks.append(pycompat.bytesurl(v))
61 61 i += 1
62 62
63 63 return ''.join(chunks)
64 64
65 65 @zi.implementer(wireprototypes.baseprotocolhandler)
66 66 class httpv1protocolhandler(object):
67 67 def __init__(self, req, ui, checkperm):
68 68 self._req = req
69 69 self._ui = ui
70 70 self._checkperm = checkperm
71 71 self._protocaps = None
72 72
73 73 @property
74 74 def name(self):
75 75 return 'http-v1'
76 76
77 77 def getargs(self, args):
78 78 knownargs = self._args()
79 79 data = {}
80 80 keys = args.split()
81 81 for k in keys:
82 82 if k == '*':
83 83 star = {}
84 84 for key in knownargs.keys():
85 85 if key != 'cmd' and key not in keys:
86 86 star[key] = knownargs[key][0]
87 87 data['*'] = star
88 88 else:
89 89 data[k] = knownargs[k][0]
90 90 return [data[k] for k in keys]
91 91
92 92 def _args(self):
93 93 args = self._req.qsparams.asdictoflists()
94 94 postlen = int(self._req.headers.get(b'X-HgArgs-Post', 0))
95 95 if postlen:
96 96 args.update(urlreq.parseqs(
97 97 self._req.bodyfh.read(postlen), keep_blank_values=True))
98 98 return args
99 99
100 100 argvalue = decodevaluefromheaders(self._req, b'X-HgArg')
101 101 args.update(urlreq.parseqs(argvalue, keep_blank_values=True))
102 102 return args
103 103
104 104 def getprotocaps(self):
105 105 if self._protocaps is None:
106 106 value = decodevaluefromheaders(self._req, b'X-HgProto')
107 107 self._protocaps = set(value.split(' '))
108 108 return self._protocaps
109 109
110 110 def getpayload(self):
111 111 # Existing clients *always* send Content-Length.
112 112 length = int(self._req.headers[b'Content-Length'])
113 113
114 114 # If httppostargs is used, we need to read Content-Length
115 115 # minus the amount that was consumed by args.
116 116 length -= int(self._req.headers.get(b'X-HgArgs-Post', 0))
117 117 return util.filechunkiter(self._req.bodyfh, limit=length)
118 118
119 119 @contextlib.contextmanager
120 120 def mayberedirectstdio(self):
121 121 oldout = self._ui.fout
122 122 olderr = self._ui.ferr
123 123
124 124 out = util.stringio()
125 125
126 126 try:
127 127 self._ui.fout = out
128 128 self._ui.ferr = out
129 129 yield out
130 130 finally:
131 131 self._ui.fout = oldout
132 132 self._ui.ferr = olderr
133 133
134 134 def client(self):
135 135 return 'remote:%s:%s:%s' % (
136 136 self._req.urlscheme,
137 137 urlreq.quote(self._req.remotehost or ''),
138 138 urlreq.quote(self._req.remoteuser or ''))
139 139
140 140 def addcapabilities(self, repo, caps):
141 141 caps.append(b'batch')
142 142
143 143 caps.append('httpheader=%d' %
144 144 repo.ui.configint('server', 'maxhttpheaderlen'))
145 145 if repo.ui.configbool('experimental', 'httppostargs'):
146 146 caps.append('httppostargs')
147 147
148 148 # FUTURE advertise 0.2rx once support is implemented
149 149 # FUTURE advertise minrx and mintx after consulting config option
150 150 caps.append('httpmediatype=0.1rx,0.1tx,0.2tx')
151 151
152 152 compengines = wireproto.supportedcompengines(repo.ui, util.SERVERROLE)
153 153 if compengines:
154 154 comptypes = ','.join(urlreq.quote(e.wireprotosupport().name)
155 155 for e in compengines)
156 156 caps.append('compression=%s' % comptypes)
157 157
158 158 return caps
159 159
160 160 def checkperm(self, perm):
161 161 return self._checkperm(perm)
162 162
163 163 # This method exists mostly so that extensions like remotefilelog can
164 164 # disable a kludgey legacy method only over http. As of early 2018,
165 165 # there are no other known users, so with any luck we can discard this
166 166 # hook if remotefilelog becomes a first-party extension.
167 167 def iscmd(cmd):
168 168 return cmd in wireproto.commands
169 169
170 170 def handlewsgirequest(rctx, req, res, checkperm):
171 171 """Possibly process a wire protocol request.
172 172
173 173 If the current request is a wire protocol request, the request is
174 174 processed by this function.
175 175
176 176 ``req`` is a ``parsedrequest`` instance.
177 177 ``res`` is a ``wsgiresponse`` instance.
178 178
179 179 Returns a bool indicating if the request was serviced. If set, the caller
180 180 should stop processing the request, as a response has already been issued.
181 181 """
182 182 # Avoid cycle involving hg module.
183 183 from .hgweb import common as hgwebcommon
184 184
185 185 repo = rctx.repo
186 186
187 187 # HTTP version 1 wire protocol requests are denoted by a "cmd" query
188 188 # string parameter. If it isn't present, this isn't a wire protocol
189 189 # request.
190 190 if 'cmd' not in req.qsparams:
191 191 return False
192 192
193 193 cmd = req.qsparams['cmd']
194 194
195 195 # The "cmd" request parameter is used by both the wire protocol and hgweb.
196 196 # While not all wire protocol commands are available for all transports,
197 197 # if we see a "cmd" value that resembles a known wire protocol command, we
198 198 # route it to a protocol handler. This is better than routing possible
199 199 # wire protocol requests to hgweb because it prevents hgweb from using
200 200 # known wire protocol commands and it is less confusing for machine
201 201 # clients.
202 202 if not iscmd(cmd):
203 203 return False
204 204
205 205 # The "cmd" query string argument is only valid on the root path of the
206 206 # repo. e.g. ``/?cmd=foo``, ``/repo?cmd=foo``. URL paths within the repo
207 207 # like ``/blah?cmd=foo`` are not allowed. So don't recognize the request
208 208 # in this case. We send an HTTP 404 for backwards compatibility reasons.
209 209 if req.dispatchpath:
210 210 res.status = hgwebcommon.statusmessage(404)
211 211 res.headers['Content-Type'] = HGTYPE
212 212 # TODO This is not a good response to issue for this request. This
213 213 # is mostly for BC for now.
214 214 res.setbodybytes('0\n%s\n' % b'Not Found')
215 215 return True
216 216
217 217 proto = httpv1protocolhandler(req, repo.ui,
218 218 lambda perm: checkperm(rctx, req, perm))
219 219
220 220 # The permissions checker should be the only thing that can raise an
221 221 # ErrorResponse. It is kind of a layer violation to catch an hgweb
222 222 # exception here. So consider refactoring into a exception type that
223 223 # is associated with the wire protocol.
224 224 try:
225 225 _callhttp(repo, req, res, proto, cmd)
226 226 except hgwebcommon.ErrorResponse as e:
227 227 for k, v in e.headers:
228 228 res.headers[k] = v
229 229 res.status = hgwebcommon.statusmessage(e.code, pycompat.bytestr(e))
230 230 # TODO This response body assumes the failed command was
231 231 # "unbundle." That assumption is not always valid.
232 232 res.setbodybytes('0\n%s\n' % pycompat.bytestr(e))
233 233
234 234 return True
235 235
236 236 def _availableapis(repo):
237 237 apis = set()
238 238
239 239 # Registered APIs are made available via config options of the name of
240 240 # the protocol.
241 241 for k, v in API_HANDLERS.items():
242 242 section, option = v['config']
243 243 if repo.ui.configbool(section, option):
244 244 apis.add(k)
245 245
246 246 return apis
247 247
248 248 def handlewsgiapirequest(rctx, req, res, checkperm):
249 249 """Handle requests to /api/*."""
250 250 assert req.dispatchparts[0] == b'api'
251 251
252 252 repo = rctx.repo
253 253
254 254 # This whole URL space is experimental for now. But we want to
255 255 # reserve the URL space. So, 404 all URLs if the feature isn't enabled.
256 256 if not repo.ui.configbool('experimental', 'web.apiserver'):
257 257 res.status = b'404 Not Found'
258 258 res.headers[b'Content-Type'] = b'text/plain'
259 259 res.setbodybytes(_('Experimental API server endpoint not enabled'))
260 260 return
261 261
262 262 # The URL space is /api/<protocol>/*. The structure of URLs under varies
263 263 # by <protocol>.
264 264
265 265 availableapis = _availableapis(repo)
266 266
267 267 # Requests to /api/ list available APIs.
268 268 if req.dispatchparts == [b'api']:
269 269 res.status = b'200 OK'
270 270 res.headers[b'Content-Type'] = b'text/plain'
271 271 lines = [_('APIs can be accessed at /api/<name>, where <name> can be '
272 272 'one of the following:\n')]
273 273 if availableapis:
274 274 lines.extend(sorted(availableapis))
275 275 else:
276 276 lines.append(_('(no available APIs)\n'))
277 277 res.setbodybytes(b'\n'.join(lines))
278 278 return
279 279
280 280 proto = req.dispatchparts[1]
281 281
282 282 if proto not in API_HANDLERS:
283 283 res.status = b'404 Not Found'
284 284 res.headers[b'Content-Type'] = b'text/plain'
285 285 res.setbodybytes(_('Unknown API: %s\nKnown APIs: %s') % (
286 286 proto, b', '.join(sorted(availableapis))))
287 287 return
288 288
289 289 if proto not in availableapis:
290 290 res.status = b'404 Not Found'
291 291 res.headers[b'Content-Type'] = b'text/plain'
292 292 res.setbodybytes(_('API %s not enabled\n') % proto)
293 293 return
294 294
295 295 API_HANDLERS[proto]['handler'](rctx, req, res, checkperm,
296 296 req.dispatchparts[2:])
297 297
298 298 # Maps API name to metadata so custom API can be registered.
299 299 # Keys are:
300 300 #
301 301 # config
302 302 # Config option that controls whether service is enabled.
303 303 # handler
304 304 # Callable receiving (rctx, req, res, checkperm, urlparts) that is called
305 305 # when a request to this API is received.
306 306 # apidescriptor
307 307 # Callable receiving (req, repo) that is called to obtain an API
308 308 # descriptor for this service. The response must be serializable to CBOR.
309 309 API_HANDLERS = {
310 wireprotov2server.HTTPV2: {
310 wireprotov2server.HTTP_WIREPROTO_V2: {
311 311 'config': ('experimental', 'web.api.http-v2'),
312 312 'handler': wireprotov2server.handlehttpv2request,
313 313 'apidescriptor': wireprotov2server.httpv2apidescriptor,
314 314 },
315 315 }
316 316
317 317 def _httpresponsetype(ui, proto, prefer_uncompressed):
318 318 """Determine the appropriate response type and compression settings.
319 319
320 320 Returns a tuple of (mediatype, compengine, engineopts).
321 321 """
322 322 # Determine the response media type and compression engine based
323 323 # on the request parameters.
324 324
325 325 if '0.2' in proto.getprotocaps():
326 326 # All clients are expected to support uncompressed data.
327 327 if prefer_uncompressed:
328 328 return HGTYPE2, util._noopengine(), {}
329 329
330 330 # Now find an agreed upon compression format.
331 331 compformats = wireproto.clientcompressionsupport(proto)
332 332 for engine in wireproto.supportedcompengines(ui, util.SERVERROLE):
333 333 if engine.wireprotosupport().name in compformats:
334 334 opts = {}
335 335 level = ui.configint('server', '%slevel' % engine.name())
336 336 if level is not None:
337 337 opts['level'] = level
338 338
339 339 return HGTYPE2, engine, opts
340 340
341 341 # No mutually supported compression format. Fall back to the
342 342 # legacy protocol.
343 343
344 344 # Don't allow untrusted settings because disabling compression or
345 345 # setting a very high compression level could lead to flooding
346 346 # the server's network or CPU.
347 347 opts = {'level': ui.configint('server', 'zliblevel')}
348 348 return HGTYPE, util.compengines['zlib'], opts
349 349
350 350 def processcapabilitieshandshake(repo, req, res, proto):
351 351 """Called during a ?cmd=capabilities request.
352 352
353 353 If the client is advertising support for a newer protocol, we send
354 354 a CBOR response with information about available services. If no
355 355 advertised services are available, we don't handle the request.
356 356 """
357 357 # Fall back to old behavior unless the API server is enabled.
358 358 if not repo.ui.configbool('experimental', 'web.apiserver'):
359 359 return False
360 360
361 361 clientapis = decodevaluefromheaders(req, b'X-HgUpgrade')
362 362 protocaps = decodevaluefromheaders(req, b'X-HgProto')
363 363 if not clientapis or not protocaps:
364 364 return False
365 365
366 366 # We currently only support CBOR responses.
367 367 protocaps = set(protocaps.split(' '))
368 368 if b'cbor' not in protocaps:
369 369 return False
370 370
371 371 descriptors = {}
372 372
373 373 for api in sorted(set(clientapis.split()) & _availableapis(repo)):
374 374 handler = API_HANDLERS[api]
375 375
376 376 descriptorfn = handler.get('apidescriptor')
377 377 if not descriptorfn:
378 378 continue
379 379
380 380 descriptors[api] = descriptorfn(req, repo)
381 381
382 382 v1caps = wireproto.dispatch(repo, proto, 'capabilities')
383 383 assert isinstance(v1caps, wireprototypes.bytesresponse)
384 384
385 385 m = {
386 386 # TODO allow this to be configurable.
387 387 'apibase': 'api/',
388 388 'apis': descriptors,
389 389 'v1capabilities': v1caps.data,
390 390 }
391 391
392 392 res.status = b'200 OK'
393 393 res.headers[b'Content-Type'] = b'application/mercurial-cbor'
394 394 res.setbodybytes(cbor.dumps(m, canonical=True))
395 395
396 396 return True
397 397
398 398 def _callhttp(repo, req, res, proto, cmd):
399 399 # Avoid cycle involving hg module.
400 400 from .hgweb import common as hgwebcommon
401 401
402 402 def genversion2(gen, engine, engineopts):
403 403 # application/mercurial-0.2 always sends a payload header
404 404 # identifying the compression engine.
405 405 name = engine.wireprotosupport().name
406 406 assert 0 < len(name) < 256
407 407 yield struct.pack('B', len(name))
408 408 yield name
409 409
410 410 for chunk in gen:
411 411 yield chunk
412 412
413 413 def setresponse(code, contenttype, bodybytes=None, bodygen=None):
414 414 if code == HTTP_OK:
415 415 res.status = '200 Script output follows'
416 416 else:
417 417 res.status = hgwebcommon.statusmessage(code)
418 418
419 419 res.headers['Content-Type'] = contenttype
420 420
421 421 if bodybytes is not None:
422 422 res.setbodybytes(bodybytes)
423 423 if bodygen is not None:
424 424 res.setbodygen(bodygen)
425 425
426 426 if not wireproto.commands.commandavailable(cmd, proto):
427 427 setresponse(HTTP_OK, HGERRTYPE,
428 428 _('requested wire protocol command is not available over '
429 429 'HTTP'))
430 430 return
431 431
432 432 proto.checkperm(wireproto.commands[cmd].permission)
433 433
434 434 # Possibly handle a modern client wanting to switch protocols.
435 435 if (cmd == 'capabilities' and
436 436 processcapabilitieshandshake(repo, req, res, proto)):
437 437
438 438 return
439 439
440 440 rsp = wireproto.dispatch(repo, proto, cmd)
441 441
442 442 if isinstance(rsp, bytes):
443 443 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp)
444 444 elif isinstance(rsp, wireprototypes.bytesresponse):
445 445 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp.data)
446 446 elif isinstance(rsp, wireprototypes.streamreslegacy):
447 447 setresponse(HTTP_OK, HGTYPE, bodygen=rsp.gen)
448 448 elif isinstance(rsp, wireprototypes.streamres):
449 449 gen = rsp.gen
450 450
451 451 # This code for compression should not be streamres specific. It
452 452 # is here because we only compress streamres at the moment.
453 453 mediatype, engine, engineopts = _httpresponsetype(
454 454 repo.ui, proto, rsp.prefer_uncompressed)
455 455 gen = engine.compressstream(gen, engineopts)
456 456
457 457 if mediatype == HGTYPE2:
458 458 gen = genversion2(gen, engine, engineopts)
459 459
460 460 setresponse(HTTP_OK, mediatype, bodygen=gen)
461 461 elif isinstance(rsp, wireprototypes.pushres):
462 462 rsp = '%d\n%s' % (rsp.res, rsp.output)
463 463 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp)
464 464 elif isinstance(rsp, wireprototypes.pusherr):
465 465 rsp = '0\n%s\n' % rsp.res
466 466 res.drain = True
467 467 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp)
468 468 elif isinstance(rsp, wireprototypes.ooberror):
469 469 setresponse(HTTP_OK, HGERRTYPE, bodybytes=rsp.message)
470 470 else:
471 471 raise error.ProgrammingError('hgweb.protocol internal failure', rsp)
472 472
473 473 def _sshv1respondbytes(fout, value):
474 474 """Send a bytes response for protocol version 1."""
475 475 fout.write('%d\n' % len(value))
476 476 fout.write(value)
477 477 fout.flush()
478 478
479 479 def _sshv1respondstream(fout, source):
480 480 write = fout.write
481 481 for chunk in source.gen:
482 482 write(chunk)
483 483 fout.flush()
484 484
485 485 def _sshv1respondooberror(fout, ferr, rsp):
486 486 ferr.write(b'%s\n-\n' % rsp)
487 487 ferr.flush()
488 488 fout.write(b'\n')
489 489 fout.flush()
490 490
491 491 @zi.implementer(wireprototypes.baseprotocolhandler)
492 492 class sshv1protocolhandler(object):
493 493 """Handler for requests services via version 1 of SSH protocol."""
494 494 def __init__(self, ui, fin, fout):
495 495 self._ui = ui
496 496 self._fin = fin
497 497 self._fout = fout
498 498 self._protocaps = set()
499 499
500 500 @property
501 501 def name(self):
502 502 return wireprototypes.SSHV1
503 503
504 504 def getargs(self, args):
505 505 data = {}
506 506 keys = args.split()
507 507 for n in xrange(len(keys)):
508 508 argline = self._fin.readline()[:-1]
509 509 arg, l = argline.split()
510 510 if arg not in keys:
511 511 raise error.Abort(_("unexpected parameter %r") % arg)
512 512 if arg == '*':
513 513 star = {}
514 514 for k in xrange(int(l)):
515 515 argline = self._fin.readline()[:-1]
516 516 arg, l = argline.split()
517 517 val = self._fin.read(int(l))
518 518 star[arg] = val
519 519 data['*'] = star
520 520 else:
521 521 val = self._fin.read(int(l))
522 522 data[arg] = val
523 523 return [data[k] for k in keys]
524 524
525 525 def getprotocaps(self):
526 526 return self._protocaps
527 527
528 528 def getpayload(self):
529 529 # We initially send an empty response. This tells the client it is
530 530 # OK to start sending data. If a client sees any other response, it
531 531 # interprets it as an error.
532 532 _sshv1respondbytes(self._fout, b'')
533 533
534 534 # The file is in the form:
535 535 #
536 536 # <chunk size>\n<chunk>
537 537 # ...
538 538 # 0\n
539 539 count = int(self._fin.readline())
540 540 while count:
541 541 yield self._fin.read(count)
542 542 count = int(self._fin.readline())
543 543
544 544 @contextlib.contextmanager
545 545 def mayberedirectstdio(self):
546 546 yield None
547 547
548 548 def client(self):
549 549 client = encoding.environ.get('SSH_CLIENT', '').split(' ', 1)[0]
550 550 return 'remote:ssh:' + client
551 551
552 552 def addcapabilities(self, repo, caps):
553 553 if self.name == wireprototypes.SSHV1:
554 554 caps.append(b'protocaps')
555 555 caps.append(b'batch')
556 556 return caps
557 557
558 558 def checkperm(self, perm):
559 559 pass
560 560
561 561 class sshv2protocolhandler(sshv1protocolhandler):
562 562 """Protocol handler for version 2 of the SSH protocol."""
563 563
564 564 @property
565 565 def name(self):
566 566 return wireprototypes.SSHV2
567 567
568 568 def addcapabilities(self, repo, caps):
569 569 return caps
570 570
571 571 def _runsshserver(ui, repo, fin, fout, ev):
572 572 # This function operates like a state machine of sorts. The following
573 573 # states are defined:
574 574 #
575 575 # protov1-serving
576 576 # Server is in protocol version 1 serving mode. Commands arrive on
577 577 # new lines. These commands are processed in this state, one command
578 578 # after the other.
579 579 #
580 580 # protov2-serving
581 581 # Server is in protocol version 2 serving mode.
582 582 #
583 583 # upgrade-initial
584 584 # The server is going to process an upgrade request.
585 585 #
586 586 # upgrade-v2-filter-legacy-handshake
587 587 # The protocol is being upgraded to version 2. The server is expecting
588 588 # the legacy handshake from version 1.
589 589 #
590 590 # upgrade-v2-finish
591 591 # The upgrade to version 2 of the protocol is imminent.
592 592 #
593 593 # shutdown
594 594 # The server is shutting down, possibly in reaction to a client event.
595 595 #
596 596 # And here are their transitions:
597 597 #
598 598 # protov1-serving -> shutdown
599 599 # When server receives an empty request or encounters another
600 600 # error.
601 601 #
602 602 # protov1-serving -> upgrade-initial
603 603 # An upgrade request line was seen.
604 604 #
605 605 # upgrade-initial -> upgrade-v2-filter-legacy-handshake
606 606 # Upgrade to version 2 in progress. Server is expecting to
607 607 # process a legacy handshake.
608 608 #
609 609 # upgrade-v2-filter-legacy-handshake -> shutdown
610 610 # Client did not fulfill upgrade handshake requirements.
611 611 #
612 612 # upgrade-v2-filter-legacy-handshake -> upgrade-v2-finish
613 613 # Client fulfilled version 2 upgrade requirements. Finishing that
614 614 # upgrade.
615 615 #
616 616 # upgrade-v2-finish -> protov2-serving
617 617 # Protocol upgrade to version 2 complete. Server can now speak protocol
618 618 # version 2.
619 619 #
620 620 # protov2-serving -> protov1-serving
621 621 # Ths happens by default since protocol version 2 is the same as
622 622 # version 1 except for the handshake.
623 623
624 624 state = 'protov1-serving'
625 625 proto = sshv1protocolhandler(ui, fin, fout)
626 626 protoswitched = False
627 627
628 628 while not ev.is_set():
629 629 if state == 'protov1-serving':
630 630 # Commands are issued on new lines.
631 631 request = fin.readline()[:-1]
632 632
633 633 # Empty lines signal to terminate the connection.
634 634 if not request:
635 635 state = 'shutdown'
636 636 continue
637 637
638 638 # It looks like a protocol upgrade request. Transition state to
639 639 # handle it.
640 640 if request.startswith(b'upgrade '):
641 641 if protoswitched:
642 642 _sshv1respondooberror(fout, ui.ferr,
643 643 b'cannot upgrade protocols multiple '
644 644 b'times')
645 645 state = 'shutdown'
646 646 continue
647 647
648 648 state = 'upgrade-initial'
649 649 continue
650 650
651 651 available = wireproto.commands.commandavailable(request, proto)
652 652
653 653 # This command isn't available. Send an empty response and go
654 654 # back to waiting for a new command.
655 655 if not available:
656 656 _sshv1respondbytes(fout, b'')
657 657 continue
658 658
659 659 rsp = wireproto.dispatch(repo, proto, request)
660 660
661 661 if isinstance(rsp, bytes):
662 662 _sshv1respondbytes(fout, rsp)
663 663 elif isinstance(rsp, wireprototypes.bytesresponse):
664 664 _sshv1respondbytes(fout, rsp.data)
665 665 elif isinstance(rsp, wireprototypes.streamres):
666 666 _sshv1respondstream(fout, rsp)
667 667 elif isinstance(rsp, wireprototypes.streamreslegacy):
668 668 _sshv1respondstream(fout, rsp)
669 669 elif isinstance(rsp, wireprototypes.pushres):
670 670 _sshv1respondbytes(fout, b'')
671 671 _sshv1respondbytes(fout, b'%d' % rsp.res)
672 672 elif isinstance(rsp, wireprototypes.pusherr):
673 673 _sshv1respondbytes(fout, rsp.res)
674 674 elif isinstance(rsp, wireprototypes.ooberror):
675 675 _sshv1respondooberror(fout, ui.ferr, rsp.message)
676 676 else:
677 677 raise error.ProgrammingError('unhandled response type from '
678 678 'wire protocol command: %s' % rsp)
679 679
680 680 # For now, protocol version 2 serving just goes back to version 1.
681 681 elif state == 'protov2-serving':
682 682 state = 'protov1-serving'
683 683 continue
684 684
685 685 elif state == 'upgrade-initial':
686 686 # We should never transition into this state if we've switched
687 687 # protocols.
688 688 assert not protoswitched
689 689 assert proto.name == wireprototypes.SSHV1
690 690
691 691 # Expected: upgrade <token> <capabilities>
692 692 # If we get something else, the request is malformed. It could be
693 693 # from a future client that has altered the upgrade line content.
694 694 # We treat this as an unknown command.
695 695 try:
696 696 token, caps = request.split(b' ')[1:]
697 697 except ValueError:
698 698 _sshv1respondbytes(fout, b'')
699 699 state = 'protov1-serving'
700 700 continue
701 701
702 702 # Send empty response if we don't support upgrading protocols.
703 703 if not ui.configbool('experimental', 'sshserver.support-v2'):
704 704 _sshv1respondbytes(fout, b'')
705 705 state = 'protov1-serving'
706 706 continue
707 707
708 708 try:
709 709 caps = urlreq.parseqs(caps)
710 710 except ValueError:
711 711 _sshv1respondbytes(fout, b'')
712 712 state = 'protov1-serving'
713 713 continue
714 714
715 715 # We don't see an upgrade request to protocol version 2. Ignore
716 716 # the upgrade request.
717 717 wantedprotos = caps.get(b'proto', [b''])[0]
718 718 if SSHV2 not in wantedprotos:
719 719 _sshv1respondbytes(fout, b'')
720 720 state = 'protov1-serving'
721 721 continue
722 722
723 723 # It looks like we can honor this upgrade request to protocol 2.
724 724 # Filter the rest of the handshake protocol request lines.
725 725 state = 'upgrade-v2-filter-legacy-handshake'
726 726 continue
727 727
728 728 elif state == 'upgrade-v2-filter-legacy-handshake':
729 729 # Client should have sent legacy handshake after an ``upgrade``
730 730 # request. Expected lines:
731 731 #
732 732 # hello
733 733 # between
734 734 # pairs 81
735 735 # 0000...-0000...
736 736
737 737 ok = True
738 738 for line in (b'hello', b'between', b'pairs 81'):
739 739 request = fin.readline()[:-1]
740 740
741 741 if request != line:
742 742 _sshv1respondooberror(fout, ui.ferr,
743 743 b'malformed handshake protocol: '
744 744 b'missing %s' % line)
745 745 ok = False
746 746 state = 'shutdown'
747 747 break
748 748
749 749 if not ok:
750 750 continue
751 751
752 752 request = fin.read(81)
753 753 if request != b'%s-%s' % (b'0' * 40, b'0' * 40):
754 754 _sshv1respondooberror(fout, ui.ferr,
755 755 b'malformed handshake protocol: '
756 756 b'missing between argument value')
757 757 state = 'shutdown'
758 758 continue
759 759
760 760 state = 'upgrade-v2-finish'
761 761 continue
762 762
763 763 elif state == 'upgrade-v2-finish':
764 764 # Send the upgrade response.
765 765 fout.write(b'upgraded %s %s\n' % (token, SSHV2))
766 766 servercaps = wireproto.capabilities(repo, proto)
767 767 rsp = b'capabilities: %s' % servercaps.data
768 768 fout.write(b'%d\n%s\n' % (len(rsp), rsp))
769 769 fout.flush()
770 770
771 771 proto = sshv2protocolhandler(ui, fin, fout)
772 772 protoswitched = True
773 773
774 774 state = 'protov2-serving'
775 775 continue
776 776
777 777 elif state == 'shutdown':
778 778 break
779 779
780 780 else:
781 781 raise error.ProgrammingError('unhandled ssh server state: %s' %
782 782 state)
783 783
784 784 class sshserver(object):
785 785 def __init__(self, ui, repo, logfh=None):
786 786 self._ui = ui
787 787 self._repo = repo
788 788 self._fin = ui.fin
789 789 self._fout = ui.fout
790 790
791 791 # Log write I/O to stdout and stderr if configured.
792 792 if logfh:
793 793 self._fout = util.makeloggingfileobject(
794 794 logfh, self._fout, 'o', logdata=True)
795 795 ui.ferr = util.makeloggingfileobject(
796 796 logfh, ui.ferr, 'e', logdata=True)
797 797
798 798 hook.redirect(True)
799 799 ui.fout = repo.ui.fout = ui.ferr
800 800
801 801 # Prevent insertion/deletion of CRs
802 802 procutil.setbinary(self._fin)
803 803 procutil.setbinary(self._fout)
804 804
805 805 def serve_forever(self):
806 806 self.serveuntil(threading.Event())
807 807 sys.exit(0)
808 808
809 809 def serveuntil(self, ev):
810 810 """Serve until a threading.Event is set."""
811 811 _runsshserver(self._ui, self._repo, self._fin, self._fout, ev)
@@ -1,227 +1,227 b''
1 1 # Copyright 2018 Gregory Szorc <gregory.szorc@gmail.com>
2 2 #
3 3 # This software may be used and distributed according to the terms of the
4 4 # GNU General Public License version 2 or any later version.
5 5
6 6 from __future__ import absolute_import
7 7
8 8 from .node import (
9 9 bin,
10 10 hex,
11 11 )
12 12 from .thirdparty.zope import (
13 13 interface as zi,
14 14 )
15 15
16 16 # Names of the SSH protocol implementations.
17 17 SSHV1 = 'ssh-v1'
18 18 # These are advertised over the wire. Increment the counters at the end
19 19 # to reflect BC breakages.
20 20 SSHV2 = 'exp-ssh-v2-0001'
21 HTTPV2 = 'exp-http-v2-0001'
21 HTTP_WIREPROTO_V2 = 'exp-http-v2-0001'
22 22
23 23 # All available wire protocol transports.
24 24 TRANSPORTS = {
25 25 SSHV1: {
26 26 'transport': 'ssh',
27 27 'version': 1,
28 28 },
29 29 SSHV2: {
30 30 'transport': 'ssh',
31 31 # TODO mark as version 2 once all commands are implemented.
32 32 'version': 1,
33 33 },
34 34 'http-v1': {
35 35 'transport': 'http',
36 36 'version': 1,
37 37 },
38 HTTPV2: {
38 HTTP_WIREPROTO_V2: {
39 39 'transport': 'http',
40 40 'version': 2,
41 41 }
42 42 }
43 43
44 44 class bytesresponse(object):
45 45 """A wire protocol response consisting of raw bytes."""
46 46 def __init__(self, data):
47 47 self.data = data
48 48
49 49 class ooberror(object):
50 50 """wireproto reply: failure of a batch of operation
51 51
52 52 Something failed during a batch call. The error message is stored in
53 53 `self.message`.
54 54 """
55 55 def __init__(self, message):
56 56 self.message = message
57 57
58 58 class pushres(object):
59 59 """wireproto reply: success with simple integer return
60 60
61 61 The call was successful and returned an integer contained in `self.res`.
62 62 """
63 63 def __init__(self, res, output):
64 64 self.res = res
65 65 self.output = output
66 66
67 67 class pusherr(object):
68 68 """wireproto reply: failure
69 69
70 70 The call failed. The `self.res` attribute contains the error message.
71 71 """
72 72 def __init__(self, res, output):
73 73 self.res = res
74 74 self.output = output
75 75
76 76 class streamres(object):
77 77 """wireproto reply: binary stream
78 78
79 79 The call was successful and the result is a stream.
80 80
81 81 Accepts a generator containing chunks of data to be sent to the client.
82 82
83 83 ``prefer_uncompressed`` indicates that the data is expected to be
84 84 uncompressable and that the stream should therefore use the ``none``
85 85 engine.
86 86 """
87 87 def __init__(self, gen=None, prefer_uncompressed=False):
88 88 self.gen = gen
89 89 self.prefer_uncompressed = prefer_uncompressed
90 90
91 91 class streamreslegacy(object):
92 92 """wireproto reply: uncompressed binary stream
93 93
94 94 The call was successful and the result is a stream.
95 95
96 96 Accepts a generator containing chunks of data to be sent to the client.
97 97
98 98 Like ``streamres``, but sends an uncompressed data for "version 1" clients
99 99 using the application/mercurial-0.1 media type.
100 100 """
101 101 def __init__(self, gen=None):
102 102 self.gen = gen
103 103
104 104 class cborresponse(object):
105 105 """Encode the response value as CBOR."""
106 106 def __init__(self, v):
107 107 self.value = v
108 108
109 109 # list of nodes encoding / decoding
110 110 def decodelist(l, sep=' '):
111 111 if l:
112 112 return [bin(v) for v in l.split(sep)]
113 113 return []
114 114
115 115 def encodelist(l, sep=' '):
116 116 try:
117 117 return sep.join(map(hex, l))
118 118 except TypeError:
119 119 raise
120 120
121 121 # batched call argument encoding
122 122
123 123 def escapebatcharg(plain):
124 124 return (plain
125 125 .replace(':', ':c')
126 126 .replace(',', ':o')
127 127 .replace(';', ':s')
128 128 .replace('=', ':e'))
129 129
130 130 def unescapebatcharg(escaped):
131 131 return (escaped
132 132 .replace(':e', '=')
133 133 .replace(':s', ';')
134 134 .replace(':o', ',')
135 135 .replace(':c', ':'))
136 136
137 137 # mapping of options accepted by getbundle and their types
138 138 #
139 139 # Meant to be extended by extensions. It is extensions responsibility to ensure
140 140 # such options are properly processed in exchange.getbundle.
141 141 #
142 142 # supported types are:
143 143 #
144 144 # :nodes: list of binary nodes
145 145 # :csv: list of comma-separated values
146 146 # :scsv: list of comma-separated values return as set
147 147 # :plain: string with no transformation needed.
148 148 GETBUNDLE_ARGUMENTS = {
149 149 'heads': 'nodes',
150 150 'bookmarks': 'boolean',
151 151 'common': 'nodes',
152 152 'obsmarkers': 'boolean',
153 153 'phases': 'boolean',
154 154 'bundlecaps': 'scsv',
155 155 'listkeys': 'csv',
156 156 'cg': 'boolean',
157 157 'cbattempted': 'boolean',
158 158 'stream': 'boolean',
159 159 }
160 160
161 161 class baseprotocolhandler(zi.Interface):
162 162 """Abstract base class for wire protocol handlers.
163 163
164 164 A wire protocol handler serves as an interface between protocol command
165 165 handlers and the wire protocol transport layer. Protocol handlers provide
166 166 methods to read command arguments, redirect stdio for the duration of
167 167 the request, handle response types, etc.
168 168 """
169 169
170 170 name = zi.Attribute(
171 171 """The name of the protocol implementation.
172 172
173 173 Used for uniquely identifying the transport type.
174 174 """)
175 175
176 176 def getargs(args):
177 177 """return the value for arguments in <args>
178 178
179 179 For version 1 transports, returns a list of values in the same
180 180 order they appear in ``args``. For version 2 transports, returns
181 181 a dict mapping argument name to value.
182 182 """
183 183
184 184 def getprotocaps():
185 185 """Returns the list of protocol-level capabilities of client
186 186
187 187 Returns a list of capabilities as declared by the client for
188 188 the current request (or connection for stateful protocol handlers)."""
189 189
190 190 def getpayload():
191 191 """Provide a generator for the raw payload.
192 192
193 193 The caller is responsible for ensuring that the full payload is
194 194 processed.
195 195 """
196 196
197 197 def mayberedirectstdio():
198 198 """Context manager to possibly redirect stdio.
199 199
200 200 The context manager yields a file-object like object that receives
201 201 stdout and stderr output when the context manager is active. Or it
202 202 yields ``None`` if no I/O redirection occurs.
203 203
204 204 The intent of this context manager is to capture stdio output
205 205 so it may be sent in the response. Some transports support streaming
206 206 stdio to the client in real time. For these transports, stdio output
207 207 won't be captured.
208 208 """
209 209
210 210 def client():
211 211 """Returns a string representation of this client (as bytes)."""
212 212
213 213 def addcapabilities(repo, caps):
214 214 """Adds advertised capabilities specific to this protocol.
215 215
216 216 Receives the list of capabilities collected so far.
217 217
218 218 Returns a list of capabilities. The passed in argument can be returned.
219 219 """
220 220
221 221 def checkperm(perm):
222 222 """Validate that the client has permissions to perform a request.
223 223
224 224 The argument is the permission required to proceed. If the client
225 225 doesn't have that permission, the exception should raise or abort
226 226 in a protocol specific manner.
227 227 """
@@ -1,479 +1,479 b''
1 1 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
2 2 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 3 #
4 4 # This software may be used and distributed according to the terms of the
5 5 # GNU General Public License version 2 or any later version.
6 6
7 7 from __future__ import absolute_import
8 8
9 9 import contextlib
10 10
11 11 from .i18n import _
12 12 from .thirdparty import (
13 13 cbor,
14 14 )
15 15 from .thirdparty.zope import (
16 16 interface as zi,
17 17 )
18 18 from . import (
19 19 encoding,
20 20 error,
21 21 pycompat,
22 22 util,
23 23 wireproto,
24 24 wireprotoframing,
25 25 wireprototypes,
26 26 )
27 27
28 28 FRAMINGTYPE = b'application/mercurial-exp-framing-0003'
29 29
30 HTTPV2 = wireprototypes.HTTPV2
30 HTTP_WIREPROTO_V2 = wireprototypes.HTTP_WIREPROTO_V2
31 31
32 32 def handlehttpv2request(rctx, req, res, checkperm, urlparts):
33 33 from .hgweb import common as hgwebcommon
34 34
35 35 # URL space looks like: <permissions>/<command>, where <permission> can
36 36 # be ``ro`` or ``rw`` to signal read-only or read-write, respectively.
37 37
38 38 # Root URL does nothing meaningful... yet.
39 39 if not urlparts:
40 40 res.status = b'200 OK'
41 41 res.headers[b'Content-Type'] = b'text/plain'
42 42 res.setbodybytes(_('HTTP version 2 API handler'))
43 43 return
44 44
45 45 if len(urlparts) == 1:
46 46 res.status = b'404 Not Found'
47 47 res.headers[b'Content-Type'] = b'text/plain'
48 48 res.setbodybytes(_('do not know how to process %s\n') %
49 49 req.dispatchpath)
50 50 return
51 51
52 52 permission, command = urlparts[0:2]
53 53
54 54 if permission not in (b'ro', b'rw'):
55 55 res.status = b'404 Not Found'
56 56 res.headers[b'Content-Type'] = b'text/plain'
57 57 res.setbodybytes(_('unknown permission: %s') % permission)
58 58 return
59 59
60 60 if req.method != 'POST':
61 61 res.status = b'405 Method Not Allowed'
62 62 res.headers[b'Allow'] = b'POST'
63 63 res.setbodybytes(_('commands require POST requests'))
64 64 return
65 65
66 66 # At some point we'll want to use our own API instead of recycling the
67 67 # behavior of version 1 of the wire protocol...
68 68 # TODO return reasonable responses - not responses that overload the
69 69 # HTTP status line message for error reporting.
70 70 try:
71 71 checkperm(rctx, req, 'pull' if permission == b'ro' else 'push')
72 72 except hgwebcommon.ErrorResponse as e:
73 73 res.status = hgwebcommon.statusmessage(e.code, pycompat.bytestr(e))
74 74 for k, v in e.headers:
75 75 res.headers[k] = v
76 76 res.setbodybytes('permission denied')
77 77 return
78 78
79 79 # We have a special endpoint to reflect the request back at the client.
80 80 if command == b'debugreflect':
81 81 _processhttpv2reflectrequest(rctx.repo.ui, rctx.repo, req, res)
82 82 return
83 83
84 84 # Extra commands that we handle that aren't really wire protocol
85 85 # commands. Think extra hard before making this hackery available to
86 86 # extension.
87 87 extracommands = {'multirequest'}
88 88
89 89 if command not in wireproto.commandsv2 and command not in extracommands:
90 90 res.status = b'404 Not Found'
91 91 res.headers[b'Content-Type'] = b'text/plain'
92 92 res.setbodybytes(_('unknown wire protocol command: %s\n') % command)
93 93 return
94 94
95 95 repo = rctx.repo
96 96 ui = repo.ui
97 97
98 98 proto = httpv2protocolhandler(req, ui)
99 99
100 100 if (not wireproto.commandsv2.commandavailable(command, proto)
101 101 and command not in extracommands):
102 102 res.status = b'404 Not Found'
103 103 res.headers[b'Content-Type'] = b'text/plain'
104 104 res.setbodybytes(_('invalid wire protocol command: %s') % command)
105 105 return
106 106
107 107 # TODO consider cases where proxies may add additional Accept headers.
108 108 if req.headers.get(b'Accept') != FRAMINGTYPE:
109 109 res.status = b'406 Not Acceptable'
110 110 res.headers[b'Content-Type'] = b'text/plain'
111 111 res.setbodybytes(_('client MUST specify Accept header with value: %s\n')
112 112 % FRAMINGTYPE)
113 113 return
114 114
115 115 if req.headers.get(b'Content-Type') != FRAMINGTYPE:
116 116 res.status = b'415 Unsupported Media Type'
117 117 # TODO we should send a response with appropriate media type,
118 118 # since client does Accept it.
119 119 res.headers[b'Content-Type'] = b'text/plain'
120 120 res.setbodybytes(_('client MUST send Content-Type header with '
121 121 'value: %s\n') % FRAMINGTYPE)
122 122 return
123 123
124 124 _processhttpv2request(ui, repo, req, res, permission, command, proto)
125 125
126 126 def _processhttpv2reflectrequest(ui, repo, req, res):
127 127 """Reads unified frame protocol request and dumps out state to client.
128 128
129 129 This special endpoint can be used to help debug the wire protocol.
130 130
131 131 Instead of routing the request through the normal dispatch mechanism,
132 132 we instead read all frames, decode them, and feed them into our state
133 133 tracker. We then dump the log of all that activity back out to the
134 134 client.
135 135 """
136 136 import json
137 137
138 138 # Reflection APIs have a history of being abused, accidentally disclosing
139 139 # sensitive data, etc. So we have a config knob.
140 140 if not ui.configbool('experimental', 'web.api.debugreflect'):
141 141 res.status = b'404 Not Found'
142 142 res.headers[b'Content-Type'] = b'text/plain'
143 143 res.setbodybytes(_('debugreflect service not available'))
144 144 return
145 145
146 146 # We assume we have a unified framing protocol request body.
147 147
148 148 reactor = wireprotoframing.serverreactor()
149 149 states = []
150 150
151 151 while True:
152 152 frame = wireprotoframing.readframe(req.bodyfh)
153 153
154 154 if not frame:
155 155 states.append(b'received: <no frame>')
156 156 break
157 157
158 158 states.append(b'received: %d %d %d %s' % (frame.typeid, frame.flags,
159 159 frame.requestid,
160 160 frame.payload))
161 161
162 162 action, meta = reactor.onframerecv(frame)
163 163 states.append(json.dumps((action, meta), sort_keys=True,
164 164 separators=(', ', ': ')))
165 165
166 166 action, meta = reactor.oninputeof()
167 167 meta['action'] = action
168 168 states.append(json.dumps(meta, sort_keys=True, separators=(', ',': ')))
169 169
170 170 res.status = b'200 OK'
171 171 res.headers[b'Content-Type'] = b'text/plain'
172 172 res.setbodybytes(b'\n'.join(states))
173 173
174 174 def _processhttpv2request(ui, repo, req, res, authedperm, reqcommand, proto):
175 175 """Post-validation handler for HTTPv2 requests.
176 176
177 177 Called when the HTTP request contains unified frame-based protocol
178 178 frames for evaluation.
179 179 """
180 180 # TODO Some HTTP clients are full duplex and can receive data before
181 181 # the entire request is transmitted. Figure out a way to indicate support
182 182 # for that so we can opt into full duplex mode.
183 183 reactor = wireprotoframing.serverreactor(deferoutput=True)
184 184 seencommand = False
185 185
186 186 outstream = reactor.makeoutputstream()
187 187
188 188 while True:
189 189 frame = wireprotoframing.readframe(req.bodyfh)
190 190 if not frame:
191 191 break
192 192
193 193 action, meta = reactor.onframerecv(frame)
194 194
195 195 if action == 'wantframe':
196 196 # Need more data before we can do anything.
197 197 continue
198 198 elif action == 'runcommand':
199 199 sentoutput = _httpv2runcommand(ui, repo, req, res, authedperm,
200 200 reqcommand, reactor, outstream,
201 201 meta, issubsequent=seencommand)
202 202
203 203 if sentoutput:
204 204 return
205 205
206 206 seencommand = True
207 207
208 208 elif action == 'error':
209 209 # TODO define proper error mechanism.
210 210 res.status = b'200 OK'
211 211 res.headers[b'Content-Type'] = b'text/plain'
212 212 res.setbodybytes(meta['message'] + b'\n')
213 213 return
214 214 else:
215 215 raise error.ProgrammingError(
216 216 'unhandled action from frame processor: %s' % action)
217 217
218 218 action, meta = reactor.oninputeof()
219 219 if action == 'sendframes':
220 220 # We assume we haven't started sending the response yet. If we're
221 221 # wrong, the response type will raise an exception.
222 222 res.status = b'200 OK'
223 223 res.headers[b'Content-Type'] = FRAMINGTYPE
224 224 res.setbodygen(meta['framegen'])
225 225 elif action == 'noop':
226 226 pass
227 227 else:
228 228 raise error.ProgrammingError('unhandled action from frame processor: %s'
229 229 % action)
230 230
231 231 def _httpv2runcommand(ui, repo, req, res, authedperm, reqcommand, reactor,
232 232 outstream, command, issubsequent):
233 233 """Dispatch a wire protocol command made from HTTPv2 requests.
234 234
235 235 The authenticated permission (``authedperm``) along with the original
236 236 command from the URL (``reqcommand``) are passed in.
237 237 """
238 238 # We already validated that the session has permissions to perform the
239 239 # actions in ``authedperm``. In the unified frame protocol, the canonical
240 240 # command to run is expressed in a frame. However, the URL also requested
241 241 # to run a specific command. We need to be careful that the command we
242 242 # run doesn't have permissions requirements greater than what was granted
243 243 # by ``authedperm``.
244 244 #
245 245 # Our rule for this is we only allow one command per HTTP request and
246 246 # that command must match the command in the URL. However, we make
247 247 # an exception for the ``multirequest`` URL. This URL is allowed to
248 248 # execute multiple commands. We double check permissions of each command
249 249 # as it is invoked to ensure there is no privilege escalation.
250 250 # TODO consider allowing multiple commands to regular command URLs
251 251 # iff each command is the same.
252 252
253 253 proto = httpv2protocolhandler(req, ui, args=command['args'])
254 254
255 255 if reqcommand == b'multirequest':
256 256 if not wireproto.commandsv2.commandavailable(command['command'], proto):
257 257 # TODO proper error mechanism
258 258 res.status = b'200 OK'
259 259 res.headers[b'Content-Type'] = b'text/plain'
260 260 res.setbodybytes(_('wire protocol command not available: %s') %
261 261 command['command'])
262 262 return True
263 263
264 264 # TODO don't use assert here, since it may be elided by -O.
265 265 assert authedperm in (b'ro', b'rw')
266 266 wirecommand = wireproto.commandsv2[command['command']]
267 267 assert wirecommand.permission in ('push', 'pull')
268 268
269 269 if authedperm == b'ro' and wirecommand.permission != 'pull':
270 270 # TODO proper error mechanism
271 271 res.status = b'403 Forbidden'
272 272 res.headers[b'Content-Type'] = b'text/plain'
273 273 res.setbodybytes(_('insufficient permissions to execute '
274 274 'command: %s') % command['command'])
275 275 return True
276 276
277 277 # TODO should we also call checkperm() here? Maybe not if we're going
278 278 # to overhaul that API. The granted scope from the URL check should
279 279 # be good enough.
280 280
281 281 else:
282 282 # Don't allow multiple commands outside of ``multirequest`` URL.
283 283 if issubsequent:
284 284 # TODO proper error mechanism
285 285 res.status = b'200 OK'
286 286 res.headers[b'Content-Type'] = b'text/plain'
287 287 res.setbodybytes(_('multiple commands cannot be issued to this '
288 288 'URL'))
289 289 return True
290 290
291 291 if reqcommand != command['command']:
292 292 # TODO define proper error mechanism
293 293 res.status = b'200 OK'
294 294 res.headers[b'Content-Type'] = b'text/plain'
295 295 res.setbodybytes(_('command in frame must match command in URL'))
296 296 return True
297 297
298 298 rsp = wireproto.dispatch(repo, proto, command['command'])
299 299
300 300 res.status = b'200 OK'
301 301 res.headers[b'Content-Type'] = FRAMINGTYPE
302 302
303 303 if isinstance(rsp, wireprototypes.bytesresponse):
304 304 action, meta = reactor.onbytesresponseready(outstream,
305 305 command['requestid'],
306 306 rsp.data)
307 307 elif isinstance(rsp, wireprototypes.cborresponse):
308 308 encoded = cbor.dumps(rsp.value, canonical=True)
309 309 action, meta = reactor.onbytesresponseready(outstream,
310 310 command['requestid'],
311 311 encoded,
312 312 iscbor=True)
313 313 else:
314 314 action, meta = reactor.onapplicationerror(
315 315 _('unhandled response type from wire proto command'))
316 316
317 317 if action == 'sendframes':
318 318 res.setbodygen(meta['framegen'])
319 319 return True
320 320 elif action == 'noop':
321 321 return False
322 322 else:
323 323 raise error.ProgrammingError('unhandled event from reactor: %s' %
324 324 action)
325 325
326 326 @zi.implementer(wireprototypes.baseprotocolhandler)
327 327 class httpv2protocolhandler(object):
328 328 def __init__(self, req, ui, args=None):
329 329 self._req = req
330 330 self._ui = ui
331 331 self._args = args
332 332
333 333 @property
334 334 def name(self):
335 return HTTPV2
335 return HTTP_WIREPROTO_V2
336 336
337 337 def getargs(self, args):
338 338 data = {}
339 339 for k, typ in args.items():
340 340 if k == '*':
341 341 raise NotImplementedError('do not support * args')
342 342 elif k in self._args:
343 343 # TODO consider validating value types.
344 344 data[k] = self._args[k]
345 345
346 346 return data
347 347
348 348 def getprotocaps(self):
349 349 # Protocol capabilities are currently not implemented for HTTP V2.
350 350 return set()
351 351
352 352 def getpayload(self):
353 353 raise NotImplementedError
354 354
355 355 @contextlib.contextmanager
356 356 def mayberedirectstdio(self):
357 357 raise NotImplementedError
358 358
359 359 def client(self):
360 360 raise NotImplementedError
361 361
362 362 def addcapabilities(self, repo, caps):
363 363 return caps
364 364
365 365 def checkperm(self, perm):
366 366 raise NotImplementedError
367 367
368 368 def httpv2apidescriptor(req, repo):
369 369 proto = httpv2protocolhandler(req, repo.ui)
370 370
371 371 return _capabilitiesv2(repo, proto)
372 372
373 373 def _capabilitiesv2(repo, proto):
374 374 """Obtain the set of capabilities for version 2 transports.
375 375
376 376 These capabilities are distinct from the capabilities for version 1
377 377 transports.
378 378 """
379 379 compression = []
380 380 for engine in wireproto.supportedcompengines(repo.ui, util.SERVERROLE):
381 381 compression.append({
382 382 b'name': engine.wireprotosupport().name,
383 383 })
384 384
385 385 caps = {
386 386 'commands': {},
387 387 'compression': compression,
388 388 }
389 389
390 390 for command, entry in wireproto.commandsv2.items():
391 391 caps['commands'][command] = {
392 392 'args': entry.args,
393 393 'permissions': [entry.permission],
394 394 }
395 395
396 396 return proto.addcapabilities(repo, caps)
397 397
398 398 def wireprotocommand(*args, **kwargs):
399 399 def register(func):
400 400 return wireproto.wireprotocommand(
401 401 *args, transportpolicy=wireproto.POLICY_V2_ONLY, **kwargs)(func)
402 402
403 403 return register
404 404
405 405 @wireprotocommand('branchmap', permission='pull')
406 406 def branchmapv2(repo, proto):
407 407 branchmap = {encoding.fromlocal(k): v
408 408 for k, v in repo.branchmap().iteritems()}
409 409
410 410 return wireprototypes.cborresponse(branchmap)
411 411
412 412 @wireprotocommand('capabilities', permission='pull')
413 413 def capabilitiesv2(repo, proto):
414 414 caps = _capabilitiesv2(repo, proto)
415 415
416 416 return wireprototypes.cborresponse(caps)
417 417
418 418 @wireprotocommand('heads',
419 419 args={
420 420 'publiconly': False,
421 421 },
422 422 permission='pull')
423 423 def headsv2(repo, proto, publiconly=False):
424 424 if publiconly:
425 425 repo = repo.filtered('immutable')
426 426
427 427 return wireprototypes.cborresponse(repo.heads())
428 428
429 429 @wireprotocommand('known',
430 430 args={
431 431 'nodes': [b'deadbeef'],
432 432 },
433 433 permission='pull')
434 434 def knownv2(repo, proto, nodes=None):
435 435 nodes = nodes or []
436 436 result = b''.join(b'1' if n else b'0' for n in repo.known(nodes))
437 437 return wireprototypes.cborresponse(result)
438 438
439 439 @wireprotocommand('listkeys',
440 440 args={
441 441 'namespace': b'ns',
442 442 },
443 443 permission='pull')
444 444 def listkeysv2(repo, proto, namespace=None):
445 445 keys = repo.listkeys(encoding.tolocal(namespace))
446 446 keys = {encoding.fromlocal(k): encoding.fromlocal(v)
447 447 for k, v in keys.iteritems()}
448 448
449 449 return wireprototypes.cborresponse(keys)
450 450
451 451 @wireprotocommand('lookup',
452 452 args={
453 453 'key': b'foo',
454 454 },
455 455 permission='pull')
456 456 def lookupv2(repo, proto, key):
457 457 key = encoding.tolocal(key)
458 458
459 459 # TODO handle exception.
460 460 node = repo.lookup(key)
461 461
462 462 return wireprototypes.cborresponse(node)
463 463
464 464 @wireprotocommand('pushkey',
465 465 args={
466 466 'namespace': b'ns',
467 467 'key': b'key',
468 468 'old': b'old',
469 469 'new': b'new',
470 470 },
471 471 permission='push')
472 472 def pushkeyv2(repo, proto, namespace, key, old, new):
473 473 # TODO handle ui output redirection
474 474 r = repo.pushkey(encoding.tolocal(namespace),
475 475 encoding.tolocal(key),
476 476 encoding.tolocal(old),
477 477 encoding.tolocal(new))
478 478
479 479 return wireprototypes.cborresponse(r)
General Comments 0
You need to be logged in to leave comments. Login now