##// END OF EJS Templates
configitems: register the 'web.style' config
Boris Feld -
r34243:db63872e default
parent child Browse files
Show More
@@ -1,659 +1,662 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('devel', 'all-warnings',
111 111 default=False,
112 112 )
113 113 coreconfigitem('devel', 'bundle2.debug',
114 114 default=False,
115 115 )
116 116 coreconfigitem('devel', 'check-locks',
117 117 default=False,
118 118 )
119 119 coreconfigitem('devel', 'check-relroot',
120 120 default=False,
121 121 )
122 122 coreconfigitem('devel', 'default-date',
123 123 default=None,
124 124 )
125 125 coreconfigitem('devel', 'deprec-warn',
126 126 default=False,
127 127 )
128 128 coreconfigitem('devel', 'disableloaddefaultcerts',
129 129 default=False,
130 130 )
131 131 coreconfigitem('devel', 'legacy.exchange',
132 132 default=list,
133 133 )
134 134 coreconfigitem('devel', 'servercafile',
135 135 default='',
136 136 )
137 137 coreconfigitem('devel', 'serverexactprotocol',
138 138 default='',
139 139 )
140 140 coreconfigitem('devel', 'serverrequirecert',
141 141 default=False,
142 142 )
143 143 coreconfigitem('devel', 'strip-obsmarkers',
144 144 default=True,
145 145 )
146 146 coreconfigitem('email', 'charsets',
147 147 default=list,
148 148 )
149 149 coreconfigitem('email', 'method',
150 150 default='smtp',
151 151 )
152 152 coreconfigitem('experimental', 'bundle-phases',
153 153 default=False,
154 154 )
155 155 coreconfigitem('experimental', 'bundle2-advertise',
156 156 default=True,
157 157 )
158 158 coreconfigitem('experimental', 'bundle2-output-capture',
159 159 default=False,
160 160 )
161 161 coreconfigitem('experimental', 'bundle2.pushback',
162 162 default=False,
163 163 )
164 164 coreconfigitem('experimental', 'bundle2lazylocking',
165 165 default=False,
166 166 )
167 167 coreconfigitem('experimental', 'bundlecomplevel',
168 168 default=None,
169 169 )
170 170 coreconfigitem('experimental', 'changegroup3',
171 171 default=False,
172 172 )
173 173 coreconfigitem('experimental', 'clientcompressionengines',
174 174 default=list,
175 175 )
176 176 coreconfigitem('experimental', 'copytrace',
177 177 default='on',
178 178 )
179 179 coreconfigitem('experimental', 'crecordtest',
180 180 default=None,
181 181 )
182 182 coreconfigitem('experimental', 'editortmpinhg',
183 183 default=False,
184 184 )
185 185 coreconfigitem('experimental', 'stabilization',
186 186 default=list,
187 187 alias=[('experimental', 'evolution')],
188 188 )
189 189 coreconfigitem('experimental', 'stabilization.bundle-obsmarker',
190 190 default=False,
191 191 alias=[('experimental', 'evolution.bundle-obsmarker')],
192 192 )
193 193 coreconfigitem('experimental', 'stabilization.track-operation',
194 194 default=False,
195 195 alias=[('experimental', 'evolution.track-operation')]
196 196 )
197 197 coreconfigitem('experimental', 'exportableenviron',
198 198 default=list,
199 199 )
200 200 coreconfigitem('experimental', 'extendedheader.index',
201 201 default=None,
202 202 )
203 203 coreconfigitem('experimental', 'extendedheader.similarity',
204 204 default=False,
205 205 )
206 206 coreconfigitem('experimental', 'format.compression',
207 207 default='zlib',
208 208 )
209 209 coreconfigitem('experimental', 'graphshorten',
210 210 default=False,
211 211 )
212 212 coreconfigitem('experimental', 'hook-track-tags',
213 213 default=False,
214 214 )
215 215 coreconfigitem('experimental', 'httppostargs',
216 216 default=False,
217 217 )
218 218 coreconfigitem('experimental', 'manifestv2',
219 219 default=False,
220 220 )
221 221 coreconfigitem('experimental', 'mergedriver',
222 222 default=None,
223 223 )
224 224 coreconfigitem('experimental', 'obsmarkers-exchange-debug',
225 225 default=False,
226 226 )
227 227 coreconfigitem('experimental', 'rebase.multidest',
228 228 default=False,
229 229 )
230 230 coreconfigitem('experimental', 'revertalternateinteractivemode',
231 231 default=True,
232 232 )
233 233 coreconfigitem('experimental', 'revlogv2',
234 234 default=None,
235 235 )
236 236 coreconfigitem('experimental', 'spacemovesdown',
237 237 default=False,
238 238 )
239 239 coreconfigitem('experimental', 'treemanifest',
240 240 default=False,
241 241 )
242 242 coreconfigitem('experimental', 'updatecheck',
243 243 default=None,
244 244 )
245 245 coreconfigitem('format', 'aggressivemergedeltas',
246 246 default=False,
247 247 )
248 248 coreconfigitem('format', 'chunkcachesize',
249 249 default=None,
250 250 )
251 251 coreconfigitem('format', 'dotencode',
252 252 default=True,
253 253 )
254 254 coreconfigitem('format', 'generaldelta',
255 255 default=False,
256 256 )
257 257 coreconfigitem('format', 'manifestcachesize',
258 258 default=None,
259 259 )
260 260 coreconfigitem('format', 'maxchainlen',
261 261 default=None,
262 262 )
263 263 coreconfigitem('format', 'obsstore-version',
264 264 default=None,
265 265 )
266 266 coreconfigitem('format', 'usefncache',
267 267 default=True,
268 268 )
269 269 coreconfigitem('format', 'usegeneraldelta',
270 270 default=True,
271 271 )
272 272 coreconfigitem('format', 'usestore',
273 273 default=True,
274 274 )
275 275 coreconfigitem('hostsecurity', 'ciphers',
276 276 default=None,
277 277 )
278 278 coreconfigitem('hostsecurity', 'disabletls10warning',
279 279 default=False,
280 280 )
281 281 coreconfigitem('http_proxy', 'always',
282 282 default=False,
283 283 )
284 284 coreconfigitem('http_proxy', 'host',
285 285 default=None,
286 286 )
287 287 coreconfigitem('http_proxy', 'no',
288 288 default=list,
289 289 )
290 290 coreconfigitem('http_proxy', 'passwd',
291 291 default=None,
292 292 )
293 293 coreconfigitem('http_proxy', 'user',
294 294 default=None,
295 295 )
296 296 coreconfigitem('merge', 'followcopies',
297 297 default=True,
298 298 )
299 299 coreconfigitem('pager', 'ignore',
300 300 default=list,
301 301 )
302 302 coreconfigitem('patch', 'eol',
303 303 default='strict',
304 304 )
305 305 coreconfigitem('patch', 'fuzz',
306 306 default=2,
307 307 )
308 308 coreconfigitem('paths', 'default',
309 309 default=None,
310 310 )
311 311 coreconfigitem('paths', 'default-push',
312 312 default=None,
313 313 )
314 314 coreconfigitem('phases', 'checksubrepos',
315 315 default='follow',
316 316 )
317 317 coreconfigitem('phases', 'publish',
318 318 default=True,
319 319 )
320 320 coreconfigitem('profiling', 'enabled',
321 321 default=False,
322 322 )
323 323 coreconfigitem('profiling', 'format',
324 324 default='text',
325 325 )
326 326 coreconfigitem('profiling', 'freq',
327 327 default=1000,
328 328 )
329 329 coreconfigitem('profiling', 'limit',
330 330 default=30,
331 331 )
332 332 coreconfigitem('profiling', 'nested',
333 333 default=0,
334 334 )
335 335 coreconfigitem('profiling', 'sort',
336 336 default='inlinetime',
337 337 )
338 338 coreconfigitem('profiling', 'statformat',
339 339 default='hotpath',
340 340 )
341 341 coreconfigitem('progress', 'assume-tty',
342 342 default=False,
343 343 )
344 344 coreconfigitem('progress', 'changedelay',
345 345 default=1,
346 346 )
347 347 coreconfigitem('progress', 'clear-complete',
348 348 default=True,
349 349 )
350 350 coreconfigitem('progress', 'debug',
351 351 default=False,
352 352 )
353 353 coreconfigitem('progress', 'delay',
354 354 default=3,
355 355 )
356 356 coreconfigitem('progress', 'disable',
357 357 default=False,
358 358 )
359 359 coreconfigitem('progress', 'estimate',
360 360 default=2,
361 361 )
362 362 coreconfigitem('progress', 'refresh',
363 363 default=0.1,
364 364 )
365 365 coreconfigitem('progress', 'width',
366 366 default=dynamicdefault,
367 367 )
368 368 coreconfigitem('push', 'pushvars.server',
369 369 default=False,
370 370 )
371 371 coreconfigitem('server', 'bundle1',
372 372 default=True,
373 373 )
374 374 coreconfigitem('server', 'bundle1gd',
375 375 default=None,
376 376 )
377 377 coreconfigitem('server', 'compressionengines',
378 378 default=list,
379 379 )
380 380 coreconfigitem('server', 'concurrent-push-mode',
381 381 default='strict',
382 382 )
383 383 coreconfigitem('server', 'disablefullbundle',
384 384 default=False,
385 385 )
386 386 coreconfigitem('server', 'maxhttpheaderlen',
387 387 default=1024,
388 388 )
389 389 coreconfigitem('server', 'preferuncompressed',
390 390 default=False,
391 391 )
392 392 coreconfigitem('server', 'uncompressed',
393 393 default=True,
394 394 )
395 395 coreconfigitem('server', 'uncompressedallowsecret',
396 396 default=False,
397 397 )
398 398 coreconfigitem('server', 'validate',
399 399 default=False,
400 400 )
401 401 coreconfigitem('server', 'zliblevel',
402 402 default=-1,
403 403 )
404 404 coreconfigitem('smtp', 'host',
405 405 default=None,
406 406 )
407 407 coreconfigitem('smtp', 'local_hostname',
408 408 default=None,
409 409 )
410 410 coreconfigitem('smtp', 'password',
411 411 default=None,
412 412 )
413 413 coreconfigitem('smtp', 'tls',
414 414 default='none',
415 415 )
416 416 coreconfigitem('smtp', 'username',
417 417 default=None,
418 418 )
419 419 coreconfigitem('sparse', 'missingwarning',
420 420 default=True,
421 421 )
422 422 coreconfigitem('trusted', 'groups',
423 423 default=list,
424 424 )
425 425 coreconfigitem('trusted', 'users',
426 426 default=list,
427 427 )
428 428 coreconfigitem('ui', '_usedassubrepo',
429 429 default=False,
430 430 )
431 431 coreconfigitem('ui', 'allowemptycommit',
432 432 default=False,
433 433 )
434 434 coreconfigitem('ui', 'archivemeta',
435 435 default=True,
436 436 )
437 437 coreconfigitem('ui', 'askusername',
438 438 default=False,
439 439 )
440 440 coreconfigitem('ui', 'clonebundlefallback',
441 441 default=False,
442 442 )
443 443 coreconfigitem('ui', 'clonebundleprefers',
444 444 default=list,
445 445 )
446 446 coreconfigitem('ui', 'clonebundles',
447 447 default=True,
448 448 )
449 449 coreconfigitem('ui', 'color',
450 450 default='auto',
451 451 )
452 452 coreconfigitem('ui', 'commitsubrepos',
453 453 default=False,
454 454 )
455 455 coreconfigitem('ui', 'debug',
456 456 default=False,
457 457 )
458 458 coreconfigitem('ui', 'debugger',
459 459 default=None,
460 460 )
461 461 coreconfigitem('ui', 'fallbackencoding',
462 462 default=None,
463 463 )
464 464 coreconfigitem('ui', 'forcecwd',
465 465 default=None,
466 466 )
467 467 coreconfigitem('ui', 'forcemerge',
468 468 default=None,
469 469 )
470 470 coreconfigitem('ui', 'formatdebug',
471 471 default=False,
472 472 )
473 473 coreconfigitem('ui', 'formatjson',
474 474 default=False,
475 475 )
476 476 coreconfigitem('ui', 'formatted',
477 477 default=None,
478 478 )
479 479 coreconfigitem('ui', 'graphnodetemplate',
480 480 default=None,
481 481 )
482 482 coreconfigitem('ui', 'http2debuglevel',
483 483 default=None,
484 484 )
485 485 coreconfigitem('ui', 'interactive',
486 486 default=None,
487 487 )
488 488 coreconfigitem('ui', 'interface',
489 489 default=None,
490 490 )
491 491 coreconfigitem('ui', 'logblockedtimes',
492 492 default=False,
493 493 )
494 494 coreconfigitem('ui', 'logtemplate',
495 495 default=None,
496 496 )
497 497 coreconfigitem('ui', 'merge',
498 498 default=None,
499 499 )
500 500 coreconfigitem('ui', 'mergemarkers',
501 501 default='basic',
502 502 )
503 503 coreconfigitem('ui', 'mergemarkertemplate',
504 504 default=('{node|short} '
505 505 '{ifeq(tags, "tip", "", '
506 506 'ifeq(tags, "", "", "{tags} "))}'
507 507 '{if(bookmarks, "{bookmarks} ")}'
508 508 '{ifeq(branch, "default", "", "{branch} ")}'
509 509 '- {author|user}: {desc|firstline}')
510 510 )
511 511 coreconfigitem('ui', 'nontty',
512 512 default=False,
513 513 )
514 514 coreconfigitem('ui', 'origbackuppath',
515 515 default=None,
516 516 )
517 517 coreconfigitem('ui', 'paginate',
518 518 default=True,
519 519 )
520 520 coreconfigitem('ui', 'patch',
521 521 default=None,
522 522 )
523 523 coreconfigitem('ui', 'portablefilenames',
524 524 default='warn',
525 525 )
526 526 coreconfigitem('ui', 'promptecho',
527 527 default=False,
528 528 )
529 529 coreconfigitem('ui', 'quiet',
530 530 default=False,
531 531 )
532 532 coreconfigitem('ui', 'quietbookmarkmove',
533 533 default=False,
534 534 )
535 535 coreconfigitem('ui', 'remotecmd',
536 536 default='hg',
537 537 )
538 538 coreconfigitem('ui', 'report_untrusted',
539 539 default=True,
540 540 )
541 541 coreconfigitem('ui', 'rollback',
542 542 default=True,
543 543 )
544 544 coreconfigitem('ui', 'slash',
545 545 default=False,
546 546 )
547 547 coreconfigitem('ui', 'ssh',
548 548 default='ssh',
549 549 )
550 550 coreconfigitem('ui', 'statuscopies',
551 551 default=False,
552 552 )
553 553 coreconfigitem('ui', 'strict',
554 554 default=False,
555 555 )
556 556 coreconfigitem('ui', 'style',
557 557 default='',
558 558 )
559 559 coreconfigitem('ui', 'supportcontact',
560 560 default=None,
561 561 )
562 562 coreconfigitem('ui', 'textwidth',
563 563 default=78,
564 564 )
565 565 coreconfigitem('ui', 'timeout',
566 566 default='600',
567 567 )
568 568 coreconfigitem('ui', 'traceback',
569 569 default=False,
570 570 )
571 571 coreconfigitem('ui', 'tweakdefaults',
572 572 default=False,
573 573 )
574 574 coreconfigitem('ui', 'usehttp2',
575 575 default=False,
576 576 )
577 577 coreconfigitem('ui', 'username',
578 578 alias=[('ui', 'user')]
579 579 )
580 580 coreconfigitem('ui', 'verbose',
581 581 default=False,
582 582 )
583 583 coreconfigitem('verify', 'skipflags',
584 584 default=None,
585 585 )
586 586 coreconfigitem('web', 'accesslog',
587 587 default='-',
588 588 )
589 589 coreconfigitem('web', 'address',
590 590 default='',
591 591 )
592 592 coreconfigitem('web', 'allow_archive',
593 593 default=list,
594 594 )
595 595 coreconfigitem('web', 'allow_read',
596 596 default=list,
597 597 )
598 598 coreconfigitem('web', 'baseurl',
599 599 default=None,
600 600 )
601 601 coreconfigitem('web', 'cacerts',
602 602 default=None,
603 603 )
604 604 coreconfigitem('web', 'certificate',
605 605 default=None,
606 606 )
607 607 coreconfigitem('web', 'collapse',
608 608 default=False,
609 609 )
610 610 coreconfigitem('web', 'csp',
611 611 default=None,
612 612 )
613 613 coreconfigitem('web', 'deny_read',
614 614 default=list,
615 615 )
616 616 coreconfigitem('web', 'descend',
617 617 default=True,
618 618 )
619 619 coreconfigitem('web', 'description',
620 620 default="",
621 621 )
622 622 coreconfigitem('web', 'encoding',
623 623 default=lambda: encoding.encoding,
624 624 )
625 625 coreconfigitem('web', 'errorlog',
626 626 default='-',
627 627 )
628 628 coreconfigitem('web', 'ipv6',
629 629 default=False,
630 630 )
631 631 coreconfigitem('web', 'port',
632 632 default=8000,
633 633 )
634 634 coreconfigitem('web', 'prefix',
635 635 default='',
636 636 )
637 637 coreconfigitem('web', 'refreshinterval',
638 638 default=20,
639 639 )
640 640 coreconfigitem('web', 'stripes',
641 641 default=1,
642 642 )
643 coreconfigitem('web', 'style',
644 default='paper',
645 )
643 646 coreconfigitem('worker', 'backgroundclose',
644 647 default=dynamicdefault,
645 648 )
646 649 # Windows defaults to a limit of 512 open files. A buffer of 128
647 650 # should give us enough headway.
648 651 coreconfigitem('worker', 'backgroundclosemaxqueue',
649 652 default=384,
650 653 )
651 654 coreconfigitem('worker', 'backgroundcloseminfilecount',
652 655 default=2048,
653 656 )
654 657 coreconfigitem('worker', 'backgroundclosethreadcount',
655 658 default=4,
656 659 )
657 660 coreconfigitem('worker', 'numcpus',
658 661 default=None,
659 662 )
@@ -1,541 +1,541 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 scmutil,
38 38 templater,
39 39 ui as uimod,
40 40 util,
41 41 )
42 42
43 43 from . import (
44 44 hgweb_mod,
45 45 webutil,
46 46 wsgicgi,
47 47 )
48 48
49 49 def cleannames(items):
50 50 return [(util.pconvert(name).strip('/'), path) for name, path in items]
51 51
52 52 def findrepos(paths):
53 53 repos = []
54 54 for prefix, root in cleannames(paths):
55 55 roothead, roottail = os.path.split(root)
56 56 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
57 57 # /bar/ be served as as foo/N .
58 58 # '*' will not search inside dirs with .hg (except .hg/patches),
59 59 # '**' will search inside dirs with .hg (and thus also find subrepos).
60 60 try:
61 61 recurse = {'*': False, '**': True}[roottail]
62 62 except KeyError:
63 63 repos.append((prefix, root))
64 64 continue
65 65 roothead = os.path.normpath(os.path.abspath(roothead))
66 66 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
67 67 repos.extend(urlrepos(prefix, roothead, paths))
68 68 return repos
69 69
70 70 def urlrepos(prefix, roothead, paths):
71 71 """yield url paths and filesystem paths from a list of repo paths
72 72
73 73 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
74 74 >>> conv(urlrepos(b'hg', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
75 75 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
76 76 >>> conv(urlrepos(b'', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
77 77 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
78 78 """
79 79 for path in paths:
80 80 path = os.path.normpath(path)
81 81 yield (prefix + '/' +
82 82 util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
83 83
84 84 def geturlcgivars(baseurl, port):
85 85 """
86 86 Extract CGI variables from baseurl
87 87
88 88 >>> geturlcgivars(b"http://host.org/base", b"80")
89 89 ('host.org', '80', '/base')
90 90 >>> geturlcgivars(b"http://host.org:8000/base", b"80")
91 91 ('host.org', '8000', '/base')
92 92 >>> geturlcgivars(b'/base', 8000)
93 93 ('', '8000', '/base')
94 94 >>> geturlcgivars(b"base", b'8000')
95 95 ('', '8000', '/base')
96 96 >>> geturlcgivars(b"http://host", b'8000')
97 97 ('host', '8000', '/')
98 98 >>> geturlcgivars(b"http://host/", b'8000')
99 99 ('host', '8000', '/')
100 100 """
101 101 u = util.url(baseurl)
102 102 name = u.host or ''
103 103 if u.port:
104 104 port = u.port
105 105 path = u.path or ""
106 106 if not path.startswith('/'):
107 107 path = '/' + path
108 108
109 109 return name, str(port), path
110 110
111 111 class hgwebdir(object):
112 112 """HTTP server for multiple repositories.
113 113
114 114 Given a configuration, different repositories will be served depending
115 115 on the request path.
116 116
117 117 Instances are typically used as WSGI applications.
118 118 """
119 119 def __init__(self, conf, baseui=None):
120 120 self.conf = conf
121 121 self.baseui = baseui
122 122 self.ui = None
123 123 self.lastrefresh = 0
124 124 self.motd = None
125 125 self.refresh()
126 126
127 127 def refresh(self):
128 128 if self.ui:
129 129 refreshinterval = self.ui.configint('web', 'refreshinterval')
130 130 else:
131 131 item = configitems.coreitems['web']['refreshinterval']
132 132 refreshinterval = item.default
133 133
134 134 # refreshinterval <= 0 means to always refresh.
135 135 if (refreshinterval > 0 and
136 136 self.lastrefresh + refreshinterval > time.time()):
137 137 return
138 138
139 139 if self.baseui:
140 140 u = self.baseui.copy()
141 141 else:
142 142 u = uimod.ui.load()
143 143 u.setconfig('ui', 'report_untrusted', 'off', 'hgwebdir')
144 144 u.setconfig('ui', 'nontty', 'true', 'hgwebdir')
145 145 # displaying bundling progress bar while serving feels wrong and may
146 146 # break some wsgi implementations.
147 147 u.setconfig('progress', 'disable', 'true', 'hgweb')
148 148
149 149 if not isinstance(self.conf, (dict, list, tuple)):
150 150 map = {'paths': 'hgweb-paths'}
151 151 if not os.path.exists(self.conf):
152 152 raise error.Abort(_('config file %s not found!') % self.conf)
153 153 u.readconfig(self.conf, remap=map, trust=True)
154 154 paths = []
155 155 for name, ignored in u.configitems('hgweb-paths'):
156 156 for path in u.configlist('hgweb-paths', name):
157 157 paths.append((name, path))
158 158 elif isinstance(self.conf, (list, tuple)):
159 159 paths = self.conf
160 160 elif isinstance(self.conf, dict):
161 161 paths = self.conf.items()
162 162
163 163 repos = findrepos(paths)
164 164 for prefix, root in u.configitems('collections'):
165 165 prefix = util.pconvert(prefix)
166 166 for path in scmutil.walkrepos(root, followsym=True):
167 167 repo = os.path.normpath(path)
168 168 name = util.pconvert(repo)
169 169 if name.startswith(prefix):
170 170 name = name[len(prefix):]
171 171 repos.append((name.lstrip('/'), repo))
172 172
173 173 self.repos = repos
174 174 self.ui = u
175 175 encoding.encoding = self.ui.config('web', 'encoding')
176 self.style = self.ui.config('web', 'style', 'paper')
176 self.style = self.ui.config('web', 'style')
177 177 self.templatepath = self.ui.config('web', 'templates', None)
178 178 self.stripecount = self.ui.config('web', 'stripes')
179 179 if self.stripecount:
180 180 self.stripecount = int(self.stripecount)
181 181 self._baseurl = self.ui.config('web', 'baseurl')
182 182 prefix = self.ui.config('web', 'prefix')
183 183 if prefix.startswith('/'):
184 184 prefix = prefix[1:]
185 185 if prefix.endswith('/'):
186 186 prefix = prefix[:-1]
187 187 self.prefix = prefix
188 188 self.lastrefresh = time.time()
189 189
190 190 def run(self):
191 191 if not encoding.environ.get('GATEWAY_INTERFACE',
192 192 '').startswith("CGI/1."):
193 193 raise RuntimeError("This function is only intended to be "
194 194 "called while running as a CGI script.")
195 195 wsgicgi.launch(self)
196 196
197 197 def __call__(self, env, respond):
198 198 req = wsgirequest(env, respond)
199 199 return self.run_wsgi(req)
200 200
201 201 def read_allowed(self, ui, req):
202 202 """Check allow_read and deny_read config options of a repo's ui object
203 203 to determine user permissions. By default, with neither option set (or
204 204 both empty), allow all users to read the repo. There are two ways a
205 205 user can be denied read access: (1) deny_read is not empty, and the
206 206 user is unauthenticated or deny_read contains user (or *), and (2)
207 207 allow_read is not empty and the user is not in allow_read. Return True
208 208 if user is allowed to read the repo, else return False."""
209 209
210 210 user = req.env.get('REMOTE_USER')
211 211
212 212 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
213 213 if deny_read and (not user or ismember(ui, user, deny_read)):
214 214 return False
215 215
216 216 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
217 217 # by default, allow reading if no allow_read option has been set
218 218 if (not allow_read) or ismember(ui, user, allow_read):
219 219 return True
220 220
221 221 return False
222 222
223 223 def run_wsgi(self, req):
224 224 profile = self.ui.configbool('profiling', 'enabled')
225 225 with profiling.profile(self.ui, enabled=profile):
226 226 for r in self._runwsgi(req):
227 227 yield r
228 228
229 229 def _runwsgi(self, req):
230 230 try:
231 231 self.refresh()
232 232
233 233 csp, nonce = cspvalues(self.ui)
234 234 if csp:
235 235 req.headers.append(('Content-Security-Policy', csp))
236 236
237 237 virtual = req.env.get("PATH_INFO", "").strip('/')
238 238 tmpl = self.templater(req, nonce)
239 239 ctype = tmpl('mimetype', encoding=encoding.encoding)
240 240 ctype = templater.stringify(ctype)
241 241
242 242 # a static file
243 243 if virtual.startswith('static/') or 'static' in req.form:
244 244 if virtual.startswith('static/'):
245 245 fname = virtual[7:]
246 246 else:
247 247 fname = req.form['static'][0]
248 248 static = self.ui.config("web", "static", None,
249 249 untrusted=False)
250 250 if not static:
251 251 tp = self.templatepath or templater.templatepaths()
252 252 if isinstance(tp, str):
253 253 tp = [tp]
254 254 static = [os.path.join(p, 'static') for p in tp]
255 255 staticfile(static, fname, req)
256 256 return []
257 257
258 258 # top-level index
259 259
260 260 repos = dict(self.repos)
261 261
262 262 if (not virtual or virtual == 'index') and virtual not in repos:
263 263 req.respond(HTTP_OK, ctype)
264 264 return self.makeindex(req, tmpl)
265 265
266 266 # nested indexes and hgwebs
267 267
268 268 if virtual.endswith('/index') and virtual not in repos:
269 269 subdir = virtual[:-len('index')]
270 270 if any(r.startswith(subdir) for r in repos):
271 271 req.respond(HTTP_OK, ctype)
272 272 return self.makeindex(req, tmpl, subdir)
273 273
274 274 def _virtualdirs():
275 275 # Check the full virtual path, each parent, and the root ('')
276 276 if virtual != '':
277 277 yield virtual
278 278
279 279 for p in util.finddirs(virtual):
280 280 yield p
281 281
282 282 yield ''
283 283
284 284 for virtualrepo in _virtualdirs():
285 285 real = repos.get(virtualrepo)
286 286 if real:
287 287 req.env['REPO_NAME'] = virtualrepo
288 288 try:
289 289 # ensure caller gets private copy of ui
290 290 repo = hg.repository(self.ui.copy(), real)
291 291 return hgweb_mod.hgweb(repo).run_wsgi(req)
292 292 except IOError as inst:
293 293 msg = encoding.strtolocal(inst.strerror)
294 294 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
295 295 except error.RepoError as inst:
296 296 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
297 297
298 298 # browse subdirectories
299 299 subdir = virtual + '/'
300 300 if [r for r in repos if r.startswith(subdir)]:
301 301 req.respond(HTTP_OK, ctype)
302 302 return self.makeindex(req, tmpl, subdir)
303 303
304 304 # prefixes not found
305 305 req.respond(HTTP_NOT_FOUND, ctype)
306 306 return tmpl("notfound", repo=virtual)
307 307
308 308 except ErrorResponse as err:
309 309 req.respond(err, ctype)
310 310 return tmpl('error', error=err.message or '')
311 311 finally:
312 312 tmpl = None
313 313
314 314 def makeindex(self, req, tmpl, subdir=""):
315 315
316 316 def archivelist(ui, nodeid, url):
317 317 allowed = ui.configlist("web", "allow_archive", untrusted=True)
318 318 archives = []
319 319 for typ, spec in hgweb_mod.archivespecs.iteritems():
320 320 if typ in allowed or ui.configbool("web", "allow" + typ,
321 321 untrusted=True):
322 322 archives.append({"type" : typ, "extension": spec[2],
323 323 "node": nodeid, "url": url})
324 324 return archives
325 325
326 326 def rawentries(subdir="", **map):
327 327
328 328 descend = self.ui.configbool('web', 'descend')
329 329 collapse = self.ui.configbool('web', 'collapse')
330 330 seenrepos = set()
331 331 seendirs = set()
332 332 for name, path in self.repos:
333 333
334 334 if not name.startswith(subdir):
335 335 continue
336 336 name = name[len(subdir):]
337 337 directory = False
338 338
339 339 if '/' in name:
340 340 if not descend:
341 341 continue
342 342
343 343 nameparts = name.split('/')
344 344 rootname = nameparts[0]
345 345
346 346 if not collapse:
347 347 pass
348 348 elif rootname in seendirs:
349 349 continue
350 350 elif rootname in seenrepos:
351 351 pass
352 352 else:
353 353 directory = True
354 354 name = rootname
355 355
356 356 # redefine the path to refer to the directory
357 357 discarded = '/'.join(nameparts[1:])
358 358
359 359 # remove name parts plus accompanying slash
360 360 path = path[:-len(discarded) - 1]
361 361
362 362 try:
363 363 r = hg.repository(self.ui, path)
364 364 directory = False
365 365 except (IOError, error.RepoError):
366 366 pass
367 367
368 368 parts = [name]
369 369 parts.insert(0, '/' + subdir.rstrip('/'))
370 370 if req.env['SCRIPT_NAME']:
371 371 parts.insert(0, req.env['SCRIPT_NAME'])
372 372 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
373 373
374 374 # show either a directory entry or a repository
375 375 if directory:
376 376 # get the directory's time information
377 377 try:
378 378 d = (get_mtime(path), util.makedate()[1])
379 379 except OSError:
380 380 continue
381 381
382 382 # add '/' to the name to make it obvious that
383 383 # the entry is a directory, not a regular repository
384 384 row = {'contact': "",
385 385 'contact_sort': "",
386 386 'name': name + '/',
387 387 'name_sort': name,
388 388 'url': url,
389 389 'description': "",
390 390 'description_sort': "",
391 391 'lastchange': d,
392 392 'lastchange_sort': d[1]-d[0],
393 393 'archives': [],
394 394 'isdirectory': True,
395 395 'labels': [],
396 396 }
397 397
398 398 seendirs.add(name)
399 399 yield row
400 400 continue
401 401
402 402 u = self.ui.copy()
403 403 try:
404 404 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
405 405 except Exception as e:
406 406 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
407 407 continue
408 408 def get(section, name, default=uimod._unset):
409 409 return u.config(section, name, default, untrusted=True)
410 410
411 411 if u.configbool("web", "hidden", untrusted=True):
412 412 continue
413 413
414 414 if not self.read_allowed(u, req):
415 415 continue
416 416
417 417 # update time with local timezone
418 418 try:
419 419 r = hg.repository(self.ui, path)
420 420 except IOError:
421 421 u.warn(_('error accessing repository at %s\n') % path)
422 422 continue
423 423 except error.RepoError:
424 424 u.warn(_('error accessing repository at %s\n') % path)
425 425 continue
426 426 try:
427 427 d = (get_mtime(r.spath), util.makedate()[1])
428 428 except OSError:
429 429 continue
430 430
431 431 contact = get_contact(get)
432 432 description = get("web", "description")
433 433 seenrepos.add(name)
434 434 name = get("web", "name", name)
435 435 row = {'contact': contact or "unknown",
436 436 'contact_sort': contact.upper() or "unknown",
437 437 'name': name,
438 438 'name_sort': name,
439 439 'url': url,
440 440 'description': description or "unknown",
441 441 'description_sort': description.upper() or "unknown",
442 442 'lastchange': d,
443 443 'lastchange_sort': d[1]-d[0],
444 444 'archives': archivelist(u, "tip", url),
445 445 'isdirectory': None,
446 446 'labels': u.configlist('web', 'labels', untrusted=True),
447 447 }
448 448
449 449 yield row
450 450
451 451 sortdefault = None, False
452 452 def entries(sortcolumn="", descending=False, subdir="", **map):
453 453 rows = rawentries(subdir=subdir, **map)
454 454
455 455 if sortcolumn and sortdefault != (sortcolumn, descending):
456 456 sortkey = '%s_sort' % sortcolumn
457 457 rows = sorted(rows, key=lambda x: x[sortkey],
458 458 reverse=descending)
459 459 for row, parity in zip(rows, paritygen(self.stripecount)):
460 460 row['parity'] = parity
461 461 yield row
462 462
463 463 self.refresh()
464 464 sortable = ["name", "description", "contact", "lastchange"]
465 465 sortcolumn, descending = sortdefault
466 466 if 'sort' in req.form:
467 467 sortcolumn = req.form['sort'][0]
468 468 descending = sortcolumn.startswith('-')
469 469 if descending:
470 470 sortcolumn = sortcolumn[1:]
471 471 if sortcolumn not in sortable:
472 472 sortcolumn = ""
473 473
474 474 sort = [("sort_%s" % column,
475 475 "%s%s" % ((not descending and column == sortcolumn)
476 476 and "-" or "", column))
477 477 for column in sortable]
478 478
479 479 self.refresh()
480 480 self.updatereqenv(req.env)
481 481
482 482 return tmpl("index", entries=entries, subdir=subdir,
483 483 pathdef=hgweb_mod.makebreadcrumb('/' + subdir, self.prefix),
484 484 sortcolumn=sortcolumn, descending=descending,
485 485 **dict(sort))
486 486
487 487 def templater(self, req, nonce):
488 488
489 489 def motd(**map):
490 490 if self.motd is not None:
491 491 yield self.motd
492 492 else:
493 493 yield config('web', 'motd', '')
494 494
495 495 def config(section, name, default=uimod._unset, untrusted=True):
496 496 return self.ui.config(section, name, default, untrusted)
497 497
498 498 self.updatereqenv(req.env)
499 499
500 500 url = req.env.get('SCRIPT_NAME', '')
501 501 if not url.endswith('/'):
502 502 url += '/'
503 503
504 504 vars = {}
505 505 styles = (
506 506 req.form.get('style', [None])[0],
507 507 config('web', 'style'),
508 508 'paper'
509 509 )
510 510 style, mapfile = templater.stylemap(styles, self.templatepath)
511 511 if style == styles[0]:
512 512 vars['style'] = style
513 513
514 514 start = url[-1] == '?' and '&' or '?'
515 515 sessionvars = webutil.sessionvars(vars, start)
516 516 logourl = config('web', 'logourl', 'https://mercurial-scm.org/')
517 517 logoimg = config('web', 'logoimg', 'hglogo.png')
518 518 staticurl = config('web', 'staticurl') or url + 'static/'
519 519 if not staticurl.endswith('/'):
520 520 staticurl += '/'
521 521
522 522 defaults = {
523 523 "encoding": encoding.encoding,
524 524 "motd": motd,
525 525 "url": url,
526 526 "logourl": logourl,
527 527 "logoimg": logoimg,
528 528 "staticurl": staticurl,
529 529 "sessionvars": sessionvars,
530 530 "style": style,
531 531 "nonce": nonce,
532 532 }
533 533 tmpl = templater.templater.frommapfile(mapfile, defaults=defaults)
534 534 return tmpl
535 535
536 536 def updatereqenv(self, env):
537 537 if self._baseurl is not None:
538 538 name, port, path = geturlcgivars(self._baseurl, env['SERVER_PORT'])
539 539 env['SERVER_NAME'] = name
540 540 env['SERVER_PORT'] = port
541 541 env['SCRIPT_NAME'] = path
@@ -1,1388 +1,1388 b''
1 1 #
2 2 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import cgi
11 11 import copy
12 12 import mimetypes
13 13 import os
14 14 import re
15 15
16 16 from ..i18n import _
17 17 from ..node import hex, short
18 18
19 19 from .common import (
20 20 ErrorResponse,
21 21 HTTP_FORBIDDEN,
22 22 HTTP_NOT_FOUND,
23 23 HTTP_OK,
24 24 get_contact,
25 25 paritygen,
26 26 staticfile,
27 27 )
28 28
29 29 from .. import (
30 30 archival,
31 31 dagop,
32 32 encoding,
33 33 error,
34 34 graphmod,
35 35 revset,
36 36 revsetlang,
37 37 scmutil,
38 38 smartset,
39 39 templatefilters,
40 40 templater,
41 41 util,
42 42 )
43 43
44 44 from . import (
45 45 webutil,
46 46 )
47 47
48 48 __all__ = []
49 49 commands = {}
50 50
51 51 class webcommand(object):
52 52 """Decorator used to register a web command handler.
53 53
54 54 The decorator takes as its positional arguments the name/path the
55 55 command should be accessible under.
56 56
57 57 Usage:
58 58
59 59 @webcommand('mycommand')
60 60 def mycommand(web, req, tmpl):
61 61 pass
62 62 """
63 63
64 64 def __init__(self, name):
65 65 self.name = name
66 66
67 67 def __call__(self, func):
68 68 __all__.append(self.name)
69 69 commands[self.name] = func
70 70 return func
71 71
72 72 @webcommand('log')
73 73 def log(web, req, tmpl):
74 74 """
75 75 /log[/{revision}[/{path}]]
76 76 --------------------------
77 77
78 78 Show repository or file history.
79 79
80 80 For URLs of the form ``/log/{revision}``, a list of changesets starting at
81 81 the specified changeset identifier is shown. If ``{revision}`` is not
82 82 defined, the default is ``tip``. This form is equivalent to the
83 83 ``changelog`` handler.
84 84
85 85 For URLs of the form ``/log/{revision}/{file}``, the history for a specific
86 86 file will be shown. This form is equivalent to the ``filelog`` handler.
87 87 """
88 88
89 89 if 'file' in req.form and req.form['file'][0]:
90 90 return filelog(web, req, tmpl)
91 91 else:
92 92 return changelog(web, req, tmpl)
93 93
94 94 @webcommand('rawfile')
95 95 def rawfile(web, req, tmpl):
96 96 guessmime = web.configbool('web', 'guessmime', False)
97 97
98 98 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
99 99 if not path:
100 100 content = manifest(web, req, tmpl)
101 101 req.respond(HTTP_OK, web.ctype)
102 102 return content
103 103
104 104 try:
105 105 fctx = webutil.filectx(web.repo, req)
106 106 except error.LookupError as inst:
107 107 try:
108 108 content = manifest(web, req, tmpl)
109 109 req.respond(HTTP_OK, web.ctype)
110 110 return content
111 111 except ErrorResponse:
112 112 raise inst
113 113
114 114 path = fctx.path()
115 115 text = fctx.data()
116 116 mt = 'application/binary'
117 117 if guessmime:
118 118 mt = mimetypes.guess_type(path)[0]
119 119 if mt is None:
120 120 if util.binary(text):
121 121 mt = 'application/binary'
122 122 else:
123 123 mt = 'text/plain'
124 124 if mt.startswith('text/'):
125 125 mt += '; charset="%s"' % encoding.encoding
126 126
127 127 req.respond(HTTP_OK, mt, path, body=text)
128 128 return []
129 129
130 130 def _filerevision(web, req, tmpl, fctx):
131 131 f = fctx.path()
132 132 text = fctx.data()
133 133 parity = paritygen(web.stripecount)
134 134 ishead = fctx.filerev() in fctx.filelog().headrevs()
135 135
136 136 if util.binary(text):
137 137 mt = mimetypes.guess_type(f)[0] or 'application/octet-stream'
138 138 text = '(binary:%s)' % mt
139 139
140 140 def lines():
141 141 for lineno, t in enumerate(text.splitlines(True)):
142 142 yield {"line": t,
143 143 "lineid": "l%d" % (lineno + 1),
144 144 "linenumber": "% 6d" % (lineno + 1),
145 145 "parity": next(parity)}
146 146
147 147 return tmpl("filerevision",
148 148 file=f,
149 149 path=webutil.up(f),
150 150 text=lines(),
151 151 symrev=webutil.symrevorshortnode(req, fctx),
152 152 rename=webutil.renamelink(fctx),
153 153 permissions=fctx.manifest().flags(f),
154 154 ishead=int(ishead),
155 155 **webutil.commonentry(web.repo, fctx))
156 156
157 157 @webcommand('file')
158 158 def file(web, req, tmpl):
159 159 """
160 160 /file/{revision}[/{path}]
161 161 -------------------------
162 162
163 163 Show information about a directory or file in the repository.
164 164
165 165 Info about the ``path`` given as a URL parameter will be rendered.
166 166
167 167 If ``path`` is a directory, information about the entries in that
168 168 directory will be rendered. This form is equivalent to the ``manifest``
169 169 handler.
170 170
171 171 If ``path`` is a file, information about that file will be shown via
172 172 the ``filerevision`` template.
173 173
174 174 If ``path`` is not defined, information about the root directory will
175 175 be rendered.
176 176 """
177 177 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
178 178 if not path:
179 179 return manifest(web, req, tmpl)
180 180 try:
181 181 return _filerevision(web, req, tmpl, webutil.filectx(web.repo, req))
182 182 except error.LookupError as inst:
183 183 try:
184 184 return manifest(web, req, tmpl)
185 185 except ErrorResponse:
186 186 raise inst
187 187
188 188 def _search(web, req, tmpl):
189 189 MODE_REVISION = 'rev'
190 190 MODE_KEYWORD = 'keyword'
191 191 MODE_REVSET = 'revset'
192 192
193 193 def revsearch(ctx):
194 194 yield ctx
195 195
196 196 def keywordsearch(query):
197 197 lower = encoding.lower
198 198 qw = lower(query).split()
199 199
200 200 def revgen():
201 201 cl = web.repo.changelog
202 202 for i in xrange(len(web.repo) - 1, 0, -100):
203 203 l = []
204 204 for j in cl.revs(max(0, i - 99), i):
205 205 ctx = web.repo[j]
206 206 l.append(ctx)
207 207 l.reverse()
208 208 for e in l:
209 209 yield e
210 210
211 211 for ctx in revgen():
212 212 miss = 0
213 213 for q in qw:
214 214 if not (q in lower(ctx.user()) or
215 215 q in lower(ctx.description()) or
216 216 q in lower(" ".join(ctx.files()))):
217 217 miss = 1
218 218 break
219 219 if miss:
220 220 continue
221 221
222 222 yield ctx
223 223
224 224 def revsetsearch(revs):
225 225 for r in revs:
226 226 yield web.repo[r]
227 227
228 228 searchfuncs = {
229 229 MODE_REVISION: (revsearch, 'exact revision search'),
230 230 MODE_KEYWORD: (keywordsearch, 'literal keyword search'),
231 231 MODE_REVSET: (revsetsearch, 'revset expression search'),
232 232 }
233 233
234 234 def getsearchmode(query):
235 235 try:
236 236 ctx = web.repo[query]
237 237 except (error.RepoError, error.LookupError):
238 238 # query is not an exact revision pointer, need to
239 239 # decide if it's a revset expression or keywords
240 240 pass
241 241 else:
242 242 return MODE_REVISION, ctx
243 243
244 244 revdef = 'reverse(%s)' % query
245 245 try:
246 246 tree = revsetlang.parse(revdef)
247 247 except error.ParseError:
248 248 # can't parse to a revset tree
249 249 return MODE_KEYWORD, query
250 250
251 251 if revsetlang.depth(tree) <= 2:
252 252 # no revset syntax used
253 253 return MODE_KEYWORD, query
254 254
255 255 if any((token, (value or '')[:3]) == ('string', 're:')
256 256 for token, value, pos in revsetlang.tokenize(revdef)):
257 257 return MODE_KEYWORD, query
258 258
259 259 funcsused = revsetlang.funcsused(tree)
260 260 if not funcsused.issubset(revset.safesymbols):
261 261 return MODE_KEYWORD, query
262 262
263 263 mfunc = revset.match(web.repo.ui, revdef, repo=web.repo)
264 264 try:
265 265 revs = mfunc(web.repo)
266 266 return MODE_REVSET, revs
267 267 # ParseError: wrongly placed tokens, wrongs arguments, etc
268 268 # RepoLookupError: no such revision, e.g. in 'revision:'
269 269 # Abort: bookmark/tag not exists
270 270 # LookupError: ambiguous identifier, e.g. in '(bc)' on a large repo
271 271 except (error.ParseError, error.RepoLookupError, error.Abort,
272 272 LookupError):
273 273 return MODE_KEYWORD, query
274 274
275 275 def changelist(**map):
276 276 count = 0
277 277
278 278 for ctx in searchfunc[0](funcarg):
279 279 count += 1
280 280 n = ctx.node()
281 281 showtags = webutil.showtag(web.repo, tmpl, 'changelogtag', n)
282 282 files = webutil.listfilediffs(tmpl, ctx.files(), n, web.maxfiles)
283 283
284 284 yield tmpl('searchentry',
285 285 parity=next(parity),
286 286 changelogtag=showtags,
287 287 files=files,
288 288 **webutil.commonentry(web.repo, ctx))
289 289
290 290 if count >= revcount:
291 291 break
292 292
293 293 query = req.form['rev'][0]
294 294 revcount = web.maxchanges
295 295 if 'revcount' in req.form:
296 296 try:
297 297 revcount = int(req.form.get('revcount', [revcount])[0])
298 298 revcount = max(revcount, 1)
299 299 tmpl.defaults['sessionvars']['revcount'] = revcount
300 300 except ValueError:
301 301 pass
302 302
303 303 lessvars = copy.copy(tmpl.defaults['sessionvars'])
304 304 lessvars['revcount'] = max(revcount / 2, 1)
305 305 lessvars['rev'] = query
306 306 morevars = copy.copy(tmpl.defaults['sessionvars'])
307 307 morevars['revcount'] = revcount * 2
308 308 morevars['rev'] = query
309 309
310 310 mode, funcarg = getsearchmode(query)
311 311
312 312 if 'forcekw' in req.form:
313 313 showforcekw = ''
314 314 showunforcekw = searchfuncs[mode][1]
315 315 mode = MODE_KEYWORD
316 316 funcarg = query
317 317 else:
318 318 if mode != MODE_KEYWORD:
319 319 showforcekw = searchfuncs[MODE_KEYWORD][1]
320 320 else:
321 321 showforcekw = ''
322 322 showunforcekw = ''
323 323
324 324 searchfunc = searchfuncs[mode]
325 325
326 326 tip = web.repo['tip']
327 327 parity = paritygen(web.stripecount)
328 328
329 329 return tmpl('search', query=query, node=tip.hex(), symrev='tip',
330 330 entries=changelist, archives=web.archivelist("tip"),
331 331 morevars=morevars, lessvars=lessvars,
332 332 modedesc=searchfunc[1],
333 333 showforcekw=showforcekw, showunforcekw=showunforcekw)
334 334
335 335 @webcommand('changelog')
336 336 def changelog(web, req, tmpl, shortlog=False):
337 337 """
338 338 /changelog[/{revision}]
339 339 -----------------------
340 340
341 341 Show information about multiple changesets.
342 342
343 343 If the optional ``revision`` URL argument is absent, information about
344 344 all changesets starting at ``tip`` will be rendered. If the ``revision``
345 345 argument is present, changesets will be shown starting from the specified
346 346 revision.
347 347
348 348 If ``revision`` is absent, the ``rev`` query string argument may be
349 349 defined. This will perform a search for changesets.
350 350
351 351 The argument for ``rev`` can be a single revision, a revision set,
352 352 or a literal keyword to search for in changeset data (equivalent to
353 353 :hg:`log -k`).
354 354
355 355 The ``revcount`` query string argument defines the maximum numbers of
356 356 changesets to render.
357 357
358 358 For non-searches, the ``changelog`` template will be rendered.
359 359 """
360 360
361 361 query = ''
362 362 if 'node' in req.form:
363 363 ctx = webutil.changectx(web.repo, req)
364 364 symrev = webutil.symrevorshortnode(req, ctx)
365 365 elif 'rev' in req.form:
366 366 return _search(web, req, tmpl)
367 367 else:
368 368 ctx = web.repo['tip']
369 369 symrev = 'tip'
370 370
371 371 def changelist():
372 372 revs = []
373 373 if pos != -1:
374 374 revs = web.repo.changelog.revs(pos, 0)
375 375 curcount = 0
376 376 for rev in revs:
377 377 curcount += 1
378 378 if curcount > revcount + 1:
379 379 break
380 380
381 381 entry = webutil.changelistentry(web, web.repo[rev], tmpl)
382 382 entry['parity'] = next(parity)
383 383 yield entry
384 384
385 385 if shortlog:
386 386 revcount = web.maxshortchanges
387 387 else:
388 388 revcount = web.maxchanges
389 389
390 390 if 'revcount' in req.form:
391 391 try:
392 392 revcount = int(req.form.get('revcount', [revcount])[0])
393 393 revcount = max(revcount, 1)
394 394 tmpl.defaults['sessionvars']['revcount'] = revcount
395 395 except ValueError:
396 396 pass
397 397
398 398 lessvars = copy.copy(tmpl.defaults['sessionvars'])
399 399 lessvars['revcount'] = max(revcount / 2, 1)
400 400 morevars = copy.copy(tmpl.defaults['sessionvars'])
401 401 morevars['revcount'] = revcount * 2
402 402
403 403 count = len(web.repo)
404 404 pos = ctx.rev()
405 405 parity = paritygen(web.stripecount)
406 406
407 407 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
408 408
409 409 entries = list(changelist())
410 410 latestentry = entries[:1]
411 411 if len(entries) > revcount:
412 412 nextentry = entries[-1:]
413 413 entries = entries[:-1]
414 414 else:
415 415 nextentry = []
416 416
417 417 return tmpl(shortlog and 'shortlog' or 'changelog', changenav=changenav,
418 418 node=ctx.hex(), rev=pos, symrev=symrev, changesets=count,
419 419 entries=entries,
420 420 latestentry=latestentry, nextentry=nextentry,
421 421 archives=web.archivelist("tip"), revcount=revcount,
422 422 morevars=morevars, lessvars=lessvars, query=query)
423 423
424 424 @webcommand('shortlog')
425 425 def shortlog(web, req, tmpl):
426 426 """
427 427 /shortlog
428 428 ---------
429 429
430 430 Show basic information about a set of changesets.
431 431
432 432 This accepts the same parameters as the ``changelog`` handler. The only
433 433 difference is the ``shortlog`` template will be rendered instead of the
434 434 ``changelog`` template.
435 435 """
436 436 return changelog(web, req, tmpl, shortlog=True)
437 437
438 438 @webcommand('changeset')
439 439 def changeset(web, req, tmpl):
440 440 """
441 441 /changeset[/{revision}]
442 442 -----------------------
443 443
444 444 Show information about a single changeset.
445 445
446 446 A URL path argument is the changeset identifier to show. See ``hg help
447 447 revisions`` for possible values. If not defined, the ``tip`` changeset
448 448 will be shown.
449 449
450 450 The ``changeset`` template is rendered. Contents of the ``changesettag``,
451 451 ``changesetbookmark``, ``filenodelink``, ``filenolink``, and the many
452 452 templates related to diffs may all be used to produce the output.
453 453 """
454 454 ctx = webutil.changectx(web.repo, req)
455 455
456 456 return tmpl('changeset', **webutil.changesetentry(web, req, tmpl, ctx))
457 457
458 458 rev = webcommand('rev')(changeset)
459 459
460 460 def decodepath(path):
461 461 """Hook for mapping a path in the repository to a path in the
462 462 working copy.
463 463
464 464 Extensions (e.g., largefiles) can override this to remap files in
465 465 the virtual file system presented by the manifest command below."""
466 466 return path
467 467
468 468 @webcommand('manifest')
469 469 def manifest(web, req, tmpl):
470 470 """
471 471 /manifest[/{revision}[/{path}]]
472 472 -------------------------------
473 473
474 474 Show information about a directory.
475 475
476 476 If the URL path arguments are omitted, information about the root
477 477 directory for the ``tip`` changeset will be shown.
478 478
479 479 Because this handler can only show information for directories, it
480 480 is recommended to use the ``file`` handler instead, as it can handle both
481 481 directories and files.
482 482
483 483 The ``manifest`` template will be rendered for this handler.
484 484 """
485 485 if 'node' in req.form:
486 486 ctx = webutil.changectx(web.repo, req)
487 487 symrev = webutil.symrevorshortnode(req, ctx)
488 488 else:
489 489 ctx = web.repo['tip']
490 490 symrev = 'tip'
491 491 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
492 492 mf = ctx.manifest()
493 493 node = ctx.node()
494 494
495 495 files = {}
496 496 dirs = {}
497 497 parity = paritygen(web.stripecount)
498 498
499 499 if path and path[-1] != "/":
500 500 path += "/"
501 501 l = len(path)
502 502 abspath = "/" + path
503 503
504 504 for full, n in mf.iteritems():
505 505 # the virtual path (working copy path) used for the full
506 506 # (repository) path
507 507 f = decodepath(full)
508 508
509 509 if f[:l] != path:
510 510 continue
511 511 remain = f[l:]
512 512 elements = remain.split('/')
513 513 if len(elements) == 1:
514 514 files[remain] = full
515 515 else:
516 516 h = dirs # need to retain ref to dirs (root)
517 517 for elem in elements[0:-1]:
518 518 if elem not in h:
519 519 h[elem] = {}
520 520 h = h[elem]
521 521 if len(h) > 1:
522 522 break
523 523 h[None] = None # denotes files present
524 524
525 525 if mf and not files and not dirs:
526 526 raise ErrorResponse(HTTP_NOT_FOUND, 'path not found: ' + path)
527 527
528 528 def filelist(**map):
529 529 for f in sorted(files):
530 530 full = files[f]
531 531
532 532 fctx = ctx.filectx(full)
533 533 yield {"file": full,
534 534 "parity": next(parity),
535 535 "basename": f,
536 536 "date": fctx.date(),
537 537 "size": fctx.size(),
538 538 "permissions": mf.flags(full)}
539 539
540 540 def dirlist(**map):
541 541 for d in sorted(dirs):
542 542
543 543 emptydirs = []
544 544 h = dirs[d]
545 545 while isinstance(h, dict) and len(h) == 1:
546 546 k, v = h.items()[0]
547 547 if v:
548 548 emptydirs.append(k)
549 549 h = v
550 550
551 551 path = "%s%s" % (abspath, d)
552 552 yield {"parity": next(parity),
553 553 "path": path,
554 554 "emptydirs": "/".join(emptydirs),
555 555 "basename": d}
556 556
557 557 return tmpl("manifest",
558 558 symrev=symrev,
559 559 path=abspath,
560 560 up=webutil.up(abspath),
561 561 upparity=next(parity),
562 562 fentries=filelist,
563 563 dentries=dirlist,
564 564 archives=web.archivelist(hex(node)),
565 565 **webutil.commonentry(web.repo, ctx))
566 566
567 567 @webcommand('tags')
568 568 def tags(web, req, tmpl):
569 569 """
570 570 /tags
571 571 -----
572 572
573 573 Show information about tags.
574 574
575 575 No arguments are accepted.
576 576
577 577 The ``tags`` template is rendered.
578 578 """
579 579 i = list(reversed(web.repo.tagslist()))
580 580 parity = paritygen(web.stripecount)
581 581
582 582 def entries(notip, latestonly, **map):
583 583 t = i
584 584 if notip:
585 585 t = [(k, n) for k, n in i if k != "tip"]
586 586 if latestonly:
587 587 t = t[:1]
588 588 for k, n in t:
589 589 yield {"parity": next(parity),
590 590 "tag": k,
591 591 "date": web.repo[n].date(),
592 592 "node": hex(n)}
593 593
594 594 return tmpl("tags",
595 595 node=hex(web.repo.changelog.tip()),
596 596 entries=lambda **x: entries(False, False, **x),
597 597 entriesnotip=lambda **x: entries(True, False, **x),
598 598 latestentry=lambda **x: entries(True, True, **x))
599 599
600 600 @webcommand('bookmarks')
601 601 def bookmarks(web, req, tmpl):
602 602 """
603 603 /bookmarks
604 604 ----------
605 605
606 606 Show information about bookmarks.
607 607
608 608 No arguments are accepted.
609 609
610 610 The ``bookmarks`` template is rendered.
611 611 """
612 612 i = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
613 613 sortkey = lambda b: (web.repo[b[1]].rev(), b[0])
614 614 i = sorted(i, key=sortkey, reverse=True)
615 615 parity = paritygen(web.stripecount)
616 616
617 617 def entries(latestonly, **map):
618 618 t = i
619 619 if latestonly:
620 620 t = i[:1]
621 621 for k, n in t:
622 622 yield {"parity": next(parity),
623 623 "bookmark": k,
624 624 "date": web.repo[n].date(),
625 625 "node": hex(n)}
626 626
627 627 if i:
628 628 latestrev = i[0][1]
629 629 else:
630 630 latestrev = -1
631 631
632 632 return tmpl("bookmarks",
633 633 node=hex(web.repo.changelog.tip()),
634 634 lastchange=[{"date": web.repo[latestrev].date()}],
635 635 entries=lambda **x: entries(latestonly=False, **x),
636 636 latestentry=lambda **x: entries(latestonly=True, **x))
637 637
638 638 @webcommand('branches')
639 639 def branches(web, req, tmpl):
640 640 """
641 641 /branches
642 642 ---------
643 643
644 644 Show information about branches.
645 645
646 646 All known branches are contained in the output, even closed branches.
647 647
648 648 No arguments are accepted.
649 649
650 650 The ``branches`` template is rendered.
651 651 """
652 652 entries = webutil.branchentries(web.repo, web.stripecount)
653 653 latestentry = webutil.branchentries(web.repo, web.stripecount, 1)
654 654 return tmpl('branches', node=hex(web.repo.changelog.tip()),
655 655 entries=entries, latestentry=latestentry)
656 656
657 657 @webcommand('summary')
658 658 def summary(web, req, tmpl):
659 659 """
660 660 /summary
661 661 --------
662 662
663 663 Show a summary of repository state.
664 664
665 665 Information about the latest changesets, bookmarks, tags, and branches
666 666 is captured by this handler.
667 667
668 668 The ``summary`` template is rendered.
669 669 """
670 670 i = reversed(web.repo.tagslist())
671 671
672 672 def tagentries(**map):
673 673 parity = paritygen(web.stripecount)
674 674 count = 0
675 675 for k, n in i:
676 676 if k == "tip": # skip tip
677 677 continue
678 678
679 679 count += 1
680 680 if count > 10: # limit to 10 tags
681 681 break
682 682
683 683 yield tmpl("tagentry",
684 684 parity=next(parity),
685 685 tag=k,
686 686 node=hex(n),
687 687 date=web.repo[n].date())
688 688
689 689 def bookmarks(**map):
690 690 parity = paritygen(web.stripecount)
691 691 marks = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
692 692 sortkey = lambda b: (web.repo[b[1]].rev(), b[0])
693 693 marks = sorted(marks, key=sortkey, reverse=True)
694 694 for k, n in marks[:10]: # limit to 10 bookmarks
695 695 yield {'parity': next(parity),
696 696 'bookmark': k,
697 697 'date': web.repo[n].date(),
698 698 'node': hex(n)}
699 699
700 700 def changelist(**map):
701 701 parity = paritygen(web.stripecount, offset=start - end)
702 702 l = [] # build a list in forward order for efficiency
703 703 revs = []
704 704 if start < end:
705 705 revs = web.repo.changelog.revs(start, end - 1)
706 706 for i in revs:
707 707 ctx = web.repo[i]
708 708
709 709 l.append(tmpl(
710 710 'shortlogentry',
711 711 parity=next(parity),
712 712 **webutil.commonentry(web.repo, ctx)))
713 713
714 714 for entry in reversed(l):
715 715 yield entry
716 716
717 717 tip = web.repo['tip']
718 718 count = len(web.repo)
719 719 start = max(0, count - web.maxchanges)
720 720 end = min(count, start + web.maxchanges)
721 721
722 722 desc = web.config("web", "description")
723 723 if not desc:
724 724 desc = 'unknown'
725 725 return tmpl("summary",
726 726 desc=desc,
727 727 owner=get_contact(web.config) or "unknown",
728 728 lastchange=tip.date(),
729 729 tags=tagentries,
730 730 bookmarks=bookmarks,
731 731 branches=webutil.branchentries(web.repo, web.stripecount, 10),
732 732 shortlog=changelist,
733 733 node=tip.hex(),
734 734 symrev='tip',
735 735 archives=web.archivelist("tip"),
736 736 labels=web.configlist('web', 'labels'))
737 737
738 738 @webcommand('filediff')
739 739 def filediff(web, req, tmpl):
740 740 """
741 741 /diff/{revision}/{path}
742 742 -----------------------
743 743
744 744 Show how a file changed in a particular commit.
745 745
746 746 The ``filediff`` template is rendered.
747 747
748 748 This handler is registered under both the ``/diff`` and ``/filediff``
749 749 paths. ``/diff`` is used in modern code.
750 750 """
751 751 fctx, ctx = None, None
752 752 try:
753 753 fctx = webutil.filectx(web.repo, req)
754 754 except LookupError:
755 755 ctx = webutil.changectx(web.repo, req)
756 756 path = webutil.cleanpath(web.repo, req.form['file'][0])
757 757 if path not in ctx.files():
758 758 raise
759 759
760 760 if fctx is not None:
761 761 path = fctx.path()
762 762 ctx = fctx.changectx()
763 763 basectx = ctx.p1()
764 764
765 style = web.config('web', 'style', 'paper')
765 style = web.config('web', 'style')
766 766 if 'style' in req.form:
767 767 style = req.form['style'][0]
768 768
769 769 diffs = webutil.diffs(web, tmpl, ctx, basectx, [path], style)
770 770 if fctx is not None:
771 771 rename = webutil.renamelink(fctx)
772 772 ctx = fctx
773 773 else:
774 774 rename = []
775 775 ctx = ctx
776 776 return tmpl("filediff",
777 777 file=path,
778 778 symrev=webutil.symrevorshortnode(req, ctx),
779 779 rename=rename,
780 780 diff=diffs,
781 781 **webutil.commonentry(web.repo, ctx))
782 782
783 783 diff = webcommand('diff')(filediff)
784 784
785 785 @webcommand('comparison')
786 786 def comparison(web, req, tmpl):
787 787 """
788 788 /comparison/{revision}/{path}
789 789 -----------------------------
790 790
791 791 Show a comparison between the old and new versions of a file from changes
792 792 made on a particular revision.
793 793
794 794 This is similar to the ``diff`` handler. However, this form features
795 795 a split or side-by-side diff rather than a unified diff.
796 796
797 797 The ``context`` query string argument can be used to control the lines of
798 798 context in the diff.
799 799
800 800 The ``filecomparison`` template is rendered.
801 801 """
802 802 ctx = webutil.changectx(web.repo, req)
803 803 if 'file' not in req.form:
804 804 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
805 805 path = webutil.cleanpath(web.repo, req.form['file'][0])
806 806
807 807 parsecontext = lambda v: v == 'full' and -1 or int(v)
808 808 if 'context' in req.form:
809 809 context = parsecontext(req.form['context'][0])
810 810 else:
811 811 context = parsecontext(web.config('web', 'comparisoncontext', '5'))
812 812
813 813 def filelines(f):
814 814 if f.isbinary():
815 815 mt = mimetypes.guess_type(f.path())[0]
816 816 if not mt:
817 817 mt = 'application/octet-stream'
818 818 return [_('(binary file %s, hash: %s)') % (mt, hex(f.filenode()))]
819 819 return f.data().splitlines()
820 820
821 821 fctx = None
822 822 parent = ctx.p1()
823 823 leftrev = parent.rev()
824 824 leftnode = parent.node()
825 825 rightrev = ctx.rev()
826 826 rightnode = ctx.node()
827 827 if path in ctx:
828 828 fctx = ctx[path]
829 829 rightlines = filelines(fctx)
830 830 if path not in parent:
831 831 leftlines = ()
832 832 else:
833 833 pfctx = parent[path]
834 834 leftlines = filelines(pfctx)
835 835 else:
836 836 rightlines = ()
837 837 pfctx = ctx.parents()[0][path]
838 838 leftlines = filelines(pfctx)
839 839
840 840 comparison = webutil.compare(tmpl, context, leftlines, rightlines)
841 841 if fctx is not None:
842 842 rename = webutil.renamelink(fctx)
843 843 ctx = fctx
844 844 else:
845 845 rename = []
846 846 ctx = ctx
847 847 return tmpl('filecomparison',
848 848 file=path,
849 849 symrev=webutil.symrevorshortnode(req, ctx),
850 850 rename=rename,
851 851 leftrev=leftrev,
852 852 leftnode=hex(leftnode),
853 853 rightrev=rightrev,
854 854 rightnode=hex(rightnode),
855 855 comparison=comparison,
856 856 **webutil.commonentry(web.repo, ctx))
857 857
858 858 @webcommand('annotate')
859 859 def annotate(web, req, tmpl):
860 860 """
861 861 /annotate/{revision}/{path}
862 862 ---------------------------
863 863
864 864 Show changeset information for each line in a file.
865 865
866 866 The ``fileannotate`` template is rendered.
867 867 """
868 868 fctx = webutil.filectx(web.repo, req)
869 869 f = fctx.path()
870 870 parity = paritygen(web.stripecount)
871 871 ishead = fctx.filerev() in fctx.filelog().headrevs()
872 872
873 873 # parents() is called once per line and several lines likely belong to
874 874 # same revision. So it is worth caching.
875 875 # TODO there are still redundant operations within basefilectx.parents()
876 876 # and from the fctx.annotate() call itself that could be cached.
877 877 parentscache = {}
878 878 def parents(f):
879 879 rev = f.rev()
880 880 if rev not in parentscache:
881 881 parentscache[rev] = []
882 882 for p in f.parents():
883 883 entry = {
884 884 'node': p.hex(),
885 885 'rev': p.rev(),
886 886 }
887 887 parentscache[rev].append(entry)
888 888
889 889 for p in parentscache[rev]:
890 890 yield p
891 891
892 892 def annotate(**map):
893 893 if fctx.isbinary():
894 894 mt = (mimetypes.guess_type(fctx.path())[0]
895 895 or 'application/octet-stream')
896 896 lines = [((fctx.filectx(fctx.filerev()), 1), '(binary:%s)' % mt)]
897 897 else:
898 898 lines = webutil.annotate(fctx, web.repo.ui)
899 899
900 900 previousrev = None
901 901 blockparitygen = paritygen(1)
902 902 for lineno, ((f, targetline), l) in enumerate(lines):
903 903 rev = f.rev()
904 904 if rev != previousrev:
905 905 blockhead = True
906 906 blockparity = next(blockparitygen)
907 907 else:
908 908 blockhead = None
909 909 previousrev = rev
910 910 yield {"parity": next(parity),
911 911 "node": f.hex(),
912 912 "rev": rev,
913 913 "author": f.user(),
914 914 "parents": parents(f),
915 915 "desc": f.description(),
916 916 "extra": f.extra(),
917 917 "file": f.path(),
918 918 "blockhead": blockhead,
919 919 "blockparity": blockparity,
920 920 "targetline": targetline,
921 921 "line": l,
922 922 "lineno": lineno + 1,
923 923 "lineid": "l%d" % (lineno + 1),
924 924 "linenumber": "% 6d" % (lineno + 1),
925 925 "revdate": f.date()}
926 926
927 927 return tmpl("fileannotate",
928 928 file=f,
929 929 annotate=annotate,
930 930 path=webutil.up(f),
931 931 symrev=webutil.symrevorshortnode(req, fctx),
932 932 rename=webutil.renamelink(fctx),
933 933 permissions=fctx.manifest().flags(f),
934 934 ishead=int(ishead),
935 935 **webutil.commonentry(web.repo, fctx))
936 936
937 937 @webcommand('filelog')
938 938 def filelog(web, req, tmpl):
939 939 """
940 940 /filelog/{revision}/{path}
941 941 --------------------------
942 942
943 943 Show information about the history of a file in the repository.
944 944
945 945 The ``revcount`` query string argument can be defined to control the
946 946 maximum number of entries to show.
947 947
948 948 The ``filelog`` template will be rendered.
949 949 """
950 950
951 951 try:
952 952 fctx = webutil.filectx(web.repo, req)
953 953 f = fctx.path()
954 954 fl = fctx.filelog()
955 955 except error.LookupError:
956 956 f = webutil.cleanpath(web.repo, req.form['file'][0])
957 957 fl = web.repo.file(f)
958 958 numrevs = len(fl)
959 959 if not numrevs: # file doesn't exist at all
960 960 raise
961 961 rev = webutil.changectx(web.repo, req).rev()
962 962 first = fl.linkrev(0)
963 963 if rev < first: # current rev is from before file existed
964 964 raise
965 965 frev = numrevs - 1
966 966 while fl.linkrev(frev) > rev:
967 967 frev -= 1
968 968 fctx = web.repo.filectx(f, fl.linkrev(frev))
969 969
970 970 revcount = web.maxshortchanges
971 971 if 'revcount' in req.form:
972 972 try:
973 973 revcount = int(req.form.get('revcount', [revcount])[0])
974 974 revcount = max(revcount, 1)
975 975 tmpl.defaults['sessionvars']['revcount'] = revcount
976 976 except ValueError:
977 977 pass
978 978
979 979 lrange = webutil.linerange(req)
980 980
981 981 lessvars = copy.copy(tmpl.defaults['sessionvars'])
982 982 lessvars['revcount'] = max(revcount / 2, 1)
983 983 morevars = copy.copy(tmpl.defaults['sessionvars'])
984 984 morevars['revcount'] = revcount * 2
985 985
986 986 patch = 'patch' in req.form
987 987 if patch:
988 988 lessvars['patch'] = morevars['patch'] = req.form['patch'][0]
989 989 descend = 'descend' in req.form
990 990 if descend:
991 991 lessvars['descend'] = morevars['descend'] = req.form['descend'][0]
992 992
993 993 count = fctx.filerev() + 1
994 994 start = max(0, count - revcount) # first rev on this page
995 995 end = min(count, start + revcount) # last rev on this page
996 996 parity = paritygen(web.stripecount, offset=start - end)
997 997
998 998 repo = web.repo
999 999 revs = fctx.filelog().revs(start, end - 1)
1000 1000 entries = []
1001 1001
1002 diffstyle = web.config('web', 'style', 'paper')
1002 diffstyle = web.config('web', 'style')
1003 1003 if 'style' in req.form:
1004 1004 diffstyle = req.form['style'][0]
1005 1005
1006 1006 def diff(fctx, linerange=None):
1007 1007 ctx = fctx.changectx()
1008 1008 basectx = ctx.p1()
1009 1009 path = fctx.path()
1010 1010 return webutil.diffs(web, tmpl, ctx, basectx, [path], diffstyle,
1011 1011 linerange=linerange,
1012 1012 lineidprefix='%s-' % ctx.hex()[:12])
1013 1013
1014 1014 linerange = None
1015 1015 if lrange is not None:
1016 1016 linerange = webutil.formatlinerange(*lrange)
1017 1017 # deactivate numeric nav links when linerange is specified as this
1018 1018 # would required a dedicated "revnav" class
1019 1019 nav = None
1020 1020 if descend:
1021 1021 it = dagop.blockdescendants(fctx, *lrange)
1022 1022 else:
1023 1023 it = dagop.blockancestors(fctx, *lrange)
1024 1024 for i, (c, lr) in enumerate(it, 1):
1025 1025 diffs = None
1026 1026 if patch:
1027 1027 diffs = diff(c, linerange=lr)
1028 1028 # follow renames accross filtered (not in range) revisions
1029 1029 path = c.path()
1030 1030 entries.append(dict(
1031 1031 parity=next(parity),
1032 1032 filerev=c.rev(),
1033 1033 file=path,
1034 1034 diff=diffs,
1035 1035 linerange=webutil.formatlinerange(*lr),
1036 1036 **webutil.commonentry(repo, c)))
1037 1037 if i == revcount:
1038 1038 break
1039 1039 lessvars['linerange'] = webutil.formatlinerange(*lrange)
1040 1040 morevars['linerange'] = lessvars['linerange']
1041 1041 else:
1042 1042 for i in revs:
1043 1043 iterfctx = fctx.filectx(i)
1044 1044 diffs = None
1045 1045 if patch:
1046 1046 diffs = diff(iterfctx)
1047 1047 entries.append(dict(
1048 1048 parity=next(parity),
1049 1049 filerev=i,
1050 1050 file=f,
1051 1051 diff=diffs,
1052 1052 rename=webutil.renamelink(iterfctx),
1053 1053 **webutil.commonentry(repo, iterfctx)))
1054 1054 entries.reverse()
1055 1055 revnav = webutil.filerevnav(web.repo, fctx.path())
1056 1056 nav = revnav.gen(end - 1, revcount, count)
1057 1057
1058 1058 latestentry = entries[:1]
1059 1059
1060 1060 return tmpl("filelog",
1061 1061 file=f,
1062 1062 nav=nav,
1063 1063 symrev=webutil.symrevorshortnode(req, fctx),
1064 1064 entries=entries,
1065 1065 descend=descend,
1066 1066 patch=patch,
1067 1067 latestentry=latestentry,
1068 1068 linerange=linerange,
1069 1069 revcount=revcount,
1070 1070 morevars=morevars,
1071 1071 lessvars=lessvars,
1072 1072 **webutil.commonentry(web.repo, fctx))
1073 1073
1074 1074 @webcommand('archive')
1075 1075 def archive(web, req, tmpl):
1076 1076 """
1077 1077 /archive/{revision}.{format}[/{path}]
1078 1078 -------------------------------------
1079 1079
1080 1080 Obtain an archive of repository content.
1081 1081
1082 1082 The content and type of the archive is defined by a URL path parameter.
1083 1083 ``format`` is the file extension of the archive type to be generated. e.g.
1084 1084 ``zip`` or ``tar.bz2``. Not all archive types may be allowed by your
1085 1085 server configuration.
1086 1086
1087 1087 The optional ``path`` URL parameter controls content to include in the
1088 1088 archive. If omitted, every file in the specified revision is present in the
1089 1089 archive. If included, only the specified file or contents of the specified
1090 1090 directory will be included in the archive.
1091 1091
1092 1092 No template is used for this handler. Raw, binary content is generated.
1093 1093 """
1094 1094
1095 1095 type_ = req.form.get('type', [None])[0]
1096 1096 allowed = web.configlist("web", "allow_archive")
1097 1097 key = req.form['node'][0]
1098 1098
1099 1099 if type_ not in web.archivespecs:
1100 1100 msg = 'Unsupported archive type: %s' % type_
1101 1101 raise ErrorResponse(HTTP_NOT_FOUND, msg)
1102 1102
1103 1103 if not ((type_ in allowed or
1104 1104 web.configbool("web", "allow" + type_, False))):
1105 1105 msg = 'Archive type not allowed: %s' % type_
1106 1106 raise ErrorResponse(HTTP_FORBIDDEN, msg)
1107 1107
1108 1108 reponame = re.sub(r"\W+", "-", os.path.basename(web.reponame))
1109 1109 cnode = web.repo.lookup(key)
1110 1110 arch_version = key
1111 1111 if cnode == key or key == 'tip':
1112 1112 arch_version = short(cnode)
1113 1113 name = "%s-%s" % (reponame, arch_version)
1114 1114
1115 1115 ctx = webutil.changectx(web.repo, req)
1116 1116 pats = []
1117 1117 match = scmutil.match(ctx, [])
1118 1118 file = req.form.get('file', None)
1119 1119 if file:
1120 1120 pats = ['path:' + file[0]]
1121 1121 match = scmutil.match(ctx, pats, default='path')
1122 1122 if pats:
1123 1123 files = [f for f in ctx.manifest().keys() if match(f)]
1124 1124 if not files:
1125 1125 raise ErrorResponse(HTTP_NOT_FOUND,
1126 1126 'file(s) not found: %s' % file[0])
1127 1127
1128 1128 mimetype, artype, extension, encoding = web.archivespecs[type_]
1129 1129 headers = [
1130 1130 ('Content-Disposition', 'attachment; filename=%s%s' % (name, extension))
1131 1131 ]
1132 1132 if encoding:
1133 1133 headers.append(('Content-Encoding', encoding))
1134 1134 req.headers.extend(headers)
1135 1135 req.respond(HTTP_OK, mimetype)
1136 1136
1137 1137 archival.archive(web.repo, req, cnode, artype, prefix=name,
1138 1138 matchfn=match,
1139 1139 subrepos=web.configbool("web", "archivesubrepos"))
1140 1140 return []
1141 1141
1142 1142
1143 1143 @webcommand('static')
1144 1144 def static(web, req, tmpl):
1145 1145 fname = req.form['file'][0]
1146 1146 # a repo owner may set web.static in .hg/hgrc to get any file
1147 1147 # readable by the user running the CGI script
1148 1148 static = web.config("web", "static", None, untrusted=False)
1149 1149 if not static:
1150 1150 tp = web.templatepath or templater.templatepaths()
1151 1151 if isinstance(tp, str):
1152 1152 tp = [tp]
1153 1153 static = [os.path.join(p, 'static') for p in tp]
1154 1154 staticfile(static, fname, req)
1155 1155 return []
1156 1156
1157 1157 @webcommand('graph')
1158 1158 def graph(web, req, tmpl):
1159 1159 """
1160 1160 /graph[/{revision}]
1161 1161 -------------------
1162 1162
1163 1163 Show information about the graphical topology of the repository.
1164 1164
1165 1165 Information rendered by this handler can be used to create visual
1166 1166 representations of repository topology.
1167 1167
1168 1168 The ``revision`` URL parameter controls the starting changeset.
1169 1169
1170 1170 The ``revcount`` query string argument can define the number of changesets
1171 1171 to show information for.
1172 1172
1173 1173 This handler will render the ``graph`` template.
1174 1174 """
1175 1175
1176 1176 if 'node' in req.form:
1177 1177 ctx = webutil.changectx(web.repo, req)
1178 1178 symrev = webutil.symrevorshortnode(req, ctx)
1179 1179 else:
1180 1180 ctx = web.repo['tip']
1181 1181 symrev = 'tip'
1182 1182 rev = ctx.rev()
1183 1183
1184 1184 bg_height = 39
1185 1185 revcount = web.maxshortchanges
1186 1186 if 'revcount' in req.form:
1187 1187 try:
1188 1188 revcount = int(req.form.get('revcount', [revcount])[0])
1189 1189 revcount = max(revcount, 1)
1190 1190 tmpl.defaults['sessionvars']['revcount'] = revcount
1191 1191 except ValueError:
1192 1192 pass
1193 1193
1194 1194 lessvars = copy.copy(tmpl.defaults['sessionvars'])
1195 1195 lessvars['revcount'] = max(revcount / 2, 1)
1196 1196 morevars = copy.copy(tmpl.defaults['sessionvars'])
1197 1197 morevars['revcount'] = revcount * 2
1198 1198
1199 1199 count = len(web.repo)
1200 1200 pos = rev
1201 1201
1202 1202 uprev = min(max(0, count - 1), rev + revcount)
1203 1203 downrev = max(0, rev - revcount)
1204 1204 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
1205 1205
1206 1206 tree = []
1207 1207 if pos != -1:
1208 1208 allrevs = web.repo.changelog.revs(pos, 0)
1209 1209 revs = []
1210 1210 for i in allrevs:
1211 1211 revs.append(i)
1212 1212 if len(revs) >= revcount:
1213 1213 break
1214 1214
1215 1215 # We have to feed a baseset to dagwalker as it is expecting smartset
1216 1216 # object. This does not have a big impact on hgweb performance itself
1217 1217 # since hgweb graphing code is not itself lazy yet.
1218 1218 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1219 1219 # As we said one line above... not lazy.
1220 1220 tree = list(graphmod.colored(dag, web.repo))
1221 1221
1222 1222 def getcolumns(tree):
1223 1223 cols = 0
1224 1224 for (id, type, ctx, vtx, edges) in tree:
1225 1225 if type != graphmod.CHANGESET:
1226 1226 continue
1227 1227 cols = max(cols, max([edge[0] for edge in edges] or [0]),
1228 1228 max([edge[1] for edge in edges] or [0]))
1229 1229 return cols
1230 1230
1231 1231 def graphdata(usetuples, encodestr):
1232 1232 data = []
1233 1233
1234 1234 row = 0
1235 1235 for (id, type, ctx, vtx, edges) in tree:
1236 1236 if type != graphmod.CHANGESET:
1237 1237 continue
1238 1238 node = str(ctx)
1239 1239 age = encodestr(templatefilters.age(ctx.date()))
1240 1240 desc = templatefilters.firstline(encodestr(ctx.description()))
1241 1241 desc = cgi.escape(templatefilters.nonempty(desc))
1242 1242 user = cgi.escape(templatefilters.person(encodestr(ctx.user())))
1243 1243 branch = cgi.escape(encodestr(ctx.branch()))
1244 1244 try:
1245 1245 branchnode = web.repo.branchtip(branch)
1246 1246 except error.RepoLookupError:
1247 1247 branchnode = None
1248 1248 branch = branch, branchnode == ctx.node()
1249 1249
1250 1250 if usetuples:
1251 1251 data.append((node, vtx, edges, desc, user, age, branch,
1252 1252 [cgi.escape(encodestr(x)) for x in ctx.tags()],
1253 1253 [cgi.escape(encodestr(x))
1254 1254 for x in ctx.bookmarks()]))
1255 1255 else:
1256 1256 edgedata = [{'col': edge[0], 'nextcol': edge[1],
1257 1257 'color': (edge[2] - 1) % 6 + 1,
1258 1258 'width': edge[3], 'bcolor': edge[4]}
1259 1259 for edge in edges]
1260 1260
1261 1261 data.append(
1262 1262 {'node': node,
1263 1263 'col': vtx[0],
1264 1264 'color': (vtx[1] - 1) % 6 + 1,
1265 1265 'edges': edgedata,
1266 1266 'row': row,
1267 1267 'nextrow': row + 1,
1268 1268 'desc': desc,
1269 1269 'user': user,
1270 1270 'age': age,
1271 1271 'bookmarks': webutil.nodebookmarksdict(
1272 1272 web.repo, ctx.node()),
1273 1273 'branches': webutil.nodebranchdict(web.repo, ctx),
1274 1274 'inbranch': webutil.nodeinbranch(web.repo, ctx),
1275 1275 'tags': webutil.nodetagsdict(web.repo, ctx.node())})
1276 1276
1277 1277 row += 1
1278 1278
1279 1279 return data
1280 1280
1281 1281 cols = getcolumns(tree)
1282 1282 rows = len(tree)
1283 1283 canvasheight = (rows + 1) * bg_height - 27
1284 1284
1285 1285 return tmpl('graph', rev=rev, symrev=symrev, revcount=revcount,
1286 1286 uprev=uprev,
1287 1287 lessvars=lessvars, morevars=morevars, downrev=downrev,
1288 1288 cols=cols, rows=rows,
1289 1289 canvaswidth=(cols + 1) * bg_height,
1290 1290 truecanvasheight=rows * bg_height,
1291 1291 canvasheight=canvasheight, bg_height=bg_height,
1292 1292 # {jsdata} will be passed to |json, so it must be in utf-8
1293 1293 jsdata=lambda **x: graphdata(True, encoding.fromlocal),
1294 1294 nodes=lambda **x: graphdata(False, str),
1295 1295 node=ctx.hex(), changenav=changenav)
1296 1296
1297 1297 def _getdoc(e):
1298 1298 doc = e[0].__doc__
1299 1299 if doc:
1300 1300 doc = _(doc).partition('\n')[0]
1301 1301 else:
1302 1302 doc = _('(no help text available)')
1303 1303 return doc
1304 1304
1305 1305 @webcommand('help')
1306 1306 def help(web, req, tmpl):
1307 1307 """
1308 1308 /help[/{topic}]
1309 1309 ---------------
1310 1310
1311 1311 Render help documentation.
1312 1312
1313 1313 This web command is roughly equivalent to :hg:`help`. If a ``topic``
1314 1314 is defined, that help topic will be rendered. If not, an index of
1315 1315 available help topics will be rendered.
1316 1316
1317 1317 The ``help`` template will be rendered when requesting help for a topic.
1318 1318 ``helptopics`` will be rendered for the index of help topics.
1319 1319 """
1320 1320 from .. import commands, help as helpmod # avoid cycle
1321 1321
1322 1322 topicname = req.form.get('node', [None])[0]
1323 1323 if not topicname:
1324 1324 def topics(**map):
1325 1325 for entries, summary, _doc in helpmod.helptable:
1326 1326 yield {'topic': entries[0], 'summary': summary}
1327 1327
1328 1328 early, other = [], []
1329 1329 primary = lambda s: s.partition('|')[0]
1330 1330 for c, e in commands.table.iteritems():
1331 1331 doc = _getdoc(e)
1332 1332 if 'DEPRECATED' in doc or c.startswith('debug'):
1333 1333 continue
1334 1334 cmd = primary(c)
1335 1335 if cmd.startswith('^'):
1336 1336 early.append((cmd[1:], doc))
1337 1337 else:
1338 1338 other.append((cmd, doc))
1339 1339
1340 1340 early.sort()
1341 1341 other.sort()
1342 1342
1343 1343 def earlycommands(**map):
1344 1344 for c, doc in early:
1345 1345 yield {'topic': c, 'summary': doc}
1346 1346
1347 1347 def othercommands(**map):
1348 1348 for c, doc in other:
1349 1349 yield {'topic': c, 'summary': doc}
1350 1350
1351 1351 return tmpl('helptopics', topics=topics, earlycommands=earlycommands,
1352 1352 othercommands=othercommands, title='Index')
1353 1353
1354 1354 # Render an index of sub-topics.
1355 1355 if topicname in helpmod.subtopics:
1356 1356 topics = []
1357 1357 for entries, summary, _doc in helpmod.subtopics[topicname]:
1358 1358 topics.append({
1359 1359 'topic': '%s.%s' % (topicname, entries[0]),
1360 1360 'basename': entries[0],
1361 1361 'summary': summary,
1362 1362 })
1363 1363
1364 1364 return tmpl('helptopics', topics=topics, title=topicname,
1365 1365 subindex=True)
1366 1366
1367 1367 u = webutil.wsgiui.load()
1368 1368 u.verbose = True
1369 1369
1370 1370 # Render a page from a sub-topic.
1371 1371 if '.' in topicname:
1372 1372 # TODO implement support for rendering sections, like
1373 1373 # `hg help` works.
1374 1374 topic, subtopic = topicname.split('.', 1)
1375 1375 if topic not in helpmod.subtopics:
1376 1376 raise ErrorResponse(HTTP_NOT_FOUND)
1377 1377 else:
1378 1378 topic = topicname
1379 1379 subtopic = None
1380 1380
1381 1381 try:
1382 1382 doc = helpmod.help_(u, commands, topic, subtopic=subtopic)
1383 1383 except error.UnknownCommand:
1384 1384 raise ErrorResponse(HTTP_NOT_FOUND)
1385 1385 return tmpl('help', topic=topicname, doc=doc)
1386 1386
1387 1387 # tell hggettext to extract docstrings from these functions:
1388 1388 i18nfunctions = commands.values()
@@ -1,628 +1,628 b''
1 1 # hgweb/webutil.py - utility library for the web interface.
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 copy
12 12 import difflib
13 13 import os
14 14 import re
15 15
16 16 from ..i18n import _
17 17 from ..node import hex, nullid, short
18 18
19 19 from .common import (
20 20 ErrorResponse,
21 21 HTTP_BAD_REQUEST,
22 22 HTTP_NOT_FOUND,
23 23 paritygen,
24 24 )
25 25
26 26 from .. import (
27 27 context,
28 28 error,
29 29 match,
30 30 mdiff,
31 31 patch,
32 32 pathutil,
33 33 templatefilters,
34 34 ui as uimod,
35 35 util,
36 36 )
37 37
38 38 def up(p):
39 39 if p[0] != "/":
40 40 p = "/" + p
41 41 if p[-1] == "/":
42 42 p = p[:-1]
43 43 up = os.path.dirname(p)
44 44 if up == "/":
45 45 return "/"
46 46 return up + "/"
47 47
48 48 def _navseq(step, firststep=None):
49 49 if firststep:
50 50 yield firststep
51 51 if firststep >= 20 and firststep <= 40:
52 52 firststep = 50
53 53 yield firststep
54 54 assert step > 0
55 55 assert firststep > 0
56 56 while step <= firststep:
57 57 step *= 10
58 58 while True:
59 59 yield 1 * step
60 60 yield 3 * step
61 61 step *= 10
62 62
63 63 class revnav(object):
64 64
65 65 def __init__(self, repo):
66 66 """Navigation generation object
67 67
68 68 :repo: repo object we generate nav for
69 69 """
70 70 # used for hex generation
71 71 self._revlog = repo.changelog
72 72
73 73 def __nonzero__(self):
74 74 """return True if any revision to navigate over"""
75 75 return self._first() is not None
76 76
77 77 __bool__ = __nonzero__
78 78
79 79 def _first(self):
80 80 """return the minimum non-filtered changeset or None"""
81 81 try:
82 82 return next(iter(self._revlog))
83 83 except StopIteration:
84 84 return None
85 85
86 86 def hex(self, rev):
87 87 return hex(self._revlog.node(rev))
88 88
89 89 def gen(self, pos, pagelen, limit):
90 90 """computes label and revision id for navigation link
91 91
92 92 :pos: is the revision relative to which we generate navigation.
93 93 :pagelen: the size of each navigation page
94 94 :limit: how far shall we link
95 95
96 96 The return is:
97 97 - a single element tuple
98 98 - containing a dictionary with a `before` and `after` key
99 99 - values are generator functions taking arbitrary number of kwargs
100 100 - yield items are dictionaries with `label` and `node` keys
101 101 """
102 102 if not self:
103 103 # empty repo
104 104 return ({'before': (), 'after': ()},)
105 105
106 106 targets = []
107 107 for f in _navseq(1, pagelen):
108 108 if f > limit:
109 109 break
110 110 targets.append(pos + f)
111 111 targets.append(pos - f)
112 112 targets.sort()
113 113
114 114 first = self._first()
115 115 navbefore = [("(%i)" % first, self.hex(first))]
116 116 navafter = []
117 117 for rev in targets:
118 118 if rev not in self._revlog:
119 119 continue
120 120 if pos < rev < limit:
121 121 navafter.append(("+%d" % abs(rev - pos), self.hex(rev)))
122 122 if 0 < rev < pos:
123 123 navbefore.append(("-%d" % abs(rev - pos), self.hex(rev)))
124 124
125 125
126 126 navafter.append(("tip", "tip"))
127 127
128 128 data = lambda i: {"label": i[0], "node": i[1]}
129 129 return ({'before': lambda **map: (data(i) for i in navbefore),
130 130 'after': lambda **map: (data(i) for i in navafter)},)
131 131
132 132 class filerevnav(revnav):
133 133
134 134 def __init__(self, repo, path):
135 135 """Navigation generation object
136 136
137 137 :repo: repo object we generate nav for
138 138 :path: path of the file we generate nav for
139 139 """
140 140 # used for iteration
141 141 self._changelog = repo.unfiltered().changelog
142 142 # used for hex generation
143 143 self._revlog = repo.file(path)
144 144
145 145 def hex(self, rev):
146 146 return hex(self._changelog.node(self._revlog.linkrev(rev)))
147 147
148 148 class _siblings(object):
149 149 def __init__(self, siblings=None, hiderev=None):
150 150 if siblings is None:
151 151 siblings = []
152 152 self.siblings = [s for s in siblings if s.node() != nullid]
153 153 if len(self.siblings) == 1 and self.siblings[0].rev() == hiderev:
154 154 self.siblings = []
155 155
156 156 def __iter__(self):
157 157 for s in self.siblings:
158 158 d = {
159 159 'node': s.hex(),
160 160 'rev': s.rev(),
161 161 'user': s.user(),
162 162 'date': s.date(),
163 163 'description': s.description(),
164 164 'branch': s.branch(),
165 165 }
166 166 if util.safehasattr(s, 'path'):
167 167 d['file'] = s.path()
168 168 yield d
169 169
170 170 def __len__(self):
171 171 return len(self.siblings)
172 172
173 173 def annotate(fctx, ui):
174 174 diffopts = patch.difffeatureopts(ui, untrusted=True,
175 175 section='annotate', whitespace=True)
176 176 return fctx.annotate(follow=True, linenumber=True, diffopts=diffopts)
177 177
178 178 def parents(ctx, hide=None):
179 179 if isinstance(ctx, context.basefilectx):
180 180 introrev = ctx.introrev()
181 181 if ctx.changectx().rev() != introrev:
182 182 return _siblings([ctx.repo()[introrev]], hide)
183 183 return _siblings(ctx.parents(), hide)
184 184
185 185 def children(ctx, hide=None):
186 186 return _siblings(ctx.children(), hide)
187 187
188 188 def renamelink(fctx):
189 189 r = fctx.renamed()
190 190 if r:
191 191 return [{'file': r[0], 'node': hex(r[1])}]
192 192 return []
193 193
194 194 def nodetagsdict(repo, node):
195 195 return [{"name": i} for i in repo.nodetags(node)]
196 196
197 197 def nodebookmarksdict(repo, node):
198 198 return [{"name": i} for i in repo.nodebookmarks(node)]
199 199
200 200 def nodebranchdict(repo, ctx):
201 201 branches = []
202 202 branch = ctx.branch()
203 203 # If this is an empty repo, ctx.node() == nullid,
204 204 # ctx.branch() == 'default'.
205 205 try:
206 206 branchnode = repo.branchtip(branch)
207 207 except error.RepoLookupError:
208 208 branchnode = None
209 209 if branchnode == ctx.node():
210 210 branches.append({"name": branch})
211 211 return branches
212 212
213 213 def nodeinbranch(repo, ctx):
214 214 branches = []
215 215 branch = ctx.branch()
216 216 try:
217 217 branchnode = repo.branchtip(branch)
218 218 except error.RepoLookupError:
219 219 branchnode = None
220 220 if branch != 'default' and branchnode != ctx.node():
221 221 branches.append({"name": branch})
222 222 return branches
223 223
224 224 def nodebranchnodefault(ctx):
225 225 branches = []
226 226 branch = ctx.branch()
227 227 if branch != 'default':
228 228 branches.append({"name": branch})
229 229 return branches
230 230
231 231 def showtag(repo, tmpl, t1, node=nullid, **args):
232 232 for t in repo.nodetags(node):
233 233 yield tmpl(t1, tag=t, **args)
234 234
235 235 def showbookmark(repo, tmpl, t1, node=nullid, **args):
236 236 for t in repo.nodebookmarks(node):
237 237 yield tmpl(t1, bookmark=t, **args)
238 238
239 239 def branchentries(repo, stripecount, limit=0):
240 240 tips = []
241 241 heads = repo.heads()
242 242 parity = paritygen(stripecount)
243 243 sortkey = lambda item: (not item[1], item[0].rev())
244 244
245 245 def entries(**map):
246 246 count = 0
247 247 if not tips:
248 248 for tag, hs, tip, closed in repo.branchmap().iterbranches():
249 249 tips.append((repo[tip], closed))
250 250 for ctx, closed in sorted(tips, key=sortkey, reverse=True):
251 251 if limit > 0 and count >= limit:
252 252 return
253 253 count += 1
254 254 if closed:
255 255 status = 'closed'
256 256 elif ctx.node() not in heads:
257 257 status = 'inactive'
258 258 else:
259 259 status = 'open'
260 260 yield {
261 261 'parity': next(parity),
262 262 'branch': ctx.branch(),
263 263 'status': status,
264 264 'node': ctx.hex(),
265 265 'date': ctx.date()
266 266 }
267 267
268 268 return entries
269 269
270 270 def cleanpath(repo, path):
271 271 path = path.lstrip('/')
272 272 return pathutil.canonpath(repo.root, '', path)
273 273
274 274 def changeidctx(repo, changeid):
275 275 try:
276 276 ctx = repo[changeid]
277 277 except error.RepoError:
278 278 man = repo.manifestlog._revlog
279 279 ctx = repo[man.linkrev(man.rev(man.lookup(changeid)))]
280 280
281 281 return ctx
282 282
283 283 def changectx(repo, req):
284 284 changeid = "tip"
285 285 if 'node' in req.form:
286 286 changeid = req.form['node'][0]
287 287 ipos = changeid.find(':')
288 288 if ipos != -1:
289 289 changeid = changeid[(ipos + 1):]
290 290 elif 'manifest' in req.form:
291 291 changeid = req.form['manifest'][0]
292 292
293 293 return changeidctx(repo, changeid)
294 294
295 295 def basechangectx(repo, req):
296 296 if 'node' in req.form:
297 297 changeid = req.form['node'][0]
298 298 ipos = changeid.find(':')
299 299 if ipos != -1:
300 300 changeid = changeid[:ipos]
301 301 return changeidctx(repo, changeid)
302 302
303 303 return None
304 304
305 305 def filectx(repo, req):
306 306 if 'file' not in req.form:
307 307 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
308 308 path = cleanpath(repo, req.form['file'][0])
309 309 if 'node' in req.form:
310 310 changeid = req.form['node'][0]
311 311 elif 'filenode' in req.form:
312 312 changeid = req.form['filenode'][0]
313 313 else:
314 314 raise ErrorResponse(HTTP_NOT_FOUND, 'node or filenode not given')
315 315 try:
316 316 fctx = repo[changeid][path]
317 317 except error.RepoError:
318 318 fctx = repo.filectx(path, fileid=changeid)
319 319
320 320 return fctx
321 321
322 322 def linerange(req):
323 323 linerange = req.form.get('linerange')
324 324 if linerange is None:
325 325 return None
326 326 if len(linerange) > 1:
327 327 raise ErrorResponse(HTTP_BAD_REQUEST,
328 328 'redundant linerange parameter')
329 329 try:
330 330 fromline, toline = map(int, linerange[0].split(':', 1))
331 331 except ValueError:
332 332 raise ErrorResponse(HTTP_BAD_REQUEST,
333 333 'invalid linerange parameter')
334 334 try:
335 335 return util.processlinerange(fromline, toline)
336 336 except error.ParseError as exc:
337 337 raise ErrorResponse(HTTP_BAD_REQUEST, str(exc))
338 338
339 339 def formatlinerange(fromline, toline):
340 340 return '%d:%d' % (fromline + 1, toline)
341 341
342 342 def commonentry(repo, ctx):
343 343 node = ctx.node()
344 344 return {
345 345 'rev': ctx.rev(),
346 346 'node': hex(node),
347 347 'author': ctx.user(),
348 348 'desc': ctx.description(),
349 349 'date': ctx.date(),
350 350 'extra': ctx.extra(),
351 351 'phase': ctx.phasestr(),
352 352 'branch': nodebranchnodefault(ctx),
353 353 'inbranch': nodeinbranch(repo, ctx),
354 354 'branches': nodebranchdict(repo, ctx),
355 355 'tags': nodetagsdict(repo, node),
356 356 'bookmarks': nodebookmarksdict(repo, node),
357 357 'parent': lambda **x: parents(ctx),
358 358 'child': lambda **x: children(ctx),
359 359 }
360 360
361 361 def changelistentry(web, ctx, tmpl):
362 362 '''Obtain a dictionary to be used for entries in a changelist.
363 363
364 364 This function is called when producing items for the "entries" list passed
365 365 to the "shortlog" and "changelog" templates.
366 366 '''
367 367 repo = web.repo
368 368 rev = ctx.rev()
369 369 n = ctx.node()
370 370 showtags = showtag(repo, tmpl, 'changelogtag', n)
371 371 files = listfilediffs(tmpl, ctx.files(), n, web.maxfiles)
372 372
373 373 entry = commonentry(repo, ctx)
374 374 entry.update(
375 375 allparents=lambda **x: parents(ctx),
376 376 parent=lambda **x: parents(ctx, rev - 1),
377 377 child=lambda **x: children(ctx, rev + 1),
378 378 changelogtag=showtags,
379 379 files=files,
380 380 )
381 381 return entry
382 382
383 383 def symrevorshortnode(req, ctx):
384 384 if 'node' in req.form:
385 385 return templatefilters.revescape(req.form['node'][0])
386 386 else:
387 387 return short(ctx.node())
388 388
389 389 def changesetentry(web, req, tmpl, ctx):
390 390 '''Obtain a dictionary to be used to render the "changeset" template.'''
391 391
392 392 showtags = showtag(web.repo, tmpl, 'changesettag', ctx.node())
393 393 showbookmarks = showbookmark(web.repo, tmpl, 'changesetbookmark',
394 394 ctx.node())
395 395 showbranch = nodebranchnodefault(ctx)
396 396
397 397 files = []
398 398 parity = paritygen(web.stripecount)
399 399 for blockno, f in enumerate(ctx.files()):
400 400 template = f in ctx and 'filenodelink' or 'filenolink'
401 401 files.append(tmpl(template,
402 402 node=ctx.hex(), file=f, blockno=blockno + 1,
403 403 parity=next(parity)))
404 404
405 405 basectx = basechangectx(web.repo, req)
406 406 if basectx is None:
407 407 basectx = ctx.p1()
408 408
409 style = web.config('web', 'style', 'paper')
409 style = web.config('web', 'style')
410 410 if 'style' in req.form:
411 411 style = req.form['style'][0]
412 412
413 413 diff = diffs(web, tmpl, ctx, basectx, None, style)
414 414
415 415 parity = paritygen(web.stripecount)
416 416 diffstatsgen = diffstatgen(ctx, basectx)
417 417 diffstats = diffstat(tmpl, ctx, diffstatsgen, parity)
418 418
419 419 return dict(
420 420 diff=diff,
421 421 symrev=symrevorshortnode(req, ctx),
422 422 basenode=basectx.hex(),
423 423 changesettag=showtags,
424 424 changesetbookmark=showbookmarks,
425 425 changesetbranch=showbranch,
426 426 files=files,
427 427 diffsummary=lambda **x: diffsummary(diffstatsgen),
428 428 diffstat=diffstats,
429 429 archives=web.archivelist(ctx.hex()),
430 430 **commonentry(web.repo, ctx))
431 431
432 432 def listfilediffs(tmpl, files, node, max):
433 433 for f in files[:max]:
434 434 yield tmpl('filedifflink', node=hex(node), file=f)
435 435 if len(files) > max:
436 436 yield tmpl('fileellipses')
437 437
438 438 def diffs(web, tmpl, ctx, basectx, files, style, linerange=None,
439 439 lineidprefix=''):
440 440
441 441 def prettyprintlines(lines, blockno):
442 442 for lineno, l in enumerate(lines, 1):
443 443 difflineno = "%d.%d" % (blockno, lineno)
444 444 if l.startswith('+'):
445 445 ltype = "difflineplus"
446 446 elif l.startswith('-'):
447 447 ltype = "difflineminus"
448 448 elif l.startswith('@'):
449 449 ltype = "difflineat"
450 450 else:
451 451 ltype = "diffline"
452 452 yield tmpl(ltype,
453 453 line=l,
454 454 lineno=lineno,
455 455 lineid=lineidprefix + "l%s" % difflineno,
456 456 linenumber="% 8s" % difflineno)
457 457
458 458 repo = web.repo
459 459 if files:
460 460 m = match.exact(repo.root, repo.getcwd(), files)
461 461 else:
462 462 m = match.always(repo.root, repo.getcwd())
463 463
464 464 diffopts = patch.diffopts(repo.ui, untrusted=True)
465 465 node1 = basectx.node()
466 466 node2 = ctx.node()
467 467 parity = paritygen(web.stripecount)
468 468
469 469 diffhunks = patch.diffhunks(repo, node1, node2, m, opts=diffopts)
470 470 for blockno, (header, hunks) in enumerate(diffhunks, 1):
471 471 if style != 'raw':
472 472 header = header[1:]
473 473 lines = [h + '\n' for h in header]
474 474 for hunkrange, hunklines in hunks:
475 475 if linerange is not None and hunkrange is not None:
476 476 s1, l1, s2, l2 = hunkrange
477 477 if not mdiff.hunkinrange((s2, l2), linerange):
478 478 continue
479 479 lines.extend(hunklines)
480 480 if lines:
481 481 yield tmpl('diffblock', parity=next(parity), blockno=blockno,
482 482 lines=prettyprintlines(lines, blockno))
483 483
484 484 def compare(tmpl, context, leftlines, rightlines):
485 485 '''Generator function that provides side-by-side comparison data.'''
486 486
487 487 def compline(type, leftlineno, leftline, rightlineno, rightline):
488 488 lineid = leftlineno and ("l%s" % leftlineno) or ''
489 489 lineid += rightlineno and ("r%s" % rightlineno) or ''
490 490 return tmpl('comparisonline',
491 491 type=type,
492 492 lineid=lineid,
493 493 leftlineno=leftlineno,
494 494 leftlinenumber="% 6s" % (leftlineno or ''),
495 495 leftline=leftline or '',
496 496 rightlineno=rightlineno,
497 497 rightlinenumber="% 6s" % (rightlineno or ''),
498 498 rightline=rightline or '')
499 499
500 500 def getblock(opcodes):
501 501 for type, llo, lhi, rlo, rhi in opcodes:
502 502 len1 = lhi - llo
503 503 len2 = rhi - rlo
504 504 count = min(len1, len2)
505 505 for i in xrange(count):
506 506 yield compline(type=type,
507 507 leftlineno=llo + i + 1,
508 508 leftline=leftlines[llo + i],
509 509 rightlineno=rlo + i + 1,
510 510 rightline=rightlines[rlo + i])
511 511 if len1 > len2:
512 512 for i in xrange(llo + count, lhi):
513 513 yield compline(type=type,
514 514 leftlineno=i + 1,
515 515 leftline=leftlines[i],
516 516 rightlineno=None,
517 517 rightline=None)
518 518 elif len2 > len1:
519 519 for i in xrange(rlo + count, rhi):
520 520 yield compline(type=type,
521 521 leftlineno=None,
522 522 leftline=None,
523 523 rightlineno=i + 1,
524 524 rightline=rightlines[i])
525 525
526 526 s = difflib.SequenceMatcher(None, leftlines, rightlines)
527 527 if context < 0:
528 528 yield tmpl('comparisonblock', lines=getblock(s.get_opcodes()))
529 529 else:
530 530 for oc in s.get_grouped_opcodes(n=context):
531 531 yield tmpl('comparisonblock', lines=getblock(oc))
532 532
533 533 def diffstatgen(ctx, basectx):
534 534 '''Generator function that provides the diffstat data.'''
535 535
536 536 stats = patch.diffstatdata(util.iterlines(ctx.diff(basectx)))
537 537 maxname, maxtotal, addtotal, removetotal, binary = patch.diffstatsum(stats)
538 538 while True:
539 539 yield stats, maxname, maxtotal, addtotal, removetotal, binary
540 540
541 541 def diffsummary(statgen):
542 542 '''Return a short summary of the diff.'''
543 543
544 544 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
545 545 return _(' %d files changed, %d insertions(+), %d deletions(-)\n') % (
546 546 len(stats), addtotal, removetotal)
547 547
548 548 def diffstat(tmpl, ctx, statgen, parity):
549 549 '''Return a diffstat template for each file in the diff.'''
550 550
551 551 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
552 552 files = ctx.files()
553 553
554 554 def pct(i):
555 555 if maxtotal == 0:
556 556 return 0
557 557 return (float(i) / maxtotal) * 100
558 558
559 559 fileno = 0
560 560 for filename, adds, removes, isbinary in stats:
561 561 template = filename in files and 'diffstatlink' or 'diffstatnolink'
562 562 total = adds + removes
563 563 fileno += 1
564 564 yield tmpl(template, node=ctx.hex(), file=filename, fileno=fileno,
565 565 total=total, addpct=pct(adds), removepct=pct(removes),
566 566 parity=next(parity))
567 567
568 568 class sessionvars(object):
569 569 def __init__(self, vars, start='?'):
570 570 self.start = start
571 571 self.vars = vars
572 572 def __getitem__(self, key):
573 573 return self.vars[key]
574 574 def __setitem__(self, key, value):
575 575 self.vars[key] = value
576 576 def __copy__(self):
577 577 return sessionvars(copy.copy(self.vars), self.start)
578 578 def __iter__(self):
579 579 separator = self.start
580 580 for key, value in sorted(self.vars.iteritems()):
581 581 yield {'name': key, 'value': str(value), 'separator': separator}
582 582 separator = '&'
583 583
584 584 class wsgiui(uimod.ui):
585 585 # default termwidth breaks under mod_wsgi
586 586 def termwidth(self):
587 587 return 80
588 588
589 589 def getwebsubs(repo):
590 590 websubtable = []
591 591 websubdefs = repo.ui.configitems('websub')
592 592 # we must maintain interhg backwards compatibility
593 593 websubdefs += repo.ui.configitems('interhg')
594 594 for key, pattern in websubdefs:
595 595 # grab the delimiter from the character after the "s"
596 596 unesc = pattern[1]
597 597 delim = re.escape(unesc)
598 598
599 599 # identify portions of the pattern, taking care to avoid escaped
600 600 # delimiters. the replace format and flags are optional, but
601 601 # delimiters are required.
602 602 match = re.match(
603 603 r'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
604 604 % (delim, delim, delim), pattern)
605 605 if not match:
606 606 repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
607 607 % (key, pattern))
608 608 continue
609 609
610 610 # we need to unescape the delimiter for regexp and format
611 611 delim_re = re.compile(r'(?<!\\)\\%s' % delim)
612 612 regexp = delim_re.sub(unesc, match.group(1))
613 613 format = delim_re.sub(unesc, match.group(2))
614 614
615 615 # the pattern allows for 6 regexp flags, so set them if necessary
616 616 flagin = match.group(3)
617 617 flags = 0
618 618 if flagin:
619 619 for flag in flagin.upper():
620 620 flags |= re.__dict__[flag]
621 621
622 622 try:
623 623 regexp = re.compile(regexp, flags)
624 624 websubtable.append((regexp, format))
625 625 except re.error:
626 626 repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
627 627 % (key, regexp))
628 628 return websubtable
General Comments 0
You need to be logged in to leave comments. Login now