##// END OF EJS Templates
fileset: keep basectx by matchctx
Yuya Nishihara -
r38913:8d6780f0 default
parent child Browse files
Show More
@@ -1,570 +1,572 b''
1 1 # fileset.py - file set queries for mercurial
2 2 #
3 3 # Copyright 2010 Matt Mackall <mpm@selenic.com>
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 __future__ import absolute_import
9 9
10 10 import errno
11 11 import re
12 12
13 13 from .i18n import _
14 14 from . import (
15 15 error,
16 16 filesetlang,
17 17 match as matchmod,
18 18 merge,
19 19 pycompat,
20 20 registrar,
21 21 scmutil,
22 22 util,
23 23 )
24 24 from .utils import (
25 25 stringutil,
26 26 )
27 27
28 28 # common weight constants
29 29 _WEIGHT_CHECK_FILENAME = filesetlang.WEIGHT_CHECK_FILENAME
30 30 _WEIGHT_READ_CONTENTS = filesetlang.WEIGHT_READ_CONTENTS
31 31 _WEIGHT_STATUS = filesetlang.WEIGHT_STATUS
32 32 _WEIGHT_STATUS_THOROUGH = filesetlang.WEIGHT_STATUS_THOROUGH
33 33
34 34 # helpers for processing parsed tree
35 35 getsymbol = filesetlang.getsymbol
36 36 getstring = filesetlang.getstring
37 37 _getkindpat = filesetlang.getkindpat
38 38 getpattern = filesetlang.getpattern
39 39 getargs = filesetlang.getargs
40 40
41 41 def getmatch(mctx, x):
42 42 if not x:
43 43 raise error.ParseError(_("missing argument"))
44 44 return methods[x[0]](mctx, *x[1:])
45 45
46 46 def stringmatch(mctx, x):
47 47 return mctx.matcher([x])
48 48
49 49 def kindpatmatch(mctx, x, y):
50 50 return stringmatch(mctx, _getkindpat(x, y, matchmod.allpatternkinds,
51 51 _("pattern must be a string")))
52 52
53 53 def patternsmatch(mctx, *xs):
54 54 allkinds = matchmod.allpatternkinds
55 55 patterns = [getpattern(x, allkinds, _("pattern must be a string"))
56 56 for x in xs]
57 57 return mctx.matcher(patterns)
58 58
59 59 def andmatch(mctx, x, y):
60 60 xm = getmatch(mctx, x)
61 61 ym = getmatch(mctx, y)
62 62 return matchmod.intersectmatchers(xm, ym)
63 63
64 64 def ormatch(mctx, *xs):
65 65 ms = [getmatch(mctx, x) for x in xs]
66 66 return matchmod.unionmatcher(ms)
67 67
68 68 def notmatch(mctx, x):
69 69 m = getmatch(mctx, x)
70 70 return mctx.predicate(lambda f: not m(f), predrepr=('<not %r>', m))
71 71
72 72 def minusmatch(mctx, x, y):
73 73 xm = getmatch(mctx, x)
74 74 ym = getmatch(mctx, y)
75 75 return matchmod.differencematcher(xm, ym)
76 76
77 77 def listmatch(mctx, *xs):
78 78 raise error.ParseError(_("can't use a list in this context"),
79 79 hint=_('see \'hg help "filesets.x or y"\''))
80 80
81 81 def func(mctx, a, b):
82 82 funcname = getsymbol(a)
83 83 if funcname in symbols:
84 84 return symbols[funcname](mctx, b)
85 85
86 86 keep = lambda fn: getattr(fn, '__doc__', None) is not None
87 87
88 88 syms = [s for (s, fn) in symbols.items() if keep(fn)]
89 89 raise error.UnknownIdentifier(funcname, syms)
90 90
91 91 # symbols are callable like:
92 92 # fun(mctx, x)
93 93 # with:
94 94 # mctx - current matchctx instance
95 95 # x - argument in tree form
96 96 symbols = filesetlang.symbols
97 97
98 98 # filesets using matchctx.status()
99 99 _statuscallers = set()
100 100
101 101 predicate = registrar.filesetpredicate()
102 102
103 103 @predicate('modified()', callstatus=True, weight=_WEIGHT_STATUS)
104 104 def modified(mctx, x):
105 105 """File that is modified according to :hg:`status`.
106 106 """
107 107 # i18n: "modified" is a keyword
108 108 getargs(x, 0, 0, _("modified takes no arguments"))
109 109 s = set(mctx.status().modified)
110 110 return mctx.predicate(s.__contains__, predrepr='modified')
111 111
112 112 @predicate('added()', callstatus=True, weight=_WEIGHT_STATUS)
113 113 def added(mctx, x):
114 114 """File that is added according to :hg:`status`.
115 115 """
116 116 # i18n: "added" is a keyword
117 117 getargs(x, 0, 0, _("added takes no arguments"))
118 118 s = set(mctx.status().added)
119 119 return mctx.predicate(s.__contains__, predrepr='added')
120 120
121 121 @predicate('removed()', callstatus=True, weight=_WEIGHT_STATUS)
122 122 def removed(mctx, x):
123 123 """File that is removed according to :hg:`status`.
124 124 """
125 125 # i18n: "removed" is a keyword
126 126 getargs(x, 0, 0, _("removed takes no arguments"))
127 127 s = set(mctx.status().removed)
128 128 return mctx.predicate(s.__contains__, predrepr='removed')
129 129
130 130 @predicate('deleted()', callstatus=True, weight=_WEIGHT_STATUS)
131 131 def deleted(mctx, x):
132 132 """Alias for ``missing()``.
133 133 """
134 134 # i18n: "deleted" is a keyword
135 135 getargs(x, 0, 0, _("deleted takes no arguments"))
136 136 s = set(mctx.status().deleted)
137 137 return mctx.predicate(s.__contains__, predrepr='deleted')
138 138
139 139 @predicate('missing()', callstatus=True, weight=_WEIGHT_STATUS)
140 140 def missing(mctx, x):
141 141 """File that is missing according to :hg:`status`.
142 142 """
143 143 # i18n: "missing" is a keyword
144 144 getargs(x, 0, 0, _("missing takes no arguments"))
145 145 s = set(mctx.status().deleted)
146 146 return mctx.predicate(s.__contains__, predrepr='deleted')
147 147
148 148 @predicate('unknown()', callstatus=True, weight=_WEIGHT_STATUS_THOROUGH)
149 149 def unknown(mctx, x):
150 150 """File that is unknown according to :hg:`status`."""
151 151 # i18n: "unknown" is a keyword
152 152 getargs(x, 0, 0, _("unknown takes no arguments"))
153 153 s = set(mctx.status().unknown)
154 154 return mctx.predicate(s.__contains__, predrepr='unknown')
155 155
156 156 @predicate('ignored()', callstatus=True, weight=_WEIGHT_STATUS_THOROUGH)
157 157 def ignored(mctx, x):
158 158 """File that is ignored according to :hg:`status`."""
159 159 # i18n: "ignored" is a keyword
160 160 getargs(x, 0, 0, _("ignored takes no arguments"))
161 161 s = set(mctx.status().ignored)
162 162 return mctx.predicate(s.__contains__, predrepr='ignored')
163 163
164 164 @predicate('clean()', callstatus=True, weight=_WEIGHT_STATUS)
165 165 def clean(mctx, x):
166 166 """File that is clean according to :hg:`status`.
167 167 """
168 168 # i18n: "clean" is a keyword
169 169 getargs(x, 0, 0, _("clean takes no arguments"))
170 170 s = set(mctx.status().clean)
171 171 return mctx.predicate(s.__contains__, predrepr='clean')
172 172
173 173 @predicate('tracked()')
174 174 def tracked(mctx, x):
175 175 """File that is under Mercurial control."""
176 176 # i18n: "tracked" is a keyword
177 177 getargs(x, 0, 0, _("tracked takes no arguments"))
178 178 return mctx.predicate(mctx.ctx.__contains__, predrepr='tracked')
179 179
180 180 @predicate('binary()', weight=_WEIGHT_READ_CONTENTS)
181 181 def binary(mctx, x):
182 182 """File that appears to be binary (contains NUL bytes).
183 183 """
184 184 # i18n: "binary" is a keyword
185 185 getargs(x, 0, 0, _("binary takes no arguments"))
186 186 return mctx.fpredicate(lambda fctx: fctx.isbinary(),
187 187 predrepr='binary', cache=True)
188 188
189 189 @predicate('exec()')
190 190 def exec_(mctx, x):
191 191 """File that is marked as executable.
192 192 """
193 193 # i18n: "exec" is a keyword
194 194 getargs(x, 0, 0, _("exec takes no arguments"))
195 195 ctx = mctx.ctx
196 196 return mctx.predicate(lambda f: ctx.flags(f) == 'x', predrepr='exec')
197 197
198 198 @predicate('symlink()')
199 199 def symlink(mctx, x):
200 200 """File that is marked as a symlink.
201 201 """
202 202 # i18n: "symlink" is a keyword
203 203 getargs(x, 0, 0, _("symlink takes no arguments"))
204 204 ctx = mctx.ctx
205 205 return mctx.predicate(lambda f: ctx.flags(f) == 'l', predrepr='symlink')
206 206
207 207 @predicate('resolved()', weight=_WEIGHT_STATUS)
208 208 def resolved(mctx, x):
209 209 """File that is marked resolved according to :hg:`resolve -l`.
210 210 """
211 211 # i18n: "resolved" is a keyword
212 212 getargs(x, 0, 0, _("resolved takes no arguments"))
213 213 if mctx.ctx.rev() is not None:
214 214 return mctx.never()
215 215 ms = merge.mergestate.read(mctx.ctx.repo())
216 216 return mctx.predicate(lambda f: f in ms and ms[f] == 'r',
217 217 predrepr='resolved')
218 218
219 219 @predicate('unresolved()', weight=_WEIGHT_STATUS)
220 220 def unresolved(mctx, x):
221 221 """File that is marked unresolved according to :hg:`resolve -l`.
222 222 """
223 223 # i18n: "unresolved" is a keyword
224 224 getargs(x, 0, 0, _("unresolved takes no arguments"))
225 225 if mctx.ctx.rev() is not None:
226 226 return mctx.never()
227 227 ms = merge.mergestate.read(mctx.ctx.repo())
228 228 return mctx.predicate(lambda f: f in ms and ms[f] == 'u',
229 229 predrepr='unresolved')
230 230
231 231 @predicate('hgignore()', weight=_WEIGHT_STATUS)
232 232 def hgignore(mctx, x):
233 233 """File that matches the active .hgignore pattern.
234 234 """
235 235 # i18n: "hgignore" is a keyword
236 236 getargs(x, 0, 0, _("hgignore takes no arguments"))
237 237 return mctx.ctx.repo().dirstate._ignore
238 238
239 239 @predicate('portable()', weight=_WEIGHT_CHECK_FILENAME)
240 240 def portable(mctx, x):
241 241 """File that has a portable name. (This doesn't include filenames with case
242 242 collisions.)
243 243 """
244 244 # i18n: "portable" is a keyword
245 245 getargs(x, 0, 0, _("portable takes no arguments"))
246 246 return mctx.predicate(lambda f: util.checkwinfilename(f) is None,
247 247 predrepr='portable')
248 248
249 249 @predicate('grep(regex)', weight=_WEIGHT_READ_CONTENTS)
250 250 def grep(mctx, x):
251 251 """File contains the given regular expression.
252 252 """
253 253 try:
254 254 # i18n: "grep" is a keyword
255 255 r = re.compile(getstring(x, _("grep requires a pattern")))
256 256 except re.error as e:
257 257 raise error.ParseError(_('invalid match pattern: %s') %
258 258 stringutil.forcebytestr(e))
259 259 return mctx.fpredicate(lambda fctx: r.search(fctx.data()),
260 260 predrepr=('grep(%r)', r.pattern), cache=True)
261 261
262 262 def _sizetomax(s):
263 263 try:
264 264 s = s.strip().lower()
265 265 for k, v in util._sizeunits:
266 266 if s.endswith(k):
267 267 # max(4k) = 5k - 1, max(4.5k) = 4.6k - 1
268 268 n = s[:-len(k)]
269 269 inc = 1.0
270 270 if "." in n:
271 271 inc /= 10 ** len(n.split(".")[1])
272 272 return int((float(n) + inc) * v) - 1
273 273 # no extension, this is a precise value
274 274 return int(s)
275 275 except ValueError:
276 276 raise error.ParseError(_("couldn't parse size: %s") % s)
277 277
278 278 def sizematcher(expr):
279 279 """Return a function(size) -> bool from the ``size()`` expression"""
280 280 expr = expr.strip()
281 281 if '-' in expr: # do we have a range?
282 282 a, b = expr.split('-', 1)
283 283 a = util.sizetoint(a)
284 284 b = util.sizetoint(b)
285 285 return lambda x: x >= a and x <= b
286 286 elif expr.startswith("<="):
287 287 a = util.sizetoint(expr[2:])
288 288 return lambda x: x <= a
289 289 elif expr.startswith("<"):
290 290 a = util.sizetoint(expr[1:])
291 291 return lambda x: x < a
292 292 elif expr.startswith(">="):
293 293 a = util.sizetoint(expr[2:])
294 294 return lambda x: x >= a
295 295 elif expr.startswith(">"):
296 296 a = util.sizetoint(expr[1:])
297 297 return lambda x: x > a
298 298 else:
299 299 a = util.sizetoint(expr)
300 300 b = _sizetomax(expr)
301 301 return lambda x: x >= a and x <= b
302 302
303 303 @predicate('size(expression)', weight=_WEIGHT_STATUS)
304 304 def size(mctx, x):
305 305 """File size matches the given expression. Examples:
306 306
307 307 - size('1k') - files from 1024 to 2047 bytes
308 308 - size('< 20k') - files less than 20480 bytes
309 309 - size('>= .5MB') - files at least 524288 bytes
310 310 - size('4k - 1MB') - files from 4096 bytes to 1048576 bytes
311 311 """
312 312 # i18n: "size" is a keyword
313 313 expr = getstring(x, _("size requires an expression"))
314 314 m = sizematcher(expr)
315 315 return mctx.fpredicate(lambda fctx: m(fctx.size()),
316 316 predrepr=('size(%r)', expr), cache=True)
317 317
318 318 @predicate('encoding(name)', weight=_WEIGHT_READ_CONTENTS)
319 319 def encoding(mctx, x):
320 320 """File can be successfully decoded with the given character
321 321 encoding. May not be useful for encodings other than ASCII and
322 322 UTF-8.
323 323 """
324 324
325 325 # i18n: "encoding" is a keyword
326 326 enc = getstring(x, _("encoding requires an encoding name"))
327 327
328 328 def encp(fctx):
329 329 d = fctx.data()
330 330 try:
331 331 d.decode(pycompat.sysstr(enc))
332 332 return True
333 333 except LookupError:
334 334 raise error.Abort(_("unknown encoding '%s'") % enc)
335 335 except UnicodeDecodeError:
336 336 return False
337 337
338 338 return mctx.fpredicate(encp, predrepr=('encoding(%r)', enc), cache=True)
339 339
340 340 @predicate('eol(style)', weight=_WEIGHT_READ_CONTENTS)
341 341 def eol(mctx, x):
342 342 """File contains newlines of the given style (dos, unix, mac). Binary
343 343 files are excluded, files with mixed line endings match multiple
344 344 styles.
345 345 """
346 346
347 347 # i18n: "eol" is a keyword
348 348 enc = getstring(x, _("eol requires a style name"))
349 349
350 350 def eolp(fctx):
351 351 if fctx.isbinary():
352 352 return False
353 353 d = fctx.data()
354 354 if (enc == 'dos' or enc == 'win') and '\r\n' in d:
355 355 return True
356 356 elif enc == 'unix' and re.search('(?<!\r)\n', d):
357 357 return True
358 358 elif enc == 'mac' and re.search('\r(?!\n)', d):
359 359 return True
360 360 return False
361 361 return mctx.fpredicate(eolp, predrepr=('eol(%r)', enc), cache=True)
362 362
363 363 @predicate('copied()')
364 364 def copied(mctx, x):
365 365 """File that is recorded as being copied.
366 366 """
367 367 # i18n: "copied" is a keyword
368 368 getargs(x, 0, 0, _("copied takes no arguments"))
369 369 def copiedp(fctx):
370 370 p = fctx.parents()
371 371 return p and p[0].path() != fctx.path()
372 372 return mctx.fpredicate(copiedp, predrepr='copied', cache=True)
373 373
374 374 @predicate('revs(revs, pattern)', weight=_WEIGHT_STATUS)
375 375 def revs(mctx, x):
376 376 """Evaluate set in the specified revisions. If the revset match multiple
377 377 revs, this will return file matching pattern in any of the revision.
378 378 """
379 379 # i18n: "revs" is a keyword
380 380 r, x = getargs(x, 2, 2, _("revs takes two arguments"))
381 381 # i18n: "revs" is a keyword
382 382 revspec = getstring(r, _("first argument to revs must be a revision"))
383 383 repo = mctx.ctx.repo()
384 384 revs = scmutil.revrange(repo, [revspec])
385 385
386 386 matchers = []
387 387 for r in revs:
388 388 ctx = repo[r]
389 mc = mctx.switch(ctx, _buildstatus(ctx.p1(), ctx, x))
389 mc = mctx.switch(ctx.p1(), ctx, _buildstatus(ctx.p1(), ctx, x))
390 390 matchers.append(getmatch(mc, x))
391 391 if not matchers:
392 392 return mctx.never()
393 393 if len(matchers) == 1:
394 394 return matchers[0]
395 395 return matchmod.unionmatcher(matchers)
396 396
397 397 @predicate('status(base, rev, pattern)', weight=_WEIGHT_STATUS)
398 398 def status(mctx, x):
399 399 """Evaluate predicate using status change between ``base`` and
400 400 ``rev``. Examples:
401 401
402 402 - ``status(3, 7, added())`` - matches files added from "3" to "7"
403 403 """
404 404 repo = mctx.ctx.repo()
405 405 # i18n: "status" is a keyword
406 406 b, r, x = getargs(x, 3, 3, _("status takes three arguments"))
407 407 # i18n: "status" is a keyword
408 408 baseerr = _("first argument to status must be a revision")
409 409 baserevspec = getstring(b, baseerr)
410 410 if not baserevspec:
411 411 raise error.ParseError(baseerr)
412 412 reverr = _("second argument to status must be a revision")
413 413 revspec = getstring(r, reverr)
414 414 if not revspec:
415 415 raise error.ParseError(reverr)
416 416 basectx, ctx = scmutil.revpair(repo, [baserevspec, revspec])
417 mc = mctx.switch(ctx, _buildstatus(basectx, ctx, x))
417 mc = mctx.switch(basectx, ctx, _buildstatus(basectx, ctx, x))
418 418 return getmatch(mc, x)
419 419
420 420 @predicate('subrepo([pattern])')
421 421 def subrepo(mctx, x):
422 422 """Subrepositories whose paths match the given pattern.
423 423 """
424 424 # i18n: "subrepo" is a keyword
425 425 getargs(x, 0, 1, _("subrepo takes at most one argument"))
426 426 ctx = mctx.ctx
427 427 sstate = ctx.substate
428 428 if x:
429 429 pat = getpattern(x, matchmod.allpatternkinds,
430 430 # i18n: "subrepo" is a keyword
431 431 _("subrepo requires a pattern or no arguments"))
432 432 fast = not matchmod.patkind(pat)
433 433 if fast:
434 434 def m(s):
435 435 return (s == pat)
436 436 else:
437 437 m = matchmod.match(ctx.repo().root, '', [pat], ctx=ctx)
438 438 return mctx.predicate(lambda f: f in sstate and m(f),
439 439 predrepr=('subrepo(%r)', pat))
440 440 else:
441 441 return mctx.predicate(sstate.__contains__, predrepr='subrepo')
442 442
443 443 methods = {
444 444 'string': stringmatch,
445 445 'symbol': stringmatch,
446 446 'kindpat': kindpatmatch,
447 447 'patterns': patternsmatch,
448 448 'and': andmatch,
449 449 'or': ormatch,
450 450 'minus': minusmatch,
451 451 'list': listmatch,
452 452 'not': notmatch,
453 453 'func': func,
454 454 }
455 455
456 456 class matchctx(object):
457 def __init__(self, ctx, status=None, badfn=None):
457 def __init__(self, basectx, ctx, status=None, badfn=None):
458 self._basectx = basectx
458 459 self.ctx = ctx
459 460 self._status = status
460 461 self._badfn = badfn
461 462
462 463 def status(self):
463 464 return self._status
464 465
465 466 def matcher(self, patterns):
466 467 return self.ctx.match(patterns, badfn=self._badfn)
467 468
468 469 def predicate(self, predfn, predrepr=None, cache=False):
469 470 """Create a matcher to select files by predfn(filename)"""
470 471 if cache:
471 472 predfn = util.cachefunc(predfn)
472 473 repo = self.ctx.repo()
473 474 return matchmod.predicatematcher(repo.root, repo.getcwd(), predfn,
474 475 predrepr=predrepr, badfn=self._badfn)
475 476
476 477 def fpredicate(self, predfn, predrepr=None, cache=False):
477 478 """Create a matcher to select files by predfn(fctx) at the current
478 479 revision
479 480
480 481 Missing files are ignored.
481 482 """
482 483 ctx = self.ctx
483 484 if ctx.rev() is None:
484 485 def fctxpredfn(f):
485 486 try:
486 487 fctx = ctx[f]
487 488 except error.LookupError:
488 489 return False
489 490 try:
490 491 fctx.audit()
491 492 except error.Abort:
492 493 return False
493 494 try:
494 495 return predfn(fctx)
495 496 except (IOError, OSError) as e:
496 497 # open()-ing a directory fails with EACCES on Windows
497 498 if e.errno in (errno.ENOENT, errno.EACCES, errno.ENOTDIR,
498 499 errno.EISDIR):
499 500 return False
500 501 raise
501 502 else:
502 503 def fctxpredfn(f):
503 504 try:
504 505 fctx = ctx[f]
505 506 except error.LookupError:
506 507 return False
507 508 return predfn(fctx)
508 509 return self.predicate(fctxpredfn, predrepr=predrepr, cache=cache)
509 510
510 511 def never(self):
511 512 """Create a matcher to select nothing"""
512 513 repo = self.ctx.repo()
513 514 return matchmod.nevermatcher(repo.root, repo.getcwd(),
514 515 badfn=self._badfn)
515 516
516 def switch(self, ctx, status=None):
517 return matchctx(ctx, status, self._badfn)
517 def switch(self, basectx, ctx, status=None):
518 return matchctx(basectx, ctx, status, self._badfn)
518 519
519 520 # filesets using matchctx.switch()
520 521 _switchcallers = [
521 522 'revs',
522 523 'status',
523 524 ]
524 525
525 526 def _intree(funcs, tree):
526 527 if isinstance(tree, tuple):
527 528 if tree[0] == 'func' and tree[1][0] == 'symbol':
528 529 if tree[1][1] in funcs:
529 530 return True
530 531 if tree[1][1] in _switchcallers:
531 532 # arguments won't be evaluated in the current context
532 533 return False
533 534 for s in tree[1:]:
534 535 if _intree(funcs, s):
535 536 return True
536 537 return False
537 538
538 539 def match(ctx, expr, badfn=None):
539 540 """Create a matcher for a single fileset expression"""
540 541 tree = filesetlang.parse(expr)
541 542 tree = filesetlang.analyze(tree)
542 543 tree = filesetlang.optimize(tree)
543 mctx = matchctx(ctx, _buildstatus(ctx.p1(), ctx, tree), badfn=badfn)
544 mctx = matchctx(ctx.p1(), ctx, _buildstatus(ctx.p1(), ctx, tree),
545 badfn=badfn)
544 546 return getmatch(mctx, tree)
545 547
546 548 def _buildstatus(basectx, ctx, tree):
547 549 # do we need status info?
548 550
549 551 if _intree(_statuscallers, tree):
550 552 unknown = _intree(['unknown'], tree)
551 553 ignored = _intree(['ignored'], tree)
552 554
553 555 return basectx.status(ctx, listunknown=unknown, listignored=ignored,
554 556 listclean=True)
555 557 else:
556 558 return None
557 559
558 560 def loadpredicate(ui, extname, registrarobj):
559 561 """Load fileset predicates from specified registrarobj
560 562 """
561 563 for name, func in registrarobj._table.iteritems():
562 564 symbols[name] = func
563 565 if func._callstatus:
564 566 _statuscallers.add(name)
565 567
566 568 # load built-in predicates explicitly to setup _statuscallers
567 569 loadpredicate(None, None, predicate)
568 570
569 571 # tell hggettext to extract docstrings from these functions:
570 572 i18nfunctions = symbols.values()
General Comments 0
You need to be logged in to leave comments. Login now