##// END OF EJS Templates
transplant: use ui out descriptor when calling util.system
Idan Kamara -
r14741:39235b39 stable
parent child Browse files
Show More
@@ -1,632 +1,633 b''
1 1 # Patch transplanting extension for Mercurial
2 2 #
3 3 # Copyright 2006, 2007 Brendan Cully <brendan@kublai.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 '''command to transplant changesets from another branch
9 9
10 10 This extension allows you to transplant patches from another branch.
11 11
12 12 Transplanted patches are recorded in .hg/transplant/transplants, as a
13 13 map from a changeset hash to its hash in the source repository.
14 14 '''
15 15
16 16 from mercurial.i18n import _
17 17 import os, tempfile
18 18 from mercurial.node import short
19 19 from mercurial import bundlerepo, hg, merge, match
20 20 from mercurial import patch, revlog, scmutil, util, error, cmdutil
21 21 from mercurial import revset, templatekw
22 22
23 23 cmdtable = {}
24 24 command = cmdutil.command(cmdtable)
25 25
26 26 class transplantentry(object):
27 27 def __init__(self, lnode, rnode):
28 28 self.lnode = lnode
29 29 self.rnode = rnode
30 30
31 31 class transplants(object):
32 32 def __init__(self, path=None, transplantfile=None, opener=None):
33 33 self.path = path
34 34 self.transplantfile = transplantfile
35 35 self.opener = opener
36 36
37 37 if not opener:
38 38 self.opener = scmutil.opener(self.path)
39 39 self.transplants = {}
40 40 self.dirty = False
41 41 self.read()
42 42
43 43 def read(self):
44 44 abspath = os.path.join(self.path, self.transplantfile)
45 45 if self.transplantfile and os.path.exists(abspath):
46 46 for line in self.opener.read(self.transplantfile).splitlines():
47 47 lnode, rnode = map(revlog.bin, line.split(':'))
48 48 list = self.transplants.setdefault(rnode, [])
49 49 list.append(transplantentry(lnode, rnode))
50 50
51 51 def write(self):
52 52 if self.dirty and self.transplantfile:
53 53 if not os.path.isdir(self.path):
54 54 os.mkdir(self.path)
55 55 fp = self.opener(self.transplantfile, 'w')
56 56 for list in self.transplants.itervalues():
57 57 for t in list:
58 58 l, r = map(revlog.hex, (t.lnode, t.rnode))
59 59 fp.write(l + ':' + r + '\n')
60 60 fp.close()
61 61 self.dirty = False
62 62
63 63 def get(self, rnode):
64 64 return self.transplants.get(rnode) or []
65 65
66 66 def set(self, lnode, rnode):
67 67 list = self.transplants.setdefault(rnode, [])
68 68 list.append(transplantentry(lnode, rnode))
69 69 self.dirty = True
70 70
71 71 def remove(self, transplant):
72 72 list = self.transplants.get(transplant.rnode)
73 73 if list:
74 74 del list[list.index(transplant)]
75 75 self.dirty = True
76 76
77 77 class transplanter(object):
78 78 def __init__(self, ui, repo):
79 79 self.ui = ui
80 80 self.path = repo.join('transplant')
81 81 self.opener = scmutil.opener(self.path)
82 82 self.transplants = transplants(self.path, 'transplants',
83 83 opener=self.opener)
84 84
85 85 def applied(self, repo, node, parent):
86 86 '''returns True if a node is already an ancestor of parent
87 87 or has already been transplanted'''
88 88 if hasnode(repo, node):
89 89 if node in repo.changelog.reachable(parent, stop=node):
90 90 return True
91 91 for t in self.transplants.get(node):
92 92 # it might have been stripped
93 93 if not hasnode(repo, t.lnode):
94 94 self.transplants.remove(t)
95 95 return False
96 96 if t.lnode in repo.changelog.reachable(parent, stop=t.lnode):
97 97 return True
98 98 return False
99 99
100 100 def apply(self, repo, source, revmap, merges, opts={}):
101 101 '''apply the revisions in revmap one by one in revision order'''
102 102 revs = sorted(revmap)
103 103 p1, p2 = repo.dirstate.parents()
104 104 pulls = []
105 105 diffopts = patch.diffopts(self.ui, opts)
106 106 diffopts.git = True
107 107
108 108 lock = wlock = None
109 109 try:
110 110 wlock = repo.wlock()
111 111 lock = repo.lock()
112 112 for rev in revs:
113 113 node = revmap[rev]
114 114 revstr = '%s:%s' % (rev, short(node))
115 115
116 116 if self.applied(repo, node, p1):
117 117 self.ui.warn(_('skipping already applied revision %s\n') %
118 118 revstr)
119 119 continue
120 120
121 121 parents = source.changelog.parents(node)
122 122 if not opts.get('filter'):
123 123 # If the changeset parent is the same as the
124 124 # wdir's parent, just pull it.
125 125 if parents[0] == p1:
126 126 pulls.append(node)
127 127 p1 = node
128 128 continue
129 129 if pulls:
130 130 if source != repo:
131 131 repo.pull(source, heads=pulls)
132 132 merge.update(repo, pulls[-1], False, False, None)
133 133 p1, p2 = repo.dirstate.parents()
134 134 pulls = []
135 135
136 136 domerge = False
137 137 if node in merges:
138 138 # pulling all the merge revs at once would mean we
139 139 # couldn't transplant after the latest even if
140 140 # transplants before them fail.
141 141 domerge = True
142 142 if not hasnode(repo, node):
143 143 repo.pull(source, heads=[node])
144 144
145 145 if parents[1] != revlog.nullid:
146 146 self.ui.note(_('skipping merge changeset %s:%s\n')
147 147 % (rev, short(node)))
148 148 patchfile = None
149 149 else:
150 150 fd, patchfile = tempfile.mkstemp(prefix='hg-transplant-')
151 151 fp = os.fdopen(fd, 'w')
152 152 gen = patch.diff(source, parents[0], node, opts=diffopts)
153 153 for chunk in gen:
154 154 fp.write(chunk)
155 155 fp.close()
156 156
157 157 del revmap[rev]
158 158 if patchfile or domerge:
159 159 try:
160 160 n = self.applyone(repo, node,
161 161 source.changelog.read(node),
162 162 patchfile, merge=domerge,
163 163 log=opts.get('log'),
164 164 filter=opts.get('filter'))
165 165 if n and domerge:
166 166 self.ui.status(_('%s merged at %s\n') % (revstr,
167 167 short(n)))
168 168 elif n:
169 169 self.ui.status(_('%s transplanted to %s\n')
170 170 % (short(node),
171 171 short(n)))
172 172 finally:
173 173 if patchfile:
174 174 os.unlink(patchfile)
175 175 if pulls:
176 176 repo.pull(source, heads=pulls)
177 177 merge.update(repo, pulls[-1], False, False, None)
178 178 finally:
179 179 self.saveseries(revmap, merges)
180 180 self.transplants.write()
181 181 lock.release()
182 182 wlock.release()
183 183
184 184 def filter(self, filter, node, changelog, patchfile):
185 185 '''arbitrarily rewrite changeset before applying it'''
186 186
187 187 self.ui.status(_('filtering %s\n') % patchfile)
188 188 user, date, msg = (changelog[1], changelog[2], changelog[4])
189 189 fd, headerfile = tempfile.mkstemp(prefix='hg-transplant-')
190 190 fp = os.fdopen(fd, 'w')
191 191 fp.write("# HG changeset patch\n")
192 192 fp.write("# User %s\n" % user)
193 193 fp.write("# Date %d %d\n" % date)
194 194 fp.write(msg + '\n')
195 195 fp.close()
196 196
197 197 try:
198 198 util.system('%s %s %s' % (filter, util.shellquote(headerfile),
199 199 util.shellquote(patchfile)),
200 200 environ={'HGUSER': changelog[1],
201 201 'HGREVISION': revlog.hex(node),
202 202 },
203 onerr=util.Abort, errprefix=_('filter failed'))
203 onerr=util.Abort, errprefix=_('filter failed'),
204 out=self.ui.fout)
204 205 user, date, msg = self.parselog(file(headerfile))[1:4]
205 206 finally:
206 207 os.unlink(headerfile)
207 208
208 209 return (user, date, msg)
209 210
210 211 def applyone(self, repo, node, cl, patchfile, merge=False, log=False,
211 212 filter=None):
212 213 '''apply the patch in patchfile to the repository as a transplant'''
213 214 (manifest, user, (time, timezone), files, message) = cl[:5]
214 215 date = "%d %d" % (time, timezone)
215 216 extra = {'transplant_source': node}
216 217 if filter:
217 218 (user, date, message) = self.filter(filter, node, cl, patchfile)
218 219
219 220 if log:
220 221 # we don't translate messages inserted into commits
221 222 message += '\n(transplanted from %s)' % revlog.hex(node)
222 223
223 224 self.ui.status(_('applying %s\n') % short(node))
224 225 self.ui.note('%s %s\n%s\n' % (user, date, message))
225 226
226 227 if not patchfile and not merge:
227 228 raise util.Abort(_('can only omit patchfile if merging'))
228 229 if patchfile:
229 230 try:
230 231 files = set()
231 232 patch.patch(self.ui, repo, patchfile, files=files, eolmode=None)
232 233 files = list(files)
233 234 if not files:
234 235 self.ui.warn(_('%s: empty changeset') % revlog.hex(node))
235 236 return None
236 237 except Exception, inst:
237 238 seriespath = os.path.join(self.path, 'series')
238 239 if os.path.exists(seriespath):
239 240 os.unlink(seriespath)
240 241 p1 = repo.dirstate.p1()
241 242 p2 = node
242 243 self.log(user, date, message, p1, p2, merge=merge)
243 244 self.ui.write(str(inst) + '\n')
244 245 raise util.Abort(_('fix up the merge and run '
245 246 'hg transplant --continue'))
246 247 else:
247 248 files = None
248 249 if merge:
249 250 p1, p2 = repo.dirstate.parents()
250 251 repo.dirstate.setparents(p1, node)
251 252 m = match.always(repo.root, '')
252 253 else:
253 254 m = match.exact(repo.root, '', files)
254 255
255 256 n = repo.commit(message, user, date, extra=extra, match=m)
256 257 if not n:
257 258 # Crash here to prevent an unclear crash later, in
258 259 # transplants.write(). This can happen if patch.patch()
259 260 # does nothing but claims success or if repo.status() fails
260 261 # to report changes done by patch.patch(). These both
261 262 # appear to be bugs in other parts of Mercurial, but dying
262 263 # here, as soon as we can detect the problem, is preferable
263 264 # to silently dropping changesets on the floor.
264 265 raise RuntimeError('nothing committed after transplant')
265 266 if not merge:
266 267 self.transplants.set(n, node)
267 268
268 269 return n
269 270
270 271 def resume(self, repo, source, opts=None):
271 272 '''recover last transaction and apply remaining changesets'''
272 273 if os.path.exists(os.path.join(self.path, 'journal')):
273 274 n, node = self.recover(repo)
274 275 self.ui.status(_('%s transplanted as %s\n') % (short(node),
275 276 short(n)))
276 277 seriespath = os.path.join(self.path, 'series')
277 278 if not os.path.exists(seriespath):
278 279 self.transplants.write()
279 280 return
280 281 nodes, merges = self.readseries()
281 282 revmap = {}
282 283 for n in nodes:
283 284 revmap[source.changelog.rev(n)] = n
284 285 os.unlink(seriespath)
285 286
286 287 self.apply(repo, source, revmap, merges, opts)
287 288
288 289 def recover(self, repo):
289 290 '''commit working directory using journal metadata'''
290 291 node, user, date, message, parents = self.readlog()
291 292 merge = len(parents) == 2
292 293
293 294 if not user or not date or not message or not parents[0]:
294 295 raise util.Abort(_('transplant log file is corrupt'))
295 296
296 297 extra = {'transplant_source': node}
297 298 wlock = repo.wlock()
298 299 try:
299 300 p1, p2 = repo.dirstate.parents()
300 301 if p1 != parents[0]:
301 302 raise util.Abort(
302 303 _('working dir not at transplant parent %s') %
303 304 revlog.hex(parents[0]))
304 305 if merge:
305 306 repo.dirstate.setparents(p1, parents[1])
306 307 n = repo.commit(message, user, date, extra=extra)
307 308 if not n:
308 309 raise util.Abort(_('commit failed'))
309 310 if not merge:
310 311 self.transplants.set(n, node)
311 312 self.unlog()
312 313
313 314 return n, node
314 315 finally:
315 316 wlock.release()
316 317
317 318 def readseries(self):
318 319 nodes = []
319 320 merges = []
320 321 cur = nodes
321 322 for line in self.opener.read('series').splitlines():
322 323 if line.startswith('# Merges'):
323 324 cur = merges
324 325 continue
325 326 cur.append(revlog.bin(line))
326 327
327 328 return (nodes, merges)
328 329
329 330 def saveseries(self, revmap, merges):
330 331 if not revmap:
331 332 return
332 333
333 334 if not os.path.isdir(self.path):
334 335 os.mkdir(self.path)
335 336 series = self.opener('series', 'w')
336 337 for rev in sorted(revmap):
337 338 series.write(revlog.hex(revmap[rev]) + '\n')
338 339 if merges:
339 340 series.write('# Merges\n')
340 341 for m in merges:
341 342 series.write(revlog.hex(m) + '\n')
342 343 series.close()
343 344
344 345 def parselog(self, fp):
345 346 parents = []
346 347 message = []
347 348 node = revlog.nullid
348 349 inmsg = False
349 350 user = None
350 351 date = None
351 352 for line in fp.read().splitlines():
352 353 if inmsg:
353 354 message.append(line)
354 355 elif line.startswith('# User '):
355 356 user = line[7:]
356 357 elif line.startswith('# Date '):
357 358 date = line[7:]
358 359 elif line.startswith('# Node ID '):
359 360 node = revlog.bin(line[10:])
360 361 elif line.startswith('# Parent '):
361 362 parents.append(revlog.bin(line[9:]))
362 363 elif not line.startswith('# '):
363 364 inmsg = True
364 365 message.append(line)
365 366 if None in (user, date):
366 367 raise util.Abort(_("filter corrupted changeset (no user or date)"))
367 368 return (node, user, date, '\n'.join(message), parents)
368 369
369 370 def log(self, user, date, message, p1, p2, merge=False):
370 371 '''journal changelog metadata for later recover'''
371 372
372 373 if not os.path.isdir(self.path):
373 374 os.mkdir(self.path)
374 375 fp = self.opener('journal', 'w')
375 376 fp.write('# User %s\n' % user)
376 377 fp.write('# Date %s\n' % date)
377 378 fp.write('# Node ID %s\n' % revlog.hex(p2))
378 379 fp.write('# Parent ' + revlog.hex(p1) + '\n')
379 380 if merge:
380 381 fp.write('# Parent ' + revlog.hex(p2) + '\n')
381 382 fp.write(message.rstrip() + '\n')
382 383 fp.close()
383 384
384 385 def readlog(self):
385 386 return self.parselog(self.opener('journal'))
386 387
387 388 def unlog(self):
388 389 '''remove changelog journal'''
389 390 absdst = os.path.join(self.path, 'journal')
390 391 if os.path.exists(absdst):
391 392 os.unlink(absdst)
392 393
393 394 def transplantfilter(self, repo, source, root):
394 395 def matchfn(node):
395 396 if self.applied(repo, node, root):
396 397 return False
397 398 if source.changelog.parents(node)[1] != revlog.nullid:
398 399 return False
399 400 extra = source.changelog.read(node)[5]
400 401 cnode = extra.get('transplant_source')
401 402 if cnode and self.applied(repo, cnode, root):
402 403 return False
403 404 return True
404 405
405 406 return matchfn
406 407
407 408 def hasnode(repo, node):
408 409 try:
409 410 return repo.changelog.rev(node) is not None
410 411 except error.RevlogError:
411 412 return False
412 413
413 414 def browserevs(ui, repo, nodes, opts):
414 415 '''interactively transplant changesets'''
415 416 def browsehelp(ui):
416 417 ui.write(_('y: transplant this changeset\n'
417 418 'n: skip this changeset\n'
418 419 'm: merge at this changeset\n'
419 420 'p: show patch\n'
420 421 'c: commit selected changesets\n'
421 422 'q: cancel transplant\n'
422 423 '?: show this help\n'))
423 424
424 425 displayer = cmdutil.show_changeset(ui, repo, opts)
425 426 transplants = []
426 427 merges = []
427 428 for node in nodes:
428 429 displayer.show(repo[node])
429 430 action = None
430 431 while not action:
431 432 action = ui.prompt(_('apply changeset? [ynmpcq?]:'))
432 433 if action == '?':
433 434 browsehelp(ui)
434 435 action = None
435 436 elif action == 'p':
436 437 parent = repo.changelog.parents(node)[0]
437 438 for chunk in patch.diff(repo, parent, node):
438 439 ui.write(chunk)
439 440 action = None
440 441 elif action not in ('y', 'n', 'm', 'c', 'q'):
441 442 ui.write(_('no such option\n'))
442 443 action = None
443 444 if action == 'y':
444 445 transplants.append(node)
445 446 elif action == 'm':
446 447 merges.append(node)
447 448 elif action == 'c':
448 449 break
449 450 elif action == 'q':
450 451 transplants = ()
451 452 merges = ()
452 453 break
453 454 displayer.close()
454 455 return (transplants, merges)
455 456
456 457 @command('transplant',
457 458 [('s', 'source', '', _('pull patches from REPO'), _('REPO')),
458 459 ('b', 'branch', [],
459 460 _('pull patches from branch BRANCH'), _('BRANCH')),
460 461 ('a', 'all', None, _('pull all changesets up to BRANCH')),
461 462 ('p', 'prune', [], _('skip over REV'), _('REV')),
462 463 ('m', 'merge', [], _('merge at REV'), _('REV')),
463 464 ('', 'log', None, _('append transplant info to log message')),
464 465 ('c', 'continue', None, _('continue last transplant session '
465 466 'after repair')),
466 467 ('', 'filter', '',
467 468 _('filter changesets through command'), _('CMD'))],
468 469 _('hg transplant [-s REPO] [-b BRANCH [-a]] [-p REV] '
469 470 '[-m REV] [REV]...'))
470 471 def transplant(ui, repo, *revs, **opts):
471 472 '''transplant changesets from another branch
472 473
473 474 Selected changesets will be applied on top of the current working
474 475 directory with the log of the original changeset. The changesets
475 476 are copied and will thus appear twice in the history. Use the
476 477 rebase extension instead if you want to move a whole branch of
477 478 unpublished changesets.
478 479
479 480 If --log is specified, log messages will have a comment appended
480 481 of the form::
481 482
482 483 (transplanted from CHANGESETHASH)
483 484
484 485 You can rewrite the changelog message with the --filter option.
485 486 Its argument will be invoked with the current changelog message as
486 487 $1 and the patch as $2.
487 488
488 489 If --source/-s is specified, selects changesets from the named
489 490 repository. If --branch/-b is specified, selects changesets from
490 491 the branch holding the named revision, up to that revision. If
491 492 --all/-a is specified, all changesets on the branch will be
492 493 transplanted, otherwise you will be prompted to select the
493 494 changesets you want.
494 495
495 496 :hg:`transplant --branch REVISION --all` will transplant the
496 497 selected branch (up to the named revision) onto your current
497 498 working directory.
498 499
499 500 You can optionally mark selected transplanted changesets as merge
500 501 changesets. You will not be prompted to transplant any ancestors
501 502 of a merged transplant, and you can merge descendants of them
502 503 normally instead of transplanting them.
503 504
504 505 If no merges or revisions are provided, :hg:`transplant` will
505 506 start an interactive changeset browser.
506 507
507 508 If a changeset application fails, you can fix the merge by hand
508 509 and then resume where you left off by calling :hg:`transplant
509 510 --continue/-c`.
510 511 '''
511 512 def incwalk(repo, csets, match=util.always):
512 513 for node in csets:
513 514 if match(node):
514 515 yield node
515 516
516 517 def transplantwalk(repo, root, branches, match=util.always):
517 518 if not branches:
518 519 branches = repo.heads()
519 520 ancestors = []
520 521 for branch in branches:
521 522 ancestors.append(repo.changelog.ancestor(root, branch))
522 523 for node in repo.changelog.nodesbetween(ancestors, branches)[0]:
523 524 if match(node):
524 525 yield node
525 526
526 527 def checkopts(opts, revs):
527 528 if opts.get('continue'):
528 529 if opts.get('branch') or opts.get('all') or opts.get('merge'):
529 530 raise util.Abort(_('--continue is incompatible with '
530 531 'branch, all or merge'))
531 532 return
532 533 if not (opts.get('source') or revs or
533 534 opts.get('merge') or opts.get('branch')):
534 535 raise util.Abort(_('no source URL, branch tag or revision '
535 536 'list provided'))
536 537 if opts.get('all'):
537 538 if not opts.get('branch'):
538 539 raise util.Abort(_('--all requires a branch revision'))
539 540 if revs:
540 541 raise util.Abort(_('--all is incompatible with a '
541 542 'revision list'))
542 543
543 544 checkopts(opts, revs)
544 545
545 546 if not opts.get('log'):
546 547 opts['log'] = ui.config('transplant', 'log')
547 548 if not opts.get('filter'):
548 549 opts['filter'] = ui.config('transplant', 'filter')
549 550
550 551 tp = transplanter(ui, repo)
551 552
552 553 p1, p2 = repo.dirstate.parents()
553 554 if len(repo) > 0 and p1 == revlog.nullid:
554 555 raise util.Abort(_('no revision checked out'))
555 556 if not opts.get('continue'):
556 557 if p2 != revlog.nullid:
557 558 raise util.Abort(_('outstanding uncommitted merges'))
558 559 m, a, r, d = repo.status()[:4]
559 560 if m or a or r or d:
560 561 raise util.Abort(_('outstanding local changes'))
561 562
562 563 sourcerepo = opts.get('source')
563 564 if sourcerepo:
564 565 source = hg.peer(ui, opts, ui.expandpath(sourcerepo))
565 566 branches = map(source.lookup, opts.get('branch', ()))
566 567 source, csets, cleanupfn = bundlerepo.getremotechanges(ui, repo, source,
567 568 onlyheads=branches, force=True)
568 569 else:
569 570 source = repo
570 571 branches = map(source.lookup, opts.get('branch', ()))
571 572 cleanupfn = None
572 573
573 574 try:
574 575 if opts.get('continue'):
575 576 tp.resume(repo, source, opts)
576 577 return
577 578
578 579 tf = tp.transplantfilter(repo, source, p1)
579 580 if opts.get('prune'):
580 581 prune = [source.lookup(r)
581 582 for r in scmutil.revrange(source, opts.get('prune'))]
582 583 matchfn = lambda x: tf(x) and x not in prune
583 584 else:
584 585 matchfn = tf
585 586 merges = map(source.lookup, opts.get('merge', ()))
586 587 revmap = {}
587 588 if revs:
588 589 for r in scmutil.revrange(source, revs):
589 590 revmap[int(r)] = source.lookup(r)
590 591 elif opts.get('all') or not merges:
591 592 if source != repo:
592 593 alltransplants = incwalk(source, csets, match=matchfn)
593 594 else:
594 595 alltransplants = transplantwalk(source, p1, branches,
595 596 match=matchfn)
596 597 if opts.get('all'):
597 598 revs = alltransplants
598 599 else:
599 600 revs, newmerges = browserevs(ui, source, alltransplants, opts)
600 601 merges.extend(newmerges)
601 602 for r in revs:
602 603 revmap[source.changelog.rev(r)] = r
603 604 for r in merges:
604 605 revmap[source.changelog.rev(r)] = r
605 606
606 607 tp.apply(repo, source, revmap, merges, opts)
607 608 finally:
608 609 if cleanupfn:
609 610 cleanupfn()
610 611
611 612 def revsettransplanted(repo, subset, x):
612 613 """``transplanted([set])``
613 614 Transplanted changesets in set, or all transplanted changesets.
614 615 """
615 616 if x:
616 617 s = revset.getset(repo, subset, x)
617 618 else:
618 619 s = subset
619 620 return [r for r in s if repo[r].extra().get('transplant_source')]
620 621
621 622 def kwtransplanted(repo, ctx, **args):
622 623 """:transplanted: String. The node identifier of the transplanted
623 624 changeset if any."""
624 625 n = ctx.extra().get('transplant_source')
625 626 return n and revlog.hex(n) or ''
626 627
627 628 def extsetup(ui):
628 629 revset.symbols['transplanted'] = revsettransplanted
629 630 templatekw.keywords['transplanted'] = kwtransplanted
630 631
631 632 # tell hggettext to extract docstrings from these functions:
632 633 i18nfunctions = [revsettransplanted, kwtransplanted]
General Comments 0
You need to be logged in to leave comments. Login now