##// END OF EJS Templates
keyword: do not inspect node1 for diff if node2 is given
Christian Ebert -
r6117:c74f1d90 default
parent child Browse files
Show More
@@ -1,556 +1,556 b''
1 1 # keyword.py - $Keyword$ expansion for Mercurial
2 2 #
3 3 # Copyright 2007, 2008 Christian Ebert <blacktrash@gmx.net>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7 #
8 8 # $Id$
9 9 #
10 10 # Keyword expansion hack against the grain of a DSCM
11 11 #
12 12 # There are many good reasons why this is not needed in a distributed
13 13 # SCM, still it may be useful in very small projects based on single
14 14 # files (like LaTeX packages), that are mostly addressed to an audience
15 15 # not running a version control system.
16 16 #
17 17 # For in-depth discussion refer to
18 18 # <http://www.selenic.com/mercurial/wiki/index.cgi/KeywordPlan>.
19 19 #
20 20 # Keyword expansion is based on Mercurial's changeset template mappings.
21 21 #
22 22 # Binary files are not touched.
23 23 #
24 24 # Setup in hgrc:
25 25 #
26 26 # [extensions]
27 27 # # enable extension
28 28 # hgext.keyword =
29 29 #
30 30 # Files to act upon/ignore are specified in the [keyword] section.
31 31 # Customized keyword template mappings in the [keywordmaps] section.
32 32 #
33 33 # Run "hg help keyword" and "hg kwdemo" to get info on configuration.
34 34
35 35 '''keyword expansion in local repositories
36 36
37 37 This extension expands RCS/CVS-like or self-customized $Keywords$
38 38 in tracked text files selected by your configuration.
39 39
40 40 Keywords are only expanded in local repositories and not stored in
41 41 the change history. The mechanism can be regarded as a convenience
42 42 for the current user or for archive distribution.
43 43
44 44 Configuration is done in the [keyword] and [keywordmaps] sections
45 45 of hgrc files.
46 46
47 47 Example:
48 48
49 49 [keyword]
50 50 # expand keywords in every python file except those matching "x*"
51 51 **.py =
52 52 x* = ignore
53 53
54 54 Note: the more specific you are in your filename patterns
55 55 the less you lose speed in huge repos.
56 56
57 57 For [keywordmaps] template mapping and expansion demonstration and
58 58 control run "hg kwdemo".
59 59
60 60 An additional date template filter {date|utcdate} is provided.
61 61
62 62 The default template mappings (view with "hg kwdemo -d") can be replaced
63 63 with customized keywords and templates.
64 64 Again, run "hg kwdemo" to control the results of your config changes.
65 65
66 66 Before changing/disabling active keywords, run "hg kwshrink" to avoid
67 67 the risk of inadvertedly storing expanded keywords in the change history.
68 68
69 69 To force expansion after enabling it, or a configuration change, run
70 70 "hg kwexpand".
71 71
72 72 Also, when committing with the record extension or using mq's qrecord, be aware
73 73 that keywords cannot be updated. Again, run "hg kwexpand" on the files in
74 74 question to update keyword expansions after all changes have been checked in.
75 75
76 76 Expansions spanning more than one line and incremental expansions,
77 77 like CVS' $Log$, are not supported. A keyword template map
78 78 "Log = {desc}" expands to the first line of the changeset description.
79 79 '''
80 80
81 81 from mercurial import commands, cmdutil, context, dispatch, filelog, revlog
82 82 from mercurial import patch, localrepo, templater, templatefilters, util
83 83 from mercurial.hgweb import webcommands
84 84 from mercurial.node import *
85 85 from mercurial.i18n import _
86 86 import re, shutil, tempfile, time
87 87
88 88 commands.optionalrepo += ' kwdemo'
89 89
90 90 # hg commands that do not act on keywords
91 91 nokwcommands = ('add addremove bundle copy export grep identify incoming init'
92 92 ' log outgoing push remove rename rollback tip'
93 93 ' convert email glog')
94 94
95 95 # hg commands that trigger expansion only when writing to working dir,
96 96 # not when reading filelog, and unexpand when reading from working dir
97 97 restricted = 'record qfold qimport qnew qpush qrefresh qrecord'
98 98
99 99 def utcdate(date):
100 100 '''Returns hgdate in cvs-like UTC format.'''
101 101 return time.strftime('%Y/%m/%d %H:%M:%S', time.gmtime(date[0]))
102 102
103 103
104 104 # make keyword tools accessible
105 105 kwtools = {'templater': None, 'hgcmd': None}
106 106
107 107 # store originals of monkeypatches
108 108 _patchfile_init = patch.patchfile.__init__
109 109 _patch_diff = patch.diff
110 110 _dispatch_parse = dispatch._parse
111 111
112 112 def _kwpatchfile_init(self, ui, fname, missing=False):
113 113 '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
114 114 rejects or conflicts due to expanded keywords in working dir.'''
115 115 _patchfile_init(self, ui, fname, missing=missing)
116 116 # shrink keywords read from working dir
117 117 kwt = kwtools['templater']
118 118 self.lines = kwt.shrinklines(self.fname, self.lines)
119 119
120 120 def _kw_diff(repo, node1=None, node2=None, files=None, match=util.always,
121 121 fp=None, changes=None, opts=None):
122 122 '''Monkeypatch patch.diff to avoid expansion except when
123 123 comparing against working dir.'''
124 124 if node2 is not None:
125 125 kwtools['templater'].matcher = util.never
126 if node1 is not None and node1 != repo.changectx().node():
126 elif node1 is not None and node1 != repo.changectx().node():
127 127 kwtools['templater'].restrict = True
128 128 _patch_diff(repo, node1=node1, node2=node2, files=files, match=match,
129 129 fp=fp, changes=changes, opts=opts)
130 130
131 131 def _kwweb_changeset(web, req, tmpl):
132 132 '''Wraps webcommands.changeset turning off keyword expansion.'''
133 133 kwtools['templater'].matcher = util.never
134 134 return web.changeset(tmpl, web.changectx(req))
135 135
136 136 def _kwweb_filediff(web, req, tmpl):
137 137 '''Wraps webcommands.filediff turning off keyword expansion.'''
138 138 kwtools['templater'].matcher = util.never
139 139 return web.filediff(tmpl, web.filectx(req))
140 140
141 141 def _kwdispatch_parse(ui, args):
142 142 '''Monkeypatch dispatch._parse to obtain running hg command.'''
143 143 cmd, func, args, options, cmdoptions = _dispatch_parse(ui, args)
144 144 kwtools['hgcmd'] = cmd
145 145 return cmd, func, args, options, cmdoptions
146 146
147 147 # dispatch._parse is run before reposetup, so wrap it here
148 148 dispatch._parse = _kwdispatch_parse
149 149
150 150
151 151 class kwtemplater(object):
152 152 '''
153 153 Sets up keyword templates, corresponding keyword regex, and
154 154 provides keyword substitution functions.
155 155 '''
156 156 templates = {
157 157 'Revision': '{node|short}',
158 158 'Author': '{author|user}',
159 159 'Date': '{date|utcdate}',
160 160 'RCSFile': '{file|basename},v',
161 161 'Source': '{root}/{file},v',
162 162 'Id': '{file|basename},v {node|short} {date|utcdate} {author|user}',
163 163 'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}',
164 164 }
165 165
166 166 def __init__(self, ui, repo, inc, exc):
167 167 self.ui = ui
168 168 self.repo = repo
169 169 self.matcher = util.matcher(repo.root, inc=inc, exc=exc)[1]
170 170 self.restrict = kwtools['hgcmd'] in restricted.split()
171 171
172 172 kwmaps = self.ui.configitems('keywordmaps')
173 173 if kwmaps: # override default templates
174 174 kwmaps = [(k, templater.parsestring(v, quoted=False))
175 175 for (k, v) in kwmaps]
176 176 self.templates = dict(kwmaps)
177 177 escaped = map(re.escape, self.templates.keys())
178 178 kwpat = r'\$(%s)(: [^$\n\r]*? )??\$' % '|'.join(escaped)
179 179 self.re_kw = re.compile(kwpat)
180 180
181 181 templatefilters.filters['utcdate'] = utcdate
182 182 self.ct = cmdutil.changeset_templater(self.ui, self.repo,
183 183 False, '', False)
184 184
185 185 def getnode(self, path, fnode):
186 186 '''Derives changenode from file path and filenode.'''
187 187 # used by kwfilelog.read and kwexpand
188 188 c = context.filectx(self.repo, path, fileid=fnode)
189 189 return c.node()
190 190
191 191 def substitute(self, data, path, node, subfunc):
192 192 '''Replaces keywords in data with expanded template.'''
193 193 def kwsub(mobj):
194 194 kw = mobj.group(1)
195 195 self.ct.use_template(self.templates[kw])
196 196 self.ui.pushbuffer()
197 197 self.ct.show(changenode=node, root=self.repo.root, file=path)
198 198 ekw = templatefilters.firstline(self.ui.popbuffer())
199 199 return '$%s: %s $' % (kw, ekw)
200 200 return subfunc(kwsub, data)
201 201
202 202 def expand(self, path, node, data):
203 203 '''Returns data with keywords expanded.'''
204 204 if not self.restrict and self.matcher(path) and not util.binary(data):
205 205 changenode = self.getnode(path, node)
206 206 return self.substitute(data, path, changenode, self.re_kw.sub)
207 207 return data
208 208
209 209 def iskwfile(self, path, islink):
210 210 '''Returns true if path matches [keyword] pattern
211 211 and is not a symbolic link.
212 212 Caveat: localrepository._link fails on Windows.'''
213 213 return self.matcher(path) and not islink(path)
214 214
215 215 def overwrite(self, node=None, expand=True, files=None):
216 216 '''Overwrites selected files expanding/shrinking keywords.'''
217 217 ctx = self.repo.changectx(node)
218 218 mf = ctx.manifest()
219 219 if node is not None: # commit
220 220 files = [f for f in ctx.files() if f in mf]
221 221 notify = self.ui.debug
222 222 else: # kwexpand/kwshrink
223 223 notify = self.ui.note
224 224 candidates = [f for f in files if self.iskwfile(f, mf.linkf)]
225 225 if candidates:
226 226 self.restrict = True # do not expand when reading
227 227 candidates.sort()
228 228 action = expand and 'expanding' or 'shrinking'
229 229 for f in candidates:
230 230 fp = self.repo.file(f)
231 231 data = fp.read(mf[f])
232 232 if util.binary(data):
233 233 continue
234 234 if expand:
235 235 changenode = node or self.getnode(f, mf[f])
236 236 data, found = self.substitute(data, f, changenode,
237 237 self.re_kw.subn)
238 238 else:
239 239 found = self.re_kw.search(data)
240 240 if found:
241 241 notify(_('overwriting %s %s keywords\n') % (f, action))
242 242 self.repo.wwrite(f, data, mf.flags(f))
243 243 self.repo.dirstate.normal(f)
244 244 self.restrict = False
245 245
246 246 def shrinktext(self, text):
247 247 '''Unconditionally removes all keyword substitutions from text.'''
248 248 return self.re_kw.sub(r'$\1$', text)
249 249
250 250 def shrink(self, fname, text):
251 251 '''Returns text with all keyword substitutions removed.'''
252 252 if self.matcher(fname) and not util.binary(text):
253 253 return self.shrinktext(text)
254 254 return text
255 255
256 256 def shrinklines(self, fname, lines):
257 257 '''Returns lines with keyword substitutions removed.'''
258 258 if self.matcher(fname):
259 259 text = ''.join(lines)
260 260 if not util.binary(text):
261 261 return self.shrinktext(text).splitlines(True)
262 262 return lines
263 263
264 264 def wread(self, fname, data):
265 265 '''If in restricted mode returns data read from wdir with
266 266 keyword substitutions removed.'''
267 267 return self.restrict and self.shrink(fname, data) or data
268 268
269 269 class kwfilelog(filelog.filelog):
270 270 '''
271 271 Subclass of filelog to hook into its read, add, cmp methods.
272 272 Keywords are "stored" unexpanded, and processed on reading.
273 273 '''
274 274 def __init__(self, opener, path):
275 275 super(kwfilelog, self).__init__(opener, path)
276 276 self.kwt = kwtools['templater']
277 277 self.path = path
278 278
279 279 def read(self, node):
280 280 '''Expands keywords when reading filelog.'''
281 281 data = super(kwfilelog, self).read(node)
282 282 return self.kwt.expand(self.path, node, data)
283 283
284 284 def add(self, text, meta, tr, link, p1=None, p2=None):
285 285 '''Removes keyword substitutions when adding to filelog.'''
286 286 text = self.kwt.shrink(self.path, text)
287 287 return super(kwfilelog, self).add(text, meta, tr, link, p1=p1, p2=p2)
288 288
289 289 def cmp(self, node, text):
290 290 '''Removes keyword substitutions for comparison.'''
291 291 text = self.kwt.shrink(self.path, text)
292 292 if self.renamed(node):
293 293 t2 = super(kwfilelog, self).read(node)
294 294 return t2 != text
295 295 return revlog.revlog.cmp(self, node, text)
296 296
297 297 def _status(ui, repo, kwt, *pats, **opts):
298 298 '''Bails out if [keyword] configuration is not active.
299 299 Returns status of working directory.'''
300 300 if kwt:
301 301 files, match, anypats = cmdutil.matchpats(repo, pats, opts)
302 302 return repo.status(files=files, match=match, list_clean=True)
303 303 if ui.configitems('keyword'):
304 304 raise util.Abort(_('[keyword] patterns cannot match'))
305 305 raise util.Abort(_('no [keyword] patterns configured'))
306 306
307 307 def _kwfwrite(ui, repo, expand, *pats, **opts):
308 308 '''Selects files and passes them to kwtemplater.overwrite.'''
309 309 kwt = kwtools['templater']
310 310 status = _status(ui, repo, kwt, *pats, **opts)
311 311 modified, added, removed, deleted, unknown, ignored, clean = status
312 312 if modified or added or removed or deleted:
313 313 raise util.Abort(_('outstanding uncommitted changes in given files'))
314 314 wlock = lock = None
315 315 try:
316 316 wlock = repo.wlock()
317 317 lock = repo.lock()
318 318 kwt.overwrite(expand=expand, files=clean)
319 319 finally:
320 320 del wlock, lock
321 321
322 322
323 323 def demo(ui, repo, *args, **opts):
324 324 '''print [keywordmaps] configuration and an expansion example
325 325
326 326 Show current, custom, or default keyword template maps
327 327 and their expansion.
328 328
329 329 Extend current configuration by specifying maps as arguments
330 330 and optionally by reading from an additional hgrc file.
331 331
332 332 Override current keyword template maps with "default" option.
333 333 '''
334 334 def demostatus(stat):
335 335 ui.status(_('\n\t%s\n') % stat)
336 336
337 337 def demoitems(section, items):
338 338 ui.write('[%s]\n' % section)
339 339 for k, v in items:
340 340 ui.write('%s = %s\n' % (k, v))
341 341
342 342 msg = 'hg keyword config and expansion example'
343 343 kwstatus = 'current'
344 344 fn = 'demo.txt'
345 345 branchname = 'demobranch'
346 346 tmpdir = tempfile.mkdtemp('', 'kwdemo.')
347 347 ui.note(_('creating temporary repo at %s\n') % tmpdir)
348 348 repo = localrepo.localrepository(ui, path=tmpdir, create=True)
349 349 ui.setconfig('keyword', fn, '')
350 350 if args or opts.get('rcfile'):
351 351 kwstatus = 'custom'
352 352 if opts.get('rcfile'):
353 353 ui.readconfig(opts.get('rcfile'))
354 354 if opts.get('default'):
355 355 kwstatus = 'default'
356 356 kwmaps = kwtemplater.templates
357 357 if ui.configitems('keywordmaps'):
358 358 # override maps from optional rcfile
359 359 for k, v in kwmaps.iteritems():
360 360 ui.setconfig('keywordmaps', k, v)
361 361 elif args:
362 362 # simulate hgrc parsing
363 363 rcmaps = ['[keywordmaps]\n'] + [a + '\n' for a in args]
364 364 fp = repo.opener('hgrc', 'w')
365 365 fp.writelines(rcmaps)
366 366 fp.close()
367 367 ui.readconfig(repo.join('hgrc'))
368 368 if not opts.get('default'):
369 369 kwmaps = dict(ui.configitems('keywordmaps')) or kwtemplater.templates
370 370 reposetup(ui, repo)
371 371 for k, v in ui.configitems('extensions'):
372 372 if k.endswith('keyword'):
373 373 extension = '%s = %s' % (k, v)
374 374 break
375 375 demostatus('config using %s keyword template maps' % kwstatus)
376 376 ui.write('[extensions]\n%s\n' % extension)
377 377 demoitems('keyword', ui.configitems('keyword'))
378 378 demoitems('keywordmaps', kwmaps.iteritems())
379 379 keywords = '$' + '$\n$'.join(kwmaps.keys()) + '$\n'
380 380 repo.wopener(fn, 'w').write(keywords)
381 381 repo.add([fn])
382 382 path = repo.wjoin(fn)
383 383 ui.note(_('\n%s keywords written to %s:\n') % (kwstatus, path))
384 384 ui.note(keywords)
385 385 ui.note('\nhg -R "%s" branch "%s"\n' % (tmpdir, branchname))
386 386 # silence branch command if not verbose
387 387 quiet = ui.quiet
388 388 ui.quiet = not ui.verbose
389 389 commands.branch(ui, repo, branchname)
390 390 ui.quiet = quiet
391 391 for name, cmd in ui.configitems('hooks'):
392 392 if name.split('.', 1)[0].find('commit') > -1:
393 393 repo.ui.setconfig('hooks', name, '')
394 394 ui.note(_('unhooked all commit hooks\n'))
395 395 ui.note('hg -R "%s" ci -m "%s"\n' % (tmpdir, msg))
396 396 repo.commit(text=msg)
397 397 format = ui.verbose and ' in %s' % path or ''
398 398 demostatus('%s keywords expanded%s' % (kwstatus, format))
399 399 ui.write(repo.wread(fn))
400 400 ui.debug(_('\nremoving temporary repo %s\n') % tmpdir)
401 401 shutil.rmtree(tmpdir, ignore_errors=True)
402 402
403 403 def expand(ui, repo, *pats, **opts):
404 404 '''expand keywords in working directory
405 405
406 406 Run after (re)enabling keyword expansion.
407 407
408 408 kwexpand refuses to run if given files contain local changes.
409 409 '''
410 410 # 3rd argument sets expansion to True
411 411 _kwfwrite(ui, repo, True, *pats, **opts)
412 412
413 413 def files(ui, repo, *pats, **opts):
414 414 '''print files currently configured for keyword expansion
415 415
416 416 Crosscheck which files in working directory are potential targets for
417 417 keyword expansion.
418 418 That is, files matched by [keyword] config patterns but not symlinks.
419 419 '''
420 420 kwt = kwtools['templater']
421 421 status = _status(ui, repo, kwt, *pats, **opts)
422 422 modified, added, removed, deleted, unknown, ignored, clean = status
423 423 files = modified + added + clean
424 424 if opts.get('untracked'):
425 425 files += unknown
426 426 files.sort()
427 427 wctx = repo.workingctx()
428 428 islink = lambda p: 'l' in wctx.fileflags(p)
429 429 kwfiles = [f for f in files if kwt.iskwfile(f, islink)]
430 430 cwd = pats and repo.getcwd() or ''
431 431 kwfstats = not opts.get('ignore') and (('K', kwfiles),) or ()
432 432 if opts.get('all') or opts.get('ignore'):
433 433 kwfstats += (('I', [f for f in files if f not in kwfiles]),)
434 434 for char, filenames in kwfstats:
435 435 format = (opts.get('all') or ui.verbose) and '%s %%s\n' % char or '%s\n'
436 436 for f in filenames:
437 437 ui.write(format % repo.pathto(f, cwd))
438 438
439 439 def shrink(ui, repo, *pats, **opts):
440 440 '''revert expanded keywords in working directory
441 441
442 442 Run before changing/disabling active keywords
443 443 or if you experience problems with "hg import" or "hg merge".
444 444
445 445 kwshrink refuses to run if given files contain local changes.
446 446 '''
447 447 # 3rd argument sets expansion to False
448 448 _kwfwrite(ui, repo, False, *pats, **opts)
449 449
450 450
451 451 def reposetup(ui, repo):
452 452 '''Sets up repo as kwrepo for keyword substitution.
453 453 Overrides file method to return kwfilelog instead of filelog
454 454 if file matches user configuration.
455 455 Wraps commit to overwrite configured files with updated
456 456 keyword substitutions.
457 457 This is done for local repos only, and only if there are
458 458 files configured at all for keyword substitution.'''
459 459
460 460 try:
461 461 if (not repo.local() or kwtools['hgcmd'] in nokwcommands.split()
462 462 or '.hg' in util.splitpath(repo.root)
463 463 or repo._url.startswith('bundle:')):
464 464 return
465 465 except AttributeError:
466 466 pass
467 467
468 468 inc, exc = [], ['.hg*']
469 469 for pat, opt in ui.configitems('keyword'):
470 470 if opt != 'ignore':
471 471 inc.append(pat)
472 472 else:
473 473 exc.append(pat)
474 474 if not inc:
475 475 return
476 476
477 477 kwtools['templater'] = kwt = kwtemplater(ui, repo, inc, exc)
478 478
479 479 class kwrepo(repo.__class__):
480 480 def file(self, f):
481 481 if f[0] == '/':
482 482 f = f[1:]
483 483 return kwfilelog(self.sopener, f)
484 484
485 485 def wread(self, filename):
486 486 data = super(kwrepo, self).wread(filename)
487 487 return kwt.wread(filename, data)
488 488
489 489 def commit(self, files=None, text='', user=None, date=None,
490 490 match=util.always, force=False, force_editor=False,
491 491 p1=None, p2=None, extra={}, empty_ok=False):
492 492 wlock = lock = None
493 493 _p1 = _p2 = None
494 494 try:
495 495 wlock = self.wlock()
496 496 lock = self.lock()
497 497 # store and postpone commit hooks
498 498 commithooks = {}
499 499 for name, cmd in ui.configitems('hooks'):
500 500 if name.split('.', 1)[0] == 'commit':
501 501 commithooks[name] = cmd
502 502 ui.setconfig('hooks', name, None)
503 503 if commithooks:
504 504 # store parents for commit hook environment
505 505 if p1 is None:
506 506 _p1, _p2 = repo.dirstate.parents()
507 507 else:
508 508 _p1, _p2 = p1, p2 or nullid
509 509 _p1 = hex(_p1)
510 510 if _p2 == nullid:
511 511 _p2 = ''
512 512 else:
513 513 _p2 = hex(_p2)
514 514
515 515 node = super(kwrepo,
516 516 self).commit(files=files, text=text, user=user,
517 517 date=date, match=match, force=force,
518 518 force_editor=force_editor,
519 519 p1=p1, p2=p2, extra=extra,
520 520 empty_ok=empty_ok)
521 521
522 522 # restore commit hooks
523 523 for name, cmd in commithooks.iteritems():
524 524 ui.setconfig('hooks', name, cmd)
525 525 if node is not None:
526 526 kwt.overwrite(node=node)
527 527 repo.hook('commit', node=node, parent1=_p1, parent2=_p2)
528 528 return node
529 529 finally:
530 530 del wlock, lock
531 531
532 532 repo.__class__ = kwrepo
533 533 patch.patchfile.__init__ = _kwpatchfile_init
534 534 patch.diff = _kw_diff
535 535 webcommands.changeset = webcommands.rev = _kwweb_changeset
536 536 webcommands.filediff = webcommands.diff = _kwweb_filediff
537 537
538 538
539 539 cmdtable = {
540 540 'kwdemo':
541 541 (demo,
542 542 [('d', 'default', None, _('show default keyword template maps')),
543 543 ('f', 'rcfile', [], _('read maps from rcfile'))],
544 544 _('hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...')),
545 545 'kwexpand': (expand, commands.walkopts,
546 546 _('hg kwexpand [OPTION]... [FILE]...')),
547 547 'kwfiles':
548 548 (files,
549 549 [('a', 'all', None, _('show keyword status flags of all files')),
550 550 ('i', 'ignore', None, _('show files excluded from expansion')),
551 551 ('u', 'untracked', None, _('additionally show untracked files')),
552 552 ] + commands.walkopts,
553 553 _('hg kwfiles [OPTION]... [FILE]...')),
554 554 'kwshrink': (shrink, commands.walkopts,
555 555 _('hg kwshrink [OPTION]... [FILE]...')),
556 556 }
General Comments 0
You need to be logged in to leave comments. Login now