##// END OF EJS Templates
Use wsgi.url_scheme instead of ad-hoc CGI checks....
Wesley J. Landaker -
r4872:419a6f71 default
parent child Browse files
Show More
@@ -1,1187 +1,1188
1 1 # hgweb/hgweb_mod.py - Web interface for a repository.
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms
7 7 # of the GNU General Public License, incorporated herein by reference.
8 8
9 9 import os, mimetypes, re, zlib, mimetools, cStringIO, sys
10 10 import tempfile, urllib, bz2
11 11 from mercurial.node import *
12 12 from mercurial.i18n import gettext as _
13 13 from mercurial import mdiff, ui, hg, util, archival, streamclone, patch
14 14 from mercurial import revlog, templater
15 15 from common import get_mtime, staticfile, style_map, paritygen
16 16
17 17 def _up(p):
18 18 if p[0] != "/":
19 19 p = "/" + p
20 20 if p[-1] == "/":
21 21 p = p[:-1]
22 22 up = os.path.dirname(p)
23 23 if up == "/":
24 24 return "/"
25 25 return up + "/"
26 26
27 27 def revnavgen(pos, pagelen, limit, nodefunc):
28 28 def seq(factor, limit=None):
29 29 if limit:
30 30 yield limit
31 31 if limit >= 20 and limit <= 40:
32 32 yield 50
33 33 else:
34 34 yield 1 * factor
35 35 yield 3 * factor
36 36 for f in seq(factor * 10):
37 37 yield f
38 38
39 39 def nav(**map):
40 40 l = []
41 41 last = 0
42 42 for f in seq(1, pagelen):
43 43 if f < pagelen or f <= last:
44 44 continue
45 45 if f > limit:
46 46 break
47 47 last = f
48 48 if pos + f < limit:
49 49 l.append(("+%d" % f, hex(nodefunc(pos + f).node())))
50 50 if pos - f >= 0:
51 51 l.insert(0, ("-%d" % f, hex(nodefunc(pos - f).node())))
52 52
53 53 try:
54 54 yield {"label": "(0)", "node": hex(nodefunc('0').node())}
55 55
56 56 for label, node in l:
57 57 yield {"label": label, "node": node}
58 58
59 59 yield {"label": "tip", "node": "tip"}
60 60 except hg.RepoError:
61 61 pass
62 62
63 63 return nav
64 64
65 65 class hgweb(object):
66 66 def __init__(self, repo, name=None):
67 67 if type(repo) == type(""):
68 68 self.repo = hg.repository(ui.ui(report_untrusted=False), repo)
69 69 else:
70 70 self.repo = repo
71 71
72 72 self.mtime = -1
73 73 self.reponame = name
74 74 self.archives = 'zip', 'gz', 'bz2'
75 75 self.stripecount = 1
76 76 # a repo owner may set web.templates in .hg/hgrc to get any file
77 77 # readable by the user running the CGI script
78 78 self.templatepath = self.config("web", "templates",
79 79 templater.templatepath(),
80 80 untrusted=False)
81 81
82 82 # The CGI scripts are often run by a user different from the repo owner.
83 83 # Trust the settings from the .hg/hgrc files by default.
84 84 def config(self, section, name, default=None, untrusted=True):
85 85 return self.repo.ui.config(section, name, default,
86 86 untrusted=untrusted)
87 87
88 88 def configbool(self, section, name, default=False, untrusted=True):
89 89 return self.repo.ui.configbool(section, name, default,
90 90 untrusted=untrusted)
91 91
92 92 def configlist(self, section, name, default=None, untrusted=True):
93 93 return self.repo.ui.configlist(section, name, default,
94 94 untrusted=untrusted)
95 95
96 96 def refresh(self):
97 97 mtime = get_mtime(self.repo.root)
98 98 if mtime != self.mtime:
99 99 self.mtime = mtime
100 100 self.repo = hg.repository(self.repo.ui, self.repo.root)
101 101 self.maxchanges = int(self.config("web", "maxchanges", 10))
102 102 self.stripecount = int(self.config("web", "stripes", 1))
103 103 self.maxshortchanges = int(self.config("web", "maxshortchanges", 60))
104 104 self.maxfiles = int(self.config("web", "maxfiles", 10))
105 105 self.allowpull = self.configbool("web", "allowpull", True)
106 106 self.encoding = self.config("web", "encoding", util._encoding)
107 107
108 108 def archivelist(self, nodeid):
109 109 allowed = self.configlist("web", "allow_archive")
110 110 for i, spec in self.archive_specs.iteritems():
111 111 if i in allowed or self.configbool("web", "allow" + i):
112 112 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
113 113
114 114 def listfilediffs(self, files, changeset):
115 115 for f in files[:self.maxfiles]:
116 116 yield self.t("filedifflink", node=hex(changeset), file=f)
117 117 if len(files) > self.maxfiles:
118 118 yield self.t("fileellipses")
119 119
120 120 def siblings(self, siblings=[], hiderev=None, **args):
121 121 siblings = [s for s in siblings if s.node() != nullid]
122 122 if len(siblings) == 1 and siblings[0].rev() == hiderev:
123 123 return
124 124 for s in siblings:
125 125 d = {'node': hex(s.node()), 'rev': s.rev()}
126 126 if hasattr(s, 'path'):
127 127 d['file'] = s.path()
128 128 d.update(args)
129 129 yield d
130 130
131 131 def renamelink(self, fl, node):
132 132 r = fl.renamed(node)
133 133 if r:
134 134 return [dict(file=r[0], node=hex(r[1]))]
135 135 return []
136 136
137 137 def nodetagsdict(self, node):
138 138 return [{"name": i} for i in self.repo.nodetags(node)]
139 139
140 140 def nodebranchdict(self, ctx):
141 141 branches = []
142 142 branch = ctx.branch()
143 143 if self.repo.branchtags()[branch] == ctx.node():
144 144 branches.append({"name": branch})
145 145 return branches
146 146
147 147 def showtag(self, t1, node=nullid, **args):
148 148 for t in self.repo.nodetags(node):
149 149 yield self.t(t1, tag=t, **args)
150 150
151 151 def diff(self, node1, node2, files):
152 152 def filterfiles(filters, files):
153 153 l = [x for x in files if x in filters]
154 154
155 155 for t in filters:
156 156 if t and t[-1] != os.sep:
157 157 t += os.sep
158 158 l += [x for x in files if x.startswith(t)]
159 159 return l
160 160
161 161 parity = paritygen(self.stripecount)
162 162 def diffblock(diff, f, fn):
163 163 yield self.t("diffblock",
164 164 lines=prettyprintlines(diff),
165 165 parity=parity.next(),
166 166 file=f,
167 167 filenode=hex(fn or nullid))
168 168
169 169 def prettyprintlines(diff):
170 170 for l in diff.splitlines(1):
171 171 if l.startswith('+'):
172 172 yield self.t("difflineplus", line=l)
173 173 elif l.startswith('-'):
174 174 yield self.t("difflineminus", line=l)
175 175 elif l.startswith('@'):
176 176 yield self.t("difflineat", line=l)
177 177 else:
178 178 yield self.t("diffline", line=l)
179 179
180 180 r = self.repo
181 181 c1 = r.changectx(node1)
182 182 c2 = r.changectx(node2)
183 183 date1 = util.datestr(c1.date())
184 184 date2 = util.datestr(c2.date())
185 185
186 186 modified, added, removed, deleted, unknown = r.status(node1, node2)[:5]
187 187 if files:
188 188 modified, added, removed = map(lambda x: filterfiles(files, x),
189 189 (modified, added, removed))
190 190
191 191 diffopts = patch.diffopts(self.repo.ui, untrusted=True)
192 192 for f in modified:
193 193 to = c1.filectx(f).data()
194 194 tn = c2.filectx(f).data()
195 195 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
196 196 opts=diffopts), f, tn)
197 197 for f in added:
198 198 to = None
199 199 tn = c2.filectx(f).data()
200 200 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
201 201 opts=diffopts), f, tn)
202 202 for f in removed:
203 203 to = c1.filectx(f).data()
204 204 tn = None
205 205 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
206 206 opts=diffopts), f, tn)
207 207
208 208 def changelog(self, ctx, shortlog=False):
209 209 def changelist(**map):
210 210 cl = self.repo.changelog
211 211 l = [] # build a list in forward order for efficiency
212 212 for i in xrange(start, end):
213 213 ctx = self.repo.changectx(i)
214 214 n = ctx.node()
215 215
216 216 l.insert(0, {"parity": parity.next(),
217 217 "author": ctx.user(),
218 218 "parent": self.siblings(ctx.parents(), i - 1),
219 219 "child": self.siblings(ctx.children(), i + 1),
220 220 "changelogtag": self.showtag("changelogtag",n),
221 221 "desc": ctx.description(),
222 222 "date": ctx.date(),
223 223 "files": self.listfilediffs(ctx.files(), n),
224 224 "rev": i,
225 225 "node": hex(n),
226 226 "tags": self.nodetagsdict(n),
227 227 "branches": self.nodebranchdict(ctx)})
228 228
229 229 for e in l:
230 230 yield e
231 231
232 232 maxchanges = shortlog and self.maxshortchanges or self.maxchanges
233 233 cl = self.repo.changelog
234 234 count = cl.count()
235 235 pos = ctx.rev()
236 236 start = max(0, pos - maxchanges + 1)
237 237 end = min(count, start + maxchanges)
238 238 pos = end - 1
239 239 parity = paritygen(self.stripecount, offset=start-end)
240 240
241 241 changenav = revnavgen(pos, maxchanges, count, self.repo.changectx)
242 242
243 243 yield self.t(shortlog and 'shortlog' or 'changelog',
244 244 changenav=changenav,
245 245 node=hex(cl.tip()),
246 246 rev=pos, changesets=count, entries=changelist,
247 247 archives=self.archivelist("tip"))
248 248
249 249 def search(self, query):
250 250
251 251 def changelist(**map):
252 252 cl = self.repo.changelog
253 253 count = 0
254 254 qw = query.lower().split()
255 255
256 256 def revgen():
257 257 for i in xrange(cl.count() - 1, 0, -100):
258 258 l = []
259 259 for j in xrange(max(0, i - 100), i):
260 260 ctx = self.repo.changectx(j)
261 261 l.append(ctx)
262 262 l.reverse()
263 263 for e in l:
264 264 yield e
265 265
266 266 for ctx in revgen():
267 267 miss = 0
268 268 for q in qw:
269 269 if not (q in ctx.user().lower() or
270 270 q in ctx.description().lower() or
271 271 q in " ".join(ctx.files()).lower()):
272 272 miss = 1
273 273 break
274 274 if miss:
275 275 continue
276 276
277 277 count += 1
278 278 n = ctx.node()
279 279
280 280 yield self.t('searchentry',
281 281 parity=parity.next(),
282 282 author=ctx.user(),
283 283 parent=self.siblings(ctx.parents()),
284 284 child=self.siblings(ctx.children()),
285 285 changelogtag=self.showtag("changelogtag",n),
286 286 desc=ctx.description(),
287 287 date=ctx.date(),
288 288 files=self.listfilediffs(ctx.files(), n),
289 289 rev=ctx.rev(),
290 290 node=hex(n),
291 291 tags=self.nodetagsdict(n),
292 292 branches=self.nodebranchdict(ctx))
293 293
294 294 if count >= self.maxchanges:
295 295 break
296 296
297 297 cl = self.repo.changelog
298 298 parity = paritygen(self.stripecount)
299 299
300 300 yield self.t('search',
301 301 query=query,
302 302 node=hex(cl.tip()),
303 303 entries=changelist,
304 304 archives=self.archivelist("tip"))
305 305
306 306 def changeset(self, ctx):
307 307 n = ctx.node()
308 308 parents = ctx.parents()
309 309 p1 = parents[0].node()
310 310
311 311 files = []
312 312 parity = paritygen(self.stripecount)
313 313 for f in ctx.files():
314 314 files.append(self.t("filenodelink",
315 315 node=hex(n), file=f,
316 316 parity=parity.next()))
317 317
318 318 def diff(**map):
319 319 yield self.diff(p1, n, None)
320 320
321 321 yield self.t('changeset',
322 322 diff=diff,
323 323 rev=ctx.rev(),
324 324 node=hex(n),
325 325 parent=self.siblings(parents),
326 326 child=self.siblings(ctx.children()),
327 327 changesettag=self.showtag("changesettag",n),
328 328 author=ctx.user(),
329 329 desc=ctx.description(),
330 330 date=ctx.date(),
331 331 files=files,
332 332 archives=self.archivelist(hex(n)),
333 333 tags=self.nodetagsdict(n),
334 334 branches=self.nodebranchdict(ctx))
335 335
336 336 def filelog(self, fctx):
337 337 f = fctx.path()
338 338 fl = fctx.filelog()
339 339 count = fl.count()
340 340 pagelen = self.maxshortchanges
341 341 pos = fctx.filerev()
342 342 start = max(0, pos - pagelen + 1)
343 343 end = min(count, start + pagelen)
344 344 pos = end - 1
345 345 parity = paritygen(self.stripecount, offset=start-end)
346 346
347 347 def entries(**map):
348 348 l = []
349 349
350 350 for i in xrange(start, end):
351 351 ctx = fctx.filectx(i)
352 352 n = fl.node(i)
353 353
354 354 l.insert(0, {"parity": parity.next(),
355 355 "filerev": i,
356 356 "file": f,
357 357 "node": hex(ctx.node()),
358 358 "author": ctx.user(),
359 359 "date": ctx.date(),
360 360 "rename": self.renamelink(fl, n),
361 361 "parent": self.siblings(fctx.parents()),
362 362 "child": self.siblings(fctx.children()),
363 363 "desc": ctx.description()})
364 364
365 365 for e in l:
366 366 yield e
367 367
368 368 nodefunc = lambda x: fctx.filectx(fileid=x)
369 369 nav = revnavgen(pos, pagelen, count, nodefunc)
370 370 yield self.t("filelog", file=f, node=hex(fctx.node()), nav=nav,
371 371 entries=entries)
372 372
373 373 def filerevision(self, fctx):
374 374 f = fctx.path()
375 375 text = fctx.data()
376 376 fl = fctx.filelog()
377 377 n = fctx.filenode()
378 378 parity = paritygen(self.stripecount)
379 379
380 380 mt = mimetypes.guess_type(f)[0]
381 381 rawtext = text
382 382 if util.binary(text):
383 383 mt = mt or 'application/octet-stream'
384 384 text = "(binary:%s)" % mt
385 385 mt = mt or 'text/plain'
386 386
387 387 def lines():
388 388 for l, t in enumerate(text.splitlines(1)):
389 389 yield {"line": t,
390 390 "linenumber": "% 6d" % (l + 1),
391 391 "parity": parity.next()}
392 392
393 393 yield self.t("filerevision",
394 394 file=f,
395 395 path=_up(f),
396 396 text=lines(),
397 397 raw=rawtext,
398 398 mimetype=mt,
399 399 rev=fctx.rev(),
400 400 node=hex(fctx.node()),
401 401 author=fctx.user(),
402 402 date=fctx.date(),
403 403 desc=fctx.description(),
404 404 parent=self.siblings(fctx.parents()),
405 405 child=self.siblings(fctx.children()),
406 406 rename=self.renamelink(fl, n),
407 407 permissions=fctx.manifest().flags(f))
408 408
409 409 def fileannotate(self, fctx):
410 410 f = fctx.path()
411 411 n = fctx.filenode()
412 412 fl = fctx.filelog()
413 413 parity = paritygen(self.stripecount)
414 414
415 415 def annotate(**map):
416 416 last = None
417 417 for f, l in fctx.annotate(follow=True):
418 418 fnode = f.filenode()
419 419 name = self.repo.ui.shortuser(f.user())
420 420
421 421 if last != fnode:
422 422 last = fnode
423 423
424 424 yield {"parity": parity.next(),
425 425 "node": hex(f.node()),
426 426 "rev": f.rev(),
427 427 "author": name,
428 428 "file": f.path(),
429 429 "line": l}
430 430
431 431 yield self.t("fileannotate",
432 432 file=f,
433 433 annotate=annotate,
434 434 path=_up(f),
435 435 rev=fctx.rev(),
436 436 node=hex(fctx.node()),
437 437 author=fctx.user(),
438 438 date=fctx.date(),
439 439 desc=fctx.description(),
440 440 rename=self.renamelink(fl, n),
441 441 parent=self.siblings(fctx.parents()),
442 442 child=self.siblings(fctx.children()),
443 443 permissions=fctx.manifest().flags(f))
444 444
445 445 def manifest(self, ctx, path):
446 446 mf = ctx.manifest()
447 447 node = ctx.node()
448 448
449 449 files = {}
450 450 parity = paritygen(self.stripecount)
451 451
452 452 if path and path[-1] != "/":
453 453 path += "/"
454 454 l = len(path)
455 455 abspath = "/" + path
456 456
457 457 for f, n in mf.items():
458 458 if f[:l] != path:
459 459 continue
460 460 remain = f[l:]
461 461 if "/" in remain:
462 462 short = remain[:remain.index("/") + 1] # bleah
463 463 files[short] = (f, None)
464 464 else:
465 465 short = os.path.basename(remain)
466 466 files[short] = (f, n)
467 467
468 468 def filelist(**map):
469 469 fl = files.keys()
470 470 fl.sort()
471 471 for f in fl:
472 472 full, fnode = files[f]
473 473 if not fnode:
474 474 continue
475 475
476 476 yield {"file": full,
477 477 "parity": parity.next(),
478 478 "basename": f,
479 479 "size": ctx.filectx(full).size(),
480 480 "permissions": mf.flags(full)}
481 481
482 482 def dirlist(**map):
483 483 fl = files.keys()
484 484 fl.sort()
485 485 for f in fl:
486 486 full, fnode = files[f]
487 487 if fnode:
488 488 continue
489 489
490 490 yield {"parity": parity.next(),
491 491 "path": os.path.join(abspath, f),
492 492 "basename": f[:-1]}
493 493
494 494 yield self.t("manifest",
495 495 rev=ctx.rev(),
496 496 node=hex(node),
497 497 path=abspath,
498 498 up=_up(abspath),
499 499 upparity=parity.next(),
500 500 fentries=filelist,
501 501 dentries=dirlist,
502 502 archives=self.archivelist(hex(node)),
503 503 tags=self.nodetagsdict(node),
504 504 branches=self.nodebranchdict(ctx))
505 505
506 506 def tags(self):
507 507 i = self.repo.tagslist()
508 508 i.reverse()
509 509 parity = paritygen(self.stripecount)
510 510
511 511 def entries(notip=False, **map):
512 512 for k, n in i:
513 513 if notip and k == "tip":
514 514 continue
515 515 yield {"parity": parity.next(),
516 516 "tag": k,
517 517 "date": self.repo.changectx(n).date(),
518 518 "node": hex(n)}
519 519
520 520 yield self.t("tags",
521 521 node=hex(self.repo.changelog.tip()),
522 522 entries=lambda **x: entries(False, **x),
523 523 entriesnotip=lambda **x: entries(True, **x))
524 524
525 525 def summary(self):
526 526 i = self.repo.tagslist()
527 527 i.reverse()
528 528
529 529 def tagentries(**map):
530 530 parity = paritygen(self.stripecount)
531 531 count = 0
532 532 for k, n in i:
533 533 if k == "tip": # skip tip
534 534 continue;
535 535
536 536 count += 1
537 537 if count > 10: # limit to 10 tags
538 538 break;
539 539
540 540 yield self.t("tagentry",
541 541 parity=parity.next(),
542 542 tag=k,
543 543 node=hex(n),
544 544 date=self.repo.changectx(n).date())
545 545
546 546
547 547 def branches(**map):
548 548 parity = paritygen(self.stripecount)
549 549
550 550 b = self.repo.branchtags()
551 551 l = [(-self.repo.changelog.rev(n), n, t) for t, n in b.items()]
552 552 l.sort()
553 553
554 554 for r,n,t in l:
555 555 ctx = self.repo.changectx(n)
556 556
557 557 yield {'parity': parity.next(),
558 558 'branch': t,
559 559 'node': hex(n),
560 560 'date': ctx.date()}
561 561
562 562 def changelist(**map):
563 563 parity = paritygen(self.stripecount, offset=start-end)
564 564 l = [] # build a list in forward order for efficiency
565 565 for i in xrange(start, end):
566 566 ctx = self.repo.changectx(i)
567 567 n = ctx.node()
568 568 hn = hex(n)
569 569
570 570 l.insert(0, self.t(
571 571 'shortlogentry',
572 572 parity=parity.next(),
573 573 author=ctx.user(),
574 574 desc=ctx.description(),
575 575 date=ctx.date(),
576 576 rev=i,
577 577 node=hn,
578 578 tags=self.nodetagsdict(n),
579 579 branches=self.nodebranchdict(ctx)))
580 580
581 581 yield l
582 582
583 583 cl = self.repo.changelog
584 584 count = cl.count()
585 585 start = max(0, count - self.maxchanges)
586 586 end = min(count, start + self.maxchanges)
587 587
588 588 yield self.t("summary",
589 589 desc=self.config("web", "description", "unknown"),
590 590 owner=(self.config("ui", "username") or # preferred
591 591 self.config("web", "contact") or # deprecated
592 592 self.config("web", "author", "unknown")), # also
593 593 lastchange=cl.read(cl.tip())[2],
594 594 tags=tagentries,
595 595 branches=branches,
596 596 shortlog=changelist,
597 597 node=hex(cl.tip()),
598 598 archives=self.archivelist("tip"))
599 599
600 600 def filediff(self, fctx):
601 601 n = fctx.node()
602 602 path = fctx.path()
603 603 parents = fctx.parents()
604 604 p1 = parents and parents[0].node() or nullid
605 605
606 606 def diff(**map):
607 607 yield self.diff(p1, n, [path])
608 608
609 609 yield self.t("filediff",
610 610 file=path,
611 611 node=hex(n),
612 612 rev=fctx.rev(),
613 613 parent=self.siblings(parents),
614 614 child=self.siblings(fctx.children()),
615 615 diff=diff)
616 616
617 617 archive_specs = {
618 618 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', None),
619 619 'gz': ('application/x-tar', 'tgz', '.tar.gz', None),
620 620 'zip': ('application/zip', 'zip', '.zip', None),
621 621 }
622 622
623 623 def archive(self, req, key, type_):
624 624 reponame = re.sub(r"\W+", "-", os.path.basename(self.reponame))
625 625 cnode = self.repo.lookup(key)
626 626 arch_version = key
627 627 if cnode == key or key == 'tip':
628 628 arch_version = short(cnode)
629 629 name = "%s-%s" % (reponame, arch_version)
630 630 mimetype, artype, extension, encoding = self.archive_specs[type_]
631 631 headers = [('Content-type', mimetype),
632 632 ('Content-disposition', 'attachment; filename=%s%s' %
633 633 (name, extension))]
634 634 if encoding:
635 635 headers.append(('Content-encoding', encoding))
636 636 req.header(headers)
637 637 archival.archive(self.repo, req.out, cnode, artype, prefix=name)
638 638
639 639 # add tags to things
640 640 # tags -> list of changesets corresponding to tags
641 641 # find tag, changeset, file
642 642
643 643 def cleanpath(self, path):
644 644 path = path.lstrip('/')
645 645 return util.canonpath(self.repo.root, '', path)
646 646
647 647 def run(self):
648 648 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
649 649 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
650 650 import mercurial.hgweb.wsgicgi as wsgicgi
651 651 from request import wsgiapplication
652 652 def make_web_app():
653 653 return self
654 654 wsgicgi.launch(wsgiapplication(make_web_app))
655 655
656 656 def run_wsgi(self, req):
657 657 def header(**map):
658 658 header_file = cStringIO.StringIO(
659 659 ''.join(self.t("header", encoding=self.encoding, **map)))
660 660 msg = mimetools.Message(header_file, 0)
661 661 req.header(msg.items())
662 662 yield header_file.read()
663 663
664 664 def rawfileheader(**map):
665 665 req.header([('Content-type', map['mimetype']),
666 666 ('Content-disposition', 'filename=%s' % map['file']),
667 667 ('Content-length', str(len(map['raw'])))])
668 668 yield ''
669 669
670 670 def footer(**map):
671 671 yield self.t("footer", **map)
672 672
673 673 def motd(**map):
674 674 yield self.config("web", "motd", "")
675 675
676 676 def expand_form(form):
677 677 shortcuts = {
678 678 'cl': [('cmd', ['changelog']), ('rev', None)],
679 679 'sl': [('cmd', ['shortlog']), ('rev', None)],
680 680 'cs': [('cmd', ['changeset']), ('node', None)],
681 681 'f': [('cmd', ['file']), ('filenode', None)],
682 682 'fl': [('cmd', ['filelog']), ('filenode', None)],
683 683 'fd': [('cmd', ['filediff']), ('node', None)],
684 684 'fa': [('cmd', ['annotate']), ('filenode', None)],
685 685 'mf': [('cmd', ['manifest']), ('manifest', None)],
686 686 'ca': [('cmd', ['archive']), ('node', None)],
687 687 'tags': [('cmd', ['tags'])],
688 688 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
689 689 'static': [('cmd', ['static']), ('file', None)]
690 690 }
691 691
692 692 for k in shortcuts.iterkeys():
693 693 if form.has_key(k):
694 694 for name, value in shortcuts[k]:
695 695 if value is None:
696 696 value = form[k]
697 697 form[name] = value
698 698 del form[k]
699 699
700 700 def rewrite_request(req):
701 701 '''translate new web interface to traditional format'''
702 702
703 703 def spliturl(req):
704 704 def firstitem(query):
705 705 return query.split('&', 1)[0].split(';', 1)[0]
706 706
707 707 def normurl(url):
708 708 inner = '/'.join([x for x in url.split('/') if x])
709 709 tl = len(url) > 1 and url.endswith('/') and '/' or ''
710 710
711 711 return '%s%s%s' % (url.startswith('/') and '/' or '',
712 712 inner, tl)
713 713
714 714 root = normurl(urllib.unquote(req.env.get('REQUEST_URI', '').split('?', 1)[0]))
715 715 pi = normurl(req.env.get('PATH_INFO', ''))
716 716 if pi:
717 717 # strip leading /
718 718 pi = pi[1:]
719 719 if pi:
720 720 root = root[:root.rfind(pi)]
721 721 if req.env.has_key('REPO_NAME'):
722 722 rn = req.env['REPO_NAME'] + '/'
723 723 root += rn
724 724 query = pi[len(rn):]
725 725 else:
726 726 query = pi
727 727 else:
728 728 root += '?'
729 729 query = firstitem(req.env['QUERY_STRING'])
730 730
731 731 return (root, query)
732 732
733 733 req.url, query = spliturl(req)
734 734
735 735 if req.form.has_key('cmd'):
736 736 # old style
737 737 return
738 738
739 739 args = query.split('/', 2)
740 740 if not args or not args[0]:
741 741 return
742 742
743 743 cmd = args.pop(0)
744 744 style = cmd.rfind('-')
745 745 if style != -1:
746 746 req.form['style'] = [cmd[:style]]
747 747 cmd = cmd[style+1:]
748 748 # avoid accepting e.g. style parameter as command
749 749 if hasattr(self, 'do_' + cmd):
750 750 req.form['cmd'] = [cmd]
751 751
752 752 if args and args[0]:
753 753 node = args.pop(0)
754 754 req.form['node'] = [node]
755 755 if args:
756 756 req.form['file'] = args
757 757
758 758 if cmd == 'static':
759 759 req.form['file'] = req.form['node']
760 760 elif cmd == 'archive':
761 761 fn = req.form['node'][0]
762 762 for type_, spec in self.archive_specs.iteritems():
763 763 ext = spec[2]
764 764 if fn.endswith(ext):
765 765 req.form['node'] = [fn[:-len(ext)]]
766 766 req.form['type'] = [type_]
767 767
768 768 def sessionvars(**map):
769 769 fields = []
770 770 if req.form.has_key('style'):
771 771 style = req.form['style'][0]
772 772 if style != self.config('web', 'style', ''):
773 773 fields.append(('style', style))
774 774
775 775 separator = req.url[-1] == '?' and ';' or '?'
776 776 for name, value in fields:
777 777 yield dict(name=name, value=value, separator=separator)
778 778 separator = ';'
779 779
780 780 self.refresh()
781 781
782 782 expand_form(req.form)
783 783 rewrite_request(req)
784 784
785 785 style = self.config("web", "style", "")
786 786 if req.form.has_key('style'):
787 787 style = req.form['style'][0]
788 788 mapfile = style_map(self.templatepath, style)
789 789
790 if req.env.get('HTTPS'):
790 proto = req.env.get('wsgi.url_scheme')
791 if proto == 'https':
791 792 proto = 'https'
792 793 default_port = "443"
793 794 else:
794 795 proto = 'http'
795 796 default_port = "80"
796 797
797 798 port = req.env["SERVER_PORT"]
798 799 port = port != default_port and (":" + port) or ""
799 800 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
800 801 staticurl = self.config("web", "staticurl") or req.url + 'static/'
801 802 if not staticurl.endswith('/'):
802 803 staticurl += '/'
803 804
804 805 if not self.reponame:
805 806 self.reponame = (self.config("web", "name")
806 807 or req.env.get('REPO_NAME')
807 808 or req.url.strip('/') or self.repo.root)
808 809
809 810 self.t = templater.templater(mapfile, templater.common_filters,
810 811 defaults={"url": req.url,
811 812 "staticurl": staticurl,
812 813 "urlbase": urlbase,
813 814 "repo": self.reponame,
814 815 "header": header,
815 816 "footer": footer,
816 817 "motd": motd,
817 818 "rawfileheader": rawfileheader,
818 819 "sessionvars": sessionvars
819 820 })
820 821
821 822 try:
822 823 if not req.form.has_key('cmd'):
823 824 req.form['cmd'] = [self.t.cache['default']]
824 825
825 826 cmd = req.form['cmd'][0]
826 827
827 828 method = getattr(self, 'do_' + cmd, None)
828 829 if method:
829 830 try:
830 831 method(req)
831 832 except (hg.RepoError, revlog.RevlogError), inst:
832 833 req.write(self.t("error", error=str(inst)))
833 834 else:
834 835 req.write(self.t("error", error='No such method: ' + cmd))
835 836 finally:
836 837 self.t = None
837 838
838 839 def changectx(self, req):
839 840 if req.form.has_key('node'):
840 841 changeid = req.form['node'][0]
841 842 elif req.form.has_key('manifest'):
842 843 changeid = req.form['manifest'][0]
843 844 else:
844 845 changeid = self.repo.changelog.count() - 1
845 846
846 847 try:
847 848 ctx = self.repo.changectx(changeid)
848 849 except hg.RepoError:
849 850 man = self.repo.manifest
850 851 mn = man.lookup(changeid)
851 852 ctx = self.repo.changectx(man.linkrev(mn))
852 853
853 854 return ctx
854 855
855 856 def filectx(self, req):
856 857 path = self.cleanpath(req.form['file'][0])
857 858 if req.form.has_key('node'):
858 859 changeid = req.form['node'][0]
859 860 else:
860 861 changeid = req.form['filenode'][0]
861 862 try:
862 863 ctx = self.repo.changectx(changeid)
863 864 fctx = ctx.filectx(path)
864 865 except hg.RepoError:
865 866 fctx = self.repo.filectx(path, fileid=changeid)
866 867
867 868 return fctx
868 869
869 870 def do_log(self, req):
870 871 if req.form.has_key('file') and req.form['file'][0]:
871 872 self.do_filelog(req)
872 873 else:
873 874 self.do_changelog(req)
874 875
875 876 def do_rev(self, req):
876 877 self.do_changeset(req)
877 878
878 879 def do_file(self, req):
879 880 path = self.cleanpath(req.form.get('file', [''])[0])
880 881 if path:
881 882 try:
882 883 req.write(self.filerevision(self.filectx(req)))
883 884 return
884 885 except revlog.LookupError:
885 886 pass
886 887
887 888 req.write(self.manifest(self.changectx(req), path))
888 889
889 890 def do_diff(self, req):
890 891 self.do_filediff(req)
891 892
892 893 def do_changelog(self, req, shortlog = False):
893 894 if req.form.has_key('node'):
894 895 ctx = self.changectx(req)
895 896 else:
896 897 if req.form.has_key('rev'):
897 898 hi = req.form['rev'][0]
898 899 else:
899 900 hi = self.repo.changelog.count() - 1
900 901 try:
901 902 ctx = self.repo.changectx(hi)
902 903 except hg.RepoError:
903 904 req.write(self.search(hi)) # XXX redirect to 404 page?
904 905 return
905 906
906 907 req.write(self.changelog(ctx, shortlog = shortlog))
907 908
908 909 def do_shortlog(self, req):
909 910 self.do_changelog(req, shortlog = True)
910 911
911 912 def do_changeset(self, req):
912 913 req.write(self.changeset(self.changectx(req)))
913 914
914 915 def do_manifest(self, req):
915 916 req.write(self.manifest(self.changectx(req),
916 917 self.cleanpath(req.form['path'][0])))
917 918
918 919 def do_tags(self, req):
919 920 req.write(self.tags())
920 921
921 922 def do_summary(self, req):
922 923 req.write(self.summary())
923 924
924 925 def do_filediff(self, req):
925 926 req.write(self.filediff(self.filectx(req)))
926 927
927 928 def do_annotate(self, req):
928 929 req.write(self.fileannotate(self.filectx(req)))
929 930
930 931 def do_filelog(self, req):
931 932 req.write(self.filelog(self.filectx(req)))
932 933
933 934 def do_lookup(self, req):
934 935 try:
935 936 r = hex(self.repo.lookup(req.form['key'][0]))
936 937 success = 1
937 938 except Exception,inst:
938 939 r = str(inst)
939 940 success = 0
940 941 resp = "%s %s\n" % (success, r)
941 942 req.httphdr("application/mercurial-0.1", length=len(resp))
942 943 req.write(resp)
943 944
944 945 def do_heads(self, req):
945 946 resp = " ".join(map(hex, self.repo.heads())) + "\n"
946 947 req.httphdr("application/mercurial-0.1", length=len(resp))
947 948 req.write(resp)
948 949
949 950 def do_branches(self, req):
950 951 nodes = []
951 952 if req.form.has_key('nodes'):
952 953 nodes = map(bin, req.form['nodes'][0].split(" "))
953 954 resp = cStringIO.StringIO()
954 955 for b in self.repo.branches(nodes):
955 956 resp.write(" ".join(map(hex, b)) + "\n")
956 957 resp = resp.getvalue()
957 958 req.httphdr("application/mercurial-0.1", length=len(resp))
958 959 req.write(resp)
959 960
960 961 def do_between(self, req):
961 962 if req.form.has_key('pairs'):
962 963 pairs = [map(bin, p.split("-"))
963 964 for p in req.form['pairs'][0].split(" ")]
964 965 resp = cStringIO.StringIO()
965 966 for b in self.repo.between(pairs):
966 967 resp.write(" ".join(map(hex, b)) + "\n")
967 968 resp = resp.getvalue()
968 969 req.httphdr("application/mercurial-0.1", length=len(resp))
969 970 req.write(resp)
970 971
971 972 def do_changegroup(self, req):
972 973 req.httphdr("application/mercurial-0.1")
973 974 nodes = []
974 975 if not self.allowpull:
975 976 return
976 977
977 978 if req.form.has_key('roots'):
978 979 nodes = map(bin, req.form['roots'][0].split(" "))
979 980
980 981 z = zlib.compressobj()
981 982 f = self.repo.changegroup(nodes, 'serve')
982 983 while 1:
983 984 chunk = f.read(4096)
984 985 if not chunk:
985 986 break
986 987 req.write(z.compress(chunk))
987 988
988 989 req.write(z.flush())
989 990
990 991 def do_changegroupsubset(self, req):
991 992 req.httphdr("application/mercurial-0.1")
992 993 bases = []
993 994 heads = []
994 995 if not self.allowpull:
995 996 return
996 997
997 998 if req.form.has_key('bases'):
998 999 bases = [bin(x) for x in req.form['bases'][0].split(' ')]
999 1000 if req.form.has_key('heads'):
1000 1001 heads = [bin(x) for x in req.form['heads'][0].split(' ')]
1001 1002
1002 1003 z = zlib.compressobj()
1003 1004 f = self.repo.changegroupsubset(bases, heads, 'serve')
1004 1005 while 1:
1005 1006 chunk = f.read(4096)
1006 1007 if not chunk:
1007 1008 break
1008 1009 req.write(z.compress(chunk))
1009 1010
1010 1011 req.write(z.flush())
1011 1012
1012 1013 def do_archive(self, req):
1013 1014 type_ = req.form['type'][0]
1014 1015 allowed = self.configlist("web", "allow_archive")
1015 1016 if (type_ in self.archives and (type_ in allowed or
1016 1017 self.configbool("web", "allow" + type_, False))):
1017 1018 self.archive(req, req.form['node'][0], type_)
1018 1019 return
1019 1020
1020 1021 req.write(self.t("error"))
1021 1022
1022 1023 def do_static(self, req):
1023 1024 fname = req.form['file'][0]
1024 1025 # a repo owner may set web.static in .hg/hgrc to get any file
1025 1026 # readable by the user running the CGI script
1026 1027 static = self.config("web", "static",
1027 1028 os.path.join(self.templatepath, "static"),
1028 1029 untrusted=False)
1029 1030 req.write(staticfile(static, fname, req)
1030 1031 or self.t("error", error="%r not found" % fname))
1031 1032
1032 1033 def do_capabilities(self, req):
1033 1034 caps = ['lookup', 'changegroupsubset']
1034 1035 if self.configbool('server', 'uncompressed'):
1035 1036 caps.append('stream=%d' % self.repo.changelog.version)
1036 1037 # XXX: make configurable and/or share code with do_unbundle:
1037 1038 unbundleversions = ['HG10GZ', 'HG10BZ', 'HG10UN']
1038 1039 if unbundleversions:
1039 1040 caps.append('unbundle=%s' % ','.join(unbundleversions))
1040 1041 resp = ' '.join(caps)
1041 1042 req.httphdr("application/mercurial-0.1", length=len(resp))
1042 1043 req.write(resp)
1043 1044
1044 1045 def check_perm(self, req, op, default):
1045 1046 '''check permission for operation based on user auth.
1046 1047 return true if op allowed, else false.
1047 1048 default is policy to use if no config given.'''
1048 1049
1049 1050 user = req.env.get('REMOTE_USER')
1050 1051
1051 1052 deny = self.configlist('web', 'deny_' + op)
1052 1053 if deny and (not user or deny == ['*'] or user in deny):
1053 1054 return False
1054 1055
1055 1056 allow = self.configlist('web', 'allow_' + op)
1056 1057 return (allow and (allow == ['*'] or user in allow)) or default
1057 1058
1058 1059 def do_unbundle(self, req):
1059 1060 def bail(response, headers={}):
1060 1061 length = int(req.env['CONTENT_LENGTH'])
1061 1062 for s in util.filechunkiter(req, limit=length):
1062 1063 # drain incoming bundle, else client will not see
1063 1064 # response when run outside cgi script
1064 1065 pass
1065 1066 req.httphdr("application/mercurial-0.1", headers=headers)
1066 1067 req.write('0\n')
1067 1068 req.write(response)
1068 1069
1069 1070 # require ssl by default, auth info cannot be sniffed and
1070 1071 # replayed
1071 1072 ssl_req = self.configbool('web', 'push_ssl', True)
1072 1073 if ssl_req:
1073 if not req.env.get('HTTPS'):
1074 if req.env.get('wsgi.url_scheme') != 'https':
1074 1075 bail(_('ssl required\n'))
1075 1076 return
1076 1077 proto = 'https'
1077 1078 else:
1078 1079 proto = 'http'
1079 1080
1080 1081 # do not allow push unless explicitly allowed
1081 1082 if not self.check_perm(req, 'push', False):
1082 1083 bail(_('push not authorized\n'),
1083 1084 headers={'status': '401 Unauthorized'})
1084 1085 return
1085 1086
1086 1087 their_heads = req.form['heads'][0].split(' ')
1087 1088
1088 1089 def check_heads():
1089 1090 heads = map(hex, self.repo.heads())
1090 1091 return their_heads == [hex('force')] or their_heads == heads
1091 1092
1092 1093 # fail early if possible
1093 1094 if not check_heads():
1094 1095 bail(_('unsynced changes\n'))
1095 1096 return
1096 1097
1097 1098 req.httphdr("application/mercurial-0.1")
1098 1099
1099 1100 # do not lock repo until all changegroup data is
1100 1101 # streamed. save to temporary file.
1101 1102
1102 1103 fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-')
1103 1104 fp = os.fdopen(fd, 'wb+')
1104 1105 try:
1105 1106 length = int(req.env['CONTENT_LENGTH'])
1106 1107 for s in util.filechunkiter(req, limit=length):
1107 1108 fp.write(s)
1108 1109
1109 1110 try:
1110 1111 lock = self.repo.lock()
1111 1112 try:
1112 1113 if not check_heads():
1113 1114 req.write('0\n')
1114 1115 req.write(_('unsynced changes\n'))
1115 1116 return
1116 1117
1117 1118 fp.seek(0)
1118 1119 header = fp.read(6)
1119 1120 if not header.startswith("HG"):
1120 1121 # old client with uncompressed bundle
1121 1122 def generator(f):
1122 1123 yield header
1123 1124 for chunk in f:
1124 1125 yield chunk
1125 1126 elif not header.startswith("HG10"):
1126 1127 req.write("0\n")
1127 1128 req.write(_("unknown bundle version\n"))
1128 1129 return
1129 1130 elif header == "HG10GZ":
1130 1131 def generator(f):
1131 1132 zd = zlib.decompressobj()
1132 1133 for chunk in f:
1133 1134 yield zd.decompress(chunk)
1134 1135 elif header == "HG10BZ":
1135 1136 def generator(f):
1136 1137 zd = bz2.BZ2Decompressor()
1137 1138 zd.decompress("BZ")
1138 1139 for chunk in f:
1139 1140 yield zd.decompress(chunk)
1140 1141 elif header == "HG10UN":
1141 1142 def generator(f):
1142 1143 for chunk in f:
1143 1144 yield chunk
1144 1145 else:
1145 1146 req.write("0\n")
1146 1147 req.write(_("unknown bundle compression type\n"))
1147 1148 return
1148 1149 gen = generator(util.filechunkiter(fp, 4096))
1149 1150
1150 1151 # send addchangegroup output to client
1151 1152
1152 1153 old_stdout = sys.stdout
1153 1154 sys.stdout = cStringIO.StringIO()
1154 1155
1155 1156 try:
1156 1157 url = 'remote:%s:%s' % (proto,
1157 1158 req.env.get('REMOTE_HOST', ''))
1158 1159 try:
1159 1160 ret = self.repo.addchangegroup(
1160 1161 util.chunkbuffer(gen), 'serve', url)
1161 1162 except util.Abort, inst:
1162 1163 sys.stdout.write("abort: %s\n" % inst)
1163 1164 ret = 0
1164 1165 finally:
1165 1166 val = sys.stdout.getvalue()
1166 1167 sys.stdout = old_stdout
1167 1168 req.write('%d\n' % ret)
1168 1169 req.write(val)
1169 1170 finally:
1170 1171 lock.release()
1171 1172 except (OSError, IOError), inst:
1172 1173 req.write('0\n')
1173 1174 filename = getattr(inst, 'filename', '')
1174 1175 # Don't send our filesystem layout to the client
1175 1176 if filename.startswith(self.repo.root):
1176 1177 filename = filename[len(self.repo.root)+1:]
1177 1178 else:
1178 1179 filename = ''
1179 1180 error = getattr(inst, 'strerror', 'Unknown error')
1180 1181 req.write('%s: %s\n' % (error, filename))
1181 1182 finally:
1182 1183 fp.close()
1183 1184 os.unlink(tempname)
1184 1185
1185 1186 def do_stream_out(self, req):
1186 1187 req.httphdr("application/mercurial-0.1")
1187 1188 streamclone.stream_out(self.repo, req, untrusted=True)
General Comments 0
You need to be logged in to leave comments. Login now