Merge with mpm
authorBrendan Cully <brendan@kublai.com>
Fri, 22 Dec 2006 17:05:10 -0800
changeset 3959 f321d841dce5
parent 3958 a8eb050c6489 (current diff)
parent 3957 2b87d3c5ab8e (diff)
child 3960 c137e21dba5a
Merge with mpm
--- a/contrib/convert-repo	Fri Dec 22 16:29:35 2006 -0800
+++ b/contrib/convert-repo	Fri Dec 22 17:05:10 2006 -0800
@@ -7,7 +7,7 @@
 #
 # convert-repo <source> [<dest> [<mapfile>]]
 #
-# Currently accepted source formats: git
+# Currently accepted source formats: git, cvs
 # Currently accepted destination formats: hg
 #
 # If destination isn't given, a new Mercurial repo named <src>-hg will
@@ -23,11 +23,19 @@
 # on each commit copied, so convert-repo can be interrupted and can
 # be run repeatedly to copy new commits.
 
-import sys, os, zlib, sha, time
+import sys, os, zlib, sha, time, re, locale
 os.environ["HGENCODING"] = "utf-8"
 from mercurial import hg, ui, util, fancyopts
 
 class Abort(Exception): pass
+class NoRepo(Exception): pass
+
+class commit:
+    def __init__(self, **parts):
+        for x in "author date desc parents".split():
+            if not x in parts:
+                abort("commit missing field %s\n" % x)
+        self.__dict__.update(parts)
 
 quiet = 0
 def status(msg):
@@ -48,13 +56,210 @@
         except:
             return s.decode("utf-8", "replace").encode("utf-8")
 
+# CVS conversion code inspired by hg-cvs-import and git-cvsimport
+class convert_cvs:
+    def __init__(self, path):
+        self.path = path
+        cvs = os.path.join(path, "CVS")
+        if not os.path.exists(cvs):
+            raise NoRepo("couldn't open CVS repo %s" % path)
+
+        self.changeset = {}
+        self.files = {}
+        self.tags = {}
+        self.lastbranch = {}
+        self.parent = {}
+        self.socket = None
+        self.cvsroot = file(os.path.join(cvs, "Root")).read()[:-1]
+        self.cvsrepo = file(os.path.join(cvs, "Repository")).read()[:-1]
+        self.encoding = locale.getpreferredencoding()
+        self._parse()
+        self._connect()
+
+    def _parse(self):
+        if self.changeset:
+            return
+
+        d = os.getcwd()
+        try:
+            os.chdir(self.path)
+            id = None
+            state = 0
+            for l in os.popen("cvsps -A"):
+                if state == 0: # header
+                    if l.startswith("PatchSet"):
+                        id = l[9:-2]
+                    elif l.startswith("Date"):
+                        date = util.parsedate(l[6:-1], ["%Y/%m/%d %H:%M:%S"])
+                        date = util.datestr(date)
+                    elif l.startswith("Branch"):
+                        branch = l[8:-1]
+                        self.parent[id] = self.lastbranch.get(branch,'bad')
+                        self.lastbranch[branch] = id
+                    elif l.startswith("Ancestor branch"):
+                        ancestor = l[17:-1]
+                        self.parent[id] = self.lastbranch[ancestor]
+                    elif l.startswith("Author"):
+                        author = self.recode(l[8:-1])
+                    elif l.startswith("Tag: "):
+                        t = l[5:-1]
+                        if t != "(none) ":
+                            self.tags[t] = id
+                    elif l.startswith("Log:"):
+                        state = 1
+                        log = ""
+                elif state == 1: # log
+                    if l == "Members: \n":
+                        files = {}
+                        log = self.recode(log[:-1])
+                        if log.isspace():
+                            log = "*** empty log message ***\n"
+                        state = 2
+                    else:
+                        log += l
+                elif state == 2:
+                    if l == "\n": #
+                        state = 0
+                        p = [self.parent[id]]
+                        if id == "1":
+                            p = []
+                        c = commit(author=author, date=date, parents=p,
+                                   desc=log, branch=branch)
+                        self.changeset[id] = c
+                        self.files[id] = files
+                    else:
+                        file,rev = l[1:-2].rsplit(':',1)
+                        rev = rev.split("->")[1]
+                        files[file] = rev
+
+            self.heads = self.lastbranch.values()
+        finally:
+            os.chdir(d)
+
+    def _connect(self):
+        root = self.cvsroot
+        local = False
+        user, host = None, None
+        cmd = ['cvs', 'server']
+
+        status("connecting to %s\n" % root)
+
+        # only non-pserver for now
+        if root.startswith(":pserver"):
+            abort("can't handle pserver mode yet: %s\n" % root)
+
+        if root.startswith(":local:"):
+            local = True
+            root = root[7:]
+        else:
+            # :ext:user@host/home/user/path/to/cvsroot
+            if root.startswith(":ext:"):
+                root = root[5:]
+            m = re.match(r'(?:([^@:/]+)@)?([^:/]+):?(.*)', root)
+            if not m:
+                local = True
+            else:
+                local = False
+                user, host, root = m.group(1), m.group(2), m.group(3)
+
+        if not local:
+            rsh = os.environ.get("CVS_RSH" or "rsh")
+            if user:
+                cmd = [rsh, '-l', user, host] + cmd
+            else:
+                cmd = [rsh, host] + cmd
+
+        self.writep, self.readp = os.popen2(cmd)
+        self.realroot = root
+
+        self.writep.write("Root %s\n" % root)
+        self.writep.write("Valid-responses ok error Valid-requests Mode"
+                          " M Mbinary E Checked-in Created Updated"
+                          " Merged Removed\n")
+        self.writep.write("valid-requests\n")
+        self.writep.flush()
+        r = self.readp.readline()
+        if not r.startswith("Valid-requests"):
+            abort("server sucks\n")
+        if "UseUnchanged" in r:
+            self.writep.write("UseUnchanged\n")
+            self.writep.flush()
+            r = self.readp.readline()
+
+    def getheads(self):
+        return self.heads
+
+    def _getfile(self, name, rev):
+        if rev.endswith("(DEAD)"):
+            raise IOError
+
+        args = ("-N -P -kk -r %s --" % rev).split()
+        args.append(os.path.join(self.cvsrepo, name))
+        for x in args:
+            self.writep.write("Argument %s\n" % x)
+        self.writep.write("Directory .\n%s\nco\n" % self.realroot)
+        self.writep.flush()
+
+        data = ""
+        while 1:
+            line = self.readp.readline()
+            if line.startswith("Created ") or line.startswith("Updated "):
+                self.readp.readline() # path
+                self.readp.readline() # entries
+                mode = self.readp.readline()[:-1]
+                count = int(self.readp.readline()[:-1])
+                data = self.readp.read(count)
+            elif line.startswith(" "):
+                data += line[1:]
+            elif line.startswith("M "):
+                pass
+            elif line.startswith("Mbinary "):
+                count = int(self.readp.readline()[:-1])
+                data = self.readp.read(count)
+            else:
+                if line == "ok\n":
+                    return (data, "x" in mode)
+                elif line.startswith("E "):
+                    warn("cvs server: %s\n" % line[2:])
+                elif line.startswith("Remove"):
+                    l = self.readp.readline()
+                    l = self.readp.readline()
+                    if l != "ok\n":
+                        abort("unknown CVS response: %s\n" % l)
+                else:
+                    abort("unknown CVS response: %s\n" % line)
+
+    def getfile(self, file, rev):
+        data, mode = self._getfile(file, rev)
+        self.modecache[(file, rev)] = mode
+        return data
+
+    def getmode(self, file, rev):
+        return self.modecache[(file, rev)]
+
+    def getchanges(self, rev):
+        self.modecache = {}
+        files = self.files[rev]
+        cl = files.items()
+        cl.sort()
+        return cl
+
+    def recode(self, text):
+        return text.decode(self.encoding, "replace").encode("utf-8")
+
+    def getcommit(self, rev):
+        return self.changeset[rev]
+
+    def gettags(self):
+        return self.tags
+
 class convert_git:
     def __init__(self, path):
         if os.path.isdir(path + "/.git"):
             path += "/.git"
         self.path = path
         if not os.path.exists(path + "/HEAD"):
-            raise TypeError("couldn't open GIT repo %s" % path)
+            raise NoRepo("couldn't open GIT repo %s" % path)
 
     def getheads(self):
         fh = os.popen("GIT_DIR=%s git-rev-parse --verify HEAD" % self.path)
@@ -68,7 +273,11 @@
     def getfile(self, name, rev):
         return self.catfile(rev, "blob")
 
+    def getmode(self, name, rev):
+        return self.modecache[(name, rev)]
+
     def getchanges(self, version):
+        self.modecache = {}
         fh = os.popen("GIT_DIR=%s git-diff-tree --root -m -r %s" % (self.path, version))
         changes = []
         for l in fh:
@@ -77,7 +286,8 @@
             m = m.split()
             h = m[3]
             p = (m[1] == "100755")
-            changes.append((f, h, p))
+            self.modecache[(f, h)] = p
+            changes.append((f, h))
         return changes
 
     def getcommit(self, version):
@@ -108,7 +318,9 @@
         tzs, tzh, tzm = tz[-5:-4] + "1", tz[-4:-2], tz[-2:]
         tz = -int(tzs) * (int(tzh) * 3600 + int(tzm))
         date = tm + " " + str(tz)
-        return (parents, author, date, message)
+
+        c = commit(parents=parents, date=date, author=author, desc=message)
+        return c
 
     def gettags(self):
         tags = {}
@@ -129,7 +341,7 @@
         try:
             self.repo = hg.repository(u, path)
         except:
-            raise TypeError("could open hg repo %s" % path)
+            raise NoRepo("could open hg repo %s" % path)
 
     def mapfile(self):
         return os.path.join(self.path, ".hg", "shamap")
@@ -152,7 +364,7 @@
         except:
             pass
 
-    def putcommit(self, files, parents, author, dest, text):
+    def putcommit(self, files, parents, commit):
         seen = {}
         pl = []
         for p in parents:
@@ -165,11 +377,18 @@
         if len(parents) < 2: parents.append("0" * 40)
         p2 = parents.pop(0)
 
+        text = commit.desc
+        extra = {}
+        try:
+            extra["branch"] = commit.branch
+        except AttributeError:
+            pass
+
         while parents:
             p1 = p2
             p2 = parents.pop(0)
-            self.repo.rawcommit(files, text, author, dest,
-                                hg.bin(p1), hg.bin(p2))
+            a = self.repo.rawcommit(files, text, commit.author, commit.date,
+                                    hg.bin(p1), hg.bin(p2), extra=extra)
             text = "(octopus merge fixup)\n"
             p2 = hg.hex(self.repo.changelog.tip())
 
@@ -202,7 +421,7 @@
                                 date, self.repo.changelog.tip(), hg.nullid)
             return hg.hex(self.repo.changelog.tip())
 
-converters = [convert_git, convert_mercurial]
+converters = [convert_cvs, convert_git, convert_mercurial]
 
 def converter(path):
     if not os.path.isdir(path):
@@ -210,16 +429,17 @@
     for c in converters:
         try:
             return c(path)
-        except TypeError:
+        except NoRepo:
             pass
     abort("%s: unknown repository type\n" % path)
 
 class convert:
-    def __init__(self, source, dest, mapfile):
+    def __init__(self, source, dest, mapfile, opts):
 
         self.source = source
         self.dest = dest
         self.mapfile = mapfile
+        self.opts = opts
         self.commitcache = {}
 
         self.map = {}
@@ -239,7 +459,7 @@
             if n in known or n in self.map: continue
             known[n] = 1
             self.commitcache[n] = self.source.getcommit(n)
-            cp = self.commitcache[n][0]
+            cp = self.commitcache[n].parents
             for p in cp:
                 parents.setdefault(n, []).append(p)
                 visit.append(p)
@@ -282,28 +502,42 @@
             if not dep:
                 # all n's parents are in the list
                 removed[n] = 1
-                s.append(n)
+                if n not in self.map:
+                    s.append(n)
                 if n in children:
                     for c in children[n]:
                         visit.insert(0, c)
 
+        if opts.get('datesort'):
+            depth = {}
+            for n in s:
+                depth[n] = 0
+                pl = [p for p in self.commitcache[n].parents if p not in self.map]
+                if pl:
+                    depth[n] = max([depth[p] for p in pl]) + 1
+
+            s = [(depth[n], self.commitcache[n].date, n) for n in s]
+            s.sort()
+            s = [e[2] for e in s]
+
         return s
 
     def copy(self, rev):
-        p, a, d, t = self.commitcache[rev]
+        c = self.commitcache[rev]
         files = self.source.getchanges(rev)
 
-        for f,v,e in files:
+        for f,v in files:
             try:
                 data = self.source.getfile(f, v)
             except IOError, inst:
                 self.dest.delfile(f)
             else:
+                e = self.source.getmode(f, v)
                 self.dest.putfile(f, e, data)
 
-        r = [self.map[v] for v in p]
-        f = [f for f,v,e in files]
-        self.map[rev] = self.dest.putcommit(f, r, a, d, t)
+        r = [self.map[v] for v in c.parents]
+        f = [f for f,v in files]
+        self.map[rev] = self.dest.putcommit(f, r, c)
         file(self.mapfile, "a").write("%s %s\n" % (rev, self.map[rev]))
 
     def convert(self):
@@ -312,14 +546,15 @@
         parents = self.walktree(heads)
         status("sorting...\n")
         t = self.toposort(parents)
-        t = [n for n in t if n not in self.map]
         num = len(t)
         c = None
 
         status("converting...\n")
         for c in t:
             num -= 1
-            desc = self.commitcache[c][3].splitlines()[0]
+            desc = self.commitcache[c].desc
+            if "\n" in desc:
+                desc = desc.splitlines()[0]
             status("%d %s\n" % (num, desc))
             self.copy(c)
 
@@ -358,10 +593,11 @@
         except:
             mapfile = os.path.join(destc, "map")
 
-    c = convert(srcc, destc, mapfile)
+    c = convert(srcc, destc, mapfile, opts)
     c.convert()
 
-options = [('q', 'quiet', None, 'suppress output')]
+options = [('q', 'quiet', None, 'suppress output'),
+           ('', 'datesort', None, 'try to sort changesets by date')]
 opts = {}
 args = fancyopts.fancyopts(sys.argv[1:], options, opts)
 
--- a/mercurial/localrepo.py	Fri Dec 22 16:29:35 2006 -0800
+++ b/mercurial/localrepo.py	Fri Dec 22 17:05:10 2006 -0800
@@ -727,11 +727,13 @@
         # update manifest
         m1.update(new)
         remove.sort()
+        removed = []
 
         for f in remove:
             if f in m1:
                 del m1[f]
-        mn = self.manifest.add(m1, tr, linkrev, c1[0], c2[0], (new, remove))
+                removed.append(f)
+        mn = self.manifest.add(m1, tr, linkrev, c1[0], c2[0], (new, removed))
 
         # add changeset
         new = new.keys()
@@ -747,7 +749,7 @@
             if p2 != nullid:
                 edittext.append("HG: branch merge")
             edittext.extend(["HG: changed %s" % f for f in changed])
-            edittext.extend(["HG: removed %s" % f for f in remove])
+            edittext.extend(["HG: removed %s" % f for f in removed])
             if not changed and not remove:
                 edittext.append("HG: no files changed")
             edittext.append("")
@@ -765,7 +767,7 @@
         text = '\n'.join(lines)
         if branchname:
             extra["branch"] = branchname
-        n = self.changelog.add(mn, changed + remove, text, tr, p1, p2,
+        n = self.changelog.add(mn, changed + removed, text, tr, p1, p2,
                                user, date, extra)
         self.hook('pretxncommit', throw=True, node=hex(n), parent1=xp1,
                   parent2=xp2)
@@ -775,7 +777,7 @@
             self.dirstate.setparents(n)
             if use_dirstate:
                 self.dirstate.update(new, "n")
-                self.dirstate.forget(remove)
+                self.dirstate.forget(removed)
 
         self.hook("commit", node=hex(n), parent1=xp1, parent2=xp2)
         return n