--- a/mercurial/merge.py Fri Sep 15 22:55:17 2006 +0200
+++ b/mercurial/merge.py Mon Sep 18 11:55:38 2006 +0200
@@ -10,11 +10,6 @@
from demandload import *
demandload(globals(), "errno util os tempfile")
-def fmerge(f, local, other, ancestor):
- """merge executable flags"""
- a, b, c = ancestor.execf(f), local.execf(f), other.execf(f)
- return ((a^b) | (a^c)) ^ a
-
def merge3(repo, fn, my, other, p1, p2):
"""perform a 3-way merge in the working directory"""
@@ -52,6 +47,193 @@
os.unlink(c)
return r
+def checkunknown(repo, m2, status):
+ """
+ check for collisions between unknown files and files in m2
+ """
+ modified, added, removed, deleted, unknown = status[:5]
+ for f in unknown:
+ if f in m2:
+ if repo.file(f).cmp(m2[f], repo.wread(f)):
+ raise util.Abort(_("'%s' already exists in the working"
+ " dir and differs from remote") % f)
+
+def workingmanifest(repo, man, status):
+ """
+ Update manifest to correspond to the working directory
+ """
+
+ modified, added, removed, deleted, unknown = status[:5]
+ for i,l in (("a", added), ("m", modified), ("u", unknown)):
+ for f in l:
+ man[f] = man.get(f, nullid) + i
+ man.set(f, util.is_exec(repo.wjoin(f), man.execf(f)))
+
+ for f in deleted + removed:
+ del man[f]
+
+ return man
+
+def forgetremoved(m2, status):
+ """
+ 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.
+ """
+
+ modified, added, removed, deleted, unknown = status[:5]
+ action = []
+
+ for f in deleted + removed:
+ if f not in m2:
+ action.append((f, "f"))
+
+ return action
+
+def manifestmerge(ui, m1, m2, ma, overwrite, backwards, partial):
+ """
+ Merge manifest m1 with m2 using ancestor ma and generate merge action list
+ """
+
+ def fmerge(f):
+ """merge executable flags"""
+ a, b, c = ma.execf(f), m1.execf(f), m2.execf(f)
+ return ((a^b) | (a^c)) ^ a
+
+ action = []
+
+ def act(msg, f, m, *args):
+ ui.debug(" %s: %s -> %s\n" % (f, msg, m))
+ action.append((f, m) + args)
+
+ # Filter manifests
+ if partial:
+ for f in m1.keys():
+ if not partial(f): del m1[f]
+ for f in m2.keys():
+ if not partial(f): del m2[f]
+
+ # Compare manifests
+ for f, n in m1.iteritems():
+ if f in m2:
+ # are files different?
+ if n != m2[f]:
+ a = ma.get(f, nullid)
+ # are both different from the ancestor?
+ if not overwrite and n != a and m2[f] != a:
+ act("versions differ", f, "m", fmerge(f), n[:20], m2[f])
+ # are we clobbering?
+ # is remote's version newer?
+ # or are we going back in time and clean?
+ elif overwrite or m2[f] != a or (backwards and not n[20:]):
+ act("remote is newer", f, "g", m2.execf(f), m2[f])
+ # local is newer, not overwrite, check mode bits
+ elif fmerge(f) != m1.execf(f):
+ act("update permissions", f, "e", m2.execf(f))
+ # contents same, check mode bits
+ elif m1.execf(f) != m2.execf(f):
+ if overwrite or fmerge(f) != m1.execf(f):
+ act("update permissions", f, "e", m2.execf(f))
+ del m2[f]
+ elif f in ma:
+ if n != ma[f] and not overwrite:
+ if ui.prompt(
+ (_(" local changed %s which remote deleted\n") % f) +
+ _("(k)eep or (d)elete?"), _("[kd]"), _("k")) == _("d"):
+ act("prompt delete", f, "r")
+ else:
+ act("other deleted", f, "r")
+ else:
+ # file is created on branch or in working directory
+ if (overwrite and n[20:] != "u") or (backwards and not n[20:]):
+ act("remote deleted", f, "r")
+
+ for f, n in m2.iteritems():
+ if f in ma:
+ if overwrite or backwards:
+ act("recreating", f, "g", m2.execf(f), n)
+ elif n != ma[f]:
+ if ui.prompt(
+ (_("remote changed %s which local deleted\n") % f) +
+ _("(k)eep or (d)elete?"), _("[kd]"), _("k")) == _("k"):
+ act("prompt recreating", f, "g", m2.execf(f), n)
+ else:
+ act("remote created", f, "g", m2.execf(f), n)
+
+ return action
+
+def applyupdates(repo, action, xp1, xp2):
+ updated, merged, removed, unresolved = 0, 0, 0, 0
+ action.sort()
+ for a in action:
+ f, m = a[:2]
+ if f[0] == "/":
+ continue
+ if m == "r": # remove
+ repo.ui.note(_("removing %s\n") % f)
+ util.audit_path(f)
+ try:
+ util.unlink(repo.wjoin(f))
+ except OSError, inst:
+ if inst.errno != errno.ENOENT:
+ repo.ui.warn(_("update failed to remove %s: %s!\n") %
+ (f, inst.strerror))
+ removed +=1
+ elif m == "m": # merge
+ flag, my, other = a[2:]
+ repo.ui.status(_("merging %s\n") % f)
+ if merge3(repo, f, my, other, xp1, xp2):
+ unresolved += 1
+ util.set_exec(repo.wjoin(f), flag)
+ merged += 1
+ elif m == "g": # get
+ flag, node = a[2:]
+ repo.ui.note(_("getting %s\n") % f)
+ t = repo.file(f).read(node)
+ repo.wwrite(f, t)
+ util.set_exec(repo.wjoin(f), flag)
+ updated += 1
+ elif m == "e": # exec
+ flag = a[2:]
+ util.set_exec(repo.wjoin(f), flag)
+
+ return updated, merged, removed, unresolved
+
+def recordupdates(repo, action, branchmerge):
+ for a in action:
+ f, m = a[:2]
+ if m == "r": # remove
+ if branchmerge:
+ repo.dirstate.update([f], 'r')
+ else:
+ repo.dirstate.forget([f])
+ elif m == "f": # forget
+ repo.dirstate.forget([f])
+ elif m == "g": # get
+ if branchmerge:
+ repo.dirstate.update([f], 'n', st_mtime=-1)
+ else:
+ repo.dirstate.update([f], 'n')
+ elif m == "m": # merge
+ flag, my, other = a[2:]
+ if branchmerge:
+ # We've done a branch merge, mark this file as merged
+ # so that we properly record the merger later
+ repo.dirstate.update([f], 'm')
+ else:
+ # We've update-merged a locally modified file, so
+ # we set the dirstate to emulate a normal checkout
+ # of that file some time in the past. Thus our
+ # merge will appear as a normal local file
+ # modification.
+ fl = repo.file(f)
+ f_len = fl.size(fl.rev(other))
+ repo.dirstate.update([f], 'n', st_size=f_len, st_mtime=-1)
+
def update(repo, node, branchmerge=False, force=False, partial=None,
wlock=None, show_stats=True, remind=True):
@@ -74,238 +256,65 @@
backwards = (pa == p2)
# is there a linear path from p1 to p2?
- linear_path = (pa == p1 or pa == p2)
- if branchmerge and linear_path:
- raise util.Abort(_("there is nothing to merge, just use "
- "'hg update' or look at 'hg heads'"))
-
- if not linear_path and not (overwrite or branchmerge):
+ if pa == p1 or pa == p2:
+ if branchmerge:
+ raise util.Abort(_("there is nothing to merge, just use "
+ "'hg update' or look at 'hg heads'"))
+ elif not (overwrite or branchmerge):
raise util.Abort(_("update spans branches, use 'hg merge' "
"or 'hg update -C' to lose changes"))
- modified, added, removed, deleted, unknown = repo.status()[:5]
+ status = repo.status()
+ modified, added, removed, deleted, unknown = status[:5]
if branchmerge and not forcemerge:
if modified or added or removed:
raise util.Abort(_("outstanding uncommitted changes"))
- m1n = repo.changelog.read(p1)[0]
- m2n = repo.changelog.read(p2)[0]
- man = repo.manifest.ancestor(m1n, m2n)
- m1 = repo.manifest.read(m1n).copy()
- m2 = repo.manifest.read(m2n).copy()
- ma = repo.manifest.read(man)
-
- if not force:
- for f in unknown:
- if f in m2:
- if repo.file(f).cmp(m2[f], repo.wread(f)):
- raise util.Abort(_("'%s' already exists in the working"
- " dir and differs from remote") % f)
+ m1 = repo.changectx(p1).manifest().copy()
+ m2 = repo.changectx(p2).manifest().copy()
+ ma = repo.changectx(pa).manifest()
# resolve the manifest to determine which files
# we care about merging
repo.ui.note(_("resolving manifests\n"))
- repo.ui.debug(_(" overwrite %s branchmerge %s partial %s linear %s\n") %
- (overwrite, branchmerge, bool(partial), linear_path))
+ repo.ui.debug(_(" overwrite %s branchmerge %s partial %s\n") %
+ (overwrite, branchmerge, bool(partial)))
repo.ui.debug(_(" ancestor %s local %s remote %s\n") %
- (short(man), short(m1n), short(m2n)))
-
- action = {}
- forget = []
-
- # update m1 from working dir
- umap = dict.fromkeys(unknown)
-
- for f in added + modified + unknown:
- m1[f] = m1.get(f, nullid) + "+"
- m1.set(f, util.is_exec(repo.wjoin(f), m1.execf(f)))
-
- for f in deleted + removed:
- del m1[f]
-
- # 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.
- if linear_path and f not in m2:
- forget.append(f)
-
- if partial:
- for f in m1.keys():
- if not partial(f): del m1[f]
- for f in m2.keys():
- if not partial(f): del m2[f]
-
- # Compare manifests
- for f, n in m1.iteritems():
- if f in m2:
- queued = 0
-
- # are files different?
- if n != m2[f]:
- a = ma.get(f, nullid)
- # are both different from the ancestor?
- if not overwrite and n != a and m2[f] != a:
- repo.ui.debug(_(" %s versions differ, resolve\n") % f)
- action[f] = (fmerge(f, m1, m2, ma), n[:20], m2[f])
- queued = 1
- # are we clobbering?
- # is remote's version newer?
- # or are we going back in time and clean?
- elif overwrite or m2[f] != a or (backwards and not n[20:]):
- repo.ui.debug(_(" remote %s is newer, get\n") % f)
- action[f] = (m2.execf(f), m2[f], None)
- queued = 1
- elif f in umap or f in added:
- # this unknown file is the same as the checkout
- # we need to reset the dirstate if the file was added
- action[f] = (m2.execf(f), m2[f], None)
+ (short(p1), short(p2), short(pa)))
- # do we still need to look at mode bits?
- if not queued and m1.execf(f) != m2.execf(f):
- if overwrite:
- repo.ui.debug(_(" updating permissions for %s\n") % f)
- util.set_exec(repo.wjoin(f), m2.execf(f))
- else:
- mode = fmerge(f, m1, m2, ma)
- if mode != m1.execf(f):
- repo.ui.debug(_(" updating permissions for %s\n")
- % f)
- util.set_exec(repo.wjoin(f), mode)
- del m2[f]
- elif f in ma:
- if n != ma[f]:
- r = _("d")
- if not overwrite:
- r = repo.ui.prompt(
- (_(" local changed %s which remote deleted\n") % f) +
- _("(k)eep or (d)elete?"), _("[kd]"), _("k"))
- if r == _("d"):
- action[f] = (None, None, None)
- else:
- repo.ui.debug(_("other deleted %s\n") % f)
- action[f] = (None, None, None)
- else:
- # file is created on branch or in working directory
- if overwrite and f not in umap:
- repo.ui.debug(_("remote deleted %s, clobbering\n") % f)
- action[f] = (None, None, None)
- elif not n[20:]: # same as parent
- if backwards:
- repo.ui.debug(_("remote deleted %s\n") % f)
- action[f] = (None, None, None)
- else:
- repo.ui.debug(_("local modified %s, keeping\n") % f)
- else:
- repo.ui.debug(_("working dir created %s, keeping\n") % f)
+ action = []
+ m1 = workingmanifest(repo, m1, status)
- for f, n in m2.iteritems():
- if f[0] == "/":
- continue
- if f in ma and n != ma[f]:
- r = _("k")
- if not overwrite:
- r = repo.ui.prompt(
- (_("remote changed %s which local deleted\n") % f) +
- _("(k)eep or (d)elete?"), _("[kd]"), _("k"))
- if r == _("k"):
- action[f] = (m2.execf(f), n, None)
- elif f not in ma:
- repo.ui.debug(_("remote created %s\n") % f)
- action[f] = (m2.execf(f), n, None)
- else:
- if overwrite or backwards:
- repo.ui.debug(_("local deleted %s, recreating\n") % f)
- action[f] = (m2.execf(f), n, None)
- else:
- repo.ui.debug(_("local deleted %s\n") % f)
-
+ if not force:
+ checkunknown(repo, m2, status)
+ if not branchmerge:
+ action += forgetremoved(m2, status)
+ action += manifestmerge(repo.ui, m1, m2, ma, overwrite, backwards, partial)
del m1, m2, ma
### apply phase
- if linear_path or overwrite:
+ if not branchmerge:
# we don't need to do any magic, just jump to the new rev
p1, p2 = p2, nullid
- xp1 = hex(p1)
- xp2 = hex(p2)
- if p2 == nullid: xxp2 = ''
- else: xxp2 = xp2
-
- repo.hook('preupdate', throw=True, parent1=xp1, parent2=xxp2)
+ xp1, xp2 = hex(p1), hex(p2)
+ if p2 == nullid: xp2 = ''
- # update files
- unresolved = []
- updated, merged, removed = 0, 0, 0
- files = action.keys()
- files.sort()
- for f in files:
- flag, my, other = action[f]
- if f[0] == "/":
- continue
- if not my:
- repo.ui.note(_("removing %s\n") % f)
- util.audit_path(f)
- try:
- util.unlink(repo.wjoin(f))
- except OSError, inst:
- if inst.errno != errno.ENOENT:
- repo.ui.warn(_("update failed to remove %s: %s!\n") %
- (f, inst.strerror))
- removed +=1
- elif other:
- repo.ui.status(_("merging %s\n") % f)
- if merge3(repo, f, my, other, xp1, xp2):
- unresolved.append(f)
- util.set_exec(repo.wjoin(f), flag)
- merged += 1
- else:
- repo.ui.note(_("getting %s\n") % f)
- t = repo.file(f).read(my)
- repo.wwrite(f, t)
- util.set_exec(repo.wjoin(f), flag)
- updated += 1
+ repo.hook('preupdate', throw=True, parent1=xp1, parent2=xp2)
+
+ updated, merged, removed, unresolved = applyupdates(repo, action, xp1, xp2)
# update dirstate
if not partial:
repo.dirstate.setparents(p1, p2)
- repo.dirstate.forget(forget)
- files = action.keys()
- files.sort()
- for f in files:
- flag, my, other = action[f]
- if not my:
- if branchmerge:
- repo.dirstate.update([f], 'r')
- else:
- repo.dirstate.forget([f])
- elif not other:
- if branchmerge:
- repo.dirstate.update([f], 'n', st_mtime=-1)
- else:
- repo.dirstate.update([f], 'n')
- else:
- if branchmerge:
- # We've done a branch merge, mark this file as merged
- # so that we properly record the merger later
- repo.dirstate.update([f], 'm')
- else:
- # We've update-merged a locally modified file, so
- # we set the dirstate to emulate a normal checkout
- # of that file some time in the past. Thus our
- # merge will appear as a normal local file
- # modification.
- fl = repo.file(f)
- f_len = fl.size(fl.rev(other))
- repo.dirstate.update([f], 'n', st_size=f_len, st_mtime=-1)
+ recordupdates(repo, action, branchmerge)
if show_stats:
stats = ((updated, _("updated")),
- (merged - len(unresolved), _("merged")),
+ (merged - unresolved, _("merged")),
(removed, _("removed")),
- (len(unresolved), _("unresolved")))
+ (unresolved, _("unresolved")))
note = ", ".join([_("%d files %s") % s for s in stats])
repo.ui.status("%s\n" % note)
if not partial:
@@ -323,6 +332,6 @@
repo.ui.status(_("There are unresolved merges with"
" locally modified files.\n"))
- repo.hook('update', parent1=xp1, parent2=xxp2, error=len(unresolved))
- return len(unresolved)
+ repo.hook('update', parent1=xp1, parent2=xp2, error=unresolved)
+ return unresolved
--- a/tests/test-trusted.py Fri Sep 15 22:55:17 2006 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,112 +0,0 @@
-#!/usr/bin/env python
-# Since it's not easy to write a test that portably deals
-# with files from different users/groups, we cheat a bit by
-# monkey-patching some functions in the util module
-
-import os
-from mercurial import ui, util
-
-hgrc = os.environ['HGRCPATH']
-
-def testui(user='foo', group='bar', tusers=(), tgroups=(),
- cuser='foo', cgroup='bar'):
- # user, group => owners of the file
- # tusers, tgroups => trusted users/groups
- # cuser, cgroup => user/group of the current process
-
- # write a global hgrc with the list of trusted users/groups and
- # some setting so that we can be sure it was read
- f = open(hgrc, 'w')
- f.write('[paths]\n')
- f.write('global = /some/path\n\n')
-
- if tusers or tgroups:
- f.write('[trusted]\n')
- if tusers:
- f.write('users = %s\n' % ', '.join(tusers))
- if tgroups:
- f.write('groups = %s\n' % ', '.join(tgroups))
- f.close()
-
- # override the functions that give names to uids and gids
- def username(uid=None):
- if uid is None:
- return cuser
- return user
- util.username = username
-
- def groupname(gid=None):
- if gid is None:
- return 'bar'
- return group
- util.groupname = groupname
-
- # try to read everything
- #print '# File belongs to user %s, group %s' % (user, group)
- #print '# trusted users = %s; trusted groups = %s' % (tusers, tgroups)
- kind = ('different', 'same')
- who = ('', 'user', 'group', 'user and the group')
- trusted = who[(user in tusers) + 2*(group in tgroups)]
- if trusted:
- trusted = ', but we trust the ' + trusted
- print '# %s user, %s group%s' % (kind[user == cuser], kind[group == cgroup],
- trusted)
-
- parentui = ui.ui()
- u = ui.ui(parentui=parentui)
- u.readconfig('.hg/hgrc')
- for name, path in u.configitems('paths'):
- print name, '=', path
- print
-
- return u
-
-os.mkdir('repo')
-os.chdir('repo')
-os.mkdir('.hg')
-f = open('.hg/hgrc', 'w')
-f.write('[paths]\n')
-f.write('local = /another/path\n\n')
-f.close()
-
-#print '# Everything is run by user foo, group bar\n'
-
-# same user, same group
-testui()
-# same user, different group
-testui(group='def')
-# different user, same group
-testui(user='abc')
-# ... but we trust the group
-testui(user='abc', tgroups=['bar'])
-# different user, different group
-testui(user='abc', group='def')
-# ... but we trust the user
-testui(user='abc', group='def', tusers=['abc'])
-# ... but we trust the group
-testui(user='abc', group='def', tgroups=['def'])
-# ... but we trust the user and the group
-testui(user='abc', group='def', tusers=['abc'], tgroups=['def'])
-# ... but we trust all users
-print '# we trust all users'
-testui(user='abc', group='def', tusers=['*'])
-# ... but we trust all groups
-print '# we trust all groups'
-testui(user='abc', group='def', tgroups=['*'])
-# ... but we trust the whole universe
-print '# we trust all users and groups'
-testui(user='abc', group='def', tusers=['*'], tgroups=['*'])
-# ... check that users and groups are in different namespaces
-print "# we don't get confused by users and groups with the same name"
-testui(user='abc', group='def', tusers=['def'], tgroups=['abc'])
-# ... lists of user names work
-print "# list of user names"
-testui(user='abc', group='def', tusers=['foo', 'xyz', 'abc', 'bleh'],
- tgroups=['bar', 'baz', 'qux'])
-# ... lists of group names work
-print "# list of group names"
-testui(user='abc', group='def', tusers=['foo', 'xyz', 'bleh'],
- tgroups=['bar', 'def', 'baz', 'qux'])
-
-print "# Can't figure out the name of the user running this process"
-testui(user='abc', group='def', cuser=None)