##// END OF EJS Templates
Backout ad09ce1d393c and replace ''' with """ to make some highlighting happy....
Thomas Arendsen Hein -
r4958:71fed370 default
parent child Browse files
Show More
@@ -1,353 +1,353 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, **opts):
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, **opts)
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.source.setrevmap(self.map)
219 self.source.setrevmap(self.map)
220 self.ui.status("scanning source...\n")
220 self.ui.status("scanning source...\n")
221 heads = self.source.getheads()
221 heads = self.source.getheads()
222 parents = self.walktree(heads)
222 parents = self.walktree(heads)
223 self.ui.status("sorting...\n")
223 self.ui.status("sorting...\n")
224 t = self.toposort(parents)
224 t = self.toposort(parents)
225 num = len(t)
225 num = len(t)
226 c = None
226 c = None
227
227
228 self.ui.status("converting...\n")
228 self.ui.status("converting...\n")
229 for c in t:
229 for c in t:
230 num -= 1
230 num -= 1
231 desc = self.commitcache[c].desc
231 desc = self.commitcache[c].desc
232 if "\n" in desc:
232 if "\n" in desc:
233 desc = desc.splitlines()[0]
233 desc = desc.splitlines()[0]
234 author = self.commitcache[c].author
234 author = self.commitcache[c].author
235 author = self.authors.get(author, author)
235 author = self.authors.get(author, author)
236 self.commitcache[c].author = author
236 self.commitcache[c].author = author
237 self.ui.status("%d %s\n" % (num, desc))
237 self.ui.status("%d %s\n" % (num, desc))
238 self.copy(c)
238 self.copy(c)
239
239
240 tags = self.source.gettags()
240 tags = self.source.gettags()
241 ctags = {}
241 ctags = {}
242 for k in tags:
242 for k in tags:
243 v = tags[k]
243 v = tags[k]
244 if v in self.map:
244 if v in self.map:
245 ctags[k] = self.map[v]
245 ctags[k] = self.map[v]
246
246
247 if c and ctags:
247 if c and ctags:
248 nrev = self.dest.puttags(ctags)
248 nrev = self.dest.puttags(ctags)
249 # write another hash correspondence to override the previous
249 # write another hash correspondence to override the previous
250 # one so we don't end up with extra tag heads
250 # one so we don't end up with extra tag heads
251 if nrev:
251 if nrev:
252 self.mapentry(c, nrev)
252 self.mapentry(c, nrev)
253
253
254 self.writeauthormap()
254 self.writeauthormap()
255 finally:
255 finally:
256 self.cleanup()
256 self.cleanup()
257
257
258 def cleanup(self):
258 def cleanup(self):
259 if self.mapfilefd:
259 if self.mapfilefd:
260 self.mapfilefd.close()
260 self.mapfilefd.close()
261
261
262 def _convert(ui, src, dest=None, mapfile=None, **opts):
262 def _convert(ui, src, dest=None, mapfile=None, **opts):
263 '''Convert a foreign SCM repository to a Mercurial one.
263 """Convert a foreign SCM repository to a Mercurial one.
264
264
265 Accepted source formats:
265 Accepted source formats:
266 - GIT
266 - GIT
267 - CVS
267 - CVS
268 - SVN
268 - SVN
269
269
270 Accepted destination formats:
270 Accepted destination formats:
271 - Mercurial
271 - Mercurial
272
272
273 If no revision is given, all revisions will be converted. Otherwise,
273 If no revision is given, all revisions will be converted. Otherwise,
274 convert will only import up to the named revision (given in a format
274 convert will only import up to the named revision (given in a format
275 understood by the source).
275 understood by the source).
276
276
277 If no destination directory name is specified, it defaults to the
277 If no destination directory name is specified, it defaults to the
278 basename of the source with \'-hg\' appended. If the destination
278 basename of the source with '-hg' appended. If the destination
279 repository doesn\'t exist, it will be created.
279 repository doesn't exist, it will be created.
280
280
281 If <mapfile> isn\'t given, it will be put in a default location
281 If <mapfile> isn't given, it will be put in a default location
282 (<dest>/.hg/shamap by default). The <mapfile> is a simple text
282 (<dest>/.hg/shamap by default). The <mapfile> is a simple text
283 file that maps each source commit ID to the destination ID for
283 file that maps each source commit ID to the destination ID for
284 that revision, like so:
284 that revision, like so:
285 <source ID> <destination ID>
285 <source ID> <destination ID>
286
286
287 If the file doesn\'t exist, it\'s automatically created. It\'s updated
287 If the file doesn't exist, it's automatically created. It's updated
288 on each commit copied, so convert-repo can be interrupted and can
288 on each commit copied, so convert-repo can be interrupted and can
289 be run repeatedly to copy new commits.
289 be run repeatedly to copy new commits.
290
290
291 The [username mapping] file is a simple text file that maps each source
291 The [username mapping] file is a simple text file that maps each source
292 commit author to a destination commit author. It is handy for source SCMs
292 commit author to a destination commit author. It is handy for source SCMs
293 that use unix logins to identify authors (eg: CVS). One line per author
293 that use unix logins to identify authors (eg: CVS). One line per author
294 mapping and the line format is:
294 mapping and the line format is:
295 srcauthor=whatever string you want
295 srcauthor=whatever string you want
296 '''
296 """
297
297
298 util._encoding = 'UTF-8'
298 util._encoding = 'UTF-8'
299
299
300 if not dest:
300 if not dest:
301 dest = hg.defaultdest(src) + "-hg"
301 dest = hg.defaultdest(src) + "-hg"
302 ui.status("assuming destination %s\n" % dest)
302 ui.status("assuming destination %s\n" % dest)
303
303
304 # Try to be smart and initalize things when required
304 # Try to be smart and initalize things when required
305 created = False
305 created = False
306 if os.path.isdir(dest):
306 if os.path.isdir(dest):
307 if len(os.listdir(dest)) > 0:
307 if len(os.listdir(dest)) > 0:
308 try:
308 try:
309 hg.repository(ui, dest)
309 hg.repository(ui, dest)
310 ui.status("destination %s is a Mercurial repository\n" % dest)
310 ui.status("destination %s is a Mercurial repository\n" % dest)
311 except hg.RepoError:
311 except hg.RepoError:
312 raise util.Abort(
312 raise util.Abort(
313 "destination directory %s is not empty.\n"
313 "destination directory %s is not empty.\n"
314 "Please specify an empty directory to be initialized\n"
314 "Please specify an empty directory to be initialized\n"
315 "or an already initialized mercurial repository"
315 "or an already initialized mercurial repository"
316 % dest)
316 % dest)
317 else:
317 else:
318 ui.status("initializing destination %s repository\n" % dest)
318 ui.status("initializing destination %s repository\n" % dest)
319 hg.repository(ui, dest, create=True)
319 hg.repository(ui, dest, create=True)
320 created = True
320 created = True
321 elif os.path.exists(dest):
321 elif os.path.exists(dest):
322 raise util.Abort("destination %s exists and is not a directory" % dest)
322 raise util.Abort("destination %s exists and is not a directory" % dest)
323 else:
323 else:
324 ui.status("initializing destination %s repository\n" % dest)
324 ui.status("initializing destination %s repository\n" % dest)
325 hg.repository(ui, dest, create=True)
325 hg.repository(ui, dest, create=True)
326 created = True
326 created = True
327
327
328 destc = convertsink(ui, dest)
328 destc = convertsink(ui, dest)
329
329
330 try:
330 try:
331 srcc = convertsource(ui, src, rev=opts.get('rev'))
331 srcc = convertsource(ui, src, rev=opts.get('rev'))
332 except Exception:
332 except Exception:
333 if created:
333 if created:
334 shutil.rmtree(dest, True)
334 shutil.rmtree(dest, True)
335 raise
335 raise
336
336
337 if not mapfile:
337 if not mapfile:
338 try:
338 try:
339 mapfile = destc.mapfile()
339 mapfile = destc.mapfile()
340 except:
340 except:
341 mapfile = os.path.join(destc, "map")
341 mapfile = os.path.join(destc, "map")
342
342
343 c = convert(ui, srcc, destc, mapfile, opts)
343 c = convert(ui, srcc, destc, mapfile, opts)
344 c.convert()
344 c.convert()
345
345
346 cmdtable = {
346 cmdtable = {
347 "convert":
347 "convert":
348 (_convert,
348 (_convert,
349 [('A', 'authors', '', 'username mapping filename'),
349 [('A', 'authors', '', 'username mapping filename'),
350 ('r', 'rev', '', 'import up to target revision REV'),
350 ('r', 'rev', '', 'import up to target revision REV'),
351 ('', 'datesort', None, 'try to sort changesets by date')],
351 ('', 'datesort', None, 'try to sort changesets by date')],
352 'hg convert [OPTION]... SOURCE [DEST [MAPFILE]]'),
352 'hg convert [OPTION]... SOURCE [DEST [MAPFILE]]'),
353 }
353 }
General Comments 0
You need to be logged in to leave comments. Login now