--- 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