##// END OF EJS Templates
hgweb: support for generating and parsing NWI URLs
Brendan Cully -
r3263:3207e30b default
parent child Browse files
Show More
@@ -1,1003 +1,1035
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, 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
10 10 import os.path
11 11 import mimetypes
12 12 from mercurial.demandload import demandload
13 13 demandload(globals(), "re zlib ConfigParser mimetools cStringIO sys tempfile")
14 14 demandload(globals(), "mercurial:mdiff,ui,hg,util,archival,streamclone,patch")
15 15 demandload(globals(), "mercurial:templater")
16 16 demandload(globals(), "mercurial.hgweb.common:get_mtime,staticfile")
17 17 from mercurial.node import *
18 18 from mercurial.i18n import gettext as _
19 19
20 20 def _up(p):
21 21 if p[0] != "/":
22 22 p = "/" + p
23 23 if p[-1] == "/":
24 24 p = p[:-1]
25 25 up = os.path.dirname(p)
26 26 if up == "/":
27 27 return "/"
28 28 return up + "/"
29 29
30 30 class hgweb(object):
31 31 def __init__(self, repo, name=None):
32 32 if type(repo) == type(""):
33 33 self.repo = hg.repository(ui.ui(), repo)
34 34 else:
35 35 self.repo = repo
36 36
37 37 self.mtime = -1
38 38 self.reponame = name
39 39 self.archives = 'zip', 'gz', 'bz2'
40 40 self.stripecount = 1
41 41 self.templatepath = self.repo.ui.config("web", "templates",
42 42 templater.templatepath())
43 43
44 44 def refresh(self):
45 45 mtime = get_mtime(self.repo.root)
46 46 if mtime != self.mtime:
47 47 self.mtime = mtime
48 48 self.repo = hg.repository(self.repo.ui, self.repo.root)
49 49 self.maxchanges = int(self.repo.ui.config("web", "maxchanges", 10))
50 50 self.stripecount = int(self.repo.ui.config("web", "stripes", 1))
51 51 self.maxshortchanges = int(self.repo.ui.config("web", "maxshortchanges", 60))
52 52 self.maxfiles = int(self.repo.ui.config("web", "maxfiles", 10))
53 53 self.allowpull = self.repo.ui.configbool("web", "allowpull", True)
54 54
55 55 def archivelist(self, nodeid):
56 56 allowed = self.repo.ui.configlist("web", "allow_archive")
57 57 for i, spec in self.archive_specs.iteritems():
58 58 if i in allowed or self.repo.ui.configbool("web", "allow" + i):
59 59 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
60 60
61 61 def listfiles(self, files, mf):
62 62 for f in files[:self.maxfiles]:
63 63 yield self.t("filenodelink", node=hex(mf[f]), file=f)
64 64 if len(files) > self.maxfiles:
65 65 yield self.t("fileellipses")
66 66
67 67 def listfilediffs(self, files, changeset):
68 68 for f in files[:self.maxfiles]:
69 69 yield self.t("filedifflink", node=hex(changeset), file=f)
70 70 if len(files) > self.maxfiles:
71 71 yield self.t("fileellipses")
72 72
73 73 def siblings(self, siblings=[], hiderev=None, **args):
74 74 siblings = [s for s in siblings if s.node() != nullid]
75 75 if len(siblings) == 1 and siblings[0].rev() == hiderev:
76 76 return
77 77 for s in siblings:
78 78 yield dict(node=hex(s.node()), rev=s.rev(), **args)
79 79
80 80 def renamelink(self, fl, node):
81 81 r = fl.renamed(node)
82 82 if r:
83 83 return [dict(file=r[0], node=hex(r[1]))]
84 84 return []
85 85
86 86 def showtag(self, t1, node=nullid, **args):
87 87 for t in self.repo.nodetags(node):
88 88 yield self.t(t1, tag=t, **args)
89 89
90 90 def diff(self, node1, node2, files):
91 91 def filterfiles(filters, files):
92 92 l = [x for x in files if x in filters]
93 93
94 94 for t in filters:
95 95 if t and t[-1] != os.sep:
96 96 t += os.sep
97 97 l += [x for x in files if x.startswith(t)]
98 98 return l
99 99
100 100 parity = [0]
101 101 def diffblock(diff, f, fn):
102 102 yield self.t("diffblock",
103 103 lines=prettyprintlines(diff),
104 104 parity=parity[0],
105 105 file=f,
106 106 filenode=hex(fn or nullid))
107 107 parity[0] = 1 - parity[0]
108 108
109 109 def prettyprintlines(diff):
110 110 for l in diff.splitlines(1):
111 111 if l.startswith('+'):
112 112 yield self.t("difflineplus", line=l)
113 113 elif l.startswith('-'):
114 114 yield self.t("difflineminus", line=l)
115 115 elif l.startswith('@'):
116 116 yield self.t("difflineat", line=l)
117 117 else:
118 118 yield self.t("diffline", line=l)
119 119
120 120 r = self.repo
121 121 cl = r.changelog
122 122 mf = r.manifest
123 123 change1 = cl.read(node1)
124 124 change2 = cl.read(node2)
125 125 mmap1 = mf.read(change1[0])
126 126 mmap2 = mf.read(change2[0])
127 127 date1 = util.datestr(change1[2])
128 128 date2 = util.datestr(change2[2])
129 129
130 130 modified, added, removed, deleted, unknown = r.status(node1, node2)[:5]
131 131 if files:
132 132 modified, added, removed = map(lambda x: filterfiles(files, x),
133 133 (modified, added, removed))
134 134
135 135 diffopts = patch.diffopts(self.repo.ui)
136 136 for f in modified:
137 137 to = r.file(f).read(mmap1[f])
138 138 tn = r.file(f).read(mmap2[f])
139 139 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
140 140 opts=diffopts), f, tn)
141 141 for f in added:
142 142 to = None
143 143 tn = r.file(f).read(mmap2[f])
144 144 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
145 145 opts=diffopts), f, tn)
146 146 for f in removed:
147 147 to = r.file(f).read(mmap1[f])
148 148 tn = None
149 149 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
150 150 opts=diffopts), f, tn)
151 151
152 152 def changelog(self, ctx, shortlog=False):
153 153 pos = ctx.rev()
154 154 def changenav(**map):
155 155 def seq(factor, maxchanges=None):
156 156 if maxchanges:
157 157 yield maxchanges
158 158 if maxchanges >= 20 and maxchanges <= 40:
159 159 yield 50
160 160 else:
161 161 yield 1 * factor
162 162 yield 3 * factor
163 163 for f in seq(factor * 10):
164 164 yield f
165 165
166 166 l = []
167 167 last = 0
168 168 maxchanges = shortlog and self.maxshortchanges or self.maxchanges
169 169 for f in seq(1, maxchanges):
170 170 if f < maxchanges or f <= last:
171 171 continue
172 172 if f > count:
173 173 break
174 174 last = f
175 175 r = "%d" % f
176 176 if pos + f < count:
177 177 l.append(("+" + r, pos + f))
178 178 if pos - f >= 0:
179 179 l.insert(0, ("-" + r, pos - f))
180 180
181 181 yield {"rev": 0, "label": "(0)"}
182 182
183 183 for label, rev in l:
184 184 yield {"label": label, "rev": rev}
185 185
186 186 yield {"label": "tip", "rev": "tip"}
187 187
188 188 def changelist(**map):
189 189 parity = (start - end) & 1
190 190 cl = self.repo.changelog
191 191 l = [] # build a list in forward order for efficiency
192 192 for i in range(start, end):
193 193 ctx = self.repo.changectx(i)
194 194 n = ctx.node()
195 195
196 196 l.insert(0, {"parity": parity,
197 197 "author": ctx.user(),
198 198 "parent": self.siblings(ctx.parents(), i - 1),
199 199 "child": self.siblings(ctx.children(), i + 1),
200 200 "changelogtag": self.showtag("changelogtag",n),
201 201 "desc": ctx.description(),
202 202 "date": ctx.date(),
203 203 "files": self.listfilediffs(ctx.files(), n),
204 204 "rev": i,
205 205 "node": hex(n)})
206 206 parity = 1 - parity
207 207
208 208 for e in l:
209 209 yield e
210 210
211 211 maxchanges = shortlog and self.maxshortchanges or self.maxchanges
212 212 cl = self.repo.changelog
213 213 count = cl.count()
214 214 start = max(0, pos - maxchanges + 1)
215 215 end = min(count, start + maxchanges)
216 216 pos = end - 1
217 217
218 218 yield self.t(shortlog and 'shortlog' or 'changelog',
219 219 changenav=changenav,
220 220 node=hex(cl.tip()),
221 221 rev=pos, changesets=count, entries=changelist,
222 222 archives=self.archivelist("tip"))
223 223
224 224 def search(self, query):
225 225
226 226 def changelist(**map):
227 227 cl = self.repo.changelog
228 228 count = 0
229 229 qw = query.lower().split()
230 230
231 231 def revgen():
232 232 for i in range(cl.count() - 1, 0, -100):
233 233 l = []
234 234 for j in range(max(0, i - 100), i):
235 235 ctx = self.repo.changectx(j)
236 236 l.append(ctx)
237 237 l.reverse()
238 238 for e in l:
239 239 yield e
240 240
241 241 for ctx in revgen():
242 242 miss = 0
243 243 for q in qw:
244 244 if not (q in ctx.user().lower() or
245 245 q in ctx.description().lower() or
246 246 q in " ".join(ctx.files()[:20]).lower()):
247 247 miss = 1
248 248 break
249 249 if miss:
250 250 continue
251 251
252 252 count += 1
253 253 n = ctx.node()
254 254
255 255 yield self.t('searchentry',
256 256 parity=self.stripes(count),
257 257 author=ctx.user(),
258 258 parent=self.siblings(ctx.parents()),
259 259 child=self.siblings(ctx.children()),
260 260 changelogtag=self.showtag("changelogtag",n),
261 261 desc=ctx.description(),
262 262 date=ctx.date(),
263 263 files=self.listfilediffs(ctx.files(), n),
264 264 rev=ctx.rev(),
265 265 node=hex(n))
266 266
267 267 if count >= self.maxchanges:
268 268 break
269 269
270 270 cl = self.repo.changelog
271 271
272 272 yield self.t('search',
273 273 query=query,
274 274 node=hex(cl.tip()),
275 275 entries=changelist)
276 276
277 277 def changeset(self, ctx):
278 278 n = ctx.node()
279 279 parents = ctx.parents()
280 280 p1 = parents[0].node()
281 281
282 282 files = []
283 283 parity = 0
284 284 for f in ctx.files():
285 285 files.append(self.t("filenodelink",
286 286 node=hex(n), file=f,
287 287 parity=parity))
288 288 parity = 1 - parity
289 289
290 290 def diff(**map):
291 291 yield self.diff(p1, n, None)
292 292
293 293 yield self.t('changeset',
294 294 diff=diff,
295 295 rev=ctx.rev(),
296 296 node=hex(n),
297 297 parent=self.siblings(parents),
298 298 child=self.siblings(ctx.children()),
299 299 changesettag=self.showtag("changesettag",n),
300 300 author=ctx.user(),
301 301 desc=ctx.description(),
302 302 date=ctx.date(),
303 303 files=files,
304 304 archives=self.archivelist(hex(n)))
305 305
306 306 def filelog(self, fctx):
307 307 f = fctx.path()
308 308 fl = fctx.filelog()
309 309 count = fl.count()
310 310
311 311 def entries(**map):
312 312 l = []
313 313 parity = (count - 1) & 1
314 314
315 315 for i in range(count):
316 316 ctx = fctx.filectx(i)
317 317 n = fl.node(i)
318 318
319 319 l.insert(0, {"parity": parity,
320 320 "filerev": i,
321 321 "file": f,
322 322 "node": hex(ctx.node()),
323 323 "author": ctx.user(),
324 324 "date": ctx.date(),
325 325 "rename": self.renamelink(fl, n),
326 326 "parent": self.siblings(fctx.parents(), file=f),
327 327 "child": self.siblings(fctx.children(), file=f),
328 328 "desc": ctx.description()})
329 329 parity = 1 - parity
330 330
331 331 for e in l:
332 332 yield e
333 333
334 334 yield self.t("filelog", file=f, node=hex(fctx.node()), entries=entries)
335 335
336 336 def filerevision(self, fctx):
337 337 f = fctx.path()
338 338 text = fctx.data()
339 339 fl = fctx.filelog()
340 340 n = fctx.filenode()
341 341
342 342 mt = mimetypes.guess_type(f)[0]
343 343 rawtext = text
344 344 if util.binary(text):
345 345 mt = mt or 'application/octet-stream'
346 346 text = "(binary:%s)" % mt
347 347 mt = mt or 'text/plain'
348 348
349 349 def lines():
350 350 for l, t in enumerate(text.splitlines(1)):
351 351 yield {"line": t,
352 352 "linenumber": "% 6d" % (l + 1),
353 353 "parity": self.stripes(l)}
354 354
355 355 yield self.t("filerevision",
356 356 file=f,
357 357 path=_up(f),
358 358 text=lines(),
359 359 raw=rawtext,
360 360 mimetype=mt,
361 361 rev=fctx.rev(),
362 362 node=hex(fctx.node()),
363 363 author=fctx.user(),
364 364 date=fctx.date(),
365 365 parent=self.siblings(fctx.parents(), file=f),
366 366 child=self.siblings(fctx.children(), file=f),
367 367 rename=self.renamelink(fl, n),
368 368 permissions=fctx.manifest().execf(f))
369 369
370 370 def fileannotate(self, fctx):
371 371 f = fctx.path()
372 372 n = fctx.filenode()
373 373 fl = fctx.filelog()
374 374
375 375 def annotate(**map):
376 376 parity = 0
377 377 last = None
378 378 for f, l in fctx.annotate(follow=True):
379 379 fnode = f.filenode()
380 380 name = self.repo.ui.shortuser(f.user())
381 381
382 382 if last != fnode:
383 383 parity = 1 - parity
384 384 last = fnode
385 385
386 386 yield {"parity": parity,
387 387 "node": hex(f.node()),
388 388 "rev": f.rev(),
389 389 "author": name,
390 390 "file": f.path(),
391 391 "line": l}
392 392
393 393 yield self.t("fileannotate",
394 394 file=f,
395 395 annotate=annotate,
396 396 path=_up(f),
397 397 rev=fctx.rev(),
398 398 node=hex(fctx.node()),
399 399 author=fctx.user(),
400 400 date=fctx.date(),
401 401 rename=self.renamelink(fl, n),
402 402 parent=self.siblings(fctx.parents(), file=f),
403 403 child=self.siblings(fctx.children(), file=f),
404 404 permissions=fctx.manifest().execf(f))
405 405
406 406 def manifest(self, ctx, path):
407 407 mf = ctx.manifest()
408 408 node = ctx.node()
409 409
410 410 files = {}
411 411
412 412 p = path[1:]
413 413 if p and p[-1] != "/":
414 414 p += "/"
415 415 l = len(p)
416 416
417 417 for f,n in mf.items():
418 418 if f[:l] != p:
419 419 continue
420 420 remain = f[l:]
421 421 if "/" in remain:
422 422 short = remain[:remain.index("/") + 1] # bleah
423 423 files[short] = (f, None)
424 424 else:
425 425 short = os.path.basename(remain)
426 426 files[short] = (f, n)
427 427
428 428 def filelist(**map):
429 429 parity = 0
430 430 fl = files.keys()
431 431 fl.sort()
432 432 for f in fl:
433 433 full, fnode = files[f]
434 434 if not fnode:
435 435 continue
436 436
437 437 yield {"file": full,
438 438 "filenode": hex(fnode),
439 439 "parity": self.stripes(parity),
440 440 "basename": f,
441 441 "permissions": mf.execf(full)}
442 442 parity += 1
443 443
444 444 def dirlist(**map):
445 445 parity = 0
446 446 fl = files.keys()
447 447 fl.sort()
448 448 for f in fl:
449 449 full, fnode = files[f]
450 450 if fnode:
451 451 continue
452 452
453 453 yield {"parity": self.stripes(parity),
454 454 "path": os.path.join(path, f),
455 455 "basename": f[:-1]}
456 456 parity += 1
457 457
458 458 yield self.t("manifest",
459 459 rev=ctx.rev(),
460 460 node=hex(node),
461 461 path=path,
462 462 up=_up(path),
463 463 fentries=filelist,
464 464 dentries=dirlist,
465 465 archives=self.archivelist(hex(node)))
466 466
467 467 def tags(self):
468 468 cl = self.repo.changelog
469 469
470 470 i = self.repo.tagslist()
471 471 i.reverse()
472 472
473 473 def entries(notip=False, **map):
474 474 parity = 0
475 475 for k,n in i:
476 476 if notip and k == "tip": continue
477 477 yield {"parity": self.stripes(parity),
478 478 "tag": k,
479 479 "date": cl.read(n)[2],
480 480 "node": hex(n)}
481 481 parity += 1
482 482
483 483 yield self.t("tags",
484 484 node=hex(self.repo.changelog.tip()),
485 485 entries=lambda **x: entries(False, **x),
486 486 entriesnotip=lambda **x: entries(True, **x))
487 487
488 488 def summary(self):
489 489 cl = self.repo.changelog
490 490
491 491 i = self.repo.tagslist()
492 492 i.reverse()
493 493
494 494 def tagentries(**map):
495 495 parity = 0
496 496 count = 0
497 497 for k,n in i:
498 498 if k == "tip": # skip tip
499 499 continue;
500 500
501 501 count += 1
502 502 if count > 10: # limit to 10 tags
503 503 break;
504 504
505 505 c = cl.read(n)
506 506 t = c[2]
507 507
508 508 yield self.t("tagentry",
509 509 parity = self.stripes(parity),
510 510 tag = k,
511 511 node = hex(n),
512 512 date = t)
513 513 parity += 1
514 514
515 515 def changelist(**map):
516 516 parity = 0
517 517 cl = self.repo.changelog
518 518 l = [] # build a list in forward order for efficiency
519 519 for i in range(start, end):
520 520 n = cl.node(i)
521 521 changes = cl.read(n)
522 522 hn = hex(n)
523 523 t = changes[2]
524 524
525 525 l.insert(0, self.t(
526 526 'shortlogentry',
527 527 parity = parity,
528 528 author = changes[1],
529 529 desc = changes[4],
530 530 date = t,
531 531 rev = i,
532 532 node = hn))
533 533 parity = 1 - parity
534 534
535 535 yield l
536 536
537 537 count = cl.count()
538 538 start = max(0, count - self.maxchanges)
539 539 end = min(count, start + self.maxchanges)
540 540
541 541 yield self.t("summary",
542 542 desc = self.repo.ui.config("web", "description", "unknown"),
543 543 owner = (self.repo.ui.config("ui", "username") or # preferred
544 544 self.repo.ui.config("web", "contact") or # deprecated
545 545 self.repo.ui.config("web", "author", "unknown")), # also
546 546 lastchange = (0, 0), # FIXME
547 547 tags = tagentries,
548 548 shortlog = changelist,
549 549 node = hex(self.repo.changelog.tip()),
550 550 archives=self.archivelist("tip"))
551 551
552 552 def filediff(self, fctx):
553 553 n = fctx.node()
554 554 path = fctx.path()
555 555 parents = fctx.changectx().parents()
556 556 p1 = parents[0].node()
557 557
558 558 def diff(**map):
559 559 yield self.diff(p1, n, [path])
560 560
561 561 yield self.t("filediff",
562 562 file=path,
563 563 node=hex(n),
564 564 rev=fctx.rev(),
565 565 parent=self.siblings(parents),
566 566 child=self.siblings(fctx.children()),
567 567 diff=diff)
568 568
569 569 archive_specs = {
570 570 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', None),
571 571 'gz': ('application/x-tar', 'tgz', '.tar.gz', None),
572 572 'zip': ('application/zip', 'zip', '.zip', None),
573 573 }
574 574
575 575 def archive(self, req, cnode, type_):
576 576 reponame = re.sub(r"\W+", "-", os.path.basename(self.reponame))
577 577 name = "%s-%s" % (reponame, short(cnode))
578 578 mimetype, artype, extension, encoding = self.archive_specs[type_]
579 579 headers = [('Content-type', mimetype),
580 580 ('Content-disposition', 'attachment; filename=%s%s' %
581 581 (name, extension))]
582 582 if encoding:
583 583 headers.append(('Content-encoding', encoding))
584 584 req.header(headers)
585 585 archival.archive(self.repo, req.out, cnode, artype, prefix=name)
586 586
587 587 # add tags to things
588 588 # tags -> list of changesets corresponding to tags
589 589 # find tag, changeset, file
590 590
591 591 def cleanpath(self, path):
592 592 p = util.normpath(path)
593 593 if p[:2] == "..":
594 594 raise Exception("suspicious path")
595 595 return p
596 596
597 597 def run(self):
598 598 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
599 599 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
600 600 import mercurial.hgweb.wsgicgi as wsgicgi
601 601 from request import wsgiapplication
602 602 def make_web_app():
603 603 return self
604 604 wsgicgi.launch(wsgiapplication(make_web_app))
605 605
606 606 def run_wsgi(self, req):
607 607 def header(**map):
608 608 header_file = cStringIO.StringIO(''.join(self.t("header", **map)))
609 609 msg = mimetools.Message(header_file, 0)
610 610 req.header(msg.items())
611 611 yield header_file.read()
612 612
613 613 def rawfileheader(**map):
614 614 req.header([('Content-type', map['mimetype']),
615 615 ('Content-disposition', 'filename=%s' % map['file']),
616 616 ('Content-length', str(len(map['raw'])))])
617 617 yield ''
618 618
619 619 def footer(**map):
620 620 yield self.t("footer",
621 621 motd=self.repo.ui.config("web", "motd", ""),
622 622 **map)
623 623
624 624 def expand_form(form):
625 625 shortcuts = {
626 626 'cl': [('cmd', ['changelog']), ('rev', None)],
627 627 'sl': [('cmd', ['shortlog']), ('rev', None)],
628 628 'cs': [('cmd', ['changeset']), ('node', None)],
629 629 'f': [('cmd', ['file']), ('filenode', None)],
630 630 'fl': [('cmd', ['filelog']), ('filenode', None)],
631 631 'fd': [('cmd', ['filediff']), ('node', None)],
632 632 'fa': [('cmd', ['annotate']), ('filenode', None)],
633 633 'mf': [('cmd', ['manifest']), ('manifest', None)],
634 634 'ca': [('cmd', ['archive']), ('node', None)],
635 635 'tags': [('cmd', ['tags'])],
636 636 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
637 637 'static': [('cmd', ['static']), ('file', None)]
638 638 }
639 639
640 640 for k in shortcuts.iterkeys():
641 641 if form.has_key(k):
642 642 for name, value in shortcuts[k]:
643 643 if value is None:
644 644 value = form[k]
645 645 form[name] = value
646 646 del form[k]
647 647
648 648 def rewrite_request(req):
649 649 '''translate new web interface to traditional format'''
650 650
651 def spliturl(req):
652 def firstitem(query):
653 return query.split('&', 1)[0].split(';', 1)[0]
654
655 base = ''
656 if req.env.has_key('REPO_NAME'):
657 base = '/' + req.env['REPO_NAME']
658 elif req.env.get('SCRIPT_NAME'):
659 base = req.env['SCRIPT_NAME']
660
661 pi = req.env.get('PATH_INFO')
662 if pi:
663 if pi.startswith(base):
664 if len(pi) > len(base):
665 base += '/'
666 query = pi[len(base):]
667 else:
668 if req.env.has_key('REPO_NAME'):
669 # We are using hgwebdir
670 base += '/'
671 else:
672 base += '?'
673 query = firstitem(req.env['QUERY_STRING'])
674 else:
675 base += '/'
676 query = pi[1:]
677 else:
678 base += '?'
679 query = firstitem(req.env['QUERY_STRING'])
680
681 return (base, query)
682
683 req.url, query = spliturl(req)
684
651 685 if req.form.has_key('cmd'):
652 686 # old style
653 687 return
654 if req.env.has_key('SCRIPT_URL'):
655 # run through web server
656 base = req.env['SCRIPT_URL']
657 # strip repo and leading '/' or '?'
658 query = req.env['REQUEST_URI'][len(base)+1:]
659 else:
660 query = req.env['PATH_INFO'][1:]
661 688
662 689 args = query.split('/', 2)
663 690 if not args or not args[0]:
664 691 return
665 692
666 693 cmd = args.pop(0)
667 694 style = cmd.rfind('-')
668 695 if style != -1:
669 696 req.form['style'] = [cmd[:style]]
670 697 cmd = cmd[style+1:]
671 req.form['cmd'] = [cmd]
698 # avoid accepting e.g. style parameter as command
699 if hasattr(self, 'do_' + cmd):
700 req.form['cmd'] = [cmd]
672 701
673 702 if args and args[0]:
674 703 node = args.pop(0)
675 704 req.form['node'] = [node]
676 705 if args:
677 706 req.form['file'] = args
678 707
679 708 if cmd == 'static':
680 709 req.form['file'] = req.form['node']
681 710 elif cmd == 'archive':
682 711 fn = req.form['node'][0]
683 712 for type_, spec in self.archive_specs.iteritems():
684 713 ext = spec[2]
685 714 if fn.endswith(ext):
686 715 req.form['node'] = [fn[:-len(ext)]]
687 716 req.form['type'] = [type_]
688 717
689 718 self.refresh()
690 719
691 720 expand_form(req.form)
692 721 rewrite_request(req)
693 722
694 723 m = os.path.join(self.templatepath, "map")
695 724 style = self.repo.ui.config("web", "style", "")
696 725 if req.form.has_key('style'):
697 726 style = req.form['style'][0]
698 727 if style:
699 728 b = os.path.basename("map-" + style)
700 729 p = os.path.join(self.templatepath, b)
701 730 if os.path.isfile(p):
702 731 m = p
703 732
704 port = req.env["SERVER_PORT"]
705 port = port != "80" and (":" + port) or ""
706 uri = req.env["REQUEST_URI"]
707 if "?" in uri:
708 uri = uri.split("?")[0]
709 url = "http://%s%s%s" % (req.env["SERVER_NAME"], port, uri)
733 if not req.url:
734 port = req.env["SERVER_PORT"]
735 port = port != "80" and (":" + port) or ""
736 uri = req.env["REQUEST_URI"]
737 if "?" in uri:
738 uri = uri.split("?")[0]
739 req.url = "http://%s%s%s" % (req.env["SERVER_NAME"], port, uri)
740
710 741 if not self.reponame:
711 742 self.reponame = (self.repo.ui.config("web", "name")
712 or uri.strip('/') or self.repo.root)
743 or req.env.get('REPO_NAME')
744 or req.url.strip('/') or self.repo.root)
713 745
714 746 self.t = templater.templater(m, templater.common_filters,
715 defaults={"url": url,
747 defaults={"url": req.url,
716 748 "repo": self.reponame,
717 749 "header": header,
718 750 "footer": footer,
719 751 "rawfileheader": rawfileheader,
720 752 })
721 753
722 754 if not req.form.has_key('cmd'):
723 755 req.form['cmd'] = [self.t.cache['default'],]
724 756
725 757 cmd = req.form['cmd'][0]
726 758
727 759 method = getattr(self, 'do_' + cmd, None)
728 760 if method:
729 761 method(req)
730 762 else:
731 763 req.write(self.t("error"))
732 764
733 765 def changectx(self, req):
734 766 if req.form.has_key('node'):
735 767 changeid = req.form['node'][0]
736 768 else:
737 769 changeid = req.form['manifest'][0]
738 770 try:
739 771 ctx = self.repo.changectx(changeid)
740 772 except hg.RepoError:
741 773 man = self.repo.manifest
742 774 mn = man.lookup(changeid)
743 775 ctx = self.repo.changectx(man.linkrev(mn))
744 776
745 777 return ctx
746 778
747 779 def filectx(self, req):
748 780 path = self.cleanpath(req.form['file'][0])
749 781 if req.form.has_key('node'):
750 782 changeid = req.form['node'][0]
751 783 else:
752 784 changeid = req.form['filenode'][0]
753 785 try:
754 786 ctx = self.repo.changectx(changeid)
755 787 fctx = ctx.filectx(path)
756 788 except hg.RepoError:
757 789 fctx = self.repo.filectx(path, fileid=changeid)
758 790
759 791 return fctx
760 792
761 793 def stripes(self, parity):
762 794 "make horizontal stripes for easier reading"
763 795 if self.stripecount:
764 796 return (1 + parity / self.stripecount) & 1
765 797 else:
766 798 return 0
767 799
768 800 def do_log(self, req):
769 801 if req.form.has_key('file') and req.form['file'][0]:
770 802 self.do_filelog(req)
771 803 else:
772 804 self.do_changelog(req)
773 805
774 806 def do_rev(self, req):
775 807 self.do_changeset(req)
776 808
777 809 def do_file(self, req):
778 810 path = req.form.get('file', [''])[0]
779 811 if path:
780 812 try:
781 813 req.write(self.filerevision(self.filectx(req)))
782 814 return
783 815 except hg.RepoError:
784 816 pass
785 817 path = self.cleanpath(path)
786 818
787 819 req.write(self.manifest(self.changectx(req), '/' + path))
788 820
789 821 def do_diff(self, req):
790 822 self.do_filediff(req)
791 823
792 824 def do_changelog(self, req, shortlog = False):
793 825 if req.form.has_key('node'):
794 826 ctx = self.changectx(req)
795 827 else:
796 828 if req.form.has_key('rev'):
797 829 hi = req.form['rev'][0]
798 830 else:
799 831 hi = self.repo.changelog.count() - 1
800 832 try:
801 833 ctx = self.repo.changectx(hi)
802 834 except hg.RepoError:
803 835 req.write(self.search(hi)) # XXX redirect to 404 page?
804 836 return
805 837
806 838 req.write(self.changelog(ctx, shortlog = shortlog))
807 839
808 840 def do_shortlog(self, req):
809 841 self.do_changelog(req, shortlog = True)
810 842
811 843 def do_changeset(self, req):
812 844 ctx = self.repo.changectx(req.form['node'][0])
813 845 req.write(self.changeset(ctx))
814 846
815 847 def do_manifest(self, req):
816 848 req.write(self.manifest(self.changectx(req),
817 849 self.cleanpath(req.form['path'][0])))
818 850
819 851 def do_tags(self, req):
820 852 req.write(self.tags())
821 853
822 854 def do_summary(self, req):
823 855 req.write(self.summary())
824 856
825 857 def do_filediff(self, req):
826 858 req.write(self.filediff(self.filectx(req)))
827 859
828 860 def do_annotate(self, req):
829 861 req.write(self.fileannotate(self.filectx(req)))
830 862
831 863 def do_filelog(self, req):
832 864 req.write(self.filelog(self.filectx(req)))
833 865
834 866 def do_heads(self, req):
835 867 resp = " ".join(map(hex, self.repo.heads())) + "\n"
836 868 req.httphdr("application/mercurial-0.1", length=len(resp))
837 869 req.write(resp)
838 870
839 871 def do_branches(self, req):
840 872 nodes = []
841 873 if req.form.has_key('nodes'):
842 874 nodes = map(bin, req.form['nodes'][0].split(" "))
843 875 resp = cStringIO.StringIO()
844 876 for b in self.repo.branches(nodes):
845 877 resp.write(" ".join(map(hex, b)) + "\n")
846 878 resp = resp.getvalue()
847 879 req.httphdr("application/mercurial-0.1", length=len(resp))
848 880 req.write(resp)
849 881
850 882 def do_between(self, req):
851 883 if req.form.has_key('pairs'):
852 884 pairs = [map(bin, p.split("-"))
853 885 for p in req.form['pairs'][0].split(" ")]
854 886 resp = cStringIO.StringIO()
855 887 for b in self.repo.between(pairs):
856 888 resp.write(" ".join(map(hex, b)) + "\n")
857 889 resp = resp.getvalue()
858 890 req.httphdr("application/mercurial-0.1", length=len(resp))
859 891 req.write(resp)
860 892
861 893 def do_changegroup(self, req):
862 894 req.httphdr("application/mercurial-0.1")
863 895 nodes = []
864 896 if not self.allowpull:
865 897 return
866 898
867 899 if req.form.has_key('roots'):
868 900 nodes = map(bin, req.form['roots'][0].split(" "))
869 901
870 902 z = zlib.compressobj()
871 903 f = self.repo.changegroup(nodes, 'serve')
872 904 while 1:
873 905 chunk = f.read(4096)
874 906 if not chunk:
875 907 break
876 908 req.write(z.compress(chunk))
877 909
878 910 req.write(z.flush())
879 911
880 912 def do_archive(self, req):
881 913 changeset = self.repo.lookup(req.form['node'][0])
882 914 type_ = req.form['type'][0]
883 915 allowed = self.repo.ui.configlist("web", "allow_archive")
884 916 if (type_ in self.archives and (type_ in allowed or
885 917 self.repo.ui.configbool("web", "allow" + type_, False))):
886 918 self.archive(req, changeset, type_)
887 919 return
888 920
889 921 req.write(self.t("error"))
890 922
891 923 def do_static(self, req):
892 924 fname = req.form['file'][0]
893 925 static = self.repo.ui.config("web", "static",
894 926 os.path.join(self.templatepath,
895 927 "static"))
896 928 req.write(staticfile(static, fname, req)
897 929 or self.t("error", error="%r not found" % fname))
898 930
899 931 def do_capabilities(self, req):
900 932 caps = ['unbundle']
901 933 if self.repo.ui.configbool('server', 'uncompressed'):
902 934 caps.append('stream=%d' % self.repo.revlogversion)
903 935 resp = ' '.join(caps)
904 936 req.httphdr("application/mercurial-0.1", length=len(resp))
905 937 req.write(resp)
906 938
907 939 def check_perm(self, req, op, default):
908 940 '''check permission for operation based on user auth.
909 941 return true if op allowed, else false.
910 942 default is policy to use if no config given.'''
911 943
912 944 user = req.env.get('REMOTE_USER')
913 945
914 946 deny = self.repo.ui.configlist('web', 'deny_' + op)
915 947 if deny and (not user or deny == ['*'] or user in deny):
916 948 return False
917 949
918 950 allow = self.repo.ui.configlist('web', 'allow_' + op)
919 951 return (allow and (allow == ['*'] or user in allow)) or default
920 952
921 953 def do_unbundle(self, req):
922 954 def bail(response, headers={}):
923 955 length = int(req.env['CONTENT_LENGTH'])
924 956 for s in util.filechunkiter(req, limit=length):
925 957 # drain incoming bundle, else client will not see
926 958 # response when run outside cgi script
927 959 pass
928 960 req.httphdr("application/mercurial-0.1", headers=headers)
929 961 req.write('0\n')
930 962 req.write(response)
931 963
932 964 # require ssl by default, auth info cannot be sniffed and
933 965 # replayed
934 966 ssl_req = self.repo.ui.configbool('web', 'push_ssl', True)
935 967 if ssl_req:
936 968 if not req.env.get('HTTPS'):
937 969 bail(_('ssl required\n'))
938 970 return
939 971 proto = 'https'
940 972 else:
941 973 proto = 'http'
942 974
943 975 # do not allow push unless explicitly allowed
944 976 if not self.check_perm(req, 'push', False):
945 977 bail(_('push not authorized\n'),
946 978 headers={'status': '401 Unauthorized'})
947 979 return
948 980
949 981 req.httphdr("application/mercurial-0.1")
950 982
951 983 their_heads = req.form['heads'][0].split(' ')
952 984
953 985 def check_heads():
954 986 heads = map(hex, self.repo.heads())
955 987 return their_heads == [hex('force')] or their_heads == heads
956 988
957 989 # fail early if possible
958 990 if not check_heads():
959 991 bail(_('unsynced changes\n'))
960 992 return
961 993
962 994 # do not lock repo until all changegroup data is
963 995 # streamed. save to temporary file.
964 996
965 997 fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-')
966 998 fp = os.fdopen(fd, 'wb+')
967 999 try:
968 1000 length = int(req.env['CONTENT_LENGTH'])
969 1001 for s in util.filechunkiter(req, limit=length):
970 1002 fp.write(s)
971 1003
972 1004 lock = self.repo.lock()
973 1005 try:
974 1006 if not check_heads():
975 1007 req.write('0\n')
976 1008 req.write(_('unsynced changes\n'))
977 1009 return
978 1010
979 1011 fp.seek(0)
980 1012
981 1013 # send addchangegroup output to client
982 1014
983 1015 old_stdout = sys.stdout
984 1016 sys.stdout = cStringIO.StringIO()
985 1017
986 1018 try:
987 1019 url = 'remote:%s:%s' % (proto,
988 1020 req.env.get('REMOTE_HOST', ''))
989 1021 ret = self.repo.addchangegroup(fp, 'serve', url)
990 1022 finally:
991 1023 val = sys.stdout.getvalue()
992 1024 sys.stdout = old_stdout
993 1025 req.write('%d\n' % ret)
994 1026 req.write(val)
995 1027 finally:
996 1028 lock.release()
997 1029 finally:
998 1030 fp.close()
999 1031 os.unlink(tempname)
1000 1032
1001 1033 def do_stream_out(self, req):
1002 1034 req.httphdr("application/mercurial-0.1")
1003 1035 streamclone.stream_out(self.repo, req)
@@ -1,188 +1,193
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
10 10 from mercurial.demandload import demandload
11 11 demandload(globals(), "ConfigParser mimetools cStringIO")
12 12 demandload(globals(), "mercurial:ui,hg,util,templater")
13 13 demandload(globals(), "mercurial.hgweb.hgweb_mod:hgweb")
14 14 demandload(globals(), "mercurial.hgweb.common:get_mtime,staticfile")
15 15 from mercurial.i18n import gettext as _
16 16
17 17 # This is a stopgap
18 18 class hgwebdir(object):
19 19 def __init__(self, config):
20 20 def cleannames(items):
21 21 return [(name.strip(os.sep), path) for name, path in items]
22 22
23 23 self.motd = ""
24 24 self.style = ""
25 25 self.repos_sorted = ('name', False)
26 26 if isinstance(config, (list, tuple)):
27 27 self.repos = cleannames(config)
28 28 self.repos_sorted = ('', False)
29 29 elif isinstance(config, dict):
30 30 self.repos = cleannames(config.items())
31 31 self.repos.sort()
32 32 else:
33 33 cp = ConfigParser.SafeConfigParser()
34 34 cp.read(config)
35 35 self.repos = []
36 36 if cp.has_section('web'):
37 37 if cp.has_option('web', 'motd'):
38 38 self.motd = cp.get('web', 'motd')
39 39 if cp.has_option('web', 'style'):
40 40 self.style = cp.get('web', 'style')
41 41 if cp.has_section('paths'):
42 42 self.repos.extend(cleannames(cp.items('paths')))
43 43 if cp.has_section('collections'):
44 44 for prefix, root in cp.items('collections'):
45 45 for path in util.walkrepos(root):
46 46 repo = os.path.normpath(path)
47 47 name = repo
48 48 if name.startswith(prefix):
49 49 name = name[len(prefix):]
50 50 self.repos.append((name.lstrip(os.sep), repo))
51 51 self.repos.sort()
52 52
53 53 def run(self):
54 54 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
55 55 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
56 56 import mercurial.hgweb.wsgicgi as wsgicgi
57 57 from request import wsgiapplication
58 58 def make_web_app():
59 59 return self
60 60 wsgicgi.launch(wsgiapplication(make_web_app))
61 61
62 62 def run_wsgi(self, req):
63 63 def header(**map):
64 64 header_file = cStringIO.StringIO(''.join(tmpl("header", **map)))
65 65 msg = mimetools.Message(header_file, 0)
66 66 req.header(msg.items())
67 67 yield header_file.read()
68 68
69 69 def footer(**map):
70 70 yield tmpl("footer", motd=self.motd, **map)
71 71
72 72 m = os.path.join(templater.templatepath(), "map")
73 73 style = self.style
74 74 if req.form.has_key('style'):
75 75 style = req.form['style'][0]
76 76 if style != "":
77 77 b = os.path.basename("map-" + style)
78 78 p = os.path.join(templater.templatepath(), b)
79 79 if os.path.isfile(p):
80 80 m = p
81 81
82 82 tmpl = templater.templater(m, templater.common_filters,
83 83 defaults={"header": header,
84 84 "footer": footer})
85 85
86 86 def archivelist(ui, nodeid, url):
87 87 allowed = ui.configlist("web", "allow_archive")
88 88 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
89 89 if i[0] in allowed or ui.configbool("web", "allow" + i[0]):
90 90 yield {"type" : i[0], "extension": i[1],
91 91 "node": nodeid, "url": url}
92 92
93 93 def entries(sortcolumn="", descending=False, **map):
94 94 rows = []
95 95 parity = 0
96 96 for name, path in self.repos:
97 97 u = ui.ui()
98 98 try:
99 99 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
100 100 except IOError:
101 101 pass
102 102 get = u.config
103 103
104 104 url = ('/'.join([req.env["REQUEST_URI"].split('?')[0], name])
105 105 .replace("//", "/")) + '/'
106 106
107 107 # update time with local timezone
108 108 try:
109 109 d = (get_mtime(path), util.makedate()[1])
110 110 except OSError:
111 111 continue
112 112
113 113 contact = (get("ui", "username") or # preferred
114 114 get("web", "contact") or # deprecated
115 115 get("web", "author", "")) # also
116 116 description = get("web", "description", "")
117 117 name = get("web", "name", name)
118 118 row = dict(contact=contact or "unknown",
119 119 contact_sort=contact.upper() or "unknown",
120 120 name=name,
121 121 name_sort=name,
122 122 url=url,
123 123 description=description or "unknown",
124 124 description_sort=description.upper() or "unknown",
125 125 lastchange=d,
126 126 lastchange_sort=d[1]-d[0],
127 127 archives=archivelist(u, "tip", url))
128 128 if (not sortcolumn
129 129 or (sortcolumn, descending) == self.repos_sorted):
130 130 # fast path for unsorted output
131 131 row['parity'] = parity
132 132 parity = 1 - parity
133 133 yield row
134 134 else:
135 135 rows.append((row["%s_sort" % sortcolumn], row))
136 136 if rows:
137 137 rows.sort()
138 138 if descending:
139 139 rows.reverse()
140 140 for key, row in rows:
141 141 row['parity'] = parity
142 142 parity = 1 - parity
143 143 yield row
144 144
145 145 virtual = req.env.get("PATH_INFO", "").strip('/')
146 if virtual:
146 if virtual.startswith('static/'):
147 static = os.path.join(templater.templatepath(), 'static')
148 fname = virtual[7:]
149 req.write(staticfile(static, fname, req) or
150 tmpl('error', error='%r not found' % fname))
151 elif virtual:
147 152 while virtual:
148 153 real = dict(self.repos).get(virtual)
149 154 if real:
150 155 break
151 156 up = virtual.rfind('/')
152 157 if up < 0:
153 158 break
154 159 virtual = virtual[:up]
155 160 if real:
156 161 req.env['REPO_NAME'] = virtual
157 162 try:
158 163 hgweb(real).run_wsgi(req)
159 164 except IOError, inst:
160 165 req.write(tmpl("error", error=inst.strerror))
161 166 except hg.RepoError, inst:
162 167 req.write(tmpl("error", error=str(inst)))
163 168 else:
164 169 req.write(tmpl("notfound", repo=virtual))
165 170 else:
166 171 if req.form.has_key('static'):
167 172 static = os.path.join(templater.templatepath(), "static")
168 173 fname = req.form['static'][0]
169 174 req.write(staticfile(static, fname, req)
170 175 or tmpl("error", error="%r not found" % fname))
171 176 else:
172 177 sortable = ["name", "description", "contact", "lastchange"]
173 178 sortcolumn, descending = self.repos_sorted
174 179 if req.form.has_key('sort'):
175 180 sortcolumn = req.form['sort'][0]
176 181 descending = sortcolumn.startswith('-')
177 182 if descending:
178 183 sortcolumn = sortcolumn[1:]
179 184 if sortcolumn not in sortable:
180 185 sortcolumn = ""
181 186
182 187 sort = [("sort_%s" % column,
183 188 "%s%s" % ((not descending and column == sortcolumn)
184 189 and "-" or "", column))
185 190 for column in sortable]
186 191 req.write(tmpl("index", entries=entries,
187 192 sortcolumn=sortcolumn, descending=descending,
188 193 **dict(sort)))
@@ -1,225 +1,225
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, 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 from mercurial.demandload import demandload
10 10 import os, sys, errno
11 11 demandload(globals(), "urllib BaseHTTPServer socket SocketServer")
12 12 demandload(globals(), "mercurial:ui,hg,util,templater")
13 13 demandload(globals(), "hgweb_mod:hgweb hgwebdir_mod:hgwebdir request:wsgiapplication")
14 14 from mercurial.i18n import gettext as _
15 15
16 16 def _splitURI(uri):
17 17 """ Return path and query splited from uri
18 18
19 19 Just like CGI environment, the path is unquoted, the query is
20 20 not.
21 21 """
22 22 if '?' in uri:
23 23 path, query = uri.split('?', 1)
24 24 else:
25 25 path, query = uri, ''
26 26 return urllib.unquote(path), query
27 27
28 28 class _error_logger(object):
29 29 def __init__(self, handler):
30 30 self.handler = handler
31 31 def flush(self):
32 32 pass
33 33 def write(self, str):
34 34 self.writelines(str.split('\n'))
35 35 def writelines(self, seq):
36 36 for msg in seq:
37 37 self.handler.log_error("HG error: %s", msg)
38 38
39 39 class _hgwebhandler(object, BaseHTTPServer.BaseHTTPRequestHandler):
40 40 def __init__(self, *args, **kargs):
41 41 self.protocol_version = 'HTTP/1.1'
42 42 BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, *args, **kargs)
43 43
44 44 def log_error(self, format, *args):
45 45 errorlog = self.server.errorlog
46 46 errorlog.write("%s - - [%s] %s\n" % (self.address_string(),
47 47 self.log_date_time_string(),
48 48 format % args))
49 49
50 50 def log_message(self, format, *args):
51 51 accesslog = self.server.accesslog
52 52 accesslog.write("%s - - [%s] %s\n" % (self.address_string(),
53 53 self.log_date_time_string(),
54 54 format % args))
55 55
56 56 def do_POST(self):
57 57 try:
58 58 self.do_hgweb()
59 59 except socket.error, inst:
60 60 if inst[0] != errno.EPIPE:
61 61 raise
62 62
63 63 def do_GET(self):
64 64 self.do_POST()
65 65
66 66 def do_hgweb(self):
67 67 path_info, query = _splitURI(self.path)
68 68
69 69 env = {}
70 70 env['GATEWAY_INTERFACE'] = 'CGI/1.1'
71 71 env['REQUEST_METHOD'] = self.command
72 72 env['SERVER_NAME'] = self.server.server_name
73 73 env['SERVER_PORT'] = str(self.server.server_port)
74 env['REQUEST_URI'] = "/"
74 env['REQUEST_URI'] = self.path
75 75 env['PATH_INFO'] = path_info
76 76 if query:
77 77 env['QUERY_STRING'] = query
78 78 host = self.address_string()
79 79 if host != self.client_address[0]:
80 80 env['REMOTE_HOST'] = host
81 81 env['REMOTE_ADDR'] = self.client_address[0]
82 82
83 83 if self.headers.typeheader is None:
84 84 env['CONTENT_TYPE'] = self.headers.type
85 85 else:
86 86 env['CONTENT_TYPE'] = self.headers.typeheader
87 87 length = self.headers.getheader('content-length')
88 88 if length:
89 89 env['CONTENT_LENGTH'] = length
90 90 for header in [h for h in self.headers.keys() \
91 91 if h not in ('content-type', 'content-length')]:
92 92 hkey = 'HTTP_' + header.replace('-', '_').upper()
93 93 hval = self.headers.getheader(header)
94 94 hval = hval.replace('\n', '').strip()
95 95 if hval:
96 96 env[hkey] = hval
97 97 env['SERVER_PROTOCOL'] = self.request_version
98 98 env['wsgi.version'] = (1, 0)
99 99 env['wsgi.url_scheme'] = 'http'
100 100 env['wsgi.input'] = self.rfile
101 101 env['wsgi.errors'] = _error_logger(self)
102 102 env['wsgi.multithread'] = isinstance(self.server,
103 103 SocketServer.ThreadingMixIn)
104 104 env['wsgi.multiprocess'] = isinstance(self.server,
105 105 SocketServer.ForkingMixIn)
106 106 env['wsgi.run_once'] = 0
107 107
108 108 self.close_connection = True
109 109 self.saved_status = None
110 110 self.saved_headers = []
111 111 self.sent_headers = False
112 112 self.length = None
113 113 req = self.server.reqmaker(env, self._start_response)
114 114 for data in req:
115 115 if data:
116 116 self._write(data)
117 117
118 118 def send_headers(self):
119 119 if not self.saved_status:
120 120 raise AssertionError("Sending headers before start_response() called")
121 121 saved_status = self.saved_status.split(None, 1)
122 122 saved_status[0] = int(saved_status[0])
123 123 self.send_response(*saved_status)
124 124 should_close = True
125 125 for h in self.saved_headers:
126 126 self.send_header(*h)
127 127 if h[0].lower() == 'content-length':
128 128 should_close = False
129 129 self.length = int(h[1])
130 130 # The value of the Connection header is a list of case-insensitive
131 131 # tokens separated by commas and optional whitespace.
132 132 if 'close' in [token.strip().lower() for token in
133 133 self.headers.get('connection', '').split(',')]:
134 134 should_close = True
135 135 if should_close:
136 136 self.send_header('Connection', 'close')
137 137 self.close_connection = should_close
138 138 self.end_headers()
139 139 self.sent_headers = True
140 140
141 141 def _start_response(self, http_status, headers, exc_info=None):
142 142 code, msg = http_status.split(None, 1)
143 143 code = int(code)
144 144 self.saved_status = http_status
145 145 bad_headers = ('connection', 'transfer-encoding')
146 146 self.saved_headers = [ h for h in headers \
147 147 if h[0].lower() not in bad_headers ]
148 148 return self._write
149 149
150 150 def _write(self, data):
151 151 if not self.saved_status:
152 152 raise AssertionError("data written before start_response() called")
153 153 elif not self.sent_headers:
154 154 self.send_headers()
155 155 if self.length is not None:
156 156 if len(data) > self.length:
157 157 raise AssertionError("Content-length header sent, but more bytes than specified are being written.")
158 158 self.length = self.length - len(data)
159 159 self.wfile.write(data)
160 160 self.wfile.flush()
161 161
162 162 def create_server(ui, repo):
163 163 use_threads = True
164 164
165 165 def openlog(opt, default):
166 166 if opt and opt != '-':
167 167 return open(opt, 'w')
168 168 return default
169 169
170 170 address = ui.config("web", "address", "")
171 171 port = int(ui.config("web", "port", 8000))
172 172 use_ipv6 = ui.configbool("web", "ipv6")
173 173 webdir_conf = ui.config("web", "webdir_conf")
174 174 accesslog = openlog(ui.config("web", "accesslog", "-"), sys.stdout)
175 175 errorlog = openlog(ui.config("web", "errorlog", "-"), sys.stderr)
176 176
177 177 if use_threads:
178 178 try:
179 179 from threading import activeCount
180 180 except ImportError:
181 181 use_threads = False
182 182
183 183 if use_threads:
184 184 _mixin = SocketServer.ThreadingMixIn
185 185 else:
186 186 if hasattr(os, "fork"):
187 187 _mixin = SocketServer.ForkingMixIn
188 188 else:
189 189 class _mixin: pass
190 190
191 191 class MercurialHTTPServer(object, _mixin, BaseHTTPServer.HTTPServer):
192 192 def __init__(self, *args, **kargs):
193 193 BaseHTTPServer.HTTPServer.__init__(self, *args, **kargs)
194 194 self.accesslog = accesslog
195 195 self.errorlog = errorlog
196 196 self.repo = repo
197 197 self.webdir_conf = webdir_conf
198 198 self.webdirmaker = hgwebdir
199 199 self.repoviewmaker = hgweb
200 200 self.reqmaker = wsgiapplication(self.make_handler)
201 201 self.daemon_threads = True
202 202
203 203 def make_handler(self):
204 204 if self.webdir_conf:
205 205 hgwebobj = self.webdirmaker(self.webdir_conf)
206 206 elif self.repo is not None:
207 207 hgwebobj = self.repoviewmaker(repo.__class__(repo.ui,
208 208 repo.origroot))
209 209 else:
210 210 raise hg.RepoError(_("There is no Mercurial repository here"
211 211 " (.hg not found)"))
212 212 return hgwebobj
213 213
214 214 class IPv6HTTPServer(MercurialHTTPServer):
215 215 address_family = getattr(socket, 'AF_INET6', None)
216 216
217 217 def __init__(self, *args, **kwargs):
218 218 if self.address_family is None:
219 219 raise hg.RepoError(_('IPv6 not available on this system'))
220 220 super(IPv6HTTPServer, self).__init__(*args, **kwargs)
221 221
222 222 if use_ipv6:
223 223 return IPv6HTTPServer((address, port), _hgwebhandler)
224 224 else:
225 225 return MercurialHTTPServer((address, port), _hgwebhandler)
General Comments 0
You need to be logged in to leave comments. Login now