changeset 237:4f802588cdfb

Merge from tah
author mpm@selenic.com
date Fri, 03 Jun 2005 13:32:37 -0800
parents fc4a6e5b5812 (diff) 85f0420fd848 (current diff)
children afe895fcc0d0
files hg mercurial/commands.py mercurial/hg.py mercurial/hgweb.py mercurial/revlog.py mercurial/ui.py templates/changelogentry.tmpl templates/fileannotate.tmpl templates/filelogentry.tmpl templates/map
diffstat 5 files changed, 401 insertions(+), 269 deletions(-) [+]
line wrap: on
line diff
--- a/hg	Thu Jun 02 09:23:44 2005 +0100
+++ b/hg	Fri Jun 03 13:32:37 2005 -0800
@@ -66,9 +66,8 @@
     else:
         date2 = time.asctime()
         if not node1:
-            node1 = repo.current
-        (c, a, d) = repo.diffdir(repo.root, node1)
-        a = [] # ignore unknown files in repo, by popular request
+            node1 = repo.dirstate.parents()[0]
+        (c, a, d, u) = repo.diffdir(repo.root, node1)
         def read(f): return file(os.path.join(repo.root, f)).read()
 
     change = repo.changelog.read(node1)
@@ -79,9 +78,7 @@
         c, a, d = map(lambda x: filterfiles(x, files), (c, a, d))
 
     for f in c:
-        to = ""
-        if mmap.has_key(f):
-            to = repo.file(f).read(mmap[f])
+        to = repo.file(f).read(mmap[f])
         tn = read(f)
         sys.stdout.write(mdiff.unidiff(to, date1, tn, date2, f))
     for f in a:
@@ -129,24 +126,21 @@
 if os.getcwd() != repo.root:
     relpath = os.getcwd()[len(repo.root) + 1: ]
 
-if cmd == "checkout" or cmd == "co":
-    node = repo.changelog.tip()
-    if args:
-        node = repo.lookup(args[0])
-    repo.checkout(node)
-
 elif cmd == "add":
     repo.add(args)
 
+elif cmd == "forget":
+    repo.forget(args)
+
 elif cmd == "remove" or cmd == "rm" or cmd == "del" or cmd == "delete":
     repo.remove(args)
 
 elif cmd == "commit" or cmd == "checkin" or cmd == "ci":
     if 1:
         if len(args) > 0:
-            repo.commit(repo.current, args)
+            repo.commit(args)
         else:
-            repo.commit(repo.current)
+            repo.commit()
 elif cmd == "rawcommit":
     "raw commit interface"
     rc = {}
@@ -209,7 +203,7 @@
         if files:
             if os.system("patch -p%d < %s %s" % (strip, pf, quiet)):
                 raise "patch failed!"
-        repo.commit(repo.current, files, text)
+        repo.commit(files, text)
 
 elif cmd == "diff":
     revs = []
@@ -256,7 +250,7 @@
     repo.addchangegroup(data)
 
 elif cmd == "addremove":
-    (c, a, d) = repo.diffdir(repo.root, repo.current)
+    (c, a, d, u) = repo.diffdir(repo.root)
     repo.add(a)
     repo.remove(d)
     
@@ -361,11 +355,6 @@
     print "}"
 
 elif cmd == "merge":
-    (c, a, d) = repo.diffdir(repo.root, repo.current)
-    if c:
-        ui.warn("aborting (outstanding changes in working directory)\n")
-        sys.exit(1)
-
     if args:
         paths = {}
         try:
@@ -422,7 +411,6 @@
             
         manifestchangeset[changes[0]] = n
         for f in changes[3]:
-            revisions += 1
             filelinkrevs.setdefault(f, []).append(i)
 
     ui.status("checking manifests\n")
@@ -473,6 +461,7 @@
         fl = repo.file(f)
         nodes = { hg.nullid: 1 }
         for i in range(fl.count()):
+            revisions += 1
             n = fl.node(i)
 
             if n not in filenodes[f]:
@@ -510,10 +499,6 @@
             nodes[n] = 1
 
         # cross-check
-        for flr in filelinkrevs[f]:
-            ui.warn("changeset rev %d not in %s\n" % (flr, f))
-            errors += 1
-            
         for node in filenodes[f]:
             ui.warn("node %s in manifests not in %s\n" % (hg.hex(n), f))
             errors += 1
--- a/mercurial/commands.py	Thu Jun 02 09:23:44 2005 +0100
+++ b/mercurial/commands.py	Fri Jun 03 13:32:37 2005 -0800
@@ -1,4 +1,4 @@
-import os, re, traceback, sys, signal
+import os, re, traceback, sys, signal, time
 from mercurial import fancyopts, ui, hg
 
 class UnknownCommand(Exception): pass
@@ -68,8 +68,13 @@
     # this should eventually support remote repos
     os.system("cp -al %s/.hg .hg" % path)
 
-def checkout(u, repo, changeset=None):
+def checkout(ui, repo, changeset=None):
     '''checkout a given changeset or the current tip'''
+    (c, a, d, u) = repo.diffdir(repo.root)
+    if c or a or d:
+        ui.warn("aborting (outstanding changes in working directory)\n")
+        sys.exit(1)
+
     node = repo.changelog.tip()
     if changeset:
         node = repo.lookup(changeset)
@@ -97,7 +102,7 @@
         ops['number'] = 1
 
     args = relpath(repo, args)
-    node = repo.current
+    node = repo.dirstate.parents()[0]
     if ops['revision']:
         node = repo.changelog.lookup(ops['revision'])
     change = repo.changelog.read(node)
@@ -117,6 +122,45 @@
         for p,l in zip(zip(*pieces), lines):
             u.write(" ".join(p) + ": " + l[1])
 
+def heads(ui, repo):
+    '''show current repository heads'''
+    for n in repo.changelog.heads():
+        i = repo.changelog.rev(n)
+        changes = repo.changelog.read(n)
+        (p1, p2) = repo.changelog.parents(n)
+        (h, h1, h2) = map(hg.hex, (n, p1, p2))
+        (i1, i2) = map(repo.changelog.rev, (p1, p2))
+        print "rev:      %4d:%s" % (i, h)
+        print "parents:  %4d:%s" % (i1, h1)
+        if i2: print "          %4d:%s" % (i2, h2)
+        print "manifest: %4d:%s" % (repo.manifest.rev(changes[0]),
+                                    hg.hex(changes[0]))
+        print "user:", changes[1]
+        print "date:", time.asctime(
+            time.localtime(float(changes[2].split(' ')[0])))
+        if ui.verbose: print "files:", " ".join(changes[3])
+        print "description:"
+        print changes[4]
+
+def parents(ui, repo, node = None):
+    '''show the parents of the current working dir'''
+    if node:
+        p = repo.changelog.parents(repo.lookup(hg.bin(node)))
+    else:
+        p = repo.dirstate.parents()
+
+    for n in p:
+        if n != hg.nullid:
+            ui.write("%d:%s\n" % (repo.changelog.rev(n), hg.hex(n)))
+
+def resolve(ui, repo, node = None):
+    '''merge a given node or the current tip into the working dir'''
+    if not node:
+        node = repo.changelog.tip()
+    else:
+        node = repo.lookup(node)
+    repo.resolve(node)
+
 def status(ui, repo):
     '''show changed files in the working directory
 
@@ -124,12 +168,13 @@
 A = added
 R = removed
 ? = not tracked'''
-    (c, a, d) = repo.diffdir(repo.root, repo.current)
-    (c, a, d) = map(lambda x: relfilter(repo, x), (c, a, d))
+    (c, a, d, u) = repo.diffdir(repo.root)
+    (c, a, d, u) = map(lambda x: relfilter(repo, x), (c, a, d, u))
 
     for f in c: print "C", f
-    for f in a: print "?", f
+    for f in a: print "A", f
     for f in d: print "R", f
+    for f in u: print "?", f
 
 def undo(ui, repo):
     repo.undo()
@@ -137,6 +182,7 @@
 table = {
     "init": (init, [], 'hg init'),
     "branch|clone": (branch, [], 'hg branch [path]'),
+    "heads": (heads, [], 'hg heads'),
     "help": (help, [], 'hg help [command]'),
     "checkout|co": (checkout, [], 'hg checkout [changeset]'),
     "ann|annotate": (annotate,
@@ -145,6 +191,8 @@
                       ('n', 'number', None, 'show revision number'),
                       ('c', 'changeset', None, 'show changeset')],
                      'hg annotate [-u] [-c] [-n] [-r id] [files]'),
+    "parents": (parents, [], 'hg parents [node]'),
+    "resolve": (resolve, [], 'hg resolve [node]'),
     "status": (status, [], 'hg status'),
     "undo": (undo, [], 'hg undo'),
     }
--- a/mercurial/hg.py	Thu Jun 02 09:23:44 2005 +0100
+++ b/mercurial/hg.py	Fri Jun 03 13:32:37 2005 -0800
@@ -5,7 +5,7 @@
 # This software may be used and distributed according to the terms
 # of the GNU General Public License, incorporated herein by reference.
 
-import sys, struct, sha, socket, os, time, re, urllib2
+import sys, struct, sha, socket, os, time, re, urllib2, tempfile
 import urllib
 from mercurial import byterange, lock
 from mercurial.transaction import *
@@ -149,57 +149,87 @@
         text = "\n".join(l)
         return self.addrevision(text, transaction, self.count(), p1, p2)
 
-class dircache:
+class dirstate:
     def __init__(self, opener, ui):
         self.opener = opener
         self.dirty = 0
         self.ui = ui
         self.map = None
+        self.pl = None
+
     def __del__(self):
-        if self.dirty: self.write()
+        if self.dirty:
+            self.write()
+
     def __getitem__(self, key):
         try:
             return self.map[key]
         except TypeError:
             self.read()
             return self[key]
-        
+
+    def __contains__(self, key):
+        if not self.map: self.read()
+        return key in self.map
+
+    def parents(self):
+        if not self.pl:
+            self.read()
+        return self.pl
+
+    def setparents(self, p1, p2 = nullid):
+        self.dirty = 1
+        self.pl = p1, p2
+
+    def state(self, key):
+        try:
+            return self[key][0]
+        except KeyError:
+            return "?"
+
     def read(self):
         if self.map is not None: return self.map
 
         self.map = {}
+        self.pl = [nullid, nullid]
         try:
-            st = self.opener("dircache").read()
+            st = self.opener("dirstate").read()
         except: return
 
-        pos = 0
+        self.pl = [st[:20], st[20: 40]]
+
+        pos = 40
         while pos < len(st):
-            e = struct.unpack(">llll", st[pos:pos+16])
-            l = e[3]
-            pos += 16
+            e = struct.unpack(">cllll", st[pos:pos+17])
+            l = e[4]
+            pos += 17
             f = st[pos:pos + l]
-            self.map[f] = e[:3]
+            self.map[f] = e[:4]
             pos += l
         
-    def update(self, files):
+    def update(self, files, state):
+        ''' current states:
+        n  normal
+        m  needs merging
+        i  invalid
+        r  marked for removal
+        a  marked for addition'''
+
         if not files: return
         self.read()
         self.dirty = 1
         for f in files:
-            try:
-                s = os.stat(f)
-                self.map[f] = (s.st_mode, s.st_size, s.st_mtime)
-            except IOError:
-                self.remove(f)
+            if state == "r":
+                self.map[f] = ('r', 0, 0, 0)
+            else:
+                try:
+                    s = os.stat(f)
+                    self.map[f] = (state, s.st_mode, s.st_size, s.st_mtime)
+                except OSError:
+                    if state != "i": raise
+                    self.map[f] = ('r', 0, 0, 0)
 
-    def taint(self, files):
-        if not files: return
-        self.read()
-        self.dirty = 1
-        for f in files:
-            self.map[f] = (0, -1, 0)
-
-    def remove(self, files):
+    def forget(self, files):
         if not files: return
         self.read()
         self.dirty = 1
@@ -207,7 +237,7 @@
             try:
                 del self.map[f]
             except KeyError:
-                self.ui.warn("Not in dircache: %s\n" % f)
+                self.ui.warn("not in dirstate: %s!\n" % f)
                 pass
 
     def clear(self):
@@ -215,9 +245,10 @@
         self.dirty = 1
 
     def write(self):
-        st = self.opener("dircache", "w")
+        st = self.opener("dirstate", "w")
+        st.write("".join(self.pl))
         for f, e in self.map.items():
-            e = struct.pack(">llll", e[0], e[1], e[2], len(f))
+            e = struct.pack(">cllll", e[0], e[1], e[2], e[3], len(f))
             st.write(e + f)
         self.dirty = 0
 
@@ -280,16 +311,8 @@
         self.tags = None
 
         if not self.remote:
-            self.dircache = dircache(self.opener, ui)
-            try:
-                self.current = bin(self.opener("current").read())
-            except IOError:
-                self.current = None
+            self.dirstate = dirstate(self.opener, ui)
 
-    def setcurrent(self, node):
-        self.current = node
-        self.opener("current", "w").write(hex(node))
-      
     def ignore(self, f):
         if self.ignorelist is None:
             self.ignorelist = []
@@ -330,7 +353,7 @@
                            self.join("undo"))
 
     def recover(self):
-        self.lock()
+        lock = self.lock()
         if os.path.exists(self.join("recover")):
             self.ui.status("attempting to rollback interrupted transaction\n")
             return rollback(self.opener, self.join("recover"))
@@ -338,23 +361,20 @@
             self.ui.warn("no interrupted transaction available\n")
 
     def undo(self):
-        self.lock()
+        lock = self.lock()
         if os.path.exists(self.join("undo")):
+            f = self.changelog.read(self.changelog.tip())[3]
             self.ui.status("attempting to rollback last transaction\n")
             rollback(self.opener, self.join("undo"))
             self.manifest = manifest(self.opener)
             self.changelog = changelog(self.opener)
 
-            self.ui.status("discarding dircache\n")
+            self.ui.status("discarding dirstate\n")
             node = self.changelog.tip()
-            mf = self.changelog.read(node)[0]
-            mm = self.manifest.read(mf)
-            f = mm.keys()
             f.sort()
 
-            self.setcurrent(node)
-            self.dircache.clear()
-            self.dircache.taint(f)
+            self.dirstate.setparents(node)
+            self.dirstate.update(f, 'i')
         
         else:
             self.ui.warn("no undo information available\n")
@@ -369,7 +389,8 @@
             raise inst
 
     def rawcommit(self, files, text, user, date, p1=None, p2=None):
-        p1 = p1 or self.current or nullid
+        p1 = p1 or self.dirstate.parents()[0] or nullid
+        p2 = p2 or self.dirstate.parents()[1] or nullid
         pchange = self.changelog.read(p1)
         pmmap = self.manifest.read(pchange[0])
         tr = self.transaction()
@@ -382,70 +403,83 @@
                 self.ui.warn("Read file %s error, skipped\n" % f)
                 continue
             r = self.file(f)
+            # FIXME - need to find both parents properly
             prev = pmmap.get(f, nullid)
             mmap[f] = r.add(t, tr, linkrev, prev)
 
         mnode = self.manifest.add(mmap, tr, linkrev, pchange[0])
         n = self.changelog.add(mnode, files, text, tr, p1, p2, user ,date, )
         tr.close()
-        self.setcurrent(n)
-        self.dircache.clear()
-        self.dircache.update(mmap)
+        self.dirstate.setparents(p1, p2)
+        self.dirstate.clear()
+        self.dirstate.update(mmap.keys(), "n")
 
-    def commit(self, parent, update = None, text = ""):
-        self.lock()
-        try:
-            remove = [ l[:-1] for l in self.opener("to-remove") ]
-            os.unlink(self.join("to-remove"))
+    def commit(self, files = None, text = ""):
+        commit = []
+        remove = []
+        if files:
+            for f in files:
+                s = self.dirstate.state(f)
+                if s in 'cai':
+                    commit.append(f)
+                elif s == 'r':
+                    remove.append(f)
+                else:
+                    self.warn("%s not tracked!\n")
+        else:
+            (c, a, d, u) = self.diffdir(self.root)
+            commit = c + a
+            remove = d
 
-        except IOError:
-            remove = []
-
-        if update == None:
-            update = self.diffdir(self.root, parent)[0]
-
-        if not update:
+        if not commit and not remove:
             self.ui.status("nothing changed\n")
             return
 
+        p1, p2 = self.dirstate.parents()
+        c1 = self.changelog.read(p1)
+        c2 = self.changelog.read(p2)
+        m1 = self.manifest.read(c1[0])
+        m2 = self.manifest.read(c2[0])
+        lock = self.lock()
         tr = self.transaction()
 
         # check in files
         new = {}
         linkrev = self.changelog.count()
-        update.sort()
-        for f in update:
+        commit.sort()
+        for f in commit:
             self.ui.note(f + "\n")
             try:
                 t = file(f).read()
             except IOError:
-                remove.append(f)
-                continue
+                self.warn("trouble committing %s!\n" % f)
+                raise
+
             r = self.file(f)
-            new[f] = r.add(t, tr, linkrev)
+            fp1 = m1.get(f, nullid)
+            fp2 = m2.get(f, nullid)
+            new[f] = r.add(t, tr, linkrev, fp1, fp2)
 
         # update manifest
-        mmap = self.manifest.read(self.manifest.tip())
-        mmap.update(new)
-        for f in remove:
-            del mmap[f]
-        mnode = self.manifest.add(mmap, tr, linkrev)
+        m1.update(new)
+        for f in remove: del m1[f]
+        mn = self.manifest.add(m1, tr, linkrev, c1[0], c2[0])
 
         # add changeset
         new = new.keys()
         new.sort()
 
-        edittext = text + "\n" + "HG: manifest hash %s\n" % hex(mnode)
+        edittext = text + "\n" + "HG: manifest hash %s\n" % hex(mn)
         edittext += "".join(["HG: changed %s\n" % f for f in new])
         edittext += "".join(["HG: removed %s\n" % f for f in remove])
         edittext = self.ui.edit(edittext)
 
-        n = self.changelog.add(mnode, new, edittext, tr)
+        n = self.changelog.add(mn, new, edittext, tr, p1, p2)
         tr.close()
 
-        self.setcurrent(n)
-        self.dircache.update(new)
-        self.dircache.remove(remove)
+        self.dirstate.setparents(n)
+        self.dirstate.update(new, "n")
+        self.dirstate.forget(remove)
 
     def checkout(self, node):
         # checkout is really dumb at the moment
@@ -464,23 +498,25 @@
                 os.makedirs(os.path.dirname(f))
                 file(f, "w").write(t)
 
-        self.setcurrent(node)
-        self.dircache.clear()
-        self.dircache.update([f for f,n in l])
+        self.dirstate.setparents(node)
+        self.dirstate.clear()
+        self.dirstate.update([f for f,n in l], "n")
 
-    def diffdir(self, path, changeset):
+    def diffdir(self, path, changeset = None):
         changed = []
+        added = []
+        unknown = []
         mf = {}
-        added = []
 
         if changeset:
             change = self.changelog.read(changeset)
             mf = self.manifest.read(change[0])
-
-        if changeset == self.current:
-            dc = self.dircache.copy()
+            dc = dict.fromkeys(mf)
         else:
-            dc = dict.fromkeys(mf)
+            changeset = self.dirstate.parents()[0]
+            change = self.changelog.read(changeset)
+            mf = self.manifest.read(change[0])
+            dc = self.dirstate.copy()
 
         def fcmp(fn):
             t1 = file(os.path.join(self.root, fn)).read()
@@ -498,22 +534,33 @@
                 if fn in dc:
                     c = dc[fn]
                     del dc[fn]
-                    if not c or c[1] < 0:
+                    if not c:
                         if fcmp(fn):
                             changed.append(fn)
-                    elif c[1] != s.st_size:
+                    elif c[0] == 'i':
+                        if fn not in mf:
+                            added.append(fn)
+                        elif fcmp(fn):
+                            changed.append(fn)
+                    elif c[0] == 'm':
                         changed.append(fn)
-                    elif c[0] != s.st_mode or c[2] != s.st_mtime:
+                    elif c[0] == 'a':
+                        added.append(fn)
+                    elif c[0] == 'r':
+                        unknown.append(fn)
+                    elif c[2] != s.st_size:
+                        changed.append(fn)
+                    elif c[1] != s.st_mode or c[3] != s.st_mtime:
                         if fcmp(fn):
                             changed.append(fn)
                 else:
                     if self.ignore(fn): continue
-                    added.append(fn)
+                    unknown.append(fn)
 
         deleted = dc.keys()
         deleted.sort()
 
-        return (changed, added, deleted)
+        return (changed, added, deleted, unknown)
 
     def diffrevs(self, node1, node2):
         changed, added = [], []
@@ -537,12 +584,34 @@
         return (changed, added, deleted)
 
     def add(self, list):
-        self.dircache.taint(list)
+        for f in list:
+            p = os.path.join(self.root, f)
+            if not os.path.isfile(p):
+                self.ui.warn("%s does not exist!\n" % f)
+            elif self.dirstate.state(f) == 'n':
+                self.ui.warn("%s already tracked!\n" % f)
+            else:
+                self.dirstate.update([f], "a")
+
+    def forget(self, list):
+        for f in list:
+            if self.dirstate.state(f) not in 'ai':
+                self.ui.warn("%s not added!\n" % f)
+            else:
+                self.dirstate.forget([f])
 
     def remove(self, list):
-        dl = self.opener("to-remove", "a")
         for f in list:
-            dl.write(f + "\n")
+            p = os.path.join(self.root, f)
+            if os.path.isfile(p):
+                self.ui.warn("%s still exists!\n" % f)
+            elif f not in self.dirstate:
+                self.ui.warn("%s not tracked!\n" % f)
+            else:
+                self.dirstate.update([f], "r")
+
+    def heads(self):
+        return self.changelog.heads()
 
     def branches(self, nodes):
         if not nodes: nodes = [self.changelog.tip()]
@@ -609,22 +678,24 @@
         seen = {}
         seenbranch = {}
 
-        self.ui.status("searching for changes\n")
-        tip = remote.branches([])[0]
-        self.ui.debug("remote tip branch is %s:%s\n" %
-                      (short(tip[0]), short(tip[1])))
-
         # if we have an empty repo, fetch everything
         if self.changelog.tip() == nullid:
+            self.ui.status("requesting all changes\n")
             return remote.changegroup([nullid])
 
         # otherwise, assume we're closer to the tip than the root
-        unknown = [tip]
+        self.ui.status("searching for changes\n")
+        heads = remote.heads()
+        unknown = []
+        for h in heads:
+            if h not in m:
+                unknown.append(h)
 
-        if tip[0] in m:
+        if not unknown:
             self.ui.status("nothing to do!\n")
             return None
-
+            
+        unknown = remote.branches(unknown)
         while unknown:
             n = unknown.pop(0)
             seen[n[0]] = 1
@@ -716,9 +787,7 @@
                 yield y
 
     def addchangegroup(self, generator):
-        changesets = files = revisions = 0
 
-        self.lock()
         class genread:
             def __init__(self, generator):
                 self.g = generator
@@ -732,9 +801,6 @@
                 d, self.buf = self.buf[:l], self.buf[l:]
                 return d
                 
-        if not generator: return
-        source = genread(generator)
-
         def getchunk():
             d = source.read(4)
             if not d: return ""
@@ -748,141 +814,154 @@
                 if not c: break
                 yield c
 
-        tr = self.transaction()
-        simple = True
-        need = {}
-
-        self.ui.status("adding changesets\n")
-        # pull off the changeset group
-        def report(x):
+        def csmap(x):
             self.ui.debug("add changeset %s\n" % short(x))
             return self.changelog.count()
 
+        def revmap(x):
+            return self.changelog.rev(x)
+
+        if not generator: return
+        changesets = files = revisions = 0
+
+        source = genread(generator)
+        lock = self.lock()
+        tr = self.transaction()
+
+        # pull off the changeset group
+        self.ui.status("adding changesets\n")
         co = self.changelog.tip()
-        cn = self.changelog.addgroup(getgroup(), report, tr)
-
+        cn = self.changelog.addgroup(getgroup(), csmap, tr, 1) # unique
         changesets = self.changelog.rev(cn) - self.changelog.rev(co)
 
+        # pull off the manifest group
         self.ui.status("adding manifests\n")
-        # pull off the manifest group
         mm = self.manifest.tip()
-        mo = self.manifest.addgroup(getgroup(),
-                                    lambda x: self.changelog.rev(x), tr)
-
-        # do we need a resolve?
-        if self.changelog.ancestor(co, cn) != co:
-            simple = False
-            resolverev = self.changelog.count()
-
-            # resolve the manifest to determine which files
-            # we care about merging
-            self.ui.status("resolving manifests\n")
-            ma = self.manifest.ancestor(mm, mo)
-            omap = self.manifest.read(mo) # other
-            amap = self.manifest.read(ma) # ancestor
-            mmap = self.manifest.read(mm) # mine
-            nmap = {}
-
-            self.ui.debug(" ancestor %s local %s remote %s\n" %
-                          (short(ma), short(mm), short(mo)))
-
-            for f, mid in mmap.iteritems():
-                if f in omap:
-                    if mid != omap[f]:
-                        self.ui.debug(" %s versions differ, do resolve\n" % f)
-                        need[f] = mid # use merged version or local version
-                    else:
-                        nmap[f] = mid # keep ours
-                    del omap[f]
-                elif f in amap:
-                    if mid != amap[f]:
-                        r = self.ui.prompt(
-                            (" local changed %s which remote deleted\n" % f) +
-                            "(k)eep or (d)elete?", "[kd]", "k")
-                        if r == "k": nmap[f] = mid
-                    else:
-                        self.ui.debug("other deleted %s\n" % f)
-                        pass # other deleted it
-                else:
-                    self.ui.debug("local created %s\n" %f)
-                    nmap[f] = mid # we created it
-
-            del mmap
-
-            for f, oid in omap.iteritems():
-                if f in amap:
-                    if oid != amap[f]:
-                        r = self.ui.prompt(
-                            ("remote changed %s which local deleted\n" % f) +
-                            "(k)eep or (d)elete?", "[kd]", "k")
-                        if r == "k": nmap[f] = oid
-                    else:
-                        pass # probably safe
-                else:
-                    self.ui.debug("remote created %s, do resolve\n" % f)
-                    need[f] = oid
-
-            del omap
-            del amap
-
-        new = need.keys()
-        new.sort()
+        mo = self.manifest.addgroup(getgroup(), revmap, tr)
 
         # process the files
-        self.ui.status("adding files\n")
+        self.ui.status("adding file revisions\n")
         while 1:
             f = getchunk()
             if not f: break
             self.ui.debug("adding %s revisions\n" % f)
             fl = self.file(f)
             o = fl.tip()
-            n = fl.addgroup(getgroup(), lambda x: self.changelog.rev(x), tr)
+            n = fl.addgroup(getgroup(), revmap, tr)
             revisions += fl.rev(n) - fl.rev(o)
             files += 1
-            if f in need:
-                del need[f]
-                # manifest resolve determined we need to merge the tips
-                nmap[f] = self.merge3(fl, f, o, n, tr, resolverev)
+
+        self.ui.status(("modified %d files, added %d changesets" +
+                        " and %d new revisions\n")
+                       % (files, changesets, revisions))
 
-        if need:
-            # we need to do trivial merges on local files
-            for f in new:
-                if f not in need: continue
-                fl = self.file(f)
-                nmap[f] = self.merge3(fl, f, need[f], fl.tip(), tr, resolverev)
-                revisions += 1
+        tr.close()
+        return
 
-        # For simple merges, we don't need to resolve manifests or changesets
-        if simple:
-            self.ui.debug("simple merge, skipping resolve\n")
-            self.ui.status(("modified %d files, added %d changesets" +
-                           " and %d new revisions\n")
-                           % (files, changesets, revisions))
-            tr.close()
+    def resolve(self, node):
+        pl = self.dirstate.parents()
+        if pl[1] != nullid:
+            self.ui.warn("last merge not committed")
             return
 
-        node = self.manifest.add(nmap, tr, resolverev, mm, mo)
-        revisions += 1
+        p1, p2 = pl[0], node
+        m1n = self.changelog.read(p1)[0]
+        m2n = self.changelog.read(p2)[0]
+        man = self.manifest.ancestor(m1n, m2n)
+        m1 = self.manifest.read(m1n)
+        m2 = self.manifest.read(m2n)
+        ma = self.manifest.read(man)
+
+        (c, a, d, u) = self.diffdir(self.root)
+
+        # resolve the manifest to determine which files
+        # we care about merging
+        self.ui.status("resolving manifests\n")
+        self.ui.debug(" ancestor %s local %s remote %s\n" %
+                      (short(man), short(m1n), short(m2n)))
+
+        merge = {}
+        get = {}
+        remove = []
 
-        # Now all files and manifests are merged, we add the changed files
-        # and manifest id to the changelog
-        self.ui.status("committing merge changeset\n")
-        if co == cn: cn = -1
+        # construct a working dir manifest
+        mw = m1.copy()
+        for f in a + c:
+            mw[f] = nullid
+        for f in d:
+            del mw[f]
+
+        for f, n in mw.iteritems():
+            if f in m2:
+                if n != m2[f]:
+                    self.ui.debug(" %s versions differ, do resolve\n" % f)
+                    merge[f] = (m1.get(f, nullid), m2[f])
+                del m2[f]
+            elif f in ma:
+                if n != ma[f]:
+                    r = self.ui.prompt(
+                        (" local changed %s which remote deleted\n" % f) +
+                        "(k)eep or (d)elete?", "[kd]", "k")
+                    if r == "d":
+                        remove.append(f)
+                else:
+                    self.ui.debug("other deleted %s\n" % f)
+                    pass # other deleted it
+            else:
+                self.ui.debug("local created %s\n" %f)
 
-        edittext = "\nHG: merge resolve\n" + \
-                   "HG: manifest hash %s\n" % hex(node) + \
-                   "".join(["HG: changed %s\n" % f for f in new])
-        edittext = self.ui.edit(edittext)
-        n = self.changelog.add(node, new, edittext, tr, co, cn)
-        revisions += 1
+        for f, n in m2.iteritems():
+            if f in ma:
+                if n != ma[f]:
+                    r = self.ui.prompt(
+                        ("remote changed %s which local deleted\n" % f) +
+                        "(k)eep or (d)elete?", "[kd]", "k")
+                    if r == "d": remove.append(f)
+                else:
+                    pass # probably safe
+            else:
+                self.ui.debug("remote created %s, do resolve\n" % f)
+                get[f] = n
+
+        del mw, m1, m2, ma
+
+        self.dirstate.setparents(p1, p2)
 
-        self.ui.status("added %d changesets, %d files, and %d new revisions\n"
-                       % (changesets, files, revisions))
+        # get the files we don't need to change
+        files = get.keys()
+        files.sort()
+        for f in files:
+            if f[0] == "/": continue
+            self.ui.note(f, "\n")
+            t = self.file(f).revision(get[f])
+            try:
+                file(f, "w").write(t)
+            except IOError:
+                os.makedirs(os.path.dirname(f))
+                file(f, "w").write(t)
+
+        # we have to remember what files we needed to get/change
+        # because any file that's different from either one of its
+        # parents must be in the changeset
+        self.dirstate.update(files, 'm')
 
-        tr.close()
+        # merge the tricky bits
+        files = merge.keys()
+        files.sort()
+        for f in files:
+            m, o = merge[f]
+            self.merge3(f, m, o)
 
-    def merge3(self, fl, fn, my, other, transaction, link):
-        """perform a 3-way merge and append the result"""
+        # same here
+        self.dirstate.update(files, 'm')
+
+        for f in remove:
+            self.ui.note("removing %s\n" % f)
+            #os.unlink(f)
+        self.dirstate.update(remove, 'r')
+
+    def merge3(self, fn, my, other):
+        """perform a 3-way merge in the working directory"""
         
         def temp(prefix, node):
             pre = "%s~%s." % (os.path.basename(fn), prefix)
@@ -892,30 +971,23 @@
             f.close()
             return name
 
+        fl = self.file(fn)
         base = fl.ancestor(my, other)
-        self.ui.note("resolving %s\n" % fn)
-        self.ui.debug("local %s remote %s ancestor %s\n" %
-                              (short(my), short(other), short(base)))
-
-        if my == base: 
-            text = fl.revision(other)
-        else:
-            a = temp("local", my)
-            b = temp("remote", other)
-            c = temp("parent", base)
+        a = fn
+        b = temp("other", other)
+        c = temp("base", base)
 
-            cmd = os.environ["HGMERGE"]
-            self.ui.debug("invoking merge with %s\n" % cmd)
-            r = os.system("%s %s %s %s %s" % (cmd, a, b, c, fn))
-            if r:
-                raise "Merge failed!"
+        self.ui.note("resolving %s\n" % fn)
+        self.ui.debug("file %s: other %s ancestor %s\n" %
+                              (fn, short(other), short(base)))
 
-            text = open(a).read()
-            os.unlink(a)
-            os.unlink(b)
-            os.unlink(c)
-            
-        return fl.add(text, transaction, link, my, other)
+        cmd = os.environ["HGMERGE"]
+        r = os.system("%s %s %s %s %s" % (cmd, a, b, c, fn))
+        if r:
+            self.ui.warn("merging %s failed!\n" % f)
+
+        os.unlink(b)
+        os.unlink(c)
 
 class remoterepository:
     def __init__(self, ui, path):
@@ -930,6 +1002,14 @@
         cu = "%s?%s" % (self.url, qs)
         return urllib.urlopen(cu)
 
+    def heads(self):
+        d = self.do_cmd("heads").read()
+        try:
+            return map(bin, d[:-1].split(" "))
+        except:
+            self.ui.warn("unexpected response:\n" + d[:400] + "\n...\n")
+            raise
+
     def branches(self, nodes):
         n = " ".join(map(hex, nodes))
         d = self.do_cmd("branches", nodes=n).read()
--- a/mercurial/hgweb.py	Thu Jun 02 09:23:44 2005 +0100
+++ b/mercurial/hgweb.py	Fri Jun 03 13:32:37 2005 -0800
@@ -608,6 +608,11 @@
         elif args['cmd'][0] == 'filelog':
             write(self.filelog(args['file'][0], args['filenode'][0]))
 
+        elif args['cmd'][0] == 'heads':
+            httphdr("text/plain")
+            h = self.repo.heads()
+            sys.stdout.write(" ".join(map(hex, h)) + "\n")
+
         elif args['cmd'][0] == 'branches':
             httphdr("text/plain")
             nodes = []
--- a/mercurial/revlog.py	Thu Jun 02 09:23:44 2005 +0100
+++ b/mercurial/revlog.py	Fri Jun 03 13:32:37 2005 -0800
@@ -156,6 +156,17 @@
     def end(self, rev): return self.start(rev) + self.length(rev)
     def base(self, rev): return self.index[rev][2]
 
+    def heads(self):
+        p = {}
+        h = []
+        for r in range(self.count() - 1, 0, -1):
+            n = self.node(r)
+            if n not in p:
+                h.append(n)
+            for pn in self.parents(n):
+                p[pn] = 1
+        return h
+    
     def lookup(self, id):
         try:
             rev = int(id)
@@ -422,7 +433,7 @@
 
         yield struct.pack(">l", 0)
 
-    def addgroup(self, revs, linkmapper, transaction):
+    def addgroup(self, revs, linkmapper, transaction, unique = 0):
         # given a set of deltas, add them to the revision log. the
         # first delta is against its parent, which should be in our
         # log, the rest are against the previous delta.
@@ -452,7 +463,10 @@
             node, p1, p2, cs = struct.unpack("20s20s20s20s", chunk[:80])
             link = linkmapper(cs)
             if node in self.nodemap:
-                raise "already have %s" % hex(node[:4])
+                # this can happen if two branches make the same change
+                if unique:
+                    raise "already have %s" % hex(node[:4])
+                continue
             delta = chunk[80:]
 
             if not chain: