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