##// END OF EJS Templates
convert: keep converted hg parents that are outside convert.hg.revs (BC)...
Mads Kiilerich -
r28900:b65966f5 default
parent child Browse files
Show More
@@ -1,492 +1,494 b''
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 from __future__ import absolute_import
7 from __future__ import absolute_import
8
8
9 import base64
9 import base64
10 import cPickle as pickle
10 import cPickle as pickle
11 import datetime
11 import datetime
12 import errno
12 import errno
13 import os
13 import os
14 import re
14 import re
15 import subprocess
15 import subprocess
16
16
17 from mercurial import (
17 from mercurial import (
18 error,
18 error,
19 phases,
19 phases,
20 util,
20 util,
21 )
21 )
22 from mercurial.i18n import _
22 from mercurial.i18n import _
23
23
24 propertycache = util.propertycache
24 propertycache = util.propertycache
25
25
26 def encodeargs(args):
26 def encodeargs(args):
27 def encodearg(s):
27 def encodearg(s):
28 lines = base64.encodestring(s)
28 lines = base64.encodestring(s)
29 lines = [l.splitlines()[0] for l in lines]
29 lines = [l.splitlines()[0] for l in lines]
30 return ''.join(lines)
30 return ''.join(lines)
31
31
32 s = pickle.dumps(args)
32 s = pickle.dumps(args)
33 return encodearg(s)
33 return encodearg(s)
34
34
35 def decodeargs(s):
35 def decodeargs(s):
36 s = base64.decodestring(s)
36 s = base64.decodestring(s)
37 return pickle.loads(s)
37 return pickle.loads(s)
38
38
39 class MissingTool(Exception):
39 class MissingTool(Exception):
40 pass
40 pass
41
41
42 def checktool(exe, name=None, abort=True):
42 def checktool(exe, name=None, abort=True):
43 name = name or exe
43 name = name or exe
44 if not util.findexe(exe):
44 if not util.findexe(exe):
45 if abort:
45 if abort:
46 exc = error.Abort
46 exc = error.Abort
47 else:
47 else:
48 exc = MissingTool
48 exc = MissingTool
49 raise exc(_('cannot find required "%s" tool') % name)
49 raise exc(_('cannot find required "%s" tool') % name)
50
50
51 class NoRepo(Exception):
51 class NoRepo(Exception):
52 pass
52 pass
53
53
54 SKIPREV = 'SKIP'
54 SKIPREV = 'SKIP'
55
55
56 class commit(object):
56 class commit(object):
57 def __init__(self, author, date, desc, parents, branch=None, rev=None,
57 def __init__(self, author, date, desc, parents, branch=None, rev=None,
58 extra={}, sortkey=None, saverev=True, phase=phases.draft):
58 extra={}, sortkey=None, saverev=True, phase=phases.draft,
59 optparents=None):
59 self.author = author or 'unknown'
60 self.author = author or 'unknown'
60 self.date = date or '0 0'
61 self.date = date or '0 0'
61 self.desc = desc
62 self.desc = desc
62 self.parents = parents
63 self.parents = parents # will be converted and used as parents
64 self.optparents = optparents or [] # will be used if already converted
63 self.branch = branch
65 self.branch = branch
64 self.rev = rev
66 self.rev = rev
65 self.extra = extra
67 self.extra = extra
66 self.sortkey = sortkey
68 self.sortkey = sortkey
67 self.saverev = saverev
69 self.saverev = saverev
68 self.phase = phase
70 self.phase = phase
69
71
70 class converter_source(object):
72 class converter_source(object):
71 """Conversion source interface"""
73 """Conversion source interface"""
72
74
73 def __init__(self, ui, path=None, revs=None):
75 def __init__(self, ui, path=None, revs=None):
74 """Initialize conversion source (or raise NoRepo("message")
76 """Initialize conversion source (or raise NoRepo("message")
75 exception if path is not a valid repository)"""
77 exception if path is not a valid repository)"""
76 self.ui = ui
78 self.ui = ui
77 self.path = path
79 self.path = path
78 self.revs = revs
80 self.revs = revs
79
81
80 self.encoding = 'utf-8'
82 self.encoding = 'utf-8'
81
83
82 def checkhexformat(self, revstr, mapname='splicemap'):
84 def checkhexformat(self, revstr, mapname='splicemap'):
83 """ fails if revstr is not a 40 byte hex. mercurial and git both uses
85 """ fails if revstr is not a 40 byte hex. mercurial and git both uses
84 such format for their revision numbering
86 such format for their revision numbering
85 """
87 """
86 if not re.match(r'[0-9a-fA-F]{40,40}$', revstr):
88 if not re.match(r'[0-9a-fA-F]{40,40}$', revstr):
87 raise error.Abort(_('%s entry %s is not a valid revision'
89 raise error.Abort(_('%s entry %s is not a valid revision'
88 ' identifier') % (mapname, revstr))
90 ' identifier') % (mapname, revstr))
89
91
90 def before(self):
92 def before(self):
91 pass
93 pass
92
94
93 def after(self):
95 def after(self):
94 pass
96 pass
95
97
96 def targetfilebelongstosource(self, targetfilename):
98 def targetfilebelongstosource(self, targetfilename):
97 """Returns true if the given targetfile belongs to the source repo. This
99 """Returns true if the given targetfile belongs to the source repo. This
98 is useful when only a subdirectory of the target belongs to the source
100 is useful when only a subdirectory of the target belongs to the source
99 repo."""
101 repo."""
100 # For normal full repo converts, this is always True.
102 # For normal full repo converts, this is always True.
101 return True
103 return True
102
104
103 def setrevmap(self, revmap):
105 def setrevmap(self, revmap):
104 """set the map of already-converted revisions"""
106 """set the map of already-converted revisions"""
105 pass
107 pass
106
108
107 def getheads(self):
109 def getheads(self):
108 """Return a list of this repository's heads"""
110 """Return a list of this repository's heads"""
109 raise NotImplementedError
111 raise NotImplementedError
110
112
111 def getfile(self, name, rev):
113 def getfile(self, name, rev):
112 """Return a pair (data, mode) where data is the file content
114 """Return a pair (data, mode) where data is the file content
113 as a string and mode one of '', 'x' or 'l'. rev is the
115 as a string and mode one of '', 'x' or 'l'. rev is the
114 identifier returned by a previous call to getchanges().
116 identifier returned by a previous call to getchanges().
115 Data is None if file is missing/deleted in rev.
117 Data is None if file is missing/deleted in rev.
116 """
118 """
117 raise NotImplementedError
119 raise NotImplementedError
118
120
119 def getchanges(self, version, full):
121 def getchanges(self, version, full):
120 """Returns a tuple of (files, copies, cleanp2).
122 """Returns a tuple of (files, copies, cleanp2).
121
123
122 files is a sorted list of (filename, id) tuples for all files
124 files is a sorted list of (filename, id) tuples for all files
123 changed between version and its first parent returned by
125 changed between version and its first parent returned by
124 getcommit(). If full, all files in that revision is returned.
126 getcommit(). If full, all files in that revision is returned.
125 id is the source revision id of the file.
127 id is the source revision id of the file.
126
128
127 copies is a dictionary of dest: source
129 copies is a dictionary of dest: source
128
130
129 cleanp2 is the set of files filenames that are clean against p2.
131 cleanp2 is the set of files filenames that are clean against p2.
130 (Files that are clean against p1 are already not in files (unless
132 (Files that are clean against p1 are already not in files (unless
131 full). This makes it possible to handle p2 clean files similarly.)
133 full). This makes it possible to handle p2 clean files similarly.)
132 """
134 """
133 raise NotImplementedError
135 raise NotImplementedError
134
136
135 def getcommit(self, version):
137 def getcommit(self, version):
136 """Return the commit object for version"""
138 """Return the commit object for version"""
137 raise NotImplementedError
139 raise NotImplementedError
138
140
139 def numcommits(self):
141 def numcommits(self):
140 """Return the number of commits in this source.
142 """Return the number of commits in this source.
141
143
142 If unknown, return None.
144 If unknown, return None.
143 """
145 """
144 return None
146 return None
145
147
146 def gettags(self):
148 def gettags(self):
147 """Return the tags as a dictionary of name: revision
149 """Return the tags as a dictionary of name: revision
148
150
149 Tag names must be UTF-8 strings.
151 Tag names must be UTF-8 strings.
150 """
152 """
151 raise NotImplementedError
153 raise NotImplementedError
152
154
153 def recode(self, s, encoding=None):
155 def recode(self, s, encoding=None):
154 if not encoding:
156 if not encoding:
155 encoding = self.encoding or 'utf-8'
157 encoding = self.encoding or 'utf-8'
156
158
157 if isinstance(s, unicode):
159 if isinstance(s, unicode):
158 return s.encode("utf-8")
160 return s.encode("utf-8")
159 try:
161 try:
160 return s.decode(encoding).encode("utf-8")
162 return s.decode(encoding).encode("utf-8")
161 except UnicodeError:
163 except UnicodeError:
162 try:
164 try:
163 return s.decode("latin-1").encode("utf-8")
165 return s.decode("latin-1").encode("utf-8")
164 except UnicodeError:
166 except UnicodeError:
165 return s.decode(encoding, "replace").encode("utf-8")
167 return s.decode(encoding, "replace").encode("utf-8")
166
168
167 def getchangedfiles(self, rev, i):
169 def getchangedfiles(self, rev, i):
168 """Return the files changed by rev compared to parent[i].
170 """Return the files changed by rev compared to parent[i].
169
171
170 i is an index selecting one of the parents of rev. The return
172 i is an index selecting one of the parents of rev. The return
171 value should be the list of files that are different in rev and
173 value should be the list of files that are different in rev and
172 this parent.
174 this parent.
173
175
174 If rev has no parents, i is None.
176 If rev has no parents, i is None.
175
177
176 This function is only needed to support --filemap
178 This function is only needed to support --filemap
177 """
179 """
178 raise NotImplementedError
180 raise NotImplementedError
179
181
180 def converted(self, rev, sinkrev):
182 def converted(self, rev, sinkrev):
181 '''Notify the source that a revision has been converted.'''
183 '''Notify the source that a revision has been converted.'''
182 pass
184 pass
183
185
184 def hasnativeorder(self):
186 def hasnativeorder(self):
185 """Return true if this source has a meaningful, native revision
187 """Return true if this source has a meaningful, native revision
186 order. For instance, Mercurial revisions are store sequentially
188 order. For instance, Mercurial revisions are store sequentially
187 while there is no such global ordering with Darcs.
189 while there is no such global ordering with Darcs.
188 """
190 """
189 return False
191 return False
190
192
191 def hasnativeclose(self):
193 def hasnativeclose(self):
192 """Return true if this source has ability to close branch.
194 """Return true if this source has ability to close branch.
193 """
195 """
194 return False
196 return False
195
197
196 def lookuprev(self, rev):
198 def lookuprev(self, rev):
197 """If rev is a meaningful revision reference in source, return
199 """If rev is a meaningful revision reference in source, return
198 the referenced identifier in the same format used by getcommit().
200 the referenced identifier in the same format used by getcommit().
199 return None otherwise.
201 return None otherwise.
200 """
202 """
201 return None
203 return None
202
204
203 def getbookmarks(self):
205 def getbookmarks(self):
204 """Return the bookmarks as a dictionary of name: revision
206 """Return the bookmarks as a dictionary of name: revision
205
207
206 Bookmark names are to be UTF-8 strings.
208 Bookmark names are to be UTF-8 strings.
207 """
209 """
208 return {}
210 return {}
209
211
210 def checkrevformat(self, revstr, mapname='splicemap'):
212 def checkrevformat(self, revstr, mapname='splicemap'):
211 """revstr is a string that describes a revision in the given
213 """revstr is a string that describes a revision in the given
212 source control system. Return true if revstr has correct
214 source control system. Return true if revstr has correct
213 format.
215 format.
214 """
216 """
215 return True
217 return True
216
218
217 class converter_sink(object):
219 class converter_sink(object):
218 """Conversion sink (target) interface"""
220 """Conversion sink (target) interface"""
219
221
220 def __init__(self, ui, path):
222 def __init__(self, ui, path):
221 """Initialize conversion sink (or raise NoRepo("message")
223 """Initialize conversion sink (or raise NoRepo("message")
222 exception if path is not a valid repository)
224 exception if path is not a valid repository)
223
225
224 created is a list of paths to remove if a fatal error occurs
226 created is a list of paths to remove if a fatal error occurs
225 later"""
227 later"""
226 self.ui = ui
228 self.ui = ui
227 self.path = path
229 self.path = path
228 self.created = []
230 self.created = []
229
231
230 def revmapfile(self):
232 def revmapfile(self):
231 """Path to a file that will contain lines
233 """Path to a file that will contain lines
232 source_rev_id sink_rev_id
234 source_rev_id sink_rev_id
233 mapping equivalent revision identifiers for each system."""
235 mapping equivalent revision identifiers for each system."""
234 raise NotImplementedError
236 raise NotImplementedError
235
237
236 def authorfile(self):
238 def authorfile(self):
237 """Path to a file that will contain lines
239 """Path to a file that will contain lines
238 srcauthor=dstauthor
240 srcauthor=dstauthor
239 mapping equivalent authors identifiers for each system."""
241 mapping equivalent authors identifiers for each system."""
240 return None
242 return None
241
243
242 def putcommit(self, files, copies, parents, commit, source, revmap, full,
244 def putcommit(self, files, copies, parents, commit, source, revmap, full,
243 cleanp2):
245 cleanp2):
244 """Create a revision with all changed files listed in 'files'
246 """Create a revision with all changed files listed in 'files'
245 and having listed parents. 'commit' is a commit object
247 and having listed parents. 'commit' is a commit object
246 containing at a minimum the author, date, and message for this
248 containing at a minimum the author, date, and message for this
247 changeset. 'files' is a list of (path, version) tuples,
249 changeset. 'files' is a list of (path, version) tuples,
248 'copies' is a dictionary mapping destinations to sources,
250 'copies' is a dictionary mapping destinations to sources,
249 'source' is the source repository, and 'revmap' is a mapfile
251 'source' is the source repository, and 'revmap' is a mapfile
250 of source revisions to converted revisions. Only getfile() and
252 of source revisions to converted revisions. Only getfile() and
251 lookuprev() should be called on 'source'. 'full' means that 'files'
253 lookuprev() should be called on 'source'. 'full' means that 'files'
252 is complete and all other files should be removed.
254 is complete and all other files should be removed.
253 'cleanp2' is a set of the filenames that are unchanged from p2
255 'cleanp2' is a set of the filenames that are unchanged from p2
254 (only in the common merge case where there two parents).
256 (only in the common merge case where there two parents).
255
257
256 Note that the sink repository is not told to update itself to
258 Note that the sink repository is not told to update itself to
257 a particular revision (or even what that revision would be)
259 a particular revision (or even what that revision would be)
258 before it receives the file data.
260 before it receives the file data.
259 """
261 """
260 raise NotImplementedError
262 raise NotImplementedError
261
263
262 def puttags(self, tags):
264 def puttags(self, tags):
263 """Put tags into sink.
265 """Put tags into sink.
264
266
265 tags: {tagname: sink_rev_id, ...} where tagname is an UTF-8 string.
267 tags: {tagname: sink_rev_id, ...} where tagname is an UTF-8 string.
266 Return a pair (tag_revision, tag_parent_revision), or (None, None)
268 Return a pair (tag_revision, tag_parent_revision), or (None, None)
267 if nothing was changed.
269 if nothing was changed.
268 """
270 """
269 raise NotImplementedError
271 raise NotImplementedError
270
272
271 def setbranch(self, branch, pbranches):
273 def setbranch(self, branch, pbranches):
272 """Set the current branch name. Called before the first putcommit
274 """Set the current branch name. Called before the first putcommit
273 on the branch.
275 on the branch.
274 branch: branch name for subsequent commits
276 branch: branch name for subsequent commits
275 pbranches: (converted parent revision, parent branch) tuples"""
277 pbranches: (converted parent revision, parent branch) tuples"""
276 pass
278 pass
277
279
278 def setfilemapmode(self, active):
280 def setfilemapmode(self, active):
279 """Tell the destination that we're using a filemap
281 """Tell the destination that we're using a filemap
280
282
281 Some converter_sources (svn in particular) can claim that a file
283 Some converter_sources (svn in particular) can claim that a file
282 was changed in a revision, even if there was no change. This method
284 was changed in a revision, even if there was no change. This method
283 tells the destination that we're using a filemap and that it should
285 tells the destination that we're using a filemap and that it should
284 filter empty revisions.
286 filter empty revisions.
285 """
287 """
286 pass
288 pass
287
289
288 def before(self):
290 def before(self):
289 pass
291 pass
290
292
291 def after(self):
293 def after(self):
292 pass
294 pass
293
295
294 def putbookmarks(self, bookmarks):
296 def putbookmarks(self, bookmarks):
295 """Put bookmarks into sink.
297 """Put bookmarks into sink.
296
298
297 bookmarks: {bookmarkname: sink_rev_id, ...}
299 bookmarks: {bookmarkname: sink_rev_id, ...}
298 where bookmarkname is an UTF-8 string.
300 where bookmarkname is an UTF-8 string.
299 """
301 """
300 pass
302 pass
301
303
302 def hascommitfrommap(self, rev):
304 def hascommitfrommap(self, rev):
303 """Return False if a rev mentioned in a filemap is known to not be
305 """Return False if a rev mentioned in a filemap is known to not be
304 present."""
306 present."""
305 raise NotImplementedError
307 raise NotImplementedError
306
308
307 def hascommitforsplicemap(self, rev):
309 def hascommitforsplicemap(self, rev):
308 """This method is for the special needs for splicemap handling and not
310 """This method is for the special needs for splicemap handling and not
309 for general use. Returns True if the sink contains rev, aborts on some
311 for general use. Returns True if the sink contains rev, aborts on some
310 special cases."""
312 special cases."""
311 raise NotImplementedError
313 raise NotImplementedError
312
314
313 class commandline(object):
315 class commandline(object):
314 def __init__(self, ui, command):
316 def __init__(self, ui, command):
315 self.ui = ui
317 self.ui = ui
316 self.command = command
318 self.command = command
317
319
318 def prerun(self):
320 def prerun(self):
319 pass
321 pass
320
322
321 def postrun(self):
323 def postrun(self):
322 pass
324 pass
323
325
324 def _cmdline(self, cmd, *args, **kwargs):
326 def _cmdline(self, cmd, *args, **kwargs):
325 cmdline = [self.command, cmd] + list(args)
327 cmdline = [self.command, cmd] + list(args)
326 for k, v in kwargs.iteritems():
328 for k, v in kwargs.iteritems():
327 if len(k) == 1:
329 if len(k) == 1:
328 cmdline.append('-' + k)
330 cmdline.append('-' + k)
329 else:
331 else:
330 cmdline.append('--' + k.replace('_', '-'))
332 cmdline.append('--' + k.replace('_', '-'))
331 try:
333 try:
332 if len(k) == 1:
334 if len(k) == 1:
333 cmdline.append('' + v)
335 cmdline.append('' + v)
334 else:
336 else:
335 cmdline[-1] += '=' + v
337 cmdline[-1] += '=' + v
336 except TypeError:
338 except TypeError:
337 pass
339 pass
338 cmdline = [util.shellquote(arg) for arg in cmdline]
340 cmdline = [util.shellquote(arg) for arg in cmdline]
339 if not self.ui.debugflag:
341 if not self.ui.debugflag:
340 cmdline += ['2>', os.devnull]
342 cmdline += ['2>', os.devnull]
341 cmdline = ' '.join(cmdline)
343 cmdline = ' '.join(cmdline)
342 return cmdline
344 return cmdline
343
345
344 def _run(self, cmd, *args, **kwargs):
346 def _run(self, cmd, *args, **kwargs):
345 def popen(cmdline):
347 def popen(cmdline):
346 p = subprocess.Popen(cmdline, shell=True, bufsize=-1,
348 p = subprocess.Popen(cmdline, shell=True, bufsize=-1,
347 close_fds=util.closefds,
349 close_fds=util.closefds,
348 stdout=subprocess.PIPE)
350 stdout=subprocess.PIPE)
349 return p
351 return p
350 return self._dorun(popen, cmd, *args, **kwargs)
352 return self._dorun(popen, cmd, *args, **kwargs)
351
353
352 def _run2(self, cmd, *args, **kwargs):
354 def _run2(self, cmd, *args, **kwargs):
353 return self._dorun(util.popen2, cmd, *args, **kwargs)
355 return self._dorun(util.popen2, cmd, *args, **kwargs)
354
356
355 def _run3(self, cmd, *args, **kwargs):
357 def _run3(self, cmd, *args, **kwargs):
356 return self._dorun(util.popen3, cmd, *args, **kwargs)
358 return self._dorun(util.popen3, cmd, *args, **kwargs)
357
359
358 def _dorun(self, openfunc, cmd, *args, **kwargs):
360 def _dorun(self, openfunc, cmd, *args, **kwargs):
359 cmdline = self._cmdline(cmd, *args, **kwargs)
361 cmdline = self._cmdline(cmd, *args, **kwargs)
360 self.ui.debug('running: %s\n' % (cmdline,))
362 self.ui.debug('running: %s\n' % (cmdline,))
361 self.prerun()
363 self.prerun()
362 try:
364 try:
363 return openfunc(cmdline)
365 return openfunc(cmdline)
364 finally:
366 finally:
365 self.postrun()
367 self.postrun()
366
368
367 def run(self, cmd, *args, **kwargs):
369 def run(self, cmd, *args, **kwargs):
368 p = self._run(cmd, *args, **kwargs)
370 p = self._run(cmd, *args, **kwargs)
369 output = p.communicate()[0]
371 output = p.communicate()[0]
370 self.ui.debug(output)
372 self.ui.debug(output)
371 return output, p.returncode
373 return output, p.returncode
372
374
373 def runlines(self, cmd, *args, **kwargs):
375 def runlines(self, cmd, *args, **kwargs):
374 p = self._run(cmd, *args, **kwargs)
376 p = self._run(cmd, *args, **kwargs)
375 output = p.stdout.readlines()
377 output = p.stdout.readlines()
376 p.wait()
378 p.wait()
377 self.ui.debug(''.join(output))
379 self.ui.debug(''.join(output))
378 return output, p.returncode
380 return output, p.returncode
379
381
380 def checkexit(self, status, output=''):
382 def checkexit(self, status, output=''):
381 if status:
383 if status:
382 if output:
384 if output:
383 self.ui.warn(_('%s error:\n') % self.command)
385 self.ui.warn(_('%s error:\n') % self.command)
384 self.ui.warn(output)
386 self.ui.warn(output)
385 msg = util.explainexit(status)[0]
387 msg = util.explainexit(status)[0]
386 raise error.Abort('%s %s' % (self.command, msg))
388 raise error.Abort('%s %s' % (self.command, msg))
387
389
388 def run0(self, cmd, *args, **kwargs):
390 def run0(self, cmd, *args, **kwargs):
389 output, status = self.run(cmd, *args, **kwargs)
391 output, status = self.run(cmd, *args, **kwargs)
390 self.checkexit(status, output)
392 self.checkexit(status, output)
391 return output
393 return output
392
394
393 def runlines0(self, cmd, *args, **kwargs):
395 def runlines0(self, cmd, *args, **kwargs):
394 output, status = self.runlines(cmd, *args, **kwargs)
396 output, status = self.runlines(cmd, *args, **kwargs)
395 self.checkexit(status, ''.join(output))
397 self.checkexit(status, ''.join(output))
396 return output
398 return output
397
399
398 @propertycache
400 @propertycache
399 def argmax(self):
401 def argmax(self):
400 # POSIX requires at least 4096 bytes for ARG_MAX
402 # POSIX requires at least 4096 bytes for ARG_MAX
401 argmax = 4096
403 argmax = 4096
402 try:
404 try:
403 argmax = os.sysconf("SC_ARG_MAX")
405 argmax = os.sysconf("SC_ARG_MAX")
404 except (AttributeError, ValueError):
406 except (AttributeError, ValueError):
405 pass
407 pass
406
408
407 # Windows shells impose their own limits on command line length,
409 # Windows shells impose their own limits on command line length,
408 # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
410 # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
409 # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
411 # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
410 # details about cmd.exe limitations.
412 # details about cmd.exe limitations.
411
413
412 # Since ARG_MAX is for command line _and_ environment, lower our limit
414 # Since ARG_MAX is for command line _and_ environment, lower our limit
413 # (and make happy Windows shells while doing this).
415 # (and make happy Windows shells while doing this).
414 return argmax // 2 - 1
416 return argmax // 2 - 1
415
417
416 def _limit_arglist(self, arglist, cmd, *args, **kwargs):
418 def _limit_arglist(self, arglist, cmd, *args, **kwargs):
417 cmdlen = len(self._cmdline(cmd, *args, **kwargs))
419 cmdlen = len(self._cmdline(cmd, *args, **kwargs))
418 limit = self.argmax - cmdlen
420 limit = self.argmax - cmdlen
419 bytes = 0
421 bytes = 0
420 fl = []
422 fl = []
421 for fn in arglist:
423 for fn in arglist:
422 b = len(fn) + 3
424 b = len(fn) + 3
423 if bytes + b < limit or len(fl) == 0:
425 if bytes + b < limit or len(fl) == 0:
424 fl.append(fn)
426 fl.append(fn)
425 bytes += b
427 bytes += b
426 else:
428 else:
427 yield fl
429 yield fl
428 fl = [fn]
430 fl = [fn]
429 bytes = b
431 bytes = b
430 if fl:
432 if fl:
431 yield fl
433 yield fl
432
434
433 def xargs(self, arglist, cmd, *args, **kwargs):
435 def xargs(self, arglist, cmd, *args, **kwargs):
434 for l in self._limit_arglist(arglist, cmd, *args, **kwargs):
436 for l in self._limit_arglist(arglist, cmd, *args, **kwargs):
435 self.run0(cmd, *(list(args) + l), **kwargs)
437 self.run0(cmd, *(list(args) + l), **kwargs)
436
438
437 class mapfile(dict):
439 class mapfile(dict):
438 def __init__(self, ui, path):
440 def __init__(self, ui, path):
439 super(mapfile, self).__init__()
441 super(mapfile, self).__init__()
440 self.ui = ui
442 self.ui = ui
441 self.path = path
443 self.path = path
442 self.fp = None
444 self.fp = None
443 self.order = []
445 self.order = []
444 self._read()
446 self._read()
445
447
446 def _read(self):
448 def _read(self):
447 if not self.path:
449 if not self.path:
448 return
450 return
449 try:
451 try:
450 fp = open(self.path, 'r')
452 fp = open(self.path, 'r')
451 except IOError as err:
453 except IOError as err:
452 if err.errno != errno.ENOENT:
454 if err.errno != errno.ENOENT:
453 raise
455 raise
454 return
456 return
455 for i, line in enumerate(fp):
457 for i, line in enumerate(fp):
456 line = line.splitlines()[0].rstrip()
458 line = line.splitlines()[0].rstrip()
457 if not line:
459 if not line:
458 # Ignore blank lines
460 # Ignore blank lines
459 continue
461 continue
460 try:
462 try:
461 key, value = line.rsplit(' ', 1)
463 key, value = line.rsplit(' ', 1)
462 except ValueError:
464 except ValueError:
463 raise error.Abort(
465 raise error.Abort(
464 _('syntax error in %s(%d): key/value pair expected')
466 _('syntax error in %s(%d): key/value pair expected')
465 % (self.path, i + 1))
467 % (self.path, i + 1))
466 if key not in self:
468 if key not in self:
467 self.order.append(key)
469 self.order.append(key)
468 super(mapfile, self).__setitem__(key, value)
470 super(mapfile, self).__setitem__(key, value)
469 fp.close()
471 fp.close()
470
472
471 def __setitem__(self, key, value):
473 def __setitem__(self, key, value):
472 if self.fp is None:
474 if self.fp is None:
473 try:
475 try:
474 self.fp = open(self.path, 'a')
476 self.fp = open(self.path, 'a')
475 except IOError as err:
477 except IOError as err:
476 raise error.Abort(_('could not open map file %r: %s') %
478 raise error.Abort(_('could not open map file %r: %s') %
477 (self.path, err.strerror))
479 (self.path, err.strerror))
478 self.fp.write('%s %s\n' % (key, value))
480 self.fp.write('%s %s\n' % (key, value))
479 self.fp.flush()
481 self.fp.flush()
480 super(mapfile, self).__setitem__(key, value)
482 super(mapfile, self).__setitem__(key, value)
481
483
482 def close(self):
484 def close(self):
483 if self.fp:
485 if self.fp:
484 self.fp.close()
486 self.fp.close()
485 self.fp = None
487 self.fp = None
486
488
487 def makedatetimestamp(t):
489 def makedatetimestamp(t):
488 """Like util.makedate() but for time t instead of current time"""
490 """Like util.makedate() but for time t instead of current time"""
489 delta = (datetime.datetime.utcfromtimestamp(t) -
491 delta = (datetime.datetime.utcfromtimestamp(t) -
490 datetime.datetime.fromtimestamp(t))
492 datetime.datetime.fromtimestamp(t))
491 tz = delta.days * 86400 + delta.seconds
493 tz = delta.days * 86400 + delta.seconds
492 return t, tz
494 return t, tz
@@ -1,608 +1,611 b''
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 from __future__ import absolute_import
7 from __future__ import absolute_import
8
8
9 import os
9 import os
10 import shlex
10 import shlex
11 import shutil
11 import shutil
12
12
13 from mercurial import (
13 from mercurial import (
14 encoding,
14 encoding,
15 error,
15 error,
16 hg,
16 hg,
17 util,
17 util,
18 )
18 )
19 from mercurial.i18n import _
19 from mercurial.i18n import _
20
20
21 from . import (
21 from . import (
22 bzr,
22 bzr,
23 common,
23 common,
24 cvs,
24 cvs,
25 darcs,
25 darcs,
26 filemap,
26 filemap,
27 git,
27 git,
28 gnuarch,
28 gnuarch,
29 hg as hgconvert,
29 hg as hgconvert,
30 monotone,
30 monotone,
31 p4,
31 p4,
32 subversion,
32 subversion,
33 )
33 )
34
34
35 mapfile = common.mapfile
35 mapfile = common.mapfile
36 MissingTool = common.MissingTool
36 MissingTool = common.MissingTool
37 NoRepo = common.NoRepo
37 NoRepo = common.NoRepo
38 SKIPREV = common.SKIPREV
38 SKIPREV = common.SKIPREV
39
39
40 bzr_source = bzr.bzr_source
40 bzr_source = bzr.bzr_source
41 convert_cvs = cvs.convert_cvs
41 convert_cvs = cvs.convert_cvs
42 convert_git = git.convert_git
42 convert_git = git.convert_git
43 darcs_source = darcs.darcs_source
43 darcs_source = darcs.darcs_source
44 gnuarch_source = gnuarch.gnuarch_source
44 gnuarch_source = gnuarch.gnuarch_source
45 mercurial_sink = hgconvert.mercurial_sink
45 mercurial_sink = hgconvert.mercurial_sink
46 mercurial_source = hgconvert.mercurial_source
46 mercurial_source = hgconvert.mercurial_source
47 monotone_source = monotone.monotone_source
47 monotone_source = monotone.monotone_source
48 p4_source = p4.p4_source
48 p4_source = p4.p4_source
49 svn_sink = subversion.svn_sink
49 svn_sink = subversion.svn_sink
50 svn_source = subversion.svn_source
50 svn_source = subversion.svn_source
51
51
52 orig_encoding = 'ascii'
52 orig_encoding = 'ascii'
53
53
54 def recode(s):
54 def recode(s):
55 if isinstance(s, unicode):
55 if isinstance(s, unicode):
56 return s.encode(orig_encoding, 'replace')
56 return s.encode(orig_encoding, 'replace')
57 else:
57 else:
58 return s.decode('utf-8').encode(orig_encoding, 'replace')
58 return s.decode('utf-8').encode(orig_encoding, 'replace')
59
59
60 def mapbranch(branch, branchmap):
60 def mapbranch(branch, branchmap):
61 '''
61 '''
62 >>> bmap = {'default': 'branch1'}
62 >>> bmap = {'default': 'branch1'}
63 >>> for i in ['', None]:
63 >>> for i in ['', None]:
64 ... mapbranch(i, bmap)
64 ... mapbranch(i, bmap)
65 'branch1'
65 'branch1'
66 'branch1'
66 'branch1'
67 >>> bmap = {'None': 'branch2'}
67 >>> bmap = {'None': 'branch2'}
68 >>> for i in ['', None]:
68 >>> for i in ['', None]:
69 ... mapbranch(i, bmap)
69 ... mapbranch(i, bmap)
70 'branch2'
70 'branch2'
71 'branch2'
71 'branch2'
72 >>> bmap = {'None': 'branch3', 'default': 'branch4'}
72 >>> bmap = {'None': 'branch3', 'default': 'branch4'}
73 >>> for i in ['None', '', None, 'default', 'branch5']:
73 >>> for i in ['None', '', None, 'default', 'branch5']:
74 ... mapbranch(i, bmap)
74 ... mapbranch(i, bmap)
75 'branch3'
75 'branch3'
76 'branch4'
76 'branch4'
77 'branch4'
77 'branch4'
78 'branch4'
78 'branch4'
79 'branch5'
79 'branch5'
80 '''
80 '''
81 # If branch is None or empty, this commit is coming from the source
81 # If branch is None or empty, this commit is coming from the source
82 # repository's default branch and destined for the default branch in the
82 # repository's default branch and destined for the default branch in the
83 # destination repository. For such commits, using a literal "default"
83 # destination repository. For such commits, using a literal "default"
84 # in branchmap below allows the user to map "default" to an alternate
84 # in branchmap below allows the user to map "default" to an alternate
85 # default branch in the destination repository.
85 # default branch in the destination repository.
86 branch = branchmap.get(branch or 'default', branch)
86 branch = branchmap.get(branch or 'default', branch)
87 # At some point we used "None" literal to denote the default branch,
87 # At some point we used "None" literal to denote the default branch,
88 # attempt to use that for backward compatibility.
88 # attempt to use that for backward compatibility.
89 if (not branch):
89 if (not branch):
90 branch = branchmap.get(str(None), branch)
90 branch = branchmap.get(str(None), branch)
91 return branch
91 return branch
92
92
93 source_converters = [
93 source_converters = [
94 ('cvs', convert_cvs, 'branchsort'),
94 ('cvs', convert_cvs, 'branchsort'),
95 ('git', convert_git, 'branchsort'),
95 ('git', convert_git, 'branchsort'),
96 ('svn', svn_source, 'branchsort'),
96 ('svn', svn_source, 'branchsort'),
97 ('hg', mercurial_source, 'sourcesort'),
97 ('hg', mercurial_source, 'sourcesort'),
98 ('darcs', darcs_source, 'branchsort'),
98 ('darcs', darcs_source, 'branchsort'),
99 ('mtn', monotone_source, 'branchsort'),
99 ('mtn', monotone_source, 'branchsort'),
100 ('gnuarch', gnuarch_source, 'branchsort'),
100 ('gnuarch', gnuarch_source, 'branchsort'),
101 ('bzr', bzr_source, 'branchsort'),
101 ('bzr', bzr_source, 'branchsort'),
102 ('p4', p4_source, 'branchsort'),
102 ('p4', p4_source, 'branchsort'),
103 ]
103 ]
104
104
105 sink_converters = [
105 sink_converters = [
106 ('hg', mercurial_sink),
106 ('hg', mercurial_sink),
107 ('svn', svn_sink),
107 ('svn', svn_sink),
108 ]
108 ]
109
109
110 def convertsource(ui, path, type, revs):
110 def convertsource(ui, path, type, revs):
111 exceptions = []
111 exceptions = []
112 if type and type not in [s[0] for s in source_converters]:
112 if type and type not in [s[0] for s in source_converters]:
113 raise error.Abort(_('%s: invalid source repository type') % type)
113 raise error.Abort(_('%s: invalid source repository type') % type)
114 for name, source, sortmode in source_converters:
114 for name, source, sortmode in source_converters:
115 try:
115 try:
116 if not type or name == type:
116 if not type or name == type:
117 return source(ui, path, revs), sortmode
117 return source(ui, path, revs), sortmode
118 except (NoRepo, MissingTool) as inst:
118 except (NoRepo, MissingTool) as inst:
119 exceptions.append(inst)
119 exceptions.append(inst)
120 if not ui.quiet:
120 if not ui.quiet:
121 for inst in exceptions:
121 for inst in exceptions:
122 ui.write("%s\n" % inst)
122 ui.write("%s\n" % inst)
123 raise error.Abort(_('%s: missing or unsupported repository') % path)
123 raise error.Abort(_('%s: missing or unsupported repository') % path)
124
124
125 def convertsink(ui, path, type):
125 def convertsink(ui, path, type):
126 if type and type not in [s[0] for s in sink_converters]:
126 if type and type not in [s[0] for s in sink_converters]:
127 raise error.Abort(_('%s: invalid destination repository type') % type)
127 raise error.Abort(_('%s: invalid destination repository type') % type)
128 for name, sink in sink_converters:
128 for name, sink in sink_converters:
129 try:
129 try:
130 if not type or name == type:
130 if not type or name == type:
131 return sink(ui, path)
131 return sink(ui, path)
132 except NoRepo as inst:
132 except NoRepo as inst:
133 ui.note(_("convert: %s\n") % inst)
133 ui.note(_("convert: %s\n") % inst)
134 except MissingTool as inst:
134 except MissingTool as inst:
135 raise error.Abort('%s\n' % inst)
135 raise error.Abort('%s\n' % inst)
136 raise error.Abort(_('%s: unknown repository type') % path)
136 raise error.Abort(_('%s: unknown repository type') % path)
137
137
138 class progresssource(object):
138 class progresssource(object):
139 def __init__(self, ui, source, filecount):
139 def __init__(self, ui, source, filecount):
140 self.ui = ui
140 self.ui = ui
141 self.source = source
141 self.source = source
142 self.filecount = filecount
142 self.filecount = filecount
143 self.retrieved = 0
143 self.retrieved = 0
144
144
145 def getfile(self, file, rev):
145 def getfile(self, file, rev):
146 self.retrieved += 1
146 self.retrieved += 1
147 self.ui.progress(_('getting files'), self.retrieved,
147 self.ui.progress(_('getting files'), self.retrieved,
148 item=file, total=self.filecount, unit=_('files'))
148 item=file, total=self.filecount, unit=_('files'))
149 return self.source.getfile(file, rev)
149 return self.source.getfile(file, rev)
150
150
151 def targetfilebelongstosource(self, targetfilename):
151 def targetfilebelongstosource(self, targetfilename):
152 return self.source.targetfilebelongstosource(targetfilename)
152 return self.source.targetfilebelongstosource(targetfilename)
153
153
154 def lookuprev(self, rev):
154 def lookuprev(self, rev):
155 return self.source.lookuprev(rev)
155 return self.source.lookuprev(rev)
156
156
157 def close(self):
157 def close(self):
158 self.ui.progress(_('getting files'), None)
158 self.ui.progress(_('getting files'), None)
159
159
160 class converter(object):
160 class converter(object):
161 def __init__(self, ui, source, dest, revmapfile, opts):
161 def __init__(self, ui, source, dest, revmapfile, opts):
162
162
163 self.source = source
163 self.source = source
164 self.dest = dest
164 self.dest = dest
165 self.ui = ui
165 self.ui = ui
166 self.opts = opts
166 self.opts = opts
167 self.commitcache = {}
167 self.commitcache = {}
168 self.authors = {}
168 self.authors = {}
169 self.authorfile = None
169 self.authorfile = None
170
170
171 # Record converted revisions persistently: maps source revision
171 # Record converted revisions persistently: maps source revision
172 # ID to target revision ID (both strings). (This is how
172 # ID to target revision ID (both strings). (This is how
173 # incremental conversions work.)
173 # incremental conversions work.)
174 self.map = mapfile(ui, revmapfile)
174 self.map = mapfile(ui, revmapfile)
175
175
176 # Read first the dst author map if any
176 # Read first the dst author map if any
177 authorfile = self.dest.authorfile()
177 authorfile = self.dest.authorfile()
178 if authorfile and os.path.exists(authorfile):
178 if authorfile and os.path.exists(authorfile):
179 self.readauthormap(authorfile)
179 self.readauthormap(authorfile)
180 # Extend/Override with new author map if necessary
180 # Extend/Override with new author map if necessary
181 if opts.get('authormap'):
181 if opts.get('authormap'):
182 self.readauthormap(opts.get('authormap'))
182 self.readauthormap(opts.get('authormap'))
183 self.authorfile = self.dest.authorfile()
183 self.authorfile = self.dest.authorfile()
184
184
185 self.splicemap = self.parsesplicemap(opts.get('splicemap'))
185 self.splicemap = self.parsesplicemap(opts.get('splicemap'))
186 self.branchmap = mapfile(ui, opts.get('branchmap'))
186 self.branchmap = mapfile(ui, opts.get('branchmap'))
187
187
188 def parsesplicemap(self, path):
188 def parsesplicemap(self, path):
189 """ check and validate the splicemap format and
189 """ check and validate the splicemap format and
190 return a child/parents dictionary.
190 return a child/parents dictionary.
191 Format checking has two parts.
191 Format checking has two parts.
192 1. generic format which is same across all source types
192 1. generic format which is same across all source types
193 2. specific format checking which may be different for
193 2. specific format checking which may be different for
194 different source type. This logic is implemented in
194 different source type. This logic is implemented in
195 checkrevformat function in source files like
195 checkrevformat function in source files like
196 hg.py, subversion.py etc.
196 hg.py, subversion.py etc.
197 """
197 """
198
198
199 if not path:
199 if not path:
200 return {}
200 return {}
201 m = {}
201 m = {}
202 try:
202 try:
203 fp = open(path, 'r')
203 fp = open(path, 'r')
204 for i, line in enumerate(fp):
204 for i, line in enumerate(fp):
205 line = line.splitlines()[0].rstrip()
205 line = line.splitlines()[0].rstrip()
206 if not line:
206 if not line:
207 # Ignore blank lines
207 # Ignore blank lines
208 continue
208 continue
209 # split line
209 # split line
210 lex = shlex.shlex(line, posix=True)
210 lex = shlex.shlex(line, posix=True)
211 lex.whitespace_split = True
211 lex.whitespace_split = True
212 lex.whitespace += ','
212 lex.whitespace += ','
213 line = list(lex)
213 line = list(lex)
214 # check number of parents
214 # check number of parents
215 if not (2 <= len(line) <= 3):
215 if not (2 <= len(line) <= 3):
216 raise error.Abort(_('syntax error in %s(%d): child parent1'
216 raise error.Abort(_('syntax error in %s(%d): child parent1'
217 '[,parent2] expected') % (path, i + 1))
217 '[,parent2] expected') % (path, i + 1))
218 for part in line:
218 for part in line:
219 self.source.checkrevformat(part)
219 self.source.checkrevformat(part)
220 child, p1, p2 = line[0], line[1:2], line[2:]
220 child, p1, p2 = line[0], line[1:2], line[2:]
221 if p1 == p2:
221 if p1 == p2:
222 m[child] = p1
222 m[child] = p1
223 else:
223 else:
224 m[child] = p1 + p2
224 m[child] = p1 + p2
225 # if file does not exist or error reading, exit
225 # if file does not exist or error reading, exit
226 except IOError:
226 except IOError:
227 raise error.Abort(_('splicemap file not found or error reading %s:')
227 raise error.Abort(_('splicemap file not found or error reading %s:')
228 % path)
228 % path)
229 return m
229 return m
230
230
231
231
232 def walktree(self, heads):
232 def walktree(self, heads):
233 '''Return a mapping that identifies the uncommitted parents of every
233 '''Return a mapping that identifies the uncommitted parents of every
234 uncommitted changeset.'''
234 uncommitted changeset.'''
235 visit = heads
235 visit = heads
236 known = set()
236 known = set()
237 parents = {}
237 parents = {}
238 numcommits = self.source.numcommits()
238 numcommits = self.source.numcommits()
239 while visit:
239 while visit:
240 n = visit.pop(0)
240 n = visit.pop(0)
241 if n in known:
241 if n in known:
242 continue
242 continue
243 if n in self.map:
243 if n in self.map:
244 m = self.map[n]
244 m = self.map[n]
245 if m == SKIPREV or self.dest.hascommitfrommap(m):
245 if m == SKIPREV or self.dest.hascommitfrommap(m):
246 continue
246 continue
247 known.add(n)
247 known.add(n)
248 self.ui.progress(_('scanning'), len(known), unit=_('revisions'),
248 self.ui.progress(_('scanning'), len(known), unit=_('revisions'),
249 total=numcommits)
249 total=numcommits)
250 commit = self.cachecommit(n)
250 commit = self.cachecommit(n)
251 parents[n] = []
251 parents[n] = []
252 for p in commit.parents:
252 for p in commit.parents:
253 parents[n].append(p)
253 parents[n].append(p)
254 visit.append(p)
254 visit.append(p)
255 self.ui.progress(_('scanning'), None)
255 self.ui.progress(_('scanning'), None)
256
256
257 return parents
257 return parents
258
258
259 def mergesplicemap(self, parents, splicemap):
259 def mergesplicemap(self, parents, splicemap):
260 """A splicemap redefines child/parent relationships. Check the
260 """A splicemap redefines child/parent relationships. Check the
261 map contains valid revision identifiers and merge the new
261 map contains valid revision identifiers and merge the new
262 links in the source graph.
262 links in the source graph.
263 """
263 """
264 for c in sorted(splicemap):
264 for c in sorted(splicemap):
265 if c not in parents:
265 if c not in parents:
266 if not self.dest.hascommitforsplicemap(self.map.get(c, c)):
266 if not self.dest.hascommitforsplicemap(self.map.get(c, c)):
267 # Could be in source but not converted during this run
267 # Could be in source but not converted during this run
268 self.ui.warn(_('splice map revision %s is not being '
268 self.ui.warn(_('splice map revision %s is not being '
269 'converted, ignoring\n') % c)
269 'converted, ignoring\n') % c)
270 continue
270 continue
271 pc = []
271 pc = []
272 for p in splicemap[c]:
272 for p in splicemap[c]:
273 # We do not have to wait for nodes already in dest.
273 # We do not have to wait for nodes already in dest.
274 if self.dest.hascommitforsplicemap(self.map.get(p, p)):
274 if self.dest.hascommitforsplicemap(self.map.get(p, p)):
275 continue
275 continue
276 # Parent is not in dest and not being converted, not good
276 # Parent is not in dest and not being converted, not good
277 if p not in parents:
277 if p not in parents:
278 raise error.Abort(_('unknown splice map parent: %s') % p)
278 raise error.Abort(_('unknown splice map parent: %s') % p)
279 pc.append(p)
279 pc.append(p)
280 parents[c] = pc
280 parents[c] = pc
281
281
282 def toposort(self, parents, sortmode):
282 def toposort(self, parents, sortmode):
283 '''Return an ordering such that every uncommitted changeset is
283 '''Return an ordering such that every uncommitted changeset is
284 preceded by all its uncommitted ancestors.'''
284 preceded by all its uncommitted ancestors.'''
285
285
286 def mapchildren(parents):
286 def mapchildren(parents):
287 """Return a (children, roots) tuple where 'children' maps parent
287 """Return a (children, roots) tuple where 'children' maps parent
288 revision identifiers to children ones, and 'roots' is the list of
288 revision identifiers to children ones, and 'roots' is the list of
289 revisions without parents. 'parents' must be a mapping of revision
289 revisions without parents. 'parents' must be a mapping of revision
290 identifier to its parents ones.
290 identifier to its parents ones.
291 """
291 """
292 visit = sorted(parents)
292 visit = sorted(parents)
293 seen = set()
293 seen = set()
294 children = {}
294 children = {}
295 roots = []
295 roots = []
296
296
297 while visit:
297 while visit:
298 n = visit.pop(0)
298 n = visit.pop(0)
299 if n in seen:
299 if n in seen:
300 continue
300 continue
301 seen.add(n)
301 seen.add(n)
302 # Ensure that nodes without parents are present in the
302 # Ensure that nodes without parents are present in the
303 # 'children' mapping.
303 # 'children' mapping.
304 children.setdefault(n, [])
304 children.setdefault(n, [])
305 hasparent = False
305 hasparent = False
306 for p in parents[n]:
306 for p in parents[n]:
307 if p not in self.map:
307 if p not in self.map:
308 visit.append(p)
308 visit.append(p)
309 hasparent = True
309 hasparent = True
310 children.setdefault(p, []).append(n)
310 children.setdefault(p, []).append(n)
311 if not hasparent:
311 if not hasparent:
312 roots.append(n)
312 roots.append(n)
313
313
314 return children, roots
314 return children, roots
315
315
316 # Sort functions are supposed to take a list of revisions which
316 # Sort functions are supposed to take a list of revisions which
317 # can be converted immediately and pick one
317 # can be converted immediately and pick one
318
318
319 def makebranchsorter():
319 def makebranchsorter():
320 """If the previously converted revision has a child in the
320 """If the previously converted revision has a child in the
321 eligible revisions list, pick it. Return the list head
321 eligible revisions list, pick it. Return the list head
322 otherwise. Branch sort attempts to minimize branch
322 otherwise. Branch sort attempts to minimize branch
323 switching, which is harmful for Mercurial backend
323 switching, which is harmful for Mercurial backend
324 compression.
324 compression.
325 """
325 """
326 prev = [None]
326 prev = [None]
327 def picknext(nodes):
327 def picknext(nodes):
328 next = nodes[0]
328 next = nodes[0]
329 for n in nodes:
329 for n in nodes:
330 if prev[0] in parents[n]:
330 if prev[0] in parents[n]:
331 next = n
331 next = n
332 break
332 break
333 prev[0] = next
333 prev[0] = next
334 return next
334 return next
335 return picknext
335 return picknext
336
336
337 def makesourcesorter():
337 def makesourcesorter():
338 """Source specific sort."""
338 """Source specific sort."""
339 keyfn = lambda n: self.commitcache[n].sortkey
339 keyfn = lambda n: self.commitcache[n].sortkey
340 def picknext(nodes):
340 def picknext(nodes):
341 return sorted(nodes, key=keyfn)[0]
341 return sorted(nodes, key=keyfn)[0]
342 return picknext
342 return picknext
343
343
344 def makeclosesorter():
344 def makeclosesorter():
345 """Close order sort."""
345 """Close order sort."""
346 keyfn = lambda n: ('close' not in self.commitcache[n].extra,
346 keyfn = lambda n: ('close' not in self.commitcache[n].extra,
347 self.commitcache[n].sortkey)
347 self.commitcache[n].sortkey)
348 def picknext(nodes):
348 def picknext(nodes):
349 return sorted(nodes, key=keyfn)[0]
349 return sorted(nodes, key=keyfn)[0]
350 return picknext
350 return picknext
351
351
352 def makedatesorter():
352 def makedatesorter():
353 """Sort revisions by date."""
353 """Sort revisions by date."""
354 dates = {}
354 dates = {}
355 def getdate(n):
355 def getdate(n):
356 if n not in dates:
356 if n not in dates:
357 dates[n] = util.parsedate(self.commitcache[n].date)
357 dates[n] = util.parsedate(self.commitcache[n].date)
358 return dates[n]
358 return dates[n]
359
359
360 def picknext(nodes):
360 def picknext(nodes):
361 return min([(getdate(n), n) for n in nodes])[1]
361 return min([(getdate(n), n) for n in nodes])[1]
362
362
363 return picknext
363 return picknext
364
364
365 if sortmode == 'branchsort':
365 if sortmode == 'branchsort':
366 picknext = makebranchsorter()
366 picknext = makebranchsorter()
367 elif sortmode == 'datesort':
367 elif sortmode == 'datesort':
368 picknext = makedatesorter()
368 picknext = makedatesorter()
369 elif sortmode == 'sourcesort':
369 elif sortmode == 'sourcesort':
370 picknext = makesourcesorter()
370 picknext = makesourcesorter()
371 elif sortmode == 'closesort':
371 elif sortmode == 'closesort':
372 picknext = makeclosesorter()
372 picknext = makeclosesorter()
373 else:
373 else:
374 raise error.Abort(_('unknown sort mode: %s') % sortmode)
374 raise error.Abort(_('unknown sort mode: %s') % sortmode)
375
375
376 children, actives = mapchildren(parents)
376 children, actives = mapchildren(parents)
377
377
378 s = []
378 s = []
379 pendings = {}
379 pendings = {}
380 while actives:
380 while actives:
381 n = picknext(actives)
381 n = picknext(actives)
382 actives.remove(n)
382 actives.remove(n)
383 s.append(n)
383 s.append(n)
384
384
385 # Update dependents list
385 # Update dependents list
386 for c in children.get(n, []):
386 for c in children.get(n, []):
387 if c not in pendings:
387 if c not in pendings:
388 pendings[c] = [p for p in parents[c] if p not in self.map]
388 pendings[c] = [p for p in parents[c] if p not in self.map]
389 try:
389 try:
390 pendings[c].remove(n)
390 pendings[c].remove(n)
391 except ValueError:
391 except ValueError:
392 raise error.Abort(_('cycle detected between %s and %s')
392 raise error.Abort(_('cycle detected between %s and %s')
393 % (recode(c), recode(n)))
393 % (recode(c), recode(n)))
394 if not pendings[c]:
394 if not pendings[c]:
395 # Parents are converted, node is eligible
395 # Parents are converted, node is eligible
396 actives.insert(0, c)
396 actives.insert(0, c)
397 pendings[c] = None
397 pendings[c] = None
398
398
399 if len(s) != len(parents):
399 if len(s) != len(parents):
400 raise error.Abort(_("not all revisions were sorted"))
400 raise error.Abort(_("not all revisions were sorted"))
401
401
402 return s
402 return s
403
403
404 def writeauthormap(self):
404 def writeauthormap(self):
405 authorfile = self.authorfile
405 authorfile = self.authorfile
406 if authorfile:
406 if authorfile:
407 self.ui.status(_('writing author map file %s\n') % authorfile)
407 self.ui.status(_('writing author map file %s\n') % authorfile)
408 ofile = open(authorfile, 'w+')
408 ofile = open(authorfile, 'w+')
409 for author in self.authors:
409 for author in self.authors:
410 ofile.write("%s=%s\n" % (author, self.authors[author]))
410 ofile.write("%s=%s\n" % (author, self.authors[author]))
411 ofile.close()
411 ofile.close()
412
412
413 def readauthormap(self, authorfile):
413 def readauthormap(self, authorfile):
414 afile = open(authorfile, 'r')
414 afile = open(authorfile, 'r')
415 for line in afile:
415 for line in afile:
416
416
417 line = line.strip()
417 line = line.strip()
418 if not line or line.startswith('#'):
418 if not line or line.startswith('#'):
419 continue
419 continue
420
420
421 try:
421 try:
422 srcauthor, dstauthor = line.split('=', 1)
422 srcauthor, dstauthor = line.split('=', 1)
423 except ValueError:
423 except ValueError:
424 msg = _('ignoring bad line in author map file %s: %s\n')
424 msg = _('ignoring bad line in author map file %s: %s\n')
425 self.ui.warn(msg % (authorfile, line.rstrip()))
425 self.ui.warn(msg % (authorfile, line.rstrip()))
426 continue
426 continue
427
427
428 srcauthor = srcauthor.strip()
428 srcauthor = srcauthor.strip()
429 dstauthor = dstauthor.strip()
429 dstauthor = dstauthor.strip()
430 if self.authors.get(srcauthor) in (None, dstauthor):
430 if self.authors.get(srcauthor) in (None, dstauthor):
431 msg = _('mapping author %s to %s\n')
431 msg = _('mapping author %s to %s\n')
432 self.ui.debug(msg % (srcauthor, dstauthor))
432 self.ui.debug(msg % (srcauthor, dstauthor))
433 self.authors[srcauthor] = dstauthor
433 self.authors[srcauthor] = dstauthor
434 continue
434 continue
435
435
436 m = _('overriding mapping for author %s, was %s, will be %s\n')
436 m = _('overriding mapping for author %s, was %s, will be %s\n')
437 self.ui.status(m % (srcauthor, self.authors[srcauthor], dstauthor))
437 self.ui.status(m % (srcauthor, self.authors[srcauthor], dstauthor))
438
438
439 afile.close()
439 afile.close()
440
440
441 def cachecommit(self, rev):
441 def cachecommit(self, rev):
442 commit = self.source.getcommit(rev)
442 commit = self.source.getcommit(rev)
443 commit.author = self.authors.get(commit.author, commit.author)
443 commit.author = self.authors.get(commit.author, commit.author)
444 commit.branch = mapbranch(commit.branch, self.branchmap)
444 commit.branch = mapbranch(commit.branch, self.branchmap)
445 self.commitcache[rev] = commit
445 self.commitcache[rev] = commit
446 return commit
446 return commit
447
447
448 def copy(self, rev):
448 def copy(self, rev):
449 commit = self.commitcache[rev]
449 commit = self.commitcache[rev]
450 full = self.opts.get('full')
450 full = self.opts.get('full')
451 changes = self.source.getchanges(rev, full)
451 changes = self.source.getchanges(rev, full)
452 if isinstance(changes, basestring):
452 if isinstance(changes, basestring):
453 if changes == SKIPREV:
453 if changes == SKIPREV:
454 dest = SKIPREV
454 dest = SKIPREV
455 else:
455 else:
456 dest = self.map[changes]
456 dest = self.map[changes]
457 self.map[rev] = dest
457 self.map[rev] = dest
458 return
458 return
459 files, copies, cleanp2 = changes
459 files, copies, cleanp2 = changes
460 pbranches = []
460 pbranches = []
461 if commit.parents:
461 if commit.parents:
462 for prev in commit.parents:
462 for prev in commit.parents:
463 if prev not in self.commitcache:
463 if prev not in self.commitcache:
464 self.cachecommit(prev)
464 self.cachecommit(prev)
465 pbranches.append((self.map[prev],
465 pbranches.append((self.map[prev],
466 self.commitcache[prev].branch))
466 self.commitcache[prev].branch))
467 self.dest.setbranch(commit.branch, pbranches)
467 self.dest.setbranch(commit.branch, pbranches)
468 try:
468 try:
469 parents = self.splicemap[rev]
469 parents = self.splicemap[rev]
470 self.ui.status(_('spliced in %s as parents of %s\n') %
470 self.ui.status(_('spliced in %s as parents of %s\n') %
471 (_(' and ').join(parents), rev))
471 (_(' and ').join(parents), rev))
472 parents = [self.map.get(p, p) for p in parents]
472 parents = [self.map.get(p, p) for p in parents]
473 except KeyError:
473 except KeyError:
474 parents = [b[0] for b in pbranches]
474 parents = [b[0] for b in pbranches]
475 parents.extend(self.map[x]
476 for x in commit.optparents
477 if x in self.map)
475 if len(pbranches) != 2:
478 if len(pbranches) != 2:
476 cleanp2 = set()
479 cleanp2 = set()
477 if len(parents) < 3:
480 if len(parents) < 3:
478 source = progresssource(self.ui, self.source, len(files))
481 source = progresssource(self.ui, self.source, len(files))
479 else:
482 else:
480 # For an octopus merge, we end up traversing the list of
483 # For an octopus merge, we end up traversing the list of
481 # changed files N-1 times. This tweak to the number of
484 # changed files N-1 times. This tweak to the number of
482 # files makes it so the progress bar doesn't overflow
485 # files makes it so the progress bar doesn't overflow
483 # itself.
486 # itself.
484 source = progresssource(self.ui, self.source,
487 source = progresssource(self.ui, self.source,
485 len(files) * (len(parents) - 1))
488 len(files) * (len(parents) - 1))
486 newnode = self.dest.putcommit(files, copies, parents, commit,
489 newnode = self.dest.putcommit(files, copies, parents, commit,
487 source, self.map, full, cleanp2)
490 source, self.map, full, cleanp2)
488 source.close()
491 source.close()
489 self.source.converted(rev, newnode)
492 self.source.converted(rev, newnode)
490 self.map[rev] = newnode
493 self.map[rev] = newnode
491
494
492 def convert(self, sortmode):
495 def convert(self, sortmode):
493 try:
496 try:
494 self.source.before()
497 self.source.before()
495 self.dest.before()
498 self.dest.before()
496 self.source.setrevmap(self.map)
499 self.source.setrevmap(self.map)
497 self.ui.status(_("scanning source...\n"))
500 self.ui.status(_("scanning source...\n"))
498 heads = self.source.getheads()
501 heads = self.source.getheads()
499 parents = self.walktree(heads)
502 parents = self.walktree(heads)
500 self.mergesplicemap(parents, self.splicemap)
503 self.mergesplicemap(parents, self.splicemap)
501 self.ui.status(_("sorting...\n"))
504 self.ui.status(_("sorting...\n"))
502 t = self.toposort(parents, sortmode)
505 t = self.toposort(parents, sortmode)
503 num = len(t)
506 num = len(t)
504 c = None
507 c = None
505
508
506 self.ui.status(_("converting...\n"))
509 self.ui.status(_("converting...\n"))
507 for i, c in enumerate(t):
510 for i, c in enumerate(t):
508 num -= 1
511 num -= 1
509 desc = self.commitcache[c].desc
512 desc = self.commitcache[c].desc
510 if "\n" in desc:
513 if "\n" in desc:
511 desc = desc.splitlines()[0]
514 desc = desc.splitlines()[0]
512 # convert log message to local encoding without using
515 # convert log message to local encoding without using
513 # tolocal() because the encoding.encoding convert()
516 # tolocal() because the encoding.encoding convert()
514 # uses is 'utf-8'
517 # uses is 'utf-8'
515 self.ui.status("%d %s\n" % (num, recode(desc)))
518 self.ui.status("%d %s\n" % (num, recode(desc)))
516 self.ui.note(_("source: %s\n") % recode(c))
519 self.ui.note(_("source: %s\n") % recode(c))
517 self.ui.progress(_('converting'), i, unit=_('revisions'),
520 self.ui.progress(_('converting'), i, unit=_('revisions'),
518 total=len(t))
521 total=len(t))
519 self.copy(c)
522 self.copy(c)
520 self.ui.progress(_('converting'), None)
523 self.ui.progress(_('converting'), None)
521
524
522 if not self.ui.configbool('convert', 'skiptags'):
525 if not self.ui.configbool('convert', 'skiptags'):
523 tags = self.source.gettags()
526 tags = self.source.gettags()
524 ctags = {}
527 ctags = {}
525 for k in tags:
528 for k in tags:
526 v = tags[k]
529 v = tags[k]
527 if self.map.get(v, SKIPREV) != SKIPREV:
530 if self.map.get(v, SKIPREV) != SKIPREV:
528 ctags[k] = self.map[v]
531 ctags[k] = self.map[v]
529
532
530 if c and ctags:
533 if c and ctags:
531 nrev, tagsparent = self.dest.puttags(ctags)
534 nrev, tagsparent = self.dest.puttags(ctags)
532 if nrev and tagsparent:
535 if nrev and tagsparent:
533 # write another hash correspondence to override the
536 # write another hash correspondence to override the
534 # previous one so we don't end up with extra tag heads
537 # previous one so we don't end up with extra tag heads
535 tagsparents = [e for e in self.map.iteritems()
538 tagsparents = [e for e in self.map.iteritems()
536 if e[1] == tagsparent]
539 if e[1] == tagsparent]
537 if tagsparents:
540 if tagsparents:
538 self.map[tagsparents[0][0]] = nrev
541 self.map[tagsparents[0][0]] = nrev
539
542
540 bookmarks = self.source.getbookmarks()
543 bookmarks = self.source.getbookmarks()
541 cbookmarks = {}
544 cbookmarks = {}
542 for k in bookmarks:
545 for k in bookmarks:
543 v = bookmarks[k]
546 v = bookmarks[k]
544 if self.map.get(v, SKIPREV) != SKIPREV:
547 if self.map.get(v, SKIPREV) != SKIPREV:
545 cbookmarks[k] = self.map[v]
548 cbookmarks[k] = self.map[v]
546
549
547 if c and cbookmarks:
550 if c and cbookmarks:
548 self.dest.putbookmarks(cbookmarks)
551 self.dest.putbookmarks(cbookmarks)
549
552
550 self.writeauthormap()
553 self.writeauthormap()
551 finally:
554 finally:
552 self.cleanup()
555 self.cleanup()
553
556
554 def cleanup(self):
557 def cleanup(self):
555 try:
558 try:
556 self.dest.after()
559 self.dest.after()
557 finally:
560 finally:
558 self.source.after()
561 self.source.after()
559 self.map.close()
562 self.map.close()
560
563
561 def convert(ui, src, dest=None, revmapfile=None, **opts):
564 def convert(ui, src, dest=None, revmapfile=None, **opts):
562 global orig_encoding
565 global orig_encoding
563 orig_encoding = encoding.encoding
566 orig_encoding = encoding.encoding
564 encoding.encoding = 'UTF-8'
567 encoding.encoding = 'UTF-8'
565
568
566 # support --authors as an alias for --authormap
569 # support --authors as an alias for --authormap
567 if not opts.get('authormap'):
570 if not opts.get('authormap'):
568 opts['authormap'] = opts.get('authors')
571 opts['authormap'] = opts.get('authors')
569
572
570 if not dest:
573 if not dest:
571 dest = hg.defaultdest(src) + "-hg"
574 dest = hg.defaultdest(src) + "-hg"
572 ui.status(_("assuming destination %s\n") % dest)
575 ui.status(_("assuming destination %s\n") % dest)
573
576
574 destc = convertsink(ui, dest, opts.get('dest_type'))
577 destc = convertsink(ui, dest, opts.get('dest_type'))
575
578
576 try:
579 try:
577 srcc, defaultsort = convertsource(ui, src, opts.get('source_type'),
580 srcc, defaultsort = convertsource(ui, src, opts.get('source_type'),
578 opts.get('rev'))
581 opts.get('rev'))
579 except Exception:
582 except Exception:
580 for path in destc.created:
583 for path in destc.created:
581 shutil.rmtree(path, True)
584 shutil.rmtree(path, True)
582 raise
585 raise
583
586
584 sortmodes = ('branchsort', 'datesort', 'sourcesort', 'closesort')
587 sortmodes = ('branchsort', 'datesort', 'sourcesort', 'closesort')
585 sortmode = [m for m in sortmodes if opts.get(m)]
588 sortmode = [m for m in sortmodes if opts.get(m)]
586 if len(sortmode) > 1:
589 if len(sortmode) > 1:
587 raise error.Abort(_('more than one sort mode specified'))
590 raise error.Abort(_('more than one sort mode specified'))
588 if sortmode:
591 if sortmode:
589 sortmode = sortmode[0]
592 sortmode = sortmode[0]
590 else:
593 else:
591 sortmode = defaultsort
594 sortmode = defaultsort
592
595
593 if sortmode == 'sourcesort' and not srcc.hasnativeorder():
596 if sortmode == 'sourcesort' and not srcc.hasnativeorder():
594 raise error.Abort(_('--sourcesort is not supported by this data source')
597 raise error.Abort(_('--sourcesort is not supported by this data source')
595 )
598 )
596 if sortmode == 'closesort' and not srcc.hasnativeclose():
599 if sortmode == 'closesort' and not srcc.hasnativeclose():
597 raise error.Abort(_('--closesort is not supported by this data source'))
600 raise error.Abort(_('--closesort is not supported by this data source'))
598
601
599 fmap = opts.get('filemap')
602 fmap = opts.get('filemap')
600 if fmap:
603 if fmap:
601 srcc = filemap.filemap_source(ui, srcc, fmap)
604 srcc = filemap.filemap_source(ui, srcc, fmap)
602 destc.setfilemapmode(True)
605 destc.setfilemapmode(True)
603
606
604 if not revmapfile:
607 if not revmapfile:
605 revmapfile = destc.revmapfile()
608 revmapfile = destc.revmapfile()
606
609
607 c = converter(ui, srcc, destc, revmapfile, opts)
610 c = converter(ui, srcc, destc, revmapfile, opts)
608 c.convert(sortmode)
611 c.convert(sortmode)
@@ -1,652 +1,655 b''
1 # hg.py - hg backend for convert extension
1 # hg.py - hg backend for 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 # Notes for hg->hg conversion:
8 # Notes for hg->hg conversion:
9 #
9 #
10 # * Old versions of Mercurial didn't trim the whitespace from the ends
10 # * Old versions of Mercurial didn't trim the whitespace from the ends
11 # of commit messages, but new versions do. Changesets created by
11 # of commit messages, but new versions do. Changesets created by
12 # those older versions, then converted, may thus have different
12 # those older versions, then converted, may thus have different
13 # hashes for changesets that are otherwise identical.
13 # hashes for changesets that are otherwise identical.
14 #
14 #
15 # * Using "--config convert.hg.saverev=true" will make the source
15 # * Using "--config convert.hg.saverev=true" will make the source
16 # identifier to be stored in the converted revision. This will cause
16 # identifier to be stored in the converted revision. This will cause
17 # the converted revision to have a different identity than the
17 # the converted revision to have a different identity than the
18 # source.
18 # source.
19 from __future__ import absolute_import
19 from __future__ import absolute_import
20
20
21 import os
21 import os
22 import re
22 import re
23 import time
23 import time
24
24
25 from mercurial import (
25 from mercurial import (
26 bookmarks,
26 bookmarks,
27 context,
27 context,
28 error,
28 error,
29 exchange,
29 exchange,
30 hg,
30 hg,
31 lock as lockmod,
31 lock as lockmod,
32 merge as mergemod,
32 merge as mergemod,
33 node as nodemod,
33 node as nodemod,
34 phases,
34 phases,
35 scmutil,
35 scmutil,
36 util,
36 util,
37 )
37 )
38 stringio = util.stringio
38 stringio = util.stringio
39
39
40 from mercurial.i18n import _
40 from mercurial.i18n import _
41 from . import common
41 from . import common
42 mapfile = common.mapfile
42 mapfile = common.mapfile
43 NoRepo = common.NoRepo
43 NoRepo = common.NoRepo
44
44
45 sha1re = re.compile(r'\b[0-9a-f]{12,40}\b')
45 sha1re = re.compile(r'\b[0-9a-f]{12,40}\b')
46
46
47 class mercurial_sink(common.converter_sink):
47 class mercurial_sink(common.converter_sink):
48 def __init__(self, ui, path):
48 def __init__(self, ui, path):
49 common.converter_sink.__init__(self, ui, path)
49 common.converter_sink.__init__(self, ui, path)
50 self.branchnames = ui.configbool('convert', 'hg.usebranchnames', True)
50 self.branchnames = ui.configbool('convert', 'hg.usebranchnames', True)
51 self.clonebranches = ui.configbool('convert', 'hg.clonebranches', False)
51 self.clonebranches = ui.configbool('convert', 'hg.clonebranches', False)
52 self.tagsbranch = ui.config('convert', 'hg.tagsbranch', 'default')
52 self.tagsbranch = ui.config('convert', 'hg.tagsbranch', 'default')
53 self.lastbranch = None
53 self.lastbranch = None
54 if os.path.isdir(path) and len(os.listdir(path)) > 0:
54 if os.path.isdir(path) and len(os.listdir(path)) > 0:
55 try:
55 try:
56 self.repo = hg.repository(self.ui, path)
56 self.repo = hg.repository(self.ui, path)
57 if not self.repo.local():
57 if not self.repo.local():
58 raise NoRepo(_('%s is not a local Mercurial repository')
58 raise NoRepo(_('%s is not a local Mercurial repository')
59 % path)
59 % path)
60 except error.RepoError as err:
60 except error.RepoError as err:
61 ui.traceback()
61 ui.traceback()
62 raise NoRepo(err.args[0])
62 raise NoRepo(err.args[0])
63 else:
63 else:
64 try:
64 try:
65 ui.status(_('initializing destination %s repository\n') % path)
65 ui.status(_('initializing destination %s repository\n') % path)
66 self.repo = hg.repository(self.ui, path, create=True)
66 self.repo = hg.repository(self.ui, path, create=True)
67 if not self.repo.local():
67 if not self.repo.local():
68 raise NoRepo(_('%s is not a local Mercurial repository')
68 raise NoRepo(_('%s is not a local Mercurial repository')
69 % path)
69 % path)
70 self.created.append(path)
70 self.created.append(path)
71 except error.RepoError:
71 except error.RepoError:
72 ui.traceback()
72 ui.traceback()
73 raise NoRepo(_("could not create hg repository %s as sink")
73 raise NoRepo(_("could not create hg repository %s as sink")
74 % path)
74 % path)
75 self.lock = None
75 self.lock = None
76 self.wlock = None
76 self.wlock = None
77 self.filemapmode = False
77 self.filemapmode = False
78 self.subrevmaps = {}
78 self.subrevmaps = {}
79
79
80 def before(self):
80 def before(self):
81 self.ui.debug('run hg sink pre-conversion action\n')
81 self.ui.debug('run hg sink pre-conversion action\n')
82 self.wlock = self.repo.wlock()
82 self.wlock = self.repo.wlock()
83 self.lock = self.repo.lock()
83 self.lock = self.repo.lock()
84
84
85 def after(self):
85 def after(self):
86 self.ui.debug('run hg sink post-conversion action\n')
86 self.ui.debug('run hg sink post-conversion action\n')
87 if self.lock:
87 if self.lock:
88 self.lock.release()
88 self.lock.release()
89 if self.wlock:
89 if self.wlock:
90 self.wlock.release()
90 self.wlock.release()
91
91
92 def revmapfile(self):
92 def revmapfile(self):
93 return self.repo.join("shamap")
93 return self.repo.join("shamap")
94
94
95 def authorfile(self):
95 def authorfile(self):
96 return self.repo.join("authormap")
96 return self.repo.join("authormap")
97
97
98 def setbranch(self, branch, pbranches):
98 def setbranch(self, branch, pbranches):
99 if not self.clonebranches:
99 if not self.clonebranches:
100 return
100 return
101
101
102 setbranch = (branch != self.lastbranch)
102 setbranch = (branch != self.lastbranch)
103 self.lastbranch = branch
103 self.lastbranch = branch
104 if not branch:
104 if not branch:
105 branch = 'default'
105 branch = 'default'
106 pbranches = [(b[0], b[1] and b[1] or 'default') for b in pbranches]
106 pbranches = [(b[0], b[1] and b[1] or 'default') for b in pbranches]
107 if pbranches:
107 if pbranches:
108 pbranch = pbranches[0][1]
108 pbranch = pbranches[0][1]
109 else:
109 else:
110 pbranch = 'default'
110 pbranch = 'default'
111
111
112 branchpath = os.path.join(self.path, branch)
112 branchpath = os.path.join(self.path, branch)
113 if setbranch:
113 if setbranch:
114 self.after()
114 self.after()
115 try:
115 try:
116 self.repo = hg.repository(self.ui, branchpath)
116 self.repo = hg.repository(self.ui, branchpath)
117 except Exception:
117 except Exception:
118 self.repo = hg.repository(self.ui, branchpath, create=True)
118 self.repo = hg.repository(self.ui, branchpath, create=True)
119 self.before()
119 self.before()
120
120
121 # pbranches may bring revisions from other branches (merge parents)
121 # pbranches may bring revisions from other branches (merge parents)
122 # Make sure we have them, or pull them.
122 # Make sure we have them, or pull them.
123 missings = {}
123 missings = {}
124 for b in pbranches:
124 for b in pbranches:
125 try:
125 try:
126 self.repo.lookup(b[0])
126 self.repo.lookup(b[0])
127 except Exception:
127 except Exception:
128 missings.setdefault(b[1], []).append(b[0])
128 missings.setdefault(b[1], []).append(b[0])
129
129
130 if missings:
130 if missings:
131 self.after()
131 self.after()
132 for pbranch, heads in sorted(missings.iteritems()):
132 for pbranch, heads in sorted(missings.iteritems()):
133 pbranchpath = os.path.join(self.path, pbranch)
133 pbranchpath = os.path.join(self.path, pbranch)
134 prepo = hg.peer(self.ui, {}, pbranchpath)
134 prepo = hg.peer(self.ui, {}, pbranchpath)
135 self.ui.note(_('pulling from %s into %s\n') % (pbranch, branch))
135 self.ui.note(_('pulling from %s into %s\n') % (pbranch, branch))
136 exchange.pull(self.repo, prepo,
136 exchange.pull(self.repo, prepo,
137 [prepo.lookup(h) for h in heads])
137 [prepo.lookup(h) for h in heads])
138 self.before()
138 self.before()
139
139
140 def _rewritetags(self, source, revmap, data):
140 def _rewritetags(self, source, revmap, data):
141 fp = stringio()
141 fp = stringio()
142 for line in data.splitlines():
142 for line in data.splitlines():
143 s = line.split(' ', 1)
143 s = line.split(' ', 1)
144 if len(s) != 2:
144 if len(s) != 2:
145 continue
145 continue
146 revid = revmap.get(source.lookuprev(s[0]))
146 revid = revmap.get(source.lookuprev(s[0]))
147 if not revid:
147 if not revid:
148 if s[0] == nodemod.nullhex:
148 if s[0] == nodemod.nullhex:
149 revid = s[0]
149 revid = s[0]
150 else:
150 else:
151 continue
151 continue
152 fp.write('%s %s\n' % (revid, s[1]))
152 fp.write('%s %s\n' % (revid, s[1]))
153 return fp.getvalue()
153 return fp.getvalue()
154
154
155 def _rewritesubstate(self, source, data):
155 def _rewritesubstate(self, source, data):
156 fp = stringio()
156 fp = stringio()
157 for line in data.splitlines():
157 for line in data.splitlines():
158 s = line.split(' ', 1)
158 s = line.split(' ', 1)
159 if len(s) != 2:
159 if len(s) != 2:
160 continue
160 continue
161
161
162 revid = s[0]
162 revid = s[0]
163 subpath = s[1]
163 subpath = s[1]
164 if revid != nodemod.nullhex:
164 if revid != nodemod.nullhex:
165 revmap = self.subrevmaps.get(subpath)
165 revmap = self.subrevmaps.get(subpath)
166 if revmap is None:
166 if revmap is None:
167 revmap = mapfile(self.ui,
167 revmap = mapfile(self.ui,
168 self.repo.wjoin(subpath, '.hg/shamap'))
168 self.repo.wjoin(subpath, '.hg/shamap'))
169 self.subrevmaps[subpath] = revmap
169 self.subrevmaps[subpath] = revmap
170
170
171 # It is reasonable that one or more of the subrepos don't
171 # It is reasonable that one or more of the subrepos don't
172 # need to be converted, in which case they can be cloned
172 # need to be converted, in which case they can be cloned
173 # into place instead of converted. Therefore, only warn
173 # into place instead of converted. Therefore, only warn
174 # once.
174 # once.
175 msg = _('no ".hgsubstate" updates will be made for "%s"\n')
175 msg = _('no ".hgsubstate" updates will be made for "%s"\n')
176 if len(revmap) == 0:
176 if len(revmap) == 0:
177 sub = self.repo.wvfs.reljoin(subpath, '.hg')
177 sub = self.repo.wvfs.reljoin(subpath, '.hg')
178
178
179 if self.repo.wvfs.exists(sub):
179 if self.repo.wvfs.exists(sub):
180 self.ui.warn(msg % subpath)
180 self.ui.warn(msg % subpath)
181
181
182 newid = revmap.get(revid)
182 newid = revmap.get(revid)
183 if not newid:
183 if not newid:
184 if len(revmap) > 0:
184 if len(revmap) > 0:
185 self.ui.warn(_("%s is missing from %s/.hg/shamap\n") %
185 self.ui.warn(_("%s is missing from %s/.hg/shamap\n") %
186 (revid, subpath))
186 (revid, subpath))
187 else:
187 else:
188 revid = newid
188 revid = newid
189
189
190 fp.write('%s %s\n' % (revid, subpath))
190 fp.write('%s %s\n' % (revid, subpath))
191
191
192 return fp.getvalue()
192 return fp.getvalue()
193
193
194 def _calculatemergedfiles(self, source, p1ctx, p2ctx):
194 def _calculatemergedfiles(self, source, p1ctx, p2ctx):
195 """Calculates the files from p2 that we need to pull in when merging p1
195 """Calculates the files from p2 that we need to pull in when merging p1
196 and p2, given that the merge is coming from the given source.
196 and p2, given that the merge is coming from the given source.
197
197
198 This prevents us from losing files that only exist in the target p2 and
198 This prevents us from losing files that only exist in the target p2 and
199 that don't come from the source repo (like if you're merging multiple
199 that don't come from the source repo (like if you're merging multiple
200 repositories together).
200 repositories together).
201 """
201 """
202 anc = [p1ctx.ancestor(p2ctx)]
202 anc = [p1ctx.ancestor(p2ctx)]
203 # Calculate what files are coming from p2
203 # Calculate what files are coming from p2
204 actions, diverge, rename = mergemod.calculateupdates(
204 actions, diverge, rename = mergemod.calculateupdates(
205 self.repo, p1ctx, p2ctx, anc,
205 self.repo, p1ctx, p2ctx, anc,
206 True, # branchmerge
206 True, # branchmerge
207 True, # force
207 True, # force
208 False, # acceptremote
208 False, # acceptremote
209 False, # followcopies
209 False, # followcopies
210 )
210 )
211
211
212 for file, (action, info, msg) in actions.iteritems():
212 for file, (action, info, msg) in actions.iteritems():
213 if source.targetfilebelongstosource(file):
213 if source.targetfilebelongstosource(file):
214 # If the file belongs to the source repo, ignore the p2
214 # If the file belongs to the source repo, ignore the p2
215 # since it will be covered by the existing fileset.
215 # since it will be covered by the existing fileset.
216 continue
216 continue
217
217
218 # If the file requires actual merging, abort. We don't have enough
218 # If the file requires actual merging, abort. We don't have enough
219 # context to resolve merges correctly.
219 # context to resolve merges correctly.
220 if action in ['m', 'dm', 'cd', 'dc']:
220 if action in ['m', 'dm', 'cd', 'dc']:
221 raise error.Abort(_("unable to convert merge commit "
221 raise error.Abort(_("unable to convert merge commit "
222 "since target parents do not merge cleanly (file "
222 "since target parents do not merge cleanly (file "
223 "%s, parents %s and %s)") % (file, p1ctx,
223 "%s, parents %s and %s)") % (file, p1ctx,
224 p2ctx))
224 p2ctx))
225 elif action == 'k':
225 elif action == 'k':
226 # 'keep' means nothing changed from p1
226 # 'keep' means nothing changed from p1
227 continue
227 continue
228 else:
228 else:
229 # Any other change means we want to take the p2 version
229 # Any other change means we want to take the p2 version
230 yield file
230 yield file
231
231
232 def putcommit(self, files, copies, parents, commit, source, revmap, full,
232 def putcommit(self, files, copies, parents, commit, source, revmap, full,
233 cleanp2):
233 cleanp2):
234 files = dict(files)
234 files = dict(files)
235
235
236 def getfilectx(repo, memctx, f):
236 def getfilectx(repo, memctx, f):
237 if p2ctx and f in p2files and f not in copies:
237 if p2ctx and f in p2files and f not in copies:
238 self.ui.debug('reusing %s from p2\n' % f)
238 self.ui.debug('reusing %s from p2\n' % f)
239 try:
239 try:
240 return p2ctx[f]
240 return p2ctx[f]
241 except error.ManifestLookupError:
241 except error.ManifestLookupError:
242 # If the file doesn't exist in p2, then we're syncing a
242 # If the file doesn't exist in p2, then we're syncing a
243 # delete, so just return None.
243 # delete, so just return None.
244 return None
244 return None
245 try:
245 try:
246 v = files[f]
246 v = files[f]
247 except KeyError:
247 except KeyError:
248 return None
248 return None
249 data, mode = source.getfile(f, v)
249 data, mode = source.getfile(f, v)
250 if data is None:
250 if data is None:
251 return None
251 return None
252 if f == '.hgtags':
252 if f == '.hgtags':
253 data = self._rewritetags(source, revmap, data)
253 data = self._rewritetags(source, revmap, data)
254 if f == '.hgsubstate':
254 if f == '.hgsubstate':
255 data = self._rewritesubstate(source, data)
255 data = self._rewritesubstate(source, data)
256 return context.memfilectx(self.repo, f, data, 'l' in mode,
256 return context.memfilectx(self.repo, f, data, 'l' in mode,
257 'x' in mode, copies.get(f))
257 'x' in mode, copies.get(f))
258
258
259 pl = []
259 pl = []
260 for p in parents:
260 for p in parents:
261 if p not in pl:
261 if p not in pl:
262 pl.append(p)
262 pl.append(p)
263 parents = pl
263 parents = pl
264 nparents = len(parents)
264 nparents = len(parents)
265 if self.filemapmode and nparents == 1:
265 if self.filemapmode and nparents == 1:
266 m1node = self.repo.changelog.read(nodemod.bin(parents[0]))[0]
266 m1node = self.repo.changelog.read(nodemod.bin(parents[0]))[0]
267 parent = parents[0]
267 parent = parents[0]
268
268
269 if len(parents) < 2:
269 if len(parents) < 2:
270 parents.append(nodemod.nullid)
270 parents.append(nodemod.nullid)
271 if len(parents) < 2:
271 if len(parents) < 2:
272 parents.append(nodemod.nullid)
272 parents.append(nodemod.nullid)
273 p2 = parents.pop(0)
273 p2 = parents.pop(0)
274
274
275 text = commit.desc
275 text = commit.desc
276
276
277 sha1s = re.findall(sha1re, text)
277 sha1s = re.findall(sha1re, text)
278 for sha1 in sha1s:
278 for sha1 in sha1s:
279 oldrev = source.lookuprev(sha1)
279 oldrev = source.lookuprev(sha1)
280 newrev = revmap.get(oldrev)
280 newrev = revmap.get(oldrev)
281 if newrev is not None:
281 if newrev is not None:
282 text = text.replace(sha1, newrev[:len(sha1)])
282 text = text.replace(sha1, newrev[:len(sha1)])
283
283
284 extra = commit.extra.copy()
284 extra = commit.extra.copy()
285
285
286 sourcename = self.repo.ui.config('convert', 'hg.sourcename')
286 sourcename = self.repo.ui.config('convert', 'hg.sourcename')
287 if sourcename:
287 if sourcename:
288 extra['convert_source'] = sourcename
288 extra['convert_source'] = sourcename
289
289
290 for label in ('source', 'transplant_source', 'rebase_source',
290 for label in ('source', 'transplant_source', 'rebase_source',
291 'intermediate-source'):
291 'intermediate-source'):
292 node = extra.get(label)
292 node = extra.get(label)
293
293
294 if node is None:
294 if node is None:
295 continue
295 continue
296
296
297 # Only transplant stores its reference in binary
297 # Only transplant stores its reference in binary
298 if label == 'transplant_source':
298 if label == 'transplant_source':
299 node = nodemod.hex(node)
299 node = nodemod.hex(node)
300
300
301 newrev = revmap.get(node)
301 newrev = revmap.get(node)
302 if newrev is not None:
302 if newrev is not None:
303 if label == 'transplant_source':
303 if label == 'transplant_source':
304 newrev = nodemod.bin(newrev)
304 newrev = nodemod.bin(newrev)
305
305
306 extra[label] = newrev
306 extra[label] = newrev
307
307
308 if self.branchnames and commit.branch:
308 if self.branchnames and commit.branch:
309 extra['branch'] = commit.branch
309 extra['branch'] = commit.branch
310 if commit.rev and commit.saverev:
310 if commit.rev and commit.saverev:
311 extra['convert_revision'] = commit.rev
311 extra['convert_revision'] = commit.rev
312
312
313 while parents:
313 while parents:
314 p1 = p2
314 p1 = p2
315 p2 = parents.pop(0)
315 p2 = parents.pop(0)
316 p1ctx = self.repo[p1]
316 p1ctx = self.repo[p1]
317 p2ctx = None
317 p2ctx = None
318 if p2 != nodemod.nullid:
318 if p2 != nodemod.nullid:
319 p2ctx = self.repo[p2]
319 p2ctx = self.repo[p2]
320 fileset = set(files)
320 fileset = set(files)
321 if full:
321 if full:
322 fileset.update(self.repo[p1])
322 fileset.update(self.repo[p1])
323 fileset.update(self.repo[p2])
323 fileset.update(self.repo[p2])
324
324
325 if p2ctx:
325 if p2ctx:
326 p2files = set(cleanp2)
326 p2files = set(cleanp2)
327 for file in self._calculatemergedfiles(source, p1ctx, p2ctx):
327 for file in self._calculatemergedfiles(source, p1ctx, p2ctx):
328 p2files.add(file)
328 p2files.add(file)
329 fileset.add(file)
329 fileset.add(file)
330
330
331 ctx = context.memctx(self.repo, (p1, p2), text, fileset,
331 ctx = context.memctx(self.repo, (p1, p2), text, fileset,
332 getfilectx, commit.author, commit.date, extra)
332 getfilectx, commit.author, commit.date, extra)
333
333
334 # We won't know if the conversion changes the node until after the
334 # We won't know if the conversion changes the node until after the
335 # commit, so copy the source's phase for now.
335 # commit, so copy the source's phase for now.
336 self.repo.ui.setconfig('phases', 'new-commit',
336 self.repo.ui.setconfig('phases', 'new-commit',
337 phases.phasenames[commit.phase], 'convert')
337 phases.phasenames[commit.phase], 'convert')
338
338
339 with self.repo.transaction("convert") as tr:
339 with self.repo.transaction("convert") as tr:
340 node = nodemod.hex(self.repo.commitctx(ctx))
340 node = nodemod.hex(self.repo.commitctx(ctx))
341
341
342 # If the node value has changed, but the phase is lower than
342 # If the node value has changed, but the phase is lower than
343 # draft, set it back to draft since it hasn't been exposed
343 # draft, set it back to draft since it hasn't been exposed
344 # anywhere.
344 # anywhere.
345 if commit.rev != node:
345 if commit.rev != node:
346 ctx = self.repo[node]
346 ctx = self.repo[node]
347 if ctx.phase() < phases.draft:
347 if ctx.phase() < phases.draft:
348 phases.retractboundary(self.repo, tr, phases.draft,
348 phases.retractboundary(self.repo, tr, phases.draft,
349 [ctx.node()])
349 [ctx.node()])
350
350
351 text = "(octopus merge fixup)\n"
351 text = "(octopus merge fixup)\n"
352 p2 = node
352 p2 = node
353
353
354 if self.filemapmode and nparents == 1:
354 if self.filemapmode and nparents == 1:
355 man = self.repo.manifest
355 man = self.repo.manifest
356 mnode = self.repo.changelog.read(nodemod.bin(p2))[0]
356 mnode = self.repo.changelog.read(nodemod.bin(p2))[0]
357 closed = 'close' in commit.extra
357 closed = 'close' in commit.extra
358 if not closed and not man.cmp(m1node, man.revision(mnode)):
358 if not closed and not man.cmp(m1node, man.revision(mnode)):
359 self.ui.status(_("filtering out empty revision\n"))
359 self.ui.status(_("filtering out empty revision\n"))
360 self.repo.rollback(force=True)
360 self.repo.rollback(force=True)
361 return parent
361 return parent
362 return p2
362 return p2
363
363
364 def puttags(self, tags):
364 def puttags(self, tags):
365 try:
365 try:
366 parentctx = self.repo[self.tagsbranch]
366 parentctx = self.repo[self.tagsbranch]
367 tagparent = parentctx.node()
367 tagparent = parentctx.node()
368 except error.RepoError:
368 except error.RepoError:
369 parentctx = None
369 parentctx = None
370 tagparent = nodemod.nullid
370 tagparent = nodemod.nullid
371
371
372 oldlines = set()
372 oldlines = set()
373 for branch, heads in self.repo.branchmap().iteritems():
373 for branch, heads in self.repo.branchmap().iteritems():
374 for h in heads:
374 for h in heads:
375 if '.hgtags' in self.repo[h]:
375 if '.hgtags' in self.repo[h]:
376 oldlines.update(
376 oldlines.update(
377 set(self.repo[h]['.hgtags'].data().splitlines(True)))
377 set(self.repo[h]['.hgtags'].data().splitlines(True)))
378 oldlines = sorted(list(oldlines))
378 oldlines = sorted(list(oldlines))
379
379
380 newlines = sorted([("%s %s\n" % (tags[tag], tag)) for tag in tags])
380 newlines = sorted([("%s %s\n" % (tags[tag], tag)) for tag in tags])
381 if newlines == oldlines:
381 if newlines == oldlines:
382 return None, None
382 return None, None
383
383
384 # if the old and new tags match, then there is nothing to update
384 # if the old and new tags match, then there is nothing to update
385 oldtags = set()
385 oldtags = set()
386 newtags = set()
386 newtags = set()
387 for line in oldlines:
387 for line in oldlines:
388 s = line.strip().split(' ', 1)
388 s = line.strip().split(' ', 1)
389 if len(s) != 2:
389 if len(s) != 2:
390 continue
390 continue
391 oldtags.add(s[1])
391 oldtags.add(s[1])
392 for line in newlines:
392 for line in newlines:
393 s = line.strip().split(' ', 1)
393 s = line.strip().split(' ', 1)
394 if len(s) != 2:
394 if len(s) != 2:
395 continue
395 continue
396 if s[1] not in oldtags:
396 if s[1] not in oldtags:
397 newtags.add(s[1].strip())
397 newtags.add(s[1].strip())
398
398
399 if not newtags:
399 if not newtags:
400 return None, None
400 return None, None
401
401
402 data = "".join(newlines)
402 data = "".join(newlines)
403 def getfilectx(repo, memctx, f):
403 def getfilectx(repo, memctx, f):
404 return context.memfilectx(repo, f, data, False, False, None)
404 return context.memfilectx(repo, f, data, False, False, None)
405
405
406 self.ui.status(_("updating tags\n"))
406 self.ui.status(_("updating tags\n"))
407 date = "%s 0" % int(time.mktime(time.gmtime()))
407 date = "%s 0" % int(time.mktime(time.gmtime()))
408 extra = {'branch': self.tagsbranch}
408 extra = {'branch': self.tagsbranch}
409 ctx = context.memctx(self.repo, (tagparent, None), "update tags",
409 ctx = context.memctx(self.repo, (tagparent, None), "update tags",
410 [".hgtags"], getfilectx, "convert-repo", date,
410 [".hgtags"], getfilectx, "convert-repo", date,
411 extra)
411 extra)
412 node = self.repo.commitctx(ctx)
412 node = self.repo.commitctx(ctx)
413 return nodemod.hex(node), nodemod.hex(tagparent)
413 return nodemod.hex(node), nodemod.hex(tagparent)
414
414
415 def setfilemapmode(self, active):
415 def setfilemapmode(self, active):
416 self.filemapmode = active
416 self.filemapmode = active
417
417
418 def putbookmarks(self, updatedbookmark):
418 def putbookmarks(self, updatedbookmark):
419 if not len(updatedbookmark):
419 if not len(updatedbookmark):
420 return
420 return
421 wlock = lock = tr = None
421 wlock = lock = tr = None
422 try:
422 try:
423 wlock = self.repo.wlock()
423 wlock = self.repo.wlock()
424 lock = self.repo.lock()
424 lock = self.repo.lock()
425 tr = self.repo.transaction('bookmark')
425 tr = self.repo.transaction('bookmark')
426 self.ui.status(_("updating bookmarks\n"))
426 self.ui.status(_("updating bookmarks\n"))
427 destmarks = self.repo._bookmarks
427 destmarks = self.repo._bookmarks
428 for bookmark in updatedbookmark:
428 for bookmark in updatedbookmark:
429 destmarks[bookmark] = nodemod.bin(updatedbookmark[bookmark])
429 destmarks[bookmark] = nodemod.bin(updatedbookmark[bookmark])
430 destmarks.recordchange(tr)
430 destmarks.recordchange(tr)
431 tr.close()
431 tr.close()
432 finally:
432 finally:
433 lockmod.release(lock, wlock, tr)
433 lockmod.release(lock, wlock, tr)
434
434
435 def hascommitfrommap(self, rev):
435 def hascommitfrommap(self, rev):
436 # the exact semantics of clonebranches is unclear so we can't say no
436 # the exact semantics of clonebranches is unclear so we can't say no
437 return rev in self.repo or self.clonebranches
437 return rev in self.repo or self.clonebranches
438
438
439 def hascommitforsplicemap(self, rev):
439 def hascommitforsplicemap(self, rev):
440 if rev not in self.repo and self.clonebranches:
440 if rev not in self.repo and self.clonebranches:
441 raise error.Abort(_('revision %s not found in destination '
441 raise error.Abort(_('revision %s not found in destination '
442 'repository (lookups with clonebranches=true '
442 'repository (lookups with clonebranches=true '
443 'are not implemented)') % rev)
443 'are not implemented)') % rev)
444 return rev in self.repo
444 return rev in self.repo
445
445
446 class mercurial_source(common.converter_source):
446 class mercurial_source(common.converter_source):
447 def __init__(self, ui, path, revs=None):
447 def __init__(self, ui, path, revs=None):
448 common.converter_source.__init__(self, ui, path, revs)
448 common.converter_source.__init__(self, ui, path, revs)
449 self.ignoreerrors = ui.configbool('convert', 'hg.ignoreerrors', False)
449 self.ignoreerrors = ui.configbool('convert', 'hg.ignoreerrors', False)
450 self.ignored = set()
450 self.ignored = set()
451 self.saverev = ui.configbool('convert', 'hg.saverev', False)
451 self.saverev = ui.configbool('convert', 'hg.saverev', False)
452 try:
452 try:
453 self.repo = hg.repository(self.ui, path)
453 self.repo = hg.repository(self.ui, path)
454 # try to provoke an exception if this isn't really a hg
454 # try to provoke an exception if this isn't really a hg
455 # repo, but some other bogus compatible-looking url
455 # repo, but some other bogus compatible-looking url
456 if not self.repo.local():
456 if not self.repo.local():
457 raise error.RepoError
457 raise error.RepoError
458 except error.RepoError:
458 except error.RepoError:
459 ui.traceback()
459 ui.traceback()
460 raise NoRepo(_("%s is not a local Mercurial repository") % path)
460 raise NoRepo(_("%s is not a local Mercurial repository") % path)
461 self.lastrev = None
461 self.lastrev = None
462 self.lastctx = None
462 self.lastctx = None
463 self._changescache = None, None
463 self._changescache = None, None
464 self.convertfp = None
464 self.convertfp = None
465 # Restrict converted revisions to startrev descendants
465 # Restrict converted revisions to startrev descendants
466 startnode = ui.config('convert', 'hg.startrev')
466 startnode = ui.config('convert', 'hg.startrev')
467 hgrevs = ui.config('convert', 'hg.revs')
467 hgrevs = ui.config('convert', 'hg.revs')
468 if hgrevs is None:
468 if hgrevs is None:
469 if startnode is not None:
469 if startnode is not None:
470 try:
470 try:
471 startnode = self.repo.lookup(startnode)
471 startnode = self.repo.lookup(startnode)
472 except error.RepoError:
472 except error.RepoError:
473 raise error.Abort(_('%s is not a valid start revision')
473 raise error.Abort(_('%s is not a valid start revision')
474 % startnode)
474 % startnode)
475 startrev = self.repo.changelog.rev(startnode)
475 startrev = self.repo.changelog.rev(startnode)
476 children = {startnode: 1}
476 children = {startnode: 1}
477 for r in self.repo.changelog.descendants([startrev]):
477 for r in self.repo.changelog.descendants([startrev]):
478 children[self.repo.changelog.node(r)] = 1
478 children[self.repo.changelog.node(r)] = 1
479 self.keep = children.__contains__
479 self.keep = children.__contains__
480 else:
480 else:
481 self.keep = util.always
481 self.keep = util.always
482 if revs:
482 if revs:
483 self._heads = [self.repo[r].node() for r in revs]
483 self._heads = [self.repo[r].node() for r in revs]
484 else:
484 else:
485 self._heads = self.repo.heads()
485 self._heads = self.repo.heads()
486 else:
486 else:
487 if revs or startnode is not None:
487 if revs or startnode is not None:
488 raise error.Abort(_('hg.revs cannot be combined with '
488 raise error.Abort(_('hg.revs cannot be combined with '
489 'hg.startrev or --rev'))
489 'hg.startrev or --rev'))
490 nodes = set()
490 nodes = set()
491 parents = set()
491 parents = set()
492 for r in scmutil.revrange(self.repo, [hgrevs]):
492 for r in scmutil.revrange(self.repo, [hgrevs]):
493 ctx = self.repo[r]
493 ctx = self.repo[r]
494 nodes.add(ctx.node())
494 nodes.add(ctx.node())
495 parents.update(p.node() for p in ctx.parents())
495 parents.update(p.node() for p in ctx.parents())
496 self.keep = nodes.__contains__
496 self.keep = nodes.__contains__
497 self._heads = nodes - parents
497 self._heads = nodes - parents
498
498
499 def _changectx(self, rev):
499 def _changectx(self, rev):
500 if self.lastrev != rev:
500 if self.lastrev != rev:
501 self.lastctx = self.repo[rev]
501 self.lastctx = self.repo[rev]
502 self.lastrev = rev
502 self.lastrev = rev
503 return self.lastctx
503 return self.lastctx
504
504
505 def _parents(self, ctx):
505 def _parents(self, ctx):
506 return [p for p in ctx.parents() if p and self.keep(p.node())]
506 return [p for p in ctx.parents() if p and self.keep(p.node())]
507
507
508 def getheads(self):
508 def getheads(self):
509 return [nodemod.hex(h) for h in self._heads if self.keep(h)]
509 return [nodemod.hex(h) for h in self._heads if self.keep(h)]
510
510
511 def getfile(self, name, rev):
511 def getfile(self, name, rev):
512 try:
512 try:
513 fctx = self._changectx(rev)[name]
513 fctx = self._changectx(rev)[name]
514 return fctx.data(), fctx.flags()
514 return fctx.data(), fctx.flags()
515 except error.LookupError:
515 except error.LookupError:
516 return None, None
516 return None, None
517
517
518 def _changedfiles(self, ctx1, ctx2):
518 def _changedfiles(self, ctx1, ctx2):
519 ma, r = [], []
519 ma, r = [], []
520 maappend = ma.append
520 maappend = ma.append
521 rappend = r.append
521 rappend = r.append
522 d = ctx1.manifest().diff(ctx2.manifest())
522 d = ctx1.manifest().diff(ctx2.manifest())
523 for f, ((node1, flag1), (node2, flag2)) in d.iteritems():
523 for f, ((node1, flag1), (node2, flag2)) in d.iteritems():
524 if node2 is None:
524 if node2 is None:
525 rappend(f)
525 rappend(f)
526 else:
526 else:
527 maappend(f)
527 maappend(f)
528 return ma, r
528 return ma, r
529
529
530 def getchanges(self, rev, full):
530 def getchanges(self, rev, full):
531 ctx = self._changectx(rev)
531 ctx = self._changectx(rev)
532 parents = self._parents(ctx)
532 parents = self._parents(ctx)
533 if full or not parents:
533 if full or not parents:
534 files = copyfiles = ctx.manifest()
534 files = copyfiles = ctx.manifest()
535 if parents:
535 if parents:
536 if self._changescache[0] == rev:
536 if self._changescache[0] == rev:
537 ma, r = self._changescache[1]
537 ma, r = self._changescache[1]
538 else:
538 else:
539 ma, r = self._changedfiles(parents[0], ctx)
539 ma, r = self._changedfiles(parents[0], ctx)
540 if not full:
540 if not full:
541 files = ma + r
541 files = ma + r
542 copyfiles = ma
542 copyfiles = ma
543 # _getcopies() is also run for roots and before filtering so missing
543 # _getcopies() is also run for roots and before filtering so missing
544 # revlogs are detected early
544 # revlogs are detected early
545 copies = self._getcopies(ctx, parents, copyfiles)
545 copies = self._getcopies(ctx, parents, copyfiles)
546 cleanp2 = set()
546 cleanp2 = set()
547 if len(parents) == 2:
547 if len(parents) == 2:
548 d = parents[1].manifest().diff(ctx.manifest(), clean=True)
548 d = parents[1].manifest().diff(ctx.manifest(), clean=True)
549 for f, value in d.iteritems():
549 for f, value in d.iteritems():
550 if value is None:
550 if value is None:
551 cleanp2.add(f)
551 cleanp2.add(f)
552 changes = [(f, rev) for f in files if f not in self.ignored]
552 changes = [(f, rev) for f in files if f not in self.ignored]
553 changes.sort()
553 changes.sort()
554 return changes, copies, cleanp2
554 return changes, copies, cleanp2
555
555
556 def _getcopies(self, ctx, parents, files):
556 def _getcopies(self, ctx, parents, files):
557 copies = {}
557 copies = {}
558 for name in files:
558 for name in files:
559 if name in self.ignored:
559 if name in self.ignored:
560 continue
560 continue
561 try:
561 try:
562 copysource, _copynode = ctx.filectx(name).renamed()
562 copysource, _copynode = ctx.filectx(name).renamed()
563 if copysource in self.ignored:
563 if copysource in self.ignored:
564 continue
564 continue
565 # Ignore copy sources not in parent revisions
565 # Ignore copy sources not in parent revisions
566 found = False
566 found = False
567 for p in parents:
567 for p in parents:
568 if copysource in p:
568 if copysource in p:
569 found = True
569 found = True
570 break
570 break
571 if not found:
571 if not found:
572 continue
572 continue
573 copies[name] = copysource
573 copies[name] = copysource
574 except TypeError:
574 except TypeError:
575 pass
575 pass
576 except error.LookupError as e:
576 except error.LookupError as e:
577 if not self.ignoreerrors:
577 if not self.ignoreerrors:
578 raise
578 raise
579 self.ignored.add(name)
579 self.ignored.add(name)
580 self.ui.warn(_('ignoring: %s\n') % e)
580 self.ui.warn(_('ignoring: %s\n') % e)
581 return copies
581 return copies
582
582
583 def getcommit(self, rev):
583 def getcommit(self, rev):
584 ctx = self._changectx(rev)
584 ctx = self._changectx(rev)
585 parents = [p.hex() for p in self._parents(ctx)]
585 _parents = self._parents(ctx)
586 parents = [p.hex() for p in _parents]
587 optparents = [p.hex() for p in ctx.parents() if p and p not in _parents]
586 crev = rev
588 crev = rev
587
589
588 return common.commit(author=ctx.user(),
590 return common.commit(author=ctx.user(),
589 date=util.datestr(ctx.date(),
591 date=util.datestr(ctx.date(),
590 '%Y-%m-%d %H:%M:%S %1%2'),
592 '%Y-%m-%d %H:%M:%S %1%2'),
591 desc=ctx.description(),
593 desc=ctx.description(),
592 rev=crev,
594 rev=crev,
593 parents=parents,
595 parents=parents,
596 optparents=optparents,
594 branch=ctx.branch(),
597 branch=ctx.branch(),
595 extra=ctx.extra(),
598 extra=ctx.extra(),
596 sortkey=ctx.rev(),
599 sortkey=ctx.rev(),
597 saverev=self.saverev,
600 saverev=self.saverev,
598 phase=ctx.phase())
601 phase=ctx.phase())
599
602
600 def gettags(self):
603 def gettags(self):
601 # This will get written to .hgtags, filter non global tags out.
604 # This will get written to .hgtags, filter non global tags out.
602 tags = [t for t in self.repo.tagslist()
605 tags = [t for t in self.repo.tagslist()
603 if self.repo.tagtype(t[0]) == 'global']
606 if self.repo.tagtype(t[0]) == 'global']
604 return dict([(name, nodemod.hex(node)) for name, node in tags
607 return dict([(name, nodemod.hex(node)) for name, node in tags
605 if self.keep(node)])
608 if self.keep(node)])
606
609
607 def getchangedfiles(self, rev, i):
610 def getchangedfiles(self, rev, i):
608 ctx = self._changectx(rev)
611 ctx = self._changectx(rev)
609 parents = self._parents(ctx)
612 parents = self._parents(ctx)
610 if not parents and i is None:
613 if not parents and i is None:
611 i = 0
614 i = 0
612 ma, r = ctx.manifest().keys(), []
615 ma, r = ctx.manifest().keys(), []
613 else:
616 else:
614 i = i or 0
617 i = i or 0
615 ma, r = self._changedfiles(parents[i], ctx)
618 ma, r = self._changedfiles(parents[i], ctx)
616 ma, r = [[f for f in l if f not in self.ignored] for l in (ma, r)]
619 ma, r = [[f for f in l if f not in self.ignored] for l in (ma, r)]
617
620
618 if i == 0:
621 if i == 0:
619 self._changescache = (rev, (ma, r))
622 self._changescache = (rev, (ma, r))
620
623
621 return ma + r
624 return ma + r
622
625
623 def converted(self, rev, destrev):
626 def converted(self, rev, destrev):
624 if self.convertfp is None:
627 if self.convertfp is None:
625 self.convertfp = open(self.repo.join('shamap'), 'a')
628 self.convertfp = open(self.repo.join('shamap'), 'a')
626 self.convertfp.write('%s %s\n' % (destrev, rev))
629 self.convertfp.write('%s %s\n' % (destrev, rev))
627 self.convertfp.flush()
630 self.convertfp.flush()
628
631
629 def before(self):
632 def before(self):
630 self.ui.debug('run hg source pre-conversion action\n')
633 self.ui.debug('run hg source pre-conversion action\n')
631
634
632 def after(self):
635 def after(self):
633 self.ui.debug('run hg source post-conversion action\n')
636 self.ui.debug('run hg source post-conversion action\n')
634
637
635 def hasnativeorder(self):
638 def hasnativeorder(self):
636 return True
639 return True
637
640
638 def hasnativeclose(self):
641 def hasnativeclose(self):
639 return True
642 return True
640
643
641 def lookuprev(self, rev):
644 def lookuprev(self, rev):
642 try:
645 try:
643 return nodemod.hex(self.repo.lookup(rev))
646 return nodemod.hex(self.repo.lookup(rev))
644 except (error.RepoError, error.LookupError):
647 except (error.RepoError, error.LookupError):
645 return None
648 return None
646
649
647 def getbookmarks(self):
650 def getbookmarks(self):
648 return bookmarks.listbookmarks(self.repo)
651 return bookmarks.listbookmarks(self.repo)
649
652
650 def checkrevformat(self, revstr, mapname='splicemap'):
653 def checkrevformat(self, revstr, mapname='splicemap'):
651 """ Mercurial, revision string is a 40 byte hex """
654 """ Mercurial, revision string is a 40 byte hex """
652 self.checkhexformat(revstr, mapname)
655 self.checkhexformat(revstr, mapname)
@@ -1,245 +1,245 b''
1
1
2 $ cat >> $HGRCPATH <<EOF
2 $ cat >> $HGRCPATH <<EOF
3 > [extensions]
3 > [extensions]
4 > convert =
4 > convert =
5 > [convert]
5 > [convert]
6 > hg.saverev = yes
6 > hg.saverev = yes
7 > EOF
7 > EOF
8
8
9 $ glog()
9 $ glog()
10 > {
10 > {
11 > hg -R "$1" log -G --template '{rev} "{desc}" files: {files}\n'
11 > hg -R "$1" log -G --template '{rev} "{desc}" files: {files}\n'
12 > }
12 > }
13
13
14 $ hg init source
14 $ hg init source
15 $ cd source
15 $ cd source
16
16
17 $ echo a > a
17 $ echo a > a
18 $ echo b > b
18 $ echo b > b
19 $ echo f > f
19 $ echo f > f
20 $ hg ci -d '0 0' -qAm '0: add a b f'
20 $ hg ci -d '0 0' -qAm '0: add a b f'
21 $ echo c > c
21 $ echo c > c
22 $ hg move f d
22 $ hg move f d
23 $ hg ci -d '1 0' -qAm '1: add c, move f to d'
23 $ hg ci -d '1 0' -qAm '1: add c, move f to d'
24 $ hg copy a e
24 $ hg copy a e
25 $ echo b >> b
25 $ echo b >> b
26 $ hg ci -d '2 0' -qAm '2: copy e from a, change b'
26 $ hg ci -d '2 0' -qAm '2: copy e from a, change b'
27 $ hg up -C 0
27 $ hg up -C 0
28 2 files updated, 0 files merged, 3 files removed, 0 files unresolved
28 2 files updated, 0 files merged, 3 files removed, 0 files unresolved
29 $ echo a >> a
29 $ echo a >> a
30 $ hg ci -d '3 0' -qAm '3: change a'
30 $ hg ci -d '3 0' -qAm '3: change a'
31 $ hg merge
31 $ hg merge
32 merging a and e to e
32 merging a and e to e
33 3 files updated, 1 files merged, 1 files removed, 0 files unresolved
33 3 files updated, 1 files merged, 1 files removed, 0 files unresolved
34 (branch merge, don't forget to commit)
34 (branch merge, don't forget to commit)
35 $ hg ci -d '4 0' -qAm '4: merge 2 and 3'
35 $ hg ci -d '4 0' -qAm '4: merge 2 and 3'
36 $ echo a >> a
36 $ echo a >> a
37 $ hg ci -d '5 0' -qAm '5: change a'
37 $ hg ci -d '5 0' -qAm '5: change a'
38 $ cd ..
38 $ cd ..
39
39
40 Convert from null revision
40 Convert from null revision
41
41
42 $ hg convert --config convert.hg.startrev=null source full
42 $ hg convert --config convert.hg.startrev=null source full
43 initializing destination full repository
43 initializing destination full repository
44 scanning source...
44 scanning source...
45 sorting...
45 sorting...
46 converting...
46 converting...
47 5 0: add a b f
47 5 0: add a b f
48 4 1: add c, move f to d
48 4 1: add c, move f to d
49 3 2: copy e from a, change b
49 3 2: copy e from a, change b
50 2 3: change a
50 2 3: change a
51 1 4: merge 2 and 3
51 1 4: merge 2 and 3
52 0 5: change a
52 0 5: change a
53
53
54 $ glog full
54 $ glog full
55 o 5 "5: change a" files: a
55 o 5 "5: change a" files: a
56 |
56 |
57 o 4 "4: merge 2 and 3" files: e f
57 o 4 "4: merge 2 and 3" files: e f
58 |\
58 |\
59 | o 3 "3: change a" files: a
59 | o 3 "3: change a" files: a
60 | |
60 | |
61 o | 2 "2: copy e from a, change b" files: b e
61 o | 2 "2: copy e from a, change b" files: b e
62 | |
62 | |
63 o | 1 "1: add c, move f to d" files: c d f
63 o | 1 "1: add c, move f to d" files: c d f
64 |/
64 |/
65 o 0 "0: add a b f" files: a b f
65 o 0 "0: add a b f" files: a b f
66
66
67 $ rm -Rf full
67 $ rm -Rf full
68
68
69 Convert from zero revision
69 Convert from zero revision
70
70
71 $ hg convert --config convert.hg.startrev=0 source full
71 $ hg convert --config convert.hg.startrev=0 source full
72 initializing destination full repository
72 initializing destination full repository
73 scanning source...
73 scanning source...
74 sorting...
74 sorting...
75 converting...
75 converting...
76 5 0: add a b f
76 5 0: add a b f
77 4 1: add c, move f to d
77 4 1: add c, move f to d
78 3 2: copy e from a, change b
78 3 2: copy e from a, change b
79 2 3: change a
79 2 3: change a
80 1 4: merge 2 and 3
80 1 4: merge 2 and 3
81 0 5: change a
81 0 5: change a
82
82
83 $ glog full
83 $ glog full
84 o 5 "5: change a" files: a
84 o 5 "5: change a" files: a
85 |
85 |
86 o 4 "4: merge 2 and 3" files: e f
86 o 4 "4: merge 2 and 3" files: e f
87 |\
87 |\
88 | o 3 "3: change a" files: a
88 | o 3 "3: change a" files: a
89 | |
89 | |
90 o | 2 "2: copy e from a, change b" files: b e
90 o | 2 "2: copy e from a, change b" files: b e
91 | |
91 | |
92 o | 1 "1: add c, move f to d" files: c d f
92 o | 1 "1: add c, move f to d" files: c d f
93 |/
93 |/
94 o 0 "0: add a b f" files: a b f
94 o 0 "0: add a b f" files: a b f
95
95
96 Convert from merge parent
96 Convert from merge parent
97
97
98 $ hg convert --config convert.hg.startrev=1 source conv1
98 $ hg convert --config convert.hg.startrev=1 source conv1
99 initializing destination conv1 repository
99 initializing destination conv1 repository
100 scanning source...
100 scanning source...
101 sorting...
101 sorting...
102 converting...
102 converting...
103 3 1: add c, move f to d
103 3 1: add c, move f to d
104 2 2: copy e from a, change b
104 2 2: copy e from a, change b
105 1 4: merge 2 and 3
105 1 4: merge 2 and 3
106 0 5: change a
106 0 5: change a
107
107
108 $ glog conv1
108 $ glog conv1
109 o 3 "5: change a" files: a
109 o 3 "5: change a" files: a
110 |
110 |
111 o 2 "4: merge 2 and 3" files: a e
111 o 2 "4: merge 2 and 3" files: a e
112 |
112 |
113 o 1 "2: copy e from a, change b" files: b e
113 o 1 "2: copy e from a, change b" files: b e
114 |
114 |
115 o 0 "1: add c, move f to d" files: a b c d
115 o 0 "1: add c, move f to d" files: a b c d
116
116
117 $ cd conv1
117 $ cd conv1
118 $ hg up -q
118 $ hg up -q
119
119
120 Check copy preservation
120 Check copy preservation
121
121
122 $ hg st -C --change 2 e
122 $ hg st -C --change 2 e
123 M e
123 M e
124 $ hg st -C --change 1 e
124 $ hg st -C --change 1 e
125 A e
125 A e
126 a
126 a
127 $ hg st -C --change 0 a
127 $ hg st -C --change 0 a
128 A a
128 A a
129
129
130 (It seems like a bug in log that the following doesn't show rev 1.)
130 (It seems like a bug in log that the following doesn't show rev 1.)
131
131
132 $ hg log --follow --copies e
132 $ hg log --follow --copies e
133 changeset: 2:82bbac3d2cf4
133 changeset: 2:82bbac3d2cf4
134 user: test
134 user: test
135 date: Thu Jan 01 00:00:04 1970 +0000
135 date: Thu Jan 01 00:00:04 1970 +0000
136 summary: 4: merge 2 and 3
136 summary: 4: merge 2 and 3
137
137
138 changeset: 0:23c3be426dce
138 changeset: 0:23c3be426dce
139 user: test
139 user: test
140 date: Thu Jan 01 00:00:01 1970 +0000
140 date: Thu Jan 01 00:00:01 1970 +0000
141 summary: 1: add c, move f to d
141 summary: 1: add c, move f to d
142
142
143 Check copy removal on missing parent
143 Check copy removal on missing parent
144
144
145 $ hg log --follow --copies d
145 $ hg log --follow --copies d
146 changeset: 0:23c3be426dce
146 changeset: 0:23c3be426dce
147 user: test
147 user: test
148 date: Thu Jan 01 00:00:01 1970 +0000
148 date: Thu Jan 01 00:00:01 1970 +0000
149 summary: 1: add c, move f to d
149 summary: 1: add c, move f to d
150
150
151 $ hg cat -r tip a b
151 $ hg cat -r tip a b
152 a
152 a
153 a
153 a
154 a
154 a
155 b
155 b
156 b
156 b
157 $ hg -q verify
157 $ hg -q verify
158 $ cd ..
158 $ cd ..
159
159
160 Convert from merge
160 Convert from merge
161
161
162 $ hg convert --config convert.hg.startrev=4 source conv4
162 $ hg convert --config convert.hg.startrev=4 source conv4
163 initializing destination conv4 repository
163 initializing destination conv4 repository
164 scanning source...
164 scanning source...
165 sorting...
165 sorting...
166 converting...
166 converting...
167 1 4: merge 2 and 3
167 1 4: merge 2 and 3
168 0 5: change a
168 0 5: change a
169 $ glog conv4
169 $ glog conv4
170 o 1 "5: change a" files: a
170 o 1 "5: change a" files: a
171 |
171 |
172 o 0 "4: merge 2 and 3" files: a b c d e
172 o 0 "4: merge 2 and 3" files: a b c d e
173
173
174 $ cd conv4
174 $ cd conv4
175 $ hg up -C
175 $ hg up -C
176 5 files updated, 0 files merged, 0 files removed, 0 files unresolved
176 5 files updated, 0 files merged, 0 files removed, 0 files unresolved
177 $ hg cat -r tip a b
177 $ hg cat -r tip a b
178 a
178 a
179 a
179 a
180 a
180 a
181 b
181 b
182 b
182 b
183 $ hg -q verify
183 $ hg -q verify
184 $ cd ..
184 $ cd ..
185
185
186 Convert from revset in convert.hg.revs
186 Convert from revset in convert.hg.revs
187
187
188 $ hg convert --config convert.hg.revs='3:4+0' source revsetrepo
188 $ hg convert --config convert.hg.revs='3:4+0' source revsetrepo
189 initializing destination revsetrepo repository
189 initializing destination revsetrepo repository
190 scanning source...
190 scanning source...
191 sorting...
191 sorting...
192 converting...
192 converting...
193 2 0: add a b f
193 2 0: add a b f
194 1 3: change a
194 1 3: change a
195 0 4: merge 2 and 3
195 0 4: merge 2 and 3
196
196
197 $ glog revsetrepo
197 $ glog revsetrepo
198 o 2 "4: merge 2 and 3" files: b c d e f
198 o 2 "4: merge 2 and 3" files: b c d e f
199 |
199 |
200 o 1 "3: change a" files: a
200 o 1 "3: change a" files: a
201 |
201 |
202 o 0 "0: add a b f" files: a b f
202 o 0 "0: add a b f" files: a b f
203
203
204 Convert from specified revs
204 Convert from specified revs
205
205
206 $ hg convert --rev 3 --rev 2 source multiplerevs
206 $ hg convert --rev 3 --rev 2 source multiplerevs
207 initializing destination multiplerevs repository
207 initializing destination multiplerevs repository
208 scanning source...
208 scanning source...
209 sorting...
209 sorting...
210 converting...
210 converting...
211 3 0: add a b f
211 3 0: add a b f
212 2 1: add c, move f to d
212 2 1: add c, move f to d
213 1 2: copy e from a, change b
213 1 2: copy e from a, change b
214 0 3: change a
214 0 3: change a
215 $ glog multiplerevs
215 $ glog multiplerevs
216 o 3 "3: change a" files: a
216 o 3 "3: change a" files: a
217 |
217 |
218 | o 2 "2: copy e from a, change b" files: b e
218 | o 2 "2: copy e from a, change b" files: b e
219 | |
219 | |
220 | o 1 "1: add c, move f to d" files: c d f
220 | o 1 "1: add c, move f to d" files: c d f
221 |/
221 |/
222 o 0 "0: add a b f" files: a b f
222 o 0 "0: add a b f" files: a b f
223
223
224 Convert in multiple steps that doesn't overlap - the link to the parent is
224 Convert in multiple steps that doesn't overlap - the link to the parent is
225 currently missing
225 preserved anyway
226
226
227 $ hg convert --config convert.hg.revs=::1 source multistep
227 $ hg convert --config convert.hg.revs=::1 source multistep
228 initializing destination multistep repository
228 initializing destination multistep repository
229 scanning source...
229 scanning source...
230 sorting...
230 sorting...
231 converting...
231 converting...
232 1 0: add a b f
232 1 0: add a b f
233 0 1: add c, move f to d
233 0 1: add c, move f to d
234 $ hg convert --config convert.hg.revs=2 source multistep
234 $ hg convert --config convert.hg.revs=2 source multistep
235 scanning source...
235 scanning source...
236 sorting...
236 sorting...
237 converting...
237 converting...
238 0 2: copy e from a, change b
238 0 2: copy e from a, change b
239 $ glog multistep
239 $ glog multistep
240 o 2 "2: copy e from a, change b" files: a b c d e
240 o 2 "2: copy e from a, change b" files: b e
241
241 |
242 o 1 "1: add c, move f to d" files: c d f
242 o 1 "1: add c, move f to d" files: c d f
243 |
243 |
244 o 0 "0: add a b f" files: a b f
244 o 0 "0: add a b f" files: a b f
245
245
General Comments 0
You need to be logged in to leave comments. Login now