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