##// END OF EJS Templates
fix typo in convert help text, update test
Benoit Boissinot -
r5488:247af577 default
parent child Browse files
Show More
@@ -1,400 +1,400 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, SKIPREV, converter_source, converter_sink
9 9 from cvs import convert_cvs
10 10 from darcs import darcs_source
11 11 from git import convert_git
12 12 from hg import mercurial_source, mercurial_sink
13 13 from subversion import svn_source, debugsvnlog
14 14 import filemap
15 15
16 16 import os, shutil
17 17 from mercurial import hg, ui, util, commands
18 18 from mercurial.i18n import _
19 19
20 20 commands.norepo += " convert debugsvnlog"
21 21
22 22 source_converters = [
23 23 ('cvs', convert_cvs),
24 24 ('git', convert_git),
25 25 ('svn', svn_source),
26 26 ('hg', mercurial_source),
27 27 ('darcs', darcs_source),
28 28 ]
29 29
30 30 sink_converters = [
31 31 ('hg', mercurial_sink),
32 32 ]
33 33
34 34 def convertsource(ui, path, type, rev):
35 35 for name, source in source_converters:
36 36 try:
37 37 if not type or name == type:
38 38 return source(ui, path, rev)
39 39 except NoRepo, inst:
40 40 ui.note(_("convert: %s\n") % inst)
41 41 raise util.Abort('%s: unknown repository type' % path)
42 42
43 43 def convertsink(ui, path, type):
44 44 for name, sink in sink_converters:
45 45 try:
46 46 if not type or name == type:
47 47 return sink(ui, path)
48 48 except NoRepo, inst:
49 49 ui.note(_("convert: %s\n") % inst)
50 50 raise util.Abort('%s: unknown repository type' % path)
51 51
52 52 class converter(object):
53 53 def __init__(self, ui, source, dest, revmapfile, opts):
54 54
55 55 self.source = source
56 56 self.dest = dest
57 57 self.ui = ui
58 58 self.opts = opts
59 59 self.commitcache = {}
60 60 self.revmapfile = revmapfile
61 61 self.revmapfilefd = None
62 62 self.authors = {}
63 63 self.authorfile = None
64 64
65 65 self.maporder = []
66 66 self.map = {}
67 67 try:
68 68 origrevmapfile = open(self.revmapfile, 'r')
69 69 for l in origrevmapfile:
70 70 sv, dv = l[:-1].split()
71 71 if sv not in self.map:
72 72 self.maporder.append(sv)
73 73 self.map[sv] = dv
74 74 origrevmapfile.close()
75 75 except IOError:
76 76 pass
77 77
78 78 # Read first the dst author map if any
79 79 authorfile = self.dest.authorfile()
80 80 if authorfile and os.path.exists(authorfile):
81 81 self.readauthormap(authorfile)
82 82 # Extend/Override with new author map if necessary
83 83 if opts.get('authors'):
84 84 self.readauthormap(opts.get('authors'))
85 85 self.authorfile = self.dest.authorfile()
86 86
87 87 def walktree(self, heads):
88 88 '''Return a mapping that identifies the uncommitted parents of every
89 89 uncommitted changeset.'''
90 90 visit = heads
91 91 known = {}
92 92 parents = {}
93 93 while visit:
94 94 n = visit.pop(0)
95 95 if n in known or n in self.map: continue
96 96 known[n] = 1
97 97 commit = self.cachecommit(n)
98 98 parents[n] = []
99 99 for p in commit.parents:
100 100 parents[n].append(p)
101 101 visit.append(p)
102 102
103 103 return parents
104 104
105 105 def toposort(self, parents):
106 106 '''Return an ordering such that every uncommitted changeset is
107 107 preceeded by all its uncommitted ancestors.'''
108 108 visit = parents.keys()
109 109 seen = {}
110 110 children = {}
111 111
112 112 while visit:
113 113 n = visit.pop(0)
114 114 if n in seen: continue
115 115 seen[n] = 1
116 116 # Ensure that nodes without parents are present in the 'children'
117 117 # mapping.
118 118 children.setdefault(n, [])
119 119 for p in parents[n]:
120 120 if not p in self.map:
121 121 visit.append(p)
122 122 children.setdefault(p, []).append(n)
123 123
124 124 s = []
125 125 removed = {}
126 126 visit = children.keys()
127 127 while visit:
128 128 n = visit.pop(0)
129 129 if n in removed: continue
130 130 dep = 0
131 131 if n in parents:
132 132 for p in parents[n]:
133 133 if p in self.map: continue
134 134 if p not in removed:
135 135 # we're still dependent
136 136 visit.append(n)
137 137 dep = 1
138 138 break
139 139
140 140 if not dep:
141 141 # all n's parents are in the list
142 142 removed[n] = 1
143 143 if n not in self.map:
144 144 s.append(n)
145 145 if n in children:
146 146 for c in children[n]:
147 147 visit.insert(0, c)
148 148
149 149 if self.opts.get('datesort'):
150 150 depth = {}
151 151 for n in s:
152 152 depth[n] = 0
153 153 pl = [p for p in self.commitcache[n].parents
154 154 if p not in self.map]
155 155 if pl:
156 156 depth[n] = max([depth[p] for p in pl]) + 1
157 157
158 158 s = [(depth[n], self.commitcache[n].date, n) for n in s]
159 159 s.sort()
160 160 s = [e[2] for e in s]
161 161
162 162 return s
163 163
164 164 def mapentry(self, src, dst):
165 165 if self.revmapfilefd is None:
166 166 try:
167 167 self.revmapfilefd = open(self.revmapfile, "a")
168 168 except IOError, (errno, strerror):
169 169 raise util.Abort("Could not open map file %s: %s, %s\n" % (self.revmapfile, errno, strerror))
170 170 self.map[src] = dst
171 171 self.revmapfilefd.write("%s %s\n" % (src, dst))
172 172 self.revmapfilefd.flush()
173 173
174 174 def writeauthormap(self):
175 175 authorfile = self.authorfile
176 176 if authorfile:
177 177 self.ui.status('Writing author map file %s\n' % authorfile)
178 178 ofile = open(authorfile, 'w+')
179 179 for author in self.authors:
180 180 ofile.write("%s=%s\n" % (author, self.authors[author]))
181 181 ofile.close()
182 182
183 183 def readauthormap(self, authorfile):
184 184 afile = open(authorfile, 'r')
185 185 for line in afile:
186 186 try:
187 187 srcauthor = line.split('=')[0].strip()
188 188 dstauthor = line.split('=')[1].strip()
189 189 if srcauthor in self.authors and dstauthor != self.authors[srcauthor]:
190 190 self.ui.status(
191 191 'Overriding mapping for author %s, was %s, will be %s\n'
192 192 % (srcauthor, self.authors[srcauthor], dstauthor))
193 193 else:
194 194 self.ui.debug('Mapping author %s to %s\n'
195 195 % (srcauthor, dstauthor))
196 196 self.authors[srcauthor] = dstauthor
197 197 except IndexError:
198 198 self.ui.warn(
199 199 'Ignoring bad line in author file map %s: %s\n'
200 200 % (authorfile, line))
201 201 afile.close()
202 202
203 203 def cachecommit(self, rev):
204 204 commit = self.source.getcommit(rev)
205 205 commit.author = self.authors.get(commit.author, commit.author)
206 206 self.commitcache[rev] = commit
207 207 return commit
208 208
209 209 def copy(self, rev):
210 210 commit = self.commitcache[rev]
211 211 do_copies = hasattr(self.dest, 'copyfile')
212 212 filenames = []
213 213
214 214 changes = self.source.getchanges(rev)
215 215 if isinstance(changes, basestring):
216 216 if changes == SKIPREV:
217 217 dest = SKIPREV
218 218 else:
219 219 dest = self.map[changes]
220 220 self.mapentry(rev, dest)
221 221 return
222 222 files, copies = changes
223 223 parents = [self.map[r] for r in commit.parents]
224 224 if commit.parents:
225 225 prev = commit.parents[0]
226 226 if prev not in self.commitcache:
227 227 self.cachecommit(prev)
228 228 pbranch = self.commitcache[prev].branch
229 229 else:
230 230 pbranch = None
231 231 self.dest.setbranch(commit.branch, pbranch, parents)
232 232 for f, v in files:
233 233 filenames.append(f)
234 234 try:
235 235 data = self.source.getfile(f, v)
236 236 except IOError, inst:
237 237 self.dest.delfile(f)
238 238 else:
239 239 e = self.source.getmode(f, v)
240 240 self.dest.putfile(f, e, data)
241 241 if do_copies:
242 242 if f in copies:
243 243 copyf = copies[f]
244 244 # Merely marks that a copy happened.
245 245 self.dest.copyfile(copyf, f)
246 246
247 247 newnode = self.dest.putcommit(filenames, parents, commit)
248 248 self.mapentry(rev, newnode)
249 249
250 250 def convert(self):
251 251 try:
252 252 self.source.before()
253 253 self.dest.before()
254 254 self.source.setrevmap(self.map, self.maporder)
255 255 self.ui.status("scanning source...\n")
256 256 heads = self.source.getheads()
257 257 parents = self.walktree(heads)
258 258 self.ui.status("sorting...\n")
259 259 t = self.toposort(parents)
260 260 num = len(t)
261 261 c = None
262 262
263 263 self.ui.status("converting...\n")
264 264 for c in t:
265 265 num -= 1
266 266 desc = self.commitcache[c].desc
267 267 if "\n" in desc:
268 268 desc = desc.splitlines()[0]
269 269 self.ui.status("%d %s\n" % (num, desc))
270 270 self.copy(c)
271 271
272 272 tags = self.source.gettags()
273 273 ctags = {}
274 274 for k in tags:
275 275 v = tags[k]
276 276 if self.map.get(v, SKIPREV) != SKIPREV:
277 277 ctags[k] = self.map[v]
278 278
279 279 if c and ctags:
280 280 nrev = self.dest.puttags(ctags)
281 281 # write another hash correspondence to override the previous
282 282 # one so we don't end up with extra tag heads
283 283 if nrev:
284 284 self.mapentry(c, nrev)
285 285
286 286 self.writeauthormap()
287 287 finally:
288 288 self.cleanup()
289 289
290 290 def cleanup(self):
291 291 try:
292 292 self.dest.after()
293 293 finally:
294 294 self.source.after()
295 295 if self.revmapfilefd:
296 296 self.revmapfilefd.close()
297 297
298 298 def convert(ui, src, dest=None, revmapfile=None, **opts):
299 299 """Convert a foreign SCM repository to a Mercurial one.
300 300
301 301 Accepted source formats:
302 302 - Mercurial
303 303 - CVS
304 304 - Darcs
305 305 - git
306 306 - Subversion
307 307
308 308 Accepted destination formats:
309 309 - Mercurial
310 310
311 311 If no revision is given, all revisions will be converted. Otherwise,
312 312 convert will only import up to the named revision (given in a format
313 313 understood by the source).
314 314
315 315 If no destination directory name is specified, it defaults to the
316 316 basename of the source with '-hg' appended. If the destination
317 317 repository doesn't exist, it will be created.
318 318
319 319 If <MAPFILE> isn't given, it will be put in a default location
320 320 (<dest>/.hg/shamap by default). The <MAPFILE> is a simple text
321 321 file that maps each source commit ID to the destination ID for
322 322 that revision, like so:
323 323 <source ID> <destination ID>
324 324
325 325 If the file doesn't exist, it's automatically created. It's updated
326 326 on each commit copied, so convert-repo can be interrupted and can
327 327 be run repeatedly to copy new commits.
328 328
329 329 The [username mapping] file is a simple text file that maps each source
330 330 commit author to a destination commit author. It is handy for source SCMs
331 331 that use unix logins to identify authors (eg: CVS). One line per author
332 332 mapping and the line format is:
333 333 srcauthor=whatever string you want
334 334
335 335 The filemap is a file that allows filtering and remapping of files
336 336 and directories. Comment lines start with '#'. Each line can
337 337 contain one of the following directives:
338 338
339 339 include path/to/file
340 340
341 341 exclude path/to/file
342 342
343 343 rename from/file to/file
344 344
345 345 The 'include' directive causes a file, or all files under a
346 346 directory, to be included in the destination repository, and the
347 exclussion of all other files and dirs not explicitely included.
347 exclusion of all other files and dirs not explicitely included.
348 348 The 'exclude' directive causes files or directories to be omitted.
349 349 The 'rename' directive renames a file or directory. To rename from a
350 350 subdirectory into the root of the repository, use '.' as the path to
351 351 rename to.
352 352 """
353 353
354 354 util._encoding = 'UTF-8'
355 355
356 356 if not dest:
357 357 dest = hg.defaultdest(src) + "-hg"
358 358 ui.status("assuming destination %s\n" % dest)
359 359
360 360 destc = convertsink(ui, dest, opts.get('dest_type'))
361 361
362 362 try:
363 363 srcc = convertsource(ui, src, opts.get('source_type'),
364 364 opts.get('rev'))
365 365 except Exception:
366 366 for path in destc.created:
367 367 shutil.rmtree(path, True)
368 368 raise
369 369
370 370 fmap = opts.get('filemap')
371 371 if fmap:
372 372 srcc = filemap.filemap_source(ui, srcc, fmap)
373 373 destc.setfilemapmode(True)
374 374
375 375 if not revmapfile:
376 376 try:
377 377 revmapfile = destc.revmapfile()
378 378 except:
379 379 revmapfile = os.path.join(destc, "map")
380 380
381 381 c = converter(ui, srcc, destc, revmapfile, opts)
382 382 c.convert()
383 383
384 384
385 385 cmdtable = {
386 386 "convert":
387 387 (convert,
388 388 [('A', 'authors', '', 'username mapping filename'),
389 389 ('d', 'dest-type', '', 'destination repository type'),
390 390 ('', 'filemap', '', 'remap file names using contents of file'),
391 391 ('r', 'rev', '', 'import up to target revision REV'),
392 392 ('s', 'source-type', '', 'source repository type'),
393 393 ('', 'datesort', None, 'try to sort changesets by date')],
394 394 'hg convert [OPTION]... SOURCE [DEST [MAPFILE]]'),
395 395 "debugsvnlog":
396 396 (debugsvnlog,
397 397 [],
398 398 'hg debugsvnlog'),
399 399 }
400 400
@@ -1,93 +1,95 b''
1 1 hg convert [OPTION]... SOURCE [DEST [MAPFILE]]
2 2
3 3 Convert a foreign SCM repository to a Mercurial one.
4 4
5 5 Accepted source formats:
6 - Mercurial
6 7 - CVS
7 8 - Darcs
8 9 - git
9 10 - Subversion
10 11
11 12 Accepted destination formats:
12 13 - Mercurial
13 14
14 15 If no revision is given, all revisions will be converted. Otherwise,
15 16 convert will only import up to the named revision (given in a format
16 17 understood by the source).
17 18
18 19 If no destination directory name is specified, it defaults to the
19 20 basename of the source with '-hg' appended. If the destination
20 21 repository doesn't exist, it will be created.
21 22
22 If <revmapfile> isn't given, it will be put in a default location
23 (<dest>/.hg/shamap by default). The <revmapfile> is a simple text
23 If <MAPFILE> isn't given, it will be put in a default location
24 (<dest>/.hg/shamap by default). The <MAPFILE> is a simple text
24 25 file that maps each source commit ID to the destination ID for
25 26 that revision, like so:
26 27 <source ID> <destination ID>
27 28
28 29 If the file doesn't exist, it's automatically created. It's updated
29 30 on each commit copied, so convert-repo can be interrupted and can
30 31 be run repeatedly to copy new commits.
31 32
32 33 The [username mapping] file is a simple text file that maps each source
33 34 commit author to a destination commit author. It is handy for source SCMs
34 35 that use unix logins to identify authors (eg: CVS). One line per author
35 36 mapping and the line format is:
36 37 srcauthor=whatever string you want
37 38
38 39 The filemap is a file that allows filtering and remapping of files
39 40 and directories. Comment lines start with '#'. Each line can
40 41 contain one of the following directives:
41 42
42 43 include path/to/file
43 44
44 45 exclude path/to/file
45 46
46 47 rename from/file to/file
47 48
48 49 The 'include' directive causes a file, or all files under a
49 directory, to be included in the destination repository. The
50 'exclude' directive causes files or directories to be omitted.
51 The 'rename' directive renames a file or directory. To rename
52 from a subdirectory into the root of the repository, use '.' as
53 the path to rename to.
50 directory, to be included in the destination repository, and the
51 exclusion of all other files and dirs not explicitely included.
52 The 'exclude' directive causes files or directories to be omitted.
53 The 'rename' directive renames a file or directory. To rename from a
54 subdirectory into the root of the repository, use '.' as the path to
55 rename to.
54 56
55 57 options:
56 58
57 59 -A --authors username mapping filename
58 60 -d --dest-type destination repository type
59 61 --filemap remap file names using contents of file
60 62 -r --rev import up to target revision REV
61 63 -s --source-type source repository type
62 64 --datesort try to sort changesets by date
63 65
64 66 use "hg -v help convert" to show global options
65 67 adding a
66 68 assuming destination a-hg
67 69 initializing destination a-hg repository
68 70 scanning source...
69 71 sorting...
70 72 converting...
71 73 4 a
72 74 3 b
73 75 2 c
74 76 1 d
75 77 0 e
76 78 pulling from ../a
77 79 searching for changes
78 80 no changes found
79 81 % should fail
80 82 initializing destination bogusfile repository
81 83 abort: cannot create new bundle repository
82 84 % should fail
83 85 abort: Permission denied: bogusdir
84 86 % should succeed
85 87 initializing destination bogusdir repository
86 88 scanning source...
87 89 sorting...
88 90 converting...
89 91 4 a
90 92 3 b
91 93 2 c
92 94 1 d
93 95 0 e
General Comments 0
You need to be logged in to leave comments. Login now