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