##// END OF EJS Templates
bookmarks: move read methods to core
Matt Mackall -
r13351:6c5368cd default
parent child Browse files
Show More
@@ -1,520 +1,492
1 1 # Mercurial extension to provide the 'hg bookmark' command
2 2 #
3 3 # Copyright 2008 David Soria Parra <dsp@php.net>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 '''track a line of development with movable markers
9 9
10 10 Bookmarks are local movable markers to changesets. Every bookmark
11 11 points to a changeset identified by its hash. If you commit a
12 12 changeset that is based on a changeset that has a bookmark on it, the
13 13 bookmark shifts to the new changeset.
14 14
15 15 It is possible to use bookmark names in every revision lookup (e.g.
16 16 :hg:`merge`, :hg:`update`).
17 17
18 18 By default, when several bookmarks point to the same changeset, they
19 19 will all move forward together. It is possible to obtain a more
20 20 git-like experience by adding the following configuration option to
21 21 your configuration file::
22 22
23 23 [bookmarks]
24 24 track.current = True
25 25
26 26 This will cause Mercurial to track the bookmark that you are currently
27 27 using, and only update it. This is similar to git's approach to
28 28 branching.
29 29 '''
30 30
31 31 from mercurial.i18n import _
32 32 from mercurial.node import nullid, nullrev, bin, hex, short
33 33 from mercurial import util, commands, repair, extensions, pushkey, hg, url
34 34 from mercurial import revset, encoding
35 35 from mercurial import bookmarks
36 36 import os
37 37
38 38 def bookmark(ui, repo, mark=None, rev=None, force=False, delete=False, rename=None):
39 39 '''track a line of development with movable markers
40 40
41 41 Bookmarks are pointers to certain commits that move when
42 42 committing. Bookmarks are local. They can be renamed, copied and
43 43 deleted. It is possible to use bookmark names in :hg:`merge` and
44 44 :hg:`update` to merge and update respectively to a given bookmark.
45 45
46 46 You can use :hg:`bookmark NAME` to set a bookmark on the working
47 47 directory's parent revision with the given name. If you specify
48 48 a revision using -r REV (where REV may be an existing bookmark),
49 49 the bookmark is assigned to that revision.
50 50
51 51 Bookmarks can be pushed and pulled between repositories (see :hg:`help
52 52 push` and :hg:`help pull`). This requires the bookmark extension to be
53 53 enabled for both the local and remote repositories.
54 54 '''
55 55 hexfn = ui.debugflag and hex or short
56 56 marks = repo._bookmarks
57 57 cur = repo.changectx('.').node()
58 58
59 59 if rename:
60 60 if rename not in marks:
61 61 raise util.Abort(_("a bookmark of this name does not exist"))
62 62 if mark in marks and not force:
63 63 raise util.Abort(_("a bookmark of the same name already exists"))
64 64 if mark is None:
65 65 raise util.Abort(_("new bookmark name required"))
66 66 marks[mark] = marks[rename]
67 67 del marks[rename]
68 68 if repo._bookmarkcurrent == rename:
69 69 bookmarks.setcurrent(repo, mark)
70 70 bookmarks.write(repo)
71 71 return
72 72
73 73 if delete:
74 74 if mark is None:
75 75 raise util.Abort(_("bookmark name required"))
76 76 if mark not in marks:
77 77 raise util.Abort(_("a bookmark of this name does not exist"))
78 78 if mark == repo._bookmarkcurrent:
79 79 bookmarks.setcurrent(repo, None)
80 80 del marks[mark]
81 81 bookmarks.write(repo)
82 82 return
83 83
84 84 if mark is not None:
85 85 if "\n" in mark:
86 86 raise util.Abort(_("bookmark name cannot contain newlines"))
87 87 mark = mark.strip()
88 88 if not mark:
89 89 raise util.Abort(_("bookmark names cannot consist entirely of "
90 90 "whitespace"))
91 91 if mark in marks and not force:
92 92 raise util.Abort(_("a bookmark of the same name already exists"))
93 93 if ((mark in repo.branchtags() or mark == repo.dirstate.branch())
94 94 and not force):
95 95 raise util.Abort(
96 96 _("a bookmark cannot have the name of an existing branch"))
97 97 if rev:
98 98 marks[mark] = repo.lookup(rev)
99 99 else:
100 100 marks[mark] = repo.changectx('.').node()
101 101 bookmarks.setcurrent(repo, mark)
102 102 bookmarks.write(repo)
103 103 return
104 104
105 105 if mark is None:
106 106 if rev:
107 107 raise util.Abort(_("bookmark name required"))
108 108 if len(marks) == 0:
109 109 ui.status(_("no bookmarks set\n"))
110 110 else:
111 111 for bmark, n in marks.iteritems():
112 112 if ui.configbool('bookmarks', 'track.current'):
113 113 current = repo._bookmarkcurrent
114 114 if bmark == current and n == cur:
115 115 prefix, label = '*', 'bookmarks.current'
116 116 else:
117 117 prefix, label = ' ', ''
118 118 else:
119 119 if n == cur:
120 120 prefix, label = '*', 'bookmarks.current'
121 121 else:
122 122 prefix, label = ' ', ''
123 123
124 124 if ui.quiet:
125 125 ui.write("%s\n" % bmark, label=label)
126 126 else:
127 127 ui.write(" %s %-25s %d:%s\n" % (
128 128 prefix, bmark, repo.changelog.rev(n), hexfn(n)),
129 129 label=label)
130 130 return
131 131
132 132 def _revstostrip(changelog, node):
133 133 srev = changelog.rev(node)
134 134 tostrip = [srev]
135 135 saveheads = []
136 136 for r in xrange(srev, len(changelog)):
137 137 parents = changelog.parentrevs(r)
138 138 if parents[0] in tostrip or parents[1] in tostrip:
139 139 tostrip.append(r)
140 140 if parents[1] != nullrev:
141 141 for p in parents:
142 142 if p not in tostrip and p > srev:
143 143 saveheads.append(p)
144 144 return [r for r in tostrip if r not in saveheads]
145 145
146 146 def strip(oldstrip, ui, repo, node, backup="all"):
147 147 """Strip bookmarks if revisions are stripped using
148 148 the mercurial.strip method. This usually happens during
149 149 qpush and qpop"""
150 150 revisions = _revstostrip(repo.changelog, node)
151 151 marks = repo._bookmarks
152 152 update = []
153 153 for mark, n in marks.iteritems():
154 154 if repo.changelog.rev(n) in revisions:
155 155 update.append(mark)
156 156 oldstrip(ui, repo, node, backup)
157 157 if len(update) > 0:
158 158 for m in update:
159 159 marks[m] = repo.changectx('.').node()
160 160 bookmarks.write(repo)
161 161
162 162 def reposetup(ui, repo):
163 163 if not repo.local():
164 164 return
165 165
166 166 class bookmark_repo(repo.__class__):
167 167
168 168 @util.propertycache
169 169 def _bookmarks(self):
170 '''Parse .hg/bookmarks file and return a dictionary
171
172 Bookmarks are stored as {HASH}\\s{NAME}\\n (localtags format) values
173 in the .hg/bookmarks file.
174 Read the file and return a (name=>nodeid) dictionary
175 '''
176 try:
177 bookmarks = {}
178 for line in self.opener('bookmarks'):
179 sha, refspec = line.strip().split(' ', 1)
180 refspec = encoding.tolocal(refspec)
181 bookmarks[refspec] = self.changelog.lookup(sha)
182 except:
183 pass
184 return bookmarks
170 return bookmarks.read(self)
185 171
186 172 @util.propertycache
187 173 def _bookmarkcurrent(self):
188 '''Get the current bookmark
189
190 If we use gittishsh branches we have a current bookmark that
191 we are on. This function returns the name of the bookmark. It
192 is stored in .hg/bookmarks.current
193 '''
194 mark = None
195 if os.path.exists(self.join('bookmarks.current')):
196 file = self.opener('bookmarks.current')
197 # No readline() in posixfile_nt, reading everything is cheap
198 mark = (file.readlines() or [''])[0]
199 if mark == '':
200 mark = None
201 file.close()
202 return mark
174 return bookmarks.readcurrent(self)
203 175
204 176 def rollback(self, dryrun=False):
205 177 if os.path.exists(self.join('undo.bookmarks')):
206 178 if not dryrun:
207 179 util.rename(self.join('undo.bookmarks'), self.join('bookmarks'))
208 180 elif not os.path.exists(self.sjoin("undo")):
209 181 # avoid "no rollback information available" message
210 182 return 0
211 183 return super(bookmark_repo, self).rollback(dryrun)
212 184
213 185 def lookup(self, key):
214 186 if key in self._bookmarks:
215 187 key = self._bookmarks[key]
216 188 return super(bookmark_repo, self).lookup(key)
217 189
218 190 def _bookmarksupdate(self, parents, node):
219 191 marks = self._bookmarks
220 192 update = False
221 193 if ui.configbool('bookmarks', 'track.current'):
222 194 mark = self._bookmarkcurrent
223 195 if mark and marks[mark] in parents:
224 196 marks[mark] = node
225 197 update = True
226 198 else:
227 199 for mark, n in marks.items():
228 200 if n in parents:
229 201 marks[mark] = node
230 202 update = True
231 203 if update:
232 204 bookmarks.write(self)
233 205
234 206 def commitctx(self, ctx, error=False):
235 207 """Add a revision to the repository and
236 208 move the bookmark"""
237 209 wlock = self.wlock() # do both commit and bookmark with lock held
238 210 try:
239 211 node = super(bookmark_repo, self).commitctx(ctx, error)
240 212 if node is None:
241 213 return None
242 214 parents = self.changelog.parents(node)
243 215 if parents[1] == nullid:
244 216 parents = (parents[0],)
245 217
246 218 self._bookmarksupdate(parents, node)
247 219 return node
248 220 finally:
249 221 wlock.release()
250 222
251 223 def pull(self, remote, heads=None, force=False):
252 224 result = super(bookmark_repo, self).pull(remote, heads, force)
253 225
254 226 self.ui.debug("checking for updated bookmarks\n")
255 227 rb = remote.listkeys('bookmarks')
256 228 changed = False
257 229 for k in rb.keys():
258 230 if k in self._bookmarks:
259 231 nr, nl = rb[k], self._bookmarks[k]
260 232 if nr in self:
261 233 cr = self[nr]
262 234 cl = self[nl]
263 235 if cl.rev() >= cr.rev():
264 236 continue
265 237 if cr in cl.descendants():
266 238 self._bookmarks[k] = cr.node()
267 239 changed = True
268 240 self.ui.status(_("updating bookmark %s\n") % k)
269 241 else:
270 242 self.ui.warn(_("not updating divergent"
271 243 " bookmark %s\n") % k)
272 244 if changed:
273 245 bookmarks.write(repo)
274 246
275 247 return result
276 248
277 249 def push(self, remote, force=False, revs=None, newbranch=False):
278 250 result = super(bookmark_repo, self).push(remote, force, revs,
279 251 newbranch)
280 252
281 253 self.ui.debug("checking for updated bookmarks\n")
282 254 rb = remote.listkeys('bookmarks')
283 255 for k in rb.keys():
284 256 if k in self._bookmarks:
285 257 nr, nl = rb[k], hex(self._bookmarks[k])
286 258 if nr in self:
287 259 cr = self[nr]
288 260 cl = self[nl]
289 261 if cl in cr.descendants():
290 262 r = remote.pushkey('bookmarks', k, nr, nl)
291 263 if r:
292 264 self.ui.status(_("updating bookmark %s\n") % k)
293 265 else:
294 266 self.ui.warn(_('updating bookmark %s'
295 267 ' failed!\n') % k)
296 268
297 269 return result
298 270
299 271 def addchangegroup(self, *args, **kwargs):
300 272 result = super(bookmark_repo, self).addchangegroup(*args, **kwargs)
301 273 if result > 1:
302 274 # We have more heads than before
303 275 return result
304 276 node = self.changelog.tip()
305 277 parents = self.dirstate.parents()
306 278 self._bookmarksupdate(parents, node)
307 279 return result
308 280
309 281 def _findtags(self):
310 282 """Merge bookmarks with normal tags"""
311 283 (tags, tagtypes) = super(bookmark_repo, self)._findtags()
312 284 tags.update(self._bookmarks)
313 285 return (tags, tagtypes)
314 286
315 287 if hasattr(repo, 'invalidate'):
316 288 def invalidate(self):
317 289 super(bookmark_repo, self).invalidate()
318 290 for attr in ('_bookmarks', '_bookmarkcurrent'):
319 291 if attr in self.__dict__:
320 292 delattr(self, attr)
321 293
322 294 repo.__class__ = bookmark_repo
323 295
324 296 def listbookmarks(repo):
325 297 # We may try to list bookmarks on a repo type that does not
326 298 # support it (e.g., statichttprepository).
327 299 if not hasattr(repo, '_bookmarks'):
328 300 return {}
329 301
330 302 d = {}
331 303 for k, v in repo._bookmarks.iteritems():
332 304 d[k] = hex(v)
333 305 return d
334 306
335 307 def pushbookmark(repo, key, old, new):
336 308 w = repo.wlock()
337 309 try:
338 310 marks = repo._bookmarks
339 311 if hex(marks.get(key, '')) != old:
340 312 return False
341 313 if new == '':
342 314 del marks[key]
343 315 else:
344 316 if new not in repo:
345 317 return False
346 318 marks[key] = repo[new].node()
347 319 bookmarks.write(repo)
348 320 return True
349 321 finally:
350 322 w.release()
351 323
352 324 def pull(oldpull, ui, repo, source="default", **opts):
353 325 # translate bookmark args to rev args for actual pull
354 326 if opts.get('bookmark'):
355 327 # this is an unpleasant hack as pull will do this internally
356 328 source, branches = hg.parseurl(ui.expandpath(source),
357 329 opts.get('branch'))
358 330 other = hg.repository(hg.remoteui(repo, opts), source)
359 331 rb = other.listkeys('bookmarks')
360 332
361 333 for b in opts['bookmark']:
362 334 if b not in rb:
363 335 raise util.Abort(_('remote bookmark %s not found!') % b)
364 336 opts.setdefault('rev', []).append(b)
365 337
366 338 result = oldpull(ui, repo, source, **opts)
367 339
368 340 # update specified bookmarks
369 341 if opts.get('bookmark'):
370 342 for b in opts['bookmark']:
371 343 # explicit pull overrides local bookmark if any
372 344 ui.status(_("importing bookmark %s\n") % b)
373 345 repo._bookmarks[b] = repo[rb[b]].node()
374 346 bookmarks.write(repo)
375 347
376 348 return result
377 349
378 350 def push(oldpush, ui, repo, dest=None, **opts):
379 351 dopush = True
380 352 if opts.get('bookmark'):
381 353 dopush = False
382 354 for b in opts['bookmark']:
383 355 if b in repo._bookmarks:
384 356 dopush = True
385 357 opts.setdefault('rev', []).append(b)
386 358
387 359 result = 0
388 360 if dopush:
389 361 result = oldpush(ui, repo, dest, **opts)
390 362
391 363 if opts.get('bookmark'):
392 364 # this is an unpleasant hack as push will do this internally
393 365 dest = ui.expandpath(dest or 'default-push', dest or 'default')
394 366 dest, branches = hg.parseurl(dest, opts.get('branch'))
395 367 other = hg.repository(hg.remoteui(repo, opts), dest)
396 368 rb = other.listkeys('bookmarks')
397 369 for b in opts['bookmark']:
398 370 # explicit push overrides remote bookmark if any
399 371 if b in repo._bookmarks:
400 372 ui.status(_("exporting bookmark %s\n") % b)
401 373 new = repo[b].hex()
402 374 elif b in rb:
403 375 ui.status(_("deleting remote bookmark %s\n") % b)
404 376 new = '' # delete
405 377 else:
406 378 ui.warn(_('bookmark %s does not exist on the local '
407 379 'or remote repository!\n') % b)
408 380 return 2
409 381 old = rb.get(b, '')
410 382 r = other.pushkey('bookmarks', b, old, new)
411 383 if not r:
412 384 ui.warn(_('updating bookmark %s failed!\n') % b)
413 385 if not result:
414 386 result = 2
415 387
416 388 return result
417 389
418 390 def diffbookmarks(ui, repo, remote):
419 391 ui.status(_("searching for changed bookmarks\n"))
420 392
421 393 lmarks = repo.listkeys('bookmarks')
422 394 rmarks = remote.listkeys('bookmarks')
423 395
424 396 diff = sorted(set(rmarks) - set(lmarks))
425 397 for k in diff:
426 398 ui.write(" %-25s %s\n" % (k, rmarks[k][:12]))
427 399
428 400 if len(diff) <= 0:
429 401 ui.status(_("no changed bookmarks found\n"))
430 402 return 1
431 403 return 0
432 404
433 405 def incoming(oldincoming, ui, repo, source="default", **opts):
434 406 if opts.get('bookmarks'):
435 407 source, branches = hg.parseurl(ui.expandpath(source), opts.get('branch'))
436 408 other = hg.repository(hg.remoteui(repo, opts), source)
437 409 ui.status(_('comparing with %s\n') % url.hidepassword(source))
438 410 return diffbookmarks(ui, repo, other)
439 411 else:
440 412 return oldincoming(ui, repo, source, **opts)
441 413
442 414 def outgoing(oldoutgoing, ui, repo, dest=None, **opts):
443 415 if opts.get('bookmarks'):
444 416 dest = ui.expandpath(dest or 'default-push', dest or 'default')
445 417 dest, branches = hg.parseurl(dest, opts.get('branch'))
446 418 other = hg.repository(hg.remoteui(repo, opts), dest)
447 419 ui.status(_('comparing with %s\n') % url.hidepassword(dest))
448 420 return diffbookmarks(ui, other, repo)
449 421 else:
450 422 return oldoutgoing(ui, repo, dest, **opts)
451 423
452 424 def uisetup(ui):
453 425 extensions.wrapfunction(repair, "strip", strip)
454 426 if ui.configbool('bookmarks', 'track.current'):
455 427 extensions.wrapcommand(commands.table, 'update', updatecurbookmark)
456 428
457 429 entry = extensions.wrapcommand(commands.table, 'pull', pull)
458 430 entry[1].append(('B', 'bookmark', [],
459 431 _("bookmark to import"),
460 432 _('BOOKMARK')))
461 433 entry = extensions.wrapcommand(commands.table, 'push', push)
462 434 entry[1].append(('B', 'bookmark', [],
463 435 _("bookmark to export"),
464 436 _('BOOKMARK')))
465 437 entry = extensions.wrapcommand(commands.table, 'incoming', incoming)
466 438 entry[1].append(('B', 'bookmarks', False,
467 439 _("compare bookmark")))
468 440 entry = extensions.wrapcommand(commands.table, 'outgoing', outgoing)
469 441 entry[1].append(('B', 'bookmarks', False,
470 442 _("compare bookmark")))
471 443
472 444 pushkey.register('bookmarks', pushbookmark, listbookmarks)
473 445
474 446 def updatecurbookmark(orig, ui, repo, *args, **opts):
475 447 '''Set the current bookmark
476 448
477 449 If the user updates to a bookmark we update the .hg/bookmarks.current
478 450 file.
479 451 '''
480 452 res = orig(ui, repo, *args, **opts)
481 453 rev = opts['rev']
482 454 if not rev and len(args) > 0:
483 455 rev = args[0]
484 456 bookmarks.setcurrent(repo, rev)
485 457 return res
486 458
487 459 def bmrevset(repo, subset, x):
488 460 """``bookmark([name])``
489 461 The named bookmark or all bookmarks.
490 462 """
491 463 # i18n: "bookmark" is a keyword
492 464 args = revset.getargs(x, 0, 1, _('bookmark takes one or no arguments'))
493 465 if args:
494 466 bm = revset.getstring(args[0],
495 467 # i18n: "bookmark" is a keyword
496 468 _('the argument to bookmark must be a string'))
497 469 bmrev = listbookmarks(repo).get(bm, None)
498 470 if bmrev:
499 471 bmrev = repo.changelog.rev(bin(bmrev))
500 472 return [r for r in subset if r == bmrev]
501 473 bms = set([repo.changelog.rev(bin(r)) for r in listbookmarks(repo).values()])
502 474 return [r for r in subset if r in bms]
503 475
504 476 def extsetup(ui):
505 477 revset.symbols['bookmark'] = bmrevset
506 478
507 479 cmdtable = {
508 480 "bookmarks":
509 481 (bookmark,
510 482 [('f', 'force', False, _('force')),
511 483 ('r', 'rev', '', _('revision'), _('REV')),
512 484 ('d', 'delete', False, _('delete a given bookmark')),
513 485 ('m', 'rename', '', _('rename a given bookmark'), _('NAME'))],
514 486 _('hg bookmarks [-f] [-d] [-m NAME] [-r REV] [NAME]')),
515 487 }
516 488
517 489 colortable = {'bookmarks.current': 'green'}
518 490
519 491 # tell hggettext to extract docstrings from these functions:
520 492 i18nfunctions = [bmrevset]
@@ -1,73 +1,107
1 1 # Mercurial bookmark support code
2 2 #
3 3 # Copyright 2008 David Soria Parra <dsp@php.net>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from mercurial.i18n import _
9 9 from mercurial.node import nullid, nullrev, bin, hex, short
10 10 from mercurial import encoding
11 11 import os
12 12
13 def read(repo):
14 '''Parse .hg/bookmarks file and return a dictionary
15
16 Bookmarks are stored as {HASH}\\s{NAME}\\n (localtags format) values
17 in the .hg/bookmarks file.
18 Read the file and return a (name=>nodeid) dictionary
19 '''
20 try:
21 bookmarks = {}
22 for line in repo.opener('bookmarks'):
23 sha, refspec = line.strip().split(' ', 1)
24 refspec = encoding.tolocal(refspec)
25 bookmarks[refspec] = repo.changelog.lookup(sha)
26 except:
27 pass
28 return bookmarks
29
30 def readcurrent(repo):
31 '''Get the current bookmark
32
33 If we use gittishsh branches we have a current bookmark that
34 we are on. This function returns the name of the bookmark. It
35 is stored in .hg/bookmarks.current
36 '''
37 mark = None
38 if os.path.exists(repo.join('bookmarks.current')):
39 file = repo.opener('bookmarks.current')
40 # No readline() in posixfile_nt, reading everything is cheap
41 mark = (file.readlines() or [''])[0]
42 if mark == '':
43 mark = None
44 file.close()
45 return mark
46
13 47 def write(repo):
14 48 '''Write bookmarks
15 49
16 50 Write the given bookmark => hash dictionary to the .hg/bookmarks file
17 51 in a format equal to those of localtags.
18 52
19 53 We also store a backup of the previous state in undo.bookmarks that
20 54 can be copied back on rollback.
21 55 '''
22 56 refs = repo._bookmarks
23 57
24 58 try:
25 59 bms = repo.opener('bookmarks').read()
26 60 except IOError:
27 61 bms = ''
28 62 repo.opener('undo.bookmarks', 'w').write(bms)
29 63
30 64 if repo._bookmarkcurrent not in refs:
31 65 setcurrent(repo, None)
32 66 wlock = repo.wlock()
33 67 try:
34 68 file = repo.opener('bookmarks', 'w', atomictemp=True)
35 69 for refspec, node in refs.iteritems():
36 70 file.write("%s %s\n" % (hex(node), encoding.fromlocal(refspec)))
37 71 file.rename()
38 72
39 73 # touch 00changelog.i so hgweb reloads bookmarks (no lock needed)
40 74 try:
41 75 os.utime(repo.sjoin('00changelog.i'), None)
42 76 except OSError:
43 77 pass
44 78
45 79 finally:
46 80 wlock.release()
47 81
48 82 def setcurrent(repo, mark):
49 83 '''Set the name of the bookmark that we are currently on
50 84
51 85 Set the name of the bookmark that we are on (hg update <bookmark>).
52 86 The name is recorded in .hg/bookmarks.current
53 87 '''
54 88 current = repo._bookmarkcurrent
55 89 if current == mark:
56 90 return
57 91
58 92 refs = repo._bookmarks
59 93
60 94 # do not update if we do update to a rev equal to the current bookmark
61 95 if (mark and mark not in refs and
62 96 current and refs[current] == repo.changectx('.').node()):
63 97 return
64 98 if mark not in refs:
65 99 mark = ''
66 100 wlock = repo.wlock()
67 101 try:
68 102 file = repo.opener('bookmarks.current', 'w', atomictemp=True)
69 103 file.write(mark)
70 104 file.rename()
71 105 finally:
72 106 wlock.release()
73 107 repo._bookmarkcurrent = mark
General Comments 0
You need to be logged in to leave comments. Login now