##// END OF EJS Templates
configitems: register the 'web.logoimg' config
Boris Feld -
r34612:c879fc7b default
parent child Browse files
Show More
@@ -1,848 +1,851 b''
1 1 # configitems.py - centralized declaration of configuration option
2 2 #
3 3 # Copyright 2017 Pierre-Yves David <pierre-yves.david@octobus.net>
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 functools
11 11
12 12 from . import (
13 13 encoding,
14 14 error,
15 15 )
16 16
17 17 def loadconfigtable(ui, extname, configtable):
18 18 """update config item known to the ui with the extension ones"""
19 19 for section, items in configtable.items():
20 20 knownitems = ui._knownconfig.setdefault(section, {})
21 21 knownkeys = set(knownitems)
22 22 newkeys = set(items)
23 23 for key in sorted(knownkeys & newkeys):
24 24 msg = "extension '%s' overwrite config item '%s.%s'"
25 25 msg %= (extname, section, key)
26 26 ui.develwarn(msg, config='warn-config')
27 27
28 28 knownitems.update(items)
29 29
30 30 class configitem(object):
31 31 """represent a known config item
32 32
33 33 :section: the official config section where to find this item,
34 34 :name: the official name within the section,
35 35 :default: default value for this item,
36 36 :alias: optional list of tuples as alternatives.
37 37 """
38 38
39 39 def __init__(self, section, name, default=None, alias=()):
40 40 self.section = section
41 41 self.name = name
42 42 self.default = default
43 43 self.alias = list(alias)
44 44
45 45 coreitems = {}
46 46
47 47 def _register(configtable, *args, **kwargs):
48 48 item = configitem(*args, **kwargs)
49 49 section = configtable.setdefault(item.section, {})
50 50 if item.name in section:
51 51 msg = "duplicated config item registration for '%s.%s'"
52 52 raise error.ProgrammingError(msg % (item.section, item.name))
53 53 section[item.name] = item
54 54
55 55 # special value for case where the default is derived from other values
56 56 dynamicdefault = object()
57 57
58 58 # Registering actual config items
59 59
60 60 def getitemregister(configtable):
61 61 return functools.partial(_register, configtable)
62 62
63 63 coreconfigitem = getitemregister(coreitems)
64 64
65 65 coreconfigitem('auth', 'cookiefile',
66 66 default=None,
67 67 )
68 68 # bookmarks.pushing: internal hack for discovery
69 69 coreconfigitem('bookmarks', 'pushing',
70 70 default=list,
71 71 )
72 72 # bundle.mainreporoot: internal hack for bundlerepo
73 73 coreconfigitem('bundle', 'mainreporoot',
74 74 default='',
75 75 )
76 76 # bundle.reorder: experimental config
77 77 coreconfigitem('bundle', 'reorder',
78 78 default='auto',
79 79 )
80 80 coreconfigitem('censor', 'policy',
81 81 default='abort',
82 82 )
83 83 coreconfigitem('chgserver', 'idletimeout',
84 84 default=3600,
85 85 )
86 86 coreconfigitem('chgserver', 'skiphash',
87 87 default=False,
88 88 )
89 89 coreconfigitem('cmdserver', 'log',
90 90 default=None,
91 91 )
92 92 coreconfigitem('color', 'mode',
93 93 default='auto',
94 94 )
95 95 coreconfigitem('color', 'pagermode',
96 96 default=dynamicdefault,
97 97 )
98 98 coreconfigitem('commands', 'status.relative',
99 99 default=False,
100 100 )
101 101 coreconfigitem('commands', 'status.skipstates',
102 102 default=[],
103 103 )
104 104 coreconfigitem('commands', 'status.verbose',
105 105 default=False,
106 106 )
107 107 coreconfigitem('commands', 'update.requiredest',
108 108 default=False,
109 109 )
110 110 coreconfigitem('debug', 'dirstate.delaywrite',
111 111 default=0,
112 112 )
113 113 coreconfigitem('devel', 'all-warnings',
114 114 default=False,
115 115 )
116 116 coreconfigitem('devel', 'bundle2.debug',
117 117 default=False,
118 118 )
119 119 coreconfigitem('devel', 'cache-vfs',
120 120 default=None,
121 121 )
122 122 coreconfigitem('devel', 'check-locks',
123 123 default=False,
124 124 )
125 125 coreconfigitem('devel', 'check-relroot',
126 126 default=False,
127 127 )
128 128 coreconfigitem('devel', 'default-date',
129 129 default=None,
130 130 )
131 131 coreconfigitem('devel', 'deprec-warn',
132 132 default=False,
133 133 )
134 134 coreconfigitem('devel', 'disableloaddefaultcerts',
135 135 default=False,
136 136 )
137 137 coreconfigitem('devel', 'empty-changegroup',
138 138 default=False,
139 139 )
140 140 coreconfigitem('devel', 'legacy.exchange',
141 141 default=list,
142 142 )
143 143 coreconfigitem('devel', 'servercafile',
144 144 default='',
145 145 )
146 146 coreconfigitem('devel', 'serverexactprotocol',
147 147 default='',
148 148 )
149 149 coreconfigitem('devel', 'serverrequirecert',
150 150 default=False,
151 151 )
152 152 coreconfigitem('devel', 'strip-obsmarkers',
153 153 default=True,
154 154 )
155 155 coreconfigitem('devel', 'warn-config',
156 156 default=None,
157 157 )
158 158 coreconfigitem('devel', 'warn-config-default',
159 159 default=None,
160 160 )
161 161 coreconfigitem('devel', 'user.obsmarker',
162 162 default=None,
163 163 )
164 164 coreconfigitem('diff', 'nodates',
165 165 default=None,
166 166 )
167 167 coreconfigitem('diff', 'showfunc',
168 168 default=None,
169 169 )
170 170 coreconfigitem('diff', 'unified',
171 171 default=None,
172 172 )
173 173 coreconfigitem('diff', 'git',
174 174 default=None,
175 175 )
176 176 coreconfigitem('diff', 'ignorews',
177 177 default=None,
178 178 )
179 179 coreconfigitem('diff', 'ignorewsamount',
180 180 default=None,
181 181 )
182 182 coreconfigitem('diff', 'ignoreblanklines',
183 183 default=None,
184 184 )
185 185 coreconfigitem('diff', 'ignorewseol',
186 186 default=None,
187 187 )
188 188 coreconfigitem('diff', 'nobinary',
189 189 default=None,
190 190 )
191 191 coreconfigitem('diff', 'noprefix',
192 192 default=None,
193 193 )
194 194 coreconfigitem('email', 'bcc',
195 195 default=None,
196 196 )
197 197 coreconfigitem('email', 'cc',
198 198 default=None,
199 199 )
200 200 coreconfigitem('email', 'charsets',
201 201 default=list,
202 202 )
203 203 coreconfigitem('email', 'from',
204 204 default=None,
205 205 )
206 206 coreconfigitem('email', 'method',
207 207 default='smtp',
208 208 )
209 209 coreconfigitem('email', 'reply-to',
210 210 default=None,
211 211 )
212 212 coreconfigitem('experimental', 'allowdivergence',
213 213 default=False,
214 214 )
215 215 coreconfigitem('experimental', 'bundle-phases',
216 216 default=False,
217 217 )
218 218 coreconfigitem('experimental', 'bundle2-advertise',
219 219 default=True,
220 220 )
221 221 coreconfigitem('experimental', 'bundle2-output-capture',
222 222 default=False,
223 223 )
224 224 coreconfigitem('experimental', 'bundle2.pushback',
225 225 default=False,
226 226 )
227 227 coreconfigitem('experimental', 'bundle2lazylocking',
228 228 default=False,
229 229 )
230 230 coreconfigitem('experimental', 'bundlecomplevel',
231 231 default=None,
232 232 )
233 233 coreconfigitem('experimental', 'changegroup3',
234 234 default=False,
235 235 )
236 236 coreconfigitem('experimental', 'clientcompressionengines',
237 237 default=list,
238 238 )
239 239 coreconfigitem('experimental', 'copytrace',
240 240 default='on',
241 241 )
242 242 coreconfigitem('experimental', 'copytrace.sourcecommitlimit',
243 243 default=100,
244 244 )
245 245 coreconfigitem('experimental', 'crecordtest',
246 246 default=None,
247 247 )
248 248 coreconfigitem('experimental', 'editortmpinhg',
249 249 default=False,
250 250 )
251 251 coreconfigitem('experimental', 'maxdeltachainspan',
252 252 default=-1,
253 253 )
254 254 coreconfigitem('experimental', 'mmapindexthreshold',
255 255 default=None,
256 256 )
257 257 coreconfigitem('experimental', 'nonnormalparanoidcheck',
258 258 default=False,
259 259 )
260 260 coreconfigitem('experimental', 'stabilization',
261 261 default=list,
262 262 alias=[('experimental', 'evolution')],
263 263 )
264 264 coreconfigitem('experimental', 'stabilization.bundle-obsmarker',
265 265 default=False,
266 266 alias=[('experimental', 'evolution.bundle-obsmarker')],
267 267 )
268 268 coreconfigitem('experimental', 'stabilization.track-operation',
269 269 default=True,
270 270 alias=[('experimental', 'evolution.track-operation')]
271 271 )
272 272 coreconfigitem('experimental', 'exportableenviron',
273 273 default=list,
274 274 )
275 275 coreconfigitem('experimental', 'extendedheader.index',
276 276 default=None,
277 277 )
278 278 coreconfigitem('experimental', 'extendedheader.similarity',
279 279 default=False,
280 280 )
281 281 coreconfigitem('experimental', 'format.compression',
282 282 default='zlib',
283 283 )
284 284 coreconfigitem('experimental', 'graphshorten',
285 285 default=False,
286 286 )
287 287 coreconfigitem('experimental', 'graphstyle.parent',
288 288 default=dynamicdefault,
289 289 )
290 290 coreconfigitem('experimental', 'graphstyle.missing',
291 291 default=dynamicdefault,
292 292 )
293 293 coreconfigitem('experimental', 'graphstyle.grandparent',
294 294 default=dynamicdefault,
295 295 )
296 296 coreconfigitem('experimental', 'hook-track-tags',
297 297 default=False,
298 298 )
299 299 coreconfigitem('experimental', 'httppostargs',
300 300 default=False,
301 301 )
302 302 coreconfigitem('experimental', 'manifestv2',
303 303 default=False,
304 304 )
305 305 coreconfigitem('experimental', 'mergedriver',
306 306 default=None,
307 307 )
308 308 coreconfigitem('experimental', 'obsmarkers-exchange-debug',
309 309 default=False,
310 310 )
311 311 coreconfigitem('experimental', 'rebase.multidest',
312 312 default=False,
313 313 )
314 314 coreconfigitem('experimental', 'revertalternateinteractivemode',
315 315 default=True,
316 316 )
317 317 coreconfigitem('experimental', 'revlogv2',
318 318 default=None,
319 319 )
320 320 coreconfigitem('experimental', 'spacemovesdown',
321 321 default=False,
322 322 )
323 323 coreconfigitem('experimental', 'treemanifest',
324 324 default=False,
325 325 )
326 326 coreconfigitem('experimental', 'updatecheck',
327 327 default=None,
328 328 )
329 329 coreconfigitem('format', 'aggressivemergedeltas',
330 330 default=False,
331 331 )
332 332 coreconfigitem('format', 'chunkcachesize',
333 333 default=None,
334 334 )
335 335 coreconfigitem('format', 'dotencode',
336 336 default=True,
337 337 )
338 338 coreconfigitem('format', 'generaldelta',
339 339 default=False,
340 340 )
341 341 coreconfigitem('format', 'manifestcachesize',
342 342 default=None,
343 343 )
344 344 coreconfigitem('format', 'maxchainlen',
345 345 default=None,
346 346 )
347 347 coreconfigitem('format', 'obsstore-version',
348 348 default=None,
349 349 )
350 350 coreconfigitem('format', 'usefncache',
351 351 default=True,
352 352 )
353 353 coreconfigitem('format', 'usegeneraldelta',
354 354 default=True,
355 355 )
356 356 coreconfigitem('format', 'usestore',
357 357 default=True,
358 358 )
359 359 coreconfigitem('hostsecurity', 'ciphers',
360 360 default=None,
361 361 )
362 362 coreconfigitem('hostsecurity', 'disabletls10warning',
363 363 default=False,
364 364 )
365 365 coreconfigitem('http_proxy', 'always',
366 366 default=False,
367 367 )
368 368 coreconfigitem('http_proxy', 'host',
369 369 default=None,
370 370 )
371 371 coreconfigitem('http_proxy', 'no',
372 372 default=list,
373 373 )
374 374 coreconfigitem('http_proxy', 'passwd',
375 375 default=None,
376 376 )
377 377 coreconfigitem('http_proxy', 'user',
378 378 default=None,
379 379 )
380 380 coreconfigitem('logtoprocess', 'commandexception',
381 381 default=None,
382 382 )
383 383 coreconfigitem('logtoprocess', 'commandfinish',
384 384 default=None,
385 385 )
386 386 coreconfigitem('logtoprocess', 'command',
387 387 default=None,
388 388 )
389 389 coreconfigitem('logtoprocess', 'develwarn',
390 390 default=None,
391 391 )
392 392 coreconfigitem('logtoprocess', 'uiblocked',
393 393 default=None,
394 394 )
395 395 coreconfigitem('merge', 'checkunknown',
396 396 default='abort',
397 397 )
398 398 coreconfigitem('merge', 'checkignored',
399 399 default='abort',
400 400 )
401 401 coreconfigitem('merge', 'followcopies',
402 402 default=True,
403 403 )
404 404 coreconfigitem('merge', 'preferancestor',
405 405 default=lambda: ['*'],
406 406 )
407 407 coreconfigitem('pager', 'ignore',
408 408 default=list,
409 409 )
410 410 coreconfigitem('pager', 'pager',
411 411 default=dynamicdefault,
412 412 )
413 413 coreconfigitem('patch', 'eol',
414 414 default='strict',
415 415 )
416 416 coreconfigitem('patch', 'fuzz',
417 417 default=2,
418 418 )
419 419 coreconfigitem('paths', 'default',
420 420 default=None,
421 421 )
422 422 coreconfigitem('paths', 'default-push',
423 423 default=None,
424 424 )
425 425 coreconfigitem('phases', 'checksubrepos',
426 426 default='follow',
427 427 )
428 428 coreconfigitem('phases', 'new-commit',
429 429 default='draft',
430 430 )
431 431 coreconfigitem('phases', 'publish',
432 432 default=True,
433 433 )
434 434 coreconfigitem('profiling', 'enabled',
435 435 default=False,
436 436 )
437 437 coreconfigitem('profiling', 'format',
438 438 default='text',
439 439 )
440 440 coreconfigitem('profiling', 'freq',
441 441 default=1000,
442 442 )
443 443 coreconfigitem('profiling', 'limit',
444 444 default=30,
445 445 )
446 446 coreconfigitem('profiling', 'nested',
447 447 default=0,
448 448 )
449 449 coreconfigitem('profiling', 'output',
450 450 default=None,
451 451 )
452 452 coreconfigitem('profiling', 'showmax',
453 453 default=0.999,
454 454 )
455 455 coreconfigitem('profiling', 'showmin',
456 456 default=dynamicdefault,
457 457 )
458 458 coreconfigitem('profiling', 'sort',
459 459 default='inlinetime',
460 460 )
461 461 coreconfigitem('profiling', 'statformat',
462 462 default='hotpath',
463 463 )
464 464 coreconfigitem('profiling', 'type',
465 465 default='stat',
466 466 )
467 467 coreconfigitem('progress', 'assume-tty',
468 468 default=False,
469 469 )
470 470 coreconfigitem('progress', 'changedelay',
471 471 default=1,
472 472 )
473 473 coreconfigitem('progress', 'clear-complete',
474 474 default=True,
475 475 )
476 476 coreconfigitem('progress', 'debug',
477 477 default=False,
478 478 )
479 479 coreconfigitem('progress', 'delay',
480 480 default=3,
481 481 )
482 482 coreconfigitem('progress', 'disable',
483 483 default=False,
484 484 )
485 485 coreconfigitem('progress', 'estimateinterval',
486 486 default=60.0,
487 487 )
488 488 coreconfigitem('progress', 'refresh',
489 489 default=0.1,
490 490 )
491 491 coreconfigitem('progress', 'width',
492 492 default=dynamicdefault,
493 493 )
494 494 coreconfigitem('push', 'pushvars.server',
495 495 default=False,
496 496 )
497 497 coreconfigitem('server', 'bundle1',
498 498 default=True,
499 499 )
500 500 coreconfigitem('server', 'bundle1gd',
501 501 default=None,
502 502 )
503 503 coreconfigitem('server', 'compressionengines',
504 504 default=list,
505 505 )
506 506 coreconfigitem('server', 'concurrent-push-mode',
507 507 default='strict',
508 508 )
509 509 coreconfigitem('server', 'disablefullbundle',
510 510 default=False,
511 511 )
512 512 coreconfigitem('server', 'maxhttpheaderlen',
513 513 default=1024,
514 514 )
515 515 coreconfigitem('server', 'preferuncompressed',
516 516 default=False,
517 517 )
518 518 coreconfigitem('server', 'uncompressed',
519 519 default=True,
520 520 )
521 521 coreconfigitem('server', 'uncompressedallowsecret',
522 522 default=False,
523 523 )
524 524 coreconfigitem('server', 'validate',
525 525 default=False,
526 526 )
527 527 coreconfigitem('server', 'zliblevel',
528 528 default=-1,
529 529 )
530 530 coreconfigitem('smtp', 'host',
531 531 default=None,
532 532 )
533 533 coreconfigitem('smtp', 'local_hostname',
534 534 default=None,
535 535 )
536 536 coreconfigitem('smtp', 'password',
537 537 default=None,
538 538 )
539 539 coreconfigitem('smtp', 'port',
540 540 default=dynamicdefault,
541 541 )
542 542 coreconfigitem('smtp', 'tls',
543 543 default='none',
544 544 )
545 545 coreconfigitem('smtp', 'username',
546 546 default=None,
547 547 )
548 548 coreconfigitem('sparse', 'missingwarning',
549 549 default=True,
550 550 )
551 551 coreconfigitem('trusted', 'groups',
552 552 default=list,
553 553 )
554 554 coreconfigitem('trusted', 'users',
555 555 default=list,
556 556 )
557 557 coreconfigitem('ui', '_usedassubrepo',
558 558 default=False,
559 559 )
560 560 coreconfigitem('ui', 'allowemptycommit',
561 561 default=False,
562 562 )
563 563 coreconfigitem('ui', 'archivemeta',
564 564 default=True,
565 565 )
566 566 coreconfigitem('ui', 'askusername',
567 567 default=False,
568 568 )
569 569 coreconfigitem('ui', 'clonebundlefallback',
570 570 default=False,
571 571 )
572 572 coreconfigitem('ui', 'clonebundleprefers',
573 573 default=list,
574 574 )
575 575 coreconfigitem('ui', 'clonebundles',
576 576 default=True,
577 577 )
578 578 coreconfigitem('ui', 'color',
579 579 default='auto',
580 580 )
581 581 coreconfigitem('ui', 'commitsubrepos',
582 582 default=False,
583 583 )
584 584 coreconfigitem('ui', 'debug',
585 585 default=False,
586 586 )
587 587 coreconfigitem('ui', 'debugger',
588 588 default=None,
589 589 )
590 590 coreconfigitem('ui', 'fallbackencoding',
591 591 default=None,
592 592 )
593 593 coreconfigitem('ui', 'forcecwd',
594 594 default=None,
595 595 )
596 596 coreconfigitem('ui', 'forcemerge',
597 597 default=None,
598 598 )
599 599 coreconfigitem('ui', 'formatdebug',
600 600 default=False,
601 601 )
602 602 coreconfigitem('ui', 'formatjson',
603 603 default=False,
604 604 )
605 605 coreconfigitem('ui', 'formatted',
606 606 default=None,
607 607 )
608 608 coreconfigitem('ui', 'graphnodetemplate',
609 609 default=None,
610 610 )
611 611 coreconfigitem('ui', 'http2debuglevel',
612 612 default=None,
613 613 )
614 614 coreconfigitem('ui', 'interactive',
615 615 default=None,
616 616 )
617 617 coreconfigitem('ui', 'interface',
618 618 default=None,
619 619 )
620 620 coreconfigitem('ui', 'logblockedtimes',
621 621 default=False,
622 622 )
623 623 coreconfigitem('ui', 'logtemplate',
624 624 default=None,
625 625 )
626 626 coreconfigitem('ui', 'merge',
627 627 default=None,
628 628 )
629 629 coreconfigitem('ui', 'mergemarkers',
630 630 default='basic',
631 631 )
632 632 coreconfigitem('ui', 'mergemarkertemplate',
633 633 default=('{node|short} '
634 634 '{ifeq(tags, "tip", "", '
635 635 'ifeq(tags, "", "", "{tags} "))}'
636 636 '{if(bookmarks, "{bookmarks} ")}'
637 637 '{ifeq(branch, "default", "", "{branch} ")}'
638 638 '- {author|user}: {desc|firstline}')
639 639 )
640 640 coreconfigitem('ui', 'nontty',
641 641 default=False,
642 642 )
643 643 coreconfigitem('ui', 'origbackuppath',
644 644 default=None,
645 645 )
646 646 coreconfigitem('ui', 'paginate',
647 647 default=True,
648 648 )
649 649 coreconfigitem('ui', 'patch',
650 650 default=None,
651 651 )
652 652 coreconfigitem('ui', 'portablefilenames',
653 653 default='warn',
654 654 )
655 655 coreconfigitem('ui', 'promptecho',
656 656 default=False,
657 657 )
658 658 coreconfigitem('ui', 'quiet',
659 659 default=False,
660 660 )
661 661 coreconfigitem('ui', 'quietbookmarkmove',
662 662 default=False,
663 663 )
664 664 coreconfigitem('ui', 'remotecmd',
665 665 default='hg',
666 666 )
667 667 coreconfigitem('ui', 'report_untrusted',
668 668 default=True,
669 669 )
670 670 coreconfigitem('ui', 'rollback',
671 671 default=True,
672 672 )
673 673 coreconfigitem('ui', 'slash',
674 674 default=False,
675 675 )
676 676 coreconfigitem('ui', 'ssh',
677 677 default='ssh',
678 678 )
679 679 coreconfigitem('ui', 'statuscopies',
680 680 default=False,
681 681 )
682 682 coreconfigitem('ui', 'strict',
683 683 default=False,
684 684 )
685 685 coreconfigitem('ui', 'style',
686 686 default='',
687 687 )
688 688 coreconfigitem('ui', 'supportcontact',
689 689 default=None,
690 690 )
691 691 coreconfigitem('ui', 'textwidth',
692 692 default=78,
693 693 )
694 694 coreconfigitem('ui', 'timeout',
695 695 default='600',
696 696 )
697 697 coreconfigitem('ui', 'traceback',
698 698 default=False,
699 699 )
700 700 coreconfigitem('ui', 'tweakdefaults',
701 701 default=False,
702 702 )
703 703 coreconfigitem('ui', 'usehttp2',
704 704 default=False,
705 705 )
706 706 coreconfigitem('ui', 'username',
707 707 alias=[('ui', 'user')]
708 708 )
709 709 coreconfigitem('ui', 'verbose',
710 710 default=False,
711 711 )
712 712 coreconfigitem('verify', 'skipflags',
713 713 default=None,
714 714 )
715 715 coreconfigitem('web', 'allowbz2',
716 716 default=None,
717 717 )
718 718 coreconfigitem('web', 'allowgz',
719 719 default=None,
720 720 )
721 721 coreconfigitem('web', 'allowpull',
722 722 default=True,
723 723 )
724 724 coreconfigitem('web', 'allow_push',
725 725 default=list,
726 726 )
727 727 coreconfigitem('web', 'allowzip',
728 728 default=None,
729 729 )
730 730 coreconfigitem('web', 'cache',
731 731 default=True,
732 732 )
733 733 coreconfigitem('web', 'contact',
734 734 default=None,
735 735 )
736 736 coreconfigitem('web', 'deny_push',
737 737 default=list,
738 738 )
739 739 coreconfigitem('web', 'guessmime',
740 740 default=False,
741 741 )
742 742 coreconfigitem('web', 'hidden',
743 743 default=None,
744 744 )
745 745 coreconfigitem('web', 'labels',
746 746 default=list,
747 747 )
748 coreconfigitem('web', 'logoimg',
749 default='hglogo.png',
750 )
748 751 coreconfigitem('web', 'accesslog',
749 752 default='-',
750 753 )
751 754 coreconfigitem('web', 'address',
752 755 default='',
753 756 )
754 757 coreconfigitem('web', 'allow_archive',
755 758 default=list,
756 759 )
757 760 coreconfigitem('web', 'allow_read',
758 761 default=list,
759 762 )
760 763 coreconfigitem('web', 'baseurl',
761 764 default=None,
762 765 )
763 766 coreconfigitem('web', 'cacerts',
764 767 default=None,
765 768 )
766 769 coreconfigitem('web', 'certificate',
767 770 default=None,
768 771 )
769 772 coreconfigitem('web', 'collapse',
770 773 default=False,
771 774 )
772 775 coreconfigitem('web', 'csp',
773 776 default=None,
774 777 )
775 778 coreconfigitem('web', 'deny_read',
776 779 default=list,
777 780 )
778 781 coreconfigitem('web', 'descend',
779 782 default=True,
780 783 )
781 784 coreconfigitem('web', 'description',
782 785 default="",
783 786 )
784 787 coreconfigitem('web', 'encoding',
785 788 default=lambda: encoding.encoding,
786 789 )
787 790 coreconfigitem('web', 'errorlog',
788 791 default='-',
789 792 )
790 793 coreconfigitem('web', 'ipv6',
791 794 default=False,
792 795 )
793 796 coreconfigitem('web', 'maxchanges',
794 797 default=10,
795 798 )
796 799 coreconfigitem('web', 'maxfiles',
797 800 default=10,
798 801 )
799 802 coreconfigitem('web', 'maxshortchanges',
800 803 default=60,
801 804 )
802 805 coreconfigitem('web', 'motd',
803 806 default='',
804 807 )
805 808 coreconfigitem('web', 'name',
806 809 default=dynamicdefault,
807 810 )
808 811 coreconfigitem('web', 'port',
809 812 default=8000,
810 813 )
811 814 coreconfigitem('web', 'prefix',
812 815 default='',
813 816 )
814 817 coreconfigitem('web', 'push_ssl',
815 818 default=True,
816 819 )
817 820 coreconfigitem('web', 'refreshinterval',
818 821 default=20,
819 822 )
820 823 coreconfigitem('web', 'stripes',
821 824 default=1,
822 825 )
823 826 coreconfigitem('web', 'style',
824 827 default='paper',
825 828 )
826 829 coreconfigitem('web', 'templates',
827 830 default=None,
828 831 )
829 832 coreconfigitem('web', 'view',
830 833 default='served',
831 834 )
832 835 coreconfigitem('worker', 'backgroundclose',
833 836 default=dynamicdefault,
834 837 )
835 838 # Windows defaults to a limit of 512 open files. A buffer of 128
836 839 # should give us enough headway.
837 840 coreconfigitem('worker', 'backgroundclosemaxqueue',
838 841 default=384,
839 842 )
840 843 coreconfigitem('worker', 'backgroundcloseminfilecount',
841 844 default=2048,
842 845 )
843 846 coreconfigitem('worker', 'backgroundclosethreadcount',
844 847 default=4,
845 848 )
846 849 coreconfigitem('worker', 'numcpus',
847 850 default=None,
848 851 )
@@ -1,492 +1,492 b''
1 1 # hgweb/hgweb_mod.py - Web interface for a repository.
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.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 contextlib
12 12 import os
13 13
14 14 from .common import (
15 15 ErrorResponse,
16 16 HTTP_BAD_REQUEST,
17 17 HTTP_NOT_FOUND,
18 18 HTTP_NOT_MODIFIED,
19 19 HTTP_OK,
20 20 HTTP_SERVER_ERROR,
21 21 caching,
22 22 cspvalues,
23 23 permhooks,
24 24 )
25 25 from .request import wsgirequest
26 26
27 27 from .. import (
28 28 encoding,
29 29 error,
30 30 hg,
31 31 hook,
32 32 profiling,
33 33 pycompat,
34 34 repoview,
35 35 templatefilters,
36 36 templater,
37 37 ui as uimod,
38 38 util,
39 39 )
40 40
41 41 from . import (
42 42 protocol,
43 43 webcommands,
44 44 webutil,
45 45 wsgicgi,
46 46 )
47 47
48 48 perms = {
49 49 'changegroup': 'pull',
50 50 'changegroupsubset': 'pull',
51 51 'getbundle': 'pull',
52 52 'stream_out': 'pull',
53 53 'listkeys': 'pull',
54 54 'unbundle': 'push',
55 55 'pushkey': 'push',
56 56 }
57 57
58 58 archivespecs = util.sortdict((
59 59 ('zip', ('application/zip', 'zip', '.zip', None)),
60 60 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
61 61 ('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
62 62 ))
63 63
64 64 def getstyle(req, configfn, templatepath):
65 65 fromreq = req.form.get('style', [None])[0]
66 66 if fromreq is not None:
67 67 fromreq = pycompat.sysbytes(fromreq)
68 68 styles = (
69 69 fromreq,
70 70 configfn('web', 'style'),
71 71 'paper',
72 72 )
73 73 return styles, templater.stylemap(styles, templatepath)
74 74
75 75 def makebreadcrumb(url, prefix=''):
76 76 '''Return a 'URL breadcrumb' list
77 77
78 78 A 'URL breadcrumb' is a list of URL-name pairs,
79 79 corresponding to each of the path items on a URL.
80 80 This can be used to create path navigation entries.
81 81 '''
82 82 if url.endswith('/'):
83 83 url = url[:-1]
84 84 if prefix:
85 85 url = '/' + prefix + url
86 86 relpath = url
87 87 if relpath.startswith('/'):
88 88 relpath = relpath[1:]
89 89
90 90 breadcrumb = []
91 91 urlel = url
92 92 pathitems = [''] + relpath.split('/')
93 93 for pathel in reversed(pathitems):
94 94 if not pathel or not urlel:
95 95 break
96 96 breadcrumb.append({'url': urlel, 'name': pathel})
97 97 urlel = os.path.dirname(urlel)
98 98 return reversed(breadcrumb)
99 99
100 100 class requestcontext(object):
101 101 """Holds state/context for an individual request.
102 102
103 103 Servers can be multi-threaded. Holding state on the WSGI application
104 104 is prone to race conditions. Instances of this class exist to hold
105 105 mutable and race-free state for requests.
106 106 """
107 107 def __init__(self, app, repo):
108 108 self.repo = repo
109 109 self.reponame = app.reponame
110 110
111 111 self.archivespecs = archivespecs
112 112
113 113 self.maxchanges = self.configint('web', 'maxchanges')
114 114 self.stripecount = self.configint('web', 'stripes')
115 115 self.maxshortchanges = self.configint('web', 'maxshortchanges')
116 116 self.maxfiles = self.configint('web', 'maxfiles')
117 117 self.allowpull = self.configbool('web', 'allowpull')
118 118
119 119 # we use untrusted=False to prevent a repo owner from using
120 120 # web.templates in .hg/hgrc to get access to any file readable
121 121 # by the user running the CGI script
122 122 self.templatepath = self.config('web', 'templates', untrusted=False)
123 123
124 124 # This object is more expensive to build than simple config values.
125 125 # It is shared across requests. The app will replace the object
126 126 # if it is updated. Since this is a reference and nothing should
127 127 # modify the underlying object, it should be constant for the lifetime
128 128 # of the request.
129 129 self.websubtable = app.websubtable
130 130
131 131 self.csp, self.nonce = cspvalues(self.repo.ui)
132 132
133 133 # Trust the settings from the .hg/hgrc files by default.
134 134 def config(self, section, name, default=uimod._unset, untrusted=True):
135 135 return self.repo.ui.config(section, name, default,
136 136 untrusted=untrusted)
137 137
138 138 def configbool(self, section, name, default=uimod._unset, untrusted=True):
139 139 return self.repo.ui.configbool(section, name, default,
140 140 untrusted=untrusted)
141 141
142 142 def configint(self, section, name, default=uimod._unset, untrusted=True):
143 143 return self.repo.ui.configint(section, name, default,
144 144 untrusted=untrusted)
145 145
146 146 def configlist(self, section, name, default=uimod._unset, untrusted=True):
147 147 return self.repo.ui.configlist(section, name, default,
148 148 untrusted=untrusted)
149 149
150 150 def archivelist(self, nodeid):
151 151 allowed = self.configlist('web', 'allow_archive')
152 152 for typ, spec in self.archivespecs.iteritems():
153 153 if typ in allowed or self.configbool('web', 'allow%s' % typ):
154 154 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
155 155
156 156 def templater(self, req):
157 157 # determine scheme, port and server name
158 158 # this is needed to create absolute urls
159 159
160 160 proto = req.env.get('wsgi.url_scheme')
161 161 if proto == 'https':
162 162 proto = 'https'
163 163 default_port = '443'
164 164 else:
165 165 proto = 'http'
166 166 default_port = '80'
167 167
168 168 port = req.env['SERVER_PORT']
169 169 port = port != default_port and (':' + port) or ''
170 170 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
171 171 logourl = self.config('web', 'logourl', 'https://mercurial-scm.org/')
172 logoimg = self.config('web', 'logoimg', 'hglogo.png')
172 logoimg = self.config('web', 'logoimg')
173 173 staticurl = self.config('web', 'staticurl') or req.url + 'static/'
174 174 if not staticurl.endswith('/'):
175 175 staticurl += '/'
176 176
177 177 # some functions for the templater
178 178
179 179 def motd(**map):
180 180 yield self.config('web', 'motd')
181 181
182 182 # figure out which style to use
183 183
184 184 vars = {}
185 185 styles, (style, mapfile) = getstyle(req, self.config,
186 186 self.templatepath)
187 187 if style == styles[0]:
188 188 vars['style'] = style
189 189
190 190 start = req.url[-1] == '?' and '&' or '?'
191 191 sessionvars = webutil.sessionvars(vars, start)
192 192
193 193 if not self.reponame:
194 194 self.reponame = (self.config('web', 'name', '')
195 195 or req.env.get('REPO_NAME')
196 196 or req.url.strip('/') or self.repo.root)
197 197
198 198 def websubfilter(text):
199 199 return templatefilters.websub(text, self.websubtable)
200 200
201 201 # create the templater
202 202
203 203 defaults = {
204 204 'url': req.url,
205 205 'logourl': logourl,
206 206 'logoimg': logoimg,
207 207 'staticurl': staticurl,
208 208 'urlbase': urlbase,
209 209 'repo': self.reponame,
210 210 'encoding': encoding.encoding,
211 211 'motd': motd,
212 212 'sessionvars': sessionvars,
213 213 'pathdef': makebreadcrumb(req.url),
214 214 'style': style,
215 215 'nonce': self.nonce,
216 216 }
217 217 tmpl = templater.templater.frommapfile(mapfile,
218 218 filters={'websub': websubfilter},
219 219 defaults=defaults)
220 220 return tmpl
221 221
222 222
223 223 class hgweb(object):
224 224 """HTTP server for individual repositories.
225 225
226 226 Instances of this class serve HTTP responses for a particular
227 227 repository.
228 228
229 229 Instances are typically used as WSGI applications.
230 230
231 231 Some servers are multi-threaded. On these servers, there may
232 232 be multiple active threads inside __call__.
233 233 """
234 234 def __init__(self, repo, name=None, baseui=None):
235 235 if isinstance(repo, str):
236 236 if baseui:
237 237 u = baseui.copy()
238 238 else:
239 239 u = uimod.ui.load()
240 240 r = hg.repository(u, repo)
241 241 else:
242 242 # we trust caller to give us a private copy
243 243 r = repo
244 244
245 245 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
246 246 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
247 247 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
248 248 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
249 249 # resolve file patterns relative to repo root
250 250 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
251 251 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
252 252 # displaying bundling progress bar while serving feel wrong and may
253 253 # break some wsgi implementation.
254 254 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
255 255 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
256 256 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
257 257 self._lastrepo = self._repos[0]
258 258 hook.redirect(True)
259 259 self.reponame = name
260 260
261 261 def _webifyrepo(self, repo):
262 262 repo = getwebview(repo)
263 263 self.websubtable = webutil.getwebsubs(repo)
264 264 return repo
265 265
266 266 @contextlib.contextmanager
267 267 def _obtainrepo(self):
268 268 """Obtain a repo unique to the caller.
269 269
270 270 Internally we maintain a stack of cachedlocalrepo instances
271 271 to be handed out. If one is available, we pop it and return it,
272 272 ensuring it is up to date in the process. If one is not available,
273 273 we clone the most recently used repo instance and return it.
274 274
275 275 It is currently possible for the stack to grow without bounds
276 276 if the server allows infinite threads. However, servers should
277 277 have a thread limit, thus establishing our limit.
278 278 """
279 279 if self._repos:
280 280 cached = self._repos.pop()
281 281 r, created = cached.fetch()
282 282 else:
283 283 cached = self._lastrepo.copy()
284 284 r, created = cached.fetch()
285 285 if created:
286 286 r = self._webifyrepo(r)
287 287
288 288 self._lastrepo = cached
289 289 self.mtime = cached.mtime
290 290 try:
291 291 yield r
292 292 finally:
293 293 self._repos.append(cached)
294 294
295 295 def run(self):
296 296 """Start a server from CGI environment.
297 297
298 298 Modern servers should be using WSGI and should avoid this
299 299 method, if possible.
300 300 """
301 301 if not encoding.environ.get('GATEWAY_INTERFACE',
302 302 '').startswith("CGI/1."):
303 303 raise RuntimeError("This function is only intended to be "
304 304 "called while running as a CGI script.")
305 305 wsgicgi.launch(self)
306 306
307 307 def __call__(self, env, respond):
308 308 """Run the WSGI application.
309 309
310 310 This may be called by multiple threads.
311 311 """
312 312 req = wsgirequest(env, respond)
313 313 return self.run_wsgi(req)
314 314
315 315 def run_wsgi(self, req):
316 316 """Internal method to run the WSGI application.
317 317
318 318 This is typically only called by Mercurial. External consumers
319 319 should be using instances of this class as the WSGI application.
320 320 """
321 321 with self._obtainrepo() as repo:
322 322 profile = repo.ui.configbool('profiling', 'enabled')
323 323 with profiling.profile(repo.ui, enabled=profile):
324 324 for r in self._runwsgi(req, repo):
325 325 yield r
326 326
327 327 def _runwsgi(self, req, repo):
328 328 rctx = requestcontext(self, repo)
329 329
330 330 # This state is global across all threads.
331 331 encoding.encoding = rctx.config('web', 'encoding')
332 332 rctx.repo.ui.environ = req.env
333 333
334 334 if rctx.csp:
335 335 # hgwebdir may have added CSP header. Since we generate our own,
336 336 # replace it.
337 337 req.headers = [h for h in req.headers
338 338 if h[0] != 'Content-Security-Policy']
339 339 req.headers.append(('Content-Security-Policy', rctx.csp))
340 340
341 341 # work with CGI variables to create coherent structure
342 342 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
343 343
344 344 req.url = req.env['SCRIPT_NAME']
345 345 if not req.url.endswith('/'):
346 346 req.url += '/'
347 347 if req.env.get('REPO_NAME'):
348 348 req.url += req.env['REPO_NAME'] + '/'
349 349
350 350 if 'PATH_INFO' in req.env:
351 351 parts = req.env['PATH_INFO'].strip('/').split('/')
352 352 repo_parts = req.env.get('REPO_NAME', '').split('/')
353 353 if parts[:len(repo_parts)] == repo_parts:
354 354 parts = parts[len(repo_parts):]
355 355 query = '/'.join(parts)
356 356 else:
357 357 query = req.env['QUERY_STRING'].partition('&')[0]
358 358 query = query.partition(';')[0]
359 359
360 360 # process this if it's a protocol request
361 361 # protocol bits don't need to create any URLs
362 362 # and the clients always use the old URL structure
363 363
364 364 cmd = req.form.get('cmd', [''])[0]
365 365 if protocol.iscmd(cmd):
366 366 try:
367 367 if query:
368 368 raise ErrorResponse(HTTP_NOT_FOUND)
369 369 if cmd in perms:
370 370 self.check_perm(rctx, req, perms[cmd])
371 371 return protocol.call(rctx.repo, req, cmd)
372 372 except ErrorResponse as inst:
373 373 # A client that sends unbundle without 100-continue will
374 374 # break if we respond early.
375 375 if (cmd == 'unbundle' and
376 376 (req.env.get('HTTP_EXPECT',
377 377 '').lower() != '100-continue') or
378 378 req.env.get('X-HgHttp2', '')):
379 379 req.drain()
380 380 else:
381 381 req.headers.append(('Connection', 'Close'))
382 382 req.respond(inst, protocol.HGTYPE,
383 383 body='0\n%s\n' % inst)
384 384 return ''
385 385
386 386 # translate user-visible url structure to internal structure
387 387
388 388 args = query.split('/', 2)
389 389 if 'cmd' not in req.form and args and args[0]:
390 390
391 391 cmd = args.pop(0)
392 392 style = cmd.rfind('-')
393 393 if style != -1:
394 394 req.form['style'] = [cmd[:style]]
395 395 cmd = cmd[style + 1:]
396 396
397 397 # avoid accepting e.g. style parameter as command
398 398 if util.safehasattr(webcommands, cmd):
399 399 req.form['cmd'] = [cmd]
400 400
401 401 if cmd == 'static':
402 402 req.form['file'] = ['/'.join(args)]
403 403 else:
404 404 if args and args[0]:
405 405 node = args.pop(0).replace('%2F', '/')
406 406 req.form['node'] = [node]
407 407 if args:
408 408 req.form['file'] = args
409 409
410 410 ua = req.env.get('HTTP_USER_AGENT', '')
411 411 if cmd == 'rev' and 'mercurial' in ua:
412 412 req.form['style'] = ['raw']
413 413
414 414 if cmd == 'archive':
415 415 fn = req.form['node'][0]
416 416 for type_, spec in rctx.archivespecs.iteritems():
417 417 ext = spec[2]
418 418 if fn.endswith(ext):
419 419 req.form['node'] = [fn[:-len(ext)]]
420 420 req.form['type'] = [type_]
421 421
422 422 # process the web interface request
423 423
424 424 try:
425 425 tmpl = rctx.templater(req)
426 426 ctype = tmpl('mimetype', encoding=encoding.encoding)
427 427 ctype = templater.stringify(ctype)
428 428
429 429 # check read permissions non-static content
430 430 if cmd != 'static':
431 431 self.check_perm(rctx, req, None)
432 432
433 433 if cmd == '':
434 434 req.form['cmd'] = [tmpl.cache['default']]
435 435 cmd = req.form['cmd'][0]
436 436
437 437 # Don't enable caching if using a CSP nonce because then it wouldn't
438 438 # be a nonce.
439 439 if rctx.configbool('web', 'cache') and not rctx.nonce:
440 440 caching(self, req) # sets ETag header or raises NOT_MODIFIED
441 441 if cmd not in webcommands.__all__:
442 442 msg = 'no such method: %s' % cmd
443 443 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
444 444 elif cmd == 'file' and 'raw' in req.form.get('style', []):
445 445 rctx.ctype = ctype
446 446 content = webcommands.rawfile(rctx, req, tmpl)
447 447 else:
448 448 content = getattr(webcommands, cmd)(rctx, req, tmpl)
449 449 req.respond(HTTP_OK, ctype)
450 450
451 451 return content
452 452
453 453 except (error.LookupError, error.RepoLookupError) as err:
454 454 req.respond(HTTP_NOT_FOUND, ctype)
455 455 msg = str(err)
456 456 if (util.safehasattr(err, 'name') and
457 457 not isinstance(err, error.ManifestLookupError)):
458 458 msg = 'revision not found: %s' % err.name
459 459 return tmpl('error', error=msg)
460 460 except (error.RepoError, error.RevlogError) as inst:
461 461 req.respond(HTTP_SERVER_ERROR, ctype)
462 462 return tmpl('error', error=str(inst))
463 463 except ErrorResponse as inst:
464 464 req.respond(inst, ctype)
465 465 if inst.code == HTTP_NOT_MODIFIED:
466 466 # Not allowed to return a body on a 304
467 467 return ['']
468 468 return tmpl('error', error=str(inst))
469 469
470 470 def check_perm(self, rctx, req, op):
471 471 for permhook in permhooks:
472 472 permhook(rctx, req, op)
473 473
474 474 def getwebview(repo):
475 475 """The 'web.view' config controls changeset filter to hgweb. Possible
476 476 values are ``served``, ``visible`` and ``all``. Default is ``served``.
477 477 The ``served`` filter only shows changesets that can be pulled from the
478 478 hgweb instance. The``visible`` filter includes secret changesets but
479 479 still excludes "hidden" one.
480 480
481 481 See the repoview module for details.
482 482
483 483 The option has been around undocumented since Mercurial 2.5, but no
484 484 user ever asked about it. So we better keep it undocumented for now."""
485 485 # experimental config: web.view
486 486 viewconfig = repo.ui.config('web', 'view', untrusted=True)
487 487 if viewconfig == 'all':
488 488 return repo.unfiltered()
489 489 elif viewconfig in repoview.filtertable:
490 490 return repo.filtered(viewconfig)
491 491 else:
492 492 return repo.filtered('served')
@@ -1,538 +1,538 b''
1 1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.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 os
12 12 import re
13 13 import time
14 14
15 15 from ..i18n import _
16 16
17 17 from .common import (
18 18 ErrorResponse,
19 19 HTTP_NOT_FOUND,
20 20 HTTP_OK,
21 21 HTTP_SERVER_ERROR,
22 22 cspvalues,
23 23 get_contact,
24 24 get_mtime,
25 25 ismember,
26 26 paritygen,
27 27 staticfile,
28 28 )
29 29 from .request import wsgirequest
30 30
31 31 from .. import (
32 32 configitems,
33 33 encoding,
34 34 error,
35 35 hg,
36 36 profiling,
37 37 pycompat,
38 38 scmutil,
39 39 templater,
40 40 ui as uimod,
41 41 util,
42 42 )
43 43
44 44 from . import (
45 45 hgweb_mod,
46 46 webutil,
47 47 wsgicgi,
48 48 )
49 49
50 50 def cleannames(items):
51 51 return [(util.pconvert(name).strip('/'), path) for name, path in items]
52 52
53 53 def findrepos(paths):
54 54 repos = []
55 55 for prefix, root in cleannames(paths):
56 56 roothead, roottail = os.path.split(root)
57 57 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
58 58 # /bar/ be served as as foo/N .
59 59 # '*' will not search inside dirs with .hg (except .hg/patches),
60 60 # '**' will search inside dirs with .hg (and thus also find subrepos).
61 61 try:
62 62 recurse = {'*': False, '**': True}[roottail]
63 63 except KeyError:
64 64 repos.append((prefix, root))
65 65 continue
66 66 roothead = os.path.normpath(os.path.abspath(roothead))
67 67 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
68 68 repos.extend(urlrepos(prefix, roothead, paths))
69 69 return repos
70 70
71 71 def urlrepos(prefix, roothead, paths):
72 72 """yield url paths and filesystem paths from a list of repo paths
73 73
74 74 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
75 75 >>> conv(urlrepos(b'hg', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
76 76 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
77 77 >>> conv(urlrepos(b'', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
78 78 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
79 79 """
80 80 for path in paths:
81 81 path = os.path.normpath(path)
82 82 yield (prefix + '/' +
83 83 util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
84 84
85 85 def geturlcgivars(baseurl, port):
86 86 """
87 87 Extract CGI variables from baseurl
88 88
89 89 >>> geturlcgivars(b"http://host.org/base", b"80")
90 90 ('host.org', '80', '/base')
91 91 >>> geturlcgivars(b"http://host.org:8000/base", b"80")
92 92 ('host.org', '8000', '/base')
93 93 >>> geturlcgivars(b'/base', 8000)
94 94 ('', '8000', '/base')
95 95 >>> geturlcgivars(b"base", b'8000')
96 96 ('', '8000', '/base')
97 97 >>> geturlcgivars(b"http://host", b'8000')
98 98 ('host', '8000', '/')
99 99 >>> geturlcgivars(b"http://host/", b'8000')
100 100 ('host', '8000', '/')
101 101 """
102 102 u = util.url(baseurl)
103 103 name = u.host or ''
104 104 if u.port:
105 105 port = u.port
106 106 path = u.path or ""
107 107 if not path.startswith('/'):
108 108 path = '/' + path
109 109
110 110 return name, pycompat.bytestr(port), path
111 111
112 112 class hgwebdir(object):
113 113 """HTTP server for multiple repositories.
114 114
115 115 Given a configuration, different repositories will be served depending
116 116 on the request path.
117 117
118 118 Instances are typically used as WSGI applications.
119 119 """
120 120 def __init__(self, conf, baseui=None):
121 121 self.conf = conf
122 122 self.baseui = baseui
123 123 self.ui = None
124 124 self.lastrefresh = 0
125 125 self.motd = None
126 126 self.refresh()
127 127
128 128 def refresh(self):
129 129 if self.ui:
130 130 refreshinterval = self.ui.configint('web', 'refreshinterval')
131 131 else:
132 132 item = configitems.coreitems['web']['refreshinterval']
133 133 refreshinterval = item.default
134 134
135 135 # refreshinterval <= 0 means to always refresh.
136 136 if (refreshinterval > 0 and
137 137 self.lastrefresh + refreshinterval > time.time()):
138 138 return
139 139
140 140 if self.baseui:
141 141 u = self.baseui.copy()
142 142 else:
143 143 u = uimod.ui.load()
144 144 u.setconfig('ui', 'report_untrusted', 'off', 'hgwebdir')
145 145 u.setconfig('ui', 'nontty', 'true', 'hgwebdir')
146 146 # displaying bundling progress bar while serving feels wrong and may
147 147 # break some wsgi implementations.
148 148 u.setconfig('progress', 'disable', 'true', 'hgweb')
149 149
150 150 if not isinstance(self.conf, (dict, list, tuple)):
151 151 map = {'paths': 'hgweb-paths'}
152 152 if not os.path.exists(self.conf):
153 153 raise error.Abort(_('config file %s not found!') % self.conf)
154 154 u.readconfig(self.conf, remap=map, trust=True)
155 155 paths = []
156 156 for name, ignored in u.configitems('hgweb-paths'):
157 157 for path in u.configlist('hgweb-paths', name):
158 158 paths.append((name, path))
159 159 elif isinstance(self.conf, (list, tuple)):
160 160 paths = self.conf
161 161 elif isinstance(self.conf, dict):
162 162 paths = self.conf.items()
163 163
164 164 repos = findrepos(paths)
165 165 for prefix, root in u.configitems('collections'):
166 166 prefix = util.pconvert(prefix)
167 167 for path in scmutil.walkrepos(root, followsym=True):
168 168 repo = os.path.normpath(path)
169 169 name = util.pconvert(repo)
170 170 if name.startswith(prefix):
171 171 name = name[len(prefix):]
172 172 repos.append((name.lstrip('/'), repo))
173 173
174 174 self.repos = repos
175 175 self.ui = u
176 176 encoding.encoding = self.ui.config('web', 'encoding')
177 177 self.style = self.ui.config('web', 'style')
178 178 self.templatepath = self.ui.config('web', 'templates', untrusted=False)
179 179 self.stripecount = self.ui.config('web', 'stripes')
180 180 if self.stripecount:
181 181 self.stripecount = int(self.stripecount)
182 182 self._baseurl = self.ui.config('web', 'baseurl')
183 183 prefix = self.ui.config('web', 'prefix')
184 184 if prefix.startswith('/'):
185 185 prefix = prefix[1:]
186 186 if prefix.endswith('/'):
187 187 prefix = prefix[:-1]
188 188 self.prefix = prefix
189 189 self.lastrefresh = time.time()
190 190
191 191 def run(self):
192 192 if not encoding.environ.get('GATEWAY_INTERFACE',
193 193 '').startswith("CGI/1."):
194 194 raise RuntimeError("This function is only intended to be "
195 195 "called while running as a CGI script.")
196 196 wsgicgi.launch(self)
197 197
198 198 def __call__(self, env, respond):
199 199 req = wsgirequest(env, respond)
200 200 return self.run_wsgi(req)
201 201
202 202 def read_allowed(self, ui, req):
203 203 """Check allow_read and deny_read config options of a repo's ui object
204 204 to determine user permissions. By default, with neither option set (or
205 205 both empty), allow all users to read the repo. There are two ways a
206 206 user can be denied read access: (1) deny_read is not empty, and the
207 207 user is unauthenticated or deny_read contains user (or *), and (2)
208 208 allow_read is not empty and the user is not in allow_read. Return True
209 209 if user is allowed to read the repo, else return False."""
210 210
211 211 user = req.env.get('REMOTE_USER')
212 212
213 213 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
214 214 if deny_read and (not user or ismember(ui, user, deny_read)):
215 215 return False
216 216
217 217 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
218 218 # by default, allow reading if no allow_read option has been set
219 219 if (not allow_read) or ismember(ui, user, allow_read):
220 220 return True
221 221
222 222 return False
223 223
224 224 def run_wsgi(self, req):
225 225 profile = self.ui.configbool('profiling', 'enabled')
226 226 with profiling.profile(self.ui, enabled=profile):
227 227 for r in self._runwsgi(req):
228 228 yield r
229 229
230 230 def _runwsgi(self, req):
231 231 try:
232 232 self.refresh()
233 233
234 234 csp, nonce = cspvalues(self.ui)
235 235 if csp:
236 236 req.headers.append(('Content-Security-Policy', csp))
237 237
238 238 virtual = req.env.get("PATH_INFO", "").strip('/')
239 239 tmpl = self.templater(req, nonce)
240 240 ctype = tmpl('mimetype', encoding=encoding.encoding)
241 241 ctype = templater.stringify(ctype)
242 242
243 243 # a static file
244 244 if virtual.startswith('static/') or 'static' in req.form:
245 245 if virtual.startswith('static/'):
246 246 fname = virtual[7:]
247 247 else:
248 248 fname = req.form['static'][0]
249 249 static = self.ui.config("web", "static", None,
250 250 untrusted=False)
251 251 if not static:
252 252 tp = self.templatepath or templater.templatepaths()
253 253 if isinstance(tp, str):
254 254 tp = [tp]
255 255 static = [os.path.join(p, 'static') for p in tp]
256 256 staticfile(static, fname, req)
257 257 return []
258 258
259 259 # top-level index
260 260
261 261 repos = dict(self.repos)
262 262
263 263 if (not virtual or virtual == 'index') and virtual not in repos:
264 264 req.respond(HTTP_OK, ctype)
265 265 return self.makeindex(req, tmpl)
266 266
267 267 # nested indexes and hgwebs
268 268
269 269 if virtual.endswith('/index') and virtual not in repos:
270 270 subdir = virtual[:-len('index')]
271 271 if any(r.startswith(subdir) for r in repos):
272 272 req.respond(HTTP_OK, ctype)
273 273 return self.makeindex(req, tmpl, subdir)
274 274
275 275 def _virtualdirs():
276 276 # Check the full virtual path, each parent, and the root ('')
277 277 if virtual != '':
278 278 yield virtual
279 279
280 280 for p in util.finddirs(virtual):
281 281 yield p
282 282
283 283 yield ''
284 284
285 285 for virtualrepo in _virtualdirs():
286 286 real = repos.get(virtualrepo)
287 287 if real:
288 288 req.env['REPO_NAME'] = virtualrepo
289 289 try:
290 290 # ensure caller gets private copy of ui
291 291 repo = hg.repository(self.ui.copy(), real)
292 292 return hgweb_mod.hgweb(repo).run_wsgi(req)
293 293 except IOError as inst:
294 294 msg = encoding.strtolocal(inst.strerror)
295 295 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
296 296 except error.RepoError as inst:
297 297 raise ErrorResponse(HTTP_SERVER_ERROR, bytes(inst))
298 298
299 299 # browse subdirectories
300 300 subdir = virtual + '/'
301 301 if [r for r in repos if r.startswith(subdir)]:
302 302 req.respond(HTTP_OK, ctype)
303 303 return self.makeindex(req, tmpl, subdir)
304 304
305 305 # prefixes not found
306 306 req.respond(HTTP_NOT_FOUND, ctype)
307 307 return tmpl("notfound", repo=virtual)
308 308
309 309 except ErrorResponse as err:
310 310 req.respond(err, ctype)
311 311 return tmpl('error', error=err.message or '')
312 312 finally:
313 313 tmpl = None
314 314
315 315 def makeindex(self, req, tmpl, subdir=""):
316 316
317 317 def archivelist(ui, nodeid, url):
318 318 allowed = ui.configlist("web", "allow_archive", untrusted=True)
319 319 archives = []
320 320 for typ, spec in hgweb_mod.archivespecs.iteritems():
321 321 if typ in allowed or ui.configbool("web", "allow" + typ,
322 322 untrusted=True):
323 323 archives.append({"type": typ, "extension": spec[2],
324 324 "node": nodeid, "url": url})
325 325 return archives
326 326
327 327 def rawentries(subdir="", **map):
328 328
329 329 descend = self.ui.configbool('web', 'descend')
330 330 collapse = self.ui.configbool('web', 'collapse')
331 331 seenrepos = set()
332 332 seendirs = set()
333 333 for name, path in self.repos:
334 334
335 335 if not name.startswith(subdir):
336 336 continue
337 337 name = name[len(subdir):]
338 338 directory = False
339 339
340 340 if '/' in name:
341 341 if not descend:
342 342 continue
343 343
344 344 nameparts = name.split('/')
345 345 rootname = nameparts[0]
346 346
347 347 if not collapse:
348 348 pass
349 349 elif rootname in seendirs:
350 350 continue
351 351 elif rootname in seenrepos:
352 352 pass
353 353 else:
354 354 directory = True
355 355 name = rootname
356 356
357 357 # redefine the path to refer to the directory
358 358 discarded = '/'.join(nameparts[1:])
359 359
360 360 # remove name parts plus accompanying slash
361 361 path = path[:-len(discarded) - 1]
362 362
363 363 try:
364 364 r = hg.repository(self.ui, path)
365 365 directory = False
366 366 except (IOError, error.RepoError):
367 367 pass
368 368
369 369 parts = [name]
370 370 parts.insert(0, '/' + subdir.rstrip('/'))
371 371 if req.env['SCRIPT_NAME']:
372 372 parts.insert(0, req.env['SCRIPT_NAME'])
373 373 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
374 374
375 375 # show either a directory entry or a repository
376 376 if directory:
377 377 # get the directory's time information
378 378 try:
379 379 d = (get_mtime(path), util.makedate()[1])
380 380 except OSError:
381 381 continue
382 382
383 383 # add '/' to the name to make it obvious that
384 384 # the entry is a directory, not a regular repository
385 385 row = {'contact': "",
386 386 'contact_sort': "",
387 387 'name': name + '/',
388 388 'name_sort': name,
389 389 'url': url,
390 390 'description': "",
391 391 'description_sort': "",
392 392 'lastchange': d,
393 393 'lastchange_sort': d[1]-d[0],
394 394 'archives': [],
395 395 'isdirectory': True,
396 396 'labels': [],
397 397 }
398 398
399 399 seendirs.add(name)
400 400 yield row
401 401 continue
402 402
403 403 u = self.ui.copy()
404 404 try:
405 405 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
406 406 except Exception as e:
407 407 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
408 408 continue
409 409 def get(section, name, default=uimod._unset):
410 410 return u.config(section, name, default, untrusted=True)
411 411
412 412 if u.configbool("web", "hidden", untrusted=True):
413 413 continue
414 414
415 415 if not self.read_allowed(u, req):
416 416 continue
417 417
418 418 # update time with local timezone
419 419 try:
420 420 r = hg.repository(self.ui, path)
421 421 except IOError:
422 422 u.warn(_('error accessing repository at %s\n') % path)
423 423 continue
424 424 except error.RepoError:
425 425 u.warn(_('error accessing repository at %s\n') % path)
426 426 continue
427 427 try:
428 428 d = (get_mtime(r.spath), util.makedate()[1])
429 429 except OSError:
430 430 continue
431 431
432 432 contact = get_contact(get)
433 433 description = get("web", "description")
434 434 seenrepos.add(name)
435 435 name = get("web", "name", name)
436 436 row = {'contact': contact or "unknown",
437 437 'contact_sort': contact.upper() or "unknown",
438 438 'name': name,
439 439 'name_sort': name,
440 440 'url': url,
441 441 'description': description or "unknown",
442 442 'description_sort': description.upper() or "unknown",
443 443 'lastchange': d,
444 444 'lastchange_sort': d[1]-d[0],
445 445 'archives': archivelist(u, "tip", url),
446 446 'isdirectory': None,
447 447 'labels': u.configlist('web', 'labels', untrusted=True),
448 448 }
449 449
450 450 yield row
451 451
452 452 sortdefault = None, False
453 453 def entries(sortcolumn="", descending=False, subdir="", **map):
454 454 rows = rawentries(subdir=subdir, **map)
455 455
456 456 if sortcolumn and sortdefault != (sortcolumn, descending):
457 457 sortkey = '%s_sort' % sortcolumn
458 458 rows = sorted(rows, key=lambda x: x[sortkey],
459 459 reverse=descending)
460 460 for row, parity in zip(rows, paritygen(self.stripecount)):
461 461 row['parity'] = parity
462 462 yield row
463 463
464 464 self.refresh()
465 465 sortable = ["name", "description", "contact", "lastchange"]
466 466 sortcolumn, descending = sortdefault
467 467 if 'sort' in req.form:
468 468 sortcolumn = req.form['sort'][0]
469 469 descending = sortcolumn.startswith('-')
470 470 if descending:
471 471 sortcolumn = sortcolumn[1:]
472 472 if sortcolumn not in sortable:
473 473 sortcolumn = ""
474 474
475 475 sort = [("sort_%s" % column,
476 476 "%s%s" % ((not descending and column == sortcolumn)
477 477 and "-" or "", column))
478 478 for column in sortable]
479 479
480 480 self.refresh()
481 481 self.updatereqenv(req.env)
482 482
483 483 return tmpl("index", entries=entries, subdir=subdir,
484 484 pathdef=hgweb_mod.makebreadcrumb('/' + subdir, self.prefix),
485 485 sortcolumn=sortcolumn, descending=descending,
486 486 **dict(sort))
487 487
488 488 def templater(self, req, nonce):
489 489
490 490 def motd(**map):
491 491 if self.motd is not None:
492 492 yield self.motd
493 493 else:
494 494 yield config('web', 'motd')
495 495
496 496 def config(section, name, default=uimod._unset, untrusted=True):
497 497 return self.ui.config(section, name, default, untrusted)
498 498
499 499 self.updatereqenv(req.env)
500 500
501 501 url = req.env.get('SCRIPT_NAME', '')
502 502 if not url.endswith('/'):
503 503 url += '/'
504 504
505 505 vars = {}
506 506 styles, (style, mapfile) = hgweb_mod.getstyle(req, config,
507 507 self.templatepath)
508 508 if style == styles[0]:
509 509 vars['style'] = style
510 510
511 511 start = url[-1] == '?' and '&' or '?'
512 512 sessionvars = webutil.sessionvars(vars, start)
513 513 logourl = config('web', 'logourl', 'https://mercurial-scm.org/')
514 logoimg = config('web', 'logoimg', 'hglogo.png')
514 logoimg = config('web', 'logoimg')
515 515 staticurl = config('web', 'staticurl') or url + 'static/'
516 516 if not staticurl.endswith('/'):
517 517 staticurl += '/'
518 518
519 519 defaults = {
520 520 "encoding": encoding.encoding,
521 521 "motd": motd,
522 522 "url": url,
523 523 "logourl": logourl,
524 524 "logoimg": logoimg,
525 525 "staticurl": staticurl,
526 526 "sessionvars": sessionvars,
527 527 "style": style,
528 528 "nonce": nonce,
529 529 }
530 530 tmpl = templater.templater.frommapfile(mapfile, defaults=defaults)
531 531 return tmpl
532 532
533 533 def updatereqenv(self, env):
534 534 if self._baseurl is not None:
535 535 name, port, path = geturlcgivars(self._baseurl, env['SERVER_PORT'])
536 536 env['SERVER_NAME'] = name
537 537 env['SERVER_PORT'] = port
538 538 env['SCRIPT_NAME'] = path
General Comments 0
You need to be logged in to leave comments. Login now