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