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