comparison mercurial/hgweb/webcommands.py @ 6393:894875eae49b

hgweb: refactor hgweb code
author Dirkjan Ochtman <dirkjan@ochtman.nl>
date Fri, 28 Mar 2008 19:40:44 +0100
parents 2540521dc7c1
children 644a56c7ae79
comparison
equal deleted inserted replaced
6392:2540521dc7c1 6393:894875eae49b
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com> 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 # 4 #
5 # This software may be used and distributed according to the terms 5 # This software may be used and distributed according to the terms
6 # of the GNU General Public License, incorporated herein by reference. 6 # of the GNU General Public License, incorporated herein by reference.
7 7
8 import os, mimetypes 8 import os, mimetypes, re
9 import webutil 9 import webutil
10 from mercurial import revlog 10 from mercurial import revlog, archival
11 from mercurial.node import hex, nullid
11 from mercurial.util import binary 12 from mercurial.util import binary
12 from mercurial.repo import RepoError 13 from mercurial.repo import RepoError
13 from common import staticfile, ErrorResponse, HTTP_OK, HTTP_NOT_FOUND 14 from common import paritygen, staticfile, get_contact, ErrorResponse
15 from common import HTTP_OK, HTTP_NOT_FOUND
14 16
15 # __all__ is populated with the allowed commands. Be sure to add to it if 17 # __all__ is populated with the allowed commands. Be sure to add to it if
16 # you're adding a new command, or the new command won't work. 18 # you're adding a new command, or the new command won't work.
17 19
18 __all__ = [ 20 __all__ = [
28 return changelog(web, req, tmpl) 30 return changelog(web, req, tmpl)
29 31
30 def rawfile(web, req, tmpl): 32 def rawfile(web, req, tmpl):
31 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0]) 33 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
32 if not path: 34 if not path:
33 content = web.manifest(tmpl, webutil.changectx(web.repo, req), path) 35 content = manifest(web, req, tmpl)
34 req.respond(HTTP_OK, web.ctype) 36 req.respond(HTTP_OK, web.ctype)
35 return content 37 return content
36 38
37 try: 39 try:
38 fctx = webutil.filectx(web.repo, req) 40 fctx = webutil.filectx(web.repo, req)
39 except revlog.LookupError, inst: 41 except revlog.LookupError, inst:
40 try: 42 try:
41 content = web.manifest(tmpl, webutil.changectx(web.repo, req), path) 43 content = manifest(web, req, tmpl)
42 req.respond(HTTP_OK, web.ctype) 44 req.respond(HTTP_OK, web.ctype)
43 return content 45 return content
44 except ErrorResponse: 46 except ErrorResponse:
45 raise inst 47 raise inst
46 48
51 mt = mt or 'application/octet-stream' 53 mt = mt or 'application/octet-stream'
52 54
53 req.respond(HTTP_OK, mt, path, len(text)) 55 req.respond(HTTP_OK, mt, path, len(text))
54 return [text] 56 return [text]
55 57
58 def _filerevision(web, tmpl, fctx):
59 f = fctx.path()
60 text = fctx.data()
61 fl = fctx.filelog()
62 n = fctx.filenode()
63 parity = paritygen(web.stripecount)
64
65 if binary(text):
66 mt = mimetypes.guess_type(f)[0] or 'application/octet-stream'
67 text = '(binary:%s)' % mt
68
69 def lines():
70 for lineno, t in enumerate(text.splitlines(1)):
71 yield {"line": t,
72 "lineid": "l%d" % (lineno + 1),
73 "linenumber": "% 6d" % (lineno + 1),
74 "parity": parity.next()}
75
76 return tmpl("filerevision",
77 file=f,
78 path=webutil.up(f),
79 text=lines(),
80 rev=fctx.rev(),
81 node=hex(fctx.node()),
82 author=fctx.user(),
83 date=fctx.date(),
84 desc=fctx.description(),
85 branch=webutil.nodebranchnodefault(fctx),
86 parent=webutil.siblings(fctx.parents()),
87 child=webutil.siblings(fctx.children()),
88 rename=webutil.renamelink(fl, n),
89 permissions=fctx.manifest().flags(f))
90
56 def file(web, req, tmpl): 91 def file(web, req, tmpl):
57 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0]) 92 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
58 if path: 93 if path:
59 try: 94 try:
60 return web.filerevision(tmpl, webutil.filectx(web.repo, req)) 95 return _filerevision(web, tmpl, webutil.filectx(web.repo, req))
61 except revlog.LookupError, inst: 96 except revlog.LookupError, inst:
62 pass 97 pass
63 98
64 try: 99 try:
65 return web.manifest(tmpl, webutil.changectx(web.repo, req), path) 100 return manifest(web, req, tmpl)
66 except ErrorResponse: 101 except ErrorResponse:
67 raise inst 102 raise inst
103
104 def _search(web, tmpl, query):
105
106 def changelist(**map):
107 cl = web.repo.changelog
108 count = 0
109 qw = query.lower().split()
110
111 def revgen():
112 for i in xrange(cl.count() - 1, 0, -100):
113 l = []
114 for j in xrange(max(0, i - 100), i + 1):
115 ctx = web.repo.changectx(j)
116 l.append(ctx)
117 l.reverse()
118 for e in l:
119 yield e
120
121 for ctx in revgen():
122 miss = 0
123 for q in qw:
124 if not (q in ctx.user().lower() or
125 q in ctx.description().lower() or
126 q in " ".join(ctx.files()).lower()):
127 miss = 1
128 break
129 if miss:
130 continue
131
132 count = 1
133 n = ctx.node()
134 showtags = webutil.showtag(web.repo, tmpl, 'changelogtag', n)
135
136 yield tmpl('searchentry',
137 parity=parity.next(),
138 author=ctx.user(),
139 parent=webutil.siblings(ctx.parents()),
140 child=webutil.siblings(ctx.children()),
141 changelogtag=showtags,
142 desc=ctx.description(),
143 date=ctx.date(),
144 files=web.listfilediffs(tmpl, ctx.files(), n),
145 rev=ctx.rev(),
146 node=hex(n),
147 tags=webutil.nodetagsdict(web.repo, n),
148 inbranch=webutil.nodeinbranch(web.repo, ctx),
149 branches=webutil.nodebranchdict(web.repo, ctx))
150
151 if count >= web.maxchanges:
152 break
153
154 cl = web.repo.changelog
155 parity = paritygen(web.stripecount)
156
157 return tmpl('search',
158 query=query,
159 node=hex(cl.tip()),
160 entries=changelist,
161 archives=web.archivelist("tip"))
68 162
69 def changelog(web, req, tmpl, shortlog = False): 163 def changelog(web, req, tmpl, shortlog = False):
70 if 'node' in req.form: 164 if 'node' in req.form:
71 ctx = webutil.changectx(web.repo, req) 165 ctx = webutil.changectx(web.repo, req)
72 else: 166 else:
75 else: 169 else:
76 hi = web.repo.changelog.count() - 1 170 hi = web.repo.changelog.count() - 1
77 try: 171 try:
78 ctx = web.repo.changectx(hi) 172 ctx = web.repo.changectx(hi)
79 except RepoError: 173 except RepoError:
80 return web.search(tmpl, hi) # XXX redirect to 404 page? 174 return _search(web, tmpl, hi) # XXX redirect to 404 page?
81 175
82 return web.changelog(tmpl, ctx, shortlog = shortlog) 176 def changelist(limit=0, **map):
177 cl = web.repo.changelog
178 l = [] # build a list in forward order for efficiency
179 for i in xrange(start, end):
180 ctx = web.repo.changectx(i)
181 n = ctx.node()
182 showtags = webutil.showtag(web.repo, tmpl, 'changelogtag', n)
183
184 l.insert(0, {"parity": parity.next(),
185 "author": ctx.user(),
186 "parent": webutil.siblings(ctx.parents(), i - 1),
187 "child": webutil.siblings(ctx.children(), i + 1),
188 "changelogtag": showtags,
189 "desc": ctx.description(),
190 "date": ctx.date(),
191 "files": web.listfilediffs(tmpl, ctx.files(), n),
192 "rev": i,
193 "node": hex(n),
194 "tags": webutil.nodetagsdict(web.repo, n),
195 "inbranch": webutil.nodeinbranch(web.repo, ctx),
196 "branches": webutil.nodebranchdict(web.repo, ctx)
197 })
198
199 if limit > 0:
200 l = l[:limit]
201
202 for e in l:
203 yield e
204
205 maxchanges = shortlog and web.maxshortchanges or web.maxchanges
206 cl = web.repo.changelog
207 count = cl.count()
208 pos = ctx.rev()
209 start = max(0, pos - maxchanges + 1)
210 end = min(count, start + maxchanges)
211 pos = end - 1
212 parity = paritygen(web.stripecount, offset=start-end)
213
214 changenav = webutil.revnavgen(pos, maxchanges, count, web.repo.changectx)
215
216 return tmpl(shortlog and 'shortlog' or 'changelog',
217 changenav=changenav,
218 node=hex(cl.tip()),
219 rev=pos, changesets=count,
220 entries=lambda **x: changelist(limit=0,**x),
221 latestentry=lambda **x: changelist(limit=1,**x),
222 archives=web.archivelist("tip"))
83 223
84 def shortlog(web, req, tmpl): 224 def shortlog(web, req, tmpl):
85 return changelog(web, req, tmpl, shortlog = True) 225 return changelog(web, req, tmpl, shortlog = True)
86 226
87 def changeset(web, req, tmpl): 227 def changeset(web, req, tmpl):
88 return web.changeset(tmpl, webutil.changectx(web.repo, req)) 228 ctx = webutil.changectx(web.repo, req)
229 n = ctx.node()
230 showtags = webutil.showtag(web.repo, tmpl, 'changesettag', n)
231 parents = ctx.parents()
232 p1 = parents[0].node()
233
234 files = []
235 parity = paritygen(web.stripecount)
236 for f in ctx.files():
237 files.append(tmpl("filenodelink",
238 node=hex(n), file=f,
239 parity=parity.next()))
240
241 diffs = web.diff(tmpl, p1, n, None)
242 return tmpl('changeset',
243 diff=diffs,
244 rev=ctx.rev(),
245 node=hex(n),
246 parent=webutil.siblings(parents),
247 child=webutil.siblings(ctx.children()),
248 changesettag=showtags,
249 author=ctx.user(),
250 desc=ctx.description(),
251 date=ctx.date(),
252 files=files,
253 archives=web.archivelist(hex(n)),
254 tags=webutil.nodetagsdict(web.repo, n),
255 branch=webutil.nodebranchnodefault(ctx),
256 inbranch=webutil.nodeinbranch(web.repo, ctx),
257 branches=webutil.nodebranchdict(web.repo, ctx))
89 258
90 rev = changeset 259 rev = changeset
91 260
92 def manifest(web, req, tmpl): 261 def manifest(web, req, tmpl):
93 return web.manifest(tmpl, webutil.changectx(web.repo, req), 262 ctx = webutil.changectx(web.repo, req)
94 webutil.cleanpath(web.repo, req.form['path'][0])) 263 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
264 mf = ctx.manifest()
265 node = ctx.node()
266
267 files = {}
268 parity = paritygen(web.stripecount)
269
270 if path and path[-1] != "/":
271 path += "/"
272 l = len(path)
273 abspath = "/" + path
274
275 for f, n in mf.items():
276 if f[:l] != path:
277 continue
278 remain = f[l:]
279 if "/" in remain:
280 short = remain[:remain.index("/") + 1] # bleah
281 files[short] = (f, None)
282 else:
283 short = os.path.basename(remain)
284 files[short] = (f, n)
285
286 if not files:
287 raise ErrorResponse(HTTP_NOT_FOUND, 'path not found: ' + path)
288
289 def filelist(**map):
290 fl = files.keys()
291 fl.sort()
292 for f in fl:
293 full, fnode = files[f]
294 if not fnode:
295 continue
296
297 fctx = ctx.filectx(full)
298 yield {"file": full,
299 "parity": parity.next(),
300 "basename": f,
301 "date": fctx.changectx().date(),
302 "size": fctx.size(),
303 "permissions": mf.flags(full)}
304
305 def dirlist(**map):
306 fl = files.keys()
307 fl.sort()
308 for f in fl:
309 full, fnode = files[f]
310 if fnode:
311 continue
312
313 yield {"parity": parity.next(),
314 "path": "%s%s" % (abspath, f),
315 "basename": f[:-1]}
316
317 return tmpl("manifest",
318 rev=ctx.rev(),
319 node=hex(node),
320 path=abspath,
321 up=webutil.up(abspath),
322 upparity=parity.next(),
323 fentries=filelist,
324 dentries=dirlist,
325 archives=web.archivelist(hex(node)),
326 tags=webutil.nodetagsdict(web.repo, node),
327 inbranch=webutil.nodeinbranch(web.repo, ctx),
328 branches=webutil.nodebranchdict(web.repo, ctx))
95 329
96 def tags(web, req, tmpl): 330 def tags(web, req, tmpl):
97 return web.tags(tmpl) 331 i = web.repo.tagslist()
332 i.reverse()
333 parity = paritygen(web.stripecount)
334
335 def entries(notip=False,limit=0, **map):
336 count = 0
337 for k, n in i:
338 if notip and k == "tip":
339 continue
340 if limit > 0 and count >= limit:
341 continue
342 count = count + 1
343 yield {"parity": parity.next(),
344 "tag": k,
345 "date": web.repo.changectx(n).date(),
346 "node": hex(n)}
347
348 return tmpl("tags",
349 node=hex(web.repo.changelog.tip()),
350 entries=lambda **x: entries(False,0, **x),
351 entriesnotip=lambda **x: entries(True,0, **x),
352 latestentry=lambda **x: entries(True,1, **x))
98 353
99 def summary(web, req, tmpl): 354 def summary(web, req, tmpl):
100 return web.summary(tmpl) 355 i = web.repo.tagslist()
356 i.reverse()
357
358 def tagentries(**map):
359 parity = paritygen(web.stripecount)
360 count = 0
361 for k, n in i:
362 if k == "tip": # skip tip
363 continue
364
365 count = 1
366 if count > 10: # limit to 10 tags
367 break
368
369 yield tmpl("tagentry",
370 parity=parity.next(),
371 tag=k,
372 node=hex(n),
373 date=web.repo.changectx(n).date())
374
375 def branches(**map):
376 parity = paritygen(web.stripecount)
377
378 b = web.repo.branchtags()
379 l = [(-web.repo.changelog.rev(n), n, t) for t, n in b.items()]
380 l.sort()
381
382 for r,n,t in l:
383 ctx = web.repo.changectx(n)
384 yield {'parity': parity.next(),
385 'branch': t,
386 'node': hex(n),
387 'date': ctx.date()}
388
389 def changelist(**map):
390 parity = paritygen(web.stripecount, offset=start-end)
391 l = [] # build a list in forward order for efficiency
392 for i in xrange(start, end):
393 ctx = web.repo.changectx(i)
394 n = ctx.node()
395 hn = hex(n)
396
397 l.insert(0, tmpl(
398 'shortlogentry',
399 parity=parity.next(),
400 author=ctx.user(),
401 desc=ctx.description(),
402 date=ctx.date(),
403 rev=i,
404 node=hn,
405 tags=webutil.nodetagsdict(web.repo, n),
406 inbranch=webutil.nodeinbranch(web.repo, ctx),
407 branches=webutil.nodebranchdict(web.repo, ctx)))
408
409 yield l
410
411 cl = web.repo.changelog
412 count = cl.count()
413 start = max(0, count - web.maxchanges)
414 end = min(count, start + web.maxchanges)
415
416 return tmpl("summary",
417 desc=web.config("web", "description", "unknown"),
418 owner=get_contact(web.config) or "unknown",
419 lastchange=cl.read(cl.tip())[2],
420 tags=tagentries,
421 branches=branches,
422 shortlog=changelist,
423 node=hex(cl.tip()),
424 archives=web.archivelist("tip"))
101 425
102 def filediff(web, req, tmpl): 426 def filediff(web, req, tmpl):
103 return web.filediff(tmpl, webutil.filectx(web.repo, req)) 427 fctx = webutil.filectx(web.repo, req)
428 n = fctx.node()
429 path = fctx.path()
430 parents = fctx.parents()
431 p1 = parents and parents[0].node() or nullid
432
433 diffs = web.diff(tmpl, p1, n, [path])
434 return tmpl("filediff",
435 file=path,
436 node=hex(n),
437 rev=fctx.rev(),
438 branch=webutil.nodebranchnodefault(fctx),
439 parent=webutil.siblings(parents),
440 child=webutil.siblings(fctx.children()),
441 diff=diffs)
104 442
105 diff = filediff 443 diff = filediff
106 444
107 def annotate(web, req, tmpl): 445 def annotate(web, req, tmpl):
108 return web.fileannotate(tmpl, webutil.filectx(web.repo, req)) 446 fctx = webutil.filectx(web.repo, req)
447 f = fctx.path()
448 n = fctx.filenode()
449 fl = fctx.filelog()
450 parity = paritygen(web.stripecount)
451
452 def annotate(**map):
453 last = None
454 if binary(fctx.data()):
455 mt = (mimetypes.guess_type(fctx.path())[0]
456 or 'application/octet-stream')
457 lines = enumerate([((fctx.filectx(fctx.filerev()), 1),
458 '(binary:%s)' % mt)])
459 else:
460 lines = enumerate(fctx.annotate(follow=True, linenumber=True))
461 for lineno, ((f, targetline), l) in lines:
462 fnode = f.filenode()
463 name = web.repo.ui.shortuser(f.user())
464
465 if last != fnode:
466 last = fnode
467
468 yield {"parity": parity.next(),
469 "node": hex(f.node()),
470 "rev": f.rev(),
471 "author": name,
472 "file": f.path(),
473 "targetline": targetline,
474 "line": l,
475 "lineid": "l%d" % (lineno + 1),
476 "linenumber": "% 6d" % (lineno + 1)}
477
478 return tmpl("fileannotate",
479 file=f,
480 annotate=annotate,
481 path=webutil.up(f),
482 rev=fctx.rev(),
483 node=hex(fctx.node()),
484 author=fctx.user(),
485 date=fctx.date(),
486 desc=fctx.description(),
487 rename=webutil.renamelink(fl, n),
488 branch=webutil.nodebranchnodefault(fctx),
489 parent=webutil.siblings(fctx.parents()),
490 child=webutil.siblings(fctx.children()),
491 permissions=fctx.manifest().flags(f))
109 492
110 def filelog(web, req, tmpl): 493 def filelog(web, req, tmpl):
111 return web.filelog(tmpl, webutil.filectx(web.repo, req)) 494 fctx = webutil.filectx(web.repo, req)
495 f = fctx.path()
496 fl = fctx.filelog()
497 count = fl.count()
498 pagelen = web.maxshortchanges
499 pos = fctx.filerev()
500 start = max(0, pos - pagelen + 1)
501 end = min(count, start + pagelen)
502 pos = end - 1
503 parity = paritygen(web.stripecount, offset=start-end)
504
505 def entries(limit=0, **map):
506 l = []
507
508 for i in xrange(start, end):
509 ctx = fctx.filectx(i)
510 n = fl.node(i)
511
512 l.insert(0, {"parity": parity.next(),
513 "filerev": i,
514 "file": f,
515 "node": hex(ctx.node()),
516 "author": ctx.user(),
517 "date": ctx.date(),
518 "rename": webutil.renamelink(fl, n),
519 "parent": webutil.siblings(fctx.parents()),
520 "child": webutil.siblings(fctx.children()),
521 "desc": ctx.description()})
522
523 if limit > 0:
524 l = l[:limit]
525
526 for e in l:
527 yield e
528
529 nodefunc = lambda x: fctx.filectx(fileid=x)
530 nav = webutil.revnavgen(pos, pagelen, count, nodefunc)
531 return tmpl("filelog", file=f, node=hex(fctx.node()), nav=nav,
532 entries=lambda **x: entries(limit=0, **x),
533 latestentry=lambda **x: entries(limit=1, **x))
534
112 535
113 def archive(web, req, tmpl): 536 def archive(web, req, tmpl):
114 type_ = req.form['type'][0] 537 type_ = req.form['type'][0]
115 allowed = web.configlist("web", "allow_archive") 538 allowed = web.configlist("web", "allow_archive")
116 if (type_ in web.archives and (type_ in allowed or 539 key = req.form['node'][0]
540
541 if not (type_ in web.archives and (type_ in allowed or
117 web.configbool("web", "allow" + type_, False))): 542 web.configbool("web", "allow" + type_, False))):
118 web.archive(tmpl, req, req.form['node'][0], type_) 543 msg = 'Unsupported archive type: %s' % type_
119 return [] 544 raise ErrorResponse(HTTP_NOT_FOUND, msg)
120 raise ErrorResponse(HTTP_NOT_FOUND, 'unsupported archive type: %s' % type_) 545
546 reponame = re.sub(r"\W+", "-", os.path.basename(web.reponame))
547 cnode = web.repo.lookup(key)
548 arch_version = key
549 if cnode == key or key == 'tip':
550 arch_version = short(cnode)
551 name = "%s-%s" % (reponame, arch_version)
552 mimetype, artype, extension, encoding = web.archive_specs[type_]
553 headers = [
554 ('Content-Type', mimetype),
555 ('Content-Disposition', 'attachment; filename=%s%s' % (name, extension))
556 ]
557 if encoding:
558 headers.append(('Content-Encoding', encoding))
559 req.header(headers)
560 req.respond(HTTP_OK)
561 archival.archive(web.repo, req, cnode, artype, prefix=name)
562 return []
563
121 564
122 def static(web, req, tmpl): 565 def static(web, req, tmpl):
123 fname = req.form['file'][0] 566 fname = req.form['file'][0]
124 # a repo owner may set web.static in .hg/hgrc to get any file 567 # a repo owner may set web.static in .hg/hgrc to get any file
125 # readable by the user running the CGI script 568 # readable by the user running the CGI script