# merge.py - directory-level update/merge handling for Mercurial
#
# Copyright 2006, 2007 Matt Mackall <mpm@selenic.com>
#
# This software may be used and distributed according to the terms
# of the GNU General Public License, incorporated herein by reference.

from node import *
from i18n import _
import errno, util, os, tempfile, context, heapq

def filemerge(repo, fw, fd, fo, wctx, mctx):
"""perform a 3-way merge in the working directory

fw = original filename in the working directory
fd = destination filename in the working directory
fo = filename in other parent
wctx, mctx = working and merge changecontexts
"""

def temp(prefix, ctx):
pre = "%s~%s." % (os.path.basename(ctx.path()), prefix)
(fd, name) = tempfile.mkstemp(prefix=pre)
data = repo.wwritedata(ctx.path(), ctx.data())
f = os.fdopen(fd, "wb")
f.write(data)
f.close()
return name

fcm = wctx.filectx(fw)
fcmdata = wctx.filectx(fd).data()
fco = mctx.filectx(fo)

if not fco.cmp(fcmdata): # files identical?
return None

fca = fcm.ancestor(fco)
if not fca:
fca = repo.filectx(fw, fileid=nullrev)
a = repo.wjoin(fd)
b = temp("base", fca)
c = temp("other", fco)

if fw != fo:
repo.ui.status(_("merging %s and %s\n") % (fw, fo))
else:
repo.ui.status(_("merging %s\n") % fw)

repo.ui.debug(_("my %s other %s ancestor %s\n") % (fcm, fco, fca))

cmd = (os.environ.get("HGMERGE") or repo.ui.config("ui", "merge")
or "hgmerge")
r = util.system('%s "%s" "%s" "%s"' % (cmd, a, b, c), cwd=repo.root,
environ={'HG_FILE': fd,
'HG_MY_NODE': str(wctx.parents()[0]),
'HG_OTHER_NODE': str(mctx),
'HG_MY_ISLINK': fcm.islink(),
'HG_OTHER_ISLINK': fco.islink(),
'HG_BASE_ISLINK': fca.islink(),})
if r:
repo.ui.warn(_("merging %s failed!\n") % fd)

os.unlink(b)
os.unlink(c)
return r

def checkunknown(wctx, mctx):
"check for collisions between unknown files and files in mctx"
man = mctx.manifest()
for f in wctx.unknown():
if f in man:
if mctx.filectx(f).cmp(wctx.filectx(f).data()):
raise util.Abort(_("untracked local file '%s' differs"
" from remote version") % f)

def checkcollision(mctx):
"check for case folding collisions in the destination context"
folded = {}
for fn in mctx.manifest():
fold = fn.lower()
if fold in folded:
raise util.Abort(_("case-folding collision between %s and %s")
% (fn, folded[fold]))
folded[fold] = fn

def forgetremoved(wctx, mctx):
"""
Forget removed files

If we're jumping between revisions (as opposed to merging), and if
neither the working directory nor the target rev has the file,
then we need to remove it from the dirstate, to prevent the
dirstate from listing the file when it is no longer in the
manifest.
"""

action = []
man = mctx.manifest()
for f in wctx.deleted() + wctx.removed():
if f not in man:
action.append((f, "f"))

return action

def findcopies(repo, m1, m2, ma, limit):
"""
Find moves and copies between m1 and m2 back to limit linkrev
"""

def nonoverlap(d1, d2, d3):
"Return list of elements in d1 not in d2 or d3"
l = [d for d in d1 if d not in d3 and d not in d2]
l.sort()
return l

def dirname(f):
s = f.rfind("/")
if s == -1:
return ""
return f[:s]

def dirs(files):
d = {}
for f in files:
f = dirname(f)
while f not in d:
d[f] = True
f = dirname(f)
return d

wctx = repo.workingctx()

def makectx(f, n):
if len(n) == 20:
return repo.filectx(f, fileid=n)
return wctx.filectx(f)
ctx = util.cachefunc(makectx)

def findold(fctx):
"find files that path was copied from, back to linkrev limit"
old = {}
seen = {}
orig = fctx.path()
visit = [fctx]
while visit:
fc = visit.pop()
s = str(fc)
if s in seen:
continue
seen[s] = 1
if fc.path() != orig and fc.path() not in old:
old[fc.path()] = 1
if fc.rev() < limit:
continue
visit += fc.parents()

old = old.keys()
old.sort()
return old

copy = {}
fullcopy = {}
diverge = {}

def checkcopies(c, man, aman):
'''check possible copies for filectx c'''
for of in findold(c):
fullcopy[c.path()] = of # remember for dir rename detection
if of not in man: # original file not in other manifest?
if of in ma:
diverge.setdefault(of, []).append(c.path())
continue
# if the original file is unchanged on the other branch,
# no merge needed
if man[of] == aman.get(of):
continue
c2 = ctx(of, man[of])
ca = c.ancestor(c2)
if not ca: # unrelated?
continue
# named changed on only one side?
if ca.path() == c.path() or ca.path() == c2.path():
if c == ca and c2 == ca: # no merge needed, ignore copy
continue
copy[c.path()] = of

if not repo.ui.configbool("merge", "followcopies", True):
return {}, {}

# avoid silly behavior for update from empty dir
if not m1 or not m2 or not ma:
return {}, {}

repo.ui.debug(_(" searching for copies back to rev %d\n") % limit)

u1 = nonoverlap(m1, m2, ma)
u2 = nonoverlap(m2, m1, ma)

if u1:
repo.ui.debug(_(" unmatched files in local:\n %s\n")
% "\n ".join(u1))
if u2:
repo.ui.debug(_(" unmatched files in other:\n %s\n")
% "\n ".join(u2))

for f in u1:
checkcopies(ctx(f, m1[f]), m2, ma)

for f in u2:
checkcopies(ctx(f, m2[f]), m1, ma)

d2 = {}
for of, fl in diverge.items():
for f in fl:
fo = list(fl)
fo.remove(f)
d2[f] = (of, fo)

if fullcopy:
repo.ui.debug(_(" all copies found (* = to merge, ! = divergent):\n"))
for f in fullcopy:
note = ""
if f in copy: note += "*"
if f in diverge: note += "!"
repo.ui.debug(_(" %s -> %s %s\n") % (f, fullcopy[f], note))

if not fullcopy or not repo.ui.configbool("merge", "followdirs", True):
return copy, diverge

repo.ui.debug(_(" checking for directory renames\n"))

# generate a directory move map
d1, d2 = dirs(m1), dirs(m2)
invalid = {}
dirmove = {}

# examine each file copy for a potential directory move, which is
# when all the files in a directory are moved to a new directory
for dst, src in fullcopy.items():
dsrc, ddst = dirname(src), dirname(dst)
if dsrc in invalid:
# already seen to be uninteresting
continue
elif dsrc in d1 and ddst in d1:
# directory wasn't entirely moved locally
invalid[dsrc] = True
elif dsrc in d2 and ddst in d2:
# directory wasn't entirely moved remotely
invalid[dsrc] = True
elif dsrc in dirmove and dirmove[dsrc] != ddst:
# files from the same directory moved to two different places
invalid[dsrc] = True
else:
# looks good so far
dirmove[dsrc + "/"] = ddst + "/"

for i in invalid:
if i in dirmove:
del dirmove[i]

del d1, d2, invalid

if not dirmove:
return copy, diverge

for d in dirmove:
repo.ui.debug(_(" dir %s -> %s\n") % (d, dirmove[d]))

# check unaccounted nonoverlapping files against directory moves
for