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