##// END OF EJS Templates
convert: rename a class and a function
Alexis S. L. Carvalho -
r5281:a176f9c8 default
parent child Browse files
Show More
@@ -1,483 +1,483
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, converter_source, converter_sink
9 9 from cvs import convert_cvs
10 10 from git import convert_git
11 11 from hg import mercurial_source, mercurial_sink
12 12 from subversion import convert_svn, debugsvnlog
13 13
14 14 import os, shlex, shutil
15 15 from mercurial import hg, ui, util, commands
16 16 from mercurial.i18n import _
17 17
18 18 commands.norepo += " convert debugsvnlog"
19 19
20 20 converters = [convert_cvs, convert_git, convert_svn, mercurial_source,
21 21 mercurial_sink]
22 22
23 23 def convertsource(ui, path, **opts):
24 24 for c in converters:
25 25 try:
26 26 return c.getcommit and c(ui, path, **opts)
27 27 except (AttributeError, NoRepo):
28 28 pass
29 29 raise util.Abort('%s: unknown repository type' % path)
30 30
31 31 def convertsink(ui, path):
32 32 if not os.path.isdir(path):
33 33 raise util.Abort("%s: not a directory" % path)
34 34 for c in converters:
35 35 try:
36 36 return c.putcommit and c(ui, path)
37 37 except (AttributeError, NoRepo):
38 38 pass
39 39 raise util.Abort('%s: unknown repository type' % path)
40 40
41 class convert(object):
41 class converter(object):
42 42 def __init__(self, ui, source, dest, revmapfile, filemapper, opts):
43 43
44 44 self.source = source
45 45 self.dest = dest
46 46 self.ui = ui
47 47 self.opts = opts
48 48 self.commitcache = {}
49 49 self.revmapfile = revmapfile
50 50 self.revmapfilefd = None
51 51 self.authors = {}
52 52 self.authorfile = None
53 53 self.mapfile = filemapper
54 54
55 55 self.map = {}
56 56 try:
57 57 origrevmapfile = open(self.revmapfile, 'r')
58 58 for l in origrevmapfile:
59 59 sv, dv = l[:-1].split()
60 60 self.map[sv] = dv
61 61 origrevmapfile.close()
62 62 except IOError:
63 63 pass
64 64
65 65 # Read first the dst author map if any
66 66 authorfile = self.dest.authorfile()
67 67 if authorfile and os.path.exists(authorfile):
68 68 self.readauthormap(authorfile)
69 69 # Extend/Override with new author map if necessary
70 70 if opts.get('authors'):
71 71 self.readauthormap(opts.get('authors'))
72 72 self.authorfile = self.dest.authorfile()
73 73
74 74 def walktree(self, heads):
75 75 '''Return a mapping that identifies the uncommitted parents of every
76 76 uncommitted changeset.'''
77 77 visit = heads
78 78 known = {}
79 79 parents = {}
80 80 while visit:
81 81 n = visit.pop(0)
82 82 if n in known or n in self.map: continue
83 83 known[n] = 1
84 84 commit = self.cachecommit(n)
85 85 parents[n] = []
86 86 for p in commit.parents:
87 87 parents[n].append(p)
88 88 visit.append(p)
89 89
90 90 return parents
91 91
92 92 def toposort(self, parents):
93 93 '''Return an ordering such that every uncommitted changeset is
94 94 preceeded by all its uncommitted ancestors.'''
95 95 visit = parents.keys()
96 96 seen = {}
97 97 children = {}
98 98
99 99 while visit:
100 100 n = visit.pop(0)
101 101 if n in seen: continue
102 102 seen[n] = 1
103 103 # Ensure that nodes without parents are present in the 'children'
104 104 # mapping.
105 105 children.setdefault(n, [])
106 106 for p in parents[n]:
107 107 if not p in self.map:
108 108 visit.append(p)
109 109 children.setdefault(p, []).append(n)
110 110
111 111 s = []
112 112 removed = {}
113 113 visit = children.keys()
114 114 while visit:
115 115 n = visit.pop(0)
116 116 if n in removed: continue
117 117 dep = 0
118 118 if n in parents:
119 119 for p in parents[n]:
120 120 if p in self.map: continue
121 121 if p not in removed:
122 122 # we're still dependent
123 123 visit.append(n)
124 124 dep = 1
125 125 break
126 126
127 127 if not dep:
128 128 # all n's parents are in the list
129 129 removed[n] = 1
130 130 if n not in self.map:
131 131 s.append(n)
132 132 if n in children:
133 133 for c in children[n]:
134 134 visit.insert(0, c)
135 135
136 136 if self.opts.get('datesort'):
137 137 depth = {}
138 138 for n in s:
139 139 depth[n] = 0
140 140 pl = [p for p in self.commitcache[n].parents
141 141 if p not in self.map]
142 142 if pl:
143 143 depth[n] = max([depth[p] for p in pl]) + 1
144 144
145 145 s = [(depth[n], self.commitcache[n].date, n) for n in s]
146 146 s.sort()
147 147 s = [e[2] for e in s]
148 148
149 149 return s
150 150
151 151 def mapentry(self, src, dst):
152 152 if self.revmapfilefd is None:
153 153 try:
154 154 self.revmapfilefd = open(self.revmapfile, "a")
155 155 except IOError, (errno, strerror):
156 156 raise util.Abort("Could not open map file %s: %s, %s\n" % (self.revmapfile, errno, strerror))
157 157 self.map[src] = dst
158 158 self.revmapfilefd.write("%s %s\n" % (src, dst))
159 159 self.revmapfilefd.flush()
160 160
161 161 def writeauthormap(self):
162 162 authorfile = self.authorfile
163 163 if authorfile:
164 164 self.ui.status('Writing author map file %s\n' % authorfile)
165 165 ofile = open(authorfile, 'w+')
166 166 for author in self.authors:
167 167 ofile.write("%s=%s\n" % (author, self.authors[author]))
168 168 ofile.close()
169 169
170 170 def readauthormap(self, authorfile):
171 171 afile = open(authorfile, 'r')
172 172 for line in afile:
173 173 try:
174 174 srcauthor = line.split('=')[0].strip()
175 175 dstauthor = line.split('=')[1].strip()
176 176 if srcauthor in self.authors and dstauthor != self.authors[srcauthor]:
177 177 self.ui.status(
178 178 'Overriding mapping for author %s, was %s, will be %s\n'
179 179 % (srcauthor, self.authors[srcauthor], dstauthor))
180 180 else:
181 181 self.ui.debug('Mapping author %s to %s\n'
182 182 % (srcauthor, dstauthor))
183 183 self.authors[srcauthor] = dstauthor
184 184 except IndexError:
185 185 self.ui.warn(
186 186 'Ignoring bad line in author file map %s: %s\n'
187 187 % (authorfile, line))
188 188 afile.close()
189 189
190 190 def cachecommit(self, rev):
191 191 commit = self.source.getcommit(rev)
192 192 commit.author = self.authors.get(commit.author, commit.author)
193 193 self.commitcache[rev] = commit
194 194 return commit
195 195
196 196 def copy(self, rev):
197 197 commit = self.commitcache[rev]
198 198 do_copies = hasattr(self.dest, 'copyfile')
199 199 filenames = []
200 200
201 201 files, copies = self.source.getchanges(rev)
202 202 parents = [self.map[r] for r in commit.parents]
203 203 if commit.parents:
204 204 prev = commit.parents[0]
205 205 if prev not in self.commitcache:
206 206 self.cachecommit(prev)
207 207 pbranch = self.commitcache[prev].branch
208 208 else:
209 209 pbranch = None
210 210 self.dest.setbranch(commit.branch, pbranch, parents)
211 211 for f, v in files:
212 212 newf = self.mapfile(f)
213 213 if not newf:
214 214 continue
215 215 filenames.append(newf)
216 216 try:
217 217 data = self.source.getfile(f, v)
218 218 except IOError, inst:
219 219 self.dest.delfile(newf)
220 220 else:
221 221 e = self.source.getmode(f, v)
222 222 self.dest.putfile(newf, e, data)
223 223 if do_copies:
224 224 if f in copies:
225 225 copyf = self.mapfile(copies[f])
226 226 if copyf:
227 227 # Merely marks that a copy happened.
228 228 self.dest.copyfile(copyf, newf)
229 229
230 230 if not filenames and self.mapfile.active():
231 231 newnode = parents[0]
232 232 else:
233 233 newnode = self.dest.putcommit(filenames, parents, commit)
234 234 self.mapentry(rev, newnode)
235 235
236 236 def convert(self):
237 237 try:
238 238 self.dest.before()
239 239 self.source.setrevmap(self.map)
240 240 self.ui.status("scanning source...\n")
241 241 heads = self.source.getheads()
242 242 parents = self.walktree(heads)
243 243 self.ui.status("sorting...\n")
244 244 t = self.toposort(parents)
245 245 num = len(t)
246 246 c = None
247 247
248 248 self.ui.status("converting...\n")
249 249 for c in t:
250 250 num -= 1
251 251 desc = self.commitcache[c].desc
252 252 if "\n" in desc:
253 253 desc = desc.splitlines()[0]
254 254 self.ui.status("%d %s\n" % (num, desc))
255 255 self.copy(c)
256 256
257 257 tags = self.source.gettags()
258 258 ctags = {}
259 259 for k in tags:
260 260 v = tags[k]
261 261 if v in self.map:
262 262 ctags[k] = self.map[v]
263 263
264 264 if c and ctags:
265 265 nrev = self.dest.puttags(ctags)
266 266 # write another hash correspondence to override the previous
267 267 # one so we don't end up with extra tag heads
268 268 if nrev:
269 269 self.mapentry(c, nrev)
270 270
271 271 self.writeauthormap()
272 272 finally:
273 273 self.cleanup()
274 274
275 275 def cleanup(self):
276 276 self.dest.after()
277 277 if self.revmapfilefd:
278 278 self.revmapfilefd.close()
279 279
280 280 def rpairs(name):
281 281 e = len(name)
282 282 while e != -1:
283 283 yield name[:e], name[e+1:]
284 284 e = name.rfind('/', 0, e)
285 285
286 286 class filemapper(object):
287 287 '''Map and filter filenames when importing.
288 288 A name can be mapped to itself, a new name, or None (omit from new
289 289 repository).'''
290 290
291 291 def __init__(self, ui, path=None):
292 292 self.ui = ui
293 293 self.include = {}
294 294 self.exclude = {}
295 295 self.rename = {}
296 296 if path:
297 297 if self.parse(path):
298 298 raise util.Abort(_('errors in filemap'))
299 299
300 300 def parse(self, path):
301 301 errs = 0
302 302 def check(name, mapping, listname):
303 303 if name in mapping:
304 304 self.ui.warn(_('%s:%d: %r already in %s list\n') %
305 305 (lex.infile, lex.lineno, name, listname))
306 306 return 1
307 307 return 0
308 308 lex = shlex.shlex(open(path), path, True)
309 309 lex.wordchars += '!@#$%^&*()-=+[]{}|;:,./<>?'
310 310 cmd = lex.get_token()
311 311 while cmd:
312 312 if cmd == 'include':
313 313 name = lex.get_token()
314 314 errs += check(name, self.exclude, 'exclude')
315 315 self.include[name] = name
316 316 elif cmd == 'exclude':
317 317 name = lex.get_token()
318 318 errs += check(name, self.include, 'include')
319 319 errs += check(name, self.rename, 'rename')
320 320 self.exclude[name] = name
321 321 elif cmd == 'rename':
322 322 src = lex.get_token()
323 323 dest = lex.get_token()
324 324 errs += check(src, self.exclude, 'exclude')
325 325 self.rename[src] = dest
326 326 elif cmd == 'source':
327 327 errs += self.parse(lex.get_token())
328 328 else:
329 329 self.ui.warn(_('%s:%d: unknown directive %r\n') %
330 330 (lex.infile, lex.lineno, cmd))
331 331 errs += 1
332 332 cmd = lex.get_token()
333 333 return errs
334 334
335 335 def lookup(self, name, mapping):
336 336 for pre, suf in rpairs(name):
337 337 try:
338 338 return mapping[pre], pre, suf
339 339 except KeyError, err:
340 340 pass
341 341 return '', name, ''
342 342
343 343 def __call__(self, name):
344 344 if self.include:
345 345 inc = self.lookup(name, self.include)[0]
346 346 else:
347 347 inc = name
348 348 if self.exclude:
349 349 exc = self.lookup(name, self.exclude)[0]
350 350 else:
351 351 exc = ''
352 352 if not inc or exc:
353 353 return None
354 354 newpre, pre, suf = self.lookup(name, self.rename)
355 355 if newpre:
356 356 if newpre == '.':
357 357 return suf
358 358 if suf:
359 359 return newpre + '/' + suf
360 360 return newpre
361 361 return name
362 362
363 363 def active(self):
364 364 return bool(self.include or self.exclude or self.rename)
365 365
366 def _convert(ui, src, dest=None, revmapfile=None, **opts):
366 def convert(ui, src, dest=None, revmapfile=None, **opts):
367 367 """Convert a foreign SCM repository to a Mercurial one.
368 368
369 369 Accepted source formats:
370 370 - GIT
371 371 - CVS
372 372 - SVN
373 373
374 374 Accepted destination formats:
375 375 - Mercurial
376 376
377 377 If no revision is given, all revisions will be converted. Otherwise,
378 378 convert will only import up to the named revision (given in a format
379 379 understood by the source).
380 380
381 381 If no destination directory name is specified, it defaults to the
382 382 basename of the source with '-hg' appended. If the destination
383 383 repository doesn't exist, it will be created.
384 384
385 385 If <revmapfile> isn't given, it will be put in a default location
386 386 (<dest>/.hg/shamap by default). The <revmapfile> is a simple text
387 387 file that maps each source commit ID to the destination ID for
388 388 that revision, like so:
389 389 <source ID> <destination ID>
390 390
391 391 If the file doesn't exist, it's automatically created. It's updated
392 392 on each commit copied, so convert-repo can be interrupted and can
393 393 be run repeatedly to copy new commits.
394 394
395 395 The [username mapping] file is a simple text file that maps each source
396 396 commit author to a destination commit author. It is handy for source SCMs
397 397 that use unix logins to identify authors (eg: CVS). One line per author
398 398 mapping and the line format is:
399 399 srcauthor=whatever string you want
400 400
401 401 The filemap is a file that allows filtering and remapping of files
402 402 and directories. Comment lines start with '#'. Each line can
403 403 contain one of the following directives:
404 404
405 405 include path/to/file
406 406
407 407 exclude path/to/file
408 408
409 409 rename from/file to/file
410 410
411 411 The 'include' directive causes a file, or all files under a
412 412 directory, to be included in the destination repository. The
413 413 'exclude' directive causes files or directories to be omitted.
414 414 The 'rename' directive renames a file or directory. To rename
415 415 from a subdirectory into the root of the repository, use '.' as
416 416 the path to rename to.
417 417 """
418 418
419 419 util._encoding = 'UTF-8'
420 420
421 421 if not dest:
422 422 dest = hg.defaultdest(src) + "-hg"
423 423 ui.status("assuming destination %s\n" % dest)
424 424
425 425 # Try to be smart and initalize things when required
426 426 created = False
427 427 if os.path.isdir(dest):
428 428 if len(os.listdir(dest)) > 0:
429 429 try:
430 430 hg.repository(ui, dest)
431 431 ui.status("destination %s is a Mercurial repository\n" % dest)
432 432 except hg.RepoError:
433 433 raise util.Abort(
434 434 "destination directory %s is not empty.\n"
435 435 "Please specify an empty directory to be initialized\n"
436 436 "or an already initialized mercurial repository"
437 437 % dest)
438 438 else:
439 439 ui.status("initializing destination %s repository\n" % dest)
440 440 hg.repository(ui, dest, create=True)
441 441 created = True
442 442 elif os.path.exists(dest):
443 443 raise util.Abort("destination %s exists and is not a directory" % dest)
444 444 else:
445 445 ui.status("initializing destination %s repository\n" % dest)
446 446 hg.repository(ui, dest, create=True)
447 447 created = True
448 448
449 449 destc = convertsink(ui, dest)
450 450
451 451 try:
452 452 srcc = convertsource(ui, src, rev=opts.get('rev'))
453 453 except Exception:
454 454 if created:
455 455 shutil.rmtree(dest, True)
456 456 raise
457 457
458 458 if not revmapfile:
459 459 try:
460 460 revmapfile = destc.revmapfile()
461 461 except:
462 462 revmapfile = os.path.join(destc, "map")
463 463
464 464
465 c = convert(ui, srcc, destc, revmapfile, filemapper(ui, opts['filemap']),
466 opts)
465 c = converter(ui, srcc, destc, revmapfile, filemapper(ui, opts['filemap']),
466 opts)
467 467 c.convert()
468 468
469 469
470 470 cmdtable = {
471 471 "convert":
472 (_convert,
472 (convert,
473 473 [('A', 'authors', '', 'username mapping filename'),
474 474 ('', 'filemap', '', 'remap file names using contents of file'),
475 475 ('r', 'rev', '', 'import up to target revision REV'),
476 476 ('', 'datesort', None, 'try to sort changesets by date')],
477 477 'hg convert [OPTION]... SOURCE [DEST [MAPFILE]]'),
478 478 "debugsvnlog":
479 479 (debugsvnlog,
480 480 [],
481 481 'hg debugsvnlog'),
482 482 }
483 483
General Comments 0
You need to be logged in to leave comments. Login now