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, SKIPREV, converter_source, converter_sink, mapfile |
8 import convcmd |
9 from cvs import convert_cvs |
9 from mercurial import commands |
10 from darcs import darcs_source |
|
11 from git import convert_git |
|
12 from hg import mercurial_source, mercurial_sink |
|
13 from subversion import debugsvnlog, svn_source, svn_sink |
|
14 import filemap |
|
15 |
10 |
16 import os, shutil |
11 # Commands definition was moved elsewhere to ease demandload job. |
17 from mercurial import hg, ui, util, commands |
|
18 from mercurial.i18n import _ |
|
19 |
|
20 commands.norepo += " convert debugsvnlog" |
|
21 |
|
22 source_converters = [ |
|
23 ('cvs', convert_cvs), |
|
24 ('git', convert_git), |
|
25 ('svn', svn_source), |
|
26 ('hg', mercurial_source), |
|
27 ('darcs', darcs_source), |
|
28 ] |
|
29 |
|
30 sink_converters = [ |
|
31 ('hg', mercurial_sink), |
|
32 ('svn', svn_sink), |
|
33 ] |
|
34 |
|
35 def convertsource(ui, path, type, rev): |
|
36 exceptions = [] |
|
37 for name, source in source_converters: |
|
38 try: |
|
39 if not type or name == type: |
|
40 return source(ui, path, rev) |
|
41 except NoRepo, inst: |
|
42 exceptions.append(inst) |
|
43 if not ui.quiet: |
|
44 for inst in exceptions: |
|
45 ui.write(_("%s\n") % inst) |
|
46 raise util.Abort('%s: unknown repository type' % path) |
|
47 |
|
48 def convertsink(ui, path, type): |
|
49 for name, sink in sink_converters: |
|
50 try: |
|
51 if not type or name == type: |
|
52 return sink(ui, path) |
|
53 except NoRepo, inst: |
|
54 ui.note(_("convert: %s\n") % inst) |
|
55 raise util.Abort('%s: unknown repository type' % path) |
|
56 |
|
57 class converter(object): |
|
58 def __init__(self, ui, source, dest, revmapfile, opts): |
|
59 |
|
60 self.source = source |
|
61 self.dest = dest |
|
62 self.ui = ui |
|
63 self.opts = opts |
|
64 self.commitcache = {} |
|
65 self.authors = {} |
|
66 self.authorfile = None |
|
67 |
|
68 self.map = mapfile(ui, revmapfile) |
|
69 |
|
70 # Read first the dst author map if any |
|
71 authorfile = self.dest.authorfile() |
|
72 if authorfile and os.path.exists(authorfile): |
|
73 self.readauthormap(authorfile) |
|
74 # Extend/Override with new author map if necessary |
|
75 if opts.get('authors'): |
|
76 self.readauthormap(opts.get('authors')) |
|
77 self.authorfile = self.dest.authorfile() |
|
78 |
|
79 def walktree(self, heads): |
|
80 '''Return a mapping that identifies the uncommitted parents of every |
|
81 uncommitted changeset.''' |
|
82 visit = heads |
|
83 known = {} |
|
84 parents = {} |
|
85 while visit: |
|
86 n = visit.pop(0) |
|
87 if n in known or n in self.map: continue |
|
88 known[n] = 1 |
|
89 commit = self.cachecommit(n) |
|
90 parents[n] = [] |
|
91 for p in commit.parents: |
|
92 parents[n].append(p) |
|
93 visit.append(p) |
|
94 |
|
95 return parents |
|
96 |
|
97 def toposort(self, parents): |
|
98 '''Return an ordering such that every uncommitted changeset is |
|
99 preceeded by all its uncommitted ancestors.''' |
|
100 visit = parents.keys() |
|
101 seen = {} |
|
102 children = {} |
|
103 |
|
104 while visit: |
|
105 n = visit.pop(0) |
|
106 if n in seen: continue |
|
107 seen[n] = 1 |
|
108 # Ensure that nodes without parents are present in the 'children' |
|
109 # mapping. |
|
110 children.setdefault(n, []) |
|
111 for p in parents[n]: |
|
112 if not p in self.map: |
|
113 visit.append(p) |
|
114 children.setdefault(p, []).append(n) |
|
115 |
|
116 s = [] |
|
117 removed = {} |
|
118 visit = children.keys() |
|
119 while visit: |
|
120 n = visit.pop(0) |
|
121 if n in removed: continue |
|
122 dep = 0 |
|
123 if n in parents: |
|
124 for p in parents[n]: |
|
125 if p in self.map: continue |
|
126 if p not in removed: |
|
127 # we're still dependent |
|
128 visit.append(n) |
|
129 dep = 1 |
|
130 break |
|
131 |
|
132 if not dep: |
|
133 # all n's parents are in the list |
|
134 removed[n] = 1 |
|
135 if n not in self.map: |
|
136 s.append(n) |
|
137 if n in children: |
|
138 for c in children[n]: |
|
139 visit.insert(0, c) |
|
140 |
|
141 if self.opts.get('datesort'): |
|
142 depth = {} |
|
143 for n in s: |
|
144 depth[n] = 0 |
|
145 pl = [p for p in self.commitcache[n].parents |
|
146 if p not in self.map] |
|
147 if pl: |
|
148 depth[n] = max([depth[p] for p in pl]) + 1 |
|
149 |
|
150 s = [(depth[n], self.commitcache[n].date, n) for n in s] |
|
151 s.sort() |
|
152 s = [e[2] for e in s] |
|
153 |
|
154 return s |
|
155 |
|
156 def writeauthormap(self): |
|
157 authorfile = self.authorfile |
|
158 if authorfile: |
|
159 self.ui.status('Writing author map file %s\n' % authorfile) |
|
160 ofile = open(authorfile, 'w+') |
|
161 for author in self.authors: |
|
162 ofile.write("%s=%s\n" % (author, self.authors[author])) |
|
163 ofile.close() |
|
164 |
|
165 def readauthormap(self, authorfile): |
|
166 afile = open(authorfile, 'r') |
|
167 for line in afile: |
|
168 try: |
|
169 srcauthor = line.split('=')[0].strip() |
|
170 dstauthor = line.split('=')[1].strip() |
|
171 if srcauthor in self.authors and dstauthor != self.authors[srcauthor]: |
|
172 self.ui.status( |
|
173 'Overriding mapping for author %s, was %s, will be %s\n' |
|
174 % (srcauthor, self.authors[srcauthor], dstauthor)) |
|
175 else: |
|
176 self.ui.debug('Mapping author %s to %s\n' |
|
177 % (srcauthor, dstauthor)) |
|
178 self.authors[srcauthor] = dstauthor |
|
179 except IndexError: |
|
180 self.ui.warn( |
|
181 'Ignoring bad line in author file map %s: %s\n' |
|
182 % (authorfile, line)) |
|
183 afile.close() |
|
184 |
|
185 def cachecommit(self, rev): |
|
186 commit = self.source.getcommit(rev) |
|
187 commit.author = self.authors.get(commit.author, commit.author) |
|
188 self.commitcache[rev] = commit |
|
189 return commit |
|
190 |
|
191 def copy(self, rev): |
|
192 commit = self.commitcache[rev] |
|
193 do_copies = hasattr(self.dest, 'copyfile') |
|
194 filenames = [] |
|
195 |
|
196 changes = self.source.getchanges(rev) |
|
197 if isinstance(changes, basestring): |
|
198 if changes == SKIPREV: |
|
199 dest = SKIPREV |
|
200 else: |
|
201 dest = self.map[changes] |
|
202 self.map[rev] = dest |
|
203 return |
|
204 files, copies = changes |
|
205 parents = [self.map[r] for r in commit.parents] |
|
206 if commit.parents: |
|
207 prev = commit.parents[0] |
|
208 if prev not in self.commitcache: |
|
209 self.cachecommit(prev) |
|
210 pbranch = self.commitcache[prev].branch |
|
211 else: |
|
212 pbranch = None |
|
213 self.dest.setbranch(commit.branch, pbranch, parents) |
|
214 for f, v in files: |
|
215 filenames.append(f) |
|
216 try: |
|
217 data = self.source.getfile(f, v) |
|
218 except IOError, inst: |
|
219 self.dest.delfile(f) |
|
220 else: |
|
221 e = self.source.getmode(f, v) |
|
222 self.dest.putfile(f, e, data) |
|
223 if do_copies: |
|
224 if f in copies: |
|
225 copyf = copies[f] |
|
226 # Merely marks that a copy happened. |
|
227 self.dest.copyfile(copyf, f) |
|
228 |
|
229 newnode = self.dest.putcommit(filenames, parents, commit) |
|
230 self.source.converted(rev, newnode) |
|
231 self.map[rev] = newnode |
|
232 |
|
233 def convert(self): |
|
234 try: |
|
235 self.source.before() |
|
236 self.dest.before() |
|
237 self.source.setrevmap(self.map) |
|
238 self.ui.status("scanning source...\n") |
|
239 heads = self.source.getheads() |
|
240 parents = self.walktree(heads) |
|
241 self.ui.status("sorting...\n") |
|
242 t = self.toposort(parents) |
|
243 num = len(t) |
|
244 c = None |
|
245 |
|
246 self.ui.status("converting...\n") |
|
247 for c in t: |
|
248 num -= 1 |
|
249 desc = self.commitcache[c].desc |
|
250 if "\n" in desc: |
|
251 desc = desc.splitlines()[0] |
|
252 self.ui.status("%d %s\n" % (num, desc)) |
|
253 self.copy(c) |
|
254 |
|
255 tags = self.source.gettags() |
|
256 ctags = {} |
|
257 for k in tags: |
|
258 v = tags[k] |
|
259 if self.map.get(v, SKIPREV) != SKIPREV: |
|
260 ctags[k] = self.map[v] |
|
261 |
|
262 if c and ctags: |
|
263 nrev = self.dest.puttags(ctags) |
|
264 # write another hash correspondence to override the previous |
|
265 # one so we don't end up with extra tag heads |
|
266 if nrev: |
|
267 self.map[c] = nrev |
|
268 |
|
269 self.writeauthormap() |
|
270 finally: |
|
271 self.cleanup() |
|
272 |
|
273 def cleanup(self): |
|
274 try: |
|
275 self.dest.after() |
|
276 finally: |
|
277 self.source.after() |
|
278 self.map.close() |
|
279 |
12 |
280 def convert(ui, src, dest=None, revmapfile=None, **opts): |
13 def convert(ui, src, dest=None, revmapfile=None, **opts): |
281 """Convert a foreign SCM repository to a Mercurial one. |
14 """Convert a foreign SCM repository to a Mercurial one. |
282 |
15 |
283 Accepted source formats: |
16 Accepted source formats: |
349 --config convert.svn.tags=tags (directory name) |
82 --config convert.svn.tags=tags (directory name) |
350 svn source: specify the directory containing tags |
83 svn source: specify the directory containing tags |
351 --config convert.svn.trunk=trunk (directory name) |
84 --config convert.svn.trunk=trunk (directory name) |
352 svn source: specify the name of the trunk branch |
85 svn source: specify the name of the trunk branch |
353 """ |
86 """ |
|
87 return convcmd.convert(ui, src, dest, revmapfile, **opts) |
354 |
88 |
355 util._encoding = 'UTF-8' |
89 def debugsvnlog(ui, **opts): |
|
90 return convcmd.debugsvnlog(ui, **opts) |
356 |
91 |
357 if not dest: |
92 commands.norepo += " convert debugsvnlog" |
358 dest = hg.defaultdest(src) + "-hg" |
|
359 ui.status("assuming destination %s\n" % dest) |
|
360 |
|
361 destc = convertsink(ui, dest, opts.get('dest_type')) |
|
362 |
|
363 try: |
|
364 srcc = convertsource(ui, src, opts.get('source_type'), |
|
365 opts.get('rev')) |
|
366 except Exception: |
|
367 for path in destc.created: |
|
368 shutil.rmtree(path, True) |
|
369 raise |
|
370 |
|
371 fmap = opts.get('filemap') |
|
372 if fmap: |
|
373 srcc = filemap.filemap_source(ui, srcc, fmap) |
|
374 destc.setfilemapmode(True) |
|
375 |
|
376 if not revmapfile: |
|
377 try: |
|
378 revmapfile = destc.revmapfile() |
|
379 except: |
|
380 revmapfile = os.path.join(destc, "map") |
|
381 |
|
382 c = converter(ui, srcc, destc, revmapfile, opts) |
|
383 c.convert() |
|
384 |
|
385 |
93 |
386 cmdtable = { |
94 cmdtable = { |
387 "convert": |
95 "convert": |
388 (convert, |
96 (convert, |
389 [('A', 'authors', '', 'username mapping filename'), |
97 [('A', 'authors', '', 'username mapping filename'), |