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