##// END OF EJS Templates
hgweb: drop useless **args from webutil.showtag() and showbookmark()...
Yuya Nishihara -
r37929:ec03f3aa default
parent child Browse files
Show More
@@ -1,739 +1,735 b''
1 # hgweb/webutil.py - utility library for the web interface.
1 # hgweb/webutil.py - utility library for the web interface.
2 #
2 #
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import copy
11 import copy
12 import difflib
12 import difflib
13 import os
13 import os
14 import re
14 import re
15
15
16 from ..i18n import _
16 from ..i18n import _
17 from ..node import hex, nullid, short
17 from ..node import hex, nullid, short
18
18
19 from .common import (
19 from .common import (
20 ErrorResponse,
20 ErrorResponse,
21 HTTP_BAD_REQUEST,
21 HTTP_BAD_REQUEST,
22 HTTP_NOT_FOUND,
22 HTTP_NOT_FOUND,
23 paritygen,
23 paritygen,
24 )
24 )
25
25
26 from .. import (
26 from .. import (
27 context,
27 context,
28 error,
28 error,
29 match,
29 match,
30 mdiff,
30 mdiff,
31 obsutil,
31 obsutil,
32 patch,
32 patch,
33 pathutil,
33 pathutil,
34 pycompat,
34 pycompat,
35 scmutil,
35 scmutil,
36 templatefilters,
36 templatefilters,
37 templatekw,
37 templatekw,
38 templateutil,
38 templateutil,
39 ui as uimod,
39 ui as uimod,
40 util,
40 util,
41 )
41 )
42
42
43 from ..utils import (
43 from ..utils import (
44 stringutil,
44 stringutil,
45 )
45 )
46
46
47 archivespecs = util.sortdict((
47 archivespecs = util.sortdict((
48 ('zip', ('application/zip', 'zip', '.zip', None)),
48 ('zip', ('application/zip', 'zip', '.zip', None)),
49 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
49 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
50 ('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
50 ('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
51 ))
51 ))
52
52
53 def archivelist(ui, nodeid, url=None):
53 def archivelist(ui, nodeid, url=None):
54 allowed = ui.configlist('web', 'allow_archive', untrusted=True)
54 allowed = ui.configlist('web', 'allow_archive', untrusted=True)
55 archives = []
55 archives = []
56
56
57 for typ, spec in archivespecs.iteritems():
57 for typ, spec in archivespecs.iteritems():
58 if typ in allowed or ui.configbool('web', 'allow' + typ,
58 if typ in allowed or ui.configbool('web', 'allow' + typ,
59 untrusted=True):
59 untrusted=True):
60 archives.append({
60 archives.append({
61 'type': typ,
61 'type': typ,
62 'extension': spec[2],
62 'extension': spec[2],
63 'node': nodeid,
63 'node': nodeid,
64 'url': url,
64 'url': url,
65 })
65 })
66
66
67 return templateutil.mappinglist(archives)
67 return templateutil.mappinglist(archives)
68
68
69 def up(p):
69 def up(p):
70 if p[0:1] != "/":
70 if p[0:1] != "/":
71 p = "/" + p
71 p = "/" + p
72 if p[-1:] == "/":
72 if p[-1:] == "/":
73 p = p[:-1]
73 p = p[:-1]
74 up = os.path.dirname(p)
74 up = os.path.dirname(p)
75 if up == "/":
75 if up == "/":
76 return "/"
76 return "/"
77 return up + "/"
77 return up + "/"
78
78
79 def _navseq(step, firststep=None):
79 def _navseq(step, firststep=None):
80 if firststep:
80 if firststep:
81 yield firststep
81 yield firststep
82 if firststep >= 20 and firststep <= 40:
82 if firststep >= 20 and firststep <= 40:
83 firststep = 50
83 firststep = 50
84 yield firststep
84 yield firststep
85 assert step > 0
85 assert step > 0
86 assert firststep > 0
86 assert firststep > 0
87 while step <= firststep:
87 while step <= firststep:
88 step *= 10
88 step *= 10
89 while True:
89 while True:
90 yield 1 * step
90 yield 1 * step
91 yield 3 * step
91 yield 3 * step
92 step *= 10
92 step *= 10
93
93
94 class revnav(object):
94 class revnav(object):
95
95
96 def __init__(self, repo):
96 def __init__(self, repo):
97 """Navigation generation object
97 """Navigation generation object
98
98
99 :repo: repo object we generate nav for
99 :repo: repo object we generate nav for
100 """
100 """
101 # used for hex generation
101 # used for hex generation
102 self._revlog = repo.changelog
102 self._revlog = repo.changelog
103
103
104 def __nonzero__(self):
104 def __nonzero__(self):
105 """return True if any revision to navigate over"""
105 """return True if any revision to navigate over"""
106 return self._first() is not None
106 return self._first() is not None
107
107
108 __bool__ = __nonzero__
108 __bool__ = __nonzero__
109
109
110 def _first(self):
110 def _first(self):
111 """return the minimum non-filtered changeset or None"""
111 """return the minimum non-filtered changeset or None"""
112 try:
112 try:
113 return next(iter(self._revlog))
113 return next(iter(self._revlog))
114 except StopIteration:
114 except StopIteration:
115 return None
115 return None
116
116
117 def hex(self, rev):
117 def hex(self, rev):
118 return hex(self._revlog.node(rev))
118 return hex(self._revlog.node(rev))
119
119
120 def gen(self, pos, pagelen, limit):
120 def gen(self, pos, pagelen, limit):
121 """computes label and revision id for navigation link
121 """computes label and revision id for navigation link
122
122
123 :pos: is the revision relative to which we generate navigation.
123 :pos: is the revision relative to which we generate navigation.
124 :pagelen: the size of each navigation page
124 :pagelen: the size of each navigation page
125 :limit: how far shall we link
125 :limit: how far shall we link
126
126
127 The return is:
127 The return is:
128 - a single element mappinglist
128 - a single element mappinglist
129 - containing a dictionary with a `before` and `after` key
129 - containing a dictionary with a `before` and `after` key
130 - values are dictionaries with `label` and `node` keys
130 - values are dictionaries with `label` and `node` keys
131 """
131 """
132 if not self:
132 if not self:
133 # empty repo
133 # empty repo
134 return templateutil.mappinglist([
134 return templateutil.mappinglist([
135 {'before': templateutil.mappinglist([]),
135 {'before': templateutil.mappinglist([]),
136 'after': templateutil.mappinglist([])},
136 'after': templateutil.mappinglist([])},
137 ])
137 ])
138
138
139 targets = []
139 targets = []
140 for f in _navseq(1, pagelen):
140 for f in _navseq(1, pagelen):
141 if f > limit:
141 if f > limit:
142 break
142 break
143 targets.append(pos + f)
143 targets.append(pos + f)
144 targets.append(pos - f)
144 targets.append(pos - f)
145 targets.sort()
145 targets.sort()
146
146
147 first = self._first()
147 first = self._first()
148 navbefore = [{'label': '(%i)' % first, 'node': self.hex(first)}]
148 navbefore = [{'label': '(%i)' % first, 'node': self.hex(first)}]
149 navafter = []
149 navafter = []
150 for rev in targets:
150 for rev in targets:
151 if rev not in self._revlog:
151 if rev not in self._revlog:
152 continue
152 continue
153 if pos < rev < limit:
153 if pos < rev < limit:
154 navafter.append({'label': '+%d' % abs(rev - pos),
154 navafter.append({'label': '+%d' % abs(rev - pos),
155 'node': self.hex(rev)})
155 'node': self.hex(rev)})
156 if 0 < rev < pos:
156 if 0 < rev < pos:
157 navbefore.append({'label': '-%d' % abs(rev - pos),
157 navbefore.append({'label': '-%d' % abs(rev - pos),
158 'node': self.hex(rev)})
158 'node': self.hex(rev)})
159
159
160 navafter.append({'label': 'tip', 'node': 'tip'})
160 navafter.append({'label': 'tip', 'node': 'tip'})
161
161
162 # TODO: maybe this can be a scalar object supporting tomap()
162 # TODO: maybe this can be a scalar object supporting tomap()
163 return templateutil.mappinglist([
163 return templateutil.mappinglist([
164 {'before': templateutil.mappinglist(navbefore),
164 {'before': templateutil.mappinglist(navbefore),
165 'after': templateutil.mappinglist(navafter)},
165 'after': templateutil.mappinglist(navafter)},
166 ])
166 ])
167
167
168 class filerevnav(revnav):
168 class filerevnav(revnav):
169
169
170 def __init__(self, repo, path):
170 def __init__(self, repo, path):
171 """Navigation generation object
171 """Navigation generation object
172
172
173 :repo: repo object we generate nav for
173 :repo: repo object we generate nav for
174 :path: path of the file we generate nav for
174 :path: path of the file we generate nav for
175 """
175 """
176 # used for iteration
176 # used for iteration
177 self._changelog = repo.unfiltered().changelog
177 self._changelog = repo.unfiltered().changelog
178 # used for hex generation
178 # used for hex generation
179 self._revlog = repo.file(path)
179 self._revlog = repo.file(path)
180
180
181 def hex(self, rev):
181 def hex(self, rev):
182 return hex(self._changelog.node(self._revlog.linkrev(rev)))
182 return hex(self._changelog.node(self._revlog.linkrev(rev)))
183
183
184 # TODO: maybe this can be a wrapper class for changectx/filectx list, which
184 # TODO: maybe this can be a wrapper class for changectx/filectx list, which
185 # yields {'ctx': ctx}
185 # yields {'ctx': ctx}
186 def _ctxsgen(context, ctxs):
186 def _ctxsgen(context, ctxs):
187 for s in ctxs:
187 for s in ctxs:
188 d = {
188 d = {
189 'node': s.hex(),
189 'node': s.hex(),
190 'rev': s.rev(),
190 'rev': s.rev(),
191 'user': s.user(),
191 'user': s.user(),
192 'date': s.date(),
192 'date': s.date(),
193 'description': s.description(),
193 'description': s.description(),
194 'branch': s.branch(),
194 'branch': s.branch(),
195 }
195 }
196 if util.safehasattr(s, 'path'):
196 if util.safehasattr(s, 'path'):
197 d['file'] = s.path()
197 d['file'] = s.path()
198 yield d
198 yield d
199
199
200 def _siblings(siblings=None, hiderev=None):
200 def _siblings(siblings=None, hiderev=None):
201 if siblings is None:
201 if siblings is None:
202 siblings = []
202 siblings = []
203 siblings = [s for s in siblings if s.node() != nullid]
203 siblings = [s for s in siblings if s.node() != nullid]
204 if len(siblings) == 1 and siblings[0].rev() == hiderev:
204 if len(siblings) == 1 and siblings[0].rev() == hiderev:
205 siblings = []
205 siblings = []
206 return templateutil.mappinggenerator(_ctxsgen, args=(siblings,))
206 return templateutil.mappinggenerator(_ctxsgen, args=(siblings,))
207
207
208 def difffeatureopts(req, ui, section):
208 def difffeatureopts(req, ui, section):
209 diffopts = patch.difffeatureopts(ui, untrusted=True,
209 diffopts = patch.difffeatureopts(ui, untrusted=True,
210 section=section, whitespace=True)
210 section=section, whitespace=True)
211
211
212 for k in ('ignorews', 'ignorewsamount', 'ignorewseol', 'ignoreblanklines'):
212 for k in ('ignorews', 'ignorewsamount', 'ignorewseol', 'ignoreblanklines'):
213 v = req.qsparams.get(k)
213 v = req.qsparams.get(k)
214 if v is not None:
214 if v is not None:
215 v = stringutil.parsebool(v)
215 v = stringutil.parsebool(v)
216 setattr(diffopts, k, v if v is not None else True)
216 setattr(diffopts, k, v if v is not None else True)
217
217
218 return diffopts
218 return diffopts
219
219
220 def annotate(req, fctx, ui):
220 def annotate(req, fctx, ui):
221 diffopts = difffeatureopts(req, ui, 'annotate')
221 diffopts = difffeatureopts(req, ui, 'annotate')
222 return fctx.annotate(follow=True, diffopts=diffopts)
222 return fctx.annotate(follow=True, diffopts=diffopts)
223
223
224 def parents(ctx, hide=None):
224 def parents(ctx, hide=None):
225 if isinstance(ctx, context.basefilectx):
225 if isinstance(ctx, context.basefilectx):
226 introrev = ctx.introrev()
226 introrev = ctx.introrev()
227 if ctx.changectx().rev() != introrev:
227 if ctx.changectx().rev() != introrev:
228 return _siblings([ctx.repo()[introrev]], hide)
228 return _siblings([ctx.repo()[introrev]], hide)
229 return _siblings(ctx.parents(), hide)
229 return _siblings(ctx.parents(), hide)
230
230
231 def children(ctx, hide=None):
231 def children(ctx, hide=None):
232 return _siblings(ctx.children(), hide)
232 return _siblings(ctx.children(), hide)
233
233
234 def renamelink(fctx):
234 def renamelink(fctx):
235 r = fctx.renamed()
235 r = fctx.renamed()
236 if r:
236 if r:
237 return templateutil.mappinglist([{'file': r[0], 'node': hex(r[1])}])
237 return templateutil.mappinglist([{'file': r[0], 'node': hex(r[1])}])
238 return templateutil.mappinglist([])
238 return templateutil.mappinglist([])
239
239
240 def nodetagsdict(repo, node):
240 def nodetagsdict(repo, node):
241 return templateutil.hybridlist(repo.nodetags(node), name='name')
241 return templateutil.hybridlist(repo.nodetags(node), name='name')
242
242
243 def nodebookmarksdict(repo, node):
243 def nodebookmarksdict(repo, node):
244 return templateutil.hybridlist(repo.nodebookmarks(node), name='name')
244 return templateutil.hybridlist(repo.nodebookmarks(node), name='name')
245
245
246 def nodebranchdict(repo, ctx):
246 def nodebranchdict(repo, ctx):
247 branches = []
247 branches = []
248 branch = ctx.branch()
248 branch = ctx.branch()
249 # If this is an empty repo, ctx.node() == nullid,
249 # If this is an empty repo, ctx.node() == nullid,
250 # ctx.branch() == 'default'.
250 # ctx.branch() == 'default'.
251 try:
251 try:
252 branchnode = repo.branchtip(branch)
252 branchnode = repo.branchtip(branch)
253 except error.RepoLookupError:
253 except error.RepoLookupError:
254 branchnode = None
254 branchnode = None
255 if branchnode == ctx.node():
255 if branchnode == ctx.node():
256 branches.append(branch)
256 branches.append(branch)
257 return templateutil.hybridlist(branches, name='name')
257 return templateutil.hybridlist(branches, name='name')
258
258
259 def nodeinbranch(repo, ctx):
259 def nodeinbranch(repo, ctx):
260 branches = []
260 branches = []
261 branch = ctx.branch()
261 branch = ctx.branch()
262 try:
262 try:
263 branchnode = repo.branchtip(branch)
263 branchnode = repo.branchtip(branch)
264 except error.RepoLookupError:
264 except error.RepoLookupError:
265 branchnode = None
265 branchnode = None
266 if branch != 'default' and branchnode != ctx.node():
266 if branch != 'default' and branchnode != ctx.node():
267 branches.append(branch)
267 branches.append(branch)
268 return templateutil.hybridlist(branches, name='name')
268 return templateutil.hybridlist(branches, name='name')
269
269
270 def nodebranchnodefault(ctx):
270 def nodebranchnodefault(ctx):
271 branches = []
271 branches = []
272 branch = ctx.branch()
272 branch = ctx.branch()
273 if branch != 'default':
273 if branch != 'default':
274 branches.append(branch)
274 branches.append(branch)
275 return templateutil.hybridlist(branches, name='name')
275 return templateutil.hybridlist(branches, name='name')
276
276
277 def showtag(repo, tmpl, t1, node=nullid, **args):
277 def showtag(repo, tmpl, t1, node=nullid):
278 args = pycompat.byteskwargs(args)
279 for t in repo.nodetags(node):
278 for t in repo.nodetags(node):
280 lm = args.copy()
279 lm = {'tag': t}
281 lm['tag'] = t
282 yield tmpl.generate(t1, lm)
280 yield tmpl.generate(t1, lm)
283
281
284 def showbookmark(repo, tmpl, t1, node=nullid, **args):
282 def showbookmark(repo, tmpl, t1, node=nullid):
285 args = pycompat.byteskwargs(args)
286 for t in repo.nodebookmarks(node):
283 for t in repo.nodebookmarks(node):
287 lm = args.copy()
284 lm = {'bookmark': t}
288 lm['bookmark'] = t
289 yield tmpl.generate(t1, lm)
285 yield tmpl.generate(t1, lm)
290
286
291 def branchentries(repo, stripecount, limit=0):
287 def branchentries(repo, stripecount, limit=0):
292 tips = []
288 tips = []
293 heads = repo.heads()
289 heads = repo.heads()
294 parity = paritygen(stripecount)
290 parity = paritygen(stripecount)
295 sortkey = lambda item: (not item[1], item[0].rev())
291 sortkey = lambda item: (not item[1], item[0].rev())
296
292
297 def entries(**map):
293 def entries(**map):
298 count = 0
294 count = 0
299 if not tips:
295 if not tips:
300 for tag, hs, tip, closed in repo.branchmap().iterbranches():
296 for tag, hs, tip, closed in repo.branchmap().iterbranches():
301 tips.append((repo[tip], closed))
297 tips.append((repo[tip], closed))
302 for ctx, closed in sorted(tips, key=sortkey, reverse=True):
298 for ctx, closed in sorted(tips, key=sortkey, reverse=True):
303 if limit > 0 and count >= limit:
299 if limit > 0 and count >= limit:
304 return
300 return
305 count += 1
301 count += 1
306 if closed:
302 if closed:
307 status = 'closed'
303 status = 'closed'
308 elif ctx.node() not in heads:
304 elif ctx.node() not in heads:
309 status = 'inactive'
305 status = 'inactive'
310 else:
306 else:
311 status = 'open'
307 status = 'open'
312 yield {
308 yield {
313 'parity': next(parity),
309 'parity': next(parity),
314 'branch': ctx.branch(),
310 'branch': ctx.branch(),
315 'status': status,
311 'status': status,
316 'node': ctx.hex(),
312 'node': ctx.hex(),
317 'date': ctx.date()
313 'date': ctx.date()
318 }
314 }
319
315
320 return entries
316 return entries
321
317
322 def cleanpath(repo, path):
318 def cleanpath(repo, path):
323 path = path.lstrip('/')
319 path = path.lstrip('/')
324 return pathutil.canonpath(repo.root, '', path)
320 return pathutil.canonpath(repo.root, '', path)
325
321
326 def changectx(repo, req):
322 def changectx(repo, req):
327 changeid = "tip"
323 changeid = "tip"
328 if 'node' in req.qsparams:
324 if 'node' in req.qsparams:
329 changeid = req.qsparams['node']
325 changeid = req.qsparams['node']
330 ipos = changeid.find(':')
326 ipos = changeid.find(':')
331 if ipos != -1:
327 if ipos != -1:
332 changeid = changeid[(ipos + 1):]
328 changeid = changeid[(ipos + 1):]
333
329
334 return scmutil.revsymbol(repo, changeid)
330 return scmutil.revsymbol(repo, changeid)
335
331
336 def basechangectx(repo, req):
332 def basechangectx(repo, req):
337 if 'node' in req.qsparams:
333 if 'node' in req.qsparams:
338 changeid = req.qsparams['node']
334 changeid = req.qsparams['node']
339 ipos = changeid.find(':')
335 ipos = changeid.find(':')
340 if ipos != -1:
336 if ipos != -1:
341 changeid = changeid[:ipos]
337 changeid = changeid[:ipos]
342 return scmutil.revsymbol(repo, changeid)
338 return scmutil.revsymbol(repo, changeid)
343
339
344 return None
340 return None
345
341
346 def filectx(repo, req):
342 def filectx(repo, req):
347 if 'file' not in req.qsparams:
343 if 'file' not in req.qsparams:
348 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
344 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
349 path = cleanpath(repo, req.qsparams['file'])
345 path = cleanpath(repo, req.qsparams['file'])
350 if 'node' in req.qsparams:
346 if 'node' in req.qsparams:
351 changeid = req.qsparams['node']
347 changeid = req.qsparams['node']
352 elif 'filenode' in req.qsparams:
348 elif 'filenode' in req.qsparams:
353 changeid = req.qsparams['filenode']
349 changeid = req.qsparams['filenode']
354 else:
350 else:
355 raise ErrorResponse(HTTP_NOT_FOUND, 'node or filenode not given')
351 raise ErrorResponse(HTTP_NOT_FOUND, 'node or filenode not given')
356 try:
352 try:
357 fctx = scmutil.revsymbol(repo, changeid)[path]
353 fctx = scmutil.revsymbol(repo, changeid)[path]
358 except error.RepoError:
354 except error.RepoError:
359 fctx = repo.filectx(path, fileid=changeid)
355 fctx = repo.filectx(path, fileid=changeid)
360
356
361 return fctx
357 return fctx
362
358
363 def linerange(req):
359 def linerange(req):
364 linerange = req.qsparams.getall('linerange')
360 linerange = req.qsparams.getall('linerange')
365 if not linerange:
361 if not linerange:
366 return None
362 return None
367 if len(linerange) > 1:
363 if len(linerange) > 1:
368 raise ErrorResponse(HTTP_BAD_REQUEST,
364 raise ErrorResponse(HTTP_BAD_REQUEST,
369 'redundant linerange parameter')
365 'redundant linerange parameter')
370 try:
366 try:
371 fromline, toline = map(int, linerange[0].split(':', 1))
367 fromline, toline = map(int, linerange[0].split(':', 1))
372 except ValueError:
368 except ValueError:
373 raise ErrorResponse(HTTP_BAD_REQUEST,
369 raise ErrorResponse(HTTP_BAD_REQUEST,
374 'invalid linerange parameter')
370 'invalid linerange parameter')
375 try:
371 try:
376 return util.processlinerange(fromline, toline)
372 return util.processlinerange(fromline, toline)
377 except error.ParseError as exc:
373 except error.ParseError as exc:
378 raise ErrorResponse(HTTP_BAD_REQUEST, pycompat.bytestr(exc))
374 raise ErrorResponse(HTTP_BAD_REQUEST, pycompat.bytestr(exc))
379
375
380 def formatlinerange(fromline, toline):
376 def formatlinerange(fromline, toline):
381 return '%d:%d' % (fromline + 1, toline)
377 return '%d:%d' % (fromline + 1, toline)
382
378
383 def succsandmarkers(context, mapping):
379 def succsandmarkers(context, mapping):
384 repo = context.resource(mapping, 'repo')
380 repo = context.resource(mapping, 'repo')
385 itemmappings = templatekw.showsuccsandmarkers(context, mapping)
381 itemmappings = templatekw.showsuccsandmarkers(context, mapping)
386 for item in itemmappings.tovalue(context, mapping):
382 for item in itemmappings.tovalue(context, mapping):
387 item['successors'] = _siblings(repo[successor]
383 item['successors'] = _siblings(repo[successor]
388 for successor in item['successors'])
384 for successor in item['successors'])
389 yield item
385 yield item
390
386
391 # teach templater succsandmarkers is switched to (context, mapping) API
387 # teach templater succsandmarkers is switched to (context, mapping) API
392 succsandmarkers._requires = {'repo', 'ctx'}
388 succsandmarkers._requires = {'repo', 'ctx'}
393
389
394 def whyunstable(context, mapping):
390 def whyunstable(context, mapping):
395 repo = context.resource(mapping, 'repo')
391 repo = context.resource(mapping, 'repo')
396 ctx = context.resource(mapping, 'ctx')
392 ctx = context.resource(mapping, 'ctx')
397
393
398 entries = obsutil.whyunstable(repo, ctx)
394 entries = obsutil.whyunstable(repo, ctx)
399 for entry in entries:
395 for entry in entries:
400 if entry.get('divergentnodes'):
396 if entry.get('divergentnodes'):
401 entry['divergentnodes'] = _siblings(entry['divergentnodes'])
397 entry['divergentnodes'] = _siblings(entry['divergentnodes'])
402 yield entry
398 yield entry
403
399
404 whyunstable._requires = {'repo', 'ctx'}
400 whyunstable._requires = {'repo', 'ctx'}
405
401
406 def commonentry(repo, ctx):
402 def commonentry(repo, ctx):
407 node = ctx.node()
403 node = ctx.node()
408 return {
404 return {
409 # TODO: perhaps ctx.changectx() should be assigned if ctx is a
405 # TODO: perhaps ctx.changectx() should be assigned if ctx is a
410 # filectx, but I'm not pretty sure if that would always work because
406 # filectx, but I'm not pretty sure if that would always work because
411 # fctx.parents() != fctx.changectx.parents() for example.
407 # fctx.parents() != fctx.changectx.parents() for example.
412 'ctx': ctx,
408 'ctx': ctx,
413 'rev': ctx.rev(),
409 'rev': ctx.rev(),
414 'node': hex(node),
410 'node': hex(node),
415 'author': ctx.user(),
411 'author': ctx.user(),
416 'desc': ctx.description(),
412 'desc': ctx.description(),
417 'date': ctx.date(),
413 'date': ctx.date(),
418 'extra': ctx.extra(),
414 'extra': ctx.extra(),
419 'phase': ctx.phasestr(),
415 'phase': ctx.phasestr(),
420 'obsolete': ctx.obsolete(),
416 'obsolete': ctx.obsolete(),
421 'succsandmarkers': succsandmarkers,
417 'succsandmarkers': succsandmarkers,
422 'instabilities': [{"instability": i} for i in ctx.instabilities()],
418 'instabilities': [{"instability": i} for i in ctx.instabilities()],
423 'whyunstable': whyunstable,
419 'whyunstable': whyunstable,
424 'branch': nodebranchnodefault(ctx),
420 'branch': nodebranchnodefault(ctx),
425 'inbranch': nodeinbranch(repo, ctx),
421 'inbranch': nodeinbranch(repo, ctx),
426 'branches': nodebranchdict(repo, ctx),
422 'branches': nodebranchdict(repo, ctx),
427 'tags': nodetagsdict(repo, node),
423 'tags': nodetagsdict(repo, node),
428 'bookmarks': nodebookmarksdict(repo, node),
424 'bookmarks': nodebookmarksdict(repo, node),
429 'parent': lambda **x: parents(ctx),
425 'parent': lambda **x: parents(ctx),
430 'child': lambda **x: children(ctx),
426 'child': lambda **x: children(ctx),
431 }
427 }
432
428
433 def changelistentry(web, ctx):
429 def changelistentry(web, ctx):
434 '''Obtain a dictionary to be used for entries in a changelist.
430 '''Obtain a dictionary to be used for entries in a changelist.
435
431
436 This function is called when producing items for the "entries" list passed
432 This function is called when producing items for the "entries" list passed
437 to the "shortlog" and "changelog" templates.
433 to the "shortlog" and "changelog" templates.
438 '''
434 '''
439 repo = web.repo
435 repo = web.repo
440 rev = ctx.rev()
436 rev = ctx.rev()
441 n = ctx.node()
437 n = ctx.node()
442 showtags = showtag(repo, web.tmpl, 'changelogtag', n)
438 showtags = showtag(repo, web.tmpl, 'changelogtag', n)
443 files = listfilediffs(web.tmpl, ctx.files(), n, web.maxfiles)
439 files = listfilediffs(web.tmpl, ctx.files(), n, web.maxfiles)
444
440
445 entry = commonentry(repo, ctx)
441 entry = commonentry(repo, ctx)
446 entry.update(
442 entry.update(
447 allparents=lambda **x: parents(ctx),
443 allparents=lambda **x: parents(ctx),
448 parent=lambda **x: parents(ctx, rev - 1),
444 parent=lambda **x: parents(ctx, rev - 1),
449 child=lambda **x: children(ctx, rev + 1),
445 child=lambda **x: children(ctx, rev + 1),
450 changelogtag=showtags,
446 changelogtag=showtags,
451 files=files,
447 files=files,
452 )
448 )
453 return entry
449 return entry
454
450
455 def symrevorshortnode(req, ctx):
451 def symrevorshortnode(req, ctx):
456 if 'node' in req.qsparams:
452 if 'node' in req.qsparams:
457 return templatefilters.revescape(req.qsparams['node'])
453 return templatefilters.revescape(req.qsparams['node'])
458 else:
454 else:
459 return short(ctx.node())
455 return short(ctx.node())
460
456
461 def changesetentry(web, ctx):
457 def changesetentry(web, ctx):
462 '''Obtain a dictionary to be used to render the "changeset" template.'''
458 '''Obtain a dictionary to be used to render the "changeset" template.'''
463
459
464 showtags = showtag(web.repo, web.tmpl, 'changesettag', ctx.node())
460 showtags = showtag(web.repo, web.tmpl, 'changesettag', ctx.node())
465 showbookmarks = showbookmark(web.repo, web.tmpl, 'changesetbookmark',
461 showbookmarks = showbookmark(web.repo, web.tmpl, 'changesetbookmark',
466 ctx.node())
462 ctx.node())
467 showbranch = nodebranchnodefault(ctx)
463 showbranch = nodebranchnodefault(ctx)
468
464
469 files = []
465 files = []
470 parity = paritygen(web.stripecount)
466 parity = paritygen(web.stripecount)
471 for blockno, f in enumerate(ctx.files()):
467 for blockno, f in enumerate(ctx.files()):
472 template = 'filenodelink' if f in ctx else 'filenolink'
468 template = 'filenodelink' if f in ctx else 'filenolink'
473 files.append(web.tmpl.generate(template, {
469 files.append(web.tmpl.generate(template, {
474 'node': ctx.hex(),
470 'node': ctx.hex(),
475 'file': f,
471 'file': f,
476 'blockno': blockno + 1,
472 'blockno': blockno + 1,
477 'parity': next(parity),
473 'parity': next(parity),
478 }))
474 }))
479
475
480 basectx = basechangectx(web.repo, web.req)
476 basectx = basechangectx(web.repo, web.req)
481 if basectx is None:
477 if basectx is None:
482 basectx = ctx.p1()
478 basectx = ctx.p1()
483
479
484 style = web.config('web', 'style')
480 style = web.config('web', 'style')
485 if 'style' in web.req.qsparams:
481 if 'style' in web.req.qsparams:
486 style = web.req.qsparams['style']
482 style = web.req.qsparams['style']
487
483
488 diff = diffs(web, ctx, basectx, None, style)
484 diff = diffs(web, ctx, basectx, None, style)
489
485
490 parity = paritygen(web.stripecount)
486 parity = paritygen(web.stripecount)
491 diffstatsgen = diffstatgen(ctx, basectx)
487 diffstatsgen = diffstatgen(ctx, basectx)
492 diffstats = diffstat(web.tmpl, ctx, diffstatsgen, parity)
488 diffstats = diffstat(web.tmpl, ctx, diffstatsgen, parity)
493
489
494 return dict(
490 return dict(
495 diff=diff,
491 diff=diff,
496 symrev=symrevorshortnode(web.req, ctx),
492 symrev=symrevorshortnode(web.req, ctx),
497 basenode=basectx.hex(),
493 basenode=basectx.hex(),
498 changesettag=showtags,
494 changesettag=showtags,
499 changesetbookmark=showbookmarks,
495 changesetbookmark=showbookmarks,
500 changesetbranch=showbranch,
496 changesetbranch=showbranch,
501 files=files,
497 files=files,
502 diffsummary=lambda **x: diffsummary(diffstatsgen),
498 diffsummary=lambda **x: diffsummary(diffstatsgen),
503 diffstat=diffstats,
499 diffstat=diffstats,
504 archives=web.archivelist(ctx.hex()),
500 archives=web.archivelist(ctx.hex()),
505 **pycompat.strkwargs(commonentry(web.repo, ctx)))
501 **pycompat.strkwargs(commonentry(web.repo, ctx)))
506
502
507 def listfilediffs(tmpl, files, node, max):
503 def listfilediffs(tmpl, files, node, max):
508 for f in files[:max]:
504 for f in files[:max]:
509 yield tmpl.generate('filedifflink', {'node': hex(node), 'file': f})
505 yield tmpl.generate('filedifflink', {'node': hex(node), 'file': f})
510 if len(files) > max:
506 if len(files) > max:
511 yield tmpl.generate('fileellipses', {})
507 yield tmpl.generate('fileellipses', {})
512
508
513 def diffs(web, ctx, basectx, files, style, linerange=None,
509 def diffs(web, ctx, basectx, files, style, linerange=None,
514 lineidprefix=''):
510 lineidprefix=''):
515
511
516 def prettyprintlines(lines, blockno):
512 def prettyprintlines(lines, blockno):
517 for lineno, l in enumerate(lines, 1):
513 for lineno, l in enumerate(lines, 1):
518 difflineno = "%d.%d" % (blockno, lineno)
514 difflineno = "%d.%d" % (blockno, lineno)
519 if l.startswith('+'):
515 if l.startswith('+'):
520 ltype = "difflineplus"
516 ltype = "difflineplus"
521 elif l.startswith('-'):
517 elif l.startswith('-'):
522 ltype = "difflineminus"
518 ltype = "difflineminus"
523 elif l.startswith('@'):
519 elif l.startswith('@'):
524 ltype = "difflineat"
520 ltype = "difflineat"
525 else:
521 else:
526 ltype = "diffline"
522 ltype = "diffline"
527 yield web.tmpl.generate(ltype, {
523 yield web.tmpl.generate(ltype, {
528 'line': l,
524 'line': l,
529 'lineno': lineno,
525 'lineno': lineno,
530 'lineid': lineidprefix + "l%s" % difflineno,
526 'lineid': lineidprefix + "l%s" % difflineno,
531 'linenumber': "% 8s" % difflineno,
527 'linenumber': "% 8s" % difflineno,
532 })
528 })
533
529
534 repo = web.repo
530 repo = web.repo
535 if files:
531 if files:
536 m = match.exact(repo.root, repo.getcwd(), files)
532 m = match.exact(repo.root, repo.getcwd(), files)
537 else:
533 else:
538 m = match.always(repo.root, repo.getcwd())
534 m = match.always(repo.root, repo.getcwd())
539
535
540 diffopts = patch.diffopts(repo.ui, untrusted=True)
536 diffopts = patch.diffopts(repo.ui, untrusted=True)
541 node1 = basectx.node()
537 node1 = basectx.node()
542 node2 = ctx.node()
538 node2 = ctx.node()
543 parity = paritygen(web.stripecount)
539 parity = paritygen(web.stripecount)
544
540
545 diffhunks = patch.diffhunks(repo, node1, node2, m, opts=diffopts)
541 diffhunks = patch.diffhunks(repo, node1, node2, m, opts=diffopts)
546 for blockno, (fctx1, fctx2, header, hunks) in enumerate(diffhunks, 1):
542 for blockno, (fctx1, fctx2, header, hunks) in enumerate(diffhunks, 1):
547 if style != 'raw':
543 if style != 'raw':
548 header = header[1:]
544 header = header[1:]
549 lines = [h + '\n' for h in header]
545 lines = [h + '\n' for h in header]
550 for hunkrange, hunklines in hunks:
546 for hunkrange, hunklines in hunks:
551 if linerange is not None and hunkrange is not None:
547 if linerange is not None and hunkrange is not None:
552 s1, l1, s2, l2 = hunkrange
548 s1, l1, s2, l2 = hunkrange
553 if not mdiff.hunkinrange((s2, l2), linerange):
549 if not mdiff.hunkinrange((s2, l2), linerange):
554 continue
550 continue
555 lines.extend(hunklines)
551 lines.extend(hunklines)
556 if lines:
552 if lines:
557 yield web.tmpl.generate('diffblock', {
553 yield web.tmpl.generate('diffblock', {
558 'parity': next(parity),
554 'parity': next(parity),
559 'blockno': blockno,
555 'blockno': blockno,
560 'lines': prettyprintlines(lines, blockno),
556 'lines': prettyprintlines(lines, blockno),
561 })
557 })
562
558
563 def compare(tmpl, context, leftlines, rightlines):
559 def compare(tmpl, context, leftlines, rightlines):
564 '''Generator function that provides side-by-side comparison data.'''
560 '''Generator function that provides side-by-side comparison data.'''
565
561
566 def compline(type, leftlineno, leftline, rightlineno, rightline):
562 def compline(type, leftlineno, leftline, rightlineno, rightline):
567 lineid = leftlineno and ("l%d" % leftlineno) or ''
563 lineid = leftlineno and ("l%d" % leftlineno) or ''
568 lineid += rightlineno and ("r%d" % rightlineno) or ''
564 lineid += rightlineno and ("r%d" % rightlineno) or ''
569 llno = '%d' % leftlineno if leftlineno else ''
565 llno = '%d' % leftlineno if leftlineno else ''
570 rlno = '%d' % rightlineno if rightlineno else ''
566 rlno = '%d' % rightlineno if rightlineno else ''
571 return tmpl.generate('comparisonline', {
567 return tmpl.generate('comparisonline', {
572 'type': type,
568 'type': type,
573 'lineid': lineid,
569 'lineid': lineid,
574 'leftlineno': leftlineno,
570 'leftlineno': leftlineno,
575 'leftlinenumber': "% 6s" % llno,
571 'leftlinenumber': "% 6s" % llno,
576 'leftline': leftline or '',
572 'leftline': leftline or '',
577 'rightlineno': rightlineno,
573 'rightlineno': rightlineno,
578 'rightlinenumber': "% 6s" % rlno,
574 'rightlinenumber': "% 6s" % rlno,
579 'rightline': rightline or '',
575 'rightline': rightline or '',
580 })
576 })
581
577
582 def getblock(opcodes):
578 def getblock(opcodes):
583 for type, llo, lhi, rlo, rhi in opcodes:
579 for type, llo, lhi, rlo, rhi in opcodes:
584 len1 = lhi - llo
580 len1 = lhi - llo
585 len2 = rhi - rlo
581 len2 = rhi - rlo
586 count = min(len1, len2)
582 count = min(len1, len2)
587 for i in xrange(count):
583 for i in xrange(count):
588 yield compline(type=type,
584 yield compline(type=type,
589 leftlineno=llo + i + 1,
585 leftlineno=llo + i + 1,
590 leftline=leftlines[llo + i],
586 leftline=leftlines[llo + i],
591 rightlineno=rlo + i + 1,
587 rightlineno=rlo + i + 1,
592 rightline=rightlines[rlo + i])
588 rightline=rightlines[rlo + i])
593 if len1 > len2:
589 if len1 > len2:
594 for i in xrange(llo + count, lhi):
590 for i in xrange(llo + count, lhi):
595 yield compline(type=type,
591 yield compline(type=type,
596 leftlineno=i + 1,
592 leftlineno=i + 1,
597 leftline=leftlines[i],
593 leftline=leftlines[i],
598 rightlineno=None,
594 rightlineno=None,
599 rightline=None)
595 rightline=None)
600 elif len2 > len1:
596 elif len2 > len1:
601 for i in xrange(rlo + count, rhi):
597 for i in xrange(rlo + count, rhi):
602 yield compline(type=type,
598 yield compline(type=type,
603 leftlineno=None,
599 leftlineno=None,
604 leftline=None,
600 leftline=None,
605 rightlineno=i + 1,
601 rightlineno=i + 1,
606 rightline=rightlines[i])
602 rightline=rightlines[i])
607
603
608 s = difflib.SequenceMatcher(None, leftlines, rightlines)
604 s = difflib.SequenceMatcher(None, leftlines, rightlines)
609 if context < 0:
605 if context < 0:
610 yield tmpl.generate('comparisonblock',
606 yield tmpl.generate('comparisonblock',
611 {'lines': getblock(s.get_opcodes())})
607 {'lines': getblock(s.get_opcodes())})
612 else:
608 else:
613 for oc in s.get_grouped_opcodes(n=context):
609 for oc in s.get_grouped_opcodes(n=context):
614 yield tmpl.generate('comparisonblock', {'lines': getblock(oc)})
610 yield tmpl.generate('comparisonblock', {'lines': getblock(oc)})
615
611
616 def diffstatgen(ctx, basectx):
612 def diffstatgen(ctx, basectx):
617 '''Generator function that provides the diffstat data.'''
613 '''Generator function that provides the diffstat data.'''
618
614
619 stats = patch.diffstatdata(
615 stats = patch.diffstatdata(
620 util.iterlines(ctx.diff(basectx, noprefix=False)))
616 util.iterlines(ctx.diff(basectx, noprefix=False)))
621 maxname, maxtotal, addtotal, removetotal, binary = patch.diffstatsum(stats)
617 maxname, maxtotal, addtotal, removetotal, binary = patch.diffstatsum(stats)
622 while True:
618 while True:
623 yield stats, maxname, maxtotal, addtotal, removetotal, binary
619 yield stats, maxname, maxtotal, addtotal, removetotal, binary
624
620
625 def diffsummary(statgen):
621 def diffsummary(statgen):
626 '''Return a short summary of the diff.'''
622 '''Return a short summary of the diff.'''
627
623
628 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
624 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
629 return _(' %d files changed, %d insertions(+), %d deletions(-)\n') % (
625 return _(' %d files changed, %d insertions(+), %d deletions(-)\n') % (
630 len(stats), addtotal, removetotal)
626 len(stats), addtotal, removetotal)
631
627
632 def diffstat(tmpl, ctx, statgen, parity):
628 def diffstat(tmpl, ctx, statgen, parity):
633 '''Return a diffstat template for each file in the diff.'''
629 '''Return a diffstat template for each file in the diff.'''
634
630
635 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
631 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
636 files = ctx.files()
632 files = ctx.files()
637
633
638 def pct(i):
634 def pct(i):
639 if maxtotal == 0:
635 if maxtotal == 0:
640 return 0
636 return 0
641 return (float(i) / maxtotal) * 100
637 return (float(i) / maxtotal) * 100
642
638
643 fileno = 0
639 fileno = 0
644 for filename, adds, removes, isbinary in stats:
640 for filename, adds, removes, isbinary in stats:
645 template = 'diffstatlink' if filename in files else 'diffstatnolink'
641 template = 'diffstatlink' if filename in files else 'diffstatnolink'
646 total = adds + removes
642 total = adds + removes
647 fileno += 1
643 fileno += 1
648 yield tmpl.generate(template, {
644 yield tmpl.generate(template, {
649 'node': ctx.hex(),
645 'node': ctx.hex(),
650 'file': filename,
646 'file': filename,
651 'fileno': fileno,
647 'fileno': fileno,
652 'total': total,
648 'total': total,
653 'addpct': pct(adds),
649 'addpct': pct(adds),
654 'removepct': pct(removes),
650 'removepct': pct(removes),
655 'parity': next(parity),
651 'parity': next(parity),
656 })
652 })
657
653
658 class sessionvars(templateutil.wrapped):
654 class sessionvars(templateutil.wrapped):
659 def __init__(self, vars, start='?'):
655 def __init__(self, vars, start='?'):
660 self._start = start
656 self._start = start
661 self._vars = vars
657 self._vars = vars
662
658
663 def __getitem__(self, key):
659 def __getitem__(self, key):
664 return self._vars[key]
660 return self._vars[key]
665
661
666 def __setitem__(self, key, value):
662 def __setitem__(self, key, value):
667 self._vars[key] = value
663 self._vars[key] = value
668
664
669 def __copy__(self):
665 def __copy__(self):
670 return sessionvars(copy.copy(self._vars), self._start)
666 return sessionvars(copy.copy(self._vars), self._start)
671
667
672 def itermaps(self, context):
668 def itermaps(self, context):
673 separator = self._start
669 separator = self._start
674 for key, value in sorted(self._vars.iteritems()):
670 for key, value in sorted(self._vars.iteritems()):
675 yield {'name': key,
671 yield {'name': key,
676 'value': pycompat.bytestr(value),
672 'value': pycompat.bytestr(value),
677 'separator': separator,
673 'separator': separator,
678 }
674 }
679 separator = '&'
675 separator = '&'
680
676
681 def join(self, context, mapping, sep):
677 def join(self, context, mapping, sep):
682 # could be '{separator}{name}={value|urlescape}'
678 # could be '{separator}{name}={value|urlescape}'
683 raise error.ParseError(_('not displayable without template'))
679 raise error.ParseError(_('not displayable without template'))
684
680
685 def show(self, context, mapping):
681 def show(self, context, mapping):
686 return self.join(context, '')
682 return self.join(context, '')
687
683
688 def tovalue(self, context, mapping):
684 def tovalue(self, context, mapping):
689 return self._vars
685 return self._vars
690
686
691 class wsgiui(uimod.ui):
687 class wsgiui(uimod.ui):
692 # default termwidth breaks under mod_wsgi
688 # default termwidth breaks under mod_wsgi
693 def termwidth(self):
689 def termwidth(self):
694 return 80
690 return 80
695
691
696 def getwebsubs(repo):
692 def getwebsubs(repo):
697 websubtable = []
693 websubtable = []
698 websubdefs = repo.ui.configitems('websub')
694 websubdefs = repo.ui.configitems('websub')
699 # we must maintain interhg backwards compatibility
695 # we must maintain interhg backwards compatibility
700 websubdefs += repo.ui.configitems('interhg')
696 websubdefs += repo.ui.configitems('interhg')
701 for key, pattern in websubdefs:
697 for key, pattern in websubdefs:
702 # grab the delimiter from the character after the "s"
698 # grab the delimiter from the character after the "s"
703 unesc = pattern[1:2]
699 unesc = pattern[1:2]
704 delim = re.escape(unesc)
700 delim = re.escape(unesc)
705
701
706 # identify portions of the pattern, taking care to avoid escaped
702 # identify portions of the pattern, taking care to avoid escaped
707 # delimiters. the replace format and flags are optional, but
703 # delimiters. the replace format and flags are optional, but
708 # delimiters are required.
704 # delimiters are required.
709 match = re.match(
705 match = re.match(
710 br'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
706 br'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
711 % (delim, delim, delim), pattern)
707 % (delim, delim, delim), pattern)
712 if not match:
708 if not match:
713 repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
709 repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
714 % (key, pattern))
710 % (key, pattern))
715 continue
711 continue
716
712
717 # we need to unescape the delimiter for regexp and format
713 # we need to unescape the delimiter for regexp and format
718 delim_re = re.compile(br'(?<!\\)\\%s' % delim)
714 delim_re = re.compile(br'(?<!\\)\\%s' % delim)
719 regexp = delim_re.sub(unesc, match.group(1))
715 regexp = delim_re.sub(unesc, match.group(1))
720 format = delim_re.sub(unesc, match.group(2))
716 format = delim_re.sub(unesc, match.group(2))
721
717
722 # the pattern allows for 6 regexp flags, so set them if necessary
718 # the pattern allows for 6 regexp flags, so set them if necessary
723 flagin = match.group(3)
719 flagin = match.group(3)
724 flags = 0
720 flags = 0
725 if flagin:
721 if flagin:
726 for flag in flagin.upper():
722 for flag in flagin.upper():
727 flags |= re.__dict__[flag]
723 flags |= re.__dict__[flag]
728
724
729 try:
725 try:
730 regexp = re.compile(regexp, flags)
726 regexp = re.compile(regexp, flags)
731 websubtable.append((regexp, format))
727 websubtable.append((regexp, format))
732 except re.error:
728 except re.error:
733 repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
729 repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
734 % (key, regexp))
730 % (key, regexp))
735 return websubtable
731 return websubtable
736
732
737 def getgraphnode(repo, ctx):
733 def getgraphnode(repo, ctx):
738 return (templatekw.getgraphnodecurrent(repo, ctx) +
734 return (templatekw.getgraphnodecurrent(repo, ctx) +
739 templatekw.getgraphnodesymbol(ctx))
735 templatekw.getgraphnodesymbol(ctx))
General Comments 0
You need to be logged in to leave comments. Login now