##// END OF EJS Templates
convert: add support for deterministic progress bar on scanning phase...
Augie Fackler -
r22411:c497e39d default
parent child Browse files
Show More
@@ -1,452 +1,459
1 # common.py - common code for the convert extension
1 # common.py - common code for the convert extension
2 #
2 #
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 import base64, errno, subprocess, os, datetime, re
8 import base64, errno, subprocess, os, datetime, re
9 import cPickle as pickle
9 import cPickle as pickle
10 from mercurial import util
10 from mercurial import util
11 from mercurial.i18n import _
11 from mercurial.i18n import _
12
12
13 propertycache = util.propertycache
13 propertycache = util.propertycache
14
14
15 def encodeargs(args):
15 def encodeargs(args):
16 def encodearg(s):
16 def encodearg(s):
17 lines = base64.encodestring(s)
17 lines = base64.encodestring(s)
18 lines = [l.splitlines()[0] for l in lines]
18 lines = [l.splitlines()[0] for l in lines]
19 return ''.join(lines)
19 return ''.join(lines)
20
20
21 s = pickle.dumps(args)
21 s = pickle.dumps(args)
22 return encodearg(s)
22 return encodearg(s)
23
23
24 def decodeargs(s):
24 def decodeargs(s):
25 s = base64.decodestring(s)
25 s = base64.decodestring(s)
26 return pickle.loads(s)
26 return pickle.loads(s)
27
27
28 class MissingTool(Exception):
28 class MissingTool(Exception):
29 pass
29 pass
30
30
31 def checktool(exe, name=None, abort=True):
31 def checktool(exe, name=None, abort=True):
32 name = name or exe
32 name = name or exe
33 if not util.findexe(exe):
33 if not util.findexe(exe):
34 exc = abort and util.Abort or MissingTool
34 exc = abort and util.Abort or MissingTool
35 raise exc(_('cannot find required "%s" tool') % name)
35 raise exc(_('cannot find required "%s" tool') % name)
36
36
37 class NoRepo(Exception):
37 class NoRepo(Exception):
38 pass
38 pass
39
39
40 SKIPREV = 'SKIP'
40 SKIPREV = 'SKIP'
41
41
42 class commit(object):
42 class commit(object):
43 def __init__(self, author, date, desc, parents, branch=None, rev=None,
43 def __init__(self, author, date, desc, parents, branch=None, rev=None,
44 extra={}, sortkey=None):
44 extra={}, sortkey=None):
45 self.author = author or 'unknown'
45 self.author = author or 'unknown'
46 self.date = date or '0 0'
46 self.date = date or '0 0'
47 self.desc = desc
47 self.desc = desc
48 self.parents = parents
48 self.parents = parents
49 self.branch = branch
49 self.branch = branch
50 self.rev = rev
50 self.rev = rev
51 self.extra = extra
51 self.extra = extra
52 self.sortkey = sortkey
52 self.sortkey = sortkey
53
53
54 class converter_source(object):
54 class converter_source(object):
55 """Conversion source interface"""
55 """Conversion source interface"""
56
56
57 def __init__(self, ui, path=None, rev=None):
57 def __init__(self, ui, path=None, rev=None):
58 """Initialize conversion source (or raise NoRepo("message")
58 """Initialize conversion source (or raise NoRepo("message")
59 exception if path is not a valid repository)"""
59 exception if path is not a valid repository)"""
60 self.ui = ui
60 self.ui = ui
61 self.path = path
61 self.path = path
62 self.rev = rev
62 self.rev = rev
63
63
64 self.encoding = 'utf-8'
64 self.encoding = 'utf-8'
65
65
66 def checkhexformat(self, revstr, mapname='splicemap'):
66 def checkhexformat(self, revstr, mapname='splicemap'):
67 """ fails if revstr is not a 40 byte hex. mercurial and git both uses
67 """ fails if revstr is not a 40 byte hex. mercurial and git both uses
68 such format for their revision numbering
68 such format for their revision numbering
69 """
69 """
70 if not re.match(r'[0-9a-fA-F]{40,40}$', revstr):
70 if not re.match(r'[0-9a-fA-F]{40,40}$', revstr):
71 raise util.Abort(_('%s entry %s is not a valid revision'
71 raise util.Abort(_('%s entry %s is not a valid revision'
72 ' identifier') % (mapname, revstr))
72 ' identifier') % (mapname, revstr))
73
73
74 def before(self):
74 def before(self):
75 pass
75 pass
76
76
77 def after(self):
77 def after(self):
78 pass
78 pass
79
79
80 def setrevmap(self, revmap):
80 def setrevmap(self, revmap):
81 """set the map of already-converted revisions"""
81 """set the map of already-converted revisions"""
82 pass
82 pass
83
83
84 def getheads(self):
84 def getheads(self):
85 """Return a list of this repository's heads"""
85 """Return a list of this repository's heads"""
86 raise NotImplementedError
86 raise NotImplementedError
87
87
88 def getfile(self, name, rev):
88 def getfile(self, name, rev):
89 """Return a pair (data, mode) where data is the file content
89 """Return a pair (data, mode) where data is the file content
90 as a string and mode one of '', 'x' or 'l'. rev is the
90 as a string and mode one of '', 'x' or 'l'. rev is the
91 identifier returned by a previous call to getchanges().
91 identifier returned by a previous call to getchanges().
92 Data is None if file is missing/deleted in rev.
92 Data is None if file is missing/deleted in rev.
93 """
93 """
94 raise NotImplementedError
94 raise NotImplementedError
95
95
96 def getchanges(self, version, full):
96 def getchanges(self, version, full):
97 """Returns a tuple of (files, copies).
97 """Returns a tuple of (files, copies).
98
98
99 files is a sorted list of (filename, id) tuples for all files
99 files is a sorted list of (filename, id) tuples for all files
100 changed between version and its first parent returned by
100 changed between version and its first parent returned by
101 getcommit(). If full, all files in that revision is returned.
101 getcommit(). If full, all files in that revision is returned.
102 id is the source revision id of the file.
102 id is the source revision id of the file.
103
103
104 copies is a dictionary of dest: source
104 copies is a dictionary of dest: source
105 """
105 """
106 raise NotImplementedError
106 raise NotImplementedError
107
107
108 def getcommit(self, version):
108 def getcommit(self, version):
109 """Return the commit object for version"""
109 """Return the commit object for version"""
110 raise NotImplementedError
110 raise NotImplementedError
111
111
112 def numcommits(self):
113 """Return the number of commits in this source.
114
115 If unknown, return None.
116 """
117 return None
118
112 def gettags(self):
119 def gettags(self):
113 """Return the tags as a dictionary of name: revision
120 """Return the tags as a dictionary of name: revision
114
121
115 Tag names must be UTF-8 strings.
122 Tag names must be UTF-8 strings.
116 """
123 """
117 raise NotImplementedError
124 raise NotImplementedError
118
125
119 def recode(self, s, encoding=None):
126 def recode(self, s, encoding=None):
120 if not encoding:
127 if not encoding:
121 encoding = self.encoding or 'utf-8'
128 encoding = self.encoding or 'utf-8'
122
129
123 if isinstance(s, unicode):
130 if isinstance(s, unicode):
124 return s.encode("utf-8")
131 return s.encode("utf-8")
125 try:
132 try:
126 return s.decode(encoding).encode("utf-8")
133 return s.decode(encoding).encode("utf-8")
127 except UnicodeError:
134 except UnicodeError:
128 try:
135 try:
129 return s.decode("latin-1").encode("utf-8")
136 return s.decode("latin-1").encode("utf-8")
130 except UnicodeError:
137 except UnicodeError:
131 return s.decode(encoding, "replace").encode("utf-8")
138 return s.decode(encoding, "replace").encode("utf-8")
132
139
133 def getchangedfiles(self, rev, i):
140 def getchangedfiles(self, rev, i):
134 """Return the files changed by rev compared to parent[i].
141 """Return the files changed by rev compared to parent[i].
135
142
136 i is an index selecting one of the parents of rev. The return
143 i is an index selecting one of the parents of rev. The return
137 value should be the list of files that are different in rev and
144 value should be the list of files that are different in rev and
138 this parent.
145 this parent.
139
146
140 If rev has no parents, i is None.
147 If rev has no parents, i is None.
141
148
142 This function is only needed to support --filemap
149 This function is only needed to support --filemap
143 """
150 """
144 raise NotImplementedError
151 raise NotImplementedError
145
152
146 def converted(self, rev, sinkrev):
153 def converted(self, rev, sinkrev):
147 '''Notify the source that a revision has been converted.'''
154 '''Notify the source that a revision has been converted.'''
148 pass
155 pass
149
156
150 def hasnativeorder(self):
157 def hasnativeorder(self):
151 """Return true if this source has a meaningful, native revision
158 """Return true if this source has a meaningful, native revision
152 order. For instance, Mercurial revisions are store sequentially
159 order. For instance, Mercurial revisions are store sequentially
153 while there is no such global ordering with Darcs.
160 while there is no such global ordering with Darcs.
154 """
161 """
155 return False
162 return False
156
163
157 def hasnativeclose(self):
164 def hasnativeclose(self):
158 """Return true if this source has ability to close branch.
165 """Return true if this source has ability to close branch.
159 """
166 """
160 return False
167 return False
161
168
162 def lookuprev(self, rev):
169 def lookuprev(self, rev):
163 """If rev is a meaningful revision reference in source, return
170 """If rev is a meaningful revision reference in source, return
164 the referenced identifier in the same format used by getcommit().
171 the referenced identifier in the same format used by getcommit().
165 return None otherwise.
172 return None otherwise.
166 """
173 """
167 return None
174 return None
168
175
169 def getbookmarks(self):
176 def getbookmarks(self):
170 """Return the bookmarks as a dictionary of name: revision
177 """Return the bookmarks as a dictionary of name: revision
171
178
172 Bookmark names are to be UTF-8 strings.
179 Bookmark names are to be UTF-8 strings.
173 """
180 """
174 return {}
181 return {}
175
182
176 def checkrevformat(self, revstr, mapname='splicemap'):
183 def checkrevformat(self, revstr, mapname='splicemap'):
177 """revstr is a string that describes a revision in the given
184 """revstr is a string that describes a revision in the given
178 source control system. Return true if revstr has correct
185 source control system. Return true if revstr has correct
179 format.
186 format.
180 """
187 """
181 return True
188 return True
182
189
183 class converter_sink(object):
190 class converter_sink(object):
184 """Conversion sink (target) interface"""
191 """Conversion sink (target) interface"""
185
192
186 def __init__(self, ui, path):
193 def __init__(self, ui, path):
187 """Initialize conversion sink (or raise NoRepo("message")
194 """Initialize conversion sink (or raise NoRepo("message")
188 exception if path is not a valid repository)
195 exception if path is not a valid repository)
189
196
190 created is a list of paths to remove if a fatal error occurs
197 created is a list of paths to remove if a fatal error occurs
191 later"""
198 later"""
192 self.ui = ui
199 self.ui = ui
193 self.path = path
200 self.path = path
194 self.created = []
201 self.created = []
195
202
196 def revmapfile(self):
203 def revmapfile(self):
197 """Path to a file that will contain lines
204 """Path to a file that will contain lines
198 source_rev_id sink_rev_id
205 source_rev_id sink_rev_id
199 mapping equivalent revision identifiers for each system."""
206 mapping equivalent revision identifiers for each system."""
200 raise NotImplementedError
207 raise NotImplementedError
201
208
202 def authorfile(self):
209 def authorfile(self):
203 """Path to a file that will contain lines
210 """Path to a file that will contain lines
204 srcauthor=dstauthor
211 srcauthor=dstauthor
205 mapping equivalent authors identifiers for each system."""
212 mapping equivalent authors identifiers for each system."""
206 return None
213 return None
207
214
208 def putcommit(self, files, copies, parents, commit, source, revmap, full):
215 def putcommit(self, files, copies, parents, commit, source, revmap, full):
209 """Create a revision with all changed files listed in 'files'
216 """Create a revision with all changed files listed in 'files'
210 and having listed parents. 'commit' is a commit object
217 and having listed parents. 'commit' is a commit object
211 containing at a minimum the author, date, and message for this
218 containing at a minimum the author, date, and message for this
212 changeset. 'files' is a list of (path, version) tuples,
219 changeset. 'files' is a list of (path, version) tuples,
213 'copies' is a dictionary mapping destinations to sources,
220 'copies' is a dictionary mapping destinations to sources,
214 'source' is the source repository, and 'revmap' is a mapfile
221 'source' is the source repository, and 'revmap' is a mapfile
215 of source revisions to converted revisions. Only getfile() and
222 of source revisions to converted revisions. Only getfile() and
216 lookuprev() should be called on 'source'. 'full' means that 'files'
223 lookuprev() should be called on 'source'. 'full' means that 'files'
217 is complete and all other files should be removed.
224 is complete and all other files should be removed.
218
225
219 Note that the sink repository is not told to update itself to
226 Note that the sink repository is not told to update itself to
220 a particular revision (or even what that revision would be)
227 a particular revision (or even what that revision would be)
221 before it receives the file data.
228 before it receives the file data.
222 """
229 """
223 raise NotImplementedError
230 raise NotImplementedError
224
231
225 def puttags(self, tags):
232 def puttags(self, tags):
226 """Put tags into sink.
233 """Put tags into sink.
227
234
228 tags: {tagname: sink_rev_id, ...} where tagname is an UTF-8 string.
235 tags: {tagname: sink_rev_id, ...} where tagname is an UTF-8 string.
229 Return a pair (tag_revision, tag_parent_revision), or (None, None)
236 Return a pair (tag_revision, tag_parent_revision), or (None, None)
230 if nothing was changed.
237 if nothing was changed.
231 """
238 """
232 raise NotImplementedError
239 raise NotImplementedError
233
240
234 def setbranch(self, branch, pbranches):
241 def setbranch(self, branch, pbranches):
235 """Set the current branch name. Called before the first putcommit
242 """Set the current branch name. Called before the first putcommit
236 on the branch.
243 on the branch.
237 branch: branch name for subsequent commits
244 branch: branch name for subsequent commits
238 pbranches: (converted parent revision, parent branch) tuples"""
245 pbranches: (converted parent revision, parent branch) tuples"""
239 pass
246 pass
240
247
241 def setfilemapmode(self, active):
248 def setfilemapmode(self, active):
242 """Tell the destination that we're using a filemap
249 """Tell the destination that we're using a filemap
243
250
244 Some converter_sources (svn in particular) can claim that a file
251 Some converter_sources (svn in particular) can claim that a file
245 was changed in a revision, even if there was no change. This method
252 was changed in a revision, even if there was no change. This method
246 tells the destination that we're using a filemap and that it should
253 tells the destination that we're using a filemap and that it should
247 filter empty revisions.
254 filter empty revisions.
248 """
255 """
249 pass
256 pass
250
257
251 def before(self):
258 def before(self):
252 pass
259 pass
253
260
254 def after(self):
261 def after(self):
255 pass
262 pass
256
263
257 def putbookmarks(self, bookmarks):
264 def putbookmarks(self, bookmarks):
258 """Put bookmarks into sink.
265 """Put bookmarks into sink.
259
266
260 bookmarks: {bookmarkname: sink_rev_id, ...}
267 bookmarks: {bookmarkname: sink_rev_id, ...}
261 where bookmarkname is an UTF-8 string.
268 where bookmarkname is an UTF-8 string.
262 """
269 """
263 pass
270 pass
264
271
265 def hascommitfrommap(self, rev):
272 def hascommitfrommap(self, rev):
266 """Return False if a rev mentioned in a filemap is known to not be
273 """Return False if a rev mentioned in a filemap is known to not be
267 present."""
274 present."""
268 raise NotImplementedError
275 raise NotImplementedError
269
276
270 def hascommitforsplicemap(self, rev):
277 def hascommitforsplicemap(self, rev):
271 """This method is for the special needs for splicemap handling and not
278 """This method is for the special needs for splicemap handling and not
272 for general use. Returns True if the sink contains rev, aborts on some
279 for general use. Returns True if the sink contains rev, aborts on some
273 special cases."""
280 special cases."""
274 raise NotImplementedError
281 raise NotImplementedError
275
282
276 class commandline(object):
283 class commandline(object):
277 def __init__(self, ui, command):
284 def __init__(self, ui, command):
278 self.ui = ui
285 self.ui = ui
279 self.command = command
286 self.command = command
280
287
281 def prerun(self):
288 def prerun(self):
282 pass
289 pass
283
290
284 def postrun(self):
291 def postrun(self):
285 pass
292 pass
286
293
287 def _cmdline(self, cmd, *args, **kwargs):
294 def _cmdline(self, cmd, *args, **kwargs):
288 cmdline = [self.command, cmd] + list(args)
295 cmdline = [self.command, cmd] + list(args)
289 for k, v in kwargs.iteritems():
296 for k, v in kwargs.iteritems():
290 if len(k) == 1:
297 if len(k) == 1:
291 cmdline.append('-' + k)
298 cmdline.append('-' + k)
292 else:
299 else:
293 cmdline.append('--' + k.replace('_', '-'))
300 cmdline.append('--' + k.replace('_', '-'))
294 try:
301 try:
295 if len(k) == 1:
302 if len(k) == 1:
296 cmdline.append('' + v)
303 cmdline.append('' + v)
297 else:
304 else:
298 cmdline[-1] += '=' + v
305 cmdline[-1] += '=' + v
299 except TypeError:
306 except TypeError:
300 pass
307 pass
301 cmdline = [util.shellquote(arg) for arg in cmdline]
308 cmdline = [util.shellquote(arg) for arg in cmdline]
302 if not self.ui.debugflag:
309 if not self.ui.debugflag:
303 cmdline += ['2>', os.devnull]
310 cmdline += ['2>', os.devnull]
304 cmdline = ' '.join(cmdline)
311 cmdline = ' '.join(cmdline)
305 return cmdline
312 return cmdline
306
313
307 def _run(self, cmd, *args, **kwargs):
314 def _run(self, cmd, *args, **kwargs):
308 def popen(cmdline):
315 def popen(cmdline):
309 p = subprocess.Popen(cmdline, shell=True, bufsize=-1,
316 p = subprocess.Popen(cmdline, shell=True, bufsize=-1,
310 close_fds=util.closefds,
317 close_fds=util.closefds,
311 stdout=subprocess.PIPE)
318 stdout=subprocess.PIPE)
312 return p
319 return p
313 return self._dorun(popen, cmd, *args, **kwargs)
320 return self._dorun(popen, cmd, *args, **kwargs)
314
321
315 def _run2(self, cmd, *args, **kwargs):
322 def _run2(self, cmd, *args, **kwargs):
316 return self._dorun(util.popen2, cmd, *args, **kwargs)
323 return self._dorun(util.popen2, cmd, *args, **kwargs)
317
324
318 def _dorun(self, openfunc, cmd, *args, **kwargs):
325 def _dorun(self, openfunc, cmd, *args, **kwargs):
319 cmdline = self._cmdline(cmd, *args, **kwargs)
326 cmdline = self._cmdline(cmd, *args, **kwargs)
320 self.ui.debug('running: %s\n' % (cmdline,))
327 self.ui.debug('running: %s\n' % (cmdline,))
321 self.prerun()
328 self.prerun()
322 try:
329 try:
323 return openfunc(cmdline)
330 return openfunc(cmdline)
324 finally:
331 finally:
325 self.postrun()
332 self.postrun()
326
333
327 def run(self, cmd, *args, **kwargs):
334 def run(self, cmd, *args, **kwargs):
328 p = self._run(cmd, *args, **kwargs)
335 p = self._run(cmd, *args, **kwargs)
329 output = p.communicate()[0]
336 output = p.communicate()[0]
330 self.ui.debug(output)
337 self.ui.debug(output)
331 return output, p.returncode
338 return output, p.returncode
332
339
333 def runlines(self, cmd, *args, **kwargs):
340 def runlines(self, cmd, *args, **kwargs):
334 p = self._run(cmd, *args, **kwargs)
341 p = self._run(cmd, *args, **kwargs)
335 output = p.stdout.readlines()
342 output = p.stdout.readlines()
336 p.wait()
343 p.wait()
337 self.ui.debug(''.join(output))
344 self.ui.debug(''.join(output))
338 return output, p.returncode
345 return output, p.returncode
339
346
340 def checkexit(self, status, output=''):
347 def checkexit(self, status, output=''):
341 if status:
348 if status:
342 if output:
349 if output:
343 self.ui.warn(_('%s error:\n') % self.command)
350 self.ui.warn(_('%s error:\n') % self.command)
344 self.ui.warn(output)
351 self.ui.warn(output)
345 msg = util.explainexit(status)[0]
352 msg = util.explainexit(status)[0]
346 raise util.Abort('%s %s' % (self.command, msg))
353 raise util.Abort('%s %s' % (self.command, msg))
347
354
348 def run0(self, cmd, *args, **kwargs):
355 def run0(self, cmd, *args, **kwargs):
349 output, status = self.run(cmd, *args, **kwargs)
356 output, status = self.run(cmd, *args, **kwargs)
350 self.checkexit(status, output)
357 self.checkexit(status, output)
351 return output
358 return output
352
359
353 def runlines0(self, cmd, *args, **kwargs):
360 def runlines0(self, cmd, *args, **kwargs):
354 output, status = self.runlines(cmd, *args, **kwargs)
361 output, status = self.runlines(cmd, *args, **kwargs)
355 self.checkexit(status, ''.join(output))
362 self.checkexit(status, ''.join(output))
356 return output
363 return output
357
364
358 @propertycache
365 @propertycache
359 def argmax(self):
366 def argmax(self):
360 # POSIX requires at least 4096 bytes for ARG_MAX
367 # POSIX requires at least 4096 bytes for ARG_MAX
361 argmax = 4096
368 argmax = 4096
362 try:
369 try:
363 argmax = os.sysconf("SC_ARG_MAX")
370 argmax = os.sysconf("SC_ARG_MAX")
364 except (AttributeError, ValueError):
371 except (AttributeError, ValueError):
365 pass
372 pass
366
373
367 # Windows shells impose their own limits on command line length,
374 # Windows shells impose their own limits on command line length,
368 # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
375 # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
369 # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
376 # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
370 # details about cmd.exe limitations.
377 # details about cmd.exe limitations.
371
378
372 # Since ARG_MAX is for command line _and_ environment, lower our limit
379 # Since ARG_MAX is for command line _and_ environment, lower our limit
373 # (and make happy Windows shells while doing this).
380 # (and make happy Windows shells while doing this).
374 return argmax // 2 - 1
381 return argmax // 2 - 1
375
382
376 def _limit_arglist(self, arglist, cmd, *args, **kwargs):
383 def _limit_arglist(self, arglist, cmd, *args, **kwargs):
377 cmdlen = len(self._cmdline(cmd, *args, **kwargs))
384 cmdlen = len(self._cmdline(cmd, *args, **kwargs))
378 limit = self.argmax - cmdlen
385 limit = self.argmax - cmdlen
379 bytes = 0
386 bytes = 0
380 fl = []
387 fl = []
381 for fn in arglist:
388 for fn in arglist:
382 b = len(fn) + 3
389 b = len(fn) + 3
383 if bytes + b < limit or len(fl) == 0:
390 if bytes + b < limit or len(fl) == 0:
384 fl.append(fn)
391 fl.append(fn)
385 bytes += b
392 bytes += b
386 else:
393 else:
387 yield fl
394 yield fl
388 fl = [fn]
395 fl = [fn]
389 bytes = b
396 bytes = b
390 if fl:
397 if fl:
391 yield fl
398 yield fl
392
399
393 def xargs(self, arglist, cmd, *args, **kwargs):
400 def xargs(self, arglist, cmd, *args, **kwargs):
394 for l in self._limit_arglist(arglist, cmd, *args, **kwargs):
401 for l in self._limit_arglist(arglist, cmd, *args, **kwargs):
395 self.run0(cmd, *(list(args) + l), **kwargs)
402 self.run0(cmd, *(list(args) + l), **kwargs)
396
403
397 class mapfile(dict):
404 class mapfile(dict):
398 def __init__(self, ui, path):
405 def __init__(self, ui, path):
399 super(mapfile, self).__init__()
406 super(mapfile, self).__init__()
400 self.ui = ui
407 self.ui = ui
401 self.path = path
408 self.path = path
402 self.fp = None
409 self.fp = None
403 self.order = []
410 self.order = []
404 self._read()
411 self._read()
405
412
406 def _read(self):
413 def _read(self):
407 if not self.path:
414 if not self.path:
408 return
415 return
409 try:
416 try:
410 fp = open(self.path, 'r')
417 fp = open(self.path, 'r')
411 except IOError, err:
418 except IOError, err:
412 if err.errno != errno.ENOENT:
419 if err.errno != errno.ENOENT:
413 raise
420 raise
414 return
421 return
415 for i, line in enumerate(fp):
422 for i, line in enumerate(fp):
416 line = line.splitlines()[0].rstrip()
423 line = line.splitlines()[0].rstrip()
417 if not line:
424 if not line:
418 # Ignore blank lines
425 # Ignore blank lines
419 continue
426 continue
420 try:
427 try:
421 key, value = line.rsplit(' ', 1)
428 key, value = line.rsplit(' ', 1)
422 except ValueError:
429 except ValueError:
423 raise util.Abort(
430 raise util.Abort(
424 _('syntax error in %s(%d): key/value pair expected')
431 _('syntax error in %s(%d): key/value pair expected')
425 % (self.path, i + 1))
432 % (self.path, i + 1))
426 if key not in self:
433 if key not in self:
427 self.order.append(key)
434 self.order.append(key)
428 super(mapfile, self).__setitem__(key, value)
435 super(mapfile, self).__setitem__(key, value)
429 fp.close()
436 fp.close()
430
437
431 def __setitem__(self, key, value):
438 def __setitem__(self, key, value):
432 if self.fp is None:
439 if self.fp is None:
433 try:
440 try:
434 self.fp = open(self.path, 'a')
441 self.fp = open(self.path, 'a')
435 except IOError, err:
442 except IOError, err:
436 raise util.Abort(_('could not open map file %r: %s') %
443 raise util.Abort(_('could not open map file %r: %s') %
437 (self.path, err.strerror))
444 (self.path, err.strerror))
438 self.fp.write('%s %s\n' % (key, value))
445 self.fp.write('%s %s\n' % (key, value))
439 self.fp.flush()
446 self.fp.flush()
440 super(mapfile, self).__setitem__(key, value)
447 super(mapfile, self).__setitem__(key, value)
441
448
442 def close(self):
449 def close(self):
443 if self.fp:
450 if self.fp:
444 self.fp.close()
451 self.fp.close()
445 self.fp = None
452 self.fp = None
446
453
447 def makedatetimestamp(t):
454 def makedatetimestamp(t):
448 """Like util.makedate() but for time t instead of current time"""
455 """Like util.makedate() but for time t instead of current time"""
449 delta = (datetime.datetime.utcfromtimestamp(t) -
456 delta = (datetime.datetime.utcfromtimestamp(t) -
450 datetime.datetime.fromtimestamp(t))
457 datetime.datetime.fromtimestamp(t))
451 tz = delta.days * 86400 + delta.seconds
458 tz = delta.days * 86400 + delta.seconds
452 return t, tz
459 return t, tz
@@ -1,532 +1,534
1 # convcmd - convert extension commands definition
1 # convcmd - convert extension commands definition
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 of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from common import NoRepo, MissingTool, SKIPREV, mapfile
8 from common import NoRepo, MissingTool, SKIPREV, mapfile
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 svn_source, svn_sink
13 from subversion import svn_source, svn_sink
14 from monotone import monotone_source
14 from monotone import monotone_source
15 from gnuarch import gnuarch_source
15 from gnuarch import gnuarch_source
16 from bzr import bzr_source
16 from bzr import bzr_source
17 from p4 import p4_source
17 from p4 import p4_source
18 import filemap
18 import filemap
19
19
20 import os, shutil, shlex
20 import os, shutil, shlex
21 from mercurial import hg, util, encoding
21 from mercurial import hg, util, encoding
22 from mercurial.i18n import _
22 from mercurial.i18n import _
23
23
24 orig_encoding = 'ascii'
24 orig_encoding = 'ascii'
25
25
26 def recode(s):
26 def recode(s):
27 if isinstance(s, unicode):
27 if isinstance(s, unicode):
28 return s.encode(orig_encoding, 'replace')
28 return s.encode(orig_encoding, 'replace')
29 else:
29 else:
30 return s.decode('utf-8').encode(orig_encoding, 'replace')
30 return s.decode('utf-8').encode(orig_encoding, 'replace')
31
31
32 source_converters = [
32 source_converters = [
33 ('cvs', convert_cvs, 'branchsort'),
33 ('cvs', convert_cvs, 'branchsort'),
34 ('git', convert_git, 'branchsort'),
34 ('git', convert_git, 'branchsort'),
35 ('svn', svn_source, 'branchsort'),
35 ('svn', svn_source, 'branchsort'),
36 ('hg', mercurial_source, 'sourcesort'),
36 ('hg', mercurial_source, 'sourcesort'),
37 ('darcs', darcs_source, 'branchsort'),
37 ('darcs', darcs_source, 'branchsort'),
38 ('mtn', monotone_source, 'branchsort'),
38 ('mtn', monotone_source, 'branchsort'),
39 ('gnuarch', gnuarch_source, 'branchsort'),
39 ('gnuarch', gnuarch_source, 'branchsort'),
40 ('bzr', bzr_source, 'branchsort'),
40 ('bzr', bzr_source, 'branchsort'),
41 ('p4', p4_source, 'branchsort'),
41 ('p4', p4_source, 'branchsort'),
42 ]
42 ]
43
43
44 sink_converters = [
44 sink_converters = [
45 ('hg', mercurial_sink),
45 ('hg', mercurial_sink),
46 ('svn', svn_sink),
46 ('svn', svn_sink),
47 ]
47 ]
48
48
49 def convertsource(ui, path, type, rev):
49 def convertsource(ui, path, type, rev):
50 exceptions = []
50 exceptions = []
51 if type and type not in [s[0] for s in source_converters]:
51 if type and type not in [s[0] for s in source_converters]:
52 raise util.Abort(_('%s: invalid source repository type') % type)
52 raise util.Abort(_('%s: invalid source repository type') % type)
53 for name, source, sortmode in source_converters:
53 for name, source, sortmode in source_converters:
54 try:
54 try:
55 if not type or name == type:
55 if not type or name == type:
56 return source(ui, path, rev), sortmode
56 return source(ui, path, rev), sortmode
57 except (NoRepo, MissingTool), inst:
57 except (NoRepo, MissingTool), inst:
58 exceptions.append(inst)
58 exceptions.append(inst)
59 if not ui.quiet:
59 if not ui.quiet:
60 for inst in exceptions:
60 for inst in exceptions:
61 ui.write("%s\n" % inst)
61 ui.write("%s\n" % inst)
62 raise util.Abort(_('%s: missing or unsupported repository') % path)
62 raise util.Abort(_('%s: missing or unsupported repository') % path)
63
63
64 def convertsink(ui, path, type):
64 def convertsink(ui, path, type):
65 if type and type not in [s[0] for s in sink_converters]:
65 if type and type not in [s[0] for s in sink_converters]:
66 raise util.Abort(_('%s: invalid destination repository type') % type)
66 raise util.Abort(_('%s: invalid destination repository type') % type)
67 for name, sink in sink_converters:
67 for name, sink in sink_converters:
68 try:
68 try:
69 if not type or name == type:
69 if not type or name == type:
70 return sink(ui, path)
70 return sink(ui, path)
71 except NoRepo, inst:
71 except NoRepo, inst:
72 ui.note(_("convert: %s\n") % inst)
72 ui.note(_("convert: %s\n") % inst)
73 except MissingTool, inst:
73 except MissingTool, inst:
74 raise util.Abort('%s\n' % inst)
74 raise util.Abort('%s\n' % inst)
75 raise util.Abort(_('%s: unknown repository type') % path)
75 raise util.Abort(_('%s: unknown repository type') % path)
76
76
77 class progresssource(object):
77 class progresssource(object):
78 def __init__(self, ui, source, filecount):
78 def __init__(self, ui, source, filecount):
79 self.ui = ui
79 self.ui = ui
80 self.source = source
80 self.source = source
81 self.filecount = filecount
81 self.filecount = filecount
82 self.retrieved = 0
82 self.retrieved = 0
83
83
84 def getfile(self, file, rev):
84 def getfile(self, file, rev):
85 self.retrieved += 1
85 self.retrieved += 1
86 self.ui.progress(_('getting files'), self.retrieved,
86 self.ui.progress(_('getting files'), self.retrieved,
87 item=file, total=self.filecount)
87 item=file, total=self.filecount)
88 return self.source.getfile(file, rev)
88 return self.source.getfile(file, rev)
89
89
90 def lookuprev(self, rev):
90 def lookuprev(self, rev):
91 return self.source.lookuprev(rev)
91 return self.source.lookuprev(rev)
92
92
93 def close(self):
93 def close(self):
94 self.ui.progress(_('getting files'), None)
94 self.ui.progress(_('getting files'), None)
95
95
96 class converter(object):
96 class converter(object):
97 def __init__(self, ui, source, dest, revmapfile, opts):
97 def __init__(self, ui, source, dest, revmapfile, opts):
98
98
99 self.source = source
99 self.source = source
100 self.dest = dest
100 self.dest = dest
101 self.ui = ui
101 self.ui = ui
102 self.opts = opts
102 self.opts = opts
103 self.commitcache = {}
103 self.commitcache = {}
104 self.authors = {}
104 self.authors = {}
105 self.authorfile = None
105 self.authorfile = None
106
106
107 # Record converted revisions persistently: maps source revision
107 # Record converted revisions persistently: maps source revision
108 # ID to target revision ID (both strings). (This is how
108 # ID to target revision ID (both strings). (This is how
109 # incremental conversions work.)
109 # incremental conversions work.)
110 self.map = mapfile(ui, revmapfile)
110 self.map = mapfile(ui, revmapfile)
111
111
112 # Read first the dst author map if any
112 # Read first the dst author map if any
113 authorfile = self.dest.authorfile()
113 authorfile = self.dest.authorfile()
114 if authorfile and os.path.exists(authorfile):
114 if authorfile and os.path.exists(authorfile):
115 self.readauthormap(authorfile)
115 self.readauthormap(authorfile)
116 # Extend/Override with new author map if necessary
116 # Extend/Override with new author map if necessary
117 if opts.get('authormap'):
117 if opts.get('authormap'):
118 self.readauthormap(opts.get('authormap'))
118 self.readauthormap(opts.get('authormap'))
119 self.authorfile = self.dest.authorfile()
119 self.authorfile = self.dest.authorfile()
120
120
121 self.splicemap = self.parsesplicemap(opts.get('splicemap'))
121 self.splicemap = self.parsesplicemap(opts.get('splicemap'))
122 self.branchmap = mapfile(ui, opts.get('branchmap'))
122 self.branchmap = mapfile(ui, opts.get('branchmap'))
123
123
124 def parsesplicemap(self, path):
124 def parsesplicemap(self, path):
125 """ check and validate the splicemap format and
125 """ check and validate the splicemap format and
126 return a child/parents dictionary.
126 return a child/parents dictionary.
127 Format checking has two parts.
127 Format checking has two parts.
128 1. generic format which is same across all source types
128 1. generic format which is same across all source types
129 2. specific format checking which may be different for
129 2. specific format checking which may be different for
130 different source type. This logic is implemented in
130 different source type. This logic is implemented in
131 checkrevformat function in source files like
131 checkrevformat function in source files like
132 hg.py, subversion.py etc.
132 hg.py, subversion.py etc.
133 """
133 """
134
134
135 if not path:
135 if not path:
136 return {}
136 return {}
137 m = {}
137 m = {}
138 try:
138 try:
139 fp = open(path, 'r')
139 fp = open(path, 'r')
140 for i, line in enumerate(fp):
140 for i, line in enumerate(fp):
141 line = line.splitlines()[0].rstrip()
141 line = line.splitlines()[0].rstrip()
142 if not line:
142 if not line:
143 # Ignore blank lines
143 # Ignore blank lines
144 continue
144 continue
145 # split line
145 # split line
146 lex = shlex.shlex(line, posix=True)
146 lex = shlex.shlex(line, posix=True)
147 lex.whitespace_split = True
147 lex.whitespace_split = True
148 lex.whitespace += ','
148 lex.whitespace += ','
149 line = list(lex)
149 line = list(lex)
150 # check number of parents
150 # check number of parents
151 if not (2 <= len(line) <= 3):
151 if not (2 <= len(line) <= 3):
152 raise util.Abort(_('syntax error in %s(%d): child parent1'
152 raise util.Abort(_('syntax error in %s(%d): child parent1'
153 '[,parent2] expected') % (path, i + 1))
153 '[,parent2] expected') % (path, i + 1))
154 for part in line:
154 for part in line:
155 self.source.checkrevformat(part)
155 self.source.checkrevformat(part)
156 child, p1, p2 = line[0], line[1:2], line[2:]
156 child, p1, p2 = line[0], line[1:2], line[2:]
157 if p1 == p2:
157 if p1 == p2:
158 m[child] = p1
158 m[child] = p1
159 else:
159 else:
160 m[child] = p1 + p2
160 m[child] = p1 + p2
161 # if file does not exist or error reading, exit
161 # if file does not exist or error reading, exit
162 except IOError:
162 except IOError:
163 raise util.Abort(_('splicemap file not found or error reading %s:')
163 raise util.Abort(_('splicemap file not found or error reading %s:')
164 % path)
164 % path)
165 return m
165 return m
166
166
167
167
168 def walktree(self, heads):
168 def walktree(self, heads):
169 '''Return a mapping that identifies the uncommitted parents of every
169 '''Return a mapping that identifies the uncommitted parents of every
170 uncommitted changeset.'''
170 uncommitted changeset.'''
171 visit = heads
171 visit = heads
172 known = set()
172 known = set()
173 parents = {}
173 parents = {}
174 numcommits = self.source.numcommits()
174 while visit:
175 while visit:
175 n = visit.pop(0)
176 n = visit.pop(0)
176 if n in known:
177 if n in known:
177 continue
178 continue
178 if n in self.map:
179 if n in self.map:
179 m = self.map[n]
180 m = self.map[n]
180 if m == SKIPREV or self.dest.hascommitfrommap(m):
181 if m == SKIPREV or self.dest.hascommitfrommap(m):
181 continue
182 continue
182 known.add(n)
183 known.add(n)
183 self.ui.progress(_('scanning'), len(known), unit=_('revisions'))
184 self.ui.progress(_('scanning'), len(known), unit=_('revisions'),
185 total=numcommits)
184 commit = self.cachecommit(n)
186 commit = self.cachecommit(n)
185 parents[n] = []
187 parents[n] = []
186 for p in commit.parents:
188 for p in commit.parents:
187 parents[n].append(p)
189 parents[n].append(p)
188 visit.append(p)
190 visit.append(p)
189 self.ui.progress(_('scanning'), None)
191 self.ui.progress(_('scanning'), None)
190
192
191 return parents
193 return parents
192
194
193 def mergesplicemap(self, parents, splicemap):
195 def mergesplicemap(self, parents, splicemap):
194 """A splicemap redefines child/parent relationships. Check the
196 """A splicemap redefines child/parent relationships. Check the
195 map contains valid revision identifiers and merge the new
197 map contains valid revision identifiers and merge the new
196 links in the source graph.
198 links in the source graph.
197 """
199 """
198 for c in sorted(splicemap):
200 for c in sorted(splicemap):
199 if c not in parents:
201 if c not in parents:
200 if not self.dest.hascommitforsplicemap(self.map.get(c, c)):
202 if not self.dest.hascommitforsplicemap(self.map.get(c, c)):
201 # Could be in source but not converted during this run
203 # Could be in source but not converted during this run
202 self.ui.warn(_('splice map revision %s is not being '
204 self.ui.warn(_('splice map revision %s is not being '
203 'converted, ignoring\n') % c)
205 'converted, ignoring\n') % c)
204 continue
206 continue
205 pc = []
207 pc = []
206 for p in splicemap[c]:
208 for p in splicemap[c]:
207 # We do not have to wait for nodes already in dest.
209 # We do not have to wait for nodes already in dest.
208 if self.dest.hascommitforsplicemap(self.map.get(p, p)):
210 if self.dest.hascommitforsplicemap(self.map.get(p, p)):
209 continue
211 continue
210 # Parent is not in dest and not being converted, not good
212 # Parent is not in dest and not being converted, not good
211 if p not in parents:
213 if p not in parents:
212 raise util.Abort(_('unknown splice map parent: %s') % p)
214 raise util.Abort(_('unknown splice map parent: %s') % p)
213 pc.append(p)
215 pc.append(p)
214 parents[c] = pc
216 parents[c] = pc
215
217
216 def toposort(self, parents, sortmode):
218 def toposort(self, parents, sortmode):
217 '''Return an ordering such that every uncommitted changeset is
219 '''Return an ordering such that every uncommitted changeset is
218 preceded by all its uncommitted ancestors.'''
220 preceded by all its uncommitted ancestors.'''
219
221
220 def mapchildren(parents):
222 def mapchildren(parents):
221 """Return a (children, roots) tuple where 'children' maps parent
223 """Return a (children, roots) tuple where 'children' maps parent
222 revision identifiers to children ones, and 'roots' is the list of
224 revision identifiers to children ones, and 'roots' is the list of
223 revisions without parents. 'parents' must be a mapping of revision
225 revisions without parents. 'parents' must be a mapping of revision
224 identifier to its parents ones.
226 identifier to its parents ones.
225 """
227 """
226 visit = sorted(parents)
228 visit = sorted(parents)
227 seen = set()
229 seen = set()
228 children = {}
230 children = {}
229 roots = []
231 roots = []
230
232
231 while visit:
233 while visit:
232 n = visit.pop(0)
234 n = visit.pop(0)
233 if n in seen:
235 if n in seen:
234 continue
236 continue
235 seen.add(n)
237 seen.add(n)
236 # Ensure that nodes without parents are present in the
238 # Ensure that nodes without parents are present in the
237 # 'children' mapping.
239 # 'children' mapping.
238 children.setdefault(n, [])
240 children.setdefault(n, [])
239 hasparent = False
241 hasparent = False
240 for p in parents[n]:
242 for p in parents[n]:
241 if p not in self.map:
243 if p not in self.map:
242 visit.append(p)
244 visit.append(p)
243 hasparent = True
245 hasparent = True
244 children.setdefault(p, []).append(n)
246 children.setdefault(p, []).append(n)
245 if not hasparent:
247 if not hasparent:
246 roots.append(n)
248 roots.append(n)
247
249
248 return children, roots
250 return children, roots
249
251
250 # Sort functions are supposed to take a list of revisions which
252 # Sort functions are supposed to take a list of revisions which
251 # can be converted immediately and pick one
253 # can be converted immediately and pick one
252
254
253 def makebranchsorter():
255 def makebranchsorter():
254 """If the previously converted revision has a child in the
256 """If the previously converted revision has a child in the
255 eligible revisions list, pick it. Return the list head
257 eligible revisions list, pick it. Return the list head
256 otherwise. Branch sort attempts to minimize branch
258 otherwise. Branch sort attempts to minimize branch
257 switching, which is harmful for Mercurial backend
259 switching, which is harmful for Mercurial backend
258 compression.
260 compression.
259 """
261 """
260 prev = [None]
262 prev = [None]
261 def picknext(nodes):
263 def picknext(nodes):
262 next = nodes[0]
264 next = nodes[0]
263 for n in nodes:
265 for n in nodes:
264 if prev[0] in parents[n]:
266 if prev[0] in parents[n]:
265 next = n
267 next = n
266 break
268 break
267 prev[0] = next
269 prev[0] = next
268 return next
270 return next
269 return picknext
271 return picknext
270
272
271 def makesourcesorter():
273 def makesourcesorter():
272 """Source specific sort."""
274 """Source specific sort."""
273 keyfn = lambda n: self.commitcache[n].sortkey
275 keyfn = lambda n: self.commitcache[n].sortkey
274 def picknext(nodes):
276 def picknext(nodes):
275 return sorted(nodes, key=keyfn)[0]
277 return sorted(nodes, key=keyfn)[0]
276 return picknext
278 return picknext
277
279
278 def makeclosesorter():
280 def makeclosesorter():
279 """Close order sort."""
281 """Close order sort."""
280 keyfn = lambda n: ('close' not in self.commitcache[n].extra,
282 keyfn = lambda n: ('close' not in self.commitcache[n].extra,
281 self.commitcache[n].sortkey)
283 self.commitcache[n].sortkey)
282 def picknext(nodes):
284 def picknext(nodes):
283 return sorted(nodes, key=keyfn)[0]
285 return sorted(nodes, key=keyfn)[0]
284 return picknext
286 return picknext
285
287
286 def makedatesorter():
288 def makedatesorter():
287 """Sort revisions by date."""
289 """Sort revisions by date."""
288 dates = {}
290 dates = {}
289 def getdate(n):
291 def getdate(n):
290 if n not in dates:
292 if n not in dates:
291 dates[n] = util.parsedate(self.commitcache[n].date)
293 dates[n] = util.parsedate(self.commitcache[n].date)
292 return dates[n]
294 return dates[n]
293
295
294 def picknext(nodes):
296 def picknext(nodes):
295 return min([(getdate(n), n) for n in nodes])[1]
297 return min([(getdate(n), n) for n in nodes])[1]
296
298
297 return picknext
299 return picknext
298
300
299 if sortmode == 'branchsort':
301 if sortmode == 'branchsort':
300 picknext = makebranchsorter()
302 picknext = makebranchsorter()
301 elif sortmode == 'datesort':
303 elif sortmode == 'datesort':
302 picknext = makedatesorter()
304 picknext = makedatesorter()
303 elif sortmode == 'sourcesort':
305 elif sortmode == 'sourcesort':
304 picknext = makesourcesorter()
306 picknext = makesourcesorter()
305 elif sortmode == 'closesort':
307 elif sortmode == 'closesort':
306 picknext = makeclosesorter()
308 picknext = makeclosesorter()
307 else:
309 else:
308 raise util.Abort(_('unknown sort mode: %s') % sortmode)
310 raise util.Abort(_('unknown sort mode: %s') % sortmode)
309
311
310 children, actives = mapchildren(parents)
312 children, actives = mapchildren(parents)
311
313
312 s = []
314 s = []
313 pendings = {}
315 pendings = {}
314 while actives:
316 while actives:
315 n = picknext(actives)
317 n = picknext(actives)
316 actives.remove(n)
318 actives.remove(n)
317 s.append(n)
319 s.append(n)
318
320
319 # Update dependents list
321 # Update dependents list
320 for c in children.get(n, []):
322 for c in children.get(n, []):
321 if c not in pendings:
323 if c not in pendings:
322 pendings[c] = [p for p in parents[c] if p not in self.map]
324 pendings[c] = [p for p in parents[c] if p not in self.map]
323 try:
325 try:
324 pendings[c].remove(n)
326 pendings[c].remove(n)
325 except ValueError:
327 except ValueError:
326 raise util.Abort(_('cycle detected between %s and %s')
328 raise util.Abort(_('cycle detected between %s and %s')
327 % (recode(c), recode(n)))
329 % (recode(c), recode(n)))
328 if not pendings[c]:
330 if not pendings[c]:
329 # Parents are converted, node is eligible
331 # Parents are converted, node is eligible
330 actives.insert(0, c)
332 actives.insert(0, c)
331 pendings[c] = None
333 pendings[c] = None
332
334
333 if len(s) != len(parents):
335 if len(s) != len(parents):
334 raise util.Abort(_("not all revisions were sorted"))
336 raise util.Abort(_("not all revisions were sorted"))
335
337
336 return s
338 return s
337
339
338 def writeauthormap(self):
340 def writeauthormap(self):
339 authorfile = self.authorfile
341 authorfile = self.authorfile
340 if authorfile:
342 if authorfile:
341 self.ui.status(_('writing author map file %s\n') % authorfile)
343 self.ui.status(_('writing author map file %s\n') % authorfile)
342 ofile = open(authorfile, 'w+')
344 ofile = open(authorfile, 'w+')
343 for author in self.authors:
345 for author in self.authors:
344 ofile.write("%s=%s\n" % (author, self.authors[author]))
346 ofile.write("%s=%s\n" % (author, self.authors[author]))
345 ofile.close()
347 ofile.close()
346
348
347 def readauthormap(self, authorfile):
349 def readauthormap(self, authorfile):
348 afile = open(authorfile, 'r')
350 afile = open(authorfile, 'r')
349 for line in afile:
351 for line in afile:
350
352
351 line = line.strip()
353 line = line.strip()
352 if not line or line.startswith('#'):
354 if not line or line.startswith('#'):
353 continue
355 continue
354
356
355 try:
357 try:
356 srcauthor, dstauthor = line.split('=', 1)
358 srcauthor, dstauthor = line.split('=', 1)
357 except ValueError:
359 except ValueError:
358 msg = _('ignoring bad line in author map file %s: %s\n')
360 msg = _('ignoring bad line in author map file %s: %s\n')
359 self.ui.warn(msg % (authorfile, line.rstrip()))
361 self.ui.warn(msg % (authorfile, line.rstrip()))
360 continue
362 continue
361
363
362 srcauthor = srcauthor.strip()
364 srcauthor = srcauthor.strip()
363 dstauthor = dstauthor.strip()
365 dstauthor = dstauthor.strip()
364 if self.authors.get(srcauthor) in (None, dstauthor):
366 if self.authors.get(srcauthor) in (None, dstauthor):
365 msg = _('mapping author %s to %s\n')
367 msg = _('mapping author %s to %s\n')
366 self.ui.debug(msg % (srcauthor, dstauthor))
368 self.ui.debug(msg % (srcauthor, dstauthor))
367 self.authors[srcauthor] = dstauthor
369 self.authors[srcauthor] = dstauthor
368 continue
370 continue
369
371
370 m = _('overriding mapping for author %s, was %s, will be %s\n')
372 m = _('overriding mapping for author %s, was %s, will be %s\n')
371 self.ui.status(m % (srcauthor, self.authors[srcauthor], dstauthor))
373 self.ui.status(m % (srcauthor, self.authors[srcauthor], dstauthor))
372
374
373 afile.close()
375 afile.close()
374
376
375 def cachecommit(self, rev):
377 def cachecommit(self, rev):
376 commit = self.source.getcommit(rev)
378 commit = self.source.getcommit(rev)
377 commit.author = self.authors.get(commit.author, commit.author)
379 commit.author = self.authors.get(commit.author, commit.author)
378 # If commit.branch is None, this commit is coming from the source
380 # If commit.branch is None, this commit is coming from the source
379 # repository's default branch and destined for the default branch in the
381 # repository's default branch and destined for the default branch in the
380 # destination repository. For such commits, passing a literal "None"
382 # destination repository. For such commits, passing a literal "None"
381 # string to branchmap.get() below allows the user to map "None" to an
383 # string to branchmap.get() below allows the user to map "None" to an
382 # alternate default branch in the destination repository.
384 # alternate default branch in the destination repository.
383 commit.branch = self.branchmap.get(str(commit.branch), commit.branch)
385 commit.branch = self.branchmap.get(str(commit.branch), commit.branch)
384 self.commitcache[rev] = commit
386 self.commitcache[rev] = commit
385 return commit
387 return commit
386
388
387 def copy(self, rev):
389 def copy(self, rev):
388 commit = self.commitcache[rev]
390 commit = self.commitcache[rev]
389 full = self.opts.get('full')
391 full = self.opts.get('full')
390 changes = self.source.getchanges(rev, full)
392 changes = self.source.getchanges(rev, full)
391 if isinstance(changes, basestring):
393 if isinstance(changes, basestring):
392 if changes == SKIPREV:
394 if changes == SKIPREV:
393 dest = SKIPREV
395 dest = SKIPREV
394 else:
396 else:
395 dest = self.map[changes]
397 dest = self.map[changes]
396 self.map[rev] = dest
398 self.map[rev] = dest
397 return
399 return
398 files, copies = changes
400 files, copies = changes
399 pbranches = []
401 pbranches = []
400 if commit.parents:
402 if commit.parents:
401 for prev in commit.parents:
403 for prev in commit.parents:
402 if prev not in self.commitcache:
404 if prev not in self.commitcache:
403 self.cachecommit(prev)
405 self.cachecommit(prev)
404 pbranches.append((self.map[prev],
406 pbranches.append((self.map[prev],
405 self.commitcache[prev].branch))
407 self.commitcache[prev].branch))
406 self.dest.setbranch(commit.branch, pbranches)
408 self.dest.setbranch(commit.branch, pbranches)
407 try:
409 try:
408 parents = self.splicemap[rev]
410 parents = self.splicemap[rev]
409 self.ui.status(_('spliced in %s as parents of %s\n') %
411 self.ui.status(_('spliced in %s as parents of %s\n') %
410 (parents, rev))
412 (parents, rev))
411 parents = [self.map.get(p, p) for p in parents]
413 parents = [self.map.get(p, p) for p in parents]
412 except KeyError:
414 except KeyError:
413 parents = [b[0] for b in pbranches]
415 parents = [b[0] for b in pbranches]
414 source = progresssource(self.ui, self.source, len(files))
416 source = progresssource(self.ui, self.source, len(files))
415 newnode = self.dest.putcommit(files, copies, parents, commit,
417 newnode = self.dest.putcommit(files, copies, parents, commit,
416 source, self.map, full)
418 source, self.map, full)
417 source.close()
419 source.close()
418 self.source.converted(rev, newnode)
420 self.source.converted(rev, newnode)
419 self.map[rev] = newnode
421 self.map[rev] = newnode
420
422
421 def convert(self, sortmode):
423 def convert(self, sortmode):
422 try:
424 try:
423 self.source.before()
425 self.source.before()
424 self.dest.before()
426 self.dest.before()
425 self.source.setrevmap(self.map)
427 self.source.setrevmap(self.map)
426 self.ui.status(_("scanning source...\n"))
428 self.ui.status(_("scanning source...\n"))
427 heads = self.source.getheads()
429 heads = self.source.getheads()
428 parents = self.walktree(heads)
430 parents = self.walktree(heads)
429 self.mergesplicemap(parents, self.splicemap)
431 self.mergesplicemap(parents, self.splicemap)
430 self.ui.status(_("sorting...\n"))
432 self.ui.status(_("sorting...\n"))
431 t = self.toposort(parents, sortmode)
433 t = self.toposort(parents, sortmode)
432 num = len(t)
434 num = len(t)
433 c = None
435 c = None
434
436
435 self.ui.status(_("converting...\n"))
437 self.ui.status(_("converting...\n"))
436 for i, c in enumerate(t):
438 for i, c in enumerate(t):
437 num -= 1
439 num -= 1
438 desc = self.commitcache[c].desc
440 desc = self.commitcache[c].desc
439 if "\n" in desc:
441 if "\n" in desc:
440 desc = desc.splitlines()[0]
442 desc = desc.splitlines()[0]
441 # convert log message to local encoding without using
443 # convert log message to local encoding without using
442 # tolocal() because the encoding.encoding convert()
444 # tolocal() because the encoding.encoding convert()
443 # uses is 'utf-8'
445 # uses is 'utf-8'
444 self.ui.status("%d %s\n" % (num, recode(desc)))
446 self.ui.status("%d %s\n" % (num, recode(desc)))
445 self.ui.note(_("source: %s\n") % recode(c))
447 self.ui.note(_("source: %s\n") % recode(c))
446 self.ui.progress(_('converting'), i, unit=_('revisions'),
448 self.ui.progress(_('converting'), i, unit=_('revisions'),
447 total=len(t))
449 total=len(t))
448 self.copy(c)
450 self.copy(c)
449 self.ui.progress(_('converting'), None)
451 self.ui.progress(_('converting'), None)
450
452
451 tags = self.source.gettags()
453 tags = self.source.gettags()
452 ctags = {}
454 ctags = {}
453 for k in tags:
455 for k in tags:
454 v = tags[k]
456 v = tags[k]
455 if self.map.get(v, SKIPREV) != SKIPREV:
457 if self.map.get(v, SKIPREV) != SKIPREV:
456 ctags[k] = self.map[v]
458 ctags[k] = self.map[v]
457
459
458 if c and ctags:
460 if c and ctags:
459 nrev, tagsparent = self.dest.puttags(ctags)
461 nrev, tagsparent = self.dest.puttags(ctags)
460 if nrev and tagsparent:
462 if nrev and tagsparent:
461 # write another hash correspondence to override the previous
463 # write another hash correspondence to override the previous
462 # one so we don't end up with extra tag heads
464 # one so we don't end up with extra tag heads
463 tagsparents = [e for e in self.map.iteritems()
465 tagsparents = [e for e in self.map.iteritems()
464 if e[1] == tagsparent]
466 if e[1] == tagsparent]
465 if tagsparents:
467 if tagsparents:
466 self.map[tagsparents[0][0]] = nrev
468 self.map[tagsparents[0][0]] = nrev
467
469
468 bookmarks = self.source.getbookmarks()
470 bookmarks = self.source.getbookmarks()
469 cbookmarks = {}
471 cbookmarks = {}
470 for k in bookmarks:
472 for k in bookmarks:
471 v = bookmarks[k]
473 v = bookmarks[k]
472 if self.map.get(v, SKIPREV) != SKIPREV:
474 if self.map.get(v, SKIPREV) != SKIPREV:
473 cbookmarks[k] = self.map[v]
475 cbookmarks[k] = self.map[v]
474
476
475 if c and cbookmarks:
477 if c and cbookmarks:
476 self.dest.putbookmarks(cbookmarks)
478 self.dest.putbookmarks(cbookmarks)
477
479
478 self.writeauthormap()
480 self.writeauthormap()
479 finally:
481 finally:
480 self.cleanup()
482 self.cleanup()
481
483
482 def cleanup(self):
484 def cleanup(self):
483 try:
485 try:
484 self.dest.after()
486 self.dest.after()
485 finally:
487 finally:
486 self.source.after()
488 self.source.after()
487 self.map.close()
489 self.map.close()
488
490
489 def convert(ui, src, dest=None, revmapfile=None, **opts):
491 def convert(ui, src, dest=None, revmapfile=None, **opts):
490 global orig_encoding
492 global orig_encoding
491 orig_encoding = encoding.encoding
493 orig_encoding = encoding.encoding
492 encoding.encoding = 'UTF-8'
494 encoding.encoding = 'UTF-8'
493
495
494 # support --authors as an alias for --authormap
496 # support --authors as an alias for --authormap
495 if not opts.get('authormap'):
497 if not opts.get('authormap'):
496 opts['authormap'] = opts.get('authors')
498 opts['authormap'] = opts.get('authors')
497
499
498 if not dest:
500 if not dest:
499 dest = hg.defaultdest(src) + "-hg"
501 dest = hg.defaultdest(src) + "-hg"
500 ui.status(_("assuming destination %s\n") % dest)
502 ui.status(_("assuming destination %s\n") % dest)
501
503
502 destc = convertsink(ui, dest, opts.get('dest_type'))
504 destc = convertsink(ui, dest, opts.get('dest_type'))
503
505
504 try:
506 try:
505 srcc, defaultsort = convertsource(ui, src, opts.get('source_type'),
507 srcc, defaultsort = convertsource(ui, src, opts.get('source_type'),
506 opts.get('rev'))
508 opts.get('rev'))
507 except Exception:
509 except Exception:
508 for path in destc.created:
510 for path in destc.created:
509 shutil.rmtree(path, True)
511 shutil.rmtree(path, True)
510 raise
512 raise
511
513
512 sortmodes = ('branchsort', 'datesort', 'sourcesort', 'closesort')
514 sortmodes = ('branchsort', 'datesort', 'sourcesort', 'closesort')
513 sortmode = [m for m in sortmodes if opts.get(m)]
515 sortmode = [m for m in sortmodes if opts.get(m)]
514 if len(sortmode) > 1:
516 if len(sortmode) > 1:
515 raise util.Abort(_('more than one sort mode specified'))
517 raise util.Abort(_('more than one sort mode specified'))
516 sortmode = sortmode and sortmode[0] or defaultsort
518 sortmode = sortmode and sortmode[0] or defaultsort
517 if sortmode == 'sourcesort' and not srcc.hasnativeorder():
519 if sortmode == 'sourcesort' and not srcc.hasnativeorder():
518 raise util.Abort(_('--sourcesort is not supported by this data source'))
520 raise util.Abort(_('--sourcesort is not supported by this data source'))
519 if sortmode == 'closesort' and not srcc.hasnativeclose():
521 if sortmode == 'closesort' and not srcc.hasnativeclose():
520 raise util.Abort(_('--closesort is not supported by this data source'))
522 raise util.Abort(_('--closesort is not supported by this data source'))
521
523
522 fmap = opts.get('filemap')
524 fmap = opts.get('filemap')
523 if fmap:
525 if fmap:
524 srcc = filemap.filemap_source(ui, srcc, fmap)
526 srcc = filemap.filemap_source(ui, srcc, fmap)
525 destc.setfilemapmode(True)
527 destc.setfilemapmode(True)
526
528
527 if not revmapfile:
529 if not revmapfile:
528 revmapfile = destc.revmapfile()
530 revmapfile = destc.revmapfile()
529
531
530 c = converter(ui, srcc, destc, revmapfile, opts)
532 c = converter(ui, srcc, destc, revmapfile, opts)
531 c.convert(sortmode)
533 c.convert(sortmode)
532
534
General Comments 0
You need to be logged in to leave comments. Login now