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