##// END OF EJS Templates
configitems: drop redundant default of web.allow<archtype>...
Yuya Nishihara -
r34655:4182d206 default
parent child Browse files
Show More
@@ -1,1400 +1,1400 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')
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 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 ``ignorews``, ``ignorewsamount``, ``ignorewseol``, and
867 867 ``ignoreblanklines`` query string arguments have the same meaning as
868 868 their ``[annotate]`` config equivalents. It uses the hgrc boolean
869 869 parsing logic to interpret the value. e.g. ``0`` and ``false`` are
870 870 false and ``1`` and ``true`` are true. If not defined, the server
871 871 default settings are used.
872 872
873 873 The ``fileannotate`` template is rendered.
874 874 """
875 875 fctx = webutil.filectx(web.repo, req)
876 876 f = fctx.path()
877 877 parity = paritygen(web.stripecount)
878 878 ishead = fctx.filerev() in fctx.filelog().headrevs()
879 879
880 880 # parents() is called once per line and several lines likely belong to
881 881 # same revision. So it is worth caching.
882 882 # TODO there are still redundant operations within basefilectx.parents()
883 883 # and from the fctx.annotate() call itself that could be cached.
884 884 parentscache = {}
885 885 def parents(f):
886 886 rev = f.rev()
887 887 if rev not in parentscache:
888 888 parentscache[rev] = []
889 889 for p in f.parents():
890 890 entry = {
891 891 'node': p.hex(),
892 892 'rev': p.rev(),
893 893 }
894 894 parentscache[rev].append(entry)
895 895
896 896 for p in parentscache[rev]:
897 897 yield p
898 898
899 899 def annotate(**map):
900 900 if fctx.isbinary():
901 901 mt = (mimetypes.guess_type(fctx.path())[0]
902 902 or 'application/octet-stream')
903 903 lines = [((fctx.filectx(fctx.filerev()), 1), '(binary:%s)' % mt)]
904 904 else:
905 905 lines = webutil.annotate(req, fctx, web.repo.ui)
906 906
907 907 previousrev = None
908 908 blockparitygen = paritygen(1)
909 909 for lineno, (aline, l) in enumerate(lines):
910 910 f = aline.fctx
911 911 rev = f.rev()
912 912 if rev != previousrev:
913 913 blockhead = True
914 914 blockparity = next(blockparitygen)
915 915 else:
916 916 blockhead = None
917 917 previousrev = rev
918 918 yield {"parity": next(parity),
919 919 "node": f.hex(),
920 920 "rev": rev,
921 921 "author": f.user(),
922 922 "parents": parents(f),
923 923 "desc": f.description(),
924 924 "extra": f.extra(),
925 925 "file": f.path(),
926 926 "blockhead": blockhead,
927 927 "blockparity": blockparity,
928 928 "targetline": aline.lineno,
929 929 "line": l,
930 930 "lineno": lineno + 1,
931 931 "lineid": "l%d" % (lineno + 1),
932 932 "linenumber": "% 6d" % (lineno + 1),
933 933 "revdate": f.date()}
934 934
935 935 diffopts = webutil.difffeatureopts(req, web.repo.ui, 'annotate')
936 936 diffopts = {k: getattr(diffopts, k) for k in diffopts.defaults}
937 937
938 938 return tmpl("fileannotate",
939 939 file=f,
940 940 annotate=annotate,
941 941 path=webutil.up(f),
942 942 symrev=webutil.symrevorshortnode(req, fctx),
943 943 rename=webutil.renamelink(fctx),
944 944 permissions=fctx.manifest().flags(f),
945 945 ishead=int(ishead),
946 946 diffopts=diffopts,
947 947 **webutil.commonentry(web.repo, fctx))
948 948
949 949 @webcommand('filelog')
950 950 def filelog(web, req, tmpl):
951 951 """
952 952 /filelog/{revision}/{path}
953 953 --------------------------
954 954
955 955 Show information about the history of a file in the repository.
956 956
957 957 The ``revcount`` query string argument can be defined to control the
958 958 maximum number of entries to show.
959 959
960 960 The ``filelog`` template will be rendered.
961 961 """
962 962
963 963 try:
964 964 fctx = webutil.filectx(web.repo, req)
965 965 f = fctx.path()
966 966 fl = fctx.filelog()
967 967 except error.LookupError:
968 968 f = webutil.cleanpath(web.repo, req.form['file'][0])
969 969 fl = web.repo.file(f)
970 970 numrevs = len(fl)
971 971 if not numrevs: # file doesn't exist at all
972 972 raise
973 973 rev = webutil.changectx(web.repo, req).rev()
974 974 first = fl.linkrev(0)
975 975 if rev < first: # current rev is from before file existed
976 976 raise
977 977 frev = numrevs - 1
978 978 while fl.linkrev(frev) > rev:
979 979 frev -= 1
980 980 fctx = web.repo.filectx(f, fl.linkrev(frev))
981 981
982 982 revcount = web.maxshortchanges
983 983 if 'revcount' in req.form:
984 984 try:
985 985 revcount = int(req.form.get('revcount', [revcount])[0])
986 986 revcount = max(revcount, 1)
987 987 tmpl.defaults['sessionvars']['revcount'] = revcount
988 988 except ValueError:
989 989 pass
990 990
991 991 lrange = webutil.linerange(req)
992 992
993 993 lessvars = copy.copy(tmpl.defaults['sessionvars'])
994 994 lessvars['revcount'] = max(revcount / 2, 1)
995 995 morevars = copy.copy(tmpl.defaults['sessionvars'])
996 996 morevars['revcount'] = revcount * 2
997 997
998 998 patch = 'patch' in req.form
999 999 if patch:
1000 1000 lessvars['patch'] = morevars['patch'] = req.form['patch'][0]
1001 1001 descend = 'descend' in req.form
1002 1002 if descend:
1003 1003 lessvars['descend'] = morevars['descend'] = req.form['descend'][0]
1004 1004
1005 1005 count = fctx.filerev() + 1
1006 1006 start = max(0, count - revcount) # first rev on this page
1007 1007 end = min(count, start + revcount) # last rev on this page
1008 1008 parity = paritygen(web.stripecount, offset=start - end)
1009 1009
1010 1010 repo = web.repo
1011 1011 revs = fctx.filelog().revs(start, end - 1)
1012 1012 entries = []
1013 1013
1014 1014 diffstyle = web.config('web', 'style')
1015 1015 if 'style' in req.form:
1016 1016 diffstyle = req.form['style'][0]
1017 1017
1018 1018 def diff(fctx, linerange=None):
1019 1019 ctx = fctx.changectx()
1020 1020 basectx = ctx.p1()
1021 1021 path = fctx.path()
1022 1022 return webutil.diffs(web, tmpl, ctx, basectx, [path], diffstyle,
1023 1023 linerange=linerange,
1024 1024 lineidprefix='%s-' % ctx.hex()[:12])
1025 1025
1026 1026 linerange = None
1027 1027 if lrange is not None:
1028 1028 linerange = webutil.formatlinerange(*lrange)
1029 1029 # deactivate numeric nav links when linerange is specified as this
1030 1030 # would required a dedicated "revnav" class
1031 1031 nav = None
1032 1032 if descend:
1033 1033 it = dagop.blockdescendants(fctx, *lrange)
1034 1034 else:
1035 1035 it = dagop.blockancestors(fctx, *lrange)
1036 1036 for i, (c, lr) in enumerate(it, 1):
1037 1037 diffs = None
1038 1038 if patch:
1039 1039 diffs = diff(c, linerange=lr)
1040 1040 # follow renames accross filtered (not in range) revisions
1041 1041 path = c.path()
1042 1042 entries.append(dict(
1043 1043 parity=next(parity),
1044 1044 filerev=c.rev(),
1045 1045 file=path,
1046 1046 diff=diffs,
1047 1047 linerange=webutil.formatlinerange(*lr),
1048 1048 **webutil.commonentry(repo, c)))
1049 1049 if i == revcount:
1050 1050 break
1051 1051 lessvars['linerange'] = webutil.formatlinerange(*lrange)
1052 1052 morevars['linerange'] = lessvars['linerange']
1053 1053 else:
1054 1054 for i in revs:
1055 1055 iterfctx = fctx.filectx(i)
1056 1056 diffs = None
1057 1057 if patch:
1058 1058 diffs = diff(iterfctx)
1059 1059 entries.append(dict(
1060 1060 parity=next(parity),
1061 1061 filerev=i,
1062 1062 file=f,
1063 1063 diff=diffs,
1064 1064 rename=webutil.renamelink(iterfctx),
1065 1065 **webutil.commonentry(repo, iterfctx)))
1066 1066 entries.reverse()
1067 1067 revnav = webutil.filerevnav(web.repo, fctx.path())
1068 1068 nav = revnav.gen(end - 1, revcount, count)
1069 1069
1070 1070 latestentry = entries[:1]
1071 1071
1072 1072 return tmpl("filelog",
1073 1073 file=f,
1074 1074 nav=nav,
1075 1075 symrev=webutil.symrevorshortnode(req, fctx),
1076 1076 entries=entries,
1077 1077 descend=descend,
1078 1078 patch=patch,
1079 1079 latestentry=latestentry,
1080 1080 linerange=linerange,
1081 1081 revcount=revcount,
1082 1082 morevars=morevars,
1083 1083 lessvars=lessvars,
1084 1084 **webutil.commonentry(web.repo, fctx))
1085 1085
1086 1086 @webcommand('archive')
1087 1087 def archive(web, req, tmpl):
1088 1088 """
1089 1089 /archive/{revision}.{format}[/{path}]
1090 1090 -------------------------------------
1091 1091
1092 1092 Obtain an archive of repository content.
1093 1093
1094 1094 The content and type of the archive is defined by a URL path parameter.
1095 1095 ``format`` is the file extension of the archive type to be generated. e.g.
1096 1096 ``zip`` or ``tar.bz2``. Not all archive types may be allowed by your
1097 1097 server configuration.
1098 1098
1099 1099 The optional ``path`` URL parameter controls content to include in the
1100 1100 archive. If omitted, every file in the specified revision is present in the
1101 1101 archive. If included, only the specified file or contents of the specified
1102 1102 directory will be included in the archive.
1103 1103
1104 1104 No template is used for this handler. Raw, binary content is generated.
1105 1105 """
1106 1106
1107 1107 type_ = req.form.get('type', [None])[0]
1108 1108 allowed = web.configlist("web", "allow_archive")
1109 1109 key = req.form['node'][0]
1110 1110
1111 1111 if type_ not in web.archivespecs:
1112 1112 msg = 'Unsupported archive type: %s' % type_
1113 1113 raise ErrorResponse(HTTP_NOT_FOUND, msg)
1114 1114
1115 1115 if not ((type_ in allowed or
1116 web.configbool("web", "allow" + type_, False))):
1116 web.configbool("web", "allow" + type_))):
1117 1117 msg = 'Archive type not allowed: %s' % type_
1118 1118 raise ErrorResponse(HTTP_FORBIDDEN, msg)
1119 1119
1120 1120 reponame = re.sub(r"\W+", "-", os.path.basename(web.reponame))
1121 1121 cnode = web.repo.lookup(key)
1122 1122 arch_version = key
1123 1123 if cnode == key or key == 'tip':
1124 1124 arch_version = short(cnode)
1125 1125 name = "%s-%s" % (reponame, arch_version)
1126 1126
1127 1127 ctx = webutil.changectx(web.repo, req)
1128 1128 pats = []
1129 1129 match = scmutil.match(ctx, [])
1130 1130 file = req.form.get('file', None)
1131 1131 if file:
1132 1132 pats = ['path:' + file[0]]
1133 1133 match = scmutil.match(ctx, pats, default='path')
1134 1134 if pats:
1135 1135 files = [f for f in ctx.manifest().keys() if match(f)]
1136 1136 if not files:
1137 1137 raise ErrorResponse(HTTP_NOT_FOUND,
1138 1138 'file(s) not found: %s' % file[0])
1139 1139
1140 1140 mimetype, artype, extension, encoding = web.archivespecs[type_]
1141 1141 headers = [
1142 1142 ('Content-Disposition', 'attachment; filename=%s%s' % (name, extension))
1143 1143 ]
1144 1144 if encoding:
1145 1145 headers.append(('Content-Encoding', encoding))
1146 1146 req.headers.extend(headers)
1147 1147 req.respond(HTTP_OK, mimetype)
1148 1148
1149 1149 archival.archive(web.repo, req, cnode, artype, prefix=name,
1150 1150 matchfn=match,
1151 1151 subrepos=web.configbool("web", "archivesubrepos"))
1152 1152 return []
1153 1153
1154 1154
1155 1155 @webcommand('static')
1156 1156 def static(web, req, tmpl):
1157 1157 fname = req.form['file'][0]
1158 1158 # a repo owner may set web.static in .hg/hgrc to get any file
1159 1159 # readable by the user running the CGI script
1160 1160 static = web.config("web", "static", None, untrusted=False)
1161 1161 if not static:
1162 1162 tp = web.templatepath or templater.templatepaths()
1163 1163 if isinstance(tp, str):
1164 1164 tp = [tp]
1165 1165 static = [os.path.join(p, 'static') for p in tp]
1166 1166 staticfile(static, fname, req)
1167 1167 return []
1168 1168
1169 1169 @webcommand('graph')
1170 1170 def graph(web, req, tmpl):
1171 1171 """
1172 1172 /graph[/{revision}]
1173 1173 -------------------
1174 1174
1175 1175 Show information about the graphical topology of the repository.
1176 1176
1177 1177 Information rendered by this handler can be used to create visual
1178 1178 representations of repository topology.
1179 1179
1180 1180 The ``revision`` URL parameter controls the starting changeset.
1181 1181
1182 1182 The ``revcount`` query string argument can define the number of changesets
1183 1183 to show information for.
1184 1184
1185 1185 This handler will render the ``graph`` template.
1186 1186 """
1187 1187
1188 1188 if 'node' in req.form:
1189 1189 ctx = webutil.changectx(web.repo, req)
1190 1190 symrev = webutil.symrevorshortnode(req, ctx)
1191 1191 else:
1192 1192 ctx = web.repo['tip']
1193 1193 symrev = 'tip'
1194 1194 rev = ctx.rev()
1195 1195
1196 1196 bg_height = 39
1197 1197 revcount = web.maxshortchanges
1198 1198 if 'revcount' in req.form:
1199 1199 try:
1200 1200 revcount = int(req.form.get('revcount', [revcount])[0])
1201 1201 revcount = max(revcount, 1)
1202 1202 tmpl.defaults['sessionvars']['revcount'] = revcount
1203 1203 except ValueError:
1204 1204 pass
1205 1205
1206 1206 lessvars = copy.copy(tmpl.defaults['sessionvars'])
1207 1207 lessvars['revcount'] = max(revcount / 2, 1)
1208 1208 morevars = copy.copy(tmpl.defaults['sessionvars'])
1209 1209 morevars['revcount'] = revcount * 2
1210 1210
1211 1211 count = len(web.repo)
1212 1212 pos = rev
1213 1213
1214 1214 uprev = min(max(0, count - 1), rev + revcount)
1215 1215 downrev = max(0, rev - revcount)
1216 1216 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
1217 1217
1218 1218 tree = []
1219 1219 if pos != -1:
1220 1220 allrevs = web.repo.changelog.revs(pos, 0)
1221 1221 revs = []
1222 1222 for i in allrevs:
1223 1223 revs.append(i)
1224 1224 if len(revs) >= revcount:
1225 1225 break
1226 1226
1227 1227 # We have to feed a baseset to dagwalker as it is expecting smartset
1228 1228 # object. This does not have a big impact on hgweb performance itself
1229 1229 # since hgweb graphing code is not itself lazy yet.
1230 1230 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1231 1231 # As we said one line above... not lazy.
1232 1232 tree = list(graphmod.colored(dag, web.repo))
1233 1233
1234 1234 def getcolumns(tree):
1235 1235 cols = 0
1236 1236 for (id, type, ctx, vtx, edges) in tree:
1237 1237 if type != graphmod.CHANGESET:
1238 1238 continue
1239 1239 cols = max(cols, max([edge[0] for edge in edges] or [0]),
1240 1240 max([edge[1] for edge in edges] or [0]))
1241 1241 return cols
1242 1242
1243 1243 def graphdata(usetuples, encodestr):
1244 1244 data = []
1245 1245
1246 1246 row = 0
1247 1247 for (id, type, ctx, vtx, edges) in tree:
1248 1248 if type != graphmod.CHANGESET:
1249 1249 continue
1250 1250 node = str(ctx)
1251 1251 age = encodestr(templatefilters.age(ctx.date()))
1252 1252 desc = templatefilters.firstline(encodestr(ctx.description()))
1253 1253 desc = cgi.escape(templatefilters.nonempty(desc))
1254 1254 user = cgi.escape(templatefilters.person(encodestr(ctx.user())))
1255 1255 branch = cgi.escape(encodestr(ctx.branch()))
1256 1256 try:
1257 1257 branchnode = web.repo.branchtip(branch)
1258 1258 except error.RepoLookupError:
1259 1259 branchnode = None
1260 1260 branch = branch, branchnode == ctx.node()
1261 1261
1262 1262 if usetuples:
1263 1263 data.append((node, vtx, edges, desc, user, age, branch,
1264 1264 [cgi.escape(encodestr(x)) for x in ctx.tags()],
1265 1265 [cgi.escape(encodestr(x))
1266 1266 for x in ctx.bookmarks()]))
1267 1267 else:
1268 1268 edgedata = [{'col': edge[0], 'nextcol': edge[1],
1269 1269 'color': (edge[2] - 1) % 6 + 1,
1270 1270 'width': edge[3], 'bcolor': edge[4]}
1271 1271 for edge in edges]
1272 1272
1273 1273 data.append(
1274 1274 {'node': node,
1275 1275 'col': vtx[0],
1276 1276 'color': (vtx[1] - 1) % 6 + 1,
1277 1277 'edges': edgedata,
1278 1278 'row': row,
1279 1279 'nextrow': row + 1,
1280 1280 'desc': desc,
1281 1281 'user': user,
1282 1282 'age': age,
1283 1283 'bookmarks': webutil.nodebookmarksdict(
1284 1284 web.repo, ctx.node()),
1285 1285 'branches': webutil.nodebranchdict(web.repo, ctx),
1286 1286 'inbranch': webutil.nodeinbranch(web.repo, ctx),
1287 1287 'tags': webutil.nodetagsdict(web.repo, ctx.node())})
1288 1288
1289 1289 row += 1
1290 1290
1291 1291 return data
1292 1292
1293 1293 cols = getcolumns(tree)
1294 1294 rows = len(tree)
1295 1295 canvasheight = (rows + 1) * bg_height - 27
1296 1296
1297 1297 return tmpl('graph', rev=rev, symrev=symrev, revcount=revcount,
1298 1298 uprev=uprev,
1299 1299 lessvars=lessvars, morevars=morevars, downrev=downrev,
1300 1300 cols=cols, rows=rows,
1301 1301 canvaswidth=(cols + 1) * bg_height,
1302 1302 truecanvasheight=rows * bg_height,
1303 1303 canvasheight=canvasheight, bg_height=bg_height,
1304 1304 # {jsdata} will be passed to |json, so it must be in utf-8
1305 1305 jsdata=lambda **x: graphdata(True, encoding.fromlocal),
1306 1306 nodes=lambda **x: graphdata(False, str),
1307 1307 node=ctx.hex(), changenav=changenav)
1308 1308
1309 1309 def _getdoc(e):
1310 1310 doc = e[0].__doc__
1311 1311 if doc:
1312 1312 doc = _(doc).partition('\n')[0]
1313 1313 else:
1314 1314 doc = _('(no help text available)')
1315 1315 return doc
1316 1316
1317 1317 @webcommand('help')
1318 1318 def help(web, req, tmpl):
1319 1319 """
1320 1320 /help[/{topic}]
1321 1321 ---------------
1322 1322
1323 1323 Render help documentation.
1324 1324
1325 1325 This web command is roughly equivalent to :hg:`help`. If a ``topic``
1326 1326 is defined, that help topic will be rendered. If not, an index of
1327 1327 available help topics will be rendered.
1328 1328
1329 1329 The ``help`` template will be rendered when requesting help for a topic.
1330 1330 ``helptopics`` will be rendered for the index of help topics.
1331 1331 """
1332 1332 from .. import commands, help as helpmod # avoid cycle
1333 1333
1334 1334 topicname = req.form.get('node', [None])[0]
1335 1335 if not topicname:
1336 1336 def topics(**map):
1337 1337 for entries, summary, _doc in helpmod.helptable:
1338 1338 yield {'topic': entries[0], 'summary': summary}
1339 1339
1340 1340 early, other = [], []
1341 1341 primary = lambda s: s.partition('|')[0]
1342 1342 for c, e in commands.table.iteritems():
1343 1343 doc = _getdoc(e)
1344 1344 if 'DEPRECATED' in doc or c.startswith('debug'):
1345 1345 continue
1346 1346 cmd = primary(c)
1347 1347 if cmd.startswith('^'):
1348 1348 early.append((cmd[1:], doc))
1349 1349 else:
1350 1350 other.append((cmd, doc))
1351 1351
1352 1352 early.sort()
1353 1353 other.sort()
1354 1354
1355 1355 def earlycommands(**map):
1356 1356 for c, doc in early:
1357 1357 yield {'topic': c, 'summary': doc}
1358 1358
1359 1359 def othercommands(**map):
1360 1360 for c, doc in other:
1361 1361 yield {'topic': c, 'summary': doc}
1362 1362
1363 1363 return tmpl('helptopics', topics=topics, earlycommands=earlycommands,
1364 1364 othercommands=othercommands, title='Index')
1365 1365
1366 1366 # Render an index of sub-topics.
1367 1367 if topicname in helpmod.subtopics:
1368 1368 topics = []
1369 1369 for entries, summary, _doc in helpmod.subtopics[topicname]:
1370 1370 topics.append({
1371 1371 'topic': '%s.%s' % (topicname, entries[0]),
1372 1372 'basename': entries[0],
1373 1373 'summary': summary,
1374 1374 })
1375 1375
1376 1376 return tmpl('helptopics', topics=topics, title=topicname,
1377 1377 subindex=True)
1378 1378
1379 1379 u = webutil.wsgiui.load()
1380 1380 u.verbose = True
1381 1381
1382 1382 # Render a page from a sub-topic.
1383 1383 if '.' in topicname:
1384 1384 # TODO implement support for rendering sections, like
1385 1385 # `hg help` works.
1386 1386 topic, subtopic = topicname.split('.', 1)
1387 1387 if topic not in helpmod.subtopics:
1388 1388 raise ErrorResponse(HTTP_NOT_FOUND)
1389 1389 else:
1390 1390 topic = topicname
1391 1391 subtopic = None
1392 1392
1393 1393 try:
1394 1394 doc = helpmod.help_(u, commands, topic, subtopic=subtopic)
1395 1395 except error.UnknownCommand:
1396 1396 raise ErrorResponse(HTTP_NOT_FOUND)
1397 1397 return tmpl('help', topic=topicname, doc=doc)
1398 1398
1399 1399 # tell hggettext to extract docstrings from these functions:
1400 1400 i18nfunctions = commands.values()
@@ -1,373 +1,403 b''
1 1 #require serve
2 2
3 3 $ hg init test
4 4 $ cd test
5 5 $ echo foo>foo
6 6 $ hg commit -Am 1 -d '1 0'
7 7 adding foo
8 8 $ echo bar>bar
9 9 $ hg commit -Am 2 -d '2 0'
10 10 adding bar
11 11 $ mkdir baz
12 12 $ echo bletch>baz/bletch
13 13 $ hg commit -Am 3 -d '1000000000 0'
14 14 adding baz/bletch
15 15 $ hg init subrepo
16 16 $ touch subrepo/sub
17 17 $ hg -q -R subrepo ci -Am "init subrepo"
18 18 $ echo "subrepo = subrepo" > .hgsub
19 19 $ hg add .hgsub
20 20 $ hg ci -m "add subrepo"
21 21 $ echo "[web]" >> .hg/hgrc
22 22 $ echo "name = test-archive" >> .hg/hgrc
23 23 $ echo "archivesubrepos = True" >> .hg/hgrc
24 24 $ cp .hg/hgrc .hg/hgrc-base
25 25 > test_archtype() {
26 26 > echo "allow_archive = $1" >> .hg/hgrc
27 > hg serve -p $HGPORT -d --pid-file=hg.pid -E errors.log
27 > test_archtype_run "$@"
28 > }
29 > test_archtype_deprecated() {
30 > echo "allow$1 = True" >> .hg/hgrc
31 > test_archtype_run "$@"
32 > }
33 > test_archtype_run() {
34 > hg serve -p $HGPORT -d --pid-file=hg.pid -E errors.log \
35 > --config extensions.blackbox= --config blackbox.track=develwarn
28 36 > cat hg.pid >> $DAEMON_PIDS
29 37 > echo % $1 allowed should give 200
30 38 > get-with-headers.py localhost:$HGPORT "archive/tip.$2" | head -n 1
31 39 > echo % $3 and $4 disallowed should both give 403
32 40 > get-with-headers.py localhost:$HGPORT "archive/tip.$3" | head -n 1
33 41 > get-with-headers.py localhost:$HGPORT "archive/tip.$4" | head -n 1
34 42 > killdaemons.py
35 43 > cat errors.log
44 > hg blackbox --config extensions.blackbox= --config blackbox.track=
36 45 > cp .hg/hgrc-base .hg/hgrc
37 46 > }
38 47
39 48 check http return codes
40 49
41 50 $ test_archtype gz tar.gz tar.bz2 zip
42 51 % gz allowed should give 200
43 52 200 Script output follows
44 53 % tar.bz2 and zip disallowed should both give 403
45 54 403 Archive type not allowed: bz2
46 55 403 Archive type not allowed: zip
47 56 $ test_archtype bz2 tar.bz2 zip tar.gz
48 57 % bz2 allowed should give 200
49 58 200 Script output follows
50 59 % zip and tar.gz disallowed should both give 403
51 60 403 Archive type not allowed: zip
52 61 403 Archive type not allowed: gz
53 62 $ test_archtype zip zip tar.gz tar.bz2
54 63 % zip allowed should give 200
55 64 200 Script output follows
56 65 % tar.gz and tar.bz2 disallowed should both give 403
57 66 403 Archive type not allowed: gz
58 67 403 Archive type not allowed: bz2
59 68
69 check http return codes (with deprecated option)
70
71 $ test_archtype_deprecated gz tar.gz tar.bz2 zip
72 % gz allowed should give 200
73 200 Script output follows
74 % tar.bz2 and zip disallowed should both give 403
75 403 Archive type not allowed: bz2
76 403 Archive type not allowed: zip
77 $ test_archtype_deprecated bz2 tar.bz2 zip tar.gz
78 % bz2 allowed should give 200
79 200 Script output follows
80 % zip and tar.gz disallowed should both give 403
81 403 Archive type not allowed: zip
82 403 Archive type not allowed: gz
83 $ test_archtype_deprecated zip zip tar.gz tar.bz2
84 % zip allowed should give 200
85 200 Script output follows
86 % tar.gz and tar.bz2 disallowed should both give 403
87 403 Archive type not allowed: gz
88 403 Archive type not allowed: bz2
89
60 90 $ echo "allow_archive = gz bz2 zip" >> .hg/hgrc
61 91 $ hg serve -p $HGPORT -d --pid-file=hg.pid -E errors.log
62 92 $ cat hg.pid >> $DAEMON_PIDS
63 93
64 94 check archive links' order
65 95
66 96 $ get-with-headers.py localhost:$HGPORT "?revcount=1" | grep '/archive/tip.'
67 97 <a href="/archive/tip.zip">zip</a>
68 98 <a href="/archive/tip.tar.gz">gz</a>
69 99 <a href="/archive/tip.tar.bz2">bz2</a>
70 100
71 101 invalid arch type should give 404
72 102
73 103 $ get-with-headers.py localhost:$HGPORT "archive/tip.invalid" | head -n 1
74 104 404 Unsupported archive type: None
75 105
76 106 $ TIP=`hg id -v | cut -f1 -d' '`
77 107 $ QTIP=`hg id -q`
78 108 $ cat > getarchive.py <<EOF
79 109 > from __future__ import absolute_import
80 110 > import os
81 111 > import sys
82 112 > from mercurial import (
83 113 > util,
84 114 > )
85 115 > try:
86 116 > # Set stdout to binary mode for win32 platforms
87 117 > import msvcrt
88 118 > msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
89 119 > except ImportError:
90 120 > pass
91 121 > if len(sys.argv) <= 3:
92 122 > node, archive = sys.argv[1:]
93 123 > requeststr = 'cmd=archive;node=%s;type=%s' % (node, archive)
94 124 > else:
95 125 > node, archive, file = sys.argv[1:]
96 126 > requeststr = 'cmd=archive;node=%s;type=%s;file=%s' % (node, archive, file)
97 127 > try:
98 128 > stdout = sys.stdout.buffer
99 129 > except AttributeError:
100 130 > stdout = sys.stdout
101 131 > try:
102 132 > f = util.urlreq.urlopen('http://$LOCALIP:%s/?%s'
103 133 > % (os.environ['HGPORT'], requeststr))
104 134 > stdout.write(f.read())
105 135 > except util.urlerr.httperror as e:
106 136 > sys.stderr.write(str(e) + '\n')
107 137 > EOF
108 138 $ $PYTHON getarchive.py "$TIP" gz | gunzip | tar tf - 2>/dev/null
109 139 test-archive-1701ef1f1510/.hg_archival.txt
110 140 test-archive-1701ef1f1510/.hgsub
111 141 test-archive-1701ef1f1510/.hgsubstate
112 142 test-archive-1701ef1f1510/bar
113 143 test-archive-1701ef1f1510/baz/bletch
114 144 test-archive-1701ef1f1510/foo
115 145 test-archive-1701ef1f1510/subrepo/sub
116 146 $ $PYTHON getarchive.py "$TIP" bz2 | bunzip2 | tar tf - 2>/dev/null
117 147 test-archive-1701ef1f1510/.hg_archival.txt
118 148 test-archive-1701ef1f1510/.hgsub
119 149 test-archive-1701ef1f1510/.hgsubstate
120 150 test-archive-1701ef1f1510/bar
121 151 test-archive-1701ef1f1510/baz/bletch
122 152 test-archive-1701ef1f1510/foo
123 153 test-archive-1701ef1f1510/subrepo/sub
124 154 $ $PYTHON getarchive.py "$TIP" zip > archive.zip
125 155 $ unzip -t archive.zip
126 156 Archive: archive.zip
127 157 testing: test-archive-1701ef1f1510/.hg_archival.txt*OK (glob)
128 158 testing: test-archive-1701ef1f1510/.hgsub*OK (glob)
129 159 testing: test-archive-1701ef1f1510/.hgsubstate*OK (glob)
130 160 testing: test-archive-1701ef1f1510/bar*OK (glob)
131 161 testing: test-archive-1701ef1f1510/baz/bletch*OK (glob)
132 162 testing: test-archive-1701ef1f1510/foo*OK (glob)
133 163 testing: test-archive-1701ef1f1510/subrepo/sub*OK (glob)
134 164 No errors detected in compressed data of archive.zip.
135 165
136 166 test that we can download single directories and files
137 167
138 168 $ $PYTHON getarchive.py "$TIP" gz baz | gunzip | tar tf - 2>/dev/null
139 169 test-archive-1701ef1f1510/baz/bletch
140 170 $ $PYTHON getarchive.py "$TIP" gz foo | gunzip | tar tf - 2>/dev/null
141 171 test-archive-1701ef1f1510/foo
142 172
143 173 test that we detect file patterns that match no files
144 174
145 175 $ $PYTHON getarchive.py "$TIP" gz foobar
146 176 HTTP Error 404: file(s) not found: foobar
147 177
148 178 test that we reject unsafe patterns
149 179
150 180 $ $PYTHON getarchive.py "$TIP" gz relre:baz
151 181 HTTP Error 404: file(s) not found: relre:baz
152 182
153 183 $ killdaemons.py
154 184
155 185 $ hg archive -t tar test.tar
156 186 $ tar tf test.tar
157 187 test/.hg_archival.txt
158 188 test/.hgsub
159 189 test/.hgsubstate
160 190 test/bar
161 191 test/baz/bletch
162 192 test/foo
163 193
164 194 $ hg archive --debug -t tbz2 -X baz test.tar.bz2 --config progress.debug=true
165 195 archiving: 0/4 files (0.00%)
166 196 archiving: .hgsub 1/4 files (25.00%)
167 197 archiving: .hgsubstate 2/4 files (50.00%)
168 198 archiving: bar 3/4 files (75.00%)
169 199 archiving: foo 4/4 files (100.00%)
170 200 $ bunzip2 -dc test.tar.bz2 | tar tf - 2>/dev/null
171 201 test/.hg_archival.txt
172 202 test/.hgsub
173 203 test/.hgsubstate
174 204 test/bar
175 205 test/foo
176 206
177 207 $ hg archive -t tgz -p %b-%h test-%h.tar.gz
178 208 $ gzip -dc test-$QTIP.tar.gz | tar tf - 2>/dev/null
179 209 test-1701ef1f1510/.hg_archival.txt
180 210 test-1701ef1f1510/.hgsub
181 211 test-1701ef1f1510/.hgsubstate
182 212 test-1701ef1f1510/bar
183 213 test-1701ef1f1510/baz/bletch
184 214 test-1701ef1f1510/foo
185 215
186 216 $ hg archive autodetected_test.tar
187 217 $ tar tf autodetected_test.tar
188 218 autodetected_test/.hg_archival.txt
189 219 autodetected_test/.hgsub
190 220 autodetected_test/.hgsubstate
191 221 autodetected_test/bar
192 222 autodetected_test/baz/bletch
193 223 autodetected_test/foo
194 224
195 225 The '-t' should override autodetection
196 226
197 227 $ hg archive -t tar autodetect_override_test.zip
198 228 $ tar tf autodetect_override_test.zip
199 229 autodetect_override_test.zip/.hg_archival.txt
200 230 autodetect_override_test.zip/.hgsub
201 231 autodetect_override_test.zip/.hgsubstate
202 232 autodetect_override_test.zip/bar
203 233 autodetect_override_test.zip/baz/bletch
204 234 autodetect_override_test.zip/foo
205 235
206 236 $ for ext in tar tar.gz tgz tar.bz2 tbz2 zip; do
207 237 > hg archive auto_test.$ext
208 238 > if [ -d auto_test.$ext ]; then
209 239 > echo "extension $ext was not autodetected."
210 240 > fi
211 241 > done
212 242
213 243 $ cat > md5comp.py <<EOF
214 244 > from __future__ import absolute_import, print_function
215 245 > import hashlib
216 246 > import sys
217 247 > f1, f2 = sys.argv[1:3]
218 248 > h1 = hashlib.md5(open(f1, 'rb').read()).hexdigest()
219 249 > h2 = hashlib.md5(open(f2, 'rb').read()).hexdigest()
220 250 > print(h1 == h2 or "md5 differ: " + repr((h1, h2)))
221 251 > EOF
222 252
223 253 archive name is stored in the archive, so create similar archives and
224 254 rename them afterwards.
225 255
226 256 $ hg archive -t tgz tip.tar.gz
227 257 $ mv tip.tar.gz tip1.tar.gz
228 258 $ sleep 1
229 259 $ hg archive -t tgz tip.tar.gz
230 260 $ mv tip.tar.gz tip2.tar.gz
231 261 $ $PYTHON md5comp.py tip1.tar.gz tip2.tar.gz
232 262 True
233 263
234 264 $ hg archive -t zip -p /illegal test.zip
235 265 abort: archive prefix contains illegal components
236 266 [255]
237 267 $ hg archive -t zip -p very/../bad test.zip
238 268
239 269 $ hg archive --config ui.archivemeta=false -t zip -r 2 test.zip
240 270 $ unzip -t test.zip
241 271 Archive: test.zip
242 272 testing: test/bar*OK (glob)
243 273 testing: test/baz/bletch*OK (glob)
244 274 testing: test/foo*OK (glob)
245 275 No errors detected in compressed data of test.zip.
246 276
247 277 $ hg archive -t tar - | tar tf - 2>/dev/null
248 278 test-1701ef1f1510/.hg_archival.txt
249 279 test-1701ef1f1510/.hgsub
250 280 test-1701ef1f1510/.hgsubstate
251 281 test-1701ef1f1510/bar
252 282 test-1701ef1f1510/baz/bletch
253 283 test-1701ef1f1510/foo
254 284
255 285 $ hg archive -r 0 -t tar rev-%r.tar
256 286 $ [ -f rev-0.tar ]
257 287
258 288 test .hg_archival.txt
259 289
260 290 $ hg archive ../test-tags
261 291 $ cat ../test-tags/.hg_archival.txt
262 292 repo: daa7f7c60e0a224faa4ff77ca41b2760562af264
263 293 node: 1701ef1f151069b8747038e93b5186bb43a47504
264 294 branch: default
265 295 latesttag: null
266 296 latesttagdistance: 4
267 297 changessincelatesttag: 4
268 298 $ hg tag -r 2 mytag
269 299 $ hg tag -r 2 anothertag
270 300 $ hg archive -r 2 ../test-lasttag
271 301 $ cat ../test-lasttag/.hg_archival.txt
272 302 repo: daa7f7c60e0a224faa4ff77ca41b2760562af264
273 303 node: 2c0277f05ed49d1c8328fb9ba92fba7a5ebcb33e
274 304 branch: default
275 305 tag: anothertag
276 306 tag: mytag
277 307
278 308 $ hg archive -t bogus test.bogus
279 309 abort: unknown archive type 'bogus'
280 310 [255]
281 311
282 312 enable progress extension:
283 313
284 314 $ cp $HGRCPATH $HGRCPATH.no-progress
285 315 $ cat >> $HGRCPATH <<EOF
286 316 > [progress]
287 317 > assume-tty = 1
288 318 > format = topic bar number
289 319 > delay = 0
290 320 > refresh = 0
291 321 > width = 60
292 322 > EOF
293 323
294 324 $ hg archive ../with-progress
295 325 \r (no-eol) (esc)
296 326 archiving [ ] 0/6\r (no-eol) (esc)
297 327 archiving [======> ] 1/6\r (no-eol) (esc)
298 328 archiving [=============> ] 2/6\r (no-eol) (esc)
299 329 archiving [====================> ] 3/6\r (no-eol) (esc)
300 330 archiving [===========================> ] 4/6\r (no-eol) (esc)
301 331 archiving [==================================> ] 5/6\r (no-eol) (esc)
302 332 archiving [==========================================>] 6/6\r (no-eol) (esc)
303 333 \r (no-eol) (esc)
304 334
305 335 cleanup after progress extension test:
306 336
307 337 $ cp $HGRCPATH.no-progress $HGRCPATH
308 338
309 339 server errors
310 340
311 341 $ cat errors.log
312 342
313 343 empty repo
314 344
315 345 $ hg init ../empty
316 346 $ cd ../empty
317 347 $ hg archive ../test-empty
318 348 abort: no working directory: please specify a revision
319 349 [255]
320 350
321 351 old file -- date clamped to 1980
322 352
323 353 $ touch -t 197501010000 old
324 354 $ hg add old
325 355 $ hg commit -m old
326 356 $ hg archive ../old.zip
327 357 $ unzip -l ../old.zip | grep -v -- ----- | egrep -v files$
328 358 Archive: ../old.zip
329 359 \s*Length.* (re)
330 360 *172*80*00:00*old/.hg_archival.txt (glob)
331 361 *0*80*00:00*old/old (glob)
332 362
333 363 show an error when a provided pattern matches no files
334 364
335 365 $ hg archive -I file_that_does_not_exist.foo ../empty.zip
336 366 abort: no files match the archive pattern
337 367 [255]
338 368
339 369 $ hg archive -X * ../empty.zip
340 370 abort: no files match the archive pattern
341 371 [255]
342 372
343 373 $ cd ..
344 374
345 375 issue3600: check whether "hg archive" can create archive files which
346 376 are extracted with expected timestamp, even though TZ is not
347 377 configured as GMT.
348 378
349 379 $ mkdir issue3600
350 380 $ cd issue3600
351 381
352 382 $ hg init repo
353 383 $ echo a > repo/a
354 384 $ hg -R repo add repo/a
355 385 $ hg -R repo commit -m '#0' -d '456789012 21600'
356 386 $ cat > show_mtime.py <<EOF
357 387 > from __future__ import absolute_import, print_function
358 388 > import os
359 389 > import sys
360 390 > print(int(os.stat(sys.argv[1]).st_mtime))
361 391 > EOF
362 392
363 393 $ hg -R repo archive --prefix tar-extracted archive.tar
364 394 $ (TZ=UTC-3; export TZ; tar xf archive.tar)
365 395 $ $PYTHON show_mtime.py tar-extracted/a
366 396 456789012
367 397
368 398 $ hg -R repo archive --prefix zip-extracted archive.zip
369 399 $ (TZ=UTC-3; export TZ; unzip -q archive.zip)
370 400 $ $PYTHON show_mtime.py zip-extracted/a
371 401 456789012
372 402
373 403 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now