##// END OF EJS Templates
fix 'convert' with single commit repositories...
Hollis Blanchard -
r4719:1069205a default
parent child Browse files
Show More
@@ -1,313 +1,318 b''
1 # convert.py Foreign SCM converter
1 # convert.py Foreign SCM converter
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms
5 # This software may be used and distributed according to the terms
6 # of the GNU General Public License, incorporated herein by reference.
6 # of the GNU General Public License, incorporated herein by reference.
7
7
8 from common import NoRepo
8 from common import NoRepo
9 from cvs import convert_cvs
9 from cvs import convert_cvs
10 from git import convert_git
10 from git import convert_git
11 from hg import convert_mercurial
11 from hg import convert_mercurial
12
12
13 import os
13 import os
14 from mercurial import hg, ui, util, commands
14 from mercurial import hg, ui, util, commands
15
15
16 commands.norepo += " convert"
16 commands.norepo += " convert"
17
17
18 converters = [convert_cvs, convert_git, convert_mercurial]
18 converters = [convert_cvs, convert_git, convert_mercurial]
19
19
20 def converter(ui, path):
20 def converter(ui, path):
21 if not os.path.isdir(path):
21 if not os.path.isdir(path):
22 raise util.Abort("%s: not a directory" % path)
22 raise util.Abort("%s: not a directory" % path)
23 for c in converters:
23 for c in converters:
24 try:
24 try:
25 return c(ui, path)
25 return c(ui, path)
26 except NoRepo:
26 except NoRepo:
27 pass
27 pass
28 raise util.Abort("%s: unknown repository type" % path)
28 raise util.Abort("%s: unknown repository type" % path)
29
29
30 class convert(object):
30 class convert(object):
31 def __init__(self, ui, source, dest, mapfile, opts):
31 def __init__(self, ui, source, dest, mapfile, opts):
32
32
33 self.source = source
33 self.source = source
34 self.dest = dest
34 self.dest = dest
35 self.ui = ui
35 self.ui = ui
36 self.opts = opts
36 self.opts = opts
37 self.commitcache = {}
37 self.commitcache = {}
38 self.mapfile = mapfile
38 self.mapfile = mapfile
39 self.mapfilefd = None
39 self.mapfilefd = None
40 self.authors = {}
40 self.authors = {}
41 self.authorfile = None
41 self.authorfile = None
42
42
43 self.map = {}
43 self.map = {}
44 try:
44 try:
45 origmapfile = open(self.mapfile, 'r')
45 origmapfile = open(self.mapfile, 'r')
46 for l in origmapfile:
46 for l in origmapfile:
47 sv, dv = l[:-1].split()
47 sv, dv = l[:-1].split()
48 self.map[sv] = dv
48 self.map[sv] = dv
49 origmapfile.close()
49 origmapfile.close()
50 except IOError:
50 except IOError:
51 pass
51 pass
52
52
53 # Read first the dst author map if any
53 # Read first the dst author map if any
54 authorfile = self.dest.authorfile()
54 authorfile = self.dest.authorfile()
55 if authorfile and os.path.exists(authorfile):
55 if authorfile and os.path.exists(authorfile):
56 self.readauthormap(authorfile)
56 self.readauthormap(authorfile)
57 # Extend/Override with new author map if necessary
57 # Extend/Override with new author map if necessary
58 if opts.get('authors'):
58 if opts.get('authors'):
59 self.readauthormap(opts.get('authors'))
59 self.readauthormap(opts.get('authors'))
60 self.authorfile = self.dest.authorfile()
60 self.authorfile = self.dest.authorfile()
61
61
62 def walktree(self, heads):
62 def walktree(self, heads):
63 '''Return a mapping that identifies the uncommitted parents of every
64 uncommitted changeset.'''
63 visit = heads
65 visit = heads
64 known = {}
66 known = {}
65 parents = {}
67 parents = {}
66 while visit:
68 while visit:
67 n = visit.pop(0)
69 n = visit.pop(0)
68 if n in known or n in self.map: continue
70 if n in known or n in self.map: continue
69 known[n] = 1
71 known[n] = 1
70 self.commitcache[n] = self.source.getcommit(n)
72 self.commitcache[n] = self.source.getcommit(n)
71 cp = self.commitcache[n].parents
73 cp = self.commitcache[n].parents
74 parents[n] = []
72 for p in cp:
75 for p in cp:
73 parents.setdefault(n, []).append(p)
76 parents[n].append(p)
74 visit.append(p)
77 visit.append(p)
75
78
76 return parents
79 return parents
77
80
78 def toposort(self, parents):
81 def toposort(self, parents):
82 '''Return an ordering such that every uncommitted changeset is
83 preceeded by all its uncommitted ancestors.'''
79 visit = parents.keys()
84 visit = parents.keys()
80 seen = {}
85 seen = {}
81 children = {}
86 children = {}
82
87
83 while visit:
88 while visit:
84 n = visit.pop(0)
89 n = visit.pop(0)
85 if n in seen: continue
90 if n in seen: continue
86 seen[n] = 1
91 seen[n] = 1
87 pc = 0
92 # Ensure that nodes without parents are present in the 'children'
88 if n in parents:
93 # mapping.
89 for p in parents[n]:
94 children.setdefault(n, [])
90 if p not in self.map: pc += 1
95 for p in parents[n]:
96 if not p in self.map:
91 visit.append(p)
97 visit.append(p)
92 children.setdefault(p, []).append(n)
98 children.setdefault(p, []).append(n)
93 if not pc: root = n
94
99
95 s = []
100 s = []
96 removed = {}
101 removed = {}
97 visit = children.keys()
102 visit = children.keys()
98 while visit:
103 while visit:
99 n = visit.pop(0)
104 n = visit.pop(0)
100 if n in removed: continue
105 if n in removed: continue
101 dep = 0
106 dep = 0
102 if n in parents:
107 if n in parents:
103 for p in parents[n]:
108 for p in parents[n]:
104 if p in self.map: continue
109 if p in self.map: continue
105 if p not in removed:
110 if p not in removed:
106 # we're still dependent
111 # we're still dependent
107 visit.append(n)
112 visit.append(n)
108 dep = 1
113 dep = 1
109 break
114 break
110
115
111 if not dep:
116 if not dep:
112 # all n's parents are in the list
117 # all n's parents are in the list
113 removed[n] = 1
118 removed[n] = 1
114 if n not in self.map:
119 if n not in self.map:
115 s.append(n)
120 s.append(n)
116 if n in children:
121 if n in children:
117 for c in children[n]:
122 for c in children[n]:
118 visit.insert(0, c)
123 visit.insert(0, c)
119
124
120 if self.opts.get('datesort'):
125 if self.opts.get('datesort'):
121 depth = {}
126 depth = {}
122 for n in s:
127 for n in s:
123 depth[n] = 0
128 depth[n] = 0
124 pl = [p for p in self.commitcache[n].parents
129 pl = [p for p in self.commitcache[n].parents
125 if p not in self.map]
130 if p not in self.map]
126 if pl:
131 if pl:
127 depth[n] = max([depth[p] for p in pl]) + 1
132 depth[n] = max([depth[p] for p in pl]) + 1
128
133
129 s = [(depth[n], self.commitcache[n].date, n) for n in s]
134 s = [(depth[n], self.commitcache[n].date, n) for n in s]
130 s.sort()
135 s.sort()
131 s = [e[2] for e in s]
136 s = [e[2] for e in s]
132
137
133 return s
138 return s
134
139
135 def mapentry(self, src, dst):
140 def mapentry(self, src, dst):
136 if self.mapfilefd is None:
141 if self.mapfilefd is None:
137 try:
142 try:
138 self.mapfilefd = open(self.mapfile, "a")
143 self.mapfilefd = open(self.mapfile, "a")
139 except IOError, (errno, strerror):
144 except IOError, (errno, strerror):
140 raise util.Abort("Could not open map file %s: %s, %s\n" % (self.mapfile, errno, strerror))
145 raise util.Abort("Could not open map file %s: %s, %s\n" % (self.mapfile, errno, strerror))
141 self.map[src] = dst
146 self.map[src] = dst
142 self.mapfilefd.write("%s %s\n" % (src, dst))
147 self.mapfilefd.write("%s %s\n" % (src, dst))
143 self.mapfilefd.flush()
148 self.mapfilefd.flush()
144
149
145 def writeauthormap(self):
150 def writeauthormap(self):
146 authorfile = self.authorfile
151 authorfile = self.authorfile
147 if authorfile:
152 if authorfile:
148 self.ui.status('Writing author map file %s\n' % authorfile)
153 self.ui.status('Writing author map file %s\n' % authorfile)
149 ofile = open(authorfile, 'w+')
154 ofile = open(authorfile, 'w+')
150 for author in self.authors:
155 for author in self.authors:
151 ofile.write("%s=%s\n" % (author, self.authors[author]))
156 ofile.write("%s=%s\n" % (author, self.authors[author]))
152 ofile.close()
157 ofile.close()
153
158
154 def readauthormap(self, authorfile):
159 def readauthormap(self, authorfile):
155 afile = open(authorfile, 'r')
160 afile = open(authorfile, 'r')
156 for line in afile:
161 for line in afile:
157 try:
162 try:
158 srcauthor = line.split('=')[0].strip()
163 srcauthor = line.split('=')[0].strip()
159 dstauthor = line.split('=')[1].strip()
164 dstauthor = line.split('=')[1].strip()
160 if srcauthor in self.authors and dstauthor != self.authors[srcauthor]:
165 if srcauthor in self.authors and dstauthor != self.authors[srcauthor]:
161 self.ui.status(
166 self.ui.status(
162 'Overriding mapping for author %s, was %s, will be %s\n'
167 'Overriding mapping for author %s, was %s, will be %s\n'
163 % (srcauthor, self.authors[srcauthor], dstauthor))
168 % (srcauthor, self.authors[srcauthor], dstauthor))
164 else:
169 else:
165 self.ui.debug('Mapping author %s to %s\n'
170 self.ui.debug('Mapping author %s to %s\n'
166 % (srcauthor, dstauthor))
171 % (srcauthor, dstauthor))
167 self.authors[srcauthor] = dstauthor
172 self.authors[srcauthor] = dstauthor
168 except IndexError:
173 except IndexError:
169 self.ui.warn(
174 self.ui.warn(
170 'Ignoring bad line in author file map %s: %s\n'
175 'Ignoring bad line in author file map %s: %s\n'
171 % (authorfile, line))
176 % (authorfile, line))
172 afile.close()
177 afile.close()
173
178
174 def copy(self, rev):
179 def copy(self, rev):
175 c = self.commitcache[rev]
180 c = self.commitcache[rev]
176 files = self.source.getchanges(rev)
181 files = self.source.getchanges(rev)
177
182
178 for f, v in files:
183 for f, v in files:
179 try:
184 try:
180 data = self.source.getfile(f, v)
185 data = self.source.getfile(f, v)
181 except IOError, inst:
186 except IOError, inst:
182 self.dest.delfile(f)
187 self.dest.delfile(f)
183 else:
188 else:
184 e = self.source.getmode(f, v)
189 e = self.source.getmode(f, v)
185 self.dest.putfile(f, e, data)
190 self.dest.putfile(f, e, data)
186
191
187 r = [self.map[v] for v in c.parents]
192 r = [self.map[v] for v in c.parents]
188 f = [f for f, v in files]
193 f = [f for f, v in files]
189 newnode = self.dest.putcommit(f, r, c)
194 newnode = self.dest.putcommit(f, r, c)
190 self.mapentry(rev, newnode)
195 self.mapentry(rev, newnode)
191
196
192 def convert(self):
197 def convert(self):
193 try:
198 try:
194 self.ui.status("scanning source...\n")
199 self.ui.status("scanning source...\n")
195 heads = self.source.getheads()
200 heads = self.source.getheads()
196 parents = self.walktree(heads)
201 parents = self.walktree(heads)
197 self.ui.status("sorting...\n")
202 self.ui.status("sorting...\n")
198 t = self.toposort(parents)
203 t = self.toposort(parents)
199 num = len(t)
204 num = len(t)
200 c = None
205 c = None
201
206
202 self.ui.status("converting...\n")
207 self.ui.status("converting...\n")
203 for c in t:
208 for c in t:
204 num -= 1
209 num -= 1
205 desc = self.commitcache[c].desc
210 desc = self.commitcache[c].desc
206 if "\n" in desc:
211 if "\n" in desc:
207 desc = desc.splitlines()[0]
212 desc = desc.splitlines()[0]
208 author = self.commitcache[c].author
213 author = self.commitcache[c].author
209 author = self.authors.get(author, author)
214 author = self.authors.get(author, author)
210 self.commitcache[c].author = author
215 self.commitcache[c].author = author
211 self.ui.status("%d %s\n" % (num, desc))
216 self.ui.status("%d %s\n" % (num, desc))
212 self.copy(c)
217 self.copy(c)
213
218
214 tags = self.source.gettags()
219 tags = self.source.gettags()
215 ctags = {}
220 ctags = {}
216 for k in tags:
221 for k in tags:
217 v = tags[k]
222 v = tags[k]
218 if v in self.map:
223 if v in self.map:
219 ctags[k] = self.map[v]
224 ctags[k] = self.map[v]
220
225
221 if c and ctags:
226 if c and ctags:
222 nrev = self.dest.puttags(ctags)
227 nrev = self.dest.puttags(ctags)
223 # write another hash correspondence to override the previous
228 # write another hash correspondence to override the previous
224 # one so we don't end up with extra tag heads
229 # one so we don't end up with extra tag heads
225 if nrev:
230 if nrev:
226 self.mapentry(c, nrev)
231 self.mapentry(c, nrev)
227
232
228 self.writeauthormap()
233 self.writeauthormap()
229 finally:
234 finally:
230 self.cleanup()
235 self.cleanup()
231
236
232 def cleanup(self):
237 def cleanup(self):
233 if self.mapfilefd:
238 if self.mapfilefd:
234 self.mapfilefd.close()
239 self.mapfilefd.close()
235
240
236 def _convert(ui, src, dest=None, mapfile=None, **opts):
241 def _convert(ui, src, dest=None, mapfile=None, **opts):
237 '''Convert a foreign SCM repository to a Mercurial one.
242 '''Convert a foreign SCM repository to a Mercurial one.
238
243
239 Accepted source formats:
244 Accepted source formats:
240 - GIT
245 - GIT
241 - CVS
246 - CVS
242
247
243 Accepted destination formats:
248 Accepted destination formats:
244 - Mercurial
249 - Mercurial
245
250
246 If destination isn't given, a new Mercurial repo named <src>-hg will
251 If destination isn't given, a new Mercurial repo named <src>-hg will
247 be created. If <mapfile> isn't given, it will be put in a default
252 be created. If <mapfile> isn't given, it will be put in a default
248 location (<dest>/.hg/shamap by default)
253 location (<dest>/.hg/shamap by default)
249
254
250 The <mapfile> is a simple text file that maps each source commit ID to
255 The <mapfile> is a simple text file that maps each source commit ID to
251 the destination ID for that revision, like so:
256 the destination ID for that revision, like so:
252 <source ID> <destination ID>
257 <source ID> <destination ID>
253
258
254 If the file doesn't exist, it's automatically created. It's updated
259 If the file doesn't exist, it's automatically created. It's updated
255 on each commit copied, so convert-repo can be interrupted and can
260 on each commit copied, so convert-repo can be interrupted and can
256 be run repeatedly to copy new commits.
261 be run repeatedly to copy new commits.
257
262
258 The [username mapping] file is a simple text file that maps each source
263 The [username mapping] file is a simple text file that maps each source
259 commit author to a destination commit author. It is handy for source SCMs
264 commit author to a destination commit author. It is handy for source SCMs
260 that use unix logins to identify authors (eg: CVS). One line per author
265 that use unix logins to identify authors (eg: CVS). One line per author
261 mapping and the line format is:
266 mapping and the line format is:
262 srcauthor=whatever string you want
267 srcauthor=whatever string you want
263 '''
268 '''
264
269
265 srcc = converter(ui, src)
270 srcc = converter(ui, src)
266 if not hasattr(srcc, "getcommit"):
271 if not hasattr(srcc, "getcommit"):
267 raise util.Abort("%s: can't read from this repo type" % src)
272 raise util.Abort("%s: can't read from this repo type" % src)
268
273
269 if not dest:
274 if not dest:
270 dest = src + "-hg"
275 dest = src + "-hg"
271 ui.status("assuming destination %s\n" % dest)
276 ui.status("assuming destination %s\n" % dest)
272
277
273 # Try to be smart and initalize things when required
278 # Try to be smart and initalize things when required
274 if os.path.isdir(dest):
279 if os.path.isdir(dest):
275 if len(os.listdir(dest)) > 0:
280 if len(os.listdir(dest)) > 0:
276 try:
281 try:
277 hg.repository(ui, dest)
282 hg.repository(ui, dest)
278 ui.status("destination %s is a Mercurial repository\n" % dest)
283 ui.status("destination %s is a Mercurial repository\n" % dest)
279 except hg.RepoError:
284 except hg.RepoError:
280 raise util.Abort(
285 raise util.Abort(
281 "destination directory %s is not empty.\n"
286 "destination directory %s is not empty.\n"
282 "Please specify an empty directory to be initialized\n"
287 "Please specify an empty directory to be initialized\n"
283 "or an already initialized mercurial repository"
288 "or an already initialized mercurial repository"
284 % dest)
289 % dest)
285 else:
290 else:
286 ui.status("initializing destination %s repository\n" % dest)
291 ui.status("initializing destination %s repository\n" % dest)
287 hg.repository(ui, dest, create=True)
292 hg.repository(ui, dest, create=True)
288 elif os.path.exists(dest):
293 elif os.path.exists(dest):
289 raise util.Abort("destination %s exists and is not a directory" % dest)
294 raise util.Abort("destination %s exists and is not a directory" % dest)
290 else:
295 else:
291 ui.status("initializing destination %s repository\n" % dest)
296 ui.status("initializing destination %s repository\n" % dest)
292 hg.repository(ui, dest, create=True)
297 hg.repository(ui, dest, create=True)
293
298
294 destc = converter(ui, dest)
299 destc = converter(ui, dest)
295 if not hasattr(destc, "putcommit"):
300 if not hasattr(destc, "putcommit"):
296 raise util.Abort("%s: can't write to this repo type" % src)
301 raise util.Abort("%s: can't write to this repo type" % src)
297
302
298 if not mapfile:
303 if not mapfile:
299 try:
304 try:
300 mapfile = destc.mapfile()
305 mapfile = destc.mapfile()
301 except:
306 except:
302 mapfile = os.path.join(destc, "map")
307 mapfile = os.path.join(destc, "map")
303
308
304 c = convert(ui, srcc, destc, mapfile, opts)
309 c = convert(ui, srcc, destc, mapfile, opts)
305 c.convert()
310 c.convert()
306
311
307 cmdtable = {
312 cmdtable = {
308 "convert":
313 "convert":
309 (_convert,
314 (_convert,
310 [('A', 'authors', '', 'username mapping filename'),
315 [('A', 'authors', '', 'username mapping filename'),
311 ('', 'datesort', None, 'try to sort changesets by date')],
316 ('', 'datesort', None, 'try to sort changesets by date')],
312 'hg convert [OPTION]... SOURCE [DEST [MAPFILE]]'),
317 'hg convert [OPTION]... SOURCE [DEST [MAPFILE]]'),
313 }
318 }
General Comments 0
You need to be logged in to leave comments. Login now