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