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