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