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