##// END OF EJS Templates
findcopies: fix rename bug...
Matt Mackall -
r5433:f9b0e4f0 default
parent child Browse files
Show More
@@ -1,635 +1,635 b''
1 1 # merge.py - directory-level update/merge handling for Mercurial
2 2 #
3 3 # Copyright 2006, 2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7
8 8 from node import *
9 9 from i18n import _
10 10 import errno, util, os, tempfile, context, heapq
11 11
12 12 def filemerge(repo, fw, fd, fo, wctx, mctx):
13 13 """perform a 3-way merge in the working directory
14 14
15 15 fw = original filename in the working directory
16 16 fd = destination filename in the working directory
17 17 fo = filename in other parent
18 18 wctx, mctx = working and merge changecontexts
19 19 """
20 20
21 21 def temp(prefix, ctx):
22 22 pre = "%s~%s." % (os.path.basename(ctx.path()), prefix)
23 23 (fd, name) = tempfile.mkstemp(prefix=pre)
24 24 data = repo.wwritedata(ctx.path(), ctx.data())
25 25 f = os.fdopen(fd, "wb")
26 26 f.write(data)
27 27 f.close()
28 28 return name
29 29
30 30 fcm = wctx.filectx(fw)
31 31 fcmdata = wctx.filectx(fd).data()
32 32 fco = mctx.filectx(fo)
33 33
34 34 if not fco.cmp(fcmdata): # files identical?
35 35 return None
36 36
37 37 fca = fcm.ancestor(fco)
38 38 if not fca:
39 39 fca = repo.filectx(fw, fileid=nullrev)
40 40 a = repo.wjoin(fd)
41 41 b = temp("base", fca)
42 42 c = temp("other", fco)
43 43
44 44 if fw != fo:
45 45 repo.ui.status(_("merging %s and %s\n") % (fw, fo))
46 46 else:
47 47 repo.ui.status(_("merging %s\n") % fw)
48 48
49 49 repo.ui.debug(_("my %s other %s ancestor %s\n") % (fcm, fco, fca))
50 50
51 51 cmd = (os.environ.get("HGMERGE") or repo.ui.config("ui", "merge")
52 52 or "hgmerge")
53 53 r = util.system('%s "%s" "%s" "%s"' % (cmd, a, b, c), cwd=repo.root,
54 54 environ={'HG_FILE': fd,
55 55 'HG_MY_NODE': str(wctx.parents()[0]),
56 56 'HG_OTHER_NODE': str(mctx)})
57 57 if r:
58 58 repo.ui.warn(_("merging %s failed!\n") % fd)
59 59
60 60 os.unlink(b)
61 61 os.unlink(c)
62 62 return r
63 63
64 64 def checkunknown(wctx, mctx):
65 65 "check for collisions between unknown files and files in mctx"
66 66 man = mctx.manifest()
67 67 for f in wctx.unknown():
68 68 if f in man:
69 69 if mctx.filectx(f).cmp(wctx.filectx(f).data()):
70 70 raise util.Abort(_("untracked local file '%s' differs"
71 71 " from remote version") % f)
72 72
73 73 def checkcollision(mctx):
74 74 "check for case folding collisions in the destination context"
75 75 folded = {}
76 76 for fn in mctx.manifest():
77 77 fold = fn.lower()
78 78 if fold in folded:
79 79 raise util.Abort(_("case-folding collision between %s and %s")
80 80 % (fn, folded[fold]))
81 81 folded[fold] = fn
82 82
83 83 def forgetremoved(wctx, mctx):
84 84 """
85 85 Forget removed files
86 86
87 87 If we're jumping between revisions (as opposed to merging), and if
88 88 neither the working directory nor the target rev has the file,
89 89 then we need to remove it from the dirstate, to prevent the
90 90 dirstate from listing the file when it is no longer in the
91 91 manifest.
92 92 """
93 93
94 94 action = []
95 95 man = mctx.manifest()
96 96 for f in wctx.deleted() + wctx.removed():
97 97 if f not in man:
98 98 action.append((f, "f"))
99 99
100 100 return action
101 101
102 102 def findcopies(repo, m1, m2, ma, limit):
103 103 """
104 104 Find moves and copies between m1 and m2 back to limit linkrev
105 105 """
106 106
107 107 def nonoverlap(d1, d2, d3):
108 108 "Return list of elements in d1 not in d2 or d3"
109 109 l = [d for d in d1 if d not in d3 and d not in d2]
110 110 l.sort()
111 111 return l
112 112
113 113 def dirname(f):
114 114 s = f.rfind("/")
115 115 if s == -1:
116 116 return ""
117 117 return f[:s]
118 118
119 119 def dirs(files):
120 120 d = {}
121 121 for f in files:
122 122 f = dirname(f)
123 123 while f not in d:
124 124 d[f] = True
125 125 f = dirname(f)
126 126 return d
127 127
128 128 wctx = repo.workingctx()
129 129
130 130 def makectx(f, n):
131 131 if len(n) == 20:
132 132 return repo.filectx(f, fileid=n)
133 133 return wctx.filectx(f)
134 134 ctx = util.cachefunc(makectx)
135 135
136 136 def findold(fctx):
137 137 "find files that path was copied from, back to linkrev limit"
138 138 old = {}
139 139 seen = {}
140 140 orig = fctx.path()
141 141 visit = [fctx]
142 142 while visit:
143 143 fc = visit.pop()
144 144 s = str(fc)
145 145 if s in seen:
146 146 continue
147 147 seen[s] = 1
148 148 if fc.path() != orig and fc.path() not in old:
149 149 old[fc.path()] = 1
150 150 if fc.rev() < limit:
151 151 continue
152 152 visit += fc.parents()
153 153
154 154 old = old.keys()
155 155 old.sort()
156 156 return old
157 157
158 158 copy = {}
159 159 fullcopy = {}
160 160 diverge = {}
161 161
162 162 def checkcopies(c, man, aman):
163 163 '''check possible copies for filectx c'''
164 164 for of in findold(c):
165 165 fullcopy[c.path()] = of # remember for dir rename detection
166 166 if of not in man: # original file not in other manifest?
167 167 if of in ma:
168 168 diverge.setdefault(of, []).append(c.path())
169 169 continue
170 170 # if the original file is unchanged on the other branch,
171 171 # no merge needed
172 172 if man[of] == aman.get(of):
173 173 continue
174 174 c2 = ctx(of, man[of])
175 175 ca = c.ancestor(c2)
176 176 if not ca: # unrelated?
177 177 continue
178 178 # named changed on only one side?
179 179 if ca.path() == c.path() or ca.path() == c2.path():
180 if c == ca or c2 == ca: # no merge needed, ignore copy
180 if c == ca and c2 == ca: # no merge needed, ignore copy
181 181 continue
182 182 copy[c.path()] = of
183 183
184 184 if not repo.ui.configbool("merge", "followcopies", True):
185 185 return {}, {}
186 186
187 187 # avoid silly behavior for update from empty dir
188 188 if not m1 or not m2 or not ma:
189 189 return {}, {}
190 190
191 191 u1 = nonoverlap(m1, m2, ma)
192 192 u2 = nonoverlap(m2, m1, ma)
193 193
194 194 for f in u1:
195 195 checkcopies(ctx(f, m1[f]), m2, ma)
196 196
197 197 for f in u2:
198 198 checkcopies(ctx(f, m2[f]), m1, ma)
199 199
200 200 d2 = {}
201 201 for of, fl in diverge.items():
202 202 for f in fl:
203 203 fo = list(fl)
204 204 fo.remove(f)
205 205 d2[f] = (of, fo)
206 206
207 207 if not fullcopy or not repo.ui.configbool("merge", "followdirs", True):
208 208 return copy, diverge
209 209
210 210 # generate a directory move map
211 211 d1, d2 = dirs(m1), dirs(m2)
212 212 invalid = {}
213 213 dirmove = {}
214 214
215 215 # examine each file copy for a potential directory move, which is
216 216 # when all the files in a directory are moved to a new directory
217 217 for dst, src in fullcopy.items():
218 218 dsrc, ddst = dirname(src), dirname(dst)
219 219 if dsrc in invalid:
220 220 # already seen to be uninteresting
221 221 continue
222 222 elif dsrc in d1 and ddst in d1:
223 223 # directory wasn't entirely moved locally
224 224 invalid[dsrc] = True
225 225 elif dsrc in d2 and ddst in d2:
226 226 # directory wasn't entirely moved remotely
227 227 invalid[dsrc] = True
228 228 elif dsrc in dirmove and dirmove[dsrc] != ddst:
229 229 # files from the same directory moved to two different places
230 230 invalid[dsrc] = True
231 231 else:
232 232 # looks good so far
233 233 dirmove[dsrc + "/"] = ddst + "/"
234 234
235 235 for i in invalid:
236 236 if i in dirmove:
237 237 del dirmove[i]
238 238
239 239 del d1, d2, invalid
240 240
241 241 if not dirmove:
242 242 return copy, diverge
243 243
244 244 # check unaccounted nonoverlapping files against directory moves
245 245 for f in u1 + u2:
246 246 if f not in fullcopy:
247 247 for d in dirmove:
248 248 if f.startswith(d):
249 249 # new file added in a directory that was moved, move it
250 250 copy[f] = dirmove[d] + f[len(d):]
251 251 break
252 252
253 253 return copy, diverge
254 254
255 255 def symmetricdifference(repo, rev1, rev2):
256 256 """symmetric difference of the sets of ancestors of rev1 and rev2
257
257
258 258 I.e. revisions that are ancestors of rev1 or rev2, but not both.
259 259 """
260 260 # basic idea:
261 261 # - mark rev1 and rev2 with different colors
262 262 # - walk the graph in topological order with the help of a heap;
263 263 # for each revision r:
264 264 # - if r has only one color, we want to return it
265 265 # - add colors[r] to its parents
266 266 #
267 267 # We keep track of the number of revisions in the heap that
268 268 # we may be interested in. We stop walking the graph as soon
269 269 # as this number reaches 0.
270 270 WHITE = 1
271 271 BLACK = 2
272 272 ALLCOLORS = WHITE | BLACK
273 273 colors = {rev1: WHITE, rev2: BLACK}
274 274
275 275 cl = repo.changelog
276 276
277 277 visit = [-rev1, -rev2]
278 278 heapq.heapify(visit)
279 279 n_wanted = len(visit)
280 280 ret = []
281 281
282 282 while n_wanted:
283 283 r = -heapq.heappop(visit)
284 284 wanted = colors[r] != ALLCOLORS
285 285 n_wanted -= wanted
286 286 if wanted:
287 287 ret.append(r)
288 288
289 289 for p in cl.parentrevs(r):
290 290 if p == nullrev:
291 291 continue
292 292 if p not in colors:
293 293 # first time we see p; add it to visit
294 294 n_wanted += wanted
295 295 colors[p] = colors[r]
296 296 heapq.heappush(visit, -p)
297 297 elif colors[p] != ALLCOLORS and colors[p] != colors[r]:
298 298 # at first we thought we wanted p, but now
299 299 # we know we don't really want it
300 300 n_wanted -= 1
301 301 colors[p] |= colors[r]
302 302
303 303 del colors[r]
304 304
305 305 return ret
306 306
307 307 def manifestmerge(repo, p1, p2, pa, overwrite, partial):
308 308 """
309 309 Merge p1 and p2 with ancestor ma and generate merge action list
310 310
311 311 overwrite = whether we clobber working files
312 312 partial = function to filter file lists
313 313 """
314 314
315 315 repo.ui.note(_("resolving manifests\n"))
316 316 repo.ui.debug(_(" overwrite %s partial %s\n") % (overwrite, bool(partial)))
317 317 repo.ui.debug(_(" ancestor %s local %s remote %s\n") % (pa, p1, p2))
318 318
319 319 m1 = p1.manifest()
320 320 m2 = p2.manifest()
321 321 ma = pa.manifest()
322 322 backwards = (pa == p2)
323 323 action = []
324 324 copy = {}
325 325 diverge = {}
326 326
327 327 def fmerge(f, f2=None, fa=None):
328 328 """merge flags"""
329 329 if not f2:
330 330 f2 = f
331 331 fa = f
332 332 a, b, c = ma.execf(fa), m1.execf(f), m2.execf(f2)
333 333 if ((a^b) | (a^c)) ^ a:
334 334 return 'x'
335 335 a, b, c = ma.linkf(fa), m1.linkf(f), m2.linkf(f2)
336 336 if ((a^b) | (a^c)) ^ a:
337 337 return 'l'
338 338 return ''
339 339
340 340 def act(msg, m, f, *args):
341 341 repo.ui.debug(" %s: %s -> %s\n" % (f, msg, m))
342 342 action.append((f, m) + args)
343 343
344 344 if not (backwards or overwrite):
345 345 rev1 = p1.rev()
346 346 if rev1 is None:
347 347 # p1 is a workingctx
348 348 rev1 = p1.parents()[0].rev()
349 349 limit = min(symmetricdifference(repo, rev1, p2.rev()))
350 350 copy, diverge = findcopies(repo, m1, m2, ma, limit)
351 351
352 352 for of, fl in diverge.items():
353 353 act("divergent renames", "dr", of, fl)
354 354
355 355 copied = dict.fromkeys(copy.values())
356 356
357 357 # Compare manifests
358 358 for f, n in m1.iteritems():
359 359 if partial and not partial(f):
360 360 continue
361 361 if f in m2:
362 362 # are files different?
363 363 if n != m2[f]:
364 364 a = ma.get(f, nullid)
365 365 # are both different from the ancestor?
366 366 if not overwrite and n != a and m2[f] != a:
367 367 act("versions differ", "m", f, f, f, fmerge(f), False)
368 368 # are we clobbering?
369 369 # is remote's version newer?
370 370 # or are we going back in time and clean?
371 371 elif overwrite or m2[f] != a or (backwards and not n[20:]):
372 372 act("remote is newer", "g", f, m2.flags(f))
373 373 # local is newer, not overwrite, check mode bits
374 374 elif fmerge(f) != m1.flags(f):
375 375 act("update permissions", "e", f, m2.flags(f))
376 376 # contents same, check mode bits
377 377 elif m1.flags(f) != m2.flags(f):
378 378 if overwrite or fmerge(f) != m1.flags(f):
379 379 act("update permissions", "e", f, m2.flags(f))
380 380 elif f in copied:
381 381 continue
382 382 elif f in copy:
383 383 f2 = copy[f]
384 384 if f2 not in m2: # directory rename
385 385 act("remote renamed directory to " + f2, "d",
386 386 f, None, f2, m1.flags(f))
387 387 elif f2 in m1: # case 2 A,B/B/B
388 388 act("local copied to " + f2, "m",
389 389 f, f2, f, fmerge(f, f2, f2), False)
390 390 else: # case 4,21 A/B/B
391 391 act("local moved to " + f2, "m",
392 392 f, f2, f, fmerge(f, f2, f2), False)
393 393 elif f in ma:
394 394 if n != ma[f] and not overwrite:
395 395 if repo.ui.prompt(
396 396 (_(" local changed %s which remote deleted\n") % f) +
397 397 _("(k)eep or (d)elete?"), _("[kd]"), _("k")) == _("d"):
398 398 act("prompt delete", "r", f)
399 399 else:
400 400 act("other deleted", "r", f)
401 401 else:
402 402 # file is created on branch or in working directory
403 403 if (overwrite and n[20:] != "u") or (backwards and not n[20:]):
404 404 act("remote deleted", "r", f)
405 405
406 406 for f, n in m2.iteritems():
407 407 if partial and not partial(f):
408 408 continue
409 409 if f in m1:
410 410 continue
411 411 if f in copied:
412 412 continue
413 413 if f in copy:
414 414 f2 = copy[f]
415 415 if f2 not in m1: # directory rename
416 416 act("local renamed directory to " + f2, "d",
417 417 None, f, f2, m2.flags(f))
418 418 elif f2 in m2: # rename case 1, A/A,B/A
419 419 act("remote copied to " + f, "m",
420 420 f2, f, f, fmerge(f2, f, f2), False)
421 421 else: # case 3,20 A/B/A
422 422 act("remote moved to " + f, "m",
423 423 f2, f, f, fmerge(f2, f, f2), True)
424 424 elif f in ma:
425 425 if overwrite or backwards:
426 426 act("recreating", "g", f, m2.flags(f))
427 427 elif n != ma[f]:
428 428 if repo.ui.prompt(
429 429 (_("remote changed %s which local deleted\n") % f) +
430 430 _("(k)eep or (d)elete?"), _("[kd]"), _("k")) == _("k"):
431 431 act("prompt recreating", "g", f, m2.flags(f))
432 432 else:
433 433 act("remote created", "g", f, m2.flags(f))
434 434
435 435 return action
436 436
437 437 def applyupdates(repo, action, wctx, mctx):
438 438 "apply the merge action list to the working directory"
439 439
440 440 updated, merged, removed, unresolved = 0, 0, 0, 0
441 441 action.sort()
442 442 # prescan for copy/renames
443 443 for a in action:
444 444 f, m = a[:2]
445 445 if m == 'm': # merge
446 446 f2, fd, flags, move = a[2:]
447 447 if f != fd:
448 448 repo.ui.debug(_("copying %s to %s\n") % (f, fd))
449 449 repo.wwrite(fd, repo.wread(f), flags)
450 450
451 451 for a in action:
452 452 f, m = a[:2]
453 453 if f and f[0] == "/":
454 454 continue
455 455 if m == "r": # remove
456 456 repo.ui.note(_("removing %s\n") % f)
457 457 util.audit_path(f)
458 458 try:
459 459 util.unlink(repo.wjoin(f))
460 460 except OSError, inst:
461 461 if inst.errno != errno.ENOENT:
462 462 repo.ui.warn(_("update failed to remove %s: %s!\n") %
463 463 (f, inst.strerror))
464 464 removed += 1
465 465 elif m == "m": # merge
466 466 f2, fd, flags, move = a[2:]
467 467 r = filemerge(repo, f, fd, f2, wctx, mctx)
468 468 if r > 0:
469 469 unresolved += 1
470 470 else:
471 471 if r is None:
472 472 updated += 1
473 473 else:
474 474 merged += 1
475 475 util.set_exec(repo.wjoin(fd), "x" in flags)
476 476 if f != fd and move and util.lexists(repo.wjoin(f)):
477 477 repo.ui.debug(_("removing %s\n") % f)
478 478 os.unlink(repo.wjoin(f))
479 479 elif m == "g": # get
480 480 flags = a[2]
481 481 repo.ui.note(_("getting %s\n") % f)
482 482 t = mctx.filectx(f).data()
483 483 repo.wwrite(f, t, flags)
484 484 updated += 1
485 485 elif m == "d": # directory rename
486 486 f2, fd, flags = a[2:]
487 487 if f:
488 488 repo.ui.note(_("moving %s to %s\n") % (f, fd))
489 489 t = wctx.filectx(f).data()
490 490 repo.wwrite(fd, t, flags)
491 491 util.unlink(repo.wjoin(f))
492 492 if f2:
493 493 repo.ui.note(_("getting %s to %s\n") % (f2, fd))
494 494 t = mctx.filectx(f2).data()
495 495 repo.wwrite(fd, t, flags)
496 496 updated += 1
497 497 elif m == "dr": # divergent renames
498 498 fl = a[2]
499 499 repo.ui.warn("warning: detected divergent renames of %s to:\n" % f)
500 500 for nf in fl:
501 501 repo.ui.warn(" %s\n" % nf)
502 502 elif m == "e": # exec
503 503 flags = a[2]
504 504 util.set_exec(repo.wjoin(f), flags)
505 505
506 506 return updated, merged, removed, unresolved
507 507
508 508 def recordupdates(repo, action, branchmerge):
509 509 "record merge actions to the dirstate"
510 510
511 511 for a in action:
512 512 f, m = a[:2]
513 513 if m == "r": # remove
514 514 if branchmerge:
515 515 repo.dirstate.update([f], 'r')
516 516 else:
517 517 repo.dirstate.forget([f])
518 518 elif m == "f": # forget
519 519 repo.dirstate.forget([f])
520 520 elif m in "ge": # get or exec change
521 521 if branchmerge:
522 522 repo.dirstate.update([f], 'n', st_mtime=-1)
523 523 else:
524 524 repo.dirstate.update([f], 'n')
525 525 elif m == "m": # merge
526 526 f2, fd, flag, move = a[2:]
527 527 if branchmerge:
528 528 # We've done a branch merge, mark this file as merged
529 529 # so that we properly record the merger later
530 530 repo.dirstate.update([fd], 'm')
531 531 if f != f2: # copy/rename
532 532 if move:
533 533 repo.dirstate.update([f], 'r')
534 534 if f != fd:
535 535 repo.dirstate.copy(f, fd)
536 536 else:
537 537 repo.dirstate.copy(f2, fd)
538 538 else:
539 539 # We've update-merged a locally modified file, so
540 540 # we set the dirstate to emulate a normal checkout
541 541 # of that file some time in the past. Thus our
542 542 # merge will appear as a normal local file
543 543 # modification.
544 544 repo.dirstate.update([fd], 'n', st_size=-1, st_mtime=-1)
545 545 if move:
546 546 repo.dirstate.forget([f])
547 547 elif m == "d": # directory rename
548 548 f2, fd, flag = a[2:]
549 549 if not f2 and f not in repo.dirstate:
550 550 # untracked file moved
551 551 continue
552 552 if branchmerge:
553 553 repo.dirstate.update([fd], 'a')
554 554 if f:
555 555 repo.dirstate.update([f], 'r')
556 556 repo.dirstate.copy(f, fd)
557 557 if f2:
558 558 repo.dirstate.copy(f2, fd)
559 559 else:
560 560 repo.dirstate.update([fd], 'n')
561 561 if f:
562 562 repo.dirstate.forget([f])
563 563
564 564 def update(repo, node, branchmerge, force, partial, wlock):
565 565 """
566 566 Perform a merge between the working directory and the given node
567 567
568 568 branchmerge = whether to merge between branches
569 569 force = whether to force branch merging or file overwriting
570 570 partial = a function to filter file lists (dirstate not updated)
571 571 wlock = working dir lock, if already held
572 572 """
573 573
574 574 if not wlock:
575 575 wlock = repo.wlock()
576 576
577 577 wc = repo.workingctx()
578 578 if node is None:
579 579 # tip of current branch
580 580 try:
581 581 node = repo.branchtags()[wc.branch()]
582 582 except KeyError:
583 583 raise util.Abort(_("branch %s not found") % wc.branch())
584 584 overwrite = force and not branchmerge
585 585 forcemerge = force and branchmerge
586 586 pl = wc.parents()
587 587 p1, p2 = pl[0], repo.changectx(node)
588 588 pa = p1.ancestor(p2)
589 589 fp1, fp2, xp1, xp2 = p1.node(), p2.node(), str(p1), str(p2)
590 590 fastforward = False
591 591
592 592 ### check phase
593 593 if not overwrite and len(pl) > 1:
594 594 raise util.Abort(_("outstanding uncommitted merges"))
595 595 if pa == p1 or pa == p2: # is there a linear path from p1 to p2?
596 596 if branchmerge:
597 597 if p1.branch() != p2.branch() and pa != p2:
598 598 fastforward = True
599 599 else:
600 600 raise util.Abort(_("there is nothing to merge, just use "
601 601 "'hg update' or look at 'hg heads'"))
602 602 elif not (overwrite or branchmerge):
603 603 raise util.Abort(_("update spans branches, use 'hg merge' "
604 604 "or 'hg update -C' to lose changes"))
605 605 if branchmerge and not forcemerge:
606 606 if wc.files():
607 607 raise util.Abort(_("outstanding uncommitted changes"))
608 608
609 609 ### calculate phase
610 610 action = []
611 611 if not force:
612 612 checkunknown(wc, p2)
613 613 if not util.checkfolding(repo.path):
614 614 checkcollision(p2)
615 615 if not branchmerge:
616 616 action += forgetremoved(wc, p2)
617 617 action += manifestmerge(repo, wc, p2, pa, overwrite, partial)
618 618
619 619 ### apply phase
620 620 if not branchmerge: # just jump to the new rev
621 621 fp1, fp2, xp1, xp2 = fp2, nullid, xp2, ''
622 622 if not partial:
623 623 repo.hook('preupdate', throw=True, parent1=xp1, parent2=xp2)
624 624
625 625 stats = applyupdates(repo, action, wc, p2)
626 626
627 627 if not partial:
628 628 recordupdates(repo, action, branchmerge)
629 629 repo.dirstate.setparents(fp1, fp2)
630 630 if not branchmerge and not fastforward:
631 631 repo.dirstate.setbranch(p2.branch())
632 632 repo.hook('update', parent1=xp1, parent2=xp2, error=stats[3])
633 633
634 634 return stats
635 635
General Comments 0
You need to be logged in to leave comments. Login now