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