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