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