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