##// END OF EJS Templates
paper: show branch/tags/bookmarks when browsing (issue3559)...
Anton Shestakov -
r25132:917b5a07 default
parent child Browse files
Show More
@@ -1,1323 +1,1324 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 import os, mimetypes, re, cgi, copy
9 9 import webutil
10 10 from mercurial import error, encoding, archival, templater, templatefilters
11 11 from mercurial.node import short, hex
12 12 from mercurial import util
13 13 from common import paritygen, staticfile, get_contact, ErrorResponse
14 14 from common import HTTP_OK, HTTP_FORBIDDEN, HTTP_NOT_FOUND
15 15 from mercurial import graphmod, patch
16 16 from mercurial import scmutil
17 17 from mercurial.i18n import _
18 18 from mercurial.error import ParseError, RepoLookupError, Abort
19 19 from mercurial import revset
20 20
21 21 __all__ = []
22 22 commands = {}
23 23
24 24 class webcommand(object):
25 25 """Decorator used to register a web command handler.
26 26
27 27 The decorator takes as its positional arguments the name/path the
28 28 command should be accessible under.
29 29
30 30 Usage:
31 31
32 32 @webcommand('mycommand')
33 33 def mycommand(web, req, tmpl):
34 34 pass
35 35 """
36 36
37 37 def __init__(self, name):
38 38 self.name = name
39 39
40 40 def __call__(self, func):
41 41 __all__.append(self.name)
42 42 commands[self.name] = func
43 43 return func
44 44
45 45 @webcommand('log')
46 46 def log(web, req, tmpl):
47 47 """
48 48 /log[/{revision}[/{path}]]
49 49 --------------------------
50 50
51 51 Show repository or file history.
52 52
53 53 For URLs of the form ``/log/{revision}``, a list of changesets starting at
54 54 the specified changeset identifier is shown. If ``{revision}`` is not
55 55 defined, the default is ``tip``. This form is equivalent to the
56 56 ``changelog`` handler.
57 57
58 58 For URLs of the form ``/log/{revision}/{file}``, the history for a specific
59 59 file will be shown. This form is equivalent to the ``filelog`` handler.
60 60 """
61 61
62 62 if 'file' in req.form and req.form['file'][0]:
63 63 return filelog(web, req, tmpl)
64 64 else:
65 65 return changelog(web, req, tmpl)
66 66
67 67 @webcommand('rawfile')
68 68 def rawfile(web, req, tmpl):
69 69 guessmime = web.configbool('web', 'guessmime', False)
70 70
71 71 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
72 72 if not path:
73 73 content = manifest(web, req, tmpl)
74 74 req.respond(HTTP_OK, web.ctype)
75 75 return content
76 76
77 77 try:
78 78 fctx = webutil.filectx(web.repo, req)
79 79 except error.LookupError, inst:
80 80 try:
81 81 content = manifest(web, req, tmpl)
82 82 req.respond(HTTP_OK, web.ctype)
83 83 return content
84 84 except ErrorResponse:
85 85 raise inst
86 86
87 87 path = fctx.path()
88 88 text = fctx.data()
89 89 mt = 'application/binary'
90 90 if guessmime:
91 91 mt = mimetypes.guess_type(path)[0]
92 92 if mt is None:
93 93 if util.binary(text):
94 94 mt = 'application/binary'
95 95 else:
96 96 mt = 'text/plain'
97 97 if mt.startswith('text/'):
98 98 mt += '; charset="%s"' % encoding.encoding
99 99
100 100 req.respond(HTTP_OK, mt, path, body=text)
101 101 return []
102 102
103 103 def _filerevision(web, tmpl, fctx):
104 104 f = fctx.path()
105 105 text = fctx.data()
106 106 parity = paritygen(web.stripecount)
107 107
108 108 if util.binary(text):
109 109 mt = mimetypes.guess_type(f)[0] or 'application/octet-stream'
110 110 text = '(binary:%s)' % mt
111 111
112 112 def lines():
113 113 for lineno, t in enumerate(text.splitlines(True)):
114 114 yield {"line": t,
115 115 "lineid": "l%d" % (lineno + 1),
116 116 "linenumber": "% 6d" % (lineno + 1),
117 117 "parity": parity.next()}
118 118
119 119 return tmpl("filerevision",
120 120 file=f,
121 121 path=webutil.up(f),
122 122 text=lines(),
123 123 rev=fctx.rev(),
124 124 node=fctx.hex(),
125 125 author=fctx.user(),
126 126 date=fctx.date(),
127 127 desc=fctx.description(),
128 128 extra=fctx.extra(),
129 129 branch=webutil.nodebranchnodefault(fctx),
130 130 parent=webutil.parents(fctx),
131 131 child=webutil.children(fctx),
132 132 rename=webutil.renamelink(fctx),
133 133 permissions=fctx.manifest().flags(f))
134 134
135 135 @webcommand('file')
136 136 def file(web, req, tmpl):
137 137 """
138 138 /file/{revision}[/{path}]
139 139 -------------------------
140 140
141 141 Show information about a directory or file in the repository.
142 142
143 143 Info about the ``path`` given as a URL parameter will be rendered.
144 144
145 145 If ``path`` is a directory, information about the entries in that
146 146 directory will be rendered. This form is equivalent to the ``manifest``
147 147 handler.
148 148
149 149 If ``path`` is a file, information about that file will be shown via
150 150 the ``filerevision`` template.
151 151
152 152 If ``path`` is not defined, information about the root directory will
153 153 be rendered.
154 154 """
155 155 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
156 156 if not path:
157 157 return manifest(web, req, tmpl)
158 158 try:
159 159 return _filerevision(web, tmpl, webutil.filectx(web.repo, req))
160 160 except error.LookupError, inst:
161 161 try:
162 162 return manifest(web, req, tmpl)
163 163 except ErrorResponse:
164 164 raise inst
165 165
166 166 def _search(web, req, tmpl):
167 167 MODE_REVISION = 'rev'
168 168 MODE_KEYWORD = 'keyword'
169 169 MODE_REVSET = 'revset'
170 170
171 171 def revsearch(ctx):
172 172 yield ctx
173 173
174 174 def keywordsearch(query):
175 175 lower = encoding.lower
176 176 qw = lower(query).split()
177 177
178 178 def revgen():
179 179 cl = web.repo.changelog
180 180 for i in xrange(len(web.repo) - 1, 0, -100):
181 181 l = []
182 182 for j in cl.revs(max(0, i - 99), i):
183 183 ctx = web.repo[j]
184 184 l.append(ctx)
185 185 l.reverse()
186 186 for e in l:
187 187 yield e
188 188
189 189 for ctx in revgen():
190 190 miss = 0
191 191 for q in qw:
192 192 if not (q in lower(ctx.user()) or
193 193 q in lower(ctx.description()) or
194 194 q in lower(" ".join(ctx.files()))):
195 195 miss = 1
196 196 break
197 197 if miss:
198 198 continue
199 199
200 200 yield ctx
201 201
202 202 def revsetsearch(revs):
203 203 for r in revs:
204 204 yield web.repo[r]
205 205
206 206 searchfuncs = {
207 207 MODE_REVISION: (revsearch, 'exact revision search'),
208 208 MODE_KEYWORD: (keywordsearch, 'literal keyword search'),
209 209 MODE_REVSET: (revsetsearch, 'revset expression search'),
210 210 }
211 211
212 212 def getsearchmode(query):
213 213 try:
214 214 ctx = web.repo[query]
215 215 except (error.RepoError, error.LookupError):
216 216 # query is not an exact revision pointer, need to
217 217 # decide if it's a revset expression or keywords
218 218 pass
219 219 else:
220 220 return MODE_REVISION, ctx
221 221
222 222 revdef = 'reverse(%s)' % query
223 223 try:
224 224 tree, pos = revset.parse(revdef)
225 225 except ParseError:
226 226 # can't parse to a revset tree
227 227 return MODE_KEYWORD, query
228 228
229 229 if revset.depth(tree) <= 2:
230 230 # no revset syntax used
231 231 return MODE_KEYWORD, query
232 232
233 233 if util.any((token, (value or '')[:3]) == ('string', 're:')
234 234 for token, value, pos in revset.tokenize(revdef)):
235 235 return MODE_KEYWORD, query
236 236
237 237 funcsused = revset.funcsused(tree)
238 238 if not funcsused.issubset(revset.safesymbols):
239 239 return MODE_KEYWORD, query
240 240
241 241 mfunc = revset.match(web.repo.ui, revdef)
242 242 try:
243 243 revs = mfunc(web.repo)
244 244 return MODE_REVSET, revs
245 245 # ParseError: wrongly placed tokens, wrongs arguments, etc
246 246 # RepoLookupError: no such revision, e.g. in 'revision:'
247 247 # Abort: bookmark/tag not exists
248 248 # LookupError: ambiguous identifier, e.g. in '(bc)' on a large repo
249 249 except (ParseError, RepoLookupError, Abort, LookupError):
250 250 return MODE_KEYWORD, query
251 251
252 252 def changelist(**map):
253 253 count = 0
254 254
255 255 for ctx in searchfunc[0](funcarg):
256 256 count += 1
257 257 n = ctx.node()
258 258 showtags = webutil.showtag(web.repo, tmpl, 'changelogtag', n)
259 259 files = webutil.listfilediffs(tmpl, ctx.files(), n, web.maxfiles)
260 260
261 261 yield tmpl('searchentry',
262 262 parity=parity.next(),
263 263 author=ctx.user(),
264 264 parent=webutil.parents(ctx),
265 265 child=webutil.children(ctx),
266 266 changelogtag=showtags,
267 267 desc=ctx.description(),
268 268 extra=ctx.extra(),
269 269 date=ctx.date(),
270 270 files=files,
271 271 rev=ctx.rev(),
272 272 node=hex(n),
273 273 tags=webutil.nodetagsdict(web.repo, n),
274 274 bookmarks=webutil.nodebookmarksdict(web.repo, n),
275 275 inbranch=webutil.nodeinbranch(web.repo, ctx),
276 276 branches=webutil.nodebranchdict(web.repo, ctx))
277 277
278 278 if count >= revcount:
279 279 break
280 280
281 281 query = req.form['rev'][0]
282 282 revcount = web.maxchanges
283 283 if 'revcount' in req.form:
284 284 try:
285 285 revcount = int(req.form.get('revcount', [revcount])[0])
286 286 revcount = max(revcount, 1)
287 287 tmpl.defaults['sessionvars']['revcount'] = revcount
288 288 except ValueError:
289 289 pass
290 290
291 291 lessvars = copy.copy(tmpl.defaults['sessionvars'])
292 292 lessvars['revcount'] = max(revcount / 2, 1)
293 293 lessvars['rev'] = query
294 294 morevars = copy.copy(tmpl.defaults['sessionvars'])
295 295 morevars['revcount'] = revcount * 2
296 296 morevars['rev'] = query
297 297
298 298 mode, funcarg = getsearchmode(query)
299 299
300 300 if 'forcekw' in req.form:
301 301 showforcekw = ''
302 302 showunforcekw = searchfuncs[mode][1]
303 303 mode = MODE_KEYWORD
304 304 funcarg = query
305 305 else:
306 306 if mode != MODE_KEYWORD:
307 307 showforcekw = searchfuncs[MODE_KEYWORD][1]
308 308 else:
309 309 showforcekw = ''
310 310 showunforcekw = ''
311 311
312 312 searchfunc = searchfuncs[mode]
313 313
314 314 tip = web.repo['tip']
315 315 parity = paritygen(web.stripecount)
316 316
317 317 return tmpl('search', query=query, node=tip.hex(),
318 318 entries=changelist, archives=web.archivelist("tip"),
319 319 morevars=morevars, lessvars=lessvars,
320 320 modedesc=searchfunc[1],
321 321 showforcekw=showforcekw, showunforcekw=showunforcekw)
322 322
323 323 @webcommand('changelog')
324 324 def changelog(web, req, tmpl, shortlog=False):
325 325 """
326 326 /changelog[/{revision}]
327 327 -----------------------
328 328
329 329 Show information about multiple changesets.
330 330
331 331 If the optional ``revision`` URL argument is absent, information about
332 332 all changesets starting at ``tip`` will be rendered. If the ``revision``
333 333 argument is present, changesets will be shown starting from the specified
334 334 revision.
335 335
336 336 If ``revision`` is absent, the ``rev`` query string argument may be
337 337 defined. This will perform a search for changesets.
338 338
339 339 The argument for ``rev`` can be a single revision, a revision set,
340 340 or a literal keyword to search for in changeset data (equivalent to
341 341 :hg:`log -k`).
342 342
343 343 The ``revcount`` query string argument defines the maximum numbers of
344 344 changesets to render.
345 345
346 346 For non-searches, the ``changelog`` template will be rendered.
347 347 """
348 348
349 349 query = ''
350 350 if 'node' in req.form:
351 351 ctx = webutil.changectx(web.repo, req)
352 352 elif 'rev' in req.form:
353 353 return _search(web, req, tmpl)
354 354 else:
355 355 ctx = web.repo['tip']
356 356
357 357 def changelist():
358 358 revs = []
359 359 if pos != -1:
360 360 revs = web.repo.changelog.revs(pos, 0)
361 361 curcount = 0
362 362 for rev in revs:
363 363 curcount += 1
364 364 if curcount > revcount + 1:
365 365 break
366 366
367 367 entry = webutil.changelistentry(web, web.repo[rev], tmpl)
368 368 entry['parity'] = parity.next()
369 369 yield entry
370 370
371 371 if shortlog:
372 372 revcount = web.maxshortchanges
373 373 else:
374 374 revcount = web.maxchanges
375 375
376 376 if 'revcount' in req.form:
377 377 try:
378 378 revcount = int(req.form.get('revcount', [revcount])[0])
379 379 revcount = max(revcount, 1)
380 380 tmpl.defaults['sessionvars']['revcount'] = revcount
381 381 except ValueError:
382 382 pass
383 383
384 384 lessvars = copy.copy(tmpl.defaults['sessionvars'])
385 385 lessvars['revcount'] = max(revcount / 2, 1)
386 386 morevars = copy.copy(tmpl.defaults['sessionvars'])
387 387 morevars['revcount'] = revcount * 2
388 388
389 389 count = len(web.repo)
390 390 pos = ctx.rev()
391 391 parity = paritygen(web.stripecount)
392 392
393 393 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
394 394
395 395 entries = list(changelist())
396 396 latestentry = entries[:1]
397 397 if len(entries) > revcount:
398 398 nextentry = entries[-1:]
399 399 entries = entries[:-1]
400 400 else:
401 401 nextentry = []
402 402
403 403 return tmpl(shortlog and 'shortlog' or 'changelog', changenav=changenav,
404 404 node=ctx.hex(), rev=pos, changesets=count,
405 405 entries=entries,
406 406 latestentry=latestentry, nextentry=nextentry,
407 407 archives=web.archivelist("tip"), revcount=revcount,
408 408 morevars=morevars, lessvars=lessvars, query=query)
409 409
410 410 @webcommand('shortlog')
411 411 def shortlog(web, req, tmpl):
412 412 """
413 413 /shortlog
414 414 ---------
415 415
416 416 Show basic information about a set of changesets.
417 417
418 418 This accepts the same parameters as the ``changelog`` handler. The only
419 419 difference is the ``shortlog`` template will be rendered instead of the
420 420 ``changelog`` template.
421 421 """
422 422 return changelog(web, req, tmpl, shortlog=True)
423 423
424 424 @webcommand('changeset')
425 425 def changeset(web, req, tmpl):
426 426 """
427 427 /changeset[/{revision}]
428 428 -----------------------
429 429
430 430 Show information about a single changeset.
431 431
432 432 A URL path argument is the changeset identifier to show. See ``hg help
433 433 revisions`` for possible values. If not defined, the ``tip`` changeset
434 434 will be shown.
435 435
436 436 The ``changeset`` template is rendered. Contents of the ``changesettag``,
437 437 ``changesetbookmark``, ``filenodelink``, ``filenolink``, and the many
438 438 templates related to diffs may all be used to produce the output.
439 439 """
440 440 ctx = webutil.changectx(web.repo, req)
441 441
442 442 return tmpl('changeset', **webutil.changesetentry(web, req, tmpl, ctx))
443 443
444 444 rev = webcommand('rev')(changeset)
445 445
446 446 def decodepath(path):
447 447 """Hook for mapping a path in the repository to a path in the
448 448 working copy.
449 449
450 450 Extensions (e.g., largefiles) can override this to remap files in
451 451 the virtual file system presented by the manifest command below."""
452 452 return path
453 453
454 454 @webcommand('manifest')
455 455 def manifest(web, req, tmpl):
456 456 """
457 457 /manifest[/{revision}[/{path}]]
458 458 -------------------------------
459 459
460 460 Show information about a directory.
461 461
462 462 If the URL path arguments are omitted, information about the root
463 463 directory for the ``tip`` changeset will be shown.
464 464
465 465 Because this handler can only show information for directories, it
466 466 is recommended to use the ``file`` handler instead, as it can handle both
467 467 directories and files.
468 468
469 469 The ``manifest`` template will be rendered for this handler.
470 470 """
471 471 ctx = webutil.changectx(web.repo, req)
472 472 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
473 473 mf = ctx.manifest()
474 474 node = ctx.node()
475 475
476 476 files = {}
477 477 dirs = {}
478 478 parity = paritygen(web.stripecount)
479 479
480 480 if path and path[-1] != "/":
481 481 path += "/"
482 482 l = len(path)
483 483 abspath = "/" + path
484 484
485 485 for full, n in mf.iteritems():
486 486 # the virtual path (working copy path) used for the full
487 487 # (repository) path
488 488 f = decodepath(full)
489 489
490 490 if f[:l] != path:
491 491 continue
492 492 remain = f[l:]
493 493 elements = remain.split('/')
494 494 if len(elements) == 1:
495 495 files[remain] = full
496 496 else:
497 497 h = dirs # need to retain ref to dirs (root)
498 498 for elem in elements[0:-1]:
499 499 if elem not in h:
500 500 h[elem] = {}
501 501 h = h[elem]
502 502 if len(h) > 1:
503 503 break
504 504 h[None] = None # denotes files present
505 505
506 506 if mf and not files and not dirs:
507 507 raise ErrorResponse(HTTP_NOT_FOUND, 'path not found: ' + path)
508 508
509 509 def filelist(**map):
510 510 for f in sorted(files):
511 511 full = files[f]
512 512
513 513 fctx = ctx.filectx(full)
514 514 yield {"file": full,
515 515 "parity": parity.next(),
516 516 "basename": f,
517 517 "date": fctx.date(),
518 518 "size": fctx.size(),
519 519 "permissions": mf.flags(full)}
520 520
521 521 def dirlist(**map):
522 522 for d in sorted(dirs):
523 523
524 524 emptydirs = []
525 525 h = dirs[d]
526 526 while isinstance(h, dict) and len(h) == 1:
527 527 k, v = h.items()[0]
528 528 if v:
529 529 emptydirs.append(k)
530 530 h = v
531 531
532 532 path = "%s%s" % (abspath, d)
533 533 yield {"parity": parity.next(),
534 534 "path": path,
535 535 "emptydirs": "/".join(emptydirs),
536 536 "basename": d}
537 537
538 538 return tmpl("manifest",
539 539 rev=ctx.rev(),
540 540 node=hex(node),
541 541 path=abspath,
542 542 up=webutil.up(abspath),
543 543 upparity=parity.next(),
544 544 fentries=filelist,
545 545 dentries=dirlist,
546 546 archives=web.archivelist(hex(node)),
547 547 tags=webutil.nodetagsdict(web.repo, node),
548 548 bookmarks=webutil.nodebookmarksdict(web.repo, node),
549 branch=webutil.nodebranchnodefault(ctx),
549 550 inbranch=webutil.nodeinbranch(web.repo, ctx),
550 551 branches=webutil.nodebranchdict(web.repo, ctx))
551 552
552 553 @webcommand('tags')
553 554 def tags(web, req, tmpl):
554 555 """
555 556 /tags
556 557 -----
557 558
558 559 Show information about tags.
559 560
560 561 No arguments are accepted.
561 562
562 563 The ``tags`` template is rendered.
563 564 """
564 565 i = list(reversed(web.repo.tagslist()))
565 566 parity = paritygen(web.stripecount)
566 567
567 568 def entries(notip, latestonly, **map):
568 569 t = i
569 570 if notip:
570 571 t = [(k, n) for k, n in i if k != "tip"]
571 572 if latestonly:
572 573 t = t[:1]
573 574 for k, n in t:
574 575 yield {"parity": parity.next(),
575 576 "tag": k,
576 577 "date": web.repo[n].date(),
577 578 "node": hex(n)}
578 579
579 580 return tmpl("tags",
580 581 node=hex(web.repo.changelog.tip()),
581 582 entries=lambda **x: entries(False, False, **x),
582 583 entriesnotip=lambda **x: entries(True, False, **x),
583 584 latestentry=lambda **x: entries(True, True, **x))
584 585
585 586 @webcommand('bookmarks')
586 587 def bookmarks(web, req, tmpl):
587 588 """
588 589 /bookmarks
589 590 ----------
590 591
591 592 Show information about bookmarks.
592 593
593 594 No arguments are accepted.
594 595
595 596 The ``bookmarks`` template is rendered.
596 597 """
597 598 i = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
598 599 parity = paritygen(web.stripecount)
599 600
600 601 def entries(latestonly, **map):
601 602 if latestonly:
602 603 t = [min(i)]
603 604 else:
604 605 t = sorted(i)
605 606 for k, n in t:
606 607 yield {"parity": parity.next(),
607 608 "bookmark": k,
608 609 "date": web.repo[n].date(),
609 610 "node": hex(n)}
610 611
611 612 return tmpl("bookmarks",
612 613 node=hex(web.repo.changelog.tip()),
613 614 entries=lambda **x: entries(latestonly=False, **x),
614 615 latestentry=lambda **x: entries(latestonly=True, **x))
615 616
616 617 @webcommand('branches')
617 618 def branches(web, req, tmpl):
618 619 """
619 620 /branches
620 621 ---------
621 622
622 623 Show information about branches.
623 624
624 625 All known branches are contained in the output, even closed branches.
625 626
626 627 No arguments are accepted.
627 628
628 629 The ``branches`` template is rendered.
629 630 """
630 631 tips = []
631 632 heads = web.repo.heads()
632 633 parity = paritygen(web.stripecount)
633 634 sortkey = lambda item: (not item[1], item[0].rev())
634 635
635 636 def entries(limit, **map):
636 637 count = 0
637 638 if not tips:
638 639 for tag, hs, tip, closed in web.repo.branchmap().iterbranches():
639 640 tips.append((web.repo[tip], closed))
640 641 for ctx, closed in sorted(tips, key=sortkey, reverse=True):
641 642 if limit > 0 and count >= limit:
642 643 return
643 644 count += 1
644 645 if closed:
645 646 status = 'closed'
646 647 elif ctx.node() not in heads:
647 648 status = 'inactive'
648 649 else:
649 650 status = 'open'
650 651 yield {'parity': parity.next(),
651 652 'branch': ctx.branch(),
652 653 'status': status,
653 654 'node': ctx.hex(),
654 655 'date': ctx.date()}
655 656
656 657 return tmpl('branches', node=hex(web.repo.changelog.tip()),
657 658 entries=lambda **x: entries(0, **x),
658 659 latestentry=lambda **x: entries(1, **x))
659 660
660 661 @webcommand('summary')
661 662 def summary(web, req, tmpl):
662 663 """
663 664 /summary
664 665 --------
665 666
666 667 Show a summary of repository state.
667 668
668 669 Information about the latest changesets, bookmarks, tags, and branches
669 670 is captured by this handler.
670 671
671 672 The ``summary`` template is rendered.
672 673 """
673 674 i = reversed(web.repo.tagslist())
674 675
675 676 def tagentries(**map):
676 677 parity = paritygen(web.stripecount)
677 678 count = 0
678 679 for k, n in i:
679 680 if k == "tip": # skip tip
680 681 continue
681 682
682 683 count += 1
683 684 if count > 10: # limit to 10 tags
684 685 break
685 686
686 687 yield tmpl("tagentry",
687 688 parity=parity.next(),
688 689 tag=k,
689 690 node=hex(n),
690 691 date=web.repo[n].date())
691 692
692 693 def bookmarks(**map):
693 694 parity = paritygen(web.stripecount)
694 695 marks = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
695 696 for k, n in sorted(marks)[:10]: # limit to 10 bookmarks
696 697 yield {'parity': parity.next(),
697 698 'bookmark': k,
698 699 'date': web.repo[n].date(),
699 700 'node': hex(n)}
700 701
701 702 def branches(**map):
702 703 parity = paritygen(web.stripecount)
703 704
704 705 b = web.repo.branchmap()
705 706 l = [(-web.repo.changelog.rev(tip), tip, tag)
706 707 for tag, heads, tip, closed in b.iterbranches()]
707 708 for r, n, t in sorted(l):
708 709 yield {'parity': parity.next(),
709 710 'branch': t,
710 711 'node': hex(n),
711 712 'date': web.repo[n].date()}
712 713
713 714 def changelist(**map):
714 715 parity = paritygen(web.stripecount, offset=start - end)
715 716 l = [] # build a list in forward order for efficiency
716 717 revs = []
717 718 if start < end:
718 719 revs = web.repo.changelog.revs(start, end - 1)
719 720 for i in revs:
720 721 ctx = web.repo[i]
721 722 n = ctx.node()
722 723 hn = hex(n)
723 724
724 725 l.append(tmpl(
725 726 'shortlogentry',
726 727 parity=parity.next(),
727 728 author=ctx.user(),
728 729 desc=ctx.description(),
729 730 extra=ctx.extra(),
730 731 date=ctx.date(),
731 732 rev=i,
732 733 node=hn,
733 734 tags=webutil.nodetagsdict(web.repo, n),
734 735 bookmarks=webutil.nodebookmarksdict(web.repo, n),
735 736 inbranch=webutil.nodeinbranch(web.repo, ctx),
736 737 branches=webutil.nodebranchdict(web.repo, ctx)))
737 738
738 739 l.reverse()
739 740 yield l
740 741
741 742 tip = web.repo['tip']
742 743 count = len(web.repo)
743 744 start = max(0, count - web.maxchanges)
744 745 end = min(count, start + web.maxchanges)
745 746
746 747 return tmpl("summary",
747 748 desc=web.config("web", "description", "unknown"),
748 749 owner=get_contact(web.config) or "unknown",
749 750 lastchange=tip.date(),
750 751 tags=tagentries,
751 752 bookmarks=bookmarks,
752 753 branches=branches,
753 754 shortlog=changelist,
754 755 node=tip.hex(),
755 756 archives=web.archivelist("tip"))
756 757
757 758 @webcommand('filediff')
758 759 def filediff(web, req, tmpl):
759 760 """
760 761 /diff/{revision}/{path}
761 762 -----------------------
762 763
763 764 Show how a file changed in a particular commit.
764 765
765 766 The ``filediff`` template is rendered.
766 767
767 768 This hander is registered under both the ``/diff`` and ``/filediff``
768 769 paths. ``/diff`` is used in modern code.
769 770 """
770 771 fctx, ctx = None, None
771 772 try:
772 773 fctx = webutil.filectx(web.repo, req)
773 774 except LookupError:
774 775 ctx = webutil.changectx(web.repo, req)
775 776 path = webutil.cleanpath(web.repo, req.form['file'][0])
776 777 if path not in ctx.files():
777 778 raise
778 779
779 780 if fctx is not None:
780 781 n = fctx.node()
781 782 path = fctx.path()
782 783 ctx = fctx.changectx()
783 784 else:
784 785 n = ctx.node()
785 786 # path already defined in except clause
786 787
787 788 parity = paritygen(web.stripecount)
788 789 style = web.config('web', 'style', 'paper')
789 790 if 'style' in req.form:
790 791 style = req.form['style'][0]
791 792
792 793 diffs = webutil.diffs(web.repo, tmpl, ctx, None, [path], parity, style)
793 794 if fctx:
794 795 rename = webutil.renamelink(fctx)
795 796 ctx = fctx
796 797 else:
797 798 rename = []
798 799 ctx = ctx
799 800 return tmpl("filediff",
800 801 file=path,
801 802 node=hex(n),
802 803 rev=ctx.rev(),
803 804 date=ctx.date(),
804 805 desc=ctx.description(),
805 806 extra=ctx.extra(),
806 807 author=ctx.user(),
807 808 rename=rename,
808 809 branch=webutil.nodebranchnodefault(ctx),
809 810 parent=webutil.parents(ctx),
810 811 child=webutil.children(ctx),
811 812 diff=diffs)
812 813
813 814 diff = webcommand('diff')(filediff)
814 815
815 816 @webcommand('comparison')
816 817 def comparison(web, req, tmpl):
817 818 """
818 819 /comparison/{revision}/{path}
819 820 -----------------------------
820 821
821 822 Show a comparison between the old and new versions of a file from changes
822 823 made on a particular revision.
823 824
824 825 This is similar to the ``diff`` handler. However, this form features
825 826 a split or side-by-side diff rather than a unified diff.
826 827
827 828 The ``context`` query string argument can be used to control the lines of
828 829 context in the diff.
829 830
830 831 The ``filecomparison`` template is rendered.
831 832 """
832 833 ctx = webutil.changectx(web.repo, req)
833 834 if 'file' not in req.form:
834 835 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
835 836 path = webutil.cleanpath(web.repo, req.form['file'][0])
836 837 rename = path in ctx and webutil.renamelink(ctx[path]) or []
837 838
838 839 parsecontext = lambda v: v == 'full' and -1 or int(v)
839 840 if 'context' in req.form:
840 841 context = parsecontext(req.form['context'][0])
841 842 else:
842 843 context = parsecontext(web.config('web', 'comparisoncontext', '5'))
843 844
844 845 def filelines(f):
845 846 if util.binary(f.data()):
846 847 mt = mimetypes.guess_type(f.path())[0]
847 848 if not mt:
848 849 mt = 'application/octet-stream'
849 850 return [_('(binary file %s, hash: %s)') % (mt, hex(f.filenode()))]
850 851 return f.data().splitlines()
851 852
852 853 parent = ctx.p1()
853 854 leftrev = parent.rev()
854 855 leftnode = parent.node()
855 856 rightrev = ctx.rev()
856 857 rightnode = ctx.node()
857 858 if path in ctx:
858 859 fctx = ctx[path]
859 860 rightlines = filelines(fctx)
860 861 if path not in parent:
861 862 leftlines = ()
862 863 else:
863 864 pfctx = parent[path]
864 865 leftlines = filelines(pfctx)
865 866 else:
866 867 rightlines = ()
867 868 fctx = ctx.parents()[0][path]
868 869 leftlines = filelines(fctx)
869 870
870 871 comparison = webutil.compare(tmpl, context, leftlines, rightlines)
871 872 return tmpl('filecomparison',
872 873 file=path,
873 874 node=hex(ctx.node()),
874 875 rev=ctx.rev(),
875 876 date=ctx.date(),
876 877 desc=ctx.description(),
877 878 extra=ctx.extra(),
878 879 author=ctx.user(),
879 880 rename=rename,
880 881 branch=webutil.nodebranchnodefault(ctx),
881 882 parent=webutil.parents(fctx),
882 883 child=webutil.children(fctx),
883 884 leftrev=leftrev,
884 885 leftnode=hex(leftnode),
885 886 rightrev=rightrev,
886 887 rightnode=hex(rightnode),
887 888 comparison=comparison)
888 889
889 890 @webcommand('annotate')
890 891 def annotate(web, req, tmpl):
891 892 """
892 893 /annotate/{revision}/{path}
893 894 ---------------------------
894 895
895 896 Show changeset information for each line in a file.
896 897
897 898 The ``fileannotate`` template is rendered.
898 899 """
899 900 fctx = webutil.filectx(web.repo, req)
900 901 f = fctx.path()
901 902 parity = paritygen(web.stripecount)
902 903 diffopts = patch.difffeatureopts(web.repo.ui, untrusted=True,
903 904 section='annotate', whitespace=True)
904 905
905 906 def annotate(**map):
906 907 last = None
907 908 if util.binary(fctx.data()):
908 909 mt = (mimetypes.guess_type(fctx.path())[0]
909 910 or 'application/octet-stream')
910 911 lines = enumerate([((fctx.filectx(fctx.filerev()), 1),
911 912 '(binary:%s)' % mt)])
912 913 else:
913 914 lines = enumerate(fctx.annotate(follow=True, linenumber=True,
914 915 diffopts=diffopts))
915 916 for lineno, ((f, targetline), l) in lines:
916 917 fnode = f.filenode()
917 918
918 919 if last != fnode:
919 920 last = fnode
920 921
921 922 yield {"parity": parity.next(),
922 923 "node": f.hex(),
923 924 "rev": f.rev(),
924 925 "author": f.user(),
925 926 "desc": f.description(),
926 927 "extra": f.extra(),
927 928 "file": f.path(),
928 929 "targetline": targetline,
929 930 "line": l,
930 931 "lineno": lineno + 1,
931 932 "lineid": "l%d" % (lineno + 1),
932 933 "linenumber": "% 6d" % (lineno + 1),
933 934 "revdate": f.date()}
934 935
935 936 return tmpl("fileannotate",
936 937 file=f,
937 938 annotate=annotate,
938 939 path=webutil.up(f),
939 940 rev=fctx.rev(),
940 941 node=fctx.hex(),
941 942 author=fctx.user(),
942 943 date=fctx.date(),
943 944 desc=fctx.description(),
944 945 extra=fctx.extra(),
945 946 rename=webutil.renamelink(fctx),
946 947 branch=webutil.nodebranchnodefault(fctx),
947 948 parent=webutil.parents(fctx),
948 949 child=webutil.children(fctx),
949 950 permissions=fctx.manifest().flags(f))
950 951
951 952 @webcommand('filelog')
952 953 def filelog(web, req, tmpl):
953 954 """
954 955 /filelog/{revision}/{path}
955 956 --------------------------
956 957
957 958 Show information about the history of a file in the repository.
958 959
959 960 The ``revcount`` query string argument can be defined to control the
960 961 maximum number of entries to show.
961 962
962 963 The ``filelog`` template will be rendered.
963 964 """
964 965
965 966 try:
966 967 fctx = webutil.filectx(web.repo, req)
967 968 f = fctx.path()
968 969 fl = fctx.filelog()
969 970 except error.LookupError:
970 971 f = webutil.cleanpath(web.repo, req.form['file'][0])
971 972 fl = web.repo.file(f)
972 973 numrevs = len(fl)
973 974 if not numrevs: # file doesn't exist at all
974 975 raise
975 976 rev = webutil.changectx(web.repo, req).rev()
976 977 first = fl.linkrev(0)
977 978 if rev < first: # current rev is from before file existed
978 979 raise
979 980 frev = numrevs - 1
980 981 while fl.linkrev(frev) > rev:
981 982 frev -= 1
982 983 fctx = web.repo.filectx(f, fl.linkrev(frev))
983 984
984 985 revcount = web.maxshortchanges
985 986 if 'revcount' in req.form:
986 987 try:
987 988 revcount = int(req.form.get('revcount', [revcount])[0])
988 989 revcount = max(revcount, 1)
989 990 tmpl.defaults['sessionvars']['revcount'] = revcount
990 991 except ValueError:
991 992 pass
992 993
993 994 lessvars = copy.copy(tmpl.defaults['sessionvars'])
994 995 lessvars['revcount'] = max(revcount / 2, 1)
995 996 morevars = copy.copy(tmpl.defaults['sessionvars'])
996 997 morevars['revcount'] = revcount * 2
997 998
998 999 count = fctx.filerev() + 1
999 1000 start = max(0, fctx.filerev() - revcount + 1) # first rev on this page
1000 1001 end = min(count, start + revcount) # last rev on this page
1001 1002 parity = paritygen(web.stripecount, offset=start - end)
1002 1003
1003 1004 def entries():
1004 1005 l = []
1005 1006
1006 1007 repo = web.repo
1007 1008 revs = fctx.filelog().revs(start, end - 1)
1008 1009 for i in revs:
1009 1010 iterfctx = fctx.filectx(i)
1010 1011
1011 1012 l.append({"parity": parity.next(),
1012 1013 "filerev": i,
1013 1014 "file": f,
1014 1015 "node": iterfctx.hex(),
1015 1016 "author": iterfctx.user(),
1016 1017 "date": iterfctx.date(),
1017 1018 "rename": webutil.renamelink(iterfctx),
1018 1019 "parent": webutil.parents(iterfctx),
1019 1020 "child": webutil.children(iterfctx),
1020 1021 "desc": iterfctx.description(),
1021 1022 "extra": iterfctx.extra(),
1022 1023 "tags": webutil.nodetagsdict(repo, iterfctx.node()),
1023 1024 "bookmarks": webutil.nodebookmarksdict(
1024 1025 repo, iterfctx.node()),
1025 1026 "branch": webutil.nodebranchnodefault(iterfctx),
1026 1027 "inbranch": webutil.nodeinbranch(repo, iterfctx),
1027 1028 "branches": webutil.nodebranchdict(repo, iterfctx)})
1028 1029 for e in reversed(l):
1029 1030 yield e
1030 1031
1031 1032 entries = list(entries())
1032 1033 latestentry = entries[:1]
1033 1034
1034 1035 revnav = webutil.filerevnav(web.repo, fctx.path())
1035 1036 nav = revnav.gen(end - 1, revcount, count)
1036 1037 return tmpl("filelog", file=f, node=fctx.hex(), nav=nav,
1037 1038 entries=entries,
1038 1039 latestentry=latestentry,
1039 1040 revcount=revcount, morevars=morevars, lessvars=lessvars)
1040 1041
1041 1042 @webcommand('archive')
1042 1043 def archive(web, req, tmpl):
1043 1044 """
1044 1045 /archive/{revision}.{format}[/{path}]
1045 1046 -------------------------------------
1046 1047
1047 1048 Obtain an archive of repository content.
1048 1049
1049 1050 The content and type of the archive is defined by a URL path parameter.
1050 1051 ``format`` is the file extension of the archive type to be generated. e.g.
1051 1052 ``zip`` or ``tar.bz2``. Not all archive types may be allowed by your
1052 1053 server configuration.
1053 1054
1054 1055 The optional ``path`` URL parameter controls content to include in the
1055 1056 archive. If omitted, every file in the specified revision is present in the
1056 1057 archive. If included, only the specified file or contents of the specified
1057 1058 directory will be included in the archive.
1058 1059
1059 1060 No template is used for this handler. Raw, binary content is generated.
1060 1061 """
1061 1062
1062 1063 type_ = req.form.get('type', [None])[0]
1063 1064 allowed = web.configlist("web", "allow_archive")
1064 1065 key = req.form['node'][0]
1065 1066
1066 1067 if type_ not in web.archives:
1067 1068 msg = 'Unsupported archive type: %s' % type_
1068 1069 raise ErrorResponse(HTTP_NOT_FOUND, msg)
1069 1070
1070 1071 if not ((type_ in allowed or
1071 1072 web.configbool("web", "allow" + type_, False))):
1072 1073 msg = 'Archive type not allowed: %s' % type_
1073 1074 raise ErrorResponse(HTTP_FORBIDDEN, msg)
1074 1075
1075 1076 reponame = re.sub(r"\W+", "-", os.path.basename(web.reponame))
1076 1077 cnode = web.repo.lookup(key)
1077 1078 arch_version = key
1078 1079 if cnode == key or key == 'tip':
1079 1080 arch_version = short(cnode)
1080 1081 name = "%s-%s" % (reponame, arch_version)
1081 1082
1082 1083 ctx = webutil.changectx(web.repo, req)
1083 1084 pats = []
1084 1085 matchfn = scmutil.match(ctx, [])
1085 1086 file = req.form.get('file', None)
1086 1087 if file:
1087 1088 pats = ['path:' + file[0]]
1088 1089 matchfn = scmutil.match(ctx, pats, default='path')
1089 1090 if pats:
1090 1091 files = [f for f in ctx.manifest().keys() if matchfn(f)]
1091 1092 if not files:
1092 1093 raise ErrorResponse(HTTP_NOT_FOUND,
1093 1094 'file(s) not found: %s' % file[0])
1094 1095
1095 1096 mimetype, artype, extension, encoding = web.archive_specs[type_]
1096 1097 headers = [
1097 1098 ('Content-Disposition', 'attachment; filename=%s%s' % (name, extension))
1098 1099 ]
1099 1100 if encoding:
1100 1101 headers.append(('Content-Encoding', encoding))
1101 1102 req.headers.extend(headers)
1102 1103 req.respond(HTTP_OK, mimetype)
1103 1104
1104 1105 archival.archive(web.repo, req, cnode, artype, prefix=name,
1105 1106 matchfn=matchfn,
1106 1107 subrepos=web.configbool("web", "archivesubrepos"))
1107 1108 return []
1108 1109
1109 1110
1110 1111 @webcommand('static')
1111 1112 def static(web, req, tmpl):
1112 1113 fname = req.form['file'][0]
1113 1114 # a repo owner may set web.static in .hg/hgrc to get any file
1114 1115 # readable by the user running the CGI script
1115 1116 static = web.config("web", "static", None, untrusted=False)
1116 1117 if not static:
1117 1118 tp = web.templatepath or templater.templatepaths()
1118 1119 if isinstance(tp, str):
1119 1120 tp = [tp]
1120 1121 static = [os.path.join(p, 'static') for p in tp]
1121 1122 staticfile(static, fname, req)
1122 1123 return []
1123 1124
1124 1125 @webcommand('graph')
1125 1126 def graph(web, req, tmpl):
1126 1127 """
1127 1128 /graph[/{revision}]
1128 1129 -------------------
1129 1130
1130 1131 Show information about the graphical topology of the repository.
1131 1132
1132 1133 Information rendered by this handler can be used to create visual
1133 1134 representations of repository topology.
1134 1135
1135 1136 The ``revision`` URL parameter controls the starting changeset.
1136 1137
1137 1138 The ``revcount`` query string argument can define the number of changesets
1138 1139 to show information for.
1139 1140
1140 1141 This handler will render the ``graph`` template.
1141 1142 """
1142 1143
1143 1144 ctx = webutil.changectx(web.repo, req)
1144 1145 rev = ctx.rev()
1145 1146
1146 1147 bg_height = 39
1147 1148 revcount = web.maxshortchanges
1148 1149 if 'revcount' in req.form:
1149 1150 try:
1150 1151 revcount = int(req.form.get('revcount', [revcount])[0])
1151 1152 revcount = max(revcount, 1)
1152 1153 tmpl.defaults['sessionvars']['revcount'] = revcount
1153 1154 except ValueError:
1154 1155 pass
1155 1156
1156 1157 lessvars = copy.copy(tmpl.defaults['sessionvars'])
1157 1158 lessvars['revcount'] = max(revcount / 2, 1)
1158 1159 morevars = copy.copy(tmpl.defaults['sessionvars'])
1159 1160 morevars['revcount'] = revcount * 2
1160 1161
1161 1162 count = len(web.repo)
1162 1163 pos = rev
1163 1164
1164 1165 uprev = min(max(0, count - 1), rev + revcount)
1165 1166 downrev = max(0, rev - revcount)
1166 1167 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
1167 1168
1168 1169 tree = []
1169 1170 if pos != -1:
1170 1171 allrevs = web.repo.changelog.revs(pos, 0)
1171 1172 revs = []
1172 1173 for i in allrevs:
1173 1174 revs.append(i)
1174 1175 if len(revs) >= revcount:
1175 1176 break
1176 1177
1177 1178 # We have to feed a baseset to dagwalker as it is expecting smartset
1178 1179 # object. This does not have a big impact on hgweb performance itself
1179 1180 # since hgweb graphing code is not itself lazy yet.
1180 1181 dag = graphmod.dagwalker(web.repo, revset.baseset(revs))
1181 1182 # As we said one line above... not lazy.
1182 1183 tree = list(graphmod.colored(dag, web.repo))
1183 1184
1184 1185 def getcolumns(tree):
1185 1186 cols = 0
1186 1187 for (id, type, ctx, vtx, edges) in tree:
1187 1188 if type != graphmod.CHANGESET:
1188 1189 continue
1189 1190 cols = max(cols, max([edge[0] for edge in edges] or [0]),
1190 1191 max([edge[1] for edge in edges] or [0]))
1191 1192 return cols
1192 1193
1193 1194 def graphdata(usetuples, **map):
1194 1195 data = []
1195 1196
1196 1197 row = 0
1197 1198 for (id, type, ctx, vtx, edges) in tree:
1198 1199 if type != graphmod.CHANGESET:
1199 1200 continue
1200 1201 node = str(ctx)
1201 1202 age = templatefilters.age(ctx.date())
1202 1203 desc = templatefilters.firstline(ctx.description())
1203 1204 desc = cgi.escape(templatefilters.nonempty(desc))
1204 1205 user = cgi.escape(templatefilters.person(ctx.user()))
1205 1206 branch = cgi.escape(ctx.branch())
1206 1207 try:
1207 1208 branchnode = web.repo.branchtip(branch)
1208 1209 except error.RepoLookupError:
1209 1210 branchnode = None
1210 1211 branch = branch, branchnode == ctx.node()
1211 1212
1212 1213 if usetuples:
1213 1214 data.append((node, vtx, edges, desc, user, age, branch,
1214 1215 [cgi.escape(x) for x in ctx.tags()],
1215 1216 [cgi.escape(x) for x in ctx.bookmarks()]))
1216 1217 else:
1217 1218 edgedata = [{'col': edge[0], 'nextcol': edge[1],
1218 1219 'color': (edge[2] - 1) % 6 + 1,
1219 1220 'width': edge[3], 'bcolor': edge[4]}
1220 1221 for edge in edges]
1221 1222
1222 1223 data.append(
1223 1224 {'node': node,
1224 1225 'col': vtx[0],
1225 1226 'color': (vtx[1] - 1) % 6 + 1,
1226 1227 'edges': edgedata,
1227 1228 'row': row,
1228 1229 'nextrow': row + 1,
1229 1230 'desc': desc,
1230 1231 'user': user,
1231 1232 'age': age,
1232 1233 'bookmarks': webutil.nodebookmarksdict(
1233 1234 web.repo, ctx.node()),
1234 1235 'branches': webutil.nodebranchdict(web.repo, ctx),
1235 1236 'inbranch': webutil.nodeinbranch(web.repo, ctx),
1236 1237 'tags': webutil.nodetagsdict(web.repo, ctx.node())})
1237 1238
1238 1239 row += 1
1239 1240
1240 1241 return data
1241 1242
1242 1243 cols = getcolumns(tree)
1243 1244 rows = len(tree)
1244 1245 canvasheight = (rows + 1) * bg_height - 27
1245 1246
1246 1247 return tmpl('graph', rev=rev, revcount=revcount, uprev=uprev,
1247 1248 lessvars=lessvars, morevars=morevars, downrev=downrev,
1248 1249 cols=cols, rows=rows,
1249 1250 canvaswidth=(cols + 1) * bg_height,
1250 1251 truecanvasheight=rows * bg_height,
1251 1252 canvasheight=canvasheight, bg_height=bg_height,
1252 1253 jsdata=lambda **x: graphdata(True, **x),
1253 1254 nodes=lambda **x: graphdata(False, **x),
1254 1255 node=ctx.hex(), changenav=changenav)
1255 1256
1256 1257 def _getdoc(e):
1257 1258 doc = e[0].__doc__
1258 1259 if doc:
1259 1260 doc = _(doc).split('\n')[0]
1260 1261 else:
1261 1262 doc = _('(no help text available)')
1262 1263 return doc
1263 1264
1264 1265 @webcommand('help')
1265 1266 def help(web, req, tmpl):
1266 1267 """
1267 1268 /help[/{topic}]
1268 1269 ---------------
1269 1270
1270 1271 Render help documentation.
1271 1272
1272 1273 This web command is roughly equivalent to :hg:`help`. If a ``topic``
1273 1274 is defined, that help topic will be rendered. If not, an index of
1274 1275 available help topics will be rendered.
1275 1276
1276 1277 The ``help`` template will be rendered when requesting help for a topic.
1277 1278 ``helptopics`` will be rendered for the index of help topics.
1278 1279 """
1279 1280 from mercurial import commands # avoid cycle
1280 1281 from mercurial import help as helpmod # avoid cycle
1281 1282
1282 1283 topicname = req.form.get('node', [None])[0]
1283 1284 if not topicname:
1284 1285 def topics(**map):
1285 1286 for entries, summary, _doc in helpmod.helptable:
1286 1287 yield {'topic': entries[0], 'summary': summary}
1287 1288
1288 1289 early, other = [], []
1289 1290 primary = lambda s: s.split('|')[0]
1290 1291 for c, e in commands.table.iteritems():
1291 1292 doc = _getdoc(e)
1292 1293 if 'DEPRECATED' in doc or c.startswith('debug'):
1293 1294 continue
1294 1295 cmd = primary(c)
1295 1296 if cmd.startswith('^'):
1296 1297 early.append((cmd[1:], doc))
1297 1298 else:
1298 1299 other.append((cmd, doc))
1299 1300
1300 1301 early.sort()
1301 1302 other.sort()
1302 1303
1303 1304 def earlycommands(**map):
1304 1305 for c, doc in early:
1305 1306 yield {'topic': c, 'summary': doc}
1306 1307
1307 1308 def othercommands(**map):
1308 1309 for c, doc in other:
1309 1310 yield {'topic': c, 'summary': doc}
1310 1311
1311 1312 return tmpl('helptopics', topics=topics, earlycommands=earlycommands,
1312 1313 othercommands=othercommands, title='Index')
1313 1314
1314 1315 u = webutil.wsgiui()
1315 1316 u.verbose = True
1316 1317 try:
1317 1318 doc = helpmod.help_(u, topicname)
1318 1319 except error.UnknownCommand:
1319 1320 raise ErrorResponse(HTTP_NOT_FOUND)
1320 1321 return tmpl('help', topic=topicname, doc=doc)
1321 1322
1322 1323 # tell hggettext to extract docstrings from these functions:
1323 1324 i18nfunctions = commands.values()
@@ -1,61 +1,61 b''
1 1 {header}
2 2 <title>{repo|escape}: {node|short} {path|escape}</title>
3 3 </head>
4 4 <body>
5 5
6 6 <div class="container">
7 7 <div class="menu">
8 8 <div class="logo">
9 9 <a href="{logourl}">
10 10 <img src="{staticurl|urlescape}{logoimg}" alt="mercurial" /></a>
11 11 </div>
12 12 <ul>
13 13 <li><a href="{url|urlescape}shortlog/{node|short}{sessionvars%urlparameter}">log</a></li>
14 14 <li><a href="{url|urlescape}graph/{node|short}{sessionvars%urlparameter}">graph</a></li>
15 15 <li><a href="{url|urlescape}tags{sessionvars%urlparameter}">tags</a></li>
16 16 <li><a href="{url|urlescape}bookmarks{sessionvars%urlparameter}">bookmarks</a></li>
17 17 <li><a href="{url|urlescape}branches{sessionvars%urlparameter}">branches</a></li>
18 18 </ul>
19 19 <ul>
20 20 <li><a href="{url|urlescape}rev/{node|short}{sessionvars%urlparameter}">changeset</a></li>
21 21 <li class="active">browse</li>
22 22 </ul>
23 23 <ul>
24 24 {archives%archiveentry}
25 25 </ul>
26 26 <ul>
27 27 <li><a href="{url|urlescape}help{sessionvars%urlparameter}">help</a></li>
28 28 </ul>
29 29 </div>
30 30
31 31 <div class="main">
32 32 <h2 class="breadcrumb"><a href="/">Mercurial</a> {pathdef%breadcrumb}</h2>
33 <h3>directory {path|escape} @ {rev}:{node|short} {tags%changelogtag}</h3>
33 <h3>directory {path|escape} @ {rev}:{node|short} {branch%changelogbranchname}{tags%changelogtag}{bookmarks%changelogtag}</h3>
34 34
35 35 <form class="search" action="{url|urlescape}log">
36 36 {sessionvars%hiddenformentry}
37 37 <p><input name="rev" id="search1" type="text" size="30" /></p>
38 38 <div id="hint">{searchhint}</div>
39 39 </form>
40 40
41 41 <table class="bigtable">
42 42 <thead>
43 43 <tr>
44 44 <th class="name">name</th>
45 45 <th class="size">size</th>
46 46 <th class="permissions">permissions</th>
47 47 </tr>
48 48 </thead>
49 49 <tbody class="stripes2">
50 50 <tr class="fileline">
51 51 <td class="name"><a href="{url|urlescape}file/{node|short}{up|urlescape}{sessionvars%urlparameter}">[up]</a></td>
52 52 <td class="size"></td>
53 53 <td class="permissions">drwxr-xr-x</td>
54 54 </tr>
55 55 {dentries%direntry}
56 56 {fentries%fileentry}
57 57 </tbody>
58 58 </table>
59 59 </div>
60 60 </div>
61 61 {footer}
General Comments 0
You need to be logged in to leave comments. Login now