##// END OF EJS Templates
openpath: specify binary mode and use util.posixfile...
Adrian Buehlmann -
r17900:47fb4806 stable
parent child Browse files
Show More
@@ -1,642 +1,642
1 1 # hg.py - repository classes for mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from i18n import _
10 10 from lock import release
11 11 from node import hex, nullid
12 12 import localrepo, bundlerepo, httppeer, sshpeer, statichttprepo, bookmarks
13 13 import lock, util, extensions, error, node, scmutil, phases, url
14 14 import cmdutil, discovery
15 15 import merge as mergemod
16 16 import verify as verifymod
17 17 import errno, os, shutil
18 18
19 19 def _local(path):
20 20 path = util.expandpath(util.urllocalpath(path))
21 21 return (os.path.isfile(path) and bundlerepo or localrepo)
22 22
23 23 def addbranchrevs(lrepo, other, branches, revs):
24 24 peer = other.peer() # a courtesy to callers using a localrepo for other
25 25 hashbranch, branches = branches
26 26 if not hashbranch and not branches:
27 27 return revs or None, revs and revs[0] or None
28 28 revs = revs and list(revs) or []
29 29 if not peer.capable('branchmap'):
30 30 if branches:
31 31 raise util.Abort(_("remote branch lookup not supported"))
32 32 revs.append(hashbranch)
33 33 return revs, revs[0]
34 34 branchmap = peer.branchmap()
35 35
36 36 def primary(branch):
37 37 if branch == '.':
38 38 if not lrepo:
39 39 raise util.Abort(_("dirstate branch not accessible"))
40 40 branch = lrepo.dirstate.branch()
41 41 if branch in branchmap:
42 42 revs.extend(node.hex(r) for r in reversed(branchmap[branch]))
43 43 return True
44 44 else:
45 45 return False
46 46
47 47 for branch in branches:
48 48 if not primary(branch):
49 49 raise error.RepoLookupError(_("unknown branch '%s'") % branch)
50 50 if hashbranch:
51 51 if not primary(hashbranch):
52 52 revs.append(hashbranch)
53 53 return revs, revs[0]
54 54
55 55 def parseurl(path, branches=None):
56 56 '''parse url#branch, returning (url, (branch, branches))'''
57 57
58 58 u = util.url(path)
59 59 branch = None
60 60 if u.fragment:
61 61 branch = u.fragment
62 62 u.fragment = None
63 63 return str(u), (branch, branches or [])
64 64
65 65 schemes = {
66 66 'bundle': bundlerepo,
67 67 'file': _local,
68 68 'http': httppeer,
69 69 'https': httppeer,
70 70 'ssh': sshpeer,
71 71 'static-http': statichttprepo,
72 72 }
73 73
74 74 def _peerlookup(path):
75 75 u = util.url(path)
76 76 scheme = u.scheme or 'file'
77 77 thing = schemes.get(scheme) or schemes['file']
78 78 try:
79 79 return thing(path)
80 80 except TypeError:
81 81 return thing
82 82
83 83 def islocal(repo):
84 84 '''return true if repo or path is local'''
85 85 if isinstance(repo, str):
86 86 try:
87 87 return _peerlookup(repo).islocal(repo)
88 88 except AttributeError:
89 89 return False
90 90 return repo.local()
91 91
92 92 def openpath(ui, path):
93 93 '''open path with open if local, url.open if remote'''
94 94 if islocal(path):
95 return open(util.urllocalpath(path))
95 return util.posixfile(util.urllocalpath(path), 'rb')
96 96 else:
97 97 return url.open(ui, path)
98 98
99 99 def _peerorrepo(ui, path, create=False):
100 100 """return a repository object for the specified path"""
101 101 obj = _peerlookup(path).instance(ui, path, create)
102 102 ui = getattr(obj, "ui", ui)
103 103 for name, module in extensions.extensions():
104 104 hook = getattr(module, 'reposetup', None)
105 105 if hook:
106 106 hook(ui, obj)
107 107 return obj
108 108
109 109 def repository(ui, path='', create=False):
110 110 """return a repository object for the specified path"""
111 111 peer = _peerorrepo(ui, path, create)
112 112 repo = peer.local()
113 113 if not repo:
114 114 raise util.Abort(_("repository '%s' is not local") %
115 115 (path or peer.url()))
116 116 return repo
117 117
118 118 def peer(uiorrepo, opts, path, create=False):
119 119 '''return a repository peer for the specified path'''
120 120 rui = remoteui(uiorrepo, opts)
121 121 return _peerorrepo(rui, path, create).peer()
122 122
123 123 def defaultdest(source):
124 124 '''return default destination of clone if none is given'''
125 125 return os.path.basename(os.path.normpath(util.url(source).path))
126 126
127 127 def share(ui, source, dest=None, update=True):
128 128 '''create a shared repository'''
129 129
130 130 if not islocal(source):
131 131 raise util.Abort(_('can only share local repositories'))
132 132
133 133 if not dest:
134 134 dest = defaultdest(source)
135 135 else:
136 136 dest = ui.expandpath(dest)
137 137
138 138 if isinstance(source, str):
139 139 origsource = ui.expandpath(source)
140 140 source, branches = parseurl(origsource)
141 141 srcrepo = repository(ui, source)
142 142 rev, checkout = addbranchrevs(srcrepo, srcrepo, branches, None)
143 143 else:
144 144 srcrepo = source.local()
145 145 origsource = source = srcrepo.url()
146 146 checkout = None
147 147
148 148 sharedpath = srcrepo.sharedpath # if our source is already sharing
149 149
150 150 root = os.path.realpath(dest)
151 151 roothg = os.path.join(root, '.hg')
152 152
153 153 if os.path.exists(roothg):
154 154 raise util.Abort(_('destination already exists'))
155 155
156 156 if not os.path.isdir(root):
157 157 os.mkdir(root)
158 158 util.makedir(roothg, notindexed=True)
159 159
160 160 requirements = ''
161 161 try:
162 162 requirements = srcrepo.opener.read('requires')
163 163 except IOError, inst:
164 164 if inst.errno != errno.ENOENT:
165 165 raise
166 166
167 167 requirements += 'shared\n'
168 168 util.writefile(os.path.join(roothg, 'requires'), requirements)
169 169 util.writefile(os.path.join(roothg, 'sharedpath'), sharedpath)
170 170
171 171 r = repository(ui, root)
172 172
173 173 default = srcrepo.ui.config('paths', 'default')
174 174 if default:
175 175 fp = r.opener("hgrc", "w", text=True)
176 176 fp.write("[paths]\n")
177 177 fp.write("default = %s\n" % default)
178 178 fp.close()
179 179
180 180 if update:
181 181 r.ui.status(_("updating working directory\n"))
182 182 if update is not True:
183 183 checkout = update
184 184 for test in (checkout, 'default', 'tip'):
185 185 if test is None:
186 186 continue
187 187 try:
188 188 uprev = r.lookup(test)
189 189 break
190 190 except error.RepoLookupError:
191 191 continue
192 192 _update(r, uprev)
193 193
194 194 def copystore(ui, srcrepo, destpath):
195 195 '''copy files from store of srcrepo in destpath
196 196
197 197 returns destlock
198 198 '''
199 199 destlock = None
200 200 try:
201 201 hardlink = None
202 202 num = 0
203 203 srcpublishing = srcrepo.ui.configbool('phases', 'publish', True)
204 204 for f in srcrepo.store.copylist():
205 205 if srcpublishing and f.endswith('phaseroots'):
206 206 continue
207 207 src = os.path.join(srcrepo.sharedpath, f)
208 208 dst = os.path.join(destpath, f)
209 209 dstbase = os.path.dirname(dst)
210 210 if dstbase and not os.path.exists(dstbase):
211 211 os.mkdir(dstbase)
212 212 if os.path.exists(src):
213 213 if dst.endswith('data'):
214 214 # lock to avoid premature writing to the target
215 215 destlock = lock.lock(os.path.join(dstbase, "lock"))
216 216 hardlink, n = util.copyfiles(src, dst, hardlink)
217 217 num += n
218 218 if hardlink:
219 219 ui.debug("linked %d files\n" % num)
220 220 else:
221 221 ui.debug("copied %d files\n" % num)
222 222 return destlock
223 223 except: # re-raises
224 224 release(destlock)
225 225 raise
226 226
227 227 def clone(ui, peeropts, source, dest=None, pull=False, rev=None,
228 228 update=True, stream=False, branch=None):
229 229 """Make a copy of an existing repository.
230 230
231 231 Create a copy of an existing repository in a new directory. The
232 232 source and destination are URLs, as passed to the repository
233 233 function. Returns a pair of repository peers, the source and
234 234 newly created destination.
235 235
236 236 The location of the source is added to the new repository's
237 237 .hg/hgrc file, as the default to be used for future pulls and
238 238 pushes.
239 239
240 240 If an exception is raised, the partly cloned/updated destination
241 241 repository will be deleted.
242 242
243 243 Arguments:
244 244
245 245 source: repository object or URL
246 246
247 247 dest: URL of destination repository to create (defaults to base
248 248 name of source repository)
249 249
250 250 pull: always pull from source repository, even in local case
251 251
252 252 stream: stream raw data uncompressed from repository (fast over
253 253 LAN, slow over WAN)
254 254
255 255 rev: revision to clone up to (implies pull=True)
256 256
257 257 update: update working directory after clone completes, if
258 258 destination is local repository (True means update to default rev,
259 259 anything else is treated as a revision)
260 260
261 261 branch: branches to clone
262 262 """
263 263
264 264 if isinstance(source, str):
265 265 origsource = ui.expandpath(source)
266 266 source, branch = parseurl(origsource, branch)
267 267 srcpeer = peer(ui, peeropts, source)
268 268 else:
269 269 srcpeer = source.peer() # in case we were called with a localrepo
270 270 branch = (None, branch or [])
271 271 origsource = source = srcpeer.url()
272 272 rev, checkout = addbranchrevs(srcpeer, srcpeer, branch, rev)
273 273
274 274 if dest is None:
275 275 dest = defaultdest(source)
276 276 ui.status(_("destination directory: %s\n") % dest)
277 277 else:
278 278 dest = ui.expandpath(dest)
279 279
280 280 dest = util.urllocalpath(dest)
281 281 source = util.urllocalpath(source)
282 282
283 283 if not dest:
284 284 raise util.Abort(_("empty destination path is not valid"))
285 285 if os.path.exists(dest):
286 286 if not os.path.isdir(dest):
287 287 raise util.Abort(_("destination '%s' already exists") % dest)
288 288 elif os.listdir(dest):
289 289 raise util.Abort(_("destination '%s' is not empty") % dest)
290 290
291 291 class DirCleanup(object):
292 292 def __init__(self, dir_):
293 293 self.rmtree = shutil.rmtree
294 294 self.dir_ = dir_
295 295 def close(self):
296 296 self.dir_ = None
297 297 def cleanup(self):
298 298 if self.dir_:
299 299 self.rmtree(self.dir_, True)
300 300
301 301 srclock = destlock = dircleanup = None
302 302 srcrepo = srcpeer.local()
303 303 try:
304 304 abspath = origsource
305 305 if islocal(origsource):
306 306 abspath = os.path.abspath(util.urllocalpath(origsource))
307 307
308 308 if islocal(dest):
309 309 dircleanup = DirCleanup(dest)
310 310
311 311 copy = False
312 312 if (srcrepo and srcrepo.cancopy() and islocal(dest)
313 313 and not phases.hassecret(srcrepo)):
314 314 copy = not pull and not rev
315 315
316 316 if copy:
317 317 try:
318 318 # we use a lock here because if we race with commit, we
319 319 # can end up with extra data in the cloned revlogs that's
320 320 # not pointed to by changesets, thus causing verify to
321 321 # fail
322 322 srclock = srcrepo.lock(wait=False)
323 323 except error.LockError:
324 324 copy = False
325 325
326 326 if copy:
327 327 srcrepo.hook('preoutgoing', throw=True, source='clone')
328 328 hgdir = os.path.realpath(os.path.join(dest, ".hg"))
329 329 if not os.path.exists(dest):
330 330 os.mkdir(dest)
331 331 else:
332 332 # only clean up directories we create ourselves
333 333 dircleanup.dir_ = hgdir
334 334 try:
335 335 destpath = hgdir
336 336 util.makedir(destpath, notindexed=True)
337 337 except OSError, inst:
338 338 if inst.errno == errno.EEXIST:
339 339 dircleanup.close()
340 340 raise util.Abort(_("destination '%s' already exists")
341 341 % dest)
342 342 raise
343 343
344 344 destlock = copystore(ui, srcrepo, destpath)
345 345
346 346 # Recomputing branch cache might be slow on big repos,
347 347 # so just copy it
348 348 dstcachedir = os.path.join(destpath, 'cache')
349 349 srcbranchcache = srcrepo.sjoin('cache/branchheads')
350 350 dstbranchcache = os.path.join(dstcachedir, 'branchheads')
351 351 if os.path.exists(srcbranchcache):
352 352 if not os.path.exists(dstcachedir):
353 353 os.mkdir(dstcachedir)
354 354 util.copyfile(srcbranchcache, dstbranchcache)
355 355
356 356 # we need to re-init the repo after manually copying the data
357 357 # into it
358 358 destpeer = peer(srcrepo, peeropts, dest)
359 359 srcrepo.hook('outgoing', source='clone',
360 360 node=node.hex(node.nullid))
361 361 else:
362 362 try:
363 363 destpeer = peer(srcrepo or ui, peeropts, dest, create=True)
364 364 # only pass ui when no srcrepo
365 365 except OSError, inst:
366 366 if inst.errno == errno.EEXIST:
367 367 dircleanup.close()
368 368 raise util.Abort(_("destination '%s' already exists")
369 369 % dest)
370 370 raise
371 371
372 372 revs = None
373 373 if rev:
374 374 if not srcpeer.capable('lookup'):
375 375 raise util.Abort(_("src repository does not support "
376 376 "revision lookup and so doesn't "
377 377 "support clone by revision"))
378 378 revs = [srcpeer.lookup(r) for r in rev]
379 379 checkout = revs[0]
380 380 if destpeer.local():
381 381 destpeer.local().clone(srcpeer, heads=revs, stream=stream)
382 382 elif srcrepo:
383 383 srcrepo.push(destpeer, revs=revs)
384 384 else:
385 385 raise util.Abort(_("clone from remote to remote not supported"))
386 386
387 387 if dircleanup:
388 388 dircleanup.close()
389 389
390 390 # clone all bookmarks except divergent ones
391 391 destrepo = destpeer.local()
392 392 if destrepo and srcpeer.capable("pushkey"):
393 393 rb = srcpeer.listkeys('bookmarks')
394 394 for k, n in rb.iteritems():
395 395 try:
396 396 m = destrepo.lookup(n)
397 397 destrepo._bookmarks[k] = m
398 398 except error.RepoLookupError:
399 399 pass
400 400 if rb:
401 401 bookmarks.write(destrepo)
402 402 elif srcrepo and destpeer.capable("pushkey"):
403 403 for k, n in srcrepo._bookmarks.iteritems():
404 404 destpeer.pushkey('bookmarks', k, '', hex(n))
405 405
406 406 if destrepo:
407 407 fp = destrepo.opener("hgrc", "w", text=True)
408 408 fp.write("[paths]\n")
409 409 u = util.url(abspath)
410 410 u.passwd = None
411 411 defaulturl = str(u)
412 412 fp.write("default = %s\n" % defaulturl)
413 413 fp.close()
414 414
415 415 destrepo.ui.setconfig('paths', 'default', defaulturl)
416 416
417 417 if update:
418 418 if update is not True:
419 419 checkout = srcpeer.lookup(update)
420 420 uprev = None
421 421 status = None
422 422 if checkout is not None:
423 423 try:
424 424 uprev = destrepo.lookup(checkout)
425 425 except error.RepoLookupError:
426 426 pass
427 427 if uprev is None:
428 428 try:
429 429 uprev = destrepo._bookmarks['@']
430 430 update = '@'
431 431 bn = destrepo[uprev].branch()
432 432 if bn == 'default':
433 433 status = _("updating to bookmark @\n")
434 434 else:
435 435 status = _("updating to bookmark @ on branch %s\n"
436 436 % bn)
437 437 except KeyError:
438 438 try:
439 439 uprev = destrepo.branchtip('default')
440 440 except error.RepoLookupError:
441 441 uprev = destrepo.lookup('tip')
442 442 if not status:
443 443 bn = destrepo[uprev].branch()
444 444 status = _("updating to branch %s\n") % bn
445 445 destrepo.ui.status(status)
446 446 _update(destrepo, uprev)
447 447 if update in destrepo._bookmarks:
448 448 bookmarks.setcurrent(destrepo, update)
449 449
450 450 return srcpeer, destpeer
451 451 finally:
452 452 release(srclock, destlock)
453 453 if dircleanup is not None:
454 454 dircleanup.cleanup()
455 455 if srcpeer is not None:
456 456 srcpeer.close()
457 457
458 458 def _showstats(repo, stats):
459 459 repo.ui.status(_("%d files updated, %d files merged, "
460 460 "%d files removed, %d files unresolved\n") % stats)
461 461
462 462 def updaterepo(repo, node, overwrite):
463 463 """Update the working directory to node.
464 464
465 465 When overwrite is set, changes are clobbered, merged else
466 466
467 467 returns stats (see pydoc mercurial.merge.applyupdates)"""
468 468 return mergemod.update(repo, node, False, overwrite, None)
469 469
470 470 def update(repo, node):
471 471 """update the working directory to node, merging linear changes"""
472 472 stats = updaterepo(repo, node, False)
473 473 _showstats(repo, stats)
474 474 if stats[3]:
475 475 repo.ui.status(_("use 'hg resolve' to retry unresolved file merges\n"))
476 476 return stats[3] > 0
477 477
478 478 # naming conflict in clone()
479 479 _update = update
480 480
481 481 def clean(repo, node, show_stats=True):
482 482 """forcibly switch the working directory to node, clobbering changes"""
483 483 stats = updaterepo(repo, node, True)
484 484 if show_stats:
485 485 _showstats(repo, stats)
486 486 return stats[3] > 0
487 487
488 488 def merge(repo, node, force=None, remind=True):
489 489 """Branch merge with node, resolving changes. Return true if any
490 490 unresolved conflicts."""
491 491 stats = mergemod.update(repo, node, True, force, False)
492 492 _showstats(repo, stats)
493 493 if stats[3]:
494 494 repo.ui.status(_("use 'hg resolve' to retry unresolved file merges "
495 495 "or 'hg update -C .' to abandon\n"))
496 496 elif remind:
497 497 repo.ui.status(_("(branch merge, don't forget to commit)\n"))
498 498 return stats[3] > 0
499 499
500 500 def _incoming(displaychlist, subreporecurse, ui, repo, source,
501 501 opts, buffered=False):
502 502 """
503 503 Helper for incoming / gincoming.
504 504 displaychlist gets called with
505 505 (remoterepo, incomingchangesetlist, displayer) parameters,
506 506 and is supposed to contain only code that can't be unified.
507 507 """
508 508 source, branches = parseurl(ui.expandpath(source), opts.get('branch'))
509 509 other = peer(repo, opts, source)
510 510 ui.status(_('comparing with %s\n') % util.hidepassword(source))
511 511 revs, checkout = addbranchrevs(repo, other, branches, opts.get('rev'))
512 512
513 513 if revs:
514 514 revs = [other.lookup(rev) for rev in revs]
515 515 other, chlist, cleanupfn = bundlerepo.getremotechanges(ui, repo, other,
516 516 revs, opts["bundle"], opts["force"])
517 517 try:
518 518 if not chlist:
519 519 ui.status(_("no changes found\n"))
520 520 return subreporecurse()
521 521
522 522 displayer = cmdutil.show_changeset(ui, other, opts, buffered)
523 523
524 524 # XXX once graphlog extension makes it into core,
525 525 # should be replaced by a if graph/else
526 526 displaychlist(other, chlist, displayer)
527 527
528 528 displayer.close()
529 529 finally:
530 530 cleanupfn()
531 531 subreporecurse()
532 532 return 0 # exit code is zero since we found incoming changes
533 533
534 534 def incoming(ui, repo, source, opts):
535 535 def subreporecurse():
536 536 ret = 1
537 537 if opts.get('subrepos'):
538 538 ctx = repo[None]
539 539 for subpath in sorted(ctx.substate):
540 540 sub = ctx.sub(subpath)
541 541 ret = min(ret, sub.incoming(ui, source, opts))
542 542 return ret
543 543
544 544 def display(other, chlist, displayer):
545 545 limit = cmdutil.loglimit(opts)
546 546 if opts.get('newest_first'):
547 547 chlist.reverse()
548 548 count = 0
549 549 for n in chlist:
550 550 if limit is not None and count >= limit:
551 551 break
552 552 parents = [p for p in other.changelog.parents(n) if p != nullid]
553 553 if opts.get('no_merges') and len(parents) == 2:
554 554 continue
555 555 count += 1
556 556 displayer.show(other[n])
557 557 return _incoming(display, subreporecurse, ui, repo, source, opts)
558 558
559 559 def _outgoing(ui, repo, dest, opts):
560 560 dest = ui.expandpath(dest or 'default-push', dest or 'default')
561 561 dest, branches = parseurl(dest, opts.get('branch'))
562 562 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
563 563 revs, checkout = addbranchrevs(repo, repo, branches, opts.get('rev'))
564 564 if revs:
565 565 revs = [repo.lookup(rev) for rev in scmutil.revrange(repo, revs)]
566 566
567 567 other = peer(repo, opts, dest)
568 568 outgoing = discovery.findcommonoutgoing(repo, other, revs,
569 569 force=opts.get('force'))
570 570 o = outgoing.missing
571 571 if not o:
572 572 scmutil.nochangesfound(repo.ui, repo, outgoing.excluded)
573 573 return None
574 574 return o
575 575
576 576 def outgoing(ui, repo, dest, opts):
577 577 def recurse():
578 578 ret = 1
579 579 if opts.get('subrepos'):
580 580 ctx = repo[None]
581 581 for subpath in sorted(ctx.substate):
582 582 sub = ctx.sub(subpath)
583 583 ret = min(ret, sub.outgoing(ui, dest, opts))
584 584 return ret
585 585
586 586 limit = cmdutil.loglimit(opts)
587 587 o = _outgoing(ui, repo, dest, opts)
588 588 if o is None:
589 589 return recurse()
590 590
591 591 if opts.get('newest_first'):
592 592 o.reverse()
593 593 displayer = cmdutil.show_changeset(ui, repo, opts)
594 594 count = 0
595 595 for n in o:
596 596 if limit is not None and count >= limit:
597 597 break
598 598 parents = [p for p in repo.changelog.parents(n) if p != nullid]
599 599 if opts.get('no_merges') and len(parents) == 2:
600 600 continue
601 601 count += 1
602 602 displayer.show(repo[n])
603 603 displayer.close()
604 604 recurse()
605 605 return 0 # exit code is zero since we found outgoing changes
606 606
607 607 def revert(repo, node, choose):
608 608 """revert changes to revision in node without updating dirstate"""
609 609 return mergemod.update(repo, node, False, True, choose)[3] > 0
610 610
611 611 def verify(repo):
612 612 """verify the consistency of a repository"""
613 613 return verifymod.verify(repo)
614 614
615 615 def remoteui(src, opts):
616 616 'build a remote ui from ui or repo and opts'
617 617 if util.safehasattr(src, 'baseui'): # looks like a repository
618 618 dst = src.baseui.copy() # drop repo-specific config
619 619 src = src.ui # copy target options from repo
620 620 else: # assume it's a global ui object
621 621 dst = src.copy() # keep all global options
622 622
623 623 # copy ssh-specific options
624 624 for o in 'ssh', 'remotecmd':
625 625 v = opts.get(o) or src.config('ui', o)
626 626 if v:
627 627 dst.setconfig("ui", o, v)
628 628
629 629 # copy bundle-specific options
630 630 r = src.config('bundle', 'mainreporoot')
631 631 if r:
632 632 dst.setconfig('bundle', 'mainreporoot', r)
633 633
634 634 # copy selected local settings to the remote ui
635 635 for sect in ('auth', 'hostfingerprints', 'http_proxy'):
636 636 for key, val in src.configitems(sect):
637 637 dst.setconfig(sect, key, val)
638 638 v = src.config('web', 'cacerts')
639 639 if v:
640 640 dst.setconfig('web', 'cacerts', util.expandpath(v))
641 641
642 642 return dst
General Comments 0
You need to be logged in to leave comments. Login now