##// END OF EJS Templates
Turns convert.py into a real extension
Edouard Gomez -
r4513:ac2fe196 default
parent child Browse files
Show More
@@ -1,731 +1,720
1 #!/usr/bin/env python
2 #
3 # This is a generalized framework for converting between SCM
4 # repository formats.
5 #
6 # To use, run:
7 #
8 # convert-repo <source> [<dest> [<mapfile>]]
9 #
10 # Currently accepted source formats: git, cvs
11 # Currently accepted destination formats: hg
1 # convert.py Foreign SCM converter
12 2 #
13 # If destination isn't given, a new Mercurial repo named <src>-hg will
14 # be created. If <mapfile> isn't given, it will be put in a default
15 # location (<dest>/.hg/shamap by default)
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
16 4 #
17 # The <mapfile> is a simple text file that maps each source commit ID to
18 # the destination ID for that revision, like so:
19 #
20 # <source ID> <destination ID>
21 #
22 # If the file doesn't exist, it's automatically created. It's updated
23 # on each commit copied, so convert-repo can be interrupted and can
24 # be run repeatedly to copy new commits.
5 # This software may be used and distributed according to the terms
6 # of the GNU General Public License, incorporated herein by reference.
25 7
26 8 import sys, os, zlib, sha, time, re, locale, socket
27 os.environ["HGENCODING"] = "utf-8"
28 from mercurial import hg, ui, util, fancyopts
9 from mercurial import hg, ui, util, commands
29 10
30 class Abort(Exception): pass
11 commands.norepo += " convert"
12
31 13 class NoRepo(Exception): pass
32 14
33 15 class commit(object):
34 16 def __init__(self, **parts):
35 17 for x in "author date desc parents".split():
36 18 if not x in parts:
37 abort("commit missing field %s\n" % x)
19 raise util.Abort("commit missing field %s\n" % x)
38 20 self.__dict__.update(parts)
39 21
40 quiet = 0
41 def status(msg):
42 if not quiet: sys.stdout.write(str(msg))
43
44 def warn(msg):
45 sys.stderr.write(str(msg))
46
47 def abort(msg):
48 raise Abort(msg)
49
50 22 def recode(s):
51 23 try:
52 24 return s.decode("utf-8").encode("utf-8")
53 25 except:
54 26 try:
55 27 return s.decode("latin-1").encode("utf-8")
56 28 except:
57 29 return s.decode("utf-8", "replace").encode("utf-8")
58 30
59 31 class converter_source(object):
60 32 """Conversion source interface"""
61 33
62 def __init__(self, path):
34 def __init__(self, ui, path):
63 35 """Initialize conversion source (or raise NoRepo("message")
64 36 exception if path is not a valid repository)"""
65 37 raise NotImplementedError()
66 38
67 39 def getheads(self):
68 40 """Return a list of this repository's heads"""
69 41 raise NotImplementedError()
70 42
71 43 def getfile(self, name, rev):
72 44 """Return file contents as a string"""
73 45 raise NotImplementedError()
74 46
75 47 def getmode(self, name, rev):
76 48 """Return file mode, eg. '', 'x', or 'l'"""
77 49 raise NotImplementedError()
78 50
79 51 def getchanges(self, version):
80 52 """Return sorted list of (filename, id) tuples for all files changed in rev.
81 53
82 54 id just tells us which revision to return in getfile(), e.g. in
83 55 git it's an object hash."""
84 56 raise NotImplementedError()
85 57
86 58 def getcommit(self, version):
87 59 """Return the commit object for version"""
88 60 raise NotImplementedError()
89 61
90 62 def gettags(self):
91 63 """Return the tags as a dictionary of name: revision"""
92 64 raise NotImplementedError()
93 65
94 66 class converter_sink(object):
95 67 """Conversion sink (target) interface"""
96 68
97 def __init__(self, path):
69 def __init__(self, ui, path):
98 70 """Initialize conversion sink (or raise NoRepo("message")
99 71 exception if path is not a valid repository)"""
100 72 raise NotImplementedError()
101 73
102 74 def getheads(self):
103 75 """Return a list of this repository's heads"""
104 76 raise NotImplementedError()
105 77
106 78 def mapfile(self):
107 79 """Path to a file that will contain lines
108 80 source_rev_id sink_rev_id
109 81 mapping equivalent revision identifiers for each system."""
110 82 raise NotImplementedError()
111 83
112 84 def putfile(self, f, e, data):
113 85 """Put file for next putcommit().
114 86 f: path to file
115 87 e: '', 'x', or 'l' (regular file, executable, or symlink)
116 88 data: file contents"""
117 89 raise NotImplementedError()
118 90
119 91 def delfile(self, f):
120 92 """Delete file for next putcommit().
121 93 f: path to file"""
122 94 raise NotImplementedError()
123 95
124 96 def putcommit(self, files, parents, commit):
125 97 """Create a revision with all changed files listed in 'files'
126 98 and having listed parents. 'commit' is a commit object containing
127 99 at a minimum the author, date, and message for this changeset.
128 100 Called after putfile() and delfile() calls. Note that the sink
129 101 repository is not told to update itself to a particular revision
130 102 (or even what that revision would be) before it receives the
131 103 file data."""
132 104 raise NotImplementedError()
133 105
134 106 def puttags(self, tags):
135 107 """Put tags into sink.
136 108 tags: {tagname: sink_rev_id, ...}"""
137 109 raise NotImplementedError()
138 110
139 111
140 112 # CVS conversion code inspired by hg-cvs-import and git-cvsimport
141 113 class convert_cvs(converter_source):
142 def __init__(self, path):
114 def __init__(self, ui, path):
143 115 self.path = path
116 self.ui = ui
144 117 cvs = os.path.join(path, "CVS")
145 118 if not os.path.exists(cvs):
146 119 raise NoRepo("couldn't open CVS repo %s" % path)
147 120
148 121 self.changeset = {}
149 122 self.files = {}
150 123 self.tags = {}
151 124 self.lastbranch = {}
152 125 self.parent = {}
153 126 self.socket = None
154 127 self.cvsroot = file(os.path.join(cvs, "Root")).read()[:-1]
155 128 self.cvsrepo = file(os.path.join(cvs, "Repository")).read()[:-1]
156 129 self.encoding = locale.getpreferredencoding()
157 130 self._parse()
158 131 self._connect()
159 132
160 133 def _parse(self):
161 134 if self.changeset:
162 135 return
163 136
164 137 d = os.getcwd()
165 138 try:
166 139 os.chdir(self.path)
167 140 id = None
168 141 state = 0
169 142 for l in os.popen("cvsps -A -u --cvs-direct -q"):
170 143 if state == 0: # header
171 144 if l.startswith("PatchSet"):
172 145 id = l[9:-2]
173 146 elif l.startswith("Date"):
174 147 date = util.parsedate(l[6:-1], ["%Y/%m/%d %H:%M:%S"])
175 148 date = util.datestr(date)
176 149 elif l.startswith("Branch"):
177 150 branch = l[8:-1]
178 151 self.parent[id] = self.lastbranch.get(branch,'bad')
179 152 self.lastbranch[branch] = id
180 153 elif l.startswith("Ancestor branch"):
181 154 ancestor = l[17:-1]
182 155 self.parent[id] = self.lastbranch[ancestor]
183 156 elif l.startswith("Author"):
184 157 author = self.recode(l[8:-1])
185 158 elif l.startswith("Tag: "):
186 159 t = l[5:-1].rstrip()
187 160 if t != "(none)":
188 161 self.tags[t] = id
189 162 elif l.startswith("Log:"):
190 163 state = 1
191 164 log = ""
192 165 elif state == 1: # log
193 166 if l == "Members: \n":
194 167 files = {}
195 168 log = self.recode(log[:-1])
196 169 if log.isspace():
197 170 log = "*** empty log message ***\n"
198 171 state = 2
199 172 else:
200 173 log += l
201 174 elif state == 2:
202 175 if l == "\n": #
203 176 state = 0
204 177 p = [self.parent[id]]
205 178 if id == "1":
206 179 p = []
207 180 c = commit(author=author, date=date, parents=p,
208 181 desc=log, branch=branch)
209 182 self.changeset[id] = c
210 183 self.files[id] = files
211 184 else:
212 185 file,rev = l[1:-2].rsplit(':',1)
213 186 rev = rev.split("->")[1]
214 187 files[file] = rev
215 188
216 189 self.heads = self.lastbranch.values()
217 190 finally:
218 191 os.chdir(d)
219 192
220 193 def _connect(self):
221 194 root = self.cvsroot
222 195 conntype = None
223 196 user, host = None, None
224 197 cmd = ['cvs', 'server']
225 198
226 status("connecting to %s\n" % root)
199 self.ui.status("connecting to %s\n" % root)
227 200
228 201 if root.startswith(":pserver:"):
229 202 root = root[9:]
230 203 m = re.match(r'(?:(.*?)(?::(.*?))?@)?([^:\/]*)(?::(\d*))?(.*)', root)
231 204 if m:
232 205 conntype = "pserver"
233 206 user, passw, serv, port, root = m.groups()
234 207 if not user:
235 208 user = "anonymous"
236 209 rr = ":pserver:" + user + "@" + serv + ":" + root
237 210 if port:
238 211 rr2, port = "-", int(port)
239 212 else:
240 213 rr2, port = rr, 2401
241 214 rr += str(port)
242 215
243 216 if not passw:
244 217 passw = "A"
245 218 pf = open(os.path.join(os.environ["HOME"], ".cvspass"))
246 219 for l in pf:
247 220 # :pserver:cvs@mea.tmt.tele.fi:/cvsroot/zmailer Ah<Z
248 221 m = re.match(r'(/\d+\s+/)?(.*)', l)
249 222 l = m.group(2)
250 223 w, p = l.split(' ', 1)
251 224 if w in [rr, rr2]:
252 225 passw = p
253 226 break
254 227 pf.close()
255 228
256 229 sck = socket.socket()
257 230 sck.connect((serv, port))
258 231 sck.send("\n".join(["BEGIN AUTH REQUEST", root, user, passw, "END AUTH REQUEST", ""]))
259 232 if sck.recv(128) != "I LOVE YOU\n":
260 233 raise NoRepo("CVS pserver authentication failed")
261 234
262 235 self.writep = self.readp = sck.makefile('r+')
263 236
264 237 if not conntype and root.startswith(":local:"):
265 238 conntype = "local"
266 239 root = root[7:]
267 240
268 241 if not conntype:
269 242 # :ext:user@host/home/user/path/to/cvsroot
270 243 if root.startswith(":ext:"):
271 244 root = root[5:]
272 245 m = re.match(r'(?:([^@:/]+)@)?([^:/]+):?(.*)', root)
273 246 if not m:
274 247 conntype = "local"
275 248 else:
276 249 conntype = "rsh"
277 250 user, host, root = m.group(1), m.group(2), m.group(3)
278 251
279 252 if conntype != "pserver":
280 253 if conntype == "rsh":
281 254 rsh = os.environ.get("CVS_RSH" or "rsh")
282 255 if user:
283 256 cmd = [rsh, '-l', user, host] + cmd
284 257 else:
285 258 cmd = [rsh, host] + cmd
286 259
287 260 self.writep, self.readp = os.popen2(cmd)
288 261
289 262 self.realroot = root
290 263
291 264 self.writep.write("Root %s\n" % root)
292 265 self.writep.write("Valid-responses ok error Valid-requests Mode"
293 266 " M Mbinary E Checked-in Created Updated"
294 267 " Merged Removed\n")
295 268 self.writep.write("valid-requests\n")
296 269 self.writep.flush()
297 270 r = self.readp.readline()
298 271 if not r.startswith("Valid-requests"):
299 abort("server sucks\n")
272 raise util.Abort("server sucks\n")
300 273 if "UseUnchanged" in r:
301 274 self.writep.write("UseUnchanged\n")
302 275 self.writep.flush()
303 276 r = self.readp.readline()
304 277
305 278 def getheads(self):
306 279 return self.heads
307 280
308 281 def _getfile(self, name, rev):
309 282 if rev.endswith("(DEAD)"):
310 283 raise IOError
311 284
312 285 args = ("-N -P -kk -r %s --" % rev).split()
313 286 args.append(os.path.join(self.cvsrepo, name))
314 287 for x in args:
315 288 self.writep.write("Argument %s\n" % x)
316 289 self.writep.write("Directory .\n%s\nco\n" % self.realroot)
317 290 self.writep.flush()
318 291
319 292 data = ""
320 293 while 1:
321 294 line = self.readp.readline()
322 295 if line.startswith("Created ") or line.startswith("Updated "):
323 296 self.readp.readline() # path
324 297 self.readp.readline() # entries
325 298 mode = self.readp.readline()[:-1]
326 299 count = int(self.readp.readline()[:-1])
327 300 data = self.readp.read(count)
328 301 elif line.startswith(" "):
329 302 data += line[1:]
330 303 elif line.startswith("M "):
331 304 pass
332 305 elif line.startswith("Mbinary "):
333 306 count = int(self.readp.readline()[:-1])
334 307 data = self.readp.read(count)
335 308 else:
336 309 if line == "ok\n":
337 310 return (data, "x" in mode and "x" or "")
338 311 elif line.startswith("E "):
339 warn("cvs server: %s\n" % line[2:])
312 self.ui.warn("cvs server: %s\n" % line[2:])
340 313 elif line.startswith("Remove"):
341 314 l = self.readp.readline()
342 315 l = self.readp.readline()
343 316 if l != "ok\n":
344 abort("unknown CVS response: %s\n" % l)
317 raise util.Abort("unknown CVS response: %s\n" % l)
345 318 else:
346 abort("unknown CVS response: %s\n" % line)
319 raise util.Abort("unknown CVS response: %s\n" % line)
347 320
348 321 def getfile(self, file, rev):
349 322 data, mode = self._getfile(file, rev)
350 323 self.modecache[(file, rev)] = mode
351 324 return data
352 325
353 326 def getmode(self, file, rev):
354 327 return self.modecache[(file, rev)]
355 328
356 329 def getchanges(self, rev):
357 330 self.modecache = {}
358 331 files = self.files[rev]
359 332 cl = files.items()
360 333 cl.sort()
361 334 return cl
362 335
363 336 def recode(self, text):
364 337 return text.decode(self.encoding, "replace").encode("utf-8")
365 338
366 339 def getcommit(self, rev):
367 340 return self.changeset[rev]
368 341
369 342 def gettags(self):
370 343 return self.tags
371 344
372 345 class convert_git(converter_source):
373 def __init__(self, path):
346 def __init__(self, ui, path):
374 347 if os.path.isdir(path + "/.git"):
375 348 path += "/.git"
376 349 self.path = path
350 self.ui = ui
377 351 if not os.path.exists(path + "/objects"):
378 352 raise NoRepo("couldn't open GIT repo %s" % path)
379 353
380 354 def getheads(self):
381 355 fh = os.popen("GIT_DIR=%s git-rev-parse --verify HEAD" % self.path)
382 356 return [fh.read()[:-1]]
383 357
384 358 def catfile(self, rev, type):
385 359 if rev == "0" * 40: raise IOError()
386 360 fh = os.popen("GIT_DIR=%s git-cat-file %s %s 2>/dev/null" % (self.path, type, rev))
387 361 return fh.read()
388 362
389 363 def getfile(self, name, rev):
390 364 return self.catfile(rev, "blob")
391 365
392 366 def getmode(self, name, rev):
393 367 return self.modecache[(name, rev)]
394 368
395 369 def getchanges(self, version):
396 370 self.modecache = {}
397 371 fh = os.popen("GIT_DIR=%s git-diff-tree --root -m -r %s" % (self.path, version))
398 372 changes = []
399 373 for l in fh:
400 374 if "\t" not in l: continue
401 375 m, f = l[:-1].split("\t")
402 376 m = m.split()
403 377 h = m[3]
404 378 p = (m[1] == "100755")
405 379 s = (m[1] == "120000")
406 380 self.modecache[(f, h)] = (p and "x") or (s and "l") or ""
407 381 changes.append((f, h))
408 382 return changes
409 383
410 384 def getcommit(self, version):
411 385 c = self.catfile(version, "commit") # read the commit hash
412 386 end = c.find("\n\n")
413 387 message = c[end+2:]
414 388 message = recode(message)
415 389 l = c[:end].splitlines()
416 390 manifest = l[0].split()[1]
417 391 parents = []
418 392 for e in l[1:]:
419 393 n,v = e.split(" ", 1)
420 394 if n == "author":
421 395 p = v.split()
422 396 tm, tz = p[-2:]
423 397 author = " ".join(p[:-2])
424 398 if author[0] == "<": author = author[1:-1]
425 399 author = recode(author)
426 400 if n == "committer":
427 401 p = v.split()
428 402 tm, tz = p[-2:]
429 403 committer = " ".join(p[:-2])
430 404 if committer[0] == "<": committer = committer[1:-1]
431 405 committer = recode(committer)
432 406 message += "\ncommitter: %s\n" % committer
433 407 if n == "parent": parents.append(v)
434 408
435 409 tzs, tzh, tzm = tz[-5:-4] + "1", tz[-4:-2], tz[-2:]
436 410 tz = -int(tzs) * (int(tzh) * 3600 + int(tzm))
437 411 date = tm + " " + str(tz)
438 412
439 413 c = commit(parents=parents, date=date, author=author, desc=message)
440 414 return c
441 415
442 416 def gettags(self):
443 417 tags = {}
444 418 fh = os.popen('git-ls-remote --tags "%s" 2>/dev/null' % self.path)
445 419 prefix = 'refs/tags/'
446 420 for line in fh:
447 421 line = line.strip()
448 422 if not line.endswith("^{}"):
449 423 continue
450 424 node, tag = line.split(None, 1)
451 425 if not tag.startswith(prefix):
452 426 continue
453 427 tag = tag[len(prefix):-3]
454 428 tags[tag] = node
455 429
456 430 return tags
457 431
458 432 class convert_mercurial(converter_sink):
459 def __init__(self, path):
433 def __init__(self, ui, path):
460 434 self.path = path
461 u = ui.ui()
435 self.ui = ui
462 436 try:
463 self.repo = hg.repository(u, path)
437 self.repo = hg.repository(self.ui, path)
464 438 except:
465 439 raise NoRepo("could open hg repo %s" % path)
466 440
467 441 def mapfile(self):
468 442 return os.path.join(self.path, ".hg", "shamap")
469 443
470 444 def getheads(self):
471 445 h = self.repo.changelog.heads()
472 446 return [ hg.hex(x) for x in h ]
473 447
474 448 def putfile(self, f, e, data):
475 449 self.repo.wwrite(f, data, e)
476 450 if self.repo.dirstate.state(f) == '?':
477 451 self.repo.dirstate.update([f], "a")
478 452
479 453 def delfile(self, f):
480 454 try:
481 455 os.unlink(self.repo.wjoin(f))
482 456 #self.repo.remove([f])
483 457 except:
484 458 pass
485 459
486 460 def putcommit(self, files, parents, commit):
487 461 seen = {}
488 462 pl = []
489 463 for p in parents:
490 464 if p not in seen:
491 465 pl.append(p)
492 466 seen[p] = 1
493 467 parents = pl
494 468
495 469 if len(parents) < 2: parents.append("0" * 40)
496 470 if len(parents) < 2: parents.append("0" * 40)
497 471 p2 = parents.pop(0)
498 472
499 473 text = commit.desc
500 474 extra = {}
501 475 try:
502 476 extra["branch"] = commit.branch
503 477 except AttributeError:
504 478 pass
505 479
506 480 while parents:
507 481 p1 = p2
508 482 p2 = parents.pop(0)
509 483 a = self.repo.rawcommit(files, text, commit.author, commit.date,
510 484 hg.bin(p1), hg.bin(p2), extra=extra)
511 485 text = "(octopus merge fixup)\n"
512 486 p2 = hg.hex(self.repo.changelog.tip())
513 487
514 488 return p2
515 489
516 490 def puttags(self, tags):
517 491 try:
518 492 old = self.repo.wfile(".hgtags").read()
519 493 oldlines = old.splitlines(1)
520 494 oldlines.sort()
521 495 except:
522 496 oldlines = []
523 497
524 498 k = tags.keys()
525 499 k.sort()
526 500 newlines = []
527 501 for tag in k:
528 502 newlines.append("%s %s\n" % (tags[tag], tag))
529 503
530 504 newlines.sort()
531 505
532 506 if newlines != oldlines:
533 status("updating tags\n")
507 self.ui.status("updating tags\n")
534 508 f = self.repo.wfile(".hgtags", "w")
535 509 f.write("".join(newlines))
536 510 f.close()
537 511 if not oldlines: self.repo.add([".hgtags"])
538 512 date = "%s 0" % int(time.mktime(time.gmtime()))
539 513 self.repo.rawcommit([".hgtags"], "update tags", "convert-repo",
540 514 date, self.repo.changelog.tip(), hg.nullid)
541 515 return hg.hex(self.repo.changelog.tip())
542 516
543 517 converters = [convert_cvs, convert_git, convert_mercurial]
544 518
545 def converter(path):
519 def converter(ui, path):
546 520 if not os.path.isdir(path):
547 abort("%s: not a directory\n" % path)
521 raise util.Abort("%s: not a directory\n" % path)
548 522 for c in converters:
549 523 try:
550 return c(path)
524 return c(ui, path)
551 525 except NoRepo:
552 526 pass
553 abort("%s: unknown repository type\n" % path)
527 raise util.Abort("%s: unknown repository type\n" % path)
554 528
555 529 class convert(object):
556 def __init__(self, source, dest, mapfile, opts):
530 def __init__(self, ui, source, dest, mapfile, opts):
557 531
558 532 self.source = source
559 533 self.dest = dest
534 self.ui = ui
560 535 self.mapfile = mapfile
561 536 self.opts = opts
562 537 self.commitcache = {}
563 538
564 539 self.map = {}
565 540 try:
566 541 for l in file(self.mapfile):
567 542 sv, dv = l[:-1].split()
568 543 self.map[sv] = dv
569 544 except IOError:
570 545 pass
571 546
572 547 def walktree(self, heads):
573 548 visit = heads
574 549 known = {}
575 550 parents = {}
576 551 while visit:
577 552 n = visit.pop(0)
578 553 if n in known or n in self.map: continue
579 554 known[n] = 1
580 555 self.commitcache[n] = self.source.getcommit(n)
581 556 cp = self.commitcache[n].parents
582 557 for p in cp:
583 558 parents.setdefault(n, []).append(p)
584 559 visit.append(p)
585 560
586 561 return parents
587 562
588 563 def toposort(self, parents):
589 564 visit = parents.keys()
590 565 seen = {}
591 566 children = {}
592 567
593 568 while visit:
594 569 n = visit.pop(0)
595 570 if n in seen: continue
596 571 seen[n] = 1
597 572 pc = 0
598 573 if n in parents:
599 574 for p in parents[n]:
600 575 if p not in self.map: pc += 1
601 576 visit.append(p)
602 577 children.setdefault(p, []).append(n)
603 578 if not pc: root = n
604 579
605 580 s = []
606 581 removed = {}
607 582 visit = children.keys()
608 583 while visit:
609 584 n = visit.pop(0)
610 585 if n in removed: continue
611 586 dep = 0
612 587 if n in parents:
613 588 for p in parents[n]:
614 589 if p in self.map: continue
615 590 if p not in removed:
616 591 # we're still dependent
617 592 visit.append(n)
618 593 dep = 1
619 594 break
620 595
621 596 if not dep:
622 597 # all n's parents are in the list
623 598 removed[n] = 1
624 599 if n not in self.map:
625 600 s.append(n)
626 601 if n in children:
627 602 for c in children[n]:
628 603 visit.insert(0, c)
629 604
630 if opts.get('datesort'):
605 if self.opts.get('datesort'):
631 606 depth = {}
632 607 for n in s:
633 608 depth[n] = 0
634 609 pl = [p for p in self.commitcache[n].parents if p not in self.map]
635 610 if pl:
636 611 depth[n] = max([depth[p] for p in pl]) + 1
637 612
638 613 s = [(depth[n], self.commitcache[n].date, n) for n in s]
639 614 s.sort()
640 615 s = [e[2] for e in s]
641 616
642 617 return s
643 618
644 619 def copy(self, rev):
645 620 c = self.commitcache[rev]
646 621 files = self.source.getchanges(rev)
647 622
648 623 for f,v in files:
649 624 try:
650 625 data = self.source.getfile(f, v)
651 626 except IOError, inst:
652 627 self.dest.delfile(f)
653 628 else:
654 629 e = self.source.getmode(f, v)
655 630 self.dest.putfile(f, e, data)
656 631
657 632 r = [self.map[v] for v in c.parents]
658 633 f = [f for f,v in files]
659 634 self.map[rev] = self.dest.putcommit(f, r, c)
660 635 file(self.mapfile, "a").write("%s %s\n" % (rev, self.map[rev]))
661 636
662 637 def convert(self):
663 status("scanning source...\n")
638 self.ui.status("scanning source...\n")
664 639 heads = self.source.getheads()
665 640 parents = self.walktree(heads)
666 status("sorting...\n")
641 self.ui.status("sorting...\n")
667 642 t = self.toposort(parents)
668 643 num = len(t)
669 644 c = None
670 645
671 status("converting...\n")
646 self.ui.status("converting...\n")
672 647 for c in t:
673 648 num -= 1
674 649 desc = self.commitcache[c].desc
675 650 if "\n" in desc:
676 651 desc = desc.splitlines()[0]
677 status("%d %s\n" % (num, desc))
652 self.ui.status("%d %s\n" % (num, desc))
678 653 self.copy(c)
679 654
680 655 tags = self.source.gettags()
681 656 ctags = {}
682 657 for k in tags:
683 658 v = tags[k]
684 659 if v in self.map:
685 660 ctags[k] = self.map[v]
686 661
687 662 if c and ctags:
688 663 nrev = self.dest.puttags(ctags)
689 664 # write another hash correspondence to override the previous
690 665 # one so we don't end up with extra tag heads
691 666 if nrev:
692 667 file(self.mapfile, "a").write("%s %s\n" % (c, nrev))
693 668
694 def command(src, dest=None, mapfile=None, **opts):
695 srcc = converter(src)
669 def _convert(ui, src, dest=None, mapfile=None, **opts):
670 '''Convert a foreign SCM repository to a Mercurial one.
671
672 Accepted source formats:
673 - GIT
674 - CVS
675
676 Accepted destination formats:
677 - Mercurial
678
679 If destination isn't given, a new Mercurial repo named <src>-hg will
680 be created. If <mapfile> isn't given, it will be put in a default
681 location (<dest>/.hg/shamap by default)
682
683 The <mapfile> is a simple text file that maps each source commit ID to
684 the destination ID for that revision, like so:
685
686 <source ID> <destination ID>
687
688 If the file doesn't exist, it's automatically created. It's updated
689 on each commit copied, so convert-repo can be interrupted and can
690 be run repeatedly to copy new commits.
691 '''
692
693 srcc = converter(ui, src)
696 694 if not hasattr(srcc, "getcommit"):
697 abort("%s: can't read from this repo type\n" % src)
695 raise util.Abort("%s: can't read from this repo type\n" % src)
698 696
699 697 if not dest:
700 698 dest = src + "-hg"
701 status("assuming destination %s\n" % dest)
699 ui.status("assuming destination %s\n" % dest)
702 700 if not os.path.isdir(dest):
703 status("creating repository %s\n" % dest)
701 ui.status("creating repository %s\n" % dest)
704 702 os.system("hg init " + dest)
705 destc = converter(dest)
703 destc = converter(ui, dest)
706 704 if not hasattr(destc, "putcommit"):
707 abort("%s: can't write to this repo type\n" % src)
705 raise util.Abort("%s: can't write to this repo type\n" % src)
708 706
709 707 if not mapfile:
710 708 try:
711 709 mapfile = destc.mapfile()
712 710 except:
713 711 mapfile = os.path.join(destc, "map")
714 712
715 c = convert(srcc, destc, mapfile, opts)
713 c = convert(ui, srcc, destc, mapfile, opts)
716 714 c.convert()
717 715
718 options = [('q', 'quiet', None, 'suppress output'),
719 ('', 'datesort', None, 'try to sort changesets by date')]
720 opts = {}
721 args = fancyopts.fancyopts(sys.argv[1:], options, opts)
722
723 if opts['quiet']:
724 quiet = 1
725
726 try:
727 command(*args, **opts)
728 except Abort, inst:
729 warn(inst)
730 except KeyboardInterrupt:
731 status("interrupted\n")
716 cmdtable = {
717 "convert": (_convert,
718 [('', 'datesort', None, 'try to sort changesets by date')],
719 'hg convert [OPTIONS] <src> [dst [map]]'),
720 }
General Comments 0
You need to be logged in to leave comments. Login now