Merge with upstream
authorBrendan Cully <brendan@kublai.com>
Tue, 03 Oct 2006 12:14:33 -0700
changeset 3241 a184cd0c2db9
parent 3240 e0069e7fe419 (current diff)
parent 3218 8d4855fd9d7b (diff)
child 3242 1539f788e913
Merge with upstream
mercurial/context.py
mercurial/localrepo.py
--- a/mercurial/context.py	Tue Oct 03 11:54:11 2006 +0200
+++ b/mercurial/context.py	Tue Oct 03 12:14:33 2006 -0700
@@ -33,7 +33,7 @@
         return short(self.node())
 
     def __repr__(self):
-        return "<changectx %s>" % short(self.node())
+        return "<changectx %s>" % str(self)
 
     def __eq__(self, other):
         return self._rev == other._rev
@@ -41,26 +41,25 @@
     def __nonzero__(self):
         return self._rev != -1
 
-    def changeset(self):
-        try:
-            return self._changeset
-        except AttributeError:
+    def __getattr__(self, name):
+        if name == '_changeset':
             self._changeset = self._repo.changelog.read(self.node())
             return self._changeset
-
-    def manifest(self):
-        try:
+        elif name == '_manifest':
+            self._manifest = self._repo.manifest.read(self._changeset[0])
             return self._manifest
-        except AttributeError:
-            self._manifest = self._repo.manifest.read(self.changeset()[0])
-            return self._manifest
+        else:
+            raise AttributeError, name
+
+    def changeset(self): return self._changeset
+    def manifest(self): return self._manifest
 
     def rev(self): return self._rev
     def node(self): return self._node
-    def user(self): return self.changeset()[1]
-    def date(self): return self.changeset()[2]
-    def files(self): return self.changeset()[3]
-    def description(self): return self.changeset()[4]
+    def user(self): return self._changeset[1]
+    def date(self): return self._changeset[2]
+    def files(self): return self._changeset[3]
+    def description(self): return self._changeset[4]
 
     def parents(self):
         """return contexts for each parent changeset"""
@@ -73,17 +72,16 @@
         return [ changectx(self._repo, x) for x in c ]
 
     def filenode(self, path):
-        node, flag = self._repo.manifest.find(self.changeset()[0], path)
+        if hasattr(self, "_manifest"):
+            return self._manifest[path]
+        node, flag = self._repo.manifest.find(self._changeset[0], path)
         return node
 
     def filectx(self, path, fileid=None):
         """get a file context from this changeset"""
         if fileid is None:
             fileid = self.filenode(path)
-            if not fileid:
-                raise repo.LookupError(_("'%s' does not exist in changeset %s") %
-                                       (path, hex(self.node())))
-        return filectx(self._repo, path, fileid=fileid)
+        return filectx(self._repo, path, fileid=fileid, changectx=self)
 
     def filectxs(self):
         """generate a file context for each file in this changeset's
@@ -104,34 +102,44 @@
 class filectx(object):
     """A filecontext object makes access to data related to a particular
        filerevision convenient."""
-    def __init__(self, repo_, path, changeid=None, fileid=None, filelog=None):
+    def __init__(self, repo, path, changeid=None, fileid=None,
+                 filelog=None, changectx=None):
         """changeid can be a changeset revision, node, or tag.
            fileid can be a file revision or node."""
-        self._repo = repo_
+        self._repo = repo
         self._path = path
 
         assert changeid is not None or fileid is not None
 
         if filelog:
             self._filelog = filelog
-        else:
-            self._filelog = self._repo.file(self._path)
+        if changectx:
+            self._changectx = changectx
+            self._changeid = changectx.node()
 
         if fileid is None:
             self._changeid = changeid
         else:
-            try:
-                self._filenode = self._filelog.lookup(fileid)
-            except revlog.RevlogError, inst:
-                raise repo.LookupError(str(inst))
-            self._changeid = self._filelog.linkrev(self._filenode)
+            self._fileid = fileid
 
     def __getattr__(self, name):
         if name == '_changectx':
             self._changectx = changectx(self._repo, self._changeid)
             return self._changectx
+        elif name == '_filelog':
+            self._filelog = self._repo.file(self._path)
+            return self._filelog
+        elif name == '_changeid':
+            self._changeid = self._filelog.linkrev(self._filenode)
+            return self._changeid
         elif name == '_filenode':
-            self._filenode = self._changectx.filenode(self._path)
+            try:
+                if hasattr(self, "_fileid"):
+                    self._filenode = self._filelog.lookup(self._fileid)
+                else:
+                    self._filenode = self._changectx.filenode(self._path)
+            except revlog.RevlogError, inst:
+                raise repo.LookupError(str(inst))
             return self._filenode
         elif name == '_filerev':
             self._filerev = self._filelog.rev(self._filenode)
@@ -146,7 +154,7 @@
         return "%s@%s" % (self.path(), short(self.node()))
 
     def __repr__(self):
-        return "<filectx %s@%s>" % (self.path(), short(self.node()))
+        return "<filectx %s>" % str(self)
 
     def __eq__(self, other):
         return self._path == other._path and self._changeid == other._changeid
@@ -218,7 +226,10 @@
         def parents(f):
             # we want to reuse filectx objects as much as possible
             p = f._path
-            pl = [ (p, r) for r in f._filelog.parentrevs(f._filerev) ]
+            if f._filerev is None: # working dir
+                pl = [ (n.path(), n.filerev()) for n in f.parents() ]
+            else:
+                pl = [ (p, n) for n in f._filelog.parentrevs(f._filerev) ]
 
             if follow:
                 r = f.renamed()
@@ -271,6 +282,13 @@
         """
 
         acache = {}
+
+        # prime the ancestor cache for the working directory
+        for c in (self, fc2):
+            if c._filerev == None:
+                pl = [ (n.path(), n.filenode()) for n in c.parents() ]
+                acache[(c._path, None)] = pl
+
         flcache = {self._path:self._filelog, fc2._path:fc2._filelog}
         def parents(vertex):
             if vertex in acache:
@@ -293,3 +311,149 @@
             return filectx(self._repo, f, fileid=n, filelog=flcache[f])
 
         return None
+
+class workingctx(changectx):
+    """A workingctx object makes access to data related to
+    the current working directory convenient."""
+    def __init__(self, repo):
+        self._repo = repo
+        self._rev = None
+        self._node = None
+
+    def __str__(self):
+        return "."
+
+    def __nonzero__(self):
+        return True
+
+    def __getattr__(self, name):
+        if name == '_parents':
+            self._parents = self._repo.parents()
+            return self._parents
+        if name == '_status':
+            self._status = self._repo.status()
+            return self._status
+        if name == '_manifest':
+            self._buildmanifest()
+            return self._manifest
+        else:
+            raise AttributeError, name
+
+    def _buildmanifest(self):
+        """generate a manifest corresponding to the working directory"""
+
+        man = self._parents[0].manifest().copy()
+        copied = self._repo.dirstate.copies()
+        modified, added, removed, deleted, unknown = self._status[:5]
+        for i,l in (("a", added), ("m", modified), ("u", unknown)):
+            for f in l:
+                man[f] = man.get(copied.get(f, f), nullid) + i
+                man.set(f, util.is_exec(self._repo.wjoin(f), man.execf(f)))
+
+        for f in deleted + removed:
+            del man[f]
+
+        self._manifest = man
+
+    def manifest(self): return self._manifest
+
+    def user(self): return self._repo.ui.username()
+    def date(self): return util.makedate()
+    def description(self): return ""
+    def files(self):
+        f = self.modified() + self.added() + self.removed()
+        f.sort()
+        return f
+
+    def modified(self): return self._status[0]
+    def added(self): return self._status[1]
+    def removed(self): return self._status[2]
+    def deleted(self): return self._status[3]
+    def unknown(self): return self._status[4]
+    def clean(self): return self._status[5]
+
+    def parents(self):
+        """return contexts for each parent changeset"""
+        return self._parents
+
+    def children(self):
+        return []
+
+    def filectx(self, path):
+        """get a file context from the working directory"""
+        return workingfilectx(self._repo, path, workingctx=self)
+
+    def ancestor(self, c2):
+        """return the ancestor context of self and c2"""
+        return self._parents[0].ancestor(c2) # punt on two parents for now
+
+class workingfilectx(filectx):
+    """A workingfilectx object makes access to data related to a particular
+       file in the working directory convenient."""
+    def __init__(self, repo, path, filelog=None, workingctx=None):
+        """changeid can be a changeset revision, node, or tag.
+           fileid can be a file revision or node."""
+        self._repo = repo
+        self._path = path
+        self._changeid = None
+        self._filerev = self._filenode = None
+
+        if filelog:
+            self._filelog = filelog
+        if workingctx:
+            self._changectx = workingctx
+
+    def __getattr__(self, name):
+        if name == '_changectx':
+            self._changectx = workingctx(repo)
+            return self._changectx
+        elif name == '_repopath':
+            self._repopath = self._repo.dirstate.copied(p) or self._path
+        elif name == '_filelog':
+            self._filelog = self._repo.file(self._repopath)
+            return self._filelog
+        else:
+            raise AttributeError, name
+
+    def __nonzero__(self):
+        return True
+
+    def __str__(self):
+        return "%s@." % self.path()
+
+    def filectx(self, fileid):
+        '''opens an arbitrary revision of the file without
+        opening a new filelog'''
+        return filectx(self._repo, self._repopath, fileid=fileid,
+                       filelog=self._filelog)
+
+    def rev(self):
+        if hasattr(self, "_changectx"):
+            return self._changectx.rev()
+        return self._filelog.linkrev(self._filenode)
+
+    def data(self): return self._repo.wread(self._path)
+    def renamed(self):
+        rp = self._repopath
+        if rp == self._path:
+            return None
+        return rp, self._workingctx._parents._manifest.get(rp, nullid)
+
+    def parents(self):
+        '''return parent filectxs, following copies if necessary'''
+        p = self._path
+        rp = self._repopath
+        pcl = self._workingctx._parents
+        fl = self._filelog
+        pl = [ (rp, pcl[0]._manifest.get(rp, nullid), fl) ]
+        if len(pcl) > 1:
+            if rp != p:
+                fl = None
+            pl.append((p, pcl[1]._manifest.get(p, nullid), fl))
+
+        return [ filectx(self._repo, p, fileid=n, filelog=l)
+                 for p,n,l in pl if n != nullid ]
+
+    def children(self):
+        return []
+
--- a/mercurial/localrepo.py	Tue Oct 03 11:54:11 2006 +0200
+++ b/mercurial/localrepo.py	Tue Oct 03 12:14:33 2006 -0700
@@ -321,6 +321,9 @@
     def changectx(self, changeid=None):
         return context.changectx(self, changeid)
 
+    def workingctx(self):
+        return context.workingctx(self)
+
     def parents(self, changeid=None):
         '''
         get list of changectxs for parents of changeid or working directory
--- a/mercurial/merge.py	Tue Oct 03 11:54:11 2006 +0200
+++ b/mercurial/merge.py	Tue Oct 03 12:14:33 2006 -0700
@@ -10,72 +10,66 @@
 from demandload import *
 demandload(globals(), "errno util os tempfile")
 
-def merge3(repo, fn, my, other, p1, p2):
-    """perform a 3-way merge in the working directory"""
+def filemerge(repo, fw, fo, fd, my, other, p1, p2, move):
+    """perform a 3-way merge in the working directory
 
-    def temp(prefix, node):
-        pre = "%s~%s." % (os.path.basename(fn), prefix)
+    fw = filename in the working directory and first parent
+    fo = filename in other parent
+    fd = destination filename
+    my = fileid in first parent
+    other = fileid in second parent
+    p1, p2 = hex changeset ids for merge command
+    move = whether to move or copy the file to the destination
+
+    TODO:
+      if fw is copied in the working directory, we get confused
+      implement move and fd
+    """
+
+    def temp(prefix, ctx):
+        pre = "%s~%s." % (os.path.basename(ctx.path()), prefix)
         (fd, name) = tempfile.mkstemp(prefix=pre)
         f = os.fdopen(fd, "wb")
-        repo.wwrite(fn, fl.read(node), f)
+        repo.wwrite(ctx.path(), ctx.data(), f)
         f.close()
         return name
 
-    fl = repo.file(fn)
-    base = fl.ancestor(my, other)
-    a = repo.wjoin(fn)
-    b = temp("base", base)
-    c = temp("other", other)
+    fcm = repo.filectx(fw, fileid=my)
+    fco = repo.filectx(fo, fileid=other)
+    fca = fcm.ancestor(fco)
+    if not fca:
+        fca = repo.filectx(fw, fileid=-1)
+    a = repo.wjoin(fw)
+    b = temp("base", fca)
+    c = temp("other", fco)
 
-    repo.ui.note(_("resolving %s\n") % fn)
-    repo.ui.debug(_("file %s: my %s other %s ancestor %s\n") %
-                          (fn, short(my), short(other), short(base)))
+    repo.ui.note(_("resolving %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': fn,
+                    environ={'HG_FILE': fw,
                              'HG_MY_NODE': p1,
-                             'HG_OTHER_NODE': p2,
-                             'HG_FILE_MY_NODE': hex(my),
-                             'HG_FILE_OTHER_NODE': hex(other),
-                             'HG_FILE_BASE_NODE': hex(base)})
+                             'HG_OTHER_NODE': p2})
     if r:
-        repo.ui.warn(_("merging %s failed!\n") % fn)
+        repo.ui.warn(_("merging %s failed!\n") % fw)
 
     os.unlink(b)
     os.unlink(c)
     return r
 
-def checkunknown(repo, m2, status):
+def checkunknown(repo, m2, wctx):
     """
     check for collisions between unknown files and files in m2
     """
-    modified, added, removed, deleted, unknown = status[:5]
-    for f in unknown:
+    for f in wctx.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
-    """
-
-    copied = repo.dirstate.copies()
-    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(copied.get(f, 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):
+def forgetremoved(m2, wctx):
     """
     Forget removed files
 
@@ -86,10 +80,9 @@
     manifest.
     """
 
-    modified, added, removed, deleted, unknown = status[:5]
     action = []
 
-    for f in deleted + removed:
+    for f in wctx.deleted() + wctx.removed():
         if f not in m2:
             action.append((f, "f"))
 
@@ -261,7 +254,7 @@
         elif m == "m": # merge
             flag, my, other = a[2:]
             repo.ui.status(_("merging %s\n") % f)
-            if merge3(repo, f, my, other, xp1, xp2):
+            if filemerge(repo, f, f, f, my, other, xp1, xp2, False):
                 unresolved += 1
             util.set_exec(repo.wjoin(f), flag)
             merged += 1
@@ -320,7 +313,8 @@
 
     ### check phase
 
-    pl = repo.parents()
+    wc = repo.workingctx()
+    pl = wc.parents()
     if not overwrite and len(pl) > 1:
         raise util.Abort(_("outstanding uncommitted merges"))
 
@@ -339,13 +333,11 @@
         raise util.Abort(_("update spans branches, use 'hg merge' "
                            "or 'hg update -C' to lose changes"))
 
-    status = repo.status()
-    modified, added, removed, deleted, unknown = status[:5]
     if branchmerge and not forcemerge:
-        if modified or added or removed:
+        if wc.modified() or wc.added() or wc.removed():
             raise util.Abort(_("outstanding uncommitted changes"))
 
-    m1 = p1.manifest().copy()
+    m1 = wc.manifest().copy()
     m2 = p2.manifest().copy()
     ma = pa.manifest()
 
@@ -359,14 +351,13 @@
     action = []
     copy = {}
 
-    m1 = workingmanifest(repo, m1, status)
     filtermanifest(m1, partial)
     filtermanifest(m2, partial)
 
     if not force:
-        checkunknown(repo, m2, status)
+        checkunknown(repo, m2, wc)
     if not branchmerge:
-        action += forgetremoved(m2, status)
+        action += forgetremoved(m2, wc)
     if not (backwards or overwrite):
         copy = findcopies(repo, m1, m2, pa.rev())
 
--- a/tests/test-merge7.out	Tue Oct 03 11:54:11 2006 +0200
+++ b/tests/test-merge7.out	Tue Oct 03 12:14:33 2006 -0700
@@ -27,7 +27,7 @@
  test.txt: versions differ -> m
 merging test.txt
 resolving test.txt
-file test.txt: my fc3148072371 other d40249267ae3 ancestor 8fe46a3eb557
+my test.txt@451c744aabcc other test.txt@a070d41e8360 ancestor test.txt@faaea63e63a9
 merging test.txt failed!
 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
 There are unresolved merges, you can redo the full merge using:
--- a/tests/test-up-local-change.out	Tue Oct 03 11:54:11 2006 +0200
+++ b/tests/test-up-local-change.out	Tue Oct 03 12:14:33 2006 -0700
@@ -21,7 +21,7 @@
  b: remote created -> g
 merging a
 resolving a
-file a: my b789fdd96dc2 other d730145abbf9 ancestor b789fdd96dc2
+my a@33aaa84a386b other a@802f095af299 ancestor a@33aaa84a386b
 getting b
 1 files updated, 1 files merged, 0 files removed, 0 files unresolved
 changeset:   1:802f095af299
@@ -55,7 +55,7 @@
  b: remote created -> g
 merging a
 resolving a
-file a: my b789fdd96dc2 other d730145abbf9 ancestor b789fdd96dc2
+my a@33aaa84a386b other a@802f095af299 ancestor a@33aaa84a386b
 getting b
 1 files updated, 1 files merged, 0 files removed, 0 files unresolved
 changeset:   1:802f095af299
@@ -106,10 +106,10 @@
  b: versions differ -> m
 merging a
 resolving a
-file a: my d730145abbf9 other 13e0d5f949fa ancestor b789fdd96dc2
+my a@802f095af299 other a@030602aee63d ancestor a@33aaa84a386b
 merging b
 resolving b
-file b: my 1e88685f5dde other 61de8c7723ca ancestor 000000000000
+my b@802f095af299 other b@030602aee63d ancestor b@000000000000
 0 files updated, 2 files merged, 0 files removed, 0 files unresolved
 (branch merge, don't forget to commit)
 changeset:   1:802f095af299