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